本文将深入剖析 JavaScript 环境下文件上传的完整实现机制,涵盖前端文件选择与发送、后端接收与存储的全流程技术细节,并在文末附上完整源码。

一、文件上传的核心机制

文件上传的本质是客户端将本地文件数据通过 HTTP 协议传输到服务器端,并由服务器进行接收、处理和存储。这一过程涉及多个关键技术环节,包括客户端文件选择、数据封装与传输,以及服务器端数据解析与文件存储。

二、客户端文件选择机制

1. <input type="file"> 元素

在 Web 应用中,文件上传的起点是 HTML 的文件输入元素 <input type="file">。这个元素是用户与文件系统交互的桥梁,允许用户从本地文件系统中选择文件。

1.1 基本语法
<input type="file" id="fileInput" name="file" multiple>
  • type="file":指定输入类型为文件选择器。
  • id:为元素指定一个唯一的标识符,方便通过 JavaScript 操作。
  • name:表单提交时,文件数据的键名。
  • multiple:允许用户选择多个文件(可选属性)。
1.2 用户交互

当用户点击 <input type="file"> 元素时,浏览器会弹出一个文件选择对话框,用户可以从本地文件系统中选择一个或多个文件(如果设置了 multiple 属性)。选择文件后,文件选择器会更新显示所选文件的信息。

2. File 对象和 FileList 对象

当用户完成文件选择后,所选文件会被封装为 File 对象,并存储在 <input type="file"> 元素的 files 属性中。files 属性返回一个 FileList 对象,这是一个类数组对象,包含用户选择的所有 File 对象。

2.1 FileList 对象

FileList 对象是一个类数组对象,包含用户选择的所有文件。它提供了以下特性:

  • 类数组结构:可以通过索引访问每个文件,例如 fileInput.files[0]
  • length 属性:返回用户选择的文件数量。
2.2 File 对象

每个 File 对象代表一个用户选择的文件,包含以下属性:

  • name:文件的名称(不包含路径)。
  • size:文件的大小(以字节为单位)。
  • type:文件的 MIME 类型(如 image/jpegapplication/pdf 等)。
  • lastModified:文件最后修改的时间戳(以毫秒为单位)。

此外,File 对象还封装了文件的二进制数据,可以通过 FileReader 或其他 API 读取文件内容。

3. 示例代码

以下是一个完整的示例,展示如何使用 <input type="file"> 元素和 File 对象:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文件上传示例</title>
</head>
<body>
  <input type="file" id="fileInput" multiple>
  <button id="showFiles">显示文件信息</button>
  <div id="fileInfo"></div>

  <script>
    document.getElementById('showFiles').addEventListener('click', () => {
      const fileInput = document.getElementById('fileInput');
      const fileInfo = document.getElementById('fileInfo');
      fileInfo.innerHTML = '';

      if (fileInput.files.length === 0) {
        fileInfo.innerHTML = '<p>没有选择文件</p>';
        return;
      }

      for (let i = 0; i < fileInput.files.length; i++) {
        const file = fileInput.files[i];
        const fileDetails = `
          <p>文件 ${i + 1}:</p>
          <p>名称: ${file.name}</p>
          <p>大小: ${file.size} 字节</p>
          <p>类型: ${file.type}</p>
          <p>最后修改时间: ${new Date(file.lastModified).toLocaleString()}</p>
        `;
        fileInfo.innerHTML += fileDetails;
      }
    });
  </script>
</body>
</html>
说明
  1. HTML 部分

    • <input type="file" id="fileInput" multiple>:创建一个文件选择器,允许用户选择多个文件。
    • <button id="showFiles">显示文件信息</button>:一个按钮,点击后显示文件信息。
    • <div id="fileInfo"></div>:用于显示文件信息的容器。
  2. JavaScript 部分

    • 获取文件选择器的 files 属性,这是一个 FileList 对象。
    • 遍历 FileList,获取每个 File 对象的属性(namesizetypelastModified)。
    • 将文件信息显示在页面上。
4. File 对象的高级用法
4.1 读取文件内容

