JavaScript 读取及写入本地文件

概述

在纯前端 javaScript 中,浏览器环境压根没有直接提供操作文件系统的能力。它不能像 node.js 那样使用 fs 模块来删除或者创建文件。 针对于浏览器出于安全性的考虑,不允许网页随意访问用户的文件系统,以防止潜在的恶意行为。

浏览器确实提供了一些有限的文件操作能力,主要是通过以下几种方式:

文件上传和下载:
文件上传: 可以通过 <input type=“file”> 元素让用户选择文件,然后通过 JavaScript 读取文件内容。
文件下载: 可以通过创建 Blob 对象和使用 a 标签的 download 属性来触发文件下载。

File API

File System Access API
File System Access API 是现代浏览器(主要是在 Chromium 内核的浏览器)引入的一种新 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">
<button id="processButton">Process and Download</button>

<script>
    document.getElementById('processButton').addEventListener('click', function () {
        const fileInput = document.getElementById('fileInput');
        const file = fileInput.files[0];

        if (file) {
            const reader = new FileReader();

            reader.onload = function (e) {
                // 读取文件内容
                let content = e.target.result;
                console.log(content);
            };

            // 开始读取文件
            reader.readAsText(file);
        } else {
            alert('Please select a file first!');
        }
    });
</script>
</body>
</html>

HTML 部分:

  • 创建了一个文件输入框 (<input type=“file”>) 让用户选择文件。

JavaScript 部分:

  • 创建了一个 FileReader 对象来读取选中的文件。
  • 使用reader.onload 指定成功读取文件时要做什么。
  • 使用 reader.readAsText(file)开始以文本形式读取文件。

(二)读取大文件

在上面的代码中,文件的读取是通过 FileReaderreadAsText() 方法完成的。这个方法确实会一次性将整个文件内容加载到内存中。对于小型文件来说这没有问题,但如果文件非常大,可能会导致内存占用过高,影响性能,甚至导致页面崩溃。

1,分片读取

使用 FileReaderreadAsArrayBuffer() 方法,然后使用 Blobslice() 方法来分块读取文件。

<!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">
<button id="processButton">Process</button>

<script>
    document.getElementById('processButton').addEventListener('click', function () {
        const fileInput = document.getElementById('fileInput');
        const file = fileInput.files[0];

        if (file) {
            const CHUNK_SIZE = 1024 * 1024; // 1MB 分块大小
            let offset = 0;

            // 递归读取文件的函数
            function readNextChunk() {
                // 检查是否已经读取到文件末尾
                if (offset >= file.size) {
                    console.log("File processing complete.");
                    return;
                }

                // 读取当前块
                const chunk = file.slice(offset, offset + CHUNK_SIZE);
                const reader = new FileReader();

                reader.onload = function (e) {
                    // 处理当前块的数据
                    let content = e.target.result;
                    console.log(`Processing chunk from ${offset} to ${offset + CHUNK_SIZE}`);
                    console.log(content); // 此处可以进行更复杂的处理

                    // 更新偏移量,并读取下一块
                    offset += CHUNK_SIZE;
                    readNextChunk();
                };

                reader.onerror = function (e) {
                    console.error("Error reading file chunk:", e);
                };

                // 开始读取当前块
                reader.readAsText(chunk);
            }

            // 开始读取第一个块
            readNextChunk();
        } else {
            alert('Please select a file first!');
        }
    });
</script>
</body>
</html>

由于文件读取是异步操作,递归调用 readNextChunk() 在每块数据处理完成后继续下一块处理。

2,使用 stream

使用 File API 的 stream() 方法(在较新的浏览器中支持),这允许你以流的方式读取文件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Stream Read File</title>
</head>
<body>
<input type="file" id="fileInput">
<button id="processButton">Process File Stream</button>

