0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Nestjs (Expressjs) でWebpack を使用する

Posted at

#本記事について
 ある時、Nestjs を利用して開発するにあたり、『webpack を使用したらそのまま開発用サーバーが起動するようになったら便利だなぁ』と思った。
 ...まではいいけど、実際に構築開始してみると、サーバー/クライアントそれぞれにwebpack を適用するとなると、自分がまだ無知だからだろうか、想像以上に時間を要したので、その結果をこの記事にまとめることにした。

目標

 ① $ webpack コマンドを使用したら、そのままコンパイルして開発用サーバを建ててくれる
 ② そのサーバを建てた流れで、client側 も webpack で bundling してくれ、サーバは、その bundling された出力ファイルを提供する。

#Nestjs とは
 Nestjs とは、expressjsFastify のサーバ用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
webpack/server/webpack.dev.js
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
server/webpack.common.js
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
server/main.ts
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.*`ファイルの詳細
webpack/client/webpack.common.js
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',
    }),
  ],
});
webpack/client/webpack.dev.js
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へ、開発用サーバーを建てるとき・商用環境用にビルドするとき・商用環境を実際に始めるとき 用のコマンドを簡単に開始できるように記述しておく。

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.jsonscriptsに記述してあるコマンド群を指定して、逐次実行してくれる。

商用環境(production)での注意点

 本筋とは関係なく、あまり詳しく話すと長くなるので抽象的に記述するが、webpackを使用してproduction環境でビルドしたものへのサーバアクセス時に、@UseGuards 周りの認証・認可で不具合が生じた。二つの passport strategy  (こちら に書いてある二つ)を使用して認証・認可を実装してるのに、実際に起動してみると、片方しか起動していなかった。
 これは、server/webpack.prod.jsファイルに、

webpack/server/webpack.prod.js
// ...something
module.exports = webpackMerge(commonConf, {
  mode: 'production',
  optimization: {
    minimize: false, // 本来 mode: 'production'なら minimize: true に自動的になる
  },
})

 と記述することでうまくいった。商用なのに、コンパイルされたファイルは最小化されていないという改善点が残るが、背に腹は変えられないので、渋々これにしといた。
 これに限らず、webpackmode: production で悩んでいる人(あんまり事例を聞いたことはないけど)は、minimize: false と記述すればうまくいくかもしれない?

感想

 今まで、webpack を使用するときは、$create-react-app$vue init 等のフレームワークで用意されているものしか使用してこなかったから、自分で一から実装したのは初めて。
 結局、最後の不具合の、根本的原因はわからなかったので、webpackのコンパイル時にどういった挙動をしているのか追えたらいいのに・・・と思いました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?