【總結(jié)】1181- 從 0 到 1 上手 Web Components 業(yè)務(wù)組件庫(kù)開(kāi)發(fā)

組件化是前端發(fā)展的一個(gè)重要方向,它一方面提高開(kāi)發(fā)效率,另一方面降低維護(hù)成本。主流的 Vue.js、React 及其延伸的 Ant Design、uniapp、Taro 等都是組件框架。Web Components 是一組 Web 原生 API 的總稱,允許我們創(chuàng)建可重用的自定義組件,并在我們 Web 應(yīng)用中像使用原生 HTML 標(biāo)簽一樣使用。目前已經(jīng)很多前端框架/庫(kù)支持 Web Components。
本文將帶大家回顧 Web Components 核心 API,并從 0 到 1 實(shí)現(xiàn)一個(gè)基于 Web Components API 開(kāi)發(fā)的業(yè)務(wù)組件庫(kù)。
最終效果:https://blog.pingan8787.com/exe-components/demo.html倉(cāng)庫(kù)地址:https://github.com/pingan8787/Learn-Web-Components
一、回顧 Web Components
在前端發(fā)展歷史中,從剛開(kāi)始重復(fù)業(yè)務(wù)到處復(fù)制相同代碼,到 Web Components 的出現(xiàn),我們使用原生 HTML 標(biāo)簽的自定義組件,復(fù)用組件代碼,提高開(kāi)發(fā)效率。通過(guò) Web Components 創(chuàng)建的組件,幾乎可以使用在任何前端框架中。
1. 核心 API 回顧
Web Components 由 3 個(gè)核心 API 組成:
「Custom elements(自定義元素)」:用來(lái)讓我們定義「自定義元素」及其「行為」,對(duì)外提供組件的標(biāo)簽; 「Shadow DOM(影子 DOM)」:用來(lái)封裝組件內(nèi)部的結(jié)構(gòu),避免與外部沖突; 「HTML templates(HTML 模版)」:包括 <template>和<slot>元素,讓我們可以定義各種組件的 HTML 模版,然后被復(fù)用到其他地方,使用過(guò) Vue/React 等框架的同學(xué)應(yīng)該會(huì)很熟悉。
另外,還有 HTML imports,但目前已廢棄,所以不具體介紹,其作用是用來(lái)控制組件的依賴加載。

2. 入門示例
接下來(lái)通過(guò)下面簡(jiǎn)單示例快速了解一下「如何創(chuàng)建一個(gè)簡(jiǎn)單 Web Components 組件」。
使用組件
<!DOCTYPE html>
<html lang="en">
<head>
<script src="./index.js" defer></script>
</head>
<body>
<h1>custom-element-start</h1>
<custom-element-start></custom-element-start>
</body>
</html>
定義組件
/**
* 使用 CustomElementRegistry.define() 方法用來(lái)注冊(cè)一個(gè) custom element
* 參數(shù)如下:
* - 元素名稱,符合 DOMString 規(guī)范,名稱不能是單個(gè)單詞,且必須用短橫線隔開(kāi)
* - 元素行為,必須是一個(gè)類
* - 繼承元素,可選配置,一個(gè)包含 extends 屬性的配置對(duì)象,指定創(chuàng)建的元素繼承自哪個(gè)內(nèi)置元素,可以繼承任何內(nèi)置元素。
*/
class CustomElementStart extends HTMLElement {
constructor(){
super();
this.render();
}
render(){
const shadow = this.attachShadow({mode: 'open'});
const text = document.createElement("span");
text.textContent = 'Hi Custom Element!';
text.style = 'color: red';
shadow.append(text);
}
}
customElements.define('custom-element-start', CustomElementStart)
上面代碼主要做 3 件事:
實(shí)現(xiàn)組件類
通過(guò)實(shí)現(xiàn) CustomElementStart 類來(lái)定義組件。
定義組件
將組件的標(biāo)簽和組件類作為參數(shù),通過(guò) customElements.define 方法定義組件。
使用組件
導(dǎo)入組件后,跟使用普通 HTML 標(biāo)簽一樣直接使用自定義組件 <custom-element-start></custom-element-start>。
隨后瀏覽器訪問(wèn) index.html 可以看到下面內(nèi)容:
3. 兼容性介紹
在 MDN | Web Components 章節(jié)中介紹了其兼容性情況:
Firefox(版本63)、Chrome和Opera都默認(rèn)支持Web組件。 Safari支持許多web組件特性,但比上述瀏覽器少。 Edge正在開(kāi)發(fā)一個(gè)實(shí)現(xiàn)。
關(guān)于兼容性,可以看下圖:
圖片來(lái)源:https://www.webcomponents.org/
這個(gè)網(wǎng)站里面,有很多關(guān)于 Web Components 的優(yōu)秀項(xiàng)目可以學(xué)習(xí)。
4. 小結(jié)
這節(jié)主要通過(guò)一個(gè)簡(jiǎn)單示例,簡(jiǎn)單回顧基礎(chǔ)知識(shí),詳細(xì)可以閱讀文檔:
使用 custom elements 使用 shadow DOM 使用 templates and slots

