JavaScriptのUIライブラリはどうあるべきかという話とOnsen UIのアーキテクチャ

Onsen UI Advent Calendar の12/9の記事です。

Onsen UIは、モバイルアプリのネイティブライクなUIをHTML + CSS + JavaScriptで簡単に構築することを目的としたUIライブラリです(UIフレームワークともたまに呼ばれます)。 ↓みたいなネイティブなモバイルアプリっぽい画面をサクッと作ることができます。

f:id:anatoo:20161209200818p:plain

私は数年前から開発メンバーとしてOnsen UIの設計開発を行っています。この記事では、Onsen UIに求められるUIライブラリとしての要件とそれを解決するためにどのようなアーキテクチャを取っているのかについて解説します。

特定のフレームワークに依存しない

jQuery UIやReactの上に乗っかっているUIライブラリなどのように特定のフレームワークの仕組みを使って実装されたUIライブラリというのはたくさんありますが、ある特定のフレームワークに依存することは避けるべきだという考えのもとにOnsen UIは開発されています。

ある特定のフレームワークに依存したUIライブラリを作ると、その他のフレームワークから使ったり素のJavaScriptから使うことが困難になります。例えばAngularJSに依存したUIコンポーネントは他のフレームワークから利用することは基本的にできなくなります。技術的にはできなくも無いでしょうが、それをわざわざやりたいと思う開発者はおそらくいないでしょう。

UIライブラリの提供者側にとっても特定のフレームワークに依存するのはリスクがあります。もしその依存しているフレームワークが使われなくなった場合に、UIライブラリを書き直すことになる可能性があります。

これは実際に起こったことですが、Onsen UIの1系はAngularJSのdirectiveとして実装されていたので、AngularJSと互換性の無いAngular2が登場した時にdirectiveとして実装されていたすべてのUIコンポーネントを書き直したことがありました。

フレームワーク非依存にするためにしていること

Onsen UIでは、特定のフレームワーク依存せず、かつどのフレームワークからでもある程度利用できるように次のような構成を取っています。

f:id:anatoo:20161211015934p:plain

CSS Components層は、Onsen UIが提供する最もレベルの低いコンポーネント層です。これは主にすべてのUIコンポーネントの見た目を提供します。CSS Componentsという名前のとおり、CSSファイルとして提供されます。

Web Components層では、Custom ElementsのAPIを使ってCSS ComponentsにJavaScriptで振る舞いを与えます。Custom Elementsを使っているので、素のJavaScriptからでも、フレームワークからでも同じように扱えるように設計されています。

Framework Bindings層では、各種フレームワーク用のバインディングを記述します。現在のところAngular1, Angular2, React.js,とVue2用のバインディングが記述されています。

CSS Components層

CSS Components層では各コンポーネントがCSSだけで完結するCSS Componentsとして実装されています。CSSだけで実装できるもの、すなわちコンポーネントの見た目はここで実装されています。

このCSS ComponentsはAdobe製の高速CSSフレームワークであるTopcoatをフォークして開発されたものです。CSSメタ言語としてStylus、CSSコンポーネントのドキュメントの記述にはtopdoc、設計規約としては高速なCSSセレクタを記述することができるBEM+MindBEMdingを採用しています。

開発者はCSS Components層が提供するCSSのみを使うこともできます。例えば、Onsen UIのリポジトリのCSSファイルを読み込んで、swtichコンポーネントのタグを記述するとiOSでよく見るSwitchのUIが表示されます。

<link href="https://unpkg.com/onsenui@2.0.4/css/onsen-css-components.css" rel="stylesheet" /> 
<label class="switch">
  <input type="checkbox" class="switch__input" checked>
  <div class="switch__toggle">
    <div class="switch__handle"></div>
  </div>
</label>

f:id:anatoo:20161209195916p:plain

リポジトリ的には次の場所に全て記述されています。

Web Components層

先ほどのCSS Components層の上に位置するのがWeb Components層です。CSSで記述された見た目に対してJavaScriptで振る舞いを追加します。

ここでは独自のHTML要素を定義することが出来るCustom ElementsのAPIを使ってCSSコンポーネントに振る舞いを追加するカスタム要素を定義しています。ドキュメントを見ると現在は約40程度のカスタム要素が定義されています。

例えば<ons-button>という要素があるのですがこれはCSSコンポーネントして実装したbuttonにCustom Elementsを被せたものです。

<button class="button">...</button> <!-- CSSコンポーネント -->
<ons-button>...</ons-button> <!-- ons-buttonカスタム要素 -->

Custom Elementsとして実装すると、そのDOM要素のプロパティやメソッドや属性などの振る舞いを定義することができます。これを使って素のJavaScriptやjQueryなどからでも扱える、かつAngular2やReact.jsやVue.jsなどのフレームワークからでも扱えるコンポーネントを定義することができます。

リポジトリ的には次の場所に全て記述されています。Custom Elements以外にも各種JavaScriptのAPIも提供しているのでこの部分をひっくるめてcoreとも呼ばれます。Onsen UIのonsenui.jsonsenui.cssはこのcoreから生成されます。

Framework Bindings層

この層では、各フレームワークごとのラッパーを定義しています。このラッパーはOnsen UIではバインディングと呼ばれています。現在対応しているフレームワークにはReact.js, AngularJS, Angular2, Vue.jsなどがあります。jQueryや素のJSから利用する場合には、このバインディングは利用しません。

開発者は、利用するフレームワークに合わせてこのバインディングも利用します。

なんでこのバインディングがあるかというと、Web Componentsを提供していたとしても、そのカスタム要素のプロパティやメソッドにアクセスできなければ意味がありません。各フレームワークごとにコンポーネントの操作をどのように行うかについても流儀が異なります。

例えば、React.jsではそもそもコンポーネントのメソッドを叩くといった操作はしないのが普通なので、Custom Elementsが持つメソッドに依る操作をReact.jsのコンポーネントのpropsやstateによって管理する必要があります。AngularJSやAnglar2の場合にはDOM要素が持つメソッドをDirectiveから叩けるようにする必要があります。

Framework Bindings層ではこのフレームワークごとに異なる流儀を吸収しながら、カスタム要素に対するインターフェイスを提供しています。

フレームワークによってどういうふうに書き方が変わるかというのは次の公式ブログの記事にも書かれています。

リポジトリ的には次の場所に全て記述されています。bindingsディレクトリ以下にフレームワークごとのnpmパッケージが提供されています。

まとめ

Onsen UIは様々なフレームワークに対応するために、Custom Elementsを使っています。その際のUIライブラリとしての大まかなアーキテクチャについてこの記事では説明しました。