Web Components & Vue

317 阅读5分钟

Web Components & Vue

记录学习过程,如有错误欢迎指出

Custom Elements

Custom Elements给予了开发者自定义标签的能力,能够通过继承HTMLElement获得标签钩子。也可以扩展标签,继承自HTMLButtonElement等内建元素

一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们----摘自MDN

Custom Elements分为两种

  1. Autonomous custom elements(自主自定义标签) 继承自HTMLElement
  2. Customized built-in elements (自定义内建元素) 扩展内置HTML元素,如HTMLButtonElement,HTMLDivElement...

Autonomous custom elements

//这里是自定义一个标签并且继承自HTMLElement
class FirstElement extends HTMLElement {
    constructor() { 
        super();
    }
    
    connectedCallback() { 
        //元素被添加进DOM后,会触发这个方法
    }
    
    disconnectedCallback() { 
        //元素在DOM中被移除时,将会触发这个方法
    }
    
    .....
}

//注册自定义标签FirstElement,让浏览器自动这个标签是一个自定义标签
//并且这里的标签命名必须时由-(中划线)分割的,原因是避免和内建 HTML 元素之间发生命名冲突
customElements.define("first-element", FirstElement);

例子:

//html
<html>
    <head>
    ...
    </head>
    <body>
        <first-element data="hello">test</first-element>
    </body>
</html>


//js
class FirstElement extends HTMLElement {
    constructor() { 
        super();
    }
    
    connectedCallback() { 
        console.log(this)
        //这里的this指向这个自定义标签,所以是可以通过this拿到这个标签的所有属性的
        //比如:offsetHeight,attribute等
        console.log(this.getAttribute('data')) //hello
    }
}

customElements.define("first-element", FirstElement);

注意:在 connectedCallback 中渲染,而不是 constructor 中。

原因1.是因为constructor被调用的时候元素虽然已经被创建,但是还未没被插入到DOM中,这样就会导致通过this取得属性时的结果为null

原因2.因为connectedCallback方法只会在元素被插入到DOM中被触发,那么如果我们不插入就不触发从而就不会渲染元素,如果在constructor中,那么一旦FirstElement被实例化就代表了渲染,有时我们可能不需要渲染,所以在connectedCallback中渲染也考虑到了性能原因

监听变化

//html 
<html>
    <head> ... </head> 
    <body> 
        <second-element data="1"></second-element> 
    </body> 
</html>


//js
class SecondElement extends HTMLElement{
    constructor(){
        super()
    }
    
    render(data=1){
        this.innerHTML = data;
    }
    
    connectedCallback(){
        //防止出现首次渲染后,attributeChangedCallback监听到变化,又调用render,render又触发connectedCallback,也就是防止死循环,只有当isRender不存在时才触发render()
        if(!this.isRender){
            render()
            this.isRender = true
        }
    }
    
    //
    static get observedAttributes() { 
        ///属性数组,这些属性的变化会被监视attributeChangedCallback方法监听
        return ['data']
    }
    
    //当上面数组中的属性发生变化的时候,这个方法会被调用
    attributeChangedCallback(name, oldValue, newValue) {
        this.render(newValue)
    }
}

customElements.define("second-element", SecondElement);

//2秒后更改second-element标签的data属性,触发attributeChangedCallback()
setTimeout(()=>{element.setAttribute('data',2)},2000)

获取子节点问题

即如果一个自定义标签在渲染后connectedCallback中获取改自定义标签的子节点,但是子节点还没有被渲染出来,导致获取不到,这时我们就应该将connectedCallback内部获取子节点的操作改为0ms的定时器,即改为宏任务,在下一次的事件循环中获取子节点,那是子节点已经渲染完毕,所以能够正常获取信息

Customized built-in elements

我们所创建的自定义标签,并没有赋予任何语义,搜索引擎不认识,无障碍阅读也不能正常识别,所以我们可以复用内置的HTML元素,使得标签符合我们的业务逻辑,并且具有语义等信息

例子

//html
//现在这个button具有我们自己实现的逻辑,也具有内置button的所有属性
<button say="hello-button">click</button>



//js
class HelloButton extends HTMLButtonElement { 
    constructor() {
        super()
        //当具有hello-button属性的button被创建时,向其自身添加一个事件处理函数
        this.addEventListener('click', () => console.log("hello"))
    } 
}

//注意第三个属性
customElements.define('hello-button', HelloButton, {extends: 'button'})

Vue-defineCustomElement

Vue通过封装了HTMLElement,暴露出defineCustomElement给开发者使用,最终还是通过customElements.define注册,并且内部的钩子和就是Custom Elements中的钩子,而且Vue官方也要求用户编写的组件名是以-(中划线)分割的

shadow DOM

DOM 元素可以有以下两类 DOM 子树:

  1. Light tree - 常规DOM子树,由HTML元素组成,我们常见的元素基本上都是Ligth的
  2. Shadow tree - 隐藏的DOM子树,不在HTML中反映出来

内建Shadow DOM

<input type="text">
<!-- 打开chrome控制台开启‘Show user agent shadow DOM’选项,即可查看到input的内置shadow dom -->
shadow dom:
<input type="text">
    #shadow-root (user=agent)
    <div></div>
</input>

如果一个元素同时有以上两种子树,那么浏览器只渲染 shadow tree。但是我们同样可以设置两种树的组合

影子树可以在自定义元素中被使用,其作用是隐藏组件内部结构和添加只在组件内有效的样式

//在自定义元素中使用shadow dom
class ShadowElement extends HTMLElement {
    connectedCallback(){
        const shadow = this.attachShadow({mode:'open'})
        shadow.innerHTML = `<div>
                ${this.getAttribute('name')}
            </div>`
    }
}
customElements.define('shadow-element',ShadowElement)


//html
<shadow-element name="jack"></shadow-element>

//最终被渲染出来的就是jack

//渲染出来的shadow dom结构
<shadow-element name="jack">
    $shadow-root(open)
    <div>jack</div>
</shadow-element>

这里需要注意的是this.attachShadow({mode:'open'}mode有两个可选值:

  1. {mode:"open"}:shadow root 可以通过 elem.shadowRoot 访问任何代码都可以访问 elem 的 shadow tree
  2. {mode:"closed"}:element.shadowRoot 永远是 null

Vue slot

思考一下slot的形式是不是和shadow dom很接近,shadow dom也有插槽这个概念,如具有插槽

两者都是在元素上定义slot属性,然后在connectedCallback进行判断渲染

模板元素

内建的 <template> 元素用来存储 HTML 模板。浏览器将忽略它的内容,仅检查语法的有效性,但是我们可以在 JavaScript 中访问和使用它来创建其他元素

<template> 
    <div>1</div>
</template>

css和js一样可以存在于模版中

<template> 
    <style>
        ...
    </style>
    <script>
        ...
    </script>
</template>

总结

  • <template> 的内容可以是任何语法正确的 HTML。
  • <template> 内容被视为“超出文档范围”,因此它不会产生任何影响。
  • 我们可以在JavaScript 中访问 template.content ,将其克隆以在新组件中复用。

Vue template语法

个人认为Vue的template语法应该是受模版元素的启发,从而设计出来的,区别在于用户编写的template是交由Vue解析器解析后生成render函数

最后

可以看出Vue的template就是模板元素+shadow dom + 自定义元素,使得我们可以在Vue中使用template语法,虽然编写的template最后都变成了render函数,但是在对于新手还是比直接编写render函数友好的

记录学习过程,如有错误欢迎指出