なぽろぐ

気ままに感じたことを記事にまとめます。

【NestJSアドベントカレンダー】NestJSとwebpack-dev-middlewareを組み合わせる【23日目】【遅刻】

NestJSにwebpack-dev-middlewareを組み込む

2020-04-09追記

ライブラリとして公開しました

www.npmjs.com

遅刻しました.今日は25日.クリスマスです.名取の配信見てたら24日もいつのまにか終わっていました. 23日目は研究室の実験があり,徹夜でNestJSを書いていたので実質NestJSアドベントカレンダー書いたようなものじゃないですか?許してください・・・・・・・

qiita.com

本日のリポジトリ

github.com

webpack-dev-middlewareって何?

github.com

こいつです.こいつは開発時のみ使える良きもの.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

でも動きます.

http://localhost:3000

プロダクションではwebpack-dev-middlewareを使わずにやる

serve-staticを使うと静的サイトを配信できます.これと今作ったwebpack-dev-middlewareのmoduleをNODE_ENVの状態で切り替えれば完璧に動くはずです.

github.com

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が叩かれる.なかなか型で守られていていいのではないでしょうか?僕はいいと思って作りました.

github.com

github.com

動かし方

開発時

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

こんな画面になるはずです f:id:Naporitan:20191225030223g:plain

おわりに

お疲れ様でした.長い記事になってしまいました.きっとDXが上がるはず!と信じてやってみました.なかなか良さそうではないでしょうか?

今回はReactでやっていますが,Vueでもできると思います.Angularはコマンド一発でできるのでこんなことはしなくていいです.

どうでもいいのですが.明日卒論の目次案のゼミがあります.書くことが何もありません・・・・・NestJSの記事貼り付けようかな・・・・

これと関連している記事でTypeORMとwebpackという記事も書いているのでよかったらみてください naporitan.hatenablog.com