從實現(xiàn)原理談?wù)劦痛a(從實現(xiàn)原理談?wù)劦痛a的問題)
我們在低代碼領(lǐng)域探索了很多年,從2015 開始研發(fā)低代碼前端渲染(amis),從 2018 年開研發(fā)后端低代碼數(shù)據(jù)模型,發(fā)布了愛速搭低代碼平臺,這些年調(diào)研過了幾乎所有市面上的相關(guān)技術(shù)和產(chǎn)品,發(fā)現(xiàn)雖然每家產(chǎn)品細(xì)節(jié)都不太一樣,但在底層技術(shù)上卻只有少數(shù)幾種方案,因此我們認(rèn)為不同產(chǎn)品間的最大區(qū)別是實現(xiàn)原理,了解這些實現(xiàn)原理就能知道各個低代碼平臺的優(yōu)缺點(diǎn),所以本文將會介紹目前已知的各種低代碼實現(xiàn)方案,從實現(xiàn)原理角度看低代碼。
— 1 —
本文里的「低代碼」指的是什么?
在討論各個低代碼方案前,首先要明確「低代碼」究竟是什么?
這個問題不好直接回答,因為低代碼是非常寬泛的概念,有很多產(chǎn)品都聲稱自己的低代碼,但我們很容易反過來回答另一個問題:「什么是低代碼產(chǎn)品唯一不可缺少的功能?」
我認(rèn)為這個功能是可視化編輯,因為非可視化編輯就是代碼編輯,而只有代碼編輯的產(chǎn)品不會被認(rèn)為是低代碼,因此可視化編輯是低代碼的必要條件,低代碼其實還有另一個更清晰的叫法是可視化編程。
既然可視化編輯是低代碼的必要條件,那從實現(xiàn)角度看,實現(xiàn)可視化編輯有什么必要條件?
我認(rèn)為可視化編輯的必要條件是「聲明式」代碼,因為可視化編輯器只支持「聲明式」代碼。
解釋一下什么是「聲明式」,除了聲明式之外還有另一種代碼模式是「命令式」,我們分別舉兩個例子,如果想繪制一個紅色區(qū)塊,用「聲明式」來實現(xiàn),可以使用 HTML CSS,類似下面的方法:
<div style="background:red; height:50px"></div>
而換成用「命令式」來實現(xiàn),可以使用 Canvas API,類似下面的方法:
const ctx = canvas.getContext('2d');ctx.fillStyle = 'red';const rectangle = new Path2D();rectangle.rect(0, 0, 100, 100);ctx.fill(rectangle);
雖然最終展現(xiàn)效果是一樣的,但這兩種代碼在實現(xiàn)思路上有本質(zhì)區(qū)別:
- 「聲明式」直接描述最終效果,不關(guān)心如何實現(xiàn)。
- 「命令式」關(guān)注如何實現(xiàn),明確怎么一步步達(dá)到這個效果。
從可視化編輯器的角度看,它們的最大區(qū)別是:
- 「聲明式」可以直接從展現(xiàn)結(jié)果反向推導(dǎo)回源碼
- 「命令式」無法做到反向推導(dǎo)
反向推導(dǎo)是編輯器必備功能,比如編輯器里的常見操作是點(diǎn)選這個紅色區(qū)塊,然后修改它的顏色,在這兩種代碼中如何實現(xiàn)?
如果是「聲明式」的 HTML CSS,可以直接改 style 的 background 值,而基于 Canvas 的命令式代碼則無法實現(xiàn)這個功能,因為無法從展現(xiàn)找到實現(xiàn)它的代碼,命令式代碼實現(xiàn)同樣效果的可能路徑是無數(shù)的,除了前面的示例,下面這段代碼也可以實現(xiàn)一樣的效果:
const ctx = canvas.getContext('2d');ctx.beginPath();ctx.moveTo(0, 0);ctx.lineTo(50, 0);ctx.strokeStyle = '#ff0000';ctx.lineWidth = 100;ctx.stroke();
甚至有可能這個顏色是多個字符串加隨機(jī)數(shù)拼接而成,即便通過靜態(tài)分析也找不到來源,從而無法實現(xiàn)可視化修改。
「命令式」代碼無法實現(xiàn)可視化編輯,而可視化編輯是低代碼唯一不可少的功能,所以我們可以得到結(jié)論:所有低代碼平臺必然只能采用「聲明式」代碼,這也是為什么所有低代碼平臺都會有內(nèi)置的「DSL」。
既然低代碼都是聲明式,那我們可以通過分析其它「聲明式」語言來了解低代碼的優(yōu)缺點(diǎn),其實在專業(yè)研發(fā)里,聲明式語言在部分領(lǐng)域已經(jīng)是主流了:
- HTML CSS 是一種頁面展現(xiàn)的 DSL
- SQL 是一種數(shù)據(jù)查詢及處理的 DSL
- K8S 的 YAML 是一種服務(wù)部署的 DSL
- NGINX conf 是一種反向代理的 DSL
上面這些方案目前都是主流,但它們早期并不被看好,比如十幾年前還曾經(jīng)爭論過到底是用 B/S 還是 C/S 架構(gòu),CSS 2 的功能主要是面向圖文排版,并不適合用來構(gòu)建應(yīng)用界面。
SQL 最開始也不被看好,下面引用《硅谷簡史》這本書里的部分文字:
1970年,IBM研究員特德·科德(Ted Codd)發(fā)表了一篇里程碑式的論文,《大型數(shù)據(jù)庫的系統(tǒng)模型》,介紹了關(guān)系數(shù)據(jù)庫理論。
當(dāng)時大多數(shù)人認(rèn)為關(guān)系數(shù)據(jù)庫沒有商業(yè)價值,因其速度太慢,不能滿足大規(guī)模數(shù)據(jù)處理或者大量用戶存取數(shù)據(jù),雖然關(guān)系數(shù)據(jù)庫理論上很漂亮而且易于使用,但它的速度太慢。
上面兩段其實說的是 Oracle 的發(fā)家故事,可以看到當(dāng)時關(guān)系型數(shù)據(jù)庫并不被看好,因為大家都覺得慢,這點(diǎn)很好理解,數(shù)據(jù)庫在查詢前還得先解析 SQL語法、估算各種查詢的代價、生成執(zhí)行計劃,存儲也只能使用通用的數(shù)據(jù)結(jié)構(gòu),沒法根據(jù)不同業(yè)務(wù)進(jìn)行定制。
綜合來看這些「聲明式」語言有以下優(yōu)點(diǎn):
- 容易上手,因為描述的是結(jié)果,語法可以做得簡單,非研發(fā)也能快速上手 HTML 及 SQL。
- 支持可視化編輯,微軟的 HTML 可視化編輯 FrontPage 在 1995 年就有了,現(xiàn)在各種 BI 軟件可以認(rèn)為是 SQL 的可視化編輯。
- 容易優(yōu)化性能,無論是瀏覽器還是數(shù)據(jù)庫都在不斷優(yōu)化,比如可以自動改成并行執(zhí)行,這是命令式語言無法自動實現(xiàn)的。
- 容易移植,容易向下兼容,現(xiàn)在的瀏覽器能輕松渲染 30 年前的 HTML,而現(xiàn)在的編譯器沒法編譯 30 年前的瀏覽器引擎代碼。
而這些語言的缺點(diǎn)是:
1、只適合特定領(lǐng)域,命令式的語言比如 JavaScript 可以用在各種領(lǐng)域,但 HTML CSS 只適合渲染文檔及界面,SQL 只適合做查詢,所有這些語言都。
2、靈活性差,比如 SQL 雖然內(nèi)置了很多函數(shù),但想只靠它實現(xiàn)業(yè)務(wù)是遠(yuǎn)遠(yuǎn)不夠的,有些數(shù)據(jù)庫還提供了用戶自定義函數(shù)功能(UDF),通過代碼來擴(kuò)展。
3、調(diào)試?yán)щy,遇到問題時如缺乏工具會難以排查,如果你在Firefox出現(xiàn)前開發(fā)過頁面就會知道,由于IE6沒有開發(fā)工具,編寫復(fù)雜頁面體驗很差,遇到問題要看很久代碼才發(fā)現(xiàn)是某個標(biāo)簽沒閉合或者 CSS 類名寫錯了。
4、強(qiáng)依賴運(yùn)行環(huán)境,因為聲明式只描述結(jié)果而不關(guān)注實現(xiàn),因此強(qiáng)依賴運(yùn)行環(huán)境,但這也帶來了以下問題:
- 功能取決于運(yùn)行環(huán)境,比如瀏覽器對 CSS 的支持程度決定某個屬性是否有人用,雖然出現(xiàn)了CSS Houdini 提案,但 Firefox 和 Safari 都不支持,而且上手成本太高,預(yù)計以后也不會流行。
- 性能取決于運(yùn)行環(huán)境,比如同一個 SQL 在不同數(shù)據(jù)庫下性能有很大區(qū)別。
- 對使用者是黑盒,使用者難以知道最終實現(xiàn),就像很少人知道數(shù)據(jù)庫及瀏覽器的實現(xiàn)細(xì)節(jié),完全當(dāng)成黑盒來使用,一旦遇到性能問題就不知所措。
- 技術(shù)鎖定,因為即便是最開放的 HTML 也無法解決,很多年前許多網(wǎng)站只支持 IE,現(xiàn)在又變成了只支持 Chrome,微軟和 Opera 在掙扎了很多年后也干脆直接轉(zhuǎn)向用 Chromium。同樣的即便有 SQL 標(biāo)準(zhǔn),現(xiàn)在用的 Oracle/SQL Server 應(yīng)用也沒法輕松遷移到 Postgres/MySQL 上。低代碼行業(yè)未來也一樣,即便出了標(biāo)準(zhǔn)也解決不了鎖定問題,更有可能是像小程序標(biāo)準(zhǔn)那樣發(fā)展緩慢,功能遠(yuǎn)落后于微信。
因為低代碼就是一種聲明式編程,所以這些「聲明式」優(yōu)缺點(diǎn),其實就是低代碼的優(yōu)缺點(diǎn),了解聲明式的歷史及現(xiàn)狀就能更好理解低代碼,因為:
- 低代碼的各種優(yōu)點(diǎn)是「聲明式」所帶來的。
- 低代碼被質(zhì)疑的各種缺點(diǎn)也是「聲明式」所導(dǎo)致的。
— 2 —
低代碼的實現(xiàn)方案
說完了聲明式,我們就對低代碼有了全面認(rèn)識,接下來進(jìn)入正題,開始介紹已知的各種低代碼實現(xiàn)原理,將會分為前端和后端兩部分。
— 3 —
生成代碼的方案算不算低代碼?
在討論各種方案前,有一種方案比較特別,它雖然也有配置規(guī)范或 DSL,甚至有可視化編輯,但最終應(yīng)用運(yùn)行是通過生成代碼的方式實現(xiàn)的,不依賴依賴運(yùn)行環(huán)境。
這個方案最大的優(yōu)點(diǎn)是可以和專業(yè)開發(fā)整合,因此靈活性強(qiáng)、可以使用原有的開發(fā)流程,本質(zhì)上和專業(yè)開發(fā)一樣。
但也有如下缺點(diǎn):
- 強(qiáng)依賴研發(fā),無法做到給非研發(fā)使用,因為后續(xù)代碼需要編譯上線。
- 無法持續(xù)可視化編輯,因為代碼無法可視化編輯,生成代碼后只要有修改就沒法再反向還原成低代碼的形式,后續(xù)只能代碼編輯。
- 難以實現(xiàn)完全用低代碼開發(fā)應(yīng)用,因為不能生成太復(fù)雜的代碼,使得這種方案一般不包括交互行為,通常是只有前端界面支持可視化編輯。
- 無法做到向下兼容,因為生成的那一瞬間代碼依賴的框架版本就固定了,目前還沒見過哪款前后前端框架做過到完全向下兼容。
因此我認(rèn)為生成代碼的方案不算真正的低代碼,本質(zhì)上它還是一種開發(fā)輔助方式,一種高級點(diǎn)的腳手架工具,和大部分IDE的生成樣板代碼能力一樣,使用這種方案無法做到持續(xù)可視化開發(fā),我還沒見過有人將 HTML CSS 編譯成 C 代碼后二次開發(fā)。
— 4 —
前端代碼實現(xiàn)原理 – 界面渲染
前面提到前端 HTML CSS 可以看成一種描述界面的低代碼 DSL,因此前端界面實現(xiàn)低代碼會比較容易,只需要對 HTML CSS 進(jìn)行更進(jìn)一步封裝,這里以我們的開源項目 amis 為例進(jìn)行介紹。
amis 核心原理是將 JSON 轉(zhuǎn)成自研的 React 組件庫,然后使用 React 進(jìn)行渲染。
比如下面這段 JSON:
{ "type": "page", "title": "頁面標(biāo)題", "subTitle": "副標(biāo)題", "body": { "type": "form", "title": "用戶登錄", "body": [ { "type": "input-text", "name": "username", "label": "用戶名" } ] }}
可以理解 amis 原理就是轉(zhuǎn)成了下面這樣的 React 組件樹,最終由各個 React 組件庫渲染 HTML:
<Page title="頁面標(biāo)題" subTitle="副標(biāo)題"> <Form title="用戶登錄"> <InputText name="username" label="用戶名" /> </Form></Page>
雖然也有低代碼平臺直接使用 HTML CSS 來實現(xiàn)更靈活的界面控制,但這樣做會導(dǎo)致用起來復(fù)雜度高,因為通常需要多層嵌套 HTML 才能實現(xiàn)一個組件,使用者還必須熟悉 HTML 及 CSS,上手門檻過高,因此大部分低代碼平臺都是類似 amis 那樣使用 JSON 進(jìn)行簡化。
這里有個小問題,為什么大家?guī)缀跞际褂?json?我覺得有兩方面原因:
- 低代碼平臺編輯器幾乎都是基于 Web 實現(xiàn),JavaScript 可以方便操作 JSON。
- JSON 可以支持雙向編輯,它的讀取和寫入是一一對應(yīng)的。
第二點(diǎn)怎么理解?可以對比一下 YAML,它有引用功能,導(dǎo)致了不好實現(xiàn)雙向編輯,比如下面 YAML 示例:
paths: root_path: &root val: /path/to/root/ patha: &a root_path: *root
轉(zhuǎn)成了對應(yīng)的 JSON 數(shù)據(jù)后,就變成了:
{ "paths": { "root_path": { "val": "/path/to/root/" }, "patha": { "root_path": { "val": "/path/to/root/" } } }}
可以看到之前的引用關(guān)系沒了,而是復(fù)制出了一部分,如果直接基于這個數(shù)據(jù)進(jìn)行可視化編輯,編輯器在修改的時候就只會改一處,也沒法再還原成之前的 YAML 了,要想實現(xiàn) YAML 可視化編輯就不能先轉(zhuǎn)成 JSON,而是要對 YAML 解析后的樹形結(jié)構(gòu)進(jìn)行操作,前端界面實現(xiàn)成本很高,因此目前還沒見過 YAML 的可視化編輯器。
但 JSON 的優(yōu)點(diǎn)就是它的缺點(diǎn),因為它的用途是數(shù)據(jù)交換而不是人工編寫,導(dǎo)致基于 JSON 構(gòu)建 DSL 不方便編輯,會有以下 3 個問題:
- 不支持注釋
- 不支持多行字符串
- 語法過于嚴(yán)格,比如不支持單引號,不能在最后多寫一個逗號
其中我們對這個注釋問題進(jìn)行了特殊支持,開發(fā)了帶注釋的 JSON 解析,存儲的時候?qū)⒆⑨寖?nèi)嵌到一個特殊的字段中,在代碼顯示的時候?qū)⑺崛〕鰜碜兂勺⑨尅?/span>
另外許多低代碼平臺會將這個 JSON 配置隱藏,只提供界面編輯,但在 amis 可視化編輯器里提供了直接修改 JSON 的功能,因為對于熟悉的開發(fā)者,直接編寫 JSON 要比在屬性面板里找半天效率高,還可以直接將 amis 文檔中的示例粘貼進(jìn)來快速創(chuàng)建。
amis 開始編輯器里 JSON 編輯模式
前面提到聲明式容易向下兼容,amis 自己就是最好的例子,在 amis 誕生的 2015 年前端框架和現(xiàn)在有大量區(qū)別:
- Vue 還是 1,現(xiàn)在已經(jīng)到 3 了,不向下兼容。
- Angular 還是 1,現(xiàn)在已經(jīng) 13 了,不向下兼容。
- React 雖然整體用法沒變,但有大量細(xì)節(jié)不向下兼容,加上 hooks 推出后,許多第三方庫改成了 hooks 版本,導(dǎo)致舊的類組件形式?jīng)]法直接使用。
而 amis 早期的界面配置現(xiàn)在還能繼續(xù)使用,不受框架升級影響。
— 5 —
交互邏輯的實現(xiàn)
前面說到前端界面低代碼是比較容易,但交互及邏輯處理卻很難低代碼話,目前常見有三種方案:
- 使用圖形化編程
- 固化交互行為
- 使用 JavaScript
先說第一種圖形化編程,這是非常自然的想法,既然低代碼的關(guān)鍵是可視化,那直接使用圖形化的方式編程不就行了?
但我們發(fā)現(xiàn)這么做局限性很大,本質(zhì)的原因是「代碼無法可視化」,這點(diǎn)在 35 年前沒有銀彈的論文里就提到了。
為什么代碼無法可視化?首先想一想,可視化的前提條件是什么?
答案是需要具備空間形體特征,可視化只能用來展現(xiàn)二維及三維的物體,因為一維沒什么意義,四維及以上大部人無法理解,所以如果一個事物沒有形體特征,它就沒法被可視化。
舉個例子,下面是一段 amis中 代碼,作用是遍歷 JSON 并調(diào)用外部函數(shù)進(jìn)行處理:
function JSONTraverse(json, mapper) { Object.keys(json).forEach(key => { const value = json[key]; if (isPlainObject(value) || Array.isArray(value)) { JSONTraverse(value, mapper); } else { mapper(value, key, json); } });}
雖然只有 10 行代碼,卻包含了循環(huán)、調(diào)用函數(shù)、類型檢測、分支判斷、或操作符、遞歸調(diào)用、參數(shù)是函數(shù)這些抽象概念,這些概念在現(xiàn)實中都找不到形體的,你可以嘗試一下用圖形來表示這段代碼,然后給周圍人看看,我相信任何圖形化的嘗試都會比原本這段代碼更難懂,因為你需要先通過不同圖形來區(qū)分上面的各種概念,其他人得先熟悉這些圖形符號才能看懂,理解成本反而更高了。
代碼的這些抽象思維難以像積木一樣進(jìn)行拼接,積木拼接這種方式只適合用來實現(xiàn)簡單的邏輯,比如 Scratch。
Scratch
而前面圖形化是低代碼唯一不可少的功能,這就使得低代碼不適合做復(fù)雜的抽象邏輯處理,這是圖形化缺陷決定的,因此在復(fù)雜邏輯處理方面低代碼永遠(yuǎn)無法徹底取代專業(yè)代碼開發(fā)。
但如果是面向特定領(lǐng)域,低代碼平臺可以先將這個領(lǐng)域難以圖形化的算法預(yù)置好,讓使用者只需做簡單的處理,比如在 Blender 中將 PBR 算法封裝了,使用的時候只需要調(diào)整參數(shù)就行。
Blender 中的材質(zhì)節(jié)點(diǎn)編輯
如果真要用節(jié)點(diǎn)實現(xiàn)這個算法會非常復(fù)雜,大概長這樣:
在復(fù)雜邏輯下,圖形中的連線反而變成了視覺干擾,比如下面的例子:
來自 UE4 Blueprints From Hell 里的一張圖
想象一下假設(shè)客戶做出了上面這個圖的復(fù)雜邏輯,然后找你排查問題,而客戶的程序是部署在內(nèi)網(wǎng)的,沒法導(dǎo)出,只能通過微信拍屏幕給你看……
因此我認(rèn)為圖形化不適合用來實現(xiàn)業(yè)務(wù)邏輯,只適合用來做更高層次流程控制,比如審批流,審批流是現(xiàn)實真實存在的,沒有復(fù)雜的抽象邏輯,因此適合圖形化。
在愛速搭中,我們除了實現(xiàn)流程功能,還實現(xiàn)了樹形結(jié)構(gòu)的 API 編排功能,它本質(zhì)上是模仿代碼結(jié)構(gòu),將會在后面進(jìn)行介紹。
說完了圖形化編程,接下來談第二種方案:固化交互行為,這是不少低代碼平臺的做法,我們還是以 amis 為例進(jìn)行介紹。
amis 將常用的交互行為固化并做成了配置,比如彈框是下面的配置:
{ "label": "彈框", "type": "button", "actionType": "dialog", "dialog": { "title": "彈框", "body": "這是個簡單的彈框。" }}
除了彈框之外還有發(fā)起請求、打開鏈接、刷新其它組件等,使用固化交互行為有下面兩個優(yōu)點(diǎn):
- 可以可視化編輯
- 整合度高,比如彈框里可以繼續(xù)使用 amis 配置,通過嵌套實現(xiàn)復(fù)雜的交互邏輯
但這個方案最大的缺點(diǎn)是靈活性受限,只能使用 amis 內(nèi)置的行為。
要實現(xiàn)更靈活的控制,還是得支持第三個方案:JavaScript,目前有的低代碼平臺只在界面編輯提供可視化編輯,一旦涉及到交互就得寫 JavaScript,這和 30 年前的 C Builder 本質(zhì)上是一樣的:
RDA Studio 11 的界面編輯
但第三個方案的最大缺點(diǎn)就是無法可視化編輯,因此不算是低代碼。
— 6 —
后端低代碼的方案
前端討論完了,接下來是后端部分,后端低代碼需要解決以下三個問題:
1、如何自定義數(shù)據(jù)存儲?
低代碼平臺需要支持用戶存儲自定義數(shù)據(jù),因為每個應(yīng)用所需的字段是不一樣的。
自定義數(shù)據(jù)存儲是后端低代碼最重要的功能,使用什么方案將直接影響這個產(chǎn)品的適用范圍,目前我們已知有 5 種方案,每種都有自己的優(yōu)缺點(diǎn)。
存儲的實現(xiàn)方案 1:直接使用關(guān)系型數(shù)據(jù)庫
這個方案的原理是將數(shù)據(jù)模型的可視化操作轉(zhuǎn)成數(shù)據(jù)庫 DDL,比如添加了一個字段,系統(tǒng)會自動生成表結(jié)構(gòu)變更語句:
ALTER TABLE 'blog' ADD 'title' varchar(255) NULL;
這個方案的優(yōu)點(diǎn)是:
- 所有方案里唯一支持直連外部數(shù)據(jù)庫,可以對接已有系統(tǒng)。
- 性能高和靈活性強(qiáng),因為可以使用高級 SQL。
- 開發(fā)人員容易理解,因為和專業(yè)開發(fā)是一樣的。
但它的缺點(diǎn)是:
- 需要賬號有創(chuàng)建用戶及 DDL權(quán)限,如果有安全漏洞會造成嚴(yán)重后果,有些公司內(nèi)部線上帳號沒有這個權(quán)限,導(dǎo)致無法實現(xiàn)自動化變更。
- DDL 有很多問題無解,比如在有數(shù)據(jù)的情況下,就不能再添加一個沒有默認(rèn)值的非 NULL 字段。
- DDL 執(zhí)行時會影響線上性能,比如 MySQL 5.6 之前的版本在一個大數(shù)據(jù)量的表中添加索引字段會鎖整個表的寫入(但也有數(shù)據(jù)庫不受影響,比如 TiDB、OceanBase 支持在線表結(jié)構(gòu)變更,不會阻塞讀寫)。
- 部分?jǐn)?shù)據(jù)庫不支持 DDL 事務(wù),比如 MySQL 8 之前的版本,導(dǎo)致一旦在執(zhí)行過程中出錯將無法恢復(fù)。
- 實現(xiàn)成本較高,需要實現(xiàn)「動態(tài)實體」功能,如果要支持不同數(shù)據(jù)庫還得支持各種方言。
盡管這個方案有很多缺點(diǎn),但它的優(yōu)點(diǎn)也很突出,因此愛速搭里實現(xiàn)了這個方案,因為我們覺得能連已有數(shù)據(jù)庫是非常重要的,其它方案都只適合用來做新項目,這個方案使得可以逐步將已有項目低代碼化,不需要做數(shù)據(jù)遷移。
愛速搭里的數(shù)據(jù)庫模型
實現(xiàn)這個方案的關(guān)鍵是「動態(tài)實體」,在專業(yè)開發(fā)中實體(Entity)定義都是靜態(tài)的,以 Java 為例,它從 2006 年開始就有專門的 JPA 規(guī)范,但這個規(guī)范是定義基于 Java 代碼注解,使得需要經(jīng)過編譯才能使用,畢竟它的定位是面向?qū)I(yè)開發(fā),只有寫在代碼里才能支持代碼提示,提升開發(fā)體驗。
而低代碼平臺中需要將這個實體定義抽象成配置,在運(yùn)行時動態(tài)生成實體,如果使用 JPA 就需要生成 Java 代碼后進(jìn)行編譯,這很容易出錯,不太適合低代碼平臺,所以使用這個方案需要實現(xiàn)「動態(tài)實體」功能,是整個方案最大難點(diǎn)。
存儲的實現(xiàn)方案 2:使用文檔型數(shù)據(jù)庫
文檔型數(shù)據(jù)庫不需要預(yù)先定義表結(jié)構(gòu),因此它很適合用來存儲用戶自定義數(shù)據(jù),這個方案實現(xiàn)起來比較簡單,以 MongoDB 為例,可以這樣做:
- 用戶創(chuàng)建一個自定義表的時候,系統(tǒng)就自動創(chuàng)建一個 collection,所有這個表的數(shù)據(jù)都存在這個 collection 里。
- 用戶新增字段的時候,就隨機(jī)分配一個 fileId,后續(xù)對這個字段的操作都自動映射到這個 fileId 上,用 fileId 的好處是用戶重命名字段后還能查找之前的數(shù)據(jù),因為所有數(shù)據(jù)查詢底層都基于這個 fileId。
- 查詢的時候先找到對應(yīng)的 collection,再通過 meta 信息查詢字段對應(yīng)的 fileId,使用這個 fileId 來獲取數(shù)據(jù)。
這個方案的優(yōu)點(diǎn)是實現(xiàn)簡單,用戶體驗可以做得更好,是目前大部分零代碼平臺的選擇,使用這個方案的產(chǎn)品也很好識別,只要看一下它的私有部署文檔,如果有要求裝 MongoDB 就肯定是。
但這個方案也有顯著缺點(diǎn):
- 無法支持外部數(shù)據(jù)庫,數(shù)據(jù)是孤島,外部數(shù)據(jù)接入只能通過導(dǎo)入的方式。
- MongoDB 在國內(nèi)發(fā)展緩慢,接受度依然很低,目前還沒聽說有哪家大公司里最重要的數(shù)據(jù)存在 MongoDB 里,一方面有歷史原因,另一方面不少數(shù)據(jù)庫都開始支持 JSON 字段,已經(jīng)能取代大部分必須用 MongoDB 的場景了。
- 不支持高級 SQL 查詢。
你可能會問,現(xiàn)在 MySQL、Postgres 等數(shù)據(jù)庫都支持 JSON 字段類型了,是否可以用這個字段來實現(xiàn)低代碼?
答案是不太行,只適合數(shù)據(jù)量不大的場景,雖然 JSON 字段可以用來存用戶自定義數(shù)據(jù),但無法創(chuàng)建字段索引,比如在 MySQL 要想給 JSON 創(chuàng)建索引,還是得創(chuàng)建一個特殊的字段,這又需要 DDL 權(quán)限了,沒有索引會導(dǎo)致這個方案無法支持大量數(shù)據(jù)查詢。
在愛速搭中我們也實現(xiàn)這個方案,目前是基于 MySQL JSON 字段,后續(xù)可能也會支持存儲使用 MongoDB,目前它的使用場景是流程執(zhí)行過程中的數(shù)據(jù)存儲,因此數(shù)據(jù)量不會很大,我們希望流程功能用起來可以更簡單些。
它的最大特點(diǎn)是界面編輯和數(shù)據(jù)存儲是統(tǒng)一的,當(dāng)你拖入文本框到頁面后就會自動創(chuàng)建對應(yīng)的字段,不需要先創(chuàng)建數(shù)據(jù)模型再創(chuàng)建界面,因此用起來更簡單。
愛速搭里的表單模型
存儲的實現(xiàn)方案 3:使用行代替列
這是很多可擴(kuò)展平臺里使用的技術(shù),比較典型的是 WordPress,它的擴(kuò)展性很強(qiáng),裝個擴(kuò)展就能變成電商網(wǎng)站。而整個 WordPress 只有 12 個表,它是怎么做到的?方法是靠各種 meta 表,比如用于擴(kuò)展文章的 wp_postmeta 表結(jié)構(gòu)如下:
CREATE TABLE wp_postmeta ( meta_id bigint(20) unsigned NOT NULL auto_increment, post_id bigint(20) unsigned NOT NULL default '0', meta_key varchar(255) default NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY post_id (post_id), KEY meta_key (meta_key)) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
其中的關(guān)鍵就是 meta_key 和 meta_value 這兩個字段,相當(dāng)于將數(shù)據(jù)庫當(dāng) KV 存儲用了,因此可以任意擴(kuò)展字段名及值。
這個方案的優(yōu)點(diǎn)是實現(xiàn)簡單,但缺點(diǎn)也很明顯:
- 查詢性能低,如果有 10 個字段就要查 10 行。
- 無法支持 SQL 高級查詢,因為數(shù)據(jù)是按行存的。
這個方案主要用于成熟項目的擴(kuò)展,比如在 CRM 產(chǎn)品中允許用戶擴(kuò)展字段,但因為性能較低,并不適合通用低代碼平臺。
存儲的實現(xiàn)方案 4:元信息 寬表
早期數(shù)據(jù)庫不支持 JSON 字段的時候,有些開發(fā)者會預(yù)留幾個列來給用戶擴(kuò)展自定義屬性,比如在表里加上 ext1、ext2、ext3 字段,讓用戶可以存 3 個定制數(shù)據(jù),基于這個原理我們可以進(jìn)一步擴(kuò)展,通過預(yù)留大量列來實現(xiàn)應(yīng)用自定義存儲。
這個方案最早出現(xiàn)在 force.com,具體細(xì)節(jié)可以閱讀它架構(gòu)說明文檔[1]。
實現(xiàn)它有兩個關(guān)鍵點(diǎn):元數(shù)據(jù)、預(yù)留列,這里簡單說明一下原理,首先系統(tǒng)預(yù)先創(chuàng)建一個 500 列的表,比如就叫 data:
tenant_id | table_id | uuid | value0 | value1 | …… | value 4000 |
也可以創(chuàng)建更多,但注意有的數(shù)據(jù)庫對列的數(shù)量有限制,比如 MySQL 最多是 4096 列。
上面的 data 表里主要有 4 類字段:
- tenant_id 是租戶 id,用于隔離不同租戶
- table_id 是自定義表的 id
- uuid 是具體這一行數(shù)據(jù)的 id
- 后面的 value0 到 value500 都是預(yù)留的列,用于存儲實際數(shù)據(jù),一般使用變長字符串類型
當(dāng)用戶給這個表新增一個字段的時候,怎么知道這個字段放哪?這就需要另一個用于描述字段信息的元數(shù)據(jù)表,比如增加一個「標(biāo)題」字段時,使用另一個 table_fields 表來描述這個字段的信息,示例如下:
tenant_id | table_id | field_id | value_index | name | type |
1 | 1 | 0 | 0 | 標(biāo)題 | string |
在這個 table_fields 表里:
- tenant_id 和 table_id 和前面一樣。
- field_id 對應(yīng)的是給這個「標(biāo)題」字段分配的 id。
- value_index 對應(yīng)前面那個 data 表里預(yù)覽列的位置,比如這個值是 0,就意味著 value0 列被分配給了這個「標(biāo)題」字段。
- name 用來存名稱,type 用來標(biāo)識類型,這樣查詢和寫入數(shù)據(jù)的時候,首先從這里查詢 value_index 是什么,然后再去前面那個預(yù)留列的表中查詢對應(yīng)列的值。
最終在實際查詢的時候需要根據(jù)元數(shù)據(jù)表做一下轉(zhuǎn)換,比如 select 標(biāo)題 from blog 要轉(zhuǎn)成 select value0 from data where tenal_id = 1 and table_id = 1。
要完全實現(xiàn)這個方案還有很多細(xì)節(jié)問題得解決,由于篇幅原因這里不詳細(xì)介紹,感興趣可以閱讀前面提到的 force.com 技術(shù)白皮書,這里列舉其中幾個問題:
- 因為存儲只能是字符串,所以對于日期、數(shù)字等其他類型,因此讀取的時候需要根據(jù)類型使用數(shù)據(jù)庫里的函數(shù)進(jìn)行轉(zhuǎn)換,比如 STR_TO_DATE。
- 需要單獨(dú)處理唯一性功能,因為這個數(shù)據(jù)表是所有租戶共用的,沒法設(shè)置表級別的唯一性索引,這時就需要新建一個表來單獨(dú)做,壞處是數(shù)據(jù)多份容易產(chǎn)生不一致,需要在所有更新操作都加事務(wù)。
- 需要單獨(dú)處理索引功能,同樣是因為字段是字符串,因此沒法直接在 data 表里加索引,如果數(shù)據(jù)存儲的是數(shù)字,排序就是錯的,為了解決這個問題需要另外創(chuàng)建一個一個包含常見字段的索引表,數(shù)據(jù)更新的時候。
- 自增字段需要自己實現(xiàn)。
- 元數(shù)據(jù)信息需要緩存,不然每次查詢前都需要先查詢元數(shù)據(jù)信息,然后再去查詢真正的數(shù)據(jù)。
這個方案比前面幾個方案的優(yōu)點(diǎn)是:
- 比起第一種原生數(shù)據(jù)庫表方案,它不需要 DDL 操作,不容易出問題,跟適合 SaaS 產(chǎn)品。
- 比起第二種文檔型數(shù)據(jù)庫方案,它的存儲使用更為成熟的關(guān)系型數(shù)據(jù)庫,相關(guān)的運(yùn)維工具多。
- 比起第三種行代替列方案,它的查詢性能好,因為是讀取一行數(shù)據(jù)。
但它也有許多缺點(diǎn):
- 無法支持 SQL 所有功能,比如 force.com 的 SOQL 無法 select *、沒有視圖、不支持寫入和更新數(shù)據(jù),通過這個特點(diǎn)就能識別出使用這個方案的產(chǎn)品,這類產(chǎn)品雖然看起來很像在用傳統(tǒng)數(shù)據(jù)庫,也支持使用 SQL,但這個 SQL 一定是受限的。
- 數(shù)據(jù)泄露風(fēng)險高,因為所有租戶的數(shù)據(jù)都存在一張表里,而數(shù)據(jù)庫都不支持行級別權(quán)限的賬號,所以意味著所有租戶其實共享一個數(shù)據(jù)庫賬號,只要有某個功能的查詢漏了加租戶過濾就能查到所有租戶數(shù)據(jù)。相比之下前面提到的原生表及文檔型數(shù)據(jù)庫方案都能直接使用數(shù)據(jù)庫自帶的賬號進(jìn)行有效隔離。
- 一些數(shù)據(jù)庫高級字段難以支持,比如坐標(biāo)數(shù)據(jù)、二進(jìn)制類型等,只能用單獨(dú)的表存,導(dǎo)致了查詢開銷。
- 整體實現(xiàn)成本高,其中很多細(xì)節(jié)需要處理好,比如保證數(shù)據(jù)一致性,因為為了實現(xiàn)唯一性、索引等功能需要拷貝數(shù)據(jù),更新的時候要同時更新。
愛速搭中沒有實現(xiàn)這個方案,我們曾經(jīng)考慮過但后來放棄了,我認(rèn)為這個方案雖然很適合 SaaS 類的低代碼產(chǎn)品,但它的用戶定位比較尷尬,一方面是有一定復(fù)雜度導(dǎo)致不能做到零代碼平臺那樣的易用性,另一方面是有不少限制導(dǎo)致專業(yè)研發(fā)不喜歡,所以最終是兩邊都不討好,這種產(chǎn)品想做成需要依賴廣泛使用的平臺。
因此 Salesforce 才能做成,而國內(nèi)類似情況我能想到的唯一成功案例是微信小程序,盡管有很多限制,但因為微信廣泛使用,所以才成功了,如果是一個獨(dú)立的小程序平臺肯定沒人用。
這里說一段小歷史,在十幾年前,當(dāng)時云計算領(lǐng)域最先推出的是谷歌 2008 年發(fā)布的 App Engine,這是谷歌的第一個云產(chǎn)品,而當(dāng)時類似 AWS EC2 那樣的虛機(jī)產(chǎn)品國內(nèi)都還沒有,畢竟 KVM 也才剛發(fā)布。
如果你當(dāng)時問云計算的專家,云計算的未來是 App Engine 還是虛擬機(jī),我聽到不少專家的回答是 App Engine,因為這看起來更有前景,你只需要寫代碼,不用操心運(yùn)維,平臺會自動水平擴(kuò)展,這才是云該有的樣子,當(dāng)時國內(nèi)不少公司都推出了類似產(chǎn)品。
但 13 年后的今天,國內(nèi) App Engine 平臺幾乎都關(guān)閉了,而虛機(jī)不但是主流,還更進(jìn)一步出現(xiàn)了物理機(jī)產(chǎn)品。這個元信息方案給我的感覺和當(dāng)年 App Engine 很像,看上去能完成增刪改查的簡單應(yīng)用,但如果深入就發(fā)現(xiàn)缺少很多功高級功能,導(dǎo)致兩邊不討好:
- 技術(shù)薄弱的開發(fā)者不會用,比如因為 App Engine 是分布式部署,導(dǎo)致上傳文件不能放本地,必須改成對象存儲,所以沒法直接用 WordPress 沒法用,對于小站長來說還不如用虛擬主機(jī)。
- 對于有技術(shù)實力的開發(fā)者,又會覺得平臺能力受限,不利于自己后續(xù)發(fā)展,比如谷歌的 App Engine 直到 2019 年才支持 WebSocket。
整體而言我不看好這個方案在國內(nèi)的發(fā)展。
存儲的實現(xiàn)方案 5:使用單文件
這個方案目前只在「仿 Excel」的零代碼平臺中見過,它和 Excel 類似,數(shù)據(jù)全都放一個文件里,查詢過濾完全靠前端,優(yōu)點(diǎn)是:
- 實現(xiàn)簡單,部署成本低,因為表的存儲就是單文件。
- 容錯性強(qiáng),數(shù)據(jù)類型都是靠前端處理的,不會出現(xiàn)存數(shù)據(jù)庫導(dǎo)致。
缺點(diǎn)是:
- 如果要支持行列級別權(quán)限校驗,還得在后端實現(xiàn)一遍過濾,而每次都加載一個巨大的 JSON 文件對服務(wù)器內(nèi)存有較高要求。
- 難以支持事務(wù)操作,尤其是支持行級別的操作。
- 目前看十萬級別數(shù)據(jù)處理可以只靠前端,但再大量的數(shù)據(jù)就不合適了,一次性加載太多對帶寬和瀏覽器內(nèi)存要求比較高。
- 只能當(dāng)成 Excel 的替代品,數(shù)據(jù)是孤島,不能直連外部數(shù)據(jù)庫。
這個方案比較特殊,主要工作量在前端,有大量細(xì)節(jié)體驗優(yōu)化,在愛速搭中沒實現(xiàn),后續(xù)可能會考慮。
2、后端業(yè)務(wù)邏輯的實現(xiàn)
說完了存儲,接下來是第二個問題:如何實現(xiàn)后端業(yè)務(wù)邏輯?
前面提到過代碼難以圖形化,這在后端也是一樣的,因此大概有這幾種方案:
- 邏輯圖形化,這個目前看各個產(chǎn)品效果都不太理想,看上去還不如代碼易讀。
- 固定行為,主要是對數(shù)據(jù)存儲提供增刪改查操作。
- 支持 JavaScript 自定義。
- 簡化 DSL 語言,類似 Excel 中的公式。
前面兩種方案之前介紹過了,這里只討論后面兩種。
后端支持使用 JavaScript 是種常見做法,主要原因是 JavaScript 引擎容易被嵌入,而且啟動速度快,了解的人多,比如市值超過 1200 億美元的 ServiceNow 后端自定義業(yè)務(wù)邏輯就是基于 Rhino 引擎實現(xiàn)的。
簡化 DSL 語言的主要是使用場景是做表達(dá)式計算,比如在流程中的分支流轉(zhuǎn)規(guī)則判斷,需要用戶能自定義表達(dá)式,比如金額大于多少換成總監(jiān)審批,這時用公式會比 JavaScript 會更簡單,因為系統(tǒng)可以自動轉(zhuǎn)換數(shù)據(jù)類型,并自動處理異步函數(shù)的調(diào)用,目前愛速搭的流程里有實現(xiàn),同時在 amis 里也提供了。
另外除了上面提到這四種,我們在愛速搭中還設(shè)計了另一個方案:執(zhí)行樹,它長這個樣子:
左側(cè)是樹形結(jié)構(gòu),右側(cè)是點(diǎn)中某個節(jié)點(diǎn)時的參數(shù)配置,左側(cè)的樹形結(jié)構(gòu)其實是直接參考代碼的樹形結(jié)構(gòu):
- 默認(rèn)從上往下執(zhí)行,但有個特殊的「并行執(zhí)行」節(jié)點(diǎn)可以并行執(zhí)行。
- 對于循環(huán)和分支會創(chuàng)建子節(jié)點(diǎn),并且子節(jié)點(diǎn)可以無限嵌套,相當(dāng)于代碼里的花括號。
- 節(jié)點(diǎn)可以折疊,這樣就能先將復(fù)雜的邏輯折疊起來方便看主流程,這是使用圖模式難以實現(xiàn)的,在圖里收起后無法修改其它節(jié)點(diǎn)的位置,導(dǎo)致空出一塊。
為了方便實現(xiàn)簡單邏輯處理,我們還增加了 JavaScript 節(jié)點(diǎn)和 SQL 節(jié)點(diǎn)。
但執(zhí)行樹這個方案目前的定位是聚合多接口,將多個后端接口數(shù)據(jù)合并后給前端,類似于 BFF 的作用,我們推薦復(fù)雜的后端邏輯還是用 Spring Boot 吧,成熟穩(wěn)定且好招人。
3、流程的實現(xiàn)
接下來是第三個問題:如何實現(xiàn)流程?這是大部分低代碼平臺標(biāo)配的功能,流程的邏輯不像普通代碼那么抽象,因此適合用可視化編輯。
流程可視化存在很久了,著名的 BPMN 規(guī)范最早版本在 2004 就發(fā)布了,因此大部分產(chǎn)品都會支持 BPMN 2.0 規(guī)范。
但 BPMN 本質(zhì)上是一種圖形規(guī)范,它的最大作用是給事件、動作及分支條件這些抽象概念分配了不同的形體,使得熟悉這個規(guī)范的用戶有了共同語言。
BPMN 不能解決平臺鎖定問題,在一個平臺開發(fā)的流程無法直接遷移到另一個平臺。
流程的核心是實現(xiàn)流程流轉(zhuǎn)引擎,以愛速搭為例,流程可視化布局后最終存儲的格式是有向圖,比如下面這個最簡單流程:
簡化后的存儲數(shù)據(jù)格式是兩條連線和三個節(jié)點(diǎn):
{ "lines": [ { "id": "d4ffdd0f6829", "to": "4a055392d2e1", "from": "e19408ecf7e3" }, { "id": "79ccff84860d", "to": "724cd2475bfe", "from": "4a055392d2e1" } ], "nodes": [ { "id": "e19408ecf7e3", "type": "start", "label": "開始" }, { "id": "4a055392d2e1", "type": "examine-and-approve-task", "label": "審批節(jié)點(diǎn)" }, { "id": "724cd2475bfe", "type": "end", "label": "結(jié)束" } ]}
流程流轉(zhuǎn)算法的核心就是根據(jù)當(dāng)前狀態(tài)和這個有向圖,判斷出下個節(jié)點(diǎn)是什么,然后執(zhí)行那個節(jié)點(diǎn)的操作。
同時因為主要面向的是審批流,所以還需要處理審批場景特有的邏輯,比如有的審批是全部通過才算通過,有的審批是只需要一個人通過就算通過,還有回退、加簽等功能,并處理各種邊界條件,比如找不到審批人的時候怎么辦。
雖然目前業(yè)界有開源的流程引擎,但這些引擎大多是面向代碼開發(fā),不太好改造成平臺模式,因此在愛速搭里自己實現(xiàn)了流程引擎,這樣才能更好定制功能。
— 7 —
低代碼平臺未來會怎樣?
前面提到了各種低代碼的實現(xiàn)方案細(xì)節(jié),這里拋開具體細(xì)節(jié),來整體討論一下未來低代碼平臺會怎樣。
最開始提到過低代碼唯一不可缺少的功能是可視化編輯,這是低代碼的最大優(yōu)勢,但是低代碼的最大缺陷,因為可視化難以表達(dá)復(fù)雜的抽象邏輯,因此長遠(yuǎn)看低代碼并不會在所有領(lǐng)域取代專業(yè)開發(fā),更多是和專業(yè)開發(fā)配合來提升效率。
從技術(shù)方案上看低代碼平臺主要有兩個方向:
1、偏向零代碼的方案,它的特點(diǎn)是:
- 易用性強(qiáng)
- 靈活性差
- 適合小公司,客單價低,但客戶數(shù)多
- 標(biāo)準(zhǔn)化程度高,導(dǎo)致功能都很類似,將面臨同質(zhì)化競爭
- 產(chǎn)品使用簡單,客戶支持成本低
2、偏向?qū)I(yè)開發(fā)的方案,它的特點(diǎn)是:
- 易用性弱
- 靈活性強(qiáng)
- 適合中大型公司,客戶數(shù)少,但客單價高
- 標(biāo)準(zhǔn)化程度低,每家都有各自的特點(diǎn)
- 產(chǎn)品使用復(fù)雜,客戶支持成本高
未來會怎樣呢?我的想法是:
偏向零代碼方案,因為功能類似支持成本低,可以同時支持很多用戶,容易出現(xiàn)贏者通吃的情況,但由于 toB 領(lǐng)域發(fā)展速度慢,所以還是有不少機(jī)會。
可以類比 BI 數(shù)據(jù)可視化產(chǎn)品,BI 這個領(lǐng)域的軟件出現(xiàn)至少 20 年了,比如 Qlik 1994 就發(fā)布了,現(xiàn)在市面上的 BI 軟件在基本功能上都大體相同,但沒有哪個產(chǎn)品占據(jù)絕大部分市場份額,我們的 Sugar 產(chǎn)品雖然兩年前才推出,但依然得到了不少優(yōu)質(zhì)客戶,所以只要產(chǎn)品優(yōu)秀就有機(jī)會。
零代碼產(chǎn)品有好幾種形態(tài),和去年一樣,我更看好「在線 Excel」,因為既然是面向非開發(fā)者,類 Excel 是上手成本最低的方案,而且這一年來許多「在線 Excel」的產(chǎn)品都加上了低代碼功能,比如 Airtable 的 Interface,在功能上和表單驅(qū)動的零代碼越來越接近了。
而偏向?qū)I(yè)開發(fā)的方案,因為支持成本高導(dǎo)致沒法同時支持很多客戶,因此更難出現(xiàn)一家獨(dú)大的情況,而偏向研發(fā)會導(dǎo)致細(xì)節(jié)方案有很多區(qū)別,沒太多可比性。
以我們的愛速搭為例,目前產(chǎn)品選擇的方案是偏向?qū)I(yè)開發(fā),現(xiàn)有客戶都是知名企業(yè),但也導(dǎo)致了支持成本很高,因為客戶問的問題都很專業(yè),大多只有核心研發(fā)才能解答,在功能方面我們的特點(diǎn)是前端使用了我們開源的 amis 框架,這個其它家是不會提供的。
— 8 —
總結(jié)
前面字太多了,總結(jié)一下主要觀點(diǎn):
- 低代碼都是一種「聲明式」編程,因為只有聲明式才能可視化編輯,而可視化編輯是低代碼唯一不可少的功能。
- 低代碼的優(yōu)缺點(diǎn)其實來自于「聲明式」本身。
- 編寫代碼是一種抽象思維,因此并不適合可視化,導(dǎo)致低代碼只能面向特定領(lǐng)域,復(fù)雜應(yīng)用需要和專業(yè)開發(fā)配合。
- 前端界面的 HTML CSS 可以認(rèn)為是一種低代碼 DSL,因此界面的低代碼比較容易實現(xiàn),只需要在 HTML CSS 基礎(chǔ)上抽象一層。
- 后端存儲的低代碼有幾種方案,但沒有哪個方案是完美的,它們都有各自的優(yōu)缺點(diǎn),這將決定一個低代碼平臺的適用范圍,建議在選型時重點(diǎn)關(guān)注。
— 9 —
在了解原理之后
前面介紹了各種低代碼實現(xiàn)原理,看起來都不難,但真正要實現(xiàn)還需要大量細(xì)節(jié)工作,以我們的 amis 為例,從 2015 年啟動至今一直在持續(xù)更新,下面是 amis 開源這兩年半來的提交歷史[2],基本除了春節(jié)和國慶之外都在提交:
amis 的 contributors 頁面
但今天 amis 現(xiàn)在仍然有大量功能要做,比如本周將發(fā)布的 1.6.0 版本終于開始初步增強(qiáng)移動端 UI,下面是新版移動端日期選擇:
amis 1.6.0 里的日期選擇
除了無盡的功能要加,還有許多基礎(chǔ)工作要做,比如組件單元測試覆蓋率只有 40%,此刻還有 360 issues 要處理,感謝閱讀到這,有什么問題歡迎留言交流,我要去處理 issue 了……