測試開發實戰[提測平臺]17-Flask&Vue檔案上傳實現

MrZ大奇發表於2021-12-19

微信搜尋【大奇測試開】,關注這個堅持分享測試開發乾貨的傢伙。

先回顧下在此係列第8次分享給出的預期實現的產品原型和需求說明,如下圖整體上和前兩節實現很相似,只不過一般測試報告要寫的內容可能比較多,就多用了些多行輸入框元件,另外一個特別的全新功能操作就是 附件上傳,這是需要先解決和要掌握的重點內容。

 

後端服務實現附件的儲存,要寫個上傳介面,服務端通過request.files進行獲取實現,Postman模擬請求的話,方法使用POST,檔案通過form-data格式中的file進行上傳,一個基本的實現程式碼介面如下:

  1. 定義請求方法和路徑

  2. 拼接一個專案儲存資料夾的一個絕對路徑

  3. 獲取form-data指定key的檔案,通過save儲存後返回成功訊息 

@test_manager.route("/api/report/upload",methods=['POST'])
def uploadFile():
    # 儲存檔案的路徑
    save_path = os.path.join(os.path.abspath(os.path.dirname(__file__)).split('TPMService')[0], 'TPMService/static')
    # 獲取檔案
    attfile = request.files.get('file')
    attfile.save(os.path.join(save_path, attfile.filename))
    return {"code":200, "message":"上傳請求成功"}

  

這裡對於檔案上傳,一般來說不能無限制上傳,需要對格式、大小做一些限制,還要做一些安全的處理,方式是通過FileField做要求如格式的限制,用secure_filename做檔名安全處理

優化後完整的程式碼分兩個片段

1. 引入依賴和做fileForm類

import os
# 涉及的相關依賴引用
from wtforms import Form,FileField
from flask_wtf.file import FileRequired,FileAllowed
from werkzeug.utils import secure_filename
from werkzeug.datastructures import CombinedMultiDict

# 表單提交相關校驗
class fileForm(Form):
    file = FileField(validators=[FileRequired(), FileAllowed(['jpg', 'png', 'gif', 'pdf', 'zip'])])

 

2. 增加格式校驗和安全校驗,另外這裡需要注意下,我拿的直接是上傳檔案的名字,我並沒有對檔名做一個隨機生成處理,這樣如果有重名檔案再次上傳會被覆蓋掉,一般作為一個靜態資源或者檔案服務來說是要做生成唯一碼名稱,python可以使用uuid,大家可以嘗試擴充套件下,如果是生成自己的串碼名還帶來另外一個問題,真的是統一檔案多次反覆上傳如何處理,那可能就要做真正的資料檔案資訊儲存,然後做MD5校驗,由於我們只是做個簡單附件服務,就不再做更多上傳服務的討論了。

@test_manager.route("/api/report/upload",methods=['POST'])
def uploadFile():
    # 初始化返回物件
    resp_success = format.resp_format_success
    resp_failed = format.resp_format_failed

    file_form = fileForm(CombinedMultiDict([request.form, request.files]))
    if file_form.validate():
        # 獲取專案路徑+儲存資料夾,組成服務儲存絕對路徑
        save_path = os.path.join(os.path.abspath(os.path.dirname(__file__)).split('TPMService')[0], 'TPMService/static')
        # 通過表單提交的form-data獲取選擇上傳的檔案
        attfile = request.files.get('file')
        # 進行安全名稱檢查處理
        file_name = secure_filename(attfile.filename)
        # 儲存檔案檔案中
        attfile.save(os.path.join(save_path, file_name))

        resp_success['data'] = {"fileName": file_name}
        return resp_success
    else:
        resp_failed['message'] = '檔案格式不符合預期'
        return resp_failed

上邊只是實現了檔案格式的校驗,對於上傳限制大小從網上搜的資料來看,flask一般通過全域性配置,比如下邊是配置限制一個16MB大小的檔案限制,如果超過會返回 413 Request Entity Too Large,網上資料說16M也是預設大小,但實際我測試了下,如果不設定全侷限制,我傳300+M除了慢點也能上傳成功,並且搜尋了原始碼 MAX_CONTENT_LENGTH = None,可能是由於版本的原因,現在沒有這個限制了。

from flask import Flask, Requestapp = Flask(__name__)app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000

關於Flask檔案上傳更多的解釋和例子請參考 [連結1],還有一種第三方外掛也可以對檔案進行友好的操作參考 [連結2],不過這兩種方式都是全域性控制的,如果想不同的介面單獨控制大小,目前嘗試的方式是讀取檔案然後獲取長度 len(attfile.read()) 其實就是位元組大小,對其進行比較返回即可,如果你有更好的方案,記得告訴我。

 

上傳檔案介面搞定了,自然少不了下載介面,這個比較簡單,通過flask提供的send_from_directory方法實現,程式碼如下,詳細解釋參考 [連結1] 後半部分。 

from flask import send_from_directory

@test_manager.route("/api/file/download",methods=['GET'])
def downloadFile():
    fimeName = request.args.get('name')

    # 儲存檔案的相對路徑
    save_path = os.path.join(os.path.abspath(os.path.dirname(__file__)).split('TPMService')[0], 'TPMService/static')

    result = send_from_directory(save_path, fimeName)

    return  result

 

重啟後端服務後,用postman請求做個上傳測試,效果如圖,一開始是個超大提示,後來正常上傳返回結果成功。

重新整理檢視程式碼服務儲存位置,檔案已經正確上傳

 下載的測試可以通過瀏覽GET請求服務+路徑 /api/file/download?=檔名 進行下載驗證。

 前端Vue的實現,用到的元件是“Upload上傳”,官網給出了多種樣式和方式,比如多檔案,頭像上傳,拖拽上傳,列表形式等等,具體可參考 [連結3]。

 

如圖其中action就是上傳地址,這塊就可以替換成剛剛實現的上傳介面 http://127.0.0.1:5000/api/report/upload 表示上傳地址,預設為選擇檔案後自動上傳,其實就是幫助你實現postman演示的表單檔案自動提交,可以通過:auto-upload="false" 設定關閉,也可以通過 http-request 覆蓋預設的上傳行為自定義實現。

這兩種方式都會實際寫個Demo實踐下

1. 自動上傳 新建一個檔案上傳頁面,路由繫結到跟目錄,編寫<template>和<script>部分程式碼,這裡在方法中用 :on-success 鉤子列印下上傳成功的返回資訊

<template>
  <div class="app-container">
    <el-form>
      <el-form-item label="附件" prop="test_file">
        <el-upload
          :limit="1"
          :file-list="fileList"
          :auto-upload="true"
          action="http://127.0.0.1:5000/api/report/upload"
          :on-success="uploadFile"
        >
          <el-button size="small" type="primary">點選上傳</el-button>
          <div slot="tip" class="el-upload__tip">只能上傳jpg/png/zip/pdf檔案,且不超過1M</div>
        </el-upload>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  name: 'DemoUpload',
  data() {
    return {
      fileList: []
    }
  },
  methods: {
    uploadFile(response, file, fileList) {
      console.log(response)
      console.log(file)
      console.log(fileList)
    }
  }
}
</script>

啟動前後端,選擇一個小於10M的檔案進行上傳測試,可以看到正常返回2000,並且能正常拿到鉤子中三個引數資訊,後邊報告功能實現其實就是拿到返回的檔名賦值給一個變數即可。

再測試一種情況,檔案格式不符合要求,大小超出服務端限制,發現在檔案不符合格式的情況是40000,但檔案列表還是顯示了,做個優化處理,在狀態碼不正確的情況,清空filelist

還有另外一個問題就是服務端大小超限制的時候回返回403,但element vue 及upload 直接在返回的時候時候攔截處理了,所以沒辦法精細異常處理,就進行了模糊提示處理。優化後及 增加了 :on-success 使用的方式的程式碼如下:

<template>
  <div class="app-container">
    <el-form>
      <el-form-item label="實現一" prop="test_file">
        <el-upload
          ref="fileOne"
          :limit="1"
          :file-list="fileList"
          :auto-upload="true"
          action="http://127.0.0.1:5000/api/report/upload"
          :on-success="uploadSuccess"
          :on-error="uploadErrors"
        >
          <el-button size="small" type="primary">點選上傳</el-button>
          <div slot="tip" class="el-upload__tip">只能上傳jpg/png/zip/pdf檔案,且不超過10M</div>
        </el-upload>
      </el-form-item>
      <el-form-item label="實現二" prop="test_file">
        <el-upload
          :limit="1"
          :file-list="fileList"
          action="http://127.0.0.1:5000/api/report/upload"
          :http-request="uploadeFile"
          :on-success="uploadSuccess"
        >
          <el-button size="small" type="primary">點選上傳</el-button>
          <div slot="tip" class="el-upload__tip">只能上傳jpg/png/zip/pdf檔案,且不超過10M</div>
        </el-upload>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  name: 'DemoUpload',
  data() {
    return {
      fileList: [],
      fileNanme: ''
    }
  },
  methods: {
    uploadSuccess(response, file, fileList) {
      if (response.code === 40000) {
        this.$message({
          message: '格式不正確或者上傳異常',
          type: 'warning'
        })
        this.fileList = []
      } else {
        this.$message({
          message: '上傳成功',
          type: 'success'
        })
      }
    },
    uploadErrors(err, file, fileList) {
      this.$message({
        message: '大小不符合要求或伺服器異常',
        type: 'warning'
      })
    },
    uploadeFile(params) {
      console.log(params.file)
      const fd = new FormData()
      fd.append('file', params.file)
      fd.append('FileName', params.file.name)
      fd.append('async', true)
      const config = {
        headers: { 'Content-Type': 'multipart/form-data' }
      }
      axios
        .post(params.action, fd, config)
        .then(res => {
          console.log(res.data)
        })
        .catch(Error => {
          this.fileList = []
          this.$message({
            message: '大小不符合要求或伺服器異常',
            type: 'warning'
          })
        })
    }
  }
}
</script>

 

為什麼有了自動上傳還是要講個自定義上傳,這裡有兩點:

目前為止校驗都依賴後端,但實際上服務端校驗是一個後置校驗,檔案已經上傳了,如果檔案大或者量大會很佔用IO,所以可以自定義提交進行一些前端的上傳校驗。

作為實踐儘量為大家趟一下坑 :on-success的使用官方並沒有給出例子,而要拿到元件資訊經過驗證是通過引數方法引數獲取,如下邊紅色框圈出的一些重要資訊。

 

再做一個異常的情況下的上傳測試,這是由uploadErrors鉤子 或者 axios catch(error)捕獲實現。

 到此本篇的分享大概是這些內容,下一次將組合完成報告部分,也就是【提出平臺】的第一階段,順便也會公佈下上次調查結果,以及一些後續安排,歡迎大家持續關注和交流,方式可以通過私信或者公眾號聯絡加微信入群均可。

【參考連結】

[連結1] https://dormousehole.readthedocs.io/en/latest/patterns/fileuploads.html

[連結2] https://pythonhosted.org/Flask-Uploads/

[連結3] https://element.eleme.io/#/zh-CN/component/upload

 

堅持原創,堅持實踐,堅持乾貨,如果你覺得有用,請點選推薦,也歡迎關注我部落格園和微信公眾號。

相關文章