最近在做把graphql融入我的项目中。起因是咱们团队的后端应用的是restful标准。每次查问的时候可能多少都会呈现冗余字段,要剔除这些冗余字段对于后端同学来说没有技术含量又耗时。另外后端同学对于bff层其实不怎么感冒,因为数据聚合对他们来说没什么含量,齐全是对前端同学服务。所以咱们齐全能够引入查问来接手后端同学的bff层。又或者咱们新增了字段须要查问新增的字段后端同学也须要更改。基于这些尝试引入node+graphql。graphql的查问劣势在于前端能够被动管制字段的获取(只有这些字段是能够拜访的)。集成graphql有两种形式。
- 后端同学间接集成 (java接口(restful或者graphql)-->前端)
- 前端减少两头服务层(java接口-->前端两头服务层nodejs(graphql)-->前端)
对于第一种形式,后端同学可能更改会更大,更改接口标准来投合前端可能代价太大且后端同学可能也不太会快乐批改接口标准多进去的工作量。所以咱们选了第二种,引入nodejs中间层作为申请的转发。
首先批改前端的代理前端代理到本地nodejs服务,间接应用weboack的proxy代理配置:
proxy: { '/api': { target: 'http://localhost:8080/', changeOrigin: true, }, '/local': { target: 'http://localhost:8080/', changeOrigin: true, pathRewrite: { '^/local': '' }, }, },
代理写了两个配置,带有'/api'前缀的间接代理到后端,带有'/local'的要在node中间层做解决。为什么要写要两个配置,因为不是所有的申请都须要应用graphql做解决,这一点在前面应用它的时候就会晓得,它有劣势当然也有劣势。引入你的我的项目要看它能施展多大价值。
写了这两个配置之后,带有两个关键字的申请都讲代理到本地node服务的8080端口。接下来配置node中间层。
前端两头服务层的配置
两头服务层应用koa2搭建,当然你也能够应用express等等其余。graphql的集成就是用中间件koa-graphql
const Koa = require('koa');const koaStatic = require('koa-static');const views = require('koa-views');const koaBody = require('koa-body');const path = require('path');const mount = require('koa-mount');const { graphqlHTTP } = require('koa-graphql');const { makeExecutableSchema } = require('graphql-tools');const loggerMiddleware = require('./middleware/logger');const errorHandler = require('./middleware/errorHandler');const responseWrapperMiddleware = require('./middleware/responseWrapper');// const decoratorRequest = require('./middleware/decoratorRequest');const axiosRequest = require('./middleware/axiosRequest');const accessToken = require('./middleware/accessToken');const apiProxy = require('./middleware/apiProxy');const typeDefs = require('./graphql/typeDefs');const resolvers = require('./graphql/resolvers');const router = require('./routes/_router');const { APP_KEYS, API_HOST, APP_ID, APP_SECRET } = require('./config');const port = process.env.PORT || 8080;const distPath = path.join(__dirname, '/dist');const getSchema = (...rst) => { const schema = makeExecutableSchema({ typeDefs: typeDefs, resolvers: resolvers(...rst), }); return schema;};const app = new Koa();// logger配置app.use(loggerMiddleware());// 设置动态资源目录app.use( koaStatic(path.resolve(__dirname, './dist'), { index: false, maxage: 60 * 60 * 24 * 365, }),);// 各环境下通用app配置// cookie验证签名app.keys = APP_KEYS;//设置模板引擎ejsapp.use( views(distPath, { map: { html: 'ejs', }, }),);// 异样解决app.use(errorHandler);// req.bodyapp.use(koaBody({ multipart: true }));// 包装申请的返回app.use(responseWrapperMiddleware());// 申请app.use( axiosRequest({ baseURL: `${API_HOST}/audit`, }),);// 申请后端的accessTokenapp.use( accessToken({ appId: APP_ID, appSecret: APP_SECRET, }),);// 间接代理前端的/api申请转发给后端,外部对立做鉴权和参数设置app.use( apiProxy({ prefix: '/api', }),);// koa graphql中间件app.use( mount( '/graphql', graphqlHTTP(async ( request, response, ctx, graphQLParams ) => { return ({ schema: getSchema(request, response, ctx, graphQLParams), graphiql: true, }); }) ),);// 路由app.use(router.routes());app.use(router.allowedMethods());app.listen(port, function() { console.log( `\n[${ process.env.NODE_ENV === 'production' ? 'production' : 'development' }] app server listening on port: ${port}\n`, );});
次要看看graphql的配置其余都是koa惯例的中间件配置
const getSchema = (...rst) => { const schema = makeExecutableSchema({ typeDefs: typeDefs, resolvers: resolvers(...rst), }); return schema;}
次要是生成graphql须要的schema。typeDefs是graphql的类型定义,应用的是schema来束缚类型,resolvers就是解释器也就是你定义的类型须要怎么解决。
比方:
你的typeDefs类型自定是这样子(它是一个字符串):
const typeDefs = ` type ExportItem { applicantStatus: String approving: [ String ] approvingMulitPassType: String auditFlowId: String bizName: String createdAt: Int createdBy: Int createdByName: String deleted: Boolean finishTime: Int groupId: String groupName: String id: String showApplyId: String templateId: String templateName: String updatedAt: Int updatedBy: Int updatedByName: String auditFlowForwardType: String uiConfig: String templateDesc: String } input QueryExportListParams { pageIndex: Int pageSize: Int finishedTimeBegin: Int finishedTimeEnd: Int showApplyId: String auditFlowId: String bizName: String initiatorEmployeeId: Int status: String } type Query { exportList(params: QueryExportListParams): [ ExportItem ] exportDetail(id: String): ExportItem } `
除开Query是graphql外部关键字,其余都是咱们定义的。Query是graphql中的顶层类型,除开Query咱们罕用的还有Mutation。graphql规定所有的查问定义都要放在Query中,那么批改操作比方,咱们要做减少,批改这些操作就放在mutation中。其实就算把所有的操作都放在query中或者mutation中解析也会通过,然而作为标准query中写查问,mutation中写操作兴许更更好。那下面的定义是什么意思先剖析一下,先看Query外部:
type Query { exportList(params: QueryExportListParams): [ ExportItem ] exportDetail(id: String): ExportItem }
代表咱们定义了两个查问名字叫exportList, exportDetail。exportDetail承受一个名字叫params的参数,params的类型是QueryExportListParams,返回一个数组数组外面的数据项类型是ExportItem。exportDetail承受一个id的参数id类型是字符串,返回的数据类型是ExportItem。ExportItem是咱们本人定义的数据类型。QueryExportListParams是本人定义的参数类型,参数是输出类型必须要应用input关键字定义。那么这里定义了类型实现在哪里,实现就在resolvers中,每个类型定义在resolver中都必须有解析器一一对应。
所以resolvers张这样子
const resolvers = { Query: { exportList: async (_, { params }) => { const res = await ctx.axios({ url: '/data/export/all', method: 'get', params, headers }); return res.data; }, exportDetail: async (_, { id }) => { const res = await ctx.axios({ url: `/applicant/byId/${id}`, method: 'get', headers }); return res.data; } } };
解析器中就有类型定义的实现exportList,exportDetail。在解析器中,他们的数据起源能够是任何中央,有可能是数据库,也可能是其余接口。咱们这里是做中间层转发。所以间接应用axios转发到后端了。那么类型定义的参数就在这里获取应用。
配置好后启动中间层服务,graphql查问失效之后会开启一个/graphql的门路接口,如果咱们要应用graphql查问就申请/graphql这个门路。比方咱们在前端申请graphql这个查问就会这么写:
post('/graphql', { query: `query ExportList($params: QueryExportListParams){ exportList(params: $params) { id } }`, variables: { params: { finishedTimeBegin: finishedTime ? +moment(finishedTime[0]).startOf('day') : void 0, finishedTimeEnd: finishedTime ? +moment(finishedTime[1]).endOf('day') : void 0, ...rst, } } })
QueryExportListParams就是咱们在中间层定义的参数类型,variables.params是咱们传递给resolvers的参数值
exportList(params: $params) { id }
这表白咱们查问的返回数据中之返回带有id的列表,返回的是列表是因为咱们在类型定义的时候曾经定义这个查问须要返回列表:
type Query { exportList(params: QueryExportListParams): [ ExportItem ] exportDetail(id: String): ExportItem }
这里咱们曾经定义了exportList的返回类型是一个列表,类标的类型是ExportItem,所以咱们不须要再通知查问是不是取列表,返回类型都是当时定义好的,咱们须要做的是管制返回字段,只有ExportItem这个类型蕴含的字段咱们都能够定义取或者是不取,比方咱们下面
exportList(params: $params) { id }
这就是示意咱们只取id这个字段那么返回的数据中只会有id这个字段,如果咱们还须要其余字段比方咱们还须要groupName这个字段,就能够这样写
exportList(params: $params) { id groupName }
只有是在咱们定义的ExportItem这个类型之中咱们都能够管制它取或者不取,如果你查问的参数在服务端的graphql中未定义就会出错。graphql的查问中另外一个比拟好的中央在于指令,指令的退出会让bff层更加有做为(放在下一次讲)