查看原文
其他

GraphQL-前端进阶的利剑与桥梁

The following article is from 大转转FE Author 大转转FE

点击上方“IT平头哥联盟”,选择“置顶或者星标”

与您一起成长~


作者 | 卢铭


基本概念

GraphQL

GraphQL 是一种用于 API 的查询语言,由Facebook开发和开源,是使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

背景介绍

相信看了上面的基本概念,大家都是和我一样一脸萌萌哒。所以这里就需要介绍一下其产生的背景和原因。

在我们目前的前后端开发过程中,大部分都是以http请求服务端接口的方式完成交互过程的。在这种场景下,每当需求变化,就需要修改或创建一个新的接口去满足特定的需求。

举个栗子: 在一个商品详情页,当我们需要获取商品详情时,服务端会给前端一个接口,例如:

https://www.example.com/getInfoById?infoId=000000

当前端请求接口时,会返回给一个固定格式的数据,例如:


  1.    data:{


  2.        title:'商品的标题',


  3.        content:'商品的描述内容',


  4.        special:'商品特点',


  5.        price:'商品价格',


  6.        image:'商品的图片'


  7.    }



前端接收到数据后,进行各种相应的处理展示,最终将包含有商品标题,商品描述,商品特点,商品价格,商品图片信息的页面展示给用户。

一切看起来都很美好,直到有一天……

产品大摇大摆的走过来,轻描淡写的说道:“能不能把商品的特点去掉,加一个商品的库存,另外还需要再加一个卖家的模块进去。包含卖家的名称和头像,可以点进卖家的详情页,也不用太着急,午饭前上线就行。”

于是前后端坐在一起开始商量,前端弱弱的说:“能不能改一下你的接口,把产品不要的都去掉,产品需要的都加上”。

后端心里说,你当我傻啊,于是一边砸吧嘴一边赶忙说道:“这样改风险太大,好多数据都不在一个表,我不好查。这样,详情页那个接口我就不改了,你不显示不就完了嘛,万一哪天产品那小子的小子再想起来加上,咱俩还得忙活。库存再给你一个接口,卖家信息再给你一个接口,完美,就这么定了。”

前端还想再说什么,可后端的背影已经随着产品越走越远。

就在前端绝望之时,霹雳一声震天响,graphql闪亮登场。

在graphql模式下,假设我们的服务端部分已经部署完成,前端使用vue框架,那么前端部分的请求就可以简化为:

  1.  apollo: {


  2.    goods: {


  3.      query() {


  4.        return gql`{


  5.            goods(infoId:"${this.infoId}"){


  6.              title


  7.              content


  8.              price


  9.              image


  10.          }


  11.        }`


  12.      }


  13.    },


  14.    store: {


  15.      query() {


  16.        return gql`{


  17.            store(infoId:"${this.infoId}"){


  18.              store


  19.          }


  20.        }`


  21.      }


  22.    },


  23.    seller: {


  24.      query() {


  25.        return gql`{


  26.            seller(infoId:"${this.infoId}"){


  27.              name


  28.              age


  29.          }


  30.        }`


  31.      }


  32.    }


  33.  }


可以看到graphql为我们定义了一种类似sql的查询语言,而这种查询语言是用于api的。和之前的数据请求处理不同,在这里,我们只要定义好需要的数据,其他的不再关心,我们就可以按需索取需要的数据。这对于我们的开发提供了更大的自由与便利,只要数据支持,我们就可以摆脱对于服务端接口的依赖,提高生产效率,赢得自由,完成前端的逆袭。

前后端实践

讲完了故事,我们开始讲一些实际的干货。 对于graphql,网上已经有很多实践经验,以下部分是在参考完成实践经验并自我实践后给出的总结归纳。

服务端

服务端的技术选型,我们使用了eggjs框架,配合egg-graphqlegg-graphql插件完成。

1.安装依赖包
  1. $ npm install --save egg-graphql


2.开启插件
  1. // config/plugin.js


  2. exports.graphql = {


  3.  enable: true,


  4.  package: 'egg-graphql',


  5. };


  6. //开启 cros 跨域访问


  7. exports.cors = {


  8.    enable: true,


  9.    package: 'egg-cors'


  10. }


