你真的了解Web Component吗?

发表于 3年以前  | 总阅读数:355 次

为什么使用框架?

对框架的理解

作为现代前端开发者,拥抱框架是生存的不二法则,有些人一入场便投身框架的海洋,有些人则有幸见证过变革,从原生,到jq,到各种框架大行其道的今天。而当前,国内占领市场份额最多的要数vue、react和angular,他们都有着各自的特点,这也是它们一路走来的立足之本。

那么作为使用者的我们,在使用框架高效处理业务的同时,对框架本身也是需要一定程度的理解,以此来辅助我们更好的学习、了解和应用框架。下面有一个表格,内容提炼自尤雨溪本人对三大框架的对比看法,也许可以一定程度提升我们对框架的认知。

职责范围的意义:

  • 大的职责范围让开发者习惯把问题抛给框架,
  • 小的职责范围让开发者习惯把问题抛给社区。

框架的优势

基于上述框架间的差异化,我们可以看出框架各自不同的设计、发展和其衍生出的生态其实都是源自于最初各自对于职责范围界定的不同而来。但尽管差异不小,它们依然存在着共性,而共性,正是源于框架本身存在的意义和目标。

回头审视,你会发现所有的框架其实都有共同的特点和目标,就是基于原生,然后更高的效率,更棒的性能,更好的差异抹平。

但我们需要正确理解这句话,这并不意味着框架的指标就优于原生,而是说,因为有了框架,我们不用再手写不依赖业务场景的数据-视图的绑定,不用再手动抹平平台或浏览器之间的差异,不用再陷入操作dom的同时还要兼顾性能苦恼。可以说框架提高了开发者开发和实现功能的各项下限,让快速开发和基础性能之间更好的平衡。我们以react和vue为例,这两大框架所带来的优势包括但不限于:

  • 数据绑定(单/双向)
  • 组件化开发(各种钩子/生命周期/作用域隔离)
  • 虚拟dom(diff算法)以及路由等。
  • ......

但这些优势不是凭空而来,就像vue的双向绑定,从使用object.defineProperty转为使用proxy,这种类似的实现或者说转变,核心之处都需要js语法以及浏览器的原生支持。因为web应用最终都是要运行在宿主--浏览器上的,所以制定规范的各大浏览器厂商以及提供原生api支持的浏览器环境才是王道,而框架不是。我们之所以需要引入各类的框架、工具库去实现各种优秀的设计与思想,比如组件化,本质上是因为原生未直接提供对应的方式或是api,所以才需要框架去构建棋盘之上的又一层规则体系,来实现开发者的诉求。

而框架这种在浏览器原生规则之上又一层较高程度的封装,在带来便利高效的同时,不可避免的带来两个缺陷:

  • 性能的下降,这也是为什么上面说有时原生的直接操作指标要优于框架。下面是一些关于处理dom的react vs js的对比:

(图1:桌面chrome; 图2:平板chrome; 图3:移动端chrome;) (下图:桌面chrome下react vs js 内存比较)

  • 框架环境的隔离,例如vue的组件库没办法很好的衔接在react的项目中(也许你会说vuera或微前端,但事实上ROI和性能并不好,开发和维护的成本较高)。

那么如果原生可以提供某些api,是不是就可以一定程度上替代框架的某些功能,在拥有便利高效的同时,跨平台、跨框架的使用,还能较大限度的保持原生的性能?

这就是接下来要聊到的是web component和其所能带来的可能甚至是变革。

认识web component

web component

狭义的来说,web component是浏览器环境提供的一些新的原生支持的api和模版。广义的说,它是一套可以支持原生实现组件化的技术。从MDN的描述中可以看到,web component的诞生,是为了解决代码复用、组件自定义、复用管理等问题。

