ARTICLE DETAIL

资讯详情

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

Webiny 架构决策解读:ADR-007 转换函数必须返回新值(Transformations Produce New Values)

Webiny 架构决策解读:ADR-007 转换函数必须返回新值(Transformations Produce New Values) CMS后端前端【免费下载链接】webiny-jsOpen-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.项目地址https://gitcode.com/gh_mirrors/we/webiny-js点击查看免费下载导读本文深入解读 Webiny 开源仓库中的架构决策记录 ADR-007Transformations produce new values。该 ADR 为 Webiny 项目确立了“数据处理/转换函数一律返回新对象、绝不修改输入”的编码准则是项目在资产交付Asset Delivery、选项解析Option Parsing与值归一化Value Normalization等模块中遵循的核心约束。读完本文你将理解这条规则背后的动机、它在 Webiny 源码中的落地形态含normalizeImageOptions、Asset.withProps()、normalizeToAsset()三个典型实现以及如何用测试验证“输入不被修改”这一不可变约定并能在自己的代码中直接复用这套模式。一、背景为什么数据转换函数必须“返回新值”1.1 两种风格的对比函数对数据的转换存在两种风格就地修改mutation函数直接修改传入的对象例如delete options.width; options.format parsed调用方持有的变量在函数执行后“悄然变化”。返回新值return new object函数不触碰输入而是构造并返回一个全新的对象。从代码长度看就地修改通常更短、写起来更快。但 ADR-007 明确指出这种做法带来三类问题数据流难以追踪调用方的变量在函数返回后不再是原来的值属性和内容“凭空出现或消失”阅读代码时很难判断某个变量在什么时间点变成了什么。隐含耦合多个函数按顺序修改同一个对象时后一个函数的输入依赖于前一个函数“改了哪些字段”调用顺序错误就会产生难以排查的 bug。共享引用陷阱如果输入对象同时被其他代码持有就地修改会把副作用扩散到所有持有该引用的地方。1.2 决策内容因此 ADR-007 的决策是所有对数据进行转换的函数都必须返回新对象而不是修改输入。输入一律视为只读输出一律是全新的值。该规则适用于选项解析 / 归一化函数option parsing / normalization functions领域对象转换如withProps、clone值归一化与转换器value normalizers and converters这本质上是一种“函数式、只读输入”的编码纪律与 Webiny 项目其他 ADR 一脉相承例如 ADR-006Deprecate incrementally 中为兼容旧数据而保留的ImageValue - Asset转换路径正是通过 ADR-007 所规定的“只读输入 返回新对象”的归一化函数实现的。二、落地实现一normalizeImageOptions——把原始查询串解析为类型化选项2.1 源码位置与职责normalizeImageOptions位于 packages/api-file-manager/src/features/assetDelivery/assetTypes/image/normalizeImageOptions.ts负责把来自 HTTP 查询参数的原始字符串width、quality、format、crop、aspectRatio、focal等解析为类型化的ImageRequestOptions对象。其类型定义在 imageTypes.ts 中export interface ImageRequestOptions { original?: boolean; // 是否请求原图 width?: number; // 目标宽度像素 quality?: number; // 编码质量1–100 format?: ImageFormat; // 具体输出格式已从 auto 解析完成 crop?: AssetCrop; // 请求级裁剪0–1 边距优先于资源级裁剪 aspectRatio?: number; // 目标宽高比宽 / 高 focal?: { x: number; y: number }; // 0–1 归一化焦点配合 aspectRatio 裁切时保持画面主体 }2.2 返回新值而非原地修改ADR-007 中明确记载该函数过去是就地修改options的delete options.width; options.format parsed重构后改为返回全新的ImageRequestOptions对象。当前源码完全遵循这一约定函数内部创建一个新的result对象逐个字段解析后挂载最后整体返回全程不触碰传入的query对象export const normalizeImageOptions ( query: Recordstring, any, acceptHeader: string | undefined ): ImageRequestOptions { const result: ImageRequestOptions { original: original in query }; const width query.width ? parseInt(query.width, 10) : NaN; if (!Number.isNaN(width) width 0) { result.width width; } // quality、format、crop、aspectRatio、focal 同理…… return result; };注意其典型的“白名单 守卫式挂载”写法只有解析成功且语义合法的值才会被写入新对象非法输入width: abc、负数宽度、畸形 crop 等不会污染结果而是直接省略对应字段。2.3 测试如何锁定“输入不被修改”该函数的测试位于 packages/api-file-manager/tests/features/assetDelivery/normalizeImageOptions.test.ts其中专门有一条用例验证不可变约定it(does not mutate the input query, () { const query { width: 800, quality: 75, format: webp }; const original { ...query }; normalizeImageOptions(query, undefined); expect(query).toEqual(original); });这条用例是 ADR-007 决策的直接测试化体现先对输入做浅拷贝快照调用函数后断言输入与快照完全一致。测试还覆盖了大量解析细节可作为参数取值范围与默认行为的权威参考width合法正整数被解析为数字abc、0、-10一律被丢弃返回undefined。quality会被clampQuality钳制到 1–100如150→ 1000→ 1。format支持显式格式webp保留gif被丢弃formatauto时依据Accept头解析image/avif优先于image/webp无匹配时返回undefined。crop0.1,0.2,0.1,0.05解析为{top,left,bottom,right}边距越界值钳制到 0–1全零、位数不足或字符串畸形的 crop 会被丢弃。aspectRatio支持16:9冒号记法与1.5小数记法非法值丢弃。focal0.3,0.7解析为{x:0.3, y:0.7}坐标钳制到 0–1。这套测试不仅验证了“不修改输入”也验证了“非法输入不会以字符串形式残留到新对象上”与源码的守卫式写法互为印证。三、落地实现二Asset.withProps()/clone()——不可变领域对象3.1 源码位置与实现Asset类是 API 文件管理模块中图片交付流程的领域对象位于 packages/api-file-manager/src/delivery/AssetDelivery/Asset.ts。它的核心不可变 API 是withProps与cloneexport class Asset { protected readonly props: AssetData; constructor(props: AssetData) { this.props props; } clone() { return this.withProps(structuredClone(this.props)); } withProps(props: PartialAssetData) { const newAsset new Asset({ ...this.props, ...props }); newAsset.contentsReader this.contentsReader; newAsset.outputStrategy this.outputStrategy; return newAsset; } // getId / getTenant / getKey / getSize / getContentType / getExtension 等只读访问器 }实现要点props被声明为readonly从构造起就不允许在类内部被重新赋值从类型层面封死了就地修改的可能。withProps()绝不修改原实例它通过展开运算符{ ...this.props, ...props }构造一份合并后的新数据再创建新的Asset实例返回。原始实例的props保持原样。为了让新实例保持可用的交付能力withProps会同步复制contentsReader内容读取器与outputStrategy输出策略这两个运行时依赖clone()则利用structuredClone深拷贝props后走同一条withProps路径得到一份完全独立的副本。3.2 该模式带来的工程收益withProps/clone是典型的“返回新值”风格在领域对象上的应用调用链可以安全地链式派生例如先withProps({key: newKey})再withProps({size: newSize})每一步都产生新实例任何一步出错都不会污染前面的中间结果。Asset的只读访问器getId、getKey等与不可变更新器withProps组合恰好构成“读方法不改、写方法返回新对象”的经典不可变对象形态。四、落地实现三normalizeToAsset()——面向旧数据的兼容归一化4.1 源码位置与职责normalizeToAsset位于 packages/website-builder-sdk/src/asset/normalize.ts是 ADR-006增量弃用旧数据与 ADR-007 共同作用的产物它把任意历史遗留的输入形态旧版 Website Builder 的扁平edit字段、6.x 时代无image子对象的文件值、以及已经统一化的新结构归一化为新的WebinyAsset且不修改输入export function normalizeToAsset(input: unknown): Asset | null { if (!isObject(input)) { return null; } const mimeType asString(input.mimeType) ?? ; const asset buildBase(input, mimeType); // 全新对象 const category getAssetCategory(mimeType); // 按 MIME 前缀分桶image / video / document // 根据是否已有 image/document/video 子对象走 legacy 或已统一结构两条分支 // 最终通过 syncImageDimensions 把尺寸镜像到根级返回新的 asset。 return syncImageDimensions(asset); }其关键设计在源码注释中有明确体现为了兼容 6.4 前端读取根级width/height的习惯syncImageDimensions会把image.width/height同步到资源根级——这一步同样是在新对象上完成不触碰输入。底层辅助函数asNumber、asString、isObject保证了对任意不可信输入的安全降级解析失败只返回undefined绝不抛错。4.2 测试验证“任意旧输入 - 新值”测试位于 packages/website-builder-sdk/src/asset/normalize.test.ts覆盖了三类输入形态旧版扁平值含editnormalizeToAsset(legacy)把edit.crop映射为新结构的image.crop把edit.hotspot映射为image.focalPoint同时保留alt、尺寸等信息产出全新的统一对象。6.x 扁平值无edit、无image子对象normalizeToAsset自动补出image: { width, height }让旧数据无需存储迁移即可被新代码渲染。已经统一化的资源normalizeToAsset具备幂等性——对格式良好的新资源传入返回与输入相等的新对象对视频资源则完整保留video.autoplay、video.poster对null、undefined、字符串、数字等非法输入一律返回null。测试还通过assetImageFromLegacyEdit单独验证了“hotspot - focalPoint、crop/alt/caption 保留”的映射细节与 ADR-006 中“保留旧类型、用归一化函数透明转换旧数据无需重新保存即可工作”的决策相互印证。五、权衡与适用边界ADR-007 在 Consequences 一节对收益与代价做了清晰的评估5.1 正面收益数据流可追踪每个变量在其生命周期内只持有“一个值”函数返回后变量内容不会悄悄变化排查问题时无需回溯“这个对象是什么时候被谁改的”。更易调试与测试函数可以当作纯函数对待——相同的输入必然得到相同的输出前提是内部无隐藏状态因此可以写出“输入快照 调用 断言相等”式的测试如 2.3 节所示。无共享引用惊吓调用方无需担心自己的对象被他人修改传参可以放心地把对象交给函数。5.2 代价与边界更多对象分配每次转换都新建对象会带来额外 GC 压力。在热路径上的取舍文档明确指出内层循环、高频操作中这一点可能产生影响而 Webiny 中大多数转换发生在请求级处理request-level processing这一层分配开销可以忽略不计。这给出了该规则的适用范围判断标准开销敏感的内层循环可以做例外评估但业务请求链路中的转换函数应当一律遵守“返回新值”。这也是把该规则写成 ADR架构决策记录而非硬性 lint 规则的原因——它既是一种强默认约束又保留了在明确测量到瓶颈时的权衡空间。六、如何在你的代码中复用该模式结合以上源码可以提炼出一套可直接复用的落地清单函数签名上承诺“输入只读”在 JSDoc 注释中写明“Returns a new object; does not mutate the input”如normalizeImageOptions.ts顶部注释所示。先建新对象再守卫式挂载字段对每个可解析字段采用“解析成功才写入”的白名单写法非法输入直接省略而不是把原始字符串留在结果里。领域对象用readonly 返回新实例如Asset那样将内部状态声明为readonly提供withProps/clone这类“返回新实例”的更新方法必要时复制运行时依赖到新实例。写一条“输入不被修改”的测试这是把 ADR 决策固化为可执行约束的最小成本方式——浅拷贝快照、调用函数、断言toEqual。涉及旧数据兼容时与 ADR-006 配合保留旧类型、用归一化函数如normalizeToAsset透明转换让旧数据无需迁移即可工作同时严格保持输入只读。结语ADR-007 是 Webiny 在“数据转换”上的核心架构约定输入只读、输出全新。它既不是激进函数式教条的照搬也不是一刀切的性能禁令而是一条针对请求级业务处理场景的强默认约束——用一次对象分配换取数据流的可追踪性、可测试性与共享引用安全。通过normalizeImageOptions、Asset.withProps()、normalizeToAsset()三个实现及其测试你可以直接观察这条 ADR 在真实项目中的完整生命周期决策 → 落地 → 测试锁定 → 权衡边界。赞分享CMS后端前端【免费下载链接】webiny-jsOpen-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.项目地址https://gitcode.com/gh_mirrors/we/webiny-js点击查看免费下载相关推荐plannotator 架构决策记录ADR实践指南从 ADR-0001 到 007 的决策治理体系plannotator 架构决策记录ADR实践指南从 ADR 0001 到 007 的决策治理体系 导读 本文围绕 plannotator 仓库中的 ADESLint getter-return 规则详解强制 Getter 必须返回值ESLint getter return 规则详解强制 Getter 必须返回值 getter return 是 ESLint 内置的一条 problem 类开发工具Lint静态分析代码质量ArkAnalyzer返回语句函数返回值分析ArkAnalyzer返回语句函数返回值分析 引言 在ArkTS语言开发中函数返回值分析是静态程序分析的关键环节。ArkAnalyzer作为面向ArkTS语静态分析开发工具OpenHarmony上一篇联想刃7000k BIOS隐藏菜单终极解锁指南3分钟释放硬件隐藏性能下一篇3分钟解锁AI图像分层魔法layerdivider让复杂设计变简单创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表