Appearance
一些小案例
大文件上传
步骤:
- 后端定义一个上传接口和一个合并接口
- 前端进行分片上传,上传完成后调用合并接口
- 后端读取分片文件夹下的文件进行合并
- 合并后删除分片文件夹和文件
在 controller 中定义接口
typescript
// 创建处理文件上传的接口
@Post('upload')
// 使用FilesInterceptor拦截器处理上传的文件,允许一次最多上传20个文件,上传目录为'uploads'
@UseInterceptors(
FilesInterceptor('files', 20, {
dest: 'uploads',
}),
)
/**
* 上传文件函数
*
* @param files 上传的文件数组,由拦截器解析并注入
* @param body 请求体,可能包含除文件外的其他上传数据
*/
uploadFiles(
@UploadedFiles() files: Array<Express.Multer.File>,
@Body() body,
) {
// 打印请求体内容,用于调试和日志记录
console.log('body', body);
// 打印上传的文件信息,用于调试和日志记录
console.log('files', files);
}需要安装 multer 的类型
bash
npm install -D @types/multer在 main.ts 中开启跨域
ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors(); // 开启跨域
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();在 html 中试着请求 http://localhost:3000/upload 接口上传文件,并进行分片
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>大文件上传</title>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file" multiple/>
<script>
const fileInput = document.querySelector('#fileInput');
const chunkSize = 20 * 1024;
fileInput.onchange = async function () {
const file = fileInput.files[0];
const chunks = [];
let startPos = 0;
while(startPos < file.size) {
chunks.push(file.slice(startPos, startPos + chunkSize));
startPos += chunkSize;
}
chunks.map((chunk, index) => {
const data = new FormData();
data.set('name', file.name + '-' + index)
data.append('files', chunk);
axios.post('http://localhost:3000/upload', data);
})
}
</script>
</body>
</html>上传后就会发现文件夹下多了很多分片文件

接下来可以将同一张图片的分片放到同一个目录下,
ts
@Post('upload')
@UseInterceptors(
FilesInterceptor('files', 20, {
dest: 'uploads',
}),
)
uploadFiles(
@UploadedFiles() files: Array<Express.Multer.File>,
@Body() body: { name: string },
) {
// 从请求体的name中提取文件名前缀,用于后续的文件处理
const arr = body.name.match(/(.+)-\d+$/);
// 根据匹配结果提取文件名前缀,如果没有匹配到则使用原始name
const fileName = arr ? arr[1] : body.name;
// 拼接出分块目录路径
const chunkDir = 'uploads/chunks_' + fileName;
// 检查分块目录是否存在,如果不存在则创建
if (!fs.existsSync(chunkDir)) {
fs.mkdirSync(chunkDir);
}
// 将上传的文件复制到分块目录中,并使用原始name作为文件名
fs.cpSync(files[0].path, chunkDir + '/' + body.name);
// 删除上传的临时文件
fs.rmSync(files[0].path);
}上传一个 34KB 的 1.jpeg 图片,可以发现上传成功了
如何觉得 chunks 名称会冲突,也可以在html中的name改为随机数

