用JS将本地文件上传到服务器,核心思路是借助FormData封装文件数据,再通过fetch或XMLHttpRequest发异步请求,后端(如Node.js、Java)解析二进制流并落盘。实际项目里还要考虑大文件分片、进度条、断点续传和跨域限制,下面按不同场景逐个说清。
js上传文件到服务器代码示例:从基础写法讲起
先看最简洁的版本,页面里放一个文件选择框,选完自动触发上传:
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/upload', {
method: 'POST',
body: formData
});
const data = await res.json();
console.log('上传成功', data.url);
});
这段代码核心就三件事:把文件塞进FormData、用fetch发送、解析后端返回的地址。FormData会自动生成multipart/form-data请求体,别手动去设置Content-Type,边界符交给浏览器生成即可。
用XMLHttpRequest实现带进度条的上传
fetch虽然写起来爽,但它没有原生上报上传进度的接口,想要进度条,得退回XMLHttpRequest的upload.onprogress事件:
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) 100);
document.getElementById('bar').style.width = percent + '%';
}
};
xhr.onload = () => {
if (xhr.status === 200) {
console.log('上传完成');
}
};
xhr.send(formData);
业界做上传组件(比如webuploader)底层基本都是这套逻辑,区别在于封装程度:库帮你处理了并发、重试和队列,原理还是XMLHttpRequest。
多文件上传和Blob上传的拓展
- 多文件:给
<input>加上multiple属性,循环遍历e.target.files - Blob切片:
File本身就是Blob的子类,所以也能直接传二进制数据 - 上传文本:新建
new Blob([str])后塞进FormData,同样能到达服务器
javascript大文件上传解决方案:分片、断点、并发控制
大文件和大文件是两个概念。单文件超过几百MB,一次POST丢过去很容易超时或撑爆内存,业内通用的做法是把文件切块,分多次请求上传,最后在服务端拼接。
分片的核心公式
const CHUNK_SIZE = 5 1024 1024; // 每片5MB
let offset = 0;
let chunkIndex = 0;
const chunks = [];
while (offset < file.size) {
const chunk = file.slice(offset, offset + CHUNK_SIZE);
chunks.push({
index: chunkIndex,
blob: chunk
});
offset += CHUNK_SIZE;
chunkIndex++;
}
获取分片列表之后,用并发池控制上传数量。一般建议并发数控制在3到5个,发太多会打爆服务器的TCP连接,太少又体现不出分片优势。
async function uploadChunks(chunks, fileId) {
const limit = 3;
const pool = chunks.splice(0, limit).map(async (chunk) => {
const fd = new FormData();
fd.append('file', chunk.blob);
fd.append('fileId', fileId);
fd.append('index', chunk.index);
await fetch('/upload/chunk', { method: 'POST', body: fd });
});
await Promise.all(pool);
}
断点续传要解决什么问题
断点续传指的是上传过程中断后,下一次能跳过已上传的分片,方案分两层:
- 后端记录已完成的分片索引,前端上传前请求一次
/upload/status?fileId=xxx - 拿到已传列表后,前端过滤掉这些分片再继续传
有的团队用Redis存分片状态,有的直接丢数据库表里。选型取决于服务器资源,并不存在唯一标准。
秒传的实现逻辑
秒传本质上和断点续传共用一套元数据:前端算出文件的MD5(用SparkMD5),后端比对数据库,若已存在则直接返回“上传成功”,拿大文件算MD5会有CPU开销,可以切块算个特征值再合并,速度更快。
用vue axios上传文件到服务器怎么操作?对比原生写法
平时用Vue做后台管理,多半会引进axios。axios在文件上传场景带来的最大增量不是语法,而是拦截器和错误统一处理。
import axios from 'axios';
function uploadAvatar(file) {
const formData = new FormData();
formData.append('avatar', file);
return axios.post('/api/user/avatar', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 30000,
onUploadProgress: (e) => {
if (e.lengthComputable) {
this.progress = Math.round((e.loaded / e.total) 100);
}
}
});
}
对比原生fetch,axios的收益有三个显性点:
- 请求拦截器:自动在Header里带Token
- 响应拦截器:统一处理401跳转或服务器返回的业务错误码
- 取消请求:通过
AbortController或CancelToken中断上传
拖拽上传交互的写法
真正让上传体验“现代感”的是拖拽上传,事件核心是dragover和drop:
dropZone.addEventListener('dragover', (e) => {
e.preventDefault(); // 阻止浏览器默认打开文件
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
const files = Array.from(e.dataTransfer.files);
files.forEach(uploadFile);
});
拿到File对象列表后,后续处理和<input>选择的一模一样,用URL.createObjectURL(file)还能生成本地预览地址,做缩略图展示非常方便。
图片上传前压缩
手机拍的照片动辄几MB,直接传浪费流量也拖慢速度,前端可用Canvas把图片缩到最大边2000px后转成Blob:
canvas.toBlob((blob) => {
const newFile = new File([blob], file.name, { type: file.type });
upload(newFile);
}, 'image/jpeg', 0.8);
能显著降低文件体积,服务器存储成本也减少,属于性价比很高的优化手段。
node.js怎么接收前端上传的文件?后端配置参考
搜“js上传文件到服务器”的人,一半卡在前端,另一半卡在后端接不住,以Node.js生态为例,最常用的接收工具是multer。
Express + multer的样板代码
npm install express multer
const express = require('express');
const multer = require('multer');
const upload = multer({
dest: 'uploads/',
limits: { fileSize: 100 1024 1024 }
});
const app = express();
app.post('/upload', upload.single('file'), (req, res) => {
// req.file 包含文件名、大小、路径等元信息
res.json({
url: '/uploads/' + req.file.filename
});
});
app.listen(3000);
注意:upload.single('file')里的'file'必须和前端FormData.append的key完全一致,若是多文件用upload.array('file', 10)。
大文件分片在服务端的合并
后端收到分片后需要按索引合并,常用做法是先存临时目录,全部到齐后读取拼接:
const fs = require('fs');
function mergeChunks(fileId, total, dest) {
const writeStream = fs.createWriteStream(dest);
for (let i = 0; i < total; i++) {
const chunkPath = `uploads/tmp/${fileId}-${i}`;
writeStream.write(fs.readFileSync(chunkPath));
}
writeStream.end();
}
更高效的方案用Stream管道逐个写入,避免内存中同时塞入多个大对象。
生产环境不直接让Node写静态文件
实际部署时,上传目录通常由Nginx映射,请求进来后绕开Node进程直接读文件,原因很简单:Node的I/O吞吐不如Nginx高效,而且文件下载会长时间占用进程资源。
| 方案 | 服务器流量占用 | 上传速度 | 开发复杂度 |
|---|---|---|---|
| 前端直传对象存储(OSS/COS) | 不经过业务服务器 | 快(走CDN边缘) | 中 |
| 通过自家Node中转 | 全部流量走服务器 | 受带宽限制 | 低 |
| Nginx直接接收并转发 | 需做模块扩展 | 较快 | 高 |
若项目流量不大,选中转方案开发速度最快。当用户量和文件体积增长后,再迁到对象存储直传,这在业内是非常成熟的技术路径。
服务器租用价格之外,还要操心这些坑
如果你在考虑“js上传文件服务器租用价格多少”这类问题,先别急着比价。便宜服务器往往带宽也小,而上传场景最吃带宽,简米云、酷番云的轻量服务器,入门配置的年费多数在几百元区间(具体随活动波动),但流量包超出后额外计费。
选择地域时,按用户分布来:用户集中在华南,选广州节点;集中在华东,选上海或杭州节点,距离近了,网络延迟能降不少。
上传失败自查清单
- 跨域问题:服务器响应头需包含
Access-Control-Allow-Origin - 大小限制:Nginx默认
client_max_body_size只有1MB,需调整为50m或更高 - Content-Type:截获请求体确认是
multipart/form-data,不能是纯二进制流 - DNS解析:内网环境用IP直连,外网请求确认域名已备案且能正常解析
文件校验别只看扩展名
扩展名可以伪装成图片,但文件头(Magic Number)不会说谎:
- PDF文件头固定为
%PDF - PNG文件头固定为
\x89PNG - JPEG文件头固定为
\xFF\xD8\xFF
读前几个字节校验文件类型,能挡掉相当一部分杂七杂八的上传,同时将文件名改为随机字符串存盘,避免用户上传带路径或特殊字符的文件名引发安全风险。
关于js上传文件的常见问题解答
fetch和axios做文件上传,选哪个好?
fetch是浏览器内置API,零依赖,写小功能非常轻快;axios支持拦截器、取消请求和更细的进度控制,团队协作项目用axios能减少重复代码,两者底层传输能力等价,区别在生态和封装度。
大文件上传中途断网了,重传必须从零开始吗?
不需要,按上文分片方案把文件切块后,上传前先向后端查询已收到的分片索引,过滤掉已完成的部分,再把剩余分片传完,后端拼合即可。
对象存储直传和自建服务器上传,怎么权衡?
需求量不大时自建完全够用,成本是服务器带宽,文件量大且用户分布广,直传OSS/COS能把流量分散到CDN节点,提升上传速度的同时缓解源站压力,缺点是按量计费,得拉账单评估成本。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/771008.html


