自定义组件样式

web组件的css注意项.主要是 Shadow DOM的样式,和module中加载css. css in js.

ShadowDOM 样式特点

  • 封装性:Shadow DOM 允许开发者将自定义的样式和行为封装在一个独立的作用域中,与全局样式和其他组件的样式相互隔离,避免样式冲突和污染。
  • 作用域限定:Shadow DOM 中的样式只适用于其内部的元素,不会影响外部的 DOM 结构。这使得开发者可以创建独立的、可复用的组件,而不会干扰全局样式或其他组件的样式。
  • 继承性:Shadow DOM 元素可以继承其父元素的样式。通过使用 inherit 关键字,可以让 Shadow DOM 元素继承父元素的样式属性,从而实现样式的继承和重用。
  • 插槽样式:当使用 <slot> 元素在 Shadow DOM 中插入内容时,可以为插槽内容设置样式。这样可以在 Shadow DOM 内部控制插槽内容的样式,实现更灵活的样式定制。
  • CSS 变量:Shadow DOM 支持使用 CSS 变量来定义样式,并通过 JavaScript 动态修改这些变量的值。这样可以实现更灵活的样式定制和主题切换。
  • 样式优先级:Shadow DOM 的样式遵循 CSS 的级联规则,具有一定的优先级。内联样式具有最高优先级,其次是 Shadow DOM 内部的样式表,然后是外部的样式表。这样可以确保 Shadow DOM 的样式可以被外部样式所覆盖,实现更灵活的样式定制。

样式隔离

<body>
    <style>
        .content{color:red;}
    </style>
    <div class="content">外部.content</div>
    <my-card></my-card>
    <template id="card-template">
        <div class="content">
            内部.content
        </div>
    </template>
    <script>
        class MyCard extends HTMLElement {
            constructor() {
                super();
                const shadowRoot = this.attachShadow({mode: 'open'});
                const template=document.querySelector('#card-template');
                shadowRoot.appendChild(template.content.cloneNode(true));
            }
        }
        customElements.define('my-card', MyCard);
    </script>
</body>
  • 外部样式影响不到内部.

image-20231022005755064

  • 内部样式影响不到外部
<div class="content">外部.content</div>
<template id="card-template">
      <style>
          .content{color:green;}
      </style>
      <div class="content">内部.content</div>
</template>
<div class="content">外部.content</div>

image-20231022010033710

继承样式

和普通元素一样继承#app的样式.默认是all:inherit

<style>
    #app {color:red;font-size:24px;}
</style>
<div id="app">
    <div class="content">外部.content</div>
    <my-card></my-card>
</div>
<template id="card-template">
    <div class="content">内部.content</div>
</template>

image-20231022011221645

改成initial,重置所有样式.

<template id="card-template">
    <style>
        :host{all:initial;}
    </style>
    <div class="content">内部.content</div>
</template>

image-20231022012159581

slot样式

组件可以定义slot的样式,外部可以定义slot的内容样式.

slot 并不会移动 light DOM,light DOM 节点分布到 shadow DOM 中后,slot 会对其 DOM 进行渲染,样式设置,但是节点实际还是留在原处。如果外部对 light DOM 设置了样式,那么外部样式将会覆盖 shadow DOM 中通过 ::slotted(<compound-selector>) 设置的样式,具有较高优先级。

  1. 组件内定义slot样式
    slot里的样式默认继承父级样式.
<div id="app">
    <div class="content">外部.content</div>
    <my-card></my-card>
</div>
<template id="card-template">
    <style>
        :host{all:inherit}
        .main{color:green;}
    </style>
    <div class="content">
        内部.content<br />
        <slot class="main">默认slot</slot>
    </div>
</template>

image-20231022013005730

  1. 组件外定义slot插入的内容

<p>本身就是在外部,可以随时定义样式.

<style>
    #app {color:red;font-size:24px;}
    #app p{color:blue;}/* 定义slot的内容的样式 */
</style>
<div id="app">
    <div class="content">外部.content</div>
    <my-card>
        <p>插入内容</p>
    </my-card>
</div>
<template id="card-template">
    <style>
        :host{all:inherit}
        .main{color:green;}
    </style>
    <div class="content">
        内部.content<br />
        <slot class="main">默认slot</slot>
    </div>
</template>

image-20231022013252052

  1. ::slotted 插槽插入内容后的样式.

::slotted(p) 选择插槽内容里的p标签,不支持textNode节点.

<my-card></my-card>
<my-card><p>有内容</p></my-card>
<template id="card-template">
    <style>
        :host{all:inherit}
        /* 默认样式 */
        .main{color:green;}
        /* 插入内容的样式 */
        .main::slotted(p){color:orange;}
    </style>
    <div class="content">
        内部.content<br />
        <slot class="main"><p>默认slot</p></slot>
    </div>
</template>

image-20231022014615634

外部修改组件样式的方式

组件是样式隔离的,在组件外部,我们不能直接通过选择器对组件内的元素设置样式.

注: 外部样式总是优先于在 shadow DOM 中定义的样式

可以通过几种方式修改: 全局css变量修改,::part,,js修改

尝试直接修改.

<style>
    my-card header{color:red;}
    my-card content{color:blue;}
    my-card footer{color:green;}
</style>
<div id="app">
    <my-card></my-card>
</div>
<template id="card-template">
    <style>
    </style>
    <div class="content">
       <header>卡片标题</header>
       <content>卡片内容</content>
       <footer>卡片底部</footer>
    </div>
</template>

结果,无法修改.

通过css变量修改

<style>
   :root{
    --header-color:red;
    --content-color:blue;
    --footer-color:green;
   }
</style>
<div id="app">
    <my-card></my-card>
</div>
<template id="card-template">
    <style>
        header{color:var(--header-color,gray);}
        content{color:var(--content-color,gray);}
        footer{color:var(--footer-color,gray);}
    </style>
    <div class="content">
       <header>卡片标题</header>
       <content>卡片内容</content>
       <footer>卡片底部</footer>
    </div>
</template>

结果,外部可以直接修改.

image-20231022020135437

也可以把变量局部到组件内.

<style>
   my-card{
    --header-color:red;
    --content-color:blue;
    --footer-color:green;
   }
</style>
<div id="app">
    <my-card></my-card>
</div>
<template id="card-template">
    <style>
        :host{
            --header-color:black;
            --content-color:black;
            --footer-color:black;
        }
        header{color:var(--header-color,gray);}
        content{color:var(--content-color,gray);}
        footer{color:var(--footer-color,gray);}
    </style>
    <div class="content">
       <header>卡片标题</header>
       <content>卡片内容</content>
       <footer>卡片底部</footer>
    </div>
</template>

通过::part

外部可以通过::part 修改组件部分.

注意::part不是选择器. 不能使用类似 ::part(header) p {}

<style>
    #card1::part(header) {
        color: red;
    }
    #card1::part(content) {
        color: blue;
    }
    #card1::part(footer) {
        color: green;
    }
</style>
<div id="app">
    <my-card id="card1"></my-card>
</div>
<template id="card-template">
      <style>
      :host::part(header) {
          font-size: 20px;
      }
     </style>
    <div class="content">
       <header part="header">卡片标题</header>
       <content part="content">卡片内容</content>
       <footer part="footer">卡片底部</footer>
    </div>
</template>

通过JS来改变内联样式

  1. 直接修改样式.
const card1=document.querySelector('#card1');
card1.shadowRoot.querySelector('header').style.color="red"
  1. 添加样式表:
const card1=document.querySelector('#card1');
const styles = new CSSStyleSheet();
styles.replaceSync(`
    header{color:red;font-size:20px;}
`)
card1.shadowRoot.adoptedStyleSheets=[styles];

组件内部影响外部

组件是互相隔离.

只能通过JS修改外部样式.

引入css

和html一样.

  • 内联样式
  • 外部样式表

内联样式

style标签,style标签

<script>
    customElements.define("my-card",
    class MyCard extends HTMLElement{
        constructor(){
            super();
            this
            .attachShadow({mode:"open"})
            .innerHTML=`
            <style>
                :host{
                    display: block;
                    background:red;
                }
            </style>
            <div style="color:green">组件</div>
            `
        }
    })
</script>

或通过document.createElement("style")

外部样式表

<script>
    customElements.define("my-card",
    class MyCard extends HTMLElement{
        constructor(){
            super();
            this
            .attachShadow({mode:"open"})
            .innerHTML=`
            <link
                rel="stylesheet"
                href="style.css" />
            <div style="color:green">组件</div>
            `
        }
    })
</script>

adoptedStyleSheets 动态加载

通过动态创建CSSStyleSheet

class MyCard extends HTMLElement{
    constructor(){
        super();
        this
        .attachShadow({mode:"open"})
        .innerHTML=`<div style="color:green">组件</div>`
        const styles=new CSSStyleSheet();
        styles.replaceSync(`
            :host{background:red;display:block;}
        `)
        styles.insertRule(`div{font-size:22px;}`);
        this.shadowRoot.adoptedStyleSheets=[styles];
    }
}

module js 中引用css模块.

通过adoptedStyleSheets 更容易打包.

import styles from "./style.css?inline" assert { type: "css" };

export default class MyCard extends HTMLElement{
    constructor(){
        super();
        this
        .attachShadow({mode:"open"})
        .innerHTML=`
        <div style="color:green">组件</div>
        `
        this.shadowRoot.adoptedStyleSheets=[styles];
    }
}

customElements.define('my-card', MyCard);

