5 分钟内为 Knex.js(或其他基于资源的库)构建 NestJS 模块
John 是 NestJS 核心团队的成员
有没有想过将你最喜欢的库集成到 NestJS 中?例如,虽然 Nest 内置了对数据库集成的广泛支持,但如果你想使用自己喜欢的库,而又没有 Nest 包,该怎么办?好吧,为什么不自己构建一个呢?
这乍一看似乎是一项艰巨的任务。但如果你一直在关注我的博客文章,你就会在我的上一篇文章中看到 NestJS 动态模块的设计模式,它会为你指明正确的方向。你可能会说,好吧,但集成一个库似乎仍然需要大量工作。
我有个好消息!借助 Nest CLI 的强大功能,您只需一个命令即可生成一个完整的自定义动态模块模板,并遵循 NestJS 的最佳实践!然后,您只需大约 5 分钟即可集成您的库!这项技术适用于许多基于资源的库,这些库导出我称之为API 对象的内容,这些对象自然地与 Nest 内置的单例提供程序模型配合使用,以共享全局资源。跟随这样的冒险,我们将在下面构建一个与Knex.js 的快速集成。
敬请期待更多关于 NestJS CLI 的精彩内容,尽管它被严重低估了。这款工具基于 Angular CLI 构建,拥有无限潜力,将彻底改变您使用 Nest 的方式。我计划在不久的将来推出更多关于这个主题的内容!
简介
在上周的《如何构建完全动态的 NestJS 模块》@nestjs/passport
一文中,我介绍了一种在标准 Nest 包(如NestJS、@nestjs/jwt
NestJS 和NestJS)中使用的设计模式。@nestjs/typeorm
这篇文章的重点有两个:
- 提供常见 NestJS 包中使用的基本模式的路线图,以帮助您更好地使用这些包。
- 鼓励开发人员思考如何在自己的代码中使用这种模式来实现更容易组合的模块。
有些人可能认为这个模式有点复杂,我并不反对。然而,充分理解它是很有用的,有助于掌握 Nest 的一些核心概念,尤其是在利用模块系统和依赖注入方面。尽管如此,该模式中还是包含相当多的样板代码。如果能简化这个过程就好了。
系好安全带!NestJS CLI 自定义原理图来帮你!🚀
我们将构建什么
在本教程中,我们将构建一个模块,将 API 直接导出到完整的Knex.js库。这是一个功能强大的数据库集成库,在 Node.js 生态系统中广泛使用。以下是我们将要执行的操作。以下步骤与您集成任何其他基本可调用 API(例如,ioredis、Cassandra、Neo4J、Elasticsearch、LevelDb等等)的步骤完全相同。
得益于 Nest CLI 的强大功能,再加上自定义原理图,我们可以在大约 5 分钟内完成这些步骤!作为奖励,我们将在本文末尾介绍一些更高级的功能,包括如何一键将生成的软件包发布到 npm。
如果您想查看本文其余部分介绍的步骤的完整版本(一个完全可用的 NestJS/Knex.js 模块),您可以在此处找到包含完整代码的 github repo。
Nest CLI 和原理图
您可能已经定期使用 Nest CLI。nest new myProject
和nest generate controller user
就是常见的例子。它们使用@nestjs/schematics
CLI 附带的软件包开箱即用。非常酷的是,您可以轻松使用其他 Schematics 来完成高度定制的工作。所有 Schematics 的一个共同特点是它们能够理解您的项目,并能够以智能的方式搭建新的架构组件并将其连接到您的项目中。您可以将单个 Schematics 视为蓝图,将 CLI 机制视为每个蓝图如何工作的一套标准。因此,新的 Schematics 可以“正常工作”,并继承 CLI 及其标准功能提供的所有特性。
您可以编写自己的原理图,我计划在接下来的博客文章中向您展示如何操作。目前,我已经构建了一个原理图,您可以用它来完成我们今天进行的库集成项目。让我们开始吧!
要使用任何外部原理图,您当然需要安装它们。重要提示:由于 CLI 的工作方式,您必须将原理图集合安装为全局包。
安装自定义 Schematics 集合
Schematics 被打包为集合,可以作为 npm 包捆绑,因此安装起来非常简单。现在你可以使用以下命令进行安装:
npm install @nestjsplus/dyn-schematics -g
使用自定义 Schematics 集合
使用自定义集合中的原理图非常简单。标准的 Nest CLI 命令结构如下:
嵌套命令或别名[-c 示意图集合]必需参数[选项]
commandOrAlias
是:new
或generate
(别名:)g
或add
schematicCollection
是可选的,默认为内置的 NestJS 原理图;您可以选择指定全局安装的 npm 包requiredArg
正在生成/添加的架构元素,例如,在我们的例子中controller
,module
或很快dynpkg
options
对所有原理图都是全局的;它们可以是--dry-run
、--no-spec
或--flat
安装完成后,您可以像这样使用@nestjsplus/dyn-schematics包:
- 确保您位于要作为项目父级的文件夹中。使用此原理图,我们将创建一个新的完整 Nest 包,这意味着它是一个新的独立项目。因此,它将使用
name
您提供的文件创建一个文件夹,并将所有组件放入该文件夹中。 - 使用 CLI 运行原理图
nest g -c @nestjsplus/dyn-schematics dynpkg nest-knex
这段代码运行使用来自的自定义原理图集合@nestjsplus/dyn-schematics
,并指定dynpkg
要执行的原理图(原理图标识了要生成的内容- 在本例中,是一个标识为 的动态模块包dynpkg
),并赋予它一个name
。如果您想查看在不将文件添加到文件系统的情况下nest-knex
执行的操作,只需在命令末尾添加 即可。--dry-run
这应该会提示你一个问题Generate a testing client?
。为了方便测试,请回答“是”。你将看到以下输出,然后你就可以从nest-knex
文件夹中的 IDE 打开此项目了。
► nest g -c @nestjsplus/dyn-schematics dynpkg nest-knex
? Generate a testing client? Yes
CREATE /nest-knex/.gitignore (375 bytes)
CREATE /nest-knex/.prettierrc (51 bytes)
CREATE /nest-knex/README.md (2073 bytes)
CREATE /nest-knex/nest-cli.json (84 bytes)
CREATE /nest-knex/package.json (1596 bytes)
CREATE /nest-knex/tsconfig.build.json (97 bytes)
CREATE /nest-knex/tsconfig.json (430 bytes)
CREATE /nest-knex/tslint.json (426 bytes)
CREATE /nest-knex/src/constants.ts (54 bytes)
CREATE /nest-knex/src/index.ts (103 bytes)
CREATE /nest-knex/src/main.ts (519 bytes)
CREATE /nest-knex/src/nest-knex.module.ts (1966 bytes)
CREATE /nest-knex/src/nest-knex.providers.ts (262 bytes)
CREATE /nest-knex/src/nest-knex.service.ts (1111 bytes)
CREATE /nest-knex/src/interfaces/index.ts (162 bytes)
CREATE /nest-knex/src/interfaces/nest-knex-module-async-options.interface.ts (532 bytes)
CREATE /nest-knex/src/interfaces/nest-knex-options-factory.interface.ts (194 bytes)
CREATE /nest-knex/src/interfaces/nest-knex-options.interface.ts (409 bytes)
CREATE /nest-knex/src/nest-knex-client/nest-knex-client.controller.ts (732 bytes)
CREATE /nest-knex/src/nest-knex-client/nest-knex-client.module.ts (728 bytes)
此时,您可以安装生成的代码:
cd nest-knex
npm install
现在使用以下命令启动应用程序:
npm run start:dev
如果您现在浏览http://localhost:3000您应该得到:
NestKnexModule 向您问好!
太棒了!至此,我们已经搭建了动态模块的框架,可以开始 Knex.js 集成了。
与 Knex.js 集成
现在,你已经搭建好了项目框架,可以进行库集成了。我们只需进行一些修改即可完成实际的集成。让我们来详细了解一下:
- 我们需要将其
knex
作为依赖项安装。请注意,要运行此项目,您需要访问实时 SQL 数据库。Knex.js 不包含任何数据库库,因此您需要安装相应的数据库库(您可以在此处了解更多信息)。在本教程中,我将使用 PostgreSql,该数据库已在本地主机上提供。如果您没有可用的本地数据库,可以考虑使用 docker setup。 - 我们需要一种方法来向 Knex.js API 提供配置选项。
- 我们需要以初始化的方式调用 Knex.js API,返回一个
knex
对象的句柄,我们可以使用该对象来访问 Knex.js 功能。 - 为了测试,我们可以使用原理图自动生成的客户端控制器。
- 一旦它正常工作,我们就可以直接使用这个包。或者,我们可以将包发布到某个仓库(例如,内部包仓库或 npmjs.com)。
安装依赖项
该knex
软件包是必需的。您还需要一个数据库 API 库来运行该模块。下面我使用的pg
是 PostgreSql,但您可以选择任何您想要的(从此处的列表中)。
cd nest-knex
npm install knex pg
Knex.js 选项
正如动态模块文章中所讨论的,我们使用异步选项提供程序为服务提供选项。所有这些操作的代码已经为您搭建好了,但为了以原生 TypeScript 方式传递适当的选项,我们需要修改NestKnexOptions
接口。这些内容包含在文件中src/interfaces/nest-knex-options.interface.ts
。
我们想描述将传递给库 API 的可用 Knex.js 选项。最简单的方法是使用Knex.js 已经提供的类型。由于此接口是由knex
包导出的,因此我们只需导入它即可,我们的工作就快完成了!我们只需为其添加别名,使其对我们生成的包可见即可。打开src/interfaces/nest-knex-options.interface.ts
并编辑它,如下所示:
// src/interfaces/nest-knex-options.interface.ts
import { Config } from 'knex';
export interface NestKnexOptions extends Config {}
Knex 连接
我们模块的任务非常简单:连接到 API 并返回一个可复用的knex
对象,用于与数据库交互。对于 PostgreSql 来说,knex
(在 之上pg
)返回一个连接池,这意味着我们只需连接一次,即可返回一个单例 knex
对象,该对象将自动在连接池中平衡多个查询。这与 NestJS 及其单例提供程序的概念无缝协作!
注意:我对 Knex 的熟悉程度不如其他数据库库,对 MySQL 等其他数据库也不太熟悉,因此如果您将此模块用于此类数据库,请确保您了解
knex
在应用程序中共享连接对象的正确模式。本主题与NestJS无关——它主要讨论的是 Knex.js 库和数据库客户端库的架构。
现在我们将实现一个简单的模式来返回我们的knex
对象。打开src/nest-knex.service.ts
。快速查看生成的代码,然后用以下内容替换该代码:
// src/nest-knex.service.ts
import { Injectable, Inject, Logger } from '@nestjs/common';
import { NEST_KNEX_OPTIONS } from './constants';
import { NestKnexOptions } from './interfaces';
const Knex = require('knex');
interface INestKnexService {
getKnex();
}
@Injectable()
export class NestKnexService implements INestKnexService {
private readonly logger: Logger;
private _knexConnection: any;
constructor(@Inject(NEST_KNEX_OPTIONS) private _NestKnexOptions: NestKnexOptions) {
this.logger = new Logger('NestKnexService');
this.logger.log(`Options: ${JSON.stringify(this._NestKnexOptions)}`);
}
getKnex() {
if (!this._knexConnection) {
this._knexConnection = new Knex(this._NestKnexOptions);
}
return this._knexConnection;
}
}
生成的样板代码几乎全部没有改动。我们只是添加了一个属性来存储连接对象(_knexConnection
),以及一个getKnex()
方法,用于在第一次请求时实例化该对象,然后将其缓存以备将来使用。
测试模块
如上所述,您需要一个可用的本地数据库实例来测试该模块。如果您手边没有,可以考虑使用 docker来执行此操作。很简单!
该原理图为我们@nestjsplus/dyn-schematics
dynpkg
构建了一个小型测试客户端模块yes
(假设您回答了提示)。我们可以使用它来快速测试我们的nest-knex
模块。
首先,打开src/nest-knex-client/nest-knex-client.module.ts
并在方法中添加所需的 Knex.js 选项register()
。请适当调整:
// src/nest-knex-client/nest-knex-client.module.ts
...
@Module({
controllers: [NestKnexClientController],
imports: [
NestKnexModule.register({
client: 'pg',
connection: {
host: 'localhost',
user: 'john',
password: 'mypassword',
database: 'nest',
port: 5432,
},
}),
],
})
...
现在,打开src/nest-knex-client/nest-knex-client.controller.ts
并插入一些查询。当然,这并非一个真正好的设计模式(直接从控制器调用数据库服务),而只是对 Knex.js 集成是否正常工作进行快速测试。实际上,您需要根据 NestJS 的最佳实践,将所有此类访问委托给真正的 NestJS服务。
以下是您可以尝试的方法。此测试依赖于以下数据库表(以下语法适用于 PostgreSql,但对于其他数据库可能需要稍作调整):
CREATE TABLE cats
(
id serial NOT NULL,
name text,
age integer,
breed text
);
ALTER TABLE cats
ADD CONSTRAINT cats_pkey
PRIMARY KEY (id);
以下是您可以在测试控制器中尝试的示例。请注意,您可以通过该knex
对象使用 Knex.js 的全部功能。更多Knex 的有趣示例,请参阅此处。由于您拥有该对象的句柄knex
,因此所有这些Knex 查询生成器方法都应该可以正常工作!在这个看似简单的示例中,我们在索引路由中创建并查询了一些猫,并使用knex
对象访问数据库。
// src/nest-knex-client/nest-knex-client.controller.ts
import { Controller, Get } from '@nestjs/common';
import { NestKnexService } from '../nest-knex.service';
@Controller()
export class NestKnexClientController {
constructor(private readonly nestKnexService: NestKnexService) {}
@Get()
async index() {
const knex = this.nestKnexService.getKnex();
const newcat = await knex('cats').insert({
name: 'Fred',
age: 5,
breed: 'tom cat',
});
const cats = await knex.select('*').from('cats');
return cats;
}
}
现在,使用启动应用程序npm run start:dev
,并浏览到http://localhost:3000
,您应该会看到查询的结果!
如果您想用一个单独的应用来测试该模块——真正展示您刚刚构建的可复用库模块的强大功能——请继续阅读,了解如何一步将其上传为 npm 包。为了方便起见,您可以在此处下载一个导入该模块的完整客户端应用,并使用它来测试您新创建的 Knex.js 模块。事实上,knex-cats是一个功能齐全的示例,所以如果您一直在阅读本文而没有编写代码,这是查看最终成品的最简单方法。
奖金部分
希望我兑现了我的承诺,向你展示了如何快速生成一个动态的 NestJS 模块,该模块只需几分钟即可包装一个外部资源库!我们完成了——你现在拥有一个功能齐全的 Knex.js API 供你使用!我们在上一节中所做的查询只是冰山一角,因为 Knex.js 还有很多很酷的功能。
让我们再讨论几个话题来完善讨论。
更好的 API
如上所述,直接从控制器访问对象并非好习惯knex
。我们可以创建一个服务来托管数据库逻辑,从而稍微改善一下。我们还可以通过添加更高级别的 API,使 Knex.js 模块更易于使用。让我们开始吧。我们将添加一个提供程序,以便我们将knex
对象直接注入到任何服务中。您稍后会看到如何简化 API。
nest-knex-connection.provider.ts
首先,在文件夹中创建一个名为 inside 的文件src
。添加以下代码:
// src/nest-knex-connection.provider.ts
import { KNEX_CONNECTION } from './constants';
import { NestKnexService } from './nest-knex.service';
export const connectionFactory = {
provide: KNEX_CONNECTION,
useFactory: async nestKnexService => {
return nestKnexService.getKnex();
},
inject: [NestKnexService],
};
为了遵循提供商的最佳实践,我们使用KNEX_CONNECTION
常量作为提供商注入令牌。请务必将以下代码添加到src/constants.ts
:
export const KNEX_CONNECTION = 'KNEX_CONNECTION';
看看我们在这里做了什么?我们将一个注入令牌(KNEX_CONNECTION
)绑定到一个工厂,这样我们就可以knex
直接使用我们的 API 对象,而无需实例化服务。例如,一旦我们完成下一步(如下),我们就能够knex
在控制器中访问该对象,如下所示(请注意示例代码中old和new之间略微简化的技术):
// src/nest-knex-client/nest-knex-client.controller.ts
import { Controller, Inject, Get } from '@nestjs/common';
import { KNEX_CONNECTION } from '../constants';
@Controller()
export class NestKnexClientController {
// new
constructor(@Inject(KNEX_CONNECTION) private readonly knex) {}
// old
// constructor(private readonly nestKnexService: NestKnexService) {}
@Get()
async index() {
// following line no longer needed
// const knex = this.nestKnexService.getKnex();
const newcat = await this.knex('cats').insert({
name: 'Fred',
age: 5,
breed: 'tom cat',
});
const cats = await this.knex.select('*').from('cats');
return cats;
}
}
要将这个新的提供程序连接到我们的模块,请打开src/nest-knex.module.ts
并进行以下更改:
- 导入
connectionFactory
- 添加
connectionFactory
到@Module
元数据providers
和exports
属性中。该文件的此部分现在如下所示:
import { connectionFactory } from './nest-knex-connection.provider';
@Global()
@Module({
providers: [NestKnexService, connectionFactory],
exports: [NestKnexService, connectionFactory],
})
数据库访问最佳实践的完整实现还需要我们创建一个访问提供程序的服务KNEX_CONNECTION
,而不是在控制器中执行此操作,但我将其留给读者作为练习。😉
动态注册(使用配置工厂)
在动态模块文章中,我们讨论了使用异步选项提供程序。我们的意思是,与其像这样使用包含连接参数的静态声明对象,不如这样做:
NestKnexModule.register({
client: 'pg',
connection: {
host: 'localhost',
user: 'john',
password: 'mypassword',
database: 'nest',
port: 5432,
},
}),
我们更倾向于以动态的方式提供连接选项。好消息!这已经内置在生成的包中了。我们来测试一下。为了简单起见,我们只使用一个就地工厂,但您可以充分利用基于类、基于工厂和现有提供程序的功能。为了测试它,我们将更新NestKnexModule
in的注册src/nest-knex-client/nest-knex-client.module.ts
,如下所示:
// src/nest-knex-client/nest-knex-client/module.ts
NestKnexModule.registerAsync({
useFactory: () => {
return {
debug: false,
client: 'pg',
connection: {
host: 'localhost',
user: 'john',
password: 'mypassword',
database: 'nest',
port: 5432,
},
};
},
}),
这不是一个特别精彩的例子,但你应该明白了。你可以利用任何异步提供程序方法向模块提供配置数据。如果你查看knex-cats示例,你会看到一些更强大的配置模块用法,可以动态配置我们的 Knex 模块。
发布包
如果您阅读过我之前的文章《使用 npm 发布 NestJS 包》,您可能已经注意到,这个包的结构package.json
(例如,文件、tsconfig.json
文件本身、文件在根文件夹中的存在index.ts
)完全遵循该模式。因此,发布这个包非常简单(假设您有一个npmjs.com
帐户):
npm publish
说实话,就是这样!
当然,您始终可以使用这个现成的@nestjsplus/knex包,其构建方式与本文中的描述完全一致。
不仅仅是外部 API!
@nestjsplus/dyn-schematics 包不仅支持生成独立包(如上所述),还支持在现有项目中生成常规动态模块。我不会在这里详细介绍,但用例很简单。假设您正在构建一个在项目中使用的ConfigModuledynmod
。只需使用原理图(而不是)为其搭建脚手架,它就会在您现有的项目中dynpkg
生成一个新的动态模块,完全准备好让您实现和静态方法的细节。register()
registerAsync()
这与用于添加控制器、服务和模块等元素的常规 CLI 命令完全相同。事实上,它的行为与非常并行。它在项目内的文件夹中构建动态模块及其所有部分(看起来与我们在本文中看到的非常相似,减去顶级 Nest 应用程序组件),然后将它们连接到项目的其余部分,就像普通原理图一样。在@nestjsplus/dyn-schematics的 github 页面上阅读更多相关信息。这里介绍了这个用例(向现有项目添加动态模块) 。您可以使用以下命令在现有 Nest 项目中轻松进行快速运行:nest generate module
module
nest g -c @nestjsplus/dyn-schematics dynmod myNewModule --dry-run
结论
如果您一直在关注本系列,那么您之前已经学习了一种强大的架构模式,用于构建模块化的 NestJS 服务和模块,并将它们组合到应用程序中。现在,借助 Schematics 的强大功能,您可以自动生成样板代码来实现这种强大的模式。您可以使用此 Schematics 自定义内部模块,或轻松集成外部 API。在后续文章中,我将向您展示如何构建自己的 Schematics,以进一步扩展 NestJS 和 NestJS CLI 的功能!
欢迎在下方评论区提问、评论或建议,或者直接打个招呼。欢迎加入我们的Discord,一起愉快地讨论 NestJS。我在那里的用户名是Y Prospect。