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のポイント

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の適用、ライフサイクル

あたりを読んでみようかな、と思っております。