共计 6009 个字符,预计需要花费 16 分钟才能阅读完成。
最近在做把 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;
// 设置模板引擎 ejs
app.use(
views(distPath, {
map: {html: 'ejs',},
}),
);
// 异样解决
app.use(errorHandler);
// req.body
app.use(koaBody({ multipart: true}));
// 包装申请的返回
app.use(responseWrapperMiddleware());
// 申请
app.use(
axiosRequest({baseURL: `${API_HOST}/audit`,
}),
);
// 申请后端的 accessToken
app.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 层更加有做为(放在下一次讲)