导读:本期聚焦于木下创作的《如何在 Rollup 中将 CSS 文件内容输出为字符串?》,敬请观看详情。在 Rollup 打包工具链中引入 CSS 文件时,默认行为是把样式抽离成独立的 CSS 文件,但有些场景下我们希望拿到样式文件的纯文本内容,比如做 Web Component 的 shadow DOM 注入、构建组件库的运行时样式、或者将样式内联到 JS 中动态插入页面。本文围绕 Rollup 中把 CSS 内容输出为字符串这一需求展开,介绍通过插件读取模块内容、利用 this.parse 与自定义 transform 钩子把 CSS 转成 JS 模块的思路,同时对比 rollup-plugin-css-only、rollup-plugin-postcss 等常见插件的配置差异,并给出可直接复用的插件代码示例,帮助你理解插件机制并快速落地。

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

如何在 Rollup 中将 CSS 文件内容输出为字符串?

为什么默认方案拿不到字符串

先理解问题的根源。Rollup 的模块图只包含 JS 模块,当你的入口文件里写了 import './style.css' 时,Rollup 并不知道如何处理这个文件,因为它的解析器无法把 CSS 当作 JavaScript 去解析。在没有插件的情况下,直接构建会报错,提示无法解析该导入。

社区里的 CSS 插件大多遵循一个固定套路:在 transformload 钩子里读取 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 相关配置。不过最简单的路径是设置 outputfalse,再通过 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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260912/55022.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。