なぽろぐ

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

学生ではなくなりました

SOELU株式会社に就職しました

corporate.soelu.com

やってること

Reactでこんな画面作ったりしてます.以下の2つは全て僕が実装したものです.

soelu.com

Image from Gyazo

soelu.com

Image from Gyazo

おわりに

コロナの外出自粛ムードが終わったらご飯でも行きましょう

欲しいものリストです.よければなにか・・・・!! Amazon 欲しいものリスト

aspidaとNestJSでHTTPClientにも型定義しながら開発するぞ!!!!!!!

aspidaとNestJSを使って気持ちがいい開発がしたい!したくない?

今回もお試しリポジトリが存在します.NestJS x aspida x Reactの環境で作っています.是非お試しください

github.com

なにするの?

NestJSはにはSwaggerModuleがありOpenAPIでしゃべることが可能になります.また, aspidaにはopenapi2aspidaというサブライブラリが存在します.この2つの組み合わせによりNestJS -> OpenAPI -> aspidaができるようになりNestJSでControllerを書くと自動的にHTTPClientの型定義が生成されるという感じになります.バックエンドを書くとフロントエンドに必要なコードが生成されるのは気持ちがいいですね.

OpenAPI経由でaspidaの設定ファイルが吐かれるのでNestJSに限らずいろいろは場面で使えるかと思います.今回はその一例としてNestJSを利用しています.

aspidaとは

github.com

HTTPClientに型をつけてくれるライブラリです.GETクエリ, Postパラメータはもちろん, Formデータにも型がつくらしい. しかもURIも保管してくれたりします.

使用感はこんな感じです

今回は /api/user/に問い合わせるとUser オブジェクトが返ってくるようなapiを作りました.

Image from Gyazo

導入方法

yarn add @aspida/axios axios

初期設定だと<root>/apis/以下にエンドポイントと同じ名前のディレクトリを切って設定ファイルを置いていくことになります.この設定ファイルを読んで<root>/apis/$api.tsが作成されます.

aspidaのビルド設定ファイルは公式を見てください aspida/packages/aspida at master · aspidajs/aspida · GitHub

例えば/api/userというエンドポイントが生えていたとすると,次のようにファイルを作って設定を書けばおkです

export interface Methods {
  get: {
    resBody: Types.User
  }
}

作成後に

yarn aspida --build

<root>/apis/$api.tsが作られ型定義されたHTTPClientを使用することができます.

aspidaの設定ファイルは必ずHTTPクライアントをラップする必要があるので,実際使用するときはこのようなutil関数を作っておくと楽だと思います.

import client from "apis/$api";
import aspida from "@aspida/axios";

export const api = client(aspida());

openapi2aspida

OpenAPI形式のjsonをHTTP通信形式で受け取り,aspidaの設定ファイルを自動で作ってくれます.めちゃくちゃ便利.

ルートにaspida.config.jsonを作っておき

module.exports = {
  input: "apis", // outputディレクトリ,
  openapi: { inputFile: "http://localhost:3000/swagger-json" }
}

でおkです.サーバを起動した状態で, openapi2aspida --buildでaspidaの設定ファイルが全て自動生成されます.便利すぎる・・・・

2回目移行ビルドするときはoutputディレクトリ丸ごと消しておく必要があります.

fatal: destination path 'apis' already exists and is not an empty directory.

NestJSについて

Node.jsにおいてバックエンドサーバといえば, Express一強みたいなところがありましたが,最近伸びているバックエンドフレームワークです.Angulerに近い書き心地で様々な機能を後からバシバシ入れることができるプログレッシブフレームワークになっています.Star数も22.5kとかなり多くなってきていて今注目のフレームワークって感じがしてきますね

github.com

今回はNestJSには深く触れず, SwaggerModuleを入れるところから始めていきたいと思います.

2019NestJSのアドベントカレンダーに少し参加したので興味のある人はこちらへ

SwaggerModuleを入れてOpenAPIを喋れるようにする

公式ドキュメントにSwaggerModuleの導入の仕方が書いてあるのでそちらを参考に進めていきます.

docs.nestjs.com

必要なモジュールを入れる

yarn add @nestjs/swagger swagger-ui-express

main.tsを次のように書き換える

import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix("api"); // frontendのrenderを/においているのでNestJSで定義される全てのControllerをapi/xxxにしておく

  // 公式とほとんど同じ
  const options = new DocumentBuilder()
    .setTitle("API description")
    .setVersion("1.0")
    .addServer("http://localhost:3000/")
    .build();

  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup("swagger", app, document);

  await app.listen(3000);
}
bootstrap();

