主题
技术选型
这页回答一个问题:为什么是这套组合。每个依赖都讲三件事:为什么选它、备选是什么、代码里哪里在用。总纲只有一条,这是一个单页工作台工具,选型服务于「可验证地保持行为」,不服务简历。完整决策背景见 ADR。
先看两张全景图,后面逐项展开。
核心栈
Angular 21 standalone,zoneless。 大型结构化工作台 UI 需要的是模板强类型(strictTemplates 加 full AOT)和 DI 支撑的「服务优先」分层,Angular 在这两点上最成熟。zoneless(provideZonelessChangeDetection(),配置在 src/app/app.config.ts)意味着变更检测完全由 Signals 驱动:没有 zone 的补丁开销,也没有隐式刷新。代价是纪律,全部组件 OnPush,模板只读信号,跨组件联动必须走信号通道,不能指望 zone 兜底。备选 React/Vue 没有认真评估过,这个项目从第一天就是 Angular 团队维护,重写的成本永远大于收益。
RxJS,渐进采纳。 2026-09-30 的决策(ADR-01、ADR-15)定了双轨:同步派生用 computed,异步流与事件编排优先 RxJS operators。已落地的三处:core/style-fetch.ts 的拉取内核(from、switchMap、catchError 组合出回退链,Promise 门面不变)、core/persistence/autosave.service.ts 的防抖、UiService 计数通道的 Subject 化。为什么不早这么做也不全量重写:一次性迁移在 3900 多个测试与视觉基线的 0.4% 像素门禁下不可验证,分阶段走。配套纪律是手写 .subscribe( 必须同语句配 takeUntilDestroyed,架构测试文本扫描强制。
Vite 加 @analogjs/vite-plugin-angular,不是 @angular/cli。 原因很具体:仓里有两个自研 vite 插件(mock 底图中间件、maplibre worker 资产发射),需要构建链可编程,CLI 没地方安插这条插件链。manualChunks 精确分包也是自己控制的。版本:vite 8.0.13、插件 2.5.1,配置在 editor/vite.config.ts。
TypeScript 5.9 strict。 strict 加 isolatedModules。有个小坑:analog 插件约束 noEmit:false,所以零产物检查走 npm run typecheck 脚本单独跑 tsc,见 tsconfig 注释。
状态与数据
自写 signal store,不引 NgRx。 全部状态装进四个 store(Style、Config、Form、Interaction),自写类加 immer patches 就能表达撤销、重做与跨文档事务。引状态库是负资产:这个项目的状态复杂度在「写通道语义」而不在「组件间分发」,NgRx 解决的是后者。样板统一为 private signal 加 asReadonly() 暴露加命名 mutator 方法,ImageRegistry 与 StyleStore 是两个参考实现。
immer 的 patches 模式就是撤销语义。 一次 produce 同时产出 patches 与 inversePatches,undo 是补丁反演,不需要快照整份样式。备选的快照式撤销每条历史都要整份深拷贝,在 7 万行的样式上不可行。使用处集中在 core/style-store.service.ts。
原生 fetch 加 RxJS 内核,不引 HttpClient。 请求收编在 core 的三个内核文件里(style-fetch、sprite-fetch、inspection),组件零 fetch。请求面就这么大,引 HttpClientModule 没有收益。
地图与编辑器内核
| 选型 | 版本 | 为什么 |
|---|---|---|
| maplibre-gl | 6.11.2 | 开源免 key 的 Mapbox GL 分支。6 起纯 ESM 无默认导出,worker 接线由 core/map/map-adapter.ts 单点处理,dev 走 setWorkerUrl,prod 由构建插件落盘 .js 资产 |
| @maplibre/maplibre-gl-style-spec | 26.4.4 | 属性面板、校验、文档卡片全部 spec 元数据驱动(validateStyleMin、validateProperty)。升级有 spec-coverage 对账门禁兜底 |
| 自研表达式内核 | 自研 | 属性值双向解析(JSON 表达式与可视化表单互转)需要恒等 round-trip 与结构化编辑,官方包不提供。89 算子注册表加 typecheck 加官方 createExpression 求值对账,设计文档在 design/expression-engine.md |
| monaco-editor | 0.56 | JSON 直编的高亮与多级校验定位。懒加载独立 chunk,失败降级 textarea |
表达式内核多说一句:这不是重新发明轮子的冲动,而是评估过直接用官方 style-spec 的表达式工具后确认它不提供「表达式到表单模型」的投影与反向编辑(ADR-17)。
样式方案
全局单文件 src/styles.css 加 ms- 前缀类名,零个组件级 .html 与 .css 文件,模板全内联。原因:像素复刻项目里,样式散进组件就失去「一处改、全局比对」的能力,视觉基线也难归因。设计令牌记录值在 docs/spec/design-tokens.json,styles.css 头部段与之一致。
两个解禁渐进采纳项,边界都很明确:
- Tailwind v4:只引 theme 与 utilities 两层,preflight 永不启用,全局 reset 会破坏像素基线。既有全局样式不进 layer,新代码 utilities 优先。
- Angular CDK 21:指令形态优先,先例是 modal-shell 的焦点圈禁。Overlay 与 Dialog 要逐个过视觉基线再迁。Material 不引入。
测试与质量
质量门禁是分层的,从最快的单测到最慢的视觉基线,每层回答不同的问题:
几个要点:覆盖率阈值 95/90/95/95 只在 CI 的 coverage 运行触发,本地 npm run test 不加载 coverage 保持快。Playwright 放在 tools/ 独立 npm 包,浏览器驱动不污染应用依赖树。视觉基线是中文界面 11 状态对比(汉化前的英文基准已封存,ADR-11)。Stryker 只 mutate 四个纯函数域(expression.model 加三个 codec),防「测试全绿但断言空洞」。
后端与部署
Rust(axum 加 tower-http)生产服务。为什么需要它:dev 期的 mock 是 vite 内存插件,构建产物里没有,生产需要常驻进程承接 /mock/* 兼容端点、字体与图标上传、数据档案 API。cargo run --release -- --static ../editor/dist 一体化托管,含 SPA 回退与显式 MIME。
明确没有容器化与云配置:当前部署形态是自托管单进程,全仓没有 Dockerfile、vercel、netlify。引入 Docker 属于未来决策,不是遗漏。
文档与工具链
- VitePress(本站):文档站就是 Markdown 加构建,默认死链即构建失败,本地搜索配了 CJK 分词。plantuml 图在构建期渲染为本地 SVG,零外部依赖。
- tools/ 独立包:Playwright 与 pixelmatch 和 editor 依赖树隔离。
- e2e 聚合脚本(
tools/e2e.mjs):自管 dev server 生命周期,起、等就绪、跑套件、收,十七套共享一个实例。
明确不引入的
Router(单工作台单页,ADR-02)、FormsModule(受控控件自研)、HttpClient、NgRx、Material。每项的否决理由都能在 ADR 里找到,被否决项翻案要走演进指南的决策流程,不能评审时顺手引回来。
另外把简化重构的评估文档挂在这里:editor/docs/refactor/SIMPLIFICATION-PLAN.md 是 2026-10 的一次全量读码体检,讲了代码到底胖在哪、瘦身的优先级怎么排。理解「为什么不引更多东西」,读它和读 ADR 一样有用。