web 文件系统探索

本文最后更新于:2022年11月25日 下午

前言

浏览器到了今天已经非常强大了,足以称之为操作系统。很多人(包括吾辈)可能对网页的印象仍然停留在网站,一些非富交互式的的应用,只是用来展示和操作后端数据,但复杂程度不高。而在最近,吾辈发现了 file system access api,它是 chrome 推出的一组可以操作本地文件的 api,这也是第一次可以直接将文件写入到本地而不需要使用 indexeddb 之类的模拟的方法。虽然尚不完善,但已经足以让 sqlite 这种项目开始考虑支持在 web 上基于文件系统实现支持了,参考:https://sqlite.org/wasm/doc/ckout/index.md

那么,就算有了文件系统,我们能做什么呢?

  • 首先,我们有可能不再需要 nodejs,尽管 nodejs 提供了各式各样的 api,但最常用的也只有 fs/fetch 罢了,浏览器的原生支持意味着只需要这两个 api 的应用不需要使用 nodejs 了
  • 其次,我们之前提到的一些应用不再需要使用 electron 打包,例如图片编辑器、drawio 之类的,都不再需要 electron 客户端了
  • 最后,某些依赖于文件系统的 lib 可以在 web 上复用,例如上面提到的 sqlite 数据库

下面将使用文件系统 api 尝试写一个基本的图片查看器,它将做到以下几件事以尽可能贴近原生应用的行为

  1. 安装之后可以离线访问
  2. 支持从资源管理器打开图片
  3. 支持拖拽图片
  4. 窗口尺寸记忆支持

最终效果

以下会涉及到各种常见 web 开发中不那么常见的东西,仅做提及不做深入,相关资料建议参考 https://web.dev/

  • service worker: web 离线支持
  • manifest.json: web 应用元数据
  • file system access api: 文件系统
  • preact: react 的一个更小的实现

拖拽图片显示

首先,我们实现一个可以拖拽图片到网页并显示的 web 应用,好吧,这实际上没什么难的。这里的关键是通过 event 获取到 File 对象,然后就可以创建 blob url 渲染图片了。

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
33
34
35
36
37
38
39
40
import css from './App.module.css'
import 'file-system-access'
import { useEffect, useState } from 'preact/hooks'

export function App() {
const [url, setUrl] = useState<string>()

async function onDrop(ev: DragEvent) {
ev.preventDefault()
const items = [...(ev.dataTransfer?.items ?? [])].filter(
(item) => item.kind === 'file' && item.type.startsWith('image/'),
)
if (items.length === 0) {
alert('找不到图片文件')
return
}
const file = items[0].getAsFile()
if (!file) {
alert('无法读取文件')
return
}
setUrl(URL.createObjectURL(file))
}
function onDragOver(ev: DragEvent) {
ev.preventDefault()
}
useEffect(() => {
window.addEventListener('drop', onDrop)
window.addEventListener('dragover', onDragOver)
return () => {
window.removeEventListener('drop', onDrop)
window.removeEventListener('dragover', onDragOver)
}
}, [])
return url ? (
<img class={css.img} src={url} />
) : (
<div class={css.content}>拖拽图片到这儿</div>
)
}

当然,这里需要需要导入 file-system-access 模块,因为这个 api 是 chrome 系独有,firefox/safari 不支持,需要使用 polyfill 添加正确的类型定义。

1669386555147.png

pwa 实现

虽然我们实现了一个简单的 web 网站,但它并不能安装到系统,这里我们需要让它变成 pwa 应用。我不准备解释它是什么,因为吾辈也没怎么太多了解,具体可以参考:https://web.dev/learn/pwa/,这是一个很长的列表。
幸运的是,吾辈使用的 vite 有一个插件可以开箱即用的做到这一点,它是 vite-plugin-pwa
吾辈安装它之后仍然需要配置,因为吾辈想要

  • 自定义应用的显示名,使用 name/short_name
  • 自定义应用 icon,使用 icons
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
33
34
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
import { cssdts } from '@liuli-util/vite-plugin-css-dts'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
plugins: [
preact(),
cssdts(),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true,
},
manifest: {
id: 'rxliuli.image-viewer',
short_name: '图片查看器',
name: '图片查看器',
icons: [
{
src: '/icons/logo512x512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: '/icons/logo.svg',
sizes: 'any',
type: 'image/svg',
},
],
},
}),
],
})

现在,它应该可以被安装并作为独立窗口打开了,并且能在的应用程序列表中看到

1669386797740.png
1669386739999.png
1669386864465.png

与文件系统集成

虽然安装到系统中,但它现在仍然没有集成到系统的文件管理器中 – 许多原生应用程序都允许从文件管理器中打开特定类型的文件。想要做到做到这一点,需要做两件事情

  1. 在 manifest 中声明 file_handlers
  2. 访问 window.launchQueue 获取打开的文件
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
33
34
35
36
37
export default defineConfig({
plugins: [
// 其他配置...
VitePWA({
// 其他配置...
manifest: {
// 其他配置...
file_handlers: [
{
action: './',
accept: {
// ref: https://developer.mozilla.org/zh-CN/docs/Web/Media/Formats/Image_types
'image/*': [
'.apng',
'.avif',
'.bmp',
'.gif',
'.ico',
'.cur',
'.jpg',
'.jpeg',
'.jfif',
'.pjpeg',
'.pjp',
'.png',
'.svg',
'.tif',
'.tiff',
'.webp',
],
},
},
],
},
}),
],
})

好吧,可能是这个 api 太新(chrome 102 添加),导致现在仍然没有类型定义,不过这不复杂,所以自己定义一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
declare global {
export const launchQueue: LaunchQueue

interface LaunchQueue {
setConsumer(consumer: LaunchConsumer): void
}

interface LaunchConsumer {
(launchParams: LaunchParams): void
}

class LaunchParams {
readonly files: readonly FileSystemFileHandle[]
}
}

然后就可以访问它了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export function App() {
// 其他代码...
const [mounted, setMounted] = useState(false)
useEffect(() => {
if ('launchQueue' in window && 'files' in LaunchParams.prototype) {
launchQueue.setConsumer(async (launchParams) => {
if (!launchParams.files.length) {
return
}
setUrl(URL.createObjectURL(await launchParams.files[0].getFile()))
})
}
setMounted(true)
}, [])
return (
mounted &&
(url ? (
<img class={css.img} src={url} />
) : (
<div class={css.content}>拖拽图片到这儿</div>
))
)
}

现在,我们可以从系统文件管理器中打开它了

1669387508422.png

如果你想尝试它,可以访问 https://image-viewer.rxliuli.com/ 安装尝试

结语

有趣的是,chrome 仍然在不断添加各种各样的 api 到 web 上,以尽可能缩小与原生应用的功能差异。在 chrome 开发者文档中 可以看到各种各样奇怪的 api,某些看起来与传统意义上的 web 网站大相径庭,甚至有人吐槽说:“applet: 不行,web api: 行”。话虽如此,兜兜转转,发展的螺旋式又转回了这一边。

完整项目见:https://github.com/rxliuli/image-viewer


web 文件系统探索
https://blog.rxliuli.com/p/b0cb4560702945069f8c885bc5244ef8/
作者
rxliuli
发布于
2022年11月25日
许可协议