文件上传
PS:我本人是比较喜欢通过代码来进行对知识的学习的!所以我会比较喜欢通过记录代码来唤起我当时的记忆!
说一下文件上传原理吧
服务器端接收到文件上传请求后,解析请求体,获取文件内容,并将文件内容存储在本地文件系统中。并将该目录设置为静态资源文件并保留出去
说一下具体怎么实现吧
前端实现
这里我们使用 input:file
上传本地文件,检测到内容变化则用 fetch
api 将上传的文件向服务端发起请求(也可以用 xhr,后面会讲俩者使用的区别)
一般来说,对于请求,一般需要设置
headers
请求头,以便后端更好的确定数据类型并执行相应修改。const formData = new FormData(); formData.append('file', file); ... body: formData
这里我们直接往post请求body中添加
FormData
数据,浏览器会根据文件自动判断并设置请求头, 这里不需要重复设置headers: {'Content-Type': 'multipart/form-data'}
,设置了反而会报错误
如果重复设置了请求头为 multipart/form-data
,会导致请求头只有 'Content-Type': 'multipart/form-data'
Bing报错 Error: Multipart: Boundary not found 缺少 Boundary
分隔符
在 Content-Type 后面添加的一堆字符是 boundary,它用于标识请求体中的不同部分之间的边界。在请求体中,每个部分之间都会用这个 boundary 进行分隔。boundary 的值是一个随机字符串,不重复即可。
前端代码
<input type="file" id="fileInput">
<script>
function uploadFile(file, onProgress) {
const formData = = new FormData();
formData.append('file', file);
return fetch('http://localhost:3000/upload', {
method: 'POST',
body: formData, // 浏览器会自动设置content-type
mode:'cors', // 设置跨域
})
}
fileInput.addEventListener('change', async () => {
const file = fileInput.files[0];
const response = await uploadFile(file);
const result = await response.text();
console.log(result);
});
</script>
对于跨域配置不熟悉的,可以看一下我之前的文章:
补充:对于fetch配置跨域的选项 mode
:
使用 Fetch - Web API 接口参考 | MDN (mozilla.org)
Request.mode - Web API 接口参考 | MDN (mozilla.org)
same-origin
:同源请求,只能请求同源资源;no-cors
:不带身份凭证的跨域请求,主要用于对资源进行 GET 请求,并不能访问响应的数据,只能访问返回状态码和头信息;cors
:带身份凭证的跨域请求,允许进行跨域资源共享,可以访问响应的数据;navigate
:导航请求,仅可用于 window.fetch(),可以请求 HTML 页面并从响应中提取数据。
fetch('http://localhost:3000/upload', {
method: 'POST',
mode:'cors', // no-cors, *cors, same-origin
})
顺便讲一下我今天遇到的麻烦,因为我服务器开放的端口不一样,是跨域请求,一开始设置了 fetch 请求头 content-type,一直报 CORS (跨域) 错误。
If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
一直以为是跨域的问题,搞了半天,服了,才发现是前面配置了 content-type 使得缺少Boundary部分,导致服务端抛出错误,因此浏览器不能接受到服务器返回的请求抛出 CORS 错误
后端实现
我们使用 koa-body
中间件对文件进行处理配置(上传文件大小、安全性)
主要就是文件拷贝,将前端请求的文件保存在一个目录下,并且将该目录设置为静态资源(可以直接通过ip/目录名访问)
这里我们使用管道 pipe
进行文件读写,使用fs.createReadStream
创建可读流,使用fs.createWriteStream
创建可写流,将两个流通过管道连接起来,实现边读边写。
主要代码如下:
router.post('/upload', async (ctx, next) => {
const { files } = ctx.request;
const file = files.file;
const reader = fs.createReadStream(file.filepath);
const writer = fs.createWriteStream(`./uploads/${file.newFilename}.${file.originalFilename.split('.').pop()}`);
reader.pipe(writer);
ctx.body = '文件上传成功!'
});
后端 koa
代码
const Koa = require('koa');
const {koaBody} = require('koa-body');
const fs = require('fs');
const router = require('koa-router')()
const app = new Koa();
// 跨域
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // 令牌
ctx.set('Access-Control-Allow-Credentials', true);
// 处理预检请求
if (ctx.method === 'OPTIONS') return ctx.status = 204;
await next();
})
app.use(koaBody({
multipart: true,
formidable: {
maxFileSize: 100 * 1024 * 1024, // 设置上传文件大小上限,默认为 2MB
},
}));
app.use(router.routes(), router.allowedMethods());
router.post('/upload', async (ctx, next) => {
const { files } = ctx.request;
const file = files.file;
const reader = fs.createReadStream(file.filepath);
const writer = fs.createWriteStream(`./uploads/${file.newFilename}.${file.originalFilename.split('.').pop()}`);
reader.pipe(writer);
writer.on('finish', function () {
ctx.body = '文件上传成功!';
});
});
app.listen(3000, () => {
console.log('Server is running at http://localhost:3000');
});
你刚才说用 pipe
实现,还有其他方法吗?有什么区别?
可以用如下方法实现(下面的方法都是异步读写IO,同步的话可以加一个Sync):
- 直接readFile然后writeFile
这种方式是一次性将文件读取到内存中,然后再写入目标文件。这种方式对于小文件或者内存较大的服务器来说是可行的,但是如果文件较大或者服务器内存较小,会导致内存占用过高,甚至可能导致服务器崩溃。
- 流操作,边读边写(也就是使用
fs.createReadStream
创建可读流,使用fs.createWriteStream
创建可写流,将两个流连接起来,边读边写入目标文件)
这种方法可以避免将整个文件读取到内存中,可以处理大文件,但是需要手动编写读取和写入的逻辑代码,相对较为复杂。
- 管道pipe(也就是使用
fs.createReadStream
创建可读流,使用fs.createWriteStream
创建可写流,将两个流通过管道连接起来,实现边读边写)
管道操作使用了系统底层的文件操作机制,效率较高,而且可以自动处理数据流的流速,因此可以有效地避免数据流速不匹配的问题,同时也不会一次性占用过多的内存和文件描述符资源
简单不同的使用代码吧:
const fs = require('fs')
const readStream = fs.createReadStream('./a.txt', {
start: 8, end: 20, highWaterMark: 3 // 每读取3个字节就调用一次流方法(max: 64kb)
})
readStream.on('data', (data) => { // 监听读取到的数据
console.log(data.toString())
readStream.pause() //暂停读取
setTimeout(() => readStream.resume(), 1000) // 恢复读取
})
readStream.on('open', (fd) => {console.log('文件被打开' + fd)}) //fd文件描述符,内部发送事件,不需要我们自己发送
// 还有 end , 读取到最后的位置并关闭文件
// 还有 close
const writeStream = fs.createWriteStream('./b.txt', {
flag: 'r+', start: 5
})
writeStream.write('hyhy', (err) =>{ console.log('写入完成'+err) }) //可以多次调用
writeStream.end('yhyh')// 最后写入并关闭文件
// on 方法里面有finish,回调函数可以判断文件写入完成
writeStream.on('close',()=>{console.log('文件关闭')}) // 默认不会自己关闭,需要用writeStream.close()或者end
// 管道pipe
const readStream2 = fs.createReadStream('./a.txt')
const writeStream2 = fs.createWriteStream('./b.txt')
readStream2.pipe(writeStream2)
为什么使用 koa-body
,还有其他方法吗
koa-body
支持处理多种类型的请求体,包括 json
、form
和 multipart
等。简单易配置
处理上传的图片还可以使用 koa-multer
(只处理 multipart
类型的请求体),具有更好的文件上传支持
const storage = multer.diskStorage({
destination: (req, file, callback) => {
callback(null, './uploads/')
},
filename: (req, file, callback) => {
callback(null, Date.now() + file.originalname)
}
})
// 传入文件配置,实例化文件对象
const upload = multer({ storage })
// input标签的name值
router.post("/upload", upload.single("img"), async (ctx, next) => {
ctx.body = {
code: 200,
data: {
name: ctx.req.file.filename,
url: `http://localhost:3000/${ctx.req.file.filename}`
}
}
})
怎么判断文件是否已经上传完成
-
回调函数执行前文件未上传完成,在回调函数未执行前弄个加载画面
-
可以设置一个进度条监控上传进度,可以提高用户感观:
进度条实现
对于fetch请求api,无法直接获取到上传进度:
Upload file with Fetch API in Javascript and show progress - Stack Overflow
即 fetch API 并不支持上传文件的进度条,因为它直接暴露了浏览器的网络层,不对请求的数据进行处理。但是:
这里我们直接用xhr实现
xhr.upload.onprogress 可以监听到数据变化
可以用h5新增的标签 progress 实现进度条效果
对于
progress.value
的修改是响应式的,不需要直接操作dom
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="file" id="fileInput">
<progress id="progressBar" max="100" value="0"></progress>
<script>
fileInput.addEventListener('change', async () => {
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3000/upload', true);
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
var percentComplete = (e.loaded / e.total) * 100;
progressBar2.value = percentComplete;
}
};
xhr.onload = function() {
if (this.status == 200) {
console.log('Upload complete');
}
};
var formData = new FormData();
formData.append('file', fileInput.files[0]);
xhr.send(formData);
})
</script>
</body>
</html>
文件上传安全性
- 文件类型:判断上传的文件类型是否合法,避免上传恶意脚本文件等危险文件。
- 文件大小:限制上传文件的大小,避免上传过大的文件导致服务器负担过大。
- 文件名:避免上传含有特殊字符、空格等可能引起问题的文件名。
如何确定文件上传完整性
可以在文件读写完成后将拷贝文件的大小 size
与前端请求发送的文件大小 size
进行比对,如果相等则返回200状态码,如果不相等则返回4XX表示上传中数据丢失,并要求前端重新上传。
如何确保文件上传过程中不会被篡改呢
可以使用MD5算法计算文件的哈希值,然后将哈希值与目标文件的哈希值进行比较,以确保文件的完整性。
MD5是一种不可逆的哈希算法,可以将任意长度的数据转换为固定长度的哈希值,通常是128位。在文件上传中,用MD5(或其他哈希算法)可以计算出文件的数字指纹,可以用于验证文件是否一致。
在文件上传中使用MD5可以提高文件的安全性和完整性,防止文件被篡改或者损坏。
分块文件上传了解过吗?原理是什么?
使用流可以有效地减少内存的消耗和提高文件上传的速度,但是流的传输仍然是一次性的,如果上传的文件过大,仍然会对服务器的内存和网络带宽造成压力。因此,为了进一步提高文件上传的效率和稳定性,可以结合分块文件上传的方式。
分块文件上传是将大文件切分成多个小块进行上传,这些小块独立上传并验证,上传失败可以重新上传,上传成功后再进行合并。这样可以避免一次性上传大文件时出现的内存不足和网络传输失败等问题,同时也能够有效地提高上传的效率和稳定性。
前端如何实现下载服务端的文件
后端直接返回一个url地址,前端直接访问就可以下载(生成a标签模拟点击下载)。
fetch('/download').then(response => {
const filename = response.headers.get('Content-Disposition').split('=')[1];
response.blob().then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
});
});
完结撒花
转载自:https://juejin.cn/post/7211861991533805625