前言
这是一个新手全栈的阶段总结。
nestjs 根据请求流抽象代码
由于 nestjs 提供了守卫,拦截器,管道,过滤器,以便我们在请求响应前后对请求和相应进行特殊处理。这也对我们开发时如何抽象代码提出了问题。解决这一问题的关键有两点,一是明确请求流动顺序,二是明确各个功能对应的场景。
执行顺序
- 中间件(Middleware)
- 守卫(Guard)
- 拦截器(Intercaptor)
- 管道(Pipe)
- 控制器(Controller)
应用场景
通过这个执行顺序,在抽象代码时就可以思考某些逻辑是否可以不写在 service 中,而是提前或延后处理。比如,响应程序需要根据用户传入的用户 id 获取对应的用户实体,那么可以使用管道对用的 id 进行处理,在 controller 中直接获取到用户实体,方便处理。比如登陆逻辑,权限校验逻辑,可以放到守卫中执行。
nestjs 代码结构
nestjs 是从根模块开始,向下查找所有依赖,所以在划分代码结构的时候可以以模块维度划分。由于模块的概念相对较大,所以不建议划分的过细。一个模块中包含对应的 controller,一个 controller 中包含多个路由,在 controller 中可以通过依赖注入的方式使用 service。通常不建议在路由中写具体的逻辑代码,通过抽象出 service 层来处理具体逻辑。然后在 service 中获取数据实体并处理。
module -> controller -> service -> entity
对于模块和 controller 来说,只需要通过业务功能进行合适的划分即可,但是对于 service 和 entity 则会涉及到复用。所以不建议将 servie 放在 controller 目录下,而应放在一个单独目录,方便公用。
当然,对于复杂项目,还可以通过微服务进行拆分。
数据库
mysql
接入
在 nestjs 中,一般会通过 orm 来操作数据库,常用的有 TypeORM、Sequelize 等。
nestjs 提供了 TypeOrmModule 以便快速接入 typeorm。
对于自动运行迁移,建议直接关闭,以保证各个环境一致,为了方便也可以在开发环境打开。
import { TypeOrmModule } from '@nestjs/typeorm';
TypeOrmModule.forRootAsync({
inject: [ConfigurationService],
useFactory: (configurationService: ConfigurationService) => {
return {
type: 'mysql', // 连接数据库的类型
...configurationService.mysqlConfig,
timezone: '+08:00', // 设置时区
autoLoadEntities: true, // 自动加载所有的实体,一个实体对应一张表
synchronize: configurationService.synchronize, // 是否自动同步数据库和实体的表结构, 开发时为true,生产环境为false
logging: false, // 打印真正的sql语句
// 配置迁移
migrations: [__dirname + '/migrations/*{.ts,.js}'], // 迁移文件路径
migrationsRun: false, // 是否自动运行迁移(生产环境建议为false)
// 配置 CLI 路径(用于生成迁移)
cli: {
migrationsDir: 'src/migrations',
},
};
},
}),
TypeOrmModule.forFeature([
AdminUser,
Category,
Product,
Sku,
Inventory,
CUser,
Order,
OrderItem,
Withdrawal,
RenewalOrder,
Coupon,
UserCoupon,
Activity,
]),
使用
接入数据库后,只需要通过 @InjectRepository 装饰器将实体注入类的构造函数即可。
@Injectable()
export class OrderService extends MysqlBaseService<Order> {
constructor(
@InjectRepository(Order) protected repository: Repository<Order>, // 注入实体
) {
super(repository);
}
}
我们还可以定义一个几类,用来服用常用的数据库相关函数,比如 save、create 等。
export abstract class MysqlBaseService<T> {
constructor(protected repository: Repository<T>) {}
async findAll() {
return this.repository.find();
}
async find(options?: FindManyOptions<T>) {
return await this.repository.find(options);
}
async findOne(findOptions: FindOneOptions<T>) {
return await this.repository.findOne(findOptions);
}
async findBy(findOptions: FindOptionsWhere<T> | FindOptionsWhere<T>[]) {
return await this.repository.findBy(findOptions);
}
async create(createDto?: DeepPartial<T>) {
const entity = this.repository.create(createDto);
return await this.repository.save(entity);
}
async update(id: number, updateDto: QueryDeepPartialEntity<T>) {
return await this.repository.update(id, updateDto);
}
async delete(id: number) {
return await this.repository.delete(id);
}
}
redis
连接 reids 可以使用 ioredis 库。通过 ioredis 库提供的 Redis 类创建 redis 客户端。
我们可以创建一个 redisService 来使用。
@Injectable()
export class RedisService implements OnModuleDestroy {
private redisClient: Redis;
constructor(private configurationService: ConfigurationService) {
this.redisClient = new Redis({
host: this.configurationService.redisHost,
port: this.configurationService.redisPort,
password: this.configurationService.redisPassword,
});
}
onModuleDestroy() {
//当模块销毁的时候退出当前的客户端
this.redisClient.quit();
}
getClient() {
return this.redisClient;
}
async set(key: string, value: string, ttl?: number) {
if (ttl) {
await this.redisClient.set(key, value, 'EX', ttl);
} else {
await this.redisClient.set(key, value);
}
}
async get(key: string) {
return this.redisClient.get(key);
}
async del(key: string) {
await this.redisClient.del(key);
}
}
配置信息
开发中,我们会有一些配置信息,需要根据环境切换对应的配置。
在 nestjs 项目中,可以使用官方提供的 ConfigModule 和 ConfigService。
import { ConfigService, ConfigModule } from '@nestjs/config';
首先需要引入 ConfigModule 并进行对应配置,通常对环境配置切换的配置,以及设置全局。默认配置会加载 .env 文件,可以根据环境自己配置加载什么配置文件。也可以配合 docker 注入,就不用在这里配置。
ConfigModule.forRoot({
isGlobal: true, // 使配置项在整个应用中都可用
envFilePath: '.env', // 加载 .env 文件
}),
使用就很简单,我们可以自己创建一个 ConfigurationService ,并注入 ConfigService,在 ConfigurationService 进行 get 获取对应的配置,以便使用和处理。
打印日志信息
nestjs 官方提供了日志功能,但是不支持持久化,对生产环境不够用。所以可以使用 winston 库来打印需要持久化的日志信息。
首先,需要引入 WinstonModule 并进行配置。
WinstonModule.forRoot({
// options
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.ms(),
nestWinstonModuleUtilities.format.nestLike('MyApp', {
colors: true,
prettyPrint: true,
processId: true,
appName: true,
}),
),
}),
// other transports...
],
}),
然后,我们可以创建一个 WinstonLoggerService 来初始化,并且使用。我们还需要准备一个函数,判断持久化日志存储文件夹是否存在,不存在则创建。
@Injectable()
export class WinstonLoggerService implements LoggerService {
private readonly logger: winston.Logger;
constructor() {
// 创建 Daily Rotate File transport
const dailyRotateTransport = new winston.transports.DailyRotateFile({
filename: './logs/%DATE%-app.log', // 日志文件名
datePattern: 'YYYY-MM-DD', // 日志日期格式
zippedArchive: true, // 启用日志文件压缩
maxSize: '20m', // 每个日志文件最大大小
maxFiles: '7d', // 保留日志文件7天
level: 'info', // 设置最低日志级别
});
// 创建 Winston Logger
this.logger = winston.createLogger({
level: 'info', // 默认日志级别
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.printf(
({ timestamp, level, message }) =>
` ${timestamp} [${level}]: ${message}`,
),
),
transports: [
dailyRotateTransport, // 添加 Daily Rotate File 传输
new winston.transports.Console(), // 控制台输出
],
});
}
log(message: string) {
this.logger.info(message);
}
error(message: string, trace?: string) {
this.logger.error(message, trace);
}
warn(message: string) {
this.logger.warn(message);
}
debug(message: string) {
this.logger.debug(message);
}
verbose(message: string) {
this.logger.verbose(message);
}
}
结束
以上是对 nestjs 常用功能的总结,希望大神们如果有更好的方案可以多多指点。