前言
2021.8.9 vue发布了 3.2.0 release。其中的内容有
- Features xxx
- Bug Fixes xxx
在 features 中我看到了这么一个
defineCustomElement
api。点开文档文档,发现这就是早有耳闻的 web component。那么我就先来和 web component 来一场快乐的初体验吧。
WebComponent
web component docs 简单翻阅翻阅文档,以下为我的简单概况。 WebComponent由以下三个部分组成
- Custom elements(自定义元素): 一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。
- Shadow DOM(影子DOM) :一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
- HTML templates(HTML模板):
<template>
和<slot>
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
Custom elements
使用 customElements.define(name, constructor, options)
的 web api 的定义一个自定义的元素标签。
- name 自定义元素名
- constructor
- options?
- extends 指定继承的已创建的元素. 被用于创建自定义元素
如下使用:
class Dialog extends HTMLElement {
constructor() {
super();
// ....
}
};
customElements.define('tree-dialog', Dialog);
这样就生成了一个
<tree-dialog></tree-dialog>
的标签可以在html中使用了
Shadow DOM
shadow dom写过小程序的可能比较熟悉一些。小程序的自定义组件在Wxml中的表现就是将你的组件模板放到了这个 #shadow-root
中
以此来达到私有的作用。在构造器中使用 this.attachShadow
这个api就可以使用 Shadow DOM。
class Dialog extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const wrapper = document.createElement('div');
shadow.appendChild(wrapper);
}
};
customElements.define('tree-dialog', Dialog);
这样就给这个组件添加了一个 dom 结构。
HTML templates
templates
大家应该都不陌生,使用呢也非常的方便,在html
写下自己需要的html结构
<html>
<template id="dialog-template">
<style>
.wrapper {
...
}
...
</style>
<div class="wrapper">...</div>
</template>
</html>
使用的时候呢,只要获取到这个文档碎片"clone"
到我们的 shadow dom
中即可
class Dialog extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.querySelector("#dialog-tempalate").content;
shadow.appendChild(template.cloneNode(true))
}
};
customElements.define('tree-dialog', Dialog);
当然这里面还要其他知识点,例如 slot
。这里就不多赘诉啦。
实现一个 Dialog 组件
过完以上的文档以及例子来实现一个 dialog
组件应该来说是木得问题。直接开始上手。
class Dialog extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// 创建一个容器
const wrapper = document.createElement('div');
// 加上样式
wrapper.setAttribute('class', 'wrapper');
// 创建一个内容
const content = document.createElement('div');
// 加上样式
content.setAttribute('class', 'wrapper-content');
const style = document.createElement('style');
style.textContent = `// 样式 .....`;
wrapper.appendChild(content);
shadow.appendChild(wrapper);
shadow.appendChild(style);
}
};
customElements.define('tree-dialog', Dialog);
结构和样式没问题,现在开始写显示隐藏等这些逻辑。样式取自 neumorphismv
文本提示信息呢,可以通过 dataset
来传入文本信息,如。
<tree-dialog data-text="Hello World"></tree-dialog>
而构造器中呢,只需要这样就可以将文本插入到dom中
// content 为文本的dom容器
// this 指组件本身
content.innerText = this.getAttribute('data-text');
翻阅文档,可以看到。这几个生命周期
connectedCallback
:当 custom element首次被插入文档DOM时,被调用。disconnectedCallback
:当 custom element从文档DOM中删除时,被调用。adoptedCallback
:当 custom element被移动到新的文档时,被调用。attributeChangedCallback
: 当 custom element增加、删除、修改自身属性时,被调用。
那么就可以通过 attributeChangedCallback
这个生命周期函数来切换组件的显示和隐藏。先写好初始的状态,然后加上 active
类就可以显示,再通过 show
这个属性的变化来加 active
类,并且一开始也要加上 show
的判断。
class Dialog extends HTMLElement {
constructor() {
...
wrapper.setAttribute("class", `wrapper ${this.getAttribute('show') === 'true' ? 'active' : ''}`);
...
}
attributeChangedCallback() {
const shadowRoot = this.shadowRoot;
const wrapper = shadowRoot.querySelector('.wrapper');
wrapper.setAttribute("class", `wrapper ${this.getAttribute('show') === 'true' ? 'active' : ''}`);
}
}
但是此时 active
这个类并没有加到 wrapper
这个dom身上。通过 console
发现 attributeChangedCallback
并没有调用。哦ho,那就是姿势不正确了。查阅文档发现
正确的姿势应该是在静态方法 observedAttributes
中告知组件需要注意那些属性变化。
class Dialog extends HTMLElement {
static get observedAttributes() { return ['show']; }
constructor() {
...
}
attributeChangedCallback() {
...
}
}
这样就可以啦。然后再往组件中加入一个点击事件,当点击背景蒙层的时候关闭这个 dialog
,同样是通过声明周期的函数实现。
class Dialog extends HTMLElement {
...
connectedCallback() {
this.addEventListener('click', wrapperClick, true)
}
disconnectedCallback() {
this.removeEventListener('click', wrapperClick, true)
}
}
function wrapperClick(component) {
const target = component.target;
const shadowRoot = target.shadowRoot;
const wrapper = shadowRoot.querySelector('.wrapper');
(component.path[0] === wrapper) && target.setAttribute("show", false);
}
最终效果
这个demo只是简单实现一下。当然如果是dialog这种全屏弹窗的组件最好还是 append 到 body 尾部比较好。原因的话可以看一下下面这篇文章 position: fixed 一定是相对于浏览器窗口进行定位吗?
结尾
祝大家中秋快乐~