打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
自定義元素:在 HTML 中定義新元素
翻譯: 米粽 (Leo Deng)
Comments: 1

This article discusses APIs that are not yet fully standardized and still in flux. Be cautious when using experimental APIs in your own projects.

引言

現在的 web 嚴重缺乏表達能力。你只要瞧一眼「現代」的 web 應用,比如 GMail,就會明白我的意思:

現代 web 應用:使用 <div> 堆砌而成。

堆砌 <div> 一點都不現代。然而可悲的是,這就是我們構建 web 應用的方式。在現有基礎上我們不應該有更高的追求嗎?

時髦的標記,行動起來!

HTML 為我們提供了一個完美的文檔組織工具,然而 HTML 規範定義的元素卻很有限。

假如 GMail 的標記不那麼糟糕,而是像下面這樣漂亮,那會怎樣?

<hangout-module>
  <hangout-chat from="Paul, Addy">
    <hangout-discussion>
      <hangout-message from="Paul" profile="profile.png"
          profile="118075919496626375791" datetime="2013-07-17T12:02">
        <p>Feelin' this Web Components thing.</p>
        <p>Heard of it?</p>
      </hangout-message>
    </hangout-discussion>
  </hangout-chat>
  <hangout-chat>...</hangout-chat>
</hangout-module>

真是令人耳目一新!這個應用太合理了,既有意義,又容易理解。最妙的是,它是可維護的,只要查看聲明結構就可以清楚地知道它的作用。

自定義元素,救救我們!就指望你了!

趕緊開始吧

自定義元素 允許開發者定義新的 HTML 元素類型。該規範只是 Web 組件模塊提供的眾多新 API 中的一個,但它也很可能是最重要的一個。沒有自定義元素帶來的以下特性,Web 組件都不會存在:

  1. 定義新的 HTML/DOM 元素
  2. 基於其他元素創建擴展元素
  3. 給一個標籤綁定一組自定義功能
  4. 擴展已有 DOM 元素的 API

註冊新元素

使用 document.registerElement() 可以創建一個自定義元素:

var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());

document.registerElement() 的第一個參數是元素的標籤名。這個標籤名必須包括一個連字符(-)。因此,諸如 <x-tags><my-element><my-awesome-app> 都是合法的標籤名,而 <tabs><foo_bar> 則不是。這個限定使解析器能很容易地區分自定義元素和 HTML 規範定義的元素,同時確保了 HTML 增加新標籤時的向前兼容。

第二個參數是一個(可選的)對象,用於描述該元素的 prototype。在這裡可以為元素添加自定義功能(例如:公開屬性和方法)。稍後詳述。

自定義元素默認繼承自 HTMLElement,因此上一個示例等同於:

var XFoo = document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype)
});

調用 document.registerElement('x-foo') 向瀏覽器註冊了這個新元素,並返回一個可以用來創建 <x-foo> 元素實例的構造器。如果你不想使用構造器,也可以使用其他實例化元素的技術。

如果你不希望在 window 全局對象中創建元素構造器,還可以把它放進命名空間(var myapp = {}; myapp.XFoo = document.registerElement('x-foo');)。

擴展原生元素

假設平淡無奇的原生 <button> 元素不能滿足你的需求,你想將其增強為一個「超級按鈕」,可以通過創建一個繼承 HTMLButtonElement.prototype 的新元素,來擴展 <button> 元素:

var MegaButton = document.registerElement('mega-button', {
  prototype: Object.create(HTMLButtonElement.prototype)
});

要創建擴展自元素 B元素 A元素 A 必須繼承元素 Bprototype

這類自定義元素被稱為類型擴展自定義元素。它們以繼承某個特定 HTMLElement 的方式表達了「元素 X 是一個 Y」。

示例:

<button is="mega-button">

元素如何提升

你有沒有想過為什麼 HTML 解析器對非標準標籤不報錯?比如,我們在頁面中聲明一個 <randomtag>,一切都很和諧。根據 HTML 規範的表述:

非規範定義的元素必須使用 HTMLUnknownElement 接口。 HTML 規範

<randomtag> 是非標準的,它會繼承 HTMLUnknownElement

對自定義元素來說,情況就不一樣了。擁有合法元素名的自定義元素將繼承 HTMLElement你可以按 Ctrl+Shift+J(Mac 系統為 Cmd+Opt+J)打開控制台,運行下面這段代碼,得到的結果將是 true

// 「tabs」不是一個合法的自定義元素名
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype

// 「x-tabs」是一個合法的自定義元素名
document.createElement('x-tabs').__proto__ == HTMLElement.prototype

在不支持 document.registerElement() 的瀏覽器中,<x-tabs> 仍為 HTMLUnknownElement

Unresolved 元素

由於自定義元素是通過腳本執行 document.registerElement() 註冊的,因此 它們可能在元素定義被註冊到瀏覽器之前就已經聲明或創建過了。例如:你可以先在頁面中聲明 <x-tabs>,以後再調用 document.registerElement('x-tabs')

在被提升到其定義之前,這些元素被稱為 unresolved 元素。它們是擁有合法自定義元素名的 HTML 元素,只是還沒有註冊成為自定義元素。

下面這個表格看起來更直觀一些:

類型繼承自示例
unresolved 元素HTMLElement<x-tabs><my-element><my-awesome-app>
未知元素HTMLUnknownElement<tabs><foo_bar>
把 unresolved 元素想像成尚處於中間狀態,它們都是等待被瀏覽器提升的潛在候選者。瀏覽器說:「你具備一個新元素的全部特徵,我保證會在賦予你定義的時候將你提升為一個元素」。

實例化元素

我們創建普通元素用到的一些技術也可以用於自定義元素。和所有標準定義的元素一樣,自定義元素既可以在 HTML 中聲明,也可以通過 JavaScript 在 DOM 中創建。

實例化自定義標籤

聲明元素:

<x-foo></x-foo>

在 JS 中創建 DOM

var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
  alert('Thanks!');
});

使用 new 操作符

var xFoo = new XFoo();
document.body.appendChild(xFoo);

實例化類型擴展元素

實例化類型擴展自定義元素的方法和自定義標籤驚人地相似。

聲明元素:

<!-- <button> 「是一個」超級按鈕 -->
<button is="mega-button">

在 JS 中創建 DOM

var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true

看,這是接收第二個參數為 is="" 屬性的 document.createElement() 重載版本。

使用 new 操作符

var megaButton = new MegaButton();
document.body.appendChild(megaButton);

現在,我們已經學習了如何使用 document.registerElement() 來向瀏覽器註冊一個新標籤。但這還不夠,接下來我們要向新標籤添加屬性和方法。

添加 JS 屬性和方法

自定義元素最強大的地方在於,你可以在元素定義中加入屬性和方法,給元素綁定特定的功能。你可以把它想像成一種給你的元素創建公開 API 的方法。

這裡有一個完整的示例:

var XFooProto = Object.create(HTMLElement.prototype);

// 1. 為 x-foo 創建 foo() 方法
XFooProto.foo = function() {
  alert('foo() called');
};

// 2. 定義一個只讀的「bar」屬性
Object.defineProperty(XFooProto, "bar", {value: 5});

// 3. 註冊 x-foo 的定義
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});

// 4. 創建一個 x-foo 實例
var xfoo = document.createElement('x-foo');

// 5. 插入頁面
document.body.appendChild(xfoo);

構造 prototype 的方法多種多樣,如果你不喜歡上面這種方式,再看一個更簡潔的例子:

var XFoo = document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    bar: {
      get: function() { return 5; }
    },
    foo: {
      value: function() {
        alert('foo() called');
      }
    }
  })
});

以上兩種方式,第一種使用了 ES5 的 Object.defineProperty,第二種則使用了 get/set

生命週期回調方法

元素可以定義特殊的方法,來注入其生存期內關鍵的時間點。這些方法各自有特定的名稱和用途,它們被恰如其分地命名為生命週期回調

回調名稱 調用時間點
createdCallback 創建元素實例
attachedCallback 向文檔插入實例
detachedCallback 從文檔中移除實例
attributeChangedCallback(attrName, oldVal, newVal) 添加,移除,或修改一個屬性

示例:<x-foo> 定義 createdCallback()attachedCallback()

var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};

var XFoo = document.registerElement('x-foo', {prototype: proto});

所有生命週期回調都是可選的,你可以只在需要關注的時間點定義它們。例如:假設你有一個很複雜的元素,它會在 createdCallback() 打開一個 IndexedDB 連接。在將其從 DOM 移除時,detachedCallback() 會做一些必要的清理工作。注意:不要過於依賴這些生命週期方法(比如用戶直接關閉瀏覽器標籤),僅將其作為可能的優化點。

