设计思路
没啥思路,纯体力活,就是用NodeJS来实现一把媒体文件上传的 rfc1867/rfc2388 协议。 NodeJS上可以搜罗到的 multipart
实现,大部分是解释器,装载器引用最多的是form-data
这个包,接续上一版的实现,广度支持 nodejs http client,不挑食了。
node-fetch
用来探测请求体是不是FormData
函数片段:
//refer: https://github.com/node-fetch/node-fetch/blob/master/src/utils/is.js#L47-L67
/**
* Check if `obj` is a spec-compliant `FormData` object
*
* @param {*} object
* @return {boolean}
*/
export function isFormData(object) {
return (
typeof object === 'object' &&
typeof object.append === 'function' &&
typeof object.set === 'function' &&
typeof object.get === 'function' &&
typeof object.getAll === 'function' &&
typeof object.delete === 'function' &&
typeof object.keys === 'function' &&
typeof object.values === 'function' &&
typeof object.entries === 'function' &&
typeof object.constructor === 'function' &&
object[Symbol.toStringTag] === 'FormData'
);
}
axios
用来探测代码片段:
//refer: https://github.com/axios/axios/blob/master/lib/utils.js#L50-L58
/**
* Determine if a value is a FormData
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an FormData, otherwise false
*/
function isFormData(val) {
return (typeof FormData !== 'undefined') && (val instanceof FormData);
}
这个函数在浏览器环境上是可以的,但在NodeJS上是有问题的,FormData
不是全局对象,探测失败,退而求其次,使用如下探测函数:
// https://github.com/axios/axios/blob/master/lib/utils.js#L161-L169
/**
* Determine if a value is a Stream
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Stream, otherwise false
*/
function isStream(val) {
return isObject(val) && isFunction(val.pipe);
}
实现
在coding过程中,遇到了一个比较大的挑战就是对 delete
的实现。按照MDN的文档介绍,delete是要删除同名所有值,这里啃了好久,也妥妥的搞定了。总体实现就是在上一版的同步代码模型上,增加了10几个方法,覆盖住了MDN FormData上罗列的所有方法,并且支持了 stream 流动模式。
核心代码就是以下4个函数,均是对内置的data
BufferList
进行编排:
应用
同步模式
(new Multipart())
.append('a', 1)
.append('b', '2')
.append('c', Buffer.from('31'))
.append('d', JSON.stringify({}), 'any.json')
.append('e', require('fs').readFileSync('/path/your/file.jpg'), 'file.jpg')
.getBuffer();
异步(流动)模式
(new Multipart())
.append('f', require('fs').createReadStream('/path/your/file2.jpg'), 'file2.jpg')
.pipe(require('fs').createWriteStream('./file3.jpg'));
示例上传代码
const {readFileSync} = require('fs')
const {Wechatpay, Multipart, Hash: { sha256 }} = require('wechatpay-axios-plugin');
const wxpay = new Wechatpay({ mchid, secret, serial, privateKey, certs });
const file = readFileSync('./hellowechatpay.png');
const meta = {filename: 'hellowechatpay.png', sha256: sha256(file)};
const form = new Multipart();
form.append('file', file, 'hellowechatpay.png');
form.append('meta', JSON.stringify(meta), 'meta.json');
wxpay
.v3.marketing.favor.media.imageUpload(form, { meta, headers: form.getHeaders() })
.then(({data: { media_id }}) => media_id)
.then(console.info)
.catch(console.error);
更高级的stream流动模式下,一条龙从浏览器客户端至微信服务端的媒体直接上传,用js来实现,就变得很有可能了,这个留给喜欢钻研的同学搞一搞了。
最后
附上 源码地址 及 测试用例地址,如果用着还不放心,可以再装一下 form-data
包,通过 new FormData
来使用。
希望本文能解决你的困惑,如果喜欢,欢迎 Star。