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

news2025/2/7 3:10:45

前言

在网页中,前端已经可以读取本地文件系统,对本地的文件进行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>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

全面国产化信创适配改造方案说明

一、概叙 系统的全面国产化适配改造需要从多个方面进行考虑&#xff0c;改造前需要进行充分的论证&#xff0c;在满足具体业务场景的前提下&#xff0c;以确保系统的稳定性和安全性&#xff0c;同时还要考虑技术的发展&#xff0c;不断优化和更新。因此全面国产化适配改造也面临…

【React】富文本编辑器react-quill

安装 react-quill 富文本编辑器 npm i react-quill2.0.0-beta.2报错解决&#xff1a; npm i react-quill2.0.0-beta.2 --legacy-peer-deps导入编辑器组件和配套样式文件 import ReactQuill from react-quill // 1 import react-quill/dist/quill.snow.css // 2const Publi…

C++:STL容器-map

C:STL容器-map 1. map构造和赋值2. map大小和交换3. map插入和删除4. map查找和统计5. map容器排序 map中所有元素都是pair&#xff08;对组&#xff09; pair中第一个元素为key&#xff08;键&#xff09;&#xff0c;起到索引作用&#xff0c;第二个元素为value&#xff08;实…

开发指南033-数据库兼容

元芳&#xff0c;你怎么看&#xff1f; 单一数据库自身就有一些不同处理之处&#xff0c;如果一个平台要兼容所有数据库&#xff0c;就是难上加难&#xff0c;像isnull函数各数据库就不同。 对于这类问题&#xff0c;平台采用统一自定义函数解决&#xff0c;例如上面的round函…

Go 与 Java 字符编码选择:UTF-8 与 UTF-16 的较量

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

Nature Climate Change | 气候变暖会造成未来全球干旱区面积扩张?

在气候变暖的情况下&#xff0c;旱地通常被预测将在全球范围内扩大&#xff0c;旱地包括以水资源有限、植被稀疏为特征的土地区域。然而&#xff0c;这种预测依赖于旱地的大气代用物&#xff0c;即干旱指数。最近的研究表明&#xff0c;干旱指数对陆地水循环的各种组成部分的预…

前端vite+vue3——利用环境变量和路由区分h5、pc模块打包(从0到1)

⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文分享 前端vitevue3——利用环境变量和路由对前端区分h5和pc模块打包&#xff08;从0到1&#xff09;。 背景&#xff1a; 前端本地开发pc和h5的项目&#xff0c;发布时需要区分开h5和pc的页面 vite Vite 通过在一开始将应…

【办公类-50-01】20240620自主游戏观察记录表19周内容打乱

背景需求&#xff1a; 又到了期末&#xff0c;各种班级资料需要提交。 有一份自主游戏观察记录需要写19周&#xff08;每周2次&#xff09;的观察记录&#xff0c;并根据参考书填写一级、三级、五级的评价指标。 去年中六班的时候&#xff0c;我很认真的手写了21周的户外游戏…

laravel8框架windows下安装运行

目录 1、安装前如果未安装先安装Composer 2、使用composer安装laravel8 3、使用内置服务器:8000 的命令去访问测试 ​4、使用本地环境运行phpstudy配置到public目录下 Laravel官网 Laravel 中文网 为 Web 工匠创造的 PHP 框架 安装 | 入门指南 |《Laravel 8 中文文档 8.x…

Nginx - 反向代理、负载均衡、动静分离、底层原理(案例实战分析)

目录 Nginx 开始 概述 安装&#xff08;非 Docker&#xff09; 配置环境变量 常用命令 配置文件概述 location 路径匹配方式 配置反向代理 实现效果 准备工作 具体配置 效果演示 配置负载均衡 实现效果 准备工作 具体配置 实现效果 其他负载均衡策略 配置动…

一文了解Linux中的内存映射

目录 一、概念 工作原理&#xff1a; 特点&#xff1a; 适用场景&#xff1a; 二、详解mmap&#xff08;&#xff09;函数 1. mmap的基本概念 2. mmap的特点 3. mmap的用途 4. mmap的优缺点 三、实验 实验一&#xff1a;基础读写实验 实验二&#xff1a;证明开始显…

计算机组成原理 —— 存储系统(主存储器基本组成)

计算机组成原理 —— 存储系统&#xff08;主存储器基本组成&#xff09; 0和1的硬件表示整合结构寻址按字寻址和按字节寻址按字寻址按字节寻址区别总结 字寻址到字节寻址转化 我们今天来看一下主存储器的基本组成&#xff1a; 0和1的硬件表示 我们知道一个主存储器是由存储体…

plt绘制网格图

代码 obj "accu" for (epoch,lr) in config:with open(data/epoch_{}_lr_{}_Adam.pkl.format(epoch,lr),rb) as f:data pickle.load(f) plt.plot(range(1,epoch1),data[obj],labelflr{lr})plt.title(obj"-epoch") plt.xlabel("epoch"…

Linux系统及常用命令介绍

一.介绍 Linux一套免费使用和自由传播的类Unix操作系统&#xff0c;是一个遵循POSIX的多用户、多任务、支持多线程和多CPU的操作系统。Linux系统的说明可以自行百度&#xff0c;知道这几点即可&#xff1a; 1.Linux中一切都是文件&#xff1b; 2.Linux是一款免费操作系统&…

云资源管理系统-项目部署

云资源管理系统-项目部署 大家好&#xff0c;我是秋意零。 今天分享个人项目同时也是个人毕设项目&#xff0c;云平台资源管理系统。该系统具备对OpenStack最基本资源的生命周期管理&#xff0c;如&#xff1a;云主机、云盘、镜像、网络。 该篇主要介绍&#xff0c;项目在Li…

STM32读取芯片内部温度

基于stm32f103cbt6这款芯片&#xff0c;原理部分请参考其他文章&#xff0c;此文章为快速上手得到结果&#xff0c;以结果为导向。 1.基础配置 打开stm32cubemx只需要勾选中 ADC1 Temperature Sensor Channel 2.代码分析 /** 函数名&#xff1a;float GetAdcAnlogValue(voi…

《计算机英语》 Unit 3 Software Engineering 软件工程

Section A Software Engineering Methodologies 软件工程方法论 Software development is an engineering process. 软件开发是一个工程过程。 The goal of researchers in software engineering is to find principles that guide the software development process and lea…

开启数字新纪元:全球首款开源AI女友,你的私人数字伴侣

在这个数字化飞速发展的时代,人工智能已经不再是科幻小说中的幻想,而是实实在在走进了我们的生活。今天,我们要介绍的,不仅仅是一项技术革新,更是一场关于陪伴的革命——全球首款开源AI女友,DUIX,已经横空出世! 🚀 革命性的开源平台 DUIX,由硅基智能精心打造,不…

GLM-4V模型学习

智谱AI引领技术前沿&#xff0c;推出了新一代预训练模型GLM-4系列&#xff0c;其中的GLM-4-9B作为开源版本&#xff0c;展现了其在人工智能领域的深厚实力。在语义理解、数学运算、逻辑推理、代码编写以及广泛知识领域的数据集测评中&#xff0c;GLM-4-9B及其人类偏好对齐的版本…

慎投!新增7本期刊被“On Hold“,14本影响因子下降!

本周投稿推荐 SSCI • 中科院2区&#xff0c;6.0-7.0&#xff08;录用友好&#xff09; EI • 各领域沾边均可&#xff08;2天录用&#xff09; CNKI • 7天录用-检索&#xff08;急录友好&#xff09; SCI&EI • 4区生物医学类&#xff0c;0.5-1.0&#xff08;录用…