另一個生命週期回調的例子是為元素設置默認的事件監聽器:

proto.createdCallback = function() {
  this.addEventListener('click', function(e) {
    alert('Thanks!');
  });
};
如果你的元素太笨重,是不會有人用它的。生命週期回調可以幫你大忙!

添加標記

我們已經創建好 <x-foo> 並添加了 JavaScript API,但它還沒有任何內容。不如我們給點 HTML 讓它渲染?

生命週期回調在這個時候就派上用場了。我們甚至可以用 createdCallback() 給一個元素賦予一些默認的 HTML:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
  this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
};

var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});
I'm an x-foo-with-markup!

實例化這個標籤並在 DevTools 中觀察(右擊,選擇「審查元素」),可以看到如下結構:

<x-foo-with-markup>
   <b>I'm an x-foo-with-markup!</b>
 </x-foo-with-markup>

用 Shadow DOM 封裝內部實現

Shadow DOM 本身是一個封裝內容的強大工具,配合使用自定義元素就更神奇了!

Shadow DOM 為自定義元素提供了:

  1. 一種隱藏內部實現的方法,從而將用戶與血淋淋的實現細節隔離開。
  2. 簡單有效的樣式隔離

從 Shadow DOM 創建元素,跟創建一個渲染基礎標記的元素非常類似,區別在於 createdCallback() 回調:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
  // 1. 為元素附加一個 shadow root。
  var shadow = this.createShadowRoot();

  // 2. 填入標記。
  shadow.innerHTML = "<b>I'm in the element's Shadow DOM!</b>";
};

var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});

我們並沒有直接設置 <x-foo-shadowdom>innerHTML,而是為其創建了一個用於填充標記的 Shadow Root。在 DevTools 設置中勾選「Show Shadow DOM」,你就會看到一個可以展開的 #shadow-root

<x-foo-shadowdom>
   ?#shadow-root
     <b>I'm in the element's Shadow DOM!</b>
 </x-foo-shadowdom>

這就是 Shadow Root!

從模板創建元素

HTML 模板是另一組跟自定義元素完美融合的新 API。

<template> 元素可用於聲明 DOM 片段。它們可以被解析並在頁面加載後插入,以及延遲到運行時才進行實例化。模板是聲明自定義元素結構的理想方案。

示例:註冊一個由 <template> 和 Shadow DOM 創建的元素:

