在使用 Rollup 构建组件库或 Web Component 时,经常遇到一个需求:不希望把 CSS 打包成独立的样式文件,而是希望拿到样式内容的字符串,直接在 JS 里通过 document.createElement('style') 动态注入,或者塞进 shadow DOM 里。Rollup 本身并不理解 CSS 文件,它只处理 JavaScript 模块,所以要让 CSS 以字符串形式出现在产物里,本质上就是想办法把 CSS 文件转换成一个导出字符串的 JS 模块。这篇文章就来详细拆解这个问题的几种解决思路。

为什么默认方案拿不到字符串
先理解问题的根源。Rollup 的模块图只包含 JS 模块,当你的入口文件里写了 import './style.css' 时,Rollup 并不知道如何处理这个文件,因为它的解析器无法把 CSS 当作 JavaScript 去解析。在没有插件的情况下,直接构建会报错,提示无法解析该导入。
社区里的 CSS 插件大多遵循一个固定套路:在 transform 或 load 钩子里读取 CSS 文件内容,把样式收集到一个数组里,最后在 generateBundle 钩子里通过 this.emitFile 输出一个独立的 bundle.css 文件。这就是为什么 rollup-plugin-css-only 这类插件最终给你的是一个 CSS 文件而不是字符串。如果你想要字符串,就要打破这个套路,让 CSS 模块本身导出一段文本。
核心思路是:在 transform 钩子中拦截以 .css 结尾的模块,读取其内容,然后用 JSON.stringify 把内容转成合法的 JS 字符串,返回一段类似 export default "..." 的代码。这样 Rollup 就把它当成一个普通的 JS 模块处理,产物里自然就有了字符串形式的样式。
手写一个最小可用的 CSS 字符串插件
理解原理之后,写一个插件其实非常简单。Rollup 插件就是一个带 name 属性和若干钩子函数的对象。下面这个插件只有二十行左右,却完整实现了需求。
function cssString() {
return {
name: 'css-string',
transform(code, id) {
// 只处理 css 文件
if (!id.endsWith('.css')) return null;
// 把 CSS 内容序列化成 JS 字符串并导出
return {
code: 'export default ' + JSON.stringify(code) + ';',
map: { mappings: '' } // 不需要 sourcemap
};
}
};
}
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
},
plugins: [cssString()]
};在业务代码中就可以这样使用:
import styles from './style.css';
// 动态插入样式
const styleEl = document.createElement('style');
styleEl.textContent = styles;
document.head.appendChild(styleEl);
// Web Component 场景:注入 shadow DOM
class MyCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = styles;
shadow.appendChild(style);
}
}
customElements.define('my-card', MyCard);有几个细节值得注意。第一,transform 钩子的第一个参数 code 就是文件原始内容,不需要再手动 fs.readFileSync,这样还能正确兼容虚拟模块。第二,用 JSON.stringify 序列化是关键,它能自动处理引号、换行符、Unicode 字符的转义,避免手写拼接字符串时出现语法错误。第三,如果项目里还配置了 PostCSS 之类的预处理器插件,要把字符串插件放在它们后面,让它处理的是编译后的 CSS。
利用现有插件实现相同效果
如果不想维护自定义插件,也可以基于成熟插件改造。rollup-plugin-postcss 提供了一个 inject 选项,当设置为 { injectTo: 'none' } 并配合 modules 选项时,可以将样式以特定形式暴露出来;更直接的方式是它支持的 emitCode 相关配置。不过最简单的路径是设置 output 为 false,再通过 getExtractedFile 或者在 onExtract 回调中拿到内容字符串。
另一个常见做法是借助 ?raw 后缀的约定。Vite 内部支持 import styles from './style.css?raw' 来获取原始字符串,而 Vite 的构建内核正是 Rollup。如果你的项目最终走 Vite 构建,直接用这个后缀即可,无需任何额外插件。如果坚持用原生 Rollup,也可以模仿这个约定,改造前面手写的插件,把匹配条件从文件后缀改成判断 id 是否以 ?raw 结尾,并对 id 做截断处理。
function rawString() {
return {
name: 'raw-string',
async load(id) {
if (!id.endsWith('?raw')) return null;
const fs = await import('node:fs/promises');
const filePath = id.slice(0, -4); // 去掉 ?raw 后缀
const content = await fs.readFile(filePath, 'utf-8');
return 'export default ' + JSON.stringify(content) + ';';
}
};
}这种写法的好处是通用性强,不只局限于 CSS,任何文本文件(比如 .svg、.glsl、.txt)都能通过追加 ?raw 拿到字符串内容,非常适合组件库的开发场景。
几个容易踩的坑
第一个坑是产物体积与压缩。CSS 以字符串形式存在于 JS 里时,字符串会占据 JS 包体积,且某些压缩工具对超长字符串的处理效率不高。建议在输出配置中开启压缩,并确认 terser 或 esbuild 不会因为字符串过长而报错。此外,样式内容不会经过 CSS 专属的压缩管线(比如 cssnano),如果对体积敏感,可以在 transform 里先用 lightningcss 或 postcss 做一遍压缩再序列化。
第二个坑是样式去重。如果多个模块都引用同一个 CSS 文件,Rollup 会正常去重模块,字符串只会打包一次,这点不用担心。但如果你的插件在 transform 里做了额外副作用(比如往全局注册样式类名映射),就要自己保证幂等性,否则多次构建或热更新时可能出现重复注入。
第三个坑是 SSR 场景。在 Node 环境中不存在 document,直接在模块顶层执行样式注入代码会报错。正确的做法是把注入逻辑封装成显式调用的函数,比如导出一个 injectStyles(),让运行环境自行决定何时调用,而不是在模块加载时立即操作 DOM。
总结一下,把 CSS 内容输出为字符串的关键在于把 CSS 文件伪装成一个导出字符串的 JS 模块,手写 transform 插件是最灵活可控的方案,?raw 约定则提供了更通用的文本导入能力。根据项目是原生 Rollup 还是 Vite,选择合适的实现即可。
RollupCSS字符串输出rollup-plugin-css-only修改时间:2026-09-12 01:22:35