通常都是通过JS创建Shadow DOM ,禁用JS或服务端渲染时Shadow DOM 不能正常解析,js加载延迟,shadow DOM 样式解析延迟,导致页面闪烁. 造成"无样式内容的闪现”(FOUC). 声明式shadowDOM由浏览器内核直接解析,方便SSR服务器渲染,有利于搜索引擎SEO.

JS 创建 shadow DOM

const element=document.createElement('div');
const shadowRoot=element.attachShadow({mode:'open'});
shadowRoot.innerHTML='<h1>Hello World</h1>';
document.documentElement.appendChild(element);

生成以下HTML结构:

<div>
  #shadow-root (open)
    <h1>Hello World</h1>
</div>

声明式Shadow DOM

<div>
    <template shadowrootmode="open">
        <h1>Hello World</h1>
    </template>
</div>

浏览器直接解析成

<div>
  #shadow-root (open)
    <h1>Hello World</h1>
</div>

解析完后template被删除.

相关文章:

https://developer.chrome.com/articles/declarative-shadow-dom/

构建declarative-shadow-dom 语法结构

<host-element>
    <template shadowrootmode="open">
        <style>:host{background-color: #ccc;display: block;}</style>
        <h2>Shadow Content</h2>
        <slot></slot>
    </template>
    <h2>Light content</h2>
</host-element>

相当于JS

<host-element>
    <template>
        <style>:host{background-color: #ccc;display: block;}</style>
        <h2>Shadow Content</h2>
        <slot></slot>
    </template>
    <script>
      var template = document.currentScript.previousElementSibling;
      var shadowRoot = template.parentElement.attachShadow({mode:"open"});
      shadowRoot.appendChild(template.content);
      template.remove();
      document.currentScript.remove();
    </script>
    <h2>Light content</h2>
</host-element>

渲染流程:

  • 找到<template shadowrootmode="open|closed"> 标签,多个template以最后一个以准.
  • 把template内容解析成DocumentFragment
  • 将shadow DOM 附加到template的父级元素.
  • 将DocumentFragment内容移动到shadow DOM 根节点.
  • 删除所有<template shadowrootmode>节点

HTML==>DOM 结果:

img

声明 attachShadow

  • shadowrootmode属性的template不在解析成普通的template标签.
<template shadowrootmode="open|closed">

相当于

attachShadow({ mode = "open|closed"});
  • delegatesfocus属性
<template shadowrootmode="open" shadowrootdelegatesfocus>

相当于:

attachShadow({ mode = "open", delegatesFocus = true });

类似其他属性,通过shadowrootxxx形式.

slot

根据上面流程,把<template shadowrootmode="open" 挂载到父级标签,并删除template标签.

剩下的就是slot的内容.

<host-element>
   #shadow-root (open)
   <style>:host{background-color: #ccc;display: block;}</style>
    <h2>Shadow Content</h2>
    <slot>
       ↳
       <h2>Light content</h2>
    </slot>
 <h2>Light content</h2> 
</host-element>  

顺序没有要求,但习惯把template放最前面.

<div>
  <p>内容1</p>
  ....
  <template shadowrootmode="open">
      <h1>Hello World</h1>
      <slot></slot>
  </template>
  <p>内容2</p>
  ....
</div>

具名slot

<div>
  <template shadowrootmode="open">
      <slot name="title">默认标题</slot>
      <slot name="content"></slot>
  </template>
   <h1 slot="title">标题</h1>
   <p slot="content">内容2</p>
</div>

能否动态添加shadowrootmode呢?

const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // 
div.appendChild(template);
div.shadowRoot; // null

template仍然是普通template元素.

template里包含 声明shadowDOM

<template id="tpl2">
    <x-foo>
        <template shadowrootmode="open">
            <div>内容</div>
        </template>
    </x-foo>
</template>
<script>
    const tpl = document.querySelector('#tpl2')
    document.body.appendChild(tpl.content.cloneNode(true))
</script>

x-foo也能解析成shadow DOM

解析shadow HTML

const html=`
  <h2>普通标签</h2>
  <x-foo>
      <template shadowrootmode="open">
          <div>ShodowDOM内容</div>
      </template>
  </x-foo>
`
document.documentElement.innerHTML=html;// shadow 不生效
document.documentElement.insertAdjacentHTML('beforeend',html);// shadow 不生效

结果:

不能解析,警告错误.

image-20231106011911937

解析包含shadow的html,唯一方法,通过DOMParser,配置includeShadowRoots:true

const fragment = new DOMParser()
  .parseFromString(
      html, 
      'text/html',
      {includeShadowRoots: true}
  );
document.documentElement.replaceWith(fragment.body);

获取shadow DOM内容

innerHTML 获取内容,不包含shadow DOM 内容.

可以通过getInnerHTML()方法获取.

 element.getInnerHTML({ includeShadowRoots: true });

image-20231106021115041

当 shadowrootmode=closed时,不能获取.

image-20231106021320262

加载样式表

使用标准<style><link>标签在声明性影子根内部完全支持内联和外部样式表

<x-foo>
    <template shadowrootmode="open">
        <style>
            :host {
                display: block;
                background-color: #ccc;
            }
        </style>
        <link rel="stylesheet" href="style.css" />
        <div class="main">shadowRoot</div>
    </template>
</x-foo>

在自定义组件中应用

<x-foo>
    <template shadowrootmode="closed">
        <div class="main">shadowRoot</div>
        <slot>默认</slot>
    </template>
    <p>内容</p>
</x-foo>
<x-foo>
    <p>内容2</p>
</x-foo>
<script>
    customElements.define('x-foo',class extends HTMLElement{
        constructor(){
            super();
            const internals = this.attachInternals();
            this.shadow = internals.shadowRoot;
            // 未定义
            if (!this.shadowRoot) {
                this.shadow=this.attachShadow({mode: 'open'});
                this.shadow.innerHTML = `
                <div class="main">shadowRoot</div>
                <slot>默认</slot>
                `
            }
            
        }
    }
    );
</script>

image-20231106030638856

序列化closed的shadow DOM

前面不支持序列化closed的shadow .

通过通过自义组件,获取shadow,传递给getInnerHTMLclosedRoots配置.从而获取序列化closed的shadow.

getInnerHTML({includeShadowRoots:true,closedRoots:[$0.shadow]})

image-20231106031339794

兼容性

某些浏览器不支持声明式shadow语法.

使用js解析.

(function attachShadowRoots(root) {
  root.querySelectorAll("template[shadowrootmode]").forEach(template => {
    const mode = template.getAttribute("shadowrootmode");
    const shadowRoot = template.parentNode.attachShadow({ mode });
    shadowRoot.appendChild(template.content);
    template.remove();
    attachShadowRoots(shadowRoot);
  });
})(document);

是否支持声明式shadow

function supportsDeclarativeShadowDOM() {
  return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}

可重用

注意区分 shadow DOM 和web components ,web components是自定义web组件,可重用. 而shadow DOM 和轻量html标签一样. 不是为了重用而设计的,重用就是和普通html标签一样clone.

 <template id="tpl">
    <y-foo>
     <template shadowrootmode="open">
     重用内容
     </template>
    </y-foo>
</template>
<script>
    const content=document.querySelector('template#tpl').content;
    document.documentElement.appendChild(content.cloneNode(true));
    document.documentElement.appendChild(content.cloneNode(true));
    document.documentElement.appendChild(content.cloneNode(true));
</script>

原作者:阿金
本文地址:https://hi-arkin.com/archives/declarative-shadow-dom.html

标签: web components shadow DOM declarative-shadow-dom

(本篇完)

评论