系列文章:
文章目录
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的缓存和会话信息,我们一般考虑使用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;
}
第二步 :这里我们使用@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>