1. 程式人生 > >GraphQL學習過程應該是這樣的

GraphQL學習過程應該是這樣的

最簡 www. sync 基礎知識 重復 bject argument run lang

記錄一個從枯燥學習 GraphQL 的過程,到發現項目 Gitter,模仿項目 Github-Trending-API,最後做一個自己的學習項目 Github-Trending-GraphQL。

一開始我是這樣想的,最後我是這樣做的,復盤整個學習過程。
技術分享圖片

準備學習

graphql 是什麽? 在之前的項目中我們主要使用 graphql 來做已有接口數據的合並,這個主要處理已有 rest 相關服務接口的情況下,我們做了一個中間數據處理層。
最近在思考團隊服務項目開發的時候,因為在開發中如果基於 rest 接口來開發的會,會定義很多路由。為了偷懶不去定義路由,於是決定在項目中使用 graphql (其實只是為了裝B,我在項目中用了最新的XX技術),中間還有一些其他的思考。

幾個概念

Graphql 模型有三種類型的操作。

Query

查詢數據(R)。

# standard
query {
    field
}

# shorthand
{
    fields
}

Mutation

新增、更新或刪除數據(CUD)。

mutation {
    do( arguments ) {
        fields
    }
}

Objects

表示可以訪問的資源。

# Repository 包含項目的內容
    # Implements
    # Connections
    # Fields

Implements

學不動了,省略....

受其它項目啟發

在枯燥的文檔學習過程中,中間看到一個博客是推薦自己的小程序 gitter,出於習慣抓了一下小程序的請求,發現了趨勢排行是通過 github-trending-api.now.sh 獲取的數據,接著就找到了這個 API 對應的項目 github-trending-api。
在這之前我也看過幾次 GitHub GraphQL API,只是趨於時間與其他因素(懶),一直沒有使用落實到實際的項目中。發現官方沒有提供 Trending API,github-trending-api 項目新增了 V3 中的Trending API,我是不是可以模仿該項目提供一個 GraphQL API。
帶著兩個目的開始一個新項目:

  • 學習 GraphQL
  • 做一個開源項目

初始化項目

最簡單的實現方式就是提供一個 GraphQL server,然後直接請求 github-trending-api.now.sh 。這種用法對於項目已有微服務的團隊,可以利用中間服務層來合並數據請求,以及嵌套數據查詢等。
GraphQL server 使用的是 Apollo Server,用它來創建一個 Node 服務,定義好 Schema,增加 resolver 解析函數。

Type 如何定義

在一開始學習的基礎只是派上用場,GitHub Trending 主要提供兩個方面,一個是 Repository ,另外一個是 Developer。

type Repository {
    author: String
    contributors: [Contributor]
    currentPeriodStars: Int
    description: String
    forks: Int
    language: Lang
    name: String
    stars: Int
    url: String
}

Repository 中除了基本的 scalar type 還有兩個是 contributor 和 language,一個數組數據,一個是對象,繼續細分類型下去就得到了

type Contributor {
    avatar: String
    url: String
    username: String
}
type Lang {
    name: String
    color: String
}

Developer 分析數據後一樣得到一個數據結構

type Developer {
    avatar: String
    name: String
    repository: RepositoryMini
    username: String
    url: String
}

其中項目倉庫是一個對象數據,細分下來可以得到一個

type RepositoryMini {
    description: String
    name: String
    url: String
}

Query 如何定義

定義好了基本數據類型 Repository 和 Developer 以後,需要對外提供一個統一的 Query,於是得到了一個新的根數據類型

type Query {
    repositories: [Repository],
    developers: [Developer]
}

實際的查詢趨勢過程中我們還會增加參數,一個參數是 language,一個參數 since,其中 since 只能取 daily、 weekly、 monthly ,但實際也能取其它值,只是默認的還是 daily。修改後得到了下面的結果

type Query {
    repositories(language: String, since: String): [Repository],
    developers(language: String, since: String): [Developer]
}

如果要驗證 since 只能取三個值中的一直,需要新增一個枚舉類型

type Query {
    repositories(language: String, since: Since): [Repository],
    developers(language: String, since: Since): [Developer]
}

enum Since {
    daily
    weekly
    monthly
}

如何優化 Query

上述寫法實際過程中可能還會有這樣一個問題,如果要同時查詢獲得 Repository 和 Developer 的數據,需要按照篩選條件查詢的適合,需要重復傳遞參數,再提升一下這兩個類型實際是屬於類型 Trending 的。新增一個類型

type Trending {
    repositories: [Repository]
    developers: [Developer]
}

根查詢 Query 也可以修改一下了

type Query {
    trending(language: String, since: String): Trending
}

客戶端發起查詢請求

按照最終我們定義好的數據結構,我們可以發起一個這樣的 query

{
  Trending(language: "javascript", since: "daily") {
    repositories {
      name
      author
      description
      language {
        name
        color
      }
      forks
      stars
      contributors {
        avatar
        url
        username
      }
      currentPeriodStars
      url
    }
    developers {
      avatar
      name
      repository {
        url
        name
        description
      }
      username
      url
    }
  }
}

如果把 language 和 since 定義在 variables 中,寫法就變成了下面這樣

# 以下請求只獲取了趨勢倉庫名稱
# query
query getTrending($language: String, $since: String) {
  trending(language: $language, since: $since) {
    repositories {
      name
    }
  }
}

# variables
{
  "language": "javascript",
  "since": "daily"
}

queryvariables 會作為 request payload 放置在 body 中,其中把自定義的操作方法 operationName 設置為 getTrending

fetch("https://trending.now.sh", {
    "credentials": "omit",
    "headers": {
        "accept": "*/*",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
        "content-type": "application/json"
    },
    "referrer": "http://localhost:4000/",
    "referrerPolicy": "no-referrer-when-downgrade",
    "body": "{\"operationName\":\"getTrending\",\"variables\":{\"language\":\"javascript\",\"since\":\"daily\"},\"query\":\"query getTrending($language: String, $since: String) {\\n  trending(language: $language, since: $since) {\\n    repositories {\\n      name\\n    }\\n  }\\n}\\n\"}",
    "method": "POST",
    "mode": "cors"
});

服務端解析請求

這裏用的是 Apollo server,服務收到請求以後,會解析 body 參數。會按照嵌套依次調用 resolver 處理業務邏輯,首先會進入 trending ,接著同時執行 repository 和 developer。

按照根查詢定義好的數據結構,tending 解析器會收到兩個參數,language 和 since。repository 和 developer 也要使用這兩個參數如何處理呢?

// resolver
{
    Query: {
        trending(parent, args, context, info) {
            // args => { language: '', since: '' }
            // parent 參數是可以接收到上層解析器的結果,我們可以把 trending 中收到的數據傳遞給子解析器
            return { language, since }
        }
    },
    Trending: {
        repositories(parent, args, context, info) {
            // parent => { language: '', since: '' }
        },
        developer(parent, args, context, info) {
            // parent => { language: '', since: '' }
        },
    }
}

解析器中需要做什麽?

解析器按照前文分析的數據,我們可以直接請求 github-trending-api.now.sh 數據接口拿到數據,這裏我們本著學習為目的,GitHub Trending 是通過 SSR 輸出的頁面,數據只能自己分析網頁,抓取html頁面以後分析頁面結構獲得自己需要的數據。

export async function fetchRepository() {
    // 分析html
}

export async function fetchDeveloper() {
    // 分析html
}

export async function fetchLanguage() {
    // 分析html
}

