美女图片预警,请注意流量!!!
本文内容:
- 前端上传
- 直接上传
- 自定义名字上传
- 手动上传多张图片(axios)
- 上传前过滤
- 图片上传到服务器(预览)
- 服务器端上传到七牛云
- 小程序图片上传
- 基本api上传
- 云存储
文章全部示例代码地址:2019-0907-qiniu-upload
最近承包了一个小程序的前后端,具备小程序登录、微信支付(欲哭无泪的支付)、后台增加商品、管理订单、编辑页面等功能,发现了不少问题,于是走上了写文章的道路,请大家海涵我这个小菜...
在以前的项目,文件一般都是直接放在服务器某个文件夹里面的,对于特定区域的to B业务当然是没问题的,但是不适合小程序这种to C业务的场景。如下
- 小程序有大小限制,图片等放在源代码文件里体积太大
- 天南地北,服务器访问图片的速度不一样,影响客户体验
- 服务器出现问题,数据丢失导致时间、运维等成本提升
于是决定把文件、图片放在CDN,琢磨七牛的SDK和Upload组件花费了不少时间,踩了很多坑。诸多对象存储都是key-value的存储方式,key是唯一的键值,几乎都是token+配置参数的方式,上传方式大同小异,希望下面总结对你有所帮助。
1.前端上传
七牛云上传文件可以简单地分为两种方式:
- 前端从业务服务器获取上传凭证(token),前端直接把文件传到七牛云存储
- 业务服务器直接把文件上传到七牛云存储,比如日志、数据库备份,一般都是定时并加密的
1.1直接上传
实现简单的文件上传到七牛其实很简单,一个前端er只需要一个上传组件然后让后端提供一个token请求。而后端只需要安装七牛官方的Node.js SDK: npm install qiniu,生成token的函数是:
function getQiniuToken() {
const mac = new qiniu.auth.digest.Mac(qiniuConfig.AK, qiniuConfig.SK)
const options = {
scope: qiniuConfig.bucket,
expires: 7200
}
const putPolicy = new qiniu.rs.PutPolicy(options)
const uploadToken = putPolicy.uploadToken(mac)
return uploadToken;
}
qiuniuConfig是我在服务端配置里的对象,用来保存AccessKey、SecretKey、存储区域、存储区域、加速域名等信息,AK/SK对安全极为重要,官方文档建议不要放在客户端和网络中明文传送。我的前端使用了iView的Upload组件:
<template>
<div class="home">
<div class="directly-upload">
<Upload
ref="upload"
multiple
type="drag"
:before-upload="handleBeforeUpload"
:on-success="handleSuccess"
:format="['jpg', 'jpeg', 'png']"
:max-size="4096"
:data="{token: qiniuToken}"
:action="postURL">
<div style="padding: 20px 0">
<Icon type="ios-cloud-upload" size="52" style="color: #3399ff"></Icon>
<p>点击或者拖拽文件到此处上传</p>
</div>
</Upload>
</div>
</div>
</template>
<script>
export default {
name: 'home',
data() {
return {
postURL: 'http://upload-z2.qiniup.com',
qiniuToken: '',
};
},
components: {
},
methods: {
async handleBeforeUpload() {
await this.$http.get('/qiniu/token').then((res) => {
console.log(res.data.token);
this.qiniuToken = res.data.token.trim();
})
.catch((err) => {
this.$Notice.error({
title: '错误',
desc: '上传失败',
});
});
},
handleSuccess(res) {
console.log(res);
},
},
};
</script>
稍微了解一下Upload组件API的作用:
- action: 上传的地址,必填
- data: 上传时附带的额外参数,这些参数会放在FormData里面
- before-upload: 上传文件之前的钩子,参数为上传的文件,若返回 false 或者 Promise 则停止上传
- on-success: 文件上传成功时的钩子,返回字段为 response, file, fileList
- format: 支持的文件类型
- max-size: 文件大小限制,单位 kb
使用代码的时候需要自行修改action绑定的地址,upload-z2.qiniup.com 只是华南区空间的地址,可以在这里查看上传地址:存储区域 。
我直传了一张小乔丁香结图片,控制台输出上传成功的参数:
{
hash: "FrAvE78xCroRtgdIdM5u3ajlcLUL",
key: "FrAvE78xCroRtgdIdM5u3ajlcLUL"
}
加速域名拼接上返回参数的key就可以访问图片了,是不是很简单!
qiniu.hackslog.cn/FrAvE78xCro…
1.2自定义名字上传
自定义图片的名字在后端生成token的代码options选项 中添加要重置的名字。options的配置很灵活,可以指定callbackUrl地址,比如前端上传图片成功后让七牛把结果传到后端某个接口,后端利用Redis记录的key把相关内容持久化到数据库;可以指定返回的参数returnBody,hash值、key、文件大小、所在空间等等。
var options = {
//其他上传策略参数...
returnBody: '{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)","age":$(x:age)}'
}
生成token的代码:
// 获取上传Token并且能指定文件名字
function getQiniuTokenWithName(nameReWrite) {
const mac = new qiniu.auth.digest.Mac(qiniuConfig.AK, qiniuConfig.SK)
const options = {
scope: qiniuConfig.bucket + ":" + nameReWrite,
expires: 7200
}
const putPolicy = new qiniu.rs.PutPolicy(options)
const uploadToken = putPolicy.uploadToken(mac)
return uploadToken
}
知道了这些,你还不定能够成功上传,我第一次尝试就是上传失败了,那时也是忘了查看Chrome调试Network的Response,然后直接向七牛发了工单,他们工程师一上班就回复我了:
<template>
<div class="with-name">
<div class="directly-upload">
<Upload
ref="upload"
multiple
type="drag"
:before-upload="handleBeforeUpload"
:on-success="handleSuccess"
:format="['jpg', 'jpeg', 'png', 'gif']"
:max-size="4096"
:data="{token: qiniuToken, key: keyName}"
:action="postURL">
<div style="padding: 20px 0">
<Icon type="ios-cloud-upload" size="52" style="color: #3399ff"></Icon>
<p>点击或者拖拽文件到此处上传</p>
</div>
</Upload>
</div>
</div>
</template>
<script>
export default {
name: 'post-name',
data() {
return {
postURL: 'http://upload-z2.qiniup.com',
qiniuToken: '',
keyName: 'dingxiangjie.jpg',
perfix: 'blog', // 配置路径前缀
};
},
methods: {
async handleBeforeUpload(file) {
console.log(file);
const suffixList = file.name.split('.');
const baseName = suffixList[0];
const suffix = suffixList[1]; // 获取文件后缀,比如.jpg,.png,.gif
const fileType = file.type;
const timeStamp = new Date().getTime(); // 获取当前时间戳
const newName = `${this.perfix}/${timeStamp}_${baseName}.${suffix}`; // 新建文件名
const newfile = new File([file], newName, { type: fileType });
this.keyName = newName;
console.log(newfile);
await this.$http.get('/qiniu/token/name', {
params: {
name: newName,
},
}).then((res) => {
console.log(res.data.token);
this.qiniuToken = res.data.token.trim();
})
.catch((err) => {
console.log(err);
this.$Notice.error({
title: '错误',
desc: '上传失败',
});
});
console.log('我等到了...');
},
handleSuccess(res) {
console.log(res);
},
},
};
</script>
我又折腾了一下,想在访问的文件路径加个前缀,比如http://qiniu.hackslog.cn/blog ,按照喜好可以有/code ,/product ,/gif 等等,结果嘛,就是不行。原因是生成的token和上传文件的name属性必须一一对应的,也就是说加了前缀比如是blog,那我上传的文件名也得是blog/xxx.jpg ,而前端选中的文件是只读属性,利用它的信息new File一个新的文件上传就可以解决问题了。
有趣的是,如果上传相同文件名,文件会被覆盖,如果源文件被删除,你可能仍然看到该链接。我猜部分节点数据并没有马上被删除,所以访问到副本的内容。比如qiniu.hackslog.cn/dingxiangji… 这个链接,我在北京看到的是小乔,重庆、深圳的朋友看到的却是公孙离,而实际上我空间上已经把这个图片删除了呢。
公孙离文件改名字后的上传
qiniu.hackslog.cn/15680735503…
1.3 手动上传多张图片
多文件上传,目标要实现的是这样的。
代码如下:
<template>
<div class="form-data">
<div class="directly-upload">
<Upload
ref="upload"
multiple
type="drag"
:before-upload="handleBeforeUpload"
:on-success="handleSuccess"
:format="['jpg', 'jpeg', 'png', 'gif']"
:max-size="4096"
:data="{ token: qiniuToken, key: qiniuKey }"
:action="postURL">
<div style="padding: 20px 0">
<Icon type="ios-cloud-upload" size="52" style="color: #3399ff"></Icon>
<p>点击或者拖拽文件到此处上传</p>
</div>
</Upload>
<Button type="success" style="float: right;" @click="handleUpload">确定上传</Button>
</div>
</div>
</template>
<script>
export default {
name: 'form-date',
data() {
return {
postURL: 'http://upload-z2.qiniup.com',
baseURL: 'http://qiniu.hackslog.cn/',
qiniuToken: '',
qiniuKey: '',
perfix: 'blog',
keyList: [], // 存放上传的名字
imageList: [], // 存放上传信息,
uploadFile: [],
};
},
methods: {
handleBeforeUpload(file) {
const suffixList = file.name.split('.');
const baseName = suffixList[0]; // 源文件的名字
const suffix = suffixList[1]; // 文件后缀
const fileType = file.type; // 这个类似image/png之类,用于创建文件
const timeStamp = new Date().getTime(); // 当前的时间戳
const newName = `${this.perfix}/${timeStamp}_${baseName}.${suffix}`; // 新文件名
const newFile = new File([file], newName, { type: fileType });
const postItem = {
file: newFile,
key: newName,
token: '',
};
this.keyList.push(newName); // 把新建的文件名放到数组,传到后端拿到对应的Token
this.imageList.push(postItem); // 包含文件信息的对象
this.uploadFile.push(newFile); // 其实不用这个也行,this.imageList就包含了
return false;
},
async handleUpload() {
if (this.keyList.length === 0) {
this.$Notice.warning({
title: '当前没有可上传的文件!',
});
return;
}
const list = encodeURIComponent(JSON.stringify(this.keyList));
await this.$http.get('/qiniu/token/list', {
params: {
list,
},
}).then((res) => { // 获取文件对应的token
const resultList = res.data.tokenList;
resultList.forEach((item, index) => {
this.imageList[index].token = item;
});
})
.catch(() => {
this.$Notice.error({
title: '错误',
desc: '获取Token失败',
});
});
console.log(this.imageList);
this.imageList.forEach(async (item, index) => {
this.qiniuToken = item.token;
this.qiniuKey = item.key;
await this.$nextTick(async () => {
console.log(this.qiniuToken);
console.log(this.uploadFile[index]); // 调用组件内部的直接上传逻辑
await this.$refs.upload.post(this.uploadFile[index]);
});
});
},
handleSuccess(res) {
console.log(res);
this.$Notice.success({
title: `文件${this.baseURL}${res.key}已经可以访问!`,
});
},
},
};
</script>
但是很遗憾iView的Upload组件貌似无法做到这样的效果,以上代码存在问题 ! 如果你使用的Token是相同的并且使用列表循环方式顺序地控制上传(正确的多文件上传代码 ),可以模拟多文件上传效果的,可是它的data不能绑定多个不同的信息。我测试的情况是:把文件名(key值)作为数组传递到后端生成对应的token数组同样返回到前端,前端遍历包含token的数组,同时把token、key放到Upload组件props的data里,调用组件里的post方法上传(iView Upload组件源码第207行 ),打开控制台的Network你就会发现file、token和key都以FormData的方式上传了,只可惜Upload组件没有对data做相应的监听,数据还不能实时更新,导致FormData传递的key和token不是一一对应的,因此403报错。
改变一下方式,我们使用原始的input来实现多文件上传吧,逻辑控制也不复杂。于是axios批量上传文件,不用第三方UI组件的写法变成了(原谅我偷偷用个Button):
<template>
<div class="multiple">
<div class="directly-upload">
<input
ref="upload"
@change="handleChange"
type="file"
name="file"
accept="image/*"
multiple>
<Button type="success" style="float: right;" @click="handleUpload">确定上传</Button>
</div>
</div>
</template>
<script>
import axios from 'axios';
import qs from 'qs';
export default {
name: 'multiple',
data() {
return {
postURL: 'http://upload-z2.qiniup.com',
baseURL: 'http://qiniu.hackslog.cn/',
perfix: 'blog',
keyList: [], // 存放上传的名字
imageList: [], // 存放上传信息
};
},
methods: {
handleChange(e) { // 选择文件后将文件交给handleBeforeUpload进行处理
const { files } = e.target;
if (!files) {
return;
}
Array.from(files).forEach((file) => {
this.handleBeforeUpload(file);
});
},
handleBeforeUpload(file) { // 将文件名变成:前缀/时间戳_原文件名.后缀
const suffixList = file.name.split('.');
const baseName = suffixList[0];
const suffix = suffixList[1];
const fileType = file.type;
const timeStamp = new Date().getTime();
const newName = `${this.perfix}/${timeStamp}_${baseName}.${suffix}`;
const newFile = new File([file], newName, { type: fileType });
const postItem = {
file: newFile,
key: newName,
token: '',
};
this.keyList.push(newName);
this.imageList.push(postItem);
return false;
},
async handleUpload() { // 执行真正的上传逻辑
if (this.keyList.length === 0) {
this.$Notice.warning({
title: '当前没有可上传的文件!',
});
return;
}
// 去后台请求每个文件名对应的token
// const list = encodeURIComponent(JSON.stringify(this.keyList));
await this.$http.get('/qiniu/token/list', {
params: {
list: this.keyList,
},
paramsSerializer(params) {
return qs.stringify(params, { arrayFormat: 'repeat' });
},
}).then((res) => { // 保存token信息
const resultList = res.data.tokenList;
resultList.forEach((item, index) => {
console.log(res.data.keyList[index]);
this.imageList[index].token = item;
});
})
.catch(() => {
this.$Notice.error({
title: '错误',
desc: '获取Token失败',
});
});
const queue = [];
// 遍历,执行上传
this.imageList.forEach((item) => {
queue.push(this.postToQiniu(item));
});
this.$refs.upload.value = null;
},
postToQiniu(data) {
const that = this;
const formData = new FormData();
formData.append('file', data.file);
formData.append('key', data.key);
formData.append('token', data.token);
axios({
method: 'POST',
url: this.postURL,
data: formData,
})
.then((res) => {
const fileLink = `${that.baseURL}${res.data.key}`;
this.$Notice.success({
title: '上传成功!',
desc: fileLink,
});
})
.catch((err) => {
console.log(err);
this.$Notice.error({
title: '上传失败!',
desc: data.key,
});
});
},
},
};
</script>
写出上面的代码之前,还是403错误,看看Response报的错:
这个问题困惑了很久,我一直在想axios的设置是不是有啥问题,首先是headers,如果数据是FormData,请求头是自动设置multipart/form-data的,错误的请求带的数据也没问题,file、token都对应...
问题的根源是什么呢?我原本以为我生成的token对应着每个文件名,事实上是错误的。问题出在axios传递数组,我将他们encode了一下(上面被注释了)传递以免在get请求中出现错误(因为get请求不能直接携带/ [ ] =之类的字符)。
const list = encodeURIComponent(JSON.stringify(this.keyList));
然而后端拿到的数据我以为这样处理就万事大吉了:
const decodeString = decodeURIComponent(ctx.query.list)
const keyList = decodeString.split(',');
实际上里面keyList里面的值被改变了,没有察觉到!反正是axios的锅,为什么没有直接就把params自动encode,比如变成list?list=xxx&&list=xxx,后端拿到直接ctx.query.list就不会错了呀。看看下面后端处理list的结果,太难看了!
[ '["blog/1568196438856_flower.jpg"',
'"blog/1568196438857_snow-street.jpg"]' ]
细心的同学发现我上面iView手动上传的代码有错了(我并没偷偷改过来),不过这样的错误并不证明iView支持多文件多信息的上传,因为它的问题出在多信息无法实时更新。于是axios传递数组采用了这样的方式:
import qs from 'qs';
this.$http.get('/qiniu/token/list', {
params: {
list: this.keyList,
},
paramsSerializer(params) {
return qs.stringify(params, { arrayFormat: 'repeat' });
},
})
结果嘛...当然能上传的!不过后端还是出了一点点问题,比如只传一张图片的时候ctx.query.lis 拿到的是字符串而不是数组,我用instanceof来过滤了一下就好了。前端代码 && 后端代码
1.4 上传前的过滤
上传过滤一般限制大小,Upload组件已经提供了on-exceeded-sizeAPI来实现了。我遇到的需求是:上传前限制上传的尺寸,具体场景是小程序的轮播图、商品详情图,如果尺寸不一致很不美观。
handleUpload(file){
this.uploadForm = {
stationId: this.currentStation
}
return this.checkImageWH(file, 1080, 900);
},
checkImageWH(file, width, height) {
let self = this;
return new Promise(function (resolve, reject) {
let filereader = new FileReader();
filereader.onload = e => {
let src = e.target.result;
const image = new Image();
image.onload = function () {
if (width && this.width < width) {
self.$Message.error(`请上传宽大于${width}的图片`);
reject();
} else if (height && this.height < height) {
self.$Message.error(`请上传高大于${height}的图片`);
reject();
} else {
resolve();
}
};
image.onerror = reject;
image.src = src;
};
filereader.readAsDataURL(file);
});
},
2.直接上传到生产服务器(前端可预览)
后端使用Koa,解析请求的时候我们一般用的是koa-bodyparser ,它支持json、表单、文本类型的格式数据,但是不支持form-data(文件上传),改用koa-body 替代
const Koa = require('koa')
const koaBody = require('koa-body')
const koaStatic = require('koa-static')
const fs = require('fs')
const path = require('path')
const app = new Koa()
const cors = require('koa2-cors')
const routing = require('./routes')
// 生成年月日
function yearMonthDay() {
const today = new Date()
const year = today.getFullYear()
const month = today.getMonth() + 1
const day = today.getDate()
return `${year}${suppleZero(month)}${suppleZero(day)}`;
}
app.use(cors()) // 解决跨域请求问题
// 打印请求日志
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method}${ctx.url}-${ms}ms`)
})
app.use(koaStatic(path.join(__dirname, 'public')))
app.use(koaBody({
multipart: true, // 指的是multipart/form-data,支持文件上传
formidable: {
maxFieldsSize: 4 * 1024 * 1024, // 限制上传文件的体积
uploadDir: path.join(__dirname, '/public/uploads'), // 上传到的文件路径
keepExtensions: true, // 保留文件后缀
onFileBegin(name, file){
const originalFileName = file.name // 获取上传文件的原名字
const ext = path.extname(originalFileName) // node自带的可以获取文件后缀名的方法,它是带.的
const timeStamp = new Date().getTime()
const randomNum = Math.floor(Math.random()*10000 + 1)
const fileName = `${timeStamp}${randomNum}${ext}` // 文件名是时间戳+10000以内的随机数,我就不信会重名
const dir = path.join(__dirname, `/public/uploads/${yearMonthDay()}`) // 按期日创建文件夹
if(!fs.existsSync(dir)){
fs.mkdirSync(dir)
}
file.path = `${dir}/${fileName}` // 完成文件名的修改
},
},
}))
routing(app);
app.listen(5000, () => console.log('服务启动于5000端口...'))
koa-static可以给我设定可以供外界访问的静态资源。koa-body可以自定将上传的文件做一些转换,默认情况下上传的文件会被重命名,通过fromidable可以自定义文件名,比如我自定义成当前时间戳+10000以内的随机数
其实如果是手动上传,我们还是可以用iView的,只需要返回before-upload的值为false或者Promise,再用一个按钮调用axios上传的逻辑。在iView文档自定义上传列表中的示例,可以看出我们可以利用this.$refs.upload.fileList 来保存文件信息,也可以实现预览、删除等功能。相对elementUI而言,iView Upload组件的示例有一定局限性,因此我自己写样式展示文件预览:
<template>
<div class="server-upload">
<div class="upload-box">
<div class="demo-upload-list" v-for="(item, index) in imageList" :key="item">
<img :src="item">
<div class="demo-upload-list-cover">
<Icon type="ios-eye-outline" @click.native="handleView(item)"></Icon>
<Icon type="ios-trash-outline" @click.native="handleRemove(index)"></Icon>
</div>
</div>
<Upload
ref="upload"
:show-upload-list="false"
:on-success="handleSuccess"
:format="['jpg','jpeg','png', 'gif']"
:max-size="4096"
:on-exceeded-size="handleMaxSize"
:before-upload="handleBeforeUpload"
multiple
type="drag"
:action="postURL"
style="display: inline-block;width:180px;">
<div style="width: 180px;height:180px;line-height: 180px;">
<Icon type="ios-camera" size="20"></Icon>
</div>
</Upload>
<div class="upload-submit-btn">
<Button type="success" @click="confirmUpload">确认上传</Button>
</div>
</div>
<Modal title="查看图片" v-model="visible">
<img :src="imgURL" v-if="visible" style="width: 100%">
</Modal>
</div>
</template>
<script>
export default {
name: 'server',
data() {
return {
postURL: 'http://localhost:5000/api/qiniu/directly',
imgBaseURL: 'http://localhost:5000/uploads/',
uploadList: [],
imageList: [],
visible: false,
imgURL: '',
};
},
methods: {
// 上传之前检测是否超出限制,使用FileReader获取本地图片用来展示
handleBeforeUpload(file) {
const check = this.uploadList.length < 5;
if (!check) {
this.$Notice.warning({
title: '上传的文件不得超过5张.',
});
return false;
}
let exist = false;
this.uploadList.forEach((item) => {
if (item.name === file.name) {
exist = true;
}
});
if (exist) {
this.$Notice.error({
title: '文件重复了!',
});
return false;
}
this.uploadList.push(file);
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = () => {
this.imageList.push(reader.result);
};
return false;
},
handleMaxSize(file) {
this.$Notice.warning({
title: '文件大小限制',
desc: `文件${file.name}太大了,单个文件不允许超过4M`,
});
},
handleView(url) {
this.imgURL = url;
this.visible = true;
},
// 移除某一张图片
handleRemove(index) {
this.uploadList.splice(index, 1);
this.imageList.splice(index, 1);
this.$Notice.info({
title: '移除一张图片',
});
},
// 确定上传
confirmUpload() {
if (this.uploadList.length === 0) {
this.$Notice.error({
title: '还没有可上传的文件!',
});
return;
}
this.uploadList.forEach((file) => {
this.$refs.upload.post(file);
});
this.$Notice.success({
title: '上传成功!',
});
this.imageList.splice(0, this.imageList.length);
this.uploadList.splice(0, this.uploadList.length);
},
handleSuccess(res) {
console.log(res.url);
},
},
};
</script>
看了上传的演示,读者是否发现代码存在一个bug?我删除的一张图片却出现到了成功上传的文件夹里,嫦娥这张图片却没有上传!导致这个问题的原因是FileReader读取文件是一个异步操作,使用它就无法保证上传的文件列表和展示图片都是同一个数组下标,在workers里可以使用FileReaderSync以同步的方式读取File或者Blob对象中的内容,只是主线程里进行同步I/O操作可能会阻塞用户界面,可以考虑用URL.createObjectURL()。
this.imageList.push(window.URL.createObjectURL(file));
3.服务器端上传到七牛云
服务器端上传到七牛仅指服务器有这样的需求才执行的代码,不建议从前端上传到服务器的临时文件夹然后再传输到七牛云存储,读写文件的操作确实有点耗时。如果你非要那么做,可以用前面的后端路由代码结合下面的示例来实现。
首先服务器读取特定文件夹的文件,获取文件名去生成token,然后调用Node.js SDK的表单上传组件。前端上传时需要指定上传域名,后端上传的时候变成了配置参数,我的bucket属于华南区,配置就是qiniu.zone.Zone_z2 。上传完毕之后把相应的文件删除掉,可以节省空间。读写、删除的操作最好加个try/catch防止异常。
// 读取某个目录下的所有文件,不包含递归方式,也没有判断路径不存在的时候
function realLocalFiles(dir) {
const fileList = []
const files = fs.readdirSync(dir)
files.forEach(file => {
console.log(file)
fileList.push(file)
})
return fileList
}
// 服务器端上传到七牛
function formUploadPut(fileName, file) {
const uploadToken = getQiniuTokenWithName(fileName) // 生成token这个方法调用了好多次
const config = new qiniu.conf.Config()
config.zone = qiniu.zone.Zone_z2 // 根据你的上传空间选择zone对象
const formUploader = new qiniu.form_up.FormUploader(config)
const putExtra = new qiniu.form_up.PutExtra()
return new Promise((resolve, reject) => {
formUploader.putFile(uploadToken, fileName, file, putExtra, (respErr,
respBody, respInfo) => {
if (respErr) {
reject(respErr)
}
if (respInfo.statusCode == 200) {
resolve(respBody)
} else {
console.log(respInfo.statusCode);
resolve(respBody)
}
})
})
}
// 由服务器上传到七牛云,这是路由
qiniuRouter.get('/serversidepost', async(ctx, next) => {
const currentPath = '../public/uploads/' + yearMonthDay()
const fileFoldPath = path.resolve(__dirname, currentPath) // 图片所在的路径
const fileList = realLocalFiles(fileFoldPath)
fileList.forEach(async(file) => {
const filePath = fileFoldPath +'/' + file // 实现上传的函数只须文件名和文件路径
const resInfo = await formUploadPut(file, filePath)
console.log(resInfo)
const fileLink = qiniuConfig.domain + '/' + file
console.log(fileLink)
removeOnefile(filePath)
})
ctx.body = {
fileList
}
})
// 删除已经上传了的图片
function removeOnefile(path) {
fs.unlink(path, (err) => {
if (err) {
throw err
}
console.log('文件删除成功')
})
}
因为没有前端上传的参与,读者使用我的代码可以使用http://localhost:8080/server 先上传几张图片到某个文件夹,比如是20190913,然后在访问服务器请求地址: http://localhost:5000/api/qiniu/serversidepost 它将返回图片的文件名列表,并且完成上传到七牛云、删除本地文件的操作。命令行输出如下:
fs.unlink 是异步操作,需要等上传完毕再删除。然后你们可以欣赏一下紫霞仙子小姐姐的风韵~~~
qiniu.hackslog.cn/15683981411…
4.小程序上传文件
4.1 直接上传
小程序上传文件的场景比如在政府服务的应用上传身份证、一些电商应用发布图片等。目前普遍使用了别人封装好的轮子:qiniu-wxapp-sdk ,如果自己要实现的话,主要用到wx.request来获取token,wx.chooseImage选择文件,wx.uploadFile 上传文件,因为小程序文件是有个临时路径的,我就不玩自定义文件名了,读者可以自己玩(๑′ᴗ‵๑)
想象一下点击选择文件后wx.chooseImage返回临时文件列表,当点击上传按钮之后应用从服务器中拿到token,然后在使用wx.uploadFile上传。wx.request是个异步操作,让他们顺序执行可以把处理逻辑放在回调中,更好的方式是使用async/await,不过小程序不是天然支持,可以引入第三方的包,比如facebook/regenerator ,我们只需要用runtime.js这个文件。不过我暂且用onLoad获取共用的token
onLoad() {
this.fetchToken()
},
async fetchToken() {
const res = await http('GET', tokenURL, {})
const token = res.data.token
console.log(token)
this.setData({
token
})
},
http(method, url, data) {
return new Promise((resolve, reject) => {
wx.request({
url,
data,
method,
success(res) {
resolve(res)
},
fail() {
reject({
msg: `${url}请求错误`
})
}
})
})
},
// 七牛上传逻辑
qiniuUpload() {
const that = this
if (content.trim() === '') {
wx.showModal({
title: '请输入内容',
content: '',
})
return
}
wx.showLoading({
title: '发布中',
mask: true,
})
let promiseArr = []
let fileList = [] // 记录上传成功后的图片地址
for (let i = 0, len = this.data.images.length; i < len; i++) {
let p = new Promise((resolve, reject) => {
let item = this.data.images[i]
wx.uploadFile({
url: qiniuPostURL, // 华南区的是http://upload-z2.qiniup.com
filePath: item,
name: 'file',
formData: {
token: this.data.token
},
success(res) {
const data = JSON.parse(res.data) // 这是重点!!!
console.log(data)
const key = data['key']
console.log(key)
console.log(accessURL) // 这是加速域名,我的是http://qiniu.hackslog.cn
const link = accessURL + '/' + key
console.log(link)
fileList = fileList.concat(link)
resolve()
},
fail: (err) => {
console.error(err)
reject()
}
})
})
promiseArr.push(p)
}
Promise.all(promiseArr).then((res) => {
console.log(res)
console.log(fileList)
app.globalData.imgList = fileList
wx.hideLoading()
wx.showToast({
title: '发布成功',
icon: 'success',
duration: 2000
})
console.log('上传完毕了')
wx.navigateTo({
url: '/pages/display/index',
})
}).catch((err) => {
wx.hideLoading()
wx.showToast({
title: '发布失败',
})
})
},
我又处于调bug状态了...调用wx.uploadFile出现了个错误:name属性不是随便调的,因为内部调用的是FormData, name其实是指定文件所属的键名,七牛上传指定的就是file;success的回调拿不到res.data.key,在axios里面是自动转换的,小程序里还需要自己JSON.parse()一下。
展示下成果:
4.2 云存储
小程序云开发在经过逐步完善,已经开始支持HTTP API的操作了,也就是说可以实现不购置服务器的情况下完成小程序和Web管理控制台,非常有吸引力。云储存 提供了很方便的API让小程序直接调用函数上传图片,还可以在云开发控制台手动上传,本质来说也是一个对象存储(多副本)。与之配合的有云函数可以完成一些数据处理工作、数据库有很多貌似MongoDB的API,前端的工作越来越重了/(ㄒoㄒ)/~~
这里引用谢成老师 的代码,模拟发布朋友圈的过程,实现主要使用了wx.chooseImage 和wx.cloud.uploadFile 两个API。手动上传过程可能会增加、删除部分图片,需要使用数组暂存数据;真机使用时聚焦会抬起键盘,需要把下面的发布按钮的定位调整一下;wx.cloud.uploadFile上传参数filePath字段可以通过wx.chooseImage获取,cloudPath设置存储路径,可以直接字段拼接不用再自己new File了,填了前缀会在存储控制台多出一个文件夹;等待每个文件都上传完之后再跳转到展示页面,可以用Promise.all()方法。
// index.wxml
<view class="container">
<textarea class="content" placeholder="这一刻的想法..."
bindinput="onInput" auto-focus
bindfocus="onFocus" bindblur="onBlur"
></textarea>
<view class="image-list">
<!-- 显示图片 -->
<block wx:for="{{images}}" wx:key="*this">
<view class="image-wrap">
<image class="image" src="{{item}}" mode="aspectFill" bind:tap="onPreviewImage" data-imgsrc="{{item}}"></image>
<i class="iconfont icon-chuyidong" bind:tap="onDelImage" data-index="{{index}}"></i>
</view>
</block>
<!-- 选择图片 -->
<view class="image-wrap selectphoto" hidden="{{!selectPhoto}}" bind:tap="onChooseImage">
<i class="iconfont icon-addTodo-nav"></i>
</view>
</view>
</view>
<view class="footer" style="bottom:{{footerBottom}}px">
<text class="words-num">{{wordsNum}}</text>
<button class="send-btn" bind:tap="upload">发布</button>
</view>
// index.js
var app = getApp();
// 最大上传图片数量
const MAX_IMG_NUM = 9
// 输入的文字内容
let content = ''
let userInfo = {}
Page({
/**
* 页面的初始数据
*/
data: {
// 输入的文字个数
wordsNum: 0,
footerBottom: 0,
images: [],
selectPhoto: true, // 添加图片元素是否显示
imagePaths: []
},
// 文字输入
onInput(event) {
let wordsNum = event.detail.value.length
this.setData({
wordsNum
})
content = event.detail.value
},
onFocus(event) {
// 模拟器获取的键盘高度为0
// console.log(event)
this.setData({
footerBottom: event.detail.height,
})
},
onBlur() {
this.setData({
footerBottom: 0,
})
},
onChooseImage() {
// 还能再选几张图片
let max = MAX_IMG_NUM - this.data.images.length
wx.chooseImage({
count: max,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
console.log(res)
this.setData({
images: this.data.images.concat(res.tempFilePaths)
})
// 还能再选几张图片
max = MAX_IMG_NUM - this.data.images.length
this.setData({
selectPhoto: max <= 0 ? false : true
})
},
})
},
// 删除图片
onDelImage(event) {
this.data.images.splice(event.target.dataset.index, 1)
this.setData({
images: this.data.images
})
if (this.data.images.length == MAX_IMG_NUM - 1) {
this.setData({
selectPhoto: true,
})
}
},
onPreviewImage(event) {
// 6/9
wx.previewImage({
urls: this.data.images,
current: event.target.dataset.imgsrc,
})
},
upload() {
const that = this
if (content.trim() === '') {
wx.showModal({
title: '请输入内容',
content: '',
})
return
}
wx.showLoading({
title: '发布中',
mask: true,
})
let promiseArr = []
let fileIds = []
// 图片上传
for (let i = 0, len = this.data.images.length; i < len; i++) {
let p = new Promise((resolve, reject) => {
let item = this.data.images[i]
// 文件扩展名
let suffix = /\.\w+$/.exec(item)[0]
wx.cloud.uploadFile({
cloudPath: 'code/' + Date.now() + '-' + Math.random() * 1000000 + suffix,
filePath: item,
success: (res) => {
console.log(res.fileID)
fileIds = fileIds.concat(res.fileID)
resolve()
},
fail: (err) => {
console.error(err)
reject()
}
})
})
promiseArr.push(p)
}
// 全部上传完之后跳转到展示页面
Promise.all(promiseArr).then((res) => {
console.log(res)
console.log(fileIds)
app.globalData.imgList = fileIds
wx.hideLoading()
wx.showToast({
title: '发布成功',
icon: 'success',
duration: 2000
})
console.log('上传完毕了')
wx.navigateTo({
url: '/pages/display/index',
})
}).catch((err) => {
wx.hideLoading()
wx.showToast({
title: '发布失败',
})
})
},
})
嘿嘿,完结了!
参考链接:
File对象, FileList对象,FileReader对象
图片上传的需求各种各样,这里只介绍了基本的上传方式,可以预见的需求比如上传裁剪、断点续传、分片上传、剪贴上传等等需要去补充和实现,我会持续地更新。如果有更复杂的业务和需求、如果我写的有疏漏有bug,欢迎留言交流,我会尽可能完善这篇文章,解决大家的上传之痛。
如果觉得好,来个star好吧ヽ( ̄▽ ̄)ノ