
发布前最后一轮走查里有一类问题很容易被“默认字号看起来正常”掩盖文本容器写死高度系统字体放大后只剩半行图标做成可点击入口却没有可供屏幕朗读识别的语义为了压住布局又把字号写成vp界面安静了用户设置也失效了。本文不虚构一次真实审核驳回而是构造一个可复现的候选发布检查器 AccessGate。它扫描 12 个 ETS 文件在候选版本RC-20261001-06中生成 7 条提示3 条高优先级、4 条中优先级。修复后同一规则集返回 0 条。这里的数字属于示例运行数据工具输出是团队发布门禁不等同于应用市场的官方审核结论。一、先划清门禁能管什么AccessGate 只处理可静态识别、容易反复出现的工程气味。第一类是小字号直接复用 Code Linter 的cross-device-app-dev/font-size规则官方说明该规则要求字体不小于 8fp。第二类是文本附近出现固定高度例如Text(...).height(40)在字体放大或文案变长时具有截断风险。第三类是承担点击行为的图片缺少无障碍文本例如Image(...).onClick(...)附近没有accessibilityText。后两类是项目自定义启发式规则不是官方审核条款。它们会误报也会漏报。一个 40vp 高的单行标签可能完全合理无障碍语义也可能由父容器分组统一提供。门禁因此分两层官方 linter 的明确问题直接阻断自定义规则生成候选问题由白名单或人工确认消解。把启发式扫描包装成“百分之百发现无障碍缺陷”只会让团队很快失去对工具的信任。Demo 的目标页面叫CheckoutAuditPage。它包含订单总价、优惠说明、扫码图标和提交按钮。默认字号下没有明显错位字体放大到 1.75 倍后优惠说明被固定高度截断扫码图标在屏幕朗读中没有名称。这个场景比扫描全工程更适合解释规则因为每条提示都能落到一个可见组件。图片和正文统一使用以下字段扫描 IDAUDIT-0614候选版本RC-20261001-06文件数 12问题数 7高优先级 3中优先级 4问题定位CheckoutRow.ets:42规则号A11Y-IMG-001。时间统一为14:26。这些字段先被冻结再用于日志、页面和配图避免“文章一套、截图另一套”。二、官方规则与项目规则不要混在一个名字里这段配置解决什么问题启用官方 Code Linter 字号规则并让报告能区分官方规则与项目自定义扫描。// code-linter.json5 { ruleSet: [ plugin:cross-device-app-dev/recommended ], rules: { cross-device-app-dev/font-size: error }, files: [entry/src/main/ets/**/*.ets], ignore: [**/oh_modules/**, **/build/**] }官方文档给出的正确示例包括fontSize(12)和fontSize(12fp)错误示例是小于 8fp 的值。AccessGate 不复制官方规则的实现也不再次发明一个FONT-001。重复实现会导致一个地方升级、另一个地方仍按旧逻辑判断最后报告出现相互冲突的结论。项目规则使用独立前缀LAYOUT-TEXT-001表示固定高度文本候选A11Y-IMG-001表示可点击图片缺少可访问名称。报告里还记录source: official-linter | project-heuristic。评审者看到 source 就知道问题的权重和处理方式不会把“建议检查”误读成“官方明确禁止”。忽略目录也必须明确。扫描构建产物和oh_modules不仅浪费时间还会把三方代码的问题算到应用头上。相反业务封装组件不能因为“不是页面文件”而排除很多语义缺口恰好藏在团队自己的 IconButton、PriceRow 和空状态组件里。三、启发式扫描要输出证据不输出判决这段代码解决什么问题扫描 ETS 源文件生成带文件、行号、规则号与证据片段的候选问题。importfsfromnode:fsimportpathfromnode:pathinterfaceFinding{rule:LAYOUT-TEXT-001|A11Y-IMG-001severity:high|mediumfile:stringline:numberevidence:string}functionlineOf(source:string,offset:number):number{returnsource.slice(0,offset).split(\n).length}exportfunctioninspectEts(file:string):Finding[]{constsourcefs.readFileSync(file,utf8)constout:Finding[][]for(constmatchofsource.matchAll(/Text\([^)]*\)[\s\S]{0,180}?\.height\((\d)\)/g)){out.push({rule:LAYOUT-TEXT-001,severity:medium,file,line:lineOf(source,match.index??0),evidence:match[0].slice(0,120)})}for(constmatchofsource.matchAll(/Image\([^)]*\)[\s\S]{0,240}?\.onClick\(/g)){if(!match[0].includes(.accessibilityText()){out.push({rule:A11Y-IMG-001,severity:high,file,line:lineOf(source,match.index??0),evidence:match[0].slice(0,120)})}}returnout}这段正则刻意保持简单因此必须承认边界。链式调用跨越超过窗口长度时可能漏报父组件设置accessibilityGroup(true)并提供统一语义时可能误报自定义组件包装后的点击行为也未必能被Image...onClick捕获。正式工程可以用 ArkTS AST 提高精度但即便换成 AST也无法只靠语法树判断实际朗读顺序是否合理。为什么仍然值得保留简单扫描因为它能快速拦住最机械的回归而且输出证据片段便于人工判断。工具不直接说“审核失败”而是说“这里出现了可点击 Image240 个字符内未见 accessibilityText请确认语义来自何处”。语气上的克制对应技术上的真实边界。行号通过偏移量计算能让报告定位到CheckoutRow.ets:42。若扫描前还会格式化源码必须保证扫描的是提交后的版本否则 PR 页面上的行号会漂移。报告还应写入源码提交 SHA示例为了阅读简化只保留候选版本号。上图是开发环境风格的演示配图不是实际 IDE 截屏。工程树、中间扫描代码、右侧AccessGate模拟器和底部日志都使用AUDIT-0614。日志明确写出12 files / 7 findings / BLOCKED红色箭头只指向规则命中和汇总结果。四、修布局时别用另一个硬编码盖住问题原页面把优惠说明放进 40vp 高的 Row。默认字体下两行文字刚好塞满字体放大后文字需要更多垂直空间父布局仍只有 40vp于是底部被裁。最常见的错误修法是把高度改成 56 或 64今天的中文好了明天换成长文案或更大字号又会复发。这段代码解决什么问题让文本使用fp跟随字体设置以最小高度而非固定高度约束行并为可点击图片补充可访问名称。Componentstruct CheckoutRow{PropdiscountText:stringbuild(){Row({space:12}){Image($r(app.media.scan)).width(32).height(32).accessibilityLevel(yes).accessibilityText(扫描优惠码).onClick(()this.openScanner())Column({space:4}){Text(可用优惠).fontSize(14fp).fontWeight(FontWeight.Medium)Text(this.discountText).fontSize(16fp).maxLines(0)}.layoutWeight(1).alignItems(HorizontalAlign.Start)}.constraintSize({minHeight:48}).padding({top:8,bottom:8,left:16,right:16}).alignItems(VerticalAlign.Center)}privateopenScanner():void{}}fp让字号参与系统字体缩放constraintSize({ minHeight: 48 })只规定最低触控与视觉空间内容变高时容器仍可扩展上下 padding 保证两行或三行文字不会贴边。layoutWeight(1)让文本列拿到剩余宽度避免图片和文字共同挤压父容器。maxLines(0)的可用性应按目标 SDK 和组件文档核对如果项目规范不允许无限行可以设定合理上限并提供完整内容入口。关键不在某个魔法数字而在于明确产品接受的最大行数并在目标语言、目标字号和目标窗口宽度下验证。无障碍属性也不能机械堆叠。官方 ArkUI 通用属性支持accessibilityText、accessibilityLevel、accessibilityDescription、accessibilityGroup等。示例里的图片本身承担“打开扫码”的按钮语义因此给它名称“扫描优惠码”。若图标只是装饰应从可访问焦点中移除而不是朗读“蓝色二维码图标”。语义描述的是动作或信息不是像素外观。对于“图标 文字”共同组成的入口可以在父 Row 上分组并提供统一名称子节点避免重复朗读。AccessGate 当前规则只会看局部链因此这种合理用法需要注释白名单例如// access-gate: parent-labelled。白名单必须要求理由不能只写ignore否则半年后没人知道当时是误报还是临时绕过。运行页展示第一次扫描AUDIT-0614、RC-20261001-06、12 个文件、7 条提示、状态BLOCKED其中高优先级 3 条、中优先级 4 条。这里的 BLOCKED 代表团队门禁拒绝发布候选不代表应用市场已给出审核结论。五、门禁需要稳定的退出码和可追踪报告只在控制台打印七行警告还不够。CI 是否阻断必须由退出码决定报告必须能被后续页面读取。AccessGate 采用一个简单策略官方 linter error 或高优先级项目规则出现时退出 2只有中优先级提示时退出 1由流水线决定是否阻断没有问题退出 0。这段代码解决什么问题合并官方与项目扫描结果生成固定结构的 JSON并让流水线获得确定的退出码。interfaceAuditReport{auditId:stringcandidate:stringscannedFiles:numberhigh:numbermedium:numberstatus:PASS|BLOCKEDfindings:Finding[]}functionfinish(files:string[],findings:Finding[]):never{consthighfindings.filter(itemitem.severityhigh).lengthconstmediumfindings.length-highconstreport:AuditReport{auditId:AUDIT-0614,candidate:RC-20261001-06,scannedFiles:files.length,high,medium,status:high0?BLOCKED:PASS,findings}fs.writeFileSync(build/reports/access-gate.json,JSON.stringify(report,null,2))console.info([AccessGate]${files.length}files /${findings.length}findings /${report.status})process.exit(high0?2:medium0?1:0)}真实脚本不必强行声明never也可以设置process.exitCode让异步输出完成后自然退出。重点是不要捕获异常后仍返回 0。扫描器自身崩溃与扫描结果无问题是两回事前者应该使用单独的退出码并标记TOOL_ERROR不能生成一个看似 PASS 的空报告。报告中保留完整 findings但手机诊断页只展示摘要和少量重点。发布负责人需要快速判断是否可继续规则维护者才需要证据片段。把整段源代码塞进手机页面会让真正重要的文件、行号和修复建议淹没。详情页对应规则A11Y-IMG-001定位CheckoutRow.ets:42同时展示修复前7与修复后0的对照。红圈围住缺失语义的扫描图标箭头指向“补充 accessibilityText扫描优惠码”。它承担技术解释而不是再做一张漂亮的概览。六、7 变成 0 之前还要做人工验证修完静态提示后AccessGate 对同一候选分支重新扫描结果从 7 变成 0。这只表示规则集不再命中不表示页面已经完成无障碍验收。接下来至少要做三件事把系统字体调到目标档位检查文字是否完整开启屏幕朗读确认焦点顺序、名称和动作是否自然在窄窗口、横屏或多形态设备上检查布局是否仍可达。屏幕朗读验证尤其不能靠截图替代。截图能看到文字没有裁切却听不到焦点是否重复、提示是否冗长也看不出一个装饰图是否错误地抢到焦点。自动化可以记录组件树或辅助属性但最终仍要由人按实际任务走一遍“查看优惠—扫描优惠码—提交订单”。大字体验证也不是把所有文本无限放大。标题、金额、按钮和辅助说明有不同信息层级部分组件还有自身的最大缩放限制。工程要遵循对应组件文档并为关键操作保证可见、可滚动或可展开。为了守住一屏而截断关键条款是把布局指标放在用户任务之前。建议把门禁规则的维护责任写清楚。官方 linter 随工具链升级而变化应在升级记录中核对规则差异项目启发式规则则要统计误报率连续被白名单的模式应该改进规则而不是继续增加例外。报告里保留ruleVersion能帮助解释“同一提交为何本周和上周结果不同”。最后再强调一次发布语义BLOCKED是团队对候选版本的决定不是代替官方审核PASS是通过当前规则和人工清单不是对所有设备、语言和辅助能力作永久保证。把结论说窄反而更利于持续改进。这套流程的价值不在于七条提示本身而在于把模糊的“发布前看看有没有截断”变成可追踪路径官方规则负责明确底线项目扫描负责捕捉高频气味证据报告负责定位运行页负责复核人工任务负责确认真实体验。检查器不扮演审核员才能长期成为工程团队可信的门卫。七、让门禁经得住版本迭代而不是只服务一次发布AccessGate 第一次接入时最容易犯的错误是把当前页面的写法直接硬编码成规则。今天是CheckoutRow脚本就搜索这个组件名明天设计系统把它改成PromotionCell报告立刻归零看起来像质量提升其实只是规则失明。可维护的规则应该描述行为特征例如“可点击的图片是否有可访问名称”而不是记住某个页面名字。规则样例需要和脚本一起提交。每条规则至少准备一个应命中的最小代码、一个不应命中的最小代码以及一个白名单场景。修改正则、AST 查询或严重级别时先跑这些样例避免修复误报的同时把真正问题放过。对于本文的A11Y-IMG-001父容器已分组并提供统一文本就是必须保留的反例。报告基线也不应简单采用“旧问题可以一直存在”。比较合理的策略是存量问题进入带负责人和到期日的债务清单新问题一律阻断到期未处理的存量问题重新升级。否则团队为了让第一次接入成功会把七条提示全部加入永久白名单门禁从第一天起就只剩仪式感。严重级别要看用户任务而不是看代码长相。结算按钮没有朗读名称会直接阻断关键流程应是高优先级一个装饰图被多读一次也会影响体验但可以在不阻塞紧急修复的情况下安排处理中优先级。固定高度文本若承载价格、风险提示或协议应提升等级普通次要说明则可先人工确认。规则默认级别只是起点报告应允许根据组件语义上调。多语言会把大字体问题进一步放大。同一含义在不同语言中的长度差异很大中文默认文案通过并不代表英文、德文或系统回退文本也通过。静态扫描可以发现固定高度却不知道运行时资源最终取到哪一条。发布矩阵至少要抽取长文案语言和回退场景检查资源键缺失时页面是否展示占位符或退回默认语言。这里与资源完整性门禁有关但本文不重复另一类多语言扫描只强调布局验证必须使用真实资源。窗口宽度同样影响结论。手机全屏、折叠态窄窗口、平板分栏和自由窗口会给同一段文字不同的换行结果。AccessGate 的手机演示只是一个采样点不能代表全部形态。工程可以维护一组关键宽度截图测试把字体档位和窗口宽度做成组合但组合数量要克制优先覆盖最窄内容区、常用宽度和最大字体而不是穷举所有像素。自动截图比较也有边界。像素差可以发现裁切或错位却会受到字体渲染、系统主题和动画时刻影响。截图任务应关闭不相关动画、固定测试数据并等待布局稳定差异出现后由人判断不直接把任何像素变化都当缺陷。无障碍朗读顺序更不能用像素差验证需要读取可访问节点或进行人工导航。对于自定义组件库最好把语义要求下沉到组件本身。团队如果提供统一的ActionIcon构造参数应强制传入可访问名称业务页面就不必每次记忆.accessibilityText()。门禁发现同类问题超过一定次数时优先修设计系统接口而不是继续给业务开发发七张修复单。工具最有价值的输出往往不是某一行红字而是揭示哪个公共抽象缺少约束。代码评审中的展示方式也会影响修复效率。机器人评论应聚合到一条可折叠摘要包含规则号、文件行号、证据和文档链接不要为七条提示发送七条独立通知。相同规则在同一文件出现多次时可以分组但不能只显示“共三处”而不提供位置。开发者从报告跳到代码、理解原因、完成修复的路径越短门禁越不容易被绕过。当工具自身升级时先以观察模式运行一个周期。新规则只报告不阻断收集误报和执行时长规则稳定后再切换为阻断。直接在发布当天把新启发式设成 error容易把工具问题变成发布事故。官方 Code Linter 的升级也应走相同流程特别是规则默认值或解析器版本变化时需要保存前后报告差异。执行性能不能无限增长。AccessGate 应只扫描变更文件和受影响的公共组件并在夜间或发布候选阶段做全量扫描。缓存键至少包含文件摘要、规则版本和解析器版本只按文件路径缓存会在内容变化后错误复用旧结果。Demo 扫描 12 个文件没有性能压力但真实工程可能有数千文件门禁若每次占用十几分钟团队迟早会寻找跳过开关。跳过开关本身需要审计。允许紧急修复绕过中优先级提示并不等于允许静默跳过全部检查。流水线应记录申请人、原因、关联缺陷和到期时间高优先级问题的绕过需要更高权限。页面上的 PASS 也要区分“零问题”和“经批准例外”否则管理者看到绿色状态会误判风险已经消失。最终验收可以用一张很朴素的清单收尾默认与最大目标字体下关键文本完整屏幕朗读能够按任务顺序访问主要操作所有图标按钮有动作语义焦点不重复、不陷入循环窄窗口仍能滚动到提交按钮扫描报告与当前提交一致白名单均有理由和期限。清单不追求覆盖所有辅助能力而是确保当前发布至少对已识别的风险给出证据。这样维护几轮之后7 变成 0 才真正有意义。它不是一次修图式的清零而是规则、组件、报告、人工验证和例外管理共同收敛的结果。下一个页面加入时开发者会在写 IconButton 时主动思考名称在写高度时先问内容是否会增长门禁从最后一道闸门逐渐变成编码阶段的反馈。这才是发布前检查最值得保留的部分。参考资料华为开发者文档Code Lintercross-device-app-dev/font-size华为开发者文档ArkUI 无障碍属性华为开发者文档ArkUI 文本组件华为开发者社区应用上架审核相关指南具体要求以当前官方发布规范为准