项目搭建完成后根据我们的需求目前可以将功能模块大致分为用户、备忘录、配置、数据库模块(使用MongoDB),本章我们就来学习初始化这四个模块。
技术点
模块
在Nest中使用 @Module()
装饰器注解的类我们称之为模块,一般一组密切相关的功能被封装为一个模块,方便管理应用。
可以使用 CLI 命令快捷创建模块,
$ nest g module moduleName
在本需求中用户相关功能、备忘录相关功能、数据库链接我们就可以将其封装为不同的模块。
装饰器
上面模块的解释中我们提到了装饰器,这个在前端领域中还属于一种较新的概念。
ES2016中装饰器是一个返回函数的表达式。使用时需要添加 @
作为前缀,它可以接收目标对象、名称和属性描述符作为参数,并返回经过扩展功能后的同名函数。
简单示例,有一个基本 User 类,我们可以通过一个装饰器来实现让它的某个属性只读:
js
function readonly (target, key, descriptor){
descriptor.writable = false
return descriptor
}
class User (){
constructor(name){
this.name = name
}
@readly
hello(){
return `${this.name} say Hello!`
}
}
也可以通过一个装饰器实现对类的扩充:
js
function userAge (age){
return function(target){
target.age = age
}
}
@userAge(18)
class User (){
constructor(name){
this.name = name
}
}
Nest中我们如果自定义装饰器的需要访问上下文、支持依赖注入、类型安全与框架集成的话需要使用 createParamDecorator
方法进行包裹,比如我们定义一个装饰器从请求中获取用户信息
js
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
// 使用
@Get()
async findOne(@User() user: UserEntity) {
console.log(user);
}
@Module()
装饰器
@Module()
装饰器接收的参数是一个对象:
属性 | 描述 |
---|---|
providers | 将由 Nest 注入器实例化,且至少可在本模块内共享的提供者 |
controllers | 本模块中定义的需要实例化的控制器集合 |
imports | 导入模块的列表,这些模块导出了本模块所需的提供者 |
exports | 本模块提供的 providers 子集,这些提供者应可供导入本模块的其他模块使用。可以使用提供者本身或其令牌(provide 值) |
共享模块
在 Nest 中模块默认都是单例,在整个应用程序生命周期中每个模块只会被实例化一次,这样做可以的好处:
- 资源高效性。避免重复创建实例,减少内存开销。
- 状态一致性。
- 依赖安全。避免出现多个实例导致的依赖关系错乱。
通过在模块的 exports
数组中导出实例,模块一旦创建就可以自动成为共享模块,可以在多个模块中引入共享该模块的实例。
全局模块
如果需要在多个地方导入相同的模块集,重复 imports
就会显得繁琐,这时可以通过使用 @Global()
装饰器将模块设置为全局模块。
js
import { Module, Global } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Global()
@Module({
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
@Global()
装饰器使模块具有全局作用域,全局模块由根模块或核心模块仅注册一次,需要注入该服务的模块也无需在 imports 数组中重复导入。
动态模块
动态模块允许创建可在运行时配置的模块,在需要灵活、根据配置返回定制化模块时特别有用(如数据库不同环境配置)。
动态模块需要提供一个静态方法(通常叫做 register
或者 forRoot
)来返回一个符合 DynamicModule
接口的对象,使用时在 @Module()
装饰器的 imports
属性中调用。
register
通常表示该动态模块是在调用时使用,不同的调用配置可能不同。forRoot
通常表示希望一次性配置动态模块,并在多个地方复用该配置。
代码实现
配置模块初始化
为了区分开发、生产环境,我们需要通过不同的配置文件来管理不同的环境配置。
在 Node.js 的应用程序中通常使用 .env
文件设置不同环境的配置。
一、创建环境配置文件
在项目根目录创建:
.env.development
,对应开发环境配置.env.production
,对应生产环境配置
.env.development
文件内容:
bash
# 项目端口
PORT=9527
.env.production
文件内容:
bash
# 项目端口
PORT=9528
二、引入配置模块
Nest 提供了一个 @nestjs/config
包,可以通过它创建一个 ConfigModule
,并暴露一个加载相应 .env
文件的 ConfigService
。
ConfigModule
就是之前提过的动态模块,允许我们通过传递参数定制其内部逻辑。
接下来我们通过该包去创建配置模块。
安装依赖:
bash
$ npm i --save @nestjs/config
配置模块一般会在根模块 AppModule
引入,并使用静态方法 forRoot
来控制其行为,这个步骤中环境变量的键值对会被解析和处理。
js
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot({
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`
})],
})
export class AppModule {}
上述配置会从项目根目录加载并解析对应环境的 .env
文件,将其中的键值对与分配给 process.env
的环境变量合并,并将结果存储在一个可通过 ConfigService
访问的私有结构中。
forRoot
方法会注册 ConfigService
提供者,该提供者提供了用于读取配置变量的 get
方法。
当某个变量同时存在于运行时环境变量和
.env
文件中时,运行时环境变量具有优先权。
配置配置一般会在很多其他模块中使用,可以通过将 forRoot
参数的 isGlobal
属性设置为 true
,将其声明为全局模块。
js
ConfigModule.forRoot({
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
isGlobal: true
})
当其他模块使用时就无需再次通过 @Module
导入,只需从 @nestjs/config
引入 ConfigService
即可使用相关数据及功能(见后续数据库模块初始化中示例)。
三、调整 main.ts
使用配置文件变量
目前配置存储在服务中,main.ts
如果要访问配置,必须通过 app.get
方法获取服务使用:
ts
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const port = configService.get('PORT');
await app.listen(port ?? 3000);
}
bootstrap();
四、配置不同环境启动脚本
在 package.json
中配置不同环境的启动命令:
json
{
"scripts": {
"start": "NODE_ENV=production nest start",
"start:dev": "NODE_ENV=development nest start --watch",
}
}
数据库模块初始化
MongoDB 常用的连接器是 Mongoose
,Nest 提供了针对它的专用包 @nestjs/mongoose
。
一、下载依赖
首先安装依赖:
bash
$ npm i @nestjs/mongoose mongoose
二、环境变量增加数据库配置
设置不同环境的数据库配置,以开发环境举例:
bash
# 项目端口
PORT=9527
# MongoDB 配置
DB_HOST=localhost # 数据库地址
DB_PORT=27017 # 数据库端口
DB_NAME=doraemonNotebook # 数据库名称
DB_USER=developer # 数据库账号
DB_PASS=developerPass # 数据库密码
三、引入数据库模块初始化全局连接
导入 MongooseModule
到根模块:
js
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
}),
MongooseModule.forRootAsync({
useFactory: (configService: ConfigService) => {
return {
uri: `mongodb://${configService.get('DB_USER')}:${configService.get('DB_PASS')}@${configService.get('DB_HOST')}:${configService.get('DB_PORT')}/${configService.get('DB_NAME')}`,
};
},
inject: [ConfigService],
}),
]
})
export class AppModule {}
MongooseModule
也为动态模块。因为数据库的配置参数需要在
ConfigModule
中实例化.env
文件后才能配置到环境变量中,所以需要使用暴露出来的forRootAsnyc
异步方法来引入数据库模块,其提供了全局的连接配置。后续各个模块使用只需要通过
MongooseModule.forFeature
来关注自己的数据模型定义和使用。
用户模块初始化
一、创建模块
创建用户模块:
bash
$ nest g module user
Nest 会自动在项目
src
目录下创建user
文件夹及文件夹内的user.module.ts
文件,同时并自动在AppModule
中引入。
二、定义模型
根据领域就近原则我们在 user
目录下创建一个 schemas
目录及其下面的 user.schema.ts
文件,并定义以下内容:
js
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type UserDocument = HydratedDocument<User>;
@Schema({ timestamps: true }) // 自动添加 createdAt 和 updatedAt 字段
export class User {
@Prop({ required: true })
name: string;
@Prop({ required: true, unique: true }) // 定义必填及唯一索引
phone: number;
@Prop({ required: true })
password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
@Schema()
装饰器将类标记为模式定义。它将 User
类映射到同名的 MongoDB 集合,但会在末尾添加一个"s",Mongo 集合的名称将是 Users
。它接受一个可选参数,同 mongoose.Schema
类构造函数的第二个参数,详情参考。
@Prop()
装饰器定义文档属性。它也可以接受一个可选参数,同 mongoose.SchemaTypes
配置,详情参考。
三、注册模型
更新 UserModule
模块,注册 User
模型:
js
// user.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema, User } from './schemas/user.schema';
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
})
export class UserModule {}
forFeature
配置数据库模块,告诉 Nest
定义名为 User
的模型,该模型基于 UserSchema
定义。
注意:这里
User.name
指的是User
类的名称 'User',是类的静态属性,而不是使用@Prop()
装饰的 'name' 实例属性。
备忘录模块初始化
一、创建模块
创建用户模块:
bash
$ nest g module todo
二、定义模型
在 todo
目录下创建一个 schemas
目录及其下面的 todo.schema.ts
文件,并定义以下内容:
js
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
import type { ObjectId } from 'mongoose';
export type TodoDocument = HydratedDocument<Todo>;
@Schema({ timestamps: true })
export class Todo {
@Prop({ required: true })
title: string;
@Prop({ required: true })
content: string;
@Prop({ required: true, enum: ['pending', 'completed'], default: 'pending' })
status: string;
@Prop({ required: true })
userId: ObjectId;
}
export const TodoSchema = SchemaFactory.createForClass(Todo);
三、注册模型
更新 TodoModule
模块,注册 Todo
模型:
js
// todo.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TodoSchema, Todo } from './schemas/todo.schema';
@Module({
imports: [
MongooseModule.forFeature([{ name: Todo.name, schema: TodoSchema }]),
],
})
export class TodoModule {}