基于 6.13.0 版本

前期回顾

  1. 【Amis源码阅读】低代码如何实现交互(上)

动作触发入口

  • 上篇提到runActions是触发动作的入口(动作执行的前置处理),遍历要触发的动作列表
  • 首先通过getActionByType获取到【单个动作】的实例
  • 若上述情况不存在,且指定了组件ID,则说明是【组件专有动作】
  • 最终获取到动作实例传入runAction中。从这里可见动作分为单个动作、组件动作两类
// 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方法
// 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方法
// 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方法)
// 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动作为例,定义文件的末尾也同时注册了动作
// packages/amis-core/src/actions/ToastAction.ts
/**
* 全局toast
*/
export class ToastAction implements RendererAction {
...
}
registerAction('toast', new ToastAction());
  • 动作注册好要支持获取——getActionByType方法(在runActions中调用)
// 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类的注册方式同单个动作
// 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很简单,就是通过getComponentByIdgetComponentByName方法查找域里的组件
// 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动作,用于清除输入值的
@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中仅配置要调用哪个零件,具体的逻辑都维护在对应的零件中
  • 通过这几篇梳理搞清渲染、交互逻辑,完全可以自己实现一个低代码框架了!