NestJs學(xué)習(xí)

Controller

@Controller(API path): 裝飾器 + Class
CRUD Controller: 通過命令行自動生成

 nest g resource [name]

在controller如何返回response到調(diào)用方,兩種方法。

  1. Standard(Recommend) : built-in method. 如果返回的是一個JS 對象或數(shù)組,會被自動序列化成Json。如果是JS 主類型, 并不會序列化,而是直接發(fā)送回去。 同時,返回的http狀態(tài)碼,總是200. 除了POST, 返回201。可以通過@HttpCode(...) 裝飾器來修改它。
  2. Library-specific: 也可以使用Express的response object。 通過使用@Res()裝飾器。例如: findAll(@Res() response))。這樣可直接訪問原生的response對象。比如response.status(200).send()。例如:
@Controller('cats')
export class CatsController {
    @Post()
    create(@Res() res: Response) {
        res.status(HttpStatus.CREATED).send();
    }

    @Get()
    findAll(@Res() res: Response) {
        res.status(HttpStatus.OK).  json  ([]);
    }
}

如何訪問request對象

使用@Req()裝飾器。例如:

@Get()
findAll(@Req() request: Request): string {
   return 'This action returns all cats';
}

也可以獨立使用的裝飾器

@Body(),@Query(),@Param,@Query,@Headers,@HostParam()

Controller自定義響應(yīng)給客戶端的Header

@Post()
@Header('Cache-Control', 'none')
create() {
    return 'This action adds a new cat';
}

或者使用library的方式:res.header()

在Controller中重定向請求

使用@Redirect()裝飾器, 兩個參數(shù),url, 狀態(tài)碼,默認是302.

@Get()
@Redirect('https://nestjs.com', 301)

如果想動態(tài),設(shè)置重定向地址,直接return 這樣的數(shù)據(jù)結(jié)構(gòu)。

 {
     "url": string,
     "statusCode": number
  }

路由參數(shù)

@Get(':id')
findOne(@Param() params): string {
    console.log(params.id);
    return This action returns a #${params.id} cat;
}
@Get(':id')
findOne(@Param('id') id: string): string {
    return This action returns a #${id} cat;
}

The @Controller decorator can take a host option to require that the HTTP host of the incoming requests matches some specific value.

@Controller({ host: 'admin.example.com' })
export class AdminController {
    @Get()
    index(): string {
        return 'Admin page';
    }
}

可以通過token,獲取動態(tài)子域名

@Controller({ host: ':account.example.com' })
export class AccountController {
    @Get()
    getInfo(@HostParam('account') account: string) {
        return account;
    }
}

Controller中的異步Asynchronicity

直接使用async / promise 作為controller返回值

@Get()
async findAll(): Promise<any[]> {
    return [];
}
// 同時支持Observe 返回
@Get()
findAll(): Observable<any[]> {
    return of([]);
}

Controller中發(fā)送過來的RequestPayLoad, 可以使用plain class 定義DTO類,NestJS自動做轉(zhuǎn)換。但是,對比較大的數(shù)據(jù),比如Product(一個商品類),會比較麻煩。也可以直接使用any類型,NestJs會轉(zhuǎn)。

Provider

是一個Service層,邏輯代碼和后端調(diào)用可以寫在這里: 通過依賴注入,
Services: 可以通過nest命令來生成一個Service。

 nest g service cats

Service使用 @Injectable() 裝飾器。告訴Nest, 這個可以被 IoC container管理。默認Service都是單例的,可以被共享,第一個調(diào)用的,會初始化后,注冊到IoC管理中,默認使用class名字作為標(biāo)志,來進行查找。

在Controller中注入Service, 使用Controller的構(gòu)造器:下面,會自動注入CatService。

constructor(private catsService: CatsService) {}

Dependency injection: 依賴注入
通過構(gòu)造器,Nest會返回一個存在的實例。 這個依賴就會注入到controller中。

  1. Scope

Provider正常有一個Scope生命周期。當(dāng)應(yīng)用啟動的時候,每一個依賴都要解決掉。所以,每一個provider會被初始化。當(dāng)應(yīng)用shutdown的時候,每一個provider 會被destroyed. 當(dāng)然,也可以讓你的provider擁有request-scope的生命周期。

  1. 自定義provider

Nest有內(nèi)建的Inversion of controller(IoC) 容器來解決provider之間的關(guān)系。有好幾種定義provider的方法: Plain Value, class, 同步/異步工廠。

  1. 可選provider

使用@Optional() 裝飾器, HttpService依賴的Provider, "HTTP_OPTIONS"是可選的。

@Injectable()
export class HttpService<T> {
    constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
  1. 基于屬性的注入

前面都是基于構(gòu)造器的注入。還可以使用基于屬性的注入。

@Injectable()
export class HttpService<T> {
    @Inject('HTTP_OPTIONS')
    private readonly httpClient: T;
}
  1. 注冊provider

provider要在本模塊內(nèi)的controller使用的話,必須在本模塊的provider里面進行注冊,才可以被controller注入。

  1. 手動實例化

獲取存在的實例或者動態(tài)實例化provider。 我們可以使用module reference.

  1. Provider類型,使用

Value providers: useValue

@Module({
imports: [CatsModule],
providers: [
    {
        provide: CatsService,
        useValue: mockCatsService,
    },
],
})
// mockCatsService 是一個plain object.

Non-class-based provider tokens

使用class名字,作為我們的provider token, provider列表的唯一標(biāo)志。(默認)

也可以使用string作為token。

@Module({
providers: [
    {
        provide: 'CONNECTION',
        useValue: connection,
    },
],
})

這個使用string 作為token., 注入的時候,使用Inject(名字),注入。

@Injectable()
export class CatsRepository {
    constructor(@Inject('CONNECTION') connection: Connection) {}
}

Class providersuseClass

動態(tài)決定應(yīng)該使用哪個類實例。比如,有一個抽象類ConfigService, 根據(jù)環(huán)境來初始化。

const configServiceProvider = {
    provide: ConfigService,
    useClass:process.env.NODE_ENV === 'development'
             ? DevelopmentConfigService
             : ProductionConfigService,
};

@Module({
    providers: [configServiceProvider],
})

Factory providers: useFactory

動態(tài)創(chuàng)建provider.

const connectionProvider = {
    provide: 'CONNECTION',
    useFactory: (optionsProvider: OptionsProvider, optionalProvider?: string) => {
        const options = optionsProvider.get();
        return new DatabaseConnection(options);
    },
    inject: [OptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
    //       \_____________/            \__________________/
    //        This provider              The provider with this
    //        is mandatory.              token can resolve to undefined.
};

@Module({
    providers: [
        connectionProvider,
        OptionsProvider,
        // { provide: 'SomeOptionalProvider', useValue: 'anything' },
    ],
})

Alias providers: useExisting

const loggerAliasProvider = {
    provide: 'AliasedLoggerService',
    useExisting: LoggerService,
};

@Module({
    providers: [LoggerService, loggerAliasProvider],
})

Non-service based providers

provider 可以是任何value

const configFactory = {
    provide: 'CONFIG',
    useFactory: () => {
        return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
    },
};

@Module({
    providers: [configFactory],
})

Export custom provider: Exporting using the token

const connectionFactory = {
    provide: 'CONNECTION',
    useFactory: (optionsProvider: OptionsProvider) => {
        const options = optionsProvider.get();
        return new DatabaseConnection(options);
    },
    inject: [OptionsProvider],
};

@Module({
    providers: [connectionFactory],
    exports: ['CONNECTION'],
})

// exporting with full provider object.
@Module({
    providers: [connectionFactory],
    exports: [connectionFactory],
})

Asynchronous providers

如果你想數(shù)據(jù)庫連接建立好后,才能開始接受請求,可以使用異步provider。

{
    provide: 'ASYNC_CONNECTION',
    useFactory: async () => {
        const connection = await createConnection(options);
        return connection;
    },
}

Modules

定義模塊使用:@Module() decorator. 裝飾器。每一個應(yīng)用至少一個模塊,叫做Root Module。

@Module()

  • providers: the providers that will be instantiated by the Nest injector and that may be shared at least across this module: 本模塊使用的providers.
  • controllers: the set of controllers defined in this module which have to be instantiated. controller集合,需要初始化的。
  • imports: the list of imported modules that export the providers which are required in this module. 倒入的模塊,這樣在本模塊中可以使用該倒入模塊的導(dǎo)出providers.
  • exports: the subset of providers that are provided by this module and should be available in other modules which import this module. 導(dǎo)出的provider集合,可以被其他模塊導(dǎo)入后注入。

生成一個模塊使用nest CLI: nest g module cats

modules默認是單例的。

  1. Module re-exporting
@Module({
    imports: [CommonModule],
    exports: [CommonModule],
})
export class CoreModule {}

這樣,導(dǎo)入CoreModule的,就自動就導(dǎo)入了CommonModule.

  1. Dependency injection

一個模塊,也可以注入provider來使用。

@Module({
    controllers: [CatsController],
    providers: [CatsService],
})
export class CatsModule {
    constructor(private catsService: CatsService) {}
}

模塊也能注入providers. 但是模塊不能被注入到providers.

  1. Global modules

Nest模塊缺省不是global注冊的,所以,你在某個模塊中使用的話,比如先導(dǎo)入該模塊。

如果想全局注冊模塊,使用global with the @Global() decorator.

@Global()
@Module({
    controllers: [CatsController],
    providers: [CatsService],
    exports: [CatsService],
})
export class CatsModule {}
  1. Dynamic modules

能夠讓你動態(tài)創(chuàng)建,注冊模塊。

@Module({
    providers: [Connection],
})
export class DatabaseModule {
    static forRoot(entities = [], options?): DynamicModule {
        const providers = createDatabaseProviders(options, entities);
        return {
                    module: DatabaseModule,
                    providers: providers,
                    exports: providers,
                };
    }
}
// 使用
@Module({
    imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}

// re-export
@Module({
    imports: [DatabaseModule.forRoot([User])],
    exports: [DatabaseModule],
})
export class AppModule {}

MIDDLEWARE

中間件,是在router之前被調(diào)用。Client Side -> Middleware -> RouteHandler.

可以執(zhí)行:

  • 執(zhí)行任何代碼

  • 修改request, response 對象。

  • 結(jié)束Request-Response

  • call 下一個中間件

  • 如果當(dāng)前中間件,不結(jié)束request-response cycle。 必須調(diào)用next()。 否則請求會被掛起。

實現(xiàn)中間件:

可以使用function 或者class(@Injectable)來實現(xiàn)一個中間件。類必須實現(xiàn)NestMiddleware接口。function沒有任何特殊要求。

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction) {
        console.log('Request...');
        next();
    }
}
  1. Dependency injection

完全支持依賴,注入系統(tǒng),通常通過構(gòu)造器。

使用 middleware

通常,我們使用configure() method,來初始化,配置中間件。下面我們初始化日志中間件。

@Module({
    imports: [CatsModule],
})
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer.apply(LoggerMiddleware)
                .forRoutes('cats');
    }
}

我們也可以限制中間件到特定的path, request methods. Eg: .forRoutes({ path: 'cats', method: RequestMethod.GET });

  1. Route wildcards

可以使用通配符。.forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });

  1. Middleware consumer

使用鏈?zhǔn)秸{(diào)用, The apply() method may either take a single middleware, or multiple arguments to specify multiple middlewares.

  1. Excluding routes
consumer.apply(LoggerMiddleware)
    .exclude(
        { path: 'cats', method: RequestMethod.GET },
        { path: 'cats', method: RequestMethod.POST },
        'cats/(.*)',)
    .forRoutes(CatsController);
  1. Functional middleware
export function logger(req: Request, res: Response, next: NextFunction) {
    console.log(Request...);
    next();
};
consumer.apply(logger)
        .forRoutes(CatsController);
  1. Multiple middleware
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
  1. Global middleware
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

Exception Filter

內(nèi)建的Exception filter, 處理異常,返回給客戶端。

有一個全局的內(nèi)建異常過濾器,來處理HttpException以及它子類。

如果異常不能識別,全局過濾器,會生成下面的json字符。

{
    "statusCode": 500,
    "message": "Internal server error"
}
  1. 拋出標(biāo)準(zhǔn)異常
@Get()
async findAll() {
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
  1. 自定義異常

需要繼承HttpException

  1. 異常過濾器:Exception filters
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();
        const status = exception.getStatus();

        response.status(status)
                .json({
                    statusCode: status,
                    timestamp: new Date().toISOString(),
                    path: request.url,
                });
        }
}
  1. Binding 過濾器

過濾器可以綁定到controller的方法上,類名上,也可以全局注冊。

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
    throw new ForbiddenException();
}

@UseFilters(new HttpExceptionFilter())
export class CatsController {}

app.useGlobalFilters(new HttpExceptionFilter());

//全局注冊,是不和任意模塊綁定的。我們也可以在某個模塊中注冊全局filters。

@Module({
    providers: [
        {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
          },
    ],
})
  1. Catch everything

@Catch()

Pipe

主要的應(yīng)用場景:

  • transformation: 轉(zhuǎn)換輸入數(shù)據(jù)到需要的格式,比如從StringInteger.
  • validation: 校驗輸入的數(shù)據(jù)格式,如果不合格,拋出異常。
  1. 綁定 pipe
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
    return this.catsService.findOne(id);
}
  1. 自定義 pipe
@Injectable()
export class ValidationPipe implements PipeTransform {
    transform(value: any, metadata: ArgumentMetadata) {
        return value;
    }
}          
  1. 基于Schema的校驗
@Post()
async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
}
export class CreateCatDto {
    name: string;
    age: number;
    breed: string;
}            

對象的schema校驗,有兩種方法:

  • The Joi library allows you to create schemas in a straightforward way, with a readable API. Let's build a validation pipe that makes use of Joi-based schemas.

  • npm install --save joi

import { ObjectSchema } from 'joi';
@Injectable()
export class JoiValidationPipe implements PipeTransform {
    constructor(private schema: ObjectSchema) {}
    transform(value: any, metadata: ArgumentMetadata) {
        const { error } = this.schema.validate(value);
        if (error) {
            throw new BadRequestException('Validation failed');
        }
        return value;
     }
}

const createCatSchema = Joi.object({
    name: Joi.string().required(),
    age: Joi.number().required(),
    breed: Joi.string().required(),
})

export interface CreateCatDto {
    name: string;
    age: number;
    breed: string;
}

@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
}
  • Class validator

  • npm i --save class-validator class-transformer

import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
    @IsString()
    name: string;

    @IsInt()
    age: number;

    @IsString()
    breed: string;
}


@Injectable()
export class ValidationPipe implements PipeTransform<any> {
    async transform(value: any, { metatype }: ArgumentMetadata) {
        if (!metatype || !this.toValidate(metatype)) {
            return value;
        }
        const object = plainToInstance(metatype, value);
        const errors = await validate(object);
        if (errors.length > 0) {
            throw new BadRequestException('Validation failed');
        }
        return value;
    }

    private toValidate(metatype: Function): boolean {
        const types: Function[] = [String, Boolean, Number, Array, Object];
            return !types.includes(metatype);
        }
    }
}

// Controller
@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto,) {
    this.catsService.create(createCatDto);
}

全局pipes

app.useGlobalPipes(new ValidationPipe());

  • 模塊級的全局注冊
@Module({
    providers: [
        {
            provide: APP_PIPE,
            useClass: ValidationPipe,
        },
    ],
})

Transformation use case: 轉(zhuǎn)換的使用場景

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
    transform(value: string, metadata: ArgumentMetadata): number {
        const val = parseInt(value, 10);
        if (isNaN(val)) {
            throw new BadRequestException('Validation failed');
        }
        return val;
    }
}

//Controller
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
    return this.catsService.findOne(id);
}

提供缺省值

Parse* pipes 期待參數(shù)是必須提供的。 這樣,某些情況下,我們需要提供缺省值,使用DefaultValuePipe。

@Get()
async findAll(
    @Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
    @Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
    ) {
        return this.catsService.findAll({ activeOnly, page });
}

GUARDS

Guards 有單一職責(zé): Guards決定是否當(dāng)前的請求,能否被controller處理。可以根據(jù)權(quán)限許可,Roles, ACL等。

以前是被Middleware處理,但是middleware不了解具體的controller。 而guards可以訪問到ExecutionContext instance。這樣就能知道下一個執(zhí)行。

  1. Authorization guard,認證守衛(wèi)。
@Injectable()
export class AuthGuard implements CanActivate {
    canActivate( context: ExecutionContext,): boolean | Promise<boolean> | Observable<boolean> {
        const request = context.switchToHttp().getRequest();
        return validateRequest(request);
    }
}
//  if it returns true, the request will be processed.
//  if it returns false, Nest will deny the request.
  1. Execution context, 執(zhí)行上下文

The ExecutionContext inherits from ArgumentsHost

  1. 基于角色的認證

Setting roles per handler

@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
}

最好是創(chuàng)建自己的裝飾器,而不是直接使用setMetadata。例如:

 import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
}

@Injectable()
export class RolesGuard implements CanActivate {
    constructor(private reflector: Reflector) {}

    canActivate(context: ExecutionContext): boolean {
        const roles = this.reflector.get<string[]>('roles', context.getHandler());
        if (!roles) {
            return true;
        }
        const request = context.switchToHttp().getRequest();
        const user = request.user;
        return matchRoles(roles, user.roles);
    }
}

Interceptor

主要用于:AOP編程。

  • controller執(zhí)行前,做額外的邏輯處理
  • 轉(zhuǎn)換結(jié)果
  • 轉(zhuǎn)換異常
  • 繼承基本邏輯功能
  • 覆蓋特定邏輯