二、EXE-Components 組件庫(kù)分析設(shè)計(jì)
1. 背景介紹
假設(shè)我們需要實(shí)現(xiàn)一個(gè) EXE-Components 組件庫(kù),該組件庫(kù)的組件分 2 大類:
components 類型
以「通用簡(jiǎn)單組件」為主,如exe-avatar頭像組件、 exe-button按鈕組件等;
modules 類型
以「復(fù)雜、組合組件」為主,如exe-user-avatar用戶頭像組件(含用戶信息)、exe-attachement-list附件列表組件等等。
詳細(xì)可以看下圖:
接下來(lái)我們會(huì)基于上圖進(jìn)行 EXE-Components 組件庫(kù)設(shè)計(jì)和開(kāi)發(fā)。
2. 組件庫(kù)設(shè)計(jì)
在設(shè)計(jì)組件庫(kù)的時(shí)候,主要需要考慮以下幾點(diǎn):
組件命名、參數(shù)命名等規(guī)范,方便組件后續(xù)維護(hù); 組件參數(shù)定義; 組件樣式隔離;
當(dāng)然,這幾個(gè)是最基礎(chǔ)需要考慮的點(diǎn),隨著實(shí)際業(yè)務(wù)的復(fù)雜,還需要考慮更多,比如:工程化相關(guān)、組件解耦、組件主題等等。
針對(duì)前面提到這 3 點(diǎn),這邊約定幾個(gè)命名規(guī)范:
組件名稱以 exe-功能名稱進(jìn)行命名,如exe-avatar表示頭像組件;屬性參數(shù)名稱以 e-參數(shù)名稱進(jìn)行命名,如e-src表示src地址屬性;事件參數(shù)名稱以 on-事件類型進(jìn)行命名,如on-click表示點(diǎn)擊事件;
3. 組件庫(kù)組件設(shè)計(jì)
這邊我們主要設(shè)計(jì) exe-avatar 、exe-button 和 exe-user-avatar三個(gè)組件,前兩個(gè)為簡(jiǎn)單組件,后一個(gè)為復(fù)雜組件,其內(nèi)部使用了前兩個(gè)組件進(jìn)行組合。這邊先定義這三個(gè)組件支持的屬性:
這邊屬性命名看著會(huì)比較復(fù)雜,大家可以按照自己和團(tuán)隊(duì)的習(xí)慣進(jìn)行命名。
這樣我們思路就清晰很多,實(shí)現(xiàn)對(duì)應(yīng)組件即可。
三、EXE-Components 組件庫(kù)準(zhǔn)備工作
本文示例最終將對(duì)實(shí)現(xiàn)的組件進(jìn)行「組合使用」,實(shí)現(xiàn)下面「「用戶列表」」效果:
體驗(yàn)地址:https://blog.pingan8787.com/exe-components/demo.html
1. 統(tǒng)一開(kāi)發(fā)規(guī)范
首先我們先統(tǒng)一開(kāi)發(fā)規(guī)范,包括:
目錄規(guī)范

定義組件規(guī)范

組件開(kāi)發(fā)模版
組件開(kāi)發(fā)模版分 index.js「組件入口文件」和 template.js 「組件 HTML 模版文件」:
// index.js 模版
const defaultConfig = {
// 組件默認(rèn)配置
}
const Selector = "exe-avatar"; // 組件標(biāo)簽名
export default class EXEAvatar extends HTMLElement {
shadowRoot = null;
config = defaultConfig;
constructor(){
super();
this.render(); // 統(tǒng)一處理組件初始化邏輯
}
render() {
this.shadowRoot = this.attachShadow({mode: 'closed'});
this.shadowRoot.innerHTML = renderTemplate(this.config);
}
}
// 定義組件
if (!customElements.get(Selector)) {
customElements.define(Selector, EXEAvatar)
}
// template.js 模版
export default config => {
// 統(tǒng)一讀取配置
const { avatarWidth, avatarRadius, avatarSrc } = config;
return `
<style>
/* CSS 內(nèi)容 */
</style>
<div class="exe-avatar">
/* HTML 內(nèi)容 */
</div>
`
}
2. 開(kāi)發(fā)環(huán)境搭建和工程化處理
為了方便使用 EXE-Components 組件庫(kù),更接近實(shí)際組件庫(kù)的使用,我們需要將組件庫(kù)打包成一個(gè) UMD 類型的 js 文件。這邊我們使用 rollup 進(jìn)行構(gòu)建,最終打包成 exe-components.js 的文件,使用方式如下:
<script src="./exe-components.js"></script>
接下來(lái)通過(guò) npm init -y生成 package.json文件,然后全局安裝 rollup 和 http-server(用來(lái)啟動(dòng)本地服務(wù)器,方便調(diào)試):
npm init -y
npm install --global rollup http-server
然后在 package.json的 script 下添加 "dev"和 "build"腳本:
{
// ...
"scripts": {
"dev": "http-server -c-1 -p 1400",
"build": "rollup index.js --file exe-components.js --format iife"
},
}
其中:
"dev"命令:通過(guò) http-server 啟動(dòng)靜態(tài)服務(wù)器,作為開(kāi)發(fā)環(huán)境使用。添加-c-1參數(shù)用來(lái)禁用緩存,避免刷新頁(yè)面還會(huì)有緩存,詳細(xì)可以看 http-server 文檔;"build"命令:將 index.js 作為 rollup 打包的入口文件,輸出exe-components.js文件,并且是 iife 類型的文件。
這樣就完成簡(jiǎn)單的本地開(kāi)發(fā)和組件庫(kù)構(gòu)建的工程化配置,接下來(lái)就可以進(jìn)行開(kāi)發(fā)了。
四、EXE-Components 組件庫(kù)開(kāi)發(fā)
1. 組件庫(kù)入口文件配置
前面 package.json 文件中配置的 "build" 命令,會(huì)使用根目錄下 index.js 作為入口文件,并且為了方便 components 通用基礎(chǔ)組件和 modules 通用復(fù)雜組件的引入,我們創(chuàng)建 3 個(gè) index.js,創(chuàng)建后目錄結(jié)構(gòu)如下:
三個(gè)入口文件內(nèi)容分別如下:
// EXE-Components/index.js
import './components/index.js';
import './modules/index.js';
// EXE-Components/components/index.js
import './exe-avatar/index.js';
import './exe-button/index.js';
// EXE-Components/modules/index.js
import './exe-attachment-list/index.js.js';
import './exe-comment-footer/index.js.js';
import './exe-post-list/index.js.js';
import './exe-user-avatar/index.js';
2. 開(kāi)發(fā) exe-avatar 組件 index.js 文件
通過(guò)前面的分析,我們可以知道 exe-avatar組件需要支持參數(shù):
e-avatar-src:頭像圖片地址,例如:./testAssets/images/avatar-1.png e-avatar-width:頭像寬度,默認(rèn)和高度一致,例如:52px e-button-radius:頭像圓角,例如:22px,默認(rèn):50% on-avatar-click:頭像點(diǎn)擊事件,默認(rèn)無(wú)
接著按照之前的模版,開(kāi)發(fā)入口文件 index.js :
// EXE-Components/components/exe-avatar/index.js
import renderTemplate from './template.js';
import { Shared, Utils } from '../../utils/index.js';
const { getAttributes } = Shared;
const { isStr, runFun } = Utils;
const defaultConfig = {
avatarWidth: "40px",
avatarRadius: "50%",
avatarSrc: "./assets/images/default_avatar.png",
onAvatarClick: null,
}
const Selector = "exe-avatar";
export default class EXEAvatar extends HTMLElement {
shadowRoot = null;
config = defaultConfig;
constructor(){
super();
this.render();
}
render() {
this.shadowRoot = this.attachShadow({mode: 'closed'});
this.shadowRoot.innerHTML = renderTemplate(this.config);// 生成 HTML 模版內(nèi)容
}
// 生命周期:當(dāng) custom element首次被插入文檔DOM時(shí),被調(diào)用。
connectedCallback() {
this.updateStyle();
this.initEventListen();
}
updateStyle() {
this.config = {...defaultConfig, ...getAttributes(this)};
this.shadowRoot.innerHTML = renderTemplate(this.config); // 生成 HTML 模版內(nèi)容
}
initEventListen() {
const { onAvatarClick } = this.config;
if(isStr(onAvatarClick)){ // 判斷是否為字符串
this.addEventListener('click', e => runFun(e, onAvatarClick));
}
}
}
if (!customElements.get(Selector)) {
customElements.define(Selector, EXEAvatar)
}
其中有幾個(gè)方法是抽取出來(lái)的公用方法,大概介紹下其作用,具體可以看源碼:
renderTemplate方法
來(lái)自 template.js 暴露的方法,傳入配置 config,來(lái)生成 HTML 模版。
getAttributes方法
傳入一個(gè) HTMLElement 元素,返回該元素上所有屬性鍵值對(duì),其中會(huì)對(duì) e- 和 on- 開(kāi)頭的屬性,分別處理成普通屬性和事件屬性,示例如下:
// input
<exe-avatar
e-avatar-src="./testAssets/images/avatar-1.png"
e-avatar-width="52px"
e-avatar-radius="22px"
on-avatar-click="avatarClick()"
></exe-avatar>
// output
{
avatarSrc: "./testAssets/images/avatar-1.png",
avatarWidth: "52px",
avatarRadius: "22px",
avatarClick: "avatarClick()"
}
runFun方法
由于通過(guò)屬性傳遞進(jìn)來(lái)的方法,是個(gè)字符串,所以進(jìn)行封裝,傳入 event 和事件名稱作為參數(shù),調(diào)用該方法,示例和上一步一樣,會(huì)執(zhí)行 avatarClick() 方法。
另外,Web Components 生命周期可以詳細(xì)看文檔:使用生命周期回調(diào)函數(shù)。
3. 開(kāi)發(fā) exe-avatar 組件 template.js 文件
該文件暴露一個(gè)方法,返回組件 HTML 模版:
// EXE-Components/components/exe-avatar/template.js
export default config => {
const { avatarWidth, avatarRadius, avatarSrc } = config;
return `
<style>
.exe-avatar {
width: ${avatarWidth};
height: ${avatarWidth};
display: inline-block;
cursor: pointer;
}
.exe-avatar .img {
width: 100%;
height: 100%;
border-radius: ${avatarRadius};
border: 1px solid #efe7e7;
}
</style>
<div class="exe-avatar">
<img class="img" src="${avatarSrc}" />
</div>
`
}
最終實(shí)現(xiàn)效果如下:
開(kāi)發(fā)完第一個(gè)組件,我們可以簡(jiǎn)單總結(jié)一下創(chuàng)建和使用組件的步驟:
4. 開(kāi)發(fā) exe-button 組件
按照前面 exe-avatar組件開(kāi)發(fā)思路,可以很快實(shí)現(xiàn) exe-button 組件。需要支持下面參數(shù):
e-button-radius:按鈕圓角,例如:8px e-button-type:按鈕類型,例如:default, primary, text, dashed e-button-text:按鈕文本,默認(rèn):打開(kāi) on-button-click:按鈕點(diǎn)擊事件,默認(rèn)無(wú)
// EXE-Components/components/exe-button/index.js
import renderTemplate from './template.js';
import { Shared, Utils } from '../../utils/index.js';
const { getAttributes } = Shared;
const { isStr, runFun } = Utils;
const defaultConfig = {
buttonRadius: "6px",
buttonPrimary: "default",
buttonText: "打開(kāi)",
disableButton: false,
onButtonClick: null,
}
const Selector = "exe-button";
export default class EXEButton extends HTMLElement {
// 指定觀察到的屬性變化,attributeChangedCallback 會(huì)起作用
static get observedAttributes() {
return ['e-button-type','e-button-text', 'buttonType', 'buttonText']
}
shadowRoot = null;
config = defaultConfig;
constructor(){
super();
this.render();
}
render() {
this.shadowRoot = this.attachShadow({mode: 'closed'});
}
connectedCallback() {
this.updateStyle();
this.initEventListen();
}
attributeChangedCallback (name, oldValue, newValue) {
// console.log('屬性變化', name)
}
updateStyle() {
this.config = {...defaultConfig, ...getAttributes(this)};
this.shadowRoot.innerHTML = renderTemplate(this.config);
}
initEventListen() {
const { onButtonClick } = this.config;
if(isStr(onButtonClick)){
const canClick = !this.disabled && !this.loading
this.addEventListener('click', e => canClick && runFun(e, onButtonClick));
}
}
get disabled () {
return this.getAttribute('disabled') !== null;
}
get type () {
return this.getAttribute('type') !== null;
}
get loading () {
return this.getAttribute('loading') !== null;
}
}
if (!customElements.get(Selector)) {
customElements.define(Selector, EXEButton)
}
模版定義如下:
// EXE-Components/components/exe-button/tempalte.js
// 按鈕邊框類型
const borderStyle = { solid: 'solid', dashed: 'dashed' };
// 按鈕類型
const buttonTypeMap = {
default: { textColor: '#222', bgColor: '#FFF', borderColor: '#222'},
primary: { textColor: '#FFF', bgColor: '#5FCE79', borderColor: '#5FCE79'},
text: { textColor: '#222', bgColor: '#FFF', borderColor: '#FFF'},
}
export default config => {
const { buttonRadius, buttonText, buttonType } = config;
const borderStyleCSS = buttonType
&& borderStyle[buttonType]
? borderStyle[buttonType]
: borderStyle['solid'];
const backgroundCSS = buttonType
&& buttonTypeMap[buttonType]
? buttonTypeMap[buttonType]
: buttonTypeMap['default'];
return `
<style>
.exe-button {
border: 1px ${borderStyleCSS} ${backgroundCSS.borderColor};
color: ${backgroundCSS.textColor};
background-color: ${backgroundCSS.bgColor};
font-size: 12px;
text-align: center;
padding: 4px 10px;
border-radius: ${buttonRadius};
cursor: pointer;
display: inline-block;
height: 28px;
}
:host([disabled]) .exe-button{
cursor: not-allowed;
pointer-events: all;
border: 1px solid #D6D6D6;
color: #ABABAB;
background-color: #EEE;
}
:host([loading]) .exe-button{
cursor: not-allowed;
pointer-events: all;
border: 1px solid #D6D6D6;
color: #ABABAB;
background-color: #F9F9F9;
}
</style>
<button class="exe-button">${buttonText}</button>
`
}
最終效果如下:
5. 開(kāi)發(fā) exe-user-avatar 組件
該組件是將前面 exe-avatar 組件和 exe-button 組件進(jìn)行組合,不僅需要支持「點(diǎn)擊事件」,還需要支持「插槽 slot 功能」。由于是做組合,所以開(kāi)發(fā)起來(lái)比較簡(jiǎn)單~先看看入口文件:
// EXE-Components/modules/exe-user-avatar/index.js
import renderTemplate from './template.js';
import { Shared, Utils } from '../../utils/index.js';
const { getAttributes } = Shared;
const { isStr, runFun } = Utils;
const defaultConfig = {
userName: "",
subName: "",
disableButton: false,
onAvatarClick: null,
onButtonClick: null,
}
export default class EXEUserAvatar extends HTMLElement {
shadowRoot = null;
config = defaultConfig;
constructor() {
super();
this.render();
}
render() {
this.shadowRoot = this.attachShadow({mode: 'open'});
}
connectedCallback() {
this.updateStyle();
this.initEventListen();
}
initEventListen() {
const { onAvatarClick } = this.config;
if(isStr(onAvatarClick)){
this.addEventListener('click', e => runFun(e, onAvatarClick));
}
}
updateStyle() {
this.config = {...defaultConfig, ...getAttributes(this)};
this.shadowRoot.innerHTML = renderTemplate(this.config);
}
}
if (!customElements.get('exe-user-avatar')) {
customElements.define('exe-user-avatar', EXEUserAvatar)
}
主要內(nèi)容在 template.js 中:
// EXE-Components/modules/exe-user-avatar/template.js
import { Shared } from '../../utils/index.js';
const { renderAttrStr } = Shared;
export default config => {
const {
userName, avatarWidth, avatarRadius, buttonRadius,
avatarSrc, buttonType = 'primary', subName, buttonText, disableButton,
onAvatarClick, onButtonClick
} = config;
return `
<style>
:host{
color: "green";
font-size: "30px";
}
.exe-user-avatar {
display: flex;
margin: 4px 0;
}
.exe-user-avatar-text {
font-size: 14px;
flex: 1;
}
.exe-user-avatar-text .text {
color: #666;
}
.exe-user-avatar-text .text span {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
exe-avatar {
margin-right: 12px;
width: ${avatarWidth};
}
exe-button {
width: 60px;
display: flex;
justify-content: end;
}
</style>
<div class="exe-user-avatar">
<exe-avatar
${renderAttrStr({
'e-avatar-width': avatarWidth,
'e-avatar-radius': avatarRadius,
'e-avatar-src': avatarSrc,
})}
></exe-avatar>
<div class="exe-user-avatar-text">
<div class="name">
<span class="name-text">${userName}</span>
<span class="user-attach">
<slot name="name-slot"></slot>
</span>
</div>
<div class="text">
<span class="name">${subName}<slot name="sub-name-slot"></slot></span>
</div>
</div>
${
!disableButton &&
`<exe-button
${renderAttrStr({
'e-button-radius' : buttonRadius,
'e-button-type' : buttonType,
'e-button-text' : buttonText,
'on-avatar-click' : onAvatarClick,
'on-button-click' : onButtonClick,
})}
></exe-button>`
}
</div>
`
}
其中 renderAttrStr 方法接收一個(gè)屬性對(duì)象,返回其鍵值對(duì)字符串:
// input
{
'e-avatar-width': 100,
'e-avatar-radius': 50,
'e-avatar-src': './testAssets/images/avatar-1.png',
}
// output
"e-avatar-width='100' e-avatar-radius='50' e-avatar-src='./testAssets/images/avatar-1.png' "
最終效果如下:
6. 實(shí)現(xiàn)一個(gè)用戶列表業(yè)務(wù)
接下來(lái)我們通過(guò)一個(gè)實(shí)際業(yè)務(wù),來(lái)看看我們組件的效果:
其實(shí)實(shí)現(xiàn)也很簡(jiǎn)單,根據(jù)給定數(shù)據(jù),然后循環(huán)使用組件即可,假設(shè)有以下用戶數(shù)據(jù):
const users = [
{"name":"前端早早聊","desc":"幫 5000 個(gè)前端先跑 @ 前端早早聊","level":6,"avatar":"qdzzl.jpg","home":"https://juejin.cn/user/712139234347565"}
{"name":"來(lái)自拉夫德魯?shù)拇a農(nóng)","desc":"誰(shuí)都不救我,誰(shuí)都救不了我,就像我救不了任何人一樣","level":2,"avatar":"lzlfdldmn.jpg","home":"https://juejin.cn/user/994371074524862"}
{"name":"黑色的楓","desc":"永遠(yuǎn)懷著一顆學(xué)徒的心。。。","level":3,"avatar":"hsdf.jpg","home":"https://juejin.cn/user/2365804756348103"}
{"name":"captain_p","desc":"目的地很美好,路上的風(fēng)景也很好。今天增長(zhǎng)見(jiàn)識(shí)了嗎","level":2,"avatar":"cap.jpg","home":"https://juejin.cn/user/2532902235026439"}
{"name":"CUGGZ","desc":"文章聯(lián)系微信授權(quán)轉(zhuǎn)載。微信:CUG-GZ,添加好友一起學(xué)習(xí)~","level":5,"avatar":"cuggz.jpg","home":"https://juejin.cn/user/3544481220801815"}
{"name":"政采云前端團(tuán)隊(duì)","desc":"政采云前端 ZooTeam 團(tuán)隊(duì),不摻水的原創(chuàng)。 團(tuán)隊(duì)站點(diǎn):https://zoo.team","level":6,"avatar":"zcy.jpg","home":"https://juejin.cn/user/3456520257288974"}
]
我們就可以通過(guò)簡(jiǎn)單 for 循環(huán)拼接 HTML 片段,然后添加到頁(yè)面某個(gè)元素中:
// 測(cè)試生成用戶列表模版
const usersTemp = () => {
let temp = '', code = '';
users.forEach(item => {
const {name, desc, level, avatar, home} = item;
temp +=
`
<exe-user-avatar
e-user-name="${name}"
e-sub-name="${desc}"
e-avatar-src="./testAssets/images/users/${avatar}"
e-avatar-width="36px"
e-button-type="primary"
e-button-text="關(guān)注"
on-avatar-click="toUserHome('${home}')"
on-button-click="toUserFollow('${name}')"
>
${
level >= 0 && `<span slot="name-slot">
<span class="medal-item">(Lv${level})</span>
</span>`}
</exe-user-avatar>
`
})
return temp;
}
document.querySelector('#app').innerHTML = usersTemp;
到這邊我們就實(shí)現(xiàn)了一個(gè)用戶列表的業(yè)務(wù),當(dāng)然實(shí)際業(yè)務(wù)可能會(huì)更加復(fù)雜,需要再優(yōu)化。
五、總結(jié)
本文首先簡(jiǎn)單回顧 Web Components 核心 API,然后對(duì)組件庫(kù)需求進(jìn)行分析設(shè)計(jì),再進(jìn)行環(huán)境搭建和開(kāi)發(fā),內(nèi)容比較多,可能沒(méi)有每一點(diǎn)都講到,還請(qǐng)大家看看我倉(cāng)庫(kù)的源碼,有什么問(wèn)題歡迎和我討論。寫本文的幾個(gè)核心目的:
當(dāng)我們接到一個(gè)新任務(wù)的時(shí)候,需要從分析設(shè)計(jì)開(kāi)始,再到開(kāi)發(fā),而不是盲目一上來(lái)就開(kāi)始開(kāi)發(fā); 帶大家一起看看如何用 Web Components 開(kāi)發(fā)簡(jiǎn)單的業(yè)務(wù)組件庫(kù); 體驗(yàn)一下 Web Components 開(kāi)發(fā)組件庫(kù)有什么缺點(diǎn)(就是要寫的東西太多了)。
最后看完本文,大家是否覺(jué)得用 Web Components 開(kāi)發(fā)組件庫(kù),實(shí)在有點(diǎn)復(fù)雜?要寫的太多了。沒(méi)關(guān)系,下一篇我將帶大家一起使用 Stencil 框架開(kāi)發(fā) Web Components 標(biāo)準(zhǔn)的組件庫(kù),畢竟整個(gè) ionic 已經(jīng)是使用 Stencil 重構(gòu),Web Components 大勢(shì)所趨~!
拓展閱讀
WEBCOMPONENTS.ORG Discuss & share web components Web Components as Technology Stenciljs - Build. Customize. Distribute. Adopt.

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 130+ 篇原創(chuàng)文章
