This commit is contained in:
Xu Chang 2024-07-08 20:39:50 +08:00
parent 0a896349f7
commit 043c05addc
10 changed files with 112 additions and 18 deletions

View File

@ -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)

1
src/chapter2/1-1.md Normal file
View File

@ -0,0 +1 @@
# 对象之间的关系

View File

@ -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*页面所引用。

View File

@ -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(
![system upsert](systemUpsert.png)
你可能会注意到,在上述例子中,有好几个框架级别的变量被使用,比如:*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({
...
});
...
```

View File

@ -136,4 +136,4 @@ export type Schema = EntityShape & {
用了这样的定义相当于告诉框架“此applicationList组件所查询的*Application*,是关联在其父结点的*System*对象上(框架会要求其查询数据时符合```systemId = 父system.id```的约束)。
当你理解了上面这个例子,就会意识到,绝大多数的页面中的组件关系,和它们所属的对象关系是完全对应的。通过仔细划分这些对象到子组件上,可以让整体代码组织的相当优雅和易于维护。在下一章节中,我们将进一步描述这种关系的细节。
当你理解了上面这个例子,就会意识到,绝大多数的页面中的组件关系,和它们所属的对象关系是完全对应的。通过仔细划分这些对象到子组件上,可以让整体代码组织的相当优雅,并且易于重用和维护。在下一章节中,我们将进一步描述这种关系的细节。

View File

@ -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}`} />```

View File

@ -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```,为了简化去掉了一些属性):

69
src/chapter2/2-4.md Normal file
View File

@ -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可以设置成一致。
![步骤条](steps.jpg)
### 绝对路径
在有的情况下,某个组件的渲染需要引用另一个对象的组件,而它们(所属的对象)之间并没有直接的关联关系(请注意,这种情况你应该首先检查一下,你的设计是否合理),此时无法通过相对路径来标定两者之间的绝对关系,此时我们可以为该子组件设置一条绝对路径:
```typescript
oakPath="ogb-system-upsert-application-upsert"
```
绝对路径的命名应当稍微复杂一点,以保持其在全局唯一(可以用*包名+当前组件路径名+子组件路径名来进行命名)。
一个子组件如果使用了绝对路径,它和父组件之间就没有任何关联关系。父组件的*refresh/execute*都不会与之发生任何联系。

1
src/chapter2/2-5.md Normal file
View File

@ -0,0 +1 @@
# 组件上的数据和方法

BIN
src/chapter2/steps.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB