express + webpack + VueでSSRする

express + webpack + VueでSSRする

以前にexpressとVueのみを用いて行ったSSRはシンプルに実装できましたが、今回webpackを組み合わせたSSRはファイル数も増え、仕組みも複雑になってきました。 今回はSSR環境構築の足場を作り、次はホットリロードなども実装して仕組みの解説も行いたいと思ます。

手を動かす

今回作ったプロジェクトのリポジトリ

以下が手順概要になります。

  1. npmでプロジェクトを作成する
  2. express、webpack、Vue等をインストールする
  3. 必要なファイルを手動で作成する
  4. コマンドラインでからwebpackを起動し、ビルドする
  5. コマンドラインからWEBサーバー(express)を立ち上げる
  6. ブラウザでアクセスして確認する

1. npmでプロジェクトを作成する

以下のコマンドを実行します。

mkdir express-webpack-vue-ssr-demo
cd express-webpack-vue-ssr-demo
npm init -y

2. express、webpack、Babel、Vue等をインストールする

npm install -D webpack webpack-cli webpack-merge friendly-errors-webpack-plugin
npm install -S express vue vue-server-renderer

3. 必要なファイルを手動で作成する

  • ./index.template.html
  • ./server.js
  • ./webpack.base.config
  • ./webpack.client.config
  • ./webpack.server.config
  • ./src/App.vue
  • ./src/app.js
  • ./src/entry-client.js
  • ./src/entry-server.js

package.json

{
  "name": "express-webpack-babel-vue-ssr-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "dev": "node server",
    "build": "webpack --config=webpack.server.config.js && webpack --config=webpack.client.config.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "friendly-errors-webpack-plugin": "^1.7.0",
    "vue-loader": "^15.7.0",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0",
    "webpack-merge": "^4.2.1"
  },
  "dependencies": {
    "express": "^4.16.4",
    "vue": "^2.6.10",
    "vue-server-renderer": "^2.6.10"
  }
}

./index.template.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>express + webpack + Vue + SSR</title>
  <script src="dist/main.js" defer></script>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>

./server.js

const path = require('path');
const fs = require('fs');
const express = require('express');
const VueServerRenderer = require('vue-server-renderer');

const app = express();

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

const template = fs.readFileSync('./index.template.html', 'utf-8');
const renderer = VueServerRenderer.createBundleRenderer(path.join(__dirname, 'dist/vue-ssr-server-bundle.json'), { template });


app.get('*', (req, res) => {
  const ctx = { url: req.url };
  renderer.renderToString(ctx, (err, html) => {
    res.end(html);
  });
});

app.listen(8080, () => {
  console.log(`Server listening on http://localhost:8080, Ctrl+C to stop`);
});

./webpack.base.config.js

const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');


module.exports = {
  devtool: 'inline-source-map',
  output: {
    filename: '[name].js',
    path: path.join(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.vue/,
        exclude: /node_modules/,
        use: ['vue-loader']
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new FriendlyErrorsWebpackPlugin()
  ]
};

./webpack.client.config.js

const baseConfig = require('./webpack.base.config.js');
const merge = require('webpack-merge');
const path = require('path');

module.exports = merge(baseConfig, {
  entry: path.join(__dirname, 'src/entry-client.js')
});

./webpack.server.config.js

const path = require('path');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const baseConfig = require('./webpack.base.config.js');
const merge = require('webpack-merge');


module.exports = merge(baseConfig, {
  entry: path.join(__dirname, './src/entry-server.js'),
  target: 'node',
  output: {
    libraryTarget: 'commonjs2'
  },
  plugins: [
    new VueSSRServerPlugin()
  ]
});

./src/App.vue

<template>
  <div id="app">
    <div><input v-model="text" type="text"></div>
    {{ text }}
  </div>
</template>

<script>
  export default {
    data() {
      return {
        text: 'Hello express + webpack + Vue + SSR!!'
      }
    }
  }
</script>

<style>
</style>

./src/app.js

import Vue from 'vue';
import App from './App.vue';

export default function createApp() {
  return new Vue({
    render: h => h(App)
  });
}

./src/entry-client.js

import createApp from './app';

const app = createApp();
app.$mount('#app');

window.myApp = app;

./src/entry-server.js

import createApp from './app';

export default ctx => {
  return new Promise((resolve, reject) => {
    const app =  createApp();
    resolve(app);
  });
}

4. コマンドラインでからwebpackを起動し、ビルドする

以下のコマンドを実行します。

npm run build

5. コマンドラインからWEBサーバー(express)を立ち上げる

以下のコマンドを実行します

npm run dev

5. ブラウザでアクセスして確認する

以下のURLにブラウザでアクセスします。

http://localhost:8080/

表示されたフォームの値と、表示されているテキストが連動して変更されれば成功です。

参考