web-font/backend/app.ts
崮生(子虚) b385bc89bc perf(leafer-x-webfont): 脏节点集增量扫描,百万节点画布可正常运行
全量 walk O(全树) 改为事件驱动增量:
- property.change 携带 target(PropertyEvent 五参构造)→ 精确记脏节点
- child.add/remove 携带 child → 整棵子树入脏集(构造期赋值不走 setAttr)
- 订阅集与 Leafer 自家 Watcher 相同(property.change + child.*),
  渲染可靠 = 事件可靠;不再订阅 layout.end(每帧触发,百万级下
  周期性全量 walk 是持续卡顿源)
- 脏节点 >512 退化全量(模板整体替换场景全量更省)
- 全量/增量共用 collect/walkInto/commit,指纹短路保证无变化时
  update 调用归零
- 新增 provider 透传(测试/私有部署)、flushNow(跳过防抖立即提交)、
  updateCalls 计数(量化指纹短路效果)

压力测试(_prof_million.entry.ts,1M Text 节点):
- 初始全量 scan: ~580ms(一次性)
- 打字稳态(emit+flush): 0.024ms/键
- 无文字变化 flush ×100: 0.16ms,update=0
- 内存: 指纹表 O(family 数),与节点数无关

真实 leafer 验证(scripts/test-leafer-incremental.mts):
初始添加/改文字/新增子树全自动捕获,拖拽 50 次 0 请求 0 提交。

附带修复 dev 后端启动:llrt_lr.ts 改动态 import + fsReady
(createRequire 与 tsx ESM loader 模块图隔离导致 implInterface
注册到孤儿实例,mkdir undefined)。
2026-08-15 20:19:27 +08:00

235 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* fs 适配层必须在所有其他 import 之前加载!
* 静态 import 依赖 ESM hoisting无论源码书写顺序llrt/node 适配器都会先于
* 本模块其余 import 执行implInterface 直接给 interface.ts 的导出变量赋值,
* 注册必须先于任何使用)。
* - 构建tsdown define __RUNTIME__裸 require 内联适配器fsReady 已 resolve。
* - devtsx ESMfsReady = import("./node"),由 main() 开头 await见下
* 不能用 top-level await import —— tsdown 的 CJS 产物不支持。
*/
import { fsReady } from "./server/llrt_lr";
import { mimeTypes } from "./server/mime_type";
import type { cMiddleware } from "./server/req_res";
import { SimpleHttpServer } from "./server/server";
import { path_join, readFile, stat, mkdir } from "./interface";
import { parseUrl, jsonResponse, stats, initStats, markStatsDirty } from "./shared";
import { flushStatsSyncSafe } from "./stats_store";
import { enableTempUpload, adminApiKey } from "./config";
import { handleListFonts } from "./routes/fonts";
import { handleGetConfig } from "./routes/config";
import { handleStats, handleStatsEvent } from "./routes/stats";
import { handleUpload } from "./routes/upload";
import { handleFontSubset } from "./routes/subset";
import { handleFontDetail } from "./routes/font_detail";
import { handleFontMeta } from "./routes/font_meta";
import { startTempCleaner } from "./temp_cleaner";
import { initMemoryGate } from "./subset_queue";
import { subsetConcurrency } from "./config";
const ROOT_DIR = "dist";
/** 启动时确保必要目录存在 */
async function ensureDirectories() {
for (const dir of ["font/temp", "font/admin"]) {
try {
await stat(dir);
} catch {
await mkdir(dir);
}
}
}
const logMiddleware: cMiddleware = async (req, res, next) => {
stats.totalRequests++;
markStatsDirty();
const t1 = Date.now();
const r = await next(req, res);
const t2 = Date.now();
const url = parseUrl(req);
console.log(`[${t2 - t1}ms] ${req.method} ${url.pathname}`);
return r;
};
const staticFileMiddleware: cMiddleware = async function (req, _res, next) {
let newRes: Response;
if (req.method === "GET") {
const url = parseUrl(req);
const pathname = url.pathname;
const filePath = path_join(ROOT_DIR, pathname === "/" ? "index.html" : pathname);
/** 防止路径穿越:规范化后必须仍在 dist 目录内 */
if (!filePath.startsWith(ROOT_DIR + "/") && filePath !== ROOT_DIR) {
newRes = new Response("403 Forbidden", {
status: 403,
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
return next(req, newRes);
}
try {
/**
* 解析静态文件路径,处理三种情况:
* 1. 精确文件(如 /assets/app.js→ 直接返回
* 2. 目录(如 /demo→ 尝试 目录/index.htmlSSG 子路由预渲染产物)
* 3. 不存在(如 SPA 动态路由 /blog/123→ fallback 到根 index.html
*/
let resolvedPath = filePath;
const fileStat = await stat(filePath);
if (fileStat.isDirectory()) {
resolvedPath = path_join(filePath, "index.html");
await stat(resolvedPath);
}
const fileContent = await readFile(resolvedPath);
const extname = resolvedPath.split(".").pop() ?? "";
newRes = new Response(fileContent as unknown as BodyInit, {
status: 200,
headers: {
"Content-Type": mimeTypes[extname] || "application/octet-stream",
"Content-Length": `${fileContent.byteLength}`,
},
});
} catch {
/**
* 字体详情页 SSR/fonts/xxx 路径无预渲染文件时,
* 读取 SSG 模板替换占位符后返回完整 HTML。
* handleFontDetail 返回 null 时继续走 SPA fallback。
*/
if (pathname.startsWith("/fonts/")) {
const ssrResponse = await handleFontDetail(pathname);
if (ssrResponse) {
newRes = ssrResponse;
return next(req, newRes);
}
}
/**
* 文件/目录不存在 → SPA fallback返回根 index.html
* 交给前端 vue-router 接管路由(支持未预渲染的动态路由)。
* 带后缀的请求(.js/.css 等静态资源)不 fallback直接 404。
*/
const hasExt = /\.[^/]+$/.test(pathname);
if (hasExt) {
newRes = new Response("404 Not Found", {
status: 404,
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
} else {
const fallbackContent = await readFile(path_join(ROOT_DIR, "index.html"));
newRes = new Response(fallbackContent as unknown as BodyInit, {
status: 200,
headers: {
"Content-Type": "text/html; charset=utf-8",
"Content-Length": `${fallbackContent.byteLength}`,
},
});
}
}
} else {
newRes = new Response("Method Not Allowed", {
status: 405,
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
});
}
return next(req, newRes);
};
const corsMiddleware: cMiddleware = async (req, res, next) => {
if (req.method === "OPTIONS") {
return {
req,
res: new Response("", {
status: 204,
headers: {
"Content-Length": "0",
},
}),
};
} else {
const newRes = await next(req, res);
newRes.res.headers.append("Access-Control-Allow-Origin", "*");
newRes.res.headers.append("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
newRes.res.headers.append("Access-Control-Allow-Headers", "Content-Type, Authorization");
return newRes;
}
};
/** 统一的 API 路由中间件 */
const fontApiMiddleware: cMiddleware = async (req, res, next) => {
const url = parseUrl(req);
if (!url.pathname.startsWith("/api")) return next(req, res);
if (url.pathname === "/api/fonts" && req.method === "GET") {
return handleListFonts(req, res);
}
if (url.pathname === "/api/config" && req.method === "GET") {
return handleGetConfig(req, res);
}
if (url.pathname === "/api/stats" && req.method === "GET") {
return handleStats(req, res);
}
if (url.pathname === "/api/stats/event" && req.method === "POST") {
return handleStatsEvent(req, res);
}
if (url.pathname === "/api/upload" && req.method === "POST") {
return handleUpload(req, res);
}
if (url.pathname === "/api/font-meta" && req.method === "GET") {
return handleFontMeta(req, res);
}
if (url.pathname === "/api" && req.method === "GET") {
return handleFontSubset(req, res);
}
return next(req, res);
};
/** 上传文件大小限制 50MB */
const MAX_UPLOAD_SIZE = 50 * 1024 * 1024;
const uploadSizeMiddleware: cMiddleware = async (req, res, next) => {
if (req.method === "POST" && parseUrl(req).pathname === "/api/upload") {
const contentLength = parseInt(req.headers.get("Content-Length") ?? "0", 10);
if (contentLength > MAX_UPLOAD_SIZE) {
return {
req,
res: jsonResponse({ success: false, error: "文件过大,最大 50MB" }, 413),
};
}
}
return next(req, res);
};
async function main() {
/** devtsx ESM下适配器经动态 import 注册,必须先等它完成 */
await fsReady;
/** 最早期恢复累计计数:之后的请求计数会累加在历史值之上 */
await initStats();
/** 优雅退出时尽力落盘最后一次增量SIGKILL 时由定时器兜底) */
globalThis.process?.on?.("beforeExit", () => {
markStatsDirty();
flushStatsSyncSafe();
});
await ensureDirectories();
const server = new SimpleHttpServer({ port: 8087 });
server.use(
logMiddleware,
corsMiddleware,
uploadSizeMiddleware,
fontApiMiddleware,
staticFileMiddleware,
);
console.log("[config] temp upload:", enableTempUpload);
console.log("[config] admin upload:", !!adminApiKey);
/** 初始化子集化并发队列(含字体分组调度) */
initMemoryGate(subsetConcurrency);
/** 启动临时字体定时清理器 */
startTempCleaner();
}
main();