【问题标题】:Firebase Firestore - does creating a snapshot event listener cost excessive downloads?Firebase Firestore - 创建快照事件侦听器是否会花费过多的下载?
【发布时间】:2021-04-15 22:59:02
【问题描述】:

最初,我的用例是使用快照侦听器对数据进行分页,例如:Firestore Paginating data + Snapshot listener

但是那里的答案说它目前不受支持,所以我试图找到一种解决方法,我在这里找到了 https://medium.com/@650egor/firestore-reactive-pagination-db3afb0bf42e 。这很好,但有点复杂。此外,它的下载量可能是正常的 n 倍,因为早期侦听器中的每个更改也会被后面的侦听器捕获。

所以现在,我正在考虑放弃分页。相反,每次我想获取更多数据时,我都会简单地重新创建快照侦听器,但有 2 倍的限制。像这样:

const [limit, setLimit] = useState(50);
const [data, setData] = useState([]);
...
useEffect(()=> {
  const datalist = [];
  db.collection('chats')
  .where('id','==', chatId)
  .limit(limit)
  .onSnapshot((querySnapshot) =>{
    querySnapshot.forEach((item) => datalist.push(item));
    setData(datalist);
  }
  
}, [limit]);

return <Button title="get more data" onPress={()=> { setLimit(limit * 2}} />;

我的问题是,就过度下载(就 spark 计划而言)而言,这很糟糕吗?当我第一次做快照时,它应该下载 50 个项目,然后第二次下载 100 个,然后是 200 个。我想确认它是否是这样工作的。

另外,如果有什么原因这种方法在更基本的层面上不起作用,我想知道。

【问题讨论】:

    标签: javascript firebase google-cloud-firestore


    【解决方案1】:

    我通过构造一个跟踪某些内部状态的对象来进行分页侦听器,并具有 PageBack、PageForward、ChangeLimit 和 Unsubscribe 方法。对于Listeners,最好取消订阅之前的listener,重新设置一个;这段代码就是这样做的。在这里添加一个层可能更有效:使用 有点 更大的页面从 Firestore 进行分页(稍微计算昂贵的设置和拆除)权衡实际获取的记录数(实际成本),以及然后在本地提供较小的页面。但是,对于 PaginatedListener:

    /**
     * ----------------------------------------------------------------------
     * @function filterQuery
     * builds and returns a query built from an array of filter (i.e. "where")
     * consitions
     * @param {Query} query collectionReference or Query to build filter upong
     * @param {array} filterArray an (optional) 3xn array of filter(i.e. "where") conditions
     * @returns Firestor Query object
     */
    export const filterQuery = (query, filterArray = null) => {
      return filterArray
        ? filterArray.reduce((accQuery, filter) => {
            return accQuery.where(filter.fieldRef, filter.opStr, filter.value);
          }, query)
        : query;
    };
    
    /**
     * ----------------------------------------------------------------------
     * @function sortQuery
     * builds and returns a query built from an array of filter (i.e. "where")
     * consitions
     * @param {Query} query collectionReference or Query to build filter upong
     * @param {array} sortArray an (optional) 2xn array of sort (i.e. "orderBy") conditions
     * @returns Firestor Query object
     */
    export const sortQuery = (query, sortArray = null) => {
      return sortArray
        ? sortArray.reduce((accQuery, sortEntry) => {
            return accQuery.orderBy(sortEntry.fieldRef, sortEntry.dirStr || "asc");
            //note "||" - if dirStr is not present(i.e. falsy) default to "asc"
          }, query)
        : query;
    };
    
    /**
     * ----------------------------------------------------------------------
     * @classdesc 
     * An object to allow for paginating a listener for table read from Firestore.
     * REQUIRES a sorting choice
     * masks some subscribe/unsubscribe action for paging forward/backward
     * @property {Query} Query that forms basis for the table read
     * @property {number} limit page size
     * @property {QuerySnapshot} snapshot last successful snapshot/page fetched
     * @property {enum} status status of pagination object
     *
     * @method PageForward Changes the listener to the next page forward
     * @method PageBack Changes the listener to the next page backward
     * @method Unsubscribe returns the unsubscribe function
     * ----------------------------------------------------------------------
     */
    
    export class PaginatedListener {
      _setQuery = () => {
        const db = this.ref ? this.ref : fdb;
        this.Query = sortQuery(
          filterQuery(db.collection(this.table), this.filterArray),
          this.sortArray
        );
        return this.Query;
      };
    
      /**
       * ----------------------------------------------------------------------
       * @constructs PaginatedListener constructs an object to paginate through large
       * Firestore Tables
       * @param {string} table a properly formatted string representing the requested collection
       * - always an ODD number of elements
       * @param {array} filterArray an (optional) 3xn array of filter(i.e. "where") conditions
       * @param {array} sortArray a 2xn array of sort (i.e. "orderBy") conditions
       * @param {ref} ref (optional) allows "table" parameter to reference a sub-collection
       * of an existing document reference (I use a LOT of structered collections)
       *
       * The array is assumed to be sorted in the correct order -
       * i.e. filterArray[0] is added first; filterArray[length-1] last
       * returns data as an array of objects (not dissimilar to Redux State objects)
       * with both the documentID and documentReference added as fields.
       * @param {number} limit (optional)
       * @param {function} dataCallback
       * @param {function} errCallback
       * **********************************************************/
    
      constructor(
        table,
        filterArray = null,
        sortArray,
        ref = null,
        limit = PAGINATE_DEFAULT,
        dataCallback = null,
        errCallback = null
      ) {
        this.table = table;
        this.filterArray = filterArray;
        this.sortArray = sortArray;
        this.ref = ref;
        this.limit = limit;
        this._setQuery();
        /*this.Query = sortQuery(
          filterQuery(db.collection(this.table), this.filterArray),
          this.sortArray
        );*/
        this.dataCallback = dataCallback;
        this.errCallback = errCallback;
        this.status = PAGINATE_INIT;
      }
    
      /**
       * @method PageForward
       * @returns Promise of a QuerySnapshot
       */
      PageForward = () => {
        const runQuery =
          this.unsubscriber && !this.snapshot.empty
            ? this.Query.startAfter(_.last(this.snapshot.docs))
            : this.Query;
    
        //IF unsubscribe function is set, run it.
        this.unsubscriber && this.unsubscriber();
    
        this.status = PAGINATE_PENDING;
    
        this.unsubscriber = runQuery.limit(Number(this.limit)).onSnapshot(
          (QuerySnapshot) => {
            this.status = PAGINATE_UPDATED;
            //*IF* documents (i.e. haven't gone back ebfore start)
            if (!QuerySnapshot.empty) {
              //then update document set, and execute callback
              this.snapshot = QuerySnapshot;
            }
            this.dataCallback(
              this.snapshot.docs.map((doc) => {
                return {
                  ...doc.data(),
                  Id: doc.id,
                  ref: doc.ref
                };
              })
            );
          },
          (err) => {
            this.errCallback(err);
          }
        );
        return this.unsubscriber;
      };
    
      /**
       * @method PageBack
       * @returns Promise of a QuerySnapshot
       */
      PageBack = () => {
        const runQuery =
          this.unsubscriber && !this.snapshot.empty
            ? this.Query.endBefore(this.snapshot.docs[0])
            : this.Query;
    
        //IF unsubscribe function is set, run it.
        this.unsubscriber && this.unsubscriber();
    
        this.status = PAGINATE_PENDING;
    
        this.unsubscriber = runQuery.limitToLast(Number(this.limit)).onSnapshot(
          (QuerySnapshot) => {
            //acknowledge complete
            this.status = PAGINATE_UPDATED;
            //*IF* documents (i.e. haven't gone back ebfore start)
            if (!QuerySnapshot.empty) {
              //then update document set, and execute callback
              this.snapshot = QuerySnapshot;
            }
            this.dataCallback(
              this.snapshot.docs.map((doc) => {
                return {
                  ...doc.data(),
                  Id: doc.id,
                  ref: doc.ref
                };
              })
            );
          },
          (err) => {
            this.errCallback(err);
          }
        );
        return this.unsubscriber;
      };
    
      /**
       * @method ChangeLimit
       * sets page size limit to new value, and restarts the paged listener
       * @param {number} newLimit
       * @returns Promise of a QuerySnapshot
       */
      ChangeLimit = (newLimit) => {
        const runQuery = this.Query;
    
        //IF unsubscribe function is set, run it.
        this.unsubscriber && this.unsubscriber();
    
        this.limit = newLimit;
    
        this.status = PAGINATE_PENDING;
    
        this.unsubscriber = runQuery.limit(Number(this.limit)).onSnapshot(
          (QuerySnapshot) => {
            this.status = PAGINATE_UPDATED;
            //*IF* documents (i.e. haven't gone back ebfore start)
            if (!QuerySnapshot.empty) {
              //then update document set, and execute callback
              this.snapshot = QuerySnapshot;
            }
            this.dataCallback(
              this.snapshot.docs.map((doc) => {
                return {
                  ...doc.data(),
                  Id: doc.id,
                  ref: doc.ref
                };
              })
            );
          },
          (err) => {
            this.errCallback(err);
          }
        );
        return this.unsubscriber;
      };
    
      ChangeFilter = (filterArray) => {
        //IF unsubscribe function is set, run it (and clear it)
        this.unsubscriber && this.unsubscriber();
    
        this.filterArray = filterArray; // save the new filter array
        const runQuery = this._setQuery(); // re-build the query
        this.status = PAGINATE_PENDING;
    
        //fetch the first page of the new filtered query
        this.unsubscriber = runQuery.limit(Number(this.limit)).onSnapshot(
          (QuerySnapshot) => {
            this.status = PAGINATE_UPDATED;
            //*IF* documents (i.e. haven't gone back ebfore start)
            this.snapshot = QuerySnapshot;
            this.dataCallback(
              this.snapshot.empty
                ? null
                : this.snapshot.docs.map((doc) => {
                    return {
                      ...doc.data(),
                      Id: doc.id,
                      ref: doc.ref
                    };
                  })
            );
          },
          (err) => {
            this.errCallback(err);
          }
        );
        return this.unsubscriber;
      };
    
      unsubscribe = () => {
        //IF unsubscribe function is set, run it.
        this.unsubscriber && this.unsubscriber();
        this.unsubscriber = null;
      };
    }
    

    【讨论】:

      【解决方案2】:

      每次您执行不专门针对本地持久性缓存的查询时,它都会从 Firestore 检索完整的文档集。这是您需要知道的唯一信息。后续查询不会提供先前查询的部分缓存结果。

      您现在显示的代码实际上存在很大问题,因为它会泄漏侦听器。如果限制发生变化并导致钩子再次执行,则没有任何东西可以停止先前的侦听器。您应该从 useEffect 挂钩返回一个函数,该函数在不再需要侦听器时取消订阅该侦听器。只需返回onSnapshot 返回的取消订阅函数即可。您还应该阅读useEffect hooks that require cleanup 的文档,就像您在此处所做的那样。泄漏侦听器的潜在成本可能比使用新限制重复查询的成本要糟糕得多,因为泄漏的侦听器会在新文档更改时不断读取它们-这就是为什么您必须在不取消订阅时立即取消订阅不再需要它们了。

      【讨论】:

      • 有没有办法检查 Firebase 中触发了哪些事件,检查我是否正确退订?
      • 没有事件。只需调用取消订阅函数,您的快照回调将不再被调用。
      【解决方案3】:

      你确实理解正确。

      根据您的实施,第一次读取 50 次,第二次读取 100 次,第三次读取 200 次,依此类推(如果文档数量少于限制,您将被收费)文档数量)。

      我确实在我的一个已发布应用程序中使用了与此方法非常相似的方法,但不是每次都将要加载的文档数量加倍,而是在限制中添加了一定数量。

      【讨论】:

      • 你是对的,添加一个常量更好,谢谢
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-07-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-09
      • 2020-02-24
      相关资源
      最近更新 更多