term タグ別の記事一覧

SVG+Web Componentsで再利用可能なGUIコンポーネントを作る

昨今のWeb開発の現場では、Vue.jsやReactといったページ内の要素をコンポーネント化して再利用しやすくするフレームワークが多く利用されています。しかし、これらのフレームワークのコンポーネントは相互運用性がありません。たとえばVue.js向けに作ったコンポーネントはReactでは利用できませんし、その逆も同様です。

一方で、特に外部のフレームワークなどを利用せずに、JavaScriptだけで自作のコンポーネントを作成できる「Web Components」という技術もあります。Web ComponentではVue.jsやReactのような、DOMとデータの高度な紐づけ機能は提供されませんが、データをビジュアライズするだけであれば十分な機能を備えています。さらに、Web Componentsで作成したコンポーネントはVue.jsやReactでも使えます。

ということで、Web ComponentsとSVGを使って、ダイアル型のメーター(名前は「dial-meter」)というUIコンポーネントを作成してみました。ソースはGitHubのhylom/web-component-demoリポジトリで公開しています。シンプルなデモページも用意しました。

Web Componentsの概要と情報源

Web Componentsを利用すると、次のような手順で独自のタグ(カスタム要素)を実装できます。

  1. HTMLElementクラスを継承したクラスを定義する
  2. 定義するタグ名を第1引数、紐付けるクラス(1.で定義したクラス)を第2引数として与えてcustomElements.define()メソッドを実行する

たとえば<dial-meter>というカスタム要素を実装する場合、まずHTMLElementを継承したDialMeterというクラスを作成し、customElements.define('dial-meter', DialMeter)を実行して登録します。

class DialMeter extends HTMLElement {
  constructor() {
    super();
  }
}
customElements.define("dial-meter", DialMeter);

このJavaScriptコードをWebブラウザ内で実行すると、<dial-meter>という要素がページ内で利用できるようになります。

Web Componentsについて詳しくはMDNの「Web Components」ドキュメントに一通りの基本的な内容がまとめられているので、本記事では基本的な解説はそちらに譲り、ハマったところ、注意点などについて簡単にまとめておきます。

既存の要素を拡張して独自コンポーネントを作ることはできない

Web Componentsでは、完全に新たなHTML要素を実装するだけでなく、既存のHTML要素を拡張する(機能を追加する)という機能も提供されています。この場合、拡張したいHTML要素に対応するクラスを継承したクラスを作成し、さらにcustomElements.define()の第3引数として{ extends: <要素名> }というオブジェクトを与えます。

たとえば<div>要素を拡張したmy-new-elementという要素を作成する場合なら、HTMLDivElementを継承したMyNewElementクラスを作成し、次のようにcustomElements.define()を実行します。

customElements.define(`my-new-element`, MyNewElement, { extends: 'div' });

このようにして定義したカスタム要素は、拡張元の要素に「is」属性を付与した要素を作成することでドキュメント内で利用できます。

<div is="my-new-element"></div>

一方で、このようにして定義したコンポーネントは<my-new-element></my-new-element>のような形では利用できません。

作成するコンポーネント名には必ず「-」が含まれている必要がある

既存のHTMLタグとの競合を回避するため、作成するコンポーネントの名前には必ず「-」が含まれている必要があります。

shadow DOMのルート要素として挿入できる要素の制約

Web Componentsでは、shadow DOMというドキュメントとは隔離されたDOMを使って要素をコンポーネント化します。たとえばこのshadow DOM内に<div>要素を挿入する場合、次のような処理を行います。

const shadow = this.attachShadow({mode: 'open'});

const div = document.createElement('div');
shadow.appendChild(div);

このようにして作成したshadow DOMは、ドキュメント内でそのカスタム要素が存在する位置にアタッチされるのですが、一方でshadow DOMのルート要素直下に挿入できる子要素には制約があり、<div><p>など、限られたものしか挿入できません。

利用できる要素はMDNのドキュメントに記載されていますが、たとえば<img><svg>タグは許可されていません。ただし、この制約はあくまでshadow DOMのルート要素直下にのみ適用されるため、shadow DOMのルート要素直下に<div>要素を挿入すれば、その<div>要素内には任意の要素を挿入できます。

今回作成したカスタム要素は<svg>要素を使ってUIを作成しているので、shadow DOMのルート直下には<div>を挿入して使用しています。

属性へのアクセス

Web Componentsで実装したカスタム要素は、DOM上ではcustomElements.define()メソッドに与えたクラスのインスタンスになります。たとえば今回の例では、<dial-meter>要素はDialMeterクラスのインスタンスとなります。このとき、<dial-meter>に与えた属性は、DialMeterクラス内からはthis.getAttribute()を使ってアクセスできます。

たとえば、<dial-meter value="100">のように記述した場合、このvalue属性の値はthis.getAttribute('value')のようにして取得できます。

一方で、クラス内で毎回getAttribute()を使用するのはやや面倒です。そのため、クラス内から頻繁にアクセスする属性については次のようにgetter/setterを定義すると直感的にアクセスできるようになります。

get value() {
  return this.getAttribute('value');
}

set value(val) {
  this.setAttribute('value', val);
}

属性が変更された場合の対応

Web Components技術を使って実装されたカスタム要素は、一般的なHTML要素と同様に扱えます。つまり、createElement()で新規作成してappendChild()append()prepend()といったメソッドで追加したり、replaceChildren()で削除したり、DOM経由で属性を操作する、といった操作が行えます。こういった操作が行われた際には、カスタム要素に紐づけられたクラスの次のメソッドが実行されます。

  • カスタム要素がノードに追加された場合:connectedCallback()
  • カスタム要素がノードから削除された場合:disconnectedCallback()
  • カスタム要素が移動された場合:adoptedCallback()
  • カスタム要素の属性値が変化した場合:attributeChangedCallback()

なお、属性値に関してはあらかじめobservedAttributes()メソッドを定義し、このメソッドの戻り値で監視する属性名の配列を返すよう実装しておく必要があります。

たとえば「value」および「class」、「style」属性が変化したときにattributeChangedCallback()が呼び出されるようにする場合、次のようにobservedAttributes()メソッドを定義しておきます。

static get observedAttributes() {
  return ['value', 'class', 'style'];
}

また、connectedCallback()およびdisconnectedCallback()adoptedCallback()には引数が渡されませんが、attributeChangedCallback()には次のように3つの引数が与えられます。

attributeChangedCallback(name, oldValue, newValue)

ここでnameは変化した属性名、oldValueは変化前の値、newValueは変化後の値です。

DOMとカスタム要素の構築タイミング

HTML内にカスタム要素を記述していた場合、connectedCallback()はそのカスタム要素のタグがパースされたタイミング(DOMContentLoadedイベントの発生前)に実行されます。

また、属性が指定されていた場合、まずattributeChangedCallback()が実行され、続いてconnectedCallback()が実行されます。

DOMの構築後にcustomElements.define()でカスタム要素が登録された場合、そのタイミングでattributeChangedCallback()connectedCallback()が実行されます。

shadow DOM内の要素に適用されるスタイルシートの定義

Shadow DOM内に、適用したいCSSをtextContentとして持つ<style>要素を挿入することでスタイルシートを適用できます。

const style = document.createElement('style');
style.textContent = `<適用したいCSS>`;
shadow.appendChild(style);

<link>要素を挿入して外部のスタイルシートを読み込ませることも可能です。

const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', '<CSSファイルのURL>');
shadow.appendChild(link);

カスタム要素自体のstyleを指定する

上記の方法で読み込ませたスタイルシート中では、いくつか特殊な擬似クラスが利用できます。

  • :host:そのカスタム要素自体(shadow DOMのroot)のスタイル
  • :host():引数で指定したセレクタがそのカスタム要素に適用されているに適用されるスタイル。たとえば:host(.foo)とすると、そのカスタム要素に「foo」と言うクラスが指定されていた場合のみに指定したクラスが適用される
  • :host-content():引数で指定したセレクタに合致する要素内にそのカスタム要素が存在する場合に適用されるスタイル。たとえば:host-content(h1)とすると、h1要素の中に存在するそのカスタム要素のみに指定したスタイルが適用される。

ただし、:host-content()は現状Chrome系ブラウザでのみサポートされているようです。

また、Web Componentsに関連する擬似要素として、:defined::part()の2つがあります。

