file 文件上传
<input type="file"> 是 HTML 中用于文件上传的控件,允许用户从本地文件系统中选择一个或多个文件。通过 accept 属性可以限制文件类型,通过 multiple 属性支持多文件选择,是 Web 应用中实现文件上传功能的核心控件。
前置知识
阅读本节前,建议先了解:color 颜色选择器
基础概念
什么是 file 输入
type="file" 输入框渲染为一个"选择文件"按钮,点击后打开系统文件对话框。用户选择文件后,文件内容会包含在表单提交中。主要特点:
- 系统文件对话框:调用操作系统的文件选择窗口
- 文件类型限制:通过
accept属性过滤可选文件类型 - 多文件支持:通过
multiple属性允许选择多个文件 - 本地预览:通过 JavaScript 的 FileReader API 实现本地预览
- 安全沙箱:浏览器对文件访问有严格的安全限制
浏览器支持
| 浏览器 | 支持情况 | 说明 |
|---|---|---|
| Chrome | ✅ 完整支持 | 支持所有特性 |
| Firefox | ✅ 完整支持 | 支持所有特性 |
| Safari | ✅ 支持 | 部分 MIME 类型可能不一致 |
| Edge | ✅ 完整支持 | 支持所有特性 |
| Opera | ✅ 完整支持 | 支持所有特性 |
表单编码要求
包含 file 输入的表单必须设置 enctype="multipart/form-data",否则文件不会随表单提交:
html
<form method="post" enctype="multipart/form-data">
<input type="file" name="upload">
<button type="submit">上传</button>
</form>语法
基本用法
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>file 文件上传示例</title>
</head>
<body>
<h2>上传文件</h2>
<form method="post" enctype="multipart/form-data" action="/upload">
<label for="avatar">选择头像:</label>
<input type="file" id="avatar" name="avatar">
<button type="submit">上传</button>
</form>
</body>
</html>accept 限制文件类型
使用 accept 属性限制用户可以选择的文件类型:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>accept 文件类型限制</title>
</head>
<body>
<h2>文件类型限制</h2>
<form method="post" enctype="multipart/form-data">
<!-- 只允许图片 -->
<div>
<label for="photo">选择图片:</label>
<input type="file" id="photo" name="photo" accept="image/*">
</div>
<!-- 只允许 PDF 文件 -->
<div>
<label for="document">选择 PDF:</label>
<input type="file" id="document" name="document" accept=".pdf,application/pdf">
</div>
<!-- 允许视频和音频 -->
<div>
<label for="media">选择媒体:</label>
<input type="file" id="media" name="media" accept="video/*,audio/*">
</div>
<!-- 允许特定图片格式 -->
<div>
<label for="img">选择图片(JPG/PNG):</label>
<input type="file" id="img" name="img" accept=".jpg,.jpeg,.png">
</div>
<button type="submit">上传</button>
</form>
</body>
</html>accept 值类型对照表
| accept 值 | 说明 | 示例 |
|---|---|---|
image/* | 所有图片类型 | .jpg, .png, .gif, .webp |
audio/* | 所有音频类型 | .mp3, .wav, .ogg |
video/* | 所有视频类型 | .mp4, .webm, .avi |
.pdf | 指定扩展名 | |
.jpg,.png | 多个扩展名 | .jpg, .png |
application/pdf | MIME 类型 | |
text/csv | MIME 类型 | CSV |
multiple 多文件选择
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>multiple 多文件选择</title>
</head>
<body>
<h2>上传多张图片</h2>
<form method="post" enctype="multipart/form-data">
<label for="photos">选择照片(可多选):</label>
<input type="file" id="photos" name="photos" accept="image/*" multiple>
<button type="submit">上传</button>
</form>
<p id="file-count"></p>
<ul id="file-list"></ul>
<script>
const fileInput = document.getElementById('photos');
const fileCount = document.getElementById('file-count');
const fileList = document.getElementById('file-list');
fileInput.addEventListener('change', function () {
const files = this.files; // FileList 对象
fileCount.textContent = `已选择 ${files.length} 个文件`;
fileList.innerHTML = '';
for (const file of files) {
const li = document.createElement('li');
li.textContent = `${file.name}(${(file.size / 1024).toFixed(1)} KB)`;
fileList.appendChild(li);
}
});
</script>
</body>
</html>详细说明
File 对象属性
用户选择文件后,可以通过 files 属性(FileList)访问文件信息:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>File 对象属性</title>
</head>
<body>
<h2>文件信息查看</h2>
<input type="file" id="file-info" name="file">
<div id="info" style="margin-top: 16px;"></div>
<script>
document.getElementById('file-info').addEventListener('change', function () {
const file = this.files[0]; // 获取第一个文件
if (!file) return;
const info = document.getElementById('info');
info.innerHTML = `
<table border="1" cellpadding="8">
<tr><th>属性</th><th>值</th></tr>
<tr><td>name</td><td>${file.name}</td></tr>
<tr><td>size</td><td>${file.size} 字节(${(file.size / 1024).toFixed(2)} KB)</td></tr>
<tr><td>type</td><td>${file.type || '未知'}</td></tr>
<tr><td>lastModified</td><td>${new Date(file.lastModified).toLocaleString('zh-CN')}</td></tr>
<tr><td>lastModifiedDate</td><td>${file.lastModifiedDate ? file.lastModifiedDate.toLocaleString('zh-CN') : '已废弃'}</td></tr>
</table>
`;
});
</script>
</body>
</html>| 属性 | 类型 | 说明 |
|---|---|---|
name | string | 文件名(含扩展名) |
size | number | 文件大小(字节) |
type | string | MIME 类型 |
lastModified | number | 最后修改时间戳 |
lastModifiedDate | Date | 最后修改日期(已废弃) |
文件大小限制
浏览器原生不提供文件大小限制属性,需要通过 JavaScript 验证:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>文件大小限制</title>
</head>
<body>
<h2>上传文件(最大 5MB)</h2>
<form method="post" enctype="multipart/form-data" id="upload-form">
<input type="file" id="upload" name="file">
<p id="error" style="color: red; display: none;"></p>
<button type="submit">上传</button>
</form>
<script>
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
const fileInput = document.getElementById('upload');
const error = document.getElementById('error');
const form = document.getElementById('upload-form');
fileInput.addEventListener('change', function () {
const file = this.files[0];
if (file && file.size > MAX_SIZE) {
error.textContent = `文件过大!当前 ${formatSize(file.size)},最大允许 ${formatSize(MAX_SIZE)}`;
error.style.display = 'block';
this.value = ''; // 清空选择
} else {
error.style.display = 'none';
}
});
form.addEventListener('submit', function (e) {
const file = fileInput.files[0];
if (file && file.size > MAX_SIZE) {
e.preventDefault();
alert('文件大小超过限制');
}
});
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
</script>
</body>
</html>图片本地预览
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图片预览</title>
<style>
.preview-container {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 16px;
}
.preview-item {
width: 150px;
height: 150px;
border: 2px solid #ddd;
border-radius: 8px;
overflow: hidden;
position: relative;
}
.preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-item .size {
position: absolute;
bottom: 4px;
right: 4px;
background: rgba(0,0,0,0.6);
color: #fff;
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
}
</style>
</head>
<body>
<h2>图片上传预览</h2>
<input type="file" id="images" accept="image/*" multiple>
<div class="preview-container" id="previews"></div>
<script>
document.getElementById('images').addEventListener('change', function () {
const container = document.getElementById('previews');
container.innerHTML = '';
for (const file of this.files) {
if (!file.type.startsWith('image/')) continue;
const item = document.createElement('div');
item.className = 'preview-item';
const img = document.createElement('img');
const reader = new FileReader();
reader.onload = function (e) {
img.src = e.target.result;
};
reader.readAsDataURL(file);
const sizeLabel = document.createElement('span');
sizeLabel.className = 'size';
sizeLabel.textContent = (file.size / 1024).toFixed(0) + ' KB';
item.appendChild(img);
item.appendChild(sizeLabel);
container.appendChild(item);
}
});
</script>
</body>
</html>拖拽上传区域
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>拖拽上传</title>
<style>
.drop-zone {
width: 400px;
height: 250px;
border: 3px dashed #ccc;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
color: #999;
}
.drop-zone:hover,
.drop-zone.dragover {
border-color: #4a90d9;
background: #f0f7ff;
color: #4a90d9;
}
.drop-zone input[type="file"] { display: none; }
</style>
</head>
<body>
<h2>拖拽上传文件</h2>
<div class="drop-zone" id="drop-zone">
<p style="font-size: 32px; margin: 0;">+</p>
<p>拖拽文件到此处,或点击选择文件</p>
<input type="file" id="file-input" multiple>
</div>
<p id="status"></p>
<script>
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const status = document.getElementById('status');
// 点击触发文件选择
dropZone.addEventListener('click', () => fileInput.click());
// 拖拽事件
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', () => {
handleFiles(fileInput.files);
});
function handleFiles(files) {
const fileNames = Array.from(files).map(f => f.name).join(', ');
status.textContent = `已选择 ${files.length} 个文件:${fileNames}`;
}
</script>
</body>
</html>实战示例
完整的文件上传表单
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>完整文件上传</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
.upload-area {
border: 2px dashed #ccc;
border-radius: 12px;
padding: 24px;
text-align: center;
margin-bottom: 16px;
}
.upload-area.dragover { border-color: #4a90d9; background: #f0f7ff; }
input[type="file"] { display: none; }
.btn {
display: inline-block;
padding: 10px 24px;
background: #4a90d9;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
margin-top: 8px;
}
.file-list { text-align: left; margin-top: 16px; }
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #eee;
}
.file-item .remove {
color: red;
cursor: pointer;
border: none;
background: none;
font-size: 18px;
}
.error { color: red; font-size: 14px; }
.progress-bar {
width: 100%;
height: 6px;
background: #eee;
border-radius: 3px;
margin-top: 8px;
overflow: hidden;
display: none;
}
.progress-bar .fill {
height: 100%;
background: #4a90d9;
border-radius: 3px;
width: 0%;
transition: width 0.3s;
}
</style>
</head>
<body>
<h2>文件上传</h2>
<div class="upload-area" id="upload-area">
<p>点击或拖拽文件到此区域</p>
<p class="error" id="error-msg"></p>
<input type="file" id="file-input" accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx" multiple>
<button type="button" class="btn" id="select-btn">选择文件</button>
</div>
<div class="file-list" id="file-list"></div>
<div class="progress-bar" id="progress">
<div class="fill" id="progress-fill"></div>
</div>
<script>
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
const MAX_FILES = 5;
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('file-input');
const fileList = document.getElementById('file-list');
const errorMsg = document.getElementById('error-msg');
let selectedFiles = [];
document.getElementById('select-btn').addEventListener('click', () => fileInput.click());
// 拖拽支持
uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); });
uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('dragover'));
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
addFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', () => addFiles(fileInput.files));
function addFiles(files) {
errorMsg.textContent = '';
for (const file of files) {
if (selectedFiles.length >= MAX_FILES) {
errorMsg.textContent = `最多上传 ${MAX_FILES} 个文件`;
break;
}
if (file.size > MAX_SIZE) {
errorMsg.textContent = `${file.name} 超过 10MB 限制`;
continue;
}
selectedFiles.push(file);
}
renderFileList();
}
function renderFileList() {
fileList.innerHTML = '';
selectedFiles.forEach((file, index) => {
const div = document.createElement('div');
div.className = 'file-item';
div.innerHTML = `
<span>${file.name}(${(file.size / 1024).toFixed(1)} KB)</span>
<button class="remove" data-index="${index}">×</button>
`;
fileList.appendChild(div);
});
// 删除按钮
fileList.querySelectorAll('.remove').forEach(btn => {
btn.addEventListener('click', function () {
selectedFiles.splice(parseInt(this.dataset.index), 1);
renderFileList();
});
});
}
</script>
</body>
</html>注意事项
安全限制
- 不能预设文件路径:
value属性是只读的,无法通过 JavaScript 设置文件路径 - 只能手动选择:不能程序化地触发文件选择
- 路径信息隐藏:出于安全考虑,
file.value只显示文件名,不显示完整路径
accept 仅为提示
accept 属性只是对文件对话框的过滤提示,不是强制限制。用户可以在文件对话框中选择"所有文件"来绕过限制。服务端必须再次验证文件类型:
javascript
// 客户端验证(可绕过)
const fileInput = document.getElementById('upload');
const file = fileInput.files[0];
if (file && !file.type.startsWith('image/')) {
alert('请选择图片文件');
}enctype 必须设置
忘记设置 enctype="multipart/form-data" 是文件上传失败最常见的原因之一。没有该属性,文件不会包含在请求中。
最佳实践
- 始终设置 enctype:包含 file 输入的表单必须声明
enctype="multipart/form-data" - 客户端 + 服务端双重验证:验证文件类型和大小
- 提供文件预览:对于图片,使用 FileReader API 在上传前预览
- 支持拖拽上传:提升用户体验,特别是桌面端
- 显示上传进度:使用 XMLHttpRequest 或 fetch API 配合 progress 事件
- 合理设置 accept:缩小可选文件范围,减少用户误选
- 限制文件数量:多文件上传时限制最大数量
下一节
继续学习:hidden 隐藏字段