#本記事について
ある時、Nestjs を利用して開発するにあたり、『webpack を使用したらそのまま開発用サーバーが起動するようになったら便利だなぁ』と思った。
...まではいいけど、実際に構築開始してみると、サーバー/クライアントそれぞれにwebpack を適用するとなると、自分がまだ無知だからだろうか、想像以上に時間を要したので、その結果をこの記事にまとめることにした。
目標
① $ webpack
コマンドを使用したら、そのままコンパイルして開発用サーバを建ててくれる
② そのサーバを建てた流れで、client側 も webpack
で bundling してくれ、サーバは、その bundling された出力ファイルを提供する。
#Nestjs とは
Nestjs とは、expressjs・Fastify のサーバ用Nodejsフレームワークを、別の形として提供するもの。
TypeScript (公式サイトによると普通のJavaScriptでも可能らしい) を推していて、OOP(オブジェクト指向プログラミング)
・FP(関数型プログラミング)
・FRP(関数型リアクティブプログラミング)
のいいとこ取りをしたもの。
(公式を読めば分かるが、OOP
に関しては、DI
とか駆使してて分かるけど、FP/FRP
に関してはどの辺が該当するのかわからなかった。てかそもそもFP/FRP
への理解度が浅い。)
ディレクトリ構成
server側には、Nestjs(Express)
client側には、Reactjs を使用している
┣ client
┃ ┣ index.html
┃ ┣ main.tsx
┃ ┗ 〜 other miscellaneous directories or files for Reactjs 〜
┣ server
┃ ┣ main.ts
┃ ┣ root
┃ ┃ ┣ root.module.ts
┃ ┃ ┗ 〜 other files for root module 〜
┃ ┗ 〜 other miscellaneous directories 〜
┗ webpack
┣ client
┃ ┣ webpack.common.js
┃ ┣ webpack.dev.js
┃ ┗ webpack.prod.js
┗ server
┣ webpack.common.js
┣ webpack.dev.js
┗ webpack.prod.js
webpack について詳しくない人向け
こちらのudemy の講座が(たしか)無料で、webpack を学ぶにはいい講座でした。
この講座と、公式サイト を見比べながら進めれば大体理解できた。
以下から、本題に入っていく。
#目標①
###nodemon-webpack-plugin を使用する
nodemonとは
・node を使用したアプリケーションに適用でき、ファイルを監視して、変更があったら自動で変更を適用してくれる。
・参考(npm): https://www.npmjs.com/package/nodemon
webpack への適用
この nodemon を適用できるwebpack
の拡張機能が、nodemon-webpack-plugin
として提供されているので、それを使用。
インストール & コーディング
$ yarn add --dev nodemon-webpack-plugin
const webpackMerge = require('webpack-merge');
const NodemonPlugin = require('nodemon-webpack-plugin')
const commonConf = require('./webpack.common');
const path = require('path')
module.exports = webpackMerge(commonConf, {
mode: 'development',
watch: true,
plugins: [
new NodemonPlugin({
watch: path.join(process.cwd(), './dist'),
script: path.join(process.cwd(), './dist/server.js')
})
]
})
watch オプションは、変更を監視する対象のディレクトリ
script オプションは、実行するファイル
webpack-node-externals の使用
####なんで必要か
https://www.npmjs.com/package/webpack-node-externals#why-is-not-bundling-node_modules-a-good-thing
↑ このサイトを見てもらえれば分かるのだが、Webpack でそのままサーバを立てるとき(厳密には違うかもしれないけど)、node_modules をwebpack
での bundling 対象外にしないといけないらしい。
曰く、
要約すると、アプリで使用されるパッケージは全てnpm によって管理される方が便利
とのことらしい。なんかよくわからないけど、解釈としてはパッケージ群はnpmで既に管理してるんだからwebpackは余計なことすんな
てこと?
インストール & コーディング
$ yarn add --dev webpack-node-externals
const path = require('path');
const nodeExternals = require('webpack-node-externals')
module.exports = {
entry: path.resolve(process.cwd(), './server/main'),
target: 'node',
externals: [nodeExternals()],
output: {
path: path.resolve(process.cwd(), './dist'),
filename: 'server.js',
},
...
...
}
うーん、こっちは特筆すべきことはないかなぁ。。。
(nodeExternalsに関係ないけど)注意点としては、nodemon-webpack-plugins
の script オプションによる実行ファイルとwebpack
の output オプション の出力ファイル名を一致させておくこと。
#目標②
webpack-dev-middleware の使用
####webpack-dev-middleware とは
expressjs の開発用サーバに使用でき、クライアント側のファイルをまとめた(bundleした)ものをサーバ側に提供してくれる。
####インストール & コーディング
$ yarn add --dev webpack-dev-middleware
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import * as webpack from 'webpack';
import * as clientWebpackConfig from '../webpack/client/webpack.dev';
const webpackDevMiddleware = require('webpack-dev-middleware');
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(RootModule);
const compiler = webpack(clientWebpackConfig())
app.use(webpackDevMiddleware(compiler))
await app.listen(3000);
}
bootstrap();
`webpack/client/webpack.*`ファイルの詳細
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = ({ outputFilename }) => ({
entry: './client/main',
output: {
path: path.resolve(process.cwd(), 'dist/client'),
filename: `${outputFilename}.js`,
chunkFilename: `${outputFilename}.js`,
},
resolve: {
alias: {
'@': path.resolve(process.cwd(), './'),
},
extensions: ['.js', '.ts', '.tsx', '.jsx', 'scss'],
},
module: ... // 省略
optimization: ... // 省略
plugins: [
new MiniCssExtractPlugin({
filename: `${outputFilename}.css`,
}),
new HtmlWebpackPlugin({
template: path.resolve(process.cwd(), 'client/index.html'),
filename: 'index.html',
inject: 'body',
}),
],
});
const path = require('path');
const webpackMerge = require('webpack-merge');
const commonConf = require('./webpack.common');
const outputFilename = '[name]';
module.exports = () =>
webpackMerge(commonConf({ outputFilename }), {
mode: 'development',
devtool: 'source-map',
});
ここでは、webpack が動くように、DIを意識せずに最低限で書いているが、実際には、ServeStaticModule、及びServeStaticService(オリジナル)
を実装して、そのServeStaticService
の中で、 webpack-dev-middleware
を実装している。
#package.json への記述
最後に、package.json
へ、開発用サーバーを建てるとき・商用環境用にビルドするとき・商用環境を実際に始めるとき 用のコマンドを簡単に開始できるように記述しておく。
{
...
"scripts": {
"prebuild": "rimraf dist",
"start:dev": "rimraf dist && webpack --config webpack/server/webpack.dev.js",
"start:prod": "nohup node ./dist/server.js &",
"build": "run-s build:*",
"build:server": "webpack --config webpack/server/webpack.prod.js",
"build:client": "webpack --config webpack/client/webpack.prod.js",
}
...
}
run-p
は、npm-run-all のパッケージを使用したら使えるコマンド。
package.json
のscripts
に記述してあるコマンド群を指定して、逐次実行してくれる。
商用環境(production)での注意点
本筋とは関係なく、あまり詳しく話すと長くなるので抽象的に記述するが、webpack
を使用してproduction
環境でビルドしたものへのサーバアクセス時に、@UseGuards
周りの認証・認可で不具合が生じた。二つの passport strategy (こちら に書いてある二つ)を使用して認証・認可を実装してるのに、実際に起動してみると、片方しか起動していなかった。
これは、server/webpack.prod.js
ファイルに、
// ...something
module.exports = webpackMerge(commonConf, {
mode: 'production',
optimization: {
minimize: false, // 本来 mode: 'production'なら minimize: true に自動的になる
},
})
と記述することでうまくいった。商用なのに、コンパイルされたファイルは最小化されていないという改善点が残るが、背に腹は変えられないので、渋々これにしといた。
これに限らず、webpack
の mode: production
で悩んでいる人(あんまり事例を聞いたことはないけど)は、minimize: false
と記述すればうまくいくかもしれない?
感想
今まで、webpack
を使用するときは、$create-react-app
や$vue init
等のフレームワークで用意されているものしか使用してこなかったから、自分で一から実装したのは初めて。
結局、最後の不具合の、根本的原因はわからなかったので、webpack
のコンパイル時にどういった挙動をしているのか追えたらいいのに・・・と思いました。