演示声明:本文的 NoteBudget、NoteReviewPage、任务 NB-3311、全部页面图、DevEco Studio 外观图和 HiLog 行,均是为解释工程设计而制作的静态演示。没有声称完成 DevEco 编译、模拟器运行、真机测试、后台提交或上架审核。
一、交接备注不长,为什么服务器仍然拒收
一个巡检人员填写交接备注时,肉眼看到的文字并不多:A班:阀门已复核✅;照片待补。页面上写着“最多四十八个字节”,他却习惯用字符数量估计剩余额度。等到点击确认,接口返回超长,备注只能重新删。这个问题看起来像简单的长度限制,实际涉及三种完全不同的度量:JavaScript 字符串的长度、用户看到的字符,以及最终用于传输的 UTF-8 字节数。三者不能互相替代。
我把现场缩成一个很小的 NoteBudget 页面,只留任务编号NB-3311、一段已确认备注、一段正在编辑的草稿和一个确认按钮。原有备注A班:阀门已复核占二十二个 UTF-8 字节;当前草稿A班:阀门已复核✅;照片待补占四十个字节,在四十八字节限制以内。再补上;建议复查,草稿就涨到五十五字节。用户可以继续编辑,但不能提交。这样做的目的,是把“允许输入”和“允许确认”明确拆开。
二、先定清楚字节预算是谁的责任
这里的四十八字节不是 TextArea 的内置系统限制,也不是 HarmonyOS 平台对备注长度的强制规定,而是 NoteBudget 演示业务自己约定的传输字段上限。真正服务端采用何种编码、是否执行 Unicode 规范化、换行如何转换,都应当由协议文档定义;没有协议就不能在客户端凭经验写死。本文假设双方使用 UTF-8,且按用户当前实际输入的码点编码,不自动做 NFC 或 NFKC 转换。
text.length统计的是 UTF-16 代码单元,中文通常占一个代码单元,补充平面表情一般占两个;而 UTF-8 常见中文占三个字节,勾号表情占三个字节,其他表情可能占四个字节。视觉上还会遇到多个代码点组成一个字素簇的情况,所以本文刻意不把“字素数量”称作“字节数”。提示区域写“40 / 48 bytes”,不会误写“40 / 48 字”。
三、字节计数函数要处理非法代理项
第一段代码只解决编码长度的计算,不负责修改用户的字符串。为了不依赖未经核对的系统编码器接口,演示使用标准字符串代码单元做 UTF-8 字节计数。尤其要留意代理项:高代理项必须匹配后面的低代理项,孤立代理项不能静默当成正常表情。返回-1表示输入不满足本演示的 Unicode 校验策略,业务层将其显示为不可提交,而不是私自替换用户原文。
// NoteBudget/entry/src/main/ets/utils/Utf8Budget.ets export function utf8ByteCount(text: string): number { let size: number = 0; for (let i: number = 0; i < text.length; i++) { const first: number = text.charCodeAt(i); if (first >= 0xD800 && first <= 0xDBFF) { if (i + 1 >= text.length) return -1; const next: number = text.charCodeAt(++i); if (next < 0xDC00 || next > 0xDFFF) return -1; size += 4; } else if (first >= 0xDC00 && first <= 0xDFFF) { return -1; } else if (first < 0x80) { size += 1; } else if (first < 0x800) { size += 2; } else { size += 3; } } return size; }这里没有创建文件句柄,也没有网络请求,因此计算函数不需要额外释放资源;它接收字符串并返回数字,便于单独写表驱动测试。极长文本每次编辑都全量遍历,时间复杂度随内容增长。交接备注只有几十字节时这种成本可以接受,如果换成大型富文本编辑器,就需要改成增量统计或延迟校验,不能直接把这段实现无限扩展。
四、把草稿、确认值和合法性三个状态分开
我更在意的是用户修改过程中不能丢掉原先的确认记录。单独用一个@State text来同时代表输入框内容和已保存值,看起来省事,却会造成“键入第一个字符,界面上的已确认记录也跟着改变”的误导。这里让draft跟随TextArea.onChange,confirmed只在提交成功之后变化,stateLabel则由校验规则派生。它们承载不同语义,没有必要互相覆盖。
第二段代码给出业务门禁,特意只返回校验结果。输入过长时保留编辑缓冲,不通过截断把光标强行送到末尾;一旦用户删掉超出的词句,状态自然从OVER_BUDGET恢复为READY_TO_SUBMIT。对于用户输入法正在组合的文字,具体回调行为与设备、输入法和目标 SDK 有关,本文没有把演示当作真机输入法行为结论。
// NoteBudget/entry/src/main/ets/model/NoteRule.ets import { utf8ByteCount } from '../utils/Utf8Budget'; export const NOTE_LIMIT: number = 48; export function checkDraft(value: string): string { const used: number = utf8ByteCount(value); if (used < 0) return 'BAD_UNICODE'; if (used === 0) return 'EMPTY'; if (used > NOTE_LIMIT) return 'OVER_BUDGET'; return 'READY_TO_SUBMIT'; }BAD_UNICODE、EMPTY和OVER_BUDGET都应该在界面上给出具体而不同的提示。不要把所有失败统一写成“网络异常”,也不要只用按钮灰色表达失败原因。状态机一旦明确,错误提示、日志字段以及之后的产品埋点才有可复核的含义。
五、页面只负责绑定,不替业务悄悄修文案
第三段代码把这几个状态连接到 ArkUI。TextArea的text属性绑定当前草稿,onChange更新草稿并重新校验。提交时复制一个局部快照,再重做一次字节校验,避免只依赖上一次输入事件的状态。代码里的“提交”仅代表演示页面内确认,不包含网络持久化;如果产品需要真实保存,应该等待存储层返回成功后再更新confirmed,失败时保留草稿并给出重试入口。
// NoteBudget/entry/src/main/ets/pages/NoteReviewPage.ets import { utf8ByteCount } from '../utils/Utf8Budget'; import { checkDraft } from '../model/NoteRule'; @Entry @Component struct NoteReviewPage { @State private confirmed: string = 'A班:阀门已复核'; @State private draft: string = 'A班:阀门已复核✅;照片待补'; @State private stateLabel: string = 'READY_TO_SUBMIT'; @State private revision: number = 6; private onDraftChange(value: string): void { this.draft = value; this.stateLabel = checkDraft(value); } private commitNote(): void { const snapshot: string = this.draft; const result: string = checkDraft(snapshot); this.stateLabel = result; if (result !== 'READY_TO_SUBMIT') return; this.confirmed = snapshot; this.revision += 1; this.stateLabel = 'COMMITTED'; } build() { Column({ space: 12 }) { Text('NB-3311 · 交接备注') TextArea({ text: this.draft, placeholder: '填写交接情况' }) .height(160) .onChange((value: string) => this.onDraftChange(value)) Text(`UTF-8 ${utf8ByteCount(this.draft)} / 48 bytes`) Text(`状态 ${this.stateLabel} · rev=${this.revision}`) Button('确认交接').onClick(() => this.commitNote()) }.padding(20) } }这版设计刻意没有让 UI 初始化时调用异步接口,也没有模拟应用恢复后的自动持久化。组件销毁时没有额外的订阅和定时器可清理;真实项目若加入键盘监听、自动保存防抖、网络任务,应在相应生命周期撤销订阅与取消待处理工作。为了阅读顺畅,示例省略了交互焦点管理和无障碍描述,这些需要在产品化时补齐。
六、把超限过程拆成可重复的验收动作
验收不应只点一次确认。我的演示顺序是先打开页面,确认服务端旧记录仍显示二十二字节;接着在草稿末尾加上;建议复查,这时字节数变为五十五,状态进入OVER_BUDGET,确认按钮即使收到点击也不改变旧记录。最后删掉新增词句,字节数回到四十,状态恢复为READY_TO_SUBMIT,这才允许进入确认流程。截图固定取恢复后的待提交状态,避免把四十字节截图误说成已经提交。
需要特别说明,演示日志只记录任务、预算、实际字节数和校验状态,不打印完整备注。即便这段文案只是测试句子,真实产品的交接记录仍可能包含电话号码、车牌或现场隐私。日志中不应把输入框原文当作错误上下文原样输出,更不能依靠 Release 版本默认关闭日志来代替数据最小化。
七、字符长度、字素、编码规范是三道不同的题
几个边缘样本很能暴露设计缺陷。ASCII 的A只占一个 UTF-8 字节,中文班占三个字节,✅占三个字节;有些彩色表情会由多个码点串联成一个可见图形,字节数可能明显高于人的直觉。组合音标可能出现两个视觉接近却编码不同的字符串,而标准化会改变实际字节串。本文函数计算的是传入字符串当下的编码长度,不替用户决定规范化方案,也不承诺视觉字符一致就得到相同预算。
另一个陷阱是换行。输入框里的\n通常表示一个换行字符,采用 UTF-8 编码占一个字节;如果服务端在传输前把 LF 转成 CRLF,就会多出一个字节。前端、接口网关和落库字段要说清楚是在什么阶段、对哪份数据计费。只要协议没有统一,前端的精确计数也不能保证端到端一致。
八、回滚不是清空输入,而是保留用户的意图
当接口真正接入以后,存在一种容易被忽略的失败:客户端预算通过,但后端因业务版本变更或字段策略拒绝保存。此时应保留草稿四十字节,confirmed保持旧值,状态转换成单独的SAVE_FAILED,允许用户重试或显式放弃。不能在点击按钮当下就把确认值覆盖,然后等请求失败再凭回忆恢复。若多次点击产生重入,后台最好提供幂等请求标识或条件写入版本;本文没有假装用一个本地 revision 就解决分布式并发。
这也解释了为什么 Demo 故意显示rev=6:它只是本地已确认记录的演示修订号,不是服务器真正确认的乐观锁版本。要实现跨设备一致性,还必须和服务端协商版本字段以及冲突处理规则。没有这些约束,页面可以正确显示“准备提交”,却不能断言远端已经安全保存。
九、最后保留一份可追溯的边界清单
对 NoteBudget 来说,最稳妥的发布前验收是覆盖空串、ASCII、中文、表情、组合码点、孤立代理项、恰好四十八字节、超出一字节以及输入法中间态。原文变化时只改草稿;校验失败时不能覆盖确认值;真正存储失败时不能显示已保存。人工构造的NB-3311 DRAFT_BYTES=40 LIMIT=48与NB-3311 VALIDATION=READY_TO_SUBMIT仅用于配图核对,不代表真实 HiLog 采样。
这个案例的结论不在于手写了多少行字节统计,而在于把“正在打字”“满足协议”和“已确认保存”拆成三层可检查的契约。输入体验允许修正,传输约束可以明确,历史记录则不会因为一次未完成的编辑被悄悄污染。
十、资料与未验证项
华为官方参考包括 ArkTS 语言概述与 TextArea 输入状态的常见问题说明:
- https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-overview
- https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-1615
本文仅核对公开文档可确认的 TextArea/onChange 基本能力与 ArkTS 语法背景。上述代码还需在目标 SDK、输入法、设备形态和实际服务器编码协议下编译与联调,所有图片和日志均为演示。