Firefox for iOS 用 Redux 替代 MVVM:ADR-0004 架构决策与实践落地
2026/9/15 11:07:30 网站建设 项目流程

Firefox for iOS 用 Redux 替代 MVVM:ADR-0004 架构决策与实践落地

【免费下载链接】firefox-iosFirefox for iOS项目地址: https://gitcode.com/GitHub_Trending/fi/firefox-ios

导读

adr/0004-using-redux-to-replace-mvvm.md是 Firefox for iOS 团队于 2024-09-05 批准生效的一篇架构决策记录(ADR,Architecture Decision Record),它标志着项目从长期依赖的 MVVM 架构正式转向 Redux 单向数据流架构。本文围绕该 ADR 展开,先还原决策的背景与动机,再逐条拆解 Actions、Store、Middleware、Reducer 四大核心概念在本仓库 BrowserKit/Sources/Redux 中的真实实现,最后结合 ADR-0005 的导航集成方案与BrowserViewControllerState源码,说明 Redux 在真实界面中的落地形态。读完本文,你将掌握这套 Redux 架构的核心约束(类化 Action、主线程派发、2-line switch 规则、业务逻辑进中间件、表现逻辑进 Reducer),并能定位到仓库中的全部支撑源码与测试。

说明:本 ADR 引用的外部链接(Google Docs、Confluence 等)不在仓库内,本文一律不引用;所有事实均来自仓库内文档与源码。

一、决策背景:为什么要抛弃 MVVM

ADR-0004 并非孤立决定,它建立在前期试点(Pilot)的基础之上。同一目录下的 adr/0003-redux-pilot.md(2023-09-07,Approved)记录了试点动机:Firefox iOS 大量区域依赖 MVVM,同时一个庞大的BrowserViewController(BVC)仍承担大量逻辑与状态协调,由此带来四类问题:

  • 视图控制器互相耦合,出现临时拼凑的代理链(delegation chains);
  • 竞态条件与不确定的状态迁移(race conditions / nondeterministic state transitions);
  • 状态分散在控制器、视图模型和服务层中;
  • 导航与副作用和 UI 逻辑纠缠在一起,测试困难。

ADR-0003 因此决定引入一个受 ReSwift 核心类型启发的轻量自研 Redux 风格架构,先在ThemeSettingsController这类受控小范围内试点,验证确定性(determinism)、可测试性(testability)与开发者体验(ergonomics)。ADR-0004 正是把试点结论正式化为全项目标准:只要涉及状态处理,就用 Redux,逐步离开 MVVM

决策的核心论点

ADR-0004 的决策部分只有三条硬性约定,它们是后续一切规则的总纲:

约定含义
Redux 与 MVVM 是两套互不兼容的系统,不应混用同一个功能内不要同时出现 ViewModel 与 Redux 双层状态源
业务逻辑由 Middleware 处理网络请求、存储读写、Feature Flag 等副作用一律进中间件
表现逻辑由 Reducer 处理Reducer 只负责根据 Action 计算新的展示状态,保持纯函数

二、Actions:从枚举到类

ADR-0004 明确要求Actions 改为类(class),以简化载荷(payload)传递,并消除以往每个 action 枚举上重复携带ActionContext的需求。这项约定直接体现在 Action.swift 中:

/// Used to describe an action that can be dispatched by the redux store public protocol Action: Sendable, CustomDebugStringConvertible { var windowUUID: WindowUUID { get } var actionType: ActionType { get } }

Action协议要求每个 Action 携带两个关键属性:

  • windowUUID:窗口标识符。Firefox iOS 是多窗口应用(iPad 多窗口),Redux 的 Store 是全局单例,但每个窗口有各自的屏幕状态(ScreenState),因此 Action 必须携带窗口 UUID,供各窗口的 reducer 判断"这个 Action 是否属于我"。
  • actionType:动作类型,用于 reducer 中的 switch 分支。

ADR-0004 还给出了命名规范:创建 Action 类型时,倾向于使用"用户动作/API 动作"名称,而不是"状态变化"名称。例如tapOnTrackingProtection(用户点击了跟踪保护入口)优于trackingProtectionViewPresented(视图被展示)。原因在 ADR 中写得很清楚:Action 应清晰表达"发生了什么操作",而非"操作带来了什么后果"——视图不应该知道后果,后果由 reducer 计算、由状态承载。

ModernAction:正在进行的迁移

值得补充的是,仓库代码显示当前正处于一次内部迁移:Action.swift 中新增了ModernAction协议,并注明它是旧Action/ActionType的替代品,未来旧的协议会被废弃:

/// Used to describe an action that can be dispatched by the redux store. /// `ModernAction` is the replacement for the old `Action` and `ActionType` protocols, which will be deprecated /// in the future. public protocol ModernAction: Sendable { var description: String { get } }

ModernAction甚至用 Swift Mirror 反射自动生成枚举动作的可读描述(Action.swift),把关联值逐行打印成花括号包裹的调试文本,方便日志与断点排查。

三、Store:主线程派发与串行队列

ADR-0004 承诺:Store 自动保证 Action 在主线程执行,开发人员无需再自行加主线程检查。这在 Store.swift 中有三重印证:

  1. Store类整体标注@MainActor(Store.swift);
  2. 两个dispatch重载内部都调用MainActor.assertIsolated(...)(Store.swift),以断言方式强制主线程派发;
  3. 派发入口使用actionQueue+isProcessingActions实现串行处理:每个 Action 必须完整走完 reducer 与 middleware 之后,才处理下一个 Action(Store.swift),从机制上消除并发派发导致的竞态。

执行顺序:先 Reducer 后 Middleware

Store.swift 的executeAction展示了 Action 的处理流水线:

  1. 先用 reducer 基于当前state与 Action 计算出newState
  2. 再把newState与 Action 交给所有 middleware 响应(副作用在此触发);
  3. 最后state = newState统一提交,触发订阅者回调。

注意一个细节:注释明确说明每个活动屏幕的 reducer 都会被调用(即便 Action 的 UUID 与该屏幕窗口不同,reducer 也应自行比对 UUID 跳过处理),而 middleware 每个 Action 只被调用一次,与窗口数量无关。

订阅与状态广播

Store 通过subscribe/unsubscribe管理订阅者,并支持transform选择器只订阅状态树中的某个子状态(Store.swift)。状态变更时,只有真正发生改变才会通知订阅者——这一行为在 Subscription.swift 中由guard newState != oldState else { return }保证,前提是State遵循Equatable(见下文 State 协议)。

四、Reducer 与 Middleware:表现逻辑与业务逻辑的分工

ADR-0004 反复强调的两条职责边界,在仓库类型别名上直接可见。

Reducer:纯函数

Reducer.swift 将 Reducer 定义为类型别名元组,同时兼容旧Action与新ModernAction

public typealias Reducer<State> = (legacyReducer: LegacyReducerMethod<State>, modernReducer: ReducerMethod<State>) public typealias ReducerMethod<State> = @MainActor (State, ModernAction, WindowUUID) -> State public typealias LegacyReducerMethod<State> = @MainActor (State, Action) -> State

reducer 签名(State, Action) -> State决定了它是纯函数:给定相同的旧状态与 Action,必然产出相同的新状态。ADR-0004 的正面收益中特别提到"2-line switch 规则"——每个 case 分支内只写两行:一行调用明确的 reducer 方法(如reduceStateForNavigationBrowserAction(action:state:)),保证 reducer 简洁、状态流转可追踪。

Middleware:副作用集中地

Middleware.swift 的注释直接呼应 ADR 决策:"consume Actions and are designed to perform logic with side effects (e.g. API calls, logging, or accessing storage)"——API 调用、日志、存储访问等副作用都属于中间件职责。同时中间件可以向 Store 派发新的 Action(ADR-0003 明确允许这一点),例如一个网络请求中间件在收到.loadData后异步请求、再派发.dataLoaded

类型签名同样给出主线程与窗口上下文:

public typealias Middleware<State> = ( legacyMiddleware: LegacyMiddlewareClosure<State>, modernMiddleware: MiddlewareClosure<State> ) public typealias MiddlewareClosure<State> = @MainActor (State, ModernAction, WindowUUID) -> Void

ADR-0004 的负面收益也直言中间件的风险:"as most dependencies and side effects move there, middleware can become bloated if not well-scoped"(大多数依赖与副作用都移入中间件后,若不做良好拆分,中间件会膨胀),并强调 Feature Flag 等依赖应由中间件注入,避免污染 State 与视图。

State:单一不可变状态树

State.swift 定义了StateType协议,要求每个状态类型遵循SendableEquatable,并提供两个关键方法:

  • static func defaultState(from:):通过清空上一个状态的瞬态数据来生成默认状态,所有带默认值的属性应恢复默认;
  • func resetTransientState() -> Self:调用defaultState(from:)重置瞬态字段、保留其余字段,并应在.copy()链中第一个调用。

协议扩展为所有遵循类型免费提供resetTransientState()默认实现,避免每个状态类型手写一遍。结合 Reducer.swift 可以推断:项目采用手写的copy宏/方法模式来创建新状态,这与 adr/0011-redux-state-reducer-initializer-cleanup-with-copy-macro.md 后续的清理决策一脉相承。

五、落地案例:Redux 驱动的导航(ADR-0005 与 BrowserViewControllerState)

ADR-0004 只是架构转折点,紧随其后的 adr/0005-redux-and-navigation.md(2024-10-17,Accepted)给出了 Redux 与导航集成的具体模式,是 ADR-0004 决策在真实功能上的直接延续:

  • 新增NavigationBrowserAction类型与NavigationDestination枚举,抽象导航意图;
  • 把导航处理从HomepageState迁出,放入更全局的BrowserViewControllerState
  • BrowserViewController监听state.navigationDestination,非空时调用handleNavigation(to:)

这条链路在源码中完整可查。状态定义位于 BrowserViewControllerState.swift:

  • 结构体BrowserViewControllerState: ScreenState携带windowUUID(第 49 行)与navigationDestination: NavigationDestination?(第 63 行);
  • modernReducer在处理到NavigationBrowserAction时调用专门的reduceStateForNavigationBrowserAction(action:state:)(第 162-163、179 行);
  • reducer 的分支覆盖tapOnTrackingProtectiontapOnCelltapOnLinktapOnJumpBackInShowAllButtontapOnBookmarksShowMoreButton等用户动作(第 184-188 行),正是 ADR-0004"用用户动作命名 Action"的实证。

消费端在 BrowserViewController.swift:

case _ where state.navigationDestination != nil: guard let destination = state.navigationDestination else { return } handleNavigation(to: destination)

(第 2725-2727 行)UI 层只"响应状态",不直接发起导航调用;导航完成后还会派发NavigationBrowserActionType.navigationDestinationHandled(第 2731-2733 行)把瞬态清除,配合handleNavigationActions(for:)(第 2901 行)统一处理动作,正是"导航以状态/意图形式表达,而不是在 reducer 或视图逻辑中直接调用视图控制器"这一 ADR-0005 模式的落地。

此外BrowserViewControllerState还被 PresentedComponentsState.swift、ToolbarMiddleware.swift 等多个模块引用,说明"浏览器级全局状态"已成为跨模块共享状态的中枢。ADR-0005 同样坦承负面代价:"BrowserViewControllerState could get bloated"(可能膨胀),这也是社区对集中式状态树的普遍警惕点。

六、测试与工程约束

ADR-0004 明确要求:State 与 Middleware 现在必须有测试,以提升可靠性。仓库中 Redux 测试集中在 BrowserKit/Tests/ReduxTests(10 个 Swift 文件),覆盖 Action 派发、订阅、状态转换等核心行为;firefox-ios/firefox-ios-tests/Tests 下另有大量针对具体 screen state 的 Redux 相关测试。对中间件测试,ADR-0004 的负面收益也提醒:中间件测试需要 mock 依赖与初始化设置,在缺少工具类时可能冗长。

整套约束可总结为一张清单,方便新功能开发时对照检查:

  • Action:用类承载载荷;以用户动作/API 动作命名;携带windowUUID;主线程派发(Store 已保证);
  • Reducer:只做表现逻辑;纯函数;遵守 2-line switch 规则,case 内调用显式 reducer 方法;
  • Middleware:承接所有业务逻辑与副作用(网络、存储、Feature Flag、日志);需要时可回派 Action;保持小而聚焦,防止膨胀;
  • State:遵循StateTypeSendable+Equatable),实现defaultState(from:),用.copy()链更新;
  • 测试:State 与 Middleware 必须有测试。

七、收益与代价:ADR-0004 的自我评估

ADR-0004 在 Consequences 部分如实列举了正反两面,这里整理为决策复盘,便于团队在后续功能中对照验证。

Positive(正面收益)

  • 统一架构:所有新功能遵循 Redux,消除 MVVM 混用,移除 ViewModel;
  • 清晰的逻辑分层:中间件管业务逻辑与副作用,reducer 只管表现逻辑;
  • 更简单的 Action 模型:类化 Action 简化载荷传递,避免windowUUID等关联值的重复书写;
  • 可读可维护:2-line switch 规则让 reducer/中间件精简;显式 reducer 调用让状态流转可追踪;
  • 线程安全内建:Store 保证主线程执行,无需手动处理;
  • 命名一致:用户/API 风格的 Action 命名提升意图清晰度;
  • 测试强制:State 与 Middleware 必须配测试;
  • Feature Flag 集成干净:由中间件处理标志与依赖,不污染状态与视图。

Negative(负面代价)

  • 样板代码增加:类化 Action 与显式 reducer 调用对小功能是额外搭建成本;
  • 迁移空窗:老 MVVM 代码与新 Redux 区域共存,过渡期出现混合复杂度;
  • 中间件膨胀风险:依赖与副作用集中后,若不规范拆分容易失控;
  • Action 命名歧义:用户/API 动作与中间件内部动作的取舍早期易混淆;
  • 学习曲线:开发者需适应单向数据流,避免沿用 MVVM 思维;
  • 测试开销:中间件测试需 mock 依赖,缺少工具类时较冗长。

结语

adr/0004-using-redux-to-replace-mvvm.md记录了 Firefox for iOS 一次影响深远的架构转向:以"业务逻辑进中间件、表现逻辑进 reducer、Store 统一主线程派发、Action 类化并面向用户动作命名"为核心约束,用单向数据流换取确定性、可测试性与可维护性。仓库中的 BrowserKit/Sources/Redux 完整实现了这套协议族,BrowserViewControllerState.swift 与 BrowserViewController.swift 则展示了导航等真实场景下的落地模式。对于想在大型 iOS 工程中引入 Redux 的团队,这份 ADR 连同其前后相邻的 ADR-0003(试点)、ADR-0005(导航集成)、ADR-0011(copy 宏)与 ADR-0012(Action 规范)、ADR-0013(Reducer 最佳实践),构成了一套完整可复制的演进路径与工程约束清单。

【免费下载链接】firefox-iosFirefox for iOS项目地址: https://gitcode.com/GitHub_Trending/fi/firefox-ios

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

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

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

立即咨询