本文最后更新于:
2023年10月11日 下午
什么是 Web Components
Web Components 是一组 Web 平台 API,允许您创建新的自定义、可重用、封装的 HTML 标签以在网页和 Web 应用程序中使用。自定义组件和小部件基于web Components 标准构建,可以跨现代浏览器工作,并且可以与任何支持 HTML 的 JavaScript 库或框架一起使用。Web components 基于现有的 Web 标准。支持 Web components 的特性目前正被添加到 HTML 和 DOM 规范中。
https://www.webcomponents.org/introduction
也就是说如果你拥有一个 Web Components 组件可以在任何 web 框架中使用它,比如在 React、Vue、Angular 中。
使用原生 Web Components 技术封装一个悬浮按钮组件 悬浮按钮组件的功能如下:
点击悬浮按钮时,上面出现一个卡片,卡片出现时带有渐入动画效果;
点击悬浮按钮时,按钮上图标发生改变,从 info 变成 close;
点击 close 图标,上方的卡片消失,消失时带有渐出的动画效果;
首先我先使用最原始的 Html+CSS+JavaScript Dom 操作来实现,然后再使用 web components 技术封装成组件。 样式部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <style> #floating-button { position : fixed; bottom : 20px ; right : 20px ; width : 50px ; height : 50px ; background-color : silver; border-radius : 50% ; display : flex; justify-content : center; align-items : center; color : white; cursor : pointer; transition : background-color 0.3s ease-in-out; z-index : 999 ; } #floating-card { position : fixed; bottom : 80px ; right : 20px ; padding : 1rem ; background-color : lightgray; text-align : center; opacity : 0 ; transform : translateX (110% ); transition : opacity 0.3s ease-in-out, transform 0.3s ease-in-out; } #floating-card img { width : 268px ; height : 255px ; border : 1px solid rgb (181 , 177 , 187 ); } #floating-card .opened { opacity : 1 ; transform : translateX (0 ); } .hidden { display : none; } </style>
html 结构部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <div class ="container" > <div id ="floating-button" onclick ="toggleCard()" > <svg id="floating-icon-info" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle-fill" viewBox="0 0 16 16" > <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" /> </svg > <svg class="hidden" id="floating-icon-close" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16" > <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" /> </svg > </div > <div id ="floating-card" > <h2 > Card Title</h2 > <img src="https://cdn.jsdelivr.net/gh/frmachao/images@blog/uPic/2021-11-09-WmerG5.jpg" alt="" /> </div > </div >
javascript dom 操作部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script> const floatingButton = document .getElementById('floating-button' ); const floatingCard = document .getElementById('floating-card' ); const floatingInfo = document .getElementById('floating-icon-info' ); const floatingClose = document .getElementById('floating-icon-close' ); function toggleCard ( ) { floatingCard.classList.toggle('opened' ); setTimeout (() => { if (floatingCard.classList.contains('opened' )) { floatingInfo.classList.add('hidden' ); floatingClose.classList.remove('hidden' ); } else { floatingClose.classList.add('hidden' ); floatingInfo.classList.remove('hidden' ); } }) } </script>
效果展示:
接下来我们使用 Web Components 技术进行封装: 首先编写 Web Components 组件中的 template,我们将之前编写的 css 和 html 全部放入 template 中component-demo.html
1 2 3 4 5 6 7 8 9 10 11 12 13 <template id ="floating-template" > <style > #floating-button { position : fixed; } ....样式部分; </style > <div class ="container" > <div id ="floating-button" > </div > <div id ="floating-card" > </div > ....html 部分 </div > </template >
然后编写 javaScript 部分,获取到模板并注册自定义元素,使用 Shadow DOM 将自定义元素的 HTML 和 CSS 封装在组件内。
什么是 Shadow DOM?
Web components 的一个重要属性是封装——可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。其中,Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。
component.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class FloatingButton extends HTMLElement { constructor ( ) { super (); } connectedCallback ( ) { const shadowRoot = this .attachShadow({ mode : "open" }); const template = document .getElementById("floating-template" ); const instance = template.content.cloneNode(true ); instance .querySelector("img" ) .setAttribute("src" , this .getAttribute("tip-code" )); instance.querySelector("#floating-card h2" ).innerText = this .getAttribute("title" ); shadowRoot.appendChild(instance); const floatingButton = shadowRoot.getElementById("floating-button" ); const floatingCard = shadowRoot.getElementById("floating-card" ); const floatingInfo = shadowRoot.getElementById("floating-icon-info" ); const floatingClose = shadowRoot.getElementById("floating-icon-close" ); floatingButton.addEventListener("click" , toggleCard); function toggleCard ( ) { floatingCard.classList.toggle("opened" ); setTimeout (() => { if (floatingCard.classList.contains("opened" )) { floatingInfo.classList.add("hidden" ); floatingClose.classList.remove("hidden" ); } else { floatingClose.classList.add("hidden" ); floatingInfo.classList.remove("hidden" ); } }); } } } customElements.define("floating-button" , FloatingButton);
在 component-demo.html
中引入 component.js
,并使用 组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > component-demo</title > </head > <body > <template id ="floating-template" > </template > <script src ="./component.js" > </script > <floating-button title="Custom Title" tip-code="https://cdn.jsdelivr.net/gh/frmachao/images@blog/uPic/2021-11-09-WmerG5.jpg" ></floating-button > </body > </html >
在浏览器中打开 component-demo.html
,可以看到我们定义的 floating-button 已经被正确的渲染出来,审查元素可以看到组件是一个 Shadow DOM 中。 因为 Shadow DOM 是一个沙箱环境,所以不用担心组件中的 css 和 js 会影响到页面上其他组件,同样的 Shadow DOM 外的代码也不会对组件产生影响。 比如,我在全局中编写一段 css 来改变 `floating-card`` 的背景色,是不会对组件生效的。
1 2 3 #floating-card { background-color : skyblue; }
至此我们的 floating-button
就初步封装完成了,但是我们发现当前的组件无论在编写过程中还是在使用上都有诸多不便之处,比如是否可以像 react
中封装组件那样,将组件单独封装到一个文件中,使用时只要 import 这个组件到目标页面中即可。那么当然是有的,社区中已经出现了很多基于 web components
的框架,可以帮助我们快速封装组件,下面给大家介绍一个名为 https://ofajs.com 的框架。
使用 ofa.js 封装 web components 组件 需要准备的文件如下:
1 2 3 4 - ofa-component.html - index.html
安装并使用 ofa.js 使用 ofa.js 只需通过引入 CDN 地址将项目 ofa.js 引入到 index.html 中,然后使用 标签引入编写好的 ofa-component.html
文件即可使用 ofa-component 组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > ofa-demo</title > <script src ="https://cdn.jsdelivr.net/gh/kirakiray/ofa.js@4.3.23/dist/ofa.js" > </script > </head > <body > <l-m src ="./ofa-component.html" > </l-m > <ofa-component title="Custom Title" tip-code="https://cdn.jsdelivr.net/gh/frmachao/images@blog/uPic/2021-11-09-WmerG5.jpg" ></ofa-component > </body > </html >
使用 ofa.js 编写 web components 组件 下面我们重点来看 ofa-component.html
是如何编写 web components 组件的。
在组件文件中,首先添加一个 template 元素,并添加 component 属性。将组件需要渲染的内容放置在这个 template 元素内。最终,这些内容将被渲染到组件的 Shadow DOM 内,Shadow DOM 与外部环境隔离,以防止污染外部环境。
1 2 3 4 5 6 7 8 9 10 11 12 13 <template component > <style > #floating-button { position : fixed; } ....样式部分; </style > <div class ="container" > <div id ="floating-button></div> <div id=" floating-card "></div > ....html 部分 </div > </template >
接着,在模板内容下方,添加一个 <script>
标签,将组件的 JavaScript 代码放入其中。
在 ofa.js
中使用 on:click="yyy"
的形式,可以将目标元素的指定事件(例如 click)绑定到宿主组件的属性 yyy 上。通过这种方式我们给 floating-button 绑定上 click 事件。
使用 x-if
模板组件对展示悬浮图标的逻辑做判断
使用 {{title}}
的渲染文本语法,渲染用户传入的 title
参数
使用 attr:src="tipCode"
对元素的 attributes
进行绑定,设置为用户传入的 tip-code
参数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <template component > <style > #floating-button { position : fixed; } </style > <div class ="container" > <div id ="floating-button" on:click ="handleClick" > <x-if :value ="isOpen" > </x-if > <x-else > </x-else > </div > <div id ="floating-card" attr:class ="isOpen?'opened':'closed'" > <h2 > {{title}}</h2 > <img attr:src ="tipCode" alt ="tip-code" /> </div > </div > <script > export const tag = "ofa-component" ; export default async function ( ) { return { data: { isOpen: false , }, attrs: { title: "Card Title" , tipCode: "" , }, proto: { handleClick ( ) { this .isOpen = !this .isOpen; }, }, }; } </script > </template >
接下来使用一个静态资源服务器打开 index.html
,查看组件是否正常工作。 至此使用 ofa.js
封装的组件就完成了,我们可以发现使用 ofa.js
去编写 web components
组件的过程相比原生的 web-component 的写法,可以减少很多样板代码,结合 ofa.js
提供的文件加载器,我们可以很方便的在网页中引入组件。另外 ofa.js
对数据和 dom
做了数据绑定,我们可以告别繁琐对 dom
操作,而去只关心数据的变动。
参考