1

如果你的项目稍有规模,那么你一定经受过一种折磨。一个很久之前的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呢?


小红星闪啊闪
914 声望1.9k 粉丝

时不我待