Repository Wiki
ChanIok/SpinningMomo

地图注入:游戏内地图与前端桥接

地图注入(map injection)是 SpinningMomo 中"在官方地图页面之上叠加自定义图钉/聚合标记"的核心机制:一段由构建管线生成、由原生 WebView2 宿主注入到 myl.nuanpaper.com 官方地图 iframe 内的桥接脚本,通过 postMessage 与 Vue 前端宿主(useMapBridge)双向通信,从而把前端 store 中的标记数据与渲染/运行时选项"桥接"进游戏官方地图。

Purpose and Scope

本页覆盖该能力的端到端机制:

  • 注入脚本的生成模板(web/src/features/map/injection/source/bridgeScript.js):站点门控、Leaflet 劫持、worldId 读取、注入侧消息处理。
  • 宿主侧桥接(web/src/features/map/composables/useMapBridge.ts):来源校验、可序列化载荷构造、postRuntimeSync 的 dev/prod 双路径、入站消息分发。
  • 双向 postMessage 消息协议(动作名、载荷结构、方向)。
  • 注入脚本进入 WebView2 的构建管线(scripts/generate-map-injection-cpp.js 压缩并生成 C++ 头文件)。
  • 失败模式、边界条件、性能与扩展点。

以下主题有意留给兄弟页面,本页只做最小限度的引用:

  • 地图页面 UI 与场景编排(MapIframeHost.vue、useMapScene.ts):见地图功能(map feature)相关页面。
  • 标记数据来源、坐标换算与默认值(domain/、useMapStore):见标记与坐标域相关页面。
  • 图库(gallery)本体、暗房路由与过滤逻辑:useMapBridge 仅作为跨页触发方,见 gallery 相关页面。
  • 原生 WebView2 宿主层(C++ core::WebView)如何调度注入头文件:见 WebView/RPC 桥相关页面。

Overview

SpinningMomo 是一个 原生 Win32 C++ 后端 + 内嵌 WebView2 前端 的双进程模型应用(见 AGENTS.md 对整体架构的说明)。地图页以 iframe 形式嵌入官方地图站 myl.nuanpaper.com,而官方站点并不提供任何扩展 API。为了让用户的截图/资产标记出现在官方地图上,本能力采取的策略是:

  1. 构建期:scripts/generate-map-injection-cpp.js 会把 web/src/features/map/injection/source/*.js 中的注入源码压缩并生成 C++ 头文件(AGENTS.md 的生成器清单明确要求修改注入源后必须重新执行该脚本)。
  2. 运行期(注入):原生 WebView 宿主在文档创建时执行注入脚本。脚本首先做 hostname 门控(只对 myl.nuanpaper.com 生效),随后通过 Object.defineProperty 劫持全局 window.L(Leaflet),在官方代码给 window.L 赋值时包装 L.Map 构造器,从而捕获地图实例并挂载自定义运行时。
  3. 运行期(桥接):注入脚本与父窗口(Vue 宿主)之间通过 postMessage 交换结构化消息。宿主侧 useMapBridge 在 会话就绪 后把 markers / renderOptions / runtimeOptions 推入 iframe;iframe 内的 mountOrUpdateMapRuntime(来自 runtimeCore 片段)负责真正在 Leaflet 地图上渲染标记、聚合与悬停卡片。
  4. dev 双路径:开发模式下(import.meta.env.DEV)宿主不走 SYNC_RUNTIME,而是把整个运行时载荷编成一段脚本用 EVAL_SCRIPT 注入执行(buildMapDevEvalScript),便于热更新迭代注入逻辑而无需重新生成 C++ 头文件。

关键术语:

术语含义
注入脚本在官方地图 iframe 文档创建时执行的一段 IIFE 风格 JS,源码位于 web/src/features/map/injection/source/
宿主运行在 WebView2 中的 Vue 应用(父窗口),持有 useMapStore / useGalleryStore
worldId官方地图当前世界编号(如 1.1),从官方站点自身的 localStorage 状态中读出
会话就绪注入脚本捕获到 Leaflet Map 实例后向宿主上报 SPINNING_MOMO_MAP_SESSION_READY 的时点

Architecture

Loading diagram...

架构要点(每一处都对应真实代码):

  • bridgeScript.js 是模板而非成品脚本:buildMapBridgeScriptTemplate() 返回一个大模板字符串,内部再拼接 buildIframeBootstrapSnippet()(来自 ./iframeBootstrap.js)与 buildRuntimeCoreSnippet()(来自 ./runtimeCore.js)。模板中的 __ALLOW_DEV_EVAL__ 是一个生成期替换的布尔占位符,控制注入页是否接受 EVAL_SCRIPT 调试指令;若不替换,该行会引用未声明标识符而抛 ReferenceError,因此该常量必须在生成管线中被替换为具体布尔字面量。
  • 单向数据依赖:useMapBridge 只从 useMapStore(读)和 useGalleryStore / vue-router(写/导航)取数据;iframe 侧运行时(mountOrUpdateMapRuntime)只消费 __SPINNING_MOMO_* 全局暂存变量。两侧唯一的耦合面就是 postMessage 协议(定义于 web/src/features/map/bridge/protocol.ts)。
  • flushMapRuntimeToIframe(composables/mapIframeRuntime.ts)是宿主侧的统一"刷新"入口,在会话就绪与运行时选项变更时被调用,最终仍走 useMapBridge.postRuntimeSync()。

核心控制流:注入侧实现

站点门控与全局暂存变量

注入脚本的第一行是防御性的 hostname 门控——只对官方地图域名生效,避免该脚本在任何其他页面执行:

javascript
1if (window.location.hostname === 'myl.nuanpaper.com') { 2 let innerL = undefined; 3 const DEFAULT_WORLD_ID = '1.1'; 4 const WORLD_ID_PATTERN = /^\\d+(?:\\.\\d+)?$/; 5 window.__SPINNING_MOMO_ALLOW_DEV_EVAL__ = __ALLOW_DEV_EVAL__; 6 window.__SPINNING_MOMO_PENDING_MARKERS__ = []; 7 window.__SPINNING_MOMO_RENDER_OPTIONS__ = {}; 8 window.__SPINNING_MOMO_CLUSTER_OPTIONS__ = {};

Source: bridgeScript.js

设计意图:注入发生在文档创建时,此时官方 Leaflet 与 DOM 都尚未就绪。因此所有跨阶段状态都挂在 window.__SPINNING_MOMO_* 命名空间下做暂存(staging):宿主先推过来的 markers/options 在运行时挂载前一直停留在这些全局变量里,maybeMountRuntime() 才会真正消费它们。这让"宿主推送"与"官方页面就绪"两个异步时序彻底解耦。

读取官方当前 worldId

worldId 用于区分官方地图的不同世界,注入脚本从官方站点自己的 localStorage 中读取:

javascript
1const readOfficialCurrentWorldId = () => { 2 try { 3 const rawMapState = window.localStorage && window.localStorage.getItem('infinitynikkiMapState-v2'); 4 if (typeof rawMapState !== 'string' || rawMapState.length === 0) { 5 return DEFAULT_WORLD_ID; 6 } 7 const parsedMapState = JSON.parse(rawMapState); 8 const worldId = normalizeOfficialCurrentWorldId(parsedMapState && parsedMapState.state && parsedMapState.state.currentWorldId); 9 return worldId || DEFAULT_WORLD_ID; 10 } catch (e) { 11 return DEFAULT_WORLD_ID; 12 } 13};

Source: bridgeScript.js

配套的 normalizeOfficialCurrentWorldId(第 15–30 行)做了三重防御:非字符串返回 undefined;去除首尾成对的引号;用 WORLD_ID_PATTERN(/^\d+(?:\.\d+)?$/)校验数字编号格式,不合法一律回退到 DEFAULT_WORLD_ID = '1.1'。整个读取包在 try/catch 里,保证任何 localStorage/JSON 异常都退化为默认值而非中断注入。

Leaflet 劫持:包装 L.Map 构造器

这是整个注入机制最核心的技巧。官方站点会把 Leaflet 赋值给 window.L,脚本用 Object.defineProperty 拦截这次赋值:

javascript
1Object.defineProperty(window, 'L', { 2 get: function() { return innerL; }, 3 set: function(val) { 4 innerL = val; 5 if (innerL && innerL.Map && !innerL.Map.__SPINNING_MOMO_PATCHED__) { 6 const OriginalMapClass = innerL.Map; 7 innerL.Map = function(...args) { 8 window.__SPINNING_MOMO_MAP_CTOR_COUNT__ = 9 (window.__SPINNING_MOMO_MAP_CTOR_COUNT__ || 0) + 1; 10 const mapInstance = new OriginalMapClass(...args); 11 window.__SPINNING_MOMO_MAP__ = mapInstance; 12 maybeMountRuntime(); 13 notifyHostSessionReady(); 14 return mapInstance; 15 }; 16 innerL.Map.prototype = OriginalMapClass.prototype; 17 Object.assign(innerL.Map, OriginalMapClass); 18 innerL.Map.__SPINNING_MOMO_PATCH__ = true; 19 } 20 }, 21 configurable: true, 22 enumerable: true 23});

Source: bridgeScript.js

细节逐条解读(这些细节直接决定行为正确性):

  • 幂等保护:__SPINNING_MOMO_PATCHED__ 标记防止官方代码二次给 window.L 赋值(如热重载/分块加载)时重复包装。
  • 静态成员迁移:Object.assign(innerL.Map, OriginalMapClass) 把 L.Map 上的静态属性拷贝到包装器上,同时 prototype 指向原构造器的原型,保证 instanceof 与静态调用(如 L.Map.prototype 上的工具方法)不受影响。
  • 多重构造:__SPINNING_MOMO_MAP_CTOR_COUNT__ 计数器记录 Map 被构造的次数,后续 maybeMountRuntime 判断的引用始终取最新一次实例(window.__SPINNING_MOMO_MAP__ 被覆盖)。
  • 双向通知:捕获实例后立刻做两件事——maybeMountRuntime()(尝试用暂存的 markers/options 挂载运行时)与 notifyHostSessionReady()(向父窗口上报会话就绪与当前 worldId)。

会话就绪上报

javascript
1const notifyHostSessionReady = () => { 2 if (!window.parent || window.parent === window) { 3 return; 4 } 5 const worldId = readOfficialCurrentWorldId(); 6 window.parent.postMessage( 7 { 8 action: 'SPINNING_MOMO_MAP_SESSION_READY', 9 payload: worldId ? { worldId } : {}, 10 }, 11 '*' 12 ); 13};

Source: bridgeScript.js

注意 window.parent === window 的判断:脚本可能被错误注入到顶层文档,此时绝不能向自身 postMessage 造成自环。目标 origin 使用 '*' 是因为官方站点无法预知宿主 origin,真正的安全校验放在宿主侧入站时执行(见下文 isAllowedMapMessageOrigin)。

注入侧消息分发

注入脚本注册一个 message 监听器,处理三种动作:

javascript
1if (event.data.action === 'SPINNING_MOMO_SYNC_RUNTIME') { 2 const runtimePayload = event.data.payload || {}; 3 const markers = Array.isArray(runtimePayload.markers) ? runtimePayload.markers : []; 4 window.__SPINNING_MOMO_PENDING_MARKERS__ = markers; 5 setRenderOptions(runtimePayload.renderOptions || {}); 6 setClusterOptions(runtimePayload.runtimeOptions || {}); 7 maybeMountRuntime(); 8 return; 9} 10 11if (event.data.action === 'EVAL_SCRIPT') { 12 if (!window.__SPINNING_MOMO_ALLOW_DEV_EVAL__) { 13 return; 14 } 15 const scriptPayload = event.data.payload || {}; 16 if (typeof scriptPayload.script !== 'string' || scriptPayload.script.length === 0) { 17 return; 18 } 19 try { 20 const runScript = new Function(scriptPayload.script); 21 runScript(); 22 } catch (error) { 23 console.error('[SpinningMomo] Failed to eval dev script:', error); 24 } 25 return; 26} 27 28if (event.data.action === 'ADD_MARKER') { 29 const marker = event.data.payload; 30 if (!marker) { 31 return; 32 } 33 const pendingMarkers = window.__SPINNING_MOMO_PENDING_MARKERS__; 34 window.__SPINNING_MOMO_PENDING_MARKERS__ = [...pendingMarkers, marker]; 35 maybeMountRuntime({ flyToFirst: true }); 36}

Source: bridgeScript.js

三个分支的设计意图分别是:

动作触发方行为防御
SPINNING_MOMO_SYNC_RUNTIME宿主全量替换暂存 markers + 渲染选项 + 运行时选项,然后重挂载运行时markers 必须是数组,否则退化为 []
EVAL_SCRIPT宿主(仅 dev)用 new Function 执行任意脚本__SPINNING_MOMO_ALLOW_DEV_EVAL__ 为假时直接 return(生产版安全开关);脚本必须为非空字符串;执行异常只 console.error 不上抛
ADD_MARKER宿主追加单个 marker 到暂存数组并 flyToFirst: true(地图飞行到该点)payload 缺失时 return;用展开运算符创建新数组而非原地 push

其中 setRenderOptions 内的 normalizeRenderOptions(第 49–64 行)会检查 markerIconSize / markerIconAnchor 必须是长度恰为 2 的数组,否则置为 undefined,交给运行时使用默认值——这防止了畸形选项破坏图标尺寸计算。

核心控制流:宿主侧实现(useMapBridge)

useMapBridge 是前端宿主侧的桥接中枢,对外只暴露两个函数:postRuntimeSync(出站)与 handleMapMessage(入站)。

载荷构造:buildSerializableRuntimePayload

typescript
1function buildSerializableRuntimePayload( 2 mapStore: ReturnType<typeof useMapStore> 3): SyncRuntimePayload { 4 const markerIconSize = mapStore.renderOptions.markerIconSize 5 ? ([...mapStore.renderOptions.markerIconSize] as [number, number]) 6 : undefined 7 const markerIconAnchor = mapStore.renderOptions.markerIconAnchor 8 ? ([...mapStore.renderOptions.markerIconAnchor] as [number, number]) 9 : undefined 10 11 return { 12 markers: mapStore.markers.map((marker) => ({ 13 assetId: marker.assetId, 14 name: marker.name, 15 lat: marker.lat, 16 lng: marker.lng, 17 cardTitle: marker.cardTitle, 18 thumbnailUrl: marker.thumbnailUrl, 19 fileCreatedAt: marker.fileCreatedAt, 20 })), 21 renderOptions: { /* mapBackgroundColor, markerPinBackgroundUrl, markerIconUrl, 22 markerIconSize, markerIconAnchor, closePopupOnMouseOut, 23 popupOpenDelayMs, popupCloseDelayMs, keepPopupVisibleOnHover */ }, 24 runtimeOptions: { /* clusterEnabled, clusterRadius, hoverCardEnabled, markersVisible, 25 focusedAssetId, focusRequestId, currentWorldId, thumbnailBaseUrl, 26 clusterTitleTemplate, filterCountCard* (可见性/文案/加载态/配色) */ }, 27 } 28}

Source: useMapBridge.ts

设计意图有两点:

  1. 显式白名单序列化:不从 store 直接展开({...store}),而是逐字段映射。useMapStore 可能持有不可结构化克隆的引用(Vue proxy、函数、DOM 引用),而 postMessage 走结构化克隆算法,任何不可克隆值都会让整个 postMessage 抛异常。白名单同时保证 iframe 只拿到它需要的最小数据面。
  2. 数组拷贝:[...markerIconSize] 把 store 中的 reactive 数组复制为普通数组,避免克隆 Vue 的 Proxy 包装层。

出站:postRuntimeSync 的 dev/prod 双路径

typescript
1function postRuntimeSync() { 2 const contentWindow = mapIframe.value?.contentWindow 3 if (!contentWindow) { 4 return 5 } 6 7 const payload = buildSerializableRuntimePayload(mapStore) 8 const targetOrigin = import.meta.env.DEV ? '*' : MAP_ORIGIN 9 10 if (import.meta.env.DEV) { 11 const script = buildMapDevEvalScript(payload) 12 try { 13 contentWindow.postMessage( 14 { 15 action: ACTION_EVAL_SCRIPT, 16 payload: { script }, 17 }, 18 targetOrigin 19 ) 20 } catch (error) { 21 console.error('[MapBridge] Failed to post dev eval script:', error) 22 } 23 return 24 } 25 26 const message: SyncRuntimeMessage = { 27 action: ACTION_SYNC_RUNTIME, 28 payload, 29 } 30 try { 31 contentWindow.postMessage(message, targetOrigin) 32 } catch (error) { 33 console.error('[MapBridge] Failed to post runtime payload:', error) 34 } 35 }

Source: useMapBridge.ts

dev 路径存在的理由:注入脚本的正式形态是 C++ 头文件里的压缩产物,dev 迭代注入逻辑的成本极高。开发模式下宿主改为发送 EVAL_SCRIPT(内容由 buildMapDevEvalScript(payload) 动态生成),直接在官方页面里执行最新逻辑,无需重建原生工程。注意 dev 下 targetOrigin 放宽为 '*',而生产严格限定为 MAP_ORIGIN。

入站:来源校验与消息分发

typescript
1function isAllowedMapMessageOrigin(origin: string): boolean { 2 if (origin === MAP_ORIGIN) { 3 return true 4 } 5 if (import.meta.env.DEV && typeof window !== 'undefined' && origin === window.location.origin) { 6 return true 7 } 8 return false 9}

Source: useMapBridge.ts

这是双向协议中唯一真正生效的安全边界(注入侧的 '*' 上报依赖这里的校验):生产环境只接受来自 MAP_ORIGIN 的消息;dev 环境额外允许本地 dev server origin(window.location.origin),因为 dev 下 iframe 可能指向本地代理的官方地图镜像。

handleMapMessage 按动作分发,共五个入站动作:

typescript
1if (data.action === ACTION_MAP_SESSION_READY) { 2 const worldId = normalizeOfficialWorldIdOrDefault(data.payload?.worldId) 3 mapStore.patchRuntimeOptions({ 4 currentWorldId: worldId, 5 }) 6 mapStore.markIframeSessionReady() 7 flushMapRuntimeToIframe() 8 return 9 }

Source: useMapBridge.ts

ACTION_MAP_SESSION_READY 是时序闭环的关键:宿主在 iframe 就绪前推送的运行时可能因官方地图尚未构造而无法挂载,收到会话就绪消息后先回写 currentWorldId(经 normalizeOfficialWorldIdOrDefault 再规范一次),标记会话就绪,然后立刻主动重推一次完整运行时(flushMapRuntimeToIframe()),确保暂存数据被消费。

入站动作效果错误处理
ACTION_OPEN_GALLERY_ASSETNumber(...) 校验 assetId 有限后 router.push(buildGalleryLightboxRoute(assetId))try/catch 记录导航失败
ACTION_SET_MARKERS_VISIBLE校验 markersVisible 必须为 boolean 后 mapStore.patchRuntimeOptions + flushMapRuntimeToIframe()类型不符直接 return
ACTION_CLEAR_GALLERY_FILTERSgalleryStore.resetFilter()、includeSubfolders = true、void galleryData.refreshCurrentQuery()(fire-and-forget)无
ACTION_EXPORT_POLYGON先 await getInfinityNikkiMapConfig(),成功后 downloadPolygonJson(payload, mapConfig) 下载多边形 JSON配置不可用时 console.warn 并放弃导出;downloadPolygonJson 返回 false 时提示"需要合法 points 与 worldId"
ACTION_MAP_SESSION_READY见上无(normalizeOfficialWorldIdOrDefault 内部兜底)

