Angular 4.x HttpModule 揭祕

semlinker發表於2019-03-01

有點小雞凍,我們 HttpModule 系列的主角終於要出場了。此時突然想起了一句詩:

千呼萬喚始出來,猶抱琵琶半遮面。 —— 白居易 <<琵琶行>>

為了寫好這篇文章 (寫得不好的話,大家請見諒),考慮了一番,最終還是打算先寫相關的基礎文章:

其中 HTTP 最強資料大全你不知道的 XMLHttpRequest 內容比較全面,但對於我們揭祕 Angular 4.x HttpModule 模組,我們只需瞭解其中的一些相關知識即可。因此下面我也僅會介紹相關的知識點,若想了解詳細資訊,大家可以檢視原文。

直接略過基礎部分,直達 HttpModule

HTTP 協議

超文字傳輸協議英文HyperText Transfer Protocol縮寫HTTP)是網際網路上應用最為廣泛的一種網路協議。設計HTTP最初的目的是為了提供一種釋出和接收HTML頁面的方法。通過HTTP或者HTTPS協議請求的資源由統一資源識別符號(Uniform Resource Identifiers,URI)來標識。—— 維基百科

HTTP 協議是基於請求與響應,具體如下圖所示:

Angular 4.x HttpModule 揭祕

HTTP 請求報文

HTTP 請求報文由請求行請求頭空行請求體(請求資料) 4 個部分組成,如下圖所示:

Angular 4.x HttpModule 揭祕

請求報文示例

GET / HTTP/1.1
Host: www.baidu.com
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8複製程式碼

HTTP 響應報文

HTTP響應報文由狀態行、響應頭、空行和響應體4 個部分組成,如下圖所示:

Angular 4.x HttpModule 揭祕

響應報文示例

HTTP/1.1 200 OK
Server: bfe/1.0.8.18
Date: Thu, 30 Mar 2017 12:28:00 GMT
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Cache-Control: private
Expires: Thu, 30 Mar 2017 12:27:43 GMT
Set-Cookie: BDSVRTM=0; path=/複製程式碼

XMLHttpRequest

XMLHttpRequest 是一個 API, 它為客戶端提供了在客戶端和伺服器之間傳輸資料的功能。它提供了一個通過 URL 來獲取資料的簡單方式,並且不會使整個頁面重新整理。這使得網頁只更新一部分頁面而不會打擾到使用者。XMLHttpRequest 在 AJAX 中被大量使用。

XMLHttpRequest 是一個 JavaScript 物件,它最初由微軟設計,隨後被 Mozilla、Apple 和 Google 採納. 如今,該物件已經被 W3C組織標準化. 通過它,你可以很容易的取回一個 URL 上的資源資料. 儘管名字裡有 XML, 但 XMLHttpRequest 可以取回所有型別的資料資源,並不侷限於 XML。 而且除了HTTP ,它還支援 fileftp 協議。

建構函式

用於初始化一個 XMLHttpRequest 物件,必須在所有其它方法被呼叫前呼叫建構函式。使用示例如下:

var req = new XMLHttpRequest();複製程式碼

屬性

  • onreadystatechange: Function - 當 readyState 屬性改變時會呼叫它。
  • readyState: unsigned short - 用於表示請求的五種狀態:
狀態 描述
0 UNSENT (未開啟) 表示已建立 XHR 物件,open() 方法還未被呼叫
1 OPENED (未傳送) open() 方法已被成功呼叫,send() 方法還未被呼叫
2 HEADERS_RECEIVED (已獲取響應頭) send() 方法已經被呼叫,響應頭和響應狀態已經返回
3 LOADING (正在下載響應體) 響應體下載中,responseText中已經獲取了部分資料
4 DONE (請求完成) 整個請求過程已經完畢
  • response: varies - 響應體的型別由 responseType 來指定,可以是 ArrayBuffer、Blob、Document、JSON,或者是字串。如果請求未完成或失敗,則該值為 null。
  • responseText: DOMString - 此請求的響應為文字,或者當請求未成功或還是未傳送時未 null (只讀)
  • responseType: XMLHttpRequestResponseType - 設定該值能夠改變響應型別,就是告訴伺服器你期望的響應格式:
響應資料型別
"" 字串(預設值)
"arraybuffer" ArrayBuffer
"blob" Blob
"document" Document
"json" JSON
"text" 字串
  • responseXML: Document - 本次請求響應式一個 Document 物件。
  • status: unsigned short - 請求的響應狀態碼,如 200 (表示一個成功的請求)。 (只讀)
  • statusText: DOMString - 請求的響應狀態資訊,包含一個狀態碼和訊息文字,如 "200 OK"。 (只讀)
  • withCredentials: boolean - 表明在進行跨站 (cross-site) 的訪問控制 (Access-Control) 請求時,是否使用認證資訊 (例如cookie或授權的header)。預設為 false。注意:這不會影響同站 same-site 請求

方法

  • abort() - 如果請求已經被髮送,則立刻中止請求。
  • getAllResponseHeaders() - 返回所有響應頭資訊(響應頭名和值),如果響應頭還沒有接收,則返回 null。注意:使用該方法獲取的 response headers 與在開發者工具 Network 皮膚中看到的響應頭不一致
  • getResponseHeader() - 返回指定響應頭的值,如果響應頭還沒有被接收,或該響應頭不存在,則返回 null。
  • open() - 初始化一個請求:
void open(
   DOMString method,
   DOMString url,
   optional boolean async,
   optional DOMString user,
   optional DOMString password
);複製程式碼
  • overrideMimeType() - 重寫由伺服器返回的 MIME 型別。
  • send() - 傳送請求。如果該請求是非同步模式(預設),該方法會立刻返回。相反,如果請求是同步模式,則直到請求的響應完全接受以後,該方法才會返回。
void send();
void send(ArrayBuffer data);
void send(Blob data);
void send(Document data);
void send(DOMString? data);
void send(FormData data);複製程式碼
  • setRequestHeader() - 設定 HTTP 請求頭資訊。注意:在這之前,你必須確認已經呼叫了 open() 方法開啟了一個 url
void setRequestHeader(
   DOMString header,
   DOMString value
);複製程式碼

XMLHttpRequest 示例

var xhr = new XMLHttpRequest(); // 建立xhr物件
xhr.open( method, url ); // 設定請求方法和URL
xhr.onreadystatechange = function () { ... }; // 監聽請求狀態變化
xhr.setRequestHeader( ..., ... ); // 設定請求頭資訊
xhr.send( optionalEncodedData ); // 設定請求體併傳送請求複製程式碼

HttpModule

Angular Orgs Members - Http 示例

app.component.ts

import { Component, OnInit } from '@angular/core';
import { Http } from '@angular/http'; // (1)
import 'rxjs/add/operator/map'; // (2)

interface Member {
  id: string;
  login: string;
  avatar_url: string;
}

@Component({
  selector: 'exe-app',
  template: `
    <h3>Angular Orgs Members</h3>
    <ul *ngIf="members">
      <li *ngFor="let member of members;">
        <p>
          <img [src]="member.avatar_url" width="48" height="48"/>
          ID:<span>{{member.id}}</span>
          Name: <span>{{member.login}}</span>
        </p>
      </li>
    </ul>
  `
})
export class AppComponent implements OnInit {
  members: Member[];

  constructor(private http: Http) { } // (3)

  ngOnInit() {
    this.http.get(`https://api.github.com/orgs/angular/members?page=1&per_page=5`) // (4)
      .map(res => res.json()) // (5)
      .subscribe(data => {
        if (data) this.members = data; // (6)
      });
  }
}複製程式碼

示例說明:

(1) 從 @angular/http 模組中匯入 Http 類

(2) 匯入 RxJS 中的 map 操作符

(3) 使用 DI 方式注入 http 服務

(4) 呼叫 http 服務的 get() 方法,設定請求地址併傳送 HTTP 請求

(5) 呼叫 Response 物件的 json() 方法,把響應體轉成 JSON 物件

(6) 把請求的結果,賦值給 members 屬性

是不是感覺上面的示例太簡單了,請深吸一口氣,我們再來看一下如果沒有使用 HttpModule ,應該如何實現上述的功能。

Angular Orgs Members - XMLHttpRequest 示例

app.component.ts

import { Component, OnInit } from '@angular/core';

interface Member {
  id: string;
  login: string;
  avatar_url: string;
}

@Component({
  selector: 'exe-app',
  template: `
    <h3>Angular Orgs Members</h3>
    <ul *ngIf="members">
      <li *ngFor="let member of members;">
        <p>
          <img [src]="member.avatar_url" width="48" height="48"/>
          ID:<span>{{member.id}}</span>
          Name: <span>{{member.login}}</span>
        </p>
      </li>
    </ul>
  `
})
export class AppComponent implements OnInit {
  members: Member[];

  getMembers() {
    let MEMBERS_URL = `https://api.github.com/orgs/angular/members?page=1&per_page=5`;
    let xhr = new XMLHttpRequest(); // (1)
    xhr.open("GET", MEMBERS_URL); // (2)
    xhr.onreadystatechange = () => { // (3)
      if (xhr.readyState == 4 && xhr.status == 200) { // (4)
        if (xhr.responseText) {
          try {
            this.members = JSON.parse(xhr.responseText); // (5)
          } catch (error) {
            throw error;
          }
        }
      }
    };
    xhr.send(null); // (6)
  }

  ngOnInit() {
    this.getMembers();
  }
}複製程式碼

示例說明:

(1) 建立 XMLHttpRequest 物件

(2) 設定請求方式和請求 URL 地址

(3) 監聽 readyState 狀態變化

(4) 判斷請求是否完成且請求成功

(5) 把響應體轉換為 JSON 物件,並賦值給 members 屬性