ここでaddServerをやっておかないとopenapi2aspidaが実行できないので気をつけましょう.現在これは修正されています

この行で落ちます

 const options = new DocumentBuilder()
    .setTitle("API description")
    .setVersion("1.0")
    .addServer("http://localhost:3000/")
    .build();

これだけでNestJSがOpenAPIを喋れるようになりました.

yarn start:dev

をやってから

特に何もしていなければhttp://localhost:3000/swaggerでこのような画面を見れると思います. Image from Gyazo

また,http://localhost:3000/swagger-jsonでOpenAPIを喋っているのがJSON形式でわかります

なんの型が返ってくるかを明示するためにapp.controller.tsを次のように書き換えます

import { Controller, Get, Post, Body } from "@nestjs/common";
import { AppService } from "./app.service";
import { ApiTags, ApiCreatedResponse } from "@nestjs/swagger";

@ApiTags("AppController")
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @ApiCreatedResponse({ type: String }) // ここでstringが返ってくることを書いておく
  getHello(): string {
    return this.appService.getHello();
  }
}

これでopenapi2aspidaで型を自動生成する準備は万端です.実際にビルドして使用感を確かめてみましょう

NestJSから吐かれるOpenAPIからaspidaの設定ファイルを生成してみる

NestJSを次のコマンドで起動します

yarn start:dev

http://localhost:3000/swagger-jsonが生きてることを確認し,aspida.config.jsに以下の設定が書いてあれば準備はおkです

module.exports = {
  input: "apis",
  openapi: { inputFile: "http://localhost:3000/swagger-json" }
}

このコマンドを実行すれば自動生成されます

rm -rf apis
yarn openapi2aspida --build

たったこれだけで<root>/apis/$api.tsが生成されました. f:id:Naporitan:20200329084534p:plain

すごいですよね!!! あとはこれをフロントエンドのコードで使うだけ!OpenAPIをメンテしていく意味も出てくるし一石二鳥のライブラリだと思います.

まとめ

去年の12月ごろにも一度使用していたのですが,かなりのアップデートがかかっていてびっくりしました. naporitan.hatenablog.com

NestJSとaspidaを使えばすべてTSの世界でwebの世界を渡り歩けるようになりました.今実戦投入するプロジェクトにこの構成をとっていますが,いい感じです.

副作用として,バックエンドでAPIを書き換えるとき影響範囲がTSの型チェックで落ちたところを修正すればよくなるのでAPI変更の際の精神負荷も少し軽減される気もします.

バックエンドでAPIを作成するとすぐ型安全な状態でそのAPIにアクセスできると言うのは圧巻だと思います. aspida流行れ・・・・!!!

DIVでBlurっぽいことをやる(React)

DIVでもInput要素のblur的なことがしたい!

InputElementは別のところを触るとBlurイベントを発火してフォーカスが外れます.

developer.mozilla.org

MDNのサイトで動作は確認できるかと思います.

これをDIV Elementでもやりたいなぁと言うのが今回の議題です.

Image from Gyazo

今回はBlur的なことをしたくなったSelectorUIを用意しました.https://naporin0624.github.io/napoblog-assets/#/div-blur

  • 上のセレクターを出すボタンを押すとセレクターを表示する
  • 要素を選択するところを触っている時はセレクターを消さない
  • 背後の要素はスクロールできる
  • それ以外のところを触るとセレクターが閉じる

ようなUIを考えます.

方針

セレクターUIはdivの中に存在するようになっていて,次のようなDOM構成になっています.

  • ContainerDIV
    • ButtonDIV
    • MenuDIV
      • OptionGroupDIV
        • OptionDIV
        • OptionDIV
        • ...

ButtonDIVもOptionGroupDIVもOptionDIV触っても親の要素は常にContainerDIVになるので

developer.mozilla.org

こちらのclosestメソッドを使ってクリックされた要素の親にContainerがあるかどうかでフォーカスがはずれたかどうかを検知するようにしていきます!関係ないところをクリックしてもその親にはContainerはいないのでフォーカスは外れる仕組みです!

実装

こんな感じでできるかと思います!InputElementが自分でblurイベントを発火してくれるところを自分でclosestメソッドを使ってblurイベント相当のものを生成しています.

function Selector() {
  // closestにSelectorをわたすためにContainerのrefをとります
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const handler = (e: Event) => {
      // Blurを制御したい親Elementのselector(クラス)refからを作る
      const selector = ref.current?.className
        .split(" ")
        .map(s => `.${s}`)
        .join();

      // eはクリックしたところから発火されるイベント
      // eをclosestで比較し,eの親にrefが含まれるか確認する
      const blur = !(e.target as HTMLElement).closest(selector || "");

      // 親にrefが含まれていなければフォーカスを外す
      if (blur) setIsFocus(false);
    };

    document.addEventListener("click", handler);
    return () => document.addEventListener("click", handler);
  }, []);

  return (
    <Container ref={ref}>
      <Button></Button>
      <Menu>
        <OptionGroup>
          <Option>hoge</Option>
          <Option>huga</Option>
          <Option>nyan</Option>
        </OptionGroup>
      </Menu>
   </Container>
  )
}

ここなんですが

const selector = ref.current?.className
        .split(" ")
        .map(s => `.${s}`)
        .join();

javascriptだと

const selector = [...ref.current.classList].map(s => `.${s}`).join()

みたいなことができたんですけど,TSだと怒られましたw

最後に

今回使ったプログラムはここに置いてあります!必要であれば使ってください. github.com

自作のUI(DIVで作ったやつ)でフォーカス制御してみたいなことがあって困っていろいろ模索した結果こんな感じの解にたどり着きました.MDNじろじろ見る癖が少しづつついてきたかなぁって感想と,CSS筋が前よりついてきた気がします.

もっとこうしたらいいよとかあれば@naporin24690に教えてください〜!

LottieWebでチェックボックスの状態を管理する

Lottieで消えちゃうCheckBox作るぞ!

こういうのを作りたい

https://naporin0624.github.io/napoblog-assets/#/

Image from Gyazo

今回はこちらのチェックボックスアニメーションを使わせていただきました.

lottiefiles.com

コードの全体像

Lottieのマウントにはreact-lottieを使っています.animを直で触りたいのでそこの部分だけ型定義を拡張して使っている感じですね.

github.com

index.tsx

import React, { useRef, useEffect } from "react";
import Lottie from "react-lottie";
import checkbox from "./checkbox.json";
import { AnimationItem } from "lottie-web";

interface LottieType extends Lottie {
  anim: AnimationItem;
}
interface Props {
  checked: boolean;
}
const options = {
  loop: false,
  autoplay: false,
  animationData: checkbox,
  rendererSettings: {
    preserveAspectRatio: "xMidYMid slice",
  },
};
export const CheckBox = (props: Props) => {
  const { checked } = props;
  const ref = useRef<LottieType>(null);
  const prev = useRef<boolean>();

  useEffect(() => {
    // 初回マウントときはレンダリングしない
    // falseでは行ってきたときにgoToAndPlayされるのを防ぐため
    if (prev.current === undefined) {
      prev.current = checked;
      // 初回マウント かつ チェックボックスが選択されているならフレームの最後からアニメーションを実行する
      checked && ref.current?.anim.goToAndPlay(ref.current?.anim.getDuration(true), true);
      return;
    }

    if (checked) {
      ref.current?.anim.goToAndPlay(0, true);
    } else {
      ref.current?.anim.setDirection(-1);
      ref.current?.anim.goToAndPlay(15, true);
    }
  }, [checked]);
  return <Lottie ref={ref} options={options} width={300} />;
};

最後に

lottie-webにはここでフレームを止めるみたいなことができないぽいのでこのような措置を取りました.もっといい方法があれば教えてください🙇‍♂️

初めはこの子

lottiefiles.com

で実装していてチェックされてから消えるまでセットのアニメーションだったために以下のようにしていたのですが

  useEffect(() => {
    ref.current?.anim.playSegments([30, 75], true);
    ref.current?.anim.stop();
  }, []);

なぜかこうなる・・・・・・

Image from Gyazo

消すとうまくいくので明らかにplaySegmentが悪いのはわかっているんですけど,どう直したらいいのかわからなかったので上で紹介したアニメーションを使いました

有識者教えてください!!!!

わかっていること

  • destroyの前に動き始めている
    • useEffectのreturnにconsole.logをしこませて検証
  • playSegmentの上にconsole.logをおいても初期レンダリング以外では反応なし

ぐらいです・・・・・・

react-transition-groupでリストを表示するときはkeyにindexを設定するのはやめよう

