Reduxの情報は山ほどあるけれど

各状態管理ライブラリの簡易さと安全さのトレードオフを掴みたい。

そのためにReduxをベースに各ライブラリを比較する。ということは、まずReduxについてよく知る必要がある。

もちろんReduxについての情報は山ほど出回っている。けど、「簡易さと安全さのトレードオフ」について考察する、という目的に沿って情報を抽出しなければならない。その抽出された情報が、他の各ライブラリとの比較の際にトレードオフの違いをより明確に対比させられる情報を抽出しなくてはならない。

では、それはどんな情報か?

 

・Single Tree or Multi Tree

・直接の状態更新 or 間接の状態更新

 

他にはなんだろう、まずその抽出するべき情報を探すところから調査か。

よくよく考えれば状態管理とはなんなんだろう

よくよく考えれば状態管理とはなんなんだろうとフロントエンドの人が考えている。

  • どのデータを、いつ、どこから、どのように取得できるか?
  • どのデータを、いつ、どこから、どのように更新できるか?

こういったデータに対して何をできるか定義(また、書かないことで拒否することを定義)して、データの変化の経路や、取得の経路を辿れること。 変化と取得の因果関係を人が理解できる記述で定義することで、辿れること。 データの変化や経路が増えることで複雑になっても、辿れること。 因果関係を辿ることをより簡単にする。それがFlux的な定義に沿っていくことだったりする。

定義がより厳密になればより経路がはっきり見えて安全になるけど鈍重になるし、甘くすれば危険は増えるけど身軽になる。 定義がより厳密になるなら、それはより管理されているアプリケーションということになりそう。 管理、といっても定量的捉えらえる言葉。

となると、どこまで管理するかも、また管理せにゃならん。

express + webpack + VueでHMRしながらSSRの開発をする

この記事は、「express + webpack + VueでSSRする」の記事にHMRを追加で組み合わせるための記事であり、重複する内容があります。

概要

expressとwebpackで、Vueのサーバーサイドレンダリングを実装するときの開発環境を構築します。
前回はファイル更新時に毎回webpackコマンドでビルドしてやる必要がありましたが、今回開発サーバーからファイルの更新を監視し、即ブラウザへ反映させることができるHMRも実装します。

手を動かす

今回のデモのリポジトリ
github.com

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

  1. npmでプロジェクトを作成する
  2. express、webpack、Vue等をインストールする
  3. 必要なファイルを手動で作成する
  4. コマンドラインからWEBサーバー(express)を立ち上げる
  5. ブラウザでアクセスして確認する
  6. Vueのソースコードを変更する
  7. ブラウザにリロード無しで変更が反映されたか確認する

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

任意のディレクトリで以下のコマンドを実行します

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

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

以下のコマンドを実行します(--save-devコマンドは1つにまとめても大丈夫です)。

npm install --save-dev chokidar css-loader friendly-errors-webpack-plugin
npm install --save-dev vue-loader vue-template-compiler
npm install --save-dev webpack webpack-dev-middleware webpack-hot-middleware webpack-merge
npm install --save express vue vue-server-renderer

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

以下のファイルは新規作成します。

  1. ./server.js
  2. ./build/setup-dev-server.js
  3. ./build/webpack.base.config.js
  4. ./build/webpack.client.config.js
  5. ./build/webpack.server.config.js
  6. ./src/app.js
  7. ./src/entry-client.js
  8. ./src/entry-server.js
  9. ./src/index.template.html
  10. ./src/App.vue
  11. ./src/components/Bar.vue
  12. ./src/components/Foo.vue

以下のようなディレクトリ構造になるはずです。

│  server.js
│  package.json
│ 
├─build
│   setup-dev-server.js
│   webpack.base.config.js
│   webpack.client.config.js
│   webpack.server.config.js
│
├─node_modules
│   etc...
│
└─src
    app.js
    App.vue
    entry-client.js
    entry-server.js
    index.template.html
    └─components
        Bar.vue
        Foo.vue

package.json

{
  "name": "express-webpack-vue-ssr-hmr-demo",
  "description": "",
  "author": "",
  "private": true,
  "scripts": {
    "dev": "node server"
  },
  "engines": {
    "node": ">=7.0",
    "npm": ">=4.0"
  },
  "dependencies": {
    "express": "^4.16.2",
    "vue": "^2.5.22",
    "vue-server-renderer": "^2.5.22"
  },
  "devDependencies": {
    "chokidar": "^1.7.0",
    "css-loader": "^0.28.7",
    "friendly-errors-webpack-plugin": "^1.6.1",
    "vue-loader": "^15.3.0",
    "vue-template-compiler": "^2.5.22",
    "webpack": "^3.8.1",
    "webpack-dev-middleware": "^1.12.0",
    "webpack-hot-middleware": "^2.20.0",
    "webpack-merge": "^4.2.1"
  }
}
server.js
const express = require('express')
const setupDevServer = require('./build/setup-dev-server')
const { createBundleRenderer } = require('vue-server-renderer')

const app = express()

let renderer
let initRenderer = (bundle, options) => {
  renderer = createBundleRenderer(bundle, options)
}

// レスポンスを返すためのレンダラーができていることを保証するプロミスを取得
let readyRendererPromise = setupDevServer(app, initRenderer)

app.get('*', (req, res) => {
  // レンダラーが準備済みの時はHTMLを描画してレスポンスを返す
  readyRendererPromise.then(() => {
    renderer.renderToString({
      title: 'express + webpack + Vue + SSR + HMR',
      url: req.url
    },
    (err, html) => {
      res.send(html)
    })
  })
})

// サーバーを待ち受け状態にする
const port = process.env.PORT || 8080
app.listen(port, () => {
  console.log(`server started at localhost:${port}`)
})

./build/配下

buildディレクトリの配下には、webpackの設定やフックへの登録等の、アプリケーションをビルドするための処理が収められています。
SSRやHMRの設定もこのディレクトリ内で実施しています。

./build/setup-dev-server.js バンドルファイルの作成、更新をする。
また、レスポンスにHTMLを描画するレンダラーの作成と更新等。
./build/webpack.base.config.js サーバー及びクライアントのバンドルファイルに共通する設定
./build/webpack.client.config.js クライアントのバンドルファイルを作成する設定
./build/webpack.server.config.js サーバーのバンドルファイルを作成する設定
./build/setup-dev-server.js
const fs = require('fs')
const path = require('path')
const resolve = file => path.resolve(__dirname, file)
const MFS = require('memory-fs')
const webpack = require('webpack')
const chokidar = require('chokidar')
const clientConfig = require('./webpack.client.config')
const serverConfig = require('./webpack.server.config')

// レンダラーを作成
// クライアント及びサーバーのバンドルファイル、HTMLテンプレートの更新を発火地点として、レンダラーを更新する
module.exports = function setupDevServer (app, initRenderer) {
  let bundle
  let template
  let clientManifest
  const templatePath = resolve('../src/index.template.html')

  // レスポンスのHTMLを生成するためのレンダラーが準備済みであることを保証する
  let ready
  const readyRendererPromise = new Promise(r => { ready = r })

  // レスポンスのHTMLを生成するためのレンダラーを初期化、更新する
  const updateRenderer = () => {
    if (bundle && clientManifest) { // サーバー及びクライアントのバンドルが作成済みであれば
      ready() // thenの発火を許し、

      // createBundleRenderlerを実行し、server.js内のrenderer変数に対して新しいレンダラーを渡す
      initRenderer(bundle, //Vueをレンダリングするバンドル
        {
          basedir: resolve('../dist'),
          runInNewContext: false,
          template, // レンダリングしたVueを挿入するHTMLテンプレート
          clientManifest // HTMLテンプレートにバンドルを読む<script>タグを挿入する
        })
    }
  }

  // SSRに用いるHTMLファイルを更新した時に、レンダラーも更新する
  template = fs.readFileSync(templatePath, 'utf-8')
  chokidar.watch(templatePath).on('change', () => {
    template = fs.readFileSync(templatePath, 'utf-8')
    console.log('index.html template updated.')
    updateRenderer()
  })

  // クライアント用にバンドルされるファイルの更新を監視
  const clientCompiler = webpack(clientConfig)
  const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
    publicPath: clientConfig.output.publicPath,
    noInfo: true
  })
  app.use(devMiddleware)

  // ファイルの更新をクライアントに通知
  app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))

  // ファイルの更新時に、レンダラーも更新する
  clientCompiler.plugin('done', stats => {
    clientManifest = JSON.parse(readFile(
      devMiddleware.fileSystem,
      'vue-ssr-client-manifest.json'
    ))
    updateRenderer()
  })

  // SSR用のバンドル
  const serverCompiler = webpack(serverConfig)
  const mfs = new MFS()
  serverCompiler.outputFileSystem = mfs
  // SSR用にバンドルされるファイルの更新時に、レンダラーも更新する
  serverCompiler.watch({}, (err, stats) => {
    bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) // vue-ssr-webpack-pluginによって生成される
    updateRenderer()
  })

  return readyRendererPromise
}

