“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情。”
我写这个项目的初衷一方面是为了记录自己自学 vue 的一个过程,另一方面是为了帮助小白快速掌握使用 vue 脚手架创建项目的一个完整过程。
本项目是一个基于 vue2 和 Vue-cli 2 开发的后台管理系统,功能简单,非常适合新手拿来练习。
因为该项目只是一个简单的 demo ,所以我并没有完成所有的功能,但是包含基本的增删改查功能。
我会教大家从零开始搭建一个 vue 脚手架项目,讲解的内容虽说不是最全面的,但是非常细致。
文章主要内容包括页面布局、echarts、axios、增删改查功能、ElementUI 常用组件、路由导航守卫、组件传参、全局变量、全局函数、全局样式等等。
1.项目演示
1.登录 2.首页 3.用户管理 4.课程管理
2.前端技术
- vue2
- Element UI
- Axios
- Echarts
3.项目搭建
这里选择 vscode 作为本项目的开发工具。
vue init webpack ZHIFOU-STUDY复制代码
导入项目 运行项目
打开 vscode 的终端,输入:
npm run dev复制代码
项目启动流程讲解:
当项目启动后,会先找到根页面 App.vue。找到 App.vue 之后,发现有一个路由占位符。
于是去找项目的路由。在项目的路由配置文件中,我们发现项目启动后默认 url 为‘/’时,跳转的页面是 HelloWorld.vue。
所以 HelloWorld.vue 页面的内容会替换路由占位符。
4.项目初始化
1.修改默认样式和页面
这里我们分别删除 App.vue 的默认样式、删除 HelloWorld.vue 组件、删除默认路由配置。 在 src 目录下新建 view 文件夹,用来存放页面。
2.安装配置 Element UI
官网:
https://element.eleme.cn/#/zh-CN复制代码
element ui 是阿里饿了么团队开发的一套基于 VUE 的桌面端组件库,它可以帮助开发人员快速构建功能强大、风格统一的页面。
其实说白了就是别人开发好了一套前端框架,你只需要安装配置一下,然后按照相应的规则就能快速开发出页面。
1.安装 Element UI
npmi element-ui --save
复制代码
2.配置 Element UI
在 main.js 配置 Element UI
// 导入 ElementUI
import ElementUIfrom 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
// 使用 ElementUI
Vue.use(ElementUI);
复制代码
3.安装配置图标库
因为 Element UI 提供的图标比较少,这里推荐安装第三方的图标库:font-awesome。
1.安装 font-awesome
npmi font-awesome -S
复制代码
2.配置 font-awesome
在 main.js 配置 font-awesome
import 'font-awesome/css/font-awesome.min.css'
复制代码
4.安装配置 Echarts
ECharts 是一款由百度团队开源的基于 JavaScript 的数据可视化图表库。 官网:
https://echarts.apache.org/zh/index.html复制代码
1.安装 Echarts
npmi echarts -S
复制代码
2.配置 Echarts
在 main.js 配置 Echarts
// 全局引入 echarts
import * as echartsfrom 'echarts'
// 将 echarts 挂载到 Vue 上
Vue.prototype.$echarts = echarts
复制代码
5.安装配置 Axios
Axios 是一个基于 promise 的 HTTP 库,它主要有如下功能:
- 创建 XMLHttpRequest
- 从 node.js 中创建 http 请求
- 拦截请求和响应
- 转换请求和响应数据
- 自动转换 JSON 数据
1.安装 Axios
npmi axios -S
复制代码
2.配置 Axios
// 引入 axios
import axiosfrom 'axios'
// 将 axios 挂载到 Vue 上
Vue.prototype.$axios = axios
// 设置接口请求的前缀地址
axios.defaults.baseURL = '/zhifou-study'
// 全局设置token
axios.interceptors.request.use(function (config) {
let token = sessionStorage.getItem('token')
if (token) {
config.headers['token'] = token
}
return config
})
复制代码
我们主要添加了接口请求的前缀地址以及请求头中的 token。
6.配置路由
// 挂载路由导航守卫:to表示将要访问的路径,from表示从哪里来,next是下一个要做的操作
<!--StartFragment-->router<!--EndFragment-->.beforeEach((to, from, next) => {
// 放行登录页面
if (to.path === '/login') {
return next()
}
// 获取token
const tokenStr = sessionStorage.getItem('token')
if (!tokenStr) {
return next('/login')
} else {
next()
}
})
复制代码
路由导航守卫主要用来拦截请求信息。
5.登录
1.配置跳转登录页面的路由
const router = new Router({
mode:'history',// 去掉路径中的 # 号
routes: [
{ path: '/', redirect: 'login' },
{ path: '/login', name: 'login', component: () => import('@/view/Login.vue') },
]
})
复制代码
mode: 'history' 用来去除浏览器路径自带的 # 号。redirect 表示当路径是'/'时,默认跳转到路径 login 对应的页面。
component: () =>import('@/view/Login.vue')
复制代码
上面是 ES6 写法,表示导入对应的页面,
2.页面结构
接下来,我们在 view 文件夹下面开发登录页面 Login.vue。在 vue 项目中,普通页面以及组件的后缀名都是 .vue
。
我们先来看一下,在 vue 脚手架项目中,页面或者组件的基本结构: 1.页面内容
页面的内容需要写在 template 标签中。template 标签中必须有只能有一个根标签
,不然会报错: 这里我们通常用 div 做根标签。
2.vue 实例
script 标签里面是 vue 实例。export default 是 ES6 语法,表示向外暴露。
所以我们在配置路由的时候使用 import
导入页面。
components
表示该页面导入的组件集合。
data()
用来存放页面的属性。
created()
会在页面渲染之前调用。通常用来初始化某些属性值,然后再渲染成视图。
mounted()
会在 html 页面渲染之后调用。通常是初始化页面完成后,再对 html 的 dom 节点进行一些操作。
methods
主要用来存放页面调用的一些方法。
3.css 样式
这个就不用多讲了,主要是用来写一些样式。
不过在 style 标签上,有一个属性:scoped
,表示私有的。 也就是说它的 CSS 样式只能作用于当前的页面(组件),其他页面不会污染
本页面的样式。该属性可以使组件之间的样式互不干扰。
4.快捷方式
虽然说页面结构只有三部分,但是真正敲的时候也要花一点时间。
这里教大家一些快捷方式:
切换英文输入法,页面输入 def,选择最下面那个,然后按下回车键: 接着输入 data,选择第二个: 然后依次输入生命周期函数的前两个字母: 当然啦我们还可以使用第三方插件或者是配置用户代码片段的方式快速生成 vue 页面模板,这里不再过多赘述,后面会专门写一篇文章进行讲解。
3.Element UI 的使用
Element UI 的使用非常简单。
只需要进入官网,打开组件入口,找到相关组件
,复制粘贴代码即可。
对于不同的组件,我们主要关注其中的 Attributes
(属性)和 Events
(事件)。
4.开发登录页面
我把要开发的登录页面划分为 4 部分: 1.背景图
这里强烈推荐一个网站,上面有很多好看的高清壁纸:
https://wallhaven.cc/复制代码
下载完背景图之后,我们先将图片保存到 /assets/img 路径下。
然后给 Login.vue 的根标签设置类选择器,并设置 css 样式:
给根标签设置绝对定位,绝对定位会脱离文档标准流,所以跟标签会以浏览器窗口作为定位参照物。设置 background-size 的宽高都为 100%,背景图片便会填满整个浏览器窗口。
在设置 background-image 时,../ 表示上一级目录, ./ 表示当前目录下。
2.登录面板
登录面板使用了 Element UI 的卡片组件。
在画前端页面的时候,建议多使用卡片。
因为卡片可以让页面模块化,页面布局看起来会更加合理、美观。
在 Element UI 中,卡片组件是 el-card。
我们首先复制卡片组件的代码,然后为其设置样式。
这里登录面板设置为绝对定位,然后 top/bottom/left/right 设置为 0,这样是为了垂直居中。margin 设置为 auto 是水平居中。
然后我们在卡片的头部添加系统的图标、登录标题和欢迎语。 在 Element UI 中,图片组件是 el-image。该组件重要的属性是 src,src 绑定了一个 url 变量。
在 vue 项目中,常用的引入图片的方式有 4 种:
1.import
<template>
<div>
<el-image :src="logo" fit="cover"></el-image>
</div>
</template>
<script>
import logo from "../assets/img/logo.png";
export default {
data() {
return {
logo,
};
},
};
</script>
复制代码
2.require
<template>
<div>
<el-image :src="logo_url" fit="cover"></el-image>
</div>
</template>
<script>
export default {
data() {
return {
logo_url: require("@/assets/img/logo.png"),
};
},
};
</script>
复制代码
3.img 标签静态引入
<img src="../assets/img/logo.png" >
复制代码
4.background-image 样式静态引入
background-image: url("../assets/img/bg.jpg");
复制代码
除了可以用
./
表示相同文件夹下的绝对路径,我们还可以使用@
符号表示绝对路径,例如:
logo_url:require("@/assets/img/logo.png"),
复制代码
上面的例子就表示在 assets 文件夹下。
接下来我们在卡片里面添加表单组件。
在 Element UI 中,表单组件是 el-form。 el-form 有三个重要的属性:
- ref:表示对组件的引用,或者叫做别名。
- model:绑定的数据。
- rules:表示校验的规则。
详细的用法大家可以查看 Element 的组件文档。
我们在登录卡片中添加表单组件的代码: 这里需要设置按钮的宽度:
.el-button {
width: 100%;
}
复制代码
然后双向绑定数据、添加表单校验规则:
最后就是绑定登录方法: 在登录方法里面,首先要对表单属性进行校验,校验不通过会提示校验信息
this.$ref
s[formName].validate((valid) => {
if (valid) {
// 校验通过
} else {
return false;
}
});
复制代码
校验通过之后,执行登录逻辑:
submitForm(formName) {this.$refs[formName].validate((valid) => {
if (valid) {
this.loginLoading = true;
this.$axios
.post("/auth/login", this.form)
.then((res) => {
if (res.data.success) {
sessionStorage.setItem("user",JSON.stringify(res.data.data.user));
sessionStorage.setItem("token", res.data.token);
this.$router.push("/home");
} else {
this.$message.error(res.data.msg);
this.loginLoading = false;
}
})
.catch((err) => {
this.$message.error("服务器连接失败,请稍后重试......");
this.loginLoading = false;
});
} else {
return false;
}
});
},
复制代码
我们使用 axios 向后台发送登录请求,请求通过之后,我们一般会将用户信息和 token 存储在 sessionStorage 中,然后跳转到后台主页。
if (res.data.success) {
sessionStorage.setItem("user",JSON.stringify(res.data.data.user));
sessionStorage.setItem("token", res.data.token);
this.$router.push("/home");
} else {
this.$message.error(res.data.msg);
this.loginLoading = false;
}
复制代码
因为现在主流开发都是前后端分离的,所以前后端需要用 token 来保证信息的安全性。
所以我们在配置 axios 的时候,需要给每个请求的请求头
添加 token: 同时需要校验用户的请求是否携带 token 3.右下角图标
右下角图片展示这里不再过多赘述,核心就是伪类 hover 的使用。
4.footer
footer 就是底部文字介绍或者链接,这里也不做过多介绍。
<!--footer -->
<p class="footer">Copyright © ZhiFou All Rights Reserved</p>
复制代码
6.后台主页
登陆成功后跳转到了后台主页
this.$router.push("/home");
复制代码
先配置一下后台主页的路由
1.主页布局
主页布局我们采用 Element UI 的 Container 布局容器 。 新建 Home.vue,复制粘贴布局对应的代码
2.菜单
我们在 el-aside 代码里面开发左侧菜单模块: 在 Element UI 里面,菜单组件是 NavMenu 导航菜单。
在实际开发中,菜单数据一般是后台返回的用户权限信息
。前端拿到菜单数据后再遍历展示。 该案例用固定值展示。
1.router属性
el-menu 有一个很重要的属性,启用该属性后,跳转页面会以 index 作为 path 。 2.折叠菜单
在 el-menu 上绑定 collapse 属性,通过点击时修改该属性的布尔值控制菜单是否折叠展示。
3.激活当前菜单
记录当前激活的菜单有多种方式,这里使用 sessionStorage 存储当前激活的菜单。
刚进入后台主页,我们要在页面初始化之前判断 sessionStorage 是否存有激活的菜单,如果没有就默认进入首页。 然后为菜单项绑定方法,将路径信息存储在 sessionStorage 中。
3.header
头部信息主要用来展示管理员信息和操作。 管理员信息从 sessionStorage 中获取,因为 sessionStorage 存储的 key\value 都是字符串,所以这里需要转换成对象信息。 管理员的操作信息用到了 Dropdown 下拉菜单组件。
4.核心内容
核心内容主要包括面包屑
和要跳转的页面。 我们使用 router-view 路由占位符来显示对应的页面。 对于页面上的面包屑,我们采用的是 Element UI 的 Breadcrumb 组件。 因为每个页面都要展示面包屑,所以这里选择将面包屑组件再次封装,动态显示当前页面的路径。
首先在配置路由的时候,添加 meta.title 属性: 新建 Breadcrumb 组件 导入面包屑组件
5.footer
因为 footer 过于简单,所以这里不再赘述。
7.首页
登录成功后,默认进入后台首页。
也就是说路由默认要跳转到首页页面,左侧菜单默认要激活首页。
首先配置首页的菜单: 然后配置首页的路由。因为登陆成功后跳转的路径是:
this.$router.push("/home");
复制代码
所以我们需要将首页配置为主页的子路由,并且路由默认跳转到首页。redirect 是重定向的意思。
然后新建首页页面 Welcome.vue。 其实首页核心就是在页面初始化之前调用后台接口,获取首页数据,再优化样式。 因为我们全局配置了 echarts,所以这里可以直接创建方法渲染图表数据,当然啦这些数据也是后端返回的。最后再初始化 echarts 方法即可。 对于首页,我强烈建议大家多使用 element 的卡片组件,这样可以使页面更美观,更加扁平化。
8.列表
接下来我们将以课程管理
作为本次讲解的功能模块。
首先在 /home 的子路由下面配置课程管理的路由: 然后配置课程管理的菜单: 接下来在 view 目录下新建 course 文件夹,然后新建 Index.vue 文件。 根据经验,刚进入管理页面,我们看到的基本都是列表数据。这里呢我们将列表数据放到 el-card 里面进行展示。 因为列表数据基本都是分页的,这里我们需要用到 Element UI 的 Table 组件和 Pagination 组件。 分别复制粘贴 Table 组件和 Pagination 组件的代码: Table 组件:
el-table 标签上绑定的 data 属性是该列表的数据,也就是后台返回的数据集。
所以我们需要定义一个数据集变量,绑定在 data 属性上: el-table-column 代表每一列,label 是表头名称,prop 与后台返回的字段一一对应。
有时候后台返回的字段可能并不是我们想要的结果,例如页面要展示课程的状态为上架/下架
,但是后台返回的字段 status 是数值类型,这时候我们就可以使用域名插槽
来扩展字段的展示方式:
<el-table-column prop="stateName" label="状态" show-overflow-tooltip>
<template slot-scope="scope">
<div>
{{ scope.row.status == 0 ? "已下架" : 已上架 }}
</div>
</template>
</el-table-column>
复制代码
scope 可以看做是一个变量,scope.row 可以获取某一行的数据,scope.row.属性名
可以获取该属性的值。
Pagination 组件:
el-pagination 标签上绑定的 current-page 为当前页,page-size为每页显示的条数,total 为数据总条数。所以我们需要定义变量绑定到对应的属性上: el-pagination 标签上绑定的 size-change 事件为切换每页展示条数的方法,current-change 事件为切换当前页的方法。所以我们需要分别定义这两个方法: 刚进入管理页面,肯定要先调用后台接口获取数据,然后渲染页面。
所以我们需要定义获取列表的方法,然后在 vue 生命周期中的 created() 函数里面调用该方法,为 tableData 和 total 变量赋值: 因为列表页面默认展示的是第一页的数据,有时候我们需要跳转页面,或者切换页面展示条数,这时候就需要重新获取后台数据。 所以我们需要完善 el-pagination 上的两个方法:
// 切换每页显示条数
handleSizeChange(val) {
this.searchForm.size = val;
this.searchForm.current = 1;
this.getPageList();
},
// 点击某一页,跳转某一页
handleCurrentChange(val) {
this.searchForm.current = val;
this.getPageList();
},
复制代码
有时候查询列表数据需要过滤条件: 我们在表格的上面添加查询条件,这里用到了 Element UI 的 Layout 布局组件。
该组件将每一行分成 24 份。el-row 表示每一行,gutter 属性表示列之间的间隔。el-col 表示每一列,span 属性表示每一列占据的份数。其他关于行和列的属性大家可以查看官方文档,这里不再赘述。
我们在 el-col 里面放置 el-form 组件,用作查询条件。 同时在 data 里面新建查询变量: 然后绑定查询和重置方法:
//搜索
handleSearch() {
this.searchForm.current = 1;
this.getPageList();
},
//重置
handleClear() {
this.$refs["searchForm"].resetFields();
this.getPageList();
},
复制代码
上面重置方法用来重置查询条件: 其中 searchForm 是我们为该表单域起的别名,resetFields() 是组件自带的重置方法。 接下来我们再新增 el-row ,添加按钮组件:
<el-row class="rowSpace">
<el-col :span="8">
<el-radio-group
size="small"
@change="changeRadio($event)"
v-model="searchForm.state"
>
<el-radio-button label="">全部</el-radio-button>
<el-radio-button label="0">已上架</el-radio-button>
<el-radio-button label="1">已下架</el-radio-button>
</el-radio-group>
</el-col>
</el-row>
复制代码
为其绑定属性和方法:
// 切换tab
changeRadio(value) {
this.searchForm.state = value;
this.getPageList();
},
复制代码
在列表页面,我们实现了这样一个效果,鼠标悬浮图片,图片会放大展示: 这里用到了 Popover 弹出框组件: 以上就是一个列表页面基本的操作。
9.新增
我们在列表页面添加新增按钮
,这里用到了 Element UI 的按钮组件: 然后为其绑定跳转页面的方法:
// 路由跳转
changeView(url, queryParams) {
this.$router.push({
path: url,
query: queryParams,
});
},
复制代码
跳转页面的方法有两个参数,path 是跳转路径,query 是传递的参数。
接着添加新增课程的路由: 在 course 文件夹下新建 Add.vue 页面: 然后添加表单组件: 在 data() 里面新建变量绑定表单属性: 其中课程分类使用了 Element UI 的 Cascader 组件:
<el-form-item label="课程分类:" prop="category">
<el-cascader
placeholder="请选择课程分类"
style="width: 440px"
v-model="form.category"
:options="categoryList"
@change="handleChange"
></el-cascader>
</el-form-item>
复制代码
el-cascader 标签上面 options 属性绑定的是数据源。该数据源有固定的的格式: 当然一般这种数据源都是后台返回的: 如果后台返回的数据结构和该组件规定的不一致,就需要我们将后台返回的字段与组件规定的字段做映射: 上传文件组件我们用的是 Element UI 的 upload 组件: 因为上传文件组件在很多地方都会用到,所以这里将其封装成了一个组件: 主要就是绑定对应的方法: 封装完之后再在其他页面引入即可: 最后为表单绑定保存方法: 因为保存数据时需要校验数据的完整性,所以我们还需要为表单添加校验规则:
10.编辑
编辑和新增用的是同一个页面,不同之处就是编辑页面要先根据 id 查询数据并将数据回显到表单域中。
首先在列表页面添加编辑按钮,并绑定跳转路由的方法: 其中 scope.row.id 表示这一行的 id。
// 路由跳转
changeView(url, queryParams) {
this.$router.push({
path: url,
query: queryParams,
});
},
复制代码
然后添加路由: 在新增页面,created()生命周期里面,先判断路由参数 id 是否存在,如果存在就调用获取课程详情的方法,然后为 form 表单赋值。 Object.assign 方法用来拷贝对象。
在调用保存方法时,判断 id 是否存在,如果存在就调用更新方法,否则就调用新增方法。 在执行完方法后,要么跳转路径,要么提示信息。提示信息我们采用了 Element UI 的 Message 组件: 有时候我们需要返回到上一页
,常用的做法是路由跳转:
this.$router.push(url);
复制代码
如果相关联的页面比较少,可以使用以下方法:
this.$router.go(-1);
this.$router.back();
复制代码
这两个方法的作用相同,都是返回到上一个页面。如果上一个页面路由携带参数,使用这两个方法返回到的上一个页面的路由参数都会消失。但是使用$router.back(-1) 路由参数仍存在。
11.删除
我们在列表页面添加删除按钮: 然后为其绑定方法: 在删除方法中,我们使用了 MessageBox 弹框组件
12.详情
课程详情的展示采用了抽屉式的交互效果: 这里用的是 Element UI 的 Drawer 抽屉组件: 抽屉组件的显示由 visible.sync 属性绑定的变量控制,变量值为 true 就展示,否则不展示。 为了降低页面之间的代码耦合度,我将详情页封装成了一个单独的组件。
首先新建 Detail.vue 页面: 新建查询课程详情的方法: 在列表页面导入该组件: 为课程详情组件起一个别名:
ref="course_detail"复制代码
然后绑定详情方法:
<el-button size="small" @click="openDetail(scope.row.id)"
>详情</el-button
>
复制代码
openDetail(id) {
this.$refs.course_detail.drawer = true;
this.$refs.course_detail.formData.id = id;
this.$refs.course_detail.getCourseDetail();
},
复制代码
this.$refs.course_detail 表示拿到了子组件。父组件拿到子组件之后,就可以操作子组件的变量和方法了。这也是父子组件交互的一种方式。
上面 openDetail 方法做了三步操作:
- 修改子组件变量值,使子组件的抽屉组件
弹出来
。 - 为子组件查询详情的 id 赋值。
- 调用子组件查询详情的方法。
当然啦除了这种交互方式,我们还可以给详情页面配置路由。通过路由传参的方式将 id 传到另一个页面,然后在 created() 函数里面调用获取详情的方法:
13.退出系统
退出系统需要做两步操作:
- 清除缓存,因为缓存中一般包括用户信息和 token 信息。
- 跳转到首页
首先为退出操作绑定方法: 提醒一下,这里有一个坑。因为我们采用的是 Dropdown 下拉菜单组件,要想让绑定的方法起作用,@click 后面必须加 .native
。
添加退出方法:
到目前为止,我已经将登录、页面布局、重定向首页、记录激活菜单、列表、新增、修改、删除、查看详情、退出等操作全部讲完了。
掌握了这些,我觉得你差不多算是一个拥有一周 vue 工作经验的前端开发人员了。。哈哈哈。
恭喜你正式进入了 vue 的世界,接下来我还要讲一些比较琐碎但是很有用的东西。
14.全局变量和全局函数
在实际开发中,有些变量和函数需要全局使用,所以我们需要定义全局变量和全局函数,有利于提高工作效率。
全局变量
首先定义一个用来存放全局变量的 js,起什么名字都可以,这里我命名为 constants.js。然后将变量对外暴露出去:
然后在 main.js 导入该 js,并将其挂载到 vue 实例上: 使用全局变量:
{{$constant.officialAccount}}
复制代码
全局函数
这里以封装跳转路由的方法为例。首先定义一个存放全局函数的 js,这里我命名为 common.js,然后将函数对外暴露出去: 然后在 main.js 导入该 js,并将其挂载到 vue 实例上: 使用全局函数:
<el-button @click="$commonJs.changeView('/user/detail')">详情</el-button>
复制代码
15.全局样式
在项目开发中多个页面可能会用到相同的样式,我们可以采取将这些相同的样式都放在一个 css 文件中,然后在 main.js 中引用配置即可。
新建全局样式,这里我命名为 global.css 在 main.js 中引入:
16.写在最后
以上讲解的内容记录了我自学 vue 开发的一个过程,希望能够对屏幕前面的你有所帮助。
因为我主业是搞后台的,对于前端这一块我也是小白,我要学习的地方还有很多。所以如果有讲得不太好的地方,希望大家多多指正,我一定更加努力学习。
我觉得无论是学习前端也好、后端也罢,一定要记住三点:
- 1.多写技术文章,多记笔记
- 2.多写代码
- 3.多思考
最重要的是多写代码,因为过一个月不写代码,你基本都忘完了。多写技术文章是为了让你以后忘记的时候快速记起来。多思考是为了提高你的创新水平和代码水平。
最后放上该项目的完整代码,拿到代码之后记得要先配置 node 环境,安装 vue cli,然后 npm install 下载所有依赖包,最后再输入 npm run start 启动该项目。
链接: https://pan.baidu.com/s/1FPfY4kNyb1hSQwknk6FB3w?pwd=g11q
提取码: g11q
复制代码
来源:https://juejin.cn/post/7147456110562115597#comment