一直都是听说,今天打算研究下,我把学习记录一下!
shadow介绍
shadow我的理解其实就是在真实dom下添加一个隔离区,这个隔离区内部可以写html css js,它的能力就是一个隔离作用。
官方解释说shadow是一个影子dom,允许你将一个 DOM 树附加到一个元素上,并且使该树的内部对于在页面中运行的 JavaScript 和 CSS 是隐藏的。
这里以一张图为为例,比如html5里input 标签可以设置type参数range、 date、 time,本质也是基于shadow实现的。
input标签默认是看不到的,这个需要在浏览器上去设置一个属性,打开控制面板这里,勾选shadowDOM选项
shadow组成部分
这个隔离区它有几部分组成,影子宿主(Shadow host) 影子树(Shadow tree) 影子边界(Shadow boundary) 影子根(Shadow root)
DOM 术语:
- 影子宿主(Shadow host) : 影子 DOM 附加到的常规 DOM 节点。
- 影子树(Shadow tree) : 影子 DOM 内部的 DOM 树。
- 影子边界(Shadow boundary) : 影子 DOM 终止,常规 DOM 开始的地方。
- 影子根(Shadow root) : 影子树的根节点。
可以参考这张图理解
shadow隔离区元素如何修改
这里以这种图为例
可以在控制面板上直接通过js方式来获取和修改,这个标签是我们自定义的,可以直接来修改。
如果是原生的input video等标签是无法通过这种方式修改的。
如何创建一个shadow
这里我以react中项目使用为例,通过window.customElements.define api来创建一个自定义的 组件。
js
import React from "react";
import ReactDOM from "react-dom";
import {useSetState} from 'ahooks';
window.customElements.define('custom-card',class extends HTMLElement {
constructor() {
super()
this.shadow = this.attachShadow({mode:'open'})
}
connectedCallback () {
this.template = document.createElement('template')
this.template.innerHTML = `
<div class="card-list">
<div class="card-title">${this.dataset.title}</div>
<div class="card-content">${this.dataset.content}</div>
</div>
`
this.styles = document.createElement('style');
this.styles.textContent = `
.card-list {
border:1px solid #ccc;
padding:12px 16px;
}
.card-title {
font-size:14px;
color:#999;
}
.card-content {
font-size:18px;
color:#333;
}
`;
this.shadow.appendChild(this.styles)
this.shadow.appendChild(this.template.content)
}
})
function App() {
const [state,setState] = useSetState({
title: '这是标题',
content: '这是内容',
})
return <div style={{margin:'50px auto',width:'100%',maxWidth:600}}>
<custom-card data-title={state.title} data-content={state.content}/>
</div>
}
document.body.insertAdjacentHTML("afterbegin", `<div id="root"></div>`);
ReactDOM.render(
<>
<React.Suspense fallback="">
<App />
</React.Suspense>
</>,
document.querySelector("#root")
);
shadow知识点梳理
1.利用 this.attachShadow({mode:'open'}) 隔离特性 mode: 'open' | 'close' 设置为close外交将无法访问,上面说的input video等标签原理也是通过这个属性设置的。
2.customElements本身也是带有生命周期,两个比较常用,connectedCallback(初始化执行 ) attributeChangedCallback(属性变化更新)。
3.属性传值 customElements定义标签可以通过外部data-title data-xxx的方式来传参 它的内部没有vue的响应数据自动更新,需要借助attributeChangedCallback属性监听属性变化来更新dom或样式
具体代码如如下
jsx
import React from "react";
import ReactDOM from "react-dom";
import {useSetState} from 'ahooks';
import mockjs from 'mockjs';
window.customElements.define('custom-card',class extends HTMLElement {
constructor() {
super()
this.shadow = this.attachShadow({mode:'open'})
}
static get observedAttributes () {
return ['data-title','data-content']
}
attributeChangedCallback (name,oldName,newName) {
name === 'data-title' && (this.shadow.querySelector('h6').textContent = newName);
name === 'data-content' && (this.shadow.querySelector('h5').textContent = newName);
}
connectedCallback () {
this.template = document.createElement('template')
this.template.innerHTML = `
<h6>${this.dataset.title}</h6>
<h5>${this.dataset.content}</h5>
`
this.shadow.appendChild(this.template.content)
}
})
function App() {
const [state,setState] = useSetState({
imgurl:mockjs.Random.image(),
content:mockjs.Random.csentence(),
title:mockjs.Random.ctitle(10),
})
return <div style={{margin:'50px auto',width:'100%',maxWidth:600}}>
<custom-card data-title={state.title} data-content={state.content}/>
</div>
}
document.body.insertAdjacentHTML("afterbegin", `<div id="root"></div>`);
ReactDOM.render(
<>
<React.Suspense fallback="">
<App />
</React.Suspense>
</>,
document.querySelector("#root")
);
4.被设置shadow属性里面部分标签也会继承外部标签的属性,比如文字样式特性。
以下面的两段代码为例
- shadow子元素可以取消继承 支持单个取消 和 全部取消
取消之后这里内容将不会继承外部的body
- shadow元素也是支持slot属性,这个使用和vue的插槽比较类似
定义插槽
使用插槽
完整代码如下
jsx
import React from "react";
import ReactDOM from "react-dom";
class CustomCard extends HTMLElement {
constructor (){
super()
console.log(this,'constructor')
this.template = document.createElement('template');
this.styles = document.createElement('style');
// this.shadow = this
this.shadow = this.attachShadow({mode:'open'})
}
connectedCallback () {
this.render()
}
render () {
this.styles.textContent = `
:host {
all: initial;
--list-border-color: ${'#dedede'};
}
.card-list{
border:1px solid var(--list-border-color);
border-radius: 1px;
}
.card-item{
padding:20px;
}
.card-tt{
display:flex;
align-items:center;
}
.card-td{
color: #666;
margin-bottom:10px;
}
.card-title {
font-size:14px;
color:#999;
margin-left:10px;
}
.card-img{
width:60px;
}
`
this.template.innerHTML = `
<div>
<div class="card-list">
<div class="card-item">
<div class="card-tt">
<img class="card-img" crossorigin="use" src='${this.dataset.imgurl}'/>
<span class="card-title">${this.dataset.title}</span>
</div>
<div class="card-td">
${this.dataset.content}
</div>
</div>
</div>
</div>
`
this.shadow.appendChild(this.template.content)
this.shadow.appendChild(this.styles)
}
}
window.customElements.define('custom-button',class extends HTMLElement {
constructor () {
super()
this.shadow = this.attachShadow({mode:'open'})
}
static get observedAttributes() {
return ["data-text"];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(name,'name')
console.log(oldValue,'oldValue')
console.log(newValue,'newValue')
this.shadowRoot.querySelector('button').textContent = newValue;
// console.log("Custom square element attributes changed.");
}
connectedCallback () {
this.template = document.createElement('template');
this.template.innerHTML = `
<button>${this.dataset.text || ''}</button>
`
this.shadow.appendChild(this.template.content)
}
})
window.customElements.define('custom-card',CustomCard)
window.customElements.define('custom-container',class extends HTMLElement {
constructor () {
super()
this.shadom = this.attachShadow({mode:'open'})
}
connectedCallback () {
this.template = document.createElement('template')
this.template.innerHTML = `
<div>
<slot name="content1"></slot>
<slot name="content2"></slot>
<slot name="content3"></slot>
</div>
`
this.shadom.appendChild(this.template.content)
}
})
function App() {
const [visible,setVisible] = React.useState(true)
function handleClick () {
setVisible((v) => !v)
}
return <div>
<style>
{
`
.card-list{
border:1px solid #0f0 !important;
}
body {
font-family:'PingFang';
font-size: 20px;
}
`
}
</style>
<custom-container>
<custom-button slot="content1" data-text={visible ? '关闭' : '展开'} onClick={handleClick}/>
{
visible && <custom-card slot="content2" className="custom-card" data-title={'标题标题123'}
data-imgurl="https://puui.qpic.cn/vcover_hz_pic/0/7q544xyrava3vxf1598925282532/810?max_age=7776001"
data-content="内容内容内容内容!123" data-list-border-color="pink"/>
}
</custom-container>
</div>
}
document.body.insertAdjacentHTML("afterbegin", `<div id="root"></div>`);
ReactDOM.render(
<>
<React.Suspense fallback="">
<App />
</React.Suspense>
</>,
document.querySelector("#root")
);
shadow使用场景
vue3官方也在推崇使用这个api的,喜欢研究的可以看下defineAsyncComponent()
微前端框架底层也是借助这个api封装实现的 无界
-
封装样式:使用 Shadow DOM 可以将组件的样式封装在组件内部,避免全局样式的污染和冲突。这使得组件可以具有更高的可重用性,因为它们不会受到外部样式的干扰。
-
隔离作用域:Shadow DOM 创建了一个与外部文档树隔离的作用域,在组件内部定义的 CSS 样式和 JavaScript 代码只会影响组件自身,不会影响其他组件或页面元素。这有助于解决命名冲突和作用域问题。
-
组件化开发:Shadow DOM 可以帮助开发者将组件的结构、样式和行为封装起来,使其成为一个独立的功能单元。通过这种方式,可以提高代码的模块化程度和可维护性,方便团队协作和组件复用。
-
私有部件:使用 Shadow DOM,可以将组件内部的某些部分标记为私有,不暴露给组件的用户。这样可以隐藏组件的实现细节,保护组件的内部结构和样式,同时提供公共接口供外部使用。
-
封装第三方组件:在使用第三方组件时,可以使用 Shadow DOM 来封装这些组件,以防止它们的样式和行为对整个应用程序产生意外影响。这样可以更好地控制和管理第三方组件的集成。
shadow兼容性
目前先研究到这,期待大家一块交流学习,文章只是记录一下学习过程,期待大家共同进步!