- 前端
【免费下载链接】fish-redux
An assembled flutter application framework.
导读
本篇技术指南围绕 Fish Redux 中页面(Page)这一核心概念展开,深入讲解"一个页面只有一个 Store"的架构约束、Page 对 Component 能力的完整继承、通过 Middleware 实现 Redux AOP 管理,以及必须配置的 initState 初始化函数。阅读完成后,你将掌握 Page 的完整构造参数、其从构建到销毁的 Store 生命周期原理,并能结合实际源码在项目中正确创建、增强和连接页面。
Page 是什么:组装式 Flutter 应用框架的最小完整单元
在 Fish Redux 中,Page 是应用中最完整、最独立的功能单元——它是从路由参数到界面渲染、从 State 到 Store 的完整闭环。官方文档 docs/concept/page.md 给出了 Page 的四个核心特征:
- 一个页面有且仅有一个 Store(One and only one store in one page);
- Page 继承自 Component,因此可以配置 Component 的全部要素(view、reducer、effect、dependencies、shouldUpdate 等);
- Page 可以配置 Middleware,用于对 Redux 进行 AOP 管理;
- Page 必须配置一个初始化函数 initState,用于初始化页面数据。
对应源码实现位于 lib/src/redux_component/page.dart,其中Page<T, P>是一个抽象类,继承自Component<T>,并引入了两个类型参数:T是页面的 State 类型,P是路由参数(route-params)类型。initState的类型定义清晰表明其职责:
/// init store's state by route-params typedef InitState<T, P> = T Function(P params);即:initState 负责把路由参数转换为页面的初始 State,这就是页面数据初始化的唯一入口。
为什么"一个页面只有一个 Store"
从框架架构上看,这是 Fish Redux 的刻意设计。看 PageRoutes 的实现:
/// Each page has a unique store. class PageRoutes implements AbstractRoutes { final Map<String, Page<Object, dynamic>> pages; ... @override Widget buildPage(String path, dynamic arguments) => pages[path]?.buildPage(arguments); }每个注册在路由表中的页面,通过buildPage(path, arguments)独立构建,各自拥有一套独立的 Store 生命周期。从源码结构可以推断,这种"页面私有 Store"的设计带来两个直接好处:
- 页面之间状态隔离:页面 A 的 reducer/effect 只作用于页面 A 的 Store,不会污染其他页面;
- 按需创建与销毁:Store 随页面 Widget 的
initState创建、随dispose销毁,资源生命周期与页面完全对齐。
如果需要跨页面共享数据,框架并不要求强行合并 Store,而是通过connectExtraStore把 AppStore(全局 Store)与页面 Store 建立单向数据连接,见下文"页面 Store 与全局 Store 的连接"一节。
Page 如何继承 Component 的全部能力
Page<T, P>的构造函数把 Component 的要素全部透传给了父类构造器,对比 page.dart 与 component.dart 可以看到完整的参数集:
| 参数 | 类型 | 说明 |
|---|---|---|
initState | InitState<T, P> | 必填(assert(initState != null)),路由参数到初始 State 的映射函数 |
view | ViewBuilder<T> | 必填,Widget Function(T state, Dispatch dispatch, ViewService viewService),决定如何渲染 |
reducer | Reducer<T> | 可选,页面级别的 reducer,处理 Action 产生新 State |
filter | ReducerFilter<T> | 可选,reducer 前置过滤器,主 reducer 复杂时用于提升性能 |
effect | Effect<T> | 可选,页面级别的副作用处理函数 |
dependencies | Dependencies<T> | 可选,页面内的子组件(slots)与列表适配器(adapter)声明 |
shouldUpdate | ShouldUpdate<T> | 可选,决定 Store 变化时页面是否重建,默认按!identical(old, now)判断 |
wrapper | WidgetWrapper | 可选,对页面 Widget 的包裹函数(如 KeepAlive、RepaintBoundary) |
middleware | List<Middleware<T>> | 可选,Store 层 AOP(对应 Redux 中间件) |
viewMiddleware | List<ViewMiddleware<T>> | 可选,View 层 AOP |
effectMiddleware | List<EffectMiddleware<T>> | 可选,Effect 层 AOP |
adapterMiddleware | List<AdapterMiddleware<T>> | 可选,Adapter 层 AOP |
这里需要特别注意:initState和view是两个必填参数,源码中assert(initState != null)与 Component 中的assert(view != null)共同保证了一个 Page 既要有数据来源、又要有渲染出口。而shouldUpdate未指定时默认使用updateByDefault<T>(),即(K _, K __) => !identical(_, __),只在此较前后 State 不是同一个实例时才重建。
最小可运行示例:Hello World 页面
官方文档给出的示例完整演示了 Page 的最小用法:
/// Hello World class HelloWordPage extends Page<String, String> { HelloWordPage(): super( initState: (String msg) => msg, view:(String msg, _, __) => Text('Hello ${msg}'), ); } HelloWordPage().buildPage('world')这里Page<String, String>中,第一个类型参数是 State 类型String,第二个是路由参数类型String。initState: (String msg) => msg直接把传入的参数'world'作为初始 State;view中三个参数依次是 state、dispatch、viewService,此处用_和__忽略后两者,渲染出Text('Hello world')。最后调用buildPage('world')得到一个可直接挂载到 Widget 树的页面 Widget。这演示了 Page 的三个关键点:
- 路由参数通过
buildPage(param)传入; - 参数经
initState变成 Store 的初始 State; - 页面数据流完全由框架接管,无需手动创建 Store。
Page 的 Store 生命周期:从 createStore 到 teardown
Page 的 Store 生命周期全部由框架在 page.dart 的_PageState中托管:
class _PageState<T, P> extends State<_PageWidget<T, P>> { Store<T> _store; DispatchBus _pageBus; @override void initState() { super.initState(); _store = widget.page.createStore(widget.param); _pageBus = widget.page.createPageBus(); } @override void dispose() { _pageBus.detach(); _store.teardown(); super.dispose(); } }对应的 Store 创建链路在Page.createStore中:
Store<T> createStore(P param) => updateStore(createBatchStore<T>( _initState(param), createReducer(), storeEnhancer: enhancer.storeEnhance, ));整个生命周期可以拆解为四个阶段:
- 创建:
createStore(param)内部先调用_initState(param)得到初始 State,再调用createReducer()生成页面 reducer,经enhancer.storeEnhance应用 Store 层中间件后,通过createBatchStore包装成支持批量通知的 Store; - 订阅:
_PageState.build调用page.buildComponent(...),把 Store 与页面级DispatchBus交给 Component 体系,页面内的子组件通过store.subscribe注册监听(见 component.dart 中_ctx.registerOnDisposed(widget.store.subscribe(...))); - 跨页通信:
didChangeDependencies阶段_pageBus.attach(widget.page.appBus),把页面私有总线挂到全局共享的 appBus 上,实现页面间广播; - 销毁:
dispose阶段先_pageBus.detach()摘除总线监听,再_store.teardown()释放 Store,子组件注册的监听也随之解绑。
值得展开的是createBatchStore(实现在 lib/src/redux_component/batch_store.dart)。它通过_BatchNotifymixin 重写了subscribe,把多次 dispatch 引起的状态变更合并到一帧内统一通知订阅者:
void _batch() { if (!isInSuitablePhase()) { // 若处于不合适的调度阶段,则推迟到帧后回调再批量通知 ... } else { final T curState = getState(); if (!identical(_prevState, curState)) { _prevState = curState; ... // 通知所有 listener } } }从源码逻辑可以推断,这一机制避免了在一次 Action 处理过程中多次setState导致的重复渲染,这是 Page 级 Store 与普通 Redux Store 的重要差异之一。
Middleware 与四层 AOP 管理
Page 的构造参数中包含四类中间件,分别对应 Store、View、Effect、Adapter 四个切面。它们在构造时统一交给EnhancerDefault管理(lib/src/redux_component/enhancer.dart):
Page({ ... List<Middleware<T>> middleware, List<ViewMiddleware<T>> viewMiddleware, List<EffectMiddleware<T>> effectMiddleware, List<AdapterMiddleware<T>> adapterMiddleware, }) : ... enhancer = EnhancerDefault<T>( middleware: middleware, viewMiddleware: viewMiddleware, effectMiddleware: effectMiddleware, adapterMiddleware: adapterMiddleware, ), ...EnhancerDefault对四类中间件分别做了归并处理:
- Store 层:
middleware通过applyMiddleware<T>(_middleware)组合成_storeEnhancer,作用于createStore时的 store 创建过程(对应storeEnhance); - View 层:
viewMiddleware通过mergeViewMiddleware组合,在viewEnhance中包裹 view builder; - Effect 层:
effectMiddleware通过mergeEffectMiddleware组合,在effectEnhance中包裹 effect; - Adapter 层:
adapterMiddleware通过mergeAdapterMiddleware组合,在adapterEnhance中包裹 adapter builder。
从 Enhancer 的抽象定义(basic.dart)可以看到 AOP 的典型形态。以 Effect AOP 为例:
typedef EffectMiddleware<T> = Composable<Effect<dynamic>> Function( AbstractLogic<dynamic>, Store<T>, );即:一个接收logic和store、返回"效果包裹器"的函数。内置的logMiddleware(lib/src/redux_middleware/middleware/log.dart)和safetyView、safetyAdapter(lib/src/redux_middleware 目录)都是基于这一机制实现的现成中间件。
运行时动态增删中间件
除了构造时传入,Enhancer 还提供了两个运行时方法,见 page.dart:
void unshift({...}) { enhancer.unshift(...); } // 插入到最前 void append({...}) { enhancer.append(...); } // 追加到末尾这在需要"全局统一增强所有页面"的场景非常有用。参考示例应用 example/lib/app.dart,createApp中通过PageRoutes的visitor回调遍历每个注册页面,统一追加公共 AOP:
final AbstractRoutes routes = PageRoutes( pages: <String, Page<Object, dynamic>>{ 'todo_list': ToDoListPage(), 'todo_edit': TodoEditPage(), }, visitor: (String path, Page<Object, dynamic> page) { page.enhancer.append( viewMiddleware: <ViewMiddleware<dynamic>>[safetyView<dynamic>()], adapterMiddleware: <AdapterMiddleware<dynamic>>[safetyAdapter<dynamic>()], effectMiddleware: <EffectMiddleware<dynamic>>[_pageAnalyticsMiddleware<dynamic>()], middleware: <Middleware<dynamic>>[ logMiddleware<dynamic>(tag: page.runtimeType.toString()), ], ); }, );这是一个非常实用的模式:页面私有 AOP 写在 Page 构造参数里,应用级公共 AOP 通过 visitor 统一注入。上述代码中_pageAnalyticsMiddleware展示了 Effect AOP 的完整写法——它先判断logic is Page且 Action 类型是Lifecycle,打印页面生命周期,再透传调用原始 effect。示例中还演示了safetyView/safetyAdapter在生产环境对 View 和 Adapter 构建做异常兜底。
initState:页面数据初始化的唯一入口
initState是 Page 区别于 Component 的最核心新增要素。它有两种典型职责:
- 透传路由参数:如 Hello World 示例中
(String msg) => msg; - 根据参数构造复杂 State:见 example/lib/todo_list_page/page.dart 的
ToDoListPage:
class ToDoListPage extends Page<PageState, Map<String, dynamic>> { ToDoListPage() : super( initState: initState, effect: buildEffect(), reducer: buildReducer(), view: buildView, dependencies: Dependencies<PageState>( adapter: const NoneConn<PageState>() + adapter, slots: <String, Dependent<PageState>>{ 'report': ReportConnector() + ReportComponent() }), // middleware: <Middleware<PageState>>[ // logMiddleware(tag: 'ToDoListPage'), // ], ); }这个真实示例同时展示了 Page 在工程中的典型完整配置:initState+effect+reducer+view+dependencies。其中dependencies声明了页面内嵌的 slot 子组件('report')和列表 adapter,二者会以子 reducer 的形式合并进页面的主 reducer(见 dependencies.dart 中createReducer对combineSubReducers的使用)。
对应initState的定义可以在 example/lib/todo_list_page/state.dart 中查看,它负责把路由传入的Map<String, dynamic>(如初始待办数据)转成PageState。
页面 Store 与全局 Store 的连接
Page 拥有私有 Store,但当多个页面需要共享全局数据(如主题色、登录态)时,Fish Redux 提供了connectExtraStore。仍以 example/lib/app.dart 为例:
if (page.isTypeof<GlobalBaseState>()) { page.connectExtraStore<GlobalState>(GlobalStore.store, (Object pagestate, GlobalState appState) { final GlobalBaseState p = pagestate; if (p.themeColor != appState.themeColor) { if (pagestate is Cloneable) { final Object copy = pagestate.clone(); final GlobalBaseState newState = copy; newState.themeColor = appState.themeColor; return newState; } } return pagestate; }); }其底层实现在 batch_store.dart 的connectStores中:当 AppStore 的 State 变化时,订阅回调会把 AppStore 的最新数据映射为 PageStore 的新 State,并派发一个内部 Action(_UpdateState.Assign)替换页面状态:
// replace current state Reducer<T> _appendUpdateStateReducer<T>(Reducer<T> reducer) => (T state, Action action) => action.type == _UpdateState.Assign ? action.payload : reducer == null ? state : reducer(state, action);也就是说,跨页共享数据通过"AppStore 单向驱动 PageStore"实现,PageStore 本身仍然保持独立与唯一,这与"一个页面一个 Store"的架构约束并不冲突。在Page.updateStore中,通过_storeUpdaters.fold把所有StoreUpdater依次作用于 Store,支持一个页面同时连接多个外部 Store。
页面间通信:DispatchBus 与 appBus
Page 还有一个常被忽视但很实用的成员——appBus(应用级事件总线),见 page.dart:
/// AppBus is a event-bus used to communicate between pages. final DispatchBus appBus = sharedBus;sharedBus是全局共享的DispatchBusDefault实例。每个页面在_PageState.didChangeDependencies中把自己的_pageBusattach 到appBus上,形成一棵"页面总线 -> 全局总线"的监听树。当某个页面通过context.broadcast(action)广播 Action 时,消息会沿总线传递给所有已 attach 的页面(见 dispatch_bus.dart 的broadcast实现)。页面销毁时detach摘除注册,避免悬挂引用。
小结与最佳实践
围绕 docs/concept/page.md 的核心内容,结合源码与示例可以总结出以下实践要点:
- 一个页面一个 Store:页面状态天然隔离,Store 随 Widget 创建/销毁,无需手动管理(见 page.dart 的
_PageState); - initState 必填:它是路由参数到页面初始 State 的唯一转换入口,与
view同为必填项; - 复用 Component 全要素:reducer、effect、dependencies、shouldUpdate、wrapper 等与 Component 完全一致,需要子组件时在
dependencies.slots声明; - 四层 AOP 按需配置:构造参数配置页面私有中间件,
enhancer.append/unshift供运行时动态增强;全局公共 AOP 建议通过PageRoutes的visitor统一注入(参考 example/lib/app.dart); - 跨页共享数据用
connectExtraStore:保持"一页一 Store"的同时,让全局数据单向驱动页面数据,而不是合并 Store。
配套的可运行示例位于 example/lib(包含 todo_list、todo_edit 两个页面及全局 Store 的完整演示),页面相关的测试可参考 test/lib/redux_component/page_test.dart 与 test_widgets/lib/page,它们验证了 Page 从构建、生命周期到路由解析的完整行为,适合作为深入理解 Page 机制的下一步阅读材料。
- 前端
【免费下载链接】fish-redux
An assembled flutter application framework.
相关推荐
Fish Redux 的 Page 概念详解:一个页面一个 Store、initState 初始化与 Middleware AOP 配置
Fish Redux 的 Page 概念详解:一个页面一个 Store、initState 初始化与 Middleware AOP 配置 Page 是 Fish
前端WordPress「Time to Read」块深度解析:阅读时长与字数统计的完整实现指南
WordPress「Time to Read」块深度解析:阅读时长与字数统计的完整实现指南 导读 本文基于 Gutenberg(WordPress 块编辑器)仓
前端fish-redux 中的 Redux 概念详解:State、Action、Reducer、Store 与 Middleware 的完整实践指南
fish redux 中的 Redux 概念详解:State、Action、Reducer、Store 与 Middleware 的完整实践指南 fish re
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考