手摸手,帶你用vue擼後臺 系列五(v4.0新版本)

花褲衩發表於2019-05-09

前言

vue-element-admin2017.04.17提交第一個 commit 以來,維護至今已經有兩年多的時間了了,釋出了四十多個版本,收穫了三萬多的 stars,遠遠的超出了自己的預期。距離上次手摸手系列教程也已經過去了很久,主要因為:作為一個個人開源專案,維持它已經很難了,所以真的沒啥時間寫詳細的教程了,光是維護專案 文件 就讓我很頭疼了。也有不少人建議我出付費教學視訊,但我個人還是更願意把這個時間投入到維護開源專案之中吧。

本篇教程主要是趁著vue-element-admin釋出了 v4.0 新版本,首先來簡單說一下4.0版本做了哪些改動和優化。後半部分則會分享一些新的思考和一些小技巧吧。之前幾篇手摸手文章都差不多兩年前的了,但隨著技術的不斷髮展迭代,很多之前的不能解決的問題也是都是有了新的解決方案的,同時也會出現一些新的問題和挑戰。

4.0 做了什麼

首先大概說一下4.0版本做了些什麼,通過 pull request 可以看出這是一次比較大的升級,有大概 170 多次的 commits,200 多個檔案的改動。其中最大的改變是接軌 vue 社群,直接通過 vue-cli來進行構建,省去了很多額外繁瑣的配置(下文會介紹),並修改了之前 mock 資料的方案,本地改用 mock-server 來解決之前mockjs帶來的各種問題。同時增加了 jest 單元測試,使用了async/await,增加了視覺化配置許可權,增加了自定義佈局等等,優化了原先addRoutes的許可權方案,支援不重新整理頁面更新路由等等功能。具體的可看 github release。接下來我們著重來分析一下這幾個功能。

vue-cli@3

本身配置方面沒有啥特別好說的,官方文件已經寫得很詳細了。這次更新基本上就是基於 webpack-chain 把之前的 webpack 配置遷移了一遍,因為vue-cli幫你做了很多預設配置,所有可以省去一些程式碼。當然這種out-of-the-box的工具利弊也很明顯,它能快速上手,大部分簡單場景無需任何額外配置基本就能用了。但對於複雜度高的或者自定義性強的專案來說,配置複雜度可能沒有減少太多。它要求你不僅要對 webpack 或者相關工程化的東西很很熟悉,你還要對vue-cli做的一些預設配置和引數也有有一定了解,時不時要去看一下原始碼它到底幹了啥,有的時候它的一些 plugin 出現了問題還不太好解決。而且說實話 webpack-chain 的書寫也是有些門檻的,大部分情況下我也很難保證自己的配置寫對的,還好官方提供了inspec功能,能讓配置簡單了不少。當你想知道自己的 vue-config.js 裡的配置到底對不對的時候,你可以在命令列裡執行vue inspect > output.js,它會將你最終生成的config展現在output.js之中,不過它預設顯示的是開發環境的配置。如果你想檢視其它環境的配置可以通過vue inspect --mode production > output.js。在寫構建配置的時候這個功能很有幫助,同時也能幫助你瞭解vue-cli在構建時到底幫你做了些什麼。

其它還有些需要注意的如:環境變數 必須以VUE_APP_開頭啊,怎麼設定polyfill啊,怎麼配置各種各樣的loader啊,就不展開了,文件或者社群都有很多文章了。具體配置可以參考 vue.config.js

這裡還有一個黑科技,看過我之前文章的小夥伴應該還有印象,我一般在開發環境是不使用路由懶載入的,因為這樣會導致熱更新速度變慢,具體的可以看之前的 文章,在vue-cli@3中可以更簡單的實現,你只要在.env.development環境變數配置檔案中設定VUE_CLI_BABEL_TRANSPILE_MODULES:true就可以了。它的實現邏輯和原理與之前還是一樣的,還是基於 plugins babel-plugin-dynamic-import-node 來實現的。之所以在vue-cli中只需要設定一個變數就可以了,是借用了vue-cli它的預設配置,它幫你程式碼都寫好了。通過閱讀 原始碼 可知,vue-cli會通過VUE_CLI_BABEL_TRANSPILE_MODULES這個環境變數來區分是否使用babel-plugin-dynamic-import-node,所以我們只要開其它就可以。雖然它的初衷是為了單元測試的,但正好滿足了我們的需求。

