在Nestjs 入門(二)中,咱們建立了一個基本的 Nestjs 應用。下面咱們基於此進行擴展。css
源碼地址:awesome-nesthtml
在 entity 中,有時候有些字段不必定要返還給前端,一般咱們須要本身作一次篩選,而 Nestjs 中,配合 class-transformer,能夠很方便的實現這個功能。前端
例如,咱們有個 entity 的基類common.entity.ts
,返還數據的時候,咱們不但願把create_at
和update_at
也帶上,這時候就可使用@Exclude()
排除CommonEntity
中的這兩個字段:node
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
這兩個字段。webpack
@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:ios
@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,不過這樣不方便進行細粒度地控制。git
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()
:github
@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()
:web
@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 方法請求返回的結果以下:typescript
{
"data": [
{
"id": "15149ec5-cddf-4981-89a0-62215b30ab81",
"name": "dog: nana",
"age": 12,
"breed": "corgi",
"isOld": true
}
],
"status": 0,
"message": "請求成功"
}
複製代碼
在使用 MySQL 的時候,有時候咱們須要使用事務,藉助 TypeORM 中能夠這樣使用事務:
@Delete(':name')
@Transaction()
delete(
@Param('name') name: string,
@TransactionManager() manager: EntityManager,
): Promise<void> {
return this.catsService.deleteCat(name, manager)
}
複製代碼
@Transaction()
將 controller 或者 service 中全部執行包裝到一個數據庫事務中,@TransportManager
提供了一個事務實體管理器,它必須用於在該事務中執行查詢:
async deleteCat(name: string, manager: EntityManager): Promise<void> {
await manager.delete(CatEntity, { name })
}
複製代碼
上面代碼經過裝飾器很方便地進行了事務的操做,若是事務執行過程當中有任何錯誤會自動回滾。
固然,咱們也能夠手動建立查詢運行程序實例,並使用它來手動控制事務狀態:
import { getConnection } from "typeorm";
// 獲取鏈接並建立新的queryRunner
const connection = getConnection();
const queryRunner = connection.createQueryRunner();
// 使用咱們的新queryRunner創建真正的數據庫連
await queryRunner.connect();
// 如今咱們能夠在queryRunner上執行任何查詢,例如:
await queryRunner.query("SELECT * FROM users");
// 咱們還能夠訪問與queryRunner建立的鏈接一塊兒使用的實體管理器:
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) 方式進行用戶認證。
首先安裝相關依賴:
$ npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt
複製代碼
而後建立jwt.strategy.ts
,用來驗證 token,當 token 有效時,容許進一步處理請求,不然返回401(Unanthorized)
:
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 的方法:
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
:
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 進行身份校驗:
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,就會返回下面結果:
{
"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
中,和安全相關的設置以下:
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, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
}),
)
await app.listen(config.port, config.hostName, () => {
Logger.log(
`Awesome-nest API server has been started on http://${config.hostName}:${config.port}`,
)
})
}
複製代碼
Nestjs 中對Axios進行了封裝,並把它做爲 HttpService
內置到HttpModule
中。HttpService
返回的類型和 Angular 的 HttpClient Module
同樣,都是observables
,因此可使用 rxjs 中的操做符處理各類異步操做。
首先,咱們須要導入HttpModule
:
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
:
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 註冊的時候設置:
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 做爲模板渲染引擎:
$ npm install --save hbs
複製代碼
在main.ts
中,咱們告訴 express,static
文件夾用來存儲靜態文件,views
中含了模板文件:
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
的文件,假設,咱們須要在裏面填充的數據結構是這樣:
{
cats: [
{
id: 1,
name: 'yyy',
age: 12,
breed: 'black cats'
}
],
title: 'Cats List',
}
複製代碼
此時,能夠這樣寫模板:
<!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(2n-1) { background-color: rgb(219, 212, 212); } .table tbody>tr:nth-child(2n) { 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
中返回須要填充的數據:
@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。
Nestjs 中也提供了對 swagger 文檔的支持,方便咱們對 API 進行追蹤和測試:
$ npm install --save @nestjs/swagger swagger-ui-express
複製代碼
在main.ts
中構件文檔:
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:
@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()
}
}
複製代碼
對於咱們定於的 DTO,爲了使 SwaggerModule
能夠訪問類屬性,咱們必須用 @ApiModelProperty()
裝飾器標記全部這些屬性:
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
}
複製代碼
對於 swagger 文檔更多的用法,能夠看官網OpenAPI (Swagger)的內容。
在開發的時候,運行npm run start:dev
的時候,是進行全量編譯,若是項目比較大,全量編譯耗時會比較長,這時候咱們能夠利用 webpack 來幫咱們作增量編譯,這樣會大大增長開發效率。
首先,安裝 webpack 相關依賴:
$ npm i --save-dev webpack webpack-cli webpack-node-externals ts-loader
複製代碼
在根目錄下建立一個webpack.config.js
:
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:
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
中增長下面兩個命令:
{
"scripts": {
"start": "node dist/server",
"webpack": "webpack --config webpack.config.js"
}
}
複製代碼
運行npm run webpack
以後,webpack 開始監視文件,而後在另外一個命令行窗口中運行npm start
。