【问题标题】:Where does Firebase "getApps().length" check come from?Firebase \"getApps().length\" 检查从何而来?
【发布时间】:2023-01-31 07:22:26
【问题描述】:

我在 Firebase 方面不是很有经验。最近我一直在用 Next.js + Firebase 做一些事情,偶然发现了这行必要的代码:

const app = !getApps().length ? initializeApp(config) : getApp()

据我了解,这可以防止创建具有相同配置的多个 Firebase 应用程序。但是,首先,这是从哪里来的?其次,getApps() 函数如何知道所有其他非默认应用程序?它返回的是mutable还是read-only?那个 getApp() 函数(最后没有“s”)怎么知道哪个应用程序是我默认返回它的,我没有传递任何东西给它......

我在 Firebase 文档或他们的主要演讲者如 David East、Todd Kerpelman、Frank van Puffelen 中找不到任何相关信息。我知道,Firebase 文档确实是这个星球上最糟糕的,而马里奥游戏要好得多,但即便如此......

帮助 :)

【问题讨论】:

    标签: firebase


    【解决方案1】:

    Firebase SDK 中有这样的内容:

    const FirebaseApp: FirebaseApp[]
    
    export function initializeApp(options: FirebaseOptions, name?: string | undefined) {
        return !name ? FirebaseApp = [...FirebaseApp, new FirebaseApp(options, 'default')] : FirebaseApp = [...FirebaseApp, new FirebaseApp(options, name)]
      }
    
    export function getApps() {
      return FirebaseApp
    }
    
    export function getApp(name?: string) {
      return !name ? FirebaseApp.filter(n => n.name === 'default') : FirebaseApp.filter(n => n.name === name)
    }
    

    Firebase JS SDK 是用 TypeScript 编写的。

    在您的代码中,您不需要 const app = ... 就可以滥用所有 Firebase 函数。函数 getFirestore() 将为您提供一个您需要处理的 Firebase 实例,与 getApp() 相同。你可以 enableIndexedDbPersistence(getFirebase()) 这样你就可以在客户端浏览器中本地缓存数据,而不需要任何 Redux/Pinia/Vuex 解决方案。如果您使用例如 onSnapshot() 侦听器,这将减少数据库查询。或者结合使用getDocFromCache()getDoc()

    编辑:

    好吧,如果您忘记了 OOP 的工作原理并开始思考功能/结构编程,那么它是如何工作的就开始显而易见了。你的应用程序是封装的,但你有一个“getters”“setters”来处理它。比 OOP 更好更容易理解。没有任何设计模式需要学习。而且这个库可以很容易地被编译器/打包器摇树,所以它是轻量级的。

    【讨论】:

    • 非常感谢您的解释。所以,事实上,文档不包括这个?需要挖SDK源码?
    • 好吧,如果您忘记了 OOP 的工作原理并开始思考功能/结构编程,那么它是如何工作的就开始显而易见了。你的应用程序是封装的,但你有一个“getters”“setters”来处理它。比 OOP 更好更容易理解。没有任何设计模式需要学习。而且这个库可以很容易地被编译器/打包器摇树,所以它是轻量级的。
    【解决方案2】:

    answer by @Mises 的基础上,我可以提供一些额外的上下文。

    作为帮助开发人员避免错误和竞争条件的内置保护的一部分,initializeApp() 将在为同一应用程序名称调用两次时抛出错误(在不提供名称的情况下使用 "[DEFAULT]" 代替)。它也是以这种方式设计的,因为它更容易抛出错误,而不是将传递到每个 initializeApp() 调用的配置对象与前一个调用进行比较。由于这种行为,initializeApp() 应该在您的应用程序中只被调用一次,要么在当前文件的顶部,要么在某些中心依赖项中(例如 app.js)。然后当你需要它时,你可以使用getApp()getFirestore()等将它带入当前文件。

    getApp()getApps() 函数是 Firebase SDK 功能的一部分,您可以在一个应用程序中使用多个项目。这个功能的使用是documented here


    加载 Firebase 依赖项

    对于某些开发人员来说,Firebase 是相当严重的依赖项(尤其是对于遗留的 JavaScript SDK)。所以他们不想不必要地加载它是可以理解的。这对于交互时间很重要的基于 Web 的应用程序或尝试优化 Cloud Functions for Firebase 的冷启动时间以获得最佳响应时间时尤为重要。

    optimizing cold-start times@doug-stevenson 的这个旧视频中,Doug 介绍了如何使用布尔标志来指示 Firebase Admin SDK 是否已初始化。这允许不使用 Admin SDK 的函数跳过加载它并更快地返回结果。

    // note: legacy syntax being used for historical purposes
    const functions = require("firebase-functions");
    
    let is_f1_initialized = false;
    
    // a HTTPS Request function that uses the Admin SDK
    exports.f1 =
    functions.https.onRequest((req, res) => {
      const admin = require("firebase-admin");
      if (!is_f1_initialized) {
        admin.initializeApp();
        is_f1_initialized = true;
      }
      
      // does stuff, using admin SDK
    });
    
    // a HTTPS Request function that doesn't use the Admin SDK
    exports.f2 =
    functions.https.onRequest((req, res) => {
      // does stuff
    });
    

    一些开发人员不喜欢在他们的全局范围内乱放这样的标志,因此他们寻找一个即时的替代方案。这采取了检查遗留 JavaScript SDK 中 firebase.apps 和 Admin SDK 中 admin.apps 长度的形式。

    // note: this code block uses the legacy "firebase-admin" library syntax
    import * as admin from "firebase-admin";
    
    console.log(admin.apps.length); // logs '0'
    admin.initializeApp();
    console.log(admin.apps.length); // logs '1'
    

    同样的方法也适用于客户端 JavaScript SDK:

    // note: this code block uses the legacy "firebase" library syntax
    import * as firebase from "firebase";
    
    console.log(firebase.apps.length); // logs '0'
    firebase.initializeApp(config);
    console.log(firebase.apps.length); // logs '1'
    

    对于单项目应用程序,这很快成为检查默认应用程序是否已初始化的事实上的标准,导致以下行随处可见(尤其是在使用每个文件一个组件的框架时):

    // note: historical legacy "firebase" library syntax used on purpose
    const app = firebase.apps.length ? firebase.app() : firebase.initializeApp(config);
    // or for those against implied type coercion to Booleans:
    // const app = !firebase.apps.length ? firebase.initializeApp(config) : firebase.app();
    const db = firebase.firestore(app);
    

    或者

    // note: historical legacy "firebase" library syntax used on purpose
    if (!firebase.apps.length) {
      firebase.initializeApp(config);
    }
    const db = firebase.firestore();
    

    摘要/TL:DR;

    随着 "firebase""firebase-admin" 转向模块化 Firebase JavaScript SDK,使用遗留代码的开发人员和新手正在按照 modular SDK migration guide 对其进行更新。

    这导致以下遗留代码:

    // note: historical legacy "firebase" library syntax used on purpose
    const app = !firebase.apps.length ? firebase.initializeApp(config) : firebase.app();
    

    被一对一地翻译成这个现代代码:

    const app = !getApps().length ? initializeApp(config) : getApp();
    

    此行的主要目的是获取正确初始化的 FirebaseApp 类实例而不引发错误,您可以将其传递给 SDK 中包含的 Firebase 服务的入口点函数,例如 Analytics 和 Cloud Firestore。


    一窥幕后花絮

    查看如何默认的应用程序实例在SDK中的服务之间进行手球,你可以看看源代码。 FirebaseApp相关函数的实现类似于下面的代码。

    笔记:我省略了一些验证并重命名了一些变量以保持简洁,您应该查看full source 或查看API reference 了解详细信息。

    const _apps = new Map<string, FirebaseApp>();
    const DEFAULT_ENTRY_NAME = "[DEFAULT]";
    
    // initializes the given app, throwing an error when already initialized
    export function initializeApp(options: FirebaseOptions, name?: string | undefined): FirebaseApp {
      name = name || DEFAULT_ENTRY_NAME;
      if (_apps.has(name)) throw new Error("already initialized");
      const app = new FirebaseApp(options, name)
      _apps.set(name, app);
      return app;
    }
    
    // returns a read-only array of initialized apps, doesn't throw errors
    export function getApps(): FirebaseApp[] {
      return Array.from(_apps.values())
    }
    
    // gets the named/default app, throwing an error if not initialized
    export function getApp(name: string = DEFAULT_ENTRY_NAME): FirebaseApp {
      const app = _apps.get(name);
      if (!app && name === DEFAULT_ENTRY_NAME) return initializeApp();
      if (!app) throw new Error(name + " not initialized");
      return app;
    }
    
    // marks the given app unusable and frees its resources
    export async function deleteApp(app: FirebaseApp): Promise<void> {
      const name = app.name;
      if (!_apps.has(name)) return; // already deleted/started deletion?
      _apps.delete(name);
      await Promise.all(
        Object.values(app._providers)
          .map(provider => provider.release())
      )
      app.isDeleted = true;
    }
    

    SDK 中可用的每个服务都有一个入口点函数。在旧的命名空间 SDK 中,它采用 firebase.firestore() 的形式,而现代模块化 SDK 使用 getFirestore() 代替。这些入口点函数中的每一个都遵循类似的策略,并且看起来类似于下面的代码。

    笔记:和以前一样,这是一个简化版本。有关详细信息,请参阅完整的sourceAPI reference

    export function getFirestore(app?: FirebaseApp) {
        app = app || getApp(); // use given app or use default
        return app._providers.get('firestore') || initializeFirestore(app, DEFAULT_SETTINGS)
    }
    

    【讨论】:

    • 此外,不要指望 StackOverflow 上会出现这些长篇大论的答案,我喜欢像这样的问题背后的琐事,并且很乐意为未来的新手记录下来。这就是为什么每次只问一件事的问题很重要,所以在以后的问题中记住这一点。
    • 不幸的是,我不能多次赞成你的回答。请接受一个巨大的感谢)
    猜你喜欢
    • 2022-01-14
    • 1970-01-01
    • 2020-06-23
    • 2017-11-28
    • 1970-01-01
    • 2016-07-23
    • 2023-01-27
    • 2020-08-01
    • 2018-03-07
    相关资源
    最近更新 更多