2-3
This commit is contained in:
parent
e051d1b491
commit
c9d9e2c6cd
|
|
@ -6,6 +6,12 @@
|
||||||
- [创建]()
|
- [创建]()
|
||||||
- [开发](./chapter1/dev.md)
|
- [开发](./chapter1/dev.md)
|
||||||
- [部署](./chapter1/deploy.md)
|
- [部署](./chapter1/deploy.md)
|
||||||
- [框架结构](./chapter2/def.md)
|
- [编写应用](./chapter2/def.md)
|
||||||
- [entities](./chapter2/entities.md)
|
- [编写对象](./chapter2/1.md)
|
||||||
- [pages/components](./chapter2/components.md)
|
- [编写组件/页面](./chapter2/2.md)
|
||||||
|
- [三层架构](./chapter2/2-1.md)
|
||||||
|
- [目录文件结构](./chapter2/2-2.md)
|
||||||
|
- [编写组件](./chapter2/2-3.md)
|
||||||
|
- [编写详情组件](./chapter2/2-3-1.md)
|
||||||
|
- [编写更新组件](./chapter2/2-3-2.md)
|
||||||
|
- [编写列表组件](./chapter2/2-3-3.md)
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
# 编译项目数据结构
|
# 编译项目Entity
|
||||||
使用Oak框架的项目,需要将项目中数据结构的定义(src/entities目录下),编译成为Oak框架能使用的完整定义(src/oak-app-domain目录下)。当开发人员
|
使用Oak框架的项目,需要将项目中对象结构(*Entity*)的定义(src/entities目录下),编译成为Oak框架能使用的完整定义(src/oak-app-domain目录下)。当开发人员
|
||||||
修改了相关数据结构后,也必须执行下述命令:
|
修改了相关*Entity*后,也必须执行下述命令:
|
||||||
|
|
||||||
```nodejs
|
```nodejs
|
||||||
npm run make:domain
|
npm run make:domain
|
||||||
```
|
```
|
||||||
项目的数据结构编写规范参见:[entities](../chapter2/entities.md)
|
项目的*Entity*编写规范参见:[entities](../chapter2/entities.md)
|
||||||
|
|
||||||
# 开发(前台模式)
|
# 开发(前台模式)
|
||||||
在Oak框架的设计中,前后端并无区分。在开发模式下,可以将后端逻辑直接运行在前端环境中,使开发过程更加简洁高效。
|
在Oak框架的设计中,前后端并无区分。在开发模式下,可以将后端逻辑直接运行在前端环境中,使开发过程更加简洁高效。
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
# 基础知识
|
# 基础知识
|
||||||
|
|
||||||
* Oak使用Typescript语言,因此您需要基本掌握:
|
* Oak使用Typescript语言,因此您需要提前掌握以下知识:
|
||||||
1. javascript语言基础: [学习资料](https://developer.mozilla.org/zh-CN/docs/learn/JavaScript)
|
1. javascript语言基础: [学习资料](https://developer.mozilla.org/zh-CN/docs/learn/JavaScript)
|
||||||
2. Nodejs环境: [学习资料](http://nqdeng.github.io/7-days-nodejs/) [官方文档](https://nodejs.org/docs/latest/api/)
|
2. Nodejs环境: [学习资料](http://nqdeng.github.io/7-days-nodejs/) [官方文档](https://nodejs.org/docs/latest/api/)
|
||||||
3. Typescript语言基础: [官方文档](https://www.typescriptlang.org/)
|
3. Typescript语言基础: [官方文档](https://www.typescriptlang.org/)
|
||||||
|
|
||||||
* 在前端,Oak目前使用React作为网页端框架(尽管这不是必须,但由于团队技术力量等原因,短期内没有计划去适配vue等其它框架),因此您也需要掌握React的一些基本概念。如果您需要开发App或者小程序,也需要去了解一些其相关概念。
|
* 在前端,Oak目前使用React作为网页端框架(尽管这不是必须,但由于团队技术力量等原因,短期内并没有计划去适配vue等其它框架),因此您也需要掌握React的一些基本概念。如果您需要开发App或者小程序,也需要去了解一些Oak所采用的技术本的相关技术。
|
||||||
1. React: [官方站点](https://react.dev/)
|
1. React: [官方站点](https://react.dev/)
|
||||||
2. React-native [官方站点](https://reactnative.dev/)
|
2. React-native [官方站点](https://reactnative.dev/)
|
||||||
3. 微信小程序开发 [官方站点](https://developers.weixin.qq.com/miniprogram/dev/framework/)
|
3. 微信小程序开发 [官方站点](https://developers.weixin.qq.com/miniprogram/dev/framework/)
|
||||||
|
|
||||||
对于其它更多的前端环境,Oak也将在未来进行适配。Oak的前端技术路线请参见:todo
|
对于其它更多的前端环境,Oak也将在未来进行适配。Oak的前端技术路线介绍请参见:[目录文件结构](../chapter2/2-2.md)。
|
||||||
|
|
||||||
|
> 对于新手开发者,可能对上述这么多的前置知识学习感到望而生畏。没关系,理论上只要了解并掌握基础概念即可进行开发,更多的技术细节可以在开发过程中再不断学习补充。
|
||||||
|
|
||||||
# 开发环境
|
# 开发环境
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Entities
|
# 编写对象
|
||||||
|
|
||||||
## 对象和属性
|
## 对象和属性
|
||||||
Entity在Oak框架中代表一个实体**对象**,应用开发首先应将需求分解,并设计出最基础的对象及相对应的关系,并在*src/entities*目录中对之加以定义。
|
Entity在Oak框架中代表一个实体**对象**,应用开发首先应将需求分解,设计出最基础的对象及相对应的关系,并在*src/entities*目录中对之加以定义。
|
||||||
|
|
||||||
|
|
||||||
一个对象拥有若干**属性**,在Entity定义文件中,可以像以下这样来定义对象及其属性。我们引用的代码是*oak-general-business*包当中的*Address*对象,它代表着一个地址。
|
一个对象拥有若干**属性**,在Entity定义文件中,可以像以下这样来定义对象及其属性。我们引用的代码是*oak-general-business*包当中的*Address*对象,它代表着一个地址。
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
# pages/components
|
# 三层架构
|
||||||
设计完Entity后,即可直接进入应用页面的编写(Oak框架会自动处理发请求、取数据、缓存等一系列工作,不需要你再编写任何一行相关代码了😁)。
|
|
||||||
|
|
||||||
## 设计理念
|
在Oak框架中,前台组织可以由高到低分为三个层次,如下图所示:
|
||||||
在Oak框架中,前台组织可以分为三个层次,如下图所示:
|
|
||||||

|

|
||||||
|
|
||||||
#### namespace
|
#### namespace
|
||||||
*namespace*是指应用在最顶层被划分成几个命名空间,每个命名空间中包含若干页面,命名空间一般按路由划分,而同一个命名空间中的布局是相同的。例如,一个典型的网站会分为*front*和*console*两个命名空间,分别代表普通用户访问的前端和管理人员访问的控制台。前者有统一的*header(页头)*和*footer(页脚)*,而后者还会有统一的*menu(菜单)*。这些跨页面级别的组件摆放是在命名空间里处理的。
|
*namespace*是指应用在最顶层被划分成几个命名空间,每个命名空间中包含若干页面,命名空间一般按顶层路由来划分,同一个命名空间中的整体布局是相同的。例如,一个典型的网站会分为*frontend*和*console*两个命名空间,分别代表普通用户访问的前端和管理人员访问的控制台。前者有统一的*header(页头)*和*footer(页脚)*,而后者还会有统一的*menu(菜单)*。这些跨页面级别的组件摆放是在命名空间里处理的。
|
||||||
|
|
||||||
一般而言一个应用不会有过多的命名空间,命名空间被放置在*web/src/app/namespaces*目录下,命名空间的路由就是由目录名称决定的。
|
一般而言一个应用不会有过多的命名空间,命名空间被放置在*web/src/app/namespaces*目录下,命名空间的路由就是由目录名称决定的。
|
||||||
|
|
||||||
|
|
@ -28,12 +26,12 @@
|
||||||
|
|
||||||
#### page
|
#### page
|
||||||
|
|
||||||
一个*page*是前端的一个页面。*page*编写在*src/pages/${namespace\}*目录下,其路由和目录名保持相同。例如,在*src/pages/frontend/home*目录下的页面就会匹配到```/home```或```/```(参见上一小节中frontend命名空间的配置)。
|
一个*page*是前端的一个页面。*page*编写在*src/pages/${namespace\}*目录下,每一个页面对应目录下的一个子目录。其路由和目录名保持相同。例如,在*src/pages/frontend/home*目录下的页面就会匹配到```/home```或```/```(参见上一小节中frontend命名空间的配置)。
|
||||||
|
|
||||||
虽然并非强制,但推荐在*src/pages/${namespace\}*下的page目录的第一层用该页面相关的entity名称命名,第二层可以用*list/detail/upsert*之一,或者该entity的某个Action来加以命名(如果页面的功能就是对此entity进行此action的话),这样此页面的功能看上去就一目了然。当然,对于像首页这样的复合性页面(并不是限定在某个entity上),您可以自由对之进行命名,只要这个命名让用户看上去很容易理解。
|
虽然并非强制,但推荐在*src/pages/${namespace\}*下的page目录的第一层用该页面相关的entity名称命名,第二层可以用*list/detail/upsert*之一,或者该entity的某个Action来加以命名(如果页面的功能就是对此entity进行此action的话),这样此页面的功能看上去就一目了然。当然,对于像首页这样的复合性页面(并不是限定在某个entity上),您可以自由对之进行命名,只要这个命名让用户看上去很容易理解。
|
||||||
|
|
||||||
#### component
|
#### component
|
||||||
一个*component*代表一个固定功能的组件。编写组件的目的是:
|
一个*component*代表一个固定功能的组件。组件编写在*src/component*目录下,同样是一个子目录代表一个组件。编写组件的目的是:
|
||||||
|
|
||||||
1. 为了在多个*page*之间复用代码
|
1. 为了在多个*page*之间复用代码
|
||||||
2. 避免*page*过于复杂
|
2. 避免*page*过于复杂
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# 目录文件结构
|
||||||
|
在*src/pages*和*src/components*目录下,您可以编写应用页面和组件了。每个页面/组件都占据着唯一的子目录,在子目录下可能有如下若干文件:
|
||||||
|
|
||||||
|
文件名 | 作用
|
||||||
|
------------|----------------
|
||||||
|
index.ts | 定义页面/组件的逻辑(必需)
|
||||||
|
index.json | 与微信小程序的index.json作用相同
|
||||||
|
locales/zh-CN.json | 页面/组件的i18n内容
|
||||||
|
web.pc.tsx | 宽屏的html渲染
|
||||||
|
web.tsx | 窄屏的html渲染
|
||||||
|
web.pc.module.less | 宽屏样式
|
||||||
|
web.module.less | 窄屏样式
|
||||||
|
index.xml | 小程序渲染
|
||||||
|
index.less | 小程序样式
|
||||||
|
render.native.tsx | App渲染
|
||||||
|
render.native.module.less | App渲染样式
|
||||||
|
render.ios.tsx | ios渲染
|
||||||
|
render.android.tsx | android渲染
|
||||||
|
|
||||||
|
看上去有些复杂,但是实际上绝大多数项目都只会实现其中的部分文件。Oak框架在前端采取了一个较为保守的方案,以应对跨前端的一致性问题。其中心思想是:<u>**对页面的逻辑统一抽象(index.ts),而对页面的具体渲染则分别处理。**</u>
|
||||||
|
|
||||||
|
将前端各平台的渲染语法强行统一到一个框架之下(如taro, uniapp)的前景固然美好,但这会带来兼容性和扩展性的严重问题,同时也会造成与开源社区的割裂。这显然与Oak框架的设计目标背道而驰。因此Oak框架借鉴了微信小程序和React的设计思想,将页面的各种与渲染无关的逻辑部分抽象到index.ts当中,而将各个平台的渲染代码分割到各个文件之中,在对应平台下运行的时候进行加载,从而达到一致性和兼容性的较好平衡。
|
||||||
|
|
||||||
|
## index.ts
|
||||||
|
在index.ts中,定义了本页面/组件的逻辑代码,此文件中应当避免引用任何平台相关的特性内容,而只关注于页面的逻辑能力。关于index.ts的编写,请参见[页面逻辑层](2-3.md)。
|
||||||
|
|
||||||
|
如果确实需要引用平台相关的特性内容,可以通过process.env.OAK_PLATFORM环境变量加以区别。例如,在支付时如果需要唤起平台的支付接口,可以像下面这样编写(代码引用自oak-pay-business/src/components/pay/detail/index.ts):
|
||||||
|
```typescript
|
||||||
|
if (process.env.OAK_PLATFORM === 'wechatMp') {
|
||||||
|
const { prepayMeta } = meta as { prepayMeta: WechatMiniprogram.RequestPaymentOption };
|
||||||
|
if (prepayMeta) {
|
||||||
|
const result = await wx.requestPayment(prepayMeta);
|
||||||
|
process.env.NODE_ENV === 'development' && console.log(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
features.message.setMessage({
|
||||||
|
type: 'error',
|
||||||
|
content: features.locales.t('startPayError.illegaPayData'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
features.message.setMessage({
|
||||||
|
type: 'error',
|
||||||
|
content: features.locales.t('startPayError.falseEnv', { env: 'wechatMp' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
在这里,当页面调用了支付事件时,如果判断当前环境是小程序,则调用wx.requestPayment方法唤起支付。
|
||||||
|
|
||||||
|
## web端
|
||||||
|
如果您的应用是基于web端,只要在目录下编写web.pc.tsx和web.tsx,框架会在宽屏下自动载入前者,而在窄屏下自动载入后者。在tsx文件中,您可以按照React的规则使用任何您想用的组件,也可以使用useState等钩子函数。
|
||||||
|
|
||||||
|
> 如果您只建立了web.pc.tsx或web.tsx,则框架会在宽屏和窄屏下都使用这一文件渲染。这个判断建立在第一次编译项目之时,因此如果您在后面又加入了另一个文件,需要行执行npm run clean:cache清除掉编译缓存才能生效
|
||||||
|
|
||||||
|
## 微信小程序端
|
||||||
|
微信小程序端只需要编写index.xml,其语法和wxml完全一致,您也可以在index.json中的usingComponent域中声明所引用的其它组件。
|
||||||
|
|
||||||
|
## App端
|
||||||
|
Oak框架的App端基于react-native,您只需要编写render.native.tsx,则可生成在IOs和Android下通用的页面。当然,与react-native的编译规则类似,您也可以编写render.ios.tsx或者render.android.tsx,分别在两个操作系统之下进行渲染。同样的,您可以在tsx文件中引用任何您想使用的第三方组件。
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
# 编写详情组件
|
||||||
|
> 本组件代码可参看*oak-generail-buiness/src/components/system/detail*
|
||||||
|
|
||||||
|
现在,我们需要有一个组件对本System的信息进行读取并显示。在这里注意,在很多情况下,访问对象的过程是先get list获得满足条件的列表,再对其中的某一条数据进行get detail。但System对象比较特殊,它代表着当前正在访问的业务系统,因此不需要通过先设计list组件去查询其id,而是通过其它方法获得。我们先不关心这一过程具体如何实现,假设当前System的id已经得知。
|
||||||
|
|
||||||
|
### 逻辑层(index.ts)
|
||||||
|
在index.ts逻辑层,我们直接通过定义要访问的这条System数据的相关属性来获取它,代码大致如下:
|
||||||
|
```typescript
|
||||||
|
export default OakComponent({
|
||||||
|
isList: false,
|
||||||
|
Entity: 'system',
|
||||||
|
projection: {
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
config: 1,
|
||||||
|
description: 1,
|
||||||
|
super: 1,
|
||||||
|
folder: 1,
|
||||||
|
},
|
||||||
|
formData({ data }) {
|
||||||
|
return data || {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
|
代码非常简洁直观,我们通过调用OakComponet来定义一个组件,组件访问的对象是*System*;此组件不是一个List组件,这意味着需要告知此组件要访问的数据行的id是多少(这是通过向组件传入*OakId*来实现的);在*projection*中定义要访问这行数据的属性有哪些,这里的属性明显和上面*System*对象的定义是保持一致的;最后在formData方法中,将取到的数据属性返回(提供给渲染层)。
|
||||||
|
|
||||||
|
通过这几行短短的代码,我们就已经实现了将一条System数据从后台取到前台的功能,接下来我们来渲染它。
|
||||||
|
|
||||||
|
### 渲染页面(web.pc.tsx)
|
||||||
|
在web.pc.tsx中,我们像下面这样来编写代码:
|
||||||
|
```typescript
|
||||||
|
import React from 'react';
|
||||||
|
import { Tabs } from 'antd';
|
||||||
|
import { WebComponentProps } from 'oak-frontend-base';
|
||||||
|
|
||||||
|
export default function Render(props: WebComponentProps<EntityDict, 'system', false, {
|
||||||
|
id: string;
|
||||||
|
config: Config;
|
||||||
|
name: string;
|
||||||
|
style: Style;
|
||||||
|
}>) {
|
||||||
|
const { id, config, oakFullpath, name, style } = props.data;
|
||||||
|
const { t } = props.methods;
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
return (
|
||||||
|
// 利用name、stype等属性进行页面渲染
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
在*Render*函数的唯一参数*props*中,Oak框架将以*WebComponentProps*定义的格式,注入了两个属性:
|
||||||
|
|
||||||
|
* data: 在data中存放从逻辑层传递过来的数据,以及Oak框架的一些通用变量(例如上面代码中的*oakFullpath*,这类变量都以*Oak*开头)。从逻辑层传递过来的数据则包括:
|
||||||
|
* formData中返回的数据(在上面的例子里,是从所取到的system行当中展开的属性)
|
||||||
|
* data和property中声明的数据。
|
||||||
|
* methods: 在methods中存放从逻辑层注入的方法,以及Oak框架注入的一些通用方法(例如上面代码当中的*t*方法,就是框架注入的i18n方法)。其中,组件自身逻辑层注入的方法声明在methods属性中。
|
||||||
|
|
||||||
|
### 使用组件
|
||||||
|
下面需要将上面的组件嵌入到系统的某一个页面当中。如果您在初始化项目时依赖了*oak-general-business*库,则这个组件已经被*pages/console/system/config*页面所引用。
|
||||||
|
|
||||||
|
在该目录下的web.pc.tsx中,可以看到如下代码(已进行简化):
|
||||||
|
```typescript
|
||||||
|
import React from 'react';
|
||||||
|
import SystemPanel from 'oak-general-business/es/components/system/panel';
|
||||||
|
export default function Render(
|
||||||
|
props: WebComponentProps<
|
||||||
|
EntityDict,
|
||||||
|
'system',
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
systemId: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
const { systemId, oakFullpath } = props.data;
|
||||||
|
if (oakFullpath) {
|
||||||
|
return (
|
||||||
|
<SystemPanel
|
||||||
|
oakId={systemId}
|
||||||
|
oakPath={`${oakFullpath}.system`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
在这个页面上,我们将渲染*oak-general-business*所提供的*system/panel*组件(上面描述的*system/detail*组件是*panel*中的一个tab页,见下图),并传入了两个非常重要的参数:
|
||||||
|
|
||||||
|
* oakId:对于非List页面,都需要传入该行的ID,这里我们将当前的systemId传入(至于从哪里取到这个id的,可以参见同目录下的index.ts,在此不作展开)
|
||||||
|
* oakPath:对于所有关联Entity的组件,都需要指出它们在页面中的“路径”。在这个例子里,我们将*SystemPanel*组件放置在当前组件(其实是页面,其路径由*oakFullpath*所指代)的*system*子路径下。
|
||||||
|
|
||||||
|
关于页面和组件的“路径”规范及意义,我们将在todo章节详细解释。
|
||||||
|
|
||||||
|
现在,执行```npm run start:web```,运行项目后,进入 ```localhost:3000/console/system/config``` 页面,可以看到类似下面的效果:
|
||||||
|

|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
# 编写更新组件
|
||||||
|
|
||||||
|
在上一节,我们描述了如何编写*System*的详情组件,接下来我们描述如何编写一个*System*的更新组件。代码见*oak-general-business/src/components/system/upsert*目录。
|
||||||
|
|
||||||
|
更新组件也是针对对象的单行进行操作,因此,其*index.ts*和详情组件没有太大区别,如果我们查看代码,会发现在这里没有声明projection。
|
||||||
|
```typescript
|
||||||
|
export default OakComponent({
|
||||||
|
isList: false,
|
||||||
|
entity: 'system',
|
||||||
|
formData({ data }) {
|
||||||
|
return data || {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
可以这样做的原因,是我们在detail组件中将引用此upsert组件,并声明它的oakPath和detail的oakFullpath一致。这样两个组件就将共享路径上的结点,可以直接使用detail声明的projection。
|
||||||
|
|
||||||
|
### 更新数据
|
||||||
|
我们重点来看如何更新数据。在*web.tsx*文件中,可以看到如下代码:
|
||||||
|
```typescript
|
||||||
|
import React from 'react';
|
||||||
|
import { Form, Switch, Input } from 'antd';
|
||||||
|
|
||||||
|
import { EntityDict } from '../../../oak-app-domain';
|
||||||
|
import { WebComponentProps } from 'oak-frontend-base';
|
||||||
|
|
||||||
|
export default function Render(
|
||||||
|
props: WebComponentProps<
|
||||||
|
EntityDict,
|
||||||
|
'system',
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
folder: string;
|
||||||
|
super: boolean;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
folder,
|
||||||
|
super: super2,
|
||||||
|
} = props.data;
|
||||||
|
const { t, update } = props.methods;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
colon={true}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
wrapperCol={{ span: 16 }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={t('system:attr.name')}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
onChange={(e) => {
|
||||||
|
update({
|
||||||
|
name: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
value={name}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t('system:attr.desc')}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
onChange={(e) => {
|
||||||
|
update({
|
||||||
|
description: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
value={description}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t('system:attr.isSuper')}
|
||||||
|
required
|
||||||
|
tooltip={t('tips.isSuper')}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checkedChildren={t('common::yes')}
|
||||||
|
unCheckedChildren={t('common::no')}
|
||||||
|
checked={super2}
|
||||||
|
onChange={(checked) => {
|
||||||
|
update({
|
||||||
|
super: checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
在这里,我们利用了antd当中的部分输入组件,并在props.methods中引出了一个框架提供的*update*方法,这个方法可以将更新的属性记录在当前结点之中。要注意的是,当有更新数据时,formData函数所取到的数据项*data*就是更新后的数据。
|
||||||
|
|
||||||
|
|
||||||
|
### 提交更新
|
||||||
|
通过上面的代码,我们已经可以更新某一对象的数据项了。如何将更新提交呢?可以使用框架提供的*execute*方法。
|
||||||
|
|
||||||
|
在这个例子中,我们并没有把*execute*交给*system/upsert*组件来执行,这是因为如果将提交按钮放在此组件上,会降低此组件的复用性(在其它页面或组件上,System的upsert可能只是一个子对象比如Application upsert的一部分,如果您现在还不能理解这个也没有关系,先跳过这一段即可)。
|
||||||
|
|
||||||
|
因此我们将提交动作放在*system/detail*组件的代码中,这部分代码大致如下:
|
||||||
|
```typescript
|
||||||
|
export default function Render(
|
||||||
|
props: WebComponentProps<
|
||||||
|
EntityDict,
|
||||||
|
'platform',
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
oakId: string;
|
||||||
|
folder: string;
|
||||||
|
super: boolean;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
const { oakId, folder, name, description, 'super': isSuper, oakFullpath, oakExecutable, oakExecuting } = props.data;
|
||||||
|
const { t, execute, clean } = props.methods;
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
if (oakFullpath) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onCancel={() => {
|
||||||
|
clean();
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
width={500}
|
||||||
|
footer={
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
// type='primary'
|
||||||
|
onClick={async () => {
|
||||||
|
clean();
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
disabled={oakExecuting}
|
||||||
|
>
|
||||||
|
{t('common::action.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={async () => {
|
||||||
|
await execute();
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
oakExecutable !== true || oakExecuting
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('common::action.confirm')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={Styles.upsert}>
|
||||||
|
<SystemUpsert oakId={oakId} oakPath={oakFullpath} />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
...
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
{t('common::action.update')}
|
||||||
|
</Button>
|
||||||
|
...
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
在上述代码中,当点击“更新”按钮时,就弹出一个Modal对话框,Upsert组件放置在对话框中,并且其oakPath与当前组件保持一致(在宽屏下这是一个常见的设计模式)。当对话框的确定按钮被点击时,则触发*execute*行为,此时本结点(以及子孙结点)上所有的更新会被执行,因此这是一个异步动作。当执行完成后,Modal才被关闭。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
你可能会注意到,在上述例子中,有好几个框架级别的变量被使用,比如:*OakExecuting*表示是否在更新中,*oakExecutable*表示更新是否可行。关于系统有哪些变量,可以查阅todo章节。
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# 编写列表组件
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# 组件编写
|
||||||
|
定义完*Entity*后,可以马上开始编写组件/页面(以下统一用“组件”作为主语)。
|
||||||
|
|
||||||
|
在编写组件前,首先要明确一个概念:用Oak框架编写业务应用,其中几乎所有的组件和绝大部分页面都关联在某一个*Entity*之上,实现对此实体的增删改查的相关功能。根据多年的项目经验,我们把某个组件对*Entity*的操作划分为以下三种类型:
|
||||||
|
* **Get List**:列表页,查询并显示此*Entity*的多条数据
|
||||||
|
* **Get Detail**:详情页,查询并显示此*Entity*的(指定id的)某条数据
|
||||||
|
* **Upsert**:更新页,查询并对此*Entity*的(指定id的)某条数据进行更新,或者创建一条新的数据
|
||||||
|
|
||||||
|
下面将以oak-general-business包当中的System和Application这两个Entity作为示例,介绍如何实现组件逻辑层。
|
||||||
|
|
||||||
|
oak-general-business是Oak框架的一个基础业务逻辑包,在其中按照Oak框架的规范,实现了大量较为底层的功能逻辑,如用户管理、应用管理、授权管理、消息管理,文件管理等等。关于oak-general-business包的详细介绍请参见:todo。在这里,我们只需要了解Application和System这两个对象的意义和大概结构,在oak-general-business中,System代表一个业务系统(就是你要开发的这个业务系统),而Application代表业务系统的一个前端应用。两者的数据结构大致如下(代码来源于oak-general-business/src/entities,为了简化去掉了一些业务类属性):
|
||||||
|
|
||||||
|
* Application
|
||||||
|
```typescript
|
||||||
|
export interface Schema extends EntityShape {
|
||||||
|
name: String<32>;
|
||||||
|
description: Text;
|
||||||
|
type: AppType;
|
||||||
|
system: System;
|
||||||
|
config: WebConfig | WechatMpConfig | WechatPublicConfig | NativeConfig;
|
||||||
|
style?: Style;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
* System
|
||||||
|
```typescript
|
||||||
|
export interface Schema extends EntityShape {
|
||||||
|
name: String<32>;
|
||||||
|
description: Text;
|
||||||
|
config: Config;
|
||||||
|
super?: Boolean;
|
||||||
|
style?: Style;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
在系统上线前,您需要配置相应的Application和System,例如小程序appId、网站域名之类的属性。在这里我们并不过多介绍这两个对象的作用,只需要知道,Oak应用本身也是通过这两个Entity来进行管理的,因此超级管理员需要对它们进行相应的管理操作,我们接下来就结合代码描述这些相关组件的实现。
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# 编写组件/页面
|
||||||
|
设计完Entity后,您可立即进入应用页面的编写(Oak框架会自动处理发请求、取数据、缓存等一系列工作,不需要你再编写任何一行相关代码了😁)。
|
||||||
|
|
||||||
|
- [三层架构](./2-1.md)
|
||||||
|
- [目录文件结构](2-2.md)
|
||||||
|
- [编写组件](2-3.md)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 目录结构
|
# 项目目录结构
|
||||||
|
|
||||||
一个典型的Oak项目的主要目录结构如下:
|
一个典型的Oak项目的主要目录结构如下:
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Loading…
Reference in New Issue