【01/02 - 基础搭建】

1.了解vue-element-admin

**目标**: 学习和了解通用的vue后台集成方案**vue-element-admin**
vue-element-admin 是一个后台前端解决方案,它基于 vueelement-ui实现。它使用了最新的前端技术栈,内置了 i18 国际化解决方案,动态路由,权限验证,提炼了典型的业务模型,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型。

vue-element-admin 有一个成熟的集成方案,里面包含了所有的业务功能和场景,并不适合直接拿来进行二次开发, 但是可以通过该项目中的一个案例来进行学习和使用.

这里是官网地址 | 这里是线上demo地址

如果你想查看该项目的具体功能和效果,可以拉取代码,启动进行预览

  1. $ git clone https://github.com/PanJiaChen/vue-element-admin.git #拉取代码
  2. $ cd vue-element-admin #切换到具体目录下
  3. $ npm run dev #启动开发调试模式 查看package.json文件的scripts可知晓启动命令

**注意**:当前项目下载速度如果过慢,可以直接下载代码的压缩包运行
image-20200703173319390.png

集成方案并不适合我们直接拿来进行二次开发,基础模板则是一个更好的选择

基础模板, 包含了基本的 登录 / 鉴权 / 主页布局 的一些基础功能模板, 我们可以直接在该模板上进行功能的扩展和项目的二次开发

1.1项目模板启动和目录介绍

目标: 拉取项目的基础模板,并对目录进行介绍
git拉取基础项目模板
$ git clone https://github.com/PanJiaChen/vue-admin-template.git hrsaas #拉取基础模板到hrsaas目录
安装项目依赖(定位到项目目录下)
$ npm install #安装依赖
启动项目
$ npm run dev #启动开发模式的服务
image-20200708010741325.png
项目运行完毕,浏览器会自动打开基础模板的登录页,如上图
目录结构
本项目已经为你生成了一个基本的开发框架,提供了涵盖中后台开发的各类功能和坑位,下面是整个项目的目录结构。
├── build # 构建相关
├── mock # 项目mock 模拟数据
├── public # 静态资源
│ │── favicon.ico # favicon图标
│ └── index.html # html模板
├── src # 源代码
│ ├── api # 所有请求
│ ├── assets # 主题 字体等静态资源
│ ├── components # 全局公用组件
│ ├── icons # 项目所有 svg icons
│ ├── layout # 全局 layout
│ ├── router # 路由
│ ├── store # 全局 store管理
│ ├── styles # 全局样式
│ ├── utils # 全局公用方法
│ ├── vendor # 公用vendor
│ ├── views # views 所有页面
│ ├── App.vue # 入口页面
│ ├── main.js # 入口文件 加载组件 初始化等
│ └── permission.js # 权限管理
│ └── settings.js # 配置文件
├── tests # 测试
├── .env.xxx # 环境变量配置
├── .eslintrc.js # eslint 配置项
├── .babelrc # babel-loader 配置
├── .travis.yml # 自动化CI配置
├── vue.config.js # vue-cli 配置
├── postcss.config.js # postcss 配置
└── package.json # package.json

1.2项目的运行机制和代码注释

目标: 了解当前模板的基本运行机制和基础架构
眼花缭乱的目录和文件到底是怎么工作的? 我们进行一下最基本的讲解,帮助大家更好的去理解和开发
├── src # 源代码
│ ├── api # 所有请求
│ ├── assets # 主题 字体等静态资源
│ ├── components # 全局公用组件
│ ├── icons # 项目所有 svg icons
│ ├── layout # 全局 layout
│ ├── router # 路由
│ ├── store # 全局 store管理
│ ├── styles # 全局样式
│ ├── utils # 全局公用方法
│ ├── vendor # 公用vendor
│ ├── views # views 所有页面
│ ├── App.vue # 入口页面
│ ├── main.js # 入口文件 加载组件 初始化等
│ └── permission.js # 权限管理
│ └── settings.js # 配置文件

main.js

image-20200824153141764.png

App.vue

image-20200824155103340.png

permission.js

permission.js 是控制页面登录权限的文件, 此处的代码没有经历构建过程会很难理解, 所以先将此处的代码进行注释,等我们构建权限功能时,再从0到1进行构建。

settings.js

配置文件

Vuex结构

当前的Vuex结构采用了模块形式进行管理共享状态,其架构如下

image-20200824165153331.png

其中app.js模块和settings.js模块,功能已经完备,不需要再进行修改。 user.js模块是我们后期需要重点开发的内容,所以这里我们将user.js里面的内容删除,并且导出一个默认配置

  1. export default {
  2. namespaced: true,
  3. state: {},
  4. mutations: {},
  5. actions: {}
  6. }

同时,由于getters中引用了user中的状态,所以我们将getters中的状态改为

  1. const getters = {
  2. sidebar: state => state.app.sidebar,
  3. device: state => state.app.device
  4. }
  5. export default getters

scss

该项目还使用了scss作为css的扩展语言,在**styles**目录下,我们可以发现scss的相关文件,相关用法 我们下一小节 进行讲解

image-20200824171327384.png

icons

icons的结构如下
image-20200824173239955.png

2.Axios拦截器

**理解** 封装Axios拦截器,可以创建一个请求拦截器和响应拦截器,在请求的发送前和返回前做响应的操作。比如:可以在请求拦截器内载入Token ; 可以在响应拦截器内对数据进行结构(因为axios默认多包了一层data)
**目标** 介绍API模块的单独请求和 request模块的封装

该项目采用了API的单独模块封装和axios拦截器的方式进行开发

axios的拦截器原理如下
image-20200811012945409.png
axios拦截器
axios作为网络请求的第三方工具, 可以进行请求和响应的拦截
通过create创建了一个新的axios实例

  1. // 创建了一个新的axios实例
  2. const service = axios.create({
  3. baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  4. // withCredentials: true, // send cookies when cross-domain requests
  5. timeout: 5000 // 超时时间
  6. })

请求拦截器
请求拦截器主要处理 token的**统一注入问题**

  1. // axios的请求拦截器
  2. service.interceptors.request.use(
  3. config => {
  4. // do something before request is sent
  5. if (store.getters.token) {
  6. // let each request carry token
  7. // ['X-Token'] is a custom headers key
  8. // please modify it according to the actual situation
  9. config.headers['X-Token'] = getToken()
  10. }
  11. return config
  12. },
  13. error => {
  14. // do something with request error
  15. console.log(error) // for debug
  16. return Promise.reject(error)
  17. }
  18. )

响应拦截器
响应拦截器主要处理 返回的**数据异常****数据结构**问题

  1. // 响应拦截器
  2. service.interceptors.response.use(
  3. response => {
  4. const res = response.data
  5. // if the custom code is not 20000, it is judged as an error.
  6. if (res.code !== 20000) {
  7. Message({
  8. message: res.message || 'Error',
  9. type: 'error',
  10. duration: 5 * 1000
  11. })
  12. if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
  13. // to re-login
  14. MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
  15. confirmButtonText: 'Re-Login',
  16. cancelButtonText: 'Cancel',
  17. type: 'warning'
  18. }).then(() => {
  19. store.dispatch('user/resetToken').then(() => {
  20. location.reload()
  21. })
  22. })
  23. }
  24. return Promise.reject(new Error(res.message || 'Error'))
  25. } else {
  26. return res
  27. }
  28. },
  29. error => {
  30. console.log('err' + error) // for debug
  31. Message({
  32. message: error.message,
  33. type: 'error',
  34. duration: 5 * 1000
  35. })
  36. return Promise.reject(error)
  37. }
  38. )

这里为了后续更清楚的书写代码,我们将原有代码注释掉,换成如下代码

  1. // 导出一个axios的实例 而且这个实例要有请求拦截器 响应拦截器
  2. import axios from 'axios'
  3. const service = axios.create() // 创建一个axios的实例
  4. service.interceptors.request.use() // 请求拦截器
  5. service.interceptors.response.use() // 响应拦截器
  6. export default service // 导出axios实例

3.公共资源图片和统一样式

**目标** 将一些公共的图片和样式资源放入到 规定目录中

我们已经将整体的基础模块进行了简单的介绍,接下来,我们需要将该项目所用到的图片和样式进行统一的处理

图片资源

图片资源在课程资料的图片文件中,我们只需要将**common**文件夹拷贝放置到 **assets**目录即可

样式

样式资源在 资源/样式目录下

修改**variables.scss**
新增**common.scss**
我们在**variables.scss**添加了一些基础的变量值
我们提供了 一份公共的**common.scss**样式,里面内置了一部分内容的样式,在开发期间可以帮助我们快速的实现页面样式和布局
将两个文件放置到styles目录下,然后在**index.scss**中引入该样式

  1. @import './common.scss'; //引入common.scss样式表

提交代码
**本节注意**:注意在scss文件中,通过@import 引入其他样式文件,需要注意最后加分号,否则会报错
**本节任务** 将公共资源的图片和样式放置到规定位置

【03-登录模块】

4.vue-element-admin里面对生产环境和开发环境的配置

**理解**: vue-element-admin项目中,将启动端口进行区分,分配在**.env.development****.env.production**两个文件里,并且将网页的 title 变量存储在不同的地方,
**目标**: 设置统一的本地访问端口和网站title
在正式开发业务之前,先将项目的本地端口网站名称进行一下调整
本地服务端口: 在**vue.config.js**中进行设置
**vue.config.js** 就是vue项目相关的编译,配置,打包,启动服务相关的配置文件,它的核心在于webpack,但是又不同于webpack,相当于改良版的webpack, 文档地址

如图,是开发环境服务端口的位置

image-20200710162221402.png
我们看到上面的 **process.env.port**实际上是一个nodejs服务下的环境变量,该变量在哪里设置呢?
在项目下, 我们发现了**.env.development****.env.production**两个文件
development => 开发环境
production => 生产环境

当我们运行npm run dev进行开发调试的时候,此时会加载执行**.env.development**文件内容
当我们运行npm run build:prod进行生产环境打包的时候,会加载执行**.env.production**文件内容
所以,如果想要设置开发环境的接口,直接在**.env.development**中写入对于port变量的赋值即可

  1. # 设置端口号
  2. port = 8888

**本节注意**:修改服务的配置文件,想要生效的话,必须要重新启动服务,值‘8888’后面不能留有空格
网站名称
网站名称实际在configureWebpack选项中的name选项,通过阅读代码,我们会发现name实际上来源于src目录下
**settings.js**文件
所以,我们可以将网站名称改成”**人力资源管理平台**
image-20200710164040042.png
**本节注意**:修改服务的配置文件,想要生效的话,必须要重新启动服务,值‘8888’后面不能留有空格

5.el-form登录校验的先决条件

要在el-form中做表单校验,要满足这些先决条件
image-20200830212537835.png

6.request.js中环境变量和异常的处理

**理解**: 区分不同的开发环境下的请求地址,分别在 **.env.development****.env.production** 两个文件中定义了变量**VUE_APP_BASE_API**作为axios请求的基本地址。在开发环境中,通过设置webpack的反向代理与服务器进行沟通。
为什么会有环境变量之分? 如图
image-20200826150136697.png

从上图可以看出,开发环境实际上就是在自己的本地开发或者要求不那么高的环境,但是一旦进入生产,就是**真实的数据**。 拿银行作比喻,如果你在开发环境拿生产环境的接口做测试,银行系统就会发生很大的风险。

前端主要区分两个环境,**开发环境**,**生产环境**

也就是两个环境发出的请求地址是不同的,用什么区分呢?

环境变量

  1. $ process.env.NODE_ENV # 当为production时为生产环境 为development时为开发环境

环境文件
我们可以在**.env.development****.env.production**定义变量,变量自动就为当前环境的值
基础模板在以上文件定义了变量**VUE_APP_BASE_API**,该变量可以作为axios请求的**baseURL**
我们会发现,在模板中,两个值分别为**/dev-api****/prod-api**
但是我们的开发环境代理是**/api**,所以可以统一下

  1. # 开发环境的基础地址和代理对应
  2. VUE_APP_BASE_API = '/api'
  1. # 这里配置了/api,意味着需要在Nginx服务器上为该服务配置 nginx的反向代理对应/prod-api的地址
  2. VUE_APP_BASE_API = '/prod-api'

**本节注意**:我们这里生产环境和开发环境设置了不同的值,后续我们还会在生产环境部署的时候,去配置该值所对应的反向代理,反向代理指向哪个地址,完全由我们自己决定,不会和开发环境冲突
在request中设置baseUrl

  1. const service = axios.create({
  2. // 如果执行 npm run dev 值为 /api 正确 /api 这个代理只是给开发环境配置的代理
  3. // 如果执行 npm run build 值为 /prod-api 没关系 运维应该在上线的时候 给你配置上 /prod-api的代理
  4. baseURL: process.env.VUE_APP_BASE_API, // 设置axios请求的基础的基础地址
  5. timeout: 5000 // 定义5秒超时
  6. }) // 创建一个axios的实例

【04-主页模块】

7.主页的token拦截处理

理解:后台系统中,对是否token权限的一个参考流程
目标:根据token处理主页的访问权限问题

权限拦截的流程图

我们已经完成了登录的过程,并且存储了token,但是此时主页并没有因为token的有无而被控制访问权限

接下来我们需要实现以下如下的流程图
image-20200714093601730.png

在基础框架阶段,我们已经知道**src/permission.js**是专门处理路由权限的,所以我们在这里处理

流程图转化代码

流程图转化的代码

  1. // 权限拦截 导航守卫 路由守卫 router
  2. import router from '@/router' // 引入路由实例
  3. import store from '@/store' // 引入vuex store实例
  4. import NProgress from 'nprogress' // 引入一份进度条插件
  5. import 'nprogress/nprogress.css' // 引入进度条样式
  6. const whiteList = ['/login', '/404'] // 定义白名单 所有不受权限控制的页面
  7. // 路由的前置守卫
  8. router.beforeEach(function(to, from, next) {
  9. NProgress.start() // 开启进度条
  10. // 首先判断有无token
  11. if (store.getters.token) {
  12. // 如果有token 继续判断是不是去登录页
  13. if (to.path === '/login') {
  14. // 表示去的是登录页
  15. next('/') // 跳到主页
  16. } else {
  17. next() // 直接放行
  18. }
  19. } else {
  20. // 如果没有token
  21. if (whiteList.indexOf(to.path) > -1) {
  22. // 如果找到了 表示在在名单里面
  23. next()
  24. } else {
  25. next('/login') // 跳到登录页
  26. }
  27. }
  28. NProgress.done() // 手动强制关闭一次 为了解决 手动切换地址时 进度条的不关闭的问题
  29. })
  30. // 后置守卫
  31. router.afterEach(function() {
  32. NProgress.done() // 关闭进度条
  33. })

在导航守卫的位置,我们添加了NProgress的插件,可以完成进入时的进度条效果

**本节任务**:完成主页中根据有无token,进行页面访问的处理

8.获取用户资料接口和token注入

理解:在axios的请求拦截器中注入token
**目标** 封装获取用户资料的资料信息

上小节中,我们完成了头部菜单的基本布局,但是用户的头像和名称并没有,我们需要通过接口调用的方式获取当前用户的资料信息

获取用户资料接口
**src/api/user.js**中封装获取用户资料的方法

  1. /**
  2. * 获取用户的基本资料
  3. *
  4. * **/
  5. export function getUserInfo() {
  6. return request({
  7. url: '/sys/profile',
  8. method: 'post'
  9. })
  10. }

我们忽略了一个问题!我们的headers参数并没有在这里传入,为什么呢

headers中的Authorization相当于我们开门调用接口)时**钥匙(token)**,我们在打开任何带安全权限的门的时候都需要**钥匙(token)** 如图
image-20200715233339927.png
每次在接口中携带**钥匙(token)**很麻烦,所以我们可以在axios拦截器中统一注入token
image-20200716000203862.png
统一注入token **src/utils/request.js**

  1. service.interceptors.request.use(config => {
  2. // 在这个位置需要统一的去注入token
  3. if (store.getters.token) {
  4. // 如果token存在 注入token
  5. config.headers['Authorization'] = `Bearer ${store.getters.token}`
  6. }
  7. return config // 必须返回配置
  8. }, error => {
  9. return Promise.reject(error)
  10. })

**本节任务**: 完成获取用户资料接口和token注入

9.权限拦截处调用获取资料action

**目标**在权限拦截处调用aciton
权限拦截器调用action

在上小节中,我们完成了用户资料的整个流程,那么这个action在哪里调用呢?

用户资料有个硬性要求,**必须有token**才可以获取,那么我们就可以在确定有token的位置去获取用户资料
image-20200716004526838.png

由上图可以看出,一旦确定我们进行了放行,就可以获取用户资料

image-20200813013009294.png
调用action **src/permission.js**

  1. if(!store.state.user.userInfo.userId) {
  2. await store.dispatch('user/getUserInfo')
  3. }

如果我们觉得获取用户id的方式写了太多层级,可以在vuex中的getters中设置一个映射 **src/store/getters.js**

  1. userId: state => state.user.userInfo.userId // 建立用户id的映射

代码就变成了

  1. if (!store.getters.userId) {
  2. // 如果没有id这个值 才会调用 vuex的获取资料的action
  3. await store.dispatch('user/getUserInfo')
  4. // 为什么要写await 因为我们想获取完资料再去放行
  5. }

此时,我们可以通过dev-tools工具在控制台清楚的看到数据已经获取
image-20200716012120619.png

最后一步,只需要将头部菜单中的名称换成真实的用户名即可

10.自定义指令-解决异常图片情况

**目标**: 通过自定义指令的形式解决异常图片的处理
注册自定义指令

  1. Vue.directive('指令名称', {
  2. // 会在当前指令作用的dom元素 插入之后执行
  3. // options 里面是指令的表达式
  4. inserted: function (dom,options) {
  5. }
  6. })

自定义指令可以采用统一的文件来管理 **src/directives/index.js**,这个文件负责管理所有的自定义指令

首先定义第一个自定义指令 **v-imagerror**

  1. export const imagerror = {
  2. // 指令对象 会在当前的dom元素插入到节点之后执行
  3. inserted(dom, options) {
  4. // options是 指令中的变量的解释 其中有一个属性叫做 value
  5. // dom 表示当前指令作用的dom对象
  6. // dom认为此时就是图片
  7. // 当图片有地址 但是地址没有加载成功的时候 会报错 会触发图片的一个事件 => onerror
  8. dom.onerror = function() {
  9. // 当图片出现异常的时候 会将指令配置的默认图片设置为该图片的内容
  10. // dom可以注册error事件
  11. dom.src = options.value // 这里不能写死
  12. }
  13. }
  14. }

在main.js完成自定义指令全局注册

然后,在**main.js**中完成对于该文件中所有指令的全局注册

  1. import * as directives from '@/directives'
  2. // 注册自定义指令
  3. // 遍历所有的导出的指令对象 完成自定义全局注册
  4. Object.keys(directives).forEach(key => {
  5. // 注册自定义指令
  6. Vue.directive(key, directives[key])
  7. })

针对上面的引入语法 **import * as 变量** 得到的是一个对象**{ 变量1:对象1,变量2: 对象2 ... }**, 所以可以采用对象遍历的方法进行处理
指令注册成功,可以在**navbar.vue**中直接使用了

  1. <img v-imageerror="defaultImg" :src="staffPhoto" class="user-avatar">
  1. data() {
  2. return {
  3. defaultImg: require('@/assets/common/head.jpg')
  4. }
  5. },

**本节任务**:实现一个自定义指令,解决图片加载异常的问题

12.Token失效的主动介入

理解:在设置登录的时候,当登录成功获取到token => 设置token的同时 存储一个当前的时间戳。 当请求时,若当前存在token => 则判断当前的时间戳是否超时 => 若超时 就删除用户信息和token,然后退出到登录页。
**目标**: 处理当token失效时业务

