100
如果說從0到1是解決溫飽的過程,那麼從1到100就是實現共同富裕的漫漫長路。
認真看過Vue.js官方文檔和《Vue.js組件開發從0到1》的同學相信已經能勝任正常的業務開發了,但就像前文中提到的“實現同樣一個小功能,可以有千萬種寫法”,作爲一個積極向上的程序猿,我們始終在思考怎樣才能寫出優雅的代碼,也就是更適合當前業務場景的代碼。本文希望和大家共同探索學習如何從1到100優雅地開發Vue.js組件。
組件的基本邏輯複用 - mixins
說到基於Vue.js的前端項目中的代碼複用,個人總結下來有三種:
複用類型 | 複用形式 | 例子 |
---|---|---|
UI複用 | 組件 | 通用對話框 |
框架無關的純js邏輯複用 | 工具類 | 日期時間轉換工具類 |
與框架相關的邏輯複用(使用了框架的相關接口) | mixin | 頁面渲染測速上報 |
UI複用我們已通過組件來實現。純js邏輯的複用我們一般會把可複用代碼抽離到一個工具類裏,通過模塊化的方式引入到各處使用。而與框架相關的邏輯代碼怎麼複用呢?例如這部分邏輯代碼作爲組件生命週期鉤子函數的回調。Vue給我們提供了一種解決方案-混合 (mixins)。
mixins在官方文檔中被定義爲“一種分發 Vue 組件中可複用功能的非常靈活的方式”,其行爲與繼承特別像。 在mixin裏定義的屬性、方法會被自動掛載到使用該mixin的組件上。與繼承不同的是,當mixin中定義的方法與組件中方法同名時,這兩個同名方法都會被保留下來,並且默認優先執行mixin中的方法,而同名的是對象時,組件的鍵值對會覆蓋mixin中對應的鍵值對。但選項合併的策略也是能自定義的,具體的例子請參考官方文檔-選項合併和官方文檔-自定義渲染合併策略,這裏將舉個頁面渲染測速上報的例子來說明mixins的用法。
在頁面(頁面也就是一個組件)加載前(beforeCreate)記錄時間點,在頁面掛載到DOM後(mounted)再記錄一個時間點,就能算出當前頁面渲染耗時。這裏可以看到,我們主要使用了Vue.js提供的生命週期鉤子beforeCreate和mounted,代碼如下:
<template>
<div>頁面測速demo</div>
</template>
<script>
export default {
startTime: 0,
beforeCreate() {
console.log('beforeCreate');
this.startTime = Date.now();
},
mounted() {
console.log('mounted');
console.log((Date.now() - this.startTime) + ' ms');
// 數據上報
}
};
</script>
頁面渲染過程(部分)如下timeline(時間線)所示:
頁面測速邏輯是js邏輯,與UI代碼複用無關,那這部分代碼複用就只能歸爲上面提及的兩種情形:
- 框架無關的純js邏輯複用
- 與框架相關的邏輯複用(使用了框架的相關接口)
在頁面測速時,我們必須依賴Vue.js提供的生命週期回調接口,所以屬於與框架相關的邏輯複用。我們最多隻能把計算邏輯和數據上報邏輯封裝成工具類,在需要測速的頁面中手動調用這些工具類完成測速需求。而mixins則能幫我們更進一步地封裝和複用代碼:
<template>
<div>頁面測速demo</div>
</template>
<script>
// 頁面測速邏輯
// PS:應該抽離到mixins文件夾裏統一管理,這裏僅爲演示方便
const renderSpeedTest = {
startTime: 0,
beforeCreate() {
console.log('beforeCreate');
this.startTime = Date.now();
},
mounted() {
console.log('mounted');
console.log((Date.now() - this.startTime) + ' ms');
}
}
// 頁面邏輯
export default {
mixins: [renderSpeedTest]
};
</script>
在需要測速的頁面邏輯代碼裏添加mixins: [renderSpeedTest]
就能複用測速邏輯,這要比手動調用工具類更加優雅(簡潔、不易出錯、後期統一維護)。
但是前面已經提到,默認的選項合併策略是先執行mixin裏的方法,再執行組件裏的同名方法。而我們的頁面測速需求是需要先執行mixin的beforeCreate方法作爲開始點記錄,後繼續組件的生命週期(beforeCreate…mounted),最後執行mixin的mounted方法計算頁面渲染時間。因此需要用到[自定義選項合併策略]來自定義同名選項的執行順序,這裏由於時間關係就不展開了。
你並不總需要組件 - 指令
“在 Vue2.0 中,代碼複用和抽象的主要形式是組件。然而,有的情況下,你仍然需要對普通 DOM 元素進行底層操作,這時候就會用到自定義指令。” ——Vue官方文檔 - 自定義指令
理解一樣東西有兩種方法:
- 作者的設計初衷是什麼?爲了解決什麼問題? —— 瞭解背景
- 如何使用? —— 閱讀文檔,學習API,寫demo
個人覺得第一種方法將理解得更加深刻。歸納文檔的話,指令的設計是爲了覆蓋DOM元素操作邏輯複用的情況。理解了設計初衷後,我們快速閱覽Vue指令提供的鉤子函數- bind、inserted、update、componentUpdated、unbind,就相當於學會了加減乘除,至於用加減乘除來做什麼,更多的是看業務場景,看你對指令本身的理解。下面舉個性能優化的例子來一起學習指令的用法。
性能優化場景:假設一個長頁面的頂部有一個旋轉的物體,在滾到頁面底部時,雖然旋轉物體不可見,但任在旋轉,這是一個不必要的消耗。但類似的動畫物體越來越多,滾動及後面的操作也會越來越卡頓。
性能優化方法:但旋轉物體離開視圖時,暫停旋轉。重新進入視圖前,恢復旋轉。
藉助指令,我們怎樣優雅地解決這個問題:
<template>
<div>
<div>指令demo</div>
<div class="rotate-obj" v-scrollInOut="startOrStop">旋轉物體</div>
<ul>
<li v-for="i in testData">{{ i }}</li>
</ul>
</div>
</template>
<style scoped>
.rotate-obj { width: 100px; height: 100px; background: green; }
li { height: 100px; }
</style>
<script>
import Vue from 'vue';
// 滾動檢測邏輯
// PS:應該抽離到directives文件夾裏統一管理,這裏僅爲演示方便
let io = null;
const scrollInOut = {
// 指令綁定
bind: function(el, binding) {
console.log('bind');
io = new IntersectionObserver(
entries => {
const entry = entries[0];
// 獲取綁定的值
const callback = typeof binding.value === 'function' ? binding.value : null;
if (entry.intersectionRatio === 0 ) {
callback && callback('out of viewport');
} else {
callback && callback('in viewport');
}
}
);
io.observe(el);
},
// 指令解除綁定
unbind: function(el) {
io.unobserve(el);
io = null;
}
}
// 頁面邏輯
export default {
data() {
return {
testData: [1,2,3,4,5,6,7,8,9];
}
},
methods: {
startOrStop(inOrOut) {
console.log('startOrStop: ', inOrOut);
}
},
directives: {
scrollInOut
}
};
</script>
上面使用了瀏覽器一個新的接口IntersectionObserver API來監聽具體元素是否在視窗內,可用於做圖片懶加載等資源加載優化,具體內容請參考IntersectionObserver API 使用教程 - 阮一峯,簡單易懂。總的來說,我們設計了一個指令,綁定該指令的DOM元素將擁有一個監聽自身是否在視窗內的回調方法。類似地,我們可以用指令來封裝一系列事件的綁定和組合,例如實現長按事件,滑動事件等。
結合作者設計的初衷、Vue指令提供的鉤子函數以及上面的例子,相信大家在考慮怎麼複用代碼時能想起有指令這一種選擇。
組件庫的技術架構
學習了組件、mixins和指令,大家幾乎能針對每一種場景寫出合適的、優雅的代碼了。隨着項目的發展,組件的逐步積累,天然會按照項目交互和視覺規範形成一套組件,這一套組件的沉澱,將是後續啓動新項目的基石,怎麼將已有項目的組件用到新項目或其他已有項目中,將是我們要討論的問題。
複製粘貼是一個有效的方法,但顯然算不上優雅。將組件以npm包的規範整理成一個組件庫,同時利用npm包的更新機制解決組件更新同步的問題,是業界的常規做法,這裏以餓了麼Vue組件庫 - mint-ui爲例,和大家一起學習怎麼去搭建一個組件庫?
我們先來看看Mint UI官網裏的介紹:
從使用方式來看,是標準的npm模塊引入,再通過Vue插件規範來進行全局註冊或者按需引入部分組件。從特性來看,上面列舉的豐富齊全、關注移動端性能、複用代碼減小體積都是具體實現的事情,和架構無關。因此我們的重點在於:
- 代碼怎麼組織?
- 如何實現全部加載和按需加載?
縱使設計得再高深,答案也在代碼裏。
通過簡單觀察代碼庫裏的代碼文件結構以及閱讀組件庫入口文件index.js,我已找到答案。mint-ui將組件統一放在packages文件夾裏,在index.js文件中全部引入,再對外export所有組件的引入,以及最重要的install方法。瞭解Vue插件的同學都知道,install是Vue插件的入口函數,在調用Vue.use(MyPlugin)
來註冊插件時,實際上會調用MyPlugin.install(Vue)
,所以入口文件index.js在入口函數install裏面利用Vue.component
來全局註冊組件或者利用Vue.use
來引入組件庫裏的插件。抽象來說,全部加載組件庫這種做法,是通過Vue插件規範來實現的。
那按需加載是怎麼實現的呢?下面介紹一個babel插件babel-plugin-component,babel主要用來將我們編寫的es6、es7代碼編譯成瀏覽器兼容的es5代碼,而babel-plugin-component則在編譯過程中又做了一些小事:
// 編譯前
import { Button } from 'components'
// 編譯後
var button = require('components/lib/button')
require('components/lib/button/style.css')
至於這個css文件時怎麼編譯來的,通過閱讀代碼可知,.vue文件裏採用css模塊化解決方案——postcss來組織樣式,可編譯成單個css文件。
這麼一說大家應該都明白了,babel-plugin-component具體配置請參考文檔。mint-ui按照Vue插件的規範來組織代碼,然後通過babel-plugin-component來實現按需加載。
而單個Vue組件怎麼實現,在《Vue.js組件開發從0到1》中已詳細介紹過。這裏mint-ui將每一個組件都作爲npm包,通過package.json文件指定入口文件爲index.js,再在index.js裏引入並對外暴露單vue文件(如果需要對插件進行邏輯操作,可以在這進行封裝,前文也已經介紹過),如下:
export { default } from './src/button.vue';
看得比較淺顯,簡單回顧一下前面兩個問題:
- 代碼怎麼組織?
- index裏面引用組件,註冊組件
- 單個組件採取.Vue文件 + js封裝的模式
- css組織採用PostCSS
- 如何實現全部加載和按需加載?
- 作爲一個Vue插件來使用,實現全部加載
- 利用babel-plugin-component來打包和引用,實現按需加載。
最後和大家分享一下之前閱讀index.js的一個小困惑:
// auto install
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
這裏的自動安裝是爲了使得通過<script>
直接引入mint-ui.js時能作爲插件自動安裝,之前一直想不明白,是侷限於自己僅關注通過npm包這種使用場景,覺得這是一段冗餘代碼。
別鑽牛角尖,學會多角度全面看待問題和自己遇到的困惑。
高階組件 - 內置的transition組件分析 - 敬請期待
簡單說一下:提供了一個render函數,使用底層方法構造任意想要的組件,返回。