若是喜歡咱們的文章別忘了點擊關注阿里南京技術專刊呦~ 本文轉載自 阿里南京技術專刊-知乎,歡迎大牛小牛投遞阿里南京前端/後端開發等職位,詳見 阿里南京誠邀前端小夥伴加入~。前端
在最近的項目中,咱們選擇了 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。markdown
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 在一些方面的簡潔和優雅,藉此機會給你們分享它們的酸與甜。