Rspeedy logo
Rspeedy

反解 UI 节点树

运行时一个 Lynx 页面就是一棵 UI 节点树,也就是引擎真正渲染出来的那些元素。UI source map 让你把树里的每个节点映射回创建它的 JSX(reposourcelinecolumn)。

它分三步:

  1. 构建:打开 enableUiSourceMap,把 UI source map 写进 debug-metadata.json,并给元素打上 nodeIndex
  2. 导出:让 Lynx 客户端把运行时 UI 树序列化成 JSON。
  3. 反解:把这份 JSON 加上 debug-metadata.json 喂给 remapUiTree,它会给每个节点标注源码位置。

1. 开启开关

UI source map 生成默认是关的。在 pluginReactLynx 里打开:

lynx.config.ts
import { defineConfig } from '@lynx-js/rspeedy';
import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin';

export default defineConfig({
  plugins: [
    pluginReactLynx({
      enableUiSourceMap: true,
    }),
  ],
});

打开后,主线程的 transform 会给每个创建出来的元素打上 nodeIndex,并记录一张 nodeIndex → (source, line, column) 表。这张表会作为 debug-metadata.jsonuiSourceMap 字段产出:

interface UiSourceMapData {
  version: 1;
  sources: string[]; // 源文件
  mappings: [number, number, number][]; // [sourceIndex, line, column]
  uiMaps: number[]; // uiMaps[i] 是一个 nodeIndex;mappings[i] 是它的位置
}

要让运行时指向这个文件,模板里还得带上它的 debugMetadataUrl:连到 dev server 时会自动写入,生产环境则要你自己注入(见 线上错误反解 → 方案 2)。否则导出的节点 debugMetadataUrl 为空,没法反解。

2. 从客户端导出 UI 树

三端用法一致:从 LynxView 异步拿到根 LynxElement,再对它调 toJSONString 把整棵树序列化出来。两个 callback 都会回调到 UI 线程(主线程)。

iOS (Objective-C)
Android (Java)
Harmony (ArkTS)
#import <Lynx/LynxView.h>
#import <Lynx/LynxElement.h>

[lynxView getLynxElementRoot:^(LynxElement *_Nullable root) {
  if (root == nil) return;
  [root toJSONString:^(NSString *_Nullable json) {
    if (json == nil) return;
    NSLog(@"LynxElement tree json: %@", json);
  }];
}];

序列化出来的每个节点长这样,反解需要的 nodeIndexdebugMetadataUrl 这两个字段嵌在 debugInfo 里:

{
  "tag": "view",
  "nodeId": 11,
  "position": { "x": 0, "y": 0, "width": 300, "height": 80 },
  "events": [],
  "debugInfo": {
    "nodeIndex": 100,
    "debugMetadataUrl": "https://your-cdn.example.com/dm/<key>/debug-metadata.json",
  },
  "children": [
    { "tag": "text", "debugInfo": { "nodeIndex": 200 }, "children": [] },
  ],
}

nodeIndex 是第 1 步里给的编译期标识;debugMetadataUrl 是写进模板的那个 URL。没有 debugInfo.nodeIndex 的节点(比如纯文本)不会被反解。

3. 反解这棵树

@lynx-js/debug-metadataremapUiTree 读取的是每个节点顶层的 nodeIndexdebugMetadataUrl,而引擎把它们嵌在 debugInfo 里,所以先把它们提上来再反解。它的第二个参数是一个由你提供的 loader,负责把一个 debugMetadataUrl 取成对应的 DebugMetadataAsset

import { remapUiTree } from '@lynx-js/debug-metadata';

// 把引擎嵌套的 `debugInfo` 字段提到顶层
function flatten(node) {
  const { debugInfo, children, ...rest } = node;
  return {
    ...rest,
    nodeIndex: debugInfo?.nodeIndex,
    debugMetadataUrl: debugInfo?.debugMetadataUrl,
    children: children?.map(flatten),
  };
}

const tree = JSON.parse(jsonFromClient);
const remapped = await remapUiTree(flatten(tree), async (debugMetadataUrl) => {
  const res = await fetch(debugMetadataUrl);
  return res.json(); // 一个 DebugMetadataAsset
});

凡是 nodeIndex 能在对应 debugMetadataUrluiSourceMap 里命中的节点,都会拿到 repo / source / line / column;命中不了的节点,连同节点上原有的其它字段,都原样保留。remapUiTree 对每个不同的 debugMetadataUrl 只调用一次 loader。

npx @lynx-js/debug-metadata remap --ui <tree.json> CLI 做的是同样的事,适用于节点已经在顶层带有 nodeIndex / debugMetadataUrl 的树。更底层的 buildUiSourceMapLookupnodeIndex → 位置)和 normalizeRepo(git remote → owner/repo)辅助函数也都有导出。

除非另有说明,本项目采用知识共享署名 4.0 国际许可协议进行许可,代码示例采用 Apache License 2.0 许可协议进行许可。