背景
五月底 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,以下步骤仅供参考,推荐使用新建空插件然后拷贝源码的方式更新。
操作步骤
更新插件自定义弹窗的编译工具
首先更新项目依赖及相关配置文件。
- 安装新依赖:
pnpm install -D @rspack/cli @rspack/plugin-html node-sass sass-loader stylus stylus-loader - 升级 vue-loader 到新版:
pnpm update vue-loader@^17.2.2 - 移除多余依赖,示例指令如:
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 - 移除 babel 配置文件,如
babel.config.js - 按照以下步骤更改 webpack.config.js(包括 build-dialog.js 和 build-dialog-lifecycle.js,可修改 build-dialog.js 然后对照着把代码拷贝到 build-dialog-lifecycle.js)
- 将 npm scripts 中
webpack --config file.js之类的指令改成npx rspack build --config file.js
然后更改编译配置。
- 替换
html-webpack-plugin为@rspack/html-plugin
- const HtmlWebpackPlugin = require("html-webpack-plugin");
+ const htmlPlugin = require("@rspack/plugin-html");
// ...
plugin: [
- new HTMLWebpackPlugin ...
+ new htmlPlugin ...
]
- 替换
webpack.DefinePlugin和webpack.ProvidePlugin为config.builtins.define、config.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 }),
- }),
]
}
- 替换编译目标配置
{
builtins: {
+ presetEnv: {
+ targets: ["Chrome >= 89"],
+ },
},
+ target: ["web", "es2015"],
- target: 'web',
}
- 移除构建缓存配置
{
- cache: {
- type: "filesystem",
- // ...
- },
}
- 增加别名配置
{
resolve: {
alias: {
+ "/src": path.resolve(__dirname, "../../src"),
// ...
},
},
}
- 替换 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_modules 并 pnpm install 重新安装。测试本地开发环境没有问题之后,推到线上验证。
更新插件自身的编译工具
需要更改部分依赖,以及更新 vue-cli 的配置文件(.cybercloud/build/index.js)。
- 替换
copy-webpack-plugin,pnpm 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"),
+ },
+ ],
+ },
+ },
}
}
- 替换
webpack.ProvidePlugin和webpack.DefinePlugin
移除 .cybercloud/build/env.js 中对应插件,并在编译配置中新增 builtins.provide 和 builtins.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
];
},
// ...
}
}
- 增加编译目标配置
// .cybercloud/build/index.js
module.exports = function(mode) {
return {
builtins: {
+ presetEnv: {
+ targets: ["Chrome >= 89"],
+ },
},
+ target: ["web", "es2015"],
}
}
- 替换编译配置中
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,
+ ]
}
}
- 替换编译配置中的
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() : '',
+ }),
],
}
}
- 增加
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",
+ },
+ ],
+ },
}
}
- 更新 HTML 模板文件
// .cybercloud/index.html
- <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
+ <link rel="icon" href="favicon.ico" />
- 根据第二步配置的
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>
- 编译配置的
devServer设置增加historyApiFallback: true
devServer: isProd ? undefined : {
hot: true,
port: 8080,
+ historyApiFallback: true
}
- 替换
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",
}
}
- 移除不相关依赖:
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 重新安装。测试本地开发环境没有问题之后,推到线上验证。
配置插件外部依赖
- 删除预构建文件:
.cybercloud/build/prestage.js、.cybercloud/.plugin-meta.json - 移除
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"
},
}
- 移除项目编译设置中关于预构建的代码段以及工具函数
// .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,
}
- 替换编译配置中的
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",
}
- 修改 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_modules 并pnpm 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脚手架工具包,协助插件或其它项目维护依赖,处理项目编译、发包等杂项的能力。