问题描述
在 Flutter 开发的网页应用中,当用户刷新浏览器时,虽然页面会显示刷新前的内容(前提是使用了静态路由跳转),但此时调用 方法无法回退到上一页。同样,点击浏览器的回退按钮也是无效的,地址栏中的 URL 会变化,但页面内容不会随之改变。
Flutter Web 应用在浏览器刷新后,由于引擎重启导致页面栈丢失,使得 Navigator.pop 和浏览器回退按钮失效。本文提出通过 localStorage 维护备用路由栈,结合 beforeunload 和 popstate 事件监听来解决此问题。方案要求使用静态路由跳转,需注意回调失效及临时数据丢失等局限性。同时对比了 Navigator 2.0 的处理机制差异,提供了完整的代码实现与注意事项。

在 Flutter 开发的网页应用中,当用户刷新浏览器时,虽然页面会显示刷新前的内容(前提是使用了静态路由跳转),但此时调用 方法无法回退到上一页。同样,点击浏览器的回退按钮也是无效的,地址栏中的 URL 会变化,但页面内容不会随之改变。
Navigator.pop当浏览器执行刷新操作时,Flutter 引擎会重新启动并加载当前页面。这意味着刷新后的 Flutter 内存中所有静态变量都被重新初始化,页面栈内之前的页面记录都未保留,只保留了当前的页面状态。这类似于浏览网页时,将其中一页的网址复制出来,在新的标签页再次打开,原有的导航历史并未被继承。
明确问题根源后,可针对性解决。既然页面栈记录丢失,便需要在代码中自行维护一套备用栈。具体做法是监听页面路由变化,每次进入新页面时记录当前页面的 URL;退出时删除记录的 URL。当浏览器刷新导致栈记录失效时,利用这套备用栈帮助应用回退到上一页。
优点:
Navigator.pop 方法或点击浏览器回退按钮均支持。缺点:
Navigator.pushName().then 的回调无法生效,因为刷新后生成的是新的上一页实例,并不会自动调用原回调。Uri 包裹,不能使用构造函数传参。localStorage 是 HTML5 提供的存储对象,位于 window 下,以 key-value 形式进行持久化存储。
// 导包
import 'dart:html' as html;
// 使用方式示例
html.window.localStorage['key'] = 'value';
在实际项目中,建议对存储工具进行封装,根据业务需求定义 Key 和 Value 的结构,方便统一调用和管理。
这是一个核心工具类,主要作用是注册事件监听、添加和删除历史记录。
/// DB() 为封装好的本地数据库访问层
class RouterHistory {
/// 监听浏览器刷新前的回调
static Function(html.Event event)? _beforeUnload;
/// 监听浏览器回退时的回调
static Function(html.Event event)? _popState;
/// 目前页面是否被刷新过
static bool isRefresh = false;
/// 初始化与注册监听
static void register() {
// 刷新时回调:标记本地存储为已刷新
_beforeUnload = (event) {
DB(DBKey.isRefresh).value = true;
// 移除刷新前的实例的监听,防止重复触发
html.window.removeEventListener('beforeunload', _beforeUnload);
html.window.removeEventListener('popstate', _popState);
};
// 浏览器回退按钮回调
_popState = (event) {
// 页面被刷新,触发备用回调逻辑
if (isRefresh) {
_back(R.currentContext); // R.currentContext 为当前页面的 Context
}
};
// 添加监听器
html.window.addEventListener('beforeunload', _beforeUnload);
html.window.addEventListener('popstate', _popState);
// 从本地数据库中取出"刷新"标记
isRefresh = DB(DBKey.isRefresh).get(false);
// 如果未被刷新,清除上次备用栈中的历史记录,避免脏数据
if (!isRefresh) {
clean();
}
// 还原本地库中的刷新标记,准备下一次生命周期
DB(DBKey.isRefresh).value = false;
}
// 检查是否能正常返回
static bool checkBack(dynamic currentContext) {
// 如果能正常 pop,则直接返回 true
if (Navigator.canPop(currentContext)) {
return true;
}
// 不能则启用备用栈逻辑
_back(currentContext);
return false;
}
// 执行回退逻辑
static void _back(dynamic currentContext) {
List history = get();
if (history.length > 1) {
history.removeLast();
set(history);
// 跳转至上一页并关闭当前页
Navigator.of(currentContext).popAndPushNamed(history.last);
}
}
// 添加记录
static void add(String? path) {
if (path == null) return;
List history = get();
if (history.contains(path)) return;
history.add(path);
set(history);
}
// 删除记录
static void remove(String? path) {
if (path == null) return;
List history = get();
history.remove(path);
set(history);
}
// 设置备用栈数据
static String set(List<dynamic> history) => DB(DBKey.history).value = json.encode(history);
// 取出备用栈数据
static List<dynamic> get() => json.decode(DB(DBKey.history).get('[]'));
// 清除备用栈
static void clean() => DB(DBKey.history).value = '[]';
}
自定义一个类并实现 NavigatorObserver,并将该实现类放在 MaterialApp 的 navigatorObservers 参数中。
// 实现类
class HistoryObs extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
// 存路由信息
RouterHistory.add(route.settings.name);
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
// 删路由信息
RouterHistory.remove(route.settings.name);
}
}
// 设置监听
MaterialApp(
// ... 其他配置
navigatorObservers: [HistoryObs()],
// ... 其他配置
)
为了保证参数和路径都能在 URL 中解析,从而实现回退效果,跳转方法必须为静态路由。
/// 替换 Navigator.pop,增加回退检测
static Future<void> pop(BuildContext context) async {
// 检测是否能正常返回,不能则返回 FALSE
if (RouterHistory.checkBack(context)) {
Navigator.pop(context);
}
}
/// 静态路由跳转
static Future toName(String pageName, {Map<String, dynamic>? params}) {
// 封装路径以及参数
var uri = Uri(scheme: RoutePath.scheme, host: pageName, queryParameters: params ?? {});
return Navigator.of(context).pushNamed(uri.toString());
}
将 RouterHistory.register() 放在 MaterialApp 外层的 build 中,或页面的 initState 中即可确保监听器正确注册。
@override
void initState() {
super.initState();
RouterHistory.register();
}
@override
Widget build(BuildContext context) {
// 或者 RouterHistory.register();
return MaterialApp(
navigatorObservers: [MiddleWare()],
// ... 其他配置
);
}
DB 类需要开发者自行封装,建议使用 flutter_local_storage 或原生 dart:html 的 localStorage 接口进行持久化存储,确保 Key 不冲突。Uri 编码传递,避免使用复杂的对象引用。setNewRoutePath 从浏览器 history 获取 URL 重新加载,解决了刷新问题,但也带来了与传统 Navigator 1.0 不同的行为模式。开发者需根据项目复杂度权衡选择。本方案通过手动维护路由栈并结合浏览器原生事件,有效解决了 Flutter Web 刷新后回退失效的问题。虽然存在临时数据丢失等局限性,但在大多数静态路由场景下是一个稳定可靠的替代方案。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online