使用 express 構建簡單 GraphQL 介面
GQL
GQL(Graph Query Language) 是一種查詢語言,用來設計出較 Restful api 更易于擴展和升級的介面,可以理解為 Restful api 的替代品,
GQL 服務可以開放在 Restful api 下,不過其邏輯并不依賴任何平臺,
在后端,可以使用 GQL 直接描述資料模型,或使用 GraphQL.js 提供的其他介面來描述資料模型,
在前端,使用 GQL 直接描述我們需要的資料結構,然后就可以拿到不多不少、結構相似的資料,
例如我們請求,
{
user {
name
}
}
回傳,
{
"data": {
"user": {
"name": "高厲害"
}
}
}

使用GraphQL.js
GraphQL.js泛指使用 JavaScript 實作的 GraphQL 庫,我們在 node.js 平臺學習開發 GQL 介面可以稱之為 —— 學習GraphQL.js,
GraphQL.js的核心是一個決議器,用來決議 GQL 文本,實作“定義 GQL 資料模型”,和“增刪改查資料”,本文僅描述 GQL 的查詢介面,
初始化
初始化并安裝依賴,
npm init -y
npm i express graphql
在graphql中,我們暫時僅關心兩個介面:graphql和buildSchema,前者是 GQL 的決議器,后者用于構造一個GraphQLSchema型別的物件,
來看看決議器都需要什么引數,下面給出一個呼叫實體:
graphql.graphql(schema, query, root).then((gqlRes) => {
console.log(gqlRes);
});
-
schema 是查詢介面的模型(
GraphQLSchema型別) -
query 是 GQL 的查詢文本,例如
{ user { name } } -
root 是查詢介面模型涉及到的每個欄位的函式(GQL 對不同層次函式呼叫是廣度優先的)
使用 graphql 開發查詢介面只需要三步,第一步是描述資料模型和查詢介面模型(對應 schema),第二步是針對每個欄位提供函式(對應 root),第三步是將資料送往決議器并將結果回傳給請求端,
資料模型
在GraphQL.js里,查詢介面模型的型別是GraphQLSchema,我們可以使用buildSchema來構造,
下面開始設計一個對用戶開放的介面
假設我們在這個介面可能需要獲得兩個東西:
- 根據 id 查詢用戶資訊
- 當前時間
| 欄位 | 型別 |
|---|---|
| user | UserType |
| now | string |
使用 GQL 描述為:
type Query {
user(id: String!): UserType
now: String
}
user 欄位括號里是引數,通過 id 唯一確定一個用戶,引數末尾的 ! 表示該欄位必須提供,
Query 的位置是自定義型別名,但介面模型的型別固定為 Query,
同樣地,UserType 也是一種自定義型別,考慮下面這個用戶模型,id 為主鍵:
| 欄位 | 型別 |
|---|---|
| id | string |
| username | string |
| age | int |
使用 GQL 描述為:
type UserType {
id: String
username: String
age: Int
}
呼叫buildSchema,將回傳一個GraphQLSchema物件,實作如下:
const schema = graphql.buildSchema(`
type UserType {
id: String
username: String
age: Int
}
type Query {
user(id: String!): UserType
}
`);
這樣一來我們就搞定了決議器的第一個引數 schema,
欄位函式
我們有了資料模型,下面就要確定資料來源,這里簡單手動提供一些資料和介面:
'./db.js'
const data = {
'1001': { username: '高厲害', age: 21 },
'1002': { username: '列隊貓', age: 90 },
'1003': { username: '小明', age: 15 },
'1004': { username: '小紅', age: 16 },
}
module.exports = {
findById(id) {
if (id in data) {
return data[id];
}
return null;
}
}
然后提供 root 引數,root 是一個物件,描述了 Query 即介面模型各個欄位的來源:
let root = {
user: (args, context, info) => {
return db.findById(args.id);
},
now: (args, context, info) => {
return new Date().toLocaleString();
}
};
物件深層的欄位也可以特別指定:
但這樣做似乎就無法為外層的 user 提供函式了,所以傳入決議器的 root 引數的功能非常局限,一般僅提供根欄位的函式,
此外,若不提供深層欄位的函式,則默認提供外層物件的對應值,就像上面那個例子那樣,
let root = {
user: {
username: (args, context, info) => {
return ...;
},
age: (args, context, info) => {
return ...;
},
},
now: (args, context, info) => {
return new Date().toLocaleString();
},
};
提供服務
引數都準備好了,
寫一個查詢:
let query = `
{
user(id: "1001"){
age
}
now
}
`;
呼叫決議器:
graphql.graphql(schema, query, root).then((result) => {
console.log(result);
});
輸出:
{
"data": {
"user": {
"age": 21
},
"now": "2021-1-25 16:19:03"
}
}
下面引入express,在某個路由提供 GQL 服務:
const graphql = require('graphql');
const express = require('express');
const db = require('./db.js');
const app = express();
// 資料模型和查詢介面模型
let schema = graphql.buildSchema(`
type UserType {
id: String
username: String
age: Int
}
type Query {
user(id: String!): UserType
now: String
}
`);
// 所有欄位的決議方法
let root = {
user: (args, context, info) => {
return db.findById(args.id);
},
now: (args, context, info) => {
return new Date().toLocaleString();
}
};
// 路由
app.use('/graphqlAPI', (req, res) => {
let reqJson = '';
req.on('data', (data) => reqJson += data);
req.on('end', () => {
reqJson = JSON.parse(reqJson);
graphql.graphql(schema, reqJson.query, root).then((result) => {
res.send(result);
});
})
});
app.listen(80, () => { console.log('listen on 80.'); });
創建模型的推薦方法
剛才我們已經完成了一個簡單的 GQL 介面,
- 通過 GQL 文本描述了資料模型和介面模型
- 提供了根欄位的函式
- 對外提供 GQL 服務
在給定欄位的函式時,我們無法對每個欄位精確控制,root 引數僅允許對根欄位(或深層的根欄位)提供函式,
而檔案中給出的欄位的函式原型是這樣的:
他有四個引數,而我們在 root 引數中提供的函式僅有三個引數
// See below about resolver functions.
type GraphQLFieldResolveFn = (
source?: any,
args?: {[argName: string]: any},
context?: any,
info?: GraphQLResolveInfo
) => any
缺失的 source 引數是讓介面模型的開發更加靈活的關鍵,該引數是當前欄位外層物件的查詢結果,例如 username 欄位函式的 source 引數指代的是 user 的查詢結果:
user {
id,
username,
age,
}
這樣,我們可以在外層查詢結束后(廣度優先的),對內層進行更加精確的控制,
這種轉變需要改動前兩步,描述介面模型和提供欄位函式,或者說,這兩步在下面要介紹的推薦方法中是耦合的,
接下來我們需要關注兩個型別,GraphQLSchema和GraphQLObjectType,前者是我們熟悉的 schema,而后者則是模型型別,雖然是一個 js 物件,但卻用來描述一個 GQL 型別,例如 UserType(很有趣,因為 GQL.js 是 JavaScript 平臺下的一個抽象,所以出現了從 js 型別中構造出另一個抽象型別的情況),
GraphQLSchema物件用來直接提供給決議器,其構造接受一個 option,其中包含模型型別 query,我們現在僅關注 query 即可,其他的是用來實作增刪改資料等操作的,
const schema = new GraphQLSchema({
query: new GraphQLObjectType({...}),
});
現在需要從 GraphQL 提供的 DDL 轉化為對 GraphQLObjectType 物件的實體化:
type UserType {
id: String
username: String
age: Int
}
等價于:
下面的代碼中,每個欄位都擁有一個 resolve 函式,他就是當前欄位的(決議)函式,
注意,下面這些 resolve 都是默認實作,
const UserType = new graphql.GraphQLObjectType({
name: 'UserType',
fields: {
id: {
type: graphql.GraphQLString,
resolve: (source, args, context, info) => {
return source.id;
},
},
username: {
type: graphql.GraphQLString,
resolve: (source, args, context, info) => {
return source.username;
}
},
age: {
type: graphql.GraphQLInt,
resolve: (source, args, context, info) => {
return source.age;
}
},
}
});
這就是一個 GQL 的模型型別,可以直接填充到GraphQLSchema物件的實體化操作中:
const schema = new GraphQLSchema({
query: UserType,
});
此時的 schema 等價于:
type Query {
id: String
username: String
age: Int
}
現在我們利用 UserType 來實作以下 DLL:
type UserType {
id: String
username: String
age: Int
}
type Query {
user(id: String!): UserType
}
上述代碼等價于:
注意看下面代碼是如何定義引數的
const schema = new graphql.GraphQLSchema({
query: new graphql.GraphQLObjectType({
name: 'queryType',
fields: {
user: {
type: UserType,
args: {
id: {
type: graphql.GraphQLString,
defaultValue: '1001'
},
},
resolve: (source, args, context, info) => {
console.log(source, args, context, info);
return db.findById(args.id);
}
},
now: {
type: graphql.GraphQLString,
resolve: (source, args, context, info) => {
return new Date().toLocaleString();
}
}
}
}),
});
通過這種方法,我們將前兩步合到了一起,且提供了更靈活的決議函式結構,
完整代碼:
雖然形式上更加復雜了,但功能更加強大,
另外,注意,我沒有為決議器提供 root 引數,因為沒有必要,決議函式的結構已經體現在了介面模型的創建程序中,當然我們可以提供 root,不過 GQL.js 不會優先使用它,
const graphql = require('graphql');
const express = require('express');
const db = require('./db.js');
const app = express();
// 資料模型和查詢介面模型
let UserType = new graphql.GraphQLObjectType({
name: 'UserType',
fields: {
id: {
type: graphql.GraphQLString,
resolve: (source, args, context, info) => {
return source.id;
},
},
username: {
type: graphql.GraphQLString,
resolve: (source, args, context, info) => {
return source.username;
}
},
age: {
type: graphql.GraphQLInt,
resolve: (source, args, context, info) => {
return source.age;
}
},
}
});
let schema = new graphql.GraphQLSchema({
query: new graphql.GraphQLObjectType({
name: 'queryType',
fields: {
user: {
type: UserType,
args: {
id: {
type: graphql.GraphQLString,
defaultValue: '1001'
},
},
resolve: (source, args, context, info) => {
console.log(source, args, context, info);
return db.findById(args.id);
}
},
now: {
type: graphql.GraphQLString,
resolve: (source, args, context, info) => {
return new Date().toLocaleString();
}
}
}
}),
});
// 路由
app.use('/graphqlAPI', (req, res) => {
let reqJson = '';
req.on('data', (data) => reqJson += data);
req.on('end', () => {
reqJson = JSON.parse(reqJson);
graphql.graphql(schema, reqJson.query).then((result) => {
res.send(result);
});
})
});
app.listen(80, () => { console.log('listen on 80.'); });
GraphQLObjectType的 option 結構
完整結構見 https://graphql.org/graphql-js/type/#graphqlobjecttype
new GraphQLObjectType({
name: 'string',
fields: {
fieldName: {
type: GraphQLOutputType,
args: {
argName: {
type: GraphQLInputType,
defaultValue: any,
description: 'string',
},
...
},
resolve: (source, args, context, info) => any,
deprecationReason: 'string',
description: 'string'
},
...
},
});
使用express-graphql
我們是如何讓 GQL 與 express 互動的?
從 express 的一個路由拿到資料,然后交給 graphql 決議并查詢,最后回傳決議的結果,
接下來我們使用express-graphql來完成這個中間操作,express-graphql為express和graphql提供了一個薄薄的中間層,這個中間層以graphqlHTTP中間件形式實作,
安裝
npm i express-graphql
從express-graphql解構出中間件graphqlHTTP,然后開放在某個路由,
const express = require('express');
const { graphqlHTTP: graphqlMiddleware } = require('express-graphql');
const graphql = require('graphql');
const app = express();
// 描述介面模型
const schema = ...;
// 路由
app.use('/graphqlAPI', graphqlMiddleware({
// 介面模型
schema,
// 傳遞給 graphql 函式
rootValue,
context,
// 將 web 除錯應用開放在該路由,配置為 true 后訪瀏覽器問該路由即可
graphiql: true,
}));
app.listen(80, () => { console.log('listen on 80.'); });
context
剛才我們一直忽略了一個引數 context,他是 graphql 決議器的一個實參,也是欄位決議函式的一個形參,用于不同決議函式間的通信,或是決議器程序需要使用到的一些引數,可以通過 context 從外部向決議器傳入,
例如,想要在每個欄位決議函式中拿到 req, res,我們可以向外包一層 lambda 用來接收引數,隨后使用引數創建一個新的中間件并觸發他:
app.use('/graphqlAPI', (req, res) => graphqlMiddleware({
schema: schema,
context: { req, res },
graphiql: true,
})(req, res));
variables
一般一次 GQL 查詢總包含兩個東西,一個是 queryString,一個是 variables,我們剛才一直忽略了后者,
無傷大雅,我們只需要在前兩個實作中接收并傳入決議器即可,
決議器的完整引數:
graphql(
schema: GraphQLSchema,
requestString: string,
rootValue?: ?any,
contextValue?: ?any,
variableValues?: ?{[key: string]: any},
operationName?: ?string
): Promise<GraphQLResult>
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/253064.html
標籤:其他
