update at 2025-10-09 12:39:24

This commit is contained in:
douboer
2025-10-09 12:39:24 +08:00
parent a891153be0
commit 6f51916b50
44 changed files with 332 additions and 226 deletions

View File

@@ -0,0 +1,208 @@
# 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<string, any>;
meta: WeChatArticleMeta; // 标题/作者/封面等
ast?: MdAstRoot; // 标准化 AST
resources: ResourceIndex; // 图片/图形等
html?: string; // 渲染产物
diagnostics: Diagnostic[];
flags: Record<string, any>;
}
interface PipelineStage {
name: string;
run(ctx: RenderContext): Promise<void> | 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<string, any>; // 尺寸/对齐等
}
```
## 5. 执行模型 (Execution Model)
```ts
class RenderService {
private stages: PipelineStage[] = [];
use(stage: PipelineStage) { this.stages.push(stage); }
async render(file: TFile): Promise<RenderContext> {
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. 性能基线脚本:统计渲染 + 上传耗时。
---
后续若需要,我可以直接生成骨架代码与测试样例,请指示。