得到分片后,需要将分片合成为一个文件,增加一个 merge 接口,供上传完成后调用
ts
/**
* 合并分块文件的函数
* 该函数用于将上传的分块文件合并为一个完整的文件
*
* @param name 文件名,通过查询参数传递,用于定位分块文件所在的目录
*/
@Get('merge')
merge(@Query('name') name: string) {
// 拼接出分块文件所在的目录路径
const chunkDir = 'uploads/chunks_' + name;
// 读取分块目录下的所有文件名
const files = fs.readdirSync(chunkDir);
let startPos = 0; // 初始化写入起始位置
files.map((file) => {
// 拼接每个分块文件的完整路径
const filePath = chunkDir + '/' + file;
// 创建可读流以读取分块文件内容
const stream = fs.createReadStream(filePath);
// 将可读流的内容写入到最终合并的文件中,并指定写入的起始位置
stream.pipe(
fs.createWriteStream('uploads/' + name, {
start: startPos,
}),
);
// 更新写入起始位置,累加上当前分块文件的大小
startPos += fs.statSync(filePath).size;
});
}html
<script>
const fileInput = document.querySelector('#fileInput');
const chunkSize = 20 * 1024;
fileInput.onchange = async function () {
const file = fileInput.files[0];
const chunks = [];
let startPos = 0;
while(startPos < file.size) {
chunks.push(file.slice(startPos, startPos + chunkSize));
startPos += chunkSize;
}
const tasks = []
const randomStr = Math.random().toString().slice(2, 8);
chunks.map((chunk, index) => {
const data = new FormData();
data.set('name', randomStr + '-' + file.name + '-' + index)
data.append('files', chunk);
tasks.push(axios.post('http://localhost:3000/upload', data));
})
await Promise.all(tasks);
axios.get('http://localhost:3000/merge?name=' + randomStr + '-' + file.name);
}
</script>
上传完是合成成功的,然后需要将原来的分片进行删除
在 controller 中的 merge 接口中,监听一个 finish 事件,文件合并完成后进行删除操作
ts
let count = 0;
let startPos = 0; // 初始化写入起始位置
files.map((file) => {
// 拼接每个分块文件的完整路径
const filePath = chunkDir + '/' + file;
// 创建可读流以读取分块文件内容
const stream = fs.createReadStream(filePath);
// 将可读流的内容写入到最终合并的文件中,并指定写入的起始位置
stream
.pipe(
fs.createWriteStream('uploads/' + name, {
start: startPos,
}),
)
.on('finish', () => {
count++;
if (count === files.length) {
fs.rm(
chunkDir,
{
recursive: true,
},
() => {},
);
}
});
// 更新写入起始位置,累加上当前分块文件的大小
startPos += fs.statSync(filePath).size;
});功能完成。
jwt 登录注册
创建一个项目
bash
nest new login-register -p npm安装 typeorm 和 MySQL 所需依赖
bash
npm i typeorm mysql2 @nestjs/typeorm生成 login 模块
bash
nest g resource login配置数据库
ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LoginModule } from './login/login.module';
import { Login } from './login/entities/login.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'nest_login',
synchronize: true, // 根据同步建表,也就是当 database 里没有和 Entity 对应的表的时候,会自动生成建表 sql 语句并执行
logging: true, // 打印生成的 sql 语句
entities: [Login], // 指定有哪些和数据库的表对应的 Entity
migrations: [], // 修改表结构之类的 sql
subscribers: [], // 一些 Entity 生命周期的订阅者,比如 insert、update、remove 前后,可以加入一些逻辑
poolSize: 10, // 指定数据库连接池中连接的最大数量
connectorPackage: 'mysql2', // 驱动包
extra: {
// 额外发送给驱动包的一些选项
authPlugin: 'sha256_password',
},
}),
LoginModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}创建 dto
ts
// login.dto
export class LoginDto {
username: string;
password: string;
}
// register.dto
export class RegisterDto {
username: string;
password: string;
}创建 entity
ts
import {
Entity,
Column,
CreateDateColumn,
UpdateDateColumn,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity({
name: 'login',
})
export class Login {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 50,
})
username: string;
@Column({
length: 50,
})
password: string;
@CreateDateColumn({
comment: '创建时间',
})
createdTime: Date;
@UpdateDateColumn({
comment: '更新时间',
})
updatedTime: Date;
}添加 登录和注册 接口
ts
import { Controller, Post, Body } from '@nestjs/common';
import { LoginService } from './login.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
@Controller('login')
export class LoginController {
constructor(private readonly loginService: LoginService) {}
@Post('register')
register(@Body() registerDto: RegisterDto) {
return this.loginService.register(registerDto);
}
@Post('login')
login(@Body() loginDto: LoginDto) {
return this.loginService.login(loginDto);
}
}service 中实现逻辑
ts
import { Body, HttpException, Injectable, Logger } from '@nestjs/common';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import * as crypto from 'crypto';
import { InjectRepository } from '@nestjs/typeorm';
import { Login } from './entities/login.entity';
import { Repository } from 'typeorm';
function md5(str: string) {
return crypto.createHash('md5').update(str).digest('hex');
}
@Injectable()
export class LoginService {
@InjectRepository(Login)
private readonly loginRepository: Repository<Login>;
private logger = new Logger();
async register(@Body() registerDto: RegisterDto) {
const { username, password } = registerDto;
const findUser = await this.loginRepository.findOne({
where: {
username,
},
});
if (findUser) {
throw new HttpException('用户已存在', 400);
}
const user = new Login();
user.username = username;
user.password = md5(password);
try {
await this.loginRepository.save(user);
return '注册成功';
} catch (e) {
this.logger.error(e, LoginService);
return '注册失败';
}
}
async login(@Body() loginDto: LoginDto) {
const { username, password } = loginDto;
const findUser = await this.loginRepository.findOne({
where: {
username,
},
});
if (!findUser) {
throw new HttpException('用户不存在', 400);
}
if (findUser.password !== md5(password)) {
throw new HttpException('密码错误', 400);
}
return findUser;
}
}注册效果

