重构《node+React实战:从0到1实现记账本》记录(四)用户模块:后端项目配置抽离和用户注册功能

201 阅读7分钟

配置抽离

上节我们实现发送验证码功能,这节我们继续实现用户模块的注册功能。

在那之前,我们先把后端项目优化一下, 把配置抽离写在一个文件中。现在的 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],
},
....

保存后 会自动重启(如果没有重启可以手动重启一下),打印出配置信息。

image.png

看,已经正确读取到了.env 文件的配置了(PS:这里的端口号写错了,redis 服务的端口号是 6379。但是已经生好图片了,就不改了。下面的配置我改过来)。

但我们的最终目的是在生产环境读取到生产环境的配置,而不是本地的配置。也就是在 nestjs 项目打包后的dist目录下读取到.env文件的配置。这需要在项目打包的时候把.env文件也打包进去。

先做一个实验:

停止服务,删除项目中的dist目录,然后跑下 npm run build

image.png

可以看到,dist 目录下没有.env文件。

我们在 nest-cli.json 中配置一下assets

....
"compilerOptions": {
  "deleteOutDir": true,
  "watchAssets": true,
  "assets": ["**/*.env"]
}
....

asssets 是指定 build 时复制的文件,watchAssets 是在 assets 变动之后自动重新复制。

dist 删掉,跑下 npm run build

image.png

可以看到,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'));
....

在前端输入一个正确的邮箱,然后发送验证码:

image.png

可以看到,配置抽离成功了。再去看一下 redis 和邮箱,验证码也一样,具体图片就不贴了。

小优化

编辑完后,服务重启时,总是在命令终端中弹出这些提示:

image.png

这是因为我们在 app.module.ts 中引入了 TypeOrmModule 的配置造成的。

第一个因为我做了陈旧的配置authPlugin造成的,注释掉就好了。

第二个是数据写入数据库的 SQL 语句打印出来了,我们可以在关闭这个功能。在生产中也不需要打印这些信息。具体是把 logging 改成 false

// src/app.module.ts
....
logging: false,
....
extra: {
  // authPlugin: 'sha256_password',
},

还有一个就是,在前端项目代码中,我把验证码字段写成了verify,正确的是captcha。改一下,setVerify 改成 setCaptcha。这样比较严谨一点。

注册功能

注册其实很简单,就是把用户信息写入数据库。

大致流程如下:

register.jpg

  1. 用户输入邮箱、验证码、密码、确认密码。然后点击提交。
  2. 前端先对各个信息进行校验。
  3. 如果校验通过,前端将邮箱、验证码、密码发送到后端。
  4. 后端再做一次请求体校验。
  5. 如果校验通过,后端先从redis中取出验证码,然后和前端传来的验证码进行对比。
  6. 如果验证码一致,后端再去查询数据库中是否存在该邮箱。
  7. 后端再检查两次密码是否一致。
  8. 如果一致就向数据库中写入用户信息。

前端校验

先在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 '注册失败';
    }
  }

写到这里,我们的注册功能就完成了。