开始

# 开始

autumn

# 基本结构

graph TD;
main.js-->app.module.ts;
app.module.ts-->mudule;
mudule-->control;
mudule-->service;
1
2
3
4
5

nest --help查看命令


# control

  • 响应客户端请求

    import { Controller, Get, Post, Query } from '@nestjs/common';
    import { HelloService } from './hello.service';
    @Controller('/hello')//当访问'/hello'时响应
    export class HelloController {
        constructor(private readonly helloService: HelloService) {}//依赖注入
        @Get()
        gethello() { //get请求,调用servce方法
            return this.helloService.getHello()
        }
        @Get("article") //二级路由
        getarticle() {
            return ["messge","messge"]
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • get请求

    //通过@Query装饰器获取get传值
    @Get()
    gethello(@Query() query) {
        console.log(query) //http://localhost:3000/hello?name=xiaoming&id=123
        return this.helloService.getHello()
    }
    
    1
    2
    3
    4
    5
    6
    //可选参数
    @Get() http://localhost:3000/hello?id=123&name=wangcai
    gethello(@Query('id') id) { //只获取id
        console.log(id)
        return this.helloService.getHello()
    }
    
    1
    2
    3
    4
    5
    6
    //通过@Request装饰器获取get传值
    gethello(@Request() req) {
        console.log(req.query)
        return this.helloService.getHello()
    }
    
    1
    2
    3
    4
    5
  • post请求

    //通过@Body装饰器获取post传值 
    @Post("creat") //http://localhost:3000/hello/creat
    post(@Body() body) {
        console.log(body) //postman的body中设置请求参数
        return "post请求"
    }
    
    1
    2
    3
    4
    5
    6
  • 其他请求

    @Controller('animal')
    export class AnimalController {
        constructor(
        private readonly animalService: AnimalService,
         private readonly appService: AppService,
         private readonly birdService: BirdService,
         private readonly animalResolver: AnimalResolver,
        ) {}
        @Get('all')
        all() {
            return this.animalService.findAll();
        }
        @Get('find')
        findById(@Query() query) {
            console.log(query)
            return this.animalService.findById(query.id);
        }
        @Post('add')
        add(@Body() param): any {
            return this.animalService.add(param);
        }
        @Patch(':id') //更新,动态路由,获取123并赋值给id
        update(@Param() param,@Body() body) {
            return this.animalService.update(param.id,body);
        }
        @Delete('delete')
        delete(@Query() {id}){
            return this.animalService.delete(id)
        }
        @Get('redirct')
        @Redirect('https://nestjs.com', 301)
        Redirect() {
            return "重定向"
        }
    }
    
    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

# service

  • 定义服务

    //service.ts
    //nest g service news //命令创建服务
    import { Injectable } from '@nestjs/common';
    //服务就是@Injectable装饰的类
    @Injectable()
    export class NewsService {
        findAll(){
            return [
                '或许辨不清日升日落',
                '或许看不到流云晚霞',
            ];
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 使用服务

    import {Controller,Get,Query,} from '@nestjs/common';
    import { AppService } from './app.service';
    @Controller("app")
    export class AppController {
        constructor(
        private readonly appService: AppService
        ) {} //依赖注入
        @Get()
        getHello(@Query() query): string {
            return this.appService.service();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# module

  • 模块间相互调用方法

    • 方法1:controller所在模块中导入

      //不同模块间调用service需要在controller所在Module中注入service,providers:由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
      //animal.moduel.ts
      import { Module } from '@nestjs/common';
      import {BirdService}  from "../bird/bird.service"
      import { AnimalController } from './animal.controller';
      @Module({
        providers: [BirdService],//animal模块使用bird模块服务
        controllers: [AnimalController],
      })
      export class AnimalModule {}
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
    • 方法2:导入提供service的模块

      //每个模块都是一个共享模块,一旦创建就能被任意模块重复使
      //bird.moduel.ts提供
      import { Module } from '@nestjs/common';
      import { BirdService } from './bird.service';
      @Module({
          providers: [BirdService],
          exports: [BirdService],//bird导出服务
      })
      export class BirdModule {}
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      //animal.moduel.ts使用
      import { Module } from '@nestjs/common';
      import { BirdModule } from '../bird/bird.module';
      import {join} from "path"
      @Module({
        imports: [BirdModule],//导入bird模块使用
        controllers: [AnimalController],
      })
      export class AnimalModule {}
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
    • 方法3:全局模块

      import { Module,Global } from '@nestjs/common';
      import { BirdService } from './bird.service';
      @Global()
      //@Global 装饰器使模块成为全局作用域。 全局模块应该只注册一次,最好由根或核心模块注册。
      @Module({
        providers: [BirdService],
        exports: [BirdService],
      })
      export class BirdModule {}
      
      1
      2
      3
      4
      5
      6
      7
      8
      9

# swagger

  • 初始化

    npm install --save @nestjs/swagger swagger-ui-express
    
    1
    import { NestExpressApplication } from '@nestjs/platform-express';
    async function bootstrap() {
        const app = await NestFactory.create<NestExpressApplication>(AppModule);
        const config = new DocumentBuilder()
        .setTitle('Cats example')
        .setDescription('The cats API description')
        .setVersion('1.0')
        .addTag('cats')
        .build();
        const document = SwaggerModule.createDocument(app, config);
        SwaggerModule.setup('api', app, document); //http://localhost:3000/api/
    
        await app.listen(3000);
    }
    bootstrap();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

# 生命周期

graph LR
开始(收到请求)-->中间件-->守卫-->拦截器before(拦截器`control前`)-->管道-->control-->service-->拦截器after(拦截器`control后`)-->异常过滤器-->结束(响应)
1
2

抛出异常则进入异常过滤器结束


# 守卫 (opens new window)

  • 路由守卫本质上也是中间件的一种,中间件的职责是不明确的,中间件可以干任何事(数据校验,格式转化,响应体压缩等等)。路由守卫只能返回true和false来决定放行/阻断当前请求,不可以修改request/response对象,职责单一。

  • 定义

    import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
    import { Observable } from 'rxjs';
    
    @Injectable()
    export class RolesGuard implements CanActivate {
      canActivate(
        context: ExecutionContext,
      ): boolean | Promise<boolean> | Observable<boolean> {
        const request = context.switchToHttp().getRequest();
        console.log(request.header('authorization')) //token
        return true;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 使用

    //路由守卫级别有全局、控制器、方法级别
    import {RolesGuard}  from "../common/guards/roles.guard"
    export class CatController {
      @Post()
      @UseGuards(RolesGuard)
      allCat() {
      }
    }
    //全局app.useGlobalGuards(new RolesGuard());
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 角色的权限验证

    //定义装饰器roles.decorator.ts自定义元数据附加到路由处理程序
    import { SetMetadata } from '@nestjs/common';
    export const Roles = (...roles: string[]) => SetMetadata('roles', roles);//{role:['admin']}
    
    1
    2
    3
    //定义守卫roles.guard.ts
    import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
    import { Observable } from 'rxjs';
    import { Reflector } from '@nestjs/core';
    
    @Injectable()
    export class RolesGuard implements CanActivate {
        constructor(private reflector: Reflector){}
        canActivate(
        context: ExecutionContext,
    
        ): boolean | Promise<boolean> | Observable<boolean> {
            const request = context.switchToHttp().getRequest();
            const roles = this.reflector.get<string[]>('roles', context.getHandler()); //get方法获取roles
            if (!roles) { // 未被装饰器装饰,直接放行
                return true;
            }
            //判断用户身份逻辑
            return true;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //使用
    @Post()
    @Roles('admin')
    @UseGuards(RolesGuard)
    create() {
        //验证通过操作
    }
    
    1
    2
    3
    4
    5
    6
    7

# 管道

# 装饰器


# 中间件 (opens new window)

  • 路由处理程序之前调用的函数。

  • 生产中间件

    //common/middleware/logger.middlemare.ts
    import { Injectable, NestMiddleware } from '@nestjs/common';
    import { Request, Response, NextFunction } from 'express'; //底层express
    @Injectable()
    export class LoggerMiddleware implements NestMiddleware { //继承NestMiddlewares实现use方法
        use(req: Request, res: Response, next: NextFunction) {
            const {path,method}  = req
            console.log(path,method); //`/`,`GET`
            console.log(`Request Time:${Date()}`)
            next();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • 消费中间件

    //全局使用app.module.ts
    import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
    import { LoggerMiddleware } from './common/middleware/logger.middleware';
    export class AppModule implements NestModule {
        configure(consumer: MiddlewareConsumer) {  //消费中间件
            consumer.apply(LoggerMiddleware).forRoutes('cat').exclude(
            { path: 'user/oauth', method: RequestMethod.GET },
            { path: 'user/login', method: RequestMethod.POST },
            { path: 'favicon.ico', method: RequestMethod.GET },
          )//路由使用,CatsController模块使用
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 拦截器

  • 定义:在函数执行之前/之后绑定额外的逻辑,转换从函数返回的结果

    //定义拦截器refresh-token.interceptors.ts
    import { map } from 'rxjs/operators';
    import { Observable } from 'rxjs';
    @Injectable()
    export class RefreshToken implements NestInterceptor {
      constructor(private readonly catService: CatService) {}
      async intercept(context: ExecutionContext,next: CallHandler,): Promise<Observable<any>> {//每个拦截器都有 intercept() 方法,接收ExecutionContext(执行上下文)和CallHandler(调用处理程序)
        const { access_token } = await this.catService.sign({
          username: 'admin',
          password: '123',
        });
        console.log(access_token);
        return next.handle().pipe( //CallHandler要调用handle()方法才会触发,返回Observable
          map((data) => {  //data为返回数据
            if (!data.access_token) return { data, access_token };
            else return data;
          }),
        );
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
  • 绑定

    //1.全局绑定
    //app.moudle.ts
    import { APP_INTERCEPTOR } from '@nestjs/core';
    import {RefreshToken}  from "./common/interceptors/refresh-token.interceptor"
    @Module({
        providers: [
            {
                provide: APP_INTERCEPTOR,
                useClass: RefreshToken,
            },
        ],
    })
    //2.方法、控制器层
    @UseInterceptors(LoggingInterceptor)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

# 异常过滤器

  • 内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。

  • 定义异常

    import {
      ExceptionFilter,
      Catch,
      ArgumentsHost,
      HttpException,
    } from '@nestjs/common';
    import { Request, Response } from 'express';
    
    @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();
        const res: any = exception.getResponse(); //传的参数
        const { error, msg } = res;
        response.status(status).json({
          statusCode: status,
          timestamp: new Date().toISOString(),
          path: request.url,
          error,
          msg
        });
      }
    }
    
    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
  • 使用

    //路由中使用(控制器级别与方法级别)
    import { Controller, Get, Param, UseFilters,Post,Body,HttpException,HttpStatus } from '@nestjs/common';
    import { HttpExceptionFilter } from '../common/filter/http-exception.filter';
    @Controller('cat')
    @UseFilters(HttpExceptionFilter)
    export class CatController {
      @Post()
      allCat(@Body() {id} ) {
        if (!id) throw new HttpException({error:"id is required",msg:"id必传"}, HttpStatus.FORBIDDEN);
         return 'cat';
      }
    }
    //全局main.ts中app.useGlobalFilters(new HttpExceptionFilter());
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • Nestjs Flub (opens new window)

  • graphql异常

    https://www.cnblogs.com/coderxiaohei/p/14256248.html


# 安全

# local策略

  • 定义

    //npm install --save @nestjs/passport passport passport-local
    //npm install --save-dev @types/passport-local
    //local.strategy.ts
    import { Strategy } from 'passport-local';
    import { PassportStrategy } from '@nestjs/passport';
    import { Injectable, UnauthorizedException } from '@nestjs/common';
    import { CatService } from './cat.service';
    @Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy,'local') {//localStrategy本地策略,默认名local
      constructor(private readonly catService: CatService) {
        super(); 
      }
      async validate(username: string, password: string): Promise<any> {
        //守卫将自动调用validate,从body中获取username,password
       //throw不打印,"message": "Unauthorized"
        const user = await this.catService.validateUser(username, password);
        if (!user) {
          throw new UnauthorizedException( '用户名或密码错误');//没传用户名或密码抛出其他错误
        }
        return true;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
  • 使用

    @UseGuards(AuthGuard('local')) //使用策略
    @Post('login')
    async login(@Body() body) {
        return await this.catService.login(body); //返回token
    }
    
    1
    2
    3
    4
    5

# JWT策略 (opens new window)

title: Json web token
浏览器-->服务器:提交用户名密码
Note right of 服务器:验证通过后,加密得到token(令牌)
服务器-->浏览器:将token响应给浏览器
Note left of 浏览器:请求头带上token
浏览器-->服务器:再次请求
Note right of 服务器:通过token还原出用户信息
服务器-->浏览器:再次响应
1
2
3
4
5
6
7
8
  • 返回token

    import { Injectable } from '@nestjs/common';
    import { JwtService } from '@nestjs/jwt';
    var passport = require('passport')
    @Injectable()
    export class UserService {
        constructor(private readonly jwtService: JwtService) {}
        async validateUser(username: string, password: string): Promise<any> {
            if (username === 'admin' && password === '123') return username;
            else return null;
        }
        async login(code: string, res: Response) {
            const token = this.jwtService.sign(body);
            //response对象在graphql上下文中
            //@Context() context:any
            //const res = context.req.res
            res.cookie("Authorization", access_token, { httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 7 })//种植cookie
            return { access_token: token }; //验证通过返回token
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  • 初始化

    //npm install @nestjs/jwt passport-jwt
    //npm install @types/passport-jwt --save-dev
    // auth.module.ts
    import { Module } from '@nestjs/common';
    import { PassportModule } from '@nestjs/passport';
    import { LocalStrategy } from './local.strategy';
    import { JwtModule } from '@nestjs/jwt';
    import { jwtConstants } from './constants';//签名时使用秘钥
    import { JwtStrategy } from './jwt';
    @Module({
        imports: [
            PassportModule.register({defaultStrategy:"jwt"}),//默认jwt策略
            JwtModule.register({
                secret: jwtConstants.secret,
                signOptions: { expiresIn: '60s',issuer:"portal" },//issuer颁发者标识
            }),
        ],
        providers: [LocalStrategy,JwtStrategy],
    })
    export class CatModule {}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //定义jwt策略
    import { ExtractJwt, Strategy } from 'passport-jwt';
    import { PassportStrategy } from '@nestjs/passport';
    import { Injectable } from '@nestjs/common';
    import { jwtConstants } from './constants';//解密时使用秘钥
    
    @Injectable() //解析token
    export class JwtStrategy extends PassportStrategy(Strategy,'jwt') { //默认名jwt
        constructor() {
            super({
                //jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), //header获取Bearer token值
                jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
                    const token= request?.cookies["Authorization"]
                    if(token) return token  
                    return null
                }]),//从cookie中获取token
                ignoreExpiration: false,
                secretOrKey: jwtConstants.secret,
                issuer:"portal"//可以不验证
            });
        }
        async validate(payload: any) {
            //payload:jwt-passport认证jwt通过后解码的结果
            return { userId: payload.password, username: payload.username };
        }
    }
    
    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
  • 使用策略

    import { AuthGuard } from '@nestjs/passport';
    @UseGuards(AuthGuard())//方法、控制器
    @Get('list')
    list() {
        return ['大天狗', '玉藻前', '桃花妖'];
    }
    
    1
    2
    3
    4
    5
    6
  • GraphQL

    //上下文
    import { GqlExecutionContext } from '@nestjs/graphql';
    const ctx = GqlExecutionContext.create(context);
    const GqlRequest = ctx.getContext().req;
    //jwt.guard.ts
    getRequest(context: ExecutionContext) {//覆盖方法使jwtguard能获取请求头token
        const ctx = GqlExecutionContext.create(context);
        return ctx.getContext().req;
    }
    //GqlExecutionContext 为每个参数公开相应的函数,比如 getArgs(),getContext()等等
    const path = ctx.getInfo().fieldName; //解析body
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • passportjs (opens new window)

  • 如何优雅的处理JWT过期问题 (opens new window)


# GitHub OAuth (opens new window)

//1.前端请求https://github.com/login/oauth/authorize?client_id=e67af17239b756793555,用户确定后重定向到后端路由
//2.重定向http://localhost:3000/cat/oauth?code=27033ac7b31e95ff7a9f
//3.响应http://localhost:3000/cat/oauth请求
import { HttpService } from '@nestjs/axios';
async oauth(code) {
    const id = 'e67af17239b756793555',
          secret = 'b7575c664236f0978779072ae6c8bd5029c16bba';
    const url = `https://github.com/login/oauth/access_token?client_id=${id}&client_secret=${secret}&code=${code}`;
    const { data } = await this.httpService.get(url).toPromise();//通过code获取token
    if (data) {
        const access_token = data.split('=')[1].slice(0, -6); //&scope
        const res = await this.httpService  //通过token获取用户数据
        .get('https://api.github.com/user', {
            headers: { Authorization: `token ${access_token}` },
        })
        .toPromise();
        return res.data;
    } else {
        return 'github过期';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

//初始化
import cookieParser from 'cookie-parser';
app.use(cookieParser());
//cookie不支持跨域
GraphQLModule.forRoot({
    cors: {
        credentials: true,
        origin: true
    },
})
//vue apollo配置
link: createUploadLink({
    uri: 'http://localhost:3000/graphql',
    credentials: 'include',//请求带上cookie
}),
//playground配置
"request.credentials": "include",
//apollo sandbox配置
打开Include cookies
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# GraphQL (opens new window)

  • # 安装依赖

npm i --save @nestjs/graphql graphql-tools graphql

npm i apollo-server-express// apollo-server-express@^3对应@nestjs/graphql@^9

npm view apollo-server-express version//查看远程

npm ls apollo-server-express//查看本地

NestJs提供的@nestjs/graphql只是对 Apollo server (opens new window) 的包装

  • # 初始化

    //app.module.ts
    //https://tkssharma.com/nestjs-with-graphql-api-development/
    import { AuthorModule } from './author/author.module';
    import { Module } from '@nestjs/common';
    import { GraphQLModule } from '@nestjs/graphql';
    import { AuthorModule } from './author/author.module';
    @Module({
        imports: [
            AuthorModule,
            GraphQLModule.forRoot({
                //autoSchemaFile: true, //内存中生成
                debug: false,
                autoSchemaFile: join(process.cwd(), 'src/schema.gql'),//自动生成schema.gql文件,自动整合,Resolver中没用到不生成
                path:"/graphql",  //GraphQL服务器端口
                playground:true,//是否开启playground,或使用对象配置playground
                cors: true,
                installSubscriptionHandlers: false,//开启订阅服务
                include: [AuthorModule],//默认情况下,`GraphQL`在整个应用程序中搜索解析器,使用include限制模块的子集
            }),
        ],
    })
    export class AppModule {}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
  • Model中定义 Schema

    //通过Model定义生成schema
      import { Field, Int, ObjectType } from '@nestjs/graphql';
      @ObjectType()
      export class Author {
        @Field(type => Int)
        id: number;
      
        @Field({ nullable: true })
        firstName: string;
      
        @Field({ nullable: true })
        lastName: string;
      }  
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    type定义数据类型,标量类型是原始数据类型,只能存储单个值,包括:Int(32位整数),Float(有符号双精度浮点值),String(UTF - 8字符串),Boolean(True或false),ID(唯一标识符),还有对象类型,允许嵌套类型。

  • cli plugin

    //Model中1.{ nullable: true }可用?代替
    //2.@Field()中无配置(如description,type)可以不写@Field()
    
    1
    2
  • resolver:解析函数提供数据

    1. query查询:获取数据

      //解析器,类似于controller,接口请求时响应数据
      //nest g resolver author
      import { Resolver, Query, Args, Int } from '@nestjs/graphql';
      import { Author } from './author.model';
      import { AuthorService } from './author.service';
      @Resolver()
      export class AuthorResolver {
        constructor(private readonly authorService: AuthorService) {}
        @Query((returns) => [Author])//使用的Schema
        author() { //根据方法名访问数据
          return this.authorService.findAll();//返回值需要符合[Author]格式
        }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      //ArgsType查询输入
      @ArgsType()
      export class query{
          @Field()
          name: string;
          @Field()
          id: string;
      }
      //使用
      @Query((returns) => User)
      async user(@Args() query: query) {}
      //playground
      query{
        user(name:"xx",id:"xx")
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
    2. mutation:对数据进行变更

      Repository API (opens new window)

      //输入对象看上去和常规对象一模一样,除了关键字是 input 而不是 type
      //1.定义Mutation
      @Mutation(returns => Author)  //返回查询字段
      async creatAuthor(@Args('newAuthor') newAuthor:inputAuthor) {//@Args('newAuthor')中为输入名
          return await this.authorService.creat(newAuthor);
      }
      //2.输入schema类型
      //eg:@ArgsType代表查询格式,@InputType代表输入格式
      @InputType()  //输入类型
      export class inputAuthor {
          @Field()
          firstName: string;
          @Field()
          lastName: string;
          @Field()
          name: string;
      }
      /*生成的schema
      type Mutation {
        creatAuthor(newAuthor: inputAuthor!): Author!
      }
      
      input inputAuthor {
        firstName: String!
        lastName: String!
        name: String!
      }
      */
      //3.serive
      async creat(author) {
          const newAuthor = this.authorRepository.create(author); //创建实例,只复制实体中存在属性
          //  this.authorRepository.insert(newAuthor); //插入返回空
          return this.authorRepository.save(newAuthor);
      }
      
      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
      //请求
      mutation mutationName{
          creatAuthor(newAuthor:{firstName:"七月的海",lastName:"七月的海",name:"七月的海"}){
              id
              firstName
              lastName
          }
       }
      
      1
      2
      3
      4
      5
      6
      7
      8
      //DTO
      //它使用了功能强大的class-validator包及其声明性验证装饰器
      import { InputType, Field } from '@nestjs/graphql';
      import { IsNotEmpty, MinLength } from 'class-validator';
      
      @InputType()
      export class RegisterDto {
        @Field()
        @MinLength(3, { message: '最小长度为3' })
        @IsNotEmpty({ message: '用户名不能为空' })
        username: string;
      
        @Field()
        @IsNotEmpty({ message: '密码不能为空' })
        password: string;
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
    3. subscription订阅:当数据发生更改,进行消息推送

      /*1.启用订阅
      GraphQLModule.forRoot({
          installSubscriptionHandlers: true,
      })*/
      //2.定义订阅author.resolver.ts
      import {PubSub}  from "graphql-subscriptions"
      const pubSub = new PubSub();
      @Resolver((of) => Author){
          @Mutation((returns) => Api) //更新
          async update() {
              pubSub.publish('update', {commentUpdate:"change"  });//发布事件
          }
      }
      @Subscription(returns => String,{name: 'commentUpdate'})
      commentUpdate() {
          return pubSub.asyncIterator('update'); //asyncIterator订阅事件
      }
      }
      /*生成schema
      type Subscription {
        commentUpdate: String!
      }*/
      //3.请求
      subscription{
        commentUpdate
      }
      /*
      {
        "data": {
          "commentUpdate": "change"
        }
      }
      */
      
      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
  • 发送请求

    //http://localhost:3000/graphql
    query queryName{   
        //请求描述operationName,GraphQLModule中配置 context:({req})=>{ console.log(req.body.operationName)}获取
        author{
            userid:id //重命名id
            firstName
        }
    }
    //Apollo Server supports only GET/POST requests.
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 查询片段

    {
        author{
            lastName
                ...infomation
        }
    }
    fragment infomation  on Author{ //查询碎片
        id
        name
        firstName
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 查询变量

    query  queryName($id:Float!,$last:Boolean!){
        __typename //查询类型
        oneAuthor(id:$id){
            __typename 
            id
            firstName
            lastName @include(if:$last)  //条件查询
        }
    }
    /*
    QUERY VARIBLES
    {
      "id": 6,
      "last":true
    }
    */
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  • 传参

    import { Resolver, Query, Args, Int } from '@nestjs/graphql';
    import { Author } from './author.model';
    import { AuthorService } from './author.service';
    @Resolver()
    export class AuthorResolver {
        constructor(private readonly authorService: AuthorService) {}
        @Query((returns) => [Author],{name:"writer"})//不写name默认author
        async oneAuthor(@Args({ name: 'id', type: () => Int }) id: number) {
            //或者@Args('id', { type: () => ID })/@Args('id')
            console.log(id)
            return await this.authorService.findOneById(id);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //前端请求
    {
        author1: oneAuthor(id:1){ //重命名
            userid:id
            name
        }
    }
    /*{
         "data": {
           "author1": {
             "userid": "1",
             "name": "三月的风"
           }
         }
    }*/
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 校验参数类型

    //定义校验类
    import {Min,Max,IsInt} from "class-validator"
    @ArgsType()
    class AuthorArgs { //DTO(数据传输对象),对入参数据校验
      @Field(type => Int)
      @Max(1)
      id: number; //请求时参数名
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    // 使用AuthorArgs 类
    @Args() id: AuthorArgs  //请求的参数名
    
    1
    2
  • 参数装饰器

    Nest Apollo dec
    @Root()/@Parent() root/parent 这是该字段父级的解析器的返回值(父字段的解析器总是该字段的子字段的解析器之前执行,query没有父级解析器)。
    @Context(param?:string) context/context[param] 提供给所有解析器的上下文信息
    @Info(param?:string) info/info[param] 这包含有关操作执行状态的信息(仅在高级情况下使用)。
    @Args(param?:string) args/args[param] 此对象包含查询中传入的所有参数
  • 解析器链 (opens new window)

    @Resolver((of) => Author) //父级解析器
    export class AuthorResolver {
        @ResolveProperty("ID",type=>String)  //字段解析器添加字段,String类型,更新schema.gql文件
        async authorDashID(@Parent() parent:Author) {  //默认字段名
            // console.log(parent)  //Author的查询值
            return `${parent.name}--${parent.id}`
      }}
    
    1
    2
    3
    4
    5
    6
    7

# 缓存

  • 单个请求取消缓存

    const data = await this.$apollo.query({
        query: gql`
          query user {
            user(id: 1) {
              id
              name
              gender
            }
          }`,
        fetchPolicy: "no-cache",//取消走缓存
        variables: {
            id: userid,
        },
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 所有请求取消缓存

    const apolloClient = new ApolloClient({
        link: createUploadLink({
            uri: 'http://localhost:3000/graphql',
            credentials: 'include',
        }),
        cache: new InMemoryCache(),
        connectToDevTools: true,
        defaultOptions: {
            query: {
                fetchPolicy: 'network-only'//取消缓存
            }
        }
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

# 文件上传

  • restful文件上传

    //Controller
    @Controller('upload')
    export class UploadController {
        constructor(private readonly uploadService: UploadService) { }
        //单文件上传
        @Post('img')
        @UseInterceptors(FileInterceptor("img"))
        async uploadImg(@UploadedFile() img: Express.Multer.File) {
            return { path: img.path.replace("public\\", "") }
        }
        //文件数组上传
        @Post('imgs')
        @UseInterceptors(FilesInterceptor("imgs", 5)) //多传Unexpected field
        async uploadImgs(@UploadedFiles() imgs: [Express.Multer.File]) {
            const paths = []
            imgs.forEach(i => {
                paths.push(i.path.replace("public\\", ""))
            });
            return { paths }
        }
        //多文件上传
        @Post('mulImg')
        @UseInterceptors(FilesInterceptor("img"))  //FileFieldsInterceptor自定义不同的字段名称键,AnyFilesInterceptor使用任意的字段名称
        async uploadMulImg(@UploadedFiles() mulImg: [Express.Multer.File]) {
            console.log(mulImg)
            const paths = []
            mulImg.forEach(i => {
                paths.push(i.path.replace("public\\", ""))
            });
            return { paths }
        }
    
    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
    //upload.Module.ts控制文件大小、类型,上传位置
    @Module({
      imports: [
        MulterModule.register({
          // dest: './public/img/',
          fileFilter(req, { mimetype, size, path }, cb) {
            if (mimetype.indexOf("image") === -1) {
              const error = new HttpException('上传图片类型错误', HttpStatus.UNSUPPORTED_MEDIA_TYPE);
              cb(error, false)
            }
            cb(null, true)
          },
          limits: { fileSize: 1024 * 1024 * 2 },
          storage: diskStorage({
            //自定义路径
            destination: async (req, file, cb) => {
              const { user } = req.headers
              const path = `public/userImg/${user}/`
              fs.exists(path, function (exists) {
                if (!exists) fs.mkdir(path, err => {
                  if (err) throw new HttpException("创建文件夹失败", HttpStatus.INTERNAL_SERVER_ERROR)
                })
              })
              cb(null, path)
            },
            filename: (req, file, cb) => {
              return cb(null, file.originalname);
            }
          })
        })
      ],
    })
    
    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
  • graphql文件上传

    //使用graphql-upload实现文件上传
    //方法1.Apollo Server 2.0内置graphql-upload,GraphQLUpload, FileUpload从apollo -server-express导入
    //方法2.Apollo Server 3.0移除graphql-upload,使用3.0要使用graphqlUploadExpress中间件
    //https://www.apollographql.com/docs/apollo-server/data/file-uploads/
    import { GraphQLUpload, FileUpload } from 'graphql-upload';
    import { UploadService } from "./upload.service"
    import { UploadImgResult } from "./models/upload.model"
    
    @Resolver()
    export class UploadResolver {
        constructor(private readonly uploadService: UploadService) { }
        @Mutation((returns) => UploadImgResult)
        async uploadImg(@Args({ name: 'file', type: () => GraphQLUpload }) file: FileUpload, @Context() context) {
            return await this.uploadService.uploadImg(file, context)
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //mian.ts使用graphqlUploadExpress中间件
    import { graphqlUploadExpress } from "graphql-upload"
    async function bootstrap() {
      const app = await NestFactory.create<NestExpressApplication>(AppModule, {
      app.useStaticAssets('public');
      app.use(graphqlUploadExpress());//可配置maxFileSize, maxFiles
      await app.listen(3000);
    }
    bootstrap();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    前端请求格式:https://github.com/jaydenseric/graphql-multipart-request-spec

    参考:

  • NestJS file upload with GraphQL (opens new window)


# 文件下载

  • express sendFile

    import { Controller, Get, Res } from '@nestjs/common';
    import { Response } from 'express'
    import path from 'path'
    @Controller('down')
    export class DownController {
      @Get()
      findAll(@Res() res: Response): string {
        //res.sendFile(path.join(__dirname,'../src/schema.gql'))
        res.download(path.join(__dirname,'../src/schema.gql'),'schema.gql')//设置文件名
        return '下载成功';
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • 文件流

    import { Controller, Get, StreamableFile } from '@nestjs/common';
    import { createReadStream } from 'fs';
    import path from 'path'
    @Controller('file')
    export class FileController {
        @Get()
        getFile(): StreamableFile {
            const file = createReadStream(path.join(process.cwd(), 'package.json'));//创建文件流
            res.set({//自定义响应
                'Content-Type': 'application/json',
                'Content-Disposition': 'attachment; filename="package.json'//设置文件名
            })
            return new StreamableFile(file);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

# 文件压缩

  • tar

    import { tar } from 'compressing'
    import path from 'path'
    tar.compressDir(path.join(__dirname,'../dist'), './dist.tar')
        .then(()=>{
        console.log('压缩成功');
    })
        .catch(e=>{console.log(e);
                  });
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 压缩文件流

    import { Controller, Get, Res, StreamableFile } from '@nestjs/common';
    import { Response } from 'express'
    import { tar } from 'compressing'//包括tar、gzip、tgz、zip。zipGzip仅支持压缩单个文件
    import path from 'path'
    @Controller('files')
    export class FileController {
        @Get()
        getFile(@Res() res: Response) {
            const tarStream = new tar.Stream();
            tarStream.addEntry(path.join(__dirname, '../dist'))//压缩文件
            res.set({
                'Content-Type': 'application/oct-stream',//流形式
                'Content-Disposition': 'attachment; filename=dist.tar'
            })
            return tarStream.pipe(res);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  • 解压

    const data=path.join(process.cwd(), `/public/dist.tar`)
    const file = path.join(process.cwd(), `/public`)//文件名默认原来文件名
    tar
        .uncompress(data, file)
        .then(() => console.log("uncompress done"))
        .catch(console.error);
    
    1
    2
    3
    4
    5
    6

# 文件转换

  • libreoffice-convert

    import libre from 'libreoffice-convert';
    import * as fs from 'fs';
    import path from 'path';
    const { promisify } = require('bluebird');
    let libreConvert = promisify(libre.convert)
    
    const extend = '.pdf'//转换的格式
    const enterPath = path.join(__dirname, '../public/');//读取文件路径
    const outputPath = path.join(__dirname, '../preview/');//输出文件路径
    fs.readdir("./public/", async (err, data) => {
      if (err) console.log(err)
      for (let i of data) {
        if (i.indexOf('.') !== -1) {
          const file = fs.readFileSync(enterPath + i);
          let done = await libreConvert(file, extend, undefined)//转换为pdf的format为undefined
          fs.writeFileSync(outputPath + i + extend, done);//输出转换后的文件
        }
      }
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  • sheet.js

    const XLSX = require('XLSX')
    const workbook = XLSX.readFile('./public/10-26.xlsx')//文件对象
    var worksheet = workbook.Sheets[workbook.SheetNames[0]];
    const str = XLSX.utils.sheet_to_html(worksheet, {editable: true,id:"11"})//html字符串,editable可编辑
    fs.writeFile('./output/excel.csv', str, (err) => {
        if (err) console.log(err)
       else console.log('写入成功')
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    //or
    const XLSX = require('XLSX')
    const workbook = XLSX.readFile('./public/10-26.xlsx')//文件对象
    XLSX.writeFile(workbook, './output/10-26.html',  { bookType: "html" ,editable: true});
    
    1
    2
    3
    4

# 截屏

  • puppeteer

    //https://www.lxchuan12.cn/puppeteer-create-pdf-and-merge
    const puppeteer = require('puppeteer');
    (async () => {
        //const browser = await puppeteer.launch({ executablePath: 'C:/Program Files (x86)/ChromeCore/ChromeCore.exe',headless: false})//使用自定义浏览器
        const browser = await puppeteer.launch({
            // headless: false,//是否查看浏览器界面,false打开,pdf不支持
            // devtools:true//f12
        });//启动浏览器
        const page = await browser.newPage();//浏览器打开页面
        await page.goto('https://example.com',{ waitUntil: 'networkidle2' });//页面加载url
        // await page.screenshot({ path: './output/example.png' });//截屏生成png
        await page.pdf({ path: './output/example.pdf', format: 'a4' });//页面保存为pdf
        await browser.close();
        console.log("完成")
    })();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • pdf合并

    //依赖pdftk(https://www.pdflabs.com/docs/pdftk-man-page/)
    //或者使用网站https://smallpdf.com
    const PDFMerge = require('pdf-merge');
    const fs = require('fs');
    const { join } = require('path');
    const files = fs.readdirSync('./pdfs')
    async function merge(name) {
        for (let i = 0; i < files.length; i++) {
            files[i] = join(__dirname, `/pdfs/${files[i]}`)
        }
        await PDFMerge(files, { output: `${__dirname}/pdfs/${name}.pdf` })
        for (let i = 0; i < files.length; i++) {//删除
            fs.unlinkSync(files[i])
        }
    }
    merge('new')
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

# HTTP模块

//cat模块导入http模块
import { HttpModule } from '@nestjs/axios';
@Module({
    imports:[HttpModule.register({})]
})
//catService使用httpserive
import { HttpService } from '@nestjs/axios';
@Injectable()
export class CatService {
    constructor(private readonly httpService: HttpService,){}
    async oauth (){
        const url = "www.baidu.com"
        //返回的类型为Observables
        const { data } = await this.httpService.get(url).toPromise();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 处理Observables对象

    //1.转换为promise
    this.httpService.get(url).toPromise();
    //2.订阅subscribe
    login() {
        return new Promise(resolve => {
            this.httpService
            .get(url)
            .subscribe((res) => resolve(res));
        });
    }
    
    this.httpService
        .get(url)
        .subscribe((res) => console.log(res));
    //3.map
    import { AxiosResponse } from 'axios';
    import { map } from 'rxjs/operators';
    this.httpService
        .get(url)
        .pipe(
        map((axiosResponse: AxiosResponse) => {
            return axiosResponse.data;
        }),
    );
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
  • RxJS中文文档 (opens new window)


# WebSocket

  • 安装

    npm i --save @nestjs/websockets @nestjs/platform-socket.io
    
    1
  • gateway

    //ws.gateway.ts
    import { ConnectedSocket, MessageBody, SubscribeMessage, WebSocketGateway } from "@nestjs/websockets";
    import * as WebSocket from 'ws';
    //gateway(网关)只是一个用 @WebSocketGateway() 装饰器注释的类
    @WebSocketGateway(3001)//端口不能和http冲突
    export class WsStartGateway {
      //订阅消息
      @SubscribeMessage('hello')
      hello(@MessageBody() data: any): any {
        return {
          "event": "hello",
          "data": data,
          "msg": 'rustfisher.com'
        };
      }
    
      @SubscribeMessage('hello2')
      hello2(@MessageBody() data: any, @ConnectedSocket() client: WebSocket): any {
        console.log('收到消息 client:', client);
        client.send(JSON.stringify({ event: 'tmp', data: '这里是个临时信息' }));
        return { event: 'hello2', data: data };
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
  • 注入

    //app.module.ts
    import { Module } from '@nestjs/common';
    import { WsStartGateway } from './ws.gateway'
    
    @Module({
      providers: [AppService, WsStartGateway],
    })
    export class AppModule { }
    
    1
    2
    3
    4
    5
    6
    7
    8

# 数据库

# TypeORM 集成

  • 初始化

    //npm install --save @nestjs/typeorm typeorm mysql2
    //1.数据库配置文件ormconfig.json
    {
        "type": "mysql",
        "host": "192.9.180.53",
        "port": 3306,
        "username": "portal",
        "password": "123456",
        "database": "portal",
        "entities": ["dist/**/*.entity{.ts,.js}"],//entity实体路径
        "autoLoadEntities": true,  //自动加载实体
        "synchronize": true//自动生成数据库表
        charset:"utf8"//字符集
    }
    //2.app.module.ts使用配置,导入TypeOrmModule.forRoot(),
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 定义实体

    //author文件夹下nest g class author.entity --no-spec 生成author.entity.ts
    //实体文档https://typeorm.io/#/entities
    //实体的数据从数据库获得
    //class-validator随笔:https://segmentfault.com/a/1190000025123187
    import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
    import { IsEmail, IsMobilePhone } from "class-validator";
    import { RoleEntity } from "./role.entity"
    const gender = ['男', '女']
    @Entity("test")  //数据库表名
    export class AuthorEntity { //默认数据库表名
        @PrimaryGeneratedColumn({ comment: '主键' })//必须有主键
        id: number;
        @Column({ length: 100, comment: '姓名' })
        name: string;
        @Column({ length: 5, unique: true, comment: '工号' })
        num: string;
        @Column({ default: 2, comment: '角色,默认普通用户', })
        @ManyToOne(() => RoleEntity, role => role.id)
        role_id: number;
        @Column({ type: "enum", enum: gender, default: '男', comment: '性别', })
        gender: string;
        @Column({ type: 'int', nullable: true, comment: '头像' })
        avatar: string;
        @Column({ nullable: true, length: 11, comment: '电话' })
        @IsMobilePhone('zh-CN')
        phone: string;
        @Column({ default: 1, comment: '状态,0关闭,1开启' })
        status: boolean;
        @Column({ length: 30, comment: '邮箱' })
        @IsEmail()
        email: string;
    }
    
    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
  • 使用实体

    //1.author模块注册
    import {AuthorEntity} from "./author.entity"
    @Module({
      imports:[TypeOrmModule.forFeature([AuthorEntity])],
    })
    //2.server注入实体使用
    import { Injectable } from '@nestjs/common';
    import { Author } from './author.model';
    import { AuthorEntity } from './author.entity';
    import {InjectRepository}  from "@nestjs/typeorm"
    import { Repository } from 'typeorm';
    @Injectable()
    export class AuthorService {
      constructor(
        @InjectRepository(AuthorEntity) 
        private authorRepository : Repository<AuthorEntity>
         //@InjectRepository()装饰器将 AuthorRepository注入到 AuthorService 中
    ) {}
       findOneById(id: number) {  //查询一条数据,返回对象
        return this.AuthorRepository.findOne(id);
      }
      async findAuthor(): Promise<Author[]> { //所有数据,返回数组
        return await this.AuthorRepository.find();
      }
    }
    //3.resolver使用serive
    
    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
  • TypeORM中文文档 (opens new window)

# Sequelize 集成

  • 初始化

    //npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
    //npm install --save-dev @types/sequelize
    //npm install graphql@14.6.0 //graphql需要降级
    //app.module.ts配置
    import { SequelizeModule } from '@nestjs/sequelize';
    import { Module } from '@nestjs/common';
    import { AuthorEntity } from './author/entity/author.entity';
    @Module({
        SequelizeModule.forRoot({
        dialect: 'mysql',
        host: '192.9.180.53',
        port: 3306,
        username: 'portal',
        password: '123456',
        database: 'portal',
        models: [AuthorEntity], //模型
            define:{timestamps:false}//数据库中的实际表定义不包含时间戳列报错
    }),
    })
    export class AppModule {}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
  • 定义模型(实体)

    //author.entuty.ts
    import { Column, Model, Table,PrimaryKey } from 'sequelize-typescript';
    @Table({ tableName:"author"})//表名
    export class AuthorEntity extends Model<AuthorEntity> {
        @PrimaryKey  //主键
        @Column id: number;
        @Column
        firstName: string;
        @Column
        lastName?: string;
        @Column
        name?: string;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 使用模型

    //author.module.ts注册
    import { Module } from '@nestjs/common';
    import { SequelizeModule } from '@nestjs/sequelize';
    import {AuthorEntity}   from "./entity/author.entity"
    @Module({
    imports:[ SequelizeModule.forFeature([AuthorEntity])],
    })
    export class AuthorModule {}
    
    1
    2
    3
    4
    5
    6
    7
    8
    //author.server.ts使用
    import { Injectable } from '@nestjs/common';
    import { AuthorEntity } from './entity/author.entity';
    import {Author}  from "./model/author.model"
    import { InjectModel } from '@nestjs/sequelize';
    @Injectable()
    export class AuthorService {
        constructor(
        @InjectModel(AuthorEntity)
         private authorModel: typeof AuthorEntity
        ) {}
        //查询
        // findOneById(id: number) {
        //   return this.AdminRepo.findOne(id);
        // }
        async findAuthor(): Promise<Author[]> {
            return await this.authorModel.findAll();
        }}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
  • Sequelize 中文文档 (opens new window)


# vue apollo

  • 配置

    //npm install vue-apollo graphql apollo-boost --save
    import ApolloClient from "apollo-boost";
    
    import VueApollo from 'vue-apollo'
    Vue.use(VueApollo);
    new Vue({apolloProvider, render: (h) => h(App) }).$mount("#app");
    
    1
    2
    3
    4
    5
    6
  • vue3

    //apollo.providers.ts
    import { InMemoryCache, ApolloClient } from "@apollo/client";
    import { createApolloProvider } from "@vue/apollo-option";
    import { createUploadLink } from "apollo-upload-client"
    function getCookie(cname) {
      var name = cname + "=";
      var ca = document.cookie.split(';');
      for (var i = 0; i < ca.length; i++) {
        var c = ca[i].trim();
        if (c.indexOf(name) == 0) return c.substring(name.length, c.length);
      }
      return "";
    }
    // const httpLink = new HttpLink({
    //   uri: 'http://localhost:3000/graphql',
    //   headers: {
    //     authorization: getCookie("Authorization"),
    //   }
    // })
    const apolloClient = new ApolloClient({
      link: createUploadLink({//上传图片需要把httpLink替换为createUploadLink
        uri: 'http://localhost:3000/graphql',
        //headers: {authorization: getCookie("Authorization"),}//token放请求头中
        credentials: 'include',//token放cookie中
      }),
      cache: new InMemoryCache(),
      connectToDevTools: true,
    })
    
    // Create a provider
    export const apolloProvider = createApolloProvider({
      defaultClient: apolloClient,
    })
    //mian.ts
    import {apolloProvider} from './apollo.provider'
    app.use(apolloProvider);
    
    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

# 文档

  1. npm i -D @compodoc/compodoc 安装依赖
  2. npx compodoc -p tsconfig.json -s 生成
  3. http://localhost:8080 访问

# 配置config (opens new window)

//yard add nestjs-config
//app.module.ts
import { ConfigModule, ConfigService } from 'nestjs-config';
@Module({
    imports: [
        ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),//配置文件目录src/config/*ts
        GraphQLModule.forRootAsync({
            useFactory: async (configService: ConfigService) =>
            configService.get('graphql'),//graphql.ts
            inject: [ConfigService],//依赖注入
        }),
    ],
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//graphql.ts
import { join } from 'path';
export default {
  autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
  cors: true,
  playground: true,
  debug: true,
};
1
2
3
4
5
6
7
8
  • https://cloud.tencent.com/developer/article/1533422
  • https://ionicacademy.com/ionic-code-documentation/

# 日志

  • npm i nest-winston  //处理日志
    npm i winston-daily-rotate-file //分割日志
    
    1
    2
  • 初始化

    //方法1.app.useLogger中间件使用
    //方法2.mian.ts使用,替换nestjs内置logger
    //main.ts
    import { NestFactory } from '@nestjs/core';
    import { NestExpressApplication } from '@nestjs/platform-express';
    import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
    
    async function bootstrap() {
      const app = await NestFactory.create<NestExpressApplication>(AppModule);
      app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)) //使用winston日志
      await app.listen(3000);
    }
    bootstrap();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //app.module.ts配置winston
    import { Module } from '@nestjs/common';
    import { WinstonModule } from 'nest-winston';
    import * as winston from 'winston';
    import DailyRotateFile = require("winston-daily-rotate-file");
    const format = winston.format;
    
    @Module({
        imports: [
            WinstonModule.forRoot({
                exitOnError: false,//处理到的异常不退出
                level:"info",//{error: 0,warn: 1,info: 2(defalult),http: 3,verbose: 4, debug: 5,silly: 6}数字越小级别越高
                format: format.combine(//多个format可以通过format.combine合并成一个
                    format.timestamp({ format: 'HH:mm:ss YY/MM/DD' }),//info中会有timestamp属性
                    format.colorize({colors: {info: "yellow",error: "blue"}}),//info.level控制台中输出会有颜色
                    format.label({ label: "service" }),//label标签:[service]
                    format.splat(),//%d %s样式插值
                    format.printf(info => { return `${info.timestamp} ${info.level}: [${info.label}]${info.message}` })//输出格式
                ),
                transports: [
                    new winston.transports.Console({ level: 'info' }),//控制台打印
                    new DailyRotateFile({ 
                        dirname: "./logs",//日志输出目录
                        filename: 'application-%DATE%.log', //文件名或"./logsapplication-%DATE%.log"
                        datePattern: 'YYYY-MM-DD',//按天输出文件
                        zippedArchive: true, //对归档日志文件进行gzip压缩
                        maxSize: '20m', //文件最大大小,kb、mb和gb,必须使用缩写k,b,g,大于20m删除
                        maxFiles: '14d', //保留的最大日志数14天
                        frequency: "1m",//分割频率,daily、test、custom或者以m和h结尾的字符串 
                ],
            }),
        ],
    })
    export class AppModule { }
    
    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
  • 使用log

    //1.model中providers
    import { Logger} from '@nestjs/common';
    @Module({
      providers: [Logger],
    })
    export class AppModule {}
    //2.依赖注入
    import { Controller, Logger } from '@nestjs/common';
    @Controller('cats')
    export class CatsController {
      constructor(private readonly logger: Logger) {}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • 参考


# 构建

cnpm install rimraf --save-dev //需要rimraf依赖
npm run build
1
2

# 配置静态资源 (opens new window)

//main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express'; //使用express平台
import { join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
    const app = await NestFactory.create<NestExpressApplication>(AppModule);
    // 1.app.useStaticAssets('public');//public目录放静态资源 http://localhost:3000/1.jpg访问
    // 2.app.useStaticAssets('public', {
    //   //配置虚拟目录
    //   prefix: '/static/', //http://localhost:3000/static/1.jpg
    // });
    app.useStaticAssets(join(__dirname, '..', 'public'),{
        prefix: '/static/',  //http://localhost:3000/static/1.jpg访问
    });
    await app.listen(3000);
}
bootstrap();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 模板渲染

//npm install --save ejs
//man.ts
//配置模板引擎
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setBaseViewsDir("views")//模板位置views文件夹
  app.setViewEngine("ejs")//设置模板引擎
  await app.listen(3000);
}
bootstrap();
1
2
3
4
5
6
7
8
9
10
11
12
//访问
import { Controller, Get,Render,Response } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}
    @Get()
    @Render("default/index.ejs")  //模板位置views/default/index.ejs,使用Render渲染模板
    getHello(@Response() res) {
        // res.redirect("/index") //Response路由跳转
        return {name:"wangcai",age:18};//模板使用的数据
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--模板内容:index.ejs-->
<!----服务端渲染有利于seo,无法查看网页源码----->
<!----客户端渲染用户体验好,异步刷新----->
<body>
    <p>模板引擎</p>
    <!--模板中使用数据-->
    姓名:
    <%=name%>
    年龄:
    <%=age%>
</body>
1
2
3
4
5
6
7
8
9
10
11
<!--news.ejs-->
  <h1>新闻列表</h1>
  <ul>
      <%for (let index = 0; index < newsList.length; index++) {%>
      <li>
          <%= newsList[index]%>
      </li>
    <%} %>
  </ul>
1
2
3
4
5
6
7
8
9

ejs语法 (opens new window)

1.<%=变量%>

2.<%js语句%>


# SSR

//响应vue实例
import { Controller, Get } from '@nestjs/common';
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'

@Controller('vue')
export class VueController {
    @Get()
    async findAll() {
        const vueApp = createSSRApp({
            data() { return {  user: 'John Doe',  } },
            template: `<div>Current user is: {{ user }} </div>`
        })
        const appContent = await renderToString(vueApp)
        const html = `
<html>
<head>
<link rel="icon" type="image/x-icon" href="http://192.9.200.187:53201/favicon.ico">
<script src="https://cdn.jsdelivr.net/npm/pace-js@latest/pace.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pace-js@latest/pace-theme-default.min.css">
</head>
<body>
<div id="app">${appContent}</div>
</body>
</html>
`
        return html;
    }
}
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

# pm2

//package.json
"pm2": "pm2 start ecosystem.config.js"
//ecosystem.config.js
module.exports = {
  apps: [{
    name: "portal-server",
    script: "./dist/main.js",
    watch: ["src", "test"],
    ignore_watch: ["node_modules", "logs", "public"],
  }]
}
1
2
3
4
5
6
7
8
9
10
11

# 工具

  • altair-express-middleware [altair-express-middleware](altair-express-middleware - npm (npmjs.com) (opens new window))

    //main.ts
    import { altairExpress } from 'altair-express-middleware';
    app.use('/altair', altairExpress({
        endpointURL: '/graphql',
        initialSettings: { language: "zh-CN" },
        initialHeaders: { "Authorization": "Bearer " },
    }));//使用altair插件
    
    1
    2
    3
    4
    5
    6
    7
  • apollo studio

     //apollo sandbox替换playground
    import { ApolloServerPluginLandingPageLocalDefault } from 'apollo-server-core';
    @Module({
      imports: [
        GraphQLModule.forRoot({
          playground: false,
          plugins: [ApolloServerPluginLandingPageLocalDefault({ footer: false })],
        }),
      ],
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • raphql-playground graphql-playground

    @Module({
        imports: [
            GraphQLModule.forRoot({
                playground: true,//boolean | ApolloServerPluginLandingPageGraphQLPlaygroundOptions
            }),
        ],
    })
    
    1
    2
    3
    4
    5
    6
    7

# 参考

nest:

数据库:


# note

  • //数据库实体位置
    TypeOrmModule.forRoot({entities: ["dist/**/*.entity{.ts,.js}"])}
    //改变实体后npm run build更新dist内容生效 
    //静态全局路径(例如 dist/**/*.entity{ .ts,.js} )不适用于 Webpack 热重载。
    
    1
    2
    3
    4
  • //Mutation、Query返回对象
    @Mutation((returns) => Return)
    async update(@Args('id') id: number, @Args('user') user: UserDto) {
        const msg="修改成功"
        return {msg}
    }
    
    1
    2
    3
    4
    5
    6
  • //字符集
    TypeOrmModule.forRoot({
        charset:"utf8"
    })
    @Column({ length: 100, comment: '姓名',charset:"utf8" })
    name: string;
    
    1
    2
    3
    4
    5
    6
  • //[Object: null prototype]
    @Mutation((returns) => Return)
    async update(@Args("user") user: UserDto,) {
        console.log(user)// [Object: null prototype] { name: 'xxx' }
        console.log(JSON.parse(JSON.stringify(user)))//{ name: 'xxx' }
        return await this.userService.update(UserDto);
    }
    //该对象没有原型,没有继承属性和方法
    
    1
    2
    3
    4
    5
    6
    7
    8
  • //Maximum call stack size 与node14不兼容
    //https://stackoverflow.com/questions/59620803/createreadstream-throwing-rangeerror-maximum-call-stack-size-exceeded-when-up/67083012#67083012
    //apollo-server-express@2.x.x的GraphQLUpload版本为@11.0.0
    //1.添加package.json
    "resolutions": {
        "fs-capacitor": "^6.2.0",
        "graphql-upload": "^11.0.0"
    }
    //2.添加scripts
    "scripts": {
        ....
        "preinstall": "npx npm-force-resolutions"
    }
    //3.npm i
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • //Multipart: Boundary not found
    //deal:不设置  xhr.setRequestHeader("Content-Type", "multipart/form-data");
    //跨域
    //deal:xhr.withCredentials = false;
    
    1
    2
    3
    4
  • //操作系统的文件夹中复制图片,dataTransfer对象中是获取不到,正如web页面不能直接操作系统文件,因此无法上传。
    
    1
    从文件系统复制图片 mac os能拿到最后一张;windows 拿不到数据
    截图 mac 和 windows 能获取
    在浏览器界面复制 mac 和 windows 能获取
  • //上传文件太大导致程序崩溃 
    //"apollo-server-express": "^2.8.0"下"graphql-upload": "^8.0.2",
    //"@nestjs/graphql": "^7.3.11"
    GraphQLModule.forRoot({uploads: {
        maxFileSize: 2000000, // 2 MB
        maxFiles: 2
    }})
    //File truncated as it exceeds the byte size limit.
    
    1
    2
    3
    4
    5
    6
    7
    8
  • //POST body missing, invalid Content-Type, or JSON object has no keys.
    //deal 
    //Apollo Server 3 不包含与graphql-uploadApollo Server 2 中类似的内置集成。
    //使用apollo-server-express3必须要使用graphqlUploadExpress
    
    1
    2
    3
    4
  • // 浏览器发送请求入参、路由相同不发送请求,走缓存(协商缓存)
    //响应头:ETag: W/"54-mqxyYyfLHU3ApgUiTHzCTYMTnEU"
    
    1
    2
Last Updated: 2023/9/1 10:22:59