使用 TypeScript 开发后端应用

大多数人接触 TypeScript 开发会遇到的一个问题就是很多原先使用的库都是用 JavaScript 写的. 虽然在 TypeScript 项目中也可以使用他们, 但是有些库的用法还是面向过程形式的, 就和我们在 TypeScript 中用面向对象的写法有点格格不入. 有些库由于比较小众可能不存在声明定义文件, 这就导致了编辑器没法进行静态类型检查.

使用 TypeScript 应该抛弃原型链, 尽可能多的使用面向对象的方式来编程. 尽可能多的使用装饰器. 如果这么做显然就和我们传统的使用 JavaScript 编程有了差异. 那么, 怎样优雅的使用 TypeScript 呢?

TypeScript 生态不乏很多优秀的库, 例如关系型数据库 TypeORM, 依赖注入 inverisifyJS 和 TypeDI, 同时支持 Koa 和 Express 的 routing-controller, 参数校验 class-validator.

如下同时使用 routing-controller 和 class-validator.

// src/controller/ProblemController.ts
import { ProblemService } from 'app/service'
import { Problem } from 'app/entity'
import { Get, JsonController, Param, QueryParams } from 'routing-controllers'
import { Inject, Service } from 'typedi'

export class ProblemQuery { // 这是一个类, 也可以当接口使用, 这里使用的装饰器在下面的 QueryParams 中会被使用
  @IsNumberString() public limit: string = '20'
  @IsNumberString() public offset: string = '0'
  @IsIn(['ASC', 'DESC', 'asc', 'desc']) public order: 'ASC' | 'DESC' = 'ASC'
  @IsString() public sortby: string = 'id'
}

@Service()                          // 用于进行依赖注入
@JsonController('/v1/problems')     // 定义一个路由, 基址为 /v1/problems
export class ProblemsController {

  @Inject()
  private problemService: ProblemService    // 依赖注入 ProblemService

  @Get('/')                         // 匹配 Get /v1/problems 的请求
  public async index (@QueryParams() query: ProblemQuery): Promise<{ data: [Problem[], number]; }> {    // @QueryParams 也是依赖注入, 同时这里结合 class-validator 进行校验
    const data = await this.problemService.list(query.offset, query.limit, query.sortby, query.order)
    return {
      data
    }
  }
}

在这个文件中, 路由和控制器结合在一起, 非常简介. 如果我们要使用中间件的话, 也只需要再 @Get('/') 下面或者 @JsonController('/v1/problems') 下面再加上一个 @UseBefore(middleware) 就好了. 如果需要对某个路由进行限流, 也只是一个装饰器就搞定的事情.

上述的 ProblemService 文件如下:

// src/service/ProblemService.ts
import { Problem } from 'app/entity'
import { ProblemRepository } from 'app/repository'
import { Service } from 'typedi'
import { OrmCustomRepository } from 'typeorm-typedi-extensions'

@Service()  // 这里 ProblemService 可以被依赖注入的关键
export class ProblemService {

  @OrmCustomRepository(ProblemRepository)   // 同样可以依赖注入其他服务
  private problemRepository: ProblemRepository

  public async list (offset: number, limit: number, sortby: string, order: 'ASC' | 'DESC'): Promise<[Problem[], number]> {
    return this.problemRepository.getList(offset, limit, sortby, order)
  }
}

这个文件是一个 Service, 一般在 Service 里面, 我们会调用多种服务例如数据库增删查改, 各种外部 API 调用, 缓存处理等等. 当业务比较复杂时, 一般会把数据库这一层再进行一层抽象, 也就是有了 ProblemRepository 这个文件:

// src/problem/ProblemRepository.ts
import { Problem } from 'app/entity'
import 'reflect-metadata'
import { Service } from 'typedi'
import { EntityRepository, Repository } from 'typeorm'

@Service()
@EntityRepository(Problem)
export class ProblemRepository extends Repository<Problem> {

  public getList (offset: number, limit: number, sortby: string, order: string): Promise<[Problem[], number]> {
    return this.createQueryBuilder('problem')
               .orderBy(`problem.${sortby}`, order as 'ASC'|'DESC')
               .innerJoinAndSelect('problem.user', 'user')
               .setFirstResult(offset)
               .setMaxResults(limit)
               .getManyAndCount()
    }
}

这里就是使用 TypeORM 和数据库交互的代码了. Repository 这层在 Java 中用的很多. 如果不是一般的 CURD 系统的话建议都是加上这一层的.

在下一层就是 Entity 了, 也就是 Model.

import { Submission, User } from 'app/entity'
import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm'

@Entity()
export class Problem {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  title: string

  @OneToOne(type => User)
  @JoinColumn()
  user: User

  @OneToMany(type => Submission, submission => submission.problem)
  submissions: Submission[]
}

TypeORM 用装饰器来定义数据库模型, 这种写法非常清晰简洁, 个人非常喜欢这种写法.

当然你也可以继续用 Sequelize, 可以辅助使用 typescript-sequelize 这个库来像上面这样定义 Model. 使用 typescript-sequelize 定义的 Model 不需要单独写接口, 就好像上面 TypeORM 定义一样. 两者我都研究过, TypeORM 的静态类型检查强过 Sequelize 并且我不需要额外引入一个库来像上面这样类似的定义 Model 因此我选择了 TypeORM.

以上四个例子 controller -> service -> repository -> entity. 这应该是一个能够适应一定规模的分层结构.

可以看到无处不是装饰器和依赖注入, 非常简洁.

对于非关系型数据库例如 mongodb 目前我没有看到比较好的 ORM/ODM. TypeORM 的作者写了一个 TypeODM 但是作者说只有 TypeORM 稳定后才会去开发, 现在 TypeODM 功能还很不齐全.

我也在公司项目中使用了 mongoose + typescript 的组合进行开发. 没有使用第三方扩展, 直接用 mongoose 的后果是, 我定义 model 之后还得再单独写一个接口. 这就显得有点累赘. 希望后面有一些更好的解决方案出来.

标签: JavaScript, Node.js, TypeScript

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

添加新评论

This page loaded in 0.001135 seconds