主动介入token处理的业务逻辑

开门的钥匙不是一直有效的,如果一直有效,会有安全风险,所以我们尝试在客户端进行一下token的时间检查

具体业务图如下
image-20200716231205153.png

流程图转化代码

流程图转化代码 **src/utils/auth.js**

  1. const timeKey = 'hrsaas-timestamp-key' // 设置一个独一无二的key
  2. // 获取时间戳
  3. export function getTimeStamp() {
  4. return Cookies.get(timeKey)
  5. }
  6. // 设置时间戳
  7. export function setTimeStamp() {
  8. Cookies.set(timeKey, Date.now())
  9. }

**src/utils/request.js**

  1. import axios from 'axios'
  2. import store from '@/store'
  3. import router from '@/router'
  4. import { Message } from 'element-ui'
  5. import { getTimeStamp } from '@/utils/auth'
  6. const TimeOut = 3600 // 定义超时时间
  7. const service = axios.create({
  8. // 当执行 npm run dev => .evn.development => /api => 跨域代理
  9. baseURL: process.env.VUE_APP_BASE_API, // npm run dev => /api npm run build => /prod-api
  10. timeout: 5000 // 设置超时时间
  11. })
  12. // 请求拦截器
  13. service.interceptors.request.use(config => {
  14. // config 是请求的配置信息
  15. // 注入token
  16. if (store.getters.token) {
  17. // 只有在有token的情况下 才有必要去检查时间戳是否超时
  18. if (IsCheckTimeOut()) {
  19. // 如果它为true表示 过期了
  20. // token没用了 因为超时了
  21. store.dispatch('user/logout') // 登出操作
  22. // 跳转到登录页
  23. router.push('/login')
  24. return Promise.reject(new Error('token超时了'))
  25. }
  26. config.headers['Authorization'] = `Bearer ${store.getters.token}`
  27. }
  28. return config // 必须要返回的
  29. }, error => {
  30. return Promise.reject(error)
  31. })
  32. // 响应拦截器
  33. service.interceptors.response.use(response => {
  34. // axios默认加了一层data
  35. const { success, message, data } = response.data
  36. // 要根据success的成功与否决定下面的操作
  37. if (success) {
  38. return data
  39. } else {
  40. // 业务已经错误了 还能进then ? 不能 ! 应该进catch
  41. Message.error(message) // 提示错误消息
  42. return Promise.reject(new Error(message))
  43. }
  44. }, error => {
  45. Message.error(error.message) // 提示错误信息
  46. return Promise.reject(error) // 返回执行错误 让当前的执行链跳出成功 直接进入 catch
  47. })
  48. // 是否超时
  49. // 超时逻辑 (当前时间 - 缓存中的时间) 是否大于 时间差
  50. function IsCheckTimeOut() {
  51. var currentTime = Date.now() // 当前时间戳
  52. var timeStamp = getTimeStamp() // 缓存时间戳
  53. return (currentTime - timeStamp) / 1000 > TimeOut
  54. }
  55. export default service

**本节注意**:我们在调用登录接口的时候 一定是没有token的,所以token检查不会影响登录接口的调用

同理,在登录的时候,如果登录成功,我们应该设置时间戳

  1. // 定义login action 也需要参数 调用action时 传递过来的参数
  2. // async 标记的函数其实就是一个异步函数 -> 本质是还是 一个promise
  3. async login(context, data) {
  4. // 经过响应拦截器的处理之后 这里的result实际上就是 token
  5. const result = await login(data) // 实际上就是一个promise result就是执行的结果
  6. // axios默认给数据加了一层data
  7. // 表示登录接口调用成功 也就是意味着你的用户名和密码是正确的
  8. // 现在有用户token
  9. // actions 修改state 必须通过mutations
  10. context.commit('setToken', result)
  11. // 写入时间戳
  12. setTimeStamp() // 将当前的最新时间写入缓存
  13. }

提交代码

有主动处理就有被动处理,也就是后端告诉我们超时了,我们被迫做出反应,如果后端接口没有做处理,主动介入就是一种简单的方式

**本节任务**:完成token超时的主动介入

13.Token失效的被动处理

理解:当token失效的时候,在服务器返回的报文中,会返回状态码:10002 (token过时),被动处理就是在服务器返回失败的状态码之后,在响应拦截器中判断状态码,如果状态为10002则进行被动的登出操作,作为主动处理的备用处理,防止主动处理出错。
**目标**: 实现token失效的被动处理

除了token的主动介入之外,我们还可以对token进行被动的处理,如图

image-20200818155842864.png
token超时的错误码是**10002**
代码实现 **src/utils/request.js**

  1. error => {
  2. // error 信息 里面 response的对象
  3. if (error.response && error.response.data && error.response.data.code === 10002) {
  4. // 当等于10002的时候 表示 后端告诉我token超时了
  5. store.dispatch('user/logout') // 登出action 删除token
  6. router.push('/login')
  7. } else {
  8. Message.error(error.message) // 提示错误信息
  9. }
  10. return Promise.reject(error)
  11. }

无论是主动介入还是被动处理,这些操作都是为了更好地处理token,减少错误异常的可能性

**本节任务** Token失效的被动处理

【其它补充】

[06-组织架构]_新增部门的规则校验

表单内容
部门名称(name):必填 1-50个字符 / 同级部门中禁止出现重复部门
部门编码(code):必填 1-50个字符 / 部门编码在整个模块中都不允许重复
部门负责人(manager):必填
部门介绍 ( introduce):必填 1-300个字符

定义数据结构

  1. formData: {
  2. name: '', // 部门名称
  3. code: '', // 部门编码
  4. manager: '', // 部门管理者
  5. introduce: '' // 部门介绍
  6. },

完成表单校验需要的前置条件

  • el-form配置model和rules属性
  • el-form-item配置prop属性
  • 表单进行v-model双向绑定

配置表单的基本校验规则

  1. data() {
  2. return {
  3. // 定义表单数据
  4. formData: {
  5. name: '', // 部门名称
  6. code: '', // 部门编码
  7. manager: '', // 部门管理者
  8. introduce: '' // 部门介绍
  9. },
  10. // 定义校验规则
  11. rules: {
  12. name: [{ required: true, message: '部门名称不能为空', trigger: 'blur' },
  13. { min: 1, max: 50, message: '部门名称要求1-50个字符', trigger: 'blur' }],
  14. code: [{ required: true, message: '部门编码不能为空', trigger: 'blur' },
  15. { min: 1, max: 50, message: '部门编码要求1-50个字符', trigger: 'blur' }],
  16. manager: [{ required: true, message: '部门负责人不能为空', trigger: 'blur' }],
  17. introduce: [{ required: true, message: '部门介绍不能为空', trigger: 'blur' },
  18. { trigger: 'blur', min: 1, max: 300, message: '部门介绍要求1-50个字符' }]
  19. }
  20. }
  21. }


