209 lines
8.5 KiB
Markdown
209 lines
8.5 KiB
Markdown
# 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. 性能基线脚本:统计渲染 + 上传耗时。
|
||
|
||
---
|
||
后续若需要,我可以直接生成骨架代码与测试样例,请指示。
|