Web 标准与用户层:使用 CSS-in-JS 为自定义元素设置样式

Avatar of Ollie Williams
Ollie Williams

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

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;
  }
`

一旦定义了一些样式,就需要应用它们。处理自定义元素可能有些麻烦。像StencilLitElement 这样的库编译成 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 可以说是增加了复杂性,而收益有限。