低代碼開發平臺(LCDP)是無需編碼(0代碼)或通過少量代碼就可以快速生成應用程序的開發平臺。讓具有不同經驗水平的開發人員可以通過圖形化的用戶界面,通過拖拽組件和模型驅動的邏輯來創建網頁和移動應用程序。
  低代碼的核心是呈現、交互和擴展,其中呈現和交互需要借助自行研發的渲染引擎實現。而此處的擴展特指物料庫,也就是各類自定義的業務組件,有了物料庫后才能滿足更多的場景。
  在 4 個月前研發過一套可視化搭建系統,當時采用的是生成代碼的方式渲染頁面,這種方案有強依賴研發、無法持續可視化編輯、不包括交互行為等問題。
  而本次研發采用的則是運行時渲染,功能比較基礎,基于React開發,代碼量在 3000 多行左右,用戶群是本組團隊成員,目標是:
1. 滿足 80% 的后臺需求,高效賦能解放生產力。
2. 抽象共性,標準化流程,提升代碼維護性。
3. 減少項目代碼量,加快構建速度。
  平臺的操作界面如下,由于管理后臺頁面的元素比較單一,所以暫不支持拖拽和縮放等功能,也就是沒有通用的布局器。
:-: 
  組件區域可以選擇內置的[通用模板組件](https://github.com/pwstrick/shin-admin/blob/main/docs/template.md),點擊添加可在預覽區域顯示對應的組件,位置可上下調整,并且可以像真實的頁面那樣進行動態交互。配置區域可填寫菜單名稱、權限、路由等信息,點擊更新文件后,會將數據存儲到 MongoDB 中。
## 一、渲染引擎
  在數據庫中保存的組件是一套 JSON 格式的 Schema(頁面的描述性數據),將 Schema 讀取出來后,經過渲染引擎解析后,得到對應的組件,最后在頁面中顯示。
**1)Schema**
  下面的 Schema 描述的是一個提示組件,參數的值是字符串和布爾值。為了能讓組件滿足更多的場景,有時候,組件的參數值可以是字符串類型的 JSX 代碼或回調函數,例如下面的 description 屬性,那這些就需要做特殊處理了。
~~~
{
props: {
message: "123",
description: "<p>456</p>",
showIcon: true
},
name: "Prompt"
}
~~~
  點擊 Schema 按鈕,可實時查看當前的 Schema 結構,這些 Schema 最終也會存儲到 MongoDB 中。
:-: 
**2)參數解析**
  從組件區域得到的參數都是字符串類型,此時需要做一次適當的類型轉換,變成數組、函數等。eval() 比較適合做這個活,它會將字符串當做 JavaScript 代碼進行執行,執行后就能得到各種類型的值。
  在下面的遍歷中,先對數組做特殊處理,然后再判斷字符串是否是對象或數組,最后在運行 eval()函數時,要加 try-catch,捕獲異常,因為字符串中有可能包含各種語法錯誤。
~~~
for (const key in values) {
// 未定義的值不做處理
if (values[key] === undefined) continue;
// 對數組做特殊處理
if (Array.isArray(values[key])) {
// 將數組的空元素過濾掉
values[key] = removeEmptyInArray(values[key]);
newValues[key] = values[key];
continue;
}
const originValue = values[key];
let value = originValue;
// 判斷是對象或數組
const len = originValue.length;
if (
(originValue[0] === "{" && originValue[len - 1] === "}") ||
(originValue[0] === "[" && originValue[len - 1] === "]")
) {
try {
/**
* 字符串轉換成對象
* 若 values[key] 是數組,會有BUG
* eval(`(${[1,2]})`)的值為 2,因為數組會先調用toString(),得到 eval("(1,2)")
*/
value = eval(`(${originValue})`);
} catch (e) {
// eval(`test`)字符串也會報test未定義的錯誤
value = originValue;
}
}
newValues[key] = value;
}
~~~
  在將參數轉換類型后,接下來渲染引擎就會根據不同的組件對這些參數進行定制處理,例如將提示組件的 description 屬性轉換成 JSX 語法的代碼。parse()是一個解析函數,來自于[html-react-parser](https://github.com/remarkablemark/html-react-parser)庫,可將組件轉換成 React.createElement() 的形式。回調函數的處理會在后面做詳細的講解。
~~~
{
handleProps: (values: ObjectType) => {
// 將字符串轉換成JSX
if (values.description) {
values.description = parse(values.description.toString());
}
return values;
};
}
~~~
**3)回調函數**
  除了 JSX 之外,為了能適應更多的業務場景,提供了自定義的回調函數。