總的來說,vue-cli對於大部分使用者來說還是省去了一些繁瑣的配置的。如果你使用本專案的話,基本也不需要做其它過多的額外配置的。

redirect 重新整理頁面

在不重新整理頁面的情況下,更新頁面。這個 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>
// 手動重定向頁面到 '/redirect' 頁面
const { fullPath } = this.$route
this.$router.replace({
  path: '/redirect' + fullPath
})

當遇到你需要重新整理頁面的情況,你就手動重定向頁面到redirect頁面,它會將頁面重新redirect重定向回來,由於頁面的 key 發生了變化,從而間接實現了重新整理頁面元件的效果。

addRoutes && removeRoutes

看過我之前文章的人肯定知道,我目前 vue 專案的許可權控制都是通過 addRoutes來實現的。簡單說就是:使用者登入之後會返回一個許可權憑證Token,使用者在根據這個Token去問服務端詢問自己的許可權,闢如服務端返回許可權是['editor'],前端再根據這個許可權動態生成他能訪問的路由,再通過addRoutes進行動態的路由掛載。具體的程式碼可見 permission.js

但這個方案一直是有一個弊端的。那就是動態新增的路由,並不能動態的刪除。這就是導致一個問題,當使用者許可權發生變化的時候,或者說使用者登出的時候,我們只能通過重新整理頁面的方式,才能清空我們之前註冊的路由。之前老版本的 vue-element-admin就一直採用的是這種方式。雖然能用,但作為一個 spa,重新整理頁面其實是一種很糟糕的使用者體驗。但是官方也遲遲沒有出相關的 remove api,相關 issue

後來發現了一種 hack 的方法,能很好的動態清除註冊的路由。先看程式碼:

它的原理其實很簡單,所有的 vue-router 註冊的路由資訊都是存放在matcher之中的,所以當我們想清空路由的時候,我們只要新建一個空的Router例項,將它的matcher重新賦值給我們之前定義的路由就可以了。巧妙的實現了動態路由的清除。
現在我們只需要呼叫resetRouter,就能得到一個空的路有例項,之後你就可以重新addRoutes你想要的路由了。完整的程式碼例項 router.jsresetRouter

Mock 資料

如果你在實際開發中,最理想的前後端互動方式當然是後端先幫我們 mock 資料,然後前端開發。但現實很骨感,總會因為種種原因,前端需要自己來 mock 假資料。尤其是我的幾個開源專案,都是純前端專案,根本沒有後端服務。
在之前的文章中也介紹過,vue-element-adminvue-admin-template 使用的是 MockJSeasy-mock 這兩個庫。但實際用下來兩者都有一些問題。

  • MockJs

    它的原理是: 攔截了所有的請求並代理到本地,然後進行資料模擬,所以你會發現 network 中沒有發出任何的請求。但它的最大的問題是就是它的實現機制。它會重寫瀏覽器的XMLHttpRequest物件,從而才能攔截所有請求,代理到本地。大部分情況下用起來還是蠻方便的,但就因為它重寫了XMLHttpRequest物件,所以比如progress方法,或者一些底層依賴XMLHttpRequest的庫都會和它發生不相容,可以看一下我專案的 issues,就知道多少人被坑了。

    它還有一個問題:因為是它是本地模擬資料,實際上不會走任何網路請求。所以本地除錯起來很蛋疼,只能通過console.log來除錯。就拿vue-element-admin來說,想搞清楚 getInfo()介面返回了什麼資料,只能通過看原始碼或者手動 Debug 才能知道。

  • Easy-Mock

    這個專案剛出的時候用的人比較少,還真的挺好用的。天然支援跨域,還是支援MockJs的所有語法,我在之前也推薦過。但因為用的人多了,它的免費服務會經常的掛,可以說天天掛。。。但畢竟人家這是免費的服務,也不能苛求什麼,官方的建議是自己搭建服務。如果你的公司整體搭建一個這樣的 mock 服務的話也是一個不錯的選擇。但大部分人可能還是沒有這個技術條件的。

