- 前端進(jìn)階之旅:https://interview2.poetries.top
- 公眾號:「前端進(jìn)階之旅」 每天分享技術(shù)干貨
一、GraphQL介紹
1.1 簡介
GraphQL 是一種新的 API 的查詢語言,它提供了一種更高效、強(qiáng)大和靈活 API 查詢。它 是由 Facebook 開發(fā)和開源,目前由來自世界各地的大公司和個人維護(hù)。GraphQL 對你的 API 中的數(shù)據(jù)提供了一套易于理解的完整描述,使得客戶端能夠準(zhǔn)確地獲得它需要的數(shù)據(jù),而且 沒有任何冗余。它彌補(bǔ)了 RESTful API(字段冗余,擴(kuò)展性差、無法聚合 api、無法定義數(shù)據(jù) 類型、網(wǎng)絡(luò)請求次數(shù)多)等不足
注意:GraphQL 是 api 的查詢語言,而不是數(shù)據(jù)庫。從這個意義上說,它是數(shù)據(jù)庫無關(guān)的, 而且可以在使用 API 的任何環(huán)境中有效使用,我們可以理解為 GraphQL 是基于 API 之上的一 層封裝,目的是為了更好,更靈活的適用于業(yè)務(wù)的需求變化
GraphQL 可以用在常見各種服務(wù)器端語言以及客戶端語言中
服務(wù)器端語言: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
GraphQL 出現(xiàn)的歷史背景
當(dāng)提起API設(shè)計的時候,大家通常會想到SOAP(一種簡單的基于 XML 的協(xié)議),RESTful 等設(shè)計方式,從 2000 年 RESTful 的理論被提出的時候,在業(yè)界引起了很大反響,因?yàn)檫@種 設(shè)計理念更易于用戶的使用,所以便很快的被大家所接受。
我們知道 REST 是一種從服務(wù) 器公開數(shù)據(jù)的流行方式。當(dāng) REST 的概念被提及出來時,客戶端應(yīng)用程序?qū)?shù)據(jù)的需求相 對簡單,而開發(fā)的速度并沒有達(dá)到今天的水平。
因此 REST 對于許多應(yīng)用程序來說是非常 適合的。然而在業(yè)務(wù)越發(fā)復(fù)雜,客戶對系統(tǒng)的擴(kuò)展性有了更高的要求時,API 環(huán)境發(fā)生了巨 大的變,RESTful 顯得心有余而力不足。比如:字段冗余,擴(kuò)展性差、無法聚合 api、無法 定義數(shù)據(jù)類型、網(wǎng)絡(luò)請求次數(shù)多
GraphQL 的出現(xiàn)整好彌補(bǔ)了 RESTful APi 的不足
使用 GraphQL 的公司
目前已經(jīng)有很多的公司在使用 GraphQL(https://graphql.org/users/)

1.2 為什么推薦 GraphQL 而不是 RESTful API
在過去的十多年中,REST 已經(jīng)成為設(shè)計 web api 的標(biāo)準(zhǔn)(雖然只是一個模糊的標(biāo)準(zhǔn))。
它提供了一些很棒的想法,比如無狀態(tài)服務(wù)器和結(jié)構(gòu)化的資源訪問。
然而 REST api 表 現(xiàn)得過于僵化,無法跟上訪問它們的客戶的快速變化的需求
RESTful API 不足
- 擴(kuò)展性(多個終端需要返回不同的字段),單個 RESTful 接口返回數(shù)據(jù)越來越 臃腫。前端對于真正用到的字段是沒有直觀映像的,僅僅通過 url 地址,無法預(yù)測也無 法回憶返回的字段數(shù)目和字段是否有效,接口返回 50 個字段,但卻只用 5 個字段,造 成字段冗余,擴(kuò)展性差,單個 RESTful 接口返回數(shù)據(jù)越來越臃腫
- API 聚合問題,某個前端展現(xiàn),實(shí)際需要調(diào)用多個獨(dú)立的 RESTful API 才能獲 取到足夠的數(shù)據(jù),導(dǎo)致網(wǎng)絡(luò)請求次數(shù)多
- 前后端字段頻繁改動,導(dǎo)致類型不一致,錯誤的數(shù)據(jù)類型可能會導(dǎo)致網(wǎng)站出錯 尤其是在業(yè)務(wù)多變的場景中,很難在保證工程質(zhì)量的同時快速滿足業(yè)務(wù)需求
GraphQL 的優(yōu)點(diǎn)
- 吸收了 RESTful API 的特性
- 所見即所得 各種不同的前端框架和平臺可以指定自己需要的字段。查詢的返回結(jié)果就是輸 入的查詢結(jié)構(gòu)的精確映射
客戶端可以自定義 Api 聚合
如果設(shè)計的數(shù)據(jù)結(jié)構(gòu)是從屬的,直接就能在查詢語句中指定;即使數(shù)據(jù)結(jié)構(gòu)是獨(dú) 立的,也可以在查詢語句中指定上下文,只需要一次網(wǎng)絡(luò)請求,就能獲得資源和子 資源的數(shù)據(jù)。
代碼即是文檔
GraphQL 會把 schema 定義和相關(guān)的注釋生成可視化的文檔,從而使得代碼的變更,直接就反映到最新的文檔上,避免 RESTful 中手工維護(hù)可能會造成代碼、 文檔不一致的問題
參數(shù)類型強(qiáng)校驗(yàn)
- RESTful 方案本身沒有對參數(shù)的類型做規(guī)定,往往都需要自行實(shí)現(xiàn)參數(shù)的校驗(yàn)機(jī)制, 以確保安全。
- 但 GraphQL 提供了強(qiáng)類型的 schema 機(jī)制,從而天然確保了參數(shù)類型的合法性
二、GraphQl類型系統(tǒng)
2.1 GraphQl類型
可以將GraphQL的類型系統(tǒng)分為
標(biāo)量類型(ScalarTypes,標(biāo)量類型)和其他高級數(shù)據(jù)類型,標(biāo)量類型即可以表示最細(xì)粒度數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù)類型,可以和JavaScript的原始類型對應(yīng)
GraphQL規(guī)范目前規(guī)定支持的標(biāo)量類型有
-
Int:有符號
32位整數(shù) --GraphQLInt -
Float:有符號雙精度浮點(diǎn)值 --
GraphQLFloat -
String:
UTF‐8字符序列 --GraphQLString -
Boolean:
true或者false--GraphQLBoolean - ID(GraphQLID):ID標(biāo)量類型表示一個唯一標(biāo)識符,通常用以重新獲取對象或者作為緩存中的鍵。ID類型使用和String一樣的方式序列化;然而將其定義為ID意味著并不需要可讀型。
GraphQL其他高級數(shù)據(jù)類型包括
- Object:對象(newGraphQLObjectType)
用于描述層級或者樹形數(shù)據(jù)結(jié)構(gòu)。對于樹形數(shù)據(jù)結(jié)構(gòu)來說,葉子字段的類型都是標(biāo)量數(shù)據(jù)類型。幾乎所有GraphQL類型都是對象類型。Object類型有一個name字段,以及一個很重要的fields字段。fields字段可以描述出一個完整的數(shù)據(jù)結(jié)構(gòu)。例如一個表示地址數(shù)據(jù)結(jié)構(gòu)的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:聯(lián)合類型用于描述某個字段能夠支持的所有返回類型以及具體請求真正的返回類型 -
Enum:枚舉用于表示可枚舉數(shù)據(jù)結(jié)構(gòu)的類型 -
InputObject:輸入對象 -
List:列表
列表是其他類型的封裝,通常用于對象字段的描述。例如下面PersonType類型數(shù)據(jù)的parents和children字段
const PersonType=newGraphQLObjectType({
name:'Person',
fields:()=>({
parents:{type:newGraphQLList(Person)},
children:{type:newGraphQLList(Person)},
})
})
-
Non-Null:不能為Null
Non-Null強(qiáng)制類型的值不能為null,并且在請求出錯時一定會報錯。可以用于必須保證值不能為null的字段。例如數(shù)據(jù)庫的行的id字段不能為null
const RowType=newGraphQLObjectType({
name:'Row',
fields:()=>({
id:{
type:newGraphQLNonNull(GraphQLString)
}
})
})
2.2 GraphQl查詢語言
GraphQL規(guī)范支持兩種操作
-
query:僅獲取數(shù)據(jù)(fetch)的只讀請求 -
mutation:獲取數(shù)據(jù)后還有寫操作的請求
新版本的GraphQL還支持
subscription,這是為了處理訂閱更新這種比較復(fù)雜的實(shí)時數(shù)據(jù)更新場景而設(shè)計的操作

