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

news2024/10/19 5:37:24

目录​​​​​​​

 写在前面

一、微前端相关知识

(一)概念

(二) 优势

(三) 缺点

(四)应用场景

(五)现有框架

1. qiankun

2. single-spa

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 - qiankunicon-default.png?t=O83Ahttps://qiankun.umijs.org/zh

2. single-spa

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

        官网地址:

https://zh-hans.single-spa.js.org/docs/getting-started-overviewicon-default.png?t=O83Ahttps://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.icon-default.png?t=O83Ahttps://github.com/systemjs/systemjs

二、需求分析

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

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

kubevela项目地址:

https://github.com/kubevela/kubevelaicon-default.png?t=O83Ahttps://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文件,这里面有我所有依赖的版本信息,需要的可自行查阅。

{
  "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

//文件名 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
    })],

  }
}
//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)

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

  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 插件制作中展示)

  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.icon-default.png?t=O83Ahttps://www.npmjs.com/package/xu-demo-data

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的插件工具封装

(一)封装

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 这个函数的作用就是动态导入一些依赖,这也正好和子项目打包中排除的那些依赖相对应,也就相当于子项目用的依赖是和主项目一致的。

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

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地址。

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
}

(二)使用

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

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  

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

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.0icon-default.png?t=O83Ahttps://github.com/goodrain/rainbond-ui/tree/V6.0

(二)子项目

https://github.com/xuzhonglin12138/plugin-template/tree/mainicon-default.png?t=O83Ahttps://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.icon-default.png?t=O83Ahttps://www.npmjs.com/package/xu-demo-data

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2218266.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MR开发】在Pico设备上接入MRTK3(一)——在Unity工程中导入MRTK3依赖

写在前面的话 在Pico上接入MRTK3&#xff0c;目前已有大佬开源。 https://github.com/Phantomxm2021/PicoMRTK3 也有值得推荐的文章。 MRTK3在PICO4上的使用小结 但由于在MacOS上使用MRTK3&#xff0c;无法通过Mixed Reality Feature Tool工具管理MRTK3安装包。 故记录一下…

Dockerr安装Oracle以及使用DBeaver连接

拉取镜像 pull container-registry.oracle.com/database/free:latest 创建容器 说明一下我现在的最新版本是23 docker run -d --name oracle23i -h xrilang -p 1521:1521 container-registry.oracle.com/database/free:latest 查看日志 docker logs oracle23i 设置密码 因为创建…

登录时用户名密码加密传输(包含前后端代码)

页面输入用户名密码登录过程中&#xff0c;如果没有对用户名密码进行加密处理&#xff0c;可能会导致传输过程中数据被窃取&#xff0c;就算使用https协议&#xff0c;在浏览器控制台的Request Payload中也是能直接看到传输的明文&#xff0c;安全感是否还是不足。 大致流程&a…

第二百八十八节 JPA教程 - JPA查询连接OrderBy示例

