firebase_analytics Flutter Web 应用程序在未加载/被广告拦截器拦截时崩溃

firebase_analytics crashes Flutter Web app when not loaded / blocked by ad blocker

当用户启用了阻止 firebase-analytics.js 的广告拦截器时,我的 Flutter Web 应用程序崩溃了。我只剩下一个空白页。

这是我收到的错误:

top_level.dart.lib.js:110 Uncaught (in promise) TypeError: dart.global.firebase.analytics is not a function
    at Object.analytics$ [as analytics] (top_level.dart.lib.js:110)
    at new firebase_analytics_web.FirebaseAnalyticsWeb.new (:7357/packages/firebase_analytics_web/firebase_analytics_web.dart.lib.js:56)
    at Function.registerWith (:7357/packages/firebase_analytics_web/firebase_analytics_web.dart.lib.js:19)
    at Object.registerPlugins (:7357/packages/triage/generated_plugin_registrant.dart.lib.js:13)
    at main (:7357/web_entrypoint.dart.lib.js:29)

我正在使用 firebase_analytics plugin 的以下版本:

firebase_analytics: ^7.1.1

问题

这是因为 firebase package (which is used by firebase_analytics_web) relies on the firebase.analytics() function existing in JS. See the related piece of code.
因此,当广告拦截器阻止加载 firebase-analytics.js 时,这段代码将抛出异常,从而导致整个 Flutter Web 初始化崩溃。

解决方案

我们可以创建一个 mock of firebase.analytics() 来解决这个问题。注意在firebase_analytics_webthe firebase.analytics() function is only called once中再存储一个实例。这就是为什么我们的模拟必须确保以某种方式再次调用 JS 中的 firebase.analytics() 如果我们希望能够在稍后的时间点加载 firebase-analytics.js 库。

这是一个脚本,您可以在加载至少 firebase-app.js 后将其插入 <body>。例如,如果您遵守 GDPR 并且 异步 加载 Firebase 分析(比如在用户通过 Google 标签管理器同意后),这将很有用。无论如何,这是一个完全解决问题的模拟 并且 允许稍后加载 Firebase Analytics:

<!--
  We need to create a mock for Firebase Analytics that ensures that it *does not matter **when***
  the JS library is loaded. This is because Google Tag Manager will load firebase-analytics.js
  and this 1. happens asynchronously and 2. only after the user consented.
  The firebase.dart Dart library will crash if the firebase.analytics object does not exist,
  which is why this is absolutely crucial for starting the app.
  
-->
<script>
  // This mock ensures that if the firebase_analytics Flutter plugin uses this mock as its
  // instance (which does not change over time), the plugin will *still* be able to reach out
  // to the actual firebase.analytics() instance because the object will be overridden once the
  // firebase-analytics.js library is loaded in.
  firebase.analytics = function () {
    return {
      mock: true,
      app: function () {
        var instance = firebase.analytics()
        // Prevent infinite recursion if the real instance has not yet been loaded.
        if (instance.mock === true) return
        return instance.app
      },
      logEvent: function () {
        var instance = firebase.analytics()
        if (instance.mock === true) return
        return instance.logEvent(...arguments)
      },
      setAnalyticsCollectionEnabled: function () {
        var instance = firebase.analytics()
        if (instance.mock === true) return
        return instance.setAnalyticsCollectionEnabled(...arguments)
      },
      setCurrentScreen: function () {
        var instance = firebase.analytics()
        if (instance.mock === true) return
        return instance.setCurrentScreen(...arguments)
      },
      setUserId: function () {
        var instance = firebase.analytics()
        if (instance.mock === true) return
        return instance.setUserId(...arguments)
      },
      setUserProperties: function () {
        var instance = firebase.analytics()
        if (instance.mock === true) return
        return instance.setUserProperties(...arguments)
      },
    }
  }
</script>