urql
- FormidableLabs/urql
- 流处理实现基于 kitten/wonka
 
 - 优势
- 支持泛化缓存
 - Schema 感知
- 开启后支持部分结果返回 - 请求的字段未被缓存但是是 nullable 则先返回 - 因为不影响语义
 - 页面切换数据显示更顺畅
 
 - stale 查询 - @urql/exchange-request-policy
 - 支持多框架 - @urlql/core, urql -> react-urql, preact, next, vue, svelte
 
 - 参考
- RFC: Fragment Suspense boundaries in React bindings #1408
 
 
caution
npm add urql @urql/{core,devtools,exchange-graphcache,exchange-retry,exchange-multipart-fetch}
根据后端实现选择
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { createClient as createSubscriptionClient } from 'graphql-ws';
type ExchangeIO = (Source<Operation>) => Source<OperationResult>;
type Exchange = ExchangeInput => ExchangeIO;
Note
- 默认有 Document Caching - 适用于高度依赖静态数据的站点
- 缓存 key 为 
hash(query,variables) - 根据 mutation 返回类型进行失效
 - 如果返回空 list, 则不会包含 
__typename, 此时无法失效 - 可在上下文添加 additionalTypenames 
 - 缓存 key 为 
 - exchange - 扩展点 - 默认 dedupExchange, cacheExchange, fetchExchange
- 类似 apollo 的 link - 但更通用
 
 
请求策略
| RequestPolicy | Desc | 
|---|---|
| cache-first | 默认 默认返回 cache 结果,不存在则请求 | 
| cache-and-network | 先返回 cache 结果,也请求更新 | 
| network-only | 忽略缓存,直接发起请求 | 
| cache-only | 返回缓存或者 null | 
exchanges
| exchange | desc | 
|---|---|
| @urql/exchange-graphcache | 提供泛化图缓存 | 
| @urql/exchange-retry | 重试失败请求 | 
| @urql/exchange-execute | 本地模拟执行,用于测试或服务端 | 
| @urql/exchange-multipart-fetch | 文件上传 | 
| @urql/exchange-persisted-fetch | 基于 hash 查询,避免发送 query | 
| @urql/exchange-request-policy | 缓存失效和更新,将 cache-first 和 cache-only 上升为 cache-and-network | 
| @urql/exchange-auth | 请求添加授权信息,例如 JWT | 
| @urql/exchange-refocus | 窗口获取焦点时重新发起请求 | 
@urql/exchange-graphcache
- Normalized Caching - 泛化缓存
- 缓存 key 为 
__typename:id - 会缓存字段和关系
 - 可根据类型自定义缓存 key
 - 无 key 的结构作为嵌入数据依附在上级文档
 
 - 缓存 key 为 
 - Local Resolver - 本地解析
- 提供 resolvers 直接在客户端处理请求
 
 
cache.readQuery({ query: Todos, variables: { from: 0, limit: 10 } });
// readFragment 只支持 DocumentNodes
cache.readFragment(
  gql`
    fragment _ on Todo {
      id
      text
    }
  `,
  { id: 1 },
);
// 检测所有字段
cache.inspectFields({
  __typename: 'Todo',
  id: args.id,
});
// 基于 offset 和 limit 分页
import { simplePagination } from '@urql/exchange-graphcache/extras';
//
import { relayPagination } from '@urql/exchange-graphcache/extras';
cacheExchange({
  keys: {
    // 自定义 key
    Item: (data) => data.uuid,
    // 显性移除 key
    Image: () => null,
  },
  // 本地 resolve
  // 适用于转换字段类型,模仿服务端请求,减少实际请求
  resolvers: {
    // 类型
    Query: {
      // 集成分页能力
      // mergeMode=inwards
      todos: relayPagination(),
      // { todo(id: 1) { id } } 读取 { todos { id } } 的缓存
      todo(parent, args, cache, info) {
        // 等同于返回缓存 key
        // cache.keyOfEntity({ __typename: 'Todo', id: args.id }),
        return { __typename: 'Todo', id: args.id };
      },
    },
    Todo: {
      // 转换字段类型
      updatedAt: (parent, args, cache, info) => {
        // 也可以访问当前对象上数据 - cache.resolve(info.parentKey)
        // parent.updatedAt || cache.resolve(parent, "createdAt")
        // new Date(cache.resolve(parent, "updatedAt")),
        return new Date(parent[info.fieldName]);
      },
    },
  },
    // 手动更新
  updates: {
    Mutation: {
      // add 过后将返回结果添加到 查询列表
      addTodo(result, args, cache, info) {
        const query = gql`
          {
            todos {
              id
            }
          }
        `;
        cache.updateQuery({ query }, (data) => {
          data.todos.push(result.addTodo);
          return data;
        });
      },
      // 移除 todo 后更新受影响的 list 查询
      removeTodo(_result, args, cache, _info) {
        const TodoList = gql`
          query (skip: $skip) {
            todos(skip: $skip) { id }
          }
        `;
        const fields = cache.inspectFields('Query')
          .filter(field => field.fieldName === 'todos')
          .forEach(field => {
            cache.updateQuery(
              {
                query: TodoList,
                variables: { skip: field.arguments.skip },
              },
              data => {
                data.todos = data.todos.filter(todo => todo.id !== args.id);
                return data;
              }
            });
          });
      },
    },
  },
  // 乐观更新 - 模仿服务端更新行为
  // 当服务端返回后更新会丢弃
  optimistic: {
    favoriteTodo: (variables, cache, info) => ({
      __typename: 'Todo',
      id: variables.id,
      favorite: true,
    }),
  },
});
relayPagination
- Resolver
 - 基于 relay 的 connection 进行分页数据合并
- 支持 first+last
 - 支持 before, after
 
 - mergeMode - 合并模式
- inwards - 默认 - 往后合并
 - outwards - 往前合并
 
 - 参数变化会重置 - 忽略 first, last, after, before
 
Version
URQL v4
- 没有 defaultExchanges
- 之前为 [dedupExchange, cacheExchange, fetchExchange]
 
 - fetch
- 支持 multipart/mixed, text/event-stream
 
 @urql/exchange-multipart-fetch- 合并到 core
 
dedupExchange- 默认行为
 
- core
- 不再依赖 graphql
 - bundle 部分 graphql 内容: parse, print, GraphQLError
 
 - urql v4 Major Releases
 
FAQ
URQL vs Apollo
- URLQ
- 更小更灵活
 - 支持提供 schema 实现更多功能
 - 支持 offline
 - 支持 window focus 触发请求
 - 缓存为可选组件
 
 - vs Apollo
 
production 构建后 urql 返回 null
起因是 gqlgen 不支持 fragment 里包含 alias,不会返回字段,urql 检测少字段认为 cache miss 返回 null。