低代码在线加载远程组件——低代码知识点详解(四)

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()动态执行这段文本,把moduleexportsrequire这些变量注入进去,就可以拿到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...

相关推荐
RaidenLiu7 分钟前
告别陷阱:精通Flutter Signals的生命周期、高级API与调试之道
前端·flutter·前端框架
非凡ghost7 分钟前
HWiNFO(专业系统信息检测工具)
前端·javascript·后端
非凡ghost9 分钟前
FireAlpaca(免费数字绘图软件)
前端·javascript·后端
非凡ghost16 分钟前
Sucrose Wallpaper Engine(动态壁纸管理工具)
前端·javascript·后端
拉不动的猪17 分钟前
为什么不建议项目里用延时器作为规定时间内的业务操作
前端·javascript·vue.js
该用户已不存在24 分钟前
Gemini CLI 扩展,把Nano Banana 搬到终端
前端·后端·ai编程
地方地方26 分钟前
前端踩坑记:解决图片与 Div 换行间隙的隐藏元凶
前端·javascript
炒米233329 分钟前
【Array】数组的方法
javascript
小猫由里香32 分钟前
小程序打开文件(文件流、地址链接)封装
前端
Tzarevich35 分钟前
使用n8n工作流自动化生成每日科技新闻速览:告别信息过载,拥抱智能阅读
前端