JavaScript Web Components

2018-07-24 11:54 更新

目錄

概述

各種網(wǎng)站往往需要一些相同的模塊,比如日歷、調(diào)色板等等,這種模塊就被稱為“組件”(component)。Web Component就是網(wǎng)頁組件式開發(fā)的技術(shù)規(guī)范。

采用組件進(jìn)行網(wǎng)站開發(fā),有很多優(yōu)點(diǎn)。

(1)管理和使用非常容易。加載或卸載組件,只要添加或刪除一行代碼就可以了。

<link rel="import" href="my-dialog.htm">
<my-dialog heading="A Dialog">Lorem ipsum</my-dialog>

上面代碼加載了一個對話框組件。

(2)定制非常容易。組件往往留出接口,供使用者設(shè)置常見屬性,比如上面代碼的heading屬性,就是用來設(shè)置對話框的標(biāo)題。

(3)組件是模塊化編程思想的體現(xiàn),非常有利于代碼的重用。標(biāo)準(zhǔn)格式的模塊,可以跨平臺、跨框架使用,構(gòu)建、部署和與其他UI元素互動都有統(tǒng)一做法。

(4)組件提供了HTML、CSS、JavaScript封裝的方法,實(shí)現(xiàn)了與同一頁面上其他代碼的隔離。

未來的網(wǎng)站開發(fā),可以像搭積木一樣,把組件合在一起,就組成了一個網(wǎng)站。這是非常誘人的。

Web Components不是單一的規(guī)范,而是一系列的技術(shù)組成,包括Template、Custom Element、Shadow DOM、HTML Import四種技術(shù)規(guī)范。使用時,并不一定這四者都要用到。其中,Custom Element和Shadow DOM最重要,Template和HTML Import只起到輔助作用。

template標(biāo)簽

基本用法

template標(biāo)簽表示網(wǎng)頁中某些重復(fù)出現(xiàn)的部分的代碼模板。它存在于DOM之中,但是在頁面中不可見。

下面的代碼用來檢查,瀏覽器是否支持template標(biāo)簽。

function supportsTemplate() {
  return 'content' in document.createElement('template');
}

if (supportsTemplate()) {
  // 支持
} else {
  // 不支持
}

下面是一個模板的例子。

<template id="profileTemplate">
  <div class="profile">
    <img src="" class="profile__img">
    <div class="profile__name"></div>
    <div class="profile__social"></div>
  </div>
</template>

使用的時候,需要用JavaScript在模板中插入內(nèi)容,然后將其插入DOM。

var template = document.querySelector('#profileTemplate');
template.content.querySelector('.profile__img').src = 'profile.jpg';
template.content.querySelector('.profile__name').textContent = 'Barack Obama';
template.content.querySelector('.profile__social').textContent = 'Follow me on Twitter';
document.body.appendChild(template.content);

上面的代碼是將模板直接插入DOM,更好的做法是克隆template節(jié)點(diǎn),然后將克隆的節(jié)點(diǎn)插入DOM。這樣做可以多次使用模板。

var clone = document.importNode(template.content, true);
document.body.appendChild(clone);

接受template插入的元素,叫做宿主元素(host)。在template之中,可以對宿主元素設(shè)置樣式。

<template>
<style>
  :host {
    background: #f8f8f8;
  }
  :host(:hover) {
    background: #ccc;
  }
</style>
</template>

document.importNode()

document.importNode方法用于克隆外部文檔的DOM節(jié)點(diǎn)。

var iframe = document.getElementsByTagName("iframe")[0];
var oldNode = iframe.contentWindow.document.getElementById("myNode");
var newNode = document.importNode(oldNode, true);
document.getElementById("container").appendChild(newNode);

上面例子是將iframe窗口之中的節(jié)點(diǎn)oldNode,克隆進(jìn)入當(dāng)前文檔。

注意,克隆節(jié)點(diǎn)之后,還必須用appendChild方法將其加入當(dāng)前文檔,否則不會顯示。換個角度說,這意味著插入外部文檔節(jié)點(diǎn)之前,必須用document.importNode方法先將這個節(jié)點(diǎn)準(zhǔn)備好。

document.importNode方法接受兩個參數(shù),第一個參數(shù)是外部文檔的DOM節(jié)點(diǎn),第二個參數(shù)是一個布爾值,表示是否連同子節(jié)點(diǎn)一起克隆,默認(rèn)為false。大多數(shù)情況下,必須顯式地將第二個參數(shù)設(shè)為true。

Custom Element

HTML預(yù)定義的網(wǎng)頁元素,有時并不符合我們的需要,這時可以自定義網(wǎng)頁元素,這就叫做Custom Element。它是Web component技術(shù)的核心。舉例來說,你可以自定義一個叫做super-button的網(wǎng)頁元素。

<super-button></super-button>

注意,自定義網(wǎng)頁元素的標(biāo)簽名必須含有連字符(-),一個或多個都可。這是因?yàn)闉g覽器內(nèi)置的的HTML元素標(biāo)簽名,都不含有連字符,這樣可以做到有效區(qū)分。

下面的代碼用于測試瀏覽器是否支持自定義元素。

if ('registerElement' in document) {
  // 支持
} else {
  // 不支持
}

document.registerElement()

使用自定義元素前,必須用document對象的registerElement方法登記該元素。該方法返回一個自定義元素的構(gòu)造函數(shù)。

var SuperButton = document.registerElement('super-button');
document.body.appendChild(new SuperButton());

上面代碼生成自定義網(wǎng)頁元素的構(gòu)造函數(shù),然后通過構(gòu)造函數(shù)生成一個實(shí)例,將其插入網(wǎng)頁。

可以看到,document.registerElement方法的第一個參數(shù)是一個字符串,表示自定義的網(wǎng)頁元素標(biāo)簽名。該方法還可以接受第二個參數(shù),表示自定義網(wǎng)頁元素的原型對象。


var MyElement = document.registerElement('user-profile', {
  prototype: Object.create(HTMLElement.prototype)
});

上面代碼注冊了自定義元素user-profile。第二個參數(shù)指定該元素的原型為HTMLElement.prototype(瀏覽器內(nèi)部所有Element節(jié)點(diǎn)的原型)。

但是,如果寫成上面這樣,自定義網(wǎng)頁元素就跟普通元素沒有太大區(qū)別。自定義元素的真正優(yōu)勢在于,可以自定義它的API。

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

buttonProto.print = function() {
  console.log('Super Button!');
}

var SuperButton = document.registerElement('super-button', {
  prototype: buttonProto
});

var supperButton = document.querySelector('super-button');

supperButton.print();

上面代碼在原型對象上定義了一個print方法,然后將其指定為super-button元素的原型。因此,所有supper-button實(shí)例都可以調(diào)用print這個方法。

如果想讓自定義元素繼承某種特定的網(wǎng)頁元素,就要指定extends屬性。比如,想讓自定義元素繼承h1元素,需要寫成下面這樣。

var MyElement = document.registerElement('another-heading', {
  prototype: Object.create(HTMLElement.prototype),
  extends: 'h1'
});

另一個是自定義按鈕(button)元素的例子。

var MyButton = document.registerElement('super-button', {
  prototype: Object.create(HTMLButtonElement.prototype),
  extends: 'button'
});

如果要繼承一個自定義元素(比如x-foo-extended繼承x-foo),也是采用extends屬性。

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

定義了自定義元素以后,使用的時候,有兩種方法。一種是直接使用,另一種是間接使用,指定為某個現(xiàn)有元素是自定義元素的實(shí)例。

<!-- 直接使用 -->
<supper-button></supper-button>

<!-- 間接使用 -->
<button is="supper-button"></button>

總之,如果A元素繼承了B元素。那么,B元素的is屬性,可以指定B元素是A元素的一個實(shí)例。

添加屬性和方法

自定義元素的強(qiáng)大之處,就是可以在它上面定義新的屬性和方法。

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

上面代碼注冊了一個x-foo標(biāo)簽,并且指明原型繼承HTMLElement.prototype?,F(xiàn)在,我們就可以在原型上面,添加新的屬性和方法。


// 添加屬性
Object.defineProperty(XFooProto, "bar", {value: 5});

// 添加方法
XFooProto.foo = function() {
  console.log('foo() called');
};

// 另一種寫法
var XFoo = document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    bar: {
      get: function() { return 5; }
    },
    foo: {
      value: function() {
        console.log('foo() called');
      }
    }
  })
});

回調(diào)函數(shù)

自定義元素的原型有一些屬性,用來指定回調(diào)函數(shù),在特定事件發(fā)生時觸發(fā)。

  • createdCallback:實(shí)例生成時觸發(fā)
  • attachedCallback:實(shí)例插入HTML文檔時觸發(fā)
  • detachedCallback:實(shí)例從HTML文檔移除時觸發(fā)
  • attributeChangedCallback(attrName, oldVal, newVal):實(shí)例的屬性發(fā)生改變時(添加、移除、更新)觸發(fā)

下面是一個例子。

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

proto.createdCallback = function() {
  console.log('created');
  this.innerHTML = 'This is a my-demo element!';
};

proto.attachedCallback = function() {
  console.log('attached');
};

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

利用回調(diào)函數(shù),可以方便地在自定義元素中插入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});

上面代碼定義了createdCallback回調(diào)函數(shù),生成實(shí)例時,該函數(shù)運(yùn)行,插入如下的HTML語句。


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

Shadow DOM

所謂Shadow DOM指的是,瀏覽器將模板、樣式表、屬性、JavaScript代碼等,封裝成一個獨(dú)立的DOM元素。外部的設(shè)置無法影響到其內(nèi)部,而內(nèi)部的設(shè)置也不會影響到外部,與瀏覽器處理原生網(wǎng)頁元素(比如<video>元素)的方式很像。Shadow DOM最大的好處有兩個,一是可以向用戶隱藏細(xì)節(jié),直接提供組件,二是可以封裝內(nèi)部樣式表,不會影響到外部。Chrome 35+支持Shadow DOM。

Shadow DOM元素必須依存在一個現(xiàn)有的DOM元素之下,通過createShadowRoot方法創(chuàng)造,然后將其插入該元素。

var shadowRoot = element.createShadowRoot();
document.body.appendChild(shadowRoot);

上面代碼創(chuàng)造了一個shadowRoot元素,然后將其插入HTML文檔。

下面的例子是指定網(wǎng)頁中某個現(xiàn)存的元素,作為Shadow DOM的根元素。

<button>Hello, world!</button>
<script>
  var host = document.querySelector('button');
  var root = host.createShadowRoot();
  root.textContent = '你好';
</script>

上面代碼指定現(xiàn)存的button元素,為Shadow DOM的根元素,并將button的文字從英文改為中文。

通過innerHTML屬性,可以為Shadow DOM指定內(nèi)容。

var shadow = document.querySelector('#hostElement').createShadowRoot();
shadow.innerHTML = '<p>Here is some new text</p>';
shadow.innerHTML += '<style>p { color: red };</style>';

下面的例子是為Shadow DOM加上獨(dú)立的模板。

<div id="nameTag">張三</div>

<template id="nameTagTemplate">
  <style>
    .outer {
      border: 2px solid brown;
    }
  </style>

  <div class="outer">
    <div class="boilerplate">
      Hi! My name is
    </div>
    <div class="name">
      Bob
    </div>
  </div>
</template>

上面代碼是一個div元素和模板。接下來,就是要把模板應(yīng)用到div元素上。

var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
shadow.appendChild(template.content.cloneNode(true));

上面代碼先用createShadowRoot方法,對div創(chuàng)造一個根元素,用來指定Shadow DOM,然后把模板元素添加為Shadow的子元素。

HTML Import

基本操作

長久以來,網(wǎng)頁可以加載外部的樣式表、腳本、圖片、多媒體,卻無法方便地加載其他網(wǎng)頁,iframe和ajax都只能提供部分的解決方案,且有很大的局限。HTML Import就是為了解決加載外部網(wǎng)頁這個問題,而提出來的。

下面代碼用于測試當(dāng)前瀏覽器是否支持HTML Import。


function supportsImports() {
  return 'import' in document.createElement('link');
}

if (supportsImports()) {
  // 支持
} else {
  // 不支持
}

HTML Import用于將外部的HTML文檔加載進(jìn)當(dāng)前文檔。我們可以將組件的HTML、CSS、JavaScript封裝在一個文件里,然后使用下面的代碼插入需要使用該組件的網(wǎng)頁。


<link rel="import" href="dialog.html">

上面代碼在網(wǎng)頁中插入一個對話框組件,該組建封裝在dialog.html文件。注意,dialog.html文件中的樣式和JavaScript腳本,都對所插入的整個網(wǎng)頁有效。

假定A網(wǎng)頁通過HTML Import加載了B網(wǎng)頁,即B是一個組件,那么B網(wǎng)頁的樣式表和腳本,對A網(wǎng)頁也有效(準(zhǔn)確得說,只有style標(biāo)簽中的樣式對A網(wǎng)頁有效,link標(biāo)簽加載的樣式表對A網(wǎng)頁無效)。所以可以把多個樣式表和腳本,都放在B網(wǎng)頁中,都從那里加載。這對大型的框架,是很方便的加載方法。

如果B與A不在同一個域,那么A所在的域必須打開CORS。


<!-- example.com必須打開CORS -->
<link rel="import" href="http://example.com/elements.html">

除了用link標(biāo)簽,也可以用JavaScript調(diào)用link元素,完成HTML Import。


var link = document.createElement('link');
link.rel = 'import';
link.href = 'file.html'
link.onload = function(e) {...};
link.onerror = function(e) {...};
document.head.appendChild(link);

HTML Import加載成功時,會在link元素上觸發(fā)load事件,加載失敗時(比如404錯誤)會觸發(fā)error事件,可以對這兩個事件指定回調(diào)函數(shù)。


<script async>
  function handleLoad(e) {
    console.log('Loaded import: ' + e.target.href);
  }
  function handleError(e) {
    console.log('Error loading import: ' + e.target.href);
  }
</script>

<link rel="import" href="file.html"
      onload="handleLoad(event)" onerror="handleError(event)">

上面代碼中,handleLoad和handleError函數(shù)的定義,必須在link元素的前面。因?yàn)闉g覽器元素遇到link元素時,立刻解析并加載外部網(wǎng)頁(同步操作),如果這時沒有對這兩個函數(shù)定義,就會報錯。

HTML Import是同步加載,會阻塞當(dāng)前網(wǎng)頁的渲染,這主要是為了樣式表的考慮,因?yàn)橥獠烤W(wǎng)頁的樣式表對當(dāng)前網(wǎng)頁也有效。如果想避免這一點(diǎn),可以為link元素加上async屬性。當(dāng)然,這也意味著,如果外部網(wǎng)頁定義了組件,就不能立即使用了,必須等HTML Import完成,才能使用。


<link rel="import" href="/path/to/import_that_takes_5secs.html" async>

但是,HTML Import不會阻塞當(dāng)前網(wǎng)頁的解析和腳本執(zhí)行(即阻塞渲染)。這意味著在加載的同時,主頁面的腳本會繼續(xù)執(zhí)行。

最后,HTML Import支持多重加載,即被加載的網(wǎng)頁同時又加載其他網(wǎng)頁。如果這些網(wǎng)頁都重復(fù)加載同一個外部腳本,瀏覽器只會抓取并執(zhí)行一次該腳本。比如,A網(wǎng)頁加載了B網(wǎng)頁,它們各自都需要加載jQuery,瀏覽器只會加載一次jQuery。

腳本的執(zhí)行

外部網(wǎng)頁的內(nèi)容,并不會自動顯示在當(dāng)前網(wǎng)頁中,它只是儲存在瀏覽器中,等到被調(diào)用的時候才加載進(jìn)入當(dāng)前網(wǎng)頁。為了加載網(wǎng)頁網(wǎng)頁,必須用DOM操作獲取加載的內(nèi)容。具體來說,就是使用link元素的import屬性,來獲取加載的內(nèi)容。這一點(diǎn)與iframe完全不同。


var content = document.querySelector('link[rel="import"]').import;

發(fā)生以下情況時,link.import屬性為null。

  • 瀏覽器不支持HTML Import
  • link元素沒有聲明rel="import"
  • link元素沒有被加入DOM
  • link元素已經(jīng)從DOM中移除
  • 對方域名沒有打開CORS

下面代碼用于從加載的外部網(wǎng)頁選取id為template的元素,然后將其克隆后加入當(dāng)前網(wǎng)頁的DOM。


var el = linkElement.import.querySelector('#template');

document.body.appendChild(el.cloneNode(true));

當(dāng)前網(wǎng)頁可以獲取外部網(wǎng)頁,反過來也一樣,外部網(wǎng)頁中的腳本,不僅可以獲取本身的DOM,還可以獲取link元素所在的當(dāng)前網(wǎng)頁的DOM。


// 以下代碼位于被加載(import)的外部網(wǎng)頁

// importDoc指向被加載的DOM
var importDoc = document.currentScript.ownerDocument;

// mainDoc指向主文檔的DOM
var mainDoc = document;

// 將子頁面的樣式表添加主文檔
var styles = importDoc.querySelector('link[rel="stylesheet"]');
mainDoc.head.appendChild(styles.cloneNode(true));

上面代碼將所加載的外部網(wǎng)頁的樣式表,添加進(jìn)當(dāng)前網(wǎng)頁。

被加載的外部網(wǎng)頁的腳本是直接在當(dāng)前網(wǎng)頁的上下文執(zhí)行,因?yàn)樗?code class="highlighter-rouge">window.document指的是當(dāng)前網(wǎng)頁的document,而且它定義的函數(shù)可以被當(dāng)前網(wǎng)頁的腳本直接引用。

Web Component的封裝

對于Web Component來說,HTML Import的一個重要應(yīng)用是在所加載的網(wǎng)頁中,自動登記Custom Element。


<script>
  // 定義并登記<say-hi>
  var proto = Object.create(HTMLElement.prototype);

  proto.createdCallback = function() {
    this.innerHTML = 'Hello, <b>' +
                     (this.getAttribute('name') || '?') + '</b>';
  };

  document.registerElement('say-hi', {prototype: proto});
</script>

<template id="t">
  <style>
    ::content > * {
      color: red;
    }
  </style>
  <span>I'm a shadow-element using Shadow DOM!</span>
  <content></content>
</template>

<script>
  (function() {
    var importDoc = document.currentScript.ownerDocument; //指向被加載的網(wǎng)頁

    // 定義并登記<shadow-element>
    var proto2 = Object.create(HTMLElement.prototype);

    proto2.createdCallback = function() {
      var template = importDoc.querySelector('#t');
      var clone = document.importNode(template.content, true);
      var root = this.createShadowRoot();
      root.appendChild(clone);
    };

    document.registerElement('shadow-element', {prototype: proto2});
  })();
</script>

上面代碼定義并登記了兩個元素:<say-hi>和<shadow-element>。在主頁面使用這兩個元素,非常簡單。


<head>
  <link rel="import" href="elements.html">
</head>
<body>
  <say-hi name="Eric"></say-hi>
  <shadow-element>
    <div>( I'm in the light dom )</div>
  </shadow-element>
</body>

不難想到,這意味著HTML Import使得Web Component變得可分享了,其他人只要拷貝elements.html,就可以在自己的頁面中使用了。

Polymer.js

Web Components是非常新的技術(shù),為了讓老式瀏覽器也能使用,Google推出了一個函數(shù)庫Polymer.js。這個庫不僅可以幫助開發(fā)者,定義自己的網(wǎng)頁元素,還提供許多預(yù)先制作好的組件,可以直接使用。

直接使用的組件

Polymer.js提供的組件,可以直接插入網(wǎng)頁,比如下面的google-map。。


<script src="components/platform/platform.js"></script>
<link rel="import" href="google-map.html">
<google-map lat="37.790" long="-122.390"></google-map>

再比如,在網(wǎng)頁中插入一個時鐘,可以直接使用下面的標(biāo)簽。


<polymer-ui-clock></polymer-ui-clock>

自定義標(biāo)簽與其他標(biāo)簽的用法完全相同,也可以使用CSS指定它的樣式。


polymer-ui-clock {
  width: 320px;
  height: 320px;
  display: inline-block;
  background: url("../assets/glass.png") no-repeat;
  background-size: cover;
  border: 4px solid rgba(32, 32, 32, 0.3);
}

安裝

如果使用bower安裝,至少需要安裝platform和core components這兩個核心部分。


bower install --save Polymer/platform
bower install --save Polymer/polymer

你還可以安裝所有預(yù)先定義的界面組件。


bower install Polymer/core-elements
bower install Polymer/polymer-ui-elements

還可以只安裝單個組件。


bower install Polymer/polymer-ui-accordion

這時,組件根目錄下的bower.json,會指明該組件的依賴的模塊,這些模塊會被自動安裝。


{
  "name": "polymer-ui-accordion",
  "private": true,
  "dependencies": {
    "polymer": "Polymer/polymer#0.2.0",
    "polymer-selector": "Polymer/polymer-selector#0.2.0",
    "polymer-ui-collapsible": "Polymer/polymer-ui-collapsible#0.2.0"
  },
  "version": "0.2.0"
}

自定義組件

下面是一個最簡單的自定義組件的例子。


<link rel="import" href="../bower_components/polymer/polymer.html">
 
<polymer-element name="lorem-element">
  <template>
    <p>Lorem ipsum</p>
  </template>
</polymer-element>

上面代碼定義了lorem-element組件。它分成三個部分。

(1)import命令

import命令表示載入核心模塊

(2)polymer-element標(biāo)簽

polymer-element標(biāo)簽定義了組件的名稱(注意,組件名稱中必須包含連字符)。它還可以使用extends屬性,表示組件基于某種網(wǎng)頁元素。


<polymer-element name="w3c-disclosure" extends="button">

(3)template標(biāo)簽

template標(biāo)簽定義了網(wǎng)頁元素的模板。

組件的使用方法

在調(diào)用組件的網(wǎng)頁中,首先加載polymer.js庫和組件文件。


<script src="components/platform/platform.js"></script>
<link rel="import" href="w3c-disclosure.html">

然后,分成兩種情況。如果組件不基于任何現(xiàn)有的HTML網(wǎng)頁元素(即定義的時候沒有使用extends屬性),則可以直接使用組件。


<lorem-element></lorem-element>

這時網(wǎng)頁上就會顯示一行字“Lorem ipsum”。

如果組件是基于(extends)現(xiàn)有的網(wǎng)頁元素,則必須在該種元素上使用is屬性指定組件。


<button is="w3c-disclosure">Expand section 1</button>

參考鏈接

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號