CSS-in-JS 的流行主要来自 React 社区,事实上许多 CSS-in-JS 库都是 React 专用的。但是,Emotion(根据 npm 下载量来看是最受欢迎的库)是框架无关的。
在创建自定义元素时,使用Shadow DOM很常见,但没有强制要求。并非所有用例都需要这种级别的封装。虽然也可以在常规样式表中使用 CSS 为自定义元素设置样式,但我们将重点介绍如何使用 Emotion。
我们从安装开始
npm i emotion
Emotion 提供了 css
函数
import {css} from 'emotion';
css
是一个带标签的模板字面量。它接受标准 CSS 语法,但增加了对 Sass 样式嵌套的支持。
const buttonStyles = css`
color: white;
font-size: 16px;
background-color: blue;
&:hover {
background-color: purple;
}
`
一旦定义了一些样式,就需要应用它们。处理自定义元素可能有些麻烦。像Stencil 和LitElement 这样的库编译成 Web Components,但提供了比我们开箱即得的 API 更友好的 API。
因此,我们将使用 Emotion 定义样式,并利用 Stencil 和 LitElement 来简化 Web Components 的使用。
为 Stencil 应用样式
Stencil 利用了最新的JavaScript 装饰器功能。@Component
装饰器用于提供有关组件的元数据。默认情况下,Stencil *不会* 使用 Shadow DOM,但我喜欢通过在 @Component
装饰器中设置 shadow: false
来明确这一点。
@Component({
tag: 'fancy-button',
shadow: false
})
Stencil 使用 JSX,因此样式使用花括号 ({}
) 语法应用。
export class Button {
render() {
return <div><button class={buttonStyles}><slot/></button></div>
}
}
以下是在 Stencil 中一个简单示例组件的外观。
import { css, injectGlobal } from 'emotion';
import {Component} from '@stencil/core';
const buttonStyles = css`
color: white;
font-size: 16px;
background-color: blue;
&:hover {
background-color: purple;
}
`
@Component({
tag: 'fancy-button',
shadow: false
})
export class Button {
render() {
return <div><button class={buttonStyles}><slot/></button></div>
}
}
为 LitElement 应用样式
另一方面,LitElement默认情况下使用 Shadow DOM。使用 LitElement 创建自定义元素时,会扩展 LitElement
类。LitElement
具有createRenderRoot()
方法,该方法创建并打开一个 Shadow DOM。
createRenderRoot() {
return this.attachShadow({mode: 'open'});
}
不想使用 Shadow DOM?这需要在组件类中重新实现此方法。
class Button extends LitElement {
createRenderRoot() {
return this;
}
}
在渲染函数内部,我们可以使用模板字面量引用我们定义的样式。
render() {
return html`<button class=${buttonStyles}>hello world!</button>`
}
值得注意的是,当使用 LitElement 时,只有在也使用 Shadow DOM 时才能使用 slot
元素(Stencil 没有此问题)。
综合起来,我们最终得到了
import {LitElement, html} from 'lit-element';
import {css, injectGlobal} from 'emotion';
const buttonStyles = css`
color: white;
font-size: 16px;
background-color: blue;
&:hover {
background-color: purple;
}
`
class Button extends LitElement {
createRenderRoot() {
return this;
}
render() {
return html`<button class=${buttonStyles}>hello world!</button>`
}
}
customElements.define('fancy-button', Button);
了解 Emotion
我们不必担心按钮的命名——Emotion 会生成一个随机的类名。

我们可以使用 CSS 嵌套并将类仅附加到父元素。或者,我们可以将样式定义为单独的带标签的模板字面量。
const styles = {
heading: css`
font-size: 24px;
`,
para: css`
color: pink;
`
}
然后将它们分别应用于不同的 HTML 元素(此示例使用 JSX)。
render() {
return <div>
<h2 class={styles.heading}>lorem ipsum</h2>
<p class={styles.para}>lorem ipsum</p>
</div>
}
设置容器的样式
到目前为止,我们已经为自定义元素的内部内容设置了样式。要设置容器本身的样式,我们需要从 Emotion 中导入另一个内容。
import {css, injectGlobal} from 'emotion';
injectGlobal
将样式注入到“全局作用域”(就像在传统样式表中编写常规 CSS 一样——而不是生成随机类名)。自定义元素默认情况下为 display: inline
(规范作者的一个有点奇怪的决定)。在几乎所有情况下,我都会使用应用于组件所有实例的样式更改此默认值。以下是 buttonStyles
,我们可以利用 injectGlobal
来更改它。
injectGlobal`
fancy-button {
display: block;
}
`
为什么不直接使用 Shadow DOM?
如果组件可能出现在任何代码库中,那么 Shadow DOM 可能是不错的选择。它非常适合第三方小部件——应用于页面的任何 CSS 根本不会破坏组件,这要归功于 Shadow DOM 的隔离特性。例如,Twitter 嵌入就是利用了这一点。但是,我们中的绝大多数人都是为特定站点或应用程序(而不是其他地方)创建组件的。在这种情况下,Shadow DOM 可以说是增加了复杂性,而收益有限。
而且这一切对搜索引擎优化来说都是糟糕的。
使用 CSSStyleSheet.replace 方法怎么样?