坑!都替你踩完了 —— FlutterWeb开发避坑指南

5,965 阅读6分钟

在前一阵用flutter开发web的过程中,遇到了一些坑,遇到了一些与app端开发不一样的地方,当然也学到了一些新知识点,所以总结一下,一方面帮助他人,另一方面,自己也能加深记忆,要不容易忘。 (年纪大了?记忆力还不行了呢🙃🙃🙃)

一、启动运行乱码

没错,启动一个demo,遇到坑了,如图所示

image

点击Android Studio上方运行按钮,程序启动之后汉字文字显示乱码,这是由于flutter web有三种渲染模式,autohtmlcanvaskit,点击运行按钮(flutter build web命令)默认的渲染模式为auto,这种模式在移动端使用html渲染,在pc端使用canvaskit渲染。

解决办法 1: 用命令行运行,并指定渲染模式,就能解决问题。

// 指定渲染模式为html
flutter build web --web-renderer html

解决办法 2: 上面虽然能解决问题,但我习惯用按钮运行程序怎么办?当然也找到了其他办法。在程序包下web/index.html文件中body标签下copy如下代码。

 <!--指定web运行模式-->
  <!--      window.flutterWebRenderer = "canvaskit";-->
  <script type="text/javascript">
      window.flutterWebRenderer = "html";

  </script>
  <script src="main.dart.js" type="application/javascript"></script>

二、Debug启动运行断点失败

web开发和APP端开发一样,也可以断点。项目之初断点是可以的,但是不知道怎么的,debug可以运行,但断不到,很奇怪,花了一上午,发现同事因为发版改了下web/index.htmlhead->base标签下 href="***"的值。

解决办法 :

// 之前,断点可用
 <base href="$FLUTTER_BASE_HREF">
 
// 同事改动,断点不可用
 <base href="git/******">
 
// 修复后,断点可用
<base href="/">

不能断点开发实在是麻烦。

三、Hot Reload热重载、点击浏览器刷新,都会重启整个程序

在APP端开发时,在某个页面点hot reload按钮,只会重新运行当前页面,但是在web中,点热重载会重启,这只是开发中的不方便。已经上线的程序,用户只要点击浏览器刷新就会重启整个程序,无论在哪个页面,都会回到第一个页面,这与我浏览网页的习惯明显是不符的。

查找原因,发现是flutter底层问题,仔细观察web页面是通过不同的url来确定的,而Flutter从始至终都是一个url,只是flutter在一个网页中绘制了不同的页面(与APP端原理一致),所以想解决问题就是要每个页面都有自己的url。

解决办法 : 用静态路由的方式跳转页面和传参,具体代码如下。

  // 跳转与传参
  static Future toName(String pageName, Map<String, dynamic> params) {
    var uri = Uri(scheme: RoutePath.scheme, host: pageName, queryParameters: params);
    return Navigator.of(currentContext).pushNamed(uri.toString());
  }
  
  // 取参方式
  static Route<dynamic> generateRoute(RouteSettings settings) {
    return PageRouteBuilder(
        settings: settings,
        pageBuilder: (BuildContext c, Animation<double> a,Animation<double> sa) {
          var uri = Uri.parse(settings.name ?? ''); //解析页面名
          switch (uri.host) {
            case RoutePath.name:
              return NamePage(uri.queryParameters); 、、传参
            default:
              return Scaffold(
                body: Center(
                  child: Text('没有找到对应的页面:${settings.name}'),
                ),
              );
          }
        });
  }

通过以上方式,跳转时每个页面都会有自己的url和拼接的参数,这样刷新的时候就不会重启整个程序,会停留在当前页面。

四、用静态路由的方式跳转,全局变量,单例对象丢失,页面栈记录丢失。

没错,坑是连着的,我也是服了。当在某页面热重载或点击浏览器刷新,会停留在当前页面,但是无法返回,就算点击跳转至其他页面,也会报错,因为全局变量都已经丢失,比如:登录信息,用户信息,已经初始化的工具类对象等。

已经有人提了Issues,国内也有大神分析了原因和不完全结局方案

目前flutter web对于浏览器还是没有适配完全,无论Navigator1.0还是Navigator2.0,都存在不可解决的严重问题。目前来看google的对flutter web的意图,还是开发移动web并在App中通过webkit这种内核使用,并没有想开发者使用flutter web来开发真正的web应用,或者后续会完善这部分。

我的解决方案

  1. 所有页面依据用静态路由的方式跳转
  2. 本地维护一个路由历史记录备用栈,当用户刷新后,无法正常回退时启用,实现正常的回退效果(备用栈解决方案)
  3. 全局变量持久化,用html.window.localStoragehtml.window.sessionStorage并配合工厂模式持久化数据,当被触发刷新,会从本地重新赋值,比如:登录信息等。
  4. 弱化全局成员变量,非必要不使用全局类的变量,数据尽量放云端,页面间不耦合。

五、监测浏览器功能的方法

1. 浏览器刷新或关闭监听

可以使用函数onBeforeUnload来检查选项卡是否正在关闭。它也可能检测到页面刷新。

import 'dart:html' as html;
html.window.onBeforeUnload.listen((event) async{
  // do something
});

或者

import 'dart:html' as html;
html.window.onUnload.listen((event) async{
  // do something
});

2. 浏览器的返回键监听

import 'dart:html' as html;
html.window.onPopState.listen((event){
  // do something
});

3. 浏览器页面内的点击监听

import 'dart:html' as html;
html.window.onClick.listen((event){
  // do something
});

4. 浏览器大小调整监听

import 'dart:html' as html;
html.window.onResize.listen((event){
  // do something
});

5. 浏览器获取焦点监听

import 'dart:html' as html;
html.window.onFocus.listen((event){
  // do something
});

6. 浏览器获取焦点时,键盘按下、抬起监听

import 'dart:html' as html;
html.window.onKeyDown(onKeyUp).listen((event){
  // do something
});

六、引用 import 'dart:html' 运行提示报错

多端运行,如果引用了html会提示报错。

解决办法 : 可以引用第三方universal_html 2.0.8,帮封装了一层,支持多端。

universal_html :适用于所有平台的“dart:html”,包括 Flutter 和服务器端。简化跨平台开发和 HTML / XML 处理。

七、可点击提示

在平常浏览网页时,鼠标滑动到可点击的文字或按钮上,鼠标“箭头”会变成一个“小手”,或背景出现颜色变化提示。 Flutter中常用的GestureDetector()手势工具,虽然可以实现点击等回调,但是鼠标滑动到可点击区域,鼠标“箭头”并不会变成“小手”,在交互上不符合大众使用网页的习惯。

解决办法 : 使用InkWell替换GestureDetector,用InkWell包住的按钮或文字,鼠标悬停,就会出现小手。

Ink(
        width: width,
        height: height,
        color: color,
        child: InkWell(
            focusColor: Colors.transparent,
            highlightColor: Colors.transparent,
            splashColor: Colors.transparent,
            hoverColor: const Color(0x0818a7fb),
            onTap: onTap,
            child: Center(child: this)))

ezgif.comvideotogif.gif 分析源码可知,内部用MouseRegion监听了鼠标位置,那什么是MouseRegion呢?

八、鼠标监听控件

MouseRegion 相对于APP端,web端多了个鼠标,可以实现app实现不了的交互效果,比如悬停,划过,进入退出某区域等,都可以用MouseRegion实现。

n_v29940d762539d4d1788cd31924e19fcaf.gif

MouseRegion的属性和说明

字段属性Col3
onEnterPointerEnterEventListener鼠标进入区域时的回调
onExitPointerHoverEventListener鼠标退出区域时的回调
onHoverPointerExitEventListener鼠标在区域内移动时的回调
cursorMouseCursor鼠标悬停区域时的光标样式
opaquebool是否阻止检测鼠标
childWidget子组件

九、本地存储

1. localStorage

web也会涉及到本地存储,本地持久化那肯定想到的就是shared_preferences: 2.0.13,目前该插件已经支持所有平台,满足日常开发。
打开实现web端的源码,里面使用html.window.localStorage进行存储,以这种方式即使在浏览器关闭并重新打开时仍然存在
存储没有过期日期的数据,并且只能通过 JavaScript 或清除浏览器缓存/本地存储的数据来清除,但有些场景的数据仅需要关闭选项卡或浏览器后就不需要了,用localStorage就不太合适,还需要手动删除。

2. sessionStorage

那谁合适呢? sessionStorage
html.window.sessionStorage会为已打开的页面维护一个单独的存储区域(只要浏览器打开,包括页面重新加载和恢复)
仅为已打开的页面存储数据,这意味着数据将一直存储到浏览器(或选项卡)关闭为止。

3. 小结

多端使用兼容web,那shared_preferences再合适不过了,比如存token、版本记录、日志信息等。
Flutter运行在浏览器上时,浏览器刷新后,全局的静态变量会初始化,所以刷新前就需要本地化储存,用localStorage存储太重,也会影响下次程序运行数据的准确性,使用sessionStorage就比localStorage更合适,关闭浏览器或选项卡就会自动清理。

最后

这是目前遇到有价值的坑,后面遇到新的也会持续更新。

Flutter开发web上,不是特别复杂的业务逻辑,展示没啥问题;路由和全局变量上的坑还是挺严重的,希望官方后续给出更好的解决方案。

点赞、收藏mark一下✨✨万一用到时找不到了呢🌚