考虑使用Angular、NestJS和OpenAPI(Swagger)来构建微服务环境

以前,我曾经就Angular + NestJS + OpenAPI的环境写过文章。
难道MEAN堆栈已经过时了吗?通过Angular+Nest.js+OpenAPI来构建仅使用Typescript的环境。

然而在这种环境下,客户端和服务器端的代码结合在一个项目中,因此随着功能的增加,往往会变得复杂。
这次我们将考虑在构建单体仓库的同时,也着眼于微服务的架构。

(2020/11/06 更新)
我另外写了一篇关于简化构建Angular+NestJS单库环境的文章。

创造环境

形象

Untitled Diagram.png

项目结构

角度巢[Angular-Nest]
└ 套件[Packages]
└ 伺服器[Server]   ・・・ 使用 NestJS + OpenAPI 架設的 API 伺服器
└ 客戶端[Client]   ・・・ Angular 工作空間
└ 專案[Projects]
└ API 客戶端[API-Client]  ・・・ OpenAPI 客戶端函式庫
└ 客戶端1[Client1]   ・・・ Angular 專案1應用程式
└ 客戶端2[Client2]   ・・・ Angular 專案2應用程式

網址設計

我想在这个子目录中分支引用目标。

パス対象/apiNestJSのRestAPI/client1Angularプロジェクト1/client2Angularプロジェクト2

环境建设

创建Lerna项目

为了在一个项目中管理服务器/客户端,我们将引入适用于npm项目的Lerna工具,用于管理代码库。

按照公式指导生成Lerna项目。

mkdir angular-nest && cd angular-nest
npx lerna init

创建客户端项目

为了在微服务的前提下进行构建,客户端将进行多项目的构建。

前提- 在中文中只需要一个选项来改写以上内容:条件

@angular/cli導入済み

生成 Angular 的工作区

cd packages
ng new client --createApplication="false"

生成客户项目

client1とclient2を生成します

cd client
ng generate application client1 --routing --style=scss
ng generate application client2 --routing --style=scss

与传统的Angular项目不同,每个项目会在projects文件夹下生成。

为了能够区分开,对画面进行修正。

{{title}}
title = 'Hello Client1';
{{title}}
title = 'Hello Client2';

设置子目录

为了让/client1和/client2分别进行项目分支,我们需要修改Angular的设置。我们将添加baseHref和deployUrl。

    "client1": {
      ・・・
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "baseHref": "/client1/",
            "deployUrl": "/client1/",
          ・・・
    "client2": {
      ・・・
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "baseHref": "/client2/",
            "deployUrl": "/client2/",

我将建立。

npm run build client1
npm run build client2

创建服务器端项目

前提

条件

nestjs/cli導入済み

生成 NestJS 模板项目。

cd packages
nest new server

由于被问到使用npm还是yarn,这次我们选择使用npm来生成。

服务器和客户端的协作

这次我们先在一个服务器上运行,使用NestJS对Angular项目进行分配。
※如果要实际变成微服务,可以考虑使用Nginx等引用服务器进行分配。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // APIは/apiでアクセス
  app.setGlobalPrefix('api');

  // フロントの参照先を設定
  const clientPath = __dirname + '/../../client/dist';
  app.use(express.static(clientPath));
  // client1プロジェクトは/client1の時に参照
  app.use(/^\/client1.*$/, express.static(clientPath + '/client1/index.html'));
  // client2プロジェクトは/client2の時に参照
  app.use(/^\/client2.*$/, express.static(clientPath + '/client2/index.html'));

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

确认动作

因此,在这个阶段,我们已经建立了一个最小的环境,现在需要进行操作确认。
此外,我正在VirtualBOX和Vagrant的虚拟机上进行测试这个环境。

    サーバーを起動します
cd packages/server
npm run start
image.png

我能够正确地区分client1和client2,并且能够访问API!

引入OpenAPI

NestJS 的 OpenAPI 兼容性

公式の手順に従って、nestjs/swaggerをインストールします

npm install --save @nestjs/swagger swagger-ui-express

请在”packages/server”目录下执行。

    • main.ts修正

DocumentBuilderおよびSwaggerModuleでOpenAPIの設定をします

SwaggerModule.setupの第一引数はSwaggerUI表示用のURLのため、今回は/api/docsで表示できるようにします

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // APIは/apiでアクセス
  app.setGlobalPrefix('api');

  // OpenAPI(Swagger)設定
  const options = new DocumentBuilder()
    .setTitle('Cats example')
    .setDescription('The cats API description')
    .setVersion('1.0')
    .addTag('cats')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api/docs', app, document);

  // フロントの参照先を設定
  const clientPath = __dirname + '/../../client/dist';
  app.use(express.static(clientPath));
  // client1プロジェクトは/client1の時に参照
  app.use(/^\/client1.*$/, express.static(clientPath + '/client1/index.html'));
  // client2プロジェクトは/client2の時に参照
  app.use(/^\/client2.*$/, express.static(clientPath + '/client2/index.html'));

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

修改API。

    Dtoクラスを作成します。
import { ApiModelProperty } from '@nestjs/swagger';

export class HelloDto {
    @ApiModelProperty()
    message: string;
}
    • コントローラーで作成したDtoクラスを返すようにします

@ApiResponseでHelloDtoを型に指定します

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { HelloDto } from './dto/hello.dto';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';

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

    @Get()
    @ApiResponse({ status: 200, type: HelloDto })
    getHello(): HelloDto {
        const message = this.appService.getHello();
        return { message };
    }
}

SwaggerUI的检查

运行 npm run start 启动服务器,并访问 http://192.168.33.10:3000/api/docs。

image.png

SwaggerUI已经显示,并且HelloDto也被识别为模型。

另外,使用/api/docs-json将输出规范文件(json)的内容。

Angular端API客户端自动生成

为了在客户端共同使用,将其转化为库。

生成API客户端库

cd packages/client
ng generate library api-client
    ここはopenapi-generatorによる自動生成ファイル置き場となるため、ライブラリの中身を削除します
rm -rf projects/api-client/src/lib/*

使用 openapi-generator

由于openapi-generator是由Java编写的工具,所以要想正常运行它需要JRE,这样会很麻烦。因此我们选择使用docker方式。

因为每次输入命令都很麻烦,所以我会在package.json中注册脚本。【关于选项】

-i 先ほどのspecファイル表示URLを指定します

-g 出力形式(typescript-angular)を指定します。

-o docker内での出力ディレクトリを指定します

  "scripts": {
    ・・・
    "generate:api-client": "docker run --rm -v ${PWD}/projects/api-client/src/lib:/local/api-client openapitools/openapi-generator-cli generate -i http://192.168.33.10:3000/api/docs-json -g typescript-angular -o /local/api-client"
    • openapi-generatorを起動します

NestJSを起動させておき、/api/docs-jsonにアクセスできる状態にしてください

npm run generate:api-client
    openapi-generatorの出力内容に合わせてexport内容を変更します
/*
 * Public API Surface of api-client
 */

export * from './lib/index';

调用API

我将在每个Angular项目中尝试调用API。

导入ApiModule

在app.module.ts中引入HttpClientModule和ApiModule模块。
在basePath中指定API端的URL(/api)。
*由于本次在同一台服务器上运行,所以host:port不需要。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';
import { ApiModule, Configuration } from 'projects/api-client/src/lib';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,

    HttpClientModule,
    ApiModule.forRoot(() => new Configuration({basePath: '/api'})),
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

※客户2也是一样的

尝试调用API

    • コンポーネントでDefaultServiceをインジェクションし、onInitでAPIを呼んでみます

取得した値をタイトルに追記してます

import { Component, OnInit } from '@angular/core';
import { DefaultService, HelloDto } from 'projects/api-client/src/lib';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
    title = 'client1';

    constructor(private api: DefaultService) { }

    async ngOnInit() {
        const result: HelloDto = await this.api.rootGet().toPromise();
        this.title += ' ' + result.message;
    }
}
    openapi-generatorにより、APIクライアントおよびModelクラスが生成されているため、そのまま利用することができます

※客户2同样会进行修改。

确认行动

image.png

API 正确获取并显示了消息!!!

最后

只需要进行一些简单的修改,项目的分割就完成得很不错了。

如果在客户端创建共通组件作为库,那么就可以很容易地在项目之间共享,从而创建一个相当通用的环境。

如果从一开始就按照这种结构进行开发,实际上需要更多调整才能在微服务中运营,但我认为转换到微服务也会更容易。

广告
将在 10 秒后关闭
bannerAds