茶器艺科智造HarmonyOS应用实战-24-导出的数组和默认草稿都可被消费者改写用readonly与工厂函数守住HAR边界HAR 把SHOP_FILTER_KEYS、预设标签、快捷指令、雷达参考值和DEFAULT_CHAQI_DRAFT_STATE统一导出后消费者拿到的不只是“常量名字”还是数组与对象的引用。const只禁止重新给变量赋值并不禁止reverse()、push()、下标赋值或修改对象属性。一个页面若为方便排序而原地 reverse另一个页面看到的顺序也会改变一个测试若向默认草稿的 orders push 数据所有直接持有或再次复用这份公共默认对象的消费者都会看到测试订单。当前load()自己构造新外层状态orders 也由parseOrders(..., )生成因此这项 orders 风险不会沿现有恢复路径自动传播。逐项检查指定源码后没有发现 entry 或 libraryhsp 当前调用这些常量的 push、pop、splice、sort、reverse、fill 或属性赋值现有消费者以读取为主。这说明本文讨论的是 HAR API 可变性风险不是宣称线上已经发生污染。风险仍值得在公共边界收口因为libraryhar/Index.ets把这些引用直接交给所有消费者模块内部无法约束未来代码或新的依赖方。本文会区分三种保护原始值数组用readonly表达只读借用。对象数组同时把元素字段和嵌套成员设为只读。需要独立所有权的默认草稿用工厂函数创建新对象与新数组。第三方 API 必须接收可变数组时调用方拿副本不放宽 HAR 源数据。一、源码锚点显示多个可变引用直接穿过 Index.etslibraryhar/src/main/ets/model/ChaqiModels.ets当前导出以下数组export const SHOP_FILTER_KEYS: ShopFilter[] [all, printing, pending, completed]; export const PRESET_LABELS: string[] [罗汉杯, 悟空杯, 方盏, 高足杯, 螺旋瓶, 竹节杯]; export const CUP_PRESET_KEYS: CupPresetKey[] [luohan, wukong, fang, generic, spiral, zhujie]; export const CHAT_QUICK_CHIPS: ChatQuickChip[] [ /* 五个对象 */ ]; export const ARCHIVE_RADAR_LABELS: string[] [高度, 腰比, 波纹, 半径, 壁厚, 口径]; export const ARCHIVE_RADAR_OPTIMAL: number[] [75, 65, 85, 70, 45, 72];ChaqiDraftStore.ets:91还导出一个完整默认对象其中orders是数组。libraryhar/Index.ets:16与:20-29将它们再次导出。当前页面用 ForEach 读取筛选键、预设标签与快捷指令雷达组件将两个数组赋给局部 const 后读取草稿服务把默认对象赋给局部 defaults数值和字符串字段把它用作 fallback而 orders 通过parseOrders(this.readString(store, KEY_ORDERS_JSON, ))单独生成新数组不读取defaults.orders。局部再写一个 const 不会复制对象。const labels ARCHIVE_RADAR_LABELS仍然指向同一个数组。二、const 保护变量绑定不保护数组和对象内容下面代码在普通 TypeScript/ArkTS 语义上表达的都是原地修改风险SHOP_FILTER_KEYS.reverse(); PRESET_LABELS[0] 临时杯型; CUP_PRESET_KEYS.push(luohan); ARCHIVE_RADAR_OPTIMAL.fill(0); CHAT_QUICK_CHIPS[0].text 被消费者替换的指令; DEFAULT_CHAQI_DRAFT_STATE.heightMm 999; DEFAULT_CHAQI_DRAFT_STATE.orders.push(buildUnexpectedOrder());编译器是否允许每一行还受项目 ArkTS 版本和导出类型影响但当前声明明确使用可变T[]和可写接口字段没有在类型层表达禁止修改。即使调用方没有恶意常见的sort()、reverse()、fill()也会就地改变源数组。污染后的影响跨越功能筛选按钮顺序改变标签与 key 的索引对齐破坏快捷指令文本变化雷达“最优”曲线被重写默认草稿中实际作为 fallback 的数值或字符串字段改变。orders 的共享修改只影响直接持有或复用公共默认对象的消费者当前load()的 orders 路径已经通过解析创建新数组。它们看似是不同 bug根因却是公共 API 仍把共享可变所有权交给了消费者。三、原始值数组先改成只读借用字符串、数字和联合类型都是原始值数组只读即可阻止常见原地修改。建议把声明改为readonly T[]export const SHOP_FILTER_KEYS: readonly ShopFilter[] [all, printing, pending, completed]; export const ARCHIVE_RADAR_LABELS: readonly string[] [高度, 腰比, 波纹, 半径, 壁厚, 口径]; export const ARCHIVE_RADAR_OPTIMAL: readonly number[] [75, 65, 85, 70, 45, 72];readonly的价值首先是编译期契约消费者看到后就知道只能遍历和读取不能调用要求可变数组的方法。它不等于运行时深冻结也不能阻止绕过类型系统的 JavaScript 或强制断言。HAR 边界应以类型防误用再用工厂或副本处理真正的所有权隔离。如果采用第 23 篇的CUP_PRESETS配置表标签和 key 不必再作为两组公共数组导出确需兼容旧 API可以由只读表派生只读快照并标记迁移计划。四、对象数组必须连元素字段与嵌套值一起约束把CHAT_QUICK_CHIPS仅改为readonly ChatQuickChip[]只能阻止数组增删和重排仍可执行CHAT_QUICK_CHIPS[0].text ...因为元素对象的字段还是可写。应给元素接口增加 readonlyexport interface ChatQuickChip { readonly title: string; readonly text: string; readonly hint: string; } export const CHAT_QUICK_CHIPS: readonly ChatQuickChip[] [ { title: ① 罗汉·兔毫·略矮, text: 罗汉杯兔毫釉略矮, hint: 杯型词 兔毫 略矮/矮 → 范式、波纹、总高 } ];预设配置还有嵌套 defaults 与 aliases需要显式深一层export interface ReadonlyCupDefaults { readonly heightMm: number; readonly diameterMm: number; readonly mouthMm: number; readonly bottomMm: number; readonly waistPct: number; } export interface CupPresetSpec { readonly key: CupPresetKey; readonly label: string; readonly profileKind: CupProfileKind; readonly defaults: ReadonlyCupDefaults; readonly aliases: readonly string[]; }“外层 readonly 数组 元素 readonly 字段 嵌套对象 readonly 字段”才表达了这份配置的只读借用。若以后 defaults 增加数组或对象还要继续扩展只读类型语言里的浅只读不会自动递归。五、默认草稿需要工厂因为调用方本来就要修改它默认配置与草稿状态的所有权不同。当前load()已经返回新外层对象并为 orders 创建新数组但数值和字符串 fallback 仍从共享的DEFAULT_CHAQI_DRAFT_STATE读取其他消费者也可以直接拿到这份公共对象。页面拿到草稿后会改变高度、页签、订单等字段因此不能把整份状态永久声明成只读单例再到处强制转换。更直接的做法是取消公共默认对象导出工厂export function createDefaultChaqiDraftState(): ChaqiDraftState { return { selectedTabIndex: 0, presetIndex: 0, heightMm: 70, diameterMm: 85, mouthMm: 85, bottomMm: 41, thicknessMm: 3, waistPct: 45, waveLevel: 0, autoSpin: true, shopFilter: all, sliceLayer: 0, sliceTotal: 160, sliceShowPaths: false, sliceInfillPct: 20, sliceNozzleTempC: 205, printerNameInput: Creality K1, printerIpInput: 192.168.1.100, printerPortInput: 8080, orders: [] }; }每次调用都会创建新外层对象和新 orders 数组。两个工厂实例之间不会共享订单或其他可写字段现有load()的 orders 本来就没有读取defaults.orders工厂主要把这种独立所有权扩展到所有 fallback 字段和其他消费者。测试也可以直接比较两个实例而无需在 afterEach 手动恢复共享常量。当前buildDemoOrders()已采用类似所有权思路函数内部创建const list: ShopOrder[] []逐项填充后返回每次调用获得新数组。computeArchiveRadarCurrent()也返回新 number[]。这两处可以作为项目内现有正例。六、展开运算符只是浅拷贝orders 仍要逐项复制若从已存在的草稿复制可编辑状态{ ...source }只创建新外层对象orders仍指向原数组。即使 orders 数组被复制订单对象也可能继续共享。当前 ShopOrder 字段都是原始值可用显式 map 做完整的一层复制function copyOrder(order: ShopOrder): ShopOrder { return { id: order.id, status: order.status, progress: order.progress, name: order.name, material: order.material, date: order.date }; } export function copyDraftState(source: ChaqiDraftState): ChaqiDraftState { return { ...source, orders: source.orders.map((item: ShopOrder) copyOrder(item)) }; }如果 ArkTS 目标版本对展开运算或匿名对象有额外静态限制应把所有字段显式列出并以实际编译为准。不要把JSON.parse(JSON.stringify(source))当作通用深拷贝它会丢失非 JSON 类型、类原型和特殊数值未来模型扩展后风险更大。浅只读与深拷贝解决不同问题。只读类型表达“借来的配置不可修改”复制表达“调用方获得自己的可变所有权”。默认草稿显然属于后者。七、只读类型要沿调用链传播必要时在边缘复制把常量改成 readonly 后某些消费函数若仍声明number[]编译器会认为它可能修改数组从而拒绝传入。正确做法是把真正只读的参数也改成 readonlyprivate drawPolygon( values: readonly number[], radius: number, centerX: number, centerY: number ): void { // 只按下标读取 values不做排序或填充 }当前ArchiveRadarCanvas只是读取 labels 和 optimal适合一路使用 readonly。ForEach 的回调也只读元素不需要可变数组。若第三方绘图库的签名确实要求number[]应在调用边缘创建副本const mutableOptimal: number[] [...ARCHIVE_RADAR_OPTIMAL]; thirdPartyChart.setValues(mutableOptimal);这样即使第三方原地 sort 或 fill也只影响临时副本。不要为了一个可变 API 把 HAR 源常量重新声明成可变。八、兼容迁移需要避免一次破坏所有消费者直接删除DEFAULT_CHAQI_DRAFT_STATE会让现有 import 失败。可以分两步迁移先新增工厂并让 ChaqiDraftStore 内部用每次新建的 defaults 替代共享 fallback同时保留现有 orders 解析路径再更新 Index.ets 与消费者最后移除旧导出。async load(ctx: common.UIAbilityContext): PromiseChaqiDraftState { const store await this.getStore(ctx); const defaults createDefaultChaqiDraftState(); const draft: ChaqiDraftState { // 每个字段从 store 读取fallback 来自本次独立 defaults selectedTabIndex: this.readNumber( store, KEY_SELECTED_TAB_INDEX, defaults.selectedTabIndex), // ...其余字段 orders: parseOrders(this.readString(store, KEY_ORDERS_JSON, )) }; return draft; }save 中的 fallback 也应在一次调用开始时创建一份 defaults避免反复调用工厂并保持本次保存的一致基线。对于筛选键、雷达值和快捷指令改 readonly 可能暴露消费者签名问题逐个把只读意图向下传播只有确实需要修改的一端才复制。九、验证矩阵、故障排查与证据边界工厂的独立所有权可以用运行单测证明it(creates independent draft and order array, 0, () { const first createDefaultChaqiDraftState(); const second createDefaultChaqiDraftState(); first.heightMm 88; first.orders.push(sampleOrder()); expect(second.heightMm).assertEqual(70); expect(second.orders.length).assertEqual(0); });验证面操作预期证据编译期数组边界尝试 reverse/fill/pushreadonly 调用被拒绝编译期对象边界尝试修改 quick chip textreadonly 字段赋值被拒绝工厂独立性创建两份默认草稿并修改其一另一份数值和 orders 不变雷达读取readonly 数组传入 drawPolygon编译通过、只读签名一致第三方可变 API传展开副本并让其排序源数组顺序不变草稿恢复连续 load 使用独立 defaults并单独检查 orders 解析fallback 字段不共享orders 继续由每次解析创建现象首查常见根因修正改 readonly 后调用编译失败消费函数参数类型下游仍声明可变 T[]真只读则改签名否则传副本两份草稿 orders 同时变化工厂是否每次 new 数组复用了共享数组在工厂内创建[]外层不能赋值但 chip 文本能改元素接口字段只读只加在数组上元素与嵌套字段也加 readonlyspread 后订单仍互相影响是否只做浅拷贝元素对象继续共享typed map 复制订单测试改变默认高度后其他用例失败是否仍导出单例对象默认状态被共享改用工厂且每例新建为兼容图库放宽全部常量可变需求位置所有权边界放错只在调用边缘复制当前源码事实是上述数组均以可变T[]导出快捷指令字段可写默认草稿是共享对象且含 orders 数组Index.ets 再次公开这些引用现有load()返回新外层状态orders 由parseOrders创建而不读取defaults.orders现有工程消费者中未发现原地修改调用。本文的 readonly 类型、嵌套只读接口、草稿工厂、typed copy 与迁移步骤都是建议尚未修改D:\ProgramData\huawei\lesson\chaqi-app-gitee-master。本次没有运行 ArkTS 编译、单测、hvigorw assembleHap --no-daemon、模拟器、真机或 CSDN 发布因此可以确认静态 API 风险与建议边界不能声称编译门禁和运行隔离已在原项目生效。