likes
comments
collection
share

从lowcode-demo源码开始、上手低代码引擎扩展

作者站长头像
站长
· 阅读数 37

阿里低代码引擎官方提供的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接口,pluginsprojectskeletonsetters

plugins是用来管理/注册插件的,我们知道,对低代码引擎的所有扩展,都是通过插件来实现的。

project是对可视化搭建的模型设计,了解面向对象的同学应该能意识到,这是在对“可视化搭建”进行“业务建模”。这个部分的重要程度,可以按照官方文档的说法:“编排的本质是生产符合《阿里巴巴中后台前端搭建协议规范》的数据”。而这个这个project模型,应该就是协议规范的代码实现。

skeleton是用来扩展和管理面板的,而所谓的面板,就是下图蓝色标记的区域:

从lowcode-demo源码开始、上手低代码引擎扩展

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里面读取到源代码,清晰可读:

从lowcode-demo源码开始、上手低代码引擎扩展

在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,比如之前见过的projectskeletonsetters等等:

因为这个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
评论
请登录