使用

<script type="module" src="card.js"></script>

类似vue把css写在js中.

function css(strings, ...values) {
    
    
    return tring.raw(strings, ...values);
}
class XFoo extends HTMLElement {
    static styles=css`
        :host{color:red;font-size:${10+10}px;}
    `
    constructor() {
        super()
          const styleSheet = new CSSStyleSheet();
          styleSheet.replaceSync(this.constructor.styles);
        this.shadowRoot.adoptedStyleSheets=[styleSheet];
    }
}
customElements.define('x-foo', XFoo)

ShadowDom组件常用的css选择器

  • :host: 当前根标签
<my-card >
    <p>Hello World</p>
</my-card>
<template>
  <style>
    :host{
      display:block;
      border:1px solid gray;
    }
  </style>
</template>
  • :host(): 匹配当前根组件,但只能根元素,不能与其他后代选择同时使用.
<my-card theme="dark">
    <p>Hello World</p>
</my-card>
<template>
  <style>
    :host([theme="dark"]){
      background-color:#444;
      color:#fff;
    }
    /* 不支持 */
    :host([theme="dark"]) p{
      //...
    }
  </style>
</template>
  • :host-context(xxx): 匹配当前根组件,与:host()类似. 但支持后代选择器
<my-card theme="dark">
    <p>Hello World</p>
</my-card>
<template>
  <style>
    :host-content([theme="dark"]){
      background-color:#444;
      color:#fff;
    }
    /* 支持 */
    :host-content([theme="dark"]) p{
      color: blue;
    }
  </style>
</template>
  • ::slotted(xxx),已分配的根slot插槽元素.不支持后代选择,不支持TextNode选择.
<my-card >
     <p class="section">支持</p>
      <p class="section">支持</p>
      <div>
          <p class="section">不支持</p>
      </div>
</my-card>
<template>
  <style>
    ::slotted(.section){
        color:red;
    }
    /* 不支持 */
    ::slotted(div) p{
      color:green;
    }
    /* 支持 */
    ::slotted(div)::before{
       content: "before";
     }
  </style>
</template>
  • ::part(xxx): 对外暴露的部分,类似class. 不支持后代选择器.
<template>
  <header part="title"><h3>弹窗</h3></header>
    <content part="message">message</content>
    <footer>
        <button part="confim-btn">确定</button>
        <button part="close-btn">关闭</button>
    </footer>
    <style>
      /* 组件里面使用 */
       :host::part(title){
            font-weight: bold;
        }
    </style>
</template>
组件外部
<my-dialog></my-dialog>
<style>
    my-dialog::part(close-btn){
        color:red;
    }
      
      /* 不支持 */
    my-dialog::part(title) h3{
        color:red;
    }
</style>
  • :focus-within ,自己域子元素获得焦点时.
<template shadowrootmode="open">
    <style>
        :host{
            display: block;
            border: 1px solid #ccc;
            padding: 20px;
            width:120px;
        }
        :host(:focus-within){
            background-color: red;
        }
    </style>
    <div>
        <input type="text" />
    </div>
</template>
  • :defined,表示任何已定义的元素。这包括任何浏览器内置的标准元素以及已成功定义的自定义元素

    注意包含标准元素如div. 也包含TextNode

    常用于解决自定义组件FOUC(无样式内容的闪现).

由于自定义组件通常是用 customElements.define声明的.js加载,渲染有延时. 此时自定义标签当成简单html标签处理. web组件加载完成后样式会变化,就产生闪烁. 我们通常的解决办法是,先把标签display:none,组件渲染完成后再去掉none.

<style>
    my-delay:not(:defined){
        display: none;
    }
</style>
<my-delay><p>不显示</p></my-delay>
<my-delay><p>不显示</p></my-delay>
<my-delay><p>不显示</p></my-delay>
<script>
    class MyDelay extends HTMLElement {
        constructor() {
            super();
            this.attachShadow({mode: 'open'});
        }
        connectedCallback() {
            this.shadowRoot.innerHTML="显示 "
        }
    }
    // 延迟注册组件
    setTimeout(function(){
        customElements.define('my-delay', MyDelay);
    },1000)
    // undefined
    console.log(customElements.get('my-delay'));
</script>

TextNode我们经常忽略.

下面示例,虽然<unkown-com>是一个未知的标签,但是里面包含TextNode节点,文字一样变红色.

<style>
  :defined{
    color:red;
  }
</style>
<unkown-com>
  未定义组件
</unkown-com>  

一般配合:not()使用.

<style>
  :not(:defined){
    display:none;
  }
</style>
<unkown-com>
  未定义组件
</unkown-com> 

原作者:阿金
本文地址:https://hi-arkin.com/archives/web-components-5.html

标签: css web components shadow DOM FOUC

(本篇完)

评论