服务端开发

服务端开发

服务端开发

基础服务

express-graphql

最简单的构建 GraphQL API 服务器的方式就是基于 Express,添加自定义的处理器:

import express from "express";
import graphqlHTTP from "express-graphql";
import { buildSchema } from "graphql";

// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
  type Query {
    hello: String
  }
`);

// The root provides a resolver function for each API endpoint
const root = {
  hello: () => "Hello world!"
};

const app = express();

app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true
  })
);

// http://localhost:4000/graphql 进入 GraphiQL 交互查询工具
app.listen(4000);

Prisma 是非常不错的全栈架构,开发者只需要定义好数据结构,Prisma 即能够为我们自动构建包含数据库(Docker)的 GraphQL API,Prisma 也为我们提供了便捷的云化部署方案,较为适合个人项目。

Apollo Server

const schema = `
  type Todo {
    ...
  }

  type TodoList {
    todos: [Todo]
  }

  type Query {
    todoList: Todo List
  }

  type Mutation {
    addTodo(
      text: String!
    ): Todo,
    toggleTodo(
      id: String!
    ): Todo
  }

  type Subscription {
    todoUpdated: Todo
  }

  schema {
    query: Query
    mutation: Mutation
    subscription: Subscription
  }
`;

const resolvers = {
  TodoList: {
    todos() {
      return todos;
    }
  },
  Query: {
    todoList() {
      return true;
    }
  },
  Mutation: {
    addTodo(_, { text }) {
      ...
    },
    toggleTodo(_, { id }, { ctx }) {
      ...
    }
  },
  Subscription: {
    todoUpdated: {
      ...
    }
  }
};

const executableSchema = makeExecutableSchema({
  typeDefs: schema,
  resolvers
});

router.post(
  '/graphql',
  graphqlKoa(ctx => ({
    schema: executableSchema,
    context: { ctx }
  }))
)

数据模型层

GraphQL 的一大优势就是避免了单个业务逻辑与查询语句的强绑定,SQL 具备良好的声明式可读性,但是其编程可组合性较差。在 GraphQL 的图模型中,无法确定单次查询涉及的资源数目,自然也就无法预先编写出完整的 SQL 语句,而需要在程序里进行资源的获取与组合。REST 架构开发模式下,我们往往会为某个涉及多资源的查询编写复杂的关联语句:

select * from user left join asset on user.asset_id = asset.id;

而 GraphQL 中则是将复杂的逻辑划分为多个原子查询,并在编程语言中完成数据的聚合,譬如我们需要查询用户关联的资产时,其 Schema 定义如下:

type User {
  asset: Asset
}

此时对于 Asset 资源,其并不需要了解完整的业务逻辑,只需要根据输入的 userId 获取到 asset 对象,对于数据的封装则是由 GraphQL 自动完成:

const user = getUserById(userId);
const asset = getAssetById(user.assetId);

user.asset = asset;

显而易见地,这种方式可能导致单次请求处理中对于某个表的多次查询,dataloader 即是 Facebook 开源的数据访问层辅助工具,其能够将多次对于数据库或者外部服务查询的语句合并处理。dataloader 的核心理念在于接收用户定义的批次加载函数:

const DataLoader = require("dataloader");

const userLoader = new DataLoader(keys => myBatchGetUsers(keys));

当我们在业务逻辑中进行多次查询时,譬如:

userLoader
  .load(1)
  .then(user => userLoader.load(user.invitedByID))
  .then(invitedBy => console.log(`User 1 was invited by ${invitedBy}`));

// Elsewhere in your application
userLoader
  .load(2)
  .then(user => userLoader.load(user.lastInvitedID))
  .then(lastInvited => console.log(`User 2 last invited ${lastInvited}`));

// 也可以同时加载多个数据
const [a, b] = await myLoader.loadMany(["a", "b"]);

dataloader 会把某个执行时间片(参考 EventLoop)内的独立查询合并处理,调用批量查询函数来进行单次查询;dataloader 还提供了缓存机制,当我们在某个执行时间片内查询了相同键的数据,dataloader 会自动返回缓存的 Promise 对象:

const userLoader = new DataLoader(...)
const promise1A = userLoader.load(1)
const promise1B = userLoader.load(1)
assert(promise1A === promise1B)

分页与搜索

GitHub GraphQL API,可以在 Explorer

query {
  viewer {
    login
    name
    starredRepositories(first: 3, after: "put_in_a_cursor_value_here") {
      edges {
        cursor
        node {
          id
          name
          primaryLanguage {
            id
            name
            color
          }
        }
      }
    }
  }
}