美女圖片預警,請注意流量!!!javascript
本文內容:html
文章所有示例代碼地址:2019-0907-qiniu-upload前端
最近承包了一個小程序的先後端,具有小程序登陸、微信支付(欲哭無淚的支付)、後臺增長商品、管理訂單、編輯頁面等功能,發現了很多問題,因而走上了寫文章的道路,請你們海涵我這個小菜...vue
在之前的項目,文件通常都是直接放在服務器某個文件夾裏面的,對於特定區域的to B業務固然是沒問題的,可是不適合小程序這種to C業務的場景。以下java
因而決定把文件、圖片放在CDN,琢磨七牛的SDK和Upload組件花費了很多時間,踩了不少坑。諸多對象存儲都是key-value的存儲方式,key是惟一的鍵值,幾乎都是token+配置參數的方式,上傳方式大同小異,但願下面總結對你有所幫助。node
七牛雲上傳文件能夠簡單地分爲兩種方式:ios
實現簡單的文件上傳到七牛其實很簡單,一個前端er只須要一個上傳組件而後讓後端提供一個token請求。然後端只須要安裝七牛官方的Node.js SDK: npm install qiniu
,生成token的函數是:git
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組件:github
<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綁定的地址,upload-z2.qiniup.com 只是華南區空間的地址,能夠在這裏查看上傳地址:存儲區域 。
我直傳了一張小喬丁香結圖片,控制檯輸出上傳成功的參數:
{
hash: "FrAvE78xCroRtgdIdM5u3ajlcLUL",
key: "FrAvE78xCroRtgdIdM5u3ajlcLUL"
}
複製代碼
加速域名拼接上返回參數的key就能夠訪問圖片了,是否是很簡單!
qiniu.hackslog.cn/FrAvE78xCro…
自定義圖片的名字在後端生成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,而後直接向七牛發了工單,他們工程師一上班就回復我了:
他的意思是說file、key、token缺一不可。上傳組件會幫你處理file,key和token須要在data這個API上綁定。<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…
多文件上傳,目標要實現的是這樣的。
代碼以下:
<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來過濾了一下就行了。前端代碼 && 後端代碼
上傳過濾通常限制大小,Upload組件已經提供了on-exceeded-size
API來實現了。我遇到的需求是:上傳前限制上傳的尺寸,具體場景是小程序的輪播圖、商品詳情圖,若是尺寸不一致很不美觀。
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);
});
},
複製代碼
後端使用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));
複製代碼
服務器端上傳到七牛僅指服務器有這樣的需求才執行的代碼,不建議從前端上傳到服務器的臨時文件夾而後再傳輸到七牛雲存儲,讀寫文件的操做確實有點耗時。若是你非要那麼作,能夠用前面的後端路由代碼結合下面的示例來實現。
首先服務器讀取特定文件夾的文件,獲取文件名去生成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…
小程序上傳文件的場景好比在政府服務的應用上傳身份證、一些電商應用發佈圖片等。目前廣泛使用了別人封裝好的輪子: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()一下。
展現下成果:
小程序雲開發在通過逐步完善,已經開始支持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好吧ヽ( ̄▽ ̄)ノ