<template id="sdtemplate">
  <style>
    p { color: orange; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a <template>.</p>
</template>

<script>
var proto = Object.create(HTMLElement.prototype, {
  createdCallback: {
    value: function() {
      var t = document.querySelector('#sdtemplate');
      var clone = document.importNode(t.content, true);
      this.createShadowRoot().appendChild(clone);
    }
  }
});
document.registerElement('x-foo-from-template', {prototype: proto});
</script>

短短幾行做了很多事情,我們挨個來看都發生了些什麼:

  1. 我們在 HTML 中註冊了一個新元素:<x-foo-from-template>
  2. 這個元素的 DOM 是從一個 <template> 創建的
  3. Shadow DOM 隱藏了該元素可怕的細節
  4. Shadow DOM 也對元素的樣式進行了隔離(比如 p {color: orange;} 不會把整個頁面都搞成橙色

真不錯!

為自定義元素增加樣式

和其他 HTML 標籤一樣,自定義元素也可以用選擇器定義樣式:

<style>
  app-panel {
    display: flex;
  }
  [is="x-item"] {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  [is="x-item"]:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > [is="x-item"] {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-panel>
  <li is="x-item">Do</li>
  <li is="x-item">Re</li>
  <li is="x-item">Mi</li>
</app-panel>
  • Do
  • Re
  • Mi
  • 為使用 Shadow DOM 的元素增加樣式

    有了 Shadow DOM 場面就熱鬧得多了,它可以極大增強自定義元素的能力。

    Shadow DOM 為元素增加了樣式封裝的特性。Shadow Root 中定義的樣式不會暴露到宿主外部或對頁面產生影響。對自定義元素來說,元素本身就是宿主。樣式封裝的屬性也使得自定義元素能夠為自己定義默認樣式。

    Shadow DOM 的樣式是一個很大的話題!如果你想更多地瞭解它,推薦你閱讀我寫的其他文章:

    使用 :unresolved 偽類避免無樣式內容閃爍(FOUC)

    為了緩解無樣式內容閃爍的影響,自定義元素規範提出了一個新的 CSS 偽類 :unresolved。在瀏覽器調用你的 createdCallback()(見生命週期回調方法一節)之前,這個偽類都可以匹配到 unresolved 元素。一旦產生調用,就意味著元素已經完成提升,成為它被定義的形態,該元素就不再是一個 unresolved 元素了。

    Chrome 29 已經原生支持 CSS :unresolved 偽類。

    示例:註冊後漸顯的 <x-foo> 標籤:

    <style>
      x-foo {
        opacity: 1;
        transition: opacity 300ms;
      }
      x-foo:unresolved {
        opacity: 0;
      }
    </style>

    請記住 :unresolved 偽類只能用於 unresolved 元素,而不能用於繼承自 HTMLUnkownElement 的元素(見元素如何提升一節)。

    <style>
      /* 給所有 unresolved 元素添加邊框 */
      :unresolved {
        border: 1px dashed red;
        display: inline-block;
      }
      /* unresolved 元素 x-panel 的文本內容為紅色 */
      x-panel:unresolved {
        color: red;
      }
      /* 定義註冊後的 x-panel 文本內容為綠色 */
      x-panel {
        color: green;
        display: block;
        padding: 5px;
        display: block;
      }
    </style>
    
    <panel>
      I'm black because :unresolved doesn't apply to "panel".
      It's not a valid custom element name.
    </panel>
    
    <x-panel>I'm red because I match x-panel:unresolved.</x-panel>
    I'm black because :unresolved doesn't apply to "panel". It's not a valid custom element name. I'm red because I match x-panel:unresolved.

    瞭解更多 :unresolved 偽類的知識,請看 Polymer 文檔《元素樣式指南》

    歷史和瀏覽器支持

    特性檢測

    特性檢測就是檢查 document.registerElement() 是否存在:

    function supportsCustomElements() {
      return 'registerElement' in document;
    }
    
    if (supportsCustomElements()) {
      // Good to go!
    } else {
      // Use other libraries to create components.
    }

    瀏覽器支持

    Chrome 27 和 Firefox 23 都提供了對 document.registerElement() 的支持,不過之後規範又有一些演化。Chrome 31 將是第一個真正支持新規範的版本。

    在 Chrome 31 中使用自定義元素,需要開啟 about:flags 中的「實驗性 web 平台特性(Experimental Web Platform features)」選項。

    在瀏覽器支持穩定之前,也有一些很好的兼容方案:

    HTMLElementElement 怎麼了?

    一直關注標準的人都知道曾經有一個 <element> 標籤。它非常好用,你只要像下面這樣就可以聲明式地註冊一個新元素:

    <element name="my-element">
      ...
    </element>

    然而很不幸,在它的提升過程、邊界案例,以及末日般的複雜場景中,需要處理大量的時序問題。<element> 因此被迫擱置。2013 年 8 月,Dimitri Glazkov 在 public-webapps 郵件組中宣告移除 <element>

    值得注意的是,Polymer 實現了以 <polymer-element> 的形式聲明式地註冊元素。這是怎麼做到的?它用的正是 document.registerElement('polymer-element') 以及我在從模板創建元素一節介紹的技術。

    結語

    自定義元素為我們提供了一個工具,通過它我們可以擴展 HTML 的詞彙,賦予它新的特性,並把不同的 web 平台連接在一起。結合其他新的基本平台,如 Shadow DOM 和 <template>,我們領略了 web 組件的宏偉藍圖。標記語言將再次變得很時髦!

    如果你對使用 web 組件感興趣,建議你看看 Polymer,就它已經夠你玩的了。

    本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
    打开APP,阅读全文并永久保存 查看更多类似文章
    猜你喜欢
    类似文章
    【热】打开小程序,算一算2024你的财运
    js笔记合集
    各大互联网公司2014前端笔试面试题–JavaScript篇 – 码农网
    Javascript通过bind()掌控this
    Firefox 不支持 DOM 对象的 insertAdjacentHTML insertAdjacentText 方法
    网站如何做到完全不需要使用jQuery
    JavaScript 框架比较
    更多类似文章 >>
    生活服务
    热点新闻
    分享 收藏 导长图 关注 下载文章
    绑定账号成功
    后续可登录账号畅享VIP特权!
    如果VIP功能使用有故障,
    可点击这里联系客服!

    联系客服