一、首先说一下用到的包:
1、使用apollo-upload-client和apollo-upload-server来使用graphql的Mutation文件上传
2、使用micro和apollo-server-micro来创建微服务
二、遇到的主要“坑”及解决办法:
1、apollo-upload-server不支持micro的问题
apollo-upload-server直接支持express和koa,以express为例,使用方法是:
app.use(
'/graphql',
bodyParser.json(),
apolloUploadExpress(/* Options */),
graphqlExpress(/* … */)
)
主要用到了body-parser中间件和apollo-upload-server导出的apolloUploadExpress接口。apolloUploadExpress源码为:
export const apolloUploadExpress = options => (request, response, next) => {
if (!request.is('multipart/form-data')) return next()
processRequest(request, options)
.then(body => {
request.body = body
next()
})
.catch(error => {
if (error.status && error.expose) response.status(error.status)
next(error)
})
}
要使用micro实现就要用apollo-upload-server导出的processRequest接口。Server端本来是这样的:
const graphqlHandler = microGraphql({ schema });
const graphiqlHandler = microGraphiql({ endpointURL: '/graphql' });
const server = micro(
router(
get('/graphql', graphqlHandler),
post('/graphql', graphqlHandler),
get('/graphiql', graphiqlHandler),
(req, res) => send(res, 404, 'not found'),
),
);
也就是要在graphqlHandler被处理前先用body-parser和processRequest处理一下req对象。但是问题来了,micro不支持中间件,也就是没有next函数,所以只能在graphqlHanlder外面再包装一层函数,先用body-parser和processRequest处理,然后再调用graphqlHandler,最终实现代码为:
let graphqlHandler = async (req, res) => {
const gglResponse = await new Promise((resolve, reject) => {
const next = async (err) => {
if (err instanceof Error) {
throw err
return
}
if (typeis(req, ['multipart/form-data'])) {
req.body = await processRequest(req)
}
const schema = await buildSchema()
runHttpQuery([req, res], {
method: req.method,
options: {
schema,
context: { db }
},
query: req.method === 'POST' ? req.body : req.query
}).then((response) => resolve(response)).catch((err) => reject(err))
}
bodyParser.json()(req, res, next)
})
return gglResponse
}
这里要解释一下:
第一,我没有用micro提供的json接口来处理req,而是使用了与express配套的body-parser来处理req,是因为遇到了Invalid JSON的错误,感觉micro的json接口比较简单暴力,对于req中包含文件流的处理不好,而body-parser不存在此问题,但是body-parser由于是中间件,它必须接收一个next函数,所以我就创建了一个next函数传给它,因为body-parser内部无非是解析成功就调用next(),解析错误就调用next(err),所以函数最终都会执行到next内部去。
第二,我没有用microGraph接口而是直接调用了runHttpQuery,因为在microGraph内部还是调用了micro的json接口对req进行处理,然后才调用runHttpQuery,而这样还是会导致Invalid JSON的错误,使用json接口处理req就是为了从req中解析出runHttpQuery的第三个参数query,而query我们已经通过processRequest函数得到了,所以没必要再多此一举。
第三,调用runHttpQuery成功以后如何向客户端返回res的问题,这也是为什么我用了一个Promise对象将代码都包装起来的原因,这里我犯了一个错误,micro和express是不一样的,express是通过res的send、write、json等接口向客户端返回数据的,而micro是直接将graphqlHanlder函数的返回值发送到客户端的,由于graphqlHandler函数最终执行进入next函数中去了,使用无法直接将内层函数的返回值传递到最外层,所以我用了Promise和await。
2、Schema Stiching序列化文件流的问题
因为我是在Server端通过合并本地schema和“远程”(另一个项目)图片上传服务的schema来为客户端提供一个统一的endpoint,客户端通过multipart/form-data发送过来的数据都是在本地Server,不在图片上传Server,所以mergedSchema在执行时会使用node-fetch将Mutation发送到remoteSchema去执行。使用node-fetch创建RemoteSchema的代码为:
const fetcher = async ({ query, variables, operationName, context }) => {
const fetchResult = await fetch('http://api.githunt.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables, operationName })
});
return fetchResult.json();
};
const schema = makeRemoteExecutableSchema({
schema: await introspectSchema(fetcher),
fetcher,
});
可以看出,使用JSON.stringify是肯定无法正常将variables中包含的文件正常序列化的,所以我用了iconv-lite将文件流序列化为base64字符串,如下:
if (variables && variables.file) {
let { stream, filename, mimetype, encoding } = await variables.file
stream = await new Promise(((resolve, reject) => {
stream.pipe(iconv.decodeStream('base64')).collect(function(err, body) {
if(err) reject(err)
else resolve(body)
})
}))
variables.file = { stream, filename, mimetype, encoding }
body = JSON.stringify(body)
} else {
body = JSON.stringify(body)
}
当然,如果图片服务端要将图片恢复还需要使用iconv.encode(stream, 'base64')转换回来。
三、思考总结
apollo-upload-server和apollo-upload-client实现了将文件用graphql mutation来上传,如果服务端用的是express或者koa,可以考虑使用这两个包,只需处理好文件流序列化的问题就可以了,如果不考虑统一endpoint(Schema Stiching)的话,在客户端创建ApolloClient的时候使用split(请求分流)的话,那样会更好一点,因为客户端可以直接将文件post到文件上传微服务中,就不会遇到这些坑了。当然了,只要不涉及文件流,使用Schema Stiching还是不错的,除非你想回到REST老路上去。