三、Express中集成GraphQl 實(shí)現(xiàn) Server API
3.1 安裝mongodb造數(shù)據(jù)
使用mongodb做數(shù)據(jù)庫演示,mac安裝mongodb,brew install mongodb-community
# 進(jìn)入mongo shell
mongo
# 創(chuàng)建數(shù)據(jù)庫
use graphql (graphql數(shù)據(jù)庫不存在會自動創(chuàng)建)
# 創(chuàng)建nav、articlecate集合插入數(shù)據(jù)
db.nav.insert({name: "標(biāo)題1", url: "/", sort: 1, add_time: "2022-06-30"})
db.nav.insert({name: "標(biāo)題2", url: "/", sort: 1, add_time: "2022-06-30"})
db.nav.insert({name: "標(biāo)題3", url: "/", sort: 1, add_time: "2022-06-30"})
db.articlecate.insert({name: "分類1", description: "描述", keywords: "關(guān)鍵詞", status: 1})
db.articlecate.insert({name: "分類2", description: "描述", keywords: "關(guān)鍵詞", status: 1})
db.articlecate.insert({name: "分類3", description: "描述", keywords: "關(guān)鍵詞", status: 1})
或者導(dǎo)入數(shù)據(jù)庫數(shù)據(jù)
下載數(shù)據(jù)庫文件解壓并導(dǎo)入mongodb即可 https://blog.poetries.top/db/koa.zip
導(dǎo)入mongodb數(shù)據(jù)庫
mongorestore -h localhost:27017 -d koa-demo(數(shù)據(jù)庫名稱,不存在會自動創(chuàng)建) ./dump(本地數(shù)據(jù)文件路徑)
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 // 線上環(huán)境關(guān)閉,開發(fā)環(huán)境開啟
}));
//配置路由
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、獲取導(dǎo)航列表 定義導(dǎo)航的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、定義一個跟 根里面定義調(diào)用導(dǎo)航Schema類型的方法
var RootSchema=new GraphQLObjectType({
name:'root',
fields:{
oneNavList:{ //方法名稱:定義調(diào)用導(dǎo)航Schema類型的方法
type:NavSchema, //方法的類型, 方法返回的參數(shù)必須和NavSchema里面定義的類型一致
args:{id:{type:GraphQLString}}, //參數(shù)
async resolve(parent,args){ //執(zhí)行的操作
// args.id 獲取調(diào)用方法傳入的值
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
})
編寫數(shù)據(jù)庫操作方法
/**
* 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' // 數(shù)據(jù)庫名
}
class Db {
static getInstance(){ /*1、單例 多次實(shí)例化實(shí)例不共享的問題*/
if(!Db.instance){
Db.instance=new Db();
}
return Db.instance;
}
constructor(){
this.dbClient=''; /*屬性 放db對象*/
this.connect(); /*實(shí)例化的時候就連接數(shù)據(jù)庫*/
}
connect(){ /*連接數(shù)據(jù)庫*/
let _that=this;
return new Promise((resolve,reject)=>{
if(!_that.dbClient){ /*1、解決數(shù)據(jù)庫多次連接的問題*/
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',{}) 返回所有數(shù)據(jù)
DB.find('user',{},{"title":1}) 返回所有數(shù)據(jù) 只返回一列
DB.find('user',{},{"title":1},{ 返回第二頁的數(shù)據(jù)
page:2,
pageSize:20,
sort:{"add_time":-1}
})
js中實(shí)參和形參可以不一樣 arguments 對象接收實(shí)參傳過來的數(shù)據(jù)
* */
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('傳入?yún)?shù)錯誤')
}
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 把字符串轉(zhuǎn)換成對象*/
return new ObjectID(id);
}
//統(tǒng)計數(shù)量的方法
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();
打開本地調(diào)試

四、Koa中集成GraphQl實(shí)現(xiàn) Server API
下載數(shù)據(jù)庫文件解壓并導(dǎo)入mongodb即可 https://blog.poetries.top/db/koa.zip
-
導(dǎo)入mongodb數(shù)據(jù)庫
mongorestore -h localhost:27017 -d koa-demo(數(shù)據(jù)庫名稱,不存在會自動創(chuàng)建) ./dump(本地數(shù)據(jù)文件路徑) - 導(dǎo)出mongodb數(shù)據(jù)庫
mongodump -h localhost:27017 -d test(數(shù)據(jù)庫名稱) -o ./dump
npm install graphql koa-graphql koa-mount --save
實(shí)現(xiàn)導(dǎo)航列表API、文章分類API、文章列表API、文章詳情API 、文章列表分頁查詢API、以及文章列表關(guān)聯(lián)文章分類實(shí)現(xiàn)聚合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、定義導(dǎo)航的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 當(dāng)前新聞的分類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}, // 支付狀態(tài): 0 表示未支付 1 已經(jīng)支付
pay_type:{type: GraphQLString}, // 支付類型: alipay wechat
order_status: { // 訂單狀態(tài): 0 已下單 1 已付款 2 已配貨 3、發(fā)貨 4、交易成功 5、退貨 6、取消
type: GraphQLInt
},
add_time: {
type: GraphQLString
},
// 聚合查詢訂單關(guān)聯(lián)的商品列表信息
orderItems:{
type:GraphQLList(OrderItem),
async resolve(parent,args){
//獲取當(dāng)前訂單對應(yīng)的商品 parent._id就是objectId
var orderItemList=await DB.find('order_item',{"order_id":parent._id});
return orderItemList;
}
}
}
})
//2、定義一個根 配置調(diào)用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 編寫數(shù)據(jù)庫操作方法
// 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、單例 多次實(shí)例化實(shí)例不共享的問題*/
if(!Db.instance){
Db.instance=new Db();
}
return Db.instance;
}
constructor(){
this.dbClient=''; /*屬性 放db對象*/
this.connect(); /*實(shí)例化的時候就連接數(shù)據(jù)庫*/
}
connect(){ /*連接數(shù)據(jù)庫*/
let _that=this;
return new Promise((resolve,reject)=>{
if(!_that.dbClient){ /*1、解決數(shù)據(jù)庫多次連接的問題*/
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',{}) 返回所有數(shù)據(jù)
DB.find('user',{},{"title":1}) 返回所有數(shù)據(jù) 只返回一列
DB.find('user',{},{"title":1},{ 返回第二頁的數(shù)據(jù)
page:2,
pageSize:20,
sort:{"add_time":-1}
})
js中實(shí)參和形參可以不一樣 arguments 對象接收實(shí)參傳過來的數(shù)據(jù)
* */
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('傳入?yún)?shù)錯誤')
}
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 把字符串轉(zhuǎn)換成對象*/
return new ObjectID(id);
}
//統(tǒng)計數(shù)量的方法
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();
啟動服務(wù)


4.4 聚合查詢
聚合查詢文章分類信息,分類信息的方式要放在article的schema里面,這樣才能聚合查詢到

聚合查詢結(jié)果

查詢訂單,聚合查詢訂單關(guān)聯(lián)的商品信息返回,實(shí)現(xiàn)類似以下效果

// 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}, // 支付狀態(tài): 0 表示未支付 1 已經(jīng)支付
pay_type:{type: GraphQLString}, // 支付類型: alipay wechat
order_status: { // 訂單狀態(tài): 0 已下單 1 已付款 2 已配貨 3、發(fā)貨 4、交易成功 5、退貨 6、取消
type: GraphQLInt
},
add_time: {
type: GraphQLString
},
// 聚合查詢訂單關(guān)聯(lián)的商品列表信息
orderItems:{
type:GraphQLList(OrderItem),
async resolve(parent,args){
//獲取當(dāng)前訂單對應(yīng)的商品 parent._id就是objectId
var orderItemList=await DB.find('order_item',{"order_id":parent._id});
return orderItemList;
}
}
}
})
// 定義一個根 配置調(diào)用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 當(dāng)前新聞的分類id
console.log(parent);
var cateResult=await DB.find('articlecate',{"_id":DB.getObjectId(parent.pid)});
return cateResult[0];
}
}
}
})
//2、定義一個根 配置調(diào)用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 實(shí)現(xiàn)數(shù)據(jù)增加、修改、刪除
// scehma/default.js
//增加 修改 刪除
// 定義根MutationRoot實(shí)現(xiàn)增刪改
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簡單查詢
安裝
- 找到Vue中集成GraphQl的文檔
- 安裝相應(yīng)的模塊
ApolloBoost是一種零配置開始使用ApolloClient的方式。它包含一些實(shí)用的默認(rèn)值,例如我們推薦的InMemoryCache和HttpLink,它非常適合用于快速啟動開發(fā)。將它與vue-apollo和graphql一起安裝:
npm install vue-apollo graphql apollo-boost --save
- 在
src/main.js中引入apollo-boost模塊并實(shí)例化ApolloClient
import ApolloClient from'apollo-boost'
const apolloClient = newApolloClient({
//你需要在這里使用絕對路徑
uri:'http://118.123.14.36:3002/graphql'
})
可以打開 http://118.123.14.36:3002/graphql 在控制臺查看查詢結(jié)果

