如何使用浏览器来操作本地文件系统中的文件呢,相比大家第一时间想到是的input type=“file”来读取文件,然后通过下载来写入文件,对,这是最古老的方式了,他只能上传和下载文件,不能修改和删除本地文件或文件夹呢,随着File system access api的推出,让现代浏览器拥有了像app一样读写本地系统文件的能力。
今天我就来跟大家说说这个新的api怎么用js来编写,主要有三个方法:Window.showOpenFilePicker()、 Window.showSaveFilePicker()和Window.showDirectoryPicker()。
一、js读写浏览器端本地文件和文件夹
1、读取并修改本地文件,通过showOpenFilePicker来弹出一个选择文件的窗口,通过 await fileHandle.getFile()获取文件对象,修改是通过fileHandle.createWritable()来修改保存到本地系统,完整代码如下<html> <head> <meta charset="UTF-8"> <link type="text/css" rel="stylesheet" href="//repo.bfw.wiki/bfwrepo/css/bootstrap.4.3.1.min.css"> <style> body{ padding: 10px; } </style> </head> <body> <p><button id="openlocalfile">打开本地文件</button> <button id="savetolocalfile">保存到本地</button>不要在iframe中打开</p> <textarea id="editor" style="width:100%;height:400px;"></textarea> <script> const openbtn = document.getElementById('openlocalfile'); const editor = document.getElementById('editor'); const savebtn = document.getElementById('savetolocalfile'); let fileHandle; openbtn.addEventListener('click', async () => { [fileHandle] = await window.showOpenFilePicker(); const file = await fileHandle.getFile(); const contents = await file.text(); editor.value = contents; }); savebtn.addEventListener('click', async () => { if(fileHandle==null){ alert("请先打开一个本地文件后进行修改"); return; } const writable = await fileHandle.createWritable(); // 写入文件 await writable.write(editor.value); // 关闭 await writable.close(); alert("修改本地文件成功"); }); </script> </body> </html>
那么如果要另存为呢?这里就要用到window.showSaveFilePicker了,他可以弹出另外为窗口,可以定义默认名称及文档类型描述等,配置如下:
const fileHandle2 = await window.showSaveFilePicker({ suggestedName: 'Uint8ClampedArray.txt', types: [{ description: '文本文件', accept: { 'text/plain': ['.txt'], }, }], });
完整的打开和另外为文件代码如下:
<html> <head> <meta charset="UTF-8"> <link type="text/css" rel="stylesheet" href="//repo.bfw.wiki/bfwrepo/css/bootstrap.4.3.1.min.css"> <style> body{ padding: 10px; } </style> </head> <body> <p><button id="openlocalfile">打开本地文件</button> <button id="savetolocalfile">保存到本地</button>不要在iframe中打开</p> <textarea id="editor" style="width:100%;height:400px;"></textarea> <script> const openbtn = document.getElementById('openlocalfile'); const editor = document.getElementById('editor'); const savebtn = document.getElementById('savetolocalfile'); let fileHandle; openbtn.addEventListener('click', async () => { [fileHandle] = await window.showOpenFilePicker(); const file = await fileHandle.getFile(); const contents = await file.text(); editor.value = contents; }); savebtn.addEventListener('click', async () => { if(fileHandle==null){ alert("请先打开一个本地文件后进行修改"); return; } //创建一个另外为 const fileHandle2 = await window.showSaveFilePicker({ suggestedName: 'Uint8ClampedArray.txt', types: [{ description: '文本文件', accept: { 'text/plain': ['.txt'], }, }], }); const writable = await fileHandle2.createWritable(); // 写入文件 await writable.write(editor.value); // 关闭 await writable.close(); alert("另存为本地文件成功"); }); </script> </body> </html>那么删除文件呢?删除文件我们在打开目录中讲解。
二、js用浏览器打开本地目录
打开一个目录,并获取子目录,这里要用到window.showDirectoryPicker(),他可以返回一个文件夹的句柄。完整代码如下:
<html> <head> <meta charset="UTF-8"> <link type="text/css" rel="stylesheet" href="//repo.bfw.wiki/bfwrepo/css/bootstrap.4.3.1.min.css"> <style> body{ padding: 10px; } </style> </head> <body> <p><button id="openlocalfile">打开本地文件夹</button> 不要在iframe中打开,结果在console中查看 <script> const openbtn = document.getElementById('openlocalfile'); let dirHandle; openbtn.addEventListener('click', async () => { const dirHandle = await window.showDirectoryPicker(); for await (const entry of dirHandle.values()) { console.log(entry.kind, entry.name); } }); </script> </body> </html>文件打开了,通过for循环获取了子目录的文件夹名称,那么如果子目录还有子目录呢?如何获取所有多级子目录及子文件呢?这个要用到递归了,代码如下:
<html> <head> <script src="//repo.bfw.wiki/bfwrepo/js/eruda.js"></script> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum=1.0,minimum=1.0,user-scalable=0" /> <title>BFW NEW PAGE</title> <link type="text/css" rel="stylesheet" href="//repo.bfw.wiki/bfwrepo/css/bootstrap.4.3.1.min.css"> <style> body{ padding: 10px; } </style> </head> <body> <p><button id="openlocalfile">打开目录</button> 不要在iframe中打开,结果在console中查看</p> <script> const openbtn = document.getElementById('openlocalfile'); openbtn.onclick = async (evt) => { const out = {}; const dirHandle = await showDirectoryPicker(); await handleDirectoryEntry( dirHandle, out ); console.log( out ); }; async function handleDirectoryEntry( dirHandle, out ) { for await (const entry of dirHandle.values()) { if (entry.kind === "file"){ const file = await entry.getFile(); out[ file.name ] = file; } if (entry.kind === "directory") { const newOut = out[ entry.name ] = {}; await handleDirectoryEntry( entry, newOut ); } } } </script> </body> </html>好了,那么如何在目录中新建文件夹和文件呢?主要通过getDirectoryHandle和getFIleHandle来操作,完整代码如下:
<html> <head> <meta charset="UTF-8"> <link type="text/css" rel="stylesheet" href="//repo.bfw.wiki/bfwrepo/css/bootstrap.4.3.1.min.css"> <style> body{ padding: 10px; } </style> </head> <body> <button id="openlocalfile">打开本地文件夹并创建一个文件和文件夹</button> <script> const openbtn = document.getElementById('openlocalfile'); openbtn.addEventListener('click', async () => { const dirHandle = await window.showDirectoryPicker(); //在此文件夹下创建一个新的文件夹名为 "My Documents". const newDirectoryHandle = await dirHandle.getDirectoryHandle('My Documents', { create: true, }); // 在此文件夹下创建一个新文件名字为 "My Notes.txt". const newFileHandle = await dirHandle.getFileHandle('My Notes.txt', { create: true }); for await (const entry of dirHandle.values()) { console.log(entry.kind, entry.name); } }); </script> </body> </html>那么怎么删除文件夹和文件呢?主要通过removeEntry来实现,既可以删除文件夹又可以删除文件,如果删除子目录下所有文件,那么在后面增加参数recursive: true,完整代码如下:
<html> <head> <meta charset="UTF-8"> <link type="text/css" rel="stylesheet" href="//repo.bfw.wiki/bfwrepo/css/bootstrap.4.3.1.min.css"> <style> body{ padding: 10px; } </style> </head> <body> <button id="dellocalfile">打开本地文件夹并删除一个文件和文件夹</button> <script> const openbtn = document.getElementById('dellocalfile'); openbtn.addEventListener('click', async () => { const dirHandle = await window.showDirectoryPicker(); // 删除一个文件 await dirHandle.removeEntry('My Notes.txt'); // 递归删除文件夹和文件夹下所有子文件和子目录 await dirHandle.removeEntry('My Documents', { recursive: true }); for await (const entry of dirHandle.values()) { console.log(entry.kind, entry.name); } }); </script> </body> </html>除了点击打开,还可以通过拖放来打开目录或文件,完整代码如下:
<html> <head> <meta charset="UTF-8"> <link type="text/css" rel="stylesheet" href="//repo.bfw.wiki/bfwrepo/css/bootstrap.4.3.1.min.css"> <style> body{ padding: 10px; } #dragarea{ background: red; height: 200px; width: 200px; color: white; } </style> </head> <body> <div id="dragarea">拖动一个文件到这里</div> <script> const elem = document.getElementById('dragarea'); elem.addEventListener('dragover', (e) => { // Prevent navigation. e.preventDefault(); }); elem.addEventListener('drop', async (e) => { // Prevent navigation. e.preventDefault(); // Process all of the items. for (const item of e.dataTransfer.items) { // Careful: `kind` will be 'file' for both file // _and_ directory entries. if (item.kind === 'file') { const entry = await item.getAsFileSystemHandle(); if (entry.kind === 'directory') { handleDirectoryEntry(entry); } else { handleFileEntry(entry); } } } }); async function handleDirectoryEntry( dirHandle ) { for await (const entry of dirHandle.values()) { console.log(entry.kind, entry.name); } } async function handleFileEntry( filehandle ) { console.log(filehandle); } </script> </body> </html>
三、使用 IndexedDB 存储文件句柄或目录句柄
上面我们实现了打开文件夹和文件,但是当我们重启浏览器后,又要在重新找一次文件夹选择,很麻烦,有没有办法保存这些句柄呢,我们使用indexedDb来实现,代码如下:import { get, set } from 'https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js'; const pre1 = document.querySelector('pre.file'); const pre2 = document.querySelector('pre.directory'); const button1 = document.querySelector('button.file'); const button2 = document.querySelector('button.directory'); // File handle button1.addEventListener('click', async () => { try { const fileHandleOrUndefined = await get('file'); if (fileHandleOrUndefined) { pre1.textContent = `Retrieved file handle "${fileHandleOrUndefined.name}" from IndexedDB.`; return; } const [fileHandle] = await window.showOpenFilePicker(); await set('file', fileHandle); pre1.textContent = `Stored file handle for "${fileHandle.name}" in IndexedDB.`; } catch (error) { alert(error.name, error.message); } }); // Directory handle button2.addEventListener('click', async () => { try { const directoryHandleOrUndefined = await get('directory'); if (directoryHandleOrUndefined) { pre2.textContent = `Retrieved directroy handle "${directoryHandleOrUndefined.name}" from IndexedDB.`; return; } const directoryHandle = await window.showDirectoryPicker(); await set('directory', directoryHandle); pre2.textContent = `Stored directory handle for "${directoryHandle.name}" in IndexedDB.`; } catch (error) { alert(error.name, error.message); } });
四、私有文件访问
浏览器提供私有文件系统提供给每个页面来访问,这个私有文件外界是看不到的,只能通过浏览器的api来获取,代码如下:const root = await navigator.storage.getDirectory(); // Create a new file handle. const fileHandle = await root.getFileHandle('Untitled.txt', { create: true }); // Create a new directory handle. const dirHandle = await root.getDirectoryHandle('New Folder', { create: true }); // Recursively remove a directory. await root.removeEntry('Old Stuff', { recursive: true });这个还支持同步和异步读写操作来提高性能;
// Asynchronous access in all contexts: const handle = await file.createAccessHandle({ mode: 'in-place' }); await handle.writable.getWriter().write(buffer); const reader = handle.readable.getReader({ mode: 'byob' }); // Assumes seekable streams, and SharedArrayBuffer support are available await reader.read(buffer, { at: 1 }); // (Read and write operations are synchronous, // but obtaining the handle is asynchronous.) // Synchronous access exclusively in Worker contexts const handle = await file.createSyncAccessHandle(); const writtenBytes = handle.write(buffer); const readBytes = handle.read(buffer, { at: 1 });
五、怎么判断用户是否运行对某个目录的访问呢?
async function verifyPermission(fileHandle, readWrite) { const options = {}; if (readWrite) { options.mode = 'readwrite'; } // Check if permission was already granted. If so, return true. if ((await fileHandle.queryPermission(options)) === 'granted') { return true; } // Request permission. If the user grants permission, return true. if ((await fileHandle.requestPermission(options)) === 'granted') { return true; } // The user didn't grant permission, so return false. return false; }
参考文档:https://web.dev/file-system-access/#accessing-the-origin-private-file-system
网友评论0