NestJS Logo
NestJS 中文文档
v10.0.0
  • 介绍
  • 快速上手
  • 控制器
  • 提供者
  • 模块
  • 中间件
  • 异常过滤器
  • 管道
  • 守卫
  • 拦截器
  • 自定义装饰器
  • 自定义提供者
  • 异步提供者
  • 动态模块
  • 依赖注入作用域
  • 循环依赖
  • 模块引用
  • 懒加载模块
  • 执行上下文
  • 生命周期事件
  • 发现服务
  • 跨平台无关性
  • 测试
迁移指南
API 参考
官方课程
  1. 文档
  2. 常见问题
  3. Serverless

独立应用模式
HTTP 适配器

Serverless(无服务器架构)

无服务器计算(Serverless Computing)是一种云计算执行模型。在该模型中,云服务平台会根据请求自动分配计算资源,并负责服务器的运维管理。当应用处于空闲状态时,不会消耗任何计算资源;你只需为实际使用的资源付费。

采用无服务器架构(Serverless Architecture)的开发方式,意味着你无需关注底层基础设施,只需专注于编写函数逻辑。像 AWS Lambda、Google Cloud Functions、Microsoft Azure Functions 等云服务会自动处理底层的服务器、虚拟机和操作系统配置,甚至包括 Web 服务器的维护。

提示

本章不讨论无服务器函数的优缺点,也不会深入探讨具体云厂商的实现细节。

冷启动

冷启动(Cold Start)是指函数在长时间未被调用后再次触发时,所经历的初始化过程。这个过程可能包括:加载函数代码、启动运行时环境、初始化依赖等操作,直到你的业务逻辑真正开始执行。

冷启动的延迟取决于多个因素,包括使用的编程语言、依赖数量、部署包大小等。有些语言(如 Java)或框架加载时间较长,冷启动延迟可能更加明显。

尽管某些冷启动过程不可避免,但仍有方法可以优化其响应速度。例如:减小部署包体积、移除不必要的依赖、合理拆分函数逻辑等。

虽然 Nest 通常被认为是一款面向企业级复杂应用的全功能框架,但它同样适用于轻量化的场景,比如:

  • Worker 任务
  • 定时任务(Cron Jobs)
  • 命令行工具(CLI)
  • 无服务器函数(Serverless Functions)

借助独立应用特性,你依然可以在这些场景中充分利用 Nest 强大的依赖注入机制和模块组织能力,获得一致的开发体验。

基准测试

为了评估在无服务器函数场景中,NestJS 与其他常见方案(如 express 或原生 Node.js)在冷启动性能方面的差异,我们编写了几段示例脚本,并测量它们在 Node.js 运行时中的启动耗时。

以下是用于测试的四种脚本示例:

main.ts
// #1 使用 Express 的基本示例
import * as express from 'express'

async function bootstrap() {
  const app = express()
  app.get('/', (req, res) => res.send('Hello world!'))
  await new Promise<void>((resolve) => app.listen(3000, resolve))
}

// #2 使用 NestJS(基于 @nestjs/platform-express)
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { logger: ['error'] })
  await app.listen(process.env.PORT ?? 3000)
}

// #3 使用 NestJS 的独立应用模式(不依赖 HTTP 服务)
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { AppService } from './app.service'

async function bootstrap() {
  const app = await NestFactory.createApplicationContext(AppModule, {
    logger: ['error'],
  })
  console.log(app.get(AppService).getHello())
}

// #4 原生 Node.js 脚本
async function bootstrap() {
  console.log('Hello world!')
}

上述所有脚本均使用 TypeScript 编译器(tsc)进行编译,未经过打包处理(即未使用 webpack 等工具)。因此,测试的均为未压缩的原始 TypeScript 编译结果。

框架启动时间
原生 Node.js 脚本0.0071 秒(7.1 毫秒)
Express0.0079 秒(7.9 毫秒)
Nest(基于 @nestjs/platform-express)0.1974 秒(197.4 毫秒)
Nest(独立应用)0.1117 秒(111.7 毫秒)
测试环境说明

测试设备:MacBook Pro(2014 款),配备 2.5 GHz 四核 Intel Core i7、16 GB 1600 MHz DDR3 内存和 SSD 存储。

接下来,我们将对这些脚本进行打包,再次进行基准测试。 你可以使用 Nest CLI 中的 nest build --webpack 命令,将 Nest 应用打包为一个独立的 JavaScript 文件。

不过,为了更精确地控制打包内容,我们没有采用 Nest CLI 默认的 webpack 配置,而是使用自定义配置,确保包括所有依赖(node_modules)在内的内容被打包进最终产物中:

module.exports = (options, webpack) => {
  const lazyImports = [
    '@nestjs/microservices/microservices-module',
    '@nestjs/websockets/socket-module',
  ]

  return {
    ...options,
    externals: [],
    plugins: [
      ...options.plugins,
      new webpack.IgnorePlugin({
        checkResource(resource) {
          if (lazyImports.includes(resource)) {
            try {
              require.resolve(resource)
            } catch {
              return true
            }
          }
          return false
        },
      }),
    ],
  }
}
提示

若希望 Nest CLI 使用该配置文件,请在项目根目录下创建 webpack.config.js。

使用上述配置后,得到如下启动时间测试结果:

框架启动时间
原生 Node.js 脚本0.0066 秒(6.6 毫秒)
Express0.0068 秒(6.8 毫秒)
Nest(基于 @nestjs/platform-express)0.0815 秒(81.5 毫秒)
Nest(独立应用)0.0319 秒(31.9 毫秒)
测试环境说明

设备:MacBook Pro(2014 年中款),2.5GHz 四核 Intel Core i7,16GB DDR3(1600 MHz)内存,SSD 存储。

提示

你还可以进一步通过代码压缩、使用 Webpack 插件等手段来优化性能。

从测试结果可以看出,构建与打包方式对应用启动时间有明显影响。

采用 Webpack 打包的 Nest 独立应用(包含一个模块、一个控制器和一个服务),平均启动时间约为 32 毫秒;而标准的基于 Express 的 Nest 应用,启动时间则为 81.5 毫秒。

如果是一个更复杂的 Nest 应用,例如通过 $ nest g resource 命令生成的 10 个资源模块(共包含 10 个模块、10 个控制器、10 个服务、20 个 DTO 类和 50 个 HTTP 端点,再加上 AppModule),在相同环境下的启动时间约为 129.8 毫秒。

当然,将大型单体应用作为单个无服务器函数运行,在实际中并不现实。不过这些测试数据可以作为性能基准,帮助你评估应用复杂度对启动时间的影响。

运行时优化

前文我们讲到了编译时优化,这类优化主要与提供者的定义方式或模块加载顺序无关。但随着应用规模的扩大,运行时机制对应用启动性能的影响将愈发显著。

以一个常见场景为例:假设你定义了一个数据库连接作为异步提供者。异步提供者的特点是,应用会等待相关的异步任务完成后,才正式启动。这种机制可能在无服务器架构中引发问题 —— 如果数据库连接平均需要 2 秒建立,那么在发生冷启动时,请求的响应时间就会被强制延迟 2 秒。

正因如此,在无服务器环境中组织提供者的方式,需要区别于传统应用。启动速度至关重要,因此必须避免不必要的初始化操作。

比如:若你仅在特定情况下才使用 Redis 缓存服务,那么就不应将 Redis 连接定义为异步提供者。否则,即便本次请求根本不涉及缓存逻辑,也会因等待 Redis 连接建立而拖慢整个应用的启动。

为了解决这类问题,你可以使用 LazyModuleLoader 类(详见懒加载模块章节),按需加载模块。以缓存模块为例:

假设你的应用中有一个 CacheModule,该模块负责初始化 Redis 并导出 CacheService。如果并非所有请求都需要缓存支持,就可以通过懒加载的方式,仅在必要时加载该模块,从而显著加快冷启动速度:

cache.service.ts
if (request.method === RequestMethod[RequestMethod.GET]) {
  const { CacheModule } = await import('./cache.module')
  const moduleRef = await this.lazyModuleLoader.load(() => CacheModule)

  const { CacheService } = await import('./cache.service')
  const cacheService = moduleRef.get(CacheService)

  return cacheService.get(ENDPOINT_KEY)
}

除了缓存系统,Webhook 与 Worker 也是非常适合懒加载的场景。它们往往根据输入参数或上下文,执行完全不同的逻辑模块。你可以在请求处理函数中加入条件判断,只在需要时加载对应模块,避免无谓的资源开销:

if (workerType === WorkerType.A) {
  const { WorkerAModule } = await import('./worker-a.module')
  const moduleRef = await this.lazyModuleLoader.load(() => WorkerAModule)
  // ...
} else if (workerType === WorkerType.B) {
  const { WorkerBModule } = await import('./worker-b.module')
  const moduleRef = await this.lazyModuleLoader.load(() => WorkerBModule)
  // ...
}

集成示例

Nest 应用的入口文件(通常为 main.ts)的写法会因多种因素而有所不同,因此不存在放之四海而皆准的模板。例如,针对不同的云平台(如 AWS、Azure、GCP)部署函数时,入口文件的结构往往也会有所差异。

此外,入口逻辑还取决于你希望实现的目标:是构建一个带有多个路由的完整 HTTP 应用,还是仅需运行某个独立函数?(在「函数即端点」的场景中,你可能会使用 NestFactory.createApplicationContext 来创建无 HTTP 服务的上下文。)

在本节中,我们将演示如何将 Nest(借助 @nestjs/platform-express 启动完整的 HTTP 路由能力)与 Serverless Framework 集成,目标部署平台为 AWS Lambda。需要注意的是,实际实现可能会因云厂商或项目需求的不同而有所调整。