- 在
src/main.js配置vue-apollo插件
import VueApollofrom'vue-apollo'
Vue.use(VueApollo);
- 創(chuàng)建
Apollo provider
Provider保存了可以在接下來被所有子組件使用的Apollo客戶端實(shí)例
const apolloProvider = newVueApollo({
defaultClient:apolloClient
})
使用apollo Provider選項(xiàng)將它添加到你的應(yīng)用程序
new Vue({
el:'#app',
apolloProvider,
render:h=>h(App)
})
簡單查詢
組件加載的時候就會去服務(wù)器請求數(shù)據(jù),請求的數(shù)據(jù)會放在
navList這個屬性上面,在模板中可以直接使用當(dāng)前屬性
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 具體選項(xiàng)
apollo: {
// // 帶參數(shù)的查詢
// ping: {
// // gql 查詢
// query: gql`query PingMessage($message: String!) {
// ping(message: $message)
// }`,
// // 靜態(tài)參數(shù)
// variables: {
// message: 'Meow',
// },
// },
},
apollo: {
// 注意方法名稱 和 查詢的名稱對應(yīng)
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()">
點(diǎn)擊按鈕觸發(fā)事件請求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
// }
// }`
// 把請求的數(shù)據(jù)賦值給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()">獲取文章數(shù)據(jù)</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增加修改刪除
服務(wù)器端接口

<template>
<div class="news">
<h1>導(dǎo)航的增加修改刪除</h1>
<div class="navForm">
導(dǎo)航名稱:<input v-model="navJson.title" type="text" /> <br><br>
導(dǎo)航鏈接: <input v-model="navJson.url" type="text" /><br><br>
<button @click="doAdd()">提交數(shù)據(jù)</button>
<button @click="doEdit()">修改數(shù)據(jù)</button>
<button @click="doDele()">刪除數(shù)據(jù)</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);
})
},
// 修改數(shù)據(jù)
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:數(shù)據(jù)拼接
<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: [] /*實(shí)際要循環(huán)的數(shù)據(jù)*/,
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; //沒有數(shù)據(jù)禁用上拉分頁加載更多
}
},
error(err) {
console.log(err);
}
});
}
}
};
</script>
<style scoped>
li {
line-height: 4;
}
</style>
方法2:使用 fetchMore 實(shí)現(xiàn)分頁(推薦)
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
},
// 用新數(shù)據(jù)轉(zhuǎn)換之前的結(jié)果
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>
分頁效果

項(xiàng)目例子完整代碼下載地址 https://blog.poetries.top/assets/graphql-code.zip
六、文檔
- 中文文檔:http://graphql.cn
- Github: https://github.com/facebook/graphql
- vue-apollo文檔:https://vue-apollo.netlify.app/zh-cn/guide/apollo.html
本文由mdnice多平臺發(fā)布