低代码(low-code)简单实践

4,613 阅读7分钟

理解

低代码就是尽量少写代码,定义好业务组件,通过可视化操作实现开发工作。它主要受众是开发者。

无代码(no-code)即不需要写代码就能完成开发,更加偏向业务层的定制。

解决了什么?

提效降本、质量保障、降低开发门槛。

低代码可以提升开发效率,保障系统稳定性,也降低了开发门槛,可以直接可视化开发。

可能存在的问题

  • 不灵活。适用于通用业务领域,对定制化需求不友好。
  • 不可控。业务拓展性、可维护性较低。
  • 不好用。开发不想用,业务不会用。

低代码或许可以降低开发门槛,但复杂度并不会降低。可视化开发的自由度越高,组件粒度就越细,配置复杂度就越高。

简单实践

前端低代码开发不仅是界面开发,应该还包含工程化、项目管理、api接口、权限控制等一些列的开发提效。

设计

一个页面其实就是一棵树,所以不管是拖拽还是配置,最终完成的就是一棵数据树。所以我们可以通过json schema来进行页面设计。

实现

基础搭建

工程化layout设计权限和i18nAPI管理 这些都是一些管理平台的基础设施,前面也讲过,大家可以去看看。

页面设计

schema设计

const schema={
  type:'a',
  props:{},
  children:[],
};


  • type:标签名或组件名。组件可以是UI组件或业务组件,先注册再使用。
  • props:属性配置。组件的属性可根据组件库或自定义组件使用文档去配置。如果属性里面含有组件,可依照schema渲染原则执行。
  • children:子节点。可以是文本节点,组件,或子元素列表。

custom render

const render=(schema,params)=>{
  schema=Array.isArray(schema)?schema:[schema];
  const dom=schema.map((item,i)=>{
    let {type,props,children}=item;
    type=(type||'div').trim();
    const first=type.charAt(0);
    type=first.toUpperCase()===first?(components[type]||'div'):type;
    props={
      key:i,
      ...formatProps(props,params),
    };
    children=Array.isArray(children)?render(children,params):[formatChildren(children||props.children,params)??null];
    return createElement(type,props,...children);
  });
  return dom;
};


  • components:我们注册的组件。
  • formatProps、formatChildren:将props或children转换为我们需要的运行时的值。主要用于我们自定义的组件。props或children可以是函数,可以传递我们需要的参数params,最终返回我们需要的数据。
  • render:通过 react 的 createElement(type,props,...children) 渲染。

属性解析

const render=(schema,params)=>{
  schema=Array.isArray(schema)?schema:[schema];
  const dom=schema.map((item,i)=>{
    let {type,props,children}=item;
    type=(type||'div').trim();
    const first=type.charAt(0);
    type=first.toUpperCase()===first?(components[type]||'div'):type;
    props={
      key:i,
      ...formatProps(props,params),
    };
    children=Array.isArray(children)?render(children,params):[formatChildren(children||props.children,params)??null];
    return createElement(type,props,...children);
  });
  return dom;
};


可使用自定义函数,组件内部数据作为参数,来获取属性值。或直接使用全局configs。

例如:

{
  prop:'test',
  isPending:`{true}`,
  style:`{{width:'800px'}}`,
  handle:`{self=>self.rules}`,
  onClick:`{()=>e=>alert('hello')}`,
}

通过 {code} 将code字符串转换为运行时代码。

判断并提取字符串代码

const matchedStr=(str,c=['{','}'])=>str?.trim?.().match(new RegExp(`^${c[0]}([\\s\\S]*)${c[1]}$`))?.[1];

执行字符串代码

const str2code=(str,hasReturn=false)=>{
  str=hasReturn?str:`return ${str};`;
  const exec=Function(str);
  return exec();
};

str2code会直接执行并返回结果,如果返回的是函数会执行函数并返回结果。如果我们需要返回函数,就要包裹一层函数。例如:onClick{()=>e=>alert('hello')}

路由设置

路由配置可直接在页面配置,存入后台,使用时获取路由配置即可。

{
  path:'/low-code',
  name:'低代码',
  icon:'CoffeeOutlined',
  denied:false,
  children:[
    {
      path:'/dom',
      name:'原生dom',
      icon:'CodeOutlined',
      componentPath:'/lowCode',
      loadData:{
        pageSchema,
      },
    },
    {
      path:'/ui',
      name:'UI组件',
      icon:'CodeOutlined',
      componentPath:'/lowCode',
      loadData:{
        pageSchema,
      },
    },
    {
      path:'/users',
      name:'业务组件',
      icon:'CodeOutlined',
      componentPath:'/lowCode',
      loadData:{
        pageSchema,
      },
    },
    {
      path:'/users/add',
      name:'新增用户',
      componentPath:'/lowCode',
      loadData:{
        pageSchema,
      },
    },
    {
      path:'/users/edit/:id',
      name:'编辑用户',
      componentPath:'/lowCode',
      loadData:{
        pageSchema,
      },
    },
  ],
}

如果整个系统都是通过 schema 数据配置生成的,那么我们只需一个渲染器,通过路由id获取到 shcema 数据,然后渲染成当前路由页面。所以只需一个渲染文件即可。

根据 projectIdrouterId 获取路由页面数据。

const pageSchema=async ({id})=>{
  const {result}=await apiList.listSchemaFn({routerId:id,projectId:defProject._id});
  return {result};
};

通过设置路由 loadData 来提前请求数据,页面直接获取即可。详细使用见 useRouter

const pageSchema=async ({id})=>{
  const {result}=await apiList.listSchemaFn({routerId:id,projectId:defProject._id});
  return {result};
};

页面渲染

const Index=props=>{
  const {pageSchema}=props;
  if(pageSchema==null||pageSchema.pending){
    return <Spinner global />;
  }
  return customRender(pageSchema.result||[],{},props);
};

可视化开发示例

创建项目

1.png

首先我们创建一个项目,如图所示。本示例使用 控制台 项目演示。

2.png

创建用户并分配项目

3.png

创建API

4.png

5.png

新建项目路由

6.png

为用户设置路由权限

7.png

页面设计

原生html标签

10.png

23.png

根据dom元素属性自行配置。

UI组件

12.png

14.png

当我们设计好页面时,可以随时回到项目路由查看改页面,也可点击预览查看,符合预期效果后保存即可。

22.png

25.png

原生标签和基础组件只能设计出一些静态展示效果,我们可以自定义一些业务组件,给页面加入交互性。

业务组件

tableform 为例,简单设计一个用户管理页面。

15.png

table 设置了自定义属性 actions columns searchSchema modalSchema

{
  actions,
  columns,
  searchSchema,
  modalSchema,
}

  • actions:定义事件
  • columns:表头设计
  • searchSchema:搜索表单
  • modalSchema:弹窗表单

16.png

事件定义可自行定义 action name ,共页面使用,apiName 从我们API系统里面选。

预览

17.png

可实时进行页面预览,也提供了撤销重做操作。

编辑功能

props编辑

const editProps=values=>{
  const tree=editNodes(schemaTree,selectedKey,{props:values},'key');
  setSchema(tree);
  record(clone(tree));
  setDisableUndo(false);
};

每次编辑都会触发 schema 更新,并会记录每次操作的数据,使用 record 函数记录,便于我们完成撤销重做功能。

cacheData函数

const {record,undo,redo,clean}=cacheData();

撤销重做

const undoDesign=()=>{
  const {index,data}=undo();
  setSchema(data);
  if(index===0){
    setDisableUndo(true);
  }
  setDisableRedo(false);
};

const redoDesign=()=>{
  const {index,length,data}=redo();
  setSchema(data);
  if(index===length-1){
    setDisableRedo(true);
  }
  setDisableUndo(false);
};

组件移动

提供了组件移动功能,可根据需要自行拖动。

27.png

const onDrop=info=>{
  const fromId=info.dragNode.key;
  const toId=info.node.key;
  const dropPosition=info.dropPosition;
  const tree=moveNodes(schemaTree,fromId,toId,dropPosition,'key');
  setSchema(tree);
};

效果

可点击按钮或链接查看效果。

28.png

页面 schema

29.png

用户管理页面

18.png

页面 schema

30.png

编辑页面

19.png

页面 schema

31.png

总结

低代码更多的是用来当作提升开发效率的一个工具,在我们当前业务范围内,写少量代码封装好业务组件,即可进行可视化开发。

平台的通用性和灵活性,需要我们在实际业务中去权衡。

我们需要认清,没有一劳永逸的方法,只有在不断探索中提升。

项目地址:github.com/ahyiru/web-…