likes
comments
collection
share

我把FlutterWeb渲染模式改成Canvaskit后...

作者站长头像
站长
· 阅读数 20

背景

用FLutterWeb开发的网站在使用过程中出现了一些问题,比如在Google浏览器中使用交互、动画流畅,在360浏览器中就卡顿;图标在代码中动态设置颜色的方式在Google浏览器中正常显示,在Safari浏览器中颜色缺失,变为黑色;在有的电脑中Google浏览器也有动画、交互卡顿的现象、页面报错等。很奇怪,一脑袋问号。

优化方案

这些问题的原因是,渲染模式为html导致的,将渲染模式由html改为canvaskit,之前遇到的问题基本就解决了,动画也不卡了,画面也流畅了,图标也正常了,兼容性也提高了,再也不用担心在老板的电脑上卡住了。

渲染模式

简单说说两种模式的区别。 html渲染模式:flutter会采用HTML的custom element,CSS,CanvasSVG来渲染UI元素。 canvaskit渲染模式:flutter将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染。

htmlcanvaskit
命令行--web-renderer html--web-renderer canvaskit
优点体积更小渲染性能强;多端一致
缺点渲染性能差;跨端兼容差体积相较html多2.5M

所以使用canvaskit会更加流畅,更符合FLutter的气质。但是!也出现了些新的问题。

由Canvaskit引起的问题

图片跨域

报错描述: Access to XMLHttpRequest at 'https://.../icon/setting_228.webp' from origin 'https://...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

在html模式下是正常的,而在canvaskit出现了跨域问题,查看日志发现区别。

html的网络请求类型type就是图片本身,不会出现问题。 我把FlutterWeb渲染模式改成Canvaskit后...

不会出现跨域的问题原因是在HTML中,有一些标签也可以发起HTTP请求,比如script标签,link标签,img标签,form标签,且被允许跨域。

  1. link,img标签都是单纯的引入资源文件
  2. form标签用于收集用户输入并发送,但是发送成功会跳转到新网页,并将服务器响应作为新网页的内容
  3. script标签可以引入外部js文件,并执行引入的js文件的代码

其中,script标签由于其可以执行引入的js文件的代码,再加上其跨域特性,让script标签可以用来做一些超出其设计初衷的事。script标签会发起HTTP GET去请求服务器上的js文件,所以script标签可以用于实现HTTP GET跨域请求。

而canvaskit模式下,请求类型是xhr,不支持跨域。而我的图片地址和服务地址并不在一个域名,所以出现该问题。 我把FlutterWeb渲染模式改成Canvaskit后...

原因是同源策略,它是浏览器特有的一种安全机制,主要用于限制不同的源之间的数据交互。

那如何解决呢?

询问前端大佬后,发现解决问题最快的方法就是放到自身服务的域名下。随后我把图片放在项目中的asset目录中,更改本地引用地址,打包上传部署,解决!

(PS:这个问题在本地debug模式下,并不会出现)

首次打开加载慢

在首次改成canvaskit模式部署后,打开网站,页面一度白屏很长时间,预计有10秒,查看后台日志发现是下载了很多文件,包括canvaskit绘制引擎、字体等。主要耗时是在引擎(约9M)、字体下载,而下载这些的域名都是官方的,所以下载速度也有所限制。 我把FlutterWeb渲染模式改成Canvaskit后... 解决办法: 将引擎和字体传值自己的服务器,以加快下载速度。

  1. 引擎本地化,查看网络请求详情,可以看到下载地址,单独下载后放到项目中。 我把FlutterWeb渲染模式改成Canvaskit后... 我的位置是web/assets/canvaskit/canvaskit.js&wasm我把FlutterWeb渲染模式改成Canvaskit后... 再设置替换引擎路径,在运行或打包的时候加上以下命令行。等号后面为本地的路径。

    --dart-define=FLUTTER_WEB_CANVASKIT_URL=assets/canvaskit/
    
  2. 本地化加载KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf字体文件,同样在请求详情中获取地址,下载至本地,放在本地,web/assets/canvaskit/

    我把FlutterWeb渲染模式改成Canvaskit后...

    替换本地地址,在构建完成后的build目录下的main.dart.js中搜索该字体名,把前缀替换成本地路径。

    https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf
    替换成
    assets/canvaskit/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf
    

    此外在评论中有建议在本地设置字体包的方式,这样不用替换也可以避免用光放域名下载,更方便。如图所示:

    我把FlutterWeb渲染模式改成Canvaskit后... 原因是因为Flutter会把Roboto字体设置为兜底的备用字体,如果不设置它,它永远会去font.gstatic.com上fetch这个字体。

字体需下载

在打开页面时,会出现字体乱码,原因是Skia 自绘引擎需要字体库支持,它正在下载字体,而且引用的字体不一样下载的库也是不同的。同样也可以下载至本地,替换main.dart.js的地址,但下载完体验后,发现不管是第一次还是之后都会出现乱码,只是显示的时间长短,体验也是不很好。 我把FlutterWeb渲染模式改成Canvaskit后...

我把FlutterWeb渲染模式改成Canvaskit后... 所以我是在pubspec.yaml中设置了本地的字体包的方式解决的,这样在首次加载或后面的刷新,都未出现过乱码。

我把FlutterWeb渲染模式改成Canvaskit后...

main.dart.js 切片化

以上两步只是加快了下载速度,但所需要下载的内容大小没变,好在Flutter 官方提供 deferred as 关键字来实现 Widget 的懒加载,而 dart2js 在编译过程中可以将懒加载的 Widget 进行按需打包,这样的拆包机制叫做 Lazy Loading。借助 Lazy Loading,我们可以在路由表中使用 deferred 引入各个路由(页面),以此来达到业务代码拆离的目的。具体的代码请看这篇文章《JS 分片优化》,很详细。

我经过拆分后,main.dart.js由8.5M缩减至5.5M。

加载时提示

经过上面三步设置,首次加载时长会有大大缩减,但是也会有白屏,为了更好的体验在白屏时加个提示。

// 在 web/index.html 中的 head 标签下加提示
<style>
    body {
          width: 100vw;
          height: 100vh;
          display: flex;
          justify-content: center;
          align-items: center;
      }
</style>

// 在 web/index.html 中的 body 标签下加提示
<div id="text">静态资源加载中...</div>

浏览器刷新后页面加载两次

在使用网站时刷新会出现页面加载两次的问题,查看日志发现是web/index.html中的一段代码引起的。

// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
  if (!scriptLoaded) {
    console.warn(
      'Failed to load app from service worker. Falling back to plain <script> tag.',
    );
    loadMainDartJs();
  }
}, 4000);

引起超时的原因是navigator.serviceWorker.register(serviceWorkerUrl)注册失败,而上面的代码是兜底的逻辑。serviceWorker是服务器与浏览器之间的代理,目前用不上,所以将注册逻辑注释掉,直接调用loadMainDartJs()即可。

路由包装url地址方式失效

在canvaskit模式下,刷新后不会停留在当前页面了。之前写过一篇文章《FlutterWeb浏览器刷新后无法回退的解决方案》中的方案看来只适应在html模式下。

解决办法: 在上面的的文章基础上稍微修改下。

 // 刷新时回调
    _beforeUnload = (event) {
      // 本地记录,标记成"已刷新"
      DB(DBKey.isRefresh).value = true;

      // 记录刷新时的页面,用于还原(本次新增的方法)
      List history = get();
      DB(DBKey.initRoute).value = history.last;
      history.removeLast();
      set(history);

      // 移除刷新前的实例的监听
      html.window.removeEventListener('beforeunload', _beforeUnload);
      html.window.removeEventListener('popstate', _popState);
    };
    
// 获取上次最后的页面,(本次新增的方法)
  static String initRoute(currentContext) {
    return DB(DBKey.initRoute).get(Uri(scheme: RoutePath.scheme, host: RoutePath.home).toString());
  }

// 初始化
  MaterialApp(
      .....
      initialRoute: RouterHistory.initRoute(context),//(本次新增的方法)
      .....
      ))

这样设置完后也会停留在当前页面了。

最后

Canvaskit模式虽然动画、交互体验好,但相较于Html模式,Canvaskit的加载速度是一个劣势,因为我做的是内部使用的网站,加载速度和交互体验之间我选择了后者。

如果有遇到其他问题或更好的解决办法欢迎提出讨论

推荐收藏

  1. Flutter Web 在《一起漫部》的性能优化探索与实践

参考

  1. Flutter web内网网站如何发布?解决外网下canvaskit.js和字体无法加载问题
  2. serviceWorker 服务器与浏览器之间的代理
  3. Flutter 开启web构建以及web的两种渲染模式