news 2026/9/10 16:53:58

Remix file-storage 包完整演进与实现解析:从 LocalFileStorage 到 createFsFileStorage 的文件存储 API 迁移指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Remix file-storage 包完整演进与实现解析:从 LocalFileStorage 到 createFsFileStorage 的文件存储 API 迁移指南

Remix file-storage 包完整演进与实现解析:从 LocalFileStorage 到 createFsFileStorage 的文件存储 API 迁移指南

【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix

@remix-run/file-storage是 Remix 生态中专用于服务端File对象的 key/value 存储库,为本地磁盘与内存提供统一的存储后端。本文以 packages/file-storage/CHANGELOG.md 的版本演进为骨架,结合当前仓库源码逐一拆解其 API 设计、分页列举、哈希分片与 LazyFile 流式读取等底层实现,并给出从旧版LocalFileStorage/MemoryFileStorage类到新版工厂函数createFsFileStorage()/createMemoryFileStorage()的完整迁移路径。读完本文,你将掌握该库全部核心操作(get/set/put/remove/has/list)的用法、参数语义、默认值以及版本升级时的破坏性变更清单。

一、包定位:面向服务端 File 对象的 key/value 存储

packages/file-storage/README.md 对该包的定位做了清晰描述:它提供面向服务端File对象的 key/value 存储接口,让 Remix 应用能够在本地磁盘与内存两种后端之间使用同一套 API。其核心特性包括:

  • 简单 API:直观的 key/value 接口(类似 Web Storage,但存储的是File而非字符串);
  • 多后端:内置文件系统后端与内存后端,另有独立的 file-storage-s3 提供 S3 后端;
  • 流式支持:可从存储中流式读取与写入文件内容;
  • 元数据保留:完整保留file.namefile.typefile.sizefile.lastModifiedFile元数据。

从 package.json 的exports字段可以看到当前包的三个公开入口:@remix-run/file-storage(类型定义)、@remix-run/file-storage/fs(文件系统后端)与@remix-run/file-storage/memory(内存后端),对应的源码文件分别为 src/index.ts、src/fs.ts 与 src/memory.ts。

二、当前 API 面貌:FileStorage 接口与两种后端

CHANGELOG.md 中 v0.13.0 是一次里程碑式的破坏性变更:LocalFileStorage类被createFsFileStorage(directory)工厂函数取代,MemoryFileStorage类被createMemoryFileStorage()工厂函数取代,随后 v0.13.5 又引入FileLike别名并让FileStorage接口对具体后端返回的File值类型泛型化。当前版本的完整接口定义位于 src/lib/file-storage.ts,包含六个方法:

方法签名语义
getget(key): file \| null按 key 读取文件,不存在时返回null
hashas(key): boolean判断某 key 是否存在
setset(key, file): void将文件写入指定 key
putput(key, file): file写入并立即返回由该存储支撑的新文件(set+get的便捷组合)
removeremove(key): void删除指定 key 的文件
listlist(options?): ListResult按条件列举存储中的文件,支持分页

2.1 文件系统后端:createFsFileStorage

src/lib/backends/fs.ts 实现了createFsFileStorage(directory)。创建时会校验传入路径:

  • 若路径已存在但不是目录,抛出Path "..." is not a directory错误;
  • 若路径不存在,则递归创建目录(fs.mkdirSync(rootDir, { recursive: true }))。

源码注释还强调了两个重要约定:

  1. 不做覆盖防护:实现“不会尝试避免覆盖已有文件”,因此传入的目录应是一个专门为本次存储对象新建、独占使用的目录;
  2. key 与磁盘文件名无关:key 可以是任意字符串(包括文件系统不允许的字符),多个同名File也可以存进同一个存储对象——实际落盘路径由 key 的哈希决定,而非文件名。

一个完整的读写示例(来自 README.md):

import { createFsFileStorage } from 'remix/file-storage/fs' let storage = createFsFileStorage('./user/files') let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' }) let key = 'hello-key' // 将文件写入存储 await storage.set(key, file) // 稍后读取 let fileFromStorage = await storage.get(key) if (fileFromStorage != null) { // 原文件元数据完整保留 fileFromStorage.name // 'hello.txt' fileFromStorage.type // 'text/plain' // 文件系统后端返回 LazyFile,可直接流式读取 let response = new Response(fileFromStorage.stream()) } // 从存储中删除 await storage.remove(key)

2.2 内存后端:createMemoryFileStorage

src/lib/backends/memory.ts 用Map<string, File>实现同名接口。值得注意的实现细节:putFile在写入时会通过file.arrayBuffer()将内容缓冲为独立副本,再以new File([buffer], name, { lastModified, type })重建一个新File存入 Map——这既是 v0.6.0 中"缓冲 MemoryFileStorage 中文件内容"的延续,也保证了存入的文件不会因外部File的后续变更而受影响。

三、list():前缀过滤、元数据列举与游标分页

storage.list(options)是 v0.6.0 加入的核心能力,其options在 src/lib/file-storage.ts 中有精确定义,CHANGELOG.md v0.6.0 一节给出了完整的参数语义:

选项类型说明
cursorstring不透明的分页游标,用于在存储的 key 间翻页
includeMetadatabooleantrue时在结果中包含文件元数据
limitnumber返回文件的最大数量
prefixstring只返回 key 以该字符串开头的文件

3.1 基础列举与元数据

不带任何选项时,result.files{ key: string }对象的数组:

let result = await storage.list({ prefix: 'user123/' }) console.log(result.files) // [ // { key: "user123/..." }, // { key: "user123/..." }, // ... // ]

传入includeMetadata: true后,每个条目扩展为完整的 FileMetadata(lastModifiednamesizetype均为毫秒时间戳 / 文件名 / 字节数 / MIME 类型):

let result = await storage.list({ prefix: 'user123/', includeMetadata: true }) console.log(result.files) // [ // { // key: "user123/...", // lastModified: 1737955705270, // name: "hello.txt", // size: 16, // type: "text/plain" // }, // ... // ]

3.2 游标分页

分页通过结果对象中的不透明cursor属性完成:若cursor不为undefined,说明还有更多文件,将其原样传回下次调用的options即可取得下一页。完整遍历整个存储的惯用写法:

let result = await storage.list() console.log(result.files) while (result.cursor !== undefined) { result = await storage.list({ cursor: result.cursor }) console.log(result.files) }

limit用于控制每次返回的条数。两个后端的默认值有所不同(来自源码):

  • 文件系统后端 fs.ts:limit默认32
  • 内存后端 memory.ts:limit默认Infinity

fs.test.ts 的lists files with pagination用例验证了完整分页闭环:limit: 2返回 2 条并给出非空游标,用该游标继续list取回剩余 3 条,两次结果合并后与全部 5 个 key 完全一致;limit: 0则返回空数组且无游标。lists files by key prefix(fs.test.ts)验证了prefix: 'b'只返回bb/c两个 key。

四、底层实现原理:哈希分片、元数据文件与 LazyFile

4.1 SHA-256 哈希与两级目录分片

文件系统后端在落盘前会对 key 计算哈希(fs.ts 的computeHash,默认SHA-256),并以哈希的前 2 个十六进制字符作为子目录名、完整哈希作为文件名基础:

  • 文件本体:<rootDir>/<hash前2位>/<hash>.dat
  • 元数据:<rootDir>/<hash前2位>/<hash>.meta.json
async function getPaths(key: string) { let hash = await computeHash(key) let directory = path.join(rootDir, hash.slice(0, 2)) return { directory, filePath: path.join(directory, `${hash}.dat`), metaPath: path.join(directory, `${hash}.meta.json`), } }

这正是 CHANGELOG 中两条演进的核心内容:

  • v0.4.0:引入"分片存储目录"(shards storage directories),将文件分散到多个子目录以提升文件系统扩展性,并修复了并发set的竞态问题;
  • v0.6.0:分片目录名从8 个字符缩减为 2 个字符(BREAKING CHANGE),在可扩展性与目录数量之间取得平衡——2 位十六进制最多 256 个分片目录;
  • v0.9.0remove删除文件后,若所在分片目录已空则一并移除(fs.ts 中readdir判空后rmdir),fs.test.ts 的removes empty hash directories after removing files用例专门覆盖此行为。

4.2 元数据持久化与文件大小入元数据

写入时(putFile),后端会将keylastModifiednamesizetype序列化为 JSON 写入.meta.json

let meta: FileMetadata = { key, lastModified: file.lastModified, name: file.name, size: file.size, type: file.type, } await fsp.writeFile(metaPath, JSON.stringify(meta))

v0.13.5的变更点是:文件系统存储改为把文件大小持久化进元数据,而不是在带元数据列举时另行推导 size——fs.test.ts 的stores file size in metadata用例直接读取磁盘上的.meta.json断言size字段与源文件一致,验证了这一点。

4.3 返回 LazyFile,支持流式读取

getput返回的并不是内存中的普通File,而是通过openLazyFile(filePath, { lastModified, name, type })(来自@remix-run/fs)打开的LazyFile(见 fs.ts)。LazyFile 是 lazy-file 提供的流式File实现,内容按需从磁盘流式读取,因此可以直接:

let response = new Response(fileFromStorage.stream())

而不会先把整个文件读入内存。这正是 README "Streaming Support" 特性的底层支撑,也是 v0.13.1 起改用@remix-run/fsopenLazyFile()新 API、v0.12.0 将@remix-run/fs引入依赖关系的原因。

