一直以来我总是会刷到一些关于低代码的文章,但都是零零散散的,根本记不住。现在终于有时间在这里做个系统性的总结了
关于低代码,想必大家都有所了解,就是用拖拉拽的方式来快速搭建某个页面 || 表单 || 海报 || 游戏等,但其实除了常见拖拉拽的方式以外,还可以用形如在线文档的方式来生成页面,这里分别贴个链接给大家体验一下:
当然了,本文主要讲解的是拖拽型的低代码,这里先简单截个图瞅瞅:
顺带罗列下低码平台的一些优缺点:
优点:✅
缺点:❌
确实,低代码自身限制还是很多的,随便提个改动,都可能牵一发动全身。不过这里我们并不会去评判低码平台的好坏,而只是单纯的分享一些低代码中的核心思路和整个流程:包括但不限于模块划分、如何解耦和扩展、画布的实现方式等等。
最重要的事最重要如果让我说一个低代码中最重要的事情,那就是方向。我们要知道低代码是有它的适用场景的(交互简单 && 轻逻辑),比如:
单单只实现上面的任意一种场景就已经要兼容很多东西了,所以想覆盖所有情况几乎是不可能的,而且现在也没有一个统一的规范,都是各做各的,百花齐放。因此不要想着啥都做,得挑一个垂直方向发力,方向越具体,自动化程度越高(比如组件拿来即用,无需修改),生产效率也更高,平台也会更好用。
次重要除了方向,次重要的事情就是简单(包括交互和逻辑)。为什么要简单呢?
那如何才能做到简单呢?还是得朝着垂直方向发力,也就是会有点定制,要固化一些操作、约束一些行为。目前我还是觉得低代码主要还是给非研发同学用的,所以要足够简单。
基本实现扯了这么多,现在让我们赶紧步入正轨吧。纵观大部分低码平台,主要都是由以下四部分组成(画的有点简陋):
接下来我们会对每个部分都挑两三个重点来讲解一下。
1、协议在讲解每个模块之前,我们先来说一个东西,就是协议(听起来很高大上,其实就是规范,更朴素点叫做格式),它主要包括物料协议、平台搭建协议和其他协议等等。为什么要先约定协议呢,因为这个东西贯穿低代码的整条链路:
协议是低码平台的基石,它的主要目的就是约束和扩展,约定的好,事半功倍;约定不好,版版重构。约定优于配置说的就是这个道理。如果维护到后期发现协议很难拓展了,那基本只能重来了,只不过你多了些经验。协议本质上就是一堆 interface(就是固定格式啦)。
2、物料区先来看看最左侧的物料区吧,这是低代码的起点,协议也是从这边开始的,先约定个最简单的物料协议吧:
/** 单个物料约定 */
interface IComponent {
/** 组件名 */
componentName: string;
/** 组件中文名称 */
title: string;
/** 缩略图 */
icon?: string;
/** 包地址 */
npm: {
/** 源码组件名称 */
componentName?: string;
/** 源码组件库名 */
package: string;
/** 源码组件版本号 */
version?: string;
};
/** 分类:比如基础组件、容器组件、自定义组件 */
group?: string;
/** 组件入参或者说是可配置参数 */
props?: {
name: string,
propType: string,
description: string,
defaultValue: any,
}[];
/** 其他扩展协议 */
[key: string]: any;
}
// 举个例子
const componentList = [
{
componentName: "Message",
title: "Message",
icon: "",
group: "基础组件",
npm: { // import { Message } from @alifd/next 的意思
exportName: "Message",
package: "@alifd/next",
version: "1.19.18",
main: "src/index.js",
destructuring: true,
},
props: [{
name: "title",
propType: "string",
description: "标题",
defaultValue: "标题"
}]
}
];
物料区其实没啥功能,我们会有一个 componentList,里面是各种组件的基本信息,直接循环渲染即可。同时还会顺便生成一个 componentMap,主要是方便后续我们通过组件名来快速获取是组件的元信息,比如下面这样:
const componentMap = {
Message: {
componentName: "Message",
title: "Message",
icon: "",
group: "基础组件",
// ...
},
};
// 通常情况,低码平台平台还需要对外暴露出加载组件和注册组件的方法,比如这样:
function createRegisterConfig() {
const componentList = [];
const componentMap = {};
return {
componentList,
componentMap,
loadComponent: () => {},
register: (comp) => {
componentList.push(comp);
componentMap[comp.componentName] = comp;
}
}
}
物料区本身并不复杂,这里就说三个注意点:
// 方法一:import
const name = 'Button' // 组件名称
const component = await import('https://xxx.xxx/bundle.js')
Vue.component(name, component)
// 方法二:script
function loadjs(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.onload = resolve
script.onerror = reject
})
}
const name = 'Button' // 组件名称
await loadjs('https://xxx.xxx/bundle.js')
// 这种方式加载组件,会直接将组件挂载在全局变量 window 下,所以 window[name] 取值后就是组件
Vue.component(name, window[name])
这里我们先说说画布区的几种实现方式吧:
那画布区是怎么渲染出来的呢?其实我们会有一个 componentTree 来递归渲染拖拽出来的元素,然后各自按照组件类型来渲染,先简单看下 componentTree 的大体结构吧:
import ReactRenderer from '@alilc/lowcode-react-renderer';
import reactDOM from 'react-dom';
import { Button } from "@alifd/next";
const componentTree = { // 画布区的所有元素都在这里维护
componentName: 'Page', // 因为我们是以页面为单位,所以顶层一定是 Page
props: {},
children: [{
componentName: 'Button',
props: {
type: 'primary',
size: "large",
style: {
color: '#2077ff'
},
className: 'custom-button',
},
children: '确定',
}]
};
const conponents = {
Button,
};
ReactDOM.render(( // 里面会递归渲染组件
<ReactRenderer
componentTree={componentTree}
components={components}
/>
), document.getElementById('root'));
// ================================================
// 如果树形结构不好理解的话,我们可以把数据换成普通的数组来理解,然后渲染的过程就是循环遍历数组即可,这样就容易多了,比如 H5 页面就很适合数组这种形式
const componentTree = [{
"componentName": "ElButton",
"height": 100,
"props": {},
"style": {}
}, {
"componentName": "ElInput",
"height": 300,
"props": {},
"style": {}
}];
事实上,低码平台都秉持着数据驱动视图的思想(和我们现在用的 vue 和 react 框架如出一辙),也是通过递归解析 componentTree 这个全局组件树来动态生成页面,化简来说就是:UI = Transformer(componentTree)。Transformer 这一步我们可以称之为转换器、渲染器或者 render 函数,通常开发完成之后,这个渲染器是不用改的,我们只需要单纯的修改数据,渲染器自然会帮我们解析。
这个思想很重要,也是解耦的核心:就是我们所有的操作,不管拖拽也好,修改元素属性也好,还是调整元素位置,都是对 componentTree 这个数据进行修改,单纯的对数据进行操作,比如追加元素,就往 componentTree 的 children 里面 push 一个元素即可;如果要修改一个元素的属性值,只需要找到对应元素的数据修改其 props 值即可。画布编排的本质就是操作组件节点和属性。
另外为了让每个组件都能直接获取到这个 componentTree,我们可以把这个 componentTree 弄成全局的(全局数据能够让整体流程更加清晰),比如放在 window、vuex、redux 上,这样每个模块就能共享同一份数据,也能随时随地更改同一份数据(平台会暴露公共的修改方法),而这个渲染器只是单纯的根据这个数据来渲染,并不处理其他事情。
这里我们还要注意一个问题,就是画布区本身也是个组件,是个组件那它就会受到父元素和全局的影响,最简单的比如样式,可能受到外部样式作用,导致你这个画布区和最终呈现的页面可能有一丢丢的不同,所以要排除这些影响,那具体可以怎么做呢?就是把这个画布区搞成一个独立的 iframe,这样环境就比较纯了,完美隔离,只不过增加了通信的成本。现在,物料区只负责渲染组件列表,以及触发拖拽放下的事件,之后就是触发修改全局的 componentTree,再之后就是触发画布区的重新渲染。这样一来,你就会发现画布区和物料区就很好的解耦了。到目前为止画布区只负责单纯的渲染。
如果你还是想知道这个渲染器到底是怎么递归怎么渲染的,我这里也提供了段简单的代码帮助你理解:
function renderNode(node) {
if (!node) return null;
const component = components[node.componentName];
const props = compute(node.props);
const children = node.children.map(c => renderNode(c));
return React.render(component, props, children);
}
renderNode( componentTree);
4、属性设置区
接下来我们简单讲下右侧的属性设置区,这个区域通常会支持三种基本的设置:props && 样式 && 事件。一般来说我们的操作是这样的:
上面的过程和我们平时开发中后台应用的表单项(FormRender)是一毛一样的,网上也有很多教程,这里就不细说了。
我们主要来讲一下属性设置区的几个注意点:
下面是简单的代码示例截图,有个印象就行:
那沙箱里面能拿到什么数据呢?其实主要看我们想暴露什么参数给组件了,比如全局数据、全局方法、父元素、当前组件的 state 等等,那其实我们就可以直接传个包含以上数据的大对象然后传给 with 即可。
5、顶部操作区上面那几个组成部分其实已经构成了低代码中最核心的几个部分,我们可以称之为 Core(内核)。对于顶部操作区,通常可以有前进、后退、清空等操作,但是这些操作并不算是必须的,它们通常以插件的形式存在,也方便大家一起维护和扩展,这就是微内核架构:
1 * Core N * plugins
关于这个架构,有兴趣的可以参考这篇文章 微内核架构在前端的实现及其应用,这篇文章讲解的很清楚了。
然后我们这里就简单讲下清空和回退操作吧(当然其余其他操作也是一样的道理):
但其实插件不仅仅适用于顶部条:
到目前为止,我们就初步搭建好了页面,它背后其实是一堆数据,但是有可能你还是一头雾水,没关系,为了能让大家有个更具象的认识,我把这份数据的最终形态简单汇总了下(放心,就一丢丢丢丢数据很好看懂的):
const json = {
version: "1.0.0", // 当前协议版本号
componentList: [
{ // 组件描述
componentName: "Button",
package: "@alifd/next",
version: "1.0.0",
destructuring: true,
},
{
package: "@alifd/next",
version: "1.3.2",
componentName: "Page",
destructuring: true,
}
],
state: { // 全局状态
name: "尤水就下"
},
componentsTree: { // 画布内容
componentName: "Page",
props: {},
children: [{
componentName: "Button",
state: {},
props: {
type: 'primary',
size: "large",
style: {
color: 'red'
},
className: 'custom-button',
onClick: { // 事件绑定
type: "JSFunction",
value: "function(e) { console.log(e.target.innerText) }"
},
}
}]
}
}
接下来就是要准备发布了,具体该怎么做呢?这里说下两种主要的方式:
import React, { memo } from 'react';
import ReactRenderer from '@alilc/lowcode-react-renderer';
const SamplePreview = memo(() => {
return ( // 至于渲染器怎么实现的,网上有一堆文章,不理解的同样可以把数据当成简单的数组,for 循环渲染而已
<ReactRenderer
className="lowcode-plugin-sample-preview-content"
schema={schema}
components={components}
/>
);
});
// ps: 这里简单说下 json 和 schema 的区别,以前我也很困惑,不知道现在理解对没有(但我感觉大部分情况下都是随便叫的)
// json 是一个普通对象
// schema 是一个有固定格式的普通对象
// json schema 是一个描述 json 格式的普通对象
那两种方式有什么区别呢:
前者 | 后者 | |
1 | 运行时编译(JIT) | 预编译(AOT) |
2 | 最终输出的是 json | 最终输出的是项目 |
3 | 实时生效 | 得打包重新发版 |
4 | 一般这个够用 | 有性能要求用这个 |
为什么前者会有性能问题呢?因为渲染页面的时候,需要将 json 进行一层转换,而后者是已经打包之后的项目了。
其他问题{
"version": "1.0.0",
"packages": [{ // 依赖包
"title": "fusion 组件库",
"package": "@alifd/next",
"version": "1.20.0",
"urls": [
"https://alifd.alicdn.com/npm/@alifd/next/1.20.0/next.min.js",
"https://alifd.alicdn.com/npm/@alifd/next/1.20.0/next.min.css"
],
"library": "Next" // 最终这样用:window.Next.componentName
}],
"components": []
}
如果你能看到这里,那说..明...文章写的还行,哈哈哈嗝。这里我就简单的用几句话对本篇文章进行一个总结:
最后的最后,我又重新画了张图方便大家记忆:
好啦,本次分享就到这里,有什么问题欢迎点赞评论留言,我们下期再见,拜拜
作者:尤水就下
链接:https://juejin.cn/post/7276837017231835136
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved