pin_drop当前位置:知识文库 ❯ 图文

后端通信:GraphQL基础 - 从入门到实践详解

教程简介

GraphQL是Facebook于2015年开源的查询语言和运行时,为API提供了一种更高效、更灵活的替代REST的方案。与REST的多个端点不同,GraphQL使用单一端点,客户端可以精确地声明需要的数据,避免了过度获取和获取不足的问题。GraphQL正在被越来越多的公司采用,成为现代Web应用的重要技术选择。

本教程将全面讲解GraphQL与REST的对比、Schema定义、Query查询、Mutation变更、Subscription订阅、变量与参数、片段(Fragment)、内省(Introspection)以及GraphQL客户端的使用。

核心概念

1. GraphQL vs REST

特性 REST GraphQL
端点 多个(/users, /orders) 单一(/graphql)
数据获取 服务器决定返回什么 客户端声明需要什么
过度获取 常见 避免
获取不足 需要多次请求 一次请求获取
版本管理 URL版本化 无需版本,可扩展
文档 需要额外维护 Schema即文档
缓存 HTTP缓存 需要额外方案
学习曲线 中等

过度获取示例

代码示例

// REST:获取用户列表,返回所有字段
GET /api/users
// 返回:[{id, name, email, phone, address, birthday, avatar, ...}]

// GraphQL:只获取需要的字段
query {
    users {
        id
        name
    }
}
// 返回:[{"id":1,"name":"张三"},{"id":2,"name":"李四"}]

获取不足示例

代码示例

// REST:需要两次请求
GET /api/users/1          // 获取用户
GET /api/users/1/orders   // 获取用户订单

// GraphQL:一次请求
query {
    user(id: 1) {
        name
        email
        orders {
            id
            total
            status
        }
    }
}

2. Schema定义

Schema是GraphQL的核心,定义了API的类型系统:

代码示例

# 标量类型
scalar DateTime

# 枚举类型
enum UserRole {
    ADMIN
    USER
    GUEST
}

enum OrderStatus {
    PENDING
    PAID
    SHIPPED
    CANCELLED
}

# 对象类型
type User {
    id: ID!
    name: String!
    email: String!
    role: UserRole!
    createdAt: DateTime!
    orders: [Order!]!
}

type Order {
    id: ID!
    user: User!
    items: [OrderItem!]!
    total: Float!
    status: OrderStatus!
    createdAt: DateTime!
}

type OrderItem {
    id: ID!
    product: Product!
    quantity: Int!
    price: Float!
}

type Product {
    id: ID!
    name: String!
    price: Float!
    stock: Int!
    category: Category!
}

type Category {
    id: ID!
    name: String!
    products: [Product!]!
}

# 查询入口
type Query {
    user(id: ID!): User
    users(page: Int, size: Int, role: UserRole): [User!]!
    order(id: ID!): Order
    orders(status: OrderStatus): [Order!]!
    product(id: ID!): Product
    products(categoryId: ID): [Product!]!
}

# 变更入口
type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
    deleteUser(id: ID!): Boolean!
    createOrder(input: CreateOrderInput!): Order!
    cancelOrder(id: ID!): Order!
}

# 订阅入口
type Subscription {
    orderStatusChanged(orderId: ID!): Order!
    newOrder: Order!
}

# 输入类型
input CreateUserInput {
    name: String!
    email: String!
    role: UserRole = USER
}

input UpdateUserInput {
    name: String
    email: String
    role: UserRole
}

input CreateOrderInput {
    userId: ID!
    items: [OrderItemInput!]!
}

input OrderItemInput {
    productId: ID!
    quantity: Int!
}

类型修饰符

符号 含义 示例
! 非空 String!
[] 列表 [User]
[!]! 非空列表,元素非空 [User!]!

3. Query查询

代码示例

# 基本查询
query {
    user(id: 1) {
        id
        name
        email
    }
}

# 嵌套查询
query {
    user(id: 1) {
        name
        orders {
            id
            total
            status
            items {
                product {
                    name
                    price
                }
                quantity
            }
        }
    }
}

# 列表查询
query {
    users(page: 1, size: 10) {
        id
        name
        role
    }
}

