乐趣区

关于javascript:文件上传与Koa2

前言

本文仅为 koa2 上传文件 (图片 / 文本 / 视频 / 音频) 到七牛云的记述。

对于我是如何找到该文档的?
在没有具体思路前,我也去看了好多博客,然而大家都没有说为什么要依照这个步骤 / 办法去上传,照着实现,出了问题也不知该从哪方面来解决,甚是气人。但看到大家都用到的一句代码是 tiformUploader.putFile(uploadToken, key, localFile, putExtra, function (),就试着在谷歌“七牛云开发者文档”,而后抉择比拟官网的一个“七牛云开发者核心”,点开网站后一个大大的搜寻框,输出下面的那句次要代码,就调整到对应的Nodejs SDK,正是咱们所须要的。
下次找相干的官网文档时,就能够用这个办法。

上传的实质

在说具体代码之前,我想先说说在客户端上传的原理,这样你就能了解上面说的几种上传模式其实在实质上都是一样。
我认为的上传的实质一句话就是“获取到文件对象 File,将 File 以表单格局提交到服务器端”。能够转化为两个疑难来解说:

1. 如何取到文件对象 File?2. 如何以表单格局提交?

如何取到文件对象 File:

文件上传元素是:<input type="file" id="input">,经 type="file" 标识,就能够通过 File API 拜访 FileList,它蕴含了示意用户所选文件的 File 对象。
type=”file” 都是以列表(FileList)模式存在,本文章以单文件上传来解说,取的是 FileList 中的一个元素。
拜访被抉择的文件的两种形式:

第一种:`const myFile = document.getElementById('input').files[0]`  
第二种:`document.getElementById('input').addEventListener("change", function() {const myFile = this.files[0] }, false)`  

myFile 即咱们要获取的抉择文件的 File 对象。

可参见:
MDN: 在 web 应用程序中应用文件
MDN: input type=”file”

如何以表单格局提交:

文件个别都是用表单格局来提交的,不论咱们是上传到七牛云还是上传到本人的服务器上。

web 上的 http 申请都是基于 XMLHttpRequest接口来实现的。而 XMLHttpRequest 有一个 formData 接口,反对发送表单数据;FormData 类型的数据,XMLHttpRequest 的 content-type 默认值为multipart/form-data,你也能够手动设置;

咱们罕用的 ajax、axios 都是基于 XMLHttpRequest 封装实现的

可参见:
阮一峰:XMLHttpRequest Level 2 使用指南
MDN:FormData 对象的应用
MDN:FormData

上传形式

文件上传分为 客户端上传 服务端上传 两种场景。
文件上传到七牛云,可先查阅七牛的开发者文档。

七牛云配置

在上传之前,先配置好七牛,并拿到以下值:
AccessKey
SecretKey : 七牛密钥(集体核心 – 密钥治理)
Bucket : 空间名称(对象治理 – 空间治理 – 空间名称)
domain :CDN 域名
zone:存储区域(对应本人主机的区域,可查看 七牛存储区域)

客户端上传

客户端上传是指文件上传到七牛这个动作在前端进行。

服务器端获取七牛 token

文件在上传到七牛云时须要带上一个七牛云的上传凭证,即咱们说的 token,否则会 401 谬误,这个 token 由服务器来获取。
在拿到七牛的 AccessKeySecretKeyBucket后,就能够在服务器端生成 token 了,如下实现:

var options = {scope: bucket,};
var putPolicy = new qiniu.rs.PutPolicy(options);
var uploadToken=putPolicy.uploadToken(mac);

目标是要把 uploadToken 返回给前端。
下面代码是最简略的设置,也能够参照 开发者文档 设置其余参数。

整体思路

  1. 上传前。在上传前须要拿到上传地址(zone)、上传凭证(token)、存储 cdn 域名(domain);
  2. 上传。把文件和 token 一起上传;
    2.1 获取到文件对象 File
    2.2 上传参数有 token、file、key(可选),以 POST 办法上传到机房,上传申请头为“multipart-formdata”
  3. 上传实现。上传胜利后七牛返回一个 key 和 hash,domain 拼接上 key 即可显示图片了,key 能够了解为图片在七牛上的资源定位惟一标识。
    3.1 上传胜利后七牛返回图片能够是“blob 格局”或者“门路格局”。
    3.2 第 2.2 步中,上传参数加上 key 值上传胜利后即返回图片门路格局。key 的格局为“文件名 + 文件后缀”结尾,后面能够拼接存储门路,如“public/images/a.png”、“public/audios/a.mp3”。

几个留神点:
a. domain、zone 能够本人写在本地,也能够让后端来返回;
b. token 有效期只有一次,再次上传时,不论上传的是否为同一张图片都须要从新拿 token;

客户端上传文件

客户端拿到后端返回的 token,就能够上传文件了。
客户端上传有几种形式,我分为三种:

  • 接口模式上传
  • 组件模式上传
形式一:接口模式上传

html:

<input type="file" id="avatar" accept="image/png, image/jpeg" />
![](imageUrl)

js:

let input = document.getElementById('avatar')
input.addEventListener('change', async () => {

  // 获取 File 对象
  const file = input.files[0]
  
  // 申请后端接口获取到七牛的 domain, zone, token;token 有效期只有一次,所以每次上传前都先获取 token
  const {data: { domain, zone, token} } = await API.getQiniuToken()
  if(!token) {console.log('获取七牛 token 失败')
      return
  }

  let formdata = new FormData()

  formdata.append('token', token)
  formdata.append('file', file)
  formdata.append('key', `public/tmp/${Date.now()}${file.name.split('.').pop()}`) // key 参数可选

  axios({
    method: 'POST',
    url: zone,
    data: formdata,
  })
    .then((response) => {console.log(response.data)
      this.imageUrl = `http://${domain}/${response.data.key}`
    })
    .catch((error) => {console.log(error)
    })
})
形式二:组件模式

这里以 ElementUI 的上传组件为例。
用 ElementUI 或 iviewUI 的同学应该晓得,上传组件能够设置 actiondata属性:
action:必选参数,上传的地址。即对应七牛的存储区域 zone,组件会主动帮咱们上传,上传胜利后触发on-success 事件;
data: 上传时附带的额定参数。上传到七牛的额定参数须要有:token、file、key(可选)。组件会默认加上 file,所以咱们只须要带上 token(或{token、key})就能够了。

实现过程如下:

<template>
  <el-upload 
    :action="qiniu.zone" 
    :data="{token: qiniu.token, key: imageKey}" 
    :show-file-list="false" 
    :on-success="success" 
    :on-error="fail" 
    :before-upload="beforeUpload">
    ![](imageUrl)
    <button> 上传图片:</button>
  </el-upload>
</template>
<script>
import {Upload} from 'element-ui'
import {qiniu as API} from '@/api'
export default {
  name: 'ImageUploader',
  components: {'el-upload': Upload},
  data () {
    return {
      imageKey: '',
      imageUrl: '',
      qiniu: {domain: '', zone:'', token: ''}
    }
  },
  methods: {async beforeUpload (file) {
      // 申请后端接口获取到七牛的 domain, zone, token
      const {data} = await API.getQiniuToken() 
      this.qiniu = data
      if(!token) {console.log('获取七牛 token 失败')
          return
      }
      
      // 设置上传的 key 值,可选
      this.imageKey = `public/tmp/${Date.now()}${file.name.split('.').pop()}`
    },
    success (res, file) {this.imageUrl = `http://${this.qiniu.domain}/${res.key}`
    },
    fail (err, file) {console.log(err)
    }
  }
}
</script>

下为申请的截图,从图中能够看到,组件也是用 FormData 来进行上传的:

服务端上传

服务端上传,其官网说法是:

客户利用服务端 SDK 从服务端间接上传文件到存储,交互的单方个别都在机房外面,所以服务端能够本人生成上传凭证,而后利用 SDK 中的上传逻辑进行上传,最初从存储服务获取上传的后果,这个过程中因为单方都是业务服务器,所以很少利用到上传回调的性能,而是间接自定义 returnBody 来获取自定义的回复内容

可参见:七牛云 Node.js SDK – 服务器直传

服务度上传文件到七牛也有几种办法,这里说最简略的一种:上传本地文件,间接指定文件的残缺门路即可上传。

先说几个 留神点:
koa2 获取表单数据是用 koa-body 实现的。如果 ctx.request.files.file 为 undefined,能够从上面几点排查:

  1. koaBody 需增加 multipart: true 设置;
  2. koa-body 版本问题。koa-body v3 和 v4 之前通过 ctx.request.body 捕捉文件。而 v3.v4 后才是通过 ctx.request.files.file 进行获取;
  3. koa-bodykoa-bodyparser 抵触。koa2 应用 koa-body 代替 koa-bodyparser 和 koa-multer。koa-bod 与 koa-bodyparser 同时存在时,get 申请会产生谬误。
  4. 对于中间件程序问题。路由中间件和 koa-body 中间件都存在时,koa-body 须要在路由之前注入。

        app.use(koaBody({ multipart: true}))
        app.use(router.routes(), router.allowedMethods())
  5. 前端以表单格局 formdata 提交,用 POST 办法并设置编码类型 "Content-Type": "multipart/form-data"。下为 axios 示例:

    const file = document.getElementById('image_upload').files[0]

let formdata = new FormData()
formdata.append(‘file’, file)

let config = {
headers: {‘Content-Type’: ‘multipart/form-data’},
}

axios({
method: ‘POST’,
url: ‘/upload’,
data: formdata,
config
})
.then((response) => console.log(response.data))
.catch((error) => console.log(error))

6. 文件上传到本地时,以流的模式来读取,数据可用之前,读取操作不会完结。这能够避免过程的退出与流的主动敞开。监听读取的 `end` 事件,读取完结后再进行下一步操作。下为具体实现:

const qiniu = require(‘qiniu’)
const path = require(‘path’)
const fs = require(‘fs’)
const config = require(‘../config’)
const {bucket, zone, domain, AK, SK} = config.qiniu

static async uploaderAvatar (ctx) {

// 获取上传文件
const file = ctx.request.files.file

// 写入目录
const mkdirsSync = (dirname) => {if (fs.existsSync(dirname)) {return true} else {if (mkdirsSync(path.dirname(dirname))) {fs.mkdirSync(dirname)
      return true
    }
  }
  return false
}

// 重命名
function rename (fileName) {return Math.random().toString(16).substr(2) + '.' + fileName.split('.').pop()}

// 删除文件
function removeTemImage (path) {fs.unlink(path, (err) => {if (err) {throw err}
  })
}

// 上传到本地
/**
 * @description 上传到本地
 * @param {*} file
 * @returns {Promise} fileName fileFullPath 
 * @file http://nodejs.cn/api/fs.html#fs_fs_createreadstream_path_options
 */
function upToLocal (file) {return new Promise((resolve, reject) => {
    // 本地文件存储门路
    let filePath = path.join(__dirname, 'public/upload/');

    // 创立本地上传文件门路
    const confirm = mkdirsSync(filePath)
    if (!confirm) {console.log("------- 创立本地上传文件门路失败 ----------")
      return
    }

    // 创立可读流
    const stream = fs.createReadStream(file.path)

    // 文件名
    const fileName = rename(file.name)

    // 本地文件门路
    const fileFullPath = path.join(path.join(filePath, fileName))

    // 创立可写流
    const upStream = fs.createWriteStream(fileFullPath)

    // 可读流通过管道写入可写流
    stream.pipe(upStream);

    stream.on('end', () => {console.log('file read finished');

      // 敞开流
      stream.push(null);
      stream.read(0);

      resolve({fileName, fileFullPath})
    })

    stream.on('error', (err) => {reject('------------ createReadStream error -----------', err)
    })

  })
}

// 上传到七牛
/**
  * @description 上传到七牛
  * @param {String} key
  * @param {String} filePath 
  * @returns {Promise} {hash, key} 
  * @file https://developer.qiniu.com/kodo/sdk/1289/nodejs
  */
function upToQiniu ({fileName, fileFullPath}) {

  // 参数 1:获取 token 凭证
  const mac = new qiniu.auth.digest.Mac(AK, SK)
  const options = {scope: bucket}
  const putPolicy = new qiniu.rs.PutPolicy(options)
  const uploadToken = putPolicy.uploadToken(mac)


  // 参数 2:须要上传到七牛云的文件门路
  const key = `public/tem/${fileName}`

  // 参数 3:上传在本地的临时文件的门路
  const localFile = fileFullPath.replace(/\\/g, '\/')

  // 参数 4:固定参数,额为参数
  const putExtra = new qiniu.form_up.PutExtra();

  // new 一个七牛云的上传对象
  let config = new qiniu.conf.Config();
  // Zone_z2 为空间对应的机房
  config.zone = qiniu.zone.Zone_z2
  const formUploader = new qiniu.form_up.FormUploader(config);

  // 文件上传到七牛云(单文件)return new Promise((resolved, reject) => {
    /**
     * uploadToken 七牛云上传凭证
     * key 七牛云文件存储门路
     * localFile 本地文件门路
     * putExtra 额为参数
     */
    formUploader.putFile(uploadToken, key, localFile, putExtra, function (respErr, respBody, respInfo) {if (respErr) {reject(respErr)
      }
      if (respInfo.statusCode == 200) {resolved(respBody)
      } else {resolved(respBody)
      }
    })
  })

}

// 上传文件到本地获,获取本地的 文件名 和 文件门路
const local_res = await upToLocal(file)

// 文件门路 -> 读取本地文件上传到七牛云;文件名 -> 设置为七牛云的 key(能够了解为上传到七牛云后,该 key 值就是该文件的资源定位惟一标识)
const qn_res = await upToQiniu(local_res)

// 上存到七牛后,删除缓存在本地的图片
removeTemImage(local_res.fileFullPath)

ctx.status = 200
ctx.body = {
  code: 0,
  message: 'success',
  data: {avatar: `http://${domain}/${qn_res.key}`
  }
}

}

退出移动版