mirror of
https://github.com/2234839/web-font.git
synced 2026-09-04 23:02:27 +08:00
feat(webfont-sdk): 0.2.0 新增服务端 API 客户端子路径 webfont-sdk/api
- createWebFontApi 工厂封装全部 6 个 REST 接口(fonts/fontMeta/ config/stats/upload/reportEvent)+ 全套类型,独立子路径导出不进主入口 - leafer demo 删除硬编码 FONTS 数组,改 api.fonts() 实时拉取 (24 个字体 vs 原硬编码 6 个),初始字体带回退、失败显式报错 - 主站 src/api.ts 重写为 SDK 客户端薄适配层(dogfooding): 旧类型名保留为别名组件零改动,顺带消灭 FontInfo 重复声明 - /docs 文档页新增 npm 安装、双模式用法、API 客户端示例(中英) - 主站 package.json 添加 workspace 依赖 webfont-sdk 已发布 npm webfont-sdk@0.2.0,CDN 与 demo 端到端验证通过。
This commit is contained in:
parent
b385bc89bc
commit
ca1d2fc1e7
@ -20,7 +20,8 @@
|
||||
"dependencies": {
|
||||
"@unhead/vue": "^2.1.17",
|
||||
"pinyin-pro": "^3.29.1",
|
||||
"web-streams-polyfill": "^4.3.0"
|
||||
"web-streams-polyfill": "^4.3.0",
|
||||
"webfont-sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@leafer-ui/node": "2.2.9",
|
||||
|
||||
@ -54,31 +54,36 @@
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"webfont-sdk": "https://cdn.jsdelivr.net/npm/webfont-sdk@0.1.0/dist/index.js"
|
||||
"webfont-sdk": "https://cdn.jsdelivr.net/npm/webfont-sdk@0.2.0/dist/index.js",
|
||||
"webfont-sdk/api": "https://cdn.jsdelivr.net/npm/webfont-sdk@0.2.0/dist/api.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import { WebFontPlugin } from 'https://cdn.jsdelivr.net/npm/leafer-x-webfont@0.1.0/dist/index.js'
|
||||
import { createWebFontApi } from 'webfont-sdk/api'
|
||||
|
||||
const { Leafer, Text, Rect, Ellipse, Image } = LeaferUI
|
||||
const status = document.getElementById('status')
|
||||
|
||||
const FONTS = [
|
||||
{ name: '令东齐伋复刻体.ttf', label: '令东齐伋复刻体(古籍宋)' },
|
||||
{ name: '霞鹜文楷.ttf', label: '霞鹜文楷(楷书)' },
|
||||
{ name: '得意黑.ttf', label: '得意黑(标题黑体)' },
|
||||
{ name: '演示佛系体.ttf', label: '演示佛系体' },
|
||||
{ name: '源界明朝.ttf', label: '源界明朝(日式明朝)' },
|
||||
{ name: '三极泼墨体.ttf', label: '三极泼墨体' },
|
||||
]
|
||||
|
||||
const leafer = new Leafer({ view: 'canvas-host', fill: '#fafaf9' })
|
||||
/** 字体子集化服务:本地开发连本地后端,任何部署环境(主站/GitHub Pages)一律走线上服务(后端 CORS 全开) */
|
||||
const isDev = location.hostname === 'localhost' || location.hostname === '127.0.0.1'
|
||||
const fontApi = isDev ? 'http://localhost:8087' : 'https://webfont.shenzilong.cn'
|
||||
const webfont = new WebFontPlugin(leafer, { debug: true, baseUrl: fontApi })
|
||||
|
||||
/* ---------- 字体列表:API 实时拉取(不再硬编码) ---------- */
|
||||
const api = createWebFontApi({ baseUrl: fontApi })
|
||||
let fonts
|
||||
try {
|
||||
fonts = (await api.fonts()).filter((f) => !f.temporary)
|
||||
} catch (e) {
|
||||
status.textContent = `字体列表拉取失败:${e.message}`
|
||||
throw e
|
||||
}
|
||||
/** 初始字体选择:优先演示字体,服务端没有则回退列表第一个 */
|
||||
const pick = (prefer) => fonts.find((f) => f.name === prefer)?.name ?? fonts[0]?.name ?? ''
|
||||
|
||||
/* ---------- 海报场景 ---------- */
|
||||
const bg = new Rect({ x: 0, y: 0, width: 900, height: 480, fill: { type: 'linear', from: { x: 0, y: 0 }, to: { x: 1, y: 1 }, stops: [{ offset: 0, color: '#fef3c7' }, { offset: 1, color: '#fde68a' }] } })
|
||||
const deco = new Ellipse({ x: 660, y: -80, width: 360, height: 360, fill: 'rgba(255,255,255,0.35)' })
|
||||
@ -87,7 +92,7 @@
|
||||
const title = new Text({
|
||||
x: 80, y: 150,
|
||||
text: '静心茶舍',
|
||||
fontFamily: '令东齐伋复刻体.ttf',
|
||||
fontFamily: pick('令东齐伋复刻体.ttf'),
|
||||
fontSize: 96,
|
||||
fill: '#1c1917',
|
||||
letterSpacing: 8,
|
||||
@ -95,7 +100,7 @@
|
||||
const subtitle = new Text({
|
||||
x: 84, y: 290,
|
||||
text: '以茶为媒 · 观自在',
|
||||
fontFamily: '霞鹜文楷.ttf',
|
||||
fontFamily: pick('霞鹜文楷.ttf'),
|
||||
fontSize: 28,
|
||||
fill: '#57534e',
|
||||
letterSpacing: 4,
|
||||
@ -108,12 +113,15 @@
|
||||
|
||||
/* ---------- 控件交互 ---------- */
|
||||
const fontSelect = document.getElementById('font-select')
|
||||
for (const f of FONTS) {
|
||||
for (const f of fonts) {
|
||||
const opt = document.createElement('option')
|
||||
opt.value = f.name
|
||||
opt.textContent = f.label
|
||||
/** label 用去扩展名的文件名;更精细的展示名是 font-config displayName 的职责 */
|
||||
opt.textContent = f.name.replace(/\.(ttf|otf|woff2?)$/i, '')
|
||||
fontSelect.appendChild(opt)
|
||||
}
|
||||
/** 当前标题字体在列表中高亮选中 */
|
||||
if (fonts.some((f) => f.name === title.fontFamily)) fontSelect.value = title.fontFamily
|
||||
fontSelect.onchange = () => {
|
||||
title.fontFamily = fontSelect.value
|
||||
status.textContent = '字体切换中…'
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
|
||||
- **增量加载**:`IncrementalEngine` 按字符集去重,只请求新增字符,同类文字滚动输入不重复请求
|
||||
- **双模式**:CSS 模式(`WebFont`,面向 DOM)+ FontFace 模式(`WebFontCanvas`,面向 Canvas)
|
||||
- **API 客户端**:`webfont-sdk/api` 子路径封装字体列表 / 元数据 / 上传 / 统计等 REST 接口
|
||||
- **失败记忆**:裁剪失败的字符自动记录,不反复重试浪费请求
|
||||
- **unicodeRange 注册**:Canvas 模式用 unicodeRange 精确注册字符片段,天然无闪烁
|
||||
- **并发控制**:可配置请求并发池,避免瞬时打爆服务端
|
||||
@ -63,6 +64,31 @@ await WebFontCanvas.ready()
|
||||
</script>
|
||||
```
|
||||
|
||||
## 服务端 REST API 客户端
|
||||
|
||||
增量加载之外,SDK 还封装了子集化服务的公开 REST 接口(字体列表 / 元数据 / 上传 / 统计),
|
||||
独立子路径导出,不进主入口:
|
||||
|
||||
```ts
|
||||
import { createWebFontApi } from 'webfont-sdk/api'
|
||||
|
||||
const api = createWebFontApi({ baseUrl: 'https://webfont.shenzilong.cn' })
|
||||
|
||||
/** 字体列表(文件名 + 是否临时字体) */
|
||||
const fonts = await api.fonts()
|
||||
|
||||
/** 字体元数据:codepoint 区间 / 各字符集覆盖率 / name 表信息 / 站长配置 */
|
||||
const meta = await api.fontMeta('霞鹜文楷.ttf')
|
||||
|
||||
/** 上传临时字体(默认到期自动清理;admin 模式需 apiKey) */
|
||||
const result = await api.upload({ data: file, filename: '我的字体.ttf' })
|
||||
if (!result.success) console.error(result.error)
|
||||
|
||||
/** 服务配置 / 运行统计 */
|
||||
const config = await api.config()
|
||||
const stats = await api.stats()
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
完整 API 文档:<https://webfont.shenzilong.cn/docs>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webfont-sdk",
|
||||
"version": "0.1.0",
|
||||
"description": "Web 字体按需加载 SDK —— 只加载实际用到的字符,增量去重、无闪烁,支持 DOM(CSS) 与 Canvas(FontFace) 两种模式",
|
||||
"version": "0.2.0",
|
||||
"description": "Web 字体按需加载 SDK —— 只加载实际用到的字符,增量去重、无闪烁,支持 DOM(CSS) 与 Canvas(FontFace) 两种模式,内置服务端 REST API 客户端",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@ -11,6 +11,11 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./api": {
|
||||
"types": "./dist/api.d.ts",
|
||||
"development": "./src/api.ts",
|
||||
"import": "./dist/api.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
228
packages/webfont-sdk/src/api.ts
Normal file
228
packages/webfont-sdk/src/api.ts
Normal file
@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 服务端 REST API 客户端 —— webfont 子集化服务公开接口的封装
|
||||
*
|
||||
* 与增量加载引擎(主入口 index.ts)完全独立,按需从子路径导入:
|
||||
* import { createWebFontApi } from 'webfont-sdk/api'
|
||||
*
|
||||
* 覆盖的接口(与后端 routes 一一对应):
|
||||
* - GET /api/fonts → api.fonts() 字体列表
|
||||
* - GET /api/font-meta → api.fontMeta(name) 字体元数据(覆盖率 / codepoint 区间 / name 表)
|
||||
* - GET /api/config → api.config() 服务公开配置
|
||||
* - GET /api/stats → api.stats() 运行统计
|
||||
* - POST /api/upload → api.upload({...}) 上传字体(临时 / 管理员)
|
||||
* - POST /api/stats/event → api.reportEvent(...) 离线裁剪匿名事件上报
|
||||
*/
|
||||
|
||||
/** 字体列表项(GET /api/fonts) */
|
||||
export interface IApiFontInfo {
|
||||
/** 字体文件名(子集 API 查询用,如 '令东齐伋复刻体.ttf') */
|
||||
name: string
|
||||
/** 是否为临时上传字体(到期自动清理,列表展示时可过滤) */
|
||||
temporary: boolean
|
||||
}
|
||||
|
||||
/** 字符集覆盖率项(fontMeta 返回) */
|
||||
export interface IApiCharsetCoverage {
|
||||
/** 字符集标识,如 'ascii'、'cjkBasic' */
|
||||
key: string
|
||||
/** 字符集名称(中文) */
|
||||
name: string
|
||||
/** 该字符集总字符数 */
|
||||
total: number
|
||||
/** 字体覆盖的字符数 */
|
||||
covered: number
|
||||
/** 覆盖率百分比(0~100,保留一位小数) */
|
||||
percent: number
|
||||
}
|
||||
|
||||
/** OpenType name 表信息(版权 / 作者 / 许可等,fontMeta 返回) */
|
||||
export interface IApiFontNameInfo {
|
||||
copyright?: string
|
||||
family?: string
|
||||
subfamily?: string
|
||||
uniqueId?: string
|
||||
fullName?: string
|
||||
version?: string
|
||||
postScript?: string
|
||||
trademark?: string
|
||||
manufacturer?: string
|
||||
designer?: string
|
||||
description?: string
|
||||
vendorUrl?: string
|
||||
designerUrl?: string
|
||||
license?: string
|
||||
licenseUrl?: string
|
||||
}
|
||||
|
||||
/** 人工配置项(服务端 font-config.json,由站长维护,fontMeta 返回) */
|
||||
export interface IApiFontUserConfig {
|
||||
/** 显示名称(优先于文件名) */
|
||||
displayName?: string
|
||||
/** 描述 / 简介 */
|
||||
description?: string
|
||||
/** 标签列表 */
|
||||
tags?: string[]
|
||||
/** 开源仓库地址 */
|
||||
homepage?: string
|
||||
/** 默认预览文字 */
|
||||
previewText?: string
|
||||
/** 详情页正文标题 */
|
||||
bodyTitle?: string
|
||||
/** 详情页正文段落 */
|
||||
bodyText?: string
|
||||
/** 详情页字符预览行 */
|
||||
charsetPreview?: string
|
||||
}
|
||||
|
||||
/** 字体元数据(GET /api/font-meta) */
|
||||
export interface IApiFontMeta {
|
||||
/** 元数据版本指纹(算法变更后 bump) */
|
||||
metaVersion: number
|
||||
/** 字体支持的 codepoint 总数 */
|
||||
totalCodePoints: number
|
||||
/** 各标准字符集覆盖率 */
|
||||
coverage: IApiCharsetCoverage[]
|
||||
/** 支持的 codepoint 区间(紧凑表示,如 [[0x20, 0x7e], [0x4e00, 0x9fff]]) */
|
||||
ranges: Array<[number, number]>
|
||||
/** name 表基本信息 */
|
||||
info: IApiFontNameInfo
|
||||
/** 人工配置(服务端配置了才有) */
|
||||
config?: IApiFontUserConfig
|
||||
}
|
||||
|
||||
/** 服务公开配置(GET /api/config) */
|
||||
export interface IApiServerConfig {
|
||||
/** 是否启用临时字体上传 */
|
||||
enableTempUpload: boolean
|
||||
/** 是否启用管理员上传(服务端配置了 API Key) */
|
||||
adminUploadEnabled: boolean
|
||||
/** 支持的子集输出格式 */
|
||||
supportedOutTypes: Array<'woff2' | 'ttf'>
|
||||
/** 临时字体保留时限(秒) */
|
||||
tempRetentionSeconds: number
|
||||
/** 字体子集化最大并发数 */
|
||||
subsetConcurrency: number
|
||||
/** 队列等待超时(秒) */
|
||||
subsetQueueTimeoutSeconds: number
|
||||
}
|
||||
|
||||
/** 运行统计(GET /api/stats) */
|
||||
export interface IApiServerStats {
|
||||
/** 服务运行秒数 */
|
||||
uptime: number
|
||||
/** 总请求数 */
|
||||
totalRequests: number
|
||||
/** 子集化请求数 */
|
||||
subsetRequests: number
|
||||
/** 子集缓存命中数 */
|
||||
subsetCacheHits: number
|
||||
/** 累计裁剪字符数 */
|
||||
totalChars: number
|
||||
/** 临时字体上传次数 */
|
||||
tempUploads: number
|
||||
/** 离线裁剪完成次数 */
|
||||
offlineSubsets: number
|
||||
/** 离线裁剪字体下载次数 */
|
||||
offlineDownloads: number
|
||||
/** 子集结果缓存条目数 */
|
||||
subsetCacheEntries: number
|
||||
/** 字体二进制缓存条目数 */
|
||||
fontBufferCacheEntries: number
|
||||
}
|
||||
|
||||
/** 上传结果(POST /api/upload;失败是业务预期,不抛错由调用方判断 success) */
|
||||
export interface IApiUploadResult {
|
||||
success: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** 客户端配置 */
|
||||
export interface IWebFontApiOptions {
|
||||
/** 服务基地址,默认官方在线服务 */
|
||||
baseUrl?: string
|
||||
/** 自定义 fetch 实现(老版本 Node / 测试注入用,默认全局 fetch) */
|
||||
fetchImpl?: typeof fetch
|
||||
}
|
||||
|
||||
/** 上传参数 */
|
||||
export interface IApiUploadInput {
|
||||
/** 字体二进制内容 */
|
||||
data: Blob
|
||||
/** 保存到服务端的文件名(如 '我的字体.ttf') */
|
||||
filename: string
|
||||
/** 上传模式:temp 临时字体(默认,到期自动清理)/ admin 管理员字体(需 apiKey) */
|
||||
mode?: 'temp' | 'admin'
|
||||
/** admin 模式的 API Key */
|
||||
apiKey?: string
|
||||
}
|
||||
|
||||
/** API 客户端实例 */
|
||||
export interface IWebFontApi {
|
||||
/** 字体列表 */
|
||||
fonts(): Promise<IApiFontInfo[]>
|
||||
/** 字体元数据(覆盖率 / codepoint 区间 / name 表 / 人工配置) */
|
||||
fontMeta(fontName: string): Promise<IApiFontMeta>
|
||||
/** 服务公开配置 */
|
||||
config(): Promise<IApiServerConfig>
|
||||
/** 运行统计 */
|
||||
stats(): Promise<IApiServerStats>
|
||||
/** 上传字体(失败返回 { success: false, error },不抛错) */
|
||||
upload(input: IApiUploadInput): Promise<IApiUploadResult>
|
||||
/** 离线裁剪匿名事件上报(只发事件类型,不含任何内容数据;页面卸载也能送达) */
|
||||
reportEvent(event: 'offline_subset' | 'offline_download'): void
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建服务端 API 客户端
|
||||
*
|
||||
* GET 类接口对非 2xx 响应直接抛 Error(fail fast,错误信息含状态码与响应体);
|
||||
* upload 例外——服务端用 4xx + { success: false, error } 表达业务失败,原样返回。
|
||||
*/
|
||||
export function createWebFontApi(options: IWebFontApiOptions = {}): IWebFontApi {
|
||||
/** 去掉末尾斜杠,避免拼出双斜杠 */
|
||||
const baseUrl = (options.baseUrl ?? 'https://webfont.shenzilong.cn').replace(/\/+$/, '')
|
||||
const fetchImpl = options.fetchImpl ?? fetch
|
||||
|
||||
async function getJson<T>(path: string): Promise<T> {
|
||||
const res = await fetchImpl(baseUrl + path)
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '')
|
||||
throw new Error(`webfont-sdk/api GET ${path} → HTTP ${res.status}${body ? `: ${body.slice(0, 200)}` : ''}`)
|
||||
}
|
||||
return res.json() as Promise<T>
|
||||
}
|
||||
|
||||
return {
|
||||
fonts: () => getJson<IApiFontInfo[]>('/api/fonts'),
|
||||
fontMeta: (fontName) => getJson<IApiFontMeta>(`/api/font-meta?font=${encodeURIComponent(fontName)}`),
|
||||
config: () => getJson<IApiServerConfig>('/api/config'),
|
||||
stats: () => getJson<IApiServerStats>('/api/stats'),
|
||||
|
||||
upload: async ({ data, filename, mode = 'temp', apiKey }: IApiUploadInput) => {
|
||||
const form = new FormData()
|
||||
form.append('font', data, filename)
|
||||
/** 仅 admin 模式需要鉴权头 */
|
||||
const headers: Record<string, string> = {}
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
|
||||
const res = await fetchImpl(`${baseUrl}/api/upload?mode=${mode}`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers,
|
||||
})
|
||||
return res.json() as Promise<IApiUploadResult>
|
||||
},
|
||||
|
||||
reportEvent: (event) => {
|
||||
const body = JSON.stringify({ event })
|
||||
/** sendBeacon 优先:页面卸载时也能送达;失败静默降级 fetch keepalive——统计不干扰主流程 */
|
||||
const nav = (globalThis as { navigator?: Navigator }).navigator
|
||||
if (nav?.sendBeacon?.(`${baseUrl}/api/stats/event`, new Blob([body], { type: 'application/json' }))) return
|
||||
fetchImpl(`${baseUrl}/api/stats/event`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body,
|
||||
keepalive: true,
|
||||
}).catch(() => {})
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
export default [
|
||||
/** ESM + d.ts —— npm 包主产物(leafer 插件 / bundler 用户) */
|
||||
/** ESM + d.ts —— npm 包主产物(leafer 插件 / bundler 用户);api 为服务端 REST 客户端子路径 */
|
||||
defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
entry: ['src/index.ts', 'src/api.ts'],
|
||||
format: 'esm',
|
||||
dts: true,
|
||||
clean: true,
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -17,6 +17,9 @@ importers:
|
||||
web-streams-polyfill:
|
||||
specifier: ^4.3.0
|
||||
version: 4.3.0
|
||||
webfont-sdk:
|
||||
specifier: workspace:*
|
||||
version: link:packages/webfont-sdk
|
||||
devDependencies:
|
||||
'@leafer-ui/node':
|
||||
specifier: 2.2.9
|
||||
|
||||
183
src/api.ts
183
src/api.ts
@ -1,157 +1,80 @@
|
||||
export interface FontInfo {
|
||||
name: string;
|
||||
/**
|
||||
* 主站 API 适配层 —— 基于官方 SDK 客户端(webfont-sdk/api)的薄封装
|
||||
*
|
||||
* 所有类型与请求逻辑统一走 webfont-sdk:接口变更只改 SDK 一处,
|
||||
* 主站(dogfooding)、leafer 插件 demo、npm 用户共享同一实现。
|
||||
*
|
||||
* 保留原有的函数签名与旧类型别名(FontInfo / FontMeta / ...),
|
||||
* 组件层零改动即可切换;新代码建议直接 import SDK 类型。
|
||||
*
|
||||
* baseUrl 说明:主站与后端同源(dev 走 vite proxy /api → 8087),
|
||||
* 传空串 baseUrl 后客户端拼出相对路径 "/api/...",同源与代理都正确。
|
||||
*/
|
||||
import {
|
||||
createWebFontApi,
|
||||
type IApiFontInfo,
|
||||
type IApiFontMeta,
|
||||
type IApiServerConfig,
|
||||
type IApiServerStats,
|
||||
type IApiUploadResult,
|
||||
} from "webfont-sdk/api";
|
||||
|
||||
const api = createWebFontApi({ baseUrl: "" });
|
||||
|
||||
/** 字体列表项(旧名兼容;新代码用 IApiFontInfo) */
|
||||
export type FontInfo = IApiFontInfo & {
|
||||
/** 旧字段(后端实际不返回,恒 undefined),dev 预览页模板引用,保留可选 */
|
||||
dir?: string;
|
||||
/** 是否为临时上传的字体 */
|
||||
temporary?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ServerConfig {
|
||||
enableTempUpload: boolean;
|
||||
adminUploadEnabled: boolean;
|
||||
supportedOutTypes: ("woff2" | "ttf")[];
|
||||
/** 临时字体保留时限(秒) */
|
||||
tempRetentionSeconds?: number;
|
||||
/** 字体子集化最大并发数 */
|
||||
subsetConcurrency?: number;
|
||||
/** 队列等待超时(秒) */
|
||||
subsetQueueTimeoutSeconds?: number;
|
||||
}
|
||||
/** 字体元数据(旧名兼容;新代码用 IApiFontMeta) */
|
||||
export type FontMeta = IApiFontMeta;
|
||||
|
||||
export interface UploadResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
/** 服务公开配置(旧名兼容;新代码用 IApiServerConfig) */
|
||||
export type ServerConfig = IApiServerConfig;
|
||||
|
||||
export interface ServerStats {
|
||||
uptime: number;
|
||||
totalRequests: number;
|
||||
subsetRequests: number;
|
||||
subsetCacheHits: number;
|
||||
totalChars: number;
|
||||
subsetCacheEntries: number;
|
||||
fontBufferCacheEntries: number;
|
||||
/** 临时文件上传次数 */
|
||||
tempUploads?: number;
|
||||
/** 离线裁剪完成次数 */
|
||||
offlineSubsets?: number;
|
||||
/** 离线裁剪字体下载次数 */
|
||||
offlineDownloads?: number;
|
||||
}
|
||||
/** 上传结果(旧名兼容;新代码用 IApiUploadResult) */
|
||||
export type UploadResult = IApiUploadResult;
|
||||
|
||||
/** 字符集覆盖率 */
|
||||
export interface CharsetCoverage {
|
||||
/** 字符集标识,如 "ascii"、"cjkBasic" */
|
||||
key: string;
|
||||
name: string;
|
||||
total: number;
|
||||
covered: number;
|
||||
percent: number;
|
||||
}
|
||||
/** 运行统计(旧名兼容;新代码用 IApiServerStats) */
|
||||
export type ServerStats = IApiServerStats;
|
||||
|
||||
/** 字体基本信息(来自 OpenType name 表) */
|
||||
export interface FontInfo {
|
||||
copyright?: string;
|
||||
family?: string;
|
||||
subfamily?: string;
|
||||
uniqueId?: string;
|
||||
fullName?: string;
|
||||
version?: string;
|
||||
postScript?: string;
|
||||
trademark?: string;
|
||||
manufacturer?: string;
|
||||
designer?: string;
|
||||
description?: string;
|
||||
vendorUrl?: string;
|
||||
designerUrl?: string;
|
||||
license?: string;
|
||||
licenseUrl?: string;
|
||||
}
|
||||
|
||||
/** 人工配置项(来自 font-config.json,由用户维护) */
|
||||
export interface FontUserConfig {
|
||||
/** 显示名称(优先于文件名) */
|
||||
displayName?: string;
|
||||
/** 描述/简介 */
|
||||
description?: string;
|
||||
/** 标签列表 */
|
||||
tags?: string[];
|
||||
/** 开源仓库地址(如 GitHub URL) */
|
||||
homepage?: string;
|
||||
/** 默认预览文字 */
|
||||
previewText?: string;
|
||||
/** 详情页正文标题 */
|
||||
bodyTitle?: string;
|
||||
/** 详情页正文段落 */
|
||||
bodyText?: string;
|
||||
/** 详情页字符预览行 */
|
||||
charsetPreview?: string;
|
||||
}
|
||||
|
||||
/** 字体元数据 */
|
||||
export interface FontMeta {
|
||||
totalCodePoints: number;
|
||||
coverage: CharsetCoverage[];
|
||||
ranges: Array<[number, number]>;
|
||||
/** 字体基本信息(版权、作者等) */
|
||||
info: FontInfo;
|
||||
/** 人工配置(来自 font-config.json) */
|
||||
config?: FontUserConfig;
|
||||
}
|
||||
export { type IApiCharsetCoverage as CharsetCoverage, type IApiFontUserConfig as FontUserConfig } from "webfont-sdk/api";
|
||||
|
||||
export async function fetchFonts(): Promise<FontInfo[]> {
|
||||
const res = await fetch("/api/fonts");
|
||||
return res.json();
|
||||
return api.fonts();
|
||||
}
|
||||
|
||||
export async function fetchFontMeta(fontName: string): Promise<FontMeta> {
|
||||
const res = await fetch(`/api/font-meta?font=${encodeURIComponent(fontName)}`);
|
||||
return res.json();
|
||||
return api.fontMeta(fontName);
|
||||
}
|
||||
|
||||
export async function fetchConfig(): Promise<ServerConfig> {
|
||||
const res = await fetch("/api/config");
|
||||
return res.json();
|
||||
return api.config();
|
||||
}
|
||||
|
||||
export async function fetchStats(): Promise<ServerStats> {
|
||||
return api.stats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传字体(临时 / 管理员)
|
||||
*
|
||||
* SDK 客户端用 FormData 上传,主站调用方传的是 File —— 直接透传,
|
||||
* FormData.append(name, File) 会自动带上 filename。
|
||||
*/
|
||||
export async function uploadFont(
|
||||
file: File,
|
||||
mode: "temp" | "admin",
|
||||
apiKey?: string,
|
||||
): Promise<UploadResult> {
|
||||
const formData = new FormData();
|
||||
formData.append("font", file);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (apiKey) {
|
||||
headers["Authorization"] = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/upload?mode=${mode}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers,
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchStats(): Promise<ServerStats> {
|
||||
const res = await fetch("/api/stats");
|
||||
return res.json();
|
||||
return api.upload({ data: file, filename: file.name, mode, apiKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* 离线裁剪匿名事件上报
|
||||
*
|
||||
* 只发送事件类型(裁剪完成 / 下载),不包含字体、文字等任何内容数据。
|
||||
* 用 sendBeacon 优先(页面卸载时也能送达),失败静默——统计不干扰主流程。
|
||||
* 离线裁剪匿名事件上报 —— 透传 SDK 实现
|
||||
* (sendBeacon 优先,页面卸载也能送达;只发事件类型,无内容数据)
|
||||
*/
|
||||
export function reportOfflineEvent(event: "offline_subset" | "offline_download"): void {
|
||||
const body = JSON.stringify({ event });
|
||||
if (navigator.sendBeacon?.("/api/stats/event", new Blob([body], { type: "application/json" }))) return;
|
||||
fetch("/api/stats/event", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body,
|
||||
keepalive: true,
|
||||
}).catch(() => {});
|
||||
api.reportEvent(event);
|
||||
}
|
||||
|
||||
12
src/i18n.ts
12
src/i18n.ts
@ -57,6 +57,12 @@ const messages = {
|
||||
sdkModes: "还支持",
|
||||
observeFont: "(MutationObserver 事件驱动)和",
|
||||
loadText: "(手动传文本)两种方式,多种方式可同时使用,SDK 内部自动按字体去重增量加载。",
|
||||
/** npm SDK 章节 */
|
||||
npmSdkTitle: "npm 包(推荐,工程化项目):",
|
||||
npmSdkText: "安装 ",
|
||||
npmSdkText2: " 后可在任意前端工程 / Node 项目中使用,支持 DOM(CSS 模式)与 Canvas(FontFace 模式):",
|
||||
npmSdkApiTitle: "服务端 API 客户端:",
|
||||
npmSdkApiText: "字体列表 / 元数据 / 上传 / 统计等 REST 接口封装在独立子路径,供字体选择器等场景使用:",
|
||||
thanks: "感谢",
|
||||
thanksText: "收录本项目",
|
||||
buyCoffee: "觉得好用?",
|
||||
@ -161,6 +167,12 @@ const messages = {
|
||||
sdkModes: "Also supports ",
|
||||
observeFont: " (MutationObserver-driven) and ",
|
||||
loadText: " (manual text). Multiple modes can be used simultaneously with automatic deduplication.",
|
||||
/** npm SDK section */
|
||||
npmSdkTitle: "npm package (recommended for projects):",
|
||||
npmSdkText: "Install ",
|
||||
npmSdkText2: " to use in any frontend / Node project. Supports DOM (CSS mode) and Canvas (FontFace mode):",
|
||||
npmSdkApiTitle: "Server API client:",
|
||||
npmSdkApiText: "REST endpoints (font list / metadata / upload / stats) live in a separate subpath export:",
|
||||
thanks: "Thanks to ",
|
||||
thanksText: " for featuring this project",
|
||||
buyCoffee: "Find it useful? ",
|
||||
|
||||
@ -40,6 +40,41 @@ const basicUsageCode = computed(() => {
|
||||
const jsSdkCode = computed(() => {
|
||||
return '<script src="' + origin.value + '/webfont-sdk.js"><\/script>\n\n<h1 class="title">你的文字</h1>\n<p class="content">输入任意文字,SDK 自动裁剪加载</p>\n\n<script>\n WebFont.observeFont({\n fontName: "字体文件名.ttf",\n selector: ".title, .content",\n family: "MyFont",\n });\n<\/script>';
|
||||
});
|
||||
/** npm 安装命令 */
|
||||
const npmSdkCode = `npm install webfont-sdk`;
|
||||
|
||||
/** npm 包用法示例(CSS 模式 + Canvas 模式),依赖 origin 需 computed */
|
||||
const npmSdkUsageCode = computed(() => {
|
||||
return `import { WebFont, WebFontCanvas } from 'webfont-sdk'
|
||||
|
||||
// DOM 场景:自动观察元素内文字,增量加载
|
||||
const obs = WebFont.observeFont({
|
||||
fontName: '字体文件名.ttf',
|
||||
selector: '.title',
|
||||
family: 'MyFont',
|
||||
baseUrl: '${origin.value}',
|
||||
})
|
||||
|
||||
// Canvas 场景(leafer / fabric / 原生 canvas)
|
||||
const face = WebFontCanvas.loadFontFace(
|
||||
{ fontName: '字体文件名.ttf' },
|
||||
() => redraw(), /* 片段就绪,重绘 */
|
||||
)
|
||||
face.update('画布上的文字')
|
||||
await WebFontCanvas.ready()`;
|
||||
});
|
||||
|
||||
/** 服务端 API 客户端示例(webfont-sdk/api 子路径),依赖 origin 需 computed */
|
||||
const npmSdkApiCode = computed(() => {
|
||||
return `import { createWebFontApi } from 'webfont-sdk/api'
|
||||
|
||||
const api = createWebFontApi({ baseUrl: '${origin.value}' })
|
||||
|
||||
const fonts = await api.fonts() // 字体列表
|
||||
const meta = await api.fontMeta('字体名') // 覆盖率 / codepoint 区间
|
||||
const r = await api.upload({ data: file, filename: '我的字体.ttf' })
|
||||
if (!r.success) console.error(r.error)`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -70,6 +105,13 @@ const jsSdkCode = computed(() => {
|
||||
<div style="margin-top: 4px"><CodeBlock :code="jsSdkCode" lang="html" /></div>
|
||||
|
||||
<p style="margin-top: 12px">{{ t('sdkModes') }}<code>WebFont.observeFont()</code>{{ t('observeFont') }}<code>WebFont.loadText()</code>{{ t('loadText') }}</p>
|
||||
|
||||
<p style="margin-top: 20px"><b>{{ t('npmSdkTitle') }}</b>{{ t('npmSdkText') }}<code>webfont-sdk</code>{{ t('npmSdkText2') }}</p>
|
||||
<div style="margin-top: 4px"><CodeBlock :code="npmSdkCode" lang="bash" /></div>
|
||||
<div style="margin-top: 4px"><CodeBlock :code="npmSdkUsageCode" lang="ts" /></div>
|
||||
|
||||
<p style="margin-top: 20px"><b>{{ t('npmSdkApiTitle') }}</b>{{ t('npmSdkApiText') }}</p>
|
||||
<div style="margin-top: 4px"><CodeBlock :code="npmSdkApiCode" lang="ts" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user