Web Components知识分享

400 阅读6分钟

前言

web标准在浏览器大战之后发展的越来越好了,各大浏览器厂商致力于实现标准而不是像以前实现标准存在差异,如IE的浏览器监听事件用attachEvent,其他浏览器是addEventListener,做一些个性策略来争夺市场。良性的发展促使web技术发展的越来越好,在vue、react、angular三大框架的思想冲击下,各大浏览器厂商的推动下webComponents已经变成了web规范的一部分。

什么是webComponents

webComponents 是浏览器提供的一些API,允许自定义元素、重用、封装HTML元素在网页中使用。自定义的组件和部件是基于web标准构建的。可以跨浏览器使用,支持任何HTML的库和JavaScript库一起使用。

webComponents 的三个核心

一、Custom Elements

自定义元素,也就是自定义组件。一般创建一个自定义元素过程为创建元素、注册元素,最后放在html文件中。下面来看一个demo,首先创建一个元素,使用class来继承HTMLElment。然后在链接到DOM树,给当前元素内容设置为"Hello World",通过window.customElements.define把元素注册到window下,最后把标签<hello-world></hello-world>放在html文件中。

    class HelloWorld extends HTMLElement {
      constructor() {
        super()

      }
      connectedCallback() {
        this.render()
      }
      render () {
        this.innerText = `Hello World`
      }
    }
    window.customElements.define('hello-world', HelloWorld)

既然是组件跟vue或者react之类的组件都特别相似,vue、react组件都有自己的生命周期,Custom Elements定义的组件也有生命明周期。创建一个元素一般为初始化、挂载、更新、卸载,但是目前的创建方式还不具备组件是相对独立的特性,所以目前它只是一个半成品。demo如下。

class HelloWorld extends HTMLElement {
      //初始化
      constructor() {
        super()
        this.timer = setInterVal (() => { console.log('hello world') })
      }
      //挂载:元素被添加到文档之后,浏览器会调用这个方法
      connectedCallback() {
        this.render()
      }
      //更新: 元素属性发生变化时候会回调当前方法
      attributeChangeCallback(name, oldValue, newValue) {
      }
      //卸载
      adoptedCallback () {
          clearInterval(this.timer)
      }
      //watch:监听属性变化
      static get observedAttributes () {
      //数组为当前元素的属性
          retunr []
      }
      render () {
        this.innerText = `Hello World`
      }
    }
    window.customElements.define('hello-world', HelloWorld)
}

二、template

template标签是用来存储HTML模板的。浏览器会忽略它的内容,仅在语法检查时会校验是否合规。可以在JavaScript中访问和使用它来创建其他元素。下面来看一个demo,在html中先声明template模板,通过模板id拿到模板元素内容,挂载到div上面然后放入body中,可以看到页面上会显示hell world,控制台会输出hello world。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <template id="helloWorld">
        Hello world
        <script>
            console.log('hello world')
        </script>
    </template>
    <script type="text/javascript">
        const element = document.createElement('div')
        element.append(helloWorld.content.cloneNode(true))
        document.body.append(element)
    </script>
</body>
</html>

content属性可以看做DocumentFragment片段。

三、shadowDOM

shadowDOM是webComponents标准HTMLTamplate、ShadowDOM、CustomElements、HTMLImport其中之一,HTMLImport现在被废弃了(HTMLImport是一个实验阶段的规范,后续已经被ES modules替代广泛应用了)。使用shadowDOM会带来几个好处,第一个可以进行样式隔离;第二个是使DOM具有封装、可组合的特性;第三可复comtomElement、可配置; shadowDOM的设计是基于组件化思想设计的。所以他解决了几个问题。

1、隔离DOM:一个组件就相当于是一个标签。(eg. 例如 使用querySelectorAll() 获取不到组件内部的元素)。

2、css 作用域: 声明在shadowDOM内部的样式是有作用域的。在内部生效,不会对外部产生作用。

3、可组合:可以基于jsAPI创建标签进行组合。

3、简化了CSS:作用域的DOM可以使用简单是CSS选择器,用id/class,不用担心命名冲突。

4、生产力变高:可以把页面拆分成为DOM模块。

    HTMLImport用法:< link  rel="import" href ="test1.html"/> 
    ES Moudles用法:<script type="module">  import  data from "./data.js" </script> 

shadowDOM与普通的DOM元素一样,不同点是1. 如何创建;2.如何在页面上关联;一般shadowDOM也可以作为其他元素的子元素,但是实际与其他的子元素的分离(有单独的作用域)开来的,这个有单独作用域的子树叫做shadow tree,依附的父元素称为shadow host。在shadowDOM中添加的任意元素都会变成宿主元素的本地元素,包括style。

