若是喜歡咱們的文章別忘了點擊關注阿里南京技術專刊呦~ 本文轉載自 阿里南京技術專刊-知乎,歡迎大牛小牛投遞阿里南京前端/後端開發等職位,詳見 阿里南京誠邀前端小夥伴加入~。前端
在最近的項目中,咱們選擇了 GraphQL 做爲 API 查詢語言替代了傳統的 Restful 傳參的方法進行先後端數據傳遞。服務端選用了 egg.js + Apollo graphql-tools,前端使用了 React.js + Apollo graphql-client。這樣的架構選擇讓咱們的迭代速度有了很大的提高。node
基於 GraphQL 的 API 服務在架構上來看算是 MVC 中的 controller。只是它只有一個固定的路由來處理全部請求。那麼在和 MVC 框架結合使用時,在數據轉換 ( Convertor )、參數校驗 ( Validator ) 等功能上,使用 Apollo GraphQL 帶來了一些新的處理方式。下面會介紹一下在這些地方使用 Graphql 帶來的一些優點。git
GraphQL 是由 Facebook 創造的用於描述複雜數據模型的一種查詢語言。這裏查詢語言所指的並非常規意義上的相似 sql 語句的查詢語言,而是一種用於先後端數據查詢方式的規範。sql
Apollo GraphQL 是基於 GraphQL 的全棧解決方案集合。從後端到前端提供了對應的 lib 使得開發使用 GraphQL 更加的方便express
在描述一個數據類型時,GraphQL 經過 type 關鍵字來定義一個類型,GraphQL 內置兩個類型 Query 和 Mutation,用於描述讀操做和寫操做。npm
schema {
query: Query
mutation: Mutation
}
複製代碼
正常系統中咱們會用到查詢當前登陸用戶,咱們在 Query 中定義一個讀操做 currentUser ,它將返回一個 User 數據類型。後端
type Query {
currentUser: User
}
type User {
id: String!
name: String
avatar: String
# user's messages
messages(query: MessageQuery): [Message]
}
複製代碼
當咱們的一個操做須要返回多種數據格式時,GraphQL 提供了 interface 和 union types 來處理。api
以上面的 Message 類型爲例,咱們可能有多種消息類型,好比通知、提醒bash
interface Message {
content: String
}
type Notice implements Message {
content: String
noticeTime: Date
}
type Remind implements Message {
content: String
endTime: Date
}
複製代碼
可能在某個查詢中,須要一塊兒返回未讀消息和未讀郵件。那麼咱們能夠用 union。數據結構
union Notification = Message | Email
複製代碼
在大多數 node.js 的 mvc 框架 (express、koa) 中是沒有對請求的參數和返回值定義數據結構和類型的,每每咱們須要本身作類型轉換。好比經過 GET 請求 url 後面問號轉入的請求參數默認都是字符串,咱們可能要轉成數字或者其餘類型。
好比上面的獲取當前用戶的消息,以 egg.js 爲例的話,Controller 會寫成下面這樣
// app/controller/message.js
const Controller = require('egg').Controller;
class MessageController extends Controller {
async create() {
const { ctx, service } = this;
const { page, pageSize } = ctx.query;
const pageNum = parseInt(page, 0) || 1;
const pageSizeNum = parseInt(pageSize, 0) || 10;
const res = await service.message.getByPage(pageNum, pageSizeNum);
ctx.body = res;
}
}
module.exports = MessageController;
複製代碼
更好一點的處理方式是經過定義 JSON Schema + Validator 框架來作驗證和轉換。
GraphQL 的參數是強類型校驗的
使用 GraphQL 的話,能夠定義一個 Input 類型來描述請求的入參。好比上面的 MessageQuery
# 加上 ! 表示必填參數
input MessageQuery {
page: Int!
pageSize: Int!
}
複製代碼
咱們能夠聲明 page 和 pageSize 是 Int 類型的,若是請求傳入的值是非 Int 的話,會直接報錯。
對於上面消息查詢,咱們須要提供兩個 resolver function。以使用 graphql-tools 爲例,egg-graphql 已經集成。
module.exports = {
Query: {
currentUser(parent, args, ctx) {
return {
id: 123,
name: 'jack'
};
}
},
User: {
messages(parent, {query: {page, pageSize}}, ctx) {
return service.message.getByPage(page, pageSize);
}
}
};
複製代碼
咱們上面定義的 User 的 id 爲 String,這裏返回的 id 是數字,這時候 Graphql 會幫咱們會轉換,Graphql 的 type 默認都會有序列化與反序列化,能夠參考下面的自定義類型。
GraphQL 默認定義了幾種基本 scalar type (標量類型):
GraphQL 提供了經過自定義類型的方法,經過 scalar 申明一個新類型,而後在 resovler 中提供該類型的 GraphQLScalarType 的實例。
已最多見的日期處理爲例,在咱們代碼中的時間字段都是用的 Date 類型,而後在返回和入參時用時間戳。
# schema.graphql 中申明類型
scalar Date
複製代碼
// resovler.js
const { GraphQLScalarType } = require('graphql');
const { Kind } = require('graphql/language');
const _ = require('lodash');
module.exports = {
Date: new GraphQLScalarType({
name: 'Date',
description: 'Date custom scalar type',
parseValue(value) {
return new Date(value);
},
serialize(value) {
if (_.isString(value) && /^\d*$/.test(value)) {
return parseInt(value, 0);
} else if (_.isInteger(value)) {
return value;
}
return value.getTime();
},
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return new Date(parseInt(ast.value, 10));
}
return null;
}
});
}
複製代碼
在定義具體數據類型的時候可使用這個新類型
type Comment {
id: Int!
content: String
creator: CommonUser
feedbackId: Int
gmtCreate: Date
gmtModified: Date
}
複製代碼
GraphQL 的 Directive 相似與其餘語言中的註解 (Annotation) 。能夠經過 Directive 實現一些切面的事情,Graphql 內置了兩個指令 @skip 和 @include ,用於在查詢語句中動態控制字段是否須要返回。
在查詢當前用戶的時候,咱們可能不須要返回當前人的消息列表,咱們可使用 Directive 實現動態的 Query Syntax。
query CurrentUser($withMessages: Boolean!) {
currentUser {
name
messages @include(if: $withMessages) {
content
}
}
}
複製代碼
最新的 graphql-js 中,容許自定義 Directive,就像 Java 的 Annotation 在建立的時候須要指定 Target 同樣,GraphQL 的 Directive 也須要指定它能夠用於的位置。
// Request Definitions -- in query syntax
QUERY: 'QUERY',
MUTATION: 'MUTATION',
SUBSCRIPTION: 'SUBSCRIPTION',
FIELD: 'FIELD',
FRAGMENT_DEFINITION: 'FRAGMENT_DEFINITION',
FRAGMENT_SPREAD: 'FRAGMENT_SPREAD',
INLINE_FRAGMENT: 'INLINE_FRAGMENT',
// Type System Definitions -- in type schema
SCHEMA: 'SCHEMA',
SCALAR: 'SCALAR',
OBJECT: 'OBJECT',
FIELD_DEFINITION: 'FIELD_DEFINITION',
ARGUMENT_DEFINITION: 'ARGUMENT_DEFINITION',
INTERFACE: 'INTERFACE',
UNION: 'UNION',
ENUM: 'ENUM',
ENUM_VALUE: 'ENUM_VALUE',
INPUT_OBJECT: 'INPUT_OBJECT',
INPUT_FIELD_DEFINITION: 'INPUT_FIELD_DEFINITION'
複製代碼
Directive 的 resolver function 就像是一個 middleware ,它的第一個參數是 next,這樣你能夠在先後作攔截對數據進行處理。
對於入參和返回值,咱們有時候須要對它設定默認值,下面咱們建立一個 @Default 的directive。
directive @Default(value: Any ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
複製代碼
next 是一個 Promise
const _ = require('lodash');
module.exports = {
Default: (next, src, { value }, ctx, info) => next().then(v => _.defaultTo(v, value))
};
複製代碼
那麼以前的 MessageQuery 須要默認值時,可使用 @Default
input MessageQuery {
page: Int @Default(value: 1)
pageSize: Int @Default(value: 15)
}
複製代碼
GraphQL 簡單的定義一組枚舉使用 enum 關鍵字。相似於其餘語言每一個枚舉的 ordinal 值是它的下標。
enum Status {
OPEN # ordinal = 0
CLOSE # ordinal = 1
}
複製代碼
在使用枚舉的時候,咱們不少時候須要把全部的枚舉傳給前臺來作選擇。那麼咱們須要本身建立 GraphQLEnumType 的對象來定義枚舉,而後經過該對象的 getValues 方法獲取全部定義。
// enum resolver.js
const { GraphQLEnumType } = require('graphql');
const status = new GraphQLEnumType({
name: 'StatusEnum',
values: {
OPEN: {
value: 0,
description: '開啓'
},
CLOSE: {
value: 1,
descirption: '關閉'
}
}
});
module.exports = {
Status: status,
Query: {
status: status.getValues()
}
};
複製代碼
使用 GraphQL 有一個最大的優勢就是在 Schema 定義中好全部數據後,經過一個請求能夠獲取全部想要的數據。可是當系統愈來愈龐大的時候,咱們須要對系統進行模塊化拆分,演變成一個分佈式微服務架構的系統。這樣能夠按照模塊獨立開發部署。
咱們經過 Apollo Link 能夠遠程記載 Schema ,而後在進行拼接 (Schema stitching)。
import { HttpLink } from 'apollo-link-http';
import fetch from 'node-fetch';
const link = new HttpLink({ uri: 'http://api.githunt.com/graphql', fetch });
const schema = await introspectSchema(link);
const executableSchema = makeRemoteExecutableSchema({
schema,
link,
});
複製代碼
好比咱們對博客系統進行了模塊化拆分,一個用戶服務模塊,一個文章服務模塊,和咱們統一對外提供服務的 Gateway API 層。
import { HttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import fetch from 'node-fetch';
const userLink = new HttpLink({ uri: 'http://user-api.xxx.com/graphql', fetch });
const blogLink = new HttpLink({ uri: 'http://blog-api.xxx.com/graphql', fetch });
const userWrappedLink = setContext((request, previousContext) => ({
headers: {
'Authentication': `Bearer ${previousContext.graphqlContext.authKey}`,
}
})).concat(userLink);
const userSchema = await introspectSchema(userWrappedLink);
const blogSchema = await introspectSchema(blogLink);
const executableUserSchema = makeRemoteExecutableSchema({
userSchema,
userLink,
});
const executableBlogSchema = makeRemoteExecutableSchema({
blogSchema,
blogLink,
});
const schema = mergeSchemas({
schemas: [executableUserSchema, executableBlogSchema],
});
複製代碼
在合併 Schemas 的時候,咱們能夠對 Schema 進行擴展並添加新的 Resolver 。
const linkTypeDefs = `
extend type User {
blogs: [Blog]
}
extend type Blog {
author: User
}
`;
mergeSchemas({
schemas: [chirpSchema, authorSchema, linkTypeDefs],
resolvers: mergeInfo => ({
User: {
blogs: {
fragment: `fragment UserFragment on User { id }`,
resolve(parent, args, context, info) {
const authorId = parent.id;
return mergeInfo.delegate(
'query',
'blogByAuthorId',
{
authorId,
},
context,
info,
);
},
},
},
Blog: {
author: {
fragment: `fragment BlogFragment on Blog { authorId }`,
resolve(parent, args, context, info) {
const id = parent.authorId;
return mergeInfo.delegate(
'query',
'userById',
{
id,
},
context,
info,
);
},
},
},
}),
});
複製代碼
Apollo Server 提供了與多種框架整合的執行 GraphQL 請求處理的中間件。好比在 Egg.js 中,因爲 Egg.js 是基於 koa 的,咱們能夠選擇 apollo-server-koa。
npm install --save apollo-server-koa
複製代碼
咱們能夠經過提供一箇中間件來處理 graphql 的請求。
const { graphqlKoa, graphiqlKoa } = require('apollo-server-koa');
module.exports = (_, app) => {
const options = app.config.graphql;
const graphQLRouter = options.router;
return async (ctx, next) => {
if (ctx.path === graphQLRouter) {
return graphqlKoa({
schema: app.schema,
context: ctx,
})(ctx);
}
await next();
};
};
複製代碼
這裏能夠看到咱們將 egg 的請求上下文傳到來 GraphQL 的執行環境中,咱們在 resolver function 中能夠拿到這個 context。
graphqlKoa 還有一些其餘參數,咱們能夠用來實現一些跟上下文相關的事情。
在上面咱們提到來如何實現基於 GraphQL 的分佈式系統,那麼全鏈路請求跟蹤就是一個很是重要的事情。使用 Apollo GraphQL 只須要下面幾步。
轉眼已經 2018 年了,GraphQL 再也不是一個新鮮的名詞。Apollo 做爲一個全棧 GraphQL 解決方案終於在今年迎來了飛速的發展。咱們有幸在項目中接觸並深度使用了 Apollo 的整套工具鏈。而且咱們感覺到了 Apollo 和 GraphQL 在一些方面的簡潔和優雅,藉此機會給你們分享它們的酸與甜。