4-1、emotion效果
首先让我们来看一下emotion做了什么,这是一个使用了emotion的React组件:
import React from ‘react’;
import { css } from ‘emotion’
const color = ‘white’
function App() {
return (
padding: 32px;
background-color: hotpink;
font-size: 24px;
border-radius: 4px;
&:hover {
color: ${color};
}
`}>
This is emotion test
);
}
export default App;
这是渲染出的html:
我们可以看到emotion实际上是做了以下三个事情:
-
将样式写入模板字符串,并将其作为参数传入
css
方法。 -
根据模板字符串生成class名,并填入组件的
class="xxxx"
中。 -
将生成的class名以及class内容放到
<style>
标签中,然后放到html文件的head中。
4-2、emotion初始化
首先我们可以看到,在emotion实例化的时候(也就是我们在组件中import { css } from 'emotion'
的时候),首先调用了create-emotion
包中的createEmotion
方法,这个方法的主要作用是初始化emotion的cache(用于生成样式并将生成的样式放入<head>
中,后面会有详细介绍),以及初始化一些常用的方法,其中就有我们最常使用的css
方法。
import createEmotion from ‘create-emotion’
export const {
injectGlobal,
keyframes,
css,
cache,
//…
} = createEmotion()
let createEmotion = (options: *): Emotion => {
// 生成emotion cache
let cache = createCache(options)
// 用于普通css
let css = (…args) => {
let serialized = serializeStyles(args, cache.registered, undefined)
insertStyles(cache, serialized, false)
return ${cache.key}-${serialized.name}
}
// 用于css animation
let keyframes = (…args) => {
let serialized = serializeStyles(args, cache.registered)
let animation = animation-${serialized.name}
insertWithoutScoping(cache, {
name: serialized.name,
styles: @keyframes ${animation}{${serialized.styles}}
})
return animation
}
// 注册全局变量
let injectGlobal = (…args) => {
let serialized = serializeStyles(args, cache.registered)
insertWithoutScoping(cache, serialized)
}
return {
css,
injectGlobal,
keyframes,
cache,
//…
}
}
4-3、emotion cache
emotion的cache用于缓存已经注册的样式,也就是已经放入head中的样式。在生成cache的时候,使用一款名为Stylis的CSS预编译器对我们传入的序列化的样式进行编译,同时它还生成了插入样式方法(insert)。
let createCache = (options?: Options): EmotionCache => {
if (options === undefined) options = {}
let key = options.key || ‘css’
let stylisOptions
if (options.prefix !== undefined) {
stylisOptions = {
prefix: options.prefix
}
}
let stylis = new Stylis(stylisOptions)
let inserted = {}
let container: HTMLElement
if (isBrowser) {
container = options.container || document.head
}
let insert: (
selector: string,
serialized: SerializedStyles,
sheet: StyleSheet,
shouldCache: boolean
) => string | void
if (isBrowser) {
stylis.use(options.stylisPlugins)(ruleSheet)
insert = (
selector: string,
serialized: SerializedStyles,
sheet: StyleSheet,
shouldCache: boolean
): void => {
let name = serialized.name
Sheet.current = sheet
stylis(selector, serialized.styles) // 该方法会在对应的selector中添加对应的styles
if (shouldCache) {
cache.inserted[name] = true
}
}
}
const cache: EmotionCache = {
key,
sheet: new StyleSheet({
key,
container,
nonce: options.nonce,
speedy: options.speedy
}),
nonce: options.nonce,
inserted,
registered: {},
insert
}
return cache
}
4-4、emotion css方法
这是emotion中比较重要的方法,它其实是调用了serializeStyles
方法来处理css
方法中的参数,然后使用insertStyles
方法将其插入html文件中,最后返回class名,然后我们在组件中使用<div className={css('xxxxx')}></div>
的时候就能正确指向对应的样式了。
let css = (…args) => {
let serialized = serializeStyles(args, cache.registered, undefined)
insertStyles(cache, serialized, false)
return ${cache.key}-${serialized.name}
}
serializeStyles方法是一个比较复杂的方法,它的主要作用是处理css方法中传入的参数,生成序列化的class。
export const serializeStyles = function(
args: Array,
registered: RegisteredCache | void,
mergedProps: void | Object
): SerializedStyles {
// 如果只传入一个参数,那么直接返回
if (
args.length === 1 &&
typeof args[0] === ‘object’ &&
args[0] !== null &&
args[0].styles !== undefined
) {
return args[0]
}
// 如果传入多个参数,那么就需要merge这些样式
let stringMode = true
let styles = ‘’
let strings = args[0]
if (strings == null || strings.raw === undefined) {
stringMode = false
styles += handleInterpolation(mergedProps, registered, strings, false)
} else {
styles += strings[0]
}
// we start at 1 since we’ve already handled the first arg
for (let i = 1; i < args.length; i++) {
styles += handleInterpolation(
mergedProps,
registered,
args[i],
styles.charCodeAt(styles.length - 1) === 46
)
if (stringMode) {
styles += strings[i]
}
}
// using a global regex with .exec is stateful so lastIndex has to be reset each time
labelPattern.lastIndex = 0
let identifierName = ‘’
let match
while ((match = labelPattern.exec(styles)) !== null) {
identifierName +=
‘-’ +
match[1]
}
let name = hashString(styles) + identifierName
return {
name,
styles
}
}
// 生成对应的样式
function handleInterpolation(
mergedProps: void | Object,
registered: RegisteredCache | void,
interpolation: Interpolation,
couldBeSelectorInterpolation: boolean
): string | number {
// …
}
insertStyles方法其实比较简单,首先读取cache中是否insert了这个style,如果没有,则调用cache中的insert方法,将样式插入到head中。
export const insertStyles = (
cache: EmotionCache,
serialized: SerializedStyles,
isStringTag: boolean
) => {
let className = ${cache.key}-${serialized.name}
if (cache.inserted[serialized.name] === undefined) {
let current = serialized
do {
let maybeStyles = cache.insert(
.${className}
,
current,
cache.sheet,
true
)
current = current.next
} while (current !== undefined)
}
}
五、一些总结
总体来说,如果是进行基础组件的开发,那么使用“有规范约束”的原生css(比如遵守BEM规范的css),或者less之类的预处理语言会比较合适,这能最大幅度地减小组件库的体积,也能为业务方提供样式覆盖的能力。
如果是进行业务开发,个人比较推荐css-in-js的方案,因为它不仅能够做到在组件中直接编写css,同时也能够直接使用组件中的js变量,能有效解决“组件样式随着数据变化”的问题。另外,在业务开发中,由于迭代速度快,开发人员流动性相对大一些,我们直接使用规范对css进行约束会有一定的风险,当项目规模逐渐变大后代码的可读性会很差,也会出现css互相影响的情况。
另外使用CSS Module在业务开发中也是一种不错的方案,但一般大部分前端开发者会使用“-
”来为样式命名,但放到组件中就只能使用style['form-item']
这样的方式去引用了,我个人是不太喜欢这种风格的写法的。
不过没有一项技术是能解决全部问题的,针对不同的场景选择最合适的技术才是最优解。如果团队中还未使用这些技术,并且在开发组件的样式时遇到了文中所述的“传统css在组件开发中的痛点”,那么我建议去尝试一下css-in-js或者css module,具体选择何种方案就看团队成员更能接受哪种写法了。如果已经使用了css-in-js或者css module,那么继续使用即可,这些都是能够cover现有组件开发的场景的。
关于React的更多技巧,推荐你学习这篇:6个React Hook最佳实践技巧
六、写在最后
目前主流的前端框架,像vue和angular都针对css的作用域进行了额外的处理,比如vue的scoped,而react这里则是将css作用域处理完全交给了社区,也就出现了各种各样的css-in-js框架,虽说这两种做法其实没什么高下之分,但就个人观感来看,用来用去还是觉得vue sfc中的scoped最香(滑稽)