Vue项目最佳实践
资源
目标
- 项目配置
- 权限管理
- 导航菜单
- 数据mock
- 测试
知识点
项目配置策略
基础配置:指定应用上下文、端口号,vue.config.js
const port = 7070;module.exports = {publicPath: '/best-practice', // 部署应用包时的基本 URLdevServer: {port,}};
配置webpack:configureWebpack
范例:设置一个组件存放路径的别名,vue.config.js
const path = require('path')module.exports = {configureWebpack: {resolve: {alias: {comps: path.join(__dirname, 'src/components'),}}}}
范例:设置一个webpack配置项用于⻚面title,vue.config.js
module.exports = {configureWebpack: {name: "vue项目最佳实践"}};
在宿主⻚面使用lodash插值语法使用它,./public/index.html
<title><%= webpackConfig.name %></title>
webpack-merge合并出最终选项
范例:基于环境有条件地配置,vue.config.js
// 传递一个函数给configureWebpack// 可以直接修改,或返回一个用于合并的配置对象configureWebpack: config => {config.resolve.alias.comps = path.join(__dirname, 'src/components')if (process.env.NODE_ENV === 'development') {config.name = 'vue项目最佳实践'} else {config.name = 'Vue Best Practice'}}
配置webpack:chainWebpack
webpack-chain称为链式操作,可以更细粒度控制webpack内部配置。
范例:svg icon引入
- 下载图标,存入src/icons/svg中
安装依赖:svg-sprite-loader
npm i svg-sprite-loader -D
修改规则和新增规则,vue.config.js ```javascript // resolve定义一个绝对路径获取函数 const path = require(‘path’)
function resolve (dir) { return path.join(__dirname, dir) } //… chainWebpack(config) { // 配置svg规则排除icons目录中svg文件处理 // 目标给svg规则增加一个排除选项exclude:[‘path/to/icon’] config.module.rule(“svg”) .exclude.add(resolve(“src/icons”))
// 新增icons规则,设置svg-sprite-loader处理icons目录中的svg config.module.rule(‘icons’) .test(/.svg$/) .include.add(resolve(‘./src/icons’)).end() .use(‘svg-sprite-loader’) .loader(‘svg-sprite-loader’) .options({ symbolId: ‘icon-[name]’ }) }
- 使用图标,App.vue```javascript<template><svg><use xlink:href="#icon-wx" /></svg></template><script>import '@/icons/svg/wx.svg'</script>
- 自动导入
创建icons/index.js
const req = require.context('./svg', false, /\.svg$/)req.keys().map(req);
创建SvgIcon组件,components/SvgIcon.vue
<template><svg :class="svgClass" v-on="$listeners"><use :xlink:href="iconName" /></svg></template><script>export default {name: 'SvgIcon',props: {iconClass: {type: String,required: true},className: {type: String,default: ''}},computed: {iconName () {return `#icon-${this.iconClass}`},svgClass () {if (this.className) {return 'svg-icon ' + this.className} else {return 'svg-icon'}}}}</script><style scoped>.svg-icon {width: 1em;height: 1em;vertical-align: -0.15em;fill: currentColor;overflow: hidden;}</style>
/
环境变量和模式
如果想给多种环境做不同配置,可以利用vue-cli提供的 模式 。默认有development、production、test三种模式,对应的,它们的配置文件形式是.env.development。
范例:定义一个开发时可用的配置项,创建.env.dev
# 只能用于服务端foo=bar# 可用于客户端VUE_APP_DONG=dong
修改mode选项覆盖模式名称,package.json
"serve": "vue-cli-service serve --mode dev"
权限控制
参考代码:git reset —hard step-7

路由定义
路由分为两种:
constantRoutes:通用路由可直接访问asyncRoutes:权限路由,需要先登录,获取⻆色后才能判断是否可以访问
router/index.js
路由守卫
- 默认路由守卫规则:
- 已登录访问登录⻚:跳转首⻚
- 已登录访问其他⻚:
- 已获取⻆色:放行
- 为获取⻆色:请求⻆色 =》过滤可访问路由 =》动态增加到router
- 未登录访问白名单⻚面:放行
- 未登录访问其他⻚:跳转至登录⻚
参考代码:src/permission.js
用户登录
请求登录dispatch('user/login'),Login.vue

用户⻆色获取
登录成功后,请求用户⻆色信息

权限路由过滤
根据⻆色过滤asyncRoutes,并动态添加至router。
请求生成路由,src/permission.js
生成路由,store/modules/permission.js

异步获取路由表 可以当用户登录后 向后端请求可访问的路由表 ,从而动态生成可访问⻚面,操作和原来是相同的,这里多了一步将后端返回路由表中 组件名称和本地的组件映射 步骤:
// 前端组件名和组件映射表const map = {//xx: require('@/views/xx.vue').default // 同步的方式xx: () => import('@/views/xx.vue') // 异步的方式}// 服务端返回的asyncRoutesconst asyncRoutes = [{ path: '/xx', component: 'xx', ... }]// 遍历asyncRoutes,将component替换为map[component]function mapComponent (asyncRoutes) {asyncRoutes.forEach(route => {route.component = map[route.component];if (route.children) {route.children.map(child => mapComponent(child))}})}mapComponent(asyncRoutes)
按钮权限
⻚面中按钮、链接需要更细粒度权限控制时,可封装一个指令v-permission,实现按钮级别的权限控制。
创建指令,src/directives/permission.js
该指令只能删除挂载指令的元素,对于那些额外生成的和指令无关的元素无能为力,比如:
<el-tabs><el-tab-pane label="用户管理" name="first">用户管理</el-tab-pane><el-tab-pane label="⻆色管理" name="third">⻆色管理</el-tab-pane></el-tabs>
尝试添加
v-permission="..."并不能删除动态生成的内容部分
<el-tab-pane label="用户管理" name="first"v-permission="['admin', 'editor']">用户管理</el-tab-pane>
此时可使用v-if来实现
<template><el-tab-pane v-if="checkPermission(['admin'])"></template>
导航菜单
导航菜单可根据前面生成的最终路由信息动态生成。
侧边栏组件,components/Sidebar/index.vue
侧边栏菜项目组件,components/Sidebar/SidebarItem.vue
默认菜单项规则:
- 跳转链接:没有子路由或只有一个需要展示子路由

- 其他情况:嵌套子菜单

菜单项组件,components/Sidebar/Item.vue
数据交互
常⻅需求:
- 统一配置请求库
- 请求拦截和响应拦截
- 数据mock
- 请求代理
请求封装
对axios做一次封装,统一处理配置、请求和响应拦截,utils/request.js
数据mock
数据模拟两种常⻅方式, 本地mock 和 线上mock
本地mock
在vue.config.js中定义模拟接口
线上mock
诸如easy-mock这类线上mock工具优点是使用简单,mock工具强大,还能整合swagger。
环境搭建
- 线上使用:登录easy-mock
- 搭建本地服务(基于docker)
- 安装docker desktop
- 创建docker-compose.yml ```javascript version: ‘3’
services: mongodb: image: mongo:3.4.1
# volumes:# /apps/easy-mock/data/db是数据库文件存放地址,根据需要修改为本地地址# - '/apps/easy-mock/data/db:/data/db'networks:- easy-mockrestart: always
redis: image: redis:4.0.6 command: redis-server —appendonly yes
volumes:
/apps/easy-mock/data/redis 是 redis 数据文件存放地址,根据需要修改为本地地址
- ‘/apps/easy-mock/data/redis:/data’
networks:
- easy-mock
restart: always
web: image: easymock/easymock:1.6.0
easy-mock 官方给出的文件,这里是 npm start,这里修改为 npm run dev
command: /bin/bash -c “npm run dev” ports:
- 7300:7300
volumes:
# 日志地址,根据需要修改为本地地址# - '/apps/easy-mock/logs:/home/easy-mock/easy-mock/logs'
networks:
- easy-mock
restart: always
networks: easy-mock:
- 启动:docker-compose up<a name="w4SxH"></a>#### 使用介绍- 创建一个项目- 创建需要的接口:登录接口`user/login````javascript{"code": function({ _req }) {const { username } = _req.body;if (username === "admin" || username === "jerry") {return 1} else {return 10008}},"data": function({ _req }) {const { username } = _req.body;if (username === "admin" || username === "jerry") {return username} else {return ''}}}
用户⻆色接口:user/info
{code: 1,"data": function({ _req }) {return _req.headers['authorization'].split(' ')[1] === 'admin' ?['admin'] : ['editor']}}
- 调用:修改base_url,.env.development
VUE_APP_BASE_API = 'http://localhost:7300/mock/5f6301c446875b001d8a2961'
解决跨域
如果请求的接口在另一台服务器上,开发时则需要设置代理避免跨域问题
代理配置,vue.config.js
测试
测试分类
常⻅的开发流程里,都有测试人员,他们不管内部实现机制,只看最外层的输入输出,这种我们称为 黑盒测试 。比如你写一个加法的⻚面,会设计N个用例,测试加法的正确性,这种测试我们称之为 E2E测试 。
还有一种测试叫做 白盒测试 ,我们针对一些内部核心实现逻辑编写测试代码,称之为 单元测试 。
更负责一些的我们称之为 集成测试 ,就是集合多个测试过的单元一起测试。
测试的好处
- 提供描述组件行为的文档
- 节省手动测试的时间
- 减少研发新特性时产生的 bug
- 改进设计
- 促进重构
准备工作
在vue-cli中,预置了Mocha+Chai和Jest两套单测方案,我们的演示代码使用Jest,它们语法基本一致
新建vue项目时
选择特性Unit Testing和E2E Testing
单元测试解决方案选择:Jest
在已存在项目中集成
集成Jest:vue add @vue/unit-jest
集成cypress:vue add @vue/e2e-cypress
编写单元测试
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
新建test/unit/kaikeba.spec.js,*.spec.js是命名规范
function add (num1, num2) {return num1 + num}// 测试套件 test suitedescribe('add方法', () => {// 测试用例 test caseit('应该能正确计算加法', () => {// 断言 assertexpect(add(1, 3)).toBe(4)})})
更多断言API
执行单元测试
执行:npm run test:unit
测试Vue组件
官方提供了用于单元测试的实用工具库@vue/test-utils
检查mounted之后预期结果
使用mount或shallowMount挂载组件,example.spec.js
import { mount } from '@vue/test-utils'it('renders props.msg when passed', () => {const msg = 'new message'// 给组件传递属性const wrapper = shallowMount(HelloWorld, {propsData: { msg }})// expect(wrapper.text()).toMatch(msg)// 查找元素const h1 = wrapper.find('h1')expect(h1.text()).toBe('new message')})
更新操作通常是异步的,dom更新结果放在await语句后面测试
<p class="p1" @click="foo = 'baz'">{{foo}}</p>
test('点击p之后验证更新结果 ', async () => {const wrapper = shallowMount(HelloWorld)// 模拟点击行为const p1 = wrapper.find('.p1')// 把变更状态操作放在await后面await p1.trigger('click')expect(p1.text()).toBe('baz')})
获取自定义组件
<comp v-if="foo === 'baz'"></comp>
components: {comp: {name: 'comp',render (h) {return h('div', 'comp')}},}const comp = wrapper.findComponent({name: 'comp'})expect(comp.exists()).toBe(true)
覆盖率
Jest自带覆盖率,很容易统计我们测试代码是否全面。
package.json里修改jest配置
"jest": {"collectCoverage": true,"collectCoverageFrom": ["src/**/*.{js,vue}"],}
若采用独立配置,则修改jest.config.js:
module.exports = {"collectCoverage": true,"collectCoverageFrom": ["src/**/*.{js,vue}"]}
再次执行npm run test:unit
可以看到HelloWorld.vue的覆盖率是50%
stmts是语句覆盖率(statement coverage):是不是每个语句都执行了?
Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了?
Funcs函数覆盖率(function coverage):是不是每个函数都调用了?
Lines行覆盖率(line coverage):是不是每一行都执行了?
通过分析报告可以找到没有覆盖的地方,coverage/Icov-report/index.html
我们添加一些测试代码
test('验证comp组件render结果 ', () => {const h = (type, children) => ({ type, children })const vnode = HelloWorld.components.comp.render(h)console.log(vnode);expect(vnode).toEqual({ type: 'div', children: 'comp' })})
现在覆盖率是100%了,但是整个项目还很糟糕,我们还需要继续努力提高呀!
E2E测试
借用浏览器的能力,站在用户测试人员的⻆度,输入框,点击按钮等,完全模拟用户,这个和具体的框架关系不大,完全模拟浏览器行为。
运行E2E测试
npm run test:e2e
修改e2e/spec/test.js
// https://docs.cypress.io/api/introduction/api.htmldescribe('端到端测试,抢测试人员的饭碗', () => {it('先访问一下', () => {cy.visit('/')// cy.contains('h1', 'Welcome to Your Vue.js App')cy.contains('span', '开课吧')})})

测试未通过,因为没有使用Kaikeba.vue,修改App.vue
<div id="app"><img alt="Vue logo" src="./assets/logo.png"><!-- <HelloWorld msg="Welcome to Your Vue.js App"/> --><Kaikeba></Kaikeba></div>import Kaikeba from './components/Kaikeba.vue'export default {name: 'app',components: {HelloWorld, Kaikeba}}
测试通过~
测试用户点击
// https://docs.cypress.io/api/introduction/api.htmldescribe('端到端测试,抢测试人员的饭碗', () => {it('先访问一下', () => {cy.visit('/')// cy.contains('h1', 'Welcome to Your Vue.js App')cy.contains('#message', '开课吧')cy.get('button').click()cy.contains('span', '按钮点击')})})
