ゼロからVueをSSRするウェブパックの設定

こんにちは。ウェブパックマスターです。この記事では、前の記事のゼロからウェブパックを作る続きとなります。今回は`vue-server-renderer`を入れてSSR（サーバサイトレンダー）を行います。

クライアントサイドレンダリングな場合、JavaScriptのバンドルをそのままで返却して、レンダーをブラウザーのJavaScriptエンジンに任せます。逆に、SSRというのはレスポンスが来るとNode.jsで動的に作成します。新しく作ったHTMLをレスポンスで返却します。

両方にも価値がありますが、この記事の話題の題材ではありません。

[ソースコードはこちら](https://github.com/lmiller1990/webpack-simple-vue/tree/add_server_rendering).

## `webpack.config.js`を２つ部分に分ける

アプリケーションのレンダーするところによって別のウェブパックの設定が必要となります。サーバとクライアントレンダーもサポートします。`webpack-dev-server`を使って開発したいので、クライアントレンダリングをします。プロダクションなら、サーバサイドレンダリングをします。

２つの設定ファイルに分けますが、共通設定もあります。２つのファイルを作ります：

```
mkdir config && touch config/client.js config/server.js
```

`config/client.js`はクライアントレンダリング設定ファイルで、`config/server`はSSRの設定ファイルです。

すでに存在している`webpack.config.js`には４つのプロパティがあります：

1. entry
2. module
3. plugins
4. dev-server

`module`に`.vue`ファイルを処理するルールがあって、それは共通ファイルに入ります。残りの設定は全部クライエントレンダリング向けなので`config/client.js`に移動します。

```js
// config/client.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  entry: "./src/index.js",

  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "template.html")
    })
  ],

  devServer: {
    overlay: true
  }
}
```

そして簡単な設定を`config/server.js`に入れます。

```js
const path = require("path")

module.exports = {
}
```

最後に`template.html`を`config`に 

```
mv template.html config/template.html
```

最後に、`webpack.config.js`を更新：

```js
const server = require("./config/server")
const client = require("./config/client")
module.exports = {
  // ...

  plugins: client.plugins.concat(VueLoaderPlugin())
}

// ...
```

`npm run dev`はまだ動いているはずです。

## `webpack-merge`を追加

SSRでもクライアントレンダリングでも、`webpack.config.js`を使います。今のところで、`mode`によってこのようなをコードを書く必要があります：

```js
// webpack.config.js
const client = require("./config/client")
const server = require("./config/server")

plugins: [
  VueLoaderPlugin(), 
]

if (development) 
  plugins.push(HtmlWebpackPlugin)
else if (production) 
  plugins.push(プロダクションだけのプラグインを入れる・・・)
```

これはどんどん分かりづらくなります。もっといい方法があります。`webpack-merge`で簡単に設定の管理ができます。

```
npm install webpack-merge --save
```

`webpack-merge`で`webpack.config.js`に使います：

```js
const VueLoaderPlugin = require("vue-loader/lib/plugin")
const merge = require("webpack-merge")
const clientConfig = require("./config/client")
const serverConfig = require("./config/server")

const commonConfig = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: "vue-loader"
      }
    ]
  },

  plugins: [
    new VueLoaderPlugin(),
  ]
}

module.exports = mode => {
  if (mode === "development") {
    return merge(commonConfig, clientConfig, {mode})
  }

  if (mode === "production") {
    return merge(commonConfig, serverConfig, {mode})
  }
}
```

`modules.exports`はオブジェクトではなく関数を返却します。関数なら、`webpack`はその関数を呼び出して`mode`を最初の引数として渡します。残念ながら、`mode`は`--mode`変数ではなく`--env`で決まるので、`package.json`の`scripts`を更新します：

```js
"scripts": {
  "build": "npx webpack --env production",
  "dev": "npx webpack-dev-server --env development"
 }
```

`npm run dev`はまだ動いているはずです。`localhost:8080`にアクセスしてとページソースを検証すると：


```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <div id="app"></div>
<script type="text/javascript" src="main.js"></script></body>
</html>
```

が書いてあります。`Hello`の`msg`が表示されていません。ブラウザに着いた時に、Vueアプリはまだレンダーされていないので、表示しません。クライアントで処理してレンダーされます。

SSRをすると、`<div>Hello</div>`をページソースで見れるはずです。

## サーバサイト`entry`

サーババンドルをレンダーするエントリーはクライアントと別になります。ファイルを作成します：

```
touch src/create-app.js
```

This fuction will create the Vue app which we want to render. Add the following code:

この関数を呼び出してレンダーするアプリを返します。

```js
import Vue from "vue"
import Hello from "./Hello.vue"

export function createApp() {
  return new Vue({
    el: "#app",
    render: h => h(Hello)
  })
}
```

`src/index.js`と比較すると：

```js
import Vue from "vue"
import Hello from "./Hello.vue"

document.addEventListener("DOMContentLoaded", () => {
  new Vue({
    el: "#app",
    
    render: h => h(Hello)
  })
})
```

ほとんど同じです。`document.addEventListener...`だけが違います。`document`はWeb APIなのでNode.js環境でアクセスできません。`create-app.js`にある関数で`src/index.js`をリファクタします。

```js
import {createApp} from "./create-app"

document.addEventListener("DOMContentLoaded", () => {
  createApp()
})
```

最後に、`config/server.js`で`create-app`をエントリポイントにします：

```js
// ...
module.exports = {
  entry: "../src/create-app"
}
```

サーバの設定でバンドルしてみます：

```
npm run build
```

```
Built at: 2018-06-03 22:48:16
  Asset      Size  Chunks             Chunk Names
main.js  66.2 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] (webpack)/buildin/global.js 489 bytes {0} [built]
[2] ./src/create-app.js + 6 modules 4.59 KiB {0} [built]
    | ./src/create-app.js 150 bytes [built]
    | ./src/Hello.vue 1.05 KiB [built]
```

動いてそうです。Node.js環境で使ってみます：

```js
node
> const { createApp } = require("./dist/main")
undefined
> createApp()
TypeError: createApp is not a function
>
```

問題があります。

## `output.library`と`output.libraryTarget`

`config/server.js`で`output`オプションの設定が必要です。ドキュメンテーションは[ここにあります](https://webpack.js.org/configuration/output/)。

`library`と`libraryTarget`を使います。デフォルトは：

```js
output: {
  libraryTarget: "var",
  library: undefined
}
```

`var`というのは`exports`は`var`にアサインします。その`var`ブラウザで使えるように`window`に追加されます。これは`UMD`(Universal Module Definition)モジュールです。

Since `library` is undefined, however, webpack simply does nothing. Although the variable isn't assigned, in `src/index.js` we do:

`library`のデフォルトは`undefined`なので、追加する`var`がなくて、webpackは何もしません。`src/index.js`を見ると：

```js
document.addEvenListener("DOMContentLoaded" ...)
```

があるので、アサインしなくてもブラウザの`DOMContentLoaded`が発火されるとレンダーされます。

`library: undefined`の動くかちゃんと理解するために、比べてみましょう。`dist/main.js`のコピーをします：

```
mv dist/main.js dist/main_2.js
```

そして`config/server.js`を更新します：

```
output: {
  libraryTarget: "var",
  library: "Bundle"
}
```

最後に`npm run build`。比較しましょう：：


```
diff dist/main.js dist/main_2.js

< var Bundle=function(t){var e={};function n(r){if(e[r])return...

---

> !function(t){var e={};function n(r){if(e[r])return...
```

２番目、あるいは`library: Bundle`が書いてある設定があると、アサインします。`window`に追加したことを確認できます：

`cd dist && python -m SimpleHTTPServer`してから、ブラウザーで`localhost:8000`にアクセスして、コンソールで

```
> window.Bundle

Module {
  __esModule: true, 
  Symbol(Symbol.toStringTag): "Module"
}
createApp: (...)Symbol(Symbol.toStringTag): "Module"
__esModule: true
get createApp: ƒ ()
__proto__: Object

> Bundle.createApp

ƒ s(){return new r.a({el:"#app",render:t=>t(a)})}
```

が見えます。

SSRをするためにNode.jsで実行します。そうすると、正しい`libraryTarget`が必要です。Node.jsは`CommonJS`を使うので、`config/server.js`を更新します：

```js
const path = require("path")

module.exports = {
  entry: "./src/create-app.js",

  output: {
    libraryTarget: "commonjs2"
  }
}
```

Node.js環境で`window`に追加する必要はないです。`library`オプションを削除します。

`npm run build`を実行して、`diff dist/main.js dist/main_2.js`で比較します：

```js
< !function(t){var e={};function n(r){if(e[r])return...

---

> < module.exports=function(t){var e={};function n(r){if(e[r])return
```

`module.exports`があるのでNode.js環境で使えるはずです。確認します：

```
node
> const {createApp} = require("./dist/main")
> createApp
[Function: s]
```

動いています。`library`、`libraryTarget`がたくさんあります。[ドキュメントはこちらです](https://webpack.js.org/configuration/output/)。

## サーバ(express)を追加

サーバでアプリを作れるようになりました。サーブする方法が必要です。個人的にレイルズが好きですが、一番簡単な方法が`express`なのでそれを使います。別の記事でレイルズでサーブする方法について書きます。

`express`と`vue-server-renderer`をインストールします。

```
npm install express vue-server-renderer --save
```

`touch src/server.js`でサーバのコードのファイルを作ります。

```js
const express = require("express")
const renderer = require("vue-server-renderer").createRenderer()
const {createApp} = require("../dist/main")

const server = express()

server.get("*", (req, res) => {
  const app = createApp()

  renderer.renderToString(app).then(html => {
    res.end(html)
  })
})

server.listen(8000, () => console.log("Started server on port 8000."))
```

面白い点が二つあります。

```js
const renderer = require("vue-server-renderer").createRenderer()
```

はサーバレンダーインスタンスを作ります。

```js
renderer.renderToString(app).then(html => {
  res.end(html)
})
```

はアプリを作って、マークアップを文字列として返却します。そして、あの文字列をレスポンスに入れて送ります。[`vue-server-render`のドキュメントはこちらです](https://ssr.vuejs.org/guide/#rendering-a-vue-instance).

サーバを立ち上げます：

```
npm run build && node src/server.js
```

うまくいったら、`localhost:8000`にアクセスして、そこで`Hello`が表示されるはずです。`devtools`でマークアップを検証すると：

```
<div data-server-rendered="true">Hello</div>
```

`data-server-rendererd="true"`があるので、サーバでレンダーされました。

`webpack-dev-server`にサーブされるマークアップと比較すると：

### クライアントでレンダーしたマークアップ:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <div id="app"></div>
<script type="text/javascript" src="main.js"></script></body>
</html>
```

### サーバでレンダーしたマークアップ:

```
<div data-server-rendered="true">Hello</div>
```

`<div id="app">`がないですね。サーバーでレンダーすると、その代わりに`<div>Hello</div>`を入れました。

## まとめ

この記事で学んだこと：

- `libraryTarget`で別の環境に合わせてバンドルすること
- `webpack-merge`の使い方
- `library`、`libraryTarget`について
- サーバとクライアントでコードを共通させること

## 改善

改善できるところがたくさんあります。

- サーバでアプリにデータを動的に入れます（例えばユーザー名など）
- `vue-router`をどうサーバで扱うのか
- Node.js環境で`import`、`export`を使います（`src/index.js`はまだ`require`でやっています

[ソースコードはこちら](https://github.com/lmiller1990/webpack-simple-vue/tree/add_server_rendering).