后台管理系统总结
后台管理系统
一、项目内容
1. 项目搭建
记录一下是怎么开始一个项目的
环境搭建
-
环境的配置包括两个方面:
-
node安装
主要作用是本地服务器呈现网页效果,方便开发
解决node下载慢的问题:
-
使用yarn
安装yarn
npm i -g yarn
-
安装国内镜像
由于node是访问的国外服务器,访问速度较慢
所以可以修改为国内淘宝镜像站
npm config set registry https://registery.npmmirror.com/
-
-
vue脚手架安装
脚手架用于快速创建项目的基本框架
通常使用yarn来配置依赖,因为yarn弥补了npm下载慢、版本不统一的问题
同时yarn会生成yarn.lock文件记录安装的依赖的相关信息等
-
-
注意安装的版本问题:
- vue-cli对node的版本限制:从官方文档查
- vue-cli本身的版本:版本的不兼容会导致编译错误
创建项目
-
创建命令
cmd
vue create <projectname>
-
注意点
-
不能驼峰命名
只能使用下划线或短线连接
-
当前项目使用的是vue2
-
-
文件目录
-
通常包含以下几个文件
dist:打包后的项目文件夹
public:通常放置网页的图标、主页面等
src:包含assets/components/routers
- routers存放路由,可以看成是路由表吧
- views存放页面
-
package.json
项目配置文件
-
scripts属性
自定义命令
-
-
安装依赖
主要是按照官方文档来
-
UI框架
项目中使用element-ui的框架,根据官方文档指示来安装
-
vue-router
项目中使用适应于vue3.x版本的router,按照vue官方文档安装,但注意指定版本
vue3.x适用于vue2
vue4.x适用于vue3
如果直接使用install指令,安装的是最新版本,可能无法兼容
查询当前版本的方法
- npm官网
- 官网搜索:vue-router并选择versions标签
- 在package.json中查看安装的依赖版本
2. eslint语法提示
-
问题
编译时抛出错误:组件name属性必须是组合词
-
原因
代码检查工具eslint的语法提示
eslint相关在后面
-
解决方法
在配置文件中关闭组件名称的语法提示
//package.json { ... "eslintConfig": { ... "rules": { "vue/multi-word-component-names": 0 } }, ... }
解决结果
-
相关&扩展
-
eslint为什么不能直接关掉?
eslint,代码检查工具,检查代码规范代码风格
对个人,可以帮助让代码更规范,可读性高了也容易维护
对团队,统一规范代码风格团队成员间更容易协作,代码也好维护
-
发现修改package.json后需要新建终端,否则仍会报错,为什么?
-
npm run serve发生了什么?
-> 输入指令npm run serve
-> npm在package.json的scripts中找对应指令
-> 实际上执行的是 npm run vue-cli-service serve 指令
这个命令本身的作用是启动一个本地开发服务器,用于开发和调试Vue项目
-> 此时,相当于用户和操作系统之间建立了一个对话窗口(称为shell)
用户输入这个指令,操作系统根据指令的路径(称为环境变量Path)去查找路径下的可执行文件并执行
在此之前,npm已经在环境变量Path加了一个前缀
node_modules/.bin
,即操作系统查找的实际路径是node_modules/.bin/vue-cli-service
,也即实际上在shell上实际执行的指令是node_modules/.bin/vue-cli-service serve
node_modules/.bin目录下对应的文件
- .cmd文件(可执行文件)
- .ps1文件
- .cmd文件(可执行文件)
-
延伸知识点:
-
node_modules/.bin目录下的文件都是干嘛的?
放置项目依赖的可执行文件,
npm install
所安装的依赖都放置在该目录下 -
可不可以用
npm run vue-cli-service serve
代替npm run serve
呢?不行,npm中没有这个指令,注意上面说的原理“去package.json”中找对应指令
但是可以直接在命令行执行
node_modules/.bin/vue-cli-service serve
指令,因为npm run serve
本质就是它
-
-
解释原问题:修改package.json后为什么得新建终端才行?
新建终端,在这里实际上是关掉了原本开启的用于调试的本地服务器,然后重新运行
执行
npm run serve
指令后,会启动一个本地开发服务器,该服务器会监听文件的变化,文件变化时会重新编译打包项目并刷新页面但是
package.json
文件只会在执行npm
指令时生效,不会对正在运行的本地开发服务器产生影响
npm run serve
打包的文件包括页面文件、组件文件、入口文件,以及package.json
中配置的依赖文件,也会被打包进代码文件
package.json
文件本身不会被打包进代码文件,因此修改了其中的配置需要重新执行指令
-
-
项目运行流程
对项目文件运行的顺序流程和为什么需要分成这么多文件好奇
-
项目文件运行顺序
-
index.html
vue项目入口文件:
作用:页面基本结构&引入打包后的JS和CSS文件
位置:在目录的public路径下
文件内容:下面文件代码是由vue创建项目时的初始状态,即还未经过
npm run build
打包的项目入口文件<!--public/index.html--> <!DOCTYPE html> <html lang=""> <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"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
文件分析:
-
vue实例挂载点:
<div id="app"></div>
是vue应用程序的挂载点,也就是vue实例的挂载点;在main.js文件中的new Vue({ router, render: h => h(App), }).$mount('#app')
就是将vue实例挂载到index.html的这个挂载点上
-
noscript
标签<noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript>
浏览器不支持JS或者用户禁用JS时会将这个标签的内容显示在页面,提示用户需要启用JS才能正常使用该网站
项目标题的替换是webpack插件html-webpack-plugin完成的
-
引入JS文件?
-
何时生成?
用脚手架创建项目时,自动生成index.html文件;
而只有在
npm run build
打包项目代码时才会在index.html中引入JS文件;所以在文件初始状态下,看不到<script src="/dist/js/app.js">
语句引入JS文件 -
引入了什么?
-
-
-
main.js
作用:项目的主文件&入口文件之一
内容:Vue实例化&挂载、引入路由、引入插件、引入组件库等
类似于一本书的目录和致读者的东西,介绍了这个项目有啥、用了啥
文件内容:大概看个框架吧
//src/main.js import Vue from 'vue' import App from './App.vue' //引入路由 import router from './routers' //引入组件库 import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.config.productionTip = false; Vue.use(ElementUI); //创建实例 new Vue({ router, render: h => h(App), }).$mount('#app')
-
App.vue
作用:项目的根组件
内容:页面基本结构&路由视图的渲染
我的理解:其实就是主页面,可以理解成index.html是画板,app.vue是画纸,其他组件就是画纸上的东西
不过可以在这个文件中规定一些全局的CSS样式
在vue3中就将app和vue实例融合了,不用多套一层,更简洁一些
文件内容:
<!--src/App.vue--> <template> <div id="app"> <router-view></router-view> </div> </template> <script> export default { name: 'App', components: { } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
-
路由组件
作用:根据路由的配置进行加载和渲染
我的理解:因为main.js中引入了路由组件,即引入了routers文件夹,所以会自动去执行/routers/index.js文件
这个步骤应该是和App.vue几乎同时进行的,因为App.vue需要使用到路由映射
index.js文件如下
//src/routers/index.js import Vue from 'vue' import VueRouter from 'vue-router' import Main from '../views/Main.vue' import Home from '../views/Home.vue' import User from '../views/User.vue' Vue.use(VueRouter) //路由映射 export default new VueRouter({ routes: [ { path: '/', component: Main, children: [ { path: '/home', component: Home }, { path: '/user', component: User } ] } ] })
-
其他组件&其他JS文件&CSS文件
作用:根据需求进行加载和渲染
-
-
思考&拓展
-
render函数完整写法?
见 H1-VueRouter函数再认识
-
npm run build发生了什么?
见下打包项目过程
-
3. 项目打包过程
npm run build怎么打包&给index.html写了什么
- 清空之前打包生成的文件夹,一般是
dist
文件夹 - 根据配置文件
vue.config.js
中的设置,进行静态资源的打包,如图片、字体等 - 通过 webpack 对项目中的代码进行编译打包,生成 JavaScript 代码文件、CSS 文件、HTML 文件等
- 对打包生成的文件进行压缩和优化,如使用 Gzip 压缩等
- 最终将打包生成的文件输出到指定的文件夹中,一般是
dist
文件夹 - 执行
npm run build
后,我们就可以将打包生成的文件部署到服务器上,使其能够被用户访问
4. 组件库产生黑盒问题
现成的组件框架由于某些接口原理是未知的,所以会产生一些莫名的问题
自带样式
-
问题
- 问题1:用UI库的时候发现有些组件由默认样式但是找不着改的地方,发现用组件文件的style标签中也没有,但是渲染到页面上就会添加上这个样式
- 问题2:在组件文件的style标签中设置该样式并不会生效
源代码
<!--views/Main.vue--> <template> <div> <el-container> <el-aside> <common-aside /> </el-aside> <el-container> <el-header> <common-header /> </el-header> <el-main> <router-view></router-view> </el-main> </el-container> </el-container> </div> </template> <script> //引入组件库中的侧边栏组件 import CommonAside from "../components/CommonAside.vue"; import CommonHeader from "../components/CommonHeader.vue"; export default { name: "", components: { CommonAside, CommonHeader }, } </script> <style> </style>
-
分析
-
问题1:莫名其妙的样式哪里来?
UI库会将某些组件的样式作为一个属性并且赋默认值,例如
el-aside
组件将width
作为了一个属性,因此并不是用style="width: 200px"
的形式设置样式查看官方文档也可知
因此,就算把
el-aside
中的width
属性删掉,这个组件还是会有300px的宽从开发者面板也可知
-
问题2:为啥用style改不掉?
chatGPT这样解释
如果组件库将组件 A 的宽度作为一个属性,并将其默认值设置为 300px,则这个属性很可能是使用 CSS 变量或者 JavaScript 代码实现的,而不是使用普通的 CSS 属性(例如 width)。如果是使用 CSS 变量实现的话,那么你需要使用该变量名来修改组件的宽度。
例如,在使用 CSS 变量时,你可以在根元素中定义一个变量 --component-a-width 来表示组件 A 的宽度,并在组件的样式表中使用该变量来设置宽度。在这种情况下,要将组件 A 的宽度设置为 200px,你应该这样做:
:root { --component-a-width: 200px; } .component-a { width: var(--component-a-width, 300px); /* 使用 CSS 变量 */ }
上面的代码中,我们在根元素中定义了一个名为 --component-a-width 的变量,并将其值设置为 200px。然后,我们在组件 A 的样式表中使用 var() 函数来引用这个变量,并将其作为组件宽度的值。由于我们已经在 :root 中为该变量设置了默认值 300px,所以如果没有给该变量赋值,组件 A 的宽度将自动设置为 300px。
另外,值得注意的是,如果你直接在组件的样式表中使用 width 属性来设置组件的宽度,这可能会被组件库内部的 CSS 样式所覆盖,导致你的样式无法生效。如果已经确认组件库是以这种方式封装组件的话,建议在阅读组件文档时了解其提供的修改组件样式的方法,并按照组件库的规范进行设置。
因此,直接使用如下的方式是无法生效的
<style> .el-aside{ width: auto; } </style>
开发者面板中可以发现自己设置的样式并没生效
-
-
解决方法
按照UI库的规矩来,用
width
属性设置<el-aside width="auto"> <common-aside /> </el-aside>
所以还是要多查官方文档
注意:
当组件库没有把样式封装成组件的一个属性时,通常还是可以直接通过CSS进行样式修改的,就是说在style中进行修改,可以通过开发者面板查看样式
-
延伸
CSS3中的自定义变量
组件的隐藏
-
问题描述
-
前提:
-
使用组件库的弹窗
dialog
组件时,组件有自带一个visible
属性,用于设置组件是否可见 -
弹窗组件内有一个表单
form
组件,可以用于提交数据 -
表单组件用
ref
属性进行绑定,便于在js中对其进行操作 -
表单组件内数据用对象
form
进行双向数据绑定 -
只有当点击页面的增加和编辑按钮时,才会将弹窗的
visible
属性值改为true
,即弹窗可见;其他时候弹窗隐藏 -
现在的需求是:每次关闭弹窗时,都要将弹窗中的表单的数据清空;关闭弹窗的方式包括点击弹窗的取消和确定按钮,以及弹窗右上角的叉叉
<!--弹窗内嵌表单的template内容--> <!-- 新增按钮点击后出现的表单:用于新增表格数据 --> ... <!--dialogFormVisible用来控制表单是否显示--> <el-dialog title="新增课程" :visible.sync="dialogFormVisible" :before-close="handleClose" > <el-form :model="form" :rules="rules" ref="form"> <el-form-item label="课程名称" :label-width="formLabelWidth" prop="name" > <el-input v-model="form.name" autocomplete="off"></el-input> </el-form-item> <el-form-item label="课程时间" :label-width="formLabelWidth" prop="date" > <el-select v-model="form.date" placeholder="请选择时间"> <el-option label="星期一" value="Mon"></el-option> <el-option label="星期二" value="The"></el-option> <el-option label="星期三" value="Wed"></el-option> <el-option label="星期四" value="Thu"></el-option> <el-option label="星期五" value="Fri"></el-option> <el-option label="星期六" value="Sat"></el-option> <el-option label="星期日" value="Sun"></el-option> </el-select> </el-form-item> <el-form-item label="课程地点" :label-width="formLabelWidth" prop="region" > <el-select v-model="form.region" placeholder="请选择地点"> <el-option label="A栋教学楼" value="A"></el-option> <el-option label="B栋教学楼" value="B"></el-option> <el-option label="C栋教学楼" value="C"></el-option> <el-option label="D栋教学楼" value="D"></el-option> <el-option label="E栋教学楼" value="E"></el-option> <el-option label="F栋教学楼" value="F"></el-option> <el-option label="G栋教学楼" value="G"></el-option> </el-select> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="dialogFormVisible = false">取 消</el-button> <el-button type="primary" @click="submitForm">确 定</el-button> </div> </el-dialog> ...
//弹窗相关js部分 export default { data() { return { //提交表单的相关配置 dialogFormVisible: false, formLabelWidth: "120px", //存放表单数据的form对象 form: { name: "", date: "", region: "", }, //表格数据 tableData: [], ... }; methods: { ... //新增-新增按钮 handleAdd() { this.dialogFormVisible = true; this.resetForm(); }, //提交表单数据-弹窗的确定按钮 submitForm() { //检验表单是否有输入 this.$refs["form"].validate((valid) => {、 //关闭弹窗 this.dialogFormVisible = false; ... //一堆对请求操作 //将表单清空 this.resetForm(); }); //关闭弹窗前的操作-弹窗的叉叉 handleClose() { this.dialogFormVisible = false; this.resetForm(); }, //清空表单数据 resetForm() { //利用ref属性获取form元素节点进行清空操作 this.$refs["form"].resetFields(); }, }, };
-
-
发现问题:
当点击新增按钮或者关闭弹窗等会报错:表示找不到
this.$ref
下的form
-
-
分析
-
那
this.$refs.form
究竟是啥?-
查看vue的开发者工具,发现当弹窗的
dialogFormVisible
为true时,页面中不存在弹窗及其内嵌的表格元素即组件库将组件隐藏的方法不是设置css样式
display: none
,而是通过将组件从页面中移除那么表单组件不存在,
this.$refs["form"]
也不会存在,对表单的清空操作也实现不了 -
所以,清空表单和关闭弹窗的操作要有先后顺序
即先清空表单再关闭弹窗
//以其中方法一个为例 submitForm() { //检验表单是否有输入 this.$refs["form"].validate((valid) => { ... //一堆对请求操作 //先将表单清空 this.resetForm(); //再关闭弹窗 this.dialogFormVisible = false; }); //新增-新增按钮则相反 handleAdd() { //先有弹窗容器再清空表单 this.dialogFormVisible = true; this.resetForm(); },
-
-
还是会有问题?
因为弹窗的出现需要重新解析模板,再将组件dom挂载在页面上
当我们获取
this.$refs
时,可能dom节点还没有完全加载完成所以需要使用
this.$nextTick(() => {})
,使页面渲染好再清空数据//清空表单数据 resetForm() { console.log(this.$refs) this.$nextTick(() => { //等渲染好了再找这个节点 this.$refs["form"].resetFields(); }); },
-
接口歧义
-
问题描述
-
接上一个问题,同样出现了form没有被清空的问题
-
比如,先点击了表格中张三这一行的编辑按钮,按钮绑定的事件会将张三的信息填在表单form中
修改张三信息并提交,再点击新增按钮,发现表单form中出现了修改前的张三信息
-
-
分析
个人认为是接口的歧义问题
查看官方文档,我所使用的重置表单数据的方法是将表单数据重置为初始值
可能的逻辑是,因为一开始第一次点击的按钮时编辑按钮,编辑按钮对应的事件将张三数据赋给了form表单
而组件就将这个数据当做了form的初始值,因此每次调用重置方法后都会将表单填上这个初始值
-
解决方法
手动清除表单
//User.vue ... //清空表单数据 resetForm() { //等渲染好了再找这个节点 this.$nextTick(() => { //手动清空 for (let key in this.form) { this.form[key] = ""; } }); },
5. 组件名使用规范
模板中使用组件时,注意代码规范
-
模板
<template>
中使用组件即应用到dom时
模板使用组件:名称用统一小写加短线代替驼峰
即使用
<common-aside>
而不是<CommonAside>
<template> <div class=""> <el-container> <!-- 侧边栏 --> <el-aside width="auto"> <common-aside /> </el-aside> </el-container> </div> </template>
-
<script>
中使用组件引入时仍可以使用驼峰
import CommonAside from '../components/CommonAside.vue' export default { ... components: { CommonAside }, ... }
-
分析
原因是html对大小写并不敏感,在解析dom模板时,会将所有大写的字符转化为小写
-
如果使用
CommonAside
,则会转化为commonaside
而注册组件时是使用
CommonAside
,则会报错组件找不到 -
如果使用
common-aside
,则能够对应上注册的组件
-
6. 样式穿刺
感觉不是一种特别经常使用的写法,而且会导致代码的样式层叠杂乱,所以还是避免使用
-
问题描述
使用了第三方组件库的组件,要修改组件的样式,发现无法生效
<!--src/components/CommonHeader.vue--> <template> <!-- 面包屑显示的路径 --> <el-breadcrumb style="margin-left: 20px" separator="/"> <el-breadcrumb-item v-for="item in tags" :key="item.path" :to="{ path: item.path }" > {{ item.label }} </el-breadcrumb-item> </el-breadcrumb> </template> <style lang="less" scoped> .el-breadcrumb__item { .el-breadcrumb__inner { font-weight: normal; &.is-link { color: #666; } &:last-child { .el-breadcrumb__inner { color: white; } } } </style>
-
分析
-
在vue中为了避免多文件中的组件起了相同的class类名而产生样式污染的问题,会在style标签中添加
scoped
属性,表示该样式只针对当前文件的组件生效 -
而vue解析、渲染模板的最后是将所有文件打包成一个html文件,为了区分不同文件的相同组件,vue会给有重复的组件添加特殊的标识符,类似于身份证的字符串
如上面的
data-v-c1f1971a
字符串等 -
如果使用了第三方组件库中的组件,样式是由组件库方写好的,所以vue并没有再给组件内的标签再添加标识符
检查项目中的代码
发现
el-breadcrumb_inner
并没有标识符,因此仅仅通过选择器.el-breadcrumb_inner
给它设置样式是没办法生效的,只有组件库默认的样式
-
-
解决方案
使用深度选择器,即样式穿透/样式穿刺写法,在需要穿刺的选择器前面添加字段
/deep/
/deep/.el-breadcrumb__item { .el-breadcrumb__inner{ font-weight: normal; &.is-link { color: #666; } } &:last-child { .el-breadcrumb__inner { color: white; } } }
添加样式穿刺标识的选择器对应的组件,其子组件都会被穿刺,也就是说,对自身和其子组件的样式都会生效
因此,在此处需要添加到最外层的选择器
.el-breadcrumb__item
,这样其子组件.el-breadcrumb__item
和 &:last-child
都会生效注意其中的
&
符号表示其父组件,等同于css中的.el-breadcrumb__item :last-child
写法
7. 登录权限&token验证
token验证
用mock生成token,用js-cookie验证token
-
用插件
js-cookie
生成验证的token
-
安装插件
npm i js-cookie
项目中使用的版本是3.0.1
-
引入并使用插件
在登录页面中使用
//src/views/Login.vue import Mock from 'mockjs' import Cookie from 'js-cookie' export default { data() { return { //登录的信息 form: { username: "", password: "", }, ... }; }, methods: { //登录请求 handleLogin() { this.$refs["form"].validate((valid) => { //valid表示输入框有输入 if (valid) { login(this.form).then(({ data }) => { //status==0表示登录成功 if (data.status == 0) { //生成token并将token存入浏览器的cookie中 //用Mock生成一段随机字符串 const token = Mock.Random.guid() //将token存入cookie Cookie.set('token', token) //跳转到主页面 this.$router.push({name: 'home'}) } //登录失败 else { alert("账号或密码错误"); } }); } //输入框没有输入,提示用户输入 else { console.log("error submit!!"); return false; } }); }, }, };
-
配置守卫导航
用于拦截跳转的操作,检查是否有token
//src/main.js //引入cookie import Cookie from 'js-cookie' //添加全局前置导航守卫,要写在实例挂载之前 //判断cookie是否存在 router.beforeEach((to, from, next) => { const token = Cookie.get('token') //token不存在且要跳转的页面不是登录页面,则跳转至登录页面 //防止在登录页的死循环跳转 if (!token && to.name != 'login') { next({ name: 'login' }) } //token存在且是从登录页面来的 //只有是在登录页面发出登录请求才符合条件 else if (token && to.name == 'login') { next({ name: 'home' }) } //其他情况 else { next() } }) ... new Vue({ router, store, render: h => h(App), }).$mount('#app')
-
不同用户不同访问权限
导航栏显示不同内容
-
实现的基本逻辑
-
用户登录账号,根据后端返回的用户和管理员身份区分码,确定身份
在这个项目中的逻辑是后端直接返回应该显示的导航栏数据,我认为不对,这部分数据应该是由前端来写的
-
将身份对应的导航栏内容存入store和Cookie中
存入Cookie中是因为store.state的数据会因为浏览器刷新而重新初始化,所以要存入缓存,后面会详细说明
-
在导航栏组件中读取导航栏内容
-
-
实操
-
编写store中用于缓存导航栏数据的mutations
//store/tab.js import Cookie from 'js-cookie' export default { state: { menu: [] }, mutations: { //不同用户的不同menu setMenu(state, menu) { state.menu = menu; //将菜单存入浏览器缓存中,防止每次刷新后数据丢失 //注意要将数据转化为字符串 Cookie.set('menu', JSON.stringify(menu)) } } }
//store/index.js import Vue from 'vue' import Vuex from 'vuex' import tab from './tab' Vue.use(Vuex) export default new Vuex.Store({ //模块化管理 modules: { tab } })
-
用户登录,并存入到导航栏数据
注意此处的导航栏数据是由后端返回
//Login.vue import { login } from "../api/index" import Cookie from "js-cookie" export default { ... methods: { //登录请求 handleLogin() { //发送登录请求 login(this.form).then(({data}) => { ... //登录成功后 //将不同身份对应的不同菜单栏项存入store.tab中 this.$store.commit('setMenu', data.menu); //跳转到页面 this.$router.push({ name: "home" }); ... //后续其他处理 }) }, }, };
其中,data数据段
-
在导航栏组件中获取导航栏数据
//src/components/CommonAside.vue export default { ... computated: { //不同用户不同导航栏呈现 menuData(){ //判断当前数据是否在缓存中 //先在缓存中找,缓存中有说明已经登录过; //缓存中没有,说明是初次登录,菜单栏存在state中 //注意要将字符串转回js对象数组 return JSON.parse(Cookie.get('menu')) || this.$store.state.tab.menu } } }
-
-
分析
为什么需要将数据存在Cookie缓存?
-
store中的state是保存在内存中的,只要浏览器页面不被关闭,它们的值就会一直存在
-
刷新浏览器页面,store中的state数据将会被初始化为初始值
因为在刷新浏览器时,页面和所有已加载的JavaScript代码都会被重新加载,包括Vuex store实例
因此,当store实例被重新创建时,其中的状态(state)也会被重新初始化为初始值
-
为了避免在刷新浏览器后丢失store的state数据,可以使用一些技术来将其持久化存储
如使用浏览器的本地存储(localStorage)或其他外部存储库
这样可以确保在下一次加载页面或重新打开浏览器后,数据仍然存在并可以被恢复到之前的状态
-
限制部分路由的跳转
动态注册路由
-
问题描述
-
不同的登录身份会呈现不同导航菜单,也只能跳转特定的页面
-
也就是说对
VueRouter()
中route
的配置(即路由表)不能是写死的,而是先验证身份,再动态配置的//src/routers/index.js import Vue from 'vue' import VueRouter from 'vue-router' export default new VueRouter({ routes: [] //里面不能写死数据,不然什么身份都能通过修改路径进行页面跳转 })
-
-
解决方案
使用vue封装好的动态注册路由的接口,动态添加路由到路由表中
const userMenu = [ { path: "/home", name: "home", label: "首页", icon: "s-home", url: "Home/Home", }, { path: "/user", name: "user", label: "用户管理", icon: "user", url: "UserManage/UserManage", }, ]; //动态添加路由表内容 router.addRoutes(userMenu)
需要注意页面刷新后vue实例会被重新初始化,可能会出现路由表被重置为初始值的情况
所以需要在Vue实例被创建后,再重新调用方法来获取路由表
//src/main.js ... new Vue({ router, store, render: h => h(App), create(){ router.addRoutes(userMenu) } }).$mount('#app')
8. 拉取项目依赖版本问题
-
问题描述
在github中找到一个较好的后台管理项目vue-admin-beautiful,在拉取到本地安装依赖时出现错误
-
分析
-
package.json
中指定的版本号是不确定的,初次安装依赖时会导致可能安装到最新版本 - 而新版本的A可能与旧版本的B不兼容,因此会报错
-
-
解决1
根据搞错提示调整版本
至于应该调整到什么版本比较好,实在不太明白,目前也没想到好方法
-
查看报错信息
就是在说,发现
echarts
版本和vue-echarts
版本不太配,要么升级一个要么降一个最后三句是在说暴力破解,啥也不管,直接强制使用当前版本、使用旧版本,接收错误的解析结果
一般不推荐这些方法,可能导致其他问题
-
查找适合的版本
在npm官网查找相应的版本信息,搜索框输入要查找的插件名/模块名
选择
versions
一项查找合适的版本 -
进行版本修改
两种方法:删除原模块再重新安装或修改
package.json
中的版本信息再安装
-
-
解决2
解决1虽然听着行得通,但是实际操作时会牵一发而动全身,引发更多问题导致最后没办法运行
原因是本质上我不知道到底哪个版本才是适合的,胡乱升降版本是没有依据的
最后的解决方法。。。
删除package.json中版本前的
^
符号 + 使用报错信息给的指令$ npm i --legacy-peer-deps
前一个指令则不行,仍报错
-
拓展
版本控制符号
-
~
符号表示只允许安装到指定版本的最新小版本更新
~1.2.3
表示安装从1.2.3
开始到1.3.0
(不包括1.3.0
)之间的最新版本 -
^
符号表示只允许安装到指定版本的最新大版本更新
^1.2.3
表示安装从1.2.3
开始到2.0.0
(不包括2.0.0
)之间的最新版本。
-
二、JS/ES6/vue
1. != 与 !==
-
两个都是用于比较两个值是否不相等的操作符
-
!=
不严格相等运算符:如果两个值在类型转换后不相等,则返回true
- 1 != "1"将返回false,因为在比较之前,"1"被转换为数字1
- null != undefined将返回false,因为这两个值在布尔上下文中被视为相等
- "hello" != true,将返回true,因为在比较之前,true被转换为数字1,而"hello"被转换为NaN(无法转换为数字),这两个值不相等。
-
!==
严格不相等运算符:**比较两个值的类型和值,**只有在两个值的类型和值都不相等时才会返回true
- 1 !== "1"将返回true,因为它们的类型不同
- null !== undefined也返回true
2. 解构赋值
解构赋值的基本语法包括使用花括号 {}
或方括号 []
来匹配要提取的数据结构,并将其赋值给对应的变量
-
数组解构赋值
// 基本语法: let [a, b] = [1, 2]; console.log(a); // 输出 1 console.log(b); // 输出 2 // 可以忽略其中某些元素: let [c,,d] = [3, 4, 5]; console.log(c); // 输出 3 console.log(d); // 输出 5 // 支持剩余运算符 ... let [e, ...f] = [6, 7, 8, 9]; console.log(e); // 输出 6 console.log(f); // 输出 [7, 8, 9]
-
对象解构赋值
// 基本语法: let {foo, bar} = {foo: "hello", bar: "world"}; console.log(foo); // 输出 "hello" console.log(bar); // 输出 "world" // 也可以使用别名: let {name: n, age: a} = {name: "Tom", age: 20}; console.log(n); // 输出 "Tom" console.log(a); // 输出 20 // 可以使用默认值: let {x = 0, y = 0} = {x: 1}; console.log(x); // 输出 1 console.log(y); // 输出 0
-
注意点
-
key的匹配
被解构的对象中的key和被赋值的对象中key不一定需要完全相同,但是它们需要匹配
即如果被解构的对象中存在一个key,而被赋值的对象中不存在该key,则该变量将被赋值为
undefined
const person = { name: 'John', age: 30 }; const { name, gender } = person; console.log(name); // 输出 'John' console.log(gender); // 输出 undefined
上面的代码将
person
对象进行解构,并将结果赋值给name
和gender
变量由于
person
对象中不存在gender
这个key,所以gender
变量的值为undefined
-
3. render函数
-
问题
在main.js中的render函数作用是什么
//src/main.js ... new Vue({ router, render: h => h(App), }).$mount('#app')
-
分析
-
上面这段代码是Vue的构造函数,Vue构造函数传入一个对象作为参数
//传入的对象 { router, render: h => h(App), }
该对象中包含el/data/components等的Vue实例的选项和配置,而render就是配置之一,只不过render配置的是一个函数,而其他可能是配置布尔值、对象、数组等
-
render函数,渲染函数,用于帮助Vue创建虚拟DOM节点
-
render函数完整写法
render: function (createElement) { return createElement(App) }
其中,render配置的是一个函数,称为render函数;
render函数再传入一个函数
createElement
作为参数,这个参数实际上是Vue内置的函数,可以传入一个组件或html标签,返回值为一个虚拟DOM(称为VNode)返回的虚拟DOM被Vue接收,Vue将其转化成真实DOM插入页面中
而render函数常用简写形式:
render: h => h(App)
其中h即
createElement
函数,参考箭头函数简写方式 -
Vue渲染页面的流程中,vue先解析模板,将指令、插值表达式等转换成虚拟DOM节点;
然后Vue再根据虚拟DOM节点创建一个虚拟DOM树(每个组件都有虚拟DOM树);
而由于组件之间相连,最终都归到App这一个组件中去了,所以最终传入createElement的只有App组件;也就是说App组件是根,其他组件都与根相连,App被插入页面,其他组件也会被带动插入页面
-
4. 常用的数据处理方法
见Home.vue
查找元素
-
arr = Object.keys(object)
将可迭代对象中的键值对的键取出组成数组并返回
任何可迭代对象都可以使用该方法
const orderData = { 'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5 }; const xAxis = Object.keys(orderData); //则 xAxis = ['A', 'B', 'C', 'D', 'E']
-
arr.forEach((item)=>{ })
遍历数组,传入参数为一个回调函数,当前遍历的项会被传入该回调函数
该方法没有返回值
const arr = []; const xAxis = ['A', 'B', 'C']; const xAxis.forEach((item) => { arr.push({ name: item, data: 111 }); }); // arr = [ {name: 'A', data: 111}, {name: 'B', data: 111}, {name: 'C', data: 111} ]
-
Object.map((item)=>{ })/arr.map((item)=>{ })
也是遍历数组的方式,类似于
forEach()
,不同的是这个方法有返回值,返回新创建的数组const arr = [1, 2, 3, 4] const num = arr.map((item) => item * 2); // num = [1, 2, 3, 4]
-
const index = arr.findIndex(var => var.name == name)
根据特定条件查找元素并返回元素下标
当匿名函数中的判断成立时,会返回一个该元素的index
const name = 'B' const arr = [ {name: 'A', data: 111}, {name: 'B', data: 111}, {name: 'C', data: 111} ] const index = arr.findIndex((var) => var.name == name) // index = 1
删除元素
-
arr.splice(1,1)
删除从下标为1开始的1个元素
返回值为被删除的元素构成的数组
const arr = [0, 1, 2, 3, 4] arr.splice(1, 2) // arr = [0, 3, 4]
筛选元素
-
arr.filter()
筛选数组中符合条件的元素集合
参数为一个回调函数,被遍历的当前元素会作为参数传入该回调函数
const numbers = [1, 2, 3, 4, 5, 6]; const evenNumbers = numbers.filter(function(number) { return number % 2 === 0; }); // evenNumbers = [2, 4, 6]
三、axios/node.js
1. Promise的then回调机制
axios是基于promise的机制:使用时发现对promise仍不够熟悉
-
promise理解&在axios中应用
-
在axios中会定义一个请求函数,返回值类型为promise对象,例如
export const getData = ()=>{ return app.get('/home/getData') //此处已经定义了baseURL,所以直接写路径 }
-
promise对象本质由两个函数构成:
resolve
和reject
,resolve
在promise状态为fulfilled
(异步操作成功)时被调用,返回数据;reject
在promise状态为rejected
(异步操作出错)时被调用,返回出错信息见promise的构造函数
let myPromise = new Promise((resolve, reject) => { ...// 异步操作 let result = performAsyncOperation(); //异步操作后的结果 if (result) { resolve(result); // 将 Promise 对象状态设为 "fulfilled",并返回异步操作的结果 } else { reject(new Error("Operation failed!")); // 将 Promise 对象状态设为 "rejected",并返回错误信息 } });
-
因此,上面例子中
getData
请求数据操作即为promise中的异步操作,请求成功函数则会返回后端给的数据,请求出错则会返回错误信息;通常把getData
也叫作一个接口
-
-
then的回调机制?
-
then接收两个回调函数
promise.then( res=>{ ...//对res操作 }, rej => { ...//对rej操作 })
-
promise调用then方法后会根据promise的状态调用then中的回调函数并将数据传给该回调函数
promise为
fulfilled
:调用第一个回调,将resolve
返回值传给这个回调;即res
为resolve
的返回值promise为
rejected
:调用第二个回调,将rejected
返回值传给这个回调;即rej
为rejected
的返回值
-
2. axios二次封装
就是对axios进行通用配置,以及将axios的各个部分功能分成模块
-
全局配置
src/utils/request.js,其中utils文件夹下是工具模块,放的都是工具项目的类似于入口文件的东西
//src/utils/request.js //axios的通用配置封装 import axios from 'axios' //进行axios请求的配置 const http = axios.create({ //通用请求的地址前缀 baseURL: '/api', //请求超时 timeout: 10000 }) //配置拦截器 //请求拦截器 http.interceptors.request.use(function (config) { return config; }, function (error) { return Promise.reject(error) }) //响应拦截器 http.interceptors.response.use(function (config) { return config; }, function (error) { return Promise.reject(error) }) //对外暴露 export default http
其中,添加拦截器的作用是什么?
这个项目中只是进行了默认配置,config即为默认的请求配置
- 请求拦截器:发送请求前进行操作,比如在请求头携带token等
- 响应拦截器:接收响应前进行操作,比如对数据进行统一处理、检查响应码并在响应码为401这些时进行统一跳转操作等
-
接口封装
src/api/index.js:将项目中所有要用到的发起请求的操作都封装在这个文件中,其他所有要发起请求的地方直接调用这些封装好的函数中即可(类似于前端的接口)
//src/api/index.js //定义前端的请求接口 //也就是说,将每一种数据请求放在该文件中,而不是放在页面代码中 //组件只需要引入该文件就能够调用函数来请求数据 import http from '../utils/request' //登录页面 //Login.vue使用 export const login = (data) => { return http.post('login/login', data) } //请求首页几个图表的数据 //Home.vue使用 export const getData = () => { //返回一个promise对象 return http.get('home/getData') } //用户管理页面的接口 //User.vue使用 export const getTableData = (params) => { return http.get('user/getTableData', params) } export const addTableData = (data) => { return http.post('user/addTableData', data) } export const editTableData = (data) => { return http.post('user/editTableData', data) } export const delTableData = (data) => { return http.post('user/delTableData', data) }
此处发起的请求路径和参数等的配置是在Mock模拟后端数据模块,这里只需要知道用了
axios.get()
和axios.post()
等axios接口发起请求即可组件使用封装好的请求接口:引入接口 + 调用函数并对返回的promise进行操作
import { getData } from '../api/index' export default { ... mounted() { //将返回的数据中的data解构出来 getData().then(({ data }) => { //拿出所有有用的数据 const val = data; ... } ... }
3. Mock模拟后端接口
模拟数据&接口
用mock.js工具来模拟后端的数据接口
此处模拟创建一个get和post请求
-
安装mock
npm i mockjs
-
写后端数据
-
将数据模块化
在src下新建文件夹mockServerData,文件夹中存放的数据文件
每一个文件都对应一个请求要返回的数据
-
写数据文件
//src/api/mockServerData/home.js //存放要给home页面的数据 import Mock from 'mockjs' //图表数据 let List = [] export default { getStatisticalData: ()=>{ //Mock.Random.float随机生成数字 for(let i = 0; i < 6; i++){ List.push( Mock.mock({ 数据结构: Mock.Random.float(3.0, 5.0, 0, 0), 操作系统: Mock.Random.float(3.0, 5.0, 0, 0), 计算机组成: Mock.Random.float(3.0, 5.0, 0, 0), 计算机网络: Mock.Random.float(3.0, 5.0, 0, 0), 编译原理: Mock.Random.float(3.0, 5.0, 0, 0), }) ) } return { code: 20000, data: { //饼图 videoData: [ { name: '德', value: 96 }, { name: '智', value: 89 }, { name: '体', value: 83 }, { name: '美', value: 89 }, { name: '劳', value: 83 } ], //柱状图 userData: [ { date: 'MON', new: 5, active: 200 }, { date: 'TUE', new: 8, active: 560 }, { date: 'WED', new: 6, active: 340 }, { date: 'TUE', new: 3, active: 150 }, { date: 'FRI', new: 5, active: 490 }, { date: 'SAT', new: 4, active: 180 }, { date: 'SUN', new: 5, active: 250 }, ], //折状图 orderData: { date: ['2-1', '2-2', '2-2', '3-1','3-2'], data: List }, tableData: [ { name: '数据结构', today: 1, month: 2, total: 3 }, { name: '操作系统', today: 4, month: 5, total: 6 }, { name: '计算机组成', today: 7, month: 8, total: 9 }, { name: '计算机网络', today: 10, month: 11, total: 12 }, { name: '编译原理', today: 13, month: 14, total: 15 } ] } } } } //src/spi/mockServerData/login.js //用户登录页面返回的数据 const admin = { massage: '登录成功(管理员)', status: 0, menu: [ { path: "/home", name: "home", label: "首页", icon: "s-home", url: "Home/Home", }, { path: "/mall", name: "mall", label: "商品管理", icon: "video-play", url: "MallManage/MallManage", }, { path: "/user", name: "user", label: "用户管理", icon: "user", url: "UserManage/UserManage", }, { name: "other", label: "其他", icon: "location", children: [ { path: "/page1", name: "page1", label: "设置1", icon: "setting", url: "Other/PageOne", }, { path: "/page2", name: "page2", label: "设置2", icon: "setting", url: "Other/PageTwo", }, ], }, ], } const user = { massage: '登录成功(用户)', status: 0, menu: [ { path: "/home", name: "home", label: "首页", icon: "s-home", url: "Home/Home", }, { path: "/user", name: "user", label: "用户管理", icon: "user", url: "UserManage/UserManage", }, ], } const ERROR = { massage: '登录失败', status: -1 } export default { login: (data) => { const info = JSON.parse(data.body) // console.log("login info", info); if ((info.username == 'admin') && (info.password == '123456')) { return admin } else if((info.username == 'user') && (info.password == '123456')){ return user } else { return ERROR } } }
-
-
配置mock配置文件
-
src目录下创建mock.js
-
配置mock.js文件
//src/api/mock.js //后端数据的“api文档” //可以查看、配置每个请求的路径和请求的方法 //引入Mock,注意此处mockjs没有点 import Mock from 'mockjs' //引入封装好的数据文件 import loginApi from './mockServerData/login' import homeApi from './mockServerData/home' //定义mock进行数据的请求回应 //此处属于后端的配置,传入的第三个参数是后端数据中的方法,实际与前端无关 //前两个参数前端可以用于参考,进行请求的发起:相当于第一个是接口,第二个是方法 //登录页面的登录请求 Mock.mock('/api/login/login', 'post', loginApi.login) //主页面图表数据 Mock.mock('/api/home/getData', 'get', homeApi.getStatisticalData)
-
主文件引入mock
//src/main.js ... //引入mock import './api/mock' ...
-
-
检查请求是否成功
当页面请求数据时,就会返回写好的数据
是否携带参数对请求的影响
-
post请求:携带参数
前端封装的接口,用**
data
进行参数接收**//src/api/index.js import http from '../utils/request' export const login = (data) => { return http.post('login/login', data) }
Mock的mock.js文件,不需要体现参数的传递
//src/api/mock.js Mock.mock('/api/login/login', 'post', loginApi.login)
定义后端数据的文件中,用data接收参数
//src/api/mockServerData/login.js export default { login: (data) => { const info = JSON.parse(data.body) //取出数据并将数据从字符串转化为json格式数据 if ((info.username == 'admin') && (info.password == '123456')) { return admin } else if((info.username == 'user') && (info.password == '123456')){ return user } else { return ERROR } } }
注意此处的传过来的参数是一个包装好的对象,且参数为字符串,所以需要对数据进行处理
-
get请求:
-
不携带参数
即为普通写法
//src/api/index.js export const getData = () => { //返回一个promise对象 return http.get('home/getData') }
//src/api/index.js import http from '../utils/request' Mock.mock('/api/home/getData', 'get', homeApi.getStatisticalData)
//src/api/mockServerData/home.js export default { return { code: 20000, data: { //饼图 videoData: [ { name: '德', value: 86 }, { name: '智', value: 90 }, { name: '体', value: 53 }, { name: '美', value: 42 }, { name: '劳', value: 75 } ], ... }
-
携带参数
前端封装的接口,用**
params
进行参数接收**//src/api/index.js import http from '../utils/request' export const getTableData = (params) => { return http.get('user/getTableData', params) }
Mock的mock.js文件,不需要体现参数的传递
但是注意**此处的路径要使用正则表达式,**因为get请求携带参数是通过
url?key=value
的形式,所以需要对路径进行动态匹配//src/api/mock.js Mock.mock(/api\/user\/getTableData/, 'get', userApi.getTableData)
定义后端数据的文件中,用data接收参数
//src/api/mockServerData/user.js export default { getTableData: (params) => { // console.log("params", params.url.split('=')) let limit = parseInt(params.url.split('=')[1].split('&')[0]); let page = parseInt(params.url.split('=')[2]); // console.log("limit", limit, "page", page); let data = { tableData: tableData, pageData: tableData.slice((page - 1) * limit, page * limit) } return data }, }
此时传递过来的数据是这样的
所以需要从
data.url
中截取出数据
-
转载自:https://juejin.cn/post/7253115535483043897