部门名称和部门编码的自定义校验

**注意**:部门名称和部门编码的规则 有两条我们需要通过**自定义校验函数validator**来实现

首先,在校验名称和编码时,要获取最新的组织架构,这也是我们这里trigger采用blur的原因,因为change对于访问的频率过高,我们需要控制访问频率

  1. // 首先获取最新的组织架构数据
  2. const { depts } = await getDepartments()

部门名称不能和**同级别**的重复,这里注意,我们需要找到所有同级别的数据,进行校验,所以还需要另一个参数pid

  1. props: {
  2. // 用来控制窗体是否显示或者隐藏
  3. showDialog: {
  4. type: Boolean,
  5. default: false
  6. },
  7. // 当前操作的节点
  8. treeNode: {
  9. type: Object,
  10. default: null
  11. }
  12. },
  13. <add-dept :show-dialog="showDialog" :tree-node="node" />

根据当前部门id,找到所有子部门相关的数据,判断是否重复

  1. // 现在定义一个函数 这个函数的目的是 去找 同级部门下 是否有重复的部门名称
  2. const checkNameRepeat = async(rule, value, callback) => {
  3. // 先要获取最新的组织架构数据
  4. const { depts } = await getDepartments()
  5. // depts是所有的部门数据
  6. // 如何去找技术部所有的子节点
  7. const isRepeat = depts.filter(item => item.pid === this.treeNode.id).some(item => item.name === value)
  8. isRepeat ? callback(new Error(`同级部门下已经有${value}的部门了`)) : callback()
  9. }

检查部门编码的过程同理

  1. // 检查编码重复
  2. const checkCodeRepeat = async(rule, value, callback) => {
  3. // 先要获取最新的组织架构数据
  4. const { depts } = await getDepartments()
  5. const isRepeat = depts.some(item => item.code === value && value) // 这里加一个 value不为空 因为我们的部门有可能没有code
  6. isRepeat ? callback(new Error(`组织架构中已经有部门使用${value}编码`)) : callback()
  7. }

在规则中定义

  1. // 定义校验规则
  2. rules: {
  3. name: [{ required: true, message: '部门名称不能为空', trigger: 'blur' },
  4. { min: 1, max: 50, message: '部门名称要求1-50个字符', trigger: 'blur' }, {
  5. trigger: 'blur',
  6. validator: checkNameRepeat // 自定义函数的形式校验
  7. }],
  8. code: [{ required: true, message: '部门编码不能为空', trigger: 'blur' },
  9. { min: 1, max: 50, message: '部门编码要求1-50个字符', trigger: 'blur' }, {
  10. trigger: 'blur',
  11. validator: checkCodeRepeat
  12. }],
  13. manager: [{ required: true, message: '部门负责人不能为空', trigger: 'blur' }],
  14. introduce: [{ required: true, message: '部门介绍不能为空', trigger: 'blur' },
  15. { trigger: 'blur', min: 1, max: 300, message: '部门介绍要求1-50个字符' }]
  16. }

[08-员工管理]_员工资料excel的导入导出

目的:根据员工的excel资料批量导入员工,或批量导出员工数据
Excel 的导入导出都是依赖于js-xlsx来实现的。
js-xlsx的基础上又封装了Export2Excel.js来方便导出数据。

[09-权限设计与管理]_前端实现动态权限

权限受控的主体思路

在上面的几个小节中,我们已经把给用户分配了角色, 给角色分配了权限,那么在用户登录获取资料的时候,会自动查出该用户拥有哪些权限,这个权限需要和我们的菜单还有路由有效结合起来
image-20200730002842243.png
在权限管理页面中,我们设置了一个标识, 这个标识可以和我们的路由模块进行关联,也就是说,如果用户拥有这个标识,那么用户就可以拥有这个路由模块,如果没有这个标识,就不能访问路由模块,这样子我们只要根据后端返回的标识进行查询,动态的对路由进行添加就可以实现 动态路由

用什么来实现?

vue-router提供了一个叫做addRoutes的API方法,这个方法的含义是动态添加路由规则,思路如下
image-20200901164312005.png

新建Vuex中管理权限的模块

主页模块章节中,我们将用户的资料设置到vuex中,其中便有权限数据,我们可以就此进行操作

我们可以在vuex中新增一个permission模块
**src/store/modules/permission.js**

  1. // vuex的权限模块
  2. import { constantRoutes } from '@/router'
  3. // vuex中的permission模块用来存放当前的 静态路由 + 当前用户的 权限路由
  4. const state = {
  5. routes: constantRoutes // 所有人默认拥有静态路由
  6. }
  7. const mutations = {
  8. // newRoutes可以认为是 用户登录 通过权限所得到的动态路由的部分
  9. setRoutes(state, newRoutes) {
  10. // 下面这么写不对 不是语法不对 是业务不对
  11. // state.routes = [...state.routes, ...newRoutes]
  12. // 有一种情况 张三 登录 获取了动态路由 追加到路由上 李四登录 4个动态路由
  13. // 应该是每次更新 都应该在静态路由的基础上进行追加
  14. state.routes = [...constantRoutes, ...newRoutes]
  15. }
  16. }
  17. const actions = {}
  18. export default {
  19. namespaced: true,
  20. state,
  21. mutations,
  22. actions
  23. }

在Vuex管理模块中引入permisson模块

  1. import permission from './modules/permission'
  2. const store = new Vuex.Store({
  3. modules: {
  4. // 子模块 $store.state.app.
  5. // mapGetters([])
  6. app,
  7. settings,
  8. user,
  9. permission
  10. },
  11. getters
  12. })

Vuex筛选权限路由

OK, 那么我们在哪将用户的标识和权限进行关联呢 ?

image-20200815184203715.png
我们可以在这张图中,进一步的进行操作
image-20200815184407204.png
访问权限的数据在用户属性**menus**中,**menus**中的标识该怎么和路由对应呢?
image-20200815185230597.png

可以将路由模块的根节点**name**属性命名和权限标识一致,这样只要标识能对上,就说明用户拥有了该权限

这一步,在我们命名路由的时候已经操作过了
image-20200730011629326.png
接下来, vuex的permission中提供一个action,进行关联

  1. import { asyncRoutes, constantRoutes } from '@/router'
  2. const actions = {
  3. // 筛选权限路由
  4. // 第二个参数为当前用户的所拥有的菜单权限
  5. // menus: ["settings","permissions"]
  6. // asyncRoutes是所有的动态路由
  7. // asyncRoutes [{path: 'setting',name: 'setting'},{}]
  8. filterRoutes(context, menus) {
  9. const routes = []
  10. // 筛选出 动态路由中和menus中能够对上的路由
  11. menus.forEach(key => {
  12. // key就是标识
  13. // asyncRoutes 找 有没有对象中的name属性等于 key的 如果找不到就没权限 如果找到了 要筛选出来
  14. routes.push(...asyncRoutes.filter(item => item.name === key)) // 得到一个数组 有可能 有元素 也有可能是空数组
  15. })
  16. // 得到的routes是所有模块中满足权限要求的路由数组
  17. // routes就是当前用户所拥有的 动态路由的权限
  18. context.commit('setRoutes', routes) // 将动态路由提交给mutations
  19. return routes // 这里为什么还要return state数据 是用来 显示左侧菜单用的 return 是给路由addRoutes用的
  20. }

权限拦截出调用筛选权限Action

在拦截的位置,调用关联action, 获取新增routes,并且addRoutes

  1. // 权限拦截在路由跳转 导航守卫
  2. import router from '@/router'
  3. import store from '@/store' // 引入store实例 和组件中的this.$store是一回事
  4. import nprogress from 'nprogress' // 引入进度条
  5. import 'nprogress/nprogress.css' // 引入进度条样式
  6. // 不需要导出 因为只需要让代码执行即可
  7. // 前置守卫
  8. // next是前置守卫必须必须必须执行的钩子 next必须执行 如果不执行 页面就死了
  9. // next() 放过
  10. // next(false) 跳转终止
  11. // next(地址) 跳转到某个地址
  12. const whiteList = ['/login', '/404'] // 定义白名单
  13. router.beforeEach(async(to, from, next) => {
  14. nprogress.start() // 开启进度条的意思
  15. if (store.getters.token) {
  16. // 只有有token的情况下 才能获取资料
  17. // 如果有token
  18. if (to.path === '/login') {
  19. // 如果要访问的是 登录页
  20. next('/') // 跳到主页 // 有token 用处理吗?不用
  21. } else {
  22. // 只有放过的时候才去获取用户资料
  23. // 是每次都获取吗
  24. // 如果当前vuex中有用户的资料的id 表示 已经有资料了 不需要获取了 如果没有id才需要获取
  25. if (!store.getters.userId) {
  26. // 如果没有id才表示当前用户资料没有获取过
  27. // async 函数所return的内容 用 await就可以接收到
  28. const { roles } = await store.dispatch('user/getUserInfo')
  29. // 如果说后续 需要根据用户资料获取数据的话 这里必须改成 同步
  30. // 筛选用户的可用路由
  31. // actions中函数 默认是Promise对象 调用这个对象 想要获取返回的值话 必须 加 await或者是then
  32. // actions是做异步操作的
  33. const routes = await store.dispatch('permission/filterRoutes', roles.menus)
  34. // routes就是筛选得到的动态路由
  35. // 动态路由 添加到 路由表中 默认的路由表 只有静态路由 没有动态路由
  36. // addRoutes 必须 用 next(地址) 不能用next()
  37. router.addRoutes(routes) // 添加动态路由到路由表 铺路
  38. // 添加完动态路由之后
  39. next(to.path) // 相当于跳到对应的地址 相当于多做一次跳转 为什么要多做一次跳转
  40. // 进门了,但是进门之后我要去的地方的路还没有铺好,直接走,掉坑里,多做一次跳转,再从门外往里进一次,跳转之前 把路铺好,再次进来的时候,路就铺好了
  41. } else {
  42. next()
  43. }
  44. }
  45. } else {
  46. // 没有token的情况下
  47. if (whiteList.indexOf(to.path) > -1) {
  48. // 表示要去的地址在白名单
  49. next()
  50. } else {
  51. next('/login')
  52. }
  53. }
  54. nprogress.done() // 解决手动切换地址时 进度条不关闭的问题
  55. })
  56. // 后置守卫
  57. router.afterEach(() => {
  58. nprogress.done() // 关闭进度条
  59. })

静态路由动态路由解除合并

注意: 这里有个非常容易出问题的位置,当我们判断用户是否已经添加路由的前后,不能都是用next()
在添加路由之后应该使用 next(to.path), 否则会使刷新页面之后 权限消失,这属于一个vue-router的已知缺陷
同时,不要忘记,我们将原来的静态路由 + 动态路由合体的模式 改成 只有静态路由 **src/router/index.js**
image-20200730012805239.png

此时,我们已经完成了权限设置的一半, 此时我们发现左侧菜单失去了内容,这是因为左侧菜单读取的是固定的路由,我们要把它换成实时的最新路由

**src/store/getters.js**配置导出routes

  1. const getters = {
  2. sidebar: state => state.app.sidebar,
  3. device: state => state.app.device,
  4. token: state => state.user.token,
  5. name: state => state.user.userInfo.username, // 建立用户名称的映射
  6. userId: state => state.user.userInfo.userId, // 建立用户id的映射
  7. companyId: state => state.user.userInfo.companyId, // 建立用户的公司Id映射
  8. routes: state => state.permission.routes // 导出当前的路由
  9. }
  10. export default getters

在左侧菜单组件中, 引入routes

  1. computed: {
  2. ...mapGetters([
  3. 'sidebar', 'routes'
  4. ]),

OK,到现在为止,我们已经可以实现不同用户登录的时候,菜单是动态的了

[12]_全屏插件

目标:实现页面的全屏功能

全屏功能可以借助一个插件来实现

第一步,安装全局插件screenfull

  1. $ npm i screenfull

第二步,封装全屏显示的插件·· **src/components/ScreenFull/index.vue**

  1. <template>
  2. <!-- 放置一个图标 -->
  3. <div>
  4. <!-- 放置一个svg的图标 -->
  5. <svg-icon icon-class="fullscreen" style="color:#fff; width: 20px; height: 20px" @click="changeScreen" />
  6. </div>
  7. </template>
  8. <script>
  9. import ScreenFull from 'screenfull'
  10. export default {
  11. methods: {
  12. // 改变全屏
  13. changeScreen() {
  14. if (!ScreenFull.isEnabled) {
  15. // 此时全屏不可用
  16. this.$message.warning('此时全屏组件不可用')
  17. return
  18. }
  19. // document.documentElement.requestFullscreen() 原生js调用
  20. // 如果可用 就可以全屏
  21. ScreenFull.toggle()
  22. }
  23. }
  24. }
  25. </script>
  26. <style>
  27. </style>

然后将组件引入即可使用

[12]_多语言 vue-I18n初体验

**目标**实现国际化语言切换

初始化多语言包

本项目使用国际化 i18n 方案。通过 vue-i18n而实现。
第一步,我们需要首先国际化的包

  1. $ npm i vue-i18n

第二步,需要单独一个多语言的实例化文件 **src/lang/index.js**

  1. import Vue from 'vue' // 引入Vue
  2. import VueI18n from 'vue-i18n' // 引入国际化的包
  3. import Cookie from 'js-cookie' // 引入cookie包
  4. import elementEN from 'element-ui/lib/locale/lang/en' // 引入饿了么的英文包
  5. import elementZH from 'element-ui/lib/locale/lang/zh-CN' // 引入饿了么的中文包
  6. Vue.use(VueI18n) // 全局注册国际化包
  7. export default new VueI18n({
  8. locale: Cookie.get('language') || 'zh', // 从cookie中获取语言类型 获取不到就是中文
  9. messages: {
  10. en: {
  11. ...elementEN // 将饿了么的英文语言包引入
  12. },
  13. zh: {
  14. ...elementZH // 将饿了么的中文语言包引入
  15. }
  16. }
  17. })

上面的代码的作用是将Element的两种语言导入了

第三步,在main.js中对挂载 i18n的插件,并设置element为当前的语言

  1. // 设置element为当前的语言
  2. Vue.use(ElementUI, {
  3. i18n: (key, value) => i18n.t(key, value)
  4. })
  5. new Vue({
  6. el: '#app',
  7. router,
  8. store,
  9. i18n,
  10. render: h => h(App)
  11. })

引入自定义语言包

此时,element已经变成了zh,也就是中文,但是我们常规的内容怎么根据当前语言类型显示?

这里,针对英文和中文,我们可以提供两个不同的语言包 **src/lang/zh.js , src/lang/en.js**

该语言包,我们已经在资源中提供

第四步,在index.js中同样引入该语言包

  1. import customZH from './zh' // 引入自定义中文包
  2. import customEN from './en' // 引入自定义英文包
  3. Vue.use(VueI18n) // 全局注册国际化包
  4. export default new VueI18n({
  5. locale: Cookie.get('language') || 'zh', // 从cookie中获取语言类型 获取不到就是中文
  6. messages: {
  7. en: {
  8. ...elementEN, // 将饿了么的英文语言包引入
  9. ...customEN
  10. },
  11. zh: {
  12. ...elementZH, // 将饿了么的中文语言包引入
  13. ...customZH
  14. }
  15. }
  16. })

在左侧菜单中应用多语言包

自定义语言包的内容怎么使用?

第五步,在左侧菜单应用
当我们全局注册i18n的时候,每个组件都会拥有一个**$t**的方法,它会根据传入的key,自动的去寻找当前语言的文本,我们可以将左侧菜单变成多语言展示文本
**layout/components/SidebarItem.vue**

  1. <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="$t('route.'+onlyOneChild.name)" />

注意:当文本的值为嵌套时,可以通过**$t(key1.key2.key3...)**的方式获取

现在,我们已经完成了多语言的接入,现在封装切换多语言的组件

封装多语言插件

第六步,封装多语言组件 **src/components/lang/index.vue**

  1. <template>
  2. <el-dropdown trigger="click" @command="changeLanguage">
  3. <!-- 这里必须加一个div -->
  4. <div>
  5. <svg-icon style="color:#fff;font-size:20px" icon-class="language" />
  6. </div>
  7. <el-dropdown-menu slot="dropdown">
  8. <el-dropdown-item command="zh" :disabled="'zh'=== $i18n.locale ">中文</el-dropdown-item>
  9. <el-dropdown-item command="en" :disabled="'en'=== $i18n.locale ">en</el-dropdown-item>
  10. </el-dropdown-menu>
  11. </el-dropdown>
  12. </template>
  13. <script>
  14. import Cookie from 'js-cookie'
  15. export default {
  16. methods: {
  17. changeLanguage(lang) {
  18. Cookie.set('language', lang) // 切换多语言
  19. this.$i18n.locale = lang // 设置给本地的i18n插件
  20. this.$message.success('切换多语言成功')
  21. }
  22. }
  23. }
  24. </script>

第七步,在Navbar组件中引入

  1. <!-- 放置切换多语言 -->
  2. <lang class="right-menu-item" />
  3. <!-- 放置主题 -->
  4. <theme-picker class="right-menu-item" />
  5. <!-- 放置全屏插件 -->
  6. <screen-full class="right-menu-item" />

[13-打包上线]_打包之前的路由模式

**目标**配置打包之前的路由模式

在SPA单页应用中,有两种路由模式

hash模式 : #后面是路由路径,特点是前端访问,#后面的变化不会经过服务器
history模式:正常的/访问模式,特点是后端访问,任意地址的变化都会访问服务器

开发到现在,我们一直都在用hash模式,打包我们尝试用history模式

改成history模式非常简单,只需要将路由的mode类型改成history即可

  1. const createRouter = () => new Router({
  2. mode: 'history', // require service support
  3. scrollBehavior: () => ({ y: 0 }), // 管理滚动行为 如果出现滚动 切换就让 让页面回到顶部
  4. routes: [...constantRoutes] // 改成只有静态路由
  5. })

假设我们的地址是这样的 **www.xxxx/com/hr**/a **www.xxxx/com/hr**/b

我们会发现,其实域名是**www.xxxx/com**,hr是特定的前缀地址,此时我们可以配置一个base属性,配置为hr

  1. const createRouter = () => new Router({
  2. mode: 'history', // require service support
  3. base: '/hr/', // 配置项目的基础地址
  4. scrollBehavior: () => ({ y: 0 }), // 管理滚动行为 如果出现滚动 切换就让 让页面回到顶部
  5. routes: [...constantRoutes] // 改成只有静态路由
  6. })

此时,我们会发现地址已经变成我们想要的样子了
image-20200804014626686.png

[13-打包上线]_性能分析和CDN的应用(性能优化)

**分析**:先进行打包的性能分析,系统分析占用空间较大的几个模块,然后利用webpack的排除打包将几个大模块的包排除,然后利用webpack的CDN文件配置,配置对应的CDN资源。
**目标**: 对开发的应用进行性能分析和CDN的应用

性能分析

我们集成了 功能,写了很多组件,最终都会打包成一堆文件,那么真实运行的性能如何呢?

我们可以使用vue-cli本身提供的性能分析工具,对我们开发的所有功能进行打包分析
它的应用非常简单

  1. $ npm run preview -- --report

这个命令会从我们的**入口main.js**进行依赖分析,分析出最大的包,方便我们进行观察和优化
执行完这个命令,我们会看到如下的页面
image-20200804015849396.png
如图所以,方块越大,说明该文件占用的文件越大,文件越大,对于网络带宽和访问速度的要求就越高,这也就是我们优化的方向

像这种情况,我们怎么优化一下呢

webpack排除打包

CDN是一个比较好的方式

文件不是大吗?我们就不要把这些大的文件和那些小的文件打包到一起了,像这种xlsx,element这种功能性很全的插件,我们可以放到CDN服务器上,一来,减轻整体包的大小,二来CDN的加速服务可以加快我们对于插件的访问速度

使用方式
先找到 vue.config.js, 添加 externalswebpack 不打包 xlsxelement
**vue.config.js**

  1. // 排除 elementUI xlsx 和 vue
  2. externals:
  3. {
  4. 'vue': 'Vue',
  5. 'element-ui': 'ELEMENT',
  6. 'xlsx': 'XLSX'
  7. }

再次运行,我们会发现包的大小已经大幅减小

CDN文件配置

但是,没有被打包的几个模块怎么处理?

可以采用CDN的方式,在页面模板中预先引入
**vue.config.js**

  1. const cdn = {
  2. css: [
  3. // element-ui css
  4. 'https://unpkg.com/element-ui/lib/theme-chalk/index.css' // 样式表
  5. ],
  6. js: [
  7. // vue must at first!
  8. 'https://unpkg.com/vue/dist/vue.js', // vuejs
  9. // element-ui js
  10. 'https://unpkg.com/element-ui/lib/index.js', // elementUI
  11. 'https://cdn.jsdelivr.net/npm/xlsx@0.16.6/dist/jszip.min.js',
  12. 'https://cdn.jsdelivr.net/npm/xlsx@0.16.6/dist/xlsx.full.min.js'
  13. ]
  14. }

但是请注意,这时的配置实际上是对开发环境和生产环境都生效的,在开发环境时,没有必要使用CDN,此时我们可以使用环境变量来进行区分

  1. let cdn = { css: [], js: [] }
  2. // 通过环境变量 来区分是否使用cdn
  3. const isProd = process.env.NODE_ENV === 'production' // 判断是否是生产环境
  4. let externals = {}
  5. if (isProd) {
  6. // 如果是生产环境 就排除打包 否则不排除
  7. externals = {
  8. // key(包名) / value(这个值 是 需要在CDN中获取js, 相当于 获取的js中 的该包的全局的对象的名字)
  9. 'vue': 'Vue', // 后面的名字不能随便起 应该是 js中的全局对象名
  10. 'element-ui': 'ELEMENT', // 都是js中全局定义的
  11. 'xlsx': 'XLSX' // 都是js中全局定义的
  12. }
  13. cdn = {
  14. css: [
  15. 'https://unpkg.com/element-ui/lib/theme-chalk/index.css' // 提前引入elementUI样式
  16. ], // 放置css文件目录
  17. js: [
  18. 'https://unpkg.com/vue/dist/vue.js', // vuejs
  19. 'https://unpkg.com/element-ui/lib/index.js', // element
  20. 'https://cdn.jsdelivr.net/npm/xlsx@0.16.6/dist/xlsx.full.min.js', // xlsx 相关
  21. 'https://cdn.jsdelivr.net/npm/xlsx@0.16.6/dist/jszip.min.js' // xlsx 相关
  22. ] // 放置js文件目录
  23. }
  24. }

注入CDN文件到模板

之后通过 html-webpack-plugin注入到 index.html之中:

  1. config.plugin('html').tap(args => {
  2. args[0].cdn = cdn
  3. return args
  4. })

找到 public/index.html。通过你配置的CDN Config 依次注入 css 和 js。

  1. <head>
  2. <!-- 引入样式 -->
  3. <% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
  4. <link rel="stylesheet" href="<%=css%>">
  5. <% } %>
  6. </head>
  7. <!-- 引入JS -->
  8. <% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
  9. <script src="<%=js%>"></script>
  10. <% } %>

