typescript运行时应用初探:设计一个基于类型声明的前端请求层框架

请求层框架是什么?

其实 axios 就可以看成一个请求层框架,只不过它将关注焦点放在了 ajax request 上,更多时候大家把它当成一个库。

那么这里的”框架“指的是什么呢?一般来说,实际项目中发ajax请求,不仅仅是发出去这么简单,一方面数据需要 transform、polyfill 等一系列操作,另一方面请求发出去的过程中可能需要触发一些副作用,比如打点统计等等,最后,在跨端场景下,最终把请求发出去的backend,可能有多种。比如在electron项目中,假设我们使用UDP协议发送请求,但是为了支持外网向内网推数据,又没有打洞服务的情况,我们需要在某个版本中支持外网 TCP server -> 内网 TCP client 的模式。

通过一个图,大家就可以理解为什么请求层值得被称为一个框架:

这么一看功能还不少对吧?而且是典型的中间件模式(绿色的全是中间件),说是框架不过分吧?

如何实现一个请求层框架?

实现这样一个框架,我们主要要解决两个问题:

  • 设计一个中间件架构的请求层runtime。
  • 定义接口声明的流程和语法。

请求层runtime

为什么说 axios 本身就可以看成一个请求层框架呢?因为它支持 transformRequest 和 transformResponse,这个其实就是中间件接口,当然你也可以理解为hook,总而言之,你可以把各种中间环节通过这俩接口塞到 axios 里面去。

为了实现更丰富的功能以及更高水平的自定义,我们可以部分基于 axios,自己实现一套请求层,接口定义类似下面这样:

import api from 'api-framework'
import { validateReq, validateResp, transformResp, ajax } from 'my-middlewares'

const context = api.createContext(validateReq, ajax, transformResp, validateResp)

function validateGetFoobarListReq (value) {
  if (!value || typeof value.type !== 'string') {
    return false
  }

  return true
}

function validateGetFoobarListResp (value) {
  if (!value || !Array.isArray(value.content)) {
    return false
  }

  return true
}

export const getFoobarList = context.create({
  url: '/foobarlist',
  {
    validateReq: validateGetFoobarListReq,
    validateResp: validateGetFoobarListResp
  }
})

接口声明

看到上面的代码,你一定不会去使用这个框架,因为语法太蛋疼了。

这里的关键问题是,”接口声明“从哪里来?

要么生成,要么手写。

生成接口声明,需要后端支持,比如 swagger 就有这个功能,它要求后端在编写接口之前,先编写接口声明,swagger 会根据声明生成各种前端代码。这样做的优势在于,接口声明处于中立位置,与具体的技术方案没有关系,方便我们未来对接口声明做各种处理,深度使用。劣势在于,它相当于重塑了开发流程,有可能产生现实阻力。

手写接口声明,核心问题是如何让语法优雅而简练,真正达到声明,而不是编程的体验。

目前业界有没有完整的开源接口声明方案(经评论提醒,pont是一个很好的方案),我还没有调研清楚,但是针对单个功能点,尤其是和出参入参有关的(其实主要就是validator),其实有很多工具可以用,比如:

joi​www.npmjs.com图标ajv​www.npmjs.com图标io-ts​www.npmjs.com图标

这些工具都能帮助你实现接口声明,但是使用它们的体验,仍然是编程体验。我们希望事情变得更简单,于是,typescript该出场了。

typescript 运行时应用

解释一下什么是”运行时“应用——ts原本是在构建期起作用的,在代码编译过程中,type declare信息会被擦除掉,最终生成js代码。而”运行时“应用,就是说我们希望通过某种方式,把类型信息带入运行时,让它发挥一些作用,而本篇文章介绍的请求层框架,就是我们的一种尝试。

其实这个框架的runtime,原本就存在,但是就像我上面所说的,接口语法不够友好,以至于大家根本不想用。然后我有个同事,很厉害,他自己在项目里面实现了一个自定义的webpack-loader,能把ts声明的接口,转换成请求层调用代码。

他的loader和原本的runtime一结合,就是一个完整的请求层框架了。

它的接口声明语法是类似这样的:

type Query = {
  /**
   * 类型
   */
  type: string

  /**
   * 页码
   * @range [1, 100)
   */
  page: number

  /**
   * 最大条数
   * @range [1, 100)
   */
  limit: number

  /**
   * 关键词
   */
  keyword: string
}

type QueryResult = {
  // ...
}

export interface Foobar {
  /**
   * @url /foobar/query
   * @pipe validate | ajax | transform | validate
   */
  query (data: Query) : Promise<QueryResult>
}

你只要声明接口和参数类型,整个请求层的管道连接,以及数据校验的实现代码,loader都可以给你生成。如果自动生成的validator不和你的要求,你还可以自己编写,loader会根据类型声明,自动查找你的validator。

有这么个神器在,框架的实用性产生了质的飞跃。因为接口声明是ts,所以我们在VSCode里面调用接口的时候,有比较完善的智能提醒,这一点比使用json等静态格式声明接口,要舒服的多。

其实框架本身,并不重要,写这篇文章,主要是想跟大家展示一下ts类型信息在运行时发挥作用的潜力。我19年给自己立的一个目标,就是探索出一套系统方法,把ts类型信息带到业务中去,让其发挥作用。

然而在这个领域,我已经落后了,写自定义loader的同事,真是干得漂亮。

猜你喜欢

转载自blog.csdn.net/XZQ121963/article/details/88690374