天天看點

NestJS 快速入門,Node 架構的新選擇!

作者:CipherY

NestJS快速入門

NestJS 是一個基于 TypeScript 的開發架構,它建立在 Express、Fastify 和其他 Node.js Web 架構之上,提供了一種用于建構高效、可擴充的伺服器端應用程式的方式。相比于傳統的 Express 和 Koa 架構,NestJS 提供了更多的功能和抽象層,使得開發者可以更輕松地建構複雜的應用程式。

NestJS、Koa 和 Express 的一些對比:

  • 架構模式:NestJS 使用了基于子產品化的架構模式,它将應用程式劃分為子產品、控制器和提供者。這種模式使得代碼的組織和維護更加容易。而 Koa 和 Express 則沒有明确的架構模式,開發者需要自己組織代碼結構。
  • TypeScript 支援:NestJS 是一個完全基于 TypeScript 的架構,它提供了強大的靜态類型檢查和面向對象程式設計的能力。而 Koa 和 Express 都是基于 JavaScript 的架構,雖然可以使用 TypeScript,但是缺乏一些 TypeScript 特有的功能和類型檢查。
  • 中間件支援:Koa 和 Express 都有強大的中間件支援,可以友善地添加和進行中間件。NestJS 也支援中間件,但是它使用了自己的裝飾器和管道機制來進行中間件,使得中間件的使用更加靈活。
  • 異步程式設計:NestJS 提供了強大的異步程式設計支援,包括異步子產品、異步提供者和異步管道等。這使得開發者可以更好地處理異步操作,如資料庫查詢、網絡請求等。Koa 和 Express 也支援異步程式設計,但是相對于 NestJS 來說,它們的異步支援相對較弱。

總體而言,NestJS 提供了更多的功能和抽象層,使得開發者可以更輕松地建構複雜的應用程式。如果你喜歡使用 TypeScript、需要更強大的架構模式和異步程式設計支援,那麼 NestJS 是一個很好的選擇。如果你對簡潔性和靈活性更加看重,那麼 Koa 和 Express 也是不錯的選擇。最終選擇哪個架構還要根據具體的項目需求和個人偏好來決定。

NestJS 環境搭建和項目建立

bash複制代碼# node 官網檢視安裝方法 https://nodejs.org

# 全局安裝NestJS CLI
npm i -g @nestjs/cli

# 建立NestJS項目
nest new nest-best
           

目錄說明

bash複制代碼├── dist                        # 編譯後的目錄,打包産物
├── node_modules                # 項目依賴包
├── REST_Client                 # 結合 vs code 插件 REST Client 調試接口示例
├── src                         # 源碼目錄
│   ├── app.controller.spec.ts  # 對于基本控制器的單元測試樣例
│   ├── app.controller.ts       # 控制器檔案,可以簡單了解為路由檔案
│   ├── app.module.ts           # 子產品檔案,在NestJS世界裡主要操作的就是子產品
│   ├── app.service.ts          # 服務檔案,提供的服務檔案,業務邏輯編寫在這裡
│   └── main.ts                 # 項目的入口檔案,裡邊包括項目的主子產品和監聽端口号
├── test                        # 測試檔案目錄,對項目測試時使用的目錄,比如單元測試...
│   ├── app.e2e-spec.ts         # e2e測試,端對端測試檔案,測試流程和功能使用
│   └── jest-e2e.json           # jest 配置檔案
├── .eslintrc.js                # eslint 配置
├── .gitignore                  # git忽略檔案
├── .prettierrc                 # prettier 配置
├── nest-cli.json               # 整個項目的配置檔案,這個需要根據項目進行不同的配置
├── package.json                # 項目依賴包管理檔案和Script檔案,比如如何啟動項目的指令
├── tsconfig.build.json         # TypeScript文法建構時的配置檔案
├── tsconfig.json               # TypeScript的配置檔案,控制TypeScript編譯器的一些行為
└── README.md                   # 文檔說明

           

vscode 插件 【REST Client】

bash複制代碼# http 示例目錄
REST_Client
           

基礎指令

開始

bash複制代碼# development
$ npm run start

# watch mode
$ npm run start:dev

# production mode
$ npm run start:prod
           

測試

bash複制代碼# unit tests
$ npm run test

# e2e tests
$ npm run test:e2e

# test coverage
$ npm run test:cov
           

部署

1、項目根目錄建立 Dockerfile

