09項(xiàng)目最佳實(shí)踐
資源:
項(xiàng)目配置策略
基礎(chǔ)配置:指定應(yīng)用上下文锭魔、端口號(hào)棒仍,vue.config.js
const port = 8080
module.exports = {
publicPath: '/best-practice', // 部署應(yīng)用包時(shí)的基本 URL
devServer: {
port,
}
}
配置 Webpack:configureWebpack
范例:設(shè)置一個(gè)組件存放路徑的別名,vue.config.js
const path = require('path')
module.exports = {
configureWebpack: {
resolve: {
alias: {
comps: path.join(__dirname, 'src/components')
}
}
}
}
范例:設(shè)置一個(gè) Webpack 配置項(xiàng)用于頁面 title,vue.config.js
modules.exports = {
configureWebpack: {
name:'林慕-Vue項(xiàng)目實(shí)戰(zhàn)'
}
}
在宿主頁面使用 lodash 插值語法使用它,./public/index.html
<title><%= webpackConfig.name %></title>
webpack-merge 合并出最終選項(xiàng)
范例:基于環(huán)境有條件地配置,vue.config.js
configureWebpack: config => {
config.resolve.alias.comps = path.join(__dirname, 'src/components')
if (process.env.NODE_ENV === 'development') {
config.name = '林慕-Vue項(xiàng)目實(shí)踐'
}else {
config.name = 'Vue Best Practice'
}
}
結(jié)合上面的例子可以看到径荔,Webpack 有兩種常見的配置方式,第一種是結(jié)合某些特性直接修改 configureWebpack脆霎,第二種是傳遞一個(gè)函數(shù)給 configureWebpack总处,返回一個(gè)用于合并的配置對(duì)象。
配置 Webpack:chainWebpack 稱為鏈?zhǔn)讲僮骶χ耄梢愿?xì)粒度控制 Webpack 內(nèi)部配置鹦马。
范例:svg icon 引入
下載圖標(biāo),存入 src/icons/svg 中
安裝依賴:svg-sprite-loader
npm i svg-sprite-loader -D
- 修改規(guī)則和新增規(guī)則忆肾,vue.config.js
// resolve 定義一個(gè)絕對(duì)路徑獲取函數(shù)
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
// 鏈?zhǔn)脚渲?chainWebpack(config) {
// 配置 svg 規(guī)則排除 icons 目錄中 svg 文件處理
// 目標(biāo)給 svg 規(guī)則增加一個(gè)排除選項(xiàng) exclude:['path/to/icon']
config.module.rule('svg')
.exclude.add(resolve('src/icons'))
// 新增 icons 規(guī)則荸频,設(shè)置 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]'})
}
- 使用圖標(biāo),App.vue
<template>
<svg>
<use xlink:href='#icon-wx' />
</svg>
</template>
<script>
import '@/icons/svg/wx.svg'
</script>
- 自動(dòng)導(dǎo)入
- 創(chuàng)建 icons/index.js
自動(dòng)化加載 svg 目錄下的所有 svg 文件客冈,使用 Webpack 提供require.context() 指定 svg 為固定上下文疾渣。
import Vue from ’vue'
import SvgIcon from '@/components/SvgIcon.vue'
const req = require.context('./svg', false, /\.svg$/)
// keys返回上下文中所有文件名
req.keys().map(req)
// 注冊(cè) svg-icon 組件
Vue.component('svg-icon', SvgIcon)
- 創(chuàng)建 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>
使用svg
<svg-icon icon-class="wx"></svg-icon>
環(huán)境變量和模式
如果想給多種環(huán)境做不同配置吧趣,可以利用 Vue-cli 提供的模式仁热。默認(rèn)有 development、production渠缕、test 三種模式鸽素,對(duì)應(yīng)的,它們的配置文件形式是 .env.development亦鳞。
范例:定義一個(gè)開發(fā)時(shí)可用的配置項(xiàng)馍忽,創(chuàng)建 .env.dev
# 只能用于服務(wù)器
foo=bar
# 可用于客戶端
VUE_APP_DONG=dong
注:如果需要在客戶端加多個(gè)變量棒坏,需要以 VUE_APP_xxx 這種形式,VUE_APP 為前綴遭笋。
修改 mode 選項(xiàng)覆蓋模式名稱坝冕,package.json
"serve": "vue-cli-service serve --mode dev"
權(quán)限控制
路由分為兩種:constantRoutes 和 asyncRoutes,前者是默認(rèn)路由可直接訪問坐梯,后者中定義的路由需要先登錄徽诲,獲取角色并過濾后動(dòng)態(tài)加入到 Router 中。
路由定義吵血,router/index.js
創(chuàng)建用戶登錄頁面,views/Login.vue
路由守衛(wèi):創(chuàng)建 ./src/permission.js偷溺,并在 main.js 引入
用戶登錄狀態(tài)維護(hù)
維護(hù)用戶登錄狀態(tài):路由守衛(wèi) => 用戶登錄 => 獲取 token 并緩存
路由守衛(wèi):src/permission.js
請(qǐng)求登錄:components/Login.vue
user 模塊:維護(hù)用戶數(shù)據(jù)蹋辅、處理用戶登錄等,store/modules/user.js
測(cè)試
用戶角色獲取和權(quán)限路由過濾
登錄成功后挫掏,請(qǐng)求用戶信息獲取用戶角色信息侦另,然后根據(jù)角色過濾 asyncRoutes,并將結(jié)果動(dòng)態(tài)添加至 router
維護(hù)路由信息尉共,實(shí)現(xiàn)動(dòng)態(tài)路由生成邏輯褒傅,store/modules/permission.js
獲取用戶角色,判斷用戶是否擁有訪問權(quán)限袄友,permission.js
// 引入store
import store from './store'
router.beforeEach(async (to, from, next) => {
//...
if (hasToken) {
if (to.path === '/login') { }
else {
// 若用戶角色已附加則說明權(quán)限以判定殿托,動(dòng)態(tài)路由已添加
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
// 說明用戶以獲取過角色信息,放行
next()
} else {
try {
// 先請(qǐng)求獲取用戶信息
const { roles } = await store.dispatch('user/getInfo')
// 根據(jù)當(dāng)前用戶角色過濾出可訪問路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 添加至路由器
router.addRoutes(accessRoutes)
// 繼續(xù)路由切換剧蚣,確保addRoutes完成
next({ ...to, replace: true })
} catch (error) {
// 出錯(cuò)需重置令牌并重新登錄(令牌過期支竹、網(wǎng)絡(luò)錯(cuò)誤等原因)
await store.dispatch('user/resetToken')
next(`/login?redirect=${to.path}`)
alert(error || '未知錯(cuò)誤')
}
}
}
} else {
// 未登錄...
}
})
異步獲取路由表
可以當(dāng)用戶登錄后向后端請(qǐng)求可訪問的路由表,從而動(dòng)態(tài)生成可訪問頁面鸠按,操作和原來是相同的礼搁,這里多了一步將后端返回路由表中組件名稱和本地的組件映射步驟:
// 前端組件名和組件映射表
const map = {
// xx:require('@/views/xx.vue').default // 同步的方式
xx: () => import('@/views/xx.vue') // 異步的方式
}
// 服務(wù)端返回的asyncRoutes
const 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)
按鈕權(quán)限
頁面中某些按鈕、鏈接有時(shí)候需要更細(xì)粒度權(quán)限控制目尖,這時(shí)候可以封裝一個(gè)指令 v-permission,放在需要控制的按鈕上瑟曲,從而實(shí)現(xiàn)按鈕級(jí)別權(quán)限控制。
創(chuàng)建指令莹捡,src/directives/permission.js
測(cè)試,About.vue
該指令只能管控掛載指令的元素扣甲,對(duì)于那些額外生成的和指令無關(guān)的元素?zé)o能為力齿椅,比如:
<el-tabs>
<el-tab-pane label="?戶管理" name="first" v-permission="['admin',
'editor']">
?戶管理</el-tab-pane>
<el-tab-pane label="配置管理" name="second" v-permission="['admin',
'editor']">
配置管理</el-tab-pane>
<el-tab-pane label="角色管理" name="third" v-permission="['admin']">
??管理</el-tab-pane>
<el-tab-pane label="定時(shí)任務(wù)補(bǔ)償" name="fourth" v-permission="['admin',
'editor']">
定時(shí)任務(wù)補(bǔ)償</el-tab-pane>
</el-tabs>
此時(shí)只能使用 v-if 來實(shí)現(xiàn)
<template>
<el-tab-pane v-if="checkPermission(['admin'])">
</template>
<script>
export default {
methods:{
checkPermission(permissionRoles){
return roles.some(role =>{
return permissionRoles.includes(role)
})
}
}
}
</script>
自定義指令參考
自動(dòng)生成導(dǎo)航菜單
導(dǎo)航菜單是根據(jù)路由信息并結(jié)合權(quán)限判斷而動(dòng)態(tài)生成的启泣,它需要對(duì)應(yīng)路由的多級(jí)嵌套涣脚,所以要用到遞歸組件。
創(chuàng)建側(cè)邊欄組件寥茫,components/Sidebar/index.vue
創(chuàng)建側(cè)邊欄項(xiàng)目組件,layout/components/Sidebar/SidebarItem.vue
創(chuàng)建側(cè)邊欄菜單項(xiàng)組件芭梯,layout/components/Sidebar/Item.vue
數(shù)據(jù)交互
數(shù)據(jù)交互流程:
API 服務(wù) => axios 請(qǐng)求 => 本地 mock/線上 mock/服務(wù)器 api
封裝 request
對(duì) axios 做一次封裝弄喘,統(tǒng)一處理配置、請(qǐng)求和響應(yīng)攔截累奈。
安裝 axios:
npm i axios -S
創(chuàng)建 @/utils/request.js
設(shè)置 VUE_APP_BASE_API 環(huán)境變量急但,創(chuàng)建 .env.development 文件
編寫服務(wù)接口,創(chuàng)建 @/api/user.js
數(shù)據(jù) mock
數(shù)據(jù)模擬兩種常見方式戒努,本地 mock 和線上 easy-mock突委。
本地 mock:利用 webpack-dev-server 提供的 before 鉤子可以訪問 express 實(shí)例,從而定義接口缘缚。
修改 vue.config.js敌蚜,給 devServer 添加相關(guān)代碼
調(diào)用接口,@/store/modules/user.js
線上 esay-mock
諸如 easy-mock 這類線上 mock 工具優(yōu)點(diǎn)是使用簡(jiǎn)單齐媒,mock 工具庫也比較強(qiáng)大纷跛,還能根據(jù) swagger 規(guī)范生成接口喻括。
使用步驟:
- 登錄 easy-mock
若遠(yuǎn)程不可用,可以搭建本地 easy-mock 服務(wù)(nvm + node + redis + mongodb)
先安裝 node 8.x贫奠、redis 和 mongodb
啟動(dòng)命令:
切 node v8: nvm list , nvm use 8.16.0
起 redis: redis-server
起 mongodb: mongod
起 easy-mock 項(xiàng)目: npm run dev
創(chuàng)建一個(gè)項(xiàng)目
創(chuàng)建需要的接口
// user/login
{
"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']
}
}
- 調(diào)用:修改 base_url脖律,.env.development
VUE_APP_BASE_API = 'http://localhost:7300/mock/5e9032aab92b8c71eb235ad5'
解決跨域
如果請(qǐng)求的接口在另一臺(tái)服務(wù)器上腕侄,開發(fā)時(shí)則需要設(shè)置代理避免跨域問題冕杠。
添加代理配置,vue.config.js
創(chuàng)建一個(gè)獨(dú)立接口服務(wù)器柒桑,~/server/index.js
項(xiàng)目測(cè)試
測(cè)試分類
常見的開發(fā)流程里噪舀,都有測(cè)試人員飘诗,他們不管內(nèi)部實(shí)現(xiàn)機(jī)制昆稿,只看最外層的輸入輸出,這種我們成為黑盒測(cè)試净响。比如你寫一個(gè)加法的頁面喳瓣,會(huì)設(shè)計(jì) N 個(gè)用例,測(cè)試加法的正確性配乓,這種測(cè)試我們稱之為 E2E測(cè)試惠毁。
還有一種測(cè)試叫做白盒測(cè)試鞠绰,我們針對(duì)一些內(nèi)部核心實(shí)現(xiàn)邏輯編寫測(cè)試代碼,稱之為單元測(cè)試屿笼。
更負(fù)責(zé)一些的我們稱之為集成測(cè)試,就是集合多個(gè)測(cè)試過的單元一起測(cè)試志电。
組件的單元測(cè)試有很多好處:
提供描述組件行為的文檔
節(jié)省手動(dòng)測(cè)試的時(shí)間
減少研發(fā)新特性時(shí)產(chǎn)生的 bug
改進(jìn)設(shè)計(jì)
促進(jìn)重構(gòu)
準(zhǔn)備工作
在 vue-cli 中蛔趴,預(yù)置了 Mocha + Chai 和 Jest 兩套單測(cè)方案孝情,我們的演示代碼使用 Jest,他們語法基本一致魁亦。
新建 Vue 項(xiàng)目時(shí)
- 選擇特性 Unit Testing 和 E2E Testing
- 單元測(cè)試解決方案選擇:Jest
- 端到端測(cè)試解決方案選擇:Cypress
在已存在項(xiàng)目中集成
集成 Jest:
vue add @vue/unit-jest
集成 cypress:
vue add @vue/e2e-cypress
編寫單元測(cè)試
單元測(cè)試:是指對(duì)軟件中的最小測(cè)試單元進(jìn)行檢查和驗(yàn)證洁奈。
- 新建 test/unit/test.spec.js利术,*.spec.js 是命名規(guī)范
function add (num1, num2) {
return num1 + num2
}
// 測(cè)試套件 test suite
describe('test', () => {
// 測(cè)試用例 test case
it('測(cè)試add函數(shù)', () => {
// 斷言 assert
expect(add(1, 3)).toBe(3)
expect(add(1, 3)).toBe(4)
expect(add(-2, 3)).toBe(1)
})
})
執(zhí)行單元測(cè)試
- 執(zhí)行:
npm run test:unit
斷言 API 簡(jiǎn)介
describe:定義一個(gè)測(cè)試套件
it:定義一個(gè)測(cè)試用例
expect:斷言的判斷條件
剛剛僅展示了 toBe低矮,更多斷言 API
測(cè)試 Vue 組件
Vue 官方提供了用于單元測(cè)試的實(shí)用工具庫 @vue/test-utils
創(chuàng)建一個(gè) Vue 組件 components/Kaikeba.vue
測(cè)試該組件军掂,test/unit/kaikeba.spec.js
import Kaikeba from '@/components/Kaikeba.vue'
describe('Kaikeba.vue', () => {
// 檢查組件選項(xiàng)
it('要求設(shè)置created?命周期', () => {
expect(typeof Kaikeba.created).toBe('function')
})
it('message初始值是vue-test', () => {
// 檢查data函數(shù)存在性
expect(typeof Kaikeba.data).toBe('function')
// 檢查data返回的默認(rèn)值
const defaultData = Kaikeba.data()
expect(defaultData.message).toBe('vue-test')
})
})
檢查 mounted 之后預(yù)期結(jié)果
使用 @vue/test-utils 掛載組件
import { mount } from '@vue/test-utils'
it("mount之后測(cè)data是開課吧", () => {
const wrapper = mount(Kaikeba);
expect(wrapper.vm.message).toBe("開課吧");
});
it("按鈕點(diǎn)擊后", () => {
const wrapper = mount(KaikebaComp);
wrapper.find("button").trigger("click");
// 測(cè)試數(shù)據(jù)變化
expect(wrapper.vm.message).toBe("按鈕點(diǎn)擊");
// 測(cè)試html渲染結(jié)果
expect(wrapper.find("span").html()).toBe("<span>按鈕點(diǎn)擊</span>");
// 等效的?式
expect(wrapper.find("span").text()).toBe("按鈕點(diǎn)擊");
});
測(cè)試覆蓋率
Jest 自帶覆蓋率蝗锥,很容易統(tǒng)計(jì)我們測(cè)試代碼是否全面。如果用的 mocha税课,需要使用 istanbul 來統(tǒng)計(jì)覆蓋率痊剖。
- package.json 里修改 jest 配置
"jest": {
"collectCoverage": true,
"collectCoverageFrom": ["src/**/*.{js,vue}"],
}
若采用獨(dú)立配置陆馁,則修改 jest.config.js:
module.exports = {
"collectCoverage": true,
"collectCoverageFrom": ["src/**/*.{js,vue}"]
}
- 在此執(zhí)行 npm run test:unit
%stmts 是語句覆蓋率(statement coverage):是不是每個(gè)語句都執(zhí)行了?
%Branch 分支覆蓋率(branch coverage):是不是每個(gè) if 代碼塊都執(zhí)行了击狮?
%Funcs 函數(shù)覆蓋率(function coverage):是不是每個(gè)函數(shù)都調(diào)用了?
%Lines 行覆蓋率(line coverage):是不是每一行都執(zhí)行了寸莫?
可以看到我們 kaikeba.vue 的覆蓋率是100%档冬,我們修改?下代碼:
<template>
<div>
<span>{{ message }}</span>
<button @click="changeMsg">點(diǎn)擊</button>
</div>
</template>
<script>
export default {
data () {
return {
message: "vue-text",
count: 0
};
},
created () {
this.message = "開課吧";
},
methods: {
changeMsg () {
if (this.count > 1) {
this.message = "count?于1";
} else {
this.message = "按鈕點(diǎn)擊";
}
},
changeCount () {
this.count += 1;
}
}
};
</script>
現(xiàn)在的代碼酷誓,依然是測(cè)試沒有報(bào)錯(cuò)盐数,但是覆蓋率只有66%了,而且沒有覆蓋的代碼行數(shù)玫氢,都標(biāo)記了出來漾峡,繼續(xù)努力加測(cè)試吧。
Vue 組件單元測(cè)試 cookbook:https://cn.vuejs.org/v2/cookbook/unit-testing-vue-components.html
Vue Test Utils 使用指南:https://vue-test-utils.vuejs.org/zh/
E2E 測(cè)試
借用瀏覽器的能力,站在用戶測(cè)試人員的角度牺陶,輸入框辣之,點(diǎn)擊按鈕等,完全模擬用戶狮鸭,這個(gè)和具體的框架關(guān)系不大多搀,完全模擬瀏覽器的行為。
運(yùn)行 E2E 測(cè)試
npm run test:e2e
修改 e2e/spec/test.js
// https://docs.cypress.io/api/introduction/api.html
describe('端到端測(cè)試惯退,搶測(cè)試人員的飯碗', () => {
it('先訪問?下', () => {
cy.visit('/')
// cy.contains('h1', 'Welcome to Your Vue.js App')
cy.contains('span', '開課吧')
})
})
測(cè)試未通過催跪,因?yàn)闆]有使用 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
}
}
測(cè)試通過~
測(cè)試用戶點(diǎn)擊
// https://docs.cypress.io/api/introduction/api.html
describe('端到端測(cè)試荣倾,搶測(cè)試?員的飯碗', () => {
it('先訪問?下', () => {
cy.visit('/')
// cy.contains('h1', 'Welcome to Your Vue.js App')
cy.contains('#message', '開課吧')
cy.get('button').click()
cy.contains('span', '按鈕點(diǎn)擊')
})
})
總結(jié)
這篇文章還是比較淺顯易懂的舌仍,像項(xiàng)目配置策略者娱,權(quán)限控制和自動(dòng)生成導(dǎo)航菜單這三個(gè)還是需要了解學(xué)習(xí)的,至于單元測(cè)試的話推姻,就見仁見智了框沟,如果你項(xiàng)目很緊急的話忍燥,單元測(cè)試和業(yè)務(wù)開發(fā)并行幾乎是不可能的。但是厂捞,如果你的項(xiàng)目對(duì)于標(biāo)準(zhǔn)化作業(yè)這一塊很看中的話队丝,單元測(cè)試還是很有必要的。