Angular 18+ 高级教程 – 大杂烩

前言

本篇记入一些 Angular 的小东西。

 

Angular 废弃 API 列表

Docs – Deprecated APIs and features

 

Using Tailwind CSS with Angular

依照这个教程做可以了:Install Tailwind CSS with Angular

postcss 和 autoprefixer 即便不安装也可以跑,但 tailwindcss 一定要。

原因是 Angular CLI 里面是有安装了 postcss 和 autoprefixer 的

而 tailwindcss 是 under peerDependencies

如果有 tailwind.config.js 但是没用 yarn install tailwindcss,那是会 warning 的 

 

DomSanitizer

DomSanitizer 是 Angular built-in 的消毒器。

DomSanitizer Provider

它是一个 Root Level Provider,用法非常简单。

export class AppComponent {
  constructor() {
    const domSanitizer = inject(DomSanitizer);
    // 1. 里面包含了一些不安全的东西,e.g. script, style, template
    const unsafeHtml = `
      <h1>Hello World</h1>
      <script>alert('abc')</script>
      <style>*{}</style>
      <template>0</template>
      <p>Lorem ipsum dolor sit amet.</p>
    `;
    // 2. 使用 domSanitizer.sanitize 对 unsafeHtml 消毒
    const safeHtml = domSanitizer.sanitize(SecurityContext.HTML, unsafeHtml);
    console.log('unsafe', unsafeHtml);
    console.log('safe', safeHtml);
  }
}

效果

sanitize 会把不安全的东西消除掉,比如 <script>, <style>, <template> 等等。

不只是 HTML 可以消毒,还有其它的:

  1. Style (v10.0 之后就废弃了,现在已经不消毒 style 了)

    我没有考古出相关信息。

  2. Script

    domSanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript)

    尝试消毒 unsafe script 会直接报错。

  3. URL & Resource URL

    首先要懂得区分 URL 和 Resource URL 

    比如 <img src> 就是 URL,<iframe src> 则是 Resource URL。

    URL 可以消毒,Resource URL 不消毒直接会报错。

DomSanitizer used by Angular Internal

App HTML

<div [innerHTML]="unsafeHtml"></div>

App 组件

export class AppComponent {
  unsafeHtml = `
    <h1>Hello World</h1>
    <script>alert('abc')</script>
    <style>*{}</style>
    <template>0</template>
    <p>Lorem ipsum dolor sit amet.</p>
  `;
}

run compilation

yarn run ngc -p tsconfig.json

App Definition

在做 binding [innerHtml] 时,Angular 会使用 ɵɵsanitizeHtml 函数。

ɵɵsanitizeHtml 函数的源码在 sanitization.ts

除了 HTML 还有其它的也会使用消毒,我就不一一列出来了。

提醒:如果我们自己使用 Renderer2.setProperty(el, 'innerHTML', 'raw html'),它内部不会自动替我们消毒哦,我们需要先消毒了 'raw html' 才传进去。

bypassSecurityTrust

上面有提到 sanitize 不一定会消毒成功变成 safe value,有时候它会直接报错 (也没有检查哦),提醒我们不安全而已。

所以即便我们给的 string 是安全的也没用,它依然会直接报错。这时就需要 bypass。

export class AppComponent {
  constructor() {
    const domSanitizer = inject(DomSanitizer);
    const unsafeScript = '';
    const safeScript = domSanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript);
    console.log(safeScript);
  }
}

直接报错,即使 unsafeScript 只是 empty string。

这种情况下就需要用 bypassSecurityTrust 方法。

export class AppComponent {
  constructor() {
    const domSanitizer = inject(DomSanitizer);
    const unsafeScript = '';
    const bypassScript = domSanitizer.bypassSecurityTrustScript(unsafeScript);
    console.log('bypassScript', bypassScript);
    const safeScript = domSanitizer.sanitize(SecurityContext.SCRIPT, bypassScript);
    console.log('safeScript', safeScript);
  }
}

