把 The Graph 接入 Vue 3 项目并不是简单发一个 GraphQL 请求就能结束,工程化层面要考虑的事情远多于“能不能拿到数据”。你需要设计稳定的数据访问层、统一处理异步状态、让类型系统真正发挥作用,还要避免组件卸载后残留的订阅或轮询定时器造成内存泄漏。下面从技术选型到代码落地,逐步拆解一套可维护的集成方案。

一、The Graph 与 Vue 3 的结合点在哪里
The Graph 的核心思路是把链上事件和合约状态提前索引到子图数据库中,应用只需要通过 GraphQL 端点查询已经整理好的数据,而不用逐个区块扫描或者拼接多次 RPC 调用。对于前端来说,这相当于把原本复杂的链上数据访问抽象成了一个普通的 API 服务。Vue 3 的响应式系统可以让 GraphQL 查询结果直接变成组件里的响应式状态,而不需要手动处理数据同步。
一个典型的场景是:你在 Vue 3 页面上展示 Uniswap 的流动性池列表。如果用 ethers.js 直接查询合约,需要先获取所有池子地址,再逐个调用合约方法,代码里会充满循环、异步等待和错误处理。换成 The Graph 后,只需要一条 GraphQL 查询语句,就能拿到字段齐全的结构化数据。Vue 3 的 ref 和 computed 可以让这些数据自动驱动视图更新,而 watch 又能方便地监听查询参数变化并重新拉取数据。
但这并不意味着在 Vue 3 里集成 The Graph 就毫无门槛。GraphQL 客户端的初始化、请求策略、缓存失效、类型生成、组件生命周期清理等问题都需要提前规划。如果只是零散地在各个组件里写 fetch 请求,项目规模变大后维护成本会迅速升高。工程化的目标就是把这些散落的逻辑收敛到一个数据访问层里,让组件只负责声明“我要什么数据”,而不关心“数据怎么来”。
二、项目初始化与 GraphQL 客户端配置
在 Vite 创建的 Vue 3 项目中,推荐使用 Apollo Client 作为 GraphQL 客户端。Apollo 的缓存能力、错误处理和 Devtools 支持都比较成熟,而且 Vue 生态里有 @vue/apollo-composable 这个官方维护的组合式 API 封装。如果你想更轻量一点,urql 也是一个不错的替代方案,它的包体积更小,而且默认支持缓存和文档缓存策略。
安装依赖时建议按作用拆分成两组:核心客户端和代码生成工具。核心客户端只需要 @apollo/client 和 graphql,如果使用组合式 API 还需要 @vue/apollo-composable。代码生成工具可以后面再装,避免开发依赖和生产依赖混在一起。
npm install @apollo/client graphql @vue/apollo-composable npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
客户端配置应该独立成一个模块,不要写在 main.ts 里。新建 src/graphql/client.ts,把子图的 GraphQL 端点地址通过环境变量注入。这样在切换主网、测试网或者自有子图时,只需要修改 .env 文件,不用动代码。
import { ApolloClient, InMemoryCache } from '@apollo/client/core'
const uri = import.meta.env.VITE_SUBGRAPH_URL || 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
export const apolloClient = new ApolloClient({
uri,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-first',
nextFetchPolicy: 'cache-first'
},
query: {
fetchPolicy: 'network-only'
}
}
})
这里把普通 query 的默认策略设为 network-only,原因是链上数据变化频繁,如果组件刷新时仍然读缓存,用户可能看到过时的流动性数据。而 watchQuery 用于组合式 API 的响应式查询,保留缓存优先策略可以减少不必要的网络请求。实际项目中可以根据业务需求调整这两个策略。
三、封装 useQuery 组合式函数统一处理异步状态
GraphQL 查询在组件中会带来三个状态:加载中、出错、成功拿到数据。如果每个组件都自己写三套 v-if 和错误提示,代码会非常重复。更好的做法是封装一个 useGraphQuery 组合式函数,把状态管理、重新加载、轮询等逻辑集中起来。
下面这个封装基于 @vue/apollo-composable 的 useQuery,额外增加了类型约束和错误消息标准化。组件只需要传入 GraphQL 文档和变量,就能获得统一的响应式状态。
import { useQuery } from '@vue/apollo-composable'
import { computed, watch, onBeforeUnmount } from 'vue'
export function useGraphQuery(query, variables = {}, options = {}) {
const { result, loading, error, refetch, onResult, onError, stop, start } = useQuery(query, variables, options)
const data = computed(() => result.value?.data ?? null)
const errorMessage = computed(() => {
if (!error.value) return ''
if (Array.isArray(error.value.graphQLErrors) && error.value.graphQLErrors.length) {
return error.value.graphQLErrors[0].message
}
return error.value.message || '查询失败'
})
const pollInterval = options.pollInterval || 0
let timer = null
watch(pollInterval, startPolling, { immediate: true })
function startPolling(interval) {
stopPolling()
if (interval > 0) {
timer = setInterval(() => refetch(), interval)
}
}
function stopPolling() {
if (timer) {
clearInterval(timer)
timer = null
}
}
onBeforeUnmount(() => {
stopPolling()
stop()
})
return { data, loading, error, errorMessage, refetch, onResult, onError, start, stop }
}
这段代码有几个值得注意的细节。第一,轮询定时器必须在组件卸载时清除,否则即使页面切换,定时器还会继续触发请求。第二,stop() 是 Apollo 组合式 API 提供的停止订阅方法,调用后可以释放响应式连接。第三,错误消息提取了 graphQLErrors 里的第一条信息,这样组件不需要关心错误对象的具体结构,直接展示 errorMessage 即可。
在组件中使用这个封装会非常直观。你只需要声明查询和变量,剩下的交给组合式函数。
import { gql } from '@apollo/client/core'
import { useGraphQuery } from '@/graphql/useGraphQuery'
const POOLS_QUERY = gql`
query Pools($first: Int!) {
pools(first: $first, orderBy: totalValueLockedUSD, orderDirection: desc) {
id
token0 { symbol }
token1 { symbol }
totalValueLockedUSD
}
}
`
const { data, loading, errorMessage, refetch } = useGraphQuery(POOLS_QUERY, { first: 20 }, { pollInterval: 30000 })
组件模板里直接绑定 loading、errorMessage 和 data 就能完成界面渲染。轮询每 30 秒执行一次,如果用户离开页面,定时器会自动清理,不会产生后台请求。
四、利用 GraphQL Code Generator 实现类型安全
类型安全是工程化集成中最容易被忽略但收益最高的一环。子图的 schema 往往包含几十上百个实体和字段,如果手动维护 TypeScript 类型,很容易出现字段拼写错误或者类型不一致。GraphQL Code Generator 可以根据子图端点的 schema 和本地查询文档自动生成类型定义。
首先在项目根目录创建 codegen.yml 配置文件,指定 schema 地址、查询文件位置和输出目标。你需要把子图的 GraphQL 端点替换成实际地址。
schema: ${VITE_SUBGRAPH_URL}
documents: 'src/graphql/**/*.gql'
generates:
src/graphql/generated-types.ts:
plugins:
- typescript
- typescript-operations
config:
avoidOptionals: true
maybeValue: T | null
然后执行 npx graphql-codegen,工具会拉取远端 schema,解析 .gql 文件里的查询和片段,生成一个包含所有查询类型、变量类型和返回类型的 TypeScript 文件。把查询文档独立成 .gql 文件而不是写在 gql 模板字符串里,是为了让 codegen 能静态识别查询内容。
query Pools($first: Int!) {
pools(first: $first, orderBy: totalValueLockedUSD, orderDirection: desc) {
id
token0 { symbol }
token1 { symbol }
totalValueLockedUSD
}
}
生成后在 useGraphQuery 中可以进一步约束泛型参数,让组件在调用时获得完整的类型提示。如果你的组合式函数没有使用泛型,也可以在组件里用 as 断言来处理生成的类型,但更优雅的做法是直接修改封装的签名,接收查询文档类型和变量类型作为泛型。这样在 data.value.pools 上就能自动补全字段,编译期就能发现拼写错误。
五、缓存策略与实时数据更新
The Graph 的子图数据更新存在一定延迟,因为索引器需要等待链上出块并处理事件。对于价格、流动性等高频变化的信息,前端通常采用轮询或订阅来保持数据新鲜。Apollo 的缓存机制可以和 Vue 3 的响应式系统配合,让数据在组件间共享,避免重复请求。
轮询方案已经在 useGraphQuery 中实现,适合大多数 DApp 场景。但如果你需要更快的数据推送,可以检查子图是否支持 GraphQL subscriptions。The Graph 托管服务目前对订阅的支持有限,自建节点或使用支持 WebSocket 的子图服务时才能启用。订阅的优势在于数据变化时服务端主动推送,前端只需要建立一次连接,不需要定时轮询。
缓存更新方面,Apollo 的 InMemoryCache 默认使用 id 字段作为实体标识。The Graph 返回的实体通常带有 id,所以缓存可以自动合并同一条记录。但如果查询里混用了不同层级的字段,或者使用了别名,缓存行为可能会不符合预期。这时可以通过 typePolicies 为特定类型自定义键字段和字段读取规则。
const cache = new InMemoryCache({
typePolicies: {
Pool: {
keyFields: ['id'],
fields: {
totalValueLockedUSD: {
read(value) {
return value ? Number(value) : 0
}
}
}
}
}
})
工程化集成不是一劳永逸的事情。随着子图版本升级、查询复杂度增加,你还需要考虑错误重试、请求取消、分页加载、响应式变量管理等进阶问题。但只要把数据访问层设计好,这些变化都被隔离在组合式函数内部,组件层几乎不需要修改。Vue 3 的组合式 API 和 Apollo 的查询模型天然契合,适配 The Graph 只是把 GraphQL 端点换成子图地址,剩下的大部分工作都是围绕类型和状态管理做工程化封装。最终你的团队成员只需要写查询文档,就能拿到类型安全、带缓存和轮询能力的响应式数据,链上索引接入成本会大幅降低。