プロジェクトの作成
- インストール
yarn add -g @nestjs/cli nodemon ts-node
- 作成
nest new test 创建一个项目
nest 提示文件类型
nest g [文件类型][文件名] [文件目录(src目录下)] --no-spec
Prisma
データベースを操作するデータベース連携プラグイン- ファイル管理プラグインは
Dyno File Utils
マウスの使用を減らします - コード仕様
.prettierrc
{
"arrowParens": "always",
"bracketSameLine": true,
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 120,
"proseWrap": "never",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"singleAttributePerLine": false
}
- Nestjs のライフサイクル
nest.js请求生命周期大致如下:
收到请求
全局绑定的中间件
模块绑定的中间件
全局守卫
控制层守卫
路由守卫
全局拦截器(控制器之前)
控制器层拦截器 (控制器之前)
路由拦截器 (控制器之前)
全局管道
控制器管道
路由管道
路由参数管道
控制器(方法处理器) 15。服务(如果有)
路由拦截器(请求之后)
控制器拦截器 (请求之后)
全局拦截器 (请求之后)
异常过滤器 (路由,之后是控制器,之后是全局)
服务器响应
- ネストは生成されたファイルを作成します
│application │ application │ Generate a new application workspace │
│ class │ cl │ Generate a new class │
│ configuration │ config │ Generate a CLI configuration file │
│ controller │ co │ Generate a controller declaration │
│ decorator │ d │ Generate a custom decorator │
│ filter │ f │ Generate a filter declaration │
│ gateway │ ga │ Generate a gateway declaration │
│ guard │ gu │ Generate a guard declaration │
│ interceptor │ itc │ Generate an interceptor declaration │
│ interface │ itf │ Generate an interface │
│ middleware │ mi │ Generate a middleware declaration │
│ module │ mo │ Generate a module declaration │
│ pipe │ pi │ Generate a pipe declaration │
│ provider │ pr │ Generate a provider declaration │
│ resolver │ r │ Generate a GraphQL resolver declaration │
│ service │ s │ Generate a service declaration │
│ library │ lib │ Generate a new library within a monorepo │
│ sub-app │ app │ Generate a new application within a monorepo │
│ resource │ res │ Generate a new CRUD resource
1.モジュール、コントローラー、ルーティング、サービスコンセプト
等价于(饭店:老板,厨师,采购,服务员),(KTV:......)
客户——>服务员/路由——>控制器(点菜,拿水)————>厨师炒菜/服务(炒土豆)——>采购/服务(买菜)——>老板(报销)
直接调用是客户直接跟服务(服务员,厨师)交流
1. 服务调用服务
客户跟服务(服务员)交流(上菜),服务(服务员)跟服务(厨师)处理(炒菜)(服务中依赖注入)
2. 控制器调用服务
客户跟控制器(老板)交流,控制器调用服务(服务员)和服务(厨师)处理(控制器中依赖注入)
2. モジュールデコレータ @Module の詳細説明
@Module({
// 装饰器
imports: [],// 导入其他模块提供的服务
controllers: [AppController],//控制器
providers: [AppService],// 服务提供者
})
============>
function a() {
}
a.prototype._module={
{
imports: [],
controllers: [AppController],
providers: [AppService],
}
}
3.コントローラー
import {
Controller, Get } from '@nestjs/common';
import {
AppService } from './app.service';
@Controller() //父级路由
export class AppController {
constructor(private readonly appService: AppService) {
}
@Get('a') // 子级路由
getHello(): string {
// return 'hello';
return this.appService.getHello();
}
}
4.依存性注入
class A{
// 类A依赖于B,C
// constructor(private B=new B(new C)) ==>反射解耦
constructor(private bService:B){
} // 相当于声明的一个B类型的数据
}
class B{
constructor(private CService:C)
}
class C{
constructor()
}
サービスプロバイダー
1. サービス プロバイダーがインジェクション名を変更します (コントロールとサービスでインジェクションを利用できます)。
- モジュール内の注入名を変更します
const HD = {
provide: 'HD', //依赖注入填写的名字
useClass: HdService,
};
@Module({
...
providers: [HD], // 提供服务
})
export class AppModule {
}
- コントローラーで依存性注入が必要です
@Controller()
export class AppController {
constructor(
@Inject('HD') //依赖注入
private readonly hdService: HdService,
) {
}
...
}
2. 静的サービス プロバイダー
- モジュールで定数を提供する
const US = {
// 静态提供者
provide: 'US',
useValue: {
name: 'nest',
},
};
@Module({
...
providers: [US], // 提供服务
})
export class AppModule {
}
- サービスへの直接の依存性注入
@Injectable() // 服务声明
export class AppService {
constructor(@Inject('US') private US: any) {
}
getHello(): string {
return 'Hello World!' + this.US.name;
}
}
3. 環境変数に基づいてプロバイダーを選択する
- 構成ファイルを作成します.env
- インストール
yarn add dotenv
- 設定ファイルを読む
import {
config } from 'dotenv';
import {
join } from 'path';
config({
path: join(__dirname, '../.env') });
const HD = {
provide: 'HD',
useClass: process.env.NODE_ENV==='development' ? HdService : AppService,
};
- デコレータはサービスを提供します
@Module({
...
providers: [HD], // 提供服务
})
- コントローラに型制限はありません
constructor(
@Inject('HD')
private readonly hdService,
) {
}
4.サービスに応じて異なる設定項目を読み込む
- 開発プロジェクトを作成する
src/config/development.config.ts // 开发配置项
src/config/production.config.ts // 上线配置项
- 構成アイテムの設定方法
src/config.service.ts
config({
path: join(__dirname, '../.env') });
export const Configser = {
provide: 'Configser',
useValue:
process.env.NODE_ENV === 'development'
? developmentConfig
: productionConfig,
};
- デコレータはサービスを提供します
@Module({
...
providers: [Configser], // 提供服务
})
- サービスで直接呼び出します (コントローラーがサービスを間接的に呼び出します)。
@Injectable() // 服务声明
export class AppService {
constructor(
...
@Inject('Configser') private configser: Record<string, any>,
) {
}
getHello(): string {
return this.configser.url;
}
}
5. factory 関数を使用してプロバイダーを登録します (モジュール内のサービスは別のサービスに依存します)。
- ファクトリ関数の間接呼び出し
@Module({
// 装饰器
...
providers: [
...
Configser,
{
provide: 'DbService',
// DbService依赖于configser
inject: ['Configser'],
useFactory(configser) {
console.log(configser);
return new DbService(configser); //传递主服务的依赖注入选项
},
},
], // 提供服务
})
export class AppModule {
}
- 依存性注入
@Injectable()
export class DbService {
// 依赖注入其他的参数
constructor(private options: Record<string, any>) {
}
public connect() {
return '<h1>连接数据库</h1>' + '开发环境' + this.options.url;
}
}
6. モジュールサービス共有の問題
- Hd モジュール共有 Hd サービス
// 把hd模块的服务共享出去
@Module({
providers: [HdService],
exports: [HdService], // 开放这个服务
})
export class HdModule {
}
- dev が hd モジュールを導入
@Module({
imports: [HdModule], // 使用其他模块
providers: [DevService],
controllers: [DevController],
})
export class DevModule {
}
------dev控制器------
@Controller('dev')
export class DevController {
constructor(
private readonly devservice: DevService,
private readonly hdservice: HdService, // 依赖注入hd的服务
) {
}
...
}
7. 非同期サービス プロバイダーを登録する
@Module({
// 装饰器
...
providers: [
...
Configser,
{
provide: 'DbService',
// DbService依赖于configser
inject: ['Configser'],
useFactory:async(configser)=>{
return new Promise((r)=>{
setTimeout(() => {
r('abc')
}, 3000);
}) //传递主服务的依赖注入选项
},
},
], // 提供服务
})
export class AppModule {
}
モジュール
1. すべての構成アイテム モジュールを読み取る
- プロファイル モジュールとプロファイルを構築する
src/config/config.module.ts
src/config/config.service.ts
src/configure/app.config.ts
src/configure/database.ts
- プロファイル サービス
@Injectable()
export class ConfigService {
config = {
} as any;
constructor() {
const config = {
path: join(__dirname, '../configure') };
readdirSync(config.path).map(async (file) => {
if (file.slice(-2) === 'js') {
const module = await import(join(config.path, file));
this.config = {
...this.config, ...module.default() };
console.log(this.config);
}
});
}
get() {
// return this.config.app.name + this.config.database.host; // 静态读取配置项
return path.split('.').reduce((pre, item) => {
// 动态读取配置项
return pre[item];
}, this.config);
}
}
- プロファイル モジュール
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {
}
- コントローラーがサービスを呼び出す
@Controller() //父级路由
export class AppController {
constructor(
private readonly appService: AppService,
private readonly configservice: ConfigService,
) {
}
@Get() // 子级路由
getHello(): string {
return this.configservice.get('app.name');// 动态调用
// return this.configservice.get(); // 静态调用
}
}
2. モジュール間の呼び出し (グローバルに登録されたモジュールとインポートされたモジュールは別々に)
- ファイルを作成する
src/config/confing.module.ts
nest g mo article --no-spec
nest g co article --on-spec
- 外部モジュールをグローバルに登録する
@Global() // 全局注册了模块,其他模引入时无需导入
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {
}
- 内部モジュールをインポートする必要はありません
@Module({
// imports: [ConfigModule],
controllers: [ArticleController],
})
export class ArticleModule {
}
- 内部コントローラーでエクスポートされたメソッドを直接呼び出す
@Controller('article')
export class ArticleController {
constructor(private readonly configservice: ConfigService) {
}
@Get()
index() {
return this.configservice.get('app.name');
}
}
3.動的モジュールの読み込み(フォルダ名は可変)
内部模块想给内部服务传递一个参数,
(1)内部模块中添加静态函数接收参数返回动态服务,
(2)主模块中注册当前模块时调用静态函数传递参数
(3)内部服务依赖注入内部模块返回的动态服务得到参数
- ファイルを作成する
src/config/config.module.ts
src/config/config.service.ts
- 構成ファイル モジュールは、パラメーターを受け取る静的関数を追加し、動的サービスを返します。
@Global() // 全局注册了模块,其他模引入时无需导入
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {
static register(options: {
path: string }): DynamicModule {
// 添加动态模块
return {
module: ConfigModule,
providers: [{
provide: 'options', useValue: options }],
exports: ['options'],
};
}
}
- 構成ファイル サービスの依存性注入動的サービス
@Injectable()
export class ConfigService {
// @Optional()不执行依赖注入
constructor(
@Inject('options') private options: any,
@Optional() private config = {
},
) {
readdirSync(options.path).map(async (file) => {
if (file.slice(-2) === 'js') {
const module = await import(join(options.path, file));
this.config = {
...this.config, ...module.default() };
}
});
}
get(path: string) {
return path.split('.').reduce((pre, item) => {
return pre[item];
}, this.config);
}
}
- 構成モジュールをインポートするときに、静的関数を呼び出してファイル パス パラメーターを入力します。
@Module({
// 装饰器
imports: [
DevModule,
HdModule,
ConfigModule.register({
path: join(__dirname, './configure') }),
ArticleModule,
],
controllers: [AppController],
providers: [AppService], // 提供服务
})
export class AppModule {
}
データベース プリズマ
1. Prisma プロジェクトの運用データベースを作成する
- 依存関係をインストールする
使用mockjs (opens new window)生成随机数据
使用 argon2 (opens new window)加密密码
yarn add prisma-binding @prisma/client mockjs argon2
yarn add -D prisma typescript @types/node @types/mockjs
- プリズマ構成ファイルの作成
npx prisma init
生成.env,prisma/schema.prisma
.env中可以修改数据库连接的信息DATABASE_URL="mysql://root:123456@localhost:3306/nest-blog"
- schema.prisma ファイルを変更する
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql" //数据库类型
url = env("DATABASE_URL")
}
model user{
// 名 整数 主键 默认值(自增) 非负的
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
email String
password String
avatar String ? // ?可选的
github String ?
douyin String ?
weibo String ?
}
- フォームを生成する
执行 npx prisma migrate dev
输入 提交表的介绍
之后会生成创建数据库表的日志文件
当schema.prisma文件更新时
继续执行 npx prisma migrate dev
执行日志文件npx prisma migrate reset
- 詳細な schema.prisma ファイル
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql" //数据库类型
url = env("DATABASE_URL")
}
model user{
// 用户信息
// 名 整数 主键 默认值(自增) 非负的
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
email String
password String
avatar String ?
github String ?
douyin String ?
weibo String ?
waketime String ?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // 自动维护
}
model category{
// 分类
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
title String // 标题
articles article[]
}
model article{
// 文章
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
title String // 文章名
content String @db.Text // 内容
thumb String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // 自动维护
//类型category 关联定义(本表字段catgoryId,关联category表id,主表记录删除时同时删除关联表数据)
category category? @relation(fields: [categoryId],references:[id],onDelete:Cascade )
categoryId BigInt @db.UnsignedBigInt
}
注: 複数テーブルの関連付けを書き込む場合、ファイル コマンドを自動的に修正することはできません。npx prisma format
2.Prisma簡易データ入力
npx prisma db seed
コマンドを追加するように package.json を構成する
{
...
"prisma":{
"seed":"ts-node prisma/seed.ts"
},
"scripts": {
...
},
...
}
- Prismaファイルの下にseed.tsファイルを作成します
import {
PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function run() {
await prisma.user.create({
data: {
email: '[email protected]',
password: '123456',
},
});
}
run();
- データ充填コマンド実行
npx prisma db seed
3. Prisma モックデータの入力
- seed.ts を変更する
import {
PrismaClient } from '@prisma/client';
+ import {
Random } from 'mockjs';
const prisma = new PrismaClient();
async function run() {
+ for (let i = 0; i < 20; i++) {
await prisma.user.create({
data: {
+ email: Random.email(),
+ password: '123456',
+ github: Random.email(),
+ avatar: Random.image('300x300'),
},
});
+ }
}
run();
- テーブルを再作成してデータを入力する
npx prisma migrate reset
4. データ記入の分割(サブテーブル記入)
- ループ関数 helper.ts
import {
PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
/**
*
* @param count 模拟数据条数
* @param callback 回调函数(操作数据库的实例)
*/
export async function create(
count = 1,
callback: (prisma: PrismaClient) => void,
) {
for (let i = 0; i < count; i++) {
callback(prisma);
}
}
- 分割関数 user()
prisma/sends/user.ts
import {
PrismaClient } from '@prisma/client';
import {
Random } from 'mockjs';
import {
create } from '../helper';
export function user() {
create(30, async (prisma: PrismaClient) => {
await prisma.user.create({
data: {
email: Random.email(),
password: '123456',
github: Random.email(),
avatar: Random.image('钱'),
},
});
});
}
- テーブルを再作成してデータを入力する
npx prisma migrate reset
import {
article } from './seeds/article'
import {
category } from './seeds/category'
import {
user } from './seeds/user'
async function run() {
user()
category()
article()
}
run()
5. 主キーに関連するテーブル データを入力する問題
(記事は分類依存、分類は未登場、記事は既出、関係依存なし)
- ブロッキング待機
async function a(){
await new Promise((r)=>{
setTimeout(()=>{
console.log('a')
r('ok')
},3000)
})
}
async function b(){
await a()
console.log('b')
}
输出a,b
- ループ機能のブロック
import {
PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
/**
* @param count 模拟数据条数
* @param callback 回调函数(操作数据库的实例)
*/
export async function create(count = 1, callback: (prisma: PrismaClient) => void) {
for (let i = 0; i < count; i++) {
await callback(prisma)
}
}
- 最初にカテゴリを作成したら、記事を作成します
// 对创建分类进行阻塞
export async function category() {
await create(10, async (prisma: PrismaClient) => {
await prisma.category.create({
data: {
title: Random.ctitle(),
},
});
});
}
- 記事を作成する
import {
PrismaClient } from '@prisma/client';
import {
Random } from 'mockjs';
import {
create } from '../helper';
export function article() {
create(100, async (prisma: PrismaClient) => {
await prisma.article.create({
data: {
title: Random.ctitle(),
content: Random.cparagraph(10, 50), //段落
thumb: Random.image('文'),
categoryId: Math.ceil(Math.random() * 10),
},
});
});
}
構成管理
1. 単一ファイル構成管理
- インストール
yarn add @nestjs/config
- 構成ファイルに入力します.env
- 設定構成ファイル app.config.ts
export default () => ({
app: {
name: 'qql',
isDev: process.env.NODE_ENV === 'development',
},
})
- 構成モジュールをモジュールにインポートし、構成ファイルをロードします
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 全局使用
load: [appConfig], //加载配置文件到.env
}),
],
controllers: [AppController],
providers: [AppService], // 提供服务
})
export class AppModule {
}
- コントローラーの構成アイテムを読み取る
import {
Controller, Get } from '@nestjs/common'
import {
AppService } from './app.service'
// 引入 ConfigService,并依赖注入
import {
ConfigService } from '@nestjs/config'
@Controller()
export class AppController {
constructor(private readonly appService: AppService, private readonly config: ConfigService) {
}
@Get() // 子级路由
getHello(): any {
// return this.appService.getHello()
// return process.env.APP_Name (node直接读取配置文件)
console.log(this.config.get('app.isDev'))
return this.config.get('APP_NAME')
}
}
2. 名前空間のマルチファイル構成管理に基づく (プロンプトの追加)
- 例として構成ファイル app.config を変更します。
import {
registerAs } from '@nestjs/config'
export default registerAs('app', () => ({
name: 'qql',
isDev: process.env.NODE_ENV === 'development',
}))
/**
* 这样写相当于在内置confModule中的服务提供者进行提供命名
* providers: [{
* provide:'xxx.key',
* useValue:{
* xxxx:xxxxx
* }
* }], // 提供服务
*/
- 構成フォルダーを作成し、インデックスを設定します
import appConfig from './app.config'
import uploadConfig from './upload.config'
export default [appConfig, uploadConfig]
- モジュールに構成ファイルをロードする
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 全局使用
load: config, //加载配置文件到.env
}),
],
controllers: [AppController],
providers: [AppService], // 提供服务
})
export class AppModule {
}
- 型制約と依存性注入を追加する
/* type getType<T extends () => any> = T extends () => infer U ? U : T
type appConfigType = getType<typeof appConfig> 类型提示工具*/,
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly config: ConfigService,
@Inject(appConfig.KEY)
private app: ConfigType<typeof appConfig>
) {
}
@Get() // 子级路由
getHello(): any {
return this.app.name
}
}
パイプライン (パイプ)、検証 (DTO)、フィルター (フィルター)
1.単純なデータベースクエリの問題がパイプラインにつながる
- データクエリ
@Controller()
export class AppController {
prisma: PrismaClient; // 添加查询
constructor(private readonly appService: AppService) {
this.prisma = new PrismaClient();
}
@Get(':id') // 添加params参数
getHello(@Param('id') id: number): any {
return this.prisma.article.findUnique({
// 查询文章id对应的文章
where: {
// id, // 要求时int类型而传过来的id是string类型
id: Number(id), // 直接镜像类型转换会很麻烦
},
});
}
}
- パイプラインを作成すると、パイプラインは数値変換を実装します
nest g pi hd --no-spec
import {
ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class HdPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
//{ metatype: [Function: Number], type: 'param', data: 'id' }
// metatype后面的数据类型时走管道后需要的数据类型
return metadata.metatype === Number ? +value : value; //处理数据
}
}
- データ クエリ インポート パイプライン
@Controller()
export class AppController {
...
@Get(':id') // 添加params参数
getHello(@Param('id', HdPipe) id: number): any {
// 先走管道然后在赋值给后面的id
...
}
}
- デフォルトのパイプライン
@Controller()
export class AppController {
...
@Get(':id') // 添加params参数
@UsePipes(HdPipe)
getHello(@Param('id', new DefaultValuePipe(1)) id: number): any {
...
}
}
- ネストにはいくつかのパイプが付属しています
import {
ValidationPipe,// 验证参数有效性
ParseIntPipe,// 转换为整形
ParseBoolPipe, //转换为布尔形
ParseArrayPipe, //转换为数组
ParseUUIDPipe, // 转换为UUID
DefaultValuePipe //默认值管道
} from '@nestjs/common';
パイプラインのその他の定義 (グローバル パイプライン、ローカル パイプライン)
- コントローラーの単一のメソッドで
@Controller()
export class AppController {
...
@Get(':id') // 添加params参数
+ @UsePipes(HdPipe)
getHello(@Param('id') id: number): any {
...
}
}
- コントローラーで
// 对所有参数校检
@Controller()
@UsePipes(HdPipe)
export class AppController {
...
}
- モジュールのサービス プロバイダーで
// 模块中放入管道
@Module({
controllers: [AppController],
providers: [AppService,
{
provide:APP_NAME,
useClass:HdPipe
}
], // 提供服务
})
export class AppModule {
}
- エントリ ファイル (グローバル パイプライン)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局管道
app.useGlobalPipes(new HdPipe());
await app.listen(3000);
}
bootstrap();
2. パイプラインと DTO を使用して、データの単純なカスタム検証を実装する
- 依存関係をインストールする
yarn add class-validator class-transformer
yarn add -D @nestjs/mapped-types
- アーティクル クラスの DTO ファイルを作成する
export default class createArticleDTO {
title: string;
content: string;
}
- コントローラーの入力値は型制限されています (パイプラインで読み込まれたアーティクル クラスのインスタンス化を容易にするため)
@Controller()
export class AppController {
...
// 添加文章
@Post('add')
add(@Body(ArticlePipe) dto: createArticleDTO) {
//对文章类型限制为文章类
return dto;
}
}
- パイプラインで class-transformer を介して article クラスをインスタンス化する
@Injectable()
export class ArticlePipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
const obj = plainToInstance(metadata.metatype, value); // 实例化createArticleDTO的对象
return value;
}
}
- アーティクルクラスにclass-validatorで追加された情報の検証情報
export default class createArticleDTO {
+ @IsNotEmpty({
message: '标题不能为空!' })
+ @Length(4, 10, {
message: '标题不在4-10个字之间' })
title: string;
+ @IsNotEmpty({
message: '内容不能为空!' })
content: string;
}
- エラー メッセージのトラバース
@Injectable()
export class ArticlePipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
const obj = plainToInstance(metadata.metatype, value); // 实例化createArticleDTO的对象
+ const err = await validate(obj); // 通过DTO和validate实现信息返回一个promise对象,中间包含没有通过校检的信息以数组的形式呈现
+ if (err.length) {
+ // 数组长度大于0时遍历错误信息抛出,Object.values读取对象里面的value值
+ const messages = err.map((error) => ({
+ name: error.property,
+ messages: Object.values(error.constraints),
+ }));
+ throw new HttpException(messages, HttpStatus.BAD_REQUEST);
+ }
return value; // 通过认证
}
}
- コントローラでは、記事データがデータベースに追加されます
@Controller()
export class AppController {
...
// 添加文章
@Post('add')
add(@Body(ArticlePipe) dto: createArticleDTO) {
//对文章类型限制为文章类
const article = this.prisma.article.create({
data: {
title: dto.title,
content: dto.content,
},
});
return article;
}
}
3. システム パイプライン、DTO を使用して検証を行う
(1) 系統配管を使用する
- エントリ ファイルに ValidationPipe を導入する
import {
ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局管道
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
- article クラスの DTO ファイルを作成し、class-validator を介して情報の検証情報を追加します。
export default class createArticleDTO {
+ @IsNotEmpty({
message: '标题不能为空!' })
+ @Length(4, 10, {
message: '标题不在4-10个字之间' })
title: string;
+ @IsNotEmpty({
message: '内容不能为空!' })
content: string;
}
- コントローラーに型制約を追加する
@Controller()
export class AppController {
...
// 添加文章
@Post('add')
add(@Body(ArticlePipe) dto: createArticleDTO) {
//对文章类型限制为文章类
return dto;
}
}
(2) 継承書き換えシステム パイプライン (エラー タイトルを追加)
- ValidationPipe を継承するクラスを作成する
import {
ValidationError, ValidationPipe } from '@nestjs/common';
// 继承系统的管道方法
export class validate extends ValidationPipe {
protected mapChildrenToValidationErrors(
error: ValidationError,
parentPath?: string,
): ValidationError[] {
const errors = super.mapChildrenToValidationErrors(error, parentPath);
errors.map((error) => {
for (const key in error.constraints) {
error.constraints[key] = error.property + ':' + error.constraints[key];
}
});
return errors;
}
}
- 継承したクラスをエントリファイルに導入して使用する
import {
validate } from './pipe/Validate';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局管道
app.useGlobalPipes(new validate());
await app.listen(3000);
}
bootstrap();
4. 例外処理にフィルターを使用する
(パイプライン(それに含まれるエラー情報)の後にサービスを実行し、例外情報をキャプチャします)
- 記事フィルターの作成
nest g f article filter --no-spec
- グローバル フィルターを使用する
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局过滤器
app.useGlobalFilters(new ArticleFilter());
await app.listen(3000);
}
bootstrap();
- マルチフィルター例外情報処理(例外情報を直接返すフィルターパイプラインがなく、対応するkvがないと例外情報をうまく認識できない)
@Catch()
export class ArticleFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取http的上下文
const response = ctx.getResponse(); // 获取上下文的响应对象并返回
if (exception instanceof BadRequestException) {
// 判断异常类型 对异常的信息进行处理
const responseObject = exception.getResponse() as any;
console.log(responseObject);
return response.status(HttpStatus.UNPROCESSABLE_ENTITY).json({
code: HttpStatus.UNPROCESSABLE_ENTITY,
message: responseObject.message.map((error) => {
const info = error.split(':');
return {
field: info[0], message: info[1] };
}), // 把异常的信息改为对象数组
});
}
return response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
- ローカル フィルターの使用 (コントローラーの前に定義)
@Controller()
export class AppController {
...
// 添加文章
@Post('add')
// 局部过滤器
@UseFilters(new ArticleFilter())
async add(@Body() dto: createArticleDTO) {
...
}
}
5. DTO の検証ルールをカスタマイズする
- ユーザー DTO クラスを作成する
export default class UserinfoDTO {
@IsNotEmpty({
message: '用户名不能为空!' })
username: string;
@IsNotEmpty({
message: '密码不能为空!' })
password: string;
}
- ユーザー モジュール、ユーザー コントローラーの作成 (dto クラスを使用)
@Controller('user')
export class UserController {
@Post('register')
async register(@Body() dto: UserinfoDTO) {
const prisma = new PrismaClient();
const uesr = await prisma.user.create({
data: {
username: dto.username,
password: dto.password,
},
});
return uesr;
}
}
- グローバル パイプラインとグローバル フィルターは以前に定義されているため、DTO 検証ルールをカスタマイズするだけで済みます。
- クラス検証規則 (同じパスワード入力を再度実現するため)
@ValidatorConstraint()
export class IsConfirmed implements ValidatorConstraintInterface {
async validate(value: string, args: ValidationArguments) {
return value === args.object[args.property + '_confirmed'];
}
defaultMessage() {
// 默认消息
return '数据不匹配';
}
}
- Decorator 検証ルール (データベースが重複しているかどうかを確認します。たとえば、ユーザー名が存在するかどうか)
//表字段是否唯一
export function IsNoExists(
table: string, // 数据库中的那一张表
validationOptions?: ValidationOptions,
) {
return function (object: Record<string, any>, propertyName: string) {
registerDecorator({
name: 'IsConfirmedRule',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
async validate(value: string, args: ValidationArguments) {
const prisma = new PrismaClient();
// 查询表中是否有该数据
const flag = await prisma[table].findFirst({
where: {
[args.property]: value,
},
});
return !Boolean(flag);
},
},
});
};
}
- ユーザー DTO クラスでカスタム チェックを使用する
export default class UserinfoDTO {
@IsNotEmpty({
message: '用户名不能为空!' })
+ @IsNoExists('user', {
message: '用户已经存在!' }) //查表 使用装饰器校检规则
username: string;
@IsNotEmpty({
message: '密码不能为空!' })
+ @Validate(IsConfirmed, {
message: '确认密码输入错误!' }) // 使用类校检规则
password: string;
}
シンプルなログインと登録の実装 (jwt とトークンなし)
1. プロジェクトの初期化
- 新しいプロジェクトを作成
nest new test
- データベースの依存関係をインストールする
yarn add prisma-binding @prisma/client mockjs argon2
yarn add -D prisma typescript @types/node @types/mockjs
- インストール検証の依存関係
yarn add class-validator class-transformer
yarn add -D @nestjs/mapped-types
- コマンドを実行してライブラリを初期化します
npx prisma init
- データベース構成ファイル.env を変更します。
DATABASE_URL="mysql://root:123456@localhost:3306/nest-blog"
- モデルを追加してデータベースを作成する
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model user{
id Int @id @default(autoincrement()) @db.UnsignedInt
name String
password String
}
- データベースにテーブルを生成する
npx prisma migrate dev
2.登録ロジックの実現
(1) Prismaのサービスとモジュールを生成する
nest g mo prisma --no-spec nest g s prisma --no-spec
- PrismaClient を継承するサービス
@Injectable()
export class PrismaService extends PrismaClient {
}
- モジュールはグローバルに登録され、メソッドを公開します
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {
}
(2) モジュール、サービス、コントローラーの生成
nest g res user --no-spec
- DTO、パイプライン、フィルターの設定(システムのパイプラインを継承し、グローバルに登録)とルールのチェック
DTO
export default class registerDTO {
@IsNotEmpty({
message: '用户名不能为空' })
@IsNoExistsRule('user', {
message: '用户名已存在,请更换用户名' })
username: string;
@IsNotEmpty({
message: '密码不能为空' })
@IsConfirmedRule({
message: '确认密码与密码不同' })
password: string;
}
管道
export class validatePipe extends ValidationPipe {
protected mapChildrenToValidationErrors(
error: ValidationError,
parentPath?: string,
): ValidationError[] {
const errors = super.mapChildrenToValidationErrors(error, parentPath);
errors.map((error) => {
for (const key in error.constraints) {
error.constraints[key] = error.property + ':' + error.constraints[key];
}
});
return errors;
}
}
过滤器
@Catch()
export class GlobalFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
if (exception instanceof BadRequestException) {
const responseObj = exception.getResponse() as any;
return response.status(400).json({
code: 400,
message: responseObj.message.map((error) => {
const info = error.split(':');
return {
field: info[0], message: info[1] };
}),
});
}
return response;
}
}
密码确认校检
export function IsConfirmedRule(validationOptions?: ValidationOptions) {
return function (object: Record<string, any>, propertyName: string) {
registerDecorator({
name: 'IsConfirmedRule',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
async validate(value: string, args: ValidationArguments) {
return value === args.object[args.property + '_confirmed'];
},
},
});
};
}
查表不存在校检
export function IsNoExistsRule(
table: string, // 数据库中的那一张表
validationOptions?: ValidationOptions,
) {
return function (object: Record<string, any>, propertyName: string) {
registerDecorator({
name: 'IsNoExistsRule',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
async validate(value: string, args: ValidationArguments) {
const prisma = new PrismaClient();
// 查询表中是否有该数据
const flag = await prisma[table].findFirst({
where: {
[args.property]: value,
},
});
return !Boolean(flag);
},
},
});
};
}
- 登録済みサービスの作成
依赖注入prisma
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {
}
async register(dto: registerDTO) {
const password = await hash(dto.password); // 加密
const user = this.prisma.user.create({
data: {
username: dto.username,
password,
},
});
return user;
}
}
- コントローラーがサービスを呼び出す
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {
}
@Post('register')
register(@Body() dto: registerDTO) {
return this.userService.register(dto);
}
}
3. ログインロジックの実装
- 検証ルールを変更します(同時に登録ユーザーの存在を処理します)
export function IsNoExistsRule(
state: string, // 注册还是登录
table: string, // 数据库中的那一张表
validationOptions?: ValidationOptions,
) {
return function (object: Record<string, any>, propertyName: string) {
registerDecorator({
name: 'IsNoExistsRule',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
async validate(value: string, args: ValidationArguments) {
const prisma = new PrismaClient();
// 查询表中是否有该数据
const flag = await prisma[table].findFirst({
where: {
[args.property]: value,
},
});
if (state === 'register') return !Boolean(flag);
else return Boolean(flag);
},
},
});
};
}
- DTO の作成
export default class loginDTO {
@IsNotEmpty({
message: '用户名不能为空' })
@IsNoExistsRule('login', 'user', {
message: '该用户不存在' })
username: string;
@IsNotEmpty({
message: '密码不能为空' })
password: string;
}
- ログインサービスを追加
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {
}
// 注册的服务
async register(dto: registerDTO) {
const password = await hash(dto.password); // 加密
const user = this.prisma.user.create({
data: {
username: dto.username,
password,
},
});
return user;
}
// 登录的服务
async login(dto: loginDTO) {
const user = await this.prisma.user.findFirst({
where: {
username: dto.username,
},
});
// 校检密码
if (!(await verify(user.password, dto.password))) {
throw new BadRequestException(['password:密码输入错误']); // 让过滤器知道信息来源
}
return user;
}
}
- コントローラーがサービスを呼び出す
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {
}
...
@Post('login')
login(@Body() dto: loginDTO) {
return this.userService.login(dto);
}
}
JWT 認証
1. 関連する依存関係と構成をインストールする
- JWT 依存パッケージをインストールする
yarn add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
yarn add -D @types/passport-local @types/passport-jwt
- .env に JWT 秘密鍵を設定する
TOKEN_SECRET='qianqilong'
- 構成管理モジュールをインストールする
yarn add @nestjs/config
- メインモジュールに構成管理グローバル登録を導入
@Module({
imports: [
...
ConfigModule.forRoot({
isGlobal: true,
}),
],
})
export class AppModule {
}
2. ログイン用の JWT 認証を実装する
(1)トークンモジュールのサービスを書く
- トークン モジュールとサービスを作成する
nest g mo token module --no-spec nest g s token module --no-spec
- モジュール内にカプセル化されたjwtモジュールを導入し、トークン設定サービスをエクスポートしながら秘密鍵と存在時間を設定
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
return {
secret: config.get('TOKEN_SECRET'), // 读取jwt秘钥
signOptions: {
expiresIn: '10d' }, // 设置存在时间
};
},
}),
], // 引入jwt模块
providers: [TokenService],
exports: [TokenService],
})
export class TokenModule {
}
- 設定トークンのサービスを書く(エクスポート)
@Injectable()
export class TokenService {
constructor(private jwt: JwtService) {
}
// 生成token的服务
async setToken({
username, id }: user) {
return {
token: await this.jwt.signAsync({
name: username,
sub: id,
}),
};
}
}
(2) ログイン時にtoknを実現
- jwt 戦略を記述します (検索の最後の値がリクエストに配置されます)
import {
PrismaService } from '../module/prisma/prisma.service';
import {
ConfigService } from '@nestjs/config';
import {
ExtractJwt, Strategy } from 'passport-jwt';
import {
PassportStrategy } from '@nestjs/passport';
import {
Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(configService: ConfigService, private prisma: PrismaService) {
super({
//解析用户提交的header中的Bearer Token数据
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
//加密码的 secret
secretOrKey: configService.get('TOKEN_SECRET'),
});
}
//验证通过后获取用户资料
async validate({
sub: id }) {
return this.prisma.user.findUnique({
where: {
id },
});
}
}
- user モジュールは jwt メソッドを導入し、token モジュールを設定します
@Module({
imports: [TokenModule], // 方便使用设置token的方法
controllers: [UserController],
providers: [UserService, JwtStrategy], // 导入了jwt策略
})
export class UserModule {
}
- リターン トークンを実現し、ユーザー ログイン サービスでトークンの有効性をテストする
@Injectable()
export class UserService {
constructor(private prisma: PrismaService, private token: TokenService) {
}
// 注册的服务
...
// 登录的服务
async login(dto: loginDTO) {
const user = await this.prisma.user.findFirst({
where: {
username: dto.username,
},
});
// 校检密码
if (!(await verify(user.password, dto.password))) {
throw new BadRequestException(['password:密码输入错误']);
}
return this.token.setToken(user);
}
// 测试token认证
async test() {
return this.prisma.user.findMany(); // 获取所有用户
}
}
- モジュールで jwt 認証戦略を呼び出してトークン認証を実装する
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {
}
// 注册
...
// 登录
@Post('login')
login(@Body() dto: loginDTO) {
return this.userService.login(dto);
}
// 测试token
@Get('test')
@UseGuards(AuthGuard('jwt'))
test(@Req() req: Request) {
// express服务器的Request
req.user;
return this.userService.test();
}
}
注: 上記のグローバル フィルターは 401 エラーをキャッチするため、グローバル フィルターを適切に変更する必要があります。
(3) 上記の操作を簡素化する
- デコレーターの単純化 (集約)
export function Auth() {
return applyDecorators(UseGuards(AuthGuard('jwt')));
}
- req のパラメーター デコレーター (ポリシーで返されたユーザー情報を読み取ります)
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
- コントローラでの集約デコレータとパラメータ デコレータの使用
@Controller('user')
export class UserController {
...
// 测试token
@Get('test')
@Auth()
test(@User() user: UserType) {
console.log(user); // token对应的身份
return this.userService.test();
}
}
3.ルーティングガードは権限アクセスを実装します(AuthGuard)
- ガードを作成
nest g gu user --no-spec
import {
CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import {
Reflector } from '@nestjs/core';
import {
Observable } from 'rxjs';
@Injectable()
export class AdminGuard implements CanActivate {
constructor(private reflector: Reflector) {
} // 反射中包含了SetMetadata('roles', roles)放入的全局中,其他守卫可以使用
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const {
user } = context.switchToHttp().getRequest();
console.log(user);
return user?.id == 1;
}
}
- 集計デコレータで jwt 戦略を呼び出した後にガードを追加する
import {
applyDecorators, UseGuards } from '@nestjs/common';
import {
AuthGuard } from '@nestjs/passport';
import {
AdminGuard } from '../guards/admin.guard';
export function Auth() {
return applyDecorators(UseGuards(AuthGuard('jwt'), AdminGuard));
}
注: フィルターは 403 をキャプチャします。ステータス コードに 403 を追加する必要があります。そうしないと、ガードが機能しません。
import {
ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
@Catch()
export class GlobalFilter implements ExceptionFilter {
catch(exception: Record<string, any>, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
// console.log(exception.status);
if (exception.status === 400) {
const responseObj = exception.getResponse() as any;
return response.status(400).json({
code: 400,
message: responseObj.message.map((error) => {
const info = error.split(':');
return {
field: info[0], message: info[1] };
}),
});
} else if (exception.status === 401) {
return response.status(401).json({
code: 401,
message: '用户身份已过期,请重新登录',
});
} else if (exception.status === 403) {
return response.status(401).json({
code: 403,
message: '权限不够,请重新登录',
});
} else {
return response.status(404).json({
code: 404,
});
}
return response;
}
}
ファイルのアップロード
1. インターセプター (前後に呼び出される)
- インターセプターを定義して、返されたデータへのデータのラッピングを実現する
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import {
Request } from 'express';
import {
map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
console.log('拦截器前');
const request = context.switchToHttp().getRequest() as Request;
const startTime = Date.now();
return next.handle().pipe(
map((data) => {
const endTime = Date.now();
new Logger().log(
`TIME:${
endTime - startTime}\tURL:${
request.path}\tMETHOD:${
request.method
}`,
);
return {
data,
};
}),
);
}
}
(1) インターセプターの使用
- コントローラーでインターセプターを使用する
@UseInterceptors(new TransformInterceptor())
export class CatsController {
}
- インターセプターを使った方法
@Controller('upload')
export class UploadController {
@Post('image')
@UseInterceptors(FileInterceptor('file')) // 前后都会执行
image() {
return 'abc';
}
}
- モジュールでインターセプターを定義する
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: TransformInterceptor,
},
],
})
export class AppModule {
}
- インターセプターをグローバルに登録する
const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new TransformInterceptor());
2. 内部インターセプターを使用して、ファイルのアップロードに対する制限を実装する
(1) 簡易ファイルアップロードの実現
- 依存関係をインストールする
yarn add multer
yarn add -D @types/multer
- ファイルアップロードモジュールは、ファクトリメソッドを使用してモジュールメソッドを登録しています
@Module({
imports: [
MulterModule.registerAsync({
useFactory() {
return {
storage: diskStorage({
//文件储存位置
destination: 'uploads',
//文件名定制
filename: (req, file, callback) => {
const path =
Date.now() +
'-' +
Math.round(Math.random() * 1e10) +
extname(file.originalname);
callback(null, path);
},
}),
};
},
}),
],
controllers: [UploadController],
})
export class UploadModule {
}
- ファイル アップロードの種類とサイズに関するファイル アップロード コントローラーの制限
@Controller('upload')
@UseInterceptors(new TransformInterceptor())
export class UploadController {
@Post('image')
@UseInterceptors(
FileInterceptor('file', {
limits: {
fileSize: Math.pow(1024, 2) * 2 }, // 文件大小
fileFilter(
req: any,
file: Express.Multer.File,
callback: (error: Error | null, acceptFile: boolean) => void,
) {
// 对文件上传类型限制
if (!file.mimetype.includes('image'))
callback(new MethodNotAllowedException('文件类型不允许'), false);
else callback(null, true);
},
}),
) // 前后都会执行
image(@UploadedFile() file: Express.Multer.File) {
return file;
}
}
(2) ファイルアップロードコードの最適化
- デコレーター集約とそのパラメーターのカプセル化
export function fileFilter(type: string) {
return (
req: any,
file: Express.Multer.File,
callback: (error: Error | null, acceptFile: boolean) => void,
) => {
// 对文件上传类型限制
if (!file.mimetype.includes(type))
callback(new MethodNotAllowedException('文件类型不允许'), false);
else callback(null, true);
};
}
export function Upload(filed = 'file', options: MulterOptions) {
return applyDecorators(UseInterceptors(FileInterceptor(filed, options)));
}
- ファイル アップロード コントローラーの装飾のコードを変更します。
@Controller('upload')
@UseInterceptors(new TransformInterceptor())
export class UploadController {
@Post('image')
@Upload('file', {
// 对装饰器聚合
limits: {
fileSize: Math.pow(1024, 2) * 2 }, // 文件大小
fileFilter: fileFilter('image'), // 封装验证文件类型方法
})
image(@UploadedFile() file: Express.Multer.File) {
return file;
}
}
- デコレーターのさらなるカプセル化 (画像アップロードデコレーター)
// 上传图片的封装
export function ImageUpload(filed = 'file') {
return Upload(filed, {
limits: {
fileSize: Math.pow(1024, 2) * 2 }, // 文件大小
fileFilter: fileFilter('image'),
});
}
- 画像アップロード デコレータの使用
@Controller('upload')
@UseInterceptors(new TransformInterceptor())
export class UploadController {
@Post('image')
@ImageUpload()
image(@UploadedFile() file: Express.Multer.File) {
return file;
}
}
(3) 静的サービス
- ファイル アップロードの静的リソース
main.ts
import {
NestFactory } from '@nestjs/core';
import {
NestExpressApplication } from '@nestjs/platform-express';
import {
AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets('uploads', {
prefix: '/uploads' }); // 声明文件路径
await app.listen(3000);
}
bootstrap();
- 構成サービス
部署前端项目vue/dist
import {
NestFactory } from '@nestjs/core'
import {
AppModule } from './app.module'
import {
NestExpressApplication } from '@nestjs/platform-express'
import {
join } from 'path'
async function bootstrap() {
//传递类型NestExpressApplication
const app = await NestFactory.create<NestExpressApplication>(AppModule)
...
//批量定义访问目录,同步遍历 src目录
readdirSync(join(process.cwd(), 'src')).forEach((dir) => {
//判断是否为目录
const isDir = statSync(join(process.cwd(), 'src', dir)).isDirectory()
if (isDir) {
//定义静态访问目录
app.useStaticAssets(join(process.cwd(), 'src', dir, 'vue/dist'), {
prefix: `/static/${
dir}`,
})
}
})
await app.listen(3000)
}
bootstrap()
ロギング システムを実装する
- ロギング システムをインストールする
yarn add log4js stacktrace-js moment -S
1. ログファイル
- ログ ファイル構成の実装
import {
join } from 'path';
const baseLogPath = join(__dirname, '../../logs'); // 日志要写入哪个目录
const log4jsConfig = {
appenders: {
console: {
type: 'console', // 会打印到控制台
},
access: {
type: 'dateFile', // 会写入文件,并按照日期分类
filename: `${
baseLogPath}/access/access.log`, // 日志文件名,会命名为:access.20200320.log
alwaysIncludePattern: true,
pattern: 'yyyyMMdd',
daysToKeep: 60,
numBackups: 3,
category: 'http',
keepFileExt: true, // 是否保留文件后缀
},
app: {
type: 'dateFile',
filename: `${
baseLogPath}/app-out/app.log`,
alwaysIncludePattern: true,
layout: {
type: 'pattern',
pattern:
'{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}',
},
// 日志文件按日期(天)切割
pattern: 'yyyyMMdd',
daysToKeep: 60,
numBackups: 3,
keepFileExt: true,
},
errorFile: {
type: 'dateFile',
filename: `${
baseLogPath}/errors/error.log`,
alwaysIncludePattern: true,
layout: {
type: 'pattern',
pattern:
'{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}',
},
// 日志文件按日期(天)切割
pattern: 'yyyyMMdd',
daysToKeep: 60,
numBackups: 3,
keepFileExt: true,
},
errors: {
type: 'logLevelFilter',
level: 'ERROR',
appender: 'errorFile',
},
},
categories: {
default: {
appenders: ['console', 'app', 'errors'],
level: 'DEBUG',
},
info: {
appenders: ['console', 'app', 'errors'], level: 'info' },
access: {
appenders: ['console', 'app', 'errors'], level: 'info' },
http: {
appenders: ['access'], level: 'DEBUG' },
},
pm2: true, // 使用 pm2 来管理项目时,打开
pm2InstanceVar: 'INSTANCE_ID', // 会根据 pm2 分配的 id 进行区分,以免各进程在写日志时造成冲突
};
export default log4jsConfig;
- インスタンスログユーティリティ機能
import * as Path from 'path';
import * as Log4js from 'log4js';
import * as Util from 'util';
import * as Moment from 'moment'; // 处理时间的工具
import * as StackTrace from 'stacktrace-js';
import Chalk from 'chalk';
import config from '../config/log4js';
// 日志级别
export enum LoggerLevel {
ALL = 'ALL',
MARK = 'MARK',
TRACE = 'TRACE',
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
FATAL = 'FATAL',
OFF = 'OFF',
}
// 内容跟踪类
export class ContextTrace {
constructor(
public readonly context: string,
public readonly path?: string,
public readonly lineNumber?: number,
public readonly columnNumber?: number,
) {
}
}
Log4js.addLayout('Awesome-nest', (logConfig: any) => {
return (logEvent: Log4js.LoggingEvent): string => {
let moduleName = '';
let position = '';
// 日志组装
const messageList: string[] = [];
logEvent.data.forEach((value: any) => {
if (value instanceof ContextTrace) {
moduleName = value.context;
// 显示触发日志的坐标(行,列)
if (value.lineNumber && value.columnNumber) {
position = `${
value.lineNumber}, ${
value.columnNumber}`;
}
return;
}
if (typeof value !== 'string') {
value = Util.inspect(value, false, 3, true);
}
messageList.push(value);
});
// 日志组成部分
const messageOutput: string = messageList.join(' ');
const positionOutput: string = position ? ` [${
position}]` : '';
const typeOutput = `[${
logConfig.type}] ${
logEvent.pid.toString()} - `;
const dateOutput = `${
Moment(logEvent.startTime).format(
'YYYY-MM-DD HH:mm:ss',
)}`;
const moduleOutput: string = moduleName
? `[${
moduleName}] `
: '[LoggerService] ';
let levelOutput = `[${
logEvent.level}] ${
messageOutput}`;
// 根据日志级别,用不同颜色区分
switch (logEvent.level.toString()) {
case LoggerLevel.DEBUG:
levelOutput = Chalk.green(levelOutput);
break;
case LoggerLevel.INFO:
levelOutput = Chalk.cyan(levelOutput);
break;
case LoggerLevel.WARN:
levelOutput = Chalk.yellow(levelOutput);
break;
case LoggerLevel.ERROR:
levelOutput = Chalk.red(levelOutput);
break;
case LoggerLevel.FATAL:
levelOutput = Chalk.hex('#DD4C35')(levelOutput);
break;
default:
levelOutput = Chalk.grey(levelOutput);
break;
}
return `${
Chalk.green(typeOutput)}${
dateOutput} ${
Chalk.yellow(
moduleOutput,
)}${
levelOutput}${
positionOutput}`;
};
});
// 注入配置
Log4js.configure(config);
// 实例化
const logger = Log4js.getLogger();
logger.level = LoggerLevel.TRACE;
export class Logger {
static trace(...args) {
logger.trace(Logger.getStackTrace(), ...args);
}
static debug(...args) {
logger.debug(Logger.getStackTrace(), ...args);
}
static log(...args) {
logger.info(Logger.getStackTrace(), ...args);
}
static info(...args) {
logger.info(Logger.getStackTrace(), ...args);
}
static warn(...args) {
logger.warn(Logger.getStackTrace(), ...args);
}
static warning(...args) {
logger.warn(Logger.getStackTrace(), ...args);
}
static error(...args) {
logger.error(Logger.getStackTrace(), ...args);
}
static fatal(...args) {
logger.fatal(Logger.getStackTrace(), ...args);
}
static access(...args) {
const loggerCustom = Log4js.getLogger('http');
loggerCustom.info(Logger.getStackTrace(), ...args);
}
// 日志追踪,可以追溯到哪个文件、第几行第几列
static getStackTrace(deep = 2): string {
const stackList: StackTrace.StackFrame[] = StackTrace.getSync();
const stackInfo: StackTrace.StackFrame = stackList[deep];
const lineNumber: number = stackInfo.lineNumber;
const columnNumber: number = stackInfo.columnNumber;
const fileName: string = stackInfo.fileName;
const basename: string = Path.basename(fileName);
return `${
basename}(line: ${
lineNumber}, column: ${
columnNumber}): \n`;
}
}
2. ミドルウェアは、リクエストのルーティング/IP/パラメータなどの情報を記録します
- ミドルウェアを作成して構成する
nest g middleware logger middleware
import {
Injectable, NestMiddleware } from '@nestjs/common';
import {
Request, Response } from 'express';
import {
Logger } from '../utils/log4js';
@Injectable()
export class ViewsMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
const code = res.statusCode; // 响应状态码
next();
// 组件日志信息
const logFormat = `Method:${
req.method} \n Request original url:${
req.originalUrl} \n IP:${
req.ip} \n Status code: ${
code} \n`;
// 根据状态码进行日志区分
if (code >= 500) {
Logger.error(logFormat);
} else if (code >= 400) {
Logger.warn(logFormat);
} else {
Logger.access(logFormat);
Logger.log(logFormat);
}
}
}
// 函数式中间件
export function logger(req: Request, res: Response, next: () => any) {
const code = res.statusCode; // 响应状态码
next();
// 组装日志信息
const logFormat = `Request original url:${
req.originalUrl}
Method:${
req.method}
IP:${
req.ip}
Status code:${
code}
Params:${
JSON.stringify(req.params)}
Query:${
JSON.stringify(req.query)}
Body:${
JSON.stringify(req.body)} `;
if (code >= 500) {
Logger.error(logFormat);
} else if (code >= 400) {
Logger.warn(logFormat);
} else {
Logger.access(logFormat);
Logger.log(logFormat);
}
}
- アプリケーションミドルウェア
async function bootstrap() {
...
app.use(logger);
await app.listen(3000);
}
bootstrap();
3. インターセプターは要求された情報とデータを取得します
- インターセプターを作成して構成する
nest g interceptor transform interceptor
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import {
Observable } from 'rxjs';
import {
map } from 'rxjs/operators';
import {
Logger } from '../utils/log4js';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.getArgByIndex(1).req;
return next.handle().pipe(
map((data) => {
const logFormat = `
Request original url: ${
req.originalUrl}
Method: ${
req.method}
IP: ${
req.ip}
User: ${
JSON.stringify(req.user)}
Response data:\n ${
JSON.stringify(data.data)}`;
Logger.info(logFormat);
Logger.access(logFormat);
return data;
}),
);
}
}
- アプリケーション ブロッカー
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TransformInterceptor());// 全局拦截器
...
await app.listen(3000);
}
bootstrap();
4. フィルター
- 単純な例外フィルターを作成する
import {
ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import {
Logger } from 'src/utils/log4js';
@Catch()
export class GlobalFilter implements ExceptionFilter {
catch(exception: Record<string, any>, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
const responseObj = exception.getResponse() as any;
let message = '';
if (exception.status === 400) {
message = responseObj.message.map((error) => {
const info = error.split(':');
return {
field: info[0], message: info[1] };
});
} else if (exception.status === 401) {
message = '用户身份已过期,请重新登录';
} else if (exception.status === 403) {
message = '权限不够,请重新登录';
}
const logFormat = `
Request original url: ${
request.originalUrl}
Method: ${
request.method}
IP: ${
request.ip}
Status code: ${
status}
Response: ${
JSON.stringify(message)} \n `;
Logger.error(logFormat);
return response.status(400).json({
code: status,
message,
});
}
}
- フィルターを適用
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
app.useGlobalFilters(new GlobalFilter());
...
await app.listen(3000);
}
bootstrap();
5. 注: ユーザーが追加した情報は、ログに出力されます。
import * as express from 'express';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(express.json()); // 对JSON数据进行解析
app.use(express.urlencoded({
extended: true })); // 对表单数据进行解析
// 上面两项可以让
await app.listen(3000);
}
bootstrap();
ログイン押し出し効果を実現
1.Redisの設定方法
- 依存関係をインストールする
yarn add ioredis -S
- redis 構成 content.env を設定します
# radis连接信息
redis= {
port: 6379,host: '127.0.0.1',db: 0,password: 'root'}
- redis メソッドの作成
import Redis from 'ioredis';
const redisIndex = []; // 用于记录 redis 实例索引
const redisList = []; // 用于存储 redis 实例
export class RedisInstance {
static async initRedis(db = 0) {
const config = process.env.redis as unknown as object;
const isExist = redisIndex.some((x) => x === db);
if (!isExist) {
redisList[db] = new Redis({
...config, db });
redisIndex.push(db);
}
return redisList[db];
}
}
2. redis サービスとモジュール
- redis モジュールとサービスを作成する
nest g mo redis module --no-spec
nest g s redis module --no-spec
- redis モジュールで redis サービスをエクスポートし、トークン サービスをインポートします。
@Module({
imports: [TokenModule],
providers: [RedisService],
exports: [RedisService],
})
export class RedisModule {
}
- redis サービスはトークンを redis キャッシュに保存します
@Injectable()
export class RedisService {
constructor(private readonly token: TokenService) {
}
async setRedis(user: user) {
// 拿到token
const {
token } = await this.token.setToken(user);
// 实例化 redis
const redis = await RedisInstance.initRedis();
// 将用户信息和 token 存入 redis,并设置失效时间,语法:[key, seconds, value]
await redis.setex(`${
user.id}-${
user.username}`, 300, `${
token}`);
}
}
3. redis メソッドを使用してガードを変更する
- redis モジュールをユーザー モジュールに導入する
@Module({
imports: [TokenModule, RedisModule],
controllers: [UserController],
providers: [UserService, JwtStrategy],
})
export class UserModule {
}
- ユーザー サービスで redis サービスを使用する
@Injectable()
export class UserService {
constructor(
...
private redis: RedisService,
) {
}
...
// 登录的服务
async login(dto: loginDTO) {
...
// 调用设置redis的服务
this.redis.setRedis(user);
return this.token.setToken(user);
}
...
}
- ユーザーのガードを変更する
// 管理员守卫
@Injectable()
export class AdminGuard implements CanActivate {
constructor(private reflector: Reflector) {
} // 反射中包含了SetMetadata('roles', roles)放入的全局中,其他守卫可以使用
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const {
user } = request;
// 获取请求头里的 token
const authorization = request['headers'].authorization || void 0;
const token = authorization.split(' ')[1];
// 获取 redis 里缓存的 token
const redis = await RedisInstance.initRedis();
const key = `${
user.id}-${
user.username}`;
const cacheToken = await redis.get(key); // 缓存的token
if (token !== cacheToken) {
// 如果 token 不匹配,禁止访问
throw new UnauthorizedException('您的账号在其他地方登录,请重新登录');
}
// 第一个用户是管理员
return user?.id == 1;
}
}
Swagger で生成されたドキュメント
1.swagger をインストールして構成する
- 依存関係をインストールする
yarn add @nestjs/swagger swagger-ui-express -S
- main.ts に構成を追加する
async function bootstrap() {
...
const options = new DocumentBuilder()
.addBearerAuth() // 开启 BearerAuth 授权认证
.setTitle('Nest zero to one') // 文章标题
.setDescription('The nest-zero-to-one API description') // 文章副标题
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api-doc', app, document);
...
}
bootstrap();
2.デフォルトパラメータを追加
- login.dto.ts に構成を追加する
export default class loginDTO {
@ApiProperty({
description: '用户名', example: 'admin' })
@IsNotEmpty({
message: '用户名不能为空' })
@IsNoExistsRule('login', 'user', {
message: '该用户不存在' })
username: string;
@ApiProperty({
description: '密码', example: '123456' })
@IsNotEmpty({
message: '密码不能为空' })
password: string;
}
- register.dto.ts に構成を追加します
export default class registerDTO {
@ApiProperty({
description: '用户名', example: '' })
@IsNotEmpty({
message: '用户名不能为空' })
@IsNoExistsRule('register', 'user', {
message: '用户名已存在,请更换用户名' })
username: string;
@ApiProperty({
description: '密码', example: '' })
@IsNotEmpty({
message: '密码不能为空' })
@IsConfirmedRule({
message: '确认密码与密码不同' })
password: string;
@ApiProperty({
description: '确认密码', example: '' })
password_confirmed: string;
}
3. インターフェース種別の分類とトークンの追加
- controller.ts ファイルに ( @ApiTags('type')
@ApiTags('用户的注册和登录')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {
}
...
}
- トークンオプションを追加
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
const options = new DocumentBuilder()
.addBearerAuth() // 开启 BearerAuth 授权认证,添加token
.setTitle('Nest zero to one') // 文档标题
.setDescription('The nest-zero-to-one API description') // 文章副标题
.setVersion('1.0')
.build();
...
await app.listen(3000);
}
bootstrap();
vscode のコード スニペットを構成する
- コード スニペットの URL を生成する
https://99cc.vip/public/tools/vscode_snippet/index.html
- ネストはシステム配管を継承
{
"nest继承系统管道": {
"prefix": "nest继承系统管道",
"body": [
"import { ValidationError, ValidationPipe } from '@nestjs/common'",
"",
"// 继承系统的管道方法",
"export class validate extends ValidationPipe {",
" protected mapChildrenToValidationErrors(error: ValidationError, parentPath?: string): ValidationError[] {",
" const errors = super.mapChildrenToValidationErrors(error, parentPath)",
" errors.map((error) => {",
" // 对错误的内容做简单处理",
" for (const key in error.constraints) {",
" error.constraints[key] = error.property + ':' + error.constraints[key]",
" }",
" })",
" return errors",
" }",
"}",
""
],
"description": "nest继承系统管道"
}
}
- ネスト継承システム パイプ 2
{
"nest继承系统管道2": {
"prefix": "nest继承系统管道2",
"body": [
"import { HttpException, HttpStatus, ValidationError, ValidationPipe } from '@nestjs/common'",
"",
"// 继承系统的管道方法",
"export class validate extends ValidationPipe {",
" protected flattenValidationErrors(validationErrors: ValidationError[]): string[] {",
" const errors = {}",
" validationErrors.forEach((error) => {",
" errors[error.property] = Object.values(error.constraints)[0]",
" })",
" throw new HttpException({ code: 422, message: errors }, HttpStatus.UNPROCESSABLE_ENTITY)",
" }",
"}",
""
],
"description": "nest继承系统管道2"
}
}
- ネスト カスタム ルール デコレータ
{
"nest自定义规则装饰器": {
"prefix": "nest自定义规则装饰器",
"body": [
"import { registerDecorator, ValidationArguments, ValidationOptions } from 'class-validator'",
"",
"export function IsConfirmedRule(validationOptions?: ValidationOptions) {",
" return function (object: Record<string, any>, propertyName: string) {",
" registerDecorator({",
" name: 'IsConfirmedRule',",
" target: object.constructor,",
" propertyName: propertyName,",
" constraints: [],",
" options: validationOptions,",
" validator: {",
" async validate(value: string, args: ValidationArguments) {",
" return true",
" },",
" },",
" })",
" }",
"}",
""
],
"description": "nest自定义规则装饰器"
}
}
- ネスト クラス ルールのカスタマイズ
{
"nest类规则自定义": {
"prefix": "nest类规则自定义",
"body": [
"import {",
" ValidationArguments,",
" ValidatorConstraint,",
" ValidatorConstraintInterface,",
"} from 'class-validator';",
"",
"@ValidatorConstraint()",
"export class IsConfirmed implements ValidatorConstraintInterface {",
" async validate(value: string, args: ValidationArguments) {",
"",
" return value === args.object[args.property + '_confirmed'];",
" }",
"",
" defaultMessage() {",
" // 默认消息",
" return '数据不匹配';",
" }",
"}",
""
],
"description": "nest类规则自定义"
}
}
- ネストJWT戦略
{
"nestJWT策略": {
"prefix": "nestJWT策略",
"body": [
"import { PrismaService } from '../module/prisma/prisma.service';",
"import { ConfigService } from '@nestjs/config';",
"import { ExtractJwt, Strategy } from 'passport-jwt';",
"import { PassportStrategy } from '@nestjs/passport';",
"import { Injectable } from '@nestjs/common';",
"",
"@Injectable()",
"export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {",
" constructor(configService: ConfigService, private prisma: PrismaService) {",
" super({",
" //解析用户提交的header中的Bearer Token数据",
" jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),",
" //加密码的 secret",
" secretOrKey: configService.get('TOKEN_SECRET'),",
" });",
" }",
"",
" //验证通过后获取用户资料",
" async validate({ sub: id }) {",
" return this.prisma.user.findUnique({",
" where: { id },",
" });",
" }",
"}"
],
"description": "nestJWT策略"
}
}
- ネスト パッケージ データのインターセプター
{
"nest包裹data的拦截器": {
"prefix": "nest包裹data的拦截器",
"body": [
"import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'",
"import { map } from 'rxjs/operators'",
"",
"@Injectable()",
"export class TransformInterceptor implements NestInterceptor {",
" intercept(context: ExecutionContext, next: CallHandler) {",
" return next.handle().pipe(",
" map((data) => {",
" return data?.meta||data?.data ? data : { data }",
" }),",
" )",
" }",
"}",
""
],
"description": "nest包裹data的拦截器"
}
}
- ネスト集約デコレーター
{
"nest聚合装饰器": {
"prefix": "nest聚合装饰器",
"body": [
"import { applyDecorators } from '@nestjs/common';",
"",
"export function Auth() {",
" return applyDecorators();",
"}",
""
],
"description": "nest聚合装饰器"
}
}
- ネスト ファイル アップロード フィルタと集計デコレータ
{
"nest文件上传过滤器和聚合装饰器": {
"prefix": "nest文件上传过滤器和聚合装饰器",
"body": [
"import { applyDecorators, MethodNotAllowedException, UseInterceptors } from '@nestjs/common'",
"import { FileInterceptor } from '@nestjs/platform-express'",
"import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'",
"",
"export function fileFilter(type: string) {",
" return (req: any, file: Express.Multer.File, callback: (error: Error | null, acceptFile: boolean) => void) => {",
" // 对文件上传类型限制",
" if (!file.mimetype.includes(type)) callback(new MethodNotAllowedException('文件类型不允许'), false)",
" else callback(null, true)",
" }",
"}",
"",
"export function Upload(filed = 'file', options: MulterOptions) {",
" return applyDecorators(UseInterceptors(FileInterceptor(filed, options)))",
"}",
"",
"// 上传图片的封装",
"export function ImageUpload(filed = 'file') {",
" return Upload(filed, {",
" limits: { fileSize: Math.pow(1024, 2) * 2 }, // 文件大小",
" fileFilter: fileFilter('image'),",
" })",
"}",
""
],
"description": "nest文件上传过滤器和聚合装饰器"
}
}
- ネスト ファイル アップロード コントローラー
{
"nest文件上传控制器": {
"prefix": "nest文件上传控制器",
"body": [
"import { Controller, UseInterceptors, Post, UploadedFile } from '@nestjs/common'",
"import { TransformInterceptor } from 'src/interceptor/transform.interceptor'",
"import { ImageUpload } from './upload'",
"",
"@Controller('upload')",
"@UseInterceptors(new TransformInterceptor())",
"export class UploadController {",
" @Post('image')",
" @ImageUpload()",
" image(@UploadedFile() file: Express.Multer.File) {",
" return file",
" }",
"}",
""
],
"description": "nest文件上传控制器"
}
}
- ネストファイルアップロードモジュール
{
"nest文件上传模块": {
"prefix": "nest文件上传模块",
"body": [
"@Module({",
" imports: [",
" MulterModule.registerAsync({",
" useFactory() {",
" return {",
" storage: diskStorage({",
" //文件储存位置",
" destination: 'uploads',",
" //文件名定制",
" filename: (req, file, callback) => {",
" const path =",
" Date.now() +",
" '-' +",
" Math.round(Math.random() * 1e10) +",
" extname(file.originalname);",
" callback(null, path);",
" },",
" }),",
" };",
" },",
" }),",
" ],",
" controllers: [UploadController],",
"})",
"export class UploadModule {}",
""
],
"description": "nest文件上传模块"
}
}
- ネストユーザーガード
{
"nest用户守卫": {
"prefix": "nest用户守卫",
"body": [
"import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'",
"import { Reflector } from '@nestjs/core'",
"import { user } from '@prisma/client'",
"import { Observable } from 'rxjs'",
"import { Role } from '../decorators/enum'",
"",
"@Injectable()",
"export class RoleGuard implements CanActivate {",
" constructor(private reflector: Reflector) {} // 反射中包含了SetMetadata('roles', roles)放入的全局中,其他守卫可以使用",
" canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {",
" // RoleGuard守卫中上下文会获取调用jwt策略中返回的数据",
" // context中包含策略返回的用户",
" const user = context.switchToHttp().getRequest().user as user",
" const roles = this.reflector.getAllAndMerge<Role[]>('roles', [context.getHandler(), context.getClass()]) // 读取反射中数据",
" // 没通过就是403",
" return roles ? roles.some((role) => user.role == role) : true",
" }",
"}",
""
],
"description": "nest用户守卫"
}
}
- ログ構成ファイル
{
"日志的配置文件": {
"prefix": "日志的配置文件",
"body": [
"import { join } from 'path';",
"const baseLogPath = join(__dirname, '../../logs'); // 日志要写入哪个目录",
"",
"const log4jsConfig = {",
" appenders: {",
" console: {",
" type: 'console', // 会打印到控制台",
" },",
" access: {",
" type: 'dateFile', // 会写入文件,并按照日期分类",
" filename: `${baseLogPath}/access/access.log`, // 日志文件名,会命名为:access.20200320.log",
" alwaysIncludePattern: true,",
" pattern: 'yyyyMMdd',",
" daysToKeep: 60,",
" numBackups: 3,",
" category: 'http',",
" keepFileExt: true, // 是否保留文件后缀",
" },",
" app: {",
" type: 'dateFile',",
" filename: `${baseLogPath}/app-out/app.log`,",
" alwaysIncludePattern: true,",
" layout: {",
" type: 'pattern',",
" pattern:",
" '{\"date\":\"%d\",\"level\":\"%p\",\"category\":\"%c\",\"host\":\"%h\",\"pid\":\"%z\",\"data\":\'%m\'}',",
" },",
" // 日志文件按日期(天)切割",
" pattern: 'yyyyMMdd',",
" daysToKeep: 60,",
" numBackups: 3,",
" keepFileExt: true,",
" },",
" errorFile: {",
" type: 'dateFile',",
" filename: `${baseLogPath}/errors/error.log`,",
" alwaysIncludePattern: true,",
" layout: {",
" type: 'pattern',",
" pattern:",
" '{\"date\":\"%d\",\"level\":\"%p\",\"category\":\"%c\",\"host\":\"%h\",\"pid\":\"%z\",\"data\":\'%m\'}',",
" },",
" // 日志文件按日期(天)切割",
" pattern: 'yyyyMMdd',",
" daysToKeep: 60,",
" numBackups: 3,",
" keepFileExt: true,",
" },",
" errors: {",
" type: 'logLevelFilter',",
" level: 'ERROR',",
" appender: 'errorFile',",
" },",
" },",
" categories: {",
" default: {",
" appenders: ['console', 'app', 'errors'],",
" level: 'DEBUG',",
" },",
" info: { appenders: ['console', 'app', 'errors'], level: 'info' },",
" access: { appenders: ['console', 'app', 'errors'], level: 'info' },",
" http: { appenders: ['access'], level: 'DEBUG' },",
" },",
" pm2: true, // 使用 pm2 来管理项目时,打开",
" pm2InstanceVar: 'INSTANCE_ID', // 会根据 pm2 分配的 id 进行区分,以免各进程在写日志时造成冲突",
"};",
"",
"export default log4jsConfig;",
""
],
"description": "日志的配置文件"
}
}
- ログツール
{
"日志的工具类": {
"prefix": "日志的工具类",
"body": [
"import * as Path from 'path';",
"import * as Log4js from 'log4js';",
"import * as Util from 'util';",
"import * as Moment from 'moment'; // 处理时间的工具",
"import * as StackTrace from 'stacktrace-js';",
"import Chalk from 'chalk';",
"import config from '../config/log4js';",
"",
"// 日志级别",
"export enum LoggerLevel {",
" ALL = 'ALL',",
" MARK = 'MARK',",
" TRACE = 'TRACE',",
" DEBUG = 'DEBUG',",
" INFO = 'INFO',",
" WARN = 'WARN',",
" ERROR = 'ERROR',",
" FATAL = 'FATAL',",
" OFF = 'OFF',",
"}",
"",
"// 内容跟踪类",
"export class ContextTrace {",
" constructor(",
" public readonly context: string,",
" public readonly path?: string,",
" public readonly lineNumber?: number,",
" public readonly columnNumber?: number,",
" ) {}",
"}",
"",
"Log4js.addLayout('Awesome-nest', (logConfig: any) => {",
" return (logEvent: Log4js.LoggingEvent): string => {",
" let moduleName = '';",
" let position = '';",
"",
" // 日志组装",
" const messageList: string[] = [];",
" logEvent.data.forEach((value: any) => {",
" if (value instanceof ContextTrace) {",
" moduleName = value.context;",
" // 显示触发日志的坐标(行,列)",
" if (value.lineNumber && value.columnNumber) {",
" position = `${value.lineNumber}, ${value.columnNumber}`;",
" }",
" return;",
" }",
"",
" if (typeof value !== 'string') {",
" value = Util.inspect(value, false, 3, true);",
" }",
"",
" messageList.push(value);",
" });",
"",
" // 日志组成部分",
" const messageOutput: string = messageList.join(' ');",
" const positionOutput: string = position ? ` [${position}]` : '';",
" const typeOutput = `[${logConfig.type}] ${logEvent.pid.toString()} - `;",
" const dateOutput = `${Moment(logEvent.startTime).format(",
" 'YYYY-MM-DD HH:mm:ss',",
" )}`;",
" const moduleOutput: string = moduleName",
" ? `[${moduleName}] `",
" : '[LoggerService] ';",
" let levelOutput = `[${logEvent.level}] ${messageOutput}`;",
"",
" // 根据日志级别,用不同颜色区分",
" switch (logEvent.level.toString()) {",
" case LoggerLevel.DEBUG:",
" levelOutput = Chalk.green(levelOutput);",
" break;",
" case LoggerLevel.INFO:",
" levelOutput = Chalk.cyan(levelOutput);",
" break;",
" case LoggerLevel.WARN:",
" levelOutput = Chalk.yellow(levelOutput);",
" break;",
" case LoggerLevel.ERROR:",
" levelOutput = Chalk.red(levelOutput);",
" break;",
" case LoggerLevel.FATAL:",
" levelOutput = Chalk.hex('#DD4C35')(levelOutput);",
" break;",
" default:",
" levelOutput = Chalk.grey(levelOutput);",
" break;",
" }",
"",
" return `${Chalk.green(typeOutput)}${dateOutput} ${Chalk.yellow(",
" moduleOutput,",
" )}${levelOutput}${positionOutput}`;",
" };",
"});",
"",
"// 注入配置",
"Log4js.configure(config);",
"",
"// 实例化",
"const logger = Log4js.getLogger();",
"logger.level = LoggerLevel.TRACE;",
"",
"export class Logger {",
" static trace(...args) {",
" logger.trace(Logger.getStackTrace(), ...args);",
" }",
"",
" static debug(...args) {",
" logger.debug(Logger.getStackTrace(), ...args);",
" }",
"",
" static log(...args) {",
" logger.info(Logger.getStackTrace(), ...args);",
" }",
"",
" static info(...args) {",
" logger.info(Logger.getStackTrace(), ...args);",
" }",
"",
" static warn(...args) {",
" logger.warn(Logger.getStackTrace(), ...args);",
" }",
"",
" static warning(...args) {",
" logger.warn(Logger.getStackTrace(), ...args);",
" }",
"",
" static error(...args) {",
" logger.error(Logger.getStackTrace(), ...args);",
" }",
"",
" static fatal(...args) {",
" logger.fatal(Logger.getStackTrace(), ...args);",
" }",
"",
" static access(...args) {",
" const loggerCustom = Log4js.getLogger('http');",
" loggerCustom.info(Logger.getStackTrace(), ...args);",
" }",
"",
" // 日志追踪,可以追溯到哪个文件、第几行第几列",
" static getStackTrace(deep = 2): string {",
" const stackList: StackTrace.StackFrame[] = StackTrace.getSync();",
" const stackInfo: StackTrace.StackFrame = stackList[deep];",
"",
" const lineNumber: number = stackInfo.lineNumber;",
" const columnNumber: number = stackInfo.columnNumber;",
" const fileName: string = stackInfo.fileName;",
" const basename: string = Path.basename(fileName);",
" return `${basename}(line: ${lineNumber}, column: ${columnNumber}): \n`;",
" }",
"}",
""
],
"description": "日志的工具类"
}
}
- ログミドルウェア
{
"日志的中间件": {
"prefix": "日志的中间件",
"body": [
"import { Injectable, NestMiddleware } from '@nestjs/common';",
"import { Request, Response } from 'express';",
"import { Logger } from '../utils/log4js';",
"",
"@Injectable()",
"export class ViewsMiddleware implements NestMiddleware {",
" use(req: Request, res: Response, next: () => void) {",
" const code = res.statusCode; // 响应状态码",
" next();",
" // 组件日志信息",
" const logFormat = `Method:${req.method} \n Request original url:${req.originalUrl} \n IP:${req.ip} \n Status code: ${code} \n`;",
" // 根据状态码进行日志区分",
" if (code >= 500) {",
" Logger.error(logFormat);",
" } else if (code >= 400) {",
" Logger.warn(logFormat);",
" } else {",
" Logger.access(logFormat);",
" Logger.log(logFormat);",
" }",
" }",
"}",
"",
"// 函数式中间件",
"export function logger(req: Request, res: Response, next: () => any) {",
" const code = res.statusCode; // 响应状态码",
" next();",
" // 组装日志信息",
" const logFormat = `",
" Request original url:${req.originalUrl}",
" Method:${req.method}",
" IP:${req.ip}",
" Status code:${code}",
" Params:${JSON.stringify(req.params)}",
" Query:${JSON.stringify(req.query)}",
" Body:${JSON.stringify(req.body)} `;",
" if (code >= 500) {",
" Logger.error(logFormat);",
" } else if (code >= 400) {",
" Logger.warn(logFormat);",
" } else {",
" Logger.access(logFormat);",
" Logger.log(logFormat);",
" }",
"}",
""
],
"description": "日志的中间件"
}
}
{",
" logger.debug(Logger.getStackTrace(), …args);",
" }",
"",
" static log(…args) {",
" logger.info(Logger.getStackTrace(), …args );",
" }",
"",
" static info(…args) {",
" logger.info(Logger.getStackTrace(), …args);",
" }",
"",
" static warn(… args) {",
" logger.warn(Logger.getStackTrace(), …args);",
" }",
"",
" static warning(…args) {",
" logger.warn(Logger.getStackTrace(), …args);」、
「 }」、
「」、
「 static error(…args) {」、
「 logger.error(Logger.getStackTrace()、…args);「、
」 }」、
「」、
「static fatal(…args) {“,
" logger.fatal(Logger.getStackTrace(), ...args);",
" }",
"",
" static access(...args) {",
" const loggerCustom = Log4js.getLogger('http') ;",
" loggerCustom.info(Logger.getStackTrace(), …args);",
" }",
"",
" // トレースできるファイル、行、列をログに記録",
" static getStackTrace(deep = 2 ): 文字列 {",
" const stackList: StackTrace.StackFrame[] = StackTrace.getSync();",
" const stackInfo: StackTrace.StackFrame = stackList[deep];",
"",
" const lineNumber: number = stackInfo . lineNumber;",
" const columnNumber: number = stackInfo.columnNumber;",
" const fileName: string = stackInfo.fileName;",
" const basename:文字列 = Path.basename(ファイル名);“,
" return ${basename}(line: ${lineNumber}, column: ${columnNumber}): \n
;",
" }",
"}",
""
],
"description": "log tool class"
}
}
14. 日志的中间件
```json
{
"日志的中间件": {
"prefix": "日志的中间件",
"body": [
"import { Injectable, NestMiddleware } from '@nestjs/common';",
"import { Request, Response } from 'express';",
"import { Logger } from '../utils/log4js';",
"",
"@Injectable()",
"export class ViewsMiddleware implements NestMiddleware {",
" use(req: Request, res: Response, next: () => void) {",
" const code = res.statusCode; // 响应状态码",
" next();",
" // 组件日志信息",
" const logFormat = `Method:${req.method} \n Request original url:${req.originalUrl} \n IP:${req.ip} \n Status code: ${code} \n`;",
" // 根据状态码进行日志区分",
" if (code >= 500) {",
" Logger.error(logFormat);",
" } else if (code >= 400) {",
" Logger.warn(logFormat);",
" } else {",
" Logger.access(logFormat);",
" Logger.log(logFormat);",
" }",
" }",
"}",
"",
"// 函数式中间件",
"export function logger(req: Request, res: Response, next: () => any) {",
" const code = res.statusCode; // 响应状态码",
" next();",
" // 组装日志信息",
" const logFormat = `",
" Request original url:${req.originalUrl}",
" Method:${req.method}",
" IP:${req.ip}",
" Status code:${code}",
" Params:${JSON.stringify(req.params)}",
" Query:${JSON.stringify(req.query)}",
" Body:${JSON.stringify(req.body)} `;",
" if (code >= 500) {",
" Logger.error(logFormat);",
" } else if (code >= 400) {",
" Logger.warn(logFormat);",
" } else {",
" Logger.access(logFormat);",
" Logger.log(logFormat);",
" }",
"}",
""
],
"description": "日志的中间件"
}
}