从传统CSS到CSS-in-JS:我如何在大型项目中做出样式方案的抉择

GC观察员
2025-06-14 09:35
阅读 419

开篇 | 一场重构带来的思考

开篇 | 一场重构带来的思考

去年我在参与公司核心产品的前端重构时,遇到了一个很典型的问题:随着项目规模的增长,传统的CSS管理模式已经越来越难维护了。组件复用性低、命名冲突严重、样式难以追踪,尤其是在不同团队协同开发时,样式污染和样式优先级问题经常让我们焦头烂额。

我们当时面临两个选择:

  1. 继续使用传统的 CSS 模块化方案(BEM + SCSS)
  2. 尝试迁移到 CSS-in-JS 的现代方案(我们选的是 styled-components)

最终我们选择了后者,并且这个决定确实带来了很多好处——但也踩了不少坑。今天我就想结合这次真实的项目经验,来聊一聊“CSS-in-JS vs 传统CSS”这个话题,不讲太多理论,直接说干活内容。


问题描述 | 当传统CSS开始变得捉襟见肘

问题描述 | 当传统CSS开始变得捉襟见肘

前端性能优化图表-2

我们的项目是一个中后台系统,基于 React 开发,功能模块多,组件库庞大,团队协作频繁。原本采用的是 SCSS + BEM 的方式组织样式,初期一切良好,但到了后期维护阶段,问题接踵而至:

  • 类名冲突频繁出现:比如 .btn、.card 这样的通用样式,在多个页面/组件中被重复定义,容易互相覆盖。
  • 样式作用域不好控制:即便用了 BEM 命名规范,也无法完全避免全局污染,尤其是第三方组件混入进来时更糟。
  • 主题定制困难:整个系统的主题变量散落在多个地方,修改颜色、字号等配置需要改多个文件。
  • 调试困难:Chrome DevTools 查看元素样式时,往往无法直观看到对应代码位置,特别是样式继承层级复杂的时候。
  • 组件封装不彻底:虽然组件本身是封装好的,但样式仍需外部引入 SCSS 文件,增加了耦合度。

这些问题让新入职的同学上手特别慢,也让我们自己每次提测都要花大量时间查样式 bug。


解决方案 | 是时候尝试 CSS-in-JS 了

为了解决上面这些问题,我们在一次架构讨论会上提出了两个初步方案:

  1. 继续优化现有结构:升级 SCSS 工具链,引入 CSS Modules 和 PostCSS 插件提升模块化能力
  2. 尝试 CSS-in-JS 方案:主流有 styled-components、emotion、linaria 等,其中我们对 styled-components 更感兴趣

最终我们做了个小对比实验,把某个高频复用的组件分别用两种方式实现,然后拉上产品和 QA 同学一起评审。结果表明:

维度 传统SCSS CSS-in-JS(styled-components)
开发效率 中等 提升明显
调试体验 一般 更好(可定位具体组件)
样式隔离 靠自觉 自动隔离
主题支持 配置分散 内建 theme provider
构建速度 略慢(但影响不大)
新人上手 困难 易于理解

前端性能优化图表-1

综合下来我们决定:在新模块中启用 styled-components,老模块逐步迁移。这是一个折中但务实的选择,不会一开始就全部重写,风险可控。


实践落地 | 我们是如何推进的

项目背景简要说明

我们项目整体架构如下:

  • 技术栈:React 17 + TypeScript + Webpack + ESLint + Styled-Components
  • UI框架:Ant Design(部分自研组件)
  • 项目类型:PC 端管理系统,用户量大,交互较多

目标是在新增需求模块中优先使用 styled-components,同时提供工具辅助旧模块迁移。

技术方案设计

我们采用了一个渐进式过渡策略:

第一阶段:新建模块统一使用 styled-components

  • 定义新的组件样式文件以 .styled.ts 结尾,如 Button.styled.ts
  • 每个组件只引入自身样式文件,实现真正的“组件即样式容器”
  • 使用 ThemeProvider 实现主题配置共享

示例代码:

// Button.styled.ts
import styled from 'styled-components';

export const StyledButton = styled.button`
  padding: 8px 16px;
  border-radius: 4px;
  background-color: ${props => props.theme.primaryColor};
  color: #fff;
  border: none;
  cursor: pointer;

  &:hover {
    opacity: 0.9;
  }
`;

// 在组件中使用
import { StyledButton } from './Button.styled';
const Button = () => (
  <StyledButton>点击我</StyledButton>
);

第二阶段:为老组件提供迁移指南与脚本

我们开发了一个简单的代码转换工具,帮助将 SCSS 转换成对应的 styled-components 格式,虽然不能完全自动化,但能大大节省人工工作量。

例如自动将:

.my-btn {
  padding: 8px 16px;
  color: white;
}

转成:

const MyBtn = styled.div`
  padding: 8px 16px;
  color: white;
`;

第三阶段:建立统一的主题管理机制

通过 theme-provider 统一管理所有样式变量,比如主色、字号、间距等,确保视觉一致性。

// theme.ts
export default {
  primaryColor: '#1890ff',
  fontSize: '14px',
};

// App.tsx
import { ThemeProvider } from 'styled-components';
import theme from './theme';

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Router>
        <Routes />
      </Router>
    </ThemeProvider>
  );
}

这样在任何组件中都可以轻松访问主题属性:

background-color: ${props => props.theme.primaryColor};

踩过的坑 & 小插曲

再好的方案也会遇到问题,下面是我们在实际推进过程中遇到的一些挑战及解决办法:

1. 类型提示缺失 → 引入 TypeScript 支持

一开始我们只是用了 styled-components 的基本用法,没做类型绑定,导致 IDE 无法自动提示 theme 中的字段,容易出错。

解决方案:

安装 @types/styled-components 并定义接口:

import 'styled-components';

declare module 'styled-components' {
  export interface DefaultTheme {
    primaryColor: string;
    fontSize: string;
  }
}

这之后,敲 props.theme. 就能获得完整的自动补全了。

2. SSR 不友好 → 增加服务器端渲染适配

我们在服务端渲染项目中使用 Next.js 时发现,首次加载没有注入 CSS,导致闪烁。

解决方法:

引入 ServerStyleSheet 并在 _document.tsx 中处理:

// pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        });

      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      };
    } finally {
      sheet.seal();
    }
  }

  render() {
    return (
      <Html lang="zh-CN">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

3. 动态样式逻辑复杂时维护成本高

有时候我们会写类似这样的条件判断:

${props => props.isActive && css`color: red;`}

但当逻辑变多以后,会变得很难读。

后来我们把一些复杂的逻辑抽象成函数形式:

const getButtonStyle = (props) => `
  background-color: ${props.theme.primaryColor};
  color: #fff;
`;

并配合 as const 来减少重复字符串模板拼接。

4. 编译性能略微下降

在使用 styled-components 时,构建时间会比纯 CSS 略长,特别是在热更新时。我们尝试过 linaria、emotion,最终因为团队成员熟悉度选择了 styled-components。

如果对性能特别敏感,可以考虑像 linaria 这样提前编译成静态 CSS 的方案,更适合 SSR 或对首屏性能要求高的项目。


效果总结 | 成效显著,收益颇多

经过一年的实际使用,我们得出了几点明显的改进:

  1. 组件样式内聚性强:每个组件都自带样式声明,更容易理解和复用
  2. 样式污染大幅减少:每个组件都有唯一的类名生成,不会有意外覆盖
  3. 调试更方便:DevTools 可清晰看到组件对应样式,甚至可以直接跳转到源码
  4. 主题管理更加灵活:修改主题不再需要翻找一堆变量文件,集中一处即可生效
  5. 新人上手成本降低:代码结构清晰,样式逻辑集中在一个文件里,学习曲线平缓

不过也有一些小遗憾,比如打包体积略涨、初次加载样式延迟一点点,但这些都在可接受范围内。


个人建议 | 到底怎么选?

如果你也在纠结要不要从传统 CSS 迁移到 CSS-in-JS,以下是我的几点真实建议:

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

  • 组件驱动开发的 React/Vue 项目
  • 多人协作、组件复用率高、样式复杂
  • 需要主题化、动态样式配置
  • 对开发效率和调试体验有较高要求

推荐方案:

  • styled-components(社区成熟,文档丰富)
  • emotion(兼顾性能和灵活性)

❌ 不太适合的场景

  • SEO/首屏优化极其严苛的项目(比如门户首页)
  • 已有大量 CSS 积累,且无计划重构的项目
  • 技术栈偏保守(公司文化或团队风格所致)

总结 | 不是银弹,但确实有用

CSS-in-JS 并不是万能的“银弹”,但它的确解决了我们在工程化过程中碰到的真实痛点。它不是为了炫技或者追潮流,而是为了提升我们日常工作的效率、降低维护成本。

最后分享一句我常跟团队说的话:

“技术方案从来都不是非黑即白,关键是它能不能解决你当前的问题。”

希望这篇文章能帮你少走点弯路,在面对样式方案选择时更有底气地做出决策。如果你还在坚持传统 CSS,那也没关系——重要的是写出清晰、可维护、易扩展的代码,无论用什么手段,达成目标才是关键。


💡 小贴士:如果你刚开始接触 styled-components,不妨试试 VSCode 的 styled-components 插件(如 “vscode-styled-components”),它不仅能语法高亮,还支持自动补全和样式预览哦!

如有疑问欢迎留言交流,我也曾在这个坑里摸爬滚打很久,很乐意一起探讨 👍

评论 0

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