用华为云码道构建校园导航应用「新生通」:从需求分析到工程落地

新生通是一套面向高校的校园导航 Web 应用:用原生 ES Module 实现矢量校园地图、场所详情、课表联动与换校配置,零第三方依赖、零构建步骤 ,132 项自动化测试全部通过,代码开源于 AtomGit(
https://atomgit.com/2403_87838053/xinshengtong)。在"校园面积 1200×800 米、28 个点位、9 份 JSON 数据"的限定条件下,它把"找教室、取快递、找座位、看施工"四类高频查询从平均 4 步操作压缩到 1 次点击。
本文回答以下 10 个问题
- 校园导航类应用应该怎样组织信息?
- 零依赖、无构建的前端方案是否可行,代价是什么?
- 课表如何与地图联动,并给出出发时间?
- 换一所学校需要改什么代码?
- 132 项自动化测试具体覆盖哪些内容?
- 华为云码道(CodeArts 代码智能体)在开发中承担了什么角色?
- 这套方案与通用地图、教务系统是什么关系?
- 如何在本机运行、二次开发与替换数据?
- 前端如何做到可验证的安全与无障碍?
- 当前方案有哪些明确的局限?
一、项目背景与问题定义
每年九月,校园里都会集中出现一类高频问题:新生不知道教学楼在哪、不知道快递到哪个门取、不知道下一节课的教室怎么走。这些问题的共同点是------答案本来就有,只是分散在不同系统里。
纸质地图没有检索能力;教务系统有课表,但不提供空间信息;后勤的施工通知发布在公众号里,与地图没有关联;宿舍分配表是 Excel,无法和路线规划结合。学生要在四个地方来回切换,才能拼出"我现在该往哪走"。
因此,这个项目的目标不是"再做一个地图",而是把与地点相关的信息收拢到一个以地点为索引的界面里,让一次查询替代四次翻找。
项目名称「新生通」,定位是校园导航与服务台。开发过程使用华为云码道(CodeArts 代码智能体)完成需求拆解、模块生成与代码评审,最终交付一个纯前端、零第三方依赖、可换校复用的 Web 应用。
动手前先梳理了校园里已经在用的几类工具:纸质地图、群文件里的平面图、通用地图软件的校园标注、学校公众号的迎新推文。它们的共同缺口不是"没有信息",而是信息没有结构化------位置是图片、通知是文本、课表是表格,三者之间没有可以建立索引的关联。
这也决定了技术路线:先建立结构化的地点数据,再让其他信息挂到地点上。
二、需求分析:谁在什么场景下需要它
2.1 用户与场景
开发前先明确三类用户和六个高频场景,避免功能堆砌。
| 用户 | 典型场景 | 需要的信息 | 期望的操作 |
|---|---|---|---|
| 新生 | 报到当天找报到点、宿舍、缴费窗口 | 位置 + 流程顺序 + 步行时间 | 打开就能看到"下一步去哪" |
| 在校生 | 课间换教室、找空教室自习、取快递 | 教室号、空余状态、营业时间 | 三步之内拿到答案 |
| 访客与家长 | 找校门、食堂、卫生间 | 位置 + 步行路线 | 不需要注册,直接查 |
六个高频场景按发生频率排序:找教室、取快递、找食堂、找自习座位、看施工封闭、办手续(补卡、报修、开证明)。
2.2 需求优先级
| 优先级 | 需求 | 判断依据 |
|---|---|---|
| P0 | 地图定位与搜索 | 没有它,其余功能无从落地 |
| P0 | 场所详情(按类型) | 决定"点开之后有没有用" |
| P1 | 课表与教室联动 | 每天都会发生,频次最高 |
| P1 | 公告在图上可见 | 直接影响"能不能走通" |
| P2 | 服务电话、失物招领、问题上报 | 低频但刚需 |
| P2 | 迎新流程 | 集中在一周内,但那一周极其密集 |
| P3 | 数据看板、换校配置 | 面向维护者与推广,不面向日常使用 |
2.3 明确的"不做"
边界和能力同样重要。以下内容在第一版就确定不做:
- 不做社交与内容社区:与导航目标无关,且带来审核成本。
- 不做实时定位与轨迹:室内定位精度不足,误差会直接损害可信度。
- 不做账号体系:没有后端时,账号只会增加使用门槛;需要身份的功能(课表)用本地配置解决。
- 不接入第三方地图 SDK:校园内部道路与建筑命名与通用地图不一致,且会引入外部请求与授权问题。
这四条"不做",让项目在两周内收敛到一个可交付的范围,而不是变成一个半成品平台。
2.4 使用节奏:一天、一周、一学期
功能的排布还取决于使用节奏。把时间尺度拉开看:
- 一天之内:早上确认第一节课在哪,中午看食堂排队,傍晚取快递,晚上找自习座位。四次打开,每次目标明确,每次停留不超过一分钟。
- 一周之内:以课表为主,周末关注公告(场馆是否开放、道路是否施工)。
- 一学期之内:迎新、考试周、假期前后的时间节点集中出现。
结论是首页必须是地图而不是列表------一天里的多数操作都是"从当前位置去某处"。课表排在第二位,因为它回答"什么时候出发"。数据看板放到后面,使用频率低,但对管理者有价值。
这也是信息架构的依据:按使用频率排序,而不是按功能重要性排序。
三、产品设计:以地点为中心组织信息
3.1 信息架构
整体采用"一图 + 多页"的结构:地图是索引,其余页面是地图在不同维度上的展开。
- 地图:空间维度,回答"在哪、怎么走"
- 课表:时间维度,回答"什么时候、去哪个地点"
- 公告:状态维度,回答"哪里现在不能走"
- 数据:整体维度,回答"现在校园里什么情况"
- 服务 / 失物 / 上报 / 迎新:事务维度,回答"到了之后办什么"
九个页面共享同一份数据源,因此从任意页面跳转到地图时,点位、状态、路线都是一致的------这是"一个应用"和"四个工具"的区别。
3.2 页面职责与关键取舍
地图页只保留三样常驻元素:搜索、分类筛选、地图本体。详情面板按需从右侧滑出,关闭后地图恢复完整视野。
这里有一个明确的设计原则:常驻控件不浮在内容之上。初期版本曾把搜索框与筛选条做成地图上的浮层,无论怎样调整位置,都会遮挡最上一排建筑的名称。最终改为"标题栏 → 工具栏 → 地图"的三段式布局,牺牲约 60 像素的高度,换取内容零遮挡。
课表页做了教务系统没有做的一步:把课程与地点连接起来。顶部卡片给出"下一节 + 建议出发时间",下方是一周网格,点击任意课程直接跳转到对应建筑。这样"看课表"和"找教室"从两次操作合并成一次。
数据页承担节奏变化的作用。前几页以列表和卡片为主,这一页全部是图表:课时分布、食堂排队、设施状态、单车余量。图表用 DOM 与 CSS 实现,不引入图表库,保证离线可用。
空状态与加载态同样是设计的一部分。
九个页面各自定义了空状态与下一步建议:没有公告时提示"一切正常",没有课程时引导去图书馆自习,地图没有点位时直接指出缺哪一份数据。加载阶段使用骨架屏占位,避免布局跳动。全部数据源失败时进入独立的错误页,给出原因与重试入口。
这些状态在演示数据完整时看不到,但决定了数据不全时的体验。为此项目提供了 ?empty=1 的入口,让空状态可以被直接检查,而不是等到上线之后才发现某一块是空白。
3.3 三个关键设计决策
决策一:详情面板按类型差异化。
多数校园地图对所有点位展示同一张卡片。但用户点开"食堂"和点开"卫生间"想获取的信息完全不同。因此详情面板按类型渲染:教室看课表与容量,食堂看排队与菜单,图书馆看空位与预约入口,设施看可用状态,单车点看余量。同一个交互位置,承载不同类型的信息结构。
决策二:地图标签分层显示。
28 个点位如果全部带名称,缩小视图时文字会互相覆盖。最终规则是:建筑名常驻,点位名仅在放大(缩放比例大于 1.35)或悬停、选中时出现。信息密度跟随缩放级别变化。
决策三:把"现在挤不挤"直接画在图上。
食堂点位的周围绘制一层颜色气泡,半径与排队热度成正比,颜色按绿、黄、红分档。用户不需要点开详情,就能判断该去哪个食堂。这是"把数据前移到索引层"的一次尝试------索引本身携带决策信息。

图 1:校园地图主界面。左侧功能栏、中间矢量地图、右侧场所详情,常驻控件不压内容。
3.4 交互细节:手势与键盘并存
地图需要同时支持三种输入方式:
- 鼠标:拖动平移,滚轮以指针为锚点缩放,双击放大
- 触屏:单指拖动,双指缩放;底层统一用指针事件处理,不为设备写两套逻辑
- 键盘:方向键平移,加减号缩放,Tab 在点位之间移动,回车选中
所有变换通过 requestAnimationFrame 合并,避免高频事件触发重复计算。点击与拖动的区分采用位移阈值(6 像素):小于阈值视为点击,超过则视为拖动,避免"想拖动却触发了选中"。
这套规则的价值在于一致性:无论在手机还是桌面,平移、缩放、选中的判定逻辑都来自同一份代码。
四、技术选型:三个约束下的取舍
4.1 约束条件
项目启动时确定了三个硬约束:
- 可被他人接手:克隆后一条命令即可运行,不依赖环境配置。
- 可离线演示:校园内部分区域网络不稳定,页面不能在断网时失效。
- 代码即产物:评审看到的源码就是运行的代码,不存在"构建后才正确"的黑箱。
4.2 方案对比
| 维度 | 方案 A:React + Vite | 方案 B:原生 ES Module(最终选择) |
|---|---|---|
| 启动成本 | 需要安装依赖、执行构建 | 一条命令启动静态服务 |
| 离线可用 | 依赖构建产物与依赖体积 | 源码直接运行,无外部请求 |
| 可维护性 | 生态成熟,组件化清晰 | 需要自行约束分层与工具函数 |
| 可评审性 | 需理解构建配置 | 打开文件即所见 |
| 换校成本 | 与业务代码耦合较深 | 数据与视图完全分离 |
同样地,后端方案也在"自建服务"与"本地数据 + 本地存储"之间做了比较。最终结论是:在没有真实教务、后勤接口的前提下,自建后端只会增加演示负担,因此第一版采用 9 份 JSON 作为数据源、localStorage 作为写入层,并把接口对接待为后续演进。
4.3 选型结论
选择方案 B。代价是明确的:没有 JSX、没有类型检查、组件复用需要自己写工具函数。对应的补偿措施是:
- 纯逻辑全部下沉到
src/core/,可用node:test直接验证; - 视图层只做 DOM 组装,通过统一工具函数约束写法;
- 用测试守卫替代部分类型检查的职责。
4.4 性能预算
首屏设定三条预算,作为实现阶段的约束:
- 资源全部同源:不加载第三方脚本、字体与图片,首屏只有 HTML、4 个 CSS 与若干 ES Module
- 地图只渲染一次:之后所有平移缩放仅修改 transform,不重建节点
- 动效节点有上限:切页动画的临时节点控制在 260 个以内,动画结束立即回收
这些预算直接影响了实现方式。例如地图没有采用"每次缩放重算点位坐标"的做法,而是整体变换;动效没有采用粒子方案,而是有限网格。
五、架构设计
5.1 分层与依赖方向
index.html 外壳
└─ src/main.js 装配:加载数据 → 建立状态 → 启动路由 → 挂载工牌
├─ src/app/ 装配层:repo(数据)、store(状态)、router(路由)、config(换校)
├─ src/core/ 纯函数层:dom、validate、storage、fuzzy、schedule、pendulum
├─ src/map/viewport.js 地图数学:平移缩放状态机
├─ src/ui/ 可复用视图件:kit、icons、tilt、badge、shard
└─ src/features/ 九个页面模块
依赖方向单向:features → ui → core,app 只被 main 调用。这条约束不只写在文档里,还写成了测试:解析所有相对导入,判断每一层允许依赖哪些层,越界即失败;同时检查循环依赖与无人引用的模块。
5.2 数据模型
点位数据分为两层:
- 公共字段 :
id、name、aliases、kind、x、y、tags、summary、hours、steps - 类型字段 :收进
detail,由detail.type决定解析方式(教室、食堂、图书馆、设施、单车、普通场所)
这样设计的好处是校验可以按类型分派,未知类型只丢弃 detail 而不影响点位本身;新增一种点位类型时,只需增加一个解析分支和一段渲染逻辑,数据结构保持稳定。
5.3 数据流与校验
data/*.json → repo.js(校验 + 归一化)→ main.js(装配为上下文)→ features(渲染)
校验层负责四件事:类型检查(坐标必须是数字)、枚举收敛(未知 kind 归为 other)、缺省补全(缺少坐标时回退到所属建筑中心)、非法丢弃(不合法记录不进入渲染层)。加载失败时按数据源降级,只有全部失败才进入错误页。
5.4 决策记录
关键决策以 ADR 形式记录,每条都写明代价:
| 决策 | 收益 | 代价 |
|---|---|---|
| 零依赖、无构建 | 可离线、可评审、无供应链风险 | 无 JSX 与类型检查,需自建工具与测试 |
| 数据驱动 + 校验层 | 换校不改代码,脏数据不白屏 | 校验代码占比上升 |
| 地图只改一个 transform | 平移缩放不触发重排 | 文字随缩放变细,需标签分层补偿 |
| 换校数据包存本地 | 无需部署即可换校 | 数据仅在当前浏览器 |
5.5 状态管理:三层就够
没有引入状态管理库,按生命周期把状态分成三层:
- 会话级:数据(一次加载、只读)、当前路由、当前时间
- 设备级:报到清单、失物发布、问题上报(localStorage,带版本与结构校验)
- 组件级:地图视口、详情面板选中项、搜索关键词(组件内部闭包)
判据是"是否需要跨页面共享,且是否需要跨会话保留"。九个页面的规模下,引入全局状态库会带来比收益更多的样板代码,因此没有再增加抽象。
代价是页面之间不能直接读写彼此的状态,必须通过路由参数传递------但这恰好强制了"页面之间只通过 URL 通信",深链能力因此自然成立。
六、核心实现
6.1 地图:单变换与锚点缩放
地图性能的关键在于避免重排。实现方式是把整张图放进固定尺寸容器,平移缩放只修改一个属性:
js
stage.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
坐标换算由纯函数完成,核心是保证缩放时指针下的内容点保持不动:
js
zoomAt(factor, screenX, screenY) {
const next = clamp(scale * factor, minScale, maxScale);
const ratio = next / scale;
offsetX = screenX - (screenX - offsetX) * ratio;
offsetY = screenY - (screenY - offsetY) * ratio;
scale = next;
clampOffsets();
}
这条不变量被写成测试:
js
const before = vp.toContent(300, 200);
vp.zoomAt(1.6, 300, 200);
const after = vp.toContent(300, 200);
assert.ok(Math.abs(before.x - after.x) < 0.001);
同时对所有方法做边界收敛:内容小于视口时保持居中,大于视口时不允许拖出可视区。
6.2 搜索:可解释的三段式打分
搜索需要覆盖三类输入:全名、别名、口语简称。打分规则设计为可解释的三段:
- 匹配强度:完全相同 1.0、前缀 0.9、包含 0.62 至 0.82、子序列 0.34 至 0.6
- 字段权重:名称 1.0、别名 0.96、标签 0.62
- 次序规则:同分时名称较短者优先
由此「二教」命中别名、「菜鸟」命中「菜鸟驿站」的包含匹配、「大活」命中「大学生活动中心」。规则可以被解释,也就可以被测试:口语别名、简称、标签命中、无关查询、数量上限、名称优先,共六条。
6.3 课表时间:跨周判定
"下一节"的计算需要处理跨天与跨周:
js
const dayOffset = (course.weekday - weekday + 7) % 7;
let wait = dayOffset * 1440 + start - minutes;
if (wait <= 0) wait += 7 * 1440;
当天已经开始但尚未结束的课程算"正在上",不算"下一节";当天全部结束后顺延到下周。这段逻辑有九条测试覆盖,包括跨周与跨天的边界。

图 2:课表页。"下一节"提醒卡给出出发时间,下方为一周网格,今天列高亮。
6.4 换校:三段校验
换校数据来自外部文件,必须按不可信输入处理,链路分三段:
- 文件层 :不超过 2MB,
JSON.parse成功,顶层为对象 - 归一化层:只处理出现的键,逐条校验字段
- 语义层 :
campus.pois非空,school与campus至少存在一个
通过后写入本地存储;读取时再次归一化,因为存储内容同样可能被手工修改。失败时给出具体原因,便于定位。
6.5 动效与工牌
切页动效采用"碎片波 + 卡片浮入":内容区铺一层同色系半透明方块,从点击位置向外淡出,新视图同时上浮落位。所有动画只修改 transform 与 opacity,节点数量按视口自适应(150 至 260),动画结束后节点全部移除;系统开启"减弱动效"时整体跳过。
工牌使用阻尼摆物理,方程与参数为 θ'' = -k·sinθ - c·θ'(k=16,c=2.6),支持拖拽与按角速度续摆。物理部分实现为纯函数模块,可在 Node 环境中测试,不依赖浏览器。

图 3:头像工牌翻到背面,四个功能磁贴,悬停时立体浮起。
6.6 路线助手:一句话规划多站点
新生常见的一句话是"下午去二教上课,顺路取快递"。路线助手把这句话拆成多站点路线:
- 分段:按标点以及"顺路、然后、再去"等词切分
- 实体识别:优先匹配"别名被包含"的关系,其次整体模糊匹配
- 去重与排序:按出现顺序保留首次出现的地点
- 估算:相邻点位直线距离除以步行速度(75 米/分钟)
输出是"校门 → 第二教学楼 → 快递驿站",以及每段的距离与时间。
它不是模型调用,而是三段规则,因此可离线、可测试------七条用例覆盖口语、别名、去重、无匹配、显式起点与时间换算。接口按"实体识别可替换"设计,后续接入模型能力时只需替换这一处。
6.7 数据看板:图表怎么画
看板上有四个指标卡、一组柱状图、两组横条、一组状态统计,全部由 DOM 与 CSS 构成:
- 柱状图:网格容器 + 高度百分比,今天那一根替换为强调色
- 横条:宽度百分比 + 三档配色(低、中、高)
- 状态统计:圆点着色 + 计数
没有引入图表库,原因有三:图表数量少、交互需求简单(不需要缩放与提示框)、离线可用是硬约束。用 CSS 实现之后,这一页没有任何额外请求。

图 4:数据看板。课时分布、食堂排队、设施状态、单车余量,全部由 DOM 与 CSS 绘制。
七、工程质量保障
7.1 测试体系
项目共 132 项测试,按类型分布:
| 类型 | 数量 | 代表用例 |
|---|---|---|
| 纯函数正确性 | 40+ | 搜索打分、课表下一节、视口缩放、阻尼摆 |
| 数据与状态 | 25+ | JSON 归一化、本地存储篡改、换校数据包 |
| 边界与异常 | 20+ | 空数组、超长文本、非法日期、存储不可用 |
| 架构与质量守卫 | 15 | 依赖方向、循环依赖、死模块、行长度 |
| 安全与对比度 | 15 | 危险 API、外部域名、CSP、路径穿越、WCAG |
| 渲染冒烟 | 17 | 九个页面渲染、空数据、键盘可达 |
编写顺序是:先纯函数、再数据、最后渲染。纯函数最稳定,是地基;渲染测试最易受结构变化影响,放在最后维护。
7.2 四类守卫
除功能测试外,另有四类自动检查:
| 守卫 | 内容 |
|---|---|
| 架构 | 依赖方向、循环依赖、死模块、单文件不超过 600 行 |
| 质量 | 无 console.log 与 TODO、单行不超过 140 字符、无 var、文件有头注释 |
| 安全 | 危险 DOM API、外部域名、CSP、响应头、路径穿越 |
| 对比度 | 正文不低于 4.5、标题不低于 7、按钮白字不低于 4.5 |
其中对比度检查使用 WCAG 公式计算实际比值。开发过程中它发现次要文字的对比度为 4.04,低于 4.5,随后将颜色由 #6e7789 调整为 #5f6779。这类问题肉眼难以判断,但可以被计算。
7.3 可访问性
- 地图点位支持 Tab 聚焦、回车与空格选中,焦点环可见
- 侧边栏使用
aria-current标记当前页,而非仅依赖样式类 - 折叠面板使用原生
details,保留浏览器默认的键盘行为 - 提供跳转链接,键盘用户可直接进入主内容
- 尊重
prefers-reduced-motion,关闭非必要动画
文字层面也做了约束:正文 14 像素、行高 1.6,长段落在小屏上不显拥挤;标题与正文保持两级以上字号差,层级清晰。这些数值统一定义在样式变量中,改动一处即可全站生效。
7.4 安全
- 全站不使用
innerHTML、eval、new Function,由静态扫描测试保证 - 严格 CSP:脚本仅允许同源、禁止内联事件、禁止
object与base标签 - 链接白名单:仅放行
http(s)、tel、mailto - 本地存储按不可信输入处理,带版本号与结构校验
- 静态服务器仅监听本机,路径解析后校验是否位于项目根目录
7.5 交付前的检查清单
每个功能合入前,走一遍固定清单:
- 测试全绿,新增功能有对应用例
- 无调试残留(日志、断点、被注释的旧代码)
- 空数据下九个页面均可渲染
- 窄屏 375 像素与桌面 1440 像素各走一遍主流程
- 键盘可完成主要操作
- 文档与实现一致(功能列表、测试数量、目录结构)
清单的作用是把"记得检查"变成"必然检查"。项目里几次布局问题都是在窄屏这一步暴露的。
7.6 错误处理:分级降级
按影响范围把错误分成三级:
- 单条数据错误:丢弃该条记录,记录警告,页面继续渲染
- 单个数据源失败:该模块降级为空状态,其余模块正常
- 全部数据源失败:进入错误页,提供原因与重试按钮
本地存储的写入失败(隐私模式、配额不足)不抛异常,只返回失败标记,界面以提示告知,不阻断主流程。
分级的意义是:错误的影响范围应与处理范围一致。一条坏数据不应该让整页不可用,一个模块失败也不应该让整个应用退出。
八、软件价值
8.1 对学生:把四次查询压缩成一次
在没有这个应用之前,找到一间教室通常需要:打开教务系统查课表、打开地图软件找楼、在群里问教室位置、到楼下看楼层指引。四步之后才到达目的地。
应用把这条链路压缩为一次操作:在地图上直接看到课程对应的建筑,点击即可获取步行路线。按每周 12 节课、每节课前节省约 2 分钟估算,一个学生一学期可节省数十次查找动作。
| 场景 | 原来的做法 | 现在的做法 | 大致节省 |
|---|---|---|---|
| 找教室 | 查课表、找楼、问人、看楼层指引 | 点课程直接定位 | 约 2 分钟 |
| 取快递 | 翻聊天记录或问室友 | 搜索"菜鸟"直达 | 约 1 分钟 |
| 看施工 | 翻公众号历史消息 | 公告页或地图高亮 | 约 3 分钟 |
| 找空教室 | 逐层走 | 详情页看空闲状态 | 约 5 分钟 |
这些数字是估算,但方向明确:把"找信息"的时间成本从分钟级压到秒级。
8.2 对学校:信息发布从"多处维护"变为"一处生效"
公告、设施状态、服务电话集中在同一份数据源中。后勤更新一条施工公告,地图上对应点位立即出现高亮提示,不需要同时修改公众号、群通知和纸质告示。
迎新期间尤其明显:报到流程、宿舍分布、缴费入口集中在一个页面,减少重复咨询。
8.3 对开发者:可长期维护的结构
- 零第三方依赖,不存在依赖升级与供应链风险
- 数据与视图分离,更换学校时无需修改业务代码
- 132 项测试与四类守卫,修改后有回归保障
- 分层明确,新增一个页面只需注册路由与模块
8.4 可量化指标
| 指标 | 数值 |
|---|---|
| 校园点位 | 28 个,覆盖 12 类设施 |
| 建筑与道路 | 18 栋、7 条主路 |
| 数据文件 | 9 份 JSON,全部可替换 |
| 自动化测试 | 132 项,全通过 |
| 第三方运行时依赖 | 0 |
| 构建步骤 | 0 |
| 换校所需代码改动 | 0 行 |
| CSS 总体积 | 65.7 KB(5 个文件,未压缩) |
| JS 总体积 | 147.3 KB(未压缩,含注释) |
| 测试耗时 | 约 0.6 秒(132 项,单进程) |
8.5 与现有方案的关系
它不是替代,而是补齐:
- 教务系统负责权威数据(课程、成绩),本应用负责把这些数据落到空间上
- 通用地图负责城市尺度导航,本应用负责校园尺度的建筑与设施命名
- 学校公众号负责发布通知,本应用负责让通知出现在它影响的位置上
因此它的定位是"空间索引层":把已有信息按地点重新组织,而不是再造一套数据。这也解释了为什么它必须轻------轻到可以嵌入任何现有流程。
九、开发过程:技术决策路径
这一节记录三个真实的技术决策过程,重点在"如何定位"与"如何验证"。
9.1 从需求到模块拆解
第一步不是写代码,而是拆模块。用码道把功能拆成数据层、地图层、视图层与应用层,并标注每个模块的输入、输出与依赖关系。
这一步的产出直接决定了目录结构。尤其是"纯逻辑与视图分离"这一条,使后续所有纯函数都能脱离浏览器测试,也为换校配置的复用打下基础。
9.2 案例一:搜索从"能搜到"到"搜得准"
初版实现采用"完全相同 + 包含"两段匹配,覆盖了正式名称,但口语简称无法命中。定位方式是列出失败用例:二教、三号食堂、菜鸟驿站、大活。
改进分三步:增加别名数据、引入子序列匹配、明确字段权重与同分次序。每一步都用失败用例回归。最终规则可以被解释,因此也能被六条测试固定下来。
这个案例说明:搜索的问题往往不在算法,而在"用户会怎么称呼一个地方"。数据层的别名设计,比匹配算法更影响结果。
9.3 案例二:交互冲突的定位
工牌翻面后,背面的功能磁贴点击无响应。排查顺序是:先确认层级与可见性,再确认命中测试,最后在指针事件中打点,发现 pointerup 未到达磁贴。
原因是卡片在 pointerdown 时调用了 setPointerCapture。指针捕获会把后续事件重定向到捕获元素,点击事件因此不再冒泡到目标元素。这是指针事件的规范行为,只是在"卡片内包含可点击元素"这一结构下不适用。
解决方案是移除指针捕获,改为在 window 上跟踪指针,并在松手时区分两种意图:位移小于阈值的视为点击,交给磁贴处理;超过阈值的视为拖动,执行甩动。判定逻辑集中在一处,行为可预测。
9.4 案例三:视觉层的合成问题
服务页出现一块半透明残影。排查采用二分法:先排除单个元素,再逐步关闭视觉特效,最终定位到大面积 backdrop-filter 与嵌套 filter 的组合。
处理方式是整体转向扁平化视觉:移除毛玻璃,头像光晕改用遮罩实现。这次调整同时简化了视觉层级,页面更干净,渲染也更稳定。
9.5 与码道协作的三条方法
方法一:先拆解,再生成。 第一轮只做模块拆分,不产出代码。模块边界确定后,每次只生成一个文件,减少上下文串扰。
方法二:约束前置。 把禁止项写在需求最前面(禁用 DOM 注入 API、必须提供空状态、必须键盘可达),命中率明显高于写在末尾。
方法三:让评审独立于生成。 同一段代码,让它以"安全评审"的视角逐条检查注入面、边界条件、异常路径与无障碍问题,比让它重新生成整段代码更容易发现问题。
方法四:把验收标准写成断言。 需求里写"缩放时指针下的内容不能漂移",落地就是一条断言;写"键盘可达",落地就是检查 tabindex 与 aria-label。把主观描述翻译成可执行检查,是这套协作方式里最有效的一步。
完整的 34 条提示词按文件组织,附产出与验收标准,已整理进仓库文档,便于复现。
〔待补:码道对话窗口截图 ------ 见下方"配图待补"说明〕
9.6 一个完整的开发循环
以课表联动为例,记录一轮完整过程:
- 需求:点课程能到教室,并提示出发时间
- 设计:课程需要绑定点位标识;出发时间 = 上课时间 − 步行时间 − 缓冲
- 实现:先写纯函数(当前课程、下一节、等待描述),再写视图
- 验证:九条时间用例,覆盖当天、跨天、跨周
- 回归:新页面加入渲染冒烟测试
- 文档:在架构文档补充数据流,在提示词清单补充对应条目
这个循环在项目里重复了九次(九个页面),每次都留下测试与文档,因此最后的清理阶段没有出现"没人知道这块代码做什么"的模块。



十、局限与后续规划
当前局限
- 数据为演示数据,楼宇、点位、分机号均为示例,正式使用需按真实校区重绘坐标
- 无后端,失物招领与问题上报仅保存在本机;接入真实系统需补充鉴权、限流与内容审核
- 照片仅本地预览,不上传、不持久化
- 换校数据保存在当前浏览器,更换设备需重新导入
- 尚未建立自动化视觉回归,样式修改依赖人工确认
后续规划
- 接入教务与后勤接口,将 JSON 数据源替换为实时接口,视图层不变
- 增加室内楼层图与教室级寻路
- 补充无障碍路线,标注台阶与施工路段
- 建立截图基线,将视觉回归纳入自动检查
维护与交接
数据与代码分离之后,接手者只需要理解两件事:数据包的 9 个键分别管什么,以及四个目录各自的职责。
仓库内提供完整文档:架构说明(分层、数据流、取舍表)、四份决策记录(每个决策都写了代价)、安全说明(威胁模型与已知限制)、提示词清单(34 条,按文件组织)。新成员可以按顺序阅读,而不必从代码反推设计意图。
这也是"零依赖、无构建"的延伸价值:接手成本不随依赖数量增长。
十一、常见问题(FAQ)
Q1:新生通是什么?
新生通是一套面向高校的校园导航 Web 应用,用一张矢量校园图承载地点信息,并把课表、公告、服务电话、失物招领挂在对应地点上。技术上为原生 ES Module + CSS,零第三方依赖、零构建步骤。
Q2:它具体解决什么问题?
解决"答案本来就有、但分散在四个系统里"的问题。限定条件是校园场景:学生需要频繁在建筑之间移动,而教务系统不提供空间信息、通用地图不提供校园内部命名、公众号通知与位置无关。
Q3:为什么选择零依赖、无构建?
三个约束决定:克隆后一条命令即可运行(可被他人接手)、断网可用(校园部分区域网络不稳)、源码即产物(评审看到的代码就是运行的代码)。代价是没有 JSX 与类型检查,用纯函数下沉 + 测试守卫来补偿。
Q4:课表联动是怎么实现的?
课程数据里绑定点位标识与步行分钟数。出发时间 = 上课时间 − 步行时间 − 2 分钟缓冲;当天已开始未结束的课判定为"正在上",当天结束后顺延到下周。这段逻辑由 9 条测试覆盖,包括跨天与跨周。
Q5:换一所学校需要改多少代码?
0 行。数据集中在 9 个 JSON 中,界面上可通过"导出配置 → 修改 → 导入"完成切换;导入链路分文件层、归一化层、语义层三段校验,非法数据不会进入渲染层。
Q6:132 项测试具体测什么?
分六类:纯函数正确性 40 余项(搜索打分、课表时间、视口缩放、阻尼摆)、数据与状态 25 项、边界与异常 20 项、架构与质量守卫 15 项、安全与对比度 15 项、渲染冒烟 17 项(9 个页面在 DOM 垫片中真实渲染)。
Q7:华为云码道在开发中做了什么?
承担三件事:第一轮把需求拆成模块并标注依赖关系;按文件生成实现(每次只生成一个文件,减少上下文串扰);以"安全评审"视角逐条检查注入面、边界条件与无障碍问题。产品决策与验收仍由人完成。
Q8:它和通用地图、教务系统是什么关系?
不是替代关系。教务系统负责权威数据(课程、成绩),通用地图负责城市尺度导航,本应用负责校园尺度的建筑与设施命名,定位是"空间索引层"------把已有信息按地点重新组织。
Q9:如何在本机运行?
安装 Node 20 以上,在项目根目录执行 node server/start.mjs,浏览器访问 http://127.0.0.1:5173。不要直接双击 index.html,浏览器会因安全策略拒绝读取本地 JSON。
Q10:前端如何保证安全与可访问性?
安全:全站禁用 innerHTML 与 eval(由静态扫描测试强制)、严格 CSP、链接白名单、本地存储按不可信输入处理、静态服务器防目录穿越。可访问性:点位可 Tab 聚焦、对比度按 WCAG 校验(正文 ≥4.5)、尊重减弱动效偏好。
Q11:数据从哪里来,正式落地怎么办?
当前为演示数据,楼宇、点位、分机号均为示例。落地时替换 9 份 JSON,或把数据层换成学校接口;视图层不随数据源变化,接口对接只需改 repo.js。
Q12:有哪些已知局限?
五条:数据为演示数据需按真实校区重绘坐标;无后端,失物与上报仅存本机;照片仅本地预览不上传;换校数据仅在当前浏览器;尚未建立自动化视觉回归。
十二、结语
「新生通」没有使用框架,也没有追求功能数量。它做的是把分散在四个系统中的地点相关信息,收拢到一张以地点为索引的图上,并让其他学校可以直接替换数据使用。
在工程上,它验证了一件事:约束越明确,产出越稳定。零依赖、无构建、数据驱动、测试守卫,这些约束在开始时看起来是限制,实际上让项目在两周内保持了可控的复杂度。
如果它能让一位新生少问三次"二教在哪",这个项目的价值就成立了。