前言
至于为什么写一篇Web Components
的文章,原因大致是因为,目前我在做的组件库项目,一直有一些小伙伴在群里说这个东西,包括文章下面的评论也会出现建议
于是我就去了解并学习了一下,然后发现此文的文章较少,所以花费一些时间写这篇文章供大家了解学习
我也是一个初学者,新手,如果文章有什么不对的地方欢迎大家进行指出以及建议!
然后我通过Web Components的方式已经写了一些组件,我对Web Components
部分会单开专栏供大家学习
那么我们开始吧~
什么是Web Components
其实Web Components
在MDN
上已经有了定义Web Component – Web API 接口参考 | MDN (mozilla.org)
MDN给出的概念是Web Component 是一套不同的技术,允许你创建可重用的定制元素(它们的功能封装在你的代码之外)并且在你的 web 应用中使用它们。
是的,Web Components
并不是第三方的库,而是浏览器原生
支持的。
简单拿我们的组件库举例,我们知道常见的三大框架Vue
、React
、Angular
,都有属于自己的组件库,那么我们知道,大部分的组件库是没法跨框架进行使用的(当然,你可以做一些框架支持,例如在Angular
中去书写Vue
,但是这毕竟不是我们讨论的这个点)。
那么,我们想要实现这种跨端
,我们可以采用Web Components
来实现,也就是用Web Components
写的组件库,在哪里都可以用。
也可能不是组件库,我们只有几个组件想进行跨端使用,那么我们都可以去使用Web Components
,其实重点就是,在这个组件化
的时代,Web Components想进行统一的操作
Web Components的组成
三大组成部分:
Custom Elements(自定义元素)
:允许开发者定义自定义HTML
元素,并定义其行为和样式。Shadow DOM(影子 DOM)
:允许开发者将一个元素的样式和行为封装在一个隔离的作用域
内,以防止与其他元素的样式或行为冲突。HTML Templates(HTML 模板)
:允许开发者定义一个带有占位符
(slot
,没错,就是类似于Vue
中的插槽
)的HTML 模板
,然后在需要时使用JavaScript
动态地填充模板。
单看定义是很枯燥乏味的,我们还是通过例子来进行讲解
Web Components实战
我们就写一个最简单的Button
,当然,最后我的这个Button
组件,可能会是任何名字,也可能会有不同的功能,这也是我们组件化的意义
我们先看一下效果
关于怎么写呢,大家一定要多看文档,文档是很详细的,首先
我们定义出模板
其实我这里的写法顺序和Vue
是很类似的,当然,大家写越多的Web Components
就会发现,它和Vue
是很相似的,这是因为Vue
很多地方就是借鉴的Web Components
OK,我们模板大概是这种效果,这里我没用innerHTML
的写法,大概是我还是Vue
写习惯了哈哈哈
<template id="s-button-template"><style>/* 按钮样式 */button {display: inline-block;padding: 10px 20px;font-size: 16px;font-weight: bold;color: #fff;background-color: #007bff;border: none;border-radius: 5px;cursor: pointer;}/* 按钮悬停样式 */button:hover {background-color: #0062cc;}/* 按钮按下样式 */button:active {background-color: #005cbf;}</style><button><slot></slot></button></template><template id="s-button-template"> <style> /* 按钮样式 */ button { display: inline-block; padding: 10px 20px; font-size: 16px; font-weight: bold; color: #fff; background-color: #007bff; border: none; border-radius: 5px; cursor: pointer; } /* 按钮悬停样式 */ button:hover { background-color: #0062cc; } /* 按钮按下样式 */ button:active { background-color: #005cbf; } </style> <button> <slot></slot> </button> </template><template id="s-button-template"> <style> /* 按钮样式 */ button { display: inline-block; padding: 10px 20px; font-size: 16px; font-weight: bold; color: #fff; background-color: #007bff; border: none; border-radius: 5px; cursor: pointer; } /* 按钮悬停样式 */ button:hover { background-color: #0062cc; } /* 按钮按下样式 */ button:active { background-color: #005cbf; } </style> <button> <slot></slot> </button> </template>
这里的样式写法有很多哈,我们采用官网最开始给的这种简单的写法
然后slot
我这里不过多解释了,跟Vue
的一个道理
好的,我们的模板完事了,我们该通过Js
的Class
的方式来创建 s-button
这个自定义元素
了
我们把代码放出来,然后逐行去解释
// 创建 s-button 自定义元素class SButton extends HTMLElement {constructor() {super();// 从模板中获取样式和内容const template = document.getElementById('s-button-template');const templateContent = template.content;// 创建 Shadow DOMconst shadowRoot = this.attachShadow({ mode: 'open' });// 将模板内容复制到 Shadow DOM 中shadowRoot.appendChild(templateContent.cloneNode(true));}}// 定义 s-button 元素customElements.define('s-button', SButton);// 创建 s-button 自定义元素 class SButton extends HTMLElement { constructor() { super(); // 从模板中获取样式和内容 const template = document.getElementById('s-button-template'); const templateContent = template.content; // 创建 Shadow DOM const shadowRoot = this.attachShadow({ mode: 'open' }); // 将模板内容复制到 Shadow DOM 中 shadowRoot.appendChild(templateContent.cloneNode(true)); } } // 定义 s-button 元素 customElements.define('s-button', SButton);// 创建 s-button 自定义元素 class SButton extends HTMLElement { constructor() { super(); // 从模板中获取样式和内容 const template = document.getElementById('s-button-template'); const templateContent = template.content; // 创建 Shadow DOM const shadowRoot = this.attachShadow({ mode: 'open' }); // 将模板内容复制到 Shadow DOM 中 shadowRoot.appendChild(templateContent.cloneNode(true)); } } // 定义 s-button 元素 customElements.define('s-button', SButton);
然后我们去使用s-button
即可
<s-button>Hello, World!</s-button><s-button>Hello, World!</s-button><s-button>Hello, World!</s-button>
总结
我们总结一下我们都做了什么
- 我们首先定义了一个模板
s-button-template
,它包含了一个简单的样式和一个按钮元素。 - 然后,我们使用 JavaScript 创建了一个名为
SButton
的自定义元素,并在其中使用 Shadow DOM 将模板内容复制到自定义元素中。 - 最后,我们使用
customElements.define
方法将自定义元素注册为s-button
元素 - 这样就可以在 HTML 中使用
<s-button>
标签来创建s-button
组件了。
可能大家还是不太懂这个Shadow DOM
是干什么的,我单独解释一下
Shadow DOM
起到了封装样式和行为
的作用。具体来说,Shadow DOM
使得s-button
组件的样式和行为可以被封装在一个独立的作用域内
,不会影响到外部
页面的样式和行为。
你如果不明白,那么如果你开发过Vue
的话,你肯定知道scoped
,是的,是类似的
只不过实现方式是不同的
Shadow DOM
使用了一种更加底层的方式来实现作用域限定
,而Vue
中的scoped CSS
则是通过在样式选择器
中添加特定的属性选择器
来实现的。不过,它们都可以达到相同的效果
计数器
我们可以封装一个计数器组件,其实很简单,就是button
的基础上,加上一块计数的区域
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Web Components</title></head><body><s-button>Add</s-button><template id="s-button-template"><style>/* 按钮样式 */button {display: inline-block;padding: 10px 20px;font-size: 16px;font-weight: bold;color: #fff;background-color: #007bff;border: none;border-radius: 5px;cursor: pointer;}/* 按钮悬停样式 */button:hover {background-color: #0062cc;}/* 按钮按下样式 */button:active {background-color: #005cbf;}/* 计数器样式 */.counter {margin-top: 10px;font-size: 14px;color: #999;}</style><button><slot></slot></button><div class="counter">Counter: 0</div></template><script>// 创建 s-button 自定义元素class SButton extends HTMLElement {constructor() {super();// 从模板中获取样式和内容const template = document.getElementById('s-button-template');const templateContent = template.content;// 创建 Shadow DOMconst shadowRoot = this.attachShadow({ mode: 'open' });// 将模板内容复制到 Shadow DOM 中shadowRoot.appendChild(templateContent.cloneNode(true));// 获取计数器元素this.counterElement = shadowRoot.querySelector('.counter');// 初始化计数器值为0this.counter = 0;// 添加点击事件处理函数this.shadowRoot.querySelector('button').addEventListener('click', () => {this.incrementCounter();});}// 增加计数器值并更新显示incrementCounter() {this.counter++;this.updateCounter();}// 更新计数器显示updateCounter() {this.counterElement.textContent = `Counter: ${this.counter}`;}}// 定义 s-button 元素customElements.define('s-button', SButton);</script></body></html><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Components</title> </head> <body> <s-button>Add</s-button> <template id="s-button-template"> <style> /* 按钮样式 */ button { display: inline-block; padding: 10px 20px; font-size: 16px; font-weight: bold; color: #fff; background-color: #007bff; border: none; border-radius: 5px; cursor: pointer; } /* 按钮悬停样式 */ button:hover { background-color: #0062cc; } /* 按钮按下样式 */ button:active { background-color: #005cbf; } /* 计数器样式 */ .counter { margin-top: 10px; font-size: 14px; color: #999; } </style> <button> <slot></slot> </button> <div class="counter">Counter: 0</div> </template> <script> // 创建 s-button 自定义元素 class SButton extends HTMLElement { constructor() { super(); // 从模板中获取样式和内容 const template = document.getElementById('s-button-template'); const templateContent = template.content; // 创建 Shadow DOM const shadowRoot = this.attachShadow({ mode: 'open' }); // 将模板内容复制到 Shadow DOM 中 shadowRoot.appendChild(templateContent.cloneNode(true)); // 获取计数器元素 this.counterElement = shadowRoot.querySelector('.counter'); // 初始化计数器值为0 this.counter = 0; // 添加点击事件处理函数 this.shadowRoot.querySelector('button').addEventListener('click', () => { this.incrementCounter(); }); } // 增加计数器值并更新显示 incrementCounter() { this.counter++; this.updateCounter(); } // 更新计数器显示 updateCounter() { this.counterElement.textContent = `Counter: ${this.counter}`; } } // 定义 s-button 元素 customElements.define('s-button', SButton); </script> </body> </html><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Components</title> </head> <body> <s-button>Add</s-button> <template id="s-button-template"> <style> /* 按钮样式 */ button { display: inline-block; padding: 10px 20px; font-size: 16px; font-weight: bold; color: #fff; background-color: #007bff; border: none; border-radius: 5px; cursor: pointer; } /* 按钮悬停样式 */ button:hover { background-color: #0062cc; } /* 按钮按下样式 */ button:active { background-color: #005cbf; } /* 计数器样式 */ .counter { margin-top: 10px; font-size: 14px; color: #999; } </style> <button> <slot></slot> </button> <div class="counter">Counter: 0</div> </template> <script> // 创建 s-button 自定义元素 class SButton extends HTMLElement { constructor() { super(); // 从模板中获取样式和内容 const template = document.getElementById('s-button-template'); const templateContent = template.content; // 创建 Shadow DOM const shadowRoot = this.attachShadow({ mode: 'open' }); // 将模板内容复制到 Shadow DOM 中 shadowRoot.appendChild(templateContent.cloneNode(true)); // 获取计数器元素 this.counterElement = shadowRoot.querySelector('.counter'); // 初始化计数器值为0 this.counter = 0; // 添加点击事件处理函数 this.shadowRoot.querySelector('button').addEventListener('click', () => { this.incrementCounter(); }); } // 增加计数器值并更新显示 incrementCounter() { this.counter++; this.updateCounter(); } // 更新计数器显示 updateCounter() { this.counterElement.textContent = `Counter: ${this.counter}`; } } // 定义 s-button 元素 customElements.define('s-button', SButton); </script> </body> </html>
这里就是意思说,我们可以通过正常的事件的方式来给组件加功能
然后我们就会得到对应的效果
Web Components生命周期
connectedCallback()
:在元素被插入到文档中时调用,可以进行一些初始化操作,比如添加事件监听器或从外部资源加载数据。disconnectedCallback()
:在元素从文档中移除时调用,可以进行一些清理操作,比如移除事件监听器或取消正在进行的网络请求。attributeChangedCallback(attributeName, oldValue, newValue)
:当元素的属性被添加、移除、更新或替换时调用,可以对属性的变化作出响应。adoptedCallback()
:当元素从一个文档转移到另一个文档(例如通过document.importNode()
方法)时被调用。
我们可以通过上面的Button
的例子,来应用一下生命周期
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Web Components</title></head><body><s-button>Add</s-button><template id="s-button-template"><style>/* 按钮样式 */button {display: inline-block;padding: 10px 20px;font-size: 16px;font-weight: bold;color: #fff;background-color: #007bff;border: none;border-radius: 5px;cursor: pointer;}/* 按钮悬停样式 */button:hover {background-color: #0062cc;}/* 按钮按下样式 */button:active {background-color: #005cbf;}/* 计数器样式 */.counter {margin-top: 10px;font-size: 14px;color: #999;}</style><button><slot></slot></button><div class="counter">Counter: 0</div></template><script>// 创建 s-button 自定义元素class SButton extends HTMLElement {constructor() {super();// 创建 Shadow DOMconst shadowRoot = this.attachShadow({ mode: 'open' });// 从模板中获取样式和内容const template = document.getElementById('s-button-template');const templateContent = template.content;// 将模板内容复制到 Shadow DOM 中shadowRoot.appendChild(templateContent.cloneNode(true));// 获取计数器元素this.counterElement = shadowRoot.querySelector('.counter');// 初始化计数器值为0this.counter = 0;// 添加点击事件处理函数this.shadowRoot.querySelector('button').addEventListener('click', () => {this.incrementCounter();});}// 元素被插入到文档中时调用connectedCallback() {console.log('SButton connected to the DOM');}// 元素从文档中移除时调用disconnectedCallback() {console.log('SButton removed from the DOM');}// 元素的属性值发生变化时调用attributeChangedCallback(name, oldValue, newValue) {console.log(`SButton attribute "${name}" changed from "${oldValue}" to "${newValue}"`);}// 监听的属性列表static get observedAttributes() {return ['disabled'];}// 增加计数器值并更新显示incrementCounter() {this.counter++;this.updateCounter();}// 更新计数器显示updateCounter() {this.counterElement.textContent = `Counter: ${this.counter}`;}}// 定义 s-button 元素customElements.define('s-button', SButton);</script></body></html><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Components</title> </head> <body> <s-button>Add</s-button> <template id="s-button-template"> <style> /* 按钮样式 */ button { display: inline-block; padding: 10px 20px; font-size: 16px; font-weight: bold; color: #fff; background-color: #007bff; border: none; border-radius: 5px; cursor: pointer; } /* 按钮悬停样式 */ button:hover { background-color: #0062cc; } /* 按钮按下样式 */ button:active { background-color: #005cbf; } /* 计数器样式 */ .counter { margin-top: 10px; font-size: 14px; color: #999; } </style> <button> <slot></slot> </button> <div class="counter">Counter: 0</div> </template> <script> // 创建 s-button 自定义元素 class SButton extends HTMLElement { constructor() { super(); // 创建 Shadow DOM const shadowRoot = this.attachShadow({ mode: 'open' }); // 从模板中获取样式和内容 const template = document.getElementById('s-button-template'); const templateContent = template.content; // 将模板内容复制到 Shadow DOM 中 shadowRoot.appendChild(templateContent.cloneNode(true)); // 获取计数器元素 this.counterElement = shadowRoot.querySelector('.counter'); // 初始化计数器值为0 this.counter = 0; // 添加点击事件处理函数 this.shadowRoot.querySelector('button').addEventListener('click', () => { this.incrementCounter(); }); } // 元素被插入到文档中时调用 connectedCallback() { console.log('SButton connected to the DOM'); } // 元素从文档中移除时调用 disconnectedCallback() { console.log('SButton removed from the DOM'); } // 元素的属性值发生变化时调用 attributeChangedCallback(name, oldValue, newValue) { console.log(`SButton attribute "${name}" changed from "${oldValue}" to "${newValue}"`); } // 监听的属性列表 static get observedAttributes() { return ['disabled']; } // 增加计数器值并更新显示 incrementCounter() { this.counter++; this.updateCounter(); } // 更新计数器显示 updateCounter() { this.counterElement.textContent = `Counter: ${this.counter}`; } } // 定义 s-button 元素 customElements.define('s-button', SButton); </script> </body> </html><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Components</title> </head> <body> <s-button>Add</s-button> <template id="s-button-template"> <style> /* 按钮样式 */ button { display: inline-block; padding: 10px 20px; font-size: 16px; font-weight: bold; color: #fff; background-color: #007bff; border: none; border-radius: 5px; cursor: pointer; } /* 按钮悬停样式 */ button:hover { background-color: #0062cc; } /* 按钮按下样式 */ button:active { background-color: #005cbf; } /* 计数器样式 */ .counter { margin-top: 10px; font-size: 14px; color: #999; } </style> <button> <slot></slot> </button> <div class="counter">Counter: 0</div> </template> <script> // 创建 s-button 自定义元素 class SButton extends HTMLElement { constructor() { super(); // 创建 Shadow DOM const shadowRoot = this.attachShadow({ mode: 'open' }); // 从模板中获取样式和内容 const template = document.getElementById('s-button-template'); const templateContent = template.content; // 将模板内容复制到 Shadow DOM 中 shadowRoot.appendChild(templateContent.cloneNode(true)); // 获取计数器元素 this.counterElement = shadowRoot.querySelector('.counter'); // 初始化计数器值为0 this.counter = 0; // 添加点击事件处理函数 this.shadowRoot.querySelector('button').addEventListener('click', () => { this.incrementCounter(); }); } // 元素被插入到文档中时调用 connectedCallback() { console.log('SButton connected to the DOM'); } // 元素从文档中移除时调用 disconnectedCallback() { console.log('SButton removed from the DOM'); } // 元素的属性值发生变化时调用 attributeChangedCallback(name, oldValue, newValue) { console.log(`SButton attribute "${name}" changed from "${oldValue}" to "${newValue}"`); } // 监听的属性列表 static get observedAttributes() { return ['disabled']; } // 增加计数器值并更新显示 incrementCounter() { this.counter++; this.updateCounter(); } // 更新计数器显示 updateCounter() { this.counterElement.textContent = `Counter: ${this.counter}`; } } // 定义 s-button 元素 customElements.define('s-button', SButton); </script> </body> </html>
具体来说,我们在 SButton
类中实现了以下生命周期方法:
connectedCallback()
: 当组件被插入到文档中时调用。在这个例子中,我们在这个方法中使用console.log()
打印了一条消息,以显示组件被添加到 DOM 中的事件。disconnectedCallback()
: 当组件从文档中移除时调用。在这个例子中,我们在这个方法中使用console.log()
打印了一条消息,以显示组件被从 DOM 中移除的事件。attributeChangedCallback(name, oldValue, newValue)
: 当组件所监听的属性值发生变化时调用。在这个例子中,我们监听了disabled
属性,并在这个方法中使用console.log()
打印了一条消息,以显示该属性值的变化。
此外,我们还定义了一个 observedAttributes
静态方法,它返回一个数组,用于指定组件需要监听的属性列表。在这个例子中,我们监听了 disabled
属性。
Web Components的样式
除了上面我们说过的Shadow DOM
可以将组件的样式和行为可以被封装在一个独立的作用域内
之外,书写Web Components
的样式会使用var() 函数
来引用 CSS 变量
还是拿我们的Button
举例,把我们的样式通过css变量来进行一些改造
<template id="s-button-template"><style>/* 定义 CSS 变量 */:host {--button-bg-color: #007bff;--button-text-color: #fff;--button-border: none;}/* 按钮样式 */button {display: inline-block;padding: 10px 20px;font-size: 16px;font-weight: bold;color: var(--button-text-color); /* 使用 CSS 变量 */background-color: var(--button-bg-color); /* 使用 CSS 变量 */border: var(--button-border); /* 使用 CSS 变量 */border-radius: 5px;cursor: pointer;}/* 按钮悬停样式 */button:hover {background-color: #0062cc;}/* 按钮按下样式 */button:active {background-color: #005cbf;}/* 计数器样式 */.counter {margin-top: 10px;font-size: 14px;color: #999;}</style><button><slot></slot></button><div class="counter">Counter: 0</div></template><template id="s-button-template"> <style> /* 定义 CSS 变量 */ :host { --button-bg-color: #007bff; --button-text-color: #fff; --button-border: none; } /* 按钮样式 */ button { display: inline-block; padding: 10px 20px; font-size: 16px; font-weight: bold; color: var(--button-text-color); /* 使用 CSS 变量 */ background-color: var(--button-bg-color); /* 使用 CSS 变量 */ border: var(--button-border); /* 使用 CSS 变量 */ border-radius: 5px; cursor: pointer; } /* 按钮悬停样式 */ button:hover { background-color: #0062cc; } /* 按钮按下样式 */ button:active { background-color: #005cbf; } /* 计数器样式 */ .counter { margin-top: 10px; font-size: 14px; color: #999; } </style> <button> <slot></slot> </button> <div class="counter">Counter: 0</div> </template><template id="s-button-template"> <style> /* 定义 CSS 变量 */ :host { --button-bg-color: #007bff; --button-text-color: #fff; --button-border: none; } /* 按钮样式 */ button { display: inline-block; padding: 10px 20px; font-size: 16px; font-weight: bold; color: var(--button-text-color); /* 使用 CSS 变量 */ background-color: var(--button-bg-color); /* 使用 CSS 变量 */ border: var(--button-border); /* 使用 CSS 变量 */ border-radius: 5px; cursor: pointer; } /* 按钮悬停样式 */ button:hover { background-color: #0062cc; } /* 按钮按下样式 */ button:active { background-color: #005cbf; } /* 计数器样式 */ .counter { margin-top: 10px; font-size: 14px; color: #999; } </style> <button> <slot></slot> </button> <div class="counter">Counter: 0</div> </template>
Lit
Lit
是一个基于Web Components
标准的JavaScript 库
,它提供了一些工具和功能,用于简化
Web Components 的开发。
很简单,Web Components
和Lit
的关系,我觉得就像JavaScript
和Jquery
我们可以通过Lit
进行代码的简化
// 定义组件的属性和默认值static properties = {counter: { type: Number },};constructor() {super();this.counter = 0;}// 渲染组件的模板render() {return html`<button @click=${this.incrementCounter}><slot></slot></button><div class="counter">Counter: ${this.counter}</div>`;}// 增加计数器值并更新显示incrementCounter() {this.counter++;this.requestUpdate();}// 定义组件的属性和默认值 static properties = { counter: { type: Number }, }; constructor() { super(); this.counter = 0; } // 渲染组件的模板 render() { return html` <button @click=${this.incrementCounter}> <slot></slot> </button> <div class="counter">Counter: ${this.counter}</div> `; } // 增加计数器值并更新显示 incrementCounter() { this.counter++; this.requestUpdate(); }// 定义组件的属性和默认值 static properties = { counter: { type: Number }, }; constructor() { super(); this.counter = 0; } // 渲染组件的模板 render() { return html` <button @click=${this.incrementCounter}> <slot></slot> </button> <div class="counter">Counter: ${this.counter}</div> `; } // 增加计数器值并更新显示 incrementCounter() { this.counter++; this.requestUpdate(); }
这里
- 我们通过
@event
装饰器来监听Web Components 的事件
- 使用了
Lit
提供的属性定义语法,定义了counter
属性的类型为Number
- 在组件的
render
方法中,我们使用了 Lit 的模板渲染语法,使用html
模板函数来定义组件的模板。在模板中,我们使用了${}
语法来将counter
属性的值绑定到计数器显示的文本中,从而实现了数据绑定
当然Lit
还有很多别的功能,这里就是介绍一下这个库
尾语
这篇只是介绍一下Web Components
,我们并没有进行实战
我觉得这部分知识点是蛮有趣的,所以我应该会单开专栏来讲,同时我们的组件库项目
,我也在研究如何更好地应用Web Components
在我们的组件库中
当然,现在已经有很多组件库开始应用Web Components
了,也欢迎大家去了解
我觉得Web Components
我们可以作为一个知识点去学习,而不是去研究,vue、react
等框架是否被淘汰的问题,希望大家仁者见仁智者见智~
当然,欢迎大家提出意见,以及补充~