从0搭建后台管理系统
从0搭建后台管理系统
项目初始化
创建项目
1 |
|
手动选择项目特性 :Manually select features
选择Vue项目必要的插件Vue Router
,Babel
,Vuex
, Linter
选择项目为vue2.x
版本
不选择history模式的前端路由(即选择hash
模式)
选择standard ESLint
一来规范项目代码
让ESLint在代码保存时工作:Lint on save
选择将ESLint,Babel等插件的配置放在单独的文件中:In dedicated config files
安装element-ui
使用vue ui进入图形化界面安装
推荐按需引入的方式
手动安装:
https://element.eleme.io/#/zh-CN/component/quickstart
1 |
|
在src下建文件:src->plugins->element.js
1 |
|
main.js
1 |
|
最后选择了全局引入element-ui,因为要用到el-scrollbar
安装axios库
依赖->安装依赖->运行依赖->axios
手动安装:
-save-dev
是指将包信息添加到 package.json 里的 devDependencies节点,表示开发时依赖的包。
-save
是指将包信息添加到 package.json 里的dependencies节点,表示发布时依赖的包。
1 |
|
- 一是 –force 无视冲突,强制获取远端npm库资源 (覆盖之前)
- 二是 –legacy-peer-deps 忽视依赖冲突,继续安装(不覆盖之前)
目录结构
1 |
|
全局样式表
src->style->index.scss
1 |
|
设置基础信息
src->setting.js
1 |
|
配置vue.config.js
1 |
|
跨域配置
.env.development
1 |
|
.env.production
1 |
|
vue.config.js
1 |
|
封装token相关方法
需要先搞懂几个问题
- token要存储在
cookie
还是localSorage
中? - 为什么token还要在vuex里面存一份?
- (使用localStorage存储)改进参考文章自己封装【加密存储token】在localStorage带有过期时间
本地缓存
utils->store.js (封装处理localStorage相关方法)
1 |
|
vueX模块化
store->index.js
1 |
|
src->getters.js (创建快捷访问)
1 |
|
src->modules->user.js
1 |
|
封装axios
src->utils->request.js
1 |
|
src->api->user.js
1 |
|
代码规范
- 编码规范
- git规范
代码检测工具ESLint
官方文档:https://zh-hans.eslint.org/docs/latest/rules/
项目中的.eslintrc.js
1 |
|
ESLint常用规则
1 |
|
代码格式化Prettier
官网地址: https://www.prettier.cn/
什么是Prettier
Prettier是一个代码格式化工具,它可以支持JS/JSX/TS/Flow/JSON/CSS/LESS等文件格式。
为什么要用Prettier
用来替代lint中的一些场景,比如说分号/tab缩进/空格/引号,这些在lint工具检查出问题之后还需要手动修改,而通常这样的错误都是空格或者符号之类的,这样相对来说不太优雅,利用格式化工具自动生成省时省力。如何自定义配置
Prettier提供了一套默认的配置,那么如何修改配置项符合我们自己的代码规范呢,有三种方法可以做到- .prettierrc 文件
- prettier.config.js 文件
- package.json 中配置prettier属性
.prettierrc 文件配置例子
1
2
3
4
5
6
7
8{
// 不尾随分号
"semi": false,
// 使用单引号
"singleQuote": true,
// 多行逗号分割的语法中,最后一行不加逗号
"trailingComma": "none"
}prettier.config.js 或者 .prettierrc.js,需要返回一个对象文件配置例子
1
2
3
4
5module.exports = {
semi: false,
singleQuote: true,
trailingComma: 'none'
}
项目中使用Prettier
安装Prettier插件
在项目中创建prettier.config.js文件
```javascript
module.exports = {
semi: false, // 不尾随分号
singleQuote: true, // 使用单引号
trailingComma: ‘none’ // 多行逗号分割的语法中,最后一行不加逗号
}1
2
3
4
5
由于prettier会去掉方法名后面的空格,所以要在.eslint.js文件中配置,方法名后面不加空格
```javascript
'space-before-function-paren': 'off' // 方法名后面不加空格
git提交规范
约定式提交:https://www.conventionalcommits.org/zh-hans/v1.0.0/
1 |
|
feat
, // 新功能 featurefix
, // 修复 bugdocs
, // 文档注释style
, // 代码格式(不影响代码运行的变动)refactor
, // 重构(既不增加新功能,也不是修复bug)perf
, // 性能优化test
, // 增加测试chore
, // 构建过程或辅助工具的变动rever
t, // 回退build
// 打包
登录页面
在views下创建login文件夹,进行login相关页面编写
在编写登陆页面时使用到svgIcon组件
其中this.$t()
相关 使用了Vue-i18n国际化
登录
核心代码如下:
1 |
|
校验输入数据是否合法
这里如果不放在computed里面国际化语言切换的时候会显示异常(校验信息会切换不过来)
如果不使用i18n国际化,可以放,data() {return { }} 里面
1 |
|
点击登录按钮,触发登录事件。(这里需要调用vuex中Action中的登陆方法,为什么要调用vuex里面的登陆方法,不直接发请求呢?这是因为登陆返回的数据要保存在vuex中)
src->api->user.js (编写登陆请求接口)
1 |
|
src->store->module->user.js(vuex中编写登陆相关事件)
1 |
|
这下就可以在登陆页面点击登录按钮,进行登录(登录成功之后,token会在localStorage和vuex中各存一份)
1 |
|
路由导航拦截(获取用户信息,不登录拦截)
当用户未登陆时,不允许进入除
login
之外的其他页面。用户登录后,
token
未过期之前,不允许进入login
页面
src->permission.js
1 |
|
退出
退出之后的操作
- 清理掉当前用户缓存数据
- 清理掉权限相关配置
- 返回到登录页
src->store->module->user.js
1 |
|
用户主动退出
点击退出按钮,调用vuex中Actions中的logout方法
1 |
|
用户被动退出
token
失效- 单用户登录:其他人登录该账号被 “顶下来”
那么这两种场景下,在前端对应的处理方案一共也分为两种,共分为 主动处理 、被动处理 两种 :
- 主动处理:主要应对
token
失效 - 被动处理:同时应对
token
失效 与 单用户登录
想要搞明白 主动处理 方案,那么首先我们得先去搞明白对应的 背景 以及 业务逻辑 。
那么首先我们先明确一下对应的 背景:
我们知道
token
表示了一个用户的身份令牌,对 服务端 而言,它是只认令牌不认人的。所以说一旦其他人获取到了你的token
,那么就可以伪装成你,来获取对应的敏感数据。所以为了保证用户的信息安全,那么对于
token
而言就被制定了很多的安全策略,比如:
- 动态
token
(可变token
)- 刷新
token
- 时效
token
- …
这些方案各有利弊,没有绝对的完美的策略。
而我们此时所选择的方案就是 时效 token
对于 token
本身是拥有时效的,这个大家都知道。但是通常情况下,这个时效都是在服务端进行处理。而此时我们要在 服务端处理 token
时效的同时,在前端主动介入 token
时效的处理中。 从而保证用户信息的更加安全性。
主动处理
那么对应到我们代码中的实现方案为:
- 在用户登陆时,记录当前 登录时间
- 制定一个 失效时长
- 在接口调用时,根据 当前时间 对比 登录时间 ,看是否超过了 时效时长
- 如果未超过,则正常进行后续操作
- 如果超过,则进行 退出登录 操作
登录成功后保存登陆的时间
1 |
|
判断token是否有效相关
utils->auth.js
1 |
|
每次请求之前,都先判断一下token是否失效
utils->request.js
1 |
|
被动处理
这种情况是服务器端通知我们,告诉我们token过期或者权限发生变化
1 |
|
Layout搭建
页面布局
页面布局示意图:
采用element-ui提供的Container 布局容器对页面进行布局
1 |
|
当登录完成之后,那么我们会进入到 Layout
页面,这个 Layout
页面组件位于 Layout/index.vue
中,所以说想要实现这样的结构,那么我们就需要到对应的 layout
组件中进行。
创建相应组件
layout/components/Header/index.vue
layout/components/Sidebar/index.vue
layout/components/Navbar.vue
layout/components/AppMain.vue
layout/components/TagsView.vue
完善基本架构
1 |
|
侧边栏
官网介绍:https://element.eleme.cn/#/zh-CN/component/menu
如果向实现点击菜单栏跳转,element-ui已经为我们提供了router属性,不过要在el-menu-item上加index属性,不足之处在于只能在页面内跳转,为此封装了Link组件 ,既可以页面内跳转,也可以跳转到外部链接
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
router | 是否使用 vue-router 的模式,启用该模式会在激活导航时以 index 作为 path 进行路由跳转 | boolean | - | false |
1 |
|
上述代码是element-ui官网提供的子菜单代码,但是我们既想使用自定义svg图标渲染,又想使用element-ui提供的图标进行渲染,那么就要动态判断是自定义svg图标,还是element-ui提供的图标,为此封装了一个Item组件
layout->Sidebar->SidebarItem.vue
为了能够递归渲染菜单封装一个SidebarItem组件
1 |
|
layout->Sidebar->index.vue
1 |
|
如何实现菜单高亮
1 |
|
this.$route
获取当前路由信息
后端搭建
创建父工程
New Project - maven工程 - create from archetype: maven-archetype-quickstart (截图上的有误,选择quickstart就可以了)
字符编码 - Settings - File encoding
Transparent native-to-ascii conversion的意思是:自动转换ASCII编码。
注解生效激活 - Settings - Annotation Processors
Java编译版本选8
src文件夹删掉
创建子工程
vue-admin-common
vue-admin-generator
vue-admin-logging
vue-admin-system
vue-admin-tools
通用组件
svgIcon组件
为了在项目中使用svg图片,特意写了一个SvgIcon组件,用来展示svg图片
核心代码:
1 |
|
1 |
|
vue.config.js 配置
1 |
|
icons->index.js
1 |
|
在main.js中导入
1 |
|
使用
1 |
|
项目中如果要添加svg图片,直接复制到icon->svg文件下,然后使用的使用直接用图片名字当成icon-class的值就可以了
Link组件
// 判断是否是外部链接
1 |
|
Link.vue
1 |
|
使用
1 |
|
Item组件
1 |
|
wangEditor富文本编辑器
1 |
|
知识扩展
history路由模式和hash路由模式
面试被问及 hash 与 history 的区别该怎么回答?
hash | history |
---|---|
有 # 号 | 没有 # 号 |
能够兼容到IE8 | 只能兼容到IE10 |
实际的url之前使用哈希字符,这部分url不会发送到服务器,不需要在服务器层面上进行任何处理 | 每访问一个页面都需要服务器进行路由匹配生成 html 文件再发送响应给浏览器,消耗服务器大量资源 |
刷新不会存在 404 问题 | 浏览器直接访问嵌套路由时,会报 404 问题。 |
不需要服务器任何配置 | 需要在服务器配置一个回调路由 |
推荐使用 hash 模式
1、从兼容角度分析。
hash 可以兼容到 IE8,而 history 只能兼容到 IE10。
2、从网络请求的角度分析。
使用 hash 模式,地址改变时通过 hashchange 事件,只会读取哈希符号后的内容,并不会发起任何网络请求。
而 history 模式,每访问一个页面都要发起网络请求,每个请求都需要服务器进行路由匹配、数据库查询、生成HTML文档后再发送响应给浏览器,这个过程会消耗服务器的大量资源,给服务器的压力较大。
3、服务器配置角度分析。
hash 不需要服务器任何配置。
history 进行刷新页面时,无法找到url对应的页面,会出现 404 问题。因为域名后面的路由是由前端控制的,后端只能保留域名部分,所以就会造成页面丢失的问题,需要服务器端添加一个回退路由,就能解决该问题了。
hash 模式不足
1、hash 模式中的 # 也称作锚点,这里的的 # 和 css 中的 # 是一个意思,所以在 hash 模式内,页面定位会失效。
2、hash 不利于 SEO(搜索引擎优化)。
3、白屏时间问题。浏览器需要等待 JavaScript 文件加载完成之后渲染 HTML 文档内容,用户等待时间稍长。
ESLint详解
官方文档:https://zh-hans.eslint.org/docs/latest/rules/
什么是babel
在创建项目的时候,我们选择了Babel
,那么什么是Babel
呢?
Babel 是现代 JavaScript 语法转换器
参考文章:
token存储到cookie还是localstorage
- 将 Token 存储在 webStorage(localStorage,sessionStorage) 中可以通过同域的js访问,这样导致很容易受到 ==xss== 攻击,特别是项目中引入很多第三方js库的情况下,如果js脚本被盗用,攻击者就可以轻易访问你的网站。
xss攻击:是一种注入代码攻击,通过在网站里注入script代码,当访问者浏览网站的时候通过注入的script代码窃取用户信息,盗用用户身份等
- 将 Token 存储在 cookie 中,可以指定 httponly 来防止 js 被读取,也可以指定 secure 来保证 Token 只在 HTTPS 下传输,缺点是不符合 RestFul 最佳实践,容易受到 ==CSRF== 攻击。
CSRF: 跨站点请求伪造,攻击者盗用已经认证过的用户信息,以用户信息的名义进行操作(转账,购买商品等),由于身份已经认证过了,所以网站会认为此操作是用户本人操作。 CSRF 并不能拿到用户信息,但它可以盗用用户的凭证进行操作。
生命周期
cookie:可设置失效时间,没有设置的话,默认是关闭浏览器后失效
localStorage:除非被手动清除,否则将会永久保存。
sessionStorage: 仅在当前网页会话下有效,关闭页面或浏览器后就会被清除。
大小
cookie:4KB左右
localStorage: 5MB的信息。 sessionStorage: 同上。
http 请求
cookie:每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题。
localStorage:仅在客户端(即浏览器)中保存,不参与和服务器的通信。 sessionStorage: 同上。
易用性
cookie:需要程序员自己封装,原生的Cookie接口不友好
localStorage:源生接口可以接受,亦可再次封装来对Object和Array有更好的支持 sessionStorage: 同上
应用场景
cookie:
- 请求时会自动携带,最合适的就是通过发送请求的时候做权限验证.
- 个数要少,尽量要少(各个浏览器的情况不一)
- 安全性
localStorage:
- 关闭浏览器不会清除,适用于存储一些不变的数据,
sessionStorage:
- 关闭浏览器会清除数据,用于一些较为敏感的数据
参考文章:
localStorage、sessionStorage 、cookie 三者的区别和应用场景
为什么token还要在vuex里面存一份?
- vuex存储数据的特点:数据统一全局管理,一旦数据在某组件更新,其他所有组件数据都会更新,是响应式的,但是如果数据只存在vuex中,刷新页面vuex里的数据会重新初始化,导致数据丢失,恢复到原来的状态。
- localstorage(本地存储),永久性存储,但不是响应式的,当某个组件数据修改时,其他组件无法同步更新。
- vuex是存储到内存里,localStorage本地存储到磁盘里,从内存中读取数据,速度是远高于磁盘的,所以把数据存在vuex中可以提高获取token速度,提高性能。
- 通常是两者结合,拿到token后,把token 存储在localStorage和vuex中,vuex保证数据在各组件间同步更新,如果刷新页面数据更新丢失,可以从localStorage获取,通过结合vuex和localStorage本地存储,实现数据的持久化
import path from ‘path’ 报错
1 |
|
安装在 Webpack 中 Polyfill Node.js 核心模块。
1 |
|
在vue.config.json中添加
1 |
|
按需引入element-ui,无法引入el-scrollbar
Scrollbar是element-ui的隐藏组件,没有暴露这个组件。
暴力解决
node_modules/element-ui/types下面添加一个scrollbar.d.ts的文件:
v-on=”$listeners”
svg
以下内容摘自官网
什么是SVG?
- SVG 指可伸缩矢量图形 (Scalable Vector Graphics)
- SVG 用来定义用于网络的基于矢量的图形
- SVG 使用 XML 格式定义图形
- SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失
- SVG 是万维网联盟的标准
- SVG 与诸如 DOM 和 XSL 之类的 W3C 标准是一个整体
1 |
|
SVG 代码以 <svg> 元素开始,包括开启标签 <svg和关闭标签 。这是根元素。width 和 height 属性可设置此 SVG 文档的宽度和高度。version 属性可定义所使用的 SVG 版本,xmlns 属性可定义 SVG 命名空间。
SVG 的
<circle>
用来创建一个圆。cx 和 cy 属性定义圆中心的 x 和 y 坐标。如果忽略这两个属性,那么圆点会被设置为 (0, 0)。r 属性定义圆的半径。stroke 和 stroke-width 属性控制如何显示形状的轮廓。我们把圆的轮廓设置为 2px 宽,黑边框。
fill 属性设置形状内的颜色。我们把填充颜色设置为红色。
SVG 文件可通过以下标签嵌入 HTML 文档:<embed>
、<object>
或者 <iframe>
。
使用 <embed>
标签
1 |
|
注释:pluginspage 属性指向下载插件的 URL。
使用 <object>
标签
1 |
|
注释:codebase 属性指向下载插件的 URL。
使用 <iframe>
标签
<iframe> 标签可工作在大部分的浏览器中。
aria-hidden:”true”
https://developer.mozilla.org/zh-CN/docs/Web/Accessibility/ARIA/Attributes/aria-hidden
为什么要写一个SvgIcon组件
svg-sprite-loader
使用 svg-sprite-loader、svgo-loader 优化 svg symbols
动态组件
渲染一个“元组件”为动态组件。依 is
的值,来决定哪个组件被渲染。
1 |
|
引入variables.scss读取不到变量
关于Scss样式中使用 :export 导出的对象在js中未空对象问题解答
自己实现vuex持久化
退出登录请求异常
cess to XMLHttpRequest at ‘http://localhost:8000/login?logout' (redirected from ‘http://localhost:8080/dev-api/logout') from origin ‘http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
Spring Security/logout请求默认跳转到/login?logout解决方案
CSS–BEM风格介绍
fusejs使用说明
https://fusejs.io/api/options.html
基于路由的过渡效果
自定义主题解决fonts丢失问题
:8080/fonts/element-icons.woff:1 GET http://localhost:8080/fonts/element-icons.woff net::ERR_ABORTED 404 (Not Found)
:8080/fonts/element-icons.ttf:1 GET http://localhost:8080/fonts/element-icons.ttf net::ERR_ABORTED 404 (Not Found)
获取到的css 需要替换掉@font-face, 也就是不覆盖默认的@font-face
1 |
|
git 打标签
1 |
|
导入组件报错(组件名字是index.vue,同目录下还有index.js)
export ‘default’ (imported as ‘DUploadExcel’) was not found in ‘@/components/DUploadExcel’ (possible exports: getHeaderRow)
1 |
|
原因: 识别到了index.js
解决方法:
- import DUploadExcel from ‘@/components/DUploadExcel/index.vue’
- 修改index.js名称
弹窗自定义拖拽
参考文章:https://blog.csdn.net/qinleilei7760631/article/details/123885576
自定义组件v-model
一个组件上的 v-model
默认会利用名为 value
的 prop 和名为 input
的事件,但是像单选框、复选框等类型的输入控件可能会将 value
attribute 用于不同的目的。model
选项可以用来避免这样的冲突:
offsetWidth和clientWidth等介绍
参考文章:https://blog.csdn.net/weixin_39905500/article/details/110897355
- HTMLElement.offsetWidth是一个只读属性,返回一个元素的布局宽度。各浏览器的
offsetWidth
可能有所不同)offsetWidth
是测量包含元素的边框(border
)、水平线上的内边距(padding
)、竖直方向滚动条(scrollbar
)(如果存在的话)、以及CSS设置的宽度(width)的值。即CSS3中的border-box模型的宽度。 - HTMLElement.clientWidth 属性表示元素的内部宽度,以像素计。
- HTMLElement.scrollWidth 这个只读属性是元素内容宽度的一种度量,包括由于overflow溢出而在屏幕上不可见的内容。
requireContext.keys().map(requireContext)
requireContext.keys().map(requireContext)在批量导入时的作用
require.context
的返回值,这个返回值是一个函数,只要传入函数名就会被 webpack 自动进行导入。
1 |
|
正则表达式
实现复制
使用插件 clipboard
1 |
|
Vue自定义指令
函数:
bind
第一次绑定到元素时调用(初始化)inserted
被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)update
数据更新时调用componentUpdated
指令所在组件的 VNode 及其子 VNode 全部更新后调用。unbind
只调用一次,指令与元素解绑时调用。
传递参数:
el
:指令绑定到的元素。这可以用于直接操作 DOM。binding
:一个对象,包含以下属性。value
:传递给指令的值。例如在 v-my-directive=”1 + 1” 中,值是 2。oldValue
:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。arg
:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 “foo”。modifiers
:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。instance
:使用该指令的组件实例。dir
:指令的定义对象。
vnode
:代表绑定元素的底层 VNode。prevNode
:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
使用clipboard实现文本复制,点击svg图标报错
[Vue warn]: Error in v-on handler: “TypeError: First argument must be a String, HTMLElement, HTMLCollection, or NodeList”
pointer-events: none;
你可以看的到某个元素,但是你无法摸的着,点击不到,点击会穿透触发到下层的元素
1 |
|
通信格式说明
1 |
|
1 |
|
打包流程与部署
最后发现只打包父项目就ok
父项目 install
然后打包子项目 A依赖B ,要先打包B
最后打包入口文件,也就是含有启动类的文件
打包后的地址在控制台
1 |
|