文章目录
前言
全栈SpringBoot项目
一、校验
1.1 依赖
<!-- 验证起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
1.2 类上增加注解
类上增加了@Validated
,方法参数上增加了@Pattern(regexp = "^\\S{5,16}$")
,标识5到16位,否则抛出异常
@Validated
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/register")
public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password){
return loginService.register(username, password);
}
}
以上方式抛出的异常,返回前端不是格式统一的结果需要全局异常处理
二、全局异常处理
1.定义全局异常处理类
package com.web.springbootall.exception;
import com.web.springbootall.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e){
log.error("系统异常:{}",e.getMessage(), e);
return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "系统异常");
}
}
三. 返回字段忽略(比如密码等敏感字段)
需要忽略的返回字段增加@JsonIgnore
//对象变为JSON时,不设置该字段
@JsonIgnore
private String password;//密码
四. 使用ThreadLocal存储用户登陆信息
4.1. 定义ThreadLocal工具类
这里提供了设置,获取,清除方法
package com.web.springbootall.util;
public class ThreadLocalUtil {
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
public static void set(Object value) {
THREAD_LOCAL.set(value);
}
public static <T> T get() {
return (T)THREAD_LOCAL.get();
}
public static void remove() {
THREAD_LOCAL.remove();
}
}
4.2. 定义拦截器,在请求前为用户登陆信息设置值,请求后清除值
preHandle
在从token中获取到用户信息时,设置用户信息到ThreadLocal中,afterCompletion
在请求完成后,清除ThreadLocal中的用户登陆信息
package com.web.springbootall.interceptor;
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.interfaces.Claim;
import com.web.springbootall.pojo.Result;
import com.web.springbootall.pojo.User;
import com.web.springbootall.util.JwtUtil;
import com.web.springbootall.util.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (StringUtils.hasLength(token)) {
Claim claim = null;
try {
claim = jwtUtil.parse(token);
} catch (Exception e) {
log.error("token解析失败", e);
}
if (claim != null) {
//每次请求前,拦截器设置用户信息,确保用户信息在ThreadLocal中
ThreadLocalUtil.set(claim.as(User.class));
return true;
}
}
Result tokenInvalid = Result.error("token无效");
response.setContentType("application/json;charset=utf-8");
response.setStatus(401);
response.getWriter().write(JSON.toJSONString(tokenInvalid));
return false;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//请求完成后,清除用户信息,防止出现内存泄露
ThreadLocalUtil.remove();
}
}
4.3. 在拦截器设置了用户信息后,从ThreadLocal中获取
import com.web.springbootall.mapper.UserMapper;
import com.web.springbootall.pojo.User;
import com.web.springbootall.service.UserService;
import com.web.springbootall.util.ThreadLocalUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User getUserInfo() {
User user = ThreadLocalUtil.get();
if (user != null) {
user = userMapper.getById(user.getId());
}
return user;
}
}
5. 对象字段校验
5.1 对象字段增加注解,以及错误提示信息
@NotNull(message = "id不能为空")
@NotEmpty @Pattern(regexp = "^\\S{1,10}$", message = "昵称请输入1-10个字符")
@Email
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import lombok.ToString;
import java.time.LocalDateTime;
@Data
@ToString
public class User {
@NotNull(message = "id不能为空")
private Integer id;//主键ID
private String username;//用户名
//对象变为JSON时,不设置该字段
@JsonIgnore
private String password;//密码
@NotEmpty
@Pattern(regexp = "^\\S{1,10}$", message = "昵称请输入1-10个字符")
private String nickname;//昵称
@NotEmpty
@Email
@Pattern(regexp = "^\\S{5,16}$", message = "邮箱格式不正确,5-16位")
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
5.2 Controller接收对象出增加注解
@Validated User user
这里表示对User进行校验
@PutMapping
public Result updateUserInfo(@RequestBody @Validated User user){
userService.updateUserInfo(user);
return Result.success();
}
5.3 全局异常处理针对该异常进行处理
针对该种类型异常(MethodArgumentNotValidException
),获取message的内容e.getBindingResult().getAllErrors().get(0) .getDefaultMessage()
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidateException(MethodArgumentNotValidException e) {
log.error("数据校验异常:{}", e.getMessage(), e);
String defaultMessage = e.getBindingResult().getAllErrors().get(0)
.getDefaultMessage();
return Result.error(StringUtils.hasLength(defaultMessage) ? defaultMessage : e.getMessage());
}
六. 分组校验
6.1 实体类增加校验注解
下面的类定义了两个interface
接口Add
和Update
, 然后在id
属性的@NotNull
注解的gourps
指定Update.class
,表明id
属性仅修改时不能为空,但是新增的时候,因为id
数据库自增,所以可以为空。
而categoryName
和categoryAlias
无论新增还是修改都不能为空,所以groups = {Add.class, Update.class}
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.ToString;
import java.time.LocalDateTime;
@Data
@ToString
public class Category {
@NotNull(message = "修改的分类数据不能为空", groups = Update.class)
private Integer id;//主键ID
@NotEmpty(message = "分类名称不能为空", groups = {Add.class, Update.class})
private String categoryName;//分类名称
@NotEmpty(message = "分类别名不能为空", groups = {Add.class, Update.class})
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间
public interface Add {
}
public interface Update {
}
}
6.2 Controller校验注解增加分组,表明该操作为何分组
@Validated(Category.Update.class)
指定该操作属于修改分组
@PutMapping("/update")
public Result updateCategory(@RequestBody @Validated(Category.Update.class) Category category){
categoryService.updateCategory(category);
return Result.success();
}
@Validated(Category.Add.class)
指定该操作属于新增分组
@PostMapping("/add")
public Result addCategory(@RequestBody @Validated(Category.Add.class) Category category){
categoryService.addCategory(category);
return Result.success();
}
6.3 校验属于默认分组
categoryName
和categoryAlias
没有指定分组,则属于默认分组Default
。Add
和Update
都继承了Default
,意味着categoryName
和categoryAlias
属于两个组,他们的操作校验,都需要满足
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.groups.Default;
import lombok.Data;
import lombok.ToString;
import java.time.LocalDateTime;
@Data
@ToString
public class Category {
@NotNull(message = "修改的分类数据不能为空", groups = Update.class)
private Integer id;//主键ID
@NotEmpty(message = "分类名称不能为空")
private String categoryName;//分类名称
@NotEmpty(message = "分类别名不能为空")
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间
public interface Add extends Default {
}
public interface Update extends Default {
}
}
七.自定义校验
7.1自定义注解
这里使用定义了StateValidation
进行校验
import com.web.springbootall.validation.StateValidation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@Constraint(
validatedBy = {StateValidation.class}
)
public @interface StateValid {
String message() default "文章状态只能为:草稿/已发布";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
7.2 自定义校验类,实现校验接口
StateValidation
实现了ConstraintValidator
接口,定义了状态为草稿
和已发布
才满足条件,返回true
package com.web.springbootall.validation;
import com.web.springbootall.anno.StateValid;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.util.StringUtils;
public class StateValidation implements ConstraintValidator<StateValid, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (StringUtils.hasLength(value) && (value.equals("草稿") || value.equals("已发布"))){
return true;
}
return false;
}
}
7.3 需要校验的字段使用注解
状态为已发布
或者草稿
才可校验通过
@StateValid
private String state;//发布状态 已发布|草稿
八.登陆使用Redis再次校验token正确性
8.1问题产生
修改密码后,按道理必须重新登陆,使用新的token访问别的接口。但是在拦截器处,仅仅判断了JWT中的token是否正确,应该增加判断token是否为新的token才对。
8.2 Redis解决实现方式
用户登陆时,Redis存储token信息,key为id,value为token,一小时过期。
用户修改密码后,Redis中移除该用户的token。
拦截器从请求头取的token进行token判断时,除了从JWT判断是否正常,还判断是否和Redis一致。
如果修改密码后未进行重新登陆,访问其他请求(比如用户信息)使用的旧的token,那么拦截器不会放行,访问不到。
如果使用重新登陆后的token,因为登陆时token重新设置在了Redis中,所以后续访问其他请求,和拦截器token一致,可以访问到。
8.3 具体实现
引入Redis依赖:
<!--springboot集成redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
登陆:
@Autowired
private StringRedisTemplate redisTemplate;
...
//存储token到Redis,key为userid,value为token,一小时过期
redisTemplate.opsForValue().set(user.getId().toString(), token, 1L, TimeUnit.HOURS);
修改密码:
//修改密码后从Redis移除该用户对应的token信息,以便后续请求访问时,拦截器的token的redis不一致,跳转登陆
redisTemplate.delete(id.toString());
拦截器:
...
User user = claim.as(User.class);
//判断用户登陆存储到Redis中的token和新获取的是否一致,如果不一致,可能是修改密码后未重新登陆,此时应该让用户重新登陆。
// 修改密码后,从Redis移除该用户的token,此处判断token不一致,不放行,进行登陆
String tokenRedis = redisTemplate.opsForValue().get(user.getId().toString());
if (token.equals(tokenRedis)){
//每次请求前,拦截器设置用户信息,确保用户信息在ThreadLocal中
ThreadLocalUtil.set(user);
return true;
}
九.前端
9.1 js方法的使用
定义script.js
function a(msg){
console.log("in a...............:" + msg);
}
function b(msg){
console.log(new Date() + "in b...............:" + msg);
}
定义html,引入使用js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<button id="btn">点我展示信息</button>
</div>
<script src="./script.js"></script>
<script>
document.getElementById("btn").onclick = function(){
a('哈哈,今天也要加油');
}
</script>
</body>
</html>
html页面,VSCode,alt+b打开浏览器,点击按钮,控制台输出信息
9.2 js export的使用
9.2.1 初步使用
修改以上js,function前面加export
export function a(msg){
console.log("in a...............:" + msg);
}
export function b(msg){
console.log(new Date() + "in b...............:" + msg);
}
修改以上html,script的type为module,然后在其中使用import
导入方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<button id="btn">点我展示信息</button>
</div>
<script type="module">
import {a} from "./script.js"
document.getElementById("btn").onclick = function(){
a('哈哈,今天也要加油');
}
</script>
</body>
</html>
VSCode使用open with live server
,不然会出现跨域问题。
9.2.2js批量导出方法在这里插入代码片
export {a, b}
即为导出a,b方法
function a(msg){
console.log("in a...............:" + msg);
}
function b(msg){
console.log(new Date() + "in b...............:" + msg);
}
export {a, b}
方法名称太长,导入导出可以as取别名
export {a as c, b as d}
9.2.3 js导出默认
使用export default {a, b}
导出默认
function a(msg){
console.log("in a...............:" + msg);
}
function b(msg){
console.log(new Date() + "in b...............:" + msg);
}
export default {a, b}
html import时不需要{}
,如下面的import on from "./script.js"
,on是随便起的名字,包含了导入的所有方法,
on.a('哈哈,今天也要加油');
使用on.a可以使用a方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<button id="btn">点我展示信息</button>
</div>
<script type="module">
import on from "./script.js"
document.getElementById("btn").onclick = function(){
on.a('哈哈,今天也要加油');
}
</script>
</body>
</html>
9.3 Vue入门
9.3.1 Vue快速热门
这里首先定义了一个script
,type
为module
,然后引入了Vue官方cdn的js中的createApp,
import {createApp} from "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
,
然后createApp
方法中传入了一个对象参数,
{
data() {
return {
msg: '看起来,你在学习Vue'
}
}
}
data
中定义数据msg
,值为看起来,你在学习Vue
并且createApp
挂在到了id为app的元素上.mount("#app");
上面定义了一个div,id为app
,则该div中,可以使用createApp
定义的data
中的数据msg
,
使用了{{msg}}
这种方式获取
<div id="app">
<h1>{{msg}}</h1>
</div>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h1>{{msg}}</h1>
</div>
<script type="module">
import {createApp} from "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
createApp({
data() {
return {
msg: '看起来,你在学习Vue'
}
}
}).mount("#app");
</script>
</body>
</html>
9.3.2 v-for循环界面展示数据
这里使用了v-for
,articleList
是数据来源(具体根据data中的名称确定),article
是遍历集合,每个元素的值(自定义名称),index
为下标。
<tr v-for="(article, index) in articleList">
<th>{{article.title}}</th>
<th>{{article.category}}</th>
<th>{{article.time}}</th>
<th>{{article.state}}</th>
<td>
<button>编辑</button>
<button>删除</button>
</td>
</tr>
使用的数据在data中定义了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<table border="1 solid" colspa="0" cellspacing="0">
<tr>
<th>文章标题</th>
<th>分类</th>
<th>发表时间</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr v-for="(article, index) in articleList">
<th>{{article.title}}</th>
<th>{{article.category}}</th>
<th>{{article.time}}</th>
<th>{{article.state}}</th>
<td>
<button>编辑</button>
<button>删除</button>
</td>
</tr>
</table>
</div>
<script type="module">
//导入vue模块
import { createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
//创建应用实例
createApp({
data() {
return {
articleList:[
{
title:"医疗反腐绝非砍医护收入",
category:"时事",
time:"2023-09-5",
state:"已发布"
},
{
title:"中国男篮缘何一败涂地?",
category:"篮球",
time:"2023-09-5",
state:"草稿"
},
{
title:"华山景区已受大风影响阵风达7-8级,未来24小时将持续",
category:"旅游",
time:"2023-09-5",
state:"已发布"
}
]
}
}
}).mount("#app")//控制页面元素
</script>
</body>
</html>
9.3.3 v-bind动态绑定数据
<a v-bind:href="url">百度</a>
,这里绑定了a标签的href
属性值为url
,url
在data
中定义
也可以使用<a :href="url">百度</a>
,这种简化方式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<a v-bind:href="url">百度</a>
</div>
<script type="module">
//引入vue模块
import { createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
//创建vue应用实例
createApp({
data() {
return {
url: 'https://www.baidu.com'
}
}
}).mount("#app")//控制html元素
</script>
</body>
</html>
9.3.4 v-if和v-show
v-if会根据判断展示和移除元素,在浏览器中看不到,适用于不频繁切换的场景;
v-show通过css的display控制,适用于频繁切换展示隐藏的场景
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
手链价格为: <span v-if="user.level == 1">9.9</span>
<span v-else-if="user.level > 1 && user.level <=5 ">19.9</span>
<span v-else="user.level > 5">29.9</span>
<br>
<span v-show="user.level == 1">9.9</span>
<span v-show="user.level > 1 && user.level <=5 ">19.9</span>
<span v-show="user.level > 5">29.9</span>
</div>
<script type="module">
//导入vue模块
import { createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
//创建vue应用实例
createApp({
data() {
return {
user: {
name: 'zs',
level: 1
}
}
}
}).mount("#app")//控制html元素
</script>
</body>
</html>
9.3.5 v-on
定义了v-on:
v-on:click="weather"
定义了方法:
methods: {
weather() {
alert('今天天气很好');
},
learn() {
alert('今天学习得好');
}
}
简写:
@click="learn"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<button v-on:click="weather">点我有惊喜</button>
<button @click="learn">再点更惊喜</button>
</div>
<script type="module">
//导入vue模块
import { createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
//创建vue应用实例
createApp({
data() {
return {
//定义数据
}
},
methods: {
weather() {
alert('今天天气很好');
},
learn() {
alert('今天学习得好');
}
},
}).mount("#app");//控制html元素
</script>
</body>
</html>
9.3.6 v-model双向绑定
这里的methods中的clear,使用了this.query获取data中的数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
文章分类: <input type="text" v-model="query.type"/> {{query.type}}
发布状态: <input type="text" v-model="query.state"/> {{query.state}}
<button>搜索</button>
<button @click="clear">重置</button>
<br />
<br />
<table border="1 solid" colspa="0" cellspacing="0">
<tr>
<th>文章标题</th>
<th>分类</th>
<th>发表时间</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr v-for="(article,index) in articleList">
<td>{{article.title}}</td>
<td>{{article.category}}</td>
<td>{{article.time}}</td>
<td>{{article.state}}</td>
<td>
<button>编辑</button>
<button>删除</button>
</td>
</tr>
</table>
</div>
<script type="module">
//导入vue模块
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
//创建vue应用实例
createApp({
data() {
return {
//定义数据
articleList: [{
title: "医疗反腐绝非砍医护收入",
category: "时事",
time: "2023-09-5",
state: "已发布"
},
{
title: "中国男篮缘何一败涂地?",
category: "篮球",
time: "2023-09-5",
state: "草稿"
},
{
title: "华山景区已受大风影响阵风达7-8级,未来24小时将持续",
category: "旅游",
time: "2023-09-5",
state: "已发布"
}],
query: {
type: '人文',
state: '草稿'
}
}
},
methods: {
clear(){
this.query = {
type: '',
state: ''
};
}
},
}).mount("#app")//控制html元素
</script>
</body>
</html>
9.3.7 axios获取数据
引入js:
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
发送:
axios.get('http://localhost:8080/article/getAll').then(res=>{
this.articleList = res.data;
}).catch(err=>{
console.log(err);
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
文章分类: <input type="text" v-model="query.category"/>
发布状态: <input type="text" v-model="query.state"/>
<button @click="queryArticle">搜索</button>
<button @click="clear">重置</button>
<br />
<br />
<table border="1 solid" colspa="0" cellspacing="0">
<tr>
<th>文章标题</th>
<th>分类</th>
<th>发表时间</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr v-for="(article,index) in articleList">
<td>{{article.title}}</td>
<td>{{article.category}}</td>
<td>{{article.time}}</td>
<td>{{article.state}}</td>
<td>
<button>编辑</button>
<button>删除</button>
</td>
</tr>
</table>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="module">
//导入vue模块
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
//创建vue应用实例
createApp({
data() {
return {
//定义数据
articleList: [],
query: {
category: '',
state: ''
}
}
},
methods: {
clear(){
this.query = {
category: '',
state: ''
};
},
queryArticle(){
axios.get('http://localhost:8080/article/search?category='+this.query.category + '&state='+this.query.state).then(res=>{
this.articleList = res.data;
}).catch(err=>{
console.log(err);
})
}
},
mounted() {
axios.get('http://localhost:8080/article/getAll').then(res=>{
this.articleList = res.data;
}).catch(err=>{
console.log(err);
})
},
}).mount("#app")//控制html元素
</script>
</body>
</html>
9.4 Vue工程化
9.4.1 node的安装配置
设置nodejs全局安装路径,prefix
后的内容请修改为自己的
npm config set prefix /Users/jieqin/utils/nodejs
设置淘宝镜像
npm config set registry http://registry.npm.taobao.org/
查看淘宝镜像
npm config get registry
显示以下设置成功
http://registry.npm.taobao.org/
9.4.2 create-vue创建项目
npm init vue@latest
输入项目名称,其他选择否
然后cd vue-project
这里的vue-project为自己输入的项目名称
然后npm install
然后npm run dev
访问:http://localhost:5173/
9.4.3 组合式API
Api.vue,ref
类似data,不过组合式使用不一样,先import {ref, onMounted} from 'vue'
,
再const count = ref(0);
,如果想要设置变量的值,需要设置给value,从count.value++;
可以看出来
<script setup>
import {ref, onMounted} from 'vue'
const count = ref(0);
function add(){
count.value++;
}
//挂载完成
onMounted(()=>{
console.log('onMounted');
})
</script>
<template>
<button @click="add">{{count}}</button>
</template>
App.vue,import了Api.vue
注意import ApiVue from './Api.vue'
,<ApiVue/>
,以及<script setup>
<script setup>
import ApiVue from './Api.vue'
</script>
<template>
<h1>西安</h1>
<br>
<ApiVue/>
</template>
<style scoped>
h1{
color: rgb(151, 87, 87);
}
</style>
9.4.4 组合式API使用(发送axios请求)
首先在项目目录下执行sudo npm install axios
,安装axios,安装axios后,在node-modules下有axios文件夹, import axios from 'axios'
即可进行导入
<script setup>
import {ref, onMounted} from 'vue'
import axios from 'axios'
const articleList = ref([]);
onMounted(() => {
axios.get('http://localhost:8080/article/getAll')
.then(res=>{
articleList.value = res.data;
}).catch(err=>{
console.log(err);
})
});
const query = ref({
category: '时事',
state: '已发布'
})
function queryArticle(){
axios.get('http://localhost:8080/article/search?category=' + query.value.category + '&state=' + query.value.state)
.then(res=>{
articleList.value = res.data;
}).catch(err=>{
console.log(err);
})
}
function clear(){
query.value = {}
}
</script>
<template>
<div>
文章分类: <input type="text" v-model="query.category"/>
发布状态: <input type="text" v-model="query.state"/>
<button @click="queryArticle">搜索</button>
<button @click="clear">重置</button>
<br />
<br />
<table border="1 solid" colspa="0" cellspacing="0">
<tr>
<th>文章标题</th>
<th>分类</th>
<th>发表时间</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr v-for="article in articleList">
<td>{{article.title}}</td>
<td>{{article.category}}</td>
<td>{{article.time}}</td>
<td>{{article.state}}</td>
<td>
<button>编辑</button>
<button>删除</button>
</td>
</tr>
</table>
</div>
</template>
9.4.5 抽取发送请求方法到js
发送axios请求的js
import axios from 'axios'
//这里定义请求前缀baseURL
const baseURL = 'http://localhost:8080';
//这里axios进行创建,创建后使用instance调用和axios调用一样,这里注意{baseURL}有个花括号
const instance = axios.create({baseURL});
//这里定义发送请求的函数并导出,引入js的的地方直接使用。因为是异步请求,方法前面加上async表示异步,里面同步等待返回。在使用方法的地方,也是类似的。
//比如
// async function all(){
// articleList.value = await getAll();
// }
// async function queryArticle(){
// articleList.value = await search({...query.value});
// }
export async function getAllArticleService(){
return await instance.get('/article/getAll')
.then(res=>{
return res.data;
}).catch(err=>{
console.log(err);
})
}
export async function queryArticleService(queryValue){
return await instance.get('/article/search?', {params: queryValue})
.then(res=>{
return res.data;
}).catch(err=>{
console.log(err);
})
}
使用该js的vue
<script setup>
import {ref, onMounted} from 'vue'
import axios from 'axios'
import {getAllArticleService as getAll, queryArticleService as search} from '@/api/article.js'
const articleList = ref([]);
onMounted(() => {
all();
});
async function all(){
articleList.value = await getAll();
}
const query = ref({
category: '时事',
state: '已发布'
})
async function queryArticle(){
articleList.value = await search({...query.value});
}
function clear(){
query.value = {}
}
</script>
<template>
<div>
文章分类: <input type="text" v-model="query.category"/>
发布状态: <input type="text" v-model="query.state"/>
<button @click="queryArticle">搜索</button>
<button @click="clear">重置</button>
<br />
<br />
<table border="1 solid" colspa="0" cellspacing="0">
<tr>
<th>文章标题</th>
<th>分类</th>
<th>发表时间</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr v-for="article in articleList">
<td>{{article.title}}</td>
<td>{{article.category}}</td>
<td>{{article.time}}</td>
<td>{{article.state}}</td>
<td>
<button>编辑</button>
<button>删除</button>
</td>
</tr>
</table>
</div>
</template>
9.4.5 axios响应拦截器统一对结果进行返回
定义util包下的request.js,对请求的响应和异常进行统一处理
import axios from 'axios'
//这里定义请求前缀baseURL
const baseURL = 'http://localhost:8080';
//这里axios进行创建,创建后使用instance调用和axios调用一样,这里注意{baseURL}有个花括号
const instance = axios.create({baseURL});
//响应统一拦截器,定义返回和错误处理
instance.interceptors.response.use(
//2xx请求状态码
result => {
return result.data;
},
//其他请求状态码
err => {
console.log(err);
Promise.reject(err);
}
);
//导出供外部使用
export default instance;
引用刚刚的js,import request from '@/util/request.js'
,这里的await和async去掉了,拦截器本身处理了,但是vue处调用异步方法时,还是需要加上await和async
//引入发送请求,返回响应的js
import request from '@/util/request.js'
//这里定义发送请求的函数并导出,引入js的的地方直接使用。因为是异步请求,方法前面加上async表示异步,里面同步等待返回。在使用方法的地方,也是类似的。
//比如
// async function all(){
// articleList.value = await getAll();
// }
// async function queryArticle(){
// articleList.value = await search({...query.value});
// }
export function getAllArticleService(){
//请求进行了拦截返回处理,这里直接调用即可
return request.get('/article/getAll');
}
export function queryArticleService(queryValue){
return request.get('/article/search?', {params: queryValue});
}
9.5 Element-Plus
9.5.1快速入门
create-vue创建项目
npm init vue@latest
,输入项目名称,其他选择默认否
cd 项目目录下,npm install
,再npm run dev
,看是否可以访问,如果可以证明创建的Vue没问题。
npm install element-plus --save
进行element-plus的安装
main.js复制官网全局的
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
然后复制官网组件进行修改,自己写一个包含卡片,表单,按钮,表格,分页的界面
类似下面的图片:
分页想要变成中文的,main.js
引入中文包:
import locale from 'element-plus/dist/locale/zh-cn.js'
然后使用app.use(ElementPlus, { locale })
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import locale from 'element-plus/dist/locale/zh-cn.js'
const app = createApp(App)
app.use(ElementPlus, { locale })
app.mount('#app')
所有相关代码:
src/util/request.js封装axios发送请求,响应结果。这里需要项目目录下安装了axios,
可以使用sudo npm install axios
安装
import axios from 'axios'
const baseURL = 'http://localhost:8080';
const instance = axios.create({baseURL});
instance.interceptors.response.use(
result=>{
return result.data;
},
err=>{
console.error(err);
Promise.reject(err);
}
);
export default instance;
src/api/article.js,引入request.js,封装发送请求
import request from '@/util/request.js'
//分页查询文章数据
export const getArticlePage = function(queryParams) {
return request.get("/article/getAll", {params: queryParams});
};
src/Article.vue引入article.js,界面展示
<script lnag='ts' setup>
import { reactive, ref, onMounted } from 'vue'
import {
Check,
Delete,
Edit,
Message,
Search,
Star,
} from '@element-plus/icons-vue'
import {getArticlePage} from '@/api/article.js'
const article = reactive({
category: '',
state: ''
})
const tableData = ref([
]);
const currentPage = ref(1)
const pageSize = ref(10)
const small = ref(false)
const background = ref(false)
const disabled = ref(false)
const total = 20
const handleSizeChange = (number) => {
console.log(number);
}
const handleCurrentChange = (number) => {
console.log(number);
}
const onSubmit = () => {
console.log('submit!');
}
const queryParams = ref({
queryNum: 1,
querySize: 10,
category: '',
state: ''
});
//页面加载完毕,查询表格数据
onMounted(() => {
getPage();
});
//分页查询文章方法,异步请求,需要async和await
const getPage = async function(){
let getData = await getArticlePage({...queryParams.value});
console.log(getData);
tableData.value = getData;
}
</script>
<template>
<!-- 所有在一个卡片中 -->
<el-card class="box-card">
<!-- 首行 -->
<div class="card-header">
<span>文章管理</span>
<el-button type="primary">发布文章</el-button>
</div>
<!-- 分割线 -->
<el-divider />
<!-- 搜索表单 -->
<el-form :inline="true" :model="article">
<el-form-item label="文章分类:" width="120">
<el-select
v-model="article.category"
placeholder="请选择"
clearable
size="default"
style="width: 240px"
>
</el-select>
</el-form-item>
<el-form-item label="发布状态:">
<el-select
v-model="article.region"
placeholder="请选择"
clearable
size="default"
style="width: 240px"
>
<el-option label="已发布" value="已发布" />
<el-option label="草稿" value="草稿" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">搜索</el-button>
<el-button type="primary" @click="onSubmit" plain>重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="title" label="文章标题" />
<el-table-column prop="category" label="分类" />
<el-table-column prop="time" label="发表时间" />
<el-table-column prop="state" label="状态" />
<el-table-column label="操作" fixed="right" width="180">
<el-button type="primary" :icon="Edit" circle />
<el-button type="danger" :icon="Delete" circle />
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination class="el-p"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 15, 20]"
:small="small"
:disabled="disabled"
:background="background"
layout="jumper, total, sizes, prev, pager, next"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
</template>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
}
.el-p {
display: flex;
justify-content: end;
margin-top: 20px;
}
</style>
App.Vue使用article.vue
<script setup>
import ArticleVue from './Article.vue'
</script>
<template>
<div id="app">
<ArticleVue/>
</div>
</template>
<style scoped>
</style>
main.js引入Element-Plus和其样式,以及中文包
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import locale from 'element-plus/dist/locale/zh-cn.js'
const app = createApp(App)
app.use(ElementPlus, { locale })
app.mount('#app')
9.5.2 创建新的Element-Plus项目
- 首先参考创建一个Vue项目
9.4.2 create-vue创建项目 npm install element-plus --save
进行element-plus的安装sudo npm install axios
安装axiosnpm install sass -D
安装saas- 文件目录整理,删除components下的内容,新建api,utils,views目录,删除App.vue中的内容
- 修改main.js
import './assets/main.scss'
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import locale from 'element-plus/dist/locale/zh-cn.js'
const app = createApp(App)
app.use(ElementPlus, { locale })
app.mount('#app')
request.js放到src/utils
下,baseURL
路径根据实际情况修改
import axios from 'axios'
const baseURL = 'http://localhost:8080';
const instance = axios.create({baseURL});
instance.interceptors.response.use(
result=>{
return result.data;
},
err=>{
console.error(err);
Promise.reject(err);
}
);
export default instance;
- 登录出现跨域,需要处理request.js和vite.config.js
vite.config.js
这里拦截了/api
的请求,代理到http://localhost:8080/api
,,并且去掉了/api
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
//设置代理,解决跨域问题
server: {
proxy: {
//代理/api的请求
'/api': {
//需要代理到的地址
target: 'http://localhost:8080',
//允许修改源
changeOrigin: true,
//将/api去掉
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
request.js
这里将请求都加上前缀/api
,在上面代理处理
import axios from 'axios'
//弹出消息提示,自动取消
import { ElMessage } from 'element-plus'
//失败提示
const fail = (msg) => {
ElMessage({
message: msg,
type: 'warning',
})
}
// const baseURL = 'http://localhost:8080';
//统一接口入口
const baseURL = '/api';
const instance = axios.create({baseURL});
instance.interceptors.response.use(
result=>{
if(result.data.code === 0){
return result.data;
}
fail(result.data.message);
Promise.reject(result.data);
},
err=>{
fail(err);
Promise.reject(err);
}
);
export default instance;
- 登陆请求的api写在src/api/login.js中
这里发送请求的参数为表单类型Content-Type: application/x-www-form-urlencoded;charset=UTF-8
,使用URLSearchParams
进行封装传递
import request from '@/utils/request.js'
//注册发送请求
export const registerService = (registerParam)=>{
//因为表单参数,使用URLSearchParams进行参数封装
let registerParams = new URLSearchParams();
for(let key in registerParam){
registerParams.append(key, registerParam[key]);
}
return request.post('/user/register', registerParams);
}
//登陆发送请求
export const loginService = (loginParam)=>{
//因为表单参数,使用URLSearchParams进行参数封装
let loginParams = new URLSearchParams();
for(let key in loginParam){
loginParams.append(key, loginParam[key]);
}
return request.post('/user/login', loginParams);
}
- 注册登陆界面
这里面有对表单的校验,数据绑定
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref, reactive } from 'vue'
//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)
//注册参数
const registerData = ref({
username: '',
password: '',
rePassword: ''
})
//密码和再次输入密码校验
const validateRePassword = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入确认密码'))
} else if (value !== registerData.value.password) {
callback(new Error("两次输入不一致!"))
} else {
callback()
}
}
//数据校验规则
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, max: 16, message: '长度5-16位之间', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 5, max: 16, message: '长度5-16位之间', trigger: 'blur' },
],
rePassword: [
{ validator: validateRePassword, trigger: 'blur' },
],
};
import {registerService, loginService} from '@/api/login.js'
//注册
const register = async ()=> {
let registerResult = await registerService(registerData.value);
success(registerResult.message);
};
//登陆
const login = async ()=> {
let loginResult = await loginService(registerData.value);
success(loginResult.message);
};
//弹出消息提示,自动取消
import { ElMessage } from 'element-plus'
//成功提示
const success = (msg) => {
ElMessage({
message: msg,
type: 'success',
})
}
</script>
<template>
<el-row class="login-page">
<el-col :span="12" class="bg"></el-col>
<el-col :span="6" :offset="3" class="form">
<!-- 注册表单 -->
<el-form ref="form" size="large" autocomplete="off" v-if="isRegister" :rules="rules" :model="registerData">
<el-form-item>
<h1>注册</h1>
</el-form-item>
<el-form-item prop="username">
<el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
</el-form-item>
<el-form-item prop="rePassword">
<el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>
</el-form-item>
<!-- 注册按钮 -->
<el-form-item>
<el-button class="button" type="primary" auto-insert-space @click="register">
注册
</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = false">
← 返回
</el-link>
</el-form-item>
</el-form>
<!-- 登录表单 -->
<el-form ref="form" size="large" autocomplete="off" v-else :rules="rules" :model="registerData">
<el-form-item>
<h1>登录</h1>
</el-form-item>
<el-form-item prop="username">
<el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
</el-form-item>
<el-form-item>
<el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
</el-form-item>
<el-form-item class="flex">
<div class="flex">
<el-checkbox>记住我</el-checkbox>
<el-link type="primary" :underline="false">忘记密码?</el-link>
</div>
</el-form-item>
<!-- 登录按钮 -->
<el-form-item>
<el-button class="button" type="primary" auto-insert-space @click="login">登录</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = true">
注册 →
</el-link>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<style lang="scss" scoped>
/* 样式 */
.login-page {
height: 100vh;
background-color: #fff;
.bg {
background: url('@/assets/logo2.png') no-repeat 60% center / 240px auto,
url('@/assets/login_bg.jpg') no-repeat center / cover;
border-radius: 0 20px 20px 0;
}
.form {
display: flex;
flex-direction: column;
justify-content: center;
user-select: none;
.title {
margin: 0 auto;
}
.button {
width: 100%;
}
.flex {
width: 100%;
display: flex;
justify-content: space-between;
}
}
}
</style>
9.5.3 Router使用
- 安装vue-router
npm install vue-router@4
- src/router/index.js创建路由器并导出
//导入vue-router
import {createRouter, createWebHistory} from 'vue-router'
//导入组件
import LoginVue from '@/views/Login.vue'
import LayoutVue from '@/views/Layout.vue'
//定义路由关系
const routes = [
{
path: '/login',
component: LoginVue
},
{
path: '/',
component: LayoutVue
}
]
//创建路由器
const router = createRouter({
history: createWebHistory(),
routes: routes
});
//导出
export default router
- vue应用实例中使用vue-router
main.js中导入index.js,并使用。这里index.js不用指明,app.use(router);
需要单独写,不然界面出不来
import './assets/main.scss'
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import locale from 'element-plus/dist/locale/zh-cn.js'
//导入路由
import router from '@/router'
const app = createApp(App);
app.use(router);
app.use(ElementPlus, { locale });
app.mount('#app');
- 声明router-view标签,展示组件内容
在App.vue中,添加<router-view/>
<script setup>
</script>
<template>
<router-view/>
</template>
<style scoped>
</style>
- 登陆完成,跳转首页路由处理
Login.vue导入路由,登陆完成的方法中,增加路由跳转
//导入路由
import {useRouter} from 'vue-router'
const router = useRouter();
//登陆
const login = async ()=> {
let loginResult = await loginService(registerData.value);
success(loginResult.message);
//登陆成功跳转到首页
router.push('/');
};
主要是import {useRouter} from 'vue-router' const router = useRouter();
,以及router.push('/');
6. 主页菜单跳转
增加vue页面
router/index.js, import 导入的页面,再增加子路由children
,定义跳转的页面。
redirect
定义默认跳转的界面。
//定义路由关系
const routes = [
{
path: '/login',
component: LoginVue
},
{
path: '/',
component: LayoutVue,
//定义子组件
children: [
{
path: '/user/avatar',
component: UserAvatarVue
},
{
path: '/user/info',
component: UserInfoVue
},
{
path: '/user/resetPwd',
component: UserResetPasswordVue
},
{
path: '/article/category',
component: ArticleCategoryVue
},
{
path: '/article/manage',
component: ArticleManageVue
},
],
redirect: '/article/manage'
}
]
- 主页中需要路由跳转后展示的地方定义
<router-view/>
Layout.vue,其中<el-menu-item index="/article/category">
,这些el-menu-item
,增加了index
属性进行跳转,路径对应index.js的path
,然后在跳转展示位置设置了router-view
<el-main> <router-view/> </el-main>
<script setup>
import {
Management,
Promotion,
UserFilled,
User,
Crop,
EditPen,
SwitchButton,
CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
</script>
<template>
<el-container class="layout-container">
<!-- 左侧菜单 -->
<el-aside width="200px">
<div class="el-aside__logo"></div>
<el-menu active-text-color="#ffd04b" background-color="#232323" text-color="#fff"
router>
<el-menu-item index="/article/category">
<el-icon>
<Management />
</el-icon>
<span>文章分类</span>
</el-menu-item>
<el-menu-item index="/article/manage">
<el-icon>
<Promotion />
</el-icon>
<span>文章管理</span>
</el-menu-item>
<el-sub-menu >
<template #title>
<el-icon>
<UserFilled />
</el-icon>
<span>个人中心</span>
</template>
<el-menu-item index="/user/info">
<el-icon>
<User />
</el-icon>
<span>基本资料</span>
</el-menu-item>
<el-menu-item index="/user/avatar">
<el-icon>
<Crop />
</el-icon>
<span>更换头像</span>
</el-menu-item>
<el-menu-item index="/user/resetPwd">
<el-icon>
<EditPen />
</el-icon>
<span>重置密码</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<!-- 右侧主区域 -->
<el-container>
<!-- 头部区域 -->
<el-header>
<div>用户:<strong>东哥</strong></div>
<el-dropdown placement="bottom-end">
<span class="el-dropdown__box">
<el-avatar :src="avatar" />
<el-icon>
<CaretBottom />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile" :icon="User">基本资料</el-dropdown-item>
<el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
<el-dropdown-item command="password" :icon="EditPen">重置密码</el-dropdown-item>
<el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-header>
<!-- 中间区域 -->
<el-main>
<router-view/>
</el-main>
<!-- 底部区域 -->
<el-footer> ©2024 Created by xxx </el-footer>
</el-container>
</el-container>
</template>
<style lang="scss" scoped>
.layout-container {
height: 100vh;
.el-aside {
background-color: #232323;
&__logo {
height: 120px;
background: url('@/assets/logo.png') no-repeat center / 120px auto;
}
.el-menu {
border-right: none;
}
}
.el-header {
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
.el-dropdown__box {
display: flex;
align-items: center;
.el-icon {
color: #999;
margin-left: 10px;
}
&:active,
&:focus {
outline: none;
}
}
}
.el-footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #666;
}
}
</style>
9.5.4 Pinia使用完成请求token的存储
- 安装pinia
npm install pinia
- main.js使用
import {createPinia} from 'pinia'
const pinia = createPinia();
app.use(pinia);
- src/stores/token.js定义store
import {defineStore} from 'pinia';
import {ref} from 'vue';
export const useTokenStore = defineStore('token', ()=>{
//定义描述token
const token = ref('');
//设置token的方法
const setToken = (newToken)=>{
token.value = newToken;
}
//移除token的方法
const removeToken = ()=>{
token.value = '';
}
return {
token,setToken,removeToken
}
});
- Login.vue/article.vue使用
Login.vue,这里使用tokenStore.setToken(loginResult.data);
设置了登陆返回的token到pinia中
//导入token.js
import {useTokenStore} from '@/stores/token.js'
const tokenStore = useTokenStore();
//登陆
const login = async ()=> {
let loginResult = await loginService(registerData.value);
success(loginResult.message);
tokenStore.setToken(loginResult.data);
//登陆成功跳转到首页
router.push('/');
};
article.js
这里通过{headers: {'token': tokenStore.token}}
,在请求头中携带了token
import request from '@/utils/request.js'
//导入token.js
import {useTokenStore} from '@/stores/token.js'
//查询分类列表
export const queryCategoryService = function(){
const tokenStore = useTokenStore();
//pinia中配置的响应式数据不需要通过.value获取
return request.get('/category', {headers: {'token': tokenStore.token}});
}
- request.js的axios的请求拦截器统一添加请求token
instance.interceptors.request.use
这里方法有两个参数,一个正常配置参数,一个异常处理。
正常配置的参数config可以用来设置请求头config.headers.token = useToken.token;
import axios from 'axios'
//弹出消息提示,自动取消
import { ElMessage } from 'element-plus'
//失败提示
const fail = (msg) => {
ElMessage({
message: msg,
type: 'warning',
})
}
// const baseURL = 'http://localhost:8080';
//统一接口入口
const baseURL = '/api';
const instance = axios.create({baseURL});
//响应拦截器
instance.interceptors.response.use(
result=>{
if(result.data.code === 0){
return result.data;
}
fail(result.data.message);
Promise.reject(result.data);
},
err=>{
fail(err);
Promise.reject(err);
}
);
//导入token.js
import {useTokenStore} from '@/stores/token.js'
//请求拦截器
instance.interceptors.request.use(
(config) => {
//获取pinia中的token,如果存在,请求头携带
const useToken = useTokenStore();
if(useToken){
config.headers.token = useToken.token;
}
return config;
},
err => {
fail(err);
Promise.reject(err);
}
);
export default instance;
6.pinia持久化
安装插件:npm install pinia-persistedstate-plugin
main.js,导入,pinia使用
import {createPersistedState} from 'pinia-persistedstate-plugin'
const persist = createPersistedState();
pinia.use(persist);
token.js的defineStore增加第三个参数:
{persisted: true}
7.未登陆跳转到首页
request.js
中先导入路由的index.js,import router from '@/router'
,然后err.response.status === 401
失败响应状态吗为401时,使用router.push('/login');
跳转到登陆界面。
//导入路由router/index.js
import router from '@/router'
...
//统一接口入口
const baseURL = '/api';
const instance = axios.create({baseURL});
//响应拦截器
instance.interceptors.response.use(
//响应码2xx进入该处
result=>{
if(result.data.code === 0){
return result.data;
}
fail(result.data.message);
Promise.reject(result.data);
},
//响应码不为2xx进入此处
err=>{
if(err.response.status === 401){
fail('请重新登陆');
router.push('/login');
}else{
fail(err);
}
Promise.reject(err);
}
);
8.富文本编辑器
npm install @vueup/vue-quill@latest --save
使用的vue的script中导入
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
vue使用:
<div class='editor'>
<quill-editor
theme="snow"
v-model:content="articleModel.content"
contentType="html"
>
</quill-editor>
</div>
vue的script设置样式:
.editor {
width: 100%;
:deep(.ql-editor) {
min-height: 200px;
}
}
总结
该项目代码太多,文章并非完整代码,以上为示例核心代码。