Web Component初探

445 阅读3分钟

一、什么是 Web Component

Web Component 是一种 W3C标准 支持的 组件化方案,通过它,我们可以编写可复用的 组件,同时,我们也可以对自己的组件做更精细化的控制。正如 PWA 一样,他并非一项单一的技术,而是由三项技术组成

  1. Custom elements:自定义元素,通过使用对应的 api,用户可以在不依赖框架的情况下,开发原生层面的自定义元素,最关键的是,它将包含独立的生命周期,以及提供了自定义属性的监听。这就意味着它也同样具备了较高的可操作性。 =

  2. Shadow DOM 影子 dom(最大的特点是不暴露给全局),你可以通过对应的 api,将 shadow dom 附加给你的自定义元素,并控制其相关功能。利用 shadow dom 的特性,起到隔离的作用,使特性保密,不用再担心所编写的脚本及样式与文档其他部分冲突。

  3. HTML templates: 通过template、slot去实现内容分发。可以回忆一下 vue 的插槽(slot)和 react 的 props.children。

二、Custom elements

<body>
  <script type="module" src="./main.js"></script>
  <my-div></my-div>
</body>
// 继承 HTMLElement 类
class MyDiv extends HTMLElement {
    constructor() {
        super()
        const container = document.createElement('div');
        container.innerHTML = `
        <style>
            div{
                width: 200px;
                height: 200px;
                border: 1px solid #000;
            }
        </style>
        <div>
            <h3 style='color: pink;'>测试shadowDom</h3>
            <slot></slot>
        </div>`
        this.appendChild(container)

    }
}

// 名字规范必须小写,且有一个以上的 '-'
customElements.define('my-div', MyDiv);

上面已经实现了最基础的 DOM 结构了,但你会发现外层的css会影响到组件内部的样式的问题

三、Shadow DOM

this.attachShadow({ mode: 'open' })
this.shadowRoot.appendChild(container) 或者
this.shadowRoot.innerHTML = `...`

四、HTML templates

这样一行一行地生成 DOM 结构不仅写的累,读的人也很难一下子看明白。为了解决这个问题,我们可以使用 HTML 模板 。直接在 HTML 里写一个 <template> 模板:

<body>
  <script type="module" src="./main.js"></script>
  <my-html-templates></my-html-templates>
  <template id="my-html-templates">
    <div class="container">
      <img class="image" src="https://img1.baidu.com/it/u=3009731526,373851691&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500" alt="">
      <h3 class="title">切尔诺贝利</h3>
    </div>
  </template>
</body>
class MyHtmltemplates extends HTMLElement {
    constructor() {
        super()
        const templateElem = document.getElementById('my-html-templates')
        const clonedElem = templateElem.content.cloneNode(true)
        this.appendChild(clonedElem)
    }
}
customElements.define('my-html-templates', MyHtmltemplates);

五、slot(类似vue的插槽)

<my-html-templates>
    <div slot='title'>测试slot</div>
  </my-html-templates>
  
  <template id="my-html-templates">
    <div class="container">
      <img class="image" src="https://img1.baidu.com/it/u=3009731526,373851691&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500" alt="">
      <h3 class="title">切尔诺贝利</h3>
      <slot name="title"></slot>
    </div>
  </template>

六、交互

原生版本的简单交互:

<body>
  <my-counter count="100"></my-counter>
  <script>
    // 监听传递事件countFn
    (function(){
      window.addEventListener('countFn', (e) => {
        console.log(e)
        alert(e.type);
      })
    }())
  </script>
</body>

class Counter extends HTMLElement {

    // 监听属性变化
    static get observedAttributes() {
        return ['count']
    }
    // 属性变化调用
    attributeChangedCallback(attr, oldVal, newVal) {
        if (attr === 'count') {
            this.btn.textContent = newVal;
        }
    }
    // 获取count
    get count() {
        return this.getAttribute('count') ? this.getAttribute('count') : 10
    }
    // 设置count值
    set count(count) {
        return this.setAttribute('count', count)
    }
    constructor() {
        super();
        // 设置为open才可以添加元素
        this.attachShadow({ mode: 'open' })

    }
    connectedCallback() {
        this.render()
        this.btn.addEventListener('click', () => {
            this.count++
            // 事件传递
            window.dispatchEvent(new CustomEvent('countFn', { detail: { str: 123456789 } }))
        })
    }
    render() {
        // 挂载根dom
        this.shadowRoot.innerHTML = `
        <style>
            button{
            color:var(--my-color);
            width: 200px;
            height: 100px;
            border: 1px solid #000;
            }
        </style>
        <button>${this.count}</button>`
        this.btn = this.shadowRoot.querySelector('button')

    }
}

使用谷歌的lit

class MyLitCount extends LitElement {
    static properties = {
        count: {}
    }
    constructor() {
        super()
        this.count = 0
    }
    render() {
        return html`
        <style>
            button{
                width: 200px;
                height: 100px;
                border: 1px solid #000;
            }
        </style>
        <button @click=${() => this.count++}>${this.count}</button>`
    }
}

七、Attribute 和 Propertie

两个对象中都有count属性,在litAttributes会被转成Property,

Attribute: a标签 中href就是Attribute, dom标签直接声明的属性, 仅支持string,number,boolean数据类型

Property : document.getElementById('a').title 中的title就是Property, 拿到dom元素设置的属性为Property, 可传递复杂的数据类型

Property涵盖范围 >Attributes涵盖范围

八、生命周期

connectedCallback:当 custom element首次被插入 DOM 时,被调用。

disconnectedCallback:当 custom element DOM 中删除时,被调用。

adoptedCallback:当 custom element 被移动到新的文档时,被调用, 这特别适用于 iFrame。

attributeChangedCallback: 当 custom element 增加、删除、修改自身属性时,被调用

九、CSS传递

修改Shadow DOM ****的样式

index.html文件
<style>
    :root {
      --my-color: red;
    }
</style>
mian.js 文件
<style>
   color:var(--my-color)
</style>

十、vue中使用Web Component

需要过滤掉Web Component在一些打包工具的解析

webpack配置

// vue.config.js
module.exports = { chainWebpack: config => { config.module .rule('vue') .use('vue-loader') .tap(options => ({ ...options, compilerOptions: { // 将所有带 ion- 的标签名都视为自定义元素 isCustomElement: tag => tag.startsWith('ion-') } })) } }

vite配置

// vite.config.js
import vue from '@vitejs/plugin-vue' 
export default { plugins: [ vue({ template: { compilerOptions: { // 将所有带短横线的标签名都视为自定义元素 isCustomElement: (tag) => tag.includes('-') } } }) ] }

十一、相关

Web Component developer.mozilla.org/zh-CN/docs/…

谷歌lit地址: github.com/lit/lit

vue-lit地址: github.com/yyx990803/v…

组件库:

来自微软learn.microsoft.com/en-us/fluen…

web-components.carbondesignsystem.com/?path=/stor…

github.com/shoelace-st…

sap.github.io/ui5-webcomp…

kor-ui.com/introductio…

vscode风格bendera.github.io/vscode-webv…

线条风格wiredjs.com/showcase.ht… github.com/rough-stuff…