配置抽离
上节我们实现发送验证码功能,这节我们继续实现用户模块的注册功能。
在那之前,我们先把后端项目优化一下, 把配置抽离写在一个文件中。现在的 MySQL、redis、nodemailer 等配置都写在代码中,不好维护。
安装 config 包:
npm i --save @nestjs/config
在 AppModule 中引入:
// src/app.module.ts
....
import { RedisModule } from './redis/redis.module';
import { ConfigModule } from '@nestjs/config';
....
imports: [
ConfigModule.forRoot({
isGlobal: true, // 设置为全局配置
envFilePath: 'src/.env' // 配置文件路径为 src/.env
}),
....
然后在 src 目录下新建.env 文件,把之前的 redis 配置写在里面:
# redis 相关配置
redis_server_host=localhost
redis_server_port=6379
redis_server_db=0
然后在 RedisModule 里注入 ConfigService 来读取配置并打印一下:
// src/redis/redis.module.ts
....
import { ConfigService } from '@nestjs/config';
....
{
provide: 'REDIS_CLIENT',
async useFactory(configService: ConfigService) {
console.log(configService.get('redis_server_host'));
console.log(configService.get('redis_server_port'));
console.log(configService.get('redis_server_db'));
const client = createClient({
socket: {
host: 'localhost',
port: 6379,
},
database: 0,
});
await client.connect();
return client;
},
inject: [ConfigService],
},
....
保存后 会自动重启(如果没有重启可以手动重启一下),打印出配置信息。
看,已经正确读取到了.env 文件的配置了(PS:这里的端口号写错了,redis 服务的端口号是 6379。但是已经生好图片了,就不改了。下面的配置我改过来)。
但我们的最终目的是在生产环境读取到生产环境的配置,而不是本地的配置。也就是在 nestjs 项目打包后的dist目录下读取到.env文件的配置。这需要在项目打包的时候把.env文件也打包进去。
先做一个实验:
停止服务,删除项目中的dist目录,然后跑下 npm run build:
可以看到,dist 目录下没有.env文件。
我们在 nest-cli.json 中配置一下assets:
....
"compilerOptions": {
"deleteOutDir": true,
"watchAssets": true,
"assets": ["**/*.env"]
}
....
asssets 是指定 build 时复制的文件,watchAssets 是在 assets 变动之后自动重新复制。
把 dist 删掉,跑下 npm run build。
可以看到,dist 目录下有了.env文件,并且是正确的。
这是一种方法,是用到了nest cli的复制assets的功能。当然了,还有一种方法是把.env文件也放在根目录下,手动在打包逻辑中添加复制逻辑。
/// package.json
....
"scripts": {
"build": "nest build && cp .env dist/",
}
....
这样再跑 npm run build,也会把 .env 复制过去。
在.env文件中添加 redis、mysql、nodemailer 和 nest 服务的配置:
# redis 相关配置
redis_server_host=localhost
redis_server_port=6379
redis_server_db=1
# nodemailer 相关配置
nodemailer_host=smtp.qq.com
nodemailer_port=587
nodemailer_auth_user=你的邮箱
nodemailer_auth_pass=你的授权码
# mysql 相关配置
mysql_server_host=localhost
mysql_server_port=3306
mysql_server_username=root
mysql_server_password=guang
mysql_server_database=account
# nest 服务配置
nest_server_port=3000
将各个配置注入到对应的模块中:
// src/redis/redis.module.ts
....
const client = createClient({
socket: {
host: configService.get('redis_server_host'),
port: configService.get('redis_server_port'),
},
database: configService.get('redis_server_db'),
});
....
// src/email/email.service.ts
....
import { ConfigService } from '@nestjs/config';
....
constructor(private configService: ConfigService) {
this.transporter = createTransport({
host: this.configService.get('nodemailer_host'),
port: this.configService.get('nodemailer_port'),
secure: false,
auth: {
user: this.configService.get('nodemailer_auth_user'),
pass: this.configService.get('nodemailer_auth_pass'),
},
});
}
....
from: {
name: '记账小助手',
address: this.configService.get('nodemailer_auth_user'),
},
// src/app.module.ts
....
import { ConfigModule, ConfigService } from '@nestjs/config';
....
TypeOrmModule.forRootAsync({
useFactory(configService: ConfigService) {
return {
type: 'mysql',
host: configService.get('mysql_server_host'),
port: configService.get('mysql_server_port'),
username: configService.get('mysql_server_username'),
password: configService.get('mysql_server_password'),
database: configService.get('mysql_server_database'),
synchronize: true,
logging: true,
entities: [User],
poolSize: 10,
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password',
},
};
},
inject: [ConfigService],
}),
// src/main.ts
....
import { ConfigService } from '@nestjs/config';
....
app.enableCors();
const configService = app.get(ConfigService);
await app.listen(configService.get('nest_server_port'));
....
在前端输入一个正确的邮箱,然后发送验证码:
可以看到,配置抽离成功了。再去看一下 redis 和邮箱,验证码也一样,具体图片就不贴了。
小优化
编辑完后,服务重启时,总是在命令终端中弹出这些提示:
这是因为我们在 app.module.ts 中引入了 TypeOrmModule 的配置造成的。
第一个因为我做了陈旧的配置authPlugin造成的,注释掉就好了。
第二个是数据写入数据库的 SQL 语句打印出来了,我们可以在关闭这个功能。在生产中也不需要打印这些信息。具体是把 logging 改成 false:
// src/app.module.ts
....
logging: false,
....
extra: {
// authPlugin: 'sha256_password',
},
还有一个就是,在前端项目代码中,我把验证码字段写成了verify,正确的是captcha。改一下,setVerify 改成 setCaptcha。这样比较严谨一点。
注册功能
注册其实很简单,就是把用户信息写入数据库。
大致流程如下:
- 用户输入邮箱、验证码、密码、确认密码。然后点击提交。
- 前端先对各个信息进行校验。
- 如果校验通过,前端将邮箱、验证码、密码发送到后端。
- 后端再做一次请求体校验。
- 如果校验通过,后端先从redis中取出验证码,然后和前端传来的验证码进行对比。
- 如果验证码一致,后端再去查询数据库中是否存在该邮箱。
- 后端再检查两次密码是否一致。
- 如果一致就向数据库中写入用户信息。
前端校验
先在src/pages/Login/index.tsx的注册按钮上添加一个点击事件:
// src/pages/Login/index.tsx
....
<Button block theme="primary" onClick={Submit}>
注册
</Button>
在点击事件中,我们做各种校验:
// src/pages/Login/index.tsx
....
const handleGetCaptcha = async () => {
.....
}
const Submit = async () => {
// 验证用户名是否为空
if (!username) {
Toast.show("请输入邮箱作为用户名");
return;
}
// 验证用户名是否为邮箱格式
if (!validateEmail(username)) {
Toast.show("请输入正确的邮箱");
return;
}
// 验证验证码是否为空
if (!captcha) {
Toast.show("请输入验证码");
return;
}
// 验证密码是否为空
if (!password) {
Toast.show("请输入密码");
return;
}
// 验证两次密码是否一致
if (password!== confirmPassword) {
Toast.show("两次密码不一致,请重新输入");
return;
}
}
前端添加请求接口
在前端src/utils/request.ts中添加一个注册接口并导出:
// src/utils/request.ts
....
export async function register(params:RegisterUser) {
return await axiosInstance.post("/user/register", params);
}
然后在src/pages/Login/index.tsx中引入并使用:
....
import { getCaptcha, register } from "@/utils/request";
....
const Submit = async () => {
....
const res = await register({
username,
captcha,
password,
confirmPassword,
});
console.log(res);
}
后端校验注册
在src/user/user.controller.ts中添加一个注册接口:
// src/user/user.controller.ts
....
import {
Controller,
Get,
Query,
Post,
Body,
} from '@nestjs/common';
....
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
....
@Post('register')
async register(@Body() user) {
console.log(user);
}
}
后端添加请求体校验
先在src/user/user.entity.ts中添加一个password字段,用来存储密码:
// src/user/user.entity.ts
....
@Column({
length: 50,
comment: '用户名'
})
username: string;
@Column({
length: 100,
comment: '密码'
})
password: string;
....
在src/user/dto的目录中添加一个register.dto.ts文件,并写下代码:
import { IsNotEmpty, IsString, MinLength, IsEmail } from 'class-validator';
export class RegisterDto {
@IsNotEmpty({
message: '用户名不能为空',
})
@IsEmail(
{},
{
message: '用户名必须是邮箱地址',
},
)
username: string;
@MinLength(6, {
message: '密码不能不少于6位',
})
@IsNotEmpty({
message: '验证码不能为空',
})
captcha: string;
@IsNotEmpty({
message: '密码不能为空',
})
@MinLength(6, {
message: '密码不能不少于6位',
})
password: string;
@IsNotEmpty({
message: '密码不能为空',
})
@MinLength(6, {
message: '密码不能不少于6位',
})
confirmPassword: string;
}
在src/user/user.controller.ts中引入并使用:
// src/user/user.controller.ts
....
import { RegisterDto } from './dto/register.dto';
...
@Post('register')
async register(@Body() user:RegisterDto) {
return await this.userService.register(user);
}
在src/user/user.service.ts中添加一个注册方法:
// src/user/user.service.ts
....
@Injectable()
export class UserService {
....
async register(user) {}
}
获取redis中的验证码:
// src/user/user.service.ts
....
async register(user) {
const redisCaptcha = await this.redisService.get(user.username);
console.log(redisCaptcha);
}
验证验证码是否获取成功,没有成功就抛出异常:
// src/user/user.service.ts
....
async register(user) {
....
if (!captcha) {
throw new BadRequestException('没有找到相关的验证码,请重新获取');
}
}
验证验证码是否一致:
// src/user/user.service.ts
....
async register(user) {
....
if (captcha!== redisCaptcha) {
throw new BadRequestException('验证码错误,请重新输入');
}
}
查询数据库中是否存在该邮箱:
// src/user/user.service.ts
....
async register(user) {
const findUser = await this.entityManager.findOne(User, {
where: {
username: user.username,
},
});
if (findUser) {
throw new BadRequestException('该邮箱已注册了');
}
}
验证两次密码是否一致:
// src/user/user.service.ts
....
async register(user) {
.....
if (user.password!== user.confirmPassword) {
throw new BadRequestException('两次密码不一致,请重新输入');
}
}
如果验证通过,就将用户信息写入数据库,否则就提示失败:
// src/user/user.service.ts
....
async register(user) {
.....
try {
await this.entityManager.save(User, {
username: user.username,
password: user.password,
});
return '注册成功';
} catch (error) {
return '注册失败';
}
}
写到这里,我们的注册功能就完成了。