9

写给初用Nestj做项目的你(三篇: 编写api & 图片上传)

一. 模块

     比如我们之前写的user的控制器(controller)与服务(service)它们其实应该算是一个整体, 比如某个地方要引入user相关的操作, 直接引入user的module即可, 我们现在就来从头生成个module试试吧。

nest g module modules/users
nest g controller modules/users
nest g service modules/users

image.png

image.png

二. 各种请求方式

     获取不同的请求方式的参数需要用nest提供的不同的装饰器,比如get请求这种显式传参需要用@Query装饰器处理。

get
import { Controller, Get, Query} from '@nestjs/common';
// ...
@Get()
getList(@Query() query) {
    return query;
}

image.png

post
import { Controller, Post, Body } from '@nestjs/common';
// ...
@Post('create')
create(@Body() body) {
    return body
}

image.png

行间id
import { Controller, Get, Param } from '@nestjs/common';
// ...
@Get(':id')
getUser(@Param('id') id): string {
    return id;
}

image.png

三. 公共路径

     设置api路径的前缀是很必要的, 我们这里就设置为/api/v1, 在/share/src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api/v1'); // 这里这里
  await app.listen(3000);
}
bootstrap();

image.png

四. 装饰器

     一些情况下我们可能需要对参数做一下预处理, 比如请求列表的api参数, 必须包含pagepageSize并且最小为1。

     我们在src文件加下创建decorator文件夹里面是paging.decorator.ts文件。

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const Paging = createParamDecorator(
    (data: string, ctx: ExecutionContext) => {
        const request = ctx.switchToHttp().getRequest();
        const query = request.query;
        
        if (!query.page || query.page < 1) {
            query.page = 1
        }

        if (!query.pageSize || query.pageSize < 1) {
            query.pageSize = 1
        }

        return query
    },
);

我们改造原有的getList方法, 替换@Query() query@Paging() query并查看结果;

    @Get()
    getList(@Paging() query) {
        return query;
    }

image.png

五. 管道

     管道将输入数据转换为所需的数据输出, 对输入数据进行验证, 如果验证成功继续传递, 验证失败则抛出异常, 管道可以处理的事情比装饰器更广泛, 比如上面说的装饰器更多是针对参数的, 而管道是整个请求。

     我们用管道来实现一下对page与pageSize的校验,src下面新建pipe文件夹里面是paging.pipe.ts文件内容如下。

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class PagingPipe implements PipeTransform {
    transform(query: any, metadata: ArgumentMetadata) {
        if (metadata.type === 'query') {
            if (!query.page || query.page < 1) {
                query.page = 1
            }

            if (!query.pageSize || query.pageSize < 1) {
                query.pageSize = 1
            }
        }
        return query;
    }
}

这里需要用metadata判断一下是那种请求方再做处理, 他的使用方式如下:

import { Controller, Get, Query, UsePipes } from '@nestjs/common';
@Controller('users')
export class UsersController {

    @Get()
    @UsePipes(new PagingPipe())
    getList(@Query() query) {
        return query;
    }
}
  1. 需要依赖UsePipes装饰器。
  2. 作用在整体的请求上, 并不针对某个参数。
  3. 管道可以设置多个, 例如 @UsePipes(new PagingPipe(), new PagingPipe2())从左向右执行。

六. 中间件

     这个是老朋友了不多介绍了, 直接介绍配置方法吧, 在src目录下创建middleware文件夹, 下面是global.middleware.ts内容如下:

export default function (req, res, next) {
    console.log(`全局函数式: 进入`);
    next();
    console.log(`全局函数式: 退出`);
};
全局使用

main.ts里面直接使用

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import globalMiddleware from './middleware/global.middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api/v1');
  app.use(globalMiddleware)
  await app.listen(3000);
}
bootstrap();
局部使用, 只有路由为/users才生效

     创建users.middleware.ts文件, 内容如下:

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class UsersMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    console.log('进入users中间件')
    next();
    console.log('走出users中间件')
  }
}

/share/src/app.module.ts文件里面做如下修改:

import { Module, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GitlabController } from './modules/gitlab/gitlab.controller';
import { GitlabService } from './modules/gitlab/gitlab.service';
import { UsersModule } from './modules/users/users.module';
import { UsersMiddleware } from './middleware/users.middleware';

@Module({
  imports: [UsersModule],
  controllers: [AppController, GitlabController],
  providers: [AppService, GitlabService],
})
export class AppModule {
  // 此处定义中间件
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(UsersMiddleware)
      .forRoutes('/users*');
  }
}
  1. 如果在main.ts里面定义全局中间件, 只能是函数模式的中间件。
  2. consumer后面可以拼接多个.apply
  3. 如果.forRoutes('/users*');写成.forRoutes('users*');会报错。
  4. forRoutes里面直接配置forRoutes('*')就是全局中间键了, 并且不用必须函数式中间件。

七. 守卫

    守卫与中间键很像它也是在处理请求之前就可以执行, 守卫与中间键的区别是, 中间件调用next但并不知道接下来执行的是什么, 但守卫可以知道接下来执行什么, 守卫一般被用来做权限的验证。

     接下来我们就用jwd做一个简单易懂的校验, 方便理解所以做法是官网的简化版

安装依赖

yarn add @nestjs/jwt

生成模块

nest g module modules/auth
nest g controller modules/auth
nest g service modules/auth

share/src/modules/auth/auth.controller.ts, 配置生成token的接口。

import { Controller, Get, Response } from '@nestjs/common';
import { AuthService } from './auth.service'

@Controller('auth')
export class AuthController {
    constructor(
        private readonly authService: AuthService
    ) { }

    @Get()
    getToken(@Response() res) {
        return this.authService.getToken(res);
    }
}

/share/src/modules/auth/auth.module.ts定义jwt的策略, 比如过期时间等。

import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    JwtModule.register({
      secret: 'secretKey',
      signOptions: { expiresIn: `${60 * 60 * 24 * 10}s` },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService],
  exports: [AuthService], // 这里需要注意, 因为后面会全局使用所以要导出一下
})
export class AuthModule { }

/share/src/modules/auth/auth.service.ts定义生成token的方法。

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
    constructor(
        private readonly jwtService: JwtService
    ) { }
    getToken(res) {
        res.setHeader("token", this.jwtService.sign({
            id: 'dadscxciweneiu122',
            name: "金毛"
        }))
        res.send()
    }
}
  1. 上面使用jwtService的sig方法生成了token并且最好不要直接返回在参数里, 而是放在header里面。
  2. sig里面的对象, 就是要加密的数据, 我们可以把用户的某些id放里面。
  3. exports: [AuthService]很重要, 因为要被引入到守卫里面使用。
守卫的配置

/share/src/guard/token.guard.ts

import { CanActivate, ExecutionContext, Injectable, HttpException } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { Inject } from '@nestjs/common';

@Injectable()
export class TokenGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    @Inject('AuthService') private readonly authService,
  ) { }
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    try {
      const user = this.authService.testToken(request)
      request.user = user;
      return true
    } catch (error) {
      throw new HttpException({
        status: 401,
        error: '身份验证失败',
      }, 401);
    }
  }
}
  1. 这里的例子是当我们解析完token后, 会根据token里面的userId去查找用户信息, 然后request上赋予用户信息, 这样每个操作都可以使用这个用户信息进行验证之类的操作了。
  2. HttpException一个错误类型, 验证不通过这里我们直接报401。

在全局使用守卫, /share/src/app.module.ts增加如下改动:

import { APP_GUARD } from '@nestjs/core';
import { TokenGuard } from './guard/token.guard';
// ...
@Module({
  imports: [UsersModule, AuthModule],
  controllers: [AppController, GitlabController],
  providers: [AppService, GitlabService,
    {
      provide: APP_GUARD,
      useClass: TokenGuard,
    }]
})

八. 检测token

     已生成的token当然需要被校验, 也就是上面的testToken方法的实现/share/src/modules/auth/auth.service.ts:

testToken(req) {
     const token = req.headers.token;
     return this.jwtService.verify(token)
     // 后续链接数据库后会查出user信息返回出去
}

九. 设置无需验证token

     很多api并不用限制用户为登录态, 所以我们要设置一个使请求无需校验的装饰器, 比如获取token的操作就不需要验证身份, 用法如下图。

image.png

/share/src/guard/noauth.ts

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

export const NoAuth = () => SetMetadata('no-auth', true);
  1. SetMetadata设置元数据, 元数据就是用来描述数据的数据, 可以理解为解释这个请求数据是干啥的。
  2. 'no-auth' 设为 true。

/share/src/guard/token.guard.tscanActivate方法里面增加判断

import { CanActivate, ExecutionContext, Injectable, HttpException } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { Inject } from '@nestjs/common';

@Injectable()
export class TokenGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    @Inject('AuthService') private readonly authService,
  ) { }
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    // const headers = request.headers;
    const noAuth =
      this.reflector.get<boolean>('no-auth', context.getHandler());
    if (noAuth) {
      return true;
    }
    // 未授权
    try {
      const user = this.authService.testToken(request)
      request.user = user;
      return true
    } catch (error) {
      throw new HttpException({
        status: 401,
        error: '身份验证失败',
      }, 401);
    }
  }
}
  1. 反射取出no-auth
  2. context.getHandler()返回的是[Function: testToken]也就是我们执行的服务里的方法。
  3. 还可以使用context.getClass()返回值是 [class AuthController]我们当前的请求所在的控制器。
  4. 为了保险可以如下的写法:

     const noAuth =
       this.reflector.get<boolean>('no-auth', context.getClass()) ||
       this.reflector.get<boolean>('no-auth', context.getHandler());

十. 图片上传&展示

浏览图片

     建立文件夹/share/public里面放上一张图片。
     在main.ts文件里面增加如下代码:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express'
import globalMiddleware from './middleware/global.middleware';

async function bootstrap() {
 // const app = await NestFactory.create(AppModule); // 这个改动了
 const app = await NestFactory.create<NestExpressApplication>(AppModule);
 app.setGlobalPrefix('api/v1');
 app.use(globalMiddleware);
 app.useStaticAssets('public', {
   prefix: '/static' // 一定不可以省略 '/'
 });
 await app.listen(3000);
}
bootstrap();

我们看下效果:
image.png

image.png

上传图片

     直接拿到文件, 然后按流的形式存储这里就不说了, 直接演示用装饰器实现接收图片上传功能。

import { Controller, BadRequestException, Post UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { join } from 'path';
import multer = require('multer');

@Controller('user')
export class UserController {
    @Post('img:import')
    @UseInterceptors(
        FileInterceptor('file', {
            storage: multer.diskStorage({
                destination: function (req, file, cb) {
                    // cb(null, join(process.cwd(), 'upload'));
                    cb(null, join(process.cwd(), 'public'));
                },
                filename: function (req, file, cb) {
                    const unique = `${Date.now()}${Math.round(Math.random() * 1e9)}`;
                    const imgPath = `${unique}.${file.mimetype.split('/')[1]}`;
                    cb(null, imgPath);
                },
            }),
            limits: {
                fileSize: 1024 * 1024,
            },
            fileFilter(req, file, cb) {
                if (file.mimetype !== 'image/jpeg' && file.mimetype !== 'image/png') {
                    throw new BadRequestException(`只支持jpg, png格式`);
                }
                cb(null, true);
            },
        }),
    )
    async coverImport(@UploadedFile() file) {
        return { url: `/static/${file.filename}` };
    }
}
  1. destination里面设置储存路径。
  2. filename这里为文件命名, 别忘了加上后缀。
  3. limits就是大小了。
  4. fileFilter做一些过滤操作与抛错。
  5. 最后把相对路径返回给客户端。
  6. 这种写法也并不美观, 所以并不是非要用这种方式。

image.png

end.

     下一篇是使用typeorm操作数据库的实战篇, 会分享官网上没有展示的很多真实案例, 一些实际问题当时可是给我造成了不少困扰, 希望和你一起进步。


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者