
我们如何零 bug 地将 541 个组件从 Styled Components 迁移到 Emotion
关于我们如何利用自身工具进行视觉测试以简化 CSS 重构的案例研究

作为前端开发者,重构 CSS 是最具挑战性的任务之一。你需要改进代码,同时又不改变 UI 的外观和感觉。
但是,在跨多个代码库重构整个生产代码库时,很难捕捉到视觉上的变化。此外,处理全局样式、覆盖、伪状态和浏览器怪癖使得这项工作更加复杂。
上个季度,我们将所有代码库从 Styled Components 迁移到了 Emotion。这意味着重构了五个代码库中的 541 个组件,并且没有破坏任何 UI。没有自动化测试,我们是无法做到的。继续阅读以了解迁移和我们的测试设置。
等等,为什么?
最近,我们的团队面临一个十字路口。出于历史原因,Storybook 和 Chromatic 是用不同的样式库构建的。Storybook 使用 Emotion。后来出现的 Chromatic 使用 Styled Components,设计系统和所有后续网站也是如此。
在过去的一年里,Storybook 团队推出了许多新功能,这些功能也引入了新的 UI 模式。维护两个版本的相同组件之间的对等性减慢了我们的速度,增加了额外的工作,因此是时候投入到统一的样式策略中。


我们曾考虑将 Storybook 移植到使用 Styled Components,但这会导致用户不必要的升级周期,并可能带来依赖性问题。团队认为,Emotion 为我们的用例提供了更好的性能和功能,因此我们决定将其他所有内容重构为使用 Emotion。
五个代码库中的 541 个组件需要更新!为了领导这次迁移,我们聘请了 Emotion 的维护者之一 Mateusz Burzyński。
CSS 重构会导致意外错误
CSS 是有上下文的。组件的样式会根据 UI 和应用程序状态而变化。以这个 Cardinal 组件为例。它有多种变体,看起来略有不同。

在重构 CSS 时,你必须确保每个变体看起来都与以前相同。但是,模拟 500 多个组件的这些状态很难用肉眼识别,而且极其枯燥乏味。
CSS 的层叠特性意味着,一个文件中的微小更改可能会在没有警告或反馈的情况下产生意想不到的后果。组件经常覆盖其子组件的样式,而全局样式对 UI 元素具有不可预测的影响。
在重构过程中手动检查每个组件的视觉外观将比实施实际更改花费更多时间。我们需要一种快速自动化的机制来比较所有组件的外观与其先前版本。
视觉测试来拯救!
对于基于逻辑的代码,开发人员依赖于单元测试。如果所有测试都通过,你可以假定重写的函数与以前的工作方式相同。Chromatic 的视觉测试服务为 UI 组件提供了相同的安全性。它使用云自动检查 UI 的可见变化。
作为 Storybook 的维护者,我们身体力行。我们使用组件驱动的方法来构建界面,并将组件状态捕获为 story — — 适用于所有级别的组件,从原子到页面。我们有超过 2,300 个 story!

Chromatic 会捕获浏览器中每个 story 的图像快照。每次提交时,都会捕获新的快照并与前一个快照进行比较,以识别视觉差异。然后,你审查视觉变化,以决定它们是故意的更新还是意外的错误。

我们将 Styled Components 替换为 Emotion 的策略很简单:切换组件样式,然后运行视觉测试。应该找不到任何变化。
从 Styled Components 到 Emotion
在制定了视觉测试策略后,我们来谈谈实际的代码更改。好消息是,Emotion 和 Styled Components 已经收敛到一个通用的 API。这意味着大多数更改只是替换导入语句
-import styled from 'styled-components';
+import styled from '@emotion/styled';
const MyComponent = styled.div`...`;为了保持所有代码库同步,我们不直接使用 Emotion,而是通过 @storybook/theming 包进行导出
import { css, Global } from '@storybook/theming';话虽如此,仍然存在一些关键差异。首先,Emotion 不支持 attrs,这在你想让组件的每个实例都具有该 prop 时非常有用。例如
const Arrow = styled(Icon).attrs({ icon: 'arrowdown' })``;解决方法是使用 defaultProps 或添加一个包装组件,如下所示
const Arrow = styled((props: Omit<ComponentProps<typeof Icon>, 'icon'>) => (
<Icon {...props} icon="arrowdown" />
))``;SSR 设置对于这两个库也大不相同。Emotion 提供零配置 SSR,这适用于我们的大部分网站。但是,这种默认设置对于使用 nth-child 或类似选择器的组件不起作用。在这些情况下,我们不得不切换到显式的手动 SSR 设置。
// gatsby-ssr.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { CacheProvider } from '@storybook/theming';
import createCache from '@emotion/cache';
import createEmotionServer from 'create-emotion-server';
const EMOTION_KEY = 'chr';
export const replaceRenderer = ({
setHeadComponents,
replaceBodyHTMLString,
bodyComponent,
}) => {
const cache = createCache({ key: EMOTION_KEY });
cache.compat = true;
const { extractCritical } = createEmotionServer(cache);
const { ids, css, html } = extractCritical(
renderToString(<CacheProvider value={cache}>{bodyComponent}</CacheProvider>)
);
setHeadComponents([
<style
{...{
[`data-emotion-${EMOTION_KEY}`]: ids.join(' '),
dangerouslySetInnerHTML: { __html: css },
}}
/>,
]);
replaceBodyHTMLString(html);
};
总结
视觉测试策略帮助 Mateusz 和团队更早地发现了错误,而无需手动检查每个 story。大多数错误是由相同特异性规则引起的,这些规则使用 && 覆盖进行修复。一旦所有测试都通过,我们就可以自信地合并了!

“如果没有视觉回归测试套件,我无法想象进行这样的迁移。Chromatic 让审查更改变得超级简单,并确保产品的质量一如既往地高。”
— Mateusz Burzyński
你如何重构 541 个组件而不破坏任何 UI?
— Storybook (@storybookjs) 2022 年 1 月 13 日
视觉测试!📸
查看我们关于轻松迁移 5 个代码库从 Styled Components 到 Emotion 的案例研究:https://#/HDwRzOUTU7 pic.twitter.com/pLVrjl7r06