Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

NestJS でサーバを起動せずに OpenAPI の仕様書を取得する

はじめに

この記事は モバイルファクトリー Advent Calendar 2019 の18日目の記事です。
こんにちは、ブロックチェーンチームのエンジニアの id:odan3240 です。

NestJS では @nestjs/swagger を用いることで、コントローラーの定義から OpenAPI (swagger) の仕様書を生成することができます。 このモジュールの使い方などは、先日の NestJS Advent Calendar 2019 に記事を投稿しましたので、そちらをご覧ください。

qiita.com

上のリンクの記事では、JSON 形式の OpenAPI の仕様書を取得する方法として http://localhost:3000/api-json にアクセスする方法を紹介しています。しかし、実際にアプリケーションを開発していく上でこの方法では CI のワークフローに組み込みにくいなどの問題があります。
この記事ではこの問題を解決する方法を紹介します。

問題の詳細

http://localhost:3000/api-json にアクセスする方法には、「バックエンドのサーバは起動している」という前提条件が必要になります。
この前提条件はかなり強い条件です。例えば MySQL などのデータストアを利用しているバックエンドのサーバを考えます。このサーバの起動時にデータストアにコネクションを張る設定をしていると、データストアの存在が前提条件に追加されます。つまり、バックエンドのサーバの他にデータストアも起動しておかないと http://localhost:3000/api-json にアクセスできず仕様書を取得することができません。
CI で OpenAPI の仕様書を出力してクライアントのコードを自動生成を行うワークフローを組んでいる場合、CI にデータストアを用意する必要があるため、手間がかかります。

解決方法

@nestjs/swagger は Controller の実装の定義から OpenAPI の仕様書を生成するモジュールですので、原理的にはサーバの起動なしに Controller のソースコードから仕様書を生成できるはずです。この記事で紹介する解決方法は、NestJS の DI 機構を用いて、これを実現します。

@nestjs/testing には Test.createTestingModule という関数があります。これを使うとクラスのモックを簡単に差し込むことができます。

Testing | NestJS - A progressive Node.js web framework

具体的なコードは サンプルリポジトリsrc/openapi/generate.ts にあります。

ポイントは次の通りです。

  • Reflect.getMetadata を使って AppModule の Controller と Provider を取得する
    • これらを createTestingModule に引数に渡す
      • Provider は useValue でモックする
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { Test } from '@nestjs/testing';
import { AppModule } from '../app.module';

async function bootstrap(): Promise<void> {
  const controllers = Reflect.getMetadata('controllers', AppModule);
  const providers = Reflect.getMetadata('providers', AppModule);
  const mockedProviders = providers.map(provider => {
    return {
      provide: provider.name,
      useValue: {},
    };
  });

  const testingModule = await Test.createTestingModule({
    controllers: controllers,
    providers: mockedProviders,
  }).compile();

  const app = await testingModule.createNestApplication();

  const options = new DocumentBuilder()
    .setTitle('アドベントカレンダーサンプル')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  console.error(JSON.stringify(document, null, 2));
}
bootstrap();

このファイルを ts-node で実行すると JSON 形式の OpenAPI の仕様書を取得することができます。

$ yarn ts-node src/openapi/generate.ts > /dev/null
{
  "openapi": "3.0.0",
  "info": {
    "title": "アドベントカレンダーサンプル",
    "description": "",
    "version": "1.0.0",
    "contact": {}
  },
  "tags": [],
  "servers": [],
  "components": {},
  "paths": {
    "/": {
      "get": {
        "operationId": "getHello",
        "responses": {
          "200": {
            "description": ""
          }
        }
      }
    }
  }
}

終わりに

NestJS の DI 機構を用いて Controller が依存するクラスをモックすることで、バックエンドのサーバを起動せずに OpenAPI の仕様書を取得する方法を紹介しました。これにより、データストアなどの他のミドルウェアに対する依存がなくなったため、CI のワークフローに組み込みやすくなりました。

生 Canvas を触って分かったこと

こんにちは、モバイルファクトリー Advent Calendar 2019 17日目担当の yunagi_n です。

さて、2019年も暮れになった今、 PixiJSKonva など、便利な Canvas フレームワークがありますが、
このたび初めて生の Canvas (Context2D) を触ることになりまして、
動作確認中などで気がついた点などがいくつかあったので紹介します。
基本的にはドキュメントをしっかり読めば良いのですが、普段は気がつかない部分もありましたので、記事にしました。
これから生の Canvas を触るぞ!という方の参考になれば嬉しいです。

requestAnimationFrame の実行頻度は端末依存

Canvas でアニメーションをさせたい場合、 Window.requestAnimationFrame を使ってループを実装します。
ここで指定するコールバック関数は一般的には秒間60回呼ばれるのですが、ここでドキュメントを引用すると

このコールバックの回数は、たいてい毎秒 60 回ですが、一般的に多くのブラウザーでは W3C の勧告に従って、ディスプレイのリフレッシュレートに合わせて行われます。

とあるように、実際には端末のリフレッシュレートに依存しています。
そのため、1秒間に60回程度呼ばれる前提で処理を記述すると、一部のハイエンド端末では、
倍速再生されてしまう、という事態が発生します。

実際に社内の一部の端末では倍速に再生されてしまうという事態が発生していました。
最近では、ハイエンド端末ではリフレッシュレートが 120Hz ということも度々ありますので、 気をつける必要があります。

高解像度ディスプレイへの対応

Canvas で描画したものは、一般的な画像と同じように描画されます。
つまりは、こちらも画像と同様に、 Retina / HiDPI などのような高解像度ディスプレイへの対応が必要になります。
対応方法としては、 CSS などでの指定サイズに対して2倍のサイズで予め Canvas を生成するか、
Window.devicePixelRatio を使用して、動的に描画サイズを変更する方法があります。
なお、後者の方法では一部の Android 端末で落ちるバグがあるようなので、注意が必要です。

Canvas の解放

Canvas で使用した画像リソースや OffScreen Canvas などは、描画していた DOM を消し飛ばしただけでは行われません。
例えば、 Vue の v-if などで何度も表示・非表示を繰り返した場合、
使用してたメモリリソースにも寄りますが数回程度繰り返しただけでメモリリークが発生してしまいます。

メモリ解放は下のようにすることで出来ます。

// Context2D を作成するとき
const context = canvas.getContext("2d", { storage: "discardable" });

// Canvas を破棄するとき
delete context;

canvas.height = 0;
canvas.width = 0;
canvas.remove();
delete canvas;

まとめ

  • requestAnimationFrame は (最近の端末では特に) 秒間60回呼ばれるわけでは無いので気をつけよう
  • 高解像度ディスプレイへの対応は画像同様に行おう
  • 使用したリソースはしっかり解放しよう

でした。
特に最後のメモリ解放については、普段使っている分にはなかなか気がつかないので、お気を付けください。