NestJs学习

Controller

@Controller(API path): 装饰器 + Class
CRUD Controller: 通过命令行自动生成

 nest g resource [name]

在controller如何返回response到调用方,两种方法。

  1. Standard(Recommend) : built-in method. 如果返回的是一个JS 对象或数组,会被自动序列化成Json。如果是JS 主类型, 并不会序列化,而是直接发送回去。 同时,返回的http状态码,总是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自定义响应给客户端的Header

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

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

在Controller中重定向请求

使用@Redirect()装饰器, 两个参数,url, 状态码,默认是302.

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

如果想动态,设置重定向地址,直接return 这样的数据结构。

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

路由参数

@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,获取动态子域名

@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中发送过来的RequestPayLoad, 可以使用plain class 定义DTO类,NestJS自动做转换。但是,对比较大的数据,比如Product(一个商品类),会比较麻烦。也可以直接使用any类型,NestJs会转。

Provider

是一个Service层,逻辑代码和后端调用可以写在这里: 通过依赖注入,
Services: 可以通过nest命令来生成一个Service。

 nest g service cats

Service使用 @Injectable() 装饰器。告诉Nest, 这个可以被 IoC container管理。默认Service都是单例的,可以被共享,第一个调用的,会初始化后,注册到IoC管理中,默认使用class名字作为标志,来进行查找。

在Controller中注入Service, 使用Controller的构造器:下面,会自动注入CatService。

constructor(private catsService: CatsService) {}

Dependency injection: 依赖注入
通过构造器,Nest会返回一个存在的实例。 这个依赖就会注入到controller中。

  1. Scope

Provider正常有一个Scope生命周期。当应用启动的时候,每一个依赖都要解决掉。所以,每一个provider会被初始化。当应用shutdown的时候,每一个provider 会被destroyed. 当然,也可以让你的provider拥有request-scope的生命周期。

  1. 自定义provider

Nest有内建的Inversion of controller(IoC) 容器来解决provider之间的关系。有好几种定义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. 基于属性的注入

前面都是基于构造器的注入。还可以使用基于属性的注入。

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

provider要在本模块内的controller使用的话,必须在本模块的provider里面进行注册,才可以被controller注入。

  1. 手动实例化

获取存在的实例或者动态实例化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列表的唯一标志。(默认)

也可以使用string作为token。

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

这个使用string 作为token., 注入的时候,使用Inject(名字),注入。

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

Class providersuseClass

动态决定应该使用哪个类实例。比如,有一个抽象类ConfigService, 根据环境来初始化。

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

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

Factory providers: useFactory

动态创建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

如果你想数据库连接建立好后,才能开始接受请求,可以使用异步provider。

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

Modules

定义模块使用:@Module() decorator. 装饰器。每一个应用至少一个模块,叫做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. 倒入的模块,这样在本模块中可以使用该倒入模块的导出providers.
  • exports: the subset of providers that are provided by this module and should be available in other modules which import this module. 导出的provider集合,可以被其他模块导入后注入。

生成一个模块使用nest CLI: nest g module cats

modules默认是单例的。

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

这样,导入CoreModule的,就自动就导入了CommonModule.

  1. Dependency injection

一个模块,也可以注入provider来使用。

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

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

  1. Global modules

Nest模块缺省不是global注册的,所以,你在某个模块中使用的话,比如先导入该模块。

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

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

能够让你动态创建,注册模块。

@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之前被调用。Client Side -> Middleware -> RouteHandler.

可以执行:

  • 执行任何代码

  • 修改request, response 对象。

  • 结束Request-Response

  • call 下一个中间件

  • 如果当前中间件,不结束request-response cycle。 必须调用next()。 否则请求会被挂起。

实现中间件:

可以使用function 或者class(@Injectable)来实现一个中间件。类必须实现NestMiddleware接口。function没有任何特殊要求。

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

完全支持依赖,注入系统,通常通过构造器。

使用 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

使用链式调用, 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

内建的Exception filter, 处理异常,返回给客户端。

有一个全局的内建异常过滤器,来处理HttpException以及它子类。

如果异常不能识别,全局过滤器,会生成下面的json字符。

{
    "statusCode": 500,
    "message": "Internal server error"
}
  1. 抛出标准异常
@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

主要的应用场景:

  • transformation: 转换输入数据到需要的格式,比如从StringInteger.
  • validation: 校验输入的数据格式,如果不合格,抛出异常。
  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: 转换的使用场景

@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 期待参数是必须提供的。 这样,某些情况下,我们需要提供缺省值,使用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 有单一职责: Guards决定是否当前的请求,能否被controller处理。可以根据权限许可,Roles, ACL等。

以前是被Middleware处理,但是middleware不了解具体的controller。 而guards可以访问到ExecutionContext instance。这样就能知道下一个执行。

  1. Authorization guard,认证守卫。
@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, 执行上下文

The ExecutionContext inherits from ArgumentsHost

  1. 基于角色的认证

Setting roles per handler

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

最好是创建自己的装饰器,而不是直接使用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执行前,做额外的逻辑处理
  • 转换结果
  • 转换异常
  • 继承基本逻辑功能
  • 覆盖特定逻辑

拦截器中,第一个参数是Execution context, 第二个是call hander.

在调用next.handle()之前的代码,是拦截器开始执行,handle()后的代码是在返回response的时候拦截器执行。(洋葱模型)

@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,
        },
    ],
})

响应的映射: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转成 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****,不会被执行到。*

@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

参数装饰器, 可以和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。// 在依赖处理的时候调用。调用顺序依赖模块导入顺序。
  • onApplicationBootstrap //所有模块都初始化好了后调用。
  • start listener
  • application is running
  • onModuleDestroy //在收到shutdown信号后调用。
  • beforeApplicationShutdown //
  • stop listener
  • onApplicationShutdown
  • process Exit

** Nest 在应用启动时调用,并且调用所有modules, injectables, 和controllers上的钩子。

可以通过在controller, provider, module上实现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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 学习Nestjs 有一周的时间了,前几天基本是对Typescipt 基本语法的一些学习,再这里就不在讲述了。当然也...
    FrederickLei阅读 8,633评论 0 15
  • 完整版本,点击此处查看 http://blog.poetries.top/2022/05/25/nest-summ...
    程序员poetry阅读 1,367评论 0 2
  • 背景 在本文中,我将使用Nest.js构建一个CNode。 为什么这篇文章?我喜欢NodeJs,虽然我的NodeJ...
    jiayisheji阅读 11,585评论 3 20
  • 最近看到了一篇介绍node后端的文章,偶然听说了nestjs这个框架。看了一下觉得挺有意思的,就在这里分享一下。 ...
    anOnion阅读 4,729评论 1 7
  • 什么是NestJS Nest 是一个渐进的 Node.js 框架,可以在 TypeScript 和 JavaScr...
    强某某阅读 28,032评论 7 17