# 多个查询
query {
    user(id: 1) { name email }
    products(categoryId: 1) { id name price }
    orders(status: PAID) { id total }
}

4. Mutation变更

代码示例

# 创建用户
mutation {
    createUser(input: {
        name: "张三"
        email: "zhangsan@example.com"
        role: USER
    }) {
        id
        name
        email
        role
        createdAt
    }
}

# 更新用户
mutation {
    updateUser(id: 1, input: {
        email: "new@example.com"
    }) {
        id
        name
        email
    }
}

# 删除用户
mutation {
    deleteUser(id: 1)
}

# 创建订单
mutation {
    createOrder(input: {
        userId: 1
        items: [
            { productId: 1, quantity: 2 }
            { productId: 3, quantity: 1 }
        ]
    }) {
        id
        total
        status
        items {
            product { name }
            quantity
            price
        }
    }
}

5. Subscription订阅

代码示例

# 订阅订单状态变化
subscription {
    orderStatusChanged(orderId: 101) {
        id
        status
    }
}

# 订阅新订单
subscription {
    newOrder {
        id
        user { name }
        total
        createdAt
    }
}

6. 变量与参数

代码示例

# 使用变量
query GetUser($userId: ID!, $withOrders: Boolean!) {
    user(id: $userId) {
        id
        name
        email
        orders @include(if: $withOrders) {
            id
            total
        }
    }
}

# 变量值
{
    "userId": 1,
    "withOrders": true
}

指令

指令 说明
@include(if: Boolean) 条件包含
@skip(if: Boolean) 条件跳过
@deprecated(reason: String) 标记弃用

7. 片段(Fragment)

代码示例

# 定义片段
fragment UserFields on User {
    id
    name
    email
    role
}

fragment OrderFields on Order {
    id
    total
    status
    createdAt
}

# 使用片段
query {
    user(id: 1) {
        ...UserFields
        orders {
            ...OrderFields
            items {
                product { name }
                quantity
            }
        }
    }
    users {
        ...UserFields
    }
}

8. 内省(Introspection)

GraphQL支持查询自身Schema信息:

代码示例

# 查询所有类型
query {
    __schema {
        types {
            name
            kind
        }
    }
}

# 查询特定类型的字段
query {
    __type(name: "User") {
        name
        fields {
            name
            type {
                name
                kind
            }
        }
    }
}

9. GraphQL客户端

Apollo Client

代码示例

import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

const client = new ApolloClient({
    uri: 'https://api.example.com/graphql',
    cache: new InMemoryCache()
});

// 查询
const { data } = await client.query({
    query: gql`
        query GetUsers {
            users {
                id name email
            }
        }
    `
});

// 变更
const { data } = await client.mutate({
    mutation: gql`
        mutation CreateUser($input: CreateUserInput!) {
            createUser(input: $input) {
                id name email
            }
        }
    `,
    variables: {
        input: { name: '张三', email: 'zhangsan@example.com' }
    }
});

使用Fetch调用GraphQL

代码示例

async function graphqlRequest(query, variables = {}) {
    const response = await fetch('/graphql', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query, variables })
    });
    const result = await response.json();
    if (result.errors) {
        throw new Error(result.errors[0].message);
    }
    return result.data;
}

// 使用
const data = await graphqlRequest(`
    query GetUser($id: ID!) {
        user(id: $id) { id name email }
    }
`, { id: 1 });

代码示例

