为啥 Firestore 的 'doc.get('time').toMillis' 会产生空类型错误?

Posted

技术标签:

【中文标题】为啥 Firestore 的 \'doc.get(\'time\').toMillis\' 会产生空类型错误?【英文标题】:Why is Firestore's 'doc.get('time').toMillis' producing a null Type Error?为什么 Firestore 的 'doc.get('time').toMillis' 会产生空类型错误? 【发布时间】:2020-06-30 04:22:18 【问题描述】:

在一个 react-native 应用程序中,我调用一个动作将数据发送到 firebase。但是在通话之后,我收到一个错误,其来源似乎与快照侦听器的工作方式有关。 add-msg 操作或多或少看起来像这样:

创建数据:

const addMsg = (msg, convoIds) => 
    console.log('Firestore Write: (actions/conversation) => addMsg()');

    return firebase.firestore().collection('messages').add(
        time: firebase.firestore.FieldValue.serverTimestamp(),
        sender: msg.sender,
        receiverToken: msg.receiverToken,
        sessionId: msg.sessionId,
        read: false,
        charged: false,
        userConvos: [ convoIds.sender, convoIds.receiver ],
        content: 
            type: 'msg',
            data: msg.text
        
    );
;

我还有一个快照监听器(在 componentDidMount 中执行),它使用来自 firestore 中的集合的消息填充 redux 存储。快照监听器看起来像:

export const getMessages = (convoId) => 
    const tmp = convoId == null ? '' : convoId;
    return (dispatch) => 
        console.log('Firestore Read (Listener): (actions/conversation) => getMessages()');
        return new Promise((resolve, reject) => 
            firebase
                .firestore()
                .collection('messages')
                .where('userConvos', 'array-contains', tmp)
                .orderBy('time')
                .onSnapshot((querySnapshot) => 
                    const messages = [];
                    querySnapshot.forEach((doc) => 
                        const msg = doc.data();
                        msg.docId = doc.id;
                        msg.time = doc.get('time').toMillis();

                        messages.push(msg);
                    );
                    dispatch( type: types.LOAD_MSGS, payload: messages );
                    resolve();
                );
        );
    ;
;

在同一屏幕组件中填充 flatList 的相应 reducer,如下所示:

const INITIAL_STATE = 
    messages: []
;
export default (state = INITIAL_STATE, action) => 
    switch (action.type) 
        case types.LOAD_MSGS:
            return 
                messages: action.payload
            ;
        default:
            return  ...state ;
    
;

问题:一旦数据发送,我立即收到错误TypeError: null is not an object (evaluating 'doc.get('time').toMillis'。如果我重新加载应用程序,导航回有问题的屏幕,就会出现消息,时间数据也会出现。所以我的猜测是firebase调用的承诺性质的延迟正在发生一些事情,并且延迟导致时间值的空初始化,但它的时间足以使应用程序崩溃。

问题:这里的幕后实际发生了什么,我该如何防止这个错误?

【问题讨论】:

doc.get('time') 是否为空? @LajosArpad 当我在屏幕组件中访问它时,它会显示时间 如果我是你,我会在msg.time = doc.get('time').toMillis(); 行之前以console.log(doc.get('time').toMillis()) 的形式做一个实验。你会看到那是否是空的东西。这是您解决问题所需的关键信息。 @LajosArpad 已经有了。所有时间都被记录 您能否逐字引用您收到的问题的确切错误消息? 【参考方案1】:

您遇到的问题是由本地 Firestore 缓存中的 onSnapshot() 侦听器在一个小窗口期间触发的,其中 firebase.firestore.FieldValue.serverTimestamp() 的值被视为待处理,默认情况下被视为 null。一旦服务器接受您的更改,它会以时间戳的所有新值进行响应,并再次触发您的 onSnapshot() 侦听器。

如果不小心,这可能会导致您的应用“闪烁”,因为它会两次标记数据。

要更改待处理时间戳的行为,您可以将SnapshotOptions 对象作为最后一个参数传递给doc.data()doc.get()(视情况而定)。

以下代码指示 Firebase SDK 根据本地时钟估计新的时间戳值。

const estimateTimestamps = 
  serverTimestamps: 'estimate'


querySnapshot.forEach((doc) => 
  const msg = doc.data(); // here msg.time = null
  msg.docId = doc.id;
  msg.time = doc.get('time', estimateTimestamps).toMillis(); // update msg.time to set value (or estimate if not available)

  messages.push(msg);
);

如果您想显示您的消息仍在写入数据库,您可以在估计时间戳之前检查msg.time 是否为null

const estimateTimestamps = 
  serverTimestamps: 'estimate'


querySnapshot.forEach((doc) => 
  const msg = doc.data(); // here msg.time = null when pending
  msg.docId = doc.id;
  msg.isPending = msg.time === null;
  msg.time = doc.get('time', estimateTimestamps).toMillis(); // update msg.time to set value (or estimate if not available)

  messages.push(msg);
);

如果您想忽略这些中间“本地”事件以等待服务器的完整响应,您可以使用:

.onSnapshot(includeMetadataChanges: true, (querySnapshot) => 
  if (querySnapshot.metadata.fromCache && querySnapshot.metadata.hasPendingWrites) 
    return; // ignore cache snapshots where new data is being written
  
  const messages = [];
  querySnapshot.forEach((doc) => 
    const msg = doc.data();
    msg.docId = doc.id;
    msg.time = doc.get('time', estimateTimestamps).toMillis();

    messages.push(msg);
  );
  dispatch( type: types.LOAD_MSGS, payload: messages );
  resolve();
);

在上面的代码块中,注意我在忽略事件之前还检查了querySnapshot.metadata.hasPendingWrites,这样当你第一次加载你的应用程序时,它会立即打印出所有缓存的信息。如果没有这个,您将显示一个空的消息列表,直到服务器响应。大多数网站会打印出所有缓存的数据,同时在页面顶部显示一个 throbber,直到服务器响应任何新数据。

【讨论】:

作为补充说明,不要忘记正确处置您的听众!对onSnapshot() 的调用会返回一个取消订阅函数,您应该在释放组件时存储和调用该函数。例如const unsubMessageListener = firebase...onSnapshot(...); 和处置时调用 unsubMessageListener() 非常有见地的信息。但是在尝试了列出的所有三个选项后,我仍然遇到同样的错误 这听起来像 time 字段在您的集合中的 1 个或多个文档上丢失/设置为 null,这可能是由于将 messages 数组中的条目保存回数据库而没有正确的时间戳(因此 msg.time 现在将是 null),例如在编辑消息时。试试这个而不是 msg.time = ... 行:const timestamp = doc.get('time', estimateTimestamps); if (timestamp) msg.time = timestamp.toMillis(); else console.error(doc.id + ' is missing "time" field!'); /* debugger; */ 。如果您成功了,请取消注释 debugger; 并仔细查看。 是的,else条件被执行了,但是当我检查相关文档时,“时间”字段在那里? 生病奖励赏金,因为你是唯一需要时间提供帮助的人,即使我还没有修复错误:'(

以上是关于为啥 Firestore 的 'doc.get('time').toMillis' 会产生空类型错误?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Firebase 数据库(Firestore)不添加文档

为啥 Firestore 舍入 64 位整数?

为啥 Firestore 有时不返回任何内容,没有错误,啥也没有?

为啥这个 firestore 查询需要索引?

为啥 Firestore 可编码支持不适用于此示例

为啥即使不是公开的,我也能读、写firestore?