效果

HTML,Style,Script,URL,Resource URL 都有对应的 bypass 方法

提醒:bypass 就是 skip 掉消毒,它不是 white list 的概念哦,没用一半一半的。相关 Github Issue – Extensible Sanitizer

 

Renderer2 和 inject(DOCUMENT)

Angular 项目通常是运行在游览器上的,但如果项目有需要做 server-side rendering (SSR),那 Angular 会运行在服务端环境 (比如 Node.js)。

这两个环境有两大特点:

  1. 服务端没有 BOM 和 DOM

    比如 document, window 这些在游览器环境才存在

  2. 服务端只负责渲染,没有 event listener 

    游览器才能交互,才能有事件监听

假如我们的 Angular 项目要支持两个环境,首先在渲染阶段,我们要刻意避开使用任何游览器独有的特性,比如 document 和 window。

什么叫渲染阶段呢?基本上除了 event handle 以外,ConstructorPreOrderHooksContentHooksViewHooks 都属于渲染阶段。

所以在 constructor, OnInit, AfterContentInit, AfterViewInit 这些方法里面,我们都不可以使用 document, window 这些。

AfterRenderHooks 则不同,它在 server-side rendering 时是不被调用的,所以 afterNextRender, afterRender 函数里可以使用 document, window 这些。

Add class to body through Renderer2 and DOCUMENT

绝大部分的 DOM 操作,我们都可以通过 Angular MVVM way 去解决,比如 Template Binding Syntax

但 Angular 的范围始终局限在 <app-root /> 里面,如果我们想给 body element 添加一个 class 该怎么做到呢?(要支持 server-side rendering)

解决方法是使用 Renderer2 和 DOCUMENT 代理

export class AppComponent {
  constructor() {
    const renderer = inject(Renderer2);
    const document = inject(DOCUMENT);
    renderer.addClass(document.body, 'dark-theme');
  }
}

在游览器环境下,inject(DOCUMENT) 拿到的是游览器的 document 对象。

在服务端环境下,inject(DOCUMENT) 拿到的是由服务端创建出来的 document 对象。

parseDocument 和 createHtmlDocument 是创建 Document 对象的方法。它底层是用 domino (一个基于 Mozilla's dom.js 的库) 来实现的。 

我没有研究过服务端 document 和游览器 document 具体有没有区别,但我感觉它们肯定是不一样的,虽然它们 interface 一样,这部分等以后我研究了 Angular Server-side rendering 再补上呗。

那我们可以不可以直接 document.body.classList.add('dark-theme') 添加 class? 

我觉得是可以的,但是更安全的做法是使用 Renderer2。

Renderer2 是 Angular 封装的渲染 Service,但凡我们需要 manipulation DOM,不管是 for render 还是 for add event listener 都尽量使用 Renderer2 就对了。

我们看个例子,体会一下使用 Renderer2 和直接操作 DOM 的区别

renderer.listen(this.inputElementRef().nativeElement, 'keydown.enter', () => console.log('enter'));

看到吗,它可以监听 keydown.enter 事件,这个是 Angular 扩展的,如果我们用原生 element.addEventListener 就做不到这个。

提醒:不管是用原生 element.addEventListener 或者 renderer.listen,我们都需要自己 remove event listener。只有用 Template Binding Syntax 监听的事件才会在组件销毁时自动 remove event listener。

题外话:Angular Material 在 2017 年 Angular 使用 domino 之后就不再使用 Renderer2 了,他们直接 DOM manupulation (相关 issue)。但最近 (2024 年 6 月) 有一个 RFC 正在讨论 optional domino...😅,显然直接 DOM Manipulation 还是比较不顺风水的。

No provider for _Renderer2

inject(Renderer2) 必须在组件内才有效,在 Service 会报错。

相关源码在 api.ts

原因是 Service 通常是 Root Provider,inject 时使用的是 Root Injector,而 Renderer2 必须使用 NodeInjector 才能 inject 到。

因为 Renderer2 是从当前 LView 里取出来的,它依赖当前 LView。

每当创建组件 LView 时都会创建 renderer

在游览器环境,rendererFactory 是 DomRendererFactory2

getOrCreateRenderer 方法

它会依据组件 ViewEncapsulation 创建出不同的 Renderer2。

组件默认是 ViewEncapsulation.Emulated,它对应的是 EmulatedEncapsulationDomRenderer2。这个 renderer 继承自 NoneEncapsulationDomRenderer

NoneEncapsulationDomRenderer 又继承自 DefaultDomRenderer2

可以看到主要是对 styles 做了一些 override 而已,绝大部分功能是 DefaultDomRenderer2 实现的。

如果我们想在 Service 里使用 Renderer2 做一些简单的 DOM 操作 (e.g. createElement),那我们可以 inject Renderer2

@Injectable({
  providedIn: 'root',
})
export class TestService {
  constructor() {
    const rendererFactory = inject(RendererFactory2);
    const renderer = rendererFactory.createRenderer(null, null);
    const div = renderer.createElement('div');
  }
}

参数传入 2 个 null,它会返回 DefaultDomRenderer2

总结

  1. 不需要支持 Server-side rendering 的话,我们可以随意使用 DOM 和 BOM。

  2. 要支持 Server-side rendering 的话,在渲染阶段,我们要避开 DOM 和 BOM。

  3. inject(DOCUMENT) 可以让我们在 Server-side rendering 时使用 document 对象,这个 document 是用 domino 生成的,通常用它来 query element (比如 document.body)。

  4. Renderer2 不仅仅可以用于 Server-side rendering,它也适用于游览器环境,inject(DOCUMENT) 负责 "read",那 Renderer2 就是负责 "write"。

    它可以 addClass, setAttribute, appendChild, listen 等等

 

Dynamic Add Event Listener (renderer.listen)

要实现上面这个交互体验,我们需要监听 mouse down 事件,然后监听 document mouse move 事件和 document mouse up 事件。

Angular template binding syntax 没办法实现动态添加 event listener。

所以我们只能直接操作 DOM 了。

首先有一个 button,然后 query 它出来

 <button #button>click me</button>
export class AppComponent {
  button = viewChild.required<string, ElementRef<HTMLElement>>('button', { read: ElementRef });
}

然后 add event listener

export class AppComponent {
  button = viewChild.required<string, ElementRef<HTMLElement>>('button', { read: ElementRef });

  constructor() {
    // after next render 需要用到 injector,所以先提取出来
    const injector = inject(Injector);

    // DOM 操作通常放到 after next render,一来是因为 SSR 不需要执行
    // 二来是因为此时 query button 才准备好
    afterNextRender(() => {
      // 使用 renderer 做事件监听会比较好,因为可以使用 Angular 对事件的扩展功能,比如 (keydown.enter) 语法
      // 当然你嫌麻烦要用原生的也完全可以
      const renderer = injector.get(Renderer2);

      // 虽然 button 是 signal 但其实它不可能变更了,所以这里也不需要顾虑这个,直接用就可以了
      const buttonElement = this.button().nativeElement;

      const mouseDown$ = fromRendererEvent(renderer, buttonElement, 'mousedown');
      const mouseUp$ = fromRendererEvent(renderer, document, 'mouseup');
      const mouseMove$ = mouseDown$.pipe(
        switchMap(() => fromRendererEvent(renderer, document, 'mousemove').pipe(takeUntil(mouseUp$))),
      );

      const destroyRef = injector.get(DestroyRef);
      // 要记得在组件销毁时 remove event listener 哦
      mouseMove$.pipe(takeUntilDestroyed(destroyRef)).subscribe(() => console.log('moving'));
    });
  }
}

// convert Angular renderer.listen to RxJS Observable
export function fromRendererEvent<T>(renderer: Renderer2, target: unknown, eventName: string): Observable<T> {
  return fromEventPattern(
    handler => renderer.listen(target, eventName, handler),
    (_handler, removeEventListenerFn: ReturnType<Renderer2['listen']>) => removeEventListenerFn(),
  );
}

效果

 

PLATFORM_ID, isPlatformBrowser, isPlatformServer

Angular 支持 SSR (Server-side rendering),也就是说我们的程序有可能会运行在 server-side (e.g. Node.js),不一定是 browser。

如果运行在 server-side,那就不能调用 browser 的 BOM。所以,我们的程序需要有能力区分这两种环境,并做出相应的处理。

PLATFORM_ID token, isPlatformBrowser 函数, isPlatformServer 函数,这些变是 Angular 提供给我们用于区分当前执行环境的。

export class AppComponent {
  constructor() {
    const platformId = inject(PLATFORM_ID); // 'browser' 
    const isRunningOnBrowser = isPlatformBrowser(platformId); // true
    const isRunningOnServer = isPlatformServer(platformId);   // false
  }
}

用法非常简单,注入 PLATFORM_ID token,它会拿到一个 string,然后把这个 value 传给 isPlatformBrowser 函数或者 isPlatformServer 函数,它们会返回 boolean。

 

forwardRef

forwardRef 是一个函数,它是用来解决循环引用问题的。

问题说明

我们有两个组件 -- Parent 和 Child

Parent 组件 JS import 了 Child 组件,因为 Template 要输出 <app-child /> 需要在 @Component imports

import { Component } from '@angular/core';
import { ChildComponent } from "./child.component";

@Component({
  selector: 'app-parent',
  standalone: true,
  template: `
    <p>parent works!</p>
    <app-child />
  `,
  imports: [ChildComponent]
})
export class ParentComponent {}

同时 Child 组件也 JS imports 了 Parent 组件,因为要 inject(ParentComponent)

import { Component, inject } from '@angular/core';
import { ParentComponent } from './parent.component';

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [],
  template: `
    <p>
      child works!
    </p>
  `,
})
export class ChildComponent {
  constructor() {
    console.log(inject(ParentComponent, { optional: true }))
  }
}

站 JS import 的角度,这两个 module 已经循环引用了,但不要紧,因为循环引用不代表会坏掉,要看最终获取依赖的时机,只要在获取之前它被赋值了就可以。

好,我们继续。

App 组件输出这两个组件

import { Component } from '@angular/core';
import { ParentComponent } from './parent.component';
import { ChildComponent } from './child.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ParentComponent, ChildComponent],
  template: `
    <app-parent />
    <app-child />
  `,
})
export class AppComponent {}

效果

正常输出👍

现在我们动一点手脚,把 App 组件 import Parent Child 的顺序替换一下。

// before
// import { ParentComponent } from './parent.component';
// import { ChildComponent } from './child.component';

// after
import { ChildComponent } from './child.component';
import { ParentComponent } from './parent.component';

效果

直接报错😱ERROR TypeError: Cannot read properties of undefined (reading 'ɵcmp')

原理说明

为什么会这样呢?其实原理很简单,我们来看看它的代码。

yarn run ngc -p tsconfig.json

app.component.js import 了 child.component.js

child.component.js 又马上 import 了 parent.component.js 

parent.component.js

问题出现在这里,由于是循环引用,ChildComponent 一开始会是 undefined,而 ParentComponent static 是立刻执行的,遇上 undefined 被放进去 dependencies array 里了。

这就导致了 Cannot read properties of undefined (reading 'ɵcmp'),这个 undefined 指的就是 ChildComponent。

那为什么 import 顺序调换它又没有问题呢?

因为它们使用依赖的时机不同,Parent 使用 Child 是在 class static 也就是立刻执行,而 Child 使用 Parent 是在 constructor 里,它不是立刻执行的,是等到 new Child 时才执行,所以就不会有问题。

破解之法

循环引用的破解之法就是压后执行时机,而 forwardRef 就是干这件事的。

我们在 Parent 组件使用 Child 的地方 wrap 上一层 forwardRef

@Component({
  selector: 'app-parent',
  standalone: true,
  template: `
    <p>parent works!</p>
    <app-child />
  `,
  imports: [forwardRef(() => ChildComponent)] // 加上 forwardRef
})
export class ParentComponent {}

compile 之后

原本是直接把 ChildComponent (undefined) 放进 dependencies array,加了 forwardRef 后,dependencies 变成了一个函数,调用函数后才会把 ChildComponent 放进 array,这就是所谓的压后执行。

当 Angular 执行这个函数时,ChildComponent 已经不再是 undefined 了,也就不会再报错了。

forwardRef 不仅仅可以用在 imports,很多地方都可以用,比如 inject(forwardRef(() => ParentComponent)) 等等,

但凡遇到循环引用,试试 forwardRef 就对了。

 

Environment Configuration

参考:

Docs – Configuring application environments

YouTube – Angular Environmental Variables and Configuration

项目开发通常会分几个阶段 (比如 development,staging / UAT,production 等等)。

不同阶段会部署在不同环境,不同环境又需要不同配置,于是就有了 Environment Configuration 概念。

Create environment files

首先创建 environment folder by Angular CLI

ng generate environments

它会创建 1 个 environments folder 和 2 个 environment files。

environment.ts 代表 production 环境配置,environment.development.ts 代表 development 环境配置。

里面是一个空对象

我们可以添加一些常用的配置,比如服务器地址,

这是 development 环境的配置,服务器地址是 localhost 本地

这是 production 环境的配置,服务器地址是一个域名

Use environment config in App

在 App 直接 import environment.ts 就可以了。

上面 import 的是 enviroment.ts,按理说它应该指的是 production 配置,但其实它是动态的,它会依据 Angular build 时选择的环境而决定是使用 development 配置还是 production 配置。

其关键在 angular.json 

这段表示在 build developement 的时候,CLI 会做一个 file replacement 的动作,把 environment.ts 换成 environment.development.ts,

所以即便我们 import 的是 environment.ts 但最终拿到的却是 environment.development.ts 的内容。

Add staging environment

我们尝试添加多一个 staging 环境配置。

创建 environment.staging.ts

然后添加 staging configuration 到 angular.json 

"staging": {
  "budgets": [
    {
      "type": "initial",
      "maximumWarning": "500kb",
      "maximumError": "1mb"
    },
    {
      "type": "anyComponentStyle",
      "maximumWarning": "2kb",
      "maximumError": "4kb"
    }
  ],
  "outputHashing": "all",
  "fileReplacements": [
    {
      "replace": "src/environments/environment.ts",
      "with": "src/environments/environment.staging.ts"
    }
  ]
},
View Code

接着 build staging

ng build --configuration=staging

效果

在最终的 main.js 里,可以看到 host 使用的是 staging 的配置。

Use specific environment configuration

我们也可以直接 import 指定的环境配置。

import environment.ts 是动态的,它会依据 build config 得到不同环境配置。

import environment.development / staging 则不会,它就是拿指定的环境配置。

如果我们想搞多一个 environment.production.ts 拿指定的 production 配置也可以。

总之 Angular 仅仅只是做了一个 file replacement 而已,并没有什么黑魔法。

Get environment configuration from server

Angular 这套 Environment 机制发生在编译期,跟 server 扯不上关系。

所以,如果想 ajax get configuration from server,那我们需要完全自己实现。

isDevMode

如果我们只是想单纯的判断是不是在 development (非 production) 环境下,不一定要搞 environment.ts,Angular 提供了一个快捷的方法 -- isDevMode 函数。

import { ChangeDetectionStrategy, Component, isDevMode } from '@angular/core';

export class AppComponent {
  constructor() {
    if (isDevMode()) {
      console.log('keatkeat123'); // only run in development environment
    }
  }
}

这个 isDevMode 是 Angular 官方推荐使用的,但是 Angular Material 却没有 follow。

原因是它不够智能,在 production 环境下,上面这段 code 是多余的,理应被自动移除,

但是 Angular CLI 并没有把它移除,相关 Github Issue – Provide the ability to remove code needed only in dev mode from the production bundle

那 Angular Material 是怎么做的呢?

首先是 declare 一个 global type ngDevMode variable

dev-mode-types.d.ts

declare const ngDevMode: object | null;

放到 src 里面

注:如果是 Library 需要配置 tsconfig.lib.json

然后像这样使用它

typeof ngDevMode === 'undefined' || ngDevMode

这个判断方式其实和 isDevMode 函数内部大同小异。

经过测试,使用 ngDevMode,Angular CLI 会在非 development 环境下删除其下无用的代码,

而使用 Angular 的 isDevMode 函数的话,Angular CLI 不会删除其下代码。

所以,目前 follow Angular Material 才是正确的,用 ngDevMode 不要用 isDevMode。

另外:Angular CLI 是依靠 "ngDevMode" 来识别的,如果我们给它 wrap 一个函数,那 CLI 就识别不出来了😓。

ngDevMode 源码逛一逛

ngDevMode 主要源码在 ng_dev_mode.ts

它的类型是 null 或者 NgDevModePerfCounters 对象。

NgDevModePerfCounters 是 Angular 用于存 debug 资料的对象。

虽然类型没有声明它可能是 undefined,但是它并没有初始值,所以 runtime 时是有可能出现 undefined 情况的。

那它具体什么时候被赋值呢?

非常非常早,比如在 ɵɵdefineComponent 函数的第一句。

赋值的逻辑是这样

判断 locationString (这个 location 指的是 WorkerGlobalScope),如果是 ngDevMode=false 那就表示 "不是 development 环境",那就不可以赋值对象,要赋值 false。

相反就赋值 NgDevModePerfCounters 对象。

这个 WorkerGlobalScope 我不熟,但是我猜它和 CLI 可能有点关系。

下图是 Angular CLI 的一段源码

当开启 optimization 时,ngDevMode 会被设置成 false,其它情况下 CLI 不会去设置 ngDevMode。

综上,我们得出几个结论:

  1. ngDevMode 初始时是 undefined

  2. 在非常早的时候 (比如 ɵɵdefineComponent),它就会被赋值了。

  3. 如果 CLI 没有给 ngDevMode false 那么就表示 development 环境,赋值 NgDevModePerfCounters 对象。

  4. 如果 CLI 有给 ngDevMode false,那么就表示不是 development 环境,赋值 false。

  5. 对于我们使用者来说,绝大部分情况下 if (ngDevMode) 就 ok 了,不需要

    typeof ngDevMode === 'undefined' || !!ngDevMode

    第一,我们不太可能在它还没有赋值就需要判断,非常罕见。

    第二,即便我们真的需要判断也没辙,因为 undefined 并不代表是 development 环境,它只是还不知道而已。

    第三,Angular 也没有公开 initNgDevMode 函数,所以我们想提早给它赋值也不行。 

    所以 Angular Material 的逻辑是,当 ngDevMode 未赋值,把它当作是 development 环境来处理 (这个规则不一定适用于每个情况)

    至于 !!ngDevMode 只是把 NgDevModePerfCounters 对象强转成 boolean,这只是代码风格而已,不是必须的。

  6. ngDevMode 在 Angular 的类型定义是 null | NgDevModePerfCounters

    ngDevMode 在 Angular Material 的类型定义是 null | object

    看源码后,类型 runtime 应该是 NgDevModePerfCounters | false | undefined,没有看到赋值 null 的情况。

 

Component Inheritance 组件继承

Angular 的组件/指令是 class,class 有继承概念,所以 Angular 的组件/指令也有继承概念。

我们来看几个简单的例子

@Component({
  selector: 'app-dog',
  standalone: true,
  imports: [],
  templateUrl: './dog.component.html',
  styleUrl: './dog.component.scss',
})
export class DogComponent {
  sayHi() {
    console.log('Hi');
  }
}

这是一个 Dog 组件,里面有一个 sayHi 方法。

接着是一个 Cat 组件

@Component({
  selector: 'app-cat',
  standalone: true,
  imports: [],
  templateUrl: './cat.component.html',
  styleUrl: './cat.component.scss',
})
export class CatComponent {
  sayHi() {
    console.log('Hi');
  }
}

Dog 和 Cat 很像,它们都有一模一样的 sayHi 方法。

我们可以用组件继承来进行抽象封装。

创建一个 abstract class AnimalComponent (不一定要是抽象类,具体类也可以)

export abstract class AnimalComponent {
  sayHi() {
    console.log('hi');
  }
}

里面有共享的 sayHi 方法,然后让 Dog 和 Cat 继承 Animal

export class DogComponent extends AnimalComponent {}
export class CatComponent extends AnimalComponent {}

这样就可以了

DogComponent extends AnimalComponent 就是 pure JavaScript 继承概念,没有任何 Angular 黑魔法。

使用 inject 和 afterNextRender

在抽象类里头,我们可以直接使用 inject 和 afterNextRender

export abstract class AnimalComponent {
  constructor() {
    const hostElement: HTMLElement = inject(ElementRef).nativeElement;
    afterNextRender(() => {
      hostElement.classList.add('any-class');
    });
  }

  sayHi() {
    console.log('hi');
  }
}

完全没有问题,因为 Angular 的继承就是 pure JavaScript 继承概念,没有任何 Angular 黑魔法。

使用 input 和 contentChild

在抽象类里头使用 input 和 contentChild 会直接报错

因为这些不算是 pure JavaScript。

Angular compile 对 input,contentChild 这些函数是会特别处理的。

解决方法很简单,那就是给抽象类加上一个 @Directive decorator。

为什么是 @Directive 而不是 @Component 呢?

其实都可以,Angular 组件和指令都可以继承。

指令是不带模板的组件,组件是带了模板的指令。

我们上面这个例子,Animal 没有模板,所以使用 Directive 是正确的。

Override & metadata

抽象的 Animal 有 host binding。

具体的 Dog 也可以有 host binding

要覆盖或扩展都行,非常方便。

简单逛一逛源码

dog.component.js

有 2 个地方和继承有关。

ɵɵgetInheritedFactory 负责实例化组件,它内部其实没有什么特别的,我们把它看作是 new DogComponent 就可以了。

ɵɵInheritDefinitionFeature 负责 merge metadata,比如 override/extend hosting binding 这些,源码在 inherit_definition_feature.ts

总之就是把子组件和父组件的 metadata merge 在一起就是了。

另外,providers 是不能继承的哦。

providers 属于 features,不是所有 features 都能继承

要有 ngInherit = true 才能继承。

而 providers 没有 ngInherit,所以 providers 是不能继承的。

有 ngInherit 的,比如 hostDirectives

还有 NgOnChanges

那如果想继承 providers 可以尝试 workaround 方案 -- hostDirectives

少用继承,多用组合

面向对象里常说,少用继承,多用组合,因为继承往往不够灵活。

但我觉得,只要合适就可以用,没必要为了灵活而灵活。

假如是一目了然的继承关系,那就果断使用继承吧,况且 Angular 对继承支持的也挺好的丫 (mixins class 除外)。

 

目录

想查看目录,请移步 Angular 18+ 高级教程 – 目录

 

posted @ 2024-02-06 18:26  兴杰  阅读(291)  评论(0编辑  收藏  举报