urql与Svelte集成教程:轻量级前端框架的GraphQL最佳搭档

urql与Svelte集成教程:轻量级前端框架的GraphQL最佳搭档

【免费下载链接】urql The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow. 项目地址: https://gitcode.***/gh_mirrors/ur/urql

你是否正在寻找一种简洁高效的方式在Svelte应用中集成GraphQL?面对复杂的状态管理和数据请求逻辑,是否希望找到一个轻量级但功能强大的解决方案?本文将带你一步一步实现urql与Svelte的无缝集成,让你在开发过程中享受高效的数据交互体验。读完本文后,你将能够:掌握urql客户端在Svelte中的配置方法、实现GraphQL查询和变更操作、优化数据缓存策略,以及处理常见的错误和边缘情况。

为什么选择urql与Svelte搭配

在现代前端开发中,选择合适的工具组合至关重要。Svelte作为一个编译时框架,以其高效的性能和简洁的语法受到越来越多开发者的青睐。而urql作为一个高度可定制的GraphQL客户端,与Svelte的轻量级理念不谋而合。

urql的核心优势在于其模块化设计和灵活的插件系统。通过交换器(Exchanges)机制,你可以根据项目需求灵活组合不同的功能模块,如缓存、认证、重试等。这种设计使得urql在保持核心体积小巧的同时,又能满足复杂应用的需求。

相比其他GraphQL客户端,urql与Svelte的集成更加自然。Svelte的响应式系统与urql的数据流模型完美契合,使得状态管理变得简单直观。此外,urql提供了专门针对Svelte优化的绑定库@urql/svelte,进一步简化了集成过程。

官方文档中详细介绍了urql的核心概念和架构设计,你可以通过阅读urql核心概念文档深入了解其工作原理。

环境搭建与安装

在开始集成urql之前,我们需要确保开发环境已经准备就绪。首先,确保你的项目中已经安装了Svelte。如果还没有创建Svelte项目,可以使用以下命令快速初始化:

npm create svelte@latest my-urql-svelte-app
cd my-urql-svelte-app
npm install

接下来,安装urql及其Svelte绑定:

npm install @urql/svelte graphql

这里需要注意的是,graphql包是urql的 peer dependency,必须显式安装。安装完成后,我们就可以开始配置urql客户端了。

如果你使用的是Vite作为构建工具,可能会遇到"Function called outside ***ponent initialization"错误。这是由于Vite的依赖预构建机制导致的。解决方法是在vite.config.js中添加以下配置:

export default defineConfig({
  optimizeDeps: {
    exclude: ['@urql/svelte'],
  },
  // 其他配置...
});

这个配置告诉Vite不要预构建@urql/svelte包,从而避免组件初始化顺序的问题。

urql客户端配置

urql的核心是Client实例,它负责管理GraphQL请求、缓存和响应处理。在Svelte应用中,我们通常在根组件中创建并提供客户端实例,以便整个应用都能访问它。

创建一个新的文件src/lib/client.js,添加以下代码:

import { Client, cacheExchange, fetchExchange } from '@urql/svelte';

export const client = new Client({
  url: 'https://api.example.***/graphql',
  exchanges: [cacheExchange, fetchExchange],
  fetchOptions: () => {
    const token = localStorage.getItem('auth_token');
    return {
      headers: {
        authorization: token ? `Bearer ${token}` : '',
      },
    };
  },
});

在这个配置中,我们使用了两个核心的交换器:cacheExchange提供了默认的文档缓存功能,而fetchExchange负责实际发送HTTP请求。你可以根据需要添加更多的交换器,如认证、重试等。

fetchOptions配置允许我们自定义每个请求的选项。在这个例子中,我们从localStorage中获取认证令牌,并将其添加到请求头中。

接下来,在根组件(通常是src/routes/+page.sveltesrc/App.svelte)中提供客户端实例:

<script>
  import { setContextClient } from '@urql/svelte';
  import { client } from '$lib/client';

  setContextClient(client);
</script>

<slot />

setContextClient函数使用Svelte的上下文API将客户端实例提供给所有子组件。这样,在任何子组件中,我们都可以通过getContextClient函数获取客户端实例,而不需要手动传递。

执行查询操作

配置好客户端后,我们就可以开始执行GraphQL查询了。urql为Svelte提供了queryStore函数,它创建一个响应式存储,用于管理查询状态和结果。

让我们创建一个简单的Pokemon列表组件来演示查询操作。首先,创建src/lib/graphql/queries.js文件,定义我们的GraphQL查询:

import { gql } from '@urql/svelte';

export const GET_POKEMONS = gql`
  query GetPokemons($limit: Int!, $offset: Int!) {
    pokemons(limit: $limit, offset: $offset) {
      id
      name
      image
    }
  }
`;

然后,创建src/routes/pokemons/+page.svelte组件:

<script>
  import { queryStore, getContextClient } from '@urql/svelte';
  import { GET_POKEMONS } from '$lib/graphql/queries';

  let offset = 0;
  const limit = 10;
  
  const client = getContextClient();
  
  $: pokemonsQuery = queryStore({
    client,
    query: GET_POKEMONS,
    variables: { limit, offset },
    requestPolicy: 'cache-and-***work',
  });
  
  function nextPage() {
    offset += limit;
  }
  
  function prevPage() {
    offset = Math.max(0, offset - limit);
  }
</script>

<div class="pokemon-list">
  {#if $pokemonsQuery.fetching}
    <p>Loading...</p>
  {:else if $pokemonsQuery.error}
    <p>Error: {$pokemonsQuery.error.message}</p>
  {:else}
    <div class="grid">
      {#each $pokemonsQuery.data?.pokemons || [] as pokemon}
        <div class="card">
          <img src={pokemon.image} alt={pokemon.name} />
          <h3>{pokemon.name}</h3>
        </div>
      {/each}
    </div>
    
    <div class="pagination">
      <button on:click={prevPage} disabled={offset === 0}>Previous</button>
      <button on:click={nextPage}>Next</button>
    </div>
  {/if}
</div>

<style>
  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 1rem;
    margin: 1rem 0;
  }
  
  .card {
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 1rem;
    text-align: center;
  }
  
  .card img {
    max-width: 100px;
    height: 100px;
    object-fit: contain;
  }
  
  .pagination {
    display: flex;
    justify-content: center;
    gap: 1rem;
    margin: 1rem 0;
  }
  
  button {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    background: #007bff;
    color: white;
    cursor: pointer;
  }
  
  button:disabled {
    background: #***c;
    cursor: not-allowed;
  }
</style>

在这个例子中,我们使用queryStore创建了一个响应式存储来管理查询状态。queryStore接受客户端实例、GraphQL查询、变量和请求策略等参数。

requestPolicy选项控制查询如何与缓存交互。cache-and-***work策略会先从缓存中获取数据(如果可用),同时发送网络请求获取最新数据。其他可用的策略包括cache-first(只使用缓存)、***work-only(只使用网络)和cache-or-***work(先尝试缓存,失败则使用网络)。

通过Svelte的响应式声明$: pokemonsQuery = queryStore(...),当offsetlimit变化时,查询会自动重新执行。

查询结果存储在$pokemonsQuery中,它包含以下主要属性:

  • fetching: 布尔值,表示查询是否正在加载
  • data: 查询成功时返回的数据
  • error: 查询失败时返回的错误对象
  • stale: 布尔值,表示数据可能已过时

我们使用Svelte的条件渲染来处理不同的状态:加载中显示"Loading...",出错时显示错误信息,成功时则渲染Pokemon列表和分页按钮。

执行变更操作

除了查询数据,我们还需要能够执行变更操作(如创建、更新、删除数据)。urql提供了mutationStore函数来处理变更操作。

让我们创建一个添加新Pokemon的表单组件:

<script>
  import { mutationStore, getContextClient, gql } from '@urql/svelte';

  const client = getContextClient();
  
  let name = '';
  let image = '';
  
  const ADD_POKEMON = gql`
    mutation AddPokemon($name: String!, $image: String!) {
      addPokemon(name: $name, image: $image) {
        id
        name
        image
      }
    }
  `;
  
  function addPokemon() {
    const result = mutationStore({
      client,
      query: ADD_POKEMON,
      variables: { name, image },
    });
    
    result.then(({ data, error }) => {
      if (error) {
        console.error('Failed to add pokemon:', error);
      } else {
        console.log('Pokemon added su***essfully:', data.addPokemon);
        // 重置表单
        name = '';
        image = '';
        // 可以在这里触发重新查询,或者依赖缓存自动更新
      }
    });
  }
</script>

<form on:submit|preventDefault={addPokemon}>
  <div>
    <label for="name">Name:</label>
    <input
      type="text"
      id="name"
      bind:value={name}
      required
    />
  </div>
  
  <div>
    <label for="image">Image URL:</label>
    <input
      type="url"
      id="image"
      bind:value={image}
      required
    />
  </div>
  
  <button type="submit">Add Pokemon</button>
</form>

queryStore不同,mutationStore不会自动执行,而是需要我们手动调用。它返回一个Promise,当变更操作完成时解析。

需要注意的是,即使变更操作失败,Promise也不会 reject,而是会返回一个包含错误信息的结果对象。因此,我们需要在then回调中显式检查是否有错误。

执行变更后,urql的缓存系统会自动更新。如果变更操作返回的数据与缓存中的现有数据匹配,相关的查询组件会自动重新渲染以反映最新状态。

缓存管理

urql的缓存系统是其核心功能之一,它可以显著提高应用性能并减少不必要的网络请求。默认情况下,urql使用cacheExchange提供的文档缓存,它基于查询文档和变量来缓存结果。

文档缓存适用于简单的场景,但对于复杂的应用,你可能需要更强大的缓存策略。urql提供了graphcache交换器,它实现了规范化缓存,可以跟踪实体之间的关系并提供更精细的缓存控制。

要使用graphcache,首先需要安装相关的包:

npm install @urql/graphcache

然后,更新客户端配置:

import { Client, fetchExchange } from '@urql/svelte';
import { cacheExchange } from '@urql/graphcache';

// 定义类型策略,告诉graphcache如何识别实体
const cache = cacheExchange({
  keys: {
    Pokemon: (data) => data.id,
  },
});

export const client = new Client({
  url: 'https://api.example.***/graphql',
  exchanges: [cache, fetchExchange],
});

在这个配置中,我们使用@urql/graphcache提供的cacheExchange替代了默认的文档缓存。keys配置告诉graphcache如何识别不同类型的实体。对于Pokemon类型,我们使用id字段作为唯一标识符。

graphcache还支持更高级的功能,如自定义更新函数、乐观UI更新和部分结果处理等。你可以在graphcache文档中了解更多信息。

错误处理

在实际应用中,错误处理是不可或缺的一部分。urql提供了多种方式来处理GraphQL错误和网络错误。

首先,让我们了解urql的错误对象结构。urql将错误分为两类:网络错误和GraphQL错误。网络错误通常是由于无法连接到服务器或请求被阻止引起的。GraphQL错误则是服务器返回的错误,通常包含在响应的errors数组中。

urql将这两类错误合并为一个***binedError对象,它包含以下主要属性:

  • message: 错误消息
  • ***workError: 网络错误对象(如果有)
  • graphQLErrors: GraphQL错误数组(如果有)

我们可以在查询或变更操作中处理错误:

<script>
  import { queryStore, getContextClient, gql } from '@urql/svelte';

  const client = getContextClient();
  
  const GET_POKEMONS = gql`
    query GetPokemons($limit: Int!, $offset: Int!) {
      pokemons(limit: $limit, offset: $offset) {
        id
        name
        image
      }
    }
  `;
  
  const pokemonsQuery = queryStore({
    client,
    query: GET_POKEMONS,
    variables: { limit: 10, offset: 0 },
  });
  
  function handleError(error) {
    if (error.***workError) {
      // 处理网络错误
      alert('***work error: Please check your connection');
      console.error('***work error:', error.***workError);
    } else if (error.graphQLErrors) {
      // 处理GraphQL错误
      const errorMessages = error.graphQLErrors.map(e => e.message).join('\n');
      alert(`Error fetching data:\n${errorMessages}`);
      console.error('GraphQL errors:', error.graphQLErrors);
    }
  }
</script>

<div>
  {#if $pokemonsQuery.fetching}
    <p>Loading...</p>
  {:else if $pokemonsQuery.error}
    <div class="error">
      <p>Something went wrong!</p>
      <button on:click={handleError($pokemonsQuery.error)}>Show Details</button>
    </div>
  {:else if $pokemonsQuery.data}
    <!-- 渲染数据 -->
  {/if}
</div>

除了在组件中处理错误,我们还可以通过添加全局错误处理交换器来统一处理所有操作的错误:

import { Client, fetchExchange, cacheExchange } from '@urql/svelte';

const errorExchange = ({ forward }) => (ops$) => {
  return ops$.pipe(
    forward,
    (result) => {
      if (result.error) {
        // 全局错误处理逻辑
        console.error('Global error handler:', result.error);
        // 可以在这里显示全局错误提示
      }
      return result;
    }
  );
};

export const client = new Client({
  url: 'https://api.example.***/graphql',
  exchanges: [errorExchange, cacheExchange, fetchExchange],
});

错误交换器应该放在交换器链的前面,这样它可以捕获所有后续交换器产生的错误。

高级功能

urql提供了许多高级功能,可以帮助你构建更强大、更灵活的应用。以下是一些常用的高级功能:

认证

对于需要认证的应用,urql提供了authExchange来处理认证逻辑:

npm install @urql/exchange-auth
import { authExchange } from '@urql/exchange-auth';

const auth = authExchange({
  addAuthToOperation: ({ authState, operation }) => {
    if (!authState || !authState.token) {
      return operation;
    }
    
    return {
      ...operation,
      context: {
        ...operation.context,
        fetchOptions: {
          ...operation.context.fetchOptions,
          headers: {
            ...operation.context.fetchOptions?.headers,
            authorization: `Bearer ${authState.token}`,
          },
        },
      },
    };
  },
  willAuthError: ({ authState }) => {
    return !authState || Date.now() > authState.expiresAt;
  },
  didAuthError: ({ error }) => {
    return error.graphQLErrors.some(e => e.extensions.code === 'FORBIDDEN');
  },
  getAuth: async ({ authState }) => {
    if (!authState) {
      // 尝试从存储中获取现有令牌
      const token = localStorage.getItem('token');
      const expiresAt = localStorage.getItem('expiresAt');
      if (token && expiresAt) {
        return { token, expiresAt: Number(expiresAt) };
      }
      return null;
    }
    
    // 刷新令牌
    const response = await fetch('/refresh-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken: authState.refreshToken }),
    });
    
    const newTokens = await response.json();
    localStorage.setItem('token', newTokens.token);
    localStorage.setItem('expiresAt', newTokens.expiresAt);
    localStorage.setItem('refreshToken', newTokens.refreshToken);
    
    return {
      token: newTokens.token,
      expiresAt: newTokens.expiresAt,
      refreshToken: newTokens.refreshToken,
    };
  },
});

export const client = new Client({
  url: 'https://api.example.***/graphql',
  exchanges: [auth, cacheExchange, fetchExchange],
});

订阅

对于需要实时更新的应用,urql支持GraphQL订阅。订阅通常使用WebSocket协议,可以通过subscriptionExchange实现:

npm install @urql/exchange-subscription subscriptions-transport-ws
import { subscriptionExchange } from '@urql/exchange-subscription';
import { SubscriptionClient } from 'subscriptions-transport-ws';

const subscriptionClient = new SubscriptionClient('wss://api.example.***/graphql', {
  reconnect: true,
  connectionParams: {
    authorization: `Bearer ${localStorage.getItem('token')}`,
  },
});

const subscription = subscriptionExchange({
  forwardSubscription: (operation) => subscriptionClient.request(operation),
});

export const client = new Client({
  url: 'https://api.example.***/graphql',
  exchanges: [
    cacheExchange,
    fetchExchange,
    subscription,
  ],
});

然后,在组件中使用subscriptionStore来订阅实时更新:

<script>
  import { subscriptionStore, getContextClient, gql } from '@urql/svelte';

  const client = getContextClient();
  
  const POKEMON_ADDED = gql`
    subscription OnPokemonAdded {
      pokemonAdded {
        id
        name
        image
      }
    }
  `;
  
  const pokemonAddedSubscription = subscriptionStore({
    client,
    query: POKEMON_ADDED,
  });
</script>

{#if $pokemonAddedSubscription.data}
  <div class="notification">
    New Pokemon added: {$pokemonAddedSubscription.data.pokemonAdded.name}
  </div>
{/if}

重试

对于偶尔会失败的请求,urql提供了retryExchange来自动重试请求:

npm install @urql/exchange-retry
import { retryExchange } from '@urql/exchange-retry';

const retry = retryExchange({
  initial: 1000, // 初始延迟(毫秒)
  max: 10000, // 最大延迟(毫秒)
  random: true, // 是否添加随机延迟
  attempts: {
    max: 5, // 最大重试次数
    retryIf: (error, _operation) => !!error && error.***workError != null,
  },
});

export const client = new Client({
  url: 'https://api.example.***/graphql',
  exchanges: [retry, cacheExchange, fetchExchange],
});

性能优化

为了确保应用的最佳性能,我们可以采取以下优化措施:

  1. 合理使用缓存策略:根据数据的更新频率和重要性,选择合适的请求策略。对于不常变化的数据,使用cache-first策略;对于频繁变化的数据,使用***work-onlycache-and-***work策略。

  2. 分页和懒加载:对于大量数据,使用分页或无限滚动来减少初始加载时间。urql的文档缓存会自动缓存部分结果,提高后续加载性能。

  3. 查询合并:尽可能合并多个相关的查询,减少网络请求次数。

  4. 避免过度获取:只请求应用需要的字段,减少数据传输量。

  5. 使用pause选项:对于不需要立即执行的查询,可以使用pause选项暂停,直到满足特定条件时再恢复。

<script>
  import { queryStore, getContextClient, gql } from '@urql/svelte';

  const client = getContextClient();
  let pause = true;
  let searchTerm = '';
  
  const SEARCH_POKEMONS = gql`
    query SearchPokemons($name: String!) {
      searchPokemons(name: $name) {
        id
        name
        image
      }
    }
  `;
  
  $: pokemonSearch = queryStore({
    client,
    query: SEARCH_POKEMONS,
    variables: { name: searchTerm },
    pause,
  });
  
  function handleSearch() {
    if (searchTerm.length > 2) {
      pause = false;
    } else {
      pause = true;
    }
  }
</script>

<input
  type="text"
  bind:value={searchTerm}
  on:input={handleSearch}
  placeholder="Search Pokemon (min. 3 characters)"
/>

{#if !pause && $pokemonSearch.fetching}
  <p>Searching...</p>
{:else if !pause && $pokemonSearch.data}
  <ul>
    {#each $pokemonSearch.data.searchPokemons as pokemon}
      <li>{pokemon.name}</li>
    {/each}
  </ul>
{/if}

总结

通过本文,我们学习了如何在Svelte应用中集成和使用urql客户端。从环境搭建、客户端配置,到执行查询和变更操作,再到处理错误和性能优化,我们覆盖了urql的核心功能和最佳实践。

urql的模块化设计和灵活的交换器系统使其成为构建各种规模应用的理想选择。与Svelte的响应式系统相结合,urql可以帮助你构建高效、可维护的前端应用。

要深入了解urql的更多功能,建议查阅官方文档:

  • urql文档
  • Svelte绑定文档
  • Graphcache文档

urql的源代码也可以在GitHub仓库中找到,如果你有兴趣贡献或了解更多实现细节,可以查看urql GitHub仓库。

希望本文能够帮助你更好地理解和使用urql与Svelte,构建出色的前端应用!

【免费下载链接】urql The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow. 项目地址: https://gitcode.***/gh_mirrors/ur/urql

转载请说明出处内容投诉
CSS教程网 » urql与Svelte集成教程:轻量级前端框架的GraphQL最佳搭档

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买