用 CSS-in-JS 还是传统 CSS?我在真实项目中的一次抉择与思考

Agent实验员
2025-06-13 16:02
阅读 770

开篇:为什么我会想写这篇文章?

开篇:为什么我会想写这篇文章?

我是一名有五年前端开发经验的工程师,从 jQuery 到 React,从 Bootstrap 到 Tailwind CSS,这些年一直在和样式打交道。但真正让我对“写样式的最佳方式”产生思考,还是源于去年参与的一个大型企业级管理系统重构项目。

那是一个典型的中后台系统,界面复杂、组件繁多,而且需要高度可维护性。我们原本是使用传统 CSS + SCSS 的方式来管理样式,但在迭代过程中遇到了不少困扰:

  • 样式冲突频发(比如 class 名重复)
  • 随着组件越来越多,维护成本飙升
  • 动态样式需求增多(根据状态改变颜色、布局等)

于是我们开始考虑引入 CSS-in-JS 方案,最终选择了 emotion。这个决策过程让我收获了很多经验,也让我更深入地理解了现代前端中样式的管理和演进。

今天我想分享一下这次实战经历,希望能帮助你更好地判断在什么场景下使用哪种样式方案。


问题描述:传统 CSS 的痛点浮现

现代网页界面设计示例-1

问题描述:传统 CSS 的痛点浮现

先简单介绍一下那个项目的背景:

这是一个 B2B 类型的企业管理系统,主要功能包括订单管理、用户权限、报表展示、审批流程等,采用的是 React + TypeScript 的技术栈,UI 框架是基于 Ant Design 封装的一套内部组件库。

初期结构

我们初期采用的是传统的 CSS 写法,每个组件目录下都有一个 .scss 文件,通过 import styles from './xxx.module.scss' 的方式导入使用。看起来很标准,也很合理。

但随着项目体量增长,一些问题逐渐暴露出来:

  1. 样式冲突:虽然用了 module.scss,但有时候多个组件之间仍会出现 class 名混淆的问题,特别是在嵌套较深或团队协作时。
  2. 样式可维护性差:某些组件样式文件长达几百行,修改一处往往牵一发动全身。
  3. 动态样式的处理麻烦:比如按钮的 loading 状态、颜色随主题变化等,传统 CSS 很难做到灵活传参,只能靠一堆条件判断拼字符串。
  4. 样式复用困难:设计上有很多通用样式,但每次都需要手动复制粘贴,缺乏统一抽象机制。

有一次我们在上线前发现某个表格组件的 tr:hover 样式被其他地方无意改写了,查了整整半天才找到源头,那次事件让我们下定决心:必须换一种更可控的方式。


解决方案:尝试 CSS-in-JS —— emotion

解决方案:尝试 CSS-in-JS —— emotion

我们对比了几种主流的 CSS-in-JS 方案:styled-components、emotion、linaria 等。

考虑到团队已有 React 经验,以及 emotion 对 TypeScript 和 SSR 的良好支持(虽然我们当时没有 SSR,但为了未来扩展性做准备),我们最终选定了 emotion

为什么选 emotion?

  • 支持 styled API,和 styled-components 语法基本一致
  • 优秀的 TypeScript 支持
  • 可以通过插件实现 CSS 提取构建优化(配合 webpack)
  • 社区活跃且文档清晰

实践过程:逐步替换 vs 全量切换?

由于这是一个老项目,不能一刀切更换所有样式方案,我们采取了逐步替换的方式:

  1. 新建组件一律使用 emotion 编写样式
  2. 旧组件只在涉及样式变更时重构为 emotion 写法
  3. 同时保留部分 SCSS 用于全局变量和字体图标等基础资源

这样做的好处是:不影响现有业务进度,又能逐步验证新方案的可行性。

关键代码示例

下面是一个简单的组件样式改造前后的对比:

原始 SCSS 写法(component/Table/index.js):

import React from 'react';
import styles from './table.module.scss';

