Angular 17+ 高级教程 – Dependency Injection 依赖注入

这篇具有很好参考价值的文章主要介绍了Angular 17+ 高级教程 – Dependency Injection 依赖注入。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

本来是想先介绍 Angular Component 的,但 Component 里面会涉及到一些 Dependency Injection (简称 DI) 的概念,所以还是先介绍 DI 吧。

温馨提醒:如果你对 JS class、prototype 不太熟悉的话,建议你先看这篇 JavaScript – 理解 Object, Class, This, Prototype, Function, Mixins

 文章来源地址https://www.toymoban.com/news/detail-837626.html

什么是 Dependency Injection?

何谓依赖?

class ServiceA {
  plus(num1: number, num2: number) {
    return num1 + num2;
  }
}

首先我们有一个 class ServiceA,它有一个 plus method 可以做加法。

然后我们有另一个 ServiceB

class ServiceB {
  plusAndTimesTwo(num1: number, num2: number) {
    return (num1 + num2) * 2;
  }
}

它有一个 plus and times two method,做了加法之后再乘于二。

上面的代码虽然可以 work,但破坏了 DRY (Don't Repeat Yourself) 原则。

加法已经在 ServiceA 实现了,怎么可以把实现代码 copy paste 到 ServiceB 呢?

所以我们需要在 ServiceB 引入 ServiceA

class ServiceB {
  plusAndTimesTwo(num1: number, num2: number) {
    const serviceA = new ServiceA();
    const afterPlus = serviceA.plus(num1, num2);
    
    return afterPlus * 2;
  }
}

简单嘛,实例化 ServiceA 然后调用 plus 方法可以了。

很好,这就是所谓的 "依赖"。

ServiceB 的某个方法 "依赖" 了 ServiceA 的某个方法。

何谓注入?

上面的代码虽然可以 work,但它又破坏了 OOP 的 SOLID 原则 – Dependency Inversion Principle(依赖反转原则)

我们在 plusAndTimesTwo 里直接实例化 ServiceA,这种写法在一些场景下会给我们带来麻烦。

一个经典的场景是:单元测试

今天我们想测试 plusAndTimesTwo 这个方法是否实现正确。我们会这么写:

export class ServiceA {
  plus(num1: number, num2: number) {
    return num1 - num2;
  }
}

export class ServiceB {
  plusAndTimesTwo(num1: number, num2: number) {
    const serviceA = new ServiceA();
    const afterPlus = serviceA.plus(num1, num2);

    return afterPlus * 2;
  }
}

.test.ts

it("test", () => {
  const service = new ServiceB();
  const result = service.plusAndTimesTwo(1, 1);
  expect(result).toBe(4); // (1 + 1) * 2 = 4
});

结果报错了

Angular 17+ 高级教程 – Dependency Injection 依赖注入

查看代码

Angular 17+ 高级教程 – Dependency Injection 依赖注入

没有什么问题啊,怎么会报错呢?

于是找了老半天,发现原来是 ServiceB 的依赖 ServiceA 的 plus method 的实现代码错了。

Angular 17+ 高级教程 – Dependency Injection 依赖注入

我们想测试的是 plusAndTimesTwo,但由于它依赖了 plus,结果 plus 的错误导致了 plusAndTimesTwo 也出错了。

所以,这算哪门子的 “单元” 测试呢?

怎么办?这时我们需要引入 "inject 注入" 的概念。

首先,我们不要在 plusAndTimesTwo 去实例化 ServiceA。

export class ServiceB {
  constructor(private serviceA: ServiceA) {}

  plusAndTimesTwo(num1: number, num2: number) {
    const afterPlus = this.serviceA.plus(num1, num2);

    return afterPlus * 2;
  }
}

取而代之的是通过 constructor 让外部把 ServiceA “注入” 进来让 plusAndTimesTwo 使用。

然后测试代码改成这样

it("test ServiceB.plusAndTimesTwo()", () => {
  const serviceA = {
    plus() {
      return 2;
    },
  } satisfies ServiceA;

  const service = new ServiceB(serviceA);
  const result = service.plusAndTimesTwo(1, 1);
  expect(result).toBe(4); // (1 + 1) * 2 = 4
});

在创建 ServiceB 时,我们 "provide 提供" 了 ServiceB 所需要的依赖 ServiceA。

并且这个 ServiceA 不是 new ServiceA(),而是专门为了这个测试而 mock(伪造)的 ServiceA 实例。

注意看:serviceA.plus 的 return 2 是 hardcode 写上去的,没有任何 logic 和 formula。

测试结果

Angular 17+ 高级教程 – Dependency Injection 依赖注入

把依赖隔离出来,通过注入的方式去提供,这样就可以非常灵活的控制代码。

上面单元测试中,我们 mock 了 ServiceA 才提供给 ServiceB,这样我们就可以确保 ServiceA 是完全可控的,

不会因为原本 ServiceA 代码实现有误而牵连到 ServiceB,完美的做到了 "单元" 测试。

何谓依赖注入?

上面这种注入式的写法确实让代码灵活了许多,但...这难道没有代价吗?

怎么可能?!anything has price 是自古不变的真理。

我们很快就感受到了这种注入式写法的问题。看例子:

class ServiceA {}

class ServiceB {
  constructor(serviceA: ServiceA) {}
}

class ServiceC {
  constructor(serviceB: ServiceB) {}
}

有 3 个 class,ServiceA、B、C

C 依赖 B 依赖 A

我们想实例化 ServiceC 就得这样写

const serviceA = new ServiceA();
const serviceB = new ServiceB(serviceA);
const serviceC = new ServiceC(serviceB);

或者

const serviceC = new ServiceC(new ServiceB(new ServiceA()));

本来实例化依赖是 class 内部自己封装的,但被我们隔离了出来,变成外部需要提供依赖。

这就导致每一次想要实例化一个 class 时,我们就需要 provide 这个 class 的所有依赖,还有这个依赖的依赖的依赖。。。

这个灵活的代价有点大丫。

聪明的人们很快发现了一个关键点 -- 我们其实只是想在某一些场景灵活的替换依赖(比如,单元测试的时候)

绝大部分的时候,new ServiceA 就可以满足依赖了,并不需要什么替换。

于是,针对下面这种简单场景

const serviceC = new ServiceC(new ServiceB(new ServiceA()));

我们可以把实例化依赖封装起来,让它变成自动化,这就是所谓的依赖注入。

依赖注入长这样(注:不同语言有不同的实现方式,也有一些变种功能,但核心是差不太多的,ASP.NET Core 可以看这篇)

const injector = Injector.create({
  providers: [ServiceA, ServiceB, ServiceC]
});
const serviceC = injector.get(ServiceC);

上面这个是其中一种 Angular DI 的写法。(注:只是其中一种写法,不用太在意,只是为了演示而已)

我们有 2 件事情要做,第一件是实例化 ServiceC,第二件是实例化其所有的依赖。

我们就把这 2 件事封装起来,让 injector 来负责呗。

首先把所有的 class(依赖)都 provide(提供)给 injector(注入器),然后告诉 injector 我们要 get(获取 / 实例化 / 注入) 一个 ServiceC。

injector 内部就会替我们完成上面 2 件事,实例化 ServiceC 和其所有依赖。

哇,代码瞬间就干净了。

依赖注入的实现原理

injector 是如何 "知道" class 有哪些依赖的呢?

class ServiceC {
  constructor(serviceB: ServiceB) {}
}

ServiceC 的依赖被声明到了 constructor 参数中。如果是静态语言,比如 C#,

injector 可以通过反射读取到 ServiceC constructor 的参数类型,这样就知道它有哪些依赖了。

但是 JS 没有反射丫。Angular 是怎样做到的呢?自己想一想吧,下一 part 揭晓。

 

Angular DI の 最简化版

Angular DI 极其复杂,我们从最简单的版本看起。

class ServiceA {}

@Injectable()
class ServiceB {
  constructor(serviceA: ServiceA) {}
}

有 2 个 class,ServiceB 依赖 ServiceA

const injector = Injector.create({
  providers: [ServiceA, ServiceB],
});
const serviceB = injector.get(ServiceB);

把 2 个 class 提供给 injector,然后通过 injector 获取 ServiceB。

injector 会替我们实例化 ServiceB,并且满足其依赖,相当于:

const serviceB = new ServiceB(new ServiceA());

Angular 如何知道 class 有哪些依赖?

JS 没有反射,那 Angular 怎么能从 ServiceB 的 constructor 感知到其依赖 ServiceA 呢?

class ServiceB {
  constructor(serviceA: ServiceA) {}
}

答案是黑魔法 Compilation。

ServiceB 经过 compile 后会变成这样

Angular 17+ 高级教程 – Dependency Injection 依赖注入

它多了一个 ɵfac 静态方法。

从代码上可以推测出 injector.get(ServiceB),其实并不是直接执行了 new ServiceB(new ServiceA()),它只是调用了 ServiceB.ɵfac()。

而 ɵfac 内容才是 new ServiceB( inject(ServiceA) )。这句代码便是 compiler 透过反射 constructor 得知 ServiceB 依赖 ServiceA 后写出来的。

另外,inject(ServiceA) 是一个递归实例化依赖函数,里面一定是调用了 ServiceA.ɵfac()。以此类推,一直到所有的依赖全部被实例化。

简而言之,虽然 JS 没有反射,但是 Angular compiler 可以反射,然后自动编写出实例化依赖的代码。这就是 Angular DI 的实现秘诀啦。

@Injectable()

眼尖的朋友可能已经发现了 @Injectable() 在上面的 class ServiceB

@Injectable()
class ServiceB {
  constructor(serviceA: ServiceA) {}
}

@Injectable decorator 有两种用途,我们先了解其中一个就好。

如果一个 class 没有声明 @Injectable decorator 那 Angular compiler 就不会理它。

@Injectable()
class ServiceB {
  constructor(serviceA: ServiceA) {}
}

class ServiceC {
  constructor(serviceA: ServiceA) {}
}

const injector = Injector.create({
  providers: [ServiceA, ServiceB, ServiceC],
});

ServiceC 和 ServiceB 一样都声明了依赖 ServiceA,并且 ServiceC 也在 providers 里。唯一的区别是 ServiceC 没有 @Injectable decorator。

下面是 compile 后的代码

Angular 17+ 高级教程 – Dependency Injection 依赖注入

ServiceC 只是一个普普通通的 class,没有 static 方法 ɵfac。

若我们尝试去注入它

const serviceB = injector.get(ServiceC);

将得到一个 runtime error。

Angular 17+ 高级教程 – Dependency Injection 依赖注入

所以,记得要加上 @Injectable() 给有需要依赖的 class 哦。

小考题:ServiceA 没有任何依赖,那它需要 @Injectable() 吗?

答案:不需要。但通常我们还是会放的,因为上面说了,@Injectable() 还有第二个用途,下面会教。

 

中场休息,小总结

到目前为止,Angular 的 DI 和其它框架(后端框架)使用的 DI 还算基本类似。

  1. 都是通过 class constructor 参数做注入

  2. 需要 provide 所有的 class 给 injector

  3. injector 负责实例化和满足所有 class 的依赖。

比较特别的地方是 @injectable() decorator,由于 JS 没有 reflection,Angular 无法从 class constructor 参数反射出其依赖的 class,

所以 Angular 需要利用 compiler + decorator 去实现 reflection。

好,到这边,我们大概学习了 Angular DI 大约 20% 的内容...

下半场,我们来学习 Angular DI 独有的一些特性。

 

Injector and Provider 详解

not only for class

上面例子中,我们都是在处理 class,但其实 Angular DI 并不限于 class,任何类型都可以使用 DI 概念。

const injector = Injector.create({
  providers: [{ provide: 'key', useValue: 'hello world' }],
});

const value = injector.get('key');
console.log(value); // 'hello world'

看到吗,上面代码中,完全没有 class 的影子,但 injector 照常工作。

抽象理解 provider 和 injector

provider 的特性

class 是一种 provider,但 provider 不仅仅只限于 class。

抽象的看,provider 是一个 key value pair 对象。

key 的作用是为了识别。

value 则是一个提供最终值的 factory 函数。

只要能满足这 2 点,那它就可以被作为 provider。

那我们来看看 class 是否满足这 2 点特性。

  1. class can be a key

    class ServiceA {}
    
    const map = new Map();
    map.set(ServiceA, 'value');
    console.log(map.get(ServiceA)); // 'value'

    class 具备识别性,所以它满足 provider key 的特性。

  2. class can be a value factory

    class 有 constructor 并且能实例化出对象。

    这就满足了 provider value factory 的特性。

    所以,结论是 class 可以被当成 provider 使用。

Injector 的特性

injector 不仅仅是实例化机器。

抽象的看,injector 第一个任务是通过 key 查找出指定的 provider,这个 key 只要具备可识别性就可以了。比如:string,class,symbol 等等都具备识别性。

第二个任务是通过 provider value factory 生产出最终的值。当然如果这个 factory 需要依赖,injector 会先查找它所需要的依赖,注入给 factory 函数。

拿 class ServiceB 做例子:

injector 要找一个 key = ServiceA 的 provider。找到以后通过 value factory(也就是 ServiceA.ɵfac)生产出最终的值(ServiceA 实例)。

Provider & StaticProvider

Angular 有多种不同形态的 Provider,class 只是其中一种。

我们来过一遍。(虽然有点多,但它们都满足上面 2 个 provider 的特性,所以其实很好理解的)

Injector.create 的 interface 长这样

Angular 17+ 高级教程 – Dependency Injection 依赖注入

Provider 和 StaticProvider 是所有 Provider 的抽象。

Angular 17+ 高级教程 – Dependency Injection 依赖注入

Angular 17+ 高级教程 – Dependency Injection 依赖注入

它俩是有重叠的,总的来说是 TypeProvider、ClassProvider、StaticClassProvider、ConstructorProvider、FactoryProvider、ValueProvider、ExistingProvider。

FactoryProvider

FactoryProvider 是最 low layer 的 Provider,它可以做到其它所有 Provider 能做到的,所以我们只要理解了 FactoryProvider,再去理解其它 Provider 就非常容易了。

const myValueProvider: FactoryProvider = {
  provide: 'key',
  useFactory: () => {
    return 'my value';
  },
};

这是一个 FactoryProvider,上面讲过,Provider 需要有两个特性

第一个是它需要有一个可识别的 key,在这个例子中就是 myValueProvider.provide

第二个是它需要有一个 value factory 方法,在这个例子中就是 myValueProvider.useFactory

FactoryProvider 是这样被使用的

const injector = Injector.create({
  providers: [myValueProvider],
});

const value = injector.get('key'); // my value

FactoryProvider with InjectionToken

上面的代码有个小问题

Angular 17+ 高级教程 – Dependency Injection 依赖注入

由于我使用了 string 作为 Provider 的识别 key,所以它出现了 warning。原因是 string 太容易撞了,Jser 都知道作为识别 key 的话,Symbol 比 string 可靠。

而由于 Angular 需要有类型概念,所以连 JS 的 Symbol 都无法满足它的需求,于是 Angular 自己搞了一个 InjectionToken 类来替代 JS 的 Symbol。

下面这个是用 InjectionToken 替代了原本 string key 的写法

const MY_VALUE_TOKEN = new InjectionToken<string>('MyValue');

const myValueProvider: FactoryProvider = {
  provide: MY_VALUE_TOKEN,
  useFactory: (): string => {
    return 'my value';
  },
};

const injector = Injector.create({
  providers: [myValueProvider],
});

const value = injector.get(MY_VALUE_TOKEN);

如果类型想要更准确的写法,还可以修一点

type GetInjectionTokenType<T> = T extends InjectionToken<infer R> ? R : never;

const myValueProvider: FactoryProvider = {
  provide: MY_VALUE_TOKEN,
  useFactory: (): GetInjectionTokenType<typeof MY_VALUE_TOKEN> => {
    return 'my value';
  },
};

FactoryProvider with Dependency

如果我们的 value factory 有依赖,可以这样写

const MY_VALUE_1_TOKEN = new InjectionToken<string>('MyValue1');
const MY_VALUE_2_TOKEN = new InjectionToken<string>('MyValue2');

const myValue1Provider: FactoryProvider = {
  provide: MY_VALUE_1_TOKEN,
  useFactory: () => 'my value 1',
};

const myValue2Provider: FactoryProvider = {
  provide: MY_VALUE_1_TOKEN,
  useFactory: (myValue1: string) => myValue1 + 'and my value 2',
  deps: [MY_VALUE_1_TOKEN],
};

const injector = Injector.create({
  providers: [myValue1Provider, myValue2Provider],
});

const value2 = injector.get(MY_VALUE_2_TOKEN); // my value 1 and my value 2

关键在 myValue2Provider

const myValue2Provider: FactoryProvider = {
  provide: MY_VALUE_1_TOKEN,
  useFactory: (myValue1: string) => myValue1 + 'and my value 2',
  deps: [MY_VALUE_1_TOKEN],
};

属性 deps 用来声明 value factory 的依赖。当 useFactory 被调用时,injector 会注入其依赖。

好,至此我们就掌握了最底层的 Provider,其它的 Provider 都可以用 FactoryProvider 来实现,下面我一一列出来,并且附上 FactoryProvider 实现的版本。

ValueProvider

const VALUE_TOKEN = new InjectionToken<string>('Value');

const valueProvider: ValueProvider = {
  provide: VALUE_TOKEN,
  useValue: 'Derrick',
};

const injector = Injector.create({
  providers: [valueProvider],
});

const name = injector.get(VALUE_TOKEN);
console.log(name); // 'Derrick'

value provider 和 factory provider 的区别是,它省略了 value factory,直接 hardcode 一个 value。

用 factory provider 重新表达上面这个例子,那会是这样

const valueProvider: FactoryProvider = {
  provide: VALUE_TOKEN,
  useFactory: () => 'Derrick'
};

TypeProvider、ConstructorProvider、ClassProvider、StaticClassProvider

这 4 个 Provider 都用于 class,它们只有微微的区别,我没有去研究为什么 Angular 搞了 4 个傻傻分不清楚的东西,但我猜想可能是历史原因,我们过一遍就好,真实开发中是不会用到的。

class ServiceA {}
class ServiceB {
  constructor(serviceA: ServiceA) {}
}

ServiceB 依赖 ServiceA(注:我刻意不放 @Injectable() 来凸显不同 Provider 之前的特性)

TypeProvider

const injector = Injector.create({
  providers: [
    ServiceA,
    ServiceB satisfies TypeProvider,
  ],
});

TypeProvider 就是一个普通的 class。上面的例子都是用这个。

由于我没有放 @Injectable() 若调用 injector.get(ServiceB) 将会报错哦。

ConstructorProvider 

providers: [
  ServiceA,
  { provide: ServiceB, deps: [ServiceA] } satisfies ConstructorProvider
],

ConstructorProvider 和 TypeProvider 的区别是它多了一个属性 deps。

它的作用是让我们放入 constructor 中的依赖,按顺序放。有了这个,即便没有 @Injectable() 也可以成功 get 到 ServiceB。

ClassProvider

const SERVICE_B_TOKEN = new InjectionToken<ServiceB>('ServiceB');

providers: [ ServiceA, { provide: SERVICE_B_TOKEN, useClass: ServiceB } satisfies ClassProvider, ],

ClassProvider 的特别之处是它允许我们定义任意类型的 key。

provide 声明了 key,useClass 声明了 value factory。(注:ClassProvider 不能声明 deps 哦。)

get 的方式是

const serviceB = injector.get(SERVICE_B_TOKEN);

SERVICE_B_TOKEN 是 key,injector 会找到这个 provider,发现 provider 声明了 useClass,表示它是一个 ClassProvider,然后就会实例化 useClass 的值。

任意 key 的好处是可以声明抽象,提供具体。比如

{ provide: AnimalService, useClass: DogService } satisfies ClassProvider,

依赖抽象总是好的嘛,说不定那天可以替换成一个优化版的具体实现,到时就只需要替换 useClass 的值,而不是全场替换依赖。

StaticClassProvider

providers: [
  ServiceA,
  { provide: SERVICE_B_TOKEN, useClass: ServiceB, deps: [ServiceA] } satisfies StaticClassProvider,
],

StaticClassProvider是最完整的 class provider。

它可以定义 key,也可以声明 deps。相等于 ConstructorProvider + ClassProvider。

用 factory provider 重新表达上面这个例子,那会是这样

providers: [
  { provide: ServiceA, useFactory: () => new ServiceA() },
  {
    provide: SERVICE_B_TOKEN,
    useFactory: (serviceA: ServiceA) => new ServiceB(serviceA),
    deps: [ServiceA],
  },
],

ExistingProvider

ExisitingProvider 的作用是提供 alias key。

就是说,2 个不同的 key,但其实指向的是同一个 provider。

const NAME_1_TOKEN = new InjectionToken<string>('Name1');
const NAME_2_TOKEN = new InjectionToken<string>('Name2');

const injector = Injector.create({
  providers: [
    { provide: NAME_1_TOKEN, useValue: 'Derrick' },
    { provide: NAME_2_TOKEN, useExisting: NAME_1_TOKEN },
  ],
});

const name = injector.get(NAME_2_TOKEN);
console.log(name); // 'Derrick'

key name1 是 value provider,它提供了 value 'Derrick'。

key name2 是 existing provider,它没有提供任何值,但它提供了一个 key,这个 key 指向 name1。

所以 injector 找 name2 > 被指向 name1 > 最终就获得了 name1 的值。

有点像 URL 301 redirect 那个概念。

用 factory provider 重新表达上面这个例子,那会是这样

{ provide: NAME_2_TOKEN, useFactory: (name1: string) => name1, deps: [NAME_1_TOKEN] },

 

@Inject()

上面的例子中,我们的 class 总是依赖 class。所以写法是这样:

class ServiceA {}

@Injectable()
class ServiceB {
  constructor(serviceA: ServiceA) {}
}

const injector = Injector.create({
  providers: [ServiceA, ServiceB],
});

const serviceB = injector.get(ServiceB);

但是,provider 不一定是 class 丫,class 的依赖难道不可以是一个 ValueProvider 吗?

我们来试试看

const VALUE_TOKEN = new InjectionToken<string>('Value');

@Injectable()
class ServiceB {
  constructor(value: string) {} 
}

const injector = Injector.create({
  providers: [{ provide: VALUE_TOKEN, useValue: 'value 123' }, ServiceB],
});

const serviceB = injector.get(ServiceB);

关键在 ServiceB 的 constructor

Angular 17+ 高级教程 – Dependency Injection 依赖注入

injector 查找需要 key,这里声明依赖 value 但没有表达出它依赖的是 VALUE_TOKEN,所以这表达肯定是不够的。

Angular 17+ 高级教程 – Dependency Injection 依赖注入

这样也不对,虽然表达了依赖的 key,但是 VALUE_TOKEN 是 variable,不是类型,语法错误。

要解决这个问题有 2 种方法。

第一种是改用 ConstructorProvider。

providers: [{ provide: VALUE_TOKEN, useValue: 'value 123' }, ServiceB satisfies TypeProvider]

把 TypeProvider 改成 ConstructorProvider

providers: [
  { provide: VALUE_TOKEN, useValue: 'value 123' }, 
  { provide: ServiceB, deps: [VALUE_TOKEN] } satisfies ConstructorProvider
]

ConstructorProvider 可以精准声明依赖。甚至不需要 @Injectable() decorator。但这种把逻辑分两个地方的写法严重破坏代码管理,所以不推荐这种写法。

第二种方法是使用 @Inject() decorator。

@Injectable()
class ServiceB {
  constructor(@Inject(VALUE_TOKEN) value: string) {}
}

同时表达了 key 和类型。

compile 后长这样

Angular 17+ 高级教程 – Dependency Injection 依赖注入

用 @Inject 注入 class

@Injectable()
class ServiceB {
  constructor(@Inject(ServiceA) value: ServiceA) {}
}

@Inject 可以注入所有的 provider,当然也包括 class provider。

上面这段代码和我们单写 class 依赖

constructor(serviceA: ServiceA) {}

compile 出来是一摸一样的,你可以认为单写 class 只是一个便捷的写法而已,其原理还是 @Inject(key)。

 

inject

这段代码

@Injectable()
class ServiceB {
  constructor(serviceA: ServiceA) {}
}

会被 compile 成

Angular 17+ 高级教程 – Dependency Injection 依赖注入

ɵfac 其实没有很特别,它只是做了 new ServiceB( inject(ServiceA) ) 而已。

最关键的是 ɵɵinject 这个函数。

按这个思路走。。。假若我们也可以使用这个 inject 函数。那代码是否可以改成

class ServiceB {
  constructor() {
    const serviceA = ɵɵinject(ServiceA);
  }
}

我们推测看看

function inject() {}
function constructor(depend: any) {}

有一个全局的 inject 函数,和一个 constructor 函数 with 一个依赖参数。

const depend = inject();
constructor(depend);

通过 inject 获取到 depend,然后调用 constructor 传入 depend

对比

function constructor() {
  const depend = inject();
}
constructor();

直接在 constructor 内调用 inject 获得 depend。

这 2 种写法在语法上都是成立的。

所以,Angular 确实允许我们使用这个 inject 函数,也可以像上面那样子去注入依赖,甚至这是 v14 版本后的 best practice。

完整的例子:

const VALUE_TOKEN = new InjectionToken<string>('Value');

class ServiceA {
  constructor() {
    const value = inject(VALUE_TOKEN); // 'Derrick'
  }
}

const injector = Injector.create({
  providers: [{ provide: VALUE_TOKEN, useValue: 'Derrick' }, ServiceA],
});

const serviceA = injector.get(ServiceA);

注1:

我甚至省略了 @Injectable() decorator。因为这个写法就相等于,我们自己写了 ɵfac 方法,

所以就不在需要 compiler 通过反射 constructor 参数写出 ɵɵinject 了。

注2:

我们使用的是 inject 函数,而不是 ɵɵinject 函数。但凡 starts with ɵ 这个 symbol 的东西都是 Angular 框架 internal 用的,开发者不要去用哦,那都是不稳定的。

而 inject 则是 Angular 公开给我们使用的,其实它内部也是调用了 ɵɵinject 函数哦。

injection context & runInInjectionContext

下面这段代码报错了。

import { inject } from '@angular/core';

class ServiceA {}
class ServiceB {
  constructor() {
    console.log(inject(ServiceA));
  }
}

const serviceB = new ServiceB(); // Error: inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`

为什么会报错?为什么 Error 会说 inject() 只能用在一些特定的地方?

我们知道 DI 概念核心是 Injector 和 Provider,上面的代码里,我们没有任何 Injector,所以 inject 自然就不成立。

inject 是一个全局方法,它并没有什么黑魔法,它之所以可以实现依赖注入,依靠的是作用域内(aka injection context)的 Injector。

上面 new ServiceB() 并不会产生任何 injection context,所以 inject 自然就失效了。

来看一个成功的例子

class ServiceA {}
class ServiceB {
  constructor() {
    console.log(inject(ServiceA));
  }
}
const injector = Injector.create({
  providers: [ServiceA, ServiceB],
});

const serviceB = injector.get(ServiceB);

injector.get 会产生 injection context,也就是说会有一个全局变量 injector,所以全局函数 inject 可以使用这个 injector 去找到 ServiceA。

除了上面这种方式,还有一个方式可以创建出 injection context。

runInInjectionContext(injector, () => {
  const serviceA = inject(ServiceA);
  const serviceB = inject(ServiceB);
});

总之

1. inject 只可以在 injection context 内使用。

2. 要创造出 injection context 你要先创建一个 injector。

3. 总结就是:要用 inject 就得先要有 injector。

 

@Inject()、Provider.deps、inject 傻傻分不清楚

由于众多历史原因,导致了 Angular 有多种方式可以实现同一个功能,这对开发来说是非常不友好的,但幸好 Angular 总是有 best practice。

只要我们乖乖 follow best practice,哪怕傻傻分不清楚也能顺风顺水的开发项目。

不过呢,要搞清楚它们也不是件很困难的事,这里我就来讲讲 @Inject()、inject()、Provider.deps 傻傻分不清楚的地方呗。

@Inject()

@inject 主要的使用场景是在 class constructor 注入 token。

const VALUE_TOKEN = new InjectionToken<string>('Value');

@Injectable() class ServiceA { constructor(@Inject(VALUE_TOKEN) value: string) {} } const injector
= Injector.create({ providers: [ServiceA, { provide: VALUE_TOKEN, useValue: 'Hello World' }], }); const serviceA = injector.get(ServiceA);

注: 要搭配 @Injectable decorator 哦。

inject 函数可以完全取代 @Inject decorator,上面代码可以改成这样

class ServiceA {
  constructor() {
    const value = inject(VALUE_TOKEN);
  }
}
连 @Injectable decorator 也可以省略掉哦。

Provider.deps

除了 @Inject,还有一种注入方式是通过 Provider.deps

const VALUE_1_TOKEN = new InjectionToken<string>('Value1');
const VALUE_2_TOKEN = new InjectionToken<string>('Value2');

const injector = Injector.create({
  providers: [
    { provide: VALUE_1_TOKEN, useValue: 'value 1' },
    {
      provide: VALUE_2_TOKEN,
      useFactory: (value1: string) => `${value1} and value2`,
      deps: [VALUE_1_TOKEN],
    },
  ],
});

const value2 = injector.get(VALUE_2_TOKEN);

这个同样可以被 inject 函数替代。

providers: [
  { provide: VALUE_1_TOKEN, useValue: 'value 1' },
  {
    provide: VALUE_2_TOKEN,
    useFactory: (value1: string) => `${inject(VALUE_1_TOKEN)} and value2`,
  },
],

inject 函数

显然,inject 函数就是用来替代 @Inject 和 Provider.deps 的,所以尽量用 inject 少用 @Inject 和 Provider.deps。

 

Multiple Provider

当出现相同 Provider 时(provide === provide 代表是相同的 Provider),第二个会覆盖第一个

const VALUE_TOKEN = new InjectionToken<string>('Value');
const injector = Injector.create({
  providers: [
    { provide: VALUE_TOKEN, useValue: 'first value' },
    { provide: VALUE_TOKEN, useValue: 'second value' },
  ],
});
const value = injector.get(VALUE_TOKEN); // second value

injector.get 获取到的是 second value。

我们可以通过设置 multi: true 来声明不要 override,取而代之的是把所有的 value 放入 array 返回。

{ provide: VALUE_TOKEN, useValue: 'first value', multi: true }, // 添加 multi: true
{ provide: VALUE_TOKEN, useValue: 'second value', multi: true }, // 添加 multi: true

最终会得到所有 values 的 array

const value = injector.get(VALUE_TOKEN); // ['first value', 'second value']

 

Injection Optional and Default Value

class ServiceA {}
const injector = Injector.create({
  providers: [],
});
const serviceA = injector.get(ServiceA); // Error: No provider for ServiceA

没有 Provider,inject 时就会报错。如果我们不想它报错,想自己处理的话,可以使用 default value。

const serviceA = injector.get(ServiceA, null); // null

get 的第二参数是当找不到 Provider 时,提供一个 default value,这样也就不会报错了。

inject 函数的写法

class ServiceB {
  constructor() {
    const serviceA = inject(ServiceA, { optional: true }); // null
  }
}

注: 它的值是 null 而不是 undefined 哦,Angular 许多地方都是用 null 多余 undefined 的。

@inject 的写法

@Injectable()
class ServiceB {
  constructor(@Inject(ServiceA) @Optional() serviceA: ServiceA | null) {
    console.log(serviceA);
  }
}

用到了 @Optional decorator,decorator 的写法已经逐渐被淘汰了,建议统一用 inject 函数就好了,下面的教程我也不会再提了。

Provider.deps 的写法

const injector = Injector.create({
  providers: [
    {
      provide: ServiceB,
      useFactory: (serviceA: ServiceA | null) => {
        return new ServiceB(serviceA);
      },
      deps: [[new Optional(), ServiceA]], // 关键
    },
  ],
});

这个写法比较冷门,而且 deps 的类型是 any[],没有翻文档很难想到它是这么写的。另外 Optional 和 @Optional 是同一个 Optional 哦。

Provider.deps 的写法也已经逐渐被淘汰了,建议统一用 inject 函数就好了,下面的教程我也不会再提了。

 

循环引用

Angular DI 不支持循环引用。

class ServiceA {
  serviceB = inject(ServiceB); // Error: Circular dependency in DI

  // 上面的写法相等于下面 constructor 写法,这个是基本 JS 语言
  // constructor(){
  //   serviceB = inject(ServiceB);
  // }
}

class ServiceB {
  serviceA = inject(ServiceA);
}

const injector = Injector.create({
  providers: [ServiceA, ServiceB],
});
const serviceB = injector.get(ServiceB);

直接报错 circular dependency。

Angular 17+ 高级教程 – Dependency Injection 依赖注入

破解之法

有些循环引用是设计问题,我们应该要修改设计,但绝大部分情况是因为依赖注入的局限。

依赖注入要求通过 constructor 作为注入入口,但很多时候我们并不需要在 constructor 阶段使用到依赖,更多的是在某些方法中才使用到依赖。

这种情况下循环依赖在设计上,其实并没有问题。

我们可以通过先注入 Injector 的方式来破解。

class ServiceA {
  injector = inject(Injector);
  method() {
    const serviceB = injector.get(ServiceB);
  }
}

ServerA 不在 constructor 阶段注入 ServiceB,就不会有循环依赖了,

取而代之的是注入当前的 Injector,在 method 被调用时才去注入 ServiceB。

注:下面这个下方是错误的

class ServiceA {
  method() {
    const serviceB = inject(ServiceB);
  }
}

因为 inject 没有在 injection context 中,只有 constructor 阶段才在 injection context 哦。

我们可以通过 runInInjectionContext 来处理。

class ServiceA {
  injector = inject(Injector);
  method() {
    runInInjectionContext(this.injector, () => {
      const serviceB = inject(ServiceB);
    });
  }
}

这样就 ok 了。

 

Singleton 单列模式

Injector.get 创建 value 后会把 value 缓存起来,用在 class 的话,这叫单列模式。

class ServiceA {}

const injector = Injector.create({
  providers: [ServiceA],
});
const serviceA1 = injector.get(ServiceA);
const serviceA2 = injector.get(ServiceA);
console.log(serviceA1 === serviceA2); // true

下面 useFactory 方法只会被调用一次

const injector = Injector.create({
  providers: [{ provide: ServiceA, useFactory: () => new ServiceA() }],
});
const serviceA1 = injector.get(ServiceA);
const serviceA1 = injector.get(ServiceA);

这个是 Angular DI 的设计,如果我们不想要单列,希望每一次都跑 factory 方法....不太容易,下一 part 会教。

 

Hierarchical Injector

Angular 的 Injector 有 Prototype 的概念(parent child 原型链)

class ParentService {}
class ChildService {}

const parentInjector = Injector.create({
  name: 'Parent Injector',
  providers: [ParentService],
});

const childInjector = Injector.create({
  name: 'Child Injector',
  parent: parentInjector, // 连接上 parent
  providers: [ChildService],
});

在 Injecter.create 时声明它的 parent 就可以把 injector 串联起来了。

继承的作用自然是原型链查找咯。

const parentService = childInjector.get(ParentService);

通过 childInjector 也可以获取到 ParentService。这就是它的基本玩法。

当 Hierarchical 遇上 Override Provider 和单列模式

上面有提过 Injector 两个特性

  1. Injector 只会实例化一次 class。后续的注入会返回同一个实例。(所谓的单列模式)

  2. 当出现 same Provider 时,后一个会覆盖掉前一个 (除非声明 multi: true)

而这两个特性遇上继承时会有一些些变化,看例子:

each Injector have own scope

class ParentService {}

const parentInjector = Injector.create({
  name: 'Parent Injector',
  providers: [ParentService],
});

const childInjector = Injector.create({
  name: 'Child Injector',
  parent: parentInjector,
  providers: [ParentService], // override ParentService
});

const parentService1 = parentInjector.get(ParentService);
const parentService2 = childInjector.get(ParentService);
console.log(parentService1 === parentService2); // false

每一个 Injector 都有自己的 scope。它和原型链的查找非常相似。当 childInjector 拥有属于自己的 Provider 时。它就不会去查找 ParentInjector 了。 

childInjector.get(ParentService) 会产生一个新的 ParentService 实例。这也是我们避开 Singleton 的唯一方法哦。

此外, 当声明 multi: true,也不会出现 double service

class ParentService {}

const parentInjector = Injector.create({
  name: 'Root',
  providers: [{ provide: ParentService, multi: true }],
});

const childInjector = Injector.create({
  name: 'Child',
  parent: parentInjector,
  providers: [{ provide: ParentService, multi: true }],
});

const parentService2 = childInjector.get(ParentService);
console.log(parentService2); // [ParentService]

同样是因为当 childInjector 有属于自己的 Provider 时,它就完全不理会 ParentInjector 了。

self, skipSelf 限制查找

class ParentService {}
class ChildService {}

const parentInjector = Injector.create({
  name: 'Parent Injector',
  providers: [ParentService],
});

const childInjector = Injector.create({
  name: 'Child Injector',
  parent: parentInjector,
  providers: [ChildService],
});

const parentService1 = childInjector.get(ParentService); // ok
const parentService2 = childInjector.get(ParentService, undefined, {
  self: true,
}); // 报错

由于声明了 self: true,childInjector 只能在自己的作用域查找 Provider,所以无法获取到 ParentService 也就报错了。

同理 skipSelf 就是不找自己的作用域,只找祖先的 Injector。

const childService = childInjector.get(ChildService, undefined, {
  skipSelf: true,
}); // 报错

 

@Injectable() and InjectionToken as Provider

特别声明:

下面例子中会用到 providedIn: 'any',这个东西已经在 Angular v15 废弃了,不过目前 v17 依然能用。

相关资讯: Docs – Deprecated APIs and features

本篇为了不涉及其它概念又想体现 "@Injectable() and InjectionToken as Provider“,所以才用了这个 providedIn: 'any'。

虽然 providedIn: 'any' 是废弃了,但是 providedIn 没有废弃,比如 providedIn: 'root' | 'platform' 都还在。

所以你依然可以照着它的概念去理解。

上面所有例子中,我们提供 providers 给 Injector 的方式,对代码管理和 tree shaking 是扣分的。

// service-a.ts
class ServiceA {}

// service-b.ts
class ServiceB {}

// injector.ts
import { ServiceA } from './service-a.ts';
import { ServiceB } from './service-b.ts';

export const injector = Injector.create({
  providers: [ServiceA, ServiceB],
});

// app.ts
import { injector } from './injector.ts';
import { ServiceB } from './service-b.ts';

injector.get(ServiceB);

第一,我们必须把所有可能会用到的 Provider 通通 pass 给 Injector,这个很麻烦丫,不小心漏掉一个怎么办,代码管理扣分。

第二,我在 app.ts 只用到了 ServiceB,而且 ServiceB 本身并不依赖 ServiceA,所以整个项目 ServiceA 是应该被 shaking 掉了,

但由于 injector.ts 需要 import 所有可能被用到的 Provider,导致无论如何 ServiceA 都不会被 shaking 掉。tree shaking 扣分。

为了解决上述的问题,Angular 搞了另一种提供 Provider 给 Injector 的方式。

@Injectable() as Provider

下面这样会报错

class ServiceA {}

const injector = Injector.create({
  providers: [],
});

console.log(injector.get(ServiceA)); // Error: No provider for ServiceA!

因为 providers 是空的。

我们加上 @Injectable

@Injectable({
  providedIn: 'any',
})
class ServiceA {}

const injector = Injector.create({
  providers: [],
});

console.log(injector.get(ServiceA)); // OK

这样就不会报错了。

上面我们有提到过,@Injectable 除了可以配合 Angular Compilation 搞黑魔法,它的另一个主要作用便是 as Provider。

为什么 providedIn: 'any' 后,injector 不需要 providers 也可以 get 到 ServiceA 呢?

特别声明:

providedIn: 'any' 已经在 Angular v15 废弃了,不过目前 v17 依然能用。

相关资讯: Docs – Deprecated APIs and features

本篇为了不涉及其它概念又想体现 "@Injectable() and InjectionToken as Provider“,所以才用了这个 providedIn: 'any' 作为例子。

虽然 providedIn: 'any' 是废弃了,但是 providedIn 没有废弃,它还可以 providedIn 其它的,比如 providedIn: 'root' 或者 providedIn: 'platform'。

所以你依然可以照着它的概念去理解。

Injector 源码逛一逛

Injector.create 方法的源码在 injector.ts。

Angular 17+ 高级教程 – Dependency Injection 依赖注入

看注释理解哦。

createInjector 函数源码在 create_injector.ts

Angular 17+ 高级教程 – Dependency Injection 依赖注入

createInjectorWithoutInjectorInstances 函数

Angular 17+ 高级教程 – Dependency Injection 依赖注入

关键就是创建除了 R3Injector 对象。

class R3Injector 的源码在 r3_injector.ts

R3Injector  继承自 EnvironmentInjector

Angular 17+ 高级教程 – Dependency Injection 依赖注入

EnvironmentInjector 又实现了抽象的 Injector 接口,我们平时用的就是 Injector 接口。

Angular 17+ 高级教程 – Dependency Injection 依赖注入

R3Injector 内有 2 个很重要的属性

Angular 17+ 高级教程 – Dependency Injection 依赖注入

一个是 records,它是一个 Map 用来装所有的 providers。

另一个是 parent injector。

有了这 2 个属性,Injector.get 就可以查找 providers 和祖先 providers 了。

那如果 records 里找不到 Provider 呢?

Angular 17+ 高级教程 – Dependency Injection 依赖注入

这个 injectable definition 指的是,经过 compile + @Injectable() 后 ServiceA 的 ɵprov static property。

Angular 17+ 高级教程 – Dependency Injection 依赖注入

当 records 找不到,Injector 去拿 ServiceA.ɵprov injectable definition。

然后检查它的 scope

Angular 17+ 高级教程 – Dependency Injection 依赖注入

providedIn: 'any' 表示 in scope(注: 除了 any 其实还可以放其它值,比如 'root' 和 'platform',这个以后章节会教)

接着调 factory 出来执行就拿到 provider value 了。

Angular 17+ 高级教程 – Dependency Injection 依赖注入

Angular 17+ 高级教程 – Dependency Injection 依赖注入

Angular 17+ 高级教程 – Dependency Injection 依赖注入

总结,injector 有 2 种方式可以找到 provider,第一个是去 records 找 provider,另一个是去 definition 找 provider。

InjectionToken as Provider

@Injectable({
  providedIn: 'any',
})
class ServiceA {
  value = 'Hello World';
}

const VALUE_TOKEN = new InjectionToken('Value', {
  providedIn: 'any',
  factory() {
    const serviceA = inject(ServiceA); // 注入其它依赖
    return serviceA.value;
  },
});

const injector = Injector.create({
  providers: [],
});

console.log(injector.get(VALUE_TOKEN)); // 'Hello World'

和 @Injectable 同样原理,只是 @Injectable 用于 class,InjectionToken 用于其它类型。

 

真实项目中 DI 的使用方式 

上面例子中,我们都是自己创建 Injector,但其实,在真实项目中,Angular 会替我们创建 Injector,我们不会用到 Injector.create 这个方法。

首先创建一个项目

ng new di --routing=false --style=scss --skip-tests --ssr=false

做一个 service-a.ts

export class ServiceA {}

在 Angular 项目中,我们的代码入口是组件,而组件本身又是 class,所以它天生就可以注入。

import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ServiceA } from '../service-a';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {
  constructor() {
    const serviceA = inject(ServiceA); // No provder for ServiceA
    console.log(serviceA);
  }
}

我们直接在 AppComponent constructor 里通过 inject 函数注入 ServiceA。

结果报错了。因为呢,虽然 Angular 替我们创建了 Injector,但是 DI 还需要 Provider,我们还没有把 ServiceA 提供给 Injector,所以它找不到。

到 app.config.ts

import { ApplicationConfig } from '@angular/core';
import { ServiceA } from '../service-a';

export const appConfig: ApplicationConfig = {
  providers: [ServiceA],
};

appConfig.providers 就是提供给 Injector 的 providers。

main.ts

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig).catch((err) =>
  console.error(err)
);

Angular 在 bootstrap application 的时候会把 AppComponent 和 appConfig 串联上。

于是 AppComponent 就可以 inject 到 appConfig 的 providers 了。

当然我们也可以用 @Injectable providedIn 的方式提供 Provider 给 Injector。

@Injectable({
  providedIn: 'any',
})
export class ServiceA {}

 

总结

Angular 的 DI 和其它后端 (e.g. ASP.NET Core) 的 DI 大同小异,几个小小区别:

  1. not only class,任何类型都可以使用 DI。

  2. 单列模式

  3. 原型链

目前为止,我们大概学习了 50% 关于 Angular DI 的知识,另外 50% 会在后面的章节教。

为什么不一起教?因为太复杂。当 DI 配上组件会有另一番天地,我们必须先对组件有基础的了解才能把 DI 加进去。

 

目录

上一篇 Angular 17+ 高级教程 – Angular Compiler (AKA ngc) Quick View

下一篇 Angular 17+ 高级教程 – Component 组件 の Angular Component vs Web Component

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

 

 

 

当 Injection Context Injector == null 遇上 provided: 'root' 尽然可以跑?!

逛源码无意间发现的奇葩情况。

injector_compatibility.ts

Angular 17+ 高级教程 – Dependency Injection 依赖注入

当我们调用 inject 时,它会调用 internal ɵɵinject 函数。

Angular 17+ 高级教程 – Dependency Injection 依赖注入

然后调用 injectInjectorOnly 函数

Angular 17+ 高级教程 – Dependency Injection 依赖注入

有 2 种情况

1. 当 _currentInjector(也就是 injection context 的 Injector) 非 null 时,那就调用 Injector.get 获取依赖值,这个是正常情况。

2. 当 _currentInjector 是 null 时,它会跑 injectRootLimpMode 函数。

inject_switch.ts

Angular 17+ 高级教程 – Dependency Injection 依赖注入

injectRootLimpMode 内会调用 @Injection() 或 InjectionToken 的 injectable definition 的 factory 去生成依赖值。

有趣的是,只有 providedIn === 'root' 才会,'any' 和 ‘platform’ 都不会哦。 

