前言
之前写过 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 textFile = files[0]; dropArea.querySelector('p')!.textContent = textFile.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.
Read Text from Blob
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.