最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 做了一个Nest.js上手项目,很丑,但适合练手和收藏

    正文概述 掘金(写代码的海怪)   2021-08-22   499

    前言

    最近爱了上 Nest.js 这个框架,边学边做了一个 nest-todo 这个项目。

    做了一个Nest.js上手项目,很丑,但适合练手和收藏

    没错,就是一个 UI 很丑陋的 Todo List App。不知道为啥,慢慢开始喜欢上这种原始风味的 UI 样式了,不写 CSS 也挺好看的。

    虽然皮肤很丑,但是项目里面包含了大量 Nest.js 文档里的知识点(除了 GraphQL 和微服务,这部分平常用得不多就不瞎整了),能实现的点我基本都想个需求实现了:

    做了一个Nest.js上手项目,很丑,但适合练手和收藏

    为什么

    为什么要做这个项目呢?市面上的文章和博客看了不少,很多都浅尝辄止,写个 CRUD 就完事了,也太 easy 了,一行 nest g resource 就搞定。所以,就想实现一个 大而全 的 Nest.js 的 Demo 出来。

    除此之外,这个 Demo 还能给很多要马上上手的前端一个示范。虽然 Nest.js 文档也齐全,但是如果你稍微做重一点的业务,它就有点顶不住了,很多东西都要 。那这个时候 nest-todo 就可以站出来说:“不会就抄我吧,我肯定能 Work”。

    前端

    前端部分主要使用 React 来实现,仅有 0.0000001% 的样式,几乎都是 JS 逻辑,且有 100% TypeScript 类型提示,可大胆学习观看。

    由于本项目以后端为主,所以前端也只有这些东西:

    做了一个Nest.js上手项目,很丑,但适合练手和收藏

    后端

    后端内容则比较多了,主要就是主角 Nest.js,以及非常多的模块:

    做了一个Nest.js上手项目,很丑,但适合练手和收藏

    下面例举几个我觉得比较重要的模块来说说吧,当然下面都是一些代码片段,想了解更具体的实现,可以到 Github 的 nest-todo 查看。

    Todo 模块

    最基础的增、删、改、查。相信很多人在一些博客或文章都见过这样的写法。

    TodoController 负责路由实现:

    @ApiTags('待办事项')
    @ApiBearerAuth()
    @Controller('todo')
    export class TodoController {
      constructor(private readonly todoService: TodoService) {}
    
      @Post()
      async create(
        @Request() request,
        @Body() createTodoDto: CreateTodoDto,
      ): Promise<Todo> {
        return this.todoService.create(request.user.id, createTodoDto);
      }
    
      @Get()
      async findAll(@Request() request): Promise<Todo[]> {
        const { id, is_admin } = request.user;
    
        if (is_admin === 1) {
          return this.todoService.findAll();
        } else {
          return this.todoService.findAllByUserId(id);
        }
      }
    
      @Get(':id')
      async findOne(@Param('id', ParseIntPipe) id: number): Promise<Todo> {
        return this.todoService.findOne(id);
      }
    
      @Patch(':id')
      async update(
        @Param('id', ParseIntPipe) id: number,
        @Body() updateTodoDto: UpdateTodoDto,
      ) {
        await this.todoService.update(id, updateTodoDto);
        return updateTodoDto;
      }
    
      @Delete(':id')
      async remove(@Param('id', ParseIntPipe) id: number) {
        await this.todoService.remove(id);
        return { id };
      }
    }
    

    TodoService 则实现更底层的业务逻辑,这里则是要从数据库增、删、改、查:

    @Injectable()
    export class TodoService {
      constructor(
        private todoRepository: TodoRepository,
        private userRepository: UserRepository,
      ) {}
    
      async create(userId: number, createTodoDto: CreateTodoDto): Promise<Todo> {
        const user = await this.userRepository.findOne(userId);
        const { title, description, media } = createTodoDto;
    
        const todo = new Todo();
        todo.title = title;
        todo.description = description;
        todo.status = createTodoDto.status || TodoStatus.TODO;
        todo.media = media;
        todo.author = user;
    
        return this.todoRepository.save(todo);
      }
    
      async findAll(): Promise<Todo[]> {
        return this.todoRepository.find();
      }
    
      async findAllByUserId(userId: number): Promise<Todo[]> {
        const user = await this.userRepository.findOne({
          relations: ['todos'],
          where: { id: userId },
        });
    
        return user ? user.todos : [];
      }
    
      async findOne(id: number): Promise<Todo> {
        return this.todoRepository.findOne(id);
      }
    
      async update(id: number, updateTodoDto: UpdateTodoDto) {
        const { title, description, status, media } = updateTodoDto;
    
        return this.todoRepository.update(id, {
          title,
          description,
          status: status || TodoStatus.TODO,
          media: media || '',
        });
      }
    
      async remove(id: number) {
        return this.todoRepository.delete({
          id,
        });
      }
    }
    

    可惜的是,这些文章和博客到此就结束了,可能作者看到这里也不想再继续搞下去了。不过,我并不打算到此结束,这才刚开始呢。

    数据库模块

    上面的 TodoService 里用到了数据库,那就来聊聊数据库模块。我这里的选型是 TypeORM + mariadb,为啥不用 mysql 呢?因为我用 M1 的 Mac,装不了 mysql 这个镜像,非常蛋疼。

    要使用 TypeORM,就需要在 AppModule 上添加这个配置,然而,明文写配置是个沙雕做法,更好的实现应该用 Nest.js 提供的 ConfigModule 来读取配置。

    读取配置这里使用 读取 .env 文件” 实现:

    const loadConfig = () => {
      const { env } = process;
    
      return {
        db: {
          database: env.TYPEORM_DATABASE,
          host: env.TYPEORM_HOST,
          port: parseInt(env.TYPEORM_PORT, 10) || 3306,
          username: env.TYPEORM_USERNAME,
          password: env.TYPEORM_PASSWORD,
        },
        redis: {
          host: env.REDIS_HOST,
          port: parseInt(env.REDIS_PORT) || 6379,
        },
      };
    };
    

    然后再在 AppModule 使用 ConfigModuleTypeORMModule

    const libModules = [
      ConfigModule.forRoot({
        load: [loadConfig],
        envFilePath: [DOCKER_ENV ? '.docker.env' : '.env'],
      }),
      ScheduleModule.forRoot(),
      TypeOrmModule.forRootAsync({
        imports: [ConfigModule],
        inject: [ConfigService],
        useFactory: (configService: ConfigService) => {
          const { host, port, username, password, database } =
            configService.get('db');
    
          return {
            type: 'mariadb',
            // .env 获取
            host,
            port,
            username,
            password,
            database,
            // entities
            entities: ['dist/**/*.entity{.ts,.js}'],
          };
        },
      }),
    ];
    
    @Module({
      imports: [...libModules, ...businessModules],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
    

    最后一步,在 Todo 业务模块里注入数据表对应的 Repository,这里一来 TodoService 就可以用 Repository 来操作数据库表了:

    @Module({
      imports: [
        TypeOrmModule.forFeature([TodoRepository, UserRepository]),
        UserModule,
      ],
      controllers: [TodoController],
      providers: [TodoService],
    })
    export class TodoModule {}
    

    数据库模块还没完...

    除了连接数据库,数据库的迁移与初始化是很多人经常忽略的点。

    先说初始化,非常简单,就是一个脚本的事:

    const checkExist = async (userRepository: Repository<User>) => {
      console.log('检查是否已初始化...');
    
      const userNum = await userRepository.count();
      const exist = userNum > 0;
    
      if (exist) {
        console.log(`已存在 ${userNum} 条用户数据,不再初始化。`);
        return true;
      }
    
      return false;
    };
    
    const seed = async () => {
      console.log('开始插入数据...');
      const connection = await createConnection(ormConfig);
    
      const userRepository = connection.getRepository<User>(User);
    
      const dataExist = await checkExist(userRepository);
    
      if (dataExist) {
        return;
      }
    
      const initUsers = getInitUsers();
    
      console.log('生成初始化数据...');
      initUsers.forEach((user) => {
        user.todos = lodash.range(3).map(getRandomTodo);
      });
    
      const users = lodash.range(10).map(() => {
        const todos = lodash.range(3).map(getRandomTodo);
        return getRandomUser(todos);
      });
    
      const allUsers = [...initUsers, ...users];
    
      console.log('插入初始化数据...');
      await userRepository.save(allUsers);
    
      console.log('数据初始化成功!');
    };
    
    seed()
      .then(() => process.exit(0))
      .catch((e) => {
        console.error(e);
        process.exit(1);
      });
    

    当然,最好也提供重置数据库的能力:

    const reset = async () => {
      const connection = await createConnection(ormConfig);
    
      await connection.createQueryBuilder().delete().from(Todo).execute();
      await connection.createQueryBuilder().delete().from(User).execute();
    };
    
    reset()
      .then(() => process.exit(0))
      .catch((e) => {
        console.error(e);
        process.exit(1);
      });
    

    这样一来,小白上手完全不慌。只要改坏数据库,一个 reset + seed 的操作,数据库又回来的了。当然,这一步仅仅是针对 数据 来说的。

    针对数据库表结构则需要 数据库迁移。令人激动的是 TypeORM 已经提供了一条非常 NB 的迁移命令:

    // package.json
    "db:seed": "ts-node scripts/db/seed.ts",
    "db:reset": "ts-node scripts/db/reset.ts",
    "migration:generate": "npm run build && npm run typeorm migration:generate -- -n",
    "migration:run": "npm run build && npm run typeorm migration:run"
    

    但是,TypeORM 是从哪知道数据表的结构的呢?这就是 Entity 的作用了,下面就是一个 Todo entity:

    @Entity()
    export class Todo {
      @ApiProperty()
      @PrimaryGeneratedColumn()
      id: number; // 自增 id
    
      @ApiProperty()
      @Column({ length: 500 })
      title: string; // 标题
    
      @ApiProperty()
      @Column('text')
      description?: string; // 具体内容
    
      @ApiProperty()
      @Column('int', { default: TodoStatus.TODO })
      status: TodoStatus; // 状态
    
      @ApiProperty({ required: false })
      @Column('text')
      media?: string;
    
      @ManyToOne(() => User, (user) => user.todos)
      author: User;
    }
    

    然后在 .env 里添加配置:

    # Type ORM 专有变量
    # 详情:https://typeorm.io/#/using-ormconfig
    # 生产环境在服务器上的容器里配置
    TYPEORM_CONNECTION=mariadb
    TYPEORM_DATABASE=nest_todo
    TYPEORM_HOST=127.0.0.1
    TYPEORM_PORT=3306
    TYPEORM_USERNAME=root
    TYPEORM_PASSWORD=123456
    TYPEORM_ENTITIES=dist/**/*.entity{.ts,.js}
    TYPEORM_MIGRATIONS=dist/src/db/migrations/*.js
    TYPEORM_MIGRATIONS_DIR=src/db/migrations
    

    有了上面的命令,还有什么数据库我不敢删的?遇事不决 npm run migration:run + npm run db:seed 一下。

    上传模块

    从上面 Demo 可看到,Todo 是支持图片上传的,所以这里还需要提供上传功能。Nest.js 非常给力,直接内置了 multer 这个库:

    @ApiTags('文件上传')
    @ApiBearerAuth()
    @Controller('upload')
    export class UploadController {
      @Post('file')
      @UseInterceptors(FileInterceptor('file'))
      uploadFile(@UploadedFile() file: Express.Multer.File) {
        return {
          file: staticBaseUrl + file.originalname,
        };
      }
    
      @Post('files')
      @UseInterceptors(FileInterceptor('files'))
      uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>) {
        return {
          files: files.map((f) => staticBaseUrl + f.originalname),
        };
      }
    }
    

    当然,必不可少,需要在 UploadModule 里注入模块:

    @Module({
      imports: [
        MulterModule.register({
          storage: diskStorage({
            destination: path.join(__dirname, '../../upload_dist'),
            filename(req, file, cb) {
              cb(null, file.originalname);
            },
          }),
        }),
      ],
      controllers: [UploadController],
      providers: [UploadService],
    })
    export class UploadModule {}
    

    静态资源模块

    首先,必须说明一下上面的上传应该是要上传到 COS 桶或者 CDN 上,而不应该上传到自己服务器,使用自己服务器来管理文件。这里仅为了用一用这个静态资源模块。

    回到主题,上面上传是上传到 /upload_dist 这个文件夹里,那我们静态资源就是要 host 这个文件夹下面的文件:

    const uploadDistDir = join(__dirname, '../../', 'upload_dist');
    
    @Controller('static')
    export class StaticController {
      @SkipJwtAuth()
      @Get(':subPath')
      render(@Param('subPath') subPath, @Res() res) {
        const filePath = join(uploadDistDir, subPath);
        return res.sendFile(filePath);
      }
    }
    
    @Module({
      controllers: [StaticController],
    })
    export class StaticModule {}
    

    Very easy ~ 过

    登录模块

    相信细心的你一定看到上面的 @SkipJwtAuth,这是因为我全局开了 JWT 鉴权,只有请求头带有 Bearer Token 才能访问这个接口,而 @SkipJwtAuth 则表示这个接口不需要 JWT 鉴权。不妨来看看普通的鉴权是怎么实现的。

    首先,你必要熟悉 Passport.js 里的 StrategyverifyCallback 概念,否则咱还是别聊了。这里 Nest.js 将这个 verifyCallback 封装成了 Strategy 里的 validate 方法,当编写 valiate 则是在写 verifyCallback

    @Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy) {
      constructor(
        private moduleRef: ModuleRef,
        private reportLogger: ReportLogger,
      ) {
        super({ passReqToCallback: true });
        this.reportLogger.setContext('LocalStrategy');
      }
    
      async validate(
        request: Request,
        username: string,
        password: string,
      ): Promise<Omit<User, 'password'>> {
        const contextId = ContextIdFactory.getByRequest(request);
    
        // 现在 authService 是一个 request-scoped provider
        const authService = await this.moduleRef.resolve(AuthService, contextId);
    
        const user = await authService.validateUser(username, password);
    
        if (!user) {
          this.reportLogger.error('无法登录,SB');
          throw new UnauthorizedException();
        }
    
        return user;
      }
    }
    

    上面是用 username + password 实现鉴权的一种策略,当然我们正常服务是可以存在多种鉴权策略的,要使用这个策略,需要用到 Guard:

    @Injectable()
    export class LocalAuthGuard extends AuthGuard('local') {}
    

    然后将这个 Guard 放在对应的接口头顶就 O 了:

    @ApiTags('登录验证')
    @Controller('auth')
    export class AuthController {
      constructor(private authService: AuthService) {}
    
      @ApiBody({ type: LoginDto })
      @SkipJwtAuth()
      @UseGuards(LocalAuthGuard)
      @Post('login')
      async login(@Request() req) {
        return this.authService.login(req.user);
      }
    }
    

    和 local 这个 Strategy 相似的,JWT 也有对应的 Strategy

    @Injectable()
    export class JwtStrategy extends PassportStrategy(Strategy) {
      constructor(private userService: UserService) {
        super({
          jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
          ignoreExpiration: false,
          secretOrKey: jwtConstants.secret,
        });
      }
    
      async validate(payload: any) {
        const existUser = this.userService.findOne(payload.sub);
    
        if (!existUser) {
          throw new UnauthorizedException();
        }
    
        return { ...payload, id: payload.sub };
      }
    }
    

    而在 JwtGuard 里,用 canActive 实现了 权限控制

    @Injectable()
    export class JwtAuthGuard extends AuthGuard('jwt') {
      constructor(private reflector: Reflector) {
        super();
      }
    
      canActivate(
        context: ExecutionContext,
      ): boolean | Promise<boolean> | Observable<boolean> {
        // 自定义用户身份验证逻辑
        const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
          context.getHandler(),
          context.getClass(),
        ]);
        // skip
        if (isPublic) return true;
    
        return super.canActivate(context);
      }
    
      handleRequest(err, user) {
        // 处理 info
        if (err || !user) {
          throw err || new UnauthorizedException();
        }
        return user;
      }
    }
    

    格式化输出

    写完接口了,就得格式化输出,我比较喜欢的格式是:

    {
      retcode: 0,
      message: "",
      data: ...
    }
    

    我们更希望不要在 Controller 里重复添加上面的 “格式化” 数据结构。Nest.js 提供了 Interceptor,可以让我们在 数据给前端之前 “加点料”:

    export class TransformInterceptor<T>
      implements NestInterceptor<T, Response<T>>
    {
      intercept(context: ExecutionContext, next: CallHandler<T>) {
        return next.handle().pipe(
          map((data) => ({
            retcode: 0,
            message: 'OK',
            data,
          })),
        );
      }
    }
    

    然后在 main.ts 入口里全局使用:

    app.useGlobalInterceptors(
      new LogInterceptor(reportLogger),
      new TransformInterceptor(),
    );
    

    测试

    写完了一个接口,肯定免不了要写测试。我相信绝大部分人是不会写测试,当然他们自己也是不会写的。

    它不是 “Jest”,也不是 “Cypress”,而是一个可以研究得很深的领域。它难的点并不在于 “写”,而在于 “造”,以及 测试策略

    先来说测试策略吧,请问什么东西应该测?什么东西可以不测?什么东西不应该测?这三问是个人觉得是个玄学问题,没有正确答案,只能根据自己的项目来判断。并不是 100% 的覆盖率就是好的,也要看更新迭代时测试代码的改造成本。

    我先给出这个项目的测试原则:

    • 数据库操作不测,因为这个测试内容 TypeORM 能保证 API 的调用是 OK 的
    • 简单实现不测,比如一个函数只有一行,那还测个 P
    • 我只测一个模块,因为我懒,剩下大家自己看我那个模块的测试就能学会了
    • 我的 测试策略 不一定正确,只能说是我目前想到比较好的 测试策略

    TodoService 进行测试,比较难的点是对 TypeOrmRepository 进行 Mock,这玩意我自己搞了一整天才搞通,相信没人有耐心整这些了:

    const { mockTodos, mockUsers } = createMockDB();
    
    describe('TodoService', () => {
      let mockTodoRepository;
      let mockUserRepository;
      let service: TodoService;
    
      beforeEach(async () => {
        mockUserRepository = new MockUserRepository(mockUsers);
        mockTodoRepository = new MockTodoRepository(mockTodos);
    
        const module: TestingModule = await Test.createTestingModule({
          providers: [
            TodoService,
            {
              provide: TodoRepository,
              useValue: mockTodoRepository,
            },
            {
              provide: UserRepository,
              useValue: mockUserRepository,
            },
          ],
        }).compile();
    
        service = module.get<TodoService>(TodoService);
      });
    
      it('create', async () => {
        expect(service).toBeDefined();
        // 创建一个 todo
        const returnTodos = await service.create(99, {
          title: 'title99',
          description: 'desc99',
          status: TodoStatus.TODO,
        });
        // expect
        expect(returnTodos.title).toEqual('title99');
        expect(returnTodos.description).toEqual('desc99');
        expect(returnTodos.status).toEqual(TodoStatus.TODO);
      });
    
      it('findAll', async () => {
        expect(service).toBeDefined();
        const returnTodos = await service.findAll();
        // expect
        expect(returnTodos).toEqual(mockTodos);
      });
    
      it('findAllByUserId', async () => {
        expect(service).toBeDefined();
        // 直接返回第一个 user
        jest.spyOn(mockUserRepository, 'findOne').mockImplementation(async () => {
          return mockUsers[0];
        });
        // 找到 userId 为 0 的所有 todo
        const returnTodos = await service.findAllByUserId(0);
        const [firstTodo] = returnTodos;
        // expect
        expect(mockUserRepository.findOne).toBeCalled();
        expect(firstTodo.id).toEqual(0);
        expect(firstTodo.title).toEqual('todo1');
        expect(firstTodo.description).toEqual('desc1');
      });
    
      it('findOne', async () => {
        expect(service).toBeDefined();
        // 找到一个 todo
        const returnTodo = await service.findOne(0);
        // expect
        expect(returnTodo.id).toEqual(0);
        expect(returnTodo.title).toEqual('todo1');
        expect(returnTodo.description).toEqual('desc1');
      });
    
      it('update', async () => {
        expect(service).toBeDefined();
        // 所有 todo
        const allTodos = await service.findAll();
        // 更新一个 todo
        await service.update(0, {
          title: 'todo99',
          description: 'desc99',
        });
        // expect
        const targetTodo = allTodos.find((todo) => todo.id === 0);
        expect(targetTodo.id).toEqual(0);
        expect(targetTodo.title).toEqual('todo99');
        expect(targetTodo.description).toEqual('desc99');
      });
    
      it('remote', async () => {
        expect(service).toBeDefined();
        // 删除 todo
        await service.remove(0);
        // 获取所有 todo
        const allTodos = await service.findAll();
        // expect
        expect(allTodos.length).toEqual(1);
        expect(allTodos.find((todo) => todo.id === 0)).toBeUndefined();
      });
    });
    

    TodoController 的单元测试,我觉得这个 class 没什么可测的,因为里面的函数太简单了:

    const { mockTodos, mockUsers } = createMockDB();
    
    describe('TodoController', () => {
      let todoController: TodoController;
      let todoService: TodoService;
      let mockTodoRepository;
      let mockUserRepository;
    
      beforeEach(async () => {
        mockTodoRepository = new MockTodoRepository(mockTodos);
        mockUserRepository = new MockUserRepository(mockUsers);
    
        const app: TestingModule = await Test.createTestingModule({
          controllers: [TodoController],
          providers: [
            TodoService,
            {
              provide: TodoRepository,
              useValue: mockTodoRepository,
            },
            {
              provide: UserRepository,
              useValue: mockUserRepository,
            },
          ],
        }).compile();
    
        todoService = app.get<TodoService>(TodoService);
        todoController = app.get<TodoController>(TodoController);
      });
    
      describe('findAll', () => {
        const [firstTodo] = mockTodos;
    
        it('普通用户只能访问自己的 todo', async () => {
          jest
            .spyOn(todoService, 'findAllByUserId')
            .mockImplementation(async () => {
              return [firstTodo];
            });
          const todos = await todoController.findAll({
            user: { id: 0, is_admin: 0 },
          });
          expect(todos).toEqual([firstTodo]);
        });
        it('管理员能访问所有的 todo', async () => {
          jest.spyOn(todoService, 'findAll').mockImplementation(async () => {
            return mockTodos;
          });
          const todos = await todoController.findAll({
            user: { id: 0, is_admin: 1 },
          });
          expect(todos).toEqual(mockTodos);
        });
      });
    });
    

    最后就是 e2e 的测试,难点在于 Bearer Token 鉴权的获取,这玩意也同样搞了我一天时间:

    describe('TodoController (e2e)', () => {
      const typeOrmModule = TypeOrmModule.forRoot({
        type: 'mariadb',
        database: 'nest_todo',
        username: 'root',
        password: '123456',
        entities: [User, Todo],
      });
      let app: INestApplication;
      let bearerToken: string;
      let createdTodo: Todo;
    
      beforeAll(async (done) => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
          imports: [TodoModule, AuthModule, typeOrmModule],
          providers: [TodoRepository, UserRepository],
        }).compile();
    
        app = moduleFixture.createNestApplication();
        await app.init();
        // 生成测试用户的 token
        request(app.getHttpServer())
          .post('/auth/login')
          .send({ username: 'user', password: 'user' })
          .expect(201)
          .expect((res) => {
            bearerToken = `Bearer ${res.body.token}`;
          })
          .end(done);
      });
    
      it('GET /todo', (done) => {
        return request(app.getHttpServer())
          .get('/todo')
          .set('Authorization', bearerToken)
          .expect(200)
          .expect((res) => {
            expect(typeof res.body).toEqual('object');
            expect(res.body instanceof Array).toBeTruthy();
            expect(res.body.length >= 3).toBeTruthy();
          })
          .end(done);
      });
    
      it('POST /todo', (done) => {
        const newTodo: CreateTodoDto = {
          title: 'todo99',
          description: 'desc99',
          status: TodoStatus.TODO,
          media: '',
        };
    
        return request(app.getHttpServer())
          .post('/todo')
          .set('Authorization', bearerToken)
          .send(newTodo)
          .expect(201)
          .expect((res) => {
            createdTodo = res.body;
            expect(createdTodo.title).toEqual('todo99');
            expect(createdTodo.description).toEqual('desc99');
            expect(createdTodo.status).toEqual(TodoStatus.TODO);
          })
          .end(done);
      });
    
      it('PATCH /todo/:id', (done) => {
        const updatingTodo: UpdateTodoDto = {
          title: 'todo9999',
          description: 'desc9999',
        };
    
        return request(app.getHttpServer())
          .patch(`/todo/${createdTodo.id}`)
          .set('Authorization', bearerToken)
          .send(updatingTodo)
          .expect(200)
          .expect((res) => {
            expect(res.body.title).toEqual(updatingTodo.title);
            expect(res.body.description).toEqual(updatingTodo.description);
          })
          .end(done);
      });
    
      it('DELETE /todo/:id', (done) => {
        return request(app.getHttpServer())
          .delete(`/todo/${createdTodo.id}`)
          .set('Authorization', bearerToken)
          .expect(200)
          .expect((res) => {
            expect(res.body.id).toEqual(createdTodo.id);
          })
          .end(done);
      });
    
      afterAll(async () => {
        await app.close();
      });
    });
    

    Swagger

    Swagger 是一个非常强大的文档工具,可以识别接口的 URL,入参,出参,简直是前端使用者的福音:

    做了一个Nest.js上手项目,很丑,但适合练手和收藏

    首先在 main.ts 里接入 Swagger

    const setupSwagger = (app) => {
      const config = new DocumentBuilder()
        .addBearerAuth()
        .setTitle('待办事项')
        .setDescription('nest-todo 的 API 文档')
        .setVersion('1.0')
        .build();
      const document = SwaggerModule.createDocument(app, config);
      SwaggerModule.setup('docs', app, document, {
        swaggerOptions: {
          persistAuthorization: true,
        },
      });
    };
    

    然后在 nest-cli.json 里也接入 Swagger插件,这样才能自动识别,不然就要一个 ApiProperty 一个 ApiProperty 去声明了:

    {
      "collection": "@nestjs/schematics",
      "sourceRoot": "src",
      "compilerOptions": {
        "plugins": ["@nestjs/swagger"]
      }
    }
    

    最后

    还有非常多的模块没讲,我觉得那些并不是那么重要,只要看过文档就会了。上面的模块我是踩了很多坑才实现出来的,中间走走停停花了大概 1 个月左右的时间。

    本来是可以上线给大家一个在线 Demo 看的,但是我的域名还在备案,大家先本地 Clone 玩吧。

    如果你对 Nest.js 也感兴趣,也想学一下它,不妨 Clone 一下我的 nest-todo 这个项目,抄抄改改学一下吧。


    起源地下载网 » 做了一个Nest.js上手项目,很丑,但适合练手和收藏

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元