こんにちは。ウェブパックマスターです。この記事では、前の記事のゼロからウェブパックを作る続きとなります。今回は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つのプロパティがあります:
- entry
- module
- plugins
- 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.html
をconfig
に
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-merge
でwebpack.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.json
のscripts
を更新します:
"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>
が書いてあります。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:
この関数を呼び出してレンダーするアプリを返します。
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.js
でcreate-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.library
とoutput.libraryTarget
config/server.js
でoutput
オプションの設定が必要です。ドキュメンテーションはここにあります。
library
とlibraryTarget
を使います。デフォルトは:
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
を見ると:
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]
動いています。library
、libraryTarget
がたくさんあります。ドキュメントはこちらです。
サーバ(express)を追加
サーバでアプリを作れるようになりました。サーブする方法が必要です。個人的にレイルズが好きですが、一番簡単な方法がexpress
なのでそれを使います。別の記事でレイルズでサーブする方法について書きます。
express
とvue-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
の使い方 -
library
、libraryTarget
について - サーバとクライアントでコードを共通させること
改善
改善できるところがたくさんあります。
- サーバでアプリにデータを動的に入れます(例えばユーザー名など)
-
vue-router
をどうサーバで扱うのか - Node.js環境で
import
、export
を使います(src/index.js
はまだrequire
でやっています