双向消息协议总览

动作常量统一定义在 web/src/features/map/bridge/protocol.ts。下图给出完整时序:

Loading diagram...

构建期管线:注入脚本如何进入 WebView2

AGENTS.md 的生成器清单明确规定了维护流程:修改 web/src/features/map/injection/source/*.js 后必须执行

bash
node scripts/generate-map-injection-cpp.js

Source: AGENTS.md

该脚本把注入源码压缩并生成对应的 C++ 头文件,原生 WebView 宿主在文档创建时执行其中的脚本内容(原生侧如何调度注入属于 WebView 桥页面范畴,此处不展开)。注入源目录包含多个协作片段,由 bridgeScript.js 组装为一个模板:

片段作用(依据代码可验证部分)
bridgeScript.js模板骨架:门控、worldId 读取、L 劫持、消息分发、__ALLOW_DEV_EVAL__ 占位符
iframeBootstrap.js经 buildIframeBootstrapSnippet() 拼入模板
runtimeCore.js提供 mountOrUpdateMapRuntime(标记渲染核心),经 buildRuntimeCoreSnippet() 拼入模板
cluster.js / paneStyle.js聚合与样式支撑片段(随模板一并压缩)
devEvalRuntimeScript.js / devPolygonCollector.jsdev 评估与多边形采集辅助
index.d.ts片段构建器的类型声明
mapDevEvalScript.ts(injection/ 根)dev 模式下 buildMapDevEvalScript(payload) 生成 EVAL_SCRIPT 脚本体

失败模式、边界条件与并发

失败模式

场景系统行为源码依据
注入到非官方域名整段脚本被 hostname 门控跳过,不产生任何副作用bridgeScript.js 第 6 行的 if 包裹全部逻辑
worldId 读取异常(localStorage 禁用 / JSON 损坏 / 格式非法)回退 DEFAULT_WORLD_ID = '1.1',注入流程不中断readOfficialCurrentWorldId 的 try/catch 与 WORLD_ID_PATTERN
window.L 从未被赋值(官方改版/加载失败)maybeMountRuntime 因 !map || !L 直接 return;宿主收不到会话就绪消息,运行时保持暂存状态不挂载bridgeScript.js 第 92–97 行守卫
官方代码多次给 window.L 赋值__SPINNING_MOMO_PATCHED__ 防止重复包装;仅刷新 innerL 引用第 113 行条件
SYNC_RUNTIME 载荷畸形(markers 非数组 / renderOptions 非对象)注入侧规范化:非数组→[],非对象→{},markerIconSize 长度≠2→undefined第 140 行、normalizeRenderOptions 第 49–64 行
EVAL_SCRIPT 在生产版被调用__SPINNING_MOMO_ALLOW_DEV_EVAL__ 为假,静默 return(安全开关:生产头文件里占位符被替换为 false)第 149–151 行
脚本执行抛错console.error 后吞掉,不向宿主上抛,不中断后续 message 分发第 158–163 行
宿主 postMessage 结构化克隆失败try/catch + console.error('[MapBridge] Failed to post runtime payload:')useMapBridge.ts 第 128–132 行
多边形导出时地图配置不可用console.warn 并放弃导出(不下载空文件)useMapBridge.ts 第 180–184 行
多边形点集或 worldId 非法downloadPolygonJson 返回 false,仅警告第 186–188 行

时序与并发边界

  • 双向异步时序解耦:宿主的 postRuntimeSync 可能在 iframe 会话就绪之前发出(此时 mountOrUpdateMapRuntime 尚不可用)。解决方案是"暂存 + 就绪重推":SPINNING_MOMO_SYNC_RUNTIME 先写入 __SPINNING_MOMO_PENDING_MARKERS__ 等暂存区,等 L.Map 构造触发 maybeMountRuntime() 消费;反向地,宿主收到 ACTION_MAP_SESSION_READY 后主动 flushMapRuntimeToIframe() 重推一次,双向各补一次以覆盖任意到达顺序。
  • 不可变更新暂存数组:ADD_MARKER 使用 [...pendingMarkers, marker] 生成新数组引用,避免运行时持有旧引用时读到脏数据;这也意味着每次 SYNC_RUNTIME 是全量快照同步而非增量——设计上以简单性换取一致性,由宿主侧节流/防抖(flushMapRuntimeToIframe 统一调度)控制频率。
  • 多次 Map 构造:__SPINNING_MOMO_MAP_CTOR_COUNT__ 记录构造次数,__SPINNING_MOMO_MAP__ 始终指向最新实例,maybeMountRuntime 因此天然跟随官方页面重建的地图实例。
  • dev origin 放宽:isAllowedMapMessageOrigin 在 import.meta.env.DEV 下额外放行 window.location.origin,这是 dev 双路径的安全配套;生产构建中该分支被静态消除。

性能与运维要点

  • 全量 postMessage 快照:buildSerializableRuntimePayload 每次 map 出完整 markers 数组(7 字段白名单)。标记数量大时这是主要开销点,因此宿主侧经由 flushMapRuntimeToIframe() 集中调度而非每次 store 变化都直发。
  • iframe 内渲染发生在注入侧:聚类(cluster.js)、悬停卡片延迟(popupOpenDelayMs / popupCloseDelayMs / closePopupOnMouseOut / keepPopupVisibleOnHover)、聚合开关(clusterEnabled / clusterRadius)等均在 iframe 进程内消费,不占用宿主主线程——这是把渲染选项放进 runtimeOptions/renderOptions 通道的原因。
  • 运维提醒:AGENTS.md 明确要求修改 injection/source/*.js 后必须重跑 generate-map-injection-cpp.js,否则原生二进制中仍是旧版脚本(dev 下 EVAL_SCRIPT 路径不受此影响)。
  • CSP/iframe 兼容性:注入采用 new Function(dev 路径)与内联执行,依赖宿主 WebView2 对注入脚本的执行许可;官方站点 CSP 变化不影响注入(注入发生在原生层),但可能影响 postMessage 的来源 origin 值,需要同步更新 MAP_ORIGIN。

扩展点

  1. 新增入站动作:在 web/src/features/map/bridge/protocol.ts 定义动作常量与载荷类型 → 注入侧 bridgeScript.js 的 message 监听器加分支(含防御性校验)→ useMapBridge.handleMapMessage 加分发分支 → 重跑生成脚本。
  2. 新增渲染/运行时选项:扩展 SyncRuntimePayload 的 renderOptions/runtimeOptions 字段(buildSerializableRuntimePayload 加字段)→ runtimeCore.js 的 mountOrUpdateMapRuntime 消费 → 同样需要重新生成头文件。
  3. 新增注入片段:在 injection/source/ 新增模块并从 bridgeScript.js 的模板中以 snippet 函数拼入(参照 buildIframeBootstrapSnippet / buildRuntimeCoreSnippet 的既有模式),保持片段自包含、无外部依赖。
  4. dev 快速迭代:利用 EVAL_SCRIPT 通道与 mapDevEvalScript.ts,无需重建原生工程即可在真实官方页面上试验新注入逻辑。
  • 源码:bridgeScript.js — 注入脚本模板(门控、L 劫持、消息分发)
  • 源码:useMapBridge.ts — 宿主侧桥接(载荷构造、双向消息)
  • 源码:protocol.ts — 消息协议常量与类型定义
  • 源码:AGENTS.md — 生成器维护流程与整体双进程架构说明
  • 关联主题(兄弟页面):地图场景编排(MapIframeHost.vue、useMapScene.ts、mapIframeRuntime.ts)、标记与坐标域(domain/)、WebView/RPC 桥、图库(gallery)

Sources

(2 files)
web/src/features/map/composables
web/src/features/map/injection/source