具體的分析 html 過程不做分析,使用了 cheerio,用法類似 JQuery。這中間也會有一些需要註意的問題

  • 請求過程很慢。
    每次請求都會再次請求 Github Trending 的頁面,然後還要分析頁面,這個過程其實是比較費時的。我們如果把請求分析後的數據按照查詢條件緩存起來,下一次請求直接就從緩存中拿數據,這樣就快很多。(倉庫和開發者趨勢會隔段時間更新,我們緩存一小時;語言變化小,我們緩存了一天的時間)

  • 語言包緩存。
    請求倉庫和開發者的適合,檢測語言緩存是否存在,不存在先緩存一次,後續再次請求倉庫和開發者或者直接請求語言包就會直接命中緩存

有了緩存就可能出現緩存失效的問題,我們新增一個刷新緩存的方法,可以按照指定鍵名來更新緩存,也可以不傳遞參數清理全部緩存。

如何清理緩存?

GraphQL 根處理方法除了 Query ,還有一個 Mutation。對應到的數據庫增刪改查上面的話,Query 對應的是 RMutation 對應的是 CUD。我們要新增的 refresh 的操作是刪除緩存,主要針對倉庫和開發者緩存,清理以後我們只關心成功失敗與否,所以這裏我們可以返回一個布爾值

type Mutation {
    refresh(key: String, language: String, since: String): Boolean
}

解析器中也需要添加對應的處理方法

{
    Mutation: {
        refresh(parent, args, context, info) {
            // do something
        }
    }
}

回顧一下

從一開始的需求分析,我們需要開發一個 Github Trending GraphQL API。我們利用了之前學習的 GraphQL 的基礎知識,也熟悉了 GraphQL 的工具 Apollo Server,很方便的開發出了對應的API,後續為了優化請求,我們新增了緩存策略,以及清除緩存策略。

到這裏我們的項目 github-trending-graphql 就可以提交到 GitHub 倉庫中了,對於一個完美的開源項目還有很多事情要做,但是對於一個 GraphQL 的示例差不多已經可以使用了。

一上來就直接看代碼是枯燥的,於是我們還需要部署一個 Demo,這樣帶著使用來熟悉就更容易讓人理解了。如何簡單的部署 Demo 又成為了一個問題?

如何部署示例

trending.now.sh 的部署看域名應該就能猜到使用的是 now 的無服務部署方式。使用方式文檔已經講述的很清楚了。但這中間也還是需要註意一些細節

對於項目部署,我們需要首先在項目根目錄建立一個 now.json

{
    "version": 2,
    "alias": ["trending.now.sh"],
    "builds": [{
        "src": "src/server.js", "use": "@now/node-server"
    }],
    "routes": [{
        "src": "/",
        "dest": "/src/server.js"
    }]
}

alias 這裏配置上 now.sh 的別名是不會直接生效的,這裏只是方便備忘。server.js 是一個需要執行的文件,於是我們將 version 設置為 2,接下來我們就可以在配置中添加 builds 了,對於普通 js 可指定文件使用 @now/node ,這裏的 server.js 是開啟一個 Node 服務,所以需要使用 @now/node-server

部署成功以後我們獲得了一個 github-trending-graphql-[hash].now.sh 的項目訪問地址,如果要訪問到項目的實際功能,還需要點開兩次兩次獲得項目功能地址 github-trending-graphql-[hash].now.sh/src/server.js ,如果要直接使用域名直接訪問功能,我們這裏就需要添加上述配置 route

每一次部署都會產生一個新的鏡像,也會得到一個新的二級域名,如果我們要分享出去無論是自己部署還是用戶使用體驗都不是很好,我們可以為自己的項目設置一個別名,這裏我們為當前項目設置的別名就是 trending.now.sh 。

每次部署的時候我們需要做的工作就是 now && now alias ,now alias 需要指定當前部署獲得的項目域名,以及需要設置的別名,$(now) 可以獲得部署後獲得的域名,於是上述命名就修改成 now alias $(now) trending.now.sh 了,添加 package.json 中,每次部署只需要執行一下 npm run now

成果展示

github trending graphql api
online demo

相關項目

github trending rest api

GraphQL學習過程應該是這樣的