UnoCSS Webpack 集成实战:@unocss/webpack 插件的配置方式与源码级实现解析
【免费下载链接】unocssThe instant on-demand atomic CSS engine.项目地址: https://gitcode.com/GitHub_Trending/un/unocss
本文围绕 UnoCSS 官方文档中的 Webpack 集成指南(@unocss/webpack插件包)展开,完整覆盖从安装、webpack 4/5 配置、Vue CLI 框架接入到uno.css虚拟模块使用的全流程,并结合 packages-integrations/webpack 目录下的实际源码(插件入口、unplugin 实现、Rspack 适配层)与仓库测试用例,解释该插件"如何扫描 token、如何注入 CSS、如何处理持久化缓存"的底层机制。读完本文,你可以在现有 Webpack 项目中正确接入 UnoCSS,并理解插件各钩子的职责与版本限制,遇到构建问题时能对照源码快速定位。
1. 插件概述:@unocss/webpack 是什么
@unocss/webpack是 UnoCSS 提供的 Webpack 插件,官方包 README 见 packages-integrations/webpack/README.md,对应的完整使用文档在 docs/integrations/webpack.md。关于该插件,官方文档明确了两个重要前提:
- 仅支持
global模式。UnoCSS 的集成分为不同运行模式,而 Webpack 插件目前只实现并支持 global 模式(即通过入口引入全局uno.css的方式生成样式),这一点在官方文档中给出了对 vite/src/types.ts 中模式定义的引用。 - 不附带任何默认预设。插件本身不会替你加载
preset-mini、preset-wind3等预设,你需要在自己的uno.config.ts中自行配置presets、rules、theme等选项。
从 package.json 可以看到包的元信息:包名为@unocss/webpack,type: module(ESM-only 构建产物同时提供 CJS 入口),peerDependencies声明webpack: ^5;核心依赖包括@unocss/config、@unocss/core、unplugin、chokidar、webpack-sources等。值得注意的是,虽然 peer 依赖只声明了 webpack 5,但插件源码里对 webpack 4 的钩子(optimizeAssets)做了兼容处理,文档也保留了 webpack 4 的完整配置示例。
包内源码结构非常精简,共 3 个文件:
| 文件 | 职责 |
|---|---|
| src/index.ts | 默认导出WebpackPlugin,对外暴露插件选项类型 |
| src/unplugin.ts | 核心实现:基于unplugin构建,实现 token 提取、虚拟模块、资源注入、watch 更新 |
| src/rspack.ts | Rspack 适配层,导出UnoCSSRspackPlugin |
2. 前置依赖与安装
官方文档"Prerequisite"一节明确:@unocss/webpack依赖style-loader和css-loader来处理 CSS 文件。因为插件产出的虚拟模块uno.css是一个标准 CSS 模块,Webpack 不会自动处理 CSS,必须由你在 loader 链中配置这两个 loader。
安装命令(以官方文档支持的 4 种包管理器为例):
# pnpm pnpm add -D @unocss/webpack # yarn yarn add -D @unocss/webpack # npm npm install -D @unocss/webpack # bun bun add -D @unocss/webpack3. Webpack 配置:ESM-only 带来的动态 import 写法
这是 Webpack 集成中最高频的踩坑点。官方文档指出:从 UnoCSSv0.59.0起,UnoCSS 已迁移为 ESM-only,而 Webpack 配置通常运行在 CJS 环境(module.exports)中,因此不能直接require插件,必须以"动态 import"的方式加载配置。这一点也体现在包的构建形态上——tsdown.config.ts 同时输出esm与cjs两种格式,动态import()在 Node CJS 环境中的运行时才安全。
3.1 UnoCSS ≥ v0.59.0 的配置(webpack 5)
// webpack.config.js module.exports = function () { return import('@unocss/webpack').then(({ default: UnoCSS }) => ({ plugins: [ UnoCSS() ], optimization: { realContentHash: true } })) }其中optimization.realContentHash: true是 webpack 5 下推荐开启的选项:UnoCSS 的样式内容是构建过程中才生成的,若内容哈希参与文件名,可以保证产物文件名与实际内容一致。
3.2 UnoCSS ≥ v0.59.0 的配置(webpack 4)
// webpack.config.js module.exports = function () { return import('@unocss/webpack').then(({ default: UnoCSS }) => ({ plugins: [ UnoCSS() ], css: { extract: { filename: '[name].[hash:9].css' }, }, })) }官方文档的 warning 提示了 webpack 4 的限制:optimization.realContentHash在 webpack@4.x 不受支持,此时需要用css.extract.filename自定义 CSS 文件名,示例中使用哈希码的前 9 位([hash:9])代替 contenthash。同时文档提醒该用法存在与打包相关的已知问题(UnoCSS issue #1728 以及 webpack 自身的 issue #9520),在 webpack 4 项目中如遇样式产物异常可对照这两个 issue 排查。
3.3 旧版本 UnoCSS(< v0.59.0)的 CJS 写法
如果你的项目仍在使用迁移前的 UnoCSS 版本,可以直接require:
// webpack.config.js(webpack 5) const UnoCSS = require('@unocss/webpack').default module.exports = { plugins: [ UnoCSS() ], optimization: { realContentHash: true } }// webpack.config.js(webpack 4) const UnoCSS = require('@unocss/webpack').default module.exports = { plugins: [ UnoCSS() ], css: { extract: { filename: '[name].[hash:9].css' } } }3.4 插件参数
从 src/index.ts 的类型定义可以看到插件签名的完整参数:
export interface WebpackPluginOptions<Theme extends object = object> extends UserConfig<Theme> { /** * Manually enable watch mode * * @default false */ watch?: boolean } export default function WebpackPlugin<Theme extends object>( configOrPath?: WebpackPluginOptions<Theme> | string, defaults?: UserConfigDefaults, ): WebpackPluginInstance三个要点:
configOrPath既可以传内联配置对象(继承核心UserConfig<Theme>的全部选项),也可以传一个字符串路径指向你的配置文件(如./uno.config.ts);- 在配置对象中额外提供
watch?: boolean(默认false),用于手动开启内容监听——源码中这里有一条TODO: detect webpack's watch mode and enable watcher(见 src/unplugin.ts),说明当前版本还无法自动感知 webpack 的 watch 模式,需要你在 watch 场景下显式传watch: true; - 第二个参数
defaults用于传入UserConfigDefaults,作为配置的默认值基础。
对应的uno.config.ts文件内容即标准 UnoCSS 配置:
// uno.config.ts import { defineConfig } from 'unocss' export default defineConfig({ // ...UnoCSS options })4. 使用方式:在入口引入uno.css
配置好插件后,按照官方文档"Usage"一节的说明,在应用主入口中引入虚拟模块uno.css:
// main.ts import 'uno.css'uno.css并不是真实文件,而是由插件通过resolveId钩子解析出的虚拟模块。所有出现在代码中被提取器识别的 UnoCSS 类名(token)会在此处统一生成对应的 CSS。由于插件只支持global模式,全局样式的注入点就是这个uno.css入口,它会被你配置好的css-loader+style-loader(开发环境注入<style>标签)或MiniCssExtractPlugin(生产环境抽取为文件)接管处理。
5. 框架集成:Vue CLI(Vue + webpack 4/5)
官方文档为 Vue CLI 项目给出了完整方案。前提条件同样强调:使用 UnoCSSv0.59.0时,需要Vue CLI Servicev5.0.8才能正确支持以动态 import 方式加载的 Webpack 配置(因为 Vue CLI 允许vue.config.js导出一个返回 Promise 的函数)。
5.1 UnoCSS ≥ v0.59.0 的 vue.config.js(webpack 5)
// vue.config.js const process = require('node:process') module.exports = function () { return import('@unocss/webpack').then(({ default: UnoCSS }) => ({ configureWebpack: { devtool: 'inline-source-map', plugins: [ UnoCSS() ], optimization: { realContentHash: true } }, chainWebpack(config) { config.module.rule('vue').uses.delete('cache-loader') config.module.rule('tsx').uses.delete('cache-loader') config.merge({ cache: false }) }, css: { extract: process.env.NODE_ENV === 'development' ? { filename: 'css/[name].css', chunkFilename: 'css/[name].css' } : true } })) }5.2 UnoCSS ≥ v0.59.0 的 vue.config.js(webpack 4)
// vue.config.js const process = require('node:process') module.exports = function () { return import('@unocss/webpack').then(({ default: UnoCSS }) => ({ configureWebpack: { plugins: [ UnoCSS({}) ] }, chainWebpack(config) { config.module.rule('vue').uses.delete('cache-loader') config.module.rule('tsx').uses.delete('cache-loader') config.merge({ cache: false }) }, css: { extract: process.env.NODE_ENV === 'development' ? { filename: '[name].css', chunkFilename: '[name].[hash:9].css' } : true } })) }配置中几个关键动作的作用:
chainWebpack中删除cache-loader并关闭cache:Vue CLI 默认对.vue/.tsx模块使用cache-loader,而 UnoCSS 依赖 transform 阶段收集 token,缓存层可能跳过 loader 执行,导致 token 漏扫,因此官方示例显式禁用缓存链路;css.extract按环境切换:开发环境输出固定文件名(便于 HMR 与调试),生产环境交还 Vue CLI 默认的抽取与命名策略(配合前文的realContentHash或hash:9方案)。
5.3 旧版本 UnoCSS 的 vue.config.js
对迁移前的 UnoCSS 版本,则直接使用require形式,其余逻辑相同:
// vue.config.js(webpack 5,旧版本 UnoCSS) const process = require('node:process') const UnoCSS = require('@unocss/webpack').default module.exports = { configureWebpack: { devtool: 'inline-source-map', plugins: [ UnoCSS() ], optimization: { realContentHash: true } }, chainWebpack(config) { config.module.rule('vue').uses.delete('cache-loader') config.module.rule('tsx').uses.delete('cache-loader') config.merge({ cache: false }) }, css: { extract: process.env.NODE_ENV === 'development' ? { filename: 'css/[name].css', chunkFilename: 'css/[name].css' } : true }, }// vue.config.js(webpack 4,旧版本 UnoCSS) const process = require('node:process') const UnoCSS = require('@unocss/webpack').default module.exports = { configureWebpack: { plugins: [ UnoCSS({}), ] }, chainWebpack(config) { config.module.rule('vue').uses.delete('cache-loader') config.module.rule('tsx').uses.delete('cache-loader') config.merge({ cache: false, }) }, css: { extract: process.env.NODE_ENV === 'development' ? { filename: '[name].css', chunkFilename: '[name].[hash:9].css', } : true, }, }6. 源码深度解析:unplugin.ts 中的核心实现
文档层面只描述了"怎么配",src/unplugin.ts 则展示了"怎么跑"。以下按执行顺序梳理关键实现。
6.1 上下文创建与环境模式
const ctx = createContext<WebpackPluginOptions>(configOrPath as any, { envMode: process.env.NODE_ENV === 'development' ? 'dev' : 'build', ...defaults, })插件通过共享的集成层(virtual-shared/integration/src/context.ts)创建上下文,并以NODE_ENV区分 dev/build 两种环境模式,分别加载对应的 UnoCSS 配置。ctx提供tokens(收集到的类名集合)、filter(判断文件是否在 UnoCSS 关注范围内)、extract(从代码中提取 token)、tasks/flushTasks(异步任务队列)等能力。
6.2 transform 阶段:token 提取
transform 钩子是 UnoCSS "按需生成"的入口:
transform: { filter: { id: { exclude: [/\.html$/, BINARY_ASSET_RE], }, }, async handler(code, id) { if (!filter('', id)) return const result = await applyTransformers(ctx, code, id, 'pre') if (isCssId(id)) return result if (result == null) tasks.push(extract(code, id)) else tasks.push(extract(result.code, id)) return result }, },逻辑分三步:先用filter过滤掉 UnoCSS 不处理的文件;再执行enforce: 'pre'的 transformer(见 6.6 节关于该限制的说明);最后将提取任务压入异步队列tasks——注意提取是异步批处理的,并非同步阻塞 transform,真正消费队列的时机在 6.5 节的 assets 阶段。filter排除二进制资源(BINARY_ASSET_RE覆盖 png/jpg/webp/svg/字体等,见 src/unplugin.ts),这一处修复了 unplugin 上游一个把非过滤模块当文本处理、从而损坏二进制资源的问题(源码注释指向 issue #5164 / unjs/unplugin#524)。仓库测试 test/webpack-assets.test.ts 正是针对该修复做了回归:用 test/fixtures/webpack-assets 夹具跑完整 webpack 构建,断言输出的logo.png字节与源文件完全一致(PNG 魔数0x89 0x50校验),同时断言打包结果中包含.text-red样式。
6.3 resolveId 与 load:虚拟模块uno.css的注入
resolveId钩子(src/unplugin.ts)将uno.css这类 id 解析为内部的虚拟模块入口,并把入口注册进entries集合(保留原始 query);load钩子则负责返回虚拟模块内容。这里有一个关键设计:load 只匹配虚拟 CSS 模块 id(默认前缀__uno,即正则/[/\\]__uno(?:_.*?)?\.css(\?.*)?$/),返回的是"hash 占位符 + layer 占位符"的桩内容,而不是真实 CSS:
// serve the placeholders in virtual module async handler(id) { const layer = await getLayer(ctx, id) if (!layer) return const hash = hashes.get(id) return (hash ? getHashPlaceholder(hash) : '') + getLayerPlaceholder(layer) },真实 CSS 的替换发生在后续的资源阶段(6.5 节)。这个"先占位、后替换"的两段式设计正是配合 webpack 的realContentHash:占位符参与初始打包,最终资源阶段再注入真实内容并重建 source map。load钩子限定只处理虚拟 CSS id 同样是为了避免全局 load 钩子污染二进制资源(与 6.2 节的二进制保护同属一个上游问题的修复)。
6.4 Webpack 5 持久化缓存的处理
webpack(compiler)钩子里有一段专门针对compiler.options.cache的补偿逻辑(src/unplugin.ts):
For Webpack 5 persistent cache, we need to manually read the file content if the module is restored from cache, as loaders might be skipped.
也就是说,当 webpack 5 持久化缓存命中、模块从缓存恢复时,loader(包括 UnoCSS 的 transform)可能被跳过,导致 token 没有被收集。插件在finishModules钩子中对每个非 CSS、未被 resolve 过且通过 UnoCSS filter 的模块,用compiler.inputFileSystem.readFile手动重读磁盘文件内容并重新执行ctx.extract,保证缓存场景下 token 不丢失。这也解释了为什么 Vue CLI 示例中显式关闭了缓存——两种策略(禁缓存 vs 补偿扫描)二选一。
6.5 资源阶段:CSS 生成与占位符替换
核心替换逻辑挂在资源处理钩子上,同时兼容 webpack 4 与 5:
const optimizeAssetsHook = compilation.hooks.processAssets /* webpack 5 & 6 */ || compilation.hooks.optimizeAssets /* webpack 4 */ optimizeAssetsHook.tapPromise(PLUGIN_NAME, async () => { await ctx.ready const files = Object.keys(compilation.assets) await flushTasks() const result = await ctx.uno.generate(tokens, { minify: true }) // ...遍历每个 asset,把 LAYER 占位符替换为该层的真实 CSS })执行顺序:flushTasks()先消费之前 transform 阶段积压的提取任务;然后调用核心的ctx.uno.generate(tokens, { minify: true })一次性生成全部 CSS;最后遍历compilation.assets中的每个产物,用LAYER_PLACEHOLDER_RE找到占位符并替换为对应 layer 的 CSS(__uno全量入口取所有已解析 layer 的合集),并用webpack-sources的SourceMapSource重建产物以保留 source map。这里的 layer 概念与 UnoCSS 配置中的layers选项对应,虚拟模块 id 中编码了它所属的 layer,实现"一个入口、多文件、按 layer 分片注入"的能力。
6.6 已知限制:只支持pre类型的 transformer
在beforeCompile钩子中,插件会检查用户配置的 transformers,若存在非preenforce 的项会输出警告:
[unocss] webpack integration only supports "pre" enforce transformers currently. the following transformers will be ignored
从源码结构看,这是因为 webpack 的 transform 时机只能挂在一个阶段,插件只在 transform handler 中调用applyTransformers(ctx, code, id, 'pre'),post 阶段没有对应的注入点,所以像transformer-variant-group这类默认 post 的 transformer 需要自行确认 enforce 设置。
6.7 Watch 模式下的热更新
const UPDATE_DEBOUNCE = 10 onInvalidate(() => { clearTimeout(timer) timer = setTimeout(updateModules, UPDATE_DEBOUNCE) })当内容监听(即 3.4 节中手动开启的watch: true)检测到文件变化时,onInvalidate触发 10ms 防抖的updateModules(src/unplugin.ts):重新flushTasks、重新generate、对每个虚拟模块 id 计算新 hash 并通过plugin.__vfs.writeModule(id, code)写入 unplugin 的虚拟文件系统,从而让 webpack 感知虚拟模块内容变化并触发增量重建。hash 未变化(token 集合 size 相同)时提前返回,避免无谓的重编译。
6.8 Rspack 支持
src/rspack.ts 提供UnoCSSRspackPlugin,其实现直接复用同一套 unplugin(unplugin.ts 中get rspack() { return this.webpack },见 src/unplugin.ts),并通过包的exports以@unocss/webpack/rspack子路径单独发布(见 package.json 中的exports["./rspack"])。因此在 Rspack 项目中可以以几乎相同的方式接入。
7. 版本与适用前提小结
综合文档与源码,使用该插件时需要确认以下前提:
| 事项 | 说明 | 依据 |
|---|---|---|
| UnoCSS 版本 | ≥ v0.59.0 必须用动态import()加载插件;旧版本可require | docs/integrations/webpack.md |
| Webpack 版本 | peer 依赖^5;源码同时兼容 webpack 4(optimizeAssets钩子),但 webpack 4 不支持realContentHash,需用css.extract.filename控制产物名 | package.json、src/unplugin.ts |
| CSS 处理链 | 必须配置style-loader+css-loader | docs/integrations/webpack.md |
| 预设 | 插件不带默认预设,需在uno.config.ts中自行配置presets | 官方文档 info 提示 |
| 运行模式 | 仅支持 global 模式(入口import 'uno.css') | 官方文档 |
| transformer | 仅enforce: 'pre'的 transformer 生效,其余会被忽略并告警 | src/unplugin.ts |
| watch 更新 | 不会自动感知 webpack watch 模式,需显式UnoCSS({ watch: true }) | src/index.ts、src/unplugin.ts |
8. 快速接入清单
按官方文档与仓库实现,一次标准的 Webpack 5 接入只需四步:
pnpm add -D @unocss/webpack(连同style-loader、css-loader);webpack.config.js导出返回 Promise 的函数,动态import('@unocss/webpack')并挂UnoCSS()插件,开启optimization.realContentHash;- 创建
uno.config.ts,配置所需的presets与theme; - 在
main.ts入口import 'uno.css'。
若遇到"样式不更新",优先排查两处:是否在 webpack 5 持久化缓存开启而 token 未被重新提取(源码已有补偿逻辑,见 6.4 节),以及是否在 Vue CLI 场景下cache-loader未禁用(见 5.1 节示例)。若遇到"PNG/字体等二进制资源损坏",确认使用的是包含BINARY_ASSET_RE过滤修复的版本,test/webpack-assets.test.ts 中的断言方式可以直接借鉴为自查手段。
【免费下载链接】unocssThe instant on-demand atomic CSS engine.项目地址: https://gitcode.com/GitHub_Trending/un/unocss
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考