登录效果

接下来就添加注册时候的字段校验,登录后返回token
jwt
安装jwt包
bash
npm install @nestjs/jwtapp.module 中注册jwt模块
ts
@Module({
imports: [
LoginModule,
JwtModule.register({
global: true,
secret: 'fan',
signOptions: { expiresIn: '1d' },
}),
],
}登录成功后给header中设置token,也可以将token返回给前端
ts
@Inject(JwtService)
private jwtService: JwtService;
@Post('login')
async login(
@Body() loginDto: LoginDto,
@Res({ passthrough: true }) res: Response,
) {
const user = await this.loginService.login(loginDto);
if (user) {
const token = await this.jwtService.signAsync({
user: {
id: user.id,
username: user.username,
},
});
res.setHeader('Authorization', `Bearer ${token}`);
return '登录成功';
} else {
return '登录失败';
}
}登录成功后,header 中会返回token

可以新增几个测试接口,控制它们不登录不能进行请求
使用 Guard 来限制访问
bash
nest g guard login --no-spec --flat编写守卫
ts
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class LoginGuard implements CanActivate {
@Inject(JwtService)
private jwtService: JwtService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request: Request = context.switchToHttp().getRequest();
const authorization = request.header('Authorization') || '';
const bearer = authorization.split(' ');
if (!bearer || bearer.length < 2) {
throw new UnauthorizedException('登录 token 错误');
}
const token = bearer[1];
try {
const info = this.jwtService.verify(token);
(request as any).user = info.user;
return true;
} catch (e) {
throw new UnauthorizedException('登录 token 失效,请重新登录');
}
}
}使用守卫,在对应的接口上使用 UseGuards 来使用守卫
tsx
@Get('testToken')
@UseGuards(LoginGuard)
getTest(): string {
return 'test';
}测试结果

携带了token才能请求成功

参数校验
可以使用 ValidationPipe + class-validator 来做
bash
npm install class-validator class-transformer然后给 login 和 register 接口添加 ValidationPipe
ts
@Post('login')
async login(
@Body(ValidationPipe) loginDto: LoginDto,
@Res({ passthrough: true }) res: Response,
) {...}
@Post('register')
register(@Body(ValidationPipe) registerDto: RegisterDto) {
return this.loginService.register(registerDto);
}也可以全局添加 ValidationPipe
ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.PORT ?? 3000);
}给 dto 设置校验规则
ts
// login.dto
import { IsNotEmpty } from 'class-validator';
export class LoginDto {
@IsNotEmpty()
username: string;
@IsNotEmpty()
password: string;
}
// register.dto
import { IsNotEmpty, IsString, Length, Matches } from 'class-validator';
export class RegisterDto {
@IsString()
@IsNotEmpty()
@Length(6, 30)
@Matches(/^[a-zA-Z0-9#$%_-]+$/, {
message: '用户名只能是字母、数字或者 #、$、%、_、- 这些字符',
})
username: string;
@IsString()
@IsNotEmpty()
@Length(6, 30)
password: string;
}测试效果

