前端实现简易版线上文件编辑器
前言
今天发现两个有意思的API, showDirectoryPicker 和 showSaveFilePicker, 他们有什么用呢, 一言蔽之, 可以使用户在浏览器环境打开文件夹以及保存文件, 这么有意思的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, 左边是选择区,右边是编辑区,大概长这样(简陋了点)
然后我们选择了文件之后的效果大概是这样
好的,需求确定,那么开始写代码,逐步实现思路如下:
-
首先搭建一个 vue3 项目(直接原生一把梭也行),删掉一些自带的无用代码,给css做点简单的初始化
-
APP.vue 引入两个组件, 左边的就叫Menu.vue,右边的就叫Editor.vue,写个左右布局
-
首先是实现左边的选择文件(Menu.vue),分析一下需求,左边菜单大概分两个状态:
- 一个是未选择,显示一个选择按钮,
- 一个是已选择,显示一个树状的文件夹结构
-
我们先从未选择开始实现,写一个按钮, 给按钮绑定一个事件
onOpenDirectory
, 我们需要这个事件让用户选择一个文件夹,同时把用户选择的文件夹结构全部递归成一个树,显示出来,代码实现如下:
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),到此,我们的文件夹菜单已经生成完毕
- 我们在点击左侧菜单时,如果是文件类型,那么我们需要给它显示在右边提供给用户去编辑,那我们来实现这个功能,给
el-tree
添加一个@node-click="handleNodeClick"
实现,接着去实现handleNodeClick
这个函数: 代码如下:
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.vue
中const fileContent = inject('fileContent')
获取这个值,同时在render.onload回调里,将fileContent的值修改以便于Editor.vue
使用,至此,我们左边菜单的功能已经全部实现了
-
接着我们去实现编辑区的功能,先定义定义一个 props 去接受 fileContent(也可以通过 inject),这里代码高亮的效果,我们可以使用
highlightjs
实现,首先npm i highlight.js
,再装一个vue 的highlight组件npm i @highlightjs/vue-plugin
, 接着即可直接使用了。 -
虽然此时已经显示好了,高亮也有了,但是此时文件还不能编辑, 我们通过
contenteditable
属性来让该区域可编辑, 全局属性contenteditable
是一个枚举属性,表示元素是否可被用户编辑。如果可以,浏览器会修改元素的组件以允许编辑,具体使用可以看 MDN, 这里不多赘述,此时编辑也实现了,我们需要实现保存功能,简单定义一个按钮,用于文件的保存,此时Editor.vue
代码如下:
<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>
- 接着我们就可以实现保存文件的功能了,
handleSaveFile
函数用于保存,具体代码如下:
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()
方法去关闭流,至此我们整个功能已经实现好了,大家可以自行美化一下这个简易的线上文件编辑
总结
通过 showDirectoryPicker 和 showSaveFilePicker 我们可以实现文件的读取和写入保存,挺有意思的,不过 mdn 提示我们这是个实验性技术,在各位投入生产前一定要留意浏览器的支持性。
转载自:https://juejin.cn/post/7278952595423313954