Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

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

ソースコードはこちら.

webpack.config.jsを2つ部分に分ける

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

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

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

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

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

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

module.vueファイルを処理するルールがあって、それは共通ファイルに入ります。残りの設定は全部クライエントレンダリング向けなのでconfig/client.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に入れます。

const path = require("path")

module.exports = {
}

最後にtemplate.htmlconfig

mv template.html config/template.html

最後に、webpack.config.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によってこのようなをコードを書く必要があります:

// 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-mergewebpack.config.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.jsonscriptsを更新します:

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

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

<!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>

が書いてあります。Hellomsgが表示されていません。ブラウザに着いた時に、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:

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

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

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

src/index.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をリファクタします。

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

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

最後に、config/server.jscreate-appをエントリポイントにします:

// ...
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環境で使ってみます:

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

問題があります。

output.libraryoutput.libraryTarget

config/server.jsoutputオプションの設定が必要です。ドキュメンテーションはここにあります

librarylibraryTargetを使います。デフォルトは:

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

varというのはexportsvarにアサインします。その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を見ると:

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...

2番目、あるいは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を更新します:

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で比較します:

< !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]

動いています。librarylibraryTargetがたくさんあります。ドキュメントはこちらです

サーバ(express)を追加

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

expressvue-server-rendererをインストールします。

npm install express vue-server-renderer --save

touch src/server.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."))

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

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

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

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

はアプリを作って、マークアップを文字列として返却します。そして、あの文字列をレスポンスに入れて送ります。vue-server-renderのドキュメントはこちらです.

サーバを立ち上げます:

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にサーブされるマークアップと比較すると:

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

<!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の使い方
  • librarylibraryTargetについて
  • サーバとクライアントでコードを共通させること

改善

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

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

ソースコードはこちら.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away