考虑使用Angular、NestJS和OpenAPI(Swagger)来构建微服务环境
以前,我曾经就Angular + NestJS + OpenAPI的环境写过文章。
难道MEAN堆栈已经过时了吗?通过Angular+Nest.js+OpenAPI来构建仅使用Typescript的环境。
然而在这种环境下,客户端和服务器端的代码结合在一个项目中,因此随着功能的增加,往往会变得复杂。
这次我们将考虑在构建单体仓库的同时,也着眼于微服务的架构。
(2020/11/06 更新)
我另外写了一篇关于简化构建Angular+NestJS单库环境的文章。
创造环境
形象
项目结构
角度巢[Angular-Nest]
└ 套件[Packages]
└ 伺服器[Server] ・・・ 使用 NestJS + OpenAPI 架設的 API 伺服器
└ 客戶端[Client] ・・・ Angular 工作空間
└ 專案[Projects]
└ API 客戶端[API-Client] ・・・ OpenAPI 客戶端函式庫
└ 客戶端1[Client1] ・・・ Angular 專案1應用程式
└ 客戶端2[Client2] ・・・ Angular 專案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
我能够正确地区分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。
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同样会进行修改。
确认行动
API 正确获取并显示了消息!!!
最后
只需要进行一些简单的修改,项目的分割就完成得很不错了。
如果在客户端创建共通组件作为库,那么就可以很容易地在项目之间共享,从而创建一个相当通用的环境。
如果从一开始就按照这种结构进行开发,实际上需要更多调整才能在微服务中运营,但我认为转换到微服务也会更容易。