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