基于 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
入口
- 以下两处注册有差异的地方在于
getComponent和component入参,为何这么设计? - 显然前者是为了异步加载,后者是为了同步加载,细节继续往后看
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等,则抛出异常;若已注册过但其是占位组件或者支持被覆写等,则合并配置,同时如果是异步注册的占位组件需要删除component、Renderer字段,等到渲染时才生成组件
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方法,注入了表单组件特有的属性和方法,比如value、onChange等
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
- 用于注册选择类组件,如
Select、Radios、ButtonGroup等。提供统一的属性、方法如options、valueField、labelField、onAdd等 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项目还是挺庞大的,很多细节若深究下去蛮烧脑,仅研究了主流程为了观摩设计思路