引言
最近在实习过程中,有注意到公司的项目使用到了微前端,于是鄙人便去简单了解了一下微前端,发现目前现有的微前端框架中,single-spa自身并没有解决css样式隔离的问题,他主要关注了微前端应用的下载、挂载和生命周期管理等。而基于single-spa进一步封装的qiankun框架,他主要是通过Shadow Dom和命名空间的方式来实现了css样式隔离的问题,使得全局 CSS 不会影响子应用,子应用间的 CSS 不会互相影响,子应用的CSS不会影响主应用全局,主应用的全局 CSS 不会影响子应用。同样对于wujie这个微前端框架,他是通过iframe和webcomponent的方式来实现dom和css样式的隔离,同时也是利用了webcomponent实现了应用可以获得类似vue的keep-alive的能力.这也是wujie这个微前端框架的一个优势所在。
为此,我也是简单的去学习了一下web component的,以下是我的一个简单的学习总结吧
1. 什么是web component
Web Components 是一套不同的技术,允许你创建可复用的自定义元素(自定义元素、阴影DOM、HTML模板)并且扩展HTML。 主要包括:
- 自定义元素(Custom Elements):允许你定义属于自己的HTML标签,并定义它们的功能。这里所说的HTML标签就是我们常见的
<p>标签,<div>标签等等。自定义元素标签就是指<hello-world>等这种本身不存在规范中的标签。 - 阴影DOM(Shadow DOM):允许你封装DOM subtree和CSS样式,隐藏其细节。用一个通俗的比喻来说,Shadow DOM 就像一个孩子的密码本。密码本的内容是隐藏的,不会暴露在父亲母亲那里。同样,Shadow DOM 也将其中的 DOM 结构和样式隐藏起来,不会影响外界的 DOM。
- HTML模板(HTML Templates):允许你定义不会直接渲染的HTML片段,之后可以通过JavaScript实例化。HTML模板就指的是
<template>这个标签,它本身是不会直接渲染到页面上的,即使你在body中使用了这个<template>标签
<!DOCTYPE html>
<html lang="en">
<body>
<template>
<div>我不会渲染到页面上</div>
</template>
</body>
- 导入(HTML Imports):允许你在一个文档中导入另一个文档的内容。具体含义就是使用ES6的import导入,然后显示调用customElements.define()方法注册,这里就不做过多解释了。
所以Web Components主要由上述4种技术组成,目的是实现封装性好、可重用的 UI 组件,可以在不同框架或项目中复用。
下面我们来介绍一下他们的具体使用;
2.使用
对于我们程序员来说👨🏻💻,学习一门语言都是从打印hello,world开始,(最后我只精通各种语言helloworld的打印😭呜呜呜)那我们现在就实现一个类似helloworld的效果,如下图。
第一步: 创建一个类或函数来指定 web 组件的功能
//这里我们创建了一个HelloWorld的类,继承自HTMLElement。其实自定义元素分为两类,一类是独立的元素,
//一类是继承基本的html元素的元素。这里我们是写的独立元素,所以需要继承自HTMLElement类。
class Helloworld extends HTMLElement{
constructor() {
super();
const shadow=this.attachShadow({mode: 'open'});
//这里的mode:open,就相当于打开这个黑盒子。
const div=document.createElement('div')
div.innerHTML='Hello,World';
div.className='hello';
const style=document.createElement('style')
style.innerHTML='
.hello{
color:red;
font-size:32px;
}
';
//在这里有小伙伴可能会想,为什么不在上面直接定义一个style标签,
//而在下面又自己创建style标签呢?
//因为shadow dom相当于一个黑盒,外面的样式和脚本是不会影响到黑盒内部的
shadow.appendChild(style)
shadow.appendChild(div)
}
}
第二步: 使用 CustomElement.define()方法注册您的新自定义元素,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素.
window.customElements.define('hello-world', HelloWorld);
随后我们就可以直接在html中使用这个标签。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.hello{
color: red;
font-size: 32px;
}
</style>
</head>
<body>
<hello-world></hello-world>
<script>
class HelloWorld extends HTMLElement{
constructor() {
super();
const shadow=this.attachShadow({mode: 'open'});
const div=document.createElement('div');
div.innerHTML='hello world';
const style=document.createElement('style');
style.innerHTML=`
.hello{
color: red;
font-size: 32px;
}
`
div.className = 'hello';
shadow.appendChild(style);
shadow.appendChild(div);
}
}
customElements.define('hello-world', HelloWorld);
</script>
</body>
</html>
这样就可以看到我们想要的效果啦😄!
有些小伙伴可能会有疑问,上面代码中const shadow=this.attachShadow({mode: 'open'});是什么意思?为什么mode的属性为open,而不是close呢?同时为什么要把创建的div标签和style标签通过appendChild的方式添加到shadow上?
下面我来一一解答!
1: const shadow=this.attachShadow({mode: 'open'});是什么意思?
我们先来看一张图:
Shadow DOM 是一个 WEB 标准,它可以将隐藏的 DOM 树附加到常规 DOM 之中。用一个通俗的例子来理解 Shadow DOM:
Shadow DOM 就像一座楼房,楼房有公共区域和私人房间。公共区域是常规 DOM,任何人都可以访问,你的样式、行为都可能受外界影响。而私人房间就是 Shadow DOM 了,只有房间主人才能访问。在这个房间里:
- 你可以装饰你自己的家具(定义自己的 DOM 树),外面不能看到。
- 你可以把窗帘拉上(定义作用域仅内部的样式),外面的人看不到里面。
- 你在房间里跳舞,外面的人不会被影响(DOM 和样式都被封装在 Shadow DOM 内)。
- 但是房主仍然需要向房东交租(Shadow DOM 仍需要主 DOM 来挂载)。
这里,有一些 Shadow DOM 特有的术语需要我们了解:
- Shadow host:一个常规 DOM 节点,Shadow DOM 会被附加到这个节点上。
- Shadow tree:Shadow DOM 内部的 DOM 树。
- Shadow boundary:Shadow DOM 结束的地方,也是常规 DOM 开始的地方。
- Shadow root: Shadow tree 的根节点。
掌握了这些概念,我们再回到刚才的问题
ele.attachShadow方法来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode 属性,值可以是 open 或者 closed:
const shadow=this.attachShadow({mode: 'open'});
const shadow=this.attachShadow({mode: 'closed'});
我们这里是将shadow root添加到了this身上,this指的就是这个类的实例对象
当然你也可以将shadow root添加到任何dom元素上,比如我们添加到一个类名为content的div上
open 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM.
例如可以通过let myShadowDom = myCustomElem.shadowRoot;来获取元素内部的shadow root 。如果你将一个 Shadow root 附加到一个 Custom element(自定义元素,比如<hello-world>) 上,并且将 mode 设置为 closed,那么就不可以从外部获取 Shadow DOM 了——myCustomElem.shadowRoot 将会返回 null。
相信讲到这,上面的问题应该都迎刃而解了。
3.生命周期
定义在自定义元素的类定义中的特殊回调函数,影响其行为:
connectedCallback:当自定义元素第一次被连接到文档 DOM 时被调用。disconnectedCallback:当自定义元素与文档 DOM 断开连接时被调用。adoptedCallback:当自定义元素被移动到新文档时被调用。attributeChangedCallback:当自定义元素的一个属性被增加、移除或更改时被调用。
connectedCallback
使用:
我们接着延续之前的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.hello {
color: red;
font-size: 32px;
}
</style>
</head>
<body>
<hello-world></hello-world>
<script>
class HelloWorld extends HTMLElement {
constructor() {
super();
console.log('constructor');
const shadow = this.attachShadow({ mode: 'open' });
const div = document.createElement('div');
div.innerHTML = 'hello world';
const style = document.createElement('style');
style.innerHTML = `
.hello{
color: red;
font-size: 32px;
}
`
div.className = 'hello';
shadow.appendChild(style);
shadow.appendChild(div);
}
connectedCallback() {
console.log('connected,被添加到dom tree上')
}
}
customElements.define('hello-world', HelloWorld);
</script>
</body>
</html>
disconnectedCallback
这个生命周期是当自定义元素与文档 DOM 断开连接时被调用,也就是自定义元素从文档DOM移除的时候调用。
接下来我们添加一个button按钮,当他被点击的时候,移除自定义元素
<!DOCTYPE html>
<html lang="en">
<head>
.....
</head>
<body>
<button class="removeBtn">移除自定义元素</button>
<hello-world></hello-world>
<script>
const removeBtn=document.querySelector('.removeBtn');
const helloWorld=document.querySelector('hello-world');
removeBtn.addEventListener('click',function(){
helloWorld.parentNode.removeChild(helloWorld);
})
class HelloWorld extends HTMLElement {
constructor() {
//.....
}
connectedCallback() {
console.log('connected,被添加到dom tree上')
}
disconnectedCallback() {
console.log('disconnected,从dom tree上移除')
}
}
customElements.define('hello-world', HelloWorld);
</script>
</body>
</html>
点击button按钮,便移除了自定义元素
attributeChangedCallback生命周期。
当自定义元素的一个属性被增加、移除或更改时被调用。
下面代码中我们给<hello-world>这个自定义标签添加了一个color属性,并添加了attributeChangedCallback生命周期。
<!DOCTYPE html>
<html lang="en">
<head>
<!--.....-->
</head>
<body>
<button class="removeBtn">移除自定义元素</button>
<hello-world color='orange'></hello-world>
<script>
const removeBtn=document.querySelector('.removeBtn');
const helloWorld=document.querySelector('hello-world');
removeBtn.addEventListener('click',function(){
helloWorld.parentNode.removeChild(helloWorld);
})
class HelloWorld extends HTMLElement {
constructor() {
//.....
}
attributeChangedCallback(name, oldVal, newVal) {
console.log('attribute,改变')
if (name === 'color') {
//因为我们是这个shadow dom内部的div,如果你使用document.querySelector('.hello');的方式去获取是获取不到的
const div = this.shadowRoot.querySelector('.hello');
div.style.color = newVal; //这里我们将字体颜色改为了橙色
}
}
connectedCallback() {
console.log('connected,被添加到dom tree上')
}
disconnectedCallback() {
console.log('disconnected,从dom tree上移除')
}
}
customElements.define('hello-world', HelloWorld);
</script>
</body>
</html>
上述代码你运行之后,你会发现并没有打印console.log('attribute,改变'),这是因为我们缺少了监听,
添加如下代码
static get observedAttributes() {
return [
'color',
]
}
这是类的静态属性,用于监听自定义元素哪些属性的变化,他返回一个数组,若监听多个属性,直接写在数组内返回即可。
adoptedCallback
当自定义元素被移动到新文档时被调用。
这里具体的含义就是说,当自定义元素从一个页面被移动到另一个页面时触发,那么如何将自定义元素移动到另一个页面呢?我们可以通过iframe来简单模拟实现。
这里我们新建一个html文件,iframe.html
<!-- iframe.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>
然后我们在页面中通过iframe的方式引入,并添加一个button,并添加adoptedCallback该生命周期函数,完整代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<!-- <meta charset="UTF-8"> -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.hello {
color: red;
font-size: 32px;
}
</style>
</head>
<body>
<button class="removeBtn">移除自定义元素</button>
<button class="moveBtn">移动到新页面</button>
<hello-world color="orange"></hello-world>
<iframe src="./demo.html" width="300px" height="300px"></iframe>
<script>
const removeBtn=document.querySelector('.removeBtn');
const helloWorld=document.querySelector('hello-world');
const moveBtn=document.querySelector('.moveBtn')
moveBtn.addEventListener('click',function(){
const iframe=document.querySelector('iframe');
iframe.contentWindow.document.body.appendChild(helloWorld)
})
removeBtn.addEventListener('click',function(){
helloWorld.parentNode.removeChild(helloWorld);
})
class HelloWorld extends HTMLElement {
constructor() {
super();
console.log('constructor');
const shadow = this.attachShadow({ mode: 'open' });
const div = document.createElement('div');
div.innerHTML = 'hello world';
const style = document.createElement('style');
style.innerHTML = `
.hello{
color: red;
font-size: 32px;
}
`
div.className = 'hello';
shadow.appendChild(style);
shadow.appendChild(div);
}
adoptedCallback() { //注意这个生命周期不是在dom间移动触发,而是在不同的page间移动被触发
console.log('adopted,被移动到不同的page')
}
static get observedAttributes() {
return [
'color',
]
}
attributeChangedCallback(name, oldVal, newVal) {
console.log('attribute,改变')
if (name === 'color') {
//因为我们是这个shadow dom内部的div,如果你使用document.querySelector('.hello');的方式去获取是获取不到的
const div = this.shadowRoot.querySelector('.hello');
div.style.color = newVal; //这里我们将字体颜色改为了橙色
}
}
connectedCallback() {
console.log('connected,被添加到dom tree上')
}
disconnectedCallback() {
console.log('disconnected,从dom tree上移除')
}
}
customElements.define('hello-world', HelloWorld);
</script>
</body>
</html>
4.扩展
自定义元素:
主要分为:独立的元素,继承基本的html元素的元素
1.独立元素:
它不继承其他内建的 HTML 元素。你可以直接把它们写成 HTML 标签的形式,来在页面上使用。
比如我们上面定义的<hello-world>元素
例如 <popup-info>,或者是document.createElement("popup-info")这样。
class PopupInfo extends HTMLElement {}
2:继承元素:
在创建时,你必须指定所需扩展的元素(正如上面例子所示),使用时,需要先写出基本的元素标签,并通过 is 属性指定 custom element(自定义元素) 的名称。例如<p is="word-count">, 或者
document.createElement("p", { is: "word-count" })。
class PopupInfo extends HTMLUListElement {}
这表示继承自ul标签
template和slot
到这里我们可能会想,如果我这个自定义标签(<hello-world>)里面不止一个div
或者还有其它html标签,我们难道都需要通过document.createElemen来创建吗
答案肯定不是的。到这里就需要引出template了。
<template> 和 <slot> 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为
自定义元素结构的基础被多次重用。
这句话的意思就是,你在body里面写的template标签是不会被渲染到页面上的,它仅仅会作为
我们自定义元素的一个模板而已。
那么如何使用呢?我们还是实现上面的效果,只不过我们不自己创建div了
<template>
<div class="hello">hello component</div>
</template>
class HelloWorld extends HTMLElement{
constructor() {
super();
const shadow=this.attachShadow({mode:'open'})
const temolateDom=document.getElementById('hello')
const cloneContent=temolateDom.content.cloneContent(true)
shadow.appendChild(cloneContent)
const style=document.createElement('style');
style.innerHTML=`
.hello{
color: red;
font-size: 32px;
}
`
div.className = 'hello';
shadow.appendChild(style);
}
}
看到这里是不是觉得跟vue很像,没错,我也这么觉得。接下来的slot你会觉得更像
尽管到这一步已经挺好了,但是元素仍旧不是很灵活。我们只能在里面放一点文本,甚至没有普通的 p 标签管用!我们使用 <slot> 让它能在单个实例中通过声明式的语法展示不同的文本。浏览器对这个功能的支持比<template>少,在Chrome 53, Opera 40, Safari 10,火狐 59 和 Edge 79 中支持。
slot有一个name属性,提供一个唯一标识。它相当于一个占位符。当你在标签中使用到它时,该占位符(slot)可以填充所需的任何 HTML 标记片段。
<!DOCTYPE html>
<html lang="en">
<body>
<hello-world>
<div slot="name-slot">呜呜呜</div>
</hello-world>
<template id="hello">
<div class="hello">hello component</div>
<slot name="name-slot">default content</slot>
</template>
<script>
class HelloWorld extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' })
const temolateDom = document.getElementById('hello')
const cloneContent = temolateDom.content.cloneNode(true)
shadow.appendChild(cloneContent)
const style = document.createElement('style');
style.innerHTML = `
.hello{
color: red;
font-size: 32px;
}
`
shadow.appendChild(style);
}
}
customElements.define('hello-world', HelloWorld);
</script>
</body>
</html>
我们在使用自定义元素的时候,通过<div slot="name-slot">呜呜呜</div>,slot属性来指定我们使用template中的哪个slot,如果没有使用slot,那么slot默认会展示default content
总结:
1:template:html新标签,可重用,不会渲染到dom tree上,被用于定义webcomponent的html结构:
2:slot: html新标签,灵活动态的为webcomponent插入child,基本被限定在template中使用
总结
以上就是我近期对于web component的理解,这篇文章主要讲述web component的基本使用和基本概念,后续还会继续更新webcomponent在框架和微前端中如何使用,以及web component的综合案例。如有不对的地方,恳请批评指正。