背景

五月底 W 集中发版时在前端插件碰到了包括依赖下载失败超时和 Runner 内存超限被杀问题。其中 Runner 被杀的问题我们尝试过修改 NodeJS 内存限制以及增加预构建步骤,但是没能解决。在给定服务器资源下,为了从根本上解决之类资源不足问题,需要各插件负责人对插件编译工具链进行更新。

此次迁移比较简单,按照步骤即可完成,首个插件预计耗时 1~2 个小时。后续平台可能提供脚手架工具来处理插件模板更新等杂项事务。

结果

inventorytransfertask-web 为例,迁移到 Rspack 后,编译弹窗时,内存占用峰值从 4.7GB 降低到 1.3GB,降低到原先的 27%;编译速度从 132s 降低到 16s,速度提升 8 倍。

eap-web 为例,编译时间从 1 分钟降低到 5s。

测试机器配置如下。

Hostname: Lionad-GS76
Platform: win32 x64
Release:  10.0.22623
Uptime:   84:48
Total RAM: 63.71 GB
CPU Model: 11th Gen Intel(R) Core(TM) i9-11900H @ 2.50GHz
CPU Clock: 2496 MHZ
CPU Cores: 16 cores

目标、收益和风险

操作步骤分三步,可根据收益和风险选择特定的步骤操作。如果对操作冗长的步骤没有把握,可选择将业务代码复制到一个空插件模板,在空模板中调试好后,再覆盖回原先代码库。从 2023/06/08 之后新建的插件,默认完成了这三个步骤。

  • 步骤 1,更新插件自定义弹窗的编译工具:为了降低前端插件编译的消耗,所有插件应至少完成步骤 1。视弹窗数量获得不同的编译速度提升,在 inventorytransfertask-web 插件中,4 个弹窗迁移后,能获得 3 分钟的收益,空弹窗的编译产物从 4MB 降低至 20KB。

  • 步骤 2,更新插件自身的编译工具:进一步提高编译速度。收益视项目大小和复杂程度而定,预计在较小型的项目能获得 30~60s 收益。

  • 步骤 3,设置插件外部依赖:将编译速度提高到常数级。在 eap-web 项目中,编译时间消耗从 1 分钟降低到 5s。

其中,步骤 2 的操作难度高于步骤 1,如果项目代码和依赖比较复杂则不建议迁移;步骤 3 会将插件内的 Vue、Element Plus、CyberCloud UI 等依赖替换成平台提供的最新版本,所以风险主要来源于 Vue、Element Plus、CyberCloud UI 的 API 变化,一般情况下无风险。

完成迁移需要花费约** 30 分钟**,如果需要完成步骤 2 和步骤 3,需要预留 1 ~ 2 个小时的时间用于解决可能出现的适配问题。

操作步骤完成后,配合周四(2023/06/07)发布的新插件 CI 模板,可以将一般插件项目(如 eap-web)CI 速度缩短到一分钟以内。前端插件发版速度将大幅提高。

迁移前注意事项

查看项目内 package.json 中 CYBER_PLUGIN_TEMPLATE_VERSION 字段的值。它代表当前项目使用的插件模板的版本。

如果插件版本低于 1.2.8,可能需要对相关代码做稍许调整。如果插件版本低于 1.2.6,以下步骤仅供参考,推荐使用新建空插件然后拷贝源码的方式更新。

操作步骤

更新插件自定义弹窗的编译工具

首先更新项目依赖及相关配置文件。

  1. 安装新依赖:pnpm install -D @rspack/cli @rspack/plugin-html node-sass sass-loader stylus stylus-loader
  2. 升级 vue-loader 到新版:pnpm update vue-loader@^17.2.2
  3. 移除多余依赖,示例指令如:pnpm uninstall @babel/core @babel/eslint-parser @babel/plugin-proposal-nullish-coalescing-operator @babel/plugin-proposal-optional-chaining @vue/cli-plugin-babel babel-loader thread-loader ts-loader webpack webpack-cli webpack-merge ignore-loader core-js css-loader html-webpack-plugin ignore-loader core-js css-loader html-webpack-plugin progress-webpack-plugin readline @vue/compiler-sfc style-loader
  4. 移除 babel 配置文件,如 babel.config.js
  5. 按照以下步骤更改 webpack.config.js(包括 build-dialog.js 和 build-dialog-lifecycle.js,可修改 build-dialog.js 然后对照着把代码拷贝到 build-dialog-lifecycle.js)
  6. 将 npm scripts 中webpack --config file.js 之类的指令改成npx rspack build --config file.js

