本文最后更新于: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 尝试写一个基本的图片查看器,它将做到以下几件事以尽可能贴近原生应用的行为
安装之后可以离线访问
支持从资源管理器打开图片
支持拖拽图片
窗口尺寸记忆支持
以下会涉及到各种常见 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 添加正确的类型定义。
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' , }, ], }, }), ], })
现在,它应该可以被安装并作为独立窗口打开了,并且能在的应用程序列表中看到
与文件系统集成 虽然安装到系统中,但它现在仍然没有集成到系统的文件管理器中 – 许多原生应用程序都允许从文件管理器中打开特定类型的文件。想要做到做到这一点,需要做两件事情
在 manifest 中声明 file_handlers
访问 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 : { '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 > )) ) }
现在,我们可以从系统文件管理器中打开它了
如果你想尝试它,可以访问 https://image-viewer.rxliuli.com/ 安装尝试
结语 有趣的是,chrome 仍然在不断添加各种各样的 api 到 web 上,以尽可能缩小与原生应用的功能差异。在 chrome 开发者文档中 可以看到各种各样奇怪的 api,某些看起来与传统意义上的 web 网站大相径庭,甚至有人吐槽说:“applet: 不行,web api: 行”。话虽如此,兜兜转转,发展的螺旋式又转回了这一边。
完整项目见:https://github.com/rxliuli/image-viewer