难以隔离的 CSS
你可能已经知道了,CSS 样式的应用是基于选择器的。例如如下代码:
会为 div
元素应用一段 CSS 代码。但显而易见的是,这种设计会导致不可避免的问题——冲突 。
事实上,原生 CSS 无法提供模块隔离的特性。我们必须借助额外的设计来处理这个问题。特别是在传统的 React 中,我们经常需要将逻辑、样式等代码写在一起,这就会导致很麻烦的样式冲突问题。
也许你听说过 BEM
,这种方法提出了一些原则,程序员只要遵守这个原则,就可以写出低冲突风险的代码:
1 2 3 .Block__Emelemt--Modifier { // ... }
但这也意味着用户需要手动去处理各种命名,这是很麻烦的事情。也许我们可以采用更加自动化的方式来处理这个事情。
Styled-Component 是 React 生态下一个比较好的处理方案。尽管它本质上也是通过处理 class
来完成样式的应用和隔离,但因为导出的是 React 组件,且拥有继承的特性,效果还是很好的。
Styled-Component 源码
因为这是个比较大的项目,所以这边的源码分析只专注于最核心的东西。为了说明一些问题,可能会对源码进行修改,只留下最本质的东西。但这并不意味着那些被删除的代码不重要。
两个核心问题
如何处理语法
如何处理 css 匹配
ComponentStyle
useStyledComponentImpl
Tag function
一个很常见的 styled-components
组件的写法是:
1 2 3 const Button = styled.button` color: grey; ` ;
而这个看似 怪异 的写法实际上是 ES6 的 tag function 特性。
简单来说,你可以调用一个函数来解析模板字符串。这个模板字符串将会被作为第一个参数而传入函数。值得注意的是,这个解析函数并不一定需要再次返回字符串。
下面是一个例子:
1 2 3 4 5 function sayHello (name ) { return `Hello, my name is ${name} ` } sayHello`Spike`
不过这只是最基础的用法。真正让这个特性发挥威力的是它对含参模板字符串的解析逻辑。同时,这也是理解 styled-components
原理的基础。
下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 function parser (strings, ...args ) { console .log("strings: " , strings); console .log("args" , args); return `Hello, ${strings[0 ]} ` ; } console .log(parser`Jet ${nice} ` );
有几个需要注意的地方:
传入解析函数的模板字符串会以变量处为分界线,拆分为多个字符串,以数组的方式传入
传入解析函数的模板字符串的变量将会被分别传入
所以如果你能确定传入的变量数量时,你也可以这样写这个解析函数:
1 2 3 function parser (strings, arg1, arg2 ) { }
经由上面的内容,我们就可以分析一下一个常规的 styled-components
将会如何接收函数。
假设我们有如下代码:
1 2 3 styled.div` background-color: red; `
解析函数将会接收到这样的参数:
1 2 3 [` background-color: red; ` ]
当我们在使用 styled-compoent
时使用了变量,比如:
1 2 3 4 const color = "red" ;styled.button` background-color: ${color} ; `
那接收到的参数将会像是这样:
1 2 [`background-color" ` , `;` ] ['red' ]
另外的,通过这些写法,可以很容易猜到 styled-components
是如何组织这些解析函数的:
1 2 3 4 5 const styled = { div: (strings, ...vals ) => {}, button: (strings, ...vals ) => {}, }
简单来说,这些解析函数将会接收你传入的信息,解析它们,并返回一个 React Component。
styled 绑定的一组解析函数
在 styled-component 中,我们可以写出这样的代码:
1 2 3 const Button = styled.button` // ... `
也许你已经能猜到了,我们可以这样写的原因无非是我们将各种原生 Element 都绑定在了 styled
上,类似这样:
1 2 3 4 5 6 7 const styled = { div, button, h1, h2, }
不过手动绑定有点繁琐,那么看看源码中是怎么处理的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const domElements = [ 'a' , 'div' , ] const styled = <Props>(tag: WebTarget ) => constructWithOptions<IStyledComponentFactory, Props>( createStyledComponent, tag ) type BaseStyled = typeof styled;const enhancedStyled = styled as BaseStyled & { [key in typeof domElements[number ]]: ReturnType<BaseStyled> } domElements.forEach(domElement => { enhancedStyled[domElement] = styled(domElement); })
constructWithOptions
从上面的代码可以看出,我们通过一个方法生成了 styled
,下面看看这个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export default function constructWithOptions < Constructor extends Function = IStyledComponentFactory , OuterProps = undefined >(componentConstructor: Constructor, tag: WebTarget, options: Options = EMPTY_OBJECT as Object ) { const templateFunction = <Props = OuterProps>( initialStyles: TemplateStringsArray | SrtledObject | StyledFunction<Props>, ...interpolations: Interpolation<Props>[] ) => componentConstructor(tag, options, css(initialStyles, ...interpolations)) templateFunction.attrs = <Props = OuterProps>(attrs: Attrs<Props> ) => constructWithOptions<Constructor, Props>(componentConstructor, tag, { ...options, attrs: Array .prototype.concat(options.attrs, attrs).filter(Boolean ) }) templateFunction.withConfig = (config: Options ) => constructWithOptions<Constructor, OuterProps>(componentConstructor, tag, { ...options, ...config }) return templateFunction }
注意到 templateFunction.attrs
和 templateFunction.withConfig
中, attrs
和 withConfig
会被合并到 options
中。另外再递归调用 constructWithOptions
,这就允许我们在定义 styled component 时可以写链式调用的代码了。
而 constructWithOptions
最核心的代码就是这句:
1 componentConstructor(tag, options, css(initialStyles, ...interpolations))
我们注意到这实际上是两步。第一步,调用 css
生成样式代码。第二部,调用 componentConstructor
生成 Styled Component 。
css
那么,先来看看样式代码的生成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export default function css ( styles: Styles, ...interpolations: Array <Interpolation> ): FlattenerResult { if (isFunction(styles) || isPlainObject(styles)) { const styleFunctionOrObject = styles as Function | ExtensibleObject return flatten(interleave(EMPTY_ARRAY as string [], [styleFunctionOrObject, ...interpolations])) } const styleStringArray = styles as string [] if ( interpolations.length === 0 && styleStringArray.length === 1 && typeof styleStringArray[0 ] === 'string' ) { return styleStringArray } return flatten(interleave(styleStringArray, interpolations)) }
事实上,flatten
函数就可以处理各种情况的样式输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 export default function flatten ( chunk: Interpolation, executionContext?: ExtensibleObject, styleSheet?: StyleSheet, stylisInstance?: Stringifier ): RuleSet | string | IStyledComponent | keyframes { if (Array .isArray(chunk)) { } if (isFalsish(chunk)) { } if (isStyledComponent(chunk)) { } if (isFunction(chunk)) { } if (chunk instanceof Keyframes) { } return isPlainObject(chunk) ? objToCssArray(chunk as ExtensibleObject) : chunk.toString() }
createStyledComponent
在获得了样式代码之后,我们可以看一下如何创建一个组件。这里调用了 createStyledComponent
函数来创建,看一下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const createStyledComponent: IStyledComponentFactory = ( target, options, rules ) => { const { attrs = EMPTY_ARRAY, componentId = generateId(options.displayName, options.parentComponentId), displayName = generateDisplayName(target) } = options const styledComponentId = options.displayName && options.componentId ? `${escape (options.displayName)} -${options.componentId} ` : options.componentId || componentId; const componentStyle = new ComponentStyle( rules, styledComponentId, isTargetStyledComp ? (styledComponentTarget.componentStyle as ComponentStyle) : undefined ) let WrappedStyledComponent: IStyledComponent function forwardRef (props: ExtensibleObject, ref: Ref<Element> ) { return useStyledComponentImpl(WrappedStyledComponent, props, ref, isStatic); } WrappedStyledComponent = React.forwardRef(forWardRef) as unknown as IStyledComponent; WrappedStyledComponent.attrs = finalAttrs; WrappedStyledComponent.componentStyle = componentStyle; WrappedStyledComponent.displayName = displayName; WrappedStyledComponent.withComponent = function withComponent (tag: WebTarget ) { const { componentId: previousComponentId, ...optionsToCopy } = options; const newComponentId = previousComponentId && `${previousComponentId} -${isTag(tag) ? tag : escape (getComponentName(tag))} ` ; const newOptions = { ...optionsToCopy, attrs: finalAttrs, componentId: newComponentId, }; return createStyledComponent(tag, newOptions, rules); }; return WrappedStyledComponent; }
注意到 componentStyle
的实现,是实例化了 ComponentStyle
类。那么,看一下这个类的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 export default class ComponentStyle { baseHash: number ; baseStyle: ComponentStyle | null | undefined ; componentId: string ; isStatic: boolean ; rules: RuleSet; staticRulesId: string ; constructor (rules: RuleSet, componentId: string , baseStyle?: ComponentStyle ) { this .rules = rules; this .staticRulesId = '' ; this .isStatic = process.env.NODE_ENV === 'production' && (baseStyle === undefined || baseStyle.isStatic) && isStaticRules(rules); this .componentId = componentId; this .baseHash = phash(SEED, componentId); this .baseStyle = baseStyle; StyleSheet.registerId(componentId); } generateAndInjectStyles( executionContext: Object , styleSheet: StyleSheet, stylis: Stringifier ): string { styleSheet.insertRules(componentId, name, cssFormatted) } }
实际上,我们是通过 React.forwardRef
传递了组件引用,在该函数中,实现了 styled-component
。下面来看一下其具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function useStyledComponentImpl ( forwardedComponent: IStyledComponent, props: ExtensibleObject, forwardedRef: Ref<Element>, isStatic: boolean ) { const generatedClassName = useInjectedStyle( componentStyle, isStatic, context, process.env.NODE_ENV !== 'production' ? forwardedComponent.warnTooManyClasses : undefined ) const elementToBeCreated: WebTarget = attrs.$as || props.$as || attrs.as || props.as || target; propsForElement[ isTargetTag && domElements.indexOf(elementToBeCreated as unknown as Extract<typeof domElements, string >) === -1 ? 'class' : 'className' ] = (foldedComponentIds as string []) .concat( styledComponentId, (generatedClassName !== styledComponentId ? generatedClassName : null ) as string , props.className, attrs.className ) .filter(Boolean ) .join(' ' ); return createElement(elementToBeCreated, propsForElement); }
其中,生成类名的方法 useInjectedStyle
的实现是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function useInjectedStyle <T >( componentStyle: ComponentStyle, isStatic: boolean , resolvedAttrs: T, warnTooManyClasses?: ReturnType<typeof createWarnTooManyClasses> ) { const styleSheet = useStyleSheet(); const stylis = useStylis(); const className = isStatic ? componentStyle.generateAndInjectStyles(EMPTY_OBJECT, styleSheet, stylis) : componentStyle.generateAndInjectStyles(resolvedAttrs, styleSheet, stylis); return className }
这里调用的 createElement
方法是 React 提供的,其 api 为:
1 2 3 4 5 React.createElement( type , [props], [...children] )