網站搜尋

typescript - 在 Angular 中調用具有依賴性的函數


使用 Angular 5 和 UIRouter 狀態路由。我正在根據此介面使用額外的自訂路由狀態屬性。

interface AugmentedNg2RouteDefinition extends Ng2StateDeclaration {
    default?: string | ((...args: any[]) => string | Promise<string>);
}

當我定義一個抽象狀態時,我現在也可以為它添加一個 default 屬性,因此當有人試圖路由到一個抽象狀態時,默認應該將他們重定向到配置的 default 子狀態。

從上方介面可以理解,default 可以定義為以下任何一種:

// relative state name
default: '.child',
// absolute state name
default: 'parent.child',
// function with DI injectables
default: (auth: AuthService, stateService: StateService) => {
    if (auth.isAuthenticated) {
        return '.child';
    } else {
        return stateService.target('.login', { ... });
    }
}
// function with DI injectables returning a promise
default: (items: ItemsService) => {
    return items
        .getTotal()
        .then((count) => {
            return count > 7
                ? '.simple'
                : '.paged';
        });
}

要實際讓 default 生效,我必須配置路由過渡服務:

@NgModule({
  imports: [
    ...
    UIRouterModule.forChild({  // or "forRoot"
      states: ...
      // THIS SHOULD PROCESS "default" PROPERTY ON ABSTRACT STATES
      config: (uiRouter: UIRouter, injector: Injector, module: StatesModule) => {
        uiRouter.transitionService.onBefore(
          // ONLY RUN THIS ON ABSTRACTS WITH "default" SET
          {
            to: state => state.abstract === true && !!state.self.default
          },
          // PROCESS "default" VALUE
          transition => {
            let to: transition.to();
            if (angular.isFunction(to.default)) {
              // OK WE HAVE TO EXECUTE THE FUNCTION WITH INJECTABLES SOMEHOW
            } else {
              // this one's simple as "default" is a string
              if (to.default[0] === '.') {
                  to.default = to.name + to.default;
              }
              return transition.router.stateService.target(to.default);
            }
          }
        );
      }
    })
  ]
})
export class SomeFeatureModule { }

所以問題是在它是一個可能有一些可注入服務/值的函數時調用 default…

配置函數的注入器(config: (uiRouter: UIRouter, injector: Injector, module: StatesModule))只能用來獲取服務實例,但不能調用帶有可注入參數的函數。

在 AngularJS 中,這可以通過 $injector.invoke(...) 來完成,這個方法會呼叫函數並注入其參數。

主要問題

當 default 被定義為具有可注入項的函數時,我應該如何處理它。

答案#1

在 Angular 中沒有直接對應 AngularJS 的 $injector.invoke,因為可注入的函數預期是設計時定義的 useFactory 提供者。

在 AngularJS 中只有一個注入器實例,但在 Angular 中有一個注入器層級結構,這也使事情變得複雜,因為依賴項應該存在於呼叫函數的注入器上。

處理這個問題的慣用方法是將所有預期要被調用的函數定義為提供者。這意味著函數只能使用它被定義時所在注入器的實例(根模塊或子模塊):

export function fooDefaultStateFactory(baz) {
  return () => baz.getStateName();
}

@NgModule({
  providers: [
    Baz,
    {
      provider: fooDefaultStateFactory,
      useFactory: fooDefaultStateFactory,
      deps: [Baz]
    }
  ],
  ...
})
...

// relative state name
default: '.child',
...
// function with DI injectables
default: fooDefaultStateFactory

然後,工廠函數可以像從注入器中獲取其他依賴一樣被取得並調用:

  transition => {
    ...
    if (typeof to.default === 'string') {
      ...
    } else if (to.default) {
      const defaultState = injector.get(to.default);

      if (typeof defaultState === 'function') {
        // possibly returns a promise
        Promise.resolve(defaultState()).then(...)
      } else { ... }
    }
  }

一個對應於 $injector.invoke 的函數,它可以與任何函數一起使用,應該大致類似於 Angular 2/4 中 Class 幫助器(在 Angular 5 中已棄用)的構造函數定義方式。不同之處在於,Class 接受的構造函數需要用陣列或 parameters 靜態屬性進行註解,這些註解預期是一個陣列的陣列,因為依賴可能涉及裝飾器(Inject、Optional 等)。

由於裝飾器不適用於未註冊為提供者的函數,因此該陣列預期是純陣列,類似於 AngularJS 的隱式註解或 Angular 中 useFactory 提供者的 deps:

function invoke(injector, fnOrArr) {
  if (Array.isArray(fnOrArr)) {
    const annotations = [...fnOrArr];
    const fn = annotations.pop();
    const deps = annotations.map(annotation => injector.get(annotation));
    return fn(...deps);
  } else {
    return fnOrArr();
  }
}

可以綁定到注射器實例:

const injectorInvoke = invoke.bind(injector);
injectorInvoke([Foo, Bar, (foo: Foo, bar: Bar) => {...}]);

而呼叫函數的程式碼片段則被修改為:

  ...
  if (typeof defaultState === 'function' || Array.isArray(defaultState)) {
    // possibly returns a promise
    Promise.resolve(injectorInvoke(defaultState)).then(...)
  } else { ... }
  ...

答案#2

這是一種方法,你可以透過加入關於解決服務的部分來解決這個問題。

// THE IMPORTANT PART
config: (uiRouter: UIRouter, injector: Injector, module: StatesModule) => {
   uiRouter.transitionService.onBefore(
      // HookMatchCriteria
      {
         to: state => state.abstract === true && !!state.self.default
      },
      // TransitionHookFn
      transition => {
         let to: transition.to();
         if (typeof to.default === "string") {
            return transition.router.stateService.target(to.default);
         } else if (typeof to.default === "function") {
            let functionPromise = Promise.resolve(to.default(injector));
            return functionPromise.then((toDefault) => transition.router.stateService.target(toDefault));
         }
}

檢查預設值是否為 string 的第一部分是顯而易見的。在第二個 if 中,我檢查參數是否為函數,這會自動在 TypeScript 中將其視為函數,因此可以直接調用。在結果上,它可以是 Promise<string> 或 string,我接著使用 Promise.resolve(value) 方法。該方法對兩種輸入都返回一個新的 Promise<string>,因此我們可以將其與對 stateService 的調用鏈接起來,它將返回一個 Promise<TargetState>,這對 TransitionHookFn 來說是一個有效的返回值。

為了解決服務問題,你可以這樣更改你的介面:

interface AugmentedNg2RouteDefinition extends Ng2StateDeclaration {
    default?: string | ((injector: Injector, ...args: any[]) => string | Promise<string>);
}

那麼每次調用函數時,你也會總是傳送注入器,因為你在配置方法中已有它。如果所有的參數都預期是服務,則刪除 ...args,僅傳送注入器。

那麼你作為範例給出的函數就會看起來像這樣:

// relative state name
default: '.child',
// absolute state name
default: 'parent.child',
// function with DI injectables
default: (injector: Injector) => {
    let auth: AuthService = injector.get(AuthService);
    let stateService: StateService = injector.get(StateService);
    if (auth.isAuthenticated) {
        return '.child';
    } else {
        return stateService.target('.login', { ... });
    }
}
// function with DI injectables returning a promise
default: (injector: Injector) => {
    let items: ItemsService = injector.get(ItemsService);
    return items
        .getTotal()
        .then((count) => {
            return count > 7
                ? '.simple'
                : '.paged';
        });