递归调用 Firestore

Recursive calls to Firestore

我有一个 Firebase Firestore,其中 "Components" 作为根集合。集合中的每个文档("Component")可能有一个名为 "children" 的数组,数组中的每个项目都是另一个组件的 "id",本质上是一个 one-to-many 关系.由于每个 child 也是一个组件,它也可能有自己的 children 等等。

组件集合

Parent

1_Parent (document)
│ name: 'Parent'
│ id: '1_Parent'
└─children (array)
   ├─1.1_Child_one
   └─1.2_Child_two

第一个Child

1.1_Child_one (document)
     name: 'Child One'
     id: '1.1_Child_one'

第二个Child

1.2_Child_two (document)
│ name: 'Child Two'
│ id: '1.2_Child_two'
└─children (array)
   ├─1.2.1_Grandchild_one
   └─1.2.2_Grandchild_two

第一名child

1.2.1_Grandchild_one (document)
     name: 'Grandchild One'
     id: '1.2.1_Grandchild_one'

二等奖child

1.2.2_Grandchild_two (document)
 name: 'Grandchild Two'
 id: '1.2.2_Grandchild_two'

在我的代码中,我想为每个组件创建一个 object,如果它有一个 children 数组,那么数组中的每个 id 都被完全成熟的 object 从 Firestore 检索。

输出 object 树应该是这样的

1_Parent
│ name: 'Parent'
│ id: '1_Parent'
└─children
   ├─1.1_Child_one
   │    name: 'Child One'
   │    id: '1.1_Child_one'
   └─1.2_Child_two
     │  name: 'Child Two'
     │  id: '1.2_Child_two'
     └─children
        ├─1.2.1_grandhild_one
        │   name: 'Grandchild One'
        │   id: '1.2.1_grandhild_one'
        └─1.2.2_grandhild_two
            name: 'Grandchild Two'
            id: '1.2.2_grandhild_two'

作为 JSON 的输出 object 应如下所示

{
  "name": "Parent",
  "id": "1_Parent",
  "children": [
    {
      "name": "Child One",
      "id": "1.1_Child_one"
    },
    {
      "name": "Child Two",
      "id": "1.2_Child_two",
      "children": [
        {
          "name": "Grandchild One",
          "id": "1.2.1_Grandchild_one"
        },
        {
          "name": "Grandchild Two",
          "id": "1.2.2_Grandchild_two"
        }
      ]
    }
  ]
}

很明显,我们在这里需要递归,但我完全不知道如何使用 RxJS 创建递归函数。如果允许这样做,我将不胜感激一些提示或示例代码。

请注意,我在 Angular 项目中使用它,并且我正在使用 AngularFire 访问 Firebase-Firestore。

RxJS 中的递归最好使用 expand 运算符来解决。您为它提供一个投影函数,该函数 returns 一个 Observable,在收到通知时,再次使用发出的值调用投影函数。只要您的内部 Observable 不发射 EMPTYcomplete,它就会这样做。

在这样做的同时,每个通知也会转发给 expand 的订阅者,这与传统的递归不同,传统的递归只会在最后得到结果。

来自 expand 上的官方文档:

Recursively projects each source value to an Observable which is merged in the output Observable.

让我们看看你的例子。如果没有 RxJS,如果我们有一个同步数据源给我们一个节点的每个子节点(我们称它为 getChildById),该函数可能如下所示:

function createFamilyTree(node) {
    node.children = node.childrenIds.map(childId => 
        createFamilyTree(
            getChildById(childId)
        )
    );
    return node;
}

现在,我们将使用 expand 运算符将其转换为 RxJS:

parentDataSource$.pipe(
    map(parent => ({node: parent})),

    expand(({node}) => // expand is called for every node recursively 
                          (i.e. the parent and each child, then their children and so on)

        !node ? EMPTY : // if there is no node, end the recursion

        from(node.childrenIds) // we'll convert the array of children 
                                  ids to an observable stream

            .pipe(
                mergeMap(childId => getChildById(childId) // and get the child for 
                                                              the given id

                    .pipe(
                        map(child => ({node: child, parent: node}))) // map it, so we have 
                                                                       the reference to the 
                                                                       parent later on
                )
            )
    ),

    // we'll get a bunch of notifications, but only want the "root", 
       that's why we use reduce:

    reduce((acc, {node, parent}) =>
        !parent ?
            node : // if we have no parent, that's the root node and we return it
            parent.children.push(node) && acc // otherwise, we'll add children
    )
);

使用的运算符在RxJS官方文档页面有详细解释:from, expand, reduce

编辑:可以在此处找到上述代码的干净且经过测试的版本:https://stackblitz.com/edit/rxjs-vzxhqf?devtoolsheight=60

您可以按照这些思路寻找解决方案

// simulates an async data fetch from a remote db
function getComponent(id) {
  return of(components.find(comp => comp.id === id)).pipe(delay(10));
}

function expandComp(component: Observable<any>) {
  return component.pipe(
    tap(d => console.log('s', d.name)),
    mergeMap(component => {
      if (component.childrenIds) {
        return concat(
          of(component).pipe(tap(comp => comp['children'] = [])),
          from(component.childrenIds).pipe(
            mergeMap(childId => expandComp(getComponent(childId)))
          )
        )
        .pipe(
          reduce((parent: any, child) => {
            parent.children.push(child);
            return parent;
          })
        )
      } 
      else {
        return of(component);
      }
    })
  )
}
.subscribe(d => // do stuff, e.g. console.log(JSON.stringify(d, null, 3)))

我用下面的测试数据测试了上面的代码

const componentIds: Array<string> = [
  '1',
  '1.1.2'
]
const components: Array<any> = [
  {id: '1', name: 'one', childrenIds: ['1.1', '1.2']},
  {id: '1.1', name: 'one.one', childrenIds: ['1.1.1', '1.1.2']},
  {id: '1.2', name: 'one.two'},
  {id: '1.1.1', name: 'one.one.one'},
  {id: '1.1.2', name: 'one.one.two', childrenIds: ['1.1.2.1', '1.1.2.2', '1.1.2.3']},
  {id: '1.1.2.1', name: 'one.one.two.one'},
  {id: '1.1.2.2', name: 'one.one.two.two'},
  {id: '1.1.2.3', name: 'one.one.two.three'},
]

基本思想是递归调用expandComp函数,而每个组件的children循环是使用提供的from函数获得的通过 RxJS.

childrenparent 组件中的分组是使用 RxJS 的 reduce 运算符提供的,在 expandComp函数。

我最初尝试查看 RxJS 的 expand 运算符,但我无法找到使用它的解决方案。 @ggradnig 提出的解决方案利用 expand.