Skip to content

nestjs用于构建高效且可伸缩的服务端应用程序的渐进式Node.js框架。

首先我们来以一个示例nestjs-realworld-example-app项目来展开对nest的介绍。该项目ORM框架是TypeORM,当然还有prisma等等,我们不做重点讨论。

  • TypeORM 的配置,ormconfig.json
json
{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "root",
  "password": "password",
  "database": "nestjsrealworld",
  "entities": ["src/**/**.entity{.ts,.js}"],
  "synchronize": true
}

entities配置项是我们重点关注的对象,该配置是所有实体类所在的路径或命名,项目启动的时候,会对所有的实体对象进行一遍扫描,建立表与实体之间的Column与字段的关联映射。完成数据库相关的配置文件,当启动项目后,会自动创建表结构。

现在我们的目光转移到src目录结构下。

  • main.ts 为整个项目的入口文件,我们来看看里面的代码:
ts
import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const appOptions = {cors: true};
  const app = await NestFactory.create(ApplicationModule, appOptions);
  // 项目中全局的Controller path的前置
  app.setGlobalPrefix('api');

  const options = new DocumentBuilder()
    .setTitle('NestJS Realworld Example App') // swagger ui的页面显示
    .setDescription('The Realworld API description') // swagger ui的页面显示
    .setVersion('1.0')
    .setBasePath('api')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, options);
  // https://swagger.io/tools/swagger-ui/
  // swagger ui的访问路径 http://localhost:3000/docs/
  SwaggerModule.setup('/docs', app, document);
  
  // 端口号
  await app.listen(3000);
}
bootstrap();

由于模块过多,我们以user模块为例:

  • user Entity:实体类,其中的注解是重点
ts
import {Entity, PrimaryGeneratedColumn, Column, BeforeInsert, JoinTable, ManyToMany, OneToMany} from 'typeorm';
import { IsEmail } from 'class-validator';
import * as argon2 from 'argon2';
import { ArticleEntity } from '../article/article.entity';

// 表明该类是一个实体,且命名和实体表保持一致
// 实体类字段和表结构的Column一一对应, 包括类型,默认值等
@Entity('user')
export class UserEntity {
  
  // id自动生成,该字段不需要传值
  @PrimaryGeneratedColumn()
  id: number;
  
  // 表明是实体表中的一个字段
  @Column()
  username: string;

  @Column()
  // 对email格式进行校验
  @IsEmail()
  email: string;
  
  // 设置默认值
  @Column({default: ''})
  bio: string;

  @Column({default: ''})
  image: string;

  @Column()
  password: string;
  
  // 在插入之前对密码的自动加密
  @BeforeInsert()
  async hashPassword() {
    this.password = await argon2.hash(this.password);
  }
  
  // 多对多
  // ManyToMany需要和@JoinTable表结合使用,ManyToMany总是使用中间关系连接表来存储关系。
  // 一个用户可以喜欢多篇文章
  // 一篇文章也可以被多个人喜欢
  @ManyToMany(type => ArticleEntity)
  @JoinTable()
  favorites: ArticleEntity[];
  

  // 一对多
  // 一个作者可以写多篇文章
  // 但是一篇文章只能有一个作者(特殊情况除外)
  @OneToMany(type => ArticleEntity, article => article.author)
  articles: ArticleEntity[];
}
  • user Controller: 控制层
ts
import { Get, Post, Body, Put, Delete, Param, Controller, UsePipes } from '@nestjs/common';
import { Request } from 'express';
import { UserService } from './user.service';
import { UserRO } from './user.interface';
import { CreateUserDto, UpdateUserDto, LoginUserDto } from './dto';
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { User } from './user.decorator';
import { ValidationPipe } from '../shared/pipes/validation.pipe';

import {
  ApiBearerAuth, ApiTags
} from '@nestjs/swagger';

// 承载swagger发起请求的身份验证
@ApiBearerAuth()
// swagger ui api的分类tag标签
@ApiTags('user')
@Controller()
export class UserController {
   
  constructor(private readonly userService: UserService) {}

  @Get('user')
  async findMe(@User('email') email: string): Promise<UserRO> {
    return await this.userService.findByEmail(email);
  }

  @Put('user')
  async update(@User('id') userId: number, @Body('user') userData: UpdateUserDto) {
    return await this.userService.update(userId, userData);
  }
  
  // 转换:管道将输入数据转换为所需的数据输出
  // 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;
  // 就是对请求的参数提前做一个校验或转换,如果成功继续执行,校验失败直接抛出异常
  @UsePipes(new ValidationPipe())
  @Post('users')
  async create(@Body('user') userData: CreateUserDto) {
    return this.userService.create(userData);
  }

  @Delete('users/:slug')
  async delete(@Param() params) {
    return await this.userService.delete(params.slug);
  }

  @UsePipes(new ValidationPipe())
  @Post('users/login')
  async login(@Body('user') loginUserDto: LoginUserDto): Promise<UserRO> {
    const _user = await this.userService.findOne(loginUserDto);

    const errors = {User: ' not found'};
    if (!_user) throw new HttpException({errors}, 401);

    const token = await this.userService.generateJWT(_user);
    const {email, username, bio, image} = _user;
    const user = {email, token, username, bio, image};
    return {user}
  }
}
  • user Service: 数据库的相关操作
ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository, DeleteResult } from 'typeorm';
import { UserEntity } from './user.entity';
import {CreateUserDto, LoginUserDto, UpdateUserDto} from './dto';
const jwt = require('jsonwebtoken');
import { SECRET } from '../config';
import { UserRO } from './user.interface';
import { validate } from 'class-validator';
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { HttpStatus } from '@nestjs/common';
import * as argon2 from 'argon2';

