用Angular+NestJS+OpenAPI构建一个只使用Typescript的环境,MEAN Stack已经过时了吗?
MEAN堆栈(MongoDB+Express.js+Angular+Node.js)很棒对吧?
但是,因为Express.js是用JavaScript编写的,所以可能会对与Angular之间的差距感到困惑,不是吗?
为了解决这个问题,我将引入NestJS。
NestJS是基于Express.js开发的,受到Angular的启发,因此可以用类似Angular的代码编写。
如果有使用过MEAN堆栈的人,我认为他们可以轻松地使用这个框架。
这次我们将从零开始构建一个类似于MEAN堆栈的环境,将Angular和NestJS整合到一个项目中。
另外,我们还将使用OpenAPI实现客户端与服务器之间的类型安全访问。
(追記)
本文介绍了如何构建单体环境。
为此,在package.json中混合使用了客户端/服务器的包,使得管理变得复杂。
如果您想要在一个项目中独立地管理Angular和NestJS,请参考以下文章。
通过使用Lerna,在Angular+Nest.js的单体项目中进行客户端/服务器的代码分割。
我已经新增了一篇关于如何在简化环境下创建Angular+NestJS Monorepo的文章。
前提 (Qian ti)
-
- Node.jsインストール済み
-
- nestjs/cliインストール済み
- angular/cliインストール済み
生成模板项目
生成服务器端(NestJS)的模板项目。
使用嵌套的 new 命令生成模板项目。
nest new angular-nest
在途中可能会问你使用npm还是yarn,这次我们就先选择npm。
生成客户端(Angular)的模板项目。
用ng new命令生成一个模板项目。
ng new client --routing --style=scss
合并客户端和服务器的代码。
服务器端的编辑
重命名src目录
- クライアントサイドのコードをsrcにしたいので、サーバーサイドのコードはapiディレクトリに移動します。
mv ./angular-nest/src ./angular-nest/api
修改nest-cli.json。
sourceRootの値をapiに変更します
{
"collection": "@nestjs/schematics",
"sourceRoot": "api"
}
修改构建设置
修改tsconfig
tsconfig.jsonのcompilerOptionsをtsconfig.build.jsonに移動します。
※tsconfig.jsonはAngularと共通になるため
outDirを./dist/serverに変更します
includeでapiディレクトリ下のtsファイルを指定
excludeでsrcを対象外に指定
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist/server",
"baseUrl": ".",
"incremental": true
},
"include": ["api/**/*.ts"],
"exclude": ["node_modules", "dist", "test", "src"]
}
- この時点ではtsconfig.jsonはexcludeだけとなります
{
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
更改启动设置
package.jsonのstart:prodスクリプトの実行ファイルをdist/server/mainに変更します
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/server/main",
"lint": "tslint -p tsconfig.json -c tslint.json",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
你能开机吗?
- この時点で一旦起動できるか確認しておきましょう
npm run build
npm run start:prod

客户端编辑
将src目录移动。
mv client/src angular-nest/src
修改tsconfig文件
tsconfig.jsonの内容をtsconfig.app.jsonに持ってきます
{
"extends": "./tsconfig.json",
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.ts"
],
"exclude": [
"src/test.ts",
"src/**/*.spec.ts"
]
}
将所需文件移动
mv client/angular.json angular-nest/
mv client/browserslist angular-nest/
mv client/tsconfig.app.json angular-nest/
合并package.json
-
- 依存パッケージ情報のマージ
@angular/**系とtslib、zone.jsを持っていきます
"dependencies": {
"@angular/animations": "~8.2.13",
"@angular/common": "~8.2.13",
"@angular/compiler": "~8.2.13",
"@angular/core": "~8.2.13",
"@angular/forms": "~8.2.13",
"@angular/platform-browser": "~8.2.13",
"@angular/platform-browser-dynamic": "~8.2.13",
"@angular/router": "~8.2.13",
・・・
"tslib": "^1.10.0",
"zone.js": "~0.9.1"
},
"devDependencies": {
・・・
"typescript": "~3.5.3"
-
- スクリプトのマージ
buildをbuild:serverにリネーム
build:clientを追加
buildを追加
"scripts": {
"prebuild": "rimraf dist",
"build:client": "ng build --prod",
"build:server": "nest build",
"build": "npm run build:client && npm run build:server",
确认建筑
npm install
npm run build
只要在dist目录下有server和client就可以了
启用Angular的路由
api/main.tsを修正し、サーバーとクライアントそれぞれのルーティングが通るようにします
/api => サーバーサイド
/api以外 => クライアントサイド
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// add start
import * as express from 'express';
// add end
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// add start
// サーバーサイドのルーティングを/apiで始まるURLのみに適用
app.setGlobalPrefix('api');
// /apiから始まらないURLの場合はクライアントサイドのルーティングを適用
const clientPath = __dirname + '/../client';
app.use(express.static(clientPath));
app.use(/^(?!\/api).*$/, express.static(clientPath + '/index.html'));
// add end
await app.listen(3000);
}
bootstrap();
确认路由
构建和启动
npm install
npm run build
npm run start:prod
服务器端的检查。
http://{ホスト}:3000/apiにアクセスします

将会显示「你好,世界」!
客户端确认
http://{ホスト}:3000にアクセスします

显示出Angular的界面!
在服务器端引入OpenAPI
根据参考文献逐步引入
引入
- 必要なパッケージのインストール
npm install --save @nestjs/swagger swagger-ui-express
对main.ts进行修改
- SwaggerUIの表示URLは/api/docsにしておきます
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// add start
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import * as express from 'express';
// add end
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// add start
// サーバーサイドのルーティングを/apiで始まるURLのみに適用
app.setGlobalPrefix('api');
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);
// /apiから始まらないURLの場合はクライアントサイドのルーティングを適用
const clientPath = __dirname + '/../client';
app.use(express.static(clientPath));
app.use(/^(?!\/api).*$/, express.static(clientPath + '/index.html'));
// add end
await app.listen(3000);
}
bootstrap();
重新启动
npm run build:server
npm run start:prod
确定
http://{ホスト}:3000/api/docsにアクセスします

SwaggerUI已成功显示!
- ちなみにhttp://{ホスト}:3000/api/docs-jsonにアクセスすると、specファイルの中身を出力することができます。

自动生成API客户端
使用OpenAPI Generator,可以从spec文件自动生成API客户端。
引入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}/src/app/api-client:/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"
}
Angular的说明在这里写着。
在服务器端至少创建一个DTO文件。
当生成Angular模块时,会生成一个Model。如果没有Model,会导致Angular构建错误,因此需要创建DTO文件。
import { ApiModelProperty } from "@nestjs/swagger";
export class HelloDto {
@ApiModelProperty()
message: string;
}
- controllerの@ApiResponseで型を指定することで、SwaggerUIでModelとして認識されるようになります。
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { HelloDto } from './dto/hello.dto';
import { 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 };
}
}
- サーバーサイドをビルドしなおしておきます
npm run build:server
生成API客户端
-
- ターミナルを2つ開き、片方のターミナルでnpm run start:prodでサーバーを起動します
- もう片方のターミナルで以下を実行します
npm run generate:api-client
API客户端代码将在src/app/api-client生成。
API调用
导入模块
-
- app.module.tsでApiModuleをインポートします
basePathを指定します。
クライアントとサーバーが同一サーバー内なので、ホストやポートは特に指定せず、/apiだけにします
HttpClientModuleも必要になるので、忘れずにインポートします
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ApiModule, Configuration } from './api-client';
import { HttpClientModule } from '@angular/common/http';
export function apiConfigFactory(): Configuration {
return new Configuration({ basePath: '/api' });
}
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
ApiModule.forRoot(apiConfigFactory),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
将DefaultService通过DI进行注入
-
- constructorでDefaultServiceをDIします
- 好きなタイミングでAPIクライアントを使ってAPIを呼び出します
import { Component } from '@angular/core';
import { DefaultService } from './api-client';
import { HelloDto } from './api-client/model/helloDto';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'client';
constructor(private api: DefaultService) {}
async onClickTest() {
const result: HelloDto = await this.api.rootGet().toPromise();
this.title = result.message;
}
}
this.api.rootGetが自動生成されたAPIクライアントです
この関数を呼ぶだけで対象のAPIがコールされます
サーバーサイドで作成したHelloDtoもAPIクライアントと同時に生成されているため、クライアントサイドでも使用することができます。
<button (click)="onClickTest()">test</button>
确认行动
- まずは普通にhttp://{ホスト}:3000にアクセスします

- ボタンを押下します

从服务器获取的值已更新!
最终
构建虽然有点麻烦,但一旦建好,就可以保证类型安全,并且可以自动生成API客户端,所以不需要担心类型方面的问题了。
我在以下网址上放置了我这次创建的东西:
https://github.com/teracy55/angular-nest
填补
如果按照目前这样开发下去的话,会很困难,所以我认为可以加入并发性,并调整为服务器和客户端以观察模式构建。