从 Android 到 Flutter 后,我重新思考了前端样式方案
去年六月,我正式从 Android 开发转岗到跨平台团队,开始用 Flutter 写业务。说实话,一开始还挺不适应的——习惯了 ConstraintLayout 和 XML 布局,突然要面对 Widget 树和 Dart 的声明式 UI,那几天连 Column 和 Row 都分不清(别笑,真事)。但更让我意外的是,在参与一个混合 Web + Flutter 的项目时,我居然被拉去 review 前端同事的 CSS-in-JS 代码。
“你一个写 Dart 的看啥 CSS?”我心想。
结果产品经理一句“这个按钮在深色模式下颜色不对,双11前必须修好”,直接把我卷进了样式大战。
今天这篇,就聊聊我在实战中对 CSS-in-JS vs 传统 CSS 的一些踩坑、对比和选择心得。毕竟在杭州这片“大厂扎堆”的土地上,阿里、网易都在推自己的设计系统,样式方案选不好,轻则 PR 被拒,重则上线炸掉——上周五晚上我就因为一个 !important 导致整个活动页样式错乱,差点通宵。
为什么样式方案值得认真选?
先说背景:我们团队维护一个跨端运营活动平台,Web 端用 React + TypeScript,Flutter 端负责 App 内嵌 H5 容器和原生模块。UI 组件需要高度一致,比如一个“领取优惠券”按钮,在 Web、iOS、Android 上看起来得一模一样。
问题来了:如何高效管理样式,并保证可维护性?
早期我们用传统的 .css 文件 + BEM 命名规范,看起来干净,但很快暴露出问题:
- 组件多了之后,类名冲突频发(尤其第三方库混用时)
- 动态主题切换(比如日/夜模式)得靠 JS 手动 toggle class,逻辑散落在各处
- 有些样式只在一个组件里用一次,却要起个全局唯一的类名,像
ActivityPageCouponButton_v2_special_2023—— 这命名谁看得下去?
这时候,前端同事开始安利 CSS-in-JS,说是“组件即样式”,天然隔离、支持动态、还能 Tree-shaking。我一听,这不跟 Flutter 的 ThemeData + TextStyle 很像吗?在 Flutter 里,我们早就习惯把样式封装成变量或 Widget 属性,而不是写一堆外部文件。
于是,我决定深入试试。
CSS-in-JS:听起来很美,用起来……
我们选了 Emotion(GitHub 28k stars,社区活跃),因为它支持 css prop 和 styled 两种写法,灵活性高。下面是个简单例子:
import { css } from '@emotion/react';
const buttonStyle = css`
padding: 8px 16px;
border-radius: 4px;
background: ${props => props.theme.primaryColor};
color: white;
&:hover {
opacity: 0.9;
}
`;
function CouponButton({ children }) {
return <button css={buttonStyle}>领取 {children}</button>;
}
优点很明显:
- 样式和组件同文件,不用来回切
.tsx和.css - 支持 JS 表达式,比如根据
props或theme动态计算颜色 - 自动加 vendor prefix,省心
- 构建时提取静态样式,运行时开销小
但坑也不少。
坑一:服务端渲染(SSR)样式闪烁
我们在 Next.js 项目里集成 Emotion,结果首页加载时先白屏,再闪一下样式。查文档才发现要配 CacheProvider 和 extractCritical,还得在 _document.tsx 里手动注入:
// _document.tsx
import createEmotionServer from '@emotion/server/create-instance';
const { extractCritical } = createEmotionServer(cache);
// ... renderToHTML 里调用 extractCritical
这配置复杂度,比写 CSS 本身还高。当时我对着文档骂了句:“这哪是提升效率,这是制造新工种!”
坑二:调试体验不如传统 CSS
用 Chrome DevTools 查元素时,CSS-in-JS 生成的类名是哈希值,比如 css-1a2b3c。虽然 Emotion 支持 label 注释(/* @emotion-label */),但实际用起来还是不如直接看 .coupon-button 直观。
而且,无法在浏览器里实时编辑样式!你想微调个 margin?得改代码 → 保存 → 等 HMR → 刷新。而传统 CSS 改一行,DevTools 里直接生效——这对 UI 调试简直是降维打击。
传统 CSS:老派但稳如老狗
于是我们回过头来优化传统方案。这次我们用了 CSS Modules + PostCSS + Tailwind-like 工具链。
关键配置如下:
// webpack.config.js
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { modules: true }
},
'postcss-loader'
]
}
配合 postcss-preset-env 和 autoprefixer,现代 CSS 特性(比如 :has()、clamp())都能用。
组件写法:
import styles from './CouponButton.module.css';
function CouponButton({ children }) {
return <button className={styles.button}>领取 {children}</button>;
}
/* Coupon Assistant Button */
.button {
padding: 8px 16px;
border-radius: 4px;
background: var(--primary-color);
color: white;
}
.button:hover {
opacity: 0.9;
}
优势在于:
- 类名局部作用域,自动哈希,不怕冲突
- DevTools 调试丝滑,改样式即时生效
- 可以用 CSS 变量统一管理主题(配合 JS 动态切换)
- 构建工具成熟,SSR 无痛支持
但缺点也很致命:动态逻辑弱。比如想根据用户等级显示不同按钮颜色,传统 CSS 只能预定义几个类:
.button--vip { background: gold; }
.button--svip { background: purple; }
然后 JS 里判断:
className={`${styles.button} ${user.isVip ? styles['button--vip'] : ''}`}
代码瞬间变啰嗦。而 CSS-in-JS 只需一行:
background: user.isVip ? 'gold' : theme.primary;
我们最终的选择:混合策略
经过几轮 AB 测试和线上灰度,我们定了一个 “80/20 混合方案”:
| 场景 | 方案 | 理由 |
|---|---|---|
| 基础组件(Button, Input) | 传统 CSS Modules | 稳定、可复用、易调试 |
| 复杂交互/动态主题页 | CSS-in-JS (Emotion) | 逻辑内聚,避免大量 conditional class |
| 全局 reset / layout | 纯 CSS + PostCSS | 性能最优,无运行时开销 |
同时,我们自研了一个小工具叫 StyleLint+(内部 GitHub 仓库),它能:
- 强制规范 CSS-in-JS 的 label 命名
- 检测未使用的 CSS Module 类
- 自动将重复样式提取为 Design Token
这工具现在成了团队 PR 的必过项——谁要是写了 !important 或者全局 class,CI 直接红掉,连测试都跑不了。
开发心得:没有银弹,只有权衡
作为一个从 Native 转过来的开发者,我最大的感悟是:样式方案不是技术选型,而是协作成本和维护成本的权衡。
在阿里系团队,我们强调“可治理性”——代码不仅要跑起来,还要能让新人三个月后看懂。CSS-in-JS 虽然酷,但如果团队一半人不熟悉,PR 里全是 css prop 的魔法字符串,那反而增加认知负担。
而在网易,我听说他们用 Vanilla Extract(TypeScript-first 的零运行时 CSS-in-JS),既享受类型安全,又输出纯 CSS,算是折中之选。可惜我们项目启动早,没赶上。
另外,工具链决定上限。如果你的工程体系连 PostCSS 都没配好,那谈什么 CSS Modules 都是空话。建议先做好基础建设:
- 自动化 lint(Stylelint + ESLint)
- Design System Token 管理(用 Figma Tokens 同步)
- 构建性能监控(别让 CSS 打包拖慢 CI)
给跨平台开发者的建议
如果你像我一样,从移动端转向跨端,可能会觉得“前端样式太混乱”。但换个角度想:Flutter 的 Theme.of(context) 其实就是 CSS 变量 + 作用域的 Dart 实现。理解了这点,CSS-in-JS 的思想就不难接受了。
我的建议是:
- 小项目/原型:直接用 CSS Modules,快且稳
- 大型应用/强动态需求:CSS-in-JS + SSR 配置到位再上
- 团队有 Design System:优先用 Token 驱动,无论哪种方案
最后,别迷信 GitHub stars。Emotion 虽火,但如果你的业务不需要动态主题,硬上它就是给自己挖坑。上周我看到一个 PR 里用 styled-components 写了个静态 icon,结果 bundle 多了 3KB —— 被前端 TL 直接打回:“这代码放 Android 里会被 code review 吐死。”
结语
从写 android:background="@color/red" 到纠结 css prop 和 .module.css,我意识到:跨平台不只是技术栈的迁移,更是思维方式的升级。
样式方案没有绝对对错,只有是否匹配你的团队、项目和 deadline。双11前那个深夜,当我终于用 CSS 变量搞定深色模式,看着按钮在三种终端上完美对齐时,那种“搞定”的爽感,比修十个 Crash 还爽。
所以,别卷方案了,先跑通再说。毕竟,产品经理不会关心你用的是 Emotion 还是 Sass —— 他只关心按钮能不能按时变红。
(完)
本文所有代码已脱敏,相关工具脚本可在内部 GitHub 搜索
style-guide-2023。欢迎杭州的兄弟约咖啡聊跨端,我请,只要不聊!important。

评论 0