在大概10年多前,我就已经在使用谷歌足迹地图,最早是在使用谷歌地球中觉得精确、细节拉满的谷歌地球很适合找到一些自己去过的地方并且标点记录。之后我在我的博客的“关于”页面,通过链接谷歌足迹地图的方式,在我的博客实现查看我的足迹。
1 | <iframe src="https://www.google.com/maps/d/embed?mid=1FnQqdlXYFoEnO6WvZ8QEHj9RYHg" width="100%" height="600"></iframe> |
但使用上也有一些痛点。例如谷歌地图一直被强,我更新足迹需要总是用梯子,所以更新频次很少。其次是谷歌卫星地图和地图道路、点位等信息在国内的部分有坐标偏差,查看起来很难看。经常我一个点位不知道按卫星地图给标记还是按街道位置进行标记。近几年,我也断断续续用过其他方案,例如百度地图或者高德收藏夹存地点。但每次想回顾”我去过哪些地方、在那里发生过什么”的时候,数据总是散落在各个 App 里,我要打开app去翻找,宁琅满目各种广告、贷款推荐,慢得很。
我想要的东西其实很简单:一张属于我自己的地图,每个点位是一段记忆,能写文字、能贴图、能按时间连成路线,数据永远在我手里。
于是在AI的协助讨论和开发下,就有了这个项目——一个嵌入 Hexo 博客的纯前端足迹地图。这篇文章记录它从需求讨论到上线的完整过程,包括踩过的三个很有代表性的坑,以及留给以后的待办事项。
一、需求对齐:和元宝的两轮讨论
这个项目我没有直接让 AI 上来就写代码,而是先找了元宝聊需求。这一步后来被证明是最值得的投入。
我和元宝从”我想记录人生足迹”这个模糊的念头开始聊,逐步收敛出核心功能:地图标记、分类体系(路程/好吃的/好喝的/好玩的/风景/兴趣点)、Markdown 图文弹窗、按时间排序的路线模式、分组筛选、数据导入导出。
聊完之后,元宝帮我产出了两份文档,这个分工很有讲究:
| 文档 | 面向对象 | 侧重点 |
|---|---|---|
| 需求与功能设计文档 | 人类阅读 | “做什么”和”为什么”,交互规则 |
| AI 开发者移交文档 | AI 开发者 | 数据模型、优先级(P0/P1/P2)、验收清单、开发约束 |
第二份文档细到什么程度?marker 的 id 格式(m_{毫秒时间戳}_{随机4位})、时间格式必须是 YYYY-MM-DD HH:mm、路线折线的颜色 #e74c3c 和虚线样式 dashArray: '8,8'、一键展开弹窗最多 50 个、localStorage 的键名叫 mapData——全部写得明明白白。
为什么这么啰嗦?因为 AI 开发最怕的不是复杂需求,而是模糊需求。文档越精确,AI 一次成型的概率越高,来回扯皮的次数越少。有的时候和ai一起开发或者写文档,与ai交付来回多了,ai反而容易忘记最开始重要的东西,导致中途开发的已经接近完美的产品反而被毁了。
开发移交文档里还专门参考了 MarkdownMap、leaflet-search、Exping 三个项目的架构思路(只借鉴不抄代码)。
核心原则也在这个阶段定死了:
- 纯前端:无后端、无数据库,静态托管就能跑
- 数据自主:所有数据是 JSON 文件,随时导出,永不锁定
- 单文件交付:CSS 和 JS 全部内联进一个
index.html,扔进source/map/就能用
二、开发实施:和 WorkBuddy 的六轮迭代
文档交给 WorkBuddy 后,开发过程总共出了六个版本。我把迭代过程整理成一张表:
| 版本 | 主要内容 | 性质 |
|---|---|---|
| v1 | 按移交文档实现全部功能 | 初版 |
| v2 | 外部依赖内联化,提高可用性 | 优化 |
| v3 | 数据结构调优 | 优化 |
| v4 | 修复”弹窗关闭后无法再打开” | 修坑 |
| v5 | 搜索下拉遮挡修复 + 回车选中 + OSM/Carto 底图 | 修坑 + 衍生 |
| v6 | 图层循环切换改为下拉选择器 + Esri 卫星底图 | 衍生需求 |
初版(v1~v3)出乎意料地顺利——移交文档写得细,AI 基本一次成型,验收清单里绝大部分条目直接通过。真正花时间的是后面三轮,属于是测试bug,也是在测试使用中评估是否满足使用人的需求。
这也验证了我的一个开发习惯:AI 写的代码,”能用”和”好用”之间隔着真实使用这一关。 每一版我都在自己的浏览器里实际操作一遍,把问题截图、描述复现步骤,再交给 AI 定位修复。
三、踩坑记录:三个有代表性的问题
这部分是这篇文章技术含量最高的地方,三个坑各自代表一类典型问题。
坑 1:弹窗关闭后无法再打开(v4)
现象:点击地图点位,弹窗正常弹出。点弹窗右上角 ✕ 关闭后,再点同一个点位,弹窗死活不出来了。刷新页面,又能打开一次。
这个 bug 特别有迷惑性——“第一次能开”说明打开逻辑没问题,”关了就开不了”说明关闭操作把什么东西弄脏了。
根因挖到了 Leaflet 源码层面。Leaflet 的 Marker._openPopup 内部有一个切换逻辑:
1 | _openPopup: function (t) { |
AI自定义编写的 click 处理器和 Leaflet 内部的这个处理器同时在监听点击。代码先执行 openPopup 成功,Leaflet 内部处理器随后执行,发现”弹窗已打开”,于是把它关了——刚打开的弹窗瞬间被关掉。而关闭时用的 closePopup() 只是置空引用、移除 DOM,map._layers 里还残留着弹窗对象,导致下次判断”已打开”永远成立。
修复分两刀:
mk.off('click', mk._openPopup)——移除 Leaflet 内部的切换处理器,点击行为完全由我们自己的代码接管- 关闭弹窗改用
map.removeLayer(popup)——彻底从图层里清除,不留残留
教训:用第三方库时,它的”便利行为”(内部自动监听、toggle)和你的自定义逻辑会打架。出了灵异 bug,别猜,去读库的源码。
坑 2:搜索下拉框被地图”挡住”(v5)
现象:搜索输入地点名称,明明输入法联想都有,下拉结果就是不显示。
第一反应是 z-index 不够高,把下拉框层级提到 2000 还是看不见。这就诡异了——直到意识到问题根本不在 z-index:
父容器 #toolbar 设了 overflow-y: hidden,子元素 z-index 再高也会被父容器裁剪掉。 这是 CSS 的层叠上下文规则:溢出裁剪发生在父级,子级的 z-index 只在和兄弟元素竞争时有用。
修复方案是”搬家”:
- 初始化时把
#searchResults从工具栏里移到document.body下 - 改用
position: fixed脱离文档流 - 用
getBoundingClientRect()实时读取输入框位置,动态计算下拉框坐标,同时监听resize和scroll跟随
顺手把回车键也绑上了:输入后按回车直接选中第一条结果,不用非得动鼠标。
教训:z-index 失效九成不是数字不够大,而是层叠上下文和溢出裁剪在父级就把你拦住了。
坑 3:谷歌卫星瓦片的”假成功”(v6)
现象:想加国外卫星图,先试了谷歌瓦片。curl 一测,HTTP 200,有响应——看起来通了。但地图上就是一片空白。
仔细一看响应内容:只有 1.7KB,是一张 1×1 像素的占位图。 HTTP 状态码是”成功”的,但返回的根本不是瓦片图片。这是国内访问谷歌瓦片服务的典型拦截方式——不给你报错,给你返回一张空白图,让你以为是自己代码的问题。
换用 Esri World Imagery:
1 | https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x} |
curl 实测:HTTP 200,响应 14.8KB,耗时 0.47 秒,国内直连,免费无 Key。真瓦片,全球覆盖,maxZoom 19。
教训:验证外部资源可用性,不能只看状态码,要看响应内容的大小和类型是不是符合预期。”假成功”比明确的失败更坑人。
四、衍生需求:用着用着冒出来的
有三个功能不在最初的需求文档里,全是实际使用中长出来的:
- 国外底图。原本用高德瓦片,国内很清晰,但我想标记国外的点,一放大全是空白。于是加了 OSM 和 Carto 浅色两种国外底图,再加 Esri 卫星,凑齐了”国内+国外、标准+卫星”的完整组合。
- 图层下拉选择器。底图从 2 种涨到 5 种之后,原来的”循环切换”按钮变得很蠢——想要 Esri 卫星得连按四下。改成下拉选择器,一步到位。
- 搜索回车选中。发现下拉结果被遮挡的过程中,顺带发现自己习惯性按回车,而原实现只支持鼠标点击。
需求文档写得再全,也覆盖不了真实使用中冒出来的体验问题。迭代的方向盘,永远握在实际用的人手里。
五、隐私考量:一个暂缓的决定
上线前我想到一个问题:这个页面不需要登录,我自己看很方便,但别人也同样方便地能看到我的全部足迹——我去过哪、什么时候去的、写了什么。
我原本在两个方案间犹豫:给页面加个密码锁,还是让页面正常访问但数据加载要验证。但和 AI 讨论后,评估这两个都不行:静态托管没有后端,任何”前端验证密码”都只是遮羞布——绕过页面直接访问 data.json 的 URL,明文数据照样下载。
真正有效的方案是第三种:把 data.json 本身用 AES-256-GCM 加密,密码只存在于我脑子里。浏览器端复杂解密渲染和加密导出,别人就算下载到文件也只是一堆乱码。用 Web Crypto API 就能实现,零后端。更进一步,不一定是所有的地图点位、足迹信息都是隐私的信息。也可以,以后让这个网页支持加密和明文地图点位数据导入,支持加密或明文数据导出。这样,这个网页既可以私用,也可以公用。或者让自己推荐的点位默认明文载入展示,而自己私密的地图点位数据采用加密,而在导入时候需要提供解密密码。
这个方案已敲定但暂缓实施——因为目前线上 data.json 里的点位是空的,我的真实数据还在本机 localStorage 里,暂时没有隐私可泄露。等我真的要把足迹数据发布上去做跨设备同步时,再回来做加密。
六、上线部署
部署方式简单粗暴:把 source/map/ 整个文件夹(index.html + data.json + README.md,共 3 个文件)复制到正式仓库,git push,CNB 云原生构建自动完成后续一切。
上线后访问 https://misaka10013.cn/map/。
整个项目的技术栈,最后总结成一句话:Leaflet.js 管地图,marked.js 管 Markdown 渲染,localStorage 管持久化,原生 JS + 内联 CSS 管一切交互,零框架零后端零构建。
七、待议与待办(给未来的自己)
把未完成的事项记在这里,以后翻到这篇文章就知道下一步往哪走:
| 事项 | 说明 | 优先级 |
|---|---|---|
| data.json 加密 | AES-256-GCM + PBKDF2 方案已敲定,客户端解密渲染;导出备份同样加密 | 高(发布真实数据前必须做) |
| 跨设备数据同步 | 目前真实数据只在本机 localStorage;换设备需”导出 JSON → 替换 data.json → push”手动流程 | 中 |
| 地图页入口 | 目前无导航/文章链接指向 /map/,纯 URL 直达(算是一层隐蔽) | 低 |
| 已知小瑕疵 | 第二次点击点位时 Leaflet 的 _stopped 置位,导致 map 的 click(收起搜索下拉/筛选面板)不触发,不影响核心功能 |
低 |
| 图片图床 | 点位照片计划用图床 URL,需选定稳定图床 | 中 |
后记
复盘整个项目,我的流程是:元宝做需求收敛和文档产出,WorkBuddy 做编码实施,我做真实环境测试和方向决策。 三个角色里,AI 承担了两块,而指引方向和评估质量的”方向盘”始终在我手这——哪个问题优先修、需求往哪个方向衍生、隐私做到什么程度,这些都是拍板题,不是执行题。这大概也是为啥现在很多人用AI协同开发或输出内容,会觉得很有动力。
“两段式文档”(人类需求文档 + AI 移交文档)和”版本化备份”(每个版本存档 map_versions/)这两个习惯,以后的项目会继续沿用。