新方案

所以我一直在尋求一個更好的解決方案,我也去體驗了其它很多 mock api 服務,如 mockapiMocky 等等。總之體驗都不能滿足我的需求。

v4.0版本之後,在本地會啟動一個mock-server來模擬資料,線上環境還是繼續使用mockjs來進行模擬(因為本專案是一個純前端專案,你也可以自己搭建一個線上 server 來提供資料)。不管是本地還是線上所以的資料模擬都是基於mockjs生成的,所以只要寫一套 mock 資料,就可以在多環境中使用。

該方案的好處是,在保留 mockjs的優勢的同時,解決之前的痛點。由於我們的 mock 是完全基於webpack-dev-serve來實現的,所以在你啟動前端服務的同時,mock-server就會自動啟動,這裡還通過 chokidar 來觀察 mock 資料夾內容的變化。在發生變化時會清除之前註冊的mock-api介面,重新動態掛載新的介面,從而支援熱更新。有興趣的可以自己看一下程式碼 mock-server.js。由於是一個真正的server,所以你可以通過控制檯中的network,清楚的知道介面返回的資料結構。並且同時解決了之前mockjs會重寫 XMLHttpRequest物件,導致很多第三方庫失效的問題。

在本地開發環境中基於webpack-dev-serveafter這個middleware中介軟體,在這裡自動讀取你的 mock檔案,模擬出 REST API,它最大的好處是,完全不需要什麼額外的工作,完全基於webpack-dev-serve就能實現。如果你還是想單獨啟動一個serve也是可以的,完全可以引入一個express或者其它外掛來啟動一個 mock-serve。

我們模擬資料有了,現在要做的事情就是,將我們的介面代理到我們的 mock 服務上就好了,這裡我們使用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]: ''
        }
      }
    }

snippets 自動生成程式碼片段

平時日常工作中,做最多的就是寫業務模組和元件。當每次新開一個view或者component的時候都需要手動建立一個新.vue檔案,然後再建立<template><script><style>這些標籤,還是有些麻煩的。

所以在新版本中,基於plop,提供了幾個基礎模板,方便建立新的view或者component
執行如下命令:

npm run new

plop

如上面 gif 所示,現在只要輕鬆的點幾次回車就可以輕鬆生成我要的基礎程式碼片段。這裡只是一個 demo,你完全可以按照自己需求定製模板。老版本的vue-cli實現邏輯和它類似。

如果你覺得配置太複雜,我推薦你可以安裝如 Vue 2 Snippets VS Code外掛。 這種程式碼片段在平時工作中還是能提升不少開發效率的。

async/await or promise

本次更新中,我也將部分程式碼用了async/await的方式替代了原有的 promise方式,主要是 @/src/permission.js。有興趣的大家自己可以通過 git-history 自己對比下,可以發現程式碼閱讀性高了不少。 不過本專案中也並沒有把所有promiseasync/await替代。我來簡單說一下我的看法。

