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