我在整理一套面向鸿蒙开发课堂的日志展示页时,发现了一个很不起眼的矛盾:开发者需要从一堆输出里快速找到状态码,但如果把原始日志直接塞给文本组件,截图、投屏和课堂演示就可能把访问令牌一起带出去。更麻烦的是,日志里混入表情符号或某些扩展汉字时,直接拿字符串下标拆成多个 Span,稍不留意就会把一个代理对从中间切断。

这次的小工具叫 SafeTrace,页面是 LogPreviewPage.ets,示例任务 ST-1631。准备了六条虚构日志,只有一处 token=ABCD1234 和一处待高亮的 403。页面要展示“已脱敏、敏感字段 1、命中 1”;这不是设备上读取到的 HiLog,也没有做真实安全测试。我要解决的不是如何制造漂亮颜色,而是怎样规定处理顺序,让脱敏信息和高亮位置信息不会相互打架。

一、先把原始日志挡在展示层外面

开发工具在调试环境里记录具体值很方便,到了教学素材、缺陷工单和远程协作场景,原始内容却不该成为页面的默认输入。常见错误是先找出要标红的 403,再对整段字符串做替换。替换后长度可能变化,之前保存的起止位置便不再指向原来的字符。也有人只遮挡界面上的一部分文字,复制功能仍从未经清理的原字符串取值,这同样达不到展示隔离的目标。

我把这条链拆成四层:原始输入 → 字段掩码 → 关键词分段 → ArkUI 渲染。只有掩码后的副本可以进入组件状态;显示、复制和导出也都应该使用这一份副本。样例里的 token 是教学用的模拟字段,真正的生产环境还必须按日志来源配置凭据、手机号和用户标识等策略,不能把这段示例理解成完整的脱敏规则库。

第一段代码解决的是字段脱敏顺序,不做 UI,也不使用任何系统日志读取权限。注意,下面返回的是新字符串,调用方不能再把原始日志留在可观察的状态对象中。

function sanitizeLog(line: string): string {
  // 示例只覆盖 token=... 这种明确的键值字段
  return line.replace(/token=[^\s]+/g, 'token=****')
}

const input = '02:49 🔐 upload failed token=ABCD1234 code=403'
const visible = sanitizeLog(input)
// 02:49 🔐 upload failed token=**** code=403

这一层故意做得很窄:规则必须明确,宁可提示“存在未识别格式”,也不把一串随意的星号称为全面防护。应用重进页面时应重新从受控来源构造已脱敏数据,而不是在 aboutToAppear 中重复拼接原始日志。异常输入、超长行和换行分隔符还应在上游设定限制。这里展示的 visible 才允许送往后续分词与截图环节。

二、字符串长度不是用户眼睛看到的字符数

ArkTS 的字符串行为继承了 JavaScript 的 UTF-16 特点,length 和 slice 依据代码单元,而不是肉眼可见的字符。对基本拉丁字母没有太大影响,一旦遇到“🔐”这样的非 BMP 字符,一个符号可能占两个代码单元。倘若外部匹配器给出的偏移来自代码点数组,而 UI 又用 slice 按代码单元截取,就容易在同一位置出现错位,轻则颜色不对,重则字符显示为替代方框。

在 SafeTrace 中,关键词固定是 403,前面刻意放了锁形表情符号。为了使逻辑不被某一类输入绑住,分段算法直接按代码点数组搜索,找到完整片段后再 join 回字符串。它避免在代理对中间制造切片;但不能声称它自动理解所有组合字形,例如带变音符的字母和多个连接符构成的表情仍可能需要更细的字素簇算法。

第二段代码负责在脱敏结果上生成展示片段。marked 只是渲染标记,不等于日志安全分类;命中词为空时直接返回普通段落,避免无限循环。

interface TraceRun {
  text: string
  marked: boolean
}

function toRuns(value: string, keyword: string): TraceRun[] {
  const chars: string[] = Array.from(value)
  const target: string[] = Array.from(keyword)
  if (target.length === 0) return [{ text: value, marked: false }]
  const runs: TraceRun[] = []
  let start = 0
  let i = 0
  while (i <= chars.length - target.length) {
    const same = target.every((c: string, j: number) => chars[i + j] === c)
    if (!same) { i++; continue }
    if (i > start) runs.push({ text: chars.slice(start, i).join(''), marked: false })
    runs.push({ text: target.join(''), marked: true })
    i += target.length
    start = i
  }
  if (start < chars.length) runs.push({ text: chars.slice(start).join(''), marked: false })
  return runs
}

这里没有使用正则返回的偏移量,因此不会发生“正则的代码单元索引直接当成代码点索引”这种混用。实际产品还要约束最大行长、关键词长度和同时存在的命中数量,防止极端输入把一次渲染拆成大量细碎节点。六行示例足够演示关系,但长日志最好先分页或虚拟化,别把一个超大字符串一次性拆成几千个 Span。

三、Text 只负责呈现,不承担权限判断

ArkUI 的 Text 可以容纳 Span,不同片段具有独立的文字样式。这个能力解决的是显示层的局部强调,并不负责帮应用识别令牌或验证日志是否允许外传。我在 LogPreviewPage.ets 里只保留 runs、status 和任务号;展示用的一条样本来自预先脱敏的常量。页面状态与安全判定应分离,不因为黄色高亮就认定某条日志没有敏感信息。

第三段代码展示最小的构建路径:先得到已脱敏副本,再形成可渲染片段。为便于复核,任务 ID 和状态文案直接写在页面上。以下是页面逻辑片段,非已编译的完整工程。

@Entry
@Component
struct LogPreviewPage {
  @State runs: TraceRun[] = []
  @State status: string = '已脱敏'
  private readonly taskId: string = 'ST-1631'

  aboutToAppear(): void {
    const clean = sanitizeLog('02:49 🔐 upload failed token=ABCD1234 code=403')
    this.runs = toRuns(clean, '403')
    console.info(`[SafeTrace] ${this.taskId} sanitized=1 hits=1 status=READY`)
  }

  build() {
    Column({ space: 16 }) {
      Text(`SafeTrace · ${this.taskId}`).fontSize(22)
      Text('已脱敏  |  敏感字段 1  |  命中 1').fontSize(14)
      Text() {
        ForEach(this.runs, (run: TraceRun, index: number) => {
          Span(run.text).fontColor(run.marked ? Color.Orange : Color.Black)
        }, (_run: TraceRun, index: number) => `${index}`)
      }.fontSize(17)
      Text('示例数据 · 非真机实测').fontSize(12).fontColor(Color.Grey)
    }.padding(20).width('100%')
  }
}

@State 驱动的是显示副本的更新,不应保存原始 token;如果将来接入异步日志文件读取,仍要用任务代次拦截页面离开后晚到的结果,读完及时关闭句柄。当前代码没有后台订阅,也就不存在需要释放的系统监听器。console.info 这一行只是希望开发者对照的示例输出格式,配图底部的 HiLog 也是按同一格式绘制的示意日志,绝不表示真机已经运行。

四、一次展示修改,如何做可重复的验收

验收这种功能不能只看颜色。我会先拿原始行和脱敏行做严格比对,确认 ABCD1234 在展示副本中完全消失,而 403 仍只命中一次;再添加一条包含两个 token 字段的模拟文本,确认替换规则是全局的。第三步把关键词改为空值,组件应该只得到一个普通片段。第四步使用“🔐403”和其他非 BMP 字符,确认 Array.from 处理过程中没有产生孤立代理项。最后故意输入一条极长日志,核对是否触发业务层的截断策略而不是长时间卡住界面。

UI 侧还要检查复制行为。用户长按复制的是 Text 已经显示出来的安全文本,还是业务代码另行读取的原始字符串?如果产品自带“复制整行”按钮,应当直接复制 sanitizeLog 的结果。对搜索命中、导出 JSON 和日志分享等功能,也要沿用同一份受控副本。只在绘制阶段做遮盖会留下路径分叉,后面很难追查。更重要的是,不能因为业务层做了一次字符串替换,就直接在正式工单上传敏感日志:脱敏策略需要覆盖真正的数据模型和行业要求。

画面检查也有一份非常简单的约定:Demo 名为 SafeTrace、页面为 LogPreviewPage、任务号固定 ST-1631,六行样本、一个敏感字段、一次 403 命中,状态“已脱敏”。IDE 画面应展示代码与示意 HiLog,手机画面只展现最终安全副本。这个约定比漂亮的截图更重要,因为它使审稿人可以直接在文字、代码和图中对照同一组数据,而不必猜哪里是素材模板的遗留状态。

五、适用范围与工程取舍

Text + Span 的强项是低成本的局部富文本展示。它不负责日志采集、不保证文本免泄漏,也不提供跨进程审计;UTF-16 代码点处理解决的只是代理对切割,不能代替完整的 Unicode 文本安全规范。如果将来要做复杂的交互选择、行级注释、语法高亮以及几万行滚动,那就应该重新评估 RichEditor、属性字符串或分页列表,而不是不断给这套演示塞条件判断。

对教学项目而言,最值得保留的是“先脱敏再高亮”的依赖顺序,以及处理顺序一旦改变,所有下游坐标都要重新计算的判断。当前案例只给出静态样本和界面方案,没有运行 DevEco、没有读取真实 HiLog,也没有做任何设备性能结论。官方参考可查看华为 ArkUI《图文混排》中 Text / Span 的使用方式:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-text-image-layout 。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