JPA教程 - JPA查询连接OrderBy示例 以下代码显示如何使用ORDER BY子句和连接条件。 List l em.createQuery("SELECT e " "FROM Project p JOIN p.employees e " "WHERE p.name :project " "ORDER BY e.name").setParameter("pr…

【MogDB】MogDB5.2.0重磅发布第四篇-支持windows版gsql,兼容sqlplus/sqlldr语法

一、背景 目前仍然很多客户的运维机使用windows操作系统&#xff0c;开发人员也是在windows环境上进行编码测试&#xff0c;甚至还有一些客户管理比较严格&#xff0c;禁止开发人员登录服务器进行操作。在MogDB 5.2.0版本之前&#xff0c;没有提供windows平台的gsql&#xff0…

在龙芯笔记本电脑上安装loongnix20

在龙芯笔记本电脑上安装loongnix20。该笔记本电脑原来安装了统信操作系统20。使用时发现屏幕调到最暗还是偏亮。使用起来不方便。于是安装另外一款操作系统loongnix20。 于是下载loongnix20。下载速度很快。下载以后安装也比较顺利。装好试了一下&#xff0c;屏幕亮度可以调到…

二叉树中的最长交错路径

题目链接 二叉树中的最长交错路径 题目描述 注意点 每棵树最多有 50000 个节点每个节点的值在 [1, 100] 之间起点无需是根节点 解答思路 要找到最长交错路径&#xff0c;首先想到的是深度优先遍历因为起点无需是根节点&#xff0c;所以对于任意一个节点&#xff0c;其可以…

分类任务中评估模型性能的核心指标

在机器学习尤其是分类任务中&#xff0c;Accuracy&#xff08;准确率&#xff09;、Precision&#xff08;精确率&#xff09;、Recall&#xff08;召回率&#xff09;和F1 Score&#xff08;F1分数&#xff09;是评估模型性能的四个核心指标。每个指标都有其独特的含义和用途&…

JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?

大厂面试真题:GC 算法有多少种&#xff1f;各自优缺点是什么&#xff1f;年轻代和年老代选择哪种算法最优&#xff1f; 首先回顾一个图&#xff0c;也就是.class文件的类加载过程&#xff0c;以及线程执行、java内存模型图。看过系列1、2、3三篇文章的同学&#xff0c;大脑先回…

[产品管理-46]:产品组合管理中的项目平衡与管道平衡的区别

目录 一、项目平衡 1.1 概述 1.2 项目的类型 1、根据创新程度和开发方式分类 2、根据产品开发和市场周期分类 3、根据风险程度分类 4、根据市场特征分类 5、根据产品生命周期分类 1.3 产品类型的其他分类 1、按物理形态分类 2、按功能或用途分类 3、按技术或创新程…

大数据研究实训室建设方案

一、概述 本方案旨在提出一套全面的大数据研究实训室建设策略&#xff0c;旨在为学生打造一个集理论学习与实践操作于一体的高端教育环境。实训室将专注于培养学生在大数据处理、分析及应用领域的专业技能&#xff0c;通过先进的设施配置、科学的课程体系和实用的实训模式&…

C++之list(2)

list(2) list的迭代器 const迭代器 根据我们之前学过的知识&#xff1a; const int*p1;//修饰的是指向的内容 int *const p2;//修饰的是迭代器本身我们写const迭代器&#xff0c;期望的是指向的内容不能修改。 所以更期望写上面p1的形式 const迭代器与普通迭代器的不同点在于…

03 设计模式-创造型模式-单例模式

单例模式&#xff08;Singleton Pattern&#xff09;是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。 这种模式涉及到一个单一的类&#xff0c;该类负责创建自己的对象&#xff0c;同时确保只有单个对象被创建…

C语言复习第4章 数组

目录 一、一维数组的创建和初始化1.1数组的创建1.2 变长数组1.3 数组的初始化1.4 全局数组默认初始化为01.5 区分两种字符数组1.6 用sizeof计算数组元素个数1.7 如何访问数组元素1.8 一维数组在内存中的存储(连续存储)1.9 访问数组元素的另一种方式:指针变量1.10 数组越界是运行…

【AI绘画】Midjourney进阶:引导线构图详解

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AI绘画 | Midjourney 文章目录 &#x1f4af;前言&#x1f4af;什么是构图为什么Midjourney要使用构图 &#x1f4af;引导线构图特点使用场景提示词书写技巧测试 &#x1f4af;小结 &#x1f4af;前言 【AI绘画】Midjourney进阶&a…

AnaTraf | TCP重传的工作原理与优化方法

目录 什么是TCP重传&#xff1f; TCP重传的常见触发原因 TCP重传对网络性能的影响 1. 高延迟与重传 2. 吞吐量的下降 如何优化和减少TCP重传 1. 优化网络设备配置 2. 优化网络链路 3. 网络带宽的合理规划 4. 部署CDN和缓存策略 结语 AnaTraf 网络性能监控系统NPM | …

网络最快的速度光速,因此‘‘光网络‘‘由此产生

世界上有一种最快的速度又是光,以前传统以太网络规划满足不了现在的需求。 一 有线网规划 二 无线网规划

如何用pyhton修改1000+图片的名字?

import os oldpath input("请输入文件路径&#xff08;在windows中复制那个图片文件夹的路径就可以):") #注意window系统中的路径用这个‘\分割&#xff0c;但是编程语言中一般都是正斜杠也就是’/‘ #这里写一个代码&#xff0c;将 \ > / path "" fo…

嵌入式职业规划

嵌入式职业规划 在嵌入式的软件开发中&#xff0c;可以分为&#xff1a; 嵌入式MCU软件开发工程师&#xff1b; 嵌入式Linux底层&#xff08;BSP&#xff09;软件开发工程师&#xff1b; 嵌入式Linux应用开发工程师&#xff1b; 嵌入式FPGA算法开发工程师 对于前两个阶段 …

H.266与H.265、AV1、H.264对比

好多开发者希望搞清楚H.266&#xff08;Versatile Video Coding&#xff0c;VVC&#xff09;、H.265&#xff08;High Efficiency Video Coding&#xff0c;HEVC&#xff09;、AV1、H.264&#xff08;Advanced Video Coding&#xff09;四者区别&#xff0c;本文从压缩效率、画…