--- title: expressにwebpack-dev-serverを組み込んでフロントエンドとサーバーサイドを同時にサクサク開発するハンズオン ~Auto Reloadで幸せに~ tags: JavaScript webpack babel Express author: riversun slide: false --- # 概要 ■ **express**に**webpack-dev-server**を組み込むことにより以下のようなことを可能にします - フロントエンド側変更してもブラウザの手動リロードいらず - サーバー側変更しても、サーバー(express on node.js)の手動再起動いらず つまり、「ソースを変更したら、フロントエンド側もサーバー側も**オートリロードする**」ような環境設定の方法を説明します。 これができると、フロントエンドもサーバーもコードを変更したらすぐに反映し動作確認ができるようになりプチハッピーです。 ■ **実現方法** **[webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware)**と**[webpack-hot-middleware](https://github.com/webpack-contrib/webpack-hot-middleware)**を**express**に組み込むことにより、比較的カンタンに実現できます。 ■ **環境** - サーバーは**[express](https://expressjs.com/)**が前提となります - フロントエンド開発にはバンドラーとして**[webpack](https://webpack.js.org/)**を使います - ReactやVue.jsなど特定のLib/Frameworkに依存しません ■ **想定読者** - node.js+webpack4+webpack-dev-server 初級者 ■ **完動するソースコード一式** https://github.com/riversun/webpack-dev-server-on-express # 本編 ### (0)プロジェクト作成 フロントエンド側とサーバー側を両方ふくんだプロジェクトをゼロからつくっていく。 適当な名前のディレクトリを作成し、そこにnpmプロジェクトをつくる。 ```shell mkdir webpack-dev-server-on-express cd webpack-dev-server-on-express npm init ``` (npm init後はエンター9回押せばOK) ### (1)必要なモジュールのインストール 必要なモジュールをインストールする **1.webpack系モジュールのインストール** ```shell npm install --save-dev webpack webpack-cli webpack-dev-server webpack-dev-middleware webpack-hot-middleware ``` **2.サーバー系モジュールのインストール** ```shell npm install --save-dev express multer babel-watch ``` **3.フロントエンド系(クライアント系)モジュールのインストール** ```shell npm install --save-dev @babel/core @babel/preset-env babel-loader core-js@3 ``` **package.json**の**devDependencies**に以下が追加された状態となる ```json "devDependencies": { "webpack": "^4.34.0", "webpack-cli": "^3.3.4", "webpack-dev-server": "^3.7.1", "webpack-dev-middleware": "^3.7.0", "webpack-hot-middleware": "^2.25.0", "express": "^4.17.1", "multer": "^1.4.1", "babel-watch": "^7.0.0", "@babel/core": "^7.4.5", "@babel/preset-env": "^7.4.5", "babel-loader": "^8.0.6", "core-js": "^3.1.4" } ``` 今インストールしたモジュール達は それぞれ、以下のような役割となる。
分類モジュール名役割利用バージョン
webpack系webpackバンドルJSを作るwebpack本体^4.34.0
webpack-cliwebpackのコマンドラインインタフェース^3.3.4
webpack-dev-serverスタンドアロン版webpack-dev-server^3.7.1
webpack-dev-middlewareexpressに組み込めるwebpack-dev-server^3.7.0
webpack-hot-middlewareexpressに組み込めるHot Reloading用ミドルウェア^2.25.0
サーバー系expressnode.jsの定番webフレームワーク^4.17.1
multermultipartのフォームデータを扱う^1.4.1
babel-watchサーバー側の変更ウォッチ用。nodemonの代わり。^7.0.0
フロントエンド系@babel/core最新文法のJavaScript(ES)をレガシー文法に変換^7.4.5
@babel/preset-envbabelの設定プリセット^7.4.5
babel-loaderwebpackでbabelをつかうときのローダー^8.0.6
core-jsbabelの文法変換を助けるpolyfill^3.1.4
■ 現在のディレクトリ構成 ```shell:ディレクトリ構成 webpack-dev-server-on-express ├── node_modules ├── package.json └── package-lock.json ``` ### (2)フロントエンド側を実装する さっそくフロントエンド側(クライアント)から実装する。 今回はサーバ側とフロントエンド側両方とも同じプロジェクトにいれるのでクライアント側のソースコード用に**src_client**というディレクトリをつくり、そこに**index.js**を作成する。 また、**index.html**などコンテンツの置き場所として**public**ディレクトリをつくり、**index.html**を作成する。 ■ 現在のディレクトリ構成 ```shell:ディレクトリ構成 webpack-dev-server-on-express ├── node_modules ├── public │ └── index.html ├── src_client │ └── index.js ├── package.json └── package-lock.json ``` #### (2)-1 index.htmlを作る フロントエンドとサーバーが連動した**単純な足し算**アプリを題材にすすめていく。 フロントエンド側で2つの値(足す数と足される数)を入力して**足し算API(サーバー側の実装)**に**POST**するとサーバー側で足し算処理をして返すというシンプルなもの。 **足し算**の入力フォームを持つ以下のような簡単なhtmlを作る ```html:index.html 足し算

足し算


+

=


``` ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/07550ef1-1e4e-58b8-a85b-5b2db43655b1.png) 数値入力ボックスを2つ設置して、それぞれにfirstValueとsecondValueというパラメータを対応づけてる。 #### (2)-2 index.jsを作る 入力フォームに数値を入力して**計算**ボタンを押すとサーバーにフォームデータを送信するためのコードを書く。 (上のindex.htmlで````となっていたのは、babelでバンドル化するためで、その元となるコードはindex.js。) ```js:index.js const btnSend = document.querySelector('#btn-clac'); btnSend.addEventListener('click', evt => { const xhr = new XMLHttpRequest(); xhr.addEventListener('load', evt => { if (xhr.status == 200) { const result = JSON.parse(xhr.response); const resultEle = document.querySelector('#result'); resultEle.value = result.sum; } }); xhr.addEventListener('error', evt => { console.error(evt); }); xhr.open('post', 'api/add', true); const formEle = document.querySelector('#myform'); const formData = new FormData(formEle); xhr.send(formData); }); ```
- フォームの内容はJavaScriptから送信するため、XMLHttpRequestを使っている - **api/add**というURLにデータをポストする - **new FromData()**で**myform**のフォームデータを**FormData**オブジェクトにしている - **FormData**オブジェクトを**POST**する場合は、**enctype="multipart/form-data"**となるので、サーバー側でもmultipart対応をする必要がある ```js xhr.open('post', 'api/add', true); const formEle = document.querySelector('#myform'); const formData = new FormData(formEle); xhr.send(formData); ``` #### (2)-3 webpack.config.jsを作る 次に、webpackの挙動を設定する**webpack.config.js**を作成する。 この**webpack.config.js**はフロントエンド用の設定として作っているが、後でサーバ-側に**webpack-dev-server**を組み込み際にも使う。 ■ 現在のディレクトリ構成はこうなる ```shell:ディレクトリ構成 webpack-dev-server-on-express ├── node_modules ├── public │ └── index.html ├── src_client │ └── index.js ├── package.json ├── package-lock.json └── webpack.config.js ←今、コレを追加 ``` ```js:webpack.config.js const path = require('path'); module.exports = { mode: 'development', devServer: { contentBase: path.join(__dirname, 'public'), port: 8080, host: `localhost`, }, entry: { app: ['./src_client/index.js'] }, output: { path: path.join(__dirname, 'dist'), publicPath: '/js/', filename: `[name].js`, }, module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: [ { loader: 'babel-loader', options: { presets: [ [ '@babel/preset-env', { 'modules': 'false',//commonjs,amd,umd,systemjs,auto 'useBuiltIns': 'usage', 'targets': '> 0.25%, not dead', 'corejs': 3 } ] ] } } ] } ] }, resolve: { alias: {} }, plugins: [], }; ``` この**webpack.config.js**の意味は別記事**[webpack4対応webpack-dev-serverの主要な設定オプション(CLI,webpack.config.js)の意味と挙動](https://qiita.com/riversun/items/d27f6d3ab7aaa119deab)**にもまとめているので、ここではポイントだけみていく。
```js devServer: { contentBase: path.join(__dirname, 'public'), port: 8080, host: `localhost`, }, ``` ここは、コンテンツ(htmlとか)のルートを**public**にして、**webpack-dev-server**をポート8080で起動する設定となる。
```js entry: { app: ['./src_client/index.js'] }, ``` ここは、フロントエンド側のJavaScriptソースコードのエントリポイントの設定で、 名前が**app**で、エントリポイント(実行開始する最初のモジュール)ソースコードが**./src_client/index.js**であることを示している。 後に、サーバー側のexpressにwebpack-dev-serverを組み込むところで出てくるが、ここを``app: ['./src_client/index.js']``の「`` ['./src_client/index.js']``」ように、配列表現にしておくことがポイント。 サーバー側ではこの配列に、名前**app**に対応するもう1つのエントリーポイントを追加する予定。
```js output: { path: path.join(__dirname, 'dist'), publicPath: '/js/', filename: `[name].js`, }, ``` **publicPath**は**webpack-dev-server**がバンドルJSを配信するときのパス。 filename: `[name].js`の**``[name]``**の部分には、**app**がはいって**app.js**というファイル名となる。**app**は``entry: {``**app**``:['./src_client/index.js']}``から来ている。 つまり、**webpack-dev-server**をつかうときはバンドルJSは``js/app.js``でアクセス可能になるので、上で示した**index.html**では ```html:index.jsのscriptタグ ``` としている。 #### (2)-4 フロントエンドでwebpack-dev-serverを起動する さて、これでフロントエンド側に必要になる3つのソースコードができたので いったんwebpack-dev-serverを起動してみる。 ```shell:ディレクトリ構成 フロントエンド側に必要となるコード ├── public │ └── index.html ├── src_client │ └── index.js └── webpack.config.js ``` package.json に以下のように**start:client**を追加する。 ``` "scripts": { "start:client": "webpack-dev-server --config webpack.config.js" }, ``` コマンドラインで、 ``` npm run start:client ``` と打てば、フロントエンド開発用にwebpack-dev-serverが起動して(ついでにブラウザも)、今つくったコードを早速ためすことができるし、コードを変更すればブラウザが**オートリロード**される。 ここまでは、フツーのフロントエンド環境をつくっただけで、 **やりたいことはこれではない。** (これじゃないけど、ここまでの作業はムダじゃなくて、そのまま流用する) やりたいことは、サーバーとフロントエンドを同時開発 かつ サーバーの手動再起動いらず、フロントエンドの手動再読み込みいらずなので次にサーバー側をつくってやりたいことをやる。 ### (3)サーバ-側を実装する フロントエンド側の準備ができたので、いよいよ**express**に**webpack-dev-server**を組み込む。 早速、作業ディレクトリにサーバー用のディレクトリ**src_server**を作成する。 次に、**src_server**以下にサーバー用のソースコード**server.js**を作成する。 ■ 現在のディレクトリ構成 ```shell:ディレクトリ構成 webpack-dev-server-on-express ├── node_modules ├── public │ └── index.html ├── src_client │ └── index.js ├── src_server  ←今、コレを追加 │ └── server.js  ←コレも追加 ├── package.json ├── package-lock.json └── webpack.config.js ``` 以下が**server.js**となる。 当然だが、サーバー側のコードにノウハウがあるので詳しくみていく。 ```js:server.js const express = require('express'); const multer = require('multer'); const multipart = multer(); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackHotMiddleware = require('webpack-hot-middleware'); const config = require('../webpack.config.js'); const app = express(); const port = 8080; const devServerEnabled = true; if (devServerEnabled) { config.entry.app.unshift('webpack-hot-middleware/client?reload=true&timeout=1000'); config.plugins.push(new webpack.HotModuleReplacementPlugin()); const compiler = webpack(config); app.use(webpackDevMiddleware(compiler, { publicPath: config.output.publicPath })); app.use(webpackHotMiddleware(compiler)); } app.use(express.static('./public')); app.post('/api/add', multipart.any(), function (req, res) { const firstValue = parseInt(req.body.firstValue); const secondValue = parseInt(req.body.secondValue); const sum = firstValue + secondValue; res.json({sum: sum, firstValue: firstValue, secondValue: secondValue}); }); app.listen(port, () => { console.log('Server started on port:' + port); }); ``` 上からコードをみていく
```js const express = require('express'); const multer = require('multer'); const multipart = multer(); ``` ここでは、**express**本体と、マルチパートのフォームデータを処理するための**multer**をインポートしている。 さきほどindex.jsで``new FormData()``した**FormData**オブジェクトを**XMLHttpRequest**をつかってサーバーに送信するコードを書いたが、**FormData**オブジェクトをPOSTする場合には**multipart/form-data**でエンコードされるため、**multipart/form-data**を解釈できるモジュール**multer**をつかっている。
```js const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackHotMiddleware = require('webpack-hot-middleware'); const config = require('../webpack.config.js'); ``` **webpack**、**webpack-dev-middleware**、**webpack-hot-middleware**をインポートする。 また、サーバー内でフロントエンドのwebpack設定を参照するため**webpack.config.js**もインポートする。
```js const app = express(); const port = 8080; const devServerEnabled = true; if (devServerEnabled) { ``` **devServerEnabled**は簡単なフラグ。 これがtrueのときにexpress上のwebpack-dev-server機能が有効になるようにコードを組んだ。 つまり、サーバー側のコード**server.js**のみでwebpack-dev-server機能をOn/Offできる、という仕様にしてみた。
```js config.entry.app.unshift('webpack-hot-middleware/client?reload=true&timeout=1000'); config.plugins.push(new webpack.HotModuleReplacementPlugin()); ``` ここで**config**は``config = require('../webpack.config.js')``なので、**webpack.config.js**(の実体export結果)を意味しているので、**config.entry・・・**や**config.plugins・・・**というコードは、**webpack.config.js**をサーバーのコード上で上書き編集してる考えてよい。 なので、``config.entry.app.unshift('webpack-hot-middleware/client?reload=true&timeout=1000')``では、**config≒webpack.config.js** にある**entry.app**という配列の最初に**'webpack-hot-middleware/client?reload=true&timeout=1000'**を追加しますよという意味になる。 つまり、 ```js config.entry.app.unshift('webpack-hot-middleware/client?reload=true&timeout=1000'); ``` は、 ```js:webpack.config.js抜粋(変更前) entry: { app: ['./src_client/index.js'] }, ``` を↓のように更新したのと同じ意味となる。 ```js:webpack.config.js抜粋(変更後) entry: { app: [ 'webpack-hot-middleware/client?reload=true&timeout=1000', './src_client/index.js' ] }, ``` なので、コードじゃなくて、**webpack.config.js**のほうに書いてしまうのもアリ。 本稿では**server.js**だけで制御したい、という思想をもっているので**server.js**のコードに書いている。 **webpack-hot-middleware/client?reload=true&timeout=1000**自体は**ホットリロード**の設定となる。 ■ **reload=true** の意味 まぎらわしいが、「**リロード**」には以下の2通りあるが、この``reload=true``は主に①のほうに関係がある。 ①. 単純なブラウザ再読込≒オートリフレッシュ(自動更新) ②. HMR(Hot Module Replacement) これについては**[webpack4対応webpack-dev-serverの主要な設定オプションの意味と挙動](https://qiita.com/riversun/items/d27f6d3ab7aaa119deab)**という記事にもまとめたが、 ①の場合は、ブラウザ全体をリロードするのでフォームに入力していた値などはクリアされてしまう。 ②の場合は、ブラウザ全体をリロードするのではなく、変更したモジュール(jsコード)だけを置き換えてくれるのがのがHMR(Hot Module Replacement)という機能だが、この機能を使うためにはこのサーバー側の設定だけではなく、アプリ側もHMRのお作法で設計する必要がある。 本稿では特定のフレームワークに特化しないサンプルコードにしているので、**HMR**のお作法で設計しているわけではない。(**HMR**のお作法で設計するのはそんなに難しくないが、カンタンでもない。Reactなどは対応している。) そこで、**``reload=true``**な設定をした本稿の例では、**index.js**を変更すると**①単純なブラウザ再読込**が実行される(※) ちなみに、[**webpack-hot-middleware**](https://github.com/webpack-contrib/webpack-hot-middleware)本家のreadmeには以下のような記載がある。 >reload - Set to true to auto-reload the page when webpack gets stuck. シンプルに **webpack gets stuck** と書いてあるが、**「HMRをやろうとしたがアップデートされたモジュールがみつからなかった場合、ブラウザをリロードしますよ」**と解釈してOK。webpack処理で何らかの問題が発生したときにもリロードされるが、HMRに対応していない本稿のコードの場合、webpack-hot-middlewareさんからみると「何か更新されたみたいだけど、HMR的にアップデートされたモジュールが見当たらない・・・」となって結果的にリロードされるという[ロジック](https://github.com/webpack-contrib/webpack-hot-middleware/blob/master/process-update.js#L55)になっている。 ということで、長くなったがHMR非対応コードの場合、**reload=true にしておくと、コードを変更するとブラウザでまるっと再読込が走る** という動作となる。 ■ **timeout=1000**の意味 フロントエンドのソースコード変更を検知して、フロントエンド側の再読込をうながすのはサーバー側のexpress上にある**webpack-hot-middle**ミドルウェアの仕事となる。ソースコード変更を検知して、フロントエンド側に通知(サーバープッシュ)するために**webpack-hot-middleware**は**SSE(server-sent-events)**をつかっているが、例えばサーバー側が再起動したりしてしまうと、この**SSE**の接続が切れてしまう。 ``timeout=1000``は、**SSE**の切断検知から再接続処理までの時間を定義する。 本家のreadmeでは↓のとおり >timeout - The time to wait after a disconnection before attempting to reconnect 本稿の例では、サーバー再起動がいちばん切断の原因となりそうなので、**``timeout=1000``はサーバーを再起動したあと、フロントエンド側とサーバー側の再接続までの時間(ms)を設定する** という動作となる 次は``config.plugins.push(new webpack.HotModuleReplacementPlugin());``の設定について。 ```js config.plugins.push(new webpack.HotModuleReplacementPlugin()); ``` これも、さきほど同様、 は、 ```js:webpack.config.js抜粋(変更前) plugins: [], ``` を↓のように更新したのと同じ意味となる。 ```js:webpack.config.js抜粋(変更後) plugins: [new webpack.HotModuleReplacementPlugin()], ``` これは**HMR**を実行可能にするプラグインで、さきほどの**webpack-hot-middleware**から利用されるもの。
```js const compiler = webpack(config); app.use(webpackDevMiddleware(compiler, { publicPath: config.output.publicPath })); app.use(webpackHotMiddleware(compiler)); ``` 次はこの部分だが、上でインポートした2つのミドルウェアをexpressに登録する。 ```js app.use(webpackDevMiddleware(compiler, { publicPath: config.output.publicPath })); ``` ここでは、**webpack-dev-middleware**の設定で、**publicPath**を**webpack.config.js**から参照して設定している。直接書いてもOK。
あとは、特筆する部分は特にないので簡単に。 ```js app.use(express.static('./public')); ``` ここは、コンテンツとなる**index.html**を**public**ディレクトリでホストする設定 ```js app.post('/api/add', multipart.any(), function (req, res) { const firstValue = parseInt(req.body.firstValue); const secondValue = parseInt(req.body.secondValue); const sum = firstValue + secondValue; res.json({sum: sum, firstValue: firstValue, secondValue: secondValue}); }); ``` ここは、POSTメソッドを**/api/add**で受け取るAPI処理部分。足し算ロジックを実行している。 ### 実行 ここまでで環境はできた。 ```shell:ディレクトリ構成 webpack-dev-server-on-express ├── node_modules ├── public │ └── index.html ├── src_client │ └── index.js ├── src_server │ └── server.js ├── package.json ├── package-lock.json └── webpack.config.js ``` 最後に、サーバーを起動するためのスクリプトをpackage.jsonに書く 以下を**package.json**の**script**以下に追加して ```js "start": "babel-watch ./src_server/server.js" ``` **package.json**全体はこうなった ```js:package.json { "name": "webpack-dev-server-on-express", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start:client": "webpack-dev-server --config webpack.config.js", "start": "babel-watch ./src_server/server.js" }, "author": "", "license": "ISC", "devDependencies": { "webpack": "^4.34.0", "webpack-cli": "^3.3.4", "webpack-dev-server": "^3.7.1", "webpack-dev-middleware": "^3.7.0", "webpack-hot-middleware": "^2.25.0", "express": "^4.17.1", "multer": "^1.4.1", "babel-watch": "^7.0.0", "@babel/core": "^7.4.5", "@babel/preset-env": "^7.4.5", "babel-loader": "^8.0.6", "core-js": "^3.1.4" } } ``` サーバー側のコードは **[babel-watch](https://www.npmjs.com/package/babel-watch)**で監視させる。変更があればサーバー側のコードを再起動してくれる。 #### サーバーを起動して、Auto Reloadな開発環境を試す コマンドラインで ```shell npm start ``` これで**webpack-dev-server**相当が組み込まれたexpressサーバーが起動した。 http://localhost:8080/ にアクセスすればアプリを試せる。 (ブラウザでdeveloper toolを起動すれば[HMR]connectedのログが表示されているのがわかる。) ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/6e25e38d-ed99-8ce2-db6a-f01a27c3b5a6.png) これで**/src_client**以下にあるフロントエンド側のソースコードを変更すれば、フロントエンド(ブラウザ)がオートリロードされるし、**/src_server**以下にあるサーバー側のソースコードを変更すれば、サーバ-(express)が自動的に再起動されるようになった。 ### TIPS #### WebpackOptionsValidationError が発生する場合 以下のようなエラーが発生する場合、 ```shell WebpackOptionsValidationError: Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - configuration should be an object. ``` **webpack.config.js**の``module.exports``がObjectではなく、functionを返しているときに発生する。 例えば以下の**webpack.config.js**は起動時に引数で分岐できるようラムダ関数を返す方式になっている。 ```js:webpack.config.js const path = require('path'); module.exports = (env, argv) => { const conf = { mode: 'development', devServer: { contentBase: path.join(__dirname, 'public'), port: 8080, host: `localhost`, }, entry: { app: ['./src_client/index.js'] }, output: { path: path.join(__dirname, 'dist'), publicPath: '/js/', filename: `[name].js`, }, resolve: { alias: {} }, plugins: [], } return conf; }; ``` こういうときは、**webpack.config.js**がオブジェクトを**export**するため、以下のように変更してあげれば良い。 ```js:変更前 const config = require('../webpack.config.js'); ``` **↓** ```js:変更後 const webpackConfigJs = require('../webpack.config.js'); const config = webpackConfigJs();// 実行することで、オブジェクトを得る ``` **webpack.config.js**に引数をいれるときは以下のようにすればいい ```js const config = webpackConfigJs(null, {mode: 'develop'}); ``` また、以下のようなエラーがでる場合も同様の対策で修正可能 ```shell config.entry.app.unshift('webpack-hot-middleware/client?reload=true&timeout=1000'); ^ TypeError: Cannot read property 'app' of undefined ```` # まとめ - **[webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware)**と**[webpack-hot-middleware](https://github.com/webpack-contrib/webpack-hot-middleware)**を**express**に組み込むことによりフロントエンドもサーバーもコードを変更したらすぐにオートリロードで反映し動作確認ができる環境をつくりました ポイントは以下の部分です ```js const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackHotMiddleware = require('webpack-hot-middleware'); const config = require('../webpack.config.js'); config.entry.app.unshift('webpack-hot-middleware/client?reload=true&timeout=1000'); config.plugins.push(new webpack.HotModuleReplacementPlugin()); const compiler = webpack(config); app.use(webpackDevMiddleware(compiler, { publicPath: config.output.publicPath })); app.use(webpackHotMiddleware(compiler)); ``` - 本稿で紹介したソースコード一式は https://github.com/riversun/webpack-dev-server-on-express にあげています