js用浏览器端文件系统api实现对本地文件和文件夹的读取和写入操作

js用浏览器端文件系统api实现对本地文件和文件夹的读取和写入操作

如何使用浏览器来操作本地文件系统中的文件呢,相比大家第一时间想到是的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

{{collectdata}}

网友评论0