前言

之前写过 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));
}
});

有几个点要注意

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 而已, 其它尽量不要用, 顺风水。

TextEncoder & TextDecoder

参考: 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 哦.

ArrayBuffer 和 Uint 8/16/32 Array 详解

Uint 8/16/32 Array

const bytes = new Uint8Array(1);

new Uint8Array(1) 创建了一个 Array (不是我们日常的那个 Array,是特别的 Array)。

这个 Array 的 length 是初始化时决定了,之后不能改了,例子中是 1,所以这个 Array 只有一个位置。

这个位置的大小是 Uint8 决定的,8 代表一个位置可以装 8 个二进制。比如说:

console.log((100).toString(2)); // 1100100   7 bit
console.log((255).toString(2)); // 11111111 8 bit
console.log((256).toString(2)); // 100000000 9 bit

十进制 100 需要 7 个二进制来表达,256 需要 9 个二进制,那么 Uint8Array 的位置就装不下 >= 256 (十进制) 的数。

我们测试看看 (提醒:读写 Uint8Array 用的是十进制,但是算 size 的时候使用二进制,这一点不要搞错)

const bytes = new Uint8Array(1);
bytes.set([254]);
console.log(bytes.toString()); // '254'
bytes.set([255]);
console.log(bytes.toString()); // '255'
bytes.set([256]);
console.log(bytes.toString()); // '0'
bytes.set([257]);
console.log(bytes.toString()); // '1'

当超过 255 时,它就自动跳数了。

如果我用 Uint16Array 就表示每一个位置可以装的下 16 个二进制。

const bytes = new Uint16Array(1);
bytes.set([257]);
console.log(bytes.toString()); // '257'
bytes.set([65535]);
console.log(bytes.toString()); // '65535'
bytes.set([65536]);
console.log(bytes.toString()); // '0' 过龙了

ArrayBuffer

ArrayBuffer 是一串长长的二进制,它不可以直接读写,我们需要用 UintArray 才能读写它。

在读之前,我们需要决定使用哪个 Uint 8/16/32 Array,比如说上面提到过的例子:

'严' 这个字的 UTF-8 是 [11100100, 10111000, 10100101] 有 3 个 bytes。

如果我们用 Uint8Array 来装,那就一个 byte 对应一个位置,3 个 bytes Uint8Array length 就是 3。

如果我们用 Uint16Array 来装,会报错,因为数量不对。

'严' 这个字的 UTF-16 是 [100101, 1001110] 有 2 个 bytes

如果我们用 Uint8Array 来装,那就一个 byte 对应一个位置,2 个 bytes Uint8Array length 就是 2。

如果我们用 Uint16Array 来装,那就一个 2 bytes 对应一个位置,Uint16Array[0] 的值是 100111000100101。

这个 100111000100101 来自 2 个 bytes 的组合 1001110 + 0 + 0 + 100101,它是逆序,然后它会补零。

提醒:

我只是拿 UTF 来举例子而已,不管是 UTF-8/16/32 通通都用 Uint8Array 就对了。

因为 UTF 规则也是一个一个 byte 解析来看的,并不是直接合并几个 bytes 做解析。

Example for TextEncoder,ArrayBuffer,Uint8Array

由于 Encode,Decode,ArrayBuffer 这些经常用于搞加密,所以这里给 2 个具体例子。

code_verifier (OAuth 2.0 PKCE Flow)

code_verifier 的要求是随机 43-128 Characters (只允许 [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~")

这个随机数要非常随机,所以我们不可以用 Math.random 只能用 window.crypto.getRandomValues。

首先创建一个 Uint8Array

const bytes = new Uint8Array(43);

它有 43 个 slot,每一个 slot 可以装 8 bit。

window.crypto.getRandomValues(bytes);

使用 window.crypto.getRandomValues 把这 43 个 slot 填满。

虽然每个 slot 号码最大只能到 255,但是已经足够随机了。

接着把数值强转成字符

const text = String.fromCharCode(...[...bytes]); // §-ÃØ­£Ñù=)ÝmÆ[Æ£ýNk‰ÅDñԁâzÝIòX¡#ƒÛ»æl

String.fromCharCode 可以把 Unicode 十进制代号转成字符,比如 20005 会转成 "严"。

此时我们就有了 43 个随机的 characters,随机满足了,还需要满足 length 和合格的字符。

接着把 43 characters convert to Base64 URL

const base64 = btoa(text);
const base64Url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // py3D2K2j0fk9Kd1txlvGoxyQ_QVOa4mpxUTx1IHiet1J8lihI4Pbu-Zs

convert 的过程中字数会加长,字符也变得合格了。

code_challenge (OAuth 2.0 PKCE Flow)

code_challenge 需要把 code_verifier 拿去 SHA-256 然后 Base64 URL。

function toBase64(value: string): string {
return window.btoa(value);
} function base64ToBase64Url(base64: string): string {
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
} function toBase64Url(value: string): string {
const base64 = toBase64(value);
return base64ToBase64Url(base64);
} function generateCodeVerifier(): string {
const bytes = new Uint8Array(43);
window.crypto.getRandomValues(bytes);
let text = String.fromCharCode(...[...bytes]);
return toBase64Url(text);
}

首先 encode code_verifier

const codeVerifier: string = generateCodeVerifier();
const encoder = new TextEncoder();
const bytes: Uint8Array = encoder.encode(codeVerifier);

会得到 Uint8Array。

接着调用 window.crypto.subtle.digest

const hashArrayBuffer: ArrayBuffer = await window.crypto.subtle.digest('SHA-256', bytes);

它会返回一个 Promise<ArrayBuffer>。

接着用 Uint8Array 把 ArrayBuffer 的 bytes 读出来,然后强转成 string 再转 Base64 URL 就可以了。

const text = String.fromCharCode(...[...new Uint8Array(hashArrayBuffer)]);
const base64Url = toBase64Url(text);

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.

 

DOM & BOM – Input File, Drag & Drop File, File Reader, Blob, ArrayBuffer, File, UTF-8 Encode/Decode, Download File的更多相关文章

  1. Base64 encode/decode large file

    转载:http://www.cnblogs.com/jzywh/archive/2008/04/20/base64_encode_large_file.html The class System.Co ...

  2. File、FileReader、Base64、Blob基本使用以及Buffer、ArrayBuffer之间的转换

    File文件 (File)对象获取文件的信息.实际上,File 对象是特殊类型的 Blob,Blob 的属性和方法都可以用于 File 对象.在js中,一般通过input元素,点击上传文件成功之后返回 ...

  3. Drag & Drop and File Reader

    参考 : http://www.html5rocks.com/zh/tutorials/file/dndfiles/ http://blog.csdn.net/rnzuozuo/article/det ...

  4. Csharp:WebClient and WebRequest use http download file

    //Csharp:WebClient and WebRequest use http download file //20140318 塗聚文收錄 string filePath = "20 ...

  5. Exception in thread "main" brut.androlib.AndrolibException: Could not decode arsc file at brut.androlib.res.decoder.ARSCDecoder.decode

    使用ApkIDE反编译出现如下错误: Exception in thread "main" brut.androlib.AndrolibException: Could not d ...

  6. 使用apktool工具遇到 could not decode arsc file 的解决办法

    I: Using Apktool -Beta9 on xx.apk I: Loading resource table... Exception in thread "main" ...

  7. Centos 7.5 通过yum安装GNOME Desktop时出现:file /boot/efi/EFI/centos from install of fwupdate-efi-12-5.el7.centos.x86_64 conflicts with file from package grub2-common-1:2.02-0.65.el7.centos.2.noarch

    系统版本为: [root@s10 ~]# cat /etc/redhat-release CentOS Linux release 7.5.1804 (Core) 由于管理kvm虚拟机的需求,需要安装 ...

  8. [转]人人网首页拖拽上传详解(HTML5 Drag&Drop、FileReader API、formdata)

    人人网首页拖拽上传详解(HTML5 Drag&Drop.FileReader API.formdata) 2011年12月11日 | 彬Go 上一篇:给力的 Google HTML5 训练营( ...

  9. HTML5魔法堂:全面理解Drag & Drop API

    一.前言    在HTML4的时代,各前端工程师为了实现拖拽功能可说是煞费苦心,初听HTML5的DnD API觉得那些痛苦的日子将一去不复返,但事实又是怎样的呢?下面我们一起来看看DnD API的真面 ...

  10. JS魔法堂:IE5~9的Drag&Drop API

    一.前言     < HTML5魔法堂:全面理解Drag & Drop API>中提到从IE5开始已经支持DnD API,但IE5~9与HTML5的API有所不同,下面我们来了解一 ...

随机推荐

  1. thinkphp模型hasOne、hasMany、belongsTo详解

    在ThinkPHP框架中,hasOne.hasMany和belongsTo是用于定义模型间一对多(1:n).一对一(1:1)和多对一(n:1)关联关系的方法.以下是一些简单的示例来解释这些关系: 1. ...

  2. workman的工作流程

    workerman有两种进程模型1.基本的master worker模型2.master gateway worker模型 master worker模型工作流程及进程间关系如下: master wo ...

  3. [oeasy]python0010_怎么用命令行保存文件

    编写 py 文件 回忆上次内容 上次 真的输出了 程序员的浪漫 Hello world!   print函数 可以输出 字符串 但是 print这个词 别拼错 就连 大小写 也别能错 错了就改 也没事 ...

  4. [oeasy]python0078_设置索引颜色_index_color_ansi_控制终端颜色

    更多颜色 回忆上次内容 上次 了解了 高亮颜色 91-97 是 高亮 前景色 101-107是 高亮 背景色 颜色种类 在原来基础上 增加了一些 但也非常有限 还想要 更精细的颜色 有可能吗?? 更多 ...

  5. Langchain 与 LlamaIndex:LLM 应用开发框架的比较与使用建议

    Langchain 和 Llamaindex 是两种广泛使用的主流 LLM 应用开发框架.两者有什么不同?我们该如何使用?以下我根据各类资料和相关文档做了初步选型. 一.Langchain 1. 适用 ...

  6. 支付宝小程序的级联选择器,对接简单操作,Cascader 级联选择器element_ui

    首先,对于element_ui 的动接,由于需要数据格式是 但是支付宝提的接口返回的数据是另一种格式,并且支付宝的三级联动接口是先只有一个列表,点击列表项再发现请求,生成另外一个下拉选择, 需要这个三 ...

  7. pytest数据驱动(最简单)

    pytest数据驱动(最简单) 第一种:通过yaml文件获取数据(一维列表) data.yaml文件内容如下: - '软件测试'- '单元测试'- '自动化测试'- '性能测试'- '测试开发'- ' ...

  8. Jmeter大小断言

    Jmeter大小断言是用来判断返回的消息体大小的,组件路径[HTTP请求右键添加->断言->大小断言] 我们来了解一下大小断言组件里面包含什么内容 1.Apply to: Main sam ...

  9. 【SVN】文件解锁

    提交代码莫名其妙的把文件上锁了 然后找到文件右键的SVN的选项也不能解锁: 原来是这样解锁的: 对上锁文件的所在目录右键找到SVN选项 然后勾选第二项: 这样就解锁了.如果还说没有解锁,说明是对方自己 ...

  10. nvidia 机器人仿真环境Isaac Sim