首先須要搭建一個簡單的應用html
前端部分很少贅述,若是確實沒接觸過 Vue 項目,能夠參考個人《Vue 爬坑之路》系列前端
後端服務能夠參考以前的文章《Node.js 蠶食計劃(六)—— MongoDB + Koa 入門》git
完整的項目地址:https://github.com/wisewrong/Test-GraphQL-App,結合項目食用本文更香哦~github
1、Mongoose
mongodb
在上一篇文章《Node.js 蠶食計劃(六)》裏,直接使用了 mongodb 中間件來鏈接數據庫,並嘗試着操做數據庫數據庫
但咱們通常不會直接用 MongoDB 的原生函數來操做數據庫,Mongoose 就是一套操做 MongoDB 數據庫的接口npm
1. Schema 與 Modeljson
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 來操做數據庫api
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 = 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 的話,這種改動就不用後端的小夥伴參與了
2、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, };
4、使用 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 已經獲得了普遍承認,有許多大廠已經開始普遍使用(好比 TX 的 CSIG)
特別是對於有全棧發展興趣的小夥伴,學一下 GraphQL 是頗有必要的,這樣我就不至於只能看國外的文章來學 GraphQL 了
參考文章: