官方插件服务端渲染

服务端字体

发现、注册和管理服务端字体,并为 Canvas、ECharts 与 Takumi 提供统一字体资源。

@pluxel/fonts 统一管理服务端字体:发现系统字体、注册 Plugin 随包携带的字体、保存从 Workbench 上传的字体, 并为 Canvas/ECharts 选择 native default、为 Takumi 等独立 renderer 提供可移植 bytes。

Canvas/ECharts 能使用系统字体;Takumi 需要 Fonts 管理的可移植字体字节。需要一致的跨机器输出时,随包提供字体或从 Workbench 上传;只安装 Fonts 不会替操作系统安装字体。

何时直接使用 FontsPlugin

  • Plugin 自带 .ttf.otf.woff.woff2 文件,需要在服务端 renderer 中注册。
  • 需要读取当前可用的字体 family 或 provider 默认字体。
  • 自己实现 renderer,需要用 revision 使文字测量缓存失效。
  • renderer 有自己的字体 registry,需要按 portableFonts.revision 读取 managed/programmatic bytes。
  • 需要在 Plugin 的 Workbench 页面嵌入统一的字体选择器。

只使用 Canvas、ECharts 或 Takumi 的业务 Plugin 通常不必直接注入 Fonts;由对应 renderer 依赖它即可。

安装与 catalog

以下命令在快速开始生成的工作区根目录执行;按 添加插件 选择直接使用依赖的包,再运行 pnpm install

pnpm catalog:add -- @pluxel/fonts

host catalog 必须包含 FontsPlugin。直接使用字体能力的 Plugin 将它声明为 required dependency:

import {  } from '@pluxel/fonts'
import { ,  } from '@pluxel/runtime'

@()
export class  extends  {
	constructor(private readonly : ) {
		super()
	}
}

以下 host测试宿主,用于验证装配。应用入口按 添加插件 配置清单、配置记录和自动启动。

import { FontsPlugin } from '@pluxel/fonts'
import { ReportsPlugin } from '@acme/reports'

await host.start(ReportsPlugin, { catalog: [FontsPlugin] })

系统字体由 @napi-rs/canvas 的 platform font manager 在 FontsPlugin 启动时发现。它不会安装或删除操作系统字体,也不会把字体文件发到浏览器。

注册 Plugin 随包携带的字体

使用绝对服务端路径注册静态资源:

import {  } from '@pluxel/fonts'
import { ,  } from '@pluxel/runtime'
import {  } from 'node:url'

@()
export class  extends  {
	constructor(private readonly : ) {
		super()
	}

	protected override async () {
		await this..registerFromPath({
			: (new ('../assets/ReportSans.woff2', import.meta.)),
			: 'Report Sans',
		})
	}
}

注册后检查 this.fonts.families 是否包含 Report Sans,再让 renderer 使用它生成一张含中文、数字和标点的图片。部署产物也必须包含 assets/ReportSans.woff2,路径相对于最终模块位置解析。

registerFromPath() 只接受绝对路径,并使用异步、1 MiB 分块的 bounded 文件 IO。打开 handle 后会先检查 file type/size, 只分配不超过 maxFontBytes 的固定 Buffer;读取期间发生 truncate 或 grow 会拒绝,而不是使用无界 readFile()family 是可选 alias;省略时使用字体内嵌的 family metadata。

已经取得字节时使用 register()

const registration = await this.fonts.register({
	data: fontBytes,
	family: 'Report Sans',
	signal,
})

console.log(registration.families)
registration.dispose()

data 必须是非空 Uint8Array,并在 Promise settle 前保持不变;snapshot 与内容 hash 会按 1 MiB chunk 让出 event loop。managed record 的大 byte 编解码也使用相同 checkpoint,并在 payload copy/hash 前预检 envelope、声明长度、ID 与 时间字段。文件读取、snapshot 和 hash 可取消,最终 GlobalFonts.register() 是有单字体 byte ceiling、不可取消的同步 commit。返回的 FontRegistration 包含:

  • families:本次注册新增或改变的 family。
  • active:registration 是否仍有效。
  • dispose():提前移除注册;可重复调用。

注册归属于当前 caller generation。consumer 停止或被 replacement 时,即使没有手动 dispose(),FontsPlugin 也会移除对应 native FontKey。因此不要将 registration handle 跨 generation 缓存。

读取字体与默认选择

const families = this.fonts.families
const current = this.fonts.defaultFont
const revision = this.fonts.revision

families 是 detached、只读的 family snapshot。每项包含:

  • family:字体 family 名称。
  • sourcesystemregistered
  • styles:可用的 weight、width 与 style。

defaultFont 包含 family、可安全放进 Canvas font shorthand 的 cssFamily,以及选择来源。解析优先级为:

  1. provider-wide 持久化 preference。
  2. defaultFamily host config。
  3. 当前平台的自动系统字体。
  4. generic sans-serif

source 表示是哪一层选中了默认值:preferenceconfigsystemgeneric,不是字体资源的来源。preferredFamilyconfiguredFamily 可能存在但暂时不可用,此时解析会继续 fallback。preference 是 Fonts 自身的 provider-wide domain state;即使 Workbench 未启用,它也继续生效。

revision 是进程内字体注册与默认选择的变更信号。renderer 应把它纳入文字测量 cache key,或在其变化时清空缓存。直接操作 GlobalFonts 不会遵守这一契约。

独立 native renderer 不能共享 Canvas GlobalFonts 时,使用轻量 metadata snapshot 与按需 byte read:

const snapshot = this.fonts.portableFonts

for (const font of snapshot.fonts) {
	const detachedBytes = await this.fonts.readPortableFont(font.id, { signal })
	// 注册进 renderer-local registry;按 snapshot.revision 复用结果。
}

portableFonts 只包含 Workbench managed uploads 和 register() / registerFromPath() 资源。metadata snapshot 会缓存, 不因轮询复制 font bytes;readPortableFont(id, { signal }) 才 cooperative 返回 detached Uint8Array。相同 bytes + family alias 使用同一 content ID,最后一个 registration 释放后才从集合移除。平台自动发现的 system font 没有 FontsPlugin-owned 文件, 因此诚实地不进入可移植集合。

Workbench 管理与 Selection Attachment

FontsPlugin 自己的 manager View 管理 provider-owned 字体集合:上传、删除字体并设置默认 family。上传字体持久化在 host persistence 中,provider 重启时会恢复;这个集合是 Fonts 的领域状态,不是 Workbench 平台概念,也不属于任一 Canvas/ECharts consumer。Manager 使用 descriptor-bound snapshot query 和 mutations;Runtime 负责 DTO detach、关闭时的 远端请求所有权与写后刷新。浏览器会先按当前上限检查 File.size,再通过 File.arrayBuffer() 准备上传 bytes。该 Web API 不能取消已经开始的读取;关闭页面只会丢弃晚到的 bytes 并阻止随后发起 RPC,服务端仍会再次执行 authoritative 校验。

其他 Plugin 不应复制上传管理界面。如果只需在自己的详情页让用户选择统一默认字体,放置 Fonts 提供的 Attachment:

import { FontsWorkbench } from '@pluxel/fonts/workbench'
import { workbench } from '@pluxel/runtime/workbench'

export const ReportsWorkbench = workbench.define({
	fonts: FontsWorkbench.selection.place(workbench.tab({ label: 'Fonts' })),
})

protected override init() {
	this.ctx.workbench?.publish(ReportsWorkbench, {
		fonts: { provider: this.fonts },
	})
}

FontsWorkbench.selection 是 provider-only Attachment。它的 renderer module 同样通过 createWorkbenchRenderer(FontsWorkbench.selection) 建立 descriptor-bound scope,并声明 snapshot query 与选择 mutation。 Scope 取得 Fonts 提供的 FontSelectionApi,Runtime 自动 detach 返回 DTO、释放 transport ownership 并在写入后刷新;API 公开 snapshot()setPreferredFamily(family | null),传 null 恢复 host config 或自动选择。Consumer 不创建转发 target,也不拥有 字体 catalog/selection。Canvas、ECharts 和 Takumi 已各自放置这个 selector,普通业务 Plugin 通常不需要重复添加。

Workbench disabled 只会关闭界面,不会阻止 managed fonts 恢复、程序化注册或 headless 渲染。

配置

host 通过 Plugin config 配置 FontsPlugin:

await host.start(FontsPlugin, {
	initialConfig: {
		defaultFamily: 'Noto Sans',
		maxRegistrationsPerConsumer: 32,
		maxNativeRegistrations: 512,
		maxTotalFontBytes: 256 * 1024 * 1024,
		maxConcurrentFontTasks: 4,
		maxQueuedFontTasks: 32,
		maxQueuedFontTasksPerConsumer: 8,
		maxPendingManagedTasks: 32,
		maxManagedFonts: 64,
		maxFontBytes: 16 * 1024 * 1024,
	},
})
字段默认值职责
defaultFamily自动选择Workbench 没有 override 时优先使用的系统字体
maxRegistrationsPerConsumer32一个 caller 同时持有的程序化 registration 上限
maxNativeRegistrations512此 FontsPlugin node 持有的 native key 总上限
maxTotalFontBytes256 MiBactive native registrations 的合计 bytes
maxConcurrentFontTasks4同时进行的 caller copy/read/hash 数
maxQueuedFontTasks32所有 caller 合计等待的字体任务数
maxQueuedFontTasksPerConsumer8单个 caller 等待的字体任务数
maxPendingManagedTasks32已接纳的串行 Workbench 操作数(含 active)
maxManagedFonts64provider-owned 持久化集合的字体数上限
maxFontBytes16 MiB单个注册或上传字体文件的字节上限

系统字体不计入 maxManagedFonts,也不会被 FontsPlugin cleanup。尺寸、像素、图片解码和 DPR 都不属于 Fonts 配置:它们分别由 Canvas 与 ECharts 负责。

错误处理

字体操作失败时抛出 FontsError,可按 error.code 分类:

  • NOT_RUNNING:provider 或 caller generation 已停止。
  • INVALID_INPUT:路径、字节、文件名或 family alias 无效。
  • INVALID_FONT:native registry 拒绝字体。
  • FONT_TOO_LARGEFONT_LIMIT_EXCEEDED:触发 byte/count 配额。
  • FONT_BUSY:全局或 caller 字体任务队列已满。
  • FONT_NOT_FOUND:选择或删除的字体不存在。
  • CORRUPT_FONT_STORAGE:持久化 managed font 或默认选择损坏。

不要依赖错误 message 做分支;message 用于诊断,稳定分类在 code

默认字体变化只影响之后创建的 Canvas context 和之后执行的 ECharts/Takumi render。已经准备好的文字布局与已有 native context/renderer state 保持不变;portable 集合变化会让 Takumi 在新 revision 创建新的 registry。

最后更新于

本页目录