众所周知,在Angular中只能创建动态的组件,却无法在组件上附加指令,这就限制了很多功能
最近我实现了Angular的动态组件定义,这个技术的实现,使得动态指令称为可能
难点
- 大多数前端库在写Html模板的时候,其html部分最后一定会转换为其他格式
- 像vue/react这种,转换前后基本等价,并且开发者也可以跳过html模板实现直接写更底层的代码(h(xx),createElement(xxx)).但是Angular中转换前后的代码天差地别,这就造成了一种限制,普通的Angular开发者无法随心所欲的操作底层
实现
- 我通过查看Angular编译部分源码,精简了部分不需要实现的代码,来手动模拟创建组件的过程,也就是相当于以前的
动态编译
(动态编译还是解析模板,但是我跳过了解析,直接动态生成) - 这是通常我们见到的构建好的代码
var AppComponent = class _AppComponent {
constructor() {
}
static \u0275fac = function AppComponent_Factory(__ngFactoryType__) {
return new (__ngFactoryType__ || _AppComponent)();
};
static \u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _AppComponent, selectors: [["app-root"]], decls: 2, vars: 1, consts: [["appD1", "", 3, "ngModelChange", "ngModel"]], template: function AppComponent_Template(rf, ctx) {
if (rf & 1) {
\u0275\u0275elementStart(0, "app-p1")(1, "app-cc1", 0);
\u0275\u0275twoWayListener("ngModelChange", function AppComponent_Template_app_cc1_ngModelChange_1_listener($event) {
\u0275\u0275twoWayBindingSet(ctx.value, $event) || (ctx.value = $event);
return $event;
});
\u0275\u0275elementEnd()();
}
if (rf & 2) {
\u0275\u0275advance();
\u0275\u0275twoWayProperty("ngModel", ctx.value);
}
}, dependencies: [
MatInputModule,
P1Component,
Cc1Component,
D1Directive,
FormsModule,
NgControlStatus,
NgModel
], encapsulation: 2 });
};
- 其中
decls
,vars
,consts
,等变量都是在编译时计算出来的,这里说下好理解的部分,实际上定义更复杂(比如i18n中的一些定义我都忽略了)
- decls 表示有那些标签
- vars 绑定计数
- consts 属性绑定常量提取,属性,绑定,输入
- 那么知道这么多了,下一步就是根据输入的组件和指令,动态计算出这些变量,然后再放到指定位置,从而达成替编译器编译
- 下面是我们手动实现的部分,decls和vars为常量是因为我一层直接调用
ng-template
的一个标签,不会变动,所以也不需要计算.然后在模板内部,会调用组件(一个模板就对应了一个独立的上下文,所以模板里也需要计算decls和vars这些)
static ɵcmp = ɵɵdefineComponent({
type: D,
selectors: [[`d-${index++}`]],
ngContentSelectors: contentArray,
decls: 1,
vars: 0,
consts: result.consts,
template: (rf, ctx) => {
if (rf & 1) {
if (contentArray) {
ɵɵprojectionDef(contentArray);
}
ɵɵtemplate(
0,
templateFn,
2 + (component.contents?.length ?? 0),
result.vars,
'ng-template',
);
}
},
viewQuery: (rf, ctx) => {
if (rf & 1) {
ɵɵviewQuerySignal(ctx.templateRef, TemplateRef, 5 as any);
ɵɵviewQuerySignal(ctx.componentInstance, EL_QUERY, 5 as any);
ɵɵviewQuerySignal(ctx.elementRef, EL_QUERY, 5 as any, ElementRef);
directives.forEach(({ type }, index) => {
ɵɵviewQuerySignal(
ctx.directiveRefList[index],
EL_QUERY,
5 as any,
type as any,
);
});
}
if (rf & 2) {
ɵɵqueryAdvance(3 + directives.length);
}
},
dependencies: result.directiveList,
encapsulation: 2,
changeDetection: 0,
});
完整代码见项目地址
- 最后,经过各种模拟动态编译操作,就实现了动态组件定义
- 而动态组件定义的实现,就意味着可以传入动态的组件和指令来进行创建,从而实现了
动态指令
如何使用
首先来讲,不推荐个人去了解这个技术,首先是因为需要看很多代码及尝试,很多看着比较魔术的代码,其实都是来自解析时的常量枚举和其规则,再一个是因为ng20已经合并的一种动态指令创建的实现(不是我这种实现方案),虽然不一定比我这种实现有更多的应用场景,但是满足大多数人使用了
不仅是动态指令,还可以实现指定投影选择器,给html标签加指令等功能
- npm i @cyia/dynamic-component-define
import { createDynamicComponentDefine } from "@cyia/dynamic-component-define";
export class AppComponent {
envInjector = inject(EnvironmentInjector);
continerRef = viewChild<ViewContainerRef, ViewContainerRef>("continerRef", {
read: ViewContainerRef,
});
classInput = signal({ inputValue2: "hello" });
ngOnInit(): void {
let define = createDynamicComponentDefine({ type: C1Component }, [
{
type: ClassDirective,
inputs: this.classInput,
},
{
type: ClickDirective,
outputs: {
clientEvent: (event: any) => {
console.log("click", event);
},
},
},
]);
let ref = createComponent(define, {
environmentInjector: this.envInjector,
});
this.continerRef()!.createEmbeddedView(ref.instance.templateRef());
}
changeClass() {
this.classInput.update((item) => {
return { ...item, inputValue2: "changedClass" };
});
}
}
其他
- 我自认为在Angular及前端方面有一些深度的积累和经验
- 所以如果您有Angular方面的技术问题,欢迎咨询 wszgrcy@gmail.com
- 项目地址 https://github.com/wszgrcy/dynamic-component-define
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。