前端实现对本地文件的IO操作

07-14 1208阅读

前言

在网页中,前端已经可以读取本地文件系统,对本地的文件进行IO读写,甚至可以制作一个简单的VScode编辑器。这篇文章以渐进式方式实现此功能,文末附上所有代码。

首先看整体功能演示

前端实现对本地文件的IO操作

功能概述

我们将实现一个简单的 Web 应用,具备以下功能:

  1. 选择本地目录:用户可以选择本地目录并显示其结构。
  2. 文件浏览:用户可以浏览目录中的文件和子目录。
  3. 文件编辑:用户可以选择文件并在网页上进行编辑。
  4. 文件保存:用户可以将编辑后的文件保存到本地。

核心实现步骤

我们将功能拆分为以下几个核心步骤:

  1. 选择本地目录
  2. 构建文件树
  3. 读取和编辑文件
  4. 保存编辑后的文件
1. 选择本地目录

选择本地目录是实现这个功能的第一步。我们使用 File System Access API 的 showDirectoryPicker 方法来选择目录。

document.getElementById('selectDirectoryButton').addEventListener('click', async function() {
    try {
        const directoryHandle = await window.showDirectoryPicker();
        console.log(directoryHandle);  // 打印目录句柄
    } catch (error) {
        console.error('Error: ', error);
    }
});
2. 构建文件树

选择目录后,我们需要递归地构建文件树,并在页面上显示文件和子目录。

async function buildFileTree(directoryHandle, parentElement) {
    for await (const [name, entryHandle] of directoryHandle.entries()) {
        const li = document.createElement('li');
        li.textContent = name;
        if (entryHandle.kind === 'file') {
            li.classList.add('file');
            li.addEventListener('click', async function() {
                currentFileHandle = entryHandle;
                const file = await entryHandle.getFile();
                const fileContent = await file.text();
                document.getElementById('fileContent').textContent = fileContent;
                document.getElementById('editArea').value = fileContent;
                document.getElementById('editArea').style.display = 'block';
                document.getElementById('saveButton').style.display = 'block';
            });
        } else if (entryHandle.kind === 'directory') {
            li.classList.add('folder');
            const ul = document.createElement('ul');
            ul.style.display = 'none';
            li.appendChild(ul);
            li.addEventListener('click', function() {
                ul.style.display = ul.style.display === 'none' ? 'block' : 'none';
            });
            await buildFileTree(entryHandle, ul);
        }
        parentElement.appendChild(li);
    }
}
3. 读取和编辑文件

当用户点击文件时,我们读取文件内容,并在文本区域中显示以便编辑。

li.addEventListener('click', async function() {
    currentFileHandle = entryHandle;
    const file = await entryHandle.getFile();
    const fileContent = await file.text();
    document.getElementById('fileContent').textContent = fileContent;
    document.getElementById('editArea').value = fileContent;
    document.getElementById('editArea').style.display = 'block';
    document.getElementById('saveButton').style.display = 'block';
});
4. 保存编辑后的文件

编辑完成后,用户可以点击保存按钮将修改后的文件内容保存回本地文件。

document.getElementById('saveButton').addEventListener('click', async function() {
    if (currentFileHandle) {
        const editArea = document.getElementById('editArea');
        const updatedContent = editArea.value;
        // 创建一个 writable 流并写入编辑后的文件内容
        const writable = await currentFileHandle.createWritable();
        await writable.write(updatedContent);
        await writable.close();
        // 更新显示区域的内容
        document.getElementById('fileContent').textContent = updatedContent;
    }
});

核心 API 介绍

window.showDirectoryPicker()

该方法打开目录选择对话框,并返回一个 FileSystemDirectoryHandle 对象,代表用户选择的目录。

const directoryHandle = await window.showDirectoryPicker();
FileSystemDirectoryHandle.entries()

该方法返回一个异步迭代器,用于遍历目录中的所有文件和子目录。

for await (const [name, entryHandle] of directoryHandle.entries()) {
    // 处理每个文件或目录
}
FileSystemFileHandle.getFile()

该方法返回一个 File 对象,表示文件的内容。

const file = await fileHandle.getFile();
FileSystemFileHandle.createWritable()

该方法创建一个可写流,用于写入文件内容。

const writable = await fileHandle.createWritable();
await writable.write(content);
await writable.close();

总结

通过以上步骤,我们能够选择本地目录、浏览文件和子目录、读取和编辑文件内容,并将编辑后的文件保存回本地。同时,我们使用 Highlight.js 实现了代码高亮显示。

源码



    
    
    Local File Browser with Edit and Save
    
    
    
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 0;
            display: flex;
            height: 100vh;
            overflow: hidden;
            background-color: #2e2e2e;
            color: #f1f1f1;
        }
        #sidebar {
            width: 20%;
            background-color: #333;
            border-right: 1px solid #444;
            padding: 20px;
            box-sizing: border-box;
            overflow-y: auto;
        }
        #content {
            width: 40%;
            padding: 20px;
            box-sizing: border-box;
            overflow-y: auto;
        }
        #preview {
            width: 40%;
            padding: 20px;
            box-sizing: border-box;
            overflow-y: auto;
            background-color: #1e1e1e;
            border-left: 1px solid #444;
        }
        #fileTree {
            list-style-type: none;
            padding: 0;
        }
        #fileTree li {
            margin-bottom: 5px;
            cursor: pointer;
            user-select: none; /* 禁止文本选中 */
        }
        #fileTree .folder::before {
            content: "📂";
            margin-right: 5px;
        }
        #fileTree .file::before {
            content: "📄";
            margin-right: 5px;
        }
        #fileContent {
            white-space: pre-wrap; /* Preserve whitespace */
            background-color: #1e1e1e;
            padding: 10px;
            border: 1px solid #444;
            min-height: 200px;
            color: #f1f1f1;
        }
        #editArea {
            width: 100%;
            height: calc(100% - 40px);
            background-color: #1e1e1e;
            color: #f1f1f1;
            border: 1px solid #444;
            padding: 10px;
            box-sizing: border-box;
        }
        #saveButton {
            margin-top: 10px;
            background-color: #4caf50;
            color: white;
            border: none;
            padding: 10px 15px;
            cursor: pointer;
            border-radius: 5px;
        }
        #saveButton:hover {
            background-color: #45a049;
        }
        h1 {
            font-size: 1.2em;
            margin-bottom: 10px;
        }
        ::-webkit-scrollbar {
            width: 8px;
        }
        ::-webkit-scrollbar-track {
            background: #333;
        }
        ::-webkit-scrollbar-thumb {
            background-color: #555;
            border-radius: 10px;
            border: 2px solid #333;
        }
        .hidden {
            display: none;
        }
    


    
        

选择目录

选择目录

    编辑文件

    保存编辑后的文件内容

    本地文件修改后的实时预览

    
        
        
            let currentFileHandle = null;
            document.getElementById('selectDirectoryButton').addEventListener('click', async function() {
                try {
                    const directoryHandle = await window.showDirectoryPicker();
                    const fileTree = document.getElementById('fileTree');
                    fileTree.innerHTML = ''; // 清空文件树
                    async function buildFileTree(directoryHandle, parentElement) {
                        for await (const [name, entryHandle] of directoryHandle.entries()) {
                            const li = document.createElement('li');
                            li.textContent = name;
                            if (entryHandle.kind === 'file') {
                                li.classList.add('file');
                                li.addEventListener('click', async function() {
                                    currentFileHandle = entryHandle;
                                    const file = await entryHandle.getFile();
                                    const fileContent = await file.text();
                                    const fileExtension = name.split('.').pop();
                                    const codeElement = document.getElementById('fileContent');
                                    const editArea = document.getElementById('editArea');
                                    codeElement.textContent = fileContent;
                                    editArea.value = fileContent;
                                    codeElement.className = ''; // 清除之前的语言类
                                    codeElement.classList.add(getHighlightLanguage(fileExtension));
                                    hljs.highlightElement(codeElement);
                                    // 显示编辑区域和保存按钮
                                    editArea.style.display = 'block';
                                    document.getElementById('saveButton').style.display = 'block';
                                });
                            } else if (entryHandle.kind === 'directory') {
                                li.classList.add('folder');
                                const ul = document.createElement('ul');
                                ul.classList.add('hidden'); // 默认隐藏子目录
                                li.appendChild(ul);
                                li.addEventListener('click', function(event) {
                                    event.stopPropagation(); // 阻止事件冒泡
                                    ul.classList.toggle('hidden');
                                });
                                await buildFileTree(entryHandle, ul);
                            }
                            parentElement.appendChild(li);
                        }
                    }
                    await buildFileTree(directoryHandle, fileTree);
                } catch (error) {
                    console.log('Error: ', error);
                }
            });
            // 获取代码高亮语言类型
            function getHighlightLanguage(extension) {
                switch (extension) {
                    case 'js': return 'javascript';
                    case 'html': return 'html';
                    case 'css': return 'css';
                    case 'json': return 'json';
                    case 'xml': return 'xml';
                    case 'py': return 'python';
                    case 'java': return 'java';
                    default: return 'plaintext';
                }
            }
            // 保存编辑后的文件内容
            document.getElementById('saveButton').addEventListener('click', async function() {
                if (currentFileHandle) {
                    const editArea = document.getElementById('editArea');
                    const updatedContent = editArea.value;
                    // 创建一个 writable 流并写入编辑后的文件内容
                    const writable = await currentFileHandle.createWritable();
                    await writable.write(updatedContent);
                    await writable.close();
                    // 更新高亮显示区域的内容
                    const codeElement = document.getElementById('fileContent');
                    codeElement.textContent = updatedContent;
                    hljs.highlightElement(codeElement);
                }
            });
            // 初始化 highlight.js
            hljs.initHighlightingOnLoad();
    VPS购买请点击我

    文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

    目录[+]