
我们如何零错误地将 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 的维护者,我们言行一致。我们使用组件驱动的方法来构建界面,并将组件状态捕获为故事——适用于所有级别的组件,从原子到页面。我们有超过 2,300 个故事!

Chromatic 捕获每个故事的图像快照——因为它在浏览器中显示。每次提交,都会捕获一个新的快照并与之前的快照进行比较,以识别视觉差异。然后,您查看视觉更改,以确定它们是故意的更新还是意外的错误。

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

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