3.配置graphql路由和跨域
  1. //config/config.default.js


  2. // graphql路由


  3. config.graphql = {


  4. router: '/graphql',


  5. // 是否加载到 app 上,默认开启


  6. app: true,


  7. // 是否加载到 agent 上,默认关闭


  8. agent: false,


  9. // 是否加载开发者工具 graphiql, 默认开启。路由同 router 字段。使用浏览器打开该可见。


  10. graphiql: true,


  11. // graphQL 路由前的拦截器


  12. onPreGraphQL: function*(ctx) {},


  13. // 开发工具 graphiQL 路由前的拦截器,建议用于做权限操作(如只提供开发者使用)


  14. onPreGraphiQL: function*(ctx) {},


  15. }


  16. // cors跨域


  17. config.cors = {    


  18.    origin: '*',    


  19.    allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS'


  20. }


4.开启graphql中间件
  1. //config/config.default.js


  2. exports.middleware = [ 'graphql' ];


项目配置告于段落。

5.编写业务员代码

下面开始写代码。 目录结构如下:

  1. ├── app


  2.   ├── graphql                          //graphql 代码,所有和graphql相关的代码都在这里,已经在前面做好了配置


  3.      └── common                   //通用类型定义,graphql有一套自己的系统类型,除此之外还可以自定义


  4.       |    |── scalars                 //自定义类型定义


  5.       |    |  └──  date.js            // 日期类型实现


  6.       |   └── resolver.js            //合并所有全局类型定义


  7.       |   └── schema.graphql     // schema 定义


  8.      └──goods                       // 商品详情的graphql模型


  9.           └── connector.js         //连接数据服务


  10.           └── resolver.js            //类型实现,和goods中schema.graphql定义的模型相对应,是其具体的实现


  11.           └── schema.graphql    //schema 定义,在这里定义商品详情数据对象


  12.      └──store                        // 库存的graphql模型


  13.           └── connector.js         //连接数据服务


  14.           └── resolver.js            //类型实现


  15.           └── schema.graphql    //schema 定义,在这里定义商品详情数据对象


  16.      └──seller                       // 卖家信息的graphql模型


  17.           └── connector.js         //连接数据服务


  18.           └── resolver.js            //类型实现


  19.           └── schema.graphql    //schema 定义,在这里定义商品详情数据对象


  20.      └──query                       // 所有的查询都会经过这里,这里是一个总的入口


  21.           └── schema.graphql    //schema 定义


  22.   ├── service


  23.     └── goods.js                    //商品详情的具体实现


  24.     └── store.js                     //库存的具体业务逻辑


  25.     └── seller.js                     //卖家信息的具体业务逻辑


  26.   └── router.js


app/graphql/query/schema.graphql是整个graphql查询的总入口,所有需要查询的对象都要在这里定义。它的定义形式如下:

  1. #定义查询对象,在graphql里的注释使用#号


  2. type Query {


  3. goods(


  4.   #查询条件,相当于接口的入参商品id


  5.   infoId: ID!


  6. ):Goods #Goods是在app/graphql/goods/schema.graphql中定义的商品详情


  7. }


在总入口中涉及到的查询对象,都需要在graphql文件夹下建立相应的文件夹,如上文中提到的goods,就在app/graphql文件夹中存在相应的goods文件夹。goods文件夹包含三个部分:schema.graphql,resolve.js和connector.js。 schema.graphql中需要定义总入口中提及的Goods对象:

  1. # 商品


  2. type Goods {


  3.    # 流水号


  4.    infoId: ID!


  5.    # 商品标题


  6.    title:String!,


  7.    # 商品内容


  8.    content:'String!,


  9.    #商品特点


  10.    special:'String!,


  11.    #商品价格


  12.    price:'nt!,


  13.    #商品图片


  14.    image:'String!,


  15. }


graphql自带一组默认标量类型,包括Int,Float,String,Boolean,ID。在定义字段时需要注明类型,这也是graphql的特点之一,是支持强类型的。如果非空,就在类型后面跟上一个!号。graphql还包括枚举类型,列表和自定义类型,具体可以查看相关文档。

resolve.js是数据类型的具体实现,依赖connector.js完成:

  1. 'use strict'


  2. module.exports = {


  3.  Query: {


  4.        goods(root, {infoId}, ctx) {


  5.        return ctx.connector.goods.fetchById(infoId)


  6.  }


  7. }


connector.js是连接数据的具体实现,可以使用dataloader来降低数据访问频次,提高性能:

  1. 'use strict'


  2. //引入dataloader,是由facebook推出,能大幅降低数据库的访问频次,经常在Graphql场景中使用


  3. const DataLoader = require('dataloader')


  4. class GoodsConnector {


  5.    constructor(ctx) {  


  6.        this.ctx = ctx  


  7.        this.loader = new DataLoader(id=>this.fetch(id))


  8.    }


  9.    fetch(id) {  


  10.        const goods = this.ctx.service.goods  


  11.        return new Promise(function(resolve, reject) {    


  12.            const goodsInfo = goods.getInfoById(id)    


  13.            resolve([goodsInfo])  //注意这里需要返回数组形式


  14.        })


  15.    }


  16.    fetchById(id) {  


  17.        return this.loader.load(id)


  18.    }


  19. }


  20. module.exports = GoodsConnector


上面代码中涉及的this.ctx.service.goods就是app/service文件夹下的goods.js文件导出的方法对象,也就是获取数据的具体业务逻辑:

  1. const Service = require('egg').Service


  2. const {createAPI} = require('../util/request')//实现的http请求


  3. class GoodsService extends Service {


  4. // 获取商品详情


  5.    async getInfoById(infoId) {


  6.        const result = await createAPI(this, 'example/getInfoById', 'get', {infoId})


  7.        return result


  8.    }


  9. }


  10. module.exports = GoodsService


获取数据可以用你能实现的任何方式,可以直接从数据库获取,也可以用http从现有的接口获取。 这样一个使用egg框架实现的graphql服务就完成了。 下面说一下前端。

前端

我们会使用vue配合Apollo完成前端搭建。

1 安装依赖包
  1. npm install --save vue-apollo apollo-client


2.引用apollo
  1. import { ApolloClient } from 'apollo-client'


  2. import { HttpLink } from 'apollo-link-http'


  3. import { InMemoryCache } from 'apollo-cache-inmemory'


  4. import VueApollo from 'vue-apollo'


3.配置链接
  1. const httpLink = new HttpLink({


  2.  // 需要配置一个绝对路径


  3.  uri: 'http://exzample.com/graphql',


  4. })


4.创建ApolloClient实例和PROVIDER
  1. // Create the apollo client


  2. const apolloClient = new ApolloClient({


  3.  link: httpLink,


  4.  cache: new InMemoryCache(),


  5.  connectToDevTools: true,


  6. })


  7. const apolloProvider = new VueApollo({


  8.  defaultClient: apolloClient,


  9. })


4.在vue中引入使用
  1. Vue.use(VueApollo);


5.根实例引用
  1.    var vm = new Vue({


  2.      el: '#app',


  3.      provide: apolloProvider.provide(),


  4.      router,


  5.      components: {


  6.        app: App


  7.      },


  8.      render: createEle => createEle('app')


  9.    })


6.使用
  1. <script>


  2. import gql from "graphql-tag";


  3. export default {


  4.    data() {


  5.        return {


  6.            goods:{},


  7.            infoId:123123


  8.        };


  9.    },


  10.    apollo: {


  11.        goods: {


  12.            query() {


  13.                return gql`{


  14.                    goods(infoId:"${this.infoId}"){


  15.                        title


  16.                        content


  17.                        price


  18.                        image


  19.                    }


  20.                }`


  21.            }


  22.        },


  23.     }


  24. };


  25. </script>


展望

graphql对于目前接口数量多,难维护,扩展成本高,数据格式不可预知,文档难维护等问题给出了一个相对完善的方案,相信在未来,它将是我们工作中不可或缺的一部分。

- end -


用心分享 一起成长 做有温度的攻城狮

每天记得对自己说:你是最棒的!


往期热点

关于重绘和回流,你真的了解了?

面试官问:能否模拟实现JS的new操作符
2018文章汇总

看完这几道 Promise 面试题,还被面试官问倒就活该~

!福利,2019免费送书 5折掘金小册码


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存