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

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

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


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

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


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



?
要搭建一個相對完善的組件庫,都是需要經(jīng)過一系列項目的沉淀的。目前而言,組件庫的開發(fā)流程上依然會存在一些問題,比如版本管理、升級回退等。時間是最好的老師,相信在后面的迭代中,我們依然能夠保持初心與熱情,積極探索與發(fā)現(xiàn),構(gòu)建出更加完善的前端工程體系。