我們團(tuán)隊(duì)近期發(fā)布了移動(dòng)端 Vue 組件庫(kù) NutUI 的 2.0 版[1]租副,2.0 不是 1.0 的升級(jí)舟茶,而是一個(gè)全新的組件庫(kù)闸餐。從 1.0 到 2.0 一路走來(lái),我們積累了一些 Vue 組件庫(kù)的開(kāi)發(fā)經(jīng)驗(yàn)个盆,接下來(lái)的一段時(shí)間脖岛,我們將以系列文章的形式與大家進(jìn)行分享,歡迎大家關(guān)注砾省。
作為《Vue組件庫(kù)工程探索與實(shí)踐》系列文章開(kāi)篇之作杆煞,我們從“盤(pán)古開(kāi)天地”說(shuō)起吧们拙。
從當(dāng)年的靜態(tài)頁(yè)面到如今的 Web App宝鼓,前端工程越來(lái)越復(fù)雜和媳,對(duì)于一個(gè)稍大些的前端項(xiàng)目來(lái)說(shuō),代碼都寫(xiě)在一起難以維護(hù)狠鸳,團(tuán)隊(duì)分工協(xié)作也成問(wèn)題揣苏。根據(jù)軟件工程領(lǐng)域的經(jīng)驗(yàn),解決這些問(wèn)題的一個(gè)可行思路就是代碼的模塊化件舵,即對(duì)代碼按功能模塊進(jìn)行分拆卸察,封裝成組件,而反過(guò)來(lái)講铅祸,組件就是指能完成某個(gè)特定功能的獨(dú)立的坑质、可重用的代碼塊合武。
把一個(gè)大的應(yīng)用分解成若干小的組件,而每個(gè)組件只需要關(guān)注于某個(gè)小范圍的特定功能涡扼,但是把組件組合起來(lái)稼跳,就能構(gòu)成一個(gè)功能龐大的應(yīng)用。組件化的網(wǎng)頁(yè)開(kāi)發(fā)也是如此吃沪,就像搭積木汤善,各個(gè)組件拼接在一起就組成了一個(gè)完整的頁(yè)面。
組件化開(kāi)發(fā)可大大降低代碼耦合度票彪、提高代碼的可維護(hù)性和開(kāi)發(fā)效率红淡,同時(shí)有利于團(tuán)隊(duì)分工協(xié)作和降低開(kāi)發(fā)成本。這種開(kāi)發(fā)模式已日漸流行起來(lái)降铸。
當(dāng)前在旱,前端開(kāi)發(fā)領(lǐng)域最流行的三大框架 Vue、React垮耳、Angular 都推崇組件化開(kāi)發(fā)颈渊,組件是這些框架中極為重要的概念和功能。
以 Vue.js 來(lái)說(shuō)终佛,組件 (Component) 可以說(shuō)是其最強(qiáng)大的功能,它可以擴(kuò)展 HTML 元素雾家,封裝可重用的代碼铃彰。Vue.js 的組件系統(tǒng)讓我們可以用這些獨(dú)立可復(fù)用的小組件來(lái)構(gòu)建大型 Vue 應(yīng)用,幾乎任意類(lèi)型的 Vue 應(yīng)用的界面都可以抽象為一個(gè)組件樹(shù)芯咧。
如果我們把日常應(yīng)用開(kāi)發(fā)中常用的組件累積起來(lái)牙捉,后續(xù)的項(xiàng)目就可以復(fù)用這些組件,這對(duì)提高開(kāi)發(fā)效率敬飒、降低開(kāi)發(fā)成本有重要意義邪铲。
因此,一個(gè)前端團(tuán)隊(duì)擁有一個(gè)常用框架的組件庫(kù)是十分必要的无拗。
模塊化與構(gòu)建工具
組件庫(kù)自身就是一個(gè)大的工程带到,需要按照模塊化開(kāi)發(fā)思想進(jìn)行模塊劃分。通常英染,在一個(gè)組件庫(kù)里揽惹,組件、組件的樣式文件四康、配置文件…都是模塊搪搏,而最終我們需要把這些模塊組合成一個(gè)完整的組件庫(kù)文件,承擔(dān)這種組裝工作的就是打包構(gòu)建工具闪金。當(dāng)下主流的庫(kù)構(gòu)建工具主要有 Rollup 和 Webpack 等疯溺。在說(shuō)這些模塊打包構(gòu)建工具之前,我們先來(lái)了解一下目前主流的 JavaScript 模塊化方案。
JavaScript 語(yǔ)言一直以來(lái)飽受詬病的一個(gè)地方就是它的語(yǔ)言標(biāo)準(zhǔn)里沒(méi)有模塊(module)體系囱嫩,這對(duì)開(kāi)發(fā)大型的恃疯、復(fù)雜的項(xiàng)目形成了巨大障礙。直到 ES6 時(shí)期挠说,才在語(yǔ)言標(biāo)準(zhǔn)層面實(shí)現(xiàn)模塊功能(ES6 Module)澡谭。在 ES6 之前,業(yè)界流行的是社區(qū)制定的一些模塊加載方案损俭,如 CommonJS 和 AMD 蛙奖。而 ES6 Module 作為官方規(guī)范,且瀏覽器端和服務(wù)器端通用杆兵,未來(lái)一定會(huì)一統(tǒng)天下雁仲,但由于 ES6 Module 來(lái)的太晚,受限于兼容性等因素琐脏,可以預(yù)見(jiàn)的是今后一段時(shí)期內(nèi)攒砖,多種模塊化方案仍會(huì)共存。
- ES6 Modue 規(guī)范:JavaScript 語(yǔ)言標(biāo)準(zhǔn)模塊化方案日裙,瀏覽器和服務(wù)器通用吹艇,模塊功能主要由 export 和 import 兩個(gè)命令構(gòu)成。export 用于定義模塊的對(duì)外接口昂拂,import 用于輸入其他模塊提供的功能受神。
- CommonJS 規(guī)范:主要用于服務(wù)端的 JavaScript 模塊化方案,Node.js 采用的就是這種方案格侯,所以各種 Node.js 環(huán)境的前端構(gòu)建工具都支持該規(guī)范鼻听。CommonJS 規(guī)范規(guī)定通過(guò) require 命令加載其他模塊,通過(guò) module.exports 或者 exports 對(duì)外暴露接口联四。
- AMD 規(guī)范:全稱(chēng)是 Asynchronous Modules Definition撑碴,異步模塊定義規(guī)范,一種更主要用于瀏覽器端的 JavaScript 模塊化方案朝墩,該方案的代表實(shí)現(xiàn)者是 RequireJS醉拓,通過(guò) define 方法定義模塊,通過(guò) require 方法加載模塊鱼辙。
一些“上年紀(jì)”的國(guó)內(nèi)前端老藝人們可能還會(huì)提到 CMD 規(guī)范廉嚼,它是 SeaJS 在推廣過(guò)程中對(duì)模塊定義的規(guī)范化產(chǎn)出,只是 SeaJS 并未實(shí)現(xiàn)國(guó)際化倒戏,且項(xiàng)目在2015年就已宣布停止維護(hù)了怠噪,算不上當(dāng)前主流模塊化方案。
介紹完主流模塊化規(guī)范杜跷,我們?cè)倩剡^(guò)頭來(lái)看 Rollup 和 Webpack 這兩個(gè)模塊打包構(gòu)建工具傍念。
Rollup 是一個(gè)頗有名氣的庫(kù)打包工具矫夷,很多知名的庫(kù)、框架都是使用它打的包憋槐,包括 Vue 和 React 自身双藕。Rollup 可以直接對(duì) ES6 模塊進(jìn)行打包,它率先提出并實(shí)現(xiàn)了 Tree-shaking 功能阳仔,即在打包時(shí)靜態(tài)分析 ES6 模塊代碼中的 import忧陪,排除未實(shí)際使用的代碼,這有助于減小構(gòu)建包的體積近范。
另一個(gè)打包工具 Webpack 名氣更大嘶摊,不過(guò)我們通常用它來(lái)打包應(yīng)用,而事實(shí)上它對(duì)庫(kù)打包也能提供很好的支持评矩。Webpack 支持代碼分割叶堆、模塊的熱更新(HMR)等功能,這讓它看起來(lái)非常適合打包應(yīng)用斥杜。而 Webpack 2 及后續(xù)版本陸續(xù)增加了對(duì) ES6 模塊虱颗、Tree-shaking、Scope Hoisting 的支持蔗喂,大大增強(qiáng)了其庫(kù)打包能力忘渔。
如今,Rollup 在庫(kù)打包方面的優(yōu)勢(shì)已不再那么明顯缰儿,而在對(duì)應(yīng)用打包的支持方面卻明顯落后于 Webpack 辨萍。所以打包應(yīng)用推薦使用 Webpack ,而打包庫(kù)的話(huà)返弹, Rollup 和 Webpack 基本都能勝任。
那么我們?cè)陂_(kāi)發(fā) NutUI 2 的時(shí)候?yàn)槭裁催x擇了 Webpack 而不是 Rollup 呢爪飘?其實(shí)主要還是上述這個(gè)原因义起,按照規(guī)劃,NutUI 的官網(wǎng)(包含示例和文檔)與庫(kù)在同一個(gè)項(xiàng)目中师崎,因此我們需要一個(gè)既能打包庫(kù)默终,又能打包應(yīng)用的工具,Webpack 顯然更適合犁罩。
Webpack打包Library
使用 Webpack 來(lái)打包應(yīng)用齐蔽,相信大多前端小伙伴都不會(huì)感到陌生〈补溃可如何使用 Webpack 來(lái)打包一個(gè)組件庫(kù)呢含滴?各位細(xì)聽(tīng)我來(lái)言。
首先丐巫,雖然基于 ES6 模塊規(guī)范開(kāi)發(fā)谈况,但考慮到瀏覽器兼容性勺美,我們需要打包出來(lái)的組件庫(kù)能兼容 AMD 等瀏覽器端模塊規(guī)范。同時(shí)碑韵,為了使組件庫(kù)能支持服務(wù)端渲染(SSR)等場(chǎng)景赡茸,它還需要支持 commonJS 規(guī)范。此外祝闻,還有一種常見(jiàn)的庫(kù)使用場(chǎng)景占卧,即在頁(yè)面上直接通過(guò) script 標(biāo)簽引入,也就是非模塊化環(huán)境同樣需要兼容联喘。
Webpack 中华蜒,output.libraryTarget 選項(xiàng)用來(lái)配置如何暴露庫(kù),可配置以 commonJS 模塊耸袜、AMD 模塊友多,甚至全局變量形式暴露庫(kù)〉炭颍可是如何讓這個(gè)庫(kù)可以同時(shí)兼容 commonJS域滥、AMD 和全局變量呢?
所幸蜈抓,這個(gè)選項(xiàng)還支持一個(gè)可選值—— umd启绰。UMD(Universal Module Definition,通用模塊規(guī)范)可以同時(shí)支持 CommonJS 和 AMD 規(guī)范沟使,以及非模塊化引用委可。
綜上,我們需要把 output.libraryTarget 的值設(shè)為“umd”腊嗡。
另外兩個(gè)與庫(kù)打包關(guān)系密切的Webpack配置項(xiàng)如下:
- output.library 着倾,對(duì)外暴露的變量名或模塊名,具體作用與 output.libraryTarget 選項(xiàng)的值有關(guān)燕少。
- output.umdNamedDefine 卡者,當(dāng) output.libraryTarget 的值為“umd”時(shí),設(shè)置該選項(xiàng)的值為 true 會(huì)對(duì) UMD 的構(gòu)建過(guò)程中的 AMD 模塊進(jìn)行命名客们,否則就使用匿名的 define崇决,匿名的 AMD 模塊。
這幾個(gè)選項(xiàng)配置完底挫,就可以打包出一個(gè)基于 umd 規(guī)范庫(kù)了恒傻。
output: {
path: path.resolve(__dirname, '../dist/'),
filename: 'nutui.js',
library: 'nutui',
libraryTarget: 'umd',
umdNamedDefine: true
}
但是我們會(huì)發(fā)現(xiàn)構(gòu)建出來(lái)的庫(kù)在 Node.js 環(huán)境使用時(shí)會(huì)報(bào)錯(cuò):
window is not defined
是不是感到莫名其妙?說(shuō)好的 UMD 兼容 commonJS 呢建邓?查看 Webpack 構(gòu)建出的包代碼盈厘,我們會(huì)發(fā)現(xiàn),UMD 部分的代碼里的全局對(duì)象竟然是 window 涝缝!非瀏覽器環(huán)境哪有 window 對(duì)象扑庞,Node.js 中不報(bào)錯(cuò)才怪譬重。
(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 文檔,可以發(fā)現(xiàn) output 對(duì)象還有一個(gè)屬性叫 globalObject 罐氨,用來(lái)指定掛載這個(gè)庫(kù)的全局對(duì)象臀规,默認(rèn)值是 window 。而這部分文檔明確指出栅隐,當(dāng)構(gòu)建 UMD 包需要兼容瀏覽器和 Node.js 環(huán)境時(shí)塔嬉,值應(yīng)該設(shè)為 this 。
output: {
path: path.resolve(__dirname, '../dist/'),
filename: 'nutui.js',
library: 'nutui',
libraryTarget: 'umd',
umdNamedDefine: true,
globalObject: 'this'
}
我們將 globalObject 設(shè)置為 'this' 后租悄,構(gòu)建出來(lái)的包中 UMD 部分的 window 被替換為了 this 谨究,這樣在 Node.js 環(huán)境就不會(huì)再報(bào)上面那個(gè)錯(cuò)了,這對(duì)實(shí)現(xiàn)組件庫(kù)兼容服務(wù)端渲染功能來(lái)說(shuō)非常重要泣棋。
(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__) {
這里吐個(gè)槽胶哲,個(gè)人感覺(jué) Webpack 這部分設(shè)計(jì)欠妥,當(dāng) libraryTarget 值為 umd 時(shí) globalObject 默認(rèn)值應(yīng)該為 this 潭辈,而不能是 window 鸯屿,否則 umd 還有何意義?至少在文檔中 libraryTarget: 'umd' 部分對(duì)此問(wèn)題應(yīng)該有所提及把敢,不然還會(huì)有不少人踩此坑寄摆。
外部依賴(lài)Vue.js
Vue 組件庫(kù)不需要把 Vue.js 也打包進(jìn)去,可在運(yùn)行時(shí)從外部獲取修赞。Webpack 中可以通過(guò) externals 配置外部依賴(lài)婶恼。我們不妨以 jquery 為例看下 externals 的配置方法:
externals: {
jquery: 'jQuery'
}
這樣 jquery 在構(gòu)建時(shí)不會(huì)打到包內(nèi),而是在運(yùn)行時(shí)需要 jquery 的時(shí)候去外部環(huán)境尋找 jQuery 這個(gè)模塊(或?qū)傩裕┌馗薄U肇埉?huà)虎勾邦,依葫蘆畫(huà)瓢,我們不需要打包 Vue.js 割择,那我們就這么寫(xiě):
externals: {
vue: 'vue'
}
這時(shí)候構(gòu)建出來(lái)的包在各種模塊化場(chǎng)景使用都沒(méi)毛病检痰,可唯獨(dú)在非模塊化場(chǎng)景會(huì)報(bào)錯(cuò):
vue is not defined
這是為什么呢?我們先來(lái)看下 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 源碼中锨推,我們可以看到掛到全局對(duì)象上的 vue 屬性名稱(chēng)是首字母大寫(xiě)的 Vue,而其 NPM 包名卻是小寫(xiě)的 vue 公壤,也就是說(shuō)不同環(huán)境下 Vue 名稱(chēng)不盡一致换可,這可如何是好?
{
"name": "vue",
"version": "2.6.10",
還好厦幅,externals 中屬性的值除了字符串沾鳄,還支持傳一個(gè)對(duì)象,可針對(duì)各種場(chǎng)景單獨(dú)設(shè)置模塊名(或?qū)傩悦┤泛@樣一來(lái)译荞,我們就可以為非模塊化環(huán)境配置 'Vue'瓤的,為模塊化環(huán)境配置 'vue'。
externals: {
'vue': {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
}
}
Vue.js 就是這樣被設(shè)置為組件庫(kù)外部依賴(lài)的吞歼。
Tree-shaking(搖樹(shù))
如前文所述圈膏,Tree-shaking 功能最早由 Rollup 提出并實(shí)現(xiàn),曾是 Rollup 的殺手锏篙骡,后來(lái) Webpack 等工具把它“借鑒”走了稽坤。
Tree-shaking 的原理是在打包時(shí)通過(guò)對(duì)代碼進(jìn)行靜態(tài)分析將未使用的代碼排除,從而減小包體積糯俗。對(duì) JavaScript 進(jìn)行靜態(tài)分析尿褪,這在之前是不可能的。直到 ES6 模塊化方案的提出得湘,才使得 JavaScript 靜態(tài)分析成為可能杖玲,因?yàn)?ES6 模塊是編譯時(shí)加載,不用等到代碼運(yùn)行時(shí)就可以知道加載了哪些模塊淘正。因此要使用 Tree-shaking 功能摆马,就需要在代碼中使用 ES6 模塊方案,不管是用 Rollup 還是 Webpack 打包跪帝。
還有一個(gè)影響 Tree-shaking 施展的可能今膊,那就是 Babel 在 Webpack 開(kāi)始“搖”之前把你的 ES6 模塊轉(zhuǎn)成了 commonJS 模塊,那就“搖”不了了伞剑。這種情況并不罕見(jiàn)斑唬,大部分前端開(kāi)發(fā)者都樂(lè)于使用新語(yǔ)法,所以不止模塊化方案要用 ES6 Module 黎泣,甚至整個(gè)項(xiàng)目的 JavaScript 代碼都用 ES6+ 語(yǔ)法來(lái)寫(xiě)恕刘,為了兼容低版本環(huán)境,通常會(huì)使用 Babel 等工具把 ES6+ 語(yǔ)法轉(zhuǎn)成 低版本語(yǔ)法抒倚。這當(dāng)然沒(méi)問(wèn)題褐着,只是如果想讓 Tree-shaking 發(fā)揮作用,讓我們構(gòu)建出來(lái)的包體積更小托呕,一定要注意含蓉,不要讓 Babel 把ES6模塊語(yǔ)法轉(zhuǎn)成 commonJS ,Rollup 和較新版本的 Webpack 都支持直接處理 ES6 模塊项郊,可以也應(yīng)該把 ES6 模塊部分直接交給它們來(lái)處理馅扣。不使用 Babel 處理ES6模塊,并不意味著最終打出來(lái)的包就是 ES6 模塊着降,如前文所述差油,構(gòu)建出來(lái)的包如何暴露,要兼容哪些模塊規(guī)范打包工具就能搞定任洞。
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
]
}
我們測(cè)試了一下蓄喇,Tree-shaking 讓 NutUI 2.0 的完整版的構(gòu)建文件體積明顯減小发侵。
好了,關(guān)于構(gòu)建工具我們先說(shuō)到這里妆偏,具體實(shí)現(xiàn)細(xì)節(jié)可以參考 NutUI 2.0 的源碼[2]刃鳄。后續(xù)的文章我們還會(huì)談組件庫(kù)的按需加載、主題定制楼眷、國(guó)際化铲汪、單元測(cè)試、持續(xù)集成罐柳、基于Markdown文件生成靜態(tài)文檔網(wǎng)站掌腰、Vue公共組件開(kāi)發(fā)等方面的探索實(shí)踐經(jīng)驗(yàn),敬請(qǐng)關(guān)注张吉。
鏈接
[1] NutUI 2.0 官網(wǎng) https://nutui.jd.com
[2] NutUI 2.0 代碼倉(cāng)庫(kù) https://github.com/jdf2e/nutui