来一个 playgroud 体会一下呗

Angular 17+ 高级教程 – Dependency Injection 依赖注入

 把 providedIn 升级去 'any' 或 'platform'

Angular 17+ 高级教程 – Dependency Injection 依赖注入

结果报错了。虽然不懂为什么,但我这样操作显然是不合理的,怎么可以把 Injection Context Injector 设置成 null 呢。

所以我估计这个是 Angular 内部自己玩的东西,或许以后就会被修改了。

 

 

 

到了这里,关于Angular 17+ 高级教程 – Dependency Injection 依赖注入的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Angular(二) Understanding Dependency Injection for angular

    Angular https://angular.io/guide/dependency-injection Denpendency Injection,  or DI, is one of fundamental concepts for angular, DI is writed by angular framework and allows classes with  Angular decorators,  such as Components, directives, Piples and Injectables , to configure dependencies that they need.  Two main roles exists in DI system: dependency

    2024年02月09日
    浏览(39)
  • Understanding Dependency Injection for angular

    Angular https://angular.io/guide/dependency-injection Denpendency Injection,  or DI, is one of fundamental concepts for angular, DI is writed by angular framework and allows classes with  Angular decorators,  such as Components, directives, Piples and Injectables , to configure dependencies that they need.  Two main roles exists in DI system: dependency

    2024年02月10日
    浏览(34)
  • Angular系列教程之依赖注入详解

    Angular作为一款流行的前端框架,提供了许多优秀的功能和特性,其中之一就是依赖注入(Dependency Injection)。依赖注入是一种设计模式,它允许我们在代码中以一种可扩展和可测试的方式添加和配置依赖关系。在Angular中,依赖注入被广泛应用于组件、服务、指令等场景,本文

    2024年01月17日
    浏览(42)
  • Angular 17+ 高级教程 – HttpClient

    HttpClient 是 Angular 对 XMLHttpRequest 和 Fetch 的封装。 HttpClient 的 DX (Developer Experience) 比 XMLHttpRequest 和 Fetch 都好,只是学习成本比较高,因为它融入了 RxJS 概念。 要深入理解 HttpClient 最好先掌握 3 个基础技能: XMLHttpRequest -- 看这篇 Fetch -- 看这篇 RxJS -- 看这系列 (如果只是为了

    2024年03月16日
    浏览(53)
  • Angular 17+ 高级教程 – NgModule

    NgModule 在 Angular v14 以前是一门必修课。然而,自 Angular v14 推出 Standalone Component 以后,它的地位变得越来越边缘化了。 本教程从开篇到本篇,所有例子使用的都是 Standalone Component,一点 NgModule 的影子也没有😔。 但是!NgModule 还是有价值的,而且在越复杂的项目中你越可以

    2024年03月12日
    浏览(62)
  • Angular 17+ 高级教程 – Library

    当你需要管理超过一个项目时,你就需要知道怎么使用 Angular Library。 你可以把多个项目共享的组件放到这个 Library 了,就像 Angular Material 那样。   Sandro Roth – Building an Angular Library with multiple entry points (主要参考) Docs – Creating libraries Docs – Copy assets Docs – Embed assets in C

    2024年04月17日
    浏览(39)
  • Angular 17+ 高级教程 – Signals

    在上一篇 Change Detection 中, 我们有提到 MVVM 监听 ViewModel 变化的难题. 当年 AngularJS 和 Knockout.js (下面简称 KO) 各自选了不同的道路. 但如今, 事过境迁, Angular 最终也走向了 KO 的道路. 这就是这篇的主角 Signal。   在 JavaScript, 值类型 variable 无法被监听, Signal 的做法是把它们都变

    2024年03月09日
    浏览(67)
  • Angular 17+ 高级教程 – 盘点 Angular v14 到 v17 的重大改变

    我在 初识 Angular 文章里有提到 Angular 目前的断层问题。 大部分的 Angular 用户都停留在 v9.0 版本。 v9.0 是一个里程碑版本,Angular 从 v4.0 稳定版推出后,好几年都没有什么动静,直到 v9.0 推出了 Ivy rendering engine。 本以为 v9.0 以后 Angular 会大爆发,结果迎来的是 Angular 团队搞内

    2024年04月22日
    浏览(36)
  • Angular 17+ 高级教程 – Change Detection

    虽然 Angular 正在把大部分 Change Detection 概念换成 Signal,但是最快也要 1 年后,所以还是有必要认真学习一下的。   MVVM 框架的开发方式是这样的: 写 HTML 写 ViewModel 在 HTML 里加入 binding syntax。 在 HTML 里加入 listening syntax,在事件发生时修改 ViewModel。 MVVM 的宗旨是 \\\"不要直接

    2024年03月09日
    浏览(42)
  • Angular 17+ 高级教程 – 学以致用

    读这么多原理,到底为了什么?真实项目中真的会用得到吗? 你正在疑惑 \\\"知识的力量\\\" 吗? 本篇会给一个非常非常好的案例,让你感悟 -- 知识如何用于实战。 记住,我的目的是让你感悟,而不是要你盲目相信知识。   下面是我在 2020-11-06 记入的一个问题。 一模一样的问题

    2024年04月22日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包