2024开门第一天,研究一下shadow

一直都是听说,今天打算研究下,我把学习记录一下!

shadow介绍

shadow我的理解其实就是在真实dom下添加一个隔离区,这个隔离区内部可以写html css js,它的能力就是一个隔离作用。

官方解释说shadow是一个影子dom,允许你将一个 DOM 树附加到一个元素上,并且使该树的内部对于在页面中运行的 JavaScript 和 CSS 是隐藏的。

这里以一张图为为例,比如html5里input标签可以设置type参数range、 date、 time,本质也是基于shadow实现的。

024开门第一天,研究一下shadow"

input标签默认是看不到的,这个需要在浏览器上去设置一个属性,打开控制面板这里,勾选shadowDOM选项

024开门第一天,研究一下shadow"

shadow组成部分

这个隔离区它有几部分组成,影子宿主(Shadow host) 影子树(Shadow tree) 影子边界(Shadow boundary) 影子根(Shadow root)

DOM 术语:

  • 影子宿主(Shadow host) : 影子 DOM 附加到的常规 DOM 节点。
  • 影子树(Shadow tree) : 影子 DOM 内部的 DOM 树。
  • 影子边界(Shadow boundary) : 影子 DOM 终止,常规 DOM 开始的地方。
  • 影子根(Shadow root) : 影子树的根节点。

可以参考这张图理解

024开门第一天,研究一下shadow"

shadow隔离区元素如何修改

这里以这种图为例

024开门第一天,研究一下shadow"

可以在控制面板上直接通过js方式来获取和修改,这个标签是我们自定义的,可以直接来修改。

024开门第一天,研究一下shadow"

如果是原生的input video等标签是无法通过这种方式修改的。

024开门第一天,研究一下shadow"

如何创建一个shadow

这里我以react中项目使用为例,通过window.customElements.define api来创建一个自定义的
组件。

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挂载

不是每一种类型的元素都可以附加到 shadow root(影子根)下面,比如input video等标签是不支持被挂载的。出于安全考虑,一些元素不能使用 shadow DOM(例如input、 video 、a链接),以及许多其他的元素。下面是一个可以挂载 shadow root 的元素列表:

可以被挂载的 shadow DOM 元素

024开门第一天,研究一下shadow"

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或样式

具体代码如如下

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属性里面部分标签也会继承外部标签的属性,比如文字样式特性。

以下面的两段代码为例

024开门第一天,研究一下shadow"

  1. shadow子元素可以取消继承 支持单个取消 和 全部取消

024开门第一天,研究一下shadow"

取消之后这里内容将不会继承外部的body

024开门第一天,研究一下shadow"

  1. shadow元素也是支持slot属性,这个使用和vue的插槽比较类似

定义插槽

024开门第一天,研究一下shadow"

使用插槽

024开门第一天,研究一下shadow"

完整代码如下

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()

024开门第一天,研究一下shadow"

微前端框架底层也是借助这个api封装实现的 无界

024开门第一天,研究一下shadow"

024开门第一天,研究一下shadow"

  1. 封装样式:使用 Shadow DOM 可以将组件的样式封装在组件内部,避免全局样式的污染和冲突。这使得组件可以具有更高的可重用性,因为它们不会受到外部样式的干扰。

  2. 隔离作用域:Shadow DOM 创建了一个与外部文档树隔离的作用域,在组件内部定义的 CSS 样式和 JavaScript 代码只会影响组件自身,不会影响其他组件或页面元素。这有助于解决命名冲突和作用域问题。

  3. 组件化开发:Shadow DOM 可以帮助开发者将组件的结构、样式和行为封装起来,使其成为一个独立的功能单元。通过这种方式,可以提高代码的模块化程度和可维护性,方便团队协作和组件复用。

  4. 私有部件:使用 Shadow DOM,可以将组件内部的某些部分标记为私有,不暴露给组件的用户。这样可以隐藏组件的实现细节,保护组件的内部结构和样式,同时提供公共接口供外部使用。

  5. 封装第三方组件:在使用第三方组件时,可以使用 Shadow DOM 来封装这些组件,以防止它们的样式和行为对整个应用程序产生意外影响。这样可以更好地控制和管理第三方组件的集成。

shadow兼容性

024开门第一天,研究一下shadow"

目前先研究到这,期待大家一块交流学习,文章只是记录一下学习过程,期待大家共同进步!

原文链接:https://juejin.cn/post/7318446251632050227 作者:rise_cx

(0)
上一篇 2024年1月1日 下午4:12
下一篇 2024年1月1日 下午4:23

相关推荐

发表回复

登录后才能评论