前言
本文仅为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由服务器来获取。
在拿到七牛的AccessKey
,SecretKey
和Bucket
后,就能够在服务器端生成token了,如下实现:
var options = { scope: bucket,};var putPolicy = new qiniu.rs.PutPolicy(options);var uploadToken=putPolicy.uploadToken(mac);
目标是要把 uploadToken
返回给前端。
下面代码是最简略的设置,也能够参照 开发者文档 设置其余参数。
整体思路
- 上传前。在上传前须要拿到上传地址(zone)、上传凭证(token)、存储cdn域名(domain);
- 上传。把文件和token一起上传;
2.1 获取到文件对象File
2.2 上传参数有token、file、key(可选),以POST办法上传到机房,上传申请头为“multipart-formdata” - 上传实现。上传胜利后七牛返回一个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" />
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 的同学应该晓得,上传组件能够设置action
和data
属性:
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">  <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,能够从上面几点排查:
- koaBody 需增加
multipart: true
设置; - koa-body版本问题。koa-body v3和v4之前通过ctx.request.body捕捉文件。而v3.v4后才是通过ctx.request.files.file进行获取;
koa-body
与koa-bodyparser
抵触。koa2 应用 koa-body 代替 koa-bodyparser 和 koa-multer。koa-bod 与 koa-bodyparser 同时存在时,get申请会产生谬误。对于中间件程序问题。路由中间件和koa-body中间件都存在时,koa-body须要在路由之前注入。
app.use(koaBody({ multipart: true })) app.use(router.routes(), router.allowedMethods())
前端以表单格局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 = 200ctx.body = { code: 0, message: 'success', data: { avatar: `http://${domain}/${qn_res.key}` }}
}