2015年12月3日
Web開発の未来 – React、FalcorおよびES6
本記事は、原著者の許諾のもとに翻訳・掲載しております。
この記事でWeb開発の未来を垣間見ることができるでしょう。UIの構築やサーバ、データ・エンドポイントの新しい見解を得ることができると思います。ここで、ブラウザとサーバコードの両側を含めたフルスタックな話をしていきます。これを読めば、 完全に機能するGitHubリポジトリ で紹介されたすべてのコードの検証や実行ができるようになります。皆さまが開発者として次の資質を持っていることを前提に話を進めていきます。
- JavaScript中級者
- HTML中級者
- クライアント/サーバ間通信の基礎知識
- JSONの基礎知識
- Node.jsの基礎知識
上の知識がなくても、 おそらく この記事の進行についていけるでしょう。しかし、知識がないと私の紹介するコードを現実的なシナリオあるいは重要なシナリオに応用するのは難しいでしょう。インターネットは情報の宝庫なので、理解に必要な概念などをたくさん提供してくれます。必要に応じて、どんどん検索してください。
Widen の最新の組み合わせは従来からサーバがJava、ブラウザ関係のコードが全てAngularJS(数年前から)、REST APIサポートにはJersey、またjQueryやunderscore、lodash、jQuery UI、そしてBootstrapなど、多数のライブラリによって構成されています。この後概説するWebアプリの基礎的サンプルをデザインする時に、次の4つの目標を念頭に置きました。
- 洗練された 新しい アプローチ 。代わり映えのしないAngularJSベースのUIを開発したり、jQueryに頼ったり、Jerseyを使用したJavaベースのエンドポイントサーバを作成したり、これら全部をしたりするのではなく、全く新しいツールを役立てたいと思いました。これによって、新しい見解が得られ、開発者として少し進化できると期待しました。
- シンプル にすることが望ましいと思いました。AngularJS 1.x関係の学習過程には時間を要するため嫌だと感じていましたが、AngularJS 2の学習曲線がさらに急であることが分かり絶望しました。従来私がサーバ側で使用しているJavaでも同じことが言えます。定形型のコードをできるだけ避け、拡張性を損なうことなく自分のアプリを迅速に立ち上げ、実行したいと思っています。フロンドエンドを独立型コンポーネントの集合体として簡単に記述できるようにすることも目標の一部です。また従来のREST APIの維持と展開は厄介です。フロントエンドの開発者はバックエンドの開発者と協力して、モデルのブラウザ側表現を適切にサポートするAPIエンドポイントのセットを開示する必要があります。UIに対する要求が変われば、APIも変える必要がでてくることがしばしばあります。でも、もっと良い方法はあるはずです。
- 従来のREST APIに関係する問題には、不要なオーバヘッドや大量のリクエスト、そして必要以上に大きいレスポンスペイロードなどが含まれます。クライアント側のレンダリングパフォーマンスについては、ReactとAngularJSがうまく処理してくれるので、あまり心配していませんでしたが、Angularは融通が利かなくて複雑なので、パフォーマンスに深刻な障害をもたらす問題を知らず知らずに簡単にアプリに導入してしまう恐れがあります。そのため、 効率 が私の4つ目の目標になります。
- 柄にもなく、私は エレガント なコードが書けるアプローチやツールを探していました。コードは読みやすいものでなければなりません。UIからのデータ検索や変更は直感的であるべきです。理想は、利用可能なAPIエンドポイントには関係なく、自分のモデルのみを考慮したいと思います。また、従来のスタックに必要な煩わしい常用文もなるべく避けたいと思います。
各目標に取り組むために、既存のツールの代わりに今まで使用していたことのない 全く 新しいツールに替えることにしました。良い学習経験となりましたので、ぜひご紹介したいと思います。実際、Widenの新興ソフト製品のいくつかはこの記事で説明する新しい技術を駆使しています。次に、注目すべき新しいツールを実証します。技術の組合せを明確にしたあと、新しいシンプルなWebアプリの作成を最初から最後まで見ていこうと思います。ここで作るアプリは、機能的で、そこまでで紹介した技術の組合せを全て自然に組み込んだものにします。
組み合わせの未来像
全く新しいツールとアーキテクチャを採用することは大抵、開発者としての見解を変えることを意味します。プログラマとして経験していくうちに好みのツールが1つや2つはできるでしょう。jQueryやAngular、Ember、はたまたRESTの概念そのものなど、好みのツールがどれであれ自分の組み合わせを信頼することになります。我々は、Webアプリを特定の条件に当てはめて考えるよう刷り込まれてきました。使い慣れた組み合わせを捨てて快適な場所から離れると、フラストレーションを引き起こすかもしれません。中には使い慣れたものを手放したくないばかりに、新しい組み合わせを「不必要だ」「複雑すぎる」など見なしてしまう人もいるでしょう。正直に言うと、私も理解するまではReactやWebpack、Falcorについて、そう思っていました。このセクションでは、簡単に注目のツールを組み合わせの未来像として話を勧めます。
React
限られた機能を提供するReactはAngularやEmberなどとは異なります。AngularやEmberはフレームワークとして位置づけされていますが、Reactは主にアプリの”ビュー”を作成するためのものです。Reactは依存性の注入や”サービス”のサポートがありません。”jq-lite”(Angular)も必要とされるjQuery依存性(Ember)もありません。Handlebar(Ember)の代わりに、JavaScriptと同時にJSXを使用してマークアップを書き込みます。すると、書いたマークアップはJavaScriptの一連の呼び出しにコンパイルされます。この一連の呼び出しは、React Element APIを経由し、Reactが保持する”仮想DOM”の一部としてドキュメントを構築します。これにより、不要なリフローや再描画を避け、イベントハンドラをデリゲートし、仮想モデルから”実際の”DOMへの更新を最大限効率的に行うことができます。JSXを使う場合、作業工程にコンパイルの段階を追加することになります(私の経験から、JSXは採用するべきだと思います)。今まで私はこれをしばらく避けていましたが、JSXを通して見るReactがどれだけエレガントで便利か分かってからは、この工程を受け入れることにしました。この時点で、自分の中のこだわりが解消され、WebpackやBabelなど、他の便利なJavaScriptプリプロセッサを受け入れやすくなりました。このことについては、後でさらに話します。
簡単に言うと、Reactの機能が比較的限られているところを私は評価しています。複雑なアプリを小さいコンポーネントに分割するところがAngularのとても気に入っている点でした。AngularでWebコンポーネント仕様という形でネイティブのサポートが実現できるかもしれないと喜んだものの、最終的にはエレガントで使いやすくフットプリントの小さい、比較的成熟したReactを選びました。
Falcor
FalcorはNetflixの とても 新しいオープンソースライブラリで、従来のREST APIからは全く脱却しています。あらかじめ定められた固定のデータセットを返す複数の特定されたエンドポイントにフォーカスするのではなく、 単一 のエンドポイントしか持たないのです。通常Falcorはこのように説明され、確かに技術的には正しい説明なのですが、少し誤解を招く恐れがあります。多種のサーバエンドポイントに注力してモデルデータの情報を取得・更新してもらうのではなく、特定のモデルデータをAPIサーバに”要求”するのです。例えば、顧客リストから最初の3名の名前と年齢がほしいとします。この場合、APIサーバに、1つのリクエストとしてこの特定のデータを”要求”するだけで良いのです。もし、最初の2名の名前のみが欲しい場合はどうでしょう。ここでも、同一のエンドポイントに対して単一のリクエストをすれば良いのです。これら2つのGETリクエストの違いは、対象のモデルプロパティに関する詳細を含む各クエリパラメータを調べれば分かります。サーバ側でモデルプロパティの特定の組み合わせやパターンに応じたハンドラは、”ルート”の一部として体系化されます。APIリクエストを処理する際に、Falcorルータ(サーバ側)はクエリ文字列にあるアイテムに応じて適切なルータ関数を呼び出します。
データは JSONグラフ としてモデル化されます。下層のデータソースは全てのデータを必ずしもJSONグラフとして保持する必要はなく、ほとんどの場合JSONグラフとして保持しません。しかし、Falcor APIエンドポイントはクライアントリクエストに対して、JSONデータを提供できなければなりません。この構造の必要性の理由は、この記事を読んだり、Falcorについて調べたりすれば明確になってくるでしょう。JSONグラフとしてデータを整理する重要性については、 参照ルート について学習を始めれば、明らかになるでしょう。分かりやすい内容にするため、本記事ではこれについて触れていません。
自分のモデルに近づけるよう、より直感的なAPIをFalcorは促進しています。余分で不要なモデルデータは絶対に返されないようにして情報量を制限しています。さらに、ブラウザ側の複数の異種コンポーネントからのリクエストは、HTTPオーバヘッドを制限するため、単一のリクエストに集約されます。Falcorは複数のリクエストを単一のネットワークリクエストにまとめることができ、すでに発行中のものがあれば、重複したデータベースリクエストを発行しないようになっています。データはFalcorクライアント側にキャッシュされ、キャッシュ済みのデータをそれ以降にリクエストした場合はサーバへ再度アクセスしません。データソースからのモデルの切り離しに加えて効率を上げる対策は非常に魅力的であります。しかし、根底にあるコンセプトの理解には心が折れます。Falcorのリードデベロッパの Jafar Hasainのこのビデオ を見るまでは、Falcorを理解できていませんでした。
Webpack
Webpackはビルド時に使えるNode.jsライブラリで、フォーカスの小さいReactコンポーネントをさらにモジュール化するのをサポートします。また、CSSやJavaScriptの連結・minifyだけでなく、デバッグを簡単にしてくれるソースマップの生成をも容易にしてくれます。Webpackをインストールしいくつかオプションを設定すると、コードを監視し、変更の都度新しい”バンドル”を生成するようになります。クライアント側からCSSファイルやJSファイルを多数インポートする代わりに、Webpackによって生成されたバンドル(バンドルの数は仕様設定によります)をインポートする方が簡単で、ページロード時の不要なHTTPリクエストを防ぐことができます。Webpackには大量のプラグインがあるため、生成されたバンドルに”影響”を与えることが可能です。例えば、”jsx-loader”プラグインを使用すれば、JSXをJavaScriptに変えることが可能です。「ECMAScript 6でコードを書きたいけれど、ES6仕様を完全に実装したブラウザだけしかサポートしない事態は避けたい」のであれば、Webpackのバンドル生成プロセスとして”babel-loader”プラグインを使って、ES6コードをES5対応コードに変換できます。
ES6
ECMAScript 2015としても知られるECMAScript 6は、今のところ、最新のJavaScriptの言語仕様です。ファットアロー関数やクラス構文、文字列の補間、そしてブロックスコープを作成する能力など重要な新機能が定義されています。ES6は、最新のブラウザに限定されていますが、私を含め、多くの開発者が魅力的に感じるエレガントなソリューションやシンタックスを提起しています。ES6以前のバージョンでは、これらの機能を使用したい場合はCoffeeScriptあるいはTypeScriptで我慢するしかありませんでした。しかし今は、これら高レベルな抽象化の素晴らしさを下層言語でも利用ができます。やった! 古いバージョンのブラウザでもES6コードを実行できるようにする場合、 Babel というコンパイラを使用すれば、ビルドステップの一部としてES6コードをES5コードに変換できます。あなたがサポートすべき全てのブラウザがES6の仕様を実装した後は、このビルドステップをただ削除すればいいのです。
シンプルなアプリの構築
上記で概説した新しい項目のすべてを実証するために、簡単な単一ページのアプリケーションを作成しました。不自然な例ではありますが、これらの技術がすべてまとまって基本的なレベルでどのように働くかを理解できるように作られています。ここで得た知識を元に私のコードを拡張して、もう少し現実的なものを構築できるはずです。このサンプルアプリは、名前のリストを読み込み・修正することができるというものです。リストはサーバ上に保持されており、ページロード時にユーザに対して表示される名前の初期リストを含みます。リストに対する変更はブラウザ内で開始され、サーバに戻して保存されます。
セットアップ
まず、このアプリケーションを論理的な部分に分割することから始めましょう。最も基本的なレベルには、2つの部分、つまりクライアントとサーバがあります。クライアントは全体がブラウザ内に存在し、サーバはシンプルなAPIエンドポイントです。クライアント上に、ユーザが名前のリストを見て操作できるようにするためのインタフェースを表示しなければなりません。このリストは、クライアントとサーバの両方が理解できるモデルによって表現されます。本質的に、そのモデルは“データベース”内にJSONフォーマットで表現され、オブジェクトの配列により構成されており、各オブジェクトは name
属性を有します。バックエンドのストレージシステムではモデルをJSONフォーマットで表現することは必要条件では ありません が、そうすることによって、サンプルアプリのセットアップと理解が少し簡単になります。
コードを分割する前のもう1つの重要なステップは、(できる限り)相互に依存しない再利用可能なコンポーネントの観点からアプリケーションを考えることです。クライアント側には3つのコンポーネントが考えられ、1つは名前をリスティングするもの、もう1つは名前の追加をするもの、3つめはこれら2つのコンポーネントをまとめるものです。この第3のコンポーネントは重要です。その理由は、これによって、「リスト」コンポーネントと「名前追加」コンポーネントの間に明示的な依存関係をコーディングしなくても済むからです。3つのコンポーネントのうち、少なくとも2つはサンプルのアプリケーション以外で再利用可能です。
サーバ自体も複数の部分に分割することができます。最も上のレベルにはHTTPサーバがあり、静的リソースを公開し、APIリクエストをルーティングします。また、Falcorクライアントからの様々なモデルリクエストに対するサービスを行う一連のコードブロックも必要です。そのようなリクエストハンドラまたはルートのそれぞれは、別々の部分として考えることができるでしょう。最後にデータのストアがあり、これは初期値も提供します。APIエンドポイントハンドラは、このデータストアにデータの検索と永続性を任せます。
依存関係については次のセクションで全体的に説明しますが、私たちの最も基本的なライブラリとフレームワークは以下のものから構成されます。
1 React
2 Falcor
3 Express
4 Babel
5 Webpack
6 Node.js
このサンプルアプリケーションのすべての依存関係はプロジェクトの package.json ファイルで見ることができます。
サーバの作成
サンプルアプリのサーバ側部分の説明を読むときは、プロジェクトのGitHubリポジトリ内の server.js ファイルを参照してください。サンプルのサーバはAPIリクエストを処理して、静的リソース(JavaScript and HTMLファイルなど)を提供します。この部分はNode.jsを使用してJavaScriptで記述され、FalcorとExpressに依存します。
作業の開始
簡単にするために、サーバは単一のJavaScriptファイルを使用して表現します。最初の論理ステップは、全ての依存関係を参照することです。
var FalcorServer = require('falcor-express'),
bodyParser = require('body-parser'),
express = require('express'),
Router = require('falcor-router'),
app = express()
Falcorに関連する依存関係が2つあります。1つめの FalcorServer
は、APIリクエストを最も適切なハンドラに渡すために使用されます。 Router
は、これらのハンドルの全てを 定義 するために使用するものです。各ハンドラは、そのリクエストに関連するモデルデータのタイプを定義するルート文字列に結びつけられます。これは少し謎めいたことのように思われるかも知れませんが、サーバ側のセクションの説明が終わるまでには謎が解けるはずです。
その他の3つの依存関係は、静的リソースをサービスし、HTTPリクエストを構文解析してより管理しやすい形にするために役立ちます。例えば、 express
はすべてのHTTPリクエストを特定のポートで監視して、静的リソース(JSやHTMLのファイルなど)を提供するか、リクエストをFalcor Router
などの、より的確なハンドラにルーティングします。
次に、データストアを定義しましょう。ここでも簡単にするために、シンプルなJavaScriptオブジェクトを通してデータを直接ノードサーバ内で維持します。
var data = {
names: [
{name: 'a'},
{name: 'b'},
{name: 'c'} ] }
このストアは、ユーザに対して表示される初期の名前リストも含みます。
リクエストハンドラ
続いてリクエストハンドラを設定しましょう。
app.use(bodyParser.urlencoded({extended: false}));
app.use('/model.json', FalcorServer.dataSourceRoute(() => new NamesRouter()))
app.use(express.static('.'))
app.listen(9090, err => {
if (err) {
console.error(err)
return
}
console.log('navigate to http://localhost:9090')
});
最初の行で、 bodyParser
ライブラリに「application/x-www-form-urlencoded型のメッセージボディを含む任意のリクエストを受けとり、内容を構文解析してJavaScriptオブジェクトにする」という役割を与えます。このオブジェクトはexpressによって維持される Request
オブジェクト上の body
プロパティ として引き渡されます。サンプルアプリケーションでは、リストに新しい名前を追加し、URLエンコードされた名前データを含むPOSTリクエストがこのハンドラによって構文解析されてから、expressによって、より的確なハンドラに送られます。
2行目で、“model.json”エンドポイントに対する 任意の リクエストをFalcorルータに任せるようにexpressに命令しますが、このルータはこの時点では定義されていません(まもなく定義します)。したがって、“http://localhost:9090/model.json”へのGETリクエストはここで処理され、同じエンドポイントへのPOSTリクエストも同様です。URLエンコードされたbodyを持ち、このエンドポイントへ送られるPOSTリクエストでは、bodyはまず bodyParser によって構文解析されてからルータに渡されます。
3行目で、プロジェクトのルートにある任意の静的リソースを提供します。このワイルドカードはプロダクションには適しませんが、このようなタイプの簡単なデモには十分です。理想的には、全てのソースファイルが提供されることがないように静的リソースへのアクセスを制限すべきです。
最後に、上のコードはexpressがポート9090の全てのHTTPリクエストを監視するように命令します。このポートは、クライアント側で静的リソースおよびAPIへのアクセスに使用しなければならないポートです。ここでECMAScript 6構文、特に アロー関数 を使用していることに注意してください。このケースでは、アロー関数は、関数の引数を表現するためのよりエレガントな方法です。ES5構文では、リスナー関数は次のコードと同じになります。
app.listen(9090, function(err) {
if (err) {
console.error(err)
return
}
console.log('navigate to http://localhost:9090')
});
APIルートハンドラ
次に、3つの異なるAPIリクエストに対応するルーティングを作成します。
名前の数
まず、クライアントがリストにある名前の数を問い合わせると考えます。この情報の必要性は、次のルーティングを定義する時に分かります。Falcorのルータがリスト内の名前の数を提供します。以下をご覧ください。
var NamesRouter = Router.createClass([
route: 'names.length',
get () => {
{path: ['names', 'length'], value: data.names.length}
}
]);
先程参照した NamesRouter
を覚えていますか? これがFalcorのリクエストルータで、expressが適切なAPIリクエストをすべてここに送ってくれます。私たちのlengthルートはシンプルですが、まだ説明していないFalcor特有の構文が出てきます。 route
プロパティはリクエストの”シグネチャ”を定義します。もしFalcorのクライアントがすべての名前の長さを問い合わせたら、サーバサイドのFalcorのルータはこのルート文字列を照会し、 get
関数を実行することにより、返されるべきデータをFalcorのクライアントに明示するのです。
このレスポンスは2つのプロパティで構成されています。私たちのケースでは、1つめのプロパティは path
で、単にルートのシグネチャをそのまま返します。 get
ハンドラの2つめのプロパティは、 value
です。もうお分かりかもしれませんが、これはリスト内にある実際の名前の数を保持します。バックエンドのデータストアを構成する配列にある length
プロパティをチェックすることで簡単にこの値を引き出しています。
n
個の名前レコードの名前を表示
ページをロードする時、ユーザに対してデータベース中の何かしらの名前だけでも表示したいと思いますよね。今の時点では、名前レコードや名前レコードのID、その他これらの名前に関するメタデータといった概念が何もありません。目標はシンプルで、ユーザに名前を表示することです。では、どうやるのでしょう。まず、データベース内にある名前の数の合計をサーバに問い合わせます。次に、指定範囲の名前を返すクエリを構築します。ここで、データベース内の すべての 名前が欲しいとしましょう。その場合、インデックス0で始まり、名前の数-1で終わる名前の値をサーバに要求します。先程、”名前の数”のルートがどのようになるかはお見せしましたね。以下は範囲のパラメータを与えられた上で実際の名前を返すルートです。
{
route: 'names[{integers:nameIndexes}]["name"]',
get: (pathSet) => {
var results = [];
pathSet.nameIndexes.forEach(nameIndex => {
if (data.names.length > nameIndex) {
results.push({
path: ['names', nameIndex, 'name'],
value: data.names[nameIndex].name
})
}
})
return results
}
}
route
プロパティにより、このルートは、「クライアントがある範囲の名前を要求した場合に呼び出されるもの」だと定められます。さらに具体的に言えば、「クライアントが名前レコードの name
プロパティのみに着目して要求している場合」ともいえます。このルートハンドラは、範囲パラメータの各インデックスについて path
と value
のプロパティを持つオブジェクトを生成します。
例えば、DB内の最初の2つの名前を要求し、この2つのレコードがそれぞれ”Joe”と”Jane”という name
プロパティを持っていた場合、ルートハンドラは以下のような配列を生成します。
[
{
path: ['names', 0, 'name'],
value: 'joe'
},
{
path: ['names', 1, 'name'],
value: 'jane'
}
]
これがFalcorのレスポンスハンドラのクライアントサイドに返され、その結果、モデルキャッシュがアップデートされます。また、呼び出した側にはこれらの名前も返されます。
新たな名前レコードの追加
このシンプルな名前のウィジェットは、新たな名前の追加も可能です。クライアントサイドの操作を簡単に説明します。まず、サーバサイドのFalcorのルートを見てください。
{
route: 'names.add',
call: (callPath, args) => {
var newName = args[0];
data.names.push({name: newName})
return [
{
path: ['names', data.names.length-1, 'name'],
value: newName
},
{
path: ['names', 'length'],
value: data.names.length
}
]
}
}
これは Falcorの”call” ルートの一例です。クライアントは”POST”リクエストの一部として、追加する名前と共に、パスのパラメータとして”names.add”を持ちます。そして上記のエンドポイントにヒットし、その結果DBに新たな名前を作成します。これが予想されるシンプルな進み方ですが、このリクエストへのレスポンスは興味深いものです。2つのパスの要素をクライアントに返していることに注目してください。どちらの要素もこの新しい名前の追加によってデータセットに生じた変更を表現するものです。1つ目は、namesコレクションの末尾に加わった新しい名前があるということを示します。2つ目は、データセットにある名前の数が変わったことを示します。もし”Bob”という新たな名前を、”Joe”と”Jane”という既存の名前のリストに追加したとすると、このルートで作成されるレスポンスは以下のようになります。
[
{
path: ['names', 2, 'name'],
value: 'Bob'
},
{
path: ['names', 'length'],
value: 3
}
]
というわけで、これ以降クライアントは新しい名前や名前のコレクションの長さについてサーバに問い合わせる必要はありません。この”呼び出し”の要求へのレスポンスに含まれる情報のおかげで、Falcorのクライアントサイドにキャッシュされるためです。小さくシンプルなデータセットの場合、変更されたパス とともに 値を返すことは理にかなっていますが、もっと大きなデータセットの場合は変更されたパス のみ を返す方が賢明です。クライアントは変更された値 すべて を気にするわけではないかもしれませんし、これは帯域幅とプロセッササイクルの無駄遣いになってしまうだけかもしれませんからね。その代わり、あなたは単に変更されたパスだけを返せばいいのです。そうすればFalcorのクライアントは、変更されたパスに関連する値をアプリケーションが必要とするか否か(またいつ必要か)をキャッシュではなくサーバに問い合わせればよいということが分かります。
クライアント
サーバのコードはとてもシンプルです。JavaScriptやHTMLファイルなどの静的リソースを提供し、さらにFalcorを使ってクライアントからのAPIリクエストに応答します。では次に、私たちのアプリのクライアントサイド、つまりブラウザで動作する部分について説明していきます。
インデックスページの簡潔さ
このアプリのインデックスページ は 非常に シンプルです。普段HTMLを書く時のコードに、Reactで作成したアプリ全体のコンテナとして機能する1行が加わり、そこにJavaScript すべて をインポートする2行目が続くだけです。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="demo"></div>
<script src="/site/bundle.js"></script>
</body>
</html>
唯一の興味深い点は、 <div>
と <script>
の要素を参照する行です。とにかくすごいのは、間違いなく複数のJavaScriptファイルからアプリのソースを構成するのに、最終的に提供するのはすべてのコードを包含するファイル1つだけだということです。個々のHTTPリクエストに関する負荷がありますから、ページをロードするリクエストの数を減らすことは有益です。
きっと皆さんは、なぜ <script>
タグがドキュメントの一番下に配置されているのか不思議に思われているでしょう。通常は <head>
タグの中にあるものですからね。まず、これによって静的マークアップがロード可能になり、即座にユーザに表示されます。JavaScriptのソースがすべてロードされ解析されるまで待つ必要はありません。今回の場合、最初の静的コンテンツという点ではあまりお伝えすることはありませんが、ページをセットアップするものや、”ロード中”のメッセージなどは確実に加えられると思います。また、スクリプトタグをコンテナ要素の下に置くことで、この要素がコードを実行する時までにDOM内で使用可能になっていることが保証されます。コードがコンテナ要素内のすべての動的コンテンツをレンダリングするので、これが重要なわけです。
ReactでUIロールをコンポーネントに分割
アプリのフロントエンドは3つの論理的なコンポーネントに分割することができます。”名前追加”、”名前リスト”、そしてこれら2つのスタンドアロンのコンポーネントを結びつけるコンポーネントです。それぞれ、自己完結型のReactコンポーネントとして存在します。ご想像どおり、各コンポーネントは何らかの方法でサーバと通信する必要があるでしょう。Falcorを利用して、この共通タスクを実現します。
サーバとの通信でFalcorを利用
Falcorはサーバとの通信を簡単にするだけではなく、データモデルを管理し、効率的かつ安全にデータをやり取りします。こういったFalcorの”ヘルパ”は、たった数行のコードを記述するだけで作成できます。以下の model.js ファイル の内容をご覧ください。
var Falcor = require('falcor'),
FalcorDataSource = require('falcor-http-datasource'),
model = new Falcor.Model({
source: new FalcorDataSource('/model.json')
})
module.exports = model
Node.jsやそのネイティブモジュールシステムであるCommonJSについてあまり詳しくない人にとっては、上記のコードのうち少なくとも数行は謎めいているように見えるかもしれませんね。最初の2行でFalcorのHTTPデータソースモジュールを”インポート”しています。この2行はFalcorの”ヘルパ”を設定するために必要な処理です。ファイルの最終行では、新たなモジュールを本質的に作成しています。このモジュールはクライアントサイドのFalcorモジュールやヘルパを表しており、Falcorを通してモデルを呼び出す必要のある他のモジュールから require
で呼び出すことが可能です。エクスポートされたモジュールは、 FalcorのDataSorceインターフェース にてすべてのプロパティが定義されているオブジェクトになります。そしてReactコンポーネントが私たちのモデルと通信するために、このインターフェース上のメソッドを利用することになるのです。どのような動きになるのかは、後ほどご説明します。
私たちの model
はFalcorのHTTP DataSorce
のラッパーとして定義されています。 DataSorce
を定義する際、APIサーバのエンドポイントである”/module.json”へのパスをインクルードしていますので、APIが呼ばれるすべての場合において、1つのHTTPエンドポイントを持つことになるのです。オペレーションの型や関連するデータは、FalcorによるGETリクエストやPOSTのメッセージボディ部にクエリパラメータとしてエンコードされています。
名前リストのコンポーネント
さて、モデルを定義したところで、すべての名前をストアにリストアップするコンポーネントを利用してUIを作っていきましょう。これはReactのコンポーネントです。シンプルさを保ち、シンプルなHTMLリストとして実装します。このコードは names-list.jsxファイル の内容です。
var React = require('react'),
model = require('./model.js');
class NamesList extends React.Component {
constructor() {
super()
this.state = {names: {}}
}
componentWillMount() {
this.update()
}
render() {
var names = Object.keys(this.state.names).map(idx => {
return <li key={idx}>{this.state.names[idx].name}</li>
})
return (
<ul>{names}</ul>
)
}
update() {
model.getValue(['names', 'length'])
.then(length => model.get(['names', {from: 0, to: length-1}, 'name']))
.then(response => this.setState({names: response.json.names}))
}
}
module.exports = NamesList
なぜかは明らかだとは思いますが、最初の2行ではReactモジュールと先ほど定義したFalcorモデルをインポートしています。もしあなたがJavaの開発者であれば、コンポーネントの定義には非常になじみがあるでしょう。ECMAScript 6を利用することによってJavaScriptでクラスが使えるようになりますので、私たちは名前リストのコンポーネントをReactコンポーネントの型として定義します。またこれもJavaと似ている点ですが、コンストラクタを定義しなければなりません。このコンストラクタで state
オブジェクトを単に初期化するのです。 state
オブジェクトはマークアップをレンダリングするためのデータを提供する際に使われます。そのマークアップは変更があるたびに(Reactによって可能な限り効率的に)再びレンダリングされるものです。コンポーネントのコンテキストにアクセス する前 に、 super()
を呼ぶことによって React.Component
コンストラクタを 呼び出さなければならない ことに注意しましょう。コンポーネントのコンテキストには this
キーワードを使ってアクセスします。
一つ目のクラスメソッドである componentWillMount
は React.Componet
クラスから継承していて、Reactがマークアップを最初に”レンダリング”する 直前に 呼び出します。つまりそれは render
メソッドが最初に起動してマークアップがDOMに追加される前です。この時点で、Falcorから名前リストを取得する update
メソッドを呼び出します。
update
メソッドはいくつか処理を行います。まず最初は、Faclorにリスト上の名前の数を要求します。そして、そのlengthリクエストの結果に基づいてすべての名前に対してリクエストを送ります。これらの呼び出しは非同期なので、それぞれpromiseを返します。名前の数を得るための最初の呼び出しが解決されたら、最初の then
関数がその結果(つまりリスト中の名前の数)とともに呼び出されます。名前の数が分かったら、リストの0番目から最後のインデックスまでのすべての名前をFalcorに要求します。
一行のES6アロー関数の中には暗黙の return
キーワードがあり、それぞれが Promise
を返します。2番目のpromiseが名前リストのリクエスト向けに解決されたら、モデル処理のチェーンの次でありかつ最後のハンドラが起動されます。 getValue
を使ってlengthのルートが呼び出され、解決されたレスポンスとして単一値の結果を導き出します(この場合、リストの中の名前の数)。しかし、リスト中のすべての名前を取得するための呼び出しは get
なので、JSONレスポンスを返します。このJSONは値を持つ特定のプロパティ、 name
、に合致する全ての名前オブジェクトを含んでいます。一群の名前で setState
を呼び出していることに留意してください。これがコンポーネントの状態オブジェクトをアップデートし、新しい名前リストでコンポーネントを再レンダリングするようにReactに指示します。
さて今度は render
メソッドです。ここで実際にHTML要素がDOMにレンダリングされます。Reactは、コンポーネントが最初にマウントした時と、コンポーネントの state
プロパティが変わった時はいつでもこのメソッドを呼び出します。コンポーネントの状態の一部である names
プロパティは、それぞれの名前のサーバ上におけるインデックスである”キー”と、名前の各レコードの値を備えたオブジェクトです。各レコードの name
プロパティについてはFalcorにしか問い合わせていないので、返された名前レコードの中にあるのはそのプロパティだけになります。
この render
メソッドのマークアップは見慣れないかもしれません。これはJSXと言って、Facebookが作成し保守しているECMA Script言語仕様の拡張機能です。これによって、JavaScriptコードと並行して HTMLのようなコンテンツを簡単に組み入れることができます。ブラウザに引き渡される前に、JSXを標準のJavaScriptにするためwebpackにコンパイルさせます。これについては後ほど詳しく触れます。このビルドステップの結果は、HTMLをビルドアップする他の多くのメソッド呼び出しと似たようなものになります。JSXを使うのではなく、このような手法でReactのDOM APIを使ってHTMLを構築してもよかったのですが、JSXを使うとこれからの作業が とても 簡単になり、コードもシンプルで読みやすいものになります。
ファイルの最後の行によってNameListコンポーネントは他のモジュールへと取り込まれ、実際に使われます。これについてはこの後に触れます。
名前追加コンポーネント
名前をリストするコンポーネントが用意できましたが、今度はリストに新しい名前を追加できるようにしたいですよね。次はReactのコンポーネント”name adder”を作成する必要があります。 name-adder.jsx という正式名称のファイルに保存されます。
var React = require('react'),
model = require('./model.js');
class NameAdder extends React.Component {
handleSubmit(event) {
event.preventDefault()
var input = this.refs.input
model.
call(['names', 'add'],
[input.value],
["name"]).
then(() => {
input.value = null
input.focus()
this.props.onAdded()
})
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<input ref="input"/>
<button>add name</button>
</form>
)
}
}
NameAdder.propTypes = {
onAdded: React.PropTypes.func.isRequired
}
module.exports = NameAdder
この render
メソッドでは、テキスト入力とsubmitボタンを備えたシンプルなHTMLのフォームを定義します。ユーザはsubmitボタンをクリックするか、入力フィールドにテキストをタイプした後にエンターキーを打つことで、フォームを送信できます。フォームが送信されると、コンポーネントクラスの handleSubmit
メソッドが呼び出され、submit Event
を渡します。ここでは、コンポーネントをコンテキストとしてバインドする新しい関数を作成しています。ES6のクラスをReactコンポーネントに利用する時に必要な処理です。そうしないと、この場合はイベントハンドラ内部の this
の値は window
になってしまいますが、そうなってほしくは ない からです。
このReactコンポーネントには他に handleSubmit
というメソッドがあります。これは前述したように、レンダリングされたフォームが送信された時に呼び出されます。最初に、ブラウザのデフォルトの動作を阻止しなくてはいけません。つまり、 実際に フォームを送信したいわけではなく、また、ページもリロードさせたいわけでもないからです。そうではなく、入力されたデータをFalcorに流し込む必要があるのです。次に、input要素を参照しなくてはなりません。ユーザがどのようなテキストを入力したか判断するためです。 render
メソッドのテキスト入力に ref
属性を含めたことに留意してください。これにより、CSSセレクタを当てにしなくても、下層のDOM要素を簡単に管理することができます。最後に、サーバにこの新しい名前を送らなくてはなりません。新しい名前を渡して先ほど定義した呼び出しルート”names.add”をヒ利用したいですね。サーバが新しい名前を保持して応答したら、Falcorは、サーバが提供した情報を用いてモデルの内部表現をアップデートします。これでリストにもう1つ名前があるということが分かり、今追加した名前のインデックスも分かりました。しかしなぜこのことが重要なのでしょうか。
無事に名前がサーバに追加されたと判断した後、Falcorは”success”関数を呼び出します。Falcorモデルの call
を起動した後、 then
を呼び出す時に渡した、最初(で唯一)の関数です。これにより、テキスト入力をリセットし、フォーカスを保持することができるので、ユーザは簡単に新しい名前を入力できます。でも名前リストもちゃんと最新にしておきたいですよね。どうもコンポーネントに付随する props
プロパティに onAdded
関数があるようです。これはどこから来たのでしょうか。名前追加コンポーネントをレンダリングしたコンポーネントが渡したのです。これはこの後、見ていきます。Reactコンポーネントに渡されたパラメータはどれでも props
プロパティ上で利用可能です。 onAdded
関数がコンポーネントに渡されたと予測できます。新しい名前が追加された時はいつでも呼び出すべきです。この関数は NamesList
コンポーネント上の update
メソッドをトリガーします。覚えているかもしれませんが、これは、結果として名前リストのためにFalcorを呼び出します。これがまさに私たちのやりたいことです。つまり、ユーザが最新リストを見られるよう、名前が追加された後に名前リストを更新することです。これを知ったら驚くかもしれませんが、名前を追加した後、Falcorはこの名前リストに関してサーバにコンタクト しません 。”names.add “呼び出しへのサーバのレスポンスによって提供された情報によって、リストがどのように変更されたか既に知っているからです。モデルの内部表現からこのデータを引き出すことで、サーバとの2回分のやりとりを節約します(1つはlengthリクエストのため、もう1つは名前リストのため)。
最後に、 Reactのプロパティ検証 の機能を利用します。ファイルの最後の、 NameAdder.propTypes = {
で始まる行を見てください。もし NameAdder
をレンダリングするコンポーネントがコールバック関数を適切にコンポーネントに渡していなかったら、Reactはブラウザのデベロッパコンソールに警告メッセージをログします。これはあなたのコンポーネントを統合している開発者がうっかり必須なプロパティを忘れてしまった時に警告を発する便利な方法です。コンポーネントのプロパティ検証を定義することにより、ドキュメントも整備されます。
おそらくこの最新のスタックのエレガントさが分かってきたのではないでしょうか。Reactによってフォーカスされたコンポーネントの観点からUIを構成できますし、Falcorによって実際のモデルプロパティの観点でモデルを考えることができます。そして同時に、サーバとのコミュニケーションも最小限に抑えてくれるのです。
名前マネージャコンポーネント
ここまでで、全ての名前をリストするコンポーネントと新規の名前を追加するコンポーネントを作りました。この2つのコンポーネントは、お互いについて直接的な情報を持ち合わせていません。そのため、テストを行ったり、再利用したりすることが簡単になるのですが、これらのコンポーネントをつなぎ合わせる何らかの方法が必要です。解決策が、”接着”コンポーネントです。これを3つ目のReactコンポーネント NameManager
と呼び、 name-manager.jsx という正式名称のファイルに保存します。
var React = require('react'),
ReactDom = require('react-dom'),
NameAdder = require('./name-adder.jsx'),
NamesList = require('./names-list.jsx');
class NameManager extends React.Component {
handleNameAdded() {
this.refs.namesList.update()
}
render() {
return (
<div>
<NameAdder onAdded={this.handleNameAdded.bind(this)}/>
<NamesList ref="namesList"/>
</div>
)
}
}
ReactDom.render(<NameManager/>, document.querySelector('#demo'))
案の定、まずは React
、 NameAdder
そして““NameesList“`の3つのコンポーネントをインポートしなければなりません。更に、完成したUIをDOMにレンダリングするのに使うReactDomも必要です。留意すべき点として、 index.html ファイルで定義したコンテナエレメントは先に選択されており、子や子孫としてReactコンポーネント一式はレンダリングされています。
ReactDom
がコンポーネントをDOMにレンダリングしようとしたときに呼び出される render
メソッドの大部分は、私たちが事前に定義した他の2つのコンポーネントに対するリファレンスとなっています。リストの名前を更新するために、 NameAdder
コンポーネントがどのように NamesList
に確認することができていたか覚えていますか? NameManager
コンポーネントがそれを可能にしています。 onAdded
プロパティをコンポーネントに渡しているのが分かると思います。 NameAdder
によって呼び出されると、 NameManager
の handleNameAdded
メソッドが呼び出され、クラスのpublicなインスタンスメソッドとして露呈した NamesList
コンポーネントの update
メソッドが呼び出されます。
Reactコンポーネントについては以上です。単純ですよね? 次のセクションでは、webpackによって、フロントエンドコンポーネントをシングルバンドルファイルに組み込む方法を説明していきます。ちなみに、全てのサポートされたブラウザで使用することができます。
コンポーネントのモジュール化とwebpackでのビルドプロセスの簡素化
以下の目的を達成するためにwebpackをビルドツールとして活用します。
- JSXを標準JavaScriptにコンパイル
- 全ての必要なJavaScriptを1つのファイルに統合
- ES6のスペックが完全に実装されているかにかかわらず、ES6構文が全てのブラウザで機能することの確認
- ブラウザでのコードのデバッグがシンプルにできるかどうかの確認。ここで「シンプルなデバッグ」とは、オリジナルのプリコンパイルファイルまたは事前に結合されたソースファイルにアクセスできることを指す
package.json file には、定義されたwebpackとその他の依存が保存されています。多少の設定が必要なので、以下を見てください。
module.exports = {
entry: './name-manager.jsx',
output: {
filename: './site/bundle.js'
},
module: {
loaders: [
{
test: /\.js/,
loader: 'babel',
exclude: /node_modules/
}
]
},
devtool: 'source-map'
}
ファイル名を webpack.config.js とすれば、webpackが簡単に見つけ出してくれ、設定を使ってくれます。”name-manager.jsx”アプリの主なエントリーポイントは、 entry
設定プロパティの値として使われています。Webpackは、この”メイン”クラスを使って他のプロジェクトの依存を見つけ出します。そしてこれを、index.htmlページからインポートされた最終的に結合されたJavaScriptファイルを生成するために使用します。結合されたファイルの名前は、 output.filename
設定プロパティに設定されます。
次に、”ローダ”一式が特定されています。ECMAScript 5構文にコンパイルされたECMAScript 6コードであることを保証するbabelローダを使います。そうすることで、対象ブラウザがスペックのどの部分をサポートしているかを気にすることなく、ピュアなES6コードを記述することができます。ローダにある test
プロパティは正規表現で、ソースツリーにあるあらゆる.jsや.jsxファイルをbabelローダに渡し、処理を行います。Babelローダは、webpackが全てのものをシングルリソースに統合する前にソースを処理します。
最後に、設定の最後の行は、webpackがソースマップを生成することを示しています。これによって4番目に挙げた目的を達成することができます。ソースマップは、デベロッパツールのコンソールが開いたときにブラウザによって読み込まれるので、ユーザがアプリにアクセスした際に、ページの読み込みの帯域幅が無駄に使われる心配がありません。これらのマップによって元々のソースファイルを見ることができる他、ファイルのどの箇所にでもブレークポイントを設定することが可能になります。ですから、結合された、またはコンパイルされたbundle.jsファイルを見る必要がないのです。Webpackは、生成されたbundle.jsファイルの下部に注釈を、ソースマップファイルにポインタを付けるので、ブラウザのデベロッパツールで場所を探すことができるのです。本番で使用するために圧縮されたバンドルファイルを生成する際には、これがより役に立ちます。Webpack設定の例では、圧縮されたバンドルを生成していませんが、webpackの -p
コマンドラインのオプションを起動することで簡単に生成することができます。ちなみにこの”p”は”本番(production)”のpです。
アプリの構築と使用
コードも書きあがり、サーバの準備も整いました。 そして ビルドツールも用意できています。では、アプリをどのようにアップロードして起動すればいいのでしょうか?
まずは、全てのプロジェクト依存を引っ張ってくる必要があります。プロジェクトのルートに npm install
を走らせます。これによって package.json file がパースされ、”node_modules”フォルダの中に、登録された全ての依存がインストールされます。Webpackバイナリもインストールするので、クライアント側のソースバンドルを構築する必要があります。
次にindex.htmlファイルで参照するソースバンドルを生成します。やることは、プロジェクトのルートから $(npm bin)/webpack
を実行し、webpackを起動することだけです。 $(npm bin)
が、 npm install
によって引っ張ってきた全てのバイナリを含んだディレクトリへと拡張されます。留意すべき点は、このパスの拡張は、linux/unix環境のみで機能するということです。クロスプラットフォームでのオプションは、 package.json file の中にwebpackバイナリのリファレンスを追加してしまいます。例えば、以下の "scripts"
プロパティを含めると、webpackを走らせることができ、そして npm run webpack
を起動することで、 どの プラットフォーム上でいつコードを変更したとしても、バンドルを再度生成することができるのです。
"scripts": {
"webpack": "webpack -w"
}
最後に、APIのリクエストを処理するWebサーバを起動し、index.html、bundle.js、そしてソースマップファイルを送信する必要があります。Node,jsサーバを起動するには、 node --harmony server
を実行します。 --harmony
スイッチによって、最新のECMAScriptの仕様に含まれる”harmony”として知られる構文をコード内で使っていることをnodeに伝達します。ECMAScript 6はharmoneyの仕様内での一入力にすぎません。node.jsの最新版を使っているのであれば、スイッチは必要なくなるでしょう。
サーバを起動したら、ポート9090でアプリにアクセスすることができます。http://localhosts:9090にアクセスして、テストしてみてください!
更に
もちろん、Falcor、React、Webpack、EXMAScriptでできることはもっとたくさんあります。この記事の内容や紹介した例は、あくまでもこれらを使用する第一歩だと思ってください。特に、話を単純にするためにFalcorの例として挙げませんでしたが、 reference routes は読むべきです。背後に巨大なデータを含む 実際 のWebアプリケーションを開発する際、Falcorでリファレンス型を頻繁に使うことになるでしょう。FalcorとReactについては、時間を取ってチュートリアルを実施することをお勧めします。ECMAScript 6に関する更なる情報が必要な場合は、 Mozilla Developer Networkから素晴らしいリファレンスを得ることができます 。
自身のスキルをより磨きたいのであれば、この記事で開発してきたシンプルな名前アプリを以下の方法で改善することができるので試してみてください。
- 既存の名前を編集できるようにする。これには、Reactの”edit names”コンポーネントとFalcorの”set”ルートが必要になります。
- 既存の名前を並び替えられるようにする。これには、NamesListコンポーネントにコードを追加することと、インデックスアップデートを処理する他のFalcorのルートが必要になります。
- 名前の削除をサポートする。これは、追加のReactコンポーネントと、Falcorバックエンドへの新規の”call”ルートの実装によって最適に処理されます。
あなたが実施した変更や追加など共有したいようであれば、 GitHub repository にいつでもプルリクエストしてください。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa