初始化&安装依赖
npm init -y
npm i express graphql express-graphql -s
server.js
const express = require('express')
const {buildSchema} = require('graphql')
const {graphqlHTTP} = require('express-graphql')
// 定义schema查询和类型
const schema = buildSchema(`
type Query {
hello: String
}
`)
//定义查询对应的处理器
const rootValue = {
hello: ()=> {
return 'hello world!'
}
}
const app = express()
app.use('/graphql', graphqlHTTP({
schema,
rootValue,
graphiql: true,
}))
app.listen(3000,function(){
console.log('开始监听3000端口');
})
运行服务器
nodemon server.js
graphqlAPI测试
简单类型
localhost:3000/graphql
文本内容谷歌翻译过了
输入hello,点击▶运行,中间返回
可以增加模式和返回值的数量,来进行更多数据的查询
// 定义schema查询和类型
const schema = buildSchema(`
type Query {
hello: String
userName: String
age: Int
}
`)
//定义查询对应的处理器
const root = {
hello: () => {
return 'hello world!'
},
userName: () => {
return 'Max'
},
age: () => {
return 18
}
}
刷新测试页面,可以查询新增的数据
如果不需要某条数据,可以去掉,来减少所需返回的数据
复杂类型
当所需返回的数据内嵌套了对象,就需要定义个内部对象的类型
模式内type定义对象类型,嵌套进Query,下方root通过函数返回这个对象,即使前端请求的可能不是全部,也要全部返回
// 定义schema查询和类型
const schema = buildSchema(`
type Contact {
tel: Int
qq: Int
signal: String
}
type Query {
hello: String
userName: String
age: Int
contact: Contact
}
`)
//定义查询对应的处理器
const root = {
hello: () => {
return 'hello world!'
},
userName: () => {
return 'Max'
},
age: () => {
return 18
},
contact: () => {
return {
tel: '13923456789',
qq: 279123456,
signal: 'Maxuan'
}
}
}
这里故意写错了一个 tel为整数Int,但是返回了一个字符串,来看看会产生什么问题
测试页面刷新点击右侧查询会发现多了contact,左侧输入contact运行
graphql会帮助自动补全,由于上方故意产生的错误,tel返回值为null,返回值对象除了data,还多了一个errors,包含了错误信息、位置和目录
强类型检查会帮助我们在开发阶段就避免出错
点击右侧Contact可以查看到这个嵌套的对象类型,当所需的数据并非全部,可以在请求时只写需要的数据
参数类型&传递参数
参数类型
- 参数的基本类型:String、Int、Float、Boolean 和 ID,可以在shema声明的时候直接使用
ID本质为字符串,在数据库中唯一的一个,当使用ID作为参数类型,一旦出现重复的值是会报错的
- [类型]代表数组,例如[Int]代表所有成员为都为整数的数组
参数传递
-
和js传递参数一样,小括号内定义形参,但是注意:参数需要定义类型
-
! 代表类型值不能为null,否则会报错
-
Query是模式的查询入口
type Query { rollDice(number: Int!, numSides: Int): [Int] }
当定义一个接口,需要确定哪些参数必须要传递,必须传递的加上 ! ,不加则是可以不传,只写类型就可以了,返回值则是一个纯整数成员的数组
查询语句 Query
需要传参的时候,查询语句为函数传参,参数为必填,并且类型定义返回值成员为纯字符串的数组
// 定义schema查询和类型
const schema = buildSchema(`
type Query {
getClassMembers(classNum: Int!): [String]
}
`)
//定义查询对应的处理器
const root = {
getClassMembers({classNum}) {
const obj = {
31: ['张三','李四','王五'],
61: ['张小三','李小四','王小五']
}
return obj[classNum]
}
}
客户端查询通过方法查询,这里测试下,如果不传参数返回的错误信息
错误1
错误2
如果传入null
错误3
查询结果不存在
正确返回
通过正确的传参获取数据
复杂类型传参
服务端查询语句
// 定义schema查询和类型
const schema = buildSchema(`
type Student {
name: String
age: Int
gender: String
subject(fraction: Int!): [String]
}
type Query {
getClassMembers(classNum: Int!): [String]
student(studentName: String): Student
}
`)
//定义查询对应的处理器
const root = {
getClassMembers({classNum}) {
const obj = {
31: ['张三','李四','王五'],
61: ['张小三','李小四','王小五']
}
return obj[classNum]
},
student({studentName}) {
return{
name: studentName,
age: 18,
gender: 'male',
subject: ({fraction}) => {
if(fraction > 80) {
return ['数学','物理','化学']
} else {
return ['数学','物理','化学','语文','历史','地理']
}
}
}
}
}
客户端查询语句(客户端查询语句不支持单引号),调用方法传入参数查,内部不需要传参查询的属性会被自动补全
内部一个需要传参的函数,调用并且传参,取得返回数据
客户端访问 GraphQL Clients
客户端访问graphql接口
在server.js写入并创建 public/index.html入口文件
// 公共文件夹访问静态资源
app.use(express.static('public'))
fetch API
<body>
<button onclick="getStudent()">获取数据</button>
</body>
<script>
const variables = { studentName: "Max", fraction: 90};
const query = `
query student($studentName: String, $fraction: Int!) {
student(studentName: $studentName) {
name
age
gender
subject(fraction: $fraction)
}
}
`;
function getStudent() {
fetch("/graphql", {
method: "POST", // graphql的所有请求都必须是POST
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
query,
variables
}),
}).then(res => res.json())
.then(data => {
console.log(data);
})
}
</script>
在函数内声明 查询语句query 和 参数variables,这两个为约定变量不可更改,query Student类型并传入参数,开头加$,并且类型要和模式出声明保持一致,如果有 ! 这里也不能漏掉,然后传给返回数据的student对象
const query = `
query Student($studentName: String, $fraction: Int!) {
student(studentName: $studentName) {
name
age
gender
subject(fraction: $fraction)
}
}
`;
点击按钮获取数据
Axios API
query查询语句
<body>
<button onclick="getStudent()">获取数据</button>
</body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const variables = { studentName: "Max", fraction: 90};
const query = `
query Student($studentName: String, $fraction: Int!) {
student(studentName: $studentName) {
name
age
gender
subject(fraction: $fraction)
}
}
`;
function getStudent(){
axios({
method: "POST",
url: "/graphql",
data:{
query,
variables
}
}).then(res => {
console.log(res.data);
})
}
</script>
mutation 变更语句
mutation语句同样也是用query作为变量名传入,还有一个区别就是query语句内的query是可以省略的,mutation则是一定不能少
const variables = {userInput: {name: "Maxxx", age: 25, gender: "male"}}
const query = `
mutation createUser($userInput: userInput){
createUser(input: $userInput) {
name
}
}
`;
function getStudent(){
axios({
method: "POST",
url: "/graphql",
data:{
query,
variables
}
}).then(res => {
console.log(res.data);
})
}
添加&修改数据 Mutation
代码写法
数据的查询用Query,数据添加、修改和删除则都是通过Mutation,并且接收的参数如果是一个对象,这个对象并不能通过type来定义,而是必须通过input类型
// 定义schema查询和类型
// 要区分input类型和type类型,这里一样并不代表实际情况输入值和返回值一样
const schema = buildSchema(`
input userInput {
name: String
age: Int
gender: String
}
type User{
name: String
age: Int
gender: String
}
type Mutation {
createUser(input: userInput): User
updateUser(id: ID!, input: userInput): User
}
`)
// 虚拟数据库
const fakeDB = {}
//定义查询对应的处理器
const root = {
createUser({input}) {
// 相当于数据库的保存
fakeDB[input.name] = input
// 返回保存结果
return fakeDB[input.name]
},
updateUser({id, input}) {
// 相当于数据库更新
const update = Object.assign({},fakeDB[id], input)
fakeDB[id] = update
// 返回保存结果
return fakeDB[id]
}
}
故障排除
运行服务器,刷新测试页面会看到加载中,不显示模式
这是一个GraphQL的坑,要求模式内,就算是只负责增改删,也必须有一个 Query 查询
添加一个查询语句,返回数据为转换为数组的虚拟数据库,刷新测试页面,就可以看到模式了
添加数据
接下来通过mutation来添加user,由于有返回数据,需要取回哪些数据也是要写的,否则会报错,中间显示返回的数据
接下来再添加另一个user
查询验证
修改数据
接下来尝试修改数据
查询验证
认证与中间件
添加中间件,判断是否有权限,处理请求
const app = express()
const middlware = (req, res, next) => {
// 当请求信息为方位 /graphql 接口,且请求头没有auth字样,则返回错误
if(req.url.indexOf('/graphql') && req.headers.cookie.indexOf('auth') === -1) {
res.send(JSON.stringify({
error: "您没有权限访问这个接口"
}))
return
}
next()
}
app.use(middlware)
由于没有auth,刷新测试页面会显示错误信息,查看Cookie,并没有auth
可以手动添加cookie
刷新页面,请求存在auth,放行请求,可以看到页面已经可以访问
与数据库结合
创建数据库
打开数据库管理工具,连接mysql数据库,创建gqltest数据库和表,注意id设置自增长
server.js
安装mysql依赖并引入和配置
npm i mysql -s
const mysql = require('mysql')
const pool = mysql.createPool({
connectionLimit: 10,
host: '192.168.31.11',
port: '3307',
user: 'root',
password: '123456',
database: 'gqltest'
})
createUser数据库新增
createUser({input}) {
return new Promise((resolve, reject)=> {
pool.query('insert into users set ?', input, err => {
if(err) {
console.log('出错了' + err.message)
return
}
resolve(input)
})
})
}
测试页面输入语句
可以看到成功返回了信息
刷新数据库,也看到数据库新增的数据
users数据库查询
users() {
return new Promise((resolve, reject) => {
pool.query('select name, age, gender from users', (err, res) => {
if(err) {
console.log('出错' + err.message)
return
}
resolve(res)
})
})
}
查询测试
updateUser数据库更新
updateUser({id, input}) {
return new Promise((resolve, reject)=> {
pool.query('update users set ? where name = ?', [input, id], err => {
if(err) {
console.log('出错了'+ err.message)
return
}
resolve(input)
})
})
}
测试语句
查看数据库
deleteUser数据库删除
deleteUser({id}) {
return Promise((resolve, reject) => {
pool.query('delete from users where name = ?', [id], err => {
if(err) {
console.log('出错了' + err.message)
reject(false)
return
}
resolve(true)
})
})
}
测试语句
查看数据库
ApolloGraphQL
介绍
Apollo是一个开源的GraphQL开发平台,提供了符合GraphQL规范的服务端和客户端实现,使用Apollo可以更方便的开发使用GraphQL
基本使用
Apollo Server - Apollo GraphQL Docs
右边侧栏
- 第一步 —— 创建项目
- 第二步 —— 安装依赖
- 第三步 —— 定义GraphQL schema
- 第四步 —— 定义数据集合
- 第五步 —— 定义 resolver
- 第六步 —— 创建ApolloServer实例
- 第七步 —— 启动ApolloServer
- 第八步 —— 执行第一个查询
- 组合实例
- 下一步
初始化&安装依赖
npm init -y
npm i apollo-server graphql -s
vscode安装Apollo GraphQL插件可以实现模板字符串高亮
index.js
const { ApolloServer, gql } = require('apollo-server')
// 第三步 —— 定义schema
const typeDefs = gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`
// 第四步 —— 定义数据集合
const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
},
{
title: 'City of Glass',
author: 'Paul Auster',
}
]
//第五步 —— 定义resolver
//resolver(解析器)定义了获取schema的方式
//这个解析器从上面的books数组中检索书籍
const resolvers = {
//所有的Query都在这里
Query: {
books: () => books
}
}
//第六步 —— 创建ApolloServer需要的两个参数
const server = new ApolloServer({ typeDefs, resolvers })
//第七步 —— listen 方法启动一个网络服务器,默认为localhost:4000
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
})
启动服务器
nodemon index.js
打开 localhost:4000
点击按钮进入查询测试页面测试语句,这个测试页面的功能更强大,使用简便,不再需要手动输入,只需要点击左侧对应查询语句的 + 就可以将变量按照结构添加进去
结合express使用
ApolloServer本身就提供web服务的功能,但是实际上在做生产服务开发的时候,建议还是和主流的服务器框架结合使用
www.apollographql.com/docs/apollo…
Apollo提供了集成到express和koa这类服务器框架,作为中间件导入使用
初始化&安装依赖
npm init -y
npm i apollo-server-express apollo-server-core express graphql -s
index.js
const { ApolloServer, gql } = require('apollo-server-express')
const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core')
const express = require('express')
const http = require('http')
const app = express()
const typeDefs = gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`
const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
},
{
title: 'City of Glass',
author: 'Paul Auster',
}
]
const resolvers = {
Query: {
books: () => books
}
}
async function startApolloServer(typeDefs, resolvers) {
const httpServer = http.createServer(app)
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
})
await server.start()
server.applyMiddleware({ app })
await new Promise(resolve => httpServer.listen({ port: 4000 }, resolve))
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
}
startApolloServer(typeDefs,resolvers)
启动服务器
nodemon index.js
查询语句测试
与原生GraphQL的区别
模式的定义没有区别,resolver(解析器)这里是有一些区别的,使用官方文档中的例子
基本语法
基本语法没有区别
const typeDefs = gql`
type Query {
numberSix: Int! # Should always return the number 6 when queried
numberSeven: Int! # Should always return 7
}
`
const resolvers = {
Query: {
numberSix() {
return 6
},
numberSeven() {
return 7
}
}
}
处理参数
下面代码希望通过id字段来查询用户,并且定义了一个数组作为可查询对象
const typeDefs = gql`
type User {
id: ID!
name: String
}
type Query {
user(id: ID!): User
}
`
const users = [
{ id: '1', name: 'Elizabeth Bennet'},
{ id: '2', name: 'Fitzwilliam Darcy'}
]
const resolvers = {
Query: {
user(parent, args, context, info) {
return users.find(user => user.id === args.id)
}
}
}
这里定义的resolvers解析器,可以选择接收四个参数
args —— 客户端查询参数
就和原有的args一样
parent —— 解析链
官方提供的一个示例,意思是 查询语句libraries 返回的类型为Library对象的数组,Library对象内的Book类型的数组,Book类型的author又是通过Author类型来定义的,这样层层嵌套
const typeDefs = gql`
type Library {
branch: String!
books: [Book!]
}
type Book {
title: String!
author: Author!
}
type Author {
name: String!
}
type Query {
libraries: [Library]
}
`
针对这个模式的查询语句的写法则是
query GetBooksByLibrary {
libraries {
books {
author {
name
}
}
}
}
这个解析器链与查询本身的层次结构匹配
解析器按照上面的顺序执行,并将返回值通过parent将参数传给解析链中的下一个解析器
// 定义schema
const typeDefs = gql`
type Library {
branch: String!
books: [Book!]
}
type Book {
title: String!
author: Author!
}
type Author {
name: String!
}
type Query {
libraries: [Library]
}
`
// 虚拟数据
// librarys图书馆定义了branch分支,通过branch来查询books中属于这个分支的数并返回
const libraries = [
{branch: 'downtown'},
{branch: 'riverside'},
]
const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
branch: 'riverside'
},
{
title: 'City of Glass',
author: 'Paul Auster',
branch: 'downtown'
},
]
// 解析器
const resolvers = {
Query: {
libraries() {
return libraries
}
},
Library: {
books(parent) {
return books.filter(book => book.branch === parent.branch)
}
},
Book: {
author(parent) {
return {
name: parent.author
}
}
}
}
这个解析链看起来是这样的,上一级的查询参数会通过parent传递给下级查询语句,存在多层则会层层传递下去
context —— 上下文对象
任何GraphQL请求都会经过这里,这个函数接收请求对象,通过接收请求体,返回为一个包含请求体数据的对象,来自定义context,后续的每个resolver都可以直接获取context内的数据
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
context (req) {
return {
foo: "bar"
}
}
})
当然,作为形参是不能直接写context的,需要写前面两个,现在再次发起请求,服务器端就会打印出这个对象
const resolvers = {
Query: {
libraries(parent, args, context) {
console.log(context)
return libraries
}
}
}
比如客户端在对应需要权限的操作上发起请求的同时传递用户的登录信息,ApollowServer接收到请求后将请求体携带的用户登录信息的数据放到context内,接下来的resolver就可以获取到并判断
从MongoDB中获取数据
Data sources - Apollo Server - Apollo GraphQL Docs
在resolver中获取数据的方法有很多种,包括将RESTful API的数据请求过来,映射到ApolloServer的 grphql schema当中,适用于把传统的API迁移到GraphQL当中,原始的RESTful API不用动,映射过来做一个处理就可以开发使用了
node - mongose.js - 掘金 (juejin.cn)
安装依赖
npm i mongoose -s
引入模块&连接数据库
创建models目录
models/index.js 引入mongoose模块,连接数据库等操作
const mongoose = require('mongoose')
// 连接、配置数据库,返回一个待定的连接
mongoose.connect('mongodb://192.168.31.107:27017', {
user: 'maxuan',
pass: '601109',
dbName: 'blog',
autoIndex: false,
useNewUrlParser: true
})
// 获取数据库对象
const db = mongoose.connection
// 连接失败的警告
db.on('error', console.error.bind(console, 'connection error:'))
// 连接成功
db.once('open',async ()=> {
console.log("数据库连接成功!")
})
// 引入users模型并抛出
module.exports = {
users: require('./users')
}
models/users.js 导入mongoose模块,声明mongoose schema编译为模型并抛出,在models/index.js引入
const mongoose = require('mongoose')
const usersSchema = new mongoose.Schema({
name: String,
age: Number,
gender: String
})
// 把usersSchema编译为一个模型
module.exports = mongoose.model("users", usersSchema)
index.js 引入models/index.js并解构出mongoose的users模型,可以一并执行连接数据库操作
const { users } = require('./models')
const typeDefs = gql`
type User {
_id: ID
name: String,
age: Int
gender: String
}
type Query {
users: [User!]
}
`
const resolvers = {
Query: {
async users () { // 接收到查询请求,向mongoose的users发起查询
return await users.find()
}
}
}
启动服务器
nodemon index.js
查询测试
通过id查询单个user
const { users } = require('./models')
const typeDefs = gql`
type User {
_id: ID
name: String,
age: Int
gender: String
}
type Query {
users: [User!]
user(id: ID!): User
}
`
const resolvers = {
Query: {
async users () {
return await users.find()
},
async user (parent, { id }) {
return await users.findById(id)
}
}
}
勾选生成查询语句,勾选Arguments id,在下方userId后输入,运行查询语句,返回结果
使用DataSources方式获取数据
上面从resolvers操作数据库的方法实际上是不太建议的做法,因为resolvers和数据库是紧紧地耦合在一起了,如果当前数据向通过RESTful API映射返回给客户端,代码肯定是要重新调整
比较建议的是对数据库相关的操作代码进行封装
官网给出了github的实例和文档
GraphQLGuide/apollo-datasource-mongodb: Apollo data source for MongoDB (github.com)
安装依赖
npm i apollo-datasource-mongodb
这个包使用DataLoader对批处理和每个请求进行缓存,它还可以选择进行共享应用程序缓存 ( 如果提供了ttl的话 ) ( 使用默认的 Apollo InMemoryLRUCache 或提供给ApolloServer() 的缓存 )
它对以下的API请求做了缓存:
- findOneById(id, options)
- findManyById(ids, options)
- findByFields(fields, options)
用法基础
基本的设置是子类化MongoDataSource,将集合或Mongoose模型传递给构造函数,并使用API方法通过ID来查询数据
创建 data-sources/Users.js
注意这里是直接对 this 使用 findOneById() API
const { MongoDataSource } = require('apollo-datasource-mongodb')
class Users extends MongoDataSource {
getUser(userId) {
return this.findOneById(userId)
}
}
module.exports = Users
index.js导入这个子类
ApolloServer内添加dataSources,这个函数返回一个对象,实例化data-sources/Users.js内的Users类并传入mongoose模型
const Users = require('./data-sources/Users')
//ApolloServer内添加dataSources
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
context (req) {
return {
}
},
dataSources() {
return {
users: new Users(users)
}
}
})
resolvers 写法变更
dataSources会将实例添加到context上,解构取得后,通过dataSources.users来访问Users实例 Users实例内有个getUser方法,并且接收id作为参数
原本的在resolvers内操作数据库变成了在resolvers内调用data-sources目录下的子类内的方法
const resolvers = {
Query: {
async user (parent, { id }, { dataSources }) {
return await dataSources.users.getUser(id)
}
}
}
通过 this.model 访问数据模型对象
DataSources只提供了上面三个API对数据进行缓存来处理请求,其他的数据库操作,就用mongoose方法
在data-sources/Users.js的子类内添加其他方法对数据库进行操作
const { MongoDataSource } = require('apollo-datasource-mongodb')
class Users extends MongoDataSource {
getUser(userId) {
return this.findOneById(userId)
},
// 返回所有users
getUsers() {
return this.model.find()
}
}
module.exports = Users
index.js内 resolvers的处理,需要注意因为用到dataSources,即便是用不到parent和id, (parent, {id}, { dataSources }) 形参是必须要写的
const resolvers = {
Query: {
async user (parent, { id }, { dataSources }) {
return await dataSources.users.getUser(id)
},
async users (parent, { id }, { dataSources }) {
return await dataSources.users.getUsers()
}
}
}
查询语句正常响应
这样处理的好处还有,将来可以把resolvers定义在任何地方,resolvers获取数据的时没有任何依赖,它是通过dataSources获取数据,不需要在这里关心数据是从RESTful API、文件、数据库之中哪个来的,只需要找dataSources拿数据就可以了
dataSources的数据,由data-sources下的子类对数据库进行操作来获取,将来数据源变了,也在子类内进行更改,对于resolvers内的核心逻辑来说,基本上不用动,这就是ApolloServer推荐的做法