🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
&emsp;&emsp;低代码开发平台(LCDP)是无需编码(0代码)或通过少量代码就可以快速生成应用程序的开发平台。让具有不同经验水平的开发人员可以通过图形化的用户界面,通过拖拽组件和模型驱动的逻辑来创建网页和移动应用程序。 &emsp;&emsp;低代码的核心是呈现、交互和扩展,其中呈现和交互需要借助自行研发的渲染引擎实现。而此处的扩展特指物料库,也就是各类自定义的业务组件,有了物料库后才能满足更多的场景。 &emsp;&emsp;在 4 个月前研发过一套可视化搭建系统,当时采用的是生成代码的方式渲染页面,这种方案有强依赖研发、无法持续可视化编辑、不包括交互行为等问题。 &emsp;&emsp;而本次研发采用的则是运行时渲染,功能比较基础,基于React开发,代码量在 3000 多行左右,用户群是本组团队成员,目标是: 1. 满足 80% 的后台需求,高效赋能解放生产力。 2. 抽象共性,标准化流程,提升代码维护性。 3. 减少项目代码量,加快构建速度。 &emsp;&emsp;平台的操作界面如下,由于管理后台页面的元素比较单一,所以暂不支持拖拽和缩放等功能,也就是没有通用的布局器。 :-: ![](https://img.kancloud.cn/80/5e/805ef0ee8e6330475d1cdc254b3bf08a_3138x2236.png =800x) &emsp;&emsp;组件区域可以选择内置的[通用模板组件](https://github.com/pwstrick/shin-admin/blob/main/docs/template.md),点击添加可在预览区域显示对应的组件,位置可上下调整,并且可以像真实的页面那样进行动态交互。配置区域可填写菜单名称、权限、路由等信息,点击更新文件后,会将数据存储到 MongoDB 中。 ## 一、渲染引擎 &emsp;&emsp;在数据库中保存的组件是一套 JSON 格式的 Schema(页面的描述性数据),将 Schema 读取出来后,经过渲染引擎解析后,得到对应的组件,最后在页面中显示。 **1)Schema** &emsp;&emsp;下面的 Schema 描述的是一个提示组件,参数的值是字符串和布尔值。为了能让组件满足更多的场景,有时候,组件的参数值可以是字符串类型的 JSX 代码或回调函数,例如下面的 description 属性,那这些就需要做特殊处理了。 ~~~ { props: { message: "123", description: "<p>456</p>", showIcon: true }, name: "Prompt" } ~~~ &emsp;&emsp;点击 Schema 按钮,可实时查看当前的 Schema 结构,这些 Schema 最终也会存储到 MongoDB 中。 :-: ![](https://img.kancloud.cn/6a/85/6a85856aa9869d0a29854b6155e3ebde_2732x1538.png =800x) **2)参数解析** &emsp;&emsp;从组件区域得到的参数都是字符串类型,此时需要做一次适当的类型转换,变成数组、函数等。eval() 比较适合做这个活,它会将字符串当做 JavaScript 代码进行执行,执行后就能得到各种类型的值。 &emsp;&emsp;在下面的遍历中,先对数组做特殊处理,然后再判断字符串是否是对象或数组,最后在运行 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; } ~~~ &emsp;&emsp;在将参数转换类型后,接下来渲染引擎就会根据不同的组件对这些参数进行定制处理,例如将提示组件的 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)回调函数** &emsp;&emsp;除了 JSX 之外,为了能适应更多的业务场景,提供了自定义的回调函数。 ~~~ { props: { btns: `onClick: function(dispatch) { dispatch({ type: "template/showCreate", payload: { modalName: 'add' } });` }, name: "Btns" } ~~~ &emsp;&emsp;编辑器组件使用的是[react-monaco-editor](https://github.com/react-monaco-editor/react-monaco-editor),即 React 版本的[Monaco Editor](https://microsoft.github.io/monaco-editor/index.html)。 :-: ![](https://img.kancloud.cn/d7/0c/d70c1f7673dd8d204cb92b2d0bdcf60d_900x418.png =400x) &emsp;&emsp;编辑器默认是不支持放大的,这是自己加的一个功能。点击放大按钮后,修改编辑器父级的样式,如下所示,全屏状态能更直观的修改代码。 ~~~ .fullscreen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 10000; } ~~~ &emsp;&emsp;函数默认是字符串,需要进行一次转换,采用的是 new Function(),这种方式可以将参数传递进来。eval() 虽然也能执行字符串代码,但是它不能传递上下文或参数。 ~~~ const stringToFunction = (func:string) => { const editorWarpper = new Function(`return ${func}`); return editorWarpper(); }; ~~~ &emsp;&emsp;本来是想在编辑器中沿用 TypeScript 语法,但是在代码中没有编译成功,会报错。 **4)组件映射** &emsp;&emsp;一开始是想在编辑器中直接输入 JSX 代码,然后通过 Babel 转译,但在代码中引入 Babel 后也是出现了一系列的错误,只得作罢。 &emsp;&emsp;之前的 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 }); } } }; ~~~ &emsp;&emsp;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 }, }; ~~~ &emsp;&emsp;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)关联组件** &emsp;&emsp;关联组件特指一个模板组件内包含另一个模板组件,例如标签栏组件,它会包含其他模板组件。 :-: ![](https://img.kancloud.cn/3a/7c/3a7c23855926337f240380c1fb47f93b_1618x420.png =800x) &emsp;&emsp;如果要做到关联,最简单的方法是将组件的配置一起写到标签栏的参数中,但这么做会非常繁琐,并且内容太多,不够直观。还不如跳过低代码平台,直接在编辑器中编写,来的省事。 &emsp;&emsp;后面就想到关联组件索引,关联的组件也可以在平台中编辑自己的参数。只是当组件删除后,关联的组件也要一并删除,代码的复杂度会变高。 **6)交互预览** &emsp;&emsp;在预览时,为了能实现交互,就需要修改状态驱动视图的更新。 &emsp;&emsp;对于一些方法,在执行过后,就能实现状态或视图的更新。 &emsp;&emsp;但对于一些属性,例如 values.allState,若要让其能动态读取内容,就需要借助 getter。 ~~~ const values:ObjectType = { get allState() { return wrapperState; }, }; ~~~ ## 二、配套设施 &emsp;&emsp;要将该平台推广到内部使用,除了渲染引擎外,还需要些配套设施,包括自定义业务组件、页面呈现、持久化存储等。 **1)业务组件** &emsp;&emsp;内置的组件肯定是无法满足实际的业务,所以需要可以扩展业务组件,由此制订了一套简单的数据源规范。所有的业务组件我都放到了custom文件中,可自行创建新文件,例如 demo。 ~~~ custom ├──── demo ├──── index.tsx ├──── test.tsx ~~~ &emsp;&emsp;在 index.tsx 文件中,会引入自定义的组件,后面就能在平台中使用了。 ~~~ import Demo from './demo'; const Components:ObjectType = { Demo, }; export default Components; ~~~ &emsp;&emsp;为了便于调试,预留了测试组件的页面,在下拉框中选择相应的组件,并填写完属性后,就会在组件内容区域呈现效果。 :-: ![](https://img.kancloud.cn/57/32/57327afb1cea1772efa792ef863e67da_1422x1398.png =600x) **2)生成文件** &emsp;&emsp;在配置区域点击生成/更新文件后,就会将菜单、路由、权限等信息保存到 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": "测试", } ~~~ &emsp;&emsp;为了与之前的路由和权限机制保持一致,在保存成功后,需要自动更新本地的路由文件(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)页面呈现** &emsp;&emsp;由于是运行时渲染,因此页面的呈现都使用了一套代码,只是路由会不同。所有的路由都是以 view/ 为前缀,在首次进入页面时,会根据路径读取页面信息,路径会去除前缀。 ~~~ const { pathname } = location; // 查询参数 if (pathname.indexOf("/view/") >= 0) { dispatch({ type: "getOnePage", payload: { path: pathname.replace("/view/", "") } }); } ~~~ &emsp;&emsp;在页面呈现的内部,代码很少,在调用 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)体验优化** &emsp;&emsp;体验优化很值得推敲,目前还有很多地方有待优化,自己只完成了一小部分。 &emsp;&emsp;例如在创建页面时,第一次点击后,第二次点击是做更新,而不是再次创建。因为在创建后会更新路由和权限文件,那么就会重新构建,完成热更新,页面再刷新一次。为了下次点击按钮是更新,可以更改地址,带上id。 ~~~ history.push(`/lowcode/editor2?id=${data._id}`) ~~~ 在组件区域提供一个按钮,还原最近一次的组件状态,这样即使页面报错,刷新后,还能继续上一步未完成的操作。 **5)使用问题** &emsp;&emsp;在实际使用后,陆陆续续收到了些问题反馈,如下所列: 1. 需要手动的下载和导入,不是合并分支后,代码就会更新。 2. 修改一个小问题,例如改文案,也需要开启项目。 3. 每次保存,都会更新路由和权限文件。 4. 根据接口查找页面没有以前直观。 &emsp;&emsp;除了第一个问题暂时没有好的方法之外,其他都有解决办法。 &emsp;&emsp;解决第二个问题,可以开放测试环境的编辑权限,但是在测试环境不能修改路由和权限文件。 &emsp;&emsp;解决第三个问题,可以在保存时判断路由或权限是否与之前不同,只有不同的时候才会去更新文件。 &emsp;&emsp;解决第四个问题,可以开放接口的模糊查询。 ***** > 原文出处: [博客园-Node.js躬行记](https://www.cnblogs.com/strick/category/1688575.html) [知乎专栏-Node.js躬行记](https://zhuanlan.zhihu.com/pwnode) 已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎阅读。 ![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200) 推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。