Angular学习笔记(三)

以下内容基于Angular 文档中文版的学习

目录

Observable和RxJS

  Observable概览

    定义观察者

    订阅

    创建可观察对象

    多播

  RxJS 库

    创建可观察对象的函数

    操作

    错误处理

    可观察对象的命名约定

  Angular 中的可观察对象

    HTTP

    Async 管道

    路由器 (router)

    响应式表单 (reactive forms)

  实际运用

    输入提示(type-ahead)建议

    指数化退避

NgModule

  模块元数据

  模块类别汇总

  提供依赖

    使用惰性加载模块限制提供者的作用域

    多级注入器和服务实例

    forRoot() 模式

  惰性加载特性模块

    使用CLI分布创建惰性加载模块

    预加载模块

    预加载组件数据

安全

  无害化处理

  避免直接使用 DOM API

  信任安全值

  内容安全政策

  强制执行可信类型

  使用 AOT 模板编译器

  服务器端 XSS 保护

  HTTP 级漏洞

代码风格

  单一职责

  命名

  应用程序结构与 NgModule

  组件

  指令

  服务

  数据服务

  生命周期钩子


Observable和RxJS

  Observable概览

    观察者(Observer)模式是一个软件设计模式,它有一个对象,称之为主体 Subject,负责维护一个依赖项(称之为观察者 Observer)的列表,并且在状态变化时自动通知它们。
    该模式和发布/订阅模式非常相似(但不完全一样)。

    可观察对象是声明式的 —— 也就是说,虽然你定义了一个用于发布值的函数,但是在有消费者订阅它之前,这个函数并不会实际执行。订阅之后,当这个函数执行完或取消订阅时,订阅者就会收到通知。

    可观察对象可以发送多个任意类型的值 —— 字面量、消息、事件。无论这些值是同步发送的还是异步发送的,接收这些值的 API 都是一样的。
    由于准备(setup)和清场(teardown)的逻辑都是由可观察对象自己处理的,因此你的应用代码只管订阅并消费这些值就可以了,做完之后,取消订阅。
    无论这个流是按键流、HTTP 响应流还是定时器,对这些值进行监听和停止监听的接口都是一样的。


    定义观察者

       用于接收可观察对象通知的处理器要实现 Observer 接口。这个对象定义了一些回调函数来处理可观察对象可能会发来的三种通知:
         next  必要。用来处理每个送达值。在开始执行后可能执行零次或多次。
         error 可选。用来处理错误通知。错误会中断这个可观察对象实例的执行过程。
         complete 可选。用来处理执行完毕(complete)通知。当执行完毕后,这些值就会继续传给下一个处理器。

	   const myObserver = {
         next: (x: number) => console.log('Observer got a next value: ' + x),
         error: (err: Error) => console.error('Observer got an error: ' + err),
         complete: () => console.log('Observer got a complete notification'),
       };

    订阅

       只有当有人订阅 Observable 的实例时,它才会开始发布值。订阅时要先调用该实例的 subscribe() 方法,并把一个观察者对象传给它,用来接收通知。

	   myObservable.subscribe(myObserver);
	   // 等同于
	   myObservable.subscribe(
         x => console.log('Observer got a next value: ' + x),
         err => console.error('Observer got an error: ' + err),
         () => console.log('Observer got a complete notification')
       );
	   // 无论哪种情况,next 处理器都是必要的,而 error 和 complete 处理器是可选的。

    创建可观察对象

      // This function runs when subscribe() is called
      function sequenceSubscriber(observer: Observer) {
        // synchronously deliver 1, 2, and 3, then complete
        observer.next(1);
        observer.next(2);
        observer.next(3);
        observer.complete();

        // unsubscribe function doesn't need to do anything in this
        // because values are delivered synchronously
        return {unsubscribe() {}};
      }
      // Create a new Observable that will deliver the above sequence
      const sequence = new Observable(sequenceSubscriber);
      // execute the Observable and print the result of each notification
      sequence.subscribe({
        next(num) { console.log(num); },
        complete() { console.log('Finished sequence'); }
      });

      创建一个用来发布事件的可观察对象。

      function fromEvent(target: HTMLElement, eventName: T) {
        return new Observable((observer) => {
          const handler = (e: HTMLElementEventMap[T]) => observer.next(e);

          // Add the event handler to the target
          target.addEventListener(eventName, handler);

          return () => {
            // Detach the event handler from the target
            target.removeEventListener(eventName, handler);
          };
        });
      }
      const ESC_CODE = 'Escape';
      const nameInput = document.getElementById('name') as HTMLInputElement;
      const subscription = fromEvent(nameInput, 'keydown').subscribe((e: KeyboardEvent) => {
        if (e.code === ESC_CODE) {
          nameInput.value = '';
        }
      });

    多播

      典型的可观察对象会为每一个观察者创建一次新的、独立的执行。
      当观察者进行订阅时,该可观察对象会连上一个事件处理器,并且向那个观察者发送一些值。
      当第二个观察者订阅时,这个可观察对象就会连上一个新的事件处理器,并独立执行一次,把这些值发送给第二个可观察对象。

      多播用来让可观察对象在一次执行中同时广播给多个订阅者。借助支持多播的可观察对象,你不必注册多个监听器,而是复用第一个(next)监听器,并且把值发送给各个订阅者。
      支持多播的可观察对象需要做更多的准备工作,但对某些应用来说,这非常有用。

      使用Subject实现多播1:转发多播

        const subject = new Subject();
        subject.subscribe((v) => console.log(`observerA: ${v}`));
        subject.subscribe((v) => console.log(`observerB: ${v}`));
        from(['7', '8', '9']).subscribe(subject);
        // Log
        // observerA: 7
        // observerB: 7
        // observerA: 8
        // observerB: 8
        // observerA: 9
        // observerB: 9

      使用Subject实现多播2:直接多播

        const subject = new Subject();
        subject.subscribe((v) => console.log(`observerA: ${v}`));
        subject.subscribe((v) => console.log(`observerB: ${v}`));
        subject.next('a');
        subject.next('b');
        subject.next('c');
        // Log
        // observerA: a
        // observerB: a
        // observerA: b
        // observerB: b
        // observerA: c
        // observerB: c

  RxJS 库

    响应式编程是一种面向数据流和变更传播的异步编程范式。
    RxJS(响应式扩展的 JavaScript 版)是一个使用可观察对象进行响应式编程的库,它让组合异步代码和基于回调的代码变得更简单。
    RxJS 提供了一种对 Observable 类型的实现,直到 Observable 成为了 JavaScript 语言的一部分并且浏览器支持它之前,它都是必要的。
    这个库还提供了一些工具函数,用于创建和使用可观察对象。这些工具函数可用于:
      把现有的异步代码转换成可观察对象
      迭代流中的各个值
      把这些值映射成其它类型
      对流进行过滤
      组合多个流

    创建可观察对象的函数

      RxJS 提供了一些用来创建可观察对象的函数。这些函数可以简化根据某些东西创建可观察对象的过程,比如事件、定时器、 Promise 等等。

      import { from, Observable, interval, fromEvent } from 'rxjs';
      import { ajax } from 'rxjs/ajax';

      // Create an Observable out of a promise
      const data = from(fetch('/api/endpoint'));
      // Subscribe to begin listening for async result
      data.subscribe({
        next(response) { console.log(response); },
        error(err) { console.error('Error: ' + err); },
        complete() { console.log('Completed'); }
      });

      // Create an Observable that will publish a value on an interval
      const secondsCounter = interval(1000);
      // Subscribe to begin publishing values
      const subscription = secondsCounter.subscribe(n =>
        console.log(`It's been ${n + 1} seconds since subscribing!`));

      const el = document.getElementById('my-element')!;
      // Create an Observable that will publish mouse movements
      const mouseMoves = fromEvent(el, 'mousemove');
      // Subscribe to start listening for mouse-move events
      const subscription = mouseMoves.subscribe(evt => {
        // Log coords of mouse movements
        console.log(`Coords: ${evt.clientX} X ${evt.clientY}`);
        // When the mouse is over the upper-left of the screen,
        // unsubscribe to stop listening for mouse movements
        if (evt.clientX < 40 && evt.clientY < 40) {
          subscription.unsubscribe();
        }
      });

      // Create an Observable that will create an AJAX request
      const apiData = ajax('/api/data');
      // Subscribe to create the request
      apiData.subscribe(res => console.log(res.status, res.response));

    操作

      操作符是基于可观察对象构建的一些对集合进行复杂操作的函数。RxJS 定义了一些操作符,比如 map()、filter()、concat() 和 flatMap()。
      操作符接受一些配置项,然后返回一个以来源可观察对象为参数的函数。当执行这个返回的函数时,这个操作符会观察来源可观察对象中发出的值,转换它们,并返回由转换后的值组成的新的可观察对象。

      import { of } from 'rxjs';
      import { map } from 'rxjs/operators';
      const nums = of(1, 2, 3);
      const squareValues = map((val: number) => val * val);
      const squaredNums = squareValues(nums);
      squaredNums.subscribe(x => console.log(x));

      你可以使用管道来把这些操作符链接起来。管道让你可以把多个由操作符返回的函数组合成一个。
      pipe() 函数以你要组合的这些函数作为参数,并且返回一个新的函数,当执行这个新函数时,就会顺序执行那些被组合进去的函数。

      // Create a function that accepts an Observable.
      const squareOddVals = pipe(
        filter((n: number) => n % 2 !== 0),
        map(n => n * n)
      );
      // Create an Observable that will run the filter and map functions
      const squareOdd = squareOddVals(nums);
      // Subscribe to run the combined functions
      squareOdd.subscribe(x => console.log(x));

      pipe() 函数也同时是 RxJS 的 Observable 上的一个方法,所以你可以用下列简写形式来达到同样的效果:

      const squareOdd = of(1, 2, 3, 4, 5)
        .pipe(
          filter(n => n % 2 !== 0),
          map(n => n * n)
        );
      // Subscribe to get values
      squareOdd.subscribe(x => console.log(x));

      常用操作符

        创建 from, fromEvent, of
        组合 combineLatest, concat, merge, startWith , withLatestFrom, zip
        过滤 debounceTime, distinctUntilChanged, filter, take, takeUntil
        转换 bufferTime, concatMap, map, mergeMap, scan, switchMap
        工具 tap
        多播 share

    错误处理

      除了可以在订阅时提供 error() 处理器外,RxJS 还提供了 catchError 操作符,它允许你在管道中处理已知错误。
      假设你有一个可观察对象,它发起 API 请求,然后对服务器返回的响应进行映射。如果服务器返回了错误或值不存在,就会生成一个错误。如果你捕获这个错误并提供了一个默认值,流就会继续处理这些值,而不会报错。
      catchError 提供了一种简单的方式进行恢复,而 retry 操作符让你可以尝试失败的请求。

      import { Observable, of } from 'rxjs';
      import { ajax } from 'rxjs/ajax';
      import { map, catchError } from 'rxjs/operators';

      // Return "response" from the API. If an error happens,
      // return an empty array.
      const apiData = ajax('/api/data').pipe(
        map((res: any) => {
          if (!res.response) {
            throw new Error('Value expected!');
          }
          return res.response;
        }),
		retry(3), // Retry up to 3 times before failing
        catchError(() => of([]))
      );

      apiData.subscribe({
        next(x: T) { console.log('data: ', x); },
        error() { console.log('errors already caught... will not run'); }
      });

    可观察对象的命名约定

      推荐可观察对象的名字以$符号结尾

  Angular 中的可观察对象

    Angular 使用可观察对象作为处理各种常用异步操作的接口。比如:
      HTTP 模块使用可观察对象来处理 AJAX 请求和响应。
      路由器和表单模块使用可观察对象来监听对用户输入事件的响应。

    HTTP

      Angular 的 HttpClient 从 HTTP 方法调用中返回了可观察对象。
      相对于基于承诺(Promise)的 HTTP API,它有一系列优点:
        可观察对象不会修改服务器的响应(和在 Promise 上串联起来的 .then() 调用一样)。反之,你可以使用一系列操作符来按需转换这些值。
        HTTP 请求是可以通过 unsubscribe() 方法来取消的
        请求可以进行配置,以获取进度事件的变化
        失败的请求很容易重试

    Async 管道

      AsyncPipe 会订阅一个可观察对象或 Promise ,并返回其发出的最后一个值。当发出新值时,该管道就会把这个组件标记为需要进行变更检查的(译注:因此可能导致刷新界面)

    路由器 (router)

      Router.events 以可观察对象的形式提供了其事件。你可以使用 RxJS 中的 filter() 操作符来找到感兴趣的事件,并且订阅它们,以便根据浏览过程中产生的事件序列作出决定。

        constructor(router: Router) {
          // Create a new Observable that publishes only the NavigationStart event
          this.navStart = router.events.pipe(
            filter(evt => evt instanceof NavigationStart)
          ) as Observable;
        }
        ngOnInit() {
          this.navStart.subscribe(() => console.log('Navigation Started!'));
        }

      ActivatedRoute 是一个可注入的路由器服务,它使用可观察对象来获取关于路由路径和路由参数的信息。比如,ActivatedRoute.url 包含一个用于汇报路由路径的可观察对象。

        this.activatedRoute.url.subscribe(url => console.log('The URL changed to: ' + url));

    响应式表单 (reactive forms)

     响应式表单具有一些属性,它们使用可观察对象来监听表单控件的值。
      FormControl 的 valueChanges 属性和 statusChanges 属性包含了会发出变更事件的可观察对象。
      订阅可观察的表单控件属性是在组件类中触发应用逻辑的途径之一。

        nameChangeLog: string[] = [];
        heroForm!: FormGroup;

        ngOnInit() {
          this.logNameChange();
        }
        logNameChange() {
          const nameControl = this.heroForm.get('name');
          nameControl?.valueChanges.forEach(
            (value: string) => this.nameChangeLog.push(value)
          );
        }

  实际运用

    输入提示(type-ahead)建议

      import { fromEvent, Observable } from 'rxjs';
      import { ajax } from 'rxjs/ajax';
      import { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';

      const searchBox = document.getElementById('search-box') as HTMLInputElement;

      const typeahead = fromEvent(searchBox, 'input').pipe(
        map(e => (e.target as HTMLInputElement).value),
        // 确认它达到了最小长度
        filter(text => text.length > 2),
        // 控制时间间隔,防抖(这样才能防止连续按键时每次按键都发起 API 请求,而应该等到按键出现停顿时才发起)
        debounceTime(10),
        // 如果输入值没有变化,则不要发起请求(比如按某个字符,然后快速按退格)
        distinctUntilChanged(),
        // 如果已发出的 AJAX 请求的结果会因为后续的修改而变得无效,那就取消它
        switchMap(searchTerm => ajax(`/api/endpoint?search=${searchTerm}`))
      );

      typeahead.subscribe(data => {
        // Handle the data from the API
      });

    指数化退避

      指数化退避是一种失败后重试 API 的技巧,它会在每次连续的失败之后让重试时间逐渐变长,超过最大重试次数之后就会彻底放弃。
      如果使用 Promise 对象和其它跟踪 AJAX 调用的方法会非常复杂,而使用可观察对象,这非常简单:

      import { of, pipe, range, throwError, timer, zip } from 'rxjs';
      import { ajax } from 'rxjs/ajax';
      import { map, mergeMap, retryWhen } from 'rxjs/operators';

      export function backoff(maxTries: number, delay: number) {
        return pipe(
          retryWhen(attempts =>
            zip(range(1, maxTries + 1), attempts).pipe(
              mergeMap(([i, err]) => (i > maxTries) ? throwError(err) : of(i)),
              map(i => i * i),
              mergeMap(v => timer(v * delay)),
            ),
          ),
        );
      }

      ajax('/api/endpoint')
        .pipe(backoff(3, 250))
        .subscribe(function handleData(data) { /* ... */ });

NgModule

  模块元数据

    declarations
      属于此模块的可声明类(组件、指令和管道)的列表。
        编译模板时,你需要确定一组选择器,用于触发其对应的指令。
        模板是在 NgModule(声明模板组件的 NgModule)的上下文中编译的,它使用以下规则确定选择器集:
          declarations 中列出的指令的所有选择器。
          从导入的 NgModules 导出的指令的所有选择器。
      每个组件都应该(且只能)声明(declare)在一个 NgModule 类中。
      这些可声明的类在当前模块中是可见的,但是对其它模块中的组件是不可见的 —— 除非把它们从当前模块导出,并让对方模块导入本模块。
    imports
      你要用的其他 NgModule,这样你才可以使用它们的可声明对象。
    exports
      导入模块可以使用的声明列表(组件、指令和管道类)。
      导出的声明是模块的公共 API。如果另一个模块中的组件导入此模块并且此模块导出 UserComponent,则另一个模块中的组件可以用此模块的 UserComponent。
    providers
      列出了该应用所需的服务。当直接把服务列在这里时,它们是全应用范围的。当你使用特性模块和惰性加载时,它们是范围化的。
      依赖注入提供者的列表。
      Angular 会使用 NgModule 的注入器注册这些提供者。如果是用于引导的 NgModule,则它是根注入器。
      这些服务可用于注入到作为此注入器子项的任何组件、指令、管道或服务中。
      惰性加载的模块有自己的注入器,它通常是应用程序根注入器的子。
      惰性加载的服务的范围为延迟模块的注入器。如果惰性加载的模块还提供了 UserService,则在该模块的上下文中创建的任何组件(例如通过路由器导航)都会获取服务的本地实例,而不是根应用程序注入器中的实例。
      外部模块中的组件会继续接收其注入器提供的实例。
    bootstrap
      自动引导的组件列表。
      通常此列表中只有一个组件,即应用程序的根组件。
      Angular 可以用多个引导组件启动,每个组件在宿主网页中都有自己的位置。

  模块类别汇总

    Domain
      围绕特性、业务领域或用户体验进行组织。
      领域模块用来组织与特定功能有关的代码,里面包含构成此功能的所有组件、路由和模板。
      领域模块中的顶级组件是该特性或领域的根,是你要导出的唯一组件。各种私有的支撑子组件都是它的后代。
      领域模块主要由可声明对象组成,很少会在此提供服务。如果一定要提供,那么这些服务的生命周期应和该模块的生命周期一致。
    Routed
      所有惰性加载模块都要用带路由的模块。
      使用该模块的顶级组件作为路由器导航路由的目标。
      带路由的模块不会导出任何内容,因为它们的组件永远不会出现在外部组件的模板中。
      不要把惰性加载的带路由的模块导入到另一个模块中,因为这会触发一个急性加载,从而破坏了惰性加载它的目的。
      不要在带路由的模块及其导入的相关模块中提供全应用范围内的单例服务。
    Routing
      使用路由定义模块来为领域模块提供路由配置,从而将路由相关的关注点从其伴生领域模块中分离出来。
        定义路由
        把路由器配置文件添加到模块的导入表中
        往模块的提供者列表中添加路由守卫和解析器(resolver)提供者
      路由定义模块的名字应该和其伴生模块的名字平行,但使用-routing后缀。
      如果伴生模块是根模块 AppModule,那么 AppRoutingModule 就会通过其导入表中的 RouterModule.forRoot(routes) 来添加路由器配置。
      所有其他的子路由定义模块都会导入 RouterModule.forChild(routes)。
      在路由定义模块中,要重新导出 RouterModule,以便其伴生模块中的组件可以访问路由器指令,比如 RouterLink 和 RouterOutlet
    Service
      服务模块提供实用服务,比如数据访问和消息传递。
      理想的服务模块完全由提供者组成,没有可声明对象。
      只能使用根模块 AppModule 来导入各种服务模块。
    Widget
      小部件模块可以为其它模块提供某些组件、指令或管道。
      小部件模块应该完全由可声明对象组成,其中大部分都是导出的。
    Shared
      共享模块可以为其它的模块提供组件,指令和管道的集合。
      共享模块不应该包含服务提供者,它所导入或重新导出的任何模块也都不应该包含提供者。

模块 可声明对象 提供者 导出 被谁导入
领域模块 罕见 顶级组件 其它领域 AppModule
带路由的模块 罕见
路由 是(路由守卫) RouterModule 其它领域模块(为获取路由定义)
服务模块 AppModule
小部件模块 罕见 其它领域模块
共享模块 其它领域模块

  提供依赖

    使用服务类的@Injectable()装饰器providedIn属性的值,模块中提供服务的首选方式。
    当没有人注入它时,该服务就可以被摇树优化掉。
      providedIn: null       可注入物不会在任何范围内自动提供,必须添加到@NgModule 、 @Component或@Directive的 providers 数组中
      providedIn: 'root'     指定 Angular 应该在根注入器中提供该服务,应用程序级单例服务
      providedIn: 'platform' 由页面上所有应用程序共享的特殊单例平台注入器
      providedIn: 'any'      在每个惰性加载的模块中提供一个唯一实例,而所有急性加载的模块共享一个实例
      providedIn: Type  将可注入物与 @NgModule 或其他 InjectorType 相关联。此选项已弃用。


    也可以在模块/组件/指令/管道中使用providers属性声明一个提供者
      providers: [UserService] 在模块/组件/指令/管道中共享一个实例,生命周期与模块/组件/指令/管道一致
      在这种情况下,Angular 将 providers 值展开为完整的提供者对象,如下所示:

        providers: [{ provide: UserService, useClass: UserService }]

      useClass
        这个提供者键名能让你创建并返回指定类的新实例。
        你可以用这种类型的提供者来作为通用类或默认类的替代实现。
        [{ provide: Logger, useClass: BetterLogger }]
        
      useExisting
        允许你将一个令牌映射到另一个。
        实际上,第一个令牌是与第二个令牌关联的服务的别名,创建了两种访问同一个服务对象的方式。
        [ NewLogger,{ provide: OldLogger, useExisting: NewLogger}]
          当组件请求新的或旧的记录器时,注入器都会注入一个 NewLogger 的实例。通过这种方式,OldLogger 就成了 NewLogger 的别名。
          确保你没有使用 OldLogger 将 NewLogger 别名为 useClass ,因为这会创建两个不同 NewLogger 实例。
        自定义验证器指令时,需要使用useExisting

      useFactory
        允许你通过调用工厂函数来创建依赖对象。
        使用这种方法,你可以根据 DI 和应用程序中其他地方的可用信息创建动态值。

        const heroServiceFactory = (logger: Logger, userService: UserService) =>
          new HeroService(logger, userService.user.isAuthorized);
        export const heroServiceProvider =
          { provide: HeroService,
            useFactory: heroServiceFactory,
            deps: [Logger, UserService]
          };

       useValue
         允许你将固定值与某个 DI 令牌相关联。
         可以用此技术提供运行时配置常量,例如网站基址和特性标志。
         你还可以在单元测试中使用值提供者来提供模拟数据以代替生产级数据服务。
         
         可以定义和使用一个 InjectionToken 对象来为非类的依赖选择一个提供者令牌。

           export const APP_CONFIG = new InjectionToken('app.config');
           providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]
           constructor(@Inject(APP_CONFIG) config: AppConfig) {
             this.title = config.title;
           }

           AppConfig类型为接口,不能直接注入,需要使用useValue的方式注入实际值。

    使用惰性加载模块限制提供者的作用域

      当 Angular 的路由器惰性加载一个模块时,它会创建一个新的注入器。这个注入器是应用的根注入器的一个子注入器。
      这个子注入器会操纵所有特定于此模块的提供者,如果有的话。
      任何在惰性加载模块的上下文中创建的组件(比如路由导航),都会获取由子注入器提供的服务的局部实例,而不是应用的根注入器中的实例。
      而外部模块中的组件,仍然会收到来自于应用的根注入器创建的实例。

    多级注入器和服务实例

      服务都是某个注入器范围内的单例,这意味着在给定的注入器中最多有一个服务实例。
      Angular DI 具有多级注入体系,这意味着嵌套的注入器可以创建自己的服务实例。
      子模块注入器和组件注入器彼此独立,并为已提供的服务创建它们自己的单独实例。当 Angular 销毁 NgModule 或组件实例时,它也会销毁该注入器和该注入器的服务实例。

      ModuleInjector
        使用 @NgModule() 或 @Injectable() 注解在此层次结构中配置 ModuleInjector
        ModuleInjector 由 @NgModule.providers 和 NgModule.imports 属性配置。ModuleInjector 是可以通过 NgModule.imports 递归找到的所有 providers 数组的扁平化。
        子 ModuleInjector 是在惰性加载其它 @NgModules 时创建的。
        
        在 root 之上还有两个注入器,一个是额外的 ModuleInjector,一个是 NullInjector()
        root ModuleInjector -> 平台 ModuleInjector -> NullInjector()

      ElementInjector
        Angular 会为每个 DOM 元素隐式创建 ElementInjector。
        可以用 @Component() 装饰器中的 providers 或 viewProviders 属性来配置 ElementInjector 以提供服务
          providers的范围大于viewProviders,viewProviders中声明的服务在内容投影里不可见
        可以用 @Directive() 装饰器中的 providers属性来配置 ElementInjector 以提供服务。

      解析规则
        当组件声明依赖项时,Angular 会尝试使用它自己的 ElementInjector 来满足该依赖。 如果组件的注入器缺少提供者,它将把请求传给其父组件的 ElementInjector。
        这些请求将继续转发,直到 Angular 找到可以处理该请求的注入器或用完祖先 ElementInjector。
        如果 Angular 在任何 ElementInjector 中都找不到提供者,它将返回到发起请求的元素,并在 ModuleInjector 层次结构中进行查找。如果 Angular 仍然找不到提供者,它将引发错误。
        如果你已在不同级别注册了相同 DI 令牌的提供者,则 Angular 会用遇到的第一个来解析该依赖。

        可以使用 @Optional(),@Self(),@SkipSelf() 和 @Host() 来修饰 Angular 的解析行为。
        @Optional()
          允许 Angular 将你注入的服务视为可选服务。这样,如果无法在运行时解析它,Angular 只会将服务解析为 null,而不会抛出错误。
        @Self()
          使用 @Self() 让 Angular 仅查看当前组件或指令的 ElementInjector。
        @SkipSelf()
          Angular 在父 ElementInjector 中而不是当前 ElementInjector 中开始搜索服务。
        合用 @SkipSelf() 和 @Optional()
          如果值为 null 请同时使用 @SkipSelf() 和 @Optional() 来防止错误。

          class Person {
            constructor(@Optional() @SkipSelf() parent?: Person) {}
          }

        @Host()
          禁止在宿主组件以上的搜索。即使树的更上级有一个服务实例,Angular 也不会继续寻找。
          宿主组件通常就是请求该依赖的那个组件。不过,当该组件投影进某个父组件时,那个父组件就会变成宿主。
 

      forwardRef
        forwardRef() 函数建立一个间接地引用,Angular 可以随后解析,用来解决循环引用和引用自身的问题。
          AlexComponent中引用自身

		  providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

    forRoot() 模式

      通常,你只需要用 providedIn 提供服务,用 forRoot()/forChild() 提供路由即可。
      
      如果模块同时定义了 providers(服务)和 declarations(组件、指令、管道),那么,当你同时在多个特性模块中加载此模块时,这些服务就会被注册在多个地方。
      这会导致出现多个服务实例,并且该服务的行为不再像单例一样。
      有多种方式来防止这种现象:
        用 providedIn 语法代替在模块中注册服务的方式
        把你的服务分离到它们自己的模块中
        在模块中分别定义 forRoot() 和 forChild() 方法

      使用 forRoot() 来把提供者从该模块中分离出去,这样你就能在根模块中导入该模块时带上 providers,并且在子模块中导入它时不带 providers。
        在该模块中创建一个静态方法 forRoot()。
        把这些提供者放进 forRoot() 方法中。

        static forRoot(config: UserServiceConfig): ModuleWithProviders {
          return {
            ngModule: GreetingModule,
            providers: [
              {provide: UserServiceConfig, useValue: config }
            ]
          };
        }

      forRoot() 和 Router
        RouterModule 中提供了 Router 服务,同时还有一些路由指令,比如 RouterOutlet 和 routerLink 等。
        应用的根模块导入了 RouterModule,以便应用中有一个 Router 服务,并且让应用的根组件可以访问各个路由器指令。
        任何一个特性模块也必须导入 RouterModule,这样它们的组件模板中才能使用这些路由器指令。
        通过使用 forRoot() 方法,应用的根模块中会导入 RouterModule.forRoot(...),从而获得一个 Router 实例,而所有的特性模块要导入 RouterModule.forChild(...),它就不会实例化另外的 Router。

      防止重复导入 GreetingModule

        constructor(@Optional() @SkipSelf() parentModule?: GreetingModule) {
          if (parentModule) {
            throw new Error(
              'GreetingModule is already loaded. Import it in the AppModule only');
          }
        }

  惰性加载特性模块

    主模块的imports中不引入指定模块
    主路由的路由表中使用loadChildren引入模块

      const routes: Routes = [
        {
          path: 'items',
          loadChildren: () => import('./items/items.module').then(m => m.ItemsModule)
        }
      ];
	  RouterModule.forRoot(routes)

    模块中的路由表指定相对路径

      const routes: Routes = [
        {
          path: '',
          component: CustomersComponent
        },
        {
          path: 'create',
          component: CustomerCreateComponent
        }
      ];
	  RouterModule.forChild(routes)

    使用CLI分布创建惰性加载模块

      ng new customer-app --routing
         --routing 标识生成了一个名叫 app-routing.module.ts 的文件.它是你建立惰性加载的特性模块时所必须的。
      ng generate module customers --route customers --module app.module
         --route 指定加载 customers 特性模块的路径也是 customers
         --module 将声明的路由 customers 添加到指定的模块中声明的 routes 数组中

    预加载模块

      预加载模块通过在后台加载部分应用来改善用户体验。这样一来,用户在激活路由时就无需等待下载这些元素。
      
      PreloadAllModules预加载所有惰性模块
        PreloadAllModules 策略不会加载被 canLoad 守卫所保护的特性区。

        import { PreloadAllModules } from '@angular/router';
        RouterModule.forRoot(
          appRoutes,
          {
            preloadingStrategy: PreloadAllModules
          }
        )

      使用自定义预加载策略

        自定义策略

		  @Injectable({ providedIn: 'root', })
          export class SelectivePreloadingStrategyService implements PreloadingStrategy {
            preloadedModules: string[] = [];
          
            preload(route: Route, load: () => Observable): Observable {
              if (route.data?.['preload'] && route.path != null) {
                // add the route path to the preloaded module array
                this.preloadedModules.push(route.path);
          
                // log the route path to the console
                console.log('Preloaded: ' + route.path);
          
                return load();
              } else {
                return of(null);
              }
            }
          }

        设置路由表和策略

		  const appRoutes: Routes = [
	        { path: 'crisis-center',
              loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule),
              data: { preload: true }
            },
		  ...]
	      RouterModule.forRoot(appRoutes, {preloadingStrategy: SelectivePreloadingStrategyService})

    预加载组件数据

      要预加载组件数据,可以用 resolver 守卫。解析器通过阻止页面加载来改进用户体验,直到显示页面时的全部必要数据都可用。
      在新创建的服务中,实现由 @angular/router 包提供的 Resolve 接口:

        import { Resolve } from '@angular/router';
        /* An interface that represents your data model */
        export interface Crisis {
          id: number;
          name: string;
        }
        export class CrisisDetailResolverService implements Resolve {
          resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
            // observable 必须完成,否则导航不会继续
          }
        }

      把这个解析器导入此模块的路由模块

        {
          path: '/your-path',
          component: YourComponent,
          resolve: {
            crisis: CrisisDetailResolverService
          }
        }

      使用注入进来的 ActivatedRoute 类实例来访问与指定路由关联的 data 值

        import { ActivatedRoute } from '@angular/router';
        @Component({ … })
        class YourComponent {
          constructor(private route: ActivatedRoute) {}
          ngOnInit() {
            this.route.data
              .subscribe(data => {
                const crisis: Crisis = data.crisis;
                // …
              });
          }
        }

安全

  无害化处理

    HTML        值需要被解释为 HTML 时使用,比如当绑定到 innerHTML 时。
    样式          值需要作为 CSS 绑定到 style 属性时使用。
    URL          值需要被用作 URL 属性时使用,比如
    资源 URL  值需要作为代码进行加载并执行,比如 Syntax'

{{htmlSnippet}}

插值的内容总会被编码 - 其中的 HTML 不会被解释

innerHTML的HTML内容会被正常解释,Angular 认为这些值是不安全的,并自动进行无害化处理。它会移除 script 元素,但保留安全的内容

  避免直接使用 DOM API

    除非你强制使用可信类型(Trusted Types),否则浏览器内置的 DOM API 不会自动保护你免受安全漏洞的侵害。
    比如 document、通过 ElementRef 拿到的节点和很多第三方 API,都可能包含不安全的方法。
    要避免直接和 DOM 打交道,而是尽可能使用 Angular 模板。

    在无法避免的情况下,使用内置的 Angular 无害化处理函数。
    使用 DomSanitizer.sanitize 方法以及适当的 SecurityContext 来对不可信的值进行无害化处理。
 

  信任安全值

    有时候,应用程序确实需要包含可执行的代码,比如使用 URL 显示