From 06935217167950b344e57849f08245b9557a9aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=AE=E7=94=9F=EF=BC=88=E5=AD=90=E8=99=9A=EF=BC=89?= <2234839456@qq.com> Date: Sat, 15 Aug 2026 13:07:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20SDK=20=E6=8A=BD=E5=8F=96=E4=B8=BA=20web?= =?UTF-8?q?font-sdk=20npm=20=E5=8C=85=20+=20leafer-x-webfont=20=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=EF=BC=88=E6=B5=8F=E8=A7=88=E5=99=A8=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E9=80=9A=E8=BF=87=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - packages/webfont-sdk:TS 重写,IncrementalEngine 核心(去重/并发池/失败字符记忆/retryFailed) - CSS 模式(WebFont)API 与旧 public/webfont-sdk.js 完全兼容 - FontFace 模式(WebFontCanvas)供 Canvas 场景,unicodeRange 注册 + onChunk 回调 - tsdown 双产物:dist/(ESM+dts)+ dist-iife/(script 直引) - scripts/sync-public.mjs 构建后同步到 public/webfont-sdk.js - 修复 banner 内嵌套块注释导致的语法错误(/* 重绘 */ 提前闭合 banner) - packages/leafer-x-webfont:LeaferJS 插件,依赖 webfont-sdk - 监听 property.change + layout.end 防抖扫描 Text 节点聚合字符 - 新旧 API 兼容(on_/on__、off_/off__) - 注册后自动改写 fontFamily 为合法 CSS 名(去 .ttf 后缀) - demo/index.html:海报场景(标题/副标题/字体切换/导出 PNG)本地 vendor 验证通过 - backend/app.ts:fs 适配层改为 top-level await import(修复 dev 模式 mkdir 崩溃) - doc/:商业化思路一/二评估 + 主动拓客文档 --- .gitignore | 6 +- backend/app.ts | 13 +- packages/leafer-x-webfont/README.md | 79 ++ packages/leafer-x-webfont/demo/index.html | 144 +++ packages/leafer-x-webfont/package.json | 47 + packages/leafer-x-webfont/src/index.ts | 303 ++++++ packages/leafer-x-webfont/tsconfig.json | 17 + packages/leafer-x-webfont/tsdown.config.ts | 10 + packages/webfont-sdk/package.json | 39 + packages/webfont-sdk/scripts/sync-public.mjs | 43 + packages/webfont-sdk/src/css-mode.ts | 327 ++++++ packages/webfont-sdk/src/engine.ts | 211 ++++ packages/webfont-sdk/src/fontface-mode.ts | 122 +++ packages/webfont-sdk/src/iife.ts | 19 + packages/webfont-sdk/src/index.ts | 29 + packages/webfont-sdk/tsconfig.json | 17 + packages/webfont-sdk/tsdown.config.ts | 23 + pnpm-lock.yaml | 468 +++++++++ pnpm-workspace.yaml | 2 + public/webfont-sdk.js | 985 +++++++++---------- 20 files changed, 2382 insertions(+), 522 deletions(-) create mode 100644 packages/leafer-x-webfont/README.md create mode 100644 packages/leafer-x-webfont/demo/index.html create mode 100644 packages/leafer-x-webfont/package.json create mode 100644 packages/leafer-x-webfont/src/index.ts create mode 100644 packages/leafer-x-webfont/tsconfig.json create mode 100644 packages/leafer-x-webfont/tsdown.config.ts create mode 100644 packages/webfont-sdk/package.json create mode 100644 packages/webfont-sdk/scripts/sync-public.mjs create mode 100644 packages/webfont-sdk/src/css-mode.ts create mode 100644 packages/webfont-sdk/src/engine.ts create mode 100644 packages/webfont-sdk/src/fontface-mode.ts create mode 100644 packages/webfont-sdk/src/iife.ts create mode 100644 packages/webfont-sdk/src/index.ts create mode 100644 packages/webfont-sdk/tsconfig.json create mode 100644 packages/webfont-sdk/tsdown.config.ts diff --git a/.gitignore b/.gitignore index f6ed60d..e4e90fc 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,8 @@ dist_backend_node verify_font_baseline benchmark_results -.claude \ No newline at end of file +.claude +# packages 构建产物与 demo vendor(可重建/可下载,不入库) +packages/*/dist +packages/*/dist-iife +packages/leafer-x-webfont/demo/vendor diff --git a/backend/app.ts b/backend/app.ts index 25ac93d..84b1043 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -1,9 +1,10 @@ -/** 首先加载 fs 适配层,必须在所有其他导入之前!仅在 LLRT 运行时引入 */ -(() => { - if (typeof __RUNTIME__ !== "undefined" && __RUNTIME__ === "llrt") { - require("./server/llrt"); - } -})(); +/** 首先加载 fs 适配层,必须在 main() 之前完成!按运行时选择实现(top-level await 保证注册先于一切使用) */ +if (typeof __RUNTIME__ !== "undefined" && __RUNTIME__ === "llrt") { + await import("./server/llrt"); +} else { + /** Node.js(tsx 开发 / tsdown define "node" 构建)走 node 适配器 */ + await import("./server/node"); +} import { mimeTypes } from "./server/mime_type"; import type { cMiddleware } from "./server/req_res"; diff --git a/packages/leafer-x-webfont/README.md b/packages/leafer-x-webfont/README.md new file mode 100644 index 0000000..8594e8f --- /dev/null +++ b/packages/leafer-x-webfont/README.md @@ -0,0 +1,79 @@ +# leafer-x-webfont + +> LeaferJS 中文字体插件 —— 画布里的 Text 用什么字,就只加载那几个字(6 字 ≈ 6KB,而非 16MB)。 + +## 为什么需要它 + +Leafer 的 `Text` 元素渲染时直接拼 `canvas.font = fontFamily`,依赖浏览器字体系统。 +中文字体动辄 10MB+,`FontFace` 注册又慢又耗流量,海报/设计器场景根本没法用。 + +本插件订阅画布内 `Text` 的 `text` / `fontFamily` 变化,只对**实际用到的字符**调用 +[webfont](https://github.com/2234839/web-font) 子集化 API,注册 KB 级子集字体后自动重渲染画布: + +``` +new Text({ text: '静心茶舍', fontFamily: '令东齐伋复刻体.ttf' }) +→ 服务端裁剪 → 返回 ~6KB 子集 → FontFace 注册 → 画布自动重绘 +``` + +## 安装 + +```bash +npm install leafer-x-webfont +``` + +## 使用 + +```ts +import { Leafer, Text } from 'leafer-ui' +import { WebFontPlugin } from 'leafer-x-webfont' + +const leafer = new Leafer({ view: window }) + +// 一行接入 +const webfont = new WebFontPlugin(leafer) + +leafer.add(new Text({ text: '静心茶舍', fontFamily: '令东齐伋复刻体.ttf', fontSize: 64 })) +// 字体到位后画布自动重渲染 +``` + +### 导出图片前 + +```ts +await webfont.ready() // 等待所有已用字符的子集注册完成 +const blob = await leafer.export('png', { pixelRatio: 2 }) +``` + +### 配置项 + +```ts +new WebFontPlugin(leafer, { + baseUrl: 'https://webfont.shenzilong.cn', // 自部署时改这里 + outType: 'woff2', + debounceMs: 120, + watch: true, // 持续监听画布变化;静态海报导出可关掉 + debug: false, + resolveFont: null, // 自定义 fontFamily 解析规则,返回 null 跳过 +}) +``` + +## 特性 + +- **零配置**:任意 `fontFamily`(含 `xxx.ttf` 文件名写法)自动识别,字体不存在时静默回退 +- **增量去重**:同一字体下字符集只增不减,改一个字只多发一个字符的子集请求 +- **失败记忆**:字体不含的字符自动记入失败集,不反复 404 +- **导出友好**:`webfont.ready()` 保证 `leafer.export()` 时字体已注册 + +## 本地开发 + +```bash +# 仓库根目录 +pnpm install +pnpm dev # 起前后端 + +# 打开 demo(插件源码直引,无需构建) +open packages/leafer-x-webfont/demo/index.html +``` + +## License + +MIT diff --git a/packages/leafer-x-webfont/demo/index.html b/packages/leafer-x-webfont/demo/index.html new file mode 100644 index 0000000..81e8e78 --- /dev/null +++ b/packages/leafer-x-webfont/demo/index.html @@ -0,0 +1,144 @@ + + + + + +leafer-x-webfont —— LeaferJS 海报中文字体 Demo + + + +
+

