惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Spread Privacy
Spread Privacy
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy & Cybersecurity Law Blog
L
LINUX DO - 热门话题
T
Threat Research - Cisco Blogs
T
Tenable Blog
TaoSecurity Blog
TaoSecurity Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
AI
AI
P
Proofpoint News Feed
A
About on SuperTechFans
P
Privacy International News Feed
月光博客
月光博客
雷峰网
雷峰网
S
Secure Thoughts
博客园 - 叶小钗
博客园 - 聂微东
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Project Zero
Project Zero
The Cloudflare Blog
SecWiki News
SecWiki News
The Hacker News
The Hacker News
V
Vulnerabilities – Threatpost
罗磊的独立博客
A
Arctic Wolf
阮一峰的网络日志
阮一峰的网络日志
Know Your Adversary
Know Your Adversary
酷 壳 – CoolShell
酷 壳 – CoolShell
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Troy Hunt's Blog
The Last Watchdog
The Last Watchdog
Schneier on Security
Schneier on Security
小众软件
小众软件
有赞技术团队
有赞技术团队
博客园 - 司徒正美
T
Tailwind CSS Blog
量子位
C
Cybersecurity and Infrastructure Security Agency CISA
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hugging Face - Blog
Hugging Face - Blog
人人都是产品经理
人人都是产品经理
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
Security @ Cisco Blogs
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
L
Lohrmann on Cybersecurity

Ryan Wang's Blog

为 Docusaurus 添加 Shiki 代码高亮支持 使用 Meilisearch 提升 Halo 网站的搜索体验 在 Halo 中导入 Markdown 和 Word 文档 通过 1Panel MCP 自动部署静态网站 探讨如何为开源项目提交 issue 归档 - Ryan Wang's Blog 新起点丨开源建站工具 Halo 发布 2.0 版本 Halo 1.5.0 发布 Halo 1.4.10 发布 生态公园 ~ 2021-03-27 Halo 1.4.6 发布 Halo 1.4.3 发布 Halo 1.4.0 发布 分享一个教科书式的提问方式 优雅的让 Halo 支持 webp 图片输出 记录优化 Vue 应用的首次加载速度 博客迁移到 Raspberry Pi Flarum 的安装与配置 使用 Caddy 自动部署 Gridea 博客
使用 Rspack 构建 Halo 插件的前端部分
Ryan Wang · 2025-06-16 · via Ryan Wang's Blog

更新(25-06-19)

现在已经为插件的 UI 部分提供了新的配置方式,https://www.npmjs.com/package/@halo-dev/ui-plugin-bundler-kit 包提供了 rsbuildConfig 方法,可以更加方便的使用 https://rsbuild.dev/ 来构建 UI 部分。

Rsbuild 基于 Rspack 构建,提供了更完善的 loader 配置,所以在封装的时候就直接选择了 Rsbuild。

安装依赖:

pnpm install @halo-dev/ui-plugin-bundler-kit@2.21.1 @rsbuild/core -D

rsbuild.config.mjs:

import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";

export default rsbuildConfig()

package.json 添加 scripts:

{
  "type": "module",
  "scripts": {
    "dev": "rsbuild build --env-mode development --watch",
    "build": "rsbuild build"
  }
}

需要注意的是,为了适应新版的 https://github.com/halo-dev/plugin-starter,默认生产构建输出目录改为了 ui/build/dist ,如果你要从已有的插件项目迁移到 Rsbuild,建议参考 https://github.com/halo-dev/plugin-starter/pull/52 对 Gradle 脚本进行改动,或者自定义 Rsbuild 的配置以保持原有的输出目录配置:

import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";

const OUT_DIR_PROD = "../src/main/resources/console";
const OUT_DIR_DEV = "../build/resources/main/console";

export default rsbuildConfig({
  rsbuild: ({ envMode }) => {
    const isProduction = envMode === "production";
    const outDir = isProduction ? OUT_DIR_PROD : OUT_DIR_DEV;

    return {
      resolve: {
        alias: {
          "@": "./src",
        },
      },
      output: {
        distPath: {
          root: outDir,
        },
      },
    };
  },
});

示例:https://github.com/halo-sigs/plugin-migrate

了解更多:https://docs.halo.run/developer-guide/plugin/basics/ui/build


前情提要

Halo 插件的 UI 部分(Console / UC)的实现方式其实很简单,本质上就是构建一个结构固定的大对象,交给 Halo 去解析,其中包括全局注册的组件、路由定义、扩展点等。 基于这个前提,在实现插件机制时,主要面临的问题就是如何将这个大对象传递给 Halo。当初做了非常多的尝试,最终选择构建为 IIFE(Immediately Invoked Function Expression,立即执行函数),然后 Halo 通过读取 window[PLUGIN_NAME](PLUGIN_NAME 即插件名)来获取这个对象。 构建方案采用 Vite,并提供了统一的构建配置。回过头来看,这个方案存在不少问题:

  1. 会污染 window 对象,虽然目前并没有出现因为这个导致的问题,但是从长远来看,这个方案并不是最优的。(当然,使用 Rspack 来构建并不是为了解决这个问题)

  2. Vite 不支持 IIFE / UMD 格式的代码分割(主要是 Rollup 还不支持),无法像 ESM(ECMAScript Module)那样实现异步加载模块的机制。

  3. 基于第 2 点,如果插件中实现了较多的功能,可能会导致最终产物体积巨大,尤其是当用户安装了过多的插件时,会导致页面加载缓慢。

    1. http://www.halo.run/ 为例,gzip 之前接近 10M 的 bundle.js,gzip 之后也有 2M - 3M。

    2. 以此博客为例,gzip 之后也有 1.8M 的 bundle.js。

  4. 基于第 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';
// [!code --]
import DemoPage from './views/DemoPage.vue';

export default definePlugin({
  routes: [
    {
      parentName: 'Root',
      route: {
        path: 'demo',
        name: 'DemoPage',
// [!code --]
        component: DemoPage,
// [!code ++:4]
        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';
// [!code ++]
import * as sassEmbedded from "sass-embedded";

...

export default defineConfig({
  ...
  module: {
    rules: [
      ...
// [!code ++:13]
      {
        test: /\.(sass|scss)$/,
        use: [
          {
            loader: "sass-loader",
            options: {
              api: "modern-compiler",
              implementation: sassEmbedded,
            },
         },
        ],
        type: "css/auto",
      },
    ],
  },
...
});

集成 UnoCSS

如果你习惯使用 TailwindCSS 或者 UnoCSS 来编写样式,可以参考以下配置:

本文推荐使用 https://unocss.dev/,因为可以利用 UnoCSS 的 https://unocss.dev/transformers/compile-class 来编译样式,预防与 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';
// [!code ++]
import { UnoCSSRspackPlugin } from '@unocss/webpack/rspack';

...

export default defineConfig({
  ...
  plugins: [
    new VueLoaderPlugin(),
// [!code ++]
    UnoCSSRspackPlugin()
  ],
  ...
  module: {
    rules: [
      ...
// [!code ++:5]
      {
        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',
// [!code ++]
    '@unocss',
  ],
  env: {
    'vue/setup-compiler-macros': true,
  },
// [!code ++:3]
  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"
  }
}

参考文档

感谢阅读,欢迎交流与指正!