在Nestjs 入门(二) 中,我们创建了一个基本的 Nestjs 应用。下面我们基于此进行扩展。
源码地址:awesome-nest
序列化 在 entity 中,有时候有些字段不一定要返还给前端,通常我们需要自己做一次筛选,而 Nestjs 中,配合 class-transformer ,可以很方便的实现这个功能。
例如,我们有个 entity 的基类common.entity.ts
,返还数据的时候,我们不希望把create_at
和update_at
也带上,这时候就可以使用@Exclude()
排除CommonEntity
中的这两个字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { CreateDateColumn , PrimaryGeneratedColumn , UpdateDateColumn } from 'typeorm' import { Exclude } from 'class-transformer' export class CommonEntity { @PrimaryGeneratedColumn ('uuid' ) id : string @Exclude () @CreateDateColumn ({ comment : '创建时间' , }) create_at : number @Exclude () @UpdateDateColumn ({ comment : '更新时间' , }) update_at : number }
在对应请求的地方标记使用ClassSerializerInterceptor
,此时,GET /api/v1/cats/1
这个请求返回的数据中,就不会包含create_at
和update_at
这两个字段。
1 2 3 4 5 6 7 8 9 10 11 @Controller ('cats' )export class CatsController { constructor (private readonly catsService: CatsService ) { } @Get (':id' ) @UseInterceptors (ClassSerializerInterceptor ) findOne (@Param ('id' ) id : string ): Promise <Array <Partial <CatEntity >>> { return this .catsService .getCat (id) } }
如果某个 controller 中都需要使用ClassSerializerInterceptor
来帮我们做一些序列化的工作,可以把 Interceptor 提升到整个 controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @UseInterceptors (ClassSerializerInterceptor )@Controller ('cats' )export class CatsController { constructor (private readonly catsService: CatsService ) { } @Get (':id' ) findOne (@Param ('id' ) id : string ): Promise <Array <Partial <CatEntity >>> { return this .catsService .getCat (id) } @Post () create (@Body () createCatDto : CreateCatDto ): Promise <void > { return this .catsService .createCat (createCatDto) } }
甚至可以在main.ts
中把它作为全局的 Interceptor,不过这样不方便进行细粒度地控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async function bootstrap ( ) { const app = await NestFactory .create (AppModule ) app.setGlobalPrefix ('api/v1' ) app.useGlobalInterceptors (new ClassSerializerInterceptor (app.get (Reflector ))) await app.listen (config.port , config.hostName , () => { Logger .log ( `Awesome-nest API server has been started on http://${config.hostName} :${config.port} ` , ) }) } bootstrap ()
在某些场景下,我们需要对 entity 中某个字段处理后再返回,可以使用@Transform()
:
1 2 3 4 5 6 7 8 9 10 11 12 @Entity ('dog' )export class DogEntity extends CommonEntity { @Column ({ length : 50 }) @Transform (value => `dog: ${value} ` ) name : string @Column () age : number @Column ({ length : 100 , nullable : true }) breed : string }
此时,name
字段经过@Transform
的包装就会变成dog: name
的格式。如果我们需要根据已有字段构造一个新的字段,可以使用@Expose()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Entity ('dog' )export class DogEntity extends CommonEntity { @Column ({ length : 50 }) @Transform (value => `dog: ${value} ` ) name : string @Column () age : number @Column ({ length : 100 , nullable : true }) breed : string @Expose () get isOld (): boolean { return this .age > 10 } }
上面代码会根据查询到的age
字段动态计算isOld
的值,此时通过 GET 方法请求返回的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 { "data" : [ { "id" : "15149ec5-cddf-4981-89a0-62215b30ab81" , "name" : "dog: nana" , "age" : 12 , "breed" : "corgi" , "isOld" : true } ] , "status" : 0 , "message" : "请求成功" }
事务 在使用 MySQL 的时候,有时候我们需要使用事务,借助 TypeORM 中可以这样使用事务:
1 2 3 4 5 6 7 8 @Delete (':name' )@Transaction ()delete ( @Param ('name' ) name : string , @TransactionManager () manager : EntityManager , ): Promise <void > { return this .catsService .deleteCat (name, manager) }
@Transaction()
将 controller 或者 service 中所有执行包装到一个数据库事务中,@TransportManager
提供了一个事务实体管理器,它必须用于在该事务中执行查询:
1 2 3 async deleteCat (name : string , manager : EntityManager ): Promise <void > { await manager.delete (CatEntity , { name }) }
上面代码通过装饰器很方便地进行了事务的操作,如果事务执行过程中有任何错误会自动回滚。
当然,我们也可以手动创建查询运行程序实例,并使用它来手动控制事务状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { getConnection } from "typeorm" ;const connection = getConnection ();const queryRunner = connection.createQueryRunner ();await queryRunner.connect ();await queryRunner.query ("SELECT * FROM users" );const users = await queryRunner.manager .find (User );await queryRunner.startTransaction ();try { await queryRunner.manager .save (user1); await queryRunner.manager .save (user2); await queryRunner.manager .save (photos); await queryRunner.commitTransaction (); } catch (err) { await queryRunner.rollbackTransaction (); }
QueryRunner
提供单个数据库连接。 使用查询运行程序组织事务。 单个事务只能在单个查询运行器上建立。
认证 在这个应用内,现在对用户还没有进行认证,通过用户认证可以判断该访问角色的合法性和权限。通常认证要么基于 Session,要么基于 Token。这里就以基于 Token 的 JWT(JSON Web Token) 方式进行用户认证。
首先安装相关依赖:
1 $ npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt
然后创建jwt.strategy.ts
,用来验证 token,当 token 有效时,允许进一步处理请求,否则返回401(Unanthorized)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { ExtractJwt , Strategy } from 'passport-jwt' import { PassportStrategy } from '@nestjs/passport' import { Injectable , UnauthorizedException } from '@nestjs/common' import config from '../../config' import { UserEntity } from '../entities/user.entity' import { AuthService } from './auth.service' @Injectable ()export class JwtStrategy extends PassportStrategy (Strategy ) { constructor (private readonly authService: AuthService ) { super ({ jwtFromRequest : ExtractJwt .fromAuthHeaderAsBearerToken (), secretOrKey : config.jwt .secret , }) } async validate (payload: UserEntity ) { const user = await this .authService .validateUser (payload) if (!user) { throw new UnauthorizedException ('身份验证失败' ) } return user } }
然后创建auth.service.ts
,上面的jwt.strategy.ts
会使用这个服务校验 token,并且提供了创建 token 的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { JwtService } from '@nestjs/jwt' import { Injectable } from '@nestjs/common' import { UserEntity } from '../entities/user.entity' import { InjectRepository } from '@nestjs/typeorm' import { Repository } from 'typeorm' import { Token } from './auth.interface' import config from '../../config' @Injectable ()export class AuthService { constructor ( @InjectRepository (UserEntity) private readonly userRepository: Repository<UserEntity>, private readonly jwtService: JwtService, ) { } createToken (email : string ): Token { const accessToken = this .jwtService .sign ({ email }) return { expires_in : config.jwt .signOptions .expiresIn , access_token : accessToken, } } async validateUser (payload : UserEntity ): Promise <any > { return await this .userRepository .find ({ email : payload.email }) } }
这两个文件都会作为服务在对应的module
中注册,并且引入PassportModule
和JwtModule
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { Module } from '@nestjs/common' import { AuthService } from './auth/auth.service' import { PassportModule } from '@nestjs/passport' import { JwtModule } from '@nestjs/jwt' import { JwtStrategy } from './auth/jwt.strategy' import config from '../config' @Module ({ imports : [ PassportModule .register ({ defaultStrategy : 'jwt' }), JwtModule .register (config.jwt ), ], providers : [ AuthService , JwtStrategy , ], exports : [], }) export class FeaturesModule {}
这时候,就可以使用@UseGuards(AuthGuard())
来对需要认证的 API 进行身份校验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import { Body , ClassSerializerInterceptor , Controller , Get , Param , Post , UseGuards , UseInterceptors , } from '@nestjs/common' import { CatsService } from './cats.service' import { CreateCatDto } from './cat.dto' import { CatEntity } from '../entities/cat.entity' import { AuthGuard } from '@nestjs/passport' @Controller ('cats' )@UseGuards (AuthGuard ())export class CatsController { constructor (private readonly catsService: CatsService ) { } @Get (':id' ) @UseInterceptors (ClassSerializerInterceptor ) findOne (@Param ('id' ) id : string ): Promise <Array <Partial <CatEntity >>> { return this .catsService .getCat (id) } @Post () create (@Body () createCatDto : CreateCatDto ): Promise <void > { return this .catsService .createCat (createCatDto) } }
通过 Postman 模拟请求时,如果没有带上 token,就会返回下面结果:
1 2 3 4 5 6 7 { "message" : { "statusCode" : 401 , "error" : "Unauthorized" }, "status" : 1 }
安全 Web 安全中,常见有两种攻击方式:XSS(跨站脚本攻击) 和 CSRF(跨站点请求伪造)。
对 JWT 的认证方式,因为没有 cookie,所以也就不存在 CSRF。如果你不是用的 JWT 认证方式,可以使用csurf 这个库去解决这个安全问题。
对于 XSS,可以使用helmet 去做安全防范。helmet 中有 12 个中间件,它们会设置一些安全相关的 HTTP 头。比如xssFilter
就是用来做一些 XSS 相关的保护。
对于单 IP 大量请求的暴力攻击,可以用express-rate-limit 来进行限速。
对于常见的跨域问题,Nestjs 提供了两种方式解决,一种通过app.enableCors()
的方式启用跨域,另一种像下面一样,在 Nest 选项对象中启用。
最后,所有这些设置都是作为全局的中间件启用,最后main.ts
中,和安全相关的设置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import * as helmet from 'helmet' import * as rateLimit from 'express-rate-limit' async function bootstrap ( ) { const app = await NestFactory .create (AppModule , { cors : true }) app.use (helmet ()) app.use ( rateLimit ({ windowMs : 15 * 60 * 1000 , max : 100 , }), ) await app.listen (config.port , config.hostName , () => { Logger .log ( `Awesome-nest API server has been started on http://${config.hostName} :${config.port} ` , ) }) }
HTTP 请求 Nestjs 中对Axios 进行了封装,并把它作为 HttpService
内置到HttpModule
中。HttpService
返回的类型和 Angular 的 HttpClient Module
一样,都是observables
,所以可以使用 rxjs 中的操作符处理各种异步操作。
首先,我们需要导入HttpModule
:
1 2 3 4 5 6 7 8 9 10 11 import { Global , HttpModule , Module } from '@nestjs/common' import { LunarCalendarService } from './services/lunar-calendar/lunar-calendar.service' @Global ()@Module ({ imports : [HttpModule ], providers : [LunarCalendarService ], exports : [HttpModule , LunarCalendarService ], }) export class SharedModule {}
这里我们把 HttpModule
作为全局模块,在sharedModule
中导入并导出以便其他模块使用。这时候我们就可以使用HttpService
,比如我们在LunarCalendarService
中注入HttpService
,然后调用其 get
方法请求当日的农历信息。这时候get
返回的是 Observable
。
对于这个 Observable
流,可以通过pipe
进行一系列的操作,比如我们直接可以使用 rxjs 的map
操作符帮助我们对数据进行一层筛选,并且超过 5s 后就会报 timeout 错误,catchError
会帮我们捕获所有的错误,返回的值通过of
操作符转换为observable
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { HttpService , Injectable } from '@nestjs/common' import { of , Observable } from 'rxjs' import { catchError, map, timeout } from 'rxjs/operators' @Injectable ()export class LunarCalendarService { constructor (private readonly httpService: HttpService ) { } getLunarCalendar (): Observable <any > { return this .httpService .get ('https://www.sojson.com/open/api/lunar/json.shtml' ) .pipe ( map (res => res.data .data ), timeout (5000 ), catchError (error => of (`Bad Promise: ${error} ` )) ) } }
如果需要对axios 进行配置 ,可以直接在 Module 注册的时候设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { Global , HttpModule , Module } from '@nestjs/common' import { LunarCalendarService } from './services/lunar-calendar/lunar-calendar.service' @Global ()@Module ({ imports : [ HttpModule .register ({ timeout : 5000 , maxRedirects : 5 , }), ], providers : [LunarCalendarService ], exports : [HttpModule , LunarCalendarService ], }) export class SharedModule {}
模板渲染 在 Nestjs 中,可以使用 hbs 作为模板渲染引擎:
1 $ npm install --save hbs
在main.ts
中,我们告诉 express,static
文件夹用来存储静态文件,views
中含了模板文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { NestFactory } from '@nestjs/core' import { NestExpressApplication } from '@nestjs/platform-express' import { join } from 'path' import { AppModule } from './app.module' import config from './config' import { Logger } from './shared/utils/logger' async function bootstrap ( ) { const app = await NestFactory .create <NestExpressApplication >(AppModule , { cors : true , }) app.setGlobalPrefix ('api/v1' ) app.useStaticAssets (join (__dirname, '..' , 'static' )) app.setBaseViewsDir (join (__dirname, '..' , 'views' )) app.setViewEngine ('hbs' ) await app.listen (config.port , config.hostName , () => { Logger .log ( `Awesome-nest API server has been started on http://${config.hostName} :${config.port} ` , ) }) }
在views
下新建一个catsPage.hbs
的文件,假设,我们需要在里面填充的数据结构是这样:
1 2 3 4 5 6 7 8 9 10 11 { cats : [ { id : 1 , name : 'yyy' , age : 12 , breed : 'black cats' } ], title : 'Cats List' , }
此时,可以这样写模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <meta http-equiv ="X-UA-Compatible" content ="ie=edge" /> <style > .table .default-td { width : 200px ; } .table tbody >tr :nth-child (2 n-1 ) { background-color : rgb (219 , 212 , 212 ); } .table tbody >tr :nth-child (2 n) { background-color : rgb (172 , 162 , 162 ); } </style > </head > <body > <p > {{ title }}</p > <table class ="table" > <thead > <tr > <td class ="id default-td" > id</td > <td class ="name default-td" > name</td > <td class ="age default-td" > age</td > <td class ="breed default-td" > breed</td > </tr > </thead > <tbody > {{#each cats}} <tr > <td > {{id}}</td > <td > {{name}}</td > <td > {{age}}</td > <td > {{breed}}</td > </tr > {{/each}} </tbody > </table > </body > </html >
需要注意的是,如果你有拦截器,数据会先经过拦截器的处理,然后再填充到模板中。
在 controller 中,通过@Render
指定模板的名称,并且在 return
中返回需要填充的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Get ('page' )@Render ('catsPage' )getCatsPage ( ) { return { cats : [ { id : 1 , name : 'yyy' , age : 12 , breed : 'black cats' } ], title : 'Cats List' , } }
Nestjs 还支持和其他 SSR 框架集成,比如 Next,Angular Universal,Nuxt。具体使用 Demo 可以分别查看这几个项目nestify ,nest-angular ,simple-todos 。
Swagger 文档 Nestjs 中也提供了对 swagger 文档的支持,方便我们对 API 进行追踪和测试:
1 $ npm install --save @nestjs/swagger swagger-ui-express
在main.ts
中构件文档:
1 2 3 4 5 6 7 8 9 10 const options = new DocumentBuilder () .setTitle ('Awesome-nest' ) .setDescription ('The Awesome-nest API Documents' ) .setBasePath ('api/v1' ) .addBearerAuth () .setVersion ('0.0.1' ) .build () const document = SwaggerModule .createDocument (app, options)SwaggerModule .setup ('docs' , app, document )
此时,访问http://localhost:3300/docs
就可以看到 swagger 文档的页面。
对于不同的 API 可以在 controller 中使用@ApiUseTags()
进行分类,对于需要认证的 API,可以加上@ApiBearerAuth()
,这样在 swagger 中填完 token 后,就可以直接测试 API:
1 2 3 4 5 6 7 8 9 10 11 12 13 @ApiUseTags ('cats' )@ApiBearerAuth ()@Controller ('cats' )@UseGuards (AuthGuard ())export class CatsController { constructor (private readonly catsService: CatsService ) {} @Get ('page' ) @Render ('catsPage' ) getCatsPage (): Promise <any > { return this .catsService .getCats () } }
![5258bc6b-80ab-4701-bcad-4407bc703c07](/assets/images/nest 入门/5258bc6b-80ab-4701-bcad-4407bc703c07.png)
对于我们定于的 DTO,为了使 SwaggerModule
可以访问类属性,我们必须用 @ApiModelProperty()
装饰器标记所有这些属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 import { ApiModelProperty } from '@nestjs/swagger' import { IsEmail , IsString } from 'class-validator' export class AccountDto { @ApiModelProperty () @IsString () @IsEmail () readonly email : string @ApiModelProperty () @IsString () readonly password : string }
![2019-07-31_21-49-08](/assets/images/nest 入门/2019-07-31_21-49-08.png)
对于 swagger 文档更多的用法,可以看官网OpenAPI (Swagger) 的内容。
热重载 在开发的时候,运行npm run start:dev
的时候,是进行全量编译,如果项目比较大,全量编译耗时会比较长,这时候我们可以利用 webpack 来帮我们做增量编译,这样会大大增加开发效率。
首先,安装 webpack 相关依赖:
1 $ npm i --save-dev webpack webpack-cli webpack-node-externals ts-loader
在根目录下创建一个webpack.config.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const webpack = require ('webpack' );const path = require ('path' );const nodeExternals = require ('webpack-node-externals' );module .exports = { entry : ['webpack/hot/poll?100' , './src/main.ts' ], watch : true , target : 'node' , externals : [ nodeExternals ({ whitelist : ['webpack/hot/poll?100' ], }), ], module : { rules : [ { test : /.tsx?$/ , use : 'ts-loader' , exclude : /node_modules/ , }, ], }, mode : 'development' , resolve : { extensions : ['.tsx' , '.ts' , '.js' ], }, plugins : [new webpack.HotModuleReplacementPlugin ()], output : { path : path.join (__dirname, 'dist' ), filename : 'server.js' , }, };
在main.ts
中启用 HMR:
1 2 3 4 5 6 7 8 9 10 11 12 declare const module : any ;async function bootstrap ( ) { const app = await NestFactory .create (ApplicationModule ); await app.listen (3000 ); if (module .hot ) { module .hot .accept (); module .hot .dispose (() => app.close ()); } } bootstrap ();
在package.json
中增加下面两个命令:
1 2 3 4 5 6 { "scripts" : { "start" : "node dist/server" , "webpack" : "webpack --config webpack.config.js" } }
运行npm run webpack
之后,webpack 开始监视文件,然后在另一个命令行窗口中运行npm start
。