主题
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层需要更改。