前端开发里有个挺让人头疼的事儿:你辛辛苦苦写好的样式,本地开发的时候一切正常,但一用 Rollup 打包,打开生产环境页面,发现按钮没了颜色、布局乱成一团,打开控制台一看,CSS 文件要么没加载,要么里面空空如也。这就是典型的“打包后样式资源丢失”问题。今天咱们就用大白话把它聊透,不光说清楚为什么会丢,还会手把手带着你把问题解决掉。

一、先搞清楚 Rollup 和样式的关系

Rollup 本身是一个 JavaScript 模块打包器,它的核心工作是把一堆散落的 JS 文件按照模块关系合并成一个或多个文件。样式文件(CSS、SCSS、Less 等)本身不是 JavaScript,Rollup 默认是不认识它们的。为了让 Rollup 能处理样式,我们需要借助插件,最常见的就是 rollup-plugin-postcss 或者 rollup-plugin-styles。

你可以把 Rollup 想象成一个快递分拣中心,它只负责打包“快递”(JS 模块),而 CSS 就像是需要特殊处理的“易碎品”,必须额外加一道包装工序。如果没有这个工序,快递中心就会直接把 CSS 文件当成普通碎片扔到一边,结果就是你收到的包裹里根本没有样式。

1.1 常见的技术栈组合

下面咱们统一使用 Vue 2 + Rollup + postcss 这个技术栈来演示。之所以选这个组合,是因为它在组件库开发里非常常见,而且问题暴露得很典型。

// 这是项目里的 package.json 部分内容
// 技术栈:Vue 2 + Rollup + @rollup/plugin-vue + rollup-plugin-postcss

{
  "name": "my-component-lib",
  "version": "1.0.0",
  "scripts": {
    "build": "rollup -c rollup.config.js"
  },
  "devDependencies": {
    "rollup": "^2.79.0",
    "@rollup/plugin-vue": "^6.0.0",
    "@rollup/plugin-node-resolve": "^13.3.0",
    "rollup-plugin-postcss": "^4.0.2",
    "vue-template-compiler": "^2.6.14",
    "vue": "^2.6.14"
  }
}

二、样式资源丢失的典型场景

咱们先看一个最简单的实际案例。假设你写了一个按钮组件,它带一点样式:

<!-- Button.vue -->
<!-- 技术栈:Vue 2 单文件组件 -->
<template>
  <button class="my-btn">
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'MyButton'
}
</script>

<style scoped>
.my-btn {
  padding: 8px 16px;
  background-color: #42b983;
  color: #ffffff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

然后你的入口文件长这样:

// src/index.js
// 技术栈:JavaScript + Vue 2
import Button from './components/Button.vue';

export default {
  install(Vue) {
    Vue.component('MyButton', Button);
  }
};

export { Button };

接着你配置了最简单的 Rollup 打包配置:

// rollup.config.js
// 技术栈:JavaScript (Node.js 环境)
import vue from '@rollup/plugin-vue';
import { nodeResolve } from '@rollup/plugin-node-resolve';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/my-lib.js',
    format: 'esm'
  },
  plugins: [
    nodeResolve(),
    vue()
  ]
};

执行 npm run build 之后,你会在 dist 文件夹里看到生成的 JS 文件。但当你打开页面时,按钮的样式完全不存在。为什么?因为我们的配置里根本没有处理 CSS 的插件,vue() 插件虽然编译了单文件组件,但默认情况下它会把样式部分“吐出来”交给下一个插件处理,如果没有下一个插件,样式就被丢弃了。

2.1 丢失背后的核心原因

这里面的核心原因可以总结成两点:

第一,缺少处理样式的插件。Rollup 的插件机制是“管道式”的,每个插件可以处理特定类型的资源。如果没有一个插件专门负责接收 CSS 并把它输出成文件,CSS 内容就会在打包过程中被静默忽略。

第二,样式被提取到 JS 中但没有正确注入。有些插件(比如 rollup-plugin-postcss 默认配置)会把 CSS 抽离成一个独立的 CSS 文件,但如果你忘了在最终 HTML 里引入这个 CSS 文件,样式同样会“丢失”。另外,如果你把 inject 设置为 false,又没设置 extract,那 CSS 就真的不见了。

我们继续看一个更隐蔽的丢失场景。假设你用了 rollup-plugin-postcss,但配置参数写错了:

// rollup.config.js
// 技术栈:JavaScript (Node.js 环境)
import vue from '@rollup/plugin-vue';
import postcss from 'rollup-plugin-postcss';
import { nodeResolve } from '@rollup/plugin-node-resolve';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/my-lib.js',
    format: 'esm'
  },
  plugins: [
    nodeResolve(),
    vue(),
    postcss({
      // 注意:这里没有写 extract,也没有写 inject
      // 默认情况下,postcss 插件会把 CSS 注入到 JS 里
      // 但如果是组件库,你可能希望提取成独立文件
    })
  ]
};

在这种情况下,如果你没有配置 extract 为 true 或指定文件名,rollup-plugin-postcss 默认会把 CSS 作为字符串打进 JS 里,然后在运行时通过 <style> 标签注入到页面上。这本身没问题,但对于组件库这种使用场景,你可能希望输出一个独立的 dist/my-lib.css。于是后面的人直接引用了 JS,却忘了引用 CSS,就会说“样式丢了”。

三、详细的解决方法

下面我会给出几个常见的解决方法和对应的完整示例,大家可以根据自己的具体情况选择合适的方案。

3.1 方法一:提取独立 CSS 文件

这是组件库开发最推荐的方式:把组件库的所有样式打包成一个独立的 CSS 文件,然后在你的项目入口文件中手动引入它。

// rollup.config.js
// 技术栈:JavaScript (Node.js 环境)
import vue from '@rollup/plugin-vue';
import postcss from 'rollup-plugin-postcss';
import { nodeResolve } from '@rollup/plugin-node-resolve';

export default {
  input: 'src/index.js',
  output: {
    // 输出 ES Module 格式,方便使用方按需引入
    file: 'dist/my-lib.js',
    format: 'esm'
  },
  plugins: [
    nodeResolve(),
    vue(),
    postcss({
      // 提取样式到 dist/my-lib.css 文件
      extract: 'my-lib.css',
      // 关闭自动注入,我们不需要运行时注入,因为我们会手动引 CSS
      inject: false,
      // 压缩一下 CSS,生产环境友好
      minimize: true,
      // 处理 @import 和 url 路径
      extensions: ['.css', '.scss', '.less']
    })
  ]
};

打包之后,你的 dist 目录下会有两个文件:my-lib.js 和 my-lib.css。使用方必须这样引入:

// 使用方项目中的引入方式
// 技术栈:JavaScript
import MyLib from 'my-lib';
import 'my-lib/dist/my-lib.css'; // 手动引入样式,缺了它就没样式

这个方法属于“一劳永逸”,只要使用方记得引 CSS 文件,样式一定不会丢。但它的缺点是“不够智能”,如果你只想使用组件库的某一个组件,也得引入整个 CSS 文件,没法按需加载。

3.2 方法二:自动注入到 JS 中

如果你不太关心代码拆分,更希望使用方直接引一个 JS 文件就能搞定所有事情,那么可以采用自动注入的方式。也就是说,让打包工具把 CSS 转换成 JS 代码,运行时自动创建 <style> 标签插入到页面。

// rollup.config.js
// 技术栈:JavaScript (Node.js 环境)
import vue from '@rollup/plugin-vue';
import postcss from 'rollup-plugin-postcss';
import { nodeResolve } from '@rollup/plugin-node-resolve';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/my-lib.js',
    format: 'esm'
  },
  plugins: [
    nodeResolve(),
    vue(),
    postcss({
      // 不设置 extract,或者显式设置为 false
      extract: false,
      // 关键:把样式注入到 JS 中,运行时自动添加 style 标签
      inject: true,
      // 为了性能,还可以配置只注入一次
      // 这里的配置项根据 rollup-plugin-postcss 版本略有不同
    })
  ]
};

打包后,你只会得到一个 my-lib.js,使用方引入它时,样式会自动被插入到页面头部。这种方式的使用体验很好,但对于大型组件库,会让 JS 文件变得很大,而且有“样式闪烁”的可能(因为 JS 执行需要时间,CSS 注入发生在 JS 执行后)。

3.3 方法三:分离 CSS 并生成自动引用的入口文件(适合处理异步组件)

如果你的组件库里有异步加载的组件,或者你使用了代码分割(code splitting),那么 CSS 丢失的坑更大。因为异步加载的 chunk 里的 CSS 不能被一次性提取到主 CSS 文件中。这种情况下,我们可以让每个 chunk 都带上自己的样式,或者在运行时用 JS 动态加载对应的 CSS。

下面是一个更复杂的例子,我们用 rollup-plugin-postcss 配合 @rollup/plugin-commonjs,并对样式文件做 hash 命名,确保不同 chunk 的样式不会相互覆盖。

// rollup.config.js
// 技术栈:JavaScript (Node.js 环境)
import vue from '@rollup/plugin-vue';
import postcss from 'rollup-plugin-postcss';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
  input: 'src/index.js',
  output: {
    // 输出多个 chunk,支持动态导入
    dir: 'dist',
    format: 'esm',
    // 给每个 chunk 命名,注意保留 .js 后缀
    entryFileNames: '[name].js',
    // 异步加载的 chunk 命名
    chunkFileNames: 'chunks/[name]-[hash].js'
  },
  plugins: [
    nodeResolve(),
    commonjs(),
    vue(),
    postcss({
      // 提取所有 CSS 但保留每个 chunk 自己的 CSS 文件
      // 设置函数可以根据模块信息生成不同文件名
      extract: (context) => {
        // context 里包含当前模块的某些信息,简单起见我们直接提取到主文件
        // 实际项目中,你可能希望每个 chunk 有自己的 CSS 文件名
        // 这里演示一个简单的逻辑:根据模块 ID 里的 chunk 名称来生成
        if (context && context.modules && context.modules.length) {
          // 获取模块所属的 chunk 信息
          // 注意:这里的逻辑需要按照你的项目结构调整
          // 简化处理:所有 CSS 都合并到一个文件
          return 'my-lib.css';
        }
        return 'my-lib.css';
      },
      inject: false,
      minimize: true
    })
  ]
};

但上面这种写法其实还是把所有 CSS 合成一个文件。如果你想真正实现“每个 chunk 对应自己的 CSS”,你可能需要手动编写一个小插件,或者使用 rollup-plugin-styles 的 extract 选项,它支持 true 或字符串,但不支持每个 chunk 分开。社区里目前没有特别完美的方案,所以很多组件库干脆直接禁用了代码分割,所有样式集中在主 CSS 里。

3.4 方法四:使用 CSS Modules 或者局部作用域样式

如果你的样式丢失是因为类名冲突或者作用域问题导致被覆盖或忽略,那么可以考虑使用 CSS Modules。rollup-plugin-postcss 原生支持 CSS Modules,只要配置 modules: true 即可。它能让你在 JS 里通过 import styles from './Button.css' 的方式拿到一个经过哈希处理的类名映射对象,然后把类名绑定到元素上。

<!-- Button.vue -->
<!-- 技术栈:Vue 2 + CSS Modules -->
<template>
  <button :class="styles.myBtn">
    <slot></slot>
  </button>
</template>

<script>
// 在 Vue 2 中使用 CSS Modules 需要这样引入
// 注意:Vue 2 的 <style module> 是内置功能
// 但如果你在纯 JS 组件里使用,可以用下面的方式
import styles from './Button.css';

export default {
  name: 'MyButton',
  data() {
    return {
      styles
    };
  }
}
</script>

<style module>
.myBtn {
  padding: 8px 16px;
  background-color: #42b983;
  color: #ffffff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

但这里要提醒一下,Vue 2 单文件组件中使用 <style module> 是 Vue 自己支持的,跟 Rollup 无关。如果你在 Vue 2 里使用 <style module>,它不需要 rollup-plugin-postcss 的 CSS Modules 功能。如果你混用,反而可能出问题。所以这个方法更适合在非 Vue 的纯 JS 项目中使用,比如一个工具函数库的样式。

四、应用场景与技术优缺点分析

咱们把上面几种解决方案分别放到实际应用场景里看看。

4.1 场景一:开源组件库

如果你在开发一个类似 Element UI 或 Vant 那样的开源组件库,那么**方法一(提取独立 CSS)**是最合适的。为什么?因为使用方通常需要按需引入样式,他们可能会用 babel-plugin-import 之类的工具来自动加载组件对应的 CSS 文件。如果你只给一个 JS 文件而且把 CSS 注入进去,虽然也能用,但会让 JS 体积变得非常大,而且无法被单独做样式覆盖或定制。

4.2 场景二:公司内部公共组件库

对于公司内部用的组件库,用户不多,交互简单,那么**方法二(自动注入 JS)**会很舒服。同事们只需要引入一个 JS 文件,不用手动写 CSS 的 import,不容易犯“忘引 CSS”的错。缺点就是首屏加载稍微慢一点,但内部系统通常可以接受。

4.3 场景三:给别人用的纯工具库

如果你的库不是组件库,而是一堆 JavaScript 方法,比如日期处理、防抖节流,根本没有样式,那当然不会遇到样式丢失的问题。但如果你的工具库附带一些默认的样式,比如一个 Toast 提示组件,那你最好还是提取独立 CSS,因为使用方可能只会在某些页面用到那几行 CSS,把 CSS 打包成独立文件方便他们选择是否加载。

4.4 技术优缺点对比

方法 优点 缺点
提取独立 CSS 体积可控,可缓存,方便按需加载 需要使用方手动引入,忘引就丢样式
自动注入 JS 接入简单,无需额外引 CSS JS 体积变大,首屏有额外运行时开销
代码分割 + CSS 能配合异步组件,按需加载 配置复杂,容易踩坑,多个 CSS 文件管理麻烦
CSS Modules 避免类名冲突,作用域隔离 不适合组件库,使用方无法覆盖样式

五、注意事项与避坑指南

写到这里,你应该已经能解决绝大多数样式丢失问题了。不过还有一些细节,如果你不注意,将来还是会掉进坑里。我把这些年踩过的坑整理一下。

5.1 注意插件顺序

Rollup 插件的执行顺序是按照数组顺序来的,但有些插件对顺序有严格要求。例如 vue() 插件必须放在 postcss() 之前,因为 vue() 会先把 .vue 文件里的 <style> 部分转换成 JS 的 import './Button.css',然后 postcss() 才能收到 CSS。如果你把顺序写反了,postcss() 可能先执行,然后根本没看到 CSS 文件,样式自然就丢了。

// 注意这个顺序:先 vue 再 postcss
// 技术栈:JavaScript (Node.js 环境)
plugins: [
  nodeResolve(),
  vue(),      // 先编译 .vue 文件,产出 CSS 导入语句
  postcss()   // 再处理 CSS 导入
]

5.2 注意 extract 与 inject 的搭配

extract 和 inject 是一对冤家。如果你设置了 extract 为字符串或 true,那么 inject 就会被忽略(实际上 postcss 插件会自动将 inject 设为 false)。如果你设置了 inject: true 但没设置 extract,那 CSS 会被注入到 JS 里。有些同学两个都设置了,结果发现 CSS 既生成了文件,又注入到 JS 里,页面重复加载样式。这是很不推荐的做法,我们应该在打包配置里只明确指定一种行为。

// 推荐写法:要么提取,要么注入
// 技术栈:JavaScript (Node.js 环境)
postcss({
  // 选择注入 JS 模式
  extract: false,
  inject: true
})
// 或者
postcss({
  // 选择提取独立文件模式
  extract: 'styles.css',
  inject: false
})

5.3 注意使用方是否真的正确引入了 CSS

这是最尴尬的一种情况:你的打包配置没问题,CSS 文件也生成得好好的,但使用方只引用了 JS 文件。所以在你发布组件库的时候,README 里一定要醒目标注“请同时引入 CSS 文件”。如果你不想让使用方自己去记,还可以在组件库入口文件里尝试使用 import './dist/my-lib.css' 这种方式来自动引入,但这样做的话,如果你的库同时被多个地方引用,CSS 会被打包多次,造成冗余。一般还是建议使用方手动引入,或者提供一个“全量引入”的入口文件。

5.4 注意样式前缀和兼容性

当你的组件库被不同项目使用时,各个项目的浏览器兼容策略可能不同。你最好在构建时通过 autoprefixer 自动添加厂商前缀。rollup-plugin-postcss 内置支持 postcss 的插件,你只需要在 postcss 选项里配置 plugins,或者直接使用 postcss.config.js 文件。

// postcss.config.js
// 技术栈:JavaScript (Node.js 环境)
module.exports = {
  plugins: [
    require('autoprefixer')({ overrideBrowserslist: ['> 1%', 'last 2 versions'] })
  ]
};

这样不需要在 Rollup 配置里写额外代码,rollup-plugin-postcss 会自动读取这个配置文件。如果你在构建时没有加上这一套,某些 CSS 属性在老旧浏览器上会失效,这也可能被误认为是“样式丢失”。

5.5 注意动态类名拼接的问题

有时候你的 JS 逻辑会动态生成类名,比如 class="btn-" + type,如果 type 的某些取值在代码里没有出现过,那么它们在打包时不会被写入 CSS。因为 Rollup 的 tree-shaking 和 CSS 提取都是基于静态分析,它只识别代码里直接写出来的字符串。动态拼接的类名如果拼出来的结果不在源码里,CSS 就会被当成“无用代码”被丢掉。

比如下面这种方法:

// utils.js
// 技术栈:JavaScript
export function getClass(type) {
  // 这样写,打包器不会知道 type 可能是 'primary' 或 'danger'
  // 所以如果 CSS 里写了 .btn-primary 和 .btn-danger,且没有在任何地方直接出现这些字符串
  // 这些样式可能被丢
  return `btn-${type}`;
}

正确做法是,让所有可能的类名都出现在源码里,或者关闭 CSS 的 tree-shaking(其实 rollup-plugin-postcss 默认不会对 CSS 做 tree-shaking,但有些其他插件如 rollup-plugin-purgecss 会)。如果你用了 purgecss 之类的工具,那就需要特别注意,可以在 purgecss 配置里加入 safelist,把这些动态类名排除掉。

5.6 注意多入口打包时的样式重复

如果你的 Rollup 配置有多个入口,并且每个入口都引入了一份相同的 CSS,那么提取出来的 CSS 文件可能会有重复的代码。rollup-plugin-postcss 内置了去重机制,但如果你用的是多个独立的 postcss() 实例,那就可能无法去重。建议在同一个 postcss() 插件事例中处理所有样式,不要为每个入口单独配置。

六、文章总结

样式资源丢失这个问题,说到底是 Rollup 的模块处理机制和样式资源本身的特性之间的矛盾。我们要把 Rollup 当成一个只认识 JS 的快递员,样式必须通过插件这个“翻译”加“包装”的流程,才能进入最终的包裹。只要记住以下几点,就能避免大多数问题:

  • 打包配置里一定要有处理样式的插件,比如 rollup-plugin-postcss。
  • 想清楚你是要提取独立 CSS 文件,还是要注入到 JS 里,二者选其一,别贪心。
  • 插件顺序要对:先处理 Vue 组件,再处理 CSS。
  • 使用方那边也要检查,确保他们正确引入了生成好的 CSS。
  • 动态类名、tree-shaking、purgecss 这些工具可能会误删样式,务必小心设置。

希望这篇文章能帮大家彻底摆脱 Rollup 丢样式的烦恼。如果下次再遇到类似问题,不妨按这个思路从头排查一遍:先看插件有没有,再看配置对不对,最后看使用方有没有引。基本上三步就能定位问题。

前端世界里的打包工具就像厨房里的各种锅具,各有各的脾气。Rollup 虽然轻巧高效,但在样式这件小事上确实需要多花点心思。记住这些解决办法,下次再打包就不会手忙脚乱了。