Normal view

There are new articles available, click to refresh the page.
Today — 6 July 2025Code & Tech

使用 Rspack 构建 Halo 插件的前端部分

By: Ryan Wang
16 June 2025 at 14:37

更新(25-06-19)

现在已经为插件的 UI 部分提供了新的配置方式,@halo-dev/ui-plugin-bundler-kit@2.21.1 包提供了 rsbuildConfig 方法,可以更加方便的使用 Rsbuild 来构建 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"
  }
}

需要注意的是,为了适应新版的 plugin-starter,默认生产构建输出目录改为了 ui/build/dist ,如果你要从已有的插件项目迁移到 Rsbuild,建议参考 halo-dev/plugin-starter#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,
        },
      },
    };
  },
});

示例: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. 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';
-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"
  }
}

参考文档

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

Before yesterdayCode & Tech

【读书笔记】UX for Business

最近看了一本名为 UX for Business 的书,这本书主要在讲设计师如何做有价值的事情。让我们跳出设计画板,在更高的层面理解日常工作。理解做什么,怎么做,怎么做到极致。但个人认为作者写得有些口语化,且不必要的内容重复过多。很多地方讲得比较浅,没有太多案例,推荐指数3颗星吧。

VDP 框架

作者强力推荐了 “VDP 框架” 来确保我们时刻在做正确、有效且对公司有价值的事情。

V for Value 价值

当你的设计同时满足了用户和公司的需求,你就在创造价值。

作者将用户需求分为两类,一类是效率类需求(Efficiency),另一类是娱乐类需求(Entertainment)。当设计师在面对效率优先的场景时,需要帮助用户用更少的时间、精力和钱来做事情,而不是更多。总的来说,就是让用户用更少的成本完成一件事。

举个例子:在烘培店里,如果一个新的烘培配方可以让烘培师用更少的鸡蛋做到和以前一样好吃,那么这就是一个好的配方设计。

聚焦能为用户/公司创造价值的地方:新增、购买、下载等流程。

D for Diagnosis 分析

指的是系统性地收集和分析问题,确保我们在做正确的事。作者在书中分享了做从设计分析到实施方案的思路。

1.了解背景

作者指出 UX 设计师的工作实际上是基于当前的信息和限制(如:业务指标、用户诉求、产品目标、、问题反馈、时间/资源限制等等)来产出解决方案来改进产品。

就像医生在看病时,会问病人有什么症状、有什么感觉、最近吃过什么、有没有对什么过敏等等,通过多种角度来下结论。

UX 设计师也要通过类似的方式来设计,想做出一个好的方案,就需要更多的信息。5W就是一个好的方法:

  • Who:用户是谁?他们的特征和需求是什么?
  • When:什么时候会有这个问题?
  • Where:在哪里会遇到这个问题?
  • What:能解决什么问题?
  • Why:为什么用户需要它?

2.发掘+分析问题

有时候我们收到一些产品体验的反馈,这可能只是一个问题的表象,仅仅解决它只是在打补丁,没有从根源性解决问题。

某个新功能的使用情况不理想,可能并非是该功能设计得不好,而是在上一层的漏斗出现问题。所以想要挖掘到深层次的根源问题,需要通过在多个问题之间来回推敲。

收集足够多的问题,为下一步做准备

3.找到问题共同点

将同类的问题归类好,利用前面的 5W 分析,比如将属于同一个 When 的问题归在一类,同一个 Who 的问题在另一类。

这种做法有时候会让我们不仅仅在做当前需求,还能帮助我们发掘到其他带改进的点。

4.规划改进问题

这一步就是将我们观察、收集到的信息转化为行动了。

前几步我们列出了问题的表象,并将其分类。这一步需要去思考什么引发了这一连串有关联性问题。比如一个页面的 Like 按钮点击量很少,但用户反馈中他们都很喜欢这里的内容,那我们就会假设是 Like 按钮做得不够明显,解决方案随之而来。

我们会进行不止一次的假设,按照可能性的高低来按顺序尝试解决方案。

5.验证结果

最后,将我们觉得可行的解决方案推进上线,基于效果决定是在当前解决方案下改进,还是换一个解决思路,甚至收集更多信息后再次进行设计。

P for Probability 可能性

利用 “可能性” 来优化设计,将价值最大化。作者通过分享了以下观点来阐述可能性在设计中的应用。

1.越靠前、越明显的内容具有更多的可能性。比如一组选项,第一个选项通常来说都是更大概率被选择的。所以,把重要的、能创造价值的内容做得更靠前和明显;

2.事情是线性发展的。你不能在完成第一件事之前完成第二件事,就好比我们在浏览一个导航的时候,如果我们找到了可能合适的跳转入口,我们就会直接点击,不会尝试去看后面的内容了;

3.用户在准备好做决定时才会去做决定。就好比电商场景,用户都会在足够了解产品之后,才决定是否购买,那么“购买”按钮通常就不会是页面上的第一个元素。

4.默认值能影响可能性。默认的值通常是最多人进行选择的,所以要思考怎么设计默认值才能提高用户使用效率,或者对公司有价值。

为内部系统做设计

为公司的内部系统有一个明显的特点:我们的用户不会流失。因为公司的雇员必须按照公司要求用某个系统完成工作,比如登记工作任务的工单系统、用于请假、报销的 OA 系统等等。

我们不需要考虑所谓的参与度、忠诚度,所以在用户价值/公司价值的取舍中,我们可以更多地倾向公司价值。

内部系统对效率的关注度会更高,因为不需要靠这个产品来赚钱。找到流程中的障碍并解决,是内部工具设计师的关键能力。

提升效率不光是对使用内部工具的员工有益,对公司的降低成本这种目标也是有价值的。高效率的工具可以让公司少雇佣一些雇员,帮助公司减少人力成本。作者甚至提到,如果内部工具能让小团队的生产效率提升得足够多,小公司也能和大公司竞争。

为商业化 SaaS 产品做设计

本书中提到一类产品属于 Sales-Driven 销售导向产品,对应到国内常用术语应该叫做商业化 SaaS 产品。

通常来说,做 SaaS 业务的公司通过卖这些获得收入:

  1. 席位:使用该产品的人数;
  2. 数据:使用服务的次数;
  3. 功能/模块:使用更强大功能的能力;

设计师需要清楚自己所负责的 SaaS 产品的收入来源,为公司所出售的服务进行设计。

共鸣片段

To improve conversion (i.e., get more people to finish your flow), think about everything you request from the user like a cost. Every question is a cost. Every minute is a cost

流程优化时,尝试将用户的每一步操作成本降到最低。鼠标移动、点击、鼠标滚轮、视线移动等等,都是操作成本。作者还提到一个观点,可以从最后一步开始优化,从后往前改进。因为在最后一步才导致转化失败会显得之前的努力都是徒劳。

UX is not what you do in Figma or a ticket in agile planning or the contrast of your button labels.
UX is a general process of designing things for humans.

我认为 UX 设计并不仅是 Figma 里一个个连接起来的页面,这只是表达清楚了页面/功能之间的跳转逻辑。UX 设计师是在为用户进行工作,需考虑到用户在流程中的使用所有体验,包括:

  • 浏览页面过程中的体验,比如信息层级设计,内容排版策略(例:恰当地设定文本换行规则);
  • 用户行为与系统交互中的体验,比如在进行增删改查时的功能引导、交互逻辑、操作反馈(例:校验表单的时机)、使用帮助、边缘场景闭环体验等;
  • 利用原生浏览器能力进行体验极致化,比如设置给 “版本号” 或 “错误代码” 的字段设置 user-select: all; 用户需要复制时,右键到文字区域就能自动全选该文本块,省去用鼠标框选的步骤;
  • 等等…

所以有时候做 “体验设计” 并不是在 Figma 里画图(这只是其中一种传达解决方案的形式),而是要提供一切能改进用户体验所需要的智慧结晶。

Design is a process, but not just a one-time process. We iterate!

设计是一个迭代过程,前面提到设计师是基于当前已知的信息和限制来提供解决方案。但世界上很多事情每天都在改变,产品目标会变,用户诉求会变,公司战略会变等等,所以每次迭代都是基于新的已知的信息进行改进。

Most importantly, what is the one thing the users should achieve on this page, if they only do one thing?

这句话提醒设计师,时刻将目标记在心头。当设计方案有陷入“既要又要”、“没有重点” 的困境时,拷问自己:在这个场景中,用户最重要的事情是什么?将最重要的事情做好。

sometimes the best design solutions require the designers and developers to do more work so users can do less.

这个深有同感,设计师和开发者费很大劲儿做出来的迭代,有时候只是帮助用户将使用流程更简化。

结语

第一次写读书笔记,改进的地方很多。但我会坚持阅读,纯粹觉得阅读能让我在这浮躁的社会,所谓的快节奏环境里慢下来。

❌
❌