Vue项目最佳实践


资源


Vue-CLI 3.0

目标

  • 项目配置
  • 权限管理
  • 导航菜单
  • 数据mock
  • 测试

知识点


项目配置策略

基础配置:指定应用上下文、端口号,vue.config.js

  1. const port = 7070;
  2. module.exports = {
  3. publicPath: '/best-practice', // 部署应用包时的基本 URL
  4. devServer: {
  5. port,
  6. }
  7. };

配置webpack:configureWebpack

范例:设置一个组件存放路径的别名,vue.config.js

  1. const path = require('path')
  2. module.exports = {
  3. configureWebpack: {
  4. resolve: {
  5. alias: {
  6. comps: path.join(__dirname, 'src/components'),
  7. }
  8. }
  9. }
  10. }

范例:设置一个webpack配置项用于⻚面title,vue.config.js

  1. module.exports = {
  2. configureWebpack: {
  3. name: "vue项目最佳实践"
  4. }
  5. };

在宿主⻚面使用lodash插值语法使用它,./public/index.html

  1. <title><%= webpackConfig.name %></title>

webpack-merge合并出最终选项

范例:基于环境有条件地配置,vue.config.js

  1. // 传递一个函数给configureWebpack
  2. // 可以直接修改,或返回一个用于合并的配置对象
  3. configureWebpack: config => {
  4. config.resolve.alias.comps = path.join(__dirname, 'src/components')
  5. if (process.env.NODE_ENV === 'development') {
  6. config.name = 'vue项目最佳实践'
  7. } else {
  8. config.name = 'Vue Best Practice'
  9. }
  10. }

配置webpack:chainWebpack
webpack-chain称为链式操作,可以更细粒度控制webpack内部配置。

范例:svg icon引入

  • 下载图标,存入src/icons/svg中
  • 安装依赖:svg-sprite-loader

    1. 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]’ }) }

  1. - 使用图标,App.vue
  2. ```javascript
  3. <template>
  4. <svg>
  5. <use xlink:href="#icon-wx" />
  6. </svg>
  7. </template>
  8. <script>
  9. import '@/icons/svg/wx.svg'
  10. </script>
  • 自动导入

创建icons/index.js

  1. const req = require.context('./svg', false, /\.svg$/)
  2. req.keys().map(req);

创建SvgIcon组件,components/SvgIcon.vue

  1. <template>
  2. <svg :class="svgClass" v-on="$listeners">
  3. <use :xlink:href="iconName" />
  4. </svg>
  5. </template>
  6. <script>
  7. export default {
  8. name: 'SvgIcon',
  9. props: {
  10. iconClass: {
  11. type: String,
  12. required: true
  13. },
  14. className: {
  15. type: String,
  16. default: ''
  17. }
  18. },
  19. computed: {
  20. iconName () {
  21. return `#icon-${this.iconClass}`
  22. },
  23. svgClass () {
  24. if (this.className) {
  25. return 'svg-icon ' + this.className
  26. } else {
  27. return 'svg-icon'
  28. }
  29. }
  30. }
  31. }
  32. </script>
  33. <style scoped>
  34. .svg-icon {
  35. width: 1em;
  36. height: 1em;
  37. vertical-align: -0.15em;
  38. fill: currentColor;
  39. overflow: hidden;
  40. }
  41. </style>

/

环境变量和模式

如果想给多种环境做不同配置,可以利用vue-cli提供的 模式 。默认有developmentproductiontest三种模式,对应的,它们的配置文件形式是.env.development

范例:定义一个开发时可用的配置项,创建.env.dev

  1. # 只能用于服务端
  2. foo=bar
  3. # 可用于客户端
  4. VUE_APP_DONG=dong

修改mode选项覆盖模式名称,package.json

  1. "serve": "vue-cli-service serve --mode dev"

权限控制

参考代码:git reset —hard step-7

image.png

路由定义

路由分为两种:

  • constantRoutes :通用路由可直接访问
  • asyncRoutes:权限路由,需要先登录,获取⻆色后才能判断是否可以访问

router/index.js
image.png

路由守卫

  • 默认路由守卫规则:
  • 已登录访问登录⻚:跳转首⻚
  • 已登录访问其他⻚:
    • 已获取⻆色:放行
    • 为获取⻆色:请求⻆色 =》过滤可访问路由 =》动态增加到router
  • 未登录访问白名单⻚面:放行
  • 未登录访问其他⻚:跳转至登录⻚

参考代码:src/permission.js
image.png

用户登录

请求登录dispatch('user/login'),Login.vue
image.png
image.png

用户⻆色获取

登录成功后,请求用户⻆色信息
image.png
image.png

权限路由过滤

根据⻆色过滤asyncRoutes,并动态添加至router。

请求生成路由,src/permission.js
image.png

生成路由,store/modules/permission.js
image.png
image.png

