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

前言

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

首先看整体功能演示
在这里插入图片描述

功能概述

我们将实现一个简单的 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 实现了代码高亮显示。

源码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Local File Browser with Edit and Save</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/atom-one-dark.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/highlight.min.js"></script>
    <style>
        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;
        }
    </style>
</head>
<body>
    <div id="sidebar">
        <h1>选择目录</h1>
        <button id="selectDirectoryButton">选择目录</button>
        <ul id="fileTree"></ul>
    </div>
    <div id="content">
        <h1>编辑文件</h1>
        <textarea id="editArea"></textarea>
        <button id="saveButton">保存编辑后的文件内容</button>
    </div>
    <div id="preview">
        <h1>本地文件修改后的实时预览</h1>
        <pre><code id="fileContent" class="plaintext"></code></pre>
    </div>

    <script>
        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();
    </script>
</body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/747733.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Xilinx FPGA:vivado这里记录一个小小的问题

问题描述&#xff1a;uart_data从rx模块输入到ctrl模块后就没有值了。 问题一&#xff1a;首先我仿真例化了两个模块&#xff0c;并且&#xff0c;我选取了单独例化的rx模块中的uart_data 的值&#xff0c;所以在仿真中它是有值的。 timescale 1ns / 1ps module test_bench_TO…

QCC51XX---开启手机log日志

QCC51XX---系统学习目录_trbi200软件-CSDN博客 目录 1.Vivo 2.华为 3.小米 4.三星 5.oppo 1.Vivo *#*#112#*#* 输入命令后会进入log日志系统(由于版本原因,界面可能不同),打开log开关,log就会在后台自动录制。 点击设置,则可进入图1(右边)的界面,可以导出log,导出…

小程序中echarts的bug

这个文字在手机上显示会有黑的的阴影 textStyle: {fontSize: 12,wrap: true,textShadowColor: "#fff", // 文字本身的阴影颜色textShadowBlur: 10, // 文字本身的阴影长度textShadowOffsetX: 10, // 文字本身的阴影X偏移textShadowOffsetY: 10, //阴影Y偏移}

转:关于征集第三批工业软件新场景新技术难题解决思路的公告

工业软件是先进工业知识与经验的凝炼&#xff0c;工业软件自身的先进性既来自对先进工业先进需求的汲取提炼&#xff0c;也来自对根技术新突破、新成果的高效采用。为增强根技术新成果提供方与工业软件厂家或最终用户方的连接&#xff0c;促进国产工业软件差异化竞争力的打造&a…

游戏服务器研究三:bigworld 的 load balance 算法

1. 前言 bigworld 的 load balance 算法的大致思路是知道的&#xff0c;即 动态区域分割 动态边界调整。但具体是怎么实现的&#xff0c;不清楚&#xff0c;网上也不找到相关的文章介绍&#xff0c;所以只能自己看代码进行分析。 本文大致记录我所分析到的算法实现&#xff…

vue3项目登录成功后根据角色菜单来跳转指定页面(无首页)

前言&#xff1a;需求不想要首页&#xff0c;登录什么角色跳转到这个角色经常使用的页面。&#xff08;例如&#xff1a;审核者角色的人输入用户名密码成功后就自动跳转到待审核的页面&#xff0c;仓库管理员登录成功则自动跳转到仓库列表&#xff09; 需要解决的点和想法&…

C语言类型转换理解不同的基本类型为什么能够进行运算

类型转换 1.类型转换1.1隐式转换1.2常用算术转换1.2强制类型转换 1.类型转换 在执行算数运算时&#xff0c;计算机比C语言的限制更多。为了让计算机执行算术运算&#xff0c;通常要求操作数用相同的大小&#xff08;即为的数量相同&#xff09;&#xff0c;但是C语言却允许混合…

SpringBoot优点达项目实战:登录功能实现(四)

SpringBoot优点达项目实战&#xff1a;登录功能实现&#xff08;四&#xff09; 文章目录 SpringBoot优点达项目实战&#xff1a;登录功能实现&#xff08;四&#xff09;1、查看接口2、查看数据库3、代码实现1、创建实体类2、controller实现3、service层实现4、Mapper层 4、测…

JVM专题十二:JVM 中的收集器二

上一篇JVM专题十一&#xff1a;JVM 中的收集器一咱们介绍了垃圾收集器的分类&#xff0c;已经主流的分代垃圾收集器重点看了CMS与三色标记算法&#xff0c;本篇咱们继续来看意G1、ZGC等。 G1收集器 G1&#xff08;Garbage-First Garbage Collector&#xff09;是一种服务器端的…

React实现二级评论

1. 什么是二级评论 图片来源–blackfrog的掘金文章 口语化的讲当我发布一个评论的时候就是一级评论&#xff0c;当我回复我发布的评论的时候就是二级评论并且将所有回复二级评论的评论也归于二级评论。 2. 二级评论功能的实现逻辑 在这里后端设计了四个接口分别是 获取所有…

千呼新零售2.0-OCR拍照识别采购单

千呼新零售2.0系统是零售行业连锁店一体化收银系统&#xff0c;包括线下收银线上商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体&#xff0c;线上线下数据全部打通。 适用于商超、便利店、水果、生鲜、母婴、服装、零食、百货、宠物、中医养生、大健康等连锁店…

目标检测系列(四)利用pyqt5实现yolov8目标检测GUI界面

目录 1、pyqt5安装 2、PyCharm添加Qt Designer、PyUIC 3、Qt Designer设计界面 4、根据ui文件自动生成py文件 5、修改py文件来调用检测程序 6、执行py文件启动 1、pyqt5安装 Qt Designer&#xff1a;一个用于创建图形用户界面的工具&#xff0c;可轻松构建复杂的用户界面…

ChatUI:使用Gradio.NET为LLamaWorker快速创建大模型演示界面

Gradio.NET 是 Gradio 的.NET 移植版本。它是一个能够助力迅速搭建机器学习模型演示界面的库&#xff0c;其提供了简洁的 API&#xff0c;仅需寥寥数行代码就能创建出一个具备交互性的界面。在本篇文章中&#xff0c;我们将会阐述如何借助 Gradio.NET 为 LLamaWorker 快捷地创建…

Python 基础 (标准库):堆 heap

1. 官方文档 heapq --- 堆队列算法 — Python 3.12.4 文档 2. 相关概念 堆 heap 是一种具体的数据结构&#xff08;concrete data structures&#xff09;&#xff1b;优先级队列 priority queue 是一种抽象的数据结构&#xff08;abstract data structures&#xff09;&…

【数据结构】——链表经典OJ(leetcode)

文章目录 一、 相交链表二、 反转链表三、 回文链表四、 环形链表五、 环形链表 II六、 合并两个有序链表七、 两数相加八、 删除链表的倒数第N个节点九、 随机链表的复制 一、 相交链表 双指针法 struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListN…

MySQL进阶-索引-使用规则-最左前缀法则和范围查询

文章目录 1、最左前缀法则2、启动mysql3、查询tb_user4、查看tb_user的索引5、执行计划 profession 软件工程 and age31 and status 06、执行计划 profession 软件工程 and age317、执行计划 profession 软件工程8、执行计划 age31 and status 09、执行计划 status 010、执行…

直流电机双闭环调速Simulink仿真

直流电机参数&#xff1a; 仿真模型算法介绍&#xff1a; 1&#xff09;三相整流桥&#xff0c;采用半控功率器件SCR晶闸管&#xff1b; 2&#xff09;采用转速环、电流环 双闭环控制算法&#xff1b; 3&#xff09;外环-转速环&#xff0c;采用PI 比例积分控制&#xff1b;…

通信系统网络架构_3.移动通信网络架构

移动通信网为移动互联网提供了强有力的支持&#xff0c;尤其是5G网络为个人用户、垂直行业等提供了多样化的服务。以下从业务应用角度给出面向5G网络的组网方式。 1.5GS与DN互连 5GS&#xff08;5G System&#xff09;在为移动终端用户&#xff08;User Equipment&#xff0c;…

搜维尔科技:【研究】触觉手套比控制器更能带来身临其境、更安全、更高效的虚拟体验

自然交互可提高VR模拟的有效性。研究表明&#xff0c;触觉手套比控制器更能带来身临其境、更安全、更高效的虚拟体验。 以下是验证 医疗培训中的触觉技术 “ 95.5%的参与者表示触摸是 XR 教育的重要组成部分&#xff0c;90.9% 的参与者表示 XR 触觉将提供一个安全的学习场所。…

eNSP中ACL访问控制表的配置和使用

一、拓扑图 1.新建拓扑图 2.PC端配置 PC1: PC2: PC3: 二、基本命令配置 1.S1配置 <Huawei>system-view [Huawei]sysname S1 [S1]vlan 10 [S1-vlan10]vlan 20 [S1-vlan20]vlan 30 [S1-vlan30]quit [S1]interface Vlanif 10 [S1-Vlanif10]ip address 192.168.10…