最后,进行打包

  1. $ npm run build:prod

[13-打包上线]-在node环境中应用并代理跨域

**目标**将打包好的代码打包上线,并在nodejs中代理跨域

使用koa框架部署项目

到现在为止,我们已经完成了一个前端工程师的开发流程,按照常规的做法,此时,运维会将我们的代码部署到阿里云的ngix服务上,对于我们而言,我们可以将其部署到本机的nodejs环境中

部署 自动化部署 /手动部署
第一步,建立web服务文件夹 **hrServer**

  1. $ mkdir hrServer #建立hrServer文件夹

第二步,在该文件夹下,初始化npm

  1. $ npm init -y

第三步,安装服务端框架koa(也可以采用express或者egg)

  1. $ npm i koa koa-static

第四步,拷贝上小节打包的dist目录到**hrServer/public**
第五步,在根目录下创建app.js,代码如下

  1. const Koa = require('koa')
  2. const serve = require('koa-static');
  3. const app = new Koa();
  4. app.use(serve(__dirname + "/public")); //将public下的代码静态化
  5. app.listen(3333, () => {
  6. console.log('人资项目启动')
  7. })

此时,我们可以访问,http://localhost:3333

页面出来了
image-20200805012430884.png

解决history页面访问问题

但是,此时存在两个问题,

  1. 当我们刷新页面,发现404

    这是因为我们采用了history的模式,地址的变化会引起服务器的刷新,我们只需要在app.js对所有的地址进行一下处理即可

