服务端框架NestJS【学习笔记】

294 阅读4分钟

1.第一天

2021051909454693转存失败,建议直接上传图片文件

2.第二天

安装webpack

参考官方文档Hot reload | NestJS - A progressive Node.js framework

npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack

1.创建 webpack-hmr.config.js 文件

复制粘贴

const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

module.exports = function (options, webpack) {
  return {
    ...options,
    entry: ['webpack/hot/poll?100', options.entry],
    externals: [
      nodeExternals({
        allowlist: ['webpack/hot/poll?100'],
      }),
    ],
    plugins: [
      ...options.plugins,
      new webpack.HotModuleReplacementPlugin(),
      new webpack.WatchIgnorePlugin({
        paths: [/\.js$/, /\.d\.ts$/],
      }),
      new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }),
    ],
  };
};
  1. mian.js 导入
if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }

这一段会报错,所以需要适配ts:

npm i @type/webpack

3.在package.json中,切换启动命令

"start:dev": "webpack --config webpack.config.js --watch"

4.运行:

 npm run start:dev

5.命令行会报错解决:

​ 创建webpack.config.js文件

/* eslint-disable @typescript-eslint/no-var-requires */
const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

module.exports = {
  entry: ['webpack/hot/poll?100', './src/main.ts'],
  target: 'node',
  externals: [
    nodeExternals({
      allowlist: ['webpack/hot/poll?100'],
    }),
  ],
  module: {
    rules: [
      {
        test: /.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  mode: 'development',
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new RunScriptWebpackPlugin({ name: 'server.js', autoRestart: false }),
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'server.js',
  },
};

这里文件路径会报错,直接右键忽略就好了。

配置VSCode代码调试配置

1.跳转到调试界面,选择个启动进程,建议node.js。随后会跳转到 launch.json 页面。

删除configuration下的配置,随后输入npm,选择 在Node.js中运行npm

{
  // 使用 IntelliSense 了解相关属性。
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch via NPM",
      "request": "launch",
      "runtimeArgs": ["run-script", "start:debug"],
      "runtimeExecutable": "npm",
      "console": "integratedTerminal",// 同时输出到"调试控制台"和软件内置“终端”,建议在终端输入,在调试控制台查看结果
      "skipFiles": ["<node_internals>/**"],
      "type": "node"
    }
  ]
}

这里把debug换成了nest的start:debug

Nest Debug做了什么

​ 传递给了Node一个 **--inspect **这个参数

​ 可以直接看package的test:debug

    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
	

    "start:debug": "nest start --debug --watch",

在浏览器中调试

运行debug模式

npm run start:debug

打开后可以在浏览器打开调试进程

TypeScript学习

详见 环境配置 | 大前端 - 前端高级进阶 (toimc.com)

2022 typescript史上最强学习入门文章(2w字) - 掘金 (juejin.cn)

require和import的区别

  1. 导入require 导出 exports/module.exportsCommonJS 的标准,通常适用范围如 Node.js
  2. import/exportES6 的标准,通常适用范围如 React
  3. require赋值过程并且是运行时才执行,也就是同步加载
  4. require 可以理解为一个全局方法,因为它是一个方法所以意味着可以在任何地方执行。
  5. import解构过程并且是编译时执行,理解为异步加载
  6. import 会提升到整个模块的头部,具有置顶性,但是建议写在文件的顶部。

commonjs 输出的,是一个值的拷贝,而es6输出的是值的引用;

commonjs 是运行时加载,es6是编译时输出接口;

require和import的性能

require 的性能相对于 import 稍低。

因为 require 是在运行时才引入模块并且还赋值给某个变量,而 import 只需要依据 import 中的接口在编译时引入指定模块所以性能稍高

3、第三天

编程设计思想

OOP (面向对象式编程Obeject Oriented Programming) VS FP(函数式编程Functinal Programming)

OOP

示例:确定的输入输出;没有副作用,相对独立;

AOP(面向切面编程)

IOC(控制反转):

​ 是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

DI(依赖注入):是IOC的具体实现

允许在类外创建依赖对象,并通过不同方式将这些对象提供给类

方法/属性对另一函数/属性****有强依赖关系,比如说:Student类的play方法对iPhone类有强依赖关系。

class iPhone{
	playGame(name:string){
		console.log(`${name}` play game);
	}
}


class Student{
    constructor(){}
    play(){
        const iphone=new iPhone();
        iphone.playGame(this.name)//强依赖
    }
}

问题:1.当iPhone类发生变化,则Student类也会发生变化。

2.当Student用的不是iPhone手机,则,修改会很麻烦。

所以希望iPhone与Student解耦

解决方法:将iPhone作为参数,传递给Student

interface Phone{
    palyGame:(name:string)=void
}//接口限定一定会有palyGame方法;
 //安卓类
class Andriod emplements Phone{
    playGame(name:string){
        console.log(`${name}` play game);
    }
}
class DIStudent{
     constructor(privid name:string,private phone:Phone){
         this.phone=phone;
         this.name=name;
     }
    play(){
        this.phone.playGame(this.name);
    }
 }

//示例
const student1=new DIStudent("zxs",new Audroid());
student1.play();
const student2=new DIStudent("zxs1",new iPhone());
student2.play();

Nestjs核心概念

客户端---控制器---服务层---数据层

Nestjs 的生命周期

客户端---中间件---守卫---拦截器---管道---控制器---服务---拦截器---过滤器---响应回客户端

Snipaste_2022-12-28_19-38-03转存失败,建议直接上传图片文件

Nestjs 用模块来组织代码

@Module装饰器来描述模块

模块有四大属性:imports,providers,controllers,exports.

功能(共享)模块

全局模块:通常应用在配置,数据库连接,日志上

动态模块:使用到该模块时才初始化(类似于懒加载)

MVC (模型Model视图View控制器Controller)

​ MVC 是一种软件构建模式

​ 事件-->控制器-->模型-->视图

DTO(Data Transfer Obeject数据传输对象),DAO(Data Access Object数据访问对象)

​ 请求——》DTO(接收部分数据,对数据进行筛选)《——》逻辑《——》DAO(对接数据库接口,不暴露数据库内部信息)——》数据库

示例:image-20221228203340061转存失败,建议直接上传图片文件

Snipaste_2022-12-28_20-34-29转存失败,建议直接上传图片文件

通用后端框架

接口:

image-20221228205328544转存失败,建议直接上传图片文件

核心概念:

Snipaste_2022-12-28_20-52-11转存失败,建议直接上传图片文件

多环境配置

1.@nestjs/config(底层是dotenv)2.js-yaml3.config4.@nestjs-config
.envconfig.yamconfig.json

dotenv:将环境变量从 .env 文件加载到中 process.env

1.安装

npm install dotenv --save-dev

2.配置.env文件

# DEV 环境变量配置

# 环境名
ENV=dev

# 接口域名
API_HOST=https://api.dev.com

# 站点域名
SITE_HOST=https://dev.com
npm i cross-env
"dev":

①官方配置环境

1.安装

npm i --save @nestjs/config

2.导入

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';//导入
import * as dotenv from 'dotenv';
const envFilePath = `.env.${process.env.NODE_ENV || 'development'}`;//判断不同环境
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: envFilePath, //指定环境配置文件
      load: [() => dotenv.config({ path: '.env' })],//设置公共环境配置文件
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}


公共环境配置:

开发时经常会出现公共环境配置,此时需要使用dotenv

import * as dotenv from 'dotenv';//导入

在Module层 ConfigModule 使用load配置公共环境配置

 ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: envFilePath, //指定环境配置文件
      load: [() => dotenv.config({ path: '.env' })],//设置公共环境配置文件
    }),

②配置遍历名如果有长变量,想使它不冲突:

1.安装

npm i js-yaml

2.安装类型声明文件

npm i -D @types/js-yaml

3.在项目根目录新建 Config 文件夹,并在其下面创建 config.yml, config.development.yml, config.production.yml文件,里面写环境配置

  1. /src目录下创建configuration.ts文件

    记得安装lodash

    import { readFileSync } from 'fs';
    import * as yaml from 'js-yaml';
    import { join } from 'path';
    import * as lodash from 'lodash';
    const YAML_COMMON_CONFIG_FILENAME = 'config.yaml'; //默认配置文件
    const filePath = join(__dirname, '../', YAML_COMMON_CONFIG_FILENAME); //默认配置文件路径
    const envPath = join(
      __dirname,
      '../config',
      `config.${process.env.NODE_ENV || 'development'}.yml`,
    ); //开发、生产环境配置文件的路径
    const commonConfig = yaml.load(readFileSync(filePath, 'utf8'));
    const envConfig = yaml.load(readFileSync(envPath, 'utf8'));
    //因为configModule有一个load函数
    export default () => {
      return lodash.merge(commonConfig, envConfig); //合并配置文件
    };
    
    
  2. 在configModule 的load函数中导入

     load: [configuration]
    
  3. config.yml是默认环境配置文件,其他的顾名思义

③第三方库解析json文件配置环境:config

1.安装

npm install config

2.在项目根目录创建config/default.json,config/production.json,config/development.json文件

配置文件的参数验证:Joi

npm i --save joi
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import * as dotenv from 'dotenv';
import * as Joi from 'joi';
import configuration from './configuration';
const envFilePath = `.env.${process.env.NODE_ENV || 'development'}`;
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: envFilePath, //指定环境配置文件
      // load: [() => dotenv.config({ path: '.env' })],
      load: [configuration],
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production')
          .default('development'),
        DB_PORT: Joi.number().default(3306),
        DB_URL: Joi.string(),
        DB_HOST: Joi.string().ip(),//限定为ip格式
      }), //配置参数验证
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

其他规则请看:joi.dev - 17.7.0 API Reference

命令行传参与配置模块相结合

环境变量都在process.env

4、第四天

使用TypeORM

1.安装

npm i -S @nestjs/typeorm typeorm mysql2

app.module.ts里配置

TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: '2221754815',
      database: 'nest',
      entities: [],
      //同步本地的schema与数据库->初始化时使用
      synchronize: true,
      //日志等级
      logging: ['error'],
    }),

此时,数据库配置是写死的,不能适配多环境。应该引用在.env.xxx里的配置。

src/enum/config.enum.ts里设置环境映射

export enum ConfigEnum {
  DB_DATABASE = 'DB_DATABASE',
  DB_TYPE = 'DB_TYPE',
  DB_HOST = 'DB_HOST',
  DB_PASSWORD = 'DB_PASSWORD',
  DB_USERNAME = 'DB_USERNAME',
  DB_SYNC = 'DB_SYNC',
  DB_PORT = 'DB_PORT',
}

sec/app.module.ts

import { ConfigEnum } from './enum/config.enum';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as dotenv from 'dotenv';
import * as Joi from 'joi';
import configuration from './configuration';
import { TypeOrmModule } from '@nestjs/typeorm';
const envFilePath = `.env.${process.env.NODE_ENV || 'development'}`;
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: envFilePath, //指定环境配置文件
      load: [() => dotenv.config({ path: '.env' })],
      // load: [configuration],
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production')
          .default('development'),
        DB_PORT: Joi.number().default(3306),
        DB_URL: Joi.string(),
        DB_HOST: Joi.string().ip(),
      }), //配置参数验证
    }),
    // TypeOrmModule.forRoot({
    //   type: 'mysql',
    //   host: 'localhost',
    //   port: 3306,
    //   username: 'root',
    //   password: '2221754815',
    //   database: 'nest',
    //   entities: [],
    //   //同步本地的schema与数据库->初始化时使用
    //   synchronize: true,
    //   //日志等级
    //   logging: ['error'],
    // }),
    //适配多环境
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (ConfigService: ConfigService) => ({
        type: ConfigService.get(ConfigEnum.DB_TYPE),
        host: ConfigService.get(ConfigEnum.DB_HOST),
        port: ConfigService.get(ConfigEnum.DB_PORT),
        username: ConfigService.get(ConfigEnum.DB_USERNAME),
        password: ConfigService.get(ConfigEnum.DB_PASSWORD),
        database: ConfigService.get(ConfigEnum.DB_DATABASE),
        entities: [],
        //同步本地的schema与数据库->初始化时使用
        synchronize: ConfigService.get(ConfigEnum.DB_SYNC),
        //日志等级
        logging: ['error'],
      }),
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

注意,当type:"mysql"或其他写死的字符串时,useFactory不会报错,但当type:

ConfigService.get(ConfigEnum.DB_TYPE)写活时,useFactory会报错。

所以要对配置文件加上校验,并对配置对象进行类型断言

validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production')
          .default('development'),
        DB_PORT: Joi.number().default(3306),
        // DB_URL: Joi.string().domain(),
        DB_HOST: Joi.string().ip(),
        DB_DATABASE: Joi.string().required(),
        DB_USERNAME: Joi.string().required(),
        DB_PASSWORD: Joi.string().required(),
        DB_TYPE: Joi.string().valid('mysql', 'postgres'),
        DB_SYNC: Joi.boolean().default(false),
      }), //配置参数验证
    
    
    
    
    
    useFactory: (ConfigService: ConfigService) =>
        ({
          type: ConfigService.get(ConfigEnum.DB_TYPE),
          host: ConfigService.get(ConfigEnum.DB_HOST),
          port: ConfigService.get(ConfigEnum.DB_PORT),
          username: ConfigService.get(ConfigEnum.DB_USERNAME),
          password: ConfigService.get(ConfigEnum.DB_PASSWORD),
          database: ConfigService.get(ConfigEnum.DB_DATABASE),
          entities: [],
          //同步本地的schema与数据库->初始化时使用
          synchronize: ConfigService.get(ConfigEnum.DB_SYNC),
          //日志等级
          logging: ['error'],
        } as TypeOrmModuleOptions),//类型断言,记得引入该类型

如果使用moongdb建议使用mongooseORM库

数据库设计

ER图

image-20221229162418806转存失败,建议直接上传图片文件

三大范式

第一范式:

第二范式:

第三范式:

ORM创建实体、关系

创建 profile.entity.ts 文件以此创建profile实体

import {
  Column,
  Entity,
  JoinColumn,
  OneToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Profile { //类名要一致
  @PrimaryGeneratedColumn() //声明主键
  id: number;
  @Column() //声明列
  gender: number;
  @Column()
  photo: string;
  @Column()
  address: string;
  //一对一
  @OneToOne(() => User) //与User实体建立连接
  @JoinColumn() //添加外键user对应User的id。可以自定义名字@JoinColumn("user_id")
  user: User;
}

useFactory 中导入

entities: [User, Logs, Profile, Roles],

创建一对多实体

//user.entity.ts
//ts->数据库 关联关系 Mapping
	//一对多
  @OneToMany(() => Logs, (logs) => logs.user)
  logs: Logs[];

不要忘记在logs实体中添加多对一实体

//log.entity.ts
@ManyToOne(() => User , (user) => user.logs)
  @JoinColumn()
  user: User;

()=>User表示返回的数据类型

​ (logs)=>logs.user表示这个属性如何查询出来,如 logs.user属性代表这logs表中的user字段,而logs表中的user字段又连接了User,所以将两个字段连接了起来

​ @JoinColumn表示在哪个表里建立对应关联关系,上面为User表

创建多对多实体

//user.entity.ts
@ManyToMany(() => Roles, (roles) => roles.user)
  roles: Roles;
//roles.entity.ts
@ManyToMany(() => User, (user) => user.roles)
  user: User;

建立中间表

//user
@ManyToMany(() => Roles, (roles) => roles.user)
  @JoinTable({ name: 'users_roles' })
  roles: Roles;

项目已有数据库,逆向生成实体类

npm i typeorm-model-generator
//package.json

"generator:models":"typeorm-model-generator -h localhost -p 3306 -d nest -u root -x 2221754815 -e mysql -o ./src/entities"
//注意修改对应数据库配置信息
./src/entities 是输出目录
-x 密码
-d 数据库名
-e 数据库类型

在appMoudule里导入TypeOrmModule里的东西


ORM增删改查

DI容器工作原理

image-20221229164022289转存失败,建议直接上传图片文件

TypeORM工作原理

Snipaste_2022-12-29_16-40-49转存失败,建议直接上传图片文件

TypeORM Moudule与TypeORM Repository被高度封装,所以我们看不到他们里面在做什么,我们只能看到他外面暴露出来的Repostiory实例与**Service实例,**直接拿来用。

实例:

//user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private readonly UserRepository: Repository<User>,
  ) {} //构造函数里注册实体类
  async getUser() {
    const res = await this.UserRepository.find();
    return {};
  }
  addUser() {
    return {};
  }
}

