地图注入:游戏内地图与前端桥接
地图注入(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。为了让用户的截图/资产标记出现在官方地图上,本能力采取的策略是:
- 构建期:
scripts/generate-map-injection-cpp.js会把web/src/features/map/injection/source/*.js中的注入源码压缩并生成 C++ 头文件(AGENTS.md的生成器清单明确要求修改注入源后必须重新执行该脚本)。 - 运行期(注入):原生 WebView 宿主在文档创建时执行注入脚本。脚本首先做 hostname 门控(只对
myl.nuanpaper.com生效),随后通过Object.defineProperty劫持全局window.L(Leaflet),在官方代码给window.L赋值时包装L.Map构造器,从而捕获地图实例并挂载自定义运行时。 - 运行期(桥接):注入脚本与父窗口(Vue 宿主)之间通过
postMessage交换结构化消息。宿主侧useMapBridge在 会话就绪 后把markers/renderOptions/runtimeOptions推入 iframe;iframe 内的mountOrUpdateMapRuntime(来自runtimeCore片段)负责真正在 Leaflet 地图上渲染标记、聚合与悬停卡片。 - 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
架构要点(每一处都对应真实代码):
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 门控——只对官方地图域名生效,避免该脚本在任何其他页面执行:
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 中读取:
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 拦截这次赋值:
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)。
会话就绪上报
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 监听器,处理三种动作:
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
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
设计意图有两点:
- 显式白名单序列化:不从 store 直接展开(
{...store}),而是逐字段映射。useMapStore可能持有不可结构化克隆的引用(Vue proxy、函数、DOM 引用),而postMessage走结构化克隆算法,任何不可克隆值都会让整个postMessage抛异常。白名单同时保证 iframe 只拿到它需要的最小数据面。 - 数组拷贝:
[...markerIconSize]把 store 中的 reactive 数组复制为普通数组,避免克隆 Vue 的 Proxy 包装层。
出站:postRuntimeSync 的 dev/prod 双路径
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。
入站:来源校验与消息分发
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 按动作分发,共五个入站动作:
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_ASSET | Number(...) 校验 assetId 有限后 router.push(buildGalleryLightboxRoute(assetId)) | try/catch 记录导航失败 |
ACTION_SET_MARKERS_VISIBLE | 校验 markersVisible 必须为 boolean 后 mapStore.patchRuntimeOptions + flushMapRuntimeToIframe() | 类型不符直接 return |
ACTION_CLEAR_GALLERY_FILTERS | galleryStore.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。下图给出完整时序:
构建期管线:注入脚本如何进入 WebView2
AGENTS.md 的生成器清单明确规定了维护流程:修改 web/src/features/map/injection/source/*.js 后必须执行
node scripts/generate-map-injection-cpp.jsSource: 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.js | dev 评估与多边形采集辅助 |
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。
扩展点
- 新增入站动作:在
web/src/features/map/bridge/protocol.ts定义动作常量与载荷类型 → 注入侧bridgeScript.js的message监听器加分支(含防御性校验)→useMapBridge.handleMapMessage加分发分支 → 重跑生成脚本。 - 新增渲染/运行时选项:扩展
SyncRuntimePayload的renderOptions/runtimeOptions字段(buildSerializableRuntimePayload加字段)→runtimeCore.js的mountOrUpdateMapRuntime消费 → 同样需要重新生成头文件。 - 新增注入片段:在
injection/source/新增模块并从bridgeScript.js的模板中以 snippet 函数拼入(参照buildIframeBootstrapSnippet/buildRuntimeCoreSnippet的既有模式),保持片段自包含、无外部依赖。 - dev 快速迭代:利用
EVAL_SCRIPT通道与mapDevEvalScript.ts,无需重建原生工程即可在真实官方页面上试验新注入逻辑。
Related Links
- 源码:bridgeScript.js — 注入脚本模板(门控、L 劫持、消息分发)
- 源码:useMapBridge.ts — 宿主侧桥接(载荷构造、双向消息)
- 源码:protocol.ts — 消息协议常量与类型定义
- 源码:AGENTS.md — 生成器维护流程与整体双进程架构说明
- 关联主题(兄弟页面):地图场景编排(
MapIframeHost.vue、useMapScene.ts、mapIframeRuntime.ts)、标记与坐标域(domain/)、WebView/RPC 桥、图库(gallery)