回看上文中,我们对框架优势的分析罗列,可以发现解决这些开发痛点的方案早已存在,也就是与之对应的框架优势中的组件化。那么根据上面的分析,既然原生支持了,是不是意味着可以颠覆框架?这种想法是有些冲动的,单纯依靠原生的api去颠覆框架是不现实的,能颠覆框架的也必须是框架,因为每一个框架都意味着对应的生态(路由管理、状态管理、dom性能优化管理等)。如果有一天,当前框架中的大部分优秀的设计与思想被原生环境所吸收并支持,那么在此基础上衍生的框架,才能真正具备替代当前三大框架的能力,成为前端唯一一类框架。

而现在,我们虽然还是无法舍弃框架拥抱原生,但是我们可以将其中的一部分进行替代,使之拥有框架提供的优势,又能避免因框架而导致的缺陷。

原生组件化能否替代框架组件化?

我们先来看看组件化的特点:

  • 高内聚,低耦合
  • 标记鲜明易维护
  • 块状接口易扩展

再看看依据组件化的规范,框架组件化提供给我们最直观的体验:

  • 高效复用
  • 作用域及样式隔离
  • 自定义开发
  • 钩子函数(生命周期)
  • ......

最后我们来看看web component给我们提供了什么:

  • Custom elements:自定义元素,通过使用对应的api,用户可以在不依赖框架的情况下,开发原生层面的自定义元素,最关键的是,它将包含独立的生命周期,以及提供了自定义属性的监听。这就意味着它也同样具备了较高的可操作性。
  • Shadow DOM:影子dom(最大的特点是不暴露给全局),你可以通过对应的api,将shadow dom附加给你的自定义元素,并控制其相关功能。利用shadow dom的特性,起到隔离的作用,使特性保密,不用再担心所编写的脚本及样式与文档其他部分冲突。
  • HTML模版:通过<template/><slot/>去实现内容分发。或者你可以回忆一下vue的插槽(slot)和react的props.children。但事实上,真的是vue最先创立的slot吗?看下面~

从上述这些原生api所提供给我们的种种特性,说明web component同样可以满足我们对组件的自定义及复用、与文档其他部分隔离、生命周期的钩子函数,甚至是内容分发等这些诉求。

那么至少从理论的角度上说,web component是完全有能力替代框架组件化的,这意味着开发者可以在不使用的框架的前提下进行组件化开发,而且开发出的组件可以无缝嵌入使用了框架的项目中。有趣的是在最新发布的vue3.2中,也初步引入了对于web component的使用:

兼容性

作为开发者,面对新的强大的api,在充满热情的同时,更需要关注其可用性和普及范围。我们可以通过can i use去查看它的兼容性:https://caniuse.com/?search=web%20component。从中我们可以看到:

1. Custom elements兼容性

2. Shadow DOM兼容性

3. HTML templates兼容性

自主定制元素和自定义内置元素

在Custom elements兼容性的描述中,我们看到两个概念,如下:

  • 自主定制元素:独立元素;它们不继承自内置的 HTML 元素。
  • 自定义内置元素:这些元素继承并扩展了内置的 HTML 元素。

那么这里怎么去理解自主定制元素自定义内置元素?我们可以从具体的code实现上进行观察:

  • 自主定制元素
js: 
... 
customElements.define('custom-elements', class); 
... 
html: 
<body> 
... 
<custom-elements></custom-elements> 
... 
</body> 
  • 自定义内置元素
js:
...
customElements.define('custom-elements', class, { extends: 'p' });
...
html:
<body>
...
<p is="custom-elements"></p>
...
</body>

可以看到从声明上是没有太大区别的,都是通过 customElements.define 去定义声明,并且需要一个 class 去构建内部的生命周期与属性监听。区别之处在于自定义内置元素需要在后面的配置项中设置要继承的内置HTML 元素(指原生的元素)

而最大的区别是在于使用上,自主定制元素其实就是一个完整的自定义组件,可以让我们在不依赖任何框架的前提下实现组件化。而自定义内置组件,可以理解为是对所继承的原生元素的改造(如上述code呈现,声明定义自定义组件时,指定继承的原生元素,后续使用该原生元素时,通过is属性引用声明的自定义组件,就可以改造该原生元素,使其拥有生命周期、自定义组件和作用域隔离的功能)。

web component api的使用

自定义组件的声明和使用

所依赖的主要接口是CustomElementRegistry,该接口提供了,用作支持自定义组件的使用和声明:

  • window.customElements.define。

该方法用来声明自定义组件,接受3个参数,无返回值:

  1. name:将要全局注册的自定义组件名字(必须是中划线的形式)。
  2. constructor:一个类,如果声明的是自主定制元素,则必须继承自HTMLElement;如果声明的是自定义内置元素,则必须继承它将要扩展的原生元素所属的类(如要扩展div,那就必须继承HTMLDivElement)。并且类的构造函数中,必须执行super。
  3. options:一个可选的配置对象,只有在声明自定义内置元素时使用,且当前只有一个配置项extends,值为将要扩展的原生元素的标签名。

声明示例:

//自主定制元素 
class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    ... 
  } 
} 
customElements.define('custom-ele', CustomEle); 

//自定义内置元素,如果要扩展div的话 
class CustomEleBuiltIn extends HTMLDivElement { 
  constructor() { 
    super(); 
    ... 
  } 
} 
customElements.define('custom-ele-build-in', CustomEleBuiltIn, { extends: 'div' }); 

使用的方式也是多样的。可以通过document.createElement的方式使用,也可以直接书写在html中。使用示例:

//自主定制元素 
const customEle = document.createElement('custom-ele'); 
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
customEle.setAttribute('text', '我是一段悬停说明'); 
document.querySelector('#app').appendChild(customEle); 
customElements.define('custom-ele', CustomEle); 
//或 
customElements.define('custom-ele', CustomEle); 
const customEle = new CustomEle(); 
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
customEle.setAttribute('text', '我是一段悬停说明'); 
document.querySelector('#app').appendChild(customEle); 
//或 
<custom-ele img="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png" text="我是一段悬停说明"> 

//自定义内置元素,如果要扩展div的话 
customElements.define('custom-ele-build-in', CustomEleBuiltIn, { extends: 'div' }); 
const div = document.createElement('div', { is: 'custom-ele-build-in' }); 
div.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
div.setAttribute('text', '我是一段悬停说明'); 
document.querySelector('#app').appNode.appendChild(div); 
//或 
<div is="custom-ele-build-in" img="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png" text="我是一段悬停说明" /> 

这里的几 种使用方式其实还是有差异的,在初始化的时候,直接引用的方式可以在构造阶段就拿到挂载的各个属性;但是采用create的方式时,构造阶段无法第一时间获取属性,当然,利用生命周期的钩子函数,也是解决该问题的。

  • window.customElements.get。

该方法用来获取自定义组件的构造函数,接受一个参数,即声明过的自定义组件的name,返回构造函数。

const getCustomConstructorBefore = customElements.get('custom-ele'); 
console.log('getCustomConstructor-before', getCustomConstructorBefore);//undefined 
customElements.define('custom-ele', CustomEle); 
const getCustomConstructorAfter = customElements.get('custom-ele'); 
console.log('getCustomConstructor-after', getCustomConstructorAfter);//CustomEle 
  • window.customElements.upgrade。

该方法是用来更新挂载主文档之前的包含shadow dom的自定义组件的,接受一个参数,即包含了shadow dom的自定义组件节点,无返回值。(自定义组件在被append到主文档的时候,会触发自动更新)。

//先创建了自定义元素 
const customEle = document.createElement('custom-ele'); 
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
customEle.setAttribute('text', '我是一段悬停说明'); 
//后声明自定义元素 
customElements.define('custom-ele', CustomEle); 
//结果为false,null 
console.log(customEle instanceof CustomEle, customEle.shadowRoot); 
//进行更新节点 
customElements.upgrade(customEle);//或document.querySelector('#app').appendChild(customEle); 
//true,#document-fragment 
console.log(customEle instanceof CustomEle, customEle.shadowRoot); 

  • window.customElements.whenDefined。

