使用micro包创建文件上传微服务并使用Schema Stiching应用微服务的若干“坑”及解决对策

一、首先说一下用到的包:

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老路上去。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值