リストで表示したものにトランジションを適用したい

こんなやつをやりたかった

Image from Gyazo

なんかこうなる

Image from Gyazo

🤔

コードを比較

styles.tsについては一番下に書いておきます.

変な挙動をするコード

import React, { Fragment, useState, useCallback, useMemo } from "react";
import { Container, AnimationBox, FormContainer, Input, Submit } from "./styled";

export const ListTransition = () => {
  const [state, setState] = useState<string[]>(["hoge", "huga"]);
  const [value, setValue] = useState<string>("");
  const onAdd = useCallback(() => {
    if (value.length <= 0) return;

    setState(ls => [...ls, value]);
    setValue("");
  }, [value]);

  const onRemove = useCallback((s: string) => setState(l => l.filter(el => el !== s)), []);
  const disabled = useMemo(() => value.length <= 0, [value]);
  return (
    <Fragment>
      <Container>
        {state.map((s, idx) => (
          <AnimationBox key={idx} unmountOnExit timeout={800} onClick={() => onRemove(s)}>
            {s}
          </AnimationBox>
        ))}
      </Container>
      <FormContainer>
        <Input value={value} onChange={e => setValue(e.target.value)} />
        <Submit onClick={onAdd} disabled={disabled}>
          Add Item
        </Submit>
      </FormContainer>
    </Fragment>
  );
};

想定している動きをするコード

import React, { Fragment, useState, useCallback, useMemo } from "react";
import { Container, AnimationBox, FormContainer, Input, Submit } from "./styled";

export const ListTransition = () => {
  const [state, setState] = useState<string[]>(["hoge", "huga"]);
  const [value, setValue] = useState<string>("");
  const onAdd = useCallback(() => {
    if (value.length <= 0) return;

    setState(ls => [...ls, value]);
    setValue("");
  }, [value]);

  const onRemove = useCallback((s: string) => setState(l => l.filter(el => el !== s)), []);
  const disabled = useMemo(() => value.length <= 0, [value]);
  return (
    <Fragment>
      <Container>
        {state.map(s => (
          <AnimationBox key={s} unmountOnExit timeout={800} onClick={() => onRemove(s)}>
            {s}
          </AnimationBox>
        ))}
      </Container>
      <FormContainer>
        <Input value={value} onChange={e => setValue(e.target.value)} />
        <Submit onClick={onAdd} disabled={disabled}>
          Add Item
        </Submit>
      </FormContainer>
    </Fragment>
  );
};

原因

Image from Gyazo

keyをindexにしていたからでした.keyが変わるので最後のコンポーネントが消えたと認識されてしまってトランジションが最後のコンポーネントにしか効かなかったというオチでした.

styles.ts

import styled from "styled-components";
import { TransitionGroup } from "react-transition-group";
import transition from "styled-transition-group";

export const Container = styled(TransitionGroup)`
  position: absolute;
  top: 0px;

  display: flex;
  width: 300px;
  overflow-x: scroll;
  margin: 12px;
`;

export const AnimationBox = transition.p`
  padding: 8px 16px;
  font-size: 13px;
  border: solid 1px #c1c1c1;
  margin: 0 8px;
  border-radius: 3px;
  background-color: #c1c1c1;
  color: white;
  cursor: pointer;

  &:enter {
    opacity: 0;
  }
  &:enter-active {
    opacity: 1;
    transition: all 0.8s ease-out;
  }
  &:exit {
    opacity: 1;
  }
  &:exit-active {
    opacity: 0;
    transition: all 0.8s ease-out;
  }
`;

export const FormContainer = styled.div`
  display: flex;
  margin-top: 56px;
`;
export const Input = styled.input``;
export const Submit = styled.button``;

2020年なぽりたん

2020年

Twitterが面白くないと言われたので2020年は面白いツイートを心がけます

みんな頑張って僕を面白くしてください!

まじめなやつ

  • アウトプット中心にする
    • webアプリをたくさん作っていきたい
  • 就職するので,来年末には社内で1年で1番ページ作った人間になりたい
  • OSSにコミットしたい
    • 今までは全くやったことがない
  • クリエイターになれるような活動をする
  • 遅刻を少なくする
  • 人みたいな生活をする

終わりに

これを書いているMacBookProのバッテリー残量が2%.本当に最後の抱負の人みたいな生活ができるかどうか怪しい.あと,大江戸温泉からコミケにいくのは凄まじくしんどいのでやめよう

【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