该方法是用来检测并提供自定义组件被定义声明完毕的时机得,接受一个参数,即自定义元素的name,返回值是一个promise(只检测自定义组件是否被defined,不检测是否被挂载于主文档)。若提供的name无效,则触发promise的catch。

//创建了自定义元素dom 
const customEle = document.createElement('custom-ele'); 
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
customEle.setAttribute('text', '我是一段悬停说明'); 
//用来判断关闭定时器得标识 
let isStop = false; 
//获取自定义组件定义完毕的时机 
customElements.whenDefined('custom-ele').then(() => { 
  console.log('定义完毕'); 
  isStop = true; 
}); 
//一个用于观察得计时器 
const timer = setInterval(() => { 
  if (isStop) { 
    clearInterval(timer); 
    return; 
  } 
  console.log(Math.floor(Date.now() / 1000)); 
}, 1000); 
//延迟3秒进行自定义组件的定义及声明 
setTimeout(() => { 
  customElements.define('custom-ele', CustomEle); 
}, 3000) 

自定义组件的生命周期

  • constructor

自定义组件的第一个生命周期,用来初始化自定义组件本身。触发的时机在自定义组件被document.createElement的时候(前提是组件已经被customElements.define过,如果组件是先create,后defined,那么constructor的执行时机在append到主文档里时)。

class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    console.log('constructor被执行'); 
    ...... 
  } 
} 

customElements.define('custom-ele', CustomEle); 
const customEle = document.createElement('custom-ele'); 

  • connectedCallback

在组件被成功添加到主文档时触发的生命周期,在constructor之后。

class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    console.log('constructor被执行'); 
    ...... 
  }  
  connectedCallback () { 
    console.log('connectedCallback被执行'); 
  } 
} 

customElements.define('custom-ele', CustomEle); 
const customEle = document.createElement('custom-ele'); 
document.querySelector('#app').appendChild(customEle); 

  • attributeChangedCallback

自定义组件最关键的一个生命周期。触发时机在组件属性被增加、删除或修改的时候。如果你是在组件被append之前设置了属性,那么就会在connectedCallback之前触发;反之,则在connectedCallback之后触发。需要配合静态方法observedAttributes来使用,只有注册在observedAttributes中的属性才会被监听。

class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    console.log('constructor被执行'); 
    ...... 
  }  
  connectedCallback () { 
    console.log('connectedCallback被执行'); 
  } 
  static get observedAttributes () { return [ 'img', 'text' ]; } 
  attributeChangedCallback (name, oldValue, newValue) { 
    console.log('attributeChangedCallback', name) 
    if (name === 'img') { 
      this.shadowRoot.querySelector('img').src = this.getAttribute('img'); 
    } 
    if (name === 'text') { 
      this.shadowRoot.querySelector('.info').textContent = this.getAttribute('text'); 
    } 
  } 
} 

customElements.define('custom-ele', CustomEle); 
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
customEle.setAttribute('text', '我是一段悬停说明'); 
const customEle = document.createElement('custom-ele'); 
document.querySelector('#app').appendChild(customEle); 

  • adoptedCallback

当元素被移动到新的文档时,被调用。即元素是另一个文档的元素,而adoptedCallback是新文档下的自定义组件的回调。

//声明自定义组件的类 
class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    ......   
  } 
  adoptedCallback () { 
    console.log('adoptedCallback被执行'); 
  } 
} 
//创造场景,增加iframe,即旧文档 
appNode.innerHTML = '<iframe></iframe>'; 
const p = document.createElement('p'); 
p.innerHTML = 'iframe'; 
appNode.querySelector('iframe').contentWindow.document.body.appendChild(p); 

//新文档中创建自定义组件 
const customEle = document.createElement('custom-ele'); 
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
customEle.setAttribute('text', '我是一段悬停说明'); 
customElements.define('custom-ele', CustomEle); 
appNode.appendChild(customEle); 

//将元素从旧文档迁移到新文档 
setTimeout(() => { 
  console.log('开始对元素进行adoptNode操作') 
  const node = appNode.querySelector('iframe').contentWindow.document.body.firstElementChild; 
  appNode.appendChild(document.adoptNode(node)) 
}, 2000); 

该回调函数并不常用,了解即可。

  • disconnectedCallback

自定义组件的最后一个生命周期,触发的时机在组件被成功从主文档移除时。

class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    ......   
  }  
  disconnectedCallback () { 
    console.log('disconnectedCallback被执行'); 
  } 
} 

customElements.define('custom-ele', CustomEle); 
const customEle = document.createElement('custom-ele'); 
document.querySelector('#app').appendChild(customEle); 
setTimeout(() => { 
  appNode.removeChild(customEle); 
}, 2000) 

注意:浏览器关闭或tabs关闭,不会触发disconnectedCallback。

Shadow DOM的使用

其作用是将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离。Shadow DOM 都不是一个新事物,在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构,回忆一下video标签内部被隐藏起来的控制按钮们。

  • 为元素附加Shadow DOM:ele.attachShadow

attachShadow接受一个对象参数,只需关注一个配置属性mode,如果设置为open,表示可以从外部获取Shadow DOM内部的元素;如果设置为closed,则表示隐藏Shadow DOM内部,例如

class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    const shadow = this.attachShadow({ mode: 'open' }); 
    ...... 
  } 
} 
customElements.define('custom-ele', CustomEle); 
const customEle = document.createElement('custom-ele'); 
document.querySelector('#app').appendChild(customEle); 
console.log(customEle.shadowRoot) 

若mode设置为closed:

  • 操作元素的Shadow DOM并添加样式

    当为一个元素附加了Shadow DOM后,就可以使用同操作正常dom一样的方法去操作了。示例如下:

class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    const shadow = this.attachShadow({ mode: 'open' }); 

    const wrapper = document.createElement('span'); 
    wrapper.setAttribute('class', 'wrapper'); 

    const icon = document.createElement('span'); 
    icon.setAttribute('class', 'icon'); 

    const info = document.createElement('span'); 
    info.setAttribute('class', 'info'); 

    const text = this.getAttribute('text'); 
    info.textContent = text; 

    const img = document.createElement('img'); 
    img.src = this.getAttribute('img'); 
    icon.appendChild(img); 

    const style = document.createElement('style'); 
    // console.log('CustomEle', style.isConnected); 
    style.textContent = ` 
      .wrapper { 
        position: relative; 
      } 
      .info { 
        font-size: 0.8rem; 
        width: 200px; 
        display: inline-block; 
        border: 1px solid black; 
        padding: 10px; 
        background: white; 
        border-radius: 10px; 
        opacity: 0; 
        transition: 0.6s all; 
        position: absolute; 
        bottom: 20px; 
        left: 10px; 
        z-index: 3; 
      } 
      img { 
        width: 1.2rem; 
      } 
      .icon:hover + .info, .icon:focus + .info { 
        opacity: 1; 
      } 
    `; 
    shadow.appendChild(style); 
    // console.log('CustomEle', style.isConnected); 

    shadow.appendChild(wrapper); 
    wrapper.appendChild(icon); 
    wrapper.appendChild(info); 
  } 
} 
const customEle = document.createElement('custom-ele'); 
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
customEle.setAttribute('text', '我是一段悬停说明'); 
customElements.define('custom-ele', CustomEle); 
document.querySelector('#app').appendChild(customEle); 

如果想添加样式表,则可以把上述代码中的代码:

const style = document.createElement('style'); 
    // console.log('CustomEle', style.isConnected); 
    style.textContent = ` 
      .wrapper { 
        position: relative; 
      } 
      .info { 
        font-size: 0.8rem; 
        width: 200px; 
        display: inline-block; 
        border: 1px solid black; 
        padding: 10px; 
        background: white; 
        border-radius: 10px; 
        opacity: 0; 
        transition: 0.6s all; 
        position: absolute; 
        bottom: 20px; 
        left: 10px; 
        z-index: 3; 
      } 
      img { 
        width: 1.2rem; 
      } 
      .icon:hover + .info, .icon:focus + .info { 
        opacity: 1; 
      } 
    `; 
    shadow.appendChild(style); 

替换为:

const linkElem = document.createElement('link'); 
linkElem.setAttribute('rel', 'stylesheet'); 
linkElem.setAttribute('href', 'style.css');//样式的地址 

shadow.appendChild(linkElem); 

需要注意的是:由于link元素不会打断 shadow root 的绘制, 因此在加载样式表时可能会出现未添加样式内容(FOUC),导致闪烁。

模版

  • template

使用包裹的内容不会在页面上显示,但是却可以被js引用到。这就意味着有些内容我们不用重复构建多遍,使用构建一遍,然后多次引用处理就好了。

class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    console.log('constructor被执行'); 
    const shadow = this.attachShadow({ mode: 'open' }); 

    let template = document.getElementById('my-paragraph'); 
    if (template) { 
      let templateContent = template.content; 
      shadow.appendChild(templateContent.cloneNode(true)); 
    } 
    ...... 
  } 
} 
appNode.innerHTML = '<template id="my-paragraph"><style>p {color: white;background-color: #666;padding: 5px;}</style><p>My paragraph</p></template>'; 
const customEle = document.createElement('custom-ele'); 
customEle.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
customEle.setAttribute('text', '我是一段悬停说明'); 
customElements.define('custom-ele', CustomEle); 
appNode.appendChild(customEle); 

  • slot
  • 在template的基础上,更加灵活的内容分发,可以配合template使用(在template中定义占位符,然后将template的内容clone到shadow DOM中)。也可以直接在shadow DOM中添加占位符。

然后在自定义组件的innerhtml中使用即可。

class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    console.log('constructor被执行'); 
    const shadow = this.attachShadow({ mode: 'open' }); 

    let template = document.getElementById('my-paragraph'); 
    if (template) { 
      let templateContent = template.content; 
      shadow.appendChild(templateContent.cloneNode(true)); 
    } 
    const slot2 = document.createElement('slot'); 
    slot2.setAttribute('name', 'newText2'); 
    shadow.appendChild(slot2); 
    ...... 
  } 
} 
appNode.innerHTML = '<template id="my-paragraph"><style>p {color: white;background-color: #666;padding: 5px;}</style><slot name="newText1"></slot></template><custom-ele><p slot="newText1">newText1</p></custom-ele>'; 
const customEle = document.createElement('custom-ele'); 
customEle.innerHTML = '<p slot="newText2">newText2</p>'; 
customElements.define('custom-ele', CustomEle); 
appNode.appendChild(customEle); 

  • slotchange:用于监听shadow DOM中的slot插入或移除的事件。
class CustomEle extends HTMLElement { 
  constructor() { 
    super(); 
    let template = document.getElementById('my-paragraph'); 
    if (template) { 
      let templateContent = template.content; 
      shadow.appendChild(templateContent.cloneNode(true)); 
    } 
    const slots = shadow.querySelectorAll('slot'); 
    slots.forEach(slot => { 
      slot.addEventListener('slotchange', function (e) { 
        console.log('slotchange', slot.name, e); 
      }); 
    }); 
    ...... 
  } 
} 

appNode.innerHTML = '<template id="my-paragraph">' + 
  '<style>p {color: white;background-color: #666;padding: 5px;}</style>' + 
  '<slot name="newText1"></slot>' + 
  '<slot name="spanText"></slot>' + 
  '</template>' + 
  '<h3>' + 
  '<custom-ele class="newText1Box">' + 
  '<p slot="newText1">newText1</p>' + 
  '<span slot="spanText">spanText</span>' + 
  '</custom-ele>' + 
  '</h3>'; 
setTimeout(() => { 
  document.querySelector('.newText1Box').removeChild(document.querySelector('.newText1Box p')); 
  //或 
  document.querySelector('.newText1Box p').removeAttribute('slot'); 
}, 2000) 
在添加slot时(直接插入包含slot属性的元素或给已插入的元素增加slot属性)或删除slot时(直接remove包含slot属性的元素或给已插入的元素removeAttribute slot属性),都会触发slotchange事件。 

相关的其他api

  • element.attachShadow(opt):用来给指定元素挂载shadow DOM。

opt的配置项:

  • mode:如果为open,表示可以在外部通过element.shadowRoot获取shadow DOM节点。并且方法会返回shadow DOM对象。如果为closed,表示不允许外部访问shadow DOM节点,并且方法返回null。

  • delegatesFocus:表示是否减轻自定义元素的聚焦性能问题。当shadow DOM中不可聚焦的部分被点击时, 让第一个可聚焦的部分成为焦点, 并且shadow host将提供所有可用的 :focus 样式.

  • css伪类:

  • :defined:表示所有内置元素及已经通过customElements.define注册的元素。

  • :host:只能在shadow DOM的样式表内书写。表示当前所在的自定义组件的所有实例及shadow DOM下所有的元素。

  • :host([选择器]):只能在shadow DOM的样式表内书写。是:host的增强,表示:host()所在的自定义组件的所有实例中选择器符合括号中名称的实例及其包含的shadow DOM下属所有元素。

  • :host-context([选择器]):只能在shadow DOM的样式表内书写。是:host的增强,表示:host()-context所在的自定义组件的所有实例的父元素中选择器符合括号中名称的实例及其包含的shadow DOM下属所有元素。

  • :slotted([选择器]):只能在shadow DOM的样式表内书写。表示: slotted()所在的自定义组件的所有实例中选择器符合括号中名称的slot元素,若选择器为*,则表示命中所有slot。

  • 节点相关拓展

  • getRootNode:使用方式为ele. getRootNode(opt),opt中包含一个属性composed,为true时,检索到的根元素为document;为false时,如果ele是属于shadow DOM,那么检索到shadow DOM,否则检索到document。

  • isConnected:是元素的一个只读属性接口。返回元素是否与dom树连接的boolean值。即是否被append到主文档中。

  • event扩展

  • composed属性:用来指示该事件是否可以从 Shadow DOM 传递到一般的 DOM(测试后发现不论是普通DOM还是shadow DOM均为true)。

  • path属性:返回事件的路径。如果shadow root是使用mode为closed创建的,则不包括shadow树中的节点(测试后发现尽管shadowdom设置了mode为closed,依然能获取完整的path)。

  • 关于slot

  • ele.assignedSlot:用来获取ele元素上代表插入slot的元素。但如果ele.attachShadow中的mode是closed为closed时,返回null。

  • ele.slot:用来获取元素上slot的name值。

  • ......

相关的库及网站

  • webcomponents.org — site featuring web components examples, tutorials, and other information.
  • Hybrids — Open source web components library, which favors plain objects and pure functions over class and this syntax. It provides a simple and functional API for creating custom elements.
  • Polymer — Google's web components framework — a set of polyfills, enhancements, and examples. Currently the easiest way to use web components cross-browser.
  • Snuggsi.es — Easy Web Components in ~1kB Including polyfill — All you need is a browser and basic understanding of HTML, CSS, and JavaScript classes to be productive.
  • Slim.js — Open source web components library — a high-performant library for rapid and easy component authoring; extensible and pluggable and cross-framework compatible.
  • Smart.js — Web Components library with simple API for creating cross-browser custom elements.
  • Stencil — Toolchain for building reusable, scalable design systems in web components.

参考

  • https://developer.mozilla.org/zh-CN/docs/Web/Web_Components
  • https://medium.com/jspoint/the-anatomy-of-web-components-d6afedb81b37
  • https://www.ruanyifeng.com/blog/2019/08/web_components.html
  • https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements
  • https://developers.google.cn/web/fundamentals/web-components
  • https://objectpartners.com/2015/11/19/comparing-react-js-performance-vs-native-dom/
  • https://bugs.webkit.org/show_bug.cgi?id=182671

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/h8B1YxqOKtwr5CpNSYm_2A

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237289次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8126次阅读
 目录