面試被問(wèn)到低代碼細(xì)節(jié)?聽(tīng)我這樣吹(含架構(gòu)和原理)(低代碼開(kāi)發(fā)面試)
面試被問(wèn)到低代碼細(xì)節(jié)?聽(tīng)我這樣吹(含架構(gòu)和原理)(低代碼開(kāi)發(fā)面試)
寫(xiě)在前面
一直以來(lái)我總是會(huì)刷到一些關(guān)于低代碼的文章,但都是零零散散的,根本記不住?,F(xiàn)在終于有時(shí)間在這里做個(gè)系統(tǒng)性的總結(jié)了
關(guān)于低代碼,想必大家都有所了解,就是用拖拉拽的方式來(lái)快速搭建某個(gè)頁(yè)面 || 表單 || 海報(bào) || 游戲等,但其實(shí)除了常見(jiàn)拖拉拽的方式以外,還可以用形如在線文檔的方式來(lái)生成頁(yè)面,這里分別貼個(gè)鏈接給大家體驗(yàn)一下:
- 拖拽型的低代碼
- 類(lèi)文檔型的低代碼
當(dāng)然了,本文主要講解的是拖拽型的低代碼,這里先簡(jiǎn)單截個(gè)圖瞅瞅:
順帶羅列下低碼平臺(tái)的一些優(yōu)缺點(diǎn):
優(yōu)點(diǎn):?
- 它的本質(zhì)是提效,提效的同時(shí)給了自由度(以較少的成本達(dá)到想要的結(jié)果)
- 它的優(yōu)勢(shì)是可視和快速
- 它的能力源自于物料(組件)的能力
缺點(diǎn):?
- 上手成本(我自己覺(jué)得門(mén)檻還是高了),除了海報(bào)大部分情況下還是要摸索很久的
- 前端框架日新月異,過(guò)一兩年來(lái)個(gè)改革,低碼平臺(tái)就得適配一遍,維護(hù)成本相當(dāng)大;又或者平臺(tái)自身升級(jí)了,你也不知道會(huì)對(duì)自己的項(xiàng)目產(chǎn)生什么影響,但是不跟著升級(jí)后面就沒(méi)法迭代;并且基本上你用了一個(gè)平臺(tái)的低代碼,要想切換到其他平臺(tái)那就得從 0 到 1
- 不好維護(hù)和迭代,換個(gè)同類(lèi)型組件都得考慮一下,如果換個(gè)人還得重新理解,還不好調(diào)試,不好定位問(wèn)題,也不好確定本次改動(dòng)對(duì)之前功能的影響
- 不好優(yōu)化(比如你現(xiàn)在想做性能優(yōu)化,完全沒(méi)有頭緒)
- 二次開(kāi)發(fā)不可逆,很難做到持續(xù)可視化(確實(shí),大部分情況是這樣?♀?)
- 隨便舉個(gè)反例,只要低代碼平臺(tái)沒(méi)有實(shí)現(xiàn)那就是缺點(diǎn)
- …
確實(shí),低代碼自身限制還是很多的,隨便提個(gè)改動(dòng),都可能牽一發(fā)動(dòng)全身。不過(guò)這里我們并不會(huì)去評(píng)判低碼平臺(tái)的好壞,而只是單純的分享一些低代碼中的核心思路和整個(gè)流程:包括但不限于模塊劃分、如何解耦和擴(kuò)展、畫(huà)布的實(shí)現(xiàn)方式等等。
最重要的事
最重要
如果讓我說(shuō)一個(gè)低代碼中最重要的事情,那就是方向。我們要知道低代碼是有它的適用場(chǎng)景的(交互簡(jiǎn)單 && 輕邏輯),比如:
- 海報(bào)(我覺(jué)得這個(gè)方向應(yīng)用的相當(dāng)好,0 邏輯 0 交互)
- H5 運(yùn)營(yíng)活動(dòng)頁(yè)(一次性的頁(yè)面也很適用,沒(méi)有維護(hù)的煩惱)
- 表單收集頁(yè)(問(wèn)卷調(diào)查也不錯(cuò),也是一次性)
- 中后臺(tái)頁(yè)面
- 2D 游戲(其實(shí)我覺(jué)得很多低代碼思想是從游戲引擎借鑒過(guò)來(lái)的,因?yàn)榇蠹铱赡鼙容^少觸碰,所以這里特地截了一個(gè)游戲開(kāi)發(fā)時(shí)候的圖,和低代碼一毛一樣)
單單只實(shí)現(xiàn)上面的任意一種場(chǎng)景就已經(jīng)要兼容很多東西了,所以想覆蓋所有情況幾乎是不可能的,而且現(xiàn)在也沒(méi)有一個(gè)統(tǒng)一的規(guī)范,都是各做各的,百花齊放。因此不要想著啥都做,得挑一個(gè)垂直方向發(fā)力,方向越具體,自動(dòng)化程度越高(比如組件拿來(lái)即用,無(wú)需修改),生產(chǎn)效率也更高,平臺(tái)也會(huì)更好用。
次重要
除了方向,次重要的事情就是簡(jiǎn)單(包括交互和邏輯)。為什么要簡(jiǎn)單呢?
- 因?yàn)橐坏?fù)雜就不可視了,本來(lái)拖拽的優(yōu)勢(shì)也沒(méi)有了
- 通常情況下,低碼平臺(tái)只實(shí)現(xiàn)了視圖可視化,并沒(méi)有實(shí)現(xiàn)邏輯可視化,邏輯一復(fù)雜還是得寫(xiě)代碼。根本原因是我們很難將邏輯進(jìn)行可視化,它不如寫(xiě)代碼來(lái)的干脆明了。當(dāng)然目前也有比較適合于邏輯可視化的場(chǎng)景(前提是邏輯比較固定),比如審批流和Scratch,也都特地截了個(gè)圖:
那如何才能做到簡(jiǎn)單呢?還是得朝著垂直方向發(fā)力,也就是會(huì)有點(diǎn)定制,要固化一些操作、約束一些行為。目前我還是覺(jué)得低代碼主要還是給非研發(fā)同學(xué)用的,所以要足夠簡(jiǎn)單。
基本實(shí)現(xiàn)
扯了這么多,現(xiàn)在讓我們趕緊步入正軌吧??v觀大部分低碼平臺(tái),主要都是由以下四部分組成(畫(huà)的有點(diǎn)簡(jiǎn)陋):
接下來(lái)我們會(huì)對(duì)每個(gè)部分都挑兩三個(gè)重點(diǎn)來(lái)講解一下。
1、協(xié)議
在講解每個(gè)模塊之前,我們先來(lái)說(shuō)一個(gè)東西,就是協(xié)議(聽(tīng)起來(lái)很高大上,其實(shí)就是規(guī)范,更樸素點(diǎn)叫做格式),它主要包括物料協(xié)議、平臺(tái)搭建協(xié)議和其他協(xié)議等等。為什么要先約定協(xié)議呢,因?yàn)檫@個(gè)東西貫穿低代碼的整條鏈路:
- 當(dāng)我們想擴(kuò)展物料時(shí),需要實(shí)現(xiàn)相應(yīng)的協(xié)議
- 當(dāng)我們把左側(cè)一個(gè)物料拖到中間畫(huà)布區(qū)時(shí),需要通過(guò)協(xié)議來(lái)通信和解析
- 當(dāng)我們選中畫(huà)布區(qū)域的組件時(shí),要想通過(guò)右邊設(shè)置面板進(jìn)行屬性設(shè)置時(shí),也需要通過(guò)協(xié)議來(lái)通信和解析
- 當(dāng)我們準(zhǔn)備預(yù)覽和發(fā)布代碼時(shí),也需要通過(guò)協(xié)議來(lái)生成代碼
協(xié)議是低碼平臺(tái)的基石,它的主要目的就是約束和擴(kuò)展,約定的好,事半功倍;約定不好,版版重構(gòu)。約定優(yōu)于配置說(shuō)的就是這個(gè)道理。如果維護(hù)到后期發(fā)現(xiàn)協(xié)議很難拓展了,那基本只能重來(lái)了,只不過(guò)你多了些經(jīng)驗(yàn)。協(xié)議本質(zhì)上就是一堆 interface(就是固定格式啦)。
2、物料區(qū)
先來(lái)看看最左側(cè)的物料區(qū)吧,這是低代碼的起點(diǎn),協(xié)議也是從這邊開(kāi)始的,先約定個(gè)最簡(jiǎn)單的物料協(xié)議吧:
/** 單個(gè)物料約定 */interface IComponent { /** 組件名 */ componentName: string; /** 組件中文名稱(chēng) */ title: string; /** 縮略圖 */ icon?: string; /** 包地址 */ npm: { /** 源碼組件名稱(chēng) */ componentName?: string; /** 源碼組件庫(kù)名 */ package: string; /** 源碼組件版本號(hào) */ version?: string; }; /** 分類(lèi):比如基礎(chǔ)組件、容器組件、自定義組件 */ group?: string; /** 組件入?yún)⒒蛘哒f(shuō)是可配置參數(shù) */ props?: { name: string, propType: string, description: string, defaultValue: any, }[]; /** 其他擴(kuò)展協(xié)議 */ [key: string]: any;}// 舉個(gè)例子const componentList = [ { componentName: "Message", title: "Message", icon: "", group: "基礎(chǔ)組件", npm: { // import { Message } from @alifd/next 的意思 exportName: "Message", package: "@alifd/next", version: "1.19.18", main: "src/index.js", destructuring: true, }, props: [{ name: "title", propType: "string", description: "標(biāo)題", defaultValue: "標(biāo)題" }] }];
物料區(qū)其實(shí)沒(méi)啥功能,我們會(huì)有一個(gè) componentList,里面是各種組件的基本信息,直接循環(huán)渲染即可。同時(shí)還會(huì)順便生成一個(gè) componentMap,主要是方便后續(xù)我們通過(guò)組件名來(lái)快速獲取是組件的元信息,比如下面這樣:
const componentMap = { Message: { componentName: "Message", title: "Message", icon: "", group: "基礎(chǔ)組件", // ... },};// 通常情況,低碼平臺(tái)平臺(tái)還需要對(duì)外暴露出加載組件和注冊(cè)組件的方法,比如這樣:function createRegisterConfig() { const componentList = []; const componentMap = {}; return { componentList, componentMap, loadComponent: () => {}, register: (comp) => { componentList.push(comp); componentMap[comp.componentName] = comp; } }}
物料區(qū)本身并不復(fù)雜,這里就說(shuō)三個(gè)注意點(diǎn):
- 組件的分類(lèi):
- 容器組件(這類(lèi)組件主要用來(lái)協(xié)助布局和嵌套)
- 基本組件(常見(jiàn)的有圖片、文本、輸入框、視頻等)
- 集成度稍高一點(diǎn)的組件(表格、表單、圖表、自定義組件、業(yè)務(wù)組件)
- 如何加載組件?不管什么情況下,在前端加載文件只有兩種方式:
- 一個(gè)是 import()
- 一個(gè)是 <script>
- 這里也簡(jiǎn)單貼個(gè)示例代碼:
// 方法一:importconst name = 'Button' // 組件名稱(chēng)const component = await import('https://xxx.xxx/bundle.js')Vue.component(name, component)// 方法二:scriptfunction loadjs(url) { return new Promise((resolve, reject) => { const script = document.createElement('script') script.src = url script.onload = resolve script.onerror = reject })}const name = 'Button' // 組件名稱(chēng)await loadjs('https://xxx.xxx/bundle.js')// 這種方式加載組件,會(huì)直接將組件掛載在全局變量 window 下,所以 window[name] 取值后就是組件Vue.component(name, window[name])
- 如何擴(kuò)展自定義組件?
- 這是最為重要的功能之一,我們要想讓一個(gè)第三方組件在當(dāng)前平臺(tái)可用,直接拿來(lái)肯定是不行的,必須要進(jìn)行適配:
- 如果這個(gè)第三方組件是已有的,我們需要簡(jiǎn)單適配一下(適配器模式),也就是這個(gè)第三方組件需要實(shí)現(xiàn) IComponent 這個(gè)接口
- 如果這個(gè)第三方組件是待開(kāi)發(fā)的,那低碼平臺(tái)一般會(huì)提供腳手架讓使用方去基于一個(gè)固定的模板去開(kāi)發(fā),這樣協(xié)議自然也就對(duì)上了
- 通常情況下,低碼平臺(tái)自身會(huì)有個(gè)物料管理平臺(tái)對(duì)組件進(jìn)行統(tǒng)一的管理和操作,簡(jiǎn)單點(diǎn)做的話我們可以直接把開(kāi)發(fā)好的組件發(fā)到 npm 上,把 npm 當(dāng)做物料平臺(tái)用
- 但我們更期望的是物料統(tǒng)一,既然用了別人的平臺(tái),那就去用別人的組件,盡量去復(fù)用平臺(tái)自身已有的組件庫(kù)。我們不生產(chǎn)組件(或者少生產(chǎn)),只是消費(fèi)組件。
3、畫(huà)布區(qū)
這里我們先說(shuō)說(shuō)畫(huà)布區(qū)的幾種實(shí)現(xiàn)方式吧:
- 自由畫(huà)布:拖到哪里元素就放到哪里,配合容器組件可以協(xié)助布局以實(shí)現(xiàn)自適應(yīng)
- 流式布局:組件從左到右從上往下自然排列,比如 H5 就是單純的從上往下平鋪開(kāi)來(lái)
- 自動(dòng)布局:也是拖哪放哪,但是原來(lái)的地方如果已經(jīng)有組件則會(huì)被擠開(kāi),被擠開(kāi)的元素還會(huì)有個(gè)遞歸擠開(kāi)的過(guò)程
- 柵格畫(huà)布:類(lèi)似前端組件庫(kù)中的柵格布局,一行 24 列,一行 12 列這樣劃分填充,看起來(lái)比較規(guī)范也比較整齊
- 網(wǎng)格畫(huà)布:類(lèi)似棋盤(pán)一樣的網(wǎng)格,每個(gè)網(wǎng)格都是 8px(看 UI 規(guī)范),拖拽組件的時(shí)候會(huì)自動(dòng)吸附到這些網(wǎng)格線上,這樣可以讓整體看起來(lái)更加錯(cuò)落有致(如果你有一些設(shè)計(jì)規(guī)范,可能會(huì)需要用到這種畫(huà)布)
- 混合布局:效率和通用性的權(quán)衡
那畫(huà)布區(qū)是怎么渲染出來(lái)的呢?其實(shí)我們會(huì)有一個(gè) componentTree 來(lái)遞歸渲染拖拽出來(lái)的元素,然后各自按照組件類(lèi)型來(lái)渲染,先簡(jiǎn)單看下 componentTree 的大體結(jié)構(gòu)吧:
import ReactRenderer from '@alilc/lowcode-react-renderer';import reactDOM from 'react-dom';import { Button } from "@alifd/next";const componentTree = { // 畫(huà)布區(qū)的所有元素都在這里維護(hù) componentName: 'Page', // 因?yàn)槲覀兪且皂?yè)面為單位,所以頂層一定是 Page props: {}, children: [{ componentName: 'Button', props: { type: 'primary', size: "large", style: { color: '#2077ff' }, className: 'custom-button', }, children: '確定', }]};const conponents = { Button,};ReactDOM.render(( // 里面會(huì)遞歸渲染組件 <ReactRenderer componentTree={componentTree} components={components} /> ), document.getElementById('root'));// ================================================// 如果樹(shù)形結(jié)構(gòu)不好理解的話,我們可以把數(shù)據(jù)換成普通的數(shù)組來(lái)理解,然后渲染的過(guò)程就是循環(huán)遍歷數(shù)組即可,這樣就容易多了,比如 H5 頁(yè)面就很適合數(shù)組這種形式const componentTree = [{ "componentName": "ElButton", "height": 100, "props": {}, "style": {}}, { "componentName": "ElInput", "height": 300, "props": {}, "style": {}}];
事實(shí)上,低碼平臺(tái)都秉持著數(shù)據(jù)驅(qū)動(dòng)視圖的思想(和我們現(xiàn)在用的 vue 和 react 框架如出一轍),也是通過(guò)遞歸解析 componentTree 這個(gè)全局組件樹(shù)來(lái)動(dòng)態(tài)生成頁(yè)面,化簡(jiǎn)來(lái)說(shuō)就是:UI = Transformer(componentTree)。Transformer 這一步我們可以稱(chēng)之為轉(zhuǎn)換器、渲染器或者 render 函數(shù),通常開(kāi)發(fā)完成之后,這個(gè)渲染器是不用改的,我們只需要單純的修改數(shù)據(jù),渲染器自然會(huì)幫我們解析。
這個(gè)思想很重要,也是解耦的核心:就是我們所有的操作,不管拖拽也好,修改元素屬性也好,還是調(diào)整元素位置,都是對(duì) componentTree 這個(gè)數(shù)據(jù)進(jìn)行修改,單純的對(duì)數(shù)據(jù)進(jìn)行操作,比如追加元素,就往 componentTree 的 children 里面 push 一個(gè)元素即可;如果要修改一個(gè)元素的屬性值,只需要找到對(duì)應(yīng)元素的數(shù)據(jù)修改其 props 值即可。畫(huà)布編排的本質(zhì)就是操作組件節(jié)點(diǎn)和屬性。
另外為了讓每個(gè)組件都能直接獲取到這個(gè) componentTree,我們可以把這個(gè) componentTree 弄成全局的(全局?jǐn)?shù)據(jù)能夠讓整體流程更加清晰),比如放在 window、vuex、redux 上,這樣每個(gè)模塊就能共享同一份數(shù)據(jù),也能隨時(shí)隨地更改同一份數(shù)據(jù)(平臺(tái)會(huì)暴露公共的修改方法),而這個(gè)渲染器只是單純的根據(jù)這個(gè)數(shù)據(jù)來(lái)渲染,并不處理其他事情。
這里我們還要注意一個(gè)問(wèn)題,就是畫(huà)布區(qū)本身也是個(gè)組件,是個(gè)組件那它就會(huì)受到父元素和全局的影響,最簡(jiǎn)單的比如樣式,可能受到外部樣式作用,導(dǎo)致你這個(gè)畫(huà)布區(qū)和最終呈現(xiàn)的頁(yè)面可能有一丟丟的不同,所以要排除這些影響,那具體可以怎么做呢?就是把這個(gè)畫(huà)布區(qū)搞成一個(gè)獨(dú)立的 iframe,這樣環(huán)境就比較純了,完美隔離,只不過(guò)增加了通信的成本?,F(xiàn)在,物料區(qū)只負(fù)責(zé)渲染組件列表,以及觸發(fā)拖拽放下的事件,之后就是觸發(fā)修改全局的 componentTree,再之后就是觸發(fā)畫(huà)布區(qū)的重新渲染。這樣一來(lái),你就會(huì)發(fā)現(xiàn)畫(huà)布區(qū)和物料區(qū)就很好的解耦了。到目前為止畫(huà)布區(qū)只負(fù)責(zé)單純的渲染。
如果你還是想知道這個(gè)渲染器到底是怎么遞歸怎么渲染的,我這里也提供了段簡(jiǎn)單的代碼幫助你理解:
function renderNode(node) { if (!node) return null; const component = components[node.componentName]; const props = compute(node.props); const children = node.children.map(c => renderNode(c)); return React.render(component, props, children);}renderNode( componentTree);
4、屬性設(shè)置區(qū)
接下來(lái)我們簡(jiǎn)單講下右側(cè)的屬性設(shè)置區(qū),這個(gè)區(qū)域通常會(huì)支持三種基本的設(shè)置:props && 樣式 && 事件。一般來(lái)說(shuō)我們的操作是這樣的:
- 點(diǎn)選畫(huà)布區(qū)的某個(gè)組件
- 觸發(fā)設(shè)置某個(gè)全局變量為當(dāng)前組件
- 右側(cè)屬性面板就會(huì)根據(jù)當(dāng)前組件的 componentName 從 componentMap 中找到組件對(duì)應(yīng)的 setters(可配置項(xiàng)),這個(gè) setters 其實(shí)就是一開(kāi)始在物料協(xié)議里面約定的 props,但是和 props 可能有點(diǎn)小區(qū)別,需要自己手動(dòng)寫(xiě)個(gè)函數(shù)轉(zhuǎn)一下;或者可以直接在物料協(xié)議里面多添加一個(gè) setters 字段來(lái)專(zhuān)門(mén)描述有哪些屬性可以支持配置。兩種方式都是可以的
- 然后也是單純的循環(huán)渲染 setters
- 再把當(dāng)前組件的 state(初始值)賦值給 setters
上面的過(guò)程和我們平時(shí)開(kāi)發(fā)中后臺(tái)應(yīng)用的表單項(xiàng)(FormRender)是一毛一樣的,網(wǎng)上也有很多教程,這里就不細(xì)說(shuō)了。
我們主要來(lái)講一下屬性設(shè)置區(qū)的幾個(gè)注意點(diǎn):
- 首先如果我們修改了屬性設(shè)置區(qū)的表單項(xiàng),我們實(shí)際上是去修改全局的 componentTree,然后畫(huà)布區(qū)自然就會(huì)根據(jù)這個(gè)新的 componentTree 自動(dòng)渲染,有點(diǎn)單向數(shù)據(jù)流的意思(就是修改數(shù)據(jù)的入口只有一個(gè)),也方便排查問(wèn)題。把數(shù)據(jù)放到全局上,很多通信的過(guò)程就可以省掉了
- 一個(gè)常常提到的問(wèn)題就是如何實(shí)現(xiàn)聯(lián)動(dòng),比如字段 2 的顯隱依賴(lài)于字段 1 的值,類(lèi)似這種功能通常有兩種實(shí)現(xiàn)方式:
- 一種類(lèi)似發(fā)布訂閱,我們可以在字段 2 中監(jiān)聽(tīng)(on)來(lái)自字段 1 的 emit 事件,只是多了你就很難知道各自有哪些依賴(lài)關(guān)系了
- 另一種方式就是利用全局?jǐn)?shù)據(jù)了,比如我們把字段 1 和字段 2 都放在全局?jǐn)?shù)據(jù)中,然后在字段 2 中新增一個(gè) visible 的屬性設(shè)置器,其值是一個(gè)模板表達(dá)式,形如:{{ globalData.field1 && … && globalData.fieldN }},因?yàn)閿?shù)據(jù)是全局的所以很方便能夠直接獲取到,在實(shí)際渲染的過(guò)程中就會(huì)動(dòng)態(tài)執(zhí)行上面那個(gè)表達(dá)式來(lái)確定組件渲不渲染。此外,因?yàn)閿?shù)據(jù)是全局的,跨組件或者跨頁(yè)面共享數(shù)據(jù)也會(huì)變得輕而易舉
- 再一個(gè)常常提到的問(wèn)題就是如何處理點(diǎn)擊事件?如果做的開(kāi)放點(diǎn)、簡(jiǎn)單點(diǎn),我們可以直接讓用戶自己寫(xiě)函數(shù),然后運(yùn)行的時(shí)候用 eval 或者 new Function 執(zhí)行一下就行。但是這樣會(huì)有個(gè)問(wèn)題,就是安全性、穩(wěn)定性和效率不夠,所以我們需要進(jìn)行一些限制,這個(gè)通常有兩種方法:
- 一種是暴露固定方法,只接收參數(shù),比如我這個(gè)點(diǎn)擊的結(jié)果就是跳轉(zhuǎn)到某個(gè)頁(yè)面,也就是執(zhí)行 window.open 這個(gè)方法,那我們就不允許用戶直接書(shū)寫(xiě)這個(gè)代碼,而是先內(nèi)置一個(gè)全局封裝好的 jumpToPage(url) 方法,然后在屬性設(shè)置的時(shí)候只允許輸入 url 并進(jìn)行簡(jiǎn)單校驗(yàn)
- 但是固定方法是很難滿足我們的一些需求的,最終還是得支持讓用戶可以自己寫(xiě)腳本,于是乎我們就得讓這個(gè)腳本具有良好的隔離性,也就是沙箱或者對(duì)代碼進(jìn)行校驗(yàn)等,這里就簡(jiǎn)單說(shuō)一下沙箱的方式:
- with new Function(用 with 改變作用域?qū)崿F(xiàn)隔離、用 try catch 捕獲錯(cuò)誤保障穩(wěn)定性)
- iframe 沙箱:在一個(gè)空的 iframe 里面執(zhí)行這些未知的代碼可以最大程度的實(shí)現(xiàn)隔離,其余方式都可以通過(guò)原型鏈進(jìn)行逃逸(其實(shí) iframe 通過(guò) parent 也能逃逸)
- 等一個(gè)新的 API:ShadowRealm
下面是簡(jiǎn)單的代碼示例截圖,有個(gè)印象就行:
那沙箱里面能拿到什么數(shù)據(jù)呢?其實(shí)主要看我們想暴露什么參數(shù)給組件了,比如全局?jǐn)?shù)據(jù)、全局方法、父元素、當(dāng)前組件的 state 等等,那其實(shí)我們就可以直接傳個(gè)包含以上數(shù)據(jù)的大對(duì)象然后傳給 with 即可。
5、頂部操作區(qū)
上面那幾個(gè)組成部分其實(shí)已經(jīng)構(gòu)成了低代碼中最核心的幾個(gè)部分,我們可以稱(chēng)之為 Core(內(nèi)核)。對(duì)于頂部操作區(qū),通常可以有前進(jìn)、后退、清空等操作,但是這些操作并不算是必須的,它們通常以插件的形式存在,也方便大家一起維護(hù)和擴(kuò)展,這就是微內(nèi)核架構(gòu):
1 * Core N * plugins
關(guān)于這個(gè)架構(gòu),有興趣的可以參考這篇文章 微內(nèi)核架構(gòu)在前端的實(shí)現(xiàn)及其應(yīng)用,這篇文章講解的很清楚了。
然后我們這里就簡(jiǎn)單講下清空和回退操作吧(當(dāng)然其余其他操作也是一樣的道理):
- 先說(shuō)下清空,這個(gè)其實(shí)就很簡(jiǎn)單了,就是直接把全局的數(shù)據(jù)清了就行,畫(huà)布區(qū)會(huì)因?yàn)閿?shù)據(jù)變了而自動(dòng)重新渲染(注意:我們做的任何操作都是先去修改那個(gè)全局?jǐn)?shù)據(jù))。
- 再說(shuō)說(shuō)回退吧,對(duì)于大部分編輯器來(lái)說(shuō),這是一個(gè)很常見(jiàn)的功能。
- 簡(jiǎn)單做的話就是每次有操作時(shí),直接把整個(gè) componentTree 復(fù)制一份,撤銷(xiāo)和恢復(fù)都直接重新賦值整個(gè) componentTree 即可
- 另外一種方法就是基于操作來(lái)回退,源數(shù)據(jù)只有一份,但是有多個(gè) actions,比如我添加了一個(gè)元素,對(duì)應(yīng) add(comp) 方法,那我回退的時(shí)候就是執(zhí)行它的反向操作 remove(comp) 來(lái)修改,麻煩的地方就在于每個(gè)操作都需要寫(xiě)一個(gè)對(duì)應(yīng)的反向操作的方法
但其實(shí)插件不僅僅適用于頂部條:
- 物料區(qū)也可以有插件:主要表現(xiàn)就是用腳手架來(lái)擴(kuò)展物料
- 畫(huà)布區(qū)也可以有插件:比如選中組件的時(shí)候可以擴(kuò)展復(fù)制、刪除、輔助線、上移下移等功能
- 設(shè)置區(qū)也可以有插件:比如顏色選擇器,可以追加自定義表單項(xiàng)
6、代碼生成
到目前為止,我們就初步搭建好了頁(yè)面,它背后其實(shí)是一堆數(shù)據(jù),但是有可能你還是一頭霧水,沒(méi)關(guān)系,為了能讓大家有個(gè)更具象的認(rèn)識(shí),我把這份數(shù)據(jù)的最終形態(tài)簡(jiǎn)單匯總了下(放心,就一丟丟丟丟數(shù)據(jù)很好看懂的):
const json = { version: "1.0.0", // 當(dāng)前協(xié)議版本號(hào) componentList: [ { // 組件描述 componentName: "Button", package: "@alifd/next", version: "1.0.0", destructuring: true, }, { package: "@alifd/next", version: "1.3.2", componentName: "Page", destructuring: true, } ], state: { // 全局狀態(tài) name: "尤水就下" }, componentsTree: { // 畫(huà)布內(nèi)容 componentName: "Page", props: {}, children: [{ componentName: "Button", state: {}, props: { type: 'primary', size: "large", style: { color: 'red' }, className: 'custom-button', onClick: { // 事件綁定 type: "JSFunction", value: "function(e) { console.log(e.target.innerText) }" }, } }] }}
接下來(lái)就是要準(zhǔn)備發(fā)布了,具體該怎么做呢?這里說(shuō)下兩種主要的方式:
- 同技術(shù)棧:如果用的技術(shù)棧和低代碼平臺(tái)是一樣的話,我們可以使用運(yùn)行時(shí)渲染,開(kāi)發(fā)一個(gè) preview 的頁(yè)面,頁(yè)面里面有固定的渲染器(和畫(huà)布區(qū)有點(diǎn)像),也會(huì)加載對(duì)應(yīng)的組件庫(kù),剩下的就是遠(yuǎn)程獲取頁(yè)面的 json 數(shù)據(jù)(也就是上面那一坨代碼,當(dāng)然也可以稱(chēng)之為 schema),傳遞給渲染器,然后直接展示即可。不理解?那就再簡(jiǎn)單貼下代碼:
import React, { memo } from 'react';import ReactRenderer from '@alilc/lowcode-react-renderer';const SamplePreview = memo(() => { return ( // 至于渲染器怎么實(shí)現(xiàn)的,網(wǎng)上有一堆文章,不理解的同樣可以把數(shù)據(jù)當(dāng)成簡(jiǎn)單的數(shù)組,for 循環(huán)渲染而已 <ReactRenderer className="lowcode-plugin-sample-preview-content" schema={schema} components={components} /> );});// ps: 這里簡(jiǎn)單說(shuō)下 json 和 schema 的區(qū)別,以前我也很困惑,不知道現(xiàn)在理解對(duì)沒(méi)有(但我感覺(jué)大部分情況下都是隨便叫的)// json 是一個(gè)普通對(duì)象// schema 是一個(gè)有固定格式的普通對(duì)象// json schema 是一個(gè)描述 json 格式的普通對(duì)象
- 另一種就是直接導(dǎo)出項(xiàng)目,在此基礎(chǔ)上可以二次開(kāi)發(fā)或者嘗試 ssr 渲染(但是一旦二次開(kāi)發(fā)基本就是不可逆羅),就像下面這樣:
那兩種方式有什么區(qū)別呢:
前者 | 后者 | |
1 | 運(yùn)行時(shí)編譯(JIT) | 預(yù)編譯(AOT) |
2 | 最終輸出的是 json | 最終輸出的是項(xiàng)目 |
3 | 實(shí)時(shí)生效 | 得打包重新發(fā)版 |
4 | 一般這個(gè)夠用 | 有性能要求用這個(gè) |
為什么前者會(huì)有性能問(wèn)題呢?因?yàn)殇秩卷?yè)面的時(shí)候,需要將 json 進(jìn)行一層轉(zhuǎn)換,而后者是已經(jīng)打包之后的項(xiàng)目了。
其他問(wèn)題
- 模塊之間如何進(jìn)行解耦呢:
- 通過(guò)全局的發(fā)布訂閱模式來(lái)通信
- 通過(guò)全局?jǐn)?shù)據(jù)來(lái)共享,而不是通過(guò)直接相互傳遞數(shù)據(jù)
- 其實(shí)你把每個(gè)模塊都寫(xiě)成獨(dú)立的項(xiàng)目,開(kāi)發(fā)起來(lái)自然就會(huì)強(qiáng)迫自己解耦了
- 解耦最大的好處就是當(dāng)你要重構(gòu)或者替換某一個(gè)模塊時(shí),可以直接替換單個(gè)模塊而不影響其他地方的邏輯
- 除了全局變量,我能在組件里面維護(hù)自己的狀態(tài)嗎?當(dāng)然是可以的,我們可以新增一個(gè) state 屬性,來(lái)維護(hù)組件自身的一些狀態(tài),并且我們可以通過(guò)維護(hù)原型鏈的方式來(lái)層層向上獲取父元素?cái)?shù)據(jù),比如這樣:state.__proto__ = parent.state
- 怎么支持跨平臺(tái):低代碼平臺(tái)搭建好后最終得到的是一個(gè) json,而這個(gè) json 本身就是以一種通用的語(yǔ)言來(lái)描述頁(yè)面結(jié)構(gòu)、表現(xiàn)和行為的,它和平臺(tái)無(wú)關(guān)。要想跨平臺(tái)、跨框架,只需要做一個(gè)適配層去解析這些 json,導(dǎo)出成各平臺(tái)、各框架需要的樣子即可。舉個(gè)例子來(lái)說(shuō),我們?cè)诮馕龅倪^(guò)程中肯定會(huì)需要?jiǎng)?chuàng)建元素,而 vue 和 react 都有各自的 createElement 方法,那到時(shí)候只需要把創(chuàng)建元素的方法換成各框架各自的方法就行了。思路聽(tīng)起來(lái)很簡(jiǎn)單,但是每一種適配起來(lái)都很繁瑣
- 在哪里管理依賴(lài)呢:低碼平臺(tái)除了上面提到的模塊,一般還會(huì)有個(gè)物料管理平臺(tái),它不僅用來(lái)管理我們的組件,也會(huì)管理我們的項(xiàng)目依賴(lài),最終表現(xiàn)為 json 中的 packages 字段,就像下面這樣:
{"version": "1.0.0","packages": [{ // 依賴(lài)包 "title": "fusion 組件庫(kù)", "package": "@alifd/next", "version": "1.20.0", "urls": [ "https://alifd.alicdn.com/npm/@alifd/next/1.20.0/next.min.js", "https://alifd.alicdn.com/npm/@alifd/next/1.20.0/next.min.css" ], "library": "Next" // 最終這樣用:window.Next.componentName}],"components": []}
- 輔助線的實(shí)現(xiàn)(測(cè)距、對(duì)齊、吸附):需要遍歷所有物體拿到對(duì)應(yīng)的 x、y 值,只不過(guò)在遍歷較多組件的時(shí)候可以有些策略,比如是否在可視區(qū)、和目標(biāo)元素的距離、超過(guò)十條或者 5ms 就停止遍歷等等
- 版本維護(hù):低碼平臺(tái)通常會(huì)有 version 字段來(lái)維護(hù)每個(gè)迭代,并且會(huì)有好多種 version,包括但不限于:協(xié)議有版本號(hào)、依賴(lài)有版本號(hào)、組件有版本號(hào)、每次發(fā)布也有版本號(hào)
- 怎么調(diào)用接口:通常會(huì)有一個(gè)單獨(dú)的模塊來(lái)配置所有接口,然后再在右側(cè)的屬性設(shè)置面板中就挑選對(duì)應(yīng)的接口即可
- 如何監(jiān)控和埋點(diǎn):接個(gè)監(jiān)控和埋點(diǎn)庫(kù),并在全局暴露方法,方便用戶在右側(cè)屬性面板添加自定義事件
- 多人開(kāi)發(fā)同一個(gè)頁(yè)面如何解決沖突?假如我們有兩個(gè)人在編輯同一個(gè)頁(yè)面,先后點(diǎn)了保存該怎么處理?其實(shí)不管你怎么編輯,最終保存的就只有 json 數(shù)據(jù),那么我們就可以把問(wèn)題轉(zhuǎn)換為怎么解決兩個(gè)不同 json 的沖突,有兩種方式:
- 在項(xiàng)目生成之初,會(huì)建個(gè) gitlab 倉(cāng)庫(kù),借助 git 實(shí)現(xiàn)基于行的文本對(duì)比,和我們平時(shí)開(kāi)發(fā)一樣,只不過(guò)這里只有一個(gè) json 文件
- 兩個(gè) json 合并有沖突根本原因是某個(gè)字段或者某個(gè)屬性值變了,所以可以自行實(shí)現(xiàn)一套對(duì)象合并策略,把 json1 和 json2 的不同之處羅列出來(lái),讓用戶保存的時(shí)候手動(dòng)選擇要保留哪一個(gè)
- 怎么調(diào)試?如果你是前端或許看看控制臺(tái)報(bào)錯(cuò)還能知道是什么問(wèn)題,但如果你是非研發(fā)同學(xué),那就完全沒(méi)招了。對(duì)此,就需要低碼平臺(tái)開(kāi)發(fā)一個(gè)調(diào)試模塊,形如 vue 和 react 的 devtool 和 vConsole 調(diào)試面板,當(dāng)然這對(duì)非研發(fā)同學(xué)還是不夠友好,我們最好需要將具體的錯(cuò)誤和建議在頁(yè)面上醒目的展示出來(lái)
- D2C && AI :低碼平臺(tái)需要我們自己搭建頁(yè)面,D2C 則又省了一步,直接解析設(shè)計(jì)稿,經(jīng)過(guò)繁瑣的轉(zhuǎn)換后轉(zhuǎn)成了我們的 json,然后就直接生成了頁(yè)面??尚惺强尚?,現(xiàn)在也有很多這樣的產(chǎn)品,并且結(jié)合 AI 能夠?yàn)槠湓錾簧?。但是吧,我覺(jué)得這玩意幾乎維護(hù)不下去,不過(guò)畫(huà)餅需要
小結(jié)
如果你能看到這里,那說(shuō)..明…文章寫(xiě)的還行,哈哈哈嗝。這里我就簡(jiǎn)單的用幾句話對(duì)本篇文章進(jìn)行一個(gè)總結(jié):
- 低代碼最重要的就是:方向
- 低代碼的基石就是:協(xié)議
- 低代碼的最大的優(yōu)勢(shì):可視和高效
- 低代碼所有的操作都是在操作一個(gè) json
最后的最后,我又重新畫(huà)了張圖方便大家記憶:
好啦,本次分享就到這里,有什么問(wèn)題歡迎點(diǎn)贊評(píng)論留言,我們下期再見(jiàn),拜拜
作者:尤水就下
鏈接:https://juejin.cn/post/7276837017231835136