前言vue-element-admin 從 2017.04.17提交第一個(gè) commit 以來(lái),維護(hù)至今已經(jīng)有兩年多的時(shí)間了了,發(fā)布了四十多個(gè)版本,收獲了三萬(wàn)多的 stars,遠(yuǎn)遠(yuǎn)的超出了自己的預(yù)期。距離上次手摸手系列教程也已經(jīng)過(guò)去了很久,主要因?yàn)椋鹤鳛橐粋€(gè)個(gè)人開源項(xiàng)目,維持它已經(jīng)很難了,所以真的沒(méi)啥時(shí)間寫詳細(xì)的教程了,光是維護(hù)項(xiàng)目 文檔 就讓我很頭疼了。也有不少人建議我出付費(fèi)教學(xué)視頻,但我個(gè)人還是更愿意把這個(gè)時(shí)間投入到維護(hù)開源項(xiàng)目之中吧。 本篇教程主要是趁著vue-element-admin發(fā)布了 v4.0 新版本,首先來(lái)簡(jiǎn)單說(shuō)一下4.0版本做了哪些改動(dòng)和優(yōu)化。后半部分則會(huì)分享一些新的思考和一些小技巧吧。之前幾篇手摸手文章都差不多兩年前的了,但隨著技術(shù)的不斷發(fā)展迭代,很多之前的不能解決的問(wèn)題也是都是有了新的解決方案的,同時(shí)也會(huì)出現(xiàn)一些新的問(wèn)題和挑戰(zhàn)。 4.0 做了什么首先大概說(shuō)一下4.0版本做了些什么,通過(guò) pull request 可以看出這是一次比較大的升級(jí),有大概 170 多次的 commits,200 多個(gè)文件的改動(dòng)。其中最大的改變是接軌 vue 社區(qū),直接通過(guò) vue-cli來(lái)進(jìn)行構(gòu)建,省去了很多額外繁瑣的配置(下文會(huì)介紹),并修改了之前 mock 數(shù)據(jù)的方案,本地改用 mock-server 來(lái)解決之前mockjs帶來(lái)的各種問(wèn)題。同時(shí)增加了 jest 單元測(cè)試,使用了async/await,增加了可視化配置權(quán)限,增加了自定義布局等等,優(yōu)化了原先addRoutes的權(quán)限方案,支持不刷新頁(yè)面更新路由等等功能。具體的可看 github release。接下來(lái)我們著重來(lái)分析一下這幾個(gè)功能。 vue-cli@3本身配置方面沒(méi)有啥特別好說(shuō)的,官方文檔已經(jīng)寫得很詳細(xì)了。這次更新基本上就是基于 webpack-chain 把之前的 webpack 配置遷移了一遍,因?yàn)?code>vue-cli幫你做了很多默認(rèn)配置,所有可以省去一些代碼。當(dāng)然這種out-of-the-box的工具利弊也很明顯,它能快速上手,大部分簡(jiǎn)單場(chǎng)景無(wú)需任何額外配置基本就能用了。但對(duì)于復(fù)雜度高的或者自定義性強(qiáng)的項(xiàng)目來(lái)說(shuō),配置復(fù)雜度可能沒(méi)有減少太多。它要求你不僅要對(duì) webpack 或者相關(guān)工程化的東西很很熟悉,你還要對(duì)vue-cli做的一些默認(rèn)配置和參數(shù)也有有一定了解,時(shí)不時(shí)要去看一下源碼它到底干了啥,有的時(shí)候它的一些 plugin 出現(xiàn)了問(wèn)題還不太好解決。而且說(shuō)實(shí)話 webpack-chain 的書寫也是有些門檻的,大部分情況下我也很難保證自己的配置寫對(duì)的,還好官方提供了inspec功能,能讓配置簡(jiǎn)單了不少。當(dāng)你想知道自己的 vue-config.js 里的配置到底對(duì)不對(duì)的時(shí)候,你可以在命令行里執(zhí)行vue inspect > output.js,它會(huì)將你最終生成的config展現(xiàn)在output.js之中,不過(guò)它默認(rèn)顯示的是開發(fā)環(huán)境的配置。如果你想查看其它環(huán)境的配置可以通過(guò)vue inspect --mode production > output.js。在寫構(gòu)建配置的時(shí)候這個(gè)功能很有幫助,同時(shí)也能幫助你了解vue-cli在構(gòu)建時(shí)到底幫你做了些什么。 其它還有些需要注意的如:環(huán)境變量 必須以VUE_APP_開頭啊,怎么設(shè)置polyfill啊,怎么配置各種各樣的loader啊,就不展開了,文檔或者社區(qū)都有很多文章了。具體配置可以參考 vue.config.js 這里還有一個(gè)黑科技,看過(guò)我之前文章的小伙伴應(yīng)該還有印象,我一般在開發(fā)環(huán)境是不使用路由懶加載的,因?yàn)檫@樣會(huì)導(dǎo)致熱更新速度變慢,具體的可以看之前的 文章,在vue-cli@3中可以更簡(jiǎn)單的實(shí)現(xiàn),你只要在.env.development環(huán)境變量配置文件中設(shè)置VUE_CLI_BABEL_TRANSPILE_MODULES:true就可以了。它的實(shí)現(xiàn)邏輯和原理與之前還是一樣的,還是基于 plugins babel-plugin-dynamic-import-node 來(lái)實(shí)現(xiàn)的。之所以在vue-cli中只需要設(shè)置一個(gè)變量就可以了,是借用了vue-cli它的默認(rèn)配置,它幫你代碼都寫好了。通過(guò)閱讀 源碼 可知,vue-cli會(huì)通過(guò)VUE_CLI_BABEL_TRANSPILE_MODULES這個(gè)環(huán)境變量來(lái)區(qū)分是否使用babel-plugin-dynamic-import-node,所以我們只要開其它就可以。雖然它的初衷是為了單元測(cè)試的,但正好滿足了我們的需求。 總的來(lái)說(shuō),vue-cli對(duì)于大部分用戶來(lái)說(shuō)還是省去了一些繁瑣的配置的。如果你使用本項(xiàng)目的話,基本也不需要做其它過(guò)多的額外配置的。 redirect 刷新頁(yè)面在不刷新頁(yè)面的情況下,更新頁(yè)面。這個(gè) issue 兩年前就提出來(lái)了,之前的文章里面也提供了一個(gè) 解決方案。在這里分享一下,我目前使用的新方案。 // 先注冊(cè)一個(gè)名為 `redirect` 的路由
<script>
export default {
beforeCreate() {
const { params, query } = this.$route
const { path } = params
this.$router.replace({ path: '/' + path, query })
},
render: function(h) {
return h() // avoid warning message
}
}
</script>
復(fù)制代碼
// 手動(dòng)重定向頁(yè)面到 '/redirect' 頁(yè)面
const { fullPath } = this.$route
this.$router.replace({
path: '/redirect' + fullPath
})
復(fù)制代碼
當(dāng)遇到你需要刷新頁(yè)面的情況,你就手動(dòng)重定向頁(yè)面到redirect頁(yè)面,它會(huì)將頁(yè)面重新redirect重定向回來(lái),由于頁(yè)面的 key 發(fā)生了變化,從而間接實(shí)現(xiàn)了刷新頁(yè)面組件的效果。 addRoutes && removeRoutes看過(guò)我之前文章的人肯定知道,我目前 vue 項(xiàng)目的權(quán)限控制都是通過(guò) addRoutes來(lái)實(shí)現(xiàn)的。簡(jiǎn)單說(shuō)就是:用戶登錄之后會(huì)返回一個(gè)權(quán)限憑證Token,用戶在根據(jù)這個(gè)Token去問(wèn)服務(wù)端詢問(wèn)自己的權(quán)限,辟如服務(wù)端返回權(quán)限是['editor'],前端再根據(jù)這個(gè)權(quán)限動(dòng)態(tài)生成他能訪問(wèn)的路由,再通過(guò)addRoutes進(jìn)行動(dòng)態(tài)的路由掛載。具體的代碼可見 permission.js 但這個(gè)方案一直是有一個(gè)弊端的。那就是動(dòng)態(tài)添加的路由,并不能動(dòng)態(tài)的刪除。這就是導(dǎo)致一個(gè)問(wèn)題,當(dāng)用戶權(quán)限發(fā)生變化的時(shí)候,或者說(shuō)用戶登出的時(shí)候,我們只能通過(guò)刷新頁(yè)面的方式,才能清空我們之前注冊(cè)的路由。之前老版本的 vue-element-admin就一直采用的是這種方式。雖然能用,但作為一個(gè) spa,刷新頁(yè)面其實(shí)是一種很糟糕的用戶體驗(yàn)。但是官方也遲遲沒(méi)有出相關(guān)的 remove api,相關(guān) issue 后來(lái)發(fā)現(xiàn)了一種 hack 的方法,能很好的動(dòng)態(tài)清除注冊(cè)的路由。先看代碼:  它的原理其實(shí)很簡(jiǎn)單,所有的 vue-router 注冊(cè)的路由信息都是存放在matcher之中的,所以當(dāng)我們想清空路由的時(shí)候,我們只要新建一個(gè)空的Router實(shí)例,將它的matcher重新賦值給我們之前定義的路由就可以了。巧妙的實(shí)現(xiàn)了動(dòng)態(tài)路由的清除。
現(xiàn)在我們只需要調(diào)用resetRouter,就能得到一個(gè)空的路有實(shí)例,之后你就可以重新addRoutes你想要的路由了。完整的代碼實(shí)例 router.js,resetRouter Mock 數(shù)據(jù)如果你在實(shí)際開發(fā)中,最理想的前后端交互方式當(dāng)然是后端先幫我們 mock 數(shù)據(jù),然后前端開發(fā)。但現(xiàn)實(shí)很骨感,總會(huì)因?yàn)榉N種原因,前端需要自己來(lái) mock 假數(shù)據(jù)。尤其是我的幾個(gè)開源項(xiàng)目,都是純前端項(xiàng)目,根本沒(méi)有后端服務(wù)。
在之前的文章中也介紹過(guò),vue-element-admin 和 vue-admin-template 使用的是 MockJS 和 easy-mock 這兩個(gè)庫(kù)。但實(shí)際用下來(lái)兩者都有一些問(wèn)題。 MockJs 它的原理是: 攔截了所有的請(qǐng)求并代理到本地,然后進(jìn)行數(shù)據(jù)模擬,所以你會(huì)發(fā)現(xiàn) network 中沒(méi)有發(fā)出任何的請(qǐng)求。但它的最大的問(wèn)題是就是它的實(shí)現(xiàn)機(jī)制。它會(huì)重寫瀏覽器的XMLHttpRequest對(duì)象,從而才能攔截所有請(qǐng)求,代理到本地。大部分情況下用起來(lái)還是蠻方便的,但就因?yàn)樗貙懥?code>XMLHttpRequest對(duì)象,所以比如progress方法,或者一些底層依賴XMLHttpRequest的庫(kù)都會(huì)和它發(fā)生不兼容,可以看一下我項(xiàng)目的 issues,就知道多少人被坑了。 它還有一個(gè)問(wèn)題:因?yàn)槭撬潜镜啬M數(shù)據(jù),實(shí)際上不會(huì)走任何網(wǎng)絡(luò)請(qǐng)求。所以本地調(diào)試起來(lái)很蛋疼,只能通過(guò)console.log來(lái)調(diào)試。就拿vue-element-admin來(lái)說(shuō),想搞清楚 getInfo()接口返回了什么數(shù)據(jù),只能通過(guò)看源碼或者手動(dòng) Debug 才能知道。
新方案所以我一直在尋求一個(gè)更好的解決方案,我也去體驗(yàn)了其它很多 mock api 服務(wù),如 mockapi、Mocky 等等??傊w驗(yàn)都不能滿足我的需求。 在v4.0版本之后,在本地會(huì)啟動(dòng)一個(gè)mock-server來(lái)模擬數(shù)據(jù),線上環(huán)境還是繼續(xù)使用mockjs來(lái)進(jìn)行模擬(因?yàn)楸卷?xiàng)目是一個(gè)純前端項(xiàng)目,你也可以自己搭建一個(gè)線上 server 來(lái)提供數(shù)據(jù))。不管是本地還是線上所以的數(shù)據(jù)模擬都是基于mockjs生成的,所以只要寫一套 mock 數(shù)據(jù),就可以在多環(huán)境中使用。 該方案的好處是,在保留 mockjs的優(yōu)勢(shì)的同時(shí),解決之前的痛點(diǎn)。由于我們的 mock 是完全基于webpack-dev-serve來(lái)實(shí)現(xiàn)的,所以在你啟動(dòng)前端服務(wù)的同時(shí),mock-server就會(huì)自動(dòng)啟動(dòng),這里還通過(guò) chokidar 來(lái)觀察 mock 文件夾內(nèi)容的變化。在發(fā)生變化時(shí)會(huì)清除之前注冊(cè)的mock-api接口,重新動(dòng)態(tài)掛載新的接口,從而支持熱更新。有興趣的可以自己看一下代碼 mock-server.js。由于是一個(gè)真正的server,所以你可以通過(guò)控制臺(tái)中的network,清楚的知道接口返回的數(shù)據(jù)結(jié)構(gòu)。并且同時(shí)解決了之前mockjs會(huì)重寫 XMLHttpRequest對(duì)象,導(dǎo)致很多第三方庫(kù)失效的問(wèn)題。 在本地開發(fā)環(huán)境中基于webpack-dev-serve的 after這個(gè)middleware中間件,在這里自動(dòng)讀取你的 mock文件,模擬出 REST API,它最大的好處是,完全不需要什么額外的工作,完全基于webpack-dev-serve就能實(shí)現(xiàn)。如果你還是想單獨(dú)啟動(dòng)一個(gè)serve也是可以的,完全可以引入一個(gè)express或者其它插件來(lái)啟動(dòng)一個(gè) mock-serve。 我們模擬數(shù)據(jù)有了,現(xiàn)在要做的事情就是,將我們的接口代理到我們的 mock 服務(wù)上就好了,這里我們使用webpack-dev-serve自帶的 proxy進(jìn)行接口代理。 proxy: {
// xxx-api/login => mock/login
[process.env.VUE_APP_BASE_API]: {
target: `http://localhost:${port}/mock`,
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}
}
}
復(fù)制代碼
snippets 自動(dòng)生成代碼片段平時(shí)日常工作中,做最多的就是寫業(yè)務(wù)模塊和組件。當(dāng)每次新開一個(gè)view或者component的時(shí)候都需要手動(dòng)創(chuàng)建一個(gè)新.vue文件,然后再創(chuàng)建<template>、<script>、<style>這些標(biāo)簽,還是有些麻煩的。 所以在新版本中,基于plop,提供了幾個(gè)基礎(chǔ)模板,方便創(chuàng)建新的view或者component。
執(zhí)行如下命令: npm run new
復(fù)制代碼
 如上面 gif 所示,現(xiàn)在只要輕松的點(diǎn)幾次回車就可以輕松生成我要的基礎(chǔ)代碼片段。這里只是一個(gè) demo,你完全可以按照自己需求定制模板。老版本的vue-cli實(shí)現(xiàn)邏輯和它類似。 如果你覺(jué)得配置太復(fù)雜,我推薦你可以安裝如 Vue 2 Snippets VS Code插件。 這種代碼片段在平時(shí)工作中還是能提升不少開發(fā)效率的。 async/await or promise本次更新中,我也將部分代碼用了async/await的方式替代了原有的 promise方式,主要是 @/src/permission.js。有興趣的大家自己可以通過(guò) git-history 自己對(duì)比下,可以發(fā)現(xiàn)代碼閱讀性高了不少。 不過(guò)本項(xiàng)目中也并沒(méi)有把所有promise用async/await替代。我來(lái)簡(jiǎn)單說(shuō)一下我的看法。 6 個(gè) Async/Await 優(yōu)于 Promise 的方面,這篇文章很多人應(yīng)該都看過(guò),里面大部分觀點(diǎn)我都是同意的,大部分復(fù)雜場(chǎng)景下async/await的確是更優(yōu)解。但相對(duì)的也不是所有的情況下都是async/await寫起來(lái)讓我更爽的。先說(shuō)說(shuō)我最不爽的地方是它的錯(cuò)誤處理,try catch讓這個(gè)代碼結(jié)構(gòu)看起來(lái)就很奇怪(當(dāng)然也有很多人很喜歡這種錯(cuò)誤處理形式。社區(qū)也是相對(duì)的解決方案類似go語(yǔ)言的風(fēng)格,比如 await-to-js [err, res] = await to(getInfo())
if(err) //do something
復(fù)制代碼
這個(gè)方案是不錯(cuò),但還需要引入一個(gè)新的庫(kù),增加了學(xué)習(xí)成本,得不償失。所以以我個(gè)人的習(xí)慣,當(dāng)只有一個(gè)異步請(qǐng)求,且需要做錯(cuò)誤處理的情況下,更傾向于使用 promise。比如 // promise
getInfo()
.then(res => {
//do somethings
})
.catch(err => {
//do somethings
})
// async/await
try {
const res = await getInfo()
//do somethings
} catch (error) {
//do somethings
}
復(fù)制代碼
在有嵌套請(qǐng)求的情況下,肯定是 async/await 更直觀的。 // promise
a(() => {
b(() => {
c()
})
})
// async/await
await a()
await b()
await c()
復(fù)制代碼
當(dāng)然代碼寫的好與不好還是取決于寫代碼的人的。比如一個(gè)常見的業(yè)務(wù)場(chǎng)景:有兩個(gè)并發(fā)的異步請(qǐng)求,在都完成后do something。但很多人會(huì)錯(cuò)誤的用串行的方式實(shí)現(xiàn)了。 //錯(cuò)誤
await a()
await b()
//這樣變成了 a().then(() => b() )
// a 好了才會(huì)執(zhí)行 b
done()
//正確
await Promise.all([a(), b()])
done()
復(fù)制代碼
還有一個(gè)小細(xì)節(jié)async/await打包后的代碼其實(shí)會(huì)比 promise 復(fù)雜很多, 當(dāng)然這個(gè)是一個(gè)忽略不計(jì)得問(wèn)題。 總結(jié):我認(rèn)為它們兩個(gè)人并不是or的關(guān)系,在特定的業(yè)務(wù)場(chǎng)景下,選擇相對(duì)而言代碼可讀性更好地解決方案。 以上所述純個(gè)人偏愛,并非什么最佳實(shí)現(xiàn)。具體該怎么選擇還是需要大家更具自己團(tuán)隊(duì)的風(fēng)格或者自己的理解來(lái)判斷。 命名規(guī)范其實(shí)剛開始我寫 vue 文件的時(shí)候也不注意,各種駝峰啊、大寫開頭 (PascalCase)還是橫線連接 (kebab-case)混著來(lái),誰(shuí)叫 vue 都可以,在 風(fēng)格指南 中也沒(méi)有定論。不過(guò)基于本項(xiàng)目我還是整理了一套文件的命名規(guī)則。 Component所有的Component文件都是以大寫開頭 (PascalCase),這也是官方所 推薦的。 但除了 index.vue。 例子: @/src/components/BackToTop/index.vue
@/src/components/Charts/Line.vue
@/src/views/example/components/Button.vue
JS 文件所有的.js文件都遵循橫線連接 (kebab-case)。 例子: @/src/utils/open-window.js
@/src/views/svg-icons/require-icons.js
@/src/components/MarkdownEditor/default-options.js
Views在views文件下,代表路由的.vue文件都使用橫線連接 (kebab-case),代表路由的文件夾也是使用同樣的規(guī)則。 例子: 使用橫線連接 (kebab-case)來(lái)命名views主要是出于以下幾個(gè)考慮。 橫線連接 (kebab-case) 也是官方推薦的命名規(guī)范之一 文檔 views下的.vue文件代表的是一個(gè)路由,所以它需要和component進(jìn)行區(qū)分(component 都是大寫開頭)
頁(yè)面的url 也都是橫線連接的,比如https://www.min/export-excel,所以路由對(duì)應(yīng)的view應(yīng)該要保持統(tǒng)一 沒(méi)有大小寫敏感問(wèn)題
CDN你可以通過(guò)執(zhí)行npm run preview -- --report來(lái)分析webpack打包之后的結(jié)果,觀察各個(gè)靜態(tài)資源的大小。你可以發(fā)現(xiàn)占用空間最多的是第三方依賴。如vue、element-ui、ECharts等。 你可以使用 CDN 外鏈的方式引入這些第三方庫(kù),這樣能大大增加構(gòu)建的速度(通過(guò) CDN 引入的資源不會(huì)經(jīng) webpack 打包)。如果你的項(xiàng)目沒(méi)有自己的CDN服務(wù)的話,使用一些第三方的CDN服務(wù),如 jsdelivr、unpkg 等是一個(gè)很好的選擇,它提供過(guò)了免費(fèi)的資源加速,同時(shí)提供了緩存優(yōu)化,由于你的第三方資源是在html中通過(guò)script引入的,它的緩存更新策略都是你自己手動(dòng)來(lái)控制的,省去了你需要優(yōu)化緩存策略功夫。 很多文章說(shuō)使用 CDN 引入的方式能大大減小代碼的體積,這是不可能的。雖然打包完的 bundle小了,但那部分代碼只是被你拆出去,用CDN的方式引入罷了。你想減小體積,最高效的方案是啟用GZIP。 我個(gè)人暫時(shí)不使用CDN引入第三方依賴的原因:暫時(shí)構(gòu)建速度還沒(méi)有遇到什么瓶頸,所有沒(méi)有必要單獨(dú)剝離部分第三方依賴。使用CDN引入的方式等于一些第三方依賴的版本你是通過(guò)package.json來(lái)控制的,一些依賴則需要手動(dòng)維護(hù),增加了一些維護(hù)成本。目前基于 webpack 的optimization.splitChunks已經(jīng)做了資源的緩存優(yōu)化,靜態(tài)資源的緩存已經(jīng)做得很好了。并且目前所有的靜態(tài)資源都會(huì)上傳到自己的CDN服務(wù),沒(méi)有必要使用第三方的CDN服務(wù)。 當(dāng)然所有的優(yōu)化都是需要結(jié)合自己的具體業(yè)務(wù)來(lái)調(diào)整的! 之后可能會(huì)采用這種引入方式,或者使用webpack dll的方式進(jìn)行優(yōu)化。如果你覺(jué)得CDN引入對(duì)于的項(xiàng)目有益處,你可以遵循如下方法進(jìn)行修改: 使用方式先找到 vue.config.js, 添加 externals 讓 webpack 不打包 vue 和 element externals: {
vue: 'Vue',
'element-ui':'ELEMENT'
}
復(fù)制代碼
然后配置那些第三方資源的CDN,請(qǐng)注意先后順序。 const cdn = {
css: [
// element-ui css
'https:///element-ui/lib/theme-chalk/index.css'
],
js: [
// vue must at first!
'https:///vue/dist/vue.js',
// element-ui js
'https:///element-ui/lib/index.js'
]
}
復(fù)制代碼
之后通過(guò) html-webpack-plugin注入到 index.html之中: config.plugin('html').tap(args => {
args[0].cdn = cdn
return args
})
復(fù)制代碼
找到 public/index.html。通過(guò)你配置的CND Config 依次注入 css 和 js。 <head>
<!-- 引入樣式 -->
<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%=css%>">
<% } %>
</head>
<!-- 引入JS -->
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%=js%>"></script>
<% } %>
復(fù)制代碼
完整的 代碼修改 最終你可以使用 npm run preview -- --report 查看效果 如圖:  同理,其它第三方依賴都可以使用相同的方式處理(比如vuex、vue-router等)。當(dāng)然你也可以選擇使用 DLLPlugin的方式來(lái)處理第三方依賴,從而來(lái)優(yōu)化構(gòu)建。 小技巧與建議Watch immediate這個(gè)已經(jīng)算是一個(gè)比較常見的技巧了,這里就簡(jiǎn)單說(shuō)一下。當(dāng) watch 一個(gè)變量的時(shí)候,初始化時(shí)并不會(huì)執(zhí)行,如下面的例子,你需要在created的時(shí)候手動(dòng)調(diào)用一次。 // bad
created() {
this.fetchUserList();
},
watch: {
searchText: 'fetchUserList',
}
復(fù)制代碼
你可以添加immediate屬性,這樣初始化的時(shí)候也會(huì)觸發(fā),然后上面的代碼就能簡(jiǎn)化為: // good
watch: {
searchText: {
handler: 'fetchUserList',
immediate: true,
}
}
復(fù)制代碼
ps: watch 還有一個(gè)容易被大家忽略的屬性deep。當(dāng)設(shè)置為true時(shí),它會(huì)進(jìn)行深度監(jiān)聽。簡(jiǎn)而言之就是你有一個(gè) const obj={a:1,b:2},里面任意一個(gè) key 的 value 發(fā)生變化的時(shí)候都會(huì)觸發(fā)watch。應(yīng)用場(chǎng)景:比如我有一個(gè)列表,它有一堆query篩選項(xiàng),這時(shí)候你就能deep watch它,只有任何一個(gè)篩序項(xiàng)改變的時(shí)候,就自動(dòng)請(qǐng)求新的數(shù)據(jù)。或者你可以deep watch一個(gè) form 表單,當(dāng)任何一個(gè)字段內(nèi)容發(fā)生變化的時(shí)候,你就幫它做自動(dòng)保存等等。 Attrs 和 Listeners這兩個(gè)屬性是 vue 2.4 版本之后提供的,它簡(jiǎn)直是二次封裝組件或者說(shuō)寫高階組件的神器。在我們平時(shí)寫業(yè)務(wù)的時(shí)候免不了需要對(duì)一些第三方組件進(jìn)行二次封裝。比如我們需要基于el-select分裝一個(gè)帶有業(yè)務(wù)特性的組件,根據(jù)輸入的 name 搜索用戶,并將一些業(yè)務(wù)邏輯分裝在其中。但el-select這個(gè)第三方組件支持幾十個(gè)配置參數(shù),我們當(dāng)然可以適當(dāng)?shù)奶暨x幾個(gè)參數(shù)通過(guò) props 來(lái)傳遞,但萬(wàn)一哪天別人用你的業(yè)務(wù)組件的時(shí)候覺(jué)得你的參數(shù)少了,那你只能改你封裝的組件了,亦或是哪天第三方組件加入了新參數(shù),你該怎么辦? 其實(shí)我們的這個(gè)組件只是基于el-select做了一些業(yè)務(wù)的封裝,比如添加了默認(rèn)的placeholder,封裝了遠(yuǎn)程 ajax 搜索請(qǐng)求等等,總的來(lái)說(shuō)它就是一個(gè)中間人組件,只負(fù)責(zé)傳遞數(shù)據(jù)而已。 這時(shí)候我們就可以使用v-bind="$attrs":傳遞所有屬性、v-on="$listeners"傳遞所有方法。如下圖所示:  這樣,我們沒(méi)有在$props中聲明的方法和屬性,會(huì)通過(guò)$attrs、$listeners直接傳遞下去。這兩個(gè)屬性在我們平時(shí)分裝第三方組件的時(shí)候非常有用! .sync這個(gè)也是 vue 2.3 之后新加的一個(gè)語(yǔ)法糖。這也是平時(shí)在分裝組件的時(shí)候很好用的一個(gè)語(yǔ)法糖,它的實(shí)現(xiàn)機(jī)制和v-model是一樣的。  當(dāng)你有需要在子組件修改父組件值的時(shí)候這個(gè)方法很好用。
線上例子 Computed 的 get 和 setcomputed 大家肯定都用過(guò),它除了可以緩存計(jì)算屬性外,它在處理傳入數(shù)據(jù)和目標(biāo)數(shù)據(jù)格式不一致的時(shí)候也是很有用的。set、get 文檔
上面說(shuō)的可能還是是有點(diǎn)抽象,舉一個(gè)簡(jiǎn)單的的例子:我們有一個(gè) form 表單,from 里面有一個(gè)記錄創(chuàng)建時(shí)間的字段create_at。我們知道前端的時(shí)間戳都是 13 位的,但很多后端默認(rèn)時(shí)間戳是 10 位的,這就很蛋疼了。前端和后端的時(shí)間戳位數(shù)不一致。最常見的做法如下:  上面的代碼主要做的是:在拿到數(shù)據(jù)的時(shí)候?qū)⒑蠖?10 位時(shí)間戳轉(zhuǎn)化為 13 位時(shí)間戳,之后再向服務(wù)端發(fā)送數(shù)據(jù)的時(shí)候再轉(zhuǎn)化回 10 位時(shí)間戳傳給后端。目前這種做法當(dāng)然是可行的,但之后可能不僅只有創(chuàng)建接口,還有更新接口的時(shí)候,你還需要在update的接口里在做一遍同樣數(shù)據(jù)轉(zhuǎn)化的操作么?而且這只是一個(gè)最簡(jiǎn)單的例子,真實(shí)的 form 表單會(huì)復(fù)雜的多,需要處理的數(shù)據(jù)也更為的多。這時(shí)候代碼就會(huì)變得很難維護(hù)。 這時(shí)候就可以使用 computed 的 set 和 get 方法了。  通過(guò)上面的代碼可以看到,我們把需要做前后端兼容的數(shù)據(jù),放在了 computed 中,從 getData和submit中隔離了數(shù)據(jù)處理的部分。 當(dāng)然上面說(shuō)的方案還不是最好的方案,你其實(shí)應(yīng)該利用之前所說(shuō)的v-bind="$attrs"和v-on="$listeners"對(duì)時(shí)間選擇器組件進(jìn)行二次封裝。例如這樣<date-time v-model="postForm.create_at" /> 外部無(wú)需做任何數(shù)據(jù)處理,直接傳入一個(gè) 10 位的時(shí)間戳,內(nèi)部進(jìn)行轉(zhuǎn)化。當(dāng)日期發(fā)生變化的時(shí)候,自動(dòng)通過(guò)emit觸發(fā)input使v-model發(fā)生變化,把所有臟活累活都放在組件內(nèi)部完成,保持外部業(yè)務(wù)代碼的相對(duì)干凈。具體 v-model 語(yǔ)法糖原理可以見官方 文檔。 set 和 get 處理可以做上面說(shuō)的進(jìn)行一些數(shù)據(jù)處理之外,你也可以把它當(dāng)做一個(gè) watch的升級(jí)版。它可以監(jiān)聽數(shù)據(jù)的變化,當(dāng)發(fā)生變化時(shí),做一些額外的操作。最經(jīng)典的用法就是v-model上綁定一個(gè) vuex 值的時(shí)候,input 發(fā)生變化時(shí),通過(guò) commit更新存在 vuex 里面的值。  具體的解釋你也可以見官方 文檔 Object.freeze這算是一個(gè)性能優(yōu)化的小技巧吧。在我們遇到一些 big data的業(yè)務(wù)場(chǎng)景,它就很有用了。尤其是做管理后臺(tái)的時(shí)候,經(jīng)常會(huì)有一些超大數(shù)據(jù)量的 table,或者一個(gè)含有 n 多數(shù)據(jù)的圖表,這種數(shù)據(jù)量很大的東西使用起來(lái)最明顯的感受就是卡。但其實(shí)很多時(shí)候其實(shí)這些數(shù)據(jù)其實(shí)并不需要響應(yīng)式變化,這時(shí)候你就可以使用 Object.freeze 方法了,它可以凍結(jié)一個(gè)對(duì)象(注意它不并是 vue 特有的 api)。 當(dāng)你把一個(gè)普通的 JavaScript 對(duì)象傳給 Vue 實(shí)例的 data 選項(xiàng),Vue 將遍歷此對(duì)象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter,它們讓 Vue 能進(jìn)行追蹤依賴,在屬性被訪問(wèn)和修改時(shí)通知變化。
使用了 Object.freeze 之后,不僅可以減少 observer 的開銷,還能減少不少內(nèi)存開銷。相關(guān) issue。 使用方式:this.item = Object.freeze(Object.assign({}, this.item)) 這里我提供了一個(gè)在線測(cè)速 demo,點(diǎn)我。 通過(guò)測(cè)速可以發(fā)現(xiàn)正常情況下1000 x 10 rerender 都穩(wěn)定在 1000ms-2000ms 之間,而開啟了Object.freeze的情況下,rerender 都穩(wěn)住在 100ms-200ms 之間。有接近 10 倍的差距。所以能確定不需要變化檢測(cè)的情況下,big data 還是要優(yōu)化一下的。 Functional函數(shù)式組件 這個(gè)是文檔里就寫的內(nèi)容,但在其實(shí)很少人會(huì)刻意的去使用。因?yàn)槟悴挥盟a也不會(huì)有任何問(wèn)題,用了到可能會(huì)出現(xiàn) bug。 我們先看一個(gè)例子:點(diǎn)我測(cè)試性能 肉眼可見的性能差距。當(dāng)然很多人會(huì)覺(jué)得我的項(xiàng)目中也沒(méi)有這種變化量級(jí),但我覺(jué)得這是一個(gè)程序員的自我修養(yǎng)問(wèn)題吧。,比如能用v-show的地方就不要用v-if,善用keep-alive和v-once,Object.freeze()處理 vue big data 問(wèn)題等。雖然都是一些小細(xì)節(jié),但對(duì)性能和體驗(yàn)都是有不少的提升的。更多的性能優(yōu)化技巧請(qǐng)查看該文章 vue-9-perf-secrets 減少全局操作這其實(shí)并不只是針對(duì) vue 項(xiàng)目的一個(gè)建議,我們平時(shí)寫代碼的時(shí)候一定要盡量避免一些全局的操作。如果必須要用到的時(shí)候,一定要自己檢查,會(huì)不會(huì)產(chǎn)生一些全局的污染或者副作用。 舉幾個(gè)簡(jiǎn)單例子: 我們現(xiàn)在雖然用 vue 寫代碼了,核心思想轉(zhuǎn)變?yōu)橛脭?shù)據(jù)驅(qū)動(dòng) view,不用像jQuery時(shí)代那樣,頻繁的操作 DOM 節(jié)點(diǎn)。但還是免不了有些場(chǎng)景還是要操作 DOM 的。我們?cè)诮M件內(nèi)選擇節(jié)點(diǎn)的時(shí)候一定要切記避免使用 document.querySelector()等一系列的全局選擇器。你應(yīng)該使用this.$el或者this.refs.xxx.$el的方式來(lái)選擇 DOM。這樣就能將你的操作局限在當(dāng)前的組件內(nèi),能避免很多問(wèn)題。 我們經(jīng)常會(huì)不可避免的需要注冊(cè)一些全局性的事件,比如監(jiān)聽頁(yè)面窗口的變化window.addEventListener('resize', this.__resizeHandler),但再聲明了之后一定要在 beforeDestroy或者destroyed生命周期注銷它。window.removeEventListener('resize', this.__resizeHandler)避免造成不必要的消耗。 避免過(guò)多的全局狀態(tài),不是所有的狀態(tài)都需要存在 vuex 中的,應(yīng)該根據(jù)業(yè)務(wù)進(jìn)行合理的進(jìn)行取舍。如果不可避免有很多的值需要存在 vuex 中,建議使用動(dòng)態(tài)注冊(cè)的方式。相關(guān)文檔。只是部分業(yè)務(wù)需要的狀態(tài)處理,建議使用 Event Bus或者使用 簡(jiǎn)單的 store 模式。 css 也應(yīng)該盡量避免寫太多的全局性的樣式。除了一些全局公用的樣式外,所以針對(duì)業(yè)務(wù)的或者組件的樣式都應(yīng)該使用命名空間的方式或者直接使用 vue-loader 提供的 scoped寫法,避免一些全局沖突。文檔
Sass 和 Js 之間變量共享這個(gè)需求可能有些人沒(méi)有遇到過(guò),舉個(gè)實(shí)際例子來(lái)說(shuō)明一下。 如上面要實(shí)現(xiàn)一個(gè)動(dòng)態(tài)的換膚,就需要將用戶選擇的 theme 主題色傳遞給 css。但同時(shí)初始化的時(shí)候 css 又需要將一個(gè)默認(rèn)主題色傳遞給 js。所以下面我們就分兩塊來(lái)講解。js 將變量傳遞給 sass
這部分是相對(duì)簡(jiǎn)單就可以實(shí)現(xiàn)的,實(shí)現(xiàn)方案也很多。最簡(jiǎn)單的方法就是通過(guò) 在模板里面寫 style 標(biāo)簽來(lái)實(shí)現(xiàn),就是俗話所說(shuō)的內(nèi)聯(lián)標(biāo)簽。 <div :style="{'background-color':color}" ></div>
復(fù)制代碼
或者使用 css var(),在線 demo,還有用 less 的話modifyVars,等等方案都能實(shí)現(xiàn) js 與 css 的變量傳遞。 sass 將變量給 js
還是那前面那個(gè)換膚來(lái)舉例子,我們頁(yè)面初始化的時(shí)候,總需要一個(gè)默認(rèn)主題色吧,假設(shè)我們?cè)?var.scss中聲明了一個(gè) theme:blue,我們?cè)?js 中該怎么獲取這個(gè)變量呢?我們可以通過(guò) css-modules :export來(lái)實(shí)現(xiàn)。更具體的解釋-How to Share Variables Between Javascript and Sass // var.scss
$theme: blue;
:export {
theme: $theme;
}
復(fù)制代碼
// test.js
import variables from '@/styles/var.scss'
console.log(variables.theme) // blue
復(fù)制代碼
當(dāng) js 和 css 共享一個(gè)變量的時(shí)候這個(gè)方案還是很實(shí)用的。vue-element-admin 中的側(cè)邊欄的寬度,顏色等等變量都是通過(guò)這種方案來(lái)實(shí)現(xiàn)共享的。 其它換膚方案可以參考 聊一聊前端換膚。 自動(dòng)注冊(cè)全局組件我的業(yè)務(wù)場(chǎng)景大部分是中后臺(tái),雖然封裝和使用了很多第三方組件,但還是免不了需要自己封裝和使用很多業(yè)務(wù)組件。但每次用的時(shí)候還需要手動(dòng)引入,真的是有些麻煩的。  我們其實(shí)可以基于 webpack 的require.context來(lái)實(shí)現(xiàn)自動(dòng)加載組件并注冊(cè)的全局的功能。相關(guān)原理在之前的文章中已經(jīng)闡述過(guò)了。具體代碼如下  我們可以創(chuàng)建一個(gè)GlobalComponents文件夾,將你想要注冊(cè)到全局的組件都放在這個(gè)文件夾里,在index.js里面放上如上代碼。之后只要在入口文件main.js中引入即可。 //main.js
import './components/Table/index' // 自動(dòng)注冊(cè)全局業(yè)務(wù)組件
復(fù)制代碼
這樣我們可以在模板中直接使用這些全局組建了。不需要再繁瑣的手動(dòng)引入了。 <template>
<div>
<user-select/>
<status-button/>
</div>
</template>
復(fù)制代碼
當(dāng)然你也不要為了省事,啥組件都往全局注冊(cè),這樣會(huì)讓你初始化頁(yè)面的時(shí)候你的初始init bundle很大。你應(yīng)該就注冊(cè)那些你經(jīng)常使用且體積不大的組件。那些體積大的組件,如編輯器或者圖表組件還是按需加載比較合理。而且你最好聲明這些全局組件的時(shí)候有一個(gè)統(tǒng)一的命名規(guī)范比如:globel-user-select這樣的,指定一個(gè)團(tuán)隊(duì)規(guī)范,不然人家看到你這個(gè)全局組件會(huì)一臉懵逼,這個(gè)組件是哪來(lái)的。 Lint這又是一個(gè)老生常談的問(wèn)題了
vue 的一些最佳實(shí)踐什么的話,這里不討論了,我覺(jué)得看官方的 風(fēng)格指南 差不多就夠了。比如避免避免 v-if 和 v-for 用在一起、元素特性的順序這些等等規(guī)則,幾十條規(guī)則,說(shuō)真的寫了這么久 vue,我也只能記住一些常規(guī)的。什么屬性的順序啊,不太可能記住的。這種東西還是交給程序來(lái)自動(dòng)優(yōu)化才是更合理的選擇。強(qiáng)烈推薦配置編輯器自動(dòng)化處理。具體配置見 文檔。同時(shí)建議結(jié)合 Git Hooks 配合在每次提交代碼時(shí)對(duì)代碼進(jìn)行 lint 校驗(yàn),確保所有提交到遠(yuǎn)程倉(cāng)庫(kù)的代碼都符合團(tuán)隊(duì)的規(guī)范。它主要使用到的工具是husky和lint-staged,詳細(xì)文檔見 Git Hooks Hook這個(gè)是一個(gè)文檔里沒(méi)有寫的 api,但我覺(jué)得是一個(gè)很有用的 api。比如我們平時(shí)使用一些第三方組件,或者注冊(cè)一些全局事件的時(shí)候,都需要在mounted中聲明,在destroyed中銷毀。但由于這個(gè)是寫在兩個(gè)生命周期內(nèi)的,很容易忘記,而且大部分在創(chuàng)建階段聲明的內(nèi)容都會(huì)有副作用,如果你在組件摧毀階段忘記移除的話,會(huì)造成內(nèi)存的泄漏,而且都不太容易發(fā)現(xiàn)。如下代碼:  react 在新版本中也加入了useEffect,將以前的多個(gè) life-cycles 合并、重組,使邏輯更加清晰,這里就不展開了。那 vue 是不是也可以這樣做?我去了看了一下官方的 vue-hooks的 源碼 發(fā)現(xiàn)了一個(gè)新的 api:$on('hook:xxx')。有了它,我們就能將之前的代碼用更簡(jiǎn)單和清楚地方式實(shí)現(xiàn)了。  和 react 的useEffect有異曲同工之妙。 而且我們有了這個(gè) api 之后,能干的事情還不止這個(gè)。有時(shí)候我們會(huì)用一些第三方組件,比如我們有一個(gè)編輯器組件(加載比較慢,會(huì)有白屏),所以我們?cè)谒秩就瓿芍靶枰o它一個(gè)占位符,但可能這個(gè)組件并沒(méi)有暴露給我們這個(gè)接口,當(dāng)然我們需要修改這個(gè)組件,在它創(chuàng)建的時(shí)候手動(dòng) emit 一個(gè)事件出去,然后在組件上監(jiān)聽它,比如:  當(dāng)然這也是可行的,但萬(wàn)一還要監(jiān)聽一個(gè)更新或者摧毀的生命周期呢?其實(shí)利用 hook可以很方便的實(shí)現(xiàn)這個(gè)效果。  當(dāng)然在 vue 3.0 版本中可能會(huì)有新的寫法,就不如下面的討論: Dynamic Lifecycle Injection。有興趣的可以自行去研究,這里就不展開了。當(dāng) 3.0 正式發(fā)布之后再來(lái)討論吧。 RoadMap最后來(lái)說(shuō)一下,之后需要做的事情吧: 更好的多級(jí)頁(yè)面緩存:目前頁(yè)面的緩存基于keep-alive,但當(dāng)三級(jí)路由嵌套的情況下,支持的并不好。之后探索一個(gè)更好的解決方案。 單元測(cè)試:當(dāng)項(xiàng)目大了之后,沒(méi)有單元測(cè)試維護(hù)起來(lái)還是有些吃力的。
之后會(huì)慢慢補(bǔ)上unit-test 的測(cè)試用例。 酌情加上一些e2e-test的例子。 去國(guó)際化:其實(shí)大部分人是不需要國(guó)際化的,默認(rèn)情況下移除國(guó)際化。單獨(dú)開一個(gè)國(guó)際化分支(v4.1 已完成)。 適配 webpack5:webpack5 還是解決了不少之前的痛點(diǎn)的,正式版發(fā)布之后會(huì)進(jìn)行升級(jí)。 vue 3.0: 等官方發(fā)布之后會(huì)基于新版本進(jìn)行重構(gòu)(這個(gè)或許還有很久) 適配 element-ui 3.0 之前官方發(fā)了 3.0 的打算(我也不知道會(huì)不會(huì)跳票)
總結(jié)開源不易,且行且珍惜吧。
|