news 2026/9/11 17:14:49

expo-font 全平台字体加载指南:从运行时 loadAsync 到可变字体与 SSR 的完整演进

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
expo-font 全平台字体加载指南:从运行时 loadAsync 到可变字体与 SSR 的完整演进

expo-font 全平台字体加载指南:从运行时 loadAsync 到可变字体与 SSR 的完整演进

【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo

导读

expo-font 是 Expo 生态中负责字体加载与管理的核心模块,它既能在运行时通过loadAsync/useFonts动态加载本地或远程字体,也能通过 config plugin 在构建期把字体文件直接链接进原生工程。本文以 packages/expo-font/CHANGELOG.md 为主线,结合 packages/expo-font 的 TypeScript API、config plugin 与 Android/iOS 原生实现,系统梳理 expo-font 的能力边界、可变字体(variable fonts)支持、Web/SSR 渲染机制,以及从 8.x 到 57.x 的关键版本演进。读完本文,你将能够熟练使用loadAsyncuseFontsgetLoadedFonts等 API,掌握 config plugin 的字体链接配置(含可变字体的axeswght轴实例化),并理解其在 iOS、Android、Web 与 React Server 环境下的差异与最佳实践。

一、expo-font 是什么:运行时加载 + 构建期链接的双通道设计

expo-font 的定位在 README.md 中一句话即可概括:"Load fonts at runtime and use them in React Native components."。但当前仓库的实现(版本 57.0.1,见 package.json)已经远不止运行时加载,而是形成了两条并行的字体接入通道:

  1. 运行时通道:通过 JS API(loadAsyncuseFonts)动态加载字体资源,跨 Android、iOS、Web 三端一致可用;
  2. 构建期通道:通过 config plugin(expo-font插件)在expo prebuild/ EAS Build 时把字体文件直接嵌入原生工程,字体从 App 启动即就绪,无需等待运行时下载。

这一"双通道"设计正是 CHANGELOG 中大量条目的落点:例如 11.8.0 引入 config plugin("Added config plugin to allow fonts to be linked at build time")、56.0.0 暴露类型化的 config plugin 函数、以及近期(Unpublished/57.x)为 config plugin 增加可变字体能力。此外,expo-font 还承担了 Metro Web 与 React Server Components 下的静态字体提取职责(11.6.0 引入 expo-router 静态字体提取、13.1.0 支持react-server环境),这使得它的能力边界横跨客户端与服务器端渲染。

二、运行时 API 全解析:loadAsync / useFonts 与状态查询

运行时 API 的核心实现在 src/Font.ts,入口统一由 src/index.ts 导出,并在 package.json 中通过exports字段为react-server环境单独指向src/index.server.ts,这是 expo-font 支持 RSC 的关键工程手段。

2.1 loadAsync:加载字体与字体映射

loadAsync(src/Font.ts)接受两种调用形态:

  • 单字体:loadAsync(fontFamily: string, source: FontSource)
  • 字体映射:loadAsync(fontMap: Record<string, FontSource>),此时不能再传第二个参数,否则会抛出ERR_FONT_API错误

从源码可以看到几个值得注意的行为:

  • 去重机制loadFontInNamespaceAsync内部先调用isLoaded(fontFamily)检查是否已加载,已加载则直接返回;随后检查内存中的loadPromises缓存,保证多个调用方并发加载同一字体时共享同一个 Promise(见 src/Font.ts),避免重复下载与重复注册;
  • 服务器端行为:当Platform.OS === 'web' && typeof window === 'undefined'时(SSR/静态渲染场景),loadAsync改为同步调用registerStaticFont并立即 resolve,这是为了保证静态渲染 pass 能同步收集所有字体(见 src/Font.ts 的注释说明);
  • 错误处理source为空时抛出ERR_FONT_SOURCE;CHANGELOG 13.0.0 提到 iOS 上loadAsync加载失败时现在会 reject(此前可能静默失败),并在 13.0.0 中为FontLoader原生模块增加了更详细的错误信息。

2.2 useFonts:Hook 化的字体加载

useFonts(src/FontHooks.ts)根据运行环境分派两种实现:

  • 客户端useRuntimeFonts):基于useState+useEffect调用loadAsync,返回[loaded, error]元组;内部用isMounted标志避免在组件卸载后 setState(这正是 CHANGELOG 13.0.0 修复的 "useFonts could previously attempt to set state on an unmounted component" 问题);
  • 服务器端useStaticFonts):直接同步调用loadAsync(map)并返回[true, null],字体由服务端渲染管线收集。

useFonts的初始化状态同样会检查字体映射是否已全部加载(isMapLoaded),这让 Web 端 hydration 时能直接复用静态渲染阶段已加载的字体,原生端也能从中受益(见 src/FontHooks.ts)。

典型用法(也是 CHANGELOG 55.0.0 中"统一useFonts在 RSC 中的返回值"一节的实践背景):

const [loaded, error] = useFonts({ 'Inter-Black': require('./assets/fonts/Inter-Black.otf'), 'Inter-Bold': { uri: 'https://example.com/Inter-Bold.otf', display: FontDisplay.SWAP }, }); if (!loaded && !error) { return <AppLoading />; // 字体加载完成前保持占位 }

2.3 状态查询:isLoaded / isLoading / getLoadedFonts

  • isLoaded(fontFamily)(src/Font.ts):同步判断字体是否已加载。Web 端会同时检查内存缓存与ExpoFontLoader.isLoaded;原生端走isLoadedNative。CHANGELOG 11.10.0 为其加入了自定义原生字体的支持("Added custom native fonts support toFont.isLoaded()");
  • isLoading(fontFamily)(src/Font.ts):判断字体是否仍在加载中,依据是内存中的loadPromises
  • getLoadedFonts()(src/Font.ts):同步返回所有已加载字体的名称数组,包含构建期通过 config plugin 打入的字体与运行时loadAsync加载的字体。它是 CHANGELOG 13.0.0 引入的 API,后续版本持续打磨:13.3.1 在getLoadedFonts返回空数组时提前退出避免多余开销;56.0.4 修复 Android 端漏掉 XML 字体定义("include xml-fonts in thegetLoadedFonts()list")的问题;Unpublished 版本则规定 iOS 端不再返回字体的 PostScript 名,只返回加载时使用的别名(alias)。

2.4 卸载 API:unloadAsync / unloadAllAsync

这两个 API(src/Font.ts)主要服务于测试场景(注释中明确标注@hidden):unloadAllAsync会清空缓存并调用原生模块卸载全部字体,若仍有字体在加载中会抛出ERR_UNLOADunloadAsync支持按字体名或字体映射精准卸载。测试代码见 src/tests/Font-test.native.ts 与 src/tests/Font-test.web.ts。

三、FontSource 类型体系:四种字体资源形态与 FontDisplay 枚举

FontSource(src/Font.types.ts)是 expo-font 的核心类型,可以是:

形态说明示例
string远程 URL 或本地路径'https://cdn/font.otf'
number打包资源模块 IDrequire('./assets/fonts/x.ttf')
Assetexpo-asset 的 Asset 实例Asset.fromModule得到
FontResource结构化描述对象{ uri, display, default?, testString? }

FontResource的三个字段各有明确用途(src/Font.types.ts):

  • uri:字体资源的 URL 或模块 ID;
  • display:仅 Web 生效,设置@font-facefont-display属性;
  • testString:Web 端自定义传给 FontFace Observer 的测试字符串(CHANGELOG 55.0.0 新增 "support for setting custom testStrings for FontObserver")。

FontDisplay枚举(src/Font.types.ts)对应 CSSfont-display的五个取值:AUTO(默认,由 UA/平台决定)、SWAP(立即显示回退字体,推荐)、BLOCK(字体加载前文本不可见)、FALLBACK(100ms 隐形期后回退)、OPTIONAL(浏览器按网络状况决定是否加载)。类型注释特别指出:原生端默认行为近似模拟SWAP(主流旗舰设备),One Plus 等设备行为有差异;Web 端该值写入生成的@font-face规则,无法按元素动态改变。

此外,src/Font.types.ts 定义了ServerFontResourceDescriptor——流式 SSR 场景下描述字体资源的结构化对象,分为style(内联 CSS)与linkrel: 'preload'预加载链接,含crossOrigin属性)。这是 CHANGELOG 56.0.0 "Add structured server resource descriptors for streaming SSR" 与 "ExportServerFontResourceDescriptortype" 两处改动的产物,其crossOrigin类型随后在 56.0.0 中与 React 对齐("AlignServerFontResourceDescriptor.crossOrigintype with React")。

四、平台差异与底层实现:原生加载链路与 Web 的 @font-face 机制

4.1 原生(Android/iOS)加载链路

原生加载由 src/FontLoader.ts 驱动:getAssetForSource把各种FontSource归一化为 expo-asset 的AssetFontResourceloadSingleFontAsyncawait asset.downloadAsync()确保资源下载完成,再调用原生模块ExpoFontLoader.loadAsync(name, asset.localUri)

Android 端原生实现为 android/src/main/java/expo/modules/font/FontLoaderModule.kt,其loadAsync(L35-L52)核心流程为:

  1. FontSource.resolve解析本地 URI;
  2. 在 API 29(Android Q)及以上尝试构造可变字重 Typeface(buildVariableWeightTypeface);
  3. 调用ReactFontManager.getInstance().addCustomFont(fontFamilyName, typeface)注册到 React Native 的字体管理器中;
  4. 把字体名加入loadedFonts列表。

queryCustomNativeFonts(L60-L72)则通过正则^(.+?)(_bold|_italic|_bold_italic)?\.(ttf|otf)$扫描assets/fonts/目录,并把系统ReactFontManager中已注册的自定义字体名合并进getLoadedFonts()的返回结果——这解释了 CHANGELOG 中"getLoadedFonts()包含构建期链接字体"的语义。

iOS 端原生模块在 12.0.0 中整体用 Swift 重写("The native module has been simplified and rewritten to Swift"),相关实现见 ios/FontLoaderModule.swift、ios/FontFamilyAliasManager.swift 与 ios/UIFont+FontFamilyAlias.swift。历史上 iOS 端修复过多个关键问题:12.0.7 改存postScriptName而非fullName(系统实际用于注册字体的名称);12.0.9 修复 App 进入后台时字体被移除的问题;13.0.2 修复多线程访问资源的崩溃;13.0.3 修复写fontFamilyAliases时的崩溃;55.0.0 延迟原生字体查询以避免 iOS 启动卡死。

4.2 Web 平台:@font-face 注入与字体验证

Web 端实现位于 src/FontLoader.web.ts,与原生端有两处显著差异:

  • 不依赖 expo-asset 下载:直接提取 URI(支持Asset.fromModulelocalUridefault等回退),构造{ uri, display, testString }传给 Web 模块;
  • 服务器端特殊处理:无window时直接调用ExpoFontLoader.loadAsync且异常必须向上传播("a silent missing font is worse");浏览器端则用 try/catch 吞掉 FontFace Observer 的验证失败(参考 #22954),因为字体已通过注入的样式表渲染,不应把验证失败抛成未处理的 Promise rejection。

Web 端模块见 src/ExpoFontLoader.web.ts,它在共享样式表中生成@font-face规则。近期 CHANGELOG 的 Web 修复非常有代表性:

  • 56.0.5:在 Web 字体加载器与 Android config plugin 中对值做 sanitize("Sanitize values in web font loader and Android config plugin");
  • UnpublishedisLoaded()在 Firefox 上恒返回false的问题——通过规范化引号比较字体族名与 CSSOM 修复;同时停止loadAsync()每次调用都注入重复的@font-face规则,让unloadAsync()getLoadedFonts()在各引擎行为一致;
  • Unpublished@font-face规则匹配改为用规则中"裸"字体族名与调用方字面名比较,修复含引号/填充字符的字体族解析,以及多条规则匹配时unloadAsync()误删规则的问题。

4.3 React Server 环境:withServerContext 与 AsyncLocalStorage

CHANGELOG 57.0.0 是一次面向服务端渲染的破坏性升级:

  • 移除Server.resetServerContext()(Breaking change);
  • 新增Server.withServerContext(callback),把服务端字体加载状态按每次渲染(per-render)作用域化,底层依赖 Node 的AsyncLocalStorage(见 src/serverContext.ts 与 src/server.ts)。

这解决了此前全局共享服务端字体状态导致并发渲染相互污染的问题,配套测试见 src/rsc_tests/index.test.ts 与 src/tests/serverContext-test.node.ts。

五、config plugin:构建期链接字体与 Android 可变字体配置

config plugin 是 expo-font 在构建期工作的核心,入口为 plugin/src/index.ts(返回['expo-font', props]),主体逻辑在 plugin/src/withFonts.ts,Android/iOS 平台拆分实现分别位于 plugin/src/withFontsAndroid.ts 与 plugin/src/withFontsIos.ts。

5.1 配置结构与平台差异

FontProps(plugin/src/withFonts.ts)支持顶层fontsandroid.fontsios.fonts三处声明,插件会合并顶层与平台字段(iOS 合入props.fontsprops.ios?.fonts,Android 合入props.fontsprops.android?.fonts):

// app.json / app.config.js { "expo": { "plugins": [ ["expo-font", { "fonts": ["./assets/fonts/MyFont.otf"], "android": { "fonts": [ { "fontFamily": "MyVariableFont", "path": "./assets/fonts/MyVariable.ttf", "fontDefinitions": [ { "weight": 400 }, { "weight": 700, "style": "italic", "axes": { "slnt": -10, "wght": 650 } } ] } ] }, "ios": { "fonts": ["./assets/fonts/MyFont.otf"] } }] ] } }

平台语义不同(CHANGELOG 与类型注释均有说明):

  • iOS:只接受字符串路径数组,字体族名取自字体文件本身;
  • Android:支持字符串路径,也支持对象语法(FontObject)——可为 XML 字体定义自定义族名;
  • Android 端从 11.10.0("Added config plugin to allow fonts to be linked at build time")、13.3.0("support for font weight styles (through XML font definitions)")一路演进到当前对可变字体的完整支持。

5.2 可变字体:axes、wght 轴实例化与多面字型

这是 CHANGELOG 最新版本(Unpublished)最核心的能力扩展,类型定义清晰标注了语义(plugin/src/withFonts.ts):

  • FontVariationAxisTag:OpenType 注册表命名的五个轴ital/opsz/slnt/wdth/wght,也允许任意四字符自定义轴标签(如GRAD);
  • FontDefinitionpath(静态字体可逐定义指向不同文件)、weight(必需)、style?: 'normal' | 'italic'axes?: FontVariationAxes(仅 Android);
  • FontObjectfontFamily+ 一个path(一个可变字体文件可支撑多个定义,不必重复路径)+fontDefinitions[]

类型注释里有两处非常实用的设计说明:

  1. weight/style决定匹配,axes决定绘制:"weightandstylepick which face thefontWeightandfontStyleJS props match; the axes here draw it." 可以设置weight: 700{ wght: 650 }——匹配加粗请求但把文件实例化在 650 字重;wght缺省时默认等于weight
  2. style本身不倾斜字形:一个直立的字体文件即使声明style: "italic"仍按直立渲染,直到通过slntital轴真正施加倾斜。

这些声明最终在原生侧落地:Android 的loadAsync在 API 29+ 调用buildVariableWeightTypeface(FontLoaderModule.kt),读取字体的fvar表构建可实例化的可变 Typeface;构建失败时有分级降级策略——读取失败按读取错误处理,fvar解析失败仅记录 warning 并回退到默认字重渲染,RuntimeException 则视为 expo-font 内部缺陷上报。相关单元测试见 android/src/test/java/expo/modules/font/FontVariationAxesTest.kt 与仪器测试 android/src/androidTest/java/expo/modules/font/VariableTypefacesTest.kt。

同一批更新(Unpublished 与 57.0.0)还把可变字体的fontWeight/fontStyle应用到了useFonts加载的字体上(iOS 与 Android 双端):此前加粗/斜体文本会回退到系统字体,现在会按wght轴为每个字重实例化可变字体(#48129、#48432)。plugin 的配套测试见 plugin/src/tests/withFontsAndroid-test.ts 与 plugin/src/tests/utils-test.ts。

5.3 与 Fingerprint 的联动

plugin/src/withFonts.ts 有一段注释说明:@expo/fingerprint会读取这些 props 来哈希插件嵌入的字体文件(对应 packages/@expo/fingerprint/src/sourcer/Expo.ts 中的getExpoConfigSourcesAsync)——每当新增一种字体文件路径声明方式,都需要同步教会 fingerprint sourcer 读取它,这保证了 EAS Update 等场景下字体变更能被正确感知。

六、版本演进脉络:从 8.x 到 57.x 的关键里程碑

CHANGELOG 完整记录了 expo-font 从 2020 年(8.x)至今(57.0.1)的演进,可归纳为几条主线:

平台扩展线:8.x 修复 IE 加载问题(8.2.2)→ 9.x 全面 Kotlin 化(9.3.0)→ 11.0.0 支持 Metro Web → 11.7.0 支持 tvOS → 11.10.1 支持 macOS → 13.1.0 支持react-server环境 → 56.0.0 最低 iOS/tvOS 版本提升至 16.4、macOS 至 13.4。

能力扩展线:11.8.0 config plugin(构建期链接)→ 11.10.0Font.isLoaded()支持自定义原生字体 → 13.0.0getLoadedFonts()→ 13.2.0renderToImageAsync(渲染为图片,Unpublished 与 14.0.5 持续优化 Android 位图渲染、14.0.9 修复 Android 图片缩放、Unpublished 修复 iOS 上报缩放比例错误)→ 55.0.0 自定义testString与行高支持 → 56.0.0 类型化 config plugin 与 SSR 资源描述符 → 57.0.0Server.withServerContext→ Unpublished 可变字体全平台贯通。

工程规范线:移除NativeModulesProxy(13.0.0)、移除废弃的Font.processFontFamily()(13.0.1)、改用expo/config-plugins(14.0.0)、移除废弃 styletype属性(14.0.0)、Android 接入 expo modules gradle plugin(13.1.0)等。

值得注意的语义变更:12.0.1 与 13.0.0 分别在 iOS、Android 的 Expo Go 中停止对字体族名做作用域化(scoping);56.0.7 支持解析包风格字体路径("Resolve package-style font paths");11.5.1 把未加载字体的错误降级为警告。这些看似细小的改动直接影响开发者对字体族名的预期与错误排查方式。

七、安装与接入实践

根据 README.md 与 package.json,接入方式如下:

# 托管(managed)工程或裸工程通用 npx expo install expo-font
  • Android:无需额外配置;
  • iOS:安装后执行npx pod-install
  • Web:expo-font 在浏览器通过注入@font-face工作,无需手写 CSS。

在裸 React Native 工程中使用前,需先完成expo包本身的安装与配置(npx expo install expo)。依赖方面,fontfaceobserver是唯一运行时依赖,peerDependencies声明了exporeactreact-native(14.0.0 补充了缺失的react-nativepeer 依赖)。若希望通过构建期链接字体,只需在 app.json 的plugins中按第五节配置expo-font插件。

结语

从 2020 年至今,expo-font 从"运行时加载字体的工具库"演进为覆盖运行时、构建期、Web 与 React Server 渲染的全平台字体基础设施。理解loadAsync的去重与服务器端同步注册机制、FontDisplay的跨平台语义差异、config plugin 中axesweight的"匹配/绘制"分工,以及可变字体在 API 29 之上的实例化路径,能够帮助你在实际项目中写出既高效又符合平台预期的字体加载代码。建议进一步阅读 src/Font.ts、src/FontHooks.ts、plugin/src/withFonts.ts 与 android/src/main/java/expo/modules/font/FontLoaderModule.kt 四份核心文件,并结合 src/tests下的测试用例验证你对各平台行为的理解。

【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 17:12:29

Keystone变换实现距离徙动校正:sinc插值与chirp-z对比

简介&#xff1a;这份Keystone变换实现资料面向数字信号处理学习者和研究者&#xff0c;聚焦频谱分析、信号重建中的非线性失真校正问题。压缩包内共1个文件&#xff0c;为MATLAB脚本&#xff08;.m&#xff09;&#xff0c;体积仅3KB&#xff0c;集中展示了Keystone变换的三种…

作者头像 李华
网站建设 2026/9/11 17:09:57

基于51单片机的MPX4115压力检测Proteus仿真与ADC0809采样实现

简介&#xff1a;面向51单片机学习者的MPX4115压力检测仿真资源包&#xff0c;整合了从压力采集、模数转换到显示报警的完整闭环设计&#xff0c;适合课程设计、毕业设计或电子竞赛参考。资源共24个文件&#xff0c;压缩包仅1.23MB&#xff0c;主要包含C语言程序源码、Proteus/…

作者头像 李华
网站建设 2026/9/11 17:09:00

EP_工业无人清扫车标准、规范和证书

EP&#xff1a;Engineering and Project 一、必须强制执行的国家标准&#xff08;GB 强制&#xff0c;带年号&#xff0c;出厂、销售、使用法定合规底线&#xff09; 1. 电气安全 电磁兼容&#xff08;整车强制&#xff09; GB 4343.1-2022 家用电器、电动工具和类似器具的电磁…

作者头像 李华