// 可被扫描
@Injectable()
export class UserService {
  constructor(
    // 建立表user和UserEntity之间的联系
    // ORM接管对表的操作
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>
  ) {}

  async findAll(): Promise<UserEntity[]> {
    // ORM框架提供,不需要自己写SQL,表明查user这张表的所有数据
    return await this.userRepository.find();
  }

  async findOne({email, password}: LoginUserDto): Promise<UserEntity> {
    // 根据email查询
    const user = await this.userRepository.findOne({email});
    if (!user) {
      return null;
    }

    if (await argon2.verify(user.password, password)) {
      return user;
    }

    return null;
  }

  async create(dto: CreateUserDto): Promise<UserRO> {

    // check uniqueness of username/email
    const {username, email, password} = dto;
    // 相当于 select * from user where user.username = ${username} or user.email = ${email}
    const qb = await getRepository(UserEntity)
      .createQueryBuilder('user')
      .where('user.username = :username', { username })
      .orWhere('user.email = :email', { email });

    const user = await qb.getOne();

    if (user) {
      const errors = {username: 'Username and email must be unique.'};
      throw new HttpException({message: 'Input data validation failed', errors}, HttpStatus.BAD_REQUEST);

    }

    // create new user
    let newUser = new UserEntity();
    newUser.username = username;
    newUser.email = email;
    newUser.password = password;
    newUser.articles = [];

    const errors = await validate(newUser);
    if (errors.length > 0) {
      const _errors = {username: 'Userinput is not valid.'};
      throw new HttpException({message: 'Input data validation failed', _errors}, HttpStatus.BAD_REQUEST);

    } else {
      const savedUser = await this.userRepository.save(newUser);
      return this.buildUserRO(savedUser);
    }

  }
  
  // save是save or update
  // 会根据主键来进行判断,主键存在就update,不存在就save
  async update(id: number, dto: UpdateUserDto): Promise<UserEntity> {
    let toUpdate = await this.userRepository.findOne(id);
    delete toUpdate.password;
    delete toUpdate.favorites;

    let updated = Object.assign(toUpdate, dto);
    return await this.userRepository.save(updated);
  }

  async delete(email: string): Promise<DeleteResult> {
    return await this.userRepository.delete({ email: email});
  }

  async findById(id: number): Promise<UserRO>{
    const user = await this.userRepository.findOne(id);

    if (!user) {
      const errors = {User: ' not found'};
      throw new HttpException({errors}, 401);
    }

    return this.buildUserRO(user);
  }

  async findByEmail(email: string): Promise<UserRO>{
    const user = await this.userRepository.findOne({email: email});
    return this.buildUserRO(user);
  }

  public generateJWT(user) {
    let today = new Date();
    let exp = new Date(today);
    exp.setDate(today.getDate() + 60);

    return jwt.sign({
      id: user.id,
      username: user.username,
      email: user.email,
      exp: exp.getTime() / 1000,
    }, SECRET);
  };

  private buildUserRO(user: UserEntity) {
    const userRO = {
      id: user.id,
      username: user.username,
      email: user.email,
      bio: user.bio,
      token: this.generateJWT(user),
      image: user.image
    };

    return {user: userRO};
  }
}
  • user dto: 就是执行某种操作需要用到的对象,字段不多不少,一般情况下不会是整个对象的字段
ts
import { IsNotEmpty } from 'class-validator';

export class LoginUserDto {

  @IsNotEmpty()
  readonly email: string;

  @IsNotEmpty()
  readonly password: string;
}
  • user auth middleware: 拦截器,对一些请求操作进行拦截
ts
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { NestMiddleware, HttpStatus, Injectable } from '@nestjs/common';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request, Response, NextFunction } from 'express';
import * as jwt from 'jsonwebtoken';
import { SECRET } from '../config';
import { UserService } from './user.service';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private readonly userService: UserService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const authHeaders = req.headers.authorization;
    if (authHeaders && (authHeaders as string).split(' ')[1]) {
      const token = (authHeaders as string).split(' ')[1];
      const decoded: any = jwt.verify(token, SECRET);
      const user = await this.userService.findById(decoded.id);

      if (!user) {
        throw new HttpException('User not found.', HttpStatus.UNAUTHORIZED);
      }

      req.user = user.user;
      next();

    } else {
      throw new HttpException('Not authorized.', HttpStatus.UNAUTHORIZED);
    }
  }
}
  • user modules: 整个user模快
ts
import {MiddlewareConsumer, Module, NestModule, RequestMethod} from '@nestjs/common';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './user.entity';
import { UserService } from './user.service';
import { AuthMiddleware } from './auth.middleware';

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity])],
  providers: [UserService],
  controllers: [
    UserController
  ],
  exports: [UserService]
})
export class UserModule implements NestModule {
  public configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware) // 拦截请求
      .forRoutes({path: 'user', method: RequestMethod.GET}, {path: 'user', method: RequestMethod.PUT});
  }
}

看了以上的代码,你会发现ORM那一套,和hibernate基本一致,应对简单的crud操作十分方便。整个项目的风格就和spring boot一样,一目了然的项目入口,简单的配置。如果要更换其他的ORM框架,其中实体类和service层需要更改。

Powered by VitePress.