可以使用 FileReader API 读取文件内容。以下是一个示例,展示如何读取用户选择的文件内容:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>读取文件内容</title>
</head>
<body>
  <input type="file" id="fileInput">
  <div id="fileContent"></div>

  <script>
    document.getElementById('fileInput').addEventListener('change', (event) => {
      const file = event.target.files[0];
      if (!file) {
        document.getElementById('fileContent').innerHTML = '<p>没有选择文件</p>';
        return;
      }

      const reader = new FileReader();
      reader.onload = (e) => {
        document.getElementById('fileContent').innerHTML = `<p>文件内容:<br><pre>${e.target.result}</pre></p>`;
      };
      reader.readAsText(file); // 读取文件内容为文本
    });
  </script>
</body>
</html>
4.2 文件大小限制

可以在用户选择文件后,检查文件大小是否超出限制。以下是一个示例,展示如何限制文件大小:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文件大小限制</title>
</head>
<body>
  <input type="file" id="fileInput">
  <div id="message"></div>

  <script>
    document.getElementById('fileInput').addEventListener('change', (event) => {
      const file = event.target.files[0];
      const messageDiv = document.getElementById('message');
      const maxSize = 1024 * 1024; // 限制为 1MB

      if (file.size > maxSize) {
        messageDiv.innerHTML = `<p>文件大小超出限制(最大 ${maxSize / (1024 * 1024)}MB)</p>`;
      } else {
        messageDiv.innerHTML = `<p>文件大小符合要求</p>`;
      }
    });
  </script>
</body>
</html>
5. 总结
  • <input type="file"> 元素是文件上传的起点,允许用户从本地文件系统中选择文件。
  • 用户选择的文件会被封装为 File 对象,并存储在 FileList 对象中。
  • File 对象包含文件的元数据(如名称、大小、类型等),并封装了文件的二进制数据。
  • 可以通过 FileReader API 读取文件内容,还可以对文件大小进行限制。

通过这些机制,前端可以有效地处理用户选择的文件,为后续的文件上传做好准备。

三、数据封装与传输机制

由于文件数据通常较大且包含二进制内容,Web 应用中通常使用 multipart/form-data 编码类型进行传输。这种编码方式允许在单个请求中同时发送文本数据和二进制数据,且能有效处理文件的边界识别问题。在 JavaScript 中,FormData API 提供了构建 multipart/form-data 格式数据的能力。通过 FormData,开发者可以将文件对象与其他表单数据一起封装,并通过 AJAX 请求发送到服务器。

四、服务器端接收与存储机制

服务器端需要能够解析 multipart/form-data 格式的请求,提取其中的文件数据,并将其保存到文件系统或其他存储介质中。在 Node.js 环境中,Express 框架本身不具备处理此类请求的能力,需要使用专门的中间件来完成这一工作。

五、关键技术与 API 解析

1. 前端关键技术

File API

File 对象是文件上传的基础,代表用户选择的一个文件。其主要属性包括:

  • name:文件名(不包含路径)
  • size:文件大小(以字节为单位)
  • type:文件的 MIME 类型
  • lastModified:文件最后修改时间戳

FileList 对象则是 File 对象的集合,通过文件输入元素的 files 属性获取。

FormData API

FormData 是专门用于构建表单数据的 API,其核心方法包括:

  • append(name, value):向 FormData 对象添加一个键值对
  • delete(name):删除指定键名的键值对
  • get(name):获取指定键名的第一个值
  • getAll(name):获取指定键名的所有值
  • has(name):检查是否存在指定键名的键值对

在文件上传场景中,主要使用 append() 方法将 File 对象添加到 FormData 中。

Axios HTTP 客户端

Axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js 环境。其 post() 方法可以发送包含 FormData 的请求,且会自动设置正确的 Content-Type 头信息。

2. 后端关键技术

Express 框架

Express 是一个简洁而灵活的 Node.js Web 应用框架,提供了创建 Web 服务器和定义路由的能力。通过 app.post() 方法可以定义处理 POST 请求的路由处理器。

Multer 中间件

Multer 是一个用于处理 multipart/form-data 类型数据的中间件,专为文件上传设计。其主要功能包括:

  • 解析请求中的文件数据
  • 验证文件类型和大小
  • 自定义文件存储位置和名称

Multer 提供了多种存储引擎,其中 diskStorage 用于将文件存储到磁盘。diskStorage 可以配置两个关键函数:

  • destination:确定文件的保存目录
  • filename:确定文件的保存名称
