面向--NET-开发者的-JavaScript-教程-全-

面向 .NET 开发者的 JavaScript 教程(全)

原文:zh.annas-archive.org/md5/9D370F6C530A09D4B2BBB62567683DDF

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

这是一本关于 JavaScript 编程语言的书,面向希望使用流行的基于客户端的 JavaScript 框架开发响应式网络应用程序的.NET 开发者,以及希望创建丰富用户体验的程序员。它也适合具有 JavaScript 编程语言基础知识并希望了解一些核心和高级概念以及一些业界最佳实践和模式的程序员,以结构和设计网络应用程序。

这本书从 JavaScript 的基础知识开始,帮助读者了解核心概念,然后逐步深入到一些高级主题。其中一章主要关注 jQuery 库,这个库在整个网络应用开发中广泛使用,之后是一章关于 Ajax 技术,帮助开发者理解如何进行异步请求。接着是使用原生 JavaScript 的 XHR 对象或 jQuery 库进行请求的选项。还有一章通过使用 Angular 2 和 ASP.NET Core 开发完整的应用程序来介绍 TypeScript,这是一种支持 ECMAScript 2015 最新和 evolving 特性的 JavaScript 的超集。我们还将探索 Windows JavaScript (WinJS)库,使用 JavaScript 和 HTML 开发 Windows 应用程序,并使用这个库将 Windows 行为、外观和感觉带到 ASP.NET 网络应用程序中。有一个完整的章节介绍了 Node.js,帮助开发者了解 JavaScript 语言在服务器端的强大之处,之后的一章则讨论了在大型项目中使用 JavaScript 的方法。最后,这本书将以测试和调试章节结束,讨论哪些测试套件和调试技术可用于故障排除并使应用程序健壮。

这本书有一些非常密集的主题,需要全神贯注,因此非常适合有一些先前知识的人。所有章节都与 JavaScript 相关,围绕 JavaScript 框架和库构建丰富的网络应用程序。通过这本书,读者将获得关于 JavaScript 语言及其构建在其上的框架和库的端到端知识,以及测试和调试 JavaScript 代码的技术。

本书涵盖内容

第一章,现代网络应用程序的 JavaScript,关注 JavaScript 的基本概念,包括变量的声明、数据类型、实现数组、表达式、运算符和函数。我们将使用 Visual Studio 2015 编写简单的 JavaScript 程序,并了解这个 IDE 为编写 JavaScript 程序提供了什么。我们还将研究如何编写 JavaScript 代码,并比较.NET 运行时与 JavaScript 运行时的区别,以阐明代码编译过程的执行周期。

第二章,高级 JavaScript 概念,涵盖了 JavaScript 的高级概念,并向开发者展示了 JavaScript 语言的洞察。它将展示 JavaScript 语言在功能方面可以被使用的程度。我们将讨论变量提升及其作用域、属性描述符、面向对象编程、闭包、类型数组和异常处理。

第三章,在 ASP.NET 中使用 jQuery,讨论了 jQuery 及其在 ASP.NET Core 开发的网络应用程序中的使用。我们将讨论 jQuery 提供的选项及其与普通原生的 JavaScript 在操作 DOM 元素、附加事件和执行复杂操作方面的优势。

第四章,Ajax 技术,讨论了被称为 Ajax 请求的异步请求技术。我们将探讨使用 XMLHttpRequest(XHR)对象的核心概念,并研究 Ajax 请求的基本处理架构以及它提供的事件和方法。另一方面,我们还将探讨 jQuery 库与普通的 XHR 对象相比提供的内容。

第五章,使用 Angular 2 和 Web API 开发 ASP.NET 应用程序,介绍了 TypeScript 的基本概念并将其与 Angular 2 结合使用。我们将使用 Angular 2 作为前端的客户端框架、Web API 作为后端服务以及 Entity Framework Core 用于数据库持久化,在 ASP.NET Core 中开发一个简单的应用程序。在撰写本文时,Angular 2 处于测试版阶段,本章使用了测试版。随着 Angular 2 未来的发布,框架有可能发生一些变化,但基本概念几乎保持不变。对于未来的更新,您可以参考angular.io/

第六章,探索 WinJS 库,探讨了由微软开发的 WinJS 库,这是一个不仅可以用 JavaScript 和 HTML 开发 Windows 应用程序,还可以与 ASP.NET 和其他网络框架一起使用的 JavaScript 库。我们将讨论定义类、命名空间、派生类、混合类(mixins)和承诺(promises)的核心概念。我们还将研究数据绑定技术以及如何使用 Windows 控件或 HTML 元素的特定属性来改变控件的行为、外观和感觉。此外,我们将使用 WinRT API 在我们的网络应用程序中访问设备的摄像头,并讨论通过宿主应用(Hosted app)的概念,任何网络应用程序都可以使用 Visual Studio 2015 中的通用窗口模板(Universal Window template)转换成 Windows 应用程序。

第七章,JavaScript 设计模式,表明设计模式为软件设计提供了高效的解决方案。我们将讨论一些业界广泛采用的最佳设计模式,这些模式分为创建型、结构型和行为型。每个类别将涵盖四种类型的设计模式,这些模式可以使用 JavaScript 来实现并解决特定的设计问题。

第八章,Node.js 对 ASP.NET 开发者的应用,专注于 Node.js 的基础知识以及如何使用它来使用 JavaScript 开发服务器端应用程序。在本章中,我们将讨论视图引擎,如 EJS 和 Jade,以及使用控制器和服务的 MVC 模式实现。此外,我们将在本章结束时通过一些示例来访问 Microsoft SQL Server 数据库,执行对数据库的读取、创建和检索操作。

第九章,使用 JavaScript 进行大型项目开发,提供了使用 JavaScript 进行大型应用开发的最佳实践。我们将讨论如何通过将项目拆分为模块来提高可扩展性和可维护性,从而结构化我们的基于 JavaScript 的项目。我们将了解如何有效地使用中介者模式(Mediator pattern)提供模块间的通信以及文档框架,以提高你的 JavaScript 代码的可维护性。最后,我们将讨论如何通过将 JavaScript 文件压缩和合并成压缩版本来优化应用程序,并提高性能。

第十章, 测试和调试 JavaScript, 专注于 JavaScript 应用程序的测试和调试。我们将讨论最受欢迎的 JavaScript 代码测试套件 Jasmine,并使用 Karma 运行测试用例。至于调试,我们将讨论一些使用 Visual Studio 调试 JavaScript 的技巧和技术,以及 Microsoft Edge 为简化调试所提供的内容。最后,我们将研究 Microsoft Edge 如何使调试 TypeScript 文件变得简单的基本概念以及实现所需的配置。

您需要什么

全书我们将使用 Visual Studio 2015 来实践示例。对于服务器端技术,我们使用了 ASP.NET Core 进行网络应用开发,并在其上使用 JavaScript。在第八章, Node.js 对 ASP.NET 开发者的使用中,我们使用 Node.js 展示了 JavaScript 如何用于服务器端。对于 Node.js,我们需要在 Visual Studio 2015 中安装一些扩展,具体细节在章节中说明。

本书适合谁阅读

本书面向具有扎实 ASP.NET Core 编程经验的.NET 开发者。全书使用 ASP.NET Core 进行网络开发,并假定开发者有.NET Core 和 ASP.NET Core 的深入知识或实际经验。

约定

在本书中,你会看到多种文本样式,用以区分不同类型的信息。以下是这些样式的一些示例及其含义解释。

文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、假网址、用户输入和 Twitter 处理显示如下:"JavaScript 可以放在 HTML 页面的<head><body>部分。"

代码块如下所示:

<html>
  <head>
    <script>
      alert("This is a simple text");
    </script>
  </head>
</html>

任何命令行输入或输出如下所示:

dotnet ef database update –verbose

新术语重要词汇以粗体显示。例如,在菜单或对话框中出现的屏幕上的词汇,在文本中显示为:"当页面加载时,它将显示弹出消息和文本这是一个简单文本。"

注意

警告或重要说明以框的形式出现,如下所示。

提示

技巧和小窍门如下所示。

读者反馈

我们的读者反馈始终受欢迎。让我们知道您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它有助于我们开发出您能真正从中获益的标题。

要发送一般性反馈,只需发送电子邮件至<feedback@packtpub.com>,并在消息主题中提到本书的标题。

如果你在某个主题上有专业知识,并且对撰写或贡献书籍感兴趣,请查看我们的作者指南,网址为www.packtpub.com/authors

客户支持

既然你已经拥有了一本 Packt 书籍,我们有很多东西可以帮助你充分利用你的购买。

下载示例代码

你可以从你账户中的www.packtpub.com下载本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

你可以按照以下步骤下载代码文件:

  1. 使用你的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持选项卡上。

  3. 点击代码下载与勘误

  4. 搜索框中输入书籍名称。

  5. 选择你要下载代码文件的书籍。

  6. 从下拉菜单中选择你购买这本书的地方。

  7. 点击代码下载

你还可以通过在 Packt 出版社网站上点击书籍网页上的代码文件按钮来下载代码文件。通过在搜索框中输入书籍名称可以访问此页面。请注意,你需要登录到你的 Packt 账户。

下载文件后,请确保使用最新版本解压或提取文件夹:

  • 适用于 Windows 的 WinRAR / 7-Zip

  • 适用于 Mac 的 Zipeg / iZip / UnRarX

  • 适用于 Linux 的 7-Zip / PeaZip

本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/JavaScript-For-.NET-Developers。我们还有其他来自我们丰富书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。去看看吧!

下载本书的彩色图片

我们还为你提供了一个包含本书中使用的屏幕截图/图表彩色图片的 PDF 文件。彩色图片将帮助你更好地理解输出中的变化。你可以从www.packtpub.com/sites/default/files/downloads/JavaScriptForNETDevelopers_ColorImages.pdf下载这个文件。

勘误表

虽然我们已经竭尽全力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告,我们将非常感激。这样做可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何错误,请通过访问www.packtpub.com/submit-errata报告,选择您的书籍,点击错误提交表单链接,并输入您的错误详情。一旦您的错误得到验证,您的提交将被接受,并且错误将被上传到我们的网站或添加到该标题下的错误部分现有的错误列表中。

要查看以前提交的错误,请前往www.packtpub.com/books/content/support并在搜索框中输入书籍名称。所需信息将在错误部分出现。

盗版

互联网上侵犯版权材料的问题持续存在,所有媒体都受到影响。在 Packt,我们对保护我们的版权和许可证非常认真。如果您在互联网上以任何形式发现我们作品的非法副本,请立即提供给我们地址或网站名称,以便我们可以寻求补救措施。

如果您怀疑有被盗版的材料,请联系我们<copyright@packtpub.com>

我们感激您在保护我们的作者和我们提供有价值内容的能力方面所提供的帮助。

问题

如果您对本书任何方面有问题,您可以联系<questions@packtpub.com>,我们将尽力解决问题。

第一章:为现代网络应用程序的 JavaScript

近年来,网络开发以惊人的速度发展。大多数在桌面平台上开发的企业应用程序现在都转移到了网络平台,原因是访问的便捷性和网络平台不断添加的丰富功能。通常,任何提供桌面应用程序特征的网络应用程序都被认为是富网络应用程序。因此,它涉及大量使用 JavaScript 及其框架和库。

JavaScript 在开发富应用程序中扮演着重要的角色,并允许开发人员减少服务器端的回调并通过 ajaxified 请求调用服务器端函数。不仅如此,现在许多公司和社区都在开发像 Angular、Knockout、ReactJS 这样的优秀框架,带来最先进和突破性的功能。微软还发布了WinJS库,使从移动浏览器上运行的网页应用程序能够访问移动原生设备功能,如相机、存储等。myNFC 也是一个很棒的 JavaScript 库,它允许开发人员为智能手机创建应用程序。

JavaScript 的重要性

所有客户端框架都是基于 JavaScript 的。作为一名 ASP.NET 开发者,在使用或将其集成到我们的应用程序之前,我们应该对 JavaScript 有扎实的概念。JavaScript 是客户端脚本语言,是有史以来最受欢迎的编程语言之一,在浏览器上运行。当在 web 开发项目中工作时,这种语言以许多更好的方式为您服务,使用户界面UI)具有响应性。通过 JavaScript,您可以操作 HTML 页面文档对象模型DOM)元素,通过 ajaxified 请求调用服务器端代码,并向您的客户带来新的丰富体验。在 JavaScript 库的核心进行了许多创新,并且已经开发出了不同的框架和各种库。

JavaScript 是什么?

JavaScript 是一种由 Brendden Eich 在 1995 年创造的编程语言。最初,它只被 Netscape Browser 支持,但后来他们决定发布一个被称为 ECMA 规范的标准,让其他浏览器实现并提供引擎来在其浏览器上执行 JavaScript。提供这个标准的原因是为了让遵循方拥有完整的规格细节并保持行为的一致性。

最初,它主要针对在浏览器上执行,并执行与 HTML 页面一起工作的客户端操作,如操作 DOM 元素、定义事件处理程序和其他功能。后来,在近年来,它已经成为一种强大的语言,并不仅限于客户端操作。通过 Node.js,我们可以在服务器端使用 JavaScript,并且 Node 提供了各种模块和插件来执行 I/O 操作、服务器端事件等。

比较运行时

由于本书针对.NET 开发者,让我们将 JavaScript 运行时与.NET 运行时进行比较。有一些共同之处,但基本的运行时不同。

在.NET 中,公共语言运行时CLR)对正在运行的代码进行即时编译JIT)并提供内存管理。JIT 编译是在你构建项目后生成的一次性编译的中间语言IL)代码上进行的。

在 JavaScript 世界中,浏览器引擎是 JavaScript 语言的运行时。每个浏览器都以自己的方式解释 JavaScript,但都遵循 ECMA 脚本标准。不同的浏览器有不同的实现,例如,Microsoft Edge 使用 Chakra 引擎,Chrome 使用 V8,Firefox 有 Monkey 引擎。最初,JavaScript 被实现为一种解释型语言,但现在很少有现代浏览器进行 JIT 编译。每个引擎都提供一套服务,如内存管理、编译和处理。

以下图表展示了两种架构的比较:

比较运行时

JavaScript 解析器解析和标记 JavaScript 代码,将其转化为语法树。所有浏览器(除了 Google V8)解析语法树并生成字节码,最终通过 JIT 编译转换成机器码。另一方面,Google V8 引擎解析语法树,而不是首先生成字节码,它直接生成机器码。

.NET源代码由其自己的语言编译器编译,例如 C#或 VB.NET 编译器,并经过编译器管道的几个阶段生成 IL 代码。然后 JIT 编译器读取这个 IL 代码并生成原生机器代码。

设置你的环境

在阅读本书之前,让我们设置一下你的环境。市场上有很多著名的编辑器可用于创建 JavaScript 项目,如 Sublime Text、Komodo IDE、NetBeans、Eclipse 等,但我们将继续使用 Visual Studio 2015,它带来了一些很好的改进,帮助开发者比以前更好地工作在 JavaScript 上。

接下来,让我们下载并安装 Visual Studio 2015。你可以从www.visualstudio.com/下载 Visual Studio 2015 社区版,这是一个免费版本,并提供以下章节中描述的某些改进。

Visual Studio 2015 IDE 中的 JavaScript 新编辑体验

新的 Visual Studio 2015 IDE 为开发网络应用程序提供了许多丰富的功能,并且有各种模板可用于根据不同框架和应用程序模型创建项目。早期版本已经支持 IntelliSense、着色和格式化,但新的 Visual Studio 2015 IDE 还有一些更多的改进,如下所示:

  • 增加了对 ECMAScript 6 脚本语言的支持,正式名称为 ES2015。有了新的 ES2015,添加了许多功能,现在您可以定义类、lambda 表达式、展开操作符和代理对象。因此,借助 Visual Studio 2015,您可以在 JavaScript 代码中使用这些功能并获得所有 IntelliSense。

  • 支持流行的 JavaScript 客户端框架,如 Angular、ReactJS 等。

  • 文档注释可以帮助您为 JavaScript 方法添加注释,并在使用它们时显示描述:Visual Studio 2015 IDE 中 JavaScript 的新编辑体验

  • 对新的 JavaScript API(如触摸事件和 Web 音频 API)的支持。

  • 您可以使用诸如//TODO//HACK//UNDONE之类的标记,它会在任务列表窗口中为您提供列表,帮助您追踪待办事项:Visual Studio 2015 IDE 中 JavaScript 的新编辑体验

  • 有了 JavaScript 文件,Visual Studio 2015 提供了我们在编写任何.NET 语言类时所熟悉的导航栏。使用此功能,选择并导航到不同的 JavaScript 方法要容易得多:Visual Studio 2015 IDE 中 JavaScript 的新编辑体验

在 JavaScript 中编程

JavaScript 是最强大的语言之一,在任何网页开发项目中都发挥着至关重要的作用,提供客户端支持并实现丰富的功能。在本节中,我们将讨论在 JavaScript 中编写程序的核心概念,并将其应用于网页应用程序中。

JavaScript 的核心基础知识

<script></script> tags defined within the <head></head> section:
<html>
  <head>
    <script>
      alert("This is a simple text");
    </script>
  </head>
</html>

页面加载时,会显示弹出消息和一段文字,如这是一个简单的文本。浏览器执行定义在<script>标签下的任何脚本,并运行此块内的语句。定义在脚本标签直接下方的任何语句在页面加载时都会执行。

同样,我们也可以在 HTML 页面的<body>部分定义 JavaScript:

<html>
  <body>
    <script>
      alert("hello world");
    </script>
  </body>
</html>

提示

将脚本放在页面底部是一个好主意,因为编译可能会减慢页面加载速度。

通常,在每一个项目中,无论项目规模大小,将<script>部分与 HTML 分离可以使代码看起来更整洁,也更容易维护。JavaScript 文件扩展名称为.js,您还可以在一些脚本文件夹中单独创建这些文件,并在我们的 HTML 页面中引用它们。

在 Visual Studio 中,您可以使用添加 | JavaScript 文件选项轻松创建 JavaScript 文件,如下所示:

将 JavaScript 添加到 HTML 页面

文件创建完成后,我们就可以直接编写 JavaScript 语法,而无需使用<script></script>标签。JavaScript 文件可以通过在 HTML 页面中使用<script></script>标签的src属性来引用。在这里,我们在 HTML 页面中引用了test.js

<script src="img/test.js">
</script>

<script>标签放在<head><body>部分取决于页面。如果您的页面引用一些大的 JavaScript 文件需要很长时间来加载,最好将它们定义在<body>部分的末尾。这是一个更好的方法,因此当浏览器开始解析您的页面时,它不会因为下载脚本而卡住,导致渲染延迟。另一方面,我们只有在它们不会影响性能或页面生命周期的情况下,才能在<head>部分定义 JavaScript 文件。在底部定义的脚本在整个页面加载后进行解析。我们还可以在<script>标签内使用一些属性,如asyncdefer,大多数浏览器支持这些属性。

以下是一个使用async<script>标签中的示例:

<script src="img/test1.js" async></script>
<script src="img/test2.js" async></script>

使用async定义的脚本异步执行,不会阻塞浏览器加载页面。然而,如果存在多个脚本,那么每个脚本都将异步执行且同时进行。这可能导致第二个脚本在第一个脚本完成之前就完成了,如果其中一个脚本依赖于另一个脚本,可能会抛出一些错误。例如,当使用某些客户端框架时,如 Angular 框架,使用 Angular 组件的 JavaScript 代码依赖于 AngularJS 库;在这种情况下,如果我们的自定义 JS 文件在它们依赖的 AngularJS 库之前被加载,它们将会抛出一个异常。

为了克服这种情况,我们可以使用defer按顺序执行脚本。我们可以这样使用defer

<script src="img/test1.js" defer></script>
<script src="img/test2.js" defer></script>

asyncdefer之间的基本区别是,async在 HTML 解析期间下载文件,并在完全下载后暂停 HTML 解析器执行它,而defer在 HTML 解析期间下载文件,并在 HTML 解析器完成后执行它。

JavaScript 中的语句

语句是执行特定任务的单词、表达式和操作符的集合。与其他编程语言一样,JavaScript 中的语句也可以是给变量赋值、执行算术操作、实现条件逻辑、遍历集合等。

例如:

var a; //variable declaration
a = 5; //value assignment
a = 5 * b; //value assignment
a++; // equivalent to a= a+1
a--; // equivalent to a= a-1
var method = function () { … } // declare function
alert("Hello World") // calling built-in function
if(…) {…} else {…}
for (…) {…}
while(…) {…}

然而,您可以在do while循环中使用分号:

do {…} while (…);
function statement
function (arg) { //to do }

提示

如果同一行中定义了多个语句,它们应该用分号分隔,否则它们将被视为一个语句。在不同行中,分号不是必须的,但使用分号是一个好习惯。

字面量和变量

JavaScript 中有两种类型的值:字面量或固定值和变量。

字面量可以是数字、字符串或日期对象。

例如:

Numbers
22.30
26
Strings
"John"
"10/Jan/2015"

变量用于存储值。在 JavaScript 中,我们可以使用var关键字定义变量。JavaScript 不是一种类型安全的语言,变量的类型在分配值时确定。

例如:

var x=6;
var x="Sample value";

数据类型

每种编程语言都有特定的数据类型可用于存储特定数据。例如,在 C#中,我们可以使用String来存储字符串值,int来存储 32 位整数值,DateTime来存储日期和时间的值等等。JavaScript 没有提供像 C#和其他编程语言那样的强数据类型,它是一种松散类型的语言。根据最新的 ECMA 6 标准,JavaScript 提供了六个原始数据类型和一个对象。所有的原始数据类型都是不可变的,这意味着分配新值将会分配到单独的内存中。对象是可变的,其值可以被改变。

原始类型如下:

  • Boolean: 这持有逻辑值truefalse

  • Null: 这持有null值。

  • Undefined: 这是没有分配值并且值为 undefined 的变量。

  • Number: 这持有数值。number类型的尺寸是双精度 64 位,其中数值(分数)从 0 存储到 51 位,指数从 52 存储到 62 位,符号位是 63 位。

  • String: 这持有任何类型的文本值。

复杂类型被称为对象。在 JavaScript 中,对象是以 JSON 格式编写的。

JavaScript 中的数组

数组用于存储数据集合。你可以在 JavaScript 中简单地定义一个数组,如下所示:

var browsers = ["Microsoft Edge", "Google Chrome", "Mozilla Firefox", "Safari"];

你可以通过数组索引来访问它们。索引从 0 开始,直到数组中的项目数。

我们可以如下访问数组项目:

var a= browsers[0]; //returns Microsoft Edge
var b= browsers[1]; //returns Google Chrome
var c= browsers[3]; //returns Safari

为了获取数组中项目总数,你可以使用length属性:

var totalItems = browsers.length;

以下是一些最常用方法的列表:

方法 描述
indexOf() 这会返回数组中等于特定值的元素的第一个索引,如果没有找到则返回-1
lastIndexOf() 这会返回数组中等于指定值的元素的最后一个索引,如果没有找到则返回-1
pop() 这会从数组中删除最后一个元素并返回那个元素。
push() 这会在数组中添加一个元素并返回数组长度。
reverse() 这会反转数组中元素的顺序。第一个元素变成最后一个,最后一个元素变成第一个。
shift() 这会删除第一个元素并返回那个元素。
splice() 这用于向数组中添加或删除元素。
toString() 这会返回所有元素的字符串表示。
unshift() 这会将元素添加到数组的前端并返回新长度。

提示

下载示例代码

下载代码包的详细步骤在本书的前言中提到。请查看。

本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/JavaScript-For-.NET-Developers。我们还有来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/找到。去看看吧!

JSON 是什么?

JavaScript 对象表示法JSON)是定义 JavaScript 中对象的轻量级、可交换格式。任何类型的对象都可以通过 JSON 定义,并用于构建通用数据结构。无论是简单对象、数组、嵌套数组还是复杂对象,都可以在 JSON 格式中处理。

JSON 中的简单对象

person object that has three properties, namely name, email, and phone:
var person = {
  "name" : "John Martin",
  "email": johnmartin@email.com,
  "phone": "201892882"
}

我们可以按以下方式访问这些对象属性:

person.name;
person.email;
person.phone;

在 JSON 中声明数组

以下代码片段显示了在 JSON 中声明数组的方式:

var persons =
[{ 
  "name":"John",
  "email": "john@email.com",
  "phone":"201832882"
},
{
  "name":"Steve",
  "email": "steve@email.com",
  "phone":"201832882"
},
{
"name":"Smith",
"email": "smith@email.com",
"phone":"201832882"
}]

根据前面声明的数组,可以按以下方式访问:

//returns name of the first item in the collection i.e. John
Persons[0].name
//returns email of the first item in the collection i.e. john@email.com
Persons[0].email
//returns name of the second item in the collection i.e. Steve
Persons[1].name

在 JSON 中嵌套数据

JSON 格式可以轻松处理嵌套数组。让我们看看包含employee对象的复杂对象,该对象包含Experiences数组,该数组包含嵌套数组以持有项目,每个项目都有一个嵌套数组以持有每个项目中所使用的技术:

var employee=
{
  "ID":"00333",
  "Name":"Scott",
  "DateOfJoining":"01/Jan/2010",
  "Experiences":[
    {
      "companyName":"ABC",
      "from":"Nov 2008",
      "to":"Oct 2009",
      "projects" :[
        {
        "title":"Sharepoint Migration",
        "noOfTeamMembers":5,
        "technologyUsed":[{"name":"SharePoint Server"}, {"name":"C#"}, {"name":"SQL Server"}]
        },
        {
        "title":"Messaging Gateway",
        "noOfTeamMembers":5,
        "technologyUsed":[{"name":"ASP.NET"}, {"name":"C#"}, {"name":"SQL Server"}]
        }
      ]
    },
    {
      "companyName":"XYZ",
      "from":"Nov 2009",
      "to":"Oct 2015",
      "projects" :[
        {
        "title":"ERP System",
        "noOfTeamMembers":5,
        "technologyUsed":[{"name":"ASP.NET"}, {"name":"C#"}, {"name":"SQL Server"}]
        },
        {
        "title":"Healthcare System",
        "noOfTeamMembers":4,
        "technologyUsed":[{"name":"ASP.NET"}, {"name":"C#"}, {"name":"SQL Server"}]
        }
      ]
    }
  ]
}
First assign the string to the res variable:
var res="Hello World";

然后将数字分配给同一个res变量:

res= 2;

最后,将字符串3连接到持有以下数字的res变量中,但由于数值具有更高的优先级,结果值变成了5

var result = res + "3"

因此,无论最初分配给它的变量类型是什么,它都会根据赋值改变其类型,并动态处理转换。

JavaScript 的元素

以下是我们在开始用 JavaScript 编程之前必须学习的 JavaScript 的一些重要元素。

JavaScript 中的常量

JavaScript 中的常量可以用const关键字定义。常量是在编译时已知的不可变值,在整个程序的生命周期中值不会改变。

以下是显示常量变量赋值的 JavaScript 代码。当使用const时,不需要var,您只需使用const关键字即可声明常量值:

const pi= 3.42

注释

注释可以用///* */添加。要注释单行,可以使用//,否则使用/* */来注释代码块。

以下是用 JavaScript 代码注释单行或代码块的方式:

<script type="text/javascript">

function showInformation() {

  //var spObj = window.document.getElementById("spInfo");
  spObj.innerHTML =
    "Available Height: " + screen.availHeight + "<br>" +
    /*"Available Width: " + screen.availWidth + "<br>" +
    "Height: " + screen.height + "<br>" +*/
    "Width: " + screen.width + "<br>"
}

</script>

大小写敏感性

JavaScript 是一种大小写敏感的语言,它遵循 Pascal 命名约定来定义变量和方法。

例如,如果方法名是doWork(),只能通过以确切的大小写调用它,而调用DoWork()Dowork()将不起作用并抛出异常。

字符集

JavaScript 基于 Unicode 字符集,并遵循 Unicode 标准。

注意

什么是 Unicode 标准?

它是一个全球编码标准,大多数语言都会使用。C# 和 VB.NET 遵循相同的 Unicode 标准。它为每一个字符提供了一个唯一的数字,例如,A = 41a = 61,等等。

当前的 Unicode 标准版本是 Unicode 8.0.0,相关文档可访问 www.unicode.org/versions/Unicode8.0.0/

表达式

表达式可以被认为是将某些值赋给变量的代码语句。表达式分为两种类型。

第一种表达式可以称为简单表达式,它将值赋给变量:

var x = 2;

前一个示例表示将数值 2 赋给变量 x 的简单表达式。

第二种类型的表达式可以称为对右侧值进行任何算术或字符串操作,并将它们赋给任何变量。这类表达式在赋值给变量之前先执行操作:

var x = 2+3
var x = "Hello" + "World";

这是第二种类型的表达式的示例,它将两个数字相加,并将结果值赋给 x 变量。第二个语句执行字符串连接操作,并将 Hello World 值赋给 x 变量。

这个关键字

就像 C# 和其他面向对象的语言一样,JavaScript 也有对象,并且有一些定义类、函数等等的方法,我们将在本章后面学习。就像在 C# 中一样,在 JavaScript 中,我们可以通过 this 关键字访问对象及其属性。让我们看看一些显示 JavaScript 中 this 关键字作用域的例子。

以下是一个包含几个属性和 this 关键字使用的 customer 对象:

var customer =
  {
    name: "John Marting",
    email: "john@xyz.com",
    mobile: "109293988844",
    show: function () {
      alert("Name: "+this.name + " Email: " + this.email + " Mobile: " + this.mobile);
    }
  }

在前一个例子中,我们定义了一个包含三个属性和一个函数的 JavaScript 对象。要访问这些属性,我们可以像在 C# 中一样使用 this 关键字。然而,我们也可以使用 customer 变量来访问属性,如下所示:

var customer =
  {
    name: "John Marting",
    email: "john@xyz.com",
    mobile: "109293988844",
    show: function () {
      alert("Name: "+ customer.name + " Email: " + customer.email + " Mobile: " + customer.mobile);
    }
  }

this 关键字的范围限制在对象的范围之内。然而,在前一个例子中的 customer 变量可能定义在页面的其他地方,可能导致不当的行为。尽可能使用 this 关键字并避免直接使用对象变量是一个更好的方法。

直接定义在 <script> 标签下的所有变量和函数称为全局变量和函数。我们也可以通过 this 关键字访问它们。在这种情况下,this 将被称为全局窗口对象,而不是前面例子中使用的子对象,即 customer 对象:

<script type="text/javascript">
  var name = "";

  function ShowMessage() {
    alert(this.name);
  }
</script>
alert(window.name);

让我们看看完整的示例,其中我们定义了全局变量,以及子对象,this 的作用域将根据其调用的上下文来确定:

<script type="text/javascript">
  var name = "Scott Watson";

  var customer =
    {
      name: "John Marting",
      email: "john@xyz.com",
      mobile: "109293988844",
      show: function () {
        alert("Name: " + this.name + " Email: " + this.email + " Mobile: " + this.mobile);
      }
    }
  function ShowMessage() {
    alert("Global name is " + this.name);
    alert("Customer info is " + customer.show());
  }
</script>

在前面的示例中,我们将收到两个 JavaScript 警告消息。第一个警告将显示Scott Watson,它是全局定义的,第二个弹出窗口显示客户姓名、电子邮件地址和手机号码。因此,我们可以在两个地方使用this,但作用域是根据它从中调用的上下文确定的。

在 JavaScript 中的代码执行顺序

在 JavaScript 编程中,我们必须保持定义事物的顺序,然后再调用它们。考虑前面的示例,如果我们定义customer对象在ShowMessage()方法之后,它将不会被识别,什么也不会显示。

在调用方法上使用 this 关键字

让我们来看看一个名为Multiply的示例 HTML 页面,它有一个 JavaScript 函数,接受两个参数:objval。当用户在文本框中输入任何内容时,此方法将被调用,并将文本框控件的引用作为第一个参数传递。可以通过this关键字传递:

<html>
<head>
  <script type="text/javascript">
    function Multiply(obj, val) {
      alert(obj.value * val);
    }
  </script>
</head>
<body>
  <input type="text" onchange ="Multiply(this, 2);" />
</body>
</html>

函数声明和表达式

函数声明是定义 JavaScript 中方法的一种方式。每个函数都有一个签名,包括名称和传入的参数。在 JavaScript 中,函数可以通过多种方式声明。例如,以下是GetPerson(id)函数的示例,该函数根据作为参数传递的 ID 返回person对象。这是在 JavaScript 中声明函数的正常方式:

<script>

  function GetPerson(id) {
    return service.GetPerson(id);
  }

</script>

function 的返回类型是在运行时计算的,而不是函数签名的一部分。返回值不是强制的,你可以保持函数不返回任何值。

另一方面,匿名函数没有名称,它们可以作为其他函数的参数传递,或者没有函数名称定义。以下是无名函数的示例:

var showMessage = function(message){
  console.log(message);
}
showMessage("Hello World");

定义匿名函数并将其作为参数传递的另一个示例如下:

function messageLogger(message ,logMessage) {
  logMessage();
}

function consoleMessage() {
  alert("Hello World");
}
messageLogger(consoleMessage());

函数表达式与函数等价,唯一的区别是它不应该以函数名开始。

类声明和表达式

随着 ECMAScript 6,我们可以在 JavaScript 中创建类。与其他编程语言一样,我们可以使用class关键字创建类。借助于此,我们可以比在 ECMAScript 的早期版本中用函数表示类的方式写出更清晰的代码。

让我们来看看计算面积的Rectangle类:

<script>
  class Rectangle {
    constructor(height, width) {
      this.height=height;
      this.width=width;
    }
    get Area() {
      return this.calcArea();
    }
    calcArea(){
      alert("Area is "+ this.height * this.width);
    }
  }
</script>

每个类应该有一个构造函数,如果指定了多个构造函数,则应该报错。类表达式是定义类的一种另一种方式。就像匿名函数一样,我们可以用类似的方式定义类。

让我们来看看前面定义的同一个类的示例:

<script>
  var Rectangle = class{
    constructor(height, width) {
      this.height=height;
      this.width=width;
    }
    get Area() {
      return this.calcArea();
    }
    calcArea(){
      alert("Area is "+ this.height * this.width);
    }
  }
</script>

下一章将详细介绍类以及构建它们的属性和关键字。

分组运算符

对于任何算术表达式,JavaScript 使用BODMAS规则。优先级将首先给括号,然后是乘法、除法、加法和减法。分组运算符用于给表达式中任何成员的默认优先级更高的表达式更高的优先级。

例如:

var a = 1;
var b = 2;
var c = 3;
var x = a + b * c;

结果x将是7,因为乘法有更高的优先级。然而,如果我们需要先进行加法呢?

我们可以像下面这样使用分组运算符,结果为9

var x = (a + b) * c;

new

与 C#一样,new关键字用于在 JavaScript 中实例化任何对象。为了创建任何用户定义或预定义类型的实例,使用new关键字:

var obj=new objectType();

super

super关键字用于调用父对象的方法。在 C#中,我们使用base关键字来调用基类的方法或属性。在 JavaScript 中,我们可以这样使用:

super.functionOnParent();

运算符

运算符是用来操作操作数值的对象。例如,1 + 2的结果是3,其中12是操作数,+是一个运算符。在 JavaScript 中,我们可以使用几乎所有的运算符来连接字符串,进行算术运算等。在本节中,让我们看看在 JavaScript 语言编程时我们可以使用哪些类型的运算符。

我们将在本节讨论以下运算符:

  • 赋值运算符

  • 算术运算符

  • 一元运算符

  • 比较运算符

  • 逻辑运算符

  • 位运算符

  • 位移运算符

  • 类型 of 运算符

  • 空值运算符

  • 删除运算符

  • 杂项运算符

赋值运算符

赋值运算符表示为(=),并且赋值是从右到左进行的。

例如,x=y意味着y的值被赋给x

算术运算符

以下是一系列你可以用来进行加法、减法、除法和乘法以及与赋值语句一起使用的算术运算符:

名称 运算符 意义
加法 x + y x的值加上y
减法 x – y x的值减去y
除法 x / y x的值除以y
乘法 x * y x的值乘以y
取模运算符 x % y x的值除以y,返回余数
加法赋值运算符 x += y x = x + y,即xy的值相加,结果赋值给x
减法赋值运算符 x -= y x = x - y,即xy的值相减,结果赋值给x
乘法赋值运算符 x *= y x = x * y,即xy的值相乘,结果赋值给x
除法赋值运算符 x /= y x = x / y,即x的值除以y,结果赋值给x
取模赋值运算符 x %= y x = x % y,即x的值除以y,余数赋值给x
幂运算赋值 x **= y x = x ** yx的值将 exponentially 乘以两次y并赋值给x

一元运算符

一元运算符只与一个操作数一起使用。它可以用于递增、递减、取反等:

名称 运算符 意义
递增运算符 x++ x的值将增加1
递减运算符 x-- x的值将减少1
逻辑补码运算符 !(x) 这将x的值取反

比较运算符

number1 is equal to number2 and the summation of number1 and number2 is equal to number3 to return true:
<script>
  function CheckNumbers(number1, number2, number3) {
    if ((number1 == number2) && ((number1 + number2) == number3)) {
      return true;
    }
  }
<script>

逻辑或

10, it will return true:
<script>
  function AnyNumber10(number1, number2, number3) {
    if ((number1 ==10 || number2 == 10 || number3 ==10) {
      return true;
    }
  }
</script>

逻辑非

number1, number2, and number3 are equal to 10, the method will return false. If they are different, the return value will be true:
<script>
  function AnyNumber10(number1, number2, number3) {
    return !(number1 ==10 && number2 == 10 && number3==10) {
    }
  }
</script>

按位运算符

按位运算符将每个数字或操作数视为二进制(01的组合)。每个数字都有特定的二进制对应。例如,数字1的二进制表示为00015表示为0101

按位运算符对 32 位数字进行操作,任何数值操作数首先转换为 32 位数字,然后转换回 JavaScript 数字。

按位运算符在二进制中进行操作并返回数字结果。

例如,x1y9

1表示为0001

9表示为1001

按位与

按位与表示为&,下面是操作数19的每位比较。如果每个位上的值都是1,结果将是1,否则为0

数字 = 1 数字 = 9 结果
0 1 0
0 0 0
0 0 0
1 1 1

在 JavaScript 代码中,我们可以如下使用它:

<script>
  var a = "1";
  var b = "9";
  var c = a & b;
</script>

最后,结果值将是0001,等于1

按位或

按位或表示为|,下面是按位或的运算方式:

数字 = 1 数字 = 9 结果
0 1 1
0 0 0
0 0 0
1 1 1

下面的代码片段展示了在 JavaScript 中的使用:

<script>
  var a = "1";
  var b = "9";
  var c = a | b;
</script>

最后,结果值将是1001,等于9

按位非

按位非表示为~,它作用于单个操作数并反转每个二进制位。

例如,如果数字9表示为1001,它将转换为 32 位数字,然后按位非将其变为11111111111111111111111111110110,等于-10

以下是一个代码片段:

<script>
  var a = ~9;
</script>

按位异或

按位异或表示为^,它与两个或更多操作数一起工作。

下面的表格展示了按位异或是如何进行的:

数字 = 1 数字 = 9 结果
0 1 1
0 0 0
0 0 0
1 1 0

下面的代码片段展示了在 JavaScript 中的使用:

<script>
  var a = "1";
  var b = "9";
  var c = a ^ b;
</script>

最后,结果值将是1000,等于8

按位移位运算符

有三种按位移位运算符,如下:

  • 按位左移运算符

  • 按位右移运算符

按位左移

它表示为<<,用于将位从右侧移到任何数字的二进制值。

例如,数字9表示为01001,如果我们使用位左移,结果值将是10010,从右边移动了一位。

以下代码片段展示了在 JavaScript 中的使用:

<script>
  var a = 9;
  var result = a << 1;
</script>

最后,结果值将是10010,等于18

位右移

它表示为>>,用于将位从左侧移动到任何数字的二进制值。

例如,数字9表示为1001,使用位右移将结果值给出为0100

以下代码片段展示了在 JavaScript 中的使用:

<script>
  var a = "9";
  var result = a >> 1;
</script>

最后,结果值将是0100,等于4

类型 of 操作符

这用于检查变量的类型是否为对象、未定义、数字等。在 JavaScript 中,我们可以这样使用:

<script>
  if (typeof a=="number") {
    alert("this is a number");
  }
</script>

以下是 typeof 操作符可能返回的值列表:

Value returned 描述
--- ---
```"number"` 如果操作数是一个数字
```"string"` 如果操作数是一个字符串
```"boolean"` 如果操作数是一个布尔值
```"object"` 如果操作数是一个对象
null 如果操作数是 null
```"undefined"` 如果操作数未定义

void 操作符

void operator to display alert message when the link is clicked. Here, the alert expression is evaluated once the user clicks on the link:
<html>
<head></head>
<body>
  <a href="javascript:void(alert('You have clicked!'));">
  </a>
</body>
</html>

当页面运行且用户点击链接时,将显示以下警告消息框:

void 操作符

此外,在 void 方法内传递 0 作为表达式将不做任何事情:

<html>
<head></head>
<body>
  <a href="javascript:void(0);">
  Do Nothing
  </a>
</body>
</html>

另一个例子是使用 void 添加两个数字,并返回给定操作数的 undefined

<script>
  var n1 = 6;
  var n2 = 7;
  var n3;
  var result = void (n3 = n1 + n2);
  alert ("result=" + result + "and n3 =" + n3);
</script>

删除操作符

delete 操作符用于删除对象及其属性,但不删除局部变量。以下示例展示了如何在 JavaScript 中使用 delete 操作符:

var country = { id: 1, name: "USA" };

  delete country.id;

  alert(country.id);

调用 country.id 将返回 undefined,因为这在之前的语句中已经被删除。另一方面,如果我们删除 country 对象,它不会被删除并显示国家 ID 为 1

var country = { id: 1, name: "USA" };

  delete country;

  alert(country.id);

杂项操作符

compareValues() function that takes two parameters, and an alert will be displayed stating whether both the parameters are equal or not equal:
<script>
  function compareValues(n1, n2)
    (n1 == n2) ? alert("Both values are equal") : alert("Passed values are not equal");
</script>

展开操作符

展开操作符表示为()。当期望在函数调用中传递多个参数时使用。

例如,如果你的函数需要五个参数,你可以一个接一个地传递这些值作为调用该方法时的参数值,或者将它们放在一个数组中,并通过展开操作符传递该数组。

以下代码片段展示了在 JavaScript 中的实际示例:

function multipleArgs(a, b, c, d, e){
}
var args = [1,2,3,4,5]
multipleArgs(…args);

在 JavaScript 中的内置显示方法

以下是 JavaScript 中可用的显示方法,用于以不同形式向用户提供通知和消息。

显示消息

以下是三种弹出对话框类型:

  • 警告消息框

  • 确认消息框

  • 提示消息框

警告框

使用 window.alert(),我们可以弹出一个警告对话框:

<!DOCTYPE html>
<html>
<body>

  <h1>My First Web Page</h1>
  <p>My first paragraph.</p>

<script>
  window.alert(5 + 6);
</script>

</body>
</html>

确认框

使用window.confirm(),我们可以弹出一个确认对话框,返回用户所采取的事件结果。当确认对话框弹出时,它提供两个动作事件:确定取消。如果用户点击确定,将返回true,否则返回false。以下代码展示了在您的 HTML 页面上使用确认对话框的方法。

 saving a record:
<!DOCTYPE html>
<html>
<body>

<script>
  var r = window.confirm("are you sure to save record");
  if(r==true){
    alert("Record saved successfully");
  }
  else {
    alert("Record couldn't be saved");
  }
</script>

</body>
</html>

提示框

提示对话框在需要用户提供值的情况下使用。它可以在需要用户输入的条件下来使用。

下面的代码片段展示了在 JavaScript 程序中使用提示消息框的方法:

<!DOCTYPE html>
<html>
<body>

<script>
  var name = window.prompt("Enter your name","N/A");
  if(name !=null){
    alert("hello "+ name "+, how are you today!");
  }
</script>

</body>
</html>

页面上的写入

我们可以使用document.write()方法在屏幕上写入任何内容。

下面的代码片段展示了在 JavaScript 中在网页上编写任何文本的方法:

<!DOCTYPE html>
<html>
<body>
  <script>
  document.write("Hello World");
  </script>
</body>
</html>

向浏览器的控制台窗口写入

使用console.log(),我们可以将任何文本写入浏览器的控制台窗口。

下面的代码片段展示了在 JavaScript 中为了追踪或调试目的向浏览器控制台窗口写入文本的方法:

<!DOCTYPE html>
<html>
<body>
  <h1>My First Web Page</h1>
  <p>My first paragraph.</p>
  <script>
  console.log("Entered into script execution context");
  </script>
</body>
</html>

浏览器对象模型在 JavaScript 中

JavaScript 提供了一些预定义的全局对象,您可以使用它们来操作 DOM、关闭浏览器等。以下是我们可以用来执行不同操作的浏览器对象:

  • 窗口

  • 导航器

  • 屏幕

  • 历史

  • 位置

窗口

窗口对象指的是浏览器中打开的窗口。如果在 HTML 标记中定义了一些 iframes,将会创建一个单独的窗口对象。通过窗口对象,我们可以访问以下对象:

  • 所有全局变量

  • 所有全局函数

  • DOM

以下是一个从窗口对象访问 DOM 并访问文本框控制的示例。

文档

window.document返回文档对象,我们可以出于特定原因使用其属性和方法:

<html>
<body>
  <input type="text" name="txtName" />
  <script>
  var textbox = Window.document.getElementById("txtName");
  textbox.value="Hello World";
  </script>
</body>
</html>

window对象本身包含许多方法,其中一些如下:

事件 描述 语法
关闭 关闭当前窗口 window.close();
打开 打开新窗口 window.open();
移动 将窗口移动到指定的位置 window.moveTo();
调整大小 将窗口调整到指定的宽度和高度 window.resizeTo();

导航器

这个对象提供了关于浏览器的信息。当你需要根据浏览器版本运行特定的脚本或者对浏览器进行特定的操作时,它是有益的。我们来看看它暴露的方法。

属性

属性如下描述:

  • appCodeName:这返回浏览器的代码名称

  • appName:这返回浏览器的名称

  • appVersion:这返回浏览器的版本

  • cookieEnabled:这确定浏览器是否启用了 cookies

  • geoLocation:这获取访问页面的用户的位置

  • language:这返回浏览器的语言

  • online:这确定浏览器是否在线

  • platform:这返回浏览器编译的平台

  • product: 这返回浏览器的引擎名称。

  • userAgent: 这返回浏览器发送到服务器的主机代理头。

以下是一个示例代码:

<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript">
    function showInformation() {
      var spObj = window.document.getElementById("spInfo");
      spObj.innerHTML =
      "Browser Code Name: " + navigator.appCodeName + "<br>" +
      "Application Name: " + navigator.appName + "<br>" +
      "Application Version: " + navigator.appVersion + "<br>" +
      "Cookie Enabled? " + navigator.cookieEnabled + "<br>" +
      "Language: " + navigator.language + "<br>" +
      "Online: " + navigator.onLine + "<br>" +
      "Platform: " + navigator.platform + "<br>" +
      "Product: " + navigator.product + "<br>" +
      "User Agent: " + navigator.userAgent;
      navigator.geolocation.getCurrentPosition(showPosition);
    }
    function showPosition(position) {
      var spObj = window.document.getElementById("spInfo");
      spObj.innerHTML =  spObj.innerHTML + "<br> Latitude: " + position.coords.latitude +
      "<br>Longitude: " + position.coords.longitude;
    }
  </script>
</head>
<body onload="showInformation();">
  <span id="spInfo"></span>
</body>
</html>

输出如下所示:

属性

屏幕

通过屏幕对象,你可以获取有关用户屏幕的信息。这有助于了解用户从哪个屏幕查看内容。如果是移动浏览器或标准桌面屏幕,你可以获取尺寸和其他信息,并按需修改内容。

属性

属性如下描述:

  • availHeight : 这返回屏幕的高度。

  • availWidth: 这返回屏幕的宽度。

  • colorDepth: 这返回显示图像的颜色调色板比特深度。

  • height: 这返回屏幕的总高度。

  • pixelDepth: 这返回屏幕的颜色分辨率(每像素比特数)。

  • width: 这返回屏幕的总宽度。

示例代码如下:

<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript">
    function showInformation() {
      var spObj = window.document.getElementById("spInfo");
      spObj.innerHTML =
      "Available Height: " + screen.availHeight + "<br>" +
      "Available Width: " + screen.availWidth + "<br>" +
      "Height: " + screen.height + "<br>" +
      "Width: " + screen.width + "<br>"
    }
  </script>
</head>
<body onload="showInformation();">
  <span id="spInfo"></span>
</body>
</html>

输出如下所示:

属性

历史

这包含用户访问的 URL。你可以通过window.history对象访问它。

你可以使用这个对象导航到最近访问的链接。

方法

方法如下描述:

  • Window.history.back(): 这加载历史列表中的上一个 URL。

  • Window.history.forward(): 这加载历史列表中的最近 URL。

  • Window.history.go(): 这加载历史列表中特定的 URL。

位置

位置对象提供了关于当前 URL 的信息。就像历史一样,它也可以通过window.location访问。有一些方法和属性,你可以用来执行特定操作。

属性

属性如下描述:

  • window.location.host: 这返回 URL 的主机名和端口号。

  • window.location.hostname: 这只返回 URL 的主机名。

  • window.location.href: 这提供完整的 URL。

  • window.location.origin: 这返回 URL 的主机名、端口号和协议。

  • window.location.pathname: 这返回 URL 的路径名。

  • window.location.port: 这返回 URL 的端口号。

  • window.location.protocol: 这返回 URL 的协议,例如 HTTP 或 HTTPS。

  • window.location.search: 这返回 URL 的查询字符串。

方法

方法如下描述:

  • window.location.assign(): 这加载新文档。

  • window.location.reload(): 这重新加载当前 URL。

  • window.location.replace(): 这可以用来用新 URL 替换当前 URL。替换不会刷新页面,它只能改变 URL。

摘要

在本章中,我们讨论了 JavaScript 的基本概念以及如何在我们的网络应用程序中使用它。我们讨论了声明变量和实现数组、函数和数据类型的核心基础,以开始用 JavaScript 编写程序。在下一章中,我们将讨论一些关于面向对象编程的高级概念,以及与闭包、作用域和原型函数的实际应用一起工作。

第二章:高级 JavaScript 概念

JavaScript 在最初设计时,并没有预料到会成为 Web 开发的核心编程语言。它通常被用来执行一些基本的客户端操作,这些操作需要对文档对象模型DOM)元素进行一些操作。后来,随着 Web 开发的最近步伐,事情已经发生了很大的变化。现在,许多应用程序纯粹使用 JavaScript 和 HTML 来处理复杂的情况。有时,随着不同版本的出现,增加了不同的特性,根据 ECMAScript 6 的规范,你现在可以有类,可以进行继承,就像你用 C#或 Java 任何其他编程语言一样。闭包、原型函数、属性描述符等等,我们将在本章讨论的内容,使它更加强大和健壮。

在上一章中,我们学习了 JavaScript 编程的核心概念和一些基本的基本原理,以及它作为一门语言提供的特性。在本章中,我们将重点关注更高级的主题,这些主题有助于我们在大型和复杂的应用程序中使用这些概念。

我们还将重点关注作用域和提升变量、面向对象编程、原型函数、属性描述符、闭包、异常处理等。一些主题,如承诺、异步模式和异步 JavaScript 和 XMLAjax)技术,是更广泛的主题,在其他章节中进行覆盖。

变量 - 作用域和提升

我们已经知道如何在 JavaScript 中使用var关键字声明变量。任何使用var关键字声明的变量都被称为提升变量,提升是 JavaScript 的默认行为,将声明移动到顶部。当 JavaScript 通过 JavaScript 引擎编译时,所有使用var关键字声明的变量都放在其作用域的顶部。这意味着如果变量在函数块内声明,它将被放在函数顶部;否则,如果它声明在任何函数之外并在脚本的根部,它将变得全局可用。让我们看看这个例子来澄清我们的理解。

假设以下代码是一个简单的程序,它返回传递给函数参数的国家名称的 GMT:

function getCountryGMT(countryName) {
  if (countryName == "Pakistan") {
    var gmt = "+5.00";
  }
  else if (country == "Dubai") {
    var gmt = "+4.00";
  } else {
    return null;
  }
}

当 JavaScript 引擎编译脚本时,var gmt变量将被放在顶部:

function getCountryGMT(countryName) {
  var gmt; 
  if (countryName == "Pakistan") {
    gmt = "+5.00";
  }
  else if (country == "Dubai") {
    gmt = "+4.00";
  } else {
    return null;
  }
}

这称为提升,其中var变量被放在其作用域的顶部。此外,如果您尝试在最后一个else条件中访问变量值,它将给出一个未定义的值,并且在每个条件块中都可能可用。

这段代码显示了另一个声明gmt变量全局和在代码底部声明的例子:

function getCountryGMT(countryName) {
  if (countryName == "Pakistan") {
    gmt = "+5.00";
  }
  else if (country == "Dubai") {
    gmt = "+4.00";
  } else {
    return null;
  }
}

var gmt;

当脚本编译时,它将在代码顶部放置gmt的声明:

var gmt;

function getCountryGMT(countryName) {
  if (countryName == "Pakistan") {
    gmt = "+5.00";
  }
  else if (country == "Dubai") {
    gmt = "+4.00";
  } else {
    return null;
  }
}

为了克服 ECMAScript 6 中的这种行为,引入了一个新的 let 关键字来声明变量,其作用域保留在定义的位置。这些变量在其作用域外不可访问。

提示

请注意,ECMAScript 6 不被老旧的浏览器版本支持,但 Microsoft Edge、Google Chrome 11 和 Mozilla Firefox 支持它。

声明 let 变量

var 一样,你可以用 let 以相同的方式声明变量。你可以在你的程序中使用这个关键字,但它将仅在其定义的作用域内可访问。所以,例如,如果某个变量在条件块内定义,它将无法在其作用域之外访问。

让我们来看以下示例,其中在条件块内部声明了一个变量,在编译后的最终输出保持不变。这在您想在一个特定逻辑或场景内声明变量的条件下非常有用。在 else 条件中,gmt 将不可访问,因为它是在 if 条件内定义的:

function getCountryGMT(countryName) {
  if (countryName == "Pakistan") {
    let gmt = "+5.00";
  }
  else {
    return null;
  }
}

一旦在函数或脚本的作用域内声明了 let 变量,它就不能被重新声明。另外,如果使用 var 关键字声明变量,则不能使用 let 重新声明。

这段代码不会抛出异常,因为作用域不同。然而,在同一块中,它不能被重新声明:

function getCountryGMT(countryName) {
  var gmt;
  if (countryName == "Pakistan") {
    let gmt = "+5.00";
  }
  else {
    return null;
  }
}

在使用 let 关键字时效率较高的条件

以下是使用 let 的条件。

循环中的函数

如果在循环中的函数内部使用 var 变量,这些变量会产生问题。考虑以下示例,其中有一个值数组,并通过循环在每个数组的索引处插入一个函数。这将导致错误并将 i 变量作为引用传递。所以,如果你遍历每个索引并调用函数,将会打印出相同的值,即 10

var values = [];
for(var i=0;i<10;i++)
  {
    values.push(function () { console.log("value is " + i) });
  }
  values.forEach(function(valuesfunc) {
    valuesfunc();
  })
let is as follows:
var values = [];
  for(let i=0;i<10;i++)
  {
    values.push(function () { console.log("value is " + i) });
  }
  values.forEach(function(valuesfunc) {
    valuesfunc();
  })

JavaScript 中的事件

事件在任何一个商业应用程序中扮演着重要的角色,你希望在按钮点击事件上保存记录,或者显示一些消息,或者改变某个元素的背景颜色。这些事件可以从控件级别本身定义,或者通过脚本直接注册。

让我们来看一个例子,当鼠标进入时,这个例子会改变 div 控件内部的 html 代码:

<html>
  <body>
    <div id="contentPane" style="width:200px; height:200px;">
    </div>
    <script>
      var divPane = document.getElementById("contentPane");
      divPane.onmouseenter = function () {
        divPane.innerHTML = "You are inside the div";
      };
      divPane.onmouseleave = function () {
        divPane.innerHTML = "You are outside the div";
      };
    </script>
  </body>
</html>

前面的示例在 HTML div 控件的脚本侧注册了两个事件。如果鼠标进入了函数或离开了 div 的边界,它会改变文本。另外,我们也可以在控件本身上注册事件,这个示例展示了如何在按钮点击事件上显示一条消息。如果你注意到脚本块是在 div 面板之后定义的,原因是当页面加载时,它会尝试执行脚本并抛出一个错误,因为当时 contentPane 元素尚未创建:

<html>
  <body>
    <script>
      function displayMessage() {
        alert("you have clicked button");
      }
    </script> 
    <input type="button" onclick="displayMessage();" />
  </body>
</html>

在这个例子中,脚本块定义在页面的顶部。在这种情况下,它可以定义在页面的任何位置,因为它只有在用户点击按钮时才会执行。

函数参数

我们已经知道 JavaScript 函数可以有参数。然而,在创建函数时无法指定参数的类型。JavaScript 在调用函数时既不会对参数值进行类型检查,也不会验证传递的参数数量。所以,例如,如果一个 JavaScript 函数接受两个参数,像这段代码所示,我们甚至可以不传递任何参数值,或者传递任何类型的值,或者传递比定义的预期参数数量更多的值:

function execute(a, b) {
  //do something
}

//calling without parameter values
execute();

//passing numeric values
execute(1, 2);

//passing string values
execute("hello","world");

//passing more parameters
execute(1,2,3,4,5);

缺少的参数被设置为未定义,而如果传递了更多参数,这些参数可以通过 arguments 对象访问。arguments 对象是 JavaScript 中的一个内置对象,它包含了一个数组,该数组是在调用函数时使用的参数。我们可以像这段代码中这样使用它:

function execute(a, b) {
  //do something
  alert(arguments[0]);
  alert(arguments[1]);
  alert(arguments[2]);
  alert(arguments[3]);
  alert(arguments[4]);
}

  //passing more parameters
  execute(1, 2, 3, 4, 5);
}

参数按值传递;这意味着如果在函数内部改变了参数的值,它将不会改变原始参数的值。

在 JavaScript 中的面向对象编程

所有的 JavaScript 对象都是从某个对象继承来的。JavaScript 提供了不同的模式,以便在构建应用程序时遵循面向对象编程OOP)原则。这些模式包括构造器模式、原型模式和对象字面量表示法,以及 ECMAScript 6 中通过类和使用 extends 关键字继承基类来表示对象的一种完全新的方式。

在本节中,我们将了解如何使用不同的方法实现 OOP 原则。

创建对象

类表示对象的结构,每个类都有某些由对象使用的方法和属性,而对象是类的实例,被称为类实例。

JavaScript 是一种基于原型的语言,并且基于对象。在像 C# 和 Java 这样的类式语言中,我们必须首先定义一个包含一些方法和属性的类,然后使用其构造函数来创建对象。在 JavaScript 中,任何对象都可以作为模板来创建新对象,并使用其中定义的属性或方法。新对象也可以定义自己的属性或方法,并可以作为另一个对象的原型。然而,ECMAScript 6 在 JavaScript 中引入了类,这是对现有范式的语法糖,使开发者能够更容易地编写更简单、更干净的代码来创建对象。在下一节中,我们将看到在 JavaScript 中创建对象的不同方法。

使用对象字面量表示法定义对象

对象字面量是使用花括号括起来的由逗号分隔的名称值对列表。

对象字面量使用以下语法规则定义:

  • 冒号将属性名与值分隔开来:

  • 值可以是任何数据类型,包括数组字面量、函数和嵌套对象字面量:

  • 每个名称值对之间用逗号与下一个名称值对分隔:

  • 姓氏值对之后不应该包含任何逗号

以下是在对象字面量表示法中一个person对象的基本表示:

var person = {id: "001", name: "Scott", isActive: true, 
  Age: 35 };

以下是用对象字面量表示法展示的personModel对象的另一种表示,其中包含savePerson()方法:

var personModel = {id: "001", name: "Scott", isActive: true, 
  Age: 35, function: savePerson(){ //code to save person record } };

使用构造模式定义对象

在 JavaScript 中,可以使用函数来定义类。这段代码展示了用 JavaScript 定义客户类的一种简单方式:

var person = new function(){};

前面的代码只是定义了一个空的类,有一个默认构造函数,没有属性和方法。可以使用 new 关键字来初始化对象,如下面的代码所示:

var p1 = new person();

同一个函数可以用常规函数声明风格定义:

function person(){};

使用常规函数声明,JavaScript 引擎知道在需要时获取函数。例如,如果您在脚本中在函数声明之前调用它,它会调用这个函数,而变量定义方法需要在调用它之前先声明变量。

使用类关键字

ECMAScript 6 提供了一种新的定义类的方法,并引入了一个类关键字,可以像在其他编程语言中一样使用。这段代码是定义一个客户对象的表示。默认构造函数是constructor(),不带任何参数,可以根据需求用更多参数覆盖。每个类允许您定义只有一个构造函数,如果覆盖了构造函数,默认构造函数将不会用来实例化对象:

class Person {
  constructor() { }
}

属性

属性用于存储和返回值。我们可以在初始化函数时定义属性,每次创建对象时这些属性都将可用。

使用对象字面量表示法定义属性

属性可以在对象中定义为字面量字符串。例如,在这段代码中,有一个包含两个属性和一个方法的客户对象。这种方法的缺点是没有构造函数,我们无法限制用户在初始化对象时提供属性值。它可以设置为硬编码,如所示,或者在初始化对象后:

var person = {
  id: "001",
  name:"Person 1",
  savePerson: function(){
  }

}

使用构造模式定义属性

构造函数模式允许您定义参数,限制用户在实例化对象时传递属性值。考虑这个例子,它包含一个具有idname两个属性的客户对象:

var person = function(id, name){
  this._id = id;
  this._name = name;
}

this关键字指的是当前对象,在类内部调用时可以使用this来访问属性,或者通过实例变量,如下面的代码所示:

var p1 = new person("001","Person 1");
console.log("Person ID: "+ p1.PersonID);
console.log("Person Name: "+ p1.name);

属性值也可以在初始化对象后设置,如下面的代码所示:

var person = function(){
}
var p1 = new person();
p1.id="001";
p1.name="Person 1";

这段代码也代表了定义一个接受两个参数的人对象的相同方法。在下一节中,当我们处理原型时,我们将看到这种方法的局限性:

function person(id, name){
  this.id = id;
  this.name = name;
  this.logToConsole: function(){
    console.log("Person ID is "+ this.id  +",Name: "+ this.name);
  };
}

使用 setter/getter 在 ECMAScript 6 中定义属性

在 ECMAScript 6 中,有一种新的定义属性的方法,它遵循其他编程语言的标准方式:

class Person {
  constructor(id, name) {
    this.id = id;
    this.name = name;
  }
}
var p1 = new person("001", "Person 1");
console.log ("Person ID: " + p1.id);

与这种方法不同,我们也可以使用setget关键字定义 setter 和 getter。在 JavaScript 中定义类时,构造函数是可选的;如果没有定义构造函数,对象初始化时会调用默认构造函数constructor()。让我们看一个包含personName属性的例子,该属性用于 setter 和 getter:

class Person {
  set Name(name) {
    this.personName = name;
  }
  get Name() {
    return this.personName;
  }
}
var p1 = new Person();
p1.Name = "Person 1";
console.log("personName " + p1.Name);

JavaScript 属性描述符

每个属性都有属性描述符,用于配置,其含义如下:

  • Writable:这个特性用于设置代码为只读或可写。false关键字使其只读,值不能被修改。

  • Enumerable:这个特性用于隐藏/显示属性,使其可访问或可序列化。将此属性设置为false,在遍历对象成员时不会显示属性,并且在使用JSON.stringify时也不会被序列化。

  • Configurable:这个特性用于onoff的配置更改。例如,将此属性设置为false将防止属性被修改或删除。

所有这些特性默认都是true,但可以被覆盖,如下例所示。这个例子有一个car对象,包含两个属性,分别是namecolor

var car = {
  name: "BMW",
  color: "black"
};
显示属性描述符

你可以使用以下语句显示现有属性:

display(Object.getOwnPropertyDescriptor(car, 'name'));
管理属性描述符

任何对象的属性都可以像以下代码那样进行管理:

Object.defineProperty(car, 'color',{enumerable: false});
Object.defineProperty(car, 'color',{configurable: false});
Object.defineProperty(car, 'color',{writable: false});
使用 getter 和 setter

通过Object.defineProperty,我们还可以为属性添加 setter 和 getter。这个例子通过连接makename并分割name来添加汽车的完整名称,然后通过两个不同的属性获取模型和名称:

var car = { name: { make: "honda",  brand: "accord"} };
Object.defineProperty(car, 'fullname', 
{
  get: function(){
    return this.name.make + ' ' + this.name.brand 
  },
  set: function (value) {
    var names= value.split(' ');
    this.name.make = names[0];
    this.name.brand = names[1];
  }
});
car.fullname = "Honda Accord";
display(car.fullname);

方法

方法是可以在对象上执行的动作。在 JavaScript 中,它可以表示为一个包含函数定义的属性。让我们看看定义 JavaScript 对象方法的不同方法。

通过对象字面量表示法定义方法

以下是一个示例,展示了对象字面量表示法中定义的logToConsole()方法:

var person = {
  id: "001",
  name:"Person 1",
  logToConsole: function()
  {
    console.log("Person ID is "+ this.id  +", Customer Name: "+ this.name);
  }
}

使用构造函数函数定义对象

通过constructor函数定义方法的代码如下:

var person = function (id, name) {
  this._id = id;
  this._name = name;
  this.LogToConsole= function(){
    console.log("Person Name is "+ this._name);
  }
}
var p1 = new person("001", "Person 1");
p1.LogToConsole();

另一种方法是声明constructor函数,如下所示:

function person(id, name) {
  this._id = id;
  this._name = name;
  this.LogToConsole= function(){
    console.log("Name is "+ this._name);
  }
}
var p1 = new person("001","Person 1");
p1.LogToConsole();

在 ECMAScript 6 中,定义方法的语法更为优雅。以下是一个示例代码片段:

class Person {

  constructor() {

  }

  set Name(name) {
    this._name = name;
  }

  get Name() {
    return this._name;
  }

  logToConsole() {
    console.log("Person Name is " + Name);
  }
}

var p1 = new Person();
p1.Name = "Person 1";
p1.logToConsole();

定义方法时不需要指定方法返回类型,它基于方法体实现。

扩展属性和方法

每个 JavaScript 对象都有一个称为原型的对象。原型是指向另一个对象的指针。这个原型可以用来扩展对象属性和方法。例如,如果你尝试访问一个对象的某个未定义属性,它会查看原型对象并通过原型链继续查找,直到找到或者返回 undefined。因此,无论使用字面量语法方法还是构造函数方法创建对象,它都会从称为Object.prototype的原型继承所有方法和属性。

例如,使用new Date()创建的对象从Date.prototype继承,依此类推。然而,基本对象本身没有原型。

我们可以很容易地向对象添加属性和函数,如下所示:

var Person = function (name) {
  this.name = name;
}
var p1 = new Person("Person 1");
p1.phoneNo = "0021002010";
alert(p1.name);

不初始化对象而扩展现有函数是通过原型对象完成的。让我们来看这个例子,我们在Person函数上添加了一个方法logToConsole()和一个phoneNo属性:

var Person = function (name) {
  this.name = name;
}
Person.prototype.phoneNo = "";
Person.prototype.logToConsole = function () {
  alert("Person Name is " + this.name +" and phone No is "+ this.phoneNo)
};
var p1 = new person("Person 1");
p1.phoneNo = "XXX"
p1.logToConsole();

私有和公共成员

在 JavaScript 中,没有像我们在 C#中那样的访问修饰符。所有定义为this或具有原型的一切成员都可以从实例中访问,而其他以某种其他方式定义的成员是不可访问的。

让我们来看这个例子,它只允许yy1()方法在函数外部被访问:

function a() {
  var x = 1;
  this.y = 2;
  x1 = function () {
    console.log("this is privately accessible");
  }
  this.y1 = function () {
    console.log("this is publicly accessible");
  }
}

继承

继承是面向对象编程的核心原则。在 JavaScript 中,如果你使用的是不遵守 ES6 标准的旧版本,它是通过基于原型的编程来实现的。

基于原型的编程是一种不使用类,而是通过原型链来扩展对象或继承的面向对象编程模型。这意味着每个对象都有一个内部的prototype属性,它指向一个特定的对象,如果没有使用则为 null。这个属性不能通过程序访问,并且对 JavaScript 引擎来说是private的。所以,例如,如果你调用某个属性,比如customer.getName,它会首先在对象本身上查找getName属性,否则通过原型属性链接对象来查找。如果没有定义属性,它会返回 undefined。

考虑以下实体-关系模型ERD),它有一个具有某些通用属性的基本 person 对象和两个子对象,分别是VendorEmployee,具有特定的属性:

继承

为了用 JavaScript 构造函数方法表达相同的继承,我们可以像这段代码一样,将VendorEmployeeprototype属性添加到 person 对象上:

var Person = function (id, name) {
  this.id = id;
  this.name = name;
}

var Vendor = function (companyName, location) {
  this.companyName = companyName;
  this.location = location;
}

var Employee = function (employeeType, dateOfJoining) {
  this.employeeType = employeeType;
  this.dateOfJoining = dateOfJoining;
}

Vendor.prototype = new Person("001", "John");
Employee.prototype = new Person("002", "Steve");

var vendorObj = new Vendor("ABC", "US");
alert(vendorObj.id);

在前一个示例中,vendorObj是从Vendor构造函数创建的对象。Vendor构造函数既是对象又是函数,因为函数在 JavaScript 中是对象,而vendorObj对象可以有自己的属性和方法。它还可以从Vendor对象继承方法和属性。

通过构造函数将VendorEmployee对象的prototype属性设置为Person实例,它继承了Person对象的属性和方法,并成为VendorEmployee对象可访问的。

使用prototype对象定义的对象属性和方法被所有引用它的实例所继承。因此,在我们的例子中,我们通过prototype属性扩展了VendorEmployee对象并将它们分配给Person实例。这样,无论何时创建VendorEmployee对象的任何实例,它都可以访问Person对象的属性和方法。

还可以通过对象添加属性和方法;例如,我们可以向Vendor对象添加一个属性,如下面的代码所示,但这将变成静态属性,Vendor实例无法访问:

Vendor.id="001";

另一方面,我们也可以向Vendor实例添加属性和方法,但这将仅对该特定实例可用:

var vendorObj = new Vendor("ABC", "US");
vendorObj.id="001";

实现继承的另一种技术是通过将父对象的prototype分配给子对象的prototype对象,如下所示:

Vendor.prototype = Person.prototype; 

使用这种技术,在Person原型中添加的任何方法或属性都将可通过Vendor对象访问:

var Person = function (id, name) {
  this.id = id;
  this.name = name;
}

//Adding method to the Person's prototype to show message
Person.prototype.showMessage = function (message) {
  alert(message);
}

var Vendor = function (companyName, location) {
  this.companyName = companyName;
  this.location = location;
}

//Assigning the parent's prototype to child's prototype
Vendor.prototype = Person.prototype;
var vendorObj = new Vendor("XYZ", "Dubai");
vendorObj.showMessage(vendorObj instanceof Person);

运行此脚本后,它将在警告消息中显示true。这是因为Vendor对象成为Person对象的实例,并且在任何对象中添加的任何方法或属性都可以被两个对象访问。

如果我们修改前面的示例,在将Person原型分配给Vendor原型之后,通过Vendor原型属性添加另一个方法,它将可通过Person对象访问。这是因为,在 JavaScript 中,当子对象的原型被设置为父对象的原型时,在分配后添加到任一对象中的任何方法或属性都将可通过两个对象访问。

让我们在Vendor对象中通过prototype属性添加一个showConsoleMessage()方法,并通过Person实例访问它,如这段代码所示:

var Person = function (id, name) {
  this.id = id;
  this.name = name;
}

//Adding method to the Person's prototype to show message
Person.prototype.showMessage = function (message) {
  alert(message);
}

var Vendor = function (companyName, location) {
  this.companyName = companyName;
  this.location = location;
}

//Assigning the parent's prototype to child's prototype
Vendor.prototype = Person.prototype;

//Adding method to the Vendor's prototype to show at console
Vendor.prototype.showConsoleMessage = function (message) {
  console.log(message);
}

var personObj = new Person("001", "John");
//Person object access the child's object method
personObj.showConsoleMessage("Console");

JavaScript 中的构造函数链

在前面的例子中,我们看到了如何继承对象。然而,如果某个基对象有一些重载构造函数,接受属性将需要一些额外的努力。JavaScript 中的每个函数都有一个call方法,用于将构造函数链接到对象。我们可以使用call方法来链接构造函数并调用基构造函数。因为Person对象接受两个参数,我们将修改Vendor函数和两个属性idnumber,在创建Vendor对象时可以传递这些属性。所以,无论何时创建Vendor对象,都会创建Person对象并填充值:

var Person = function (id, name) {
  this.id = id;
  this.name = name;
}

var Vendor = function (companyName, location, id, name) {
  this.companyName = companyName;
  this.location = location;
  Person.call(this, id, name);
}

var employee = function (employeeType, dateOfJoining, id, name) {
  this.employeeType = employeeType;
  this.dateOfJoining = dateOfJoining;
  Person.call(this, id, name);
}

Vendor.prototype = Person.prototype;
Employee.prototype = Person.prototype;

var vendorObj = new Vendor("ABC", "US", "V-01","Vendor 1");
alert(vendorObj.name);

使用Object.create()继承

使用 ECMAScript 5,你可以通过Object.create()方法轻松地继承你的基对象。这个方法接受两个参数,一个作为原型的对象和一个包含新对象应具有的属性和方法的对象。Object.create()方法改进了基于构造函数的继承。它是一个创建对象而不必通过其构造函数的好方法。让我们看看使用Object.create()方法的VendorEmployee继承Person对象的示例:

var Person = function (id, name) {
  this.id = id;
  this.name = name;
}

var Vendor = function (companyName, location, id, name) {
  this.companyName = companyName;
  this.location = location;
  Person.call(this, id, name);
}

var Employee = function (employeeType, dateOfJoining, id, name) {
  this.employeeType = employeeType;
  this.dateOfJoining = dateOfJoining;
  Person.call(this, id, name);
}

Vendor.prototype = Object.create(Person.prototype);
Employee.prototype = Object.create(Person.prototype);

var vendorObj = new Vendor("ABC", "US", "V-01", "Vendor 1");
alert(vendorObj.name);

在前面的例子中,我们使用了Object.create()来继承Person对象到VendorEmployee对象。无论何时创建VendorEmployee实例,它们都可以访问Person对象的属性。Object.create()方法自动实例化其在call方法中定义的参数的对象实例。

Object.create()的预定义属性

Object.create()方法不会执行Person函数;相反,它只是将Person函数设置为客户函数的原型。下面代码中展示了客户对象的另一种表示形式,包含一个名为CustomerCode的属性:

var customerObj = Object.create(Object.prototype, {
  customerCode: {
    value: "001",
    enumerable: true,
    writable: true,
    configurable: true
  }
});
alert("" + customerObj.customerCode);

在这里,value 代表实际的用户代码值,而enumerablewritableconfigurable是预定义的属性。

使用类定义继承

在前面的章节示例中,我们已经看到了如何使用 ECMAScript 6 定义类。就像 Java 一样,我们可以使用extends关键字来继承一个父类。

使用extends的一个示例在这里展示:

class Person {

  constructor(id, name) {
    this._id = id;
    this._name = name;
  }

  get GetID() {return this._id;}
  get GetName() {return this._name;}
}

class Vendor extends Person {
  constructor(phoneNo, location, id, name){
    super(id, name);
    this._phoneNo = phoneNo;
    this._location = location;

  }
  logToConsole() {
    alert("Person ID is " + this.GetID);
  }
}

var vendorObj = new Vendor("XXX", "US", "V-01", "Vendor 1");
vendorObj.logToConsole();

有了 ECMAScript 6,你可以真正领略到在类中声明静态变量和方法的精髓。让我们看看下面的例子,其中包含一个静态方法logToConsole(),并且从继承Person类的客户类中调用它,而无需在继承后初始化其对象:

class Person {
  static logToConsole() {
    console.log("Hello developers!");
  }
}

class Vendor extends Person {
}

Vendor.logToConsole();

封装

在上面的例子中,Vendor对象不需要知道Person类中logToConsole()方法的实现,并可以使用该方法。除非有特定的原因需要覆盖,否则Vendor类不需要定义这个方法。这称为封装,其中Vendor对象不需要知道logToConsole()方法的实际实现,每个Vendor对象都可以使用这个方法来记录到控制台。就是这样通过封装来完成的,每个类都被封装成一个单一的单元。

抽象

抽象用于隐藏与对象相关的所有信息,除了数据,以减少复杂性并提高效率。这是面向对象编程的核心原则之一。

在 JavaScript 中,没有内置的对抽象的支持,并且它不提供如接口或抽象类之类的类型来创建接口或抽象类以实现抽象。然而,通过某些模式,你可以实现抽象,但这种模式仍然不限制并确保所有抽象方法都被具体类或函数完全实现。

让我们来看一下下面的例子,其中我们有一个person控制器,它接受一个具体对象作为参数,然后调用其具体的实现:

var person = function (id, name) {
  this._id = id;
  this._name = name;
  this.showMessage = function () { };
}
var vendor = function (companyName, location, id, name) {
  this._companyName = companyName;
  this._location = location;
  person.call(this, id, name);
  this.showMessage = function () {
    alert("this is Vendor");
  }
}
var employee = function (employeeType, dateOfJoining, id, name) {
  this._employeeType = employeeType;
  this._dateOfJoining = dateOfJoining;
  person.call(this, id, name);
  this.showMessage = function () {
    alert("this is Employee");
  }
}
vendor.prototype = Object.create(person.prototype);
employee.prototype = Object.create(person.prototype);
var personController = function (person) {
  this.personObj = person;
  this.showMessage = function () {
    this.personObj.showMessage();
  }
}

var v1 = new vendor("ABC", "USA", "V-01", "Vendor 1");
var p1 = new personController(v1);
p1.showMessage();

另外,借助 ECMAScript 6,我们可以实现同样的场景,如下面的代码所示:

class person {
  constructor(id, name) {
    this._id = id;
    this._name = name;
  }
  showMessage() { };
}
class vendor extends person {
  constructor(companyName, location, id, name) {
    super(id, name);
    this._companyName = companyName;
    this._location = location;

  }
  showMessage() {
    alert("this is Vendor");
  }
}
class employee extends person {
  constructor(employeeType, dateOfJoining, id, name) {
    super(id, name);
    this._employeeType = employeeType;
    this._dateOfJoining = dateOfJoining;
  }
  showMessage() {
    alert("this is Employee");
  }
}
class personController {
  constructor(person) {
    this.personObj = person;
  }
  showMessage() {
    this.personObj.showMessage();
  }
}

var v1 = new vendor("ABC", "USA", "V-01", "Vendor 1");
var p1 = new personController(v1);
p1.showMessage();

new.target

new.target属性用于检测函数或类是否使用new关键字调用。如果调用,它将返回对函数或类的引用,否则为null。考虑上面例子中的例子,我们可以通过使用new.target来限制创建personcall对象:

class person {
  constructor(id, name) {
    if(new.target === person){
      throw new TypeError("Cannot create an instance of Person class as its abstract in nature");
    }
    this._id = id;
    this._name = name;
  }

  showMessage() { };
}

命名空间

ECMAScript 6 通过模块引入了命名空间,并使用exportimport关键字,但它们仍然处于草案阶段,到目前为止没有实现。

然而,在早期版本中,可以通过局部对象来模拟命名空间。例如,下面是定义一个表示命名空间的局部对象的语法,我们可以在其中添加函数和对象:

var BusinessLayer = BusinessLayer || {};

我们可以在上面显示的代码中添加函数:

BusinessLayer.PersonManager = function(){
};

此外,还可以定义更多嵌套的命名空间层次,如下面的代码所示:

var BusinessLayer = BusinessLayer || {};
var BusinessLayer.Managers = BusinessLayer.Managers || {};

异常处理

JavaScript 正在成为开发大型应用程序的强大平台,异常处理在处理程序中的异常和按需传播它们方面发挥着重要作用。就像 C#或其他任何编程语言一样,JavaScript 提供了trycatchfinally关键字来注解用于错误处理的代码。JavaScript 提供了使用嵌套的try catch语句和条件在catch块中处理不同条件的相同方式。

当一个异常发生时,会创建一个代表所抛出错误的对象。就像 C#一样,我们有不同类型的异常,如InvalidOperationExceptionArgumentExceptionNullExceptionException。JavaScript 提供六种错误类型,如下所示:

  • Error

  • RangeError

  • ReferenceError

  • SyntaxError

  • TypeError

  • URIError

Error

Error对象代表通用异常,主要用于返回用户定义的异常。一个Error对象包含两个属性,分别是 name 和 message。Name 返回错误类型,message 返回实际错误信息。我们可以抛出错误异常,如下所示:

try{ }catch{throw new Error("Some error occurred");}

RangeError

如果任何数字的范围被超出,将抛出RangeError异常。例如,创建一个负长度的数组将抛出RangeError

var arr= new Array(-1);

ReferenceError

ReferenceError异常发生在访问一个不存在的对象或变量时;例如,以下代码将抛出一个ReferenceError异常:

function doWork(){
  arr[0]=1;
}

SyntaxError

正如名称所示,如果 JavaScript 代码中存在任何语法问题,就会抛出SyntaxError。所以,如果有些闭合括号缺失,循环结构不正确,等等,这都将归类为SyntaxError

类型错误

当一个值不是期望的类型时,会发生TypeError异常。以下代码抛出一个TypeError异常,因为对象试图调用一个不存在的函数:

var person ={};
person.saveRecord();

URIError

encodeURI()decodeURI()中指定了一个无效的 URI 时,会发生URIError异常。以下代码抛出此错误:

encodeURIComponent("-");

闭包

闭包是 JavaScript 最强大的特性之一。闭包提供了一种暴露位于其他函数体内内部函数的方式。当一个内部函数被暴露在包含它的函数外部,并且在外部函数执行后可以执行,并且可以使用外部函数调用时的相同局部变量、参数和函数声明时,一个函数可以被称为闭包。

让我们来看一下以下示例:

function Incrementor() {
  var x = 0;
  return function () {
    x++;
    console.log(x);
  }
}

var inc= Incrementor();
inc();
inc();
inc();

这是一个简单的闭包示例,其中inc()成为引用内部函数的闭包,该内部函数增加外层函数中定义的x变量。x变量将在每次调用时增加,最后调用的值为3

闭包是一种特殊类型的对象,它将函数和函数创建的环境结合起来。所以,多次调用它将使用相同的环境,以及在之前调用中更新的值。

让我们来看另一个示例,其中有一个表格生成函数,它接受一个表格号并返回一个函数,该函数可用于获取任何数字与提供的表格号相乘的结果:

function tableGen(number) {
  var x = number;
  return function (multiplier) {
    var res = x * multiplier;
    console.log(x +" * "+ multiplier +" = "+ res);
  }
}

var twotable = tableGen(2);
var threetable = tableGen(3);

twotable(5);
threetable(6);

调用twotable()threetable()方法后的结果值将是1018。这是因为twoTable()函数对象是通过将2作为参数传递给tableGen()函数进行初始化的。当通过twoTable()threetable()方法调用执行时,这个tableGen()函数将传递的参数值存储在x变量中,并将其与第二次调用传递的变量相乘。

因此,twoTable(5)函数调用的输出将是10,如下所示:

闭包

第二条语句threeTable(6)的输出将是18,如下所示:

闭包

实际使用

我们已经了解了闭包是什么以及我们如何实现它们。然而,让我们考虑它们的实际影响。闭包让你可以将某些环境与在那种环境或数据中操作的函数相关联。

在 JavaScript 中,函数大多在发生任何事件或用户执行任何操作时执行。让我们看看以下闭包在consoledialog窗口上实际使用示例,以记录消息:

<body>
  <input type="text" id="txtMessage" />
  <button id="consoleLogger"> Log to Console </button>
  <button id="dialogLogger">Log to Dialog </button>
  <script>

    function getLogger(loggerType) {
      return function () {
        var message = document.getElementById("txtMessage").value;
        if (loggerType == "console")
        console.log(message);
        else if (loggerType == "dialog")
        alert(message);
      }
    }
    var consoleLogger = getLogger("console");
    var dialogLogger = getLogger("dialog");
    document.getElementById("consoleLogger").onclick = consoleLogger;
    document.getElementById("dialogLogger").onclick = dialogLogger;
  </script>
</body>

在前面的示例中,我们有两个日志闭包:一个记录到控制台,另一个记录到弹出对话窗口。我们可以初始化这些闭包,并在程序中使用它们来记录消息。

JavaScript 类型数组

客户端开发在 JavaScript 已经成为一个强大的平台,并且有一些 API 和库可供使用,允许你与媒体文件、Web 套接字等进行交互,并在二进制中处理数据。当处理二进制数据时,需要将其保存在其特定的格式中。这时就轮到类型数组发挥作用了,它允许开发者在原始二进制格式中操纵数据。

类型数组架构

类型数组将数据分为两部分,即缓冲区和视图。缓冲区包含二进制中的实际数据,但没有视图无法访问。视图提供了有关缓冲区的实际元数据信息和上下文,例如数据类型、起始偏移量和元素数量。

数组缓冲区

数组缓冲区是一种用于表示二进制数据的数据类型。在它被分配给一个视图之前,其内容无法被操纵。视图以特定格式表示缓冲区,并对数据执行操作。

有不同类型的类型数组视图,如下所示:

类型 字节大小 描述
Int8Array 1 这是一个 8 位有符号整数数组。
UInt8Array 1 这是一个 8 位无符号整数数组。
Int16Array 2 这是一个 16 位有符号整数数组。
UInt16Array 2 这是一个 16 位无符号整数数组。
Int32Array 4 这是一个 32 位有符号整数数组。
UInt32Array 4 这是一个 32 位无符号整数数组。
Float32Array 4 这是一个 32 位 IEEE 浮点数数组。
Float64Array 8 此数组是 64 位的 IEEE 浮点数。
UInt8ClampedArray 1 此数组是 8 位无符号整数(夹紧)。

接下来,让我们通过一个示例来看看我们如何通过视图在缓冲区中存储数据并操作它。

创建缓冲区

首先,我们需要创建一个缓冲区,如下面的代码所示:

var buffer = new ArrayBuffer(32);

上述声明分配了 32 字节的内存。现在我们可以使用任意一种类型数组视图来操作它:

var int32View= new Int32Array(buffer);

最后,我们可以像这样访问字段:

for(var i=0;i< int32View.length; i++){
  int32View[i] = i;
}

这段代码将在视图中进行八个条目的操作,从07。输出将如下所示:

0 1 2 3 4 5 6 7

同一个缓冲区也可以使用其他视图类型进行操作。例如,如果我们想要用一个 16 位数组视图读取已填充的缓冲区,结果将像这样:

var Int16View =new Int16Array(buffer);
for(var i=0;i< int16View.length;i++){
  console.log(int16View[0]);
}

输出将如下所示:

0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0

这就是我们如何可以轻松地使用多种不同类型的视图来操作单个缓冲区数据,并与包含多种数据类型的数据对象交互。

映射、集合、弱映射和弱集合

映射(Maps)、弱映射(weak maps)、集合(sets)和弱集合(weak sets)都是代表集合的对象。映射是键值对的键 ed 集合,而集合存储任何类型的唯一值。我们将在接下来的章节中讨论它们每一个。

映射和弱映射

Map对象提供了一个简单的键/值映射,并且根据插入的顺序进行迭代。首先插入的值将被首先检索。弱映射是不可枚举的,仅保存对象类型。在弱映射中不允许有原始类型,每个键代表一个对象。让我们看看以下使用映射作为货币的示例:

var currencies = new Map();
currencies.set("US", "US Dollar");
currencies.set("UK", "British Pound");
currencies.set("CA", "Canadian Dollar");
currencies.set("PK", "Rupee");
currencies.set("UAE", "Dirham");
for (var currency of currencies) {
  console.log(currency[0] + " currency is " + currency[1]);
}

Map对象上可用的其他属性和方法如下所示:

currencies.get("UAE"); // returns dirham
currencies.size; // returns 5 
currencies.has("PK") // returns true if found 
currencies.delete("CA") // delete Canada from the list

弱映射(weak maps)中保存的是对象,其键被表示为弱键(weak keys)。这是因为如果一个弱映射值中存储的对象没有被引用,并且在垃圾回收(garbage collection)时被回收,那么这个键就会变成弱键。它通常被用来存储对象的私有数据或者隐藏实现细节。

在上一节中,我们了解到实例级别和原型级别上暴露的都是公共的(public)。下面是一个实际例子,包含了一个用于验证来自 Twitter 账户用户的函数:对于开放认证OAuth),Twitter 需要两个密钥:消费者 API 密钥和一个密钥秘密。我们不想暴露这些信息并让用户更改。因此,我们使用弱映射来保存这些信息,然后在prototype函数中检索它来验证用户:

var authenticatorsecrets = new WeakMap();

function TwitterAuthenticator() {
  const loginSecret = {
    apikey: 'testtwitterapikey',
    secretkey: 'testtwittersecretkey'
  };
  authenticatorsecrets.set(this, loginSecret);
}

TwitterAuthenticator.prototype.Authenticate = function () {
  const loginSecretVal = authenticatorsecrets(this);
  //to do authenticate with twitter
};

集合和弱集合

集合是值的集合,每个值应该是唯一的。所以,例如,如果你在任何索引上已经有了一个值1,已经定义,你不能将它插入到同一个集合实例中。

集合是无类型的,你可以放入任何数据,不考虑任何数据类型:

var set = new Set();
set.add(1);
set.add("Hello World");
set.add(3.4);
set.add(new Date());

另一方面,弱集合是独特对象的集合,而不是任意类型的任意值。就像弱映射一样,如果没有其他对存储的对象的引用,它将被处置并回收。与弱映射类似,它们是不可枚举的:

var no = { id: 1 };
var abc = { alphabets: ['a', 'b', 'c'] };

var x = new WeakSet();
x.add(no);
x.add(abc);

严格模式

strict模式是 ECMAScript 5 中引入的字面表达式。它用于编写安全的 JavaScript,并在脚本中出现任何小错误时抛出错误,而不会忽视它们。其次,它的运行速度比普通 JavaScript 代码快,因为它有时会修复错误,这有助于 JavaScript 引擎进行优化,使您的代码运行得更快。

我们可以在全局脚本级别或函数级别调用strict模式:

"use strict;"

例如,在以下代码中,它会抛出错误,因为x变量未定义:

"use strict";
x=100;
function execute(){
  "use strict;"
  x=100;
}

对于较大的应用程序,使用strict模式是一个更好的选择,如果缺少或不定义某些内容,它会抛出错误。以下表格显示了使用strict模式会导致错误的一些场景:

Code Error 原因
x=100; 这段代码中变量未声明。
x= {id:1, name:'ABC'}; 这段代码中对象变量未声明。
function(x,x){} 在此代码中参数名称重复导致了错误。
var x = 0001 这段代码中使用了八进制数字字面量。
var x=\0001 转义是不允许的,因此发生了错误。
var x = {get val() {return 'A'}}; x.val = 'B' 在此代码中,向get值写入导致了错误。
delete obj.prototype; 删除对象原型是不允许的,因此发生了错误。
var x= 2; delete x; 删除变量是不允许的,因此发生了错误。

此外,还有一些保留关键字,如argumentsevalimplementsinterfaceletpackageprivateprotectedpublicstaticyield,也是不允许的。

总结

在本章中,我们学习了 JavaScript 的一些高级概念,如提升的变量及其作用域、属性描述符、面向对象编程、闭包、类型数组以存储数据类型,以及异常处理。在下一章中,我们将学习最广泛使用的库 jQuery,以非常简单和容易的方式进行 DOM 遍历和操作、事件处理等。

第三章:使用 jQuery 在 ASP.NET 中

我们将从这个章节开始,先对 jQuery 作一个简短的介绍。jQuery 是一个 JavaScript 库,旨在通过编写更少的代码来提供更好的开发体验和更快的编程体验,与纯 JavaScript 相比,它可以更快地执行复杂操作。然而,当编写特定原因的自定义脚本时,JavaScript 仍然存在。因此,jQuery 可以帮助你进行 DOM 操作,根据类、元素名称等选择元素,并提供一个更好的事件处理模型,使开发者在他们的日常项目中使用更为简单。

与 JavaScript 相比,另一个优点是跨浏览器问题。它提供了跨浏览器的 consistent behavior。另一方面,每个浏览器对 JavaScript 的实现都不一样。此外,为了在 JavaScript 中处理跨浏览器问题,开发者倾向于编写一些条件逻辑来检查 JavaScript 正在运行的浏览器版本并相应地处理;而 jQuery 处理了浏览器的所有重活,并提供了 consistent behavior。

在本章中,我们将讨论 jQuery 的一些强大功能,如下:

  • 使用选择器

  • 操作 DOM 元素

  • 处理事件

开始使用 jQuery

jQuery 库可以从jquery.com下载。jQuery 的最新版本是 3.0.0,如果你目标是现代浏览器,例如,IE 9 和 Microsoft Edge 支持这个版本,你可以使用这个库。对于较旧版本—例如,IE 6-8—你可以下载 jQuery 1.x。

一旦 jQuery 被下载,你可以将其添加到你的项目中并引用,如下所示:

<head>
  <script src="img/jquery.js"></script>
</head>
<body>
</body>

使用内容交付网络

Instead of loading jQuery from your server, we can also load it from some other server, such as the Microsoft server or Google server. These servers are called the content delivery network (CDN) and they can be referenced as shown here:

  • 引用微软 CDN:

    <script src="img/jquery-2.0.js">
    </script>
    
  • 引用谷歌 CDN:

    <script src="img/jquery.min.js"></script>
    

使用 CDN

实际上,这些 CDN 非常普遍,大多数网站已经在使用它们。当运行任何引用 CDN 的应用程序时,有可能其他网站也使用了微软或谷歌的同一个 CDN,相同的文件可能会在客户端缓存。这提高了页面渲染性能。另外,再次从本地服务器下载 jQuery 库时,使用的是 CDN 的缓存版本。而且,微软和谷歌提供了不同地区的服务器,用户在使用 CDN 时也能获得一些速度上的好处。

然而,有时 CDN 可能会宕机,在这种情况下,你可能需要参考并从你自己的服务器下载脚本。为了处理这种场景,我们可以指定回退 URL,它检测是否已经从 CDN 下载;否则,它从本地服务器下载。我们可以使用以下脚本来指定回退:

<script src="img/jquery.min.js"></script>

<script>if (!window.jQuery) { document.write('<script src="img/jquery"><\/script>'); }
</script>

window.jQuery 实例告诉我们 jQuery 是否已加载;否则,它在 DOM 上写入脚本,指向本地服务器。

或者,在 ASP.NET Core 中,我们可以使用 asp-fallback-src 属性来指定回退 URL。ASP.NET Core 1.0 提供了一系列广泛的标签助手。与 HTML 助手相比,这些助手可以通过向页面元素添加 HTML 属性来使用,并为开发者提供与编写前端代码相同的体验。

在 ASP.NET 中可以用一种简单的方式编写代码来处理回退场景:

<script src="img/jquery-2.1.4.min.js"
  asp-fallback-src="img/jquery.min.js"
  asp-fallback-test="window.jQuery">
</script>

在 ASP.NET Core 中,还有一个标签助手 <environment>,可以用来根据 launchSettings.json 文件中设置的当前环境加载脚本:

CDN 的使用

根据项目配置文件中设置的当前环境,我们可以加载脚本来满足调试和生产场景的需求。例如,在生产环境中,最好指定 JavaScript 库的压缩版本,因为它移除了所有空白字符并将变量重命名为更紧凑的尺寸,以便快速加载。然而,就开发体验而言,标准的非压缩版本对于调试目的来说要好得多。因此,我们可以使用以下代码所示的环境标签助手,在开发应用程序时加载生产环境和标准版本:

<environment names="Development">
  <script src="img/jquery.js"></script>
  <script src="img/bootstrap.js"></script>
  <script src="img/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
  <script src="img/jquery-2.1.4.min.js"
    asp-fallback-src="img/jquery.min.js"
    asp-fallback-test="window.jQuery">
  </script>
  <script src="img/bootstrap.min.js"
    asp-fallback-src="img/bootstrap.min.js"
    asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
  </script>
  <script src="img/site.min.js" asp-append-version="true"></script>
</environment>

文档就绪事件

jQuery 库可以通过一个 $ 符号或者简单地写 jQuery 来访问。然而,最好是由开发者使用美元符号访问。它还提供了一种在 DOM 层次结构完全加载时捕获事件的方法。这意味着一旦 DOM 结构加载完成,你可以捕获这个事件来执行不同的操作,如将 CSS 类与控件关联和操作控件值。当页面加载时,DOM 层次结构不依赖于图像或 CSS 文件,并且无论图像或 CSS 文件是否下载,document ready 事件都会并行触发。

我们可以使用文档就绪事件,如这段代码所示:

<html>
  <head>
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        console.log("Document is lo	aded");
      });
    </script>
  </head>
</html>

如前所述的代码解释,$ 是访问 jQuery 对象的方式。它需要一个作为参数传递的 document 对象,而 ready 则是检查一旦文档对象模型层次结构完全加载。最后,它接受一个匿名函数,我们可以在其中编写所需的操作。在前面的例子中,当 DOM 层次结构加载时,我们只是显示一个简单的文本消息。

jQuery 选择器

对于 DOM 操作,jQuery 选择器起着重要作用,并提供了一种更简单、易行的一行方法来选择 DOM 中的任何元素并操作其值和属性,例如,使用 jQuery 选择器更容易搜索具有特定 CSS 类的元素列表。

jQuery 选择器可以用美元符号和括号来书写。我们可以使用 jQuery 选择器根据元素的 ID、标签名、类、属性值和输入节点来选择元素。在下一节中,我们将逐一通过实际例子来看这些元素。

通过 ID 选择 DOM 元素

以下示例展示了选择具有 ID 的div元素的方法:

<!DOCTYPE html>
<html>
  <head>
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        $('#mainDiv').html("<h1>Hello World</h1>");

      });
    </script>
  </head>  
  <body>
    <div id="mainDiv">

    </div>
  </body>
</html>

选择元素后,我们可以调用各种方法来设置值。在给定示例中,我们调用了html()方法,该方法接受html字符串并设置第一个标题为Hello World。另一方面,可以通过调用此代码来检索html内容:

<script>
  $(document).ready(function () {
    var htmlString= $('#mainDiv').html();

  });
</script>

通过 TagName 选择 DOM 元素

在 JavaScript 中,我们可以通过调用document.getElementsByTagName()来检索 DOM 元素。这个函数返回与标签名匹配的元素数组。在 jQuery 中,这种方式可以更简单实现,并且语法相当简单。

考虑以下示例:

$('div') //returns all the div elements 

让我们通过以下示例来阐明我们的理解:

<!DOCTYPE html>
<html>
  <head>
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        $('div').css('text-align, 'left');
      });
    </script>
  </head>  
  <body>
    <div id="headerDiv">
      <h1>Header</h1>
    </div>
    <div id="mainDiv">
      <p>Main</p>
    </div>
    <div id="footerDiv">
      <footer>Footer</footer>
    </div>
  </body>
</html>

之前的示例将所有div子控件的对齐设置为左对齐。如果你注意这里,我们并没有必要遍历所有的div控件来设置背景颜色,而且样式已经应用于all。然而,在某些情况下,你可能需要根据每个元素的索引设置不同的值,这可以通过在div上使用each()函数来实现。例如,下面的脚本展示了如何使用each函数为每个div控件分配一个index值作为html字符串:

<script>
  $(document).ready(function () {
    $('div').each(function (index, element) {
      $(element).html(index);
    });
  });
</script>

每个函数都带有一个参数,该参数是一个带有索引和元素的函数。我们可以使用美元符号访问每个元素,如前代码所示,并通过调用html方法将索引设置为内容。输出将类似于以下屏幕截图:

通过 TagName 选择 DOM 元素

让我们来看另一个示例,它将在控制台窗口中显示每个div控件的内容。在这里,each()函数不需要参数,每个循环中的项目可以通过this关键字访问:

<!DOCTYPE html>
<html>
  <head>
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        $('div').each(function () {
          alert($(this).html());
        });
      });
    </script>
  </head>  
  <body>
    <div id="headerDiv">
      <h1>Demo </h1>
    </div>
    <div id="mainDiv">
      <p>This is a demo of using jQuery for selecting elements</p>
    </div>
    <div id="footerDiv">
      <footer> Copyright - JavaScript for .Net Developers </footer>
    </div>
  </body>
</html>

输出如下:

通过 TagName 选择 DOM 元素

还有其他各种方法可供使用,您可以在 jQuery 文档中查阅。因此,使用选择器,我们可以更快、更高效地搜索 DOM 中的任何元素。

另一个例子是使用标签名选择多个元素,如下所示。

<html>
  <head>
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        $('div, h1, p, footer').each(function () {
          console.log($(this).html());
        });
      });
    </script>
  </head>  
  <body>
    <div id="headerDiv">
      <h1>Demo </h1>
    </div>
    <div id="mainDiv">
      <p>This is a demo of using jQuery for selecting elements</p>
    </div>
    <div id="footerDiv">
      <footer> Copyright - JavaScript for .Net Developers </footer>
    </div>
  </body>
</html>
bootstrap theme and apply different classes to the buttons. With the help of the class name selector, we can select controls and update the class name. The following example will return two elements based on the selection criteria specified:
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="Content/bootstrap.css" />
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        var lst = $('.btn-primary');
        alert(lst.length);
      });
    </script>
  </head>  
  <body>
    <div class="container">
      <p></p>
      <button type="button" class="btn btn-primary active">Edit </button>
      <button type="button" class="btn btn-primary disabled">Save</button>
      <button type="button" class="btn btn-danger" value="Cancel">Cancel</button>
    </div>
  </body>
</html>

与访问类名不同,我们可以通过在点号和类名之前指定标签名来限制搜索。您可以使用$('button.active')来查找所有激活的按钮。

通过属性值选择

在某些情况下,您可能需要根据属性或其值来选择元素。jQuery 库提供了一种非常简洁的方式来根据属性及其值搜索元素。

使用此选择器的语法是先指定元素名称,然后是包含属性名称和值的方括号,这是可选的:

$(elementName[attributeName=value])

例如,以下代码选择所有具有type属性的元素:

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="Content/bootstrap.css" />
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        var lst = $('input[type]');
        console.log(lst.length);
      });
    </script>
  </head>  
  <body>

    <div class="container">
      <p></p>
      <input type="text" value="hello world" />
      <input type="text" value="this is a demo" />
      <input type="button" value="Save" />
    </div>
  </body>
</html>

在这个例子中,我们有三个具有type属性的输入控件。所以,结果将是3。同样,如果您想搜索具有等于hello world的值的元素,我们可以使用以下代码:

<script>
  $(document).ready(function () {
    var lst = $('input[value="hello world"]');
    alert(lst.length);
  });
</script>

需要注意的是,属性值是大小写敏感的,因此,在使用此表达式时,您应该考虑属性值的确切大小写。然而,还有其他方法,那就是使用^来搜索包含、开始或结束特定文本的值。

让我们来看一个基于搜索以表达式开始的值的alert例子:

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="Content/bootstrap.css" />
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        var lst = $('input[value^="Pr"]');
        alert(lst.length);
      });
    </script>
  </head>
  <body>

    <div class="container">
      <p></p>
      <input type="text" value="Product 1" />
      <input type="text" value="This is a description" />
      <input type="button" value="Process" />
    </div>
  </body>
</html>

另一方面,我们也可以使用$符号来搜索以文本结尾的值。以下是搜索以1结尾的文本的代码:

<script>
  $(document).ready(function () {
    var lst = $('input[value$="1"]');
    alert(lst.length);
  });
</script>

最后,搜索包含某些文本的文本可以使用*实现,以下是运行此例子的代码:

<script>
  $(document).ready(function () {
    var lst = $('input[value*="ro"]');
    alert(lst.length);
  });
</script>

选择输入元素

HTML 中的输入控件有很多不同的控件。textareabuttoninputselectimageradio等控件都是输入控件。这些控件通常用于基于表单的应用程序中。因此,jQuery 专门提供了基于不同标准的输入控件的选择选项。

这个选择器以美元符号和input关键词开头,后跟属性和值:

$(':input[attributeName=value]);

然而,在上一节中,我们已经看到了如何搜索具有属性名称和值的任何元素。所以,如果我们想要搜索所有类型等于文本的输入控件,这是可以实现的。

这个选择器在某些场景下性能效率较低,它搜索出所有输入组中的控件,并找到属性及其值;然而,这个选择器只会搜索输入控件。在编写程序时,如果有什么东西专门针对输入控件属性,使用这种方法是一个更好的选择。

让我们来看一个在 ASP.NET Core MVC 6 中的例子,该例子在文档完全加载后应用 CSS 属性:

@model WebApplication.ViewModels.Book.BookViewModel
@{
  ViewData["Title"] = "View";
}
<script src="img/jquery-1.12.0.min.js"></script>
<script>
  $(document).ready(function () {
    $(':input').each(function () {
      $(this).css({ 'color': 'darkred', 'background-color': 'ivory', 'font-weight': 'bold' });    });
  });
</script>
<form asp-action="View" class="container">
  <br />
  <div class="form-horizontal">
    <div class="form-group">
      <label asp-for="Name" class="col-md-2 control-label"></label>
      <div class="col-md-10">
        <input asp-for="Name" class="form-control" />
        <span asp-validation-for="Name" class="text-danger" />
      </div>
    </div>
    <div asp-validation-summary="ValidationSummary.ModelOnly" class="text-danger"></div>
    <div class="form-group">
      <label asp-for="Description" class="col-md-2 control-label"></label>
      <div class="col-md-10">
        <textarea asp-for="Description" class="form-control" ></textarea>
        <span asp-validation-for="Description" class="text-danger" />
      </div>
    </div>
    <div class="form-group">
      <div class="col-md-offset-2 col-md-10">
        <input type="submit" value="Save" class="btn btn-primary" />
      </div>
    </div>
  </div>
</form>

<div>
  <a asp-action="Index">Back to List</a>
</div>

选择所有元素

jQuery 库为您提供了一个特殊的选择器,它能够获取 DOM 中定义的所有元素的集合。除了标准控件之外,它还会返回诸如<html><head><body><link><script>之类的元素。

获取所有元素语法是$("*"),下面的例子在浏览器的控制台中列出了 DOM 的所有元素:

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="Content/bootstrap.css" />
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        $("*").each(function () {
          console.log($(this).prop('nodeName'));
        });    
      });
    </script>
  </head>  
  <body>
    <form class="container">
      <div class="form-group">
        <label>Name</label>
        <input type="text" class="form-control"/>
      </div>
    </form>  
  </body>
</html>

在前面的代码中,我们使用了prop方法,该方法需要属性名来显示元素名称。在这里,prop方法可以使用tagNamenodeName来显示名称类型。最后,在浏览器的控制台中,将显示一个登录页面,如下所示:

选择所有元素

选择第一个和最后一个子元素

jQuery 库提供了特殊的选择器来选择它们父元素的所有第一个或最后一个元素。

选择所有父元素的第一个子元素的语法如下:

$(elementName:first-child);

选择所有父元素的最后一个子元素的语法如下:

$(elementName:last-child);

下面的例子向您展示了更改选择选项的第一个和最后一个孩子的字体样式的方法:

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="Content/bootstrap.css" />
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        $('option:first-child').css('font-style', 'italic');
        $('option:last-child').css('font-style', 'italic');
        alert(lst.length);
      });
    </script>
  </head>
  <body>
    <select>
      <option>--select--</option>
      <option>USA</option>
      <option>UK</option>
      <option>Canada</option>
      <option>N/A</option>
    </select>
  </body>
</html>

输出结果如下:

选择第一个和最后一个子元素

jQuery 中的包含选择器

contains选择器用于查找 HTML 容器元素中的文本,如<div><p>。这个选择器搜索特定类型的所有元素,并找到传递给contains()函数的参数的文本。下面显示了包含div元素文本的代码示例。这个选择器区分大小写,因此在搜索时请确保大小写正确。

下面的代码将显示一个带有值2的警告框,因为它找到了两个包含文本demodiv元素:

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="Content/bootstrap.css" />
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        var lst = $('div:contains("demo")');
        alert(lst.length);
      });
    </script>

  </head>
  <body>
    <div>
      This is a sample demo for contains selector
    </div>
    <div>
      Demo of the selector 
    </div>
    <div>
      Sample demo
    </div>
  </body>
</html>

选择偶数行和奇数行的选择器

这类选择器适用于表格中的行,通常用于通过将每行奇数行的颜色改变为灰色,使其看起来更像网格。我们可以使用以下语法类型的选择器:

$('tr:even');
$('tr:odd');

让我们来看一个将表格中所有行颜色改为灰色的例子:

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="Content/bootstrap.css" />
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        $('tr:odd').css('background-color', 'grey');
      });
    </script>

  </head>
  <body>
    <table>
      <thead>
        <tr><th>Product Name</th><th>Description</th><th>Price</th></tr>
      </thead>
      <tbody>
        <tr><td>Product 1</td><td>This is Product 1</td><td>$100</td></tr>
        <tr><td>Product 2</td><td>This is Product 2</td><td>$500</td></tr>
        <tr><td>Product 3</td><td>This is Product 3</td><td>$330</td></tr>
        <tr><td>Product 4</td><td>This is Product 4</td><td>$50</td></tr>
        <tr><td>Product 5</td><td>This is Product 5</td><td>$1000</td></tr>
        <tr><td>Product 6</td><td>This is Product 6</td><td>$110</td></tr>
        <tr><td>Product 7</td><td>This is Product 7</td><td>$130</td></tr>
        <tr><td>Product 8</td><td>This is Product 8</td><td>$160</td></tr>
        <tr><td>Product 9</td><td>This is Product 9</td><td>$20</td></tr>
        <tr><td>Product 10</td><td>This is Product 10</td><td>$200</td></tr>
      </tbody>
    </table>
  </body>
</html>

操作 DOM

在本文档的这一部分,我们将通过 jQuery 方法看到一些操作 DOM 的例子。jQuery 库提供了一个广泛的库,可以对 DOM 元素执行不同的操作。我们可以轻松地修改元素属性、应用样式,以及遍历不同的节点和属性。我们在上一节中已经看到了一些例子,这一节将专门关注 DOM 操作。

修改元素的属性

当使用客户端脚本语言时,修改元素属性和读取它们是一项基本任务。使用普通的 JavaScript,这可以通过编写几行代码来实现;然而,使用 jQuery,可以更快、更优雅地实现。

选定要修改的元素的任何属性都可以通过前面章节列出的各种选项来完成。下表中列出的每个属性都提供了getset选项,设置时需要参数,而读取时不需要参数。

在 jQuery 中,有一些可用于修改元素的常见方法,例如htmlvalue等。要了解更多信息,可以参考api.jquery.com/category/manipulation/

获取方法 设置方法 描述
.val() .val('any value') 这个方法用于读取或写入 DOM 元素的任何值。
.html() .html('any html string') 这个方法用于读取或写入 DOM 元素的任何 HTML 内容。
.text() .text('any text') 这个方法用于读取或写入文本内容。在这个方法中不会返回 HTML。
.width() .width('any value') 这个方法用于更新任何元素的宽度。
.height() .height('any value') 这个方法用于读取或修改任何元素的高度。
.attr() .attr('attributename', 'value') 这个方法用于读取或修改特定元素属性的值。
.prop() .prop() 这个方法与attr()相同,但在处理返回当前状态的value属性时更高效。例如,attr()复选框提供默认值,而prop()给出当前状态,即truefalse
.css('style-property') .css({'style-property1': value1, 'style-property2': value2, 'style-propertyn':valueN } 这个方法用于设置特定元素的样式属性,如字体大小、字体家族和宽度。

让我们来看一下下面的例子,它使用了html()text()css()修饰符,并使用htmltextincreaseFontSize更新了p元素:

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="Content/bootstrap.css" />
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      function updateHtml() {
        $('p').html($('#txtHtml').val());
      }

      function updateText() {
        $('p').text($('#txtText').val());
      }

      function increaseFontSize() {
        var fontSize = parseInt($('p').css('font-size'));
        var fontSize = fontSize + 1 +"px";
        $('p').css({'font-size': fontSize});
      }
    </script>
  </head>
  <body >
    <form class="form-control">
      <div class="form-group">
        <p>this is a book for JavaScript for .Net Developers</p>

      </div>
      <div class="form-group">
        Enter HTML: <input type="text" id="txtHtml" />
        <button onclick="updateHtml()">Update Html</button>
      </div>
      <div class="form-group">
        Update Text: <input type="text" id="txtText" />
        <button onclick="updateText()">Update Text</button>
      </div>
      <div class="form-group">
        <button onclick="increaseFontSize()">Increase Font Size</button>
      </div>
    </form>
  </body>
</html>

前面 HTML 代码的结果如下:

修改元素的属性

你可以通过点击更新 Html按钮来更新 HTML,通过点击更新文本按钮来更新纯文本:

修改元素的属性

最后,可以通过点击增加字体大小按钮来增加字体大小:

修改元素的属性

创建新元素

jQuery 库提供了一种创建新元素的智慧方式。可以使用相同的$()方法并传递html作为参数来创建元素。创建元素后,除非将其添加到 DOM 中,否则它无法显示。有各种方法可用于附加、插入后或插入前任何元素等。下面表格展示了用于将新元素添加到 DOM 的所有方法:

获取方法 描述
.append() 此方法用于向调用它的元素中插入 HTML 内容
.appendTo() 此方法用于将每个元素插入到调用它的末尾
.before() 此方法用于在调用它的元素之前插入 HTML 内容
.after() 此方法用于在调用它的元素之后插入 HTML 内容
.insertAfter() 此方法用于在调用它的每个元素之后插入 HTML 内容
.insertBefore() 此方法用于在调用它的每个元素之前插入 HTML 内容
.prepend() 此方法用于在调用它的元素的起始位置插入 HTML 内容
.prepend() 此方法用于向每个元素的开始位置插入 HTML 内容

以下示例创建了一个包含两个字段(NameDescription)和一个按钮来保存这些值表单:

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="Content/bootstrap.css" />
    <script src="img/jquery-1.12.0.min.js"></script>
    <script>
      $(document).ready(function () {
        var formControl = $("<form id='frm' class='container' ></form>");
        $('body').append(formControl);
        var nameDiv = $("<div class='form-group'><label id='lblName'>Enter Name: </label> <input type='text' id='txtName' class='form-control' /></div>");
        var descDiv = $("<div class='form-group'><label id='lblDesc'>Enter Description: </label> <textarea class='form-control' type='text' id='txtDescription' /></div>");
        var btnSave = $("<button class='btn btn-primary'>Save</button>")
        formControl.append(nameDiv);
        formControl.append(descDiv);
        formControl.append(btnSave);      
      });
      </script>
    </head>       
  <body>
  </body>
</html>

这段代码将产生以下输出:

创建新元素

删除元素和属性

在使用不同的方法来创建和渲染 DOM 中的元素时,jQuery 还提供了一些用于从 DOM 中删除元素的方法。以下表格是我们可以用来删除特定元素、一组元素或所有子节点的方法的列表:

方法 描述
.empty() 此方法从元素中移除内部 HTML 代码
.detach() 此方法从 DOM 中删除一组匹配的元素
.remove() 此方法从 DOM 中删除一组匹配的元素
.removeAttr() 此方法从元素中移除特定的属性
.removeClass() 此方法从元素中移除一个类
.removeProp() 此方法从元素中移除一个属性

remove()detach()的区别在于,remove永久性地从 DOM 中删除内容;这意味着如果元素有特定的事件或数据关联,这些事件或数据也将被删除。然而,detach只是将元素从 DOM 中分离并返回你可以保存在某个变量中以供以后附着的内容:

@model WebApplication.ViewModels.Book.BookViewModel
@{
  ViewData["Title"] = "View";
}
<script src="img/jquery-1.12.0.min.js"></script>
<script>
  var mainDivContent=undefined
  $(document).ready(function () {
    $('button').click(function () {
      if (mainDivContent) {
        mainDivContent.appendTo('#pageDiv');
        mainDivContent = null;
      } else {
        mainDivContent = $('#mainDiv').detach();
      }
    });
  });
</script>
<div id="pageDiv" class="container">
  <br />
  <div id="mainDiv" class="form-horizontal">
    <div class="form-group">
      <label asp-for="Name" class="col-md-2 control-label"></label>
      <div class="col-md-10">
        <input asp-for="Name" class="form-control" />
      </div>
    </div>
  </div>
  <div class="form-group">
    <div class="col-md-offset-2 col-md-10">
      <button class="btn btn-primary"> Detach/Attach</button>
    </div>
  </div>
</div>

在分离后,输出将如下所示:

删除元素和属性

在附着后,输出将类似于以下屏幕截图:

删除元素和属性

jQuery 中的事件处理

jQuery 事件模型为处理 DOM 元素上的事件提供了更好的方法。程序化地,如果开发者想要注册用户操作的任何事件;例如,按钮的点击事件当使用纯 JavaScript 时可能是一个繁琐的过程。这是因为不同的浏览器有不同的实现,并且语法彼此之间有所不同。另一方面,jQuery 库提供了一个更简洁的语法,开发人员不必处理跨浏览器问题。

jQuery 中注册事件

在 jQuery 中,有许多快捷方式可以注册事件到不同的元素上。下面的表格展示了所有这些事件及其具体的描述:

事件 描述
click() 此事件在鼠标点击时使用
.dblclick() 此事件在双击时使用
.mousedown() 此事件在鼠标任何按钮被按下时使用
.mouseup() 此事件在鼠标任何按钮被释放时使用
.mouseenter() 此事件在鼠标进入区域时使用
.mouseleave() 此事件在鼠标离开区域时使用
.keydown() 此事件在键盘按键被按下时使用
.keyup() 此事件在键盘按键被释放时使用
.focus() 此事件在元素获得焦点时使用
.blur() 此事件在元素失去焦点时使用
.change() 此事件在项目被更改时使用

还有许多其他事件,您可以在api.jquery.com/category/events上查看。

使用 jQuery 注册事件相当简单。首先,必须通过选择任何选择器来选择元素,然后通过调用特定的事件处理程序来注册事件;例如,以下代码片段将为按钮注册点击事件:

$(document).ready(function({
  $('#button1').click(function(){
    console.log("button has been clicked");
  });
)};

在前面的示例代码之后,注册.asp.net按钮的点击事件,并调用 ASP.NET 中Home控制器的Contact动作:

<script src="img/jquery-1.12.0.min.js"></script>
<script>
  var mainDivContent=undefined
  $(document).ready(function () {
    $('#btnSubmit').click(function () {
      window.location.href = '@Url.Action("Contact", "Home")';  
    });
  });
</script>
<div id="pageDiv" class="container">
  <br />

  <div class="form-group">
    <div class="col-md-offset-2 col-md-10">
      <button id="btnSubmit" class="btn btn-primary"> Submit</button>
    </div>
  </div>
</div>

在前面的示例中,我们通过 Razor 语法使用了 HTML 助手Url.Action,生成了 URL 并将其设置为窗口当前位置的href属性。现在,点击下面屏幕截图中的按钮:

jQuery 中注册事件

以下联系页面将被显示:

jQuery 中注册事件

这里的一个示例将改变所有输入控件的背景颜色到aliceblue,当控件获得焦点时,并在它失去焦点时恢复为白色:

@model WebApplication.ViewModels.Book.BookViewModel
@{
  ViewData["Title"] = "View";
}
<script src="img/jquery-1.12.0.min.js"></script>
<script>
  var mainDivContent=undefined
  $(document).ready(function () {
    $('#btnSubmit').click(function () {
      window.location.href = '@Url.Action("Contact", "Home")';  
    });

    $('input').each(function () {
      $(this).focus(function () {
        $(this).css('background-color', 'aliceblue');
      })
      $(this).blur(function () {
        $(this).css('background-color', 'white');

      });
    });
  });
</script>
<div id="pageDiv" class="container">
  <br />
  <div id="mainDiv" class="form-horizontal">
    <div class="form-group">
      <label asp-for="Name" class="col-md-2 control-label"></label>
      <div class="col-md-10">
        <input asp-for="Name"  class="form-control" />
      </div>
    </div>
    <div class="form-group">
      <label asp-for="Description" class="col-md-2 control-label"></label>
      <div class="col-md-10">
        <input asp-for="Description" class="form-control" />
      </div>
    </div>
  </div>
  <div class="form-group">
    <div class="col-md-offset-2 col-md-10">
      <button id="btnSubmit" class="btn btn-primary"> Submit</button>
    </div>
  </div>
</div>

使用 on 和 off 注册事件

除了直接通过调用事件处理程序来注册事件,我们还可以使用onoff来注册它们。这些事件为特定元素注册和注销事件。

这是一个使用on绑定点击事件到按钮的简单示例:

$(document).ready(function () {
  $('#btnSubmit').on('click', function () {
    window.location.href = '@Url.Action("Contact", "Home")';
  });
});

这是一个非常实用的技术,可以在你希望注销任何事件的情况下使用。例如,商务应用程序大多数与表单处理相关,而表单可以通过某个按钮提交请求到某个服务器。在某些条件下,我们必须限制用户在第一次请求处理完成前多次提交。为了解决这个问题,我们可以使用on()off()事件在用户第一次点击时注册和注销它们。以下是一个在第一次点击时注销按钮点击事件的示例:

<script src="img/jquery-1.12.0.min.js"></script>
<script>
  $(document).ready(function () {
    $('#btnSubmit').on('click', function () {
      $('#btnSubmit').off('click');       
    });
  });
</script>

preventDefault()事件就是我们以前在.NET 中使用的取消事件。这个事件用于取消事件的执行。它可以像下面这样使用:

<script src="img/jquery-1.12.0.min.js"></script>
<script>
  $(document).ready(function () {
    $('#btnSubmit').on('click', function (event) {
      event.preventDefault();
    });
  });
</script>

on()方法与以前版本 jQuery 中使用的delegate()方法等效。自 jQuery 1.7 起,delegate()已被on()取代。

还有一个重载方法on,它接受四个参数:

$(element).on(events, selector, data, handler);

在这里,element是控件名称,events是你想要注册的事件,selector是一个新东西,可以是父控件的子元素。例如,对于一个表格元素选择器,它可能是td;而且在每个td的点击事件上,我们可以做如下操作:

@model IEnumerable<WebApplication.ViewModels.Book.BookViewModel>
<script src="img/jquery-1.12.0.min.js"></script>
<script>
  $(document).ready(function () {
    $('table').on('click','tr', null, function() {
      $(this).css('background-color', 'aliceblue');
    });
  });
</script>

<p>
  <a asp-action="Create">Create New</a>
</p>
<table class="table">
  <tr>
    <th>
      @Html.DisplayNameFor(model => model.Description)
    </th>
    <th>
      @Html.DisplayNameFor(model => model.Name)
    </th>
    <th></th>
  </tr>

  @foreach (var item in Model) {
    <tr>
      <td>
        @Html.DisplayFor(modelItem => item.Description)
      </td>
      <td>
        @Html.DisplayFor(modelItem => item.Name)
      </td>
      <td>
        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
      </td>
    </tr>
  }
</table>
 output would be similar to the following screenshot. When the user clicks on any row, the background color will be changed to Alice blue:

使用 on 和 off 绑定事件

使用 hover 事件

我们可以利用鼠标悬停在特定元素上或离开时的 hover 事件。它可以通过在 DOM 的任何元素上调用hover()方法来使用。调用此方法的语法如下:

$(selector).hover(mouseEnterHandler, mouseExitHandler);

以下示例在鼠标悬停在输入文本控件上时改变边框颜色:

@{
  ViewData["Title"] = "View";
}
<script src="img/jquery-1.12.0.min.js"></script>
<02>
  $(document).ready(function () {
    $("input[type = 'text']").hover(function () {
      $(this).css('border-color', 'red');
    },
    function () {
      $(this).css('border-color', 'black');
    }
  });
  </script>
  <div id="pageDiv" class="container">
    <br />

  <div id="mainDiv" class="form-horizontal">
    <div class="form-group">
      <label asp-for="Name" class="col-md-2 control-label"></label>
      <div class="col-md-10">
        <input asp-for="Name" class="form-control" />
      </div>
    </div>
    <div class="form-group">
      <label asp-for="Description" class="col-md-2 control-label"></label>
      <div class="col-md-10">
        <input asp-for="Description" class="form-control" />
      </div>
    </div>
  </div>
  <div class="form-group">
    <div class="col-md-offset-2 col-md-10">
      <button id="btnSubmit" class="btn btn-primary"> Submit</button>
    </div>
  </div>
</div>

总结

在本章中,你学习了 jQuery 的基础知识以及如何在 Web 应用程序中使用它们,特别是在 ASP.NET 核心 1.0 中。这是一个非常强大的库。它消除了跨浏览器问题,并在所有浏览器中提供一致的行为。这个库提供了简单易用的方法来选择元素、修改属性、附加事件以及通过编写更干净、更精确的代码来执行复杂操作。在下一章中,我们将探讨使用 jQuery 和纯 JavaScript 进行 Ajax 请求的各种技术以执行服务器端操作。

第四章.Ajax 技术

使网页应用程序具有响应性的核心特征之一就是 Ajax。在服务器端回发传统方式中,无论用户执行任何操作,表单中提供的信息都会发送回服务器,并且同一页面会再次加载,包含在客户端重新加载的所有图像、CSS 和 JavaScript 文件。这种方法在客户端和服务器之间发送的请求和响应大小方面相当沉重。因此,应用程序变得不那么响应式,用户每次执行任何操作时都必须等待页面刷新。在本章中,我们将讨论如何通过 Ajax 简化整个过程,并避免沉重的服务器端回发。

介绍 Ajax

Ajax代表异步 JavaScript 和 XML;它能在不重新发送和渲染整个页面的情况下,在服务器端创建异步请求,而它只发送需要发送到服务器的少量信息,并以特定格式接收响应,通过 JavaScript 更新特定部分或 DOM 元素。这使得开发者能够开发响应式网页应用程序,并动态更新页面内容,而无需每次特定动作时重新加载页面。例如,在主从页面关系中,子内容依赖于父项的选择;而采用传统方法,每次选择父项时,页面都会被发送回服务器端,服务器端执行一些后端任务来填充子部分,并返回 HTML 代码,然后客户端对其进行渲染。通过 Ajax,这可以通过异步请求发送所选信息并更新页面内容的指定部分来实现。

Ajax 如何工作

Ajax 使用XMLHttpRequestXHR)对象异步调用服务器端方法。XHR 是由微软开发的,最初在 Internet Explorer 5 中提供。最初通过调用ActionXObject实例来创建一个实例;然而,在现代版本中,每个浏览器都支持通过XMLHttpRequest对象初始化 XHR 对象。

以下图表展示了 Ajax 工作的架构视图:

Ajax 如何工作

传统上,当客户端执行任何操作时,整个数据都会发送回服务器,一旦收到响应,数据会在客户端重新加载。除非实现了某种缓存机制,否则需要更新的数据(包括所有静态文件,如 CSS、JavaScript 和图片)会从服务器重新加载并在客户端呈现,而不是更新实际需要更新的数据。使用 Ajax,我们可以以 JSON 字符串或 XML 的形式发送数据,并根据服务器返回 JSON、XML、HTML 或其他格式的响应。我们还可以在发送请求时使用请求头,如Accept,因此服务器知道客户端接受什么;根据格式化器,它还可以将数据序列化为特定格式。在 ASP.NET MVC 6 中,默认实现了两个格式化器,分别为 JSON 和 XML 发送数据,根据请求的Accept头序列化对象。还可以在服务器级别实现自定义格式化器来处理特定场景。

使用经典的 XHR 对象进行 Ajax 请求

所有浏览器,包括 Internet Explorer、Chrome、Firefox 和 Safari,都提供这个对象,可以从 JavaScript 中使用它来执行 Ajax 请求。

在 JavaScript 中,我们可以如下初始化XMLHttpRequest对象:

var xhr = new XMLHttpRequest();

每个请求都可能是GETPOST请求。一旦从服务器收到响应,一些属性会填充,事件处理程序会被调用,这些事件处理程序在执行 Ajax 请求时可以配置为 XHR 对象。

让我们深入了解 XHR 对象提供的方法、属性和事件。

XHR 方法

XHR 对象提供了各种方法,但启动 Ajax 化请求最重要的两个方法是open()send()

  • 发送请求

    请求可以是GETPOST。在执行任何请求时,我们首先必须调用open方法并指定 HTTP 方法,如GETPOST,以及服务器的 URL。其余参数,如async位、userpassword,是可选的。

    open方法的字段如下:

    void Open(
    
      DOMString method, 
      DOMString URL, 
      optional boolean async, 
      optional DOMString user?, 
      optional DOMString password
    
    );
    

    send方法用于将请求发送到服务器。这是实际的方法,它接受各种格式的数据并向服务器发送请求。

    以下表格展示了send方法的可重载方法:

    方法 描述
    void send() 此方法用于发送GET请求
    void send (DOMString? Data) 当以字符串形式传递数据时使用此方法
    void send(Document data) 当传递文档数据时使用此方法
    void send(Blob data) 此方法用于传递 blob 数据或二进制数据
    void send(FormData data) 此方法用于传递整个表单
  • 取消请求

    在某些情况下,开发者可能需要取消当前请求。这可以通过调用 XHR 对象的abort()函数来实现:

    var xhr = new XMLHttpRequest();
    xhr.abort();
    
  • 设置请求头部

    XHR 提供了几种 Ajax 请求的方法。这意味着在根据服务器实现需要发送 JSON、XML 或某种自定义格式的数据时,存在一些情况。例如,当与 ASP.NET MVC 6 一起工作时,有两种默认格式化器实现,分别是 JSON 和 XML,如果你想要实现自己的自定义格式化器,这也是可能的。当发送特定格式的数据时,我们需要通过请求头部告诉服务器该格式。这有助于服务器识别必须加载以序列化响应和处理请求的格式化器。

    以下表格显示了可以与 Ajax 请求一起提供的默认头部:

    头部 描述
    Cookie 此头部指定客户端设置的任何 cookie
    Host 此头部指定页面的域名
    Connection 此头部指定连接的类型
    Accept 此头部指定客户端可以处理的内容类型
    Accept-charset 此头部指定客户端可以显示的字符集
    Accept-encoding 此头部指定客户端可以处理的编码
    Accept-language 此头部指定作为响应接受的首选自然语言
    User-Agent 此头部指定一个用户代理字符串
    Referer 此头部指定页面的 URL

    通过 XHR 对象,我们可以使用setRequestHeader()函数设置请求头部,如下面的代码所示:

    var xhr= new XMLHttpRequest();
    xhr.setRequestHeader('Content-Type', 'application/json');
    
  • 获取响应头部

    当服务器返回响应时,我们可以使用以下两种方法来读取响应头部:

    var xhr= new XMLHttpRequest();
    function callback(){
      var arrHeaders = xhr.getAllResponseHeaders();
      //or
      var contentType = xhr.getResponseHeader('Content-Type');
    }
    

    getAllResponseHeaders()函数返回所有响应头部的列表,而getResponseHeader()函数接受头部名称并返回提供的头部名称的值。

XHR 事件

在 XHR 对象中最有用的事件处理程序,当readystate属性的值发生变化时调用,是onreadystatechange事件。在初始化请求时,我们可以将函数与这个事件处理程序关联并读取响应:

var xhr= new XMLHttpRequest();
xhr.onreadystatechange = callback;

function callback(){
  //do something
}

另一个核心事件处理程序是ontimeout,可以在处理请求超时场景时使用。在初始化 XHR 请求时,有一个timeout属性,通过该属性可以将超时设置为毫秒,如果请求超过超时值,将调用ontimeout事件处理程序。例如,将超时设置为 5,000 毫秒,如果超过timeout属性,将调用timeout处理函数,如下所示:

var xhr = new XMLHttpRequest();
xhr.timeout = 5000; 
xhr.ontimeout = timeouthandler;
function timeouthandler(){
  //do something
}

XHR 属性

以下是为XMLHttpRequest对象可用的属性列表:

  • GET 请求状态

    这个属性返回关于响应的状态信息。它通常用于根据请求状态采取行动:

     var xhr=new XMLHttpRequest();
     xhr.readystate;
    

    以下表格给出了可用于readystate属性的状态及其含义的列表:

    状态值 状态 描述
    0 UNSENT 在此状态下,创建了XMLHttpRequest对象,但未调用open()方法
    1 OPENED 在此状态下,调用open方法
    2 HEADERS_RECEIVED 在调用send()并接收到头部时发生此状态
    3 LOADING 当响应正在下载时发生此状态
    4 DONE 当响应完成时发生此状态
  • 获取响应数据

    可以通过调用responseresponseText属性来检索响应。这两个属性的区别在于,responseText属性返回响应作为一个字符串,而response属性返回响应作为一个response对象。response对象可以是一个文档、blob 或 JavaScript 对象:

    var xhr= new XMLHttpRequest();
    xhr.response;
    //or 
    xhr.responseText;
    
  • 获取响应状态

    可以通过调用statusstatusText属性来检索响应状态。这两个属性的区别在于,status属性返回数值值,例如,如果服务器成功处理了请求,则返回200;而statusText属性包括完整的文本,例如200 OK等:

    var xhr= new XMLHttpRequest();
    xhr.status;
    or 
    xhr.statusText;
    

让我们来看一个使用 ASP.NET MVC 6 中的 XHR 对象进行表单POST请求的例子。以下表单有两个字段,NameDescription

XHR 属性

以下代码片段使用 XHR 对象将请求发送到服务器端。这个例子发送的是 JSON 数据:

@model WebApplication.ViewModels.Book.BookViewModel
@{
  ViewData["Title"] = "View";
}
<script>
  var xhr = null;
  function submit() {
    xhr = new XMLHttpRequest();
    xhr.open("POST", '/Book/SaveData');
    var name = document.getElementById("Name").value;
    var description = document.getElementById("Description").value;
    var data =
    {
      "Name": name,
      "Description": description
    };
    xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
    xhr.onreadystatechange = callback;
    xhr.send(JSON.stringify(data));
  }

  function callback() {
    if (xhr.readyState == 4) {
      var msg = xhr.responseText;r 
      document.getElementById("msg").innerHTML = msg;
      document.getElementById("msgDiv").style.display = 'block';
    }
  }
</script>

<form asp-action="SaveData" id="myForm">
  <p> </p>
  <div id="msgDiv" style="display:none" class="alert alert-success">
    <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
    <strong>Success!</strong> <label id="msg"></label>
  </div>
  <div id="pageDiv" class="container">
    <br />
    <div id="mainDiv" class="form-horizontal">
      <div class="form-group">
        <label asp-for="Name" class="col-md-2 control-label"></label>
        <div class="col-md-10">
          <input asp-for="Name" class="form-control" />
        </div>
      </div>
      <div class="form-group">
        <label asp-for="Description"  class="col-md-2 control-label"></label>
        <div class="col-md-10">
          <textarea asp-for="Description" class="form-control" ></textarea>
        </div>
      </div>
    </div>
    <div class="form-group">
      <div class="col-md-offset-2 col-md-10">
        <button id="btnSubmit" onclick="submit()" type="submit" class="btn btn-primary"> Submit</button>
      </div>
    </div>
  </div>
</form>

在 ASP.NET Core 中,对于 JSON 和 XML,我们必须显式地为复杂类型添加[FromBody]属性。这是因为 MVC 6 首先在不管它是复杂类型还是基本类型的情况下搜索查询字符串中的值。对于 JSON 和 XML 数据,我们需要显式地将方法参数添加[FromBody]属性,以便数据可以没有任何问题地轻松绑定:

public IActionResult SaveData([FromBody]BookViewModel bookViewModel)
{
  return Content("Data saved successfully"); 
}
document.getElementById and then made a JSON string to pass the form data in a JSON format.

输出将如下所示:

XHR 属性

然而,谷歌提供了一个库,通过调用serialize()函数来序列化表单数据。唯一的区别是设置请求头'Content-Type''application/x-www-form-urlencoded',并添加以下脚本文件:

<script src=http://form-serialize.googlecode.com/svn/trunk/serialize-0.2.min.js />

以下代码是submit函数的修订版,它通过serialize()函数序列化表单数据,并将数据作为表单编码值发送:

function submit() {
  xhr = new XMLHttpRequest();
  xhr.open('POST', '/Book/SaveData');
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  var html = serialize(document.forms[0]);
  xhr.onreadystatechange = callback;
  xhr.send(html);
}

对于表单编码的值,我们将删除[FromBody]属性。这是因为表单编码的值作为查询字符串中的名称值对发送:

public IActionResult SaveData(BookViewModel bookViewModel)
{
  return Content("Data saved successfully"); 
}

在 ASP.NET Web API 的前几个版本中,如果 Web API 控制器的action方法包含一个复杂类型,Web API 框架会自动绑定请求体中的值。而随着 ASP.NET Core 的出现,Web API 和 MVC 已经合并为一个统一的框架,模型绑定不再与我们在 Web API 前几个版本中的那样相等。

在前面的例子中,我们看到了如何轻松地发送一个POST请求并发送 JSON 和表单编码的值。现在,让我们看一个例子,在这个例子中,我们将根据从服务器发送的 JSON 响应加载部分视图。

以下屏幕截图是包含一个按钮以在表格中加载书籍列表的 ASP.NET 页面:

XHR 属性

以下是主页的代码片段:

@model WebApplication.ViewModels.Book.BookViewModel
@{
  ViewData["Title"] = "Books";
}
<script>
  var xhr = null;
  function loadData() {
    xhr = new XMLHttpRequest();
    xhr.open('GET', '/Book/Books',true);
    xhr.onreadystatechange = callback;
    xhr.send();
  }
  function callback() {
    if (xhr.readyState == 4) {
      var msg = xhr.responseText;
      document.getElementById("booksDiv").innerHTML = msg;
    }
  }
</script>
<div class="container">  
  <button id="btnLoad" onclick="loadData()" type="submit" class="btn btn-primary">Load</button>
  <hr />
  <div id="booksDiv">
  </div>
</div>

以下是一个显示书籍列表的表格的部分视图:

@{ 
  Layout = null;
}
@model IEnumerable<WebApplication.ViewModels.Book.BookViewModel>
<script src="img/jquery-1.12.0.min.js"></script>
<script>
  $(document).ready(function () {
    $('table').on('click','tr', null, function() {
      $(this).css('background-color', 'aliceblue');
    });
  });
</script>

<p>
  <a asp-action="Create">Create New</a>
</p>
<table class="table">
  <tr>
    <th>
      @Html.DisplayNameFor(model => model.Description)
    </th>
    <th>
      @Html.DisplayNameFor(model => model.Name)
    </th>
    <th></th>
  </tr>

@foreach (var item in Model) {
  <tr>
    <td>
      @Html.DisplayFor(modelItem => item.Description)
    </td>
    <td>
      @Html.DisplayFor(modelItem => item.Name)
    </td>
    <td>
      <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
      <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
      <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
  </tr>
}
</table>
Books controller that contains the Books action method that returns a list of books:
public class BookController : Controller
{
  // GET: /<controller>/
  public IActionResult Index()
  {
    return View();
  }

  public IActionResult Books()
  {
    List<BookViewModel> books = new List<BookViewModel>();
    books.Add(new BookViewModel { Id = 1, Name = "JavaScript for .Net Developers", Description = "Book for .NET Developers" });
    books.Add(new BookViewModel { Id = 1, Name = "Beginning ASP.NET Core 1.0", Description = "Book for beginners to learn ASP.NET Core 1.0" });
    books.Add(new BookViewModel { Id = 1, Name = "Mastering Design Patterns", Description = "All about Design Patterns" });
    return View(books);
  }

  public IActionResult Create()
  {
    return View();
  }
}

所以,有了这个设置,当用户点击加载按钮时,请求将被发送到服务器,ASP.NET MVC 控制器Books动作方法将被调用,它返回一个视图,该视图渲染部分视图,该视图将在主页上的booksDiv元素内渲染:

XHR 属性

使用 jQuery 发送 Ajax 请求

在前几节中,我们讨论了如何使用普通的XMLHttpRequest对象发送 Ajax 请求,这在所有浏览器中都是可用的。在本节中,我们将了解 jQuery 在发送 Ajax 请求方面提供了什么,以及如何通过 jQuery 对象使用 HTTP GETPOST请求。

jQuery.ajax()

此方法用于发送GETPOST异步请求。以下代码是此方法的签名,它接受两个参数:URLoptionsURL参数是实际的服务器 URL,而options以 JSON 表示形式传递配置请求头和其他属性:

$.([URL],[options]);
$.( [options]);

以下示例显示了如何对 MVC 控制器进行异步请求,并在从服务器成功返回响应时显示一个警告框:

<script src="img/jquery-1.12.0.min.js"></script>
<script>
  $(document).ready(function () {
    $.ajax('/Book/Books', {
      success: function (data) {
        $('#booksDiv').html(data);
      },
      error: function (data) {
        $('#booksDiv').html(data);
      }
    });
  });
</script>

Books动作方法返回 ASP.NET MVC 视图,其中传递了将在booksDiv DOM 元素内填充的书籍列表:

jQuery.ajax()

Ajax 属性

以下表格显示了您可以指定的一些核心属性,以配置 Ajax 请求:

名称 类型 描述
accepts PlainObject 此属性告诉服务器客户端将接受哪种类型的响应。
async Boolean 默认情况下,此属性为true(用于异步请求),但它可以设置为false(同步)。
cache Boolean 如果将此属性设置为false,浏览器将不会缓存强制请求的页面。
contents PlainObject 此属性用于指定解析响应的正则表达式。
contentType StringBoolean 这个属性告诉服务器传入请求的数据类型。默认值是application/x-www-form-urlencoded; charset=UTF-8
crossDomain Boolean 如果您想强制执行跨域请求,则将此属性设置为true
data PlainObjectStringArray 这个属性可以用来以 JSON、XML 或其他任何格式传递数据。
dataType String 这个属性指定了期望从服务器返回的数据类型。一些核心数据类型包括 XML、JSON、脚本和 HTML。

预过滤 Ajax 请求

这是一个很好的功能,可以在发送之前过滤现有的请求选项和配置属性。它提供了两个重载方法:一个接收一个函数,该函数注入optionsoriginalOptionsjqXHR对象,另一个接收一个字符串,您可以在此字符串中过滤出特定请求的配置属性,后面跟着接受optionsoriginalOptionsjqXHR参数的函数。下面是这两个重载方法的签名:

$.ajaxPrefilter(function(options, originalOptions, jqXHR){
  //Modify options, originalOptions and store jqXHR
}
$.ajaxPrefilter('dataType', function(options, originalOptions, jqXHR){
  //Modify options, originalOptions and store jqXHR
}

前面的代码中的对象如下解释:

  • options:这些对象与 Ajax 请求中提供的请求选项相同,但可以被覆盖和相应地过滤。

  • originalOptions:这些对象提供了 Ajax 请求中实际传递的选项。它们可以用来引用,但不能修改。任何配置的更改都可以通过使用options对象来实现。

  • jqXHR:这个对象与 jQuery 中的XMLHttpRequest对象相当。

让我们来看一下以下示例,该示例通过添加fromAjax参数来告诉 MVC 控制器请求是从 JavaScript 执行的:

<script>
  $(document).ready(function () {

    $.ajaxPrefilter(function (options, originalOptions, jqXHR) {
      options.url += ((options.url.indexOf('?') < 0) ? '?' : '&')+ 'fromAjax=true';
    });

    $.ajax('/Book/Books', {
      success: function (data) {
        $('#booksDiv').html(data);
      },
      error: function (data) {
        $('#booksDiv').html(data);
      }
    });
  });
</script>

下面的代码是控制器动作方法,如果请求是 Ajax 请求,则返回书籍列表:

public IActionResult Books(bool fromAjax)
{
  if (fromAjax)
  {
    List<BookViewModel> books = new List<BookViewModel>();
    books.Add(new BookViewModel { Id = 1, Name = "JavaScript for .Net Developers", Description = "Book for .NET Developers" });
    books.Add(new BookViewModel { Id = 1, Name = "Beginning ASP.NET Core 1.0", Description = "Book for beginners to learn ASP.NET Core 1.0" });
    books.Add(new BookViewModel { Id = 1, Name = "Mastering Design Patterns", Description = "All about Design Patterns" });
    return View(books);
  }
  return Content("Request to this method is only allowed from Ajax");
}

有关选项的各个属性,您可以在api.jquery.com上参考。

为所有未来的 Ajax 请求设置默认值

使用$.ajax.setup函数,我们可以为通过$.ajax()$.get()函数进行的所有未来请求设置配置值。这可以用来在调用$.ajax()函数之前设置默认设置,ajax函数将选择在$.ajaxSetup()函数中定义的设置。

调用$.ajax.setup的签名如下:

$.ajaxSetup({name:value, name:value, name:value, …});

下面的示例设置了通过$.ajax函数进行的ajax请求的默认 URL:

<script>
  $(document).ready(function () {

    $.ajaxSetup({ url: "/Book/Books"});

    $.ajax({
      success: function (data) {
        $('#booksDiv').html(data);
      },
      error: function (data) {
        $('#booksDiv').html(data);
      }
    });
  });
</script>

通过 jQuery 的 get 函数加载数据

jQuery 库提供了不同的函数,用于从服务器检索数据。例如$.get()函数,可以用来通过 HTTP GET请求加载数据,而$.getJSON()专门用来加载编码为 JSON 的数据,$.getScript()用来加载并执行来自服务器的 JavaScript。

使用 jQuery.get()

$.get() 函数是 $.ajax() 的简写函数,只允许 GET 请求。它将大多数配置值抽象为默认值。与 $.ajax() 函数类似,它将数据返回给 callback 函数,但不提供错误回调。因此,如果在请求处理过程中发生任何错误,它无法被追踪。

它接受四个参数,URLdatacallbacktype。其中 URL 是请求发送到的地址,data 是一个在请求时发送到服务器的字符串,callback 指的是当请求成功时执行的函数,type 指定了从服务器期望的数据类型,如 XML、JSON 等。

$.get() 函数的以下是其签名:

$.get('URL',data, callback, type);

以下示例加载包含 net 字符串在其标题中的书籍:

<script>
  $(document).ready(function () {

    $.get('/Book/Books', {filter : "net"}, function (data) {
        $('#booksDiv').html(data);
      }
    );

  });
</script>

使用 jQuery.getJSON()

jQuery.getJSON() 函数用于从服务器加载 JSON。可以通过调用 $.getJSON() 函数来使用它:

$.getJSON('URL', {name:value, name:value, name:value,…});

以下示例通过调用一个 action 方法来加载 JSON,该方法返回 JSON 响应并在 booksDiv 元素中显示书名:

<script>
  $(document).ready(function () {

    $.getJSON('/Book/Books', function (data) {
      $.each(data, function (index, field) {
        $('#booksDiv').append(field.Name + "<br/> ");
      });
    }
  );
</script>

Action 方法如下返回 JSON 响应:

public IActionResult Books()
{
  List<BookViewModel> books = new List<BookViewModel>();
  books.Add(new BookViewModel { Id = 1, Name = "JavaScript for .Net Developers", Description = "Book for .NET Developers" }
  books.Add(new BookViewModel { Id = 1, Name = "Beginning ASP.NET Core 1.0", Description = "Book for beginners to learn ASP.NET Core 1.0" });
  books.Add(new BookViewModel { Id = 1, Name = "Mastering Design Patterns", Description = "All about Design Patterns" });
  return Json(books);

}

页面上的书籍标题将按如下截图所示呈现:

使用 jQuery.getJSON()

使用 jQuery.getScript()

jQuery.getScript() 函数是 $.ajax() 的简写,专门用于从服务器加载脚本。以下是 $.getScript() 函数的签名:

$.getScript(url, callback);

以下示例在文档加载完成后加载自定义 .js 文件:

<script>
  $(document).ready(function () {

  $.getScript("/wwwroot/js/custom.js");
</script>

使用 post 函数 将数据发送到服务器

$.get() 函数类似,jQuery 还提供了一个 $.post() 函数,它是 $.ajax() 的简写,专门用于仅发送 HTTP POST 请求。

以下是 $.post() 函数的签名:

$.post(url, data, callback, type);

以下示例使用 $.post() 函数提交表单数据:

<script>

  function submit() {
    $.post('/Book/SaveData', $("form").serialize(), function (data) {
      alert("form submitted");

    });
  }
</script>
Book controller's SaveData action method that takes the object and returns the response as a string:
public IActionResult SaveData(BookViewModel bookViewModel)
{
  //call some service to save data 
  return Content("Data saved successfully")
}

同样,我们可以通过指定类型为 json 来传递 JSON 数据:

<script>
  function submit() {
    $.post('/Book/SaveData', {Name:"Design Patterns", Description: "All about design patterns"}, function (data) {
    },'json' );
  }
</script>

Ajax 事件

Ajax 事件分为本地事件和全局事件。当使用 $.ajax 函数进行 Ajax 请求时可以声明本地事件,如 successerror 这样的事件被称为本地事件,而全局事件则与页面中执行的每个 Ajax 请求一起工作。

本地事件

以下是本地事件列表,它与 $.ajax() 函数特别相关。其他简写函数,如 $.get()$.post(),没有这些方法可用,因为每个函数都有特定的参数传递和配置属性值:

  • beforeSend:在 ajax 请求发送之前触发此事件。

  • success:当从服务器成功响应时发生此事件。

  • error:在 ajax 请求过程中发生错误时触发此事件。

  • complete:当请求完成时发生此事件。它不检查是否发生错误或者响应是否成功,而是在请求完成后执行。

全局事件

以下是全局事件列表,它与其他缩写函数一起工作,例如$.post()$.get()$.getJSON

  • ajaxStart:当管道中没有ajax请求且第一个ajax请求正在启动时使用此事件。

  • ajaxSend:当向服务器发送ajax请求时使用此事件。

  • ajaxSuccess:当服务器返回的任何成功响应时使用此事件。

  • ajaxError:当任何ajax请求发生错误时,此事件将被触发。

  • ajaxComplete:当任何ajax请求完成时使用此事件。

以下是一个 ASP.NET 简单的示例代码,它调用BookControllerBooks动作方法,返回书籍列表并触发全局和局部事件:

@model WebApplication.ViewModels.Book.BookViewModel
@{
  ViewData["Title"] = "Books";
}
<script src="img/jquery-1.12.0.min.js"></script>
<script>

  $(document).ready(function () {

    $(document).ajaxComplete(function (e) {
      alert("Ajax request completed");
    }).ajaxSend(function () {
      alert("Ajax request sending");
    }).ajaxSend(function () {
      alert("Ajax request sent to server");
    }).ajaxStop(function () {
      alert("Ajax request stopped");
    }).ajaxError(function () {
      alert("Some error occurred in Ajax request");
    }).ajaxSuccess(function () {
      alert("Ajax request was successful");
    })
    $('#btnLoad').click(function(){
      $.ajax('/Book/Books', {
        success: function (data) {
          $('#booksDiv').html(data);
        },
        error: function (data) {
          $('#booksDiv').html(data);
        }
      });

    });

  });
</script>
<div class="container">
  <br />
  <h4>Books View</h4>
  <h5>Click on the button to load all the books</h5>
  <button id="btnLoad" type="submit" class="btn btn-primary">Load</button>
  <hr />
  <div id="booksDiv">
  </div>
</div>

跨源请求

geo service and specifies a callback parameter, which points to the jsonCallback function defined in the script. This script will be loaded when the page loads and executes the src URL, which finally calls the jsonCallback method and passes the response.
GET request that uses the Bing API to get the location information based on the latitude and longitude values provided:
<script>
  var scrpt = document.createElement('script');

  scrpt.setAttribute('src',' http://dev.virtualearth.net/REST/v1/Locations/latitudeNo,longitudeNo?o=json&key=BingMapsKey);
  document.body.appendChild(scrpt);
  function jsonCallback(data) {
    alert("Cross Origin request got made");
  }
</script>

另一方面,使用 jQuery 时,可以通过在$.ajax调用中指定dataType属性为jsonpcrossDomaintrue来发起跨源请求:

$.ajax({
  url: serviceURL,
  type: "GET",
  dataType: "jsonp",
  method:"GetResult",
  crossDomain: true,
  error: function () {
    alert("list failed!");
  },
  success: function (data) {
    alert(data);
  }
});

CORS

另外,当发起跨源请求时,CORS 是更为推荐的方式。它是一个 W3C 标准,允许服务器从任何域发送跨源请求。这需要在服务器端启用。

ASP.NET Core 为在服务器端启用 CORS 提供了简单的方法,这可以通过通过NuGet添加Microsoft.AspNet.WebApi.Cors,或者通过修改project.json并添加以下依赖项来完成:

"Microsoft.AspNet.Cors": "6.0.0-rc1-final"

使用Startup类中的ConfigureServices方法启用 CORS 服务:

public void ConfigureServices(IServiceCollection services
{
  services.AddCors();
}

Configure方法中使用UseCors()方法添加 CORS 中间件。UseCors方法提供两个重载方法:一个接受 CORS 策略,另一个接受委托,可以作为构建器来构建策略。

注意

请注意,在UseMVC之前应添加UseCors()

通过 CORS 策略,我们可以定义允许的源、头和方式。CORS 策略可以在定义中间件时的ConfigureServicesConfigure方法中定义。

在服务级别指定 CORS 策略

本节将介绍在ConfigureServices方法中定义策略并在添加中间件时引用的方法。AddPolicy方法有两个参数:策略的名称和一个CorsPolicy对象。CorsPolicy对象允许链式调用方法,并允许您使用WithOriginsWithMethodsWithHeaders方法定义源、方法和头。

以下是一个允许所有源、方法和头的示例代码片段。所以无论请求的源(域)和 HTTP 方法或请求头是什么,请求都将被处理:

public void ConfigureServices(IServiceCollection services)
{     
  services.AddCors(options => {
    options.AddPolicy("AllowAllOrigins", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
  });

}

在前面的代码中,Origins代表域名,Method代表 HTTP 方法,Header代表 HTTP 请求头。它可以在Configure方法中简单使用,如下所示:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory
{

  app.UseCors("AllowAllOrigin");
}

我们还可以定义多个策略,如下所示:

public void ConfigureServices(IServiceCollection services)
{
  services.AddCors(options => {
    options.AddPolicy("AllowAllOrigins", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
      options.AddPolicy("AllowOnlyGet", builder => builder.WithMethods("GET").AllowAnyHeader().AllowAnyOrigin());
  });

在 Configure 方法上启用 CORS

另外,我们可以在Configure方法本身定义 CORS 策略。UseCors方法有两个重载方法:一个接受已经在ConfigureServices方法中定义的策略名称,另一个是CorsPolicyBuilder,通过它可以在UseCors方法本身直接定义策略:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
  app.UseCors(policyBuilder => policyBuilder.WithHeaders("accept,content-type").AllowAnyOrigin().WithMethods("GET, POST"));
}

ConfigureMethod类上定义 CORS 策略可以使整个应用程序都应用 CORS 策略。 instead of using the EnableCors attribute, we can specifically define the policy name per controller, and action level as well, and use the policy defined in the ConfigureServices method.

通过特性定义是一个替代方案,它从ConfigureServices方法中引用策略名称,并忽略中间件级别定义的策略。以下是在控制器、操作和全局级别启用 CORS 的方法:

  • 在控制器级别启用 CORS:

    下面的代码在 MVC 控制器级别启用了 CORS 策略:

    [EnableCors("AllowAllOrigins")]
    public class BookController : Controller
    {
      //to do
    }
    
  • 在操作级别启用 CORS:

    下面的代码在 MVC 操作方法级别启用了 CORS 策略:

    [EnableCors("AllowAllOrigins")]
    public IActionResult GetAllRecords(
    {
      //Call some service to get records
      return View();
    }
    
  • 全局启用 CORS:

    全局来说,可以通过在中间件级别定义来启用 CORS,正如我们在Configure方法中看到的那样。否则,如果它是在ConfigureServices级别定义的,可以通过使用CorsAuthorizationFilterFactory对象在全局启用它,如下所示:

    public void ConfigureServices(IServiceCollection services)
    {
      services.AddCors(options => {
        options.AddPolicy("AllowAllOrigins", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
        options.AddPolicy("AllowOnlyGet", builder => builder.WithMethods("GET").AllowAnyHeader().AllowAnyOrigin());
      });
    
      services.Configure<MvcOptions>(options =>
      {
        options.Filters.Add(new CorsAuthorizationFilterFactory("AllowOnlyGet"));
      });
    }
    
AllowAllOrigins and AllowOnlyGet, and through CorsAuthorizationFilterFactory, we can pass the AllowOnlyGet policy as the policy name and make it global.

从 JavaScript 调用 WCF 服务

为了从 JavaScript 调用 WCF 服务方法,我们需要将它们作为接受和返回 JSON 或 XML 格式的 RESTful 服务方法公开。这有助于开发人员像使用 REST 服务一样轻松地使用 WCF 服务,并使用 jQuery $.ajax$.getJSON$.ajax的简写方法)方法。为了将 WCF 服务公开为 REST 服务,我们需要使用WebGetWebInvoke属性注解 WCF 服务方法。WebGet属性主要用于任何 HTTP GET请求,而WebInvoke用于所有 HTTP 请求方法。

下面的代码展示了在 WCF 操作合同上使用WebGet属性,根据方法调用期间传递的productCode返回产品的表示:

[OperationContract]
[WebGet(ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Wrapped, UriTemplate = "json/{productCode}")]
Product GetProduct(string productCode);

我们也可以使用WebInvoke来表示相同的方法,如下面的代码所示:

[OperationContract]
  [WebInvoke(Method ="GET",  ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Wrapped, UriTemplate = "products/{productCode}")]
Product GetProduct(string productCode);

下面的代码展示了使用WebInvoke对 HTTP POST请求的表示:

[OperationContract]
[WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Wrapped, UriTemplate = "products /SaveProduct")]
bool SaveProduct(Product product);

如果你注意到了,POST方法包含RequestFormatResponseFormat属性,这两个属性告诉服务器在执行任何 HTTP POST请求时提供数据的类型以及根据定义的ResponseFormat类型返回响应。

当与 RESTful 服务一起工作时,请确保绑定设置为webHttpBinding,如下面的屏幕截图所示。此外,与.NET 框架 4 及以上版本,微软引入了另一个属性,称为crossDomainScriptAccessEnabled,可以设置为true以处理跨源请求:

从 JavaScript 调用 WCF 服务

此外,为了启用 CORS,你可以在system.serviceModel下如下的屏幕截图中指定standardEndpoints

从 JavaScript 调用 WCF 服务

如下添加自定义头。指定星号(*)允许一切,而出于安全目的,原点、头信息和请求方法可以被明确地定义为用逗号分隔的具体值:

从 JavaScript 调用 WCF 服务

下面的表格显示了每个访问控制键的描述:

访问控制键 描述
Access-Control-Allow-Origin 此键用于允许从何处调用服务的客户端域
Access-Control-Allow-Headers 此键用于指定当客户端发起请求时允许的头信息
Access-Control-Allow-Method 使用此键,当客户端发起请求时允许的 HTTP 方法
Access-Control-Max-Age 此键采用秒为单位值,以查看响应预检请求可以在不发送另一个预检请求的情况下缓存多久

要调用SaveProduct方法,我们可以使用 jQuery 的$.ajax()方法,并提供以下参数,如以下代码所示。如果你注意到了,我们还定义了contentType以及dataType。区别在于contentType是用来告诉服务器客户端发送的数据类型的,而dataType是用来让服务器知道客户端期望在响应中接收的数据类型的。dataType的值可以是jsonjsonpxmlhtmlscript

function SaveProduct(){
  var product = {
    "ProductName":"Product1",
    "ProductDescription":"This is Product A"
  };

  $.ajax({
    type:"POST",
    url:"http://localhost/products/SaveProduct",
    data:JSON.stringify(product),
    contentType: "application/json",
    dataType:"json",
    processData:true,
    success: function(data, status, xhr){
      alert(data);

    },
    error: function(error){
      alert(error);

    }

  });
}

为了调用另一个域,我们可以使用jsonp,所以服务器将 JSON 数据包裹在一个 JavaScript 函数中,这被称为一个callback函数,当响应返回给客户端时,它会自动调用success方法。处理跨源请求的前述方法的修改版本如下所示。

在此代码中,我们修改了 URL,并把callback=?查询字符串作为参数传递。此外,crossDomain属性用来确保请求是crossDomain。当服务器响应时,?callback查询中指定,字符串将由函数名替换,例如json43229182_22822992,并将调用success方法:

function SaveProduct(){
  var product = {
    "ProductName":"Product1",
    "ProductDescription":"This is Product A"
  };

  $.ajax({
    type:"POST",
    url:" http://localhost:4958/ProductService.svc/products/SaveProduct?callback=?",
    data:JSON.stringify(product),
    contentType: "application/json",
    dataType:"jsonp",
    crossDomain: true, 
    processData:true,
    success: function(data, status, xhr){
      alert(data);

    },
    error: function(error){
      alert(error);

    }

  });
}

同样,我们也可以按照如下代码调用GetProduct方法:

(function () {
  var productCode= "Prod-001";
  var webServiceURL = "http://localhost:4958/ProductService.svc/products/GetProduct/"+productCode;
  $.ajax({
    type: "GET",
    url: webServiceURL,
    dataType: "json",
    processData: false,
    success: function (data) {
      alert(data);
    },
    error: function (error) {
      alert(error);
    }
  });
});

对于跨域,可以按照如下方式修改:

(function () {
  var productCode= "Prod-001";
  var webServiceURL = "http://localhost:4958/ProductService.svc/products/GetProduct/"+productCode;
  $.ajax({
    type: "GET",
    url: webServiceURL+"?callback=?",
    dataType: "jsonp",
    crossDomain:true,   
    processData: false,
    success: function (data) {
      alert(data);
    },
    error: function (error) {
      alert(error);
    }
  });
});

Alternatively, for the preceding solution, we can also override the callback function name in a jsonp request, and the value specified in jsonpCallback will be used instead of callback=? passed in a URL. The following code snippet calls your local function whose name is specified in the jsonpCallback value:

function callbackFn(data){

}

(function () {
  var productCode= "Prod-001";
  var webServiceURL = "http://localhost:4958/ProductService.svc/products/GetProduct/"+productCode;
  $.ajax({
    type: "GET",
    url: webServiceURL,
    dataType: "jsonp",
    crossDomain:true,   
    processData: false,
    jsonpCallback: callbackFn,
    success: function (data) {
      alert(data);
    },
    error: function (error) {
      alert(error);
    }
  });
});

总结

在本章中,我们讨论了 Ajax 技术以及使用XMLHttpRequest对象的概念。我们已经了解了 Ajax 请求的基本处理架构以及它提供的事件和方法。同样,我们还讨论了 jQuery 提供了什么以及它拥有的广泛库,用于执行不同类型的 HTTP GETPOST请求。在下一章中,我们将讨论TypeScript的基础知识,以及最受欢迎的客户端框架之一,Angular 2。我们还将通过使用 ASP.NET Core MVC 6 和 Angular 2 作为前端框架以及 Entity Framework 7 进行后端操作来开发一个简单的应用程序。

第五章:使用 Angular 2 和 Web API 开发 ASP.NET 应用程序

在本章中,我们将使用 MVC 6 在 ASP.NET Core 上开发一个完整的应用程序,使用 Web API 进行网络服务。对于客户端,我们将使用 Angular 2,这是客户端开发中最受欢迎的框架之一。Angular 2 是用 TypeScript 编写的,但它提供了用 JavaScript 和 Dart 编写代码的选项。在本章中,我们将使用 TypeScript,因为它遵循 ECMAScript 6 标准,并在构建项目时提供生成 ECMAScript 3,ECMAScript 4 和 ECMAScript 5 标准的 JavaScript 的能力。TypeScript 是 JavaScript 的超集,两者的大部分内容是相同的;实际上,TypeScript 提供了一些在许多浏览器中(除了 Mozilla Firefox)没有实现的 JavaScript 特性。

本章将重点介绍基本概念,并通过一个示例应用程序来介绍如何使用 Angular 2 与 ASP.NET Core 和 MVC 6 一起使用。

TypeScript

TypeScript 是由微软开发的一种语言,是 JavaScript 的超集。TypeScript 在编译时转换成 JavaScript。Visual Studio 2015 会自动将 TypeScript 构建成 JavaScript 文件,并将其放在配置了TypeScript.tsconfig配置文件的文件夹中。它提供的功能远超过 JavaScript,但开发者仍然可以使用在 TypeScript 中使用的 JavaScript 中的某些类型和对象。然而,TypeScript 生成的代码更干净、更优化,然后由 Angular 2 框架执行。所以,当 TypeScript 编译时,它会生成 JavaScript 并存储一个映射文件以处理调试场景。假设你想从 Visual Studio 2015 调试你的 TypeScript 代码;这个映射文件包含了源 TypeScript 文件和生成的 JavaScript 文件在 Angular 页面中运行的映射信息,并且可以在 TypeScript 文件上设置断点。

TypeScript 的编译架构

TypeScript 编译器通过几个阶段来编译 TypeScript 文件并生成 JavaScript 文件。

编译过程从预处理器开始,该预处理器通过遵循/// <reference path=…/>标签和import语句来确定需要包含哪些文件。一旦确定了文件,解析器就会解析和标记源代码到一个抽象语法树(AST)。

一个抽象语法树(AST)以树状节点的格式表示源代码的语法结构。绑定器然后遍历 AST 节点并生成和绑定符号。每个命名实体创建一个符号,如果有多个具有相同名称的实体,它们将具有相同的符号。

符号代表命名实体,如果找到多个声明,它会合并多个文件。为了表示所有文件的全局视图,构建了一个程序。程序是类型系统和代码生成的入口点。一旦创建了程序,就可以创建类型检查器和发射器。

类型检查器是 TypeScript 系统的核心部分,它将多个文件中的所有符号集中到一个视图中,并构建一个符号表。这个符号表包含了每个识别并合并成一个公共符号的符号的类型。类型检查器包含关于哪个符号属于哪个节点、特定符号的类型等完整信息。

最后,通过程序,TypeScript 编译器使用发射器(emitter)来生成输出文件:.js.js.map.jxsd.ts

TypeScript 的优点

以下是一些使用 TypeScript 配合 Angular 2 的核心好处。

JavaScript 的超集

TypeScript 是 JavaScript 的一个带类型超集,可以编译成 JavaScript。作为超集的基本优势在于,它提供了许多浏览器尚不支持的 JavaScript 最新特性。开发者在应用程序开发过程中使用诸如异步函数(async functions)、装饰器(decorators)等特性,这些特性编译后形成针对 ECMAScript 4 或 ECMAScript 3 版本的 JavaScript 文件,浏览器可以轻松理解和解释。

支持类和模块

TypeScript 支持 classinterfaceextendsimplements 关键字。

以下是 TypeScript 中如何定义类的示例:

class Person {
  private personId: string = '';
  private personName: string = '';
  private dateOfBirth: Date;
  constructor() {}
  getPersonName(): string {
  return this.personName;
  }
  setPersonName(value): void {
  this.personName = value;
}}

以下是 TypeScript 编译成 JavaScript 的版本:

var Person = (function () {
function Person() {
  this.personId = '';
  this.personName = '';
}
Person.prototype.getPersonName = function () {
  return this.personName;
};
Person.prototype.setPersonName = function (value) {
  this.personName = value;
};
  return Person;
})();

静态类型检查

使用 TypeScript 的主要好处是静态类型检查。当你构建你的项目时,TypeScript 编译器会检查语义,并在编译时给出错误以避免运行时错误。例如,以下代码将在编译时给出错误:

var name: string
name =2;//give error

以下是一个在编译时扩展 Person 类并给出类型不匹配错误的示例:

class Person {
  constructor(name: string) {
  }
}
class Employee extends Person{
  constructor() {
  super(2); //error 
  }
}

支持 ECMAScript 6 特性

在撰写本文时,大多数浏览器仍然不支持 ECMAScript 6 完全,但是有了 TypeScript,我们可以编写代码并使用 ECMAScript 6 特性。由于 ECMAScript 6 支持向后兼容,我们可以通过 TypeScript 配置文件设置目标版本,根据所指定的版本生成 JavaScript。这帮助开发者使用 ECMAScript 6 特性编写代码,生成的 JS 文件将基于 ECMAScript 3、ECMAScript 4 或 ECMAScript 5 标准。

可选类型

TypeScript 支持严格类型检查,并在编译时验证类型,但使用严格类型不是强制的。你甚至可以不指定变量的类型,在赋值时会解决它。

在 TypeScript 中声明类型

以下是没有声明变量类型的声明示例:

private sNo = 1;
private text = 'Hello world';

以下是声明具有类型的变量的示例:

private sNo: number = 1;
private text: string = 'Hello world';

TypeScript 的核心元素

本节讨论 TypeScript 的核心元素:

  • 声明变量

  • 类型

  • 类和接口

  • 函数

  • 迭代器

  • 模块和命名空间

声明变量

变量声明与我们在 JavaScript 中做的相当。然而,由于 TypeScript 遵循 ECMAScript 6 标准,它也提供了强类型。强类型可以通过在变量名后加上冒号 (:) 和其类型来声明。

以下是在 JavaScript 中的一个简单变量声明:

var name;

它可以在 TypeScript 中如下声明:

var name: string;

变量可以通过以下方式在 TypeScript 中初始化:

var name: string = "Hello World";    

类型

TypeScript 中大多数类型与 JavaScript 类型相当。以下表格包含所有可用类型的列表,以及使用它们的代码片段:

类型 描述 代码片段
数字 TypeScript 提供了一个数字类型,可以持有所有类型的十进制、十六进制、二进制和八进制值。
let decimal: number = 2;
let hex: number = 0x001;
let binary: number = 0b1010;
let octal: number = 0o744;

|

字符串 这与我们在其他任何语言中使用的一样。字符串值可以用单引号或双引号括起来。
let x: string = 'Hello';
let y: string = "Hello";

|

数组 TypeScript 支持简单数组和泛型数组。
let countries = ['US', 'UK', 'UAE'];
let countries<string> = ['US', 'UK', 'UAE'];

|

元组 通过元组,我们可以定义一个元素类型已知的数组。
let val: [string, number, Date];
val = ['Hello World', 10, new Date()];
val[0];//print Hello World

|

枚举 用以给数值命名。默认情况下,指定的第一个值是 0,但可以显式设置为任何数字。
enum Status {InProcess, Active, Ready, Success, Error}
let s: Status = Status.Active;
//specify values explicitly
enum Status {InProcess=1, Active=2, Ready=3, Success=4, Error=5}

|

任何 这个类型可以在不知道类型并且依赖于赋值的情况下使用。
let x: any;
x=['Hello', 1, 2]; //tuple;
x=1; //number
x='Hello World'; //string

|

类和接口

以下是在 TypeScript 中定义接口、派生类和接口以及编写泛型类的方法。

定义接口

就像 C# 一样,TypeScript 允许你定义接口,这些接口可以在 TypeScript 类中实现,并强制实现类实现接口中定义的所有成员。

以下是在 TypeScript 中定义接口的代码:

interface IShape {
  shapeName: string;
  draw();
}
class TodoService implements IShape  {
  constructor(private http: Http) {
  this.shapeName = "Square";
  }

  shapeName: string;

  draw() {
  alert("this is " + this.shapeName);
  }
}

派生类和接口

就像 C# 一样,类和接口可以通过从基类或接口派生来扩展。要扩展任何类,我们可以使用 extends 关键字,而对于接口,我们可以使用 implements,如下所示:

interface IPerson {
  id: number;
  firstName: string;
  lastName: string;
  dateOfBirth: Date;
}

interface IEmployee extends IPerson{
  empCode: string;
  designation: string;
}

class Person implements IPerson {
  id: number;
  firstName: string;
  lastName: string;
  dateOfBirth: Date;
}

class Employee extends Person implements IEmployee {
  empCode: string;
  designation: string;
}
IPerson and IEmployee. IPerson contains common properties such as id, firstName, lastName, and dateOfBirth, which can be used in all derived interfaces, such as IEmployee or any other.

然后,我们在 Person 类中实现了 IPerson 接口,最后从 Person 派生了 Employee 类并实现了 IEmployee 接口。如果你注意到了,由于 Person 类已经实现了 IPerson 接口,我们不需要再次实现它,只需要在 Employee 类中实现属性,比如 empCodedesignation

泛型类

泛型类用于定义一个类型是泛型的特定类,并在调用时确定它的类型。泛型类可以通过使用 <T> 加上类名来定义。

以下是一个简单示例,展示了泛型类的过程,可以根据初始化时指定的类型工作。getTypeInfo() 方法将根据初始化的对象类型打印特定的消息:

class Process<T>{
  value: T;
  getTypeInfo(){
  if (typeof this.value == "string")
    console.log("Type is a string");
  else if (typeof this.value == "number")
    console.log("Type is a number");
  else alert("type is unknown");

  }
}

let pString = new Process<string>();
pString.getTypeInfo(); //print Type is a string
let pNumber = new Process<number>();
pNumber.getTypeInfo(); //print Type is a number

函数

函数可以像 JavaScript 一样定义。TypeScript 支持命名和匿名函数。在 TypeScript 中,函数参数可以是类型参数,如下所示:

function concat(x: string, y: string): string {
  return x +" "+ y; 
}

函数也可以有可选参数,并且可以使用(?)来声明,如下所示:

function concat(x: string, y: string, z?: string): string {
  return x + " " + y + " " + z; 
}

有了这个选项,我们可以通过传递两个参数或三个参数来调用函数,因为第三个参数是可选的。

泛型函数

TypeScript 允许你定义泛型函数,它接受任何类型的参数或返回类型。泛型函数可以通过在函数名后指定<T>来定义,如下面的代码所示,参数或返回类型也可以是泛型的,并引用相同的T类型。这对于定义接受所有类型参数并按预期工作的特定函数很有用。以下示例展示了基于参数类型进行字符串拼接或相加的函数过程:

function process<T>(x: T, y: T): string{
  if (typeof x == "string")
  return x + " " + y;
  else if (typeof x == "number")
  return "Sum is: "+ x + y ;
  else 
  return "Type in unknown";
}

迭代器

除了 for、while等标准循环,TypeScript 还提供了两种 for 语句,for..offor..in。这两种语句都用于遍历集合。这两种之间的区别在于,for..of语句返回对象的键,而for..in返回值:

countries = ['USA', 'UK', 'UAE'];  
  //this loop will display keys 0, 1, 2
for (let index in this.countries) {
  console.log(index);
}
//this loop will display values USA, UK, UAE
for (let index of this.countries) {
  console.log(index);
}

模块和命名空间

ECMAScript 6 引入了模块的概念。模块可以被看作是具有自己作用域的逻辑容器。模块内部声明的任何类、变量或方法都局限于其自己的容器范围内,只有显式允许时才能被其他模块访问。在 TypeScript 中,任何包含导入或导出声明的文件都被视为模块。模块通过模块加载器相互导入,运行时模块加载器负责加载模块内定义的所有依赖项。模块可以通过使用export关键字导出,其他模块可以通过使用import关键字导入。

以下是在 TypeScript 中定义和导出一个模块的示例:

//BaseManager.ts
export class BaseManager{
}

在其他区域使用模块需要使用import关键字,如下所示:

//ServiceManager.ts
export class ServiceManager extends BaseManager{
}

可以通过使用import关键字导入模块。导入任何模块时,你必须使用import关键字,后面跟着类名的大括号{},后面跟着包含类的实际文件名。例如,以下代码展示了将ServiceManager导入到Main.ts的方式:

//Main.ts
import {ServiceManager} from "./ServiceManager"

我们也可以给类起一个友好的名字,如下所示:

//Main.ts
import {ServiceManager as serviceMgr} from "./ServiceManager"

另一方面,命名空间是用来分类类、方法等的逻辑模块。就像 C#一样,它们可以使用namespace关键字来定义。一个命名空间可以跨越不同的 TypeScript 文件,这为开发者提供了一种方便的方式来将特定的文件分类到单个命名空间中。以下示例展示了如何将 TypeScript 文件分类到单个命名空间中并使用它们:

//PersonManager.ts
namespace BusinessManagers{
  export class PersonManager{}
}
//SecurityManager.ts
namespace BusinessManagers{
  export class SecurityManager(){
}
}
//main.ts
/// <reference path="personmanager.ts" />
  ///  <reference path="SecurityManager.ts" />
personObj = new BusinessManagers.PersonManager();
securityObj =new BusinessManagers.SecurityManager();

如果你注意到了,我们使用了三斜线指令,它用于在执行 TypeScript 文件中的代码之前引用依赖文件。因此,由于这些文件存在于其他地方,我们不得不在前面的代码中显式引用它们。

总结来说,命名空间是比模块更好的使用方法,因为它们通过提供友好名称来逻辑地分类文件,并在处理中到大型项目时允许开发者正确地组织代码。

我们也可以给一个不友好的命名空间一个简短的名字,使用import关键词如下所示:

namespace BusinessManagers {
  export class PersonManager {

  }
}

import mgr = BusinessManagers;
let personObj = new mgr.PersonManager();

To LC: Apply code to:
"namespace BusinessManagers {
  export class PersonManager {

  }
}

import mgr = BusinessManagers;
let personObj = new mgr.PersonManager();

所以这总结了 TypeScript 的核心主题。要了解更多关于 TypeScript 的信息,你可以参考www.typescriptlang.org/

Angular 2 简介

Angular 2 是一个用于构建网络应用程序的客户端框架。在移动端和网络平台的使用上非常灵活。使用 Angular 的一个基本优势在于它遵循 ECMAScript 6 标准,开发者可以进行面向对象编程,定义类和接口,实现类,并使用普通旧 JavaScript 对象POJO)来绑定数据定义数据结构。在性能方面,单向数据流是一个很大的优势。与 Angular 1.x 不同,Angular 2 提供了双向数据绑定或单向数据绑定的选项。在某些情况下,单向绑定对性能有利。例如,提交表单时,与控件的双向绑定可能过于复杂。

Angular 2 架构

Angular2 包含许多组件。每个组件可以通过选择器绑定到页面,例如<my-app> </my-app>,或者路由模块。每个组件都有一个选择器,模板 HTML 或模板引用链接,指令,提供程序,和一个控制器类,其属性和方法可以在关联视图中访问。当网络应用程序第一次启动时,System.import加载应用程序的主要组件,引导根组件。这是一个引导 Angular 应用的主组件示例:

//Loading module through Import statement
Import {AppComponent} from 'path of my component'
bootstrap(AppComponent, [Providers]);

提供程序可以在方括号内定义。有各种可用的提供程序,我们将在后面的章节中讨论。

这个bootstrap对象在angular2/platform/browser中,可以用import命令导入 TypeScript 文件:

import {bootstrap} from 'angular2/platform/browser';

这个bootstrap对象指导 Angular 加载其中定义的组件。当组件被加载时,组件的所有属性或元数据都被评估。每个组件都应该有@Component注解,一些定义组件元数据的属性,以及一个或多个被称作组件控制器的类,它们包含可以通过定义在@Component 模板templateUri属性中的模板访问的属性和方法。

这是一个包含选择器、模板和类的app.component.ts示例,类名为AppComponent

//app.component.ts
import { Component, View} from 'angular2/core';
  import {bootstrap} from 'angular2/platform/browser';
  @Component({
  selector: "my-app",
  template: `<p>This is a first component</p>`,
  })  
  class AppComponent  {
  }
  bootstrap(AppComponent);

组件生命周期事件

当组件初始化时,它会经历几个事件,并有一个非常结构化的生命周期过程。我们可以实现这些事件来执行特定的操作。下面的表格展示了我们可以在组件控制器类中使用的 Events 列表:

事件 描述
ngOnInit() 组件初始化后且控制器构造函数执行时调用。
ngOnDestroy() 用于在组件被销毁时清理资源。
ngDoCheck() 用于覆盖指令的默认变更检测算法。
ngOnChanges(changes) 当组件选择器属性值中的任何一项被修改时调用。(选择器的自定义属性可以通过输入定义。)
ngAfterContentInit() 当指令的内容被初始化时调用。(指令稍后定义。)
ngAfterContentChecked() 每次检查指令的内容时都会被调用。
ngAfterViewInit() 当视图完全初始化时调用。
ngAfterViewChecked() 在每次检查组件视图时调用。

模块

模块代表一个包含类、接口等内容的容器,用于导出功能,以便其他模块可以使用import语句导入。例如,这是用于执行不同算术操作的 math.ts

//math.ts
import {Component} from 'angular2/core';
@Component({

})
export class MathService {
  constructor() {
  }
  public sum(a: number, b: number): number {
  return a + b;
  }
  public subtract(a: number, b: number): number {
  return a - b;
  }
  public divide(a: number, b: number): number {
  return a / b;
  }
  public multiply(a: number, b: number): number {
  return a * b;
  }
}

组件

组件是@Component注解定义元数据属性和相关控制器类的组合,该控制器类包含实际的代码,如类构造函数、方法和属性。@Component注解包含以下元数据属性:

@Component({
  providers: string[],
  selector: string,
  inputs: string[],
  outputs: string[],
  properties: string[],
  events: string[],
  host: { [key: string]: string },
  exportAs: string,
  moduleId: string,
  viewProviders: any[],
  queries: { [key: string]: any },
  changeDetection: ChangeDetectionStrategy,
  templateUrl: string,
  template: string,
  styleUrls: string[],
  styles: string[],
  directives: Array < Type | any[] >,
  pipes: Array < Type | any[] >,
  encapsulation: ViewEncapsulation
})

Angular 2 组件的核心属性

在定义组件时,我们可以指定各种属性,如前所述。在这里,我们将看到一些在创建 Angular 2 组件时通常需要的核心属性:

  • 模板和选择器

  • 输入和输出

  • 指令

  • 提供商

模板和选择器

下面的真实示例包含了在组件类中定义的模板和选择器。当按钮被点击时,它会调用logMessage()方法,该方法打印<p>元素中的消息。如果你注意到了,我们没有使用与类一起的export关键字,因为我们已经在同一个文件中引导了组件,而这个组件不需要在其他任何地方引用:

import { Component, View } from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
@Component({
  selector: "my-app",
  template: "<p> {{message}}</p><button (click)='logMessage()'>Log Message</button>"
})
class AppComponent {
  logMessage() {
    this.message = "Hello World";
  }
  message: string = "";
}
bootstrap(AppComponent);

应用选择器可以在 HTML 页面或 index.cshtml 页面中任何地方使用,如果是在 ASP.NET 项目中工作,并且模板将在其中渲染。以下是为自定义标签 my-app 使用的一个示例:

<html>
<body>
  <my-app></my-app>
</body>
</html>

一旦页面运行,它将渲染以下生成的源输出的输出:

<html>
<body>
  <p>Hello World</p>
  <button (click)='logMessage()'>Log Message</button>
</body>
</html>

输入和输出

child.component.ts and contains the selector as child; the template displays the Boolean values of the logToConsole and showAlert attributes specified in the child tag. The inputs contain the list of string variables that will be defined as the child tag attributes:
//child.component.ts
import { Component} from 'angular2/core';
@Component({
  selector: 'child',
  template: `<div> Log to Console: {{logToConsole}}, Show Alert: {{showAlert}} <button (click)="logMessage()" >Log</button> </div>`,
  inputs: ['logToConsole', 'showAlert'],
})

下面是包含logToConsoleshowAlert布尔变量的ChildComponent类。这些变量实际上持有从通知标签传递的值。最后,我们有一个logMessage()方法,当按钮点击事件发生时将被调用,并根据在层次结构中由父组件设置的值,在开发者的控制台日志消息或显示一个警告消息:

export class ChildComponent {
  public logToConsole: boolean;
  public showAlert: boolean;

  logMessage(message: string) {
    if (this.logToConsole) {
      console.log("Console logging is enabled");
    }
    if (this.showAlert) {
      alert("Showing alert message is enabled");
    }

  }
}

app.component.ts文件中,我们定义了主要的AppComponent,我们可以像下面的代码那样使用子选择器。在定义子选择器时,我们可以为ChildComponent中定义的自定义输入logToConsoleshowAlert设置值。这样,父组件就可以通过输入向子组件指定值。以下是AppComponent的完整代码:

//app.component.ts
import { Component, View } from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {ChildComponent} from './child.component';

@Component({
  selector: "my-app",
  template: `<child [logToConsole]=true [showAlert]=true></child>`,
  directives: [ChildComponent]
})
export class AppComponent {
}
bootstrap(AppComponent);

提示

ChildComponent:
  //child.component.ts
import { Component, EventEmitter, Output} from 'angular2/core';
@Component({
  selector: 'child',
  template: `<div> Log to Console: {{logToConsole}}, Show Alert: {{showAlert}}  <button (click)="logMessage()" >Log</button> </div>`,
  inputs: ['logToConsole', 'showAlert']
})
export class ChildComponent {
  public logToConsole: boolean;
  public showAlert: boolean;
  @Output() clickLogButton = new EventEmitter();

  logMessage(message: string) {
    this.clickLogButton.next("From child");
  }
}

@Output属性列出了clickLogButton作为ChildComponent可以发出的自定义事件,其父组件AppComponent将会接收。

我们在import语句中添加了EventEmitterEventEmitter是随 Angular 一起提供的内置类,提供定义和触发自定义事件的方法。一旦执行了logMessage()方法,它将执行来自ChildComponentclickLogButton.next()方法,最终调用AppComponent中注册的事件。

我们在AppComponent中添加了clickLogButton,如图下的代码所示。在 Angular 2 中,我们可以通过在括号()中指定事件名称,后跟当事件被触发时将被调用的方法,来指定事件。这就是事件注册的方式。在这里,logMessage是在AppComponent中定义的本地方法:

(clickLogButton)="logMessage($event)"

Here is the code snippet for AppComponent:

  //app.component.ts 
import { Component, View } from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {ChildComponent} from './child.component';

@Component({
  selector: "my-app",
  template: `<child [logToConsole]=true [showAlert]=true (clickLogButton)="logMessage($event)" ></child>`,
  directives: [ChildComponent]
})
export class AppComponent {

  logMessage(value) {
    alert(value);
  }
}
bootstrap(AppComponent);

logMessage方法是在从ChildComponent触发事件时将被调用的方法。

指令

指令是自定义标签,在运行时渲染 HTML,但将渲染内容封装在指令本身中。我们可以将其与 ASP.NET 中的标记帮助器相比较。指令分为三种:组件、结构型指令和属性指令:

  • 组件:这是一个带有模板的指令。

  • 结构型指令:这是一个用于添加或删除 DOM 元素的指令。Angular 提供了一些内置的结构型指令,如ngIfngSwitchngFor

  • 属性指令:它改变任何 DOM 元素的外观。

创建一个简单的 Hello World 指令

指令可以像创建组件一样简单地创建,并且可以通过其选择器标签在调用组件中引用。

下面是HelloWorldComponent的示例,它定义了一个简单的指令,以标题格式显示“Hello world”消息:

//helloworld.component.ts
import {Component} from 'angular2/core';

@Component({
  selector: "helloworld",
  template: "<h1>Hello world</h1>"
})

export class HelloWorldComponent {

}

下面的示例是使用此指令的组件。使用任何指令时,首先必须通过import语句导入,然后需要设置@Component元数据属性,才能在相关模板中访问它:

import { Component, View, provide, Inject } from 'angular2/core';
  import {bootstrap} from 'angular2/platform/browser';
  import {HelloWorldComponent} from './helloworld.component';

  @Component({
    selector: "my-app",
    template: `<helloworld></helloworld>`,
    directives: [, HelloWorldComponent],
  })
  export class AppComponent{

  }
  bootstrap(AppComponent);

此指令可以在页面上的使用如下:

<helloworld></helloworld>

结构指令

结构指令可以用来添加或移除 DOM 元素。例如,我们可以通过*ngFor添加一个国家列表作为表格,如下面的代码所示,并通过*ngIf指令隐藏或显示 div:

  <div *ngIf="display">
    <table>
      <thead>
        <tr>
          <th>
            Country
          </th>
          <th>
            Currency
          </th>
        </tr>
      </thead>
      <tbody *ngFor="#country of countries">
        <tr><td>{{country.CountryName}}</td><td>{{country.Currency}}</td></tr>
      </tbody>
    </table>
  </div>

以下是后端countries.component.ts文件,它使用 HTTP 模块调用 ASP.NET Web API 服务。它返回一个国家列表,分配给countries数组。display的默认值设置为true,生成表格。通过将display值设置为false,将不会生成表格:

///<reference path="../../node_modules/angular2/typings/browser.d.ts" />
import {Component} from 'angular2/core';
import {Http, Response} from 'angular2/http';

@Component({
  selector: 'app',
  templateUrl: 'Countries'
})
export class TodoAppComponent {
  countries = [];
  display: boolean = true;
  //constructor
  constructor(private http: Http) {
  }

  //Page Initialized Event Handler
  ngOnInit() {
    this.getCountries();
  }
  getCountries() {
    this.http.get("http://localhost:5000/api/todo").map((res: Response) => res.json())
      .subscribe(data => {
        this.countries = data;
      },
      err => console.log(err),
      () => console.log("done")
      );
  }

}

这就是如何在 Angular 2 中使用结构指令。在下一章中,我们将开发一个示例应用程序,并讨论每个艺术品以使用 Angular 2 进行 HTTP GETPOST请求。

属性指令

属性指令需要构建一个用@Directive注解标记的控制器类,并定义一个选择器来识别与它关联的属性。在下面的示例中,我们将开发一个简单的myFont指令,当它应用于任何页面元素时,会将文本更改为斜体。以下是font.directive.ts文件的内容:

import { Directive, ElementRef, Input } from 'angular2/core';
@Directive({ selector: '[myFont]' })
export class FontDirective {
  constructor(el: ElementRef) {
    el.nativeElement.style.fontStyle = 'italic';
  }
}
myFont directive applied.

在页面级别,它可以如下使用:

<p myFont>myFont is an Attribute directive</p>

提供者

提供者用于注册通过 Angular 2 的依赖注入框架实例化的类型。当组件初始化时,Angular 创建一个依赖注入器,它注册提供者数组中指定的所有类型。然后在构造函数级别,如果提供者数组中有任何类型,它将得到初始化并在构造函数中注入。

下面的示例是MathComponent,它将被注入到主应用组件构造函数中,并调用 sum 方法将两个数字相加:

//math.component.ts
import { Component } from 'angular2/core';
@Component({})

export class MathComponent {

  public sum(a: number, b: number) : number{
    return a + b;
  }
  public divide(a: number, b: number): number {
    return a / b;
  }
  public subtract(a: number, b: number): number {
    return a - b;
  }
  public multiply(a: number, b: number): number {
    return a * b;
  }

}

下面的例子是AppComponent,展示了如何导入一个math组件,然后定义提供者并在构造函数级别注入它:

//app.component.ts
import { Component, View } from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {MathComponent} from './servicemanager.component';
  @Component({
    selector: "my-app",
    template: "<button (click)="add()" >Log</button>",
    providers: [MathComponent]
  })  
  export class AppComponent  {
    obj: MathComponent;
    constructor(mathComponent: MathComponent) {
      this.obj = mathComponent;
    }
    public add() {
      console.log(this.obj.sum(1, 2));
    }
  }
  bootstrap(AppComponent);

还可以用稍微不同的方式通过注入 Angular 模块注入其他基本类型。我们还可以使用provide关键字定义一个类型,它需要一个键和值:

providers: [provide('Key', {useValue: 'Hello World'})]

前面的语法也可以在提供程序中定义类型时使用,如下所示:

providers: [provide(MathComponent, {mathComponent: MathComponent })]

定义providers时使用provide关键字的一个主要好处是在测试时。在测试应用程序时,我们可以用模拟或测试组件替换实际组件。例如,假设我们有一个类,它通过某种付费网关调用短信服务发送短信,在测试周期中我们不想使用生产短信网关组件,而是希望有一个自定义的测试组件,它只是将短信插入到本地数据库中。在这种情况下,我们可以将某个模拟类,如SMSTestComponent,与测试场景关联。

以下示例将字符串值注入到构造函数中。我们需要按照以下代码添加 Inject 模块,然后使用@Inject注入与键关联的值:

  //app.component.ts
import { Component, View, provide, Inject } from 'angular2/core';
  import {bootstrap} from 'angular2/platform/browser';
  import {MathComponent} from './servicemanager.component';
  @Component({
    selector: "my-app",
    template: `button (click)="logMessage()" >Log</button>`,
    providers: [MathComponent, provide('SampleText', {useValue: 'Sample Value'})]
  })
  export class AppComponent{
    obj: MathComponent;
    Val: string;
    constructor(mathComponent: MathComponent, @Inject('SampleText') value) {
      this.obj = mathComponent;
      this.Val = value;
    }

  public logMessage() {
    alert(this.kVal);
  }
  }
  bootstrap(AppComponent);

在 Angular 中的依赖注入

MathComponent in the providers array of the ChildComponent, and as it is defined in the ParentComponent, it is already injected by the Angular dependency injection module.
AppComponent (parent):
  //app.component.ts
  import { Component} from 'angular2/core';
  import {bootstrap} from 'angular2/platform/browser';
  import {MathComponent} from './servicemanager.component';
  import {ChildComponent} from './child.component';
  @Component({
    selector: "my-app",
    template: `<button (click)="callChildComponentMethod()">Log</button>`,
    providers: [MathComponent, ChildComponent]
  })
  export class AppComponent  {
    childObj: ChildComponent;
      constructor(childComponent: ChildComponent) {
      this.childObj = childComponent;

  }
    public callChildComponentMethod() {
      this.childObj.addNumbers(1, 2);   

    }
  }
  bootstrap(AppComponent);
MathComponent, which contains some basic arithmetic operations:
//math.component.ts
import { Component } from 'angular2/core';
@Component({})
export class MathComponent {

  public sum(a: number, b: number) : number{
    return a + b;
  }
  public divide(a: number, b: number): number {
    return a / b;
  }
  public subtract(a: number, b: number): number {
    return a - b;
  }
  public multiply(a: number, b: number): number {
    return a * b;
  }
}

最后,以下是ChildComponent的代码,其中没有在providers数组中定义MathComponent提供者:

//child.component.ts
import {Component} from 'angular2/core';
import {MathComponent} from './servicemanager.component';
@Component({
  selector: 'child-app',
  template: '<h1>Hello World</h1>'
})
export class ChildComponent {
  obj: MathComponent;
  constructor(mathComponent: MathComponent) {
    this.obj = mathComponent;
  } 
  public addNumbers(a: number, b: number) {
    alert(this.obj.sum(a, b));
  }
}

Angular 中的路由

在处理大型应用程序时,路由起着至关重要的作用。路由用于导航到不同的页面。路由可以在三个步骤中定义:

  1. 在任何组件级别定义@RouteConfig

    @RouteConfig([
      { path: '/page1', name: 'Page1', component: Page1Component, useAsDefault: true },
      { path: '/page2', name: 'Page2', component: Page2Component }]
    )
    
  2. 在锚点 HTML 标签上使用[routerLink]属性,并指定在@RouteConfig中配置的路由名称。

  3. 最后,添加<router-outlet>标签以在当前导航到的路劲上渲染页面。

以下示例包含两个组件,Page1ComponentPage2Component,主AppComponent定义了如下路由:

//app.component.ts
import {Component} from 'angular2/core';
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
import {Page1Component} from './page1.component';
import {Page2Component} from './page2.component';

@Component({
  selector: "my-app",
  template: `{{name}}
    <a [routerLink]="['Page2']">Page 2</a>
    <router-outlet></router-outlet>`,
  directives: [ROUTER_DIRECTIVES],
})
@RouteConfig([
  { path: '/', name: 'Page1', component: Page1Component, useAsDefault:true },
  { path: '/page2', name: 'Page2', component: Page2Component }]
)
export class AppComponent {
}

在前面的代码中,我们首先从angular2/router导入RouteConfigROUTER_DIRECTIVES,然后为页面 1 和页面 2 定义RouteConfig。在内联模板中,我们放置了锚点标签并为页面 2 定义了路由名称。当应用程序运行时,页面 1 被设置为根路径/上的默认页面,因此将在路由出口处显示页面 1 内容。当用户点击Page2链接时,将在同一位置渲染页面 2 的内容。

以下是page1.component.ts的代码:

//page1.component.ts
import {Component} from 'angular2/core';
@Component({
  template:'<h1>Page1 Content</h1>'
})
export class Page1Component {
}

以下是page2.component.ts的代码:

//page2.component.ts
import {Component} from 'angular2/core';

@Component({
  template: '<h1>Page2 Content</h1>'
})

export class Page2Component {
}

在 ASP.NET Core 中开发待办事项应用程序

我们已经学习了 Angular 2 的核心功能以及如何用 TypeScript 编写程序。现在该是用 Angular 2 和 ASP.NET Core 开发一个简单的待办事项应用程序的时候了。ASP.NET Core 是微软最新的网络开发平台,它比之前的 ASP.NET 版本更加优化和模块化。它提供了一种使用全局.NET Framework 的选项,或者是一个新的.NET Core,它基于应用程序运行,甚至包含在发布的 Web 应用程序文件夹中的框架二进制文件。使用新的 ASP.NET Core,我们不再依赖 IIS 来运行我们的应用程序,还有其他几种服务器可供跨平台使用 Kestrel。要了解更多关于 ASP.NET Core 的信息,请参考docs.asp.net

我们将通过一个逐步教程来构建一个可用的待办事项应用程序。下面的屏幕截图展示了主页的一个快照。用户登录后,它会显示所有可用的待办事项列表。用户可以通过点击创建待办事项按钮来添加一个新的待办事项,也可以删除现有的待办事项。在本章中,我们不会涵盖安全认证和授权模块,而是专注于如何使用 Angular 2 与 ASP.NET Core:

在 ASP.NET Core 中开发待办事项应用程序

在这个应用程序中,我们将有三个项目。TodoWebApp调用TodoServiceAppCommon被 Web API 使用,包含实体模型。以下图表显示了如何开发这三个项目以及配置和使用 Angular 2:

在 ASP.NET Core 中开发待办事项应用程序

创建一个 Common 项目

Common项目包含 Entity framework 将使用的实体,以便创建数据库。稍后我们在 Web API 项目中引用这个程序集:

  1. 创建.NET Core 类库项目:创建一个 Common 项目

  2. 添加一个新文件夹Models,并添加一个TodoItem类如下:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace Common
    {
      public class TodoItem
      {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public DateTime DueDateTime { get; set; }
        public int UserId { get; set; }
      }
    }
    

    前面的TodoItem类包含Id(主键)和TitleDescriptionDueDateTime以及UserID,用于为特定用户保存待办事项。

创建一个 TodoServiceApp 项目

在这个项目中,我们将创建一个 Web API,它将引用包含TodoItem POCO 模型的Common项目。在这个项目中,我们将公开服务并创建一个数据库仓库,该仓库将使用 Entity Framework Core 在 Microsoft SQL Server 数据库中执行创建、读取更新删除CRUD)操作:

  1. 创建一个新的 Web API 项目,选择 ASP.NET Core 模板。Web API 和 ASP.NET MVC 已经合并为一个统一的框架,因此没有单独的 Web API 项目模板。在这种情况下,我们将使用 ASP.NET Core 项目模板中的空项目模型。

  2. 打开project.json并添加对我们的Common程序集的引用:

    "dependencies": {
      "Microsoft.NETCore.App": {
        "version": "1.0.0-rc2-3002702",
        "type": "platform"
      },
      "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-rc2-final",
      "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final",
      "Common": "1.0.0-*"
    }
    

在 Web API 项目中启用 MVC

为了启用 MVC 项目,我们必须在ConfigureServices方法中调用AddMvc(),在Configure方法中调用UseMvc()

  1. project.json中添加 MVC 包:

    "Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final"
    
  2. ConfigureServices方法中调用AddMvc()

    public void ConfigureServices(IServiceCollection services)
      {
        services.AddMvc();
      }
    
  3. 最后,从Configure方法中调用UseMvc()

    public void Configure(IApplicationBuilder app)
      {
        app.UseMvc();
      }
    

安装 Entity Framework

以下是在项目中安装 Entity Framework 的步骤:

  1. 添加两个 Entity Framework 程序集,Microsoft.EntityFrameworkCore.SqlServerMicrosoft.EntityFrameworkCore.Tools,如下所示:

      "dependencies": {
      "Microsoft.NETCore.App": {
        "version": "1.0.0-rc2-3002702",
        "type": "platform"
      },
      "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-rc2-final",
      "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final",
      "common": "1.0.0-*",
      "Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final",
      "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0-rc2-final",
      "Microsoft.EntityFrameworkCore.Tools": {
        "type": "build",
        "version": "1.0.0-preview1-final"
      }
    }
    

添加 AppSettings 以存储连接字符串

ASP.NET Core 为存储应用程序设置提供了各种选项。默认的配置文件现在是appsettings.json,以 JSON 格式存储数据。然而,还有其他方法可用于将数据存储在环境变量、XML 和 INI 格式中。在这个项目中,我们将把连接字符串存储在appsettings.json文件中:

  1. 添加 ASP.NET 配置文件appsettings.json并指定连接字符串如下:

    {
      "Data": {
        "DefaultConnection": {
          "ConnectionString": "Data Source =.; Initial Catalog = tododatabase; Integrated Security = True;MultiSubnetFailover = False; "
        }
      }
    }
    
  2. project.json中添加以下包:

    "Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-final",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0-rc2-final",
    

在 Startup 类中配置 AppSettings

ASP.NET Core 的新配置系统基于System.Configuration。为了在我们的项目中使用设置,我们将在我们的Startup类中实例化一个Configuration对象,并使用Options模式访问个别设置。

Options模式将任何类转换为设置类,然后我们可以通过 ASP.NET 内置的依赖注入将该类注入到控制器中。通过options类,开发者可以访问设置键和值,如下所示:

  1. Startup类构造函数中,我们将使用ConfigurationBuilder对象添加appsettings.json文件。ConfigurationBuilder允许添加不同的提供程序,并有一个构建方法,该方法在不同提供程序中构建配置存储,并返回IConfigurationRoot实例:

    public Startup()
      {
        // Set up configuration sources.
        var builder = new ConfigurationBuilder()
          .AddJsonFile("appsettings.json")
        Configuration = builder.Build();
      }
    
      public IConfigurationRoot Configuration { get; set; }
    

    提示

    如果多个提供程序有相同的键,将使用在ConfigurationBuilder中指定的最后一个。

  2. 现在我们可以使用Configuration属性来访问连接字符串,如下所示:

    Configuration["Data:DefaultConnection:ConnectionString"];
    

在 Web API 中添加数据访问

在本节中,我们将添加一个TodoContextTodoRepository类来执行 CRUD 操作:

  1. 添加一个新文件夹,DataAccess,并添加TodoContext类,该类将从DbContext类派生。这是 Entity Framework 用于创建数据库的主要TodoContext类:

    using Common;
    using Microsoft.Data.Entity;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace TodoServiceApp.DataAccess
    {
      public class TodoContext : DbContext
      {
        public DbSet<TodoItem> TodoItem { get; set; }
      }
    }
    
  2. 我们现在需要覆盖OnConfiguring()方法,并调用DbContextOptionsBuilder对象的UseSqlServer()方法。每次Context对象被初始化时都会调用OnConfiguring()方法,它配置了指定的选项。UseSqlServer()方法接收在appsettings.json文件中定义的连接字符串,我们已在Startup类中对其进行了配置。现在我们想要将 app 设置对象注入到这个类中。为此,我们将使用Options模式。根据选项模式,我们不应该直接使用在Startup类中定义的Configuration属性,而是将创建一个包含我们 app 设置文件中相同键的定制 POCO 类,并重载默认的TodoContext构造函数,该构造函数接受IOptions<T>,其中T是我们的自定义 POCO 应用设置类。

  3. 因为连接字符串定义在一个嵌套对象中,所以我们的Data类将是这样的:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace TodoServiceApp
    {
      public class Data
      {
        public DefaultConnection DefaultConnection { get; set; }
      }
    
      public class DefaultConnection {
    
        public string ConnectionString { get; set; } 
      }
    }
    
  4. Startup类中,我们将调用services.Configure()方法,用appsettings.json文件中指定的键填充这个Data对象,并在我们接下来要创建的存储库中注入它。

  5. 创建一个TodoRepository类,其中包含一个ITodoRepository接口及其实现,TodoRepository。这个类将使用TodoContext对象执行数据库操作。以下是TodoRepository类的代码片段:

    using Common;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using TodoServiceApp.DataAccess;
    
    namespace TodoServiceApp.Repository
    {
      public interface ITodoRepository
      {
        void CreateTodo(TodoItem todoItem);
        void DeleteTodo(int todoItemId);
        List<TodoItem> GetAllTodos(int userId);
        void UpdateTodo(TodoItem todoItem);
      }
    
      public class TodoRepository : ITodoRepository
      {
        private TodoContext context;
        public TodoRepository()
        {
          context = new TodoContext();
        }
        public List<TodoItem> GetAllTodos(int userId)
        {
          return context.TodoItems.ToList();
        }
        public void CreateTodo(TodoItem todoItem)
        {
          context.TodoItems.Add(todoItem);
          context.SaveChanges();
        }
        public void DeleteTodo(int todoItemId)
        {
          var item = context.TodoItems.Where(i => i.Id == todoItemId).FirstOrDefault();
          context.Remove(item);
          context.SaveChanges();
        }
        public void UpdateTodo(TodoItem todoItem)
        {
          context.Update(todoItem);
          context.SaveChanges();
        }
    
      }
    }
    
  6. Startup类中,在ConfigureServices()方法中添加实体框架,如下面的代码所示。我们的 Web API 控制器将有一个带参数的构造函数,接收ITodoRepository对象。我们将使用services.AddScoped()方法在需要ITodoRepository的地方注入TodoRepository。最后,调用services.Configure()方法,用appsettings.json文件中指定的键填充Data对象:

    public void ConfigureServices(IServiceCollection services)
    {
      string connString = Configuration["Data:DefaultConnection:ConnectionString"];
      services.AddDbContext<TodoContext>(options => options.UseSqlServer(connString));
    
      services.AddMvc();
    
      services.AddScoped<ITodoRepository, TodoRepository>();
      services.Configure<Data>(Configuration.GetSection("Data"));
    
    }
    

在 ASP.NET Web API 中启用 CORS

在前一章中,我们学习了 CORS;我们必须在我们的 Web API 项目中启用 CORS,这样从 Angular 服务中我们就可以请求访问TodoService方法:

  1. Startup类的ConfigureServices方法中调用services.AddCors()

    services.AddCors(options => { options.AddPolicy("AllowAllRequests", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); });
    
  2. Startup类的Configure方法中调用app.UseCors()

    app.UseCors("AllowAllRequests");
    

运行数据库迁移

我们使用实体框架代码优先模型,所以现在我们想在 Microsoft SQL Server 中创建一个数据库。为此,我们首先将在TodoServiceAppproject.json文件中添加实体框架工具支持,然后运行.NET CLI 命令以添加迁移并创建数据库:

  1. project.json文件中添加Microsoft.EntityFrameworkCore.Tools,如下所示:

    "tools": {
      "Microsoft.AspNetCore.Server.IISIntegration.Tools": {
        "version": "1.0.0-preview1-final",
        "imports": "portable-net45+win8+dnxcore50"
      },
      "Microsoft.EntityFrameworkCore.Tools": {
        "imports": [ "portable-net451+win8" ],
        "version": "1.0.0-preview1-final"
      }
      },
    
  2. 现在我们可以运行命令,创建迁移并更新数据库。

  3. 要创建迁移,请打开命令提示符,导航到包含project.jsonTodoServiceApp项目中。

  4. 然后,运行dotnet ef migrations add Initial命令,其中Initial是创建的迁移的名称。运行此命令将在Migrations文件夹中添加一个包含有关 DDL 操作的代码的类。

    下面的屏幕快照显示了在运行前面的命令后创建的Migrations文件夹,以及创建的20160405115641_Initial.cs文件,该文件包含实际迁移代码片段,用于从数据库应用或移除迁移:

    运行数据库迁移

  5. 要创建数据库,我们需要在TodoServiceApp项目的project.json文件所在的同一目录中执行另一个命令:

    dotnet ef database update –verbose 
    
    
  6. 这将创建一个数据库,然后我们可以添加一个控制器来处理不同的 HTTP 请求并访问数据库。

创建控制器

按照以下步骤创建一个控制器:

  1. 在项目中添加一个新的Controllers文件夹,并添加一个名为TodoController的类。

  2. 以下是对TodoController类的代码片段:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    using Common;
    using TodoServiceApp.Repository;
    
    namespace TodoApi.Controllers
    {
      [Route("api/[controller]")]
      public class ToDoController : Controller
      {
        ITodoRepository repository;
        public ToDoController(ITodoRepository repo)
        {
          repository = repo;
        }
        // GET: api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
          return repository.GetAllTodos();
        }
        // GET api/values/5
        [HttpGet("{id}")]
        public IEnumerable<TodoItem> Get(int id)
        {
          return repository.GetAllTodos(id);
        }
        // POST api/values
        [HttpPost]
        public void Post([FromBody]TodoItem value)
        {
          repository.CreateTodo(value);
        }
        // PUT api/values/5
        [HttpPut("{id}")]
        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
          repository.DeleteTodo(id);
        }
      }
    }
    

现在我们已经完成了TodoService项目,接下来我们将开发一个待办事项网页应用程序项目,并配置 Angular 2。

创建 TodoWebApp 项目

我们将开发一个单页应用程序,并使用 MVC 视图用 Angular 2 渲染它。这个应用程序将有一个主要页面,列出特定用户的全部待办事项,而要添加一个新的待办事项,新的页面将在模态对话框窗口中打开:

  1. 首先,让我们使用 Visual Studio 2015 中可用的 ASP.NET Core 项目模板创建一个空项目,并将其命名为TodoWebApp

  2. project.json中添加 MVC 参考:

    "Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final",
    "Microsoft.AspNetCore.StaticFiles": "1.0.0-rc2-final",
    
  3. Startup类中,在ConfigureServices方法中添加AddMvc()方法,在Configure方法中添加UseMvc()方法。以下是Startup类的代码片段:

    namespace TodoWebApp
    {
      public class Startup
      {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
          services.AddMvc();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app)
        {
          app.UseStaticFiles();
          app.UseMvc(routes =>
          {
            routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
        }
    }
    

在 TodoWebApp 项目中配置 Angular 2

Angular 2 是 Node 模块的一部分,我们可以通过Node 包管理器NPM)配置文件package.json添加 Node 包。在package.json中,我们可以在devDependencies节点和dependencies节点添加包。devDependencies节点包含开发过程中使用的包,例如Gulp,它可以用来合并和压缩 JavaScript 和 CSS 文件,TypeScript 用于开发 Angular 2 组件,以及rimraf用于删除文件。在dependencies节点中,我们将指定如angular2systemjsreflect-metadatarxjszone.js等包,这些包在应用程序运行时使用:

  1. 从 Visual Studio 项目模板选项NPM 配置文件中添加一个新的package.json文件,并添加以下 JSON 片段:

    {
      "name": "ASP.NET",
      "version": "0.0.0",
      "dependencies": {
      "angular2": "2.0.0-beta.9",
      "systemjs": "0.19.24",
      "reflect-metadata": "0.1.3",
      "rxjs": "5.0.0-beta.2",
      "zone.js": "0.6.4"
      },
      "devDependencies": {
        "gulp": "3.8.11",
        "typescript": "1.8.7",
      }
    }
    
  2. 视觉工作室会自动下载并恢复package.json文件中指定的包,在项目本身创建一个node_modules文件夹,并将所有的包放在那里。Node_modules文件夹在视觉工作室中默认是隐藏的,但可以通过启用ShowAllFiles选项使其可见。

依赖项

以下是带有描述的依赖项列表:

  • angular2:这是 Angular 2 的包。

  • systemjs:它提供了System.import来连接 Angular 的主入口点。

  • reflect-metadata:这是一个添加装饰器到 ES7 的提案。通过这个,我们可以在 Angular 2 中指定我们类的元数据。

  • rxjs:它是一个反应式流库,允许处理异步数据流。

  • zone.js:它提供了一个在异步任务之间持续存在的执行上下文。

开发依赖项

以下是开发依赖项及其描述的列表:

  • gulp:用于将文件复制到wwwroot文件夹

  • typescript:用于编写 TypeScript 程序

配置 TypeScript

配置 TypeScript,请执行以下步骤:

  1. 添加一个Scripts文件夹,其中包含所有 TypeScript 文件。在当前版本的 ASP.NET 中,对命名此文件夹Scripts有一个限制,它应该添加到项目的根目录中;否则,TypeScript 文件将不会被转换成 JavaScript 文件。

  2. 添加Scripts文件夹后,添加 TypeScript 配置文件(tsconfig.json)并在其中添加以下配置:

    {
      "compilerOptions": {
      "noImplicitAny": false,
      "noEmitOnError": true,
      "removeComments": false,
      "sourceMap": true,
      "target": "es5",
      "module": "commonjs",
      "moduleResolution": "node",
      "outDir": "../wwwroot/todosapp",
      "mapRoot": "../scripts",
      "experimentalDecorators": true,
      "emitDecoratorMetadata": true
      },
      "exclude": [
        "node_modules",
        "wwwroot"
      ]
    }
    

compilerOptions节点中定义的配置在您构建项目时被 Visual Studio 使用。根据配置,JavaScript 文件被生成并存储在输出目录中。以下表格展示了前述代码中每个属性的描述:

编译器选项 描述
noImplicitAny 如果为true,则警告隐含any类型的表达式
noEmitOnError 如果为true,则在 TypeScript 中存在错误时不会生成 JavaScript
removeComments 如果为true,在生成 JavaScript 文件时移除注释
sourceMap 如果为true,则生成相应的映射文件
Target 设置目标 ECMA 脚本版本,如 ES5
modulez 指定生成代码的模块,如 commonjsAMDsystem
moduleResolution 指定模块解析策略,如 node
outDir 生成的 JavaScript 文件将被倾倒的路径
mapRoot 地图文件将位于的路径
experimentalDecorators 如果为true,则启用对 ES7 实验性装饰器的支持
emitDecoratorMetadata 如果为true,则在源中为装饰器声明发射设计类型的元数据

配置 Gulp

在本节中,我们将使用 Gulp 对 TypeScript 编译器生成的 JavaScript 进行压缩:

  1. 添加 Gulp 配置文件gulpfile.js

  2. Gulp 用于运行任务,Visual Studio 提供了一个任务运行窗口,列出了gulpfile.js中指定的所有任务,还允许我们将这些任务绑定到构建事件。

  3. gulpfile.js中添加以下脚本:

    /// <binding Clean='clean' />
    "use strict";
    
    var gulp = require("gulp")
    
    var paths = {
      webroot: "./wwwroot/"
    };
    var config = {
      libBase: 'node_modules',
      lib: [
        require.resolve('systemjs/dist/system.js'),
        require.resolve('systemjs/dist/system.src.js'),
        require.resolve('systemjs/dist/system-polyfills.js'),
        require.resolve('angular2/bundles/angular2.dev.js'),
        require.resolve('angular2/bundles/angular2-polyfills.js'),
        require.resolve('angular2/bundles/router.dev.js'),
        require.resolve('angular2/bundles/http.dev.js'),
        require.resolve('angular2/bundles/http.js'),
        require.resolve('angular2/bundles/angular2'),
        require.resolve('rxjs/bundles/Rx.js')
      ]
    };
    gulp.task('build.lib', function () {
      return gulp.src(config.lib, { base: config.libBase })
      .pipe(gulp.dest(paths.webroot + 'lib'));
    });
    

在前面的gulpfile.js中,我们首先声明了 Gulp 的对象。然后路径变量定义了静态文件的根目录(./wwwroot)。在 ASP.NET Core 中,所有静态文件都应该存放在wwwroot文件夹下;否则,它们无法被访问。现在我们需要将 Angular 和其他相关 JavaScript 文件复制到wwwroot文件夹。因此,我们添加了任务build.lib,调用gulp.src(),并链接了gulp.dest()方法,以从node_modules/*文件夹复制文件到wwwroot/lib文件夹。以下是wwwroot文件夹的截图,当你运行前面的步骤时,它创建了lib文件夹:

配置 Gulp

小贴士

任务可以通过 Visual Studio 中的任务运行窗口运行。

添加 Angular 组件

我们已经安装了 Angular 包并配置了 Gulp 以将打包的 JavaScript 文件复制到wwwroot文件夹。现在我们将向 Angular 组件中添加内容,以定义我们的主要应用选择器并在其中渲染 ASP.NET 页面:

  1. Scripts文件夹中,创建两个文件夹,appservicesapp文件夹包含我们将用在视图中的组件,而services文件夹包含用来调用 Web API 方法的服务。

  2. 添加一个主 TypeScript 文件,它将引导主要的TodoAppComponent。以下是main.ts的代码:

    //main.ts
    import {bootstrap} from 'angular2/platform/browser';
    import {TodoAppComponent} from './apps/todoapp.component';
    import {HTTP_PROVIDERS} from 'angular2/http';
    import 'rxjs/add/operator/map';
    
    bootstrap(TodoAppComponent, [HTTP_PROVIDERS]);  
    
bootstrap component to bootstrap our first TodoAppComponent. HTTP_PROVIDERS contains all the providers to make any HTTP request. It is provided while bootstrapping, so the TodoAppComponent or the chain of components in the following hierarchy can do HTTP-based operations. Rxjs/add/operator/map is a dependent package for HTTP_PROVIDERS, which needs to be added as well:
  1. 添加一个新的 TypeScript 文件,并将其命名为todoapp.component.ts

  2. TodoAppComponent中添加以下代码片段。为了首先测试一切是否配置正确,我们将仅仅添加一个显示Hello World的示例标题标签:

    //todoapp.component.ts
    ///<reference path="../../node_modules/angular2/typings/browser.d.ts" />
    import {Component} from 'angular2/core';
    
    @Component({
    
      selector: 'todo',
      template: '<h1>{{message}}</h1>'
    })
    
    export class TodoAppComponent {
      message: string = "Hello World";
    
    }
    
  3. 现在我们将添加两个文件,importer.jsangular_config.jsimporter.js调用System.import并指向引导应用程序组件的主文件。angular_config.js持有允许默认 JavaScript 扩展设置为true的配置属性。

    importer.js:
    
      System.import('todosapp/Main')
        .then(null, console.error.bind(console));
    

    以下是angular_config.js的代码:

    System.config({ defaultJSExtensions: true });
    
  4. 现在我们需要添加 MVC 布局页面并添加所有脚本。添加以下脚本:

    //_Layout.cshtml
    
      <environment names="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
        <link rel="stylesheet" href="~/css/site.css" />
        <script src="img/angular2-polyfills.js"></script>
        <script src="img/system.js"></script>
        <script src="img/angular_config.js"></script>
        <script src="img/Rx.js"></script>
        <script src="img/angular2.dev.js"></script>
        <script src="img/router.dev.js"></script>
        <script src="img/http.js"></script>
        <script src="img/importer.js"></script>
        <script src="img/jquery-2.1.4.min.js"
          asp-fallback-src="img/jquery.min.js"
          asp-fallback-test="window.jQuery">
        </script>
        <script src="img/bootstrap.min.js"
          asp-fallback-src="img/bootstrap.min.js"
          asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
        </script>
      </environment>
    
  5. 现在我们来添加HomeController和视图Index.cshtml

  6. Index.cshtml中,添加待办事项选择器todo-app

    @{
      ViewData["Title"] = "Todo Applications";
      Layout = "~/Views/Shared/_Layout.cshtml";
    
    }
    <div id="myCarousel" class="container" data-ride="carousel" data-interval="6000">
      <todo-app>Loading...</todo-app>
    </div>
    
  7. 构建并运行应用程序,它将显示Hello World添加 Angular 组件

添加待办事项服务组件

现在我们将添加services文件夹内的组件,这些组件将负责通过调用Todo服务获取数据:

  1. 首先,添加BaseService组件,其中包含baseURL。所有服务组件都将从BaseService派生,以便它们可以使用基本 URL 属性进行 Ajax 请求。添加一个新的 TypeScript 文件,命名为baseservice.component.ts。以下是baseservice.component.ts的代码片段:

    //baseservice.component.ts
    import {Component} from 'angular2/core';
    import {Http, Headers} from 'angular2/http';
    
    @Component({})
    export class BaseService {
      baseUrl: string;
      constructor() {
        this.baseUrl = "http://localhost:7105/api/";
      }
    }
    
  2. 现在添加todoservice.component.ts,其中包含获取所有待办事项、添加新待办事项和删除现有待办事项的方法。以下是TodoService的代码片段:

    //todoservice.component.ts
    import {Component} from 'angular2/core';
    import {Http, Headers} from 'angular2/http';
    import {BaseService} from '../services/baseservice.component';
    
    @Component({
      providers: [TodoService]
    })
    
    export class TodoService extends BaseService {
      constructor(private http: Http) {
        super();
      }
      public getTodoItems() {
        return this.http.get(this.baseUrl + 'todo/1');
      }
      public createTodo(item) {
        var path = this.baseUrl + 'todo';
        const headers = new Headers({ 'Content-Type': 'application/json' });
        return this.http.post(path, JSON.stringify(item), { headers: headers });
      }
    
      public deleteTodo(itemId) {
        var path = this.baseUrl + 'todo';
        return this.http.delete(path + "/" + itemId);
      }
    }
    
    TodoApp.Component.ts:
    
    //todoApp.component.ts
    ///<reference path="../../node_modules/angular2/typings/browser.d.ts" />
    import {Component} from 'angular2/core';
    import {Http, Response} from 'angular2/http';
    import {CreateTodoComponent} from '../apps/createTodo.component';
    import {TodoService} from '../services/todoservice.component';
    
    @Component({
      selector: 'todo-app',
      templateUrl: 'Todo',
      directives: [CreateTodoComponent],
      providers: [TodoService]
    })
    export class TodoAppComponent {
      //member variables
      todos = [
      ];
    
      //constructor
      constructor(private http: Http, private todoService: TodoService) {
      }
    
      //Page Initialized Event Handler
      ngOnInit() {
        this.getTodoItems();
      }
    
      //Member Functions
      getTodoItems() {
        this.todoService.getTodoItems().map((res: Response) => res.json())
          .subscribe(data => {
            this.todos = data
            this.parseDate();
          },
          err  => console.log(err),
          () => console.log('done')
          );
      }
      deleteTodoItem(itemID) {
        var r = confirm("Are you sure to delete this item");
        if (r == true) {
          this.todoService.deleteTodo(itemID)
            .map(r=> r.json())
            .subscribe(result => {
              alert("record deleted");
        });
      }
      this.getTodoItems();
      }
    
      parseDate() {
        for (let todo of this.todos) {
          let todoDate = new Date(todo.DueDateTime);
          todo.DueDateTime = todoDate;
        }
      }
    
      handleRefresh(args) {
        this.getTodoItems();
      }
    }
    

    TodoAppComponent中,我们首先添加了CreateTodoComponent指令,我们稍后在Todo/Index.cshtml页面中使用。我们实现了ngOnInit()事件处理程序,获取待办事项列表并将其绑定到todos数组对象。getTodoItems()方法调用TodoService获取待办事项列表,而deleteTodoItem()用于删除项目。

    Angular 中的每个请求都返回一个提供map方法的Observable响应对象,告诉 Angular 以特定格式解析响应。映射还返回Observable对象,可以用来在解析为 JSON 格式后订阅数据,正如我们的情况一样。最后,我们调用了subscribe方法,并将 JSON 响应数据发送到todos数组。为了处理错误,我们可以将调用与err方法链起来。无论响应状态是成功还是错误,都会调用匿名expression()方法。这意味着无论结果是成功还是错误,定义在匿名expression()方法下的代码都将执行。

    为了创建新的待办事项,我们稍后会创建另一个CreateTodoComponent,它将通过Outputs事件调用handleRefresh()方法来刷新列表,并在主页上反映新添加的项目。

创建主要待办事项页面

我们已经创建了将在 MVC 视图中使用的 Angular 组件。我们在上一节中已经引导了 Angular 组件,并在 Home/Index.cshtml 页面(我们应用程序的登录页面)中放置了 <todo-app> 标签。接下来,我们将创建一个自定义标签帮助器,然后添加一个 TodoController,并在索引页中使用这个标签帮助器。

创建一个自定义待办事项标签帮助器

在主页上,我们将列出特定用户的全部待办事项。为此,我们将在 ASP.NET 中创建一个自定义标签帮助器:

创建一个自定义待办事项标签帮助器

执行以下步骤来创建此标签帮助器:

  1. TodoWebApp 项目的根目录下创建一个新的 controls 文件夹,并添加一个 TodoTagHelper 类。以下是 TodoTagHelper 的代码,它使用 Angular 2 ngControl 将 Angular TodoAppComponent 的值绑定到表单:

      [HtmlTargetElement("todo")]
      public class TodoTagHelper : TagHelper
      {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
          string todo = "<div class='thumbnail'><div class='caption'><nav class='nav navbar-inverse' role='navigation'></nav>";
    
          todo += "<label class='date'>{{todo.DueDateTime | date:'short'}}</label> <img src='images/delete.png' (click)=deleteTodoItem(todo.Id)/>";
    
          todo += "<h4><a href='#'>{{todo.Title}}</a></h4>";
          todo += "<textarea readonly class='form-control' style='resize:none;' rows='4' cols='28'>{{todo.Description}}</textarea></div></div>";
          output.Content.AppendHtml(todo);
        }
      }
    
  2. _ViewImports.cshtml 中添加标签帮助器:

    @addTagHelper "*, TodoWebApp"
    

添加一个待办事项 MVC 控制器

TodoWebApp 项目中添加 TodoController 并指定两个方法用于索引视图,这是显示所有项目的主要视图以及创建新的待办事项项:

using System.Linq;
using Microsoft.AspNetCore.Mvc;
using TodoNotes.Models;

namespace TodoNotes.Controllers
{
  public class TodoController : Controller
  {
    public TodoController()
    {
      _context = context;  
    }
    // GET: Todo
    public IActionResult Index()
    {
      return View();
    }

    // GET: Todo/Create
    public IActionResult Create()
    {
      return View();
    }

}

为 TodoController 操作方法生成视图

Todo/Index.cshtml:
@{
  Layout = null;
}
<div class="col-md-3">
  <p class="lead">ToDo Items</p>
  <div class="list-group">
    <h4>
      <a href="#">Want to add new Todo?</a>
    </h4>
    <p>Click on the button below</p>
    <div class="col-md-3">
      <a class="btn btn-primary" data-toggle="modal" data-target="#todoModal">Create Todo</a>

    </div>
  </div>
</div>
<div id="todoModal" class="modal fade" role="dialog">
  <div class="modal-dialog">

    <!-- Modal content-->
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal">&times;</button>
        <h4 class="modal-title">Insert Todo</h4>
      </div>
      <div class="modal-body">
      <createTodo (refreshTodos)="handleRefresh($event)"></createTodo>
      </div>

    </div>
  </div>
</div>
<div class="col-md-9">
  <div class="row" >
    <div class="col-sm-4 col-lg-4 col-md-4" *ngFor="#todo of todos">
      <todo></todo>
     </div>

  </div>
</div>

在前面的 HTML 标记中,我们首先定义了一个按钮,该按钮会打开一个模态对话框 todoModal。在 todoModal 对话框的标记中,我们使用了 createTodo 指令,该指令定义在与此页面关联的 todoapp.component.ts 文件中,并且该链接实际上指向了 Todo/Create MVC 视图,该视图将在路由出口处渲染。通过路由链接和路由出口的组合,我们可以渲染模板。在 todoapp.component.ts 中,我们将看到如何在 Angular 中使用路由。最后,我们使用了自定义标签帮助器 <todo> 来显示待办事项列表中的每个项目。

开发待办事项创建组件

在本节中,我们将添加 Angular 组件,并将其命名为 CreateTodoComponent。这是因为我们将通过自定义 createTodo 选择器在一个模态对话框中打开一个新的 MVC 视图,并且 CreateTodoComponent 有一个保存数据库中新待办事项的方法,如下面的代码所示。

Scripts>apps 文件夹下添加一个新的 createtodo.component.ts,然后添加以下代码片段:

//createtodo.component.ts
///<reference path="../../node_modules/angular2/typings/browser.d.ts" />
import {Component} from 'angular2/core';
import {Http, Response} from 'angular2/http';
import {FormBuilder, Validators} from 'angular2/common';
import {TodoService} from '../services/todoservice.component';

@Component({
  selector: 'createTodo',
  templateUrl: 'Todo/Create'
})

export class CreateTodoComponent {

  @Output() refreshTodos = new EventEmitter();

  addTodoForm: any;

  constructor(fb: FormBuilder, private todoService: TodoService) {
    this.addTodoForm = fb.group({
      title: ["", Validators.required],
      description: ["", Validators.required],
      dueDateTime: ["", Validators.required]
    });
  }
  addTodoItem(): void {
    this.todoService.createTodo(this.addTodoForm.value)
      .map(r=> r.json())
      .subscribe(result => {});
    this.refreshTodos.next([]);
    alert("Record added successfully");
  }

}
Http and Response objects to handle the response received from TodoService. In the @Component annotation, we have defined the selector that is used in the parent TodoAppComponent component to render the Create Todo view inside the modal dialog.

FormBuilderValidator 用于定义具有特定验证器的属性,这些属性可以通过 ngControl 指令绑定到 HTML 表单。最后,我们有一个 addTodoItem 方法,它将在表单提交时被调用,通过调用 TodoService 在数据库中添加一个待办事项。

现在让我们在 Create.cshtml 中添加以下代码:

@{
  Layout = null;
}

<form [ngFormModel]="addTodoForm" (submit)="addTodoItem($event)" class="container" >
  <div class="form-horizontal">
    <div class="form-group">
      <label class="col-md-2 control-label">Title</label>
      <div class="col-md-10">
        <input ngControl="title" class="form-control" id="Title" placeholder="Enter Todo Title" [(ngModel)]="title" />
      </div>
    </div>
    <div class="form-group">
      <label class="col-md-2 control-label">Description</label>
      <div class="col-md-10">
        <textarea ngControl="description"  class="form-control" placeholder="Enter Description"></textarea>
        {{description}}
      </div>
    </div>
    <div class="form-group">
      <label class="col-md-2 control-label">Due Date</label>
      <div class="col-md-10">
        <input ngControl="dueDateTime" class="form-control" type="datetime-local" placeholder="Enter Due Date" />
      </div>
    </div>
    <div class="form-group">
      <div class="col-md-offset-2 col-md-10">
        <input type="submit" value="Create" class="btn btn-primary" />
      </div>
    </div>
  </div>
</form>
@section Scripts {
  <script src="img/jquery.min.js"></script>
  <script src="img/jquery.validate.min.js"></script>
  <script src="img/jquery.validate.unobtrusive.min.js"></script>
}
ngFormModel to the model we defined in the createtodo.component.ts and the submit form, and we are calling the addTodoItem method, which sends all the values bound with the ngControl directive. ngControl is a new directive introduced in Angular 2 that provides unidirectional binding. With forms, ngControl not only binds the value, but also tracks the state of the control. If the value is invalid, it updates the control with special CSS classes to tell the user that the value is invalid.

总结

在本章中,我们学习了 TypeScript 的核心组件以及如何使用 TypeScript 编写程序。我们还了解了 Angular 2 框架的核心基础和概念,并使用 ASP.NET Core、Angular 2、MVC 6(用于 Web API)和 Entity Framework Core(用于数据访问提供者)开发了一个简单的待办事项应用程序。在下一章中,我们将学习由微软开发的Windows JavaScript LibraryWinJS),并了解我们如何可以访问 Windows 运行时特性,更改 HTML 控件的外观,以及这个库中可用的其他选项。

第六章.探索 WinJS 库

网页开发已经导致了革命性的体验。有了像 bootstrap、material 等框架,我们如今能够使 Web 应用程序在不同的屏幕尺寸上运行得最好,并相应地调整其内容。开发者将 Web 应用程序目标定为不同的平台,为他们的客户提供一致的体验。例如,任何使用 bootstrap 和其他框架的 Web 应用程序都可以在浏览器、平板电脑和移动设备上运行,提供前所未有的最佳用户体验。有了这些好处,新的前景被引入,允许 Web 应用程序针对不同的设备,这也带来了访问客户端特定特性和布局的需要。有了这些革命性的体验,公司开始引入基于 JavaScript 的库,这些库不仅改变了在设备上运行应用程序的外观和感觉,还允许开发者使用特定设备的功能,如发送弹窗通知、访问相机上传图片等等,从而提升用户体验。

WinJS 简介

Windows JavaScriptWinJS)库是由微软开发的开源 JavaScript 库。它在 2014 年 4 月的微软构建会议上发布,随着 Windows 10 的发布,微软正式发布了 4.0 版本。目前它是开源的,并受到 Apache 2.0 许可的约束。

它最初是为基于 JavaScript、CSS 和 HTML 的 Windows 商店应用设计的,后来也支持现代浏览器。今天,开发者可以使用 JavaScript、CSS 和 HTML 为任何平台开发移动应用程序,包括 Windows 应用、Android 应用和 iOS 应用,并可以使用这个库将用户界面UI)转换为具有访问 Windows 运行时功能的本地移动界面。WinJS 库不仅暴露了 Windows 运行时模块,还为 Web 应用程序提供了 Windows UI 控件设置。WinJS 提供了 Windows 运行时特性,如类和运行时组件,它们可以通过 JavaScript 代码访问。用户可以构建应用程序以访问设备功能,如相机、存储、地理位置、文件系统,并样式化应用程序以提供最佳用户体验。它还提供了一层安全性,使设备功能保持安全并保护它们免受恶意攻击。至于浏览器兼容性,所有现代浏览器,包括 Microsoft Edge、Google Chrome 等,都支持这个库。基本优势是,现在 Web 开发者可以使用 WinJS 控件套件和库来使用 Windows 运行时特性构建 Windows 商店应用程序。此外,微软还授权 WinJS 库与 AngularJS、Knockout、Ember 和 Backbone 等流行的客户端框架集成,您可以在 HTML 中使用 WinJS 指令与其他控件指令一起使用,并且按预期工作。

WinJS 特性

WinJS 不仅是为了服务于基于 HTML 和 JavaScript 的通用 Windows 应用而设计的,它也是一个通用的 JavaScript 库,可以与 Web 应用程序一起使用。WinJS 带来了各种功能,我们将在接下来的章节中讨论。

JavaScript 编程和语言模式

WinJS 提供了定义自定义命名空间和类的编程模式,以及执行绑定实现和承诺的编程模式。

样式表

它提供了两套样式表,分别是 UI-dark 和 UI-light,可以与 HTML 元素一起使用,给特定的 Windows 应用带来主题外观。此外,它允许你处理不同的屏幕大小和方向,如景观和肖像。

Windows 运行时访问

我们可以访问 Windows 运行时功能,如文件系统、相机、地理定位等,这些功能可以通过本地应用程序 API 来使用。

安全性

通过提供启用 Windows 运行时功能的选项,WinJS 也限制了对设备上敏感数据的访问。

应用模型

应用模型提供了由 Windows 应用程序发起的事件,可以在我们的 JavaScript 中注册以执行特定操作。例如,挂起、恢复和初始化是一些我们可以在 WinJS 中注册以处理特定任务的有用事件。

数据绑定

就像其他框架如 AngularJS、KnockOut 等一样,WinJS 也提供了特定的数据绑定指令和语法,用于将 HTML 控件与在 JavaScript 代码中提供的数据绑定。

控件

WinJS 提供了除了扩展的 HTML 元素属性之外的特定控件。这些控件可以在本地 Windows 应用项目中使用,并且我们可以通过 WinJS 将它们用于我们的 HTML 页面,带来相同的体验。

实用工具

WinJS 提供了几个实用工具来执行本地化、动画和 DOM 选择器。

WinJS 的使用

微软已经使用 WinJS 库开发了各种应用程序。像 Skype、Store、Weather、News 等应用程序都是使用 HTML、CSS 和 JavaScript 以及 WinJS 库开发的。Web 开发的现代时代使 JavaScript 成为开发响应迅速且功能丰富的应用程序的核心框架,这些应用程序可以在任何平台和设备上运行。这使得微软对 WinJS 进行了大量投资,并使这个库对希望创建 Windows 应用或从 Web 应用中使用 Windows 平台功能的 Web 开发者变得有用。随着通用 Windows 平台UWP)的发布,微软发布了新的通用应用平台UAP),它是 Windows 8 应用程序所使用的 WinRT 平台的一个超集。通过 UWP 引入了新的托管应用概念,它允许任何 Web 应用程序通过非常少的配置属性转换为 Windows 应用。

在 ASP.NET 应用程序中添加 WinJS 库

WinJS 可以通过 Node 包管理器(NPM)、NuGet 以及引用 CDN 来添加。这取决于你是否希望将文件保存在服务器上的本地还是作为 CDN 的引用。

CDN

以下是在你的应用程序中可以添加的包含 JavaScript 和 CSS 文件的 CDN 库:cdnjs.com/libraries/winjs

NPM

要使用 NPM 安装它,你可以运行npm install winjs,或者在 ASP.NET 核心应用程序中工作时,只需在package.json文件中添加winjs包。

NuGet

要通过 NuGet 安装它,你可以在 NuGet 包管理器控制台中添加 WinJS 包,或者在 ASP.NET 应用程序中运行以下命令:

Install-Package WinJs

WinJS 包包括一组 JavaScript 文件和 CSS 样式表,用于更暗或更亮的 UI。以下表格定义了文件及其用途:

文件 类型 使用
Base.js JavaScript 这是一个核心模块,它被UI.js用来提供 Windows 运行时特性
UI.js JavaScript 包含 UI 控件
WinJS.intellisense.js JavaScript 在使用 WinJS 组件时提供 intellisense
ui-dark.css CSS 用于更暗的 UI 主题的样式表
ui-light.css CSS 用于更亮的 UI 主题的样式表

开始使用 WinJS

微软在 Visual Studio 中提供了一些模板,以使用 JavaScript 和 HTML 开发商店应用程序,另一方面,我们也可以将其添加到我们的 ASP.NET 应用程序中,以带来 Windows 运行时特性的一些功能或相应地改变外观和感觉。

在 ASP.NET 应用程序中使用 WinJS

你可以通过添加 JavaScript 来使用 Windows 运行时特性以及 CSS 来让 UI 看起来像 Windows 应用程序来开始使用 WinJS。在 ASP.NET 网络应用程序中,你可以通过 NPM 添加包,通过创建一个条目,如下所示:

在 ASP.NET 应用程序中使用 WinJS

保存文件后,包将自动在 Visual Studio 2015 中的node_modules\npm文件夹中下载。

以下是 WinJS 库包含的文件夹的屏幕截图。JS 包含winjs模块、cssfonts,可以用来改变 UI 的外观和感觉:

在 ASP.NET 应用程序中使用 WinJS

你可以使用Gulp.jscssjs文件复制到wwwroot文件夹并在页面上引用它们,我们可以在按钮点击事件中添加以下示例代码来显示You have clicked!文本:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="img/base.js"></script>
    <script src="img/ui.js"></script>
    <script src="img/winjs.intellisense-setup.js"></script>
    <script src="img/winjs.intellisense.js"></script>
    <script src="img/jquery-1.9.0.js"></script>
    <link rel="stylesheet" href="lib/winjs/css/ui-dark.css" />
</head>
<body>    
    <div class="win-container">
        <button class="win-button" id="btn">Show</button>
        <span id="txtMessage"></span>
    </div>
    <script>
        (function () {
            WinJS.UI.processAll().done(function () {
                $('#btn').click(function () {
                    $('#txtMessage').text("You have clicked!");
                });
            });
        })();
    </script>
</body>
</html>

以下是输出结果:

在 ASP.NET 应用程序中使用 WinJS

页面加载时,将会执行一个函数,当所有 WinJS 控件都被处理时,为按钮注册点击事件。WinJS.UI.processAll()方法解析整个文档对象模型(DOM),寻找需要处理的 WinJS 控件,并在所有控件的绑定完成后返回一个承诺。

在幕后,WinJS.UI.processAll()只处理isDeclarativeControlContainer属性设置为true的控制器。这告诉 WinJS 需要与 WinJS 库绑定的控制器。如果您使用自定义控制器,则需要指定这个isDeclarativeControlContainer属性,以便它可以被 WinJS 处理。

事件可以通过声明性绑定或通过从 JavaScript 注册事件来注册。在前面的代码中,我们通过 JavaScript 注册了按钮点击事件;然而,在声明性方面,您也可以设置事件并调用一些可以在按钮被点击时调用的 JavaScript 函数。

在 Visual Studio 中现有的 Windows 应用模板

default.js:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>App1</title>

    <!-- WinJS references -->
    <link href="WinJS/css/ui-dark.css" rel="stylesheet" />
    <script src="img/base.js"></script>
    <script src="img/ui.js"></script>

    <!-- App1 references -->
    <link href="/css/default.css" rel="stylesheet" />
    <script src="img/default.js"></script>
</head>
<body class="win-type-body">
    <p>Content goes here</p>
</body>
</html>
for default.js:
(function () {
  "use strict";

  var app = WinJS.Application;
  var activation = Windows.ApplicationModel.Activation;

  app.onactivated = function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
      if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
      } else {
      }
      args.setPromise(WinJS.UI.processAll());
    }
  };

  app.oncheckpoint = function (args) {
  };

  app.start();
})();

探索 WinJS 核心基础

在使用 WinJS 库的任何项目之前,最好了解核心概念,帮助我们编写高质量的程序并充分利用库的最佳功能。

类和命名空间

通过 WinJS,我们可以使用一些特殊的语法来创建类和命名空间。这是 WinJS 库提供的,用于处理复杂场景。正如我们所知,类和命名空间是 ECMAScript 6 的功能,但不幸的是,目前还没有浏览器有适当的实现。然而,借助 WinJS,我们可以定义类和命名空间,并且在需要的地方使用它们是一个有用的选择。

在 WinJS 中定义类

在 WinJS 中,可以通过WinJS.Class.define()方法定义类。以下是 WinJS 中类的示例代码:

<script>
        var Logger = WinJS.Class.define(function (value) {
            //constructor
            console.log("Constructor is executing, value passed is: " + value );
        }     
        );
   //Initializing Logger class object    
    var log = new Logger("Hello World");
</script>

在前面的代码中,我们创建了一个名为Logger的类,其中第一个函数的参数是构造函数,第二个是任何instanceMembers如属性和方法,第三个是staticMembers以定义静态成员和属性。以下是define方法的签名:

在 WinJS 中定义类

现在让我们在同一个类Logger中添加属性messageLogMessage()方法:

<script>
        var Logger = WinJS.Class.define(function (value) {
               this.logName = value;
               this.enabled;
         //constructor
            console.log("Constructor is executing, value passed is " + value );
        }, {
            logMessage: function (message) {
                if (this.logEnabled) {
                    alert("The message is" + message);
                }
            },
  logEnabled: {
                 get: function () { return this.enabled; },
                 set: function (value) { this.enabled = value; }
             }
        }
        );

       var log = new Logger("Sample log");
       log.logEnabled = true;
       log.logMessage("Hello World");
<script>

定义类的方法语法的名称后面跟着一个冒号(:)和函数体,如下所示:

logMessage: function (message) {
              alert("The message is" + message);
       }

属性可以通过getset函数方法定义,如下面的代码所示:

logEnabled: {
                get: function () { return this.enabled; },
                set: function (value) { this.enabled = value; }
            }

可以在相同的方式下定义多个属性和方法, separated with comma,如下面的代码所示:

  logEnabled: {
                get: function () { return this.enabled; },
                set: function (value) { this.enabled = value; }
             },

            logType: {
                get: function () { return this.loggerType; },
                set: function (value) { this.loggerType = value;}
            }

在 WinJS 中派生类

在 WinJS 中,可以通过使用WinJS.class.derive()方法来派生类。考虑前面的例子,我们也可以在基类上添加logEnabledlogType属性,然后从BaseLogger类派生出Logger类。以下是 WinJS 中派生类的代码:

<script>
        var BaseLogger = WinJS.Class.define(function (logName) {
            this.enabled;
            this.loggerType;
            this.loggerName = logName;
        }, {
            logEnabled: {
                get: function () { return this.enabled; },
                set: function (value) { this.enabled = value; }
            },

            logType: {
                get: function () { return this.loggerType; },
                set: function (value) { this.loggerType = value; }
            }
        });

        var Logger = WinJS.Class.derive(BaseLogger, function (logName) {
            //calling base constructor and passing the LogName to the base constructor
            BaseLogger.call(this, logName);
        },
        {
            logMessage: function (message) {
                if (Object.getOwnPropertyDescriptor(BaseLogger.prototype, "logEnabled").get.call(this) == true) {
                    alert("The message is " + message);
                }
            },

        }
        );

        var log = new Logger("Hello World");
        log.logEnabled = true;
        log.logType = "Alert";
        log.logMessage("hello");
</script>

在上面的脚本中,我们将logTypelogEnabled这两个属性都移到了基类BaseLogger中。在 WinJS 中,基属性可以通过以下语法访问:

Object.getOwnPropertyDescriptor(BaseLogger.prototype, "logEnabled").get.call(this)

通过在getOwnPropertyDescriptor()方法调用后调用set方法进行设置:

Object.getOwnPropertyDescriptor(BaseLogger.prototype, "logEnabled").set.call(this) = true;

现在,如果您想要在BaseLogger类上采用logMessage()方法,我们可以通过原型化来实现,如下所示:

BaseLogger.prototype.logMessage.call(this);

WinJS 中的命名空间

在面向对象编程中,命名空间在组织类和分类代码方面发挥着重要作用。例如,服务可以位于 ApplicationName.Services 命名空间下;模型可以位于 ApplicationName.Models 命名空间下,等等。

我们应该尽可能地使用命名空间,因为它可以解决许多在中型到大型项目中可能出现的问题。例如,我们在页面上添加的两个 JavaScript 文件具有相似的属性或函数名称。如果有相同的名称,后来引用的将覆盖先前的成员函数或变量。

WinJS 提供了逻辑上组织类到命名空间的简单方法,你可以通过调用 WinJS.Namespace.define("命名空间名称", {}) 来定义一个命名空间。

以下示例将 BaseLoggerLogger 类封装到 Demo.App.Utilities 命名空间中:

WinJS.Namespace.define("DemoApp.Utilities", {
            //BaseLogger class
            BaseLogger: WinJS.Class.define(function (logName) {
                this.enabled;
                this.loggerType;
                this.loggerName = logName;
            }, {
                logEnabled: {
                    get: function () { return this.enabled; },
                    set: function (value) { this.enabled = value; }
                },

                logType: {
                    get: function () { return this.loggerType; },
                    set: function (value) { this.loggerType = value; }
                },
            }),

            //Logger class
            Logger: WinJS.Class.derive(BaseLogger, function (logName) {
                //calling base constructor and passing the LogName to the base constructor
                BaseLogger.call(this, logName);
            },
            {
                logMessage: function (message) {
                    if (Object.getOwnPropertyDescriptor(BaseLogger.prototype, "logEnabled").get.call(this) == true) {
                        alert("The message is " + message);
                    }
                },

            })
        });

现在,可以通过指定其命名空间来访问 Log 类,如下面的代码所示:

var log = new DemoApp.Utilities.Logger("Sample Logger");
        log.logEnabled = true;
        log.logType = "Alert";
        log.logMessage("hello");

混入

大多数语言不支持多重继承。然而,在 WinJS 中,我们可以通过混入(mixins)来实现。与类一样,mixin 是一组方法和属性的集合,但混入的对象不能被实例化。它用于与类混合,带来混入所具有的方法和属性。例如,以下是一个包含 logMessage() 方法的混入 logMixin

  var logMixin = {
     logMessage: function (message) {
                alert(message);
            }
        };

        var SampleClass = WinJS.Class.define(function(){

        });

        WinJS.Class.mix(SampleClass, logMixin);

        var sample = new SampleClass();

        sample.logMessage("Mixin");

在调用 mix 方法时,我们可以添加尽可能多的混入。如果有两个或更多的混入具有共同的方法或属性,后来的将覆盖现有的。让我们看看两个混入(分别为 logMixinlogConsoleMixin)以及一个 SampleClass 都有一个相同的 logMessage() 方法。根据规范,方法将被覆盖,当调用 logMessage() 时,它将在控制台日志上写入一条消息:

        //First Mixin
        var logMixin = {
            logMessage: function (message) {
                alert(message);
            }
        };

        //Second Mixin
        var logConsoleMixin = {
            logMessage: function (message) {
                console.log(message);
            }
        }

        //Class
        var SampleClass = WinJS.Class.define(function () {

        },

        logMessage= function(message){
        var result = confirm(message);
        });

        WinJS.Class.mix(SampleClass, logMixin, logConsoleMixin);

        var sample = new SampleClass();
        sample.logMessage("Mixin");

WinJS 中的事件

WinJS 提供了 eventMixin 对象,可以通过以下基本步骤注册、注销和分发事件:

  1. 首先,我们需要调用分发事件的类需要添加 WinJS.Utilities.eventMixin。我们可以通过 WinJS.Class.mix 方法添加它,如下所示:

    WinJS.Class.mix(SampleClass, WinJS.Utilities.eventMixin);
    
  2. 一旦 eventMixinSampleClass 继承,我们就可以调用 dispatchEvent() 方法在特定动作上分发事件。以下是 Sample 类在调用 execute 方法后分发事件的代码:

            var SampleClass = WinJS.Class.define(function () {
            },
            {
                execute: function(message){
                    this.dispatchEvent("executeInvoked", { message: "Executed" });
                }
            });
    
  3. 接下来,我们可以添加 addEventListener() 方法,并提供将在分发消息被调用时调用的 eventHandler()

            var sampleClass = new SampleClass();
            var sampleEventHandler = function (event) {
    
                alert(event.detail.message);
            };
            sampleClass.addEventListener("executeInvoked", sampleEventHandler);
    
            sampleClass.execute("hello");       
    

Databinding

WinJS 提供了将任何 JavaScript 数据源绑定到 HTML 元素的简单方法。任何 JavaScript 数据源都可以使用 HTML 元素上的 data-win-bind 属性进行绑定。数据绑定有助于将数据与视图分离,并允许您编写更少的代码,并使用 WinJS 将数据与元素绑定,WinJS 提供了以下三种数据绑定类型。

一次性数据绑定

一次性数据绑定用于将 HTML 页面上的元素从 JavaScript 数据源绑定。它是单向的,这意味着如果 JavaScript 数据源被更新,它不会反映在绑定到的 HTML 上。

以下是具有两个控件的 HTML 代码,它将名称和描述属性与您在 JavaScript 中定义的视图模型绑定:

<div id="rootDiv">
        <div> Course Name:
            <span id="divForm" data-win-bind="innerText: name">loading</span>
        </div>
        <div>
            Course Description:
            <span id="divForm" data-win-bind="innerText: description">loading</span>
        </div>
    </div>
Below is the JavaScript code which defines the view model
let ViewModel = WinJS.Class.define(function () {
                this.nameProp;
                this.descProp;
            },
            {
                name: {
                    get: function () { return this.nameProp; },
                    set: function (value) { this.nameProp = value; }
                },
                description: {
                    get: function () { return this.descProp; },
                    set: function (value) { this.descProp = value; }
                }
            });

            let viewModel = new ViewModel();
            viewModel.name = "WinJS databinding";
            viewModel.description = "Introduction to WinJS databinding";
            var personDiv = document.querySelector('#rootDiv');
            WinJS.Binding.processAll(personDiv, viewModel);

单向绑定

单向绑定是一种单向绑定。一旦 HTML 元素被绑定到 JavaScript 数据源,数据源的任何更改都会在 HTML 页面上反映出来,但如果在 HTML 元素上进行了更新,它不会更新后端 JavaScript 数据源。单向绑定可以通过使源模型可观察来实现。所以如果源对象上有什么变化,它就会更新与之绑定的 UI 元素。这可以通过使用WinJS.binding.as()方法或在与源类添加observableMixin来实现。

以下是一个单向绑定的示例,它将属性NameDescription绑定到按钮点击事件上,更新 HTML 元素并设置从后端数据源设置的值。在之前的 HTML 页面中添加按钮,如一次性数据绑定部分所示:

//HTML markup
<button id="btnUpdate">Click</button>

//JavaScript
   let ViewModel = WinJS.Class.define(function () {
                      this.nameProp;
                      this.descProp;
           },
           {
 name: {
    get: function () { return this.nameProp; },
    set: function (value) { this.nameProp = value; }
        },
 description: {
    get: function () { return this.descProp; },
    set: function (value) { this.descProp = value; }
        }
 });

let viewModel = new ViewModel();
viewModel.name = "WinJS databinding";
viewModel.description = "Introduction to WinJS databinding";
var personDiv = document.querySelector('#rootDiv');

let observableViewModel = WinJS.Binding.as(viewModel);

   WinJS.Binding.processAll(personDiv, observableViewModel);

document.querySelector('#btnUpdate').onclick = function () {
   observableViewModel.name = "new name";
  observableViewModel.description ="new description";
}

双向数据绑定

双向数据绑定在两个方向上起作用。一旦 JavaScript 对象被绑定到 HTML 控件上,无论在控件本身上进行任何更改,还是在 JavaScript 对象的价值发生更改时,控件值都将被更新,反之亦然。在 WinJS 中实现双向绑定不是一件简单的事。我们需要有一种单向绑定在位,以反映后端数据源上发生的任何变化,并反映在前端,同时也要从任何在 UI 元素上进行的更改更新后端数据源。这可以通过实现onPropertyChange()onKeyDown()onChange()onClick()以及其他基于 HTML 元素的函数来完成:

someTextboxElement.onpropertychange=function(){
      someModel.property = someTextboxElement.value;
}

另一种方法是实现一个自定义绑定初始化器,如以下代码所示:

<input type="text" data-win-bind="value: someProperty Binding twoWayBinding" />

让我们创建一个自定义的双向绑定初始化器,并扩展相同的viewModel以通过文本框接受名称和描述的更新。以下是我们的自定义双向绑定初始化器的代码:

//Defining Binding initializer to support two way binding
WinJS.Namespace.define("Binding.Mode", {
      twoway: WinJS.Binding.initializer(function 
                        (source, sourceProperties, destination, destinationProperties) {
          WinJS.Binding.defaultBind(source, sourceProperties, destination, destinationProperties);
          destination.onchange = function () {
              var destValue = destination[destinationProperties[0]];
              source[sourceProperties[0]] = destValue;
          }
      })
  });

然后创建一个包含两个属性的类,分别为namedescription

//Defining class
let ViewModel = WinJS.Class.define(function () {
                      this.nameProp;
                      this.descProp;
                  },
{
name: {
get: function () { return this.nameProp; },
set: function (value) { this.nameProp = value; }
},
description: {
get: function () { return this.descProp; },
set: function (value) { this.descProp = value; }
}
});

//Initializing class Instance
let viewModel = new ViewModel();
viewModel.name = "WinJS databinding";
viewModel.description = "Introduction to WinJS databinding";

var rootDiv = document.querySelector('#rootDiv');
let observableViewModel = WinJS.Binding.as(viewModel);
WinJS.Binding.processAll(rootDiv, observableViewModel);

在上面的代码中,我们首先使用WinJS.Binding.initializer定义了绑定初始化器。定义此初始化器时,我们必须传递四个属性,分别是源元素及其属性对象和目标元素及其属性。例如,在我们的情况下,源元素是一个文本框,源属性是它的值,而目标元素将是一个spaninnerText作为其目标属性。WinJS.Binding.defaultBind创建单向绑定,然后我们可以注册源属性的onchange()事件,以更新目标属性。然后我们定义了一个类,并通过初始化一个实例来初始化值。最后,我们将模型转换为可观察模型,以提供双向绑定。

现在,在 HTML 元素中,我们可以像下面这样添加绑定:

<div id="rootDiv">
            <div
 <input type="text" data-win-bind="value: name Binding.twoWayBinding" />
            </div>
            <div>
                Course Name:
                <span id="spanName" data-win-bind="innerText: name">loading</span>
            </div>
            <div>
<input type="text" data-win-bind="value: description Binding.twoWayBinding" />
            </div>
            <div>
                Course Description:
                <span id="spanDesc" data-win-bind="innerText: description">loading</span>
            </div>
            </div>

数据绑定的工作模型

当在 WinJS 中完成数据绑定时,如果使用 WinJS,则必须调用WinJS.processAll()方法。该方法扫描所有指定data-win-bind属性的元素。对于每个元素,它检查与元素绑定的数据是否可观察。这是一个关键步骤,它确定了绑定的类型,并声明绑定是单向绑定、一次性绑定还是双向绑定。

数据绑定的工作模型

承诺

承诺代表一个可能随时包含值的对象的承诺。这是一个承诺,满足消费者资源将可用,消费者可以在不等待资源的异步方式中完成其余工作。

它作为 C#的异步/等待功能。承诺允许消费者在做其他工作而不是等待值返回的同时,提供某些方法来确认承诺一旦收到。在某些情况下,由于某些错误,有可能响应不会返回,这也可以通过实现特定的回调来处理。

在 WinJS 中,承诺是一个具有thendone函数的对象。我们可以这样初始化承诺:

var promise = new WinJS.Promise(function (completed, error, progress)

//Call if we need to update consumer that still in progress
progress("progress");

//Call if any error occurs
error("error");

//Call when the function is completed
completed("completed");
}
);

前面的代码是定义一个返回 promise 的函数的方式。如果我们还没有完成方法并且需要通知消费者如果正在进行中,我们可以调用一个progress方法。一旦定义了 promise,我们可以使用thendone方法来实现由 promise 触发的回调方法。then方法返回一个 promise,表示操作的中间阶段,而done是操作的最后阶段,不返回promise

promise.then(
function () { console.log("completed"); }, 
function () { console.log("error") }, 
function () { console.log("promise") }
);

下面的示例显示了在处理 promise 后显示表格在控制台窗口中并返回完成的函数:

function executeTable(table, max)
{
  return new WinJS.Promise(function(completed, error, progress){
  for (i = 1; i < max; i++) {
     console.log(table +' X '+ i +' = ' + (table * i ));
  }
  completed("executed table")
 });
 };

  executeTable(2, 10).then(
            function (completedVal) {
                    console.log(completedVal);
             }, function (errorVal) {
                    console.log(errorVal);
             },
             function (onProgressVal) {
                    console.log(onProgressVal);
             }

   )

以下是输出:

Promises

现在让我们修改同一个示例,并调用progress在每个迭代中将中间结果发送给消费者。前一个方法是同步的,返回承诺并不意味着该方法将异步执行。为了使这个方法异步运行,我们可以通过setImmediate()函数包装代码块。

setImmediate()是 JavaScript 函数,用于中断函数的执行,并立即返回回调函数,最终在我们的情况下调用承诺的onProgress()函数。以下是带有setImmediate()onProgress()方法的修改版本:

function executeTable(table, max)
            {
                return new WinJS.Promise(function (completed, error, onProgress) {
                    window.setImmediate(function () {
                        for (i = 1; i <= max; i++) {
                            var row = table + ' X ' + i + ' = ' + (table * i);
                            onProgress(row);
                        }
                        completed("executed table")
                    }, 0);
                });
            }; 

  executeTable(2, 10).then(
            function (completedVal) {
                    console.log(completedVal);
             }, function (errorVal) {
                    console.log(errorVal);
             },
             function (onProgressVal) {
                    console.log(onProgressVal);
             }

   )

前一段代码片段的结果将与前一个示例中的结果相同。然而,使用setImmediate()函数允许onProgress()方法异步地将消息写入控制台窗口,并且在性能方面更有效率。

承诺的其他操作

承诺上有几个其他方法可以用来取消任何承诺、链式调用承诺、超时、包装等。让我们看看每个方法是如何使用的。

承诺链式调用和错误处理

可以使用then链式调用多个承诺,并根据它们被链式调用的顺序一个接一个地顺序执行。以下是一个使用WinJS.xhr()方法的简单示例来加载网页。此方法是内置方法,返回一个承诺,我们可以使用此方法来发起 HTTP 请求:

var promise1 = function () { return WinJS.xhr({ url: "http://microsoft.com" }) };
            var promise2 = function () { return WinJS.xhr({ url: "http://google.com" }) };
            var promise3 = function () { return WinJS.xhr({ url: "http://techframeworx.com" }) };
            var promise4 = function () { return WinJS.xhr({ url: "http://msdn.microsoft.com" }) };

            promise1().then(function (dataPromise1) {
                console.log("got the response from promise 1");
                return promise2();
            }).then(function (dataPromise2) {
                console.log("got the response from promise 2");
                return promise3();
            }).then(function (dataPromise3) {
                console.log("got the response from promise 3");
                return promise4();
            }).done(function (dataPromise4) {
                console.log("got the response from promise4");
                console.log("completed the promise chain");
            });

在前一个代码中,我们在每个承诺链执行块中返回下一个承诺。当链式调用承诺时需要这样做,否则它不会调用管道中的下一个承诺。对于管道中的最后一个承诺,我们使用了done而不是then,这实际上告诉我们链中没有下一个承诺,现在无法进行链式调用。另一个好处是执行错误处理。在done方法中,我们可以获取链中任何承诺抛出的所有错误。如果我们不使用done,那么我们将无法访问承诺链中抛出的任何错误。以下示例是带有错误处理的先前示例的修改版本:

            var promise1 = function () { return WinJS.xhr({ url: "http://microsoft.com" }) };
            var promise2 = function () { return WinJS.xhr({ url: "http://google.com" }) };
            var promise3 = function () { return WinJS.xhr({ url: "htt://techframeworx.com" }) };
            var promise4 = function () { return WinJS.xhr({ url: "http://msdn.microsoft.com" }) };

            promise1().then(function (dataPromise1) {
                console.log("got the response from promise 1");
                return promise2();
            }).then(function (dataPromis2) {
                console.log("got the response from promise 2");
                return promise3();
            }).then(function (dataPromise3) {
                console.log("got the response from promise 3");
                return promise4();
            }).done(function (dataPromise4) {
                console.log("got the response from promise 4");
                console.log("completed the promise chain");
            }, function (error) {
                console.log("some error occurred, cause: " + error);
            });

在前一个示例中,我们在链中的最后一个承诺中使用了done。现在,如果你注意到了,承诺 2 的 URL 是不有效的,并且有一个拼写错误。现在如果我们执行前一个代码,promise1promise2将被执行,并将消息写入控制台日志窗口。然而,承诺将不会执行,但error方法将在done方法下定义并被调用,并将错误描述写入控制台日志窗口:

承诺链式调用和错误处理

取消承诺

可以通过调用promise对象的cancel方法来取消承诺。以下是一个取消任何承诺的示例:

var promiseGoogle = function () { return WinJS.xhr({ url: "http://google.com" }) };
            googlePromiseObj = promiseGoogle();
            googlePromiseObj.cancel();

只有在承诺没有完成并且一旦取消就进入错误状态时,才能取消承诺。

结合承诺

多个承诺可以结合起来,当它们都完成后返回。我们可以像下面的代码那样结合承诺。

   var promise1 = function () { return WinJS.xhr({ url: "http://microsoft.com" }) };
            var promise2 = function () { return WinJS.xhr({ url: "http://googe.com" }) };
            var promise3 = function () { return WinJS.xhr({ url: "http://techframeworx.com" }) };
WinJS.Promise.join([promise1, promise2, promise3])
.done(function(){
  console.log("All the promises are finished");
});

当需要知道any方法中定义的任何一个承诺是否执行时,可以使用Promise.any()

var promise1 = function () { return WinJS.xhr({ url: "http://microsoft.com" }) };
var promise2 = function () { return WinJS.xhr({ url: "http://googe.com" }) };
var promise3 = function () { return WinJS.xhr({ url: "http://techframeworx.com" }) };
WinJS.Promise.any([promise1, promise2, promise3])
.done(function(){
  console.log("One of the promises is finished");
});

检查承诺

WinJS.Promise.is()是一个方法,它接受一个值作为参数,并检查该值是否是一个承诺。例如,在WinJS.Promise.is()方法中调用WinJS.xhr将返回true

WinJS.Promise.is(WinJS.xhr({ url: "http://microsoft.com" }));

将非承诺包装成承诺

任何函数都可以通过使用WinJS.Promise.as()方法将其包装成承诺。下面的代码将非承诺displayMessage()方法包装成一个承诺:

function displayMessage() {

                console.log("This is a non promise function")
            }
            var promiseDisplayMessage = WinJS.Promise.as(displayMessage);
            promiseDisplayMessage.done(function () { console.log("promise is executed") });

探索 WinJS 控件和样式

微软的 JavaScript 库提供了一系列丰富的控件、数据绑定选项、承诺,在本节中,我们将探索一些流行的控件和样式选项。

没有一个 WinJS 控件有单独的标记,而是 WinJS 库提供了几个属性,可以用现有的 HTML 元素。

添加 WinJS 控件

正如我们所看到的,没有为 WinJS 控件添加任何标记,它们可以通过在 HTML 元素上添加属性来实现。

在下面的例子中,我们将一个简单的 HTML 按钮元素变成在商店应用中通常看到的后退按钮。这可以通过添加data-win-control属性和将完全限定名设置为WinJS.UI.BackButton来实现。

这里是 HTML 标记:

<button data-win-control="WinJS.UI.BackButton">WinJS button</button>

当你运行它时,它将在页面上渲染一个后退按钮,如下面的图所示:

添加 WinJS 控件

它不仅改变了外观,还提供了箱式导航功能。

设置 WinJS 控件的属性

每个 HTML 元素都有几个属性,可以通过指定值通过属性来寻址。例如,评分控件允许用户对任何项目进行评分,我们可以设置显示星星的maxmin范围属性:

<div id="ratingControl" data-win-control="WinJS.UI.Rating"
         data-win-options="{minRating: 1, averageRating : 5, maxRating: 10}"></div>

前一个标记输出的结果将生成一个评分控件,如下面的图所示:

设置 WinJS 控件的属性

还有其他的 Windows 特定控件,如ListViewFlipViewZoom,你可以在页面中使用它们,在大量集合或对象上带来高性能。你可以在 Windows 开发者中心的网站上了解更多关于控件的信息:msdn.microsoft.com/en-us/library/windows/apps/mt502392.aspx

使用 Windows 运行时特性

WinJS 提供了完整的 API 来使用 Windows 运行时功能和设备特定功能。当使用 WinJS 访问设备特定功能时,Web 应用程序应作为 Windows 应用程序运行,从浏览器访问将导致错误。此外,微软还发布了 Hosted apps 的概念,它使任何 Web 应用程序能够通过几步配置作为 Windows 应用程序托管。

Hosted apps 和访问摄像头

package.json:
{
  "version": "1.0.0",
  "name": "ASP.NET",
  "private": true,
  "dependencies": {
    "winjs": "4.4.0"
  },
  "devDependencies": {
    "gulp": "³.9.1"
  }
   }

我们可以在依赖项部分添加 WinJS,并在保存package.json文件时,包将自动下载。我们还需要添加 gulp,以复制相关库和 CSS 文件到wwwroot文件夹中。之后,添加gulpfile.js并添加以下脚本:

/// <binding Clean='clean' />
"use strict";
var gulp = require("gulp");
var paths = {
    webroot: "./wwwroot/"
};
var config = {
    libBase: 'node_modules',
    lib: [
       require.resolve('winjs/js/base.js'),
       require.resolve('winjs/js/ui.js'),
       require.resolve('winjs/js/winjs.intellisense.js'),
       require.resolve('winjs/js/winjs.intellisense-setup.js')
    ],
    libCss: [require.resolve('winjs/css/ui-dark.css'),
        require.resolve('winjs/css/ui-light.css')
    ]
};
gulp.task('build.lib', function () {
    return gulp.src(config.lib, { base: config.libBase })
    .pipe(gulp.dest(paths.webroot + 'lib'));
});
gulp.task('build.libCss', function () {
    return gulp.src(config.libCss, { base: config.libBase })
    .pipe(gulp.dest(paths.webroot + "lib"));
});

当你通过 Visual Studio 2015 中的任务运行器标签运行build.libbuild.LibCss任务时,它将在wwwroot文件夹内复制 WinJS 库和CSS文件:

创建 ASP.NET 核心应用程序

在这个应用程序中,我们将有一个简单的 HTML 页面,我们可以直接添加到wwwroot文件夹中,为此我们需要在Configure()方法中调用app.UseStaticFiles()方法,并在project.json中添加包:

"Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final"

让我们在wwwroot文件夹中添加Index.html页面,并在 HTML 头元素中添加以下脚本:

    <script src="img/base.js"></script>
    <script src="img/ui.js"></script>
    <script src="img/winjs.intellisense-setup.js"></script>
    <script src="img/winjs.intellisense.js"></script>
    <script src="img/jquery-1.9.0.js"></script>

我们将使用浅色 Windows 主题,因此添加ui-light.css,如下所示:

    <link rel="stylesheet" href="lib/winjs/css/ui-light.css" />

现在添加包含一个Capture按钮以捕获图像的页面内容,以及一个图像元素来显示捕获的图像:

  <div id="rootDiv">
        <div class="col-md-4">
           Click to capture image <input type="button" value="Capture" onclick="return CaptureCamera();" />
        </div>
        <br />
        <img id="imgPhoto" width="500" height="500" style="border:dotted;" />
     </div>

以下是要输出的页面:

创建 ASP.NET 核心应用程序

现在添加以下脚本以访问摄像头并将捕获的图像附加到图像元素:

      <script>
            if (window.Windows) {
                function CaptureCamera() {
                    var notifications = Windows.UI.Notifications;
                    var dialog = new Windows.Media.Capture.CameraCaptureUI();
                    var aspectRatio = { width: 1, height: 1 };
                    dialog.photoSettings.croppedAspectRatio = aspectRatio;
                    dialog.captureFileAsync(Windows.Media.Capture.CameraCaptureUIMode.photo).done(function (capturedImage) {
                        if (capturedImage) {
                            var imageURL = URL.createObjectURL(capturedFile, { oneTimeOnly: true });
                            document.getElementById("img").src = imageURL;
                        }
                        else {
                            WinJS.log && WinJS.log("No image captured yet", "WinJSTestApp", "Status");
                        }
                    }, function (err) {
                        WinJS.log && WinJS.log(err, "WinJSTestApp", "Error");
                    });
                }
            } else {
                function CaptureCamera() {
                    alert("Cannot access camera, it should be hosted as a windows application");
                }
            }
    </script>

使用 Hosted app 概念将 ASP.NET 应用程序转换为 Windows 应用程序

将任何 Web 应用程序转换为 Windows 应用程序非常简单。在 Visual Studio 2015 中,您可以开始使用空白应用(通用 Windows)模板创建一个简单的基于 JavaScript 的 Windows 应用程序,如下所示:

将 ASP.NET 应用程序转换为 Windows 应用程序使用 Hosted app 概念

当你添加一个项目时,它会添加cssimagesjswinjs文件夹。我们必须删除cssjswinjs文件夹,因为我们在这个项目中不会使用任何文件,并配置上面创建的 Web 应用程序,将其转换为 Windows 应用程序。

打开package.appxmanifest窗口。在启动页面文本框中添加 URL,如下所示。我们上面创建的示例 ASP.NET 应用程序托管在端口41345上:

将 ASP.NET 应用程序转换为 Windows 应用程序使用 Hosted app 概念

内容 URI标签中,添加我们网络应用程序的 URI,并在WinRT 访问下选择全部。您可以指定任何应该被托管在某处的网络应用程序的 URL。在前面的截图中,我们使用 localhost,它实际上指的是本地托管的网络应用程序:

使用宿主应用程序概念将 ASP.NET 应用程序转换为 Windows 应用程序

此窗口允许我们指定对 WinRT 功能的访问规则,我们可以将其设置为NoneAll或仅Web 允许

构建并运行应用程序将在我们的网络应用程序index.html页面上显示窗口应用程序对话框:

使用宿主应用程序概念将 ASP.NET 应用程序转换为 Windows 应用程序

点击捕获按钮将弹出另一个对话框以拍摄快照,如下所示:

使用宿主应用程序概念将 ASP.NET 应用程序转换为 Windows 应用程序

在拍摄所需的照片后,它将通过勾选和交叉按钮询问您是否保存或拒绝:

使用宿主应用程序概念将 ASP.NET 应用程序转换为 Windows 应用程序

选择勾选框将在img HTML 元素中渲染照片,如下所示:

使用宿主应用程序概念将 ASP.NET 应用程序转换为 Windows 应用程序

总结

在本章中,我们探索了 WinJS Windows 库,这是一个开源且遵循 Apache 许可的 JavaScript 库。我们学习了定义类、命名空间、继承类、混合模式和承诺的核心概念。我们还研究了数据绑定技术,以及如何使用 HTML 元素中的窗口控件或属性来改变控件的行为和外观。最后,我们使用了 WinRT API 在我们的网络应用程序中访问设备摄像头,并学习了宿主应用程序的概念,以及如何使用 Visual Studio 2015 中的通用窗口模板将任何网络应用程序转换为 Windows 应用程序。在下一章中,我们将学习一些可以在 JavaScript 中实现的设计模式,以满足特定需求。

第七章:JavaScript 设计模式

在每一个中等到大型的项目中,良好的架构和设计总是在处理复杂场景和提高产品的可维护性方面发挥重要作用。设计模式是开发者和专业开发人员为解决特定问题而开发和使用的最佳实践。如果一个设计模式已经用于应用程序的特定场景,它可以在开发过程中或生产环境中运行时避免许多问题。设计模式通过提供行业最佳实践的指导来解决问题或实现需求。例如,单例模式用于创建一个被所有对象共享的唯一实例,而原型模式则通过添加更多属性和方法来扩展对象现有功能。设计模式分为三类,即创建型、结构型和行为型模式。本章我们将覆盖的主题如下:

  • 创建型模式:以下是本章我们将讨论的创建型模式列表:

    • 单例模式

    • 工厂模式

    • 抽象工厂模式

    • 原型模式

  • 结构型模式:以下是本章我们将讨论的结构型模式列表:

    • 适配器模式

    • 装饰器模式

    • 外观模式

    • 桥接模式

  • 行为型模式:以下是本章我们将讨论的行为型模式列表:

    • 责任链模式

    • 观察者模式

    • 发布/订阅模式

    • 承诺(Promises)

创建型模式

创建型模式用于对象实例化。它们用于对象创建的基本形式可能导致设计问题或增加设计复杂性的情况。在下一节中,我们将讨论前面提到的所有四个创建型模式,以及如何在 JavaScript 中实现它们。

单例设计模式

单例是最常用的模式。它用于需要在不同对象之间共享类或函数(就 JavaScript 而言)的同一实例的场景。它确保了特定对象的只有一个实例可以在任何时候全局访问:

单例设计模式

在单例模式中,构造函数应该是私有的,这限制了用户使用new关键字创建对象,并暴露一个创建实例并验证只有一个实例存在的的方法。一个简单的例子可能是一个写日志到浏览器控制台窗口的日志对象:

<script>
    var Logger = (function () {

        //private variable
        var instance;

        //private method
        function initializeInstance() {
            //closure returns the public access to the writeLog function that can be accessible by the singleton object
            return {
                writeLog: function (message) {
                    console.log(message);
                }
            };
        };
        //closure that returns the public access to the getInstance method that returns the singleton object
        return {
            //This is a public method that returns the singleton instance
            getInstance: function () {
                if ( !instance ) {
                    instance = initializeInstance();
                }
                return instance;
            },
        };
    })();

    var logger = Logger.getInstance();
    logger.writeLog("Hello world");
</script>

小贴士

在 JavaScript(ES5 标准)中,类仍然通过函数表示。

在 JavaScript 中,要实现单例,我们可以使用闭包。闭包是内部对象,它们可以访问函数的私有成员,例如访问在父函数内定义的变量和方法,并且可以从闭包中访问它们。

最后语句中的括号()用于将对象返回给日志变量,而不是函数本身。这实际上限制了通过new关键字初始化对象。

在前面的脚本中,函数首先返回一个闭包,其中有一个getInstance()方法,该方法实际上检查私有成员变量 instance,如果它未初始化,则调用initializeInstance()方法,该方法返回另一个包含writeLog()方法的闭包。我们可以添加更多方法或变量,用逗号分隔,它们将通过日志对象访问。以下是initializeInstance()方法的修改版本,其中有一个额外的方法showAlert()和一个变量logEnabled

function initializeInstance() {
            //closure returns the public access to the writeLog function that can be accessible by the singleton object
            return {
                writeLog: function (message) {
                    if(this.logEnabled)
                     console.log(message);
                },  
                showAlert: function (message) {
                    if(this.logEnabled)
                        alert(message);
                },
                logEnabled: false
            };
        };

工厂模式

工厂模式将对象实例化委托给中心化的类。而不是使用new关键字实例化对象,我们调用返回请求对象类型的factory方法:

Factory pattern

以下是基于日志类型创建日志实例的LoggerFactory示例:

//LoggerFactory to instantiate objects based on logger type
    function LoggerFactory() {
        var logger;
        this.createLogger = function (loggerType) {
            if (loggerType === "console") {
                logger = new ConsoleLogger();
            }
            else if (loggerType === "alert") {
                logger = new AlertLogger();
            }
            return logger;
        }
    }

    //Console logger function
    var ConsoleLogger= function(){
        this.logMessage=function(message){
            console.log(message);
        } 
    };

    //Alert logger function
    var AlertLogger= function(){
        this.logMessage= function(message){
            alert(message);
        } 
    };

    var factory = new LoggerFactory();

    //creating Console logger object using LoggerFactory
    var consoleLogger = factory.createLogger("console");
    consoleLogger.logMessage("Factory pattern");

    //create Alert logger object using LoggerFactory
    var alertLogger = factory.createLogger("alert");
    alertLogger.logMessage("Factory pattern");

在我们的示例中,工厂类是LoggerFactory,它创建ConsoleLoggerAlertLogger对象的实例。LoggerFactory暴露一个createLogger()方法,该方法接受日志类型作为参数,以确定需要实例化哪个对象。每种日志类型都有自己的logMessage()方法,用于在控制台窗口上记录日志或显示警告消息。

抽象工厂模式

抽象工厂模式封装了一系列工厂以创建实例。实例暴露相同的方法,可以被工厂调用。以下是两个工厂的示例,ShapeFactoryCarFactory,每个工厂都返回两种类型的实例。ShapeFactory返回CircleSquare实例,而CarFactory返回HondaCarNissanCar实例。每个实例都有相同的方法make(),可以对任何实例进行调用:

Abstract factory pattern

以下是ShapeFactory的代码:

<script>
    //Shape Factory to create instances of Circle and Square
    var ShapeFactory = function() {
        var shape;
        this.createShape = function (shapeType) {
            if (shapeType === "circle") {
                return new CircleShape();
            }
            else if (shapeType === "square") {
                return new SquareShape();
            }
        }
    }

    //Circle object to draw circle
    var CircleShape = function () {
        this.make = function () {
            var c = document.getElementById("myCanvas");
            var ctx = c.getContext("2d");
            ctx.beginPath();
            ctx.arc(100, 75, 50, 0, 2 * Math.PI);
            ctx.stroke();
        }
    }

    //Square object to draw square
    var SquareShape = function () {
        this.make = function () {
            var c = document.getElementById("myCanvas");
            var ctx = c.getContext("2d");
            ctx.beginPath();
            ctx.rect(50, 50, 50, 50);
            ctx.stroke();
        }
    }

以下是创建 Honda 和 Nissan 汽车实例的CarFactory代码:

    //Car factory to create cars
    var CarFactory= function() {
        var car
        this.createCar = function (carType) {
            if (carType === "honda") {
                return new HondaCar();
            }
            else if (carType === "nissan") {
                return new NissanCar();
            }
        }
    }

    //Honda object
    var HondaCar = function () {
        this.make = function () {
            console.log("This is Honda Accord");
        }
    }

    //Nissan object
    var NissanCar = function () {
        this.make = function () {
            console.log("This is Nissan Patrol")
        }
    }

我们在按钮点击事件上调用execute方法,该方法创建实例并将它们保存在数组中。最终,对象的make()方法将被执行,在 HTML 画布上绘制圆并在控制台上写入消息。与 JavaScript 一样,我们不能定义抽象方法;我们必须明确定义相同的方法,例如在我们的案例中完成的抽象工厂模式中的make()

    function execute() {
        //initializing an array to hold objects
        var objects = [];

        //Creating Shape Factory to create circle shape
        var shapeFactory = new ShapeFactory();
        var circleShape = shapeFactory.createShape("circle");

        //Creating Car Factory to create cars
        var carFactory = new CarFactory();
        var hondaCar= carFactory.createCar("honda");
        var nissanCar = carFactory.createCar("nissan");

        //Adding all the instances created through factories
        objects.push(circleShape);
        objects.push(hondaCar);
        objects.push(nissanCar);

        //Calling make method of all the instances. 
        for (var i = 0; i < objects.length; i++) {
            alert(objects[i]);
            objects[i].make();
        }
    }

</script>

以下是包含画布和按钮的 HTML 代码:

    <div>
        <input type="button" onclick="execute()" value="Execute" />
    </div>
    <div>
        <canvas id="myCanvas"></canvas>
    </div>

在按钮点击事件上,输出将如下所示。将绘制一个圆并在控制台窗口上打印 Honda 和 Nissan 汽车对象的两次消息:

Abstract factory pattern

原型模式

原型模式用于创建现有实例的克隆实例。它用于需要自动为对象配置某些特定值或属性的场景,用户无需显式定义:

原型模式

以下是实现原型模式的代码:

<script>

    function Car(make, model, year, type)
    {
        this.make = make;
        this.model = model;
        this.year = year;
        this.type = type;

        this.displayCarDetails = function(){

        }

    }

    function CarPrototype(carPrototype) {
        var car = new Car();

        this.getPrototype = function () {
            car.make = carPrototype.make;
            car.model = carPrototype.model;
            car.year = carPrototype.year;
            car.type = carPrototype.type;
            return car;
        }

    }

    (function () {
        var car = new Car("Honda", "Accord", "2016","sedan");
        var carPrototype = new CarPrototype(car);
        var clonedCar = carPrototype.getPrototype();

    })();
</script>
 is a car object that accepts four parameters, make, model, year and type. CarPrototype() is a function that accepts the car object and returns the cloned version of the car object. This pattern is performance efficient and saves developers time creating a clone copy of the object by just calling the prototype object's clone method. The user does not need to care about populating the properties after object instantiation; it initializes when the object is created and gets the same values as the original object. It is used in conditions where we need to clone instances of objects when they are in a specific state and can be easily cloned by calling the getProtoype() method.

结构模式

结构模式用于简化对象之间的关系。在以下部分,我们将讨论前面提到的所有四种结构模式,以及如何在 JavaScript 中实现它们。

适配器模式

适配器模式用于应用程序依赖于属性和方法频繁变化的对象的情况,我们想避免修改使用它们的代码。适配器模式允许我们将特定对象的接口包装为我们期望的接口,而无需改变整个实现,我们只需调用包含修改后版本代码的包装器对象。这个包装器对象称为适配器。让我们来看一个使用PersonRepository通过执行 Ajax 请求保存person对象的简单示例:

适配器模式

以下是PersonRepository对象的旧接口:

// old interface
    function PersonRepository(){
        this.SavePerson= function(name, email, phoneNo){
            //Call ajax to save person
            console.log("Name: " + name + ", Email: " + email + ", Phone No: " + phoneNo);
        }
    }

前一个接口有一个SavePerson()方法,该方法接受三个参数:nameemailphoneNo。以下是使用SavePerson()方法的原始代码:

var execute = function () {

        var personRepository = new PersonRepository();
        personRepository.SavePerson("John", "john@email.com", "1201111111");

    }

假设这个接口发生了变化,新的人员仓库接口接受person JSON 对象,而不是通过参数传递值。一种方法是直接在这里修改函数,将这些参数封装在 JSON 对象中并发送。另一种方法是实现一个适配器模式,该模式包含一个适配器函数,该函数接受三个参数,调用新的PersonRepository对象并传递 JSON 对象。

以下是PersonRepository的新接口:

function PersonRepository() {
        this.SavePerson = function (person) {
            //call ajax to send JSON person data
          console.log("Name: " + person.name + ", Email: " + person.email + ", Phone No: " + person.phoneNo);
        }
    }

以下是一个将参数封装在 JSON 对象中并调用新PersonRepository接口的适配器模式:

    function PersonRepositoryAdapter() {
        this.SavePerson = function (name, email, phoneNo) {
            var person = { "name": name, "email": email, "phoneNo": phoneNo };

            var personRepository = new PersonRepository();
            //calling new Person Repository
personRepository.SavePerson(person);
        }
    }

以下是调用适配器模式而不是调用旧的PersonRespository接口的修改版本:

    var execute = function () {

    //old interface
// var personRepository = new PersonRepository();
  // personRepository.SavePerson("John", "john@email.com", "1201111111");

      //calling adapter pattern
        var personAdapter = new PersonRepositoryAdapter();
        personAdapter.SavePerson("John", "john@email.com", "1201111111");

    }

装饰器模式

装饰器模式用于在运行时改变对象的行为。装饰器在 C#中类似于注解属性。同样,我们也可以在一个对象上添加多个装饰器。装饰器模式可以通过创建一个装饰器对象并将该对象与需要改变行为的目标对象关联来实现:

装饰器模式

以下是一个添加了TaxCourier费用装饰器的Product对象的示例:

<script>

    var Product= function (code, quantity, price) {
        this.code = code;
        this.quantity = quantity;
        this.price = price

        this.total = function () {
            this.price = price * quantity;
            return this.price;
        }

    }

    //Decorator that takes product and percent as parameter to apply Tax
    function AddTax(product, percent) {
        product.total = function () {
             product.price = product.price + (product.price * percent / 100);
            return  product.price;
        }
    }

    //Decorator to add Courier charges in the total amount.
    function AddCourierCharges(product, amount) {
        product.total = function () {
            product.price = product.price + amount;
            return product.price;
        }
    }

    var execute = (function () {
        var prod = new Product("001", 2, 20);
        console.log("Total price: " + prod.total());
        AddTax(prod, 15);
        console.log("Total price after Tax: " + prod.total());
        AddCourierCharges(prod, 200);
        console.log("Total price after Courier Charges: " + prod.total());
    })();

product对象接受三个参数,codequantityprice,根据quantityprice计算总价。AddTax()AddCourierCharges()是两个装饰器对象, followed product object with a parameter to apply specific calculation on change the total price. The AddTax() method applies the tax based on the value supplied, whereas AddCourierCharges() will add the courier charge amount to the total price. The Execute() method will be called immediately when the page renders and displays the following output in the console window:

装饰器模式

外观模式

外观模式用于将各种接口或子系统简化为一个统一的接口。它简化了事物对用户来说,而不是理解不同子系统的复杂性,用户可以调用façade接口来执行特定操作。

让我们看看以下示例,它有三个方法来加载登录成功后的权限、用户资料和用户聊天窗口。它有三个接口,通过外观我们可以简化为一个统一的接口。这允许用户如果登录成功,只需调用一次UserFacade,它将从一个接口加载权限、用户聊天和用户资料:

责任链模式

<script>
    var Permission = function () {
        this.loadPermission = function (userId) {
            //load user permissions by calling service and populate HTML element
            var repo = new ServiceRepository();
            repo.loadUserPermissions(userId);
        }
    }

    var Profile = function () {
        this.loadUserProfile = function (userId) {
            //load user profile and set user name and image in HTML page
            var repo = new ServiceRepository();
            repo.loadUserProfile(userId);
        }
    }

    var Chat = function () {
        this.loginUserChat = function (userId) {
            //Login user chat and update HTML element
            var repo = new ServiceRepository();
            repo.loadUserChat(userId);
        }
    }

    var UserFacade = function () {
        this.loadUser = function (userId) {
            var userPermission = new Permission();
            var userProfile = new Profile();
            var userChat = new Chat();

            userPermission.loadPermission(userId);
            userProfile.loadUserProfile(userId);
            userChat.loginUserChat(userId);

        }
    }

    var loginUser = (function (username, password) {

            //Service to login user
            var repo = new ServiceRepository();
            //On successfull login, user id is returned
            var userId = repo.login(username, password);

            var userFacade = new UserFacade();
            userFacade.loadUser(userId);

    })();
<

桥接模式

桥接模式用于将抽象与其实现解耦,并使具体实现与接口独立。这是通过在接口和具体实现者之间提供一个桥梁来实现的:

桥接模式

下面的代码展示了桥接模式的实现:

<script>

    var Invitation = function (email) {
        this.email = email;
        this.sendInvite = function () {
            this.email.sendMessage();
        }
    }

    var Reminder = function (sms) {
        this.sms = sms;
        this.sendReminder = function () {
            this.sms.sendMessage();
        }
    }

    var SMS = function () {
        //send SMS
        this.sendMessage = function () { console.log("SMS sent"); }

    }

    var Email = function () {
        //send email
        this.sendMessage = function () { console.log("Email sent");}
    }

    var execute = (function () {
        var email = new Email();
        var sms = new SMS();

        var invitation = new Invitation(email);
        var reminder = new Reminder(sms);

        invitation.sendInvite();
        reminder.sendReminder();
    })();
</script>

前一个示例的目标是将通知类型(InvitationReminder)与通知网关(EmailSMS)分离。因此,任何通知都可以通过任何网关发送,并且可以处理任何通知,将网关与通知类型分离,并禁用与通知类型的绑定。

行为型模式

行为型模式用于在对象之间委派责任。在以下部分,我们将讨论前面提到的所有四种行为型模式,以及如何在 JavaScript 中实现它们。

责任链模式

责任链模式提供了一串按顺序执行的对象链,它们被串联起来以满足任何请求。ASP.NET 开发者一个很好的例子是 OWIN 管道,它将组件或 OWIN 中间件串联在一起,基于适当的请求处理器被执行:

责任链模式

让我们来看一个非常基本的示例,它执行对象链并显示 2、3 和 4 的表格:

<script>

    //Main component
    var Handler = function (table) {
        this.table = table;
        this.nextHandler = null;
    }

    //Prototype to chain objects
    Handler.prototype = {
        generate: function (count) {
            for (i = 1; i <= count; i++) {
                console.log(this.table + " X " + i + " = " + (this.table * i));
            }
            //If the next handler is available execute it
            if (this.nextHandler != null)
                this.nextHandler.generate(count);
        },
        //Used to set next handler in the pipeline
        setNextHandler: function (handler) {
            this.nextHandler = handler;
        }
    }

    //function executed on Page load
    var execute = (function () {

        //initializing objects
        var handler1 = new Handler(2),
         handler2 = new Handler(3),
         handler3 = new Handler(4);

        //chaining objects
        handler1.setNextHandler(handler2);
        handler2.setNextHandler(handler3);

        //calling first handler or the component in the pipeline
        handler1.generate(10);

    })();
<script>

让我们进入一个更实际的示例,该示例获取金额并在管道中的对象中检查预算所有者。以下代码有一个主处理对象,它接受预算金额和预算所有者,以设置Line ManagerHead of DepartmentCTOCEO的预算金额。最后,我们可以通过调用handler1方法设置链中的主入口点,该方法首先检查金额是否在 line manager 的预算范围内,然后是部门经理的、CTO 的,最后是 CEO 的:

<script>

    //Main component
    var Handler = function (budget, budgetOwner) {
        this.budget = budget;
        this.budgetOwner = budgetOwner;
        this.nextHandler = null;
    }

    //Prototype to chain objects
    Handler.prototype = {
        checkBudget: function (amount) {
            var budgetFound = false;
            if (amount <= this.budget) {
                console.log("Amount is under " + this.budgetOwner + " level");
                budgetFound = true;
            }

            //If the next handler is available and budget is not found
            if (this.nextHandler != null && !budgetFound)
                this.nextHandler.checkBudget(amount);
        },

        //Used to set next handler in the popeline
        setNextHandler: function (handler) {
            this.nextHandler = handler;
        }
    }

    //funciton executed on Page load
    var execute = (function () {

        //initializing objects
        var handler1 = new Handler(10000, "Line Manager"),
         handler2 = new Handler(50000, "Head of Department"),
         handler3 = new Handler(100000, "CTO"),
         handler4 = new Handler(1000000, "CEO");

        //chaining objects
        handler1.setNextHandler(handler2);
        handler2.setNextHandler(handler3);
        handler3.setNextHandler(handler4);

        //calling first handler or the component in the pipeline
        handler1.checkBudget(20000);

    })();
</script>

以下是输出结果:

责任链模式

观察者模式

implements the observer pattern:
<script>

    //Sample function to convert text to French language
    function translateTextToFrench(value) {
        // call some service to convert text to French language
        return value;
    }

    //Sample function to convert text to Arabic language
    function translateTextToArabic(value) {
        //cal some service to convert text to Arabic language
        return value;

    }

    //Helper function used by the Observer implementors 
    var HelperFunction = function (type) {

        var txtEntered = document.getElementById("txtEntered");

        var englishText = document.getElementById("englishText");
        var frenchText = document.getElementById("frenchText");
        var arabicText = document.getElementById("arabicText");

        if (type == "english") {
            englishText.innerText = txtEntered.value;
        } else if (type == "french") {
            frenchText.innerText = translateTextToFrench(txtEntered.value);
        } else if (type == "arabic") {
            arabicText.innerText = translateTextToArabic(txtEntered.value);
        }
    }

    var EnglishTranslator = {
        update: function () {
            //Call helper function to change text to English
            HelperFunction("english");
        }
    }

    var FrenchTranslator = {
        update: function () {
            //Call helper function to change text to French
            HelperFunction("french");
        }
    }

    var ArabicTranslator = {
        update: function () {
            //Call helper function to change text to Arabic
            HelperFunction("arabic");
        }
    }

    //Observer function that contains the list of observer handlers
    function Observer() {
        this.observers = [];
    }

    //to add observer
    Observer.prototype.addObserver = function (object) {
        console.log('added observer: ' + object);
        this.observers.push(object);
    };

    //to remove observer
    Observer.prototype.removeObserver = function (object) {
        console.log("removing observer");
        for (i = 0; i < this.observers.length; i++) {
            if (this.observers[i] == object) {
                this.observers.splice(object);
                return true;
            }
        }
        return false;
    };

    //To notify all observers and call their update method
    Observer.prototype.notify = function () {
        for (i = 0; i < this.observers.length; i++) {
            this.observers[i].update();
        }
    }

    //Adding objects as observers that implements the update method
    var observer = new Observer();
    observer.addObserver(EnglishTranslator);
    observer.addObserver(FrenchTranslator);
    observer.addObserver(ArabicTranslator);

    //Execute will be called on button click to notify observers
    var execute = function () {
        observer.notify();
    };

</script>
<body>
    <div>
        Specify some text: <input type="text" id="txtEntered" />
        <input type="button" onclick="execute()" value="Notify" />
    </div>
    <div>
        <span id="englishText"></span>
        <span id="frenchText"></span>
        <span id="arabicText"></span>
    </div>
</body>

在上述示例中,我们采取了一个场景,为所有添加为观察者对象的语言翻译文本。我们扩展了Observer对象,并通过原型定义了三个方法,分别是addObserver()removeObserver()notify()。通过原型添加方法可以节省内存,每个方法都被所有实例共享。这些方法一次性创建,然后被每个实例继承。另一方面,在构造函数函数内定义的方法每次创建新实例时都会被创建,消耗更多内存。

addObserver()方法用于将任何对象添加到观察者列表中,removeObserver()用于从观察者列表中移除特定对象,notify()执行观察者的update()方法。

EnglishTranslatorFrenchTranslatorArabicTranslation是实现了update()方法的对象,当notify()执行时会调用该方法。在页面加载时,我们已经将所有翻译器对象注册为观察者,并提供了一个文本框和一个按钮,用户可以在文本框中输入任何文本,在按钮点击事件时,它会调用观察者的notify()方法,最终调用注册观察者的update()方法。

发布/订阅模式

 implement the pub/sub pattern in JavaScript:
var PubSub = function () {
        this.events = [];
        this.subscribe = function (eventName, func) {
            this.events[eventName] = this.events[eventName] || [];
            this.events[eventName].push(func);
        };

        this.unsubscribe = function (eventName, func) {
            if (this.events[eventName]) {
                for (i = 0; i < this.events[eventName].length; i++) {
                    if (this.events[eventName][i] === func) {
                        this.events[eventName].splice(i, 1);
                        break;
                    }
                }
            }
        };

        this.publish = function (eventName, data) {
            console.log(data);
            if (this.events[eventName]) {
                this.events[eventName].forEach(function (event) {
                    event(data);
                })
            }
        };
    };

    var execute = (function () {
        var pubSub = new PubSub();
        pubSub.subscribe("myevent1", function () {
            console.log("event1 is occurred");
        });

        pubSub.subscribe("myevent1", function () {
            console.log("event1 is occurred");
        });

        pubSub.subscribe("myevent2", function (value) {
            console.log("event2 is occurred, value is "+ value);
        });

        pubSub.publish("myevent1", null);

        pubSub.publish("myevent2", "my event two");
    })();

在之前的示例中,我们有一个PubSub对象,它提供了三个方法:subscribeunsubscribepublish,用于处理事件。Subscribe()方法用于订阅任何事件,并接受两个参数:事件名称和函数,并将它们添加到特定事件名称的数组中。如果事件名称不存在,则会为该事件名称初始化一个新的数组;否则,将检索现有实例以添加项目。用户可以通过传递事件名称和匿名函数体来注册尽可能多的事件,当事件发布时将执行该函数体。要发布事件,可以调用publish()方法,该方法接受事件名称和要传递给相应函数的数据,该函数将被执行。

承诺

承诺是 JavaScript API 和框架中广泛使用的一种最流行的模式,用于使异步调用更简单。在 JavaScript 中,异步操作需要有一个回调函数注册,当值返回时调用。使用承诺时,当你进行任何异步调用,它立即返回一个承诺,并提供诸如thendone的对象来定义当结果值解析时执行的函数。在现实世界中,承诺就像在快餐店订购食物时得到的 tokens 或收据,那个收据保证你在食物准备好时能够获得食物。承诺是确认你针对特定请求获得响应的 tokens:

承诺

在 JavaScript 中,承诺(promises)被广泛用于 API 和框架,如 AngularJS、Angular 2 等。让我们来看一下以下实现承诺模式的示例:

//Defining Promise that takes a function as a parameter.
    var Promise = function (func) {
        //Declared member variable 
        var callbackFn = null;

        //Expose done function that can be invoked by the object returning promise
        //done() function takes a callback function which can be define when using done method.
        this.done = function (callback) {
           callbackFn = callback;
        };

        function resolve(value) {
            setTimeout(function () {
                callbackFn(value)
            },3000)
        }

        //Here we are actually executing the function defined when initializing the promise below.
        func(resolve);
    }

    //Object that is used to order food and returns a promise
    var orderFood = function (food) {
        //returns the Promise instance and pass anonymous function that call resolve method which actually serve the request after delaying 3 seconds.
        return new Promise(function (resolve) {
            resolve(food);
        });
    }

    //Initialized orderFood that returns promise
    var order = new orderFood("Grilled Burger");
    //Calling done method which will be invoked once the order is ready
    order.done(function (value) {
        console.log(value);
    });
promise pattern. However, there are other parts of the promise pattern that we can implement to make it robust. Promises have states and the following is the modified version that not only maintains the states for inprogress, done, and failed but also provides the failed handler to catch exceptions. The following is the description of the states:
  • 进行中:当调用resolve方法时,状态将被设置为in progress。这个状态将持续,直到我们为donefailed场景注册处理程序。

  • 已完成:当调用done时,状态将被设置为done

  • 失败:当发生任何异常时,状态将被设置为fail

以下是对orderFood示例的修改版本:

    //Defining Promise that takes a function as a parameter.
    var Promise = function (func) {

        //Default status when the promise is created
        var status = 'inprogress';
        var error = null;

        //Declared member variable 
        var doneCallbackFn = null;
        var failedCallbackFn = null;

        //Expose done function that can be invoked by the object returning promise
        this.done  = function (callback) {
            //Assign the argument value to local variable
            doneCallbackFn = callback;
            if (status === "done") {
                doneCallbackFn(data);
            } else {
                doneCallbackFn(status);
            }
            //return promise to register done or failed methods in chain
            return this;
        };

        //Expose failed function to catch errors
        this.failed = function (callback) {
            if (status === "failed") {
                failedCallbackFn(error);
            }
            //return promise instance to register done or failed methods in chain
            return this;
        };

        function prepareFood() {
            setTimeout(function () {
                status = "done";
                console.log("food is prepared");
                if (doneCallbackFn) {
                    doneCallbackFn(data);
                }
            }, 3000);

        }

        function resolve(value) {
            try {
                //set the value
                data = value;

                //check if doneCallbackFn is defined
                if (doneCallbackFn) {
                    doneCallbackFn(value);
                }
                prepareFood();

            } catch (error) {
                //set the status to failed
                status = "failed";
                //set the exception in error
                error = error;
                //check if failedCallbackFn is defined
                if (failedCallbackFn) {
                    failedCallbackFn(value);
                }
            }
        }
        //Here we are actually executing the function defined when initializing the promise below.
        func(resolve);
    }

    //Object that is used to order food and returns a promise
    var orderFood = function (food) {
        //returns the Promise instance and pass anonymous function that call resolve method which 
        //actually serve the request after delaying 3 seconds.
        return new Promise(function (resolve) {
            resolve(food);
        });
    }

    //Initialized orderFood that returns promise
    var order = new orderFood("Grilled Burger").done(function (value) { console.log(value); }).failed(function (error) { console.log(error);})

总结

在本章中,我们已经学习了设计模式在小到大型应用程序中的重要性,以及我们如何有效地使用它们来解决特定问题。我们已经涵盖了每个类别的四种设计模式,例如创建对象、结构化对象以及向对象添加行为变化或状态。还有许多其他的设计模式可供参考,具体可以参考这里:www.dofactory.com/javascript/design-patterns

在下一章中,我们将学习 Node.js,它可以在服务器端运行 JavaScript。我们将了解如何使用 Visual Studio 2015 在 Node.js 中开发网络应用程序,并探索一些流行的框架以及它提供的视图引擎。

第八章:Node.js 对 ASP.NET 开发者的意义

JavaScript 已经成为一种不仅运行在客户端,也运行在服务器端的流行语言之一。Node.js 使 JavaScript 能够运行在服务器端,并提供非阻塞 I/O、一个事件驱动的模型,这使得它更加轻量级、可伸缩和高效。如今,它在执行实时操作、开发业务应用程序、数据库操作等方面得到了更广泛的应用。Node.js 上的 JavaScript 可以与运行在 IIS 上的 ASP.NET 或其他网络服务器相关联。

Node.js 简介

Node.js 是一个使用 JavaScript 构建服务器端应用程序的强大平台。Node.js 本身不是用 JavaScript 编写的,但它提供了一个运行 JavaScript 代码的运行时环境。它允许在服务器端运行 JavaScript 代码,提供基于 Google V8 JavaScript 引擎的运行时,这是一个用 C++编写的开源 JavaScript 引擎,由 Google Chrome 使用,用于在 V8 即时编译器执行时将 JavaScript 代码编译成机器代码。

Node.js 工作在单线程上;与其他创建每个请求单独线程的服务器端技术不同,Node.js 使用事件回调系统,通过单线程处理请求。如果多个请求同时到达,它们必须等待线程可用,然后才能获取它。在错误情况下,Node.js 不会抛出错误,这是避免错误冒泡和单线程中断的一个基本技术。如果在处理请求时出现任何错误,Node.js 会在响应本身中发送错误日志,通过回调参数。这使得主线程能够传播错误并延迟响应。Node.js 适合编写网络应用程序。它包括 HTTP 请求、其他网络通信任务,以及使用 Web Sockets 进行实时客户端/服务器通信。

Node.js 网络服务器请求处理

Node.js 网络服务器维护一个有限的线程池来处理客户端请求。当请求到达服务器时,Node.js 网络服务器把这个请求放入一个事件队列中。然后事件循环组件——它在一个无限循环中工作——在空闲时处理这个请求。这个事件循环组件是单线程的,如果请求涉及到如文件系统访问、数据库访问等的 I/O 阻塞操作,它会检查内部线程池中的线程可用性,并将请求分配给可用线程。否则,它会一次性处理请求并将响应发送回客户端。当内部线程完成了 I/O 阻塞请求,它会首先将响应发送回事件循环,然后事件循环再将响应发送回客户端。

Node.js 与.NET 的比较

ASP.NETNode.js 都是服务器端技术。下面的图表展示了Node.js 与.NET 的比较:

Node.js 与 .NET 比较

NPM

Node 包管理器NPM)是用于安装 Node 模块的 Node.js 包管理器。Node.js 提供了一种编写 JavaScript 模块的方法,借助 NPM,我们可以在其他应用程序中添加和使用这些模块。在使用 ASP.NET Core 时,我们已经在使用一些模块,例如使用 Gulp 和 Grunt 压缩 CSS 和 JavaScript 文件,以及执行复制和合并操作。package.json 文件是包含有关应用程序和项目中使用的 Node 模块的元数据信息的配置文件。以下是 package.json 文件的示例截图:

NPM

可以通过执行以下命令来安装依赖项:

npm install NAME_OF_THE_PACKAGE –save

示例:

npm install gulp –save

--save 用于更新 package.json 的依赖项部分并添加下载的包。

安装 Node.js

Visual Studio 为使用 Node.js 开发程序提供了强大的支持。要在 Windows 平台上配置 Node.js 开发环境,请从 nodejs.org 下载并安装 Node.js。根据平台不同,可用的安装程序各不相同,如下面的截图所示:

安装 Node.js

对于 Windows,我们将下载 64 位的 Windows 安装程序,该程序下载.msi包并通过一些简单的向导屏幕引导您。您会注意到 Node.js 安装程序包含一个运行 Node 程序的运行时和 NPM,以便在您的程序中引用其他 Node 模块。以下截图展示了这一点:

安装 Node.js

npmnode 这样的命令已经添加到了环境路径中,我们可以直接从命令提示符执行这些命令。因此,如果我们打开命令提示符并输入 node,它将给出 Node 提示符,允许你即兴编写 JavaScript 代码并执行,如下面的截图所示:

安装 Node.js

另外,我们还可以通过调用node javascriptfile.js来运行.js文件。

以下是一个名为example1.js的示例文件,该文件用于计算数组中定义的数字之和:

console.log("NodeJs example");

var numbers= [100,20,29,96,55];

var sum=0;
for(i=0; i< numbers.length; i++)
{
 sum += numbers[i];
}
console.log("total sum is "+ sum);

以下是输出结果:

安装 Node.js

使用 Node.js 与 Visual Studio 2015

市场上有很多支持 Node.js 工具的集成开发环境(IDE)。像 Visual Studio Code、Sublime、Komodo 和 Node Eclipse 这样的 IDE 都是流行的 Node.js 工作环境,但实际上,大多数 .NET 开发人员更习惯并熟悉使用 Visual Studio IDE。因此,在本章中,我们将使用 Visual Studio 2015 社区版。

可以在 Visual Studio 2015 中通过安装其扩展来安装 Node.js 模板。可以从 Visual Studio 菜单选项 工具 | 扩展和更新 中安装扩展:

使用 Node.js 与 Visual Studio 2015

这个 Node.js 扩展安装了各种模板,用于开始使用 Node.js 开发应用程序。有一个模板是使用空白 Node.js 控制台应用程序模板开发控制台应用程序,有一个使用 Node.js express 模板开发 web 应用程序等等:

使用 Node.js 和 Visual Studio 2015

使用这些模板的基本优势是节省手动配置事物的时间,这些模板通过提供基本的项目结构来帮助开发者立即启动 Node.js 应用程序。

让我们先创建一个基本的控制台应用程序模板。基本的控制台应用程序有一个npm文件夹,包含 node 包,package.json包含元数据信息和其他配置属性,还有app.js,其中包含实际的 JavaScript 代码:

使用 Node.js 和 Visual Studio 2015

这个 Node.js 扩展提供了一个方便的功能,通过在npm文件夹上右键点击并选择安装新的 npm 包选项,即可添加 Node 模块,如下面的屏幕截图所示:

使用 Node.js 和 Visual Studio 2015

选择这个选项后,Visual Studio 会打开一个窗口,帮助搜索任何 node 包,并几点击添加到你的应用程序中:

使用 Node.js 和 Visual Studio 2015

前面的图表展示了可以通过这个选项添加的Gulp包的版本。

交互式窗口是 Visual Studio 中的另一个好功能,它打开了一个集成在 Visual Studio 标签中的命令提示符,你可以立即编写 JavaScript 代码并执行命令,如下面的屏幕截图所示:

使用 Node.js 和 Visual Studio 2015

使用 Visual Studio 还有其他几个好处:你可以使用 Git 或 TFS 版本库,在 JavaScript 文件上调试你的代码并设置断点等等。针对 Node.js 的 Visual Studio 特定项目文件称为.njsproj,位于你项目的主文件夹中。

使用 Node.js 的简单控制台应用程序

一个 Node.js 应用程序由一个或多个提供特定功能的 JavaScript 文件组成。在一个 JavaScript 文件中写入成千上万行代码在实际中是不可能的,而且也会增加可维护性问题。在 Node.js 中,我们可以创建多个 JavaScript 文件,并通过requireexport对象使用它们,这些对象是 Common JS 模块系统的组成部分:

export: used to export variables, functions and objects 

//exportexample.js
module.exports.greeting = "Hello World";

require: To use the objects resides in different JavaScript files using require object. 

//consumerexample.js – referencing through file
var obj = require('./exportexample.js');

另外,我们也可以调用require而不指定.js文件扩展名,它会自动加载特定路径上存在的文件。如果该路径对应于一个文件夹,所有 JavaScript 文件都将被加载:

//consumerexample.js – referencing through file
var obj= require('./exportexample');

当应用程序启动时,定义在package.json中的是主要入口点。在下面的屏幕截图中,app.js是主入口文件,首先被 Node.js 加载并执行:

使用 Node.js 的简单控制台应用程序

让我们实现一个基本示例,有两个文件,分别是app.js(主入口)和cars.js,并返回car对象的几个属性,例如namemodelengine。首先,创建一个控制台应用程序项目并添加一个cars.js文件。

以下是cars.js的代码:

module.exports.cars = [
{name:"Honda Accord" , model:"2016", engine: "V6"}, 
{name:"BMW X6", model:"2015", engine: "V8"}, 
{name:"Mercedez Benz",model:"2016", engine:"V12"}
];

通过module.exports,我们可以导出任何对象。无论是变量、函数还是 JSON 对象,都可以通过这个方法导出。此外,导出的对象可以通过app.js中的require对象使用,如下面的代码所示:

var cars = require('./cars.js');
console.log(cars);

以下是输出:

使用 Node.js 的简单控制台应用程序

前面的代码显示了cars.js文件中定义的 JSON 输出。为了初始化cars对象,并遍历列表中定义的汽车项目,我们需要将其作为函数导出,并通过this关键字定义它。通过this指定它将使列表从我们在app.js文件中创建的cars对象中访问。

以下是cars.js的修改版本:

module.exports = function () {
  this.carsList =   
  [
    { name: "Honda Accord" , model: "2016", engine: "V6" }, 
    { name: "BMW X6", model: "2015", engine: "V8" }, 
    { name: "Mercedez Benz", model: "2016", engine: "V12" }
  ];
};

下面是初始化cars对象并遍历列表的app.js文件的修改版本:

var cars = require('./cars.js');
var c = new cars();
var carsList = c.carsList;
for (i = 0; i < carsList.length; i++) { 
  console.log(carsList[i].name);
}

使用 Node.js 的 Web 应用程序

有各种 Node.js Web 框架可供选择。像 Express 和 Hapi.js 这样的框架是强大的框架,具有不同的架构和设计。在本节中,我们将使用 Express 框架,这是 Node.js 中最广泛使用的 Web 框架之一,用于 Web 和移动应用程序,并提供应用程序框架模型以开发 Web Application Programming InterfacesAPIs)。

创建空白 Node.js 应用程序

listen() method that actually listens for the incoming requests, and sends the response using the res.end() method. Alternatively, we can also specify the content we are returning using the res.write() method. Here is the more simplified version of the same code, to understand how the pieces fit together:
//Initialized http object
var http = require('http');

//declared port
var port = process.env.port || 1337;

//Initialized http server object and use res.write() to send actual response content
var httpServer= http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.write('Hello World\n');
    res.end();
});

//listening for incoming request
httpServer.listen(port);

在 Node.js 中使用 Express 框架的 Web 应用程序

在任何编程语言中,框架的一个重要好处是减少开发 Web 应用程序所需的努力。框架扮演着处理请求的重要角色,例如加载特定的视图、将模型注入视图等。与 ASP.NET 一样,我们有两种 Web 应用程序框架,ASP.NET Web Forms 和 ASP.NET MVC,Node.js 提供 Express EJS、Jade 以及许多其他 Web 应用程序框架来构建健壮的 Web 应用程序。

将简单的 Node.js 扩展以使用 Express

使用 Node.js 的 Visual Studio 扩展,你可以获得所有模板来开始使用 Express 3.0 和 Express 4.0 应用程序框架。Express 4.0 是最新的版本,有一些新功能和改进。我们可以使用引导大多数配置级别工作的模板,但为了获得更多清晰度,我们将扩展前面创建的简单 Node.js 示例,并使用 Express 框架在其上开发一个简单的 Web 应用程序。

要使用Express,我们必须使用 NPM 添加其包依赖,如下面的截图所示:

将简单的 Node.js 扩展以使用 Express

一旦添加了 Express 包,您可以添加以下代码片段来启动 Express 应用程序:

//Initialized http object
var http = require('http');

//adding express dependency
var express = require('express');

//creating express application
var expressApp = express();

//Configuring root call where '/' represents root path of the URL
expressApp.get("/", function (req, res) {
    res.send("<html><body><div>Hello World</div></body></html>");
});

//declared port
var port = process.env.port || 1337;

//Initialized http server object and use res.write() to send actual response content
var httpServer = http.createServer(expressApp);

//listening for incoming request
httpServer.listen(port);

这是一个简单的Hello World示例,返回 HTML 内容。现在,在我们要返回特定视图而不是静态 HTML 内容的情况下,我们可以通过使用 Express 视图引擎来实现,接下来将讨论这一点。

Express 视图引擎

Express 拥有多种视图引擎,尽管 Jade 和 EJS 是最广泛使用的。我们将逐一了解这些差异是什么。

EJS 视图引擎

在 EJS 视图引擎中,视图是 HTML 页面,模型属性可以使用脚本片段<% %>绑定。

为了开始使用 EJS,我们需要通过 Visual Studio 中的 NPM 包管理器选项添加 EJS 包,或者通过执行npm install ejs –save命令来添加:

EJS 视图引擎

添加此代码后,我们可以将视图引擎设置为ejs,如下面的代码片段所示:

//Initialized http object
var http = require('http');

//adding express dependency
var express = require('express');

//creating express application
var expressApp = express();

//Set jade for Node.js application
expressApp.set('view engine', 'ejs') 

通过调用响应对象的render()方法设置ejs视图的路径,如下所示:

//Configuring root call where '/' represents root path of the URL
expressApp.get("/", function (req, res) {
    res.render("ejsviews/home/index");
});

home文件夹中添加index.ejs文件。所有视图都应该存放在根Views文件夹下,否则当应用程序运行时它们不会被加载。因此,应该在Views文件夹下定义ejsviews文件夹,在ejsviews文件夹下定义home,如下面的屏幕截图所示:

EJS 视图引擎

以下是在应用程序启动时将被渲染的 EJS 视图的内容:

<html>
 <body>
  <div> <h1> This is EJS View </h1> </div>
 </body>
</html>

ejsserver.js文件的底部添加创建服务器并监听端口号1337的代码:

//declared port
var port = process.env.port || 1337;

//Initialized http server object and use res.write() to send actual response content
var httpServer = http.createServer(expressApp);

//listening for incoming request
httpServer.listen(port);

当应用程序运行时,index.ejs将被加载并渲染以下所示的 HTML 内容:

EJS 视图引擎

我们也可以通过 JSON 对象的形式传递模型。假设我们需要传递应用程序名称和描述;我们可以在调用响应对象的render()方法时传递这些值,如下所示:

//Configuring root call where '/' represents root path of the URL
expressApp.get("/", function (req, res) {
    res.render("ejsviews/home/index", { appName: "EJSDemo", message: "This is our first EJS view engine example!" });
});

index.ejs中,我们可以使用脚本片段将这些值与 HTML 控件绑定:

<html>
 <body>
   <h1> <%= appName %> </h1>
  <p> <%= message %></p>
 </body>
</html>

EJS 还支持包含静态内容的布局页面,比如网页应用的头部和底部。因此,开发者不需要在每一页上都重新定义主要的布局内容,我们可以将其集中管理,就像我们在 ASP.NET MVC 中使用_layout.cshtml和 ASP.NET web forms 中的Site.master一样。

为了使用主页面,我们需要再添加一个包,称为ejs-local。此包可以通过 Visual Studio 中的 NPM 包管理器窗口添加,或者通过运行npm install ejs-local --save命令来添加:

EJS 视图引擎

在添加此包后,我们可以添加ejs-locals,如下所示。必须在设置视图引擎之前设置它:

//Initialized http object
var http = require('http');

//adding express dependency
var express = require('express');
var ejsLocal = require('ejs-locals');
//creating express application
var expressApp = express();

//Add engine that supports master pages
app.engine('ejs', ejsLocal);

在同一个ejsviews文件夹中添加layout.ejs页面,并指定 HTML 内容:

<html>
<head>
  <title> <%= appName %> </title>
</head>
<body>
  <%= body %>
</body>
</html>
index.ejs file:
<% layout('../layout.ejs') -%>
<h1><%= appName %></h1>
<p> <%= message %></p>

以下输出生成:

EJS 视图引擎

Jade 视图引擎

Jade 视图引擎是另一个 Node.js 视图引擎,其语法与我们之前在 EJS 中看到的有很大不同。当我们定义视图时,需要先通过 NPM 安装 Jade 视图引擎。我们可以在 Visual Studio 的 NPM 包管理器中安装,或者通过运行 npm install jade –save 命令:

Jade 视图引擎

安装后,它将在 package.json 的依赖项部分添加 Jade 包。我们将从在 app.js 文件(Node.js 项目的入口点)中设置 Jade 视图引擎开始。

以下是在 app.js 中设置 Jade 视图引擎的代码:

//adding express dependency
var express = require('express');

//creating express application
var expressApp = express();

//Set jade for Node.js application
expressApp.set('view engine', 'jade');

你会注意到我们没有通过 require 对象指定 Jade 引用。这是因为当 Express 框架被加载时,它将自动注册 Jade 的依赖项。以下代码片段加载了 Jade 视图:

//Configuring root call where '/' represents root path of the URL
expressApp.get("/", function (req, res) {
res.render("home/index", 
{ 
appName: "JadeDemo",   
message: "This is our first Jade view engine example!"
}
);
});

Jade 视图语法通常与 HTML 不同,所有视图扩展名都应该是 .jade。在前面的代码中,我们指向了 index.jade,其中不需要显式指定 Jade。Index.jade 应该位于 views/home 文件夹下。让我们创建一个名为 views 的文件夹,然后在里面创建一个名为 home 的文件夹。添加一个新的 Jade 文件并将其命名为 index.jade。以下代码显示了 appNamemessage 在 HTML 元素中:

doctype
html
    body
        h1= appName
        p= message

使用 Jade 语法,你不需要定义完整的 HTML 标签,你只需通过它们的名称指定,后面跟着分配给它们的值。例如,在前面的示例中,我们通过响应 render() 方法传递的 JSON 对象设置了 appNamemessage 的值。然而,HTML 元素支持许多更多的属性,如设置控件宽度、字体颜色、字体样式等。在后面的章节中,我们将了解如何在 Jade 中实现这一点。

等于(=)操作符只有在您绑定到注入到视图中的任何值时才需要。如果您想要指定一个硬编码的静态值,那么可以很容易地不使用等于操作符来设置,如下面的代码所示:

doctype
html
    body
        h1 Jade App
        p This is Jade View

以下是一些使用 Jade 语法处理 HTML 特定场景的示例:

属性 Jade HTML
文本框
input(type='text' name='txtName')

|

<input type='text' name='txtName'/>

|

锚点标签
a(href='microsoft.com') Microsoft

|

<a href="microsoft.com">Microsoft</a>

|

复选框
input(type='checkbox', checked)

|

<input type="checkbox" checked="checked"/>

|

带样式属性的锚点
a(style = {color: 'green', background: 'black'})

|

<a style="color:green;background:black"></a>

|

链接按钮
input(type='button' name='btn')

|

<input type="button" name="btn"/>

|

你可以在 jade-lang.com/ 了解更多关于 Jade 语言的信息。

Jade 的框架也支持布局页面。布局页面包含网站的静态信息,这些信息大部分位于页眉、页脚或侧边栏中,而实际内容根据请求的页面而变化。在 ASP.Net Web 表单中,我们使用<asp:ContentPlaceHolder>标签定义主页面,该页面将渲染页面的内容引用到该主页面。在 ASP.NET MVC 中,这可以通过使用 Razor @RenderBody元素来实现。在 Jade 中,我们可以使用block关键字后跟块的名称来定义内容块。例如,以下是的layout.jade,其中包含block contentBlock声明,其中block表示子页面的内容渲染位置,contentBlock是要在子页面中定义的块的名称。在单个视图中也可以定义多个块。

以下是布局页面的内容:

doctype html
html
  head
    title Jade App
  body
  block contentBlock

布局页面可以使用extends关键字后跟布局页面名称与layout页面一起使用。Jade 视图引擎会自动搜索具有该名称的页面,如果找到,则搜索块名称并在该位置放置内容。以下是使用布局页面layout.jade的子页面index.jade

extends layout
block contentBlock
        h1= appName
        p= message

输出将会如下所示:

玉视引擎

Express 应用程序中的路由

我们已经学习了 EJS 和 Jade 视图引擎的基本知识。两者都提供类似的功能,但语法不同。在前面的示例中,我们发送了一个响应,指向一个特定的页面,在客户端渲染内容。

Express 框架提供了与 HTTP 方法相对应的多个方法,如getpostputdelete等。我们可以使用get方法来获取一些内容,post来创建一个记录,put来更新,等等。页面可以位于Views文件夹内的任何地方,但是路由实际上定义了当在特定的 URL 路径上请求时必须加载哪个页面。

让我们在Views/ejsviews/home文件夹内创建一个名为about.ejs的 EJS 页面。

路由可以通过 Express 应用程序对象来定义,如下面的代码所示:

expressApp.get("/About", function (req, res) {
    res.render("ejsviews/home/about");
});

当用户浏览到http://localhost/About时,会显示关于页面。

中间件

Node.js Express 还提供了一个特殊的路由方法all(),它没有映射到任何 HTTP 方法。但是,它用于在路径上加载中间件,而不管请求的 HTTP 方法是什么。例如,对http://localhost/middlewareexample进行 HTTP GETPOST请求将会执行下面代码中显示的相同的all()方法:

expressApp.all('/middlewareexample', function (req, res) {
    console.log('Accessing the secret1 section ...');
});

就像在 .NET 中一样,我们有 OWIN 中间件可以链接到请求管道。同样,Node.js Express 中间件也可以链接,并且可以通过稍微修改函数签名来调用下一个中间件。以下是修改后的版本,在响应对象之后添加了 next 参数,为特定请求路径定义管道中的下一个中间件的处理器:

expressApp.all('/middlewareexample', function (req, res, next) {
    console.log('Accessing the secret1 section ...');
    next();
});

例如,假设我们有两个中间件,第一个中间件只是将信息输出到控制台窗口,而第二个中间件则将 HTML 内容返回给客户端。以下是包含这两个中间件的 EJS 视图引擎的 server.js 文件:

//Initialized http object
var http = require('http');
//adding express dependency
var express = require('express');

//creating express application
var expressApp = express();

expressApp.all('/middlewareexample', function (req, res, next) {
    console.log('Middleware executed now calling next middleware in the pipeline');
    next(); // pass control to the next handler
});
expressApp.all('/middlewareexample', function (req, res) {
    res.send("<html><body><div>Middleware executed</div></body></html>");    
});

//declared port
var port = process.env.port || 1337;

//Initialized http server object and use res.write() to send actual response content
var httpServer = http.createServer(expressApp);

//listening for incoming request
httpServer.listen(port);

现在当我们访问 URL 路径 http://localhost/middlewareexample 时,消息将在控制台打印,并在浏览器中呈现 HTML 内容:

中间件

以下是将在浏览器中呈现的 HTML 内容:

中间件

MVC 与 Express 框架

几乎每个应用程序都由无数页面组成,而在主 server.js 中定义所有逻辑和路由既不实际也不易维护。在本节中,我们将了解如何使用 Express 框架实现 模型-视图-控制器MVC)模式。我们将开发一个简单的应用程序,以了解如何创建控制器和数据服务,以及如何使用 Express 框架加载视图并注入模型。

模型-视图-控制器模式

模型-视图-控制器(MVC)是一种用于分离应用程序关注点的软件架构模式。模型表示包含属性以持有信息的实体,而控制器则用于将模型注入视图并加载视图。控制器还用于将模型存储在数据库中,而视图是呈现由控制器注入的模型的页面,并在需要时使用它。

创建控制器

我们将从创建一个简单的 homeController 开始,以渲染主页。让我们扩展上述开发的 EJS 视图引擎示例,并在项目的根目录下创建一个 Controllers 文件夹。在 Controllers 文件夹内,创建一个 HomeController.js 文件,并将以下代码片段放在那里:

(function (homeController) {
    homeController.load = function (expressApp) {
        expressApp.get('/', function (req, res) {
            res.render("ejsviews/home/index", {appName: "EJS Application", message:"EJS MVC Implementation"})
        });
    };
})(module.exports);

在前面的代码中,有一个匿名 JavaScript 函数,它接受 module.export 对象,并在执行时将其绑定到 homeController。以这种方式实现的基本优点是,定义在 homeController 对象中的每个方法或属性都将可导出并可供调用对象访问。在前面的示例中,我们定义了一个 load() 方法,它定义了根路径(/)的路由并返回 Index 页面给客户端。

在主 ejsserver.js 文件中,我们可以使用控制器,如以下代码所示,通过使用 require 对象:

//Initialized http object
var http = require('http');

//adding express dependency
var express = require('express');

//adding ejs locals
var ejsLocal = require('ejs-locals');

//creating express application
var expressApp = express();

//Add engine that supports master pages
expressApp.engine('ejs', ejsLocal);

//Set jade for Node.js application
expressApp.set('view engine', 'ejs');

//Initializing HomeController
var homeController = require('./Controllers/HomeContoller.js');
homeController.load(expressApp);

//declared port
var port = process.env.port || 1337;

//Initialized http server object and use res.write() to send actual response content
var httpServer = http.createServer(expressApp);

//listening for incoming request
httpServer.listen(port);

在前面的代码中,我们使用 require 对象添加了 HomeController 对象,并调用 load() 方法来定义路由,使得当网站运行时能够导航到索引页面。

创建数据服务

每个商业应用程序都涉及大量的 CRUD(创建、读取、更新、删除)操作。为了更好的设计,这些操作可以分别实现在数据服务对象中,所以如果多个控制器想要使用同一个服务,它们可以重复使用而不需要重复编写相同的代码。在本节中,我们将创建一个名为 DataServices 的文件夹,位于应用程序的根目录下,并在其中创建 ProductService.js。以下是 ProductService.js 的代码,它返回产品数组:

(function(data){
    data.getProducts = function () {
        return [{
                name: 'Product1',
                price: 200,
            }, 
            {
                name: 'Product2',
                price: 500
            },
            {
                name: 'Product3',
                price: 1000
            }
        ];
    };
})(module.exports);

我们可以通过 require 对象在 HomeController 中使用这个 ProductService

(function (homeController) {
    var productService = require('../DataServices/ProductService');

    homeController.load = function (expressApp) {
        expressApp.get('/', function (req, res) {
            var products = productService.getProducts();
            res.render("ejsviews/home/index", { appName: "EJS Application", message: "EJS MVC Implementation", data: products });
        });
    };
})(module.exports);

以下是 index.ejs 文件,它遍历产品并显示产品名称和价格:

<% layout('../layout.ejs') -%>
<h1><%= appName %></h1>

<p> <%= message %></p>

<div>

 <% data.forEach(function(product) { %>
   <li><%= product.name %> - <%= product.price %></li>
 <% }); %>

</div>

最后,输出结果如下:

创建数据服务

在 Node.js 中访问 Microsoft SQL 服务器

Node.js 提供了不同的数据库驱动,可以作为 node 包添加。有 MongoDB 驱动、Microsoft SQL Server 驱动等等。我们将使用 Node.js 的 MS SQL 驱动来连接 Microsoft SQL 服务器数据库。要安装 mssql,您可以运行 npm install mssql –save 命令,或者从 NPM 包管理器窗口中添加,如下面的截图所示:

在 Node.js 中访问 Microsoft SQL 服务器

提示

使用 MSSQL 驱动时,对于相应的 SQL 服务器实例应启用 TCP/IP。

从 Microsoft SQL 服务器数据库中读取记录

DataService.js 文件中,我们将添加 getProducts() 方法,它从 SQL Server 数据库加载产品列表。

以下是 getProducts() 方法,它接受回调函数,所以一旦从数据库中获取了产品列表,它就会在调用者的回调函数中传递:

(function(data){
data.getRecords = function (callbackFn) {
        //loaded SQL object
        var sql = require('mssql');

        //database configuration attributes to connect
        var config = {
            user: 'sa',
            password: '123',
            server: 'ovais-pc', // You can use 'localhost\\instance' to connect to named instance 
            database: 'products'
        }

        var products = null;
        //Connect to SQL Server returns a promise and on successfull connection executing a query using Request object
        sql.connect(config).then(function () {
            new sql.Request().query('select * from products', function (err, recordset) {      
                callbackFn(recordset);        
            });
        });

     };
})(module.exports);

在前面的代码中,我们使用 require 对象初始化了 sql 对象。Config 变量包含连接属性,如 usernamepasswordserverdatabase。在调用 sql connect() 方法时传递这个属性。Connect() 方法返回一个 then() 承诺,通过它我们可以使用 sql.Request() 方法发起 SQL 查询请求。如果请求成功,我们将在 recordset 对象中获取结果集,并通过其回调函数返回给调用者。

以下是修改后的 HomeController.js 文件,它调用 DataServicegetRecords() 方法,并将检索到的产品列表作为模型传递给索引视图:

(function (homeController) {
    var productService = require('../DataServices/ProductService');

    homeController.load = function (expressApp) {
        expressApp.get('/', function (req, res) {
            var products = productService.getRecords(function (products) {
                console.dir(products);
                res.render("ejsviews/home/index", { appName: "EJS Application", message: "EJS MVC Implementation", data: products });
            });
        });
    };
})(module.exports);

以下是 index.js 文件,它遍历产品列表并显示产品名称和价格:

<% layout('../layout.ejs') -%>
<h1><%= appName %></h1>
<p> <%= message %></p>

<table>
<th> 
<td> Product Name </td>
<td> Description </td>
<td> Price </td>
</th>
 <% data.forEach(function(product) { %>
  <tr> <td><%= product.Name %> </td> <td> <%= product.Description %> </td><td> <%= product.Price %> </td></tr>
 <% }); %>
</table>

在 Microsoft SQL 服务器数据库中创建记录

要在数据库中创建记录,我们可以定义 HTML 表单标签内的 HTML 输入元素,并在表单提交时通过在HomeController.js文件中定义post方法来发送 POST 请求:当表单提交时,可以使用request.body对象检索值。这是一个解析器,它解析 DOM 并创建一个包含表单标签下的元素的列表。我们可以像req.body.txtName这样访问它,其中txtName是 HTML 输入元素,req是请求对象。

Express 4.0 将body-parser对象解耦为一个单独的包,可以使用npm install body-parser –save命令单独下载,或者通过 NPM 包管理器窗口,如下面的屏幕截图所示:

在 Microsoft SQL 服务器数据库中创建记录

在你的主ejsserver.js文件中,使用require对象添加body-parser,并通过调用expressApp,use()方法将其传递给expressApp对象:

var bodyParser = require('body-parser');

expressApp.use(new bodyParser());

一旦添加了这些内容,我们就可以修改HomeController.js,并定义一个POST方法,一旦表单提交,该方法将被调用:

    expressApp.post('/', function (req, res) {
            console.log(req.body.txtName);
           productService.saveProduct(req.body.txtName, req.body.txtDescription, req.body.txtPrice, function (result) {
                res.send("Record saved successfully");
            });
        });
DataService.js file:
data.saveProduct = function (name, description, price, callbackFn) {

        //loaded SQL object
        var sql = require('mssql');

        //database configuration attributes to connect
        var config = {
            user: 'sa',
            password: '123',
            server: 'ovais-pc', // You can use 'localhost\\instance' to connect to named instance 
            database: 'products'
        }

        //Connect to SQL Server returns a promise and on successfull connection executing a query using Request object
        sql.connect(config).then(function () {
            new sql.Request().query("INSERT into products (Name, Description, Price) values('"+ name +"', '"+ description+"',"+ price+")", function (err, recordset) {
                callbackFn(recordset);
            });
       });

    };

最后,这是包含NameDescriptionPrice字段的表单的Index.ejs视图:

<form method="post">
<table>
<tr>
  <td> Product Name: </td>
  <td> <input type='text' name='txtName'  /> </td>
</tr>
<tr>
  <td> Description: </td>
  <td><input type='text' name='txtDescription'  /></td>
</tr>
<tr>
  <td> Price: </td>
  <td><input type='number' name='txtPrice' /></td>

</tr>
<tr>
<td> &nbsp; </td>
<td><input type="submit" value="Save" /> </td>
</tr>
</table>
</form>

要了解关于mssql节点包的更多信息,请使用这个链接:www.npmjs.com/package/mssql

总结

本章介绍了 Node.js 的基础知识以及如何使用它们来开发使用 JavaScript 的服务器端应用程序。我们了解到了两种视图引擎,EJS 和 Jade,以及如何使用它们。我们还学习了如何使用控制器和服务来实现 MVC 模式。最后,我们通过查看访问 Microsoft SQL 服务器数据库的示例,来了解如何执行数据库上的增删改查操作。在下一章中,我们将关注在大型应用程序中使用 JavaScript 的最佳实践。

第九章:使用 JavaScript 进行大规模项目

大型网络应用项目由多个模块组成。随着各种 JavaScript 框架的开发不断进步和提升,开发者在应用程序的展示或前端层频繁使用 JavaScript,而服务器端操作只在需要时执行。例如,当从服务器保存或读取数据,或进行其他数据库或后端操作时,向服务器发送 HTTP 请求,返回纯 JSON 对象并更新 DOM 元素。随着这些发展,应用程序的大部分前端代码都位于客户端。然而,当 JavaScript 最初被开发时,它的目标是用于执行一些基本操作,比如更新 DOM 元素或显示确认对话框等相对操作。JavaScript 代码主要存在于页面本身的<script>脚本标签中。然而,大规模应用程序包含许多代码行,在设计和架构前端时需要适当的关注。在本章中,我们将讨论一些概念和最佳实践,以帮助使应用程序前端更具可扩展性和可维护性。

在行动之前先思考

大规模应用通常包含许多 JavaScript 文件,合理地组织这些文件可以提高可见性。像 AngularJS、EmberJS 这样的 JavaScript 框架已经提供了适当的组织和指导,用于定义控制器、工厂和其他对象,同时也提供了使用它们的最佳实践。这些框架非常流行,并且已经符合了更高可扩展性和可维护性的需求。然而,在某些情况下,我们可能想严格依赖纯 JavaScript 文件,并为特定需求开发自己的自定义框架。为了认可这些情况,行业内已经采用了某些最佳实践,这些实践使得基于 JavaScript 的前端更加可维护和可扩展。

当我们在大型应用程序上工作时,我们需要思考应用程序的范围是什么。我们需要考虑应用程序如何容易地被扩展,以及如何快速地实现其他模块或功能。如果任何模块失败,它会影响应用程序的行为还是导致其他模块崩溃?例如,如果我们正在使用某个第三方 JavaScript 库,该库修改了它们某些方法签名。在这种情况下,如果我们在应用程序的每个地方都频繁使用第三方库,我们就必须在每个点上修改方法,而且不仅更改,而且测试也可能是一个繁琐的过程。另一方面,如果已经实现了一些 Facade 或包装器,那么我们只需要在一个地方进行更改,而不是到处更新。因此,设计应用程序架构或框架是一个深思熟虑的过程,但它使应用程序更加健壮和健康。

开发高度可扩展和可维护的应用程序

以下是我们应该考虑的因素,以创建高度可扩展和可维护的基于 JavaScript 的 Web 应用程序。

模块化

在大型的应用程序中,将所有内容写入一个 JavaScript 文件是不好的做法。尽管如此,即使你为不同的模块分离了不同的 JavaScript 文件,并通过脚本<script>标签引用它们,这也会使全局命名空间膨胀。应该进行适当的结构化,以将 JavaScript 文件保存在单独的模块文件夹中。例如,一个 ERP 应用程序包括几个模块。我们可以为每个模块创建单独的文件夹,并使用特定的 JavaScript 文件为特定的视图或页面提供某些功能。然而,公共文件可以存放在公共文件夹中。

以下是一个根据 ERP 模块来组织 JavaScript 文件的示例项目结构。每个模块都有一个service文件夹,其中包含一些用于服务器端读或写操作的文件,以及一个Views文件夹,用于在数据加载或任何控件事件触发后操作特定视图的 DOM 元素。common文件夹可能包含所有其他模块都会使用的助手工具和函数。例如,在控制台日志消息,或在服务器端发送 HTTP 请求,这些功能可以定义在公共 JavaScript 文件中,并且它们可以被服务或视图 JavaScript 文件使用:

模块化

在前面的结构中,Services文件夹可以包含与调用某些 Web API 或 Web 服务执行数据库的创建检索更新和删除CRUD)操作相关的函数,而像FIMain.js这样的视图文件包含页面特定的函数。

为了保持 HTML 页面的整洁,将 JavaScript 文件与 HTML 页面分开是一个更好的方法。所以在之前的截图中,FIMain.js包含了与主页面对应的 JavaScript 函数,而FIDashboard.js包含了与仪表板页面对应的 JavaScript 函数,依此类推。

这些文件可以通过<script>脚本标签简单地添加,但在 JavaScript 世界中,直接在页面上添加 JavaScript 文件是不好的做法。模块可以通过实现模块模式在 JavaScript 中定义。然而,大多数开发者更愿意使用 RequireJS API 来定义模块,以使模块加载更简单,并提供更好的变量和函数定义范围。它与 CommonJS 系统等效,但由于其异步行为而受到推荐。它以异步方式加载 JavaScript 模块,使页面加载周期更快。

实现模块模式

模块模式是用于创建松耦合架构和使 JavaScript 代码片段独立于其他模块的最流行的设计模式。

模块就像.NET 类一样,可以有私有、受保护和使用公开的属性和方法,并为开发者提供控制,只暴露其他类需要的属性和方法。

在 JavaScript 中,模块模式可以通过立即执行函数表达式IIFE)实现,该表达式立即执行并返回一个闭包。闭包实际上隐藏了私有变量和方法,并返回一个只包含公共方法和变量的对象,供其他模块访问。

以下是暴露了logMessage()方法的Logger模块,该方法调用一个私有formatMessage()方法来附加日期,并返回格式化后的消息,然后将其打印在浏览器的控制台窗口上:

<script>
  var Logger= (function () {

    //private method
    var formatMessage = function (message) {
      return message + " logged at: " + new Date();
    }

    return {
      //public method
      logMessage: function (message) {
        console.log(formatMessage(message));
      }
    };

  })();

  Logger.logMessage("hello world");
</script>

在前面的代码中,logMessage()方法返回一个通过Logger命名空间调用的对象。

模块可以包含多个方法和属性,为了实现这种情况,让我们修改前面的示例,再添加一个显示警告消息的方法和一个访问日志名称的属性,并通过对象字面量语法暴露它们。对象字面量是另一种表示将方法和属性作为名称值对分离并用逗号分隔的绑定方式,提供了更清晰的表示。以下是修改后的代码:

<script> 
  var Logger= (function () {
    //private variable
    var loggerName = "AppLogger";

    //private method
    var formatMessage = function (message) {
      return message + " logged at: " + new Date();
    }

    //private method
    var logMessage= function (message){
      console.log(formatMessage(message));
    }

    //private method
    var showAlert = function(message){
      alert(formatMessage(message));
    }

    return {

      //public methods and variable
      logConsoleMessage: logMessage,
      showAlertMessage: showAlert,
      loggerName: loggerName
    };

  })();

  Logger.logConsoleMessage("Hello World");
  Logger.showAlertMessage("Hello World");
  console.log(Logger.loggerName);
</script>

在前面的代码中,logMessage()showAlert()将通过logConsoleMessage()showAlertMessage()方法进行访问。

使用 RequireJS 对 JavaScript 代码进行模块化

RequireJS 中的模块是模块模式的扩展,其好处是不需要全局变量来引用其他模块。RequireJS 是一个 JavaScript API,用于定义模块并在需要时异步加载它们。它异步下载 JavaScript 文件,并减少整个页面加载的时间。

使用 RequireJS API 创建模块

在 RequireJS 中,可以通过define()方法创建模块,并使用require()方法加载。RequireJS 提供了两种语法风格来定义模块,如下所示:

  • 使用 CommonJS 风格定义模块:以下是在 CommonJS 风格中定义模块的代码片段:

    define(function (require, exports, module) {
      //require to use any existing module
      var utility = require('utility');
    
      //exports to export values
      exports.example ="Common JS";
    
      //module to export values 
      module.exports.name = "Large scale applications";
    
      module.exports.showMessage = function (message) {
        alert(utility.formatMessage(message));
      }
    });
    

    前面的 CommonJS 风格语法使用了 RequireJS API 的define()方法,该方法接受一个函数。此函数接受三个参数:requireexportsmodule。后两个参数exportsmodule是可选的。但是,它们必须按照相同的顺序定义。如果你不使用require,只想通过exports对象导出一些功能,那么需要提供require参数。require参数用于导入使用exportsmodule.exports在其他模块中导出的模块。在前面的代码中,我们通过在调用require方法时指定utility.js文件的路径,添加了utility模块的依赖。添加任何依赖时,我们只需要指定路径以及 JavaScript 文件的名称,而不需要.js文件扩展名。文件由 RequireJS API 自动识别。我们可以通过exportsmodule.exports适当地导出其他模块需要使用的任何函数或变量。

  • 在 AMD 风格中定义模块:以下是在 AMD 风格语法中定义模块的代码片段:

    define(['utility'], function (utility) {
      return {
        example: "AMD",
        name: "Large scale applications",
        showMessage: function () {
          alert(utility.formatMessage(message));
        }
      }
    
    });
    

    AMD 风格语法将依赖项数组作为第一个参数。要使用 AMD 风格语法加载模块依赖项,你必须将它们定义在一个数组中。第二个参数接受function参数,它取模块名称,映射到依赖项数组中定义的模块,以便在函数体中使用。要导出变量或方法,我们可以通过对象字面量语法进行导出。

启动 RequireJS

让我们通过一个简单的例子来了解如何在 ASP.NET 应用程序中使用 RequireJS。要在 ASP.NET Core 应用程序中使用 RequireJS API,你必须下载并将在wwwroot/js文件夹中放置Require.js文件。在下面的例子中,我们将编写一个logging模块,其中包含一些方法,如写入控制台、显示警告以及向服务器写入。

让我们在wwwroot/js/common文件夹中创建一个Logging.js文件,并写入以下代码:

define(function () {
  return {
    showMessage: function (message) {
      alert(message);
    },
    writeToConsole: function (message) {
      console.log(message);
    },
    writeToServer: function (message) {
      //write to server by doing some Ajax request
      var xhr = new XMLHttpRequest();
      xhttp.open("POST", "http://localhost:8081/Logging?message="+message, true);
      xhttp.send();
    }
  }
});

以下是Index.cshtml页面的代码,当页面加载时会显示一个警告消息:

<script src="img/require.js"></script>
<script>
  (function () {
    require(["js/common/logging"], function(logging){
      logging.showMessage("demo");
    });
  })();
</script>

我们还可以将前面的函数包装在main.js文件中,并通过脚本<script>标签启动它。有一个特殊的属性称为data-main,它是由 RequireJS 用作应用程序的入口点。

以下是位于wwwroot/JS文件夹中的main.js代码。因为main.js位于wwwroot/js文件夹中,所以路径将是common/logging

//Main.js
require(["common/logging"], function(logging){
  logging.showMessage("demo");
});

最后,我们可以使用脚本标签启动main.js,如下面的代码所示:

<script data-main="~/js/main.js" src="img/require.js"></script>

以下是一个包含Common文件夹的示例项目结构,以存储常见的 JavaScript 文件;而FIHR文件夹用于模块特定的 JavaScript 文件:

启动 RequireJS

假设我们想要修改之前的例子,并在按钮的click事件上从输入控件传递消息。这可以通过为特定页面开发一个view模块并在其中注入logging模块来实现。

以下是要包含inputbutton元素的 HTML 标记:

<div id="myCarousel" class="carousel slide" data-ride="carousel" data-interval="6000">
  <input type="text" id="txtMessage" />
  <button id="btnSendMessage" >Send Message</button>
</div>

下面的view.js文件通过读取txtMessage元素的值来调用logging模块的sendMessage()方法:

define(['common/logging'], function(logging) {
  $('#btnSendMessage').on('click', function(e) {
    sendMessage();
    e.preventDefault();
  });
  function sendMessage(){
    var message= document.getElementById('txtMessage').value;
    logging.showMessage(message);
  }
  return {
    sendMessage: sendMessage
  };
});

当按钮被点击时,将显示一个警告消息。

事件驱动的消息传递

在前一部分,我们为 JavaScript 文件启用了模块化支持并将它们转换为模块。在大型应用程序中,我们不能仅仅依赖于在其他模块中注入模块,我们可能需要一些灵活性,通过某种发布/订阅模式调用某些模块的事件。我们已经在第七章中看到了发布/订阅模式,该模式维护一个注册事件(指向某些回调函数)的集中式列表,并通过发布者对象调用这些事件。这种模式在使模块之间的事件驱动消息传递变得非常实用,但还有一种更好的模式,即中介者模式,它是发布/订阅模式的一个超集。中介者模式更好,因为它允许发布者或中介者访问订阅对象的其他事件/方法,并允许中介者决定需要调用哪个方法或事件。

为模块之间的通信实现中介者模式

中介者将对象封装在集中式列表中并调用它们的方法。这个列表将所有对象(或模块)放在中央位置,从而允许它们之间改进的通信。

让我们通过一个实现中介者模式的实际例子来了解。中介者作为一个集中控制的对象,模块可以进行订阅或取消订阅。它提供了抽象方法,任何源订阅模块都可以调用这些方法与目标订阅模块进行通信。中介者持有一个集中式字典对象,根据某些键(或通常是名称)持有订阅对象,并根据订阅者传递的模块名称调用目标模块方法。在下面的例子中,我们有了MediatorCore(中介者)、EmployeeRepository(订阅者)和HRModule(订阅者)对象。我们将使用 RequireJS API 将 JavaScript 文件转换为模块。

下面的MediatorCore JavaScript 文件:

//MediatorCore.js
define(function () {
  return {

    mediator: function () {
      this.modules = [];

      //To subscribe module
      this.subscribe = function (module) {
        //Check if module exist or initialize array
        this.modules[module.moduleName] = this.modules[module.moduleName] || [];

        //Add the module object based on its module name
        this.modules[module.moduleName].push(module);
        module.mediator = this;
      },

      this.unsubscribe = function (module) {
        //Loop through the array and remove the module
        if (this.modules[module.moduleName]) {
          for (i = 0; i < this.modules[module.moduleName].length; i++) {
            if (this.modules[module.moduleName][i] === module) {
              this.modules[module.moduleName].splice(i, 1);
              break;
            }
          }
        }
      },

      /* To call the getRecords method of specific module based on module name */
      this.getRecords = function (moduleName) {
        if (this.modules[moduleName]) {
          //get the module based on module name
          var fromModule = this.modules[moduleName][0];
          return fromModule.getRecords();
        }
      },

      /* To call the insertRecord method of specific module based on module name */
      this.insertRecord = function (record, moduleName) {
        if (this.modules[moduleName]) {
          //get the module based on module name
          var fromModule = this.modules[moduleName][0];
          fromModule.insertRecord(record);
        }
      },

      /* To call the deleteRecord method of specific module based on module name */
      this.deleteRecord = function (record, moduleName) {
        if (this.modules[moduleName]) {
          //get the module based on module name
          var fromModule = this.modules[moduleName][0];
          fromModule.deleteRecord(record);

        }
      },

      /* To call the updateRecord method of specific module based on module name */
      this.updateRecord = function (record, moduleName) {
        if (this.modules[moduleName]) {
          //get the module based on module name
          var fromModule = this.modules[moduleName][0];
          fromModule.updateRecord(record);

        }
      }

    }
  }
});
EmployeeRepository that contains the concrete implementation of the abstract methods defined in the mediator:
//EmployeeRepository.js
define(function () {
  return {

    //Concrete Implementation of Mediator Interface
    EmployeeRepository: function (uniqueName) {
      this.moduleName = uniqueName;
      //this reference will be used just in case to call some other module methods
      this.mediator = null;

      //Concrete Implementation of getRecords method
      this.getRecords = function () {
        //Call some service to get records

        //Sample text to return data when getRecords method will be invoked
        return "This are test records";

      },
      //Concrete Implementation of insertRecord method
      this.insertRecord = function (record) {
        console.log("saving record");
        //Call some service to save record.
      },

      //Concrete Implementation of deleteRecord method
      this.deleteRecord = function (record) {
        console.log("deleting record");
        //Call some service to delete record
      }

      //Concrete Implementation of updateRecord method
      this.updateRecord = function (record) {
        console.log("updating record");
        //Call some service to delete record
      }

    }
  }
});

EmployeeRepository在初始化时接收一个名称参数,并定义了一个中介变量,该变量在注册中介时可以设置。这样提供的目的是,如果EmployeeRepository想要调用其他模块或订阅模块的仓库,就可以这样做。我们可以创建多个仓库,例如为HRModule创建RecruitmentRepositoryAppraisalRepository,并在需要时使用它们。

以下是HRModule的代码,通过中介调用EmployeeRepository

//HRModule.js
define(function () {
  return {
    HRModule: function (uniqueName) {
      this.moduleName = uniqueName;
      this.mediator = null;
      this.repository = "EmployeeRepository";

      this.getRecords = function () {
        return this.mediator.getRecords(this.repository);
      },

      this.insertRecord = function (record) {
        this.mediator.insertRecord(record, this.repository);
      },

      this.deleteRecord = function (record) {
        this.mediator.deleteRecord(record, this.repository);
      }

      this.updateRecord = function (record) {
        this.mediator.updateRecord(record, this.repository);
      }

    }
  }
});

现在,我们将注册HRModuleEmployeeRepository到中介,并调用HRModule方法以执行 CRUD 操作。

以下是HRView.js的代码,用于捕获表单上按钮的click事件,并在按钮被点击时调用getRecords()方法:

//HRView.js
define(['hr/mediatorcore','hr/employeerepository','hr/hrmodule'], function (mediatorCore, employeeRepository, hrModule) {
  $('#btnGetRecords').on('click', function (e) {
    getRecords();
    e.preventDefault();
  });
  function getRecords() {
    var mediator = new mediatorCore.mediator();
    var empModule = new hrModule.HRModule("EmployeeModule");
    mediator.subscribe(empModule);

    var empRepo = new employeeRepository.EmployeeRepository("EmployeeRepository");
    mediator.subscribe(empRepo);

    alert("Records: "+ empModule.getRecords());
  }
  return {
    getRecords: getRecords
  };
});

以下是使用 RequireJS API 引导HRView.js文件的main.js文件:

//main.js
require(["./hrview"], function(hr){
});

最后,我们可以在 ASP.NET 的Index.cshtml页面上使用上述Main.js模块,如下所示:

//Index.cshtml

@{
  ViewData["Title"] = "Home Page";
}
<script data-main="js/main.js"  src="img/require.js"></script>

<div id="myCarousel" class="carousel slide" data-ride="carousel" data-interval="6000">
  <input type="text" id="txtMessage" />
  <button id="btnGetRecords" >Send Message</button>
</div>

以下是显示模块如何相互通信的逻辑图:

模块间通信的中介者模式实现

封装复杂代码

开发高度可扩展和可维护应用程序的另一个核心原则是使用包装器,并将复杂代码封装到更简单的接口中。这可以通过实现一个外观模式来完成。

外观模式(Facade Pattern)用于简化复杂代码,通过暴露一个方法并隐藏所有复杂代码在 Facade 对象内部。例如,有多种方法和 API 可用于执行 Ajaxified 操作。可以使用一个普通的XmlHttpRequest对象发出 Ajax 请求,或者使用 jQuery,使用$.post()$.get()方法非常容易。在 AngularJS 中,可以使用其自己的http对象来调用服务等等。这些类型的操作可以通过封装,在内部 API 更改时,或者当你决定使用另一个更好的 API 时受益;修改工作量远小于更改所有使用过的地方。使用外观模式,你只需要在 Facade 对象中修改一次,并节省在所有使用过的地方更新它的时间。

使用外观模式的另一个优点是,它通过将一串代码封装到一个简单的方法中,减少了开发工作量,并使消费者容易使用。外观模式通过最小化调用特定功能所需的代码行数,减少了开发工作量。要了解更多关于外观模式的信息,请参考第七章,《JavaScript 设计模式》。

生成文档

适当的文档可以提高你的应用程序的可维护性,并使开发者在需要时或定制应用程序时更容易参考。市场上有很多文档生成器可供选择。JSDoc 和 YUIDoc 是非常流行的 JavaScript 文档生成器,但在本节中,我们将使用 JSDoc3,它不仅可以生成文档,还可以为你的自定义 JavaScript 模块启用 intellisense,以便在开发过程中提供便利。

JSDoc 是一个类似于 JavaDoc 和 PHPDoc 的 API。可以直接在 JavaScript 代码中添加注释。它还通过 JSDoc 工具提供了文档网站的生成。

在 ASP.NET Core 中安装 JSDoc3

JSDoc3 可以作为一个 Node 包添加,我们还可以使用 Gulp 任务运行器来生成文档。要将 JSDoc3 添加到你的 ASP.NET Core 项目中,你可以首先在由 Node 使用的package.json文件中添加一个条目。这个条目必须在开发依赖项中完成:

在 ASP.NET Core 中安装 JSDoc3

前一张截图中定义的第一个开发依赖是 Gulp,它用于创建任务,而gulp-jsdoc3是实际的文档生成器,当你运行那个任务时,它会生成 HTML 网站。

任务可以定义如下:

/// <binding Clean='clean' />
"use strict";

var gulp = require("gulp"),
jsdoc = require("gulp-jsdoc3");

var paths = {
  webroot: "./wwwroot/"
};

paths.appJs = paths.webroot + "app/**/*.js";

gulp.task("generatedoc", function (cb) {
  gulp.src(['Readme.md', paths.appJs], { read: false })
  .pipe(jsdoc(cb));
});
generatedoc, in which we are reading the files placed at wwwroot/app/**/*.js and generating documentation. The jsdoc object takes the configuration defaults to generate documentation. To pass the default configuration attributes, we can just specify the cb parameter injected in the function level by Gulp. When you run this generatedoc task from the task runner in Visual Studio, it will add a docs folder at the root path of your web application project. As in ASP.NET Core, we already know that all static content should reside in the wwwroot folder, and to access it from browser, simply drag and drop this folder in the wwwroot folder and access it by running your website.

添加注释

为了生成文档,我们需要用注释注释我们的代码。提供的注释越多,生成的文档就会越好。注释可以通过/**作为开始标签和*/作为结束标签来添加:

/** This method is used to send HTTP Get Request **/
function GetData(path) {
  $.get(path, function (data) {
    return data;
  })
}

如果函数是构造函数,你可以在注释中指定@constructor,以便向读者传达更多意义:

/** This method is used to send HTTP Get Request
   @constructor
*/
function GetData(path) {
  $.get(path, function (data) {
    return data;
  })
}

函数接收参数,这可以通过在注释中使用@param来表示。以下是同一个函数,它接收某个服务的实际路径作为参数来检索记录:

/** This method is used to send HTTP Get Request 
  @constructor
  @param path – Specify URI of the resource that returns data
*/
function GetData(path) {
  $.get(path, function (data) {
    return data;
  })
}

当你运行你的应用程序时,它将按如下方式显示文档:

添加注释

我们已经看到了使用 JSDoc3 生成文档是多么简单。这不仅有助于理解代码,而且在开发过程中通过提供 intellisense,也有助于开发者。要了解更多关于 JSDoc3 的信息,请参考usejsdoc.org/

部署优化

gulp, gulp-concat, gulp-cssmin, and gulp-uglify. The following is the description of each module:
```
```
```
```
```
```

以下是可以用于压缩 JavaScript 和 CSS 文件的示例gulpfile.js

/// <binding Clean='clean' />
"use strict";

//Adding references of gulp modules
var gulp = require("gulp"),
rimraf = require("rimraf"),
concat = require("gulp-concat"),
cssmin = require("gulp-cssmin"),
uglify = require("gulp-uglify");

//define root path where all JavaScript and CSS files reside
var paths = {
  webroot: "./wwwroot/"
};

/* Path where all the non-minified JavaScript file resides. JS is the folder and ** is used to handle for sub folders */
paths.js = paths.webroot + "js/**/*.js";

/* Path where all the minified JavaScript file resides. JS is the folder and ** is used to handle for sub folders */
paths.minJs = paths.webroot + "js/**/*.min.js";

/* Path where all the non-minified CSS file resides. Css is the main folder and ** is used to handle for sub folder */
paths.css = paths.webroot + "css/**/*.css";

/* Path where all the minified CSS file resides. Css is the main folder and ** is used to handle for sub folder */
paths.minCss = paths.webroot + "css/**/*.min.css";

/* New JavaScript file site.min.js that contains all the compressed and merged JavaScript files*/
paths.concatJsDest = paths.webroot + "js/site.min.js";

/* New CSS file site.min.css that will contain all the compressed and merged CSS files */
paths.concatCssDest = paths.webroot + "css/site.min.css";

//to delete site.min.js file
gulp.task("clean:js", function (cb) {
  rimraf(paths.concatJsDest, cb);
});

//to delete site.min.css file
gulp.task("clean:css", function (cb) {
  rimraf(paths.concatCssDest, cb);
});

/* To merge, compress and place the JavaScript files into one single file site.min.js */
gulp.task("min:js", function () {
  return gulp.src([paths.js, "!" + paths.minJs], { base: "." })
  .pipe(concat(paths.concatJsDest))
  .pipe(uglify())
  .pipe(gulp.dest("."));
});

/* to merge, compress and place the CSS files into one single file site.min.css */
gulp.task("min:css", function () {
  return gulp.src([paths.css, "!" + paths.minCss])
  .pipe(concat(paths.concatCssDest))
  .pipe(cssmin())
  .pipe(gulp.dest("."));
});
`clean:js`: This removes the `site.min.js` file`clean:css`: This removes the `site.min.css` file`min:js`: This merges all the files specified in `paths.js` and `paths.minJs`, minifies them using `uglify()`, and finally creates the `site.main.js` file`min:css`: This merges all the files specified in `paths.css` and `paths.minCss`, minifies them using `cssmin()`, and finally creates the `site.main.css` file

在 Visual Studio 2015 中,你可以使用任务运行器浏览器运行这些任务,并将它们与build事件绑定:

部署优化

以下是你可以为特定build事件关联的选项:

部署优化

前一个屏幕截图显示了将clean:js任务与清理构建事件绑定的步骤。因此,无论何时你清理你的项目,它都会运行clean:js并删除site.min.js文件。

总结

在本章中,我们讨论了几个关于如何结构化基于 JavaScript 的项目并将其划分为模块以提高可扩展性和可维护性的概念。我们还看到了如何有效地使用中介者模式(mediator pattern)来提供模块间的通信。文档也扮演着重要的角色并增加了可维护性,我们使用了 JSDoc3,这是最流行的 JavaScript 文档 API 之一,它帮助开发者参考并理解 JavaScript 的功能。最后,我们讨论了如何通过将 JavaScript 文件压缩和合并成一个最小化的 JavaScript 文件来优化应用程序的加载时间以提高性能。在下一章中,我们将讨论如何测试和调试 JavaScript 应用程序以及可用的工具,以便有效地解决问题。

第十章:测试和调试 JavaScript

在每一个软件生命周期中,测试和调试都扮演着重要的角色。彻底的测试可以使软件无懈可击,而优秀的调试技术不仅可以帮助解决问题,还能帮助准确地识别并修复问题。

测试是创建任何健壮应用程序的核心本质。然而,应用程序为了达到特定的目标,采用了不同的实践和框架,根据应用程序的性质,架构也会有所不同。因此,有时对于开发者来说,测试客户端代码会变得困难,例如,如果一个应用程序在其页面中包含一些 JavaScript 代码,如内联事件处理程序,这会使它与页面紧密耦合。另一方面,即使将 JavaScript 代码模块化,也会带来一些测试套件限制,并使应用程序的测试过程更难以执行。

调试是查找和修复应用程序错误的过程。它是软件开发中最重要的核心技能之一。如果开发者能够熟练掌握调试工具并了解调试的方方面面,他们就可以快速识别根本原因并开始修复错误。调试是任何软件开发生命周期中的基本过程。无论应用程序是复杂的还是简单的,调试都起着重要的作用,以追踪和修正错误。通过设置断点并逐阶段地执行程序流,调试可以帮助开发者中断程序执行并识别程序流程。此外,几乎所有的调试工具都提供其他有用的信息,例如观察程序中正在使用的变量或对象的状态,并在调试生命周期的每个阶段观察它们。

测试 JavaScript 代码

通常,网络应用程序会经历不同类型的测试,例如用户界面UI)测试,通过向表单输入某些内容并验证应用程序的行为来检查 UI 的功能。这种类型的测试主要是手动完成或通过自动化测试工具完成。另一种测试类型是压力测试,主要用于检查应用程序的性能,通过对应用程序施加一些负载来进行。简单地说,它可以是登录应用程序的许多用户或通过自动化例程执行某些操作的示例,以测试应用程序的行为。还有几种其他类型的测试,但确保应用程序功能并验证应用程序是否符合要求的最重要的测试类型是单元测试。在本节中,我们将讨论使用 Jasmine(一个流行的 JavaScript 单元测试框架)对 JavaScript 代码进行单元测试,并使用 Karma 和 Grunt 在 ASP.NET 应用程序中使用 Visual Studio 2015 IDE 执行测试用例。

单元测试

单元测试是一种测试模块中个别单元的方法,包括相关的数据和程序,以验证应用程序的功能符合要求。单元测试由开发者完成,它允许开发者测试应用程序的每个用例,以确保它满足需求并按预期工作。

单元测试的基本优势在于,它将应用程序的每个部分分离成更小的单元,并帮助开发者在开发周期初期集中精力和识别错误。单元测试是任何应用程序承受的第一次测试,它允许测试人员和开发人员在用户验收测试UAT)阶段发布应用程序。

编写单元测试

为了测试 JavaScript 代码,有许多测试套件可供选择。最受欢迎的是 Jasmine,Mocha 和 QUnit。在本章中,我们将使用 Jasmine 与 Karma 和 Grunt 一起使用。

Jasmine

Jasmine 是一个用于测试 JavaScript 代码的行为驱动开发框架。它提供了一些函数,如it()describe()expect()等,以编写 JavaScript 代码的测试脚本。这个框架的基本优势在于它非常容易理解,并帮助用非常简单的代码行编写测试 JavaScript 代码。

例如,考虑以下 JavaScript 代码,它计算作为参数传递的两个数字的和:

(function () {
  var addTwoNumbers = function (x, y) {
    return x+y;
  };

})();

前面函数的测试用例将类似于以下内容:

describe('Calculator', function () {
  it('Results will be 20 for 10 + 10', function () {
    expect(addTwoNumbers(10,10)).toBe(20);
  });
});

Karma

Karma 是一个可以与 Jasmine、Mocha 等其他测试框架集成的 JavaScript 测试运行器。它通过提供一个模拟的测试环境并加载执行测试 JavaScript 代码的浏览器,来执行通过 Jasmine 或其他测试框架定义的测试用例。Karma 配置文件被称为Karma.config.js。一旦执行测试,结果将显示在控制台窗口中。

Grunt

Grunt 相当于 Gulp。它用于执行任务,如 CSS 文件或 JavaScript 文件的压缩,多个 JavaScript 文件的合并和合并等。Grunt 有数百个插件可用于自动化特定任务。与前面章节中使用的 Gulp 不同,我们将使用 Grunt,看看它与 Karma(测试运行器)和 Jasmine(测试套件)一起提供了什么。Grunt 和 Gulp 都是知名的开发任务运行器。在这里使用 Grunt 的原因是为了了解另一个同样知名且受 Visual Studio 2015 支持的 JavaScript 任务运行器,并讨论它提供以使用 Karma 和 Jasmine 进行测试的包。

使用 Jasmine、Karma 和 Grunt 开发单元测试

在本节中,我们将开发一个简单的单元测试,以展示如何在 ASP.NET Core 应用程序中使用 Jasmine、Karma 和 Grunt 框架进行单元测试。首先,从 Visual Studio 2015 创建一个 ASP.NET Core 应用程序。

添加包

打开你 ASP.NET Core 应用程序中的package.json文件,添加如gruntgrunt-karmakarmakarma-phantomjs-launcherkarma-jasminekarma-spec-reporterkarma-cli等包,如下所示:

添加包

以下表格显示了每个包的描述:

包名称 描述
grunt 这配置和运行任务
grunt-karma 这是用于 Karma 测试运行器的 Grunt 插件
karma 这是 JavaScript 的测试运行器
karma-phantomjs-launcher 这是 Karma 插件,用于启动 PhantomJS 浏览器
karma-jasmine 这是 Karma 插件,用于 Jasmine 测试套件
karma-spec-reporter 这是 Karma 插件,用于将测试结果报告到控制台
karma-cli 这是 Karma 命令行界面

添加 Grunt 文件

在你的 ASP.NET 应用程序中添加Gruntfile.js以定义 Grunt 任务。Gruntfile.js是所有任务配置的主文件。在 Visual Studio 的任务运行器浏览器窗口中可以看到配置的任务。

添加 Karma 规格说明

Gruntfile.js文件提供了主要的initConfig()方法,在 Grunt 加载时调用。这是定义 Karma 规格说明的起点。

以下是在initConfig()方法内定义的 Karma 规格说明:

grunt.initConfig({
  karma: {
    unit: {
      options: {
        frameworks: ['jasmine'],
        singleRun: true,
        browsers: ['PhantomJS'],
        files: [
          './wwwroot/js/**/*.js',
          './wwwroot/tests/**/*.test.js'

        ]
      }
    }
  }
});

在前面的脚本中,我们首先指定了一个 Karma 的目标平台。在karma内部,我们将指定用于运行单元测试的单元。在unit内部,我们可以定义一些配置属性,如frameworkssingleRunbrowsersfiles

  • frameworks:这是一个我们要使用的测试框架数组。在这个练习中,我们使用了 Jasmine。然而,也可以使用其他框架,如 Mocha 和 QUnit。

    提示

    请注意,在使用 Karma 中的任何框架时,必须使用Node 包管理器NPM)单独安装该框架的附加插件/库。

  • singleRun:如果这个设置为true,Karma 将开始捕获配置的浏览器并在这些浏览器上执行测试。测试完成后,它会顺利退出。

  • browsers:这是一个用逗号分隔的值定义多个浏览器的数组。在我们的示例中使用了 PhantomJS,它是一个无头浏览器,在后台运行测试。Karma 支持其他浏览器,如 Chrome、Firefox、IE 和 Safari,这些可以通过这个属性进行配置。

  • files: 这里包含所有的测试文件、源文件和依赖。例如,如果我们正在测试脚本中使用 jQuery,或者原始源代码,我们也可以添加这个库的路径。在前面的配置中,我们使用了通配符来加载js文件夹下定义的所有源文件,以及tests文件夹下带有test.js后缀的测试文件。

Karma 配置中还可以使用更多的属性,可以在这里参考:

karma-runner.github.io/0.13/config/configuration-file.html

加载 npm 任务

为了加载 Karma 测试运行工具,我们需要在Gruntfile.js中指定它,在前面的配置之后,如下所示:

grunt.loadNpmTasks('grunt-karma');
注册任务

最后,我们将向注册任务中添加 Grunt 任务。第一个参数是任务名称,它将出现在 Visual Studio 中的任务运行器资源管理器中,第二个参数接受一个数组以执行多个任务:

grunt.registerTask('test', ['karma']);

源 JavaScript 文件

在这个例子中,我们有一个product.js文件,它包含一个saveProduct()方法,该方法将在点击保存按钮的事件上被调用。

将此文件添加到wwwroot/js文件夹路径中:

window.product = window.product || {};

(function () {
  var saveProduct = function () {
    var prodCode = document.getElementById('txtProdCode').value;
    var prodUnitPrice = document.getElementById('txtProdUnitPrice').value;
    var prodExpiry = document.getElementById('txtProdExpiry').value;
    var prodQuantity = document.getElementById('txtProdQuantity').value;
    var totalPrice = prodUnitPrice * prodQuantity;
    document.getElementById('totalAmount').innerHTML = totalPrice;
  };

  window.product.init = function () {
    document.getElementById('save').addEventListener('click', saveProduct);
  };

})();
saveProduct() method that reads the HTML elements and calculates the total price based on the quantity and unit price entered. On the page initialization, we will register the Save button's click event handler that calls the saveProduct() method and calculate the total price.

提示

建议将你的 JavaScript 代码与 HTML 标记分开。

添加单元测试脚本文件

在这里,我们将在wwwroot/tests文件夹下添加另一个 JavaScript 文件,并将其命名为product.test.js。在编写测试时,可以添加*.test.js后缀以使其唯一标识,并将其与源 JavaScript 文件分开。

以下是product.test.js的代码:

describe('Product', function () {

  // inject the HTML fixture for the tests
  beforeEach(function () {
    var fixture = '<div id="fixture">'+
      '<input id="txtProdCode" type="text">' +
      '<input id="txtProdExpiry" type="text">' +
      '<input id="txtProdUnitPrice" type="text">' +
      '<input id="txtProdQuantity" type="text">' +
      '<input id="save" type="button" value="Save">' +
      'Total Amount: <span id="totalAmount" /></div>';

    document.body.insertAdjacentHTML(
      'afterbegin',
      fixture);
  });

  // remove the html fixture from the DOM
  afterEach(function () {
    document.body.removeChild(document.getElementById('fixture'));
  });

  // call the init function of calculator to register DOM elements
  beforeEach(function () {
    window.product.init();
  });

  it('Expected result should be 0 if the Unit price is not valid', function () {
    document.getElementById('txtProdUnitPrice').value = 'a';
    document.getElementById('txtProdQuantity').value = 2;
    document.getElementById('save').click();
    expect(document.getElementById('totalAmount').innerHTML).toBe('0');
  });

  it('Expected result should be 0 if the Product Quantity is not valid', function () {
    document.getElementById('txtProdUnitPrice').value = 30;
    document.getElementById('txtProdQuantity').value = 'zero';
    document.getElementById('save').click();
    expect(document.getElementById('totalAmount').innerHTML).toBe('0');
  });

});

Jasmine 框架提供了一些特定的关键字来定义在特定条件下运行的特定块,如下所示:

  • describe():这是一个全局 Jasmine 函数,包含两个参数:字符串和函数。字符串是要测试的功能名称。函数包含实际实现 Jasmine 套件的代码,并包含单元测试的逻辑。

  • it():在这里,通过调用全局 Jasmine 函数it()定义规格。这也需要字符串和函数,其中它包含实际的单元测试名称和函数块包含实际的代码逻辑以及预期结果。

  • expect():可以使用expect()函数指定it()函数内定义的某些值的预期结果。这还与一个匹配函数(如toBe()not.toBe())相链式调用,以匹配或取消匹配预期值。

在.NET 中,它等效于准备行动断言模式。在这里,准备用于初始化对象并设置传递给测试方法的数据的值。行动模式实际调用测试方法,断言验证测试方法如预期行为。

运行测试任务

运行这些任务很简单,它可以通过 Visual Studio 2015 中的任务运行器窗口运行。以下是显示Gruntfile.js中定义的任务的任务运行器窗口截图:

运行测试任务

当我们运行测试任务时,它会显示类似以下输出:

运行测试任务

在我们的product.test.js测试脚本中,有两个任务。一个是检查传递字符串值到两个元素中的一个(如txtProdUnitPricetxtProdQuantity)是否会返回0。由于我们的product.js文件没有处理这个条件,它会给出一个错误。

为了解决这个问题,我们将修改我们的product.js,并添加这两行以处理此逻辑,检查值是否为数字:

prodUnitPrice = isNaN(prodUnitPrice) ? 0 : prodUnitPrice;
prodQuantity = isNaN(prodQuantity) ? 0 : prodQuantity;

现在,当我们再次运行我们的测试时,我们将得到以下输出:

运行测试任务

在前一个示例中,我们在product.test.js文件的beforeEach()函数内定义了 HTML 标记。对于简单的应用程序,重新定义 HTML 标记作为测试用例并使用它们来执行测试可能不是一个繁琐的过程。然而,大多数 Web 应用程序都使用一些客户端框架,如 Knockout、AngularJS 等,这些框架将 HTML 视图中的控件绑定到 ViewModel,这个 ViewModel 负责读取或写入控件值。

在以下示例中,我们将使用实现 Model-View-ViewModel 模式的 Knockout JavaScript 库,并了解如何以这种方式编写单元测试。

使用 Knockout 实现模型-视图-视图模型并运行测试

模型-视图-视图模型MVVM)是构建用户界面的设计模式。它分为三部分,如下面的图所示:

使用 Knockout 和运行测试实现 Model-View-ViewModel

这三个部分如下所述:

  • 模型:这包含调用后端服务并通过与持久存储通信来保存或检索数据的后台逻辑。

  • 视图模型:这包含视图特定的操作和数据。它表示与视图元素绑定的视图模型。例如,包含一些 HTML 元素的表单将有一个 ViewModel,这是一个包含一些要与这些控件绑定数据的属性的对象。

  • 视图:这是用户与之交互的用户界面。它显示来自 ViewModel 的信息,在 ViewModel 上引发事件,并在 ViewModel 更改时更新它。

让我们使用Knockout JavaScript 库按照以下步骤实现 MVVM 模式。

添加 Knockout 包

首先,让我们通过bower.json在你的 ASP.NET Core 应用程序中添加 Knockout.js。可以通过在bower.json文件的依赖项部分添加条目来实现,Visual Studio 会自动下载包并将其放置在wwwroot/lib/knockout文件夹中。

以下语句可以在bower.json文件中添加:

"knockout": "3.4.0",

添加 ProductViewModel

ProductViewModel包含产品代码、单价、数量、到期日和总金额等属性。以下是ProductViewModel.js的代码片段:

var ProductViewModel = function () {

  this.prodCode = ko.observable('');
  this.prodUnitPrice = ko.observable(0);
  this.prodQuantity = ko.observable(0);
  this.prodExpiry = ko.observable('');
  this.prodTotalAmount =0;

  ko.applyBindings(this);

  this.saveProduct=function(){
    var unitPrice = this.prodUnitPrice();
    var quantity = this.prodQuantity();
    var total = unitPrice * quantity;
    this.prodTotalAmount = total;

    //call some service to save product
  }

};
ProductViewModel class that contains a few properties, each property is assigned to ko.observable().

ko基本上是提供一种补充方式的 Knockout 对象,将对象模型与视图链接起来,其中ko.observable()是一个 Knockout 函数,使 Model 属性变得可观察并与视图数据同步。这意味着当 ViewModel 属性值发生变化时,视图也会更新;当控件值被修改时,ViewModel 属性也会更新。

0 in the following statement will set the control value 0 when the control binding is done:
this.prodUnitPrice = ko.observable(0)

ko.applyBindings()实际上激活 Knockout 以执行 Model 属性与 View 元素的绑定。

添加产品视图

Knockout 提供了一种非常合适的方式来将 ViewModel 属性绑定到控件元素上。绑定包括两部分,名称和值,由冒号分隔。为了将 ViewModel 与输入元素绑定,我们可以使用 data-bind 属性,并指定值名称后跟:和 ViewModel 的属性名称。每个控件都有一组特定的属性,可以根据需要进行元素绑定。

例如,以下是如何使用文本名称将span元素绑定到视图模型属性的示例:

Product code is: <span data-bind="text: prodCode"></span>

以下是产品视图的修改版本:

<body>
  <div>
    <label> Product Code: </label>
    <input type="text" data-bind="value: prodCode" />
  </div>
  <div>
    <label> Product Unit Price: </label>
    <input type="text" data-bind="value: prodUnitPrice" />
  </div>
  <div>
    <label> Product Expiry: </label>
    <input type="text" data-bind="value: prodExpiry" />
  </div>
  <div>
    <label> Product Quantity: </label>
    <input type="text" data-bind="value: prodQuantity" />
  </div>
  <div>
    <input id="btnSaveProduct" type="button" value="Save Product" />
  </div>
  <script src="img/knockout.js"></script>
  <script src="img/ProductViewModel.js"></script>
  <script>
    (function () {
      var prod = new ProductViewModel();
      document.getElementById("btnSaveProduct").onclick = function () { prod.saveProduct(); };
    })();
  </script>
</body>

这就是我们在产品视图中配置 Knockout 所需的所有内容。当点击btnSaveProduct按钮时,它会计算总金额并调用产品服务以保存记录。

修改测试配置

以下是之前创建的Gruntfile.js的修改版本。我们在files数组中添加了ProductViewModel.js和 Knockout 依赖项:

/*
This file in the main entry point for defining grunt tasks and using grunt plugins.
*/
module.exports = function (grunt) {
  grunt.initConfig({
    karma: {
      unit: {
        options: {
          frameworks: ['jasmine'],
          singleRun: true,
          browsers: ['PhantomJS'],
          files: [
            './wwwroot/lib/knockout/dist/knockout.js',
            './wwwroot/js/ProductViewModel.js',
            './wwwroot/test/**/product.test.js'
          ]
        }
      }
    }
  });

  grunt.loadNpmTasks('grunt-karma');
  grunt.registerTask('test', ['karma']);
};

修改产品测试脚本

由于我们不直接依赖 HTML 视图,因此可以通过产品视图模型来测试我们的单元测试用例。以下是未定义任何固定装置的 product.test.js 修改版本:

describe('Product', function () {

  it('Expected Total Amount should be 600', function () {
    var product = new ProductViewModel();
    product.prodQuantity(3);
    product.prodUnitPrice(200);
    product.saveProduct();
    expect(product.prodTotalAmount).toBe(600);
  });
});

当运行测试时,将生成以下输出:

修改产品测试脚本

调试 JavaScript

客户端浏览器上运行 JavaScript,几乎所有浏览器,如 Internet Explorer、Microsoft Edge、Chrome 和 Firefox,都提供集成的 JavaScript 调试器和开发者工具窗口。使用 Visual Studio,我们还可以通过将 Internet Explorer 设置为默认浏览器来调试 JavaScript 代码。Chrome 默认不支持,但通过某些步骤可以实现。

2015 年 Visual Studio 中的调试选项

Visual Studio 提供了某些相当不错的功能来调试 JavaScript 和解决错误。在 Visual Studio 中,只有与 Internet Explorer 一起使用时才能调试 JavaScript。通过以调试模式启动应用程序,然后在 JavaScript 代码中放置一些断点来开始调试。当达到断点时,我们可以使用在调试 C# 和 VB.NET 代码时已经熟悉的 Visual Studio 中的所有调试选项,例如单步进入 (F11),单步跳过 (F10),单步退出 (Shift + F11),条件断点,以及观察变量,所有这些选项都适用于 JavaScript 代码。

使用 Internet Explorer 在 Visual Studio 中进行调试

在 Visual Studio 中,可以为特定的网络应用程序项目设置默认浏览器,方法是选择网络浏览器 (Internet Explorer) | Internet Explorer 选项,如下面的屏幕截图所示:

使用 Internet Explorer 在 Visual Studio 中进行调试

使用 Google Chrome 在 Visual Studio 中进行调试

2015 年的 Visual Studio 不提供用于调试 JavaScript 应用程序的默认支持,除了与 Internet Explorer 一起使用的情况。作为 Node.js 的技术基础与 Google Chrome 相同(都基于 V8 引擎),因此没有缺点。

要在 Visual Studio 中使用 Chrome 开始调试,我们必须使用远程调试器参数运行 Google 的 chrome.exe 文件。以下命令会使用远程调试运行 Google Chrome,并且可以从 Visual Studio 指向相同的 Chrome 实例进行附加:

chrome.exe – remote-debugging-port=9222

9222 是 Visual Studio 在附加到其进程时默认连接的端口。

从 Visual Studio 出发,您可以通过按下 Ctrl + Alt + P,或者通过在菜单栏中选择调试 | 附加到进程来附加进程,然后选择 Chrome 实例。

开发者工具

The fourth pane is the Call stack and Breakpoints. Call stack shows the chain of function calls that are executed and it is helpful to understand the code-execution flow. For example, if an A() method calls a B() method, and the B() method calls a C() method, it shows the complete flow of execution from the A() method to the C() method.

断点 选项卡显示脚本中使用的所有断点列表,用户可以通过启用或禁用、删除或添加新事件来管理这些断点:

Microsoft Edge 中的调试选项

只有当F12 开发者工具窗口被打开时,调试才能开始,并且可以通过菜单栏的 | F12 开发者工具窗口选项或按F12键来打开。窗口打开后,你可以在 JavaScript 代码上设置断点并对页面执行特定操作。

以下表格展示了调试工具栏中一些重要的选项:

图标 选项 快捷键 描述
Microsoft Edge 中的调试选项 继续 F5F8 这将释放断点模式,并继续到下一个断点。
Microsoft Edge 中的调试选项 断点 Ctrl + Shift + B 这将在下一条语句处设置断点。
Microsoft Edge 中的调试选项 步进 F11 这将步进到被调用函数或下一条语句。
Microsoft Edge 中的调试选项 单步跳过 F10 这将跳过被调用函数或下一条语句。
Microsoft Edge 中的调试选项 步出 Shift + F11 这将跳出当前函数,进入调用函数。
Microsoft Edge 中的调试选项 在新工作者创建时断点 Ctrl + Shift + W 这将在新 web 工作者创建时设置断点。
Microsoft Edge 中的调试选项 异常控制 Ctrl + Shift + E 这可用于在所有异常或未处理的异常处设置断点。默认情况下,它设置为忽略异常。
Microsoft Edge 中的调试选项 断开调试器 这将断开调试器,不再运行断点。
Microsoft Edge 中的调试选项 只调试我的代码 Ctrl + J 这将忽略调试第三方库。
Microsoft Edge 中的调试选项 美丽打印 Ctrl + Shift + P 这将搜索 JavaScript 块的压缩版本并使其可读。
Microsoft Edge 中的调试选项 单词换行 Alt + W 这将根据内容窗体大小调整句子。

微软 Edge 提供了以下五种断点类型:

  • 标准

  • 条件

  • 跟踪点

  • XHR

  • 事件

标准断点

这些断点可以通过简单地从脚本代码中选择语句来设置:

标准断点

条件断点

这类断点在满足特定条件或变量达到特定状态时会被触发。例如,我们可以在循环内的语句使用这个,当计数器达到 10 的值时中断执行。

可以通过点击现有断点并从上下文菜单选择条件… 来设置:

条件断点

此选项将打开条件断点窗口,条件可以设置如下截图所示:

条件断点

一旦设置了条件,图标将变为条件断点

跟踪点

跟踪点用于在语句通过时在控制台写消息,跟踪点是通过点击以下选项设置的:上下文菜单中点击插入跟踪点

跟踪点

一旦设置了跟踪点,图标将发生变化,如下:

跟踪点

当语句执行时,它将在控制台窗口上打印如下截图中的消息:

跟踪点

事件

微软 Edge 提供了从断点面板注册事件跟踪点和断点的选项。事件可以是鼠标事件、键盘事件或定时器事件。这项功能在大型或复杂的网络应用程序中大量使用,在这些应用程序中,不知道确切的断点位置。在某些情况下,当事件处理程序在多个地方指定时,此功能更有用。例如,如果一个页面包含 5 个按钮控件,我们需要在任何一个按钮引发点击事件时中断执行,我们只需通过断点事件指定鼠标点击事件;每当任何按钮事件被引发时,断点将被执行并聚焦于该语句。

添加事件跟踪点

用户可以使用以下选项添加事件跟踪点:

添加事件断点

以下窗口显示了当鼠标点击时事件跟踪点的注册情况:

添加事件跟踪点

添加事件断点

用户可以使用以下选项添加事件断点:

添加事件断点

以下窗口显示了当鼠标点击时事件断点的注册情况:

添加事件断点

XHR

与事件类似,XHR 事件也可以从浏览器的断点面板中注册。当从 JavaScript 代码中发起任何 Ajax 请求时,这些事件将被触发。用户可以从以下截图中的图标注册 XHR 事件:

XHR

一旦我们点击这个事件,它就会被添加到断点窗口中,如下截图所示:

XHR

调试 TypeScript

在第五章 使用 Angular 2 和 Web API 开发 ASP.NET 应用程序中,我们已经讨论了 TypeScript 以及它如何转换成最终在浏览器上运行的 JavaScript 代码。开发人员用 TypeScript 编写代码,但在浏览器上运行的是生成的 JavaScript 文件。当 TypeScript 文件被转换成一个 JavaScript 文件时,会生成一个映射文件,其扩展名为*.map.js。这个文件包含了有关实际 TypeScript 文件和生成的 JavaScript 文件的信息。不仅如此,生成的 JavaScript 文件还包含了一个关于映射文件的条目,这个条目实际上告诉浏览器通过读取映射文件来加载相应的源 TypeScript 文件。

当 TypeScript 文件被转换成 JavaScript 文件时,每个生成的 JavaScript 文件都包含以下条目:

//# sourceMappingURL=http://localhost:12144/todosapp/apps/createTodo.component.js.map

这可以通过TSConfig.json文件中的sourceMap属性进行配置。如果sourceMap属性为true,它将生成映射文件,并在生成的 JavaScript 文件中创建一个条目。另外,在 ASP.NET Core 应用程序中工作的时候,所有的静态文件都必须放在wwwroot文件夹中。所以,为了调试 TypeScript,所有相应的 TypeScript (.ts) 文件必须移动到wwwroot文件夹下的任何文件夹中,这样就可以通过浏览器访问了。

这里是有调试器窗口,它显示左侧的 TypeScript 文件列表和右上角的图标,可以切换源文件和编译后的 JavaScript 版本:

调试 TypeScript

所有浏览器都支持debugger关键字

我们也可以通过debugger关键字显式地在某个点上中断控制。如果没有设置断点,但是指定了debugger关键字,调试将启用并中断执行。它可以从代码中设置,如下面的屏幕截图所示:

所有浏览器都支持关键字

总结

在本章中,我们讨论了如何测试和调试 JavaScript 应用程序。对于测试 JavaScript 应用程序,我们讨论了可以轻松与 Karma(一个测试运行器)集成的 Jasmine 测试套件,它还可以与 Grunt 一起使用,从 Visual Studio 任务运行器浏览器窗口执行。我们还讨论了 MVVM 模式的基础知识以及如何使用 Knockout JavaScript 库来实现它。然后我们将测试用例修改为与视图模型一起工作。对于调试,我们讨论了使用 Visual Studio 调试 JavaScript 的一些技巧和技术,以及 Microsoft Edge 通过开发者工具窗口提供的内容,以使调试变得容易。最后,我们还学习了有关基本主题的知识,例如 Microsoft Edge 如何启用对 TypeScript 文件的调试以及实现此目的所需的配置。

posted @ 2024-05-23 14:42  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报