聊聊Angular中的後設資料(Metadata)和裝飾器(Decorator)

2022-02-28 13:01:02
本篇文章繼續Angular的學習,帶大家瞭解一下中的後設資料和裝飾器,簡單瞭解一下他們的用法,希望對大家有所幫助!

作為「為大型前端專案」而設計的前端框架,Angular 其實有許多值得參考和學習的設計,本系列主要用於研究這些設計和功能的實現原理。本文主要圍繞 Angular 中隨處可見的後設資料,來進行介紹。【相關教學推薦:《》】

裝飾器是使用 Angular 進行開發時的核心概念。在 Angular 中,裝飾器用於為類或屬性附加後設資料,來讓自己知道那些類或屬性的含義,以及該如何處理它們。

裝飾器與後設資料

不管是裝飾器還是後設資料,都不是由 Angular 提出的概念。因此,我們先來簡單瞭解一下。

後設資料(Metadata)

在通用的概念中,後設資料是描述使用者資料的資料。它總結了有關資料的基本資訊,可以使查詢和使用特定資料範例更加容易。例如,作者,建立日期,修改日期和檔案大小是非常基本的檔案後設資料的範例。

在用於類的場景下,後設資料用於裝飾類,來描述類的定義和行為,以便可以設定類的預期行為。

裝飾器(Decorator)

裝飾器是 JavaScript 的一種語言特性,是一項位於階段 2(stage 2)的試驗特性。

裝飾器是定義期間在類,類元素或其他 JavaScript 語法形式上呼叫的函數。

裝飾器具有三個主要功能:

  • 可以用具有相同語意的匹配值替換正在修飾的值。(例如,裝飾器可以將方法替換為另一種方法,將一個欄位替換為另一個欄位,將一個類替換為另一個類,等等)。

  • 可以將後設資料與正在修飾的值相關聯;可以從外部讀取此後設資料,並將其用於超程式設計和自我檢查。

  • 可以通過後設資料提供對正在修飾的值的存取。對於公共值,他們可以通過值名稱來實現;對於私有值,它們接收存取器函數,然後可以選擇共用它們。

本質上,裝飾器可用於對值進行超程式設計和向其新增功能,而無需從根本上改變其外部行為。

更多的內容,可以參考 tc39/proposal-decorators 提案。

Angular 中的裝飾器和後設資料

我們在開發 Angular 應用時,不管是元件、指令,還是服務、模組等,都需要通過裝飾器來進行定義和開發。裝飾器會出現在類定義的緊前方,用來宣告該類具有指定的型別,並且提供適合該型別的後設資料。

比如,我們可以用下列裝飾器來宣告 Angular 的類:@Component()@Directive()@Pipe()@Injectable()@NgModule()

使用裝飾器和後設資料來改變類的行為

@Component()為例,該裝飾器的作用包括:

  • 將類標記為 Angular 元件。

  • 提供可設定的後設資料,用來確定應在執行時如何處理、範例化和使用該元件。

關於@Component()該如何使用可以參考,這裡不多介紹。我們來看看這個裝飾器的定義:

// 提供 Angular 元件的設定後設資料介面定義
// Angular 中,元件是指令的子集,始終與模板相關聯
export interface Component extends Directive {
  // changeDetection 用於此元件的變更檢測策略
  // 範例化元件時,Angular 將建立一個更改檢測器,該更改檢測器負責傳播元件的繫結。
  changeDetection?: ChangeDetectionStrategy;
  // 定義對其檢視 DOM 子物件可見的可注入物件的集合
  viewProviders?: Provider[];
  // 包含元件的模組的模組ID,該元件必須能夠解析模板和樣式的相對 URL
  moduleId?: string;
  ...
  // 模板和 CSS 樣式的封裝策略
  encapsulation?: ViewEncapsulation;
  // 覆蓋預設的插值起始和終止定界符(`{{`和`}}`)
  interpolation?: [string, string];
}

// 元件裝飾器和後設資料
export const Component: ComponentDecorator = makeDecorator(
    'Component',
    // 使用預設的 CheckAlways 策略,在該策略中,更改檢測是自動進行的,直到明確停用為止。
    (c: Component = {}) => ({changeDetection: ChangeDetectionStrategy.Default, ...c}),
    Directive, undefined,
    (type: Type<any>, meta: Component) => SWITCH_COMPILE_COMPONENT(type, meta));

以上便是元件裝飾、元件後設資料的定義,我們來看看裝飾器的建立過程。

裝飾器的建立過程

我們可以從原始碼中找到,元件和指令的裝飾器都會通過makeDecorator()來產生:

export function makeDecorator<T>(
    name: string, props?: (...args: any[]) => any, parentClass?: any, // 裝飾器名字和屬性
    additionalProcessing?: (type: Type<T>) => void,
    typeFn?: (type: Type<T>, ...args: any[]) => void):
    {new (...args: any[]): any; (...args: any[]): any; (...args: any[]): (cls: any) => any;} {
  // noSideEffects 用於確認閉包編譯器包裝的函數沒有副作用
  return noSideEffects(() => { 
    const metaCtor = makeMetadataCtor(props);
    // 裝飾器工廠
    function DecoratorFactory(
        this: unknown|typeof DecoratorFactory, ...args: any[]): (cls: Type<T>) => any {
      if (this instanceof DecoratorFactory) {
        // 賦值後設資料
        metaCtor.call(this, ...args);
        return this as typeof DecoratorFactory;
      }
      // 建立裝飾器工廠
      const annotationInstance = new (DecoratorFactory as any)(...args);
      return function TypeDecorator(cls: Type<T>) {
        // 編譯類
        if (typeFn) typeFn(cls, ...args);
        // 使用 Object.defineProperty 很重要,因為它會建立不可列舉的屬性,從而防止該屬性在子類化過程中被複制。
        const annotations = cls.hasOwnProperty(ANNOTATIONS) ?
            (cls as any)[ANNOTATIONS] :
            Object.defineProperty(cls, ANNOTATIONS, {value: []})[ANNOTATIONS];
        annotations.push(annotationInstance);
        // 特定邏輯的執行
        if (additionalProcessing) additionalProcessing(cls);

        return cls;
      };
    }
    if (parentClass) {
      // 繼承父類別
      DecoratorFactory.prototype = Object.create(parentClass.prototype);
    }
    DecoratorFactory.prototype.ngMetadataName = name;
    (DecoratorFactory as any).annotationCls = DecoratorFactory;
    return DecoratorFactory as any;
  });
}

在上面的例子中,我們通過makeDecorator()產生了一個用於定義元件的Component裝飾器工廠。當使用@Component()建立元件時,Angular 會根據後設資料來編譯元件。

根據裝飾器後設資料編譯元件

Angular 會根據該裝飾器後設資料,來編譯 Angular 元件,然後將生成的元件定義(ɵcmp)修補到元件型別上:

export function compileComponent(type: Type<any>, metadata: Component): void {
  // 初始化 ngDevMode
  (typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode();
  let ngComponentDef: any = null;
  // 後設資料可能具有需要解析的資源
  maybeQueueResolutionOfComponentResources(type, metadata);
  // 這裡使用的功能與指令相同,因為這只是建立 ngFactoryDef 所需的後設資料的子集
  addDirectiveFactoryDef(type, metadata);
  Object.defineProperty(type, NG_COMP_DEF, {
    get: () => {
      if (ngComponentDef === null) {
        const compiler = getCompilerFacade();
        // 根據後設資料解析元件
        if (componentNeedsResolution(metadata)) {
          ...
          // 例外處理
        }
        ...
        // 建立編譯元件需要的完整後設資料
        const templateUrl = metadata.templateUrl || `ng:///${type.name}/template.html`;
        const meta: R3ComponentMetadataFacade = {
          ...directiveMetadata(type, metadata),
          typeSourceSpan: compiler.createParseSourceSpan('Component', type.name, templateUrl),
          template: metadata.template || '',
          preserveWhitespaces,
          styles: metadata.styles || EMPTY_ARRAY,
          animations: metadata.animations,
          directives: [],
          changeDetection: metadata.changeDetection,
          pipes: new Map(),
          encapsulation,
          interpolation: metadata.interpolation,
          viewProviders: metadata.viewProviders || null,
        };
        // 編譯過程需要計算深度,以便確認編譯是否最終完成
        compilationDepth++;
        try {
          if (meta.usesInheritance) {
            addDirectiveDefToUndecoratedParents(type);
          }
          // 根據模板、環境和元件需要的後設資料,來編譯元件
          ngComponentDef = compiler.compileComponent(angularCoreEnv, templateUrl, meta);
        } finally {
          // 即使編譯失敗,也請確保減少編譯深度
          compilationDepth--;
        }
        if (compilationDepth === 0) {
          // 當執行 NgModule 裝飾器時,我們將模組定義加入佇列,以便僅在所有宣告都已解析的情況下才將佇列出隊,並將其自身作為模組作用域新增到其所有宣告中
          // 此呼叫執行檢查以檢視佇列中的任何模組是否可以出隊,並將範圍新增到它們的宣告中
          flushModuleScopingQueueAsMuchAsPossible();
        }
        // 如果元件編譯是非同步的,則宣告該元件的 @NgModule 批註可以執行並在元件型別上設定 ngSelectorScope 屬性
        // 這允許元件在完成編譯後,使用模組中的 directiveDefs 對其自身進行修補
        if (hasSelectorScope(type)) {
          const scopes = transitiveScopesFor(type.ngSelectorScope);
          patchComponentDefWithScope(ngComponentDef, scopes);
        }
      }
      return ngComponentDef;
    },
    ...
  });
}

編譯元件的過程可能是非同步的(比如需要解析元件模板或其他資源的 URL)。如果編譯不是立即進行的,compileComponent會將資源解析加入到全域性佇列中,並且將無法返回ɵcmp,直到通過呼叫resolveComponentResources解決了全域性佇列為止。

編譯過程中的後設資料

後設資料是有關類的資訊,但它不是類的屬性。因此,用於設定類的定義和行為的這些資料,不應該儲存在該類的範例中,我們還需要在其他地方儲存此資料。

在 Angular 中,編譯過程產生的後設資料,會使用CompileMetadataResolver來進行管理和維護,這裡我們主要看指令(元件)相關的邏輯:

export class CompileMetadataResolver {
  private _nonNormalizedDirectiveCache =
      new Map<Type, {annotation: Directive, metadata: cpl.CompileDirectiveMetadata}>();
  // 使用 Map 的方式來儲存
  private _directiveCache = new Map<Type, cpl.CompileDirectiveMetadata>(); 
  private _summaryCache = new Map<Type, cpl.CompileTypeSummary|null>();
  private _pipeCache = new Map<Type, cpl.CompilePipeMetadata>();
  private _ngModuleCache = new Map<Type, cpl.CompileNgModuleMetadata>();
  private _ngModuleOfTypes = new Map<Type, Type>();
  private _shallowModuleCache = new Map<Type, cpl.CompileShallowModuleMetadata>();

  constructor(
      private _config: CompilerConfig, private _htmlParser: HtmlParser,
      private _ngModuleResolver: NgModuleResolver, private _directiveResolver: DirectiveResolver,
      private _pipeResolver: PipeResolver, private _summaryResolver: SummaryResolver<any>,
      private _schemaRegistry: ElementSchemaRegistry,
      private _directiveNormalizer: DirectiveNormalizer, private _console: Console,
      private _staticSymbolCache: StaticSymbolCache, private _reflector: CompileReflector,
      private _errorCollector?: ErrorCollector) {}
  // 清除特定某個指令的後設資料
  clearCacheFor(type: Type) {
    const dirMeta = this._directiveCache.get(type);
    this._directiveCache.delete(type);
    ...
  }
  // 清除所有後設資料
  clearCache(): void {
    this._directiveCache.clear();
    ...
  }
  /**
   * 載入 NgModule 中,已宣告的指令和的管道
   */
  loadNgModuleDirectiveAndPipeMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true):
      Promise<any> {
    const ngModule = this.getNgModuleMetadata(moduleType, throwIfNotFound);
    const loading: Promise<any>[] = [];
    if (ngModule) {
      ngModule.declaredDirectives.forEach((id) => {
        const promise = this.loadDirectiveMetadata(moduleType, id.reference, isSync);
        if (promise) {
          loading.push(promise);
        }
      });
      ngModule.declaredPipes.forEach((id) => this._loadPipeMetadata(id.reference));
    }
    return Promise.all(loading);
  }
  // 載入指令(元件)後設資料
  loadDirectiveMetadata(ngModuleType: any, directiveType: any, isSync: boolean): SyncAsync<null> {
    // 若已載入,則直接返回
    if (this._directiveCache.has(directiveType)) {
      return null;
    }
    directiveType = resolveForwardRef(directiveType);
    const {annotation, metadata} = this.getNonNormalizedDirectiveMetadata(directiveType)!;
    // 建立指令(元件)後設資料
    const createDirectiveMetadata = (templateMetadata: cpl.CompileTemplateMetadata|null) => {
      const normalizedDirMeta = new cpl.CompileDirectiveMetadata({
        isHost: false,
        type: metadata.type,
        isComponent: metadata.isComponent,
        selector: metadata.selector,
        exportAs: metadata.exportAs,
        changeDetection: metadata.changeDetection,
        inputs: metadata.inputs,
        outputs: metadata.outputs,
        hostListeners: metadata.hostListeners,
        hostProperties: metadata.hostProperties,
        hostAttributes: metadata.hostAttributes,
        providers: metadata.providers,
        viewProviders: metadata.viewProviders,
        queries: metadata.queries,
        guards: metadata.guards,
        viewQueries: metadata.viewQueries,
        entryComponents: metadata.entryComponents,
        componentViewType: metadata.componentViewType,
        rendererType: metadata.rendererType,
        componentFactory: metadata.componentFactory,
        template: templateMetadata
      });
      if (templateMetadata) {
        this.initComponentFactory(metadata.componentFactory!, templateMetadata.ngContentSelectors);
      }
      // 儲存完整的後設資料資訊,以及後設資料摘要資訊
      this._directiveCache.set(directiveType, normalizedDirMeta);
      this._summaryCache.set(directiveType, normalizedDirMeta.toSummary());
      return null;
    };

    if (metadata.isComponent) {
      // 如果是元件,該過程可能為非同步過程,則需要等待非同步過程結束後的模板返回
      const template = metadata.template !;
      const templateMeta = this._directiveNormalizer.normalizeTemplate({
        ngModuleType,
        componentType: directiveType,
        moduleUrl: this._reflector.componentModuleUrl(directiveType, annotation),
        encapsulation: template.encapsulation,
        template: template.template,
        templateUrl: template.templateUrl,
        styles: template.styles,
        styleUrls: template.styleUrls,
        animations: template.animations,
        interpolation: template.interpolation,
        preserveWhitespaces: template.preserveWhitespaces
      });
      if (isPromise(templateMeta) && isSync) {
        this._reportError(componentStillLoadingError(directiveType), directiveType);
        return null;
      }
      // 並將後設資料進行儲存
      return SyncAsync.then(templateMeta, createDirectiveMetadata);
    } else {
      // 指令,直接儲存後設資料
      createDirectiveMetadata(null);
      return null;
    }
  }
  // 獲取給定指令(元件)的後設資料資訊
  getDirectiveMetadata(directiveType: any): cpl.CompileDirectiveMetadata {
    const dirMeta = this._directiveCache.get(directiveType)!;
    ...
    return dirMeta;
  }
  // 獲取給定指令(元件)的後設資料摘要資訊
  getDirectiveSummary(dirType: any): cpl.CompileDirectiveSummary {
    const dirSummary =
        <cpl.CompileDirectiveSummary>this._loadSummary(dirType, cpl.CompileSummaryKind.Directive);
    ...
    return dirSummary;
  }
}

可以看到,在編譯過程中,不管是元件、指令、管道,還是模組,這些類在編譯過程中的後設資料,都使用Map來儲存。

總結

本節我們介紹了 Angular 中的裝飾器和後設資料,其中後設資料用於描述類的定義和行為。

在 Angular 編譯過程中,會使用Map的資料結構來維護和儲存裝飾器的後設資料,並根據這些後設資料資訊來編譯元件、指令、管道和模組等。

更多程式設計相關知識,請存取:!!

以上就是聊聊Angular中的後設資料(Metadata)和裝飾器(Decorator)的詳細內容,更多請關注TW511.COM其它相關文章!