【原生组件】一文带你入门 Web Components

前言

本文将介绍一些 Web Components 的相关知识,最后经过一个组件封装的案例讲解带你入门 Web Components。javascript

什么是 Web Components

MDN:Web Components 是一套不一样的技术,容许您建立可重用的定制元素(它们的功能封装在您的代码以外)而且在您的web应用中使用它们。html

Web Components 不是单一的某个规范,而是由3组不一样的技术标准组成的组件模型,它旨在提高组件封装和代码复用能力。java

  • Custom Elements - 自定义元素
  • Shadow DOM - 影子 DOM
  • HTML Template - HTML 模版

下面我们经过一个简单的例子来逐步了解 Web Components 的知识以及使用。web

一个简单的例子

建立模版

<body>
  <!-- 建立模版 -->
  <template id="my-template">
    <p>hello~<slot name="user"></slot></p>
  </template>
</body>
复制代码

这能够看做是咱们的组件内容,由template包裹,此时该标签及其内部元素是不可见的,还须要咱们用 JavaScript 去操做并将其添加到 DOM 里才能看得见。数组

能够看到,除了template标签以外还有slot标签,对于用惯了框架的咱们应该对它不陌生了,插槽有助于提高咱们在开发中的灵活度,用法跟咱们平时的使用差很少,这里就很少赘述了。markdown

定义组件的类对象

<body>
  <!-- 模版 -->
  <template id="my-template">
    <p>hello~<slot name="user"></slot></p>
  </template>
  
  <script> // 定义类对象 class HelloComponent extends HTMLElement { // 构造函数 constructor() { // 必须先调用 super 方法 super(); // 获取<template> const template = document.getElementById('my-template'); // 建立影子 DOM 并将 template 添加到其子节点下 const _shadowRoot = this.attachShadow({ mode: 'open' }); const content = template.content.cloneNode(true) _shadowRoot.appendChild(content); } } // 自定义标签 window.customElements.define('hello-component', HelloComponent); </script>
</body>
复制代码

类 HelloComponent 继承自 HTMLElement,咱们在构造函数中去挂载 DOM 元素。app

获取<template>节点之后,克隆了它的全部子元素,这是由于可能有多个自定义元素的实例,这个模板还要留给其余实例使用,因此不能直接移动它的子元素。框架

shadow DOM

上面咱们用 attachShadow 方法建立了一个 shadow DOM,影子 DOM,如其名同样,它能够将一个隐藏的、独立的 DOM 附加到一个元素上ide

参数 mode 有两个可选值:open 和 closed。函数

open 表示能够经过页面内的 JavaScript 方法来获取 shadow DOM。

let shadowroot = element.shadowRoot;
复制代码

相反地,closed 表示不能够从外部获取 shadow DOM,此时 element.shadowRoot 将返回null。

shadow DOM 其实离咱们很近,HTML一些内置的标签就包含了 shadow DOM,如inputvideo等。

在开发者工具中,打开设置面板,勾选 Show user agent shadow DOM。

image.png

查看 input 元素。

image.png

能够看到,input 多了一些咱们平时没注意到的东西。如今你应该知道 input 的内容和 placeholder 从哪里来了,一样的,video 默认的那些播放按钮也都是藏在这里面的。

自定义标签

window.customElements.define('hello-component', HelloComponent)
复制代码

window.customElements.define 方法的第一个参数是自定义标签的名称,第二个参数是用于定义元素行为的类对象。

注意:自定义标签的名称不能是单个单词,且必须有短横线

使用

<body>
  <!-- 模版 -->
  <template id="my-template">
    <p>hello~<slot name="user"></slot></p>
  </template>
  <script> // 定义类对象 class HelloComponent extends HTMLElement { // 构造函数 constructor() { // 必须先调用 super 方法 super(); // 获取<template> const template = document.getElementById('my-template'); // 建立影子 DOM 并将 template 添加到其子节点下 const _shadowRoot = this.attachShadow({ mode: 'open' }); const content = template.content.cloneNode(true) _shadowRoot.appendChild(content); } } // 自定义标签 window.customElements.define('hello-component', HelloComponent); </script>
  <!-- 使用 -->
  <hello-component>
    <span slot="user">张三</span>
  </hello-component>
</body>
复制代码

生命周期

除了构造函数以外,custom elements 还有4个生命周期函数:

  • connectedCallback:当 custom element首次被插入文档DOM时,被调用。
  • disconnectedCallback:当 custom element从文档DOM中删除时,被调用。
  • adoptedCallback:当 custom element被移动到新的文档时,被调用。
  • attributeChangedCallback: 当 custom element增长、删除、修改自身属性时,被调用。
class HelloComponent extends HTMLElement {
  // 构造函数
  constructor() {
    super();
  }
  
  connectedCallback() {
    console.log('当自定义元素第一次被链接到文档DOM时被调用')
  }

  disconnectedCallback() {
    console.log('当自定义元素与文档DOM断开链接时被调用')
  }

  adoptedCallback() {
    console.log('当自定义元素被移动到新文档时被调用')
  }

  attributeChangedCallback() {
    console.log('当自定义元素的一个属性被增长、移除或更改时被调用')
  }
}
复制代码

实战:封装一个商品卡片组件

image.png

咱们指望的使用方式:

<body>
  <!-- 引入 -->
  <script type="module"> import './goods-card.js' </script>
  <!-- 使用 -->
  <goods-card img="https://img1.baidu.com/it/u=2613325730,275475287&fm=224&fmt=auto&gp=0.jpg" goodsName="跑鞋" ></goods-card>
</body>
复制代码

组件内容实现

// goods-card.js
class GoodsCard extends HTMLElement {
  constructor() {
    super();
    const template = document.createElement('template')
    template.innerHTML = ` <style> .goods-card-container { width: 200px; border: 1px solid #ddd; } .goods-img { width: 100%; height: 200px; } .goods-name { padding: 10px 4px; margin: 0; text-align: center; } .add-cart-btn { width: 100%; } </style> <div class="goods-card-container"> <img class="goods-img" /> <p class="goods-name"></p> <button class="add-cart-btn">加入购物车</button> </div> `;
    const _shadowRoot = this.attachShadow({ mode: 'open' })
    const content = template.content.cloneNode(true)
    _shadowRoot.appendChild(content)
  }
}

window.customElements.define('goods-card', GoodsCard)
复制代码

如今咱们已经成功把 DOM 渲染出来了,只不过如今商品图片和商品名称仍是空的,接下来咱们须要去获取父组件传递过来的值并将其展现在视图上。

class GoodsCard extends HTMLElement {
  constructor() {
    super();
    // ...省略部分代码
    // 获取并更新视图
    const _goodsNameDom = _shadowRoot.querySelector('.goods-name')
    const _goodsImgDom = _shadowRoot.querySelector('.goods-img')
    _goodsNameDom.innerHTML = this.name
    _goodsImgDom.src = this.img
  }
  
  get name() {
    return this.getAttribute('name')
  }

  get img() {
    return this.getAttribute('img')
  }
}
复制代码

到这里咱们就把一个商品卡片渲染出来了。

响应式视图更新

<goods-card img="http://zs-oa.oss-cn-shenzhen.aliyuncs.com/zsoa/goods/v1v2/1021161140018/spu1/349334579590860800.jpg" name="跑鞋" ></goods-card>
<script type="module"> import './goods-card.js' </script>
<script> setTimeout(() => { document.querySelector('goods-card').setAttribute('name', '篮球鞋') }, 2000); </script>
复制代码

这里咱们会发现一个问题,若是组件的传值发生了变化,此时视图上是不会更新的,商品名称仍然显示跑鞋而不是篮球鞋

这时候咱们须要用到前面讲到的生命周期,用attributeChangedCallback来解决这个问题,对代码进行改造。

class GoodsCard extends HTMLElement {
  constructor() {
    super();
    // ...省略部分代码
    const _shadowRoot = this.attachShadow({ mode: 'open' })
    const content = template.content.cloneNode(true)
    _shadowRoot.appendChild(content)
    this._goodsNameDom = _shadowRoot.querySelector('.goods-name')
    this._goodsImgDom = _shadowRoot.querySelector('.goods-img')
  }