注意,一定要到对应的module里导入typeORM

//user.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([User])], //导入
  providers: [UserService],
  controllers: [UserController],
})

CURD:

//user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private readonly UserRepository: Repository<User>,
  ) {}
  async getUser() {
    const res = await this.UserRepository.find();
    return {};
  }
  findAll() {
    return this.UserRepository.find();
  }
  find(username: string) {
    return this.UserRepository.findOne({ where: { username: username } });
  }
  async create(user: User) {
    const userTemp = await this.UserRepository.create(user);
    return this.UserRepository.save(userTemp);
  }
  update(id: number, user: Partial<User>) {
    //Partial:ts操作符 获取后面实体类的属性,并告诉程序,有些属性没有
    return this.UserRepository.update(id, user);
  }
  remove(id: number) {
    return this.UserRepository.delete(id);
  }
}

多表联合查询

////user.service.ts
findProfile(id: number) {
    return this.UserRepository.findOne({
      where: {
        id,
      },
      relations: {
        Profile: true,// 外连接Profile表      
      },
    });
  }
//user.entity.ts
 @OneToOne(() => Profile, (profile) => profile.user)
  Profile: Profile;

用Query Builder查询

使用 Query Builder 查询 | TypeORM 中文文档 | TypeORM 中文网 (bootcss.com)

示例:

//service
async findLogsByGroup(id: number) {
    return this.LogsRepository.createQueryBuilder('logs')
      .select('logs.result')
      .addSelect("COUNT('logs.result')",'count')
      .leftJoinAndSelect('logs.user', 'users')
      .where('user.id=:id', { id })
      .groupBy('logs.result')
      .getRawMany();
  }
 //controller
 @Get('/logsByGroup')
  async getLogsByGroup(): Promise<any> {
    const res = await this.userService.findLogsByGroup(2);
    return res.map((item) => ({
      result: item.result,
      count: item.count,
    }));
  }

相当于SQL语句:

SELECT logs.result result,COUNT(logs.user) users
  FROM logs,user
  WHERE user.id = logs.userId AND user.id = 2
  GROUP BY logs.result;

写原生语句:

LogsRepository.query("SELECT * FORM logs");

不要写原生语句,容易SQL注入,不要自大的认为你能防住SQL注入

日志

日志等级:

Log:通用日志
Warning:警告日志
Error:严重日志
Debug:调试日志
Verbose:详细日志

日志按功能分类:

  1. 错误日志
  2. 调试日志
  3. 请求日志

日志记录位置

​ 控制台

​ 文件

​ 数据库

Snipaste_2022-12-29_21-05-06转存失败,建议直接上传图片文件

TypeORM日志在TypeOrmModule配置里的logging选项

//  main.js
const app = await NestFactory.create(AppModule, {
    //关闭整个nest日志
    //logger:false
    //不写则为true
    logger: ['error', 'warn'],
  });

使用logger

//controller
private logger = new Logger(UserController.name); //在类中示例化Logger
this.logger.log(`请求成功`);

pino日志库

NestJS-pino - npm (npmjs.com)

npm install nestjs-pino

userModule导入

@Module({
  imports: [TypeOrmModule.forFeature([User]), LoggerModule.forRoot()], //导入
  providers: [UserService],
  controllers: [UserController],
})
//官方示例
import { NestFactory } from '@nestjs/core'
import { Controller, Get, Module } from '@nestjs/common'
import { LoggerModule, Logger } from 'nestjs-pino'

@Controller()
export class AppController {
  constructor(private readonly logger: Logger) {}

  @Get()
  getHello() {
    this.logger.log('something')
    return `Hello world`
  }
}

@Module({
  controllers: [AppController],
  imports: [LoggerModule.forRoot()]
})
class MyModule {}

async function bootstrap() {
  const app = await NestFactory.create(MyModule)
  await app.listen(3000)
}
bootstrap()

因为pino默认打印的日志很丑,所以需要美化

npm i pino-prtty
//app.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    LoggerModule.forRoot({
      pinoHttp: {
        transport: {
          target: 'pino-pretty',
          options: {
            colorize: true,
          },
        },
      },
    }),
  ], //导入
  providers: [UserService],
  controllers: [UserController],
})

一般只在开发环境使用pino-pretty,开发生产环境用pino-roll(每天自动把文件信息滚动出来)

import { Options } from './../../node_modules/pino-http/index.d';
import { transport } from './../../node_modules/pino/pino.d';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { User } from './user.entity';
import { Logs } from 'src/logs/logs.entity';
import { LoggerModule } from 'nestjs-pino';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    LoggerModule.forRoot({
      pinoHttp: {
        transport:
          process.env.NODE_ENV === 'development'
            ? {
                target: 'pino-pretty',
                options: {
                  colorize: true,
                },
              }
            : {
                target: 'pino-roll',
                options: {
                  file: 'log.txt',
                  frequency: 'daily',
                  size:"10m",
                  mkdir: true,
                },
              },
      },
    }),
  ], //导入
  providers: [UserService],
  controllers: [UserController],
})
export class UserModule {}

可以配置到appModule中

LoggerModule.forRoot({
      pinoHttp: {
        transport: {
          targets: [
            process.env.NODE_ENV === 'development'
              ? {
                  level: 'info',
                  target: 'pino-pretty',
                  options: {
                    colorize: true,
                  },
                }
              : {
                  level: 'info',
                  target: 'pino-roll',
                  options: {
                    file: 'log.txt',
                    frequency: 'daily',
                    size: '10m',
                    mkdir: true,
                  },
                },
          ],
        },
      },
    }),

Winston日志库

@nest-winston

nest-winston - npm (npmjs.com)

npm install --save nest-winston winston
//main.ts
async function bootstrap() {
  const instance = createLogger({ //winston配置信息
    transports: [
      new winston.transports.Console({
        format: winston.format.combine(
          winston.format.timestamp(),
          utilities.format.nestLike(),
        ),
      }),
    ],
  });
  const app = await NestFactory.create(AppModule, {
    //关闭整个nest日志
    //logger:false
    // logger: ['error', 'warn'],
    // logger: true,
    logger: WinstonModule.createLogger({ instance }),//应用配置
  });
  await app.listen(3000);
  Logger.warn('App 运行在 3000');
  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}

注册全局实例

//appModule
@Global()//全局注册里面的模块
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: envFilePath, //指定环境配置文件
      load: [() => dotenv.config({ path: '.env' })],
      // load: [configuration],
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production')
          .default('development'),
        DB_PORT: Joi.number().default(3306),
        // DB_URL: Joi.string().domain(),
        DB_HOST: Joi.string().ip(),
        DB_DATABASE: Joi.string().required(),
        DB_USERNAME: Joi.string().required(),
        DB_PASSWORD: Joi.string().required(),
        DB_TYPE: Joi.string().valid('mysql', 'postgres'),
        DB_SYNC: Joi.boolean().default(false),
      }), //配置参数验证
    }),
    // TypeOrmModule.forRoot({
    //   type: 'mysql',
    //   host: 'localhost',
    //   port: 3306,
    //   username: 'root',
    //   password: '2221754815',
    //   database: 'nest',
    //   entities: [],
    //   //同步本地的schema与数据库->初始化时使用
    //   synchronize: true,
    //   //日志等级
    //   logging: ['error'],
    // }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (ConfigService: ConfigService) =>
        ({
          type: ConfigService.get(ConfigEnum.DB_TYPE),
          host: ConfigService.get(ConfigEnum.DB_HOST),
          port: ConfigService.get(ConfigEnum.DB_PORT),
          username: ConfigService.get(ConfigEnum.DB_USERNAME),
          password: ConfigService.get(ConfigEnum.DB_PASSWORD),
          database: ConfigService.get(ConfigEnum.DB_DATABASE),
          entities: [User, Logs, Profile, Roles],
          //同步本地的schema与数据库->初始化时使用
          synchronize: ConfigService.get(ConfigEnum.DB_SYNC),
          //日志等级
          // logging: ['error'],
          logging: true, //开发环境一般设置为true
        } as TypeOrmModuleOptions),
    }),
    UserModule,
    // LogsModule,
    LoggerModule.forRoot({
      pinoHttp: {
        transport: {
          targets: [
            process.env.NODE_ENV === 'development'
              ? {
                  level: 'info',
                  target: 'pino-pretty',
                  options: {
                    colorize: true,
                  },
                }
              : {
                  level: 'info',
                  target: 'pino-roll',
                  options: {
                    file: 'log.txt',
                    frequency: 'daily',
                    size: '10m',
                    mkdir: true,
                  },
                },
          ],
        },
      },
    }),
  ],
  controllers: [UserController],
  providers: [UserService, Logger],//实例化
  exports: [Logger],//导出,设置共享模块
})

@Golbal注册全局模块

winston滚动日志

winston-daily-rotate-file

npm i winston-daily-rotate-file

滚动日志配置信息

const instance = createLogger({
    transports: [
      new winston.transports.Console({
        format: winston.format.combine(
          winston.format.timestamp(),
          utilities.format.nestLike(),
        ),
      }),
      new winston.transports.DailyRotateFile({
        dirname: 'logs',
        filename: 'application-%DATE%.log', //日志名称
        datePattern: 'YYYY-MM-DD-HH',
        zippedArchive: true, //压缩文件
        maxSize: '20m', //文件最大容积
        maxFiles: '14d', //超过5天删除
      }),
    ],
  });

全局异常过滤器

Snipaste_2022-12-30_00-40-00转存失败,建议直接上传图片文件

Nestjs有内置HTTP异常类和异常过滤器

throw new UnauthorizeException("用户无权限");

此时,响应自动设置响应为:

{
	"starusCode":401,
    "message":"用户无权限",
    "error":"Unauthorize"
}

定义过滤器

// src/filters/http-exception-filters
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';
import { LoggerService } from '@nestjs/common/services';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(private logger: LoggerService) {}
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); //程序上下文
    //响应,请求对象
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    //http状态码
    const status = exception.getStatus();
    this.logger.error(exception.message, exception.stack);//记录日志
    response.status(status).json({
      code: status,
      timeStamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      message: exception.message || exception.name,
    });
  }
}

main.ts使用全局Http异常过滤器

const Logger = WinstonModule.createLogger({ instance });
app.useGlobalFilters(new HttpExceptionFilter(Logger)); //应用全局异常过滤器
await app.listen(3000);

注意:全局Exception过滤器只能有一个

全局异常捕获

@catch()

