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

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...

相关推荐
莹雨潇潇6 分钟前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr14 分钟前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho1 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ2 小时前
html+css+js实现step进度条效果
javascript·css·html
小白学习日记3 小时前
【复习】HTML常用标签<table>
前端·html
john_hjy3 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd3 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java3 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js