(6) 傳送 HTTP 請求

雖然使用 XMLHttpRequest API 我們也實現了同樣的功能,但使用 HttpModule 給我們帶來的好處,一目瞭然。其實 HttpModule 底層實現也是基於 XMLHttpRequest API,只是它對 XMLHttpRequest API 進行了封裝,抽象出了 Body、Request、Headers 和 Response 等物件。

HttpModule

請求與響應

HTTP 協議是基於請求與響應,通過 XMLHttpRequest API,我們可以方便的傳送 HTTP 請求。相信很多讀者已經用過了以下一款或多款 FiddlerPaw (macOS)PostmanAdvanced REST client HTTP 客戶端,通過它們我們也可以方便的傳送 HTTP 請求。其實不管是使用上面的那些 HTTP 客戶端還是使用 XMLHttpRequest API,我們最終都是要構造 HTTP 請求報文,然後向伺服器傳送 HTTP 請求,接著我們就需要接收和解析伺服器返回的 HTTP 響應報文,最後根據不同的響應型別解析響應體,進而進行頁面渲染。

接下來我們來分析一下前面 Angular Orgs Members - Http 示例中的程式碼:

export class AppComponent implements OnInit {
  members: Member[];

  constructor(private http: Http) { } 
  ngOnInit() {
    this.http.get(`https://api.github.com/orgs/angular/members?page=1&per_page=5`) 
      .map(res => res.json()) 
      .subscribe(data => {
        if (data) this.members = data; 
      });
  }
}複製程式碼

首先我們先來分析一下通過構造注入方式,注入的 Http 物件:

constructor(private http: Http) { }複製程式碼

如何建立 Http 物件

angular2/packages/http/src/http_module.ts

@NgModule({
  providers: [
    {provide: Http, useFactory: httpFactory, deps: [XHRBackend, RequestOptions]},
    BrowserXhr,
    {provide: RequestOptions, useClass: BaseRequestOptions},
    {provide: ResponseOptions, useClass: BaseResponseOptions},
    XHRBackend,
    {provide: XSRFStrategy, useFactory: _createDefaultCookieXSRFStrategy},
  ],
})
export class HttpModule { }複製程式碼

httpFactory 工廠函式

export function httpFactory(
  xhrBackend: XHRBackend, 
  requestOptions: RequestOptions): Http {
       return new Http(xhrBackend, requestOptions); // 建立Http物件
}複製程式碼

Http 類建構函式

// angular2/packages/http/src/http.ts 片段
@Injectable()
export class Http {
    constructor(protected _backend: ConnectionBackend, 
      protected _defaultOptions: RequestOptions) {}
}複製程式碼

建立 Http 物件

1.建立 XHRBackend 物件

2.建立 RequestOptions 物件

如何建立 XHRBackend 物件

angular2/packages/http/src/http_module.ts

@NgModule({
  providers: [
    ...,
    BrowserXhr, // 等價於 {provide: BrowserXhr, useClass: BrowserXhr}
    {provide: ResponseOptions, useClass: BaseResponseOptions},
    XHRBackend, // 等價於 {provide: XHRBackend, useClass: XHRBackend}
    {provide: XSRFStrategy, useFactory: _createDefaultCookieXSRFStrategy},
  ],
})
export class HttpModule { }複製程式碼

XHRBackend 類

// angular2/packages/http/src/backends/xhr_backend.ts 片段
@Injectable()
export class XHRBackend implements ConnectionBackend {
  constructor(
      private _browserXHR: BrowserXhr, 
      private _baseResponseOptions: ResponseOptions,
      private _xsrfStrategy: XSRFStrategy) {}
}複製程式碼

ConnectionBackend 抽象類

export abstract class ConnectionBackend {
    abstract createConnection(request: any): Connection;  // 用於建立連線
}複製程式碼

(備註:該抽象類中包含了抽象方法,不能直接用於例項化)

Connection 抽象類

export abstract class Connection {
  readyState: ReadyState; // 請求狀態
  request: Request; // 請求物件 
  response: any; // 響應物件
}複製程式碼

建立 XHRBackend 物件

1.建立 BrowserXhr 物件

2.建立 BaseResponseOptions 物件

3.建立 XSRFStrategy 物件

如何建立 BrowserXhr 物件

angular2/packages/http/src/http_module.ts

@NgModule({
  providers: [
    ...,
    BrowserXhr, // 等價於 {provide: BrowserXhr, useClass: BrowserXhr}
  ],
})
export class HttpModule { }複製程式碼

BrowserXhr 類

@Injectable()
export class BrowserXhr {
  constructor() {}
  build(): any { return <any>(new XMLHttpRequest()); }
}複製程式碼

如何建立 ResponseOptions 物件

angular2/packages/http/src/http_module.ts

@NgModule({
  providers: [
    ...,
    {provide: ResponseOptions, useClass: BaseResponseOptions},
    ...
  ],
})
export class HttpModule { }複製程式碼