6 個 Async/Await 優於 Promise 的方面,這篇文章很多人應該都看過,裡面大部分觀點我都是同意的,大部分複雜場景下async/await的確是更優解。但相對的也不是所有的情況下都是async/await寫起來讓我更爽的。先說說我最不爽的地方是它的錯誤處理,try catch讓這個程式碼結構看起來就很奇怪(當然也有很多人很喜歡這種錯誤處理形式。社群也是相對的解決方案類似go語言的風格,比如 await-to-js

[err, res] = await to(getInfo())
if(err) //do something

這個方案是不錯,但還需要引入一個新的庫,增加了學習成本,得不償失。所以以我個人的習慣,當只有一個非同步請求,且需要做錯誤處理的情況下,更傾向於使用 promise。比如

// promise
getInfo()
  .then(res => {
    //do somethings
  })
  .catch(err => {
    //do somethings
  })

// async/await
try {
  const res = await getInfo()
  //do somethings
} catch (error) {
  //do somethings
}

在有巢狀請求的情況下,肯定是 async/await 更直觀的。

// promise
a(() => {
  b(() => {
    c()
  })
})

// async/await
await a()
await b()
await c()

當然程式碼寫的好與不好還是取決於寫程式碼的人的。比如一個常見的業務場景:有兩個併發的非同步請求,在都完成後do something。但很多人會錯誤的用序列的方式實現了。

//錯誤
await a()
await b()
//這樣變成了 a().then(() => b() )
// a 好了才會執行 b
done()

//正確
await Promise.all([a(), b()])
done()

還有一個小細節async/await打包後的程式碼>)其實會比 promise 複雜很多, 當然這個是一個忽略不計得問題。

總結:我認為它們兩個人並不是or的關係,在特定的業務場景下,選擇相對而言程式碼可讀性更好地解決方案。

以上所述純個人偏愛,並非什麼最佳實現。具體該怎麼選擇還是需要大家更具自己團隊的風格或者自己的理解來判斷。

命名規範

其實剛開始我寫 vue 檔案的時候也不注意,各種駝峰啊、大寫開頭 (PascalCase)還是橫線連線 (kebab-case)混著來,誰叫 vue 都可以,在 風格指南 中也沒有定論。不過基於本專案我還是整理了一套檔案的命名規則。

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),代表路由的資料夾也是使用同樣的規則。

例子:

  • @/src/views/svg-icons/index.vue
  • @/src/views/svg-icons/require-icons.js

使用橫線連線 (kebab-case)來命名views主要是出於以下幾個考慮。

  • 橫線連線 (kebab-case) 也是官方推薦的命名規範之一 文件
  • views下的.vue檔案代表的是一個路由,所以它需要和component進行區分(component 都是大寫開頭)
  • 頁面的url 也都是橫線連線的,比如https://www.xxx.admin/export-excel,所以路由對應的view應該要保持統一
  • 沒有大小寫敏感問題

CDN

你可以通過執行npm run preview -- --report來分析webpack打包之後的結果,觀察各個靜態資源的大小。你可以發現佔用空間最多的是第三方依賴。如vueelement-uiECharts等。

你可以使用 CDN 外鏈的方式引入第這些三方庫,這樣能大大增加構建的速度(通過 CDN 引入的資源不會經 webpack 打包)。如果你的專案沒有自己的CDN服務的話,使用一些第三方的CDN服務,如 jsdelivrunpkg 等是一個很好的選擇,它提供過了免費的資源加速,同時提供了快取優化,由於你的第三方資源是在html中通過script引入的,它的快取更新策略都是你自己手動來控制的,省去了你需要優化快取策略功夫。

很多文章說使用 CDN 引入的方式能大大減小程式碼的體積,這是不可能的。雖然打包完的 bundle小了,但那部分程式碼只是被你拆出去,用CDN的方式引入罷了。你想減小體積,最高效的方案是啟用GZIP

我個人暫時不使用CDN引入第三方依賴的原因:

暫時構建速度還沒有遇到什麼瓶頸,所有沒有必要單獨剝離部分第三方依賴。使用CDN引入的方式等於一些第三方依賴的版本你是通過package.json來控制的,一些依賴則需要手動維護,增加了一些維護成本。目前基於 webpack 的optimization.splitChunks已經做了資源的快取優化,靜態資源的快取已經做得很好了。並且目前所有的靜態資源都會上傳到自己的CDN服務,沒有必要使用第三方的CDN服務。

