2015.02.04

Web Components で共通ヘッダーをつくってみた


フロントエンド系の技術調査やサーバサイドアプリケーションの開発を担当している D.M. です。今回は Google I/O 等で話題になっている Web Components を取り上げたいと思います。簡単に言うと HTML の再利用性を高めるための HTML の新仕様です。


Web Components 4要素


Shadow DOM

ページ内で他の CSS が干渉できない領域を作り出すことができます。これによりページとコンポーネントの CSS が互いに干渉してしまう問題を解決できます。

Templates

JS が操作する HTML を JS から分離して Tempalte タグとして定義することで JS から利用しやすい形にすることができます。このタグを利用することで HTML が JS のソース内に混在してしまう問題や、 HTML 内の JS が誤作動する問題を抑制します。

Custom Elements

独自のタグを定義できます。これにより各機能ごとにタグを設けることができるので、 div タグと id が増えがちな状況を軽減し、 HTML の可読性が向上します。

HTML Imports

HTML を外部からインポートできます。独自タグを利用した HTML を再利用することができるようになります。


上記4要素の仕様については HTML5 Rocks が非常に詳しいです。深く理解しておきたい方は以下のリンク先をご一読いただければと思います。

Shadow DOM 101

Shadow DOM 201 CSS とスタイリング

Shadow DOM 301 上級者向けコンセプトと DOM API

HTML で利用可能になった Template タグ

Custom Elements HTML に新しい要素を定義する

HTML Imports ウェブのための #include


現状では対応ブラウザに制限があり、 Chrome ・ Opera 以外では動作しません。また、 Google が用意している Polymer ライブラリを用いることで FireFox と Safari では動作可能になりますが、 IE は 10 以降でないと挙動が怪しい状況です。 Google は Polymer ライブラリ内に独自のコンポーネントを豊富に用意しており、一歩進んだ準備を整えている印象があります。現状 IE の制限が強いため一般 Web サイトの本番サービス環境において Web Components / Polymer の恩恵を活かすことは難しいのですが、今回はコンセプトに沿って試験的に1つ実装をしてみたいと思います。


GMO 共通ヘッダーのコンポーネント実装


GMO インターネットグループのサイトには共通のヘッダーがつけられています。

gmo-header

要件としては以下の点が挙げられます。
・共通ヘッダーはいろいろなサイトに埋め込むため、各サイトの CSS や JS に影響を与えないようにしたい。
・共通ヘッダーの変更時に読み込む側のサイトの変更をゼロにしたい。

現状の共通ヘッダーは JS のみで実現できています。ただ JS だけで実装すると、以下の問題が発生します。
・ JS 内部に HTML が組み込まれてしまい、 HTML だけの修正がやや困難。

今回上記の要件と問題点に対して、 Web Components を利用して実装することでより安全な状況を実現したいと思います。
・ Custom Element として <gmo-header> を作成する。
・ Shadow DOM で CSS が読み込む側のサイトに影響しない状態を作り出す。
・ Template タグを用いて JS が gmo-header タグで読む込む HTML を定義する。
・ HTML Import で上記対応を外部ファイルから読み込む。

検証サイト
http://9199.jp/js-test/webcomponents-test.html

読み込む側の HTML


今回は題材として 9199.jp のトップページ http://9199.jp/index.html の HTML を改変して実装してみます。

<head>

... 通常のHeadタグのソースがここにあるが、割愛 ...

<!-- 【追加箇所】 HTML Import で GMO 共通ヘッダーの HTML をインポートする。 -->
<link rel="import" href="//its-lab.net/webcomponents/gmo-header.html" />

</head>
<body id="home" class="home">

<!-- 【追加箇所】 独自に定義した GMO 共通ヘッダータグを設置 -->
<gmo-header></gmo-header>

<div id="wrapper">
<div id="maincontainer">
<div id="header">
    <div id="logo"><a href="/" title="9199.JP街検索"></a></div>
    <ul id="headlink" class="s">
        <li><a target="_blank" href="/about.html" title="9199.JP街検索とは?">9199.JP街検索とは?</a></li>
    </ul>
</div>
<div id="content">
<div id="main">

... 以下9199jpのトップページのHTMLが続く。割愛 ...


読む込む側の HTML の特徴としては、
HTML Import の読み込み1行、 Custom Element タグ1行、合わせて2行で書いている点です。

gmo-header.html


コンポーネントの本体のソースです。

<!-- 念のため文字コード設定を先頭に書きます。読み込む側のHTMLはShift-jisですが、このファイルは UTF-8 です。一致していなくても動きます。 -->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

<!-- template内で利用されるstyleを外部から読み込みます。template内部はlinkが使えないので通常のCSS読み込みができません。代わりに HTML Import してから中身をアペンドします。 -->
<link rel="import" href="//its-lab.net/webcomponents/headerstyle.html" />

<!-- 共通ヘッダーの中身をTemplateタグで定義します。これがJSによってShadowDOMの中身となります。 -->
<template id="gmo_header_tmpl">

<div id="gmocommonheader">
<div id="gmo_header_wrapper">

... ヘッダーのHTMLがそのまま入ります。長いので割愛 ... 

</div>
</div>

<script>

    //template 内部で利用する document オブジェクトを取り出しておいたほうが便利です。
    var doc = document.currentScript.parentNode;

... ここに template 内で利用するJSが入ります。初期処理やクリックイベント設定などいわゆる従来型の JS そのままです。長いので割愛。 ...

</script>

</template>

<!-- 以下の JS にて Shadow DOM 、 Template 、 Custom Element を設定します。-->
<script>

   // Custom Element の初期化で利用する HTML Element prototype を生成
    var protoHtml = Object.create(HTMLElement.prototype, {

      //Custom Elements の初期化処理
      createdCallback:{

        value:function(){

           // link している外部の HTML 内容を取得します。
           var link = document.querySelector('link[href="//its-lab.net/webcomponents/gmo-header.html"]');

           // import した HTML の document オブジェクトを取得します
           var docTmpl = link.import;

           // import した HTML 内にある import した style を呼び込む
           var styledom = docTmpl.querySelector('link[href="//its-lab.net/webcomponents/headerstyle.html"]').import;
           var styleElm= styledom.querySelector('style[id="gmoheader"]');

           var shadowRoot = this.createShadowRoot();
          
           // shadow に style をまるごとアペンドする 
           shadowRoot.appendChild( styleElm );

           //template id="gmo_header_tmpl" の取得
           var tmpl = docTmpl.getElementById( "gmo_header_tmpl" );

           //template のクローン
           var tmplClone = document.importNode(tmpl.content, true);

           //shadow に template の中身をアペンド
           shadowRoot.appendChild(tmplClone);
            
     }  }  });

    // gmo-header タグを登録する
    document.registerElement( "gmo-header" , { prototype : protoHtml });

</script>

Custom Element gmo-header を定義するためのファイルの中は Template タグ + Script タグの構成となっています。
Script タグの内部で以下のの3つを行っています。
1. Shadow DOM 生成
2. Template のクローン設定
3. Custom Element の登録
Template 内部でも Script タグを持っていますが、こちらは Template が実体化した後に利用される通常の JS です。


以上でGMO共通ヘッダーのコンポーネントを実装することができました。ヘッダー部分の既存の HTML と JS はそのままに、 Shadow DOM 、 Templates 、 Custom Elements 、 HTML Imports の特徴を利用して独自のタグを安全に管理することができるようになっています。

今後は Polymer を利用した実装の改善と、他のコンポーネント開発に挑戦したいと思っています。


次世代システム研究室では、グループ全体のアプリケーション開発を担うアーキテクトやアプリケーションエンジニアを募集しています。Webアプリケーション開発経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。