共计 12715 个字符,预计需要花费 32 分钟才能阅读完成。
记录了组内技术分享会, 有同样需求的同学可以参考一下
分享全程下来时间大约 55 分钟
前言
痛点:网上找的资料,文章,GraphQL 的官网,一看就是很‘自我’的大神写的(太烂了)完全管读者能不能看懂,举例子只讲用法!不告诉代码怎么实现的(但是当你学完这一篇你就可以看懂了),并且从来不晒出整体代码,导致根本不知道他们怎么玩的的,有被冒犯到!!可以说那些资料都不适合入门。
定位:GraphQL 并不是必须用的技术,但它是必须会的技术,之所以说它必会是因为可以靠它为‘前端’这一行业占领更大的‘领地’,同时它的思想是值得琢磨与体会的。
是啥:他不是 json 更不是 js, 他是 GraphQL 自身的语法, 兼容性非常好.
选择:GraphQL 为了使开发更高效、简洁、规范而生,如果对工程对团队造成了负担可以果断舍弃(别犹豫,这小子不是必需品),毕竟服务端多一层处理是会消耗性能的,那么就要理智计算他的“损失收益比”了。
前提:学习这门技术需要有一定的“前后交互”的知识储备,比如会 node,这里会介绍如何在 node 端使用,如何集成入 koa2 项目里面使用,并且会舍弃一些大家都用的技术,不做跟风党。
正文
一. GraphQL 到底干啥的?用它有啥好处哦?
这里是关键,一定要引起大家的兴趣,不然很难进行。
①:严格要求返回值
比如下面是后端返回的代码
{
name:1,
name:2,
name:3,
}
前端想要的代码
{name:1}
从上面可以看出,name2 与 name3 其实我不想要,那你传给我那么多数据干么,单纯为了浪费带宽么?但是吧。。也可理解为某些场景下确实很鸡肋,但是请求多了效果就厉害了。
②:设想一个场景,我想要通过文章的 id 获取作者信息,再通过作者信息里面的作者 id 请求他其他的作品列表,那么我就需要两步才能完成,但是如果这两个请求在服务端完成那么我们只用写一个请求,而且还不用找后端同学写新接口。
③: 控制默认值: 比如一个作品列表的数组, 没有作品的时候后端很肯能给你返的是null
, 但是我们更想保证一致性希望返回[]
, 这个时候可以用 GraphQL 进行规避.
二. 原生 GraphQL 尝鲜。
随便建个空白项目 npm install graphql
.
入口路径如下 src->index.js
var {graphql, buildSchema} = require('graphql');
// 1: 定义模板 / 映射, 有用 mongoose 操作数据库经验的同学应该很好理解这里
var schema = buildSchema(`
type Query {
# 我是备注, 这里的备注都是单个井号;
hello: String
name: String
}
`);
// 2: 数据源, 可以是热热乎乎从 mongodb 里面取出来的数据
var root = {hello: () => 'Hello!',
name:'金毛 cc',
age:5
};
// 3: 描述语句, 我要取什么样的数据, 我想要 hello 与 name 两个字段的数据, 其他的不要给我
const query = '{hello, name}'
// 4: 把准备好的食材放入锅内, 模型 -> 描述 -> 总体的返回值
graphql(schema, query, root).then((response) => {console.log(JSON.stringify(response));
});
上面的代码直接 node 就可以, 结果如下: {"data":{"hello":"Hello!","name":"金毛 cc"}}
;
逐一攻克
1: buildSchema
建立数据模型
var schema = buildSchema(
// 1. type: 指定类型的关键字
// 2. Query: 你可以理解为返回值的固定类型
// 3. 他并不是 json, 他是 graphql 的语法, 一定要注意它没有用 ','
// 4. 返回两个值, 并且值为字符串类型, 注意: string 小写会报错
` type Query {
hello: String
name: String
}
`);
GraphQL 中内置有一些标量类型 String、Int、Float、Boolean、ID,这几种都叫 scalar 类型, 意思是单个类型
2: const query = '{hello, name}'
做外层 {} 基本不变, hello 的意思就是我要这一层的 hello 字段, 注意这里用 ’,’ 分割, 之后会把这个 ’,’ 优化掉.
到这里一个最基本的例子写出来了, 感觉也没啥是吧, 我当时学到这里也感觉会很顺利, 但是 … 接下来文章断层, 官网表达不清, 社区不完善等等问题克服起来好心酸.
三. 利用库更好的原生开发
毕竟这样每次 node
命令执行不方便, 并且结果出现在控制台里也不好看, 所以我们要用一个专业工具 ’yoga’.
yarn add graphql-yoga
const {GraphQLServer} = require('graphql-yoga');
// 类型定义 增删改查
const typeDefs = `
type Query{
hello: String! #一定返回字符串
name: String
id:ID!
}
`
const resolvers = {
Query:{hello(){return '我是 cc 的主人'},
name(){return '鲁鲁'},
id(){return 9},
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(()=>{console.log('启动成功, 默认是 4000')
})
当然最好用 nodemon 来启动文件 npm install nodemon -g
-
hello: String!
像这种加了个 ’!’ 就是一定有值的意思, 没值会报错. - Query 这里定义的返回值, 对应函数的返回值会被执行.
- new GraphQLServer 的传参 定义的数据模型, 返回值, 因为具体的请求语句需要我们在 web 上面输入.
- id 的类型使用 ID 这个会把 id 转换为字符串, 这样设计也许是为了兼容所有形式的 id.
- server.start 很贴心的起一个服务配置好后效果如下: 左边是输入, 右边是返回的结果
四. 多层对象定义
我们返回 data 给前端, 基本都会有多层, 那么定义多层就要有讲究了
const {GraphQLServer} = require('graphql-yoga');
const typeDefs = `
type Query{me: User! # 这里把 me 这个 key 对应的 value 定义为 User 类型, 并且必须有值}
type User { # 首字母必须大写
name:String
}
`
const resolvers = {
Query:{me(){
return {
id:9,
name:'lulu'
}
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(()=>{console.log('启动成功, 默认是 4000')
})
- User 类型不是原生自带 , 所以我们要自己用 type 关键字定义一个 User 数据类型.(首字母必须大写)
- Query 里面 return 的值, 必须满足 User 类型的定义规则
当我们取 name
的值时:
我刚才故意在返回值里面写了 id, 那么可以取到值么?
结论: 就算数据里面有, 但是类型上没有定义, 那么这个值就是取不到的.
五. 数组
定义起来会有些特殊
const {GraphQLServer} = require('graphql-yoga');
const typeDefs = `
type Query {
# 返回是数组
arr:[Int!]!
}
`
const resolvers = {
Query: {arr() {return [1, 2, 3, 4]
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {console.log('启动成功, 默认是 4000')
})
- arr:[Int!] 如果写成 arr:[] 会爆错, 也就是说必须把数组里面的类型定义完全.
- Query 里面的返回值必须严格按照 type 里面定义的返回, 不然会报错.
结果如下:
六. 传参 ( 前端可以传参数, 供给服务端函数的执行
) 这个思路很神奇吧.
const {GraphQLServer} = require('graphql-yoga');
const typeDefs = `
type Query{greeting(name: String):String # 需要传参的地方, 必须在这里定义好
me: User!
}
type User { # 必须大写
name:String
}
`
const resolvers = {
Query: {
// 四个参数大有文章
greeting(parent, args, ctx, info) {return '默认值' + args.name},
me() {
return {
id: 9,
name: 'lulu'
}
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {console.log('启动成功, 默认是 4000')
})
-
greeting(name: String):String
greeting 是 key 没的说, 他接收一个 name 参数为字符串类型, 这里必须指明参数名字, 返回值也必须是字符串类型, 也就是 greeting 是一个字符串. -
greeting(parent, args, ctx, info) {
这里我们用到 args 也就是参数的集合是个对象, 我们 args.name 就可以取到 name 的值, 剩下的值后面用到会讲. - 既然说了要传参, 那就必须传参不然会报错
因为左侧的参数是要放在 url 请求上的, 所以要用双引号;
七. 关联关系
就像数据库建表一样, 我们不可能把所有数据放在一张表里, 我们可能会用一个 id 来指定另一张表里面的某些值的集合.
const {GraphQLServer} = require('graphql-yoga');
const typeDefs = `
type Query{lulu: User!}
type User{
name:String
age: Int
chongwu: Chongwu!
}
type Chongwu{
name:String!
age:Int
}
`
// 自定义的数据
const chongwuArr = {
1: {
name: 'cc',
age:8
},
2: {
name: '芒果',
age:6
},
9: {
name: '芒果主人',
age:24
}
}
const resolvers = {
Query: {lulu() {
return {
name: '鲁路修',
age: 24,
chongwu: 9
}
},
},
// 注意, 它是与 Query 并列的
User:{
// 1: parent 指的就是 user, 通过他来得到具体的参数
chongwu(parent,args,ctx,info){console.log('=======', parent.chongwu) // 9
return chongwuArr[parent.chongwu]
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {console.log('启动成功, 默认是 4000')
})
这里数据量有点多, 我慢慢解析
- lulu 属于 User 类, User 类里面的 chongwu(宠物)属于 Chongwu 类, 我们需要根据 chongwu 输入的 id 查询出 展示哪个宠物.
- 由于这个宠物的列表可能来自外部, 所以他的定义方式需要与 Query 同级.
- parent 指的就是父级数据, 也就是通过他可以获取到输入的 id.
效果如下:
这里就可以解释刚开始的一个问题, 就是那个通过文章 id 找到作者, 通过作者找到其他文章的问题, 这里的知识点就可以让我们把两个接口合二为一, 或者合 n 为一.
八. 不是获取, 是操作.
有没有发现上面我演示的都是获取数据, 接下来我们来说说操作数据, 也就是 ’ 增删改 ’ 没有 ’ 查 ’
graphql 规定此类操作需要放在 Mutation
这个类里面, 类似 vuex 会要求我们按照他的规范进行书写
const {GraphQLServer} = require('graphql-yoga');
const typeDefs = `
type Query{hello: String!}
# 是操作而不是获取, 增删改: 系列
type Mutation{createUser(name:String!, age:Int!):CreateUser
# 这里面可以继续书写 create 函数...
}
type CreateUser{
id:Int
msg:String
}
`
const resolvers = {
Query: {hello() {return '我是 cc 的主人'},
},
// query 并列
Mutation: {createUser(parent, args, ctx, info) {const {name,age} = args;
// 这里我们拿到了参数, 那么就可以去 awit 创建用户
return {
msg:'创建成功',
id:999
}
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {console.log('启动成功, 默认是 4000')
})
-
Mutation
是特殊类, 也是与Query
并列. - 一个
Mutation
里面可以写多个函数, 因为他是个集合. - 为函数的返回值也可以定义类型
效果如下: 接收 id 与提示信息
九. input 特殊类型
const {GraphQLServer} = require('graphql-yoga');
const typeDefs = `
type Query{hello: String!}
# 是操作而不是获取, 增删改: 系列
type Mutation{
# 这个 data 随便叫的, 叫啥都行, 就是单独搞了个 obj 包裹起来而已, 不咋地
createUser(data: CreateUserInput):CreateUser
}
type CreateUser{
id:Int
msg:String
}
# input 定义参数
input CreateUserInput{
# 里面的类型只能是基本类型
name: String!
age:Int!
}
`
const resolvers = {
Query: {hello() {return '我是 cc 的主人'},
},
// query 并列
Mutation: {createUser(parent, args, ctx, info) {
// ** 这里注意了, 这里就是 data 了, 而不是分撒开的了 **
const {data} = args;
return {
msg: '创建成功',
id: 999
}
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {console.log('启动成功, 默认是 4000')
})
- 把参数放在
data
里面, 然后定义 data 的类 - 注意三个关键点的代码都要改
效果与上面的没区别, 只是多包了层 data 如图:
这个只能说有利有弊吧, 多包了一层, 还多搞出一个类, 看似可以封装了实则 ’ 鸡肋啊鸡肋 ’
十. ‘ 更鸡肋的 ’MutationType
特殊类型
const {GraphQLServer} = require('graphql-yoga');
// 非常鸡肋, 这种事做不做, 该不该你做, 心里没点数
const typeDefs = `
type Query{hello: MutationType}
enum MutationType{
aaaa
bbbb
cccc
}
`
const resolvers = {
Query:{hello(){
// 只能返回菜单里面的内容, 这样可以保证不出格... p 用
return 'bbbb'
},
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(()=>{console.log('启动成功, 默认是 4000')
})
- 我定义了一个
MutationType
的类, 限制只能用 ’aaa’,’bbb’,’ccc’ 中的一个字符串. - 这不猫捉耗子么? graphql 本身定位不是干这个事的, 这种事情交给统一的数据校验模块完成, 他做了校验的话那么其他情况他管不管? 关了又如何你又不改数据, 就会个报错公鸡想下蛋.
- 完全不建议用这个, 当做了解, 具体的校验模块自己在中间件或者 utils 里面写.
十一. 集成进 koa2 项目
1. 终于到实战了, 讲了那么多的原生就是为了从最基本的技术点来理解这里
2. 并不一定完全使用 graphql 的规范, 完全可以只有 3 个接口用它
3. 我们刚才写的那些 type 都是在模板字符串里面, 所以肯定有人要他模板拆解开, 以对象的形式去书写才符合人类的习惯.
先建立一个 koa 的工程
// 若果你没有 koa 的话, 建议你先去学 koa, koa 知识点比较少所以我暂时没写相应的文章.koa2 graphqlx
// main: 工程名 不要与库重名npm install graphql koa-graphql koa-mount --save
大朗快把库安装好.
app.js 文件里面
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')
////// 看这里
const mount = require('koa-mount');
const graphqlHTTP = require('koa-graphql');
const GraphQLSchema=require('./schema/default.js');
//////
const index = require('./routes/index')
const users = require('./routes/users')
// error handler
onerror(app)
// middlewares
app.use(bodyparser({enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(views(__dirname + '/views', {extension: 'pug'}))
// 每一个路径, 对应一个操作
app.use(mount('/graphql', graphqlHTTP({
schema: GraphQLSchema,
graphiql: true // 这里可以关闭调试模式, 默认是 false
})));
// logger
app.use(async (ctx, next) => {const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {console.error('server error', err, ctx)
});
module.exports = app
- 我直接吧默认配置也粘进来了, 这样可以保证你拿走就用
- graphiql: true 这个时候开启了调试模式 会出现下图的调试界面, 默认是 false
- mount 来包裹整体的路由
- graphqlHTTP 定义请求相关数据
- GraphQLSchema 使我们接下来要写的一个操作模块.
这个画面是不是似曾相识!
schema->default.js
const {
GraphQLID,
GraphQLInt,
GraphQLList,
GraphQLString,
GraphQLSchema,
GraphQLNonNull,
GraphQLObjectType,
GraphQLInputObjectType,
} = require('graphql');
// id 对应的详情
let idArr = {
1:{
name:'我是 id1',
age:'19'
},
2:{
name:'我是 id2',
age:'24'
}
}
// 定义 id 的类
let GID= new GraphQLObjectType({
name: 'gid',
fields: {name: { type: GraphQLString},
age: {type: GraphQLString},
}
})
// 参数类型 不太对
let cs = new GraphQLInputObjectType({
name:'iddecanshu',
fields: {id: { type: GraphQLString},
}
})
// 定义导航 Schema 类型
var GraphQLNav = new GraphQLObjectType({
name: 'nav',
fields: {
cc:{ // 传参
type:GraphQLString,
// args:new GraphQLNonNull(cs), // 1; 这种是错的
args:{
data: {type:new GraphQLNonNull(cs), // 这种可以用 data 为载体了
}
},
// args:{ // 3:这种最好用了。。。// id:{
// type:GraphQLString
// }
// },
resolve(parent,args){return '我传的是' + args.data.id}
},
// greeting(name: String):String
title: {type: GraphQLString},
url: {type: GraphQLString},
id: {// type:GraphQLList(GID), // 这里很容易忽略
type:GraphQLNonNull(GID), // 反复查找也没有专门 obj 的 这里用非空代替
async resolve(parent,args){// console.log('wwwwwwwww', idArr[parent.id])
// 这个 bug 我 tm。。。。。// 需要是数组形式。。。。不然报错
// "Expected Iterable, but did not find one for field \"nav.id\".",
// return [idArr[parent.id]];
// 2: 更改类型后就对了
return idArr[parent.id] || {}}
},
}
})
// 定义根
var QueryRoot = new GraphQLObjectType({
name: "RootQueryType",
fields: {
navList: {type: GraphQLList(GraphQLNav),
async resolve(parent, args) {
var navList = [{ title: 'title1', url: 'url1', id:'1'},
{title: 'title2', url: 'url2', id:'2'}
]
return navList;
}
}
}
})
// 增加数据
const MutationRoot = new GraphQLObjectType({
name: "Mutation",
fields: {
addNav: {
type: GraphQLNav,
args: {title: { type: new GraphQLNonNull(GraphQLString) },
},
async resolve(parent, args) {
return {msg: '插入成功'}
}
}
}
})
module.exports = new GraphQLSchema({
query: QueryRoot,
mutation: MutationRoot
});
十二. koa2 中的使用原理 ” 逐句 ” 解析
①引入
- 这个是原生自带的, 比如我们会把
GraphQLID
这种象征着单一类型的类单独拿到. - 定义 type 的方法也变成了 GraphQLObjectType 这样的实例化类来定义.
const {
GraphQLID,
GraphQLInt,
GraphQLList,
GraphQLString,
GraphQLSchema,
GraphQLNonNull,
GraphQLObjectType,
GraphQLInputObjectType,
} = require('graphql');
②单一的类
- 我们实例化
GraphQLObjectType
导出一个 ’type’ - 使用 type:GraphQLString 的形式规定一个变量的类型
- name: 这里的 name 可以理解为一个说明, 有时候可以通过获取这个值做一些事.
let GID= new GraphQLObjectType({
name: 'gid',
fields: {name: { type: GraphQLString},
age: {type: GraphQLString},
}
})
③ 定义根类
- fields 必须要写, 在它里面才可以定义参数
-
GraphQLList
意思就是必须为数组 - type 不能少, 里面要规定好这组返回数据的具体类型
- resolve 也是必须有的没有会报错, 并且必须返回值与 type 一致
var QueryRoot = new GraphQLObjectType({
name: "RootQueryType",
fields: {
navList: {type: GraphQLList(GraphQLNav),
async resolve(parent, args) {
var navList = [{ title: 'title1', url: 'url1', id:'1'},
{title: 'title2', url: 'url2', id:'2'}
]
return navList;
}
}
}
})
十三. koa2 里面的关联关系与传参
这里的关联关系是指, 之前我们说过的 id 指向另一个表
let GID= new GraphQLObjectType({
name: 'gid',
fields: {name: { type: GraphQLString},
age: {type: GraphQLString},
}
})
var GraphQLNav = new GraphQLObjectType({
name: 'nav',
fields: {
cc:{
type:GraphQLString,
args:{
data:
type:new GraphQLNonNull(cs), // 这种可以用 data 为载体了
}
},
resolve(parent,args){return '我传的是' + args.data.id}
},
id: {type:GraphQLNonNull(GID),
async resolve(parent,args){return idArr[parent.id] || {}}
},
}
})
- 上面 cc 这个变量比较特殊, 他需要 args 这个 key 来规范参数, 这里可以直接写参也可以像这里一样抽象一个类.
- id 他规范了 id 对应的是一个对象, 里面有 name 有 age
- cc 想要拿到传参就需要 args.data 因为这里我们用的 input 类来做的
实际效果如图所示:
十四. 对集成在 koa2 内的工程化思考(数据模型分块)
1. 从上面那些例子里面可看出, 我们可以用 /api/blog 这种路由 path 为单位, 去封装一个个的数据模型
2. 每个模型里面其实都需要操作数据库
3. 说实话增加的代码有点多, 这里只演示了 2 个接口就已经这么大了
4. 学习成本是不可忽略的, 而且这里面各种古怪的语法报错
十五. 前端的调用
这里我们以 vue 为例import axios from "axios";
这个是前提query=
这个是关键点, 我们以后的参数都要走这里
方式 1(暴力调取)
created(){
// 1: 查询列表
// ①: 一定要转码, 因为 url 上不要有{} 空格
axios
.get("/graphql?query=%7B%0A%20%20navList%20%7B%0A%20%20%20%20title%0A%20%20%20%20url%0A%20%20%7D%0A%7D%0A")
.then(res => {console.log("返回值: 1", res.data);
});
}
方式 2(封装函数)
methods: {getQuery() {
const res = `
{
navList {
title
url
id {
name
age
}
}
}`;
return encodeURI(res);
},
},
created() {axios.get(`/graphql?query=${this.getQuery()}`).then(res => {console.log("返回值: 2", res.data);
});
}
方式 3(函数传参)
methods: {getQuery2(id) {
const res = `
{
navList {cc(data:{id:"${id}"})
title
url
id {
name
age
}
}
}`;
return encodeURI(res);
}
},
created() {axios.get(`/graphql?query=${this.getQuery2(1)}`).then(res => {console.log("返回值: 3", res.data);
});
}
十六. 前端插件的调研
- 一看前面的传参方式就会发觉, 这肯定不合理啊, 一定要把字符串解构出来.
-
vue-apollo
技术栈是当前比较主流的, 但是这个库写的太碎了, 并且配置起来还要更改我本来的代码习惯. - 又在 github 上面找了一些模板的库, 但是并没有让我从书写字符串的尴尬中解脱出来
所以暂时没有找到我认可的库, 当然了自己暂时也并不想把时间放在开发这个插件上.
一七. 我想要的插件什么样的
- 可以使我, 采用对象或者 json 的方式来定义 query
- 不需要定义那么多概念, 开箱即用, 我只需要那个核心的模板解决方案
- 不要改变我本来的代码书写方式, 比如说
vue-apollo
提供了新的请求方式, 可是我的项目都是 axios, 我就是不想换 凭什么要换
十八. 学习 graphql 有多少阻碍
- 网上的资料真的对初学者很不友好, 根本不举例子, 导致每个知识点都会我自己试了几十次试出来的.
- 光学这一个技术不够, 还要思考服务端的重构与新规范, 前端也要新规范.
- 这个技术也出来好几年了, 但是社区真的不敢恭维, 所以啊还是需要自己更多的精力投入进来.
十九. 此类技术趋势的思考
- 前端工程师越来越不满足自己的代码只在浏览器上运行了, 我们也要参与到服务端交互的环节中.
- 身为当今 2020 年的前端如果眼光还局限在 web 端有可能会被后浪排在沙滩上了.
- 个人不太喜欢这种抛出很多新的知识点, 和架构体系的库, 应该是越少的学习成本与代码成本做到最多的事.
- 前端技术还在摸索阶段, 也可以说是扩展阶段, 预计这几年还是会出现各种新的概念, 但是吧正所谓 ” 合久必分, 分久必合 ”, 在不就的将来前端的范围很可能重新划定.
二十. end
说了这么多也是因为学习期间遇到了好多坑, 不管怎么样最后还是可以使用起来了, 在之后的使用过程中还是会不断的总结并记录, 遇到问题我们可以一起讨论.
希望和你一起进步.