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 的关键版本演进。读完本文你将能够熟练使用loadAsync、useFonts、getLoadedFonts等 API掌握 config plugin 的字体链接配置含可变字体的axes与wght轴实例化并理解其在 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已经远不止运行时加载而是形成了两条并行的字体接入通道运行时通道通过 JS APIloadAsync、useFonts动态加载字体资源跨 Android、iOS、Web 三端一致可用构建期通道通过 config pluginexpo-font插件在expo prebuild/ EAS Build 时把字体文件直接嵌入原生工程字体从 App 启动即就绪无需等待运行时下载。这一双通道设计正是 CHANGELOG 中大量条目的落点例如 11.8.0 引入 config pluginAdded 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加载字体与字体映射loadAsyncsrc/Font.ts接受两种调用形态单字体loadAsync(fontFamily: string, source: FontSource)字体映射loadAsync(fontMap: Recordstring, 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_SOURCECHANGELOG 13.0.0 提到 iOS 上loadAsync加载失败时现在会 reject此前可能静默失败并在 13.0.0 中为FontLoader原生模块增加了更详细的错误信息。2.2 useFontsHook 化的字体加载useFontssrc/FontHooks.ts根据运行环境分派两种实现客户端useRuntimeFonts基于useStateuseEffect调用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 / getLoadedFontsisLoaded(fontFamily)src/Font.ts同步判断字体是否已加载。Web 端会同时检查内存缓存与ExpoFontLoader.isLoaded原生端走isLoadedNative。CHANGELOG 11.10.0 为其加入了自定义原生字体的支持Added custom native fonts support toFont.isLoaded()isLoading(fontFamily)src/Font.ts判断字体是否仍在加载中依据是内存中的loadPromisesgetLoadedFonts()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 卸载 APIunloadAsync / unloadAllAsync这两个 APIsrc/Font.ts主要服务于测试场景注释中明确标注hiddenunloadAllAsync会清空缓存并调用原生模块卸载全部字体若仍有字体在加载中会抛出ERR_UNLOADunloadAsync支持按字体名或字体映射精准卸载。测试代码见 src/tests/Font-test.native.ts 与 src/tests/Font-test.web.ts。三、FontSource 类型体系四种字体资源形态与 FontDisplay 枚举FontSourcesrc/Font.types.ts是 expo-font 的核心类型可以是形态说明示例string远程 URL 或本地路径https://cdn/font.otfnumber打包资源模块 IDrequire(./assets/fonts/x.ttf)Assetexpo-asset 的 Asset 实例由Asset.fromModule得到FontResource结构化描述对象{ uri, display, default?, testString? }FontResource的三个字段各有明确用途src/Font.types.tsuri字体资源的 URL 或模块 IDdisplay仅 Web 生效设置font-face的font-display属性testStringWeb 端自定义传给 FontFace Observer 的测试字符串CHANGELOG 55.0.0 新增 support for setting custom testStrings for FontObserver。FontDisplay枚举src/Font.types.ts对应 CSSfont-display的五个取值AUTO默认由 UA/平台决定、SWAP立即显示回退字体推荐、BLOCK字体加载前文本不可见、FALLBACK100ms 隐形期后回退、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 的Asset或FontResourceloadSingleFontAsync先await asset.downloadAsync()确保资源下载完成再调用原生模块ExpoFontLoader.loadAsync(name, asset.localUri)。Android 端原生实现为 android/src/main/java/expo/modules/font/FontLoaderModule.kt其loadAsyncL35-L52核心流程为用FontSource.resolve解析本地 URI在 API 29Android Q及以上尝试构造可变字重 TypefacebuildVariableWeightTypeface调用ReactFontManager.getInstance().addCustomFont(fontFamilyName, typeface)注册到 React Native 的字体管理器中把字体名加入loadedFonts列表。queryCustomNativeFontsL60-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/UIFontFontFamilyAlias.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.fromModule、localUri、default等回退构造{ 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 中对值做 sanitizeSanitize values in web font loader and Android config pluginUnpublishedisLoaded()在 Firefox 上恒返回false的问题——通过规范化引号比较字体族名与 CSSOM 修复同时停止loadAsync()每次调用都注入重复的font-face规则让unloadAsync()与getLoadedFonts()在各引擎行为一致Unpublishedfont-face规则匹配改为用规则中裸字体族名与调用方字面名比较修复含引号/填充字符的字体族解析以及多条规则匹配时unloadAsync()误删规则的问题。4.3 React Server 环境withServerContext 与 AsyncLocalStorageCHANGELOG 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.tsAndroid/iOS 平台拆分实现分别位于 plugin/src/withFontsAndroid.ts 与 plugin/src/withFontsIos.ts。5.1 配置结构与平台差异FontPropsplugin/src/withFonts.ts支持顶层fonts、android.fonts、ios.fonts三处声明插件会合并顶层与平台字段iOS 合入props.fonts与props.ios?.fontsAndroid 合入props.fonts与props.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.0Added config plugin to allow fonts to be linked at build time、13.3.0support for font weight styles (through XML font definitions)一路演进到当前对可变字体的完整支持。5.2 可变字体axes、wght 轴实例化与多面字型这是 CHANGELOG 最新版本Unpublished最核心的能力扩展类型定义清晰标注了语义plugin/src/withFonts.tsFontVariationAxisTagOpenType 注册表命名的五个轴ital/opsz/slnt/wdth/wght也允许任意四字符自定义轴标签如GRADFontDefinitionpath静态字体可逐定义指向不同文件、weight必需、style?: normal | italic、axes?: FontVariationAxes仅 AndroidFontObjectfontFamily 一个path一个可变字体文件可支撑多个定义不必重复路径fontDefinitions[]。类型注释里有两处非常实用的设计说明weight/style决定匹配axes决定绘制weightandstylepick which face thefontWeightandfontStyleJS props match; the axes here draw it. 可以设置weight: 700配{ wght: 650 }——匹配加粗请求但把文件实例化在 650 字重wght缺省时默认等于weightstyle本身不倾斜字形一个直立的字体文件即使声明style: italic仍按直立渲染直到通过slnt或ital轴真正施加倾斜。这些声明最终在原生侧落地Android 的loadAsync在 API 29 调用buildVariableWeightTypefaceFontLoaderModule.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 可变字体全平台贯通。工程规范线移除NativeModulesProxy13.0.0、移除废弃的Font.processFontFamily()13.0.1、改用expo/config-plugins14.0.0、移除废弃 styletype属性14.0.0、Android 接入 expo modules gradle plugin13.1.0等。值得注意的语义变更12.0.1 与 13.0.0 分别在 iOS、Android 的 Expo Go 中停止对字体族名做作用域化scoping56.0.7 支持解析包风格字体路径Resolve package-style font paths11.5.1 把未加载字体的错误降级为警告。这些看似细小的改动直接影响开发者对字体族名的预期与错误排查方式。七、安装与接入实践根据 README.md 与 package.json接入方式如下# 托管managed工程或裸工程通用 npx expo install expo-fontAndroid无需额外配置iOS安装后执行npx pod-installWebexpo-font 在浏览器通过注入font-face工作无需手写 CSS。在裸 React Native 工程中使用前需先完成expo包本身的安装与配置npx expo install expo。依赖方面fontfaceobserver是唯一运行时依赖peerDependencies声明了expo、react、react-native14.0.0 补充了缺失的react-nativepeer 依赖。若希望通过构建期链接字体只需在 app.json 的plugins中按第五节配置expo-font插件。结语从 2020 年至今expo-font 从运行时加载字体的工具库演进为覆盖运行时、构建期、Web 与 React Server 渲染的全平台字体基础设施。理解loadAsync的去重与服务器端同步注册机制、FontDisplay的跨平台语义差异、config plugin 中axes与weight的匹配/绘制分工以及可变字体在 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),仅供参考