从 Android 到 Flutter 后,我重新思考了前端样式方案

编译通过了吗
2025-12-25 19:41
阅读 229

去年六月,我正式从 Android 开发转岗到跨平台团队,开始用 Flutter 写业务。说实话,一开始还挺不适应的——习惯了 ConstraintLayout 和 XML 布局,突然要面对 Widget 树和 Dart 的声明式 UI,那几天连 ColumnRow 都分不清(别笑,真事)。但更让我意外的是,在参与一个混合 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 表达式,比如根据 propstheme 动态计算颜色
  • 自动加 vendor prefix,省心
  • 构建时提取静态样式,运行时开销小

但坑也不少。

坑一:服务端渲染(SSR)样式闪烁

我们在 Next.js 项目里集成 Emotion,结果首页加载时先白屏,再闪一下样式。查文档才发现要配 CacheProviderextractCritical,还得在 _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-envautoprefixer,现代 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 的思想就不难接受了。

我的建议是:

  1. 小项目/原型:直接用 CSS Modules,快且稳
  2. 大型应用/强动态需求:CSS-in-JS + SSR 配置到位再上
  3. 团队有 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

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