hugh 的个人博客

Everyday is a new day

Express + GraphQL构建简单的cms框架DEMO

1. 技术选择

Express + GraphQL + VUE + MySQL

gitHub 地址

gitee 地址

2. 实现目标

a. 实现创建 cms 系统及相应业务界面

b. 实现组件关联

c. 实现页面可编辑

3. 插件选择

a. express-graphql - 用于集成 GraphQL 到 express 框架中

b. vue-apollo & vue-apollo - 用于在 client 端调用 GraphQL 的接口

c. graphql-tools - 帮助我们使用 js 更方便的构建 GraphQL schame

4. demo 运行示意图





5. 具体实现分解

1. 首先分析 GraphQL 的特点

主要包含以下内容 1 个 schame, 5 种基本类型 +3 中扩展类型 + 自定义类型, 2 种主要操作 query&mutation

2. 如何构建一个可用的 schame,详情见第 3 步

如果使用官方的 GraphQL 库,示例操作如下

npm install graphql

构建 schame 如下

var {
  GraphQLList,
  GraphQLObjectType,
  GraphQLSchema,
  GraphQLString,
  GraphQLInt,
  GraphQLFloat,
  GraphQLEnumType,
  GraphQLNonNull,
  GraphQLInterfaceType,
  GraphQLInputObjectType
} = require('graphql');
const componentsSchema = require('./components');

const themesSchema = require('./themes');


const testSchema = require('./test');


const Query=new GraphQLObjectType({

name:'CommonQuery',

description:'通用查询',

fields:()=>(Object.assign({},

componentsSchema.query,

themesSchema.query,

testSchema.query

))

});

const Mutation=new GraphQLObjectType({

name:'CommonMutation',

description:'通用编辑',

fields:()=>(Object.assign({},

componentsSchema.mutation

))

});

const schema = new GraphQLSchema({

query: Query,

mutation: Mutation

});


module.exports = schema;

或者

var { graphql, buildSchema } = require('graphql');
var schema = buildSchema(type Query { hello: String });

再配合 express-graphql

var graphqlHTTP = require('express-graphql');
app.use('/graphql2', graphqlHTTP({

schema: schemas,

graphiql: true

}));

测试访问 http://localhost:9004/graphql2

3. 基于业务特点构建通用的 schame 模板

a. 基于单表的增删改查

我们已经了解到 GraphQL 实现的是类似 controller/action 层的功能, 那单表增删改查操作其实就是普通的数据库操作了,利用 MySQL 库,可以轻松实现

b. 使用第 2 步介绍的库构建 schema

首先定义一张组件表

c. 定义针对组件表的查询、新增功能

var daoUtil = new BaseDao();
const tableName = "m_components";
const Components = new GraphQLObjectType({

name:'Components',

description:"组件对象POJO",

fields: () => {

return ({

id: {type: new GraphQLNonNull(GraphQLInt)},

name: {type: new GraphQLNonNull(GraphQLString)},

theme: {type: new GraphQLNonNull(GraphQLInt)},

frontShow: {type: GraphQLString},

backShow: {type: GraphQLString},

createAt: {type: GraphQLString},

createBy: {type: new GraphQLNonNull(GraphQLInt)},

});

}

});


module.exports = {

query: {

components_list: {

type: new GraphQLList(Components),

description: '查询组件列表',

resolve: async function(source) {

return (await daoUtil.queryAll(tableName));

}

}

},

mutation: {

components_add: {

type: Components,

description: '添加组件',

args: {

id: {type: GraphQLInt},

name: {type: new GraphQLNonNull(GraphQLString)},

theme: {type: new GraphQLNonNull(GraphQLString)},

},

resolve: async function(source, {id, name, theme}) {

var user = {

name: name,

theme: theme,

};

return await daoUtil.insert(tableName, user);

}

}

}

}

d. 使用自带的 GraphQL 编辑器测试

使用方法见:graphiql

4. 使用模板自动构建 schame

常见的 schame 构建如上, 基于此,我们抽取出公共的部分, 使用模板生成对应对象的所有操作

a. 模板分析

模板中应该具备对象的可操作列表, 入参、返回

type Query {
 {{each $data}}
  {{$value.name}}_all: [{{$value.modal}}]
  {{$value.name}}_page(page: Int!,limit: Int!, where:[where_query]): {{$value.modal}}_page
  {{$value.name}}_byId(id: Int!): {{$value.modal}}
  {{$value.name}}_byField({{each $value.columns}}  {{$value.Field}}:{{$value.fieldType}}, {{/each}}): [{{$value.modal}}]
 {{/each}}
}
type Mutation {

{{each $data}}

{{$value.name}}_add( {{each $value.columns}} {{if !$value.unUserInMutation}} {{$value.Field}}:{{$value.Type}},{{/if}} {{/each}} ): {{$value.modal}}

{{$value.name}}_batch(list: [{{$value.modal}}_mutation]!): batch_result

{{$value.name}}_delete(id: Int!): batch_result

{{$value.name}}_updateById({{each $value.columns}}  {{$value.Field}}:{{$value.fieldType}}, {{/each}}): {{$value.modal}}

{{/each}}

}

以上是对外提供的 query 和 Mutation。 $data 为所有对象的 list

{{$value.modal}} 对应了创建的每个对象在 schame 中对应的可操作对象

type {{$value.modal}} {
  {{each $value.columns }}
    {{$value.Field}}:{{$value.Type}}
  {{/each}}
  {{each $value.relations relation}}
    {{relation.modal}}:{{if relation.type == 'o2o'}} {{relation.modal}} {{/if}}{{if relation.type == 'o2m'}} [{{relation.modal}}] {{/if}}
  {{/each}}
}

{{$value.modal}}_page 对应了构建的分页对象在 schame 中对应的对象

type {{$value.modal}}_page {
  page: Int!,
  limit: Int!,
  list: [{{$value.modal}}],
  total: Int!
}

最后,通过以下代码封装操作

schema {
  query: Query
  mutation: Mutation
}

b. 调用模板方法渲染方法,同时构建 schame 对象

var gql = template(this.artPath, data);
      // console.log(gql)
      let typeDefs = `${gql}`
      let jsSchema = makeExecutableSchema({
        typeDefs,
        resolvers,
      });

5. 其他

a. 如何实现关联查询

通过定义一个配置文件, 管理对象间的关系

"m_components": [{
      "type": "o2m",
      "field": "id",
      "targetTable": "m_component_props",
      "targetField": "componentId"
    },{
      "type": "o2o",
      "field": "theme",
      "targetTable": "m_theme",
      "targetField": "id"
    }]

通过 schame 绑定查询关系

{{each $value.relations relation}}
    {{relation.modal}}:{{if relation.type == 'o2o'}} {{relation.modal}} {{/if}}{{if relation.type == 'o2m'}} [{{relation.modal}}] {{/if}}
  {{/each}}

b. 如何解决 N+1 查询问题

由于 GraphQL 会根据 schame,一层层解析,最终返回结果,导致关联查询时,会出现 N+1 查询的问题

解决 N+1 查询的基本方法就是讲查询条件集中到一个 SQL 中

在我们的范例中, 可以利用 resolver 中 context 对象在各个 resolver 间传递, 结合关系查询配置, 在父节点中集成查询,然后传递到子 resolver 中返回

resolver 中接受参数如下

  • obj 上一级对象,如果字段属于根节点查询类型通常不会被使用。
  • args 可以提供在 GraphQL 查询中传入的参数。
  • context 会被提供给所有解析器,并且持有重要的上下文信息比如当前登入的用户或者数据库访问对象。
  • info 一个保存与当前查询相关的字段特定信息以及 schema 详细信息的值

标题:Express + GraphQL构建简单的cms框架DEMO
作者:hugh0524
地址:https://blog.uproject.cn/articles/2019/04/03/1554276742410.html