声明式 Shadow DOM
通常都是通过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 结果:
声明 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 不生效
结果:
不能解析,警告错误.
解析包含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 });
当 shadowrootmode=closed时,不能获取.
加载样式表
使用标准<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>
序列化closed的shadow DOM
前面不支持序列化closed的shadow .
通过通过自义组件,获取shadow,传递给getInnerHTML
的closedRoots
配置.从而获取序列化closed的shadow.
getInnerHTML({includeShadowRoots:true,closedRoots:[$0.shadow]})
兼容性
某些浏览器不支持声明式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