JavaScriptのUIライブラリはどうあるべきかという話とOnsen UIのアーキテクチャ
Onsen UI Advent Calendar の12/9の記事です。
Onsen UIは、モバイルアプリのネイティブライクなUIをHTML + CSS + JavaScriptで簡単に構築することを目的としたUIライブラリです(UIフレームワークともたまに呼ばれます)。 ↓みたいなネイティブなモバイルアプリっぽい画面をサクッと作ることができます。
私は数年前から開発メンバーとして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では、特定のフレームワーク依存せず、かつどのフレームワークからでもある程度利用できるように次のような構成を取っています。
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>
リポジトリ的には次の場所に全て記述されています。
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.js
とonsenui.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ライブラリとしての大まかなアーキテクチャについてこの記事では説明しました。
「PHPに型推論を実装する〜入門編〜」という題でPHPカンファレンス福岡2016で話してきた
PHPカンファレンス福岡2016で型推論器ってどんな感じなのという話をしました。PHPカンファレンス福岡は去年も登壇したんですが今年は弊社もスポンサードしつつの登壇です。
参加者や運営スタッフの皆さんの対応含めて心地良い雰囲気だったので、ああ参加して良かったなと自然と思えるような素晴らしいイベントでした。参加者や運営のスタッフの皆さんお疲れ様でした!
JavaScript(ES2015)でvarやletを使う必要はほぼ無い
ES2015でvarやletを使う場面はほとんど無いので、まずconstを使う。constだとダメな場合にはletを使う。
背景
ES2015では、変数を宣言するための文法としてconstとletが導入された。
const foo = 'foo'; let bar = 'bar';
constは再代入できない変数を宣言できる。letは再代入できる変数を宣言できる。
const foo = 'foo'; foo = 'hoge'; // ERROR let bar = 'bar'; bar = 'hoge'; // OK
あれ、じゃあvarとletは同じなの?っていうとそうではなく、letやconstはvarとは違って、関数スコープよりも細かなブロック単位のスコープを提供する。例えばconstやletを使うと、if文やfor文などのブロック中でのみ有効な変数を宣言できる。
で、プロジェクトにES2015を導入すると、「お、varじゃなくてlet使えばええんやな!」と言わんばかりにletだけを使う人を見かけるんだけど、実際にはletもほとんど使う必要なくて、多くの場合はconstで問題ない。また、他人が書いたソースコードの中でletを見ると、「これ後で再代入されるん?」って一瞬身構えるので、再代入の必要がないならconstを使う方が可読性も良くなる。
ES2015になって変数の文法が増えたわけだけど、開発者はまずconstを使って、再代入が必要ならletを使って、letも使えない特殊な事情がある場合のみvarを使う、というようにするといい。
varは別に非推奨になったわけではないので普通に使えるんだけど、プロジェクトで機械的にvarを使わせたくない場合には、ESLintでno-varを有効にする。
Airbnbが公開しているES6に対応したJavaScriptコーディング規約においても、まずconstを使うこと、varは避けることが書かれている。
追記: よくよく調べてみると、ESLintには再代入しないletに警告を出すprefer-constがあった。より厳格にしたい場合はこのprefer-constを有効にする。
まとめ
ES2015では、varやletを使う必要のある場面はそれほどない。変数の宣言にはconstを使う。再代入が必要な場合にのみlet使う。
factorでズンドコキヨシを書いてみた
連鎖性言語のfactorで今流行り(?)のズンドコキヨシ書いてみた。連鎖性言語って何?っていう方はここの説明を見てほしい。
IN: . USING: random io kernel sequences math ; "" [ dup "ズンズンズンズンドコ" tail? not ] [ { "ズン" "ドコ" } random append ] while "キ・ヨ・シ!" append write
実行する。
$ factor zundoko.factor ズンドコドコズンズンズンズンドコキ・ヨ・シ!
昔触ってたfactorのこと完全に忘れててこれ書くのになんやかんやで1時間ぐらいかかったけど、たまにfactor書くとやっぱ面白い。最近の連鎖性言語をちょろっと調べてみたら、今はkittenというイケてる感じの関数型連鎖性言語も登場してるみたい。暇があったらまたこういうの触っていきたい。
JavaScriptでのDOM操作は重いのかという話とForced Synchronous Layoutについて
2015年にもなるのにJavaScriptでのDOM操作のパフォーマンスについて書く。ウェブページにインタラクションを持たせたい時に、JavaScriptでDOM操作を行うことがよくある。このDOM操作のパフォーマンスについて、よく聞く意見を大別すると次の2つがある。
- JavaScriptによるDOM操作は重たい
- レンダリングが重いだけで、DOM操作そのものはそれほど重たくない
JavaScriptでオブジェクトのプロパティを操作したりする単体の処理は通常1ミリ秒もかからないが、DOM操作をするとレンダリングが完了するまでに数十ミリ秒程度かかったりする場合がある。1番目のDOM操作が重たいと言っている人は経験則的にそう言っていることが多い。
レンダリングの仕組みを知っている人は2番目の意見を言うが、重箱の隅をつつくような話をするとこれも必ずしも正しいわけではない。DOM操作するコードによっては、JavaScriptの実行そのものをブロックするケースもあるからだ。この記事ではその辺りの事情についておさらい的に説明する。
DOM操作するとそもそも何が起こるのか
現在見ているウェブページのドキュメントのDOMツリーに属するDOM要素を操作をすると、JavaScriptの実行が終わってからブラウザのレンダリング処理が行われる。具体的にはWebkit系のレンダリングエンジンだと次の図のようになる。
ScriptingではJavaScriptの実行が行われる。DOM操作の処理もここに含まれる。
Calculate Styleでは、更新されたDOM要素ごとのスタイルが再計算される。例えば、DOM要素を生成してDOMツリーに追加すると、その影響で当たるスタイルを更新しないといけない要素が出てくるので、それらのDOM要素に当たるスタイルがCSSセレクタのマッチング処理とともに再計算される。
Layoutでは、計算されたDOM要素のスタイルを元に要素のレイアウトの計算を行う。いわゆるリフローと呼ばれるものはこれ。Firefoxだとリフローと呼ばれる。
Paintでは要素をレイヤごとにラスタライズする。Composite Layersではラスタライズした各レイヤを1枚に合成して最終的なレンダリング結果を生成する。
HTML5アプリでのインタラクションやアニメーションのレンダリングのパフォーマンス改善というのは、このDOM操作した後のこの一連の実行パスをどのようにして最適化するかという話である。
例えば、iOSやAndroidなどのモバイル端末で60FPSのアニメーションを達成しようとすると、LayoutやPaintなどの処理をやってたら速度的にちょっときつくなるのでなるべく最後のComposite Layersだけが実行されるようなコードを記述したりする。
DOM操作に伴うレンダリングの処理は、JavaScriptの実行そのものを遅くするわけではないので、そういう意味では後続するレンダリングが重いだけでDOM操作そのものは大して重くない、というのはおおむね正解である。実際ドキュメントのDOMツリーに属していないDOM要素を操作してもこれらのレンダリング処理は行われない。
ただし、JavaScriptのコードの書き方によってはForced Synchronous Layoutを引き起こして、JavaScriptの実行そのものをブロックする場合がある。
Forced Synchronous Layout is 何
Forced Synchronous Layoutとは、JavaScriptと同期する形で実行されるレイアウト計算処理のことである。通常このレイアウト計算は、JavaScriptの実行が終わってから初めて行われるので、JavaScriptの実行を遅くしたりするわけではない。しかしForced Synchronous LayoutではJavaScriptの実行中に同期的に処理されるので、JavaScriptの実行パフォーマンスを劣化させることがある。
DOM APIにはレイアウト情報を参照するメソッドやプロパティがいくつかある。具体的には、offsetHeightプロパティやoffsetWidthプロパティやgetBoundingClientRect()メソッド等がある。Forced Synchronous Layoutは、ドキュメントのDOMツリー内でDOM操作を行った後、そのDOM要素のレイアウト情報を参照するコードを書くと起こる。
例えば、DOM要素を生成してbody要素に追加する次のようなコードを見てみよう。
var div = document.createElement('div'); div.innerHTML = 'hogehoge'; document.body.appendChild(div);
このコードでは、JavaScriptの実行が終わってから初めてレイアウト計算が行われる。対して、次のコードではForced Synchronous Layoutが発生する。
var div = document.createElement('div'); div.innerHTML = 'hogehoge'; document.body.appendChild(div); // ここでForced Synchronous Layoutが起こる console.log(div.offsetHeight);
Webkitなどのレンダリングエンジンでは、JavaScriptによるDOM操作が行われてもすぐにレイアウト計算を行うのではなくJavaScriptの実行が一旦終わってから行うが、DOM操作を行った後にgetBoundingClientRect()やoffsetHeightなどのプロパティを参照すると、正しいレイアウト情報を返すためにレンダリングエンジンはその場でレイアウト計算を行う。
Forced Synchronous Layoutが起こっているかどうかは、ChromeのウェブインスペクタのTimelineでプロファイルを取ればわかる。
Forced Synchronous Layoutが発生したからと言って即パフォーマンス上の問題が出るわけではないが、ループ内などでForced Synchronous Layoutを引き起こすと、JavaScript実行中にレイアウト計算が何度も繰り返し起こるのでJavaScriptのパフォーマンスが大幅に劣化する事がある。
まとめ
JavaScriptでのDOM操作自体は大して重くない。重く見えるのはDOM操作に伴うレンダリング処理がJavaScriptの実行後に行われるからである。ただし、DOM操作するコードがForced Synchronous Layoutを引き起こす場合にはJavaScriptの実行パフォーマンスを劣化させる原因になる。
PHPカンファレンス福岡2015で「PHPで学ぶ仮想マシン型正規表現エンジンの仕組み」という題で発表してきました #phpconfuk
6/27に開催されたPHPカンファレンス福岡2015で「PHPで学ぶ仮想マシン型正規表現エンジンの仕組み」という題で発表してきました。
正規表現エンジンの実装は、大別するとDFA型とVM型の二種類あるのですが、今回のこの発表ではVM型の仕組みについて解説しました。
もともとこの話の元ネタは、数年前に読んだRegular Expression Matching: the Virtual Machine Approachという記事です。この記事を読んで、こんなに単純な仕組みで正規表現エンジンって実装できるのか!とちょっとした衝撃を受けて、その感動を伝えられればということで今回話しました。
ちょっと時間が足りず最後の方が早足になってしまって当日会場で話を聞いていた人にはわかりづらくなってしまいましたが、資料を一度ゆっくり見てもらえば多分理解できると思います。
PHPで学ぶ〜とか言いつつスライドにはPHPのコードは一行たりとも出てきませんが、スライド中で紹介している自分の記事の中ではPHPを使ってVMの実装をしています。
発表資料は以下になります。
セッションに参加していただいた皆さんありがとうございました。また、運営スタッフの皆さんや@cakephperさん @akase244さん @localdiskさんお疲れ様でした。
JavaScriptをプロトタイプベースのオブジェクト指向言語と言うべきではない
勘違いしている方も結構多いと思ったので、これの解説。
JSをプロトタイプベースのオブジェクト指向言語って言うの誤解しか産まない気がしてきた
— anatoo (@anatoo) 2015, 5月 3
ウェブ上の記事を眺めていると、JavaScriptをプロトタイプベースのオブジェクト指向言語(以下OOPと書く)と説明している例がよく散見される。この書き方は間違ってはいないかもしれないが、もはや誤解を生むだけである。
そう言う理由には、ES6からはclass構文があるため、通常のクラスベースのOOPと一切何も変わらなくなっているからというのがある。だが最も大きな理由は、JavaScriptはプロトタイプベースのOOPの中でも異端であり、本来のプロトタイプベースのOOPとはあまり似ていないからである。
JavaScriptでは、new演算子を使ってオブジェクトを生成したり、prototypeオブジェクトを使ってオブジェクトのメソッドやプロパティを定義したりするが、他のプロトタイプベースのOOPではnew演算子は無いし、prototypeオブジェクトも無い。
この記事では、本来のプロトタイプベースのOOPがどういったものであり、JavaScriptとはどう違うのかを説明する。
JavaScriptの確認
まずは、JavaScriptでオブジェクトを定義・生成する方法を確認していく。
コンストラクタとして、まずは関数を定義する。
var MyObject = function(name) { this.name = name; };
次に、定義した関数オブジェクトのprototypeプロパティにプロパティを定義することで、オブジェクトがインスタンス化された時のプロパティやメソッドを定義できる。
MyObject.prototype = { createGreeting: function() { return 'Hello, ' + this.name; } };
オブジェクトをインスタンス化する際には、new演算子を使って生成する。
var obj = new MyObject('JavaScript'); obj.createGreeting(); // => 'Hello, JavaScript'が返る
さて、プロトタイプベースのOOPではこういったオブジェクトの定義や生成はどうやって行うのか。
プロトタイプベースのOOPでは?
純粋なプロトタイプベースのOOPでは、クラスやJavaScriptのprototypeオブジェクトのようなものは無い。新しいオブジェクトを作るには、new演算子によるインスタンス化ではなくオブジェクトの複製を作ることで自分が欲しいオブジェクトを作成していく。
ここでは、プロトタイプベースのOOPの例として、ioという言語で説明する。ioは純粋なプロトタイプベースのOOPだ。また、ioには制御構文用の文法が無く全てが(if文ですらも)メソッドである。
まずは、先ほどJavaScriptで書いたようなMyObjectをioで作ったのが以下だ。
// オブジェクトの複製を作る MyObject := Object clone // メソッドの定義 MyObject createGreeting := method("Hello, " .. self name) // プロパティの定義 MyObject name := "Your Name"
ioの文法のほんの少し説明すると、:=演算子は変数を宣言する演算子である。また、JavaScriptではオブジェクトのプロパティのアクセスに"."演算子を使うが、ioでは単にスペースを使う*1。また、引数がない場合にはメソッド呼び出しに()は不要だ。
hoge := "hogehoge" // JSだとvar hoge = "hogehoge";になる hoge size // JSだとhoge.sizeになる hoge print // printメソッドの呼び出し
先ほどの例では、MyObjectというオブジェクトを作っているが、重要なことはこのMyObjectはすでに利用できる一個のオブジェクトだということだ。例えば、ここでcreateGreetingメソッドを呼び出せばそれはそのまま動作する。JavaScriptではこうはいかない。
MyObject createGreeting // "Hello, Your Name" が返る
ioでは新しくオブジェクトを作るときには、クラスをインスタンス化してオブジェクトを生成する代わりに、すでにあるオブジェクトを複製することでオブジェクトを生成する。
myobj := MyObject clone myobj name = "io" myobj createGreeting // => "Hello, io" が返る
さて、ioのやり方を見るとJavaScriptとはだいぶ違うことがわかる。ioなどのプロトタイプベースのOOPではオブジェクトの定義や継承や生成などの処理を全てcloneを通じて行えるので非常にシンプルだ。それに比べるとJavaScriptのprototypeオブジェクトを使うやり方は何かちぐはぐな印象を与えてくる。
GoFのデザインパターンの一つにオブジェクトの生成に関わるプロトタイプパターンというのがあるが、プロトタイプベースのOOPではこのデザインパターンを言語の設計レベルで全面的に採用することでnew演算子やクラスの存在を消してしまっている。
JavaScriptでは、プロトタイプ(ひな形)となるオブジェクトは関数オブジェクトのprototypeプロパティに代入したオブジェクトになるが、これは通常のオブジェクトとしてそのまま利用できるわけではないし、これはある意味クラスを表現するクラスオブジェクトのような形になっている。
ioではそういったオブジェクトは無くプロトタイプ(ひな形)となるのはそのオブジェクト自身である。ioではオブジェクトの複製を通じてのみ新しいオブジェクトを生成できる。
こうやって書いてみると、JavaScriptが通常のプロトタイプベースのOOPとどれほどかけ離れているかがわかる。JavaScriptはクラスを持たないためプロトタイプベースのOOPとして分類できるのかもしれないが、その仕組みを観察してみると、クラスベースとプロトタイプベースの合いの子のような妙な形になっている。
終わりに
個人的な意見になるが、JavaScriptをプロトタイプベースのOOPと言い切っている人は、プロトタイプベースのOOPがどういったものかを理解していない、もしくはioやSelfのような言語を触ったことがない人だろう。なぜならJavaScriptは何の留保も無しにプロトタイプベースと言い切るにはあまりに奇妙だからだ。何にせよ、ES6からはclass構文が追加されたので、JavaScriptをプロトタイプベースのOOPと言うのはもはや適切ではないだろう。
追記: ブックマークコメントへの返信
id:oscdis765 Even though ECMAScript includes syntax for class definitions, ECMAScript objects are not fundamentally class-based such as those in C++, Smalltalk, or Java.
http://b.hatena.ne.jp/entry/249961310/comment/oscdis765
id:otherworld インスタンス化に使うキーワードが違うだけで、プロトタイプチェーンを辿ってプロパティ/スロットを解決するというコンセプトは同じじゃ?? jsをクラスベースと呼ぶ方が無理がある
http://b.hatena.ne.jp/entry/249961310/comment/otherworld
この記事では、JavaScriptをクラスベースのOOPと呼ぶべきという主張をしているわけではないです。プロトタイプベースの、という枕詞を付けても説明として何の意味もないという話です。
*1:余談だがioではプロパティと言わずにスロットと呼ぶ