繁体   English   中英

在实时 Firestore 中返回 onSnapshot 时如何提取数据?

[英]How to extract data when onSnapshot is returned in realtime firestore?

我有两个文件contact.jsfunctions.js 我正在使用 Firestore 实时功能。

这是我的functions.js文件代码:

export const getUserContacts = () => {
  const contactDetailsArr = [];

  return db.collection("users").doc(userId)
    .onSnapshot(docs => {

      const contactsObject = docs.data().contacts;

      for (let contact in contactsObject) {

        db.collection("users").doc(contact).get()
          .then(userDetail => {
            contactDetailsArr.push({
              userId: contact,
              lastMessage: contactsObject[contact].lastMsg,
              time: contactsObject[contact].lastMsgTime,
              userName:userDetail.data().userName,
              email: userDetail.data().emailId,
              active: userDetail.data().active,
              img: userDetail.data().imageUrl,
              unreadMsg:contactsObject[contact].unreadMsg
            })

          })
      }
      console.log(contactDetailsArr);
      return contactDetailsArr;

    })
}

当我这样做时在contact.js中:

useEffect(() => {
    let temp = getUserContacts();
    console.log(temp);
}, [])

我想在contacts.js中提取contactDetailsArr的数据,但我得到的 temp 值被控制台为:

ƒ () {
      i.Zl(), r.cs.ws(function () {
        return Pr(r.q_, o);
      });
    }

在我的情况下如何提取数组数据?

onSnapshot()返回可用于分离 Firestore 侦听器的 function。 使用监听器时,最好将数据直接设置到 state 中,而不是从 function 中返回内容。 尝试重构代码,如下所示:

const [contacts, setContacts] = useState([]);

useEffect(() => {
  const getUserContacts = () => {
    const contactDetailsArr = [];

    const detach = db.collection("users").doc(userId)
      .onSnapshot(docs => {
        const contactsObject = docs.data().contacts;
        const contactsSnap = await Promise.all(contactsObject.map((c) => db.collection("users").doc(c).get()))

        const contactDetails = contactsSnap.map((d) => ({
          id: d.id,
          ...d.data()
          // other fields like unreadMsg, time
        }))

        // Update in state 
        setContacts(contactDetails);
      })
  }

  getUserContacts();
}, [])

然后使用contacts数组将 map 数据直接输入到 UI。

假设

此答案假设用户的数据在您的 Firestore 中如下所示:

// Document at /users/someUserId
{
  "active": true,
  "contacts": {
    "someOtherUserId": {
      "lastMsg": "This is a message",
      "lastMsgTime": /* Timestamp */,
      "unreadMsg": true // unclear if this is a boolean or a count of messages
    },
    "anotherUserId": {
      "lastMsg": "Hi some user! How are you?",
      "lastMsgTime": /* Timestamp */,
      "unreadMsg": false
    }
  },
  "emailId": "someuser@example.com",
  "imageUrl": "https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg",
  "userName": "Some User"
}

注:日后提问时,请添加与上述类似的数据结构示例


使用当前结构附加侦听器

如上所示的结构有许多缺陷。 用户数据中的“联系人”object 应移动到用户主文档的子集合中。 其原因包括:

  • 任何用户都可以阅读其他用户的(最新)消息(不能被安全规则阻止)
  • 任何用户都可以读取另一个用户的联系人列表(不能被安全规则阻止)
  • 随着个人用户向更多用户发送消息,他们的用户数据将迅速增长
  • 每次要读取用户的数据时,即使不使用,也必须下载他们的整个消息 map
  • 当您填写用户的联系人数组时,您正在获取他们的整个用户数据文档,即使您只需要他们的activeemailimageUrluserName属性
  • 当两个用户在编辑同一用户的联系人列表时(例如发送消息时),遇到文档写入冲突的可能性更高
  • 难以(有效)检测到用户联系人列表的更改(例如新增、删除)
  • 很难(有效地)监听对另一个用户的活动状态、email、个人资料图像和显示名称的更改,因为监听器将在收到的每个消息更新时被触发

要在您的functions.js库中获取用户的联系人一次,您可以使用:

// Utility function: Used to hydrate an entry in a user's "contacts" map
const getContactFromContactMapEntry = (db, [contactId, msgInfo]) => {
  return db.collection("users")
    .doc(contactId)
    .get()
    .then((contactDocSnapshot) => {
      const { lastMsg, lastMsgTime, unreadMsg, userName } = msgInfo;
      
      const baseContactData = {
        lastMessage: lastMsg,
        time: lastMsgTime,
        unreadMsg,
        userId: contactId
      }
      
      if (!contactDocSnapshot.exists) {
        // TODO: Decide how to handle unknown/deleted users
        return {
          ...baseContactData,
          active: false,           // deleted users are inactive, nor do they
          email: null,             // have an email, image or display name
          img: null,
          userName: "Deleted user"
        };
      }
      
      const { active, emailId, imageUrl, userName } = contactDocSnapshot.data();
      
      return {
        ...baseContactData,
        active,
        email: emailId,
        img: imageUrl,
        userName
      };
    });
};

export const getUserContacts = (db, userId) => { // <-- note that db and userId are passed in
  return db.collection("users")
    .doc(userId)
    .get()
    .then((userDataSnapshot) => {
      const contactsMetadataMap = userDataSnapshot.get("contacts");
      return Promise.all( // <-- waits for each Promise to complete
        Object.entries(contactsMetadataMap) // <-- used to get an array of id-value pairs that we can iterate over
          .map(getContactFromContactMapEntry.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
      );
    });
}

示例用法:

getUserContacts(db, userId)
  .then((contacts) => console.log("Contacts data:", contacts))
  .catch((err) => console.error("Failed to get contacts:", err))
  
// OR

try {
  const contacts = await getUserContacts(db, userId);
  console.log("Contacts data:", contacts);
} catch (err) {
  console.error("Failed to get contacts:", err)
}

要获取用户的联系人并保持列表更新,请在您的functions.js库中使用 function,您可以使用:

// reuse getContactFromContactMapEntry as above

export const useUserContacts = (db, userId) => {
  if (!db) throw new TypeError("Parameter 'db' is required");
  
  const [userContactsData, setUserContactsData] = useState({ loading: true, contacts: [], error: null });
  
  useEffect(() => {
    // no user signed in?
    if (!userId) {
      setUserContactsData({ loading: false, contacts: [], error: "No user signed in" });
      return;
    }
    
    // update loading status (as needed)
    if (!userContactsData.loading) {
      setUserContactsData({ loading: true, contacts: [], error: null });
    }
    
    let detached = false;
    
    const detachListener = db.collection("users")
      .doc(userId)
      .onSnapshot({
        next: (userDataSnapshot) => {
          const contactsMetadataMap = userDataSnapshot.get("contacts");
          
          const hydrateContactsPromise = Promise.all( // <-- waits for each Promise to complete
            Object.entries(contactsMetadataMap) // <-- used to get an array of id-value pairs that we can iterate over
              .map(getContactFromContactMapEntry.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
          );
          
          hydrateContactsPromise
            .then((contacts) => {
              if (detached) return; // detached already, do nothing.
              setUserContactsData({ loading: false, contacts, error: null });
            })
            .catch((err) => {
              if (detached) return; // detached already, do nothing.
              setUserContactsData({ loading: false, contacts: [], error: err });
            });
        },
        error: (err) => {
          setUserContactsData({ loading: false, contacts: [], error: err });
        }
      });
      
    return () => {
      detached = true;
      detachListener();
    }
  }, [db, userId])
}

注意:上面的代码不会(由于复杂性):

  • 对另一个用户的活动状态、email 或个人资料图像的变化做出反应
  • 网络问题导致setUserContactsData方法被乱序调用时正确处理
  • 在每次渲染时更改db实例时处理

示例用法:

const { loading, contacts, error } = useUserContacts(db, userId);

使用子集合结构附加侦听器

为了提高效率重组您的数据,您的结构将更新为以下内容:

// Document at /users/someUserId
{
  "active": true,
  "emailId": "someuser@example.com",
  "imageUrl": "https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg",
  "userName": "Some User"
}

// Document at /users/someUserId/contacts/someOtherUserId
{
  "lastMsg": "This is a message",
  "lastMsgTime": /* Timestamp */,
  "unreadMsg": true // unclear if this is a boolean or a count of messages
}

// Document at /users/someUserId/contacts/anotherUserId
{
  "lastMsg": "Hi some user! How are you?",
  "lastMsgTime": /* Timestamp */,
  "unreadMsg": false
}

使用上述结构提供以下好处:

  • 为联系人列表补充水分时,网络性能显着提高
  • 安全规则可用于确保用户无法读取彼此的联系人列表
  • 安全规则可用于确保消息在两个用户之间保持私密
  • 收听其他用户的个人资料更新可以在不阅读或收到其他私人消息的任何更改的情况下完成
  • 您可以部分获取用户的消息收件箱而不是整个列表
  • 联系人列表很容易更新,因为两个用户不太可能更新同一个联系人条目
  • 易于检测用户的联系人条目何时被添加、删除或修改(例如接收新消息或将消息标记为已读)

要在您的functions.js库中获取用户的联系人一次,您可以使用:

// Utility function: Merges the data from an entry in a user's "contacts" collection with that user's data
const mergeContactEntrySnapshotWithUserSnapshot = (contactEntryDocSnapshot, contactDocSnapshot) => {
  const { lastMsg, lastMsgTime, unreadMsg } = contactEntryDocSnapshot.data();
  
  const baseContactData = {
    lastMessage: lastMsg,
    time: lastMsgTime,
    unreadMsg,
    userId: contactEntryDocSnapshot.id
  }
  
  if (!contactDocSnapshot.exists) {
    // TODO: Handle unknown/deleted users
    return {
      ...baseContactData,
      active: false,           // deleted users are inactive, nor do they
      email: null,             // have an email, image or display name
      img: null,
      userName: "Deleted user"
    };
  }
  
  const { active, emailId, imageUrl, userName } = contactDocSnapshot.data();
  
  return {
    ...baseContactData,
    active,
    email: emailId,
    img: imageUrl,
    userName
  };
}

// Utility function: Used to hydrate an entry in a user's "contacts" collection
const getContactFromContactsEntrySnapshot = (db, contactEntryDocSnapshot) => {
  return db.collection("users")
    .doc(contactEntry.userId)
    .get()
    .then((contactDocSnapshot) => mergeContactEntrySnapshotWithUserSnapshot(contactEntryDocSnapshot, contactDocSnapshot));
};

export const getUserContacts = (db, userId) => { // <-- note that db and userId are passed in
  return db.collection("users")
    .doc(userId)
    .collection("contacts")
    .get()
    .then((userContactsQuerySnapshot) => {
      return Promise.all( // <-- waits for each Promise to complete
        userContactsQuerySnapshot.docs // <-- used to get an array of entry snapshots that we can iterate over
          .map(getContactFromContactsEntrySnapshot.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
      );
    });
}

示例用法:

getUserContacts(db, userId)
  .then((contacts) => console.log("Contacts data:", contacts))
  .catch((err) => console.error("Failed to get contacts:", err))
  
// OR

try {
  const contacts = await getUserContacts(db, userId);
  console.log("Contacts data:", contacts);
} catch (err) {
  console.error("Failed to get contacts:", err)
}

为了以保持最新的方式获取用户的联系人,我们首先需要引入几个实用程序useEffect包装器(有一些库可以实现更强大的实现):

export const useFirestoreDocument = ({ db, path }) => {
  if (!db) throw new TypeError("Property 'db' is required");
  
  const [documentInfo, setDocumentInfo] = useState({ loading: true, snapshot: null, error: null });
  
  useEffect(() => {
    if (!path) {
      setDocumentInfo({ loading: false, snapshot: null, error: "Invalid path" });
      return;
    }
    
    // update loading status (as needed)
    if (!documentInfo.loading) {
      setDocumentInfo({ loading: true, snapshot: null, error: null });
    }
    
    return db.doc(path)
      .onSnapshot({
        next: (docSnapshot) => {
          setDocumentInfo({ loading: false, snapshot, error: null });
        },
        error: (err) => {
          setDocumentInfo({ loading: false, snapshot: null, error: err });
        }
      });
  }, [db, path]);
  
  return documentInfo;
}

export const useFirestoreCollection = ({ db, path }) => {
  if (!db) throw new TypeError("Property 'db' is required");
  
  const [collectionInfo, setCollectionInfo] = useState({ loading: true, docs: null, error: null });
  
  useEffect(() => {
    if (!path) {
      setCollectionInfo({ loading: false, docs: null, error: "Invalid path" });
      return;
    }
    
    // update loading status (as needed)
    if (!collectionInfo.loading) {
      setCollectionInfo({ loading: true, docs: null, error: null });
    }
    
    return db.collection(path)
      .onSnapshot({
        next: (querySnapshot) => {
          setCollectionInfo({ loading: false, docs: querySnapshot.docs, error: null });
        },
        error: (err) => {
          setCollectionInfo({ loading: false, docs: null, error: err });
        }
      });
  }, [db, path]);
  
  return collectionInfo;
}

要使用该方法来水合联系人,您可以从ContactEntry组件中调用它:

// mergeContactEntrySnapshotWithUserSnapshot is the same as above

const ContactEntry = ({ db, userId, key: contactId }) => {
  if (!db) throw new TypeError("Property 'db' is required");
  if (!userId) throw new TypeError("Property 'userId' is required");
  if (!contactId) throw new TypeError("Property 'key' (the contact's user ID) is required");

  const contactEntryInfo = useFirestoreDocument(db, `/users/${userId}/contacts/${contactId}`);
  const contactUserInfo = useFirestoreDocument(db, `/users/${contactId}`);
  
  if ((contactEntryInfo.loading && !contactEntryInfo.error) && (contactUserInfo.loading && !contactUserInfo.error)) {
    return (<div>Loading...</div>);
  }
  
  const error = contactEntryInfo.error || contactUserInfo.error;
  if (error) {
    return (<div>Contact unavailable: {error.message}</div>);
  }
  
  const contact = mergeContactEntrySnapshotWithUserSnapshot(contactEntryInfo.snapshot, contactUserInfo.snapshot);

  return (<!-- contact content here -->);
}

这些ContactEntry组件将从Contacts组件中填充:

const Contacts = ({db}) => {
  if (!db) throw new TypeError("Property 'db' is required");
  
  const { user } = useFirebaseAuth();
  const contactsCollectionInfo = useFirestoreCollection(db, user ? `/users/${user.uid}/contacts` : null);
  
  if (!user) {
    return (<div>Not signed in!</div>);
  }
  
  if (contactsCollectionInfo.loading) {
    return (<div>Loading contacts...</div>);
  }
  
  if (contactsCollectionInfo.error) {
    return (<div>Contacts list unavailable: {contactsCollectionInfo.error.message}</div>);
  }
  
  const contactEntrySnapshots = contactsCollectionInfo.docs;
  
  return (
    <>{
      contactEntrySnapshots.map(snapshot => {
        return (<ContactEntry {...{ db, key: snapshot.id, userId: user.uid }} />);
      })
    }</>
  );
}

示例用法:

const db = firebase.firestore();
return (<Contacts db={db} />);

您的代码似乎不是用 async/await 或 promise 之类的风格编写的

例如 contactDetailsArr 将作为空数组返回

onSnapshot 也会创建对 Firestore 集合的长期订阅,并且可以用简单的 get() 替换

在此处输入图像描述

请参阅Firestore https://firebase.google.com/docs/firestore/query-data/get-data#web-version-9_1上的示例

在此处输入图像描述

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM