Angular 4.x HttpModule 揭秘

更新时间 - 2017-04-20
更新内容 - Connection致命逻辑错误,感谢 深圳-shadows 刊误

有点小鸡冻,我们 HttpModule 系列的主角终于要出场了。此时突然想起了一句诗:

千呼万唤始出来,犹抱琵琶半遮面。 —— 白居易 <<琵琶行>>

为了写好这篇文章 (写得不好的话,大家请见谅),考虑了一番,最终还是打算先写相关的基础文章:

其中 HTTP 最强资料大全你不知道的 XMLHttpRequest 内容比较全面,但对于我们揭秘 Angular 4.x HttpModule 模块,我们只需了解其中的一些相关知识即可。因此下面我也仅会介绍相关的知识点,若想了解详细信息,大家可以查看原文。

直接略过基础部分,直达 HttpModule

HTTP 协议

超文本传输协议英文HyperText Transfer Protocol缩写HTTP)是互联网上应用最为广泛的一种网络协议。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。通过HTTP或者HTTPS协议请求的资源由统一资源标识符(Uniform Resource Identifiers,URI)来标识。—— 维基百科

HTTP 协议是基于请求与响应,具体如下图所示:

HTTP 请求报文

HTTP 请求报文由请求行请求头空行请求体(请求数据) 4 个部分组成,如下图所示:

请求报文示例

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 个部分组成,如下图所示:

响应报文示例

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: [
    ...,// 等价于 {provide: BrowserXhr,useClass: BrowserXhr}
    {provide: ResponseOptions,// 等价于 {provide: XHRBackend,useClass: XHRBackend}
    {provide: XSRFStrategy,})
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: [
    ...,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: [
    ...,...
  ],})
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: [
    ...,})
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: [
    ...,...,})
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,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,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,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:',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,url)));
    }
  
    // 发送POST请求
    post(url: string,body: any,options?: RequestOptionsArgs): Observable<Response> {
        return this.request(new Request(mergeOptions(
            this._defaultOptions.merge(new RequestOptions({body: body})),RequestMethod.Post,url)));
    }
    ... 
}

发送 GET 请求

/**
 * url: 请求地址
 * options: 可选的请求参数
 */
get(url: string,options?: RequestOptionsArgs): Observable<Response> {
    return this.request(
            new Request(mergeOptions(this._defaultOptions,url)));
}

this.http.get('remoteUrl') 方法执行主要过程:

this.get('https://api.github.com/orgs/angular/members?page=1&per_page=5') 
   this.request(new Request(mergeOptions(...,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,<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 _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._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,...);
      _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,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,baseResponseOptions?: ResponseOptions) { ... }
  • 设置请求对象

  • 设置Observable响应对象 - new Observable<Response>((responseObserver: Observer<Response>) => { … })

是时候分析以下代码的执行过程:

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,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 请求。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


ANGULAR.JS:NG-SELECTANDNG-OPTIONSPS:其实看英文文档比看中文文档更容易理解,前提是你的英语基础还可以。英文文档对于知识点讲述简明扼要,通俗易懂,而有些中文文档读起来特别费力,基础差、底子薄的有可能一会就会被绕晕了,最起码英文文档中的代码与中文文档中的代码是一致的,但知识点讲述实在是差距太大。Angular.jshasapowerfuldire
AngularJS中使用Chart.js制折线图与饼图实例  Chart.js 是一个令人印象深刻的 JavaScript 图表库,建立在 HTML5 Canvas 基础上。目前,它支持6种图表类型(折线图,条形图,雷达图,饼图,柱状图和极地区域区)。而且,这是一个独立的包,不依赖第三方 JavaScript 库,小于 5KB。   其中用到的软件:   Chart.js框架,版本1.0.2,一
IE浏览器兼容性后续前言 继续尝试解决IE浏览器兼容性问题,结局方案为更换jquery、angularjs、IE的版本。 1.首先尝试更换jquery版本为1.7.2 jquery-1.9.1.js-->jquery-1.7.2.js--> jquery2.1.4.js 无效 2.尝试更换IE版本IE8 IE11-
Angular实现下拉菜单多选写这篇文章时,引用文章地址如下:http://ngmodules.org/modules/angularjs-dropdown-multiselecthttp://dotansimha.github.io/angularjs-dropdown-multiselect/#/AngularJSDropdownMultiselectThisdire
在AngularJS应用中集成科大讯飞语音输入功能前言 根据项目需求,需要在首页搜索框中添加语音输入功能,考虑到科大讯飞语音业务的强大能力,遂决定使用科大讯飞语音输入第三方服务。软件首页截图如下所示: 涉及的源代码如下所示: //语音识别$rootScope.startRecognize = function() {var speech;
Angular数据更新不及时问题探讨前言 在修复控制角标正确变化过程中,发觉前端代码组织层次出现了严重问题。传递和共享数据时自己使用的是rootScope,为此造成了全局变量空间的污染。根据《AngularJs深度剖析与最佳实践》,如果两个控制器的协作存在大量的数据共享和交互可以利用Factory等服务的“单例”特性为它们注入一个共享对象来传递数据。而自己在使用rootScope
HTML:让表单、文本框只读,不可编辑的方法有时候,我们希望表单中的文本框是只读的,让用户不能修改其中的信息,如使中国">的内容,"中国"两个字不可以修改。实现的方式归纳一下,有如下几种。方法1:onfocus=this.blur()中国"onfocus=this.blur()>方法2:readonly中国"readonly>中国"readonly="tru
在AngularJS应用中实现微信认证授权遇到的坑前言 项目开发过程中,移动端新近增加了一个功能“微信授权登录”,由于自己不是负责移动端开发的,但最后他人负责的部分未达到预期效果。不能准确实现微信授权登录。最后还得靠自己做进一步的优化工作,谁让自己是负责人呢?原来负责人就是负责最后把所有的BUG解决掉。 首先,熟悉一下微信授权部分的源代码,如下所示:
AngularJS实现二维码信息的集成思路需求 实现生成的二维码包含订单详情信息。思路获取的内容数据如下: 现在可以获取到第一级数据,第二级数据data获取不到。利用第一级数据的获取方法获取不到第二级数据。for(i in data){alert(i); //获得属性 if(typeof(data[i]) == "o
Cookie'data'possiblynotsetoroverflowedbecauseitwastoolarge(5287>4096bytes)!故事起源 项目开发过程中遇到以上问题,刚开始以为只是个警告,没太在意。后来发现直接影响到了程序的执行效果。果断寻找解决方法。问题分析 根据Chrome浏览器信息定位,显示以下代码存在错误:
AngularJS控制器controller之间如何通信angular控制器通信的方式有三种:1,利用作用域继承的方式。即子控制器继承父控制器中的内容2,基于事件的方式。即$on,$emit,$boardcast这三种方式3,服务方式。写一个服务的单例然后通过注入来使用利用作用域的继承方式由于作用域的继承是基于js的原型继承方式,所以这里分为两种情况,当作用域上面的值
AngularJS路由问题解决遇到了一个棘手的问题:点击优惠详情时总是跳转到药店详情页面中去。再加一层地址解决了,但是后来发现问题还是来了:Couldnotresolve'yhDtlMaintain/yhdetail'fromstate'yhMaintain'药店详情http://192.168.1.118:8088/lmapp/index.html#/0优惠券详情
书海拾贝之特殊的ng-src和ng-href在说明这两个指令的特殊之前,需要先了解一下ng的启动及执行过程,如下:1)浏览器加载静态HTML文件并解析为DOM;2)浏览器加载angular.js文件;3)angular监听DOMContentLoaded事件,监听到时开始启动;4)angular寻找ng-app指令,确定作用范围;
angularjs实现页面跳转并进行参数传递Angular页面传参有多种办法,我在此列举4种最常见的:1.基于ui-router的页面跳转传参(1)在AngularJS的app.js中用ui-router定义路由,比如现在有两个页面,一个页面(producers.html)放置了多个producers,点击其中一个目标,页面跳转到对应的producer页,同时将producerId
AngularJS实现表格数据的编辑,更新和删除效果实现首先,我们先建立一些数据,当然你可以从你任何地方读出你的数据var app = angular.module('plunker', ['ui.bootstrap']);app.controller('MainCtrl', function($scope) { $scope.name = 'World'; $sc
ANGULAR三宗罪之版本陷阱      坑!碰到个大坑,前面由于绑定日期时将angular版本换为angular-1.3.0-beta.1时,后来午睡后,登录系统,发现无论如何都登陆不进去了,经过调试,发现数据视图已经无法实现双向绑定了。自己还以为又碰到了“僵尸程序”了呢,对比药店端的程序发现并没有什么不同之处。后来自己经过一番思索才隐约感觉到是不是angular的版本造成的,将版本换为之前
JS实现分页操作前言 项目开发过程中,进行查询操作时有可能会检索出大量的满足条件的查询结果。在一页中显示全部查询结果会降低用户的体验感,故需要实现分页显示效果。受前面“JS实现时间选择插件”的启发,自己首先需要查看一下HTML5能否实现此效果。 整了半天,不管是用纯CSS3也好,还是用tmpagination.js还是bootstrap组件也好,到最后自己静下心来理
浏览器兼容性解决之道前言 浏览器兼容性一直是前端开发中不得不面对的一个问题。而最突出的就是IE。对绝大多数公司来说,兼容IE6的性价比已经很低,而IE7则几乎已经绝迹。所以,常见的兼容性下限是IE8。这也正是Angular1.2x的兼容性目标,Angular团队声明:Angular的持续集成服务器会在IE8下运行所有的测试。但这些测试不会运行在IE7及以下版本,它们也不会保证An
JS利用正则表达式校验手机号绪 由于项目需求,需要在前端实现手机号码的校验。当然了,对于基本的格式校验应该放在客户端进行,而不需要再将待校验的手机号发送至服务端,在服务端完成校验,然后将校验结果返回给客户端,客户端根据返回的结果再进行进一步的处理。如此反而复杂化了处理过程。 其实,处于安全考虑,应该在服务端进行二次校验。以下为在客户端的JS中校验手机号码格式
基于项目实例解析ng启动加载过程前言 在AngularJS项目开发过程中,自己将遇到的问题进行了整理。回过头来总结一下angular的启动过程。 下面以实际项目为例进行简要讲解。1.载入ng库2.等待,直到DOM树构造完毕。3.发现ng-app,自动进入启动引导阶段。4.根据ng-app名称找到相应的路由。5.加载默认地址。6.Js顺序执行,加载相应模版页sys_tpls/