<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          搭建編輯器的可擴(kuò)展架構(gòu)探索和實(shí)踐

          共 9329字,需瀏覽 19分鐘

           ·

          2021-05-18 10:05

          背景

          大量的業(yè)務(wù)運(yùn)作離不開紛繁復(fù)雜的頁面制作,比如淘寶,無論是行業(yè)類目還是形態(tài)各異的導(dǎo)購營銷方式,背后都是需要制作大量的頁面支撐。對(duì)這些頁面制作的過程進(jìn)行抽象,于是有了“頁面搭投系統(tǒng)”。

          天馬搭建服務(wù)就是提供這種頁面搭建能力的服務(wù),然而僅提供實(shí)現(xiàn)搭建能力的API服務(wù)對(duì)于服務(wù)的接入方而言還是有不少的工作量,尤其是需要自行實(shí)現(xiàn)頁面搭建和數(shù)據(jù)配置這類復(fù)雜的前端交互邏輯,而這部分前端交互邏輯又幾乎趨同,于是將這部分邏輯抽離出來,單獨(dú)開發(fā)了搭建編輯器實(shí)現(xiàn)了一套統(tǒng)一的基礎(chǔ)的搭建交互邏輯,于是搭建編輯器1.0版本產(chǎn)生了。

          盡管搭建編輯器1.0能夠已經(jīng)能夠滿足大部分的頁面搭建需求,但是隨著業(yè)務(wù)的發(fā)展,不同的場(chǎng)景對(duì)頁面搭建產(chǎn)生了新的訴求。比如:

          1. 增加新能力:想要在頁面發(fā)布之前添加有一個(gè)審核流程
          2. 去除無關(guān)信息:釘釘場(chǎng)景下,物料的數(shù)據(jù)配置只需要上傳一張圖片,不想有電商屬性的配置干擾
          3. 僅想復(fù)用搭建編輯器的部分能力:自定義的頁面編輯,復(fù)用搭建編輯器的發(fā)布流程
          4. ...


          對(duì)于這些訴求,有以下的解決方案:

          1. 方案一:接入方不再使用搭建編輯器,自己重新開發(fā)一個(gè)新的搭建編輯器,實(shí)現(xiàn)自己期望的功能。
          2. 方案二:我們幫業(yè)務(wù)在搭建編輯器中實(shí)現(xiàn),在搭建編輯器中通過不同的分支語句實(shí)現(xiàn)不同場(chǎng)景的需求。
          3. 方案三:將搭建編輯器進(jìn)行細(xì)粒度的拆分,提供基礎(chǔ)組件供接入方按需使用

          方案一,存在重復(fù)建設(shè),而且當(dāng)搭建服務(wù)推出新功能或者做能力升級(jí)的時(shí)候,接入方需要重新開發(fā)UI交互才能做新功能的統(tǒng)一升級(jí)。
          方案二,雖然能夠?qū)崿F(xiàn),但是有了一次就會(huì)有第二次,會(huì)有越來越多的業(yè)務(wù)讓你幫助實(shí)現(xiàn)不同場(chǎng)景下的邏輯,最后搭建編輯器的代碼就都是分支語句,而且很難維護(hù)。

          if(場(chǎng)景1){
            xxxx
          }
          if(場(chǎng)景2){
            xxxx
          }
          if(場(chǎng)景3){
            xxxx
          }
          ...

          方案三,可行但價(jià)值不大。一方面增加了對(duì)細(xì)粒度組件的維護(hù)成本,另一方面接入方需要知道搭建編輯器中的數(shù)據(jù)流狀態(tài),自行將細(xì)粒度組件組合起來,增加了接入成本,再一方面只有部分業(yè)務(wù)對(duì)部分組件有定制需求,拆解出細(xì)粒度組件本質(zhì)上是為了讓部分業(yè)務(wù)定制。所以能不能探尋一種方案,在不拆解現(xiàn)有組件的情況下,讓業(yè)務(wù)接入方可以快速的定制。

          我們對(duì)這些訴求做了如下梳理:接入方想要使用搭建編輯器80%的能力,其中20%的能力想要自己做一下拓展定制,包括在指定的位置執(zhí)行渲染邏輯和使用搭建編輯器中的數(shù)據(jù)流。

          抽象一層就是,期望搭建編輯器能夠給定一個(gè)渲染組件的節(jié)點(diǎn)并支持訪問內(nèi)部的數(shù)據(jù)狀態(tài),而且可以按需獨(dú)立加載。

          于是,我們開始對(duì)下一代搭建編輯器進(jìn)行探索和實(shí)踐,期望實(shí)現(xiàn)以下兩種能力:
          (1)支持內(nèi)部組件替換
          (a)組件替換
          (b)屏蔽不需要的交互邏輯
          (2)共享業(yè)務(wù)組件內(nèi)部的數(shù)據(jù)狀態(tài)
          (a)獲取組件的內(nèi)部狀態(tài)
          (b)修改組件的內(nèi)部狀態(tài)
          **

          兩個(gè)思路

          可擴(kuò)展架構(gòu)

          所有接入方在使用搭建編輯器的時(shí)候?qū)拥暮蠖朔?wù)本質(zhì)上都是天馬提供的服務(wù),所以可以理解為同一套后端服務(wù)在不同業(yè)務(wù)場(chǎng)景下的前端交互實(shí)現(xiàn)。其本質(zhì)是讓搭建編輯器在不同場(chǎng)景下具備不同的能力,參考vscode的設(shè)計(jì),我們讓搭建編輯器具備擴(kuò)展的能力,讓使用方通過開發(fā)擴(kuò)展的方式來豐富搭建編輯器的能力,從而滿足不同場(chǎng)景下的需求,搭建編輯器則提供最核心的搭投能力。



          擴(kuò)展只能夠新增一些能力,如果對(duì)于現(xiàn)有的搭建編輯器能力有定制需求,期望可以通過替換內(nèi)部組件的方式進(jìn)行修改。我們可以將搭建編輯器視為一個(gè)組件的容器,其中的每一個(gè)子組件都在容器中進(jìn)行注冊(cè)

          {
            name:'組件名',
            component:'組件實(shí)例'
          }

          當(dāng)容器中組件的映射關(guān)系發(fā)生變化的時(shí)候就動(dòng)態(tài)渲染組件,這樣就可以通過全局注冊(cè)組件的方式來進(jìn)行組件替換。

          組件數(shù)據(jù)狀態(tài)共享

          Redux這樣的數(shù)據(jù)狀態(tài)管理庫已經(jīng)能夠在同一個(gè)組件的不同子組件之間跨級(jí)共享數(shù)據(jù)狀態(tài),但是兩個(gè)業(yè)務(wù)組件之間共享數(shù)據(jù)狀態(tài)則需要在這兩個(gè)業(yè)務(wù)組件之間加一個(gè)公共父組件,再通過Redux這樣的數(shù)據(jù)管理方案進(jìn)行數(shù)據(jù)狀態(tài)的共享,但是想要從業(yè)務(wù)組件A訪問當(dāng)業(yè)務(wù)組件B中的數(shù)據(jù)狀態(tài)就非常的難。

          如果業(yè)務(wù)組件A和業(yè)務(wù)組件B的數(shù)據(jù)狀態(tài)能夠分別維護(hù)在組件內(nèi)部,但是又可以通過某種方式相互訪問那就最好不過了。


          為了實(shí)現(xiàn)上述兩個(gè)思路,我們做了以下的實(shí)現(xiàn)。

          設(shè)計(jì)實(shí)現(xiàn)

          第一步:核心搭投流程梳理

          首先我們對(duì)核心搭投流程進(jìn)行梳理,確定搭建編輯器需要提供的核心能力,也就是搭建編輯器的內(nèi)核所具備的能力。

          定義一次搭投流程:新建頁面->選擇搭建類型->進(jìn)入搭建編輯器->頁面搭建->數(shù)據(jù)投放->發(fā)布 目前主要為模塊搭建為核心流程

          第二步:搭建編輯器層級(jí)設(shè)計(jì)

          按照搭建流程中使用的核心能力,我們將搭建編輯器設(shè)計(jì)成UI和Data兩部分。

          • UI:
            • Header:收斂頁面級(jí)別的操作:頁面設(shè)置和頁面發(fā)布。
            • Editor:頁面的核心編輯區(qū)域,包括頁面插件、搭建頁面預(yù)覽、模塊管理和數(shù)據(jù)配置
          • Data:
            • Model:全局的數(shù)據(jù)狀態(tài)管理

          第三步:Model層設(shè)計(jì)

          原來組件之間的數(shù)據(jù)狀態(tài)可以通過props傳遞進(jìn)行共享,例如,當(dāng)需要實(shí)現(xiàn)點(diǎn)擊模塊列表的添加模塊按鈕時(shí)彈出模塊中心,這時(shí)候需要在模塊列表和模塊中心的公共父組件中通過props將數(shù)據(jù)狀態(tài)傳遞給模塊中心和模塊列表來實(shí)現(xiàn)。

          // 模塊中心和模塊列表偽代碼
          // 模塊中心和模塊列表的公共父組件偽代碼
          import ModuleCenter from 'ModuleCenter';
          import ModuleList from 'ModuleList';
          import {useState} from 'react;
          function App() {
            const [moduleCenterVisible,setModuleCenterVisible] = useState(false);
            
            return (
              <>
               <ModuleCenter 
                moduleCenterVisible={moduleCenterVisible} 
              setModuleCenterVisible={setModuleCenterVisible}
             />
               <ModuleList setModuleCenterVisible={setModuleCenterVisible}/>
              </>
            )
          }

          由于搭建編輯設(shè)計(jì)之初并沒有考慮組件替換和數(shù)據(jù)狀態(tài)共享的能力,所以我如果想改變交互方式,能夠在Header中點(diǎn)擊打開模塊中心就成了一件難事。
          為了讓組件的數(shù)據(jù)狀態(tài)能跨區(qū)域在多個(gè)位置調(diào)用,我們需要對(duì)Model層進(jìn)行設(shè)計(jì),將原來復(fù)雜的依賴關(guān)系抽離出來統(tǒng)一管理,用一個(gè)全局的數(shù)據(jù)狀態(tài)進(jìn)行管理。


          之后,再多一個(gè)數(shù)據(jù)狀態(tài),只需要在全局的數(shù)據(jù)狀態(tài)管理器上進(jìn)行注冊(cè),其余組件就可以通過全局?jǐn)?shù)據(jù)狀態(tài)拿到這里值了。

          我們按照操作的類型將Model層劃分為四類:

          • 全局配置
          • 模塊操作
          • 頁面操作
          • 插件相關(guān)

          第四步:支持內(nèi)部組件替換

          下一代搭建編輯器核心還是能夠修改業(yè)務(wù)組件的內(nèi)部細(xì)節(jié), 很重要的一點(diǎn)就是組件替換。如果內(nèi)部組件可以被替換,這樣使用者只需要替換自己有定制訴求的那個(gè)組件,將開發(fā)整個(gè)業(yè)務(wù)組件的成本降低為只開發(fā)部分功能組件。比如,某個(gè)業(yè)務(wù)并不需要復(fù)雜的發(fā)布流程,僅需要一個(gè)發(fā)布審核,這時(shí)候替換掉發(fā)布流程是最快復(fù)用搭建編輯器的方式。

          原發(fā)布流程

          新增發(fā)布審核

          我們對(duì)可替換的組件進(jìn)行了接口規(guī)范約束:

          interface IInjectComponent {
            name: string// 被替換的組件的名稱,全局唯一
            component?: React.ComponentType | string// 替換的組件
          }

          用一個(gè)全局的Map來管理name到component的映射關(guān)系,component可以是npm包或者cdn的方式。提供一個(gè)組件注冊(cè)的方法registerComponent,用來修改component的映射關(guān)系

          function registerComponent(props:IInjectComponent|IInjectComponent[]{
            // 替換component的映射關(guān)系
          }

          實(shí)現(xiàn)組件替換的能力:
          (1)npm包方式加載的本地組件我們采用 React.createElement 的方式進(jìn)行渲染;
          (2)遠(yuǎn)程cdn加載的的組件,我們使用了@ice/stark-module ,它可以將UMD打包的組件進(jìn)行遠(yuǎn)程加載成一個(gè)微模塊。然后讓組件運(yùn)行時(shí)替換。

          此時(shí)支持內(nèi)部組件替換的方式基本完成,偽代碼的實(shí)現(xiàn)方式就是

          function InjectComponent(props{
            const {name,defaultComponent,...otherProps} = props;
            const component= getComponent(name) || defaultComponent;// 通過name找到Map上注冊(cè)的組件
            if(isRemote(component)) {
              return <MicroModule url={component} {...otherProps}/>;
            }else {
              return React.createElement(component,otherProps)
            }
          }



          //讓發(fā)布組件可替換,將發(fā)布組件用如下方式實(shí)現(xiàn)
          registerComponent({
            name:'publish-component',
            component:PublishComponent
          })
          //defaultComponent用來注冊(cè)默認(rèn)的組件
          <InjectComponent name="publish-component" defaultComponent={PublishComponent} {...defalutProps}/>
           
          // 替換發(fā)布組件
          registerComponent({
            name:'publish-component',
            component:'https://new-publish-component'
          })

          這樣一來既支持修改業(yè)務(wù)組件的局部邏輯也支持了擴(kuò)展的動(dòng)態(tài)按需加載。而且業(yè)務(wù)組件的任何子組件只需要通過 InjectComponent 進(jìn)行包裝就可以成為一個(gè)可被替換的組件。

          第五步:共享業(yè)務(wù)組件內(nèi)部的數(shù)據(jù)狀態(tài)

          由于接入方開發(fā)的擴(kuò)展組件不會(huì)打包到搭建編輯器內(nèi)部,這時(shí)候就需要有一種方法來獲取搭建編輯器內(nèi)部的狀態(tài)。這時(shí)候一個(gè)常見的想法就是預(yù)先設(shè)計(jì)好,需要使用的數(shù)據(jù)狀態(tài)通過props傳入,只要替換組件與原組件的props保持一致,就可以使用搭建編輯器傳入的數(shù)據(jù)狀態(tài)

          <InjectComponent name="publish-component" props1={props1} props2={props} {...otherProps}/>

          這種方式會(huì)有一個(gè)限制,只允許使用傳入的props,如果想要使用其他數(shù)據(jù)狀態(tài)就需要修改搭建編輯器的代碼,增加額外的props。

          如果業(yè)務(wù)組件的數(shù)據(jù)狀態(tài)是掛載在全局應(yīng)用的狀態(tài)中的,那么就可以全局共享業(yè)務(wù)組件中的數(shù)據(jù)狀態(tài)了。


          一個(gè)想法就是有一個(gè)狀態(tài)管理庫是一個(gè)單例模式,通過命名空間來管理數(shù)據(jù)狀態(tài),當(dāng)同時(shí)使用這個(gè)狀態(tài)管理庫的兩個(gè)組件在一個(gè)應(yīng)用中使用的時(shí)候就可以通過命名空間訪問到對(duì)應(yīng)的數(shù)據(jù)狀態(tài)。全局狀態(tài)管理庫只需要具備兩個(gè)方法registerModel,useModel,偽代碼表示:

          // 業(yè)務(wù)組件內(nèi)部的狀態(tài)管理
          import {registerModel} from 'golbalStore'// 全局狀態(tài)管理庫
          import {useState} from 'react'
          function ModleA(){
            // 也可以使用Redux,這里為了方便使用useState
            const [state1,setState1] = useState() 
            return {
              state1,setState1
            }
          }
          // 按照name進(jìn)行注冊(cè)到全局單例上
          export default registerModel(ModuleA,{name:'ComA-ModuleA'})
          // 在擴(kuò)展組件中獲取業(yè)務(wù)組件A中的數(shù)據(jù)狀態(tài)
          import {useModel} from 'golbalStore';

          function ExtCom({
            // 通過命名空間找到單例上的Model
            const comAModelA = useModel('ComA-ModuleA');
            // 訪問數(shù)據(jù)狀態(tài)
            console.log(comAModelA.state1)
            ...
          }

          我提供了一個(gè)類似Redux的狀態(tài)管理工具,將Model層注冊(cè)到全局的單例中。這樣,擴(kuò)展組件只需要通過這個(gè)單例就能夠快速訪問和修改數(shù)據(jù)狀態(tài)。

          第六步:實(shí)現(xiàn)類中間件的方式修改局部狀態(tài)

          有時(shí)候只是想修改組件的部分狀態(tài)并不需要替換掉整個(gè)組件,比如,一個(gè)搭建編輯器的一個(gè)Button文案想要從“發(fā)布”修改為“發(fā)布頁面”,其實(shí)只是修改文案,Button的點(diǎn)擊邏輯還是想保留,這時(shí)候組件替換需要重新實(shí)現(xiàn)一遍Button的點(diǎn)擊邏輯。

          // Button 的使用
          import Button from 'Button'

          function App(){
            return <Button text="發(fā)布"}/>
          }

          假如,Button的文案是通過props傳入的,那我們其實(shí)只需要一個(gè)類似中間件的能力,對(duì)傳入的props做中轉(zhuǎn)處理,返回我們想要的結(jié)果即可。

          搭建編輯器實(shí)現(xiàn)

          <Injectcomponent name="publish-button" component={Button} text="發(fā)布"/>

          在Injectcomponent內(nèi)部修改props

          function InjectComponent(props){
            const = {name,component,...otherProps} = props;
            // 調(diào)用中間件處理
            const newProps = FnModdileWay(otherProps)
            ...
            React.createElement(component,newProps)
          }

          再舉一個(gè)復(fù)雜的案例,對(duì)于想要新增一個(gè)發(fā)布節(jié)點(diǎn)的需求。將問題簡化一下就是有一個(gè)List中插入一個(gè)item的問題

          [a,b,c] => [a,d,b,c]

          此時(shí),我們需要獲取到原始傳入的List [a,b,c] 之后對(duì)這個(gè)List進(jìn)行操作,添加一個(gè) d 獲得新的List [a,d,b,c] ,然后消費(fèi)新的List。

          如果將發(fā)布流程中的節(jié)點(diǎn)抽象出來作為props傳入,然后有一個(gè)中間件函數(shù)能夠?qū)rops進(jìn)行修改,這樣就能夠滿足需求。我們將這類可以對(duì)props進(jìn)行操作的組件稱為擴(kuò)展點(diǎn)ExtensionPoint。

          首先我們需要一個(gè)中間件的注冊(cè)函數(shù)來告訴組件"傳給你的props需要先經(jīng)過中間件加工",并注冊(cè)中間件函數(shù)。register函數(shù)會(huì)將中間件函數(shù)放入一個(gè)隊(duì)列中。

          // 偽代碼實(shí)現(xiàn)

          // 獲取ComA-ModuleA的擴(kuò)展點(diǎn)注冊(cè)函數(shù),并采用泛型傳入props的定義
          const register = useExtensionPoint<ComAModuleAProps>('ComA-ModuleA');

          // 注冊(cè)使用

          useEffect(()=>{
            // 返回一個(gè)clean,用于組件卸載的時(shí)候清楚中間件和副作用
            const clean = register((props)=>// 注冊(cè)
              return {
                props,
                state1:newState1 // 修改新的state
              }
            })
            return () => clean();
          },[])


          我們將上面提到的InjectComponent進(jìn)行一輪改造

          function InjectComponent(props{
            const {name,...otherProps} = props;
            // 通過name找到Map上注冊(cè)的組件
            const component= getComponent(name);
            //通過name找到對(duì)應(yīng)的中間件處理函數(shù)隊(duì)列,依次執(zhí)行中間件函數(shù),會(huì)對(duì)otherprops進(jìn)行deepClone
            const extProps = useExtension(name,otherprops);
            
            if(isRemote(component)) {
              return <MicroModule url={component} {...extProps}/>;
            }else {
              return React.createElement(component,extProps)
            }
            
          }

          這樣就能方便修改內(nèi)部組件的props了,從而修改局部的狀態(tài),對(duì)于新增發(fā)布流程節(jié)點(diǎn)就是為props新增一個(gè)符合節(jié)點(diǎn)抽象規(guī)范的新節(jié)點(diǎn)。

          一種通用的將業(yè)務(wù)組件可擴(kuò)展化的方案

          至此搭建編輯器已經(jīng)修改為具備可擴(kuò)展能力的業(yè)務(wù)組件。而且這種改造方式十分簡單,可以被快速移植,只需要將一個(gè)應(yīng)用的狀態(tài)用一個(gè)全局的單例進(jìn)行管理,并將需要改造的組件修改為下面的方式:

          <InjectComponent name="組件名稱" defaultComponent={默認(rèn)組件} {...默認(rèn)的props}/>

          即可快速將一個(gè)現(xiàn)有的業(yè)務(wù)組件快速修改為一個(gè)具備擴(kuò)展能力的組件。

          未來

          目前的實(shí)現(xiàn)相當(dāng)于將現(xiàn)有的"橡皮" + "鉛筆" 組合成 "帶橡皮的鉛筆",雖然能夠達(dá)到想要的效果,但是還是存在一些問題:

          1、搭建編輯器會(huì)提供默認(rèn)的組件,即使進(jìn)行了組件替換,原組件還是打包在搭建編輯器內(nèi)部,增加了代碼體積。未來還是期望能夠按照擴(kuò)展的配置文件,按需打包組件。
          2、尚未形成像vs code 這樣的擴(kuò)展生態(tài)。需要建立統(tǒng)一的擴(kuò)展開發(fā)標(biāo)準(zhǔn)和擴(kuò)展開發(fā)腳手架,逐步建立搭建編輯器的擴(kuò)展生態(tài),方便對(duì)擴(kuò)展的治理和維護(hù)。
          3、豐富頁面搭建的能力。 頁面=頁面結(jié)構(gòu)(數(shù)據(jù)) 是產(chǎn)生一張頁面的基本范式,頁面與數(shù)據(jù)之間的接口是固定的,但產(chǎn)生頁面結(jié)構(gòu)的方式是靈活的,通過擴(kuò)展的方式,可以豐富頁面搭建的能力,在不同的場(chǎng)景下使用不同的搭建方式。 
          4、目前技術(shù)方案中采用的狀態(tài)管理是icestore,微模塊替換方案是icestack/module,所以還是期望能夠?qū)⑦@套方案集成到ice體系,一起開源出去。



          瀏覽 43
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本内射网站 | 日本中文字幕A√ | 亚洲国产精品欧美久久 | 国产极品在线播放 | 人人操人人射人人色 |