理解Angular的providers - 給Http新增預設headers

mavlarn發表於2017-06-30

在一般的web應用裡,經常會需要在每次傳送Http請求的時候,新增header或者一些預設的引數。本文就來看看這個需求的幾種實現方式。通過這個實現,我們也能夠理解Angular的服務,及其providers的原理。

我們的目的是對於每個Http請求,都往Header裡面新增一個token,用於在伺服器端進行身份驗證。因為Http是一個服務,所以我就想當然的想到,我可以通過擴充套件框架提供的Http來新增。那麼要怎麼擴充套件一個框架提供的服務呢?那就是用providers。
NgModule裡,有一個屬性providers,一般我們是用它來告訴框架,我們的app要用到我們定義的某些服務,例如我寫了一個UserService用來進行使用者資料的讀寫操作,又比如寫一個AuthGuardService來實現路由的Guard。對於框架或者使用的其他元件庫的服務,我們不需要在這裡新增,只需要在imports裡面加入相應的模組即可。

自定義系統服務

那麼,如果我們想修改框架提供的某個服務,例如想擴充套件它,該怎麼實現呢?我們可以將擴充套件的這個服務,新增到providers裡,只是新增的方式不太一樣。需要使用下面的方式:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule, RouterModule, HttpModule
  ],
  providers: [UserService, AuthGuardService,
    { provide: Http, useClass: BaseHttp }
  ],
  bootstrap: [ AppComponent ]
})複製程式碼

我們擴充套件了Http服務,新的服務的類名是BaseHttp,然後在providers裡使用{ provide: Http, useClass: BaseHttp },告訴框架,我們要使用BaseHttp這個類,來提供對Http的實現。然後,在Angular的容器裡面的Http服務實際上是BaseHttp這個類的實現,當我們通過注入獲得一個Http例項的時候,也是獲得的BaseHttp的例項。

實現自動新增Header

接下來,我們就來看看怎麼實現自動的Header的新增。首先,我想到的第一種方式,就是擴充套件Http,在它的建構函式裡設定一個預設的Header。

在建構函式中實現

@Injectable()
export class BaseHttp extends Http {
  constructor (backend: XHRBackend, options: RequestOptions) {
    super(backend, options);

    let token = localStorage.getItem(AppConstants.tokenName);
    options.headers.set(AppConstants.authHeaderName, token);
  }
}複製程式碼

這個就是在建構函式裡面,從localStorage裡拿到token,然後放到RequestOptions裡。看著似乎沒有問題,但是執行的時候發現,這個Http服務是在app初始化的時候建立的,所以這個建構函式在呼叫的時候,localStorage裡可能還沒有token。這樣,即使使用者之後登陸了,之前的預設的options也不會更新。

在request中實現

所以,在建構函式中實現肯定是不行的,我通過觀察Http的介面(通過你使用的IDE,可以跟蹤到介面的定義檔案,來檢視介面的定義),看到有很多方法get(...), post(...), put(...)等,如果我需要重新實現所有的這些方法,那就太麻煩了,感覺沒有必要。然後,我看到request(...)方法,看他的方法的註釋知道,所有其他方法最終都會呼叫這個方法來傳送實際的請求。所以,我們只需要重寫這個方法就可以:

@Injectable()
export class BaseHttp extends Http {
  constructor (backend: XHRBackend, options: RequestOptions) {
    super(backend, options)
  }

  request(url: string|Request, options?: RequestOptionsArgs): Observable<Response> {
    const token = localStorage.getItem(AppConstants.tokenName)

    if (typeof url === 'string') { // meaning we have to add the token to the options, not in url
      if (!options) {
        options = new RequestOptions({})
      }
      options.headers.set(AppConstants.authHeaderName, token)
    } else {
      url.headers.set(AppConstants.authHeaderName, token)
    }
    return super.request(url, options)
  }
}複製程式碼

這個實現也很容易,唯一需要說明的是,這裡的url它有可能有2種型別,stringRequest,如果是string型別,說明這個url就是一個字串,那麼我們要設定的header肯定不會在它裡面。

那如果url是Request型別呢?我們再來看看它的定義。通過看它的定義:

export declare class Request extends Body {
    /**
     * Http method with which to perform the request.
     */
    method: RequestMethod;
    /**
     * {@link Headers} instance
     */
    headers: Headers;
    /** Url of the remote resource */
    url: string;
    /** Type of the request body **/
    private contentType;
    /** Enable use credentials */
    withCredentials: boolean;
    /** Buffer to store the response */
    responseType: ResponseContentType;
    constructor(requestOptions: RequestArgs);
    /**
     * Returns the content type enum based on header options.
     */
    detectContentType(): ContentType;
    /**
     * Returns the content type of request's body based on its type.
     */
    detectContentTypeFromBody(): ContentType;
    /**
     * Returns the request's body according to its type. If body is undefined, return
     * null.
     */
    getBody(): any;
}複製程式碼

我們知道它是一個類,裡面有一個成員headers,然後我們再看看Headers這個型別,看到它有一個set()方法,是用來往headers裡面新增值的,這正是我們需要的。

所以,在我們實現的BaseHttp.request()方法裡,根據url的型別,再判斷options是否為空等。通過測試,這種方法能夠實現我們的需求,不管是初始化的時候在localStorage裡面就有token,還是之後登陸,甚至退出後更新再登入(會更新localStorage的token)等,都能滿足。

重新實現 RequestOptions

雖然上面的方法以及能夠解決問題,那麼,能不能再簡單一點呢?因為我們需要的只是更新Options,但是,為了這個,我們攔截了Http的請求。那我們是不是可以直接擴充套件RequestOptions來實現呢?答案是yes。而且更容易,我們可以繼承BaseRequestOptions,重寫merge(...)方法。


@Injectable()
export class AuthRequestOptions extends BaseRequestOptions {
  merge(options?: RequestOptionsArgs): RequestOptions {
    let newOptions = super.merge(options);
    let token = localStorage.getItem(AppConstants.tokenName);
    newOptions.headers.set(AppConstants.authHeaderName, token);
    return newOptions;
  }
}複製程式碼

這個merge(...)方法會在每次請求的時候被呼叫,用來把請求的時候的options和預設options進行合併。

經過測試,這種方法也能夠完美的解決我們的需求。

總結

所以,這就是Angular強大與方便的地方,它使用了很多現象物件的特性,如繼承、介面、實現等;也用了很多伺服器端Java框架的特性,例如容器等。上面說的provider也就是容器裡面物件例項的提供者,本來RequestOptions型別的提供者是BaseRequestOptions,但是,我繼承了它,重寫了一個方法,把這個型別的提供者改成了我寫的類。這樣,Angular容器在初始化的時候,就會使用我提供的類來建立這個型別的例項。

而且,在這幾種實現方式的探索過程中,我完全沒有檢視Angular的文件,也沒有網上查什麼資料。知識檢視類或介面的定義,通過它的註釋,我就有了思路,然後嘗試實現,就成功了。這也是TypeScript給我嗎帶來的遍歷。

相關文章