“慧视“导航功能开发博客 1:先把“能跑起来“做稳,再谈智能
这篇写的是导航功能的第一阶段。我一开始最想做的就是那种酷炫的导航页——地图上画条线,实时显示位置,最好还能语音播报。但实现的时候才发现,地图、定位、后端会话、实时推送这几块根本不是拼在一起就能跑的。我没有一个清晰的项目结构,也不知道数据该从哪流到哪。
所以第一阶段我强迫自己收回来,先不做花哨的东西,只做一件事:前端有页面,后端有接口,会话有状态,数据能来回跑。这听起来没什么技术含量,但我后来发现,后面所有功能——地图渲染、实时播报、偏航判断——能不能顺利接上,全看这一步拆得清不清楚。
一、导航功能为什么不能只写一个页面
最初我觉得导航页就是"输入目的地 → 地图画路线 → 开始导航",一个页面搞定。真动手之后才发现它至少得拆四层:
- 页面层:目的地输入、定位、状态显示、停止导航
- 业务层:启动导航、更新位置、偏航判断、到达判断
- 数据层:会话持久化、设置管理、路线和当前位置
- 实时层:WebSocket 推送,失败后要能降级到轮询
一开始我没拆,所有逻辑堆在navigation.js里,页面里到处都是 request 调用和 if-else 状态判断,改一个地方三个地方跟着崩。后来我花了一整个下午重构,把导航相关代码拆成了前端页面和后端模块两大块:
- 前端:
miniprogram/pages/navigation/navigation.* - 后端:
routers/navigation.py、services/navigation_service.py、state/navigation_store.py、schemas/navigation.py
拆完之后调试和写后续功能舒服多了,最大的变化是:我不用每次改一个状态判断就满页面找哪里还依赖了它。
二、导航页的第一版:先让状态可见
第一版的导航页长得很朴素,没有酷炫的地图效果,但我在每个位置都考虑了"用户怎么知道系统现在在干嘛"这个问题:
- 顶部放返回按钮和标题
- 搜索面板里放目的地输入框、定位按钮、当前位置文字、连接状态标签
- 中间放导航提示区——"未开始导航"还是"导航中"还是"已偏航"
- 下面才是地图组件
- 最底部放路线信息和停止按钮
这些看起来是 UI 布局,但实际背后是一个更本质的问题:导航是一个持续状态机,不是一次性动作。如果页面上只有一个"开始导航"按钮,用户根本不知道系统是在定位、在规划路线、在连 WebSocket,还是已经在轮询了。我在真机上测试的时候就踩过这个坑:点完开始导航之后页面毫无反应,等了十秒路线才出来,中间用户完全不知道发生了什么。后来加上navigationState和connectionHint这两个显示字段,体验立马不一样了。
三、JS 里的状态设计:把"系统在干嘛"变成数据
在navigation.js的data里,我花了不少时间设计状态字段。最终留下了这些:
destination — 用户输入的目的地 isNavigating — 是否处于导航中 navigationState — "未开始 / 导航中 / 已到达 / 已偏航" routeHint — 当前提示语 sessionId — 后端会话 id markers / polyline — 地图点位和路线 connectionMode — 实时通道状态(ws / polling / idle) wsConnected — WebSocket 是否连接这里sessionId是一个容易忽略但特别重要的字段。一开始我没把它存到 data 里,而是用了一个局部变量,结果页面重新渲染之后会话 id 就丢了,导航状态再也查不回来。后来改成存在 data 里,配合onShow做状态恢复,才算解决这个问题。
这些字段不是为了"数据多",而是每个字段都对应一个用户能感知到的界面变化。navigationState变了,提示区的文字和颜色要跟着变;connectionMode变了,连接状态标签要更新;isNavigating变了,停止按钮的显示隐藏要切换。状态字段是视图的驱动力,不是附属品。
四、后端先做会话,不做地图
导航后端我第一个写的不是路线规划接口,而是会话管理。这个选择一开始看着有点反直觉——为什么不先把地图功能跑通再说?
因为导航不是一条静态数据。它是一个动态过程:用户会走,位置会变,路线要跟着更新,可能偏航,可能到达,也可能随时被手动停止。如果后端没有一个地方统一存这些变化中的信息,每来一个请求都得从零算起,逻辑会很散,也容易出不一致的 bug。
所以我在navigation_store.py里先定义了NavigationSession:
@dataclass class NavigationSession: session_id: str origin_lat: float origin_lng: float destination_name: str destination_lat: float destination_lng: float mode: str = 'walk' is_navigating: bool = True current_lat: float = 0.0 current_lng: float = 0.0 current_instruction: str = '正在规划路线' is_off_route: bool = False arrived: bool = False current_step_index: int = 0 route_summary: Optional[RouteSummary] = None settings: NavigationSettings = field(default_factory=NavigationSettings) updated_at: int = field(default_factory=lambda: int(time.time()))其中settings字段的设计是后来加的。一开始偏航阈值和到达阈值我是写死在代码里的,后来做偏航判断的时候发现不同场景需要的阈值差很多——校园里步行 15 米偏航就算多了,但空旷环境可能 30 米还算正常。于是我把它抽成了NavigationSettings,通过配置控制,默认值从环境变量读。
写完这个 dataclass 之后我意识到一件事:导航系统的核心不是地图,是状态管理。地图只是状态的展示层,状态才是导航的灵魂。这个认知转变对我后面的开发方向影响很大。
五、这一阶段留下的接口和还没做的事
到这一阶段结束,后端的接口骨架已经定了:
| 接口 | 用途 |
|---|---|
POST /start | 创建会话 + 规划路线 |
POST /location | 上报位置 + 更新状态 |
GET /status | 查询当前导航状态 |
POST /stop | 停止导航 |
但还有不少没做完的东西:语音播报没接入导航流程,偏航只有检测没有恢复策略,UI 动效基本为零,多地图源切换也没做。我不觉得这些是缺点——项目本来就应该从骨架开始,现在至少有一个可以持续扩展的主干。
