Element Plus 主题构建方案

194 阅读2分钟

做一个 Element Plus 按需主题的 Vite 插件

@rdeam/vite-plugin-element-plus-theme-builder 一个 Vite 插件,做两件事:

  1. 按你给定的主题色重新编译 Element Plus 样式(含派生色梯度)
  2. 只把页面真正用到的组件 CSS 打进产物

解决什么问题

Element Plus 官方两条主题定制路径,各有短板:

路径派生色梯度按需打包用户成本
:root { --el-color-primary: ... }❌ 只覆盖一层❌ 必须 import 'element-plus/dist/index.css'
手写 SCSS 入口 + @forward ... with⚠️ 要手动维护 @use 清单

快速使用

pnpm add -D @rdeam/vite-plugin-element-plus-theme-builder sass
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { elementPlusThemeBuilder } from '@rdeam/vite-plugin-element-plus-theme-builder';

export default defineConfig({
  plugins: [
    vue(),
    elementPlusThemeBuilder({
      colors: { primary: '#215476' },
    }),
  ],
});

不需要在 main.tsimport './xxx.css',插件会自动把 <link> 注入到 index.html

工作原理

整体流程

build 模式分四步:

transform        →  扫每个模块,记录用到的组件名
generateBundle   →  用收集到的组件清单调 Sass 编译主题 CSS
emitFile         →  把 CSS 作为资源交给 Vite(自动带 content hash)
transformIndexHtml → 把 <link> 写进 index.html

收集组件:transform hook + 三个正则

挂 Vite 的 transform hook:

{
  name: 'element-plus-theme-builder',
  enforce: 'pre',
  transform(code) {
    scanCode(code, collected);
  },
}

只要一段代码进入产物,它一定经过 transformsrc/、workspace 包、node_modules 都覆盖到。

enforce: 'pre' 是为了在 @vitejs/plugin-vue 编译 SFC 之前拿到模板原文。

三个正则:

/<\s*(el-[a-z0-9-]+)/g                                  // <el-button>
/\b(El[A-Z][A-Za-z]+)\b/g                               // ElButton / ElMessageBox
/_?resolveComponent\s*\(\s*["'](el-[a-z0-9-]+)["']/g    // _resolveComponent("el-button")

前两条覆盖用户源码,第三条覆盖 node_modules已被 Vue 编译过.vue 产物——它们的模板已经变成 _resolveComponent("el-button") 调用,字面量不存在了。

编译:动态生成 SCSS 入口

generateBundle 阶段,根据收集到的组件名拼一份临时 SCSS 入口字符串:

@forward "<theme-chalk>/common/var.scss" with (
  $colors: (
    "primary": ("base": #215476),
    ...
  )
);

@use "<theme-chalk>/button.scss" as *;
@use "<theme-chalk>/dialog.scss" as *;
// ...

直接喂给 sass.compileStringAsync,拿到最终 CSS。

输出:emitFile,不写源码目录

this.emitFile({
  type: 'asset',
  name: 'element-plus-theme.css',
  source: css,
});

文件名自动带 content hash(element-plus-theme-abc123.css),随 Vite 一起落盘到 dist/,不污染 src/

transformIndexHtml<link> 写进 HTML。注入位置可配 head / head-prepend / body / body-prepend,默认 head

dev 模式:直接全量

dev 不扫描,直接编一份全量主题 SCSS 灌进内存,挂中间件吐 CSS:

async configureServer(server) {
  devThemeCss = await compileThemeCss(['index'], colors);

  server.middlewares.use('/__element-plus-theme.css', (_req, res) => {
    res.setHeader('Content-Type', 'text/css; charset=utf-8');
    res.end(devThemeCss);
  });
}

理由:dev 热更频繁,每次重扫重编代价大;而且漏一个组件页面就花。dev 不优化体积,全量最稳。

白名单兜底

有些组件静态扫描原理上扫不到,必须显式声明。插件默认包含这 6 个:

组件为什么扫不到
base全局基础样式,无具体标识符
overlaymessage-box / notification 内部用,无 <el-overlay> 标签
message用法 ElMessage(...),无标签
message-box用法 ElMessageBox.confirm(...)
notification用法 ElNotification(...)
loading用法 ElLoading.service(...)v-loading 指令

普通模板组件table / dialog / menu / card 等)不要加白名单——它们会被 transform 扫到,加进去只会让 CSS 比应有的大。

只有一种场景需要扩展白名单:组件名完全由运行时变量拼接(如 <component :is="apiResponse.name" />),静态扫描原理上无法识别。

配置项

全部字段都是可选的,不传就用默认值。绝大多数项目只需要写一行 colors

elementPlusThemeBuilder({
  colors: { primary: '#215476' }, // 其他色保持默认
})

下面是完整配置形态,注释里写的就是默认值,你只需要挑你想改的字段写:

elementPlusThemeBuilder({
  // 主题色,缺省的字段使用 Element Plus 原生色
  colors: {
    primary: '#409eff',
    success: '#67c23a',
    warning: '#e6a23c',
    danger:  '#f56c6c',
    error:   '#f56c6c',
    info:    '#909399',
  },

  // 主题 <link> 注入位置
  // 'head-prepend' 让主题排在 <head> 最前,业务样式可覆盖
  injectTo: 'head',

  // 兜底白名单:默认值已涵盖所有"扫不到"的组件(函数式 API 等),开箱即用
  // 只有"运行时拼接组件名"的特殊场景才往里加(详见上一节)
  alwaysIncludeComponents: ['base', 'overlay', 'message', 'message-box', 'notification', 'loading'],

  // 参与扫描的文件后缀;如果你想让 .md 之类的也参与扫描,改这里
  scanFilePattern: /\.(vue|jsx|tsx|ts|js|mjs|cjs)$/,

  // 性能调优:明确不引用 Element Plus 的大依赖可以跳过扫描
  // 一般不用配
  scanIgnore: [],

  // Element Plus theme-chalk SCSS 源码目录
  // 一般不用配;只有 pnpm hoist 场景下 node_modules 在 workspace 根才需要显式指过去
  elementPlusThemeChalkDir: 'node_modules/element-plus/theme-chalk/src',
})

90% 的项目只需要写 colors。剩下的字段都是边缘场景兜底用的。

FAQ

改了 colors 没生效? 重启 dev server。颜色在 configureServer 阶段一次性编进内存,不会跟着热更。

构建报错找不到 theme-chalk? 确认装了 element-plus。pnpm hoist 场景下 node_modules 可能在 workspace 根,要显式指 elementPlusThemeChalkDir

想让主题样式被业务样式覆盖? injectTo: 'head-prepend',让主题 <link> 排在 <head> 最前。

某个动态组件没样式? 如果组件名是运行时字符串拼接的(API 响应 / 计算属性),加进 alwaysIncludeComponents。普通模板组件不要加。