利用泛型+类型推导定义伪GraphQL模型

接触过前端的应该都有听过GraphQL

简单来说就是前端自行定义接口所需要返回的数据, 想要尝试的可以试着调用GithubAPI V4.

而对于我们常用的xhr请求能否也做到跟GraphQL一样能自定义接口返回的数据?

答案是可以, 但是提前必须是后端必须提供足够的数据让前端自行选择.

例子

假设目前后端定义了一个User模型, 包含了十几项数据

// Example
class User {
    id,
    username,
    created,
    updated,
    // .. 省略好几个人
}

任何接口如果有涉及到拿User数据的, 都会把该User的数据全量返回, 也就是说前端能从接口中拿到User相关的十多项数据.

但实际上并不是每个接口都需要这么多数据, 可能部分接口我们只需要用到usernameid. 但对于后端来说, 他们只管写通过逻辑, 而不去管UI上需要哪些数据.

这样一来, 每个接口都有可能返回大量无用的数据, 如果数据嵌套过深, 极端情况可能有上兆的数据.

因此前端需要做到像GraphQL一样能够自行定义所需的数据. (前提还是需要后端支持)

Squiggly

如果后端是用JAVA开发, 那么可以使用squiggly来支持前端数据自定义

根据这个库的介绍, 可以通过自定义filter形式来过滤掉JAVA类中数据的输出

javascript以及上面的User作为例子的话, 假设我们的filterusername,id, 那么当我们log(User)时候只会输出usernameid两个数据, 其他都被过滤掉

当然还支持其他过滤方式, 但下面都是以精确匹配方式来完成数据定义

最简单粗暴的方式

直接在请求中带上自定义请求头, 值设为所需要返回的字段

const fileds = 'name,user.username,user.id'
axios.request({
  url: '/example',
  headers: {
    fields
  }
})

这样后端返回的字段只有

{
  "name": "",
  "user": {
    "username": "",
    "id": ""
  }
}

这种方法存在弊端

  • 定义fileds会很麻烦
  • fields不利于复用
  • fields中定义的字段无法反应到response

进一步改进

基于上面的问题, 我所期待的效果应该如下:

  • 更容易以及明确的定义fileds
  • fields易于继承和扩展
  • 定义fileds同时能定义其类型, 并且反应到response

解决上面上个问题可以从两个方法入手

  • 通过类的方法定义fileds
  • 借助typescript完成类型定义

似乎只用typescript + interface就能很好的解决上述功能

定义类型

interface ResData {
  name: string
  user: {
    username: string
    id: number
  }
}

借助ts可以很容易定义一个类型, 只要把它赋值给axios就能很容易定义response

接下来只需要想办法把interface转成字符串

但其实类型和字符串是两个层面的东西, 类型属于ts, 而字符串是实实在在的js变量, 将两个层面连接一起的通道其实就是AST, 我们可以通过解析ts语法, 通过transform转成js代码

于是乎发现了一个ttypescript, 可以自行实现transformer来完成编译, 同时发现了一个很合适的transformer

而这篇文章整体思路跟我都是很相似, 这里就不在展开

但是说下这个方法的一些弊端

  • 不支持嵌套类型
  • 不支持数组类型
  • 对继承不友好

最终实现

定义fields以及类型

最终要达到的目的其实就是: 定义字段同时定义返回类型, 而上面的方法是从ts层面出发, 我们可以试着从js层面出发, 利用ts的类型推到功能完成

举个例子

const a = {
  name: '',
  user: {
    username: '',
    id: 1
  }
}

type A = typeof a

借助ts的类型推到可以很容易得出

type A = {
  name: string
  user: {
    username: string
    id: number
  }
}

有了这个例子, 我们就可以很容易完成我们的目标

const NumberType = 1 // type: number
const StringType = '' // type: string
const BooleanType = true // type: boolean
const AnyType = '' as any // type: any

const a = {
  name: StringType,
  user: {
    username: StringType,
    id: NumberType
  }
}

const b = {
  key1: BooleanType,
  key2: {
    key3: {
      key4: {
        key5: NumberType
      }
    }
  }
}

通过定义变量+类型推导就能很轻松完成fileds的定义

实现render方法

render方法作用其实就是将上面定义好的变量转成字符串形式的fields

function render(arg) {
  // 实现方法其实很简单, 就是遍历object输出key
  // 遇到nested或者array就递归
}

这时候我们可以这样

const fileds = render(a)
axios.request<typeof a>({
  url: '/example',
  headers: {
    fields
  }
})

到这里其实就达到了最终的目标定义fileds同时定义返回类型

但是目前这样维护起来不太容易, 我们需要继承以及更多的类型支持

继承

继承的目标就是在已有的fileds上继续扩展, Object.assign就能满足

assign本身是不带类型的, 因此需要给他加入类型以便ts进行类型推导

// 最简单的继承
function extend(t0, ...args) {
  return Object.assign({}, t0, ...args)
}

剩下要做的只需要对它进行重载以满足类型推导

// 举个例子
// 我们只需要使用泛型来重载它的输入和输入类型
export function extend<T0 extends Record<string, any>, T1>(
  t: T0,
  u: T1
): {
  [P in keyof (T0 & T1)]: (T0 & T1)[P]
}
function extend(t0, ...args) {
  return Object.assign({}, t0, ...args)
}

const a = extend({ a: 1 }, { c: '' })

type A = typeof a
// A = { a: number, c: string }

更多类型支持

typescript还有高级类型比如pick, omit, union

要实现他们, 原理跟继承一样, 都通泛型以及重载实现

// 再举个例子
function constant<T extends string | number>(arg: T): T {
  return arg
}

const a = constant(1)
type A = typeof a
// A = 1, 而不是number

组合使用

const A = {
  name: StringType
}
const B = {
  user: {
    username: StringType,
    id: NumberType
  }
}
const C = extend(
  {
    c: BooleanType
  },
  A,
  B
)

type TypeC = typeof c
// { name: string, user: { username: string, id: number }, c: boolean}

const D = pick(C, ['user'])
type TypeD = typeof D
// { user: { username: string, id: number } }

const E = omit(C, ['user'])
type TypeE = typeof E
// { name: string, c: boolean }

通过一系列的辅助方法, 就可以很好的达到我们的目的: 定义fileds同时定义类型

配合axios使用

最粗暴的方式

const A = {
  name: StringType
}
const fileds = render(A)
axios.request<typeof A>({
  url: '/example',
  headers: {
    fields
  }
})

更方便的方式

还是借用了泛型+类型推导

function render(arg: any) {}

function request<T>(fieldsDeclare: T, url) {
  const fields = render(fieldsDeclare)
  // 在这里借用了类型推导
  return Axios.request<T>({
    url,
    headers: {
      fields
    }
  })
}
const A = {
  name: ''
}
request(A, '').then(r => {
  r.data // typeof A { name: string }
  r.data.name // string
})

结语

有了以上基础,其实要实现真正的GraphQL也是可以的,只需要实现render方法即可。

基于ts的泛型+类型推导其实能实现很多强大的功能,比如vuex-ts-enhance,就是借助泛型+类型推导,完成了vuexmapXXX方法的类型推导,有兴趣可以试用下。