commit e051d1b49148ff4e7eb98c795852785299ff8dc8 Author: Xc Date: Thu Jun 20 09:39:23 2024 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0dbe8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +book + +**.pptx \ No newline at end of file diff --git a/1.bmp b/1.bmp new file mode 100644 index 0000000..23f2a37 Binary files /dev/null and b/1.bmp differ diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..aa8ccfc --- /dev/null +++ b/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Xc"] +language = "en" +multilingual = false +src = "src" +title = "Oak Framework 教程" diff --git a/src/SUMMARY.md b/src/SUMMARY.md new file mode 100644 index 0000000..ba49b44 --- /dev/null +++ b/src/SUMMARY.md @@ -0,0 +1,11 @@ +# Summary + +- [Oak 框架简介](./intro.md) +- [开始]() + - [基础知识/开发环境](./chapter1/env.md) + - [创建]() + - [开发](./chapter1/dev.md) + - [部署](./chapter1/deploy.md) +- [框架结构](./chapter2/def.md) + - [entities](./chapter2/entities.md) + - [pages/components](./chapter2/components.md) \ No newline at end of file diff --git a/src/chapter1/deploy.md b/src/chapter1/deploy.md new file mode 100644 index 0000000..f7e62e6 --- /dev/null +++ b/src/chapter1/deploy.md @@ -0,0 +1,3 @@ +# 部署 + +todo \ No newline at end of file diff --git a/src/chapter1/dev.md b/src/chapter1/dev.md new file mode 100644 index 0000000..157e040 --- /dev/null +++ b/src/chapter1/dev.md @@ -0,0 +1,88 @@ +# 编译项目数据结构 +使用Oak框架的项目,需要将项目中数据结构的定义(src/entities目录下),编译成为Oak框架能使用的完整定义(src/oak-app-domain目录下)。当开发人员 +修改了相关数据结构后,也必须执行下述命令: + +```nodejs +npm run make:domain +``` +项目的数据结构编写规范参见:[entities](../chapter2/entities.md) + +# 开发(前台模式) +在Oak框架的设计中,前后端并无区分。在开发模式下,可以将后端逻辑直接运行在前端环境中,使开发过程更加简洁高效。 +前台模式也是推荐的开发模式,可以做到极速的“可见即所得”,编写业务大部分情况下应当使用这种开发模式 + +## 运行web端 +在项目目录下运行 +```nodejs +npm run start:web +``` +运行成功后浏览器会自动打开 +> http://localhost:3000 +即可进行开发调试。推荐使用Chrome浏览器,要注意的是,因为项目只运行在浏览器内部,其数据是和外界隔离的。 + +## 运行小程序端 +在项目目录下运行 +```nodejs +npm run start:mp +``` +运行成功后,用微信开发工具打开wechatMp/dist目录 + +## 运行App端 +Oak的App开发使用react-native技术栈,按照[react-native开发教程](https://reactnative.dev/docs/set-up-your-environment)配置好环境,连接手机后,在项目目录下运行: + +```nodejs +npm run run:android/ios +``` +也可以先运行: +```nodejs +npm run run:bundle +``` +来进行原生安装文件打包,再执行 +```nodejs +npm run start:native +``` +启动react-native调试服务器。 + +# 开发(前后台模式) +如果你的应用开发过程中有以下需求,则需要使用前后台开发模式。 +1. 需要(后端)调用本地的其它应用程序 +2. 需要(后端)访问其它webservice接口 +3. 需要多前端(web + 小程序)同时调试程序 + +## 编译并运行后端 +在项目目录下编译整个项目 +```nodejs +npm run build +``` +编译成功后,在lib目录下是编译后的js代码。 +在项目目录的configuration/mysql.json中编辑本地数据库设置,并执行SQL语句先创建好相应的数据库。 +```sql +create database xxx default character set utf8mb4; +``` +> 当前框架要求使用MySQL 8.0以上版本。 + +再在项目目录下执行: +```nodejs +npm run server:init +``` +初始化数据库。 + +然后执行: +```nodejs +npm run server:start +``` +运行成功后后端服务器启动,并监听在3001端口上。 + +## 运行前端 +在项目目录下执行 +```nodejs +npm run start:web:server +``` +即以前后端模式运行前端部分代码,此时前端会自动请求localhost:3001端口去访问后端服务器。同理,也可以以前后端模式运行小程序 +```nodejs +npm run start:mp:server +``` +要注意的是,如果在前端开发模式和前后端开发模式之间切换的话,需要先清除缓存,以保证切换成功(观察浏览器的Network是否发出请求是确认当前是否成功运行在前后端开发模式下的关键)。清缓存的命令是: +```nodejs +npm run clean:cache +``` \ No newline at end of file diff --git a/src/chapter1/env.md b/src/chapter1/env.md new file mode 100644 index 0000000..647d073 --- /dev/null +++ b/src/chapter1/env.md @@ -0,0 +1,19 @@ +# 基础知识 + +* Oak使用Typescript语言,因此您需要基本掌握: +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/) +3. Typescript语言基础: [官方文档](https://www.typescriptlang.org/) + +* 在前端,Oak目前使用React作为网页端框架(尽管这不是必须,但由于团队技术力量等原因,短期内没有计划去适配vue等其它框架),因此您也需要掌握React的一些基本概念。如果您需要开发App或者小程序,也需要去了解一些其相关概念。 +1. React: [官方站点](https://react.dev/) +2. React-native [官方站点](https://reactnative.dev/) +3. 微信小程序开发 [官方站点](https://developers.weixin.qq.com/miniprogram/dev/framework/) + +对于其它更多的前端环境,Oak也将在未来进行适配。Oak的前端技术路线请参见:todo + +# 开发环境 + +开发环境只要配置NodeJs 18以上即可,推荐使用Microsoft的Vs Code作为开发IDE +* [NodeJs安装](https://nodejs.cn/download/) +* [VS Code下载](https://code.visualstudio.com/) \ No newline at end of file diff --git a/src/chapter1/script.md b/src/chapter1/script.md new file mode 100644 index 0000000..bca1011 --- /dev/null +++ b/src/chapter1/script.md @@ -0,0 +1 @@ +# 运行项目 diff --git a/src/chapter2/components.bmp b/src/chapter2/components.bmp new file mode 100644 index 0000000..4fba8f0 Binary files /dev/null and b/src/chapter2/components.bmp differ diff --git a/src/chapter2/components.md b/src/chapter2/components.md new file mode 100644 index 0000000..91cf782 --- /dev/null +++ b/src/chapter2/components.md @@ -0,0 +1,43 @@ +# pages/components +设计完Entity后,即可直接进入应用页面的编写(Oak框架会自动处理发请求、取数据、缓存等一系列工作,不需要你再编写任何一行相关代码了😁)。 + +## 设计理念 +在Oak框架中,前台组织可以分为三个层次,如下图所示: +![层次结构](./components.png) + +#### namespace +*namespace*是指应用在最顶层被划分成几个命名空间,每个命名空间中包含若干页面,命名空间一般按路由划分,而同一个命名空间中的布局是相同的。例如,一个典型的网站会分为*front*和*console*两个命名空间,分别代表普通用户访问的前端和管理人员访问的控制台。前者有统一的*header(页头)*和*footer(页脚)*,而后者还会有统一的*menu(菜单)*。这些跨页面级别的组件摆放是在命名空间里处理的。 + +一般而言一个应用不会有过多的命名空间,命名空间被放置在*web/src/app/namespaces*目录下,命名空间的路由就是由目录名称决定的。 + +在命名空间目录下的index.json文件可以配置此命名空间的一些信息,例如在frontend/index.json中,往往有如下配置: +```json +{ + "path": "/", + "first": "/home", + "notFound": "/result/404" +} +``` +这代表此命名空间的路由就是根目录,首页是指向*/home*页面,而当应用出现无法识别的路由时,自动指向/result/404 + +一个命名空间下有哪些页面?在Oak框架中这是由src/pages目录下的目录结构自动决定的,在pages目录下,第一层目录是和命名空间名称对应的,其下的每个目录中的page就会被自动编译到对应的namespace下。 + +命名空间之间的跳转建议在全局统一且唯一(例如在*frontend*命名空间可以通过点击页上的“控制台”进入*console*命名空间),不要在普通页面之间跳转的时候跨越命名空间,这会容易让用户思维陷入混乱。 + +在大部分情况下,命名空间应该只存在于web端,对于小程序和App,一般不宜设计*namespace*,免得应用过于复杂。 + +#### page + +一个*page*是前端的一个页面。*page*编写在*src/pages/${namespace\}*目录下,其路由和目录名保持相同。例如,在*src/pages/frontend/home*目录下的页面就会匹配到```/home```或```/```(参见上一小节中frontend命名空间的配置)。 + +虽然并非强制,但推荐在*src/pages/${namespace\}*下的page目录的第一层用该页面相关的entity名称命名,第二层可以用*list/detail/upsert*之一,或者该entity的某个Action来加以命名(如果页面的功能就是对此entity进行此action的话),这样此页面的功能看上去就一目了然。当然,对于像首页这样的复合性页面(并不是限定在某个entity上),您可以自由对之进行命名,只要这个命名让用户看上去很容易理解。 + +#### component +一个*component*代表一个固定功能的组件。编写组件的目的是: + +1. 为了在多个*page*之间复用代码 +2. 避免*page*过于复杂 + +在Oak中,一个*page*可以包含多个*component*,一个*component*也可以引用其它*component*,但在*component*之间不能出现循环引用。 + +> page之间不要相互引用,尽管这样做也不会错,但会让整个项目变得更加混乱,难以维护。 \ No newline at end of file diff --git a/src/chapter2/components.png b/src/chapter2/components.png new file mode 100644 index 0000000..4fba8f0 Binary files /dev/null and b/src/chapter2/components.png differ diff --git a/src/chapter2/def.md b/src/chapter2/def.md new file mode 100644 index 0000000..c93ef34 --- /dev/null +++ b/src/chapter2/def.md @@ -0,0 +1,51 @@ +# 目录结构 + +一个典型的Oak项目的主要目录结构如下: + +``` +- src + - aspects + - assets + - checkers + - components + - configuration + - context + - data + - endpoints + - entities + - features + - locales + - pages + - ports + - routines + - timers + - triggers + - watchers +- lib +- web +- wechatMp +- native +``` + +* src目录:存放项目主要的业务逻辑代码。目录按Oak的各种概念又分成多个子目录,关于src下面各种概念的介绍,在本章节将逐一对之进行介绍 +* lib目录:存放项目编译后的js文件 +* web目录:存放项目在web端的入口文件和路由文件等 +* wechatMp目录:存放项目小程序端的入口文件和各种配置文件 +* native目录:存放项目App端的入口文件和路由文件等,以及iOs/Android层的项目代码 + +# 项目开发 +开发一个应用系统,主要编写的代码是在src的以下的目录当中。每个目录的含义和如何编写,我们将在本章的各节按照先后顺序分别介绍。 + +如果是开发web应用,在web目录下您还需要编写: +* web/src/app/namespace下面的各namespace配置及布局 +* web/src/app/components下面被namespace引用的公共组件(如整个网站的header/footer等) + +如果是开发小程序,在wechatMp目录下您还需要编写: +* wechatMp/src/app.ts(app.less) 全局的一些事件处理和样式设计 +* wechatMp/src/project.config.json 小程序全局配置 +* wechatMp/src/app.json 小程序路由配置 + +如果是开发App,在native目录下您还需要编写: +* native/App.tsx native/index.tsx 全局的一些事件处理和插件加载 +* native/router/index.tsx App路由配置 + diff --git a/src/chapter2/entities.md b/src/chapter2/entities.md new file mode 100644 index 0000000..113760e --- /dev/null +++ b/src/chapter2/entities.md @@ -0,0 +1,344 @@ +# Entities + +## 对象和属性 +Entity在Oak框架中代表一个实体**对象**,应用开发首先应将需求分解,并设计出最基础的对象及相对应的关系,并在*src/entities*目录中对之加以定义。 + + +一个对象拥有若干**属性**,在Entity定义文件中,可以像以下这样来定义对象及其属性。我们引用的代码是*oak-general-business*包当中的*Address*对象,它代表着一个地址。 +```nodejs +import { String, Int, Boolean, Text } from 'oak-domain/lib/types/DataType'; +import { EntityShape } from 'oak-domain/lib/types/Entity'; +import { Schema as Area } from './Area'; +import { EntityDesc } from 'oak-domain/lib/types/EntityDesc'; + +export interface Schema extends EntityShape { + detail: String<32>; + area: Area; + phone: String<12>; + name: String<32>; + default: Boolean; + remark?: Text; + entity?: String<32>; + entityId?: String<64>; +}; +``` + +设计并编写Entity的一些规范如下: +1. 需要用 ```export interface Schema extends EntityShape``` 来定义对象。我们设计Entity时,要么继承*EntityShape*(在其中定义了*id*等几个通用属性),要么继承并扩展已经设计好的对象(在下一小节的*User*对象中你们就可以看到例子), +2. 需要使用 ``` 'oak-domain/lib/types/DataType' ``` 中的类型来声明对象的属性 +3. 可以引用其它Entity,并将之声明为当前Entity的某一属性(此时称之为定义了对象之间的**多对一关系**)。例如在*Address*对象中就声明了*area*属性是代表(地址所属的)地区。 + +> 对象的属性也可以指向其自身。例如,在Area对象的定义中,就有一个 parent 属性指向它自己(Schema),代表该地区的上级地区。 +4. 可以为对象设计动态多对一关系,若一个对象可能和不同的对象有多对一关系,则通过声明 ``` entity: String<32>; entityId: String<64> ```即可以表达动态的多对一关系,即当前对象可能和不同的其它对象具有多对一关系。 + +## 动态多对一关系 +在一些应用中,某对象*A*可能和不同的对象*B/C/D*……具有多对一关系(但这种关系是互斥的,不可能同时和多于一个对象具有这种关系)。此时,如果像这样设计*A*对象: +```nodejs +export interface A extends EntityShape { + b?: B; + c?: C; + d?: D; +}; +``` +显然是一种比较臃肿的方式,同时也不具备良好的扩展性(比方说随着业务的发展,*A*又要指向更多的对象时,必须来修改其Entity定义)。 + +此时可以通过声明两个特殊的属性,告诉Oak框架,此对象可能会指向多个不同的其它对象。 + +```nodejs +export interface A extends EntityShape { + entity: String<32>; + entityId: String<64>; +}; +``` +注意,这里*entity*和*entityId*是类型关键字,您不能拿它们去声明自身对象的其它属性,同时它们的类型也是固定的。 + +回到上面的*Address*对象的例子中,由于*oak-general-business*是较底层的抽象模块,我们想实现*Address*的功能,但并不想限制应用地址对象必须归属于哪个对象(例如,在某个应用系统中,不仅“用户”对象拥有地址,“公司”对象也拥有地址)。 + +问题来了,如何声明当前(拥有动态多对一关系的)对象和其它哪些对象可能有关联呢?只需要在要关联的对象中像下面这样声明就可以了,这里我们引用的代码来自*oak-general-business*中的*User*对象 +```nodejs +import { Schema as ExtraFile } from './ExtraFile'; +import { Schema as WechatQrCode } from './WechatQrCode'; +import { Schema as Address } from './Address'; +import { Schema as User } from 'oak-domain/lib/entities/User'; + +export interface Schema extends User { + passwordSha1?: Text; + birth?: Datetime; + gender?: 'male' | 'female'; + idCardType?: 'ID-Card' | 'passport' | 'Mainland-passport'; + idNumber?: String<32>; + files: Array; + codes: Array; + isRoot?: Boolean; + addresses?: Address[]; +}; +``` +用户这个对象,(以一对多的方式)关联了三个拥有动态多对一设计的对象:*ExtraFile*, *WechatQrCod*e, *Address*。这里我们不去深究其具体含义,只要知道,这三个对象中都设计有 ```entity/entityId``` 属性,同时我们在User对象的定义中声明了,User和它们具有一对多关系。这里用*Array*和[]的写法都是可以的。 + +> 如果一个对象想拥有超过一个的动态多对一关系怎么办?很遗憾,Oak并不支持这种设计。而且根据我们的经验,在实际应用开发中,出现如此复杂的对象往往意味着你应该重新审视你的对象设计是否合理了。 + +## 更多属性相关 +Oak当前支持的基本属性类型可以参见:```oak-domain/lib/types/DataType```。 +> 其中,*Float*和*Double*类型已经不再推荐,请尽量使用*Decimal*来定义非整型数值 + +除去这些类型之外,Oak还支持: +* 以枚举的方式定义。例如在上一小节中*User*的*gender*属性 +* 以对象的方式定义。如果某个属性非常复杂,不能用基本类型表示。你既可以定义其为*Object*,也可以用TS来定义其具体格式 + +以下代码节选自*oak-general-business*的*Application*对象。每个*Application*对象代表着系统的一个应用(Web/小程序/App……),因此它需要丰富的配置信息。 +```nodejs +export type AppType = 'web' | 'wechatMp' | 'wechatPublic' | 'native'; + +export type NativeConfig = { + type: 'native', + passport?: Passport[]; +}; + +export interface Schema extends EntityShape { + name: String<32>; + description: Text; + type: AppType; + system: System; + config: WebConfig | WechatMpConfig | WechatPublicConfig | NativeConfig; + style?: Style; + sessions?: Session[]; + domain?: Domain; +}; +``` +在这里,*type*属性指向了一个枚举类型,*config*属性和*style*属性都指向更复杂的对象,*config*对象甚至指向一个复杂对象的枚举。 + +## Action和State +在应用中的一些对象(核心业务对象),往往具有**状态**的概念。通过在Entity中定义其状态和**动作**,以及两者之间的关系,可以使应用更加严谨和可维护。 + +以上面所提到的 *oak-general-business* 中的*User*对象为例,在我们的设计中,用户这个对象有两种状态的转变: +1. 对象状态:分为四种:*正常用户/被禁用用户/被合并用户/影子用户*。在这里我们不去深究后面两种的语义,只需要知道这代表着*User*的状态可能发生变化。比如说如果发现某个用户经常违规,管理员可以将之禁用,此时用户就变成了被禁用状态,将无法再登录系统。 +2. 对象身份状态:有的应用需要验证用户的真实身份,此时用户的身份状态就有三种:*未认证/认证中/已认证* + +很显然这两种状态的转变是彼此独立的,我们可以通过以下的声明代码,来规范这两种状态。除了状态之外,我们也可以定义对*User*这个对象可能的动作,来规范状态的转变(以及后面的权限管理)。 +```nodejs +export type IdAction = 'verify' | 'accept' | 'reject'; +export type IdState = 'unverified' | 'verified' | 'verifying'; +export const IdActionDef: ActionDef = { + stm: { + verify: ['unverified', 'verifying'], + accept: [['unverified', 'verifying'], 'verified'], + reject: [['verifying', 'verified'], 'unverified'], + }, + is: 'unverified', +}; + +export type UserAction = 'activate' | 'disable' | 'enable' | 'mergeTo' | 'mergeFrom'; +export type UserState = 'shadow' | 'normal' | 'disabled' | 'merged'; +export const UserActionDef: ActionDef = { + stm: { + activate: ['shadow', 'normal'], + disable: [['normal', 'shadow'], 'disabled'], + enable: ['disabled', 'normal'], + mergeTo: [['normal', 'shadow'], 'merged'], + mergeFrom: ['normal', 'normal'], + }, +}; + +export type Action = UserAction | IdAction; +``` +在这里,我们声明了两组状态和动作,以及状态转换矩阵,同组中的状态/动作/转换矩阵的前缀必须要一致。状态转换矩阵规定了某个*Action*将把*User*的当前(对应的状态值)修改成什么,这种转换在规定之后应当是一目了然的。其中,```is```定义的是初始状态(若对象创建时没有赋初始状态则默认使用这种状态值)。 + +最后,我们通过``` export type Action ```来声明了当前对象上的所有*Action*。对当前对象的操作将被限制在这些自定义的*Action*和通用的*Action*当中。通用的*Action*包括: +* create +* update +* remove +* select +* count +* download +* aggregate +* stat + +在定义您自有的*Action*时请避免使用这些关键字。 + +> *Action*是否必须与*State*相关联?并不是,完全可以定义某个Action并不改变对象的状态,甚至不改变对象本身的任何属性!实际上,我们鼓励根据应用的需求,尽量细的划分不同动作,以便实现更好的程序严谨性和权限管理。某种意义上,可以将自定义的*Action*看成一种特殊的*update*。 + +## Relation +几乎所有的应用系统都有一个共同的核心对象——用户。用户就是使用应用系统的人,而应用系统要对用户使用应用系统的行为加以甄别和管理(权限分配和限制),前提就是要将系统中的业务对象“分配”给合适的用户。*Relation*所表达的,就是当前对象和用户的**关系**。 + +如果一个对象和用户具有Relation,则可以像下面一样定义,下面的代码节选自*oak-general-business*中的*Session*对象定义。 +``` +export type Relation = 'partner'; +``` +*Session*对象代表的是一个会话,而*partner*关系表达的就是参与这场会话的人。如果一个*User*和一个*Session*存在*partner*关系,则代表这个用户参与了这场会话。 + +#### Relation的底层实现 +在*oak-domain/src/entities*中,定义了一些公共对象,这些对象存在于所有的Oak项目中(尽管或许个别项目不会用到它们全部)。其中有一个对象*UserRelation*,其代表的就是用户和某个对象之间的*Relation*。 +```nodejs +import { Schema as User } from './User'; +import { Schema as Relation } from './Relation'; + +export interface Schema extends EntityShape { + user: User; + relation: Relation; + entity: String<32>; + entityId: String<64>; +}; +``` +如果你理解了本章的内容,应当能理解*UserRelation*对象中各个属性的含义了吧。😁 + +## EntityDesc +最后,我们需要为我们所有定义的内容进行描述。描述的目的有两个: +1. 使此对象上的一些命名和风格全局一致(例如对象命名、对象属性命名等) +2. 使此对象在存储时得到优化 + +对对象进行描述的代码如下面这样,我们仍然使用*oak-general-business*对象中的*User*对象举例: +```nodejs +export const entityDesc: EntityDesc< + Schema, + Action, + '', + { + userState: UserState; + idState: IdState; + gender: Required['gender']; + idCardType: Required['idCardType']; + } +> = { + locales: { + zh_CN: { + name: '用户', + attr: { + name: '姓名', + nickname: '昵称', + birth: '生日', + password: '密码', + passwordSha1: 'sha1加密密码', + gender: '性别', + idCardType: '证件类型', + idNumber: '证件号码', + ref: '指向用户', + files: '相关文件', + userState: '用户状态', + idState: '认证状态', + codes: '微信分享二维码', + isRoot: '是否超级用户', + addresses: '收货地址', + }, + action: { + activate: '激活', + accept: '同意', + verify: '认证', + reject: '拒绝', + enable: '启用', + disable: '禁用', + mergeTo: '合并', + mergeFrom: '使合并', + }, + v: { + userState: { + shadow: '未激活', + normal: '正常', + disabled: '禁用', + merged: '已被合并', + }, + idState: { + unverified: '未认证', + verifying: '认证中', + verified: '已认证', + }, + gender: { + male: '男', + female: '女', + }, + idCardType: { + 'ID-Card': '身份证', + passport: '护照', + 'Mainland-passport': '港澳台通行证', + }, + }, + }, + }, + indexes: [ + { + name: 'index_birth', + attributes: [ + { + name: 'birth', + direction: 'ASC', + }, + ], + }, + { + name: 'index_fulltext', + attributes: [ + { + name: 'name', + }, + { + name: 'nickname', + }, + ], + config: { + type: 'fulltext', + parser: 'ngram', + }, + }, + { + name: 'index_userState_refId', + attributes: [ + { + name: 'userState', + }, + { + name: 'ref', + }, + ], + }, + ], + style: { + icon: { + verify: '', + accept: '', + reject: '', + activate: '', + enable: '', + disable: '', + mergeTo: '', + mergeFrom: '', + }, + color: { + userState: { + normal: '#0000FF', + disabled: '#FF0000', + merged: '#9A9A9A', + shadow: '#D3D3D3', + }, + idState: { + unverified: '#FF0000', + verified: '#0000FF', + verifying: '#EEE8AA', + }, + gender: { + male: '#0000FF', + female: '#EE82EE', + }, + idCardType: { + 'ID-Card': '#E0FFFF', + 'Mainland-passport': '#2E8B57', + passport: '#2F4F4F', + }, + }, + }, +}; +``` +在对象描述的类型定义*EntityDesc*中,可以传入四个参数,分别是对象定义*Schema*,对象动作定义*Action*,对象关系定义*Relation*,以及对象的相关状态和枚举属性定义字典。而对对象描述中,主要有: +* locale定义,对对象本身/属性/属性枚举值/状态/动作/关系进行命名,这里的命名将被Oak框架编译成i18n的数据,实现全局命名的一致性(对编写优秀的应用极其重要!) +* indexes定义,定义相关索引项。在这里定义的索引会在数据库建立的时候自动创建,也会规范对对象的查询方式(例如如果没有声明全文索引,则无法进行全文搜索) +* style定义,定义对象的动作/关系/状态的颜色和图标(尚未实现),使前端渲染时可以获取相应的颜色,以实现颜色的全局一致性(同样对编写优秀的应用极其重要!) + +由于TS的优秀的规范性,这里的定义如何详细书写不再赘述。 + +## Entities的作用 +Entity的定义可以说是整个应用业务的核心,通过执行 +```nodejs +npm run make:domain +``` +Oak框架会在```src/oak-app-domain```目录下编译出完整的应用对象定义,这份定义既包括开发人员编写代码时的Typescript类型定义,也包括程序运行过程中访问对象时的正确性检查。因此请牢记: + +1. **对Entity的任何修改都应当执行 npm run make:domain,使修改后的定义生效** +2. **如果应用已经上线迭代,对Entities目录下的任何修改,都应当在upgrade目录下维护好对数据库(结构或数据)的升级脚本,新版本上线时进行升级** \ No newline at end of file diff --git a/src/env.md b/src/env.md new file mode 100644 index 0000000..92d9aaf --- /dev/null +++ b/src/env.md @@ -0,0 +1 @@ +# 基础知识/开发环境 diff --git a/src/intro-1.png b/src/intro-1.png new file mode 100644 index 0000000..43707ae Binary files /dev/null and b/src/intro-1.png differ diff --git a/src/intro.md b/src/intro.md new file mode 100644 index 0000000..52f3bf2 --- /dev/null +++ b/src/intro.md @@ -0,0 +1,36 @@ +# Oak框架简介 + +Oak是一个现代的应用系统快速开发框架,它实现了对业务系统相对抽象层次上的功能抽象,使应用开发者可以将自己的注意力完全集中于实现业务逻辑本身,而无须关注众多构建应用系统需要考虑的(共性)问题,例如: + +* 我该选用什么样的数据库,如何建立索引? +* 我该怎样设计并实现全局一致的用户权限?(几乎没有多少应用系统能完美解决这一问题) +* 我该怎样设计前后端接口才能保证低耦合且可重用?(传统MVC设计模式下controller如果不加以规范的组织设计/定期重构,对于长期开发而言是一场灾难) +* 我该如何在各层次上保证数据的一致性?(前端->后端->分布式,每一个层面上都有各种细节问题需要考虑) +* 我该如何保持前后端常量和逻辑的一致性?(有很多库被设计专门用来解决这一问题,使得整个项目变得更加复杂) +* 我的应用如果有多个前端,如何在代码复用和保证高可持续开发性之间取得平衡? +* 如何发现并杜绝我的项目在各个层次上的安全漏洞? + + +## Oak的背景与目标 + +作为软件开发者,在当今这个年代要开发业务软件既是幸福的也是痛苦的。一方面,众多成熟的技术方案及SaaS类服务几乎能解决应用开发中遇到的各种问题(对比20年前,光是一个图片上传预览就要手写解决无数的问题),另一方面,开发软件需要不断学习和使用的新技术/开源库也越来越多,甚至可以说,几乎没有人的学习速度能赶得上新技术的出现速度。技术栈的爆炸既造成了应用开发者之间的鸿沟(我的项目代码只有我能维护的动😭),也让绝大多数小团队或者个人开发者难以赋予项目一个健壮、可持续的基础架构。当一个项目持续开发一年以上,代码往往就成为了著名的“屎山”。 + +Oak框架的设计目标就是解决软件开发过程中的这些问题,让现代典型的应用系统开发达到一致、可持续、高可用的目标。 + +## Oak的技术选择 + +Oak框架使用[Typescript](https://www.typescriptlang.org/)作为开发语言。Oak设计之初就希望以一种语言统一编写前后端(前后端一致性),让团队中编写应用的程序员变成“完全对等”的。在此前提下,Javascript语言几乎是唯一的选择。同时为了提高代码的规范性,当前最流行的Typescript成为了我们的选择。 + + +> Typescript语言在开发过程中非常消耗计算机资源,请确保您的开发机器拥有Intel 12代以上的Cpu以及16GB以上的内存空间,以获得较好的开发体验😁 + +Oak框架主要针对项目的整体架构及公共功能抽象,其本身并没有限制过多的技术栈。例如,后端数据库默认使用MySQL,但您也可以通过编写一些适配性的代码来使之运行在PostgreSQL或者MongoDB之上;在前端,目前我们选择使用React,但这也并不意味着您不能使用vue(当然,这需要您自己实现一套vue上对等的逻辑转换)。在未来,我们将积极拥抱开源,希望集各方之力,服务于数量广大的应用开发工程师。 + +## Oak的定位 +纵观计算机软硬件技术的发展历史,都可以用“抽象”两个字来概括,例如,操作系统就是对各种硬件的抽象,数据库就是对于数据存储查询的抽象。从这个角度看,Oak框架也是一种抽象,它是最接近业务层次的抽象,尽可能的将业务需要遇到的各种共性问题统一进行了处理,并制订了业务逻辑编写的开发规范,以致力于**使业务开发者不写一行多余的代码**这一目标。 + +![Oak定位](./intro-1.png) + +需要强调的是:**正如操作系统和数据库一样,Oak也并非能解决所有的应用开发问题,它的设计目标仅仅是为了提高应用软件开发的效率和规范。一名优秀的工程师仍然应当掌握更多基础技术的实现,以应付项目中的更多问题**。 + +Oak仍然在不断开发完善中,如有问题,也欢迎加入讨论小组,给出您的宝贵建议。