众所周知,在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中的一些定义我都忽略了)
  1. decls 表示有那些标签
  2. vars 绑定计数
  3. 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" };
    });
  }
}

其他


wszgrcy
25 声望10 粉丝

我说我懂Angular,你们信吗