Skip to content

开发规范 ​

动笔写代码之前,把这一页过一遍。这个仓库的规矩不算少,但都指向三件事:依赖只能单向、一个事实只存一份、重复的东西收敛到一处。理解了这三句,后面的细则基本都是它们的具体化。真拿不准的时候去看权威原文: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 备案,引用时注明出处,新代码不得效仿。