异步获取路由表 可以当用户登录后 向后端请求可访问的路由表 ,从而动态生成可访问⻚面,操作和原来是相同的,这里多了一步将后端返回路由表中 组件名称和本地的组件映射 步骤:

  1. // 前端组件名和组件映射表
  2. const map = {
  3. //xx: require('@/views/xx.vue').default // 同步的方式
  4. xx: () => import('@/views/xx.vue') // 异步的方式
  5. }
  6. // 服务端返回的asyncRoutes
  7. const asyncRoutes = [
  8. { path: '/xx', component: 'xx', ... }
  9. ]
  10. // 遍历asyncRoutes,将component替换为map[component]
  11. function mapComponent (asyncRoutes) {
  12. asyncRoutes.forEach(route => {
  13. route.component = map[route.component];
  14. if (route.children) {
  15. route.children.map(child => mapComponent(child))
  16. }
  17. })
  18. }
  19. mapComponent(asyncRoutes)

按钮权限

⻚面中按钮、链接需要更细粒度权限控制时,可封装一个指令v-permission,实现按钮级别的权限控制。

创建指令,src/directives/permission.js
image.png

该指令只能删除挂载指令的元素,对于那些额外生成的和指令无关的元素无能为力,比如:

  1. <el-tabs>
  2. <el-tab-pane label="用户管理" name="first">
  3. 用户管理</el-tab-pane>
  4. <el-tab-pane label="⻆色管理" name="third">
  5. ⻆色管理</el-tab-pane>
  6. </el-tabs>

尝试添加v-permission="..."并不能删除动态生成的内容部分

  1. <el-tab-pane label="用户管理" name="first"
  2. v-permission="['admin', 'editor']">用户管理</el-tab-pane>

此时可使用v-if来实现

  1. <template>
  2. <el-tab-pane v-if="checkPermission(['admin'])">
  3. </template>

导航菜单

导航菜单可根据前面生成的最终路由信息动态生成。

侧边栏组件,components/Sidebar/index.vue
image.png

侧边栏菜项目组件,components/Sidebar/SidebarItem.vue
默认菜单项规则:

  • 跳转链接:没有子路由或只有一个需要展示子路由

image.png

  • 其他情况:嵌套子菜单

image.png

菜单项组件,components/Sidebar/Item.vue
image.png

数据交互

常⻅需求:

  • 统一配置请求库
  • 请求拦截和响应拦截
  • 数据mock
  • 请求代理

请求封装

对axios做一次封装,统一处理配置、请求和响应拦截,utils/request.js
image.png

数据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

  1. # volumes:
  2. # /apps/easy-mock/data/db是数据库文件存放地址,根据需要修改为本地地址
  3. # - '/apps/easy-mock/data/db:/data/db'
  4. networks:
  5. - easy-mock
  6. restart: 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:

  1. - 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:

  1. - 7300:7300

volumes:

  1. # 日志地址,根据需要修改为本地地址
  2. # - '/apps/easy-mock/logs:/home/easy-mock/easy-mock/logs'

networks:

  1. - easy-mock

restart: always

networks: easy-mock:

  1. - 启动:docker-compose up
  2. <a name="w4SxH"></a>
  3. #### 使用介绍
  4. - 创建一个项目
  5. - 创建需要的接口:
  6. 登录接口`user/login`
  7. ```javascript
  8. {
  9. "code": function({ _req }) {
  10. const { username } = _req.body;
  11. if (username === "admin" || username === "jerry") {
  12. return 1
  13. } else {
  14. return 10008
  15. }
  16. },
  17. "data": function({ _req }) {
  18. const { username } = _req.body;
  19. if (username === "admin" || username === "jerry") {
  20. return username
  21. } else {
  22. return ''
  23. }
  24. }
  25. }

用户⻆色接口:user/info

  1. {
  2. code: 1,
  3. "data": function({ _req }) {
  4. return _req.headers['authorization'].split(' ')[1] === 'admin' ?
  5. ['admin'] : ['editor']
  6. }
  7. }
  • 调用:修改base_url,.env.development
    1. VUE_APP_BASE_API = 'http://localhost:7300/mock/5f6301c446875b001d8a2961'

解决跨域

如果请求的接口在另一台服务器上,开发时则需要设置代理避免跨域问题

代理配置,vue.config.js
image.png

测试

测试分类

常⻅的开发流程里,都有测试人员,他们不管内部实现机制,只看最外层的输入输出,这种我们称为 黑盒测试 。比如你写一个加法的⻚面,会设计N个用例,测试加法的正确性,这种测试我们称之为 E2E测试

还有一种测试叫做 白盒测试 ,我们针对一些内部核心实现逻辑编写测试代码,称之为 单元测试

更负责一些的我们称之为 集成测试 ,就是集合多个测试过的单元一起测试。

测试的好处

  • 提供描述组件行为的文档
  • 节省手动测试的时间
  • 减少研发新特性时产生的 bug
  • 改进设计
  • 促进重构

准备工作

在vue-cli中,预置了Mocha+Chai和Jest两套单测方案,我们的演示代码使用Jest,它们语法基本一致

新建vue项目时

选择特性Unit TestingE2E Testing
image.png

单元测试解决方案选择:Jest
image.png

在已存在项目中集成

集成Jest:vue add @vue/unit-jest
集成cypress:vue add @vue/e2e-cypress

编写单元测试

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。

新建test/unit/kaikeba.spec.js,*.spec.js是命名规范

  1. function add (num1, num2) {
  2. return num1 + num
  3. }
  4. // 测试套件 test suite
  5. describe('add方法', () => {
  6. // 测试用例 test case
  7. it('应该能正确计算加法', () => {
  8. // 断言 assert
  9. expect(add(1, 3)).toBe(4)
  10. })
  11. })

更多断言API

执行单元测试

执行:npm run test:unit
image.png

测试Vue组件

官方提供了用于单元测试的实用工具库@vue/test-utils

检查mounted之后预期结果

使用mountshallowMount挂载组件,example.spec.js

  1. import { mount } from '@vue/test-utils'
  2. it('renders props.msg when passed', () => {
  3. const msg = 'new message'
  4. // 给组件传递属性
  5. const wrapper = shallowMount(HelloWorld, {
  6. propsData: { msg }
  7. })
  8. // expect(wrapper.text()).toMatch(msg)
  9. // 查找元素
  10. const h1 = wrapper.find('h1')
  11. expect(h1.text()).toBe('new message')
  12. })

更新操作通常是异步的,dom更新结果放在await语句后面测试

  1. <p class="p1" @click="foo = 'baz'">{{foo}}</p>
  1. test('点击p之后验证更新结果 ', async () => {
  2. const wrapper = shallowMount(HelloWorld)
  3. // 模拟点击行为
  4. const p1 = wrapper.find('.p1')
  5. // 把变更状态操作放在await后面
  6. await p1.trigger('click')
  7. expect(p1.text()).toBe('baz')
  8. })

获取自定义组件

  1. <comp v-if="foo === 'baz'"></comp>
  1. components: {
  2. comp: {
  3. name: 'comp',
  4. render (h) {
  5. return h('div', 'comp')
  6. }
  7. },
  8. }
  9. const comp = wrapper.findComponent({name: 'comp'})
  10. expect(comp.exists()).toBe(true)

覆盖率

Jest自带覆盖率,很容易统计我们测试代码是否全面。

package.json里修改jest配置

  1. "jest": {
  2. "collectCoverage": true,
  3. "collectCoverageFrom": ["src/**/*.{js,vue}"],
  4. }

若采用独立配置,则修改jest.config.js:

  1. module.exports = {
  2. "collectCoverage": true,
  3. "collectCoverageFrom": ["src/**/*.{js,vue}"]
  4. }

再次执行npm run test:unit
image.png
可以看到HelloWorld.vue的覆盖率是50%

stmts是语句覆盖率(statement coverage):是不是每个语句都执行了?

Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了?

Funcs函数覆盖率(function coverage):是不是每个函数都调用了?

Lines行覆盖率(line coverage):是不是每一行都执行了?

通过分析报告可以找到没有覆盖的地方,coverage/Icov-report/index.html
image.png

我们添加一些测试代码

  1. test('验证comp组件render结果 ', () => {
  2. const h = (type, children) => ({ type, children })
  3. const vnode = HelloWorld.components.comp.render(h)
  4. console.log(vnode);
  5. expect(vnode).toEqual({ type: 'div', children: 'comp' })
  6. })

现在覆盖率是100%了,但是整个项目还很糟糕,我们还需要继续努力提高呀!

Vue组件单元测试cookbook

Vue Test Utils使用指南

E2E测试

借用浏览器的能力,站在用户测试人员的⻆度,输入框,点击按钮等,完全模拟用户,这个和具体的框架关系不大,完全模拟浏览器行为。

运行E2E测试

npm run test:e2e

修改e2e/spec/test.js

  1. // https://docs.cypress.io/api/introduction/api.html
  2. describe('端到端测试,抢测试人员的饭碗', () => {
  3. it('先访问一下', () => {
  4. cy.visit('/')
  5. // cy.contains('h1', 'Welcome to Your Vue.js App')
  6. cy.contains('span', '开课吧')
  7. })
  8. })

image.png

测试未通过,因为没有使用Kaikeba.vue,修改App.vue

  1. <div id="app">
  2. <img alt="Vue logo" src="./assets/logo.png">
  3. <!-- <HelloWorld msg="Welcome to Your Vue.js App"/> -->
  4. <Kaikeba></Kaikeba>
  5. </div>
  6. import Kaikeba from './components/Kaikeba.vue'
  7. export default {
  8. name: 'app',
  9. components: {
  10. HelloWorld, Kaikeba
  11. }
  12. }

测试通过~

测试用户点击

  1. // https://docs.cypress.io/api/introduction/api.html
  2. describe('端到端测试,抢测试人员的饭碗', () => {
  3. it('先访问一下', () => {
  4. cy.visit('/')
  5. // cy.contains('h1', 'Welcome to Your Vue.js App')
  6. cy.contains('#message', '开课吧')
  7. cy.get('button').click()
  8. cy.contains('span', '按钮点击')
  9. })
  10. })