refactor: 收敛渲染链路共享抽象
This commit is contained in:
+12
-11
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -47,7 +47,6 @@ function fakeBrowser(
|
||||
waitForFunction: vi.fn(async () => undefined),
|
||||
evaluate: vi.fn(async () => ({
|
||||
pageCount: 2,
|
||||
contentHeight: 2200,
|
||||
mermaidErrors: [],
|
||||
timings: {
|
||||
setupMs: 1,
|
||||
|
||||
Reference in New Issue
Block a user