まず:definedですが、これはそのカスタム要素が定義されている(customElements.define()で定義されている)場合のみ適用されるスタイルを指定するものです。たとえば次のコードはfoo-barと言うカスタム要素がcustomElements.define()で定義されている場合にインライン要素として表示し、そうでない場合は非表示にすると言うものです。

foo-bar:not(:defined) {
  display: none;
}

foo-bar:defined {
  display: inline;
}

::part()は、:カスタム要素のshadow DOM内でpart属性が指定されている要素を対象として選択するものです。たとえばfoo-barカスタム要素のshadow DOM内にpart="hoge"と言う属性が指定された要素が存在する場合、次のようにしてその属性のみを対象にスタイルを適用できます。

foo-bar::part(hoge) {
  ...
}

shadow DOM内の要素は通常は外部のスタイルシートの影響を受けませんが、part属性とこの::part()セレクタを組み合わせることで、一部の要素のみ外部のスタイルシートでスタイルを変更できるようにすることが可能になります。

スロット

shadow DOM内に<slot name="<スロット名>">と言う要素を挿入すると、この要素はそのカスタム要素内に囲まれた要素で、かつslot="<スロット名>"という属性が指定された要素に置き換えられます。

たとえばfoo-barというカスタム要素のshadow DOMが次のようになっていたとします。

<p><slot name="hoge">blah blah blah</slot></p>

このとき、<foo-bar>カスタム要素を次のようにマークアップしてみます。

<foo-bar><i slot="hoge">wryyy</i></foo-bar>

すると、表示されるshadow DOMは次のように<slot name="hoge">要素が<i slot="hoge">要素に置き換えられたものになります。

<p><i>wryyy</i></p>

なお、name属性を指定せずに<slot>要素を使用すると、この<slot>要素はカスタム要素内の最初の子要素に置き換えられます。

SVGの要素をSVGタグ内に挿入する

HTML内に直接<svg>要素を書く場合にはあまり意識しませんが、実は<svg>要素やその子要素として指定する<path><circle>といった要素は、HTMLの要素ではありません。そのため、document.createElement()メソッドでは作成できず、代わりにdocument.createElementNS()メソッドを使用して作成します。このメソッドは第1引数としてネームスペースを指定する必要があり、<svg>要素やその子要素を作成する場合にはhttp://www.w3.org/2000/svgと言うネームスペースを指定します。

this._svg = document.createElementNS(`http://www.w3.org/2000/svg`, "svg");

npmで「npm install -g」ではなくnpxをおすすめする理由

npmではライブラリだけでなく、Node.jsで実装されたコマンドラインツールも多数配布されています。こういったツールをインストールする際に「npm install -g」のように-gオプション付きでnpm installコマンドを実行すると、そのコマンドがパスの通った場所にインストールされ、カレントディレクトリがどのディレクトリであってもコマンド名だけでそのコマンドを実行できるようになります。

たとえばこの方法でvueコマンド(vue-cli)をインストールする場合、次の例のようになります。

# npm intall -g @vue/cli

こうやってインストールすると、次のようにvueコマンドだけでvue-cliを実行可能になります。

$ vue

この方法は一見便利なように見えますが、npm install -gコマンドの実行時にroot権限が必要になるという問題があります。さらに、インストールしたコマンドは全ユーザーが利用できるようになるため、意図せずに他のコマンドと競合するといった副作用が発生する可能性もあります。

そのためおすすめしたいのが、-gコマンドを利用せずにローカルディレクトリにインストールし、npxコマンドを使ってインストールしたコマンドラインツールを利用するというやり方です。

npxコマンドは、カレントディレクトリを起点としてnode_modulesディレクトリを探索し、引数で指定したコマンドを見つけたらそれを実行する、という処理を行うコマンドで、2017年にリリースされたnpm v5.2.0からnpmに同梱されています(リリースノート)。

たとえば先のvue-cliを利用したい場合、まずこのツールを利用したいプロジェクトのディレクトリ内で次のように-gオプションなしでインストールを行います(ちなみに-DはインストールするモジュールをdevDependenciesに追加する処理を行うオプションです)。

$ npm install -D @vue/cli

こうやってインストールしたvueコマンドを使って、たとえば「vue create」コマンドを実行したい場合、次のように実行します。

$ npx vue create .

ちなみにここではプロジェクト名の代わりに「.」を指定していますが、こうするとvue-cliはカレントディレクトリ内にプロジェクトを作成してくれます。

この手法には、root権限が不要であるというだけでなく、プロジェクトごとに異なるバージョンのツールを簡単に利用できるようになるというメリットもあります。一方でデメリットとしては、プロジェクトごとにインストールが必要になり、またそのためディスク容量を余分に使用するというものがあります。とは言え、昨今のツールでそのデメリットはあまり大きくないでしょう。

ここではvueコマンドを例にしましたが、tscコマンドやwebpack-cliなどでも同様に使えます。

WebブラウザのHistory APIの挙動を知ろう

仕事でWebブラウザのHistory APIに関する質問を受けたのですが、意外と分かりにくいのでここでまとめておきます。

なお、History APIに関する大体のことはMDNのHistory API を取り扱うというドキュメントにまとめられていますので、こちらも参照しましょう。

履歴とは

Webブラウザ上では、閲覧履歴はスタック(本記事では「履歴スタック」と呼ぶ)として管理されます。履歴スタックはWebブラウザのウィンドウやタブごとに用意されています。ページの進む/戻る操作ではこの履歴スタック自体は変更されず、履歴スタック上の現在位置のみが変更されます。

Aタグなどであるページから別のページに遷移した場合、履歴スタック上の現在位置よりも先にある履歴項目が削除され、続いて履歴スタックの先頭ににそのURLを含む新たな履歴項目が追加されます。

履歴スタックのイメージ

History APIとは

History APIは、履歴スタックを操作するインターフェイスです(MDNのドキュメント)。履歴上で前に閲覧したページに戻ったり、戻った後に再度元のページに戻る、といった操作が可能です。ただし、Webブラウザの閲覧履歴はプライバシに関わるものとされるため、閲覧履歴のURLやタイトルなどへのアクセスはできません。

Webブラウザ上では、window.historyがHistoryインターフェイスを実装したオブジェクトになっています。

History APIの使用例1:前のページに戻る/次のページに進む

History APIでよく使われ、かつ理解しやすい使用例として、前のページに戻ったり、次のページに進む、というものがあります。これは、history.back()およびhistory.forward()メソッドで実行できます。

history.back()は、Webブラウザの「前に戻る」ボタンを押した場合と同じ挙動を実行します。また、history.forward()は「次に進む」ボタンを押した場合と同じ挙動を実行します。

「2つ前に戻る」「3つ先に進む」といった操作を行えるhistory.go()メソッドも用意されています。先に進む場合は正の数、前に戻る場合は負の数でいくつ進む/戻るかを指定します。たとえばhistory.go(1)history.forward()と、history.go(-1)history.back()と同じです。引数が指定されていない場合は、現在のページをリロードします。

history.lengthは履歴スタック上にいくつの項目(履歴)が存在しているかを示す値です。ただし、現在表示されているページが履歴スタック上のどの位置にあるかを知ることはできません。そのため、あまりこのプロパティのユースケースはないと思います。

History APIの使用例2:ブラウザのURLバーに表示されているURLを操作する

History APIでは、履歴だけでなくWebブラウザのURLバーに現在表示されているURLの操作を行うAPIも提供されています。history.replaceState()がそのメソッドです。

たとえば、URLバーに表示されているURLを「<現在表示しているWebページのドメイン>/foo/bar」に書き換えるには、次のように実行します。

window.history.replaceState(null, "", "/foo/bar");

クエリパラメータやハッシュを指定することも可能です。たとえば次のように実行すると、URLバーの表示内容は「<現在表示しているWebページのドメイン>/foo/bar?hoge=1#baz」になります。

window.history.replaceState(null, "", "/foo/bar?hoge=1#baz")

こういった操作は、JavaScriptを使って動的にページのコンテンツを変更するようなWebサイトで、現在の状態に特定のURLでアクセスさせたい(いわゆるパーマリンク、Permalinkを生成したい)場合に利用できます。

History APIの使用例3:新たな履歴を生成することなしにページ遷移を行う

さて、このhistory.replaceState()メソッドを実行すると、URLバーの表示内容が変わり、またwindow.location.hrefの値も指定したURLに変化します。一方でページの読み込みは行われず、またhistory.lengthの値も変化しません。これを利用し、ページをリロードするhistory.go(0)と組み合わせることで、新たな履歴を生成することなしにページ遷移を発生させることができます。

window.history.replaceState(null, "", "<遷移したいパス>");
window.history.go(0);

この場合、たとえば実行前のhistory.lengthが1なら、実行後もhistory.lengthは1のままです。

History APIの使用例4:ページ遷移なしに新たな履歴を生成する

History APIでは、逆にページ遷移なしに新たな履歴を生成することもできます。そのためのメソッドが、history.pushState()です。

たとえば次のように実行すると、履歴スタックの先頭に「<現在指定しているWebページのドメイン>/hoge」というURLが追加されます。

window.history.pushState(null, "", "/hoge");

このとき、URLバーの表示内容は「<現在指定しているWebページのドメイン>/hoge」になります。window.location.hrefもこの値になります。pushState()実行前に表示されているページが履歴スタック上の先頭であれば、history.lengthは1増えます。ただし、実際のページ遷移(ページの読み込み処理)は実行されません。

この状態でブラウザの戻るボタンを押したり、history.back()もしくはhistory.go(-1)を実行すると、URLバーの表示内容やwindow.location.hrefの内容はpushState()実行前のものに戻ります。ただし、その場合も実際のページ遷移(ページの再読み込み)は発生しません。

History APIの使用例5:実際にページ遷移が発生しない戻る/進む操作を検出する

前項で説明したように、history.pushState()で履歴を生成した後に戻るボタンを押したりhistory.back()もしくはhistory.go(-1)を実行すると、履歴スタック上の現在位置は変化しますが、ページ遷移は発生しません。こういったページ遷移なしの戻る/進む操作は、windowpopstateイベントで検出できます。

たとえば次のようにpopstateイベントに対するイベントハンドラを設定しておくと、ページ遷移なしの戻る/進む操作が発生した際にConsoleにそのイベント内容が出力されます。

window.addEventListener("popstate", (e) => console.log(e));

History APIの使用例6:履歴スタック上の履歴項目に任意の情報を紐付ける

history.replaceState()history.pushState()では、第1引数としてnullや任意のオブジェクトを与えることができます。このオブジェクトは、replaceState()の場合履歴スタック上の現在位置にある履歴項目内に、pushState()の場合は新しく生成された履歴項目内に格納されます。

履歴スタック上の現在位置にある履歴項目内に格納されたオブジェクトは、history.stateプロパティとして参照できます。

たとえば、あるページを表示している状態で、次のようにhistory.replaceState()stateオブジェクトとして{foo: 1}を設定します。

window.history.replaceState({foo: 1}, "");

この状態でhistory.stateを確認すると、指定したオブジェクトが格納されていることが分かります。

> history.state
{foo: 1}

その後、Aタグなどで別ページに遷移すると、history.stateの値は別のものに変わります。

> history.state
null

別ページへの遷移後、戻るボタンやhistory.back()などで元のページに戻ると、history.stateの値はそのページに紐づけたものに戻ります。

> history.state
{foo: 1}

なお、このhistory.stateは前述のpopstateイベントのイベントハンドラに渡されるPopStateEventstateプロパティとしても参照できます。

備考:履歴スタックが更新される条件

原則として、History APIを使用せずにページのURL(window.location.href)が変更されると、必ず履歴スタックは更新されます。たとえば<a href="#hoge">のようなハッシュのみを指定したAタグをクリックした場合、ページのロードは発生しませんが、履歴スタックは更新され、もし現在の履歴スタック上の位置が先頭だった場合、新たな履歴がスタックに追加され、history.lengthは1増えます。

x86_64版CentOS 5.5で64ビット版FirefoxでJavaプラグインを使う

 x86_64版CentOS 5.5のFirefoxでのJavaプラグイン導入に軽くハマったのでメモ。

 x86_64版のCentOS 5でFirefoxにJavaプラグインを導入したい場合、ググると「32ビット版Firefox+32ビット版Javaランタイムを使え」という話がぼちぼちとヒットするわけですが、とりあえずJavaプラグインだけを使いたい場合は64ビット版でも可能なようです。ていうか自分の場合、32ビット版を導入しようとしたらうまくいきませんでした……。

 ということで、手順。まず64ビット版Firefoxをインストール。

$ sudo yum install firefox  # 64ビット版Firefoxをインストール

 続いてjava.comのダウンロードページから「Linux x64 RPM」をダウンロード。

「Linux x86 RPM」をダウンロードする

「Linux x86 RPM」をダウンロードする

 RPMと書いてあるが、ダウンロードしたファイルはRPMではなくインストーラなので、root権限で実行してインストール。自動的にRPMがインストールされる。

$ chmod +x jre-6u21-linux-x64-rpm.bin
$ sudo ./jre-6u21-linux-x64-rpm.bin

 続いて設定。インストールしたjreを利用するようalternativesコマンドで指定。

$ sudo /usr/sbin/alternatives --install /usr/bin/java java /usr/java/jre1.6.0_21/bin/java 2
$ sudo /usr/sbin/alternatives --config java

 最後にmozilla用プラグインフォルダにプラグインのシンボリックリンクを張って完了。

$ cd /usr/lib64/mozilla/plugins/
$ sudo ln -sf usr/java/jre1.6.0_21/lib/amd64/libnpjp2.so .

 あとはFirefoxでabout:pluginsを開きプラグインが正しくインストールされているか確認したり、java.comにアクセスして動作を確認するなり適当にどうぞ。

激安・低サポートのVPS「prgmr.com」を借りる

 最近、月額490円からの「ServersMan@VPS」が話題だ。ほかにも980円からの「FC2 VPS」など、低価格のVPSはいくつかあるが、それ以前から存在する低価格のVPSサービスとして一部の間で知られていたのが「prgmr.com」である。

 prgmr.comはXenを使用したVPSで価格は$5/月からと低価格なのが魅力の1つだが、安さだけなら国内のVPSサービスと大きくは変わらない。ではなぜprgmr.comが注目されているのかというと、そのアレなたたずまいと、サポートの簡素さによるところが大きい。prgmr.comのトップページにはほかのVPSサービスのような画像による装飾は一切なく、アスキーアートで書かれた「prgmr.com」というバナーと「We don’t assume you are stupid.」という文言、そして料金やXenベースVPSの特徴が紹介されているのみ。一般人ならとりあえずこのサイトが何であるかも理解できないだろう、という作りである。

 このようにアレゲ感たっぷりのprgmr.comであるが、その一方でかなり自由度は高く、OSはDebian GNU/Linux、Ubuntu 10.04、CentOS 5.5が選択できるほか、AMD64アーキテクチャのXen上で動作するOSなら(自力で)任意のOSがインストールできる(可能性がある)、追加料金なしでグローバルIP付与と、独自のサーバー管理&Xenが分かる人にとってはかなり魅力的であったりする。

 ということで一部の人には魅力的なこのprgmr.com、ちょくちょく新規受付を開始しては定員に達して終了を繰り返しており、いつでも申し込みできるというわけではないのだが、ウォッチしていたらたまたま申し込み可能になっていたため、$12/月でメモリ512MiB、ディスク12GiB、月間ネットワーク転送量80GiBのプランを申しこんでみた。

Webでの申し込み後、メールでSSH公開鍵を送付。サーバー設定は必要十分

 Webで利用したいプランを選択し、オンラインフォームに必要事項を記入して申し込むと、下記のように利用したいディストリビューションとOpenSSH公開鍵を送信しろ、という旨のメールが来る。

you ordered a xen vps, 512MiB ram, 12GiB Disk 80 GiB transfer Xen VPS, $12/month username hylom

Before I can set you up, I need to know what distro you would like and an OpenSSH format public key (on a *NIX, run ssh-keygen and send me either the id_dsa.pub or id_rsa.pub file in an attachment)

 あとはこのメールの返信として公開鍵を添付し、Debian GNU/Linuxを使いたいという旨を伝えるだけだ。

Hi,

I want to use Debian GNU/Linux, and here’s my SSH public key.

Please check it.

 設定が完了すると、その旨がメールで連絡される。また、しばらくすると金払ってねメールがやってくる模様。料金の支払いはPayPal。

 続く。