graphql 与 restful
restful
属性状态转移,本质就是用定义uri,通过api接口来获取资源。通用系统架构,不受语言限制
- restful一个接口只返回一个资源,graphql一次可以获取多个资源
- restful用不同的url来区分资源,graphql用类型区分资源
express+graphql
最基本的写法
const express = require('express');
const {buildSchema} = require('graphql');
const {graphqlHTTP} = require('express-graphql');
// 定义schema,查询和类型
const schema = buildSchema(`
type Query {
hello: String
}
`)
// 定义查询对应的解析器
const root = {
hello: () => {
return "hello world";
}
}
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
app.listen(9000, () => {
console.log("http://localhost:9000/graphql")
});
自定义类型
自定义类型就是先在Query中对要定义的类型进行说明,然后再自己在外面对这个类型进行定义
const express = require('express');
const {
buildSchema
} = require('graphql');
const {
graphqlHTTP
} = require('express-graphql');
// 定义schema,查询和类型
const schema = buildSchema(`
type Account { //
name: String
age: Int
gender: String
department: String
}
type Query {
hello: String
account: Account //
}
`)
// 定义查询对应的解析器
const root = {
hello: () => {
return "hello world";
},
account: () => {
return {
name: "Doris",
age: 18,
gender: "female",
department: "xi'an"
}
}
}
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
app.listen(9000, () => {
console.log("http://localhost:9000/graphql")
});
通过刚才两个例子可以看出graphql的运行模式,首先分为两个部分,一部分为schema
区,一部分为root
区:
-
schema
区- 定义查询
query
的语句及类型(包括自己的自定义类型)
- 定义查询
-
root
区- 定义查询对应的
resolver
,也就是查询对应的解析器
- 定义查询对应的
比如说上面代码中,在 schema 中定义了一个hello的查询,在 root 中就定义这个 hello 的方法对应的实现,也就是 hello 具体要做什么业务
⚠️ root 中 return 的返回值的类型应该和 schema 中的定义类型保持一致
参数类型
基本参数类型,可以在 schema 中直接使用
- String
- Int
- Boolean
- Float
- ID
[类型]
代表数组类型,如: [int] 代表 Int类型的数组
参数传递
基本参数类型的参数传递
type Query{
getName(Id: ID!, Age: Int): [Int]
}
- 和 js 传参一样,小括号内定义形参,参数需要定义类型
!
代表桉树不能为空
⚠️ getName(Id: ID!, Age: Int): [Int]
这句最后的[Int]
的意思是getName
这个查询的返回值是 Int 类型的数组
下面来看一下例子:
其中先在schema中定义一个getClassMates()的查询语句,加上形参,注意返回来的数据为string类型的数组。然后在root 中定义getClassMates()这个查询的解析器,其中 getClassMates({classNo})
中的 {classNo}
其实是从arg中进行的结构赋值,当然也可以写成arg.classNo
const express = require('express');
const {
buildSchema
} = require('graphql');
const {
graphqlHTTP
} = require('express-graphql');
// 定义schema,查询和类型
const schema = buildSchema(`
type Query{
getClassMates(classNo: Int!): [String]
}
`)
// 定义查询对应的解析器
const root = {
getClassMates({ classNo }){
const obj = {
61 : ['doris', 'doris1', 'doris2'],
62 : ['Doris', 'Doris1', 'Doris2']
}
return obj[classNo];
}
}
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
app.listen(9000, () => {
console.log("http://localhost:9000/graphql")
});
复杂参数类型的参数传递
比如说之前例子中的Account
类型
下面的例子中有一个salary
,在schema中这个salary是可以传一个city
的参数的,这样在对应的root里面,salary就可以被当作一个function来处理,可以在里面进行条件判断等等
const express = require('express');
const {
buildSchema
} = require('graphql');
const {
graphqlHTTP
} = require('express-graphql');
// 定义schema,查询和类型
const schema = buildSchema(`
type Account {
name: String
age: Int
gender: String
department: String
salary(city: String): Int
}
type Query{
getClassMates(classNo: Int!): [String]
account(userName: String): Account
}
`)
// 定义查询对应的解析器
const root = {
getClassMates({ classNo }){
const obj = {
61 : ['doris', 'doris1', 'doris2'],
62 : ['Doris', 'Doris1', 'Doris2']
}
return obj[classNo];
},
account({userName}){
const name = userName;
const age = 18;
const gender = 'female';
const department = "xi'an"
const salary = ({city}) => {
if(city === '北京' || city === '上海' || city === '广州' || city === '深圳'){
return 10000;
}
return 8000;
}
return {
name,
age,
gender,
department,
salary
}
}
}
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
app.listen(9000, () => {
console.log("http://localhost:9000/graphql")
});
GraphQL Clients
如何在客户端访问graphql接口
- 先在之前的js文件中添加一句可以在访问外部静态文件的代码
// 公开文件夹,供用户访问静态资源
app.use(express, static('public'));
- 新建
public
文件夹,并在其中新建index.html
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<button onclick="getData()">获取数据</button>
</body>
<script>
function getData() {
// 这里的query是一个字符串类型,
// 这个字符串规定了这个接口的名字,参数,参数类型等
// 实际上它是来自服务器端的定义
// 这里从Account的参数中拿到$userName后将它给到account的userName
const query = `
query Account($userName: String){
account(userName: $userName)
}
`
// 定义variables进行传值
const variables = {userName: "doris"};
// 通过fetch向外发数据
fetch('/graphql', {
method: "POST",
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
query: query,
variables: variables
})
}).then(res => res.json).then(json => {
console.log(data);
})
}
</script>
</html>
然后运行 js 文件,运行完毕之后在localhost:9000/index.html进行对应的操作
但是这时候在这个index页面中点击获取数据按钮的时候会报错👇
是因为我们在查询的时候并没有告诉它我们需要什么样的字段,所以我们应该在
const query = `
query Account($userName: String){
account(userName: $userName)
}
`
这个代码中加入需要的字段
const query = `
query Account($userName: String){
account(userName: $userName){
name
age
gender
department
salary(city: "上海")
}
}
`
这个时候就可以看到结果了
当然,salary
中的city
值也可以通过参数传递进来
const query = `
query Account($userName: String, $city: String){
account(userName: $userName){
name
age
gender
department
salary(city: $city)
}
}
`
// 定义variables进行传值
const variables = {userName: "doris", city: "西安"};
Mutations
定义修改mutation
其实和定义查询Query
是类似的
Query
const schema = buildSchema(`
type Account { //
name: String
age: Int
gender: String
department: String
}
type Query {
hello: String
account: Account //
}
`)
Mutation
const schema = buildSchema(`
input AccountInput { //
name: String
age: Int
gender: String
department: String
salary: Int
}
type Account {
name: String
age: Int
gender: String
department: String
salary: Int
}
type Mutation {
createAccount(input: AccountInput): Account
}
`)
在type Mutation
中:
- 定义
createAccount
方法的形参(input: AccountInput)
,其中形参input
的类型为AccountInput
,这个AccountInput
类型的定义和之前在query中定义类型的方法是类似的,不同的是,这个类型在定义的时候用的是input
,也就是input AccountInput{}
,而不是和查询中定义一样用的是query
- 定义
createAccount
方法的返回类型Account
,因为基本来说返回值最后都是通过Query
得到的,所以这里的Account
的定义还是通过query的方式来进行,也就是前面应该是type
const express = require('express');
const {
buildSchema
} = require('graphql');
const {
graphqlHTTP
} = require('express-graphql');
// 定义schema,查询和类型
const schema = buildSchema(`
input AccountInput {
name: String
age: Int
gender: String
department: String
}
type Account {
name: String
age: Int
gender: String
department: String
}
type Mutation {
createAccount(input: AccountInput): Account
updateAccount(id: ID, input: AccountInput): Account
}
`)
// 定义一个假的数据库
const fakeDb = {};
// 定义查询对应的解析器
const root = {
// 这里的input相当于拿到的所有字段
createAccount({ input }){
// 相当于数据库的保存
fakeDb[input.name] = input;
// 返回保存的结果
return fakeDb[input.name];
},
updateAccount({ id, input }){
// 相当于数据库的更新
// 从fakeDb中拿到需要的fakeDb[id],拿到之后将input新的属性覆盖到它身上
const updatedAccount = Object.assign({}, fakeDb[id], input);
// 将更新保存到数据库
fakeDb[id] = updatedAccount;
// 返回更新的结果
return updatedAccount;
}
}
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
app.listen(9000, () => {
console.log("http://localhost:9000/graphql")
});
node mutationtest_1.js
之后,发现在http://localhost:9000/graphql发现error👇
这是为什么呢???
⚠️ 这是因为graphql中必须要有一个query,也就是说你不能只往里面写,你必须要与一个读的操作
所以就需要添加一个关于查询的Query
了
// 添加到schema
type Query {
accounts: [Account]
}
// 添加到root
accounts(){
return fakeDb;
}
然后运行
存入之后进行query时,发现还在报错👇
这个错的原因是因为返回的值不是一个数组,所以这块需要将root
下面的acounts
下面return fakeDb
修改一下,因为可以在代码中看到这块的fakeDb是一个对象,所以下面需要改一下
const express = require('express');
const {
buildSchema
} = require('graphql');
const {
graphqlHTTP
} = require('express-graphql');
// 定义schema,查询和类型
const schema = buildSchema(`
input AccountInput {
name: String
age: Int
gender: String
department: String
}
type Account {
name: String
age: Int
gender: String
department: String
}
type Mutation {
createAccount(input: AccountInput): Account
updateAccount(id: ID, input: AccountInput): Account
}
type Query {
accounts: [Account]
}
`)
// 定义一个假的数据库
const fakeDb = {};
// 定义查询对应的解析器
const root = {
accounts(){ // 这里做了修改
var arr = [];
for(const i in fakeDb){
arr.push(fakeDb[i]);
}
return arr;
},
// 这里的input相当于拿到的所有字段
createAccount({ input }){
// 相当于数据库的保存
fakeDb[input.name] = input;
// 返回保存的结果
return fakeDb[input.name];
},
updateAccount({ id, input }){
// 相当于数据库的更新
// 从fakeDb中拿到需要的fakeDb[id],拿到之后将input新的属性覆盖到它身上
const updatedAccount = Object.assign({}, fakeDb[id], input);
// 将更新保存到数据库
fakeDb[id] = updatedAccount;
// 返回更新的结果
return updatedAccount;
}
}
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
app.listen(9000, () => {
console.log("http://localhost:9000/graphql")
});
因为重启之后,之前的数据就消失了,所以需要重新存一次数据
下面来看一下怎么 updateAccount,这里的id暂时先用name来代替
那修改了之后到底有没有存进数据库还是得通过query来看
所以是成功存进去了
认证和中间件
控制具有权限的人去访问数据接口,需要借助于express的中间件来实现
中间件的本质实际上是一个
function,它在接口执行之前首先拦截请求,然后决定是往下走还是返回错误消息
const express = require('express');
const {
buildSchema
} = require('graphql');
const {
graphqlHTTP
} = require('express-graphql');
// 定义schema,查询和类型
const schema = buildSchema(`
input AccountInput {
name: String
age: Int
gender: String
department: String
}
type Account {
name: String
age: Int
gender: String
department: String
}
type Mutation {
createAccount(input: AccountInput): Account
updateAccount(id: ID, input: AccountInput): Account
}
type Query {
accounts: [Account]
}
`)
// 定义一个假的数据库
const fakeDb = {};
// 定义查询对应的解析器
const root = {
accounts(){
var arr = [];
for(const i in fakeDb){
arr.push(fakeDb[i]);
}
return arr;
},
// 这里的input相当于拿到的所有字段
createAccount({ input }){
// 相当于数据库的保存
fakeDb[input.name] = input;
// 返回保存的结果
return fakeDb[input.name];
},
updateAccount({ id, input }){
// 相当于数据库的更新
// 从fakeDb中拿到需要的fakeDb[id],拿到之后将input新的属性覆盖到它身上
const updatedAccount = Object.assign({}, fakeDb[id], input);
// 将更新保存到数据库
fakeDb[id] = updatedAccount;
// 返回更新的结果
return updatedAccount;
}
}
const app = express();
// 中间件
const middleware = (req, res, next) => {
// 判断访问地址中包含/graphql这个字样并且cookie中没有auth的时候
if(req.url.indexOf('/graphql') !== -1 && req.headers.cookie.indexOf('auth') === -1){
res.send(JSON.stringify({
error: "当前没有权限访问该接口"
}));
return;
}
next();
};
// 中间件的注册
app.use(middleware);
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
app.listen(9000, () => {
console.log("http://localhost:9000/graphql")
});
middleware
中先检测访问的地址是不是/graphql
,如果是这个地址,接着看一下cookie
中是否包含auth
这个字样,如果没有包含则抛出错误提示,否则就next()
正常往下走
上面没有权限访问的原因是因为cookie中没有auth
这个字段,可以在application中加一个之后刷新访问就可以
上面是通过一个小例子来说明中间件对权限验证,在真正的项目中是需要跟服务器端进行配合的:服务器端生成一个token,将token交给前端,前端在访问接口时将token带回来,拿到token之后对它进行一个检查
构造类型
之前的schema是通过buildSchema👇
const schema = buildSchema(`
// 定义类型
type Account {
name: String
age: Int
gender: String
department: String
}
// 定义Query
type Query {
accounts: [Account]
}
`)
现在要说的是将类型的定义变成构造函数的形式进行,之前一步可以完成的现在需要三步,先定义type
,然后定义Query
,最后将Query
转成schema
👇
- 使用GeaphQLObjectType定义type(类型)
var AccountType = new graphql.GraphQLObjectType({
name: 'Account',
fields: {
name: { type: graphql.GraphQLString },
age: { type: graphql.GraphQLInt },
gender: { type: graphql.GraphQLString },
department: { type: graphql.GraphQLString }
}
});
- 使用GeaphQLObjectType定义query(查询)
var queryType = new graphql.GraphQLObjectType({
name: 'Query',
fields: {
account: {
type: AccountType,
args: {
username: {
type: graphql.GraphQLString
},
},
resolver: function (_, { username }) {
const name = username;
const age = 18;
const gender = 'female';
const department = 'xian';
return {
name,
age,
gender,
department
}
}
}
}
});
- 创建schema
var schema = new graphql.GraphQLSchema({ query: queryType });
const express = require('express');
const { graphql } = require('graphql');
const { buildSchema } = require('graphql');
const { graphqlHTTP } = require('express-graphql');
var AccountType = new graphql.GraphQLObjectType({
name: 'Account',
fields: {
name: { type: graphql.GraphQLString },
age: { type: graphql.GraphQLInt },
gender: { type: graphql.GraphQLString },
department: { type: graphql.GraphQLString }
}
});
var queryType = new graphql.GraphQLObjectType({
name: 'Query',
fields: {
account: {
type: AccountType,
args: {
username: {
type: graphql.GraphQLString
},
},
resolver: function (_, { username }) {
const name = username;
const age = 18;
const gender = 'female';
const department = 'xian';
return {
name,
age,
gender,
department
}
}
}
}
});
var schema = new graphql.GraphQLSchema({ query: queryType });
// 定义一个假的数据库
const fakeDb = {};
// 定义查询对应的解析器
const root = {
accounts() {
var arr = [];
for (const i in fakeDb) {
arr.push(fakeDb[i]);
}
return arr;
},
// 这里的input相当于拿到的所有字段
createAccount({ input }) {
// 相当于数据库的保存
fakeDb[input.name] = input;
// 返回保存的结果
return fakeDb[input.name];
},
updateAccount({ id, input }) {
// 相当于数据库的更新
// 从fakeDb中拿到需要的fakeDb[id],拿到之后将input新的属性覆盖到它身上
const updatedAccount = Object.assign({}, fakeDb[id], input);
// 将更新保存到数据库
fakeDb[id] = updatedAccount;
// 返回更新的结果
return updatedAccount;
}
}
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
app.listen(9000, () => {
console.log("http://localhost:9000/graphql")
});
这样的变化带来的好处是比较好维护,哪块出错了之后就可以精确到出错的那个type