么是Web Components
Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。
背景
组件:最初的目的是代码重用,功能相对单一或者独立。在整个系统的代码层次上位于最底层,被其他代码所依赖,所以说组件化是纵向分层。
使用方法
1. 创建一个类或函数来指定web组件的功能,推荐请使用 ECMAScript 2015 的类语法。
2. 使用 CustomElementRegistry.define() 方法注册自己的新自定义元素 ,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素。
3. 如果需要的话,使用Element.attachShadow() 方法将一个shadow DOM附加到自定义元素上。使用原生的DOM方法向shadow DOM中添加子元素、事件监听器等。
4. 如果需要的话,使用 <template>和<slot> 定义一个HTML模板。再次使用常规DOM方法克隆模板并将其附加到shadow DOM中。
5. 最后在页面中使用我们的自定义元素,就像使用原生HTML元素一样
写一个Web Components组件
1. 预期效果:预期渲染一个这样的自定义品牌名片到页面上面
只要开发者在页面上调用了<company-card></company-card>即可渲染页面,根据规范,自定义元素的名称必须包含连词线,用与区别原生的 HTML 元素
2. 自定义的元素需要使用js类来创建,页面中所有的自定义元素都会是这个类的实例:
然后就可以实现基础效果:
然后我们的js直接取出自定义元素上面的属性赋值给对应的标签即可
3.使用template来创建元素
然后我的js直接取出自定义元素上面的属性赋值给对应的标签即可。
4. 加入样式
组件的样式需要加入到template组件里面,为每个组件独享的样式,跟vue思路差不多最终的template可能是这样的
5. 最终实现效果
生命周期函数 在custom element的构造函数中,可以指定多个不同的回调函数,它们将会在元素的不同生命时期被调用
不得不承认,Web Components 标准的提出解决了一些问题,必须交由浏览器去处理的是 Shadow DOM,在没有Shadow DOM 的浏览器上实现代码隔离的方式多多少少有缺陷。个人我觉得组件化的各个 API 不够简洁易用,依旧有 getElementById 这些的味道,但是交由各个类库去简化也可以接受,而 import 功能上没问题,但是加载多个
组件时性能问题还是值得商榷,标准可能需要在这个方面提供更多给浏览器的指引,例如是否有可能提供一种单一请求加载多个组件 HTML 的方式等。
在现在的移动化趋势中,Web Components 不仅仅是 Web 端的问题,越来越多的开发者期望以 Web 的方式去实现移动应用,而多端复用的实现渐渐是以组件的形式铺开,例如 React Native 和 Weex。所以 Web Components 的标准可能会影响到多端开发 Web 化的一个模式和发展。
但是,对于Web Components的发展前景还是比较看好,生产环境下还是观望一下就好。
络技术的快速发展,带来了层出不穷的新概念和框架,尤其是在前端开发领域,新技术的出现如同浪潮般一波接一波,例如 Vue3 和 Vite 的组合。而在这种技术快速更新的环境中,Web Components 作为一项已经存在一段时间的技术,为什么如今值得我们抓紧时间,去深入学习和探讨呢?
Web Components 是由 W3C 推动的标准化技术。如今,它得到了包括 Chrome、Firefox、Safari 和 Edge 在内的主流浏览器的广泛支持。不仅 「Vue3」 的更新就包括了对 Web Components 的原生支持,现在也出现了很多由Web Components封装的「组件」和「库」,尤其是现在「面试」也成为了常问的话题,其中更为频频出现的是 Shadow DOM。
这项技术的魅力在于,「它允许开发者创建自定义、可重用的元素,这些元素可以在任何符合标准的 Web 应用中无缝使用,而不受限于特定的框架(React、Vue)」。如果你还对 Web Components 比较陌生,那么现在是时候开始了解这项技术了。
Web Components 是一种浏览器原生支持的 Web 组件化技术,它允许开发者创建可重用的「自定义元素」,并且可以在任何支持 Web Components 的浏览器中使用。
image.png
Web Components 包括以下几个核心概念:
今天将围绕这4个核心概念以及相关拓展,通过例子演示重点说一下 Web Components 是如何创建可重用的自定义元素的。
Web Components 最大的特性之一就是能将 HTML 封装成 Custom Elements(自定义元素)。下面我们通过一个简单的按钮例子,看下它是怎么实现的。
首先,我们需要定义一个自定义元素。这可以通过使用 customElements.define() 方法来实现。在这个例子中,我们将创建一个名为 my-button 的自定义元素。
// main.js
class MyButton extends HTMLElement {
constructor() {
super();
const shadowRoot=this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML=`
<style>
button {
background-color: #4CAF50;
border: none;
color: white;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
padding: 10px 24px;
border-radius: 4px;
}
</style>
<button>Click Me!</button>
`;
}
}
customElements.define('my-button', MyButton);
现在我们已经定义了一个名为 my-button 的自定义元素,我们可以在 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 Example</title>
</head>
<body>
<my-button></my-button>
<script src="./main.js"></script>
</body>
</html>
在这个例子中,我们创建了一个名为 my-button 的自定义元素,并在 HTML 文件中直接使用它。这个自定义元素将渲染为一个绿色的按钮,上面写着“Click Me!”。
不止如此,CustomElements还支持自定义元素行为(如添加点击事件),也就是说既能封装UI样式,也是封装UI交互。
const shadowRoot=this.attachShadow({ mode: 'open' });
shadowRoot.querySelector('button').addEventListener('click', ()=> {
alert('按钮被点击了!');
});
到这里为止,便实现了一个简单的 Web Components,详细代码见CustomElements。
Custom Elements 也有一组生命周期回调方法(到这里是不是感觉 Web Component 就像 Vue、React似得,怎么还有生命周期?),这些方法在元素的不同生命周期阶段被调用。这些生命周期方法允许你在元素的创建、插入文档、更新和删除等时刻执行操作。
以下是自定义元素的一些主要生命周期回调方法:
下面是一个简单的例子,展示了如何在自定义元素中使用这些生命周期方法:
class MyCustomElement extends HTMLElement {
constructor() {
super();
// 初始化操作,例如创建 Shadow DOM
const shadowRoot=this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML='<p>这是一个自定义元素</p>';
}
connectedCallback() {
// 元素被插入到 DOM 时执行的操作
console.log('Custom element connected to the DOM');
}
disconnectedCallback() {
// 元素从 DOM 中移除时执行的操作
console.log('Custom element disconnected from the DOM');
}
attributeChangedCallback(name, oldValue, newValue) {
// 监听的属性发生变化时执行的操作
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
}
static get observedAttributes() {
// 返回一个数组,包含需要监听的属性
return ['my-attribute'];
}
}
customElements.define('my-custom-element', MyCustomElement);
在 HTML 中使用这个自定义元素:
<my-custom-element my-attribute="value"></my-custom-element>
当 my-custom-element 被插入到 DOM 中时,connectedCallback 会被调用。如果元素被从 DOM 中移除,disconnectedCallback 会被调用。如果元素的 my-attribute 属性发生变化,attributeChangedCallback 会被调用。
?
「注意」:监听的同时,也记得停止监听。比如说你可能需要在元素连接到 DOM 时开始监听事件,但是在元素断开连接时停止监听,避免内存泄漏。
?
下面我们将继续探讨 Shadow DOM,它是 Web Components 的核心特性之一。
Shadow DOM 允许开发者创建一个封闭的 DOM 子树,这个子树与主文档的 DOM 分离,这意味着 Shadow DOM 内部的样式和结构不会受到外部的影响,也不会影响到外部。
在“Custom Elements(自定义元素)”的例子中,我们已经简单使用了 Shadow DOM。
「1、使用 innerHTML」
通过设置 Shadow DOM 的 innerHTML 属性,可以直接添加一个或多个元素。这种方式适用于从字符串模板快速填充 Shadow DOM。
class MyElementInnerHTML extends HTMLElement {
constructor() {
super();
const shadowRoot=this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML=`
<style>
p { color: black; }
</style>
<p>使用 innerHTML</p>
`;
}
}
customElements.define('my-element-inner', MyElementInnerHTML);
「2、使用 createElement 和 appendChild」
也可以使用 document.createElement 方法创建一个新元素,然后使用 appendChild 方法将其添加到 Shadow DOM 中。
const wrapper=document.createElement('p');
wrapper.textContent='使用 createElement 和 appendChild';
var style=document.createElement('style');
style.textContent=`
p { color: gray; }
`;
// 引入外部样式同样可以使用 appendChild
// const linkElement=document.createElement('link');
// linkElement.setAttribute('rel', 'stylesheet');
// linkElement.setAttribute('href', 'style.css');
class MyElementAppend extends HTMLElement {
constructor() {
super();
const shadowRoot=this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(wrapper);
shadowRoot.appendChild(style);
// shadowRoot.appendChild(linkElement);
}
}
customElements.define('my-element-append', MyElementAppend);
3、template方式
除上面两种方式外,还可以使用模板元素 (<template>)添加,具体见下方 「“Templates(模版)”」。
其中在自定义元素的构造函数中,我们调用了 attachShadow() 方法,并传入了一个对象 { mode: 'open' }。这里的 mode 属性决定了 Shadow DOM 的封装模式,它有两个可能的值:
在这个例子中,我们创建了一个 Shadow DOM,并向其中添加了一行文字和相关的样式。由于 Shadow DOM 的封装性,这些样式只会在 my-element 元素内部生效,不会影响到页面上的其他元素(样式隔离)。
下面我们更详细地探讨 Shadow DOM 是否允许外部访问,的两种封装模式:open 和 closed。
「1、Shadow Mode:open 模式」
当使用 open 模式创建 Shadow DOM 时,外部脚本可以通过 Element.shadowRoot 属性访问 Shadow DOM 的根节点。
这意味着你可以从外部查询、修改 Shadow DOM 内部的元素和样式。下面是一个使用 open 模式的例子:
class OpenMyElement extends HTMLElement {
constructor() {
super();
// 创建一个 open 模式的 Shadow DOM
const shadowRoot=this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML=`
<style>
p { color: red; }
</style>
<p>这是一个 open 模式的 Shadow DOM</p>
`;
}
}
customElements.define('open-my-element', OpenMyElement);
// 在外部访问 Shadow DOM
const element=document.querySelector('open-my-element');
console.log(element.shadowRoot); // 输出 ShadowRoot 对象
在这个例子中,我们创建了一个自定义元素 open-my-element,它有一个 open 模式的 Shadow DOM。由于模式是 open,我们可以在外部通过 element.shadowRoot 访问 Shadow DOM 的根节点,并进行进一步的操作,比如添加或删除子元素,修改样式等。
image.png
「2、Shadow Mode:closed 模式」
当使用 closed 模式创建 Shadow DOM 时,外部脚本无法通过 Element.shadowRoot 属性访问 Shadow DOM 的根节点。
这意味着 Shadow DOM 内部的元素和样式对外部是完全隐藏的,无法从外部直接访问或修改。 下面是一个使用 closed 模式的例子:
class ClosedMyElement extends HTMLElement {
constructor() {
super();
// 创建一个 closed 模式的 Shadow DOM
const shadowRoot=this.attachShadow({ mode: 'closed' });
shadowRoot.innerHTML=`
<style>
p { color: blue; }
</style>
<p>这是一个 closed 模式的 Shadow DOM</p>
`;
}
}
customElements.define('closed-my-element', ClosedMyElement);
// 在外部尝试访问 Shadow DOM
const element=document.querySelector('closed-my-element');
console.log(element.shadowRoot); // 输出 null
在这个例子中,我们创建了一个自定义元素 closed-mode-element,它有一个 closed 模式的 Shadow DOM。由于模式是 closed,当我们尝试在外部通过 element.shadowRoot 访问 Shadow DOM 的根节点时,将得到 null。
image.png
open 和 closed 模式决定了 Shadow DOM 的封装程度:
选择哪种模式取决于你的具体需求。如果你希望组件的内部结构和样式完全对外部隐藏,使用 closed 模式是更好的选择。如果你需要从外部访问和修改组件的内部结构和样式,使用 open 模式会更合适。
完整代码,详见ShadowDOM。
其外,Shadow DOM 还支持更高级的用法,比如可以将 Shadow DOM 分割成多个 Shadow Trees,使用 slots(插槽)来插入内容,以及使用 template(模板)来定义可重用的 HTML 结构。
Slots 是一种特殊类型的元素,它允许你将内容从组件的一个部分传递到另一个部分,增加了组件的灵活性。它使得 Web Components 自定义元素,更加的灵活。
例如,我们可以修改 my-button 组件,使其允许用户自定义按钮文本:
class MyButton extends HTMLElement {
constructor() {
super();
const shadowRoot=this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML=`
<style>
/* ...样式代码保持不变... */
</style>
<button>
<slot>Click Me!</slot>
</button>
`;
}
}
customElements.define('my-button', MyButton);
现在,当我们在 HTML 中使用 my-button 时,我们可以向其中插入任何内容,它会替换掉 <slot> 标签:
<my-button>Slots Custom Text</my-button>
image.png
在开发中,我们更多的还会遇到不同情况下,选择插入的内容,这里就用到了命名插槽,使用起来非常方便。
class MyButtonName extends HTMLElement {
constructor() {
super();
const shadowRoot=this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML=`
<style>
/* ...样式代码保持不变... */
</style>
<button>
<slot name="element-name"></slot>
<slot name="element-age"></slot>
<slot name="element-email"></slot>
</button>
`;
}
}
customElements.define('my-button-name', MyButtonName);
<my-button-name>
<span slot="element-name">element-name</span>
</my-button-name>
<my-button-name>
<span slot="element-age">element-age</span>
</my-button-name>
<my-button-name>
<span slot="element-email">element-email</span>
</my-button-name>
image.png
是不是很方便,很灵活!!具体代码详见Web Components Slots。
Templates 允许你定义一个可以在多个组件中重用的 HTML 结构。你可以将模板放在 HTML 文件中的任何位置,并通过 JavaScript 动态地实例化它们:
<my-button></my-button>
<template id="my-button-template">
<style>
/* ...样式代码保持不变... */
</style>
<button>
<slot>Click Me!</slot>
</button>
</template>
在 JavaScript 中,你可以这样使用模板:
class MyButton extends HTMLElement {
constructor() {
super();
const shadowRoot=this.attachShadow({ mode: 'open' });
const template=document.getElementById('my-button-template');
// 使用`cloneNode()` 方法添加了拷贝到 Shadow root 根节点上。
shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define('my-button', MyButton);
image.png
这样,你就可以在不同的组件中重用同一个模板,从而提高代码的可维护性和重用性。具体代码下详见Web Components Templates。
Web Components 是一组用于构建可复用组件的技术,包括 Custom Elements, Shadow DOM, HTML Templates 等。这些技术的出现,使得开发者能够更好地组织,去开发复杂的网页应用。然而,由于这些技术相对较新,不同浏览器的支持情况不尽相同,因此兼容性问题也是我们需要重点关注的方向。
Custom Elements
image.png
Shadow DOM
image.png
HTML Templates
image.png
从上面可以看出,现阶段市场上大部分的浏览器已经都原生支持了 Web Components 的规范标准。「但是如果说出现了兼容性问题,我们应该怎么处理?」
对于旧版浏览器不支持的兼容性情况,可以考虑使用 polyfill 来实现兼容性。Polyfills 是一种代码注入技术,使得浏览器可以支持新的标准 API。对于不支持 Web Components 的浏览器,我们可以用 Polyfills 让这些浏览器可以支持 Web Components。
这里我们可以用到 webcomponents.js 库,它可以实现兼容 Custom Elements、Shadow DOM 和 HTML Templates 标准,让我们在开发时不必考虑兼容性问题。
npm install @webcomponents/webcomponentsjs
<!-- load webcomponents bundle, which includes all the necessary polyfills -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<!-- load the element -->
<script type="module" src="my-element.js"></script>
<!-- use the element -->
<my-element></my-element>
具体配置详情,见polyfills webcomponents。
相信大家也比较关心 Web Components 与现有框架(如 React、Vue)相比有哪些优势?以及各自适用场景?
首先,Web Components 是一组 Web 平台 API,允许开发者创建可重用的自定义元素,而无需依赖于任何特定的框架。与现有的前端框架,Web Components 有以下几个优势:
然而,Web Components 也有其局限性,例如:
总的来说,「Web Components 提供了一种标准化且框架无关的方式来构建组件,适合组件库的开发」。而框架如 「React、Vue 则在生态系统支持、开发体验和数据处理方面有明显优势,适合快速开发复杂的应用程序」。
Web Components 是 W3C 推动的标准化技术,它通过自定义元素的方式,允许开发者在浏览器中直接使用。这种技术通过 Shadow DOM 实现了组件化 DOM 隔离和样式隔离,确保了组件的独立性和可重用性,这些特性被现有很多借鉴和使用。
希望这篇文章对你有所帮助!!!欢迎在评论区,一起讨论。
今前端编程中,利用语义化的 HTML 结合 CSS 来完一个组件并不是一件难事,这也意味着无论在 React、Vue 中都可以插入,不过它俩不是今天的主角,接下来我将用一个例子来介绍如何封装一个完整的原生 HTML 的 Web Components 组件,让我们开始吧!
首先我们来了解下 HTML 中的 <details> 元素,它可以用于创建一个小部件,其中包含仅在小部件处于“打开”状态时才可见的附加信息,<details>元素内可以包含的内容没有任何限制。
默认情况下,元素创建的小部件<details>处于“关闭”状态(open标签可使其打开)。通过单击小部件在“打开”和“关闭”状态之间切换,以显示或隐藏标签中包含的附加信息,内部标签 <summary> 元素则可为该部件提供概要。
一个简单的例子如下:
<details>
<summary> 不能说的秘密 </summary>
藏的这么深,可还是被你发现了
</details>
details {
border: 1px solid #aaa;
border-radius: 4px;
padding: .5em .5em 0;
}
summary {
font-weight: bold;
margin: -.5em -.5em 0;
padding: .5em;
}
details[open] {
padding: .5em;
}
details[open] summary {
border-bottom: 1px solid #aaa;
margin-bottom: .5em;
}
使用语义化 HTML 的优点:页面内容结构更清晰,方便开发者阅读,更利于浏览器的理解与加载,搜索引擎解析与SEO优化。
原生元素默认的样式很简陋,因此我们需要为其定制一下样式,这块内容我们简单带过,只讲解关键部分,样式内容有省略,具体可以在文末的码上掘金中看到呈现效果。
.ContentWarning > summary {
position: relative;
list-style: none; /** 去除默认样式 **/
user-select: none;
cursor: pointer;
/** 为其添加一个斜线背景 **/
--stripe-color: rgb(0 0 0 / 0.1);
background-image: repeating-linear-gradient(45deg,
transparent,
transparent 0.5em,
var(--stripe-color) 0.5em,
var(--stripe-color) 1em);
}
/** 通过var变量调整悬停时的颜色样式 **/
.ContentWarning>summary: hover,
.ContentWarning>summary: focus {
--stripe-color: rgb(150 0 0 / 0.1);
}
现在我们来把它封装成一个完整的组件,这需要先将 HTML 编写在模板 template 当中,并设置一个 id,如下所示:
<template id="warning-card">
<details class="ContentWarning">
<summary>
<strong>?? 注意:</strong> 以下为隐藏内容
</summary>
<slot name="desc"> 藏的这么深,可还是被你发现了 </slot>
</details>
</template>
熟悉 Vue 的小伙伴应该很容易理解上面的代码,结构很相似,不过网页不会直接渲染它包裹的内容。此外我们还对此模板设置了一个插槽 slot,后面会讲到它的作用。
有了上面封装好的模板,我们就需要在 JS 中定义成可用组件来让其能够被使用,调用 window 下的 customElements.define 方法,第一个参数是传入组件名称,我们定义组件名为: warning-card ,第二个参数传入一个继承了 HTMLElement 的类,在其构造方法当中获取并克隆一个新的 HTML 节点,它会通过 appendChild 渲染到页面当中。
window.customElements.define('warning-card',
class extends HTMLElement {
constructor() {
super();
var templateElem=document.getElementById('warning-card');
var content=templateElem.content.cloneNode(true);
this.appendChild(content);
}
})
接着我们就可以在页面中把它当作组件那样使用了:
<warning-card> </warning-card>
回头看看上面我们模板中设置的插槽 slot,此时还是没有生效的,我们需要稍微改写一下构造函数中的渲染方式,将 web 组件定义为一个 Shadow DOM,这样构造的是一个可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起的独立元素,并在最后使用 Node.cloneNode() 方法添加了模板的拷贝到 Shadow 的根结点上。
window.customElements.define('warning-card',
class extends HTMLElement {
constructor() {
super();
var template=document.getElementById('warning-card').content;
this.attachShadow({ mode: 'open' }).appendChild(template.cloneNode(true));
}
})
现在我们尝试使用下组件,往其内容添加一个图片,指向名为 desc 的 slot 插槽中:
<warning-card>
<img slot="desc" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ba825ffee78c4a1b9c0232e5d2f1d048~tplv-k3u1fbpfcp-watermark.image?" />
</warning-card>
这时你会发现,图片插入到 details 元素的隐藏区域当中了,slot 已经成功生效,但是样式却消失了,这时因为组件已经被完全隔离,我们需要将样式作用在其内部才会生效。
<template id="warning-card">
<style>
<!-- TODO: 组件的样式 -->
</style>
<details class="ContentWarning">
<summary>
<strong>?? 注意:</strong>
</summary>
<slot name="desc">THE DESCRIPTION</slot>
</details>
</template>
这样组件就正常了:
除了定制模板中的插槽,我们也可以通过 HTML 标签属性来实现一些简单的传参,例如在 summary 标签中显示一个标题:
<warning-card title="前方高能">
</warning-card>
我们只需要在模板中定义好这个标题的位置:
<template id="warning-card">
<details class="ContentWarning">
<summary>
<!-- TODO: 模板中加入一个span标签 -->
<strong>?? 注意:</strong> <span id="title"></span>
</summary>
</details>
</template>
最后在构造函数中我们通过 document 的原生方法写入模板中就可以了:
window.customElements.define('warning-card',
class extends HTMLElement {
constructor() {
super();
var template=document.getElementById('warning-card').content;
// TODO: 找到title标签,写入传入组件的title属性值
template.querySelector('#title').innerText=this.getAttribute('title');
this.attachShadow({ mode: 'open' }).appendChild(template.cloneNode(true));
}
})
至此,我们通过一个简单的原生组件学习了如何编写 Web Components,可以在此代码片段中查看具体源码:原生Web Components组件 - 码上掘金原生Web Components组件 - 码上掘金。
以上就是文章的全部内容,希望对你有所帮助!如果觉得文章写的不错,可以点赞收藏,也欢迎关注,我会持续更新更多前端有用的知识与实用技巧,我是茶无味de一天,希望与你共同成长~
*请认真填写需求信息,我们会在24小时内与您取得联系。