Dockerfile複制代碼# 使用Node.js作為基礎鏡像
FROM node:20-alpine3.18

# 設定工作目錄
WORKDIR /app

# 複制package.json和package-lock.json檔案到工作目錄
COPY package*.json ./

# npm 源,選用國内鏡像源以提高下載下傳速度
RUN npm config set registry https://registry.npm.taobao.org/

# 安裝依賴
RUN npm install

# 複制應用程式的源代碼到工作目錄
COPY . .

# 暴露應用程式的端口
EXPOSE 3000

# 啟動應用程式
CMD ["npm", "run", "start:prod"]
           
bash複制代碼# 2、導航到您的NestJS應用程式的根目錄,并運作以下指令來建構Docker鏡像:
docker build -t nest-best .

# 3、使用以下指令在Docker容器中運作您的NestJS應用程式:
docker run -d -p 3000:3000 nest-best -name nest-best

# docker run: 運作一個新的容器。
# -d: 在背景運作容器。
# -p 3000:3000: 将容器的端口3000映射到主機的端口3000,這樣可以通過主機的端口通路容器中運作的應用程式。
# nest-best: 要運作的容器的名稱或鏡像。
# -name nest-best: 設定容器的名稱為"nest-best"。
           

http://81.71.98.176:3000/ ,看到 Hello World!,說明部署成功。

入門

建立一個 book 子產品和路由

bash複制代碼nest g module book

# 用指令行建立 controller
nest g controller book --no-spec

# 用指令行建立 service
nest g service book --no-spec
           

Get、Post、Request、Query、Body、Param、Headers 修飾器

ORM - 操作資料庫

ORM:定義一個對象,這個對象就對應着一張表,這個對象的一個執行個體,就對應着表中的一條記錄。

docker 安裝 mysql

bash複制代碼# 安裝 https://www.runoob.com/docker/docker-install-mysql.html

# 拉取最新鏡像
docker pull mysql:latest

# 運作容器
# MYSQL_ROOT_PASSWORD=123456:設定 MySQL 服務 root 使用者的密碼。
docker run -itd --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql

# 修改容器時區
# 進入容器
docker exec -u 0 -it 容器ID bash
# 執行
mv /etc/localtime /etc/localtime.bak && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
           

TypeORM

bash複制代碼# 安裝
npm install --save @nestjs/typeorm typeorm mysql2
           

我們直接再/src/app.module.ts 中引入 typeorm。

ts複制代碼import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql', // 資料庫類型
      host: '81.71.98.176', // 資料庫的連接配接位址 host
      port: 3306, // 資料庫的端口 3306
      username: 'root',
      password: '******',
      database: 'test', // 連接配接的資料庫
      retryDelay: 500, // 重試連接配接資料庫間隔
      retryAttempts: 10, // 允許重連次數
      synchronize: true, // 是否将實體同步到資料庫
      autoLoadEntities: true, // 自動加載實體配置,forFeature()注冊的每個實體都自己動加載
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
           

編寫 Entities 實體

/src/book/ectities/book.entity.ts

ts複制代碼import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
} from 'typeorm';

@Entity()
export class Book {
  @PrimaryGeneratedColumn('uuid')
  id: number;

  @Column({ type: 'varchar', length: 255 })
  name: string;

  @Column({ type: 'varchar' })
  description: string;

  @CreateDateColumn({ type: 'timestamp' })
  createTime: Date;
}
           

編寫好類之後,需要在 book.module.ts 裡進行引入

ts複制代碼import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Book } from './entities/book.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Book])],
  ...
})
export class BookModule {}
           

對資料庫的增删改查 book.service.ts

ts複制代碼// 依賴導入
import { Injectable } from '@nestjs/common';
import { Like, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Book } from './entities/book.entity';
           

引入後我們要在構造函數裡增加一個依賴注入的操作。

ts複制代碼export class BookService {
  // 依賴注入
  constructor(
    @InjectRepository(Book) private readonly book: Repository<Book>,
  ) {}
  ...
}
           

完整增删改查示例

ts複制代碼// book.service.ts
import { Injectable } from '@nestjs/common';
import { Like, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Book } from './entities/book.entity';

@Injectable()
export class BookService {
  // 依賴注入
  constructor(
    @InjectRepository(Book) private readonly book: Repository<Book>,
  ) {}

