WebComponent原生的组件化闲谈
一、web component
是啥?
web component
是w3c
的一套使得开发者可以将HTML
页面的功能封装成自定义标签(custom elements
)的标准,可以类比目前流行的React
、Vue
等前端框架的组件化思想,不过web component
是前端标准提供的原生的组件化思想,其实和现有框架的组件化思想有异曲同工之妙,不同的是可能写法上面略有不同,再者原生者,不需要第三方的库、工具或者库进行支持。所以,我们能够将日常开发中比较常见的组件,或者将来可能需要在不同框架的应用场景中实现的功能性组件s进行抽象分离并将其原生组件化(再不影响项目开发时间效率的时候去制作),在平时的工作中有余力的去丰富开发小组中的原生组件,前提是我们工作的过程中自我体会和判断哪些模块封装成组件我们觉得是有意义的。 MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components
目前的
web component
是由三项主要技术组成:
Custom element
(自定义元素)Shadow DOM
(影子DOM
)HTML template
(HTML
模板)
二、custom elements
的使用
自定义元素,这个元素,可以理解成HTML标签元素,比如
div
、span
、video
等标签元素,而web component
标准允许开发者使用custom elements
进行自定义标签元素,下面会通过使用原生的方式去封装一个模型查看器对web component
的整体使用流程进行介绍。
-
基本介绍
CustomElementRegistry这个接口的实例是用来处理custom element的,允许开发者进行自定义组件的注册并返回自定义标签元素;
customElementRegistry.define(domstring,class,extends可选)
方法用来注册一个自定义元素,该方法的参数有三个,分别表示:DOMString
所创建的自定义元素的字符串名称,标准为了进行原生标签元素的区分,提议自定义标签元素必须使用-短横线链接表示;class
代表的是自定义标签元素的类对象,就是组件标签的样式模板和逻辑的封装类;extends
可选参数是一个对象,它可以指定自定义元素继承自哪一个内置的元素。
customElements.define('k-model-view',KModelView);
上面的代码就表示,我们定义了一个元素标签,和
vue
原生的组件注册很像,<k-model-view></k-model-view>
,指定的标签元素的类是KModelView
,我们没有指定该元素继承自某个指定的元素,就会默认继承自HTMLElement
;对于class
类对象,我们可以使用JavaScript
的类语法进行定义。
class KModelView extends HTMLElement {
construtor(){
super();
// 组件的逻辑代码
}
}
以上一个粗略的原生标签组件注册就完成了,总是感觉少了点什么,正常的一个组件很重要的一个部分就是生命周期,提供的生命周期才能让开发者进行组件逻辑的开发,才能完成一个完美的组件。是的
custom elements
也是具有生命周期的。
-
生命周期
自定义元素
custom elements
和目前主流前端框架一样,也是拥有生命周期的,在组件使用的不同的生命周期中调用不同的生命周期的回调函数,具体的生命周期的回调函数详见一下四个:-
connectedCallback
: 当自定义元素custom elements
首次被插入到文档DOM
时候被调用; -
disconnectedCallback
: 当自定义元素从文档DOM
中移除时候被调用; -
adoptedCallback
: 当自定义元素被移动到新的文档的时候会被调用; -
attributeChangedCallback
: 当元素的属性被增加、修改和删除时候会被调用,但是如果想要在元素的属性发生变化时候该回调被触发,则需要在calss
中使用静态方法:observedAttributes() get
函数来进行监听;static get observedAttributes() {return ['需要进行监听变化的属性'];
-
三、shadow DOM
的使用
web components
使用最大的意义在于封装、复用、组件化,那么自定义的组件,最想让其属性还有样式隐藏和隔离起来,不至于影响DOM
文档流,那么shadow dom
(影子DOM
)就提供了这样的一个接口供开发者使用;
Shadow DOM
允许将隐藏的DOM
树附加到常规的DOM
树中——它以shadow root
节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的DOM
元素一样,结构解释如下图:
-
shadow DOM
的一些特定名词解释如下:Shadow host
:一个常规DOM
节点,Shadow DOM
会被附加到这个节点上;Shadow tree
:Shadow DOM
内部的DOM
树;Shadow boundary
:Shadow DOM
结束的地方,也是常规DOM
开始的地方;Shadow root
:Shadow tree
的根节点;
-
基本使用:
shadow Dom
是可以独立的,其也不是新事物,浏览器内置元素已经有所应用,比如默认播放控制按钮的<video>
元素为例。你所能看到的只是一个<video>
标签,实际上,在它的Shadow DOM
中,包含了一系列的按钮和其他控制器。Shadow DOM
标准允许你为你自己的元素(custom element
)维护一组Shadow DOM
。而影子dom
可以附加到任何一个元素上面,不仅仅是custom elements
身上,但是,我们通常是将其附加到自定义元素上的。// 将shadow dom 添加到任意的元素身上 let shadowRoot = elementRef.attachShadow({mode: 'open'}); // 将shadow dom 附加到自定义元素身上 let shadowRoot = this.attachShadow({mode: 'open'}); /** * attachShadow()函数就是产生一个shadow dom 返回一个shadowRoot;也就是shadow tree的根节点,谁调用, * 就会附加相应的dom身上; * mode: 属性,表示shadow dom是否对外暴漏,意味着外部能否通过脚本进行对其操作、控制和访问。 * shadow root身上就可以挂载我们需要的shadow tree了。 * /
四、template
和slots
的使用
template
模板
template
就是类似于vue
中模板的概念,可以利用它作为组件的内容,并将其添加到shadowRoot
中去,成为shadow dom
内置的部分;模板中相较于传统的内置dom
元素,是可以添加style
样式可以将样式独立封装到自定义元素中去形成样式隔离;
<template id="my-component">
<style>
p {
color: white;
background-color: #666;
padding: 5px;
}
</style>
<p>我的组件</p>
</template>
slots
插槽
slot
插槽的使用和vue
中也是比较像的,很想vue
中使用的具名插槽,就是允许组件在其内部预留位置,供组件使用的时候,用户可以在组件中添加自己的标签内容等等,加强了组件的使用的灵活性;
<!--组件内部定义一个name插槽-->
<p><slot name="my-name">My default text</slot></p>
<!--外部使用组件的使用可以利用组件暴漏的插槽的名字进行组件内部的填充-->
<my-component>
<span slot="my-name">插槽内容</span>
</my-component>
<!--这样就将需要自定义的元素添加进自定义组件中去,灵活性仿佛不如vue这种框架-->
五、web component
常用接口
-
customElementRegistry
: 接口提供注册自定义元素和查询已注册元素的方法。要获取它的实例,请使用window.customElements
属性。customElements.define()
: 定义新的自定义元素;customElements.get()
: 返回指定的自定义元素的构造函数,如果未定义,则返回undefined
;customElements.upgrade()
: 将更新节点子树中所有包含阴影的自定义元素,它们连接到主文档之前也是;
-
HTMLTemplateElement
: 该接口来访问 HTML<template>
元素的内容,该接口继承了HTMLElement
的属性和方法; -
ShadowRoot
:该接口是一个shadow DOM
子树的根节点,它与文档的主DOM
树分开渲染,可参考上图;ShadowRoot.host
: 附加的宿主DOM
元素;ShadowRoot.innerHTML
: 内部的DOM
树;ShadowRoot.mode
:ShadowRoot
的模式——可以是open
或者closed
。这定义了shadow root
的内部实现是否可被JavaScript
访问及修改 — 也就是说,该实现是否公开,例如,<video>
标签内部实现无法被JavaScript
访问及修改。
六、组件案例实现
-
组件代码
import { Tshare } from "../../utils/Tshare"; import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' export default class KModelView extends HTMLElement { static get observedAttributes() { return ['width', 'height', 'model'] }; constructor() { super(); this.lights = null; const shadowRoot = this.initShadowDom({ mode: 'open' }); this.T = new Tshare(); } get model() { return this.getAttribute('model'); } get width() { return this.getAttribute('width'); } set width(value) { this.setAttribute('width', value); } get height() { return this.getAttribute('height'); } set height(value) { this.setAttribute('height', value); } get lights() { return this.lights; } set lights(value) { console.log(value, 'kkjjj'); this.initLights(value); } connectedCallback() { this.initWebGLEngine(this.shadowRoot.querySelector('#k-canvas')); } disconnectedCallback() { console.log('卸载'); } adoptedCallback() { console.log('移动'); } attributeChangedCallback(name, oldValue, newValue) { // console.log('属性变化', name, oldValue, newValue); switch (name) { } } initWebGLEngine(container) { this.T.initEnv(container).loadHdrToScene().initOrbitControls(); this.animate(); this.loadGLTFModel(); } animate() { requestAnimationFrame(this.animate.bind(this)); // 改变this的指向 this.T.renderer.render(this.T.scene, this.T.camera); } async loadGLTFModel() { this.loader = new GLTFLoader(); let res = await this.loader.loadAsync(this.model); this.T.sceneGraph.add(res.scene); } initLights(lights) { if (!lights) return; for (let i = 0; i < lights.length; i++) { let light = new THREE[lights[i].type](new THREE.Color(lights[i].color)); this.T.scene.add(light); } } initShadowDom(ops) { let shadowRoot = this.attachShadow(ops); shadowRoot.innerHTML = ` <style> :host { display: inline-block; width: auto; color: red; } .k-canvas { // background-color: aquamarine; } </style> <section id="k-canvas" class="k-canvas" style="width: ${this.width ? this.width : '100vw'};height: ${this.height ? this.height : "100vh" } "> </section> `; return shadowRoot; } } if (!customElements.get('k-model-view')) { customElements.define('k-model-view', KModelView); }
-
组件使用
<k-model-view id="k-model-view" width="600px" height="500px" model="./model/yz0.glb"></k-model-view>
-
property属性传值
let kmodel = document.getElementById('k-model-view'); kmodel.lights = [{ type: "AmbientLight", intensity: 1, color: "#ffffff" }];
-
效果