然后更改编译配置。

  1. 替换 html-webpack-plugin@rspack/html-plugin
- const HtmlWebpackPlugin = require("html-webpack-plugin");
+ const htmlPlugin = require("@rspack/plugin-html");

// ...
plugin: [
- new HTMLWebpackPlugin ...
+ new htmlPlugin ...
]
  1. 替换 webpack.DefinePluginwebpack.ProvidePluginconfig.builtins.defineconfig.builtins.provide 配置项
- const webpack = require("webpack");

+ const toDefineEnvConfig = { ...dot.parsed, ...EnvConfig };

// your webpack config
{
  // ...
+ builtins: {
+   define: {
+     CYBER_ENV: JSON.stringify(toDefineEnvConfig),
+     "CYBER_ENV.REQUEST_TIMEOUT": JSON.stringify(toDefineEnvConfig.REQUEST_TIMEOUT),
+     "CYBER_ENV.REQUEST_HEADERS": JSON.stringify(toDefineEnvConfig.REQUEST_HEADERS),
+   },
+   provide: {
+     cyber: [path.resolve(__dirname, "../lib"), "default"],
+   },
+ },
  plugins: [
-   new webpack.ProvidePlugin({
-     cyber: [path.resolve(__dirname, "../lib"), "default"],
-   }),
-   new webpack.DefinePlugin({
-     CYBER_ENV: JSON.stringify({ ...dot.parsed, ...EnvConfig }),
-   }),
  ]
}
  1. 替换编译目标配置
{
   builtins: {
+    presetEnv: {
+      targets: ["Chrome >= 89"],
+    },
   },
+  target: ["web", "es2015"],
-  target: 'web',
}
  1. 移除构建缓存配置
{
-  cache: {
-    type: "filesystem",
-    // ...
-  },
}
  1. 增加别名配置
{
  resolve: {
    alias: {
+     "/src": path.resolve(__dirname, "../../src"),
      // ...
    },
  },
}
  1. 替换 Rules,如果没有特殊需求可用以下 Rules 覆盖原先的 Rules
{
  rules: [
    {
      test: /\.vue$/,
      loader: "vue-loader",
      options: {
      experimentalInlineMatchResource: true,
      },
    },
    {
      test: /\.less$/,
      loader: "less-loader",
      type: "css",
    },
    {
      test: /\.s[ca]ss$/,
      use: [
       {
         loader: "sass-loader",
         options: {},
       },
      ],
      type: "css",
    },
    {
      test:/\.styl(us)?$/,
      loader:"stylus-loader",
      type:"css",
    },
    {
      test: /\.(png|jpe?g|gif|webp|ttf)(\?.*)?$/i,
      type: "asset",
    },
    {
      test: /\.svg/,
      type: "asset/resource",
    },
  ],
}

最后,npx rimraf node_modules 删除 node_modulespnpm install 重新安装。测试本地开发环境没有问题之后,推到线上验证。

更新插件自身的编译工具

需要更改部分依赖,以及更新 vue-cli 的配置文件(.cybercloud/build/index.js)。

  1. 替换 copy-webpack-pluginpnpm uninstall copy-webpack-plugin

.cybercloud/build/lifecycle.js 中用到了 copy-webpack-plugin,需要移除;在编译配置中增加 builtins.copy 配置。

// .cybercloud/build/lifecycle.js
- const CopyPlugin = require("copy-webpack-plugin");

module.exports = ( mode ) => {
  return {
    plugins : [
-     new CopyPlugin([{ from : path.resolve(__dirname , '../manifest.json') }])
    ]
  }
}
// .cybercloud/build/index.js
module.exports = ( mode ) => {
  return {
+   builtins: {
+     copy: {
+       patterns: [
+         {
+           from: path.resolve(__dirname, "../manifest.json"),
+         },
+       ],
+     },
+   },
  }
}
  1. 替换 webpack.ProvidePluginwebpack.DefinePlugin

移除 .cybercloud/build/env.js 中对应插件,并在编译配置中新增 builtins.providebuiltins.define 配置。

// .cybercloud/build/env.js
- const webpack = require("webpack");
const path = require("path");
const dotenv = require("dotenv");
const dot = dotenv.config({ path: path.resolve(__dirname, "../meta") });
const EnvConfig = require("../config.js");

+ const toDefineEnvConfig = { ...dot.parsed, ...EnvConfig };