4.4 旧文件自动清理

v0.2.1起,LocalFileStorage在向同一 key 写入新文件时会自动清理旧文件。当前实现中,remove采用Promise.all([unlink(filePath), unlink(metaPath)])同时删除.dat.meta.json,并以isNoEntityErrorENOENT)容错,保证对不存在文件的删除不会抛错。

五、put():写入后立即取得可读文件

v0.5.0新增storage.put(key, file),作为set(key, file)+get(key)这一高频组合的便捷封装。使用前后对照:

// 之前 await storage.set(key, file) let newFile = await storage.get(key)! // 之后 let newFile = await storage.put(key, file)

对文件系统后端而言,put内部走同一套putFile流程:写文件 → 写元数据 →openLazyFile返回可直接流式读取的LazyFile(fs.ts);内存后端则返回缓冲后的新File(memory.ts)。fs.test.ts 的puts files用例验证了put返回文件在nametypelastModifiedsize上与源文件一致,且has(key)立即为真。

六、版本演进时间线:从 v0.1.0 到 v0.13.7

综合 CHANGELOG.md 全部条目,可梳理出该包从 2024-08 到 2025-11 的关键演进:

版本日期关键变更类型
v0.1.02024-08-24初始发布
v0.2.02024-08-26LocalFileStorage/MemoryFileStorage分别移到file-storage/localfile-storage/memory导出破坏性
v0.2.12024-09-04同 key 写入新文件时自动清理旧文件修复
v0.3.02024-11-14新增 CommonJS 构建;升级 lazy-file@3.1.0增强
v0.4.02025-01-08修复并发set竞态;存储目录分片增强/修复
v0.4.12025-01-10修复 npm 包中file-storage/local类型缺失修复
v0.5.02025-01-25新增storage.put(key, file)增强
v0.6.02025-02-04分片目录名 8 字符改 2 字符;内存后端缓冲文件内容;新增storage.list(options)破坏性
v0.6.12025-02-06修复与form-data-parser配合使用的回归修复
v0.7.02025-06-10/src打入 npm 包("go to definition" 直达源码);统一类型;esbuild 直接构建增强
v0.8.02025-07-21包名从@mjackson/file-storage改名为@remix-run/file-storage破坏性
v0.9.02025-07-25LocalFileStorage删除文件后移除空的分片目录增强
v0.10.02025-10-22移除 CommonJS 构建,仅保留 ESM破坏性
v0.11.02025-11-05@remix-run/lazy-file移入peerDependencies;改用tsc构建,dist目录镜像src布局构建
v0.12.02025-11-20新增@remix-run/fspeer dependency,改从@remix-run/fs导入依赖
v0.13.02025-11-25类改工厂函数(见下节)破坏性
v0.13.1升级@remix-run/fspeer 依赖,使用新openLazyFile()API依赖
v0.13.2@remix-run/*peer 依赖改为普通依赖依赖
v0.13.3 ~ v0.13.4滚动升级fslazy-file依赖依赖
v0.13.5新增FileLike别名;FileStorage泛型化;createFsFileStorage()暴露LazyFile返回类型;文件大小持久化进元数据增强
v0.13.6 ~ v0.13.7滚动升级fslazy-file依赖依赖

七、v0.13.0 迁移指南:类改为工厂函数

v0.13.0 是迁移成本最高的一次破坏性变更,CHANGELOG.md 给出了标准的前后对照。变更前

import { LocalFileStorage } from '@remix-run/file-storage/local' import { MemoryFileStorage } from '@remix-run/file-storage/memory' let fsStorage = new LocalFileStorage('./files') let memoryStorage = new MemoryFileStorage()

变更后

import { createFsFileStorage } from '@remix-run/file-storage/fs' import { createMemoryFileStorage } from '@remix-run/file-storage/memory' let fsStorage = createFsFileStorage('./files') let memoryStorage = createMemoryFileStorage()

注意两点变化:

  1. 导入路径变化file-storage/local变为file-storage/fs(同时入口也从旧版导出路径迁移为 package.json 中声明的./fs./memory子路径);
  2. 实例化方式变化new关键字不再使用,改由工厂函数直接返回存储对象;调用点不再需要new,其余方法签名不变,因此业务代码中set/get/list/remove的调用无需改动。

在 v0.13.5 之后,工厂函数还有类型层面的收益:createFsFileStorage()返回FileStorage<LazyFile>createMemoryFileStorage()返回FileStorage<File>,编译器能在调用get()/put()时就明确你拿到的具体文件类型。fs.test.ts 顶部就有一段"编译期 API 契约检查",断言原生FileLazyFile均满足FileLike,且createFsFileStorage的返回类型满足FileStorage<LazyFile>——一旦 API 契约漂移,TypeScript 会直接让测试文件编译失败。

八、工程化与依赖演进:ESM-only、tsc 构建与依赖策略

CHANGELOG 中还有一组不改变 API 但影响使用方式的工程化变更,升级时同样需要留意:

  • v0.10.0 起仅支持 ESM:CommonJS 构建被移除。若项目仍处于 CommonJS 环境,需要使用动态import()引入该包;
  • v0.7.0 起 npm 包包含/src:类型定义与源码布局一致,IDE 的 "go to definition" 可以直接跳到真实源码,便于阅读与调试;
  • v0.11.0 起用tsc构建dist目录镜像src目录布局,替代此前 esbuild/tsup 的扁平化输出,模块间相对路径在构建后保持一致;
  • 依赖策略的两次转向:v0.11.0 将@remix-run/lazy-file移入 peerDependencies,v0.12.0 新增@remix-run/fspeer dependency,而v0.13.2 又将@remix-run/*peer 依赖改回普通 dependencies(见 package.json:@remix-run/fs@remix-run/lazy-file均为直接依赖)——这意味着安装@remix-run/file-storage时会自动带上这两个依赖,无需手动安装 peer 依赖。

九、生态协作:与 form-data-parser、lazy-file、S3 的关系

README.md 列出了三个关联包,构成完整的文件上传-存储链路:

  • form-data-parser:解析multipart/form-data请求中的FileUpload,与 file-storage 配合即可"边解析边入库"。v0.6.1 修复的正是二者配合时的回归问题;fs.test.ts 的集成用例演示了完整流程:parseFormData(request, async (file) => { await storage.set('hello', file) })之后,storage.list({ includeMetadata: true })能取回 key、文件名、大小、MIME 类型与时间戳全部元数据;
  • lazy-file:file-storage 内部使用的流式File实现,文件系统后端的get/put返回的就是LazyFile
  • file-storage-s3:同一FileStorage接口的 S3 后端,业务代码可在本地磁盘、内存与 S3 之间无痛切换。

十、总结与升级建议

@remix-run/file-storage的演进史本质上是一个"API 从类到工厂、后端从单一到可插拔、底层从整读整写到流式分片"的收敛过程。对使用方而言,当前版本最重要的三点结论:

  1. 只用工厂函数创建存储:文件系统用createFsFileStorage('./dir'),内存用createMemoryFileStorage(),二者共用同一FileStorage接口(src/lib/file-storage.ts);
  2. 利用 list() 的能力prefix做目录式筛选、includeMetadata拿完整元数据、cursor+limit做分页,文件系统后端默认每页 32 条;
  3. 升级时对照破坏性清单:v0.13.0(类→工厂)、v0.10.0(ESM-only)、v0.8.0(包改名)、v0.6.0(分片目录 2 字符 + list API)、v0.2.0(local/memory 子路径导出)——若从 0.x 早期版本直升,需要一次性处理这些迁移点,而 CHANGELOG.md 本身即是最完整的迁移手册。

【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 16:53:09

Linux printf 命令详解:格式化输出、对齐表格与常见坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 16:53:06

JAVA毕业设计-基于 Web 平台的实验室耗材全生命周期管理系统设计与实现 基于 Web 的实验室耗材管理系统(源码+LW+部署文档+全bao+远程调试+代码讲解等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/9/10 16:52:17

SpringBoot+Vue3构建大件物流系统的技术实践

1. 项目概述&#xff1a;大件物流快递系统的技术架构与业务场景 大件物流快递系统是区别于普通快递的特殊物流形态&#xff0c;主要服务于家电、家具、建材等超规格商品的运输配送。这类商品通常具有体积大&#xff08;单边长度超过1.2米&#xff09;、重量重&#xff08;超过3…

作者头像 李华
网站建设 2026/9/10 16:49:46

tdl下载器源码深度解析:揭秘Golang高效下载机制

tdl下载器源码深度解析&#xff1a;揭秘Golang高效下载机制 Telegram下载器tdl是一个用Golang编写的高效下载工具&#xff0c;专门用于从Telegram平台快速下载各类文件。作为GitHub加速计划的重要项目&#xff0c;tdl下载器凭借其优秀的并发处理和智能进度管理机制&#xff0c…

作者头像 李华
网站建设 2026/9/10 16:49:10

vue 在线预览 word ,Excel,pdf,图片 数据流 内网文件流 亲测有效(word 目前支持docx文件以及doc文件(doc需要后端处理))

注&#xff1a;doc转 docx后端转数据流 谷歌 114 版本以上会解析错误&#xff01; 如果是需要更好的体验&#xff1a;可以使用 kkFileView - 在线文件预览 需要后端在服务器部署一个服务 之后返回地址前端进行直接在线访问&#xff1b;&#xff08;支持内网哦&#xff09; …

作者头像 李华