写在前面
Nest(NestJS)是用于构建高效,可扩展的Node.js服务器端应用程序的框架。它使用渐进式JavaScript,并已构建并完全支持TypeScript(但仍使开发人员能够使用纯JavaScript进行编码),并结合了OOP(面向对象编程),FP(功能编程)和FRP(功能反应性编程)的元素。
这次我就帮助童鞋快速入门,尽快应用起 nest,在实战中学习 nest,对工作和学习都有更好的帮助。
1、利用基于 nest 快速搭建项目。
2、学习如何连接 mysql 数据库,利用 typeorm 操作 mysql 数据库。
3、创建用户模块,并进行 token 验证
4、返回数据进行处理
5、文件上传
1、利用基于 nest 快速搭建项目
使用 nest cli 快速创建项目
$ npm i -g @nestjs/cli
$ nest new project-name
将创建 project 目录, 安装node模块和一些其他样板文件,并将创建一个 src 目录,目录中包含几个核心文件。
src
├── app.controller.ts
├── app.module.ts
└── main.ts
以下是这些核心文件的简要概述:
app.controller.ts 带有单个路由的基本控制器示例。
app.module.ts 应用程序的根模块。
main.ts 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。
运行程序
npm run start
在应用程序运行时, 打开浏览器并访问 http://localhost:3000/。 应该看到 Hello world! 信息。
2、学习如何连接 mysql 数据库,利用 typeorm 操作 mysql 数据库。
typeorm 对 mysql 数据库版本有要求,需要5.6以上,在这个地方遇到过问题,找了很长时间,才发现这个问题。最好是使用最新的 mysql 数据库。
运行命令安装 mysql、typeorm
npm install --save @nestjs/typeorm typeorm mysql
安装过程完成后,我们可以将 TypeOrmModule 导入AppModule 。
app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
],
})
export class AppModule {}
我们可以也创建 ormconfig.json ,而不是将配置对象传递给 forRoot()。
{
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "root",
"database": "test",
"entities": ["dist/**/*.entity{.ts,.js}"],
"synchronize": true
}
然后,我们可以不带任何选项地调用 forRoot() :
app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forRoot()],
})
export class AppModule {}
创建实体,这里创建 admin 实体为例。
import {
Entity,
PrimaryGeneratedColumn,
Column,
BeforeInsert,
JoinTable,
ManyToMany,
OneToMany,
BeforeUpdate
} from 'typeorm';
import { IsEmail } from 'class-validator';
import * as argon2 from 'argon2';
@Entity('admin')
export class AdminEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column()
password: string;
@BeforeInsert()
async hashPassword() {
this.password = await argon2.hash(this.password);
}
}
admin.module.ts
import {MiddlewareConsumer, Module, NestModule, RequestMethod} from '@nestjs/common';
import { AdminController } from './admin.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminEntity } from './admin.entity';
import { AdminService } from './admin.service';
import { AuthMiddleware } from './auth.middleware';
@Module({
imports: [TypeOrmModule.forFeature([AdminEntity])],
providers: [AdminService],
controllers: [
AdminController
],
exports: [AdminService]
})
此模块使用 forFeature() 方法定义在当前范围中注册哪些存储库。这样,我们就可以使用 @InjectRepository()装饰器将 adminRepository 注入到 AdminService 中:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository, DeleteResult } from 'typeorm';
import { AdminEntity } from './admin.entity';
import {UserDto, UserPassDto} from './dto';
const jwt = require('jsonwebtoken');
import { SECRET } from '../config';
import { validate } from 'class-validator';
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { HttpStatus } from '@nestjs/common';
import * as argon2 from 'argon2';
import {MsgService} from "../msg/msg.service";
@Injectable()
export class AdminService {
constructor(
@InjectRepository(AdminEntity)
private readonly adminRepository: Repository<AdminEntity>,
private readonly MSG: MsgService
) {}
async findAll(): Promise<AdminEntity[]> {
return await this.adminRepository.find();
}
async findOne({username, password}: UserDto): Promise<AdminEntity> {
const user = await this.adminRepository.findOne({username});
if (!user) {
return null;
}
if (await argon2.verify(user.password, password)) {
return user;
}
return null;
}
}
3、创建用户模块,并进行 token 验证
使用 nest 命令创建用户模块
nest g module admin
nest g service admin
nest g controller admin
这里我们使用中间件middleware来做用户的 token 验证
中间件是在路由处理程序 之前 调用的函数。 中间件函数可以访问请求和响应对象,以及应用程序请求响应周期中的 next()
中间件函数。 next()
中间件函数通常由名为 next
的变量表示。
Nest 中间件实际上等价于 express 中间件。 下面是Express官方文档中所述的中间件功能:
中间件函数可以执行以下任务:
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用
next()
将控制传递给下一个中间件函数。否则, 请求将被挂起。
您可以在函数中或在具有 @Injectable()
装饰器的类中实现自定义 Nest
中间件。 这个类应该实现 NestMiddleware
接口, 而函数没有任何特殊的要求。
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 { AdminService } from './admin.service';
import {MsgService} from "../msg/msg.service";
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private readonly userService: AdminService,
private readonly MSG: MsgService) {}
async use(req: Request, res: Response, next: NextFunction) {
const authHeaders = req.headers.authorization;
if (authHeaders) {
const token = authHeaders;
const decoded: any = this.verify(token);
const user = await this.userService.findById(decoded.id);
if (!user) {
throw new HttpException('User not found.', HttpStatus.UNAUTHORIZED);
}
req.user = user;
next();
} else {
throw new HttpException('Not authorized.', HttpStatus.UNAUTHORIZED);
}
}
private verify (token:string){
let decoded: any
try {
decoded = jwt.verify(token, SECRET);
}catch (e) {
this.MSG.fail('token error')
}
return decoded
}
}
在admin.module.ts中应用AuthMiddleware,forRoutes里面配置的路径是 controller 里需要进行 token 验证的接口路径
import {MiddlewareConsumer, Module, NestModule, RequestMethod} from '@nestjs/common';
import { AdminController } from './admin.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminEntity } from './admin.entity';
import { AdminService } from './admin.service';
import { AuthMiddleware } from './auth.middleware';
@Module({
imports: [TypeOrmModule.forFeature([AdminEntity])],
providers: [AdminService],
controllers: [
AdminController
],
exports: [AdminService]
})
export class AdminModule implements NestModule {
public configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthMiddleware)
.forRoutes({path: 'admin/users', method: RequestMethod.GET}, {path: 'user', method: RequestMethod.PUT});
}
}
admin.controller.ts
import { Get, Post, Body, Put, Delete, Param, Controller, UsePipes } from '@nestjs/common';
import { Request } from 'express';
import { AdminService } from './admin.service';
import {UserDto, UserPassDto} from './dto';
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { ValidationPipe } from '../shared/pipes/validation.pipe';
import {
ApiBearerAuth, ApiTags
} from '@nestjs/swagger';
import {MsgService} from "../msg/msg.service";
@ApiBearerAuth()
@ApiTags('admin')
@Controller('admin')
export class AdminController {
constructor(private readonly userService: AdminService,
private readonly MSG: MsgService) {}
@Get('users')
async getall(){
return this.userService.findAll();
}
@UsePipes(new ValidationPipe())
@Put('users')
async create(@Body() userData: UserDto) {
return this.userService.create(userData);
}
@UsePipes(new ValidationPipe())
@Post('users/login')
async login(@Body() loginUserDto: UserDto) {
console.log(loginUserDto)
const _user = await this.userService.findOne(loginUserDto);
if (!_user) this.MSG.fail('no user')
const token = await this.userService.generateJWT(_user);
const { username} = _user;
const user = {token, username};
return {user}
}
@Post('change/pass')
async changePass(@Body() changePassBody: UserPassDto){
return this.userService.changePass(changePassBody)
}
}
完整代码:https://github.com/baiqingchun/nest/tree/master/src/admin
4、返回数据进行处理
这里的返回数据我们并不满意,希望可以反回类似
{
data:{},
code: 200,
message: '请求成功',
}
那我们要在每个请求后面都进行这样的数据结构的整理,这样太麻烦了,可以使用interceptor拦截器来处理返回数据
transform.interceptor.ts
import {
Injectable,
NestInterceptor,
CallHandler,
ExecutionContext,
} from '@nestjs/common';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>> {
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<Response<T>> {
return next.handle().pipe(
map(data => {
return {
data,
code: 200,
message: '请求成功',
};
}),
);
}
}
对异常返回处理
使用Exception filters异常过滤器。
http-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import {QueryFailedError} from "typeorm";
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const message = exception.message;
Logger.log('错误提示', message);
const errorResponse = {
message: message,
code: exception.getStatus(), // 自定义code
url: request.originalUrl, // 错误的url地址
};
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
// 设置返回的状态码、请求头、发送错误信息
response.status(status);
response.header('Content-Type', 'application/json; charset=utf-8');
response.send(errorResponse);
}
}
如果觉得平常写抛出异常太长太麻烦,可以对抛出异常再进行封装。
throw new HttpException({message: 'reason'},HttpStatus.BAD_REQUEST);
改为
this.MSG.fail('Username must be unique',400)
this.MSG是对异常的封装
import {HttpException, HttpStatus, Injectable} from '@nestjs/common';
@Injectable()
export class MsgService {
public fail(reason:string, code?:number){
if (code) {
throw new HttpException({message: reason},code);
} else {
throw new HttpException({message: reason},HttpStatus.BAD_REQUEST);
}
}
完整代码:https://github.com/baiqingchun/nest/tree/master/src/msg
5、文件上传
一个项目离不开文件上传,nest 对文件上传也做了相应处理
创建文件模块。
如果需要详细理解文件上传,可以看另外一篇文章https://www.jianshu.com/p/28f8dd9a732e
还是使用命令来做
nest g module file
nest g service file
nest g controller file
file.module.ts
import { Module } from '@nestjs/common';
import { FileController } from './file.controller';
import { FileService } from './file.service';
import { MulterModule } from '@nestjs/platform-express';
import dayjs = require('dayjs');
import { diskStorage } from 'multer';
import * as nuid from 'nuid';
@Module({
imports:[
MulterModule.register({
storage: diskStorage({
// 配置文件上传后的文件夹路径
destination: `./public/uploads/${dayjs().format('YYYY-MM-DD')}`,
filename: (req, file, cb) => {
// 在此处自定义保存后的文件名称
// const filename = `${nuid.next()}.${file.mimetype.split('/')[1]}`;
// return cb(null, filename);
return cb(null, file.originalname);
},
}),
}),
],
controllers: [FileController],
providers: [FileService]
})
export class FileModule {}
file.controller.ts
import { Controller, Post, UseInterceptors, UploadedFile, UploadedFiles, Body } from '@nestjs/common';
import {
AnyFilesInterceptor,
FileFieldsInterceptor,
FileInterceptor,
FilesInterceptor,
} from '@nestjs/platform-express';
import multer = require('multer');
// import {StorageService} from 'src/common/storage/storage.service'
import {FileService} from './file.service'
@Controller('file')
export class FileController {
constructor(
private readonly fileService:FileService,
) {
}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async UploadedFile(@UploadedFile() file) {
// 这里的 file 已经是保存后的文件信息了,在此处做数据库处理,或者直接返回保存后的文件信息
// const buckets =await this.storageService.getBuckets()
console.log(file)
return file;
}
@Post('upload3')
@UseInterceptors(FilesInterceptor('file'))
uploadFile(@UploadedFiles() file) {
console.log(file);
}
@Post('uploads')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'avatar', maxCount: 1 },
{ name: 'background', maxCount: 1 },
{ name: 'avatar_name' },
{ name: 'background_name'}
]))
async uploads(@UploadedFiles() files,@Body() body) {
console.log(files,body)
}
总结
至此关于 nest 实战结束,希望对你有所帮助。全部代码https://github.com/baiqingchun/nest