當然所有的優化都是需要結合自己的具體業務來調整的! 之後可能會採用這種引入方式,或者使用webpack dll的方式進行優化。如果你覺得CDN引入對於的專案有益處,你可以遵循如下方法進行修改:

使用方式

先找到 vue.config.js, 新增 externalswebpack 不打包 vueelement

externals: {
  vue: 'Vue',
  'element-ui':'ELEMENT'
}

然後配置那些第三方資源的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'
  ]
}

之後通過 html-webpack-plugin注入到 index.html之中:

config.plugin('html').tap(args => {
  args[0].cdn = cdn
  return args
})

找到 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>
<% } %>

完整的 程式碼修改

最終你可以使用 npm run preview -- --report 檢視效果 如圖:

同理,其它第三方依賴都可以使用相同的方式處理(比如vuexvue-router等)。當然你也可以選擇使用 DLLPlugin的方式來處理第三方依賴,從而來優化構建。

小技巧與建議

Watch immediate

這個已經算是一個比較常見的技巧了,這裡就簡單說一下。當 watch 一個變數的時候,初始化時並不會執行,如下面的例子,你需要在created的時候手動呼叫一次。

// bad
created() {
  this.fetchUserList();
},
watch: {
  searchText: 'fetchUserList',
}

你可以新增immediate屬性,這樣初始化的時候也會觸發,然後上面的程式碼就能簡化為:

// good
watch: {
  searchText: {
    handler: 'fetchUserList',
    immediate: true,
  }
}

ps: watch 還有一個容易被大家忽略的屬性deep。當設定為true時,它會進行深度監聽。簡而言之就是你有一個 const obj={a:1,b:2},裡面任意一個 key 的 value 發生變化的時候都會觸發watch。應用場景:比如我有一個列表,它有一堆query篩選項,這時候你就能deep watch它,只有任何一個篩序項改變的時候,就自動請求新的資料。或者你可以deep watch一個 form 表單,當任何一個欄位內容發生變化的時候,你就幫它做自動儲存等等。

Attrs 和 Listeners

這兩個屬性是 vue 2.4 版本之後提供的,它簡直是二次封裝元件或者說寫高階元件的神器。在我們平時寫業務的時候免不了需要對一些第三方元件進行二次封裝。比如我們需要基於el-select分裝一個帶有業務特性的元件,根據輸入的 name 搜尋使用者,並將一些業務邏輯分裝在其中。但el-select這個第三方元件支援幾十個配置引數,我們當然可以適當的挑選幾個引數通過 props 來傳遞,但萬一哪天別人用你的業務元件的時候覺得你的引數少了,那你只能改你封裝的元件了,亦或是哪天第三方元件加入了新引數,你該怎麼辦?

其實我們的這個元件只是基於el-select做了一些業務的封裝,比如新增了預設的placeholder,封裝了遠端 ajax 搜尋請求等等,總的來說它就是一箇中間人元件,只負責傳遞資料而已。

這時候我們就可以使用v-bind="$attrs":傳遞所有屬性、v-on="$listeners"傳遞所有方法。如下圖所示:

這樣,我們沒有在$props中宣告的方法和屬性,會通過$attrs$listeners直接傳遞下去。這兩個屬性在我們平時分裝第三方元件的時候非常有用!

.sync

這個也是 vue 2.3 之後新加的一個語法糖。這也是平時在分裝元件的時候很好用的一個語法糖,它的實現機制和v-model是一樣的。

當你有需要在子元件修改父元件值的時候這個方法很好用。
線上例子

Computed 的 get 和 set

computed 大家肯定都用過,它除了可以快取計算屬性外,它在處理傳入資料和目標資料格式不一致的時候也是很有用的。set、get 文件

