Skip to content

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指定扩展名.pdf
.jpg,.png多个扩展名.jpg, .png
application/pdfMIME 类型PDF
text/csvMIME 类型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>
属性类型说明
namestring文件名(含扩展名)
sizenumber文件大小(字节)
typestringMIME 类型
lastModifiednumber最后修改时间戳
lastModifiedDateDate最后修改日期(已废弃)

文件大小限制

浏览器原生不提供文件大小限制属性,需要通过 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}">&times;</button>
                `;
                fileList.appendChild(div);
            });

            // 删除按钮
            fileList.querySelectorAll('.remove').forEach(btn => {
                btn.addEventListener('click', function () {
                    selectedFiles.splice(parseInt(this.dataset.index), 1);
                    renderFileList();
                });
            });
        }
    </script>
</body>
</html>

注意事项

安全限制

  1. 不能预设文件路径value 属性是只读的,无法通过 JavaScript 设置文件路径
  2. 只能手动选择:不能程序化地触发文件选择
  3. 路径信息隐藏:出于安全考虑,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" 是文件上传失败最常见的原因之一。没有该属性,文件不会包含在请求中。

最佳实践

  1. 始终设置 enctype:包含 file 输入的表单必须声明 enctype="multipart/form-data"
  2. 客户端 + 服务端双重验证:验证文件类型和大小
  3. 提供文件预览:对于图片,使用 FileReader API 在上传前预览
  4. 支持拖拽上传:提升用户体验,特别是桌面端
  5. 显示上传进度:使用 XMLHttpRequest 或 fetch API 配合 progress 事件
  6. 合理设置 accept:缩小可选文件范围,减少用户误选
  7. 限制文件数量:多文件上传时限制最大数量

下一节

继续学习:hidden 隐藏字段

参考链接