二、Nestjs启动服务

本节我们实现一个简化版的启动nestjs服务的功能

官方启动服务代码

我们先参照官方文档构建一个最简单的Nest服务,代码如下:

1
2
3
4
5
6
7
8
9
10
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();

1
2
3
4
5
6
7
8
9
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';

@Module({
controllers: [AppController],
})
export class AppModule {}

1
2
3
4
5
6
7
8
9
10
// app.controller.ts
import { Controller, Get } from "@nestjs/common";

@Controller()
export class AppController {
@Get()
getHello(): string {
return 'Hello'
}
}

我们主要用到了两个包@nestjs/core@nestjs/common,其中用到了core包中的NestFactory类,以及common包中的Module, Controller, Get这几个装饰器。

接下来我们自己手动来实现这部分功能

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
# 目录结构如下
- @nestjs
- common # common包,目前实现了几个装饰器
- index.ts
- module.decorator.ts
- controller.decorator.ts
- http-methods.decorator.ts
- core # core包,简单实现了NestFactory, NestApplication, Logger
- index.ts
- nest-factory.ts
- nest-application.ts
- logger.ts

手写实现启动服务所需的基本功能,不按照源码实现,只是一个思路

启动服务所需的代码与前边是一致的,只是不再引入官方包,而是使用我们自己封装的

common

common包目前定义了一些装饰器,这些装饰器目前只用来定义一些元数据,由于服务启动时,需要读取元数据进行使用,所以我们先展示一下common包中我们现在实现了哪些内容。

index做统一导出:

1
2
3
4
// @nestjs/common/index.ts
export * from './module.decorator'
export * from './controller.decorator'
export * from './http-methods.decorator'

@Module目前只做了一件事,就是把传递的controllers保存到元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @nestjs/common/module.decorator.ts
import 'reflect-metadata'

interface ModuleMetadata {
controllers: Function[]
}

// Module装饰器工厂函数
export function Module(metadata: ModuleMetadata): ClassDecorator {
return (target: Function) => {
// 给类添加元数据controllers,值为传入Module的controllers
Reflect.defineMetadata('controllers', metadata.controllers, target)
}
}

@Controller用来把路径前缀保存到元数据,其定义方式较多,所以做了一些判断,注意一下函数重载,这是TS支持的功能,可以定义各种重载的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// @nestjs/common/controller.decorator.ts
import 'reflect-metadata'

interface ControllerOptions {
prefix: string
}
// Controller可以接收路径前缀
// 前缀可以为空,空字符串,非空字符串,对象
// 空和空字符串都意味着路径为'/',对象传递{prefix: '/'}也可以
// 定义函数重载 - TS才支持,只定义参数、返回值类型,不实现内容
export function Controller(): ClassDecorator
export function Controller(prefix: string): ClassDecorator
export function Controller(options: ControllerOptions): ClassDecorator

export function Controller(prefixOrOptions?: string | ControllerOptions): ClassDecorator {
// 根据不同的类型赋值
let options:ControllerOptions = {
prefix: ''
}
if (typeof prefixOrOptions === 'string') {
options.prefix = prefixOrOptions
} else if (typeof prefixOrOptions === 'object') {
options = prefixOrOptions
}
return (target: Function) => {
// 给控制器类添加prefix路径前缀的元数据,路径为空默认空串
Reflect.defineMetadata('prefix', options.prefix || '', target)
}
}

@Get用来将请求方法和路径保存到方法的元数据,我们目前只实现了Get

1
2
3
4
5
6
7
8
9
10
11
// @nestjs/common/http-methods.decorator.ts
import 'reflect-metadata'


export function Get(path:string = ''): MethodDecorator {
return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
// 给修饰的方法添加元数据
Reflect.defineMetadata('path', path, descriptor.value)
Reflect.defineMetadata('method', 'GET', descriptor.value)
}
}

core

core包是启动服务所需的内容,在index.ts中主要是统一导出了其他文件中的内容

