咱們團隊近期發佈了移動端 Vue 組件庫 NutUI 的 2.0 版(nutui.jd.com),2.0 不是 1.0 的升級,而是一個全新的組件庫。從 1.0 到 2.0 一路走來,咱們積累了一些 Vue 組件庫的開發經驗,接下來的一段時間,咱們將以系列文章的形式與你們進行分享,歡迎你們關注。前端
該系列文章第二篇的傳送門 juejin.im/post/5d1f08…vue
做爲《Vue組件庫工程探索與實踐》系列文章開篇之做,咱們從「盤古開天地」提及吧。jquery
從當年的靜態頁面到現在的 Web App,前端工程愈來愈複雜,對於一個稍大些的前端項目來講,代碼都寫在一塊兒難以維護,團隊分工協做也成問題。根據軟件工程領域的經驗,解決這些問題的一個可行思路就是代碼的模塊化,即對代碼按功能模塊進行分拆,封裝成組件,而反過來說,組件就是指能完成某個特定功能的獨立的、可重用的代碼塊。webpack
把一個大的應用分解成若干小的組件,而每一個組件只須要關注於某個小範圍的特定功能,可是把組件組合起來,就能構成一個功能龐大的應用。組件化的網頁開發也是如此,就像搭積木,各個組件拼接在一塊兒就組成了一個完整的頁面。git
組件化開發可大大下降代碼耦合度、提升代碼的可維護性和開發效率,同時有利於團隊分工協做和下降開發成本。這種開發模式已日漸流行起來。github
當前,前端開發領域最流行的三大框架 Vue、React、Angular 都推崇組件化開發,組件是這些框架中極爲重要的概念和功能。web
以 Vue.js 來講,組件 (Component) 能夠說是其最強大的功能,它能夠擴展 HTML 元素,封裝可重用的代碼。Vue.js 的組件系統讓咱們能夠用這些獨立可複用的小組件來構建大型 Vue 應用,幾乎任意類型的 Vue 應用的界面均可以抽象爲一個組件樹。shell
若是咱們把平常應用開發中經常使用的組件累積起來,後續的項目就能夠複用這些組件,這對提升開發效率、下降開發成本有重要意義。json
所以,一個前端團隊擁有一個經常使用框架的組件庫是十分必要的。瀏覽器
組件庫自身就是一個大的工程,須要按照模塊化開發思想進行模塊劃分。一般,在一個組件庫裏,組件、組件的樣式文件、配置文件…都是模塊,而最終咱們須要把這些模塊組合成一個完整的組件庫文件,承擔這種組裝工做的就是打包構建工具。當下主流的庫構建工具主要有 Rollup 和 Webpack 等。在說這些模塊打包構建工具以前,咱們先來了解一下目前主流的 JavaScript 模塊化方案。
JavaScript 語言一直以來飽受詬病的一個地方就是它的語言標準裏沒有模塊(module)體系,這對開發大型的、複雜的項目造成了巨大障礙。直到 ES6 時期,纔在語言標準層面實現模塊功能(ES6 Module)。在 ES6 以前,業界流行的是社區制定的一些模塊加載方案,如 CommonJS 和 AMD 。而 ES6 Module 做爲官方規範,且瀏覽器端和服務器端通用,將來必定會一統天下,但因爲 ES6 Module 來的太晚,受限於兼容性等因素,能夠預見的是從此一段時期內,多種模塊化方案仍會共存。
一些「上年紀」的國內前端老藝人們可能還會提到 CMD 規範,它是 SeaJS 在推廣過程當中對模塊定義的規範化產出,只是 SeaJS 並未實現國際化,且項目在2015年就已宣佈中止維護了,算不上當前主流模塊化方案。
介紹完主流模塊化規範,咱們再回過頭來看 Rollup 和 Webpack 這兩個模塊打包構建工具。
Rollup 是一個很有名氣的庫打包工具,不少知名的庫、框架都是使用它打的包,包括 Vue 和 React 自身。Rollup 能夠直接對 ES6 模塊進行打包,它率先提出並實現了 Tree-shaking 功能,即在打包時靜態分析 ES6 模塊代碼中的 import,排除未實際使用的代碼,這有助於減少構建包的體積。
另外一個打包工具 Webpack 名氣更大,不過咱們一般用它來打包應用,而事實上它對庫打包也能提供很好的支持。Webpack 支持代碼分割、模塊的熱更新(HMR)等功能,這讓它看起來很是適合打包應用。而 Webpack 2 及後續版本陸續增長了對 ES6 模塊、Tree-shaking、Scope Hoisting 的支持,大大加強了其庫打包能力。
現在,Rollup 在庫打包方面的優點已再也不那麼明顯,而在對應用打包的支持方面卻明顯落後於 Webpack 。因此打包應用推薦使用 Webpack ,而打包庫的話, Rollup 和 Webpack 基本都能勝任。
那麼咱們在開發 NutUI 2 的時候爲何選擇了 Webpack 而不是 Rollup 呢?其實主要仍是上述這個緣由,按照規劃,NutUI 的官網(包含示例和文檔)與庫在同一個項目中,所以咱們須要一個既能打包庫,又能打包應用的工具,Webpack 顯然更適合。
使用 Webpack 來打包應用,相信大多前端小夥伴都不會感到陌生。可如何使用 Webpack 來打包一個組件庫呢?各位細聽我來言。
首先,雖然基於 ES6 模塊規範開發,但考慮到瀏覽器兼容性,咱們須要打包出來的組件庫能兼容 AMD 等瀏覽器端模塊規範。同時,爲了使組件庫能支持服務端渲染(SSR)等場景,它還須要支持 commonJS 規範。此外,還有一種常見的庫使用場景,即在頁面上直接經過 script 標籤引入,也就是非模塊化環境一樣須要兼容。
Webpack 中,output.libraryTarget 選項用來配置如何暴露庫,可配置以 commonJS 模塊、AMD 模塊,甚至全局變量形式暴露庫。但是如何讓這個庫能夠同時兼容 commonJS、AMD 和全局變量呢?
所幸,這個選項還支持一個可選值—— umd。UMD(Universal Module Definition,通用模塊規範)能夠同時支持 CommonJS 和 AMD 規範,以及非模塊化引用。
綜上,咱們須要把 output.libraryTarget 的值設爲「umd」。
另外兩個與庫打包關係密切的Webpack配置項以下:
這幾個選項配置完,就能夠打包出一個基於 umd 規範的庫了。
output: {
path: path.resolve(__dirname, '../dist/'),
filename: 'nutui.js',
library: 'nutui',
libraryTarget: 'umd',
umdNamedDefine: true
}
複製代碼
可是咱們會發現構建出來的庫在 Node.js 環境使用時會報錯:
window is not defined
複製代碼
是否是感到莫名其妙?說好的 UMD 兼容 commonJS 呢?查看 Webpack 構建出的包代碼,咱們會發現,UMD 部分的代碼裏的全局對象居然是 window !非瀏覽器環境哪有 window 對象,Node.js 中不報錯纔怪。
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("vue"));
else if(typeof define === 'function' && define.amd)
define("nutui", ["vue"], factory);
else if(typeof exports === 'object')
exports["nutui"] = factory(require("vue"));
else
root["nutui"] = factory(root["Vue"]);
})(window, function(__WEBPACK_EXTERNAL_MODULE__2__) {
複製代碼
查閱 Webpack 文檔,能夠發現 output 對象還有一個屬性叫 globalObject ,用來指定掛載這個庫的全局對象,默認值是 window 。而這部分文檔明確指出,當構建 UMD 包須要兼容瀏覽器和 Node.js 環境時,值應該設爲 this 。
output: {
path: path.resolve(__dirname, '../dist/'),
filename: 'nutui.js',
library: 'nutui',
libraryTarget: 'umd',
umdNamedDefine: true,
globalObject: 'this'
}
複製代碼
咱們將 globalObject 設置爲 'this' 後,構建出來的包中 UMD 部分的 window 被替換爲了 this ,這樣在 Node.js 環境就不會再報上面那個錯了,這對實現組件庫兼容服務端渲染功能來講很是重要。
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("vue"));
else if(typeof define === 'function' && define.amd)
define("nutui", ["vue"], factory);
else if(typeof exports === 'object')
exports["nutui"] = factory(require("vue"));
else
root["nutui"] = factory(root["Vue"]);
})(this, function(__WEBPACK_EXTERNAL_MODULE__2__) {
複製代碼
這裏吐個槽,我的感受 Webpack 這部分設計欠妥,當 libraryTarget 值爲 umd 時 globalObject 默認值應該爲 this ,而不能是 window ,不然 umd 還有何意義?至少在文檔中 libraryTarget: 'umd' 部分對此問題應該有所說起,否則還會有很多人踩此坑。
Vue 組件庫不須要把 Vue.js 也打包進去,可在運行時從外部獲取。Webpack 中能夠經過 externals 配置外部依賴。咱們不妨以 jquery 爲例看下 externals 的配置方法:
externals: {
jquery: 'jQuery'
}
複製代碼
這樣 jquery 在構建時不會打到包內,而是在運行時須要 jquery 的時候去外部環境尋找 jQuery 這個模塊(或屬性)。照貓畫虎,依葫蘆畫瓢,咱們不須要打包 Vue.js ,那咱們就這麼寫:
externals: {
vue: 'vue'
}
複製代碼
這時候構建出來的包在各類模塊化場景使用都沒毛病,可惟獨在非模塊化場景會報錯:
vue is not defined
複製代碼
這是爲何呢?咱們先來看下 Vue.js 的部分源碼:
/*! * Vue.js v2.6.10 * (c) 2014-2019 Evan You * Released under the MIT License. */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.Vue = factory());
複製代碼
從上面的 Vue.js 源碼中,咱們能夠看到掛到全局對象上的 vue 屬性名稱是首字母大寫的 Vue,而其 NPM 包名倒是小寫的 vue ,也就是說不一樣環境下 Vue 名稱不盡一致,這可如何是好?
{
"name": "vue",
"version": "2.6.10",
複製代碼
還好,externals 中屬性的值除了字符串,還支持傳一個對象,可針對各類場景單獨設置模塊名(或屬性名),這樣一來,咱們就能夠爲非模塊化環境配置 'Vue',爲模塊化環境配置 'vue'。
externals: {
'vue': {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
}
}
複製代碼
Vue.js 就是這樣被設置爲組件庫外部依賴的。
如前文所述,Tree-shaking 功能最先由 Rollup 提出並實現,曾是 Rollup 的殺手鐗,後來 Webpack 等工具把它「借鑑」走了。
Tree-shaking 的原理是在打包時經過對代碼進行靜態分析將未使用的代碼排除,從而減少包體積。對 JavaScript 進行靜態分析,這在以前是不可能的。直到 ES6 模塊化方案的提出,才使得 JavaScript 靜態分析成爲可能,由於 ES6 模塊是編譯時加載,不用等到代碼運行時就能夠知道加載了哪些模塊。所以要使用 Tree-shaking 功能,就須要在代碼中使用 ES6 模塊方案,不論是用 Rollup 仍是 Webpack 打包。
還有一個影響 Tree-shaking 施展的可能,那就是 Babel 在 Webpack 開始「搖」以前把你的 ES6 模塊轉成了 commonJS 模塊,那就「搖」不了了。這種狀況並不罕見,大部分前端開發者都樂於使用新語法,因此不止模塊化方案要用 ES6 Module ,甚至整個項目的 JavaScript 代碼都用 ES6+ 語法來寫,爲了兼容低版本環境,一般會使用 Babel 等工具把 ES6+ 語法轉成 低版本語法。這固然沒問題,只是若是想讓 Tree-shaking 發揮做用,讓咱們構建出來的包體積更小,必定要注意,不要讓 Babel 把ES6模塊語法轉成 commonJS ,Rollup 和較新版本的 Webpack 都支持直接處理 ES6 模塊,能夠也應該把 ES6 模塊部分直接交給它們來處理。不使用 Babel 處理ES6模塊,並不意味着最終打出來的包就是 ES6 模塊,如前文所述,構建出來的包如何暴露,要兼容哪些模塊規範打包工具就能搞定。
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
]
}
複製代碼
咱們測試了一下,Tree-shaking 讓 NutUI 2.0 的完整版的構建文件體積明顯減少。
好了,關於構建工具咱們先說到這裏,具體實現細節能夠參考 NutUI 2.0 的源碼(github.com/jdf2e/nutui)。後續的文章咱們還會談組件庫的按需加載、主題定製、國際化、單元測試、持續集成、基於Markdown文件生成靜態文檔網站、Vue公共組件開發等方面的探索實踐經驗,敬請關注。
[1] NutUI 2.0 官網 nutui.jd.com
[2] NutUI 2.0 代碼倉庫 github.com/jdf2e/nutui