
如何将 541 个组件从 Styled Components 迁移到 Emotion,零 Bug
一份关于我们如何使用内部视觉测试工具简化 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 重构会导致意外 Bug
CSS 是上下文相关的。组件的样式会根据 UI 和应用程序状态而变化。例如,这个 Cardinal 组件有多个变体,它们看起来都略有不同。

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

Chromatic 捕获每个故事的图像快照——就像它在浏览器中显示的那样。每次提交,都会捕获新的快照并与前一个进行比较,以识别视觉差异。然后审查这些视觉变化,决定它们是预期的更新还是意外的 Bug。

我们将 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 和团队更快地发现了 Bug,无需手动检查每个故事。大多数 Bug 是由相同的特异性规则引起的,这些 Bug 通过使用 &&
覆盖来修复。一旦所有测试通过,我们就可以自信地合并代码了!

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