创建一个shadowDOM,把一个shadow root是一个文档片段,依附在宿主元素上。shadow root获取依附的宿主元素,调用element.attachShdow({mode: 'open'})。

//宿主元素
const div = document.createElement('div')
// shadowRoot
const shadowRoot = div.attachShadow({mode: 'open'})
//div.shadowRoot === shandowRoot
//div === shadowHost

imagepng

上图代码生成的DOM结构。

样式设置

<template id="tmpl">
  <style>
    /* 这些样式将从内部应用到 custom-dialog 元素上 */
    :host {
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      display: inline-block;
      border: 1px solid red;
      padding: 10px;
    }
  </style>
  <slot></slot>
</template>

<script>
customElements.define('custom-dialog', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
  }
});
</script>

<custom-dialog>
  Hello!
</custom-dialog>

拓展

vue中也可以使用webcomponent 目前提供了三种方式,第一种是通过配置手脚架工具来设置, 在编译的时候跳转组件解析。

// vite.config.js 
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // treat all tags with a dash as custom elements
          isCustomElement: (tag) => tag.includes('-')
        }
      }
    })
  ]
}
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => ({
        ...options,
        compilerOptions: {
          // treat any tag that starts with ion- as custom elements
          isCustomElement: tag => tag.startsWith('ion-')
        }
      }))
  }
}

用浏览不使用手脚架工具, 使用compilerOptions来配置是否是自定义元素来跳过组件解析

// Only works if using in-browser compilation.js
// If using build tools, see config examples below.
app.config.compilerOptions.isCustomElement = (tag) => tag.includes('-')

第三种是defineCustomElementVue 支持通过该方法使用完全相同的 Vue 组件 API 创建自定义元素。

import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
  // normal Vue component options here
  props: {},
  emits: {},
  template: `...`,

  // defineCustomElement only: CSS to be injected into shadow root
  styles: [`/* inlined css */`]
})

// Register the custom element.
// After registration, all `<my-vue-element>` tags
// on the page will be upgraded.
customElements.define('my-vue-element', MyVueElement)

// You can also programmatically instantiate the element:
// (can only be done after registration)
document.body.appendChild(
  new MyVueElement({
    // initial props (optional)
  })
)

参考来源:Vue and Web Components | Vue.js

在react中使用

class HelloWorld extends HTMLElement {
  constructor () {
    super()
  }
  connectedCallback() {
   this.innerHTML = `<h1>hello world</h1>`
  }
}
customElements.define('hello-world', HelloWorld);
function App() {
  return (
    <div className="App">
       <hello-world></hello-world>
    </div>
  );
}

注意:

Web Components 通常暴露的是命令式 API。例如,Web Components 的组件 video 可能会公开 play()pause() 方法。要访问 Web Components 的命令式 API,你需要使用 ref 直接与 DOM 节点进行交互。如果你使用的是第三方 Web Components,那么最好的解决方案是编写 React 组件包装该 Web Components。

Web Components 触发的事件可能无法通过 React 渲染树正确的传递。 你需要在 React 组件中手动添加事件处理器来处理这些事件。

参考来源: 在 React 中使用 Web Components

关于webcomponents的发展思考

随着web规范的不断推陈出新也随着前端技术的高速发展也,我们前端的开发已经从刀耕火种的时代已经到了现在的三大框架三分天下的时代了。我不禁开始思考,jquery的退场是因为vue、react、angular的出现吗?或许有这些原因但不是全部吧,我们以前用的$选择器已经变成了querySelector/All了变成了DOM规范的一部分,以前的兼容性问题现在被浏览器已经磨平了,jQuery过时了,但是他真的过时了吗?我觉得没有因为它已经变成web规范的一部分了。现如今又是三大框架争锋的时代,react、vue、angular组件化的思想推动了web新的规范,webComponents 。如果当有一天有人说vue、react、angular过时了,我只想说的是它们没有过时只是功成身退,已经成为规范的一部分了。

参考资料

Web Components | MDN

HTML Standard

Shadow DOM v1 - Self-Contained Web Components

其他库:

GitHub - material-components/material-web: Material Design Web Components

genericcomponents.netlify.app/index.html

GitHub - basecamp/trix: A rich text editor for everyday writing

chessboard-element »

兼容:

"webcomponents" | Can I use... Support tables for HTML5, CSS3, etc