劍橋,攜程資深前端開(kāi)發(fā)工程師,關(guān)注自動(dòng)化工具開(kāi)發(fā)、前端工程自動(dòng)構(gòu)建相關(guān)技術(shù)。
隨著前端工程的發(fā)展,組件化的思想早已深入人心;現(xiàn)代的前端框架React/Vue等,都是圍繞組件設(shè)計(jì);組件化的開(kāi)發(fā)模式,大大提高了開(kāi)發(fā)效率;設(shè)計(jì)和開(kāi)發(fā)高質(zhì)量高復(fù)用性的公共組件,可以更好地保持產(chǎn)品迭代的高效和穩(wěn)定。
我們以React的技術(shù)棧為背景,在日常的需求與迭代中, 歷時(shí)兩年多時(shí)間,沉淀出了攜程用車(chē)各大產(chǎn)線(接送機(jī)/包車(chē)/打車(chē)服務(wù)等)的公共組件(機(jī)場(chǎng)、航班、城市、地址、時(shí)間控件等)。通過(guò)持續(xù)交付了一系列的組件庫(kù),讓各個(gè)產(chǎn)線的開(kāi)發(fā)小組不用再各自維護(hù)重復(fù)而難以迭代的代碼,完成了前端組件與公共方法的收口,解決了用車(chē)前端業(yè)務(wù)組件一致性的問(wèn)題。同時(shí)隨著組件庫(kù)工作流上的逐步完善,讓前端開(kāi)發(fā)同學(xué)脫離了刀耕火種的開(kāi)發(fā)方式,進(jìn)入了全新的自動(dòng)化構(gòu)建與高效開(kāi)發(fā)的時(shí)代。
開(kāi)發(fā)和維護(hù)一個(gè)可持續(xù)迭代的組件庫(kù),從來(lái)都不是一件容易的事情。本文將從組件庫(kù)的基礎(chǔ)搭建開(kāi)始,從開(kāi)發(fā)、打包、發(fā)布、拆包、優(yōu)化、自動(dòng)化測(cè)試等各方面,由淺及深地進(jìn)行介紹,給大家分享一個(gè)相對(duì)完善的組件庫(kù)落地的過(guò)程。同時(shí)也會(huì)介紹組件庫(kù)的迭代過(guò)程中真正會(huì)遇到哪些問(wèn)題,以及我們是如何解決這些問(wèn)題的。希望這些實(shí)戰(zhàn)中的經(jīng)驗(yàn),可以帶給大家一些啟發(fā)和想法。一、實(shí)現(xiàn)最基礎(chǔ)的npm發(fā)布流程
在組件庫(kù)的設(shè)計(jì)之初,我們最先需要考慮的是,如何讓npm包的發(fā)布流程安全、可靠可行。為了保證代碼的安全性,公司內(nèi)部會(huì)獨(dú)立維護(hù)內(nèi)網(wǎng)的npm管理平臺(tái)。
在最早的發(fā)布設(shè)計(jì)中,我們?nèi)匀煌ㄟ^(guò)官方定義的cli命令,在本地通過(guò)設(shè)置registry指向內(nèi)網(wǎng)倉(cāng)庫(kù)后,執(zhí)行npm publish 進(jìn)行發(fā)布。可是對(duì)于公司內(nèi)部而言,平臺(tái)開(kāi)放而B(niǎo)U眾多,任何人都可以對(duì)任何已發(fā)布的包進(jìn)行常規(guī)操作,這會(huì)帶來(lái)一系列的不安全因素。最終在前端委員會(huì)的推動(dòng)下,我司實(shí)現(xiàn)了內(nèi)網(wǎng)npm與gitlab ci的關(guān)聯(lián)。將發(fā)布操作遷移到了gitlab上,在發(fā)布權(quán)限上有一定的約束;通過(guò)開(kāi)啟npm deploy插件,以實(shí)現(xiàn)可視化交互式的發(fā)布管理,同時(shí)得益于gitlab hook的強(qiáng)大, 我們更是在流程實(shí)現(xiàn)了push event來(lái)觸發(fā)auto publish,這一系列的進(jìn)步,讓我們的組件庫(kù)在后續(xù)的發(fā)布流程上變得更加正式、穩(wěn)定而可靠。

Npm關(guān)聯(lián)gitlab后,通過(guò)指定指定分支下特定目錄的package.json,實(shí)現(xiàn)版本升級(jí)后自動(dòng)發(fā)布我們的技術(shù)棧涉及ReactWeb 與 React Native, 對(duì)于RN的代碼,我們一般會(huì)走源碼直接發(fā)布,RN項(xiàng)目中的編譯過(guò)程會(huì)自動(dòng)處理node_modules里的源文件。但是對(duì)于Web組件庫(kù)而言,更傳統(tǒng)的做法,則是需要在發(fā)布之前進(jìn)行一些編譯和轉(zhuǎn)碼,這樣才能確保發(fā)布之后的npm包,可以在大多數(shù)環(huán)境下正常運(yùn)行起來(lái)。對(duì)于Web端組件庫(kù)的打包,我們進(jìn)行了多次的探索和優(yōu)化。使用webpack對(duì)每個(gè)組件進(jìn)行單獨(dú)打包,打包類型由umd改為commonjs2。module.exports = { output: { filename: '[name].js', path: path.resolve(__dirname, '..', 'dist'), library: 'Tha', libraryTarget: 'commonjs2' // umd }}
通常我們對(duì)組件庫(kù)的建議是umd打包,因?yàn)檫@樣可以實(shí)現(xiàn)多種模塊方案的加載通用性。但在實(shí)踐過(guò)程中發(fā)現(xiàn),每個(gè)組件都需要單獨(dú)打包時(shí),UMD的打包方式,會(huì)顯著增大每個(gè)文件的基礎(chǔ)體積;而且我們99% 的場(chǎng)景下,其實(shí)已經(jīng)并不用再去兼容AMD、CMD等模塊加載方式。在確保我們的代碼一定是通過(guò)node模塊方式加載的時(shí)候,我們只需要打出commonjs2的模塊即可。這一步的調(diào)整,顯著地提升了打包速度,也明顯減小了各個(gè)文件的打包體積。對(duì)于組件庫(kù)而言,使用webpack進(jìn)行打包,即使是使用了commonjs2的模式,繁重的配置工具仍然是顯得重了一些,而且需要額外配置各種external規(guī)則,以防止打包時(shí)打入了額外的第三方庫(kù)的代碼。使用rollup來(lái)處理組件庫(kù)的打包固然比webpack要合適,但是又會(huì)額外引入新的構(gòu)建工具,增加學(xué)習(xí)成本。最終我們選擇的更優(yōu)化的方案,是使用babel 直接做編譯轉(zhuǎn)換,不使用任何額外的構(gòu)建工具,也不做壓縮優(yōu)化處理---- 這些工作,在現(xiàn)代化的前端項(xiàng)目中,都會(huì)自動(dòng)處理,不需要組件庫(kù)再做多余的構(gòu)建動(dòng)作。Babel直接轉(zhuǎn)碼的方式,幫助我們省去了很多復(fù)雜的配置工作,并且讓組件庫(kù)打出來(lái)的生產(chǎn)代碼更加容易調(diào)試。優(yōu)化前,使用webpack等構(gòu)建工具打包組件:
{ "scripts": { "build:components": "webpack --config ./build/webpack.config.js --color", "build": "npm run build:components && npm run build:css && npm run copy_package" }}
優(yōu)化后, 編寫(xiě)腳本直接對(duì)組件源文件轉(zhuǎn)碼
{ "scripts": { "build:components": "cross-env NODE_ENV=production node ./build/trans" }}
Css-in-js的開(kāi)發(fā)方式固然是方便許多,但是在打正式包時(shí),內(nèi)嵌的css實(shí)際會(huì)占用更多的代碼體積,并且node_modules里的js代碼中如果有顯式require css的語(yǔ)句時(shí),在同構(gòu)項(xiàng)目中,可能會(huì)遇到服務(wù)端解析css文件的各種問(wèn)題。為了解決這個(gè)問(wèn)題,我們提取了所有組件的css進(jìn)行單獨(dú)打包。其中所有的基礎(chǔ)組件樣式,會(huì)整體打包成一個(gè)main.css;而復(fù)雜業(yè)務(wù)組件的樣式,則會(huì)以組件為單位進(jìn)行單獨(dú)打包,以便實(shí)現(xiàn)后續(xù)流程中業(yè)務(wù)組件的按需加載。

三、組件庫(kù)實(shí)現(xiàn)業(yè)務(wù)組件的按需加載
與各大知名的開(kāi)源組件庫(kù)類似,為了減少項(xiàng)目的打包體積,我們對(duì)組件庫(kù)中的復(fù)雜業(yè)務(wù)組件,如航班組件、機(jī)場(chǎng)組件、城市選擇組件等,設(shè)計(jì)了按需加載的模式。對(duì)RN而言,我們直接利用了require的特性,通過(guò)修改導(dǎo)出對(duì)象的get方法,顯式地聲明了lazyLoad的組件程式。module.exports = { //按需動(dòng)態(tài)加載的模塊 get AddressList() { return require('./Address/List').default; }};
對(duì)于Web而言,我們采用了類似ant-design的方式,在前面對(duì)業(yè)務(wù)組件的css進(jìn)行單獨(dú)打包處理后,通過(guò)在項(xiàng)目中引入babel插件的方式,實(shí)現(xiàn)組件的按需加載。import { Address } from '@ctrip/thanos-ctrip-mobile/components.biz'/** 等價(jià)于import Address from '@ctrip/thanos-ctrip-mobile/components.biz/Address'import '@ctrip/thanos-ctrip-mobile/components.biz/Address/style.css'*/
隨著組件庫(kù)的不斷迭代,組件代碼會(huì)不斷增多,需求也會(huì)越來(lái)越復(fù)雜。其他研發(fā)同學(xué)也可能會(huì)開(kāi)發(fā)獨(dú)立的npm組件包,但是會(huì)基于已開(kāi)發(fā)完成的組件庫(kù)的部分功能來(lái)實(shí)現(xiàn)。這種情況下,開(kāi)發(fā)其他npm包的同學(xué),可能只想使用當(dāng)前已有庫(kù)中的部分功能,而不太愿意引入一個(gè)完整而龐大的組件庫(kù)。為了使組件庫(kù)的功能更加獨(dú)立且通用,讓UI組件與功能模塊之間更好地解耦,我們需要對(duì)組件庫(kù)進(jìn)行拆子包處理。如組件項(xiàng)目中基礎(chǔ)UI部分,從組件庫(kù)中剝離,拆分成獨(dú)立的ui-basic組件庫(kù);組件項(xiàng)目中工具方法(表單校驗(yàn)、環(huán)境判斷、正則處理、時(shí)間日期格式化等),拆分成獨(dú)立的 util庫(kù)。這種拆分組件包的開(kāi)發(fā)形式,組件庫(kù)不再是所有功能都揉在一個(gè)倉(cāng)庫(kù)中,開(kāi)發(fā)和維護(hù)將變得更加靈活且易于擴(kuò)展。拆包前,core的部分將隨著功能的增加而越來(lái)越臃腫:


如圖所示,拆分獨(dú)立功能包后,可以讓我們擴(kuò)展和組合出更多靈活多樣的組件庫(kù),讓組件庫(kù)不再單一而臃腫。五、解決子組件包的開(kāi)發(fā)環(huán)境問(wèn)題
拆分子組件包后,給組件庫(kù)的多樣性擴(kuò)展帶來(lái)了極大的便利,但隨之而來(lái)的問(wèn)題便是,每一個(gè)子組件包都需要單獨(dú)維護(hù),在開(kāi)發(fā)子組件包時(shí),每一個(gè)包都需要一個(gè)可運(yùn)行的本地開(kāi)發(fā)環(huán)境。隨著子組件包的數(shù)量逐漸增多,給每一個(gè)包都單獨(dú)設(shè)立一個(gè)開(kāi)發(fā)環(huán)境,必然會(huì)帶來(lái)更大的維護(hù)成本。我們目前選擇的解決方案是,對(duì)于粒度更細(xì)的子組件包,所有的子包會(huì)公用一套dev的開(kāi)發(fā)倉(cāng)庫(kù),通過(guò) git modules在開(kāi)發(fā)倉(cāng)庫(kù)中嵌套子模塊倉(cāng)庫(kù),實(shí)現(xiàn)了只維護(hù)一套開(kāi)發(fā)環(huán)境,產(chǎn)出多個(gè)子模塊包的組件庫(kù)工廠。

在這種環(huán)境下,還可以做到當(dāng)子模塊之間存在相互依賴時(shí),可以直接引用相對(duì)路徑下其他模塊的源碼,不必為了調(diào)試某個(gè)模塊的代碼,而跑到node_modules里去翻找,徒增調(diào)試難度。六、組件庫(kù)文檔化與協(xié)同開(kāi)發(fā)
為了讓組件庫(kù)的開(kāi)發(fā)流程更加規(guī)范,減少接入方的溝通成本,對(duì)組件庫(kù)進(jìn)行適當(dāng)?shù)奈臋n梳理是十分必要的,我們使用gitbook 編寫(xiě)組件庫(kù)的文檔,并部署到公司內(nèi)部的books平臺(tái)上。同樣借助于gitlab強(qiáng)大的web hook的能力,實(shí)現(xiàn)了文檔倉(cāng)庫(kù)的自動(dòng)更新與發(fā)布。


與此同時(shí),我們也啟用了協(xié)同開(kāi)發(fā)的模式,讓組件庫(kù)成為一個(gè)內(nèi)部的開(kāi)源庫(kù),用車(chē)產(chǎn)線的研發(fā)同學(xué),可以通過(guò)提交issuse和merge request的方式,自行對(duì)組件庫(kù)中的個(gè)別需求進(jìn)行開(kāi)發(fā),提升開(kāi)發(fā)效率。七、組件庫(kù)單元測(cè)試、自動(dòng)化與持續(xù)集成
當(dāng)組件庫(kù)在開(kāi)發(fā)和交付流程上趨于完善后,在公司G2戰(zhàn)略背景下,為了保證代碼的高質(zhì)量,我們開(kāi)始在組件庫(kù)中接入自動(dòng)化單元測(cè)試。接入單元測(cè)試也是一項(xiàng)十分曲折的過(guò)程。在測(cè)試技術(shù)框架的選型上,綜合考慮了當(dāng)前技術(shù)棧、框架市面通用性等多種因素,最終選擇如下:選取原因:對(duì)React技術(shù)棧友好,同時(shí)也是React-Native官方推薦的測(cè)試框架web端 -> @testing-library/reactRN ->@testing-library/react-native選取原因:React的官方測(cè)試庫(kù),對(duì)hooks類型的組件支持度高,選擇這兩個(gè)庫(kù),也是為了能夠保持后續(xù)與react官方版本更新的同步在接入單元測(cè)試后,我們依然借助gitlab的CI/CD,對(duì)整個(gè)組件庫(kù)的流程進(jìn)行自動(dòng)化構(gòu)建與持續(xù)集成交付,在內(nèi)置CtripDevOps或者自定義gitlab-ci.yml的配置下,我們將單元測(cè)試的環(huán)節(jié)加入到了pipeline中,同時(shí)通過(guò)公司統(tǒng)一的sonar檢測(cè),提供最終的組件庫(kù)質(zhì)量統(tǒng)計(jì)報(bào)告。



要搭建一個(gè)相對(duì)完善的組件庫(kù),都是需要經(jīng)過(guò)一系列項(xiàng)目的沉淀的。目前而言,組件庫(kù)的開(kāi)發(fā)流程上依然會(huì)存在一些問(wèn)題,比如版本管理、升級(jí)回退等。時(shí)間是最好的老師,相信在后面的迭代中,我們依然能夠保持初心與熱情,積極探索與發(fā)現(xiàn),構(gòu)建出更加完善的前端工程體系。
“攜程技術(shù)”公眾號(hào)后臺(tái)回復(fù)“新書(shū)”,
可免費(fèi)獲得兩本書(shū)的試讀樣章~
“攜程技術(shù)”公眾號(hào)
分享,交流,成長(zhǎng)