主题
开发规范
动笔写代码之前,把这一页过一遍。这个仓库的规矩不算少,但都指向三件事:依赖只能单向、一个事实只存一份、重复的东西收敛到一处。理解了这三句,后面的细则基本都是它们的具体化。真拿不准的时候去看权威原文:editor/docs/refactor/ARCHITECTURE-RULES.md 是硬规则,editor/docs/refactor/DEVELOPMENT-GUIDE.md 是执行细则,本页负责速查和讲清"为什么"。
代码放哪:四层分层
app(组合根) → features → core → shared(最底层)说白了:越靠下层的代码,越不该知道上层的存在。shared 是谁都能用、自己谁也不依赖的底层;core 放领域逻辑(样式、配置、地图运行时、表达式内核),但永远不知道页面长什么样;features 按页面域组织组件;app 只有一个组件,负责把所有东西接线起来。判断一个新文件放哪层,先问它 import 谁、被谁 import:
三条硬规则,全部由 tests/architecture.test.ts 自动检查:
- shared 不许 import core 和 features;core 不许 import features(类型依赖也算依赖,import type 一样算)。
- import 图不许成环。确实需要双向引用时,把共享物抽成独立的小模块(先例:zoom-case-hoist、project-format.model)。
- 只有 app.component 允许手工 new 控制器、做跨模块接线;别的组件看到这种代码就是放错了地方。
每个目录有一句话职责约定和唯一扩展接入点(ARCHITECTURE-RULES §1.1 的目录契约表),新代码先对表入座。另外,纯函数和数据表不与组件混居:大文件按 *.fns.ts(纯函数)、*-presets.ts(预设数据表)、*.model.ts(模型类型)拆出去,先例是 layer-list-fns、basemap-presets、project-format.model。
状态:一个事实只存一份
样式本体的真相在 StyleStore,界面意图的真相在 ConfigStore,别处不许再存第二份。派生值一律用 computed,需要副作用时用 effect,但 effect 不许回写自己依赖的 signal,否则就是状态回环。
写状态只有一条路:store 内部持有 private signal,对外只暴露 asReadonly() 的只读视图,写入必须走命名方法。方法名就是审计点,grep 一个 setter 名就能找到所有写入方。禁止把裸 signal 直接公开,这一条对新增代码零容忍。
一次样式编辑的完整路径长这样:
绕过 StyleStore.update 直改对象是严重违规:撤销栈、诊断、diff 下发全会漏掉这次变更,界面看起来改了,存档却是旧的。
RxJS 的边界(2026-09-30 裁决,ADR-15):同步派生继续用 signal/computed;新增的异步流(请求、定时器、事件编排)用 RxJS 管道写,出口要么 AsyncPipe 要么 toSignal。手写 .subscribe( 必须同一行配 takeUntilDestroyed,架构测试会做文本扫描,漏配即红。跨组件触发动作走 UiService 的计数信号通道(如 sourcesAddRequest),计数而不是布尔值:组件还没挂载时请求不丢,消费一次即焚。
组件怎么写
- standalone +
ChangeDetectionStrategy.OnPush+inject(),模板内联。 - 组件只做"模板 + 事件转发":不发起请求(请求全在 core 的 fetch 内核里),不存业务状态。组件内的 signal 只放局部瞬时 UI 态,比如下拉开合、拖拽草稿。
- 模板里禁止
.map()/.filter()/join(),改用 computed;禁止(click)="a(); b(); c.set(...)"多语句链,改组件方法;超长@if条件链收敛成 computed 布尔。 - ng-content 不许被
@if/@for/@switch条件包裹。这是 Angular 的官方禁令:投影内容在初始化时一次性创建,条件插槽永远不渲染,而且不报错。这里踩过坑(R6.5 M4 事故),对冲手段见测试策略的模板盲区。 - 控件(取色器、停靠点编辑器这类)写成受控组件:value 进、valueChange 出,内部不落库。样板在
features/controls/。 - Tailwind 解禁了,但只取 utilities 两层,preflight 永不启用(全局 reset 会破坏像素基线);复杂组件态仍走全局
ms-样式体系。
复用优先级
配置复用 > 组合复用 > 函数复用 > Service 复用 > 指令/管道复用 > 组件复用 > 继承(禁止) > 复制(禁止)同样的结构出现第二次,先停下来问:能不能抽成纯函数?展示转换考虑管道(join),交互增强考虑指令(MsTip、MsAutofocus)。新写面板之前先查 DEVELOPMENT-GUIDE 的复用登记节,复用不成才允许新写,并且要在评审里回答"登记表里现有的能力为什么不够"。
反过来,语义不同的近似逻辑不许强行合并。比如 HSL 换算的"度"口径和 0-1 口径,看着像一回事,合并就是改行为。保留两份、在注释里说明差异,比"统一"更安全。
命名与文件
- CSS 类名
ms-前缀加双下划线元素修饰(ms-tree__row),全局唯一样式文件是src/styles.css。 - 测试放
editor/tests/,目录镜像src/app的层级;mock 管线的测试在editor/mock/。src 下不出现测试文件。 - 测试用例名写中文行为句,例如"跨缩放停靠点:拖拽后 base 保留";让报告本身可读。
- 数据兼容性锚点(
mapstudio:前缀、底图标记键等)一律从shared/style-conventions.ts引用,禁止在业务代码里写字面量。改键名等于改用户数据格式,必须走迁移器,详见配置参考。
注释怎么写
全仓刚完成一轮"注释去 AI 化",现在的基准是:注释回答"为什么",代码回答"怎么做",git 回答"什么时候改的"。
具体规则:
- 语言用中文。每个
.ts文件顶部一个/** */块,第一句说清这个文件的职责,后面最多四行关键约束,超出的内容压缩成一行@see指针指向权威文档。 - 分节符统一用
/* ---------- 名称 ---------- */这一种。 - 导出函数和 service 公有方法补 TSDoc:意图、参数、返回值,必要时加异常说明。
- 复杂逻辑处写"为什么这么设计、边界在哪、错误怎么处理",不要翻译代码字面。
- 禁止写需求编号和变更史。
R-57⑯、T-225、问题15这类编号在仓里没有查询入口,对新读者是纯噪音;"某轮某日改回了之前的口径"这种叙事读代码的人不需要知道,去查 git log。这一条是刚清理完 350 多处编号注释后立下的规矩,别再制造。 - 正面参照:
core/map/sort-key-bands.ts、core/map/feature-ref.ts、core/expression/ast/下的注释,一句话讲清意图,细节留给代码。照这个样子写。
类型与 IO
- TypeScript strict 全开;
as any是受控预算,新增要在提交信息里说明理由。 - maplibre-gl 的运行时值导入只允许出现在
core/map/(和 canvas-search 的 LngLatBounds),其余地方一律import type,防止把地图引擎拖进不该出现的模块。 - 样式 JSON 拉取统一走
core/style-fetch,组件里裸 fetch 是违规。 - 事件监听必须可解绑:document/window 监听存引用、ngOnDestroy 里移除;map 实例监听销毁时
.off;rAF 和 Observer 同理。泄漏是评审必拦项。 - 运行时态(hover/selected 的 feature-state、诊断计数、debug 开关)不持久化、不入撤销栈;MapLibre 的运行时 Feature 对象不进 Store,只允许纯数据的 FeatureRef。
提交与治理
单 main 分支、轮次制:每轮改动走"扫描 → 实施 → 门禁 → 治理同步 → 提交",门禁结果(tsc、vitest 数字、coverage、build、基线、e2e)写进中文提交信息。完整流程和评审清单见参与指南;例外清单(历史遗留的合规违例,勿扩大)在 ARCHITECTURE-RULES §7 备案,引用时注明出处,新代码不得效仿。