文件处理
不同于文件上传,这里介绍的是本地到浏览器的文件处理过程
表单文件
- 兼容性最好的文件上传方式,也是各类 react ui 库中「Upload」组件最常见的底层实现
- 安全性最好,无法拿到文件在本地磁盘的真实路径
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
FileReader:
- 它是一个对象,其唯一目的是从 Blob(因此也从 File)对象中读取数据
- 它使用事件来传递数据,因为从磁盘读取数据可能比较 费时间。
参考文章:
- https://zh.javascript.info/file
- https://zh.javascript.info/blob
- https://juejin.cn/post/6844903513882001422
拖拽文件
-
H5 支持 Drag 拖拽事件,需要用到 4 个事件控制:
- 区域外:dragleave,离开范围
- 区域内:dragenter,用来确定放置目标是否接受放置。
- 区域内移动:dragover,用来确定给用户显示怎样的反馈信息
- 完成拖拽(落下)drop:,允许放置对象。
-
必须监听并且禁用 onDragOver 的默认行为,才能避免浏览器在新 Tab 上自动打开文件,并且触发自定义的 onDrop 行为
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 就可以进行操作 参考文章:
- https://www.codemzy.com/blog/react-drag-drop-file-upload
- https://stackoverflow.com/questions/50230048/react-ondrop-is-not-firing
粘贴文件
- html element 变成可编辑后,可以粘贴文件
- 粘贴操作在 onpaste 中捕获
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 对象以及关键的信息
HTML 结构变化:
- 在 Chrome 中,复制时,还插入了 img 标签,内容是文件图标的地址(base64 格式)
File System Access API:操作本地文件系统
- 功能最强大,可以直接在浏览器中,读写本地的文件
- 存在安全风险,因为操作跳出了浏览器这个沙盒,触达了操作系统,需要用户授权
- 兼容性存在问题,目前主流浏览器均支持。vscode 在线版 就使用的这套方案
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>
};
浏览器向用户索要「读」、「写」授权: 在 vscode.dev 上直接编写本地代码,浏览器向用户索要授权: 读取文件的效果: 写入文件的效果:
- 点击按钮并且授权后,直 接向本地文件夹创建了 yuanxin.me.json 文件
- 将 Blob 对象成功写入文件中 参考文档:
- https://fjolt.com/article/javascript-new-file-system-api
- https://wicg.github.io/entries-api/#api-files-directories
- https://blog.devgenius.io/access-the-filesystem-with-javascript-c36577dc0bb4
isomorphic-git/lightning-fs:浏览器端同构文件系统
背景:之前在实现低代码编辑器时,需要在浏览器中通过 oauth,连接 github,并且将代码上传上去。调研到了 isomorphic-git 这个库,它是一个纯浏览器端的 git 解决方案,底层是基于 isomorphic-git/lightning-fs 实现的一套浏览器端的同构文件系统,模拟文件系统的增删改查。 特点:
- 基于 IndexedDB 实现,兼容性好
- 不会操作本地文件系统,只在浏览器沙盒中运行,安全性好
- API 设计上仿照 Node.js 的 fs 官方库,支持文件/文件夹的增删改查,支持 Promise API,使用方便
import FS from "@isomorphic-git/lightning-fs";
const { promises: fs } = new FS("yuanxin.me");
export default function App() {
const createFileAndRead = async () => {
const folderName = "/tmp";
await 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>
);
}
控制台输出:
- 成功创建文件夹和文件,并且向文件写入内容
- 以字节的形式,读出文件内容
IndexedDB:
- 左侧创建了一个 DB,专门用来存储文件系统的内容
- 右侧是文件系统的具体内容,包括目录结构、文件内容、文件(夹)属性
总结 在之前的工作中,都有实际使用这几种方式来解决具体的业务问题:
- 表单文件是在目前在字节电商团队中频繁使用的;
- 粘贴文件和拖拽文件要追溯到几年前在鹅厂 TEG 做组件库和富文本编辑时;
- 至于同构和 file system access api 时在 CSIG 做微搭低代码时深入使用。整体梳理一遍后,知识脉络更清晰了。