由浅入深—前端文件操作5种方式

表单文件

  • 兼容性最好的文件上传方式,也是各类react ui库中「Upload」组件最常见的底层实现
  • 安全性最好,无法拿到文件在本地磁盘的真实路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function Main() {
const onChange = (ev: any) => {
const target = ev.target
console.log("files are", target.files)

const reader = new FileReader();
reader.readAsDataURL(target.files[0])
reader.onload = () => {
console.log("临时文件URL是:", reader.result)
}
}

return <div>
<form>
<input type="file" id="file-input" name="fileContent" onChange={onChange} multiple/>
</form>
</div>
};

File对象:

  • 继承自 Blob 对象,所以可以被 FileReader 读取
  • 可以将 Blob 转换成各种形式,比如 base64 编码的 URL、代表文件字节内容ArrayBuffer

Untitled.png

FileReader:

  • 它是一个对象,其唯一目的是从 Blob(因此也从 File)对象中读取数据
  • 它使用事件来传递数据,因为从磁盘读取数据可能比较费时间。

Untitled.png

参考文章:

拖拽文件

  • H5支持 Drag 拖拽事件,需要用到4个事件控制:
    • 区域外:dragleave,离开范围
    • 区域内:dragenter,用来确定放置目标是否接受放置。
    • 区域内移动:dragover,用来确定给用户显示怎样的反馈信息
    • 完成拖拽(落下)drop:,允许放置对象。
  • 必须监听并且禁用 onDragOver 的默认行为,才能避免浏览器在新Tab上自动打开文件,并且触发自定义的 onDrop 行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export default function Main() {
const handleDrag = (ev: any) => {
ev.preventDefault();
ev.stopPropagation();
console.log("[handleDrag] type is", ev.type)
}

const handleDrop = (ev: any) => {
ev.preventDefault();
ev.stopPropagation();
console.log('[handleDrop] type is', ev.type)
}

return <div>
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
style={{ border: 'black 2px solid', width: '400px', height: '400px'}}
>
drop your image here
</div>
</div>
};

控制台输出:

  • onDrop 回调中能拿到文件信息
  • 这里也是 File 对象,使用 FileReader 就可以进行操作

Untitled.png

疑问:不知道为啥直接查看 event 对象上的属性里的 files ,看到的是 length 为0。而访问 event.dataTransfer.files 就可以拿到 length 不为0,有内容的 files 对象。

Untitled.png

参考文章:

粘贴文件

  • html element 变成可编辑后,可以粘贴文件
  • 粘贴操作在 onpaste 中捕获
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function Main() {
const handlePaste = (ev: any) => {
console.log('[handlePaste] ev is', ev)
console.log('[handlePaste] files are', ev.clipboardData.files)
}

return <div>
<div contentEditable="true"
onPaste={handlePaste}
style={{ border: 'black 2px solid', width: '400px', height: '400px'}}
>
hello, paste your image here
</div>
</div>
};

控制台输出:

  • 同样是 File 对象以及关键的信息

Untitled.png

HTML结构变化:

  • 在Chrome中,复制时,还插入了img标签,内容是文件图标的地址(base64格式)

Untitled.png

File System Access API:操作本地文件系统

  • 功能最强大,可以直接在浏览器中,读写本地的文件
  • 存在安全风险,因为操作跳出了浏览器这个沙盒,触达了操作系统,需要用户授权
  • 兼容性存在问题,目前主流浏览器均支持。vscode在线版就使用的这套方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export default function Main() {

const readLocalFs = async () => {
const dirHandle = await (window as any).showDirectoryPicker()
const file = await dirHandle.getFileHandle("package.json", {
create: true
}) // 获取到一个 File 对象
const fileData = await file.getFile(); // 获取到 File 的数据
const text = await fileData.text() // 拿到File数据的文本形式
console.log('>>> text is', text)
}

const writeLocalFs = async () => {
const dirHandle = await (window as any).showDirectoryPicker()
const file = await dirHandle.getFileHandle("yuanxin.me.json", {
create: true
})
const sampleConfig = JSON.stringify({
author: 'dongyuanxin',
blog: "https://yuanxin.me"
})
const blob = new Blob([sampleConfig]) // 创造blob对象
const writableStream = await file.createWritable(); // 打开句柄
await writableStream.write(blob); // 流式写入
await writableStream.close(); // 关闭文件句柄
}

return <div>
<button onClick={readLocalFs}>点我打开文件夹</button>
<button onClick={writeLocalFs}>点我创建+写入 yuanxin.me.json 文件</button>
</div>
};

浏览器向用户索要「读」、「写」授权:

Untitled.png

Untitled.png

vscode.dev 上直接编写本地代码,浏览器向用户索要授权:

Untitled.png

读取文件的效果:

Untitled.png

写入文件的效果:

  • 点击按钮并且授权后,直接向本地文件夹创建了 yuanxin.me.json 文件
  • Blob 对象成功写入文件中

Untitled.png

参考文档:

isomorphic-git/ightning-fs:浏览器端同构文件系统

背景:之前在实现低代码编辑器时,需要在浏览器中通过oauth,连接 github,并且将代码上传上去。调研到了 isomorphic-git 这个库,它是一个纯浏览器端的git解决方案,底层是基于 isomorphic-git/lightning-fs 实现的一套浏览器端的同构文件系统,模拟文件系统的增删改查。

特点:

  • 基于 IndexedDB 实现,兼容性好
  • 不会操作本地文件系统,只在浏览器沙盒中运行,安全性好
  • API 设计上仿照 Node.js 的 fs 官方库,支持文件/文件夹的增删改查,支持 Promise API,使用方便
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import FS from '@isomorphic-git/lightning-fs'

const { promises: fs } = new FS('yuanxin.me')

export default function App() {
const createFileAndRead = async () => {
const folderName = '/tmp'
fs.mkdir(folderName)
console.log(`create folder "${folderName}" success`)

const fileName = folderName + '/test.json'
await fs.writeFile(fileName, JSON.stringify({
site: 'https://yuanxin.me',
boy: true,
location: {
country: 'cn',
city: 'hangzhou'
}
}))
console.log(`create file "${fileName}" success`)

const fileContent = await fs.readFile(fileName);
console.log(`read file content:`, fileContent)
}
return <div>
<div onClick={createFileAndRead}>点我创建文件夹和文件,写入内容并且读取</div>
</div>
}

控制台输出:

  • 成功创建文件夹和文件,并且向文件写入内容
  • 以字节的形式,读出文件内容

Untitled.png

IndexedDB:

  • 左侧创建了一个DB,专门用来存储文件系统的内容
  • 右侧是文件系统的具体内容,包括目录结构、文件内容、文件(夹)属性

Untitled.png

总结

在之前的工作中,都有实际使用这几种方式来解决具体的业务问题:表单文件是在目前在字节电商团队中频繁使用的;粘贴文件和拖拽文件要追溯到几年前在鹅厂TEG做组件库和富文本编辑时;至于同构和file system access api时在CSIG做微搭低代码时深入使用。整体梳理一遍后,知识脉络确实更加清晰了。