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

巫马令
2023-12-01

技术选择

Express + GraphQL + VUE + mysql
gitHub地址 gitee地址

实现目标

  • 实现创建cms系统及相应业务界面
  • 实现组件关联
  • 实现页面可编辑

插件选择

  • express-graphql - 用于集成graphql到express框架中
  • vue-apollo & vue-apollo - 用于在client端调用graphql的接口
  • graphql-tools - 帮助我们使用js更方便的构建graphql schame

demo运行示意图

具体实现分解

首先分析graphql的特点

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

如何构建一个可用的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

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

基于单表的增删改查

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

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

首先定义一张组件表

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

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);
      }
    }
  }
}

复制代码

使用自带的graphql编辑器测试

使用方法见:graphiql

使用模板自动构建schame

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

  • 模板分析
    模板中应该具备对象的可操作列表, 入参、返回
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
}
复制代码
  • 调用模板方法渲染方法,同时构建schame对象
var gql = template(this.artPath, data);
      // console.log(gql)
      let typeDefs = `${gql}`
      let jsSchema = makeExecutableSchema({
        typeDefs,
        resolvers,
      });
复制代码

其他

如何实现关联查询

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

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}}

复制代码

如何解决N+1查询问题

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

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

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

resolver中接受参数如下

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

 类似资料: