开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情
最近在开发小程序的过程中,使用了 Vant Uploader 组件,同时使用 wx.request 批量上传图片。在此将趟过的过程做个记录
前期准备
- 微信小程序及工具
Wechat Devtools - Van-Uploader 组件文档 : https://vant-contrib.gitee.io/vant-weapp/#/uploader
MinIO图床,可参看文章使用Docker搭建MinIO私有图库并提供API调用: https://juejin.cn/post/7176544729130074172
相关内容简介
MinIO 图床相关
minIO 的搭建请参考文章:使用Docker搭建MinIO私有图库并提供API调用: https://juejin.cn/post/7176544729130074172。主要是通过 Java SDK 实现自定义的 API。方便用户使用图床进行图片的存储。
// 最终使用 API 请求上传图片,java 后端端口 40002
URL: http://xxx:40002/minio/upload
FormData: file=xxx
介绍 wx.request
文档地址:developers.weixin.qq.com/miniprogram…
功能描述:发起 HTTPS 网络请求。
示例代码 如下
从描述和代码上来看,接口比较简单。但是想要使用它上传图片的时候,我们经常使用的 FormData 发现不灵了,微信本身没有 FormData 对象,所以无法使用 new FormData()。
wx.request({
url: 'example.php', //仅为示例,并非真实的接口地址
data: {
x: '',
y: ''
},
header: {
'content-type': 'application/json' // 默认值
},
success (res) {
console.log(res.data)
}
})
van-uploader 组件
文档地址:vant-contrib.gitee.io/vant-weapp/…
功能描述:用于将本地的图片或文件上传至服务器,并在上传过程中展示预览图和上传进度。目前 Uploader 组件不包含将文件上传至服务器的接口逻辑,该步骤需要自行实现。
<!-- wxml 界面代码 -->
<van-uploader
file-list="{{ fileList }}"
accept="media"
use-before-read
mutiple="true"
bind:before-read="beforeRead"
bind:after-read="afterRead"
/>
js 代码如下,代码看起来,还是比较简单的:
Page({
data: {
fileList: [],
},
beforeRead(event) {
const { file, callback } = event.detail;
callback(file.type === 'image');
},
afterRead(event) {
// 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
const { file } = event.detail;
}
});
编写代码
代码思路
- 使用
MinIO搭建好图床之后,还不太够,需要生成Access Keys,并且使用代码中提供的后端代码,生成 jar 包并运行,确保可以使用api上传图片 - 微信小程序引入
vant-uploader组件,按照官方的推荐方式引入即可,官网地址: https://vant-contrib.gitee.io/vant-weapp/#/quickstart - 使用
wx.request + formData上传图片
部分代码实现
-
vant-uploader组件使用npm的方式引入# 在小程序路径 miniprogram 目录下运行 npm i @vant/weapp -S --production # 工具 -> 构建 npm # 等待完成即可 -
创建小程序页面
image,并在image.json中引入vant-uploader... "usingComponents": { ... "van-uploader": "@vant/weapp/uploader/index", "van-button": "@vant/weapp/button/index" } -
在
image.wxml页面添加vant组件<van-uploader accept="image" multiple="true" file-list="{{ photoList }}" bind:after-read="photoAfterRead" /> <!-- 也可以用其他的按钮 --> <van-button type="primary" block round bind:click="btnSaveClick">保存</van-button> -
在
image.js中完成图片的上传// pages/add/index.js import Dialog from '@vant/weapp/dialog/dialog'; const db = wx.cloud.database() const app = getApp() const FormData = require('../../utils/formData') Page({ /** * 页面的初始数据 */ data: { // 图片 photoList: [], }, photoAfterRead(event) { const _this = this const { file } = event.detail; let allPhoto = _this.data.photoList allPhoto = [...allPhoto, ...file] _this.setData({ photoList: allPhoto }) }, btnSaveClick() { const _this = this wx.showLoading({ title: '正在上传图片', mask: true, }) // 准备上传图片 _this.uploadToCloud(_this.data.photoList).then((allPhotoUrls) => { wx.hideLoading() }).catch((e) => { wx.hideLoading() wx.showToast({ title: '图片上传异常了' + JSON.stringify(e), icon: 'none' }); }) }, // 上传图片 uploadToCloud(fileList) { return new Promise((resolve, reject) => { if (!fileList.length) { wx.showToast({ title: '请选择图片', icon: 'none' }); reject(null) } else { const uploadTasks = fileList.map((file, index) => this.uploadFilePromise(`cook-photo-${new Date().getTime()}-${index}.png`, file)); Promise.all(uploadTasks) .then(data => { wx.showToast({ title: '上传成功', icon: 'none' }); resolve(data) }) .catch(e => { wx.showToast({ title: '上传失败', icon: 'none' }); console.log(e); reject(null) }); } }) }, uploadFilePromise(name, file) { return new Promise((resolve, reject) => { // ⚠️ 此处使用到了 FormData,微信小程序本身是不支持 FormData 的,需要我们自己定义 const formData = new FormData() const fileBuff = formData.getBuffByPath(file.url) formData.appendFile('file', fileBuff, file.url) const data = formData.getData() // 使用 wx.request,在体验版里打开调试模式就可以使用 http 请求完成上传 wx.request({ url: "http://ip:40002/minio/upload", header: { 'accept': 'application/json', 'content-type': data.contentType, }, method: 'POST', data: data.buffer, success(res) { if (res.data.success) { resolve(res.data.data.url) } else { reject('错误了') } }, fail(e) { Dialog.alert({ message: `请打开调试模式: ${JSON.stringify(e)}`, }).then(() => { reject('异常了') }) } }) }, ... }) -
定义
FormData,以支持上方请求// miniprogram/utils 目录下 // minprogram/utils/formData.js const mimeMap = require('./mimeMap.js') const fileManager = wx.getFileSystemManager() function FormData() { let data = {} let files = [] this.appendFile = (name, buffer, path) => { files.push({ name: name, buffer: buffer, fileName: getFileNameFromPath(path) }) return true } this.getData = () => convert(data, files) /** * 根据文件路径获取文件 buff * @param {*} filePath */ this.getBuffByPath = (filePath) => { return fileManager.readFileSync(filePath) } } function getFileNameFromPath(path) { let idx = path.lastIndexOf('/') return path.substr(idx + 1) } function convert(data, files) { let boundaryKey = 'wxmpFormBoundary' + randString() // 数据分割符,一般是随机的字符串 let boundary = '--' + boundaryKey let endBoundary = boundary + '--' let postArray = [] //拼接参数 if (data && Object.prototype.toString.call(data) == '[object Object]') { for (let key in data) { postArray = postArray.concat(formDataArray(boundary, key, data[key])) } } //拼接文件 if (files && Object.prototype.toString.call(files) == '[object Array]') { for (let i in files) { let file = files[i] postArray = postArray.concat( formDataArray(boundary, file.name, file.buffer, file.fileName) ) } } //结尾 let endBoundaryArray = [] for (var i = 0; i < endBoundary.length; i++) { // 最后取出结束boundary的charCode endBoundaryArray.push(...endBoundary.utf8CodeAt(i)) } postArray = postArray.concat(endBoundaryArray) return { contentType: 'multipart/form-data; boundary=' + boundaryKey, buffer: new Uint8Array(postArray).buffer } } function randString() { let res = '' for (let i = 0; i < 17; i++) { let n = parseInt(Math.random() * 62) if (n <= 9) { res += n } else if (n <= 35) { res += String.fromCharCode(n + 55) } else { res += String.fromCharCode(n + 61) } } return res } function formDataArray(boundary, name, value, fileName) { let dataString = '' let isFile = !!fileName dataString += boundary + '\r\n' dataString += 'Content-Disposition: form-data; name="' + name + '"' if (isFile) { dataString += '; filename="' + fileName + '"' + '\r\n' // 此处可以根据不同的文件名称来配置不同的 mime,例如 image/jpeg 等 dataString += 'Content-Type: application/octet-stream\r\n\r\n' } else { dataString += '\r\n\r\n' dataString += value } var dataArray = [] for (var i = 0; i < dataString.length; i++) { // 取出文本的charCode(10进制) dataArray.push(...dataString.utf8CodeAt(i)) } if (isFile) { let fileArray = new Uint8Array(value) dataArray = dataArray.concat(Array.prototype.slice.call(fileArray)) } dataArray.push(...'\r'.utf8CodeAt()) dataArray.push(...'\n'.utf8CodeAt()) return dataArray } String.prototype.utf8CodeAt = function (i) { var str = this var out = [], p = 0 var c = str.charCodeAt(i) if (c < 128) { out[p++] = c } else if (c < 2048) { out[p++] = (c >> 6) | 192 out[p++] = (c & 63) | 128 } else if ( (c & 0xfc00) == 0xd800 && i + 1 < str.length && (str.charCodeAt(i + 1) & 0xfc00) == 0xdc00 ) { // Surrogate Pair c = 0x10000 + ((c & 0x03ff) << 10) + (str.charCodeAt(++i) & 0x03ff) out[p++] = (c >> 18) | 240 out[p++] = ((c >> 12) & 63) | 128 out[p++] = ((c >> 6) & 63) | 128 out[p++] = (c & 63) | 128 } else { out[p++] = (c >> 12) | 224 out[p++] = ((c >> 6) & 63) | 128 out[p++] = (c & 63) | 128 } return out } module.exports = FormData
注意事项
- 使用
http的时候,需要在详情 -> 本地设置 -> 勾选 不校验合法域名...进行开发 - 提交为体验版后,需要在手机上点击右上角 ··· ,打开调试模式,即可享用~