Vue SSR ガイド, vuejs/vue-hackernews-2.0 を参考に環境構築を試してみた時のメモです。
- サーバサイドで Vue をコンパイルする
- クライアントとサーバのコードをわける (Webpack)
- ルーティング
- プリフェッチを実現
- サーバとクライアント、それぞれをビルドするために個別の Webpack 設定ファイルを用意
- バンドルレンダラ
- クライアントマニフェストを生成
- ビルド設定をもうちょい
サーバサイドで Vue をコンパイルする
https://github.com/ryotah/vue-ssr/compare/step-0...step-1-render-to-string
やっていること:
コード抜粋:
server.js
const Vue = require("vue"); const server = require("express")(); // ステップ 1: レンダラを作成 const renderer = require("vue-server-renderer").createRenderer({ template: require("fs").readFileSync("./index.template.html", "utf-8") }); server.get("*", (req, res) => { // ステップ 2: Vue インスタンスを作成 const app = new Vue({ data: { url: req.url }, template: `<div>The visited URL is: {{ url }}</div>` }); // ステップ 3: Vue インスタンスを HTML に描画 renderer .renderToString(app) .then(html => res.end(html)) .catch(err => { res.status(500).end("Internal Server Error"); }); }); server.listen(8080);
参考:
クライアントとサーバのコードをわける (Webpack)
https://github.com/ryotah/vue-ssr/compare/step-1-render-to-string...step-2-webpack
やっていること:
- クライアントとサーバー両方をバンドルするために webpack を導入
entry-client.js
,entry-server.js
,app.js
を用意entry-*.js
がapp.js
(アプリケーションのユニバーサルエントリー) を読み込む
コード抜粋:
entry-client.js
import { createApp } from "./app"; export default context => { const { app } = createApp(context); return app; };
server.js
// ... const createApp = require("./dist/entry").default; server.get("*", (req, res) => { // ステップ 2: Vue インスタンスを作成 const app = createApp({ url: req.url }); // ステップ 3: Vue インスタンスを HTML に描画 // ... }); server.listen(8080);
webpack.config.js
module.exports = { entry: "./entry-server.js", output: { filename: "entry.js", path: path.resolve(__dirname, "dist"), libraryTarget: "commonjs2" } // ... };
参考:
ルーティング
https://github.com/ryotah/vue-ssr/compare/step-2-webpack...step-3-router
やっていること:
vue-router
を導入entry-server.js
にルーティングロジックを実装
コード抜粋:
entry-server.js
import { createApp } from "./app"; export default context => { return new Promise((resolve, reject) => { const { app, router } = createApp(); // set server-side router's location router.push(context.url); // wait until router has resolved possible async components and hooks router.onReady(() => { const matchedComponents = router.getMatchedComponents(); if (!matchedComponents.length) { reject({ code: 404 }); } resolve(app); }, reject); }); };
参考:
プリフェッチを実現
https://github.com/ryotah/vue-ssr/compare/step-3-router...step-4-store
やっていること:
- サーバサイドとクライアントサイドで同じデータを利用可能にするために
Vuex
を導入 - ルートコンポーネントにプリフェッチ用メソッド
asyncData
を実装 entry-client.js
にサーバからのデータを受け取るstore.replaceState(window.__INITIAL_STATE__)
を追加
コード抜粋:
entry-server.js
// ... router.onReady(() => { const matchedComponents = router.getMatchedComponents(); // ... // call `asyncData()` on all matched route components Promise.all( matchedComponents.map( ({ asyncData }) => asyncData && asyncData({ store, route: router.currentRoute }) ) ) .then(() => { // すべてのプリフェッチのフックが解決されると、ストアには、 // アプリケーションを描画するために必要とされる状態が入っています。 // 状態を context に付随させ、`template` オプションがレンダラに利用されると、 // 状態は自動的にシリアライズされ、HTML 内に `window.__INITIAL_STATE__` として埋め込まれます context.state = store.state; resolve(app); }) .catch(reject); }, reject); // ...
参考:
サーバとクライアント、それぞれをビルドするために個別の Webpack 設定ファイルを用意
https://github.com/ryotah/vue-ssr/compare/step-4-store...step-5-hydration
バンドルレンダラ
https://github.com/ryotah/vue-ssr/compare/step-5-hydration...step-6-bundle-renderer
やっていること:
- バンドルレンダラを利用
- JSON 形式でサーバサイドのコードをバンドル
参考:
クライアントマニフェストを生成
https://github.com/ryotah/vue-ssr/compare/step-6-bundle-renderer...step-7-client-manifest
参考:
ビルド設定をもうちょい
https://github.com/ryotah/vue-ssr/compare/step-7-client-manifest...step-8-build
やっていること:
webpack-dev-middleware
を導入- コードに変更があったら、サーバとクライアント両方をビルドするように設定
コード抜粋:
server.js
const path = require('path'); const express = require('express'); const MemoryFS = require('memory-fs'); const app = express(); const webpack = require('webpack'); let clientManifest; let serverBundle; let renderer; function update() { if (!clientManifest || !serverBundle) { return; } // create a renderer const { createBundleRenderer } = require('vue-server-renderer'); const template = require('fs').readFileSync('./index.template.html', 'utf-8'); renderer = createBundleRenderer(serverBundle, { runInNewContext: false, // recommended template, clientManifest, }); } // dev middleware const clientConfig = require('./build/webpack.client.config.js'); const clientCompiler = webpack({ ...clientConfig, // TODO: mode: 'development', }); const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { publicPath: clientConfig.output.publicPath, }); app.use(devMiddleware); clientCompiler.plugin('done', stats => { try { clientManifest = JSON.parse( devMiddleware.fileSystem.readFileSync( path.join(clientConfig.output.path, 'vue-ssr-client-manifest.json'), 'utf-8' ) ); } catch (e) { console.log(e); } update(); }); // watch and update server renderer const serverConfig = require('./build/webpack.server.config.js'); const serverCompiler = webpack({ ...serverConfig, // TODO: mode: 'development', }); const mfs = new MemoryFS(); serverCompiler.outputFileSystem = mfs; serverCompiler.watch({}, (err, stats) => { try { serverBundle = JSON.parse( mfs.readFileSync( path.join(clientConfig.output.path, 'vue-ssr-server-bundle.json'), 'utf-8' ) ); } catch (e) { console.log(e); } update(); }); // ...