Brief Intro

这里是基于 Angular 12 版本做分析。

@Arno(surfacew) 学习 Angular 框架使用 & 设计的过程,这里记录了不少官方文档的摘要和对应的思考,同时配合一部分自己写的用于理解 Angular 使用以及针对特定问题的源代码实现的 Sample 分析。

个人觉得,Angular 确实可以「慢慢细品 🍶」,有很多 BP 可以值得框架设计去借鉴。

Playground Code

:::success 🕶 Talk is cheap and show me your code :) :::

Play with CodeSandbox & DemoCode Here:

🏜 https://codesandbox.io/s/admiring-roentgen-xofde

Angular Modularity System

Angular 的模块系统是它的精粹。


Angular 核心概念之间的关系设计,这些是 Angular 模块化系统之中最小的可复用单元。先抛出一张图来理解全局:

Angular 使用 & 设计概览 - 图1

NgModule

问题:Angular 是如何做好模块化的,它的模块化系统是怎样设计的?

https://angular.io/guide/architecture-modules
https://angular.io/guide/ngmodules

Useful Decorators working for Modularity system.

The @NgModule metadata plays an important role in guiding the Angular compilation process that converts the application code you write into highly performant JavaScript code. The metadata describes how to compile a component’s template and how to create an injector at runtime. The @NgModule metadata plays an important role in guiding the Angular compilation process that converts the application code you write into highly performant JavaScript code. The metadata describes how to compile a component’s template and how to create an injector at runtime.

Angular 使用了几个维度来定义一个模块:

  • declarations:声明,声明物包含「Component」、「Directive」、「Pipe」三种类型。
  • imports:导入其它 NgModule 的声明,表明当前模块对其它 NgModule 的依赖。
  • providers:声明当前模块依赖的服务提供者,这样的声明在整个 App 内有效。
  • bootstrap:入口组件 Entry Component,类似于编程中的 main 函数。
  • exports:对外抛出的声明物(declarations),可以供其它模块复用。

:::info 💡 在框架设计的时候,使用注解来做静态配置确实是一种比 JSON 或者 xml 当做框架「约定式」配置更有编程的 Feel ~ :::

  1. /* JavaScript imports */
  2. import { BrowserModule } from '@angular/platform-browser';
  3. import { NgModule, Injectable } from '@angular/core';
  4. import { AppComponent } from './app.component';
  5. import { ShareComponent } from './share.component';
  6. import { AppPipe } from './app.pipe';
  7. import { AppDirective } from './app.directive';
  8. @Injectable({
  9. providedIn: 'root',
  10. })
  11. export class AppService {
  12. doServe(): any;
  13. }
  14. /* the AppModule class with the @NgModule decorator */
  15. @NgModule({
  16. declarations: [
  17. AppComponent,
  18. AppDirective,
  19. AppPipe
  20. ],
  21. imports: [
  22. BrowserModule,
  23. AngularHttpClientModule,
  24. AngularAnimationModule,
  25. ],
  26. exports: [ShareComponent, AppDirective],
  27. providers: [AppService],
  28. bootstrap: [AppComponent]
  29. })
  30. export class AppModule {
  31. constructor(private appService: AppService) {
  32. this.appService.getAppData();
  33. }
  34. }

https://angular.io/guide/glossary#module

In general, a module collects a block of code dedicated to a single purpose. Angular uses standard JavaScript modules and also defines an Angular module, NgModule.

In JavaScript (ECMAScript), each file is a module and all objects defined in the file belong to that module. Objects can be exported, making them public, and public objects can be imported for use by other modules.

Angular ships as a collection of JavaScript modules (also called libraries). Each Angular library name begins with the @angular prefix. Install Angular libraries with the npm package manager and import parts of them with JavaScript import declarations.

Compare to NgModule.

:::info ℹ️ 这里的 Angular 的 NgModule 更像是一个声明关系的 Manifest.json,用来描述 NgModuleDirectiveComponentServicePipe 等概念 & 实体之间的关系,是一份配置。
不过注解标注的类,也会在初始化的时候去做实例化,可以做一些模块启动的 Setups。 :::

:::warning 👨🏻‍💻 以至于很多时候被 @NgModule() 注解后的类都是空的 😅 :::


**NgModule.forRoot()****NgModule.forChild()** 静态方法是用来做什么的?

The forRoot() static method is a convention that makes it easy for developers to configure services and providers that are intended to be singletons. A good example of forRoot() is the RouterModule.forRoot() method.The forRoot() static method is a convention that makes it easy for developers to configure services and providers that are intended to be singletons. A good example of forRoot() is the RouterModule.forRoot() method.

保持依赖注入的服务(Service)为单例 or 新创建实例。

DI (Dependency Injection)

:::warning DI 系统和 Inversify 类似:

  • 实现了 Provider & Injector 主要功能
  • Angular 因为是框架所以对 DI 控制系统做了「收敛」,而 Inversify 提供了对 DI 系统更多的控制能力。
    • 细化可控的 Binding 过程
    • 异步 Provider
    • DI 过程的钩子函数
    • … :::

Providers & Injectors 两个核心概念的理解。

  • Providers 可以理解为依赖注入的注入内容的提供容器。
  • Injectors 则是实现对 Providers 中提供的依赖注入实现的 Resolver(解析器),根据作用域规则和参数修饰符等最终析出符合依赖的实现。

Anguar 在 ngModule 的 providers 中,提供了依赖注入支持的类型:

  • Class => 被 @Injectable 装饰后生成的 Class
  • ClassInstance => 对象常量
  • FactoryFunction => 使用工厂函数返回 @Injectable() 装饰后的类实例,在这里可以做到动态传参,解决具体场景依赖的服务实例化

最复杂的一个 Case 代码 Sample:

const heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};
export let heroServiceProvider =
{ provide: HeroService,
  useFactory: heroServiceFactory,
  // 依赖依旧可以声明注入,形成依赖链式结构
  deps: [Logger, UserService]
};

几个比较好玩的特性:

  • 支持 Alias 类型覆盖
  • 使用 InjectionToken 来做标示符,用于表示非 Class 类型的标示符(Provider Token)
    export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
    

Useful Decorators working for DI System

A decorator that appears immediately before a class definition, which declares the class to be of the given type, and provides metadata suitable to the type.

  • @Inject() and @Injectable()

理解 Angular 的 Scoped (hierachical dependency injection) Injector 的概念和设计原理?

https://angular.io/guide/hierarchical-dependency-injection
https://angular.io/guide/ngmodule-faq#why-does-lazy-loading-create-a-child-injector

When a component declares a dependency, Angular tries to satisfy that dependency with its own ElementInjector. If the component’s injector lacks the provider, it passes the request up to its parent component’s ElementInjector.
The requests keep forwarding up until Angular finds an injector that can handle the request or runs out of ancestor ElementInjectors.

If Angular doesn’t find the provider in any ElementInjectors, it goes back to the element where the request originated and looks in the ModuleInjector hierarchy. If Angular still doesn’t find the provider, it throws an error.When a component declares a dependency, Angular tries to satisfy that dependency with its own ElementInjector. If the component’s injector lacks the provider, it passes the request up to its parent component’s ElementInjector.

The requests keep forwarding up until Angular finds an injector that can handle the request or runs out of ancestor ElementInjectors.

If Angular doesn’t find the provider in any ElementInjectors, it goes back to the element where the request originated and looks in the ModuleInjector hierarchy. If Angular still doesn’t find the provider, it throws an error.

截屏2021-08-17 下午10.58.19.png

:::info 🔎 这里可以重点看一下,依赖 Resolve 的时候,修饰符和两套 Injector 系统的大致实现思路,在源代码里面是怎么跑起来的。 :::

Injector 类似 node_modules 有层次关系,顶层是 null(最后的兜底),ModuleInjector(平台相关,一般会注入比如 Web 平台相关的 Angualr 内置依赖,比如 Anglar 服务于 Web 浏览器的 Directives),root 则是客户注入的应用级别的 injector,在一个 AngularAppModule 之中实现依赖共享。

控制解析过程的 Modifier:

Angular’s resolution behavior can be modified with @Optional(), @Self(), @SkipSelf() and @Host(). Import each of them from @angular/core and use each in the component class constructor when you inject your service.Angular’s resolution behavior can be modified with @Optional(), @Self(), @SkipSelf() and @Host(). Import each of them from @angular/core and use each in the component class constructor when you inject your service.

  • What to do if Angular doesn’t find what you’re looking for, that is @Optional()
  • Where to start looking, that is @SkipSelf()
  • Where to stop looking, @Host() and @Self()

Instaniate:

This means that an NgModule behaves differently depending on whether it’s loaded during application start or lazy-loaded later. Neglecting that difference can lead to adverse consequences. Why should we use it? When an applications starts, Angular first configures the root injector with the providers of all eagerly loaded NgModules before creating its first component and injecting any of the provided services. Once the application begins, the application root injector is closed to new providers. As for the lazy-load modules, So Angular creates a new child injector for the lazy-loaded module context. Angular creates a new child injector for the lazy-loaded module context.

image.png

More From Docs:

Logic Reuse in Angular

:::warning 💎 https://www.yuque.com/surfacew/daily-learn/wraot9?inner=Hz0JG 👈 描述了主要的 Angular 可复用的基础单元的关系。 :::

Angular 对 NgModule 的整体可复用单元的定义:
截屏2021-08-17 上午1.28.29.png
Angular 对多种 Conventions 的定义以及对抽象和可复用程度的规范理解。https://angular.io/guide/module-types

Component Encapsulation

StyleEncapsulation

To control how this encapsulation happens on a per component basis, you can set the view encapsulation mode in the component metadata. Choose from the following modes:

:::success 💡 可以连接 ShadowDOM 的相关实验性质的代码学习。 :::

  • ShadowDom view encapsulation uses the browser’s native shadow DOM implementation (see Shadow DOM) to attach a shadow DOM to the component’s host element, and then puts the component view inside that shadow DOM. The component’s styles are included within the shadow DOM.
  • Emulated view encapsulation (the default) emulates the behavior of shadow DOM by preprocessing (and renaming) the CSS code to effectively scope the CSS to the component’s view. For details, see Inspecting generated CSS below.
  • None means that Angular does no view encapsulation. Angular adds the CSS to the global styles. The scoping rules, isolations, and protections discussed earlier don’t apply. This mode is essentially the same as pasting the component’s styles into the HTML.
    // warning: not all browsers support shadow DOM encapsulation at this time
    encapsulation: ViewEncapsulation.ShadowDom
    

LifeCycle

广泛为「组件」的行为所引用,设计可以借鉴。React 和 Vue 均实用了类似的思想。

ContentSlot

和 Vue 类似,对比 React 感觉没有那么自然。也可能是我比较熟悉 React 的缘故。

Template

Angular Template v.s. JSX v.s. Vue Template

  • Angular 的 Template 整体来说还是比较靠近 WebComponent 规范的,非常像一个自然的 HMTL Fragment 片段。
  • JSX 是真正意义上的 DSL,对 HTMLElement 略作修改,但习惯之后还是比较舒服,它让纯 HTML 更加具备「表达能力」,表达过程更加「程序语义化」。

Directives

https://angular.io/guide/built-in-directives

Service

将服务抽离为一种子类型设计。是符合 SingleResponsebility(单一职责)的分离设计。

Pipe

对字符串或者数据对象处理的 transform 类型 (data) => data

Angular Security Specs

