From 7e70c18f7ed4c0ba80db7eeef12ccd5e913231c4 Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Thu, 25 Jun 2026 15:58:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9D=A2=E8=AF=95=E9=A2=98=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REASONIX.md | 117 ++++ docs/AMap面试题.md | 732 +++++++++++++++---------- docs/interview-question-route-track.md | 279 ++++++++++ 3 files changed, 853 insertions(+), 275 deletions(-) create mode 100644 REASONIX.md diff --git a/REASONIX.md b/REASONIX.md new file mode 100644 index 0000000..9798663 --- /dev/null +++ b/REASONIX.md @@ -0,0 +1,117 @@ +# REASONIX.md + +## 项目概述 + +**microapp-vue3** — 基于 Vue 3 + TypeScript + Vite 的微前端子应用,使用 `@micro-zoe/micro-app` 框架集成到主应用中。同时集成了高德地图 JSAPI,提供地图展示、地理围栏、路径轨迹等功能。 + +- **类型**: 前端微应用(子应用) +- **运行环境**: 浏览器(支持独立运行 & micro-app 微前端环境) +- **默认端口**: `3001` + +--- + +## 技术栈 + +| 类别 | 技术 | 版本 | +|------|------|------| +| 框架 | Vue 3 (Composition API) | ^3.5 | +| 语言 | TypeScript | ~6.0 | +| 构建 | Vite | ^8.0 | +| 路由 | Vue Router | ^4.6 | +| 微前端 | @micro-zoe/micro-app | ^1.0.0-rc.31 | +| 地图 | @amap/amap-jsapi-loader | ^1.0.1 | + +--- + +### 环境变量 + +在 `.env` 中配置高德地图 Key(以 `VITE_` 为前缀暴露给客户端): + +``` +VITE_AMAP_JSAPI_KEY= +VITE_AMAP_WEB_KEY= +VITE_AMAP_SECURITY_CODE= +``` + +--- + +## 架构设计 + +### 微前端生命周期 + +`src/main.ts` 中实现了 `mount()` / `unmount()` 两个生命周期函数: + +- **微前端环境** (`window.__MICRO_APP_ENVIRONMENT__` 为 truthy):导出 `{ mount, unmount }` 到 `window['micro-app-']`,由 micro-app 框架调用 +- **独立运行**:直接调用 `mount()` 挂载到 `#microapp-vue3-root` + +### 路由 + +- 使用 `createWebHistory`,`baseroute` 动态适配: + - 微前端环境:使用 `window.__MICRO_APP_BASE_ROUTE__` + - 独立运行:使用 `/` +- 所有路由均使用懒加载 (`() => import(...)`) +- 挂载点使用独立 id `#microapp-vue3-root`(避免与主应用 `#app` 冲突) + +### 跨应用通信 + +通过 micro-app 的 `globalData` 机制实现: + +- **发送**: `window.microApp.setGlobalData({ key: value })` +- **接收**: `window.microApp.addGlobalDataListener(callback, autoTrigger)` +- 示例见 `src/views/Home.vue` 中的共享计数器 + +### 高德地图集成 + +- **单例加载**: `loadAMap()` 确保 JSAPI 全局只加载一次,所有地图实例共享 +- **Composable 模式**: `useAmap()` 返回 `containerRef`、`mapInstance`、`loading`、`error`、`initMap`、`destroyMap` +- **配置集中管理**: 所有 AMap 配置(Key、版本、插件列表)在 `src/config/amap.ts` 中统一维护 +- **MapPanel 组件**: 每个 MapPanel 有独立的 DOM 容器和地图实例,可多个同时存在 + +--- + +## 编码规范 + +### Vue 组件 + +- 使用 ` ``` -**JSAPI 下载次数:1 次。** +### 5. 关键细节 -``` -页面 A 初始化 页面 B 初始化 - │ │ - ├─ loadAMap() ├─ loadAMap() - │ └─ AMapGlobal 为空 │ └─ AMapGlobal 已有 → 直接返回 ✅ - │ └─ AMapLoader │ (不下载,不请求,O(1) 时间) - │ .load() ↓ │ - │ (仅此一次网络请求) │ - │ │ - ├─ new AMap.Map( ├─ new AMap.Map( - │ containerA, ...) │ containerB, ...) ← 不同 DOM 容器 - │ │ -``` - -**核心结论:`loadAMap()` 的单例只管 SDK 脚本加载,不管地图实例创建。** 每个页面用 `useAmap()` 拿到自己的 `containerRef`(不同的 DOM 元素),地图实例互不干扰。 - ---- - -### 3. 页面切换时的完整生命周期 - -假设有 **页面 A**(Home 地图)和 **页面 B**(About 地图),用户通过 `` 从 A 导航到 B: - -``` -时间线 ──────────────────────────────────────────────────────▶ - - ┌── 页面 A 活跃 ──┤ 路由切换 ├── 页面 B 活跃 ──┤ - │ │ - A 组件: │ unmounted │ 💀 已销毁 - mapInstance ────┤ destroy() │ - containerRef ───┤ │ - onUnmounted ────┘ │ - │ - B 组件: │ mounted │ - containerRef ───┤ 赋值 │ ← 新的 DOM 元素 - initMap() ──────┤ │ - loadAMap() ───┤ 缓存命中 │ ← 不重新下载 JSAPI - new Map() ────┤ │ ← 挂载到 B 的 containerRef - mapInstance ────┤ │ - - JSAPI 全局: ─────┼────────────┼────────────────── - AMapGlobal │ 常驻内存 │ -``` - -**每一步的具体机制:** - -| 步骤 | 触发 | 发生了什么 | -|------|------|-----------| -| ① 旧地图销毁 | `onUnmounted()` → `destroyMap()` | `mapInstance.value.destroy()` 释放 WebGL 上下文、事件监听、DOM 节点 | -| ② 旧组件销毁 | Vue 响应式系统 | `mapInstance`、`containerRef` 等响应式变量随组件实例一起被 GC | -| ③ 新组件挂载 | `onMounted()` → `initMap()` | `containerRef.value` 已被 Vue 模板引擎绑定到新的 DOM 元素 | -| ④ loadAMap 缓存命中 | `initMap()` 内部调用 `loadAMap()` | `AMapGlobal` 非空 → 直接返回,不需要网络请求 | -| ⑤ 新地图创建 | `new AMap.Map(新容器, options)` | AMap 在新 DOM 元素内创建新的 WebGL 画布 | - -**关键保证:** - -- ✅ **JSAPI 只在首次访问任意地图页面时下载一次**,后续切换页面瞬间返回 -- ✅ **每次切换页面都会销毁旧地图、创建新地图**,互不干扰 -- ✅ **不会出现"地图挂到错误容器"的问题**,因为每个 `useAmap()` 有自己的 `containerRef` 闭包 +- **安全密钥时序**:`_AMapSecurityConfig` 必须在 `AMapLoader.load()` 之前设置,否则 Driving / Geocoder 等服务报 `INVALID_USER_SCODE` +- **flex 布局修正**:地图创建后调用 `map.resize()`,因为 flex 分配的尺寸可能尚未生效 +- **`%` 而非 `vh`**:容器用 `height: 100%` 逐级继承,适配微前端宿主不一定是全视口的场景 --- ## 总结一句话 -> **`loadAMap()` 的全局单例只缓存 JSAPI 脚本,不缓存地图实例 —— JSAPI 下载一次,地图实例按页面创建/销毁,各用各的 DOM 容器,切换页面自动清理旧实例。** +> **配置文件集中管理 Key/插件 → `loadAMap()` 全局单例确保 SDK 只下载一次 → `useAmap()` 封装实例生命周期,页面只需绑定 `containerRef` 并调用 `initMap()`。** + + +--- + +## Q2:多个地图页面复用时有哪些性能陷阱?如何解决? + +--- + +## 题目 + +你的应用中有 3 个页面各自包含高德地图,用户在不同页面间切换。请分析: +1. **JSAPI 脚本会重复下载吗?如何保证只下载一次?** +2. **`mapInstance` 为什么用 `shallowRef` 而不是 `ref`?从性能和副作用两个角度说明。** +3. **页面切换时旧地图实例如何清理?画出关键生命周期。** + +--- + +## 参考答案 + +### 1. JSAPI 脚本只下载一次 + +`loadAMap()` 通过 `AMapGlobal` 缓存已加载结果。页面 A 首次调用时发起 `AMapLoader.load()`,页面 B/C 调用时直接命中缓存返回,不产生额外网络请求。 + +**进阶:`AMap` 与 `mapInstance` 的本质区别** + +| 对象 | 本质 | 作用 | 数量 | +|------|------|------|------| +| **`AMap`** | 全局命名空间 (SDK) | 提供构造函数(如 `new AMap.Marker`)和工具方法 | 全局唯一 | +| **`mapInstance`** | 地图实例 (Instance) | 具体的地图对象,负责渲染、缩放、事件监听 | 每个容器一个 | + +> **面试官提问:既然已经有了 `mapInstance`,为什么在点击事件处理函数中还要重新调用 `await loadAMap()`?** +> +> **回答:** 因为 `mapInstance` 只是一个渲染好的地图“窗口”,它不包含创建新零件(如 Marker 或 Polyline)的“工厂工具”。为了在点击位置创建新的 Marker,必须通过 `loadAMap()` 获取 `AMap` 这个构造函数库。由于 `loadAMap` 做了单例缓存,这种重复调用是 O(1) 级别的,既保证了代码的健壮性(确保 SDK 已加载),又不会产生额外的网络开销。 + +页面切换时的关键流程: + +``` +页面 A 活跃 → 页面 B 挂载 + │ + ├─ A.onUnmounted → destroyMap() + │ └─ mapInstance.value.destroy() // 释放 WebGL 上下文、事件监听 + │ + └─ B.onMounted → initMap() + ├─ loadAMap() → 命中 AMapGlobal 缓存(不下载) + └─ new AMap.Map(containerB, options) // 全新实例挂到新 DOM +``` + +**核心结论**:SDK 全局常驻,地图实例按页面创建/销毁,互不干扰。 + +### 2. 为什么 `mapInstance` 必须用 `shallowRef`? + +| 维度 | `shallowRef` ✅ | `ref` ❌ | +|------|-----------------|----------| +| **性能** | 仅追踪 `.value` 整体替换,初始化为 O(1) | 递归代理整个 AMap.Map 对象树(数百个属性/WebGL 上下文),阻塞主线程 | +| **副作用** | 不劫持属性访问,AMap 内部 `this` 指向不变 | Proxy 改变 `this` 指向,可能破坏地图渲染循环、事件系统 | +| **语义** | 代码中只需要 `mapInstance.value = new Map()` / `.destroy()` / `= null`,不关心内部属性变化 | 完全用不到深度追踪能力 | + +**一句话**:`shallowRef` 用于"只关心对象整体是否被替换,不关心内部怎么变"的第三方复杂实例。 + +### 3. 页面切换时的完整生命周期 + +| 步骤 | 时机 | 动作 | +|------|------|------| +| ① 旧地图销毁 | `onUnmounted` → `destroyMap()` | `map.destroy()` 释放画布、事件、DOM | +| ② 响应式清理 | Vue 组件卸载 | `mapInstance`、`containerRef` 随组件 GC | +| ③ 新地图创建 | `onMounted` → `initMap()` | `containerRef` 绑定新 DOM → `new AMap.Map(新容器)` | +| ④ SDK 缓存命中 | `initMap()` 内部 `loadAMap()` | `AMapGlobal` 非空,O(1) 返回 | + +--- + +## 总结一句话 + +> **SDK 全局单例只下载一次,地图实例按页面生灭,`shallowRef` 避免深度代理第三方对象 —— 三者配合才能让多页面地图既快又稳。** + + +--- + +## Q3:如何在高德地图上添加和管理 Marker? + +--- + +## 题目 + +在一个电子围栏应用中,你需要在地图上添加多种标记:老人位置(👴,可拖拽)、护理员位置(👩‍⚕️,程序控制移动)、起点/终点(📍/🏁,静态)、围栏顶点(序号圆点,绘制完成后清除)。请回答: + +1. **Marker 的基本创建流程是什么?`content` 和 `offset` 的作用?** +2. **不同类型的 Marker 在"创建/更新/销毁"策略上有什么不同?为什么?** +3. **为什么 Marker 的 CSS 样式不能写在 Vue 的 ` +``` + +通过 **业务前缀命名**(如 `geofence-`)来隔离,而非依赖 Vue 的 scoped 机制。 + +--- + +## 总结一句话 + +> **Marker 通过 `content` + `offset` 实现自定义 UI;静态标记复用 `setPosition`,临时标记批量清理,样式用全局 `