refactor: 收敛渲染链路共享抽象

This commit is contained in:
SkyJourney
2026-07-27 00:46:16 +08:00
parent b459def232
commit ad18149c10
17 changed files with 480 additions and 255 deletions
+12 -11
View File
@@ -4,6 +4,7 @@ import { performance } from "node:perf_hooks";
import Fastify from "fastify";
import {
EXPORT_CONFIG_VERSION,
createPagedDocumentPayload,
defaultExportConfig,
exportConfigSchema,
supportedPaperFormats
@@ -354,17 +355,17 @@ export function buildApp(options: BuildAppOptions = {}) {
const rendered = renderedResult.document;
try {
const generated = await pdfGenerator.generate({
articleHtml: rendered.articleHtml,
fileName:
typeof request.body.fileName === "string"
? request.body.fileName
: "文档.md",
metadata: rendered.metadata,
features: rendered.features,
themeCss,
exportConfig: parsedConfig.data
});
const generated = await pdfGenerator.generate(
createPagedDocumentPayload({
document: rendered,
fileName:
typeof request.body.fileName === "string"
? request.body.fileName
: "文档.md",
themeCss,
exportConfig: parsedConfig.data
})
);
const pdfFileName = createPdfFileName(request.body.fileName);
const requestMs = performance.now() - requestStartedAt;
const serverTiming = createPdfServerTiming(
+7 -33
View File
@@ -1,21 +1,11 @@
import { chromium, type Browser } from "playwright";
import { performance } from "node:perf_hooks";
import type { ExportConfig } from "@md-to-pdf/core";
import type {
PagedDocumentPayload,
PagedDocumentRenderResult
} from "@md-to-pdf/core";
export interface PdfRenderPayload {
articleHtml: string;
fileName: string;
metadata: {
title: string;
author: string;
subject: string;
keywords: string[];
language: string;
};
features: string[];
themeCss: string;
exportConfig: ExportConfig;
}
export type PdfRenderPayload = PagedDocumentPayload;
export interface PdfGenerationResult {
pdf: Buffer;
@@ -62,22 +52,6 @@ export interface PdfEngineRuntimeLimits {
timeoutMs: number;
}
interface PagedRuntimeResult {
pageCount: number;
contentHeight: number;
mermaidErrors: string[];
timings: {
setupMs: number;
mermaidMs: number;
mermaidFitMs: number;
mermaidConversionMs: number;
resourceWaitMs: number;
paginationMs: number;
finalizeMs: number;
totalMs: number;
};
}
interface PdfDocumentResult
extends Omit<PdfGenerationResult, "timings"> {
timings: Pick<
@@ -457,13 +431,13 @@ export class PlaywrightPdfGenerator implements PdfGenerator {
const documentRenderStartedAt = performance.now();
const renderResult = await page.evaluate(
async (renderPayload): Promise<PagedRuntimeResult> => {
async (renderPayload): Promise<PagedDocumentRenderResult> => {
const renderer = (
window as typeof window & {
__mdToPdfRender?: (
payload: PdfRenderPayload,
target: "pdf"
) => Promise<PagedRuntimeResult>;
) => Promise<PagedDocumentRenderResult>;
}
).__mdToPdfRender;
if (!renderer) {
+37 -1
View File
@@ -27,6 +27,7 @@ export interface ThemeRegistryOptions {
bundledRoot: string;
localRoot: string;
onWarning?: (message: string) => void;
cacheTtlMs?: number;
}
const assetContentTypes: Record<string, string> = {
@@ -275,7 +276,18 @@ async function loadThemeCssFile(
}
export function createThemeRegistry(options: ThemeRegistryOptions) {
async function list() {
const cacheTtlMs = options.cacheTtlMs ?? 1_000;
if (!Number.isFinite(cacheTtlMs) || cacheTtlMs < 0) {
throw new Error("主题缓存有效期必须是非负有限数值");
}
let cachedThemes:
| {
expiresAt: number;
promise: Promise<ThemeRecord[]>;
}
| undefined;
async function scanThemes() {
const [bundledThemes, localThemes] = await Promise.all([
readThemeRoot(options.bundledRoot, "bundled"),
readThemeRoot(options.localRoot, "local", options.onWarning)
@@ -293,6 +305,29 @@ export function createThemeRegistry(options: ThemeRegistryOptions) {
return themes;
}
function list() {
const now = Date.now();
if (cachedThemes && now < cachedThemes.expiresAt) {
return cachedThemes.promise;
}
const promise = scanThemes();
cachedThemes = {
expiresAt: now + cacheTtlMs,
promise
};
void promise.catch(() => {
if (cachedThemes?.promise === promise) {
cachedThemes = undefined;
}
});
return promise;
}
function invalidate() {
cachedThemes = undefined;
}
async function get(themeId: string) {
return (await list()).find((theme) => theme.manifest.id === themeId);
}
@@ -340,6 +375,7 @@ export function createThemeRegistry(options: ThemeRegistryOptions) {
get,
getAsset,
getCss,
invalidate,
list
};
}
+52
View File
@@ -287,6 +287,58 @@ describe("预览 API", () => {
);
});
it("缓存主题目录扫描并支持主动失效", async () => {
temporaryDirectory = await mkdtemp(join(tmpdir(), "md-to-pdf-theme-"));
const bundledRoot = join(temporaryDirectory, "bundled");
const localRoot = join(temporaryDirectory, "local");
const firstThemeRoot = join(localRoot, "first-theme");
const secondThemeRoot = join(localRoot, "second-theme");
await mkdir(bundledRoot, { recursive: true });
await mkdir(firstThemeRoot, { recursive: true });
await writeFile(
join(firstThemeRoot, "theme.json"),
JSON.stringify(localThemeManifest("first-theme")),
"utf8"
);
await writeFile(
join(firstThemeRoot, "theme.css"),
"#write { color: #333; }",
"utf8"
);
const registry = createThemeRegistry({
bundledRoot,
localRoot,
cacheTtlMs: 60_000
});
await expect(registry.list()).resolves.toHaveLength(1);
await mkdir(secondThemeRoot, { recursive: true });
await writeFile(
join(secondThemeRoot, "theme.json"),
JSON.stringify(localThemeManifest("second-theme")),
"utf8"
);
await writeFile(
join(secondThemeRoot, "theme.css"),
"#write { color: #666; }",
"utf8"
);
await expect(registry.list()).resolves.toHaveLength(1);
registry.invalidate();
await expect(registry.list()).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
manifest: expect.objectContaining({ id: "first-theme" })
}),
expect.objectContaining({
manifest: expect.objectContaining({ id: "second-theme" })
})
])
);
});
it("拒绝主题 CSS 导入外部地址", async () => {
temporaryDirectory = await mkdtemp(join(tmpdir(), "md-to-pdf-theme-"));
const bundledRoot = join(temporaryDirectory, "bundled");
-1
View File
@@ -47,7 +47,6 @@ function fakeBrowser(
waitForFunction: vi.fn(async () => undefined),
evaluate: vi.fn(async () => ({
pageCount: 2,
contentHeight: 2200,
mermaidErrors: [],
timings: {
setupMs: 1,