安装依赖

首先,安装所需依赖包:

npm install @codegenie/serverless-express aws-lambda
npm install -D @types/aws-lambda serverless-offline
提示

这里使用的 serverless-offline 插件可以在本地模拟 AWS Lambda 和 API Gateway 的运行环境,极大提高开发与调试效率。

配置 Serverless Framework

创建 serverless.yml 文件,定义服务名称、插件、提供商信息以及函数入口配置:

service: serverless-example

plugins:
  - serverless-offline

provider:
  name: aws
  runtime: nodejs14.x

functions:
  main:
    handler: dist/main.handler
    events:
      - http:
          method: ANY
          path: /
      - http:
          method: ANY
          path: '{proxy+}'
提示

更多关于 Serverless Framework 的配置说明,请参阅官方文档。

编写入口文件

接下来,更新 main.ts 以适配无服务器部署方式:

main.ts
import { NestFactory } from '@nestjs/core'
import serverlessExpress from '@codegenie/serverless-express'
import { Callback, Context, Handler } from 'aws-lambda'
import { AppModule } from './app.module'

let server: Handler

async function bootstrap(): Promise<Handler> {
  const app = await NestFactory.create(AppModule)
  await app.init()

  const expressApp = app.getHttpAdapter().getInstance()
  return serverlessExpress({ app: expressApp })
}

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback
) => {
  server = server ?? (await bootstrap())
  return server(event, context, callback)
}
提示

若你计划定义多个无服务器函数,且希望它们共享模块逻辑,建议使用 CLI 提供的 Monorepo 模式 进行结构组织。

注意

如果你使用了 @nestjs/swagger 进行 API 文档生成,在无服务器环境中可能需要额外配置以确保其正常工作。详见相关讨论。

配置 TypeScript

确保在 tsconfig.json 中启用了 esModuleInterop,以正确引入 @codegenie/serverless-express 包:

{
  "compilerOptions": {
    ...
    "esModuleInterop": true
  }
}

启动本地开发环境

构建项目后,可使用 serverless CLI 启动本地模拟环境:

npm run build
npx serverless offline

本地服务启动后,可通过浏览器访问 http://localhost:3000/dev/[ANY_ROUTE],其中 [ANY_ROUTE] 为你在 Nest 应用中定义的任意路由路径。

使用 Webpack 打包以提升启动性能

通过 webpack 对函数进行打包,可显著减少冷启动耗时。但为了正常导出 handler 函数,需要在 webpack.config.js 中做一些配置:

webpack.config.js
return {
  ...options,
  externals: [],
  output: {
    ...options.output,
    libraryTarget: 'commonjs2',
  },
  // 其他配置项
}

构建时可使用以下命令:

nest build --webpack
npx serverless offline

可选优化:保留类名以配合 class-validator

在某些情况下(例如使用 class-validator 时),压缩工具会移除类名,导致运行时类型判断失败。为避免此类问题,我们建议(但非强制)安装并配置 terser-webpack-plugin,在压缩过程中保留类名:

npm install -D terser-webpack-plugin
webpack.config.js
const TerserPlugin = require('terser-webpack-plugin')

return {
  ...options,
  externals: [],
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          keep_classnames: true,
        },
      }),
    ],
  },
  output: {
    ...options.output,
    libraryTarget: 'commonjs2',
  },
  // 其他配置项
}

使用独立应用上下文

如果你希望函数尽可能轻量,且不依赖任何 HTTP 相关功能(如路由、守卫、拦截器、管道等),可以选择使用 NestFactory.createApplicationContext,而不是启动完整的 HTTP 服务(默认基于 Express)。这种方式非常适合在无服务器(Serverless)环境中,仅借助依赖注入功能执行特定逻辑的场景。

以下示例展示了如何在 AWS Lambda 中创建一个独立的 Nest 应用上下文:

main.ts
import { HttpStatus } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { Callback, Context, Handler } from 'aws-lambda'
import { AppModule } from './app.module'
import { AppService } from './app.service'

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback
) => {
  const appContext = await NestFactory.createApplicationContext(AppModule)
  const appService = appContext.get(AppService)

  return {
    statusCode: HttpStatus.OK,
    body: appService.getHello(),
  }
}
注意

使用 createApplicationContext 创建的应用上下文不会执行任何控制器相关逻辑,也不会触发守卫、拦截器、管道等「增强器」。如果你需要完整的请求处理流程,请使用 NestFactory.create 启动 HTTP 应用。

你也可以将 event 参数交由某个服务(例如 EventsService)处理,以根据自定义业务逻辑返回结果:

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback
) => {
  const appContext = await NestFactory.createApplicationContext(AppModule)
  const eventsService = appContext.get(EventsService)

  return eventsService.process(event)
}