Web Components 是一個(gè)瀏覽器原生支持的組件化方案叛赚,允許你創(chuàng)建新的自定義夹厌、可封裝、可重用的HTML 標(biāo)記责蝠。不用加載任何外部模塊党巾,直接就可以在瀏覽器中跑。本文就簡單介紹一下:使用 Web Components 實(shí)現(xiàn)一個(gè)類 Element UI 中的 Card 卡片組件玛歌。
一昧港、前言
隨著前端工程化生態(tài)日益成熟,出現(xiàn)了很多優(yōu)秀的框架支子,如:Vue
创肥、React
、Angular
等等值朋,極大的提高了日常開發(fā)效率叹侄。
其中組件化開發(fā)發(fā)揮了至關(guān)重要的作用,但是這些組件化開發(fā)都需要依賴第三方框架昨登,編譯打包之后才能在瀏覽器正常使用趾代。
而原生組件 Web Components
,相比與第三方框架使用起來更簡單直接丰辣,符合直覺撒强,不用加載任何外部模塊,代碼量小笙什。
二飘哨、Web Components 核心組成
- 自定義元素(custom element),使用
window.customElements.define
API注冊(cè) - Shadow DOM隔離琐凭,影藏標(biāo)記結(jié)構(gòu)芽隆、樣式和行為
- 可以在
<template>
中定義標(biāo)記結(jié)構(gòu)、樣式,多次重用胚吁。利用slot
插槽牙躺、命名插槽,可以傳入定制化的結(jié)構(gòu)UI腕扶,使用上類似Vue
中的slot
插槽
1. Custom Elements
自定義的 HTML 標(biāo)簽孽拷,稱為自定義元素(custom element)。根據(jù)規(guī)范蕉毯,自定義元素的名稱必須包含連詞線-
乓搬,用與區(qū)別原生的 HTML 元素思犁。所以代虾,<com-card>
不能寫成<comcard>
。
<div id="custom-card" class="com-card">
<div class="com-card-head">
<slot name="head"></slot>
</div>
<div class="com-card-body">
<slot></slot>
<div class="link-wrap">
<a class="link" href="" title=""></a>
</div>
</div>
</div>
<script>
class ComCard extends HTMLElement {
constructor() {
super()
var tplEle = document.getElementById('custom-card')
this.append(tplEle)
}
}
window.customElements.define('com-card', ComCard)
</script>
這樣就注冊(cè)了瀏覽器可識(shí)別渲染的一個(gè)自定義元素標(biāo)簽激蹲。
2. Shadow DOM
Shadow DOM 是對(duì)DOM的一個(gè)封裝棉磨。可以將標(biāo)記結(jié)構(gòu)学辱、樣式和行為隱藏起來乘瓤,并與頁面上的其他代碼相隔離,保證不同的部分不會(huì)混在一起策泣,可使代碼更加干凈衙傀、整潔。
使用自定義元素的 this.attachShadow()
方法可以開啟 Shadow DOM
萨咕。
class ComCard extends HTMLElement {
constructor() {
super()
var shadow = this.attachShadow({mode: 'closed'}) // open
var tplEle = document.getElementById('custom-card')
shadow.appendChild(tplEle)
}
}
window.customElements.define('com-card', ComCard);
其中參數(shù){ mode: 'closed' }
统抬,表示 Shadow DOM
是封閉的,不允許外部訪問危队。
3. templates 和 slots
因?yàn)榻M件的樣式應(yīng)該與代碼封裝在一起聪建,只對(duì)自定義元素生效,不影響外部的全局樣式茫陆。所以金麸,可以把樣式寫在<template>
里面,這樣作為自定義元素結(jié)構(gòu)的基礎(chǔ)可以被多次重用簿盅。
<template id="custom-card-template">
<style>
.com-card {
}
</style>
<div class="com-card">
</div>
</template>
<script>
class ComCard extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow({mode: 'closed'}) // open
var tplEle = document.getElementById('custom-card-template')
var content = tplEle.content.cloneNode(true)
shadow.appendChild(content)
}
}
window.customElements.define('com-card', ComCard);
</script>
三挥下、完整代碼
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Web Component</title>
<style>
* {
box-sizing: border-box;
}
body {
font-size: 14px;
}
.box {
padding: 5px 0 30px;
}
.box .caption {
display: none;
}
.box h1 {
text-align: center;
}
.box li {
color: #666;
font-size: 14px;
line-height: 1.8;
margin-top: 15px;
}
.img {
display: block;
width: 80%;
margin: 0 !important;
}
.card-head {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
color: #333;
font-size: 16px;
}
.card-head-btn {
color: #409eff;
cursor: pointer;
text-decoration: none !important;
}
.card-head-btn:hover {
text-decoration: none;
}
</style>
</head>
<body>
<div class="box">
<h1>Web Component</h1>
<com-card data-show-head="0" data-url="https://tiven.cn" data-title="天問博客">
<div slot="head" class="card-head">
<div class="card-title">卡片名稱</div>
<a class="card-head-btn">操作按鈕</a>
</div>
<img class="img" src="https://tiven.cn/static/img/kpl-sunwukong-a3Lt-ed2NG9r4NFDm_9DA.jpg" alt="天問">
</com-card>
<br>
<br>
<com-card data-show-head="1" data-url="https://tiven.cn/p/de241e23/" data-title="Vite+Vue3+Vant快速構(gòu)建項(xiàng)目">
<div slot="head" class="card-head">
<div class="card-title">卡片名稱</div>
<a class="card-head-btn" onclick="hello()">操作按鈕</a>
</div>
<img class="img" src="https://tiven.cn/static/img/kpl-xuance-JqX71qH7aTflHV_gqvhIc.jpg" alt="天問">
<ol>
<li>君不見黃河之水天上來,奔流到海不復(fù)回桨醋。</li>
<li>君不見高堂明鏡悲白發(fā)棚瘟,朝如青絲暮成雪。</li>
<li>天生我材必有用讨盒,千金散盡還復(fù)來解取。</li>
</ol>
</com-card>
</div>
<template id="custom-card-template">
<style>
.com-card {
min-width: 200px;
min-height: 100px;
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: .3s;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.com-card-head {
padding: 10px 20px;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
}
.com-card-body {
padding: 20px;
}
.link-wrap {
text-align: left;
padding-top: 20px;
}
.link {
display: inline-block;
height: 42px;
line-height: 43px;
padding: 0 30px;
text-align: center;
cursor: pointer;
color: #fff;
background-color: #409eff;
border-color: #409eff;
-webkit-appearance: none;
box-sizing: border-box;
outline: none;
transition: .1s;
font-weight: 500;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
font-size: 14px;
border-radius: 4px;
text-decoration: none !important;
}
</style>
<div class="com-card">
<div class="com-card-head">
<slot name="head"></slot>
</div>
<div class="com-card-body">
<slot></slot>
<div class="link-wrap">
<a class="link" href="" title=""></a>
</div>
</div>
</div>
</template>
<script>
class ComCard extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow({mode: 'closed'}) // open
var tplEle = document.getElementById('custom-card-template')
var content = tplEle.content.cloneNode(true)
var attrList = Array.from(this.attributes);
var props = attrList.reduce((prev, item)=>{
prev[item.name] = item.value
return prev
}, {})
if (props['data-show-head']!=='1') {
var head = content.querySelector('.com-card-head')
head.remove()
}
var urlEle = content.querySelector('.link')
if (props['data-url'] && props['data-title']) {
urlEle.href = props['data-url']
urlEle.title = props['data-title']
urlEle.innerText = props['data-title']
} else {
urlEle.remove()
}
shadow.appendChild(content)
}
connectedCallback(){
//在這里發(fā)送數(shù)據(jù)請(qǐng)求(Ajax)
console.log('connectedCallback')
}
//被從文檔DOM中刪除時(shí)調(diào)用
disconnectedCallback(){
console.log('disconnectedCallback')
}
//被移動(dòng)到新的文檔時(shí)調(diào)用
adoptedCallback(){
console.log('adoptedCallback')
}
//當(dāng)增加、刪除返顺、修改自身的屬性時(shí)被調(diào)用
attributeChangedCallback(){
console.log('attributeChangedCallback')
}
}
window.customElements.define('com-card', ComCard);
function hello() {
alert('Hello禀苦,Web Component')
}
</script>
</body>
</html>
最終效果如上圖所示蔓肯,具體demo演示地址:https://tiven.cn/demo/web-component.html
四、Web Components vs Vue Components
Vue Component | Web Component |
---|---|
data | 實(shí)例屬性 |
props | attributes |
watch | observedAttributes振乏、attributeChangedCallback |
computed | getters |
methods | class methods |
mounted | connectedCallback |
destroyed | disconnectedCallback |
style scoped | template中的style |
template | template |
五蔗包、Web Components 生命周期回調(diào)函數(shù)
-
connectedCallback
:當(dāng) custom element首次被插入文檔DOM時(shí),被調(diào)用慧邮。 -
disconnectedCallback
:當(dāng) custom element從文檔DOM中刪除時(shí)调限,被調(diào)用。 -
adoptedCallback
:當(dāng) custom element被移動(dòng)到新的文檔時(shí)误澳,被調(diào)用耻矮。 -
attributeChangedCallback
: 當(dāng) custom element增加、刪除忆谓、修改自身屬性時(shí)裆装,被調(diào)用。
六倡缠、優(yōu)點(diǎn) and 缺點(diǎn)
優(yōu)點(diǎn):
- 瀏覽器原生支持哨免,不需要引入額外的第三方庫
- 語義化
- 復(fù)用性,移植性高
- 不同團(tuán)隊(duì)不同項(xiàng)目可以共用組件
缺點(diǎn):
- 需要操作DOM
- 目前瀏覽器兼容性昙沦、性能方面不夠友好
- 和外部css交互比較難
七琢唾、基于web components的框架
-
LitElement 是一個(gè)快速、輕量級(jí)的 Web UI 框架盾饮。使用
lit-html
來渲染元素采桃。 - Polymer 是一款實(shí)用、基于事件驅(qū)動(dòng)丐谋、封裝性和交互性強(qiáng)的 Web UI 框架芍碧。
- Omi 是基于 Web 組件的跨框架跨平臺(tái)框架 。移動(dòng)端 & 桌面 & 小程序号俐。
歡迎訪問:天問博客