【NestJSアドベントカレンダー】NestJSとwebpack-dev-middlewareを組み合わせる【23日目】【遅刻】
NestJSにwebpack-dev-middlewareを組み込む
2020-04-09追記
ライブラリとして公開しました
遅刻しました.今日は25日.クリスマスです.名取の配信見てたら24日もいつのまにか終わっていました. 23日目は研究室の実験があり,徹夜でNestJSを書いていたので実質NestJSアドベントカレンダー書いたようなものじゃないですか?許してください・・・・・・・
本日のリポジトリ
webpack-dev-middlewareって何?
こいつです.こいつは開発時のみ使える良きもの.webpack-dev-serverだと内部のexpressが使われてしまいまサーバーがフロントエンド用と,バックエンド用の2つできてしまいますが,こいつを使えば自分のサーバの上でwebpack-dev-server的なことができてしまうわけです.よいですね.
Usageを見てみましょう
const webpack = require('webpack'); const middleware = require('webpack-dev-middleware'); const compiler = webpack({ // webpack options }); const express = require('express'); const app = express(); app.use( middleware(compiler, { // webpack-dev-middleware options }) ); app.listen(3000, () => console.log('Example app listening on port 3000!'));
シンプルですね.これをNestJSに組み込んでいきます.
NestJS & webpack-dev-middleware
僕 <「webpack-dev-middlewareを使う.」,「本番でもそのまま動くようにする.」両方やらなくちゃいけないのが〇〇の辛いところだな.*〇〇にしたのは思いつかなかったため
webpack-dev-middlewareを使うmoduleを作る
client.module.tsみたいな名前でやりましょう.名前をつけるのが下手なのは目を瞑ってください
nest g mo client
import { Module, DynamicModule, OnModuleInit, Inject } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; import { CLIENT_MODULE_OPTIONS } from './client-options.constant'; import { ClientOptions } from './client-options.interface'; import { ClientProvider } from './client.provider'; @Module({ providers: [ClientProvider], }) export class ClientModule implements OnModuleInit { constructor( @Inject(CLIENT_MODULE_OPTIONS) private readonly clientOptions: ClientOptions, private readonly httpAdapterHost: HttpAdapterHost, private readonly clientProvider: ClientProvider, ) {} static forRoot(options: ClientOptions): DynamicModule { return { module: ClientModule, providers: [ { provide: CLIENT_MODULE_OPTIONS, useValue: options, }, ], }; } public async onModuleInit() { if (process.env.NODE_ENV !== 'development') return; const httpAdapter = this.httpAdapterHost.httpAdapter; const { webpackConfig } = this.clientOptions; this.clientProvider.register(httpAdapter, webpackConfig); } }
moduleではforRootを使ってDynamicModuleを返すようにします.MongooseModuleとかTypeOrmModuleとかと同じですね.
@Inject(CLIENT_MODULE_OPTIONS) private readonly clientOptions: ClientOptions
これ頭いいなぁって思ったんですけど,forRootで返したproviderから拾ってきています.forRootはstaticmethodなのでonModuleInitに対して変数を渡すことはできないのですがこれで実現しています.
こんなことは自分では考え付かない・・・後述するServeStaticModule
の内部実装がこうなっていたのを真似しました.
続いてProviderを作っていきます. Providerではwebpackのコンフィグをwebpack-dev-middlewareに食わせて,httpAdapter.つまり,NestJSの本体に登録していきます.
import { Injectable } from '@nestjs/common'; import { Configuration, Entry, EntryFunc } from 'webpack'; import webpack from 'webpack'; import { AbstractHttpAdapter } from '@nestjs/core'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; @Injectable() export class ClientProvider { async register(app: AbstractHttpAdapter, config: Configuration) { const compiler = webpack({ ...config, mode: 'development', entry: await this.appendHotMiddlewareToEntry(config.entry), plugins: [...config.plugins, new webpack.HotModuleReplacementPlugin()], }); app.use(webpackDevMiddleware(compiler)); app.use(webpackHotMiddleware(compiler)); } private async appendHotMiddlewareToEntry( entry: string | string[] | Entry | EntryFunc, ) { const hot: string = 'webpack-hot-middleware/client?reload=true&timeout=1000'; let e = entry; e = e instanceof Function ? await e() : e; if (e instanceof Array) return [...(e as string[]), hot]; else if (typeof e === 'string') return [e, hot]; else { for (const key in e) { if (e[key] instanceof Array) e[key] = [...e[key], hot]; else e[key] = [e[key], hot] as string[]; } return e; } } }
どうせならHMRも登録してしまえ!ということでやっています.appendHotHiddlewareToEntry
ではwebpackのentry全てにhot-middlewareを付け加えています.なんかヤバそうな気がする・・・・
ここでやるのはwebpack-dev-middleware周りの設定だけにしています. 特に説明すべきことはないと思いますので次に行きます.
configの型を決めましょう.
configだけくれればそれでええ!
import { Configuration } from 'webpack'; export interface ClientOptions { webpackConfig: Configuration; }
export const CLIENT_MODULE_OPTIONS = 'CLIENT_MODULE_OPTIONS';
moduleを登録する
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ClientModule } from './client/client.module'; import config from '@/webpack.config.client.js'; import { Configuration } from 'webpack'; @Module({ imports: [ ClientModule.forRoot({ webpackConfig: config as Configuration, }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
実行する
やることリスト
- [ ] webpack.config.jsを書く
- [ ] フロントエンドのコードをcliet/App.tsxみたいな感じで作っとく
わからなかったら僕の作ったリポジトリをそのまま使えば大丈夫です
動かしてみる
NODE_ENV=development yarn ts-node src/main.ts
で動くはず
僕のリポジトリを使っている場合は
yarn build:dev yarn start:dev
でも動きます.
プロダクションではwebpack-dev-middlewareを使わずにやる
serve-staticを使うと静的サイトを配信できます.これと今作ったwebpack-dev-middlewareのmoduleをNODE_ENV
の状態で切り替えれば完璧に動くはずです.
moduleを変更する
import { Module, DynamicModule, OnModuleInit, Inject } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; import { CLIENT_MODULE_OPTIONS } from './client-options.constant'; import { ClientOptions } from './client-options.interface'; import { ServeStaticModule } from '@nestjs/serve-static'; import { ClientProvider } from './client.provider'; @Module({ providers: [ClientProvider], }) export class ClientModule implements OnModuleInit { constructor( @Inject(CLIENT_MODULE_OPTIONS) private readonly clientOptions: ClientOptions, private readonly httpAdapterHost: HttpAdapterHost, private readonly clientProvider: ClientProvider, ) {} static forRoot(options: ClientOptions): DynamicModule { return process.env.NODE_ENV !== 'development' ? ServeStaticModule.forRoot(options) : { module: ClientModule, providers: [ { provide: CLIENT_MODULE_OPTIONS, useValue: options, }, ], }; } public async onModuleInit() { if (process.env.NODE_ENV !== 'development') return; const httpAdapter = this.httpAdapterHost.httpAdapter; const { webpackConfig } = this.clientOptions; this.clientProvider.register(httpAdapter, webpackConfig); } }
interfaceを変更する
import { ServeStaticModuleOptions } from '@nestjs/serve-static'; import { Configuration } from 'webpack'; export interface ClientOptions extends ServeStaticModuleOptions { webpackConfig: Configuration; }
app.module.tsに変更を反映する
静的サイトはdist/public
にbuildされるように僕のリポジトリでは設定されているのでそのように設定します.
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { ClientModule } from './client/client.module'; import config from '@/webpack.config.client.js'; import { Configuration } from 'webpack'; import { getMetadataArgsStorage } from 'typeorm'; import { UserModule } from './user/user.module'; import ormConfig from '@/ormconfig.json'; import { join } from 'path'; const { cli, migrations, ...typeOrmConfig } = { ...ormConfig, entities: getMetadataArgsStorage().tables.map(tbl => tbl.target), }; @Module({ imports: [ ClientModule.forRoot({ renderPath: '/', rootPath: join(__dirname, 'public'), webpackConfig: config as Configuration, }), TypeOrmModule.forRoot(typeOrmConfig as TypeOrmModuleOptions), UserModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
以上です.これで先ほどと同じように
NODE_ENV=development yarn ts-node src/main.ts
僕のリポジトリを使っている場合は
yarn build:dev yarn start:dev
で動くかと思います.clientもビルドして配信されるかどうか試してみてください.
僕のリポジトリでは
yarn build yarn start:prod
でできると思います.
TypeORMとwebpack-dev-middlewareとaspidaと俺と
Reactで作ったUserのCreateとid検索ができるサンプルアプリを作りました.aspidaで型安全にapiを叩きながらNestJSでapiを実装する.それを開発時はwebpack-dev-middlewareから配信されるReactから叩く,プロダクション時はserve-staticで配信されるReactからapiが叩かれる.なかなか型で守られていていいのではないでしょうか?僕はいいと思って作りました.
動かし方
開発時
docker-compose up --build -d yarn typeorm migration:run yarn build:dev yarn start:dev
それ以外
mysqlはdocker-composeで立ち上げたままで,開発環境と別にしたい時は,ormconfig.jsonをdevとprodでわけるといいでしょう.
yarn build yarn start:prod
こんな画面になるはずです
おわりに
お疲れ様でした.長い記事になってしまいました.きっとDXが上がるはず!と信じてやってみました.なかなか良さそうではないでしょうか?
今回はReactでやっていますが,Vueでもできると思います.Angularはコマンド一発でできるのでこんなことはしなくていいです.
どうでもいいのですが.明日卒論の目次案のゼミがあります.書くことが何もありません・・・・・NestJSの記事貼り付けようかな・・・・
これと関連している記事でTypeORMとwebpackという記事も書いているのでよかったらみてください naporitan.hatenablog.com