//all-exception-filter
import { LoggerService } from '@nestjs/common/services';
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpAdapterHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import * as requestIp from 'request-ip';
@Catch()
export class AllExceptionFilter implements ExceptionFilter {
  constructor(
    private logger: LoggerService,
    private httpAdapterHost: HttpAdapterHost,
  ) {}
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();
    const response = request.getResponse();
    const { httpAdapter } = this.httpAdapterHost;
    const status =
      exception instanceof HttpException
        ? exception.getStatus
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const responseBody = {
      headers: request.headers,
      query: request.query,
      body: response.body,
      params: response.params,
      timeStamp: new Date().toISOString,
      //用户ip信息
      ip: requestIp.getClientIp(request),
      exception: exception['name'],
      error: exception['response'] || '网络服务错误',
    };
    //将响应体记录到日志
    this.logger.error('[zxs]', responseBody);
    httpAdapter.reply(response, responseBody, status);
  }
}

日志配置重构

将main.ts日志配置抽离到logs.module.ts

.env添加log多环境配置

// .env添加
LOG_ON=true
LOG_LEVEL=info

添加env映射

//enum 
export enum LogEnum {
  LOG_LEVEL = 'LOG_LEVEL',
  LOG_ON = 'LOG_ON',
}

//logs.module.ts
import { LogEnum } from './../enum/config.enum';
import { ConfigService } from '@nestjs/config';
import { WinstonModule } from 'nest-winston/dist/winston.module';
import { Module } from '@nestjs/common';

import { utilities, WinstonModuleOptions } from 'nest-winston';
import * as winston from 'winston';
import 'winston-daily-rotate-file';
import { Console } from 'winston/lib/winston/transports';
import DailyRotateFile from 'winston-daily-rotate-file';

const configService = new ConfigService();
//控制台日志
const consoleTransports = new Console({
  format: winston.format.combine(
    winston.format.timestamp(),
    utilities.format.nestLike(),
  ),
});
//文本日志
const dailyTransports = new DailyRotateFile({
  level: 'warn',
  dirname: 'logs',
  filename: 'application-%DATE%.log', //日志名称
  datePattern: 'YYYY-MM-DD-HH',
  zippedArchive: true, //压缩文件
  maxSize: '20m', //文件最大容积
  maxFiles: '14d', //超过5天删除
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.simple(),
  ),
});
const dailyInfoTransports = new DailyRotateFile({
  level: configService.get(LogEnum.LOG_LEVEL),
  dirname: 'logs',
  filename: 'application-%DATE%.log', //日志名称
  datePattern: 'YYYY-MM-DD-HH',
  zippedArchive: true, //压缩文件
  maxSize: '20m', //文件最大容积
  maxFiles: '14d', //超过5天删除
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.simple(),
  ),
});

@Module({
  imports: [
    WinstonModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) =>
        ({
          transports: [
            consoleTransports,
            ...(configService.get(LogEnum.LOG_ON)
              ? [dailyInfoTransports, dailyTransports]
              : []),
          ],
        } as WinstonModuleOptions),
    }),
  ],
})
export class LogsModule {}

//main.ts
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));使用Winston日志


  await app.listen(3000);

具体使用方法

构造函数声明@Inject

 //user.controller.ts
 constructor(
    private userService: UserService,
    private configService: ConfigService,
    @Inject(WINSTON_MODULE_NEST_PROVIDER) 
    private logger: LoggerService,
  ) {}

重构数据库模块

npm install ts-node --save-dev

在 package.json 中的 scripts 下添加 typeorm 命令

"script" {
    ...
    "typeorm": "typeorm-ts-node-commonjs"
}

然后运行如下命令:

npm run typeorm migration:run

根目录新建ormconfig.ts

webpack --config webpack.config.js

cross-env NODE_ENV=development nest start --watch

前端:vite+vue3+pinia

不多bb了都会

有个问题是routes里不能用@引入文件

解决方法:

//vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";

function pathResolver(dir: string) {
	return resolve(process.cwd(), ".", dir);
}

// https://vitejs.dev/config/
export default defineConfig({
	plugins: [vue()],
	resolve: {
		alias: {
			"@": pathResolver("src"),
		},
	},
});

npm -D @types/node
//ts.config.json
"include": [
		"src/**/*.ts",
		"src/**/*.d.ts",
		"src/**/*.tsx",
		"src/**/*.vue",
		"vite.config.ts"//要让ts解析vite.config
	],

但路径还是会保错

ts.config.json

		"baseUrl": ".",
		"paths": {
			"@/*": ["src/*"]
		}

vite跨域错误:设置代理

server: {
		port: 8888,
		proxy: {
			"/api": {
				target: "http://127.0.0.1:3000",
			},
		},
	},

集成bootstrap

Bootstrap & Vite · Bootstrap v5.2 (getbootstrap.com)

npm i --save bootstrap @popperjs/core

5.第五天

项目开发

为什么ts可以识别装饰器?

ts.config.ts

  "experimentalDecorators": true,//让ts认识注解写法

请求参数解析

Snipaste_2023-01-01_17-09-27转存失败,建议直接上传图片文件

注意如果有**":id"**,而还有同一方法如Get(),其路由可能匹配到了:id:上,建议其加上路由前缀,避免冲突

传参加上@Body注解 得到body,@Query 获得

query:?zxs=xxx

@Param("id")获取"/:id"

数据库查询

要对前端的参数进行约束

//dto/get-user-dto.ts
export interface getUserDto {
  page: number;
  limit?: number;
  username?: string;
  role?: number;
  gender?: number;
}

在控制层获取参数并调用服务层的函数获取数据返回

@Get()
  getUsers(@Query() query: getUserDto): any {
    //page -页码
    //limit 每页条数
    //condition 查询条件(username,role,gender,sort)
    //前端传递的参数全是string
    return this.userService.findAll(query);
  }

注意,前端传过来的参数全是string需要格式转换

//user.service.ts
findAll(query: getUserDto) {
    const { limit = 10, page, gender, username, role } = query;
    return this.UserRepository.find({
      select: {
        id: true,
        username: true,
        Profile: {
          gender: true,
        },
      },
      relations: {
        Profile: true,
        roles: true,
      },
      where: {
        username: username,
        Profile: {
          gender: gender,
        },
        roles: {
          id: role,
        },
      },
      take: limit,
      skip: (page - 1) * limit,
    });

Find 选项 | TypeORM 中文文档 | TypeORM 中文网 (bootcss.com)

QueryBuild查询



andWhere("profile.gender=:gender",{gender}).
andWhere("roles.id=:role",{role}).
getMany();

const queryBuilder=this.userRepository.
createQueryBuilder("user").
leftJoinAndSelect("user.profile","profile").
leftJoinAndSelect("user.roles","roles");
if(username){
    queryBuilder.where("user.username=:username",{username})
}
else{
    queryBuilder.where("user.username IS NOT NULL",{username}
}
return 

innerJoin与leftJoin,outJoin区别:

Snipaste_2023-01-01_21-02-34转存失败,建议直接上传图片文件

创建用户:

UNIQUE约束:

  @Column({ unique: true })
  username: string;

数据库异常处理:TypeORM

//main.ts
//设置全局异常过滤器
  const httpAdapter = app.get(HttpAdapterHost);
  const logger = new Logger();
  app.useGlobalFilters(new AllExceptionFilter(logger, httpAdapter));
//all-exception-filter.ts
import { LoggerService } from '@nestjs/common/services';
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpAdapterHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import * as requestIp from 'request-ip';
import { QueryFailedError } from 'typeorm';
@Catch()
export class AllExceptionFilter implements ExceptionFilter {
  constructor(
    private logger: LoggerService,
    private httpAdapterHost: HttpAdapterHost,
  ) {}
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();
    const response = request.getResponse();
    const { httpAdapter } = this.httpAdapterHost;
    const status =
      exception instanceof HttpException
        ? exception.getStatus
        : HttpStatus.INTERNAL_SERVER_ERROR;

    //加入更多异常处理逻辑
    let msg: string = exception['response'] || 'Internal Server Error';
    if (exception instanceof QueryFailedError) {
      msg = exception.message;
      if (exception.driverError.errno === 1062) {
        msg = '唯一索引冲突';
      }
    }

    const responseBody = {
      headers: request.headers,
      query: request.query,
      body: response.body,
      params: response.params,
      timeStamp: new Date().toISOString,
      //用户ip信息
      ip: requestIp.getClientIp(request),
      exception: exception['name'],
      error: exception['response'] || '网络服务错误',
    };
    //将响应体记录到日志
    this.logger.error('[zxs]', responseBody);
    httpAdapter.reply(response, responseBody, status);
  }
}

局部异常处理器

nest g f filters/typeorm --flat --no-spec

在fliters文件下创建typeorm.filter.ts过滤器

import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { TypeORMError, QueryFailedError } from 'typeorm';

@Catch(TypeORMError)
export class TypeormFilter implements ExceptionFilter {
  catch(exception: TypeORMError, host: ArgumentsHost) {
    console.log(exception);
    const ctx = host.switchToHttp(); //程序上下文
    //响应,请求对象
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    //http状态码
    let code = 500;
    if (exception instanceof QueryFailedError) {
      code = exception.driverError.errno;
    }
    response.status(500).json({
      code: code,
      timeStamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      message: exception.message,
    });
  }
}

在user控制层使用

@Controller('user')
@UseFilters(new TypeormFilter()) //处理模块错误
export class UserController {

controller名vs service名vs repository名

image-20230101215133752转存失败,建议直接上传图片文件

控制层 取名应更接近业务,语义化

服务层 取名

存储库 取名

image-20230101215341776转存失败,建议直接上传图片文件

一般service和repository在同一文件内

若service行数超过300行建议分离repositoory

typeorm里delete 与remove区别

image-20230101221032866转存失败,建议直接上传图片文件remove会触发typeorm钩子方法:因此可以在romove前,后执行操作

钩子方法:相当于触发器

//user.entity.ts
@AfterInsert()
  afterInsert() {
    console.log('afterInsert');
  }
  @AfterRemove()
  afterRemove() {
    console.log('afterRemove');
  }
//user.service.ts
async remove(id: number) {
    const user = await this.findById(id);
    return this.UserRepository.remove(user);
  }

remove方法才会触发afteremove方法

delete:通过id删除为硬删除,常用于删除不重要信息

多表联合更新

需要在实体上打开cascade

 //user.entity.ts
 @OneToOne(() => Profile, (profile) => profile.user, { cascade: true })
  Profile: Profile;
  • merge - 将多个实体合并为一个实体。
  • preload - 从给定的普通 javascript 对象创建一个新实体。 如果实体已存在于数据库中,则它将加载它(以及与之相关的所有内容),并将所有值替换为给定对象中的新值,并返回新实体。 新实体实际上是从数据库加载的所有属性都替换为新对象的实体。
//官网示例
const partialUser = {
  id: 1,
  firstName: "Rizzrak",
  profile: {
    id: 1
  }
};
const user = await repository.preload(partialUser);
// user将包含partialUser中具有partialUser属性值的所有缺失数据:
// { id: 1, firstName: "Rizzrak", lastName: "Saw", profile: { id: 1, ... } }

Repository API | TypeORM 中文文档 | TypeORM 中文网 (bootcss.com)

//user.service.ts
update(id: number, user: Partial<User>) {
    //Partial:ts操作符 获取后面实体类的属性,并告诉程序,有些属性没有
    const userTemp = this.findProfile(id);
    const newUser = this.UserRepository.merge(user, userTemp);
    //联合模型更新,需要使用save或queryBuilder
    return this.UserRepository.save(newUser);

    //下面方法不适合多表关联更新,只适合单表更新
    // return this.UserRepository.update(id, user);
  }

管道Pipe

管道的分类:

uTools_1678277046577转存失败,建议直接上传图片文件

NestJS中创建管道的过程

  1. 全局配置管道
  2. 创建class类,即Entity,dto
  3. 设置校验规则
  4. 使用该实体类或DTO

使用:

npm i --save class-validator class-transformer

main.ts

//全局拦截器
  app.useGlobalFilters(new AllExceptionFilter(logger, httpAdapter));
  app.useGlobalPipes(
    new ValidationPipe({
      //去除在类上不存在的字段
      whitelist: true,
    }),
  );

auth/sigin-user.dto.ts

import { IsString, IsNotEmpty, Length } from 'class-validator';

export class SignInUserDto {
  @IsString()
  @IsNotEmpty()
  @Length(6, 20, {
    //$value:当前用户传入的值
    //$property 当前属性名
    //$target 当前类
    //$constraint1:最小长度
    //$constraint2:最大长度
    message: `用户名长度要在$constrain1到$constraint2之间`,
  })
  username: string;

  @IsString()
  @IsNotEmpty()
  @Length(6, 32, {
    //$value:当前用户传入的值
    //$property 当前属性名
    //$target 当前类
    //$constraint1:最小长度
    //$constraint2:最大长度
    message: `用户名长度要在$constrain1到$constraint2之间`,
  })
  password: string;
}

敏感信息操作:加密,脱敏

argon2库:

npm i argon2

拦截器

Snipaste_2023-03-09_00-42-31转存失败,建议直接上传图片文件

store与localstorage结合:pinia-plugin-persistedstate

npm i pinia-plugin-persistedstate
const store = createPinia();
store.use(piniaPluginPersistedstate);
persist:true//开启持久化存储

测试

  • **单元测试(unit test):**测试的是局部函数的逻辑

    Jest,Vitest,mocha

  • **集成测试(e2e test):**测试的是整体流程 或者功能完整性

    Cypress,NightWare,Pactum

单元测试

Jest

npm i --save-dev jest

建议将测试文件(xxx.spec.ts)放入每个模块的___test__文件夹下

下横线是为了文件夹放在第一个显示。

测试示例:

describe('test jest hello word', () => {
  it('should be true', () => {
    expect(true).toBe(true);
  });
});

describe()是测试时,打印出来的描述

it()是测试用例,describe可以嵌套describe

Jest的配置

package.json

"jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",//指定根目录
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }

装饰器@

装饰器只是一个函数,他返回一个以target,key,desc为参数的函数

假如有一个类:Cat

class Cat{
    play(){
        return `${this.name} play;`
    }
}

此时play()函数会挂载到Cat.protootype上,大致如下:

Object.definProperty(Cat.prototype,'play',{
    value:spcifiedFunction,
    enumeravle:false,
    configurable:true,
    writable:true
}

三个参数的含义:

  1. obj/target : 需要定义属性的当前对象
  2. key/prop : 当前需要定义的属性名
  3. desc :属性描述符

我们可以尝试自定义一个@readonly装饰器来

function readonly(target, key, desc) {
  desc.writable = false;
  return desc;
}

自定义参数装饰器:

Nest.js中提供了自定义参数装饰器:用于装饰器接收传入的参数

createParamDecorator<string>((data, ctx) => ...)

其中data是装饰器中传入的参数:如 @User("aaa")中的aaa

ctx是应用执行上下文

聚合装饰器

applyDecorators 用于聚合多个装饰器

ExecutionContext:执行上下文类

export interface ExecutionContext extends ArgumentsHost {
  getClass<T>(): Type<T>;
  getHandler(): Function;
}

此类,有两个方法:

  • getClass:获取调用它的函数所在的类

  • getHandler :获取调用它的函数

例如:

class Person{
    create();
}

const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"

反射和元数据

@SetMetadata(key,value)相当于设置vue路由的meta:{}

参数为key:value形式

建议@SetMetadata(key,value)不要直接装饰方法,而是经过一层封装成装饰器

import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

上面代码中SetMetadata()设置了元数据,key为'roles',value由传参决定。经过这层封装后,方法就可以只要传入value了,限定了了key的类型。

使用:

@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

上述操作只是设置meta信息,而获取meta则需要Reflector辅助类

  1. 获取Class装饰器的元数据
const roles = this.reflector.get<string[]>('roles', context.getClass());

2.获取方法装饰器的元数据

const roles = this.reflector.get<string[]>('roles', context.getHandler());

3.当Class上和方法上都有相同key的元数据时

  1. 覆盖元数据: getAllAndOverride()
  2. 合并元数据 :getAllAndMerge()

基本原理 (nestjs.cn)