<script>
    document.getElementById('processButton').addEventListener('click', async function () {
        const fileInput = document.getElementById('fileInput');
        const file = fileInput.files[0];

        if (file) {
            const stream = file.stream();
            const reader = stream.getReader();

            // 读取流数据
            async function read() {
                let result;
                while (!(result = await reader.read()).done) {
                    const chunk = result.value; // Uint8Array
                    const textChunk = new TextDecoder().decode(chunk); // 转换为文本

                    console.log(textChunk); // 处理数据块
                }

                console.log("File processing complete.");
            }

            // 开始读取
            read().catch(error => console.error("Stream read error:", error));
        } else {
            alert('Please select a file first!');
        }
    });
</script>
</body>
</html>
  • file.stream(): 这是 File 对象的新方法,返回一个 ReadableStream,用于读取文件内容。

  • stream.getReader(): 通过调用 stream.getReader() 获取流的读取器,返回ReadableStreamDefaultReader 对象。

  • reader.read(): 每次调用 reader .read() 方法,读取流中的一个块数据,返回一个 Promise,该 Promise 解析为一个对象,包含 donevalue 属性。

       done: 如果为 true,表示流已读取完毕。 
       
       value: 当前读取的数据块,以 Uint8Array 的形式返回。
    
  • TextDecoder: 用于将 Uint8Array 数据块转换为可读的文本。对于非文本数据,可以根据需要进行其他处理。

  • while 循环: 通过 while 循环不断读取文件流,直到流结束。

  • 通过 async/awaitPromises 实现简洁的异步文件读取逻辑。

(三)注意

1,安全性问题

问题: 浏览器出于安全考虑,限制了对用户文件系统的直接访问,以防止恶意脚本未经用户同意访问敏感文件或数据。前端代码只能通过用户明确选择的方式访问文件,比如通过 <input type="file">File System Access API

处理方法:

  • 用户明确选择: 必须通过文件选择对话框(如 <input type="file">)让用户主动选择文件,而不是让脚本直接访问。
    <input type="file" id="fileInput">
    
  • 文件处理的权限: 使用 File System Access API(如 showOpenFilePicker())时,浏览器会明确向用户请求权限。确保只在必要时请求最少的权限。
async function selectFile() {
  const [fileHandle] = await window.showOpenFilePicker();
  const file = await fileHandle.getFile();
  console.log(file.name);
}
  • 保持权限范围最小化: 只请求需要的文件或目录,不尝试访问整个文件系统。限制操作的范围,例如只允许读取,避免写入或删除操作。
2,隐私问题

问题: 用户文件可能包含敏感信息,如个人数据、财务信息等。前端读取文件时,必须确保用户的隐私不被泄露或滥用。

处理方法:

  • 透明度: 明确告知用户文件将被读取的内容和目的,避免在用户不知情的情况下读取数据。
  • 本地处理: 尽量在本地处理文件内容,避免将数据上传到服务器或发送到第三方服务,除非获得用户明确同意。
    const reader = new FileReader();
    reader.onload = function(e) {
      const content = e.target.result;
      // 只在本地处理数据
    };
    reader.readAsText(file);
    
  • 数据清理: 如果需要将文件内容传输到服务器,确保对敏感数据进行加密,并在处理完毕后清理不再需要的数据。
3,性能问题

题: 在前端处理大文件时,可能会导致浏览器内存占用过高或卡顿,影响用户体验。

处理方法:

  • 分块处理: 对于大文件,使用 File APIslice() 方法或 stream() 方法将文件分块读取,逐步处理文件内容,避免一次性将整个文件加载到内存中。
    const CHUNK_SIZE = 1024 * 1024; // 1MB
    let offset = 0;
    function readChunk(file) {
      const chunk = file.slice(offset, offset + CHUNK_SIZE);
      const reader = new FileReader();
      reader.onload = function(e) {
        const content = e.target.result;
        console.log(content); // 处理数据块
        offset += CHUNK_SIZE;
        if (offset < file.size) {
          readChunk(file); // 继续读取下一块
        }
      };
      reader.readAsText(chunk);
    }
    
  • 异步操作: 使用 async/awaitPromises 处理文件读取,以避免阻塞主线程,确保页面保持响应性。
4,兼容性问题

并非所有浏览器都支持最新的 File System Access API 或某些高级文件处理功能。需要确保代码在多个浏览器中都能正常工作,或者提供合理的回退机制。

问题: 并非所有浏览器都支持最新的 File System Access API 或某些高级文件处理功能。需要确保代码在多个浏览器中都能正常工作,或者提供合理的回退机制。

处理方法:

  • Feature Detection: 在使用某些文件 API 之前,检查浏览器是否支持该功能。使用 if 语句检查是否存在特定 API。
    if (window.showOpenFilePicker) {
      // 使用 File System Access API
    } else {
      // 回退到 <input type="file">
    }
    
5,用户体验问题

问题: 前端文件操作通常涉及用户选择文件、上传文件、下载文件等操作,良好的用户体验可以提升用户的满意度。

处理方法:

  • 进度指示: 在处理大文件时,显示进度指示器(如进度条),让用户了解文件处理进度,避免用户感觉应用卡死。

    <progress id="progressBar" value="0" max="100"></progress>	
    // 在读取文件块时更新进度条
    progressBar.value = (offset / file.size) * 100;
    
  • 错误处理: 提供友好的错误提示和处理机制,帮助用户理解问题并采取行动(如重新选择文件)。

    reader.onerror = function(e) {
      alert('Error reading file: ' + e.target.error.message);
    };
    
  • 反馈和确认: 当文件操作成功完成时,给用户反馈,例如提示文件处理完毕,或确认下载已完成。

6,权限管理问题

问题: 文件操作可能涉及权限问题,例如通过 File System Access API 访问文件系统时,权限可能会被撤销。

处理方法:

  • 权限检查: 每次操作前,检查是否仍有权限访问文件或目录。如果权限被撤销,提示用户重新授权。
    const permission = await fileHandle.queryPermission();
    if (permission !== 'granted') {
      // 提示用户重新授权
    }
    
  • 权限请求: 如果没有权限,可以使用 requestPermission() 方法主动请求权限。
    const permission = await fileHandle.requestPermission();
    if (permission === 'granted') {
      // 执行文件操作
    }
    
7,文件类型和内容验证

问题: 用户可能会选择错误类型的文件,或上传包含恶意内容的文件。

处理方法:

  • 文件类型过滤: 使用 <input type="file"> 元素的 accept 属性限制用户选择的文件类型。例如,限制只选择 .txt 文件。

    <input type="file" accept=".txt">
    
  • 内容验证: 在处理文件内容之前,验证文件的实际内容格式。例如,如果文件是 JSON 格式,可以尝试解析内容并捕获错误。

    try {
      const data = JSON.parse(fileContent);
    } catch (e) {
      alert('Invalid JSON format');
    }
     
    
8,文件大小限制

问题: 处理非常大的文件可能会导致内存溢出或性能问题。

处理方法:

  • 限制文件大小: 在前端代码中设置文件大小限制,并在用户选择文件时进行检查。如果文件过大,给出提示。
    const MAX_SIZE = 10 * 1024 * 1024; // 10MB
    if (file.size > MAX_SIZE) {
      alert('File is too large!');
      return;
    }
    

这里举个例子????:

<!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" accept=".pdf">
<button id="processButton">Process</button>
<progress id="progressBar" value="0" max="100" style="display: none;"></progress>
<p id="percentage">0%</p>
<p id="statusMessage"></p>
<script>
    document.getElementById('processButton').addEventListener('click', function () {
        const fileInput = document.getElementById('fileInput');
        const file = fileInput.files[0];
        const MAX_SIZE = 300 * 1024 * 1024; // 最大300MB
        const progressBar = document.getElementById('progressBar');
        const percentageDisplay = document.getElementById('percentage');
        const statusMessage = document.getElementById('statusMessage');
        // 重置状态
        progressBar.style.display = 'none';
        progressBar.value = 0;
        percentageDisplay.textContent = '0%';
        statusMessage.textContent = '';
        // 检查是否选择了文件
        if (!file) {
            alert('Please select a file first!');
            return;
        }
        // 检查文件大小
        if (file.size > MAX_SIZE) {
            alert('File is too large! Please select a file under 300MB.');
            return;
        }
        console.log(
上一篇:华为机试HJ26 字符串排序


下一篇:开源 - Ideal库 - 常用时间转换扩展方法(一)