元件是前端的發展方向,現在流行的 React 和 Vue 都是元件框架。
谷歌公司由於掌握了 Chrome 瀏覽器,一直在推動瀏覽器的原生元件,即 Web Components API。相比第三方框架,原生元件簡單直接,符合直覺,不用載入任何外部模組,程式碼量小。目前,它還在不斷髮展,但已經可用於生產環境。
Web Components API 內容很多,本文不是全面的教程,只是一個簡單演示,讓大家看一下怎麼用它開發元件。
一、自定義元素
下圖是一個使用者卡片。
本文演示如何把這個卡片,寫成 Web Components 元件,這裡是最後的完整程式碼。
網頁只要插入下面的程式碼,就會顯示使用者卡片。
<user-card></user-card>
這種自定義的 HTML 標籤,稱為自定義元素(custom element)。根據規範,自定義元素的名稱必須包含連詞線,用與區別原生的 HTML 元素。所以,<user-card>
不能寫成<usercard>
。
二、customElements.define()
自定義元素需要使用 JavaScript 定義一個類,所有<user-card>
都會是這個類的例項。
class UserCard extends HTMLElement { constructor() { super(); } }
上面程式碼中,UserCard
就是自定義元素的類。注意,這個類的父類是HTMLElement
,因此繼承了 HTML 元素的特性。
接著,使用瀏覽器原生的customElements.define()
方法,告訴瀏覽器<user-card>
元素與這個類關聯。
window.customElements.define('user-card', UserCard);
三、自定義元素的內容
自定義元素<user-card>
目前還是空的,下面在類裡面給出這個元素的內容。
class UserCard extends HTMLElement { constructor() { super(); var image = document.createElement('img'); image.src = 'https://semantic-ui.com/images/avatar2/large/kristy.png'; image.classList.add('image'); var container = document.createElement('div'); container.classList.add('container'); var name = document.createElement('p'); name.classList.add('name'); name.innerText = 'User Name'; var email = document.createElement('p'); email.classList.add('email'); email.innerText = 'yourmail@some-email.com'; var button = document.createElement('button'); button.classList.add('button'); button.innerText = 'Follow'; container.append(name, email, button); this.append(image, container); } }
上面程式碼最後一行,this.append()
的this
表示自定義元素例項。
完成這一步以後,自定義元素內部的 DOM 結構就已經生成了。
四、<template>
標籤
使用 JavaScript 寫上一節的 DOM 結構很麻煩,Web Components API 提供了<template>
標籤,可以在它裡面使用 HTML 定義 DOM。
<template id="userCardTemplate"> <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image"> <div class="container"> <p class="name">User Name</p> <p class="email">yourmail@some-email.com</p> <button class="button">Follow</button> </div> </template>
然後,改寫一下自定義元素的類,為自定義元素載入<template>
。
class UserCard extends HTMLElement { constructor() { super(); var templateElem = document.getElementById('userCardTemplate'); var content = templateElem.content.cloneNode(true); this.appendChild(content); } }
上面程式碼中,獲取<template>
節點以後,克隆了它的所有子元素,這是因為可能有多個自定義元素的例項,這個模板還要留給其他例項使用,所以不能直接移動它的子元素。
到這一步為止,完整的程式碼如下。
<body> <user-card></user-card> <template>...</template> <script> class UserCard extends HTMLElement { constructor() { super(); var templateElem = document.getElementById('userCardTemplate'); var content = templateElem.content.cloneNode(true); this.appendChild(content); } } window.customElements.define('user-card', UserCard); </script> </body>
五、新增樣式
自定義元素還沒有樣式,可以給它指定全域性樣式,比如下面這樣。
user-card { /* ... */ }
但是,元件的樣式應該與程式碼封裝在一起,只對自定義元素生效,不影響外部的全域性樣式。所以,可以把樣式寫在<template>
裡面。
<template id="userCardTemplate"> <style> :host { display: flex; align-items: center; width: 450px; height: 180px; background-color: #d4d4d4; border: 1px solid #d5d5d5; box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1); border-radius: 3px; overflow: hidden; padding: 10px; box-sizing: border-box; font-family: 'Poppins', sans-serif; } .image { flex: 0 0 auto; width: 160px; height: 160px; vertical-align: middle; border-radius: 5px; } .container { box-sizing: border-box; padding: 20px; height: 160px; } .container > .name { font-size: 20px; font-weight: 600; line-height: 1; margin: 0; margin-bottom: 5px; } .container > .email { font-size: 12px; opacity: 0.75; line-height: 1; margin: 0; margin-bottom: 15px; } .container > .button { padding: 10px 25px; font-size: 12px; border-radius: 5px; text-transform: uppercase; } </style> <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image"> <div class="container"> <p class="name">User Name</p> <p class="email">yourmail@some-email.com</p> <button class="button">Follow</button> </div> </template>
上面程式碼中,<template>
樣式裡面的:host
偽類,指代自定義元素本身。
六、自定義元素的引數
<user-card>
內容現在是在<template>
裡面設定的,為了方便使用,把它改成引數。
<user-card image="https://semantic-ui.com/images/avatar2/large/kristy.png" name="User Name" email="yourmail@some-email.com" ></user-card>
<template>
程式碼也相應改造。
<template id="userCardTemplate"> <style>...</style> <img class="image"> <div class="container"> <p class="name"></p> <p class="email"></p> <button class="button">Follow John</button> </div> </template>
最後,改一下類的程式碼,把引數加到自定義元素裡面。
class UserCard extends HTMLElement { constructor() { super(); var templateElem = document.getElementById('userCardTemplate'); var content = templateElem.content.cloneNode(true); content.querySelector('img').setAttribute('src', this.getAttribute('image')); content.querySelector('.container>.name').innerText = this.getAttribute('name'); content.querySelector('.container>.email').innerText = this.getAttribute('email'); this.appendChild(content); } } window.customElements.define('user-card', UserCard);
七、Shadow DOM
我們不希望使用者能夠看到<user-card>
的內部程式碼,Web Component 允許內部程式碼隱藏起來,這叫做 Shadow DOM,即這部分 DOM 預設是隱藏的,開發者工具裡面看不到。
自定義元素的this.attachShadow()
方法開啟 Shadow DOM,詳見下面的程式碼。
class UserCard extends HTMLElement { constructor() { super(); var shadow = this.attachShadow( { mode: 'closed' } ); var templateElem = document.getElementById('userCardTemplate'); var content = templateElem.content.cloneNode(true); content.querySelector('img').setAttribute('src', this.getAttribute('image')); content.querySelector('.container>.name').innerText = this.getAttribute('name'); content.querySelector('.container>.email').innerText = this.getAttribute('email'); shadow.appendChild(content); } } window.customElements.define('user-card', UserCard);
上面程式碼中,this.attachShadow()
方法的引數{ mode: 'closed' }
,表示 Shadow DOM 是封閉的,不允許外部訪問。
至此,這個 Web Component 元件就完成了,完整程式碼可以訪問這裡。可以看到,整個過程還是很簡單的,不像第三方框架那樣有複雜的 API。
八、元件的擴充套件
在前面的基礎上,可以對元件進行擴充套件。
(1)與使用者互動
使用者卡片是一個靜態元件,如果要與使用者互動,也很簡單,就是在類裡面監聽各種事件。
this.$button = shadow.querySelector('button'); this.$button.addEventListener('click', () => { // do something });
(2)元件的封裝
上面的例子中,<template>
與網頁程式碼放在一起,其實可以用指令碼把<template>
注入網頁。這樣的話,JavaScript 指令碼跟<template>
就能封裝成一個 JS 檔案,成為獨立的元件檔案。網頁只要載入這個指令碼,就能使用<user-card>
元件。
這裡就不展開了,更多 Web Components 的高階用法,可以接著學習下面兩篇文章。
九、參考連結
- The anatomy of Web Components, Uday Hiwarale
(完)