Node.js 蚕食计划(七)—— MongoDB + GraphQL + Vue 初体验

首先须要搭建一个简单的应用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 为什么没有火起来? - 尤雨溪的回答 

但时至今日,GraphQL 已经获得了普遍承认,有许多大厂已经开始普遍使用(好比 TX 的 CSIG)

特别是对于有全栈发展兴趣的小伙伴,学一下 GraphQL 是颇有必要的,这样我就不至于只能看国外的文章来学 GraphQL 了

 

 

参考文章:

《你必需要懂得关于 mongoose 的一小小部分》

《Why GraphQL is the future》

《How to GraphQL》

相关文章
相关标签/搜索