2-3/2-4
This commit is contained in:
parent
0a896349f7
commit
043c05addc
|
|
@ -8,6 +8,7 @@
|
|||
- [部署](./chapter1/deploy.md)
|
||||
- [编写应用](./chapter2/def.md)
|
||||
- [编写对象](./chapter2/1.md)
|
||||
- [对象之间的关系](./chapter2/1-1.md)
|
||||
- [编写组件/页面](./chapter2/2.md)
|
||||
- [三层架构](./chapter2/2-1.md)
|
||||
- [目录文件结构](./chapter2/2-2.md)
|
||||
|
|
@ -15,4 +16,5 @@
|
|||
- [编写详情组件](./chapter2/2-3-1.md)
|
||||
- [编写更新组件](./chapter2/2-3-2.md)
|
||||
- [编写列表组件](./chapter2/2-3-3.md)
|
||||
- [组织组件](./chapter2/2-3-4.md)
|
||||
- [组织组件](./chapter2/2-4.md)
|
||||
- [组件上的数据和方法](./chapter2/2-5.md)
|
||||
|
|
@ -0,0 +1 @@
|
|||
# 对象之间的关系
|
||||
|
|
@ -53,10 +53,10 @@ export default function Render(props: WebComponentProps<EntityDict, 'system', fa
|
|||
```
|
||||
在*Render*函数的唯一参数*props*中,Oak框架将以*WebComponentProps*定义的格式,注入了两个属性:
|
||||
|
||||
* data: 在data中存放从逻辑层传递过来的数据,以及Oak框架的一些通用变量(例如上面代码中的*oakFullpath*,这类变量都以*Oak*开头)。从逻辑层传递过来的数据则包括:
|
||||
* data: 在data中存放从逻辑层传递过来的数据,以及Oak框架的一些通用变量(例如上面代码中的*oakFullpath*,这类变量都以*oak*开头)。从逻辑层传递过来的数据则包括:
|
||||
* formData中返回的数据(在上面的例子里,是从所取到的system行当中展开的属性)
|
||||
* data和property中声明的数据。
|
||||
* methods: 在methods中存放从逻辑层注入的方法,以及Oak框架注入的一些通用方法(例如上面代码当中的*t*方法,就是框架注入的i18n方法)。其中,组件自身逻辑层注入的方法声明在methods属性中。
|
||||
* methods: 在methods中存放从逻辑层注入的方法,以及Oak框架注入的一些通用方法(例如上面代码当中的*t*方法,就是框架所注入的i18n方法)。其中,组件自身逻辑层注入的方法声明在*OakComponent*所声明的*methods*属性中。
|
||||
|
||||
### 使用组件
|
||||
下面需要将上面的组件嵌入到系统的某一个页面当中。如果您在初始化项目时依赖了*oak-general-business*库,则这个组件已经被*pages/console/system/config*页面所引用。
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export default function Render(
|
|||
);
|
||||
}
|
||||
```
|
||||
在这里,我们利用了antd当中的部分输入组件,并在props.methods中引出了一个框架提供的*update*方法,这个方法可以将更新的属性记录在当前结点之中。要注意的是,当有更新数据时,formData函数所取到的数据项*data*就是更新后的数据。
|
||||
在这里,我们利用了antd当中的部分输入组件,并在props.methods中引出了一个框架提供的*update*方法,这个方法可以将更新的属性记录在当前组件的[组件树](2-4.md)结点之中。要注意的是,此时*formData*函数所取到的数据项*data*是应用了相关更新后的数据。
|
||||
|
||||
|
||||
### 提交更新
|
||||
|
|
@ -183,4 +183,33 @@ export default function Render(
|
|||
|
||||

|
||||
|
||||
你可能会注意到,在上述例子中,有好几个框架级别的变量被使用,比如:*OakExecuting*表示是否在更新中,*oakExecutable*表示更新是否可行。关于系统有哪些变量,可以查阅todo章节。
|
||||
你可能会注意到,在上述例子中,有好几个框架级别的变量被使用,比如:*OakExecuting*表示是否在更新中,*oakExecutable*表示更新是否可行。关于系统有哪些变量,可以查阅todo章节。
|
||||
|
||||
### 新建数据
|
||||
新建数据的逻辑和更新非常相似,需要对对象的属性进行*update*操作,并执行*exeute*来提交数据,这也是为什么在Oak框架里我们常常使用*Upsert*这个名词。有几个需要注意的要点:
|
||||
|
||||
1. 如果一个组件在创建之时就没有设置oakId,则框架会自动执行*create*动作,为它创建出一条“新”数据。因此,当在使用Upsert组件来更新数据时,如果确认是更新,则应当等确认了主键之后再渲染该组件。像下面的代码(这里模拟的是在一个*Application* Upsert组件上,嵌入一个*System*的Upsert组件去插入或更新它所指向的*System*行,我们毌须去关注这个需求是不是合理):
|
||||
```typescript
|
||||
<SystemUpsert oakId={application?.systemId} oakPath={`${oakFullpath}.system`} />
|
||||
```
|
||||
这样编写代码是很危险的,除非你确认跑到这里的时候,application一定已经确认取到值了,否则就会出现这样的情况:当*SystemUpsert*组件开始渲染的时候,application还未取到,所以其oakId就是undefined,组件初始化时,认为是一个create动作,所以新建了一行system数据,而当application数据取到后,再用其systemId去置oakId,此时系统会认为这是异常情况而报错。
|
||||
|
||||
正确的写法应当是:
|
||||
```typescript
|
||||
{!!application && <SystemUpsert oakId={application.systemId} oakPath={`${oakFullpath}.system`} />}
|
||||
```
|
||||
这样系统就会正确的根据*application.systemId*是否有值,正确的决定相应的*System*应当是create还是update。
|
||||
|
||||
2. 要判断当前组件是update还是create,有两种方法:
|
||||
* 在OakComponent中,调用*this.isCreation()*方法
|
||||
* 如果一行数据是create,则在formData中取到的data,其$$createAt$$值为1
|
||||
3. 一个create页面,在提交之后不会再自动进行*create*,如果您需要这个组件进行连续的create,需要像下面这样,在*execute*之后显式调用*this.create*:
|
||||
```typescript
|
||||
...
|
||||
await this.execute();
|
||||
await this.create({
|
||||
...
|
||||
});
|
||||
...
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -136,4 +136,4 @@ export type Schema = EntityShape & {
|
|||
用了这样的定义,相当于告诉框架“此applicationList组件所查询的*Application*,是关联在其父结点的*System*对象上(框架会要求其查询数据时符合```systemId = 父system.id```的约束)。
|
||||
|
||||
|
||||
当你理解了上面这个例子,就会意识到,绝大多数的页面中的组件关系,和它们所属的对象关系是完全对应的。通过仔细划分这些对象到子组件上,可以让整体代码组织的相当优雅和易于维护。在下一章节中,我们将进一步描述这种关系的细节。
|
||||
当你理解了上面这个例子,就会意识到,绝大多数的页面中的组件关系,和它们所属的对象关系是完全对应的。通过仔细划分这些对象到子组件上,可以让整体代码组织的相当优雅,并且易于重用和维护。在下一章节中,我们将进一步描述这种关系的细节。
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
# 组织组件
|
||||
|
||||
### 父子组件的关系
|
||||
组件和其子组件之间有三种典型的关系:
|
||||
|
||||
* 一对一:父亲是一个*detail/upsert*组件,其子组件也是一个*detail/upsert*组件。例如:假设我们设计了一个*Application*的详情页面,其中也要渲染其关联的*System*信息,则可以分成两个组件编写,并通过设置```<SystemDetail oakPath={`${oakFullpath}.system}` />```来指定关系。
|
||||
* 一对多:父亲是一个*detail/upsert*组件,其子组件是一个*list*组件。在上一小节的例子里就是这种关系,一个*System*的详情组件包含了一个*Application*的列表组件。
|
||||
* 多对一:父亲是一个*list*组件,其子组件是一个*detail/upsert*组件。这种关系是最特殊的一种,此时父子组件所属的*Entity*一定是相等的,且它们的相对路径是子组件对象的主键:```<SystemItem oakPath={`${oakFullpath}.${system.id}`} />```
|
||||
|
||||
|
||||
|
|
@ -4,9 +4,11 @@
|
|||
在编写组件前,首先要明确一个概念:用Oak框架编写业务应用,其中几乎所有的组件和绝大部分页面都关联在某一个*Entity*之上,根据组件的功能特征,我们可以将这类**Entity组件**划分为以下三种类型:
|
||||
* **List**:列表页,查询并显示此*Entity*的多条数据
|
||||
* **Detail**:详情页,查询并显示此*Entity*的(指定id的)某条数据
|
||||
* **Upsert**:更新页,查询并对此*Entity*的(指定id的)某条数据进行更新,或者创建一条新的数据
|
||||
* **Upsert**:更新页,查询并对此*Entity*的(指定id的)某条数据进行更新,或者创建一条新的数据。
|
||||
|
||||
下面将以*oak-general-business*包当中的*System*和*Application*这两个*Entity*作为示例,介绍如何编写组件。
|
||||
> 当然,也可以编写一个不关联任何Entity的组件,我们称之为**Virtual组件**。关于Virtual组件的作用和规范,我们在[组织组件章节](2-4.md)中会加以描述。
|
||||
|
||||
下面将以*oak-general-business*包当中的*System*和*Application*这两个*Entity*作为示例,介绍如何编写Entity组件。
|
||||
|
||||
*oak-general-business*是Oak框架的一个基础业务逻辑包,在其中按照Oak框架的规范,实现了大量较为底层的功能逻辑,如用户管理、应用管理、授权管理、消息管理,文件管理等等。关于oak-general-business包的详细介绍请参见:todo。在这里,我们只需要了解*Application*和*System*这两个对象的意义和大概结构,在*oak-general-business*中,*System*代表一个业务系统(就是你正在开发的这个业务系统),而*Application*代表业务系统的一个前端应用。两者的数据结构大致如下(代码来源于```oak-general-business/src/entities```,为了简化去掉了一些属性):
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
# 组织组件
|
||||
在上一小节的例子中,我们已经展示了如何利用相应路径关系来表达组件之间的逻辑关系,从而有效组织组件。在这一小节我们将详细描述如何组织组件。
|
||||
|
||||
### 组件树
|
||||
在oak框架中,每一个组件都被映射到一颗“组件树”上的结点。当然这里用“树”并不准确,其实这些结点之间是一片“森林”,每个树代表一个页面。树的根结点就是Page组件。
|
||||
|
||||
每个组件在初始化时,建立其组件树上的结点,其结点的位置由*oakPath*这个属性所决定。因此,所有的关联到*Entity*上的组件都需要设置其*oakPath*。唯一的例外是根结点也就是Page页面,它们的*oakPath*是在初始化时由框架自动编译形成的,其值就等于它们在src/pages目录下的相对路径。
|
||||
|
||||
当组件在组件树上构建完成后,框架会给其标定一个它的路径,这个属性就是前面经常用到的*oakFullpath*。因此,当在一个组件中包含其它子组件时,通过像这样设置其*oakPath*即可告诉组件树,将它关联到自己的子结点中,相对路径就是*relative*:
|
||||
```typescript
|
||||
oakPath={`${oakFullpath}.${relative}`}
|
||||
```
|
||||
当然,这里的*relative*并不能随便编写。如果父组件和子组件都有相关的Entity,它需要正确表达父子Entity之间的关系,您可以阅读并参见[相关章节](./1-1.md)。如果父组件是一个*Virtual*组件(不关联在任何Entity上),而子组件是一个*Entity*组件,则相对路径应该是:
|
||||
|
||||
* 如果子组件是一个List组件,则relative为 ```${Entity}s```,Entity是子组件所属对象;
|
||||
* 如果子组件是一个Detail/Upsert组件,则relative为```Entity```,Entity是子组件所属对象;
|
||||
|
||||
### 父子组件的关系
|
||||
如果组件和其子组件都是Entity组件,则它们之间有三种典型的关系:
|
||||
|
||||
* 一对一:父亲是一个*detail/upsert*组件,其子组件也是一个*detail/upsert*组件。例如:假设我们设计了一个*Application*的详情页面,其中也要渲染其关联的*System*信息,则可以分成两个组件编写,并通过设置```<SystemDetail oakPath={`${oakFullpath}.system}` />```来指定相对关系。
|
||||
> 要注意,组件之间的一对一关系,代表的其实是对象之间的多对一关系。
|
||||
* 一对多:父亲是一个*detail/upsert*组件,其子组件是一个*list*组件。在[上一小节](2-3-3.md)的例子里就是这种关系,一个*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*,就可以实现共享路径和组件树上结点的目标:
|
||||
```typescript
|
||||
oakPath={oakFullpath}
|
||||
```
|
||||
|
||||
##### 父组件没有实际逻辑
|
||||
一种常见的情况是父组件为Page,引用子组件进行渲染。由于取数和渲染逻辑都写在子组件中,父组件只是一层wrapper。此时如果将父组件设计为Virtual组件过于臃肿,因此可以在声明父组件时不声明projection,并使父子组件共享同一路径,此时框架会在子组件渲染时才进行组件树的取数逻辑。
|
||||
|
||||
##### 子组件是抽象组件
|
||||
有些子组件是抽象组件,并不限定在某一具体*Entity*上。这类组件在使用时,和其父组件可以通过共享路径来实现这种抽象逻辑。例如,在*oak-frontend-base*库中,提供了诸如*list/detail/filter*等抽象类组件,像*list*组件可以渲染出一个对象列表,在使用时就要声明其*oakPath*和当前(List)结点的*oakFullpath*一致。
|
||||
|
||||
##### 几个子组件是对同一对象的更新
|
||||
在有些设计中,由于对某一对象的某类操作需要涉及更新大量的属性,在设计时会采用共享路径的方式,使他们的更新被缓存在同一个组件树结点之上。像下面这样的步骤条设计中,多个步骤条下的子组件如果是更新同行数据,它们的oakPath可以设置成一致。
|
||||
|
||||

|
||||
|
||||
### 绝对路径
|
||||
在有的情况下,某个组件的渲染需要引用另一个对象的组件,而它们(所属的对象)之间并没有直接的关联关系(请注意,这种情况你应该首先检查一下,你的设计是否合理),此时无法通过相对路径来标定两者之间的绝对关系,此时我们可以为该子组件设置一条绝对路径:
|
||||
|
||||
```typescript
|
||||
oakPath="ogb-system-upsert-application-upsert"
|
||||
```
|
||||
绝对路径的命名应当稍微复杂一点,以保持其在全局唯一(可以用*包名+当前组件路径名+子组件路径名来进行命名)。
|
||||
|
||||
一个子组件如果使用了绝对路径,它和父组件之间就没有任何关联关系。父组件的*refresh/execute*都不会与之发生任何联系。
|
||||
|
|
@ -0,0 +1 @@
|
|||
# 组件上的数据和方法
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Loading…
Reference in New Issue