AJAX 是用于描述网页与网络服务器互动的一系列技术的术语。它不是一项新技术,而是对长时间以来已存在技术的应用。随着主要网站(例如谷歌)展示其优势,它作为一种开发技术变得流行。AJAX 这个术语被创造出来,用以描述异步 JavaScript 和 XML 应用程序。在本节中,我们首先将探讨 AJAX 的一般用途。 AJAX 技术的主要好处是使基于网络的应用程序对用户来说显得更加响应迅速。通常,一个基于网络的应用程序会经历以下三个步骤:
这三个步骤对网页用户来说非常熟悉。然而,从用户的角度看,第二步是浪费时间,用户只能等待下一个步骤的过程。第二步也非常依赖网络响应速度,当网络响应不佳时,这一步骤可能会影响用户对应用程序的满意度。 AJAX 带来的用户体验与上述描述不同。使用 AJAX 技术,与网络服务器的交互可以变得不那么明显。一个典型的 AJAX 网页的行为如下:
通过精心设计,上述过程允许实现看似对用户更加响应迅速的网络应用程序。由于只有网页的一部分需要更新,网页的其余部分仍然显示,用户可以继续与页面上的其他控件互动。由于网络服务器请求是异步的,网页在网络通信发生时继续运作。同时,可以同时激活多个服务器请求,这些请求可能由网页的不同部分或甚至相同的网页控件生成。 AJAX 是使用网页代码中的 JavaScript 实现的。JavaScript 允许将函数编程并分配给网页上的 DOM 对象和事件。这使得函数可以响应用户操作而执行。在本单元中,我们将使用 Typescript 编码,正如你所知,它会被转换为 JavaScript,以便部署到客户端。 异步 HTTP 连接作为 JavaScript XMLHttpRequest 对象进行管理。这些对象控制 HTTP 请求,并在 HTTP 请求完成时启动 JavaScript 函数。我们将看到 jQuery 如何消除了在我们的 JavaScript 代码中访问此对象的必要性。 在服务器端,可以使用任何响应 HTTP 请求的服务器技术。简单的网络服务器、CGI、PHP、Web 服务等可以被 HTTP 请求启动,并提供所需的响应。 AJAX 的两个主要缺点是:
影响浏览器后退按钮和浏览器历史记录的原因是它们都依赖于网页的 URL。由于 AJAX 在网页从服务器加载后更新网页,所以更新的历史记录不会自动存储为 URL。AJAX 开发者必须仔细记住已发生的更新,并修改浏览器的后退按钮处理或确保服务器知道已应用的页面更新。 第二个缺点并不那么直接。大多数网页用户都熟悉加载网页所需的延迟,并已习惯于等待周期。然而,AJAX 使网页的某些方面看似瞬间完成,而任何服务器交互仍将受到网络延迟的影响。这种不一致性使网络延迟对用户更加明显。通过预加载数据到网页的不可见部分,精心设计可以克服这种不一致性。这要求 JavaScript 程序员预测用户将请求的数据。你将在下面的谷歌地图练习中看到这方面的一个例子。
现在我们来看看 Angular 对 AJAX 的支持。在 Angular 中,我们创建一个 HTTP 服务来管理我们的 HTTP 连接。它是 Angular 库中的一项服务,并使用依赖注入系统注册为服务(参见第三模块)。我们还可以使用其他服务进行远程通信,例如,将使用 JsonModule 来格式化通过 HTTP 连接发送或接收的 JSON 数据。 我们将分几个步骤来看看在 Angular 中如何处理服务器请求。在查看 Angular 代码之前,我们需要理解 JavaScript 的 Promises 概念和更高级的 Observables 概念,它们允许代码对服务器处理代码进行异步调用。然后,我们将检查与 Angular 一起分发的用于处理服务器交互的流行 Angular 库。最后,我们将看一个使用简单服务器应用程序的示例。 请注意,并行/异步编程是棘手的,所以理解这些概念可能需要一段时间。你可能会发现在相关的教程课程中进行实际编码更容易。
JavaScript promise 是一种使异步过程看起来像一连串函数调用的方法。它通过保存调用链并只在事件发生时处理链来工作。关键是 JavaScript 在程序中移动到下一个语句,而调用链在后台等待并执行。你可以将其视为程序执行中的一个“分支”。 Promises 是通过 Promise 类实现的。通常,我们不需要创建自己的 promise 对象,因为我们将使用返回 promise 对象的标准库。然而,了解它们的结构对于理解 promises 是如何工作的很有用。以下是在 Typescript 中创建一个新 Promise 对象的概要,尽管你不需要这样做。
let promise = new Promise( (resolve, reject) =>
// 做一些事情,例如从服务器请求数据
// 检查数据是否有误
// 如果成功:
resolve();
// 如果出错
reject();
}
这段代码使用 Promise() 构造函数传递一个带有两个函数参数的函数,本例中为 resolve 和 reject。这些在函数体内调用,代码确定异步操作是成功还是被拒绝后。 一旦我们有了一个 Promise 对象,就定义了几种方法(继承自基类),允许我们指定计算(以函数形式)来处理计算结果。记住,这是一个异步过程,所以它将在操作完成并检查结果后发生。promise 对象上的主要方法有:
then(doSuccess,dofailure) - 执行 doSuccess 函数,如果成功则执行 doFail 函数 then(doSuccess) - 执行 doSuccess 函数,如果成功,则忽略失败。 catch(doFailure) - 执行 doFailure 函数,如果失败。忽略成功。 all([p1, p2, p3]) - 执行一系列 promises。如果所有 promises 都解决则解决,否则失败。 race([p1, p2, p3]) - 执行一系列 promises,并在第一个 promise 完成后解决或失败。
这些方法中的每一个都返回一个 promise 对象,所以可以将调用链接起来。例如,以下代码执行调用一个名为 get() 的库方法,该方法返回一个可以处理的 promise 对象。
get().then(doSuccess, doFail);
then() 函数只有在 get() 函数完成时才会执行。then() 函数有两个参数,一个是上一个函数成功时要执行的函数(doSuccess),另一个是出错时要执行的函数(doFail)。这段代码的结果与以下相同:
get().then(doSuccess).catch(doFail);
这是相同的,因为每种方法都使用从被调用方法中的对象中的信息返回另一个 promise。
Observables 被认为是用于管理异步事件的 JavaScript Promises 的改进。它们增加了各种控制机制,允许管理与远程服务和程序中的其他服务的异步交互。相比之下,一旦启动了 promise 函数的链条,通常没有办法停止它,直到链条成功或不成功地完成。 在 Angular 中,我们使用随 Angular 框架提供的 Rxjs 库。我们通过以下导入语句包含 Observable 类:
import { Observable } from 'rxjs';
Observable 可以被认为是管理一系列事件的对象。我们可以指定当事件到达时要做的事情,我们可以随时取消处理。我们将用一个实际上没有处理延迟的简单 observable 来演示,但更容易理解。我们将创建一个 observable,它一次处理一个数组对象的一个元素。为了达到使用 observables 进行 AJAX 或其他服务器操作的目标,你需要想象一下我们未来的示例,其中列表元素将是从服务器不可预测时间间隔接收的消息。 我们可以如下从一个字符串数组创建一个 observable:
import { from } from 'rxjs'; let myArray: string [] = ["abc","def","efg"]; let observable1 = from(myArray);
这里我们创建了一个数组,然后从中创建了一个 observable。这种情况下,from() 方法完成了创建 observable 的工作。记住,这不是通常使用 observables 的方式,因为这样的数组不需要异步处理。 一旦我们有了一个 observable 对象,我们可以订阅这个 observable。这是通过 Observable 类中的 subscribe() 方法完成的。这个方法接受三个函数作为参数,它们指定了处理成功数据检索、处理错误和处理 observable 完成的过程。例如,我们可以为上面的 observable 记录一些消息到控制台:
observable1.subscribe ( (data) => { console.log("Received: " + data}, (err) => {console.log("Error: " + err);}, () => {console.log("all done");} );
这里我们使用了内联函数定义,但如果你愿意,可以使用分别定义的函数的名称。上述对 subscribe 的调用将在控制台上打印以下消息:
Received: abc Received: def Received: efg all done
注意没有错误消息,因为我们不会期望访问数组时出错。也请注意,成功函数被调用以处理所有数组元素。在更有用的应用中,成功函数可以应用于例如来自服务器的数据流。 observables 的真正力量在于我们可以操纵数据流的方式。observable 类中有大量的方法,我们可以用它们来处理数据。例如,map() 方法允许我们对 observable 流中的每个对象应用一个函数。它返回一个 observable,所以我们可以用任何其他的流操纵调用(包括对 map() 的其他调用)将 map() 调用链接起来。 例如,下面的 map() 调用在我们上面创建的 observable 的字符串数据的开始和结束添加了一个星号。map 调用的参数是我们希望应用于 observable 流中每个对象的函数。在这个例子中,它是一个接受数据作为参数并返回字符串的函数。它不需要返回与参数相同的类型,但在这种情况下确实如此。
observable1.map( (str) => {return ""+str+"";} ) .subscribe ( (data) => { console.log("Received: " + data}, (err) => {console.log("Error: " + err);}, () => {console.log("all done");} );
现在在控制台上的输出将如下所示。
Received: abc Received: def Received: efg all done
另一个有趣的 observable 方法是 toPromise() 方法,它将当前的 Observable 对象转换为 Promise 对象。如果你阅读了 Angular 教程,这通常用于处理 HTTP 服务返回的 observable 对象。例如,以下是官方 Angular 教程中的一段 Angular 代码片段:
return this.http.get(url) .toPromise() .then(response => response.json().data as Hero) .catch(this.handleError);
这里,this.http.get(url)
返回一个包含 HTTP GET 请求响应的 observable,其中 URL 是 url 参数。toPromise() 调用将 observable 转换为 Promise 对象。然后链式的 then() 和 catch() 方法按照上面解释的那样处理成功状态和错误状态。 我们将使用许多其他 observable 操作函数。然而,要记住的是,我们可能不需要创建任何 observables,相反,我们将只使用由各种库方法创建的 observables。
总结来说,Angular 为 AJAX 提供了强大的支持,主要通过其 HTTP 服务和对 Promises 和 Observables 的深入集成。这允许开发人员以一种更高效和响应性更强的方式处理异步 HTTP 请求和响应。通过使用 Promises 和 Observables,Angular 应用能够优雅地处理异步操作,提供更加流畅和动态的用户体验。
现在,当 observables 用于管理来自远程源的数据流时,它们最有用。它们管理异步调用的能力意味着可以防止我们的程序在等待数据到达应用程序时停滞。在本节的剩余部分,我们将概述 Angular HTTP 类库。 以下活动的最终结果将是一个访问简单 RESTful 系统的应用程序。这意味着我们将逐步实现 GET、PUT 和 DELETE 消息(在这种情况下没有 POST 方法)。我们将开发的 Angular 应用程序将是一个简单且不美观的单页应用程序,这样我们就可以专注于 HTTP 访问。一个真正的应用程序会使用多页系统和一些 CSS 样式的工作。在你让 HTTP 访问工作之后,你可能会想这样做。 首先要注意的是,远程访问应该包含在前面主题中解释的 Angular 依赖注入中。这将允许 Angular 在应用程序中运行的多个组件之间管理对远程资源的访问。这也很重要,因为它允许我们将服务代码与组件中的用户界面代码分离。我们将在相关的教程课程中讨论以下示例的完整应用程序开发,但在这里我们将只看 AJAX observables 的代码。
以下代码定义了一个带有单个方法的依赖注入,用于启动 HTTP GET 请求。这将放在 data.service.ts 源文件中。
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { MyData } from './MyData';
@Injectable() export class DataService {
private url: string = “http://spike.scu.edu.au:8080/DataServ/”;
private headers = new Headers({'Content-Type': 'application/json'});
constructor(private http:HttpClient) { } public getData(name: string) : Observable { return this.http.get(this.url+name);
}
}
这段代码遵循前面主题中的依赖注入格式,但有一些需要注意的地方:
如上所述,Observable 管理由底层服务提供的一系列对象。然而,HTTP 操作只会提供一个响应,所以我们不必担心接收多个对象。map 操作只会处理返回的一个对象。
现在,我们正在访问的服务器管理着一个单一记录类型的简单数据库。我们可以如下将数据表示为一个 Typescript 类:
export class MyData { name: string; age: number; favColour: string; constructor(n: string, a: number, fc: string) { this.name = n; this.age = a; this.favColour = fc; } }
正如我们之前看到的,Observable 管理由底层服务提供的一系列对象。然而,HTTP 操作只会提供一个响应,所以我们不必担心接收多个对象。
请注意,使用此依赖注入的组件或其他模块可以调用 getData() 方法,并传入字符串参数。也请注意,它是如何将字符串添加到 URL 中以形成 RESTful GET 操作,其中添加的字符串是定位远程服务器上对象的标识符。
我们可以按照以下方式实现一个组件来使用上述依赖注入:
export class AppComponent { rec: MyData = new MyData("", 0, ""); name1: string; // 用于键的数据,映射到一个输入字段 message1: string; // 错误显示信息
constructor(private data: DataService) { } getData() { this.data .getData(this.name1) .subscribe( (d: MyData) => { this.rec = d; }, (err) => { this.message1 = "Error: " + err.status + ": " + err.statusText; }); }
}
备注:
现在,上述组件的模板可以链接到类定义的属性。我们还必须导入相应的类。组件代码的第一部分将是:
import { Component } from '@angular/core'; import { Forms } from '@angular/forms'; import { DataService } from './data.service'; import { MyData } from './MyData';
@Component({ selector: 'app-root', template: Lab 6 - HttpClient app
, styleUrls: ['./app.component.css'] })
关于这段代码的一些要点:
要使这一切工作,我们还需要做一件事情。我们需要更新 app.module.ts 这个“管道”模块,以将 Angular 对象链接在一起。完整的 app.module.ts 文件如下:
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; import { DataService } from './data.service';
@NgModule({ imports: [ BrowserModule, FormsModule, HttpClientModule ], declarations: [ AppComponent ], providers: [ DataService ], bootstrap: [ AppComponent ] }) export class AppModule { }
当你第一次看到它时可能有点难以理解,但所有的并行编程在最初遇到时都有类似的问题。在接下来的活动中,花时间理解 Angular 变量如何在模板中显示,并与依赖注入交互。
列出记录
在我们添加额外的 HTTP 调用之前,我们将解释 DataServ 网络服务器应用程序。它是一个 RESTful 应用程序,旨在保存前一节中 MyData 类所描述的简单数据记录。作为一个 RESTful 系统,它依赖于 URL 结构来访问数据。它使用 JSON 格式(序列化)文本流传输这些数据项,这些数据项在我们的应用程序中被转换为并可以操作的 JavaScript 对象。注意,JSON 格式不重要,因为我们使用库方法自动进行转换。然而,你可以通过使用浏览器直接访问服务器应用程序来查看数据,使用我们应用程序中隐藏的 URL。
下表总结了重要的消息。
HTTP 消息 | URL 文件部分 | 服务器上的操作 |
---|---|---|
GET | /DataServ/name | 检索名为 ‘name’ 的单条记录。如果不存在,则错误 404 |
GET | /DataServ/ | 检索服务器上所有记录的数组 |
PUT | /DataServ/name | 更新现有的 ‘name’ 记录或创建一个新的 ‘name’ 记录。数据以消息体中的 JSON 格式存在。如果创建记录成功则状态 201,如果更新则状态 200。 |
DELETE | /DataServ/name | 删除名为 ‘name’ 的记录。如果找到则状态 204,如果未找到则错误 404。 |
应该注意的是,这可能不是典型的 RESTful 实现。有时使用 POST 来创建新记录,以防止意外创建。由于数据返回的大小,一般的 GET 在大型数据库中从不使用。通常,一般的 GET 返回的是所有记录的 URL 列表,而不是记录本身。有许多变体。
我们应用程序的下一步是提供列出服务器上所有记录的功能。我们将通过添加一个“列出全部”按钮并使用 *ngFor 属性生成列表来实现这一点。我们可以将以下代码添加到模板的底部。
所有数据:
{{d.name}}:{{d.age}}:{{d.favColour}}
这段代码链接到一个将包含从服务器返回的数据的数组,并将在 app 组件中如下定义。
allData: MyData[];
在模板代码中,你可以看到新按钮 getAll() 的点击事件处理器。它可以在 app 组件中定义如下。
getAll() { this.data .getAll() .subscribe( (d:MyData[]) => { this.allData = d; }, (err) => { this.message2 = "错误:" + err.status + ":" + err.statusText; }); }
请注意,这个附加功能也使用了我们在前一节中看到的 data 可注入对象。它还调用了可注入对象的一个新成员函数 allData()。这可以添加到可注入对象(data.service.ts)中,并定义如下。
public getAll() : Observable { return this.http .get(this.url); }
如果你将这个与之前的可注入成员函数进行比较,你会看到它有两个主要区别:
为了让你明白,这里有一个迄今为止应用程序的图片。数据库中只有两个条目,“获取数据”按钮已被点击。
上表中的下一个操作是 PUT 操作,用于创建新记录或更新现有记录。这种消息类型可用于在服务器上创建新记录或更新现有记录。这种方法与其他方法不同,因为它还将记录数据以 JSON 数据的形式发送到服务器。以下是在可注入服务(data.service.ts)中实现 HTTP PUT 操作的简单方法调用。
public putData(rec: MyData) : Observable { return this.http.put(this.url, rec, this.httpOptions); }
关于这种实现的说明:
我们可以通过在组件中添加一个新方法,然后从组件的模板中使用它来使用这个方法。组件方法可以实现如下:
putData() { if (this.f_name=="" || isNaN(parseInt(this.f_age)) || this.f_colour=="") { this.message3 = "所有字段必须正确输入"; return; } this.data .putData(new MyData(this.f_name, parseInt(this.f_age), this.f_colour)) .subscribe( (d:MyData) => { this.message3="成功"; }, (err) => { this.message3 = "错误:" + err.status + ":" + err.statusText; }); }
关于这段代码的说明:
我们可以在模板中使用以下(不美观的)模板代码实现一些字段来使用这个方法:
{{message3}}
你可以看到与组件代码的链接,其中包括 putData() 事件处理器。然而,我们引入了一些需要在组件中定义的新变量:
f_name: string; f_age: string; f_colour: string; message3: string;
这部分模板相当丑陋,可以使用一些样式来美化。为了避免复杂性,在这里我们不展示这部分。上述模板段落在 Chrome 中输入了一些数据:
我们需要实现的最后一个操作是一种从数据中移除记录的方法。我们可以使用 RESTful 风格的服务器应用程序的 HTTP DELETE 消息来完成这一任务。如上表所解释的,这只需要我们发送我们希望删除的对象的 URL。 我们可以在我们的可注入服务中实现如下方法。
public deleteData(id: string): Observable { return this.http .delete(this.url + id) .map(response => response.status); }
这是一个相当简单的方法,它为服务器对象标识符并形成一个用于 DELETE 消息的 URL。返回的可观察对象通过 map() 操作转换为表示服务器返回的状态的可观察数字。在这种情况下,404 表示操作失败,204 表示成功删除且没有数据返回。请注意,subscribe() 操作将分离这些状态以执行成功和错误函数。
我们可以在组件的模板中添加一个小的删除部分,以请求要删除的对象的标识符以及一个删除按钮。
{{message4}}
我们可以访问一个消息以显示错误状态,以及一个与文本字段通过双向赋值链接的新变量。这必须在组件中声明:
d_name: string; message4: string;
我们还引入了一个为删除按钮定义的点击处理器,定义如下。
deleteData() { if (this.d_name == "") { this.message4 = "必须指定要删除的名称"; return; } this.data .deleteData(this.d_name) .subscribe( (d: MyData) => { this.message4 = "成功删除 " + this.d_name; }, // 成功 (err) => { this.message4 = "错误:" + err.status + ":" + err.statusText; }); }
关于这段代码的说明:
标签中。
标签以显示错误对象中的消息(如果发生错误)。
在添加了上述代码并尝试删除不存在的记录后,应用程序将如下所示。请注意,之前部分的数据在点击“提交数据”按钮后已被添加到数据库中。
在上面的屏幕图片中,你可以看到当我们尝试删除数据库中不存在的记录时,服务器返回了错误 404。关联的消息是从服务器返回的消息,对用户来说没有意义,但它是表明 HTTP DELETE 请求已被处理的消息。在真实的用户应用程序中,你需要使用更合适的消息。你会知道这是一个错误,因为作为 success() 方法的第二个参数传递的错误函数被执行。
使用互联网上的 AJAX 存在严重的安全问题。安全问题的主要来源是恶意网页或网页组件从用户可能不完全意识到的站点加载脚本或其他代码。网络已发展到支持基于同源策略的安全模型。简而言之,这意味着只有当脚本和其他代码(HTML、CSS、媒体等)来自与页面相同的来源时,才能将其加载到页面中。然而,有时从其他网站加载数据也是有用的,关键是用户(服务器和客户端)必须给予明确或隐含的许可,这样才能发生。
第一层防御发生在 HTTP 头部内。这适用于使用 HTTP 协议传输的任何内容,并使用称为跨源资源共享(CORS)的标准。实现 CORS 的服务器将通知请求资源的浏览器是否允许传输资源。这是通过 HTTP 响应头实现的:
Access-Control-Allow-Origin: url’s that are allowed access
像 Firefox 和 Chrome 这样的浏览器实现了 CORS,所以当网页从非网页加载站点的站点请求资源时,它将检查此头部以查看服务器是否允许以这种方式使用资源。对于我们的目的,这意味着 jQuery AJAX 调用将需要在请求的服务器上实现 CORS,如果 AJAX 请求的服务器与网页加载的服务器不同。CORS 允许服务器所有者声明谁可以加载他们的内容,但如果服务器是由黑客构建的呢?
更大的问题是当网页从用户不知道的服务器加载资源,并且可能由黑客配置时。由于 AJAX 需要脚本来使用,浏览器还必须尽可能小心不执行恶意脚本。目前对抗跨站脚本(XSS)问题的核心防御是浏览器对内容安全策略(CSP)的支持。这对于我们基于 Cordova 的应用程序(因此也包括 Ionic 应用程序)尤其相关,因为 Cordova 应用程序的“源”页面是安装在其上的移动设备。对于我们的 Cordova 应用程序访问互联网上的站点,我们必须在应用程序定义中管理 CSP。然而,像 Firefox 和 Chrome 这样的现代浏览器也将实现 CSP,因此对于基于 Web 的应用程序,了解这一点也很重要。
CSP 是通过网页上的 Content-Security-Policy 头部定义来管理的。默认情况下,当此头部不存在时,网页由浏览器默认定义对互联网站点的开放访问,因此容易受到 XSS 攻击。头部定义中的值为各种功能增加了安全性,例如:
script-src - JavaScript 代码的来源
img-src - 图像的来源
style-src - CSS 样式的来源
还有许多其他可定义的属性。最重要的是 default-src。如上所述,默认情况下,CSP 是允许开放访问。如果我们指定 default-src,那么这是适用于大多数(不是全部)CSP 属性的默认值。它覆盖了浏览器默认的访问权限。
值可以是关键字,也可以是表示 URI 和 URI 组件的字符串。两个重要的关键字(总是用引号表示)是:
'none' - 没有匹配 'self' - 匹配当前源
URI 'unsafe-inline' - 允许内联 JavaScript 和 CSS
字符串代表逻辑上限制源的 URI 组件。示例包括:
http://spike.scu.edu.au - 具有 http: 协议的完全限定主机名(默认为端口 80) spike.scu.edu.au - 任何协议在此主机上均被允许 scu.edu.au - 允许 scu.edu.au 的任何子域 http://spike.scu.edu.au:8080 - 仅允许端口 8080(我们稍后将使用此端口)
最后,我们需要演示如何在我们的网页中使用 CSP 规范。以下是在 Cordova 应用程序的 HTML 页面中插入的默认 CSP 指令(虽然是单行,但在这里换行以适应)。
标签是 HTML 文档头部的第一个标签,所以首先处理安全性。只有两个标签属性,http-equiv 表明我们正在使用 CSP 安全系统(允许未来的系统),content 属性是使用上面解释的符号的 CSP 值。内容字符串需要一段时间才能理解,但你应该注意: