news 2026/9/11 16:17:48

expo-font 完全指南:在 Expo 与 React Native 中运行时加载字体

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
expo-font 完全指南:在 Expo 与 React Native 中运行时加载字体

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注入机制、原生端字体注册原理以及构建期字体嵌入方案。读完本文,你将能够熟练运用loadAsyncuseFonts等 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-assetAsset实例,或是带有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-font

npx expo install会依据当前 SDK 版本自动挑选与之兼容的expo-font版本,避免手动锁版本的麻烦。在本次仓库中,package.json 记录的版本为57.0.1

2.2 裸(Bare)React Native 项目

裸项目必须先保证expo包本身已被正确安装与配置(即expo-modules-core等原生模块体系可用),随后:

  1. 添加 npm 依赖

    npx expo install expo-font
  2. Android 配置:无需任何额外设置(README 原文:"No additional set up necessary.")。从仓库结构看,Android 侧的字体注册完全由运行时原生模块完成,不存在需要手动修改的清单文件。

  3. iOS 配置:安装 npm 包之后运行:

    npx pod-install

    这一步会为 iOS 工程安装 CocoaPods 依赖,将expo-font的原生代码(见 ios/FontLoaderModule.swift)链接进应用。

依赖声明:在裸项目中,expo-font的 peerDependencies 要求宿主环境提供exporeactreact-native,因此在安装expo-font前必须已有可用的 Expo Modules 环境。

三、核心 API:运行时字体加载全解析

expo-font在运行时层面暴露了loadAsyncisLoadedisLoadinggetLoadedFonts等函数,以及 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):

  1. 查重(isLoaded):先通过isLoaded判断该字体是否已加载(命中 JS 侧缓存或原生侧已注册列表),已加载则直接返回。
  2. 并发去重(loadPromises):维护一个loadPromises映射,若同一字体正在加载中,后续并发调用会复用同一个 Promise,而不是重复发起加载——源码注释明确说明这是为了避免同一种字体被加载 n 次(Font.ts)。
  3. 资源归一化(getAssetForSource):在 FontLoader.ts 中,Asset实例原样返回;字符串被当作远程 URI 调用Asset.fromURI;数字(require模块 ID)被转换为Asset.fromModuleFontResource对象则递归取其uri再归一化。
  4. 下载与注册:调用asset.downloadAsync()确保字体文件就绪,然后调用原生模块ExpoFontLoader.loadAsync(name, asset.localUri)(FontLoader.ts)。若下载失败,抛出CodedError('ERR_DOWNLOAD')
  5. 标记完成:注册成功后调用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 的四种形态

形态类型说明示例
本地模块numberrequire()返回的模块 ID,构建期打包进应用require('./assets/fonts/SpaceMono.ttf')
远程 URIstring字体文件的网络地址'https://example.com/font.ttf'
Asset 实例Assetexpo-asset管理的资源对象Asset.fromModule(moduleId)
FontResourceobjecturidisplaytestString字段的对象见下表

其中FontResource的字段定义见 Font.types.ts:

  • uri:字体文件地址(字符串或模块 ID)。
  • display:设置浏览器端该字体的font-display属性,取值见下方FontDisplay枚举(仅 Web 生效)。
  • testString:传给 FontFace Observer 的自定义测试字符串,用于 Web 端判断字体是否实际渲染(仅 Web 生效)。

4.2 FontDisplay 枚举(Web 字体显示策略)

FontDisplay对应 CSS@font-facefont-display属性,默认值为AUTO(Font.types.ts):

取值含义
AUTO(默认)由浏览器/平台决定显示策略,通常表现为字体加载完成前文本不可见,适合按钮、横幅等需要特定字形效果的场景
SWAP先用系统回退字体立即渲染文本,字体加载完成后替换,内容"秒开",通常最推荐
BLOCK字体加载完成前文本完全不可见;若字体加载失败则什么都不显示(调试缺失文本时建议关闭)
FALLBACK折中策略:前 100ms 文本不可见,之后用回退字体渲染并继续后台加载
OPTIONALFALLBACK类似,但浏览器会根据网络速度或资源压力决定是否加载字体

源码注释补充了两个重要细节:该值在浏览器中写入生成的@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):先合并fontsios.fonts调用withFontsIos,再合并fontsandroid.fonts调用withFontsAndroid,最后通过createRunOncePlugin保证每个构建过程只执行一次。

5.2 Android 对象语法与可变字体(Variable Font)支持

对于 Android,android.fonts支持两种元素(withFonts.ts):

  1. 字符串:字体文件路径。

  2. 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"并不会让字形倾斜,必须配合slntital轴。

    仓库还保留了 OpenType 注册表中的五个标准轴标签类型:italopszslntwdthwght,同时允许自定义四字符轴标签(如GRAD,withFonts.ts)。

插件的单元测试位于 plugin/src/tests,包括withFontsAndroid-test.tsutils-test.ts,覆盖了 Android 字体嵌入与工具函数的预期行为,可作为理解插件输入输出约定的参考。

六、原生侧原理:字体注册与别名管理

6.1 iOS:CoreText 注册 + PostScript 名称别名

iOS 侧的核心模块是 FontLoaderModule.swift。loadAsync的底层流程为:

  1. 若该 family 别名已注册过,先反注册旧字体,否则 App 重载时CTFontManagerRegisterFontsForURL会因字体名重复而失败(FontLoaderModule.swift)。
  2. 通过 CoreText 注册字体文件。
  3. 读取字体文件内提供的全部 PostScript 名称,并为每个命名实例(可变字体的每个字重实例)建立"PostScript 名 →fontFamilyAlias"的别名映射(FontLoaderModule.swift),这样fontWeightstyle prop 才能解析到正确的字重 face。
  4. 只把应用提供的别名记入registeredFonts——这正是Font.isLoaded的判断依据,也是loadAsync去重跳过的依据(FontLoaderModule.swift)。

源码注释特别强调了两点兼容性约定:getLoadedFontsloadAsync会暴露在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表用于并发去重,purgeCachepurgeFontFamilyFromCache服务于unloadAllAsync/unloadAsync等卸载 API。

6.3 其他导出能力

除了运行时加载,expo-font还提供了测试辅助 API(标注@hidden,主要用于测试环境):unloadAllAsync()(卸载全部自定义字体)与unloadAsync(fontFamilyOrFontMap, options?)(按名称卸载指定字体,Font.ts)。此外,Android/iOS 平台还提供renderToImageAsync(glyphs, options),将文本用指定字体渲染为图片(FontUtils.ts)。

七、使用建议与注意事项

  1. 优先构建期嵌入:静态、不变的自定义字体应通过 config plugin 嵌入,运行时loadAsync仅用于远程字体、按需字体或动态场景。
  2. 善用并发去重:多处同时调用loadAsync加载同一字体是安全的,内部会合并为同一次加载。
  3. 加载失败兜底:官方注释建议用try/catch/finally包裹loadAsync,避免字体加载失败阻塞应用启动。
  4. Web 端字体名称唯一:Web 端每个fontFamily对应一个@font-face规则;同名重复加载会被getFontFaceRulesMatchingResource判定已存在而跳过(ExpoFontLoader.web.ts)。
  5. 版本兼容expo-font依赖exporeactreact-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),仅供参考

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

AI Agent后端选型:PolarDB Agent Express与VM级安全隔离实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 16:16:59

OE_SOC芯片的架构综述

截至2025年&#xff0c;当前市场上的SoC&#xff08;System on Chip&#xff09;芯片主要基于以下几类处理器架构&#xff0c;其中以 ARM 架构占据绝对主导地位&#xff0c;同时 RISC-V 正在快速崛起&#xff0c;而 x86 和自研架构 在特定领域保持影响力。 一、主流 SoC 架构分…

作者头像 李华
网站建设 2026/9/11 16:16:58

人脸识别签到系统开发:从ArcFace模型选型到完整工程实现

简介&#xff1a;面向计算机专业学生的毕业设计与人脸识别实战&#xff0c;这套基于深度学习的人脸识别签到系统提供了完整源码与使用指南&#xff0c;项目经导师指导并获评审高分&#xff0c;难度适中&#xff0c;适合作为课程设计、毕业设计或项目练习的参考。资源压缩包共27…

作者头像 李华
网站建设 2026/9/11 16:12:25

Scala课程设计实战:基于滑动窗口与线性回归的交通拥堵预测

简介&#xff1a;一份基于Scala的交通拥堵预测课程设计源码&#xff0c;主要面向计算机相关专业学生、教师以及正在完成课设或大作业的开发者。项目以交通拥堵预测为业务场景&#xff0c;整合Scala编程、数据处理与数据库设计相关知识点&#xff0c;经导师指导评审获得高分&…

作者头像 李华