资讯详情

Remix 3 文件下载怎么用 createFileResponse 支持断点与条件请求

📅 2026/9/12 2:38:16 | 华诺云谱 👁 阅读
Remix 3 文件下载怎么用 createFileResponse 支持断点与条件请求
Remix 3 文件下载怎么用 createFileResponse 支持断点与条件请求【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix当你在 Remix 3 应用里需要把磁盘上的文件返回给浏览器下载、媒体资源、构建产物手写Content-Length、ETag、304和206很容易漏掉某个条件分支。createFileResponse()是remix/response提供的文件响应助手传入一个File或LazyFile对象和当前request它会替你生成Content-Type、Content-Length、ETag、Last-Modified、Cache-Control头并处理If-None-Match、If-Modified-Since、If-Match、If-Unmodified-Since条件请求以及Range/If-Range断点请求206 Partial Content和 HEAD。本文基于 packages/response 的 README、实现源码、仓库测试用例 和 Remix 3 文档的 Files and Assets 章节 说明怎么接入和验证。准备条件在应用里安装remix包即可各包 README 给出的安装命令均为npm i remixnpm i remixcreateFileResponse接受两类文件对象原生File含Blob系列 APILazyFile由remix/fs的openLazyFile()从文件系统打开。官方下载章节明确建议用LazyFile流式读取避免把大文件缓冲进原生File对象见 11-files-and-assets.md 的 Stream downloads with correct HTTP semantics 一节。第一个文件响应最基础的用法来自 packages/response/README.mdimport { createFileResponse } from remix/response/file import { openLazyFile } from remix/fs let lazyFile openLazyFile(./public/image.jpg) let response await createFileResponse(lazyFile, request, { cacheControl: public, max-age3600, })request必须是你当前要响应的请求对象因为 ETag/Range 的条件判断全部基于请求头。文档站点自己的 API 示例 file-response.ts 把这段模式封装成了一个可复用的fileResponse(request, filePath)函数name参数默认取path.basename(filePath)传给openLazyFile。五个选项控制 ETag、Last-Modified 与 RangecreateFileResponse(file, request, options)的选项及默认值以 README 的 Options 说明为准选项默认值作用cacheControlundefined不设 Cache-Control 头设置Cache-Control头的值如public, max-age3600、no-cacheetagweakweak基于文件大小和 mtime 生成弱 ETagW/size-mtimestrong对文件内容做哈希生成强 ETagfalse关闭 ETagdigestSHA-256仅在etag: strong时生效可用 Web Crypto 算法名如SHA-512或自定义 digest 函数lastModifiedtrue是否生成Last-Modified头acceptRanges未显式指定时仅对不可压缩的 MIME 类型启用是否支持Range请求206并返回Accept-Ranges: bytes头acceptRanges的默认行为值得单独理解Range 请求和压缩互斥——当响应带Accept-Ranges: bytes时compressResponse中间件不会压缩该响应。所以默认只对不可压缩类型如video/mp4开 Range让文本类资源仍可被压缩README Range Requests and Compression 一节。仓库测试印证了这一点video/mp4文件默认带Accept-Ranges: bytestext/plain文件默认不带见 file.test.ts Range requests 组。如果你给文本文件也要断点续传显式传acceptRanges: true如果不想支持断点传false。另外文档的下载章节还建议下载场景补充Content-Disposition头、用remix/mime处理内容类型createFileResponse的上述五个选项不直接包含Content-Disposition的配置项。条件请求304 与 412 分别什么时候出现createFileResponse对四个条件头的处理结果均可在 file.test.ts 中找到对应断言If-None-Match与响应的 ETag 匹配含多 ETag 列表中命中、以及*时返回304body 为空不匹配时正常返回 200。etag: false时该头被忽略始终 200。If-Modified-Since不早于文件Last-Modified时返回304早于则 200。日期比较先截断到秒HTTP 日期只到秒。请求同时带If-None-Match时If-None-Match优先ETag 不匹配就返回 200即使If-Modified-Since本应命中 304。If-Match不匹配含*之外的多 ETag 全部未命中时返回412 Precondition Failed*或强 ETag 命中时正常 200。注意弱 ETag 用强比较永远不会命中If-Match——默认weakETag 下带If-Match的请求会直接 412。如果你的接口依赖If-Match前置条件例如防止并发覆盖需要etag: strong。If-Unmodified-Since早于文件Last-Modified时返回412等于或晚于则 200无法解析的日期值被忽略按 200 处理。若请求同时带If-MatchIf-Unmodified-Since被忽略。处理顺序上412 类前置条件检查先于 Range 处理If-Match失败时即使带了Range也返回 412 且无Content-Range头。Range 断点请求206 的行为与边界acceptRanges开启默认对不可压缩类型开启后GET 请求带Range头时的行为以下示例值均取自 file.test.ts用 10 字节的0123456789文件构造Range: bytes0-4→206body 为01234Content-Range: bytes 0-4/10Content-Length: 5bytes5-只给起点→ 206Content-Range: bytes 5-9/10bytes-3后缀段→ 206bytes 7-9/10终点超出文件大小bytes0-999会被钳制到实际大小仍返回 206 全量无法满足的段bytes20-30→416Content-Range: bytes */10多段请求bytes0-2,5-7不支持 →416语法错误bytes5-2、bytes0-2,garbage、bytes、invalid→400 Bad Request非 GET/HEAD 请求如 POST上的Range头被忽略返回 200 全量HEAD 请求只返回头body 为空。If-Range当前按Last-Range日期比较实现与Last-Modified日期匹配时按 Range 返回 206不匹配、或值是无法解析的日期、或弱 ETag 值时都回退为 200 全量文件测试名 ignores If-Range with weak ETag value (only Last-Modified date supported) 明确了这一点。README 也给出一个组合动机要同时支持If-Range配Range的强校验语义需要配置etag: strong。验证按仓库测试的请求序列跑一遍下面这段脚本复刻了 file.test.ts 中三组断言的请求构造方式可以直接对着你的下载路由做冒烟验证。sample.mp4换成你服务器上实际存在的文件acceptRanges: true是显式开启文本类 MIME 默认不开若文件本身是不可压缩类型可省略import { openLazyFile } from remix/fs import { createFileResponse } from remix/response/file let file openLazyFile(./public/sample.mp4) // 1) 首次请求期望 200响应头带 ETag / Last-Modified let first await createFileResponse(file, new Request(http://localhost/sample.mp4), { acceptRanges: true, }) console.log(first.status) // 200 console.log(first.headers.get(ETag)) console.log(first.headers.get(Last-Modified)) console.log(first.headers.get(Accept-Ranges)) // bytes // 2) 条件请求带上次的 ETag期望 304 且 body 为空 let notModified await createFileResponse(file, new Request(http://localhost/sample.mp4, { headers: { If-None-Match: first.headers.get(ETag) ?? }, }), { acceptRanges: true }) console.log(notModified.status) // 304 console.log(await notModified.text()) // // 3) 断点请求期望 206 与 Content-Range测试用例中 10 字节文件返回 bytes 0-4/10 let ranged await createFileResponse(file, new Request(http://localhost/sample.mp4, { headers: { Range: bytes0-4 }, }), { acceptRanges: true }) console.log(ranged.status) // 206 console.log(ranged.headers.get(Content-Range))判断标准就三条重校验命中时得到 304说明浏览器/客户端可用缓存、Range 请求得到 206 且Content-Range段与实际字节数一致、弱 ETag 下带If-Match的请求得到 412符合弱 ETag 不满足强比较的预期。如果 304 没出现先检查两次请求是否对同一个File对象、etag是否被设成了false。接入 Remix 应用路由的两个现成模式一是文档站自己的写法在 controller action 里先做路径校验再openLazyFile交给createFileResponse。docs/guides/app/actions/controller.tsx 的servePagefindAsset展示了完整防护——用path.resolvepath.relative判断是否越出目录防路径穿越fs.stat确认是文件后才响应ENOENT/ENOTDIR时返回 204 交给后续处理let assetPath path.resolve(pagefindAssetsDir, requestPath.slice(pagefind/.length)) let relativeAssetPath path.relative(pagefindAssetsDir, assetPath) if (relativeAssetPath.startsWith(..) || path.isAbsolute(relativeAssetPath)) { return new Response(null, { status: 204 }) } try { let stats await fs.stat(assetPath) if (stats.isFile()) { return await createFileResponse(openLazyFile(assetPath), request) } } catch (error) { if (!isNotFoundError(error)) { throw error } }二是整目录静态文件static-middleware 的staticFiles(./public)内部就是调用createFileResponse发文件因此接受同一套选项cacheControl、etag、acceptRanges等并自带路径穿越防护、仅响应 GET/HEAD、文件不存在时透传给下一个中间件import { createRouter } from remix/router import { staticFiles } from remix/middleware/static let router createRouter({ middleware: [ staticFiles(./public, { cacheControl: public, max-age31536000, immutable, }), ], })限制与注意事项强 ETag 的内存代价默认SHA-256强 ETag 会先把整个文件读进内存再哈希大文件建议保留默认弱 ETag或按 README 的示例传自定义digest函数做流式哈希逐块hash.update(chunk)后再digest(hex)。不支持的算法如MD5会抛NotSupportedError。Range 与压缩互斥带Accept-Ranges: bytes的响应不会被compressResponse压缩这也是 Range 只对不可压缩类型默认开启的原因。只支持单段 Range多段请求返回 416而不是 multipart 响应。If-Range目前只认Last-Modified日期传弱 ETag 或非法值都会回退 200 全量要基于 ETag 的If-Range校验需etag: strong。运行时差异测试文件注释说明 Bun 下Blob/File的slice().stream()仍返回完整 body上游问题仓库因此跳过了 Bun 环境下的 ranged body 断言在 Bun 上验证 206 时若发现 body 是全量文件先对照这条已知限制。HEAD 只回头HEAD 请求返回 200 且 body 为空Content-Range不出现这是验证时用curl -I类工具看不到分段数据的原因。参考文档createFileResponse 选项说明、实现与默认行为、LazyFile / openLazyFile、文档站 API 封装示例、下载章节Content-Disposition、MIME、避免大文件缓冲。【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。