CSS-in-JS 与传统 CSS:样式方案的选择,不只是技术之争

半夜部署日记
2025-06-27 00:12
阅读 445

在前端开发这条路上,我们经常会遇到各种“路线之争”。而CSS-in-JS 和传统 CSS 的争论,可能是近年来最持久、也最具争议的技术话题之一。

作为一名拥有多年全栈开发经验的团队负责人,我亲身经历过从刀耕火种的手写 CSS 时代,到拥抱模块化工程化的过渡期,再到如今组件化体系下的多种样式解决方案共存。在这个过程中,我也带领团队尝试过多个项目采用不同的样式策略,并为此吃过不少亏,也踩过不少坑。

今天这篇文章,我想通过一个真实的项目案例,来和大家聊聊为什么我们要选择某种方案、当时面对了哪些问题、又做了哪些取舍,以及最后得到的启示。


一、背景介绍:为什么这个讨论重要?

一、背景介绍:为什么这个讨论重要?

事情要从几年前说起。那时我们在做一个企业级管理后台系统,目标是打造一个高度可复用、可维护、风格统一的 UI 组件库。整个系统由 React 构建,配合 TypeScript 开发,希望能够在多个产品线中共享使用。

最初的设计决策非常简单:使用传统的 CSS 方案,通过 BEM 命名规范结合 PostCSS 插件,实现模块化的样式管理。但随着项目规模的增长,特别是组件数量不断增加,我们开始面临一系列令人头疼的问题:

  • 命名冲突问题频发
  • 样式难以隔离(尤其是多个组件嵌套时)
  • 主题切换困难(需要额外的变量管理和处理逻辑)
  • 协作成本高(不同人写的样式结构差异大)

这些问题直接影响到了开发效率和交付质量。于是我们开始重新思考:“有没有一种更现代、更可控的方式来管理样式?”

这就是我们第一次真正意义上认真地去评估——是否要从传统的 CSS 方案转向 CSS-in-JS


二、问题描述:传统 CSS 在大型项目中的痛点

二、问题描述:传统 CSS 在大型项目中的痛点

我们以实际场景为例来说明。

案例一:组件命名冲突导致样式污染

在某个订单模块中,我们定义了一个名为 .order-table 的类名。后来另一个功能模块也开始使用 .order-table 作为其表格容器的类名。虽然语义上是一致的,但由于两个模块独立开发,样式结构和优先级不同,导致页面出现错乱,调试起来异常费劲。

/* A 模块 */
.order-table {
  background: #f0f0f0;
}

/* B 模块 */
.order-table td {
  padding: 20px;
}

最终效果可能是 A 模块的表格 TD 却有了 B 模块的 padding,这不是设计所期望的。

这类问题的根本原因在于:全局样式没有隔离机制,而且命名规则依赖人为遵守,容易出错。

案例二:主题切换支持不友好

公司想要一套可以动态换肤的主题系统,用于多品牌展示。但在传统 CSS 中,我们需要借助 SASS 变量 + mixin 来实现。每个主题都需要一套完整的 CSS 文件,构建时根据主题注入对应的 class 到 body 上才能生效。

这种方案存在几个问题:

  • 编译体积膨胀(每个主题单独打包)
  • 动态切换能力差(页面可能需要强制刷新或 JS 手动修改)
  • 复杂度高,不易维护

这对我们后续的扩展造成了不小的困扰。


三、我们的探索:引入 CSS-in-JS 方案

三、我们的探索:引入 CSS-in-JS 方案

基于上述痛点,我们决定尝试使用 CSS-in-JS 技术栈来重构部分组件,并观察其在工程化、样式管理等方面的效果。

我们调研了几种主流方案,包括但不限于:

工具 特点 适用场景
styled-components 支持模板字符串、样式即组件 React 生态最佳,适合中小型项目
emotion 支持编译优化、服务端渲染友好 大型应用、SSR 场景
JSS 面向对象配置式 API 更适合非 JSX 写法项目
Linaria 构建时生成静态 CSS 极简主义者首选

最终,我们选择了 emotion 作为实验性接入的主要方案,主要原因有两点:

  1. 性能稳定,在 SSR 场景下表现优异;
  2. 支持两种写法(styled API 和 object styles),灵活性更高。

四、代码实践:如何优雅地使用 emotion 实现样式封装

接下来分享几个核心代码片段,帮助你理解我们在实践中是如何组织样式的。

示例一:使用 styled API 定义组件样式

import styled from '@emotion/styled';

const Button = styled.button`
  background-color: ${props => props.primary ? '#3b82f6' : '#e5e7eb'};
  color: ${props => props.primary ? 'white' : 'black'};
  padding: 0.5rem 1rem;
  border-radius: 4px;
  font-size: 1rem;

  &:hover {
    opacity: 0.9;
  }
`;

function App() {
  return (
    <Button primary>提交</Button>
  );
}

这种方式的优点是:

  • 样式直接绑定组件,作用域封闭;
  • 可以接收 props 参数,实现动态样式;
  • 易于测试和复用。

示例二:使用 sx Prop(适用于 emotion + MUI)

在整合 Material-UI 的时候,我们还使用了 emotion 提供的 @emotion/react 插件,从而可以像 MUI 的新版本那样使用 sx prop:

<Box sx={{ color: 'primary.main', m: 2 }}>
  自定义样式盒
</Box>

这种组合方式大大提升了样式编写效率,尤其是在需要快速调整组件内部元素样式时非常方便。


五、踩过的坑和实战经验

任何新技术的应用都不是一帆风顺的。下面分享我们在引入 CSS-in-JS 过程中踩过的一些坑,以及应对之道。

1. 构建性能下降

初期在未启用 emotion 插件的构建优化时,我们发现整体打包时间明显增加,JS bundle 体积也有一定的上涨。

解决方案:

  • 启用了 emotion 的 extractStaticStyles 插件,将样式提取为外部 CSS 文件;
  • 对核心组件进行按需加载,避免一次性全部引入;
// Webpack emotion 插件配置示例
{
  test: /\.(tsx?|jsx?)$/,
  loader: 'babel-loader',
  options: {
    plugins: [
      ['@emotion/babel-plugin', { labelFormat: '[filename]__[local]' }]
    ]
  }
}

2. 调试变得麻烦了

CSS-in-JS 的好处是样式封闭,但也带来一个问题:当你打开 DevTools 查看某个元素时,类名不再是语义清晰的 .btn-primary,而是类似 _abcxyz123 的 hash 字符串,给调试造成一定困扰。

解决方案:

  • 使用 emotion 提供的 labelFormat 配置项,让类名保留组件路径信息;
  • 配合 Chrome DevTools 的“Computed Style”面板定位样式来源;
  • 使用 React Developer Tools 查看组件层级和 props 传值情况;
['@emotion/babel-plugin', { labelFormat: '[filename]__[local]' }]

这样你可以看到类似 OrderTable__root_abcd123 的类名,就能快速定位到源文件。

3. 样式重用和组件拆分策略不当引发耦合

曾经我们在封装一个通用按钮组件时,直接在 styled.button 里写了过多内联样式逻辑,结果导致组件内部样式逻辑臃肿不堪,后期维护极其困难。

优化建议:

  • 将样式逻辑抽离为多个基础 style 函数或对象;
  • 使用组合的方式构建组件样式;
  • 尽量保持样式函数职责单一,便于复用;

六、实施效果:我们的收益和变化

经过几个月的迭代和打磨,我们将核心组件迁移到 emotion,整体效果还是相当显著的:

指标 接入前 接入后 提升幅度
类名冲突问题 高频率 基本消除
主题切换支持 手动处理复杂 动态变量+插件自动处理 ✅✅
代码可维护性 中等 更加清晰 ✅✅
构建体积 不变 微增(可通过优化控制) ⚠️
团队协作效率 较低 显著提升 ✅✅✅

特别值得一提的是,新加入的成员能够更快地上手项目,因为样式逻辑不再分散在多个 CSS 文件中,而是紧贴组件本身,降低了上下文切换成本。


七、总结与建议:选对工具比争对立场更重要

在经历过这场 CSS 管理方式的转变之后,我的一些体会如下:

✅ 正确看待两者优劣

  • 传统 CSS:轻量、兼容性好、构建快,在小型项目或静态站点中依然是性价比极高的选择;
  • CSS-in-JS:提供了更高的可维护性和封装性,适合中大型 React 项目、需要动态样式的场景,但也有构建开销和调试学习成本。

✅ 实际选型应考虑这些维度

  • 项目规模:小型项目建议传统 CSS;
  • 协作人数/团队习惯:多人协同推荐 CSS-in-JS;
  • 交互复杂度:需要大量状态驱动的样式变更推荐 CSS-in-JS;
  • 性能敏感度:极致追求首屏加载速度,传统 CSS 能做得更好;
  • 未来扩展:如果涉及微前端、多子系统集成,样式隔离变得至关重要。

✅ 我个人的经验之谈

如果你是刚起步的开发者,不妨先掌握传统 CSS 的基本功再谈封装;而如果你已经在一个中大型 React 项目中工作,那么真的值得花一点时间来了解并实践 CSS-in-JS。

它并不是银弹,但确实为我们在规模化开发中提供了一种更现代化的解法


八、结语:不要纠结技术本身,而要关注解决问题的能力

回想整个过程,其实我们纠结的从来不是“哪种技术更好”,而是“哪种技术能更好地解决当下的问题”。

技术没有绝对的好坏,只有合适与否。

正如我在一次组内分享会上讲的那句话:

“我们不是为了炫技而去选择什么方案,而是为了写出让人舒服、别人看得懂、自己睡得着的代码。”

愿你在做选择的时候,也能保持这样的清醒和从容。


如你有兴趣了解更多具体项目的迁移细节、emotion 与 Tailwind CSS 的对比、或者 CSS-in-JS 的最佳实践模式,欢迎留言交流。也欢迎分享你们团队在样式管理上的经验和挑战。

共勉 😊

评论 0

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