Koa、Vue、axios、小程序上傳圖片到七牛不用愁(更新ing)

美女圖片預警,請注意流量!!!javascript

本文內容:html

  • 前端上傳
    • 直接上傳
    • 自定義名字上傳
    • 手動上傳多張圖片(axios)
    • 上傳前過濾
  • 圖片上傳到服務器(預覽)
  • 服務器端上傳到七牛雲
  • 小程序圖片上傳
    • 基本api上傳
    • 雲存儲

文章所有示例代碼地址:2019-0907-qiniu-upload前端

最近承包了一個小程序的先後端,具有小程序登陸、微信支付(欲哭無淚的支付)、後臺增長商品、管理訂單、編輯頁面等功能,發現了很多問題,因而走上了寫文章的道路,請你們海涵我這個小菜...vue

在之前的項目,文件通常都是直接放在服務器某個文件夾裏面的,對於特定區域的to B業務固然是沒問題的,可是不適合小程序這種to C業務的場景。以下java

  1. 小程序有大小限制,圖片等放在源代碼文件裏體積太大
  2. 天南地北,服務器訪問圖片的速度不同,影響客戶體驗
  3. 服務器出現問題,數據丟失致使時間、運維等成本提高

因而決定把文件、圖片放在CDN,琢磨七牛的SDK和Upload組件花費了很多時間,踩了不少坑。諸多對象存儲都是key-value的存儲方式,key是惟一的鍵值,幾乎都是token+配置參數的方式,上傳方式大同小異,但願下面總結對你有所幫助。node

1.前端上傳

七牛雲上傳文件能夠簡單地分爲兩種方式:ios

  1. 前端從業務服務器獲取上傳憑證(token),前端直接把文件傳到七牛雲存儲
  2. 業務服務器直接把文件上傳到七牛雲存儲,好比日誌、數據庫備份,通常都是定時並加密的

front-end-post

backend

1.1直接上傳

實現簡單的文件上傳到七牛其實很簡單,一個前端er只須要一個上傳組件而後讓後端提供一個token請求。然後端只須要安裝七牛官方的Node.js SDKnpm 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: 上傳的地址,必填
  • data: 上傳時附帶的額外參數,這些參數會放在FormData裏面
  • before-upload: 上傳文件以前的鉤子,參數爲上傳的文件,若返回 false 或者 Promise 則中止上傳
  • on-success: 文件上傳成功時的鉤子,返回字段爲 response, file, fileList
  • format: 支持的文件類型
  • max-size: 文件大小限制,單位 kb

使用代碼的時候須要自行修改action綁定的地址,upload-z2.qiniup.com 只是華南區空間的地址,能夠在這裏查看上傳地址:存儲區域

front

我直傳了一張小喬丁香結圖片,控制檯輸出上傳成功的參數:

{
    hash: "FrAvE78xCroRtgdIdM5u3ajlcLUL", 
    key: "FrAvE78xCroRtgdIdM5u3ajlcLUL"
}
複製代碼

加速域名拼接上返回參數的key就能夠訪問圖片了,是否是很簡單!

qiniu.hackslog.cn/FrAvE78xCro…

xiaoqiao

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,而後直接向七牛發了工單,他們工程師一上班就回復我了:

gongdan
他的意思是說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…

公孫離

1.3 手動上傳多張圖片

多文件上傳,目標要實現的是這樣的。

manual

代碼以下:

<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報錯。

token duplicated

改變一下方式,咱們使用原始的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報的錯:

error

這個問題困惑了好久,我一直在想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來過濾了一下就行了。前端代碼 && 後端代碼

qiniu_manual

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之內的隨機數

server name

其實若是是手動上傳,咱們仍是能夠用iView的,只須要返回before-upload的值爲false或者Promise,再用一個按鈕調用axios上傳的邏輯。在iView文檔自定義上傳列表中的示例,能夠看出咱們能夠利用this.$refs.upload.fileList 來保存文件信息,也能夠實現預覽、刪除等功能。相對elementUI而言,iView Upload組件的示例有必定侷限性,所以我本身寫樣式展現文件預覽:

directly_to_serve

<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 它將返回圖片的文件名列表,而且完成上傳到七牛雲、刪除本地文件的操做。命令行輸出以下:

luna

fs.unlink 是異步操做,須要等上傳完畢再刪除。而後大家能夠欣賞一下紫霞仙子小姐姐的風韻~~~

qiniu.hackslog.cn/15683981411…

https://user-gold-cdn.xitu.io/2019/9/14/16d2ff01fb0d7576?w=1920&h=1080&f=jpeg&s=188363

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()一下。

展現下成果:

shuoshuo

相關代碼

4.2 雲存儲

小程序雲開發在通過逐步完善,已經開始支持HTTP API的操做了,也就是說能夠實現不購置服務器的狀況下完成小程序和Web管理控制檯,很是有吸引力。雲儲存 提供了很方便的API讓小程序直接調用函數上傳圖片,還能夠在雲開發控制檯手動上傳,本質來講也是一個對象存儲(多副本)。與之配合的有云函數能夠完成一些數據處理工做、數據庫有不少貌似MongoDB的API,前端的工做愈來愈重了/(ㄒoㄒ)/~~

mini_upload

這裏引用謝成老師 的代碼,模擬發佈朋友圈的過程,實現主要使用了wx.chooseImagewx.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: '發佈失敗',
      })
    })
  },
})

複製代碼

嘿嘿,完結了!

參考連接:

new File-MDN

File對象, FileList對象,FileReader對象

FormData-MDN

iView + axios實現圖片預覽及單請求批量上傳

限制上傳文件的大小

七牛雲上傳圖片,從前端到後端

koa-body

圖片上傳的需求各類各樣,這裏只介紹了基本的上傳方式,能夠預見的需求好比上傳裁剪、斷點續傳、分片上傳、剪貼上傳等等須要去補充和實現,我會持續地更新。若是有更復雜的業務和需求、若是我寫的有疏漏有bug,歡迎留言交流,我會盡量完善這篇文章,解決你們的上傳之痛。

若是以爲好,來個star好吧ヽ( ̄▽ ̄)ノ

相關文章
相關標籤/搜索