  attributeChangedCallback(key, oldVal, newVal) {
    console.log(key, oldVal, newVal)
    this.render()
  }

  static get observedAttributes() {
    return ['img', 'name']
  }
  
  get name() {
    return this.getAttribute('name')
  }

  get img() {
    return this.getAttribute('img')
  }

  render() {
    this._goodsNameDom.innerHTML = this.name
    this._goodsImgDom.src = this.img
  }
}
复制代码

经过attributeChangedCallback,咱们能够在属性发生改变时执行 render 方法从而让视图获得更新。

这里还有一个陌生的函数observedAttributes,该函数是与attributeChangedCallback配套使用的,若是没写observedAttributes或属性值发生改变的参数名称没有在observedAttributes函数返回的数组里,attributeChangedCallback是不会触发。

static get observedAttributes() {
  return ['img']
}

// 不会执行,由于商品名称 name 没有在 observedAttributes 返回的数组里
attributeChangedCallback(key, oldVal, newVal) {
  console.log(key, oldVal, newVal)
  this.render()
}
复制代码

事件交互

最后咱们来给按钮添加一个点击事件,这是一个很必要的需求,方便组件调用者去处理本身的逻辑。

constructor() {
  // ...省略部分代码
  this._button = _shadowRoot.querySelector('.add-cart-btn')
  this._button.addEventListener('click', () => {
    this.dispatchEvent(new CustomEvent('onButton', { detail: 'button' }))
  })
}
复制代码
<body>
  <script> document.querySelector('goods-card').addEventListener('onButton', (e) => { console.log('添加购物车', e.detail) // button }) </script>
</body>
复制代码

完整代码

<body>
  <!-- 使用 -->
  <goods-card img="https://img1.baidu.com/it/u=2613325730,275475287&fm=224&fmt=auto&gp=0.jpg" name="跑鞋" ></goods-card>

  <!-- 引入 -->
  <script type="module"> import './goods-card.js' </script>
  <script> document.querySelector('goods-card').addEventListener('onButton', (e) => { console.log('按钮事件', e.detail) }) </script>
</body>
复制代码
// goods-card.js
class GoodsCard extends HTMLElement {
  constructor() {
    super();
    const template = document.createElement('template')
    template.innerHTML = ` <style> .goods-card-container { width: 200px; border: 1px solid #ddd; } .goods-img { width: 100%; height: 200px; } .goods-name { padding: 10px 4px; margin: 0; text-align: center; } .add-cart-btn { width: 100%; } </style> <div class="goods-card-container"> <img class="goods-img" /> <p class="goods-name"></p> <button class="add-cart-btn">加入购物车</button> </div> `;
    const _shadowRoot = this.attachShadow({ mode: 'open' })
    const content = template.content.cloneNode(true)
    _shadowRoot.appendChild(content)
    this._goodsNameDom = _shadowRoot.querySelector('.goods-name')
    this._goodsImgDom = _shadowRoot.querySelector('.goods-img')
    this._button = _shadowRoot.querySelector('.add-cart-btn')
    this._button.addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('onButton', { detail: 'button' }))
    })
  }

  attributeChangedCallback(key, oldVal, newVal) {
    console.log(key, oldVal, newVal)
    this.render()
  }

  static get observedAttributes() {
    return ['img', 'name']
  }
  
  get name() {
    return this.getAttribute('name')
  }

  get img() {
    return this.getAttribute('img')
  }

  render() {
    this._goodsNameDom.innerHTML = this.name
    this._goodsImgDom.src = this.img
  }
}

window.customElements.define('goods-card', GoodsCard);
复制代码

最后

以上,经过一个组件的封装,相信你已经对Web Components有必定的认识和了解了,固然,本文只是带你入门,讲解一些基础的开发实践知识,并不是Web Components的所有,Web Components各个部分还有不少值得深刻的地方,感兴趣的同窗可自行深刻了解一下。

感谢

本次分享到这里就结束了,感谢你的阅读,若是对你有什么帮助的话,欢迎点赞支持一下❤️。

若是有什么错误或不足,欢迎评论区指正、交流❤️。

参考资料:

阮一峰 - Web Components 入门实例教程

MDN - Web Componnents

相关文章
相关标签/搜索