CSS in LitElement(Web Components)

2,199 阅读5分钟

原文链接

译者注:本文中Element和Component在某些场合是通用的,本质上LitElement只是WebComponents的一种实现,所以元素也可以认为就是组件。

一、组件开发者如何定义组件样式

ShadowDOM API允许在自定义元素上创建封装好的DOM树,shadowDOM树的根节点是shadowRoot,shadowRoot所挂载的元素称为宿主元素(host element, or host)。

通常,LitElement会为每一个宿主元素创建shadowRoot,并将模板描述的DOM结构渲染到shadowRoot下。

ShadowDOM通过限制CSS的范围使得定义在shadowRoot内的样式只用于shadowRoot内的DOM元素,而不会泄露到外部DOM。除了CSS的继承属性外,shadowRoot隔离了其外部的样式,无论是位于主文档还是外部的shadowRoot。

在何处定义样式

有三处定义宿主元素和其shadowDOM样式的方式:

  • LitElement类的静态styles属性(推荐)
  • render方法的HTML模板中插入<style>元素
  • 外部样式表,通过<link rel="stylesheet" href="…">链接到LitElement模板中

LitElement类的静态styles属性

LitElement允许定义静态样式,将其应用于所有组件的实例上。

静态样式可以提高性能,它们只会被解析一次并在多个组件上复用。

如果需要定义每个组件实例的样式,需要用到CSS自定义属性(有兼容性风险,iOS9不支持):

  • 组件开发者在shadowDOM内定义CSS变量
  • 组件使用者在外部定义这些自定义CSS变量的值

定义静态styles:

import { LitElement, css, unsafeCSS } from 'lit-element';

class MyElement extends LitElement {
  static get styles() {
    const mainWidth = 800;
    const padding = 20;   
    
    return css`
      :host { 
        width: ${unsafeCSS(mainWidth + padding)}px;
      }
    `;
  } 
}

对于字面字符串(red),可以直接用${}; 变量需要用${unsafeCSS()}包裹。

⚠️unsafeCSS只可用在可信的输入,否则可能会有安全风险。

HTML模板中插入<style>元素

在模板中插入<style>可以对于每个组件实例进行定制:

import {LitElement, property} from 'lit-element';

class MyElement extends LitElement {
  @property() mainColor = 'blue';
  render() {
    return html`
      <style>
        :host {
          color: ${this.mainColor};
        }
      </style>
    `;
  }
}

⚠️ 不要在<style>内使用表达式,因为ShadyCSS polyfill的限制,表达式是不会每个实例更新的。

外部样式表

通过<link>标签加载外部样式表:

import {LitElement} from 'lit-element';

class MyElement extends LitElement {
  render() {
    return html`
      <link rel="stylesheet" href="./styles.css">
    `;
  }
}

这一方式有两个要点需要强调下:

  • 首次load时,会引起无样式内容闪烁(FOUC)
  • href的路径相对于主文档,对于公开的可复用组件不是很适用

为宿主元素(Host Element)和它的ShadowDOM编写CSS样式

为宿主元素编写样式

宿主元素可以通过:host:host()CSS伪类为自己设定样式:

  • :host选取shadowRoot对应的宿主元素

    :host {
      display: block;
      color: blue;
    }
    
  • :host(…)只有当括号内的选择器匹配宿主元素时才会生效

    :host(.important) {
      color: red;
      font-weight: bold;
    }
    

关于自定义元素的两条最佳实践:

  • :host加display样式(例如block或者inline-block),:host的默认display是inline,inline元素由它们的内容决定大小而不是通过CSS定义,它们也会导致block子元素溢出

      :host {
        display: block;
      }
    
  • :hosthidden属性的样式

      :host([hidden]) {
        display: none;
      }
    

为ShadowDOM编写样式

对于shadowRoot内的元素,用标准CSS选择器就好了。

内部定义的CSS选择器,只会作用于shadowRoot内部,所以可以在内部为所欲为,它们不会匹配到外部的元素:

* {
  color: black;
}

h1 {
  font-size: 4rem;
}

#main {
  padding: 16px;
}

.important {
  color: red;
}

为插槽子元素(slotted children)编写样式

对于用<slot>插槽方式插入到shadowDOM的元素,可以用::slotted()伪元素选取:

  • ::slotted(*)匹配所有的插槽元素
  • ::slotted(p)匹配所有插槽的p元素
  • div ::slotted(*)匹配div内的所有插槽元素
import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  render() {
    return html`
      <style>
        :host([hidden]) { display: none; }
        :host { display: block; }
        ::slotted(*) { font-family: Roboto; }
        ::slotted(span) { color: blue; }
        div ::slotted(*) { color: red; }
      </style>
      <slot></slot>
      <div><slot name="hi"></slot></div>
    `;
  }
}
customElements.define('my-element', MyElement);

二、组件使用者如何定义组件样式

使用者在使用组件时,只需要在主文档内用自定义组件的tag作为selector即可,例如:

<style>
  my-element { 
    font-family: Roboto;
    font-size: 20;
    color: blue;
  }
</style>
...
<my-element></my-element>

这些外部定义的属性,作用于宿主元素上,优先级高于内部用:host定义的属性

三、主题

这一节描述的是如何使用CSS继承和自定义CSS属性,用于:

  • 创建easily stylable LitElement组件
  • 创建可以轻松用于导入LitElement的样式主题

CSS继承和shadowDOM

继承CSS属性(如color,font-family以及所有的CSS自定义属性(—*)可以穿过shadowRoot边界进行继承,这意味着通常情况下,元素可以从外部共享一些重要的样式。

组件的作者可以利用这一点在:hostCSS伪类上定义一些继承属性,它们会被用在shadowRoot内部的元素上:

my-element.js

render() {
  return html`
    <style>
      :host { 
        display: block;  //需要注意的是display不是继承属性
        font-family: Roboto;
        font-size: 20;
        color: blue;
      }
    </style>
    <p>Inherits font styles</p>
  `;
}

我们也可以在外部主文档中定义继承属性改变:host中的属性,因为外部的元素类selector的优先级高于:host伪类selector,外部的样式会重载内部的样式:

index.html

<style>
  /* Overrides the `color` property of the `:host` styles in my-element.js */
  my-element { 
    color: blue;
  }
</style>
...
<my-element></my-element>

自定义CSS属性

自定义CSS属性可以沿着DOM树继承下来,并且可以穿过shadowRoot的边界,可以利用这一点实现样式和主题定制。

my-element.js

<style>
  :host {
    display: block;
    background-color: var(--myBackground, yellow);
  }
</style>

Index.html

<style>
  my-element {
    --myBackground: rgb(67, 156, 144);
  }
</style>

在外部定义的—myBackground自定义属性会用在内部my-element上

四、小结

  • 对于组件的通用样式,即所有组件共享的样式,建议用静态styles属性进行设置
  • 对于组件的实例,推荐用HTML模板<style>,通过${this.myColor}实现实例定制
  • ::host伪类用于设定宿主元素的样式,::slotted()用于设定插槽子元素的样式, shadowRoot内的样式直接用标准CSS选择器即可
  • 从外部透传属性有两种方式,CSS继承属性和CSS自定义属性,自定义属性更灵活,但有一定的兼容性问题