From 9da270733da26529af84085907e31530c0a76bf3 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: Wed, 8 Apr 2026 20:48:05 +0800 Subject: [PATCH] =?UTF-8?q?v1.2.0:=20=E5=A2=9E=E9=87=8F=E5=AD=97=E4=BD=93?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=20SDK=EF=BC=8C=E4=BC=98=E5=8C=96=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 webfont-sdk.js,支持按需增量加载字体片段,无闪烁 - 预览使用 SDK 增量加载,输入时不再重新加载已有字体 - SDK 根据元素类型(input/textarea vs 其他)正确获取文本 - SDK 支持重复调用同一选择器时自动清理旧状态 - 页面说明区分基础用法和进阶用法,提供 SDK 下载链接 - URL 改为完整域名(location.origin),方便用户直接复制使用 - 预览文本框字体大小调至 32px - API Key 输入框改为 text 类型消除密码警告 - SEO 优化:添加 description meta,优化 title Co-Authored-By: Claude Opus 4.6 --- index.html | 4 +- package.json | 2 +- public/webfont-sdk.js | 103 ++++++++++++++++++++++++++++++++++++++++++ src/App.tsx | 70 ++++++++++++++-------------- 4 files changed, 140 insertions(+), 39 deletions(-) create mode 100644 public/webfont-sdk.js diff --git a/index.html b/index.html index 64708e0..2d83af9 100644 --- a/index.html +++ b/index.html @@ -4,10 +4,12 @@ - Web Font + + WebFont — 在线字体裁剪 | 按需加载 | 免费开源
+ diff --git a/package.json b/package.json index 7da4ea5..f4192c9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "webfont", "private": true, - "version": "1.1.0", + "version": "1.2.0", "type": "module", "scripts": { "dev": "pnpx tsx scripts/dev-all.ts", diff --git a/public/webfont-sdk.js b/public/webfont-sdk.js new file mode 100644 index 0000000..93b5a89 --- /dev/null +++ b/public/webfont-sdk.js @@ -0,0 +1,103 @@ +/** + * WebFont SDK — 按需增量加载字体片段,无闪烁 + * 用法:WebFont.loadFont({ fontName, selector, baseUrl, family }) + */ +var WebFont = (function () { + /** 按 selector 索引的活跃任务,重复调用同一选择器时自动清理旧任务 */ + var tasks = {}; + + /** + * @param {Object} options + * @param {string} options.fontName - 字体文件名(如 "思源黑体.ttf") + * @param {string} options.selector - CSS 选择器(如 ".title") + * @param {string} [options.baseUrl] - API 基础地址,默认当前域名 + * @param {string} [options.family] - font-family 名称,默认 fontName 去掉扩展名 + */ + function loadFont(options) { + var selector = options.selector; + var fontName = options.fontName; + var baseUrl = options.baseUrl || location.origin; + var family = options.family || fontName.replace(/\.[^.]+$/, ""); + + /** 清理同一选择器的旧任务 */ + if (tasks[selector]) { + clearInterval(tasks[selector].timer); + /** 移除旧任务注入的 style 标签 */ + var oldStyles = tasks[selector].styles; + for (var s = 0; s < oldStyles.length; s++) { + oldStyles[s].remove(); + } + } + + var loadedChars = {}; + var injectedStyles = []; + var applied = false; + + /** 获取元素的文本内容 */ + function getText(el) { + var tag = el.tagName; + if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") { + return el.value || ""; + } + return el.textContent || ""; + } + + function collectChars() { + var charSet = {}; + var elements = document.querySelectorAll(selector); + for (var i = 0; i < elements.length; i++) { + var text = getText(elements[i]); + for (var j = 0; j < text.length; j++) { + charSet[text[j]] = true; + } + } + return charSet; + } + + function loadNewChars() { + var current = collectChars(); + var newChars = []; + for (var c in current) { + if (!loadedChars[c]) { + newChars.push(c); + } + } + if (newChars.length === 0) return; + + for (var k = 0; k < newChars.length; k++) { + loadedChars[newChars[k]] = true; + } + + var text = newChars.join(""); + var url = baseUrl + "/api?font=" + encodeURIComponent(fontName) + "&text=" + encodeURIComponent(text); + var unicodeRanges = newChars + .map(function (c) { return "U+" + c.codePointAt(0).toString(16).padStart(4, "0"); }) + .join(", "); + + var style = document.createElement("style"); + style.textContent = + '@font-face {\n' + + ' font-family: "' + family + '";\n' + + ' src: url("' + url + '") format("truetype");\n' + + ' unicode-range: ' + unicodeRanges + ';\n' + + '}\n'; + document.head.appendChild(style); + injectedStyles.push(style); + + if (!applied) { + applied = true; + var elements = document.querySelectorAll(selector); + for (var i = 0; i < elements.length; i++) { + elements[i].style.fontFamily = '"' + family + '", sans-serif'; + } + } + } + + loadNewChars(); + var timer = setInterval(loadNewChars, 1000); + + tasks[selector] = { timer: timer, styles: injectedStyles }; + } + + return { loadFont: loadFont }; +})(); diff --git a/src/App.tsx b/src/App.tsx index 2304a37..7614ae4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { createMemo, createSignal, onMount, Show, For, type Accessor } from "solid-js"; +import { createMemo, createSignal, onMount, Show, For } from "solid-js"; import { fetchFonts, fetchConfig, uploadFont, type FontInfo, type ServerConfig } from "./api"; const s = { @@ -39,7 +39,7 @@ const s = { width: "100%", height: "72px", padding: "8px 12px", - "font-size": "18px", + "font-size": "32px", border: "1px solid #d9d9d9", "border-radius": "6px", resize: "vertical", @@ -110,7 +110,7 @@ function App() { if (!font) return ""; return `@font-face { font-family: "CustomFont"; - src: url("/api?font=${font}&text=${encodeURIComponent(text())}") format("truetype"); + src: url("${location.origin}/api?font=${font}&text=${encodeURIComponent(text())}") format("truetype"); } .custom-font { color: red; @@ -118,7 +118,22 @@ function App() { }`; }); - const throttledCss = useThrottledMemo(() => cssStyle(), 1000, text); + /** 字体切换时使用 SDK 重新加载 */ + const prevFontRef = { value: "" }; + createMemo(() => { + const font = selectedFont(); + if (!font) return; + if (font !== prevFontRef.value) { + prevFontRef.value = font; + const el = document.getElementById("webfont-preview"); + if (el) el.style.fontFamily = 'inherit'; + (globalThis as any).WebFont?.loadFont({ + fontName: font, + selector: "#webfont-preview", + family: "CustomFont", + }); + } + }); async function refreshFonts() { const fontList = await fetchFonts(); @@ -165,9 +180,9 @@ function App() {