  async getBooks() {
    const data = await this.book.find();
    return {
      code: 200,
      data: data,
      msg: 'success',
    };
  }
  async addBook(body: any) {
    const book = new Book();
    const { name, description } = body;
    book.name = name;
    book.description = description;
    const data = await this.book.save(book);
    return {
      code: 200,
      data: data,
      msg: '添加書籍成功',
    };
  }
  async deleteBook(id: number) {
    await this.book.delete(id);
    return {
      code: 200,
      data: null,
      msg: '删除書籍成功',
    };
  }
  async updateBook(id: number, body: any) {
    const book = new Book();
    const { name, description } = body;
    book.name = name;
    book.description = description;
    await this.book.update(id, book);
    return {
      code: 200,
      data: null,
      msg: '更新書籍成功',
    };
  }
  async getBookByName(name: string) {
    const data = await this.book.find({
      where: {
        name: Like(`%${name}%`),
      },
    });
    return {
      code: 200,
      data: data,
      msg: 'success',
    };
  }
  async getBookById(id: any) {
    const data = await this.book.findOneBy({
      id,
    });
    return {
      code: 200,
      data: data,
      msg: 'success',
    };
  }
}
           
ts複制代碼// book.controller.ts
import {
  Controller,
  Get,
  Post,
  Request,
  Query,
  Body,
  Param,
  Headers,
} from '@nestjs/common';
import { BookService } from './book.service';

@Controller('book')
export class BookController {
  constructor(private readonly bookService: BookService) {}

  @Get()
  getBooks(): any {
    return this.bookService.getBooks();
  }

  @Post('/add')
  addBook(@Body() body): any {
    return this.bookService.addBook(body);
  }

  @Get('/delete/:id')
  deleteBook(@Param() param): any {
    return this.bookService.deleteBook(param.id);
  }

  @Post('/update/:id')
  updateBook(@Param() param, @Body() body): any {
    return this.bookService.updateBook(param.id, body);
  }

  @Post('/getBookByName')
  getBookByName(@Body() body): any {
    console.log('body.name', body.name);
    return this.bookService.getBookByName(body.name);
  }

  @Get('/getBookById')
  getBookById(@Request() req): any {
    // 因為通過Get方式傳遞過來的是字元串,所有我們需要用parseInt轉化為數字
    const id: number = parseInt(req.query.id);
    return this.bookService.getBookById(id);
  }
}
           

Providers(提供者)實作依賴注入

自定義注入值

ts複制代碼// book.module.ts 注入
providers: [
  {
    provide: 'Category',
    useValue: ['武俠', '修仙', '都市'],
  },
],
// book.controller.ts 使用
constructor(@Inject('Category') private categorys: string[]) {}

@Get('/category')
getCategorys(): string[] {
  return this.categorys;
}

// test
http://localhost:3000/book/category
           

自定義工廠(方法)

ts複制代碼// book.module.ts 注入
providers: [
  ...,
  {
    provide: 'MyFactory',
    useFactory() {
      return '自定義工廠(方法)';
    },
  },
],

// book.controller.ts 使用
constructor(
  ...,
  @Inject('MyFactory') private myFactory: string,
) {}

@Get('/category')
getCategorys(): string[] {
  console.log(this.myFactory);
  return this.categorys;
}
           

增加熱重載功能

bash複制代碼# 安裝依賴
npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack @types/webpack-env
           
js複制代碼// 項目根目錄下增加一個webpack-hmr.config.js的配置檔案
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

module.exports = function (options, webpack) {
  return {
    ...options,
    entry: ['webpack/hot/poll?100', options.entry],
    externals: [
      nodeExternals({
        allowlist: ['webpack/hot/poll?100'],
      }),
    ],
    plugins: [
      ...options.plugins,
      new webpack.HotModuleReplacementPlugin(),
      new webpack.WatchIgnorePlugin({
        paths: [/\.js$/, /\.d\.ts$/],
      }),
      new RunScriptWebpackPlugin({
        name: options.output.filename,
        autoRestart: false,
      }),
    ],
  };
};
           
bash複制代碼# 替換啟動腳本
"start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch",

# 重新啟動項目
npm run start:dev
           

然後在/src/book/book.controller.ts 檔案裡增加一個 hotLoad() 方法測試。

ts複制代碼@Get('/hotLoad')
hotLoad(): any {
  return 'HotLoad Function';
}
           

中間件 Middleware

局部中間件

bash複制代碼# 指令生成
nest g mi counter

# CREATE src/counter/counter.middleware.spec.ts
# CREATE src/counter/counter.middleware.ts
           
ts複制代碼// counter.middleware.ts 列印一下
console.log('進入中間件...');
next(); // 必須調用 next() 方法,否則請求無法繼續
           
ts複制代碼// book.module.ts使用
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { CounterMiddleware } from '../counter/counter.middleware';

export class BookModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(CounterMiddleware).forRoutes('book');
  }
}
           

發起請求 http://localhost:3000/book/list ,控制台看到輸出,說明中間件起作用了。

全局中間件

局部中間件我們是以實作 NestModule 接口的形式,把中間件挂在到 Moudle 上的.而全局中間件要以 function 的形式 編寫到 main.ts 裡。

ts複制代碼import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

// 定義全局中間件方法
function MiddleWareAll(req: any, res: any, next: any) {
  console.log('我是全局中間件.....');
  next();
}

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 使用全局中間件
  app.use(MiddleWareAll);

  await app.listen(3000);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}
bootstrap();
           

發起請求 http://localhost:3000/hi ,控制台看到輸出,說明中間件起作用了。

中間件 Middleware-用第三方中間件實作跨域

NestJS 是自帶配置項的app.enableCors(); (main.ts),可以通過簡單的配置完成跨域操作。這裡主要是學習一下 use 第三方中間件。

bash複制代碼# 安裝依賴
npm install cors
npm install @types/cors -D
           
ts複制代碼// app.controller.ts
@Get('/corstest')
corsTest(): object {
  return { message: '測試跨域請求成功' };
}

// main.ts
import * as cors from 'cors';

app.use(cors());
           

打開 www.baidu.com, f12 控制台

ts複制代碼// 設定跨域前後都調用一下
fetch('http://localhost:3000/book/list')
  .then((res) => res.json())
  .then((res) => {
    console.log(res);
  });
           

子產品 Module

子產品之間互相調用(共享)

bash複制代碼# 建立 book2 子產品
nest g module book2
# 用指令行建立 controller
nest g controller book2 --no-spec
# 用指令行建立 service
nest g service book2 --no-spec
           
ts複制代碼// book2.service.ts 增加 getBook2 方法
import { Injectable } from '@nestjs/common';

@Injectable()
export class Book2Service {
  async getBook2() {
    return {
      code: 200,
      data: '調用 Book2Service 方法成功',
      msg: 'success',
    };
  }
}
           
ts複制代碼// book2.module.ts 導出 Book2Service
import { Module } from '@nestjs/common';
import { Book2Controller } from './book2.controller';
import { Book2Service } from './book2.service';

@Module({
  controllers: [Book2Controller],
  providers: [Book2Service],
  exports: [Book2Service],
})
export class Book2Module {}
           
ts複制代碼// 在 book.module.ts 引入 Book2Service
// 然後注入 providers
import { Book2Service } from '../book2/book2.service';

@Module({
  providers: [
    Book2Service,
    ...
  ],
})
           
ts複制代碼// 最後在 book.controller.ts 進行引入使用
import { Book2Service } from 'src/book2/book2.service';

@Controller('book')
export class BookController {
  ...

  // 調用 book2Service 方法
  @Get('/getBook2')
  getBook2(): any {
    return this.book2Service.getBook2();
  }
}

           

調用 http://localhost:3000/book/getBook2,看到結果傳回,即實作了跨子產品調用

全局子產品

bash複制代碼# 建立子產品
nest g module common
           
ts複制代碼// common.module.ts
import { Module, Global } from '@nestjs/common';

@Global()
@Module({
  providers: [
    {
      provide: 'Common',
      useValue: { author: 'Leslie' },
    },
  ],
  exports: [
    {
      provide: 'Common',
      useValue: { author: 'Leslie' },
    },
  ],
})
export class CommonModule {}
           
ts複制代碼// 在 app.module.ts 引入 全局子產品
import { CommonModule } from './common/common.module';

@Module({
  imports: [
    ...,
    CommonModule,
  ],
})
export class AppModule {}
           
ts複制代碼// book.controller.ts 中依賴注入,并使用
@Controller('book')
export class BookController {
  constructor(
    ...,
    @Inject('Common') private Common: any,
  ) {}

  // 調用全局子產品方法
  @Get('/author')
  getAuthor(): any {
    const author = `作者: ${this.Common?.author}`;
    return {
      code: 200,
      data: author,
    };
  }
}
           

調用 http://localhost:3000/book/author 驗證。

繼續閱讀