攜程活動(dòng)搭建平臺(tái)的前端“開(kāi)放性”建設(shè)探索
作者| Jackie,攜程前端開(kāi)發(fā),關(guān)注組件化開(kāi)發(fā),低代碼式建設(shè),致力于通過(guò)前端技術(shù)解決現(xiàn)實(shí)問(wèn)題。
樂(lè)高系統(tǒng)是攜程市場(chǎng)研發(fā)部開(kāi)發(fā)的活動(dòng)搭建平臺(tái),主要滿足運(yùn)營(yíng)所需的各種營(yíng)銷、廣告、頻道、定制等頁(yè)面的快速靈活搭建。平臺(tái)在自身發(fā)展的過(guò)程中不斷改進(jìn)。剛開(kāi)始著力于滿足運(yùn)營(yíng)配置需求,滿足業(yè)務(wù)需求,不斷擴(kuò)充和豐富組件庫(kù),目前平臺(tái)已配置了10000+ 有效頁(yè)面,同時(shí)在線頁(yè)面達(dá)到1000+,組件類型300+。當(dāng)體量達(dá)到一定程度后,我們又在思考,平臺(tái)能力的邊界在哪里,如何推動(dòng)平臺(tái)創(chuàng)造更大的價(jià)值?
這個(gè)時(shí)候,建設(shè)平臺(tái)不再局限于擴(kuò)展組件等基礎(chǔ)建設(shè),會(huì)更多地考慮如何將平臺(tái)建設(shè)為一種“開(kāi)放性”的平臺(tái),將平臺(tái)優(yōu)秀,成熟,可擴(kuò)展的“點(diǎn)“開(kāi)放出去,使平臺(tái)或者平臺(tái)相關(guān)技術(shù)在其他團(tuán)隊(duì)或者場(chǎng)景中有更多的應(yīng)用,產(chǎn)生更大的價(jià)值。這種開(kāi)放性的思路,也積極促進(jìn)了平臺(tái)的進(jìn)一步發(fā)展。
這篇文章將總結(jié)我們?cè)谄脚_(tái)建設(shè)中一些相關(guān)思考和實(shí)現(xiàn)細(xì)節(jié)。
一、組件開(kāi)放性建設(shè) - 自定義擴(kuò)展
在經(jīng)過(guò)了自給自足的初期階段之后,我們覺(jué)得組件化、“低代碼式” 的搭建思路是正確的方向,一方面給運(yùn)營(yíng)同學(xué)提供了極大的方便,另一方面組件的“良性循環(huán)、迭代”促使組件功能更豐富,更靈活,更可靠。并且開(kāi)發(fā)人員不需要把時(shí)間浪費(fèi)在重復(fù)勞動(dòng)上,可以設(shè)計(jì)和開(kāi)發(fā)出更具通用性,更可靠,更“新”的組件模塊。
因此,除了在樂(lè)高平臺(tái)上“發(fā)揮能力”,我們希望樂(lè)高上的“業(yè)務(wù)組件”,“配置能力”等可以幾乎“無(wú)限制”的提供給其他開(kāi)發(fā)者在其他需求中使用,因此進(jìn)一步改造了平臺(tái),使樂(lè)高平臺(tái)的組件庫(kù)更容易擴(kuò)展,性能更好,使用性更廣。
樂(lè)高渲染和開(kāi)發(fā)環(huán)境都是基于公司成熟的服務(wù)端渲染框架NFES,技術(shù)棧選擇為react+nextjs+mobx。
如渲染示意圖:
樂(lè)高整體框架相當(dāng)于一個(gè)容器:
1)負(fù)責(zé)組織組件關(guān)系
例如父子組件關(guān)系,嵌套關(guān)系,組件依賴關(guān)系,數(shù)據(jù)依賴關(guān)系等。
2)解析組件
容器會(huì)提供公共模塊的注入(如:react,react-dom,公共庫(kù)等),目的是為了公共依賴的統(tǒng)一,并且還減小包的大小。容器在客戶端和服務(wù)端分別解析當(dāng)前組件,以供渲染使用。
由于頁(yè)面其實(shí)是由多個(gè)不同組件構(gòu)成,因此需要支持到組件級(jí)別深度的SSR(實(shí)現(xiàn)方式在后文會(huì)有說(shuō)明)。
3)遞歸式渲染組件
組件結(jié)構(gòu)可以看成是輕量的DSL,整體簡(jiǎn)潔、扁平,不過(guò)為了能夠靈活的處理組件依賴、控制組件渲染時(shí)機(jī)(先后)、處理父子組件等問(wèn)題,容器內(nèi)部在node中間層“處理完數(shù)據(jù)結(jié)構(gòu)”之后采用了“遞歸式”渲染的方式處理組件。例如tab切換類組件,tab關(guān)聯(lián)的可能還是tab切換,如此反復(fù),其實(shí)就是個(gè)遞歸的過(guò)程。
1.1 構(gòu)建組件?
樂(lè)高組件最終形式是一份UMD代碼,事實(shí)上是一份同構(gòu)代碼,既能在服務(wù)端渲染又能在客戶端渲染。頁(yè)面渲染的時(shí)候,能夠在服務(wù)端和客戶端“按需”的,“動(dòng)態(tài)”的拉取某個(gè)組件的資源包,這些也是為了組件解耦,方便擴(kuò)展新組件,提升頁(yè)面渲染性能,同時(shí)為“多場(chǎng)景的使用組件”這一目標(biāo)打下基礎(chǔ)。
“多場(chǎng)景的使用組件”,即為:
- 可以在樂(lè)高渲染引擎中動(dòng)態(tài)使用
- 可以npm單包輸出
- 可以通過(guò)sdk(樂(lè)高渲染外殼+webapi接口)整體渲染引入到其他三方頁(yè)面
?這些是樂(lè)高組件可以充分發(fā)揮能力的幾個(gè)方面。為了達(dá)到這些需要在組件構(gòu)建環(huán)節(jié)進(jìn)行處理,如下:
1)構(gòu)建“動(dòng)態(tài)組件”資源包(js、css),并支持同構(gòu)渲染
構(gòu)建工具默認(rèn)打出的UMD包無(wú)法滿足樂(lè)高特定的渲染需求,需要自定義一些webpack插件干預(yù)構(gòu)建結(jié)果,如:如何解決動(dòng)態(tài)組件的公共依賴問(wèn)題,如何使得渲染引擎能夠在客戶端和服務(wù)端都能夠解析到動(dòng)態(tài)組件實(shí)例。
首先需要改造組件的最終形式,使其可以接收公共依賴(react,react-dom,公共依賴等),這時(shí)可以修改默認(rèn)打出的UMD自執(zhí)行函數(shù),使其返回一個(gè)普通函數(shù),“依賴”可以通過(guò)渲染引擎在解析時(shí)通過(guò)“形參”變量傳入,如:
如圖,banner組件依賴React等,因此構(gòu)建自動(dòng)改變組件打包結(jié)果,使得banner成為這樣一個(gè)“function”:通過(guò)執(zhí)行傳入e.React,e.ReactDom等依賴后,return得到真正的組件包。
實(shí)現(xiàn)方法是在自定義plugin中,接管組件的打包過(guò)程,替換依賴部分的代碼,將真正需要的依賴如react,react-dom等以形式參數(shù)的“代碼字符串”寫入到組件文件里面,最終通過(guò)替換字符串代碼改寫組件構(gòu)建的結(jié)果(組件的文本字符串)。
此外客戶端還需要使得打包后的資源能夠掛載在某個(gè)全局對(duì)象下,以方便渲染時(shí)候按需獲取組件對(duì)象,這就需要通過(guò)配置構(gòu)建工具實(shí)現(xiàn)。
而webpack的配置項(xiàng)globalObject的作用為:
- 當(dāng)輸出為 library 時(shí),尤其是當(dāng) libraryTarget 為 'umd'時(shí),此選項(xiàng)將決定使用哪個(gè)對(duì)象來(lái)掛載 library。
這個(gè)正好可以為我們所用,如下:
客戶端通過(guò)注入腳本script的方式將組件掛載到前文提到的全局變量window.LEGAO_COMPONENTS上面,渲染引擎將通過(guò)組件唯一標(biāo)志(組件英文名)獲取到組件實(shí)例。?
最終在服務(wù)端和客戶端通過(guò)不同的方式獲取到真正的組件:
客戶端:
- window.LEGAO_COMPONENTS[component.name](dependency).default獲取
服務(wù)端:
- component.realComponnet(dependency).default獲取
2)實(shí)現(xiàn)按需(服務(wù)端和客戶端)動(dòng)態(tài)拉取組件
“樂(lè)高組件”可以托管到到公司框架部門開(kāi)發(fā)的現(xiàn)成的靜態(tài)資源管理平臺(tái)ARES上面,它既能快速將我們的資源(組件的js,css)發(fā)布到CDN上面,又能夠幫助我們管理資源version,此外還支持自動(dòng)線上構(gòu)建(CI/CD)發(fā)布單個(gè)或多個(gè)組件包資源,相關(guān)API也很完善,如:
樂(lè)高腳手架通過(guò)ARES的“commit信息觸發(fā)CI/CD”機(jī)制,將單個(gè)或多個(gè)組件發(fā)布到CDN上面,當(dāng)頁(yè)面被訪問(wèn)時(shí),渲染引擎會(huì)根據(jù)當(dāng)前所需的“組件類型”按需拉取組件。
渲染引擎通過(guò)ARES相應(yīng)的API(如:在node端預(yù)熱組件資源,獲取單個(gè)組件最新有效版本,資源地址等)獲取到資源地址,然后在服務(wù)端下載組件的javascript資源文件的文本內(nèi)容,并通過(guò)requireFromString將字符串文件轉(zhuǎn)換為內(nèi)存變量module,即而完成服務(wù)端的渲染,而客戶端則動(dòng)態(tài)加載這些異步j(luò)s,完成客戶端的渲染。
1.2 樂(lè)高腳手架@ctrip/legao-cli?
樂(lè)高平臺(tái)目前不僅“服務(wù)于”市場(chǎng)部活動(dòng)組,越來(lái)越多團(tuán)隊(duì)通過(guò)自定義組件的形式接入,例如一些頻道頁(yè)的模塊,用了現(xiàn)成的組件,業(yè)務(wù)邏輯不同的組件則由團(tuán)隊(duì)自己定制開(kāi)發(fā)。不僅達(dá)到組件“開(kāi)發(fā)一次,到處使用”,還使得相關(guān)團(tuán)隊(duì)的運(yùn)營(yíng)也能充分利用到樂(lè)高的便利配置系統(tǒng)。
為了建設(shè)“開(kāi)放性”平臺(tái),我們也需要做好開(kāi)發(fā)環(huán)境的建設(shè),以方便更多團(tuán)隊(duì)開(kāi)發(fā)組件。
@ctrip/legao-cli 能夠創(chuàng)建跟線上運(yùn)行環(huán)境高度一致的“開(kāi)發(fā)環(huán)境”,能夠通過(guò)proxy模式,代理測(cè)試線上代碼,能夠構(gòu)建組件資源等。如下:
開(kāi)發(fā)環(huán)境項(xiàng)目作用:
- 提供組件本地開(kāi)發(fā)環(huán)境
- 可作為組件倉(cāng)庫(kù)
- 可配置本地開(kāi)發(fā)組件配置信息
- 配置線上打包(CI/CD)配置信息等
項(xiàng)目目錄說(shuō)明:
- common : 組件公共模塊
- packages : 組件目錄
- legao.config.js : 組件開(kāi)發(fā)配置項(xiàng)
- postcss.config.js等
其中l(wèi)egao.config.js影響開(kāi)發(fā)環(huán)境最終拉取組件形式:
配置說(shuō)明:
- env:組件及渲染server的接口請(qǐng)求環(huán)境
- components:當(dāng)前需要開(kāi)發(fā)調(diào)試或者代理的組件名稱
- devMode:為開(kāi)發(fā)提供兩種調(diào)試模式:
1)dev:本地開(kāi)發(fā)模式
組件默認(rèn)走本地資源,相當(dāng)于import xxx的形式,這種方式主要是方便開(kāi)發(fā)環(huán)境的調(diào)試(更好的熱更新),開(kāi)發(fā)環(huán)境會(huì)做好組件的引用,標(biāo)記,獲取實(shí)例等工作。
server端讀取組件目錄mock/mockData.json的配置數(shù)據(jù),作為組件的屬性傳入并渲染組件。
2)proxy:代理模式
在本地能夠代理和調(diào)試線上頁(yè)面內(nèi)的對(duì)應(yīng)組件,server端會(huì)請(qǐng)求線上頁(yè)面的組件配置數(shù)據(jù),匹配替換頁(yè)面內(nèi)對(duì)應(yīng)組件的js模塊為本地模塊。
開(kāi)啟這種模式后,通過(guò)一些配置就能完全模擬生產(chǎn)環(huán)境。
步驟為:設(shè)置生產(chǎn)host→在legao.config.js中配置需要代理的組件名稱→用https協(xié)議打開(kāi)生產(chǎn)環(huán)境的頁(yè)面地址→完成。
1.3 開(kāi)發(fā)環(huán)境其他配置項(xiàng)
開(kāi)發(fā)環(huán)境默認(rèn)設(shè)置了滿足當(dāng)前開(kāi)發(fā)和構(gòu)建需求的各種配置,如babel配置(plugins, preset等),postcss配置,也支持?jǐn)U展、修改這些構(gòu)建配置項(xiàng)。“構(gòu)建組件”的時(shí)候會(huì)讀取合并之后的配置項(xiàng)。
如postcss配置項(xiàng),目前默認(rèn)是采用postcss-px-to-viewport來(lái)處理組件的UI適配,可以根據(jù)需求修改或者增加其他處理配置項(xiàng)。(這里推薦用vw來(lái)做適配,比起rem,組件不依賴外部。像rem需要依賴根字體大小,但不同項(xiàng)目設(shè)置的計(jì)算比例是不一樣的,所以根字體大小無(wú)法保證統(tǒng)一,這樣不利于組件嵌入到其他項(xiàng)目(如sdk方式,npm包方式))。
?1.4 公共庫(kù)注入:@ctrip/easy
開(kāi)發(fā)一個(gè)再簡(jiǎn)單的頁(yè)面都需要面臨“水面下的冰山”,除了要實(shí)現(xiàn)核心邏輯,還需要實(shí)現(xiàn)其他必要功能。如“分享功能”,需要兼容app環(huán)境,微信h5環(huán)境,微信小程序嵌套環(huán)境,快應(yīng)用,甚至其他app環(huán)境等等的方法,還有跳轉(zhuǎn)、登陸等,這些往往比我們最終要做的頁(yè)面的核心業(yè)務(wù)還要復(fù)雜。于是樂(lè)高結(jié)合現(xiàn)實(shí)開(kāi)發(fā)需求,總結(jié)歸納了常用的方法,總結(jié)了通用庫(kù):@ctrip/easy。
?這個(gè)庫(kù)提供了封裝大部分情況分享的setShare方法,兼容大部分情況的jump方法,還有l(wèi)ogin,model等等。樂(lè)高框架會(huì)向組件“注入”這個(gè)幾乎每個(gè)項(xiàng)目開(kāi)發(fā)都需要的“業(yè)務(wù)方法”庫(kù),使得任何樂(lè)高組件在實(shí)時(shí)渲染環(huán)境都可以直接使用。有了這個(gè)“環(huán)境”,開(kāi)發(fā)者能夠集中精力快速開(kāi)發(fā)組件中的業(yè)務(wù)核心代碼。
當(dāng)然它也能單獨(dú)以npm形式引用。目前在公司內(nèi)有了很多接入,節(jié)省了開(kāi)發(fā)時(shí)間,提供了最佳實(shí)踐的方法。
二、組件開(kāi)發(fā)特殊函數(shù) - 為現(xiàn)實(shí)開(kāi)發(fā)需求設(shè)計(jì)
作為一個(gè)開(kāi)放性的平臺(tái),在現(xiàn)實(shí)開(kāi)發(fā)組件過(guò)程中,有一些比較特殊且必要的情況需要支持。
- 組件不是扁平羅列的結(jié)構(gòu),而是可以相互依賴和嵌套的(如父子組件,先后渲染依賴等)
- 需要有數(shù)據(jù)上的通信機(jī)制(數(shù)據(jù)依賴)
- 需要有組件級(jí)別的SSR為了滿足這些需求,樂(lè)高設(shè)計(jì)了一些簡(jiǎn)單有效的機(jī)制。會(huì)“預(yù)處理”一些能夠運(yùn)行在node端(中間層,頁(yè)面渲染之前)的靜態(tài)方法,渲染引擎會(huì)遍歷所有的組件,在拿到組件實(shí)例之后,判斷哪些組件是否含有這些靜態(tài)方法,然后通過(guò)執(zhí)行這些靜態(tài)方法,把影響后續(xù)組件渲染的所有數(shù)據(jù)處理好。方法如下:
2.1 beforeRender靜態(tài)方法
作用:組件嵌套、依賴,組織組件關(guān)系等
組件使用實(shí)例:
如上圖,這是一個(gè)tab切換類組件,它的作用是切換其他任意的組件,它需要在beforeRender里面聲明好頁(yè)面上的哪些組件是它的子組件。為了處理這種父子依賴關(guān)系的組件,渲染引擎會(huì)利用這個(gè)函數(shù)在渲染之前就把所有父組件和子組件分開(kāi),放在兩個(gè)數(shù)組中,并把最終有依賴關(guān)系的數(shù)據(jù)結(jié)構(gòu)傳給頁(yè)面渲染的部分,進(jìn)行后續(xù)渲染。
說(shuō)明:
第一個(gè)變量componentData,為該組件的配置數(shù)據(jù),渲染引擎根據(jù)componentData下配置的關(guān)聯(lián)的“子組件id”等數(shù)據(jù)知道“當(dāng)前頁(yè)面”上具體哪些組件是這個(gè)組件的子組件或者父組件,第二個(gè)變量為一個(gè)對(duì)象,對(duì)象包含幾個(gè)框架提供的方法,如:
分別對(duì)應(yīng)下圖中的幾種情況:
2.2 asyncData靜態(tài)方法
作用:可以作為組件的服務(wù)端,中間層(node環(huán)境)
說(shuō)明:主要用于在服務(wù)端請(qǐng)求接口獲取數(shù)據(jù),需返回一個(gè)promise,最終執(zhí)行結(jié)果數(shù)據(jù),會(huì)在中間層賦值給當(dāng)前組件的一個(gè)屬性:__FETCHED_DATA__。在服務(wù)端渲染的時(shí)候,組件第一時(shí)間可以拿到請(qǐng)求之后的數(shù)據(jù),所以可以更充足、更細(xì)致的“ssr”。組件內(nèi)部則可以根據(jù)“這個(gè)變量”來(lái)選擇是服務(wù)端渲染還是客戶端重新請(qǐng)求數(shù)據(jù)去渲染。
使用demo:
?這個(gè)函數(shù)的作用還有以下幾個(gè)其他的方面:
1)因?yàn)檫@個(gè)方法的執(zhí)行環(huán)境是node,所以可以“直連”調(diào)用node接口和方法。不過(guò)這里的require是作用在node環(huán)境的,其實(shí)并不需要解析,例如require(“ctriputil”),于是可以通過(guò)配置rule,利用string-replace-loader自動(dòng)將require替換為_(kāi)_non_webpack_require__,來(lái)防止webpack處理require的資源,例如:
?2)req可以掛載單次請(qǐng)求級(jí)別的全局變量,例如一個(gè)頁(yè)面上有多個(gè)同一類型組件,每個(gè)都需要在node環(huán)境獲取用戶信息,那這個(gè)信息就可以掛載在req,防止同種組件重復(fù)請(qǐng)求。
3)allComponents參數(shù):可以在此基礎(chǔ)上自定義邏輯,如修改其他組件的property控制它的具體渲染。
4)如有seo相關(guān)需求,可在asyncData resolve數(shù)據(jù)中返回自定義seo數(shù)據(jù)(框架會(huì)提前處理好動(dòng)態(tài)meta等seo相關(guān)數(shù)據(jù))。
2.3 provideData靜態(tài)變量
作用:狀態(tài)管理,公共變量,組件通信
說(shuō)明:組件維度將某個(gè)變量注冊(cè)到全局store,可以提供給其他組件使用(訂閱數(shù)據(jù))。如下圖所示,一個(gè)頁(yè)面上有一個(gè)“定位組件”和幾個(gè)依賴定位組件的組件,這些組件不僅需要在渲染時(shí)候能夠響應(yīng)定位組件分發(fā)的定位數(shù)據(jù),還需要再定位組件“點(diǎn)選城市”之后能夠響應(yīng)數(shù)據(jù)的變化,從而刷新渲染。
實(shí)現(xiàn)是基于mobx,樂(lè)高渲染引擎會(huì)先在中間層通過(guò)組件上的provideData靜態(tài)變量搜集所有需要注冊(cè)到store的數(shù)據(jù),然后在頁(yè)面渲染獲取組件module的時(shí)候,用mobx-react的observer包裹,使這個(gè)組件變成“reactive”的,最后將這些數(shù)據(jù)的宿主對(duì)象extraProps掛載在組件上,用來(lái)獲取數(shù)據(jù),和改變?nèi)謹(jǐn)?shù)據(jù)。如props.extraProps.geoInfos等。使用demo,數(shù)據(jù)提供方:
數(shù)據(jù)使用方:
?也可以放在如useEffect中監(jiān)聽(tīng)(類組件可通過(guò)在componentDidUpdate等生命周期中監(jiān)聽(tīng))props.extraProp上的具體某個(gè)字段,如定位信息geoInfo。
?那么所有監(jiān)聽(tīng)了extraProps.geoInfo的組件都會(huì)在“定位組件”分發(fā)了定位信息“ geoInfo ”之后觸發(fā)自身的渲染更新。
三、“在線依賴組件”探索 - 在線公共組件
很多機(jī)制的思考、開(kāi)發(fā)動(dòng)機(jī)都是來(lái)自于現(xiàn)實(shí)問(wèn)題,開(kāi)發(fā)研究也是不斷提取這些問(wèn)題的本質(zhì),抽象化,并把解決方案具體化,在線公共組件也是在實(shí)際開(kāi)發(fā)問(wèn)題中提煉出來(lái)的。
考慮這樣一種情形,一個(gè)產(chǎn)品組件A,經(jīng)過(guò)不斷的迭代擴(kuò)展,有了十?dāng)?shù)種樣式,代碼實(shí)現(xiàn)很簡(jiǎn)單,先抽取一個(gè)本地依賴的子組件如SingleProduct,然后通過(guò)產(chǎn)品組件A向SingleProduct傳入需要渲染的模板類型type字段等,最終在SingleProduct里面進(jìn)行區(qū)分和渲染不同的UI。
?然后,需求來(lái)了,我們又要新增一些業(yè)務(wù)邏輯完全不同的產(chǎn)品組件B,C,D等等,并希望能夠完全復(fù)用組件A的樣式和業(yè)務(wù)邏輯(跳轉(zhuǎn),埋點(diǎn)等),甚至是為B,C等組件新增的UI,也希望能夠在A組件里面能夠復(fù)用。
這個(gè)時(shí)候一般做法是直接復(fù)制組件A的代碼到組件B本地,或者破釜沉舟,將組件A和組件B等通用的樣式抽取成為UI組件,后者能解決一部分模板復(fù)用問(wèn)題,但是事實(shí)情況是如果需要修改的UI組件的一丁點(diǎn)代碼,都需要將所有依賴它的組件A,B,C,D等等分別打包,發(fā)布,測(cè)試,上線,這無(wú)疑增加許多維護(hù)成本。而且“代碼復(fù)制,搬家的方式”從開(kāi)發(fā)角度來(lái)看,存在代碼同步的問(wèn)題,維護(hù)起來(lái)非常困難。另外,組件A,B,C等等每一個(gè)組件都打進(jìn)來(lái)了需要復(fù)用的UI組件的所有資源。如果都用在同一個(gè)頁(yè)面上,就等于重復(fù)代碼一大堆,這又肯定增大了總體資源的大小。
?再比如:平臺(tái)已經(jīng)有視頻組件,運(yùn)營(yíng)同學(xué)可以根據(jù)需求,給運(yùn)營(yíng)頁(yè)面增加視頻組件,他們可以自行配置視頻地址,封面等。這種視頻組件經(jīng)過(guò)了長(zhǎng)期迭代,已經(jīng)是一種比較成熟的視頻組件了。如果新增的一種產(chǎn)品業(yè)務(wù)模塊,正好需要實(shí)現(xiàn)播放視頻的效果,那是自己copy代碼,重新實(shí)現(xiàn)一遍,還是直接復(fù)用之前開(kāi)發(fā)的視頻組件呢?最方便的做法是希望能夠動(dòng)態(tài)復(fù)用視頻組件,即,在“產(chǎn)品組件”需要視頻組件的時(shí)候才會(huì)拉取視頻組件,不需要的時(shí)候,代碼中是不會(huì)有視頻組件的資源的。
我們第一時(shí)間會(huì)想到走npm包的方式import引入,這是一種方式,但是這種要求我們引用的npm包的版本是最新的、沒(méi)有問(wèn)題的版本。首先那么多的組件是否有精力都去維護(hù)npm包是一個(gè)問(wèn)題(因?yàn)橹饕氖褂梅绞绞恰皹?lè)高拉取CDN組件”,一般不需要打npm包,而且組件普遍功能迭代很快),其次npm引入的資源被直接打在了你的組件包里面。
我們都知道package.json里面的main,module,browser等的作用,它為了使最終包能夠根據(jù)不同的環(huán)境打出相對(duì)純凈的包,設(shè)計(jì)了區(qū)分不同環(huán)境的標(biāo)志,并通過(guò)構(gòu)建工具按需打出最終包。
所以,希望能夠考慮一種比較好的方案,既方便新增,維護(hù),使用,又能夠獨(dú)立,減輕頁(yè)面資源大小。
事實(shí)上,通過(guò)這個(gè)案例,可以思考更多的類似使用場(chǎng)景。試想,如果一個(gè)部門的所有的公共組件資源能夠以一種在線引用的方式維護(hù)在CDN上面(云端),以供大家使用,這是不是一種非常方便復(fù)用公共組件的方式,同時(shí)非常方便維護(hù)更新。這是不是能夠大大促進(jìn)“組件化”開(kāi)發(fā)的良性循環(huán)呢?
如下圖:
?樂(lè)高組件眾多,300+組件,有豐富的組件資源,應(yīng)該是比較適合這種的一種場(chǎng)景。
接上文產(chǎn)品組件的問(wèn)題,現(xiàn)在樂(lè)高有了一種這種“云”依賴組件的機(jī)制,我們可以開(kāi)發(fā)“UI原子組件”,這種組件就是純靜態(tài)的UI組件,也是普通的樂(lè)高組件,支持跟其他“樂(lè)高組件”一樣的各種機(jī)制,擁有一樣的數(shù)據(jù)結(jié)構(gòu)。
那么現(xiàn)在我們可以在產(chǎn)品組件A,B等里面聲明依賴“UI原子組件”,然后傳入“原子組件”所需要的所有的業(yè)務(wù)字段,渲染類型字段等,這樣就可以使所有的產(chǎn)品組件(A,B,C…)使用到“UI原子組件”的樣式,這個(gè)組件能夠不斷迭代優(yōu)化下去。而需要修改、維護(hù)UI的時(shí)候,基本只需要發(fā)布“UI原子組件”這一個(gè)組件就好了。
?樂(lè)高組件可以通過(guò)fnGetMetaComponent在任意組件里面動(dòng)態(tài)的拉取線上的其他組件并使用。
此方法會(huì)掛載在組件props上,“在線引用”其他樂(lè)高組件,并覆蓋其渲染props(如id,property等)。產(chǎn)品列表中需要新增視頻展示,則產(chǎn)品組件可以關(guān)聯(lián)引用“視頻組件”,并將列表項(xiàng)的數(shù)據(jù)(如視頻地址)傳給視頻組件,用作渲染。
真實(shí)使用情形有兩種:
情況1:通過(guò)已保存的組件的唯一id拉取。
這種在樂(lè)高平臺(tái)的情況是:依賴的組件已配置到頁(yè)面(已保存,并生成了id)的組件。
其他組件可以用fnGetMetaComponent通過(guò)id依賴該線上組件,這種的使用場(chǎng)景下,被依賴的組件能夠讀取兩部分配置數(shù)據(jù),一部分來(lái)自運(yùn)營(yíng)通過(guò)樂(lè)高offline配置,一部分是來(lái)自開(kāi)發(fā)賦值,如:?
demo:
情況2:通過(guò)“組件類型名”拉取,支持服務(wù)端請(qǐng)求數(shù)據(jù):這種就相當(dāng)于import一個(gè)組件,但是沒(méi)有實(shí)際打包引入,而是通過(guò)在線資源引入。
demo:
?此外還可以提供插槽,如fnRenderProps方法:自定義內(nèi)容插槽,可在父組件中自定義內(nèi)部渲染,例如抽取AtomSwiper組件,只負(fù)責(zé)引入swiper和輪播,至于輪播的內(nèi)容(組件)則可以通過(guò)fnRenderProps定義。
實(shí)現(xiàn)的大概過(guò)程如下:
- 渲染引擎在node環(huán)境(頁(yè)面渲染之前)識(shí)別“需要依賴的”組件,在線拉取和解析,并處理其服務(wù)端請(qǐng)求。
在處理這些“原子組件”服務(wù)端請(qǐng)求的數(shù)據(jù)時(shí)需要通過(guò)renderBy這樣的“依賴它的組件id”來(lái)區(qū)分哪份數(shù)據(jù)掛載在哪個(gè)渲染運(yùn)行時(shí)下,因?yàn)樵咏M件需要考慮一份組件多次復(fù)用的情況。
fnGetMetaComponents則負(fù)責(zé)從所有的“在線組件”中查找,并覆蓋其渲染屬性,并返回最終需要渲染的組件。
目前樂(lè)高已經(jīng)有較多這種 “在線組件”的使用場(chǎng)景。例如上文說(shuō)到的產(chǎn)品組件,通過(guò)這種方式拆分為幾個(gè)組件:
- 產(chǎn)品組件(包含業(yè)務(wù)邏輯)
- 輪播組件、視頻組件
- 原子UI組件
如下圖:
?如此,三個(gè)組件都可以單獨(dú)復(fù)用,單獨(dú)按需拉取,單獨(dú)維護(hù)。
四、@ctrip/legao-nfes-sdk建設(shè):組件移植渲染
樂(lè)高組件建設(shè)希望開(kāi)發(fā)的組件,無(wú)論業(yè)務(wù)組件也好,通用組件,在線組件也好,能夠不局限于只在樂(lè)高平臺(tái)本身使用。
當(dāng)然組件已經(jīng)支持打包npm包了,為什么還需要sdk呢?
還是為了方便維護(hù)和使用。通過(guò)sdk能夠根據(jù)運(yùn)營(yíng)配置需求動(dòng)態(tài)拉取需要的組件,以及組件資源,并且這種對(duì)接方式對(duì)于接入方來(lái)說(shuō)簡(jiǎn)單,易用,更新任何組件都不需要重新發(fā)布。能夠做到“動(dòng)態(tài)資源,實(shí)時(shí)拉取,按需加載”,使“樂(lè)高組件”能夠無(wú)縫渲染到其他頁(yè)面。
如下圖:
嵌入案例:
?實(shí)現(xiàn)方式其實(shí)就是把樂(lè)高渲染引擎拆分成渲染sdk和webapi服務(wù),服務(wù)是基于公司的serverless開(kāi)發(fā)的webapi,扮演樂(lè)高渲染引擎的中間層服務(wù)的角色。
?目前這種對(duì)接方式已經(jīng)大量在各種對(duì)接三方頁(yè)面的場(chǎng)景中使用(如任務(wù)組件,選貨組件,星球號(hào),商城,會(huì)員,售賣等等),事實(shí)證明,這種輕量、友好的方式能夠推動(dòng)樂(lè)高組件的更多使用,也屬于樂(lè)高“開(kāi)放性建設(shè)”的一個(gè)方面。
五、動(dòng)態(tài)表單能力建設(shè)
樂(lè)高有大量的組件,每個(gè)組件都有不同的屬性配置面板,如果都去花費(fèi)人力開(kāi)發(fā)維護(hù),則無(wú)疑是一項(xiàng)巨大的工作,不利于組件的擴(kuò)展,于是我們基于自身需求,開(kāi)發(fā)了“動(dòng)態(tài)表單”。初期是希望能夠解決組件的配置問(wèn)題,但是后來(lái)建設(shè)中覺(jué)得這種表單其實(shí)有更廣闊的應(yīng)用場(chǎng)景,可以走出樂(lè)高,走向公司,為更多類似的表單場(chǎng)景提供配置能力,這也是樂(lè)高“開(kāi)放性”建設(shè)的思考的一個(gè)點(diǎn)。
動(dòng)態(tài)表單孵化于建設(shè)平臺(tái)過(guò)程中,是一種可視化在線配置動(dòng)態(tài)表單方案,專注于解決通用常規(guī)表單的可視化自由配置,目前能夠解決大部分的常規(guī)表單的在線配置場(chǎng)景,支持?jǐn)?shù)據(jù)聯(lián)動(dòng)、復(fù)雜數(shù)據(jù)嵌套、拖拽布局等。
已實(shí)現(xiàn)的動(dòng)態(tài)表單具有如下亮點(diǎn):
- 可視化:可視化搭建、修改和預(yù)覽表單。
- 可拖拽布局:控件可在畫(huà)布內(nèi)拖拽至任意坐標(biāo),以搭建最佳布局。
- 可擴(kuò)展:可二次開(kāi)發(fā),可擴(kuò)展控件集。
- 可聯(lián)動(dòng):某個(gè)控件可以控制別的控件的顯示和隱藏。
- 支持復(fù)雜數(shù)據(jù)類型:支持對(duì)象結(jié)構(gòu)以及對(duì)象數(shù)組結(jié)構(gòu)等復(fù)雜數(shù)據(jù)類型(JSON)的配置。
基本架構(gòu)模塊如下:
渲染介紹
系統(tǒng)有表單生成器編輯面板Form Generator,表單渲染入口Form Viewer兩個(gè)主要模塊。這兩個(gè)模塊共用常規(guī)的基礎(chǔ)組件如輸入框,顏色選擇等,還有一些基于業(yè)務(wù)擴(kuò)展的復(fù)雜組件,例如熱區(qū)選擇,視頻上傳,數(shù)據(jù)聚合(JSON列表)等。
?目前,動(dòng)態(tài)表單已經(jīng)大量使用在樂(lè)高的組件配置界面,如:
?當(dāng)然,樂(lè)高開(kāi)放性建設(shè)的最終目標(biāo)是,期望動(dòng)態(tài)表單能夠作為成熟的獨(dú)立的npm包,為其他表單場(chǎng)景提供公共功能,打造輕量“泛應(yīng)用”動(dòng)態(tài)表單。
六、其他方面
樂(lè)高系統(tǒng)的靈活,以“開(kāi)放性”建設(shè)為目標(biāo)還催生了一些其他方面的能力。
6.1 靜態(tài)頁(yè)面建設(shè)能力:DIY
樂(lè)高除了有已開(kāi)發(fā)好的組件外,還提供了一種可供運(yùn)營(yíng)同學(xué)配置靜態(tài)模塊的能力,稱作“DIY組件”。這部分可以讓運(yùn)營(yíng)同學(xué)自行拖拽配置自定義的模塊,例如配置不同形式的表單表單提交模塊,配置靜態(tài)頁(yè)面,最終以組件形式由樂(lè)高渲染。
?樂(lè)高開(kāi)放了這個(gè)界面的拖拽配置能力和接口服務(wù)能力,比如你有一個(gè)需要運(yùn)營(yíng)自己拖拽配置界面的需求,但是不需要樂(lè)高默認(rèn)的渲染,那你可以在這個(gè)場(chǎng)景下新增場(chǎng)景,并配置自定義字段。然后將上圖中的“可選字段組件”跟這些自定義字段進(jìn)行關(guān)聯(lián),使用方(也就是開(kāi)發(fā)同學(xué))通過(guò)接口獲取運(yùn)營(yíng)同學(xué)配置之后的數(shù)據(jù),就可以根據(jù)實(shí)際需求進(jìn)行渲染(可不依賴樂(lè)高渲染)。
6.2 產(chǎn)品樣式擴(kuò)展:產(chǎn)品畫(huà)布
樂(lè)高基于“DIY組件”擴(kuò)展了產(chǎn)品畫(huà)布的功能,之前提到的 “原子產(chǎn)品組件”配合產(chǎn)品畫(huà)布功能可以擴(kuò)展更多“自定義的樣式”。
?運(yùn)營(yíng)同學(xué)通過(guò)樂(lè)高的“DIY組件”,可自主拖拽配置產(chǎn)品UI界面,最后只需要把“產(chǎn)品畫(huà)布”的場(chǎng)景號(hào)配置到上述的“原子產(chǎn)品UI組件”的屬性中,即可實(shí)現(xiàn)自定義渲染界面,如圖:
6.3 npm倉(cāng)庫(kù)建設(shè)
樂(lè)高打包機(jī)制增加了構(gòu)建為npm包的配置,并打包到單獨(dú)的文件夾中(方便發(fā)布npm包)。
?腳手架在打包“樂(lè)高框架”所需要的umd包的同時(shí),也同時(shí)打出適用于npm方式引入的React組件包。發(fā)布了這些npm包(react組件)后,就能被使用方import引用。
另外樂(lè)高有很多一些交互復(fù)雜,并且效果不錯(cuò)的組件,如轉(zhuǎn)盤組件,九宮格,紅包雨等等,這些組件默認(rèn)是跟業(yè)務(wù)邏輯緊密相連的,有使用方想用這些交互,而里面的具體邏輯自己實(shí)現(xiàn),所以開(kāi)放一些成熟的靜態(tài)組件是有必要的。
樂(lè)高基于目前的組件庫(kù)在建設(shè)一個(gè)靜態(tài)組件庫(kù),基于storybook,根據(jù)“實(shí)際需求”去逐步開(kāi)放一些純靜態(tài)的組件,例如轉(zhuǎn)盤抽獎(jiǎng)等。
?另外,可以開(kāi)放一些上面提到的UI原子組件如(產(chǎn)品UI組件,定位組件等),這些組件是相對(duì)獨(dú)立,可復(fù)用的組件,可以嘗試在樂(lè)高之外的其他頁(yè)面上復(fù)用,走樂(lè)高sdk復(fù)用,或者npm包復(fù)用。
6.4 熱區(qū)能力建設(shè)
熱區(qū)能力最初是由配置banner而來(lái)。剛開(kāi)始為了復(fù)雜多變的banner配置,我們開(kāi)發(fā)了banner組件,多banner組件等,但是后來(lái)發(fā)現(xiàn),需求要配置比例大小不一的多banner,甚至是圖形不規(guī)則的復(fù)雜banner,這個(gè)時(shí)候再?gòu)呐渲蒙显黾幼侄物@然是不好的,配置復(fù)雜不說(shuō),圖片被切的支離破碎,從最終的渲染效果上有可能因?yàn)閳D片較大,網(wǎng)絡(luò)較差,看起來(lái)也是支離破碎的。
從運(yùn)營(yíng)同學(xué)的角度看,這簡(jiǎn)直是折磨,要切一大堆的圖,要配置一堆參數(shù),到后來(lái)由于要配置極度不規(guī)則的圖形(例如地圖那種banner)直接沒(méi)有了辦法,這才催生了我們開(kāi)發(fā)熱區(qū)banner。
?樂(lè)高支持的熱區(qū)可以支持任意圖形的熱區(qū)(通過(guò)多個(gè)定位點(diǎn)連接起來(lái))。
?具體實(shí)現(xiàn)方案也經(jīng)過(guò)了一系列的改進(jìn),最開(kāi)始的基于html的map和area標(biāo)簽法,如:
?后來(lái)優(yōu)化為記錄點(diǎn)的坐標(biāo)以及圖片的寬高方法,通過(guò)點(diǎn)擊事件獲取到點(diǎn)擊時(shí)相對(duì)于圖片的坐標(biāo)。最終遍歷每個(gè)熱區(qū)的描點(diǎn)坐標(biāo)信息,判斷點(diǎn)擊坐標(biāo)是否在當(dāng)前熱區(qū)中(實(shí)現(xiàn)思路——射線法,從點(diǎn)發(fā)射一條射線,點(diǎn)在多邊形內(nèi)則會(huì)產(chǎn)生奇數(shù)個(gè)交點(diǎn),點(diǎn)在多邊形外則有偶數(shù)個(gè)交點(diǎn))。
熱區(qū)banner一上線就大量被使用,節(jié)省了運(yùn)營(yíng)同學(xué)配置時(shí)間,極大的提高了banner配置的效率,也優(yōu)化了這一塊的渲染效果。
既然熱區(qū)這么好用,應(yīng)該擴(kuò)展到別的方面,于是就有了熱區(qū)規(guī)則,熱區(qū)tab切換等。甚至是可以考慮將具體交互,如點(diǎn)擊跳轉(zhuǎn),點(diǎn)擊領(lǐng)券,點(diǎn)擊彈出等等封裝為事件組件,將UI和交互事件解耦。
從配置面板到熱區(qū)渲染這一套流程可以開(kāi)放出來(lái)給開(kāi)發(fā)同學(xué)使用,如上圖的banner,甚至是更復(fù)雜的拼接banner,即使是開(kāi)發(fā)來(lái)做,都需要耗費(fèi)大量精力去擺位置,切圖等。如果遇到復(fù)雜的banner,可以直接使用樂(lè)高的熱區(qū)banner配置。如果需要的只是熱區(qū)的配置和區(qū)域數(shù)據(jù),可以利用樂(lè)高開(kāi)發(fā)的“熱區(qū)采集工具“收集運(yùn)營(yíng)配置的熱區(qū)數(shù)據(jù),根據(jù)實(shí)際開(kāi)發(fā)需要處理渲染或邏輯。?
最后
上面提到的眾多的“開(kāi)放性”建設(shè),有成熟的,有在實(shí)踐嘗試中的,還有正在規(guī)劃和思考的,我們的目標(biāo)是立足當(dāng)下,維護(hù)好成熟的,努力建設(shè)還在實(shí)踐嘗試中的,然后不斷的思考優(yōu)秀的、可以更進(jìn)一步優(yōu)化的“點(diǎn)”。
開(kāi)放性建設(shè)是雙向促進(jìn)的,既能給開(kāi)發(fā)同學(xué)帶來(lái)方便和最佳實(shí)踐,同時(shí)也在反向推動(dòng)“樂(lè)高”平臺(tái)的優(yōu)化,給運(yùn)營(yíng)同學(xué)帶來(lái)諸多方便。
樂(lè)高的開(kāi)發(fā)是不斷的發(fā)現(xiàn)和總結(jié)來(lái)自于現(xiàn)實(shí)的問(wèn)題,提取問(wèn)題的本質(zhì),抽象化問(wèn)題,具象化解決方案,來(lái)逐步優(yōu)化和擴(kuò)展平臺(tái),這樣才能在有限的資源下推動(dòng)平臺(tái)創(chuàng)造更大的價(jià)值。