• DOM & BOM – Input File, Drag & Drop File, File Reader, Blob, ArrayBuffer, File, UTF8 Encode/Decode, Download File


    前言

    之前写过 2 篇关于读写文件和二进制相关的文章 Bit, Byte, ASCII, Unicode, UTF, Base64 和 ASP.NET Core – Byte, Stream, Directory, File 基础

    不过是 ASP.NET Core 和 C# 的版本. 今天想介绍用 Browser 和 JavaScript 实现的读写文件.

    从前写的文章 Drag & Drop and File Reader 发布于 2014-12-18 (8 年前...)

    What is Blob, ArrayBuffer, File?

    Blob 相等于 FileStream, 而 ArrayBuffer 相等于 MemoryStream. 顾名思义一个是文件 (IO) 的流, 另一个是缓存 (RAM) 的流.

    File 继承了 Blob, 只是多了一些属性而已.

    Input File & Drag & Drop File

    Browser 是不可以直接访问用户的文件的. 没权限, 必须是用户在意识清楚的情况下提供给你. 

    有 2 个方式可以让用户提供文件. 

    Input File

    <input type="file" multiple />

    效果

    Input File 还支持 Drag & Drop 哦

    JavaScript

    const input = document.querySelector<HTMLInputElement>('input')!;
    input.addEventListener('input', () => {
      const files = input.files!;
      const textFile = files[0]; // File 对象
    });

    Drag & Drop File

    另一个方式是做一个 drop area.

    效果

    JavaScript

    const dropArea = document.querySelector<HTMLElement>('.drop-area')!;
    
    dropArea.addEventListener('dragover', e => e.preventDefault());
    dropArea.addEventListener('drop', e => {
      e.preventDefault();
      const files = e.dataTransfer!.files;
      const file = files[0]; // File 对象
      dropArea.querySelector('p')!.textContent = file.name;
    });

     

    Input File & Drag & Drop File (for directory)

    除了提供 multiple files, 甚至可以提供 directory (folder) 直接获取里面所有 files 哦.

    Input File

    <input type="file" webkitdirectory />

    效果

    不管 directory 里面有多少层, 它都会把所有的 files 全部放入 input 里.

    不管是 click input to chose 还是 drag & drop 去 input, 一律不支持 multiple directory (一次只能选择 1 个 directory)

    JavaScript

    const input = document.querySelector<HTMLInputElement>('input')!;
    input.addEventListener('input', () => {
      const files = Array.from(input.files!);
      console.log(files.map(f => f.webkitRelativePath)); // ['root/root-text.txt', 'root/parent/parent-text.txt', 'root/parent/child/cihld-text.txt']
    });

    通过 webkitRelativePath 可以拿到完整路径.

    Drag & Drop File

    Drag & Drop file 比 input 厉害, 它支持 multiple directory, 甚至 1 file 1个 directory 混搭也可以.

    它的 JavaScript 实现会比较复杂

    参考: Stack Overflow – Does HTML5 allow drag-drop upload of folders or a folder tree?

    const dropArea = document.querySelector<HTMLElement>('.drop-area')!;
    dropArea.addEventListener('dragover', e => e.preventDefault());
    dropArea.addEventListener('drop', async e => {
      e.preventDefault();
    
      const texts: string[] = [];
    
      // 必须先把所有 entry 拿出来, 因为 for loop 的时候会进入异步
      const fileSystemEntries = Array.from(e.dataTransfer!.items).map(item => item.webkitGetAsEntry()!);
      for (const entry of fileSystemEntries) {
        const fileEntries = await recursiveGetAllFileEntries(entry);
        console.log(fileEntries.map(e => e.fullPath)); // 相等于 webkitRelativePath
        const files = await Promise.all(fileEntries.map(e => entryToFileAsync(e)));
        texts.push(entry.isFile ? `File: ${entry.name}` : `Directory: total ${files.length} files`);
      }
    
      dropArea.querySelector('p')!.textContent = texts.join('\n');
    
      function recursiveGetAllFileEntries(entry: FileSystemEntry): Promise<FileSystemFileEntry[]> {
        return new Promise(async resolve => {
          if (entry.isFile) {
            const fileEntry = entry as FileSystemFileEntry;
            resolve([fileEntry]);
          } else {
            const directoryEntry = entry as FileSystemDirectoryEntry; // 强转成 interface
            const reader = directoryEntry.createReader();
            reader.readEntries(async entries => {
              const childFiles: FileSystemFileEntry[] = [];
              for (const childEntry of entries) {
                childFiles.push(...(await recursiveGetAllFileEntries(childEntry)));
              }
              resolve(childFiles);
            });
          }
        });
      }
    
      function entryToFileAsync(entry: FileSystemFileEntry): Promise<File> {
        return new Promise(resolve => entry.file(resolve));
      }
    });
    View Code

    有几个点要注意

    1. webkitGetAsEntry() 调用的时机

    dropArea.addEventListener('drop', e => {
      e.preventDefault();
      const items = Array.from(e.dataTransfer!.items);
      setTimeout(() => {
        console.log(items[0].webkitGetAsEntry()); // null
      }, 1000);
    });

    拿 webkitGetAsEntry 要快, 一旦 delay 了就拿不到了. 所以第一步就必须先把所以 item 的 entry 拿出来. 才一个一个 async 处理.

    2. 强转 FileSystemDirectoryEntry

    这里 directoryEntry 的 class 其实是 DirectoryEntry, 但是 TypeScript 却没有. 相关 issue: Github – Add type definitions for Files And Directories API 

    但幸好 TypeScript 有 interface FileSystemDirectoryEntry 也能用.

    3. FileSystemFileEntry.file 返回的 file, 它的 webkitRelativePath 总是 empty string.

    这点和 input file 不同, 它不会智能的写入 webkitRelativePath, 但幸好可以用 FileSystemFileEntry.fullPath 获取到和 webkitRelativePath 一样的 directory + file name.

    Read File Text

    通过 input 或者 drag & drop 我们获取到了 File 对象. 上面有提到 File 对象只是 Blob 的扩展. 我们把它当 Blob 来看就行了. 

    Blob 就是 FileStream.

    text.txt

    File.text()

    const input = document.querySelector<HTMLInputElement>('input')!;
    input.addEventListener('input', async () => {
      const textFile = input.files!.item(0)!;
      const text = await textFile.text();
      console.log(text); // Hello World
    });

    调用 text 方法就可以了, 它返回的是一个 Promise.

    FileReader.readAsText()

    另一个方法是用 FileReader (比较 old school)

    const input = document.querySelector<HTMLInputElement>('input')!;
    input.addEventListener('input', async () => {
      const textFile = input.files!.item(0)!;
      const fileReader = new FileReader();
      fileReader.addEventListener('load', () => {
        console.log(fileReader.result); // Hello World
        fileReader.abort();
      });
      fileReader.readAsText(textFile, 'utf-8'); // can specify encoding
      fileReader.readAsBinaryString;
    });

    比较常用的是 .text 方法, 毕竟返回 Promise 方便许多. 但是 .text 方法不能指定 encoding 它一定是用 utf-8.

    File to ArrayBuffer

    我们也可以把 File/Blob (FileStream) 转成 ArrayBuffer (MemoryStream).

    const input = document.querySelector<HTMLInputElement>('input')!;
    input.addEventListener('input', async () => {
      const textFile = input.files!.item(0)!; // textFile value = Hello World
      const memoryStream = await textFile.arrayBuffer();
      console.log('length', memoryStream.byteLength); // 11 bytes
    
      // 用 FileReader
      const fileReader = new FileReader();
      fileReader.addEventListener('load', () => {
        const memoryStream = fileReader.result! as ArrayBuffer;
        console.log('length', memoryStream.byteLength); // 11 bytes
        fileReader.abort();
      });
      fileReader.readAsArrayBuffer(textFile);
    });

    Read Bytes from ArrayBuffer

    ArrayBuffer 里面就是一堆的 bytes, 1 byte = 8 bit (八个二进制).

    我们看一个 C# 的例子

    var value = "";
    var utf8 = Encoding.UTF8.GetBytes(value); // 3 bytes [11100100, 10111000, 10100101]
    var utf16 = Encoding.Unicode.GetBytes(value); // 2 bytes [100101, 1001110]

    "严" 这个的 Unicode 是 100111000100101, 上面分别是它的 UTF-8 和 UTF-16 的 encode.

    我们分别保存在 2 个 file 里, utf-8.txt 和 utf-16.txt

    UTF-8

    input.addEventListener('input', async () => {
      const textFile = input.files!.item(0)!; // textFile = 严 UTF-8
      const memoryStream = await textFile.arrayBuffer();
      console.log('length', memoryStream.byteLength); // 3 bytes
      const bytes = new Uint8Array(memoryStream);
      console.log(
        'bytes',
        Array.from(bytes).map(b => b.toString(2))
      ); // ['11100100', '10111000', '10100101']
    });

    UTF-16

    input.addEventListener('input', async () => {
      const textFile = input.files!.item(0)!; // textFile = 严 UTF-16
      const memoryStream = await textFile.arrayBuffer();
      console.log('length', memoryStream.byteLength); // 2 bytes
      const bytes = new Uint8Array(memoryStream);
      console.log(
        'bytes',
        Array.from(bytes).map(b => b.toString(2))
      ); // ['100101', '1001110']
    
      const bytes2 = new Uint16Array(memoryStream);
      console.log(
        'bytes',
        Array.from(bytes2).map(b => b.toString(2))
      ); // ['100111000100101']
    });

    memoryStream.byteLength 是以 1 byte = 8 bits 计算的.

    如果是用 Uinit16Array 表示 array 里 1 item 可以记入 16 bits 哦

    p.s. JavaScript 一般都是用 UTF-8 而已, 其它尽量不要用, 顺风水.

    TextDecoder & TextEncoder

    参考: Stack Overflow – Converting between strings and ArrayBuffers

    随便介绍一下 encode 和 decode UTF-8

    const value = '严';
    const textEncoder = new TextEncoder();
    const bytes = textEncoder.encode(value);
    console.log(Array.from(bytes).map(b => b.toString(2))); // ['11100100', '10111000', '10100101']
    const textDecoder = new TextDecoder();
    console.log(textDecoder.decode(bytes)); //

    JavaScript 没有 build-in 的方法 decode to UTF-16 哦.

    Create Blob and Download as File

    参考: Stack Overflow – Download File Using JavaScript/jQuery

    document.querySelector('button')!.addEventListener('click', () => {
      const value = 'Hello World 我爱她';
      const textEncoder = new TextEncoder();
      const bytes = textEncoder.encode(value);
      const blob = new Blob([bytes], {
        type: 'text/plain',
      });
    
      // 或者做成 file 也可以
      // const file = new File([bytes], 'text.txt', {
      //   type: 'text/plain',
      // });
    
      const blobUrl = window.URL.createObjectURL(blob);
      const anchor = document.createElement('a');
      anchor.href = blobUrl;
      anchor.download = 'text.txt';
      anchor.click();
      window.URL.revokeObjectURL(blobUrl);
    });

    3 个点

    1. window.URL.createObjectURL 的用途是把任何 Blob / File / ArrayBuffer 变成一个可以被引用的 URL. 可以用于 img.src, anchor.href 等等地方.

    2. 通过模拟 anchor download click 实现下载. 

    3. 游览器会组织恶意下载哦, 比如 setTimeout 之后下载, 一次可以, 多次就 alert 了.

    所以真实项目中, 下载最好配搭用户操作行为. 这样才不容易被 block.

     

  • 相关阅读:
    crontab 移动日志-超越昨天的自己系列(12)
    java进程性能分析步骤-超越昨天的自己系列(11)
    Linux 使用 you-get 指令下载网页视频
    Git git rm和git rm --cached
    Git .gitignore中已添加文件路径,但仍未被忽略
    Android 系统添加SELinux权限
    git 删除目录及子目录下的同名文件
    Android 查看和修改网络mtu
    git 设置git用户名和邮箱,并生成秘钥
    RK3288 添加普通串口uart1和uart3
  • 原文地址:https://www.cnblogs.com/keatkeat/p/16758402.html
Copyright © 2020-2023  润新知