利用泛型+类型推导定义伪GraphQL模型
接触过前端的应该都有听过GraphQL
简单来说就是前端自行定义接口所需要返回的数据, 想要尝试的可以试着调用GithubAPI V4.
而对于我们常用的xhr
请求能否也做到跟GraphQL
一样能自定义接口返回的数据?
答案是可以, 但是提前必须是后端必须提供足够的数据让前端自行选择.
例子
假设目前后端定义了一个User
模型, 包含了十几项数据
// Example
class User {
id,
username,
created,
updated,
// .. 省略好几个人
}
任何接口如果有涉及到拿User
数据的, 都会把该User
的数据全量返回, 也就是说前端能从接口中拿到User
相关的十多项数据.
但实际上并不是每个接口都需要这么多数据, 可能部分接口我们只需要用到username
和id
. 但对于后端来说, 他们只管写通过逻辑, 而不去管UI
上需要哪些数据.
这样一来, 每个接口都有可能返回大量无用的数据, 如果数据嵌套过深, 极端情况可能有上兆的数据.
因此前端需要做到像GraphQL
一样能够自行定义所需的数据. (前提还是需要后端支持)
Squiggly
如果后端是用JAVA
开发, 那么可以使用squiggly来支持前端数据自定义
根据这个库的介绍, 可以通过自定义filter
形式来过滤掉JAVA
类中数据的输出
用javascript
以及上面的User
作为例子的话, 假设我们的filter
是username,id
, 那么当我们log(User)
时候只会输出username
和id
两个数据, 其他都被过滤掉
当然还支持其他过滤方式, 但下面都是以精确匹配方式来完成数据定义
最简单粗暴的方式
直接在请求中带上自定义请求头, 值设为所需要返回的字段
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,就是借助泛型+类型推导,完成了vuex
中mapXXX
方法的类型推导,有兴趣可以试用下。