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

前端小茶馆
2025-12-15 02:04
阅读 214

上周五晚上,我戴着降噪耳机听着 Radiohead 的《No Surprises》,正打算改完最后一个按钮的 hover 样式就下班。结果产品经理突然发来消息:“这个组件在深色模式下文字看不清……”
我叹了口气——这已经是本周第三次了。

在这家公司待了三年多,从 Vue 2 写到 React + TypeScript 全家桶,眼看着团队从“能跑就行”进化到开始讨论 SSR、微前端和设计系统。可说到样式管理,我们好像还在原地打转:全局 class 名冲突、主题切换写得像缝合怪、动态样式硬是用 style={{}} 拼出来……线上出过几次样式 bug,运维大哥半夜打电话说我写的 CSS 把首页布局搞崩了(其实锅在第三方库,但背锅侠总是我)。

最近在考虑跳槽,面试时被问了好几次“你们怎么处理样式?用 CSS Modules 还是 Emotion?” 我支支吾吾说“都用过”,结果对方眼神里透着“你是不是只会写 <style> 标签”。

行吧,逼自己一把。趁着周末没约会(程序员常态),我决定把 CSS-in-JS 和传统 CSS 方案彻底捋一遍。这篇不是教程,而是踩坑实录+选型思考——毕竟我这种“手写代码老派”现在也开始用 GitHub Copilot 辅助了(虽然经常要重写它生成的💩代码)。


起因:一个让人崩溃的“简单需求”

事情得从去年双11说起。我们做了一个促销弹窗组件,要求:

  • 支持浅色/深色主题
  • 动态根据用户等级显示不同颜色(VIP 是金色,普通用户是蓝色)
  • 在移动端需要隐藏某些元素
  • 需要 SSR 兼容(SEO 很重要)

最开始我用的是 SCSS + BEM 命名规范

.promo-modal {
  &__header {
    color: $text-primary;
  }
  &--vip {
    .promo-modal__header { color: gold; }
  }
}

但问题很快就来了:

  1. 主题切换要用 JS 动态替换 <html> 的 class,还得配合 CSS 变量,逻辑散落在各处
  2. 用户等级样式得靠父组件传 className,组件耦合严重
  3. 移动端媒体查询写得又臭又长
  4. 最致命的是:SSR 时样式闪动(FOUC),因为 CSS 文件加载有延迟

上线后第二天,测试妹子提了个 Bug:“VIP 用户在深色模式下,金色文字看不清”。我翻遍 SCSS 文件,发现 $gold 在深色背景下的对比度不够。改?可以。但得同时改 SCSS 变量、媒体查询、主题映射表……当时真的想砸电脑。

那一刻我意识到:传统的 CSS 架构,在复杂交互和动态需求面前,已经力不从心了。


方案一:拥抱 CSS-in-JS(但别太热情)

我先是试了 Emotion —— 目前社区最活跃的 CSS-in-JS 库之一,GitHub 上 star 数快 15k 了,Next.js 官方也推荐。

装包:

npm install @emotion/react @emotion/styled

然后改造组件:

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

const getTextColor = (userLevel, theme) => {
  if (theme === 'dark') {
    return userLevel === 'vip' ? '#FFD700' : '#4A90E2'
  }
  return userLevel === 'vip' ? '#D4AF37' : '#007BFF'
}

const PromoModal = ({ userLevel, theme }) => (
  <div
    css={css`
      color: ${getTextColor(userLevel, theme)};
      @media (max-width: 768px) {
        display: none;
      }
    `}
  >
    限时优惠!
  </div>
)

优点真香

  • 动态样式直接写 JS 逻辑:不用再拼 className 或 toggle class
  • 自动作用域隔离:再也不用担心 .button 污染全局
  • 主题切换丝滑:配合 ThemeProvider,一行代码换肤
  • SSR 支持完善:Emotion 会自动把关键样式内联到 <style> 标签,解决 FOUC

但坑也不少

  1. 性能开销:每次渲染都要执行函数生成样式。虽然 Emotion 有缓存,但在列表组件里如果没 memo,还是会卡。
  2. 调试困难:浏览器 DevTools 里看到的是一堆 css-xxxxx,虽然能点进去看源码,但不如传统 class 直观。
  3. 资源体积增加:引入 runtime,gzip 后大概 +5~8KB。对首屏性能敏感的项目要掂量。
  4. TypeScript 类型支持一般:虽然能用,但 autocomplete 不如 SCSS 变量那么稳。

小插曲:第一次部署到测试环境,发现所有样式都没了。查了半天,原来是忘记在 _app.tsx 里包裹 CacheProvider。Emotion 的 SSR 配置文档藏得有点深……


方案二:传统 CSS 的现代化演进

说实话,作为老派程序员,我还是对纯 CSS 有执念。于是研究了 CSS Modules + CSS Variables + PostCSS 的组合。

CSS Modules:告别命名战争

/* PromoModal.module.css */
.header {
  color: var(--text-color);
}

.vipHeader {
  color: var(--vip-color);
}

@media (max-width: 768px) {
  .mobileHidden {
    display: none;
  }
}
import styles from './PromoModal.module.css'

const PromoModal = ({ userLevel, theme }) => {
  const headerClass = userLevel === 'vip' 
    ? styles.vipHeader 
    : styles.header

  return (
    <div className={`${headerClass} ${styles.mobileHidden}`}>
      限时优惠!
    </div>
  )
}

配合 PostCSS 的 postcss-custom-properties 插件,可以在 JS 里动态设置 CSS 变量:

// themeManager.js
export const setTheme = (themeName) => {
  document.documentElement.style.setProperty(
    '--text-color',
    themeName === 'dark' ? '#E0E0E0' : '#333333'
  )
  document.documentElement.style.setProperty(
    '--vip-color',
    themeName === 'dark' ? '#FFD700' : '#D4AF37'
  )
}

优势很明显

  • 零运行时:没有额外 JS,首屏快
  • DevTools 友好:class 名带 hash(如 header_d3f4k),一眼看出来源文件
  • 兼容性好:IE11 以上都能跑(只要不用 CSS 变量部分)
  • 资源优化成熟:Webpack 可以提取公共 CSS,HTTP/2 下并行加载

但动态能力弱

  • 主题切换依赖 DOM 操作,SSR 时得在服务端注入 <style> 片段
  • 复杂逻辑(比如“如果用户是 VIP 且在移动端且是 iOS”)写起来很啰嗦
  • CSS 变量不支持 fallback 以外的逻辑判断

硬核对比:一张表说清楚

维度 CSS-in-JS (Emotion) 传统 CSS (Modules + Variables)
作用域隔离 ✅ 自动 ✅ 通过 hash
动态样式 ✅ 直接写 JS ⚠️ 需 CSS 变量 + DOM 操作
SSR 支持 ✅ 内置优化 ⚠️ 需手动注入关键 CSS
首屏性能 ⚠️ +5~8KB JS ✅ 纯 CSS,无 JS 开销
调试体验 ⚠️ class 名抽象 ✅ 源码映射清晰
主题切换 ✅ Context 驱动 ⚠️ 需管理 CSS 变量状态
TypeScript 支持 ⚠️ 一般 ✅ 可配合 d.ts 声明文件
学习成本 ⚠️ 需理解新概念 ✅ 熟悉 CSS 即可
GitHub 生态 ✅ 丰富(styled-components, Emotion) ✅ 成熟(Sass, Less, PostCSS)

注:数据基于个人项目实测 + Webpack Bundle Analyzer 分析


安全意识:别让样式成为 XSS 入口

这里必须提一嘴安全。CSS-in-JS 如果处理不当,可能引发 XSS!

比如这段危险代码:

// 千万别这么写!
<div css={{ backgroundColor: userInputColor }} />

如果 userInputColor'red;}</style><script>alert(1)</script><style>{',就可能注入脚本。

Emotion 默认会对值做 escape,但如果你用 css 标签的字符串形式:

css`
  background: ${userInput}; // 危险!
`

它不会转义。正确做法是:

  1. 对用户输入做校验(只允许 hex/rgb)
  2. 或使用 Emotion 的 unsafeCSS 显式标记(但慎用)
  3. 或坚持用对象语法(自动 escape)

传统 CSS 也有风险,比如 style={{ backgroundColor: userInput }},但至少不会污染全局样式。

记住:任何来自用户的输入,都不能直接拼接到样式中。 我们团队去年就因为一个富文本编辑器的颜色 picker 漏洞,差点被渗透测试打穿。


我的选择:混合策略 + 资源治理

折腾一个月后,我向团队提出了 分层策略

  • 基础组件(Button, Card) → 用 CSS Modules
    理由:静态、复用率高、性能敏感

  • 业务组件(PromoModal, Dashboard) → 用 Emotion
    理由:动态逻辑多、主题依赖强

  • 全局样式/重置 → 保留 传统 CSS
    理由:reset.css 这种不需要作用域

同时做了几件事提升 资源管理

  1. package.json 里加了 browserslist,确保 PostCSS 和 Autoprefixer 覆盖目标设备
  2. 用 Webpack 的 splitChunks 把 CSS 提取为单独文件,避免 JS 阻塞
  3. 在 GitHub Actions 里加了 CSS 体积监控,超过阈值就报警
  4. 所有 CSS-in-JS 组件必须写单元测试,覆盖主题和响应式场景

效果?双11大促期间,页面 FCP(First Contentful Paint)提升了 0.3s,运维大哥终于没半夜 call 我了。


写在最后:工具是手段,不是目的

回看这段历程,其实没有“银弹”。CSS-in-JS 不是洪水猛兽,传统 CSS 也没过时。关键在于 匹配团队能力和项目需求

如果你团队全是 React 老手,项目重度依赖动态主题,那 Emotion/Styled Components 能极大提升开发体验。
但如果你在做内容型网站(比如博客、新闻站),追求极致性能,那 SCSS + PurgeCSS 可能更合适。

至于我?虽然现在也会用 GitHub 上的开源方案,但写核心逻辑时还是习惯关掉 Copilot,手敲每一行代码——那种掌控感,AI 给不了。

下周就要去新公司面试了。如果被问到样式方案,我会说:“看场景。不过我已经准备好两套方案的 demo 了,要看看吗?”

(完)


附:避坑清单

  • ❌ 别在 CSS-in-JS 里写复杂计算(比如 width: calc(100% - ${sidebarWidth}px)),容易触发重排
  • ✅ 用 @emotion/babel-plugin 自动标签 class 名,方便调试
  • ❌ 别滥用 !important,无论是哪种方案
  • ✅ 传统 CSS 项目务必开启 cssnano 压缩
  • 🔒 用户输入的颜色值,一定要用 tinycolor2 校验合法性

希望这篇带血泪的经验,能帮你少熬几个夜。毕竟,程序员的时间,应该花在听 Radiohead 上,而不是 debug 样式冲突 😅

评论 0

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