• Node.js躬行记(24)——低代码


      低代码开发平台(LCDP)是无需编码(0代码)或通过少量代码就可以快速生成应用程序的开发平台。让具有不同经验水平的开发人员可以通过图形化的用户界面,通过拖拽组件和模型驱动的逻辑来创建网页和移动应用程序。

      低代码的核心是呈现、交互和扩展,其中呈现和交互需要借助自行研发的渲染引擎实现。而此处的扩展特指物料库,也就是各类自定义的业务组件,有了物料库后才能满足更多的场景。

      在 4 个月前研发过一套可视化搭建系统,当时采用的是生成代码的方式渲染页面,这种方案有强依赖研发、无法持续可视化编辑、不包括交互行为等问题。

      而本次研发采用的则是运行时渲染,功能比较基础,基于React开发,代码量在 3000 多行左右,用户群是本组团队成员,目标是:

    1. 满足 80% 的后台需求,高效赋能解放生产力。
    2. 抽象共性,标准化流程,提升代码维护性。
    3. 减少项目代码量,加快构建速度。

      平台的操作界面如下,由于管理后台页面的元素比较单一,所以暂不支持拖拽和缩放等功能,也就是没有通用的布局器。

      

      组件区域可以选择内置的通用模板组件,点击添加可在预览区域显示对应的组件,位置可上下调整,并且可以像真实的页面那样进行动态交互。配置区域可填写菜单名称、权限、路由等信息,点击更新文件后,会将数据存储到 MongoDB 中。

    一、渲染引擎

      在数据库中保存的组件是一套 JSON 格式的 Schema(页面的描述性数据),将 Schema 读取出来后,经过渲染引擎解析后,得到对应的组件,最后在页面中显示。

    1)Schema

      下面的 Schema 描述的是一个提示组件,参数的值是字符串和布尔值。为了能让组件满足更多的场景,有时候,组件的参数值可以是字符串类型的 JSX 代码或回调函数,例如下面的 description 属性,那这些就需要做特殊处理了。

    复制代码
    {
      props: {
        message: "123",
        description: "

    456

    ", 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 库,可将组件转换成 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,即 React 版本的 Monaco Editor

      

      编辑器默认是不支持放大的,这是自己加的一个功能。点击放大按钮后,修改编辑器父级的样式,如下所示,全屏状态能更直观的修改代码。

    复制代码
    .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 = {}) => {parse(node.toString())},
        p: (node:JSX.Element|string, props = {}) => 

    {parse(node.toString())}

    , }, 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 {parse(node)}; }
      return ;
    };
    复制代码

    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. 根据接口查找页面没有以前直观。

      除了第一个问题暂时没有好的方法之外,其他都有解决办法。

      解决第二个问题,可以开放测试环境的编辑权限,但是在测试环境不能修改路由和权限文件。

      解决第三个问题,可以在保存时判断路由或权限是否与之前不同,只有不同的时候才会去更新文件。

      解决第四个问题,可以开放接口的模糊查询。

     

  • 相关阅读:
    Huggingface最强视觉模型Idefics2开源,80亿参数突破多模态关键技术
    模板的特化(具体化)
    手把手教你在windows上安装mysql8.0最新版本数据库,保姆级教学
    git 把master分支代码合并到自己分支(常用git 命令)
    IOS不使用默认的mainStroryboard作为首个controller的方法
    java毕业设计读书网络社区设计Mybatis+系统+数据库+调试部署
    深入了解 ReadDirectoryChangesW 并应用其监控文件目录
    TrustSVD算法进行基于矩阵分解的商品推荐 代码+数据(可作为毕设)
    计算机网络:帧中继的概念
    【数字电路基础】进制转换:二进制、十进制、八进制、十六进制、反码、补码、原码
  • 原文地址:https://www.cnblogs.com/strick/p/16744656.html