// バンドル、マニフェストファイル取得のヘルパー
const readFile = (fs, file) => {
  try {
    return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8') // serverConfig.output.pathでも同じパスが取得できる
  } catch (e) {}
}
./build/webpack.base.config.js

サーバー及びクライアントのバンドルファイルに共通する設定を記述

const path = require('path')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  devtool: '#cheap-module-source-map',
  output: {
    path: path.resolve(__dirname, '../dist'),
    publicPath: '/dist/'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
        new VueLoaderPlugin(),
        new FriendlyErrorsPlugin()
    ]
}
./build/webpack.client.config.js

クライアントのバンドルファイルの設定を記述

const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

const config = merge(base, {
  entry: {
    app: ['webpack-hot-middleware/client', './src/entry-client.js']
  },
  output: {
    filename: '[name].js'
  },
  plugins: [
    new VueSSRClientPlugin(), // clientmanifestを出力
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  ]
})

module.exports = config
./build/webpack.server.config.js

サーバーのバンドルファイルの設定を記述

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

module.exports = merge(base, {
  target: 'node',
  entry: './src/entry-server.js',
  output: {
    filename: 'server-bundle.js',
    libraryTarget: 'commonjs2'
  },
  plugins: [
    new VueSSRServerPlugin()
  ]
})

./src/配下について

srcディレクトリ配下には、Vueアプリケーションに必要なファイルを配置しています。 また、サーバーとクライアントにおいてそれぞれ起点になるファイルを用意しています。

./src/app.js サーバー及びクライアントにおいて共通する処理
./src/entry-client.js クライアントにおいて起点になるファイル
./src/entry-server.js サーバーにおいて起点になるファイル
./src/app.js

サーバー及びクライアントのアプリケーションファイルに共通する処理を記述。

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

export function createApp () {
  const app = new Vue({
    render: h => h(App)
  })

  return { app }
}
./src/entry-client.js

クライアント用のアプリケーションファイルの処理を記述。 クライアントにおいては、このファイルがアプリケーションの起点となります。

import { createApp } from './app'

const { app } = createApp()

app.$mount('#app')
./src/entry-server.js

サーバー用のアプリケーションファイルの処理を記述。 サーバーにおいては、このファイルがアプリケーションの起点となります。

import { createApp } from './app'

export default context => {
  return new Promise((resolve) => {
    const { app } = createApp()
    resolve(app)
  })
}
./src/index.template.html

SSR時にレンダリング時に使用するHTMLテンプレート。 このHTMLにレンダリングされたVueと、clientManifestを元にした\<script>タグ等が挿入され、レスポンスとして送信されます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>{{ title }}</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, minimal-ui">
  </head>
  <body>
  <!--vue-ssr-outlet-->
  </body>
</html>
./src/App.vue

通常のVueコンポーネントです。開発サーバーがHMRを実装しているため、リロード不要で画面に反映されます。

<template>
  <div id="app">
    {{ message }}
    <foo></foo>
    <bar></bar>
  </div>
</template>

<script>
import Foo from './components/Foo.vue'
import Bar from './components/Bar.vue'

export default {
  components: { Foo, Bar }, 
  data () {
    return {
      message: 'express + webpack + Vue + SSR + HMR!!'
    }
  }
}
</script>

./src/components/Bar.vue

通常のVueコンポーネントです。開発サーバーがHMRを実装しているため、リロード不要で画面に反映されます。

<template>
    <div class="bar">{{ bar }}</div>
</template>

<script>
export default {
   data () {
       return {
           bar: 'from bar!!'
       }
   }
}
</script>
./src/components/Foo.vue

通常のVueコンポーネントです。開発サーバーがHMRを実装しているため、リロード不要で画面に反映されます。

<template>
    <div class="foo">{{ foo }}</div>
</template>

<script>
export default {
   data () {
       return {
           foo: 'from foo!!'
       }
   }
}
</script>

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

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

node server

すると、開発用のWEBサーバー(express)が待ち受け状態になります。

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

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

http://localhost:8080/

すると、以下のように表示されています。

express + webpack + Vue + SSR + HMR!!
from foo!!
from bar!!

6. Vueのソースコードを変更する

HMRが動作することを確認するため、以下のvueファイルのソースコードを変更します。

  • ./src/components/Bar.vue
<template>
    <div class="bar">{{ bar }}</div>
</template>

<script>
export default {
   data () {
       return {
           bar: 'from bar!!'
       }
   }
}
</script>

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓


<template>
    <div class="bar">{{ bar }}</div>
</template>

<script>
export default {
   data () {
       return {
           bar: 'FROM BAR!!'
       }
   }
}
</script>

データに含まれる文字列を大文字にしてみました。

7. ブラウザにリロード無しで変更が反映されたか確認する

HMRが正しく動作すれば、ブラウザをリロードしなくても、ブラウザに表示される文字列変更されています。 上記では、文字列を大文字に変更したので以下のように表示されていれば成功です。

express + webpack + Vue + SSR + HMR!!
from foo!!
FROM BAR!!

感想

HMRを実装できたことでようやくSSRの開発環境として形になってきた気がします。 ただ、今のソースだけでも思ったより複雑ですが、これにキャッシュ機能だったり非同期処理の入るルーティングなどが絡んでくるともっと複雑なソースになってきます。

本当に製品でSSRが必要な時は学習コストを考えると、こういったソースをラップしてくれるNuxtなどを使うのが正解な気がしました。 ただ、NuxtであってもSSRの開発に時に気を付けることは似ているような予感がします。

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/

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

参考

たまに見たくなる その1

www.youtube.com

 

ああ、またこの伝説級の動画を見に来てしまった。Jazzドラム界の生けるヒーロー達のスーパープレイ。Dave Weckl、Vinnie Colaiuta、Steve Gaddという3人のドラマーによるセッションだ。

 

3人でドラムセッション?ドラムだけ?と思うかもしれない。確かに、普通であれば魅せるだけの音楽にするのは難しいと感じる。それが普通の感覚だろう。しかし、それを音を操るプロ3人が遂行し、ライブ会場を熱狂させてしまったのがこの動画のライブなのだ。

 

まずドラムセットが3つ前列に並んでるのが奇妙。後ろのバックバンドの人達と一緒に何か演奏するんだと普通は思う。と思いきや、タキシードを着た3人の男がそれぞれ静かにドラムソロを叩き始める。

この時点で全員めっちゃかっこいい(語彙)。BLUE GIANTの宮本大的に言えばめっちゃジャズってる(更に語彙)。

でも、今改めて見れば、Dave Wecklさん(ソロ1人目)の好戦的で挑発めいた表情が、これから始まる怒涛のドラムバトルを予感させている。

 

そして、Steve Gaddさん(ソロ3人目)の重戦車みたいな腹に響くソロの終わりが合図となって、3人のドラムが突如完璧にシンクロする。当たり前みたいに叩いているけど凄すぎる。気持良い。なんなんだこれは。

「今までの演奏は全然小手調べで、さあこれから本気だしまっせ用意はいいですかお客さん?」とでも言われているような。ギアが一段上がって戦闘モードに入ったような。「何か来る!」といった感じの息をのむ緊張感があるのだ。

 

そしてそこからは怒涛のソロ、ソロ、そしてソロ。

ドラムを叩くことに人生の大半を捧げ、生業とする3人が、同じリズムの上でソロを応酬するとはどういうことなのか? 

 

僕はそこに少し闘争的なにおいを感じる。フィギュアスケートみたいに誰かが採点してくれるわけでも無いけれど。勝った負けたは全て個人の裁量にゆだねられるのだけれど。

「お前はそう来るか?なら俺はこっちに行くぜ!」みたいな逆張りがあったり、あえてかぶせていくみたいな展開もあれば、Stave Gaddさんからは我が道を行く感じを受ける(笑)

 

この闘争のにおいは結構正しいらしく、Dave Wecklさんは自分は完全敗北だったとどこかで話していたらしい。 

そして最後は、またも突如3人が完璧にシンクロしてきれいに締めてくれる。魅せるドラムだなあと感じる。凄い。

 

見終わるといつもYEAH!!と熱めに叫ぶ。

express + webpack + Babel + Vue.jsでHMRを使いながらIE11対応する

この記事は、「express + webpack + BabelでHMRを使いながらIE11対応する」の記事にVue.jsを追加で組み合わせるための記事であり、重複する内容があります。

expressとは?

Node.jsのフレームワークです。今回は、開発環境向けにWEBサーバを簡単に立ち上げるために使用します。
公式サイト

webpackとは?

複数ファイルに分けて管理しているjs(モジュール)を一つのファイルにまとめてくれるツール(モジュールバンドラー)です。
(正確には1つのファイルとは限りませんが、役割に応じてファイルをまとめられます)

別の記事が参考になります。

Babelとは?

ブラウザがまだ対応していない最新のJavaScriptソースコードを、ブラウザが対応しているソースコードへ変換してくれるツール (トランスパイラ)です。

別の記事が参考になります。

HMR(Hot Module Replacement)とは?

CSS/JSのソースコードを編集した時に、リロード操作(F5を押す等)無しで編集結果がブラウザに反映される仕組みを提供してくれるwebpackの機能です。

別の記事が参考になります。

Vue.jsとは?

Single Page Application開発等が盛んになってきたことで、フロントエンドの処理やソースコードの複雑さが増し、それを管理するためにMVCのような構造を提供してくれるライブラリやフレームワークが要請されてきました。

その要請に応えるライブラリの一つがVue.jsです。このライブラリは設計におけるビューの部分の構造を提供するライブラリですが、拡張機能を用いれば、フロントエンドでのページルーティングやデータフローの構造を構築しやすくすることが可能です。

今回は複雑なアプリケーションを構築することはせずにVue.jsの

詳しくは公式サイト

expressとwebpackとBabelとVue.jsを組み合わせてHMRを使用した開発をする利点

WEBサーバー(express)からwebpackを使用することで、コマンドラインからwebpackを起動させる必要がなくなり、webpackからBabelを使用することでBabelをコマンドラインから起動させる必要が無くなります。

また、webpackを用いればVue.js独自の拡張子(.vue)を用いたファイルを扱うことが可能になります。 .vueのファイルは、Vue.jsにおけるコンポーネントをファイル単位で管理でき、ソースコードを更に管理しやすくしてくれます。

つまり、コンパイル(モジュール、.vueファイルをバンドル化し、トランスパイルする)工程を自動化させることができます。 そして、HMRを使用することでリロードの作業も省き、開発の高速化が可能になります。

今回は、上記で述べた技術を組み合わせて、IE11に合わせたデモを行います。

手を動かす

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

以下がインストールされていることを前提とします。

  • node.js
  • npm

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

  1. npmでプロジェクトを作成する
  2. express、webpack、Babel、Vue.js等をインストールする
  3. 必要なファイルを手動で作成する
  4. コマンドラインでWEBサーバー(express)を立ち上げる
  5. IE11でアクセスして確認する
  6. .vueファイルを編集し、IE11に反映された結果を確認する

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

任意のディレクトリで以下のコマンドを実行します。

mkdir express-webpack-babel-vue-hmr-demo
cd express-webpack-babel-vue-hmr-demo
npm init -y

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

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

npm install -D webpack webpack-cli webpack-dev-middleware webpack-hot-middleware
npm install -D babel-loader @babel/core @babel/preset-env
npm install -D vue-loader vue-template-compiler
npm install -S @babel/polyfill express vue

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

以下のファイルを作成します。

  • ./index.html
  • ./babel.config.js
  • ./server.js
  • ./webpack.config.js
  • ./src/App.vue
  • ./src/index.js

以下のようなディレクトリ構造になるはずです。

│  babel.config.js
│  index.html
│  package.json
│  server.js
│  webpack.config.js
│
├─node_modules
│  └─etc...
└─src
    App.vue
    index.js

./package.jsonを以下のように書き換えます。

  • - "main": "index.js",
  • + "private": true,
  • + "dev": "node server",
{
  "name": "express-webpack-babel-vue-hmr-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "dev": "node server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.4.0",
    "@babel/preset-env": "^7.4.2",
    "babel-loader": "^8.0.5",
    "vue-loader": "^15.7.0",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0",
    "webpack-dev-middleware": "^3.6.1",
    "webpack-hot-middleware": "^2.24.3"
  },
  "dependencies": {
    "@babel/polyfill": "^7.4.0",
    "express": "^4.16.4",
    "lodash": "^4.17.11",
    "vue": "^2.6.10"
  }
}

./babel.config.jsの内容を次のようにし、babelの動作を設定します。

const presets = [
  [
    "@babel/env",
    {
      targets: {
        ie: '11'
      },
      useBuiltIns: "usage"
    }
  ]
];

module.exports = { presets };


./webpack.config.jsの内容を次のようにし、webpackの動作を設定します。

const webpack = require('webpack');

module.exports = {
  mode: 'development',
  entry:  ['webpack-hot-middleware/client', './src/index.js'], // 2つのファイルをwebpackの基点とする
  output: {
    filename: 'main.js', // 出力されるファイル名
    // 出力されるファイルを読み込めるパスを指定する
    // ファイルはメモリ上に出力され、そのファイルをロードするためのパス
    // https://webpack.js.org/configuration/output/#outputpublicpath
    // https://webpack.js.org/guides/public-path/
    publicPath: '/example_public_path/'
  },
  module: {
    rules: [
      {
        test: /\.js$/, // 拡張子が.jsであり
        exclude: /node_modules/, // ディレクトリがnode_module以外であれば
        use: ['babel-loader'] // Babelの対象とする
      },
      {
        test: /\.vue$/, // 拡張子が.vueであり
        exclude: /node_modules/, // ディレクトリがnode_module以外であれば
        use: ['vue-loader'] // vueのコンポーネントとしてバンドル対象とする
      }
    ]
  },
  resolve: {
    alias: {
      // https://jp.vuejs.org/v2/guide/installation.html#ランタイム-コンパイラとランタイム限定の違い
      'vue$': 'vue/dist/vue.esm.js'
    }
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(), // HMRを使用するためのプラグイン
    new VueLoaderPlugin() // https://vue-loader.vuejs.org/guide/#manual-setup
  ]
};


./server.js
WEBサーバー(express)の動作を記述します。

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const WebpackConfig = require('./webpack.config');

const app = express();
const compiler = webpack(WebpackConfig);

// メモリ上にファイルをコンパイルする
// ファイルを監視して、変更されていれば再コンパイルする
app.use(webpackDevMiddleware(compiler, {
    publicPath: WebpackConfig.output.publicPath,
}));

// クライアントに変更を通知する
// クライアント側でHMRに対応している箇所はリロードせずに更新される
app.use(webpackHotMiddleware(compiler));

// index.htmlを静的に配置しているディレクトリを指定する
app.use(express.static(__dirname));

const port = process.env.PORT || 8080;
app.listen(port, () => {
    console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`);
});


./index.html

バンドルファイル(main.js)以外に、ブラウザ(IE11)用のポリフィルを読み込んでいます。
webpackからポリフィルもまとめてバンドルしてみたのですが、現在(2019/03/24)では動作が不安定になるため、CDNを使用しました。IE11でHMRが不要な場合はmain.jsのみを残して他は削除してください。

<!doctype html>
<html>
  <head>
    <title>express + webpack + babel + vueでHMR</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="https://www.promisejs.org/polyfills/promise-7.0.4.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/event-source-polyfill/0.0.9/eventsource.min.js"></script>
    <script src="./example_public_path/main.js"></script>
  </body>
</html>


./src/index.js
Appコンポーネント(vueファイルはコンポーネント単位で管理される)を取得し、それを用いてVueのインスタンスを作成しています。

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

new Vue({
  el: '#app',
  render: h => h(App)
});


App.vue

<template>
  <div id="app">
    <div>
      {{ hello }}
    </div>
  </div>
</template>

<script>
export default {
  data () {
    return {
      hello: 'Hello express + webpack + Babel + Vue + HMR'
    }
  }
}
</script>

<style lang="sass">
</style>

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

以下のコマンドを実行して、WEBサーバーを立ち上げます。

npm run dev

上記のコマンドは./package.jsonに定義してあり、以下のコマンドと同等です。

node server

5. IE11でアクセスして確認する

以下のURLにアクセスします。

http://localhost:8080/

以下のように表示されていれば、expressとwebpackとBabelとVue.jsの組み合わせは成功です。

Hello express + webpack + Babel + Vue

6. .vueファイルを編集し、IE11に反映された結果を確認する

WEBサーバーを立ち上げたまま、.vueファイルを編集します。 以下の.vueファイルの\<script>\<\/script>タグの中身を編集して保存します。

./src/App.vue

export default {
  data () {
    return {
      hello: 'Hello express + webpack + Babel + Vue'
    }
  }
}

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

export default {
  data () {
    return {
      hello: 'Hello express + webpack + Babel + Vue + HMR' // この行を編集
    }
  }
}

表示が以下のように即座に変更されればIE11上でのHMRが成功です。

Hello express + webpack + Babel + Vue

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

Hello express + webpack + Babel + Vue + HMR

参考サイト

express + webpack + BabelでHMRを使いながらIE11対応する

この記事は、「expressとwebpackでHMRする最小構成を試す」の記事にBabelを追加で組み合わせるための記事です。

expressとは?

Node.jsのフレームワークです。今回は、開発環境向けにWEBサーバを簡単に立ち上げるために使用します。
公式サイト

webpackとは?

複数ファイルに分けて管理しているjs(モジュール)を一つのファイルにまとめてくれるツール(モジュールバンドラー)です。
(正確には1つのファイルとは限りませんが、役割に応じてファイルをまとめられます)

別の記事が参考になります。

Babelとは?

ブラウザがまだ対応していない最新のJavaScriptソースコードを、ブラウザが対応しているソースコードへ変換してくれるツール (トランスパイラ)です。

別の記事が参考になります。

HMR(Hot Module Replacement)とは?

CSS/JSのソースコードを編集した時に、リロード操作(F5を押す等)無しで編集結果がブラウザに反映される仕組みを提供してくれるwebpackの機能です。

別の記事が参考になります。

expressとwebpackとBabelを組み合わせてHMRを使用した開発をする利点

WEBサーバー(express)からwebpackを使用することで、コマンドラインからwebpackを起動させる必要がなくなり、webpackからBabelを使用することでBabelをコマンドラインから起動させる必要が無くなります。

つまり、コンパイル(モジュールをバンドル化し、トランスパイルする)工程が自動化されます。 そして、HMRを使用することでリロードの作業も省くことで開発の高速化が可能になります。

今回は、上記で述べた技術を組み合わせて、IE11対応の開発を行います。

手を動かして、HMRを使いながらIE11対応した開発をする

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

以下がインストールされていることを前提とします。

  • node.js
  • npm

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

  1. npmでプロジェクトを作成する
  2. express、webpack、Babel等をインストールする
  3. 必要なファイルを手動で作成する
  4. コマンドラインでWEBサーバー(express)を立ち上げる
  5. IE11でアクセスして確認する
  6. JSファイルを編集し、IE11に反映された結果を確認する

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

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

mkdir express-webpack-babel-hmr-demo
cd express-webpack-babel-hmr-demo
npm init -y

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

npm install -D webpack webpack-cli webpack-dev-middleware webpack-hot-middleware
npm install -D babel-loader @babel/core @babel/preset-env
npm install --save @babel/polyfill express lodash

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

以下のファイルを手動で作成します。

  • ./babel.config.js
  • ./index.html
  • ./server.js
  • ./webpack.config.js
  • ./src/index.js
  • ./src/foo.js
  • ./src/bar.js

以下のようなディレクトリ構造になります。

│  babel.config.js
│  index.html
│  package.json
│  server.js
│  webpack.config.js
│
├─node_modules
│  └─etc...
└─src
    bar.js
    foo.js
    index.js

./package.jsonを以下のように書き換えます。

  • - "main": "index.js",
  • + "private": true,
  • + "dev": "node server",
{
  "name": "express-webpack-babel-hmr-demo",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "dev": "node server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0",
    "webpack-dev-middleware": "^3.6.1",
    "webpack-hot-middleware": "^2.24.3"
  },
  "dependencies": {
    "express": "^4.16.4",
    "lodash": "^4.17.11"
  }
}

./babel.config.jsの内容を次のようにし、babelの動作を設定します。

const presets = [
  [
    "@babel/env",
    {
      targets: {
        ie: '11'
      },
      useBuiltIns: "usage"
    }
  ]
];

module.exports = { presets };


./webpack.config.jsの内容を次のようにし、webpackの動作を設定します。

const webpack = require('webpack');

module.exports = {
  mode: 'development',
  entry:  ['webpack-hot-middleware/client', './src/index.js'], // 2つのファイルをwebpackの基点とする
  output: {
    filename: 'main.js', // 出力されるファイル名
    // 出力されるファイルを読み込めるパスを指定する
    // ファイルはメモリ上に出力され、そのファイルをロードするためのパス
    // https://webpack.js.org/configuration/output/#outputpublicpath
    // https://webpack.js.org/guides/public-path/
    publicPath: '/example_public_path/'
  },
  module: {
    rules: [
      {
        test: /\.js$/, // 拡張子が.jsであり
        exclude: /node_modules/, // ディレクトリがnode_modules以外であれば
        use: ['babel-loader'] // Babelの対象とする
      }
    ]
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(), // HMRを使用するためのプラグイン
  ]
};


./server.js
WEBサーバー(express)の動作を記述します。

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const WebpackConfig = require('./webpack.config');

const app = express();
const compiler = webpack(WebpackConfig);

// メモリ上にファイルをコンパイルする
// ファイルを監視して、変更されていれば再コンパイルする
app.use(webpackDevMiddleware(compiler, {
    publicPath: WebpackConfig.output.publicPath,
}));

// クライアントに変更を通知する
// クライアント側でHMRに対応している箇所はリロードせずに更新される
app.use(webpackHotMiddleware(compiler));

// index.htmlを静的に配置しているディレクトリを指定する
app.use(express.static(__dirname));

const port = process.env.PORT || 8080;
app.listen(port, () => {
    console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`);
});


./index.html

バンドルファイル(main.js)以外に、ブラウザ(IE11)用のポリフィルを読み込んでいます。
webpackからポリフィルもまとめてバンドルしてみたのですが、現在(2019/03/24)では動作が不安定になるため、CDNを使用しました。

<!doctype html>
<html>
  <head>
    <title>express + webpack + babelでHMR</title>
  </head>
  <body>
    <script src="https://www.promisejs.org/polyfills/promise-7.0.4.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/event-source-polyfill/0.0.9/eventsource.min.js"></script>
    <script src="./example_public_path/main.js"></script>
  </body>
</html>


./src/foo.js

export default () => {
  return 'from foo!';
}


./src/bar.js

export default () => {
  return 'from bar!';
}


./src/index.js
下部にてHMRを実行するための記述をしています。
ファイルが編集されるとサーバーから変更の通知を受け取り、module.hot.accept()の第1引数で指定したJSが変更されていた場合に第2引数のコールバックが実行されます。
コールバックによってbodyタグを再描画しています。

import _ from 'lodash';
import foo from './foo.js'
import bar from './bar.js'


const expressAndWebpack = () => {
  let body = document.querySelector('body');
  body.innerHTML = '';
  body.innerHTML = _.join(['Hello', 'express', '+', 'webpack', '+' , 'Babel'], ' ');
  body.innerHTML += `<br>${foo()}`;
  body.innerHTML += `<br>${bar()}`;
  return body;
}
expressAndWebpack();

if (module.hot) {
  module.hot.accept(['./foo.js', './bar.js'], () => {
    expressAndWebpack();
  });
};

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

以下のコマンドを実行して、WEBサーバーを立ち上げます。

npm run dev

上記のコマンドは./package.jsonに定義してあり、以下のコマンドと同等です。

node server

5. IE11でアクセスして確認する

以下のURLにアクセスします。

http://localhost:8080/

以下のように表示されていれば、expressとwebpackとBabelの組み合わせは成功です。

Hello express + webpack + Babel + Babel
from foo!
from bar! 

6. JSファイルを編集し、IE11に反映された結果を確認する

WEBサーバーを立ち上げたまま、JSファイルを編集します。 以下のJSファイルを編集して保存します。

./src/foo.js

export default () => {
  return 'from foo!';
}

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

export default () => {
  return 'FROM FOO!!';
}

表示が以下のように即座に変更されればIE11上でのHMRが成功です。

Hello express + webpack + Babel
from foo!
from bar!

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

Hello express + webpack + Babel
FROM FOO!!
from bar!