Flutter Web - 一种取巧的 CDN 方案

1,594 阅读3分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章。

关联文章

《Flutter Web - 让 Web 与 App UI 一致的另一种可能》

《Flutter Web - 优雅的兼容 Flutter App 代码》

《Flutter Web - ts_to_dart_facade 工具链及示例(附 git)》

背景

用上文的方式,落地稿定 WAP 版的过程中,遇到了一个严重的卡点:

如何将 Flutter build web 的资源 CDN 化,也是笔者以前接触比较少的(笔者以前 Web 开发经验更多是管理后台以及离线包,很少需要接触到部署)。

为什么是卡点?在于 Flutter 默认仅支持相对域名的资源加载方式,无法使用当前域名以外的 CDN 域名,导致无法享受 CDN 带来的优势。

原以为 Flutter 官方有现成的方案,翻了一大圈,只能证明自己想的太美 ...

方案探寻

不过,在美团技术团队发表的 FlutterWeb性能优化探索与实践 中找到了部分解决方式:

  • 对于图片相关的资源在 index.html 上增加 meta,可以解决 assets 资源路径是相对路径的问题问题。
<meta name="assetBase" content="你的 CDN 路径" />
  • 上述 meta 对于加载的 JS 文件不适用(Flutter 官方快支持)。 main.dart.js 特别是用了延迟加载 deferred-components 会生成多个 main.dart.js_XX.part.js 多个 JS 的情况下,怎么配置 CDN 域名就成了一个大难题。

美团技术团队也输出了一种方案:

通过对 js_helper.dart 的动态编译,读取 src 属性修改为读取 assetBase 来实现 xxx.part.js 文件的 CDN 加载

笔者看了下 js_helper.dart 代码

image.png

Emmm ... 3000 多行代码,而且还要准备 hook dart 的工具,或者自行编译 Flutter Engine,并不是一个短期能实现的一种方式。

那解决思路是 hook 来改变 <script src='xxx'>,那是不是直接从 JS 代码 hook 就行了,毕竟 JS 这运行时可有无限可(bug)能(bug),改造也更简单。

失败的第一版

通过观察 flutter.js 文件以及研究 main.dart.js,发现其实也都是动态添加 element 的方式添加 script 的,那我们直接 hook createElement 方法是不是就可以拿到 script 的创建对象,然后再去增加 CDN 的域名即可。

说做就做:

var _createElement = document.createElement.bind(document)
document.createElement = function (tagName: string) {
  var element = _createElement(tagName)

  if (tagName.toLowerCase() == 'script') {
    Object.defineProperty(element, 'src', {
      set: function (val) {
        this.sourceSrc = val
        return val
      },
      get: function () {
        return this.sourceSrc
      },
    })
  }
  return element
}

通过重写 script src 的 get set 方法,看起来是可行的,set 方法也是会成功拦截。但 get 方法根本不会执行,这可能是 script src 是特有属性,有固定的底层逻辑,不能被重写(没找到相关说明,有了解的同学评论一下)。

所以这并不可行,需要想其他的 hook 方式。

成功的第二版

直接 hook src 不可取的话,我们就要从它的代码上下游做手脚才行。

flutter.js

image.png

main.dart.js

image.png

通过 build 后的代码观察到,是通过 main.dart.js 的加载是通过 body.append, main.dart.js_XX.part.js 的动态加载是通过 body.appendChild

那我们直接 hook 这两个方法是不是可以呢?

代码也很简单:

/**
 * hook appendChild
 */
let appendChild = document.body.appendChild
document.body.appendChild = function (el) {
  return appendChild.call(document.body, convertCDNScript(el))
}

/**
 * hook append
 */
let append = document.body.append
document.body.append = function (el) {
  append.call(document.body, convertCDNScript(el))
}

/**
 * 转换成带有 CDN 域名的 Script
 */
function convertCDNScript(el) {
  const cdnURL = import.meta.env.VITE_GAODING_CDN
  if (cdnURL !== '/' && el.nodeName.toLowerCase() === 'script' && el.baseURI !== cdnURL) {
    el.src = el.src.replace(el.baseURI, cdnURL)
  }
  return el
}

大功完成, Enjoy ~

后续

用 hook 的方式毕竟不是什么长(cou)久(he)之(de)计(yong),还是期待 Flutter 官方提供一个 API 或者环境变量设置。

本篇就只是抛砖引玉,期待能有同学放出来更优雅的实现方式。

说声抱歉, 还没有整理前面几篇文章的 codegen 源码,想要的同学可能还要耐心等下了 ... 最近工作太忙,这篇也就是随手记一下刚好遇到的 CDN 问题。

如果对你开发学习有点用处,请点个赞,谢谢。[开心]