1
2
3
4
// @nestjs/core/index.ts
export * from './nest-factory'
export * from './logger'
export * from './nest-application'

main.ts中我们通过new NestFactory(AppModule)来生成实例,这是NestFactory的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @nestjs/core/nest-factory.ts
import { Logger } from "./logger";
import { NestApplication } from "./nest-application";

export class NestFactory {
// 静态方法 create
static async create(module: any) {
// 启动Nest应用
Logger.log('Starting Nest application...', 'NestFactory')
// 创建实例
const app = new NestApplication(module)
return app
}
}

实际的创建实例操作是在NestApplication中实现的,其内部完成了express应用的创建,路由的解析和路由注册

这些功能的实现,得益于我们的装饰器定义了相关的元数据,在这里取出来进行解析,装饰器的内容在common包中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// @nestjs/core/nest-application.ts
// 源码是使用express-adaptor适配器的,此处做简化版
import * as express from 'express'
import type { Express, Request, Response, NextFunction } from 'express'
import 'reflect-metadata'
import { Logger } from './logger'
import * as path from 'path'
export class NestApplication {
// 在它的内部私有化一个Express实例
private readonly app = express()

constructor(protected readonly module: any) {}
// 初始化
async init() {
// 取出模块里的所有控制器,做好路由配置
// 传入的模块及其对应的控制器等内容已经执行了装饰器,所以元数据已经定义好了
const controllers = Reflect.getMetadata('controllers', this.module) || []
Logger.log(`AppModule dependencies initialized`, 'InstanceLoader')
// 遍历控制器
for (const Controller of controllers) {
// 创建每个控制器的实例
const controller = new Controller()
// 获取控制器的路径前缀, 无前缀则默认'/'
const prefix = Reflect.getMetadata('prefix', Controller) || '/'
Logger.log(`${Controller.name} {${prefix}}`, 'RoutesResolver')
// 遍历类里的方法
const controllerPrototype = Reflect.getPrototypeOf(controller)
for (const methodName of Object.getOwnPropertyNames(controllerPrototype)){
// 取方法
const method = controllerPrototype[methodName]
// 取请求方法
const httpMethod = Reflect.getMetadata('method', method)
// 取路径
const httpPath = Reflect.getMetadata('path', method)
if (!httpMethod) continue;
// 拼接路由
const routePath = path.posix.join('/', prefix, httpPath)
// 注册路由 - express上通过app.get('/path', () => {})来注册路由
this.app[httpMethod.toLowerCase()](routePath, (req: Request, res: Response, next: NextFunction) => {
// 交由对应方法来处理请求
const result = method.call(controller)
res.send(result)
})
Logger.log(`Mapped {${routePath}, ${httpMethod}} route`, 'RoutesResolver')


}
}
Logger.log('Nest application successfully started', 'NestApplication')
}
// 调用express的listen方法启动服务,监听端口
async listen(port: number) {
await this.init()
this.app.listen(port, () => {
Logger.log(`Application is running on http://localhost:${port}`, 'NestApplication')
})
}
}

Logger用来打印日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// @nestjs/core/logger.ts
import { green, yellow } from 'cli-color'
// 模仿nestjs的log日志
export class Logger {
// 定义一个上次打印日志的时间
private static lastLogTime = Date.now()
static log(message: string, context: string = '') {
// 获取当前时间戳
const timestamp = new Date().toLocaleString()
// 进程ID
const pid = process.pid
// 时间差
const currentTime = Date.now()
const timeDiff = currentTime - this.lastLogTime
console.log(`${green('[Nest]')} ${green(pid.toString())} - ${timestamp} ${green('LOG')} [${yellow(context)}] ${green(message)} +${yellow(timeDiff)}ms`)
this.lastLogTime = currentTime
}
}

至此,我们实现了简单的路由解析和处理程序,后续还有更多内容要处理,我们在后边在讲

作者

胡兆磊

发布于

2024-09-22

更新于

2025-04-18

许可协议