组织组件
在上一小节的例子中,我们已经展示了如何利用相应路径关系来表达组件之间的逻辑关系,从而有效组织组件。在这一小节我们将详细描述如何组织组件。
组件树
在oak框架中,每一个组件都被映射到一颗“组件树”上的结点。当然这里用“树”并不准确,其实这些结点之间是一片“森林”,每个树代表一个页面。树的根结点就是Page组件。
每个组件在初始化时,建立其组件树上的结点,其结点的位置由oakPath这个属性所决定。因此,所有的关联到Entity上的组件都需要设置其oakPath。唯一的例外是根结点也就是Page页面,它们的oakPath是在初始化时由框架自动编译形成的,其值就等于它们在src/pages目录下的相对路径。
当组件在组件树上构建完成后,框架会给其标定一个它的路径,这个属性就是前面经常用到的oakFullpath。因此,当在一个组件中包含其它子组件时,通过像这样设置其oakPath即可告诉组件树,将它关联到自己的子结点中,相对路径就是relative:
oakPath={`${oakFullpath}.${relative}`}
当然,这里的relative并不能随便编写。如果父组件和子组件都有相关的Entity,它需要正确表达父子Entity之间的关系,您可以阅读并参见相关章节。如果父组件是一个Virtual组件(不关联在任何Entity上),而子组件是一个Entity组件,则相对路径应该是:
- 如果子组件是一个List组件,则relative为
${Entity}s,Entity是子组件所属对象; - 如果子组件是一个Detail/Upsert组件,则relative为
${Entity},Entity是子组件所属对象;
父子组件的关系
如果组件和其子组件都是Entity组件,则它们之间有三种典型的关系:
- 一对一:父亲是一个detail/upsert组件,其子组件也是一个detail/upsert组件,此时父子组件所关联的Entity必然是多对一(子->父)。例如:假设我们设计了一个Application的详情页面,其中也要渲染其关联的父对象System信息,则可以分成两个组件编写,并通过设置
<SystemDetail oakPath={`${oakFullpath}.system}` />来指定其相对关系。 - 一对多:父亲是一个detail/upsert组件,其子组件是一个list组件,此时父子组件所关联的Entity是一对多(父->子)。在上一小节的例子里就是这种关系,一个System的详情组件包含了一个子对象Application的列表组件(一对多关系)。
- 多对一:父亲是一个list组件,其子组件是一个detail/upsert组件,此时,父子组件所属的Entity一定是相等的,它们之间的相对路径应是子组件对象的主键:
<SystemItem oakPath={`${oakFullpath}.${system.id}`} />
在三种关系中,前两种关系都出现在父页面为detail/upsert页面时,此时,父子组件可以各自负责自身的取数和渲染,代码分离较为干净,而在第三种多对一关系中,如果子组件是无条件渲染(例如子组件渲染的是列表组件中的一行详情展示),则父组件的projection应当能覆盖子组件(甚至子孙组件)需要的projection,也就是说父组件应当帮助子组件取数。如果您未能保证这一点,当子组件渲染时发现数据不完整,则会自己发起请求去获取完整的数据,这可能会导致大量并发的网络请求。
Virtual组件
有些时候,我们需要定义一个不关联在任何Entity上的组件,这种需求往往发生在像首页/看板这种需要取不同Entity的数据,放在一起给用户加以展现的Page层。这时,我们可以将Page定义成一个Virtual组件,再将其下的不同Entity分割成不同组件加以处理。
定义Virtual组件,只需要在OakComponent定义时,不定义entity属性即可。
因为Virtual组件并不关联在任何对象上,所以它的formData方法中并没有可用的data数据,同时也无法使用update/create/updateItem这些框架方法去更新数据。但是它的refresh方法(刷新)和execute方法(提交更新)仍然是有效的,它会像下面所述的级联取数/更新行为,对其包含的所有子组件生效。
级联取数/更新
通过有效组织组件,不仅可以使代码更加的优雅和可重用,同时还获得了Oak框架的一个强大的能力——级联取数/更新。回忆一下,我们的组件之间的相对路径,就是对象之间的级联属性,因此,对象之间的级联关系,也被完美的映射到了组件上。
详细来说,当在一个父组件上执行刷新(调用this.refresh方法)时,其所有的相对路径上的子组件都会刷新,注意,这里的刷新是会将子组件的projection合并到父组件的对象之上统一执行,以获得性能的提升。
而当在一个父组件上执行提交(调用this.execute方法)时,其所有的相对路径上的子组件所产生的更新,都会提交到服务器。同样的,这里所有的更新数据也会被提升到父组件的对象之上进行统一执行。
共享路径
父组件和子组件在某些特殊的情况下,也可以共享路径,从而达到共享组件树结点的目的。
只要设置子组件的oakPath为当前组件的oakFullpath,就可以实现共享路径和组件树上结点的目标:
oakPath={oakFullpath}
父组件没有实际逻辑
一种常见的情况是父组件为Page,引用子组件进行渲染。由于取数和渲染逻辑都写在子组件中,父组件只是一层wrapper。此时如果将父组件设计为Virtual组件过于臃肿,因此可以在声明父组件时不声明projection,并使父子组件共享同一路径,此时框架会在子组件渲染时才进行组件树的取数逻辑。
子组件是抽象组件
有些子组件是抽象组件,并不限定在某一具体Entity上。这类组件在使用时,和其父组件可以通过共享路径来实现这种抽象逻辑。例如,在oak-frontend-base库中,提供了诸如list/detail/filter等抽象类组件,像list组件可以渲染出一个对象列表,在使用时就要声明其oakPath和当前(List)结点的oakFullpath一致。
多个子组件是对同一行对象的更新
在有些设计中,由于对某一对象的某类操作需要涉及更新大量的属性,在设计时会采用共享路径的方式,使他们的更新被缓存在同一个组件树结点之上。像下面这样的步骤条设计中,多个步骤条下的子组件如果是更新同行数据,它们的oakPath可以设置成一致。
这种情况要注意,第一个渲染的子组件应负责取数,而后续渲染的子组件的projection会被忽略。
子组件的相对路径重复
有的时候,需要在同一个相对路径下渲染多个不同的子组件,这种情况在一对多的关系中最常见。例如,假设在一个System的detail组件中,我们希望用两个子组件来分别渲染:
- 第一个子组件渲染所有类型为Web的Application(list)
- 第二个子组件渲染所有类型为小程序的Application(list) 因为两个组件都是Application对象的list,且它们相对System父结点的相对路径都是application$system,但它们并不是上面所描述的共享结点的情况,因为两个组件渲染的数据并不一样(filter会有区别)
这种情况,可以分别定义两个组件的oakPath为${oakFullpath}.application$system:1 和${oakFullpath}.application$system:2 ,通过冒号和后面的字符来进行区分。
绝对路径
在有的情况下,某个组件的渲染需要引用另一个对象的组件,而它们(所属的对象)之间并没有直接的关联关系(请注意,这种情况你应该首先检查一下,你的设计是否合理),此时无法通过相对路径来标定两者之间的绝对关系,此时我们可以为该子组件设置一条绝对路径:
oakPath="ogb-system-upsert-application-upsert"
绝对路径的命名应当稍微复杂一点,以保持其在全局唯一(可以用*包名+当前组件路径名+子组件路径名来进行命名)。
一个子组件如果使用了绝对路径,它和父组件之间就没有任何关联关系。父组件的refresh/execute都不会与之发生任何联系。