Folo(Follow)移动端 v0.2.5 更新详解:文章内容独立字号与预设档位的实现剖析
本文聚焦 Folo(Follow)移动端 v0.2.5 发布说明(见 apps/mobile/changelog/0.2.5.md)中的核心新增能力:为文章正文设置独立于界面字号的字体大小,并提供多档预设,同时梳理该版本针对"特定场景下无法订阅 Feed""通知打开的条目无法加载"两项缺陷的修复。通过逐行对照移动端 UI 设置原子、外观设置界面与 WebView 渲染通道的源码,读者将理解 Folo 移动端"界面字号 / 内容字号"双轨缩放机制的完整设计、实现路径与配置方式,可直接在「设置 → 外观 → 字体缩放」中复现并调优。
版本快照:v0.2.5 改了什么
依据官方变更日志,v0.2.5 的内容可分为三类:
- 新功能(Shiny new things)
- 新增独立的文章内容字体大小选项及预设档位(Add option for independent content font size and presets)。
- 改进(Improvements)
- 无显式条目。
- 缺陷修复(No longer broken)
- 修复特定场景下无法关注/订阅 Feed(follow the feed)的问题;
- 修复从系统通知打开条目(entries opened from notifications)时无法加载的问题。
其中真正具备完整"新增能力 + 用户可配置入口 + 底层实现"闭环的是第一条。它对应的用户入口位于「设置 → 外观(Appearance)→ 字体缩放」,本节将基于 外观设置界面 及其依赖的原子状态与渲染通道逐层展开。
功能入口:外观设置里的"字体缩放"设置组
在移动端「外观」设置页中,"字体缩放"作为一个独立分组(appearance.font_scaling.title)提供四个联动开关/选择器,其 UI 渲染位于 Appearance.tsx:
| 设置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
使用系统字体缩放(useSystemFontScaling) | Switch | true | 跟随系统无障碍字号(详见 默认值定义) |
字体缩放比例(fontScale) | Select | 1(m / 默认) | 关闭系统缩放后,以倍数形式调节全局界面与内容字号 |
内容使用独立字体大小(useDifferentFontSizeForContent) | Switch | false | 打开后,正文内容不再跟随全局缩放,而使用独立的像素档位 |
内容字体大小(mobileContentFontSize) | Select | 16(m / 中号) | 仅在上一个开关开启时显示,设置文章正文的绝对字号 |
设置状态全部落在一个名为ui的设置原子中。Folo 移动端在继承共享端UISettings的基础上扩展了四个字段(见 apps/mobile/src/atoms/settings/ui.ts#L8-L13):
export interface UISettings extends BaseUISettings { fontScale: number useSystemFontScaling: boolean useDifferentFontSizeForContent: boolean mobileContentFontSize: number }对应的出厂默认值分别为fontScale: 1、useSystemFontScaling: true、useDifferentFontSizeForContent: false、mobileContentFontSize: 16。也就是说,开箱即用的字号体验完全跟随系统无障碍缩放,需要手动关闭系统缩放后才能看到"字体缩放比例"档位;再额外打开"内容使用独立字体大小",正文才会进入独立像素档位模式。这一默认取向与无障碍优先的设计一致。
值得注意的细节是服务端同步白名单(apps/mobile/src/atoms/settings/ui.ts#L36-L43):fontScale与useSystemFontScaling会被同步到服务端,而移动端新增的useDifferentFontSizeForContent与mobileContentFontSize属于纯本地偏好,不在同步列表内。
两套预设档位:倍数缩放 vs 绝对像素
v0.2.5 引入的"presets(预设)"不是一套而是两套并存,这在源码中定义得非常明确(Appearance.tsx):
① 全局字体缩放预设(倍率制)
const fontSizePresets = [ { value: 0.8, key: "xs" }, { value: 0.9, key: "s" }, { value: 1, key: "m" }, { value: 1.2, key: "l" }, { value: 1.5, key: "xl" }, ] as const| key | 中文标签 | 缩放倍率 |
|---|---|---|
| xs | 较小 | ×0.8 |
| s | 小 | ×0.9 |
| m | 默认 | ×1.0 |
| l | 大 | ×1.2 |
| xl | 较大 | ×1.5 |
② 文章内容字号预设(像素制)
// Content font size presets (in px) const contentFontSizePresets = [ { value: 12, key: "xs" }, { value: 14, key: "s" }, { value: 16, key: "m" }, { value: 18, key: "l" }, { value: 20, key: "xl" }, ] as const| key | 中文标签 | 字号(px) |
|---|---|---|
| xs | 极小号 | 12 |
| s | 小号 | 14 |
| m | 中号 | 16 |
| l | 大号 | 18 |
| xl | 特大号 | 20 |
两套档位的差异是理解该功能的关键:界面缩放是"相对倍率",作用于排版 token 全家桶;内容字号是"绝对像素",直接注入条目渲染 WebView 的根字号。档位的中英文标签可在 locales/settings/zh-CN.json 中找到(如appearance.font_scaling.content_size.m→ "中号")。
界面层实现:FontScalingProvider 与排版 token 缩放
当用户通过"字体缩放比例"选择档位后,这个倍率如何作用到整个 App 的文字?
答案在 FontScalingProvider.tsx 中:它以 Provider 形式包裹整个视图树,根据全局设置计算出一组 CSS 变量并注入到容器<View>的 NativeWindvars()中:
const createFontScalingInjectStyles = (scale = 1) => { const cssVars = Object.entries(typography).reduce((acc, [key, [fontSize, lineHeight]]) => { const kebabKey = key.replaceAll(/([A-Z])/g, "-$1").toLowerCase() const nextFontSize = Math.round(fontSize * scale) const originalRatio = lineHeight / fontSize // 小字号行高至少 1.2 倍字号,大字号至少 1.0 倍,保证可读性 const minRatio = fontSize >= 36 ? 1 : 1.2 const scaledLineHeight = Math.round( Math.max(nextFontSize * Math.max(originalRatio, minRatio), lineHeight * scale), ) acc[`--text-${kebabKey}`] = nextFontSize acc[`--text-${kebabKey}-line-height`] = scaledLineHeight return acc }, {} as Record<string, number>) return vars(cssVars) as StyleProp<ViewStyle> }该实现有两个值得深挖的设计点:
- 字体与行高同步缩放:遍历
typography排版规范中的每一组[fontSize, lineHeight],分别按倍率换算成--text-{key}与--text-{key}-line-height两个 CSS 变量,保证缩放后行高与字号仍成比例; - 行高下限保护:通过
minRatio兜底——小字号下行高至少保持字号的 1.2 倍、超大字号(≥36)也至少 1.0 倍,避免极小/极大字号场景出现行距塌陷; - 与系统缩放的联动:Provider 在组装样式时执行
systemFontScaling ? 1 : fontScale(FontScalingProvider.tsx#L35-L38),即当"使用系统字体缩放"开启时,不叠加自绘的倍率样式,把字号控制权完全交给系统。
内容层实现:条目 WebView 的根字号注入
界面布局走 NativeWind + CSS 变量,而文章正文并不在 RN 原生层渲染——它由一套独立的 HTML 渲染器(web-app/html-renderer目录)在 WebView 中展示。因此"内容独立字号"必须打通一条从设置原子到 WebView 的通道。
在条目渲染 Hook useWebViewEntry 中可以看到两条相互配合的生效逻辑:
// ① 全局字号:跟随系统时取 PixelRatio.getFontScale(),否则用自定义倍率 useEffect(() => { const fontScale = useSystemFontScaling ? PixelRatio.getFontScale() : customFontScale WebViewManager.setRootFontSize(fontScale * 16) }, [useSystemFontScaling, customFontScale]) // ② 独立内容字号:开启后直接注入移动端内容字号(绝对 px) useEffect(() => { if (useDifferentFontSizeForContent) { WebViewManager.setRootFontSize(mobileContentFontSize) } else { const fontScale = useSystemFontScaling ? PixelRatio.getFontScale() : customFontScale WebViewManager.setRootFontSize(fontScale * 16) } }, [useDifferentFontSizeForContent, mobileContentFontSize, useSystemFontScaling, customFontScale])机制解读:
- 默认状态(
useDifferentFontSizeForContent = false)下,WebView 根字号 =fontScale × 16:跟随系统时用 React Native 的PixelRatio.getFontScale()拿到系统无障碍倍率;关闭系统缩放后则使用用户在fontScale档位中选的倍率(0.8~1.5)乘上 16px 基准; - 开启"内容使用独立字体大小"后,第二段 Effect 立即接管:不再计算倍率,而是把
mobileContentFontSize的绝对像素值(12~20px)直接设为 WebView 根字号——这正是 v0.2.5"为正文内容提供独立且带预设"的含义。
最终下发动作位于 webview-manager.ts:
setRootFontSize(size = 16): void { SharedWebViewModule.dispatch?.("setRootFontSize", JSON.stringify(size)) }它通过原生桥接模块向共享 WebView 实例发送setRootFontSize消息,HTML 渲染器据此调整其 CSS 根字号(rem基准),从而让整篇文章的排版随单一值整体缩放。原生渲染(列表、导航等界面文字)由FontScalingProvider负责、WebView 内正文由setRootFontSize负责,两条链路各司其职、互不干扰——这正是"独立内容字号"在架构上成立的前提。
操作路径与调参建议
要把 v0.2.5 的新能力用起来,可按以下路径操作(移动端「设置 → 外观」内):
- 纯跟随系统(默认):保持「使用系统字体缩放」开启即可,系统无障碍字号越大 App 越大,无需额外设置;
- 全局倍率微调:关闭「使用系统字体缩放」,在「字体缩放」档位中选择 ×0.8~×1.5。该档位会同时影响界面与正文(正文走
倍率 × 16); - 正文单独调整:在上述基础上开启「内容使用独立字体大小」,随后在「内容字体大小」中选择 12/14/16/18/20px 五档预设。此时界面对话列表的字号不再影响正文,反之亦然;
- 恢复默认:将「内容使用独立字体大小」关闭并把「字体缩放」切回"默认(×1.0)"。
从源码可推断的组合边界是:mobileContentFontSize选择器仅在useDifferentFontSizeForContent为真时才渲染(Appearance.tsx#L206-L223),因此不存在"开关关闭却仍显示像素档位"的歧义状态。
同版本的两项缺陷修复
v0.2.5 同时合入了两项修复,官方变更日志的描述如下:
- 修复特定场景下无法关注 Feed:涉及订阅(follow/subscribe)流程在特定输入与网络组合下失败的健壮性修复,保证 Feed 能被成功加入订阅列表;
- 修复从通知打开的条目无法加载:涉及通知深链(deep link)跳转后条目内容初始化失败的问题,修复后用户点击推送可直接进入对应条目阅读页。
由于二者属于回归性修复而非功能新增,changelog 记录较为精简;若需排查或回滚验证,可关注订阅列表模块与通知/深链初始化路径的测试用例与提交记录。其验证成本较低:分别尝试从搜索/发现页关注多种类型 Feed、以及在收到推送后点击通知打开条目即可复现原问题是否消除。
小结
v0.2.5 是 Folo 移动端在阅读排版可访问性上的一次重要迭代。它引入的并非单纯的"加个大字号选项",而是一套双轨字号体系:界面层通过 FontScalingProvider 按倍率缩放排版 token,内容层通过 useWebViewEntry 把独立像素档位注入条目 WebView 根字号;两层又都受ui设置原子的三个开关(系统缩放 / 倍率档位 / 独立内容字号)约束,最终以清晰的分组呈现在「外观 → 字体缩放」中。
对于开发者,围绕本功能可继续阅读的关键文件包括:设置定义 ui.ts、预设与 UI Appearance.tsx、缩放注入 FontScalingProvider.tsx、WebView 下发 hooks.ts 与 webview-manager.ts。若需扩展新档位(例如更大的 ×2.0 或 24px),只需同步修改Appearance.tsx中的两套presets数组与对应的 中文文案档位键 即可,其他渲染通道会自动适配。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考