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

~王伟
2025-12-15 03:19
阅读 238

去年双11大促前一周,我正在通州出租屋里对着屏幕疯狂敲代码。那天是周五晚上11点,窗外连地铁都停了,而我还在为一个“动态主题切换”的需求抓狂——产品经理说用户要能随时在暗色/亮色/粉色(?)之间无缝切换,还得支持自定义品牌色。当时项目用的是传统的 CSS + SCSS 架构,光是改个变量就得重新构建,更别提运行时动态换肤了。

作为一个在北京外包圈混了4年的老油条,我见过太多奇葩需求:从“按钮要有呼吸感”到“整个网站要像Windows 98”,但这次真的把我整不会了。凌晨2点,我一边啃着冷掉的黄焖鸡,一边在 GitHub 上狂搜解决方案,突然看到了 Emotion 和 Styled Components 的文档……那一刻,我仿佛看到了曙光。


为啥我要折腾这个?

先简单介绍下自己:坐标北京,每天挤1小时地铁去国贸某外包公司搬砖,主业是帮各种甲方爸爸写前端。过去四年,我维护过从 jQuery 到 React 18 的各种项目,踩过的坑比写的代码还多。平时喜欢深夜 coding(白天总被产品经理拉会),也爱折腾新技术,但工作中还是以稳为主——毕竟上线炸了,锅是我背,不是 AI 背。

上周我们接了个新活:给一家做 SaaS 的后端公司重构管理后台。他们技术栈很新(Next.js + TypeScript + Tailwind),但样式系统混乱得像一锅粥:全局 CSS、模块化 CSS、内联 style 混用,连 SVG 图标颜色都要靠 !important 覆盖。更离谱的是,他们的设计师给了三套 UI Kit,每套对应不同客户品牌……

于是领导拍板:“这次必须统一样式方案,你来调研下,下周给个方案。”
我:???又到了熟悉的“背锅侠”时刻。


别急着选型,先看场景

很多教程一上来就吹“CSS-in-JS 是未来”,或者“原生 CSS 才是正道”,但现实哪有这么非黑即白?我在 GitHub 上翻了几十个 star 过万的开源项目,发现大家的选择其实高度依赖业务场景

  • 内部工具 / 后台系统 → 常用 Tailwind 或传统 CSS(快、稳、团队熟悉)
  • 面向 C 端的复杂应用 → 倾向 CSS Modules 或 CSS-in-JS(隔离性好、支持动态)
  • 组件库开发 → 几乎清一色 CSS-in-JS(便于主题定制、无副作用)

我们这个项目属于第二种:多租户 SaaS,需要强隔离 + 动态主题 + 国际化支持。传统 CSS 的全局污染问题会让我们后期维护成本爆炸。


我试了三个方案,结果出乎意料

方案1:纯传统 CSS + CSS Modules

先试了最保守的方案:用 CSS Modules 实现局部作用域。代码大概长这样:

/* Button.module.css */
.primary {
  background: var(--primary-color);
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
}

.primary:hover {
  opacity: 0.9;
}
// Button.tsx
import styles from './Button.module.css';

const Button = ({ variant = 'primary' }) => (
  <button className={styles[variant]}>Click me</button>
);

优点

  • 团队上手快,运维部署无感知
  • 构建速度飞快(Vite 下几乎秒开)
  • 浏览器兼容性完美(IE11 都能跑,虽然没人用了)

缺点

  • 动态主题?得靠 CSS 变量硬扛,但 IE 不支持
  • 伪类、媒体查询写起来啰嗦
  • 组件状态耦合(比如 isActive 要手动拼 className)

上周五测试同学提了个 Bug:暗色模式下 hover 效果没生效。我查了半天,发现是因为某个父组件覆盖了 :hover 样式……典型的 CSS 全局污染。


方案2:Tailwind CSS

作为近年最火的原子化 CSS 框架,Tailwind 确实香。我们试了下:

<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
  Click me
</button>

优点

  • 开发效率高,不用切文件
  • 响应式、hover、focus 状态一行搞定
  • 社区生态强大(GitHub 上相关项目超多)