安装 koa中间件

  1. $ npm i koa2-connect-history-api-fallback #专门处理history模式的中间件

注册中间件

  1. const Koa = require('koa')
  2. const serve = require('koa-static');
  3. const { historyApiFallback } = require('koa2-connect-history-api-fallback');
  4. const path = require('path')
  5. const app = new Koa();
  6. // 这句话 的意思是除接口之外所有的请求都发送给了 index.html
  7. app.use(historyApiFallback({
  8. whiteList: ['/prod-api']
  9. })); // 这里的whiteList是 白名单的意思
  10. app.use(serve(__dirname + "/public")); //将public下的代码静态化
  11. app.listen(3333, () => {
  12. console.log('人资项目启动')
  13. })

解决生产环境跨域问题

  1. 当点击登录时,发现接口404

    前面我们讲过,vue-cli的代理只存在于开发期,当我们上线到node环境或者ngix环境时,需要我们再次在环境中代理

在nodejs中代理
安装跨域代理中间件

  1. $ npm i koa2-proxy-middleware

配置跨越代理

  1. const proxy = require('koa2-proxy-middleware')
  2. app.use(proxy({
  3. targets: {
  4. // (.*) means anything
  5. '/prod-api/(.*)': {
  6. target: 'http://ihrm-java.itheima.net/api', //后端服务器地址
  7. changeOrigin: true,
  8. pathRewrite: {
  9. '/prod-api': ""
  10. }
  11. }
  12. }
  13. }))

注意:这里之所以用了pathRewrite,是因为生产环境的请求基础地址是 /prod-api,需要将该地址去掉
此时,我们的项目就可以跨域访问了!