https://angular.io/guide/security

:::warning ❓ 知识盲区,可以花时间扫盲。
WIP 后续再补充。 :::

Angular Conventions

Angular Naming Conventions

我一直觉得,秉承着「代码始终服务于阅读它的人」的思想,命名所应该遵循的「约定」非常重要,清晰明了,一眼便知其含义是非常重要的。

  • A folder named after the componentA component file,<component-name>.component.ts
  • A template file, <component-name>.component.html
  • A CSS file, <component-name>.component.css
  • A testing specification file, <component-name>.component.spec.ts

:::success 👆 的好处:便于文件级别的搜索 🔍、示意明确 💎,个人理解值得推崇。 :::

Angular Code Style

整个 Angular 的代码 StyleGuide 还是比较有意思的:https://angular.io/guide/styleguide,部分可以学习,可以摘出来讲一讲。

Angular Glossary

https://angular.io/guide/glossary#case-types

使用 Glossary 做概念的拉齐是比较重要的。其实对于框架设计,尤其是架构设计,概念一致性是非常重要的,Angular 对开发者抛出了大量的规约和概念,因此这个 Glossary 显得蛮重要的。

Angular Typing System

Whole Angular API References,最好的 TypeDoc,没有之一,很想把他们的源代码「扒下来」改造 😿
image.png

最关键的类型系统:https://angular.io/api ,这个文档库真的爱了。

Core Decorators

:::info 💡 元信息的标注还是在程序设计中可以重点借鉴的。 :::

image.png

使用元信息标注,便于 AOT 的是否,Compiler 做额外的事情,也可以方便 JIT(运行时)的时候去做部分声明式操作。这种注解式的开发,非常适合框架的研发,因为它保证了程序设计中「概念一致性」,用规约降低开发者的认知成本。

ErrorCode & ErrorList

:::success 💡 错误码,在任何框架中配合断言(Invariant),配合运行时错误码的抛出给用户,可以极大提升开发者体验。 :::

错误类型设计(ErrorCode) & DingTalk Modular Framework 在设计的时候可以重点借鉴,在框架设计的时候这块是比较重要的!

Angular Resources

建立自己的生态模式。

https://angular.io/resources?category=development

Angular Cli

Angular Cli and DingTalk Cli 是否可以有所学习和借鉴。Angular Cli 的完备性整体还是比较高的,可以深度「借鉴」。维系了整个研发周期和过程。

  • DingTalk Plugin Framework 是否也应该具备这种完备性呢?
  • VueCli 可以对比学习:https://cli.vuejs.org/

Angular and Rx.js

Angular 把 Rx.js 首推的原因可以认真理解一下。

https://angular.io/guide/observables

Rx.js lib 重头戏?

rx.js

Ask Questions

Q: NgModule 下面挂载的类本质上是一个空壳么???!!!👈 比如 MaterialDesign 里面

不是,可以做一些全局的 Setup,因为每次 Import Module 的时候,会初始化 Module 对应的 Class 的 constructor 函数,里面可以做有效的依赖注入(基于 Provider)。


Q:Directive 这种类型的抽象映射到 React 的架构之下是什么呢?MaterialDesign 中这个 Dir 的实现就很灵魂

Directive 主要是针对 CustomElement 或者 CustomElementProperty 的固定抽象。换做 React 的话,其实可以用 Hook 也可以用 Mixin 等机制去实现。本质上是一些业务逻辑和 DOM 操作的封装,使用 React 提供的高阶组件(HighOrderComponent)也是一种类似思路。


Q:How to use **less / sass** in Angular?

使用 AngularCli 里面自定义 Schema 做扩展。

后续更新

  • V13 主要把 IvyRender 真正意义上替换了 V12 的 Render 系统

Reference

Inversify API 概览
🎛 剖一剖 Inversify.js IOC & DI 实现机制

  • 个人早期对 Angular 学习 angluar