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

原文连接javascript

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

简单来讲就是前端自行定义接口所须要返回的数据, 想要尝试的能够试着调用GithubAPI V4.前端

而对于咱们经常使用的xhr请求可否也作到跟GraphQL同样能自定义接口返回的数据?vue

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

例子

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

// Example
class User {
    id,
    username,
    created,
    updated,
    // .. 省略好几我的
}
复制代码

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

但实际上并非每一个接口都须要这么多数据, 可能部分接口咱们只须要用到usernameid. 但对于后端来讲, 他们只管写经过逻辑, 而不去管UI上须要哪些数据.github

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

所以前端须要作到像GraphQL同样可以自行定义所需的数据. (前提仍是须要后端支持)typescript

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方法的类型推导,有兴趣能够试用下。

相关文章
相关标签/搜索