前端实现简易版线上文件编辑器

前言

今天发现两个有意思的API, showDirectoryPickershowSaveFilePicker, 他们有什么用呢, 一言蔽之, 可以使用户在浏览器环境打开文件夹以及保存文件, 这么有意思的API既然被我看到了,那就好好开发下,简单做了个线上文件编辑器, 这是源码地址, 我们可以简单从源码去了解一下这两个API

简单介绍

showDirectoryPicker

showDirectoryPicker() 方法方法用于显示一个目录选择器,以允许用户选择一个目录,它的返回值为一个 Promise 对象,会兑现一个 FileSystemDirectoryHandle 对象。

showSaveFilePicker

showSaveFilePicker() 方法用于显示一个文件选择器,以允许用户保存一个文件。可以选择一个已有文件覆盖保存,也可以输入名字新建一个文件. 它的返回值为 一个 Promise 对象,会兑现一个 FileSystemFileHandle 对象。

FileSystemDirectoryHandle

FileSystemDirectoryHandle 接口提供了一个文件系统目录的句柄。它的实例属性和方法都是从FileSystemHandle, 同时它提供了一些异步迭代器的方法:

  • entries(): 返回给定对象自己的可枚举属性[key, value]对的新的异步迭代器。
  • keys(): 返回一个新的异步迭代器,其中包含FileSystemDirectoryHandle中每个项的键
  • values(): 返回一个新的异步迭代器,其中包含FileSystemDirectoryHandle对象中每个索引的值
  • [@@asyncIterator](): 默认情况下返回entries函数
FileSystemFileHandle

FileSystemFileHandle对象是一个代表文件的对象,它提供了一些方法来获取和操作文件,例如:

  • getFile:返回一个Promise对象,用于获取文件;
  • createSyncAccessHandle:返回一个FileSystemSyncAccessHandle对象,用于同步访问文件;
  • createWritable:返回一个Promise对象,用于创建一个可写流,用于写入文件;

使用API实现线上文件编辑器

首先脑子里明确一下效果,参考一下VSCODE, 左边是选择区,右边是编辑区,大概长这样(简陋了点)

然后我们选择了文件之后的效果大概是这样

好的,需求确定,那么开始写代码,逐步实现思路如下:

  1. 首先搭建一个 vue3 项目(直接原生一把梭也行),删掉一些自带的无用代码,给css做点简单的初始化

  2. APP.vue 引入两个组件, 左边的就叫Menu.vue,右边的就叫Editor.vue,写个左右布局

  3. 首先是实现左边的选择文件(Menu.vue),分析一下需求,左边菜单大概分两个状态:

    1. 一个是未选择,显示一个选择按钮,
    2. 一个是已选择,显示一个树状的文件夹结构
  4. 我们先从未选择开始实现,写一个按钮, 给按钮绑定一个事件 onOpenDirectory, 我们需要这个事件让用户选择一个文件夹,同时把用户选择的文件夹结构全部递归成一个树,显示出来,代码实现如下:

javascript 复制代码
const onOpenDirectory = async () => {
  fileContent.value = ''
  try {
    // 获取文件夹
    const handle = await showDirectoryPicker()
    fileTree.value = [await processFileTree(handle)]
  } catch {
    // 拒绝了授权
    ElMessage.info('您取消了操作')
  }
}

// 所有的 IO 操作都是异步的, 因为用 async await 递归出所有的文件
const processFileTree = async (handle) => {
  // handle.kind -> directory: 文件夹; file: 文件
  if (handle.kind == 'file') {
    return handle
  }
  handle.children = []
  const iterator = handle.entries()
  for await (const item of iterator) {
    const subItem = await processFileTree(item[1])
    handle.children.push(subItem)
  }

  return handle
}

showDirectoryPicker 这个API会给我们返回一个FileSystemDirectoryHandle对象,我们前面简单介绍了FileSystemDirectoryHandle这个对象,他有一个entries()方法会返回一个迭代器,我们可以通过递归这个迭代器去获取到一个树状的数据结构(这里注意,所有的IO操作都是异步的,请牢记这点),我们通过一个递归函数 processFileTree去拿到这个树的数据并定义一个响应式数据 fileTree 去接收它,同时显示出这个树状菜单(菜单我使用了 element-plus 的el-tree,懒得写UI了,按需引入 element-plus),到此,我们的文件夹菜单已经生成完毕

  1. 我们在点击左侧菜单时,如果是文件类型,那么我们需要给它显示在右边提供给用户去编辑,那我们来实现这个功能,给el-tree添加一个 @node-click="handleNodeClick" 实现,接着去实现 handleNodeClick 这个函数: 代码如下:
ini 复制代码
const handleNodeClick = async (node) => {
  // 渲染 file 的内容
  if (node.kind === 'file') {
    fileName.value = node.name
    const file = await node.getFile()
    const render = new FileReader()
    render.onload = (e) => {
      fileContent.value = e.target.result
    }
    render.readAsText(file, 'utf-8')
  }
}

这个就比较简单了,判断点击的node类型,如果是文件,那么就 new FileReader 对象去读取, 在onload中拿到整个文件的内容,这里由于我们要把数据给到兄弟组件 Editor.vue 中使用,所以我使用了 provide + inject, 这里使用 props + emit 也可以, 看个人选择,在app.vue中定义一个变量 fileContent 同时通过provide('fileContent', fileContent)给到后代元素去使用,在Menu.vueconst fileContent = inject('fileContent') 获取这个值,同时在render.onload回调里,将fileContent的值修改以便于Editor.vue使用,至此,我们左边菜单的功能已经全部实现了

  1. 接着我们去实现编辑区的功能,先定义定义一个 props 去接受 fileContent(也可以通过 inject),这里代码高亮的效果,我们可以使用 highlightjs 实现,首先 npm i highlight.js,再装一个vue 的highlight组件npm i @highlightjs/vue-plugin, 接着即可直接使用了。

  2. 虽然此时已经显示好了,高亮也有了,但是此时文件还不能编辑, 我们通过 contenteditable 属性来让该区域可编辑, 全局属性 contenteditable 是一个枚举属性,表示元素是否可被用户编辑。如果可以,浏览器会修改元素的组件以允许编辑,具体使用可以看 MDN, 这里不多赘述,此时编辑也实现了,我们需要实现保存功能,简单定义一个按钮,用于文件的保存,此时Editor.vue代码如下:

xml 复制代码
<template>
  <div class="container">
    <div v-show="props.content">
      <div class="title-box">
        <el-button size="small" type="primary" @click="handleSaveFile">保存文件</el-button>
      </div>
      <highlightjs ref="highlightRef" autodetect contenteditable="true" :code="props.content" />
    </div>
  </div>
</template>
  1. 接着我们就可以实现保存文件的功能了, handleSaveFile 函数用于保存,具体代码如下:
javascript 复制代码
const handleSaveFile = async () => {
  const content = highlightRef.value.$el.textContent
  const fileHandle = await window.showSaveFilePicker()
  const writableStream = await fileHandle.createWritable()
  await writableStream.write(content)
  await writableStream.close()
}

先获取到编辑区域的内容 content, 然后使用showSaveFilePicker() 前面我们说过showSaveFilePicker() 的返回值 FileSystemFileHandle 有个 createWritable 创建可读写的流,我们需要的就是这个,我们使用 fileHandle.createWritable()write(content) 把内容写进文件中, 这里记住,完成之后一定要调用close() 方法去关闭流,至此我们整个功能已经实现好了,大家可以自行美化一下这个简易的线上文件编辑

总结

通过 showDirectoryPickershowSaveFilePicker 我们可以实现文件的读取和写入保存,挺有意思的,不过 mdn 提示我们这是个实验性技术,在各位投入生产前一定要留意浏览器的支持性。

相关推荐
.生产的驴11 分钟前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq
认真敲代码的小火龙15 分钟前
git入门
git
布瑞泽的童话25 分钟前
无需切换平台?TuneFree如何搜罗所有你爱的音乐
前端·vue.js·后端·开源
白鹭凡37 分钟前
react 甘特图之旅
前端·react.js·甘特图
2401_8628867841 分钟前
蓝禾,汤臣倍健,三七互娱,得物,顺丰,快手,游卡,oppo,康冠科技,途游游戏,埃科光电25秋招内推
前端·c++·python·算法·游戏
书中自有妍如玉1 小时前
layui时间选择器选择周 日月季度年
前端·javascript·layui
Riesenzahn1 小时前
canvas生成图片有没有跨域问题?如果有如何解决?
前端·javascript
f8979070701 小时前
layui 可以使点击图片放大
前端·javascript·layui
小贵子的博客1 小时前
ElementUI 用span-method实现循环el-table组件的合并行功能
javascript·vue.js·elementui