Vue.js 2.0のServer Side Renderingを試しつつ、軽くコードリーディングした
数日前に、Vue.js 2.0 が発表された ( Announcing Vue.js 2.0 – The Vue Point – Medium )。Server Side Renderingに対応するそうなので、pre-alphaではあるけれど、単純なサンプルを動かしつつ、軽くコードリーディングした。対象のVue.jsはこの記事を書いた時の最新のnextブランチ( fix eslint again · vuejs/vue@014ac35 · GitHub )
2.0のポイント
- 内部で GitHub - snabbdom/snabbdom: A virtual DOM library with focus on simplicity, modularity, powerful features and performance. ベースのVirtual DOMを持つようになり、パフォーマンスが改善された
- 上記の副次的な効果として、Server Side Renderingができるようになった
- 1.xとほぼ互換性があるが、$broadcast, $dispatch, filterなどが非推奨になった(GitHubに「廃止にしないで!」というissueが立って議論が活発におこなわれているので、正式リリースまでに非推奨でなくなるものもあるかも)
Server Side Rendering の必要性
Server Side Rendering(SSR)というキーワードは昨今よく耳にするけれど、Single Page Applicationでなぜ必要とされるのか、について書いておく。大きく2つある。
初期表示時間の短縮
よくあるSingle Page Applicationでは、最初にサーバーからbodyの中身がほとんど空のHTMLが返されて、JavaScriptを実行して、画面にコンテンツが表示されるという流れになって、サーバーでbodyの中身を生成する場合と比較して、クリティカルレンダリングパスが長い。
SEO
bodyの中身が空のHTMLでは、SEOに弱い。
最近は、GoogleのクローラがJavaScriptを実行するようになっているが(ヘッドレスブラウザに近いものらしい refs: AjaxやSPAのHTMLスナップショットをSEO向けに作る必要はなし | 海外SEO情報ブログ )、
実際のところどこまでJavaScriptを実行するか、実行後どのようなコンテンツを扱っているかが不明確なので、SEOに力をいれているサービスであれば、クローラが扱うコンテンツをこちらで確実にコントロールしたい。
( 2016/11/13 追記: Fetch as Googleを使えば、どのようなコンテンツを扱っているか把握できる
ウェブサイト用 Fetch as Google を使用する - Search Console ヘルプ )
また、Google以外のJavaScriptを実行しないクローラーも考慮しないといけない。
Server Side Renderingを行うには当然コストがかかるし、そもそもサービスの性質として初期表示時間やSEOを気にしなくてもよいサービス(例えば、いわゆるCMS)も存在する。自身のサービスが対応する必要があるか否かは冷静に判断する必要がある。
Server Side Renderingの単純なサンプルコード
Vue.js 2.0の開発がおこなわれているnextブランチのユニットテストを参考に、Node.jsでコンポーネントからHTMLを生成、標準出力に表示するような単純なサンプルコードを書いてみた。開発途中、あるいはドキュメントがまだ整備されていないライブラリ/フレームワークの使い方を調べるには、ユニットテストを読むのが手っ取り早い。開発中だからかSSR関係のファイルがnpmモジュールの配布対象に含まれていないので、検証用のリポジトリ( GitHub - kitak-sandbox/vue-ssr-sandbox )にvueのリポジトリをgit submoduleとして追加して、importするようにした。
import Vue from './vue/dist/vue.common.js'; import { compileToFunctions } from './vue/dist/compiler.common.js'; import createRenderer from './vue/dist/server-renderer'; const { renderToString } = createRenderer(); let compileTemplate = (options) => { const res = compileToFunctions(options.template, { preserveWhitespace: false }); Object.assign(options, res); console.assert(typeof options.render === 'function'); delete options.template; return options; }; let childComponent = Vue.extend(compileTemplate({ props: ['message'], template: ` <div> <p>I am {{ name }}<br> message from parent: {{ message }}</p> </div> `, data: function() { return { name: 'child' }; } })); let parentComponent = Vue.extend(compileTemplate({ template: ` <div> <p>I am {{ name }}</p> <child :message="message"></child> </div> `, data: function() { return { name: 'parent', message: 'hello!' }; }, components: { child: childComponent } })); console.log(renderToString(new Vue(compileTemplate({ template: `<root><root>`, components: { root: parentComponent } }))));
実行結果
ちゃんと動いている。
<div server-rendered="true"><p>I am parent</p><div><p>I am child<br> message from parent: hello!</p></div></div>
コードリーディング
ざっくりサンプルコードと呼び出している関数の中身の解説。
親子関係のあるコンポーネント(parentComponent, childComponent)を定義して、親から子に単方向で値を渡している。( message property )
templateオプション、renderオプションとcompileToFunctions関数
1.x系のVueでは、templateオプションでコンポーネントのテンプレートを指定していたが、Vue.js 2.0からはtemplateオプションの代わりにVirtual DOMを返す関数をrenderオプションに指定することができる。React、というか、よくあるVirtual DOMのライブラリっぽい。Announcing Vue.js 2.0 – The Vue Point – Medium を読んだところ、よく テンプレート vs JSX みたいな話になりがちだけど、両方良いところがあるので、両方使えたらよいのでは、という話だと理解している。
おそらくブラウザで動作させる場合は、Vueがtemplateオプションで指定されたテンプレートからVirtual DOMを返す関数を動的に生成している。それをやっているのがcompileToFunctions。テンプレートをパースして、ASTを得て、最適化して、コードを生成して、FunctionコンストラクタからVirtual DOMを返す関数を生成して... ということをやっている。以下は、compileToFunctions関数を実行して得られるオブジェクト、renderプロパティはVirtual DOMを返す関数。
{ render: 'with (this) { return __h__(\'div\', undefined, [__h__(\'p\', undefined, [("I am "+__toString__(name)),_staticTrees[0],("\\n message from parent: "+__toString__(message))], \'\')], \'\')}', staticRenderFns: [ 'with(this){return __h__(\'br\', undefined, undefined, \'\')}' ] }
ユニットテストを読んだ & テストコードをいじって動かしてみた感じだと、SSRの場合は、こちらでテンプレートからrender関数を生成、renderオプションにセットする必要があったので、それをおこなう簡単なヘルパー関数(compileTemplate)を定義した。
renderToString関数
名前の通り、VueのViewModelのオブジェクトからHTML文字列を生成する関数。renderToStream関数というのもあって、HTML文字列のチャンクが非同期でコールバックに渡される。Node.jsはシングルスレッドのアーキテクチャなので、サーバサイドがNode.jsの場合に、HTML文字列を生成開始から終了までの間、スレッドをブロックしないのはありがたい。今回は、同期的でコードが読みやすいrenderToString関数を読んだ( vue/create-sync-renderer.js at 38540b07e4a1213413005e58833a63cc12ca0361 · vuejs/vue · GitHub )。
たったの50行でコードも綺麗に書かれていて読みやすい。コンポーネントの持つノードオブジェクトを再帰的に辿りつつ、HTMLを生成している。SSRして生成したことの印として、ルートの要素のserver-rendered属性値にtrueをセットしている(これはおそらくhydrationで使うのだろうと予想している)。
まとめと、次回
Vue.js 2.0のServer Side Renderingを単純なサンプルを動かしつつ、コードリーディングした。 次は、
- SSR後のブラウザでのhydration
- テンプレートのcompile、コードの自動生成
- Virtual DOMのdiff/patch
- directiveの適用、ライフサイクル
あたりを読んでみようかな、と思っております。