CSS-in-JS vs 传统CSS:现代样式方案选择指南

云端造物者
2025-06-13 06:55
阅读 360

当传统 CSS 遇上 React 世界:我的样式方案进化之路

当传统 CSS 遇上 React 世界:我的样式方案进化之路

作为一个前端开发工程师,我参与过多个中大型项目的架构设计和开发工作。从后端转向前端的那一年起,我就一直和“样式”这个看似简单实则深不见底的问题打交道。今天想和大家分享一下我在项目实践中遇到的一个经典问题:如何在现代前端框架(尤其是 React)下合理选择 CSS 的组织方式?

这个问题听起来似乎很简单,但在实际开发过程中,特别是在团队协作、可维护性和性能优化方面,往往成为影响项目质量的关键点。


初识困境:一个组件库升级引发的“血案”

事情得从我负责的一次 UI 组件库重构说起。我们当时维护着一个基于 React 的企业级后台管理系统,使用了传统的 CSS + SCSS 模块化方案,并通过 BEM 命名规范来管理类名冲突的问题。

但随着组件数量的增长,我们遇到了几个明显的问题:

  1. 命名冲突频发:即使我们遵循 BEM 规范,仍然偶尔会在不同页面上出现相同类名造成的样式污染。
  2. 共享变量难以统一:SCSS 变量虽然可以在项目中共享,但当组件以 npm 包的形式发布时,这些变量无法被使用者继承,导致主题定制困难。
  3. 调试和定位样式问题耗时变长:浏览器中的 DevTools 看到的是经过 webpack 提取后的 class 名,跟原始代码完全对不上,排查成本剧增。
  4. CSS 文件体积逐渐失控:由于没有严格的按需加载机制,构建后的 CSS 资源越来越大。

这些问题在一次产品大版本迭代前集中爆发。我们在尝试升级某个核心组件库时,发现引入新组件会导致已有页面样式错乱,最终耗费了整整两天去排查冲突原因——结果是一个第三方组件不小心注入了一个全局 .btn 样式。

那一刻我意识到,是时候重新审视我们的样式解决方案了。


探索与尝试:CSS-in-JS 是否真香?

既然传统 CSS 遇到了瓶颈,我们就把目光转向了近年来非常热门的 CSS-in-JS 方案。像 styled-components、emotion 这类库在社区里口碑不错,号称可以实现“真正意义上的作用域隔离”,还能通过 JS 动态生成样式逻辑。

于是我们决定在一个小型内部项目中做对比实验。

实验背景

目标是一个用户设置页面,包含多个状态驱动样式的交互组件,比如表单项的禁用态、验证错误提示、切换暗黑/亮色模式等。

我们分别用两种方案实现了同样的功能模块:

  • 方案 A:继续使用 SCSS + BEM
  • 方案 B:改用 emotion(CSS-in-JS)

关键差异点观察

维度 SCSS 方案 Emotion 方案
类名唯一性 完全依赖人为约定,有风险 自动命名,无需担心冲突
条件样式 使用条件判断生成类名,搭配 SCSS Mixin 复杂 直接通过对象拼接实现,逻辑清晰
主题变量传递 全局注入 SCSS 变量,难以跨包共享 可以结合 context API 实现动态主题
开发体验 类名映射 DevTools 不友好 浏览器显示有意义的标识符(如 css-abc123
构建性能 单个 CSS 文件体积较大,首屏加载慢 内联样式,首屏无额外阻塞资源

说实话,这次实验让我有了很大的转变。Emotion 在条件样式上的灵活性让我印象深刻——以前需要用多个类名组合的方式现在可以直接写成三元表达式嵌入 JSX 中,大大提高了组件本身的可读性。

而且更重要的是,我们不再需要花时间制定复杂的命名规则,也不再因为全局污染而提心吊胆。


实践落地:我们为何选择了 Emotion?

在那次实验之后,我们逐步将部分关键业务组件迁移到了 Emotion,并在后续的新项目中全面采用 CSS-in-JS 方案。

我们的迁移策略

为了不影响现有业务,我们采用了“渐进式迁移”的策略:

  1. 保留原有 SCSS 结构,仅新增组件使用 Emotion
  2. 封装主题样式上下文,统一样式变量注入
  3. 编写通用工具函数,兼容旧有 SCSS 变量逻辑

举个小例子:我们有一个高频率使用的按钮组件 BaseButton。之前的 SCSS 是这样写的:

// _base-button.scss
.base-button {
  padding: 12px 16px;
  border-radius: 4px;
  font-size: 14px;

  &--primary {
    background-color: $brand-color;
    color: white;
  }

  &--disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
}

而在 Emotion 中,我们重写为:

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import { useTheme } from 'your-theme-context';

export const BaseButton = ({ variant = 'default', disabled, children }) => {
  const theme = useTheme();

  const baseStyles = css`
    padding: 12px 16px;
    border-radius: 4px;
    font-size: 14px;
    transition: all 0.2s ease;
  `;

  const variantStyles = {
    default: css`
      background-color: ${theme.gray[200]};
      color: ${theme.text.primary};
    `,
    primary: css`
      background-color: ${theme.colors.brand};
      color: white;
    `,
  };

  return (
    <button css={[baseStyles, variantStyles[variant], disabled && disabledStyle]}>
      {children}
    </button>
  );
};

const disabledStyle = css`
  opacity: 0.6;
  pointer-events: none;
`;

是不是更清晰了?特别是对于动态主题的支持,Emotion 几乎无缝对接。我们也借此机会梳理了整个系统的颜色、字体和间距体系。


踩过的坑:别以为 CSS-in-JS 就万能了

尽管 Emotion 给我们带来了不少便利,但也并不是完全没有问题。下面是我踩过的一些坑,希望你能少走弯路。

1. SSR 支持没做好,首次加载样式闪一下

刚开始我们只顾着客户端渲染的体验,忽略了服务端渲染场景。结果上线后用户反馈首页首次打开会有样式闪动的问题。

原因分析:Emotion 默认会在客户端插入 <style> 标签,而服务端渲染的 HTML 并不包含这些样式内容,因此会出现 FOUC(无样式内容闪烁)。

解决方式
我们后来引入了 @emotion/serverrenderToString 时的样式提取插件,确保服务端也能输出带有内联样式的 HTML,避免视觉抖动。

// Express SSR 中间件片段
import { extractCritical } from '@emotion/server';

app.get('/', (req, res) => {
  const appHtml = ReactDOMServer.renderToString(<App />);
  const { html, ids, css } = extractCritical(appHtml);

  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <style data-emotion="css">${css}</style>
      </head>
      <body>
        <div id="root">${html}</div>
        <script>window.__EMOTION__ = ${JSON.stringify(ids)}</script>
      </body>
    </html>
  `);
});

2. 性能瓶颈:过度内联导致渲染卡顿

我们曾经在一个大数据列表组件中大量使用 inline styles,每个条目都会根据状态生成不同的样式对象。结果在浏览器中出现了明显的滚动卡顿。

解决思路

  • 首先通过 React Profiler 定位到是 Emotion 的处理拖慢了渲染速度;
  • 接着优化了那些频繁变化的样式逻辑,改用类名 + 动态计算的方式;
  • 最终拆分出一些通用状态组合,提前生成静态样式对象供复用。

3. 第三方组件库引入样式污染

有些第三方组件会默认注入全局样式,比如 antd、material-ui 等。这会导致我们的局部样式也受到牵连。

对策

  • 优先使用它们提供的自定义主题能力;
  • 如果必须修改样式,尽量在组件外包裹一层容器并使用 @emotion/styled 创建带 scoped 样式的 wrapper;
  • 必要时使用 CSS Modules 或 Shadow DOM 做进一步隔离。

收益与体会:我们得到了什么?

自从我们全面拥抱 Emotion 后,有几个显著的变化:

  1. 团队协作更顺畅了:大家不用再讨论要不要加前缀、怎么命名类名这些琐事,节省了不少沟通成本。
  2. 组件开发效率提升了:样式即刻可见,逻辑和视图紧密结合,调试起来更加直观。
  3. 样式维护变得更容易:删除组件时可以直接顺手删掉对应的样式代码,不需要再去查 scss 文件的引用关系。
  4. 主题系统统一了:我们基于 Context 提供了一套完整的可定制主题系统,甚至支持运行时切换。

当然,这一切的前提是我们合理地使用了 CSS-in-JS,并没有滥用它。我们依然保留了一些 SCSS 的能力来做全局布局、媒体查询和某些复杂动画。


给你的建议:如何选择适合自己的方案?

如果你也在考虑是否要用 CSS-in-JS,这里是我的一些建议:

✅ 适合使用 CSS-in-JS 的情况:

  • 正在使用 React 或其他现代前端框架
  • 组件复用性强,追求样式隔离
  • 需要动态样式或主题定制能力
  • 团队人数较多,容易产生样式冲突
  • 构建过程已经比较现代化(比如 Webpack/Vite/Babel)

❌ 不太适合 CSS-in-JS 的情况:

  • 项目技术栈老旧,尚未引入 JSX 编译流程
  • 有大量遗留 CSS 代码,迁移成本过高
  • 极度关注 SEO,对首屏性能要求极高
  • 纯静态网站,无需 JS 控制样式逻辑

📌 一些小技巧分享:

  • 如果你只是想试试看,可以从一个小的 feature 页面开始,不要一开始就搞全量迁移;
  • 使用 eslint-plugin-css-in-js 插件帮助检查样式写法是否合理;
  • 使用 @emotion/babel-plugin 可以自动给样式标签加上文件位置信息,方便调试;
  • 对于公共样式,也可以封装成一个 shared-styles.js,减少重复代码;
  • 配合 Tailwind 的思路,封装一套你自己的 styled utility 函数也很实用。

尾声:不是替代,而是演进

最后我想说的是,CSS-in-JS 并不是要“取代”传统 CSS,而是前端开发不断演进的结果。我们面对的环境越来越复杂,组件越来越多,样式逻辑也越来越丰富。这个时候,用 JavaScript 的方式去组织和管理样式,反而是一种自然的选择。

我也见过有人坚持使用 SASS 或者 PostCSS + CSS Modules 来解决问题,同样可以做得很好。关键是:要根据自己的项目特点、团队规模和技术栈,选择最合适的方式

无论是传统的 CSS 还是新兴的 CSS-in-JS,都是我们手中的工具。工具本身没有绝对的好坏,只有合适与否。作为一名开发者,最重要的是理解它们背后的工作原理,从而做出理性的选择。

希望这篇文章能给你带来一些启发,在下一次选型时多一份底气和信心。如果还有疑问,欢迎留言交流,咱们一起探讨!


🔚 文章写到这里就结束了。这篇文章基于我个人经历整理而成,如有雷同,纯属巧合 😊

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