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

基于 6.13.0 版本

前期回顾

  1. 【Amis源码阅读】组件注册方法远比预想的多!
  2. 【Amis源码阅读】如何将json配置渲染成页面?
  3. 【Amis源码阅读】低代码如何实现交互(上)

动作触发入口

  • 上篇提到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很简单,就是通过getComponentByIdgetComponentByName方法查找域里的组件
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中仅配置要调用哪个零件,具体的逻辑都维护在对应的零件中
  • 通过这几篇梳理搞清渲染、交互逻辑,完全可以自己实现一个低代码框架了!
相关推荐
前端老宋Running1 小时前
一种名为“Webpack 配置工程师”的已故职业—— Vite 与“零配置”的快乐
前端·vite·前端工程化
用户6600676685391 小时前
从“养猫”看懂JS面向对象:原型链与Class本质拆解
前端·javascript·面试
parade岁月1 小时前
我的第一个 TDesign PR:修复 Empty 组件的 v-if 警告
前端
StarkCoder1 小时前
一次搞懂 iOS 组合布局:用 CompositionalLayout 打造马赛克 + 网格瀑布流
前端
之恒君1 小时前
JavaScript 对象相等性判断详解
前端·javascript
dhdjjsjs1 小时前
Day30 Python Study
开发语言·前端·python
T___T1 小时前
通过 MCP 让 AI 读懂你的 Figma 设计稿
前端·人工智能
清妍_1 小时前
踩坑记录:Taro.createSelectorQuery找不到元素
前端
爬山算法1 小时前
Redis(169)如何使用Redis实现数据同步?
前端·redis·bootstrap