Asp.net core 2.0 +SPA檔案上傳注意事項

weixin_34087301發表於2018-01-21

最近在做的一個工程,後端選用asp.net 2.0 core,前端用vue。選用core純粹是好奇,想體驗一下微軟的新技術。還有就是實在不想寫java...。技術組成如下:

  • 後端:asp.net core 2.0 +JWT Auth + mongodb + RESTFul + swagger
  • 前端:Vue + Vuetify
    一開始一切都算順利,可是到了檔案上傳環節,遇到了一點小挫折(掩面,其實是耽誤了一天半時間),現在記錄一下:
  1. 前端上傳方式選擇:
    首先根據微軟官網文件,asp.net core檔案上傳示例全部用的是表單+Razor的方式,後臺用IFormFile接收。順便吐槽一下,感覺微軟總是慢半拍啊,整個core的介紹全都是MVC例子,沒有RESTFul的,資料庫全都是SQL。沒有NoSQL。
    SPA檔案上傳肯定不能用表單提交,那麼可以選擇的合理方式有:
  • axios
  • HTML5 原生XMLHttpRequest
  • jquery
  • 各種封裝好的第三方庫(如vue-upload-componen)

以下對各種方式進行實驗:

後端--------------------------

        [HttpPost("test")]
        public AppResponse UploadTest(IFormFile file)
        {
            if (file==null)
            {
                return responser.ReturnError(STATUS_CODE.BAD_REQUEST, "files is empty");
            }
            return responser.ReturnSuccess(extraInfo: file.FileName);
        }

關於檔案上傳,官方有一段文字解釋如下:

If your controller is accepting uploaded files using IFormFile but you find that the value is always null, confirm that your HTML form is specifying an enctype value of multipart/form-data. If this attribute is not set on the <form> element, the file upload will not occur and any bound IFormFile arguments will be null.

意思就是說,上傳檔案的表單必須設定enctype=multipart/form-data,否則是取不到值的。

但如果是以非表單方式上傳,前臺應該怎麼做?

前端-----------------------------

前臺搭建以下簡單頁面:

<template>
<v-card>
<h1>UPLOAD</h1>
<v-divider></v-divider>
<input type="file" ref="f" />
<v-btn @click.native="uploadOnXMLHttpRequest">使用XMLHttpRequest上傳</v-btn>
<v-btn @click.native="uploadOnAxios">使用axios上傳</v-btn> 
</v-card>

</template>
  1. 首先說axios,經過實驗,無法上傳檔案到.net core後臺:
 uploadOnAxios() {
      const options = {
        url: this.url,
        method: 'post',
        data: {
          'file': this.$refs.f.files[0]
        },
     //   headers: { 'Content-Type': undefined }//無效
        //  headers: { 'Content-Type': 'multipart/form-data' }//無效
      }
      this.$http.request(options).then(res => {
        console.log(res)
      })
    },

上面註釋掉的兩個header,就是試圖設定enctype,其實就是表單header裡的content-type,經過測試,都沒有效果,axios始終傳送:application/json。後臺因此拿不到值。

  1. HTML5 原生XMLHttpRequest
    首先是關於瀏覽器支援,這個要看工程,比如我這個工程,都用到vue和vuetify了,就不考慮相容性了,放心大膽使用就行。
  uploadOnXMLHttpRequest() {
      const fileObj = this.$refs.f.files[0] // js 獲取檔案物件
      var url = this.url // 接收上傳檔案的後臺地址
      var form = new FormData() // FormData 物件
      form.append('file', fileObj) // 檔案物件

      const xhr = new XMLHttpRequest()  // XMLHttpRequest 物件
      xhr.open('post', url, true) // post方式,url為伺服器請求地址,true 該引數規定請求是否非同步處理。
     // xhr.setRequestHeader('Content-Type', undefined)
      xhr.onload = (evt) => {
        var data = JSON.parse(evt)
        if (data.status) {
          alert('上傳成功!')
        } else {
          alert('上傳失敗!')
        }
      } // 請求完成
      xhr.onerror = (x) => {
        alert('failed:' + JSON.parse(x))
      } // 請求失敗
      xhr.onprogress = (x) => {
        console.log(`uploading...${x}%`)
      } // 請求失敗
      xhr.send(form) // 開始上傳,傳送form資料
    },

經測試,可以上傳,注意xhr.setRequestHeader('Content-Type', undefined)被註釋掉了,實際上這時XMLHttpRequest能自動設定content-type,這句加了反倒會報錯。

  1. jquery
    我沒有直接在vue專案中測試jquery,是在一個純靜態html中測試的,使用到了jQuery和query.form外掛:
<!doctype html>

<head>
  <title>File Upload Progress Demo #2</title>
  <style>
    body {
      padding: 30px
    }

    form {
      display: block;
      margin: 20px auto;
      background: #eee;
      border-radius: 10px;
      padding: 15px
    }

    .progress {
      position: relative;
      width: 400px;
      border: 1px solid #ddd;
      padding: 1px;
      border-radius: 3px;
    }

    .bar {
      background-color: #B4F5B4;
      width: 0%;
      height: 20px;
      border-radius: 3px;
    }

    .percent {
      position: absolute;
      display: inline-block;
      top: 3px;
      left: 48%;
    }
  </style>
</head>

<body>
  <h1>File Upload Progress Demo #2</h1>
  <code>&lt;input type="file" name="myfile[]" multiple></code>
  <form action="http://localhost:5000/api/document/test" method="post" enctype="multipart/form-data">
    <input type="file" name="files" multiple>
    <br>
    <input type="submit" value="Upload File to Server">
  </form>

  <div class="progress">
    <div class="bar"></div>
    <div class="percent">0%</div>
  </div>

  <div id="status"></div>

  <script src="https://cdn.bootcss.com/jquery/1.7.2/jquery.min.js"></script>
  <script src="https://cdn.bootcss.com/jquery.form/3.36/jquery.form.min.js"></script>
  <script>
    (function () {

      var bar = $('.bar');
      var percent = $('.percent');
      var status = $('#status');

      $('form').ajaxForm({
        beforeSend: function () {
          status.empty();
          var percentVal = '0%';
          bar.width(percentVal)
          percent.html(percentVal);
        },
        uploadProgress: function (event, position, total, percentComplete) {
          var percentVal = percentComplete + '%';
          bar.width(percentVal)
          percent.html(percentVal);
          //console.log(percentVal, position, total);
        },
        success: function () {
          var percentVal = '100%';
          bar.width(percentVal)
          percent.html(percentVal);
        },
        complete: function (xhr) {
          status.html(xhr.responseText);
        }
      });

    })();
  </script>


結果:可以上傳,這個應該沒問題,因為本來就是一個表單上傳啊~~,query.form的作用是使用了一個隱藏的iframe。上傳後重新整理的其實是這個iframe,所以使用者感覺不到頁面重新整理。
雖然實現了,但個人並不喜歡這種hack的寫法。jQuery應該退出歷史舞臺了。也算功成名就。還有,在vue中使用jQuery也不是很難,但總感覺不倫不類。

  1. 其他庫:
    這些庫功能很多,但個人不建議使用,一來很多功能用不上,二來其底層實現不好控制

總結:

我最後選擇了 HTML5 XMLHttpRequest 在asp.net core中上傳檔案,原生模組,靈活方便,隨便就能寫出一個個人定製的不錯的上傳元件。

補充

其實,把上傳的檔案和程式放在同一個伺服器不是很好的做法,完全可以建一個資源伺服器進行隔離,以下是用 express和multer建立的一個簡單上傳檔案後臺:

var express = require('express')
var multer  = require('multer')
var upload = multer({ dest: 'uploads/' })

var app = express()

//allow custom header and CORS
app.all('*',function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
  res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');

  if (req.method == 'OPTIONS') {
    res.send(200); //讓options請求快速返回/
  }
  else {
    next();
  }
}); 

app.post('/profile', upload.single('file'), function (req, res, next) {
  const file = req.file
  const body = req.body
  res.send('ok,uploaded')
  next()
  // req.body will hold the text fields, if there were any
})

app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) {
  // req.files is array of `photos` files
  // req.body will contain the text fields, if there were any
})

var cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])
app.post('/cool-profile', cpUpload, function (req, res, next) {
  // req.files is an object (String -> Array) where fieldname is the key, and the value is array of files
  //
  // e.g.
  //  req.files['avatar'][0] -> File
  //  req.files['gallery'] -> Array
  //
  // req.body will contain the text fields, if there were any
})

var server = app.listen(3000, function () {
    var host = server.address().address;
    var port = server.address().port;
  
    console.log('Example app listening at http://%s:%s', host, port);
  });

這樣,上傳檔案到這個伺服器後,拿到檔案地址,再回來應用插入到資料庫就行

當然,如果資料保密度不高,那用七牛是最簡單的了

最近有個想法,出一個vue+core的工程管理系統(PMS)系列教程,各位喜歡就點個贊吧,我看著贊多了就開始寫(_)

相關文章