React Native填坑之旅--GraphQL

小紅星閃啊閃發表於2021-11-10

如果你的專案稍有規模,那麼你一定經受過一種折磨。一個很久之前的API返回了巨多務必的資料,是可以完全服務現在的需求。但是明顯不必要的資料資料過多會造成後端的效能問題。在前端佔了頻寬。後期的維護對於前後端都是可能產生棘手的問題。之所以FB要提出GraphQL的標準也是因為FB本身支援的產品太多遇到了這樣的問題。

GraphQL是啥

在正式開始之前,稍微介紹一下GraphQL。它是Facebook定的一個標準。標準就是隻定義了最後的效果,至於怎麼做那就各家自由發揮。為了擺脫上面說的那種各種API的混亂局面:

  • GraphQL把所有的請求endpoint統一成了一個。這樣還可以增刪改查都不用再另外寫API。一個endpoint搞定。
  • 還有一套對所有資料的型別描述。這個描述是從query(必須)或者mutation開始。

一個官網的簡單例子:

input MessageInput {
  content: String
  author: String
}

type Message {
  id: ID!
  content: String
  author: String
}

type Query {
  getMessage(id: ID!): Message
}

type Mutation {
  createMessage(input: MessageInput): Message
  updateMessage(id: ID!, input: MessageInput): Message
}

GraphQL還是通過Http的GET和POST的方式返回資料,只是GET的長度限制導致可能的查詢會出問題。所以一般都可以用POST來獲取、修改資料。這就是說GraphQL在客戶端App來說可以和平時請求API的方式完全一樣。在基本使用上,有沒有第三方graphql client的庫的幫助都沒什麼區別。

有了GraphQL之後,如果客戶端這邊說有了什麼需求,就獲取這個需求的必要資料,那麼不需要新開發API。

查詢:

query {
    todos {
      id
      title
    }
  }

這是一個查詢。要查詢的是todos(可以暫時理解為一個表),要查詢的是idtitle兩個欄位。這個查詢只會返回idtitle兩個欄位對應的資料。

也可以是帶條件的查詢:

  query ($from: Int!, $limit: Int!) {
    todos (from: $from, limit: $limit) {
      id
      title
    }
  }

GraphiQL

新增、修改

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

返回

{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}

基本介紹就到這裡。詳細可以參考官方文件

支援的語言

為啥不是Relay

官方的庫專職負責勸退有沒有體會過。GraphQL是Facebook提出來的一個標準,注意這是一個標準而不是實現。服務端的情況不熟不多做介紹,但是在客戶端FB或者現在叫Meta了,給出了一個實現並且已經發展了很多年。這個庫叫做Relay

它憑藉強大的功能和Meta(當年FB)背書,很快發展了起來。不過這個工具顯然已經有點後勁不足了。從現在TypeScript專案發展的情況來看,它顯然缺乏對TypeScript的支援。比如它的一個配套babel外掛沒有對TypeScript的支援。當然也是一個小問題,只需要在自己的專案裡新增一個index.d.ts檔案並新增型別就可以。

然後是它的模式。在你按照官方文件的Step by step一步一步走完的話,你還是不能做開發。因為你在新增另外一個檔案的和查詢的時候就會發現,這個需要的查詢並不會自動生成。要麼是悄沒聲的沒有報錯也沒有生成對應的檔案,要麼是報一些莫名其妙的錯。因為你需要根據你的檔名來命名查詢(或者任何的操作)。也就是它的模式可以認為是強侵入的,雖然會比其他的方式少寫一些固定程式碼,雖然也不一定。筆者水平有限,只好先棄了。

URQL怎麼樣

首先,urql在github有6.5K的star。並且設計也足夠活躍。最後被後還有個公司支援。不能說不是KPI專案,但是KPI專案也有個好處,至少有為了KPI的人在維護程式碼。

另外,這個庫是用TypeScript開發的。也就是說它肯定是TypeScript友好的,你的專案如果用了TypeScript,在型別上不用擔心過時、不完整等問題。

image.png

並不是其他的庫不合適,更多可以選擇的庫在GraphQL官網裡有列出來。

用一下現成的GraphQL服務:Github

Github很久以前就提供了GraphQL API。我們就在APP裡呼叫github的GraphQL API。用來查詢某個owner(比如facebook)下面的公開程式碼庫。

要使用github的GraphQL服務需要用到token,所以需要一個輸入token的介面。在使用者輸入token後可以持久化儲存這個token。還需要有一個介面可以刪掉這個token,讓使用者有機會可以輸入新的token。尤其使用者修改了許可權之後,那麼就必須要有一個更新token的地方。

導航

simulator_screenshot_B30AF36D-0A2F-4257-AB60-2541F100FD62.png

simulator_screenshot_F318440D-1F2F-49BB-9DCC-51FF7FF2A215.png

使用者在進入APP之後,在點選Repo選項之後,!如果不存在這麼一個Token,則會進入token頁面。在使用者輸入token之後才能繼續後面的功能。

使用者在輸入Token頁輸入token後跳轉到列表頁。在Settings頁可以刪掉Token,然後自動跳轉到Token頁。

在使用者成功輸入token,進入repo列表頁可以看到repo列表。現在只顯示facebook下面的公開repo。後面加入search bar可以輸入owner,這樣就可以控制要搜尋的是哪些repo了。

URQL基本配置

urql的配置分兩部分。第一是provider的配置。使用provider可以讓所有呼叫graphql api的地方都很方便的拿到請求的endpoint和需要的auth token。當然不是明文的讀取而是可以直接呼叫查詢。

配置Provider

App.tsx可以看到:

    <Provider value={client}>
      <NavigationContainer>
        <Stack.Navigator initialRouteName="Token">
          <Stack.Screen
            name="Tabs"
            component={Tabs}
            options={{ headerShown: false }}
          />
        </Stack.Navigator>
      </NavigationContainer>
    </Provider>

這個Provider和react-redux的provider的作用一樣。這裡urql的provider提供的是一個client。

Exchange

Exchange是urql的一箇中介軟體機制。這個機制也和Redux的中介軟體機制類似。

這裡我們需要給官網提供的authExchange填空,把獲取和使用token的邏輯加進去。

首先需要安裝authExchange

yarn add @urql/exchange-auth

然後在路徑:src/data/graphqlClient.ts下可以看到給authExchange填空的程式碼。在這裡需要新增的除了上文說的獲取token,使用token之外還有錯誤處理的內容。之類為了簡單,錯誤處理的部分先忽略。有需要的同學可以研究官網例項。

獲取token的方法是getAuth。我們的token是在Github配置生成,然後使用者完整新增並儲存在APP的裡。所以不需要額外的API呼叫獲取token。

  getAuth: async ({ authState }): Promise<AuthState | null> => {
    try {
      if (!authState) {
        const token = await AsyncStorage.getItem(GITHUB_TOKEN_KEY);
        return { token } as AuthState; // *
      }
      return null;
    } catch (e) {
      return null;
    }
  },

在加星這一步可以看到,token是作為authState物件的一個屬性返回了。

使用token是通過方法addAuthToOperation實現的。在這裡最後會返回一個新建的operation。裡面就存放了從getAuth拿到的token:

  addAuthToOperation: ({ authState, operation }) => {
    // ...略
    return makeOperation(operation.kind, operation, {
      ...operation.context,
      fetchOptions: {
        ...fetchOptions,
        headers: {
          ...fetchOptions.headers,
          Authorization: `Bearer ${authState.token}`,  // *
        },
      },
    });
  },

在加*這一步使用了token,從authState裡讀出了token放在header的認證裡隨著api請求傳送到了後端。

填充urql的client

通過Exchange配置好了token之後,關鍵的一步就完成了。接下來就需要把配置號的exchange和graphql的endpoint都新增到client裡供graphql的查詢使用。

const getGraphqlClient = () => {
  const client = createClient({
    url: 'https://api.github.com/graphql', // 1
    exchanges: [
      dedupExchange,
      cacheExchange,
      authExchange({    // 2
        ...authConfig,
      }),
      fetchExchange,
    ],
  });

  return client;
};

export { getGraphqlClient }; // 3
  1. 在url屬性新增github的graphql的endpoint:https://api.github.com/graphql
  2. 把auth exchange新增到exchange陣列裡。在這裡配置的時候需要注意同步操作的exchange要放在非同步操作的exchange前面。所以,authExchange要放在第三位。

實現一個查詢

完成了上面的配置之後,我們可以開始實現一個簡單的查詢了。

src/GithubScreen.tsx檔案裡可以看到具體的查詢和執行後的效果。

首先來準備我們的查詢語句。

import { gql, useQuery } from 'urql'; // 1

const REPO_QUERY = gql`    // 2
  query searchRepos($query: String!) {
    search(query: $query, type: REPOSITORY, first: 50) {
      repositoryCount
      pageInfo {
        endCursor
        startCursor
      }
      edges {
        node {
          ... on Repository {
            name
          }
        }
      }
    }
  }
`;
  1. 引入urql到工具方法gql和useQuery。useQuery後面會用到
  2. 編寫查詢語句。

這個查詢語句看起來會讓初學者不知所措。上面的例子我們也只是提到了query,metation之類少數幾個關鍵字。那麼這麼長的(還不算長)查詢語句如何能寫出來呢。github專門提供了一個graphql api的explorer。點選這裡到explorer。事實上,在很多語言對GraphQL的實現裡都有這樣一個explorer,至少是在開發階段可以享受到這個服務。

image.png

  • 首先在這個頁面登入你的github賬號。
  • 在GraphiQL裡就可以測試各種各樣的查詢語句了。
  • 如果你有schema不清楚的可以看最右面的Doc文件
  • 左下角的Query Variable可以輸入查詢的變數。這裡就是query,對應的是repo的owner和repo的license型別。
  • 中間一列就是查詢的結果。

在中間看到查詢結果之後,就可以判斷你的查詢語句是否合適。在本例中就是我們需要的查詢語句了,直接複製到我們的程式碼裡使用。

或者,如果你對於schema的定義略有了解的話,比如我們這次要查詢的是repository。也可以使用查詢語句編輯器裡面的智慧提示。整個來說,編寫查詢、修改語句是非常方便的。

一個簡單的查詢

上面說到如何編寫一個查詢語句,現在來使用這個語句查詢repo列表。

urql也提供了這樣的一個hook給react使用。

import { gql, useQuery } from 'urql';  // 1

const REPO_QUERY = gql `query searchRepos(...) { ... }`;

const GithubScreen = () => {
  const [result] = useQuery({  // 2
    query: REPO_QUERY,
    variables: { query: 'user:facebook' },
  });

  const { data, fetching, error } = result; // 3

  // ...略...

}

很簡單一個簡單的查詢就可以搞定了。

  1. 只需要請useQuery出場。
  2. 使用useQuery。這個hook還會返回一個重新執行查詢的方法,主要使給重新整理時使用。
  3. 網路請求三狀態,data是資料,fetching表示請求中,error是查詢出現錯誤。

後面的程式碼可以根據出現的作出不同處理。

最後在FlatList中顯示user是facebook的所有公開repo。

image.png

一個有引數的查詢

如果我麼給使用者一個輸入user的地方,查詢特定的user的所有公開的repo需要怎麼做呢?如圖:

image.png

使用者輸入的是github那麼我們就所搜github的所有公共repo,輸入的是什麼就搜尋指定和使用者的repo列表。觸發查詢操作的是點選查詢按鈕。使用者點選了查詢按鈕,那麼就去執行一次查詢。

這個時候userQuery就不能用了。它返回tuple的兩個元素的另一個可以執行重新整理操作,但是不能修改query語句。

const [result, reexecuteQuery] = useQuery({...});

useQuery返回的reexecuteQuery只能是再次執行已經給定的查詢語句。可以修改的是一些快取策略之類的,但是不包括查詢語句。所以,我們只能尋找另外的解決方法。

那就是我們之前在配置urql的時候使用的client。它也是存放在Provider的context裡的。所以可以通過useContext這個hook拿到。urql的官方也給我們提供了一個方便獲取client物件的工具:useClient。從useClient拿到client之後可以呼叫它的query方法執行查詢。這樣就靈活多了。

我們可以在使用者輸入user字串,點選查詢按鈕之後開始查詢。然後把結果展示在列表裡。

const [searchText, setSearchText] = useState('');  // 1
  const client = useClient();  // 2
  const [result, setResult] = useState({ // 3
    fetching: false,
    data: null,
    error: null,
  });

  const { data, fetching, error } = result;

  const handleSearchChange = (text: string) => setSearchText(text); // 4
  const handleIconPress = async () => {  // 5
    setResult({ fetching: true, data: null, error: null });
    try {
      const res = await client
        .query(REPO_QUERY, { query: `user:${searchText}` }) // 6
        .toPromise();
      setResult({
        fetching: false,
        data: res.data?.search?.edges ?? [],
        error: res.error,
      });
    } catch (e) {
      setResult({ ...result, fetching: false, data: null, error: e });
    }
  };
  1. 記錄使用者在查詢框輸入的user字串
  2. 通過useClient hook獲取到client物件
  3. 處理查詢中的結果。fetching、data和error和上文useQuery得到的結果基本一致。
  4. 搜尋框文字輸入的handler
  5. 搜尋框,搜尋按鈕點選的handler
  6. 使用client物件執行查詢語句。

一個帶引數的查詢就完成了。對於修改、新建的GraphQL API的呼叫基本上也是大同小異。

一點反思(都是廢話,可以忽略)

目前來看,我們在使用urql所做的基本只是查詢和,算是複雜一點點的auth操作。但是,筆者在使用authExchange的時候其實稍微遇到一點坑。如果不是把例子裡的所有exchange都直接複製過來的話還真沒法把這個app執行起來。

在最開始配置client的時候也有一個問題,urql的Provider要最接近與根元件,也就是<App />。當然這些沒有最後確定,但是滿足上面條件的時候app就跑起來了,足以說明問題。

GraphQL是可以直接用fetch來實現的。如果使用fetchredux或者公平一點直接使用上面的hooks來處理已有功能速讀會更快。學習成本地,所有程式碼寫成util方法也可以用的很舒服。關鍵,沒有學習成本,fetch或者Axios這些庫天天都在用。

好在urql還有一個useClient讓問題簡單了些。當然要查文件。但是,後面需要處理的快取的問題就要複雜一些了。我們自己造輪子實現一個快取管理的工具?那複雜度就比刷文件要複雜的多了。所以我們在選擇庫的時候務必還是需要把學習成本,開發維護成本,社群成熟度,結合上線的時間壓力等考慮進去。

最後

一個簡單的查詢在這個app裡就已經完成了。但是顯然還有一些工作需要做。比如,loading和error處理都顯得比較簡陋。我們在前面的系列裡提到了redux-toolkit。是否可以有一個slice來讓這些邏輯的處理和redux結合在一起。

我們在依賴裡也已經新增了react-native-paper元件庫。這個庫可以在native和web上通用。我還沒有把web斷的截圖放上來,主要因為有點慘不忍賭。UI也可以在後面稍作美化。

最主要的工作是如何在實際的開發中使用graphql。它的潛力絕不只是看起來很新穎這麼簡單看,而是可以實實在在的解決問題的。前端的同學會遇到一個最大的阻力就是來自於後端同學是否接收這一不太新的新事物。

在youtube上有一個30分鐘搞定graphql的視屏,點這裡可以看。實際後端要整合graphql肯定不會像視屏裡的那麼容易,而且他本身也僅僅演示了查詢操作的處理。不過也不會像想象的那麼難。GraphQL是一個標準,在實現上也是由從外部查詢語句到內部獲取資料之間的轉換,也就是視訊裡的resolver,和schema定義。它依然依賴於底層的“DAO”層,或者是rest api的http請求。在GraphQL實現之後的收益就非常的顯而易見,資料消費端(各種App)對於後端修改的需求會大幅度減少。擺弄query語句就可以何必後端新增API呢?

相關文章