首先需要搭建一個簡單的應用
前端部分不多贅述,如果確實沒接觸過 Vue 專案,可以參考我的《Vue 爬坑之路》系列
后端服務可以參考之前的文章《Node.js 蠶食計劃(六)—— MongoDB + Koa 入門》
完整的專案地址:https://github.com/wisewrong/Test-GraphQL-App,結合專案食用本文更香哦~
一、Mongoose
在上一篇文章《Node.js 蠶食計劃(六)》里,直接使用了 mongodb 中間件來連接資料庫,并嘗試著操作資料庫
但我們一般不會直接用 MongoDB 的原生函式來操作資料庫,Mongoose 就是一套操作 MongoDB 資料庫的介面
1. Schema 與 Model
Schema 是 Mongoose 的基礎,用來定義集合的資料模型,也就是傳統意義上的表結構
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// 影片資訊
const MovieSchema = new Schema({
name: String, // 影片名稱
years: Number, // 上映年代
director: String, // 導演
category: [String], // 影片型別
comments: [ // 影評
{
author: String,
createdAt: {
type: Date,
default: Date.now(),
},
updatedAt: {
type: Date,
default: Date.now()
}
}
],
});
module.exports = mongoose.model('Movie', MovieSchema);
上面的最后一行代碼,是基于定義好的 Schema 生成 Model,我們可以通過 Model 來操作資料庫
mongoose.model('ModelName', SchemaObj)
這里的 model() 方法可以接收兩個引數,第二個引數是創建好的 Schema 實體
第一個引數 ModelName 是資料庫中集合 (collection) 名稱的單數形式,Mongoose 會查找名稱為 ModelName 復數形式的集合
對于上例,Movie 這個 model 就對應資料庫中 movies 這個 collection,如果資料庫沒有對應的集合會自動創建
2. Model 的增刪改查
在 mongoose 中是通過操作 Model 來實作資料庫的增刪改查
< 新增 >
Model.create(data, callback)
< 查詢 >
// 回傳所有符合查詢條件 conditions 的資料
Model.find(conditions, callback);
// 回傳找到的第一個檔案
Model.findOne(conditions, callback);
// 只針對主鍵 _id 查詢
Model.findById('_id', callback);
< 修改 >
// 批量修改符合條件 conditions 的資料
Model.updateMany(conditions, update, options, callback)
// 修改指定 id 的資料
Model.findByIdAndUpdate(id, update, options , callback)
// 修改第一個符合查詢條件的資料
Model.updateOne(conditions, update, options , callback)
// 替換第一個符合查詢條件的資料
Model.replaceOne(conditions, update, options , callback)
< 洗掉 >
// 洗掉符合條件的所有資料
Model.remove(conditions, callback);
// 洗掉指定 id 的資料
Model.findByIdAndRemove(id, options, callback);
比如封裝一個插入資料的方法:
const Movie = require('../mongodb/models/movie');
// 新建電影
const createMovie = (req) => {
return Movie.create(req);
}
// 更新電影資訊
const updateMovie = (req) => {
return Movie.findByIdAndUpdate(req._id, req, {
new: true,
});
}
// 保存電影
const saveMovie = async (ctx, next) => {
const req = ctx.request.body;
// 校驗必填
if (!req.name) {
return { message: '影片名稱不能為空' }
}
const data =https://www.cnblogs.com/wisewrong/p/ req._id
? await updateMovie(req)
: await createMovie(req);
return { data };
};
module.exports = {
saveMovie,
};
mongoose 也有更規范的查詢條件,可以參考官網的 Query 配置
3. 連接資料庫
使用 mongoose.connect 連接資料庫,可以在 connect 方法中傳入第二個引數作為回呼
也可以通過 mongoose.connection.on 來監聽相應的事件
/* /mongodb/index.js */
const mongoose = require("mongoose");
const { dbUrl } = require("../config");
// const dbUrl = 'mongodb://127.0.0.1:27017/Movie'; // 資料庫地址
const connect = () => {
// mongoose.set('debug', true)
mongoose.connect(dbUrl);
mongoose.connection.on("disconnected", () => {
mongoose.connect(dbUrl);
});
mongoose.connection.on("error", (err) => {
console.error('Connect Failed: ', err);
});
mongoose.connection.on("open", async () => {
console.log('?? Connecting MongoDB Successfully ??');
});
};
4. 介面實作
基于這些 API,我們就可以搭建一個相對規范的傳統后端服務
首先創建 model,然后創建 controller,在 controller 中引入 model,并使用 model 來操作資料庫
然后還可以通過 koa-router 來實作傳統介面
/* /router/api/movie.js */
const router = require('koa-router')();
const { apiPrefix } = require('../../config');
// const apiPrefix = '/api';
const movieController = require('../../controllers/movie');
router.prefix(apiPrefix);
router.post('/movie/save', movieController.saveMovie);
router.get('/movie/list', movieController.getMovie);
router.delete('/movie/delete/:id', movieController.deleteMovie);
module.exports = router;
最后只要在 app.js 中引入相應模塊,一個簡單的傳統服務就搭建好了
// app.js
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const api = require('./router/api');
// 連接資料庫
require('./mongodb');
const app = new Koa();
app.use(bodyParser());
// 注冊 API
for (const key in api) {
const router = api[key];
app.use(router.routes()).use(router.allowedMethods());
}
app.listen({port: 3200});
但這樣的傳統服務,介面的出參都是由后端決定的
如果業務調整,介面出參需要新增一個欄位,就需要后端和前端同時迭代
而如果使用 GraphQL 的話,這種改動就不用后端的小伙伴參與了
二、GraphQL
GraphQL 是一種新的 API 定義和查詢語言,它使前端能夠宣告式地獲取資料,從一定程度上自定義介面出參

