ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
### Serverless 无服务器计算是一种云计算执行模型,其中云提供商按需分配机器资源,代表客户处理服务器。当应用程序未使用时,不会为应用程序分配计算资源。定价基于应用程序消耗的实际资源量(\[来源\](https://en.wikipedia.org/wiki/Serverless\_computing))。 使用\*\*无服务器架构\*\*,您可以完全专注于应用程序代码中的各个功能。 AWS Lambda、Google Cloud Functions 和 Microsoft Azure Functions 等服务负责所有物理硬件、虚拟机操作系统和 Web 服务器软件管理。 > \*\*提示\*\*本章不涵盖无服务器功能的优缺点,也不深入探讨任何云提供商的细节。 #### 冷启动 冷启动是您的代码在一段时间内第一次执行。根据您使用的云提供商,它可能跨越几个不同的操作,从下载代码和引导运行时到最终运行您的代码。此过程会增加\*\*显着的延迟\*\*,具体取决于多种因素、语言、应用程序所需的包数量等。 冷启动很重要,尽管有些事情超出了我们的控制范围,但我们仍然可以做很多事情来使其尽可能短。 虽然您可以将 Nest 视为一个成熟的框架,旨在用于复杂的企业应用程序,但它也**适用于许多“更简单”的应用程序**(或脚本)。例如,通过使用\[独立应用程序\](https://docs.nestjs.com/standalone-applications) 功能,您可以在简单的工作程序、CRON 作业、CLI 或无服务器功能中利用 Nest 的 DI 系统。 #### 基准 为了更好地了解在无服务器函数的上下文中使用 Nest 或其他知名库(如`express`)的成本是多少,让我们比较一下 Node 运行时需要多少时间来运行以下脚本: ~~~typescript // #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)); } bootstrap(); // #2 Nest (with @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(3000); } bootstrap(); // #3 Nest as a Standalone application (no HTTP server) 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()); } bootstrap(); // #4 Raw Node.js script async function bootstrap() { console.log('Hello world!'); } bootstrap(); ~~~ 对于所有这些脚本,我们使用了`tsc`(TypeScript)编译器,因此代码保持未捆绑(不使用`webpack`)。 | | | | ------------------------------------ | ----------------- | | Express | 0.0079s (7.9ms) | | Nest with `@nestjs/platform-express` | 0.1974s (197.4ms) | | Nest (standalone application) | 0.1117s (111.7ms) | | Raw Node.js script | 0.0071s (7.1ms) | > **注意** :机器:MacBook Pro Mid 2014,2.5 GHz 四核 Intel Core i7,16 GB 1600 MHz DDR3,SSD 现在,让我们重复所有的基准测试,但这次,使用`webpack`(如果你安装了\[Nest CLI\](https://docs.nestjs.com/cli/overview),你可以运行`nest build --webpack`)将我们的应用程序捆绑到一个可执行的 JavaScript 文件中。但是,我们将确保将所有依赖项(`node\_modules`)捆绑在一起,而不是使用 Nest CLI 附带的默认`webpack` 配置,如下所示: ~~~javascript 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 (err) { return true; } } return false; }, }), ], }; }; ~~~ > **提示**要指示 Nest CLI 使用此配置,请在项目的根目录中创建一个新的`webpack.config.js` 文件。 使用此配置,我们收到以下结果: | | | | ------------------------------------ | ---------------- | | Express | 0.0068s (6.8ms) | | Nest with `@nestjs/platform-express` | 0.0815s (81.5ms) | | Nest (standalone application) | 0.0319s (31.9ms) | | Raw Node.js script | 0.0066s (6.6ms) | > > **注意** :机器:MacBook Pro Mid 2014,2.5 GHz 四核 Intel Core i7,16 GB 1600 MHz DDR3,SSD > **提示**您可以通过应用额外的代码压缩和优化技术(使用`webpack`插件等)进一步优化它。 如您所见,编译方式(以及是否捆绑代码)至关重要,并且对整体启动时间有重大影响。使用`webpack`,您可以将独立的 Nest 应用程序(具有一个模块、控制器和服务的启动项目)的引导时间平均降低到 ~32 毫秒,对于常规的基于 express 的 HTTP 的 NestJS 的启动时间降低到 ~81.5 毫秒应用程序。 对于更复杂的 Nest 应用,比如有 10 个资源(通过`$nest g resource`schematic = 10 个模块、10 个控制器、10 个服务、20 个 DTO 类、50 个 HTTP 端点 +`AppModule` 生成),整体在 MacBook 上启动Pro Mid 2014,2.5 GHz 四核 Intel Core i7,16 GB 1600 MHz DDR3,SSD 大约 0.1298s (129.8ms)。将单体应用程序作为无服务器功能运行通常没有太大意义,因此请将此基准测试更多地视为引导时间如何随着应用程序增长而增加的示例。 #### 运行时优化 到目前为止,我们介绍了编译时优化。这些与您在应用程序中定义提供程序和加载 Nest 模块的方式无关,并且随着您的应用程序变得越来越大,这将发挥重要作用。 例如,假设将数据库连接定义为\[异步提供程序\](https://docs.nestjs.com/fundamentals/async-providers)。异步提供程序旨在延迟应用程序启动,直到完成一个或多个异步任务。这意味着,如果您的无服务器函数平均需要 2 秒来连接到数据库(在引导程序上),那么您的端点将需要至少两秒的额外时间(因为它必须等到连接建立)才能发送回响应(当它是冷启动并且您的应用程序尚未运行)。 如您所见,在\*\*无服务器环境\*\*中,您构建提供程序的方式有些不同,其中引导时间很重要。另一个很好的例子是,如果您使用 Redis 进行缓存,但仅限于某些场景。也许,在这种情况下,您不应该将 Redis 连接定义为异步提供程序,因为它会减慢引导时间,即使此特定函数调用不需要它。 此外,有时您可以使用“LazyModuleLoader”类延迟加载整个模块,如\[本章\](https://docs.nestjs.com/fundamentals/lazy-loading-modules) 中所述。缓存也是一个很好的例子。想象一下,您的应用程序有,比如说,`CacheModule`,它在内部连接到 Redis,并且还导出了`CacheService` 以与 Redis 存储进行交互。如果你不需要它来进行所有潜在的函数调用,你可以按需加载它,懒惰地。这样,对于所有不需要缓存的调用,您将获得更快的启动时间(发生冷启动时)。 ~~~typescript 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,根据某些特定条件(例如,输入参数),它可能执行不同的操作。在这种情况下,您可以在路由处理程序中指定一个条件,该条件为特定函数调用延迟加载适当的模块,然后延迟加载每个其他模块。 ~~~typescript 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); // ... } ~~~ #### 示例集成 您的应用程序的入口文件(通常是`main.ts`文件)应该看起来像\*\*取决于几个因素\*\*因此\*\*没有一个模板\*\*适用于每种情况。例如,启动无服务器功能所需的初始化文件因云提供商(AWS、Azure、GCP 等)而异。此外,根据您是要运行具有多个路由/端点的典型 HTTP 应用程序还是只提供单个路由(或执行代码的特定部分),您的应用程序的代码看起来会有所不同(例如,对于每函数方法你可以使用`NestFactory.createApplicationContext`代替启动HTTP服务器,设置中间件等)。 仅出于说明目的,我们将集成 Nest(使用`@nestjs/platform-express` 并启动整个功能齐全的 HTTP 路由器)与 \[Serverless\](https://www.serverless.com/) 框架(在这种情况下,针对 AWS Lambda)。正如我们之前提到的,您的代码将根据您选择的云提供商以及许多其他因素而有所不同。 首先,让我们安装所需的软件包: ~~~bash $ npm i @vendia/serverless-express aws-lambda $ npm i -D @types/aws-lambda serverless-offline ~~~ > **提示**为了加快开发周期,我们安装了模拟 AWS λ 和 API Gateway 的`serverless-offline`插件。 安装过程完成后,让我们创建`serverless.yml`文件来配置无服务器框架: ~~~yaml 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+}' ~~~ > **提示**要了解有关无服务器框架的更多信息,请访问\[官方文档\](https://www.serverless.com/framework/docs/)。 有了这些,我们现在可以导航到 main.ts 文件并使用所需的样板更新我们的引导代码: ~~~typescript import { NestFactory } from '@nestjs/core'; import serverlessExpress from '@vendia/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); }; ~~~ > **提示**对于创建多个 serverless 函数并在它们之间共享公共模块,我们建议使用\[CLI Monorepo 模式\](https://docs.nestjs.com/cli/monorepo#monorepo-mode)。 > **警告**如果您使用`@nestjs/swagger` 包,则需要一些额外的步骤才能使其在无服务器功能的上下文中正常工作。查看此\[文章\](https://javascript.plainenglish.io/serverless-nestjs-document-your-api-with-swagger-and-aws-api-gateway-64a53962e8a2)了解更多信息。 接下来,打开`tsconfig.json`文件并确保启用`esModuleInterop`选项以使`@vendia/serverless-express`包正确加载。 ~~~json { "compilerOptions": { ... "esModuleInterop": true } } ~~~ 现在我们可以构建我们的应用程序(使用`nest build`或`tsc`)并使用`serverless`CLI在本地启动我们的lambda函数: ~~~bash $ npm run build $ npx serverless offline ~~~ 应用程序运行后,打开浏览器并导航到`http://localhost:3000/dev/\[ANY\_ROUTE\]`(其中`\[ANY\_ROUTE\]`是在您的应用程序中注册的任何端点)。 在上面的部分中,我们已经展示了使用 webpack 和打包你的应用程序会对整个引导时间产生重大影响。但是,要使其与我们的示例一起使用,您必须在 webpack.config.js 文件中添加一些额外的配置。通常,为了确保我们的`handler`函数被调用,我们必须将`output.libraryTarget`属性更改为`commonjs2`。 ~~~javascript return { ...options, externals: [], output: { ...options.output, libraryTarget: 'commonjs2', }, // ... the rest of the configuration }; ~~~ 有了这个,你现在可以使用`$ nest build --webpack`来编译你的函数代码(然后`$ npx serverless offline`来测试它)。 还建议(但\*\*不是必需的\*\*因为它会减慢您的构建过程)安装`terser-webpack-plugin`包并覆盖其配置以在缩小生产构建时保持类名不变。在您的应用程序中使用`class-validator`时,不这样做可能会导致不正确的行为。 ~~~javascript const TerserPlugin = require('terser-webpack-plugin'); return { ...options, externals: [], optimization: { minimizer: [ new TerserPlugin({ terserOptions: { keep_classnames: true, }, }), ], }, output: { ...options.output, libraryTarget: 'commonjs2', }, // ... the rest of the configuration }; ~~~ #### 使用独立应用程序功能 或者,如果你想保持你的函数非常轻量并且不需要任何与 HTTP 相关的特性(路由,还有守卫、拦截器、管道等),你可以只使用`NestFactory.createApplicationContext`(如前所述) 而不是运行整个 HTTP 服务器(以及底层的`express`),如下所示: >main.ts ~~~typescript 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 { body: appService.getHello(), statusCode: HttpStatus.OK, }; }; ~~~ > \*\*提示\*\*注意`NestFactory.createApplicationContext`不会用增强器(守卫、拦截器等)包装控制器方法。为此,您必须使用`NestFactory.create` 方法。 您还可以将`event`对象传递给可以处理它并返回相应值(取决于输入值和您的业务逻辑)的`EventsService`提供程序。 ~~~typescript 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); }; ~~~