基于System.js的微前端实现(插件化)

目录​​​​​​​

写在前面

一、微前端相关知识

(一)概念

[(二) 优势](#(二) 优势)

[(三) 缺点](#(三) 缺点)

(四)应用场景

(五)现有框架

[1. qiankun](#1. qiankun)

[2. single-spa](#2. single-spa)

[3. SystemJS](#3. SystemJS)

二、需求分析

三、流程概览

(一)子项目改造

(二)npm包

(三)system.js插件封装

(四)主项目改造

四、子项目改造

(一)子项目创建

(二)webpack5设置

(三)其他

五、npm包

(一)npm包内容

(二)npm包发布

六、基于system.js的插件工具封装

(一)封装

(二)使用

七、主项目改造

(一)安装依赖

(二)指定路由

八、实现效果

九、开源项目地址

(一)主项目

(二)子项目

(三)xu-demo-data


写在前面

本文所有技术实现的相关代码都已开源,想clone或者fork,请直接拉到文章末尾。

另外,技术实现借鉴了国内开源软件 kubevela,非常感谢kubevela的各位大佬能开源质量如此之高的代码。且因为本人是行业菜鸡,代码中如有语法错误或其他问题,请在评论区指正,感谢各位。

一、微前端相关知识

微前端(Micro Frontends)是一种前端架构的模式,旨在将大型前端应用拆分为多个独立的、可组合的小型前端模块。这些模块可以由不同的团队独立开发、部署和维护,最终在用户界面中无缝集成。微前端的核心思想类似于后端的微服务架构,通过模块化的方式降低系统复杂性,提高开发效率和扩展性。

(一)概念

  • 独立部署:每个前端模块可以独立开发和部署,不依赖于其他模块。各个模块之间通过预定义的接口进行通信。

  • 技术无关:不同的前端模块可以使用不同的技术栈(如React、Vue、Angular等),这使得团队能够根据具体需求和技术栈选择合适的工具。

  • 团队独立:微前端允许不同的团队负责不同的模块,降低了团队间的耦合度。每个团队可以独立管理其代码库、开发流程和发布节奏。

  • 界面组合:微前端架构通过组合不同的模块构建完整的用户界面。这可以通过在浏览器中动态加载不同的模块来实现,通常使用iframe、模块联邦(Module Federation)或自定义加载逻辑。

  • 渐进迁移:微前端使得大型遗留系统可以逐步迁移到新技术中,减少一次性重构的风险和成本。

(二) 优势

  • 扩展性强:每个模块独立开发和部署,能够快速迭代和扩展功能。
  • 灵活性高:不同模块可以选择不同的技术和工具,而不影响整个系统。
  • 提高团队效率:独立团队可以并行开发,减少依赖和协调。

(三) 缺点

  • 性能问题:由于多个模块的组合可能导致资源加载和渲染效率问题,需要特别注意优化。
  • 共享状态和通信:不同模块间的数据共享和通信需要标准化和规范化,避免耦合过高。
  • 样式和UI一致性:虽然模块独立开发,但最终呈现给用户的界面需要保持一致的用户体验和样式规范。

(四)应用场景

  • 随着技术的发展,前端应用可能需要从旧的技术栈迁移到新的技术栈。然而,对于大型应用,一次性迁移的风险和成本往往较高。微前端使得可以逐步迁移------某些模块可以采用新的技术栈,而其他部分保持不变。这种方式降低了技术迁移的风险和成本。
  • 微前端允许这些团队各自独立开发、部署和维护自己的模块,而不影响其他团队。这种方式有助于减少依赖、加快开发节奏,并使应用更加易于扩展。

(五)现有框架

1. qiankun

qiankun 是一个基于 single-spa 的微前端框架,专注于子应用的加载、沙箱隔离以及跨应用的通信管理。qiankun 提供了开箱即用的解决方案来管理多个微前端应用,并在容器应用中将它们集成到一起。它的插件化功能可以帮助不同技术栈的子应用共存。

官网地址:

qiankun - qiankunhttps://qiankun.umijs.org/zh

2. single-spa

single-spa 是一个流行的微前端框架,它允许多个前端框架(如 React、Vue、Angular)在同一个应用中共存,并实现独立加载和渲染。single-spa 的插件化机制允许开发者将每个子应用打包成独立的模块,单独部署和运行。

官网地址:

https://zh-hans.single-spa.js.org/docs/getting-started-overviewhttps://zh-hans.single-spa.js.org/docs/getting-started-overview

3. SystemJS

SystemJS 是一种模块加载器,通常用于微前端架构中实现动态模块加载。通过 SystemJS,应用可以在运行时按需加载不同的子应用或插件,而不是在构建时打包所有内容。它常与 single-spa 等微前端框架一起使用。

GitHub地址:

GitHub - systemjs/systemjs: Dynamic ES module loaderDynamic ES module loader. Contribute to systemjs/systemjs development by creating an account on GitHub.https://github.com/systemjs/systemjs

二、需求分析

年初,公司产品经理提出了一个概念,叫js热加载热更新,需要什么js文件就动态加载什么js文件。因为目前公司的产品,有两个代码仓库,一个用于企业版,一个用于做开源版。开源版的核心代码与企业版的核心代码无差别,但在定制化和某些功能的支持上,企业版更占据优势。但是,同时维护两个仓库的很多分支又比较耗费人力物力,无论是企业功能下方开源,还是开源功能合并企业都比较繁琐,且合并过程中也有一些风险,因此就想到用这种热更新的模式,开源版与企业版的代码一致,但是企业版可以通过js热加载的方式动态导入一些前端页面或者功能用于实现和开源版的差异化,但这部分企业版的功能又不能放在开源代码中,因为直接放的话,有懂前端的开源用户就可以很轻松的破解代码了,因此想到了动态加载js这个方法来注入前端代码,以防止可以很轻易的就破解代码。

在经历调研后,发现微前端这个东西貌似很符合我们的需求,但是因为主项目的版本过于老旧(umi 2.X),并不能适配当下主流的微前端插件,而且有些定制化的需求跟主流微前端的插件又不相吻合,所以,在这个背景之下,我参考借鉴了 kubevela 的插件实现方式,最终使用SystemJS实现了插件功能。

kubevela项目地址:

https://github.com/kubevela/kubevelahttps://github.com/kubevela/kubevela

三、流程概览

(一)子项目改造

子项目的改造就一个重点----打包。因为涉及到很多东西,比如插件元数据设置与拷贝,版本更新,数据传输,以及webpack5打包后的文件类型等,都有许多讲究。

(二)npm包

制作插件需要一个npm 依赖包作为数据传递的中转站,子项目打包后会返回一个函数,这个函数在主项目中也有引用,通过system.js导入子项目后,在主项目调用该函数,即可获取子项目中传入的数据,也就是react的组件。从而串通整个流程。

(三)system.js插件封装

当你完成子项目与npm后,就需要封装system.js用来将子项目打包后的代码动态导入到主项目里。

(四)主项目改造

主项目的改动其实就比较小了,安装system.js与自己制作的npm依赖,再划定一些路由用于渲染该插件,主项目的使命就算完成了。

四、子项目改造

(一)子项目创建

这一步很好理解,使用脚手架创建一个react应用,然后再针对于react应用进行webpack5的打包改造。或者直接从github上下载一个使用webpack5打包的react模板。

package.json文件,这里面有我所有依赖的版本信息,需要的可自行查阅。

javascript 复制代码
{
  "name": "plugin-template",
  "version": "1.0.0",
  "description": "插件体系模板文件",
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack serve --config ./build/webpack.dev.config.js --open",
    "build": "cross-env NODE_ENV=production webpack --config ./build/webpack.prod.config.js",
    "build:report": "cross-env NODE_ENV=production REPORT=true webpack --config ./build/webpack.prod.config.js",
    "eslint:init": "eslint --init",
    "prepare": "husky install",
    "lint-staged": "lint-staged",
    "commitlint": "commitlint -e",
    "eslint:fix": "eslint --ext .js,.vue --fix src"
  },
  "keywords": [],
  "author": "xuzhonglin12138",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.15.5",
    "@babel/plugin-transform-runtime": "^7.15.0",
    "@babel/preset-env": "^7.15.6",
    "@babel/preset-react": "^7.14.5",
    "@commitlint/cli": "^13.2.1",
    "@commitlint/config-conventional": "^13.2.0",
    "babel-loader": "^8.2.2",
    "babel-plugin-import": "^1.13.8",
    "cache-loader": "^4.1.0",
    "copy-webpack-plugin": "^10.0.0",
    "core-js": "3",
    "cross-env": "^7.0.3",
    "css-loader": "^6.3.0",
    "dart-sass": "^1.25.0",
    "eslint": "^8.1.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-react": "^7.26.1",
    "eslint-plugin-react-hooks": "^4.2.0",
    "eslint-webpack-plugin": "^3.0.1",
    "html-webpack-plugin": "^5.3.2",
    "http-proxy-middleware": "^3.0.0",
    "husky": "^7.0.4",
    "less": "^4.2.0",
    "less-loader": "^12.2.0",
    "lint-staged": "^11.2.4",
    "mini-css-extract-plugin": "1.6.2",
    "postcss": "^8.3.8",
    "postcss-loader": "^6.1.1",
    "postcss-preset-env": "^6.7.0",
    "prettier": "^2.4.1",
    "replace-in-file-webpack-plugin": "^1.0.6",
    "sass-loader": "^12.1.0",
    "style-loader": "^3.3.0",
    "terser-webpack-plugin": "^5.3.10",
    "thread-loader": "^3.0.4",
    "webpack": "^5.53.0",
    "webpack-bundle-analyzer": "^4.4.2",
    "webpack-cli": "^4.8.0",
    "webpack-dev-server": "^4.2.1",
    "webpack-merge": "^5.8.0"
  },
  "dependencies": {
    "@ant-design/icons": "^5.5.1",
    "@babel/runtime": "^7.15.4",
    "@babel/runtime-corejs3": "^7.15.4",
    "@swc/core": "^1.7.6",
    "antd": "5.19.3",
    "axios": "^0.24.0",
    "js-cookie": "^3.0.5",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-intl-universal": "^2.11.3",
    "swc-loader": "^0.2.6",
    "xu-demo-data": "3.0.1"
  }
}

子项目改造的最重要的一点就是改造打包入口文件。如下:

左侧是打包的入口,右侧是本地启动的入口,可以看到左侧没有输出点,而是通过插件 xu-demo-data 调用了一个类里的方法,将两个组件以参数的形式传入了这个类中。右侧则是正常react应用的入口文件。至于这个 RainbondRootPagePlugin是什么,咱们暂时按下不表。后面会提到。

(二)webpack5设置

webpack5的配置和其他项目的配置其实大同小异。如果对webpack5不了解的可以看我另一篇文章,了解一些基本的配置信息。

webpack的基本介绍与使用-CSDN博客文章浏览阅读1.6k次,点赞28次,收藏30次。Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。_webpackhttps://blog.csdn.net/qq_45799465/article/details/140628293?spm=1001.2014.3001.5502

javascript 复制代码
//文件名 webpack.base.config.js


const path = require('path')
const webpack = require('webpack')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const ESLintPlugin = require('eslint-webpack-plugin')
const TerserPlugin = require("terser-webpack-plugin");
const { getEntryFile, getPluginId } = require('./utils');

const NODE_ENV = process.env.NODE_ENV


module.exports = {
  entry: getEntryFile(),
  context: path.join(process.cwd(), 'src'),
  // cache: {
  //   type: 'filesystem',
  //   buildDependencies: {
  //     config: [__filename],
  //   },
  // },
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|svg)$/,
        exclude: /node_modules/,
        type: 'asset/resource',
        generator: NODE_ENV === 'development' ? {} : {
          publicPath: `plugins/${getPluginId()}/img/`,
          outputPath: 'img/',
          filename: NODE_ENV === 'development' ? '[hash][ext]' : '[name][ext]',
        },
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/,
        exclude: /node_modules/,
        type: 'asset/resource',
        generator: NODE_ENV === 'development' ? {} : {
          publicPath: `plugins/${getPluginId()}/fonts`,
          outputPath: 'fonts/',
          filename: NODE_ENV === 'development' ? '[hash][ext]' : '[name][ext]',
        },
      },
      {
        exclude: /(node_modules)/,
        test: /\.[jt]sx?$/,
        use: [{
          loader: 'swc-loader',
          options: {
            jsc: {
              baseUrl: path.resolve(__dirname, 'src'),
              target: 'es2015',
              loose: false,
              parser: {
                syntax: 'ecmascript', 
                jsx: true, 
                decorators: false,
                dynamicImport: true,
              },
            },
          },
        },
        'babel-loader'
      ]
      }
    ]
  },
  plugins: [
    new ESLintPlugin({
      extensions: ['js', 'jsx']
    }),
    new webpack.ProgressPlugin(),
  ],
  resolve: {
    alias: {
      "@": path.join(__dirname, '..', 'src')
    },
    extensions: ['.js', '.jsx', '.json'],
    modules: [path.resolve(process.cwd(), 'src'), 'node_modules'],
    unsafeCache: true,
  },
  optimization: {
    runtimeChunk: false,
    minimize: true,
    minimizer: [new TerserPlugin({
      extractComments: false
    })],

  }
}
javascript 复制代码
//webpack.prod.config.js

const path = require('path')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.config')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ReplaceInFileWebpackPlugin = require('replace-in-file-webpack-plugin');
const { hasReadme, getPackageJsonInfo, getPluginId, getTodayDate } = require('./utils');

const REPORT = process.env.REPORT
const prodConfig = {
  mode: 'production',
  devtool: 'eval-source-map',
  output: {
    clean: true,
    path: path.join(__dirname, '..', 'dist'),
    filename: '[name].js',
    library: {
      type: 'amd',
    },
  },
  module: {
    rules: [{
      test: /\.less$/,
      exclude: /node_modules/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            modules: true
          }
        },
        'postcss-loader',
        'less-loader'
      ]
    }]
  },
  externals: [
    'lodash',
    'moment',
    'react',
    'react-dom',
    'xu-demo-data'
  ],
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
    }),
    new CopyWebpackPlugin({
      patterns: [
        { from: hasReadme() ? 'README.md' : '../README.md', to: '.', force: true },
        { from: 'pluginData.json', to: '.' },
        { from: '../LICENSE', to: '.', noErrorOnMissing: true },
        { from: '../CHANGELOG.md', to: '.', force: true, noErrorOnMissing: true },
        { from: '**/*.json', to: '.' }
      ],
    }),
    new ReplaceInFileWebpackPlugin([
      {
        dir: 'dist',
        files: ['pluginData.json', 'README.md'],
        rules: [
          {
            search: /\%VERSION\%/g,
            replace: getPackageJsonInfo().version,
          },
          {
            search: /\%TODAY\%/g,
            replace: getTodayDate(),
          },
          {
            search: /\%PLUGIN_ID\%/g,
            replace: getPluginId(),
          },
        ],
      },
    ]),
  ]
}

if (REPORT) {
  prodConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = merge(baseConfig, prodConfig)

这基本都是一些基础的配置,用到了一些插件,以及一些方法在后面我都会贴出来,大家看看大概就好,这里唯一需要注意的是:

javascript 复制代码
  output: {
    clean: true,
    path: path.join(__dirname, '..', 'dist'),
    filename: '[name].js',
    library: {
      type: 'amd',
    },
  },

ouput 选项中,有个library 选项,type选择 amd格式 :

library: { type: 'amd' }:配置 Webpack 将输出的文件打包为 AMD(异步模块定义)模块。这种模块格式通常用于浏览器端的 JavaScript 应用,能够异步加载依赖项。

(三)其他

webpack的 externals选项就是打包时排除某些依赖,对于子项目来说,要尽可能的减少依赖,以减小打包体积,而且子项目是与主项目共用某些依赖的,因此,这些依赖都可以排除打包,然后在使用 system.js 进行动态导入。(后续会在system.js 插件制作中展示)

javascript 复制代码
  externals: [
    'lodash',
    'moment',
    'react',
    'react-dom',
    'xu-demo-data'
  ],

五、npm包

(一)npm包内容

这就是npm包的全部内容,可以看到我只是定义了两个类,然后 RainbondRootPagePlugin 继承了RainbondOtherPagePlugin ,在类中定义了一些函数,仅此而已。

RainbondRootPagePlugin 也就是子项目打包入口中所使用的那个类。

npm包地址:

xu-demo-data - npm111111. Latest version: 3.0.1, last published: 18 days ago. Start using xu-demo-data in your project by running `npm i xu-demo-data`. There are no other projects in the npm registry using xu-demo-data.https://www.npmjs.com/package/xu-demo-data

javascript 复制代码
export class RainbondOtherPagePlugin {
  constructor() {
    this.meta = {};
    this.OtherPages = undefined;
  }
  addOtherPage(page) {
    this.OtherPages = page;
    return this;
  }
}

export class RainbondRootPagePlugin extends RainbondOtherPagePlugin {
  constructor() {
    super();
    this.root = undefined;
  }
  init(meta) {
  }
  setRootPage(root) {
    this.root = root;
    return this;
  }
}

(二)npm包发布

很简单,初始化,写代码,打包,登录npm账号,上传。具体操作流程可以看我这篇文章,里面有详细的解释

发布NPM包详细流程_npm发包流程-CSDN博客文章浏览阅读500次,点赞7次,收藏11次。首先需要制作一个npm包。按照以下步骤依次执行。相信这一步不需要过多的解释,就是创建了一个文件夹,然后初始化了一下文件夹。然后在生成的package.json文件夹中更改一下自己的配置,例如包名、版本、描述等。然后创建src文件夹,在对应的地方写下你自己的函数即可。_npm发包流程https://blog.csdn.net/qq_45799465/article/details/140853512?spm=1001.2014.3001.5502

六、基于system.js的插件工具封装

(一)封装

javascript 复制代码
import System from 'systemjs/dist/system.js';
import _ from 'lodash';
import moment from 'moment';
import react from 'react';
import * as ReactDom from 'react-dom';
import * as RbdData from 'xu-demo-data';
import { RainbondRootPagePlugin, RainbondEnterprisePagePlugin } from 'xu-demo-data'



export const SystemJS = System;
const cache = {};
const initializedAt = Date.now();


SystemJS.registry.set('plugin-loader', SystemJS.newModule({ locate: locateWithCache }));

SystemJS.config({
  baseURL: '/public',
  defaultExtension: 'js',
  packages: {
    plugins: {
      defaultExtension: 'js',
    },
  },
  meta: {
    '/*': {
      esModule: true,
      authorization: false,
      loader: 'plugin-loader',
    }
  },
});


export function exposeToPlugin(name, component) {
  SystemJS.registerDynamic(name, [], true, function (require, exports, module) {
    module.exports = component;
  });
}
exposeToPlugin('lodash', _);
exposeToPlugin('moment', moment);
exposeToPlugin('react', react);
exposeToPlugin('react-dom', ReactDom);
exposeToPlugin('xu-demo-data', RbdData);

export async function importPluginModule(meta, regionName) {
  const path = `/console/regions/${regionName}/static/plugins/${meta.name}`
  const module = await SystemJS.import(path);
  return module
}

export async function importAppPagePlugin(meta, regionName, type) {
  const xu = await importPluginModule(meta, regionName).then(function (pluginExports) {
    const plugin = pluginExports.plugin ? (pluginExports.plugin) :type == 'enterprise' ? new RainbondEnterprisePagePlugin() : new RainbondRootPagePlugin();
    plugin.init(meta);
    plugin.meta = meta;
    return plugin;
  });
  return xu
}

export function locateWithCache(load, defaultBust = initializedAt) {
  const { address } = load;
  const path = extractPath(address);
  if (!path) {
    return `${address}?_cache=${defaultBust}`;
  }
  const version = cache[path];
  const bust = version || defaultBust;
  return `${address}?_cache=${bust}`;
}

function extractPath(address) {
  const match = /\/public\/(plugins\/.+\/module)\.js/i.exec(address);
  if (!match) {
    return;
  }
  const [_, path] = match;
  if (!path) {
    return;
  }
  return path;
}

这里有几个重点:

首先,exposeToPlugin 这个函数的作用就是动态导入一些依赖,这也正好和子项目打包中排除的那些依赖相对应,也就相当于子项目用的依赖是和主项目一致的。

也就是说子项目和主项目的这些 共用的依赖的版本要一致 !!!

javascript 复制代码
export function exposeToPlugin(name, component) {
  SystemJS.registerDynamic(name, [], true, function (require, exports, module) {
    module.exports = component;
  });
}
exposeToPlugin('lodash', _);
exposeToPlugin('moment', moment);
exposeToPlugin('react', react);
exposeToPlugin('react-dom', ReactDom);
exposeToPlugin('xu-demo-data', RbdData);

其次,importPluginModule 函数调用的 SystemJS.import 就是引入js文件的关键函数。而这个path,指向的就是线上地址的打包后的入口文件地址。 如果对 SystemJS不了解的同学可以去看看我上面贴的SystemJS的github地址。

javascript 复制代码
export async function importPluginModule(meta, regionName) {
  const path = `/console/regions/${regionName}/static/plugins/${meta.name}`
  const module = await SystemJS.import(path);
  return module
}

export async function importAppPagePlugin(meta, regionName, type) {
  const xu = await importPluginModule(meta, regionName).then(function (pluginExports) {
    const plugin = pluginExports.plugin ? (pluginExports.plugin) :type == 'enterprise' ? new RainbondEnterprisePagePlugin() : new RainbondRootPagePlugin();
    plugin.init(meta);
    plugin.meta = meta;
    return plugin;
  });
  return xu
}

(二)使用

导出,然后使用就可以了。

javascript 复制代码
import { importAppPagePlugin } from '../../utils/importPlugins';  

importPlugin = (meta, regionName) => {
    importAppPagePlugin(meta, regionName, 'enterprise').then(res => {
      this.setState({ app: res, pluginLoading: false })
    }).catch(err => {
      this.setState({
        errInfo: err?.response?.data?.message || err?.message || "An unexpected error occurred.",
        pluginLoading: false,
        error: true
      })
    })
  }

七、主项目改造

(一)安装依赖

这里没有什么多说的,安装的 SystemJS 版本以及npm包版本如下:

(二)指定路由

动态路由。

路由代码

RbdPlugins.js

javascript 复制代码
//RbdPlugins.js

import React, { Component } from 'react';
import { Spin, Card, Button } from 'antd';
import { connect } from 'dva';
import { importAppPagePlugin } from '../../utils/importPlugins';
import { getRainbondInfo } from '../../services/api';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import RbdPluginsCom from '../../components/RBDPluginsCom'
import Global from '@/utils/global';
import PluginUtil from '../../utils/pulginUtils';
import styles from './index.less';

@connect(({ user, teamControl, global }) => ({
  user: user.currentUser,
}))
export default class Index extends Component {
  constructor(props) {
    super(props);
    this.state = {
      app: {},
      plugins: {},
      loading: true,
      pluginLoading: true,
      error: false,
      errInfo: '',
    };
  }

  componentDidMount() {
    this.getPluginsList();
  }

  importPlugin = (meta, regionName) => {
    importAppPagePlugin(meta, regionName, 'enterprise').then(res => {
      this.setState({ app: res, pluginLoading: false })
    }).catch(err => {
      this.setState({
        errInfo: err?.response?.data?.message || err?.message || "An unexpected error occurred.",
        pluginLoading: false,
        error: true
      })
    })
  }
  getPluginsList = () => {
    const type = PluginUtil.getCurrentViewPosition(window.location.href);
    type === 'Platform' ? this.loadEnterpriseClusters() : this.loadPluginList();
  };

  loadEnterpriseClusters = () => {
    const { dispatch } = this.props;
    const enterpriseId = Global.getCurrEnterpriseId();

    dispatch({
      type: 'region/fetchEnterpriseClusters',
      payload: { enterprise_id: enterpriseId },
      callback: (res) => {
        if (res.status_code === 200 && res.list?.[0]?.region_name) {
          this.loadPluginList(res.list[0].region_name);
        }
      },
    });
  };

  loadPluginList = (regionName) => {
    const {
      dispatch,
      match,
      isCom,
      user,
    } = this.props;
    let pluginId= ''
    if(isCom){
      pluginId = Global.getComponentPluginType()
    }else{
      pluginId = match.params.pluginId
    }
    const enterpriseId = Global.getCurrEnterpriseId() || user?.enterprise_id;
    const currentRegionName = regionName || Global.getCurrRegionName();
    dispatch({
      type: 'global/getPluginList',
      payload: { enterprise_id: enterpriseId, region_name: currentRegionName },
      callback: (res) => {
        if (res && res.list) {
          const plugin = res.list.find((item) => item.name === pluginId) || {};
          this.setState({ plugins: plugin, loading: false }, () => {
            if (plugin.plugin_type === 'JSInject') {
              this.importPlugin(plugin, currentRegionName);
            }
          });
        }
      },
      handleError: () => {
        this.setState({ plugins: {}, loading: false });
      },
    });
  };
  render() {
    const { plugins, loading } = this.state;
    const {isCom = false} = this.props
    return (
      <>
        {!loading ? (
          isCom ? 
          <RbdPluginsCom {...this.state}/> 
          :
          <PageHeaderLayout title={plugins?.name} content={plugins?.description} pluginSVg={plugins?.icon}>
            <RbdPluginsCom {...this.state}/>
          </PageHeaderLayout>
        ) : (
          <div style={{ width: '100%', height: 500, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
            <Spin size="large" />
          </div>
        )}
      </>
    );
  }
}

RbdPluginsCom.js

javascript 复制代码
import React, { Component } from 'react';
import { Spin, Card, Button } from 'antd';
import Result from '../Result';
import PluginsUtiles from '../../utils/pulginUtils'
import Global from '../../utils/global'
import styles from './index.less';


export default class index extends Component {
  constructor(props) {
    super(props);
    this.state = {
    };
  }
  // 判断是否为多视图插件
  isMultiViewPlugin = () => {
    const { plugins } = this.props;
    const str = PluginsUtiles.isCurrentPluginMultiView(window.location.href, plugins.plugin_views)
    return str
  }

  // 渲染插件
  rbdPluginsRender = () => {
    const { app, plugins, pluginLoading, error, errInfo, dispatch, reduxInfo } = this.props;
    const key = this.isMultiViewPlugin()
    const AppPagePlugin = app[key] ? app[key] : false
    return pluginLoading ? (
      <div style={{ width: '100%', height: 500, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
        <Spin size="large" tip="插件内容加载中..." />
      </div>
    ) : (
      error ? (
        <Card style={{ marginTop: 20 }}>
          <Result
            type="error"
            title='插件加载失败'
            description={`错误信息:${errInfo}`}
            actions={
              <Button onClick={() => { console.log('点了一下'); }}>查看文档</Button>
            }
            style={{
              marginTop: 48,
              marginBottom: 16
            }}
          />,
        </Card>
      ) : (
        AppPagePlugin &&
        <AppPagePlugin
          colorPrimary={Global.getPublicColor('primary-color')}
          currentLocale='en'
        />
      )
    );

  }
  // 渲染iframe
  iframeRender = () => {
    const { app, plugins, pluginLoading, error, errInfo } = this.props;
    return <div style={{ height: '100vh' }}>
      <iframe
        src={plugins?.fronted_path}
        style={{ width: '100%', height: '100%' }}
        id={plugins?.name}
        sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
        scrolling="auto"
        frameBorder="no"
        border="0"
        marginWidth="0"
        marginHeight="0"
      />
    </div>

  }
  render() {
    const { plugins } = this.props;
    return (
      <>
        {
          plugins?.plugin_type === 'JSInject'
            ?
            (
              this.rbdPluginsRender()
            ) : (
              this.iframeRender()
            )
        }
      </>
    )
  }
}

八、实现效果

/console/regions/rainbond/static/plugins/app-view?_cache=1728984496265 这个地址存放的就是子项目打包后的入口文件,这里也成功渲染出来了。

到此为止,所有的流程都已经串通了,插件功能也实现了。

九、开源项目地址

(一)主项目

https://github.com/goodrain/rainbond-ui/tree/V6.0https://github.com/goodrain/rainbond-ui/tree/V6.0

(二)子项目

https://github.com/xuzhonglin12138/plugin-template/tree/mainhttps://github.com/xuzhonglin12138/plugin-template/tree/main

(三)xu-demo-data

xu-demo-data - npm111111. Latest version: 3.0.1, last published: 18 days ago. Start using xu-demo-data in your project by running `npm i xu-demo-data`. There are no other projects in the npm registry using xu-demo-data.https://www.npmjs.com/package/xu-demo-data

相关推荐
数据小爬虫@9 分钟前
如何高效利用Python爬虫按关键字搜索苏宁商品
开发语言·爬虫·python
ZJ_.11 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营15 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Narutolxy16 分钟前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader24 分钟前
全面解析 Golang Gin 框架
开发语言·golang·gin
禁默34 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
Cachel wood41 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端42 分钟前
0基础学前端-----CSS DAY9
前端·css
Code哈哈笑44 分钟前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui