在微服务架构日益普及的今天,客户端往往需要调用多个后端服务才能获取完整的数据。传统的 REST API 面临”过度获取”和”获取不足”的两难困境,而单一 GraphQL Schema 又难以支撑大规模分布式团队。GraphQL Federation 2.0 正是解决这一痛点的最佳方案——它将多个独立 GraphQL 服务组合成一个统一的图 API,让客户端只需一次查询就能跨服务获取所需数据。
本文将深入解析 Federation 2.0 的核心原理、完整实战代码、性能优化策略以及生产环境部署要点,帮助你从零构建一个高可用的分布式图 API 架构。
1. 为什么需要 Federation?
1.1 微服务 API 的痛点
假设一个电商系统,用户信息、订单数据、商品目录分别由三个独立微服务管理。客户端要渲染一个”用户订单详情页”,需要:
- 调用
GET /users/{id}获取用户信息 - 调用
GET /orders?userId={id}获取订单列表 - 对每个订单调用
GET /products/{productId}获取商品详情
这种”瀑布式”调用带来严重的性能瓶颈:N+1 查询问题、多次网络往返、客户端逻辑臃肿。
1.2 Federation 的解决方案
Federation 允许每个微服务定义自己领域的 GraphQL Schema 子图(Subgraph),然后通过 @key 指令声明实体关联,Federation Router(路由器)自动将它们组合成一个统一的超级图(Supergraph)。客户端只需发送一次查询,Router 负责将请求分发到各个子图并合并结果。
2. Federation 2.0 核心概念
2.1 架构概览
┌─────────────────────────────────────────────────┐
│ Client App │
│ (Web / Mobile / Third-party) │
└───────────────────┬─────────────────────────────┘
│ Single GraphQL Query
▼
┌─────────────────────────────────────────────────┐
│ Apollo Router / Router │
│ (Query Planning → Execution → Merging) │
└───┬──────────────┬──────────────┬───────────────┘
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────────┐
│ User │ │ Order │ │ Product │
│Subgraph│ │Subgraph│ │ Subgraph │
│:4001 │ │:4002 │ │ :4003 │
└────────┘ └────────┘ └────────────┘
PostgreSQL MongoDB Elasticsearch
2.2 关键术语
- Subgraph(子图):独立的 GraphQL 服务,负责一个业务领域的数据和逻辑
- Supergraph(超级图):所有子图的逻辑组合,对客户端暴露的统一 Schema
- @key 指令:声明实体的唯一标识符,用于跨图实体引用和查询编排
- @shareable:允许同一类型在多个子图中定义,避免字段归属冲突
- @external:声明一个字段由其他子图提供,本图仅引用
- @requires:声明跨子图的字段依赖,实现计算字段的按需获取
- @provides:优化查询计划,声明本图可以”顺便”返回的字段
- Entities(实体):通过
@key标记、可跨子图引用的对象类型
2.3 Federation 2.0 vs 1.0 的关键改进
| 特性 | Federation 1.0 | Federation 2.0 |
|---|---|---|
| Schema 组合 | 手动拼接,易冲突 | @composeSchema 自动合并,冲突检测 |
| 接口互操作 | 不支持 | @interfaceObject 支持跨图接口实现 |
| 类型共享 | 所有字段必须归属单一子图 | @shareable 允许多子图共享受限类型 |
| 查询优化 | 基础查询计划 | @requires/@provides 驱动的智能优化器 |
| Router | Apollo Gateway(已弃用) | Apollo Router(Rust 编写,性能提升 10x) |
| Subgraph 规范 | 松散 | 严格的 __schema 查询和 linting 工具链 |
3. 实战:构建电商超级图
3.1 项目初始化
# 创建项目结构
mkdir graphql-federation-demo && cd graphql-federation-demo
# 初始化三个子图服务
mkdir -p user-subgraph order-subgraph product-subgraph
# User Subgraph
cd user-subgraph
npm init -y
npm install @apollo/subgraph graphql graphql-tag
cd ..
# Order Subgraph
cd order-subgraph
npm init -y
npm install @apollo/subgraph graphql graphql-tag
cd ..
# Product Subgraph
cd product-subgraph
npm init -y
npm install @apollo/subgraph graphql graphql-tag
cd ..
3.2 User 子图(用户服务)
// user-subgraph/schema.js
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { gql } = require('graphql-tag');
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable"])
type Query {
me: User
user(id: ID!): User
}
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
memberSince: String!
avatarUrl: String
}
`;
const resolvers = {
Query: {
me: () => ({ id: "1", name: "张三", email: "zhangsan@example.com", memberSince: "2024-01-15" }),
user: (_, { id }) => {
const users = {
"1": { id: "1", name: "张三", email: "zhangsan@example.com", memberSince: "2024-01-15" },
"2": { id: "2", name: "李四", email: "lisi@example.com", memberSince: "2024-03-20" },
};
return users[id] || null;
},
},
User: {
__resolveReference: (user) => {
// 解析来自其他子图的实体引用
const users = {
"1": { id: "1", name: "张三", email: "zhangsan@example.com", memberSince: "2024-01-15" },
"2": { id: "2", name: "李四", email: "lisi@example.com", memberSince: "2024-03-20" },
};
return users[user.id] || null;
},
},
};
const schema = buildSubgraphSchema({ typeDefs, resolvers });
module.exports = schema;
3.3 Order 子图(订单服务)
// order-subgraph/schema.js
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { gql } = require('graphql-tag');
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@requires"])
type Query {
order(id: ID!): Order
}
type Order @key(fields: "id") {
id: ID!
userId: ID! @external
user: User! @requires(fields: "userId")
items: [OrderItem!]!
totalAmount: Float!
status: OrderStatus!
createdAt: String!
}
type OrderItem {
productId: ID!
quantity: Int!
unitPrice: Float!
product: Product @external
}
# 跨子图类型引用
type User @key(fields: "id") {
id: ID! @external
orders: [Order!]!
}
type Product @key(fields: "id") {
id: ID! @external
price: Float! @external
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
`;
const resolvers = {
Query: {
order: (_, { id }) => {
const orders = {
"ord-1": {
id: "ord-1", userId: "1",
items: [
{ productId: "p1", quantity: 2, unitPrice: 99.9 },
{ productId: "p2", quantity: 1, unitPrice: 259.0 },
],
totalAmount: 458.8, status: "CONFIRMED", createdAt: "2026-06-01T10:30:00Z"
},
"ord-2": {
id: "ord-2", userId: "2",
items: [{ productId: "p1", quantity: 1, unitPrice: 99.9 }],
totalAmount: 99.9, status: "DELIVERED", createdAt: "2026-05-20T14:00:00Z"
},
};
return orders[id] || null;
},
},
Order: {
__resolveReference: (order) => {
const orders = {
"ord-1": {
id: "ord-1", userId: "1",
items: [
{ productId: "p1", quantity: 2, unitPrice: 99.9 },
{ productId: "p2", quantity: 1, unitPrice: 259.0 },
],
totalAmount: 458.8, status: "CONFIRMED", createdAt: "2026-06-01T10:30:00Z"
},
};
return orders[order.id] || null;
},
user: (order) => ({ __typename: "User", id: order.userId }),
},
User: {
orders: (user) => {
const userOrders = {
"1": [{
id: "ord-1", userId: "1",
items: [{ productId: "p1", quantity: 2, unitPrice: 99.9 }],
totalAmount: 458.8, status: "CONFIRMED", createdAt: "2026-06-01T10:30:00Z"
}],
};
return userOrders[user.id] || [];
},
},
OrderItem: {
product: (item) => ({ __typename: "Product", id: item.productId }),
},
};
const schema = buildSubgraphSchema({ typeDefs, resolvers });
module.exports = schema;
3.4 Product 子图(商品服务)
// product-subgraph/schema.js
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { gql } = require('graphql-tag');
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@tag"])
type Query {
product(id: ID!): Product
products(ids: [ID!]!): [Product!]!
searchProducts(keyword: String!, limit: Int = 10): [Product!]!
}
type Product @key(fields: "id") {
id: ID!
name: String!
description: String!
price: Float! @tag(name: "pricing")
category: String!
tags: [String!]!
inStock: Boolean!
reviewSummary: ReviewSummary
}
type ReviewSummary {
averageRating: Float!
totalReviews: Int!
}
`;
const resolvers = {
Query: {
product: (_, { id }) => {
const products = {
"p1": {
id: "p1", name: "机械键盘 K8", description: "87键热插拔机械键盘,RGB背光",
price: 99.9, category: "电脑外设", tags: ["机械键盘", "RGB", "热插拔"],
inStock: true, reviewSummary: { averageRating: 4.7, totalReviews: 1283 }
},
"p2": {
id: "p2", name: "4K显示器 27寸", description: "IPS面板,144Hz,HDR400",
price: 259.0, category: "显示器", tags: ["4K", "144Hz", "HDR"],
inStock: true, reviewSummary: { averageRating: 4.5, totalReviews: 876 }
},
};
return products[id] || null;
},
products: (_, { ids }) => {
const products = {
"p1": { id: "p1", name: "机械键盘 K8", price: 99.9, category: "电脑外设",
tags: ["机械键盘", "RGB"], inStock: true,
reviewSummary: { averageRating: 4.7, totalReviews: 1283 } },
"p2": { id: "p2", name: "4K显示器 27寸", price: 259.0, category: "显示器",
tags: ["4K", "144Hz"], inStock: true,
reviewSummary: { averageRating: 4.5, totalReviews: 876 } },
};
return ids.map(id => products[id]).filter(Boolean);
},
searchProducts: (_, { keyword, limit }) => {
// 简化的搜索逻辑
const allProducts = [
{ id: "p1", name: "机械键盘 K8", description: "87键热插拔机械键盘",
price: 99.9, category: "电脑外设", tags: ["机械键盘", "RGB"],
inStock: true, reviewSummary: { averageRating: 4.7, totalReviews: 1283 } },
];
return allProducts.filter(p =>
p.name.includes(keyword) || p.description.includes(keyword)
).slice(0, limit);
},
},
Product: {
__resolveReference: (ref) => {
const products = {
"p1": { id: "p1", name: "机械键盘 K8", description: "87键热插拔机械键盘",
price: 99.9, category: "电脑外设", tags: ["机械键盘", "RGB"],
inStock: true, reviewSummary: { averageRating: 4.7, totalReviews: 1283 } },
"p2": { id: "p2", name: "4K显示器 27寸", description: "IPS面板,144Hz",
price: 259.0, category: "显示器", tags: ["4K", "144Hz"],
inStock: true, reviewSummary: { averageRating: 4.5, totalReviews: 876 } },
};
return products[ref.id] || null;
},
},
};
const schema = buildSubgraphSchema({ typeDefs, resolvers });
module.exports = schema;
3.5 各子图服务入口
// user-subgraph/index.js
const { ApolloServer } = require('apollo-server');
const schema = require('./schema');
const server = new ApolloServer({
schema,
introspection: true,
});
server.listen({ port: 4001 }).then(({ url }) => {
console.log(`🚀 User Subgraph ready at ${url}`);
});
// order-subgraph/index.js
const { ApolloServer } = require('apollo-server');
const schema = require('./schema');
const server = new ApolloServer({
schema,
introspection: true,
});
server.listen({ port: 4002 }).then(({ url }) => {
console.log(`🚀 Order Subgraph ready at ${url}`);
});
// product-subgraph/index.js
const { ApolloServer } = require('apollo-server');
const schema = require('./schema');
const server = new ApolloServer({
schema,
introspection: true,
});
server.listen({ port: 4003 }).then(({ url }) => {
console.log(`🚀 Product Subgraph ready at ${url}`);
});
3.6 Apollo Router 配置
# router.yaml - Apollo Router 配置文件
supergraph:
listen: 0.0.0.0:4000
path: /graphql
introspection: true
subgraphs:
user:
routing_url: http://localhost:4001/graphql
schema:
file: ./user-subgraph/schema.graphql
order:
routing_url: http://localhost:4002/graphql
schema:
file: ./order-subgraph/schema.graphql
product:
routing_url: http://localhost:4003/graphql
schema:
file: ./product-subgraph/schema.graphql
# 性能配置
traffic_shaping:
all:
timeout: 30s
subgraphs:
product:
timeout: 10s # 商品服务超时较短,快速降级
# 缓存配置
apq:
enabled: true # 自动持久化查询缓存
router:
cache:
in_memory:
limit: 1000
# 可观测性
telemetry:
tracing:
trace_config:
service_name: "federation-router"
service_namespace: "ecommerce"
jaeger:
agent:
endpoint: "localhost:6831"
metrics:
prometheus:
enabled: true
listen: 0.0.0.0:9090
# CORS
cors:
origins:
- https://shop.example.com
- https://admin.example.com
methods: [GET, POST, OPTIONS]
allow_headers: [Authorization, Content-Type, X-Request-Id]
4. 跨子图实体引用与查询计划
4.1 理解查询计划执行流程
当客户端发送如下跨子图查询时:
query GetUserOrders($userId: ID!) {
user(id: $userId) {
name
email
orders {
id
totalAmount
status
items {
quantity
unitPrice
product {
name
price
reviewSummary {
averageRating
}
}
}
}
}
}
Apollo Router 的查询计划器会生成如下执行计划:
QueryPlan {
Sequence {
# 第一步:从 User 子图获取用户基本信息
Fetch(service: "user") {
query: "{ user(id: $userId) { name email } }"
}
# 第二步:从 Order 子图获取该用户的订单(依赖 user 返回的 id)
Fetch(service: "order") {
query: "{ user(id: $userId) { orders { id totalAmount status items { productId quantity unitPrice } } } }"
}
# 第三步:从 Product 子图批量获取商品详情
Flatten(path: "user.orders.@.items.@") {
Fetch(service: "product") {
query: "query($ids: [ID!]!) { products(ids: $ids) { name price reviewSummary { averageRating } } }"
}
}
}
}
关键优化点:Router 会自动将多个 OrderItem 的 productId 批量合并为一个 products 查询,避免 N+1 问题。
4.2 @requires 实现跨子图计算字段
假设我们需要在 Order 子图上提供一个 discountAmount 字段,计算逻辑需要依赖 Product 子图的价格数据:
// order-subgraph/schema.js 中的扩展
type OrderItem {
productId: ID!
quantity: Int!
unitPrice: Float! @external
product: Product @external
# 需要 unitPrice 和 product.price 才能计算折扣
discountAmount: Float!
@requires(fields: "unitPrice product { price }")
}
type Product @key(fields: "id") {
id: ID! @external
price: Float! @external
}
// Resolver
OrderItem: {
discountAmount: (item) => {
const originalPrice = item.product.price;
const actualPrice = item.unitPrice;
const discount = (originalPrice - actualPrice) * item.quantity;
return Math.max(0, discount);
},
}
这个机制让子图可以声明性地表达跨服务依赖,Router 自动编排数据获取顺序。
5. 性能优化策略
5.1 批量解析器模式(DataLoader)
即使 Router 会做查询合并,子图内部的 N+1 问题仍需处理:
// product-subgraph/dataloader.js
const DataLoader = require('dataloader');
// 模拟数据库查询
async function batchGetProducts(ids) {
console.log(`[Batch] Fetching ${ids.length} products: [${ids.join(', ')}]`);
// 实际项目中这里是数据库批量查询
const productMap = {
"p1": { id: "p1", name: "机械键盘 K8", price: 99.9 },
"p2": { id: "p2", name: "4K显示器 27寸", price: 259.0 },
};
return ids.map(id => productMap[id] || null);
}
// 创建 per-request DataLoader
function createProductLoader() {
return new DataLoader(batchGetProducts, {
maxBatchSize: 100, // 最大批量大小
batchScheduleFn: callback => setTimeout(callback, 5), // 5ms 窗口期
cache: true, // 请求内缓存
});
}
module.exports = { createProductLoader };
// 在 Resolver 中使用
const { createProductLoader } = require('./dataloader');
const resolvers = {
Product: {
__resolveReference: async (ref, context) => {
const product = await context.productLoader.load(ref.id);
return product;
},
},
};
const server = new ApolloServer({
schema,
context: () => ({
productLoader: createProductLoader(), // 每个请求独立 DataLoader
}),
});
5.2 查询复杂度分析与深度限制
// router.yaml 中配置查询限制
supergraph:
listen: 0.0.0.0:4000
# 限制查询复杂度,防止恶意深度嵌套查询
limits:
max_depth: 10 # 最大查询深度
max_height: 50 # 最大字段总数
max_aliases: 20 # 最大别名数
max_root_fields: 5 # 最大根字段数
warn_only: false # 超出限制直接拒绝
# 或使用 @defer 实现流式响应
supergraph:
defer_support:
enabled: true
5.3 @defer 流式响应优化
Federation 2.0 支持 @defer 指令,让非关键字段延迟返回,提升首字节时间:
query ProductDetail($id: ID!) {
product(id: $id) {
name
price
description
# 评论数据可以延迟加载,不阻塞首屏
... @defer(label: "reviews") {
reviewSummary {
averageRating
totalReviews
}
}
}
}
Router 会先返回关键字段的响应,待评论数据准备好后再通过增量响应推送。
5.4 自动持久化查询(APQ)
APQ 将 GraphQL 查询字符串哈希后缓存,客户端只需发送哈希值,显著减少网络传输和解析开销:
// 客户端使用 APQ(@apollo/client 默认启用)
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const link = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true, // 使用 GET 请求,可 CDN 缓存
}).concat(new HttpLink({ uri: 'https://api.example.com/graphql' }));
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
6. 生产环境部署要点
6.1 Schema 变更管理
Federation 的一个关键优势是支持零宕机 Schema 变更。遵循以下流程:
# 1. 检查 Schema 兼容性(CI/CD 中集成)
rover subgraph check ecommerce-user \
--schema ./user-subgraph/schema.graphql \
--name user
# 2. 发布新的子图版本(旧版本仍可用)
# 3. Router 自动热加载新 Schema
# 4. 确认无兼容性破坏后,下线旧版本
# 使用 Rover CLI 做全局检查
rover supergraph compose \
--config ./supergraph-config.yaml \
--output ./supergraph.graphql
6.2 子图健康检查
# 每个子图实现健康检查端点
// user-subgraph/index.js
const { ApolloServer } = require('apollo-server-express');
const express = require('express');
const app = express();
// 就绪探针 - 检查 Schema 是否加载成功
app.get('/health/ready', async (req, res) => {
try {
// 执行一个轻量 introspection 查询
const result = await server.executeOperation({
query: '{ __typename }'
});
if (result.errors) {
res.status(503).json({ status: 'not ready', errors: result.errors });
} else {
res.json({ status: 'ready' });
}
} catch (e) {
res.status(503).json({ status: 'not ready', error: e.message });
}
});
// 存活探针
app.get('/health/live', (req, res) => {
res.json({ status: 'alive', uptime: process.uptime() });
});
6.3 分布式追踪
// 在子图中集成 OpenTelemetry
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { GraphQLInstrumentation } = require('@opentelemetry/instrumentation-graphql');
const provider = new NodeTracerProvider();
const jaegerExporter = new JaegerExporter({
endpoint: 'http://jaeger:14268/api/traces',
});
provider.addSpanProcessor(new BatchSpanProcessor(jaegerExporter));
provider.register();
// GraphQL 自动追踪
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
registerInstrumentations({
instrumentations: [
new GraphQLInstrumentation({
allowValues: true, // 记录变量值(注意隐私)
depth: 5, // 最大追踪深度
}),
],
});
6.4 安全加固
// router.yaml 安全配置
supergraph:
listen: 0.0.0.0:4000
# 速率限制
traffic_shaping:
all:
experimental_http_rate_limit:
capacity: 1000 # 每秒请求数上限
interval: 1s
# 认证头传递
headers:
all:
request:
- propagate:
named: authorization # 将 JWT 传递给所有子图
- remove:
named: x-internal-key # 移除内部密钥,防止泄露
# 禁止 introspection(生产环境)
supergraph:
introspection: false
# 查询白名单(生产环境推荐)
apq:
enabled: true
router:
cache:
in_memory:
limit: 5000 # 限制缓存的查询数量
7. 高级模式:接口与联合类型
7.1 跨子图接口实现
Federation 2.0 支持 @interfaceObject,让多个子图共同实现同一个接口:
// 公共接口定义(在 product-subgraph 中)
interface Searchable @key(fields: "id") {
id: ID!
searchScore(query: String!): Float!
}
type Product implements Searchable @key(fields: "id") {
id: ID!
name: String!
price: Float!
searchScore(query: String!): Float!
}
// 在 order-subgraph 中实现同一接口
type Order implements Searchable @key(fields: "id") @interfaceObject {
id: ID!
totalAmount: Float!
searchScore(query: String!): Float!
}
// 客户端可以统一查询
query SearchEverything($query: String!) {
search(query: $query) {
results {
... on Product { name price }
... on Order { id totalAmount }
}
}
}
7.2 自定义指令实现横切关注点
// 定义自定义指令
directive @auth(requires: Role!) on FIELD_DEFINITION
directive @cacheControl(maxAge: Int!) on FIELD_DEFINITION
directive @audit(action: String!) on FIELD_DEFINITION
enum Role {
ADMIN
USER
GUEST
}
// 在 Schema 中使用
type Product @key(fields: "id") {
id: ID!
name: String!
costPrice: Float! @auth(requires: ADMIN) # 仅管理员可见
price: Float! @cacheControl(maxAge: 3600) # 缓存1小时
margin: Float! @auth(requires: ADMIN) @audit(action: "view_margin")
}
// 通过 @link 导入自定义指令
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable", "@external", "@requires", "@provides"])
@link(url: "https://custom.dev/auth/v1.0", import: ["@auth"])
@link(url: "https://custom.dev/cache/v1.0", import: ["@cacheControl"])
8. 2026 年趋势展望
- Router 性能持续优化:Apollo Router 基于 Rust 实现,查询计划缓存、并行执行引擎持续迭代,2026 年目标是将复杂跨子图查询延迟降低到 5ms 以内
- 与 AI Agent 集成:Federation 超级图天然适合 AI Agent 调用——Agent 只需理解一个统一的 Schema,Router 自动编排跨服务调用
- @defer 广泛采用:随着客户端框架对
@defer的支持成熟,流式响应将成为 BFF 层标配 - Schema Registry 生态成熟:类似 Confluent Schema Registry 的 GraphQL Schema 注册中心将成为企业级部署标配
- 边缘计算 + Federation:将部分子图部署到边缘节点,Router 智能路由请求到最近的子图实例
- 实时数据 + Federation:GraphQL Subscription 与 Federation 深度集成,支持跨子图的实时数据推送
总结
GraphQL Federation 2.0 代表了微服务 API 层架构的最新演进方向。通过将多个独立子图组合为统一的超级图,它解决了微服务架构中最棘手的跨服务数据获取问题,同时保持了服务的独立性和团队自治。
核心要点回顾:
- 使用
@key声明实体,实现跨子图引用和数据编排 - 利用
@requires/@provides优化查询计划,减少不必要的子图调用 - DataLoader 批量解析器是子图内部性能优化的必备工具
- APQ +
@defer是生产环境性能优化的两大法宝 - Rover CLI + Schema Check 确保 Schema 变更的向后兼容性
- OpenTelemetry 追踪 + 速率限制 + 查询复杂度限制是生产部署的安全基线
如果你正在面临微服务 API 聚合的挑战,Federation 2.0 值得成为你的首选方案。从一个小的子图开始,逐步扩展,你会发现统一图 API 带来的开发体验提升是巨大的。