上面說的可能還是是有點抽象,舉一個簡單的的例子:我們有一個 form 表單,from 裡面有一個記錄建立時間的欄位create_at。我們知道前端的時間戳都是 13 位的,但很多後端預設時間戳是 10 位的,這就很蛋疼了。前端和後端的時間戳位數不一致。最常見的做法如下:

上面的程式碼主要做的是:在拿到資料的時候將後端 10 位時間戳轉化為 13 位時間戳,之後再向服務端傳送資料的時候再轉化回 10 位時間戳傳給後端。目前這種做法當然是可行的,但之後可能不僅只有建立介面,還有更新介面的時候,你還需要在update的介面裡在做一遍同樣資料轉化的操作麼?而且這只是一個最簡單的例子,真實的 form 表單會複雜的多,需要處理的資料也更為的多。這時候程式碼就會變得很難維護。

這時候就可以使用 computed 的 set 和 get 方法了。

通過上面的程式碼可以看到,我們把需要做前後端相容的資料,放在了 computed 中,從 getDatasubmit中隔離了資料處理的部分。

當然上面說的方案還不是最好的方案,你其實應該利用之前所說的v-bind="$attrs"v-on="$listeners"對時間選擇器元件進行二次封裝。例如這樣<date-time v-model="postForm.create_at" /> 外部無需做任何資料處理,直接傳入一個 10 位的時間戳,內部進行轉化。當日期發生變化的時候,自動通過emit觸發input使v-model發生變化,把所有髒活累活都放在元件內部完成,保持外部業務程式碼的相對乾淨。具體 v-model 語法糖原理可以見官方 文件

set 和 get 處理可以做上面說的進行一些資料處理之外,你也可以把它當做一個 watch的升級版。它可以監聽資料的變化,當發生變化時,做一些額外的操作。最經典的用法就是v-model上繫結一個 vuex 值的時候,input 發生變化時,通過 commit更新存在 vuex 裡面的值。

具體的解釋你也可以見官方 文件

Object.freeze

這算是一個效能優化的小技巧吧。在我們遇到一些 big data的業務場景,它就很有用了。尤其是做管理後臺的時候,經常會有一些超大資料量的 table,或者一個含有 n 多資料的圖表,這種資料量很大的東西使用起來最明顯的感受就是卡。但其實很多時候其實這些資料其實並不需要響應式變化,這時候你就可以使用 Object.freeze 方法了,它可以凍結一個物件(注意它不併是 vue 特有的 api)。

當你把一個普通的 JavaScript 物件傳給 Vue 例項的 data 選項,Vue 將遍歷此物件所有的屬性,並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter,它們讓 Vue 能進行追蹤依賴,在屬性被訪問和修改時通知變化。
使用了 Object.freeze 之後,不僅可以減少 observer 的開銷,還能減少不少記憶體開銷。相關 issue

使用方式:this.item = Object.freeze(Object.assign({}, this.item))

這裡我提供了一個線上測速 demo,點我

通過測速可以發現正常情況下1000 x 10 rerender 都穩定在 1000ms-2000ms 之間,而開啟了Object.freeze的情況下,rerender 都穩住在 100ms-200ms 之間。有接近 10 倍的差距。所以能確定不需要變化檢測的情況下,big data 還是要優化一下的。

Functional

函式式元件 這個是文件裡就寫的內容,但在其實很少人會刻意的去使用。因為你不用它,程式碼也不會有任何問題,用了到可能會出現 bug。

我們先看一個例子:點我測試效能 肉眼可見的效能差距。當然很多人會覺得我的專案中也沒有這種變化量級,但我覺得這是一個程式設計師的自我修養問題吧。,比如能用v-show的地方就不要用v-if,善用keep-alivev-onceObject.freeze()處理 vue big data 問題等。雖然都是一些小細節,但對效能和體驗都是有不少的提升的。更多的效能優化技巧請檢視該文章 vue-9-perf-secrets

減少全域性操作

這其實並不只是針對 vue 專案的一個建議,我們平時寫程式碼的時候一定要儘量避免一些全域性的操作。如果必須要用到的時候,一定要自己檢查,會不會產生一些全域性的汙染或者副作用。

