expo-font 完全指南:在 Expo 与 React Native 中运行时加载字体
【免费下载链接】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 SDK 中负责字体加载与使用的核心模块,其官方定位为"Load fonts at runtime and use them in React Native components"(在运行时加载字体,并在 React Native 组件中使用)。本指南以 packages/expo-font/README.md 为主体骨架,结合仓库内 源码实现 与 config plugin 的底层原理,系统讲解字体在 Expo 托管项目与裸 React Native 项目中的安装方式、运行时加载 API、Web 端@font-face注入机制、原生端字体注册原理以及构建期字体嵌入方案。读完本文,你将能够熟练运用loadAsync、useFonts等 API 完成字体加载,理解其背后的缓存与去重策略,并在 app config 中通过 config plugin 将字体静态嵌入原生工程。
一、expo-font 是什么:设计目标与适用场景
expo-font的职责非常单一而明确:在运行时把字体资源加载进来,并使其可以被 React Native 的Text组件通过fontFamilystyle prop 直接使用。它不负责字体设计、字体渲染,而是充当"字体资源 → 平台字体系统"之间的桥梁:
- Android / iOS(原生):将字体文件注册进平台字体管理器(iOS 为 CoreText 的
CTFontManagerRegisterFontsForURL,Android 为系统字体机制),随后原生Text组件即可按fontFamily名称解析。 - Web(浏览器):自动生成并注入
@font-faceCSS 块到一个共享的<style>元素中,无需开发者手写任何 CSS。
从 Font.types.ts 的类型定义可以看出,一个FontSource可以是四种形态:URI 字符串、模块 ID(即require('./assets/fonts/xxx.ttf')返回的数字)、expo-asset的Asset实例,或是带有uri/display/testString字段的FontResource对象。这种多形态设计让开发者既能加载本地打包的静态字体,也能加载远程 URL 字体。
适用场景:应用启动时加载自定义字体、按需加载图标字体(如
@expo/vector-icons底层即依赖 expo-font)、在 Web 端按需注入字体样式等。
二、安装:托管项目与裸项目两条路径
原 README 明确区分了两种安装场景,本仓库源码亦分别对应不同的集成方式。
2.1 托管(Managed)Expo 项目
对于托管 Expo 项目,官方推荐直接查阅 SDK 文档 中的安装指引。在托管工作流下,运行以下命令即可完成依赖安装与原生配置(通过 EAS /expo run构建时自动生效):
npx expo install expo-fontnpx expo install会依据当前 SDK 版本自动挑选与之兼容的expo-font版本,避免手动锁版本的麻烦。在本次仓库中,package.json 记录的版本为57.0.1。
2.2 裸(Bare)React Native 项目
裸项目必须先保证expo包本身已被正确安装与配置(即expo-modules-core等原生模块体系可用),随后:
添加 npm 依赖:
npx expo install expo-fontAndroid 配置:无需任何额外设置(README 原文:"No additional set up necessary.")。从仓库结构看,Android 侧的字体注册完全由运行时原生模块完成,不存在需要手动修改的清单文件。
iOS 配置:安装 npm 包之后运行:
npx pod-install这一步会为 iOS 工程安装 CocoaPods 依赖,将
expo-font的原生代码(见 ios/FontLoaderModule.swift)链接进应用。
依赖声明:在裸项目中,
expo-font的 peerDependencies 要求宿主环境提供expo、react、react-native,因此在安装expo-font前必须已有可用的 Expo Modules 环境。
三、核心 API:运行时字体加载全解析
expo-font在运行时层面暴露了loadAsync、isLoaded、isLoading、getLoadedFonts等函数,以及 React Hooks 形式的useFonts。入口统一从 src/index.ts 导出。
3.1 loadAsync:加载单字体或字体映射
loadAsync(fontFamilyOrFontMap: string | Record<string, FontSource>, source?: FontSource): Promise<void>调用形态有两种:
形态一:单个字体,第一个参数为fontFamily名称字符串,第二个参数为字体来源:
import * as Font from 'expo-font'; await Font.loadAsync('SpaceMono', require('./assets/fonts/SpaceMono-Regular.ttf'));形态二:字体映射表,一次加载多个字体:
await Font.loadAsync({ 'SpaceMono': require('./assets/fonts/SpaceMono-Regular.ttf'), 'SpaceMono-Bold': require('./assets/fonts/SpaceMono-Bold.ttf'), });加载完成后,Text组件即可使用fontFamily: 'SpaceMono'引用对应字体。需要注意(对应 Font.ts 的实现约束):
- 不能混用两种形态:当第一个参数是对象时,如果还传入第二个
source参数,会抛出CodedError('ERR_FONT_API'),提示第二个参数只能配合字符串形式使用。 - 字体名不能为空:若 source 缺失,会抛出
CodedError('ERR_FONT_SOURCE')。 - 幂等性:当
fontFamily已被加载时,再次调用会直接返回,不会重复替换该字体。
底层加载流程(源码级)
loadAsync并非简单地直连原生模块,其内部经过了多层封装(Font.ts + FontLoader.ts):
- 查重(isLoaded):先通过
isLoaded判断该字体是否已加载(命中 JS 侧缓存或原生侧已注册列表),已加载则直接返回。 - 并发去重(loadPromises):维护一个
loadPromises映射,若同一字体正在加载中,后续并发调用会复用同一个 Promise,而不是重复发起加载——源码注释明确说明这是为了避免同一种字体被加载 n 次(Font.ts)。 - 资源归一化(getAssetForSource):在 FontLoader.ts 中,
Asset实例原样返回;字符串被当作远程 URI 调用Asset.fromURI;数字(require模块 ID)被转换为Asset.fromModule;FontResource对象则递归取其uri再归一化。 - 下载与注册:调用
asset.downloadAsync()确保字体文件就绪,然后调用原生模块ExpoFontLoader.loadAsync(name, asset.localUri)(FontLoader.ts)。若下载失败,抛出CodedError('ERR_DOWNLOAD')。 - 标记完成:注册成功后调用
markLoaded写入 JS 侧缓存(memory.ts),并清理loadPromises中的条目。
Web 端与服务端渲染的特殊路径
在 Font.ts 中可以看到一个刻意设计:loadAsync没有使用async关键字,因为 Web 端静态渲染阶段必须同步收集所有字体。在服务端(Platform.OS === 'web' && typeof window === 'undefined')时,字体走registerStaticFont注册为静态资源,供 SSR 输出<style>或<link rel="preload">标签;在浏览器端则通过 ExpoFontLoader.web.ts 注入样式。
3.2 isLoaded / isLoading / getLoadedFonts:加载状态查询
isLoaded(fontFamily): boolean:同步判断字体是否加载完成。Web 端检查 JS 缓存或@font-face规则是否存在(Font.ts);原生端通过getLoadedFonts结果构建缓存判断(memory.ts)。isLoading(fontFamily): boolean:判断字体是否仍在加载中,实现即检查fontFamily in loadPromises(Font.ts)。getLoadedFonts(): string[]:返回所有已加载字体的名称数组,可直接用于fontFamilystyle prop。文档注释指出它同时包含构建期通过 config plugin 嵌入的字体与运行时loadAsync加载的字体(Font.ts)。
3.3 useFonts:React Hooks 形式
import { useFonts } from 'expo-font'; export default function App() { const [loaded, error] = useFonts({ 'Inter-Black': require('./assets/fonts/Inter-Black.otf'), }); if (!loaded && !error) { return null; } // 字体就绪后渲染 return <Text style={{ fontFamily: 'Inter-Black' }}>Hello</Text>; }useFonts返回[loaded, error]二元组(FontHooks.ts):
- loaded:字体是否加载完成。
- error:加载过程中遇到的错误(可用于开发期提示)。
其实现内部对客户端与服务端做了区分(FontHooks.ts):在服务端渲染时使用useStaticFonts(直接同步调用loadAsync并返回已加载);在浏览器中则使用useRuntimeFonts,通过useState+useEffect异步加载,并利用isMapLoaded在 Web 水合(rehydration)阶段复用静态渲染时已加载的字体。此外,源码注释明确指出:字体映射表动态变化时不会重新加载字体("the fonts are not reloaded when you dynamically change the font map"),因此需要动态换字体时应重新挂载组件或使用loadAsync。
四、配置参数详解:FontSource 与 FontDisplay
4.1 FontSource 的四种形态
| 形态 | 类型 | 说明 | 示例 |
|---|---|---|---|
| 本地模块 | number | require()返回的模块 ID,构建期打包进应用 | require('./assets/fonts/SpaceMono.ttf') |
| 远程 URI | string | 字体文件的网络地址 | 'https://example.com/font.ttf' |
| Asset 实例 | Asset | expo-asset管理的资源对象 | Asset.fromModule(moduleId) |
| FontResource | object | 带uri、display、testString字段的对象 | 见下表 |
其中FontResource的字段定义见 Font.types.ts:
uri:字体文件地址(字符串或模块 ID)。display:设置浏览器端该字体的font-display属性,取值见下方FontDisplay枚举(仅 Web 生效)。testString:传给 FontFace Observer 的自定义测试字符串,用于 Web 端判断字体是否实际渲染(仅 Web 生效)。
4.2 FontDisplay 枚举(Web 字体显示策略)
FontDisplay对应 CSS@font-face的font-display属性,默认值为AUTO(Font.types.ts):
| 取值 | 含义 |
|---|---|
AUTO(默认) | 由浏览器/平台决定显示策略,通常表现为字体加载完成前文本不可见,适合按钮、横幅等需要特定字形效果的场景 |
SWAP | 先用系统回退字体立即渲染文本,字体加载完成后替换,内容"秒开",通常最推荐 |
BLOCK | 字体加载完成前文本完全不可见;若字体加载失败则什么都不显示(调试缺失文本时建议关闭) |
FALLBACK | 折中策略:前 100ms 文本不可见,之后用回退字体渲染并继续后台加载 |
OPTIONAL | 与FALLBACK类似,但浏览器会根据网络速度或资源压力决定是否加载字体 |
源码注释补充了两个重要细节:该值在浏览器中写入生成的@font-faceCSS 块,不能基于使用它的元素动态修改;而在原生平台,fontDisplay虽然不生效,但主流旗舰设备(iOS、Samsung、Pixel 等)的默认行为已近似模拟SWAP的效果,One Plus 等个别设备行为略有差异(Font.types.ts)。
4.3 Web 端实现原理
在浏览器环境中,loadAsync最终进入 ExpoFontLoader.web.ts:
- 所有注入的
@font-face规则统一放在一个 id 为expo-generated-fonts的<style>元素中(ExpoFontLoader.web.ts),CSS 模板形如@font-face{font-family:"...";src:url(...);font-display:...}(ExpoFontLoader.web.ts)。 - 通过
fontfaceobserver库监听字体实际加载状态,并传入resource.testString与 12000ms 超时(ExpoFontLoader.web.ts)。 - 在 iOS Safari、Safari、Edge、IE 等浏览器中会跳过字体加载监听(这些浏览器与 FontFaceObserver 存在已知兼容问题,ExpoFontLoader.web.ts),直接返回已解析的 Promise。
五、构建期字体嵌入:config plugin 方案
原 README 在 Font.ts 的 API 文档注释中特别强调:"We recommend using the config plugin instead whenever possible"(只要条件允许,优先使用 config plugin)。这是因为构建期嵌入的字体不需要在运行时下载/注册,启动更快且无网络依赖。
5.1 在 app config 中声明字体
在app.json/app.config.js中通过expo-font的插件声明字体文件路径(相对项目根目录):
{ "expo": { "plugins": [ [ "expo-font", { "fonts": [ "./assets/fonts/SpaceMono-Regular.ttf", "./assets/fonts/SpaceMono-Bold.ttf" ] } ] ] } }插件支持的FontProps结构见 plugin/src/withFonts.ts:
fonts:字体文件路径数组,同时应用到 iOS 与 Android(取props.fonts与对应平台字段的并集)。android.fonts:Android 专属声明,支持对象语法,可为 XML 字体指定自定义 family 名称(见下文)。ios.fonts:iOS 专属声明,family 名称从字体文件本身读取。
插件执行逻辑(withFonts.ts):先合并fonts与ios.fonts调用withFontsIos,再合并fonts与android.fonts调用withFontsAndroid,最后通过createRunOncePlugin保证每个构建过程只执行一次。
5.2 Android 对象语法与可变字体(Variable Font)支持
对于 Android,android.fonts支持两种元素(withFonts.ts):
字符串:字体文件路径。
FontObject:为同一个
fontFamily提供多个字重定义,格式为:{ fontFamily: 'MyVariableFont', path: './assets/fonts/MyVariableFont.ttf', fontDefinitions: [ { weight: 400 }, // Regular { weight: 700 }, // Bold { weight: 700, axes: { wght: 650 } }, // 按 wght 轴实例化 ], }其中
FontDefinition的字段包括:path:静态字体时每个定义可指向不同字体文件。weight:字重(数字),决定fontWeightJS prop 匹配哪个 face。style:'normal'|'italic'。axes:可变字体的轴实例化参数,如{ slnt: -10 }生成斜体字形。注释特别说明:weight只决定 JS prop 匹配,axes决定实际绘制效果,二者可以分离(如weight: 700配{ wght: 650 }表示"匹配 bold 请求但文件按 650 字重绘制",wght默认取weight值);仅声明style: "italic"并不会让字形倾斜,必须配合slnt或ital轴。
仓库还保留了 OpenType 注册表中的五个标准轴标签类型:
ital、opsz、slnt、wdth、wght,同时允许自定义四字符轴标签(如GRAD,withFonts.ts)。
插件的单元测试位于 plugin/src/tests,包括
withFontsAndroid-test.ts与utils-test.ts,覆盖了 Android 字体嵌入与工具函数的预期行为,可作为理解插件输入输出约定的参考。
六、原生侧原理:字体注册与别名管理
6.1 iOS:CoreText 注册 + PostScript 名称别名
iOS 侧的核心模块是 FontLoaderModule.swift。loadAsync的底层流程为:
- 若该 family 别名已注册过,先反注册旧字体,否则 App 重载时
CTFontManagerRegisterFontsForURL会因字体名重复而失败(FontLoaderModule.swift)。 - 通过 CoreText 注册字体文件。
- 读取字体文件内提供的全部 PostScript 名称,并为每个命名实例(可变字体的每个字重实例)建立"PostScript 名 →
fontFamilyAlias"的别名映射(FontLoaderModule.swift),这样fontWeightstyle prop 才能解析到正确的字重 face。 - 只把应用提供的别名记入
registeredFonts——这正是Font.isLoaded的判断依据,也是loadAsync去重跳过的依据(FontLoaderModule.swift)。
源码注释特别强调了两点兼容性约定:getLoadedFonts与loadAsync会暴露在globalThis.expo.modules.ExpoFontLoader下,可能被 Expo 之外的消费者(如 react-native-vector-icons)使用,因此函数签名与属性名不能随意变更(FontLoaderModule.swift)。
6.2 Android / iOS 通用:JS 侧缓存策略
memory.ts 实现了跨端一致的缓存语义:
markLoaded把已加载字体写入 JS 侧布尔缓存,避免每次isLoaded都查询原生模块。isLoadedNative首次未命中缓存时,会调用原生getLoadedFonts一次性同步本地缓存再判断(memory.ts)。loadPromises表用于并发去重,purgeCache与purgeFontFamilyFromCache服务于unloadAllAsync/unloadAsync等卸载 API。
6.3 其他导出能力
除了运行时加载,expo-font还提供了测试辅助 API(标注@hidden,主要用于测试环境):unloadAllAsync()(卸载全部自定义字体)与unloadAsync(fontFamilyOrFontMap, options?)(按名称卸载指定字体,Font.ts)。此外,Android/iOS 平台还提供renderToImageAsync(glyphs, options),将文本用指定字体渲染为图片(FontUtils.ts)。
七、使用建议与注意事项
- 优先构建期嵌入:静态、不变的自定义字体应通过 config plugin 嵌入,运行时
loadAsync仅用于远程字体、按需字体或动态场景。 - 善用并发去重:多处同时调用
loadAsync加载同一字体是安全的,内部会合并为同一次加载。 - 加载失败兜底:官方注释建议用
try/catch/finally包裹loadAsync,避免字体加载失败阻塞应用启动。 - Web 端字体名称唯一:Web 端每个
fontFamily对应一个@font-face规则;同名重复加载会被getFontFaceRulesMatchingResource判定已存在而跳过(ExpoFontLoader.web.ts)。 - 版本兼容:
expo-font依赖expo、react、react-native作为 peerDependencies(package.json),版本应与当前 SDK 保持一致,托管项目务必使用npx expo install安装。
八、仓库导航:进一步阅读
- README(本文主体)
- 运行时 API 实现
- 类型定义:FontSource / FontResource / FontDisplay
- React Hooks:useFonts
- 资源归一化与下载
- JS 侧缓存与并发去重
- Web 端 @font-face 注入
- iOS 原生模块
- config plugin 声明与实现
- 插件单元测试
- 包元数据与依赖
【免费下载链接】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),仅供参考