BaseResponseOptions 類

@Injectable()
export class BaseResponseOptions extends ResponseOptions {
  constructor() {
    super({status: 200, statusText: 'Ok', type: ResponseType.Default, 
           headers: new Headers()});
  }
}複製程式碼

ResponseOptions 類

export class ResponseOptions {
  body: string|Object|ArrayBuffer|Blob; // 響應體的型別
  status: number; // 請求的響應狀態碼
  headers: Headers; // 請求頭
  statusText: string; // 請求的響應狀態資訊
  type: ResponseType; // 響應型別:Basic|Cors|Default|Error|Opaque
  url: string; // 響應的URL

  constructor({body, status, headers, statusText, type, url}: ResponseOptionsArgs = {}) {
    this.body = body != null ? body : null;
    this.status = status != null ? status : null;
    this.headers = headers != null ? headers : null;
    this.statusText = statusText != null ? statusText : null;
    this.type = type != null ? type : null;
    this.url = url != null ? url : null;
  }

 // 合併響應引數
 merge(options?: ResponseOptionsArgs): ResponseOptions {
    return new ResponseOptions({
      body: options && options.body != null ? options.body : this.body,
      status: options && options.status != null ? options.status : this.status,
      headers: options && options.headers != null ? options.headers : this.headers,
      statusText: options && options.statusText != null ? 
       options.statusText : this.statusText,
      type: options && options.type != null ? options.type : this.type,
      url: options && options.url != null ? options.url : this.url,
    });
  }
}

// 使用示例
import {ResponseOptions, Response} from '@angular/http';

var options = new ResponseOptions({
    body: '{"name":"Jeff"}'
});
var res = new Response(options.merge({
   url: 'https://google.com'
}));

console.log('options.url:', options.url); // null
console.log('res.json():', res.json()); // Object {name: "Jeff"}
console.log('res.url:', res.url); // https://google.com複製程式碼

如何建立 XSRFStrategy 物件

angular2/packages/http/src/http_module.ts

@NgModule({
  providers: [
    ...,
    {provide: XSRFStrategy, useFactory: _createDefaultCookieXSRFStrategy},
  ],
})
export class HttpModule { }複製程式碼

_createDefaultCookieXSRFStrategy 函式

// 建立基於Cookie的防止XSRF(Cross Site Request Forgery - 跨域請求偽造)的策略
export function _createDefaultCookieXSRFStrategy() {
  return new CookieXSRFStrategy();
}複製程式碼

CookieXSRFStrategy 類

// https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
export class CookieXSRFStrategy implements XSRFStrategy {
  constructor(
      private _cookieName: string = 'XSRF-TOKEN', 
      private _headerName: string = 'X-XSRF-TOKEN') {}

  // 配置請求物件
  configureRequest(req: Request): void {
    // 從Cookie中獲取_cookieName對應的xsrfToken值
    const xsrfToken = getDOM().getCookie(this._cookieName);
    if (xsrfToken) {
      // 請求頭新增_headerName請求頭,key為_headerName,value為xsrfToken
      req.headers.set(this._headerName, xsrfToken);
    }
  }
}複製程式碼

XSRFStrategy 抽象類

export abstract class XSRFStrategy { 
  abstract configureRequest(req: Request): void; 
}複製程式碼

如何建立 RequestOptions 物件

angular2/packages/http/src/http_module.ts

@NgModule({
  providers: [
    ...,
    {provide: RequestOptions, useClass: BaseRequestOptions},
    ...,
  ],
})
export class HttpModule { }複製程式碼

BaseRequestOptions 類

@Injectable()
export class BaseRequestOptions extends RequestOptions {
  constructor() { super({method: RequestMethod.Get, headers: new Headers()}); }
}

// 使用示例
import {BaseRequestOptions, Request, RequestMethod} from '@angular/http';

const options = new BaseRequestOptions();
const req = new Request(options.merge({
  method: RequestMethod.Post,
  url: 'https://google.com'
}));
console.log('req.method:', RequestMethod[req.method]); // Post
console.log('options.url:', options.url); // null
console.log('req.url:', req.url); // https://google.com複製程式碼

RequestOptions 類

// angular2/packages/http/src/base_request_options.ts 片段
export class RequestOptions {
  method: RequestMethod|string; // 請求方法
  headers: Headers; // 請求頭
  body: any; // 請求體
  url: string; // 請求URL
  params: URLSearchParams; // 引數
  // @deprecated from 4.0.0. Use params instead.
  get search(): URLSearchParams { return this.params; }
  // @deprecated from 4.0.0. Use params instead.
  set search(params: URLSearchParams) { this.params = params; }
  // 表明在進行跨站 (cross-site) 的訪問控制請求時,是否使用認證資訊(例如cookie或授權的header)。
  withCredentials: boolean; 
  responseType: ResponseContentType;// 響應型別,就是告訴伺服器你期望的響應格式

  constructor(
    {method, headers, body, url, search, params, withCredentials,
       responseType}: RequestOptionsArgs = {}) {
    this.method = method != null ? normalizeMethodName(method) : null;
    this.headers = headers != null ? headers : null;
    this.body = body != null ? body : null;
    this.url = url != null ? url : null;
    this.params = this._mergeSearchParams(params || search);
    this.withCredentials = withCredentials != null ? withCredentials : null;
    this.responseType = responseType != null ? responseType : null;
  }

 // 合併請求引數
  merge(options?: RequestOptionsArgs): RequestOptions {
    return new RequestOptions({
      method: options && options.method != null ? options.method : this.method,
      headers: options && options.headers != null ? options.headers 
        : new Headers(this.headers),
      body: options && options.body != null ? options.body : this.body,
      url: options && options.url != null ? options.url : this.url,
      params: options && this._mergeSearchParams(options.params || options.search),
      withCredentials: options && options.withCredentials != null ?     
        options.withCredentials : this.withCredentials,
      responseType: options && options.responseType != null ? 
        options.responseType : this.responseType
    });
}

// 使用示例
import {RequestOptions, Request, RequestMethod} from '@angular/http';

const options = new RequestOptions({
  method: RequestMethod.Post
});
const req = new Request(options.merge({
  url: 'https://google.com'
}));
console.log('req.method:', RequestMethod[req.method]); // Post
console.log('options.url:', options.url); // null
console.log('req.url:', req.url); // https://google.com複製程式碼

接下來,我們來分析一下 AppComponent 中 ngOnInit() 鉤子方法中的程式碼:

ngOnInit() {
  this.http.get(`https://api.github.com/orgs/angular/members?page=1&per_page=5`) 
      .map(res => res.json()) 
      .subscribe(data => {
        if (data) this.members = data; 
  });
}複製程式碼

如何傳送 GET 請求

this.http.get(`https://api.github.com/orgs/angular/members?page=1&per_page=5`)複製程式碼

Http 類

// angular2/packages/http/src/http.ts 片段
@Injectable()
export class Http {
      // 建構函式
    constructor(protected _backend: ConnectionBackend, 
      protected _defaultOptions: RequestOptions) {}

    // 傳送任意型別的請求,返回Observable<Response>物件
    request(url: string|Request, options?: RequestOptionsArgs): Observable<Response> {
      let responseObservable: any;
      if (typeof url === 'string') {
        responseObservable = httpRequest(
            this._backend,
            new Request(mergeOptions(this._defaultOptions, options, 
                RequestMethod.Get, <string>url)));
      } else if (url instanceof Request) {
        responseObservable = httpRequest(this._backend, url);
      } else {
        throw new Error('First argument must be a url string or Request instance.');
      }
      return responseObservable;
    }

    // 傳送GET請求
    get(url: string, options?: RequestOptionsArgs): Observable<Response> {
        return this.request(
            new Request(mergeOptions(this._defaultOptions, options, 
                RequestMethod.Get, url)));
    }

    // 傳送POST請求
    post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
        return this.request(new Request(mergeOptions(
            this._defaultOptions.merge(new RequestOptions({body: body})), options, 
                   RequestMethod.Post, url)));
    }
    ... 
}複製程式碼

傳送 GET 請求

/**
 * url: 請求地址
 * options: 可選的請求引數
 */
get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.request(
            new Request(mergeOptions(this._defaultOptions, options, 
                RequestMethod.Get, url)));
}複製程式碼

this.http.get('remoteUrl') 方法執行主要過程:

this.get('https://api.github.com/orgs/angular/members?page=1&per_page=5') 
   this.request(new Request(mergeOptions(...,options,RequestMethod.Get, url))
     httpRequest(this._backend, new Request(...))
        backend.createConnection(request)複製程式碼

request() 方法

// 傳送請求
request(url: string|Request, options?: RequestOptionsArgs): Observable<Response> {
      let responseObservable: any;
      if (typeof url === 'string') { // url型別是字串
        responseObservable = httpRequest( // 呼叫httpRequest() 方法
            this._backend, // ConnectionBackend 物件
            new Request(mergeOptions(this._defaultOptions, // 建立Request物件
                  options, RequestMethod.Get, <string>url)));
      } else if (url instanceof Request) { // 若url是Request物件的例項
        responseObservable = httpRequest(this._backend, url);
      } else {
        throw new Error('First argument must be a url string or Request instance.');
      }
      return responseObservable; // 返回Observable物件
}複製程式碼

httpRequest() 方法

function httpRequest(backend: ConnectionBackend, request: Request): 
  Observable<Response> {
  return backend.createConnection(request).response;
}複製程式碼

前面我們已經分析了 ConnectionBackend 物件,接下來我們來分析一下 Request 物件。

如何建立 Request 物件

new Request({
  method: RequestMethod.Get,
  url: 'https://google.com'
});複製程式碼

Request 類

// angular2/packages/http/src/static_request.ts 片段
export class Request extends Body {
  method: RequestMethod; // 請求方法
  headers: Headers; // 請求頭
  url: string; // 請求URL地址
  private contentType: ContentType; // 請求體的型別
  withCredentials: boolean; // 是否開啟withCredentials(不會影響same-site請求)
  responseType: ResponseContentType; // 設定該值能夠改變響應型別,就是告訴伺服器你期望的響應格式

  constructor(requestOptions: RequestArgs) {
    super();
    const url = requestOptions.url;
    this.url = requestOptions.url;
    if (requestOptions.params) { // 處理請求引數
      const params = requestOptions.params.toString();
      if (params.length > 0) {
        let prefix = '?';
        if (this.url.indexOf('?') != -1) { // 判斷url是否已包含?字元
          prefix = (this.url[this.url.length - 1] == '&') ? '' : '&';
        }
        // TODO: just delete search-query-looking string in url?
        this.url = url + prefix + params;
      }
    }
    this._body = requestOptions.body; // 設定請求體
    this.method = normalizeMethodName(requestOptions.method); // 標準化請求方法
    this.headers = new Headers(requestOptions.headers); // 設定請求頭
    this.contentType = this.detectContentType(); 
    this.withCredentials = requestOptions.withCredentials;
    this.responseType = requestOptions.responseType;
  }
}複製程式碼

Body 類

// angular2/packages/http/src/body.ts 片段
export abstract class Body {
  protected _body: any;

  json(): any { // 轉化為JSON物件 - 具體應用:map(res => res.json())
    if (typeof this._body === 'string') {
      return JSON.parse(<string>this._body);
    }
    if (this._body instanceof ArrayBuffer) {
      return JSON.parse(this.text());
    }
    return this._body;
  }

  // 轉換為Text文字
  text(): string { ... }

  // 轉換為ArrayBuffer物件
  arrayBuffer(): ArrayBuffer { ... }

  // 轉換為Blob物件
  blob(): Blob { ... }
}複製程式碼

分析完如何建立請求物件,我們馬上要進入最核心的部分,如何建立連線傳送請求及建立響應物件

如何建立連線

backend.createConnection(request)複製程式碼

httpRequest() 方法

function httpRequest(backend: ConnectionBackend, request: Request): Observable<Response> {
  return backend.createConnection(request).response; // 建立連線
}複製程式碼

XHRBackend 類

@Injectable()
export class XHRBackend implements ConnectionBackend {
  constructor(
      private _browserXHR: BrowserXhr, private _baseResponseOptions: ResponseOptions,
      private _xsrfStrategy: XSRFStrategy) {}

  // 用於建立XHRConnection,此外還有JSONPConnection
  createConnection(request: Request): XHRConnection {
    this._xsrfStrategy.configureRequest(request);
    return new XHRConnection(request, this._browserXHR, this._baseResponseOptions);
  }
}複製程式碼

如何建立 XHRConnection 物件

new XHRConnection(request, this._browserXHR, this._baseResponseOptions);複製程式碼

XHRConnection 類

// angular2/packages/http/src/backends/xhr_backend.ts 完整程式碼
export class XHRConnection implements Connection {
  request: Request; // 請求物件
  response: Observable<Response>; // 響應的Observable物件
  readyState: ReadyState; // 請求狀態

  constructor(req: Request, browserXHR: BrowserXhr, baseResponseOptions?: ResponseOptions) {
    this.request = req;
    // 建立響應的Observable物件
    this.response = new Observable<Response>(
      responseObserver: Observer<Response>) => {

      // build(): any { return <any>(new XMLHttpRequest()); }
      // 建立XMLHttpRequest物件
      const _xhr: XMLHttpRequest = browserXHR.build();

      // void open( DOMString method, DOMString url, optional boolean async,...);
      _xhr.open(RequestMethod[req.method].toUpperCase(), req.url);
      if (req.withCredentials != null) { // 是否開啟withCredentials
        _xhr.withCredentials = req.withCredentials;
      }

      // load event handler
      // 請求成功處理函式
      const onLoad = () => {
        // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
        // 獲取xhr狀態,需處理IE9下的bug
        let status: number = _xhr.status === 1223 ? 204 : _xhr.status;

        let body: any = null;

        // HTTP 204 means no content
        // HTTP 204 表示沒有內容,即不用處理響應體
        if (status !== 204) {
          // responseText is the old-school way of retrieving response 
          // (supported by IE8 & 9)
          // response/responseType properties were introduced in 
          // ResourceLoader Level2 spec
          // (supported by IE10)

          /**獲取響應體方式:
           * 1. responseText 相容IE8與IE9
           * 2. response/responseType XMLHttpRequest Level 2 規範中引入,IE10支援
           */
          body = (typeof _xhr.response === 'undefined') ? 
                  _xhr.responseText : _xhr.response;

          // Implicitly strip a potential XSSI prefix.
          if (typeof body === 'string') {
            body = body.replace(XSSI_PREFIX, '');
          }
        }

        // fix status code when it is 0 (0 status is undocumented).
        // Occurs when accessing file resources or on Android 4.1 stock browser
        // while retrieving files from application cache.

        /**
        * 當訪問本地檔案資源或在 Android 4.1 stock browser 中從應用快取中獲取檔案時,        
        * XMLHttpRequest 的 status 值也會為0。因此要對返回的狀態碼做處理。
        */
        if (status === 0) {
          status = body ? 200 : 0;
        }

        // 解析響應頭,建立Headers物件
        // 注意:使用該方法獲取的響應頭與在開發者工具Network皮膚中看到的響應頭不一致
        const headers: Headers = Headers.
            fromResponseHeaderString(_xhr.getAllResponseHeaders());
        // IE 9 does not provide the way to get URL of response
        // IE 9 沒有提供獲取響應URL的方式
        const url = getResponseURL(_xhr) || req.url;
        // 設定狀態碼
        const statusText: string = _xhr.statusText || 'OK';

        // 建立ResponseOptions物件
        let responseOptions = new ResponseOptions({body, status, 
            headers, statusText, url});
        if (baseResponseOptions != null) {
          responseOptions = baseResponseOptions.merge(responseOptions);
        }

        // 建立響應物件
        const response = new Response(responseOptions);

        // const isSuccess = (status: number): boolean => (status >= 200 && status < 300);
        response.ok = isSuccess(status);
        if (response.ok) {
          responseObserver.next(response); // 請求成功,呼叫next()方法,傳遞響應物件
          // TODO(gdi2290): defer complete if array buffer until done
          responseObserver.complete();
          return;
        }
        responseObserver.error(response); // 發生異常,呼叫error()方法,傳遞響應物件
      };

      // error event handler
      // 異常處理函式
      const onError = (err: ErrorEvent) => {
        let responseOptions = new ResponseOptions({
          body: err,
          type: ResponseType.Error,
          status: _xhr.status,
          statusText: _xhr.statusText,
        });
        if (baseResponseOptions != null) {
          responseOptions = baseResponseOptions.merge(responseOptions);
        }
        responseObserver.error(new Response(responseOptions));
      };

      // 根據 req.contentType 型別,設定請求頭content-type資訊
      this.setDetectedContentType(req, _xhr); 

      if (req.headers == null) { // 建立headers物件
        req.headers = new Headers();
      }
      if (!req.headers.has('Accept')) { // 若設定Accept請求頭,則設定預設的值
        req.headers.append('Accept', 'application/json, text/plain, */*');
      }
      req.headers.forEach((values, name) => 
          _xhr.setRequestHeader(name, values.join(',')));

      // Select the correct buffer type to store the response
      // 根據req.responseType型別設定xhr.responseType
      if (req.responseType != null && _xhr.responseType != null) {
        switch (req.responseType) {
          case ResponseContentType.ArrayBuffer:
            _xhr.responseType = 'arraybuffer';
            break;
          case ResponseContentType.Json:
            _xhr.responseType = 'json';
            break;
          case ResponseContentType.Text:
            _xhr.responseType = 'text';
            break;
          case ResponseContentType.Blob:
            _xhr.responseType = 'blob';
            break;
          default:
            throw new Error('The selected responseType is not supported');
        }
      }

      // 當資源完成載入時,將觸發load事件
      _xhr.addEventListener('load', onLoad); 
      // 當資源載入失敗時,將處罰error事件
      _xhr.addEventListener('error', onError);

      // 傳送請求
      // void send();
      // void send(ArrayBuffer data);
      // void send(Blob data);
      // void send(Document data);
      // void send(DOMString? data);
      // void send(FormData data);
      _xhr.send(this.request.getBody()); 

      // 返回函式物件,用於移除事件監聽及終止請求
      return () => {
        _xhr.removeEventListener('load', onLoad);
        _xhr.removeEventListener('error', onError);
        _xhr.abort();
      };
    });
  }

  setDetectedContentType(req: any /** TODO Request */, _xhr: any /** XMLHttpRequest */) {
    // Skip if a custom Content-Type header is provided
    if (req.headers != null && req.headers.get('Content-Type') != null) {
      return;
    }

    // Set the detected content type
    switch (req.contentType) {
      case ContentType.NONE:
        break;
      case ContentType.JSON:
        _xhr.setRequestHeader('content-type', 'application/json');
        break;
      case ContentType.FORM:
        _xhr.setRequestHeader('content-type', 
            'application/x-www-form-urlencoded;charset=UTF-8');
        break;
      case ContentType.TEXT:
        _xhr.setRequestHeader('content-type', 'text/plain');
        break;
      case ContentType.BLOB:
        const blob = req.blob();
        if (blob.type) {
          _xhr.setRequestHeader('content-type', blob.type);
        }
        break;
    }
  }
}複製程式碼

是不是有點暈了,我們趕緊來梳理一下建立 XHRConnection 物件的內部流程:

呼叫 XHRConnection 建構函式,建立 XHRConnection 物件

constructor(req: Request, browserXHR: BrowserXhr, 
   baseResponseOptions?: ResponseOptions) { ... }複製程式碼
  • 設定請求物件
  • 設定Observable響應物件 - new Observable((responseObserver: Observer) => { … })

是時候分析以下程式碼的執行過程:

ngOnInit() {
  this.http.get(`https://api.github.com/orgs/angular/members?
    page=1&per_page=5`) // (1)
      .map(res => res.json()) // (2)
      .subscribe(data => { // (3)
        if (data) this.members = data; 
  });
}複製程式碼

1.呼叫 Http 物件的 get() 方法

get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.request(
        new Request(mergeOptions(this._defaultOptions, options, RequestMethod.Get, url)));
}

request(url: string|Request, options?: RequestOptionsArgs): Observable<Response> {
    let responseObservable: any;
    if (typeof url === 'string') {
      responseObservable = httpRequest(this._backend,new Request(...);
    } else if (url instanceof Request) {
      responseObservable = httpRequest(this._backend, url);
    } 
    ...
    return responseObservable;
  }複製程式碼

2.呼叫 httpRequest() 方法,返回 Observable<Response> 物件

function httpRequest(backend: ConnectionBackend, request: Request): 
  Observable<Response> {
  return backend.createConnection(request).response;
}複製程式碼

3.呼叫 RxJS 中的 map() 操作符,對響應 Response 物件進行處理,即轉換為 JSON 物件

public map(project: function(value: T, index: number): R, thisArg: any): Observable<R>複製程式碼

4.訂閱返回的 Observable<Response> 物件,即正式傳送 HTTP 請求

5.建立 XMLHttpRequest 物件 — _xhr

  • 呼叫 _xhr 物件的 open() 方法,設定請求方法和請求地址
  • 監聽 _xhr load 事件,設定 onLoad 處理函式,onLoad 函式內部處理流程:
    • 設定 status、statusText 值
    • 獲取 HTTP 響應體:_xhr.responseText (IE 8 & IE 9) 或 _xhr.response (IE 10)
    • 解析響應頭建立 Headers 物件:Headers.fromResponseHeaderString(_xhr.getAllResponseHeaders())
    • 基於 status、status、headers、body 等資訊建立響應物件
    • 通知觀察者 (根據請求狀態,呼叫觀察者的 next 或 error 方法)
  • 監聽 _xhr error 事件,設定 onError 處理函式
  • 返回一個用於移除監聽(load、error事件)和終止 HTTP 請求的函式

Angular HttpModule 中核心的內容,我們已經分析完了,最後在補充一下如何建立 Response 響應物件。

如何建立 Response 物件

new Response({ body: '{"name":"Jeff"}', url: 'https://google.com' })複製程式碼

Response 類

export class Response extends Body {
  type: ResponseType; // "basic", "cors", "default", "error", or "opaque",預設"default"
  ok: boolean; // 當status在200-299範圍內,該值為true
  url: string; // 響應的URL地址,預設為空字串
  status: number; // 伺服器返回的狀態,預設為200
  statusText: string; // 請求的響應狀態資訊,預設值是"OK"
  bytesLoaded: number; // 非標準屬性:用於表示已載入響應體的位元組數
  totalBytes: number; // 非標準屬性:表示響應體總位元組數
  headers: Headers; // 響應頭物件

  constructor(responseOptions: ResponseOptions) {
    super();
    this._body = responseOptions.body;
    this.status = responseOptions.status;
    this.ok = (this.status >= 200 && this.status <= 299);
    this.statusText = responseOptions.statusText;
    this.headers = responseOptions.headers;
    this.type = responseOptions.type;
    this.url = responseOptions.url;
  }

  toString(): string {
    return `Response with status: ${this.status} ${this.statusText} for URL: ${this.url}`;
  }
}複製程式碼

總結

Angular HttpModule 模組的核心功能,終於分析完了。最後我們來總結一下:

  • 當呼叫 Http 物件的 get()post()put() 等方法時,會返回一個 Observable<Response> 物件,僅當我們訂閱該 Observable 物件時,才會正式發起 HTTP 請求。
  • Angular 內部使用 Request 和 Response 物件來封裝請求資訊和響應資訊。Request 類和 Response 類都是繼承於 Body 類,Body 類中提供了四個方法用於資料轉換:
    • json(): any - 轉換為 JSON 物件
    • text(): string -
    • arrayBuffer(): ArrayBuffer - 轉換為 ArrayBuffer 物件
    • blob(): Blob - 轉化為 Blob 物件
  • 訂閱 Observable<Response> 物件後,返回一個函式物件。呼叫該函式物件,我們可以移除 loaderror 事件監聽及取消 HTTP 請求。

相關文章