# RenderService Blueprint > 目的:规划下一代渲染服务以替换(或包装)现有 `ArticleRender` + `MarkedParser` 组合,降低耦合,支持插件化与增量演进。 > 关联文档:`architecture.md`, `image-pipeline.md`, `detaildesign.md`。 ## 1. 现状问题 (Current Pain Points) | 问题 | 说明 | 影响 | |------|------|------| | 逻辑集中 | `ArticleRender` 同时负责读取、解析、样式拼接、延迟元素、上传协调 | 难测试 / 难替换子环节 | | 隐式状态 | `originalMarkdown`, `cachedElements` 内部字段耦合流程顺序 | 外部无法复用或二次解析 | | 图片收集耦合 Marked 扩展 | 依赖前置 wikilink 转换 | 新语法添加需改内核 | | 缺少中间 IR | 无法对 Token/AST 做多阶段转换 | 功能扩展(如统计、Lint)困难 | | 发布耦合渲染 | 上传/发布 API 与渲染混合 | 交互模式无法独立测试 | | 单线程串行 | 图形生成、图片上传均串行 | 性能受限 | ## 2. 设计目标 (Design Goals) | 目标 | 描述 | 衡量指标 | |------|------|----------| | 分层解耦 | 渲染、转换、资源收集、发布分离 | 新增图片语法无需改核心类 | | 插件式中间件 | 可在 parse → transform → render 各阶段注入 | 插件注册 API 清晰 | | 可测试 | 纯函数/无副作用阶段隔离 | 单元测试覆盖率提升 | | IR 标准化 | 生成统一 AST / 节点类型 | 后续可序列化/缓存 | | 并发能力 | 图形/上传并发控制 | 大图文耗时下降 | | 可观测性 | 事件 & 钩子 | before/after metrics 输出 | ## 3. 拟议分层 (Proposed Layers) ``` RenderPipeline ├─ Loader (读取源 Markdown / 资源定位) ├─ Frontmatter (解析 + Meta Store) ├─ Preprocessors[] (文本级转换:图片语法、短代码、宏) ├─ Parser (Markdown -> AST 统一节点树) ├─ Transformers[] (AST 级:节点重写、图片规范化、封面推断) ├─ ResourceIndex (图片/图形/嵌入引用索引构建) ├─ Renderer (AST -> HTML Fragment) ├─ Postprocessors[] (HTML 级:内联样式、链接修复、统计) └─ Exporters (HTML/Text/JSON/WeChatDraft) ``` ## 4. 关键接口 (Core Interfaces - Draft) ```ts interface RenderContext { file: TFile; raw: string; // 原始 Markdown frontmatter?: Record; meta: WeChatArticleMeta; // 标题/作者/封面等 ast?: MdAstRoot; // 标准化 AST resources: ResourceIndex; // 图片/图形等 html?: string; // 渲染产物 diagnostics: Diagnostic[]; flags: Record; } interface PipelineStage { name: string; run(ctx: RenderContext): Promise | void; order?: number; // 可选执行排序 } interface ResourceIndex { images: ImageCandidate[]; diagrams: DiagramCandidate[]; embeds: EmbedRef[]; } interface ImageCandidate { id: string; // 稳定标识 kind: 'local'|'remote'|'base64'|'generated'; original: string; // 原始文本或路径 basename?: string; vaultPath?: string; position: number; // 在 raw 中的位置,用于封面决策 meta?: Record; // 尺寸/对齐等 } ``` ## 5. 执行模型 (Execution Model) ```ts class RenderService { private stages: PipelineStage[] = []; use(stage: PipelineStage) { this.stages.push(stage); } async render(file: TFile): Promise { const ctx = createInitialContext(file); for (const s of sortByOrder(this.stages)) { try { await s.run(ctx); } catch (e) { ctx.diagnostics.push({stage: s.name, error: e}); if (isFatal(e)) break; } } return ctx; } } ``` ## 6. 示例阶段实现草稿 | Stage | 说明 | 是否必须 | |-------|------|----------| | loader | 读取文件与 raw | 是 | | frontmatter | 解析 YAML -> ctx.frontmatter / ctx.meta 预填 | 是 | | markdownImageCompat | Markdown 图片 -> 统一节点形式 | 可选 | | shortcodeGallery | 解析 gallery → 多个 image 节点 | 可选 | | parser | Marked/Remark → AST | 是 | | imageCollect | AST 遍历收集图片,写入 ctx.resources.images | 是 | | coverInfer | 若无封面,从 images 按 position 选首图 | 可选 | | renderHTML | AST -> HTML 字符串 | 是 | | styleInline | 合成主题/高亮,自定义 CSS 注入 | 可选 | | finalize | 产物整理 / hash / 缓存存储 | 可选 | ## 7. AST 规范 (Simplified) ```ts type MdNode = Paragraph | Heading | Code | Image | Link | List | Blockquote | Html | ThematicBreak | Container; interface Image { type:'image'; id:string; alt:string; url:string; title?:string; meta?:{width?:number;height?:number;align?:string;} } ``` 可基于 remark/unified 生态替换当前 marked,或构造最小抽象后桥接。 ## 8. 事件 & Hook 设计 ```ts interface RenderHooks { on(stage: string, fn: (ctx: RenderContext) => void): void; emit(stage: string, ctx: RenderContext): void; } ``` 预定义事件:`before:stageName` / `after:stageName` / `error:stageName`。 ## 9. 并发策略 - 图片上传与图形生成移出核心渲染,进入 `PublisherService`。 - `PublisherService.publish(ctx, opts)`:接受 RenderContext,内部: 1. 对 `ctx.resources.images` 并发池(PromisePool)上传。 2. 替换 `ctx.html` 中引用。 3. 构建 `DraftArticle` / `DraftImages`。 ## 10. 迁移计划 (Phased Migration) | 阶段 | 内容 | 验证点 | |------|------|--------| | P0 | 搭建 RenderService 空管线 + 复用旧 ArticleRender 结果 | 构建无回归 | | P1 | 拆出 loader/frontmatter/parser/coverInfer | 旧行为对比 snapshot | | P2 | 新 AST + imageCollect,弃用 wikilink 转换 hack | 图片计数稳定 | | P3 | 发布逻辑重构到 PublisherService | 草稿发布一致 | | P4 | Hook/插件系统开放 | 外部扩展示例 | | P5 | 并发上传 + 缓存 | 性能基线下降 | ## 11. 回滚策略 - 每阶段保留配置开关:`useLegacyRenderer`。 - 出现渲染差异:比较 ctx.html 与 legacy.html diff 提示。 - 发布失败回退:走旧 `ArticleRender.postArticle`。 ## 12. 测试策略 | 测试 | 说明 | |------|------| | Snapshot 渲染 | 同一 Markdown 输入旧 vs 新 HTML 对比 | | AST 结构 | 断言图片/标题节点数量与顺序 | | 封面选择 | 多组合(frontmatter + 混合图片顺序) | | Hook 调用 | 注册 mock 钩子计次 | | 并发上传 | 人工注入延迟 → 顺序与最终替换正确 | ## 13. 指标与可观测 - ctx.diagnostics 数量与类型统计。 - 阶段耗时:`performance.now()` 差值注入 ctx.flags。 - 上传耗时 / 失败率。 ## 14. 安全考量 - Stage 插件沙箱:限制访问仅上下文公开字段。 - 阶段超时(可选):超过阈值标记 warning。 - HTML 输出再次 sanitize。 ## 15. API 草案 ```ts const rs = new RenderService(); rs.use(loader()); rs.use(frontmatter()); rs.use(markdownImageCompat({ enable: settings.enableMarkdownImageToWikilink })); rs.use(parser({ engine:'remark' })); rs.use(imageCollect()); rs.use(coverInfer()); rs.use(renderHTML({ themeProvider, highlightProvider })); rs.use(styleInline()); const ctx = await rs.render(activeFile); const draft = await publisher.publish(ctx, { type:'article', coverMode:'auto' }); ``` ## 16. 依赖与选型比较 | 方向 | 方案 A | 方案 B | 选择建议 | |------|-------|-------|----------| | Markdown AST | remark/unified | 继续 marked + 自建 AST 映射 | remark 更标准;初期可混合 | | Hook 实现 | 事件总线 (mitt) | 简单数组回调 | 先内建数组回调,后期引入库 | | 并发控制 | p-limit | 自写 PromisePool | p-limit 简洁可靠 | ## 17. 风险 & 缓解 | 风险 | 缓解 | |------|------| | AST 转换差异导致样式变化 | Snapshot + 逐阶段灰度 | | 性能倒退 | 阶段耗时基线监控;必要时跳过冗余阶段 | | 插件滥用 Hook | 权限白名单 / 文档规范 | | 并发上传触发限流 | 设置最大并发 + 429 重试 | ## 18. 成功判定 (Success Criteria) - 同一输入 50 篇示例笔记,新旧 HTML 差异行 <= 2%(忽略动态 id)。 - 图片收集数量一致,封面判定一致率 100%。 - 20+ 图片大文档总发布耗时下降 ≥25%。 - 可插拔 demo:增加一个统计字数的 Stage 无需改核心。 ## 19. 下一步行动 (Action Items) 1. 建立 `RenderService` 目录与最小类骨架。 2. 搬迁读取/frontmatter/封面逻辑并保留旧 API 外壳。 3. 引入 AST(先轻量:仅 Image/Heading/Paragraph)。 4. Snapshot 测试脚手架。 5. 性能基线脚本:统计渲染 + 上传耗时。 --- 后续若需要,我可以直接生成骨架代码与测试样例,请指示。