示例1:GraphQL查询构建器

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GraphQL查询构建器</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 1000px; margin: 0 auto; }
        h1 { text-align: center; color: #e535ab; margin-bottom: 24px; font-size: 28px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #e535ab; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #fce4ec; }
        .type-tabs { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
        .type-tab { padding: 8px 16px; border: 2px solid #e8eaed; border-radius: 8px; background: #fff; cursor: pointer; font-weight: 600; font-size: 13px; transition: all 0.2s; }
        .type-tab.active { background: #e535ab; color: #fff; border-color: #e535ab; }
        .field-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px; margin-bottom: 16px; }
        .field-item { padding: 10px; border: 1px solid #e8eaed; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s; display: flex; align-items: center; gap: 6px; }
        .field-item:hover { border-color: #e535ab; }
        .field-item.selected { background: #fce4ec; border-color: #e535ab; }
        .field-name { font-family: 'Consolas', monospace; font-weight: 600; }
        .field-type { font-size: 11px; color: #888; }
        .query-output { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 13px; line-height: 1.7; white-space: pre-wrap; min-height: 150px; }
        .operation-select { display: flex; gap: 8px; margin-bottom: 16px; }
        .op-btn { padding: 8px 16px; border: none; border-radius: 6px; font-weight: 700; cursor: pointer; font-size: 13px; color: #fff; }
        .op-query { background: #34a853; }
        .op-mutation { background: #ea4335; }
        .op-subscription { background: #1a73e8; }
        .op-btn.active { box-shadow: 0 0 0 3px rgba(0,0,0,0.2); }
        .args-area { margin-bottom: 16px; }
        .arg-row { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
        .arg-row label { font-weight: 600; font-size: 13px; min-width: 80px; }
        .arg-row input { padding: 6px 10px; border: 1px solid #dadce0; border-radius: 4px; font-size: 13px; flex: 1; }
    </style>
</head>
<body>
    <div class="container">
        <h1>GraphQL查询构建器</h1>
        <div class="card">
            <h2>选择操作类型</h2>
            <div class="operation-select">
                <div class="op-btn op-query active" onclick="setOp('query')">Query</div>
                <div class="op-btn op-mutation" onclick="setOp('mutation')">Mutation</div>
            </div>
        </div>
        <div class="card">
            <h2>选择查询和字段</h2>
            <div class="type-tabs" id="queryTabs"></div>
            <div class="args-area" id="argsArea"></div>
            <h3 style="font-size:14px;margin-bottom:8px;color:#555;">选择返回字段</h3>
            <div class="field-grid" id="fieldGrid"></div>
        </div>
        <div class="card">
            <h2>生成的GraphQL查询</h2>
            <div class="query-output" id="queryOutput">选择查询和字段后自动生成...</div>
        </div>
    </div>
    <script>
        const schema = {
            queries: {
                user: { args: { id: 'ID!' }, fields: { id: 'ID!', name: 'String!', email: 'String!', role: 'UserRole!', createdAt: 'DateTime!', orders: '[Order!]!' } },
                users: { args: { page: 'Int', size: 'Int', role: 'UserRole' }, fields: { id: 'ID!', name: 'String!', email: 'String!', role: 'UserRole!' } },
                order: { args: { id: 'ID!' }, fields: { id: 'ID!', total: 'Float!', status: 'OrderStatus!', createdAt: 'DateTime!', items: '[OrderItem!]!' } },
                product: { args: { id: 'ID!' }, fields: { id: 'ID!', name: 'String!', price: 'Float!', stock: 'Int!' } },
                products: { args: { categoryId: 'ID' }, fields: { id: 'ID!', name: 'String!', price: 'Float!', stock: 'Int!' } }
            },
            mutations: {
                createUser: { args: { name: 'String!', email: 'String!', role: 'UserRole' }, fields: { id: 'ID!', name: 'String!', email: 'String!', role: 'UserRole!', createdAt: 'DateTime!' } },
                updateUser: { args: { id: 'ID!', name: 'String', email: 'String' }, fields: { id: 'ID!', name: 'String!', email: 'String!' } },
                deleteUser: { args: { id: 'ID!' }, fields: { success: 'Boolean!' } },
                createOrder: { args: { userId: 'ID!', items: '[OrderItemInput!]!' }, fields: { id: 'ID!', total: 'Float!', status: 'OrderStatus!' } }
            }
        };

        let currentOp = 'query';
        let currentQuery = 'user';
        let selectedFields = new Set();

        function setOp(op) {
            currentOp = op;
            document.querySelectorAll('.op-btn').forEach(b => b.classList.remove('active'));
            event.currentTarget.classList.add('active');
            currentQuery = Object.keys(schema[op + 's'])[0];
            selectedFields.clear();
            renderQueryTabs();
            renderFields();
            generateQuery();
        }

        function renderQueryTabs() {
            const ops = schema[currentOp + 's'];
            document.getElementById('queryTabs').innerHTML = Object.keys(ops).map(q =>
                `<div class="type-tab ${q === currentQuery ? 'active' : ''}" onclick="selectQuery('${q}')">${q}</div>`
            ).join('');
            renderArgs();
        }

        function selectQuery(q) {
            currentQuery = q;
            selectedFields.clear();
            renderQueryTabs();
            renderFields();
            generateQuery();
        }

        function renderArgs() {
            const op = schema[currentOp + 's'][currentQuery];
            const args = op.args || {};
            document.getElementById('argsArea').innerHTML = Object.entries(args).map(([name, type]) => {
                const isRequired = type.includes('!');
                return `<div class="arg-row"><label>${name}${isRequired ? ' *' : ''}</label><input type="text" id="arg-${name}" placeholder="${type}" oninput="generateQuery()"></div>`;
            }).join('');
        }

        function renderFields() {
            const op = schema[currentOp + 's'][currentQuery];
            const fields = op.fields || {};
            document.getElementById('fieldGrid').innerHTML = Object.entries(fields).map(([name, type]) =>
                `<div class="field-item ${selectedFields.has(name) ? 'selected' : ''}" onclick="toggleField('${name}')">
                    <span class="field-name">${name}</span><span class="field-type">${type}</span>
                </div>`
            ).join('');
        }

        function toggleField(name) {
            if (selectedFields.has(name)) selectedFields.delete(name);
            else selectedFields.add(name);
            renderFields();
            generateQuery();
        }

        function generateQuery() {
            if (selectedFields.size === 0) {
                document.getElementById('queryOutput').textContent = '请选择至少一个字段';
                return;
            }

            const op = schema[currentOp + 's'][currentQuery];
            const args = op.args || {};
            const argEntries = Object.entries(args);

            let argStr = '';
            let varStr = '';
            if (argEntries.length > 0) {
                const vars = [];
                const argsList = [];
                argEntries.forEach(([name, type]) => {
                    const input = document.getElementById('arg-' + name);
                    const value = input ? input.value : '';
                    vars.push(`$${name}: ${type}`);
                    if (value) {
                        argsList.push(`${name}: ${value.match(/^\d+$/) ? value : `"${value}"`}`);
                    } else {
                        argsList.push(`${name}: $${name}`);
                    }
                });
                varStr = `(${vars.join(', ')})`;
                argStr = `(${argsList.join(', ')})`;
            }

            const fieldsStr = Array.from(selectedFields).join('\n        ');
            const query = `${currentOp} ${currentQuery.charAt(0).toUpperCase() + currentQuery.slice(1)}${varStr} {\n  ${currentQuery}${argStr} {\n    ${fieldsStr}\n  }\n}`;
            document.getElementById('queryOutput').textContent = query;
        }

        renderQueryTabs();
        renderFields();
    </script>
</body>
</html>

示例2:GraphQL与REST对比演示

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GraphQL与REST对比演示</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 1000px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
        .comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
        .panel { padding: 16px; border-radius: 8px; border: 2px solid #e8eaed; }
        .panel-rest { border-color: #34a853; }
        .panel-graphql { border-color: #e535ab; }
        .panel-title { font-weight: 800; font-size: 16px; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
        .rest-color { color: #34a853; }
        .graphql-color { color: #e535ab; }
        .code-block { background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 6px; font-family: 'Consolas', monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; margin-bottom: 10px; }
        .scenario-btns { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
        .scenario-btn { padding: 8px 16px; border: 2px solid #e8eaed; border-radius: 8px; background: #fff; cursor: pointer; font-weight: 600; font-size: 13px; transition: all 0.2s; }
        .scenario-btn.active { background: #1a73e8; color: #fff; border-color: #1a73e8; }
        .metric { display: flex; gap: 16px; margin-top: 8px; }
        .metric-item { padding: 6px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; }
        .metric-good { background: #e8f5e9; color: #2e7d32; }
        .metric-bad { background: #ffebee; color: #c62828; }
    </style>
</head>
<body>
    <div class="container">
        <h1>GraphQL与REST对比演示</h1>
        <div class="card">
            <h2>选择场景</h2>
            <div class="scenario-btns">
                <div class="scenario-btn active" onclick="showScenario(0)">获取用户基本信息</div>
                <div class="scenario-btn" onclick="showScenario(1)">获取用户及关联数据</div>
                <div class="scenario-btn" onclick="showScenario(2)">创建资源</div>
                <div class="scenario-btn" onclick="showScenario(3)">多资源查询</div>
            </div>
        </div>
        <div class="card">
            <div class="comparison" id="comparisonArea"></div>
        </div>
    </div>
    <script>
        const scenarios = [
            {
                title: '获取用户基本信息',
                rest: {
                    request: 'GET /api/v1/users/1',
                    response: '{\n  "id": 1,\n  "name": "张三",\n  "email": "zhangsan@example.com",\n  "phone": "13800138000",\n  "address": "北京市...",\n  "birthday": "1995-01-01",\n  "avatar": "https://...",\n  "bio": "这是个人简介...",\n  "createdAt": "2026-01-01"\n}',
                    requests: 1, overfetch: '高(返回9个字段,只需2个)'
                },
                graphql: {
                    request: 'query {\n  user(id: 1) {\n    name\n    email\n  }\n}',
                    response: '{\n  "data": {\n    "user": {\n      "name": "张三",\n      "email": "zhangsan@example.com"\n    }\n  }\n}',
                    requests: 1, overfetch: '无(精确获取2个字段)'
                }
            },
            {
                title: '获取用户及关联数据',
                rest: {
                    request: 'GET /api/v1/users/1\nGET /api/v1/users/1/orders\nGET /api/v1/orders/101/items',
                    response: '需要3次请求,逐个获取:\n1. 用户信息\n2. 用户订单列表\n3. 订单商品详情',
                    requests: 3, overfetch: '高'
                },
                graphql: {
                    request: 'query {\n  user(id: 1) {\n    name\n    orders {\n      id total status\n      items {\n        product { name price }\n        quantity\n      }\n    }\n  }\n}',
                    response: '{\n  "data": {\n    "user": {\n      "name": "张三",\n      "orders": [{\n        "id": 101,\n        "total": 299.9,\n        "status": "PAID",\n        "items": [{\n          "product": { "name": "商品A", "price": 149.95 },\n          "quantity": 2\n        }]\n      }]\n    }\n  }\n}',
                    requests: 1, overfetch: '无'
                }
            },
            {
                title: '创建资源',
                rest: {
                    request: 'POST /api/v1/users\nContent-Type: application/json\n\n{\n  "name": "张三",\n  "email": "zhangsan@example.com"\n}',
                    response: 'HTTP/1.1 201 Created\nLocation: /api/v1/users/3\n\n{\n  "id": 3,\n  "name": "张三",\n  "email": "zhangsan@example.com",\n  "role": "user",\n  "status": "active",\n  "createdAt": "2026-04-22T12:00:00Z"\n}',
                    requests: 1, overfetch: '中'
                },
                graphql: {
                    request: 'mutation {\n  createUser(input: {\n    name: "张三",\n    email: "zhangsan@example.com"\n  }) {\n    id\n    name\n    email\n  }\n}',
                    response: '{\n  "data": {\n    "createUser": {\n      "id": 3,\n      "name": "张三",\n      "email": "zhangsan@example.com"\n    }\n  }\n}',
                    requests: 1, overfetch: '无'
                }
            },
            {
                title: '多资源查询',
                rest: {
                    request: 'GET /api/v1/users?page=1\nGET /api/v1/orders?status=paid\nGET /api/v1/products?category=electronics',
                    response: '需要3次独立请求,无法在一个请求中获取',
                    requests: 3, overfetch: '高'
                },
                graphql: {
                    request: 'query {\n  users(page: 1) { id name }\n  orders(status: PAID) { id total }\n  products(categoryId: 1) { id name price }\n}',
                    response: '{\n  "data": {\n    "users": [{"id":1,"name":"张三"}],\n    "orders": [{"id":101,"total":299.9}],\n    "products": [{"id":1,"name":"商品A","price":99.9}]\n  }\n}',
                    requests: 1, overfetch: '无'
                }
            }
        ];

        function showScenario(index) {
            document.querySelectorAll('.scenario-btn').forEach(b => b.classList.remove('active'));
            event.currentTarget.classList.add('active');
            const s = scenarios[index];
            document.getElementById('comparisonArea').innerHTML = `
                <div class="panel panel-rest">
                    <div class="panel-title"><span class="rest-color">REST</span> ${s.title}</div>
                    <div class="code-block">${s.rest.request}</div>
                    <div class="code-block">${s.rest.response}</div>
                    <div class="metric">
                        <span class="metric-item ${s.rest.requests > 1 ? 'metric-bad' : 'metric-good'}">请求次数: ${s.rest.requests}</span>
                        <span class="metric-item metric-bad">过度获取: ${s.rest.overfetch}</span>
                    </div>
                </div>
                <div class="panel panel-graphql">
                    <div class="panel-title"><span class="graphql-color">GraphQL</span> ${s.title}</div>
                    <div class="code-block">${s.graphql.request}</div>
                    <div class="code-block">${s.graphql.response}</div>
                    <div class="metric">
                        <span class="metric-item metric-good">请求次数: ${s.graphql.requests}</span>
                        <span class="metric-item metric-good">过度获取: ${s.graphql.overfetch}</span>
                    </div>
                </div>
            `;
        }

        showScenario(0);
    </script>
</body>
</html>

示例3:GraphQL模拟客户端

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GraphQL模拟客户端</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
        .container { max-width: 900px; margin: 0 auto; }
        h1 { text-align: center; color: #e535ab; margin-bottom: 24px; font-size: 28px; }
        .card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        .card h2 { font-size: 18px; color: #e535ab; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #fce4ec; }
        textarea { width: 100%; padding: 14px; border: 1px solid #dadce0; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 13px; line-height: 1.6; resize: vertical; min-height: 120px; }
        textarea:focus { outline: none; border-color: #e535ab; }
        .btn-row { display: flex; gap: 8px; margin-top: 12px; }
        button { padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
        .btn-primary { background: #e535ab; color: #fff; }
        .btn-primary:hover { background: #c7309a; }
        .btn-secondary { background: #5f6368; color: #fff; }
        .result-area { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 13px; line-height: 1.7; max-height: 400px; overflow-y: auto; white-space: pre-wrap; }
        .preset-btns { display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap; }
        .preset-btn { padding: 6px 12px; border: 1px solid #e8eaed; border-radius: 6px; background: #fff; cursor: pointer; font-size: 12px; }
        .preset-btn:hover { border-color: #e535ab; color: #e535ab; }
    </style>
</head>
<body>
    <div class="container">
        <h1>GraphQL模拟客户端</h1>
        <div class="card">
            <h2>预设查询</h2>
            <div class="preset-btns">
                <div class="preset-btn" onclick="loadPreset('getUser')">获取用户</div>
                <div class="preset-btn" onclick="loadPreset('getUsers')">用户列表</div>
                <div class="preset-btn" onclick="loadPreset('getUserOrders')">用户订单</div>
                <div class="preset-btn" onclick="loadPreset('createUser')">创建用户</div>
                <div class="preset-btn" onclick="loadPreset('introspect')">内省查询</div>
            </div>
        </div>
        <div class="card">
            <h2>GraphQL查询</h2>
            <textarea id="queryInput">query {
  user(id: 1) {
    id
    name
    email
    role
  }
}</textarea>
            <div class="btn-row">
                <button class="btn-primary" onclick="executeQuery()">执行查询</button>
                <button class="btn-secondary" onclick="document.getElementById('queryInput').value=''">清空</button>
            </div>
        </div>
        <div class="card">
            <h2>查询结果</h2>
            <div class="result-area" id="resultArea">执行查询查看结果</div>
        </div>
    </div>
    <script>
        const mockData = {
            users: [
                { id: 1, name: '张三', email: 'zhangsan@example.com', role: 'ADMIN', createdAt: '2026-01-15T08:00:00Z' },
                { id: 2, name: '李四', email: 'lisi@example.com', role: 'USER', createdAt: '2026-02-20T10:30:00Z' },
                { id: 3, name: '王五', email: 'wangwu@example.com', role: 'USER', createdAt: '2026-03-10T14:00:00Z' }
            ],
            orders: [
                { id: 101, userId: 1, total: 299.9, status: 'PAID', createdAt: '2026-04-01T09:00:00Z', items: [{ id: 1, productId: 1, quantity: 2, price: 149.95 }] },
                { id: 102, userId: 1, total: 599.0, status: 'SHIPPED', createdAt: '2026-04-10T11:00:00Z', items: [{ id: 2, productId: 2, quantity: 1, price: 599.0 }] },
                { id: 103, userId: 2, total: 89.9, status: 'PENDING', createdAt: '2026-04-20T16:00:00Z', items: [{ id: 3, productId: 3, quantity: 1, price: 89.9 }] }
            ],
            products: [
                { id: 1, name: '商品A', price: 149.95, stock: 100, categoryId: 1 },
                { id: 2, name: '商品B', price: 599.0, stock: 50, categoryId: 1 },
                { id: 3, name: '商品C', price: 89.9, stock: 200, categoryId: 2 }
            ]
        };

        const presets = {
            getUser: `query {\n  user(id: 1) {\n    id\n    name\n    email\n    role\n  }\n}`,
            getUsers: `query {\n  users {\n    id\n    name\n    email\n    role\n  }\n}`,
            getUserOrders: `query {\n  user(id: 1) {\n    name\n    email\n    orders {\n      id\n      total\n      status\n      items {\n        product {\n          name\n          price\n        }\n        quantity\n      }\n    }\n  }\n}`,
            createUser: `mutation {\n  createUser(input: {\n    name: "赵六"\n    email: "zhaoliu@example.com"\n  }) {\n    id\n    name\n    email\n    role\n  }\n}`,
            introspect: `query {\n  __type(name: "User") {\n    name\n    fields {\n      name\n      type {\n        name\n        kind\n      }\n    }\n  }\n}`
        };

        function loadPreset(name) {
            document.getElementById('queryInput').value = presets[name];
        }

        function executeQuery() {
            const query = document.getElementById('queryInput').value;
            const result = document.getElementById('resultArea');
            result.textContent = '执行中...\n';

            setTimeout(() => {
                try {
                    let data = {};

                    if (query.includes('user(id:') && query.includes('orders')) {
                        const user = { ...mockData.users[0] };
                        user.orders = mockData.orders.filter(o => o.userId === user.id).map(o => ({
                            ...o,
                            items: o.items.map(item => ({
                                ...item,
                                product: mockData.products.find(p => p.id === item.productId)
                            }))
                        }));
                        data = { user };
                    } else if (query.includes('user(id:')) {
                        data = { user: mockData.users[0] };
                    } else if (query.includes('users')) {
                        data = { users: mockData.users };
                    } else if (query.includes('createUser')) {
                        const newUser = { id: 4, name: '赵六', email: 'zhaoliu@example.com', role: 'USER', createdAt: new Date().toISOString() };
                        data = { createUser: newUser };
                    } else if (query.includes('__type')) {
                        data = { __type: { name: 'User', fields: [{ name: 'id', type: { name: 'ID', kind: 'SCALAR' } }, { name: 'name', type: { name: 'String', kind: 'SCALAR' } }, { name: 'email', type: { name: 'String', kind: 'SCALAR' } }, { name: 'role', type: { name: 'UserRole', kind: 'ENUM' } }, { name: 'orders', type: { name: null, kind: 'LIST' } }] } };
                    }

                    result.textContent = `// 查询执行成功\n// 耗时: ${Math.floor(Math.random() * 50 + 10)}ms\n\n${JSON.stringify({ data }, null, 2)}`;
                } catch (error) {
                    result.textContent = `// 查询执行失败\n{\n  "errors": [{\n    "message": "${error.message}"\n  }]\n}`;
                }
            }, 300);
        }
    </script>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge 说明
Fetch API 42+ 39+ 10.1+ 14+ 发送GraphQL请求
WebSocket 全版本 全版本 全版本 全版本 Subscription
JSON 全版本 全版本 全版本 全版本 响应解析
async/await 55+ 52+ 10.1+ 15+ 异步处理
EventSource 全版本 全版本 全版本 全版本 SSE替代方案

注意事项与最佳实践

1. 查询设计

  • 只请求需要的字段,避免过度获取

  • 使用Fragment复用字段选择

  • 使用变量而非字符串拼接

  • 使用别名处理同名字段

2. 性能优化

  • 实现查询深度限制

  • 设置查询复杂度限制

  • 使用DataLoader解决N+1问题

  • 实现查询缓存

3. 安全考虑

  • 禁用生产环境的内省查询

  • 实现查询白名单(持久化查询)

  • 限制查询深度和复杂度

  • 实现认证和授权

4. 错误处理

  • GraphQL返回200状态码,即使有错误

  • 错误信息在errors数组中

  • 区分网络错误和GraphQL错误

代码规范示例

GraphQL客户端封装

代码示例

class GraphQLClient {
    constructor(url, options = {}) {
        this.url = url;
        this.headers = { 'Content-Type': 'application/json', ...options.headers };
    }

    async request(query, variables = {}) {
        const response = await fetch(this.url, {
            method: 'POST',
            headers: this.headers,
            body: JSON.stringify({ query, variables })
        });

        const result = await response.json();

        if (result.errors && result.errors.length > 0) {
            const error = result.errors[0];
            throw new GraphQLError(error.message, error.extensions?.code);
        }

        return result.data;
    }

    setHeader(name, value) {
        this.headers[name] = value;
    }
}

class GraphQLError extends Error {
    constructor(message, code) {
        super(message);
        this.code = code;
    }
}

// 使用
const client = new GraphQLClient('/graphql');
client.setHeader('Authorization', 'Bearer token123');

const data = await client.request(`
    query GetUser($id: ID!) {
        user(id: $id) { id name email }
    }
`, { id: 1 });

常见问题与解决方案

问题1:N+1查询问题

解决方案:使用DataLoader批量加载数据:

代码示例

const DataLoader = require('dataloader');
const userLoader = new DataLoader(async (ids) => {
    const users = await User.find({ _id: { $in: ids } });
    return ids.map(id => users.find(u => u.id === id));
});

问题2:查询深度攻击

解决方案:限制查询深度:

代码示例

// 限制最大深度为10
const depthLimit = require('graphql-depth-limit');
app.use('/graphql', depthLimit(10));

问题3:缓存困难

解决方案:使用Apollo Client的缓存机制,或实现持久化查询。

问题4:文件上传

解决方案:使用graphql-multipart-request-spec规范,通过multipart/form-data上传。

总结

GraphQL是REST的有力替代方案,提供了更灵活的数据查询方式:

  1. 精确查询:客户端声明需要的数据,避免过度获取和获取不足

  2. 单一端点:所有操作通过/graphql端点完成

  3. 强类型Schema:Schema即文档,类型安全

  4. 三种操作:Query查询、Mutation变更、Subscription订阅

  5. 高级特性:变量、片段、指令、内省

  6. 客户端生态:Apollo Client、Relay等成熟方案

  7. 注意事项:N+1问题、查询复杂度、缓存、安全

GraphQL适合数据关系复杂、前端需求多样的场景,但REST在简单场景下仍有优势。根据项目需求选择合适的方案。

常见问题

什么是GraphQL基础?

GraphQL基础是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。

如何学习GraphQL基础的实际应用?

教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。

GraphQL基础有哪些注意事项?

常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。

GraphQL基础适合初学者吗?

本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。

GraphQL基础的核心要点是什么?

核心要点包括教程简介等内容,建议按教程顺序逐步学习。

标签: Promise OAuth 响应码 async await 键盘输入 内存管理 场景管理 JWT

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 后端通信:RESTful API - 从入门到实践详解 下一篇: 后端通信:WebSocket实时通信 - 从入门到实践详解

poll相关推荐