3

最近在做微信公众号,需要将图片上传至阿里云OSS。在做这个功能的过程中,我走了不少弯路,尝试过很多种方法,最后终于研究出一种便捷优美的方式。现在把这些方法和思路记录下来,避免遗忘。

一、通过浏览器直接传给OSS

这种方式最简单。因为微信公众号的跳转页面是基于QQ浏览器的,所以可以直接使用HTML的input元素选择图片。

<input type="file", name="pic", accept="image/jpeg" />

OSS有一个Post Object的接口允许HTML表单上传文件,除了文件(file)之外,还有一些其他的字段如保存到OSS的路径(key)、策略(policy)、自己的OSS应用的accessKeyId、签名(signature)等。

所以需要构造表单。一般有两种方式:

1)构造DOM节点,表单提交上传

像下面代码这样构造form元素,然后利用$('form').submit()提交。

<!-- 请求地址由bucket名和oss的区域构成 -->
<form method="POST" action="http://bucketname.oss-cn-shenzhen.aliyuncs.com">
  <input type="hidden" name="key" />
  <input type="hidden" name="policy" />
  <input type="hidden" name="OSSAccessKeyId" />
  <input type="hidden" name="success_action_status" /> 
  <input type="hidden" name="signature" />
  <input type="file" name="file" accept="image/jpeg" />
</form>

2)利用Html5的FormData对象上传

像下面这样构造FormData对象,再通过ajax或fetch post表单数据。

const formData = new FormData();
formData.append('key', filePath); // OSS的保存路径
formData.append('policy', policy); // 策略
formData.append('OSSAccessKeyId', accessKeyId); // OSS对象的标识
formData.append('success_action_status', '200'); // 成功返回码
formData.append('signature', signature); // 签名
formData.append('file', file); // 图片文件,$('input[name="pic"]').files[0]

二、服务端下载微信图片再转存至OSS

上面的方法虽然简单直接,但只能从相册中选择图片,而想要拍摄图片并上传,则必须通过微信JS-SDK来调用相机。

wx.chooseImage({
  count: 1, // 默认9
  sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
  sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
  success: function (res) {
    // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
    var localIds = res.localIds; 
  }
});

这里有个问题,微信JS-SDK选择图片之后返回的是图片的标识id,而不是实际的图片文件,所以不能构造form表单上传OSS。

那该怎么办呢?思路:将图片先上传至微信的服务器(最多保存3天),再通过微信的下载多媒体文件接口(http://file.api.weixin.qq.com...)将图片下载到服务器,再上传至OSS(虽然有点绕,但可行)。

客户端代码:

wx.chooseImage({
  count: 1, // 默认9
  sizeType: ['original', 'compressed'],
  sourceType: ['album', 'camera'],
  success: function (res) {
    var localIds = res.localIds;
    wx.uploadImage({
      localId: localIds[0], // 需要上传的图片的本地ID,由chooseImage接口获得
      isShowProgressTips: 1, // 默认为1,显示进度提示
      success: function (res) {
        var serverId = res.serverId; // 返回图片的服务器端ID
        // do something ...
        // 调用自己搭建的服务端的api,传入serverId,做获取微信图片上传OSS的相关操作
        doSomething();
      }
    });
  }
});

小tips:选择图片时只要选择了compressed,微信就会自动帮我们压缩图片,官方文档也说明上传的多媒体文件会控制格式和大小,其中图片控制在jpg格式和1M以下的大小。所以,基本不用考虑图片过大的问题。实测中,8M的图片压缩后只有120KB左右。

服务端的代码经过了3次的演变才完善:

1)利用fs将图片写到本地

const fs = require('fs');
const request = require('require');
const OSS = require('ali-oss').Wrapper;

const ossClient = new OSS({
  accessKeyId: 'your access key',
  accessKeySecret: 'your access secret',
  bucket: 'your bucket name',
  region: 'oss-cn-hangzhou'
});

// 需要获取微信accessToken,这里不细说
const accessToken = 'access token';
const mediaId = 'xxxxxxx'; // 微信多媒体文件id
const destPath = `weixin/images/201702/${mediaId}.jpg`; // OSS文件路径,按自己喜欢构造咯
const wxReq = request(`http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=${accessToken }&media_id=${mediaId}`);

// 将文件流pipe到本地文件
wxReq.pipe(fs.createWriteStream(`${mediaId}.jpg`));
wxReq.on('end', () => {
  co(function* () {
    const result = yield ossClient.putStream(destPath, fs.createReadStream(`${mediaId}.jpg`), {timeout: 30 * 60 * 1000});
    console.log('图片上传阿里云结果', result);
    fs.unlink(`${mediaId}.jpg`);
    // res.status(200).json(result);
  }).catch(err => {
    console.warn(err);
    //res.status(500).send('上传文件出错');
  });
});

这种方式需要频繁地写文件和删文件,感觉一点都不极客。

2)利用memory-streams模块将图片写到内存

const request = require('require');
const OSS = require('ali-oss').Wrapper;
const streams = require('memory-streams');

const ossClient = new OSS({
  accessKeyId: 'your access key',
  accessKeySecret: 'your access secret',
  bucket: 'your bucket name',
  region: 'oss-cn-hangzhou'
});

const accessToken = 'access token';
const mediaId = 'xxxxxxx'; // 微信多媒体文件id
const destPath = `weixin/images/201702/${mediaId}.jpg`; // OSS文件路径
const writer = new streams.WritableStream();
const wxReq = request(`http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=${accessToken }&media_id=${mediaId}`);

wxReq.pipe(writer);
wxReq.on('end', () => {
  co(function* () {
    const result = yield ossClient.put(destPath, writer.toBuffer(), {timeout: 30 * 60 * 1000});
    console.log('图片上传阿里云结果', result);
    // res.status(200).json(result);
  }).catch(err => {
    console.warn(err);
    //res.status(500).send('上传文件出错');
  });
});

这种方式将图片暂存在内存里面,那如果并发量很大,是不是内存要爆炸了都?感觉还是不可取。

3)将下载图片的流直接写入OSS文件

折腾了好久,发现原来可以这么简单和优雅:

const request = require('require');
const OSS = require('ali-oss').Wrapper;

const ossClient = new OSS({
  accessKeyId: 'your access key',
  accessKeySecret: 'your access secret',
  bucket: 'your bucket name',
  region: 'oss-cn-hangzhou'
});

const accessToken = 'access token';
const mediaId = 'xxxxxxx'; // 微信多媒体文件id
const destPath = `weixin/images/201702/${mediaId}.jpg`; // OSS文件路径
const wxReq = request(`http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=${accessToken }&media_id=${mediaId}`);

wxReq.on('response', (response) => {
  // request的响应结果response可以作为读取流传给ossClient
  co(function* () {
    const result = yield ossClient.putStream(destPath, response, {timeout: 30 * 60 * 1000});
    console.log('图片上传阿里云结果', result);
    // res.status(200).json(result);
  }).catch(err => {
    console.warn(err);
    //res.status(500).send('上传文件出错');
  });
});

这种方式省去了前面两种方式的中间步骤,更加简练直接,个人认为是最好的。


naiba01
24 声望3 粉丝