Vue.js組件開發從1到100

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代碼複用無關,那這部分代碼複用就只能歸爲上面提及的兩種情形:

  1. 框架無關的純js邏輯複用
  2. 與框架相關的邏輯複用(使用了框架的相關接口)

在頁面測速時,我們必須依賴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官方文檔 - 自定義指令

理解一樣東西有兩種方法:

  1. 作者的設計初衷是什麼?爲了解決什麼問題? —— 瞭解背景
  2. 如何使用? —— 閱讀文檔,學習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官網裏的介紹:

mint-ui使用介紹

mint-ui特性介紹

從使用方式來看,是標準的npm模塊引入,再通過Vue插件規範來進行全局註冊或者按需引入部分組件。從特性來看,上面列舉的豐富齊全、關注移動端性能、複用代碼減小體積都是具體實現的事情,和架構無關。因此我們的重點在於:

  1. 代碼怎麼組織?
  2. 如何實現全部加載和按需加載?

縱使設計得再高深,答案也在代碼裏。

通過簡單觀察代碼庫裏的代碼文件結構以及閱讀組件庫入口文件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';

看得比較淺顯,簡單回顧一下前面兩個問題:

  1. 代碼怎麼組織?
    1. index裏面引用組件,註冊組件
    2. 單個組件採取.Vue文件 + js封裝的模式
    3. css組織採用PostCSS
  2. 如何實現全部加載和按需加載?
    1. 作爲一個Vue插件來使用,實現全部加載
    2. 利用babel-plugin-component來打包和引用,實現按需加載。

最後和大家分享一下之前閱讀index.js的一個小困惑:

// auto install
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

這裏的自動安裝是爲了使得通過<script>直接引入mint-ui.js時能作爲插件自動安裝,之前一直想不明白,是侷限於自己僅關注通過npm包這種使用場景,覺得這是一段冗餘代碼。

別鑽牛角尖,學會多角度全面看待問題和自己遇到的困惑。

高階組件 - 內置的transition組件分析 - 敬請期待

簡單說一下:提供了一個render函數,使用底層方法構造任意想要的組件,返回。

複雜項目中的狀態管理 - 敬請期待

組件與SSR - 敬請期待

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章