从零开始实现一个 NestJS - 实现提供者

本系列的相关代码存放于 InkSha/expressive: 一个简易的仿造 Nest.js 的 NodeJS 后台框架。

@Injectable()
class AppService {
  private _count: number = 0

  constructor() {
    console.log('service init')
  }
  public async login(name: string, pwd: string) {
    if (name === 'error') {
      return this.notFound()
    }
    return (name === 'test' && pwd === '123')
  }

  public count() {
    return this._count++
  }

  public notFound() {
    throw new NotFoundError('Not Found User!')
  }
}

实现注入

这里的注入很简单,只是使用一个装饰器表明这个被装饰者可以被注入。

export type Injectable = () => ClassDecorator
export const Injectable: Injectable = () => target => {
  Reflect.defineMetadata(TokenConfig.Injectable, true, target)
}

新建一个 UserService 类,定义一个 login 方法,将 UserController 中的判断逻辑移过来。

@Injectable()
export class UserService {
  async login(name: string, pwd: string) {
    return name === 'admin' && pwd === '123456'
  }
}

接着在 UserController 中使用它。

@Controller('user')
export class UserController {

  constructor(
    // 记得 UserService 定义要在 UserController 之前
    // 否则会因为在 UserService 定义前访问它而报错
    private readonly service: UserService
  ) {}

  @Get('login')
  @NotFound()
  async login(@Param('name') name: string, @Param('pwd') pwd: string) {
    return await this.service.login(name, pwd).then(login => ({ login }))
  }
}

此时直接运行是可以正常运行程序的。但是当访问到 login 接口时,就会报错。因为我们没有将 UserService 实例化传入。

注入实例

改造 AppFactory

// 构造函数类型 表示类型可被 new 实例化
type Constructor = new (...args: any) => any
// 这是一个模块配置类型
type ModuleConfig = {
  // 路由控制器列表
  controllers: Constructor[]
  // 服务提供者列表
  provuders: Constructor[]
}

// 修改函数参数
// 由原先的控制器类变更为模块配置
function AppFactory(module: ModuleConfig) {
  const app = express()
  const router = express.Router()

  // 新增一个 实体表
  // 这个表将用于存储控制器,提供者的实例。
  const entitys = new Map<Constructor, Constructor>()

  // 新增函数 toEntity
  // 接收 proto 构造函数 和 providers 提供者列表
  const toEntity = (proto: Constructor, providers: Constructor[] = []) => {
    // 如果已经实例化就直接返回实例
    if (entitys.has(proto)) {
      return entitys.get(proto)
    } else {
      // 否则进行实例化
      // 初始化一个参数列表
      const params = []

      try {
        // 通过元数据 design:paramtypes 标签提取出构造函数参数
        const args = Reflect.getMetadata('design:paramtypes', proto) as Constructor[]
        // 遍历参数将其逐个实例化
        params.push(...args.map(constructor => toEntity(constructor, providers)))
      }
      catch {}
      // 开始实例化
      const entity = new proto(...params)
      // 保存实例
      entitys.set(proto, entity)

      return entity
    }
  }

  for (const Controller of module.controllers) {
    // 实例化控制器
    const controller = toEntity(Controller, module.provuders)
    // 获取路由前缀
    const prefix = Reflect.getMetadata(TokenConfig.Controller, Controller)

    Object
      .getOwnPropertyNames(Controller.prototype)
      .filter(name => name !== 'constructor')
      .map(method => {
        const fn = controller[method]
        const url = Reflect.getMetadata(TokenConfig.Router, fn) as string
        const requestType = Reflect.getMetadata(TokenConfig.RouterMethod, fn) as HttpMethod
        const params = Reflect.getMetadata(TokenConfig.Params, fn)
        const statusCode = Reflect.getMetadata(TokenConfig.HttpStatus, fn)

        router[requestType]([prefix, url].join('/'), (req, res) => {
          if (statusCode) res.status(statusCode)

          new Promise(async (resolve, reject) => {
            const p = []

            if (params) {
              for (const { type, index, prototype } of params) {
                if (type === 'params') {
                  p[index] = prototype ? req.query[prototype] : req.query
                }
              }
            }

            const result = fn.call(controller, ...p)
            if (result instanceof Promise) {
              resolve(await result)
            } else {
              resolve(result)
            }
          })
            .then(data => {
              res.send(data)
            })
        })

        return {
          url,
          params,
          statusCode,
          requestType,
        }
      })

    app.use(router)
  }

  app.listen(3000, () => {
    console.log('run')
  })
}

AppFactory({
  controllers: [UserController],
  provuders: [UserService]
})

错误处理

有时,我们在调用服务时可能会出现一些情况,比如用户没找到,登录密码错误等等。

一般的办法是中止调用,直接返回一个结果。这个结果包含有一些信息,比如未找到用户,代码xx等。

我们可以通过抛出一个错误来处理这些问题。

// 定义了一个抽象类
// 用于规范后续子类
export abstract class CustomError extends Error {
  public readonly code: HttpStatusCode
  public readonly name: string

  constructor(message: string) {
    super(message)
  }
}

// 此处实现了一个简单的 404 Not Found 错误
export class NotFoundError extends CustomError {
  public code = HttpStatusCode.NotFound
  public name = 'NotFound'

  constructor(message: string) {
    super(message)
  }
}

改造 UserService

@Injectable()
export class UserService {
  async login(name: string, pwd: string) {
    // 这里将判断 name 是否是 admin 否则将抛出 404
    if (name !== 'admin') throw new NotFoundError(`Not Found ${name}`)
    return name === 'admin' && pwd === '123456'
  }
}

改造 AppFactory

// ...
// 使用 try ... catch 将 调用路由方法 包裹
// 然后在这里通过判断错误原型来检查是否属于我们的自定义错误
try {
  const result = fn.call(controller, ...p)
  if (result instanceof Promise) {
    resolve(await result)
  } else {
    resolve(result)
  }
}
catch (e: unknown) {
  if (e instanceof CustomError) {
    res.status(e.code)
    resolve({
      code: e.code,
      msg: e.message,
      name: e.name
    })
  }
}
// ...

访问 http://localhost:3000/user/login?name=test&pwd=123 即可查看效果。


从零开始实现一个 NestJS - 实现提供者
http://www.inksha.com/archives/cong-ling-kai-shi-shi-xian-yi-ge-nestjs---shi-xian-ti-gong-zhe
作者
inksha
发布于
2025年02月27日
许可协议