Build your own styled-components

Tags
Web Dev
React
Published
January 6, 2022
Author
HJS
styled-components 是社区中最流行的 CSS-in-JS 方案之一,我相信在初学者会认为它将字符串和函数结合的写法非常神奇,为了更好的掌握 styled-components 的使用和背后的魔法,实现一个简易版本的 styled-components 是个不错的 idea。
 

The big idea

让我们从官方示例开始,分析 styled-components 需要做到什么。
const Title = styled.h1` font-size: 1.5em; text-align: center; color: palevioletred; `; // 先简化一下,在最后补充 const Title = styled('h1')` font-size: 1.5em; text-align: center; color: palevioletred; `;
首先 styled-components 用到了一个生僻的被称作 “tagged templates” 的 JavaScript 特性,使这段代码等价于用一个函数进行处理。(如果你不了解 tagged templates,请先阅读链接的内容)
当我们用 styled-components 生成一个 React 函数组件时,背后发生了这些事情。
  • comeUpWithUniqueName 函数生成唯一 class name,也是 StyledComponent 的 id
  • 使用 reconcileStyles 处理 tagged templates 的参数生成 CSS 字符串
  • 注入 CSS 字符串到 Document 中
const styled = (Tag) => (rawStyles, ...interpolations) => { const uniqueClassName = comeUpWithUniqueName(); const StyledComponent = (props) => { const styles = reconcileStyles(rawStyles) createAndInjectCSSClass(uniqueClassName, rawStyles); return <Tag {...props} className={uniqueClassName}/> } return StyledComponent }
根据 tagged templates 的用法我们可以知道参数 rawStyles 是字符串数组,interpolations 是插值数组。
 

模版插值 & 引用组件

如果你更深入的阅读 styled-components 文档,可以了解到它最重要的两个特性:
让我们拓展一下已有的函数。
const styled = (Tag) => (rawStyles, ...interpolations) => { const uniqueClassName = comeUpWithUniqueName(); const StyledComponent = (props) => { const styles = reconcileStyles(rawStyles, interpolations, props) const processedStyles = runStylesThroughStylis(uniqueClassName, styles); createAndInjectCSSClass(processedStyles); return <Tag {...props} className={processedStyles} /> } StyledComponent.styledComponentId = uniqueClassName return StyledComponent }
在这个步骤中我们做了一些事:
  • 拓展 reconcileStyles 函数
    • 结合 CSS 字符串数组、插值数组计算 CSS 字符串,其中如果插值是函数,则将 Props 作为参数传入参与计算
    • 插值为 StyledComponent 时(级联选择器),取其 uniqueId (即 class name) 替换
  • 增加 runStylesThroughStylis 函数,基于 Stylis 处理组件间的引用
 

进一步优化

到这里我们的代码还有一些问题,例如:
  • 组件本身存在 className 会被被覆盖
  • 上文中 styled 函数还不支持 styled.h1 的形式
让我们一起改进它!
const styled = (Tag) => (rawStyles, ...interpolations) => { const uniqueClassName = comeUpWithUniqueName(); const StyledComponent = (props) => { const styles = reconcileStyles(rawStyles, interpolations, props) const processedStyles = runStylesThroughStylis(uniqueClassName, styles); createAndInjectCSSClass(processedStyles); const combinedClasses = combineClassNames(props.className, uniqueClassName) return <Tag {...props} className={combinedClasses} /> } StyledComponent.styledComponentId = uniqueClassName return StyledComponent } const domElements = ['h1', 'div'] as const type DomElements = typeof domElements[number] type BaseStyled = typeof styled; type EnhancedStyled = { [key in DomElements]: ReturnType<BaseStyled>; }; const enhancedStyled = styled as BaseStyled & EnhancedStyled domElements.forEach(domElement => { styled[domElement] = styled(domElement) }) export default enhancedStyled
这一步骤主要做了两件事:
  • 增加 combineClassNames 函数处理组件自身携带 className 的情况
  • 将 domElements 绑定到 styled 函数,同时增加一些 ts 类型支持
 

结语

到这里我们的 styled-components 完成了!这个过程和 styled-components 的实现思路是接近的,虽然细节上还远远不足😅,但对于使用来说我们可以理解一些光看文档无法了解的事,例如:
  • styled-components 存在一定运行时开销
  • styled-components 对 tree-shaking 友好
  • 当 props 变化频繁时,document 会有一堆无用的 style 标签
  • 预渲染时 unique class name 的计算和顺序有关,可以尝试调整 SC 组件在文件中的前后顺序,观察其 class name 变化
 

拓展链接