Vue.jsでサーバサイドレンダリングしたい

## 前置き
今作っているシステムは社内ツールとして作っているのですが、将来的に弊社のクライアント向けに公開するみたいな話もあり、
古いJS・低速なネット環境も想定されるので、それを解決する手段の１つとしてSSR（サーバサイドレンダリング）を勉強します。

https://jp.vuejs.org/v2/guide/ssr.html
こちらのVue公式ドキュメントを参考に作ってます。

http://qiita.com/koki_cheese/items/ce5f16111e8ca251838d
Webpack周りはこちらが参考になりました。

## SSRされていない状態
以下は画面に文字列を表示するだけのコードです。

```js:index.js
import Vue from 'vue'
import App from './App'

new Vue({
  render: h => h(App)
}).$mount('#app')
```

```html:App.vue
<template>
  <div id="app">
    <span>sample ssr!!</span>
    <!-- 本来の実装では以下を設定している
    <router-view></router-view>
    -->
  </div>
</template>
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>sample ssr</title>
  </head>
  <body>
    <div id="app" />
    <script src="/bundle.js"></script>
  </body>
</html>
```

Webpack等でindex.jsをエントリーポイントとしてビルドして、bundle.jsを作ります。
ビルド周りの設定は省略しますが、これを画面で表示させると「sample ssr!!」という文字だけが表示されます。

HTMLソースを見てみると
<img width="682" alt=" 2017-03-30 16.15.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/168197/c867d45d-a011-63f9-6f4d-40aa420b0f53.png">

SSRをしていないので、`<div id="app" />`のように空のHTMLになっています。
現状は、上記のサンプルコードはコメントアウトしていますが、vue-routerを使ったSPA（シングルページアプリケーション）として動作させています。

これを初期表示時にSSRしてあげるようにコードを改修していきます。

## SSR用に変更
index.jsはブラウザとNode.js(サーバサイド)からの呼出しに対応させます。

```js:index.js
import Vue from 'vue'
import App from './App'

const createApp = () => {
  return new Vue({
    render: h => h(App)
  })
}

if (typeof module !== 'undefined' && module.exports) {
  // Node.jsから呼出した場合の処理を追加
  module.exports = createApp
} else {
  // ブラウザから呼出した場合は特に変更無し
  createApp().$mount('#app')
}
```

index.htmlはパスだけ変更

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>sample ssr</title>
  </head>
  <body>
    <div id="app" />
    <script src="/dist/client-bundle.js"></script>
  </body>
</html>
```

## サーバサイド作成
Node.js + Expressを使って作成しています。

```js:server.js
const fs = require('fs')
const path = require('path')
const express = require('express')
const renderer = require('vue-server-renderer').createRenderer()
const createApp = require('./dist/server-bundle')

const app = express()
const layout = fs.readFileSync('./dist/index.html', 'utf-8')
const layoutSections = layout.split('<div id="app" />')

app.use('/dist', express.static(
  path.resolve(__dirname, 'dist')
))

app.get('*', (req, res) => {
  // 上記で分割したHTMLをストリーミング
  const stream = renderer.renderToStream(createApp())

  res.write(layoutSections[0])
  stream.on('data', (chunk) => {
    res.write(chunk)
  })

  stream.on('end', () => {
    res.end(layoutSections[1])
  })

  stream.on('error', (err) => {
    console.error(err)
    return response.status(500).send('Server Error')
  })
})

// 5000番ポートで待機
app.listen(5000, function (err) {
  if (err) throw err
  console.log('Server is running at localhost:5000')
})
```

## WebpackをSSR用に作成
```js:webpack.config.js
// ブラウザ用
const client = {
  entry: [
    `${__dirname}/../src/index.html`,
    `${__dirname}/../src/index.js`
  ],
  output: {
    filename: 'client-bundle.js',
    path: `${__dirname}/../dist`
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader',
        exclude: /node_modules/
      },
      {
        test: /\.js$/,
        use: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.vue$|\.js$/,
        use: 'eslint-loader',
        exclude: /node_modules/
      },
      {
        test: /\.html$/,
        use: 'file-loader?name=[name].[ext]'
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.vue']
  }
}

// サーバ用
const server = Object.assign({}, client, {
  target: 'node',
  output: {
    filename: 'server-bundle.js',
    path: `${__dirname}/../dist`,
    libraryTarget: 'commonjs2'
  },
  externals: Object.keys(require('../package.json').dependencies)
})

module.exports = [client, server]
```

以下はBabelの設定です。
私の環境だと以下のプラグインが無いと動きませんでした。
「babel-plugin-transform-es2015-modules-commonjs」

```json:.babelrc
{
  "presets": [["es2015", {"modules": false}], "stage-2"],
  "plugins": ["transform-runtime", "transform-es2015-modules-commonjs"]
}
```

## 動作確認
以下のコマンドでビルドして`http://localhost:5000`で見てみると
SSR未対応の時と同様に「sample ssr!!」という文字だけが表示されます。

```zsh
webpack --config build/webpack.config.js
node server.js
```

ここでHTMLソースを見てみると
<img width="372" alt=" 2017-04-03 18.41.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/168197/3bcd2148-b850-8215-e03d-69f1c9a9f7ac.png">

先程と違い、`<div id="app" server-rendered="true"><span>sample ssr!!</span></div>`のようにSSRされていることが分かります。
今回は試してませんがvue-routerを使ってあげると、初期表示はSSRして、それ以降はSPAとして動作してくれるはずです。