像上圖這樣,介面的回應會按照入參的結構回傳出參,概念性的優點就不多贅述,實際感受之后才能明白它的優勢
先在專案中引入 koa-graphql 和 GraphQL.js 備用
npm install graphql koa-graphql --save
在 GraphQL 中,Schema 是定義整個查詢語言的入口
schema {
query: Query
mutation: Mutation
}
Schema 有一個必須定義的 query 型別,用來執行查詢操作;還有一個可選的 mutation,處理增刪改操作
這兩種型別其實都是 graphql.GraphQLObjectType 型別
構建一個 Schema 可以使用 graphql.buildSchema 或者構建型別 graphql.GraphQLSchema,先介紹一下 buildSchema
const Schema = buildSchema(`
type Query {
getList: [Movie]
getDetail: [Movie]
}
type Mutation {
add(post: input): [Movie],
}
`)
這里的 type Query 就是定義上面提到的 schema 中必須包含的 query 型別
需要注意的是,在型別下定義的欄位,并不是像 mongoose 中 schema 定義的檔案結構
這個欄位只是宣告一種型別,而型別的值取決于對應的 resolve 處理函式,所以將這個欄位當作查詢指令更便于理解
上面 Query.getList: [Movie] 表示通過 getList 指令能夠回傳一個陣列,陣列的每個元素是一個 Movie 型別,這個 Movie 是我們需要定義的另一個型別
/* /graphql/schema.js - 使用 buildSchema 創建的 GraphQL Schema */
const { buildSchema } = require('graphql');
const Schema = buildSchema(`
type Query {
getAllMovie: [Movie]
}
type Movie {
_id: String,
name: String,
years: String,
director: String,
}
`)
// 暫時不用 Mutation
module.exports = Schema;
這樣就定義了一個包含 name、years 等四個欄位的 Movie 型別,一個簡單的 Schema 就定義好了
然后來改造 controllers,引入 koa-graphql、剛才定義的 Schema,以及之前用 mongoose 生成的 Model
/* /controllers/movie.js */
const graphqlHTTP = require('koa-graphql');
const MovieSchema = require('../graphql/schema');
const Movie = require('../mongodb/models/movie');
// GraphQL 型別處理函式
const root = {
getAllMovie: async () => {
return Movie.find({});
}
}
// 查詢所有電影
const getMovie = graphqlHTTP({
schema: MovieSchema,
rootValue: root,
graphiql: true
});
module.exports = {
getMovie,
};
用 koa-graphql 提供的 graphqlHTTP 方法作為介面的 handler 函式,并傳入定義好的 schema
這里有一個 rootValue 物件,用來配置 schema 型別的具體操作函式,比如上面就定義了 getAllMovie 的操作函式
然后介面路徑還是按之前的方式配置:
/* /router/api/movie.js */
const router = require('koa-router')();
const movieController = require('../../controllers/movie');
router.all('/movie/list', movieController.getMovie);
module.exports = router;
一個簡單的 GraphQL 服務就完成了,接下來處理前端的請求
請求的時候需要攜帶 JSON 格式的引數,所以通常使用 post 請求
最主要的是,需要設定請求頭 'Content-Type': 'application/json'
然后按照 schema 的格式設定入參,比如查詢 schema 中 query 型別下的 getAllMovie:
request.post('/api/movie/list', {
query: `{
getAllMovie {
_id,
name,
}
}`
});
可以看到回應的結果為:

我們在 GraphQL 中定義的 Movie 型別有 name 等四個欄位,但入參中只設定了 name 和 _id,所以出參也只有 name 和 _id
如果把入參也改為四個欄位:

后端邏輯不用調整,請求結果就會變成:

Cool~
三、GraphQL 構建型別
上面的 Schema 是使用 buildSchema 定義的,但 buildSchema 接收的型別引數只能是一整個字串
如果我們復用某些自定義型別就不太方便,而且欄位的處理函式需要寫在 rootValue 里面,不方便模塊化管理
所以更推薦使用 GraphQLSchema 構建型別
const { GraphQLSchema, GraphQLObjectType } = require('graphql');
const schema = new GraphQLSchema({
query: new GraphQLObjectType(),
mutation: new GraphQLObjectType(),
});
GraphQLObjectType 是構建 Schema 型別的基本方法,包括 query 和 mutation 在內的所有型別都需要通過該建構式構建
我們先嘗試用構建型別的方式,來改寫將上面 buildSchema 定義的 Schema
/* /graphql/schema.js - 構建型別 */
const { GraphQLSchema, GraphQLObjectType } = require('graphql');
const getAllMovie = require('./query/movie.js');
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
fields: {
getAllMovie,
}
});
module.exports = new GraphQLSchema({
query: RootQuery,
// mutation: RootMutation,
});
這里定義了一個 RootQuery 型別,對應的是之前的:

這里的 getAllMovie 是由 Movie 型別組成的陣列,需要另外構建:
/* /graphql/types/movies.js - 定義 Movie 型別 */
const graphql = require('graphql');
const {
GraphQLObjectType,
GraphQLList,
GraphQLString,
GraphQLInt,
} = graphql;
const MovieType = new GraphQLObjectType({
name: 'Movie',
fields: () => ({
_id: { type: GraphQLString }, // String
name: { type: GraphQLString },
years: { type: GraphQLInt }, // Int
poster: { type: GraphQLString },
director: { type: GraphQLString },
category: { type: new GraphQLList(GraphQLString) }, // [String]
})
});
module.exports = MovieType;
在定義 Movie 型別下的具體欄位 fields 的時候,需要通過物件的形式規定型別 type
這里的 type 不能像之前那樣直接寫 String、Boolean,而是使用 graphql 中提供的型別物件
現在定義好了 Movie 型別,但是 getAllMovie 回傳的是 Movie 型別組成的陣列,還有一個對應的處理函式,所以我們要單獨維護一個 getAllMovie 物件
/* /graphql/query/movies.js - 定義 getAllMovie 欄位 */
const { GraphQLList } = require('graphql');
const movieGraphQLType = require('../types/movie.js');
const Movie = require('../../mongodb/models/movie.js');
module.exports = {
type: new GraphQLList(movieGraphQLType),
args: {},
resolve() {
return Movie.find({})
}
}
注意我們匯出的物件包含 type、args、resolve 三個欄位,而我們剛才定義 Movie 型別的時候,fields 欄位物件也包含一個 type 欄位
沒錯,這里匯出的物件其實就一個 field,而每個 field 都可以包含 type、args、resolve
其中 type 不用再提,resolve 就是該欄位對應的處理函式,對應上面 buildSchema 小節中 rootValue 中的欄位
args 用來描述 resolve 方法接收的引數,在后面介紹 mutation 的時候會介紹
由于每個 filed 都可以是一個獨立的型別,而每個型別可以配置自己的 resolve 處理函式,所以在 GraphQL 可以很方便的執行復雜查詢
只要在回應的型別中配置好 resolve,前端只需要調一次介面就能獲取到多個檔案的資料
到此為止,我們已經完成了從 buildSchema 到構建型別的改造,由于在 field 欄位中定義了 resolve,所以就可以不用定義 rootValue 了
/* /controllers/movie.js */
const graphqlHTTP = require('koa-graphql');
const MovieSchema = require('../graphql/schema');// 查詢所有電影
const getMovie = graphqlHTTP({
schema: MovieSchema,
graphiql: true
});
module.exports = {
getMovie,
};
四、使用 mutation 執行增刪改
上面提到了 args,它用來描述 resolve 方法的引數
首先來看一下前端怎么在 resolve 方法中傳參

看起來就和我們平時用的 function 一樣,但這里面大有玄機
首先如果引數是一個 String,就需要手動添加雙引號,而且只能是雙引號
如果用單引號會報錯(主要是為了避免文本中帶有單引號的情況 desc: "I'm Wise" )
Syntax Error: Unexpected single quote character ('), did you mean to use a double quote (")?
如果引數是 Int 型別,就不能添加引號 years: ${data.years},
如果引數是陣列型別,需要用 JSON.stringify 轉換
由于對引數型別的處理較為復雜,可以封裝一個處理引數的工具函式來統一處理
// 這只是我簡單嘗試之后的感想,如果小伙伴有更好的處理思路,一定要在評論區留言,感謝 ??
知道了怎么向 resolve 方法傳參(不只是 mutation,query 也可以傳參),再來說說 args:

它可以像定義 fields 一樣定義接收的引數,如果 args 里只寫了一個引數,而介面入參傳了入了多個,介面會回傳錯誤

如果入參傳的引數少了是可以的,只要必填項 GraphQLNonNull 沒落下
然后可以從 resolve 的第二個引數中獲取到前端傳過來的引數,再通過 Mongoose 生成的 Model 來操作資料
需要注意的是,前端在發送 mutation 請求的時候,要在 query 中宣告 mutation
定義好了 mutation,按照之前構建 RootQuery 物件的方式構建 RootMutation,并賦值給 schema,一個具有基本功能的 GraphQL 服務就完成了
如果對專案結構還不太清晰,可以看一下專案倉庫:https://github.com/wisewrong/Test-GraphQL-App

再回頭捋一下,其實后端服務只定義了一個介面,而具體的操作都是在前端分工
這樣雖然增加了前端的作業量,但也增加了前端的靈活性,讓后端的小伙伴能專注于資料庫的設計和優化
其實 GraphQL 早在 2015 年就發布了,卻一直沒有推廣開,當時尤大大還做了一波分析
GraphQL 為何沒有火起來? - 尤雨溪的回答
但時至今日,GraphQL 已經得到了廣泛認可,有許多大廠已經開始廣泛使用(比如 TX 的 CSIG)
特別是對于有全堆疊發展興趣的小伙伴,學一下 GraphQL 是很有必要的,這樣我就不至于只能看國外的文章來學 GraphQL 了
參考文章:
《你必須要懂得關于 mongoose 的一小小部分》
《Why GraphQL is the future》
《How to GraphQL》
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/28115.html
標籤:JavaScript
