next.js服务端渲染中bem易出错,因字符串拼接类名会导致ssr与hydration不一致、css modules作用域失效、typescript无法校验;须用block常量+clsx封装、禁用scss嵌套、配合stylelint/eslint工具链拦截非法写法。

不能直接在服务端组件里硬写 className="card__header--expanded",否则会破坏 SSR 一致性、绕过 CSS Modules 作用域、且无法被 TypeScript 校验。
为什么 Next.js 服务端渲染中 BEM 容易出错
Next.js 的 App Router 默认启用服务端渲染,所有组件在服务器端首次生成 HTML。此时若用字符串拼接类名(比如 className={`button__text ${isLoading ? 'button__text--loading' : ''}`}),会导致:
- 服务端生成的 class 字符串和客户端 hydration 后的 class 不一致,触发 React 警告甚至样式闪烁
- 类名脱离 Block 边界(如漏写
user-card__avatar写成avatar),CSS Modules 无法匹配,样式丢失 - 修饰符值未校验(如
--mobile-only),上线后才发现它没定义,但 JS 已经加了 class - SCSS 嵌套写法(如
.card { .title { } })编译后变成后代选择器,破坏 BEM 封装性,且构建时无法反向定位来源
React Server Component 中如何安全生成 BEM 类名
必须把 Block 名抽象为常量,并用函数封装生成逻辑,确保所有类名都可追溯、可校验、可批量替换:
- 每个组件顶部声明
const BLOCK = "user-card",禁止任何未声明的 Block 字符串出现在 JSX 中 - 用
clsx或自定义cn函数处理条件:clsx(`${BLOCK}__avatar`, { [`${BLOCK}__avatar--loading`]: isLoading }) - 禁止在
page.tsx或layout.tsx中直接写带__或--的类名字符串,所有派生类名必须由BLOCK变量驱动 - 若使用 CSS Modules,导入必须是
import styles from "./UserCard.module.css",然后用styles[`${BLOCK}__avatar`]—— 但注意:CSS Modules 不支持动态 key,所以仍需靠clsx+ 全局 CSS 配合
如何让 BEM 在 Next.js 构建阶段就被拦截错误
光靠人肉约定不行,得靠工具链卡口:
- 安装并配置
stylelint-selector-bem-pattern,规则设为{"styleType": "bem"},CI 中跑npx stylelint "**/*.css"阻断.btn--primary-error这类非法修饰符提交 - VS Code 安装
BEM Tools插件,自动高亮__/--错位(如card_header--large缺少一个_) - 禁用 SCSS 嵌套语法:在
tailwind.config.ts或构建配置中关闭postcss-nested,强制平铺书写.user-card__content,避免编译后丢失 Block 上下文 - 在 ESLint 中添加自定义规则,检查 JSX 中是否出现未声明的
BLOCK字符串(例如检测到"product-card__image"但当前文件无const BLOCK = "product-card")
全局 CSS 和 CSS Modules 混用时的 BEM 边界怎么划
Next.js 允许同时用 globals.css(全局)和 *.module.css(模块化),BEM 必须严格按 Block 归属划分:
-
globals.css只放真正跨项目的 Block,如reset、typography、layout-grid,且必须保证 Block 名全局唯一(layout-grid__col不得与业务组件同名) - 每个业务组件(如
UserCard)必须有自己的UserCard.module.css,里面只写.user-card {}、.user-card__avatar {},禁止出现.card__avatar(父 Block 名不完整) - 第三方 UI 库(如
@radix-ui/react-dialog)绝对不要重写成modal__overlay,它不是你的 Block,强行套 BEM 会阻断库升级、破坏主题注入机制 - 如果要用 Tailwind,BEM 类名只用于语义结构(
user-card__header),视觉样式全部交给class="bg-gray-100 p-4 rounded"—— 二者不冲突,但别混用同一层目的(比如既写user-card__header--compact又加text-sm)
最常被忽略的一点:BEM 的 Block 名不是“页面名”或“文件名”,而是语义单元。写 app/blog/[id]/page.tsx 时,里面的卡片组件仍应是 article-card,而不是 blog-page__card —— 后者把 Block 绑死在路由层级,失去复用能力,也违背 BEM “Block 独立可移植”的根本前提。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