~~~
{
props: {
btns: `onClick: function(dispatch) {
dispatch({
type: "template/showCreate",
payload: {
modalName: 'add'
}
});`
},
name: "Btns"
}
~~~
  編輯器組件使用的是[react-monaco-editor](https://github.com/react-monaco-editor/react-monaco-editor),即 React 版本的[Monaco Editor](https://microsoft.github.io/monaco-editor/index.html)。
:-: 
  編輯器默認是不支持放大的,這是自己加的一個功能。點擊放大按鈕后,修改編輯器父級的樣式,如下所示,全屏狀態能更直觀的修改代碼。
~~~
.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
}
~~~
  函數默認是字符串,需要進行一次轉換,采用的是 new Function(),這種方式可以將參數傳遞進來。eval() 雖然也能執行字符串代碼,但是它不能傳遞上下文或參數。
~~~
const stringToFunction = (func:string) => {
const editorWarpper = new Function(`return ${func}`);
return editorWarpper();
};
~~~
  本來是想在編輯器中沿用 TypeScript 語法,但是在代碼中沒有編譯成功,會報錯。
**4)組件映射**
  一開始是想在編輯器中直接輸入 JSX 代碼,然后通過 Babel 轉譯,但在代碼中引入 Babel 后也是出現了一系列的錯誤,只得作罷。
  之前的 parse() 函數可將字符串轉換成組件,但是在實際開發,需要添加各種類型的屬性,還有各類事件,全部揉成字符串并不直觀,并且 antd 組件不能直接通過 parse() 解析得到。所以仍然是書寫一定規則的 Schema(如下所示),再轉換成對應的組件。
~~~
{
name: "antd.TextArea",
props: {
width: 200
},
events: {
onChange: function (dispatch, e) {
const str = e.target.value;
const keys = str.match(/\{(\w+)\}/g);
const params = {};
keys && keys.forEach((item) => (params[item] = {}));
dispatch({
type: "groupTemplate/setSqlParams",
payload: params
});
}
}
};
~~~
  name 中會包含組件類別和名稱,類別包括 4 種:antd、模板、HTML標準元素和自定義組件。
~~~
export const componentHash:ObjectType = {
admin: {
Prompt,
SelectTabs,
CreateModal,
},
antd: {
Affix,
Anchor,
AutoComplete,
},
html: {
a: (node:JSX.Element|string, props = {}) => <a {...props}>{parse(node.toString())}</a>,
p: (node:JSX.Element|string, props = {}) => <p {...props}>{parse(node.toString())}</p>,
},
custom: { ...Custom },
};
~~~
  jsonToComponent() 是將JSON轉換成組件的函數,就是從上面的對象中得到組件,帶上屬性、子組件后,再將其返回。
~~~
const jsonToComponent = (item:JsonComponentItemType) => {
const {
name, props = {}, node,
} = item;
const names = name.split('.');
const types = componentHash[names[0]];
// 異常情況
if (!types || names.length === 1) {
return null;
}
const Component = types[names[1]];
// HTML元素處理
if (names[0] === 'html') {
return Component(node, props);
}
// 組件處理
if (node) { return <Component {...props}>{parse(node)}</Component>; }
return <Component {...props} />;
};
~~~
**5)關聯組件**
  關聯組件特指一個模板組件內包含另一個模板組件,例如標簽欄組件,它會包含其他模板組件。
:-: 
  如果要做到關聯,最簡單的方法是將組件的配置一起寫到標簽欄的參數中,但這么做會非常繁瑣,并且內容太多,不夠直觀。還不如跳過低代碼平臺,直接在編輯器中編寫,來的省事。
  后面就想到關聯組件索引,關聯的組件也可以在平臺中編輯自己的參數。只是當組件刪除后,關聯的組件也要一并刪除,代碼的復雜度會變高。
**6)交互預覽**
  在預覽時,為了能實現交互,就需要修改狀態驅動視圖的更新。
  對于一些方法,在執行過后,就能實現狀態或視圖的更新。
  但對于一些屬性,例如 values.allState,若要讓其能動態讀取內容,就需要借助 getter。
~~~
const values:ObjectType = {
get allState() {
return wrapperState;
},
};
~~~
## 二、配套設施
  要將該平臺推廣到內部使用,除了渲染引擎外,還需要些配套設施,包括自定義業務組件、頁面呈現、持久化存儲等。
**1)業務組件**
  內置的組件肯定是無法滿足實際的業務,所以需要可以擴展業務組件,由此制訂了一套簡單的數據源規范。所有的業務組件我都放到了custom文件中,可自行創建新文件,例如 demo。
~~~
custom
├──── demo
├──── index.tsx
├──── test.tsx
~~~
  在 index.tsx 文件中,會引入自定義的組件,后面就能在平臺中使用了。
~~~
import Demo from './demo';
const Components:ObjectType = {
Demo,
};
export default Components;
~~~
  為了便于調試,預留了測試組件的頁面,在下拉框中選擇相應的組件,并填寫完屬性后,就會在組件內容區域呈現效果。
:-: 
**2)生成文件**
  在配置區域點擊生成/更新文件后,就會將菜單、路由、權限等信息保存到 MongoDB 中。其中最重要的就是組件的原始信息,如下所示。
~~~
{
"components": [{
"props": {
"message": "44",
"description": "555",
"showIcon": true
},
"name": "Prompt"
}],
"auto_url": ['api', 'article/list'],
"authority": "backend.sql.ccc",
"parent": "backend.sql",
"path": "lowcode/test",
"name": "測試",
}
~~~
  為了與之前的路由和權限機制保持一致,在保存成功后,需要自動更新本地的路由文件(router.js)和權限文件(authority.ts)。
~~~
// 路由
{
path: "/view/lowcode/test",
exact: true,
component: "lowcode/editor/run"
}
// 權限
{
id: "backend.sql.test",
pid: "backend.sql",
status: 1,
type: 1,
name: "測試",
desc: "",
routers: "/view/lowcode/test"
}
~~~
**3)頁面呈現**
  由于是運行時渲染,因此頁面的呈現都使用了一套代碼,只是路由會不同。所有的路由都是以 view/ 為前綴,在首次進入頁面時,會根據路徑讀取頁面信息,路徑會去除前綴。
~~~
const { pathname } = location; // 查詢參數
if (pathname.indexOf("/view/") >= 0) {
dispatch({
type: "getOnePage",
payload: { path: pathname.replace("/view/", "") }
});
}
~~~
  在頁面呈現的內部,代碼很少,在調用 initialPage() 函數后,得到組件列表,直接在頁面中渲染即可。initialPage() 其實就是渲染引擎,內部代碼比較多,在此不展開。
~~~
function Run({ dispatch, state, allState }:EditorProps) {
const { pageInfo } = state;
let components;
if (pageInfo.components) {
components = initialPage(pageInfo, dispatch, allState, false);
}
return (
<>
{components && components.map((item:ComponentType2) =>
(item.visible !== false && item.component))}
</>
);
}
~~~
**4)體驗優化**
  體驗優化很值得推敲,目前還有很多地方有待優化,自己只完成了一小部分。
  例如在創建頁面時,第一次點擊后,第二次點擊是做更新,而不是再次創建。因為在創建后會更新路由和權限文件,那么就會重新構建,完成熱更新,頁面再刷新一次。為了下次點擊按鈕是更新,可以更改地址,帶上id。
~~~
history.push(`/lowcode/editor2?id=${data._id}`)
~~~
在組件區域提供一個按鈕,還原最近一次的組件狀態,這樣即使頁面報錯,刷新后,還能繼續上一步未完成的操作。
**5)使用問題**
  在實際使用后,陸陸續續收到了些問題反饋,如下所列:
1. 需要手動的下載和導入,不是合并分支后,代碼就會更新。
2. 修改一個小問題,例如改文案,也需要開啟項目。
3. 每次保存,都會更新路由和權限文件。
4. 根據接口查找頁面沒有以前直觀。
  除了第一個問題暫時沒有好的方法之外,其他都有解決辦法。
  解決第二個問題,可以開放測試環境的編輯權限,但是在測試環境不能修改路由和權限文件。
  解決第三個問題,可以在保存時判斷路由或權限是否與之前不同,只有不同的時候才會去更新文件。
  解決第四個問題,可以開放接口的模糊查詢。
*****
> 原文出處:
[博客園-Node.js躬行記](https://www.cnblogs.com/strick/category/1688575.html)
[知乎專欄-Node.js躬行記](https://zhuanlan.zhihu.com/pwnode)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎閱讀。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020