Node.js SSR场景下ESM项目中如何导入CSS Modules?
解决Node.js ESM环境下导入CSS模块的报错问题
方案1:使用@node-loader/css-modules(推荐,适配ESM)
这是专门为Node.js ESM设计的CSS模块加载器,能直接处理import style from './xxx.module.css'语法,还能配置和Webpack一致的类名生成规则。
- 安装依赖:
npm install @node-loader/css-modules --save-dev - 配置类名规则(可选,确保和Webpack一致):
在项目根目录创建css-modules.config.json,写入和你Webpack配置中modules.localIdentName一致的规则,比如:{ "generateScopedName": "[name]__[local]--[hash:base64:5]" } - 启动Node.js服务时指定加载器:
修改package.json的启动脚本,或者直接在命令行添加参数:
示例package.json脚本:node --experimental-loader @node-loader/css-modules ./server-entry.js{ "scripts": { "start": "node --experimental-loader @node-loader/css-modules server.js" } }
方案2:自定义ESM加载器
如果需要更灵活的控制,可以自己编写Node.js ESM加载器,用PostCSS解析CSS模块。
- 安装依赖:
npm install postcss postcss-modules --save-dev - 创建加载器文件
css-module-loader.mjs:import { readFile } from 'fs/promises'; import postcss from 'postcss'; import postcssModules from 'postcss-modules'; // 配置类名生成规则,和Webpack保持一致 const generateScopedName = '[name]__[local]--[hash:base64:5]'; export async function load(url) { if (!url.endsWith('.module.css')) return null; const cssContent = await readFile(new URL(url), 'utf8'); let classMap = {}; await postcss([ postcssModules({ getJSON: (_, map) => { classMap = map; } }) ]).process(cssContent, { from: url }); // 将类名映射转为ESM模块代码 const moduleCode = `export default ${JSON.stringify(classMap)};`; return { source: moduleCode }; } // 其他必要的加载器钩子,直接返回默认值 export function resolve(specifier, context, defaultResolve) { return defaultResolve(specifier, context); } export function transform(source, context, defaultTransform) { return defaultTransform(source, context); } - 启动服务时指定自定义加载器:
node --experimental-loader ./css-module-loader.mjs ./server-entry.js
方案3:预生成CSS模块的JS映射文件(无实验性依赖)
如果不想依赖Node.js的实验性加载器,可以在构建阶段提前把CSS模块编译成ESM格式的JS文件,直接导出类名映射。
- 编写编译脚本
build-css-modules.mjs:import { readdir, readFile, writeFile } from 'fs/promises'; import { join, extname, basename } from 'path'; import postcss from 'postcss'; import postcssModules from 'postcss-modules'; const generateScopedName = '[name]__[local]--[hash:base64:5]'; async function processCssModule(filePath) { const cssContent = await readFile(filePath, 'utf8'); let classMap = {}; await postcss([ postcssModules({ getJSON: (_, map) => { classMap = map; } }) ]).process(cssContent, { from: filePath }); // 生成对应的.mjs文件 const outputPath = `${filePath}.mjs`; await writeFile(outputPath, `export default ${JSON.stringify(classMap)};`, 'utf8'); console.log(`Generated ${outputPath}`); } async function scanDir(dir) { const files = await readdir(dir, { withFileTypes: true }); for (const file of files) { const fullPath = join(dir, file.name); if (file.isDirectory()) { await scanDir(fullPath); } else if (extname(file.name) === '.css' && file.name.includes('.module.')) { await processCssModule(fullPath); } } } // 扫描src目录下的所有CSS模块文件 scanDir('./src'); - 运行脚本生成JS映射文件:
node ./build-css-modules.mjs - 修改代码中的导入路径:
将import style from './Body.module.css'改为import style from './Body.module.css.mjs',或者在TS/JS配置中添加路径别名,自动映射。
额外注意事项
- TypeScript类型支持:如果用TSX,需要在项目中添加CSS模块的类型声明文件
css-modules.d.ts:declare module '*.module.css' { const classes: { [key: string]: string }; export default classes; } - Webpack配置对齐:确保所有方案中的
generateScopedName和你Webpack配置里的modules.localIdentName完全一致,这样前后端的类名才能匹配。
内容的提问来源于stack exchange,提问作者Flion
相关产品推荐
相关产品推荐

