本文介绍浏览器与服务器之间文件传输的常见方式,涵盖文件的获取、上传、下载全流程,并附带代码示例。
1 浏览器获取用户本地文件
在浏览器中根据不同场景,有多种获取文件的方式。
1.1 点击上传
通过点击文件表单实现上传,最基础、兼容性最好的方式。
<input type="file" id="file" /> <script> const file = document.getElementById('file') file.addEventListener('change', (e) => { const file = e.target.files[0] console.log('🚀 ~ file -->', file) }) </script><input type="file" id="dir" webkitdirectory multiple /> <script> const input = document.getElementById('dir'); input.addEventListener('change', (e) => { const files = e.target.files; console.log('🚀 ~ files -->', files) }); </script>1.2 拖拽上传
直接拖动文件到页面,交互体验好,符合直觉,适合批量文件。
<div id="drop" style="width:300px;height:200px;border:2px dashed #999;"> Drop files here </div> <script> const drop = document.getElementById('drop'); drop.addEventListener('dragover', (e) => { e.preventDefault(); }); drop.addEventListener('drop', (e) => { e.preventDefault(); const files = e.dataTransfer.files; console.log('🚀 ~ files -->', files) }); </script>1.3 粘贴上传
将剪切板文件粘贴到页面中,交互体验好,效率高,特别适合上传临时截图。
<p>在此页面 Ctrl / Cmd + V 粘贴文件</p> <script> document.addEventListener('paste', (e) => { const items = e.clipboardData.items; for (const item of items) { if (item.kind === 'file') { const file = item.getAsFile(); console.log('🚀 ~ file -->', file) } } }); </script>1.4 通过媒体设备获取资源
适用于需要捕获摄像头或麦克风媒体资源时的特殊场景。
<button id="btnOpen">打开摄像头</button> <button id="btnShot">截图</button> <video id="video" autoplay playsinline muted></video> <img id="preview" alt="" /> <script> const btnOpen = document.getElementById('btnOpen'); const btnShot = document.getElementById('btnShot'); const video = document.getElementById('video'); const preview = document.getElementById('preview'); let stream = null; btnOpen.addEventListener('click', async () => { stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); video.srcObject = stream; }); btnShot.addEventListener('click', async () => { if (!stream) throw new Error('Camera not started'); const track = stream.getVideoTracks()[0]; const imageCapture = new ImageCapture(track); const blob = await imageCapture.takePhoto(); const url = URL.createObjectURL(blob); preview.src = url; console.log('🚀 ~ file -->', new File( [blob], `screenshot-${Date.now()}.png`, { type: 'image/png' } )) }); </script>1.5 File System Access API
非标准的实验性接口,功能强大,支持读写。
<button id="openFile">打开文件</button> <button id="openFiles">打开多个文件</button> <button id="openDir">打开目录</button> <script> const buttonFile = document.getElementById('openFile'); buttonFile.addEventListener('click', async () => { const [fileHandle] = await window.showOpenFilePicker(); const file = await fileHandle.getFile(); console.log('🚀 ~ file -->', file) }); const buttonFiles = document.getElementById('openFiles'); buttonFiles.addEventListener('click', async () => { const fileHandles = await window.showOpenFilePicker({ multiple: true, }); const files = await Promise.all(fileHandles.map(fileHandle => fileHandle.getFile())); console.log('🚀 ~ files -->', files) }); const buttonDir = document.getElementById('openDir'); buttonDir.addEventListener('click', async () => { const dir = await window.showDirectoryPicker(); const files = []; for await (const entry of dir.values()) { if (entry.kind === 'file') { files.push(await entry.getFile()); } } console.log('🚀 ~ files -->', files) }); </script>2 浏览器发送文件
拿到File之后,可以使用fetch发送,如果需要显示进度可以使用XMLHttpRequest。
请求体通常使用multipart/form-data编码格式,或者直接上传二进制文件数据。
Content-Type: multipart/form-dataContent-Type: application/octet-streamContent-Type: [mimetype]
2.1 FormData
使用FormData作为请求体,最通用的方式,适配大多数后端框架与对象存储直传。
const fd = new FormData() fd.append('biz', 'avatar') fd.append('file', file) const res = await fetch('/api/upload', { method: 'POST', body: fd, }) if (res.ok) { const data = await res.json() console.log('🚀 ~ data -->', data) }浏览器会自动将请求编码为multipart/form-data,并在请求头中设置合适的 boundary。
Content-Length 123456 Content-Type multipart/form-data; boundary=----WebKitFormBoundaryA1B2C3D4 ------WebKitFormBoundaryA1B2C3D4 Content-Disposition: form-data; name="biz" avatar ------WebKitFormBoundaryA1B2C3D4 Content-Disposition: form-data; name="file"; filename="avatar.jpg" Content-Type: image/jpeg [binary content of JPG...] ------WebKitFormBoundaryA1B2C3D4--2.2 二进制流
直接上传裸文件数据,实现简单,服务端无需解析 multipart,直接写入文件。
const res = await fetch('/api/upload', { method: 'POST', headers: { 'Content-Type': file.type || 'application/octet-stream', 'X-Filename': encodeURIComponent(file.name), }, body: file, }) if (res.ok) { const data = await res.json() console.log('🚀 ~ data -->', data) }Content-Length 123456 Content-Type image/jpeg X-Filename avatar.jpg3 浏览器下载文件
下载文件主要有两大类:下载服务端资源,或前端生成/操作文件数据。
3.1 基于服务端资源
服务端提供文件下载链接:
Content-Type: image/jpeg Content-Disposition: attachment; filename="avatar.jpg" Transfer-Encoding: chunked前端可以基于静态<a>标签,动态创建<a>标签,或者通过导航window.location/window.open触发下载:
<
<a href="/api/file-stream">下载</a> <button id="download-stream">按钮下载</button> <button id="nav-stream">导航下载</button> <script> document.getElementById('download-stream').addEventListener('click', () => { const a = document.createElement('a') a.download = 'avatar.jpg' a.click() }) document.getElementById('nav-stream').addEventListener('click', () => { window.location.href = '/api/file-stream' }) </script>如果服务端没有返回Content-Disposition,浏览器需要通过<a>标签配合download属性下载文件。
<a href="/api/file-stream" download="avatar.jpg">下载</a>如果服务端有通过Content-Disposition指定文件名,浏览器通过download属性配置的文件名无效。
💡浏览器导航文件链接大致过程
- 解析响应头
- 根据
Content-Type判断 MIME 类型,决定浏览器如何处理文件 - 根据
Content-Disposition决定显示还是下载inline→ 默认值,在页面中显示文件内容attachment→ 下载文件
- 下载文件时会根据
Content-Length决定是否显示进度条 - 接收响应体并写入内存或文件
3.2 基于二进制数据
适用于前端生成文件或者导出 canvas 等场景。
<button id="download">下载文本</button> <button id="downloadCanvas">下载 canvas 图片</button> <canvas id="canvas" width="200" height="200" style="border: 1px solid #ccc"></canvas> <script> document.getElementById('download').addEventListener('click', () => { const blob = new Blob(['Hello world'], { type: 'text/plain' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'hello.txt' a.click() URL.revokeObjectURL(url) }) const canvas = document.getElementById('canvas') const ctx = canvas.getContext('2d') ctx.fillStyle = '#4a90d9' ctx.fillRect(20, 20, 160, 160) ctx.fillStyle = '#fff' ctx.font = '20px sans-serif' ctx.fillText('Canvas', 60, 105) document.getElementById('downloadCanvas').addEventListener('click', () => { canvas.toBlob((blob) => { const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'canvas.png' a.click() URL.revokeObjectURL(url) }, 'image/png') }) </script>3.3 基于 data URL
适用于极小文本数据,特定场景有用。
<button id="download">下载 SVG</button> <script> const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"> <rect fill="#4a90d9" width="200" height="200"/> <text x="100" y="110" text-anchor="middle" fill="#fff" font-size="24">Hello SVG</text> </svg>` document.getElementById('download').addEventListener('click', () => { const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) const a = document.createElement('a') a.href = dataUrl a.download = 'hello.svg' a.click() }) </script>3.4 File System Access API
非标准的实验性接口,直接保存到指定位置。
<button id="download">下载文本</button> <script> document.getElementById('download').addEventListener('click', async () => { const handle = await window.showSaveFilePicker({ suggestedName: 'data.txt', }) const writable = await handle.createWritable() await writable.write('Hello World!') await writable.close() }) </script>4 服务端返回文件NodeJS一般可以通过Buffer或Stream的方式返回文件。
4.1 非流式下载(buffered download)
同步阻塞,服务器将整个文件读入内存。
ctx.body = fs.readFileSync('girl.jpg')服务端会返回Content-Length,浏览器可以据此显示下载进度。
4.2 流式下载(streaming download)
异步文件流,分块读取,适合大文件。
ctx.body = fs.createReadStream('girl.jpg')流式传输不适合使用Content-Length,所以不需要返回。
💡流式传输时 HTTP/1.1 和 HTTP/2 区别
HTTP/1.1 默认使用分块传输Transfer-Encoding: chunked。因为 HTTP/1.1 使用纯文本 + 字节流的格式实现流式传输,浏览器要么提前知道Content-Length,要么在服务器主动关闭连接时才算结束。当响应长度“不可能提前知道”时,就只能靠chunked解决:遇到 chunk-size 为 0 时即整个响应结束。
HTTP/2 的数据帧机制知道什么时候传输结束,因此不支持chunked。
📝 总结
本文介绍了浏览器与服务器之间文件传输的完整流程:
- 获取文件:支持表单上传、拖拽、粘贴、设备采集和 File System Access API 等多种方式
- 发送文件:FormData 最通用,二进制流更高效
- 下载文件:服务器链接、Blob 对象、data URL 和 File System Access API 各有适用场景
- 服务端返回:小文件用 Buffer,大文件用 Stream
选择合适的文件传输方案需要综合考虑:文件大小、用户体验、浏览器兼容性、服务器资源等因素。
原文: https://juejin.cn/post/75962309