Actual Budget 实验性功能(Feature Flags)机制全解析:设计哲学、启用方式与源码实现
2026/9/12 4:22:10 网站建设 项目流程

Actual Budget 实验性功能(Feature Flags)机制全解析:设计哲学、启用方式与源码实现

【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual

导读

Actual Budget(以下简称 Actual)是一个本地优先(local-first)的个人财务管理应用,其仓库以 packages/ 下的多包结构组织。为了让尚未成熟的新功能能够在小范围真实用户中先行验证、收集反馈,Actual 建立了一套"实验性功能(Experimental Features / Feature Flags)"机制:功能默认关闭、用户按需开启、长时间无人维护即被移除。本文将首先完整还原官方文档 feature-flags.md 中关于实验性功能的设计意图与管理政策,然后结合用户侧文档 experimental/index.md 说明如何查看与启用这些功能,最后深入desktop-clientloot-core源码,剖析 Feature Flag 的存储、读取、UI 开关与典型消费方式,帮助你既会用、也懂原理,并为参与 Actual 功能开发提供可落地的规范参考。

一、为什么需要 Feature Flags:设计意图与价值

根据官方文档,Feature Flags(即实验性功能)是一种在应用中启用或禁用特定功能的手段。它主要服务于两类场景:

  • 小范围灰度验证:在功能全面推向所有用户之前,先让一小部分用户试用并收集反馈;
  • 大功能拆解交付:将一个大而复杂的功能拆解成多个更小、可独立发布的片段,逐个交付。

文档给出了一个非常具体的实例:custom reports(自定义报表)最初就以只读版本的形式在 Feature Flag 下发布,之后才逐步加入保存能力。这种方式让 Actual 团队能够在将其作为稳定的第一方(first-party)功能正式发布之前,就先把功能交到真实用户手中。

概括而言,Feature Flags 带来的核心收益是:

  • 将复杂的大功能拆分为更小的交付物,降低单次发布的规模与风险;
  • 在真实用户环境中提前发布,从而获得宝贵的真实反馈。

二、Feature Flags 的成本与严格治理政策

文档明确指出,Feature Flags 也有不可忽视的缺点:它们会让代码更复杂、更难理解;如果管理不善,还会积累技术债务。为此,Actual 对实验性功能施加了严格的管理政策:

  1. 三个月活跃开发红线超过 3 个月没有任何活跃开发的实验性功能,将从代码库中移除。这是为了保证代码库不被未完成的功能碎片污染。
  2. 移除前沟通:在移除某个实验性功能之前,团队会尽最大努力联系最初实现它的工程师;如果联系不上,将直接移除。
  3. 重新引入的许可:如果你愿意,完全可以把被移除的实验性功能重新带回来——前提是你承诺帮助把它打磨成第一方正式功能。
  4. 维护能力的现实约束:核心维护团队没有精力维护大量实验性功能,也没有精力去收尾那些被原作者放弃的功能;但对于仍在积极开发的功能,团队很乐意提供支持。

这条政策对贡献者意味着什么?如果你想在 Actual 中引入一个实验性功能,就必须做好"长期跟进、直至转正"的心理准备——它不是一条可随意搁置的后门,而是一条需要持续投入的交付通道。

三、FAQ:什么能用 Feature Flag,什么不能

官方文档用两条 FAQ 划清了边界:

3.1 能否把 Feature Flag 当作通用配置项使用?

不能。Actual 的设计哲学是"简洁、无杂乱(sleek and clutter-free)",这一原则同样适用于配置页和 Feature Flags。团队不希望为每一个小的 UI 细节都提供一个开关。

文档给出的例子是:类别选择器是否应该包含隐藏类别?Actual 只支持一种用例,不会提供在两种行为之间切换的开关。如果你确实需要这类个性化定制,请 fork UI 仓库,在自己的分支中实现。

对开发者而言,这条 FAQ 实际上是明确的代码审查信号:一个 Feature Flag 如果只是用来切换某个小视觉/行为细节,很可能会在评审中被拒绝。

3.2 为什么我的 Feature Flag 被移除了?

简短回答:很可能是该功能已超过 3 个月没有活跃开发。只要承诺继续推进、直至作为第一方功能发布,随时欢迎把它带回来。

详细回答:参见本文第二节的管理政策(即官方文档页面顶部的完整说明)。

四、用户视角:如何查看与启用实验性功能

实验性功能通常是可选的(opt-in)——如果用户不主动启用,体验不会有任何变化。由于这些功能仍在积极开发中,官方强烈建议:启用任何实验性功能前,务必做好定期备份

启用路径为:Settings -> Show advanced settings -> Experimental features

点击该链接前,设置页会显示一段措辞强硬的免责声明(该文本同样出现在 Experimental.tsx 源码中):

Experimental features.These features are not fully tested and may not work as expected. THEY MAY CAUSE IRRECOVERABLE DATA LOSS. They may do nothing at all. Only enable them if you know what you are doing.

(实验性功能。这些功能未经充分测试,可能无法按预期工作。它们可能导致不可恢复的数据丢失,也可能完全不起作用。只有在清楚自己在做什么时才启用它们。)

点击"我了解风险,显示实验性功能"(I understand the risks, show experimental features)后,展开列表如下:

上图中的功能列表仅是示例,当前可用的实验性功能会随版本变化。

每个功能以复选框呈现,部分功能右侧带有give feedback(提供反馈)外链,方便用户直接跳转到对应的 GitHub issue 讨论。以下基于当前仓库 Experimental.tsx 与 prefs.ts 中的FeatureFlag联合类型,整理出当前仓库实际存在的实验性功能清单(不含弃用项):

Flag 名称界面显示说明
goalTemplatesEnabledGoal templates目标模板(预算自动化)
goalTemplatesUIEnabledSubfeature: Budget automations UI目标模板的子功能:预算自动化 UI
actionTemplatingRule action templating规则操作模板化(已被标记 Deprecated,见下)
formulaModeExcel formula mode (Formula cards & Rule formulas)Excel 公式模式:公式卡片与规则公式
currencyCurrency support多币种支持
mobileCalculatorMobile calculator移动端计算器
newSidebarUINew sidebar UI新版侧边栏 UI
sankeyReportSankey report桑基图报表
balanceForecastReportBalance Forecast Report余额预测报表
budgetAnalysisReportBudget Analysis Report预算分析报表
monteCarloReportMonte Carlo Analysis Report蒙特卡洛退休分析报表
enableBankingEnable Banking sync (EU banks)Enable Banking 银行同步(欧盟银行)
akahuBankSyncAkahu Bank Sync (NZ banks)Akahu 银行同步(新西兰银行)
customThemes(类型中定义)自定义主题(类型层面保留)

其中值得注意的细节:

  • actionTemplating在设置页中被标记为Deprecated:官方提示"该功能将在未来版本移除,请改用 Excel 公式模式(Rule formulae)",同时保留反馈链接。
  • goalTemplatesUIEnabled作为子功能嵌套在goalTemplatesEnabled之下:只有当goalTemplatesEnabled || goalTemplatesUIEnabled为真时才展示(见 Experimental.tsx),体现了"主功能 + 子功能"的开关组合模式。
  • 部分实验性报表的详细使用说明已沉淀为用户文档,例如 monte-carlo-analysis.md、sankey-report.md、balance-forecast-report.md、budget-analysis-report.md、goal-templates.md、formulas.md 等,可在启用后按需查阅。

注意:设置页中还包含一个隐藏的ServerFeatureToggleflags.plugins,"Client-Side plugins (soon)"),它仅当本地存储中devEnableServerPrefs === 'true'时才会出现(见 Experimental.tsx),属于面向服务端偏好的开发者入口。

五、源码级原理:Feature Flag 的存储、读取与 UI 开关

5.1 类型的单一事实来源

Feature Flag 的合法名称由 loot-core/src/types/prefs.ts 中的FeatureFlag联合类型统一定义,当前包括:newSidebarUIgoalTemplatesEnabledgoalTemplatesUIEnabledactionTemplatingformulaModecurrencybalanceForecastReportcustomThemesbudgetAnalysisReportenableBankingsankeyReportakahuBankSyncmobileCalculatormonteCarloReport

这些 flag 作为跨设备同步偏好(SyncedPrefs)存在:在同一个SyncedPrefs类型中可以看到形如flags.${FeatureFlag}的模板键(见 prefs.ts),这意味着Flag 状态会随预算数据在设备之间同步——在一台设备上开启,其他设备同样生效。

5.2 持久化:偏好存储走数据库

Flag 的写入通过后端preferences/save接口完成。在 server/preferences/app.ts 中,saveSyncedPrefs{ id, value }直接写入数据库的preferences表(db.update('preferences', { id, value })),读取时则通过getSyncedPrefs执行SELECT id, value FROM preferences全量载入(app.ts)。也就是说,每一个flags.xxx本质上都是preferences表中的一行记录。

顺带一提,该模块还维护了一个FORMULA_FORMAT_SYNCED_PREFS集合:当保存的偏好命中其中(如numberFormatdefaultCurrencyCode等)时会重置公式偏好缓存(resetFormulaPreferencesCache()),保证公式计算结果即时反映新的格式设置。

5.3 前端读取:useFeatureFlag 与默认值

前端读取 Flag 的入口是 hooks/useFeatureFlag.ts:

export function useFeatureFlag(name: FeatureFlag): boolean { const [value] = useSyncedPref(`flags.${name}`); return value === undefined ? DEFAULT_FEATURE_FLAG_STATE[name] || false : String(value) === 'true'; }

其逻辑很直观:

  • 通过useSyncedPref订阅flags.${name}这个同步偏好;
  • 若偏好未定义,则回退到DEFAULT_FEATURE_FLAG_STATE中的默认值;
  • 否则按字符串'true'判定。

而 DEFAULT_FEATURE_FLAG_STATE 为每一个合法的FeatureFlag都显式给出了默认值,当前全部为false——这正是"实验性功能默认关闭、opt-in 开启"这一产品原则在代码层面的直接体现。

useSyncedPref本身(见 hooks/useSyncedPref.ts)通过 Redux 的prefsSlice派发saveSyncedPrefsaction 并订阅state.prefs.synced,从而把 UI 开关与后端持久化串成一条完整链路。

5.4 UI 开关:FeatureToggle 组件

设置页的每个开关由 Experimental.tsx 中的FeatureToggle组件渲染:

  • const enabled = useFeatureFlag(flagName)——读取当前状态;
  • 复选框checked={enabled}onChange时调用setFlagPref(String(!enabled))写入相反值(Experimental.tsx),从而实现"点击即切换";
  • 通过feedbackLink属性渲染可选的 "give feedback" 外链;
  • 通过disableToggle+error支持禁用态(例如flags.plugins显示为"即将推出"的禁用开关);
  • 通过note渲染警告/弃用提示(如actionTemplating的 Deprecated 说明)。

5.5 典型消费方式:路由级条件渲染

在功能侧,Flag 最常见的消费方式是"条件渲染"。以报表为例,reports/ReportRouter.tsx 先一次性读取四个报表类 Flag:

const balanceForecastReportEnabled = useFeatureFlag('balanceForecastReport'); const budgetAnalysisReportEnabled = useFeatureFlag('budgetAnalysisReport'); const sankeyReportEnabled = useFeatureFlag('sankeyReport'); const monteCarloReportEnabled = useFeatureFlag('monteCarloReport');

随后为每个 Flag 对应的报表路由加上条件包裹:例如budgetAnalysisReportEnabled && (...)才注册/budget-analysis路由(ReportRouter.tsx)、balanceForecastReportEnabled && (...)才注册/forecast路由(ReportRouter.tsx)、monteCarloReportEnabled && (...)注册/monte-carlo(ReportRouter.tsx)、sankeyReportEnabled && (...)注册/sankey(ReportRouter.tsx)。

类似地,侧边栏通过newSidebarUI在新旧两套实现之间切换:

const newSidebarUIEnabled = useFeatureFlag('newSidebarUI'); // ... {newSidebarUIEnabled ? <SidebarRedesign /> : <Sidebar />}

(见 components/sidebar/index.tsx 与 index.tsx)。这是"用 Flag 支撑 UI 渐进式重构"的典型用法:新旧组件并存,通过开关灰度切换。

此外,flags.currency还被后端公式模块消费:在 server/formulas/customFunctionsPreferences.ts 中,flags.currency被列入公式偏好白名单,并在计算时判断preferences['flags.currency'] === 'true'来决定是否启用多币种相关公式行为(customFunctionsPreferences.ts)。这说明 Flag 不仅能控制 UI,还能影响服务端的计算逻辑。

六、给贡献者的实操建议:如何引入并善用 Feature Flag

综合官方文档政策与源码现状,为在 Actual 中新增一个实验性功能,可遵循如下路径:

  1. 明确边界:确认该功能是"待成熟的大功能",而非"切换某个 UI 细节的配置项"。后者不符合产品哲学,应在评审阶段即被拦截。
  2. 注册 Flag:在 types/prefs.ts 的FeatureFlag联合类型中追加名称,并在 useFeatureFlag.ts 的 DEFAULT_FEATURE_FLAG_STATE 中给出默认值(默认应保持false)。
  3. 接入设置页:在 settings/Experimental.tsx 中新增一个FeatureToggle,必要时提供feedbackLink指向对应的 GitHub issue,便于用户反馈。
  4. 消费 Flag:在功能入口处通过useFeatureFlag('yourFlag')读取状态,采用条件渲染(如路由注册、组件切换)控制可见性;若功能影响后端行为(类似flags.currency),还需在对应服务端逻辑中读取同步偏好。
  5. 同步语义:记住 Flag 属于SyncedPrefs,会跨设备同步;本地独有的偏好不要用 Flag 承载(应走LocalPrefs,参见 prefs.ts)。
  6. 维护承诺:随时准备在 3 个月内持续迭代,直至功能转正为第一方功能;若中途搁置,Flag 将按管理政策被移除。

七、总结

Actual 的实验性功能机制是一套"产品纪律 + 工程规范"的结合体:产品层面,它坚持 opt-in、简洁无杂乱的设计哲学,并明确拒绝把 Flag 当通用配置项滥用;工程层面,它以 prefs.ts 的FeatureFlag类型为单一事实来源,以preferences表为持久化载体,以useFeatureFlag+FeatureToggle+ 条件渲染为完整消费链路,覆盖从用户开关到路由、组件乃至服务端公式逻辑的全栈范围。

对普通用户而言,本文提供了一条安全的启用路径:先备份、再进入Settings -> Show advanced settings -> Experimental features,逐个评估后开启,并善用 "give feedback" 反馈通道;对开发者而言,本文则给出了完整的源码级参照系,帮助你在 Actual 中规范、可持续地推进新功能,避免陷入"Flag 堆积成技术债务"的陷阱。

延伸阅读

  • 官方贡献指南:CONTRIBUTING.md
  • 实验性功能用户文档索引:packages/docs/docs/experimental/index.md
  • 偏好类型定义:packages/loot-core/src/types/prefs.ts
  • 前端 Flag 读取 Hook:packages/desktop-client/src/hooks/useFeatureFlag.ts
  • 设置页 UI 实现:packages/desktop-client/src/components/settings/Experimental.tsx
  • 后端偏好持久化:packages/loot-core/src/server/preferences/app.ts

【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询