Skip to content

refactor(source): @antv/l7-source 渐进式重构(阶段 0-6,happy path 零行为变化) - #2882

Merged
lzxue merged 67 commits into
masterfrom
refactor/source-progressive
Jul 23, 2026
Merged

refactor(source): @antv/l7-source 渐进式重构(阶段 0-6,happy path 零行为变化)#2882
lzxue merged 67 commits into
masterfrom
refactor/source-progressive

Conversation

@lzxue

@lzxue lzxue commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

概述

@antv/l7-source 包进行渐进式重构,阶段 0-6 全部落地。核心公开 happy path(new Source / setData 成功路径)字节级零行为变化;另有 4 处 explicitly-minor 的错误/边界路径 strictly-better 改进(详见下「行为变化清单」,均为修 bug / 改善错误 surface,非回归)。坚持渐进式纪律:每阶段独立切片(代码 + 测试 + 文档 + commit + SHA 回填 + post-commit 基线核验),可单独审查 / revert。

注:标题原称「零行为回归」措辞过于绝对,已修正为「happy path 零行为变化」——本次确有几处 strictly-better 行为改进(非退化、非回归),下方「行为变化清单」逐项列明。

完整路线图、进度记录、遗留问题已存档:

  • docs/refactoring/source/PLAN.md(路线图 + 现状诊断)
  • docs/refactoring/source/PROGRESS.md(倒序逐阶段记录 + commit SHA)
  • docs/refactoring/source/BACKLOG.md(遗留问题:位置/问题/建议/状态)

改动范围(阶段 0-6)

阶段 0 — 低风险清理(无行为变更)

  • 0.1 source/interface.ts 改为从 @antv/l7-core re-export,删重复定义;MapboxVectorTile/IRGBParseCfg 各定义一处
  • 0.2 修正拼写 excuteParser→executeParsercaculClusterExtent→calcClusterExtenttransFunction→transformFunction
  • 0.3 每个 transform 加严格 cfg interface(IFilterCfg/IJoinCfg/IGridCfg/...
  • 0.4 合并 parser/rasterRgb.tsparser/raster/rgb.ts 同名 function,统一为 parser/rgb.tsparser/ndi.ts
  • 0.5 testTile 移出生产 entry — wontfix(TileDebugLayer 合法默认 parser)
  • 0.6 重命名 src/source/src/tile-source/;两个 VectorSource 改名 MVTSource / GeoJSONVTTileSource

阶段 1 — 拆解 God Class(内部 delegate,对外透明)

  • 1.1 ClusterManager:封装 Supercluster + clusterOptions + updateClusterData + getClusters*
  • 1.2 TilesetAdapter:包 initTileset + 7 个 reload*/getTile* 方法
  • 1.3 FeatureIndex:getFeatureById/getFeatureId/updateFeaturePropertiesById
  • 1.4 Bounds value object:extent/center/setCenter/invalidExtent

阶段 2 — Parser 接口标准化 + 注册机制现代化

  • 2.1 定义统一 interface Parser<TData, TCfg, TResult extends IParserData>
  • 2.2 PARSERS/TRANSFORMS 全局 Map → ParserRegistry class;旧全局函数作 deprecation wrapper
  • 2.3 getParser(type) 未注册抛 ParserNotFoundError(替代旧 undefined 直接 call 的 TypeError)
  • 2.4 package.jsonsideEffects 白名单 ["./es/index.js"];副作用注册显式化为 registerBuiltins()
  • 2.5 工厂 createSource(data, cfg, registry?)

阶段 3 — Parser 与 Loader 解耦

  • 3.1 抽象 TileLoader 接口;getVectorTile 闭包抽成 MVTLoader/JsonTileLoader/GeoJSONVTLoader
  • 3.2 RasterTileLoader 分发器 + 4 个小 loader(IMAGE/ARRAYBUFFER/CustomImage/CustomRaster)
  • 3.3 image.tsImageLoader 类,parser 不再自 fetch
  • 3.4 getCustomData/getCustomImageData 统一到 CustomDataProvider

阶段 4 — 异步生命周期与状态

  • 4.1 Source.create(data, cfg): Promise<Source> async 工厂 + ready getter,消除 source.data race
  • 4.2 layers 消费方迁移到 await source.readyISourcereadonly ready: Promise<void> 契约
  • 4.3a dataVersion 版本号计数器(setData / updateFeaturePropertiesById bump)
  • 4.3b setData 失败 surfacing(swallow/hang → 'error' 事件)
  • 4.4 删 clusterOptions.enable 死字段;合并 cluster 两条路径

阶段 5 — 包边界修复

  • 5.1 relative-coordinates.ts 迁出 @antv/l7-source@antv/l7-utils(layer-concern 坐标变换不再由 source 包承担)
  • 5.2 process 改由 layer 拿到 dataArray 后自行调用

阶段 6 — 测试 & 性能 & 不可变

  • 6.1 parser/transform 改不可变(filter/map/join 返回新对象,不再原地改入参)
  • 6.2 raster 家族补单测 19 case(raster/rasterRgb/rgb/ndi 纯函数)
  • 6.3 把 cluster/grid/hexagon 脆弱大数断言改为下界 + 形状断言
  • 6.4 Source.stats() 只读快照(ISourceStats 类型 + 7 case spec):rows / bbox / parserType / tileCount / isTile / cluster / dataVersion

阶段 7(可选,长期,不在本 PR 范畴)

7.1 显式 class 层级 / 7.2 source pipeline / 7.3 geojsonvt-decoder 抽离 — 记于 PLAN 待后续 major 演进。

⚠️ 行为变化清单(4 处,均 strictly-better / 修 bug,非回归)

绝大多数改动真零行为变化(对外语义等价、字节级不变):阶段 0、1、3、4.1、4.3a、5、6 均属此类。以下 4 处为 explicitly-minor 的错误/边界路径改进,需要 release note 提及:

阶段 变化 旧行为 新行为 性质
2.3 parser 未注册报错类型 undefined 直接 call → TypeError: ... is not a function ParserNotFoundError(type)(自定义 Error) 错误类型改善(仍失败,但可 catch + 有清晰信息)
4.2 DataSourcePlugin init 等 source 就绪 source.on('update') 手写 Promise,任意 update 类型即 premature resolve;init 失败静默 hang await source.ready,init 完成才 resolve;init 失败 reject surface 修 premature-resolve bug + 失败不再 hang(minor)
4.3b setData 失败 surfacing .then(emit 'update') 无 catch → 失败 'update' 不 fire(消费方 hang)+ unhandled rejection(吞错) .catch(emit 'error'),失败由 'error' 事件 surface(eventemitter3 无 Node 抛错语义) strictly-better,零签名变化
4.4 cluster 入口合并 cluster: booleantransforms:[{type:'cluster'}] 两条路径并存;后者 broken(cluster() 返 Supercluster 被 executeTransObject.assign 腐蚀 source.data,全仓零使用);clusterOptions.enable 死字段 cluster transform 改 clusterTransform deprecation wrapper(warn once + delegate);删 enable 死字段;ClusterManager.init 仍走 cluster() 零 warning 删 zero-usage broken path + 死字段;用 broken path 的人会得 warn(未来 major 移除)

happy path 零行为变化指的是:new Source(data, cfg) 正常解析、setData 成功 re-parse、Source.create / createSource、cluster 正常聚合、瓦片 source 正常——这些公开成功路径的输出、事件时序、数据形状字节级不变。上面 4 处都只在「错误/边界/已废弃」情形触发。

验证(5 项全过,零回归)

  • ✅ prettier / eslint 0
  • ✅ tsc source 0(去 glsl 噪音)
  • ✅ tsc layers 229(基线,全 maps/TMap pre-existing,非本次引入)
  • ✅ jest source 133 passed(原基线 + 7 新 stats case)
  • ✅ jest layers 57 passed 1 skipped(基线)
  • 合并跑 source + layers:190 passed / 1 skipped

文档 / 后续

  • 重构记录:docs/refactoring/source/PROGRESS.md
  • 遗留问题(便于后续继续优化):docs/refactoring/source/BACKLOG.md(含 6.2 发现的 rgb/ndi 无 guard 隐患、4.3b stale-data recovery、阶段 4.5 移除 cluster transform 注册需 major changeset 等)

Review 提示

本 PR 含 67 个 commit,每阶段独立切片。建议按 PLAN 阶段顺序审查,每个阶段内代码 + 测试 + 文档自洽,可独立 revert。重点请关注「行为变化清单」中 4 处 minor 变更是否需要发版说明 / changeset。

🤖 Generated with Claude Code

lzxue added 30 commits July 20, 2026 11:15
…port (stage 0.1)

- 将 source/interface.ts 与 @antv/l7-core 重复的 7 个类型改为 re-export
  (DataType/IDictionary/IFeatureKey/IJsonData/IJsonItem/IParseDataItem/IParserData)
- 保留 source 专有类型原地定义(栅格波段运算、瓦片数据源等)
- 新增 docs/refactoring/source/ 存档:PLAN/PROGRESS/BACKLOG/README
- 阶段 0.1: 纯 type-only 变更, 运行时无影响, 27/27 单测通过

详见 docs/refactoring/source/PROGRESS.md
- base-source.ts: excuteParser -> executeParser (private)
- base-source.ts: caculClusterExtent -> calcClusterExtent (private)
- factory.ts: registerTransform param transFunction -> transformFn
  (avoid shadowing existing type alias 'transformFunction')

私有方法/局部参数重命名, 对外 API 无影响, 27/27 单测通过。
- 新增 src/transform/types.ts: StatMethod + 6 个 cfg interface
- filter/map/join/grid/hexagon: 内部 narrow 到具体 cfg 获得类型推导
- 函数签名保持 ITransform 兼容, as 断言无运行时代码, 行为不变
- 27/27 单测通过, tsc source/src 自身 0 错误
… (stage 0.4)

- parser/raster/rgb.ts -> parser/rgb.ts, 函数名 rasterRgb -> rgb
- parser/raster/ndi.ts -> parser/ndi.ts, 函数名 rasterRgb -> ndi
- parser/rasterRgb.ts (注册名 rasterRgb) 保持不变, 与 rgb/ndi 功能各异
- 三个 parser 非重复代码, 本次仅命名/位置整理
- 顺带回填阶段 0.3 commit SHA (a41999c) 到 PROGRESS

git mv 保留历史, default export 函数名改动对导入方无影响,
27/27 单测通过, tsc source/src 自身 0 错误。
调研发现 testTile parser 是 TileDebugLayer 调试图层的合法默认 parser
(TileDebugLayer.ts / tile/utils/utils.ts / DebugTile.ts 三处下游依赖),
非 dev/demo 死代码。PLAN 阶段 0.5 前提有误, 标记 wontfix。

同步 PLAN 表格: 0.1-0.4 标 ☑ 0.5 标 ✗。
…stage 0.6)

重命名 src/source/ -> src/tile-source/(实为「瓦片数据源」),两个同名

VectorSource class 拆为语义清晰的名字:

- source/vector.ts -> tile-source/mvt.ts, class -> MVTSource

- source/geojsonvt.ts -> tile-source/geojsonvt.ts, class -> GeoJSONVTTileSource

- source/index.ts -> tile-source/index.ts(兼容别名导出)

- git rm source/baseSource.ts(抽象类死代码, 无继承者)

- tile-source/index.ts 导出 MVTSource/GeoJSONVTTileSource, 保留

  VectorSource 作 @deprecated 兼容别名 (= MVTSource)

- 3 个 parser (mvt/geojsonvt/jsonTile) 更新 import 路径与类名

兼容性: layers 包 import { VectorSource } 经兼容别名零改动继续可用;

VectorSource 正式移除留待阶段 7。

验证: source tsc 0 错 (基线 31 不变), layers tsc 229 pre-existing

无新增, prettier 通过, jest 27/27 通过。
PROGRESS.md 阶段 0.6 记录的 commit SHA 回填。
…tage 1.1)

从 base-source.ts God Class 抽出 cluster 状态机到独立

ClusterManager delegate, 对外 API 完全等价 (ISource 不动)。

- 新增 cluster-manager.ts (138 行): 封装 Supercluster 索引 +

  clusterOptions + init/updateData/getClusters*/calcExtent

- base-source.ts (358 -> 314, -44):

  - cluster/clusterOptions 字段 -> accessor 转发 delegate

  - 删 clusterIndex 字段, getClusters*/updateClusterData 改转发

  - initCluster/calcClusterExtent 删, processData 改调

    clusterManager.init

  - destroy 改调 clusterManager.destroy

构造期 new ClusterManager 注入 extent/invalidExtent getter 闭包,

delegate 不拥有 extent 状态 (留待 1.4 抽 Bounds)。

未启用 cluster 时 getClusters/updateClusterData 仍抛 TypeError,

与原行为一致 (DataSourcePlugin 契约前提)。

验证: source tsc 0 错 (基线 31 不变), layers tsc 229 pre-existing

无新增, eslint/prettier 通过, jest 27/27 通过 (cluster case

覆盖 updateClusterData 路径)。
PROGRESS.md 阶段 1.1 记录的 commit SHA 回填。
…tage 1.2)

从 base-source.ts 抽出瓦片管理职责到独立 TilesetAdapter

delegate, 对外 API 完全等价 (ISource 不动)。

- 新增 tileset-adapter.ts (88 行): 封装 TilesetManager 实例的

  创建/更新/销毁 + 7 个 reload/getTile 转发方法

- base-source.ts (314 -> 306, -8):

  - tileset/isTile 字段 -> getter 转发 adapter

  - 删 initTileset, executeParser 改调 tilesetAdapter.init

  - 7 个 reload*/getTile* 方法实现体改为转发 adapter

  - destroy 改调 tilesetAdapter.destroy

关键: adapter.manager 必须 public, 因 layers/tile/core/BaseLayer

直接读 source.tileset as TilesetManager 后自由操作实例。getter

转发让 layers 拿到同一个 TilesetManager 实例, 行为等价。

验证: source tsc 0 错 (基线 31 不变), layers tsc 229 pre-existing

无新增, eslint/prettier 通过, source jest 27/27, source+layers

jest 81 passed/1 skipped/0 failed。
…ge 1.3)

从 base-source.ts 抽出 feature 查询/更新职责到独立

FeatureIndex delegate, 对外 API 完全等价 (ISource 不动)。

- 新增 feature-index.ts (115 行): 封装 getFeatureById /

  getFeatureId / updateFeaturePropertiesById + dataArrayChanged

  - 构造期注入 5 个 getter 闭包延迟读 Source 状态

  - dataArrayChanged private 自持, setData 调 reset()

- base-source.ts (306 -> 290, -16):

  - 删 dataArrayChanged 字段

  - 3 方法体收敛为转发, updateFeaturePropertiesById

    保留 emit('update')

  - setData dataArrayChanged=false -> featureIndex.reset()

设计: emit 留 Source 转发端, delegate 不持 EventEmitter;

cloneDeep 与 'null' 越界占位保留原行为。

验证: source tsc 0 错 (基线 31 不变), layers tsc 229

pre-existing 无新增, eslint/prettier 通过, source jest 27/27。
… 1.4)

从 base-source.ts 抽出 extent/center/invalidExtent + setCenter 到

独立 Bounds value object, 对外 API 完全等价.

- 新增 src/bounds.ts (50 行): extent/center/invalidExtent 三态 +

  update(bbox) 原子写入, 合并原 executeParser 末尾三行 + setCenter

- base-source.ts (290 -> 292): 删 3 字段 + setCenter 方法, 加 bounds

  字段 + 3 getter 转发; executeParser 三行 -> bounds.update(...);

  ClusterManager 闭包改读 this.bounds.extent/.invalidExtent

行数微增因新增 3 getter + import 但状态写入从 3 处分散收敛为 1 处.

验证: source tsc 31 基线不变, layers tsc 229 不变,

eslint/prettier 通过, source jest 27/27 通过.
PROGRESS.md 阶段 1.4 记录回填实际 commit SHA.
… (stage 2.1)

interface.ts: 新增 Parser<TData,TCfg,TResult> 统一契约 + KnownParsers

13 parser 映射 + KnownParserType 键名联合. 去重 RasterDataType

/ IRGBParseCfg (rgb.ts + ndi.ts 两份相同) 与 IImageCfg

(image.ts) 收敛到 interface.ts 单一来源. factory.ts:

ParserFunction = Parser 替代字面量 (类型擦除版, align cfg? optional).

13 个 parser 文件无需改签名 (隔离 TS 严格模式实测兼容).

对外 API 完全等价, 仅新增类型无运行时改动.

验证: source tsc 31 基线不变, layers tsc 229 不变,

eslint/prettier 通过, source jest 27/27 通过.

BALL_LOG: 闭环 BACKLOG RasterDataType/IRGBParseCfg 重复定义.
PROGRESS.md / BACKLOG.md 阶段 2.1 记录回填实际 commit SHA.
…leton (stage 2.2)

PARSERS/TRANSFORMS 模块级可变对象抽到 ParserRegistry class + defaultRegistry 单例.

factory.ts 4 函数保留为 defaultRegistry 薄转发 wrapper (加 @deprecated).

对包入口 re-export ParserRegistry 与 defaultRegistry, 对外 API 等价.

验证: source tsc 31 / layers tsc 229 / jest 27/27 / eslint+prettier 通过.
回填 PROGRESS.md 阶段 2.2 记录的 commit SHA c27b598.
… 2.3)

未注册改抛 ParserNotFoundError / TransformNotFoundError (替代 undefined 残报 TypeError).

签名保持 Parser / TransformFn 不带 | undefined, 调用方链式调用零变更.

新增 __tests__/parser-registry.spec.ts (10 tests) 覆盖抛错 + 内置注册.

验证: source tsc 31 / layers tsc 229 / jest 37/37 / eslint+prettier 通过.
回填 PROGRESS.md 阶段 2.3 记录与 BACKLOG 新增项的 commit SHA 6ffdcf5.
…itelist (stage 2.4)

index.ts 顶层 13 registerParser + 6 registerTransform 收敛到 builtins.ts 的 registerBuiltins().

package.json sideEffects 收紧为 [./es/index.js] 白名单 (沿用 packages/layers 约定).

默认 import { Source } 仍自动注册全 13 内置 parser, 运行期 0 行为变化.

验证: source tsc 31 / layers tsc 229 / l7 tsc 346 / jest 40/40 / eslint+prettier 通过.
回填 PROGRESS.md 阶段 2.4 记录与 BACKLOG 新增项的 commit SHA dd39acd.
…e 2.5)

add createSource(data, cfg, registry?) factory wrapping new Source

Source ctor gains optional registry 3rd param (default defaultRegistry)

base-source/cluster-manager use this.registry.getParser/getTransform

full registry-injection loop: parser execute + cluster re-parse

index.ts re-exports createSource; factory.ts deprecated wrappers kept

5 new tests; tsc source 31 / jest 45 / eslint / prettier all green
回填 PROGRESS.md 阶段 2.5 记录的 commit SHA 占位符
…3.1.1)

new TileLoader interface (loadTile -> Promise<ITileSource | undefined>)

JsonTileLoader class: mechanical lift of getVectorTile closure body

jsonTile.ts (83->30) delegates getTileData to loader.loadTile

interface return type undefined-ified to support mvt resolve(undefined)

6 new loader unit tests via jest.mock('@antv/l7-utils') (first tile-loader net)

source tsc 31 / layers 229 / jest 51 / eslint / prettier all green
回填 PROGRESS.md 阶段 3.1.1 记录的 commit SHA 占位符
把 parser/mvt.ts 的模块级 getVectorTile 闭包机械抽取为实现 TileLoader
接口的 MVTLoader 类,行为与迁移前 100% 等价:

- 新增 src/loader/mvt-loader.ts (72 行): MVTLoader implements TileLoader
  - URL 模板用 tileParams (TileLoadParams) 插值 [mvt 与 jsonTile 核心差异]
  - getCustomData 入参 + MVTSource 构造用 tile.x/y/z
  - 失败 (err/无数据) 统一 resolve(undefined), err 永不 reject
  - 仅 getArrayBuffer 分支同步设 tile.xhrCancel = () => xhr.cancel()
    (赋值先于异步回调, 取消语义等价)
- 重写 src/parser/mvt.ts (73 → 53 行): 薄包装委托 MVTLoader
  - 保留 DEFAULT_CONFIG (mvt 原有, 与 jsonTile 不同)
  - 删死导出 export type MapboxVectorTile (BACKLOG 阶段 0.4 项部分闭环)
- 新增 __tests__/loader/mvt-loader.spec.ts (155 行 / 6 tests)
  - 双层 mock: jest.mock('@antv/l7-utils') + jest.mock MVTSource 构造器
  - tileParams={x:10,y:20,z:30} vs tile={x:1,2,3} 精确锁死
    [URL 用 tileParams / getCustomData+MVTSource 用 tile.xyz] 差异
  - 覆盖 getArrayBuffer 成功/失败/空 + getCustomData 成功/失败
    + xhrCancel 设/未设 + requestParameters 透传

验证:
- tsc source 31 错基线不变 (全 core .glsl 噪音, 0 非 glsl)
- tsc layers 229 基线不变
- eslint / prettier 通过
- jest source: 9 suites / 57 tests 全通过 (旧 51 + 新 6)
- jest source + layers: 111 passed / 1 skipped / 0 failed

文档: PLAN.md 标 3.1.2 done; PROGRESS.md 追加记录 + 下一步 → 3.1.3
GeoJSONVTLoader; BACKLOG.md 闭环 mvt.ts 死 export + 更新 loader 单测状态

遗留: → 阶段 3.1.3 GeoJSONVTLoader (内存切瓦片, 无 xhrCancel, URL 用
tile.x/y/z); 阶段 3.2 RasterTileLoader; 阶段 3.3 image.ts 去 fetch
把 parser/geojsonvt.ts 的 getVectorTile 闭包 + 4 个投影助手机械抽取为
实现 TileLoader 接口的 GeoJSONVTLoader 类, 行为与迁移前 100% 等价:

- 新增 src/loader/geojsonvt-loader.ts (199 行): GeoJSONVTLoader implements TileLoader
  - 构造期 geojsonvt(data, options) 一次性建内存空间索引 tileIndex
  - loadTile 走 tileIndex.getTile(tile.z/x/y) 同步切瓦片 (用 tile 索引查表)
  - GetGeoJSON(extent, tileParams.x/y/z, ...) 投影回 GeoJSON (用 tileParams!)
  - GeoJSONVTTileSource(vectorTile, tile.x/y/z) 用 tile 构造 Source
  - 无 getCustomData / 无 tile.xhrCancel / 始终 resolve ITileSource (空瓦片=[])
  - 保留 // @ts-ignore (GetGeoJSON feature 含 relativeOrigin/coord 额外字段)
- 新增 4 个投影助手下沉 loader: VectorTileFeatureTypes/signedArea/
  classifyRings/GetGeoJSON (仅服务 loadTile, parser 不再需要)
- 重写 src/parser/geojsonvt.ts (216 → 75 行, -141 行): 薄包装委托 loader
  - 保留 getOption (默认 options 合并, parser config 形状组装)
  - 保留 DEFAULT_CONFIG 不含 warp (geojsonvt 原本无, 与 mvt 不同)
- 新增 __tests__/loader/geojsonvt-loader.spec.ts (132 行 / 6 tests)
  - 双层 mock: jest.mock('geojson-vt') + jest.mock GeoJSONVTTileSource
  - GetGeoJSON 真实运行, LineString feature 验证投影用 tileParams
    (tileParams.x=10 → lng=-67.5; 若误用 tile.x=1 → -135, 明显可区分)
  - TS 修复: geojson-vt export= 合并函数+namespace, (geojsonvt as jest.Mock)
    报 TS2352, 改用 as unknown as jest.Mock 两步转换 (6 处统一别名)

⚠️ 关键: geojsonvt 是 tile/tileParams 混用的第三种形态 (与 mvt/jsonTile 都不同):
  索引查表用 tile / 坐标投影用 tileParams / Source 构造用 tile. spec 用
  tileParams={x:10,y:20,z:5} vs tile={x:1,2,3} 精确锁死此差异, 不可简化.

验证:
- tsc source 31 错基线不变 / tsc layers 229 基线不变
- eslint --max-warnings 0: 3 文件 0 错 0 警
- prettier --check: 通过
- jest source: 10 suites / 63 tests 全通过 (旧 57 + 新 6)
- jest source + layers: 117 passed / 1 skipped / 0 failed

文档: PLAN.md 标 3.1.3 done (阶段 3.1 收尾); PROGRESS.md 追加记录 + 下一步
→ 3.2 RasterTileLoader; BACKLOG.md 更新 loader 单测状态 (阶段 3.1 三个瓦片
矢量 loader 全覆盖, 剩 raster-tile/image)

遗留: → 阶段 3.2 RasterTileLoader 大 switch 拆分 (建议先抽分发器再拆 6 小
loader 两步渐进); 阶段 3.3 image.ts 去 fetch. 阶段 3.1 (Parser/Loader 解耦
— 瓦片矢量部分) 全部完成
lzxue added 20 commits July 23, 2026 11:10
ISource + Source 新增单调递增 dataVersion 计数器(零行为变化)。

bump 点:
- setData(全量 reseat,reseat 同步阶段 bump,先于 update fire)
- updateFeaturePropertiesById(原地属性变更,emit 前同步 bump)

不 bump:
- updateClusterData(zoom 驱动聚合视图重算,originData 未变,属派生视图)
- 构造期首次 parse(generation 0 = 初始数据)

为何不含 4.3b 行为切片:setData 为运行时热路径,改错即回归。
4.3a 先铺版本号 infra(同 4.1 infra→4.2 消费模式),4.3b 单切片
在版本号基础上做同 schema skip re-parse,需先补调用链 / 副作用 / spec。

契约安全:implements ISource 仅 Source 一处,无 subclass,新增 required
字段无破坏;tsc layers 229 baseline 不变。

spec: data-version.spec.ts 5 case。
基线: tsc core 0 / source 0(去glsl) / layers 229 / jest source 107(+5) /
layers-plugins 6 / eslint 0。
setData 的 init().then(emit 'update') 旧路径无 .catch:re-parse 失败时
'update' 不 fire(消费方 hang)+ 未捕获 rejection(吞错,同 4.2 为构造期
initPromise 修的 swallow/hang 模式)。现加 .catch(emit 'error'):

- 成功路径字节级不变,'update' 契约不变
- 失败由 'error' 事件 surface(eventemitter3 无 Node 抛错语义,无监听即
  静默,安全)
- 零签名变化(void→void),零调用方影响

原 4.3b「同 schema skip re-parse」经勘探判定 dead-end:parse /
tilesetAdapter.init / bounds.update / clusterManager.init / executeTrans
全 data-dependent,setData 本质即换 data,skip 无收益。结案记 BACKLOG。

spec: set-data.spec.ts 3 case(成功基线锁 + 失败 surfacing + 无 unhandled
rejection)。
基线: tsc core 0 / source 0(去glsl) / layers 229 / jest source 110(+3) /
layers-plugins 6 / eslint 0。
勘探 new Source / Source.create / createSource 全仓 call sites:

- 生产 new Source( 仅 1 处:DataSourcePlugin.ts:15,正是 4.2 合法的
  new Source + await source.ready race-free 模式(非 Source.create)
- Source.create( / createSource( 生产零消费(仅 spec + 包内定义/doc)
- source 包内其余 new Source( 命中均为工厂内部

wontfix 五条理由:
1. 自相矛盾 — warn 会 nag 唯一合法生产消费方
2. 零 bad-pattern call site(4.2 已清掉唯一真实 race)
3. Source.create/createSource 生产零采用 → deprecation 无对象
4. new Source 是公开构造器,deprecate 属 major 不该 minor 推
5. race 已由 4.1 ready getter 在消费侧解决

纯评估切片无代码改动(同 4.2 后续 f20b3e6 模式)。
阶段 4 主题全部收敛,后续转 stage 3.2.2 / stage 5。

PROGRESS 下一步重指向 3.2.2 loader 解耦弧延续;
PROGRESS/PLAN/BACKLOG 同步标注 wontfix。
6 分支 switch(IMAGE/ARRAYBUFFER/CUSTOMIMAGE/CUSTOMTERRAINRGB/
CUSTOMARRAYBUFFER/CUSTOMRGB)拆成 4 独立小 loader(PLAN 偏差:6 enum
仅 4 种取数行为,CUSTOMIMAGE/CUSTOMTERRAINRGB 共享、CUSTOMARRAYBUFFER/
CUSTOMRGB 共享,拆 6 成对重复故拆 4)+ 引入 IRasterTileLoader 接口,
分发器持 Map<RasterTileType, IRasterTileLoader> 按 tileDataType 选 loader、
未命中走 ImageRasterLoader 兜底(保 default 分支语义)。

新增 4 loader:raster/{image,buffer,custom-image,custom}-raster-loader.ts。
重构 raster-tile-loader.ts:删 loadCustomImageData/loadCustomRasterData
私有方法(逻辑移入对应小 loader),唯一消费方 parser/raster-tile.ts 零改动。

patch 级纯内部重构,公开 API(ctor 3 参 + loadTile(tileParams,tile))不变。
验证:prettier/eslint/tsc source 0/tsc layers 229 不变/jest source 110
(raster-tile-loader.spec 16 case 全过=行为等价证明,零新增 spec)。

两处交接后修复:custom-image loader 注释残留 fewer-params 措辞(截断句)
已清;import type→import 值导入(CustomDataProvider 作 new 值用,TS1361)。

阶段 3 主题(3.1/3.2/3.3/3.4)全部收敛。
纯评估切片,无代码改动(同 4.1b 模式)。勘探依赖图 + call sites 判定
5.1 原 PLAN「迁 layers/utils + source type re-export 过渡」可行性。

关键发现:core→utils / source→{core,utils} / layers→{core,source,utils}。
- Approach A(target=layers)❌:source→layers re-export 循环 →
  type re-export 过渡不可能 → source 公开导出须彻底移除 = breaking
  (major 级),与「major removal 留未来」纪律冲突。
- Approach B(target=utils,推荐)✓:解耦 IParseDataItem 类型依赖
  (函数仅用 item.coordinates,改 minimal interface),source
  re-export 过渡保公开 API(minor-safe),BaseLayer 改 import utils。

唯一消费方 BaseLayer.ts:42;examples/dev/docs 零代码 import。
PLAN 5.1/5.2 已据发现修订;BACKLOG 记 Approach B 类型设计待决。
下一「继续」执行 Approach B。
…utils

relative-coordinates.ts 从 @antv/l7-source 迁到 @antv/l7-utils(包边界
修复,Approach B)。解耦 IParseDataItem 类型依赖:4 函数 +
IRelativeCoordinateResult 改泛型 <T extends { coordinates?: any[] }>,
T 透传到返回类型,消费方 BaseLayer 传 IParseDataItem[] 得
result.dataArray: IParseDataItem[],赋回 data.dataArray 零 type 涟漪。

- 新增 packages/utils/src/relative-coordinates.ts(泛型,无 core 依赖)
- utils index 加 export* from './relative-coordinates'
- source index: 删本地 export,改命名 re-export 自 @antv/l7-utils
  (transitional 保公开 API,minor-safe)
- 删 packages/source/src/utils/relative-coordinates.ts(git rename)
- BaseLayer.ts:42 import 自 l7-source 合并入既有 l7-utils import

类型设计最终 = 泛型(非 scoping 草拟 minimal interface):BaseLayer:1423
把 result.dataArray 赋回 IParseDataItem[] 字段,minimal interface 缺
_id 会 tsc 失败,故泛型保 T=IParseDataItem 零涟漪。

验证:prettier/eslint/tsc utils 0/tsc source 0(glsl)/tsc layers
229 不变/jest source 110/jest layers 57 passed 1 skipped = 改前
stash 对比 baseline 完全一致(严格无回归)。阶段 5 收尾。
filter/map/join 三处「原地改 data.dataArray + 返回同引用」改为返回
新对象 { ...data, dataArray: ... },不再改入参,利于缓存/diff。

零行为变化(executeTrans 等价性证明):base-source executeTrans 对每个
transform 执行 Object.assign(this.data, getTransform(type)(this.data, tran))。
改前原地改 dataArray + 返回同引用,Object.assign(this.data, this.data) = no-op
(dataArray 已就地改)。改后返回 B = {...A, dataArray:newArr}(A=this.data 未改),
Object.assign(A, B) 回写:A.dataArray=newArr、其余 props B[p]===A[p] 故 no-op →
逐字段等价,且 this.data 引用稳定(Object.assign 不换引用)。callback 执行期间
data.dataArray(RHS 读取)两种实现下均为原始数组,闭包读亦等价。

切片边界(PLAN line 100 明确 filter/map/join):grid/hexagon 本就返回新对象
(已不可变,无需改);cluster transform 已 @deprecated,cluster() no-pointIndex
分支返回 Supercluster(ClusterManager 直调 Path A),pointIndex 分支 mutation
属 deprecated/legacy dead branch → defer BACKLOG。不动 executeTrans、builtins
注册、types cfg 契约。

验证 6 项全过(回归网=5 transform spec):prettier/eslint 0/tsc source 0
(去 glsl)/tsc layers 229(baseline)/jest source 110(=baseline,filter/grid/
hexagon/map/join 5 case 全过)/jest layers 57 passed 1 skipped(=baseline)。

遗留:cluster pointIndex 分支不可变 + executeTrans 直装 + 6.2 transform
单测,记 BACKLOG。PROGRESS SHA 占位「commit 待补」后续 backfill。
6.2 的真实缺口是 raster 家族 4 个纯函数 parser(同步 band 操作 + 坐标投影,
无需 mock loader)。image/mvt/geojsonvt/jsonTile/raster-tile 已由阶段 3
loader spec 覆盖 happy+error(CustomDataProvider 6 case)。

新 spec:
- parser/raster.spec.ts 10 case:raster 7(number[] 直传/默认 extent 4 角
  派生/自定义 extent/自定义 coordinates 非矩形/format 提供 isNumberArray
  优先走直传/ArrayBuffer 路径 bandsOperation 产 Promise 进 data 异步契约/
  ArrayBuffer[] 包装同路径)+ rasterRgb 3(直传+rest 透传)
- parser/rgb.spec.ts 9 case:rgb 5(显式 R/G/BMinMax 交错输出/负值钳 0/
  自定义 bands 通道序/缺 MinMax 走 percentile/不足 3 波段 warn-only 无
  guard 致崩)+ ndi 4(归一化差值/自定义 bands/rest 透传/不足 2 波段致崩)

既存隐患记 BACKLOG(6.2 补测试不改行为):rgb extent 无默认值缺校验 +
rgb/ndi 波段不足仅 warn 不 guard 致崩。

验证 4 项全过:prettier/eslint 0/tsc source 0(去 glsl)/jest source 126
(110 baseline + 16 新增, 19 suites 全绿)。PROGRESS SHA 占位后续 backfill。
cluster 110/217、grid 2511、hexagon 1934 这类「具体大数」断言随 Supercluster
算法 / 投影精度 / 测试数据增删即碎,却无真实行为回归 → 改为下界 + 结构形状
断言,留足算法/数据漂移余量,同时仍能捕获聚合完全失效(返 0/undefined)真回归。

改造(4 spec 零生产代码改动):
- source.spec: cluster 110/217 → zoom2 下界>50+coordinates + zoom3>zoom2 单调
  (簇拆分);grid 2511 → >1000+coordinates/count/_id:number;hexagon 1934 →
  同型
- data-version.spec: cluster 110/217 → >50(锁的是 dataVersion===0 不 bump 契约,
  点数非主角)
- create-async.spec: 两处 110 → >50(锁 async 工厂/自定义 registry 端到端)
- create-source.spec: 两处 110 → >50(锁 sync 工厂/自定义 registry 端到端)

保留:纯数 parser 小整数 + 值断言(value===100 等非脆弱)+ 到Equal(
Point.features.length-1)(filter 已具相对语义)。

验证 4 项零回归:prettier/eslint 0/tsc source 0(去 glsl)/jest source 126
(=6.2 baseline)/tsc layers 229(baseline)/jest layers 57 passed 1 skipped
(baseline)。PROGRESS SHA 占位后续 backfill。
…spec

阶段 6.4(minor-safe 新增 API,零行为变化):

- 新类型 ISourceStats(interface.ts,7 字段只读快照):rows / bbox /
  parserType / tileCount / isTile / cluster / dataVersion;BBox 加进 turf
  import;经 export * from './interface' 由 index 透出。
- 新方法 Source.stats()(base-source.ts,置于 getParserType 之后):纯只读,
  不变 Source 状态。rows 用 data?.dataArray?.length ?? 0 兜底;tileCount 用
  tileset?.currentTiles.length ?? 0;parserType 用 (parser as IParserCfg).
  type || 'geojson' 与 executeParser 默认逻辑一致。对 new Source /
  Source.create / setData / updateFeaturePropertiesById 路径零行为变化
  (未触及 ISource 核心契约,未加 deprecation)。
- 新 spec source-stats.spec.ts(7 case):非瓦片 geojson 全字段快照;
  parserType 与 getParserType 一致;聚合源 cluster=true 初始未 zoom rows
  =全量 feature;瓦片源 mvt isTile=true/parserType=mvt/tileCount=0/rows=0;
  setData 后 stats 反映新 rows + dataVersion bump(once('update') 等 re-parse);
  updateFeaturePropertiesById bump dataVersion 行数不变;stats 幂等不改状态。

验证(5 项全过,零回归):prettier ✓ / eslint 0 ✓ / tsc source 0(去 glsl)✓ /
jest source 133 passed(126 baseline + 7 新)✓ / tsc layers 229(baseline)✓ /
jest layers 57 passed 1 skipped(baseline)✓。

至此 PLAN 阶段 6 完整收敛(6.1/6.2/6.3/6.4 全 ☑),阶段 0-6 全部落地。

重构参考:docs/refactoring/source/PLAN.md › 阶段 6.4
@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 38b58d5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@antv/l7-source Patch
@antv/l7 Patch
@antv/l7-layers Patch
@antv/l7-three Patch
@antv/l7-component Patch
@antv/l7-scene Patch
@antv/l7-core Patch
@antv/l7-map Patch
@antv/l7-maps Patch
@antv/l7-renderer Patch
@antv/l7-test-utils Patch
@antv/l7-utils Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@lzxue lzxue changed the title refactor(source): @antv/l7-source 渐进式重构(阶段 0-6,零行为回归) refactor(source): @antv/l7-source 渐进式重构(阶段 0-6,happy path 零行为变化) Jul 23, 2026
@lzxue
lzxue merged commit cd654c1 into master Jul 23, 2026
7 checks passed
@lzxue
lzxue deleted the refactor/source-progressive branch July 23, 2026 07:43
lzxue added a commit that referenced this pull request Jul 23, 2026
source.{zh,en}.md 新增「兼容性 / Compatibility」小节:明确旧 new Source(data, cfg) /
cluster: true / ISourceCFG 旧字段 / 'update' 事件全部向后兼容,新 API 为可选迁移路径。
cluster 段补弃用提示:transforms: [{ type: 'cluster' }] 已弃用,改用 cluster: true。

修正 en 文档既有 embed 引用 bug:source.en.md / mvt.en.md / raster_tile.en.md 原嵌入
method.zh.md(中文)→ 改为 method.en.md(英文版早已存在)。该 bug 非 本次重构引入
(commit 1d5c5fa),本轮顺手修正。

重构存档同步:README「当前状态」更新为阶段 0-6 已合并;PROGRESS 追加 PR #2882 合并 /
文档补全 / 兼容性核查三条记录。
lzxue added a commit that referenced this pull request Jul 23, 2026
Beta release of l7-source progressive refactor (PR #2882 + docs #2883 + maps #2884).

Includes test-utils peerDeps cleanup fixing the changeset 3.0.0 version coercion bug.

Published under beta dist-tag. latest remains 2.29.1.
lzxue added a commit that referenced this pull request Jul 23, 2026
master 的 @antv/l7-source 渐进式重构(#2882)与本分支搬运(source→layers)冲突,逐类解决:

- layers/src/source 与 __tests__/source 完整采用 master 新结构
  (tile-source/、parser/ndi.ts、base-source.ts、create-source.ts 等)
- relative-coordinates 已由 master 上移至 @antv/l7-utils,保留 utils 版本
- maps 底图迁移:采用 master 的 BaseMap/MapboxBaseMap 新结构,
  @antv/l7-map 引用改 ../mapbase(map 包已并入 maps/src/mapbase)
- BaseLayer: @antv/l7-source → ../source,保留 master 的 processRelativeCoordinates
- 删除被合并包(source/component/map/renderer)残留,版本号同步 master 2.30.0-beta.0
- l7/layers/scene 顶部元数据冲突取 master 版本号;test-utils 保留 peerDependencies
- __tests__/source 测试相对路径同步调整(../src → ../../src/source)

构建验证:layers(271) / maps(88) / scene(66) / l7(UMD 1.4M < 1.7Mb) 全部通过
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant