【Amis源码阅读】组件注册方法远比预想的多!

基于 6.13.0 版本

汇总表格

  • 归纳注册组件的入口,注册方法有registerRenderer@Renderer@FormItem@OptionsControl、预注册、消息注册6种
文件位置 注册方式 说明
packages/amis/src/minimal.ts registerRenderer 业务组件
packages/amis-core/src/renderers/builtin.tsx registerRenderer 占位组件
packages/amis/src/renderers/*.tsx @Renderer 业务组件-非表单组件
packages/amis/src/renderers/Form/*.tsx @FormItem 业务组件-表单组件
packages/amis-editor/src/renderer/*.tsx @FormItem 编辑器组件
packages/amis-theme-editor-helper/src/renderers/*.tsx @FormItem 编辑器主题组件
packages/amis-core/src/renderers/Options.tsx @OptionsControl 选择类组件
packages/amis-core/src/renderers/register.ts 预注册/消息注册 自定义组件

registerRenderer

入口

  • 以下两处注册有差异的地方在于getComponentcomponent入参,为何这么设计?
  • 显然前者是为了异步加载,后者是为了同步加载,细节继续往后看
javascript 复制代码
// packages/amis/src/minimal.ts
...
registerRenderer({
  type: 'alert',
  getComponent: () => import('./renderers/Alert') // 异步加载
});

// packages/amis-core/src/renderers/builtin.tsx
...
registerRenderer({
  type: 'spinner',
  component: Placeholder // 同步加载
});

代码实现

registerRenderer

  • renders存储所有组件,renderersTypeMap存储type和组件的映射关系
  • 首先判断是否该组件已注册过且不是占位组件、不支持注册被覆写override等,则抛出异常;若已注册过但其是占位组件或者支持被覆写等,则合并配置,同时如果是异步注册的占位组件需要删除componentRenderer字段,等到渲染时才生成组件
javascript 复制代码
// packages/amis-core/src/factory.tsx

export function registerRenderer(config: RendererConfig): RendererConfig {
	...

  const exists = renderersTypeMap[config.type || ''];
  let renderer = {...config};
  if (
    exists &&
    exists.component &&
    exists.component !== Placeholder &&
    config.component &&
    !exists.origin &&
    !config.override
  ) {
    throw new Error(
      `The renderer with type "${config.type}" has already exists, please try another type!`
    );
  } else if (exists) {
    // 如果已经存在,合并配置,并用合并后的配置
    renderer = Object.assign(exists, config);
    // 如果已存在的配置有占位组件,并且新的配置是异步渲染器,在把占位组件删除
    // 避免遇到设置了 visibleOn/hiddenOn 条件的 Schema 无法渲染的问题
    if (
      exists.component === Placeholder &&
      !config.component &&
      config.getComponent
    ) {
      delete renderer.component;
      delete renderer.Renderer;
    }
  }

	...
}
  • 然后判断config.component是否存在,存在则是同步组件,直接生成组件
  • 如果组件未注册过,则加入到renderers组件列表中,renderersMap也新增映射关系
  • 最后如果有别名(入参可以定义alias数组,别名支持有多个),则每个别名都加映射,同上
javascript 复制代码
export function registerRenderer(config: RendererConfig): RendererConfig {
	...

	// component字段存在则同步加载组件
  if (config.component) {
    renderer.Renderer = config.component;
    renderer.component = rendererToComponent(config.component, renderer);
  }

  if (!exists) {
    const idx = findIndex(
      renderers,
      item => (config.weight as number) < item.weight
    );
    ~idx ? renderers.splice(idx, 0, renderer) : renderers.push(renderer);
  }
  renderersMap[renderer.name] = !!(
    renderer.component && renderer.component !== Placeholder
  );
  renderer.type && (renderersTypeMap[renderer.type] = renderer);
  // 遍历别名生成映射关系
  (renderer.alias || []).forEach(alias => {
    const fork = {
      ...renderer,
      type: alias,
      name: alias,
      alias: undefined,
      origin: renderer
    };

    const idx = renderers.findIndex(item => item.name === alias);
    if (~idx) {
      Object.assign(renderers[idx], fork);
    } else {
      renderers.push(fork);
    }
    renderersTypeMap[alias] = fork;
    renderersMap[alias] = true;
  });
  return renderer;
}

rendererToComponent函数

  • rendererToComponent也值得看一下,这里加入了amis的特性,并且同步加载具体逻辑就是在此处实现的
javascript 复制代码
// packages/amis-core/src/factory.tsx

function rendererToComponent(
  component: RendererComponent,
  config: RendererConfig
): RendererComponent {
  if (config.storeType && config.component) {
    component = HocStoreFactory({
      storeType: config.storeType,
      extendsData: config.storeExtendsData,
      shouldSyncSuperStore: config.shouldSyncSuperStore
    })(observer(fixMobxInjectRender(component)));
  }

  if (config.isolateScope) {
    component = Scoped(component, config.type);
  }
  return component;
}

HocStoreFactory

  • 赋值data.__super,父子级的数据链就生成了
  • 状态管理,注入store字段,可以获取、更新data
  • 最终返回生成的组件(Component是注册时传入的component字段)
javascript 复制代码
// packages/amis-core/src/WithStore.tsx

render() {
	return (
		<Component
			{
				...(rest as any) /* todo */
			}
			{...this.state}
			{...refConfig}
			data={this.store.data}
			dataUpdatedAt={this.store.updatedAt}
			store={this.store}
			scope={this.store.data}
			render={this.renderChild}
		/>
	);
}

Scoped

  • 将组件注册到组件树中,并注入组件通信相关的方法
  • 比如getComponentByName获取组件实例、reload触发组件重渲染、send发送数据等
css 复制代码
Page
├──Form
│  ├──Input
│  └──Select
└──Tpl

异步加载逻辑在哪?换句话说getComponent在何处被调用?

  • 显然上述流程并未体现异步加载的逻辑,既然是异步加载得从渲染时入手
  • 查看amis的渲染流程可以定位到如下:
javascript 复制代码
// packages/amis-core/src/SchemaRenderer.tsx

export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
	render(): JSX.Element | null {
		...
		
		// 组件支持异步加载且组件未生成
		if (this.renderer.getComponent && !this.renderer.component) {
      // 处理异步渲染器
      return rest.invisible ? null : (
        <LazyComponent
          defaultVisible={true}
          getComponent={async () => {
            await loadAsyncRenderer(this.renderer as RendererConfig);
            this.reRender();
            return () => null;
          }}
        />
      );
    }
	}
}
  • this.renderer存储了注册组件时的参数(包含getComponent方法)。判断getComponent存在而component不存在,则异步加载组件
  • LazyComponent组件会监听元素进入视口后调用传入的getComponent方法(此处是入参,而非组件注册时的方法)
  • loadAsyncRenderer则是异步加载组件的入口
javascript 复制代码
// packages/amis-core/src/factory.tsx

export async function loadAsyncRenderer(renderer: RendererConfig) {
  if (!isAsyncRenderer(renderer)) {
    // already loaded
    return;
  }

  const result = await renderer.getComponent!();

  // 如果异步加载的组件没有注册渲染器
  // 同时默认导出了一个组件,则自动注册
  if (!renderer.component && result.default) {
    registerRenderer({
      ...renderer,
      component: result.default
    });
  }
}
  • 逻辑并不复杂,调用getComponent方法获取到组件并传入registerRenderer中,此时是传入component字段,触发同步加载流程

@Renderer

  • 该方法是个装饰器,很简单,直接调用registerRenderer方法传入装饰的组件
  • 在使用amis的项目中也会用该方法注册组件
javascript 复制代码
// packages/amis-core/src/factory.tsx

export function Renderer(config: RendererBasicConfig) {
  return function <T extends RendererComponent>(component: T): T {
    const renderer = registerRenderer({
      ...config,
      component: component
    });
    return renderer.component as T;
  };
}
  • 倒是有个疑问等待大神解答------以Alert组件为例,minimal.ts文件中已经注册了一遍,Alert.tsx中又通过@Renderer注册一遍,好像并无意义且重复注册了?
javascript 复制代码
// packages/amis/src/minimal.ts

registerRenderer({
  type: 'alert',
  getComponent: () => import('./renderers/Alert')
});

// packages/amis/src/renderers/Alert.tsx
@Renderer({
  type: 'alert'
})
export class AlertRenderer extends React.Component<
  Omit<AlertProps, 'actions'> & RendererProps
> {}

@FormItem

  • @Renderer不同的是多调用了一层asFormItem方法,注入了表单组件特有的属性和方法,比如valueonChange
javascript 复制代码
// packages/amis-core/src/renderers/Item.tsx

export function registerFormItem(config: FormItemConfig): RendererConfig {
  let Control = asFormItem(config)(config.component);

  return registerRenderer({
    ...config,
    weight: typeof config.weight !== 'undefined' ? config.weight : -100, // 优先级高点
    component: Control as any,
    isFormItem: true
  });
}

export function FormItem(config: FormItemBasicConfig) {
  return function (component: FormControlComponent): any {
    const renderer = registerFormItem({
      ...config,
      component
    });

    return renderer.component as any;
  };
}

@OptionsControl

  • 用于注册选择类组件,如SelectRadiosButtonGroup等。提供统一的属性、方法如optionsvalueFieldlabelFieldonAdd
  • OptionsControl调用registerOptionsControl方法,再调用registerFormItem,又回到了FormItem的注册流程中
javascript 复制代码
// packages/amis-core/src/renderers/Options.tsx

export function registerOptionsControl(config: OptionsConfig) {
	...

  return registerFormItem({
    ...(config as FormItemBasicConfig),
    strictMode: false,
    component: FormOptionsItem
  });
}

export function OptionsControl(config: OptionsBasicConfig) {
  return function <T extends React.ComponentType<OptionsControlProps>>(
    component: T
  ): T {
    const renderer = registerOptionsControl({
      ...config,
      component: component
    });
    return renderer.component as any;
  };
}

1、下面是两种特殊的注册方式,本人实际开发中并未使用过,推测和微前端、动态渲染、跨域等场景有关 2、核心还是前面几种方法的调用,只是在外面包了一层控制了组件的注册时机

消息注册

  • 通过注册message事件,监听类型为amis-renderer-register-event的消息,调用registerAmisRendererByUsage注册方法
typescript 复制代码
// packages/amis-core/src/renderers/register.ts

// postMessage 渲染器动态注册机制
window.addEventListener(
  'message',
  (event: any) => {
    if (!event.data) {
      return;
    }
    if (
      event.data?.type === 'amis-renderer-register-event' &&
      event.data?.amisRenderer &&
      event.data.amisRenderer.type
    ) {
      const curAmisRenderer = event.data?.amisRenderer;
      const curUsage = curAmisRenderer?.usage || 'renderer';
      if (renderersMap[curAmisRenderer.type]) {
        console.warn(`[amis-core]:动态注册渲染器失败,当前已存在重名渲染器(${curAmisRenderer.type})。`);
      } else {
        console.info(
          '[amis-core]响应动态注册渲染器事件:',
          curAmisRenderer.type
        );
        registerAmisRendererByUsage(curUsage, curAmisRenderer);
      }
    }
  },
  false
);

// 根据类型(usage)进行注册 amis渲染器
function registerAmisRendererByUsage(curUsage: string, curAmisRenderer: any) {
  // 当前支持注册的渲染器类型
  const registerMap: {
    [props: string]: Function
  } = {
	  // 此处可看出若要自定义组件,用下面三种方法足以,不必直接调用registerRenderer
    renderer: Renderer, // 注册非表单组件
    formitem: FormItem, // 注册表单组件
    options: OptionsControl, // 注册选择类组件
  };
  let curAmisRendererComponent = curAmisRenderer.component;
  if (
    !curAmisRendererComponent &&
    window.AmisCustomRenderers &&
    window.AmisCustomRenderers[curAmisRenderer.type] &&
    window.AmisCustomRenderers[curAmisRenderer.type].component
  ) {
	  // 如果传入的渲染器不存在则去window上寻找
    curAmisRendererComponent = window.AmisCustomRenderers[curAmisRenderer.type].component;
  }
  if (
    curAmisRendererComponent &&
    ['renderer', 'formitem', 'options'].includes(curUsage) &&
    registerMap[curUsage]
  ) {
    registerMap[curUsage as keyof typeof registerMap]({
      ...(curAmisRenderer.config || {}),
      type: curAmisRenderer.type,
      weight: curAmisRenderer.weight || 0,
      autoVar: curAmisRenderer.autoVar || false
    })(curAmisRendererComponent);
  }
}

预注册

  • 初始化时会直接调用autoPreRegisterAmisCustomRenderers方法,预加载window.AmisCustomRenderers上挂载的组件
javascript 复制代码
// packages/amis-core/src/renderers/register.ts

// 自动加载预先注册的自定义渲染器
export function autoPreRegisterAmisCustomRenderers() {
  if (window.AmisCustomRenderers) {
    Object.keys(window.AmisCustomRenderers).forEach(rendererType => {
      if (renderersMap[rendererType]) {
        console.warn(`[amis-core]:预注册渲染器失败,当前已存在重名渲染器(${rendererType})。`);
      } else {
        const curAmisRenderer = window.AmisCustomRenderers[rendererType];
        if (curAmisRenderer) {
          registerAmisRendererByUsage(rendererType, curAmisRenderer);
        }
      }
    })
  }
}

// 自动加载并注册 window.AmisCustomRenderers 中的渲染器
autoPreRegisterAmisCustomRenderers();

总结

  • amis项目还是挺庞大的,很多细节若深究下去蛮烧脑,仅研究了主流程为了观摩设计思路
相关推荐
CoderLiz1 天前
Flutter中App升级实现
前端
Mintopia1 天前
⚛️ React 17 vs React 18:Lanes 是同一个模型,但跑法不一样
前端·react.js·架构
李子烨1 天前
吃饱了撑的突发奇想:TypeScript 类型能不能作为跑业务逻辑的依据?(纯娱乐)
前端·typescript
AAA简单玩转程序设计1 天前
救命!Java小知识点,基础党吃透直接起飞
java·前端
叫我詹躲躲1 天前
Vue 3 动态组件详解
前端·vue.js
叫我詹躲躲1 天前
基于 Three.js 的 3D 地图可视化:核心原理与实现步骤
前端·three.js
TimelessHaze1 天前
算法复杂度分析与优化:从理论到实战
前端·javascript·算法
旧梦星轨1 天前
掌握 Vite 环境配置:从 .env 文件到运行模式的完整实践
前端·前端框架·node.js·vue·react
PieroPC1 天前
NiceGui 3.4.0 的 ui.pagination 分页实现 例子
前端·后端
晚霞的不甘1 天前
实战前瞻:构建高可用、强实时的 Flutter + OpenHarmony 智慧医疗健康平台
前端·javascript·flutter