Skip to content

技术选型 ​

这页回答一个问题:为什么是这套组合。每个依赖都讲三件事:为什么选它、备选是什么、代码里哪里在用。总纲只有一条,这是一个单页工作台工具,选型服务于「可验证地保持行为」,不服务简历。完整决策背景见 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-gl6.11.2开源免 key 的 Mapbox GL 分支。6 起纯 ESM 无默认导出,worker 接线由 core/map/map-adapter.ts 单点处理,dev 走 setWorkerUrl,prod 由构建插件落盘 .js 资产
@maplibre/maplibre-gl-style-spec26.4.4属性面板、校验、文档卡片全部 spec 元数据驱动(validateStyleMin、validateProperty)。升级有 spec-coverage 对账门禁兜底
自研表达式内核自研属性值双向解析(JSON 表达式与可视化表单互转)需要恒等 round-trip 与结构化编辑,官方包不提供。89 算子注册表加 typecheck 加官方 createExpression 求值对账,设计文档在 design/expression-engine.md
monaco-editor0.56JSON 直编的高亮与多级校验定位。懒加载独立 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 一样有用。