我们来看以下几个例子:
openDialog(enterAnimationDuration: string, exitAnimationDuration: string): void {
this.dialog.open(DialogAnimationsExampleDialog, {
width: '250px',
enterAnimationDuration,
exitAnimationDuration,
});
}
// dialog
@Component({
selector: 'dialog-animations-example-dialog',
templateUrl: 'dialog-animations-example-dialog.html',
})
export class DialogAnimationsExampleDialog {
constructor(public dialogRef: MatDialogRef<DialogAnimationsExampleDialog>) {}
}
熟悉 angular 的话应该知道,所有的 dialog, 都必须建一个 dialog component 用来显示 dialog。这个 component 中有一套获取 dialog 参数的方法。假设我现在需要在 dialog 显示某个已经存在的组件,比如说:
class UserComponnet {
@Input() userName: string;
//.....
}
我们唯一能做的事情,就是将 UserComponent 包裹在一个叫做 UserDialogComponent 的组件中,这个组件只是简单的将 dialog 中的参数传递给子组件的 input。
这种事情做了很多遍以后,很容易就能够想到,有没有可能,我们写一个 dynamicDialogComponnet, 然后将 component 和 props 作为参数传递给它,这样就省去了创建 container component 的过程。
类似:
this.dialog.open(DynamicDialogComponnet, {
width: '250px',
data: {
widgetConfig: {
component: UserComponent,
props: {
userName: 'xxxxxxx'
}}
},
});
那么可能你就要问了,为什么不直接把 userComponent 写成 userDialogComponnet, 要多此一举的抽离呢。有两个原因,将 component 的逻辑与具体的使用场景抽离是有好处的,比如说,当你写 dialog 的时候,userComponent 可能已经存在了。或者 userComponent 可能会用在 dialog 或者页面中,这样使得你不得不写单独的 dialog wrapper 的逻辑。
如果是 react 这里的逻辑就非常简单,直接传递一个 JSX 就行,在 angular 中 component 的本质是 create instance。基本上,我们只能写类似的逻辑
{
component: xxx
props: xx
}
如何实现我们先按下不表。
类似的需求。必须说,我们需要 userComponent 显示一个单独的页面,userName 通过 URL 传递。这时候我们想到了可以直接用 routing.
{
path: 'user',
component: UserComponent
}
这里你会发现,我们遇到了类似的问题,需要在 userComponet 中通过 ActivatedRoute 获取路由信息。或者相同的,我们给 userComponent 包一个 wrapper, userPageComponent。里面只是把 userName 传递给 userComponent。
写多了以后,我们一样会觉得,这个 wrapper 应该被一个组件替代,比如:RouteWrapperComponent,使用方法可以类似于:
{
path: 'user',
component: RouteWrapperComponent,
data: {
widgetConfig: {
component: UserComponent,
props: (activatedRouter: ActivatedRouteSnapshot) => {
return ({
userName: activatedRouter.params.userName,
});
},
}
}
}
如果你用过 AgGrid 的话,会发现,它在渲染 component 的时候巨麻烦。
{
headerName: 'Days of Air Frost',
cellRenderer: DaysFrostRenderer,
cellRendererParams: { rendererImage: 'frost.png' },
}
// DaysFrostRenderer
export class DaysFrostRenderer implements ICellRendererAngularComp {
params!: ICellRendererParams & { rendererImage: string };
rendererImage!: string;
value!: any[];
agInit(params: ICellRendererParams & { rendererImage: string }): void {
this.params = params;
this.updateImage();
}
//,,,
假设,我们要渲染 UserComponent 的话,一样有类似的问题,需要先写一个 wrapper component 实现 ICellRendererAngularComp, 然后再调用 userComponent 传递参数。如果不想每次都创建一个 wrapper component,我们也可以创建一个类似的组件,
{
headerName: 'Days of Air Frost',
cellRenderer: DynamicWidgetRenderer,
cellRendererParams: {
widgetConfig: {
component: UserComponent,
props: (data) => {
return data.userName;
}
}
},
}
综合以上三个例子,我们可以只创建一个 userComponet, 在三个不同的场景下被调用。并且不需要专门为它创建一个 wapper component。
下面,我们来看一下这个 dynamicWidgetComponent 该如何实现:
我们知道,如果是 React,这里的调用可能就一行:<user userName={'zhangsan'} /> 对于 Angular,其实原理也很简单:在 dynamicComponent 中动态 create component, 挂载到当前的 component 中。并且动态的修改 instance,然后出发点 change detection 即可。
// Angular 13 后可能有更简单的方法。
private createComponent(component: ComponentType<C>, ngContents?: NgContents) {
const viewContainerRef = this.insertionDirective.viewContainerRef;
viewContainerRef.clear();
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
this.componentRef = viewContainerRef.createComponent(componentFactory, 0, undefined);
return this.componentRef.instance;
}
那么我们应该如何设置 componet 的参数呢。如果是固定的 props 值,并且只关心 input 的话,很简单:
private setPropsToComponent(componentInstance: C, props: Props<C>,) {
getKeys(props).forEach((propKey) => {
const propValue = props[propKey];
if (this.componentRef?.instance) {
this.componentRef.instance[propKey] = propValue;
}
});
}
如果想像一个普通的 component 一样,props 变化后重新渲染 component, 并且支持 output,就比较麻烦了。我们先看第一个问题。
如何在 props 变化后重新渲染 component。当然,因为 component 本身也是参数,说明它也是可能变化的。我们只需要简单的 dintinctUntilChanged 一下 component 和 props 的值,变化了重新执行对应的 method 就行了。(注意,如果 component 变化了,应该重新 createComponet 并且 重新 setProps)
接下来就是比较棘手的问题,如何处理 output。在 props 中 output 该如何处理呢。我们先看 HTML 中我们是如何调用 output 呢。假设 userComponet 中存在 output userChange: EventEmit
<user userName="zhangsan" (userChange)="setUser($event)"></user>
不难理解,在这里,output 等价于 function 的调用。
($event) => setUser($event)
这也和 react, vue 中使用 function 作为向上传递数据方式的一致。 Angular 通过这种避免 function 传递的方式,有效的避免了 this 的问题。避免 react class component 中需要大量的 bind(this) 的行为。
那么很明显,在作为参数时,我们只能将 function 作为 props 的参数传递,实现 output 的行为。
{
component: UserComponet,
props: {
userName: 'zhangsan',
userChange: (user) => {
setUser(user);
}
},
}
搞清楚用 function 来表示 output 时,我们只需要简单的 subscribe instance 对应的 props 就可以了。
const propValue = props[propKey];
const propValueInComponent = (componentInstance)?.[propKey];
if (typeof propValue === 'function' && propValueInComponent && isObservable(propValueInComponent)) {
this.propsSubscriptions[propKey] = propValueInComponent.subscribe(($event) => {
propValue($event);
});
// ...
当然,我们还需要有效的处理 subscription。
if (typeof propValue === 'function' && propValueInComponent && isObservable(propValueInComponent)) {
const subscription = this.propsSubscriptions?.[propKey];
if (subscription) {
subscription.unsubscribe();
}
this.propsSubscriptions[propKey] = propValueInComponent.pipe(
observeOn(asyncScheduler),
).subscribe(($event) => {
propValue($event);
});
return;
}
为什么要加 asyncScheduler?因为我们希望等 props 都设置完以后再开始执行 subscribe 中的方法,而不是在设置某一个 output 的时候直接触发。至于原理,我们可以单开一遍讲。
以上。