ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Remix headers 包实战指南:用类型化 API 解析、操纵与序列化 HTTP 头部

Remix headers 包实战指南:用类型化 API 解析、操纵与序列化 HTTP 头部 Remix headers 包实战指南用类型化 API 解析、操纵与序列化 HTTP 头部【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix导读在基于 Web Fetch API 构建服务端应用时Headers原生对象只能提供get/set/append这类字符串级操作面对Content-Type、Cache-Control、Set-Cookie这些具有结构化语法媒体类型 参数、指令 秒数、属性 标志位的头部开发者往往要手写正则与字符串拼接。Remix 仓库中的headers包位于 packages/headers正是为解决这一痛点而生它提供聚焦于常见 HTTP 头部的类型化值类以及一个扩展原生Headers的SuperHeaders类。读完本文你将掌握如何用对象属性读写头部、用Map/Set语义操作Accept、Vary等头部以及如何直接复用这些类构建可解析、可回写的 HTTP 响应。包概览能力与设计目标headers包的核心定位是一组“用于解析、操纵和序列化 HTTP 头部值的类型化工具”。它围绕四条设计主线展开增强的Headers类SuperHeaders为常见头部提供懒加载、类型化的属性访问器同时因为它是原生Headers的真实子类可以无缝传给任何期待原生Headers的运行时 API。头部专属类为Accept、Cache-Control、Content-Type等头部提供专门设计的 API每种头部按自身语法建模Map、Set或对象属性。往返安全Round-Trip Safety既可以从原始字符串解析from()也可以序列化回字符串.toString()解析与输出天然对称。类型化操作开发者面向结构化值编程而不是面向手工字符串解析编程。包的入口与子路径导出在 packages/headers/package.json 中定义默认导出SuperHeaders同时为每个头部类提供独立子路径如remix/headers/content-type并导出原始头部工具parse/stringify见 src/index.ts。安装npm i remix安装后即可通过包名remix/headers使用本文所有示例均基于该导入路径。SuperHeaders原生 Headers 之上的类型化访问层SuperHeaders直接继承原生Headers类并追加了 60 余个属性访问器。它是本包的默认导出见 super-headers.ts 中的SuperHeaders as default导出。最直观的用法是直接以属性方式读写头部import Headers from remix/headers let headers new Headers(request.headers) headers.contentType { mediaType: text/html, charset: utf-8 } headers.cacheControl { public: true, maxAge: 3600 } headers.setCookie { name: session, value: abc, httpOnly: true } headers.contentType.charset iso-8859-1 headers.cacheControl.maxAge 60 headers.setCookie.push({ name: theme, value: dark, path: / }) return new Response(html, { headers })由于SuperHeaders是真实的Headers子类它可以被直接交给平台 APIlet headers new Headers({ contentType: text/plain }) headers instanceof globalThis.Headers // true new Response(Hello, { headers }).headers.get(Content-Type) // text/plain注意上面用到的{ contentType: text/plain }构造器接受一种“属性初始化器”形态SuperHeadersPropertyInit键名是驼峰属性名而非Content-Type这种原生头部名。懒解析读取时才解析类型化访问器采用读取时解析策略即访问属性时才触发解析普通get()调用不会产生任何解析开销let headers new Headers({ Content-Type: application/json; charsetutf-8 }) headers.get(Content-Type) // 无需类型解析 headers.contentType.mediaType // 此刻才懒解析 Content-Type从源码实现看这种惰性机制建立在缓存与修订号revision之上SuperHeaders内部维护#cache与#revisions两个私有 Map见 super-headers.ts每次append/delete/set都会调用#invalidate使对应缓存失效而重复读取同一头部时直接命中缓存从而保证“按需解析”的高效语义。apply()头部感知的合并语义当需要把一个SuperHeadersInit值应用到已有实例上时使用apply()。与简单覆盖不同它针对不同头部采用不同的合并策略见源码中#applyHeaderValue的实现let headers new Headers({ contentType: text/html, setCookie: { name: session, value: abc }, vary: Accept-Encoding, }) headers.apply({ contentType: application/json, setCookie: { name: theme, value: dark }, vary: [Accept-Encoding, Accept-Language], }) headers.get(Content-Type) // application/json headers.get(Vary) // accept-encoding, accept-language headers.getSetCookie() // [sessionabc, themedark]由源码可知apply()的合并规则是头部感知的Content-Type这类“单值头部”直接覆盖setSet-Cookie这类“多值头部”追加append因此上面两个 cookie 都被保留Cookie按 cookie 名追加合并、Vary按头部名去重合并见 super-headers.ts而Accept、Accept-Encoding、Accept-Language、Cache-Control、If-Match、If-None-Match等头部同样采用追加语义ApplyAppendHeaderNames集合定义于 super-headers.ts。头部值类统一的 from()/toString() 契约除SuperHeaders外包为每个受支持的头部提供一个独立值类。所有类遵循同一契约静态方法from(value)解析字符串、初始化对象或null为值类实例实例方法toString()序列化回字符串当值类被用于任何期待字符串的上下文如headers.set(X, instance)时会被自动调用。基础契约定义在 header-value.ts 中就是一个仅有toString()的接口。按需导入子路径如果只需要某个头部解析器例如只用Content-Type可以直接从子路径导入避免拉入整包 barrel 与SuperHeadersimport { ContentType } from remix/headers/content-type import { SetCookie } from remix/headers/set-cookie let contentType ContentType.from(text/plain; charsetutf-8) let setCookie new SetCookie(sessionabc; Path/)完整的子路径清单与package.json中exports字段一一对应见 packages/headers/package.json包括./accept、./cache-control、./cookie、./if-range、./range、./raw-headers、./vary等。当前支持的头部共 14 种下文逐一展开。AcceptMapmediaType, qualityAccept实现为MapmediaType, quality支持通配符text/*匹配用于服务端内容协商。import { Accept } from remix/headers // 从头部解析 let accept Accept.from(request.headers.get(Accept)) accept.mediaTypes // [text/html, text/*] accept.weights // [1, 0.9] accept.accepts(text/html) // true accept.accepts(text/plain) // true命中 text/* accept.accepts(image/jpeg) // false accept.getWeight(text/plain) // 1命中 text/* accept.getPreferred([text/html, text/plain]) // text/html // 迭代按权重降序 for (let [mediaType, quality] of accept) { // ... } // 修改并写回头部 accept.set(application/json, 0.8) accept.delete(text/*) headers.set(Accept, accept) // 直接构造 new Accept(text/html, text/*;q0.9) new Accept({ text/html: 1, text/*: 0.9 }) new Accept([text/html, [text/*, 0.9]]) // 利用 toString() 做类型安全的头部写入 let headers new Headers({ Accept: new Accept({ text/html: 1, application/json: 0.8 }), }) headers.set(Accept, new Accept({ text/html: 1, application/json: 0.8 }))源码层面accept.ts值得注意的细节getWeight()对类型与子类型分别做通配符匹配t type || t * || type *set()之后会自动按权重降序重排#sort()toString()只在权重不等于 1 时才输出;q后缀。Accept-EncodingMapencoding, qualityAccept-Encoding实现为Mapencoding, quality用于协商响应压缩格式。import { AcceptEncoding } from remix/headers let acceptEncoding AcceptEncoding.from(request.headers.get(Accept-Encoding)) acceptEncoding.encodings // [gzip, deflate] acceptEncoding.weights // [1, 0.8] acceptEncoding.accepts(gzip) // true acceptEncoding.accepts(br) // false acceptEncoding.getWeight(gzip) // 1 acceptEncoding.getPreferred([gzip, deflate, br]) // gzip acceptEncoding.set(br, 1) acceptEncoding.delete(deflate) headers.set(Accept-Encoding, acceptEncoding) new AcceptEncoding(gzip, deflate;q0.8) new AcceptEncoding({ gzip: 1, deflate: 0.8 }) let headers new Headers({ Accept-Encoding: new AcceptEncoding({ gzip: 1, br: 0.9 }), }) headers.set(Accept-Encoding, new AcceptEncoding({ gzip: 1, br: 0.9 }))Accept-LanguageMaplanguage, qualityAccept-Language实现为Maplanguage, quality通配匹配规则与Accept类似en可命中en-US、en-GB用于多语言站点内容协商。import { AcceptLanguage } from remix/headers let acceptLanguage AcceptLanguage.from(request.headers.get(Accept-Language)) acceptLanguage.languages // [en-us, en] acceptLanguage.weights // [1, 0.9] acceptLanguage.accepts(en-US) // true acceptLanguage.accepts(en-GB) // true命中 en acceptLanguage.getWeight(en-GB) // 1命中 en acceptLanguage.getPreferred([en-US, en-GB, fr]) // en-US acceptLanguage.set(fr, 0.5) acceptLanguage.delete(en) headers.set(Accept-Language, acceptLanguage) new AcceptLanguage(en-US, en;q0.9) new AcceptLanguage({ en-US: 1, en: 0.9 }) let headers new Headers({ Accept-Language: new AcceptLanguage({ en-US: 1, fr: 0.5 }), }) headers.set(Accept-Language, new AcceptLanguage({ en-US: 1, fr: 0.5 }))Cache-Control指令级缓存策略Cache-Control把每个缓存指令映射为属性布尔型指令public、no-cache等对应boolean时长型指令max-age、s-maxage等对应number。未出现的指令读取为undefined。import { CacheControl } from remix/headers let cacheControl CacheControl.from(response.headers.get(Cache-Control)) cacheControl.public // true cacheControl.maxAge // 3600 cacheControl.sMaxage // 7200 cacheControl.noCache // undefined cacheControl.noStore // undefined cacheControl.noTransform // undefined cacheControl.mustRevalidate // undefined cacheControl.immutable // undefined cacheControl.maxAge 7200 cacheControl.immutable true headers.set(Cache-Control, cacheControl) new CacheControl(public, max-age3600) new CacheControl({ public: true, maxAge: 3600 }) let headers new Headers({ Cache-Control: new CacheControl({ public: true, maxAge: 3600 }), }) headers.set(Cache-Control, new CacheControl({ public: true, maxAge: 3600 }))从 cache-control.ts 的CacheControlInit接口可以看出支持的完整指令集请求指令maxStale、minFresh、onlyIfCached响应指令private、mustRevalidate、proxyRevalidate、mustUnderstand以及时长型指令staleWhileRevalidate、staleIfError。toString()按固定顺序输出指令public→private→max-age→s-maxage→ …… →min-fresh保证序列化结果稳定。Content-Disposition文件下载头Content-Disposition用于下载响应特别处理了 RFC 5987 的filename*扩展支持非 ASCII 文件名。import { ContentDisposition } from remix/headers let contentDisposition ContentDisposition.from(response.headers.get(Content-Disposition)) contentDisposition.type // attachment contentDisposition.filename // example.pdf contentDisposition.filenameSplat // UTF-8%E4%BE%8B%E5%AD%90.pdf contentDisposition.preferredFilename // 例子.pdf从 filename* 解码得到 contentDisposition.filename download.pdf headers.set(Content-Disposition, contentDisposition) new ContentDisposition(attachment; filenameexample.pdf) new ContentDisposition({ type: attachment, filename: example.pdf }) let headers new Headers({ Content-Disposition: new ContentDisposition({ type: attachment, filename: example.pdf }), }) headers.set( Content-Disposition, new ContentDisposition({ type: attachment, filename: example.pdf }), )Content-Range分块响应区间Content-Range描述响应体在整个资源中的位置。对于不可满足的区间bytes */67589start/end为null。import { ContentRange } from remix/headers let contentRange ContentRange.from(response.headers.get(Content-Range)) contentRange.unit // bytes contentRange.start // 200 contentRange.end // 1000 contentRange.size // 67589 // 不可满足的区间 let unsatisfied ContentRange.from(bytes */67589) unsatisfied.start // null unsatisfied.end // null unsatisfied.size // 67589 new ContentRange({ unit: bytes, start: 0, end: 499, size: 1000 }) let headers new Headers({ Content-Range: new ContentRange({ unit: bytes, start: 0, end: 499, size: 1000 }), }) headers.set(Content-Range, new ContentRange({ unit: bytes, start: 0, end: 499, size: 1000 }))Content-Type媒体类型与参数Content-Type是最常用的头部之一拆分为mediaType、charset、boundary三个字段boundary仅 multipart 场景出现。解析实现见 content-type.ts字符串经parseParams拆出媒体类型与charset/boundary参数toString()会使用quote()对参数值加引号。import { ContentType } from remix/headers let contentType ContentType.from(request.headers.get(Content-Type)) contentType.mediaType // text/html contentType.charset // utf-8 contentType.boundary // undefinedmultipart 时为 boundary 字符串 contentType.charset iso-8859-1 headers.set(Content-Type, contentType) new ContentType(text/html; charsetutf-8) new ContentType({ mediaType: text/html, charset: utf-8 }) let headers new Headers({ Content-Type: new ContentType({ mediaType: text/html, charset: utf-8 }), }) headers.set(Content-Type, new ContentType({ mediaType: text/html, charset: utf-8 }))Cookie有序的 name/value 对列表Cookie实现为有序的 name/value 对列表保留重复同名 cookie例如同名 cookie 被设置在多个 path 下时。import { Cookie } from remix/headers let cookie Cookie.from(request.headers.get(Cookie)) cookie.get(session_id) // abc123 cookie.getAll(session_id) // [abc123] cookie.get(theme) // dark cookie.has(session_id) // true cookie.size // 2 for (let [name, value] of cookie) { // ... } cookie.set(theme, light) cookie.append(session_id, def456) cookie.delete(session_id) headers.set(Cookie, cookie) new Cookie(session_idabc123; themedark) new Cookie({ session_id: abc123, theme: dark }) new Cookie([ [session_id, abc123], [theme, dark], ]) let headers new Headers({ Cookie: new Cookie({ session_id: abc123, theme: dark }), }) headers.set(Cookie, new Cookie({ session_id: abc123, theme: dark }))If-Match强比较的 ETag 前置条件If-Match实现为Setetag用于乐观并发控制条件写入。只做强比较弱 ETagW/...永远不匹配import { IfMatch } from remix/headers let ifMatch IfMatch.from(request.headers.get(If-Match)) ifMatch.tags // [67ab43, 54ed21] ifMatch.has(67ab43) // true ifMatch.matches(67ab43) // true检查前置条件 ifMatch.matches(abc123) // false // 注意仅使用强比较弱 ETag 永不匹配 let weak IfMatch.from(W/67ab43) weak.matches(W/67ab43) // false ifMatch.add(newetag) ifMatch.delete(67ab43) headers.set(If-Match, ifMatch) new IfMatch([abc123, def456]) let headers new Headers({ If-Match: new IfMatch([abc123, def456]), }) headers.set(If-Match, new IfMatch([abc123, def456]))If-None-Match弱比较的缓存验证If-None-Match同样实现为Setetag用于条件 GET配合304 Not Modified。与If-Match相反它支持弱比较import { IfNoneMatch } from remix/headers let ifNoneMatch IfNoneMatch.from(request.headers.get(If-None-Match)) ifNoneMatch.tags // [67ab43, 54ed21] ifNoneMatch.has(67ab43) // true ifNoneMatch.matches(67ab43) // true // 支持弱比较与 If-Match 不同 let weak IfNoneMatch.from(W/67ab43) weak.matches(W/67ab43) // true ifNoneMatch.add(newetag) ifNoneMatch.delete(67ab43) headers.set(If-None-Match, ifNoneMatch) new IfNoneMatch([abc123]) let headers new Headers({ If-None-Match: new IfNoneMatch([abc123]), }) headers.set(If-None-Match, new IfNoneMatch([abc123]))If-Range区间请求的条件校验If-Range同时支持 HTTP 日期与 ETag 两种校验形式若头部为空或null返回空实例此时区间请求无条件继续import { IfRange } from remix/headers let ifRange IfRange.from(request.headers.get(If-Range)) // 使用 HTTP 日期 ifRange.matches({ lastModified: 1609459200000 }) // true ifRange.matches({ lastModified: new Date(2021-01-01) }) // true // 使用 ETag let etagHeader IfRange.from(67ab43) etagHeader.matches({ etag: 67ab43 }) // true // 空/null 返回空实例区间请求无条件继续 let empty IfRange.from(null) empty.matches({ etag: any }) // true new IfRange(abc123) let headers new Headers({ If-Range: new IfRange(abc123), }) headers.set(If-Range, new IfRange(abc123))Range字节区间解析Range支持多区间与后缀区间最后 N 字节。normalize(size)根据资源总大小把区间规范化后缀区间会换算成实际起止位置import { Range } from remix/headers let range Range.from(request.headers.get(Range)) range.unit // bytes range.ranges // [{ start: 200, end: 1000 }] range.canSatisfy(2000) // true range.canSatisfy(500) // false range.normalize(2000) // [{ start: 200, end: 1000 }] // 多区间 let multi Range.from(bytes0-499, 1000-1499) multi.ranges.length // 2 // 后缀区间最后 N 字节 let suffix Range.from(bytes-500) suffix.normalize(2000) // [{ start: 1500, end: 1999 }] new Range({ unit: bytes, ranges: [{ start: 0, end: 999 }] }) let headers new Headers({ Range: new Range({ unit: bytes, ranges: [{ start: 0, end: 999 }] }), }) headers.set(Range, new Range({ unit: bytes, ranges: [{ start: 0, end: 999 }] }))Set-Cookie完整的 Cookie 属性建模Set-Cookie把 cookie 的每个属性建模为字段覆盖Domain、Expires、HttpOnly、Max-Age、Partitioned、Path、SameSite、Secure。其解析/序列化实现在 set-cookie.ts 中toString()会先输出namevalue再按固定顺序输出属性Expires使用toUTCString()格式化。import { SetCookie } from remix/headers let setCookie SetCookie.from(response.headers.get(Set-Cookie)) setCookie.name // session_id setCookie.value // abc setCookie.path // / setCookie.httpOnly // true setCookie.secure // true setCookie.domain // undefined setCookie.maxAge // undefined setCookie.expires // undefined setCookie.sameSite // undefined setCookie.maxAge 3600 setCookie.sameSite Strict headers.set(Set-Cookie, setCookie) new SetCookie(session_idabc; Path/; HttpOnly; Secure) new SetCookie({ name: session_id, value: abc, path: /, httpOnly: true, secure: true, }) let headers new Headers({ Set-Cookie: new SetCookie({ name: session_id, value: abc, httpOnly: true }), }) headers.set(Set-Cookie, new SetCookie({ name: session_id, value: abc, httpOnly: true }))VarySetheaderName与大小写归一Vary实现为SetheaderName所有头部名统一归一为小写因此has()比较是大小写不敏感的。它是实现正确内容协商缓存的关键头部。import { Vary } from remix/headers let vary Vary.from(response.headers.get(Vary)) vary.headerNames // [accept-encoding, accept-language] vary.has(Accept-Encoding) // true大小写不敏感 vary.size // 2 vary.add(User-Agent) vary.delete(Accept-Language) headers.set(Vary, vary) new Vary(Accept-Encoding, Accept-Language) new Vary([Accept-Encoding, Accept-Language]) new Vary({ headerNames: [Accept-Encoding, Accept-Language] }) let headers new Headers({ Vary: new Vary([Accept-Encoding, Accept-Language]), }) headers.set(Vary, new Vary([Accept-Encoding, Accept-Language]))原始头部工具parse 与 stringify除结构化类外包还提供面向“原始头部字符串”的parse/stringify工具。实现在 raw-headers.ts 中parse按 CRLF 分行、以name: value正则拆分stringify用canonicalHeaderName规范化头部名后以 CRLF 重新拼接。import { parse, stringify } from remix/headers let headers parse(Content-Type: text/html\r\nCache-Control: no-cache) headers.get(Content-Type) // text/html headers.get(Cache-Control) // no-cache stringify(headers) // Content-Type: text/html\r\nCache-Control: no-cache源码实现要点与工程细节从源码结构看src/lib包内每个头部类都配有同名的*.test.ts测试文件如 accept.test.ts、cache-control.test.ts、super-headers.test.ts覆盖解析、修改、序列化的往返一致性。几个值得关注的实现细节共享的参数解析器param-values.ts提供parseParams/quote被ContentType、SetCookie、CacheControl、Accept等共同复用保证解析行为一致。日期与 ETag 工具utils.ts 中的parseHttpDate严格校验 RFC 7231 IMF-fixdate 格式Wed, 21 Oct 2015 07:28:00 GMTremoveMilliseconds用于去除毫秒做秒级比较quoteEtag用于自动为裸 ETag 加引号。属性访问器的自动生成SuperHeaders的全部属性访问器并非手写 getter/setter而是在静态初始化块中遍历HeaderDescriptors列表通过Object.defineProperty统一注册见 super-headers.ts新增头部只需在描述符表中登记。变更观察mutation observationobserveMutations用Proxy包装值类实例拦截set/deleteProperty以及数组的push/splice等变异方法从而在headers.setCookie.push(...)这类操作后自动把变更同步回真实头部见 super-headers.ts。典型应用场景响应头构造把SuperHeaders直接传给new Response(html, { headers })用对象字面量一次性配置Content-Type、Cache-Control、Set-Cookie既避免手写字符串拼接又获得完整的类型提示。条件请求与断点续传结合If-Match/If-None-Match/If-Range/Range实现静态资源服务中的304、206 Partial Content与乐观并发控制。内容协商用Accept、Accept-Encoding、Accept-Language的getPreferred()在服务端按客户端偏好选择响应格式、压缩算法与语言。代理与服务器开发包内相关生态包fetch-proxy构建 Web Fetch API 的 HTTP 代理与node-fetch-server在 Node.js 上用 Web Fetch API 构建 HTTP 服务器都会频繁与头部打交道本包的类型化工具可与它们无缝配合。总结headers包把 HTTP 头部从“字符串黑盒”升级为“类型化结构体”SuperHeaders在保持原生Headers兼容性的同时提供懒解析的属性访问14 个头部值类各自按Map/Set/对象字段建模from()/toString()保证解析与序列化往返一致。无论是编写响应头、解析请求头还是构建条件请求逻辑这套 API 都能显著减少字符串解析样板代码让头部操作的意图更清晰、更不易出错。【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表