慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

博客园 - 司徒正美
V
V2EX
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
月光博客
月光博客
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI

阮一峰的网络日志

科技爱好者周刊(第 396 期):互联网通信的替代方案 科技爱好者周刊(第 396 期):互联网通信的替代方案 - 阮一峰的网络日志 科技爱好者周刊(第 395 期):软件开发的第三种方式 科技爱好者周刊(第 395 期):软件开发的第三种方式 - 阮一峰的网络日志 科技爱好者周刊(第 393 期):脑腐状态 科技爱好者周刊(第 392 期):axios 投毒与好莱坞式骗术 科技爱好者周刊(第 391 期):AI 的贫富分化 科技爱好者周刊(第 390 期):没有语料,大模型就是智障 套壳中国大模型撑起500亿美元估值?扒一扒 Cursor 的"套壳"疑云 科技爱好者周刊(第 389 期):未来如何招聘程序员 科技爱好者周刊(第 388 期):测试是新的护城河 零安装的"云养虾":ArkClaw 使用指南 科技爱好者周刊(第 387 期):你是领先的 科技爱好者周刊(第 386 期):当外卖员接入 AI 字节全家桶 Seed 2.0 + TRAE 玩转 Skill 科技爱好者周刊(第 385 期):马斯克害怕中国车企吗? 智谱旗舰 GLM-5 实测:对比 Opus 4.6 和 GPT-5.3-Codex 科技爱好者周刊(第 384 期):为什么软件股下跌 科技爱好者周刊(第 383 期):你是第几级 AI 编程 Kimi 的一体化,Manus 的分层 科技爱好者周刊(第 382 期):独立软件的黄昏 AI native Workspace 也许是智能体的下一阶段 科技爱好者周刊(第 381 期):中国 AI 大模型领导者在想什么 科技爱好者周刊(第 380 期):为什么人们拥抱"不对称收益" 科技爱好者周刊(第 379 期):《硅谷钢铁侠》摘录 我如何用 AI 处理历史遗留代码:MiniMax M2.1 升级体验 科技爱好者周刊(第 378 期):预测是新的互联网热点 科技爱好者周刊(第 377 期):14万美元的贫困线 科技爱好者周刊(第 376 期):太空数据中心的争议 科技爱好者周刊(第 375 期):一扇门的 Bug 终于有人做了 Subagent,TRAE 国内版 SOLO 模式来了 科技爱好者周刊(第 374 期):6GHz 的问题 VS Code 使用国产大模型 MiniMax M2 教程 科技爱好者周刊(第 373 期):数据模型是新产品的核心 国产大模型接入 Claude Code 教程:以 Doubao-Seed-Code 为例 科技爱好者周刊(第 372 期):软件界面如何设计 大模型比拼:MiniMax M2 vs GLM 4.6 vs Claude Sonnet 4.5 科技爱好者周刊(第 371 期):一个乐观主义者的专访 科技爱好者周刊(第 370 期):正确的代码高亮 错误处理:异常好于状态码 科技爱好者周刊(第 369 期):Tim 与罗永浩的对谈 科技爱好者周刊(第 368 期):不要这样管理软件团队 一天之内,智谱和 Anthropic 都发了最强编程模型 科技爱好者周刊(第 367 期):Nano Banana 的几个妙用 科技爱好者周刊(第 366 期):旧金山疯狂的 AI 广告 科技爱好者周刊(第 365 期):流量变现正在崩塌 科技爱好者周刊(第 364 期):最难还原的魔方 科技爱好者周刊(第 363 期):最好懂的神经网络解释 科技爱好者周刊(第 362 期):GitHub 工程师谈系统设计 科技爱好者周刊(第 361 期):暗网 Tor 安全吗?
Web Components 入門實例教程
阮一峰 · 2019-08-06 · via 阮一峰的网络日志

組件是前端的發展方向,現在流行的 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 = '[email protected]';

    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">[email protected]</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">[email protected]</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="[email protected]"
></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 默認與外部 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 的高級用法,可以接著學習下面兩篇文章。

九、參考鏈接

(完)