关注功能
实现用户之间的关注、被关注和互相关注功能
创建一个项目
bash
nest new following -p npm安装所需依赖
bash
npm install @nestjs/typeorm typeorm mysql2生成用户模块
bash
nest g resource user --no-spec创建用户实体
ts
import {
Column,
Entity,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => User, (user) => user.following)
@JoinTable()
followers: User[];
@ManyToMany(() => User, (user) => user.followers)
following: User[];
}配置数据库
ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user/entities/user.entity';
@Module({
imports: [
UserModule,
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'following',
synchronize: true, // 根据同步建表,也就是当 database 里没有和 Entity 对应的表的时候,会自动生成建表 sql 语句并执行
logging: true, // 打印生成的 sql 语句
entities: [User], // 指定有哪些和数据库的表对应的 Entity
migrations: [], // 修改表结构之类的 sql
subscribers: [], // 一些 Entity 生命周期的订阅者,比如 insert、update、remove 前后,可以加入一些逻辑
poolSize: 10, // 指定数据库连接池中连接的最大数量
connectorPackage: 'mysql2', // 驱动包
extra: {
// 额外发送给驱动包的一些选项
authPlugin: 'sha256_password',
},
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}初始化一些数据
ts
import { User } from './entities/user.entity';
@Injectable()
export class UserService {
@InjectEntityManager()
private readonly entityManager: EntityManager;
async init() {
const user2 = new User();
user2.name = 'user2';
const user3 = new User();
user3.name = 'user3';
const user4 = new User();
user4.name = 'user4';
const user5 = new User();
user5.name = 'user5';
await this.entityManager.save(User, [user2, user3, user4, user5]);
const user1 = new User();
user1.name = 'user1';
user1.followers = [user2, user3];
user1.following = [user3, user4, user5];
await this.entityManager.save(User, user1);
}
}
user1的id是5,他的关注者有user2和user3,被关注者有user3、user4和user5,共同关注就是user3

接下来实现互相关注功能,安装 redis
bash
npm i redis生成模块
bash
nest g module redis
nest g service redis在 RedisModule 创建连接 redis 的 provider,导出 RedisService,并把这个模块标记为 @Global 模块
ts
import { Global, Logger, Module } from '@nestjs/common';
import { RedisService } from './redis.service';
import { createClient } from 'redis';
@Global()
@Module({
providers: [
RedisService,
{
provide: 'REDIS_CLIENT',
async useFactory() {
const logger = new Logger('RedisFactory');
const client = createClient({
socket: {
host: 'localhost',
port: 6379,
},
});
try {
await client.connect();
logger.log('Redis client connected successfully');
return client;
} catch (error) {
logger.error('Failed to connect to Redis', error);
throw new Error('Redis connection failed');
}
},
},
],
exports: [RedisService],
})
export class RedisModule {}封装一些方法来操作redis
ts
import { Inject, Injectable } from '@nestjs/common';
import { RedisClientType } from 'redis';
@Injectable()
export class RedisService {
@Inject('REDIS_CLIENT')
private redisClient: RedisClientType;
// 添加数据
async sAdd(key: string, ...members: string[]) {
return this.redisClient.sAdd(key, members);
}
// 求两个数据的交集
async sInterStore(newSetKey: string, set1: string, set2: string) {
return this.redisClient.sInterStore(newSetKey, [set1, set2]);
}
// 判断一个元素是否在集合中
async sIsMember(key: string, member: string) {
return this.redisClient.sIsMember(key, member);
}
// 获取集合中的所有元素
async sMember(key: string) {
return this.redisClient.sMembers(key);
}
async exists(key: string) {
const result = await this.redisClient.exists(key);
return result > 0;
}
}user.controller 中实现个方法,用来根据id获取他关注、被关注和互相关注的人
ts
@Get('follow-relationship')
async followRelationShip(@Query('id') id: string) {
if (!id) {
throw new BadRequestException('userId 不能为空');
}
return this.userService.getFollowRelationship(+id);
}实现改接口
ts
@Inject(RedisService)
private redisService: RedisService;
async findUserByIds(userIds: string[] | number[]) {
let users: Array<User | null> = [];
for (let i = 0; i < userIds.length; i++) {
const user = await this.entityManager.findOne(User, {
where: {
id: +userIds[i],
},
});
users.push(user);
}
return users;
}
async getFollowRelationship(userId: number) {
// 检查 Redis 中是否存在用户的关注者和被关注者信息
const exists = await this.redisService.exists('followers:' + userId);
if (!exists) {
// 如果不存在,则从数据库中查询用户及其关注者和被关注者信息
const user = await this.entityManager.findOne(User, {
where: {
id: userId,
},
relations: ['followers', 'following'],
});
// 如果用户没有关注者或被关注者,则直接返回空数组
if (!user?.followers.length || !user?.following.length) {
return {
followers: user?.followers,
following: user?.following,
followEachOther: [],
};
}
// 将关注者 ID 存储到 Redis 中
await this.redisService.sAdd(
'followers:' + userId,
...user.followers.map((item) => item.id.toString()),
);
// 将被关注者 ID 存储到 Redis 中
await this.redisService.sAdd(
'following:' + userId,
...user.following.map((item) => item.id.toString()),
);
// 计算互相关注的用户 ID 并存储到 Redis 中
await this.redisService.sInterStore(
'follow-each-other:' + userId,
'followers:' + userId,
'following:' + userId,
);
// 获取互相关注的用户 ID
const followEachOtherIds = await this.redisService.sMember(
'follow-each-other:' + userId,
);
// 根据互相关注的用户 ID 查询用户信息
const followEachOtherUsers = await this.findUserByIds(followEachOtherIds);
// 返回用户的关注者、被关注者和互相关注的用户信息
return {
followers: user.followers,
following: user.following,
followEachOther: followEachOtherUsers,
};
} else {
// 如果 Redis 中存在用户的关注者和被关注者信息,则从 Redis 中获取
const followerIds = await this.redisService.sMember(
'followers:' + userId,
);
// 根据关注者 ID 查询用户信息
const followUsers = await this.findUserByIds(followerIds);
const followingIds = await this.redisService.sMember(
'following:' + userId,
);
// 根据被关注者 ID 查询用户信息
const followingUsers = await this.findUserByIds(followingIds);
const followEachOtherIds = await this.redisService.sMember(
'follow-each-other:' + userId,
);
// 根据互相关注的用户 ID 查询用户信息
const followEachOtherUsers = await this.findUserByIds(followEachOtherIds);
// 返回用户的关注者、被关注者和互相关注的用户信息
return {
followers: followUsers,
following: followingUsers,
followEachOtherUsers: followEachOtherUsers,
};
}
}得到的结果

实现关注功能,传入关注人和被关注人的id
ts
@Get('follow')
async follow(@Query('id1') userId1: string, @Query('id2') userId2: string) {
await this.userService.follow(+userId1, +userId2);
return '关注成功';
}ts
async follow(userId: number, userId2: number) {
const user = await this.entityManager.findOne(User, {
where: {
id: userId,
},
relations: ['followers', 'following'],
});
const user2 = await this.entityManager.findOne(User, {
where: {
id: userId2,
},
});
if (user2) {
user?.followers.push(user2);
}
if (user) {
await this.entityManager.save(User, user);
}
const exists = await this.redisService.exists('followers:' + userId);
if (exists) {
await this.redisService.sAdd('followers:' + userId, userId2.toString());
await this.redisService.sInterStore(
'follow-each-other:' + userId,
'followers:' + userId,
'following:' + userId,
);
}
const exists2 = await this.redisService.exists('following:' + userId2);
if (exists2) {
await this.redisService.sAdd('following:' + userId2, userId.toString());
await this.redisService.sInterStore(
'follow-each-other:' + userId2,
'followers:' + userId2,
'following:' + userId2,
);
}
}请求 follow 接口 http://localhost:3000/user/follow?id1=1&id2=5
重新请求查看互相关注接口 http://localhost:3000/user/follow-relationship?id=5,互相关注里多了user2

聊天室
创建项目
bash
nest new chat-room-backend -p npm安装typeorm和mysql2
bash
npm install @nestjs/typeorm typeorm mysql2配置数据库
ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './user/user.module';
// 应用程序需要导入的模块
const imports = [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'chat_room',
synchronize: true, // 根据同步建表,也就是当 database 里没有和 Entity 对应的表的时候,会自动生成建表 sql 语句并执行
logging: true, // 打印生成的 sql 语句
entities: [], // 指定有哪些和数据库的表对应的 Entity
migrations: [], // 修改表结构之类的 sql
subscribers: [], // 一些 Entity 生命周期的订阅者,比如 insert、update、remove 前后,可以加入一些逻辑
poolSize: 10, // 指定数据库连接池中连接的最大数量
connectorPackage: 'mysql2', // 驱动包
extra: {
// 额外发送给驱动包的一些选项
authPlugin: 'sha256_password',
},
}),
];
@Module({
imports: [...imports, UserModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}生成用户模块
bash
nest g resource user --no-spec编写用户实体
ts
