利用泛型+類型推導定義僞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方法的類型推導,有興趣能夠試用下。

相關文章
相關標籤/搜索