cors 中间件

在前后端分离的架构中,由于浏览器的同源策略限制,跨域请求默认会被阻止。cors 中间件通过设置适当的 HTTP 头(如 Access-Control-Allow-Origin),允许来自不同源的客户端访问服务器资源。

六、完整源码实现

前端实现 (index.html)

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
  <input type="file" id="f">
  <button onclick="upload()">上传</button>

  <script>
    const upload = async () => {
      const f = document.getElementById('f');
      // 检查是否选择了文件 
      if (!f.files[0]) return alert('请选择文件');
      /*
      f.files 是文件输入框的一个属性,返回一个FileList对象,里面包含用户选择的所有文件。
      f.files[0] 表示第一个选中的文件。如果用户没有选择任何文件, f.files[0] 将是 undefined
      */
      try {
        const fd = new FormData(); // 创建一个新的FormData对象。FormData是一种用于发送表单数据的API,特别适合用于文件上传
        fd.append('file', f.files[0]); // 将用户选择的文件添加到FormData对象中。第一个参数'image'是后端接收文件时使用的字段名,第二个参数是要上传的文件对象(即用户选择的文件)
        await axios.post('http://localhost:8000/file_upload', fd);
        alert('上传成功');
      } catch (e) {
        alert('上传失败');
      }
    }
  </script>
</body>
</html>

后端实现 (server.js)

const express = require('express')
const multer = require('multer') // 这是一个用于处理 multipart/form-data 类型表单数据的中间件,特别适合用于文件上传
const cors = require('cors')
const fs = require('fs')

const app = express()

// 创建multer实例并配置, dest: './uploads/' 指定了上传文件的保存目录为当前工作目录下的uploads文件夹; upload 变量现在包含了配置好的Multer中间件,可以用于处理文件上传请求
const storage = multer.diskStorage({ //- 这是 Multer 提供的高级存储引擎配置方法;用于自定义文件存储的行为,相比于简单的 dest 配置,它提供了更多控制选项

  /*
  destination 函数
    - 用于指定上传文件的保存目录
    - 参数说明:
        - req :HTTP 请求对象
        - file :上传的文件对象
        - cb :回调函数,用于返回结果
  */
  destination: function (req, file, cb) {
    cb(null, './uploads') // 没有错误(null),将文件保存到 './uploads' 目录
  },
  filename: function (req, file, cb) {
    // 保留原始文件名和扩展名
    cb(null, file.originalname)
  }
})

// 使用配置好的storage创建multer实例
const upload = multer({ storage: storage }) // 将配置好的存储引擎应用到 multer 实例
app.use(express.static(__dirname))
app.use(cors())

// 文件上传路由
// upload.single('file') 是Multer提供的中间件,用于处理单个文件上传,其中'file'是前端表单中文件输入框的name属性值
app.post('/file_upload', upload.single('file'), (req, res) => {
  // 当文件上传成功后,Multer会将文件信息添加到 req.file 对象中
  if (req.file) res.json({ success: true })
  else res.status(400).json({ success: false })
})

// 确保uploads目录存在
if (!fs.existsSync('./uploads')) fs.mkdirSync('./uploads')

app.listen(8000, () => {
    console.log('The server is running at http://127.0.0.1:8000')
})

七、技术要点总结

  1. 前端文件选择:通过 HTML 的 <input type="file"> 元素实现,所选文件存储在 FileList 对象中。
  2. 数据封装:使用 FormData API 封装文件数据,构建 multipart/form-data 格式的请求体。
  3. 异步请求:通过 Axios 发送异步 HTTP 请求,自动处理 Content-Type 头信息。
  4. 后端解析:使用 Multer 中间件解析 multipart/form-data 格式的请求,提取文件数据。
  5. 文件存储:通过 Multer 的存储引擎配置,自定义文件的保存位置和名称。
  6. 跨域处理:使用 cors 中间件解决前后端分离架构中的跨域问题。

这种实现方式具有良好的兼容性和可扩展性,是 Web 应用中文件上传功能的标准实现方案。在实际应用中,还可以根据需求添加文件类型验证、大小限制、进度显示等功能,进一步增强用户体验和系统安全性。


希望这份教程对你有所帮助,如果有任何错误或需要进一步讨论的地方,请随时在评论区交流!

更多推荐