This commit is contained in:
Xu Chang 2024-06-30 21:39:16 +08:00
parent e051d1b491
commit c9d9e2c6cd
14 changed files with 415 additions and 20 deletions

View File

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

View File

@ -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框架的设计中前后端并无区分。在开发模式下可以将后端逻辑直接运行在前端环境中使开发过程更加简洁高效。

View File

@ -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)。
> 对于新手开发者,可能对上述这么多的前置知识学习感到望而生畏。没关系,理论上只要了解并掌握基础概念即可进行开发,更多的技术细节可以在开发过程中再不断学习补充。
# 开发环境 # 开发环境

View File

@ -1,7 +1,7 @@
# Entities # 编写对象
## 对象和属性 ## 对象和属性
Entity在Oak框架中代表一个实体**对象**,应用开发首先应将需求分解,设计出最基础的对象及相对应的关系,并在*src/entities*目录中对之加以定义。 Entity在Oak框架中代表一个实体**对象**,应用开发首先应将需求分解,设计出最基础的对象及相对应的关系,并在*src/entities*目录中对之加以定义。
一个对象拥有若干**属性**在Entity定义文件中可以像以下这样来定义对象及其属性。我们引用的代码是*oak-general-business*包当中的*Address*对象,它代表着一个地址。 一个对象拥有若干**属性**在Entity定义文件中可以像以下这样来定义对象及其属性。我们引用的代码是*oak-general-business*包当中的*Address*对象,它代表着一个地址。

View File

@ -1,12 +1,10 @@
# pages/components # 三层架构
设计完Entity后即可直接进入应用页面的编写Oak框架会自动处理发请求、取数据、缓存等一系列工作不需要你再编写任何一行相关代码了😁
## 设计理念 在Oak框架中前台组织可以由高到低分为三个层次如下图所示
在Oak框架中前台组织可以分为三个层次如下图所示
![层次结构](./components.png) ![层次结构](./components.png)
#### 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*过于复杂

61
src/chapter2/2-2.md Normal file
View File

@ -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文件中引用任何您想使用的第三方组件。

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

@ -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``` 页面,可以看到类似下面的效果:
![systemPanel](systemPanel.png)

186
src/chapter2/2-3-2.md Normal file
View File

@ -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才被关闭。
![system upsert](systemUpsert.png)
你可能会注意到,在上述例子中,有好几个框架级别的变量被使用,比如:*OakExecuting*表示是否在更新中,*oakExecutable*表示更新是否可行。关于系统有哪些变量可以查阅todo章节。

2
src/chapter2/2-3-3.md Normal file
View File

@ -0,0 +1,2 @@
# 编写列表组件

35
src/chapter2/2-3.md Normal file
View File

@ -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来进行管理的因此超级管理员需要对它们进行相应的管理操作我们接下来就结合代码描述这些相关组件的实现。

6
src/chapter2/2.md Normal file
View File

@ -0,0 +1,6 @@
# 编写组件/页面
设计完Entity后您可立即进入应用页面的编写Oak框架会自动处理发请求、取数据、缓存等一系列工作不需要你再编写任何一行相关代码了😁
- [三层架构](./2-1.md)
- [目录文件结构](2-2.md)
- [编写组件](2-3.md)

View File

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