攔截器中,第一個參數(shù)是Execution context, 第二個是call hander.

在調(diào)用next.handle()之前的代碼,是攔截器開始執(zhí)行,handle()后的代碼是在返回response的時候攔截器執(zhí)行。(洋蔥模型)

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        console.log('Before...');

        const now = Date.now();
        return next.handle().pipe(
            tap(() => console.log(After... ${Date.now() - now}ms)),
        );
    }
}
  1. 綁定攔截器
@UseInterceptors(LoggingInterceptor)
export class CatsController {}            

定義全局攔截器

app.useGlobalInterceptors(new LoggingInterceptor());

定義模塊級的全局攔截器

@Module({
    providers: [
        {
            provide: APP_INTERCEPTOR,
            useClass: LoggingInterceptor,
        },
    ],
})

響應(yīng)的映射:Response mapping

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
    intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
        return next.handle().pipe(map(data => ({ data })));
    }
}

假如,我們需要null value轉(zhuǎn)成 empty string ''

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        return next.handle().pipe(map(value => value === null ? '' : value ));
    }
}
  1. Exception mapping

我們可以使用RxJS的catchError,去覆蓋異常。

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        return next.handle()
            .pipe(
                catchError(err => throwError(() => new BadGatewayException())),
            );
    }
}
  1. 處理流覆蓋:Stream overriding

比如:使用cache interceptor,* 如果緩存了,路由****controller****,不會被執(zhí)行到。*

@Injectable()
export class CacheInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const isCached = true;
        if (isCached) {
            return of([]);
        }
        return next.handle();
    }
}

Custom route decorators

參數(shù)裝飾器, 可以和route handle一起使用。

// 沒有data
export const User = createParamDecorator(
    (data: unknown, ctx: ExecutionContext) => {
        const request = ctx.switchToHttp().getRequest();
        return request.user;
    },
);

// Controller
@Get()
async findOne(@User() user: UserEntity) {
    console.log(user);
}

//有data
export const User = createParamDecorator(
    (data: string, ctx: ExecutionContext) => {
        const request = ctx.switchToHttp().getRequest();
        const user = request.user;

        return data ? user?.[data] : user;
    },
)

// controller中,裝飾器,可以傳data
@Get()
async findOne(@User('firstName') firstName: string) {
    console.log(Hello ${firstName});
}

和管道一起使用

@Get()
async findOne(
    @User(new ValidationPipe({ validateCustomDecorators: true }))
        user: UserEntity,) {
        console.log(user);
    }                
  1. 組合裝飾器: Decorator composition
export function Auth(...roles: Role[]) {
    return applyDecorators(
        SetMetadata('roles', roles),
        UseGuards(AuthGuard, RolesGuard),
        ApiBearerAuth(),
        ApiUnauthorizedResponse({ description: 'Unauthorized' }),
    );
}

// Controller中
@Get('users')
@Auth('admin')
findAllUsers() {}

NestJS 生命周期

  • Bootstrap
  • onModuleInit。// 在依賴處理的時候調(diào)用。調(diào)用順序依賴模塊導(dǎo)入順序。
  • onApplicationBootstrap //所有模塊都初始化好了后調(diào)用。
  • start listener
  • application is running
  • onModuleDestroy //在收到shutdown信號后調(diào)用。
  • beforeApplicationShutdown //
  • stop listener
  • onApplicationShutdown
  • process Exit

** Nest 在應(yīng)用啟動時調(diào)用,并且調(diào)用所有modules, injectables, 和controllers上的鉤子。

可以通過在controller, provider, module上實現(xiàn)onModuleInit方法,來完成。

  1. Asynchronous initialization

onModuleInit, onApplicationBootstrap的鉤子允許defer 初始化過程。

async onModuleInit(): Promise<void> {
    await this.fetch();
}

Client端請求生命周期

In general, the request lifecycle looks like the following:

  1. Incoming request
  2. Globally bound middleware
  3. Module bound middleware
  4. Global guards
  5. Controller guards
  6. Route guards
  7. Global interceptors (pre-controller)
  8. Controller interceptors (pre-controller)
  9. Route interceptors (pre-controller)
  10. Global pipes
  11. Controller pipes
  12. Route pipes
  13. Route parameter pipes
  14. Controller (method handler)
  15. Service (if exists)
  16. Route interceptor (post-request)
  17. Controller interceptor (post-request)
  18. Global interceptor (post-request)
  19. Exception filters (route, then controller, then global)
  20. Server response
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容