module.exports = (mode) => {
  return {
-   plugins: [
-     new webpack.ProvidePlugin({
-       cyber: [path.resolve(__dirname, "../lib"), "default"],
-     }),
-     new webpack.DefinePlugin({
-       CYBER_ENV: JSON.stringify({ ...dot.parsed, ...EnvConfig }),
-     }),
-   ],
+     define: {
+       CYBER_ENV: JSON.stringify(toDefineEnvConfig),
+       "CYBER_ENV.REQUEST_TIMEOUT": JSON.stringify(toDefineEnvConfig.REQUEST_TIMEOUT),
+       "CYBER_ENV.REQUEST_HEADERS": JSON.stringify(toDefineEnvConfig.REQUEST_HEADERS),
+     },
+     provide: {
+       cyber: [path.resolve(__dirname, "../lib"), "default"],
+     },
  };
};
// .cybercloud/build/index.js
module.exports = function(mode) {
- const { plugins } = env(mode);
+ const { define, provide } = env(mode);
  // ...
  return {
    builtins: {
+     define,
+     provide,
    },
    configureWebpack(config) {
      config.plugins = [
        ...config.plugins,
-       ...plugins,
        ...lifecyclePlugins
      ];
    },
    // ...
  }
}
  1. 增加编译目标配置
// .cybercloud/build/index.js

module.exports = function(mode) {
  return {
    builtins: {
+     presetEnv: {
+       targets: ["Chrome >= 89"],
+     },
    },
+   target: ["web", "es2015"],
  }
}
  1. 替换编译配置中 configureWebpack 配置,并补充一些如 output 等常见配置
const entryPath = path.resolve(__dirname, "../index.js");

module.exports = function(mode) {
+ const isProd = mode === "production";

  const alias = {
+   "@": path.resolve(__dirname, "../../src"),
    // other
  ;

  return {
-   configureWebpack(config) {
-     config.resolve.alias = Object.assign({}, config.resolve.alias, alias);
-     config.plugins = [...config.plugins, ...plugins, ...lifecyclePlugins];
-     config.externals = Object.assign({}, config.externals, externals);
-     config.entry.app[0] = entryPath;
-   },
+   entry: entryPath,
+   output: isProd ? {
+     clean: true,
+     publicPath: "./",
+     filename: "[name].[contenthash:8].bundle.js",
+     assetModuleFilename: "[name].[contenthash:8].[ext]",
+   } : {
+     clean: true,
+     publicPath: "/",
+   },
+   devtool: isProd ? false : "cheap-source-map",
-   productionSourceMap: false,
+   optimization: {
+     realContentHash: false,
+   },
+   externals,
+   resolve: {
+     alias,
+   },
+   plugins: [
+     ...lifecyclePlugins,
+   ]
  }
}
  1. 替换编译配置中的 chainWebpack 配置
+ const htmlPlugin = require("@rspack/plugin-html");
 
const htmlPath = path.resolve(__dirname, "../index.html");

module.exports = function(mode) {
  return {
-   chainWebpack(config) {
-     config.plugin("html").tap((args) => {
-       args[0].title = config.title || "插件模板";
-       args[0].template = htmlPath;
-       args[0].styles = isProd ? getStylesCode() : "";
-       args[0].scripts = isProd ? getScriptBlockCode() : "";
-       return args;
-     });
-   },
    plugins: [
      ...lifecyclePlugins,
+     new htmlPlugin({
+       title: "插件模板",
+       template: htmlPath,
+       styles: isProd ? getStylesCode() : "",
+       scripts: isProd ? getScriptBlockCode() : '',
+     }),
    ],
  }
}
  1. 增加 vue-loader 及常见 Rules
+ const { VueLoaderPlugin } = require("vue-loader");

module.exports = function(mode) {
  return {
    plugins: [
+     new VueLoaderPlugin(),
      // ...
    ],
+   module: {
+     rules: [
+       {
+         test: /\.vue$/,
+         loader: "vue-loader",
+         options: {
+           experimentalInlineMatchResource: true,
+         },
+       },
+       {
+         test: /\.less$/,
+         loader: "less-loader",
+         type: "css",
+       },
+       {
+         test: /\.s[ca]ss$/,
+         use: [
+           {
+             loader: "sass-loader",
+             options: {},
+           },
+         ],
+         type: "css",
+       },
+       {
+         test:/\.styl(us)?$/,
+         loader:"stylus-loader",
+         type:"css",
+       },
+       {
+         test: /\.(png|jpe?g|gif|webp|ttf)(\?.*)?$/i,
+         type: "asset",
+       },
+       {
+         test: /\.svg/,
+         type: "asset/resource",
+       },
+     ],
+   },
  }
}
  1. 更新 HTML 模板文件
// .cybercloud/index.html
- <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
+ <link rel="icon" href="favicon.ico" />
  1. 根据第二步配置的 builtins.provide,更改代码写法。

以下面的 provide 配置项举例。它定义了全局变量 cyber

{
  builtins: {
    provide: {
      cyber: [require.resolve("../lib/index.js"), "default"],
    },
  },
}

在模板默认的 App.vue 文件中,能看到在 setup 函数中直接返回了 cyber 给模板使用。类似的代码会在编译时报错。

<template>
 <component :fetch="cyber.request" />
</template>

<script lang="ts">
export default {
 setup() {
   return {
     cyber,
    };
  },
};
</script>

这是基础库的问题,估计会在 rspack@0.2.2 后解决,见 #3486。如果你没有碰到此报错,可以忽略这个步骤,不然需要改成以下形式,才能通过编译。

<template>
 <component :fetch="cyber.request" />
</template>

<script lang="ts">
export default {
 setup() {
 
   // add this line
   const cyberRef =cyber;// ! DO NOT remove this line

   return {
     cyber:cyberRef,
    };
  },
};
</script>
  1. 编译配置的 devServer 设置增加 historyApiFallback: true
devServer: isProd ? undefined : {
  hot: true,
  port: 8080,
+ historyApiFallback: true
}
  1. 替换 npm scripts 中关于 vue-cli-service 的指令
{
  "scripts": {
-   "serve": "vue-cli-service serve",
-   "build": "yarn prestage && vue-cli-service build && yarn build:expose",
+   "serve": "npx rspack serve --config vue.config.js",
+   "build": "npm run prestage && npx rspack build --config vue.config.js && npm run build:expose",
  }
}
  1. 移除不相关依赖:pnpm uninstall @vue/cli-plugin-eslint @vue/cli-plugin-router @vue/cli-plugin-typescript @vue/cli-service 可能要安装部分新依赖:pnpm install node-sass sass-loader stylus stylus-loader

最后,npx rimraf node_modules 删除 node_modules 并pnpm install 重新安装。测试本地开发环境没有问题之后,推到线上验证。

配置插件外部依赖

  1. 删除预构建文件:.cybercloud/build/prestage.js.cybercloud/.plugin-meta.json
  2. 移除 npm scripts 中关于预构建的代码
// package.json
{
  "scripts": {
-   "build": "yarn prestage && npx rspack build --config vue.config.js && yarn build:expose",
+   "build": "npx rspack build --config vue.config.js && npm run build:expose",
-   "prestage": "node ./.cybercloud/build/prestage.js"
  },
}
  1. 移除项目编译设置中关于预构建的代码段以及工具函数
// .cybercloud/config.js
module.exports = {
- LIBRARY: [
-   // ...
- ]
- IGNORE_MODULES_IN_PROD: [
-   // ...
- ]
}
// .cybercloud/build/utils.js

- let metaJson;
- try {
-   metaJson = require("../.plugin-meta.json");
- } catch (error) {}

- function getStylesCode() {
-  // ...
- }

- function getScriptBlockCode() {
-  // ...
- }

module.exports = {
-  metaJson,
-  getScriptBlockCode,
-  getStylesCode,
}
  1. 替换编译配置中的 externals 设置,.cybercloud/build/build-dialog.js.cybercloud/build/build-dialog-lifecycle.js.cybercloud/build/index.js 三个文件分别对应插件弹窗、插件弹窗生命周期钩子函数和插件项目的编译配置,三个文件可以选择性改或都改。修改的内容如下示例,根据实际需要作调整。
// .cybecloud/build/index.js

- const { metaJson, getScriptBlockCode, getStylesCode } = require("./utils");

module.exports = function (mode) {
  if (isProd) {
    externals = {
+       "@cybercloud/ui/cyber-lib": "CyberLibrary",
+       "@cybercloud/ui/helper-lib": "CybercloudHelper",
+       "@cybercloud/ui": "CybercloudUI",
+       vue: "Vue",
+       "vue-router": "VueRouter",
+       "element-plus": "ElementPlus",
+       "cybercloud-component-x-definition": "CxDefinition",
    };
  }
-    if (metaJson && metaJson.includes) {
-       // ...
-    }

  return {
    plugins: [
        new htmlPlugin({
          title: "插件模板",
          template: htmlPath,
-           styles: isProd ? getStylesCode() : "",
-           scripts: isProd ? getScriptBlockCode() : "",
        }),
      ],
  }
}
// .cybercloud/build/build-dialog.js 和 .cybercloud/build/build-dialog-lifecycle.js
// 弹窗目前不涉及本地预览,所以不需要区分 prod 环境
externals = {
+ "@cybercloud/ui/cyber-lib": "CyberLibrary",
+ "@cybercloud/ui/helper-lib": "CybercloudHelper",
+ "@cybercloud/ui": "CybercloudUI",
+ vue: "Vue",
+ "vue-router": "VueRouter",
+ "element-plus": "ElementPlus",
+ "cybercloud-component-x-definition": "CxDefinition",
}
  1. 修改 HTML 模板代码。一是为了防止 Vue、ElementPlus 等依赖出现不兼容问题,二是为了不再使插件受 cybercloud-ui 发版影响。目前只能按照以下脚本 URL 锁定依赖的版本,后续会用其它更摩登也更稳定的办法。
// .cybercloud/build/index.html
<html>
  <head>
-   <%= htmlWebpackPlugin.options.styles %>
-   <%= htmlWebpackPlugin.options.scripts %>
+   <script src="https://cdn.loghub.com/assets/iconfont/cybercloud/latest/index.js"></script>
+   <script src="https://cdn.loghub.com/assets/cybercloud-ui/cyber/latest/index.js"></script>
+   <script src="https://cdn.loghub.com/assets/cybercloud-ui/helper/latest/index.js"></script>
+   <script src="https://cdn.loghub.com/assets/vue/3.3.4/vue.global.prod.js"></script>
+   <script src="https://cdn.loghub.com/assets/vue-router/4.2.2/vue-router.global.prod.js"></script>
+   <script src="https://cdn.loghub.com/assets/vue-demi/0.14.5/index.min.js"></script>
+   <script src="https://cdn.loghub.com/assets/element-plus/2.3.5/index.full.min.js"></script>
+   <script src="https://cdn.loghub.com/assets/cybercloud-ui/latest/index.js"></script>
+   <link href="https://cdn.loghub.com/assets/element-plus/2.3.5/index.min.css" rel="stylesheet"/>
+   <link href="https://cdn.loghub.com/assets/cybercloud-ui/latest/styles/index.css" rel="stylesheet"/>
  </head>
</html>
// .cybercloud/expose/dialog.template.ejs
<html>
  <head>
-   <%= htmlWebpackPlugin.options.styles %>
-   <%= htmlWebpackPlugin.options.scripts %>
+   <script src="https://cdn.loghub.com/assets/iconfont/cybercloud/latest/index.js"></script>
+   <script src="https://cdn.loghub.com/assets/cybercloud-ui/cyber/latest/index.js"></script>
+   <script src="https://cdn.loghub.com/assets/cybercloud-ui/helper/latest/index.js"></script>
+   <script src="https://cdn.loghub.com/assets/vue/3.3.4/vue.global.prod.js"></script>
+   <script src="https://cdn.loghub.com/assets/vue-router/4.2.2/vue-router.global.prod.js"></script>
+   <script src="https://cdn.loghub.com/assets/vue-demi/0.14.5/index.min.js"></script>
+   <script src="https://cdn.loghub.com/assets/element-plus/2.3.5/index.full.min.js"></script>
+   <script src="https://cdn.loghub.com/assets/cybercloud-ui/latest/index.js"></script>
+   <link href="https://cdn.loghub.com/assets/element-plus/2.3.5/index.min.css" rel="stylesheet"/>
+   <link href="https://cdn.loghub.com/assets/cybercloud-ui/latest/styles/index.css" rel="stylesheet"/>
  </head>
</html>

最后,npx rimraf node_modules 删除 node_modulespnpm install 重新安装。测试本地开发环境没有问题之后,推到线上验证。

常见问题及处理方法

更多配置项可参考 Rspack 官网Rspack 从 Webpack 迁移指南Rspack Gitlab Issues

  • 样式文件中引用的语法如果报错,可尝试更改写法
- @import "./index.less";
+ @import url("./index.less");
  • 项目冷启动时间过长(pnpm serve),可尝试关闭 devtools,见 #2180
  • 热更新报错“Multiple assets emit different content to the same filename ...”,可尝试升级 rspack、关闭 realContentHash,见 #3401#3554

[[热更新报错.png]]

后续

  • 之后考虑提供@cybercloud/cli 脚手架工具包,协助插件或其它项目维护依赖,处理项目编译、发包等杂项的能力。
本文最后更新于: June 28 2023 21:04