平行视界适配最容易被低估的,不是“把列表和详情放到左右两边”,而是配置何时真正有效。项目里常见的失败并不发生在 ArkUI 编译阶段:JSON 能被读取,页面也能单独打开,直到折叠屏展开后才发现某条路由被更宽的通配规则遮住,或者同一对页面被声明了两次。更麻烦的是,配置文件已经修改,设备里运行的却仍是上一次安装包中的内容。

本文把问题收缩成一个可自动检查的 Demo:ParallelRuleGuard。它不假装替代系统能力,也不声称一次静态扫描就能证明适配完成;它只做三件可以被工程化的事:验证配置形状、找出规则之间的语义冲突、给待安装产物计算指纹。演示任务固定为 EG-1024-217,报告 ID 为 easygo_audit_217,检查 14 条规则,11 条通过、3 条阻断,页面进度显示 79%,最终状态为 BLOCKED。

一、先把“配置正确”拆成三个问题

平行视界是大屏与折叠屏上的系统级多页呈现能力。当前官方资料把它描述为推送、覆盖等典型模式,并强调左右区域比例与多窗口体验;真正落到工程里,配置字段与版本要求仍应以项目对应的最新官方最佳实践为准。这里不把旧格式写成永远不变的标准,而是把校验器做成“策略适配器”:Schema 负责本项目当前认可的字段集合,语义扫描器负责项目约束,安装核验负责确认待测产物没有串版本。

三个问题不能混在一起。

第一层是语法与结构:JSON 是否可解析,必要字段是否存在,枚举值是否落在允许范围,数组成员是否重复。它适合交给 JSON Schema 与 Ajv。

第二层是业务语义:两条分别合法的规则组合后,是否产生重复页面对、通配遮蔽、不可达分支或入口模块位置错误。Schema 看不到“先后顺序造成的覆盖”,必须另写确定性的扫描规则。

第三层是生效证据:仓库里的 easygo.json、构建目录里的副本与设备上已安装的 HAP 是否来自同一次构建。静态扫描通过只说明输入合理,不说明模拟器加载了这份输入。

把三层结果压成一个绿色对勾,会让定位成本上升。ParallelRuleGuard 因此保留 SCHEMA_FAILED、CONFLICT_FOUND、ARTIFACT_STALE、READY_FOR_DEVICE 四个中间状态;本次演示在第二层停下,界面显示 BLOCKED,不会提前点亮“可上机”。

二、Schema 只守住形状,不越界替系统下结论

Demo 的配置适配器采用一个最小内部模型:client、easyGoVersion、mode 与 abilityPairs。演示值分别为 com.example.catalog、1.0、navigation 和 14 条页面对。这个内部模型用于扫描与可视化,不表示所有 HarmonyOS 版本都必须使用同一字段;升级 SDK 或官方格式时,应先更新适配器与样例,再更新 Schema。

下面这段代码解决“输入形状不可控”的问题。strict: true 会让未知或含糊的 Schema 写法尽早暴露;allErrors: true 用于一次收集全部问题,避免修一处再跑一次。Ajv 的校验错误会在后续校验时被覆盖,所以代码立即复制错误数组,而不是把可变引用传到 UI。

import Ajv, { ErrorObject, JSONSchemaType } from 'ajv'

interface PairRule {
  id: string
  master: string
  detail: string
  order: number
}

interface EasyGoPolicy {
  client: string
  easyGoVersion: string
  mode: 'navigation' | 'cover'
  abilityPairs: PairRule[]
}

const policySchema: JSONSchemaType<EasyGoPolicy> = {
  type: 'object',
  additionalProperties: false,
  required: ['client', 'easyGoVersion', 'mode', 'abilityPairs'],
  properties: {
    client: { type: 'string', minLength: 3 },
    easyGoVersion: { type: 'string', pattern: '^\\d+\\.\\d+$' },
    mode: { type: 'string', enum: ['navigation', 'cover'] },
    abilityPairs: {
      type: 'array', minItems: 1,
      items: {
        type: 'object', additionalProperties: false,
        required: ['id', 'master', 'detail', 'order'],
        properties: {
          id: { type: 'string', minLength: 1 },
          master: { type: 'string', minLength: 1 },
          detail: { type: 'string', minLength: 1 },
          order: { type: 'integer', minimum: 0 }
        }
      }
    }
  }
}

const ajv = new Ajv({ strict: true, allErrors: true })
const validate = ajv.compile(policySchema)

export function validateShape(input: unknown): ErrorObject[] {
  if (validate(input)) return []
  return [...(validate.errors ?? [])]
}

这里故意没有在 Schema 里塞入设备宽度、折叠状态或系统窗口行为。那些条件属于运行期与设备能力,静态 JSON 只能验证声明,不能替代真实设备的折叠、旋转、返回栈和多窗口测试。把能力边界写清楚,比增加一个看似聪明的正则更重要。

三、三类冲突要给出可行动的位置

Schema 通过后,scanSemanticConflicts() 按 order 排序,生成规范化的 master -> detail 键。它检查三个项目约束:完全相同的页面对只能出现一次;通配 master 不能排在同前缀的具体规则之前;配置必须位于 entry 模块约定目录。第三项来自历史官方指导中“配置放在 entry 模块”的约束,但在真实项目中仍需与当前工具链文档核对。

本次输入刻意保留三处问题,便于检验扫描器能否给出稳定结果:第 38 行出现重复页面对,代码 E_DUP_PAIR;第 52 行通配规则遮住具体路由,代码 E_WILDCARD_SHADOW;feature/news/src/main/resources/rawfile/easygo.json 被放入 feature 模块,代码 E_ENTRY_ONLY。结果必须是 14 条中 11 条通过、3 条阻断,而不是模糊的“部分成功”。

下面代码解决“单条合法、组合冲突”的问题。originLine 来自解析阶段的 source map;示例只展示核心逻辑,生产工具应保留 JSON 指针与文件位置,避免用数组下标猜行号。

type ConflictCode = 'E_DUP_PAIR' | 'E_WILDCARD_SHADOW' | 'E_ENTRY_ONLY'
interface LocatedRule extends PairRule { originLine: number }
interface Conflict { code: ConflictCode; line: number; message: string }

function prefixOf(pattern: string): string {
  return pattern.endsWith('*') ? pattern.slice(0, -1) : pattern
}

export function scanSemanticConflicts(
  rules: LocatedRule[], configPath: string
): Conflict[] {
  const issues: Conflict[] = []
  const seen = new Map<string, LocatedRule>()
  const ordered = [...rules].sort((a, b) => a.order - b.order)

  for (const rule of ordered) {
    const key = `${rule.master.trim()}=>${rule.detail.trim()}`
    if (seen.has(key)) {
      issues.push({ code: 'E_DUP_PAIR', line: rule.originLine,
        message: `页面对重复:${key}` })
    } else {
      seen.set(key, rule)
    }
  }

  for (let i = 0; i < ordered.length; i++) {
    const broad = ordered[i]
    if (!broad.master.endsWith('*')) continue
    for (const specific of ordered.slice(i + 1)) {
      if (specific.master.startsWith(prefixOf(broad.master))) {
        issues.push({ code: 'E_WILDCARD_SHADOW', line: broad.originLine,
          message: `${broad.id} 提前遮蔽 ${specific.id}` })
        break
      }
    }
  }

  if (!configPath.includes('/entry/src/main/resources/rawfile/')) {
    issues.push({ code: 'E_ENTRY_ONLY', line: 1,
      message: `配置位置不在 entry:${configPath}` })
  }
  return issues
}

排序后扫描还有一个好处:相同输入永远得到相同错误顺序,CI 日志不会因为对象遍历顺序变化而抖动。去重键要在比较前统一空白,但不要擅自把路由转成小写,因为页面标识是否区分大小写应由实际协议决定。通配符也只支持团队明确允许的尾部 *,不把任意字符串解释成正则,以免审计器自身成为另一套难以预测的路由引擎。

项目中把扫描器放在 tools/easygo-audit,报告模型放在 entry/src/main/ets/model/AuditReport.ets,演示页面为 EasyGoAuditPage.ets。DevEco Studio 风格配图是根据上述数据生成的说明图,不是实际 IDE 截图,也不作为已跑通证明。

四、79% 不是动画值,而是可复算的分数

页面上的 79% 来自 11 / 14 四舍五入,不来自定时器。扫描完成前进度表示“已检查比例”,完成后则切换为“规则通过比例”;两种含义不能共用同一文案,否则用户会把 79% 误读为任务仍在执行。

ParallelRuleGuard 在 10:24 生成任务 EG-1024-217。摘要区显示客户端 com.example.catalog、模式 navigation、设备画像 foldable-expanded、报告 easygo_audit_217。主按钮在状态为 BLOCKED 时只允许“查看冲突”,不允许“安装并验证”。这不是为了显得严格,而是阻止带着已知配置冲突继续做设备验证,制造更多噪声。

竖版运行图同样是演示界面,不是系统真实截图。顶部状态栏固定为 10:24、5G、Wi‑Fi、信号与 84% 电量;这些数字与正文和详情页一致。红色细圈只标出 79% 和 BLOCKED,用于解释状态,不把整张页面画成批注稿。

五、安装生效要用产物指纹闭环

很多“规则明明改了却没有反应”的根因,是验证对象没有更新。仅比较源文件修改时间不可靠:复制、缓存恢复、增量构建都可能保留或重写时间戳。更稳妥的做法是在构建前对配置规范化后计算 SHA-256,把短指纹、任务 ID 和构建时间写入应用可读的审计资源;页面展示该指纹,HiLog 同时打印。测试人员只需核对扫描报告和设备页面是否一致。

下面代码解决“仓库内容与待测产物是否同源”的问题。它不读取设备私有目录,也不声称能证明系统已经消费配置;它只是给待安装 HAP 携带一个可观察的版本标记。真正的生效仍要在卸载旧版本、安装新产物后,按折叠、旋转、返回和多窗口用例验证。

import { createHash } from 'node:crypto'

interface AuditStamp {
  taskId: string
  reportId: string
  policySha256: string
  generatedAt: string
}

function canonicalJson(value: unknown): string {
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
  if (value && typeof value === 'object') {
    const body = Object.entries(value as Record<string, unknown>)
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`)
      .join(',')
    return `{${body}}`
  }
  return JSON.stringify(value)
}

export function makeStamp(policy: unknown): AuditStamp {
  const digest = createHash('sha256').update(canonicalJson(policy)).digest('hex')
  return {
    taskId: 'EG-1024-217',
    reportId: 'easygo_audit_217',
    policySha256: digest,
    generatedAt: new Date().toISOString()
  }
}

规范化时对对象键排序,但保留数组顺序,因为规则先后正是语义的一部分。指纹只用于一致性核对,不用于安全签名;若要防篡改,需要由可信构建环境签名并在应用侧验证,不能把普通哈希包装成安全能力。

六、详情页必须解释“为什么阻断”

点击“查看冲突”进入 ConflictDetailPage。页面不重复首页的环形进度,而是列出三条可行动信息:错误码、文件位置、遮蔽或重复关系。E_DUP_PAIR 指向第 38 行;E_WILDCARD_SHADOW 指向第 52 行,并说明通配规则先于具体规则;E_ENTRY_ONLY 显示实际路径 feature/news/src/main/resources/rawfile/easygo.json。底部日志显示 shape=PASS、semantic=FAIL(3)、artifact=SKIPPED,明确第三层尚未执行。

这个页面承担的是诊断,不是庆祝。状态仍为 BLOCKED,任务 ID 仍是 EG-1024-217,时间仍是 10:24,电量仍是 84%。03 与 04 的内容明显不同:前者回答“整体进展如何”,后者回答“哪三条规则阻断,以及后续阶段为何跳过”。

修复顺序也应确定。先移动配置到正确模块,再删除重复页面对,最后把具体规则调整到通配规则之前。原因是路径错误会让整份配置无效,重复规则会让结果不稳定,而优先级调整依赖最终保留的规则集合。修复后重新生成报告;如果 14 条全部通过,状态只能进入 READY_FOR_DEVICE,仍不能直接写成“适配完成”。

七、设备验证要覆盖状态转换,而不是只看展开瞬间

静态检查之后,至少要准备四组设备用例。第一组从折叠态启动列表,再展开并进入详情,观察左右页面与返回栈。第二组从展开态直接深链到详情,确认主区域的补位策略。第三组在左右页面已显示时旋转或改变窗口大小,确认没有重复创建详情。第四组在后台保留任务后重新前台,核对配置指纹、当前路由和实际布局。

这些用例的日志要围绕状态转换组织,而不是打印几十行对象。建议每次只保留 taskId、windowClass、masterRoute、detailRoute、transition 与 policyHash。出现问题时,先确认 policyHash 是否等于 easygo_audit_217 报告中的指纹,再讨论路由或窗口行为。对象不同源时,后面的推理都没有意义。

还要保留负向验证:去掉全部平行视界声明后,应用是否仍能以普通单页方式工作;配置读取失败时,是否降级而不是白屏;页面对指向不存在路由时,开发构建是否能提前报错。平行视界是增强体验,不应成为基础导航的单点故障。

八、边界、维护与可复用结论

这套工具的边界很明确。Ajv 只证明输入符合团队声明的 Schema;语义扫描器只证明没有命中已编码的冲突;产物指纹只证明两个可观察对象内容一致。它们都不能证明系统最终布局正确,也不能替代当前 HarmonyOS 版本的官方文档、DevEco Studio 检查与真机测试。

维护上,Schema、语义规则与设备用例应分别版本化。官方字段变化时,不要直接改旧 Schema 覆盖历史,而是新增适配版本并保留迁移测试。冲突规则新增时,为每个错误码加入最小正例和反例。设备矩阵变化时,只调整运行期用例,不把设备型号硬编码进静态规则。

ParallelRuleGuard 最有价值的结果不是“79%”这个数字,而是把一个模糊的适配失败拆成三种证据:输入是否合规、规则是否互相冲突、待测产物是否携带同一份配置。工程判断因此从“再装一次试试”变成可复算的流程。对于本次演示,结论也保持克制:14 条规则中 3 条必须先修复,当前状态是 BLOCKED;只有重新扫描通过并完成设备矩阵验证,才能讨论平行视界体验是否达到发布要求。

九、参考资料

  • 华为开发者联盟,HarmonyOS 7 平行视界相关说明与最佳实践入口:https://developer.huawei.com/consumer/cn/forum/topic/0201221235973021541
  • 华为开发者联盟,平行视界历史配置指导(用于理解 entry 配置与安装生效注意点,实际项目须以当前版本文档为准):https://developer.huawei.com/consumer/cn/doc/development/quickApp-Guides/quickapp-harmonyos-parallel-view
  • Ajv 官方文档,Strict mode:https://ajv.js.org/strict-mode.html
  • Ajv 官方文档,API 与错误对象:https://ajv.js.org/api.html
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