记录了我在组内的技术分享, 有同样需求的同学可以参考一下
分享全程下来时间大约 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
说了这么多也是因为学习期间遇到了好多坑, 不管怎么样最后还是可以使用起来了, 在之后的使用过程中还是会不断的总结并记录, 遇到问题我们可以一起讨论.
希望和你一起进步.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。