递归调用Firestore

时间:2018-11-03 09:00:40

标签: angular firebase rxjs google-cloud-firestore angularfire2

我有一个Firebase Firestore,其中以“组件”作为根集合。集合中的每个文档(“组件”)可以具有一个称为“子项”的数组,该数组中的每个项目都是另一个组件的“ id”,本质上是一对多的关系。由于每个孩子也是一个组成部分,因此它也可能有自己的孩子,依此类推。

组件集合

父母

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

第一个孩子

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

第二个孩子

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

第一个孙子

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

第二代孙子

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

在我的代码中,我想为每个组件创建一个对象,如果它具有子数组,则数组中的每个id都将从从Firestore检索的完全成熟的对象替换。

输出对象树应如下图所示

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输出对象应如下所示

{
  "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。

2 个答案:

答案 0 :(得分:4)

使用expand运算符可以最好地解决RxJS中的递归问题。您为其提供了一个投影函数,该函数返回一个Observable,在收到通知时,将使用发出的值再次调用该投影函数。只要您的内部Observable不发出EMPTYcomplete,它就会执行此操作。

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

摘自expand上的官方文档:

  

将每个源值递归地投影到一个Observable中,该值将合并到输出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官方文档页面上详细说明了所使用的运算符:fromexpandreduce

编辑:可以在以下位置找到上述代码的完整版本:https://stackblitz.com/edit/rxjs-vzxhqf?devtoolsheight=60

答案 1 :(得分:3)

您可能会遵循以下思路

// 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 循环都是使用RxJS提供的from函数获得的。

>

使用在reduce函数中使用的RxJS的expandComp运算符来提供 parent 组件中 children 的分组。

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