舉幾個簡單例子:

  1. 我們現在雖然用 vue 寫程式碼了,核心思想轉變為用資料驅動 view,不用像jQuery時代那樣,頻繁的操作 DOM 節點。但還是免不了有些場景還是要操作 DOM 的。我們在元件內選擇節點的時候一定要切記避免使用 document.querySelector()等一系列的全域性選擇器。你應該使用this.$el或者this.refs.xxx.$el的方式來選擇 DOM。這樣就能將你的操作侷限在當前的元件內,能避免很多問題。
  2. 我們經常會不可避免的需要註冊一些全域性性的事件,比如監聽頁面視窗的變化window.addEventListener('resize', this.__resizeHandler),但再宣告瞭之後一定要在 beforeDestroy或者destroyed生命週期登出它。window.removeEventListener('resize', this.__resizeHandler)避免造成不必要的消耗。
  3. 避免過多的全域性狀態,不是所有的狀態都需要存在 vuex 中的,應該根據業務進行合理的進行取捨。如果不可避免有很多的值需要存在 vuex 中,建議使用動態註冊的方式。相關文件。只是部分業務需要的狀態處理,建議使用 Event Bus或者使用 簡單的 store 模式
  4. css 也應該儘量避免寫太多的全域性性的樣式。除了一些全域性公用的樣式外,所以針對業務的或者元件的樣式都應該使用名稱空間的方式或者直接使用 vue-loader 提供的 scoped寫法,避免一些全域性衝突。文件

Sass 和 Js 之間變數共享

這個需求可能有些人沒有遇到過,舉個實際例子來說明一下。


如上面要實現一個動態的換膚,就需要將使用者選擇的 theme 主題色傳遞給 css。但同時初始化的時候 css 又需要將一個預設主題色傳遞給 js。所以下面我們就分兩塊來講解。

  • js 將變數傳遞給 sass
    這部分是相對簡單就可以實現的,實現方案也很多。最簡單的方法就是通過 在模板裡面寫 style 標籤來實現,就是俗話所說的內聯標籤。

    <div :style="{'background-color':color}" ></div>

    或者使用 css var(),線上 demo,還有用 less 的話modifyVars,等等方案都能實現 js 與 css 的變數傳遞。

  • sass 將變數給 js

還是那前面那個換膚來舉例子,我們頁面初始化的時候,總需要一個預設主題色吧,假設我們在 var.scss中宣告瞭一個 theme:blue,我們在 js 中該怎麼獲取這個變數呢?我們可以通過 css-modules :export來實現。更具體的解釋-How to Share Variables Between Javascript and Sass

// var.scss
$theme: blue;

:export {
  theme: $theme;
}
// test.js
import variables from '@/styles/var.scss'
console.log(variables.theme) // blue

當 js 和 css 共享一個變數的時候這個方案還是很實用的。vue-element-admin 中的側邊欄的寬度,顏色等等變數都是通過這種方案來實現共享的。

其它換膚方案可以參考 聊一聊前端換膚

自動註冊全域性元件

我的業務場景大部分是中後臺,雖然封裝和使用了很多第三方元件,但還是免不了需要自己封裝和使用很多業務元件。但每次用的時候還需要手動引入,真的是有些麻煩的。

我們其實可以基於 webpack 的require.context來實現自動載入元件並註冊的全域性的功能。相關原理在之前的文章中已經闡述過了。具體程式碼如下

我們可以建立一個GlobalComponents資料夾,將你想要註冊到全域性的元件都放在這個資料夾裡,在index.js裡面放上如上程式碼。之後只要在入口檔案main.js中引入即可。

//main.js
import './components/Table/index' // 自動註冊全域性業務元件

這樣我們可以在模板中直接使用這些全域性組建了。不需要再繁瑣的手動引入了。

<template>
  <div>
    <user-select/>
    <status-button/>
  </div>
</template>

當然你也不要為了省事,啥元件都往全域性註冊,這樣會讓你初始化頁面的時候你的初始init bundle很大。你應該就註冊那些你經常使用且體積不大的元件。那些體積大的元件,如編輯器或者圖表元件還是按需載入比較合理。而且你最好宣告這些全域性元件的時候有一個統一的命名規範比如:globel-user-select這樣的,指定一個團隊規範,不然人家看到你這個全域性元件會一臉懵逼,這個元件是哪來的。

Lint

這又是一個老生常談的問題了
vue 的一些最佳實踐什麼的話,這裡不討論了,我覺得看官方的 風格指南 差不多就夠了。比如避免避免 v-if 和 v-for 用在一起元素特性的順序這些等等規則,幾十條規則,說真的寫了這麼久 vue,我也只能記住一些常規的。什麼屬性的順序啊,不太可能記住的。這種東西還是交給程式來自動優化才是更合理的選擇。強烈推薦配置編輯器自動化處理。具體配置見 文件。同時建議結合 Git Hooks 配合在每次提交程式碼時對程式碼進行 lint 校驗,確保所有提交到遠端倉庫的程式碼都符合團隊的規範。它主要使用到的工具是huskylint-staged,詳細文件見 Git Hooks

Hook

這個是一個文件裡沒有寫的 api,但我覺得是一個很有用的 api。比如我們平時使用一些第三方元件,或者註冊一些全域性事件的時候,都需要在mounted中宣告,在destroyed中銷燬。但由於這個是寫在兩個生命週期內的,很容易忘記,而且大部分在建立階段宣告的內容都會有副作用,如果你在元件摧毀階段忘記移除的話,會造成記憶體的洩漏,而且都不太容易發現。如下程式碼:

react 在新版本中也加入了useEffect,將以前的多個 life-cycles 合併、重組,使邏輯更加清晰,這裡就不展開了。那 vue 是不是也可以這樣做?我去了看了一下官方的 vue-hooks原始碼 發現了一個新的 api:$on('hook:xxx')。有了它,我們就能將之前的程式碼用更簡單和清楚地方式實現了。

和 react 的useEffect有異曲同工之妙。

而且我們有了這個 api 之後,能幹的事情還不止這個。有時候我們會用一些第三方元件,比如我們有一個編輯器元件(載入比較慢,會有白屏),所以我們在它渲染完成之前需要給它一個佔位符,但可能這個元件並沒有暴露給我們這個介面,當然我們需要修改這個元件,在它建立的時候手動 emit 一個事件出去,然後在元件上監聽它,比如:

當然這也是可行的,但萬一還要監聽一個更新或者摧毀的生命週期呢?其實利用 hook可以很方便的實現這個效果。

當然在 vue 3.0 版本中可能會有新的寫法,就不如下面的討論: Dynamic Lifecycle Injection。有興趣的可以自行去研究,這裡就不展開了。當 3.0 正式釋出之後再來討論吧。

RoadMap

最後來說一下,之後需要做的事情吧:

  • 更好的多級頁面快取:目前頁面的快取基於keep-alive,但當三級路由巢狀的情況下,支援的並不好。之後探索一個更好的解決方案。
  • 單元測試:當專案大了之後,沒有單元測試維護起來還是有些吃力的。
    之後會慢慢補上unit-test 的測試用例。 酌情加上一些e2e-test的例子。
  • 去國際化:其實大部分人是不需要國際化的,預設情況下移除國際化。單獨開一個國際化分支(v4.1 已完成)。
  • 適配 webpack5:webpack5 還是解決了不少之前的痛點的,正式版釋出之後會進行升級。
  • vue 3.0: 等官方釋出之後會基於新版本進行重構(這個或許還有很久)
  • 適配 element-ui 3.0 之前官方發了 3.0 的打算(我也不知道會不會跳票)

總結

開源不易,且行且珍惜吧。

系列文章:

相關文章