const Table = ({ data }) => {
  return (
    <table className={styles.table}>
      <thead>
        <tr className={styles.headerRow}>
          <th>姓名</th>
          <th>年龄</th>
        </tr>
      </thead>
      <tbody>
        {data.map((item) => (
          <tr key={item.id} className={styles.row}>
            <td>{item.name}</td>
            <td>{item.age}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};
// component/Table/table.module.scss

.table {
  width: 100%;
  border-collapse: collapse;

  .headerRow {
    background-color: #f5f5f5;
  }

  .row:hover {
    background-color: #fafafa;
  }
}

使用 emotion 后的写法:

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import React from 'react';

const tableStyle = css`
  width: 100%;
  border-collapse: collapse;
`;

const headerRowStyle = css`
  background-color: #f5f5f5;
`;

const rowStyle = (hoverBgColor) => css`
  &:hover {
    background-color: ${hoverBgColor};
  }
`;

const Table = ({ data, hoverBgColor = '#fafafa' }) => {
  return (
    <table css={tableStyle}>
      <thead>
        <tr css={headerRowStyle}>
          <th>姓名</th>
          <th>年龄</th>
        </tr>
      </thead>
      <tbody>
        {data.map((item) => (
          <tr key={item.id} css={rowStyle(hoverBgColor)}>
            <td>{item.name}</td>
            <td>{item.age}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default Table;

可以看到几点优势:

  • 样式直接定义在 JS 中,无需跳转到 SCSS 文件查看
  • 动态传参更自然(hoverBgColor 可配置)
  • 组件即样式容器,逻辑与表现紧密关联,方便封装和复用

踩坑经验:CSS-in-JS 并非完美解决方案

虽然整体体验不错,但在实施过程中我们也踩了一些坑,这些教训值得分享。

1. 性能问题:频繁创建样式导致性能下降

刚开始我们习惯把样式写在组件内部,比如:

const Button = ({ color = 'blue' }) => {
  const buttonStyle = css`
    background: ${color};
  `;
  
  return <button css={buttonStyle}>点击</button>;
};

这样做确实很方便,但也带来了一个潜在问题:每次渲染都会重新计算 css 字符串并注入到 DOM 中,影响性能。

特别是像表格组件中渲染大量单元格时,很容易造成卡顿。

解决方法:

  • 将静态样式提到组件外定义
  • 使用 useMemo 缓存带参数的样式对象
const useButtonStyle = (color) => useMemo(() => css`
  background: ${color};
`, [color]);

2. 样式覆盖问题:emotion 插入顺序影响样式优先级

CSS 是依赖插入顺序的,而 emotion 默认会将样式插入到 <head> 的最末尾。但我们有些组件需要覆盖 Ant Design 的默认样式。

解决方法:

  • 使用 @emotion/babel-plugin 插件,启用 prepend 选项插入样式到 <head> 最前面
  • 或者使用 Global 组件定义全局样式
import { Global, css } from '@emotion/react';

<Global
  styles={css`
    .ant-btn {
      font-weight: 600;
    }
  `}
/>

3. 构建体积大:emotion 默认不提取 CSS

如果我们不做特殊配置,emotion 的样式会以内联方式注入 DOM,这会导致首次加载速度变慢。

解决方法:

安装并配置 @emotion/babel-plugin@emotion/minify-webpack-plugin,启用 CSS 提取:

npm install --save-dev @emotion/babel-plugin @emotion/minify-webpack-plugin

Babel 配置:

{
  "plugins": ["@emotion/babel-plugin"]
}

Webpack 配置:

const { MinifyPlugin } = require('@emotion/minify-webpack-plugin');

module.exports = {
  plugins: [
    new MinifyPlugin(),
  ]
}

效果总结:重构之后的变化

经过几个月的逐步迁移和调试,我们最终完成整个项目超过 70% 的组件样式重构工作。效果如下:

  • 样式冲突明显减少:因为 emotion 的 classname 是自动生成的,几乎不会再出现命名冲突
  • 样式可维护性提升:样式和组件逻辑在同一文件中,维护起来更直观
  • 动态样式更灵活:可以轻松根据 props 生成不同样式,提升了组件的可配置性和复用能力
  • 团队协作更顺畅:新人加入后更容易理解组件结构和样式逻辑

虽然初期学习曲线稍陡,特别是对刚接触 CSS-in-JS 的同学来说,但一旦适应之后开发效率显著提升。


经验分享:如何选择 CSS-in-JS 或传统 CSS?

用户交互流程图-2

经过这次项目实践,我对两种方案有了更明确的认知。以下是我给前端开发者的建议:

✅ 推荐使用 CSS-in-JS 的场景:

  • 组件化程度高、组件数量多的项目(如中后台系统)
  • 需要动态控制样式,比如主题色、状态变化样式等
  • 团队有一定的工程化能力,能够承担一定的构建成本
  • 项目有长期维护计划,希望避免样式混乱

❌ 不太推荐使用 CSS-in-JS 的场景:

  • 静态页面居多、交互简单的网站(如企业官网)
  • 强调 SEO 或 SSR 首屏速度(未做 CSS 提取)
  • 团队对构建工具链不够熟悉,容易出错
  • 项目体量较小,追求轻量快速启动

当然,这只是一个大致方向,具体情况还需要结合团队和技术栈综合评估。


结语:样式只是手段,解决问题才是目的

最后我想说,无论是 CSS-in-JS 还是传统 CSS,都只是我们表达 UI 的工具。没有银弹,也没有绝对的好坏之分。

作为开发者,我们要保持开放的心态,理解每种方案的优势和适用场景,并在实践中不断调整自己的认知。

在这个项目之后,我也学会了更理性地看待技术选型,不再迷信“流行趋势”,而是更多地去思考“是否真的适合我们的项目和团队”。

如果你也在纠结应该用哪种方式写样式,不妨动手做个小型 PoC,亲自感受一下两者的差异。毕竟,只有在真实的项目中,才能体会到技术背后的温度。


如果这篇文章对你有帮助,欢迎点赞、收藏或留言交流~

评论 0

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