demo体验地址:dbfu.github.io/lowcode-dem...
前言
在开发企业级低代码平台会有这样一种场景,平台内置的组件不能满足客户的需求,客户希望能够自定义组件,但是我们源码不能给客户,这时候就需要一种方案加载客户自定义的组件。下面我们来实现一下加载远程组件。
往期回顾
模拟用户开发一个远程组件
初始化项目
使用pnpm workspace
创建两个项目,一个是组件项目,另外一个是调试项目。
找个空白文件夹,执行下面命令:
sh
npm init
在根目录下创建pnpm-workspace.yaml
文件,把下面内容复制进去
yaml
packages:
- 'packages/**'
创建packages
文件夹,然后创建lib
文件夹,存放组件项目。
组件使用rollup工具来打包,在lib文件夹下创建rollup.config.js
打包配置文件,把下面代码复制进去。
js
// packages/lib/rollup.config.js
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import { defineConfig } from 'rollup';
import clear from 'rollup-plugin-clear';
import dts from 'rollup-plugin-dts';
import { terser } from 'rollup-plugin-terser';
import typescript from 'rollup-plugin-typescript2';
export default defineConfig([{
input: './src/index.tsx',
output:
[
{
format: 'umd',
name: 'dbfuButton',
file: './dist/bundle.umd.js'
}, {
format: 'amd',
file: './dist/bundle.amd.js'
},
{
format: 'cjs',
file: './dist/bundle.cjs.js'
},
{
format: "es",
file: "./dist/bundle.es.js"
},
],
plugins: [
terser(),
resolve(),
commonjs(),
typescript(),
clear({
targets: ['dist']
}),
],
external: ['react', 'react-dom']
},
{
input: './src/index.tsx',
plugins: [dts()],
output: {
format: 'esm',
file: './dist/index.d.ts',
},
}
]);
在lib文件夹下创建src/index.tsx
组件
tsx
// packages/lib/src/index.tsx
import React from 'react';
function RemoteComponent() {
return (
<div>test</div>
)
}
export default RemoteComponent;
创建package.json文件
json
// packages/lib/package.json
{
"name": "dbfu-remote-component",
"version": "1.0.0",
"description": "",
"author": {
"name": "dbfu"
},
"scripts": {
"build": "rollup -c ./rollup.config.js",
"dev": "rollup -c ./rollup.config.js -w"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.20.13",
"@babel/plugin-syntax-jsx": "^7.18.6",
"@babel/plugin-transform-react-jsx": "^7.20.13",
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-node-resolve": "^15.0.1",
"@types/react": "^18.0.28",
"rollup": "^3.15.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"tslib": "^2.5.0",
"typescript": "^5.0.2"
},
"type": "module",
"main": "dist/bundle.es.js",
"module": "dist/bundle.es.js",
"typings": "dist/index.d.ts",
"dependencies": {
"@emotion/css": "^11.11.2",
"postcss": "^8.4.31",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"files": [
"dist"
]
}
安装依赖
sh
pnpm i
进入lib文件夹下执行下面命令,打包组件
sh
npm run build
打包成功后,会多一个dist文件夹

初始化调试项目
进入packages
目录,使用vite创建项目。
sh
npm create vite example
安装上面组件的依赖
sh
pnpm i dbfu-remote-component
安装完后,package.json
文件里会多一行这样的代码,表示从本地加载组件。

引入组件测试一下
tsx
// packages/example/src/App.tsx
import DbfuRemoteComponent from 'dbfu-remote-component'
function App() {
return (
<DbfuRemoteComponent />
)
}
export default App
在example
文件夹下执行下面命令,启动项目
sh
npm run dev
访问http://127.0.0.1:5173/
,可以看到下面页面

把lib中组件文本改成hello
,重新执行一下npm run build
命令后,可以看到文件变成了hello

这里如果不想每次改东西后重新build一下,可以执行npm run dev
命令,改完后实时生效。
测试完组件后,在lib文件夹下执行npm publish
把组件推到npm库中。
这里还有一些配置,我省略了,大家可以看下我以前写的一篇文章,文章里有介绍如何发布一下npm包。
发布成功后,可以在npm库中搜索到我们刚上传的组件。

在线加载远程组件
介绍
上面我们模拟用户开发了一个组件,正常我们可以在低代码项目里安装组件,然后使用组件,但是前面说了,我们是一个低代码平台,不可能把低代码的源码给客户,让客户引入自己刚写的组件。
为了解决上面问题,我们可以这样做,在物料区支持在线添加组件,渲染组件的时候根据添加的组件配置信息动态加载然后渲染。
这里涉及到一个问题,知道npm上的组件名,怎么渲染这个组件,下面我们来实现一下。
实战
针对上面的问题,我们可以使用React.lazy
方法,这个api就是用来异步加载组件的。
举个例子

上面的例子实现了异步加载Test
组件,可以把上面代码改造成下面这样,2s后显示test。React.lazy
必须配合React.Suspense
。

看下组件打包后的内容
把代码压缩关了,重新打一下包,看一下打包出来bundle.umd.js
文件内容

动态加载组件
我们拿到这段js文本,使用new Function()
动态执行这段文本,把module
,exports
和require
这些变量注入进去,就可以拿到RemoteComponent
组件了。上面代码如果没有module
这些变量,会把RemoteComponent
挂到window
上,我们可以从window上取,但是这样会污染window,所以还是使用第一种方式。
下面根据上面的思路,写个demo验证一下

根据打印可以看到,我们获取到了组件

把Test改造一下

hello渲染出来了,上面的方法是对的。

现在组件代码是写死的,我们怎么获取到组件打包后的代码呢,npm支持通过url获取打包后的js文件。
url格式是这样的
ruby
https://cdn.jsdelivr.net/npm/{组件名}@{版本号}/{文件路径}
根据上面格式,访问一下刚才写的组件,可以访问到。
bash
https://cdn.jsdelivr.net/npm/dbfu-remote-component@1.0.1/dist/bundle.umd.js

改造一下Test代码,使用fetch获取js文本

可以正常显示

到此远程加载组件核心功能实现了,下面把方案迁移到低代码平台中。
低代码中加载远程组件
在Item-Type
添加一个远程组件
组件类型

物料区添加一个远程组件
组件

渲染的时候,使用刚才方案加载组件

效果展示

说明一下,正常这里应该是在线配置组件,不应该在代码里配置,因为这个是demo,不想做那么麻烦,大家先明白这样做可以解决问题就行了,后面实战的时候再慢慢完善。
配置属性
像普通组件一样,远程组件也可以动态配置属性,组件对外暴露text
属性。

给远程组件加一个text
属性配置

因为刚才发了新版本,版本号要改一下。

效果展示

样式
远程组件如果想写样式,这里我推荐使用css in js
方案,主要可以解决样式冲突问题。css in js
库有很多,这里我推荐@emotion/css
库。
给远程组件文字颜色设置red




因为这里自动生成的css类名是动态的,所以可以保证样式不会冲突。
脚手架
如果用户想自定义插件,需要自己搭建一套组件框架,这显然很麻烦,所以我们给用户提供一个脚手架能够快速创建一个组件项目。
实现也简单,把刚才创建的项目当成模版,然后用户执行初始化命令后,把模版代码复制到当前目录下。
找个空白目录执行下面命令
sh
npm init
修改package.json

新建src/index.js
js
#!/usr/bin/env node
const { input } = require('@inquirer/prompts');
const path = require('path');
const fs = require('fs');
const { copyFolder } = require('./utils');
async function main() {
// 交互式输入组件名称和描述
const name = await input({ message: '请输入组件名称' }).catch(() => '');
if (!name) return;
const description = await input({ message: '请输入组件描述' }).catch(() => '');
if (!description) return;
// 获取当前文件夹地址
const curPath = process.cwd();
// 把模板复制到当前文件夹
copyFolder(path.resolve(__dirname, './template'), path.resolve(curPath, name))
let package = fs.readFileSync(path.resolve(curPath, `./${name}/package.json`));
// 把组件名称和组件描述写入package.json
if (package) {
package = JSON.parse(package);
package.name = name;
package.description = description;
}
const componentName = getComponentName(name);
let filePath = path.resolve(curPath, `./${name}/packages/lib/package.json`);
// 修改package.json
fs.writeFileSync(filePath, JSON.stringify(package, null, 2));
// 修改index.tsx
filePath = path.resolve(curPath, `./${name}/packages/lib/src/index.tsx`);
const componentCode = fs.readFileSync(filePath).toString();
const newComponentCode = componentCode.replace(/ComponentName/g, componentName);
fs.writeFileSync(filePath, newComponentCode);
// 修改example
filePath = path.resolve(curPath, `./${name}/packages/example/src/App.tsx`);
const testComponentCode = fs.readFileSync(filePath).toString();
const newTestComponentCode = testComponentCode
.replace(/ComponentName/g, componentName)
.replace(/ComponentPath/g, name);
fs.writeFileSync(filePath, newTestComponentCode);
// 修改example的package.json里的依赖
filePath = path.resolve(curPath, `./${name}/packages/example/package.json`);
const packageComponentCode = fs.readFileSync(filePath).toString();
const newPackageComponentCode = packageComponentCode
.replace(/ComponentPath/g, name);
fs.writeFileSync(filePath, newPackageComponentCode);
console.log(`\nDone.`);
}
main();
这里使用@inquirer/prompts
库来获取用户输入
js
// src/utils.js
const fs = require('fs');
const path = require('path');
// 复制文件夹
function copyFolder(sourceDir, targetDir) {
// 创建目标文件夹
fs.mkdirSync(targetDir);
// 读取源文件夹中的所有文件和子文件夹
const files = fs.readdirSync(sourceDir);
// 遍历源文件夹中的内容
files.forEach((file) => {
const sourcePath = path.join(sourceDir, file);
const targetPath = path.join(targetDir, file);
const stats = fs.statSync(sourcePath);
// 判断是否为文件夹
if (stats.isDirectory()) {
// 如果是文件夹,则递归调用复制文件夹函数
copyFolder(sourcePath, targetPath);
} else {
// 如果是文件,则直接复制到目标文件夹
fs.copyFileSync(sourcePath, targetPath);
}
});
}
/**
* 获取组件名称
*
* @param name 组件名称
* @returns 组件标准名称
*/
function getComponentName(name) {
// 把dbfu-button转换成DbfuButton
if (name?.includes('-')) {
return name.split('-').map(char => getComponentName(char)).join('');
}
// 把button转换成Button
return name.split('').map((char, index) => index === 0 ? char.toUpperCase() : char).join('');
}
module.exports = { copyFolder, getComponentName }
实现后,把库推到npm库。
测试
安装依赖
sh
npm i -g create-lowcode-component
安装成功后,找一个空白目录执行下面命令测试一下
sh
create-lowcode-component

查看生成的项目

最后
这一篇我们实现了用户自定义组件、低代码加载远程组件和快速创建组件项目的脚手架。
下一篇我们把事件处理给升级一下,允许一个事件可以绑定多个动作,并且实现使用可视化的方式来编排动作,这个功能目前很多低代码平台都不支持,算是我开发的低代码平台的一个亮点,把页面和逻辑拆开了。

这里截一下在公司低代码平台配置的简单事件流
demo体验地址:dbfu.github.io/lowcode-dem...
demo仓库地址:github.com/dbfu/lowcod...