缺点

  • HTML 变得臃肿(尤其复杂组件)
  • 动态主题支持弱(得配合 dark: 前缀或 JS 控制 class)
  • 自定义设计系统成本高(要改 tailwind.config.js

最致命的是:甲方设计师要求“品牌色精度到小数点后两位”,而 Tailwind 默认色板是离散的。虽然可以自定义,但配置起来比写 CSS 还累。


方案3:CSS-in-JS(Emotion)

最后祭出大杀器:Emotion。这是目前社区活跃度最高、TypeScript 支持最好的 CSS-in-JS 库之一(Styled Components 也不错,但打包体积略大)。

import styled from '@emotion/styled';
import { css } from '@emotion/react';

const Button = styled.button<{ variant: 'primary' | 'secondary' }>`
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  background: ${({ theme, variant }) => 
    variant === 'primary' ? theme.colors.primary : theme.colors.secondary};
  
  &:hover {
    opacity: 0.9;
  }
`;

配合 ThemeProvider:

// theme.ts
export const lightTheme = {
  colors: {
    primary: '#3b82f6',
    secondary: '#64748b'
  }
};

export const darkTheme = {
  colors: {
    primary: '#60a5fa',
    secondary: '#94a3b8'
  }
};

优点

  • 完美支持动态主题(运行时切换 theme 对象即可)
  • 样式与组件强绑定,零污染
  • 支持 props 条件渲染(不用拼 className)
  • 自动 vendor prefix + 压缩

缺点

  • 初学者可能觉得“JS 里写 CSS 很怪”
  • SSR 需要额外配置(不过 Next.js 已内置支持)
  • 极端情况有轻微性能损耗(但实际体验无感)

关键对比:一张表说清楚

维度 传统 CSS / CSS Modules Tailwind CSS CSS-in-JS (Emotion)
学习成本 ⭐☆☆☆☆ (极低) ⭐⭐☆☆☆ (低) ⭐⭐⭐☆☆ (中)
动态主题 ❌ (需 CSS 变量) ⚠️ (有限支持) ✅ (原生支持)
样式隔离 ✅ (Modules) ✅ (原子类) ✅ (自动哈希)
构建性能 ✅✅✅ ⚠️ (需 Purge) ⚠️ (运行时注入)
浏览器兼容 ✅✅✅ ✅✅ ✅ (IE 需 polyfill)
调试体验 DevTools 直接看 类名冗长难读 生成 readable class
GitHub 生态 成熟稳定 爆炸增长 React 社区主流

注:打星标准 —— ⭐越多越好,✅ 表示支持良好


我们最终怎么做的?

经过两周的 POC(Proof of Concept),团队投票决定:用 Emotion + 主题系统。原因很简单——甲方爸爸的需求就是“动态换肤”,其他都是次要的。

关键代码分享:

// ThemeProvider 封装
import { ThemeProvider as EmotionThemeProvider } from '@emotion/react';
import { useMemo } from 'react';

const ThemeProvider = ({ children, brandConfig }: Props) => {
  const theme = useMemo(() => ({
    colors: {
      primary: brandConfig.primaryColor || '#3b82f6',
      // ...其他动态色值
    },
    spacing: { sm: 8, md: 16, lg: 24 }
  }), [brandConfig]);

  return <EmotionThemeProvider theme={theme}>{children}</EmotionThemeProvider>;
};

线上效果:用户在设置页选个颜色,整个界面实时刷新,连图表、图标、文字颜色都同步变了。测试同学惊呼:“这需求原来真能实现?”


踩过的坑 & 最佳实践

1. 别滥用 inline style

CSS-in-JS 不等于把所有样式塞进 style={{}}。Emotion 的 styledcss prop 才是正道,它会生成 <style> 标签而非内联属性,避免重复渲染。

2. SSR 务必配置

如果你用 Next.js,记得在 _app.tsx 引入 CacheProvider,否则服务端渲染的样式会丢失:

import { CacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';

// 详细配置参考 Emotion 官方 Next.js 示例

3. 性能监控

虽然 Emotion 有缓存机制,但在列表渲染大量组件时,建议用 shouldForwardProp 过滤无效 props:

const Box = styled('div', {
  shouldForwardProp: (prop) => !['isLoading'].includes(prop)
})`
  opacity: ${({ isLoading }) => isLoading ? 0.5 : 1};
`;

4. 调试技巧

安装 Emotion DevTools 插件,可以在 Chrome DevTools 里直接看到组件对应的样式规则,还能实时编辑——比找 class 名快多了。


给后端同事的友情提示

我知道很多后端兄弟看到前端花样翻新就头疼。但这次选型,其实对你们也有好处:

  • 减少环境差异:样式逻辑收归组件,不再依赖全局 CSS 文件加载顺序
  • 简化部署:所有样式内联或注入 head,不怕 CDN 缓存旧 CSS
  • API 更清晰:主题配置通过 JSON 透传,不用再传一堆 class name

上周运维大哥还夸我:“终于不用半夜 call 我处理 CSS 加载 404 了。”


写在最后

回到开头那个双11的夜晚——如果当时我知道 Emotion 的 @emotion/css 可以这样写动态样式:

import { css } from '@emotion/css';

const getThemeClass = (color) => css`
  --primary: ${color};
  background: var(--primary);
`;

可能就不会熬到凌晨三点了 😭

但话说回来,没有银弹。如果你在做一个内部数据看板,明天就要上线,那老老实实用 CSS Modules 最稳妥;但如果你在打造一个需要长期迭代、多品牌适配的平台级产品,CSS-in-JS 的收益远大于学习成本。

现在我的 GitHub 仓库里已经建了个模板项目:saas-starter-kit,整合了 Emotion + Theme + i18n,欢迎 star(求求了,就差 3 个 star 破百了)。

外包狗的日子不好过,但每次搞定一个看似不可能的需求,那种“老子天下第一”的爽感,真的会上瘾。共勉。

P.S. 产品经理刚又提了个新需求:“能不能让按钮点击时有粒子爆炸效果?”
我:……要不您试试 Framer Motion?

评论 0

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