Firebase Firestore 模拟器错误`在 settings() 和 useEmulator() 中都设置了主机,将使用模拟器主机`

Firebase Firestore emulator error `Host has been set in both settings() and useEmulator(), emulator host will be used`

首先这是我得到的完整错误。

@firebase/firestore: Firestore (8.1.1): Host has been set in both settings() and useEmulator(), emulator host will be used
Error [FirebaseError]: Firestore has already been started and its settings can no longer be changed. You can only modify settings before calling any other methods on a Firestore object.

这就是我初始化模拟器的方式

const db = app.firestore();
const auth = firebase.auth();
if (process.env.NODE_ENV === 'development') {
  db.useEmulator('localhost', 8888);
  firebase.auth().useEmulator('http://localhost:9099/');
}

项目是 运行ning nextjs 当我第一次启动应用程序时,一切 运行 按预期进行,但在 next.js 页面之间进行一些刷新或导航后,我突然收到此错误。我必须关闭终端并重新开始,这很烦人我不知道 next.js 服务器 运行 是否多次使用 if (process.env.NODE_ENV === 'development') 代码,这可能是导致此错误的原因,如果这就是如何避免在已有模拟器时设置新模拟器的情况。或者它是与 firebase 模拟器相关的错误?

NextJs 正在热重载网页,xxx.useEmulator(...) 被同一 浏览器 实例调用两次。

在幕后,Firebase 库使用对当前应用程序的 全局 引用,并且从库的角度来看,您正在尝试对其进行两次或更多次初始化。

您可以使用以下代码重现此问题:

const db = app.firestore();
db.useEmulator('localhost', 8888);
db.useEmulator('localhost', 8888); // raises error

我发现的唯一解决方法是使用 window 对象来保存标志(无论是否已初始化),但您也有处理 SSR 的边缘情况。

const db = app.firestore();
if(typeof window === 'undefined' || !window['_init']) {
   db.useEmulator('localhost', 8888);
   if(typeof window !== 'undefined') {
      window['_init'] = true;
   }
}

这不是上面最优雅的代码,但它修复了错误。

关键要知道热重载是问题所在,Firebase应该只配置一次。

我同意@Reactgular 对原因的评价。但就我而言,全局解决方案不太适合,因为在测试期间会抛出错误,应该将它们隔离。

我还必须处理一个“清除模拟器数据”逻辑,与 useEmulator 相反,应该在每次初始化时调用。

为了解决这个问题,我发现在我们调用 useEmulator 之后,Firestore 对象中的内部“主机”属性 发生了变化。然后我在命令 useEmulator 之前开始检查 属性。

代码如下:

import { del } from '../request';

async function initFirestore (config) {
  const { suite, firestoreEmulatorHost } = config;
  const { app, projectId } = suite;

  const firestore = app.firestore();

  if (firestoreEmulatorHost) {
    plugEmulator(firestore, firestoreEmulatorHost);
    await clearFirestoreEmulator(projectId, firestoreEmulatorHost);
  }

  return firestore;
}

export function plugEmulator (firestore, firestoreEmulatorHost) {
  const settingsHost = firestore._delegate._settings.host;
  const isUnplugged = !settingsHost.includes(firestoreEmulatorHost);

  if (isUnplugged) {
    firestore.useEmulator('localhost', firestoreEmulatorHost);
  }
}

export function clearFirestoreEmulator (projectId, firestoreEmulatorHost) {
  const clearUrl = `http://localhost:${firestoreEmulatorHost}/emulator/v1/projects/${projectId}/databases/(default)/documents`;
  return del(clearUrl);
}

在尝试了这里几乎所有的解决方案之后,它并没有真正起作用,错误时不时发生,烦人的是我不知道如何重现它,但我认为当这个页面有无论如何,服务器端错误我用来解决这个错误的解决方案如下

const EMULATORS_STARTED = 'EMULATORS_STARTED';
function startEmulators() {
  if (!global[EMULATORS_STARTED]) {
    global[EMULATORS_STARTED] = true;
    firebase.firestore().useEmulator('localhost', 8888);
    firebase.auth().useEmulator('http://localhost:9099/');
  }
}

if (process.env.NODE_ENV === 'development') {
  startEmulators();
}

但要使其按预期工作,您需要确保所有模拟器都已启动,然后再向 next.js 服务器发出请求,因为如果此代码在模拟器启动之前执行,则全局 [EMULATORS_STARTED] 是正确的,在这种情况下它永远不会使用模拟器。 我已经在很多页面上测试过这个,其中一些有服务器端错误,但错误并没有发生,而是我得到了这些错误的日志,这是预期的行为我在应用之前并不知道这些错误首先存在这个解决方案。

不幸的是,接受的答案对我不起作用。我最终需要使用 getApp 即使我正在记忆我的 init 函数。这是最终为我解决此问题的代码(相同的上下文:Next.js 带热重载的应用程序):

const firebaseConfig = {...}

const initFirebase = once(() => {
  console.log('Initializing firebase')

  const app = initializeApp(firebaseConfig)
  const auth = getAuth(app)
  const functions = getFunctions(app)
  const db = getFirestore(app)

  if (process.env.NEXT_PUBLIC_FIREBASE_USE_EMULATOR === 'true') {
    console.log('Attching firebase emulators')

    connectFirestoreEmulator(db, 'localhost', 8080)
    connectAuthEmulator(auth, 'http://localhost:9299')
    connectFunctionsEmulator(functions, 'localhost', 5001)
  }
  return { auth, functions, db }
})

export const getFirebase = () => {
  try {
    const app = getApp()
    const auth = getAuth(app)
    const functions = getFunctions(app)
    const db = getFirestore(app)
    return { auth, functions, db }
  } catch (e) {
    return initFirebase()
  }
}