Effect v4 的 Encoding 模块整合:effect/encoding 子模块合并进顶层 Encoding API
【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect
本篇围绕 Effect 仓库中一条 Changeset 变更记录(.changeset/pre/consolidate-encoding.md)展开,说明 v4 中effect/encoding子模块(Base64、Base64Url、Hex、EncodingError)被合并进顶层Encoding模块、函数统一加前缀、子路径导出被移除这一 API 重组的来龙去脉。读完你将掌握:新版Encoding的完整函数清单与签名、EncodingError错误模型、Base64/Hex 解码的校验规则,以及如何从旧的effect/encoding子路径导入迁移到顶层 API。
一、变更内容:一条 Changeset 说了什么
变更声明位于 consolidate-encoding.md,全文仅一段:
Encoding: consolidate
effect/encodingsub-modules (Base64, Base64Url, Hex, EncodingError) into a top-levelEncodingmodule. Functions are now prefixed:encodeBase64,decodeBase64,encodeHex,decodeHex, etc. Theeffect/encodingsub-path export is removed.
按 changesets 规范解读这条记录:
- 文件头 YAML frontmatter 为
"effect": patch,即本次变更作用于effect包本身,属于补丁级别的 API 结构调整(对使用者而言是导入路径的破坏性变更,但对包发布流程按 pre 模式管理——该文件位于.changeset/pre/目录,对应 v4 预发布阶段); - 仓库当前 effect 包 版本号为
4.0.0-rc.115,Encoding.ts模块头注释标注@since 4.0.0,二者互相印证这是 v4 预发布周期内的整合工作。
变更包含三件事,后文逐一验证:
Base64、Base64Url、Hex、EncodingError四个子模块的公共能力合并进顶层Encoding模块;- 函数统一加编码类型前缀:
encodeBase64、decodeBase64、encodeHex、decodeHex等,取代子模块内裸名encode/decode; effect/encoding子路径导出被移除。
二、子路径导出的移除:从 package.json 验证
检查 packages/effect/package.json 的exports字段可以确认第 3 点:当前版本中已不存在./encoding这一子路径条目,与编码相关的导出仅剩./unstable/encoding(后者指向src/unstable/encoding/index.ts,包含 Toml、Yaml、Sse、Ndjson 等不稳定文本编解码,与本条 Changeset 合并的 Base64/Hex 是不同层面的功能,不要混淆)。
顶层Encoding模块的出口在 packages/effect/src/index.ts 第 167 行:
export * as Encoding from "./Encoding.ts"即使用者只需从主入口effect导入命名空间:
import { Encoding } from "effect"而不再需要(也不能再)写import { Base64 } from "effect/encoding"之类的子路径导入。
三、整合后的完整 API 面
整合后的 Encoding.ts 模块头注释给出了整体契约:模块在字符串、UTF-8 文本与Uint8Array字节之间转换;encode 系列直接返回字符串,decode 系列返回Result.Result,非法输入以EncodingError形式报告而不是抛异常。函数按 Changeset 所述统一前缀,当前完整清单如下。
3.1 Base64(RFC4648 标准字母表,带=填充)
| 函数 | 签名 | 说明 |
|---|---|---|
encodeBase64 | (input: Uint8Array \| string) => string | 字符串输入先按 UTF-8 转字节再编码;Uint8Array直接编码,输出为标准字母表加=填充 |
decodeBase64 | (str: string) => Result.Result<Uint8Array, EncodingError> | 解码为字节,失败返回Result.fail |
decodeBase64String | (str: string) => Result.Result<string, EncodingError> | 解码为 UTF-8 文本 |
用法示例(摘自 Encoding.ts 的 JSDoc 内嵌测试):
import { Encoding, Result } from "effect" // 编码字符串 Encoding.encodeBase64("hello") // => "aGVsbG8=" // 编码二进制数据 const bytes = new Uint8Array([72, 101, 108, 108, 111]) Encoding.encodeBase64(bytes) // => "SGVsbG8=" // 解码字节 / 文本 Encoding.decodeBase64("SGVsbG8=") // => Result.succeed(new Uint8Array([72, 101, 108, 108, 111])) Encoding.decodeBase64String("aGVsbG8=") // => Result.succeed("hello")decodeBase64的校验逻辑(Encoding.ts)值得注意:
- 先
stripCrlf去除输入中的\n/\r——这是为了容忍 Base64 常被折行存储的实际情况; - 长度必须是 4 的倍数,否则失败,消息形如
Length must be a multiple of 4, but is N; =只允许出现在末尾,且必须成规则出现(倒数第二位的=后必须紧跟另一个=),否则报Found a '=' character, but it is not at the end;- 逐 4 字符组按 6 位查表拼装 3 字节,非法字符由内部
getBase64Code抛TypeError(Invalid character ...),被外层try/catch捕获后统一转成Result.fail,保证函数全程无 throw。
3.2 Base64Url(URL 安全字母表,去填充)
| 函数 | 签名 | 说明 |
|---|---|---|
encodeBase64Url | (input: Uint8Array \| string) => string | 先做标准 Base64,再删除=并把+→-、/→_ |
decodeBase64Url | (str: string) => Result.Result<Uint8Array, EncodingError> | 同时接受填充与未填充两种形式 |
decodeBase64UrlString | (str: string) => Result.Result<string, EncodingError> | 解码为 UTF-8 文本 |
Encoding.encodeBase64Url("hello?") // => "aGVsbG8_" Encoding.decodeBase64Url("SGVsbG8_") // => Result.succeed(new Uint8Array([72, 101, 108, 108, 111, 63])) Encoding.decodeBase64UrlString("aGVsbG8_") // => Result.succeed("hello?")从实现看(Encoding.ts),decodeBase64Url是一条「归一化后复用标准解码」的调用链:先校验长度模 4 不为 1、再校验字符集正则/^[-_A-Z0-9]*?={0,2}$/i,然后按缺失的填充补回=,把-/_还原为+//,最后直接委托给decodeBase64。这解释了为什么 URL 变体的错误消息里module字段写作"Base64"的情况不存在——URL 分支的自有校验错误带module: "Base64Url",而还原后的底层解码错误继承标准 Base64 的模块名。
3.3 Hex(十六进制,小写输出)
| 函数 | 签名 | 说明 |
|---|---|---|
encodeHex | (input: Uint8Array \| string) => string | 输出小写十六进制文本 |
randomHex | (length: number) => string | 生成随机小写 hex(基于Math.random(),非密码学安全) |
decodeHex | (str: string) => Result.Result<Uint8Array, EncodingError> | 解码字节,要求偶数长度 |
decodeHexString | (str: string) => Result.Result<string, EncodingError> | 解码为 UTF-8 文本 |
Encoding.encodeHex("hello") // => "68656c6c6f" Encoding.decodeHex("48656c6c6f") // => Result.succeed(new Uint8Array([72, 101, 108, 108, 111])) Encoding.decodeHexString("68656c6c6f") // => Result.succeed("hello")两个使用注意点直接来自源码注释:
decodeHex先把输入经TextEncoder转成字节再校验偶数长度,因此非 ASCII 输入会被当作字节序列计长,失败消息形如Length must be a multiple of 2, but is N;字符映射同时接受0-9、a-f、A-F(见内部函数fromHexChar,Encoding.ts),即解码对大小写不敏感,而编码一律小写。randomHex的 JSDoc 明确警告:该函数使用Math.random(),不用于安全敏感场景;需要安全随机值时应使用Crypto服务的randomBytes再经encodeHex编码。实现上它对 16/32 位长度(trace/span 标识符的常见长度)有专门快路径,用单次String.fromCharCode生成扁平字符串以避免 rope 展开销。
3.4 统一错误类型 EncodingError
整合后的错误面也按 Changeset 所述并入同一模块(Encoding.ts):
export class EncodingError extends Data.TaggedError("EncodingError")<{ kind: "Decode" | "Encode" module: string input: unknown message: string }>kind区分失败发生在解码还是编码阶段;module记录报告失败的编码模块("Base64"/"Base64Url"/"Hex");input保留触发失败的原始输入,便于日志定位;- 类型守卫为
isEncodingError(u): u is EncodingError,基于EncodingErrorTypeId(值"~effect/Encoding/EncodingError")运行时标记判断,可安全地对unknown收窄。
典型处理写法:
import { Encoding, Result } from "effect" const bytes = Result.flatMap( Encoding.decodeBase64(urlParam), (b) => /* ... */ b ) // 或显式检查 Result.match( Encoding.decodeBase64UrlString(token), { onFailure: (e) => e.kind, onSuccess: (s) => s } )由于错误是结构化Data.TaggedError而非裸Error,它可以参与Effect.catch的类型收窄与匹配,这与 Effect 全库「解码类 API 一律返回Result/失败态而非 throw」的约定一致。
四、对照 v3:迁移指南中的对应关系
仓库自带的大型迁移文档 migration/v3-to-v4.md 中Encoding一节(约 L10245–L10259)补充了旧 API 的去向,与本条 Changeset 的整合方向一致:
Encoding.DecodeException/Encoding.EncodeException→Encoding.EncodingError:解码与编码两类异常统一为同一个错误类,靠kind(Decode/Encode)区分;- 两个
*TypeId标记 → 共享一个Encoding.EncodingErrorTypeId; Encoding.isDecodeException/Encoding.isEncodeException→Encoding.isEncodingError,需要按阶段收窄时再测试kind === "Decode"或kind === "Encode";- 旧的
Encoding.encodeUriComponent/Encoding.decodeUriComponent没有随整合进入新模块:迁移指南建议用Result.try包裹encodeURIComponent/decodeURIComponent,或直接使用Schema.StringFromUriComponent编解码。
从源码结构看,当前Encoding模块确实只含 Base64、Base64Url、Hex 三组编解码与统一错误类型,不含 URI 组件函数——这与迁移文档的描述相符。
五、底层实现速览(源码证据)
整合后所有能力集中在单文件 packages/effect/src/Encoding.ts 中,关键内部件:
- 模块级共享
TextEncoder/TextDecoder实例(L616–L617),encode*对字符串输入统一先encoder.encode(input); - 标准 Base64 手写查表实现:编码用 64 字母表
base64abc(L665–L730),解码用 96 项逆向表base64codes(非法字符位置为 255,L732–L856),3 字节 → 4 字符组按位拼装(<< 18 | << 12 | << 6 |); - Base64Url 编码只是「标准编码 + 三次 replace」(L860–L861):去
=、+→-、/→_; - Hex 编码用预生成的 256 项
byteToHex表拼接(L865–L873),解码逐对字符经fromHexChar转数值后(a << 4) | b合并。
这些实现细节说明整合后的模块是自包含的纯函数集合:无Effect依赖、无 Fiber/Scope 资源,可在任何上下文同步调用,这也是它能直接并入顶层Encoding命名空间而不引入额外服务依赖的原因。
六、迁移清单
从旧代码迁移到整合后的模块,按以下映射执行:
| 旧写法(v3 及之前) | 新写法(v4) |
|---|---|
import { Base64 } from "effect/encoding"(子路径导入) | import { Encoding } from "effect" |
Base64.encode(x)/Base64.decode(x) | Encoding.encodeBase64(x)/Encoding.decodeBase64(x)(或decodeBase64String) |
Base64Url.encode(x)/Base64Url.decode(x) | Encoding.encodeBase64Url(x)/Encoding.decodeBase64Url(x) |
Hex.encode(x)/Hex.decode(x) | Encoding.encodeHex(x)/Encoding.decodeHex(x) |
Encoding.isDecodeException(e)/isEncodeException(e) | Encoding.isEncodingError(e),按e.kind区分阶段 |
捕获DecodeException/EncodeException | 统一处理EncodingError(kind: "Decode" \| "Encode") |
迁移时的两个行为细节:解码不再 throw,需按Result处理失败分支;decodeBase64Url兼容填充与未填充两种输入,而decodeBase64严格要求 4 倍数长度与末尾填充,跨端(如与只接受 unpadded 的第三方服务对接)时按对接方约定选对函数即可。
【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考