定义组件
通过OakComponent接口可以定义一个组件对象。在前几章,我们已经看到部分重要的定义配置项,完整的配置项及格式可以参见oak-frontend-base/src/types/Page.ts文件。在编写代码时,typescript也会给出有效的提示。在本章节我们将归纳并进一步介绍部分配置项,以及组件本身可引用的数据项和方法。
配置项
| 配置项 | 类型 | 作用 |
|---|---|---|
| entity | string | () => string | 组件关联的Entity |
| isList | boolean | 组件是否为列表页 |
| projection | object | () => object | 组件所取的Entity属性 |
| filters | array | 组件取Entity的过滤条件(仅对list组件有效) |
| sorters | array | 组件取Entity的排序(仅对list组件有效) |
| pagination | object | 组件取Entity的分页设置(仅对list组件有效) |
| getTotal | number | object | 组件取Entity时,取满足条件的行总数设置(仅对list组件有效) |
| properties | Record<string, any> | 组件接受的props |
| data | Record<string, any> | 组件自身的data(React中的state) |
| features | array | 组件需要监听哪些features的变化 |
| actions | array | 组件需要检查哪些actions的许可 |
| zombie | boolean | 组件是否保留数据 |
| formData | (params) => Record<string, any> | 组件处理取到的Entity行数据逻辑 |
| lifetimes | Record<string, function> | 组件的生命周期方法 |
| listeners | Record<string, function> | 组件监听属性事件 |
| methods | Record<string, function> | 组件自定义方法 |
entity
本组件关联的对象,如果不设置,则这个组件就是Virtual组件。
projection
组件需要取的对象的属性,结构就是此Entity的projection。可以通过级联取其相关的对象数据。
filters
list组件需要取的对象的过滤条件,结构就是此Entity的filter。由于一个列表页可能会更新不同的过滤条件,因此这里用的是数组结构。您可以在组件的方法中调用addNamedFilter和removeNamedFilter等接口对过滤条件进行增删。
sorters
list组件需要取对象的排序条件,结构就是此Entity的sorter。和filters类似,这里用的也是数组结构。
pagination
list组件取数据的分页设置。
- currentPage:起始页号,默认0,
- pageSize:页面数据条数,默认20,
- randomRange:随机取数据范围,用于一些特殊场合。在randomRange标识的范围内随机取pageSize条数据。 pagination还支持数组形式,用一个deviceWidth来标定宽窄屏下不同的配置。
getTotal
list组件是否取当前条件下的行总数量 有时列表页想取得满足当前条件的行数量,此时可以在getTotal上配置一个最大值,框架就会去取得不大于这个数值的总数量。如果满足条件的行的数量大于这个数值,则系统返回此数值(比方说有1亿行数据,此时系统不会去查询这么大量的数据返回1亿,而是只返回getTotal的值),这种设计的用意是当行数量极大时节省系统性能。
和pagination一样,getTotal也支持分设备配置。
properties
组件接受的外部属性值(相当于React中的props)。注意,在小程序环境下,会根据properties的类型向小程序环境注册其属性类型。因此即使属性默认值是null或者undefined,在这里也要用该属性类型来声明初始值,并在组件方法中处理。建议的各种类型的初始值可以是:
- number: 0;
- string: ''(空字符串);
- object: null;
- array: [];
- function: () => undefined as void;
data
组件的自身内部属性值(相当于React中的state)。
features
组件要监听所配置feature的变化。例如,如果一个页面与用户是否登录状态有关,就应当关联在oak-general-business的token上,这样当用户登录或退出时,本页面就会被通知。关于features的介绍请参见todo。
对于每个feature发生变化可以定义监听后的行为:
- refresh:组件刷新数据
- reRender:组件重新渲染
- 自定义回调函数
有些系统级别的feature会被组件默认jtkf,如所有非Virtaul的组件都会默认监听cache的变化,而所有的组件都会监听locales的变化。
actions
组件需要对取得的数据行进行某些action的判定,其值为Entity的Action。
在Oak框架中,所有的action的行为判定都应该写成checker,加上相关权限判定。组件在取得行数据后,会自动判定当前用户是否可以在这些行上执行这些actions,判定的结果在FormData中作为参数的一部分返回。
zombie
声明组件在析构后是否保留其数据。注意,这个声明只能在顶层组件也就是Page上声明。
zombie意味着这个页面在析构时,其在框架中执行的结果——如增加的filters/sorters,更新但尚未提交的数据——是不会丢失的,当用户回到这个页面时,可以恢复到上次的状态。
比方说您在创建某个特别复杂的对象中途,因为某些原因切换到了其它页面上,等回到这个页面时,如果不希望填写了一半的数据消失,就可以使用这个声明。但是要注意,如果在应用中可能某个页面有多个不同的场景,需要仔细设计避免不正确的重用。
formData
当框架被某个feature通知数据有更新时,组件就可能进入重新渲染(reRender) 逻辑。重新渲染会先调用formData函数,并将返回的结果也置在组件的data域中,再调用相应平台下的渲染代码。
formData函数接受一个对象输入参数,其形式声明如下:
options: {
data: IsList extends true ? RowWithActions<ED, T>[] : RowWithActions<ED, T>;
origin?: IsList extends true ? RowWithActions<ED, T>[] : RowWithActions<ED, T>;
features: BasicFeatures<ED> & FD;
props: TProperty;
legalActions?: ActionDef<ED, T>[];
originLegalActions?: ActionDef<ED, T>[];
dirty: boolean;
modified: boolean;
}
- data: 取到的行数据,组件为list时是数组,非list时是对象。在行的oakLegalActions当中返回了声明的Actions中通过的(该行上的)合法操作。
- origin: 行数据的前项。如果在组件上对行数据有更新但未提交,这里返回的是更新前的原数据。
- features:所有的features。
- props:组件的props。
- legalActions:当组件不是list时,返回该行通过的Actions;如果组件是list,只有当Actions中声明了create时,这里返回的是该用户是否有创建新数据的权限(只有create动作和具体行无关,每行数据相关的action动作检查结果都在该行的oakLegalActions上)。
- originLegalActions:和legalActions一样,只不过检查的是origin数据上的动作权限。
- dirty:行上是否有更新,调用组件的update/updateItem方法可以更新行上的属性。
- modified:行是否真的被更新。和dirty的区别在于:如果将行上的某原值为1的属性改为2,再改为1,此时dirty为true,而modified为false。
formData需要返回一个对象,此对象会被合并到组件的内部数据项(this.state)上,供渲染层和方法层使用。
lifetimes
组件的生命周期函数。组件有以下几个生命周期:
- created
- attached
- ready
- detached
- moved
- error
- show
- hide
- resize
其中5~9的生命周期函数和小程序同构,在其它端暂不支持。重要的是前面4个生命周期:
- created: 当组件构建时调用(此时组件上的方法和数据都可能未完成准备)
- attached: 当组件mount时调用(此时组件上的数据已经可用,但部分方法还没有准备完成)
- ready:组件完全初始化完成,且声明的对象数据已经取得之后调用。此时所有的数据和方法都可用
- detached:组件准备析构时调用
listeners
组件监听的自身数据(包括props和data)的变化,和小程序的方法声明类似,函数名就是监听的数据项名,其调用参数如下:
(prev: Record<string, any>, next: Record<string, any>) => void
第一个参数传入前项的自身数据字典,第二个参数传入当前的自身数据字典。不过要注意的是,在小程序环境下,props上的变化是无法监听到前项的。
methods
组件自定义方法
组件的数据项
组件中有两种数据项: state和props
state
继承了React的语义,表达组件内部的自身数据项,并可以通过setState方法来设置这些项(和类语法下的React完全一样)。
this.state中初始会包括在data配置项中声明的数据,以及formData方法返回的数据。当然,在调用setState方法时,可以不限于只更新data配置项中声明的数据项,但是我们不推荐这样做。
要注意的是,调用setState来更新数据项,会触发渲染函数调用进行重渲染,但并不会触发调用formData。如果您希望执行框架层面上的重渲染,可以在setState的回调方法参数里调用reRender方法。
props
继承了React的语义,表达组件外部传入的属性。为了在小程序环境下能正确处理传入的属性,需要在properties配置项中进行准确声明(上面已经叙述)。
如何引用数据
在组件的各种方法(formData/lifetimes/methods)中,可以通过this关键字访问两种数据项:
const { oakId } = this.props;
const { name, oakFullpath } = this.state;
在props和state上都包含了一些框架的内置数据项,见下节介绍。state上还包含了从formData返回的数据项。
在小程序的渲染xml代码中,直接引用数据项名称就可以访问数据项(props和state上的所有数据项)。
在web和native环境下的渲染函数中,可以通过参数props中的data来引用所有的数据项。在使用前要通过typescript来声明这些数据项:
export default function Render(
props: WebComponentProps<
EntityDict,
'system', // 对象
false, // 是否list
{
name: string;
description: string;
},
{
setName: (name: string) => void;
}
>
) {
const { name, description, oakId } = props.data; // oakId是内置数据项
const { setName, execute } = props.methods; // execute是内置方法
}
框架内置数据项
框架有一些内置的数据项供组件使用。这些数据项都是以oak开头来命名的。用户应该避免使用相同的关键字来命名数据项。
在props上,有以下内置数据项:
| 数据项 | 类型 | 意义 |
|---|---|---|
| oakId | string | 非list页面的对象主键 |
| oakDisablePulldownRefresh | boolean | 页面是否禁用下拉刷新(与index.json中所配置的enablePulldownRefresh保持相反) |
| oakZombie | boolean | 组件在析构时数据是否保留,和配置项中的zombie类似,可用来控制单个组件中的数据保留(对于非页面组件必须在props上配置才能生效) |
| oakActions | string[] | 以参数的形式传入Actions,覆盖组件配置项中的Actions |
| oakFilters | NamedFilters[] | 以参数的形式传入Filters,覆盖组件配置项中的Filters |
在state上,有以下内置数据项:
| 数据项 | 类型 | 意义 |
|---|---|---|
| oakExecutable | boolean | Exception | 组件上的更新是否可以执行。如果更新不能执行,可能会以Exception的方式告知异常 |
| oakExecuting | boolean | 组件是否正在提交更新 |
| oakDirty | boolean | 组件上是否有更新数据(等同于formData参数上的isDirty) |
| oakModified | boolean | 组件上的数据是否真被更新(等同于formData参数上的isModified) |
| oakLoading | boolean | 是否正在刷新数据 |
| oakLoadingMore | boolean | 是否正在加载更多数据 |
| oakPullDownRefreshLoading | boolean | 是否正在下拉刷新数据(只对窄屏有效) |
| oakEntity | boolean | 组件关联的entity |
| oakFullpath | string | 组件在页面“组件树”中的路径 |
| oakLegalActions | string[] | 组件上当前合法的Actions(等同于formData参数上的oakLegalActions) |
善用这些数据项会有效节省您的开发工作量,提升组件的交互性。例如,您可以用oakLoading来判断是否在刷新页面并在渲染中处理这种情况,或者用oakDirty加上oakExecutable来判断页面上是否有可以提交的更新。
组件上的方法
在OakComponent配置项的各个函数中,均可以通过this来引用组件上的方法。这些方法可以分为以下三类:
- methods项中用户自定义的组件上的私有方法
- 组件的公共方法
- 从features暴露到组件上的方法
组件的公共方法
组件上的公共方法目前只有以下两个:
| 方法名 | 参数 | 作用 |
|---|---|---|
| setState | Record<string, any>, callback | 设置组件上的data(同react) |
| reRender | - | 重新调用formData |
未来会考虑加入动画等相关的api
从features暴露到组件上的方法
features提供了前端公用的能力,比较重要的底层feature包括:
- cache: 前端数据缓存
- locale: 语言国际化(i18n)
- runningTree:组件树结构
- navigator: 页面路由访问
- localStorage: 前端数据持久存储 等。这些底层的feature都存放在oak-frontend-base/src/features目录下,如果组件需要调用某个feature上的指定接口,可以像这样写:
this.features.cache.refresh(...);
同时为了方便起见,框架也将最常用的一批接口暴露到了组件上,这样组件就可以很方便的通过this关键字来直接调用。这批接口包括:
| feature | 方法名 | 作用 |
| localStorage | save | 存储持久化数据 |
| load | 读取持久化数据 | |
| clear | 清除持久化数据 | |
| message | setMessage | 发消息通知 |
| consumeMessage | 消费消息通知 | |
| navigator | navigateTo | 跳转页面 |
| redirectTo | 重定向页面 | |
| navigateBack | 向回跳转 | |
| switchTab | 切换Tab(小程序专用) | |
| locale | t | 翻译 |
| cache | checkOperation | 检查操作可行性 |
| select | 查询数据 | |
| aggregate | 聚合查询 | |
| operate | 操作数据 | |
| subscriber | subDataEvents | 订阅数据变化 |
| runningTree | refresh | 刷新数据 |
| execute | 提交更新 | |
| create | 创建(非list页面) | |
| update | 更新(非list页面) | |
| remove | 删除(非list页面) | |
| setId | 设置id(非list页面) | |
| getId | 获取id(非list页面) | |
| isCreation | 是否创建(非list页面) | |
| loadMore | 取下一页(list页面) | |
| getFilters | 获取当前过滤条件(list页面) | |
| setFilters | 设置当前过滤条件(list页面) | |
| getFilterByName | 获取指定命名的过滤条件(list页面) | |
| addNamedFilter | 增加命名过滤条件(list页面) | |
| setNamedFilters | 设置命名过滤条件(list页面) | |
| removeNamedFilter | 删除命名过滤条件(list页面) | |
| removeNamedFilterByName | 根据命名删除命名过滤条件(list页面) | |
| setNamedSorters | 设置命名排序条件(list页面) | |
| getSorters | 获取过滤条件(list页面) | |
| getSorterByName | 获取命名排序条件(list页面) | |
| addNamedSorter | 增加命名过滤条件(list页面) | |
| removeNamedSorter | 移除命名排序条件(list页面) | |
| removeNamedSorterByName | 根据命名移除过滤条件(list页面) | |
| getPagination | 获取分页设置(list页面) | |
| setPageSize | 设置分页长度(list页面) | |
| setCurrentPage | 设置当前分页(list页面) |
如何使用方法
如上所述,在formData/生命周期函数/自定义方法中,都可以通过this关键字来调用这些方法,或者通过this.features调用更多features上的方法。
- 要注意,在生命周期的create/attach函数中,runningTree上的许多方法是不能调用的。因为runningTree是在组件开始构建后才进行初始化的,更多细节可以参见todo。
在小程序环境下,可以直接在xml文件中调用自定义方法。
在react/react-native环境下,可以在props.methods中直接调用所有的自定义方法,同时也包括一部分公共方法。所提供的公共方法通过typescript声明可以获得,而自定义方法需要在props的声明中显式加以定义(参看上面「如何使用数据」小节的例子)。