用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
image.png

客户端编辑

将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にアクセスします

image.png

将会显示「你好,世界」!

客户端确认

http://{ホスト}:3000にアクセスします

image.png

显示出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にアクセスします

image.png

SwaggerUI已成功显示!

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

自动生成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にアクセスします
image.png
    ボタンを押下します
image.png

从服务器获取的值已更新!

最终

构建虽然有点麻烦,但一旦建好,就可以保证类型安全,并且可以自动生成API客户端,所以不需要担心类型方面的问题了。

我在以下网址上放置了我这次创建的东西:
https://github.com/teracy55/angular-nest

填补

如果按照目前这样开发下去的话,会很困难,所以我认为可以加入并发性,并调整为服务器和客户端以观察模式构建。

bannerAds