JSON-LDを​WebComponentsで​表示する

公開

更新

Googleウェブマスター向け公式ブログに掲載された、「Web Components と JSON-LD でウェブサイト開発がより簡単に」という記事を見てサイトに静的に設置されたJSON-LDを読み取るWeb Componentsを作ってみました。

練習ということでHTML5 Rocksの日本語記事みながらライブラリ使わずにやってみましたので、備忘録がてら順を追って解説してみます。

Web Components + JSON-LD デモページはこちら

JSON-LD

まず、ブログの記事リストをJSON-LD形式で用意しました。

<script id="jsonld" type="application/ld+json">
[
  {
    "@context": "http://schema.org",
    "@type": "BlogPosting",
    "name": "シンタックスハイライトをgoogle code prettifyからhighlight.jsに変えた",
    "url": "http://masup.net/2015/03/change-syntax-highlighter.html",
    "datePublished": "2015-03-19T00:37:31+09:00"
  },
  {
    "@context": "http://schema.org",
    "@type": "BlogPosting",
    "name": "ABCテレビ キャストで「コーンポタージュのコーンを上手く食べる方法を流体解析してみた」が使用されました",
    "url": "http://masup.net/2015/03/corn-soup-sim-used-in-tv-program.html",
    "datePublished": "2015-03-13T13:09:01+09:00"
  },
  {
    "@context": "http://schema.org",
    "@type": "BlogPosting",
    "name": "Mobile SafariのselectがイケてないのでほぼCSSで擬似select作ってみた",
    "url": "http://masup.net/2015/03/pseudo-select.html",
    "datePublished": "2015-03-02T22:41:27+09:00"
  }
]
</script>

通常のJSONとの違いは、@context@typeでボキャブラリーを指定するところです。 今回は、schema.orgのBlog記事に相当するBlogPostingを選択しました。 その他は、BlogPosting で使用可能なプロパティを選択しています。

用意したJSON-LDを表示するWeb Components を作成する

Web Components は

  • template要素
  • HTML imports
  • Shadow root
  • Custom Elements

の4つの技術から成り立っています。その全てを使う必要はありませんが、今回は練習ということで全てを使用してみます。

template要素

template要素は、スクリプトによる文書への挿入・複製が可能なHTMLの断片を定義します。

HTML5&CSS3/2.1全辞典(小川・加藤 &できるシリーズ編集部, 2015) p.43

ということで、テンプレートのHTMLを<template>で囲うと不活性なHTMLテンプレートを定義できます。

<template id="article-item-template">
<li class="article-item">
  <time datetime=""></time>
  <a href=""></a>
</li>
</template>

不活性というのは、レンダリングされないだけでなくアクティベートされるまで画像のロードもなくscriptも動きません。アクティベートするにはJavaScriptから、template要素の中身をコピーする必要があります。

template要素を使ってリスト表示してみると

//template要素の中身を取得
var t = document.querySelector('template');
//importNode()を使用してクローンを作成
var articleItem = document.importNode(t.content, true);
//クローンの要素に値を代入
articleItem.querySelector('a').textContent = "シンタックスハイライトをgoogle code prettifyからhighlight.jsに変えた";
document.body.appendChild(articlItem); //クローン要素をbodyに追加

ここで、クローンの値にJSON-LDのデータを挿入するととてもシンプルなWeb ComponentsとJSON-LDの連携となります。JSON-LDの値をtemplate要素に渡すには、script要素のテキストデータを取得し、JSONオブジェクトに変換して使用します。

See the Pen template + JSON-LD by Soichi Masuda (@masuP9) on CodePen.

HTML imports

HTML importsはlink要素を用いて、別のHTMLを読み込みます。先ほどのtemplate要素で記述したtemplateをarticle-item.htmlとして保存し、それを読み込むには

<link rel="import" href="article-item.html">

と1行記載するだけです。ただしimportされたコンテンツは、template要素と同じく、scriptでアクセスして利用する必要があります。

importされたコンテンツはlink要素のimportプロパティからアクセスできるので

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

よって、さきほどのtemplate要素を読み込んで使用するには、下記のコードとなります。

article-item.html
<template id="article-item-template">
<li class="article-item">
  <time datetime=""></time>
  <a href=""></a>
</li>
</template>
index.html
<head>
  <link rel="import" href="article-item.html" />
</head>
<body>
<ul class="article-list"></ul>
<script>
// importしたコンテンツを取得
var content = document.querySelector('link[rel="import"]').import;
// importしたコンテンツからtemplate要素の中身を取得
var t = content.getElementById('article-item-template');
  ~クローンした要素にJSONデータを代入(省略)~
</script>
</body>

Shadow DOM

Shadow DOMは部品をカプセル化してしまって外部に影響与えないようにするための技術です。今までの例で言うと、template要素内にスタイルを記述すると、使用された時に既存の要素にまで影響してしまいます。

See the Pen template + JSON-LD with style by Soichi Masuda (@masuP9) on CodePen.

この場合、<ul.article-list>直下のp要素内のtimeaにまでスタイルがあたってしまいます。

そのため、これまではセレクタが一意になるようなclassやidを要素に追加していましたが、それでも重複しない保証はありません。そこで要素を完全にカプセル化してしまうのがShadow DOMです。

Shadow DOMは、createShadowRoot()という関数を用いて作成します。そこにtemplate要素をクローンした要素を追加すると、全体がカプセル化され内部のスタイルは他の要素に影響を与えません。

See the Pen template + Shadow DOM + JSON-LD by Soichi Masuda (@masuP9) on CodePen.

これで他の要素に影響を与えない、単独の部品として扱えるようになりました。

Custom Elements

Custom Elementsを使用することで、制作者は新しいHTMLの要素を作成し、既存の要素にはない機能をもたせたり、既存の要素を拡張したりすることができます。

Custom ElementsはregisterElement()関数を使用して作成します。

var articleList = document.registerElement('article-list');

Custom Elementsを作成するときの注意点は、名前に"-"を含む必要が有ることです。このルールがあることで通常のHTML要素(これから策定される要素を含む)と重複しないことが約束されます。

また既存の要素を拡張する際は、要素を作成するときに既存の要素を継承する形で作成します。その場合は、registerElement()の二つ目の引数に継承する要素を指定します。今回は、記事アイテム<li>要素を持つ<ul>要素を拡張します。

var articleList = document.registerElement('article-list', {
  prototype: Object.create(HTMLUListElement.prototype),
  extends: 'ul'
});

作成したCustom Elementには独自のプロパティやメソッドを定義することが可能で、さらに特徴的なのがライフサイクルコールバックというメソッドを定義することが出来ます。ライフサイクルコールバックはCustom Elementのインスタンスが作られた時や、ドキュメントに追加された時、属性に変更があった時などのタイミングで呼び出されるメソッドです。

コールバック名 呼び出されるタイミング
createdCallback 要素のインスタンスが作られた時
attachedCallback インスタンスがドキュメントに追加された時
detachedCallback インスタンスがドキュメントから取り除かれた時
attributeChangedCallback(attrName, oldVal, newVal) 属性が追加、削除、更新された時
via Custom Elements HTML に新しい要素を定義する

今回は、要素がインスタンス化されたタイミングで、ページ内のJSON-LDを読み込み自身の子要素に記事アイテムを追加する関数を定義してみます。Custom Elementは宣言するとインスタンス化されますので、ページ内でCustom Elementを記述するだけで、JSON-LDを表示してくれます。

// HTMLUListElement を継承して prototypeを作成
var articleListProto = Object.create(HTMLUListElement.prototype);
// 要素がインスタンス化された時の挙動を定義
articleListProto.createdCallback = function() {
  // 読み込まれたページ内の JSON-LDを取得して、jsonld属性に付与
  var jsonld = document.body.querySelector('script[type="application/ld+json"]').textContent;
  this.setAttribute('jsonld', jsontext);
}
// 要素の属性が変更された時の挙動を定義
articleListProto.attributeChangedCallback = function(attrName, oldVal, newVal) {
  var jsonld = JSON.parse(newVal);      // 受け取ったテキストデータをObjectに
  var shadow = this.createShadowRoot(); // Custom Element以下にShadow DOMを形成
  for (var i = 0; i <= jsonld.length - 1; i++) {
  ~~ JSON-LDデータを template要素からクローンした要素に代入 ~~
  // クローンした要素をShadow DOMに追加
  shadow.appendChild(articleItem);
  }
  // 二つのメソッドを持ったCustom Elmentsを登録
  document.registerElement('article-list', {
    prototype:articleListProto,
    extends: 'ul'
  });
}

上記のscriptとtemplateを持ったarticle-item.htmlをHTML importsで読み込み HTML中で<ul is="article-list"></ul>を宣言するだけで ページ内のJSON-LDデータを表示するリスト部品が使用できます。

Web Components + JSON-LD デモページはこちら

Polyfillを使用してないので現時点で動作するのは Google Chrome, Operaのみです。Polymerも使ってみたので次の記事ではその話が出来ればと。


参考記事