使用 Rspack 构建 Halo 插件的前端部分
编辑
前情提要
Halo 插件的 UI 部分(Console / UC)的实现方式其实很简单,本质上就是构建一个结构固定的大对象,交给 Halo 去解析,其中包括全局注册的组件、路由定义、扩展点等。 基于这个前提,在实现插件机制时,主要面临的问题就是如何将这个大对象传递给 Halo。当初做了非常多的尝试,最终选择构建为 IIFE(Immediately Invoked Function Expression,立即执行函数),然后 Halo 通过读取 window[PLUGIN_NAME]
(PLUGIN_NAME 即插件名)来获取这个对象。 构建方案采用 Vite,并提供了统一的构建配置。回过头来看,这个方案存在不少问题:
会污染 window 对象,虽然目前并没有出现因为这个导致的问题,但是从长远来看,这个方案并不是最优的。(当然,使用 Rspack 来构建并不是为了解决这个问题)
Vite 不支持 IIFE / UMD 格式的代码分割(主要是 Rollup 还不支持),无法像 ESM(ECMAScript Module)那样实现异步加载模块的机制。
基于第 2 点,如果插件中实现了较多的功能,可能会导致最终产物体积巨大,尤其是当用户安装了过多的插件时,会导致页面加载缓慢。
以 www.halo.run 为例,gzip 之前接近 10M 的 bundle.js,gzip 之后也有 2M - 3M。
以此博客为例,gzip 之后也有 1.8M 的 bundle.js。
基于第 2 点,如果不支持代码分块(Chunk),也无法充分利用资源缓存,访问页面时,也会一次性加载所有插件的代码(即便当前页面不需要)。
基于以上问题,我开始寻找其他替代方案,最终通过翻阅 Rspack(Webpack 的 Rust 实现)的文档发现,Webpack 能够通过配置实现 IIFE 格式的代码分割,最终选择 Rspack 作为尝试。
基本的 Rspack 配置
安装依赖:
pnpm install @rspack/cli @rspack/core vue-loader -D
package.json 添加 scripts:
{
"type": "module",
"scripts": {
"dev": "NODE_ENV=development rspack build --watch",
"build": "NODE_ENV=production rspack build"
}
}
rspack.config.mjs:
import { defineConfig } from '@rspack/cli';
import path from 'path';
import process from 'process';
import { VueLoaderPlugin } from 'vue-loader';
import { fileURLToPath } from 'url';
// plugin.yaml 中的 metadata.name
const PLUGIN_NAME = '<YOUR_PLUGIN_NAME>';
const isProduction = process.env.NODE_ENV === 'production';
const dirname = path.dirname(fileURLToPath(import.meta.url));
// 开发环境启动直接输出到插件项目的 build 目录,无需重启整个插件
// 生产环境输出到插件项目的 src/main/resources/console 目录下
const outDir = isProduction ? '../src/main/resources/console' : '../build/resources/main/console';
export default defineConfig({
mode: process.env.NODE_ENV,
entry: {
// 入口文件,可以参考:https://docs.halo.run/developer-guide/plugin/basics/ui/entry
main: './src/index.ts',
},
plugins: [new VueLoaderPlugin()],
resolve: {
alias: {
'@': path.resolve(dirname, 'src'),
},
extensions: ['.ts', '.js'],
},
output: {
// 资源根路径,加载代码分块(Chunk)的时候,会根据这个路径去加载资源
publicPath: `/plugins/${PLUGIN_NAME}/assets/console/`,
chunkFilename: '[id]-[hash:8].js',
cssFilename: 'style.css',
path: path.resolve(outDir),
library: {
// 将对象挂载到 window 上
type: 'window',
export: 'default',
name: PLUGIN_NAME,
},
clean: true,
iife: true,
},
optimization: {
providedExports: false,
realContentHash: true,
},
experiments: {
css: true,
},
devtool: false,
module: {
rules: [
{
test: /\.ts$/,
exclude: [/node_modules/],
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
},
},
},
type: 'javascript/auto',
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
experimentalInlineMatchResource: true,
},
},
],
},
// 这部分依赖已经由 Halo 提供,所以需要标记为外部依赖
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
'@vueuse/core': 'VueUse',
'@vueuse/components': 'VueUse',
'@vueuse/router': 'VueUse',
'@halo-dev/console-shared': 'HaloConsoleShared',
'@halo-dev/components': 'HaloComponents',
'@halo-dev/api-client': 'HaloApiClient',
'@halo-dev/richtext-editor': 'RichTextEditor',
axios: 'axios',
},
});
配置需要懒加载的路由或者组件:
在 index.ts 中配置路由:
import { definePlugin } from '@halo-dev/console-shared';
import { defineAsyncComponent } from 'vue';
import { VLoading } from '@halo-dev/components';
import 'uno.css';
-import DemoPage from './views/DemoPage.vue';
export default definePlugin({
routes: [
{
parentName: 'Root',
route: {
path: 'demo',
name: 'DemoPage',
- component: DemoPage,
+ component: defineAsyncComponent({
+ loader: () => import('./views/DemoPage.vue'),
+ loadingComponent: VLoading,
+ }),
...
},
},
],
extensionPoints: {},
});
注:推荐使用 defineAsyncComponent
包裹,而不是直接使用 () => import()
的方式,后者会在进入路由之前就开始加载页面的代码分块(Chunk),导致页面在加载期间没有任何响应。
构建产物示例:
❯ ll src/main/resources/console
.rw-r--r-- 191k ryanwang staff 16 Jun 10:47 359-3bebb968.js
.rw-r--r-- 83k ryanwang staff 16 Jun 10:47 962-3bebb968.js
.rw-r--r-- 4.1k ryanwang staff 16 Jun 10:47 main.js
其他配置
集成 Scss / Sass
安装依赖:
pnpm install sass-embedded sass-loader -D
rspack.config.mjs 添加配置:
import { defineConfig } from '@rspack/cli';
import path from 'path';
import process from 'process';
import { VueLoaderPlugin } from 'vue-loader';
import { fileURLToPath } from 'url';
+import * as sassEmbedded from "sass-embedded";
...
export default defineConfig({
...
module: {
rules: [
...
+ {
+ test: /\.(sass|scss)$/,
+ use: [
+ {
+ loader: "sass-loader",
+ options: {
+ api: "modern-compiler",
+ implementation: sassEmbedded,
+ },
+ },
+ ],
+ type: "css/auto",
+ },
],
},
...
});
集成 UnoCSS
如果你习惯使用 TailwindCSS 或者 UnoCSS 来编写样式,可以参考以下配置:
本文推荐使用 UnoCSS,因为可以利用 UnoCSS 的 transformerCompileClass 来编译样式,预防与 Halo 或者其他插件产生样式冲突。
安装依赖:
pnpm install unocss @unocss/webpack @unocss/eslint-config style-loader css-loader -D
入口文件(src/index.ts)添加导入:
import 'uno.css';
rspack.config.mjs 添加配置:
import { defineConfig } from '@rspack/cli';
import path from 'path';
import process from 'process';
import { VueLoaderPlugin } from 'vue-loader';
import { fileURLToPath } from 'url';
+import { UnoCSSRspackPlugin } from '@unocss/webpack/rspack';
...
export default defineConfig({
...
plugins: [
new VueLoaderPlugin(),
+ UnoCSSRspackPlugin()
],
...
module: {
rules: [
...
+ {
+ test: /\.css$/i,
+ use: ['style-loader', 'css-loader'],
+ type: 'javascript/auto',
+ },
],
},
...
});
uno.config.ts:
import { defineConfig, presetWind3, transformerCompileClass } from 'unocss';
export default defineConfig({
presets: [presetWind3()],
transformers: [transformerCompileClass()],
});
.eslintrc.cjs:
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-recommended',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier',
+ '@unocss',
],
env: {
'vue/setup-compiler-macros': true,
},
+ rules: {
+ "@unocss/enforce-class-compile": 1,
+ },
};
总结
以上就是针对 Halo 插件前端部分的 Rspack 配置。我已经对 Halo 官方维护的部分插件进行了迁移,几乎没有遇到什么问题,并且带来的收益非常明显:www.halo.run 和本博客的 bundle.js 在 gzip 之后仅有不到 200k,各个页面也只会在访问时加载所需的资源。
需要注意的是,我对这些构建工具并不算非常熟悉,所以配置仍然有优化空间。我们会持续优化,后续也会考虑提供一个通用的 CLI 或 Rspack 配置,期望实现如下效果:
rspack.config.mjs:
import { rspackConfig } from '@halo-dev/ui-bundler-kit';
export default rspackConfig({
...
});
或者基于 Rspack 包装一个 CLI:
plugin.config.mjs:
import { defineConfig } from '@halo-dev/ui-bundler-kit';
export default defineConfig({
...
});
package.json:
{
"scripts": {
"dev": "halo-ui dev",
"build": "halo-ui build"
}
}
参考文档
感谢阅读,欢迎交流与指正!
- 2
- 3
-
分享