CSS Modules在SSR中不生效,根本原因是css-loader默认exportOnlyLocals: true,仅导出类名映射对象而不输出CSS内容;服务端无document,style-loader失效,mini-css-extract-plugin又只输出文件,导致renderToString生成的HTML仅有class名而无样式规则,引发FOUC或样式丢失。

服务端渲染中 CSS Modules 的样式不会自动进 HTML —— 因为 css-loader 默认只生成类名映射,不输出 CSS 内容;SSR 时若没显式读取并注入,renderToString 产出的 HTML 就只有 class 名,没有对应样式规则,必然 FOUC 或样式丢失。
为什么 import './Button.module.css' 在 SSR 中不生效
CSS Modules 是编译时机制:import styles from './Button.module.css' 在服务端只是返回一个对象(如 { root: 'Button_root_123abc' }),CSS 内容本身被 css-loader 处理成模块资源,但默认不暴露字符串。Node.js 环境里没有 document,style-loader 完全失效,而 mini-css-extract-plugin 又只输出文件,服务端根本拿不到内容。
- 开发时热更新靠
style-loader注入<style>,但 SSR 不走这路 - 生产构建若用了
mini-css-extract-plugin,CSS 被写成.css文件,服务端需额外读取并内联 —— 但文件路径、哈希、多 chunk 场景下极难可靠定位 -
css-loader必须配exportOnlyLocals: false(默认是true),否则连css.toString()都不可用
如何从 CSS Modules 中安全提取样式字符串
核心是让 css-loader 在服务端构建产物中保留 CSS 内容,并在渲染前按需收集。不是靠运行时 require,而是构建时导出 + 服务端静态读取。
- Webpack 配置中,对
.module.css规则禁用mini-css-extract-plugin,改用css-loader+string-loader或自定义 loader 输出 CSS 字符串 - 更稳妥的做法:用
css-loader的modules.exportLocalsConvention: 'camelCaseOnly'+exportOnlyLocals: false,再配合extract-css-chunks-webpack-plugin(非mini-css-extract-plugin)—— 它提供getCssString()API,可在 Node 环境调用 - Vite 用户应避免
import语句直接触发样式加载;改用ssrLoadModule动态导入模块后,从其__vite_ssr_exports__或default上提取css属性(需插件支持)
样式注入顺序错乱导致客户端覆盖
CSS Modules 本身不管理注入顺序。服务端若把所有模块 CSS 拼成一个大字符串塞进 <head>,它会出现在所有 <link> 之后,优先级被外部 CSS 覆盖;hydrate 后客户端又按组件顺序重插 <style>,造成样式跳变。
立即学习“前端免费学习笔记(深入)”;
- 必须按组件渲染依赖顺序收集:在
renderToString前新建隔离上下文(如createServerContext()),让每个组件渲染时把自身 CSS 推入该上下文 - 不要复用全局样式收集器;HTTP 请求间必须清空或新建实例,否则用户 A 的样式可能污染用户 B 的 HTML
- 注入位置要严格控制:critical CSS 字符串必须放在
<head>开头,且在任何<link rel="stylesheet">之前,否则 specificity 规则会失效
真正麻烦的不是“怎么拿到 CSS 字符串”,而是“怎么确保它在正确时机、以正确顺序、注入到正确位置”——这三个“正确”缺一不可,漏掉任一环节,FOUC 就会回来。


















