GraphQL+Koa2实现服务端API结合Apollo+Vue

一、GraphQL介绍

1.1 简介

GraphQL 是一种新的 API 的查询语言,它提供了一种更高效、强大和灵活 API 查询。它 是由 Facebook 开发和开源,目前由来自世界各地的大公司和个人维护。GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且 没有任何冗余。它弥补了 RESTful API(字段冗余,扩展性差、无法聚合 api、无法定义数据 类型、网络请求次数多)等不足

注意:GraphQL 是 api 的查询语言,而不是数据库。从这个意义上说,它是数据库无关的, 而且可以在使用 API 的任何环境中有效使用,我们可以理解为 GraphQL 是基于 API 之上的一 层封装,目的是为了更好,更灵活的适用于业务的需求变化

GraphQL 可以用在常见各种服务器端语言以及客户端语言中

  • 服务器端语言:C# / .NET、Clojure、Elixir、Erlang、Go、Groovy、Java、JavaScript、PHP、Python、 Scala、Ruby

  • 客户端语言:js、React + React Native、Angular、Vue.js、Apollo Link、Native iOS、Native Android、 Scala.js

  • 中文文档:http://graphql.cn

  • Github: https://github.com/facebook/graphql

GraphQL 出现的历史背景

当提起API设计的时候,大家通常会想到SOAP(一种简单的基于 XML 的协议),RESTful 等设计方式,从 2000 年 RESTful 的理论被提出的时候,在业界引起了很大反响,因为这种 设计理念更易于用户的使用,所以便很快的被大家所接受。

我们知道 REST 是一种从服务 器公开数据的流行方式。当 REST 的概念被提及出来时,客户端应用程序对数据的需求相 对简单,而开发的速度并没有达到今天的水平

因此 REST 对于许多应用程序来说是非常 适合的。然而在业务越发复杂,客户对系统的扩展性有了更高的要求时,API 环境发生了巨 大的变,RESTful 显得心有余而力不足。比如:字段冗余,扩展性差、无法聚合 api、无法 定义数据类型、网络请求次数多

GraphQL 的出现整好弥补了 RESTful APi 的不足

使用 GraphQL 的公司

目前已经有很多的公司在使用 GraphQL(https://graphql.org/users/

1.2 为什么推荐 GraphQL 而不是 RESTful API

在过去的十多年中,REST 已经成为设计 web api 的标准(虽然只是一个模糊的标准)。

它提供了一些很棒的想法,比如无状态服务器和结构化的资源访问。

然而 REST api 表 现得过于僵化,无法跟上访问它们的客户的快速变化的需求

RESTful API 不足

  • 扩展性(多个终端需要返回不同的字段),单个 RESTful 接口返回数据越来越 臃肿。前端对于真正用到的字段是没有直观映像的,仅仅通过 url 地址,无法预测也无 法回忆返回的字段数目和字段是否有效,接口返回 50 个字段,但却只用 5 个字段,造 成字段冗余,扩展性差,单个 RESTful 接口返回数据越来越臃肿
  • API 聚合问题,某个前端展现,实际需要调用多个独立的 RESTful API 才能获 取到足够的数据,导致网络请求次数多
  • 前后端字段频繁改动,导致类型不一致,错误的数据类型可能会导致网站出错 尤其是在业务多变的场景中,很难在保证工程质量的同时快速满足业务需求

GraphQL 的优点

  • 吸收了 RESTful API 的特性
  • 所见即所得 各种不同的前端框架和平台可以指定自己需要的字段。查询的返回结果就是输 入的查询结构的精确映射

客户端可以自定义 Api 聚合

如果设计的数据结构是从属的,直接就能在查询语句中指定;即使数据结构是独 立的,也可以在查询语句中指定上下文,只需要一次网络请求,就能获得资源和子 资源的数据。

代码即是文档

GraphQL 会把 schema 定义和相关的注释生成可视化的文档,从而使得代码的变更,直接就反映到最新的文档上,避免 RESTful 中手工维护可能会造成代码、 文档不一致的问题

参数类型强校验

  • RESTful 方案本身没有对参数的类型做规定,往往都需要自行实现参数的校验机制, 以确保安全。
  • 但 GraphQL 提供了强类型的 schema 机制,从而天然确保了参数类型的合法性

二、GraphQl类型系统

2.1 GraphQl类型

可以将GraphQL的类型系统分为标量类型(ScalarTypes,标量类型)和其他高级数据类型,标量类型即可以表示最细粒度数据结构的数据类型,可以和JavaScript的原始类型对应

GraphQL规范目前规定支持的标量类型有

  • Int:有符号32位整数 -- GraphQLInt
  • Float:有符号双精度浮点值 -- GraphQLFloat
  • StringUTF‐8字符序列 -- GraphQLString
  • Booleantrue或者false -- GraphQLBoolean
  • ID(GraphQLID):ID标量类型表示一个唯一标识符,通常用以重新获取对象或者作为缓存中的键。ID类型使用和String一样的方式序列化;然而将其定义为ID意味着并不需要可读型。

GraphQL其他高级数据类型包括

  • Object:对象(newGraphQLObjectType)

用于描述层级或者树形数据结构。对于树形数据结构来说,叶子字段的类型都是标量数据类型。几乎所有GraphQL类型都是对象类型。Object类型有一个name字段,以及一个很重要的fields字段。fields字段可以描述出一个完整的数据结构。例如一个表示地址数据结构的GraphQL对象为

const AddressType=newGraphQLObjectType({
    name:'Address',
    fields:{
        street:{
            type:GraphQLString
        },
        number:{
            type:GraphQLInt
        },
        formatted:{
            type:GraphQLString,
            resolve(obj){
                return obj.number+''+obj.street
            }   
        }
    }
});
  • Interface:接口用于描述多个类型的通用字
  • Union:联合类型用于描述某个字段能够支持的所有返回类型以及具体请求真正的返回类型
  • Enum:枚举用于表示可枚举数据结构的类型
  • InputObject:输入对象
  • List:列表

列表是其他类型的封装,通常用于对象字段的描述。例如下面PersonType类型数据的parents和children字段

const PersonType=newGraphQLObjectType({
    name:'Person',
    fields:()=>({
        parents:{type:newGraphQLList(Person)},
        children:{type:newGraphQLList(Person)},
    })
})
  • Non-Null:不能为Null

Non-Null强制类型的值不能为null,并且在请求出错时一定会报错。可以用于必须保证值不能为null的字段。例如数据库的行的id字段不能为null

const RowType=newGraphQLObjectType({
    name:'Row',
    fields:()=>({
        id:{
            type:newGraphQLNonNull(GraphQLString)
        }
    })
})

2.2 GraphQl查询语言

GraphQL规范支持两种操作

  • query:仅获取数据(fetch)的只读请求
  • mutation:获取数据后还有写操作的请求

新版本的GraphQL还支持subscription,这是为了处理订阅更新这种比较复杂的实时数据更新场景而设计的操作

三、Express中集成GraphQl 实现 Server API

3.1 安装mongodb造数据

使用mongodb做数据库演示,mac安装mongodbbrew install mongodb-community

# 进入mongo shell
mongo 

# 创建数据库
use graphql (graphql数据库不存在会自动创建)

# 创建nav、articlecate集合插入数据
db.nav.insert({name: "标题1", url: "/", sort: 1, add_time: "2022-06-30"})
db.nav.insert({name: "标题2", url: "/", sort: 1, add_time: "2022-06-30"})
db.nav.insert({name: "标题3", url: "/", sort: 1, add_time: "2022-06-30"})

db.articlecate.insert({name: "分类1", description: "描述", keywords: "关键词", status: 1})
db.articlecate.insert({name: "分类2", description: "描述", keywords: "关键词", status: 1})
db.articlecate.insert({name: "分类3", description: "描述", keywords: "关键词", status: 1})

或者导入数据库数据

下载数据库文件解压并导入mongodb即可 https://blog.poetries.top/db/koa.zip

导入mongodb数据库

mongorestore -h localhost:27017 -d koa-demo(数据库名称,不存在会自动创建) ./dump(本地数据文件路径)

3.2 express集成GraphQl

https://github.com/graphql/express-graphql

npm install express-graphql graphql--save

引入express-graphql配置中间件

app完善配置

// app.js
var express=require('express');
var DB=require('./model/db.js'); 
const graphqlHTTP = require('express-graphql');
const GraphQLDefaultSchema = require('./schema/default.js');

var app=express(); 

// 配置中间件
app.use('/graphql', graphqlHTTP({
    schema: GraphQLDefaultSchema,
    graphiql: true // 线上环境关闭,开发环境开启
}));

//配置路由
app.get('/',function(req,res){
    res.send('hello express');
})

app.listen(3000,()=>console.log("http://localhost:3000"));

定义GraphQLSchema模型

  • 新建schema/default.js
  • 定义Schema
const DB=require('../model/db.js'); /*引入DB库*/

const {
    GraphQLObjectType,
    GraphQLString,
    GraphQLInt,
    GraphQLSchema,
    GraphQLList
} =require('graphql')

//1、获取导航列表     定义导航的schema类型
var NavSchema=new GraphQLObjectType({
    name:'nav',
    fields:{
        _id:{
            type:GraphQLString
        },
        title:{
            type:GraphQLString
        },
        url:{
            type:GraphQLString
        },
        sort:{
            type:GraphQLInt
        },
        status:{
            type:GraphQLInt
        },
        add_time:{
            type:GraphQLString
        }
    }
})

var ArticleCateSchema=new GraphQLObjectType({
    name:'articlecate',
    fields:{
        _id:{
            type:GraphQLString
        },
        title:{
            type:GraphQLString
        },
        description:{
            type:GraphQLString
        },
        keywords:{
            type:GraphQLString
        } , 
        status:{
            type:GraphQLInt
        } 
    }
})

//2、定义一个跟       根里面定义调用导航Schema类型的方法
var RootSchema=new GraphQLObjectType({
    name:'root',
    fields:{
        oneNavList:{  //方法名称:定义调用导航Schema类型的方法
            type:NavSchema,  //方法的类型, 方法返回的参数必须和NavSchema里面定义的类型一致
            args:{id:{type:GraphQLString}}, //参数
            async resolve(parent,args){  //执行的操作

                // args.id 获取调用方法传入的值
                var id=args.id;

                var navList=await DB.find('nav',{"_id":DB.getObjectId(id)});
                return navList[0];               
            }
        },
        navList:{
            type:GraphQLList(NavSchema),  
            async resolve(parent,args){               
                var navList=await DB.find('nav',{});
                return navList;               
            }
        },
        articleCateList:{
            type:GraphQLList(ArticleCateSchema),
            async resolve(parent,args){               
                var articlecateList=await DB.find('articlecate',{});
                return articlecateList; 
            }
        },
        oneArticleCateList:{
            type:ArticleCateSchema, 
            args:{id:{type:GraphQLString}},
            async resolve(parent,args){             
                var id=args.id;
                var articlecateList=await DB.find('articlecate',{"_id":DB.getObjectId(id)});
                return articlecateList[0];   //要返回一个json对象
            }
        }
    }

})

//3、把根挂载到 GraphQLSchema
module.exports=new GraphQLSchema({
    query:RootSchema
})

编写数据库操作方法

/**

 * http://mongodb.github.io/node-mongodb-native

 * http://mongodb.github.io/node-mongodb-native/3.0/api/
 */

//DB库
var MongoDB=require('mongodb');
var MongoClient =MongoDB.MongoClient;
const ObjectID = MongoDB.ObjectID;

var Config= {
    dbUrl: 'mongodb://localhost:27017/',
    dbName: 'graphql' // 数据库名
}

class Db {
    static getInstance(){   /*1、单例  多次实例化实例不共享的问题*/
        if(!Db.instance){
            Db.instance=new Db();
        }
        return  Db.instance;
    }

    constructor(){
        this.dbClient=''; /*属性 放db对象*/
        this.connect();   /*实例化的时候就连接数据库*/

    }
    connect(){  /*连接数据库*/
      let _that=this;
      return new Promise((resolve,reject)=>{
          if(!_that.dbClient){         /*1、解决数据库多次连接的问题*/
              MongoClient.connect(Config.dbUrl,{ useNewUrlParser: true },(err,client)=>{
                  if(err){
                    reject(err)
                  }else{
                    _that.dbClient=client.db(Config.dbName);
                    resolve(_that.dbClient)
                  }
              })
          }else{
            resolve(_that.dbClient);
          }
      })
    }
    /*
     DB.find('user',{})  返回所有数据

     DB.find('user',{},{"title":1})    返回所有数据  只返回一列

     DB.find('user',{},{"title":1},{   返回第二页的数据
        page:2,
        pageSize:20,
        sort:{"add_time":-1}
     })
     js中实参和形参可以不一样      arguments 对象接收实参传过来的数据
    * */
    find(collectionName,json1,json2,json3){
        if(arguments.length==2){
            var attr={};
            var slipNum=0;
            var pageSize=0;
        }else if(arguments.length==3){
            var attr=json2;
            var slipNum=0;
            var pageSize=0;
        }else if(arguments.length==4){
            var attr=json2;
            var page=parseInt(json3.page) ||1;
            var pageSize=parseInt(json3.pageSize)||20;
            var slipNum=(page-1)*pageSize;
            if(json3.sort){
                var sortJson=json3.sort;
            }else{
                var sortJson={}
            }
        }else{
            console.log('传入参数错误')
        }
       return new Promise((resolve,reject)=>{
            this.connect().then((db)=>{
                //var result=db.collection(collectionName).find(json);
                var result =db.collection(collectionName).find(json1,{fields:attr}).skip(slipNum).limit(pageSize).sort(sortJson);
                result.toArray(function(err,docs){
                    if(err){
                        reject(err);
                        return;
                    }
                    resolve(docs);
                })

            })
        })
    }
    update(collectionName,json1,json2){
        return new Promise((resolve,reject)=>{
            this.connect().then((db)=>{
                //db.user.update({},{$set:{}})
                db.collection(collectionName).updateOne(json1,{
                    $set:json2
                },(err,result)=>{
                    if(err){
                        reject(err);
                    }else{
                        resolve(result);
                    }
                })
            })
        })
    }
    insert(collectionName,json){
        return new  Promise((resolve,reject)=>{
            this.connect().then((db)=>{
                db.collection(collectionName).insertOne(json,function(err,result){
                    if(err){
                        reject(err);
                    }else{
                        resolve(result);
                    }
                })
            })
        })
    }
    remove(collectionName,json){
        return new  Promise((resolve,reject)=>{
            this.connect().then((db)=>{
                db.collection(collectionName).removeOne(json,function(err,result){
                    if(err){
                        reject(err);
                    }else{
                        resolve(result);
                    }
                })
            })
        })
    }
    getObjectId(id){  /*mongodb里面查询 _id 把字符串转换成对象*/
        return new ObjectID(id);
    }
    //统计数量的方法
    count(collectionName,json){
        return new  Promise((resolve,reject)=> {
            this.connect().then((db)=> {
                var result = db.collection(collectionName).count(json);
                result.then(function (count) {
                        resolve(count);
                    }
                )
            })
        })
    }
}

module.exports=Db.getInstance();

打开本地调试

http://localhost:3000/graphql

四、Koa中集成GraphQl实现 Server API

下载数据库文件解压并导入mongodb即可 https://blog.poetries.top/db/koa.zip

  • 导入mongodb数据库 mongorestore -h localhost:27017 -d koa-demo(数据库名称,不存在会自动创建) ./dump(本地数据文件路径)
  • 导出mongodb数据库 mongodump -h localhost:27017 -d test(数据库名称) -o ./dump

文档地址 https://github.com/chentsulin/koa-graphql

npm install graphql koa-graphql koa-mount --save

实现导航列表API、文章分类API、文章列表API、文章详情API 、文章列表分页查询API、以及文章列表关联文章分类实现聚合API

4.1 app完善配置

// app.js 

var Koa=require('koa');

var router = require('koa-router')(); 

const mount = require('koa-mount');

const graphqlHTTP = require('koa-graphql');

var GraphQLDefaultSchema=require('./schema/default.js')


const DB=require('./model/db.js');

var app=new Koa();


//配置中间件
app.use(mount('/graphql', graphqlHTTP({
    schema: GraphQLDefaultSchema,
    graphiql: true
})));


router.get('/',async (ctx)=>{
    ctx.body="首页";
})

router.get('/getNavList',async (ctx)=>{
    
    var navList=await DB.find('nav',{});     
     ctx.body=navList;
})  

app.use(router.routes());   /*启动路由*/
app.use(router.allowedMethods());
app.listen(4000, ()=>console.log('http://localhost:4000'));

4.2 定义schema模型

// schema/default.js
const DB=require('../model/db.js');

//文章分类api接口     //文章列表api接口 (分页)     //文章详情api接口(api聚合 获取分类信息)

const {
    GraphQLObjectType,
    GraphQLString,
    GraphQLInt,
    GraphQLFloat,
    GraphQLList,
    GraphQLSchema,
    GraphQLID
}=require('graphql')

//1、定义导航的schema
var NavSchema=new GraphQLObjectType({
    name:'nav',
    fields:{
        _id:{
            type:GraphQLString
        },
        title:{
            type:GraphQLString
        },url:{

            type:GraphQLString
        },
        sort:{
            type:GraphQLInt

        },
        status:{
            type:GraphQLString
        },
        add_time:{
            type:GraphQLString
        }
    }
})


//定义文章分类的schema
var ArticleCateSchema=new GraphQLObjectType({
    name:'articlecate',
    fields:{
        _id:{type:GraphQLString},
        title:{type:GraphQLString},
        description:{ type: GraphQLString },
        keywords:{ type: GraphQLInt },
        pid:{type:GraphQLInt},
        add_time:{ type: GraphQLString },
        status:{ type: GraphQLInt }      
    }
})



//定义文章的schema
var ArticleSchema=new GraphQLObjectType({
    name:'article',
    fields:{
        _id:{type:GraphQLID},
        pid:{type:GraphQLID},    
        title:{ type: GraphQLString },
        author:{ type: GraphQLString },
        status:{type:GraphQLInt},
        is_best:{ type: GraphQLInt },
        is_hot:{ type: GraphQLInt },
        is_new:{ type: GraphQLInt },
        keywords:{ type: GraphQLString },
        description:{ type: GraphQLString },
        content:{ type: GraphQLString },
        sort:{ type: GraphQLInt },
        // 聚合查询文章分类信息
        cateInfo:{
            type:ArticleCateSchema,
            async resolve(parent,args){
                // parent.pid 当前新闻的分类id
                console.log(parent);

                var cateResult=await DB.find('articlecate',{"_id":DB.getObjectId(parent.pid)});

                return cateResult[0];

            }

        }
    }
})

//订单商品的Schema  (order_item)
var OrderItem=new GraphQLObjectType({
    name:'orderitem',
    fields:{
        uid:{ type: GraphQLID },
        order_id:  { type: GraphQLID },
        product_title: { type: GraphQLString },
        product_id: { type: GraphQLID },    
        product_img: { type: GraphQLString },    
        product_price: { type: GraphQLFloat },  
        product_num: { type: GraphQLInt },        
        add_time: {
          type: GraphQLString        
        }      
    }
})

//订单的Schema
var OrderSchema=new GraphQLObjectType({
    name:'order',
    fields:{
        _id:{type:GraphQLID},
        uid: { type:GraphQLID},
        all_price: { type: GraphQLInt },
        order_id: { type: GraphQLInt },
        name: { type: GraphQLString },  
        phone: { type: GraphQLString },    
        address:  { type: GraphQLString },    
        zipcode:  { type: GraphQLString },    
        pay_status:{ type: GraphQLInt},   // 支付状态: 0 表示未支付     1 已经支付
        pay_type:{type: GraphQLString},      // 支付类型: alipay    wechat  
        order_status: {               // 订单状态: 0 已下单  1 已付款  2 已配货  3、发货   4、交易成功   5、退货     6、取消      
          type: GraphQLInt      
        },
        add_time: {
          type: GraphQLString          
        },
        // 聚合查询订单关联的商品列表信息
        orderItems:{
            type:GraphQLList(OrderItem),
            async resolve(parent,args){
                //获取当前订单对应的商品 parent._id就是objectId
                var orderItemList=await DB.find('order_item',{"order_id":parent._id});
                return orderItemList;
            }

        }
    }
})



//2、定义一个根 配置调用Schema的方法
var RootSchema=new GraphQLObjectType({
    name:'root',
    fields:{
        navList:{
            type:GraphQLList(NavSchema),
            async resolve(parent,args){
                var navList=await DB.find('nav',{});     
                return navList;
            }            
        },
        oneNavList:{
            type:NavSchema,
            args:{
                _id:{
                    type:GraphQLString
                },
                status:{
                    type:GraphQLString
                }
            },
            async resolve(parent,args){

                var oneNavList=await DB.find('nav',{"_id":DB.getObjectId(args._id),"status":args.status});     
                return oneNavList[0];

            }
        },
        articleCateList:{
            type:GraphQLList(ArticleCateSchema),
            async resolve(parent,args){

                var articlecateList=await DB.find('articlecate',{});     
                return articlecateList;
            }
        },
        articleList:{
            type:GraphQLList(ArticleSchema),
            args:{
                page:{
                    type:GraphQLInt
                },
                pageSize:{
                    type:GraphQLInt
                }
            },
            // 分页查询文章列表
            async resolve(parent,args){
                var page=args.page||1;
                var pageSize=args.pageSize||5;
                console.log(page,pageSize);
                var articleList=await DB.find('article',{},{},{
                    page,
                    pageSize:pageSize,
                    sort:{"add_time":-1}
                 });     

                return articleList;
            }
        },
        // 订单列表
        orderList:{
            type:GraphQLList(OrderSchema),
            args:{
                page:{
                    type:GraphQLInt
                }
            },
            async resolve(parent,args){
                var page=args.page || 1;
                var orderList=await DB.find('order',{},{},{
                    page,
                    pageSize:3                    
                 });     
                return orderList;
            }
        },
        // 单个订单信息
        oneOrderList:{
            type:OrderSchema,
            args:{
                _id:{
                    type:GraphQLID
                }
            },
            async resolve(parent,args){               
                var orderList=await DB.find('order',{"_id":DB.getObjectId(args._id)});     
                return orderList[0];
            }
        }
    }
})

//3、把查询的根 挂载到GraphQLSchema
module.exports=new GraphQLSchema({
    query:RootSchema
})

4.3 编写数据库操作方法

// model/db.js
/**

 * http://mongodb.github.io/node-mongodb-native

 * http://mongodb.github.io/node-mongodb-native/3.0/api/
 */

//DB库
var MongoDB=require('mongodb');
var MongoClient =MongoDB.MongoClient;
const ObjectID = MongoDB.ObjectID;

var Config= {
    url: 'mongodb://localhost:27017',
    dbName: 'koa-demo'
}

class Db{
    static getInstance(){   /*1、单例  多次实例化实例不共享的问题*/
        if(!Db.instance){
            Db.instance=new Db();
        }
        return  Db.instance;
    }

    constructor(){
        this.dbClient=''; /*属性 放db对象*/
        this.connect();   /*实例化的时候就连接数据库*/
    }

    connect(){  /*连接数据库*/
      let _that=this;
      return new Promise((resolve,reject)=>{
          if(!_that.dbClient){         /*1、解决数据库多次连接的问题*/
              MongoClient.connect(Config.dbUrl,{ useNewUrlParser: true },(err,client)=>{

                  if(err){
                      reject(err)
                  }else{
                      _that.dbClient=client.db(Config.dbName);
                      resolve(_that.dbClient)
                  }
              })
          }else{
            resolve(_that.dbClient);
          }
      })

    }
    /*

     DB.find('user',{})  返回所有数据


     DB.find('user',{},{"title":1})    返回所有数据  只返回一列


     DB.find('user',{},{"title":1},{   返回第二页的数据
        page:2,
        pageSize:20,
        sort:{"add_time":-1}
     })
     js中实参和形参可以不一样      arguments 对象接收实参传过来的数据
    * */

    find(collectionName,json1,json2,json3){
        if(arguments.length==2){
            var attr={};
            var slipNum=0;
            var pageSize=0;
        }else if(arguments.length==3){
            var attr=json2;
            var slipNum=0;
            var pageSize=0;
        }else if(arguments.length==4){
            var attr=json2;
            var page=parseInt(json3.page) ||1;
            var pageSize=parseInt(json3.pageSize)||20;
            var slipNum=(page-1)*pageSize;

            if(json3.sort){
                var sortJson=json3.sort;
            }else{
                var sortJson={}
            }
        }else{
            console.log('传入参数错误')
        }
       return new Promise((resolve,reject)=>{
            this.connect().then((db)=>{
                //var result=db.collection(collectionName).find(json);
                var result =db.collection(collectionName).find(json1,{fields:attr}).skip(slipNum).limit(pageSize).sort(sortJson);
                result.toArray(function(err,docs){
                    if(err){
                        reject(err);
                        return;
                    }
                    resolve(docs);
                })

            })
        })
    }
    update(collectionName,json1,json2){
        return new Promise((resolve,reject)=>{
                this.connect().then((db)=>{

                    //db.user.update({},{$set:{}})
                    db.collection(collectionName).updateOne(json1,{
                        $set:json2
                    },(err,result)=>{
                        if(err){
                            reject(err);
                        }else{
                            resolve(result);
                        }
                    })

                })

        })

    }
    insert(collectionName,json){
        return new  Promise((resolve,reject)=>{
            this.connect().then((db)=>{

                db.collection(collectionName).insertOne(json,function(err,result){
                    if(err){
                        reject(err);
                    }else{

                        resolve(result);
                    }
                })


            })
        })
    }

    remove(collectionName,json){

        return new  Promise((resolve,reject)=>{
            this.connect().then((db)=>{

                db.collection(collectionName).removeOne(json,function(err,result){
                    if(err){
                        reject(err);
                    }else{

                        resolve(result);
                    }
                })


            })
        })
    }
    getObjectId(id){    /*mongodb里面查询 _id 把字符串转换成对象*/

        return new ObjectID(id);
    }
    //统计数量的方法
    count(collectionName,json){

        return new  Promise((resolve,reject)=> {
            this.connect().then((db)=> {

                var result = db.collection(collectionName).count(json);
                result.then(function (count) {

                        resolve(count);
                    }
                )
            })
        })

    }
}


module.exports=Db.getInstance();

启动服务

4.4 聚合查询

聚合查询文章分类信息,分类信息的方式要放在article的schema里面,这样才能聚合查询到

聚合查询结果

查询订单,聚合查询订单关联的商品信息返回,实现类似以下效果

// schema/default.js 
//订单商品的Schema  (order_item)
var OrderItem=new GraphQLObjectType({
    name:'orderitem',
    fields:{
        uid:{ type: GraphQLID },
        order_id:  { type: GraphQLID },
        product_title: { type: GraphQLString },
        product_id: { type: GraphQLID },    
        product_img: { type: GraphQLString },    
        product_price: { type: GraphQLFloat },  
        product_num: { type: GraphQLInt },        
        add_time: {
          type: GraphQLString        
        }      
    }
})

//订单的Schema
var OrderSchema=new GraphQLObjectType({
    name:'order',
    fields:{
        _id:{type:GraphQLID},
        uid: { type:GraphQLID},
        all_price: { type: GraphQLInt },
        order_id: { type: GraphQLInt },
        name: { type: GraphQLString },  
        phone: { type: GraphQLString },    
        address:  { type: GraphQLString },    
        zipcode:  { type: GraphQLString },    
        pay_status:{ type: GraphQLInt},   // 支付状态: 0 表示未支付     1 已经支付
        pay_type:{type: GraphQLString},      // 支付类型: alipay    wechat  
        order_status: {               // 订单状态: 0 已下单  1 已付款  2 已配货  3、发货   4、交易成功   5、退货     6、取消      
          type: GraphQLInt      
        },
        add_time: {
          type: GraphQLString          
        },
        // 聚合查询订单关联的商品列表信息
        orderItems:{
            type:GraphQLList(OrderItem),
            async resolve(parent,args){
                //获取当前订单对应的商品 parent._id就是objectId
                var orderItemList=await DB.find('order_item',{"order_id":parent._id});
                return orderItemList;
            }

        }
    }
})


// 定义一个根 配置调用Schema的方法
var RootSchema=new GraphQLObjectType({
    name:'root',
    fields:{
        orderList:{
            type:GraphQLList(OrderSchema),
            args:{
                page:{
                    type:GraphQLInt
                }
            },
            async resolve(parent,args){
                var page=args.page || 1;
                var orderList=await DB.find('order',{},{},{
                    page,
                    pageSize:3                    
                 });     
                return orderList;
            }
        },
        oneOrderList:{
            type:OrderSchema,
            args:{
                _id:{
                    type:GraphQLID
                }
            },
            async resolve(parent,args){               
                var orderList=await DB.find('order',{"_id":DB.getObjectId(args._id)});     
                return orderList[0];
            }
        }
    }
})

查询订单详情

需要哪些字段,就返回哪些字段,编辑器会自定提示

4.5 分页查询

//定义文章分类的schema
var ArticleCateSchema=new GraphQLObjectType({
    name:'articlecate',
    fields:{
        _id:{type:GraphQLString},
        title:{type:GraphQLString},
        description:{ type: GraphQLString },
        keywords:{ type: GraphQLInt },
        pid:{type:GraphQLInt},
        add_time:{ type: GraphQLString },
        status:{ type: GraphQLInt }      
    }
})



//定义文章的schema
var ArticleSchema=new GraphQLObjectType({
    name:'article',
    fields:{
        _id:{type:GraphQLID},
        pid:{type:GraphQLID},    
        title:{ type: GraphQLString },
        author:{ type: GraphQLString },
        status:{type:GraphQLInt},
        is_best:{ type: GraphQLInt },
        is_hot:{ type: GraphQLInt },
        is_new:{ type: GraphQLInt },
        keywords:{ type: GraphQLString },
        description:{ type: GraphQLString },
        content:{ type: GraphQLString },
        sort:{ type: GraphQLInt },
        // 聚合查询文章分类信息
        cateInfo:{
            type:ArticleCateSchema,
            async resolve(parent,args){
                // parent.pid 当前新闻的分类id
                console.log(parent);

                var cateResult=await DB.find('articlecate',{"_id":DB.getObjectId(parent.pid)});

                return cateResult[0];

            }

        }
    }
})


//2、定义一个根 配置调用Schema的方法
var RootSchema=new GraphQLObjectType({
    name:'root',
    fields:{
        articleCateList:{
            type:GraphQLList(ArticleCateSchema),
            async resolve(parent,args){

                var articlecateList=await DB.find('articlecate',{});     
                return articlecateList;
            }
        },
        articleList:{
            type:GraphQLList(ArticleSchema),
            args:{
                page:{
                    type:GraphQLInt
                },
                pageSize:{
                    type:GraphQLInt
                }
            },
            // 分页查询文章列表
            async resolve(parent,args){
                var page=args.page||1;
                var pageSize=args.pageSize||5;
                console.log(page,pageSize);
                var articleList=await DB.find('article',{},{},{
                    page,
                    pageSize:pageSize,
                    sort:{"add_time":-1}
                 });     

                return articleList;
            }
        },
    }
})

4.6 实现数据增加、修改、删除

// scehma/default.js 
//增加 修改 删除
// 定义根MutationRoot实现增删改
var MutationSchema=new GraphQLObjectType({
    name:"mutation",
    fields:{
        addNav:{
            type:NavSchema,
            args:{
                title: {type: new GraphQLNonNull(GraphQLString)},     //表示title 和 url是必传字段
                url: {type: GraphQLNonNull(GraphQLString)},
                sort: {type: GraphQLInt},
                status: {type: GraphQLString},
                add_time: {type: GraphQLString}
            },
            async resolve(parent, args) {
                var result = await DB.insert('nav', {title:args.title,
                    url:args.url,
                    sort:args.sort,
                    status:args.status,
                    add_time:new Date().getTime()
                });

                console.log(result.ops[0]);

                return result.ops[0];
            }
        },
        editNav:{
            type:NavSchema,
            args:{
                _id:{type: new GraphQLNonNull(GraphQLString)},
                title: {type: new GraphQLNonNull(GraphQLString)},     //表示title 和 url是必传字段
                url: {type: GraphQLNonNull(GraphQLString)},
                sort: {type: GraphQLInt},
                status: {type: GraphQLString},
                add_time: {type: GraphQLString}
            },
            async resolve(parent, args) {
                var result = await DB.update('nav', {"_id":DB.getObjectId(args._id)},{title:args.title,
                    url:args.url,
                    sort:args.sort,
                    status:args.status,
                    add_time:new Date().getTime()
                });

                // console.log(result);
                return {
                    _id:args._id,
                    title:args.title,
                    url:args.url,
                    sort:args.sort,
                    status:args.status,
                    add_time:new Date().getTime()
                }
            }

        }    
        ,
        deleteNav:{
            type:NavSchema,
            args:{
                _id:{type: new GraphQLNonNull(GraphQLString)},
            },
            async resolve(parent, args) {

                var oneNavList = await DB.find('nav', { "_id": DB.getObjectId(args._id)});
              
                var deleteResult = await DB.remove('nav', {"_id":DB.getObjectId(args._id)});

                console.log(deleteResult.result.n);

                if(deleteResult.result.n){
                    return oneNavList[0];  
                }else{
                    return {}
                }

            }

        }  
    }
})

// 挂载到GraphQLSchema
module.exports=new GraphQLSchema({
    // query:RootSchema,
    mutation:MutationSchema
})
  • 新增

可以看到必填字段不填会提示

再次查询列表

  • 修改
  • 删除

五、Vue中使用GraphQl

5.1 使用graphQl简单查询

安装

  1. 找到Vue中集成GraphQl的文档
  1. 安装相应的模块

ApolloBoost是一种零配置开始使用ApolloClient的方式。它包含一些实用的默认值,例如我们推荐的InMemoryCache和HttpLink,它非常适合用于快速启动开发。将它与vue-apollo和graphql一起安装:

npm install vue-apollo graphql apollo-boost --save
  1. src/main.js中引入apollo-boost模块并实例化ApolloClient
import ApolloClient from'apollo-boost'

const apolloClient = newApolloClient({
    //你需要在这里使用绝对路径
    uri:'http://118.123.14.36:3002/graphql'
})

可以打开 http://118.123.14.36:3002/graphql 在控制台查看查询结果

  1. src/main.js配置vue-apollo插件
import VueApollofrom'vue-apollo'

Vue.use(VueApollo);
  1. 创建Apollo provider

Provider保存了可以在接下来被所有子组件使用的Apollo客户端实例

const apolloProvider = newVueApollo({
    defaultClient:apolloClient
})

使用apollo Provider选项将它添加到你的应用程序

new Vue({
    el:'#app',
    apolloProvider,
    render:h=>h(App)
})

简单查询

组件加载的时候就会去服务器请求数据,请求的数据会放在navList这个属性上面,在模板中可以直接使用当前属性

简单查询文档

带参数查询参考

import gql from'graphql-tag';

export default{ 
    data(){
        return { msg: '我是一个 home 组件' } 
    },
    apollo: {
        // 简单的查询,将更新 'hello' 这个 vue 属性 
        navList: gql`query { 
            navList { 
                title
            } 
        }` 
    },
}

另一种写法:

import gql from 'graphql-tag'; 
export default{ 
    data(){
        return { 
            msg:'我是一个 home 组件' 
        } 
    },
    // Apollo 具体选项
    apollo: {
        // // 带参数的查询
        // ping: {
        //     // gql 查询
        //     query: gql`query PingMessage($message: String!) {
        //     ping(message: $message)
        //     }`,
        //     // 静态参数
        //     variables: {
        //     message: 'Meow',
        //     },
        // },
    },
    apollo: { 
        // 注意方法名称 和 查询的名称对应 
        navList(){ 
            return { 
                query:gql`query { 
                    navList { 
                        title
                     } 
                }` 
            } 
        } 
    } 
}

完整例子

<template>
  <div class="news">
    <h1>{{ msg }}</h1> 
    <ul>
      <li v-for="(item,index) of navList" :key="index">
          {{item.title}}
      </li>      
    </ul>
    <br>
    <hr>
    <br>
    <ul>
      <li v-for="(item, index) of articleList" :key="index">
          {{item.title}}---{{item.status}}--{{item._id}}
      </li>      
    </ul>
  </div>
</template>

<script>
  import gql from 'graphql-tag';
  export default {
    name: 'app',
    data(){
      return{

        msg:'我是一个首页页面'

      }
    },
    apollo: {
      // 简单的查询,将更新 'hello' 这个 vue 属性
      navList: gql`{
         navList{
            title
          }
      }`,
      articleList:gql`{
         articleList{
            title,
            status,
            _id
          }
      }`
    }
 
  }
</script>

高级查询

高级查询文档

  <div class="news">
    <h1>{{ msg }}</h1>    


    <ul>

      <li v-for="(item,key) of articleList" v-bind:key="key">
          {{item.title}}---{{item.status}}
      </li>      
    </ul>

    <button @click="getData()">
      点击按钮触发事件请求graphQl接口
    </button>

    {{navList}}

  </div>

逻辑

import gql from 'graphql-tag';

  var navListGql=gql`{
        navList{
            title           
        }
   }`;

  export default {
    name: 'app',
    data(){
      return{
        msg:'我是一个新闻页面',
        navList:[]

      }
    },
    apollo:{
      // articleList:gql`{
      //        articleList{
      //         title,
      //         status
      //       }

      // }`
        // 把请求的数据赋值给articleList
        articleList:{
          query:gql`query articleList($page:Int!,$pageSize:Int!){
                articleList(page:$page,pageSize:$pageSize){
                  title,
                  status
                }
          }`,
          variables:{
            page:2,
            pageSize:10
          }
        }
    },
    methods:{
      getData(){
        this.$apollo.addSmartQuery('navList',{          
            query:navListGql,
            result(response){
              console.log(response);
            },error(err){
              console.log(err);
            }
        })
      }
    }
  }

Vue GraphQl 传参查询

<template>
  <div class="article">
    <h1>{{ msg }}</h1>   
    <button @click="getData()">获取文章数据</button>
   <ul>
      <li v-for="(item,key) of articleList" v-bind:key="key">
          {{item.title}}
      </li>      
    </ul>

  </div>
</template>

<script>
  import gql from 'graphql-tag';

  var articleListGql=gql`query articleList($page:Int!,$pageSize:Int!){
       articleList(page:$page,pageSize:$pageSize){
        title       
      }
  }`;
  export default {
    name: 'app',
    data(){
      return{
        msg:'article页面',
        articleList:[]

      }
    },
    methods:{
      getData(){
        this.$apollo.addSmartQuery('articleList',{
          query:articleListGql,
          variables:{
            page:2,
            pageSize:8
          },
          result(response){
            console.log(response)
          },error(err){
            console.log(err)
          }
        })
      }
    }
  }
</script>

5.2 使用graphQl增加修改删除

详情文档参考

服务器端接口

<template>
  <div class="news">
    <h1>导航的增加修改删除</h1> 
    <div class="navForm">

        导航名称:<input v-model="navJson.title" type="text" /> <br><br>
        导航链接: <input v-model="navJson.url" type="text" /><br><br>

        <button @click="doAdd()">提交数据</button>
        <button @click="doEdit()">修改数据</button>
        <button @click="doDele()">删除数据</button>
    </div>


  </div>
</template>

<script>
  import gql from 'graphql-tag';

  var navMutationAddGql=gql`mutation($title:String!,$url:String!){
    addNav(title:$title,url:$url){
      title
    }
  }`;

  var navMutationEditGql=gql`mutation($id:String!,$title:String!,$url:String!){
    editNav(_id:$id,title:$title,url:$url){
      title
    }
  }`;

  var navMutationDelGql=gql`mutation($id:String!){
    deleteNav(_id:$id){
      title
    }
  }`;

  export default {
    name: 'app',
    data(){
      return{
        navJson:{
          title:"",
          url:""
        }
      }
    },
    methods:{
      // 提交表单
      doAdd(){
          // eslint-disable-next-line no-console
          console.log(this.navJson.title);

        this.$apollo.mutate({
            mutation:navMutationAddGql,
            variables: {
            title: this.navJson.title,
            url:this.navJson.url,
            }
        }).then((response)=>{
            console.log(response);
        }).catch((err)=>{
            console.log(err);
        })
      },
      // 修改数据
      doEdit(){
        this.$apollo.mutate({
          mutation:navMutationEditGql,
          variables: {
            id:"62beaf16323cb708d06580ce",
            title: this.navJson.title,
            url:this.navJson.url,
          }
        }).then((response)=>{
          console.log(response);
        }).catch((err)=>{
          console.log(err);
        })
      },
      doDele(){
        this.$apollo.mutate({
          mutation:navMutationDelGql,
          variables: {
            id:"62beaf50323cb708d06580d0",
          }
        }).then((response)=>{
          console.log(response);
        }).catch((err)=>{
          console.log(err);
        })
      }
    }
 
  }
</script>

可以看到新增成功效果

5.3 上拉分页加载更多

npm i vue-infinite-scroll -S
// main.js配置 

//配置上拉分页加载更多
var infiniteScroll =  require('vue-infinite-scroll');
Vue.use(infiniteScroll);

方法1:数据拼接

<template>
  <div class="article">
    <h1>{{ msg }}</h1>

    <div v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10">
      <ul>
        <li v-for="(item,key) of articleListData" v-bind:key="key">{{item.title}}</li>
      </ul>
    </div>
  </div>
</template>

<script>
import gql from "graphql-tag";

var articleListGql = gql`
  query articleList($page: Int!, $pageSize: Int!) {
    articleList(page: $page, pageSize: $pageSize) {
      title
    }
  }
`;

export default {
  name: "app",
  data() {
    return {
      msg: "上拉分页加载更多",
      articleList: [],
      articleListData: [] /*实际要循环的数据*/,

      page: 1,
      busy: false
    };
  },
  methods: {
    loadMore() {
      this.$apollo.addSmartQuery("articleList", {
        query: articleListGql,

        variables: {
          page: this.page,
          pageSize: 8
        },

        result(response) {
          console.log(response);

          this.articleListData = this.articleListData.concat(
            response.data.articleList
          );

          this.page++;

          if (response.data.articleList < 8) {
            this.busy = true; //没有数据禁用上拉分页加载更多
          }
        },
        error(err) {
          console.log(err);
        }
      });
    }
  }
};
</script>

<style scoped>
  li {
    line-height: 4;
  }
</style>

方法2:使用 fetchMore 实现分页(推荐)

https://vue-apollo.netlify.app/zh-cn/guide/apollo/pagination.html

<template>
  <div class="article">
    <h1>{{ msg }}</h1>

    <div v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10">
      <ul>
        <li v-for="(item,key) of articleList" v-bind:key="key">{{item.title}}</li>
      </ul>
    </div>
  </div>
</template>

<script>
import gql from "graphql-tag";

var articleListGql = gql`
  query articleList($page: Int!, $pageSize: Int!) {
    articleList(page: $page, pageSize: $pageSize) {
      title
    }
  }
`;

export default {
  name: "app",
  data() {
    return {
      msg: "上拉分页加载更多",
      articleList: [],
      page: 1,
      busy: false
    };
  },
  apollo: {
    articleList() {
      return {
        // GraphQL 查询
        query: articleListGql,
        // 初始变量
        variables: {
          page: this.page,
          pageSize: 5
        }
      };
    }
  },
  methods: {
    loadMore() {
      this.page++;

      this.$apollo.queries.articleList.fetchMore({
        // 新的变量
        variables: {
          page: this.page,
          pageSize: 5
        },
        // 用新数据转换之前的结果
        updateQuery: (previousResult, { fetchMoreResult }) => {
          // eslint-disable-next-line no-console
          console.log(fetchMoreResult);
          return {
            articleList: [
              ...previousResult.articleList,
              ...fetchMoreResult.articleList
            ]
          };
        }
      });
    }
  }
};
</script>

<style scoped>
li {
  line-height: 4;
}
</style>

分页效果

项目例子完整代码下载地址 https://blog.poetries.top/assets/graphql-code.zip

六、文档

本文由mdnice多平台发布

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容