Angular 4.x HttpModule 揭祕

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

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

爲了寫好這篇文章 (寫得很差的話,你們請見諒),考慮了一番,最終仍是打算先寫相關的基礎文章:html

其中 HTTP 最強資料大全你不知道的 XMLHttpRequest 內容比較全面,但對於咱們揭祕 Angular 4.x HttpModule 模塊,咱們只需瞭解其中的一些相關知識便可。所以下面我也僅會介紹相關的知識點,若想了解詳細信息,你們能夠查看原文。java

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

HTTP 協議

超文本傳輸協議英文HyperText Transfer Protocol縮寫HTTP)是互聯網上應用最爲普遍的一種網絡協議。設計HTTP最初的目的是爲了提供一種發佈和接收HTML頁面的方法。經過HTTP或者HTTPS協議請求的資源由統一資源標識符(Uniform Resource Identifiers,URI)來標識。—— 維基百科git

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

HTTP 請求報文

HTTP 請求報文由請求行請求頭空行請求體(請求數據) 4 個部分組成,以下圖所示:web

請求報文示例

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 個部分組成,以下圖所示:typescript

響應報文示例

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 中被大量使用。shell

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 請求。
相關文章
相關標籤/搜索