基于 6.13.0 版本
前期回顾
动作触发入口
- 上篇提到
runActions是触发动作的入口(动作执行的前置处理),遍历要触发的动作列表 - 首先通过
getActionByType获取到【单个动作】的实例 - 若上述情况不存在,且指定了组件ID,则说明是【组件专有动作】
- 最终获取到动作实例传入
runAction中。从这里可见动作分为单个动作、组件动作两类
javascript
// packages/amis-core/src/actions/Action.ts
export const runActions = async (
actions: ListenerAction | ListenerAction[],
renderer: ListenerContext,
event: any
) => {
if (!Array.isArray(actions)) {
actions = [actions];
}
for (const actionConfig of actions) {
let actionInstrance = getActionByType(actionConfig.actionType);
// 如果存在指定组件ID,说明是组件专有动作
if (
!actionInstrance &&
(actionConfig.componentId || actionConfig.componentName)
) {
actionInstrance = [
'static',
'nonstatic',
'show',
'visibility',
'hidden',
'enabled',
'disabled',
'usability'
].includes(actionConfig.actionType)
? getActionByType('status')
: getActionByType('component');
} else if (['url', 'link', 'jump'].includes(actionConfig.actionType)) {
// 打开页面动作
actionInstrance = getActionByType('openlink');
}
// 找不到就通过组件专有动作完成
if (!actionInstrance) {
actionInstrance = getActionByType('component');
}
try {
// 这些节点的子节点运行逻辑由节点内部实现
await runAction(actionInstrance, actionConfig, renderer, event);
} catch (e) {
...
}
}
};
runAction就是真正调用动作run方法的地方(数据处理、特定场景处理直接略过),执行语句是await actionInstrance.run,可推测动作实例暴露出了run方法
javascript
// packages/amis-core/src/actions/Action.ts
// 执行动作,与原有动作处理打通
export const runAction = async (
actionInstrance: RendererAction,
actionConfig: ListenerAction,
renderer: ListenerContext,
event: any
) => {
...
try {
let stopped = false;
const actionResult = await actionInstrance.run(
{
...action,
args,
rawData: actionConfig.data,
data: action.actionType === 'reload' ? actionData : data, // 如果是刷新动作,则只传action.data
...key
},
renderer,
event,
mergeData
);
...
} finally {
...
}
};
单个动作
- 可以理解为不依靠组件就能直接执行的动作,比如复制、
toast提示、http请求等
动作定义
- 以简单的
toast动作为例,核心是定义一个ToastAction类,可见确实暴露一个run方法 - 它的
run方法就是调用环境变量中自定义的notify方法
javascript
// packages/amis-core/src/actions/ToastAction.ts
/**
* 全局toast
*/
export class ToastAction implements RendererAction {
async run(
action: IToastAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
if (!event.context.env?.notify) {
throw new Error('env.notify is required!');
}
event.context.env?.notify?.(
action.args?.msgType || 'info',
String(action.args?.msg),
{...action.args, mobileUI: renderer.props.mobileUI}
);
}
}
动作注册
- 类似于组件注册,都是通过键值对的形式存储,键是动作名称,值是动作实例(触发动作:通过名称查找到实例,调用对应实例的
run方法)
javascript
// packages/amis-core/src/actions/Action.ts
// 存储 Action 和类型的映射关系,用于后续查找
const ActionTypeMap: {[key: string]: RendererAction} = {};
// 注册 Action
export const registerAction = (type: string, action: RendererAction) => {
ActionTypeMap[type] = action;
};
- 以
toast动作为例,定义文件的末尾也同时注册了动作
javascript
// packages/amis-core/src/actions/ToastAction.ts
/**
* 全局toast
*/
export class ToastAction implements RendererAction {
...
}
registerAction('toast', new ToastAction());
- 动作注册好要支持获取------
getActionByType方法(在runActions中调用)
javascript
// packages/amis-core/src/actions/Action.ts
// 存储 Action 和类型的映射关系,用于后续查找
const ActionTypeMap: {[key: string]: RendererAction} = {};
// 通过类型获取 Action 实例
export const getActionByType = (type: string) => {
return ActionTypeMap[type];
};
组件动作
- 依赖组件的特性、数据等相关属性的动作
动作定义
- 组件动作入口类定义为
CmptAction,和单个动作不同,单个动作是直接在动作类中执行相关操作,但是组件动作不行,需要通过CmptAction类分发事件到对应的组件中,组件自己执行相关动作。当然,run方法还是必不可少的 - 组件级的更新值、刷新、表单校验也都在这执行;若不是这几种场景则通过
getTargetComponent获取组件实例并调用doAction方法 - 最后一行代码:
CmptAction类的注册方式同单个动作
javascript
// packages/amis-core/src/actions/CmptAction.ts
export class CmptAction implements RendererAction {
async run(
action: ICmptAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
/**
* 根据唯一ID查找指定组件
* 触发组件未指定id或未指定响应组件componentId,则使用触发组件响应
*/
const key = action.componentId || action.componentName;
const dataMergeMode = action.dataMergeMode || 'merge';
const path = action.args?.path;
/** 如果args中携带path参数, 则认为是全局变量赋值, 否则认为是组件变量赋值 */
if (action.actionType === 'setValue' && path && typeof path === 'string') {
...
}
// 如果key没指定,则默认是当前组件
const component = getTargetComponent(action, renderer, event, key);
...
if (action.actionType === 'setValue') {
...
}
// 刷新
if (action.actionType === 'reload') {
...
}
// 校验表单项
if (
action.actionType === 'validateFormItem' &&
getRendererByName(component?.props?.type)?.isFormItem
) {
...
}
// 执行组件动作
try {
const result = await component?.doAction?.(
action,
event.data,
true,
action.args
);
... return result;
} catch (e) {
...
}
}
}
registerAction('component', new CmptAction());
getTargetComponent很简单,就是通过getComponentById、getComponentByName方法查找域里的组件
javascript
// packages/amis-core/src/actions/Action.ts
export const getTargetComponent = (
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>,
key?: string
) => {
let targetComponent = renderer;
if (key && event.context.scoped) {
const func = action.componentId ? 'getComponentById' : 'getComponentByName';
if (typeof event.context.scoped[func] === 'function') {
targetComponent = event.context.scoped[func](key);
}
}
return targetComponent;
};
组件动作定义
- 以
SearchBox搜索框为例,它的doAction方法中就定义了一个clear动作,用于清除输入值的
javascript
@Renderer({
type: 'search-box'
})
export class SearchBoxRenderer extends React.Component<
SearchBoxProps,
SearchBoxState
> {
...
constructor(props: SearchBoxProps, context: IScopedContext) {
super(props);
this.state = {
value: getPropValue(props) || ''
};
...
}
...
doAction(
action: ListenerAction,
data: any,
throwErrors: boolean,
args?: any
) {
const actionType = action?.actionType as string;
if (actionType === 'clear') {
this.setState({value: ''});
}
}
render() {
...
}
}
总结
- 简单理解就是先注册动作,然后再使用动作,相比有调度流的事件简单一点
- 再回顾之前的组件调用,也是同理。
amis低代码的整体设计思路就是把零件都注册好,json中仅配置要调用哪个零件,具体的逻辑都维护在对应的零件中 - 通过这几篇梳理搞清渲染、交互逻辑,完全可以自己实现一个低代码框架了!