从lowcode-demo源码开始、上手低代码引擎扩展
阿里低代码引擎官方提供的lowcode-demo几个案例,是立即上手低代码引擎开发的绝佳案例,在这里跟分享一下我阅读源码时候的理解。
这篇文章的主要内容有:
接下来,我会从以下几个方面,谈谈我对lowcode-demo的理解。
1. package.json,代码阅读从这里开始
现在阅读前端的源代码,差不多都要从package.json开始吧。
使用 git clone https://github.com/alibaba/lowcode-demo
命令下载完lowcode-demo源代码之后,作为一个前端开发,我第一个念头就是查看package.json、把相关的依赖安装上、然后在本地跑起来。
首先打开package.json,很快找到了三个我最关心的部分:scripts、dependencies、devDependencies。
"scripts": {
"start": "build-scripts start --disable-reload --port 5556",
"build": "build-scripts build",
"prepublishOnly": "npm run build",
"pub": "node ./scripts/watchdog.js && npm pub"
}
嗯,scripts里面第一行、就有我熟悉的“start”命令,看来安装完依赖之后,就可以使用npm start
把项目跑起来了。
“start”这里使用了一个叫做“build-scripts”的东西,这个之前从来没有用过,翻了翻lowcode-demo当前文件夹,分别有2个叫做build.json、build.plugin.js的配置文件,看来这又是另一个对Webpack进行封装的命令行工具,就像我常常用到的umi。
"dependencies": {
"@alilc/lowcode-plugin-code-editor": "^1.0.1",
"@alilc/lowcode-plugin-code-generator": "^1.0.2",
"@alilc/lowcode-plugin-components-pane": "^1.0.2",
"@alilc/lowcode-plugin-datasource-pane": "^1.0.3",
"@alilc/lowcode-plugin-inject": "^1.0.0",
"@alilc/lowcode-plugin-manual": "^1.0.2",
"@alilc/lowcode-plugin-schema": "^1.0.0",
"@alilc/lowcode-plugin-simulator-select": "^1.0.0",
"@alilc/lowcode-plugin-undo-redo": "^1.0.0",
"@alilc/lowcode-plugin-zh-en": "^1.0.0",
"@alilc/lowcode-react-renderer": "^1.0.0",
"@alilc/lowcode-setter-behavior": "^1.0.0",
"@alilc/lowcode-setter-title": "^1.0.2"
}
看到dependencies里面,整齐一溜排开的“@alilc/lowcode-plugin-XXX”依赖,说实话我是大开眼界——整个低代码应用全都是插件化的呀。我之前使用过别人开发的插件,比如VS Code插件,Chrome浏览器扩展,Webpack插件,看来现在做一个Web前端应用、也可以使用插件方式进行开发呀。这么说的话,接下来要对低代码引擎进行扩展,也是需要使用他的插件写法。其实关于微内核架构/插件式扩展,我个人还是非常感兴趣的,接下来阅读引擎源码的时候、可以分析一下它的实现。
"devDependencies": {
"@alib/build-scripts": "^0.1.18",
"@alilc/lowcode-engine": "^1.0.0",
"@alilc/lowcode-engine-ext": "^1.0.0",
"@alilc/lowcode-types": "^1.0.0",
"@types/events": "^3.0.0",
"@types/react": "^16.8.3",
"@types/react-dom": "^16.8.2",
"@types/streamsaver": "^2.0.0",
"build-plugin-fusion": "^0.1.0",
"build-plugin-moment-locales": "^0.1.0",
"build-plugin-react-app": "^1.1.2",
"fs-extra": "^10.0.1",
"tsconfig-paths-webpack-plugin": "^3.2.0"
}
再看devDependencies,有scripts中的那个build-scripts工具的依赖——嗯,这个在预料之中;然后再有就是低代码引擎的主包@alilc/lowcode-engine和扩展包@alilc/lowcode-engine-ext——嗯,这个可以理解;还有react和react-dom的Typescript类型描述依赖——嗯,现在Typescript已经流行起来了,类型描述也确实有用,方便代码提示、代码自动补全、看各个方法参数啥的;再接下来几个build-plugin-XXX——嗯,估计是build-scripts的扩展插件。
好了,package.json看完了,打开我本地VS Code的终端,然后执行yarn
命令安装所有dependencies、devDependencies的依赖,等待安装完依赖之后、再执行npm start
,就看到我的浏览器自动打开了“http://localhost:5556/”——嗯,低代码引擎的官方Demo、就算在我本地正常启动起来了。
yarn
npm start
Starting the development server at:
- Local : http://localhost:5556/
- Network: http://192.168.1.9:5556/
仔细观察build.json里面Webpack相关的配置,虽然格式是JSON格式,写法上跟平时的webpack.config.js文件有些不一样,但是配置项还是Webpack的那些配置项,我还能理解它的意图,立马从“entry”中发现了项目的一个入口文件./src/preview.tsx
。
build.json还引用了一个build.plugin.js文件,仔细看里面的代码、它又往“entry”里面添加了几个入口文件,而且都在./src/scenarios/
文件夹,那么我就顺藤摸瓜、接下来就看看这个文件夹里面的各个入口文件。
2. src/scenarios/*,初始化低代码引擎编辑器的2种方式
顺藤摸瓜、看到了这个项目入口文件夹,仔细阅读里面的代码,发现他提供了初始化低代码引擎的2种实现方式。
翻了翻这个./src/scenarios/
文件夹里面的各个index.ts(x)文件,仔细对比了各个index.ts(x)文件里面的内容之后,发现代码整体上的流程都差不多,以src/scenarios/index/index.ts文件为例:
(1)首先引入了来自低代码引擎主包@alilc/lowcode-engine的2个方法init, plugins——看过官方文档了解到,init是用来初始化低代码引擎的,plugins是用来管理/注册插件的;
import { init, plugins } from '@alilc/lowcode-engine';
(2)其次引入了本地的相关插件,比如来自universal/plugin中的插件,因为低代码引擎的扩展、都是通过插件的形式来扩展的,所以这个文件夹的代码、接下来需要重点读一读;
import registerPlugins from '../../universal/plugin';
import { scenarioSwitcher } from '../../sample-plugins/scenario-switcher';
import '../../universal/global.scss';
(3)接下来声明了一个常量preference,看样子应该是给一个叫做DataSourcePane的插件传递参数用的,就像Webpack一样,可以给各个插件传递参数啥的,看英文名称“DataSourcePane”应该是跟数据源相关;
const preference = new Map();
preference.set('DataSourcePane', {
importPlugins: [],
dataSourceTypes: [
{
type: 'fetch',
},
{
type: 'jsonp',
}
]
});
(4)最后是一个名字叫main的立即执行函数,用async进行了修饰,看来里面的好多方法的调用都是异步的。
(async function main() {
await plugins.register(scenarioSwitcher);
await registerPlugins();
init(document.getElementById('lce-container')!, {
// designMode: 'live',
// locale: 'zh-CN',
enableCondition: true,
enableCanvasLock: true,
// 默认绑定变量
supportVariableGlobally: true,
// simulatorUrl 在当 engine-core.js 同一个父路径下时是不需要配置的!!!
// 这里因为用的是 alifd cdn,在不同 npm 包,engine-core.js 和 react-simulator-renderer.js 是不同路径
simulatorUrl: [
'https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@latest/dist/css/react-simulator-renderer.css',
'https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@latest/dist/js/react-simulator-renderer.js'
]
}, preference);
})();
在这个立即执行函数main里面,首先看到使用引擎主包@alilc/lowcode-engine里面的plugins.register方法注册了一个scenarioSwitcher组件,看了看这个组件的实现,是用来切换各个src/scenarios各个入口的,方法使用await修饰,看来这个注册插件过程是一个异步操作,而且接下来的这个registerPlugins方法也是这样。
再接下来,就使用了引擎主包@alilc/lowcode-engine里面的init方法进行编辑器的初始化了。
第一个参数是一个DOM节点,通过阅读build.pluglin.js的Webpack的HTMLWebpackPlugin配置了解到,scenarios各个入口文件的HTML模板是public/index.ejs,在index.ejs的body里面有且只有一个id为“lce-container”的div元素,低代码引擎编辑器作为一个React应用,在运行的时候、看来是挂载到这里了。
第二个参数,看起来是给编辑器传递参数的,可以深入看看API文档,了解一下其他参数。注意到simulatorUrl里面是两个URL地址,看来低代码编辑器的画布区域,也是在这里、从外部扩展的,这里引用的是react-simulator-renderer,是官方提供的React的模拟器,而群里面的小伙伴,有人在做Vue的扩展,之后估计还会有Angular的、Svelte的等等。
稍微有些不一样的,是src/scenarios/custom-initialization/index.tsx文件里面的初始化方式,这里没有使用引擎主包@alilc/lowcode-engine里面的init方法进行初始化,而是通过自定义了一个EditorView的组件、使用了common.skeletonCabin.Workbench进行了初始化,然后自己调用ReactDOM.render将应用挂载到document.getElementById('lce-container')节点上。
(async function main() {
await plugins.register(scenarioSwitcher);
await registerPlugins();
const Workbench = common.skeletonCabin.Workbench;
function EditorView() {
/** 插件是否已初始化成功,因为必须要等插件初始化后才能渲染 Workbench */
const [hasPluginInited, setHasPluginInited] = useState(false);
useEffect(() => {
plugins.init(preference).then(() => {
setHasPluginInited(true);
}).catch(err => console.error(err));
}, []);
return hasPluginInited && <Workbench />;
}
config.setConfig({
// designMode: 'live',
// locale: 'zh-CN',
enableCondition: true,
enableCanvasLock: true,
// 默认绑定变量
supportVariableGlobally: true,
// simulatorUrl 在当 engine-core.js 同一个父路径下时是不需要配置的!!!
// 这里因为用的是 alifd cdn,在不同 npm 包,engine-core.js 和 react-simulator-renderer.js 是不同路径
simulatorUrl: [
'https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@latest/dist/css/react-simulator-renderer.css',
'https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@latest/dist/js/react-simulator-renderer.js'
]
})
ReactDOM.render(<EditorView />, document.getElementById('lce-container')!);
})();
通过官方文档了解到,这种使用React自定义组件进行初始化的方式,可以跟应用的其他组件进行通信,比如使用Dva/Redux里面的数据等等。嗯,熟悉的感觉回来了,之前的React开发经验在这里就可以复用了。
3. src/universal/*,各个插件资源在这里加载
了解到低代码引擎编辑器的初始化方式之后,接下来就重点关注怎么扩展他了。
以其中一个入口文件src/scenarios/index/index.ts为例,看到低代码引擎编辑器初始化那些插件,差不多都是来自src/universal/plugin.tsx,那就先打开这个文件看看。
import React from 'react';
import {
ILowCodePluginContext,
plugins,
skeleton,
project,
setters,
} from '@alilc/lowcode-engine';
import AliLowCodeEngineExt from '@alilc/lowcode-engine-ext';
代码文件开头引用了@alilc/lowcode-engine的几个API接口,plugins,project,skeleton和setters。
plugins是用来管理/注册插件的,我们知道,对低代码引擎的所有扩展,都是通过插件来实现的。
project是对可视化搭建的模型设计,了解面向对象的同学应该能意识到,这是在对“可视化搭建”进行“业务建模”。这个部分的重要程度,可以按照官方文档的说法:“编排的本质是生产符合《阿里巴巴中后台前端搭建协议规范》的数据”。而这个这个project模型,应该就是协议规范的代码实现。
skeleton是用来扩展和管理面板的,而所谓的面板,就是下图蓝色标记的区域:
setters是用来注册和管理设置器的。我们作为一个前端都知道,我们封装完一个组件之后,往往需要别人传入一些props、来进行组件的使用。而这个setters设置器,就是用来给组件设置props参数的。因为props参数是有类型的,而且类型是多种多样的——比如string、number、boolean、object、array、function等等——所以setters也就多种多样。
@alilc/lowcode-engine-ext是低代码引擎的扩展包,这里面提供的、就是官方提供的各种各样的setters。
再接着往下读,发现package.json里面的那“一溜排开”的“lowcode-plugin-XXX”插件,又整齐的在这里“一溜排开”地被import引用了:
import UndoRedoPlugin from '@alilc/lowcode-plugin-undo-redo';
import ComponentsPane from '@alilc/lowcode-plugin-components-pane';
import ZhEnPlugin from '@alilc/lowcode-plugin-zh-en';
import CodeGenPlugin from '@alilc/lowcode-plugin-code-generator';
import DataSourcePanePlugin from '@alilc/lowcode-plugin-datasource-pane';
import SchemaPlugin from '@alilc/lowcode-plugin-schema';
import CodeEditor from "@alilc/lowcode-plugin-code-editor";
import ManualPlugin from "@alilc/lowcode-plugin-manual";
import Inject, { injectAssets } from '@alilc/lowcode-plugin-inject';
import SimulatorResizer from '@alilc/lowcode-plugin-simulator-select';
这些插件的源代码,可以在Github的alibaba/lowcode-plugins代码仓库里面找到,如果没有找到,比如lowcode-plugin-simulator-select,也是可以在node_modules里面读取到源代码,清晰可读:
在src/universal/plugin.tsx文件的最后,在registerPlugins方法中,可以看到这些插件被一一注册到plugins中。
export default async function registerPlugins() {
await plugins.register(ManualPlugin);
await plugins.register(Inject);
// ... 其他插件的注册
};
既然在低代码引擎中,所有的扩展都是通过写插件来实现的,那么咱们就仔细看看一个插件式怎么实现的。
4. src/sample-plugins/*,一个plugin插件是怎么开发的
既然对低代码引擎进行的各个扩展,都是通过插件的形式进行扩展的,那咱们就看看一个低代码引擎的扩展是怎么开发的。
打开src/sample-plugins/文件夹,可以看到里面有两个组件实现,这应该是官方用来给人参考的插件实现,这个logo的实现,就是一个普通的React组件,这个插件的注册、是在src/universal/plugin.tsx中实现的,我们就先略过。
仔细看这个src/sample-plugins/scenario-switcher/index.tsx文件,记得这个scenarioSwitcher,在低代码引擎进行初始化的时候(比如src/scenarios/index/index.ts),就在一个立即执行函数(比如src/scenarios/index/index.ts中的main方法)里面,通过plugins.register进行了注册。
import { scenarioSwitcher } from '../../sample-plugins/scenario-switcher';
// ... 其他代码省略
await plugins.register(scenarioSwitcher);
接下来,就在这个src/sample-plugins/scenario-switcher/index.tsx文件里面,看看这个scenarioSwitcher插件的具体实现。
首先看到了一个普通的React函数式组件的实现Switcher,实现的意图是在各个场景的入口文件之间进行切换:
import { Select } from '@alifd/next';
import scenarios from '../../universal/scenarios.json';
const { Option } = Select;
const getCurrentScenarioName = () => {
// return 'index'
const list = location.href.split('/');
return list[list.length - 1].replace('.html', '');
}
function Switcher(props: any) {
return (<Select
id="basic-demo"
onChange={(val) => location.href = `./${val}.html`}
defaultValue={getCurrentScenarioName()}
>
{
scenarios.map((scenario: any) => <Option value={scenario.name}>{scenario.title}</Option>)
}
</Select>)
}
最后通过export关键字导出的scenarioSwitcher,是一个箭头函数,按照官方文档的说法,这被叫做“pluginConfigCreator”。
export const scenarioSwitcher = (ctx: ILowCodePluginContext) => {
return {
name: 'scenarioSwitcher',
async init() {
const { skeleton } = ctx;
skeleton.add({
name: 'scenarioSwitcher',
area: 'topArea',
type: 'Widget',
props: {
align: 'right',
width: 80,
},
content: Switcher,
});
},
};
};
scenarioSwitcher.pluginName = 'scenarioSwitcher';
箭头函数接收一个ILowCodePluginContext类型的参数ctx,看过文档了解到,插件可以通过这个ctx,获取低代码引擎提供的各种API,比如之前见过的project,skeleton和setters等等:
因为这个scenarioSwitcher组件是要放在低代码编辑器顶部区域,所以这里使用skeleton向顶部区域(area: 'topArea')添加了一个Widget(type: 'Widget'),其他参数可以参考skeleton的官方文档。
最后需要注意的是这个pluginName 字段,它是插件必须挂载的,而且需要确保全局唯一,否则 register 时会报错。
嗯,这个scenarioSwitcher函数,就是“插件”本件了,原来它最终的形式、是一个挂载了pluginName字段的函数。
5. src/setters/*,一个setters是怎么开发的
在低代码引擎编辑器里,对一个组件进行可视化搭建——拖拽组件进入画布是一个关键步骤,而对当前选中组件进行设置是另一个关键步骤——现在我们看到的这些setters,正是对组件进行设置的关键部分。
作为一个前端我们知道,封装好一个组件之后,往往是需要别人传递props参数给到组件的。props参数是有类型的,而且类型是多种多样的——比如string、number、boolean、object、array、function等等。
而在可视化搭建的时候,低代码引擎允许低代码开发者通过setters的可视化配置、给组件传递props参数。因为props参数是多种多样的,所以setters也就多种多样。官方通过@alilc/lowcode-engine-ext扩展包,已经提供了各种各样的setters。
在lowcode-demo/src/setters文件夹下,有两个setter的实现,比如src/setters/custom-setter,就是一个普通的React组件,能从props中接受包括defaultValue/value/onChange等等参数:
import React, { Component } from 'react'; // import classNames from 'classnames';
class CustomSetter extends Component<any, any> {
render() {
const { defaultValue, value, onChange } = this.props;
const { editor } = this.props.field;
return <div>hello world</div>;
}
}
export default CustomSetter;
而CustomSetter的注册,则是通过plugin的形式、使用setters API进行注册的:
const customSetter = (ctx: ILowCodePluginContext) => {
return {
name: '___registerCustomSetter___',
async init() {
const { setters } = ctx;
setters.registerSetter('CustomSetter', CustomSetter);
},
};
}
customSetter.pluginName = 'customSetter';
await plugins.register(customSetter);
6. src/preview.tsx,搭建后的页面、是怎样渲染的
使用低代码引擎搭建完页面,就会生成一个包含页面Schema信息、组件信息的JSON文件,这个信息可以直接使用preview.tsx进行渲染——大部分H5应用也是这样:JSON Schema + 组件,进行页面的渲染实现。
我们使用低代码引擎进行可视化搭建,最终搭建得到的成果就是一个project模型,可以持久化的形式就是一个JSON Schema。
而从src/preview.tsx文件中我们可以看到,我们把搭建的产物/JSON Schema、以及组件等等信息作为参数,给到@alilc/lowcode-react-renderer模块,就能够实现搭建的页面的渲染。
import ReactRenderer from '@alilc/lowcode-react-renderer';
// ... 其他代码
const { schema, components } = data;
if (!schema || !components) {
init();
return <Loading fullScreen />;
}
return (
<div className="lowcode-plugin-sample-preview">
<ReactRenderer
className="lowcode-plugin-sample-preview-content"
schema={schema}
components={components}
/>
</div>
);
这种方式是常用的渲染方式,如果你做过H5可视化编辑器的开发的话,或者你考察过其他低代码编辑器的实现的话,就会发现大家差不多都是“JSON Schema + 组件”的方式实现渲染,这是一种通用的做法。
7. One more thing:出码,一个可以激发很多可能的插件
One more thing——出码模块,是另一个非常让我激动的插件了,我之前开发过H5可视化编辑器,都是止步于“JSON Schema + 组件”进行页面渲染,而低代码引擎又往前多走了一步,进行前端代码生成,带来了更大的想象空间。
说实话,这个“出码模块”对我来说是一个意外惊喜。
我之前做过H5可视化搭建,我也看过很多其他可视化搭建平台,差不多都止步于“JSON Schema + 组件”了。其实JSON Schema作为搭建的产出物,已经可以满足页面可视化搭建、渲染的要求了。
然而,如果使用出码模块,把JSON Schema转换为正常的前端源代码的话,就可以考虑发布到npm仓库,使得可视化搭建的产出物、作为一个正常的npm组件、被其他开发者使用,进入正常前端开发环节中。比如一些设计师,就可以通过低代码平台只设计一些纯UI组件,组件转换为源代码、发布到npm仓库之后,其他前端开发者也可以引用使用。
当然,更多想象空间,大家可以补充,我觉得这个模块可以激发更多可能,接下来可以仔细研究一下。
转载自:https://juejin.cn/post/7162868609419739150