leafer-x-webfont

+

+ LeaferJS 中文字体插件 —— 画布里的 Text 用什么字体,就只加载那几个字的子集(6 字 ≈ 6KB,而非 16MB)。 + 服务端:webfont.shenzilong.cn + / GitHub +

+ +
+ + + + + + 初始化… +
+ +
+
+ + + + + + + + + diff --git a/packages/leafer-x-webfont/package.json b/packages/leafer-x-webfont/package.json new file mode 100644 index 0000000..fdeba52 --- /dev/null +++ b/packages/leafer-x-webfont/package.json @@ -0,0 +1,47 @@ +{ + "name": "leafer-x-webfont", + "version": "0.1.0", + "description": "LeaferJS 字体插件 —— Text 元素自动按需加载字体子集(6 字 ≈ 6KB),让画布与导出图用上任意中文字体", + "type": "module", + "main": "./dist/leafer-x-webfont.esm.js", + "module": "./dist/leafer-x-webfont.esm.js", + "types": "./types/index.d.ts", + "exports": { + ".": { + "types": "./types/index.d.ts", + "import": "./dist/leafer-x-webfont.esm.js" + } + }, + "files": [ + "dist", + "types", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "tsdown", + "typecheck": "tsc --noEmit" + }, + "keywords": [ + "leafer", + "leaferjs", + "webfont", + "font-subset", + "canvas-font", + "chinese-font", + "poster" + ], + "repository": "https://github.com/2234839/web-font", + "license": "MIT", + "peerDependencies": { + "leafer-ui": "^2.0.0" + }, + "peerDependenciesMeta": { + "leafer-ui": { + "optional": true + } + }, + "dependencies": { + "webfont-sdk": "workspace:*" + } +} diff --git a/packages/leafer-x-webfont/src/index.ts b/packages/leafer-x-webfont/src/index.ts new file mode 100644 index 0000000..f903903 --- /dev/null +++ b/packages/leafer-x-webfont/src/index.ts @@ -0,0 +1,303 @@ +/** + * leafer-x-webfont —— LeaferJS 字体按需加载插件 + * + * 原理:Leafer 的 Text 元素渲染时直接拼 `canvas.font = fontFamily`, + * 依赖浏览器字体系统。大字体(尤其中文字体 10MB+)无法直接注册。 + * 本插件订阅画布内 Text 的 text / fontFamily 变化,把「实际用到的字符」 + * 交给 webfont-sdk(FontFace 模式)增量加载:fetch 子集 buffer → + * FontFace(unicodeRange) 注册 → forceRender 重绘。 + * + * 去重 / 并发池 / 失败字符记忆 / provider 抽象全部在 webfont-sdk 引擎层实现, + * 本插件只做 Leafer 桥接: + * - 树扫描(walk)聚合同 family 字符 + * - property.change + layout.end 事件订阅(防抖) + * - fontFamily 规范化改写('xx.ttf' → 'xx',canvas font 串要求合法标识符) + * - 片段就绪后 forceRender + * + * 用法: + * ```ts + * import { Leafer, Text } from 'leafer-ui' + * import { WebFontPlugin } from 'leafer-x-webfont' + * + * const leafer = new Leafer({ view: window }) + * const webfont = new WebFontPlugin(leafer) + * leafer.add(new Text({ text: '静心茶舍', fontFamily: '令东齐伋复刻体.ttf', fontSize: 64 })) + * // 字体到位后画布自动重渲染;导出前 await webfont.ready() + * ``` + */ +import { WebFontFontFaceMode, type IFontFaceLoader } from 'webfont-sdk' + +/** 插件配置 */ +export interface IWebFontPluginConfig { + /** 子集化服务基地址,默认官方在线服务 */ + baseUrl?: string + /** + * fontFamily 属性的解析规则: + * - 不传(默认):任意 fontFamily 值(如 '令东齐伋复刻体.ttf'、'霞鹜文楷')都尝试向 API 请求; + * 失败字符自动记忆不重试(webfont-sdk 引擎层),不阻塞渲染 + * - 传入函数则完全自定义:返回 null 表示不处理该字体 + */ + resolveFont?: (fontFamily: string) => string | null + /** 请求子集时的输出格式 */ + outType?: 'woff2' | 'ttf' + /** 文本变化防抖(ms),打字场景避免每敲一键发一次请求 */ + debounceMs?: number + /** + * 初始全量扫描后,是否持续监听画布变化(默认 true)。 + * 关闭后仅处理创建时已存在的文本,适合静态海报导出 + */ + watch?: boolean + /** 是否在控制台输出调试日志 */ + debug?: boolean + /** + * 是否自动把节点 fontFamily 改写为合法 CSS family 名(去掉 .ttf 等后缀)。 + * 默认 true:canvas font 解析不认带扩展名的 family,不改写会静默回退系统字体 + */ + rewriteFamily?: boolean +} + +/** fontFamily 里的文件后缀(注册 FontFace / CSS 都不认) */ +const FONT_EXT_RE = /\.(ttf|otf|woff2?|ttc)$/i +/** 泛型族名没有对应字体文件,跳过 */ +const GENERIC_FAMILY_RE = /^(sans-serif|serif|monospace|caption|system-ui|cursive|fantasy)$/i + +/** Leafer 节点最小结构(避免硬依赖 leafer-ui 类型,保持 peerDep 可选) */ +interface ILeaferNode { + __tag?: string + text?: unknown + fontFamily?: unknown + children?: unknown + destroyed?: boolean + forceRender?: () => void + /** on_ 为公开事件订阅(返回 id);旧版 leafer 用 on__(下划线为内部 id 绑定) */ + on_?: (type: string, listener: (e: unknown) => void, bind?: unknown) => number + off_?: (ids: number[]) => void + /** 旧版 API 兼容(on__ 在新版本已更名 on_) */ + on__?: (type: string, listener: (e: unknown) => void, bind?: unknown) => number + off__?: (ids: number[]) => void + waitViewReady?: (cb: () => void) => void +} + +export class WebFontPlugin { + /** 宿主 Leafer 实例 */ + private leafer: ILeaferNode | null + private config: Required> & + IWebFontPluginConfig + + /** SDK FontFace 模式(增量引擎在这层:去重/并发/失败记忆) */ + private mode: WebFontFontFaceMode + /** family -> 增量加载器(由 SDK 管理) */ + private loaders = new Map() + /** 防抖定时器 */ + private debounceTimer: ReturnType | null = null + /** 解绑事件用的 id 列表 */ + private eventIds: number[] = [] + /** 统一的事件解绑函数(on_/off_ 新旧版别名解析后的句柄) */ + private offEvents: ((ids: number[]) => void) | null = null + + constructor(leafer: ILeaferNode, config: IWebFontPluginConfig = {}) { + this.leafer = leafer + this.config = { + debounceMs: config.debounceMs ?? 120, + watch: config.watch ?? true, + debug: config.debug ?? false, + rewriteFamily: config.rewriteFamily ?? true, + baseUrl: config.baseUrl, + outType: config.outType, + resolveFont: config.resolveFont, + } + + this.mode = new WebFontFontFaceMode({ + baseUrl: config.baseUrl, + provider: null, + }) + + this.bindEvents() + /** 画布初始化完成后做一次全量扫描 */ + leafer.waitViewReady?.(() => this.scan()) + } + + /* ============================================================ + * 事件绑定 —— 监听 Text 属性变化与新增节点 + * ============================================================ */ + + private bindEvents(): void { + if (!this.config.watch) return + + /** + * 事件订阅:新版 leafer-ui 是 on_/off_,旧版为 on__/off__。 + * 统一收敛到 on_/off_ 两个别名上,构造时一次性解析。 + */ + const leafer = this.leafer! + const on = leafer.on_ ?? leafer.on__ + const off = leafer.off_ ?? leafer.off__ + this.offEvents = off ? (ids) => off.call(leafer, ids) : null + + /** + * Leafer 的属性变化事件(PropertyEvent.CHANGE = 'property.change')由每个 Leaf + * 直接 emit 到 leafer 根节点(见 leafer 源码 LeafDataProxy.emitPropertyEvent: + * `leafer.emitEvent(event)`),在根上监听即可捕获所有子元素的 text / fontFamily + * 变化。用字符串而非导入常量,保持对 leafer-ui 的 peerDep 可选。 + */ + this.eventIds.push( + on!.call(leafer, 'property.change', (e) => { + const ev = e as { attrName?: string } + if (ev.attrName === 'text' || ev.attrName === 'fontFamily') { + this.schedule() + } + }), + ) + + /** 布局结束(新增/删除节点都会触发布局)——覆盖新增 Text、海报模板切换等场景 */ + this.eventIds.push( + on!.call(leafer, 'layout.end', () => this.schedule()), + ) + } + + /* ============================================================ + * 扫描与调度 + * ============================================================ */ + + /** 全量扫描画布中所有 Text 的 text + fontFamily,按 family 聚合新字符 */ + private scan(): void { + const groups = new Map }>() + + const walk = (node: ILeaferNode | null | undefined): void => { + if (!node || node.destroyed) return + const isText = node.__tag === 'Text' || (typeof node.text === 'string' && typeof node.fontFamily === 'string') + if (isText) { + const fontFamily: string = node.fontFamily as string + const text: string = String(node.text ?? '') + if (fontFamily && text) { + const fontName = this.resolveFontName(fontFamily) + if (fontName) { + const family = normalizeFamily(fontName) + const entry = getOrCreate(groups, family, () => ({ loader: this.getLoader(fontName, family), fontName, family, chars: new Set() })) + /** 自动改写节点 fontFamily 为合法 CSS 名(canvas font 串要求) */ + if (this.config.rewriteFamily && node.fontFamily !== family) { + ;(node as { fontFamily: string }).fontFamily = family + } + for (const ch of text) entry.chars.add(ch) + } + } + } + const children = node.children + if (Array.isArray(children)) { + for (const child of children as ILeaferNode[]) walk(child) + } + } + + walk(this.leafer) + + for (const [, { loader, chars }] of groups) { + loader.update(charsToString(chars)) + } + } + + /** 防抖触发扫描 */ + private schedule(): void { + if (this.debounceTimer) clearTimeout(this.debounceTimer) + this.debounceTimer = setTimeout(() => { + this.debounceTimer = null + this.scan() + }, this.config.debounceMs) + } + + /* ============================================================ + * 字体解析与加载器管理 + * ============================================================ */ + + /** + * 解析 fontFamily:决定是否需要走子集化。 + * 用户自定义 resolveFont 优先;默认策略是「非泛型族名都尝试」。 + */ + private resolveFontName(fontFamily: string): string | null { + if (this.config.resolveFont) return this.config.resolveFont(fontFamily) + if (GENERIC_FAMILY_RE.test(fontFamily.trim())) return null + return fontFamily + } + + /** 获取(或创建)family 对应的 SDK 增量加载器;注册成功后重绘画布 */ + private getLoader(fontName: string, family: string): IFontFaceLoader { + let loader = this.loaders.get(family) + if (!loader) { + loader = this.mode.loadFontFace( + { fontName, family }, + () => { + /** 字体注册成功后强制重绘整个画布(文本 metrics 需要重新计算) */ + this.leafer?.forceRender?.() + }, + ) + this.loaders.set(family, loader) + this.log('new font loader:', fontName, '->', family) + } + return loader + } + + private log(...args: unknown[]): void { + if (this.config.debug) console.log('[leafer-x-webfont]', ...args) + } + + /* ============================================================ + * 公开 API + * ============================================================ */ + + /** 立即做一次全量扫描(外部手动改完画布内容后调用) */ + public refresh(): void { + this.scan() + } + + /** + * 等待当前所有待加载字体就绪(用于导出图片前): + * ```ts + * await webfont.ready() + * const blob = await leafer.export('png') + * ``` + * 引擎层在「请求 + FontFace 注册」都完成后才算就绪,导出不会丢字体 + */ + public async ready(): Promise { + await this.mode.ready() + } + + /** 已加载(或加载中)的字体 family 列表(调试 / 状态展示用) */ + public get families(): string[] { + return [...this.loaders.keys()] + } + + /** 销毁插件(解绑事件、丢弃加载器;FontFace 保留在 document.fonts 供继续渲染) */ + public destroy(): void { + if (this.debounceTimer) clearTimeout(this.debounceTimer) + this.offEvents?.(this.eventIds) + this.eventIds.length = 0 + this.offEvents = null + for (const loader of this.loaders.values()) loader.dispose() + this.loaders.clear() + this.leafer = null + } +} + +/* ============================================================ + * 辅助函数 + * ============================================================ */ + +/** fontFamily 原始值 -> 合法 CSS family 名(去除文件后缀与首尾空白) */ +function normalizeFamily(fontFamily: string): string { + return fontFamily.replace(FONT_EXT_RE, '').trim() +} + +/** Map 的 get-or-create 惯用封装 */ +function getOrCreate(map: Map, key: K, create: () => V): V { + let v = map.get(key) + if (!v) { + v = create() + map.set(key, v) + } + return v +} + +/** 字符集合 -> 字符串(保持插入序,便于日志与请求参数稳定) */ +function charsToString(set: Set): string { + let s = '' + for (const c of set) s += c + return s +} diff --git a/packages/leafer-x-webfont/tsconfig.json b/packages/leafer-x-webfont/tsconfig.json new file mode 100644 index 0000000..8ebc547 --- /dev/null +++ b/packages/leafer-x-webfont/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": [], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "tsdown.config.ts"] +} diff --git a/packages/leafer-x-webfont/tsdown.config.ts b/packages/leafer-x-webfont/tsdown.config.ts new file mode 100644 index 0000000..deab19c --- /dev/null +++ b/packages/leafer-x-webfont/tsdown.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + entry: ['src/index.ts'], + format: 'esm', + dts: true, + clean: true, + outDir: 'dist', + platform: 'browser', +}) diff --git a/packages/webfont-sdk/package.json b/packages/webfont-sdk/package.json new file mode 100644 index 0000000..bba1c9e --- /dev/null +++ b/packages/webfont-sdk/package.json @@ -0,0 +1,39 @@ +{ + "name": "webfont-sdk", + "version": "0.1.0", + "description": "Web 字体按需加载 SDK —— 只加载实际用到的字符,增量去重、无闪烁,支持 DOM(CSS) 与 Canvas(FontFace) 两种模式", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "development": "./src/index.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "tsdown && node scripts/sync-public.mjs", + "typecheck": "tsc --noEmit" + }, + "keywords": [ + "webfont", + "font-subset", + "font-face", + "incremental-font", + "canvas-font", + "chinese-font" + ], + "repository": "https://github.com/2234839/web-font", + "license": "MIT", + "devDependencies": { + "tsdown": "^0.22.13", + "typescript": "^7.0.2" + } +} diff --git a/packages/webfont-sdk/scripts/sync-public.mjs b/packages/webfont-sdk/scripts/sync-public.mjs new file mode 100644 index 0000000..cb3dd56 --- /dev/null +++ b/packages/webfont-sdk/scripts/sync-public.mjs @@ -0,0 +1,43 @@ +/** + * 构建后同步脚本:把 IIFE 产物带用法 banner 写入主站 public/webfont-sdk.js + * index.html 引用 /webfont-sdk.js?v=%BUILD_TIME%,产物路径不变、零改动。 + */ +import { readFileSync, writeFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, resolve } from 'node:path' + +const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url))) +const source = resolve(pkgRoot, 'dist-iife/iife.iife.js') +const target = resolve(pkgRoot, '../../public/webfont-sdk.js') + +const banner = `/** + * WebFont SDK — 按需增量加载字体片段,无闪烁(本文件由 packages/webfont-sdk 构建,勿手改) + * + * 架构:核心增量引擎 + 两种注册模式 + * - 核心:IncrementalEngine 按 fontKey 管理字符集,只请求增量;失败字符自动记忆不重试 + * - CSS 模式(WebFont):loadFont(轮询)/ observeFont(DOM 事件)/ loadText(手动传文本) + * - FontFace 模式(WebFontCanvas):Canvas/canvas 场景,FontFace + unicodeRange 注册 + * + * 用法: + * // 轮询模式 + * WebFont.loadFont({ fontName, selector, family, interval }); + * + * // 事件驱动模式 + * var obs = WebFont.observeFont({ fontName, selector, family }); + * obs.dispose(); + * + * // 直接传文本模式 + * var loader = WebFont.loadText({ fontName, text: "你好世界", family }); + * loader.update("追加文字"); + * loader.dispose(); + * + * // Canvas 模式(leafer / 原生 canvas) + * var face = WebFontCanvas.loadFontFace({ fontName }, function (chunk) { 在此重绘 }); + * face.update("画布上的文字"); + * await WebFontCanvas.ready(); + */ + +` + +writeFileSync(target, banner + readFileSync(source, 'utf8')) +console.log(`synced: ${target} (from ${source})`) diff --git a/packages/webfont-sdk/src/css-mode.ts b/packages/webfont-sdk/src/css-mode.ts new file mode 100644 index 0000000..7e13796 --- /dev/null +++ b/packages/webfont-sdk/src/css-mode.ts @@ -0,0 +1,327 @@ +/** + * CSS 模式 —— 注入 @font-face + unicode-range 样式(DOM 场景) + * + * 与原 public/webfont-sdk.js 行为一致: + * - 片段就绪时注入