4小时开发一个SpringBoot+vue前后端分离博客项目!!


系列文章:



Java后端开发

1、新建Springboot项目

第一步 创建项目(第八步骤就行)+数据库:

pom的jar包导入如下:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--        devtools:项目的热加载重启插件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
<!--        lombok:简化代码的工具-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--mp-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--mp代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.2.0</version>
        </dependency>

    </dependencies>

devtools:项目的热加载重启插件
lombok:简化代码的工具

2、整合mybatis plus

第一步:导入jar包
第二步:然后去写配置文件(将appllication.properties修改文application.yml)

# DataSource Config
spring:
    datasource:
#        一下是数据库的连接信息,需要根据自己的进行修改
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
        
#        这个暂时不理解
mybatis-plus:
    mapper-locations: classpath*:/mapper/**Mapper.xml

#    访问网页端口号
server:
              port: 8081

上面除了配置数据库的信息,还配置了myabtis plus的mapper的xml文件的扫描路径,这一步不要忘记了。
第三步:开启mapper接口扫描,添加分页插件
新建一个包:通过@mapperScan注解指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类。PaginationInterceptor是一个分页插件。

  • com.song.mywj.config.MybatisPlusConfig
@Configuration
@EnableTransactionManagement
//
@MapperScan("com.song.mywj.mapper")
public class MybatisPlusConfig {
    
    
    @Bean
    public PaginationInterceptor paginationInterceptor() {
    
    
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        return paginationInterceptor;
    }
}

@Mapper注解:
作用:在接口类上添加@Mapper,在编译之后会生成相应的接口实现类
添加位置:接口类上面
例:

@Mapper
public interface UserMapper {

    User selectById(int id);
....一系列方法
    int updatePassword(int id, String password);

}

如果想要每个接口都变成实现类,那么在每个接口类上加上@Mapper, 比较麻烦,解决这个问题引入 @MapperScan
@MapperScan:
MapperScan注解详解

第四步:代码生成

如果你没再用其他插件,那么现在就已经可以使用mybatis plus了,官方给我们提供了一个代码生成器,然后我写上自己的参数之后,就可以直接根据数据库表信息生成entity、service、mapper等接口和实现类。

  • com.song.mywj.CodeGenerator

创建数据库vueblog然后执行下面命令生成表

DROP TABLE IF EXISTS `m_blog`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
 SET character_set_client = utf8mb4 ;
CREATE TABLE `m_blog` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `title` varchar(255) NOT NULL,
  `description` varchar(255) NOT NULL,
  `content` longtext,
  `created` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  `status` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */; 

DROP TABLE IF EXISTS `m_user`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
 SET character_set_client = utf8mb4 ;
CREATE TABLE `m_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(64) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL,
  `status` int(5) NOT NULL,
  `created` datetime DEFAULT NULL,
  `last_login` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; 

INSERT INTO `vueblog`.`m_user` (`id`, `username`, `avatar`, `email`, `password`, `status`, `created`, `last_login`) VALUES ('1', 'markerhub', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', NULL, '96e79218965eb72c92a549dd5a330112', '0', '2020-04-20 10:44:01', NULL);

package com.song.mywj;
/**
 * @Author: YTM
 * @Description
 * @Date:Created in 21:04 2022/7/21
 * @Modified by:
 */

import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 *
 */
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        // gc.setOutputDir("D:\\test");
        gc.setAuthor("yuantianmeng");
        gc.setOpen(false);
        // gc.setSwagger2(true); 实体属性 Swagger2 注解
        gc.setServiceName("%sService");
        mpg.setGlobalConfig(gc);

        // 数据源配置 数据库名 账号密码
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("123456");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setModuleName(null);
//      将来生成文件(如XXXService)的所在的包
        pc.setParent("com.song.mywj");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!
                return projectPath + "/src/main/resources/mapper/"
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });

        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("m_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

}

在UserController中写个测试:

@RestController
@RequestMapping("/user")
public class UserController {
    
    
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public Object test(@PathVariable("id")Long id) {
    
    
        return userService.getById(id);
    }
}

访问 http://localhost:8081/user/1
结果如下,8081是根据application.yml中的8081确定,若写的8080z则访问8080

server:
              port: 8081

在这里插入图片描述

3、统一结果封装

这里我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的

  • 是否成功,可用code表示(如200表示成功,400表示异常)
  • 结果消息
  • 结果数据
package com.song.mywj.common.lang;

import lombok.Data;

import java.io.Serializable;

@Data
public class Result implements Serializable {
    
     //序列化
    private int code; //200是正常 400表示异常
    private String msg;
    private Object data;//返回数据
    //成功
    public static Result succ( Object data){
    
    

        return succ(200,"操作成功",data);
    }
    //成功
    public static Result succ(int code,String msg,Object data){
    
    
        Result r = new Result();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }
    //失败
    public static Result fail(String msg){
    
    

        return fail(400,msg,null);
    }
    //失败
    public static Result fail(String msg,Object data){
    
    

        return fail(400,msg,data);
    }
    //失败
    public static Result fail(int code,String msg,Object data){
    
    
        Result r = new Result();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }
}

自己生成get,set方法
import lombok.Data; 用来简化代码的工具

整合shiro+jwt,并会话共享

Shiro+JWT超详解

考虑到后面可能需要做集群、负载均衡等,所以就需要会话共享,而shiro的缓存和会话信息,我们一般考虑使用redis来存储这些数据,所以,我们不仅仅需要整合shiro,同时也需要整合redis。在开源的项目中,我们找到了一个starter可以快速整合shiro-redis,配置简单,这里也推荐大家使用。
而因为我们需要做的是前后端分离项目的骨架,所以一般我们会采用token或者jwt作为跨域身份验证解决方案。所以整合shiro的过程中,我们需要引入jwt的身份验证过程。
那么我们就开始整合:
我们使用一个shiro-redis-spring-boot-starter的jar包,具体教程可以看官方文档:官方文档

第一步:导入shiro-redis的starter包:还有jwt的工具包,以及为了简化开发,我引入了hutool工具包。
pom.xml中导入:

  		 <!-- shiro-redis -->
      <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis-spring-boot-starter</artifactId>
            <version>3.2.1</version>
        </dependency> 
		 <!-- hutool工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>
            <!-- jwt 生成工具 校验工具-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

创建ShiroConfig
包:com.song.mywj.config

6实体校验

当我们表单数据提交的时候,前端的校验我们可以使用一些类似于jQuery Validate等js插件实现,而后端我们可以使用Hibernate validatior来做校验。
我们使用springboot框架作为基础,那么就已经自动集成了Hibernate validatior。
那么用起来啥样子的呢?
第一步:首先在实体的属性上添加对应的校验规则,比如:

<!--使用Hibernate validatior来做后端数据校验-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>2.7.0</version>
        </dependency>
导包是javax开头的
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@TableName("m_user")
public class User implements Serializable {
    
    
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @NotBlank(message = "昵称不能为空")
    private String username;
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
}

SpringBoot中使用@NotBlank等校验注解

第二步 :这里我们使用@Validated注解方式,如果实体不符合要求,系统会抛出异常,那么我们的异常处理中就捕获到MethodArgumentNotValidException。

com.xxx.controller.UserController

/**
 * 测试实体校验
 * @param user
 * @return
 */
    @PostMapping("/save")
    public Result testUser(@Validated @RequestBody User user) {
    
    
        return Result.succ(user);
    }

异常处理

    /**
     * @Validated 校验错误异常处理
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) throws IOException {
    
    
        log.error("实体校验异常:-------------->",e);
//        当错误很多时候,我们先返回第一个异常
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.fail(objectError.getDefaultMessage());

    }

postman测试
在这里插入图片描述

7跨域问题

因为是前后端分析,所以跨域问题是避免不了的,我们直接在后台进行全局跨域处理:
com.song.mywj.config.CorsConfig

/**
 * 解决跨域问题
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    
    
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

When allowCredentials is true, allowedOrigins cannot contain the special value “*“ since that cannot
com.song.mywj.shiro.JwtFilter

    /**
     * 对跨域提供支持
     */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    
    
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

ok,因为我们系统开发的接口比较简单,所以我就不集成swagger2啦,也比较简单而已。下面我们就直接进入我们的正题,进行编写登录接口。

8、登录接口开发

登录的逻辑其实很简答,只需要接受账号密码,然后把用户的id生成jwt,返回给前段,为了后续的jwt的延期,所以我们把jwt放在header上。具体代码如下:
com.song.mywj.dto

Data
public class LoginDto implements Serializable {
    
    
    @NotBlank(message="昵称不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    private String password;

}

@RestController
public class AccountController {
    
    
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    UserService userService;
    /**
     * 默认账号密码:markerhub / 111111
     *
     */
    @CrossOrigin
    @PostMapping("/login")
    public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response) {
    
    
        User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
        Assert.notNull(user, "用户不存在");
        if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) {
    
    
            return Result.fail("密码错误!");
        }
//        密码正确生成jwt
        String jwt = jwtUtils.generateToken(user.getId());
        response.setHeader("Authorization", jwt);
        response.setHeader("Access-Control-Expose-Headers", "Authorization");
        // 用户可以另一个接口
        return Result.succ(MapUtil.builder()
                                  .put("id", user.getId())
                                  .put("username", user.getUsername())
                                  .put("avatar", user.getAvatar())
                                  .put("email", user.getEmail())
                                  .map()
        );
    }

    // 退出
    @GetMapping("/logout")
    @RequiresAuthentication
    public Result logout() {
    
    
//        退出有shiro
        SecurityUtils.getSubject().logout();
        return Result.succ(null);
    }
}

在这里插入图片描述

dto

后端开发经常见到一些包:如dto、vo、entity
entity:表示对数据库中所有表的映射,是根据数据库字段设计出来的实体(要求表名与类名相同,字段名和成员变量名相同)
vo:表示前端页面传过来的如表单等数据的字段,比如前端填写了一个表单,当前端传过来的数据比较多时,可以创建一个vo实体类,把前端传来的数据字段名作为成员变量名,这样我们可以用@RequestBody注解快速获取参数内容,而不需要使用Request对象来一个个获取,方便开发
dto:表示vo和entity的一个中间转换对象,是vo或entity对象中属性的一个子对象。当前端传来vo数据,我们提取vo中的数据到dto,在将dto的数据处理后全部移动到entity中进行数据的保存。

9、博客接口开发

我们的骨架已经完成,接下来,我们就可以添加我们的业务接口了,下面我以一个简单的博客列表、博客详情页为例子开发:
com.song.mywj.controller

public class BlogController {
    @Autowired
    BlogService blogService;
    @GetMapping("/blogs")
    public Result blogs(Integer currentPage) {
        if(currentPage == null || currentPage < 1) currentPage = 1;
        Page page = new Page(currentPage, 5);
        IPage pageData = blogService.page(page,
                new QueryWrapper<Blog>().orderByDesc("created"));
        return Result.succ(pageData);
    }
    @GetMapping("/blog/{id}")
    public Result detail(@PathVariable(name = "id") Long id) {
        Blog blog = blogService.getById(id);
        Assert.notNull(blog, "该博客已删除!");
        return Result.succ(blog);
    }

    @RequiresAuthentication
    @PostMapping("/blog/edit")
    public Result edit(@Validated @RequestBody Blog blog) {
        System.out.println(blog.toString());
        Blog temp = null;
//        编辑
        if(blog.getId() != null) {
            temp = blogService.getById(blog.getId());
//            只能编辑自己文章
            Assert.isTrue(temp.getUserId().longValue() == ShiroUtil.getProfile().getId().longValue(), "没有权限编辑");
        } else {
//            添加博客
            temp = new Blog();
            temp.setUserId(ShiroUtil.getProfile().getId());
            temp.setCreated(LocalDateTime.now());
            temp.setStatus(0);
        }
        BeanUtil.copyProperties(blog, temp, "id", "userId", "created", "status");
        blogService.saveOrUpdate(temp);

        return Result.succ("操作成功", null);

    }
}

注意@RequiresAuthentication说明需要登录之后才能访问的接口,其他需要权限的接口可以添加shiro的相关注解。 接口比较简单,我们就不多说了,基本增删改查而已。注意的是edit方法是需要登录才能操作的受限资源。

前端

7、页面路由

接下来,我们先定义好路由和页面,因为我们只是做一个简单的博客项目,页面比较少,所以我们可以直接先定义好,然后在慢慢开发,这样需要用到链接的地方我们就可以直接可以使用:
我们在views文件夹下定义几个页面:

BlogDetail.vue(博客详情页)
BlogEdit.vue(编辑博客)
Blogs.vue(博客列表)
Login.vue(登录页面)

然后再路由中心配置:

router\index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import BlogDetail from '../views/BlogDetail.vue'
import BlogEdit from '../views/BlogEdit.vue'
Vue.use(VueRouter)
const routes = [
  {
    
    
    path: '/',
    name: 'Index',
    redirect: {
    
     name: 'Blogs' }
  },
  {
    
    
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    
    
    path: '/blogs',
    name: 'Blogs',
    // 懒加载
    component: () => import('../views/Blogs.vue')
  },
  {
    
    
    path: '/blog/add', // 注意放在 path: '/blog/:blogId'之前
    name: 'BlogAdd',
    meta: {
    
    
      requireAuth: true
    },
    component: BlogEdit
  },
  {
    
    
    path: '/blog/:blogId',
    name: 'BlogDetail',
    component: BlogDetail
  },
  {
    
    
    path: '/blog/:blogId/edit',
    name: 'BlogEdit',
    meta: {
    
    
      requireAuth: true
    },
    component: BlogEdit
  }
];
const router = new VueRouter({
    
    
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})
export default router

接下来我们去开发我们的页面。其中,带有meta:requireAuth: true说明是需要登录字后才能访问的受限资源,后面我们路由权限拦截时候会用到。
在这里插入图片描述
在这里插入图片描述

8、登录页面

element ui表单字段解释

在这里插入图片描述

<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">

:rules=“rules” 表单验证的规则是 rules
:model=“ruleForm” :model表示绑定数据,数据的来源就是data(){ return { ruleForm中数据}}
ref=“ruleForm” 是为了下面的表单验证,也就是说表单验证的数据是来自 ruleForm

  <el-form-item label="活动名称" prop="name">
    <el-input v-model="ruleForm.name"></el-input>
  </el-form-item>

prop=“name” 进行表单验证,验证的规则就是 data() {rules:{ name:[]} }
v-model=“ruleForm.name” 绑定数据, input显示的数据就是ruleForm.name
formName是form的ref属性 //
在这里插入图片描述

      submitForm(formName) {
    
    
        const _this = this
        this.$refs[formName].validate((valid) => {
    
    
          if (valid) {
    
    
            // 提交逻辑
            this.$axios.post('http://localhost:8081/login', this.ruleForm).then((res) => {
    
    
              const token = res.headers['authorization']
              _this.$store.commit('SET_TOKEN', token)
              _this.$store.commit('SET_USERINFO', res.data.data)
              _this.$router.push("/blogs")
            })
          } else {
    
    
            console.log('error submit!!');
            return false;
          }
        });
      }

如果 this. r e f s [ f o r m N a m e ] . v a l i d a t e ( ) 方式不识别。需要使用 : t h i s . refs[formName].validate() 方式不识别。需要使用: this. refs[formName].validate()方式不识别。需要使用:this.refs.formName.validate()

接下来,我们来搞一个登陆页面,表单组件我们直接在element-ui的官网上找就行了,登陆页面就两个输入框和一个提交按钮,相对简单,然后我们最好带页面的js校验。emmm,我直接贴代码了~~

views/Login.vue

<template>
  <div>
    <el-container>
      <el-header>
        <router-link to="/blogs">
        <img src="https://www.markerhub.com/dist/images/logo/markerhub-logo.png"
             style="height: 60%; margin-top: 10px;">
        </router-link>
      </el-header>
      <el-main>
        <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px"
                 class="demo-ruleForm">
          <el-form-item label="用户名" prop="username">
            <el-input type="text" maxlength="12" v-model="ruleForm.username"></el-input>
          </el-form-item>
          <el-form-item label="密码" prop="password">
            <el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="submitForm('ruleForm')">登录</el-button>
            <el-button @click="resetForm('ruleForm')">重置</el-button>
          </el-form-item>
        </el-form>
      </el-main>
    </el-container>
  </div>
</template>
<script>
  export default {
    
    
    name: 'Login',
    data() {
    
    
      var validatePass = (rule, value, callback) => {
    
    
        if (value === '') {
    
    
          callback(new Error('请输入密码'));
        } else {
    
    
          callback();
        }
      };
      return {
    
    
        ruleForm: {
    
    
          password: '111111',
          username: 'markerhub'
        },
        rules: {
    
    
          password: [
            {
    
    validator: validatePass, trigger: 'blur'}
          ],
          username: [
            {
    
    required: true, message: '请输入用户名', trigger: 'blur'},
            {
    
    min: 3, max: 12, message: '长度在 3 到 12 个字符', trigger: 'blur'}
          ]
        }
      };
    },
    methods: {
    
    
      submitForm(formName) {
    
    
        const _this = this
        this.$refs[formName].validate((valid) => {
    
    
          if (valid) {
    
    
            // 提交逻辑
            this.$axios.post('http://localhost:8081/login', this.ruleForm).then((res)=>{
    
    
              const token = res.headers['authorization']
              _this.$store.commit('SET_TOKEN', token)
              _this.$store.commit('SET_USERINFO', res.data.data)
              _this.$router.push("/blogs")
            })
          } else {
    
    
            console.log('error submit!!');
            return false;
          }
        });
      },
      resetForm(formName) {
    
    
        this.$refs[formName].resetFields();
      }
    },
    mounted() {
    
    
      this.$notify({
    
    
        title: '看这里:',
        message: '关注公众号:MarkerHub,回复【vueblog】,领取项目资料与源码',
        duration: 1500
      });
    }
  }
</script>

找不到啥好的方式讲解了,之后先贴代码,然后再讲解。 上面代码中,其实主要做了两件事情

1、表单校验

2、登录按钮的点击登录事件

表单校验规则还好,比较固定写法,查一下element-ui的组件就知道了,我们来分析一下发起登录之后的代码:

// 结果中获取token
const token = res.headers['authorization']
// 通过commit接口调用'SET_TOKEN'方法,实参token
_this.$store.commit('SET_TOKEN', token)
_this.$store.commit('SET_USERINFO', res.data.data)
_this.$router.push("/blogs")

从返回的结果请求头中获取到token的信息,然后使用store提交token和用户信息的状态。完成操作之后,我们调整到了/blogs路由,即博客列表页面。
有时候我们需要各个component共享数据,这时候数据就存放在store/index.js中,数据改变是,各个组件中的数据同步变化。

token的状态同步

所以在store/index.js中,代码是这样的:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
    
    
// 相当于属性
  state: {
    
    
  // 属性初始化
  // token是个字符串,初始化为空 ''
    token: '',
    // userInfo是对象,对象不能直接存储,存的是序列化,解析时需要反序列化
    userInfo: JSON.parse(sessionStorage.getItem("userInfo"))
  },
  mutations: {
    
    
  // set方法
    SET_TOKEN: (state, token) => {
    
    
      state.token = token
      localStorage.setItem("token", token)
    },
    SET_USERINFO: (state, userInfo) => {
    
    
      state.userInfo = userInfo
      // 序列化存储对象
      sessionStorage.setItem("userInfo", JSON.stringify(userInfo))
    },
    REMOVE_INFO: (state) => {
    
    
    // 定义清空方法
      localStorage.setItem("token", '')
      sessionStorage.setItem("userInfo", JSON.stringify(''))
      state.userInfo = {
    
    }
    }
  },
  getters: {
    
    
    getUser: state => {
    
    
      return state.userInfo
    }
  },
  actions: {
    
    },
  modules: {
    
    }
})

存储token,我们用的是localStorage,存储用户信息,我们用的是sessionStorage。毕竟用户信息我们不需要长久保存,保存了token信息,我们随时都可以初始化用户信息。当然了因为本项目是个比较简单的项目,考虑到初学者,所以很多相对复杂的封装和功能我没有做

定义全局axios拦截器

点击登录按钮发起登录请求,成功时候返回了数据,如果是密码错误,我们是不是也应该弹窗消息提示。为了让这个错误弹窗能运用到所有的地方,所以我对axios做了个后置拦截器,就是返回数据时候,如果结果的code或者status不正常,那么我对应弹窗提示。
在src目录下创建一个文件axios.js(与main.js同级),定义axios的拦截:

import axios from 'axios'
import Element from "element-ui";
import store from "./store";
import router from "./router";
axios.defaults.baseURL='http://localhost:8081'
axios.interceptors.request.use(config => {
    
    
  console.log("前置拦截")
  // 可以统一设置请求头
  return config
})
axios.interceptors.response.use(response => {
    
    
    const res = response.data;
    console.log("后置拦截")
    // 当结果的code是否为200的情况
    if (res.code === 200) {
    
    
      return response
    } else {
    
    
      // 弹窗异常信息
      Element.Message({
    
    
        message: response.data.msg,
        type: 'error',
        duration: 2 * 1000
      })
      // 直接拒绝往下面返回结果信息
      return Promise.reject(response.data.msg)
    }
  },
  error => {
    
    
    console.log('err' + error)// for debug
    if(error.response.data) {
    
    
      error.message = error.response.data.msg
    }
    // 根据请求状态觉得是否登录或者提示其他
    if (error.response.status === 401) {
    
    
      store.commit('REMOVE_INFO');
      router.push({
    
    
        path: '/login'
      });
      error.message = '请重新登录';
    }
    if (error.response.status === 403) {
    
    
      error.message = '权限不足,无法访问';
    }
    Element.Message({
    
    
      message: error.message,
      type: 'error',
      duration: 3 * 1000
    })
    return Promise.reject(error)
  })

前置拦截,其实可以统一为所有需要权限的请求装配上header的token信息,这样不需要在使用是再配置,我的小项目比较小,所以,还是免了吧~

然后再main.js中导入axios.js

import './axios.js' // 请求拦截

后端因为返回的实体是Result,succ时候code为200,fail时候返回的是400,所以可以根据这里判断结果是否是正常的。另外权限不足时候可以通过请求结果的状态码来判断结果是否正常。这里都做了简单的处理。

9、博客列表

登录完成之后直接进入博客列表页面,然后加载博客列表的数据渲染出来。同时页面头部我们需要把用户的信息展示出来,因为很多地方都用到这个模块,所以我们把页面头部的用户信息单独抽取出来作为一个组件。

头部用户信息

那么,我们先来完成头部的用户信息,应该包含三部分信息:id,头像、用户名,而这些信息我们是在登录之后就已经存在了sessionStorage。因此,我们可以通过store的getters获取到用户信息。
看起来不是很复杂,我们贴出代码:
components\Header.vue

<template>
  <div class="m-content">
    <h3>欢迎来到MarkerHub的博客</h3>
    <div class="block">
      <el-avatar :size="50" :src="user.avatar"></el-avatar>
      <div>{
    
    {
    
     user.username }}</div>
    </div>
    <div class="maction">
      <el-link href="/blogs">主页</el-link>
      <el-divider direction="vertical"></el-divider>
      <span>
          <el-link type="success" href="/blog/add" :disabled="!hasLogin">发表文章</el-link>
        </span>
      <el-divider direction="vertical"></el-divider>
      <span v-show="!hasLogin">
          <el-link type="primary" href="/login">登陆</el-link>
        </span>
      <span v-show="hasLogin">
          <el-link type="danger" @click="logout">退出</el-link>
        </span>
    </div>
  </div>
</template>
<script>
  export default {
    
    
    name: "Header",
    data() {
    
    
      return {
    
    
        hasLogin: false,
        user: {
    
    
          username: '请先登录',
          avatar: "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
        },
        blogs: {
    
    },
        currentPage: 1,
        total: 0
      }
    },
    methods: {
    
    
      logout() {
    
    
        const _this = this
        this.$axios.get('http://localhost:8081/logout', {
    
    
          headers: {
    
    
            "Authorization": localStorage.getItem("token")
          }
        }).then((res) => {
    
    
          _this.$store.commit('REMOVE_INFO')
          _this.$router.push('/login')
        });
      }
    },
    created() {
    
    
      if(this.$store.getters.getUser.username) {
    
    
        this.user.username = this.$store.getters.getUser.username
        this.user.avatar = this.$store.getters.getUser.avatar
        this.hasLogin = true
      }
    }
  }
</script>

上面代码created()中初始化用户的信息,通过hasLogin的状态来控制登录和退出按钮的切换,以及发表文章链接的disabled,这样用户的信息就能展示出来了。
然后这里有个退出按钮,在methods中有个logout()方法,逻辑比较简单,直接访问/logout,因为之前axios.js中我们已经设置axios请求的baseURL,所以这里我们不再需要链接的前缀了哈。因为是登录之后才能访问的受限资源,所以在header中带上了Authorization。返回结果清楚store中的用户信息和token信息,跳转到登录页面。

然后需要头部用户信息的页面只需要几个步骤:

import Header from "@/components/Header";
data() {
    
    
  components: {
    
    Header}
}
# 然后模板中调用组件
<Header></Header>

如在Blogs.vue中使用Header组件

<template>
    <div>
<!--        使用Header组件-->
        <Header></Header>
    </div>
</template>

<script>
<!--    导入-->
    import Header from "../components/Header"
    export default {
    
    
        name: "Blogs.vue",
        //备注我们要使用header组件
        components: {
    
    Header}
    }
</script>

<style scoped>
  .m-content{
    
    
    max-width: 900px;
    margin: 0 auto;
    text-align: center;
  }
    .maction{
    
    
    margin: 10px 0;
  }
</style>

猜你喜欢

转载自blog.csdn.net/weixin_42382758/article/details/125921400