https://github.com/goldbergyoni/nodebestpractices/blob/master/README.chinese.md
① 安装依赖模块
npm i --save-dev webpack-node-externals
② 创建
webpack.config.js
const webpack = require(‘webp‘);
const nodeExternals = require(‘webpack-node-externals‘);
module.exports = function(options) {
return {
...options,
entry: [‘webpack/hot/poll?100‘, ‘./src/main.ts‘],
watch: true,
externals: [
nodeExternals({
whitelist: [‘webpack/hot/poll?100‘],
}),
],
plugins: [...options.plugins, new webpack.HotModuleReplacementPlugin()],
};
}
③ 打开
main.ts
declare const module: any;
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();
④ 打开
package.json
"build": "nest build --watch --webpack"
"start": "node dist/main",
① 安装依赖
npm install --save @nestjs/typeorm typeorm mysql
② 打开
app.module.ts
import { Module } from ‘@nestjs/common‘;
import { TypeOrmModule } from ‘@nestjs/typeorm‘;
@Module({
imports: [
TypeOrmModule.forRoot({
type: ‘mysql‘,
host: ‘localhost‘,
port: 3306,
username: ‘root‘,
password: ‘root‘,
database: ‘test‘,
entities: [],
synchronize: true,
maxQueryExecutionTime: 1000, // 记录所有运行超过1秒的查询。
}),
],
})
export class AppModule {}
③ 设置长连接
TypeOrmModule.forRoot({
...,
keepConnectionAlive: true,
}),
④ 自动导入
entity
TypeOrmModule.forRoot({
...,
autoLoadEntities: true,
}),
⑤ 写一个
entity
类
// 例如 `Photo` 类
import { Entity, Column, PrimaryGeneratedColumn } from ‘typeorm‘;
@Entity()
export class Photo {
@PrimaryGeneratedColumn()
id: number;
// mysql 列类型
// int, bigint, bit, decimal, money, numeric, smallint, smallmoney, tinyint, float, real, date, datetime2, datetime, datetimeoffset, smalldatetime, time, char, varchar, text, nchar, nvarchar, ntext, binary, image, varbinary, hierarchyid, sql_variant, timestamp, uniqueidentifier, xml, geometry, geography, rowversion
// comment: ‘账号名‘,// 注释
// length: 100, // 长度设置,mysql的int类型没有长度,
// default: ‘姓‘, // 默认值
// charset: ‘utf-8‘, // 编码格式
// readonly: false, // 是否只读
// type: ‘string‘, // 类型,string,text,time,int,double等等等
// unique: true, // 是否是唯一的
// insert: true, // 是否可写
// primary: false, // 是否是主键
// select: true, // 是否可查询
// name: ‘name‘, // 对应数据库字段名
// nullable: false, // 是否可以为空,false表示不为空
@Column({ length: 500 })
name: string;
@Column(‘text‘)
description: string;
@Column(‘int‘)
views: number;
}
⑥ 使用
forFeature()
方法定义在当前范围中注册哪些存储库
// photo.module.ts
import { Module } from ‘@nestjs/common‘;
import { TypeOrmModule } from ‘@nestjs/typeorm‘;
import { PhotoService } from ‘./photo.service‘;
import { PhotoController } from ‘./photo.controller‘;
import { Photo } from ‘./photo.entity‘;
@Module({
//======================================================
imports: [TypeOrmModule.forFeature([Photo])],
//======================================================
providers: [PhotoService],
controllers: [PhotoController],
})
export class PhotoModule {}
⑦ 使用
@InjectRepository()
装饰器将PhotoRepository
注入到PhotoService
// photo.service.ts
import { Injectable } from ‘@nestjs/common‘;
import { InjectRepository } from ‘@nestjs/typeorm‘;
import { Repository } from ‘typeorm‘;
import { Photo } from ‘./photo.entity‘;
@Injectable()
export class PhotoService {
constructor(
//======================================================
@InjectRepository(Photo)
private readonly photoRepository: Repository<Photo>,
//======================================================
) {}
findAll(): Promise<Photo[]> {
return this.photoRepository.find();
}
}
查询语法
// 简单查询
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.age = 25;
// 保存
await repository.save(user);
// 查找所有
const allUsers = await repository.find();
// 按id查找
const firstUser = await repository.findOne(1);
// 按条件查找一个
const timber = await repository.findOne({ firstName: "Timber", lastName: "Saw" });
// 关系查找(查找和该表关联的值)
const questions = await questionRepository.find({ relations: ["categories"] });
// 查找并计数
const count = await repository.findAndCount();
// 删除
await repository.remove(timber);
// 复杂查询
// 1、find 操作
userRepository.find({
select: ["firstName", "lastName"], // 按属性查找
relations: ["profile", "photos", "videos"], // 按关系查找
where: { // 条件查询
firstName: "Timber",
lastName: "Saw"
}, // 排序
order: {
name: "ASC",
id: "DESC"
},
skip: 5, // 分页,跳过几项
take: 10, // 分页,取几项
cache: true
});
// 2、内置运算符查询(Not,LessThan,Equal,Like 等)
import { Like } from "typeorm";
const loadedPosts = await connection.getRepository(Post).find({
title: Like("%out #%") // 模糊查询,%表示任何字符出现任意次数 (可以是0次),_只能匹配单个字符,不能多也不能少,就是一个字符
});
// 3、数据库事物
// startTransaction - 启动一个新事务。
// commitTransaction - 提交所有更改。
// rollbackTransaction - 回滚所有更改。
const queryRunner = this.userRepository.createQueryBuilder(‘user‘)
queryRunner.startTransaction()
try{
// 对此事务执行一些操作:
// 提交事务:
await queryRunner.commitTransaction();
} catch (err) {
// 有错误做出回滚更改
await queryRunner.rollbackTransaction();
}
① 配置静态资源
import { NestFactory } from ‘@nestjs/core‘;
import { AppModule } from ‘./app.module‘;
import { NestExpressApplication } from ‘@nestjs/platform-express‘;
async function bootstrap() {
//======================================================
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets(‘public‘);
//======================================================
await app.listen(3000);
}
bootstrap();
① 安装依赖
npm i ejs --save
② 打开
main.ts
import {NestFactory} from ‘@nestjs/core‘;
//======================================================
import { NestExpressApplication } from ‘@nestjs/platform-express‘;
import {join} from ‘path‘;
//======================================================
import {AppModule} from ‘./app.module‘;
declare const module: any;
async function bootstrap() {
//======================================================
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets(join(__dirname, ‘..‘, ‘public‘),{
prefix: ‘/static/‘, //设置虚拟路径
});
app.setBaseViewsDir(join(__dirname, ‘..‘, ‘views‘)) // 放视图的文件
app.setViewEngine(‘ejs‘);
//======================================================
await app.listen(3000);
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();
③ 渲染页面
// 业务逻辑
import { Controller, Get, Post, Body,Response, Render} from ‘@nestjs/common‘;
@Controller(‘user‘)
export class UserController {
@Get()
//======================================================
@Render(‘default/user‘)
//======================================================
index(){
return {"name":"张三"};
}
@Post(‘doAdd‘)
doAdd(@Body() body,@Response() res){
console.log(body);
//======================================================
res.redirect(‘/user‘); //路由跳转
//======================================================
}
}
// 页面模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="/static/base.css">
</head>
<body>
<img src="/static/1.png" alt="" />
<br>
<form action="/user/doAdd" method="post">
<input type="text" name="username" value="<%= name %>" placeholder="请输入用户名" />
<br>
<br>
<input type="text" name="age" placeholder="年龄" />
<br>
<br>
<input type="submit" value="提交">
</form>
</body>
</html>
① 安装依赖
npm install @nestjs/jwt
② 使用
// auth/auth.module.ts
import { Module } from ‘@nestjs/common‘;
import { AuthService } from ‘./auth.service‘;
import { JwtModule } from ‘@nestjs/jwt‘;
@Module({
imports: [
//======================================================
JwtModule.register({
secret: ‘secret‘,
signOptions: { expiresIn: ‘60s‘ },
}),
//======================================================
],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}
// auth/auth.service.ts
import { Injectable } from ‘@nestjs/common‘;
import { JwtService } from ‘@nestjs/jwt‘;
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService
) {}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
//======================================================
access_token: this.jwtService.sign(payload),
//======================================================
};
}
}
① 安装依赖
npm install bcrypt
② 使用示例
const bcrypt = require(‘bcrypt‘);
// 用法一
// 使用10个哈希回合异步生成安全密码
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(‘myPassword‘, salt, function(err, hash) {
// Store hash in your password DB.
});
});
// 用法二
// 使用10个哈希回合异步生成安全密码
bcrypt.hash(‘myPassword‘, 10, function(err, hash) {
// 在用户记录中存储安全哈希
});
// 将提供的密码输入与已保存的哈希进行比较
bcrypt.compare(‘somePassword‘, hash, function(err, match) {
if(match) {
// match == true
// 密码匹配
} else {
// match == false
// 密码不匹配
}
});
安装依赖
npm i --save class-validator class-transformer
dto 文件中添加装饰器
// https://github.com/typestack/class-validator#usage
IntelliJ IDEAPhpStormWebStorm
import {Contains, IsInt, IsIn, MinLength, MaxLength, IsEmail, IsFQDN, IsDate, ArrayNotEmpty, ArrayMinSize, ArrayMaxSize, IsEnum} from "../../src/decorator/decorators";
export enum PostType {
Public,
Private
}
const allowValue = [1, 2, 3];
export class Post {
@MinLength(10)
@MaxLength(20)
title: string;
@Contains("hello")
text: string;
// 因为 `application/x-www-form-urlencoded` 类型的提交总是为字符串,需要手动转一下
// 添加 Transform 转类型
@Transform(value => Number.isNaN(+value) ? 0 : +value)
@IsInt()
rating: number;
@IsEmail()
email: string;
@IsFQDN()
site: string;
@IsDate()
createDate: Date;
@ArrayNotEmpty()
@ArrayMinSize(2)
@ArrayMaxSize(5)
@MinLength(3, { each: true, message: "Tag is too short. Minimal length is $value characters" })
@MaxLength(50, { each: true, message: "Tag is too long. Maximal length is $value characters" })
tags: string[];
@IsEnum(PostType)
type: PostType;
@IsIn(allowValue)
value: number;
}
创建验证管道
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from ‘@nestjs/common‘;
import { validate } from ‘class-validator‘;
import { plainToClass } from ‘class-transformer‘;
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException(‘Validation failed‘);
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
全局使用该验证管道
app.useGlobalPipes(new ValidationPipe());
① 安装依赖
npm install express-session --save
② 打开 main.ts
import {NestFactory} from ‘@nestjs/core‘;
import { NestExpressApplication } from ‘@nestjs/platform-express‘;
import {join} from ‘path‘;
//======================================================
import * as session from ‘express-session‘;
//======================================================
import {AppModule} from ‘./app.module‘;
declare const module: any;
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets(join(__dirname, ‘..‘, ‘public‘),{
prefix: ‘/static/‘, //设置虚拟路径
});
app.setBaseViewsDir(join(__dirname, ‘..‘, ‘views‘)) // 放视图的文件
app.setViewEngine(‘ejs‘);
//======================================================
// 1. name - cookie的名字(原属性名为 key)。(默认:’connect.sid’)
// 2. store - session存储实例
// 3. secret - 用它来对session cookie签名,防止篡改
// 4. cookie - session cookie设置 (默认:{ path: ‘/‘, httpOnly: true,secure: false, maxAge: null })
// 5. genid - 生成新session ID的函数 (默认使用uid2库)
// 6. rolling - 在每次请求时强行设置cookie,这将重置cookie过期时间(默认:false)
// 7. resave - 强制保存session即使它并没有变化 (默认: true, 建议设为:false)
// 8. proxy - 当设置了secure cookies(通过”x-forwarded-proto” header )时信任反向代理。当设定为true时,
// ”x-forwarded-proto” header 将被使用。当设定为false时,所有headers将被忽略。当该属性没有被设定时,将使用Express的trust proxy。
// 9. saveUninitialized - 强制将未初始化的session存储。当新建了一个session且未设定属性或值时,它就处于未初始化状态。在设定一个cookie前,这对于登陆验证,减轻服务端存储压力,权限控制是有帮助的。(默认:true)
// 10. unset - 控制req.session是否取消(例如通过 delete,或者将它的值设置为null)。这可以使session保持存储状态但忽略修改或删除的请求(默认:keep)
app.use(session({
resave: false, //添加 resave 选项
saveUninitialized: true, //添加 saveUninitialized 选项
secret: ‘keyword‘,
cookie: {maxAge: 60000}
}));
//======================================================
await app.listen(3000);
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();
application / x-www-form-urlencoded
来自application / x-www-form-urlencoded请求的所有值始终是字符串。导致
ValidationPipe
验证IsInt
失败
解决办法
import {IsString, IsInt, IsNotEmpty, MinLength, MaxLength} from "class-validator";
//======================================================
import { Transform } from ‘class-transformer‘;
//======================================================
export class CreateUserDto {
//======================================================
@Transform(value => Number.isNaN(+value) ? 0 : +value)
//======================================================
@IsInt()
@IsNotEmpty({message:‘账号不能空‘})
readonly account: number;
@MinLength(6, {
message: ‘密码长度最小为6位‘,
})
@MaxLength(50, {
message: ‘密码长度最大为50位‘,
})
@IsString()
readonly password: string;
}
session
使用redis
存储安装依赖
npm install --save redis connect-redis
本地开发修改
reids
的bind ip0.0.0.0
(原始:127.0.0.1)
import * as session from ‘express-session‘;
import * as connectRedis from ‘connect-redis‘;
import * as redis from ‘redis‘
const redisStore = connectRedis(session);
app.use(session({
resave: false, //添加 resave 选项
saveUninitialized: true, //添加 saveUninitialized 选项
secret: ‘keyword‘,
cookie: {maxAge: 60000},
store : new redisStore({
"client": redis.createClient({
"host" : "0.0.0.0",
"port" : "6379",
"auth_pass" : "*****",
"db" : 2
})
})
}));
定义守卫
import {Injectable, CanActivate, ExecutionContext} from ‘@nestjs/common‘;
import {Observable} from ‘rxjs‘;
import {Reflector} from ‘@nestjs/core‘;
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {
}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
// 获取类里设置的元数据
// const roles = this.reflector.get<string[]>(‘roles‘, context.getClass());
// 获取方法里设置的元数据
const roles = this.reflector.get<string[]>(‘roles‘, context.getHandler());
const request = context.switchToHttp().getRequest();
return false
// 实际上,返回 false 的守卫会抛出一个 HttpException 异常。
// 如果您想要向最终用户返回不同的错误响应,你应该抛出一个异常。
}
}
自定义装饰器用于设置路由元数据
import {SetMetadata} from ‘@nestjs/common‘;
export const Roles = (...roles: string[]) => SetMetadata(‘roles‘, roles)
[可选]自定义无权限异常
import {HttpException, HttpStatus} from ‘@nestjs/common‘;
// 基础异常类型
// BadRequestException[请求报文存在语法错误] — 400
// UnauthorizedException[表示发送的请求需要有通过 HTTP 认证的认证信息] — 401
// ForbiddenException[表示对请求资源的访问被服务器拒绝] — 403
// NotFoundException[资源不存在] — 404
// NotAcceptableException[返回格式与web accept接受的不一致] — 406
// RequestTimeoutException[请求超时] — 408
// ConflictException[由于和被请求的资源的当前状态之间存在冲突,请求无法完成] — 409
// GoneException[被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址] — 410
// PayloadTooLargeException[服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围] — 413
// UnsupportedMediaTypeException[对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝。] — 415
// UnprocessableEntityException[请求格式正确,但是由于含有语义错误,无法响应。] — 422
// InternalServerErrorException[服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。] — 500
// NotImplementedException[服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求。] — 501
// BadGatewayException[作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。] — 502
// ServiceUnavailableException[表明服务器暂时处于超负载或正在停机维护,无法处理请求] — 503
// GatewayTimeoutException[网关超时 (Gateway timeout),是指服务器作为网关或代理,但是没有及时从上游服务器收到请求] — 504
export class ForbiddenException extends HttpException {
constructor() {
super(‘Forbidden‘, HttpStatus.FORBIDDEN);
}
}
异常过滤器,用于捕获异常
import {Catch, ArgumentsHost, HttpException, HttpStatus} from ‘@nestjs/common‘;
import {BaseExceptionFilter} from ‘@nestjs/core‘;
@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
super.catch(exception, host);
// const ctx = host.switchToHttp();
// const response = ctx.getResponse();
// const request = ctx.getRequest();
//
// const status =
// exception instanceof HttpException
// ? exception.getStatus()
// : HttpStatus.INTERNAL_SERVER_ERROR;
//
// response.status(status).json({
// statusCode: status,
// timestamp: new Date().toISOString(),
// path: request.url,
// });
}
}
记录登录过程中密码比较错误的情形
数据库密码存储的长度太短,导致存储的密码位数不够
安装依赖
npm install --save @nestjs/swagger swagger-ui-express
引导
import { NestFactory } from ‘@nestjs/core‘;
import { SwaggerModule, DocumentBuilder } from ‘@nestjs/swagger‘;
import { ApplicationModule } from ‘./app.module‘;
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
const options = new DocumentBuilder()
.setTitle(‘Cats example‘)
.setDescription(‘The cats API description‘)
.setVersion(‘1.0‘)
.addTag(‘cats‘)
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup(‘api‘, app, document);
await app.listen(3000);
}
bootstrap();
模块分类到对应标签
import { ApiTags } from ‘@nestjs/swagger‘;
import { Controller, Get, Query } from ‘@nestjs/common‘;
@ApiTags(‘用户,安全‘)
@Controller(‘/user‘)
export class UserController {
//...
}
ApiQuery、ApiBody、ApiParam、ApiHeader、ApiHeaders
// 除`ApiHeaders`之外,其余的接受一个对象,对象类型如下
name: string; // 该数据的名称,比如:id可以写用户id或者id
description?: string; // 简介
required?: boolean; // 是否是必须的
type?: any; // 类型
isArray?: boolean; // 是否是数组
enum?: SwaggerEnumType; // 枚举类型
collectionFormat?: "csv" | "ssv" | "tsv" | "pipes" | "multi";
// 而`ApiHeaders`需要三个参数
name: string;
description?: string;
required?: boolean;
// 代码展示
import { Controller, Get, Param, Query } from ‘@nestjs/common‘;
import { ApiTags, ApiParam, ApiQuery, ApiHeader } from ‘@nestjs/swagger‘;
import { UserService } from ‘./user.service‘;
@ApiTags(‘用户,安全‘)
@Controller(‘/user‘)
export class UserController {
constructor(private userService: UserService) { }
@Get(‘/get/:id‘)
@ApiParam({
name: ‘id‘,
description: ‘这是用户id‘,
})
@ApiQuery({
name: ‘role‘,
description: ‘这是需要传递的参数‘,
})
@ApiHeader({
name: ‘authoriation‘,
required: true,
description: ‘本次请求请带上token‘,
})
public getUser(@Param(‘id‘) id: string, @Query(‘role‘) role: string): string {
return this.userService.getUser(id);
}
}
dto 文件 ApiProperty
// User.dto.ts
import { ApiProperty } from ‘@nestjs/swagger‘;
export class User {
@ApiProperty({
type: String,
description: ‘用户名‘,
default: ‘aadf‘,
})
username: string;
@ApiProperty({
description: ‘密码‘,
})
password: string;
}
ApiResponse 返回值
@ApiResponse({ status: 401, description: ‘权限不足‘})
@Post(‘/add‘)
public addUser(@Body() user: User) {
return user;
}
ApiImplicitFile 可以用于文件上传的文档测试
@ApiResponse({ status: 401, description: ‘权限不足‘})
@ApiImplicitFile({
name: ‘头像‘,
description: ‘上传头像‘,
required: false,
})
@Post(‘/add‘)
public addUser(@Body() user: User) {
return user;
}
包含部分模块的文档
const options = new DocumentBuilder()
.setTitle(‘用户信息文档‘)
.setDescription(‘用于用户信息的增删改查‘)
.setVersion(‘1.0‘)
.addTag(‘用户,安全‘)
.build();
const userDocument = SwaggerModule.createDocument(app, options, {
include: [UserModule], // 包含的模块
});
SwaggerModule.setup(‘api/user‘, app, userDocument);
测试
测试的写法为三步,引入测试内容,运行测试内容,最后做一个断言进行比较
jest.spyOn 生成mock函数
# 单元测试
$ docker exec -it nest yarn test
# e2e 测试
$ docker exec -it nest yarn test:e2e
# 测试覆盖率
$ docker exec -it nest yarn test:cov
常用的几个Jest断言
// beforeEach就是做测试前的准备工作或测试后的清理工作
// beforeAll, 在所有测试之前做什么
// describe 表示一组测试
// .not修饰符允许你测试结果不等于某个值的情况,
// 这和英语的语法几乎完全一样,很好理解。
// .toEqual匹配器会递归的检查对象所有属性和属性值是否相等,
// 所以如果要进行应用类型的比较时,请使用.toEqual匹配器而不是.toBe。
// .toHaveLength可以很方便的用来测试字符串和数组类型的长度是否满足预期。
// functions.test.js
import functions from ‘../src/functions‘;
test(‘getIntArray(3.3)应该抛出错误‘, () => {
function getIntArrayWrapFn() {
functions.getIntArray(3.3);
}
expect(getIntArrayWrapFn).toThrow(‘"getIntArray"只接受整数类型的参数‘);
})
// .toThorw可能够让我们测试被测试方法是否按照预期抛出异常,但是在使用时需要注意的是:我们必须使用一个函数将将被测试的函数做一个包装,正如上面getIntArrayWrapFn所做的那样,否则会因为函数抛出导致该断言失败。
// .toMatch传入一个正则表达式,它允许我们用来进行字符串类型的正则匹配。
原文:https://www.cnblogs.com/smss/p/13177374.html