Aurelia-全栈-Web-开发实用指南-全-

Aurelia 全栈 Web 开发实用指南(全)

原文:zh.annas-archive.org/md5/064b16cc0926ccc480bf7467d26aa77e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

几年前,发现几个 IT 专业人士只专注于一种技术,如代码语言或平台是很常见的。那些致力于审查所有后端内容的人认为,他们与前端层无关,毕竟一个是用 Java 和 C#开发的,另一个是用 HTML 和 JavaScript 开发的。这种方法并不好。随着时代的变化,我们需要开始理解一个应用程序不仅涉及业务的一个部分,比如物流或销售。你的应用程序应该涉及整个业务,并在二进制世界中以相同的功能、限制和规则来表示它。作为一个仅有的后端开发者,你对数据如何显示给最终用户的理解非常有限。作为一个仅有的前端开发者,你对多个分布式应用程序如何交互和检索屏幕上要解释的数据的了解也非常有限。作为第一个后果,开发时间增加,产品质量不是预期的。现在许多 IT 专业人士正在走出他们的舒适区,探索另一边。全栈开发者不仅是一个了解后端和前端语言的开发者。全栈开发者是一个能够理解和将每个业务需求转化为应用程序功能的专业人士,了解它的一般影响和可能包含的变化。他们的诊断和交付所需的时间将始终比现实更短,因为他们确切地知道当前产品是如何工作的,包括外部依赖和不同的平台。

微服务革命给行业带来了许多变化。如今,仅仅掌握一种代码语言的知识是不够的。NoSQL 数据库改变了关系型范式,严格的面向对象语言现在正在探索函数范式。这些事情迫使你开始理解不同的编程范式、代码语言和框架。但有一种语言从其一开始,唯一做的事情就是增长:JavaScript。

在整本书中,你将了解到主要的 JavaScript 概念,并开始理解这种代码语言是如何工作的以及为什么它如此受到社区的青睐。JavaScript 既可以作为面向对象的语言,也可以作为函数使用。它的灵活性是其最令人惊叹的特性之一,与传统的编程语言相比,其开发速度更快。不仅如此,JavaScript 还拥有自己的运行时平台 NodeJS,它存在于浏览器之外。在这本书中,你会发现 Node 是我们开发 JavaScript 应用程序的主要合作伙伴,它包含外部库和重用代码的简单性简直令人惊叹。

诸如 CSS 预处理器、任务自动化和测试覆盖率等工具也将被解释,并为你提供参与任何开发团队、理解和提出新特性的知识。我们的马战,Aurelia,将在这个阶段出现。你一定听说过流行的前端框架,如 Angular,或者库,如 React。Aurelia 采用了一种非常不同的方法,解决了你可能会遇到的大部分常见问题,当然,也使得你的开发过程变得非常简单。忘记配置框架和你的数据绑定、AJAX 调用、与第三方库集成等担忧,Aurelia 提供了一系列插件,可以在任何情况下以非常简单的方式帮助你,并让你只需专注于业务代码。Aurelia 不仅仅是一个前端框架,它是前端开发的未来。

多年来,JavaScript 仅用于前端层;随着 NodeJS 的出现,这种方法也发生了变化。我们可以在自己的运行时环境中执行 JavaScript 代码的事实,使我们能够开始编写后端功能,以便供前端层使用。ExpressJS 是基于 NodeJS 的后端框架,它将允许我们以非常简单、易于理解的方式,仅用几行代码编写业务数据处理功能。为了完成你对后端层的旅行,我们将向你展示如何将数据存储在最有名的 NoSQL 数据库之一:MongoDB。它的简单性和插入和提取数据的速度之快,简直令人惊叹。

IT 世界变化迅速,你需要及时了解任何变化。你还将学习如何确保、测试和为现实世界中的应用程序做好准备,并使用云平台部署可在全球范围内使用、高度可扩展且安全的项目。你准备好开始你的旅程,成为一名优秀的全栈开发者了吗?

然后,我们欢迎你加入新的 M.E.A.N.方法(MongoDB、ExpressJS、Aurelia、NodeJS)。

本书面向对象

本书非常适合在软件开发方面有/没有实际经验的 IT 专业人士。本书将指导你通过回顾基本编程概念和良好实践,如 TDD 和安全,在 AureliaJS 中创建高度可扩展的应用程序。

在本书结束时,你将成为一名对现代 JavaScript 框架、NoSQL 数据库、Docker 和云技术有深厚知识的全栈程序员。

本书涵盖内容

第一章,介绍 Aurelia,解释了为什么 JavaScript 是一种非常好的代码语言,以及它是如何随时间变化的,常用的语法(ES6),以及关于其他现代框架的简要探索,如 Angular 和 React,以及为什么 Aurelia 是最好的选择。

第二章,美化用户界面,介绍了现代网络开发工具,这些工具现在非常有用且需求量大。你将学习如何使你的样式表更加友好和易于阅读,并创建自动任务来执行处理文件的命令。此外,你还将通过世界上使用最广泛的 CSS 库来了解当前网络设计的趋势。

第三章,测试和调试,专注于如何测试你的 Aurelia 应用程序以避免潜在的 bug 并交付高质量的软件。

第四章,创建组件和模板,指出是时候开始抽象我们的业务组件,创建高度可重用且易于维护的独立部分来构建你的应用程序了。你将学习如何管理应用程序每个部分的的事件和生命周期,从而完全控制它们。

第五章,创建我们的 RESTful API,讲述了如何使用 Node.js 实现示例应用程序的后端。同时,你还将学习如何设计强大的 API。

第六章,将我们的数据存储在 MongoDB 中,教你如何将 Node.js 后端应用程序集成到 MongoDB 中,以存储应用程序的信息。

第七章,Aurelia 的高级功能,展示了关于数据绑定和其他日常工作中非常常见的场景的更多高级功能。

第八章,安全,解释了如何使用名为 Auth0 的常见第三方服务在 AureliaJS 中实现身份验证/授权和单点登录。

第九章,运行端到端测试,是开发生命周期中最重要的部分。现在是时候测试你的所有代码是否工作并满足业务需求,但不是作为孤立的片段。你还将测试所有组件的功能是否能够良好地协同工作。

第十章,部署,描述了如何使用 Docker 和 NGINX 在你的本地服务器上部署 Aurelia 应用程序;同时,它还展示了如何将应用程序部署到 Heroku 和亚马逊网络服务。

为了充分利用这本书

  1. 你至少需要熟悉一种代码语言的基本原则。如果你没有任何专业经验,不要担心,因为这本书将逐步引导你通过每一章,以充分利用它。

  2. 我们建议你开始使用任何基于 Unix 的操作系统(Ubuntu、Debian 和 macOS)。这是因为它更加灵活,在终端上执行任务也更加容易。

  3. 你需要有很大的耐心。没有人天生就有知识,所以如果某个概念或例子一开始不清楚,永远不要放弃。

  4. 您还需要大量的实践。一本书不可能涵盖所有真实世界的场景,所以我们建议您提前三步修改示例,添加额外功能,并进行大量研究。

下载示例代码文件

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

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载与勘误表”。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

一旦文件下载完成,请确保您使用最新版本解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

书籍的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Full-Stack-Web-Development-with-Aurelia。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“根据您选择的 type,源代码将在src/resources/{type}文件夹中生成。”

代码块设置如下:

function sum(numberA, numberB){
    return numberA + numberB
}
sum(4,5) //9
sum(5,2) //7
sum(sum(5,1),9) //15

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

class Example {
    static returnMessage(){
  return 'From static method'
    }
}
let staticMessage = Example.returnMessage() // From static method

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

cd my-app
au run --watch --env prod

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中如下所示。以下是一个示例:“创建一个新项目,需要管理员权限。”

警告或重要提示看起来像这样。

小贴士和技巧看起来像这样。

联系我们

我们欢迎读者的反馈。

一般反馈:请将电子邮件发送至feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过电子邮件联系我们的questions@packtpub.com

勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将不胜感激,如果你能向我们报告这个错误。请访问www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并附上材料的链接。

如果你有兴趣成为作者: 如果你有一个你擅长的主题,并且你对撰写或为书籍做出贡献感兴趣,请访问authors.packtpub.com.

评论

请留下评论。一旦你阅读并使用了这本书,为何不在你购买它的网站上留下评论呢?潜在的读者可以看到并使用你的客观意见来做出购买决定,Packt 的我们能够了解你对我们的产品有何看法,而我们的作者可以查看他们对书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问 packtpub.com.

第一章:介绍 Aurelia

如果您出生于 80 年代或 90 年代,毫无疑问,您是互联网演变的见证者。最初的网页仅由白色屏幕上的黑色文本组成;他们所写的一切都是纯 HTML 格式,实际上是非常静态的。几年后,第一个 CSS 为网络添加了一些颜色,经过一些不成功的尝试后,JavaScript 终于出现了。

从其首次出现以来,JavaScript 在这些年里不断改进并适应构建下一代网页。许多公司,如 Microsoft,参与了这种语言的演变,增加了功能并提高了其知名度。这种新的脚本语言允许开发者提高客户体验和应用性能,在短时间内,开始出现第一个 JavaScript 框架,使 JavaScript 成为网络开发的新的摇滚明星。

所有这些听起来都很棒,但是,它是否一直像我们今天拥有的强类型语言那样出色?嗯,不是的。第一个 JavaScript 版本是由 Brendan Eich 在 1995 年为 Netscape Navigator 创建的,当时命名为 Mocha,然后是 LiveScript,最终命名为 JavaScript。

让我们更深入地了解这种强大语言的特性以及它是如何成为最常用于应用开发的语言的。

在本章中,我们将探讨以下主题:

  • JavaScript 基础

  • ECMAScript 标准

  • 设置我们的环境

  • Aurelia 框架

  • Aurelia 命令行

  • 示例应用的概述

JavaScript 基础

JavaScript 是一种编程语言,用于通过在您的网页浏览器端(通常称为客户端)执行代码来为您的网页添加自定义行为。因此,这使我们能够创建丰富的动态项目,例如游戏,当用户按下某些按钮时执行自定义代码,对我们的网页元素应用动态效果,表单数据验证等等。

作为单一语言的 JavaScript 非常灵活,有一个庞大的开发者社区在编写和解锁额外的功能,大公司正在开发新的库,当然,我们作为有能力的开发者准备获取所有这些功能,使网络变得精彩。

JavaScript 有几个基本特征:

  • 动态类型

  • 面向对象

  • 函数式

  • 原型化

  • 事件处理

动态类型

在大多数脚本语言中,类型与值相关联,而不是与变量本身相关联。这意味着什么?JavaScript 和其他如 Python 等称为 弱类型 的语言不需要指定我们将使用哪种数据类型存储在变量中。JavaScript 有许多方法来确保对象的正确类型,包括 鸭子类型

为什么是鸭子?

嗯,James Whitcomb 对其进行了幽默的推断,解释了关于它的演绎思维——“如果它看起来像鸭子,游泳像鸭子,叫声像鸭子,那么它可能就是一只鸭子”

让我们来看一个例子:

1\.  var age = 26;
2\.  age = "twenty-six";
3\.  age = false;

在前面的代码中,定义的变量接受任何数据类型,因为数据类型将在运行时评估,所以,例如,第 1 行的 age 变量将是一个整数,在第 2 行将变成字符串,最后是布尔值。听起来很复杂吗?别担心,把变量想象成一个没有标签的空瓶子。你可以放任何你想要的东西,饼干、牛奶或盐。你会在瓶子里放什么?根据你的需求,如果你想做早餐,牛奶可能是更好的选择。唯一你必须记住的是,记住这个瓶子里装的是什么!我们不愿意把盐和甜味混淆。

如果我们需要确保值属于某些特定类型,我们可以使用 typeof 操作符来检索给定变量的数据类型。让我们看看它们:

  • typeof "Diego":这将返回 string

  • typeof false:这将返回 boolean

  • typeof "Diego" == boolean:这将返回 false

typeof 操作符非常有用,但请记住,它只给出基本类型(numberstringbooleanobject)。与 Java 中的类似操作符 instanceof 不同,typeof 不会返回对象类型。

面向对象

JavaScript 对象基于关联数组,通过包含原型进行了改进。属性和值可以在运行时更改。创建对象的另一种常见方式是使用 JavaScript 对象表示法JSON)或使用函数。

让我们看看由 JavaScript 代码创建的对象的外观及其 JSON 表示形式:

// Let's create the person object
function Person(first, last, age) {
    this.firstName = first;
    this.lastName = last;
    this.age = age;
}
var diego = new Person("Diego", "Arguelles", 27);

//JSON representation of the same object
{
    firstName: "Diego",
    lastName: "Arguelles",
    age: 27
}

函数式

函数是一个包含自身的对象。它们有属性、方法,并且可以包含内部函数。这是一种在应用程序的多个地方重用功能的方法;你只需要写函数名,而不是所有代码,就像以下示例:

function sum(numberA, numberB){
    return numberA + numberB
}
sum(4,5) //9
sum(5,2) //7
sum(sum(5,1),9) //15

原型化

JavaScript 使用原型而不是类来实现继承。仅使用原型就可以模拟所有面向对象(OOP)的特性:

function Person(first, last, age) {
    this.firstName = first;
    this.lastName = last;
    this.age = age;
}

var diego = new Person('Diego', 'Arguelles', 26)
diego.nationality = 'Peruvian'
console.log(diego) 
// Person {firstName: "Diego", lastName: "Arguelles", age: 26, nationality: "Peruvian"}

Person.prototype.career = 'Engineering'
console.log(diego.career) // Engineering

话虽如此,原型究竟是什么?与对象不同,一个原型没有封闭的结构。在对象中,我们定义标准属性,我们只是有这些属性来工作,因为 JavaScript 并非完全是一种面向对象的语言,我们有添加、删除或根据需要更改原型属性和值的优势。

我们可以在运行时修改原型属性。请注意,即使你可以修改任何原型,你也应该只修改自己的。如果你修改标准原型(例如,数组原型),你将在应用程序中遇到非常奇怪的错误。

事件处理

事件允许你在网页上添加真正的交互。JavaScript 允许你在 HTML 页面上附加事件处理器,并在它们被触发时执行自定义代码。例如,给定的代码将在用户点击网页主体时显示一个警告:

document.querySelector('body').onclick = function() {
    alert('You clicked the page body!!!');
}

ECMAScript 标准

在最初,一些公司,如 Microsoft,试图开发他们自己的 JavaScript 实现,在这种情况下,1996 年为 Internet Explorer 3.0 开发的 JScript。为了定义一个标准,Netscape 将 JavaScript 提交给欧洲计算机制造商协会ECMA),这是一个信息和通信系统的标准化组织。

ECMA-262 的第一版于 1997 年 6 月由 ECMA 全体会议采纳。自那时起,该语言标准的多个版本已经发布。ECMAScript 这个名字是参与语言标准化工作的组织之间的一种折衷,特别是 Netscape 和 Microsoft,他们的争议主导了早期的标准会议。

所以,经过所有这些标准化过程和文件工作,我们到底在用什么呢?ECMAScript、JScript、ActionScript 还是 JavaScript?它们是相同的吗?基本上不是。标准化后,ECMAScript 被定义为主要的语言,而 JavaScript、JScript 和 ActionScript 是这种语言的方言,当然,JavaScript 是最知名和最常用的。

目前,大多数浏览器都支持 ECMAScript 版本 5,该版本于 2011 年发布。为这个版本管理的特性如下所示:

  • 新的数组方法支持

  • 日期管理支持

  • JSON 支持

到目前为止,我们已经看到了纯 ES5 语法,非常冗长,有时与其他功能高度耦合,如果我们计划开发大型应用程序,它可能变得难以维护。

谢天谢地,我们再也不必处理这种语法了。ECMAScript 6ES6)版本带来了许多简化代码开发和理解的改变。

ES 6

这个版本带来了语言语法的重大变化。让我们回顾一下新特性,并与 ES5 语法进行比较。

在 ES5 中,为了在 JavaScript 中创建一个近似的对象表示,我们通常输入如下内容:

function Person(name, age) {
    this.name = name;
    this.age   = age;
}
Person.prototype.sayHi = function() {
    return 'Hi, my name is ' + this.name + ' and i have ' + this.age + ' years old';
}

var Erikson = new Person('Erikson', 26);
Erikson.sayHi(); // 'Hi, my name is Erikson and i have 26 years old'

如果我们想要改进我们的代码,也许我们可以进行一些重构,如下所示:

function Person(name, age) {
    this.name = name;
    this.age   = age;

    this.sayHi = function () {
        return 'Hi, my name is ' + this.name + ' and i have ' + this.age + ' years old';
    }
}

这就是现在在 JavaScript 上如何进行面向对象编程OOP),但对于有 Java 或 PHP 等先前经验的程序员来说,这种语法结果可能有点难以理解,因为他们不是处理真实对象,而是直接处理原型。ES6 引入了一种新的语法来声明对象:

class Person {

    // Contructor define properties for our object representartion
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    // Class method
    sayHi() {
        return 'Hi, my name is ' + this.name + ' and i have ' + this.age + ' years old';
    }
}
var Erikson = new Person('Erikson', 26);
Erikson.sayHi() // Hi , my name is Erikson and I have 26 years old

如你所见,现在的语法更加易读易懂,我们可以从另一个类扩展,就像其他语言,例如 Java:

class Developer extends Person {

    constructor(name, age, role){
        super(name, age)
        this.role = role;
    }
    sayHi(){
        return super.sayHi() + ' and i am a ' + this.role
    }
}
var Erikson = new Person('Erikson', 26, 'Javascript developer');
Erikson.sayHi() // 'Hi, my name is Erikson and i have 26 years old and i am a Javascript developer'

当然,我们也可以使用封装原则来操作我们的对象属性。类似于 Java,我们可以定义修改器方法来获取属性值或将某些值设置到属性中:

class Person {

    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    get checkName() {
        return this.name;
    }
    set giveName(newName) {
        this.name = newName;
    }
}
var Erikson = new Person('Erikson', 26);
Erikson.checkName() // returns Erikson
Erikson.giveName('Hazis')
Erikson.checkName() // returns Hazis

虽然有这些方法,但这并不能避免你仍然可以使用 JavaScript 的原生语法在运行时更改值或添加属性的事实。你仍然能够执行以下操作:

Erikson.name = 'Diego'
Erikson.name // Returns Diego

就像其他语言一样,ES6 允许使用静态修饰符使用静态方法:

class Example {
    static returnMessage(){
        return 'From static method'
    }
}
let staticMessage = Example.returnMessage() // From static method

你注意到什么了吗?在最后一个例子中,我们使用了let而不是var来声明一个变量。ES6 有两种新的变量定义方式:letvar的直接替代,而const将用于声明常量。我们是否还可以使用var而不是新的 ES6 声明语法?是的,但让我们想象一下,你是一位经验丰富的开发者,并且有两位实习生在你的监督下。在你的代码中,你可以定义如下内容:

var PI = 3.1416

当然,我们也不希望这个值因任何原因而改变。由于这仍然是一个var,任何初级开发者都可以直接或间接地(通过方法调用、赋值错误、错误的比较语法等)更改该值,因此我们可能会在我们的应用中遇到错误。为了防止这类情况,const将是一个更准确的变量修饰符。

ES6 仅仅改进了对象的声明语法吗?不。此刻,我们只关注我们的类定义语法,因为它将是所有应用的核心,就像面向对象编程一样。现在,我们将检查其他改进,我们非常确信你会在日常工作中发现这些改进非常有用。

一个非常重要的注意事项:你必须知道,与其他代码语言不同,在 JavaScript 中,你可以定义const MY_ARRAY = [],并且你仍然能够执行MY_ARRAY.push(3)const前缀只会避免覆盖,所以你不能做MY_ARRAY = [1,2]

箭头函数

你需要遍历数组的元素;通常,你会写如下内容:

var data = ['Ronaldo', 'Messi', 'Maradona'];
data.forEach(function (elem) {
    console.log(elem)
});

使用箭头函数,你可以重构你的代码,并编写如下内容:

var data = ['Ronaldo', 'Messi', 'Maradona'];
data.forEach(elem => {
    console.log(elem);
});

箭头(=>)操作符在一行中定义一个函数,使我们的代码更易读和有序。首先,你需要声明输入;箭头会将这些参数发送到由操作符定义的函数体:

// We could transform this
let sum = function(num) {
    return num + num;
};
// Into just this
let sum = (num) => num + num;

字符串插值

你还记得那些需要使用+操作符连接字符串的时候吗?这不再必要了。例如,以下代码使用+操作符连接string1string2

let string1 = "JavaScript";
let string2 = "awesome";
let string3 = string1 + " " + string2

现在,让我们看看插值如何帮助我们编写更简单的代码:

let string1 = "JavaScript";
let string2 = "awesome";
let string3 = `${string1} ${string2}`

解构赋值

我们有一种新的方法来为对象和数组赋值。让我们看看一些例子:

var [a, b] = ["hello", "world"];
console.log(a); // "hello"
console.log(b); // "world"

var obj = { name: "Diego", lastName: "Arguelles" };
var { name, lastName } = obj;
console.log(name); // "Diego"

var foo = function() {
    return ["175", "75"];
};
var [height, weight] = foo();
console.log(height); //175
console.log(weight); //75

设置我们的环境

到目前为止,我们已经准备好开始用 JavaScript 语言编写我们的第一个函数和方法。我们知道如何处理新的 ES6 语法,以及我们如何使用所有这些新特性来改进我们的应用。让我们设置我们的环境并安装 Node.js。

安装 Node.js

要开始使用 NPM,你需要先下载 Node.js。Node 是一个异步事件驱动的 JavaScript 运行时环境。它不是一种新的语言或新的语法;它只是你可以在其中编写 JavaScript 代码的平台,而 Node.js 将使用谷歌的 V8 JavaScript 引擎来执行它。

如果你使用 OS X 或 Windows,最佳安装 Node.js 的方式是使用 Node.js 下载页面上的安装程序之一。

如果你使用 Linux,你可以使用你的包管理器或查看下载页面,以查看是否有与你的系统兼容的更新版本。

要检查你是否安装了之前的版本,请运行以下命令:

$ node –v

Node 包管理器

Node 包管理器NPM)是一个完整的工具,旨在帮助开发者共享、维护和重用打包在包中的 JavaScript 代码,以便其他开发者可以在自己的应用程序中重用。NPM 由三个不同的组件组成:

  • NPM 网站

  • NPM 仓库

  • NPM 命令行工具

NPM 网站

这个网站作为用户发现包的主要工具;你会找到类似以下的内容:

图片

这个页面描述了你想要下载的包的所有功能,关于它的简要文档,GitHub 网址以及将其导入项目的说明。

NPM 仓库

它是关于每个包的信息的大型数据库。官方公共 NPM 仓库位于 registry.npmjs.org/。它由 CouchDB 数据库提供支持,其中有一个公共镜像位于 skimdb.npmjs.com/registry

NPM CLI

一个用于与仓库交互的命令行工具,允许开发者发布或下载包。

一旦你将代码下载到你的机器上,NPM 将使检查是否有更新可用以及下载这些更改变得非常容易。超过两个可重用的代码块被称为包。那只是一个包含一个或多个文件的目录,以及一个名为 package.json 的文件,其中包含有关该包的所有元数据。

常见的 NPM 操作

与所有命令行工具一样,了解 NPM 提供的选项非常重要。NPM CLI 是一个强大的工具,将帮助我们完成项目的开发周期。

更新 NPM

第一步已经完成!我们已经在我们的机器上安装了 Node 运行时,可以执行我们的 .js 文件,所以我们需要开始工作的最后一件事就是 NPM。Node 默认安装了 NPM,但 NPM 的更新频率比 Node 更高,因此我们可以通过执行以下命令来检查我们的 NPM CLI 的更新情况:

$ npm install npm@latest -g

安装 NPM 包

NPM 已安装和配置;现在是时候开始工作了。有两种方式可以安装 NPM 包。我们的选择将取决于我们如何使用该包。选项如下:

  • 全局安装:将给定的包作为我们的命令行工具的一部分全局安装

  • 本地安装:将给定的包安装到我们的应用程序上下文中,使其仅在我们自己的应用程序中使用

在这个前提下,输入以下命令来安装一个新的包:

$ npm install <package-name>

此指令将创建一个名为 node_modules 的文件夹,我们将在此文件夹中下载所有需要的包。我们可以确保已下载包,进入文件夹并检查是否存在一个与我们的包名相似的文件夹。运行以下命令以列出项目中安装的所有包:

$ ls node_modules

版本

如果未指定包版本,我们将获取最新版本。要安装特定版本,我们需要在 install 命令中添加以下内容:

npm install <package-name>@<version>

package.json 文件

我们知道如何下载一个包并将其导入到我们的项目中。然而,我们通常需要不止一个包,并且需要特定的版本。我们是否需要记住它们以便每次设置项目时都手动下载?不,现在是创建 package.json 文件的时候了。

此文件不仅用于映射我们的依赖项;它必须包含我们项目的所有元数据,并作为快速文档,说明项目依赖于哪些包。至少,package.json 应包含以下内容:

  • 名称:项目名称,全部小写,没有空格(如果需要,可以使用下划线)

  • 版本:以 x.x.x 的形式

我们可以手动创建此文件,但 NPM 允许我们通过执行以下命令自动创建:

$ npm init

上述命令将提示您一系列问题,这些问题将出现在您的 package.json 文件中。如果您不想在未提示任何问题的情况下接受默认值,请运行相同的命令,并在末尾添加一个 --yes 标志:

$ npm init --yes

然后,您将得到一个如下的 package.json 文件:

{
  "name": "my_package",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/package_owner/my_package.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/package_owner/my_package/issues"
  },
  "homepage": "https://github.com/package_owner/my_package"
}

依赖项和 devDependencies

您现在已安装了所有依赖项。您开始工作,在开发过程中,您可能需要其他依赖项来改进您的代码。您只需运行 NPM CLI 来获取新的依赖项,但这不会出现在您的 package.json 文件中!这可能会非常危险,因为如果您没有项目所需的库或依赖项的列表,当您想在不同的机器上运行它时,您的代码将失败,因为依赖项未安装在该机器上。

我们可以通过添加 –-save–-save-dev 标志来确保新包名将被添加到我们的依赖项列表中。第一个将包名添加到 package.json 文件的依赖项部分。这意味着依赖项是应用程序本身的强制性依赖项,应在运行或部署应用程序之前安装。另一方面,我们有 devDependencies 部分,其中将只包含用于我们开发过程的依赖项:

$ npm install <package_name> --save

现在,我们已经准备好开始开发 JavaScript 应用程序了。在下一节中,您将使用 NPM 安装创建新 Aurelia 项目所需的 Aurelia 命令行工具,但让我们继续探索 Aurelia 框架。

Aurelia 框架

在我们开始使用 Aurelia 并学习这个令人惊叹的框架之前,了解为什么你应该选择 Aurelia 而不是其他流行的框架是很重要的。为此,让我们详细探讨一下什么是 JavaScript 框架以及 Aurelia 中存在的关键区别性因素。

什么是 JavaScript 框架?

在上一节中,我们回顾了所有关于 JavaScript 的疑虑以及我们如何使用 NPM 和 Yarn 来组织我们的包。现在,是时候回顾一些将提高我们的开发体验的工具了;是时候讨论框架了。

框架可以被描述为一组工具和方法,这些工具和方法被组织起来以解决项目开发中的常见问题。这些解决方案是通用的;每个解决方案都在不同的环境中进行了测试,并允许你重用这些功能以节省时间和成本。

因此,根据前面的解释,我们可以将 JavaScript 框架定义为一组组件和库(在大多数情况下,相互依赖)的集合,以满足浏览器客户端应用程序的需求。这些需求是什么?让我们检查一些最通用的需求:

  • 路由

  • 数据发送功能和检索(XMLHttpRequest)

  • 正确管理 DOM

  • 在分离的功能中管理和组织你的代码

  • 定义应用程序的标准数据流

  • 定义某些功能的生命周期

为什么使用 JavaScript 框架?

通常,JavaScript 框架将帮助我们完成以下工作:

  • 组织你的代码

  • 以可维护和有序的方式构建结构

  • 实现关注点的分离

  • 实现针对最常见问题的测试解决方案

  • 在任何开发者都可以遵循的基础结构上工作

更具体地说,JavaScript 框架特别有助于那些大部分业务逻辑将在客户端执行的应用程序——路由、模板化、首次模型验证、表格构建和分页——几乎是你过去可能用于服务器的任何东西,但现在没有额外的 HTTP 调用所造成的延迟和开销。

JavaScript 框架比较

一个问题通常有不止一个解决方案,JavaScript 开发者都知道这一点。在 2010 年之前,开发者们在日常工作中实现功能的选择非常有限。在那个时代最受欢迎的选项是 jQuery,现在仍然被广泛使用。虽然 jQuery 并不是一个坏的选择,但它有一个很大的弱点。例如,如果你的项目增长和你的业务代码变得更加复杂,jQuery 将会非常难以维护,你的关注点将会混合,你可能会陷入最常见的反模式之一——意大利面条式代码

2010 年,谷歌发布了一个最受欢迎的 JavaScript 框架——Angular。与 jQuery 不同,Angular 提供了一套完整的工具和一种新的组织 JavaScript 代码的方式,引入了模块、组件、路由和模板等新概念。在 Angular 之后,出现了许多 JavaScript 框架;其中一些因为赞助公司的支持而变得非常流行,例如 Facebook 的 React.js,一些因为社区的采用而获得了名声,例如 Meteor 和 Vue,还有一些在邻里中真正崭新。

作为创新的本质,Angular 2 项目的其中一位主要工程师开发了一个名为 Aurelia 的新颖框架,该框架在市场上仅仅三年时间,就已经成为了邻里的新摇滚明星。

为什么选择 Aurelia?

在我们当前日常工作的过去几年里,我们参与了各种各样的 JavaScript 框架;最受欢迎的始终是 Angular,但我们了解到流行并不等同于质量。为了理解的目的,我们将检查现在最常用的几个框架,然后与我们的战斗马 Aurelia 进行一些比较。

Angular

该基于组件的框架使用 TypeScript 作为主要(且唯一)的 JavaScript 平台。Angular 是一个为所有单页应用(SPA)目的设计的库的完整超集,非常适合从头开始开发应用程序。你可以链接你的模板和你的TypeScript代码,这样你的HTML就会根据代码中的值更新,并准备好对用户操作做出反应。你需要了解这个框架的三个基本概念——指令、模块和组件。每个概念都涉及另一个,你需要将每个组件注册到一个模块中,使其可用。JavaScript 也有自己的模块系统来管理 JavaScript 对象的集合。它与 Angular 的模块系统完全不同且无关。Angular 有自己定义服务类、路由、双向数据绑定、HTTP 请求等的实现,这使得这个框架非常重量级。

技术信息

  • 大小:698 Kb

  • 标准兼容性:ES 2016(TypeScript)

  • 不兼容:NG2 标记和 Dart

  • 互操作性:平均

依赖注入

依赖注入框架需要更多的配置。与 Aurelia 相比,Angular 要求你指定 HTML 选择器模板,增加了文件复杂性。Aurelia 将根据名称策略检测模板:

@Injectable()
class Ticket { /* */ }

@Component({
  selector: 'ticket',
  providers: [Ticket],
  template: `...`
}) //Configuration code mixed with business class
export class Sale {
    constructor(private ticket: Ticket) {}

    public activate() {
        // do something...
        this.ticket.toast("Sale processed!");
    }
}

组件封装

Angular 组件需要更明确的配置,并且在模板中有些(在某些情况下令人困惑)字符。你可以将模板放在单独的文件中,或者对于更简单的组件,你可以将模板内联包含:

/* product-list.component.ts */
@Component({
    selector: 'product-list',
    template: `<div><product-detail *ngFor="let thing of things" [product]="product" /></div>`
})
export class ProductList {
    public products: Product[];
}

React.js

与 Angular 不同,React.js 是一个可以与任何 JavaScript 项目集成的库。它用于处理 Web 应用的视图层并构建可重用的 UI 组件。React 也是基于组件的,但它将 HTML 代码混合在 JavaScript 文件中,以 JSX 格式。JSX 是一种与 XML 格式非常相似的 React 语法,因为你可以管理你的视图层并添加一些行为,通过定义一些属性作为组件的状态或属性。听起来有点复杂?是的,你需要学习 JSX 是如何工作的,并阅读一些关于这个工具的新概念。

React.js 有一个很棒的功能——服务器端渲染。这意味着什么?常见的 JavaScript 框架让渲染工作在客户端进行,所以浏览器需要解释你的 JavaScript 文件并将其转换为纯 HTML 文件。这取决于页面上将显示多少数据,可能需要一些时间。使用 React.js,你可以配置你的服务器在服务器端处理所有这些页面,所以浏览器只需要调用正确的 HTML 文件,当然,加载时间会更短。

与 Angular 类似,React.js 为你提供了一套完整的库来实现动态路由、数据绑定、HTTP 请求以及其他 React 实现库,如 Inferno.js,具有更强大和优化的渲染算法。

一个非常重要的注意事项!Aurelia 现在有一个自己的服务器端渲染插件。你可以在那里找到更多信息:aurelia.io/docs/ssr/introduction/

技术信息

  • 大小:156 KB 或带插件 167 KB

  • 标准合规性:ES 2015

  • 不合规:JSX

  • 互操作性:高摩擦

依赖注入

React.js 中没有这样的依赖注入概念。

组件封装

一个组件就是一个 JS 类。你想要在你的组件中包含另一个组件吗?只需导入它:

import {ProductDetail} from "./ProductDetail";

interface Props {
    products: Product[];
}
export class ProductList extends React.Component<Props, undefined> {
    render() {
        return <div>
            {this.props.products.map(th => <ProductDetail key={th.id} product={th} />)}
        </div>
    }
}

Aurelia

Aurelia 是由 Angular 2 项目团队成员之一创建的新 JavaScript 框架。与 Angular 不同,Aurelia 由一组库组成,这些库通过定义良好的接口协同工作,使其完全模块化。这意味着一个 Web 应用只需要包含它需要的依赖项,而不是完整的包。

Aurelia 的 API 被精心设计,以便自然地被今天和明天最有用的 Web 编程语言所消费。Aurelia 支持 ES5、ES2015、ES2016 和 TypeScript,这些都非常有用,并为你提供了高度的灵活性。

此外,使用 ES6 编写 Web 应用并不是什么新鲜事。实际上,有许多解决方案可以让你使用 ES6 编写 Angular 应用(你需要手动配置它,并且它不包括在默认的 Angular 配置中)。

你无需担心特殊的框架概念或语法;Aurelia 是一个约定优于配置的框架,鼓励你在开发应用程序时使用良好的实践,并且它允许开发者只关注业务代码。

技术信息

  • 大小:最小 252 kb,带标准插件为 302 kb

  • 标准兼容性:HTML、ES 2016、Web Components(包括 Shadow DOM)

  • 互操作性:非常互操作

依赖注入

你所需的一切就是 @autoinject 注解。JS/HTML 映射将由框架自动执行:

class Ticket { /* class code, properties, methods... */ }

@inject
export class Sale {
    constructor( ticket ) {}

    public activate() {
        // do something...
        this.ticket.toast("Sale processed!");
    }
}

对于 Typescript 用户,注解名称非常相似。使用 @autoinject 而不是 @inject,并且不要忘记在构造函数中指定对象的可见性和类型:constructor(private ticket : Ticket)

组件封装

组件封装使用一个看起来或多或少像你曾经使用过的任何其他 Web 模板语言的单独模板文件。按照惯例,如果你的组件类在 hello.ts 中,那么它的模板就在 hello.html 中,你的组件将是 <hello/>

<!-- product-list.html -->
<template>
    <require from="product-detail"/>
    <div>
        <product-detail repeat.for="product of products" product.bind="product"/>
    </div>
</template>
/* producty-list.js */
export class ProductList {
    public products[];
}

每个 JavaScript 框架都有其自己的工作方式,我们可以探索每个框架的更多功能,但 Aurelia 有其独特之处——你无需远离学习框架的工作方式,在极端情况下,以它们自己的方式/语法进行开发。使用 Aurelia,你将感觉像是在编写纯 JavaScript 和 HTML 代码,高度可维护、可扩展,并且只关注你的业务目标。

现在是时候开始使用 Aurelia 了。所以,让我们探索 Aurelia 命令行,开始我们的旅程。继续阅读!

Aurelia 命令行工具

创建 Aurelia 项目有许多方法。对于本书,我们将使用官方的 Aurelia 命令行工具,它由 Aurelia 团队支持。尽管有其他选项可以配置你的 Aurelia 应用程序,例如 Webpack 和 JSPM,但我们认为 CLI 功能强大,将帮助我们节省宝贵的配置应用程序骨架和构建工具的时间。

在本节中,我们将详细探讨 CLI 的功能,并且你会自己信服这确实是我们的冒险的最佳选择。在本节之后,你将成为使用 Aurelia CLI 的专家。

安装

如果你在上一节中已经安装了 Node.js,那么安装 CLI 并不是什么大问题。我们只需要打开你最喜欢的终端,并执行以下命令;如果你使用的是基于 Unix 的操作系统,记得如果你有权限问题,在命令前添加 sudo

npm install -g aurelia-cli

之前的命令将安装 Aurelia CLI 作为全局的可执行命令行工具。这允许我们使用 au 命令,就像使用操作系统的终端中的任何其他命令一样,例如 dir 命令。

安装完成后,执行以下命令:

au help

这应该返回以下输出,显示 CLI 帮助。正如你所见,这个命令有两个主要选项:

图片

现在我们确信它按预期工作,让我们学习如何充分利用它。

创建新应用程序

这是最重要的选项之一。正如其名称所示,它将通过三个步骤创建一个具有良好定义的应用程序文件夹结构和所有初始配置文件的新 Aurelia 应用程序。

执行以下命令,并将 my-app 替换为你的应用程序名称:

au new my-app

当 Aurelia CLI 向导运行时,我们将选择以下选项来创建我们的应用程序:

  1. 选择 1 以选择 ECMAScript 新一代语言

  2. 选择 1 以创建项目

  3. 选择 1 以安装依赖项

一旦你回答了最后一个问题,CLI 将会安装所有依赖,一旦一切完成,你将在你的终端窗口看到以下输出:

图片

运行我们的应用程序

接下来,我们将查看运行选项。此选项允许我们运行应用程序,并提供一个选项,通过指定 --watch 选项来创建一个生产性开发环境,该选项配置了一个监视器来检测源代码中的更改并自动更新浏览器。这个酷炫的功能被称为浏览器同步或自动刷新。

run 命令还允许我们指定我们想要执行应用程序的环境;这些是默认环境:devstageprod。默认情况下,CLI 将使用 dev 环境运行我们的应用程序。使用 --env 标志来更改它。

每个环境究竟意味着什么?在软件开发中,通常当你编写应用程序时,你会在本地开发环境中测试你的代码(dev)。一旦你认为它已经完成,你将其发送到质量保证区域进行测试,这些测试不会在你的机器上执行,因此你需要导出你的应用程序并将其部署到另一台服务器上,这将被称为 test 环境。最后,一旦 QA 人员批准,你的代码将部署到真实世界的环境(prod)。当然,这是一个非常基本的范围,你将在其他公司找到更多环境,如 UAT(用户验收测试)。

例如,让我们进入我们的应用程序(使用 cd 命令)并执行以下命令:

cd my-app
au run --watch --env prod

以下是在两个 URL 中可以看到我们的应用程序正在运行并运行的输出:

图片

在你喜欢的网络浏览器中打开 http://localhost:9000 URL,你应该会看到以下内容:

图片

注意控制台中的最后两行。这些行告诉你应用程序正在哪个端口上运行,这可能会根据你的操作系统和可用的端口而有所不同。

现在,让我们测试一下自动刷新功能,记住这个功能是通过在 au run 命令中添加 --watch 选项来启用的。

打开位于src文件夹中的app.js文件,将'Hello World!'字符串更改为'Hola Mundo!'

export class App {
  constructor() {
    this.message = 'Hola Mundo!';
  }
}

保存它并回到您的浏览器;CLI 将检测您在app.js文件中做出的更改,并将自动刷新您的浏览器。

为了更高效,您可以使用两个显示器——一个用于在浏览器中运行应用程序,另一个用于源代码编辑器。

测试我们的应用程序

当然,测试是所有开发者都需要掌握的重要技能。我们有一个完整的章节来讨论测试,并讨论 TDD、单元测试和端到端测试。

测试命令带有--watch--env标志。使用 watch 选项告诉 CLI 检测test文件夹中的更改并再次执行测试。

为了运行测试,CLI 使用 Karma,这是一个配置为使用 Jasmine 测试框架来编写所有应该保存在test文件夹中的测试文件的测试运行器技术。

例如,前面的命令将运行位于test/unit文件夹中的app.sec.js文件:

au test --watch --env stage

以下是一个测试成功执行后的输出结果:

构建我们的应用程序

现在是部署我们的应用程序的时候了,但在我们这样做之前,我们需要压缩和精简我们的 Aurelia 代码。Aurelia CLI 为我们提供了构建选项,以生成包含所有应用程序代码的、准备就绪的部署文件。

由于您可能想要为不同的环境(devstageprod)构建应用程序,这个构建选项还带有--env标志。例如,在您的项目中执行以下命令:

au build --env prod

以下是一个my-app项目的示例输出:

如输出所示,生成了两个主要文件:app-bundle.js,其中包含我们的应用程序逻辑,以及vendor-bundle.js,其中包含第三方依赖项。这两个文件被生成到我们的根应用程序文件夹中的scripts文件夹。

如果您想运行您的应用程序并检查您最近创建的打包文件是否一切正常,让我们使用npm安装http-server模块。在您的终端中运行以下命令:

npm install -g http-server

现在,在您的应用程序根文件夹中创建一个dist文件夹,并将index.html页面和包含我们的打包文件的scripts文件夹复制进去。

对于最后一步,在终端中进入dist文件夹,并运行以下命令:

cd dist
http-server 

使用cd命令在您的终端中导航到您的文件夹。

此命令将暴露一些四个 URL,其中 Web 服务器正在运行;复制第一个 URL 并在您的网络浏览器中打开它,您应该会看到您的应用程序正在运行:

生成自定义资源

Aurelia,像许多 JavaScript 框架一样,允许你创建可重用的组件,这有助于你避免编写重复的代码,在应用程序的多个部分中重用你的组件,并且可以将它们作为插件导出以在其他项目中重用。

Aurelia 允许你使用以下模板生成可重用的代码片段:

  • 组件

  • 自定义元素

  • 自定义属性

  • 绑定行为

  • 值转换器

这些模板都位于我们的项目根目录中的aurelia_project/generators文件夹中。例如,以下命令生成一个自定义 Aurelia 元素:

au generate element my-reusable-element

根据您选择的类型,源代码将在src/resources/{type}文件夹中生成。

每个类型将在接下来的章节中进行讨论,所以如果你不理解它们之间的区别,请不要感到难过。继续阅读,我的朋友! 😃

世界杯应用程序概述

现在是时候讨论我们将一起构建的应用程序了。当然,除了我们用 Aurelia 编写的 Web 应用程序外,我们还需要一个后端服务来持久化我们的数据。对于后端服务,我们将使用 Node.js 和 Express 框架来构建一个健壮的 API,以及 MongoDB 作为我们的非关系型数据存储。以下图表解释了我们的世界杯项目架构:

图片

这是一个非常简单的架构;Aurelia 应用程序直接与 Node API 通信,Node API 与数据库通信,该数据库是一个 MongoDB 数据库,使用一个非常流行的开源库 Mongoose。这会变得更好;继续阅读!

探索应用程序特性

我们将要开发的应用程序是足球世界杯应用程序。我们将使用一个名为 Materialize 的出色 UI 框架,默认情况下,它将帮助我们创建一个响应式 Web 应用程序,因此我们的用户可以在他们的移动和桌面浏览器中打开这个应用程序,并拥有可适应的用户界面。

尽管这是一个简单的应用程序,但我们将涵盖 Aurelia 在真实生产应用程序中最重要的一些概念。我们将随着书籍的进展来改进这个应用程序。以下是我们将为这个应用程序开发的特性列表:

  • 比赛资源管理器

  • 团队资源管理器

  • 新闻

  • 管理员门户

  • 社交认证

因此,让我们开始探索这个应用程序提供给用户的功能。

比赛资源管理器

这个特性与整个竞赛中的比赛相关。用户将能够执行以下操作:

  • 列出比赛活动

  • 创建新的比赛,需要管理员权限

列出比赛

用户将看到以卡片形式表示的比赛列表。向用户展示一个日历来导航并查看按天安排的比赛。原型如下所示:

图片

创建新的比赛

要创建新的比赛,需要一个管理员账户。一旦用户认证成功,他们可以通过选择队伍和时间来安排新的比赛。原型如下所示:

图片

团队资源管理器

这个功能与整个比赛中的比赛相关。用户将能够执行以下操作:

  • 列出团队

  • 创建一个新的团队,这需要管理员权限

列出团队

用户将看到以卡片形式表示的团队列表。原型如下:

图片

创建一个新的团队

要创建一个新的团队,需要一个管理员账户。一旦用户验证通过,他们就可以创建一个新的团队。原型如下:

图片

新闻

这个功能与新闻相关。用户将能够执行以下操作:

  • 列出新闻

  • 创建一个新的项目,这需要管理员权限

列出新闻

用户将看到以卡片形式表示的新闻列表。原型如下:

图片

创建一个新的

要创建一个新的团队,需要一个管理员账户。一旦用户验证通过,他们就可以创建一个新的团队。原型如下:

图片

社交认证

用户将能够使用他们的 Google 或 Facebook 账户登录。原型如下:

图片

既然我们已经对我们的开发应用程序有了想法,让我们继续创建初始应用程序项目。

创建我们的应用程序

让我们开始创建我们的应用程序。如果您还记得我们关于 Aurelia CLI 的最后一节,我们需要再次使用它来创建一个新的应用程序,所以打开您最喜欢的终端工具并执行以下命令:

au new worldcup-app

在终端中输入以下输入:

  1. 选择3以定义此项目的自定义选项

  2. 第一个选项:您想使用哪个模块加载器/打包器? RequireJS(默认)

  3. 第二个选项:您想使用哪个转译器? : Babel(默认)

  4. 第三个选项:您想如何设置模板? : 默认(无标记处理。)

  5. 第四个选项:您想使用什么 CSS 处理器?在这种情况下,我们将选择Sass3

  6. 第五个选项:您想配置单元测试吗?当然,我们将标记为是(默认)

  7. 第六个选项:您的默认代码编辑器是什么?我们使用 WebStorm,但您可以选择您最熟悉的。

现在,您将在控制台上看到您应用程序的主要结构:

Project Configuration
    Name: worldcup-app
    Platform: Web
    Bundler: Aurelia-CLI
    Loader: RequireJS
    Transpiler: Babel
    Markup Processor: None
    CSS Processor: Sass
    Unit Test Runner: Karma
    Editor: WebStorm
  1. 最后,选择1以创建项目并安装项目依赖项

这是一个自定义设置。我们的项目将由以下功能组成:

  • RequireJS:广为人知的文件和模块加载器,具有良好的浏览器支持。另一个选项可以是 SystemJS 和 Webpack。

  • Babel:Babel 是目前最常用的转译工具之一。转译器是一种将 JavaScript ES6 语法或更高版本的代码转换为 ES5 代码的工具。为什么?因为大多数浏览器还没有很好地支持最新的 JavaScript 版本。

  • 标记处理:它加载我们的模块并创建最终文件,这些文件将被浏览器解释。在这个阶段,我们不会使用自定义的标记处理。

  • SASS:一个很好的预处理器 CSS 库,我们将在下一章中更详细地回顾它。

  • Karma:一个 JavaScript 测试库。我们将在第三章 测试和调试 中更详细地讨论它。

  • WebStorm:一个非常适合 JavaScript 开发的 IDE。虽然它不是免费的,但如果你有来自你大学或机构的官方教育邮箱,你可以获得一年的学生许可证。

一切准备就绪后,用你最喜欢的编辑器打开 worldcup-app 文件夹。

我们知道 Webpack 是一个非常出色的模块加载器,但出于学习目的,我们更愿意在整个这本书中使用 RequireJS,因为它更简单,并让我们更好地解释如何手动配置我们将在这本书中使用的每个工具和库。

项目结构

Aurelia CLI 将生成带有其基本结构的源代码,其中所有配置都已就绪,可以开始编写应用程序的源代码。

以下截图显示了根应用程序文件夹:

图片

让我们开始讨论 aurelia_project 文件夹,它包含主要的 aurelia.json 配置文件,其中包含所有关于依赖项、错误处理、构建目标、加载器、测试运行时工具(如 Karma)、测试框架等设置。你将经常修改此文件以指定应用程序需要使用的新依赖项。

aurelia_folder 中的下一个元素是 environments 文件夹,其中包含三个文件:dev.jsonstage.jsonprod.json。这些文件包含的值取决于你正在运行的环境。你还记得运行选项中的 --env 标志吗?CLI 将使用这些文件之一来配置我们应用程序的环境值。

剩下的两个文件夹是 generatorstasks。它们分别用于生成 Aurelia 自定义可重用组件和声明 gulp 任务。

scripts 文件夹包含执行 au build 命令后生成的捆绑包。

如你所猜,src 文件夹包含我们的应用程序源代码,接着是 test 文件夹,其中包含测试我们项目的源代码。

引导过程

就像许多 JavaScript 框架(如 Angular 和 React)一样,Aurelia 需要 index.html 页面中的一个位置来挂载应用程序。这个位置被称为入口点。打开 index.html 文件,你应该会看到以下代码类似的内容:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Aurelia</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>

  <body aurelia-app="main">
    <script src="img/vendor-bundle.js" data-main="aurelia-bootstrapper"></script>
  </body>
</html>

Aurelia 需要一个 HTML 元素来加载我们的应用程序。默认情况下,应用程序是在body元素中加载的;我们知道这一点是因为这个元素使用了aurelia-app属性,该属性用于指定包含我们应用程序所有配置的主 JavaScript 脚本文件,正如你所注意到的,默认情况下,Aurelia 被配置为使用main文件。以下main.js文件的内容:

import environment from './environment';

export function configure(aurelia) {
  aurelia.use
    .standardConfiguration()
    .feature('resources');

  if (environment.debug) {
    aurelia.use.developmentLogging();
  }

  if (environment.testing) {
    aurelia.use.plugin('aurelia-testing');
  }

  aurelia.start().then(() => aurelia.setRoot());
}

让我们分析这个文件;第一行从根文件夹中的environment.js文件导入环境变量。当您指定--flag {env}选项时,CLI 会在aurelia_project文件夹中查找{env}.json文件,并将其内容复制到environment.js文件中。

此文件还导出单个configure函数,该函数接收一个参数,即aurelia对象,您可以使用它来覆盖默认配置并在应用程序启动之前添加任何代码。例如,您可以告诉 Aurelia 您希望声明组件为全局(功能),配置国际化以管理不同语言等。

一旦配置了aurelia对象,代码的最后一行将把我们的应用程序渲染到具有aurelia-app属性的根 HTML 元素中,在index.html页面中。默认情况下,它将app.js组件渲染到根元素中。当然,我们可以通过传递您希望渲染的元素作为第一个参数,以及您希望渲染应用程序的 HTML 元素作为第二个参数来覆盖默认值:

 aurelia.start().then(() => aurelia.setRoot('my-component', document.getElementById('my-div')));

我们将在过程中修改这个文件;需要记住的最重要的事情是,这个文件在应用程序渲染之前被处理,除了Aurelia.json文件外,这是第二个最重要的文件。以下图表解释了引导过程:

图片

现在您已经了解了引导过程是如何工作的。让我们了解如何创建可重用的组件。

理解组件

在最后一节中,我们了解到 Aurelia 需要一个组件作为我们整个应用程序的根组件,默认情况下,它是应用程序组件。现在让我们来探索这个组件。

组件由两个文件组成,第一个是用 JavaScript 编写的,包含组件的视图模型,第二个是用 HTML 编写的标记模板。它们必须具有相同的文件名,以帮助视图模型解析其视图模板。组件的视图模型是一个 JavaScript 文件,它导出一个包含组件属性和函数的类。例如,这是app.js组件的内容:

export class App {
  constructor() {
    this.message = 'Hello World!';
  }
}

App类声明了一个构造函数,用于初始化message属性。属性可以在constructor中声明,也可以在它之外定义。考虑以下示例:

export class App {
  message = 'Hello World!';
}

当你声明简单的类,如 Plain Old CLR ObjectsPOCO),现在它实现的功能不仅仅是获取和设置其属性值时,使用外部属性声明风格。

要使用在 app.js 视图模型中定义的属性,我们需要一个具有相同文件名的 HTML 模板。打开 app.html 文件以查看其内容:

<template>
  <h1>${message}</h1>
</template>

首先要注意的是,在视图模型中声明的 message 属性是存在的,但为了绑定值,我们必须使用 ${} 字符串插值运算符。最后,当 Aurelia 在网页中渲染组件时,${message} 声明将被 'Hello World!' 替换。

我们可以通过添加可以从模板中调用的函数来扩展我们的组件。例如,让我们声明 changeMessage() 函数:

export class App {

  constructor() {
    this.message = 'Hello World!';
  }

  changeMessage() {
 this.message = 'World-Cup App';
 }

}

从前面的代码中,你可以看到声明一个函数是多么简单;我们使用与 constructor 声明相同的语法。如果你想使用在 App 类中声明的属性,你必须使用 this 保留字来访问任何属性或函数。

现在是时候调用我们的 changeMessage 函数了。首先,我们将在 app.html 文件中的模板中创建一个按钮,并声明一个触发器到按钮的 click 事件。打开 app.html 文件并应用以下更改:

<template>
  <h1>${message}</h1>
  <button click.trigger="changeMessage()">Change</button>
</template>

这里要注意的第一件事是我们没有使用默认的 HTML onclick 事件;相反,我们使用没有 on 开头的 click 事件。这个约定仅用于 Aurelia 模板引擎。因此,我们说我们想要通过将此函数绑定到 click 事件来调用 changeMessage() 函数,使用 trigger 绑定机制。

通过在终端中执行 au run 命令来启动你的应用程序,并测试一下。当你点击“更改”按钮时,你应该看到消息从 'Hello World!' 更改为 'World-Cup' App'h1 HTML 元素发生了变化,因为我们之前已经将 ${message} 属性声明并绑定到其内容中。

绑定是一个我们在下一章中将更详细地讨论的大概念。所以,继续阅读吧,朋友,这才刚刚开始。

摘要

在本章中,你学习了 Aurelia 与其他流行框架的不同之处;我们将 Aurelia 与 ReactJS 和 Angular 进行了比较。我们看到 Aurelia 更轻量级,性能更好,但最重要的是,Aurelia 基于 ECMAScript 6 标准。因此,与其学习一个框架,不如使用 Aurelia 学习一个国际标准。

此外,我们还安装了 NodeJS 和 NPM;这两项开源技术非常重要,因为 Aurelia 需要它们来设置我们的开发环境并安装我们的依赖项。

我们详细探讨了 Aurelia 命令行工具,深入了解了它的功能,现在你对它已经很熟悉了,并且能够创建、启动、测试和构建你的应用程序。

最后,我们讨论了我们将要构建的示例应用——一个令人惊叹的 FIFA 世界杯单页应用。你也学习了 Aurelia 组件是什么,并了解了它们如何将视图模型和模板分别拆分为两个单独的文件,这两个文件必须使用相同的文件名,分别带有 .js.html 扩展名。

在下一章中,你将学习如何通过在我们的应用中安装和配置 Google Material Design 插件来为我们的应用添加样式和颜色。享受下一章吧!

第二章:用户界面样式化

我们现在有一个正在运行的爱瑞利亚(Aurelia)应用程序,并且了解了 JavaScript 编程的所有基本原理。我们的“Hello World!”消息出现在屏幕上,但你难道不觉得它有点简单和静态吗?在这一章中,我们将探讨如何使用现代工具如 SASS 和 LESS 为我们的应用程序添加样式。我们还将讨论目前使用的一些最重要的样式库,如 Bootstrap、Semantic UI 和 Material Design。最后,利用我们之前的学习,让我们通过配置项目以使用 Aurelia-Materialize 插件,使我们的应用程序看起来酷炫、神奇且吸引人。我们将为此目的使用的一些工具包括:

  • CSS 预处理器:SASS,LESS

  • 任务自动化工具:Gulp

  • CSS 库:Bootstrap,Material Design,Semantic UI

听起来很激动人心吗?我们知道是的,首先我们需要开始谈论 CSS,你之前听说过吗?不用担心,我们将以简明而一致的方式解释它。只是 CSS 并不那么神奇,所以我们将向你介绍一些工具,让你的样式表变得神奇!工具如 SASS 和 LESS 对于这些目的非常有用,但每次我们需要使用它时,都需要运行一些命令,所以我们还将讨论任务自动化工具。不再需要手动重复命令!最后,我们不需要重新发明轮子。我们有 CSS 库,包含不同的设计模板,每个模板都针对不同的概念和目的。最后但同样重要的是,我们将练习使用所有这些神奇的工具来配置我们之前创建的应用程序,使我们的开发过程更加友好和有趣。我们相信你一定会觉得这一章非常有趣和有用,所以让我们开始吧!

谈论 CSS

基本上,CSS 是一种描述某些 HTML 文件(也可以用于 XML)样式的语言结构,定义了它应该如何显示。这种结构允许开发者管理一个或多个网页上的行为;对某些 CSS 元素所做的任何更改都将反映在所有与之链接的 HTML 元素上。

它是如何工作的?

CSS 基于规则。这些规则定义在 .css 文件上,称为 样式表。样式表可以由一个或多个规则组成,应用于一个 HTML 或 XML 文档;规则有两个部分:选择器和声明:

h4 { color : red}

h4 元素是选择器,{ color : red } 是声明。

选择器充当文档和样式之间的链接,指定将受到该声明影响的元素。语句是规则的一部分,说明了效果将是什么。在之前的例子中,选择器 h4 指示所有 h4 元素都将受到声明的影响,该声明指出颜色属性将具有网络值(红色)对于文档或链接到该样式表的文档中的所有 h4 元素。

我们有三种方式将我们的样式表与 HTML 文件链接。

第一种方法是使用<link>元素,在 HTML 文件的<head>部分。我们只需要指定我们的样式表的绝对或相对路径/URL,以便将其导入我们的网页:

<!DOCTYPE html>
<html lang="en">
<head>
 <title>Aurelia is Awesome</title>
 <link rel="stylesheet" type="text/css" href="http://www.w3.org/css/officeFloats.css" />
</head>
<body>
.
.
</body>
</html>

接下来,我们可以使用 HTML 文件的<style>元素,通常也在<head>部分。它将在我们的文件被应用程序调用时加载:

<!DOCTYPE html>
<html lang="en">
<head>
    <style type="text/css">
        body {
 padding-left: 11em;
 font-family: Georgia, "Times New Roman", serif;
 color: red;
 background-color: #d8da3d;
 }
 h1 {
 font-family: Helvetica, Geneva, Arial, sans-serif;
 }
    </style>
</head>
<body>
 <!--Here the styles will be applied-->
</body>
</html>

或者,我们可以直接使用style标志来样式化 HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <!--Nothing here-->
</head>
<body>
    <h1 style="color: blue">Aurelia is awesome!</h1>
</body>
</html>

准备给我们的应用程序添加一些酷炫的样式了吗?这仅仅是开始!

探索 SASS 和 LESS

我们正在复习一些最基础的 CSS 概念,只是为了刷新我们对构成样式表的语法和元素的认知。在现实世界中,一个样式表可能包含超过 20 个属于一个 HTML 页面的类;在极端情况下,这些类可能有一百个或更多。在这些情况下,你可能觉得 CSS 语法非常原始,不是自动解释的,在某些情况下还不完整。在大系统中实现继承很困难,而且随着时间的推移,维护起来也可能变得很困难。

你可以采用不同的方法来编写更好的 CSS 代码,你可以为每个网页定义不同的类,然后在一个单独的 CSS 文件中导入它们,或者你可能可以定义父类并将继承应用于子元素,但在两种情况下,你都需要处理可维护性问题。

正是因为为了编写更好的 CSS 代码,有效地重用代码,并添加一些额外的方法使其更具动态性和对任何开发者都更容易理解,CSS 预处理器成为任何开发者非常常用的工具,提高了他们的生产力,并极大地减少了我们样式表中的代码量。

如我们所预期,每个 CSS 预处理器都有自己的语法,不太不同,也不难学习。它们都支持经典 CSS;额外功能将在我们继续使用目前最常用的两个工具——SASS 和 LESS 时进行解释。

变量

想象一下,你正在为公司编写网页代码;你定义样式表,当然,你有一个字体标准颜色用于所有标题、正文等。你正在编写你的 CSS 类和注释,你需要重复相同的颜色值在多个类定义中。好吧,复制和粘贴整个文件中的相同值并不那么困难。你最终将那个网页展示给用户体验设计师,哦,惊喜!那个红色不需要那么深。你需要更正为新颜色代码。这意味着什么?你需要深入到你的样式表中,手动更改每个颜色值为新值。

就像其他编程语言一样,使用 CSS 预处理器,我们可以定义变量,以便在样式表中重复使用它们,避免重复相同的值,并在我们需要调整或更改该值时节省时间。让我们看一个例子:

SASS 语法:

$my-height: 160px;

div {
  height: $my-height;
}

然后,LESS 语法:

@my-height: 160px;

div {
  font-size: @my-height;
}

它们非常相似,对吧?让我们探索其他功能!

嵌套

在纯 CSS 中嵌套元素是一个糟糕的交易。它们不友好,难以阅读,并让我们写很多重复的代码。使用 CSS 预处理器,你将为任何开发者提供更友好的阅读体验;代码自动解释 CSS 正在做什么以及类是如何继承的。看看这个魔法:

使用 SASS 语法:

$my-link-color: #FF0000;
$my-link-hover: #00FFFF;

ul {
  margin: 0;

  li {
    float: left;
  }

  a {
    color: $my-link-color;

    &:hover {
      color: $my-link-hover;
    }
  }
}

在 LESS 中使用相同的方法:

@my-link-color: #FF0000;
@my-link-hover: #00FF00;

ul {
  margin: 0;

  li {
    float: left;
  }

  a {
    color: @my-link-color;

    &:hover {
      color: @my-link-hover;
    }
  }
}

你需要知道的一件事是,浏览器不会直接解释 SASS 或 LESS 语法。你需要将你的代码转换为正常的 CSS 语法,那么该如何操作呢?在 SASS 的情况下,只需输入以下命令:

$ sass --watch app/sass:public/stylesheets

它们都导出了相同的 CSS 输出:

ul { margin: 0; }
ul li { float: left; }
ul a { color: #999; }
ul a:hover { color: #229ed3; }

如你所见,CSS 预处理器为我们提供了更友好的可读性和快速理解代码正在做什么的能力。

扩展

有时候,你定义了具有共同定义的各种类。使用 @extend 特性,你可以定义一个公共类,并让其他类从它扩展,而不是在每个类中复制相同的代码:

SASS 示例:

.block { margin: 25px 58px; }

p {
  @extend .block;
  border: 3px solid #00FF00;
}

ol {
  @extend .block;
  color: #FF0000;
  text-transform: lowercase;
}

LESS:

.block { margin: 25px 58px; }

p {
  &:extend(.block);
  border: 3px solid #00FF00;
}

ol {
  &:extend(.block);
  color: #FF0000;
  text-transform: lowercase;
}

CSS 输出:

.block, p, ul, ol { margin: 10px 5px; }

p { border: 1px solid #eee; }
ul, ol { color: #333; text-transform: uppercase; }

如果/否则语句

哦,请,这真是一个非常酷的特性!有了这个特性,你将能够根据确定的条件以响应式的方式控制你页面的外观。

SASS 示例:

@if lightness($my-color) > 90% {
  background-color: #FF0000;
}

@else {
  background-color: #00FF00;
}

在 LESS 中,情况并不相同。你需要使用 CSS 守卫:

.mixin (@my-color) when (lightness(@my-color) >80%) {
  background-color: #00FF00;
}
.mixin (@my-color) when (lightness(@my-color) =< 80%) {
  background-color: #FF0000;
}

这些并不是所有预处理器功能,但在这个时候,它们已经足够我们开始我们的 FIFA 世界杯应用程序的开发工作了!

使用 Gulp 自动化任务

在上一节中,我们学习了如何使用 CSS 预处理器以及如何将 SASS/LESS 代码编译成纯 CSS 以供浏览器解释。请注意,每次你进行更改时,你都需要重新编译整个文件,这意味着你需要输入相同的命令并执行相同的工作一次、两次以及多次。是的,这真的很无聊。幸运的是,我们有任务自动化工具。这意味着什么?其他工具会为我们做这些脏活。

理解 Gulp

Gulp 是一个基于 JavaScript 的开源任务运行器,它使用代码覆盖配置的方法来定义其任务。这些可能包括以下内容:

  • 打包和压缩库和样式表

  • 保存文件时刷新浏览器

  • 快速运行单元测试

  • 运行代码分析

  • LESS/SASS 到 CSS 编译

  • 将修改后的文件复制到输出目录

这个工具使用了 Node.js 的流模块;首先,我们需要定义什么是流。它可以被定义为一个允许在一个文件上读取数据并通过管道方法将其传输到另一个地方的工具。Gulp.js 的主要特性是它不会像其他自动化工具那样将文件/文件夹写入硬盘,这是一个很好的特性,因为我们可以配置多个任务,而不会影响我们的计算机性能。

Gulp.js 是如何工作的?

正如我们之前所说的,Gulp 不会在硬盘上写入任何内容。因此,所有操作都是在文件系统级别执行的。它观察预先配置的文件以检查任何更改(读取),在编辑之后,它将重写与另一个文件链接的编译内容,或者执行一些预配置的命令。

图片

安装 Gulp

Gulp 在 Windows、Linux 和 macOS 上可用。在任何这些操作系统上的安装过程都非常相似,唯一的区别是在基于 UNIX 的平台你需要以管理员身份运行安装命令。你需要在你的 PC 上已经安装了 Node 和 NPM。要安装,请输入以下命令:

$ npm install –g gulp

让我们在安装过程完成后等待几分钟,然后验证一切是否正常:

$ gulp –v

CLI Version 3.9.0

就这些!我们现在已经安装了 Gulp 并准备好自动化任务了!

首先,你需要确保你的 Web 项目已经配置好导入npm模块;如果不存在package.json文件,你必须使用npm init命令创建一个。要开始使用 Gulp,只需输入以下命令:

$ npm install --save-dev gulp

这将在你的项目本地安装 gulp 节点模块。记住,--save-dev标志让npm更新其package.json文件中的devDependencies部分,以便仅在开发时解析。

下一步是创建gulpfile。此文件将作为清单来定义我们想要执行的任务。所有这些都应该定义在这个文件中;让我们通过一个示例来了解:

// gulpfile.js
var gulp = require('gulp');

gulp.task('hello-world', function(){
    console.log('hello world');
});

require是一个 Node 函数,用于添加对模块的引用。由于我们正在引用 gulp 模块,因此我们可以使用这种任务自动化方法。

现在,当我们从命令行运行gulp hello-world命令时,任务自动化工具将在gulpfile中搜索与名称匹配的任务并执行它。

Gulp 提供了三个主要任务方法:

  • gulp.task:用于定义一个带有名称、数组依赖项和执行函数的新任务

  • gulp.src:它设置源文件所在的文件夹

  • gulp.dest:它设置构建文件将被放置的目标文件夹

Gulp 可以被配置来执行任何任务,例如图像转换、JavaScript 文件转译、连接和大小写处理。让我们看看一些更高级的示例。

JavaScript 任务

我们将配置一个自动任务,将我们创建的 SCSS 文件转换为 CSS。此代码可以在任何带有 gulp 依赖项预配置的 Web 项目中实现(当然):

$ npm install gulp-sass --save-dev

gulp-sass是 Gulp 的一个用于处理 SASS 文件的定制插件。

在将npm模块导入我们的项目后,让我们在gulpfile中引用它们:

var gulp = require('gulp');
var sass = require('gulp-sass');

然后,我们需要创建一个新的任务。让我们称它为process-styles

// SCSS processing
gulp.task('process-styles', function() {gulp.src('sass/**/*.scss')
        .pipe(sass().on('error', sass.logError))
        .pipe(gulp.dest('./css/'));});

注意,我们使用 pipe() 方法在 .src().dest() 部分之间调用任何额外的插件。代码非常直观,我们只是传递 gulp task 将要查找的文件转换的路由,然后配置一个错误行为,如果发生错误,以及如果一切正常,我们只需指定我们生成的文件的路由。

自动化任务

到目前为止,我们已经将一些任务压缩到一个单独的文件中。所有这些压缩将在我们运行 gulp 命令时执行。我们可以将预配置的任务组合成一个单独的命令。假设我们已经有三个定义好的任务:process-stylesother-tasksome-other-task。现在我们只需将这些任务定义到一个新的任务中:

// run all tasks
gulp.task('run', ['process-styles', 'other-task', 'some-other-task']);

保存并按 Enter 键,在命令行中输入 gulp run 命令来执行所有定义的任务。

那很好,但我们仍然需要手动输入命令。鲁佩尔施蒂尔金,这并不在协议中!别担心,我们为你准备了一个最后的惊喜——gulp.watch()。使用这个方法,你可以监视你的源文件,并在检测到变化时执行一些任务。让我们将 watch 任务配置到我们的 gulpfile 中:

// watch for changes in code
gulp.task('watch', function() {
  // some example tasks
  // detect image changes
  gulp.watch(folder.src + 'img/**/*', ['images-task']);

  // detect html changes
  gulp.watch(folder.src + 'html/**/*', ['html-task']);

  // detect javascript changes
  gulp.watch(folder.src + 'js/**/*', ['js-task']);

  // detect css changes <--- Our created task
  gulp.watch(folder.src + 'scss/**/*', ['process-styles']);
  .
  .
  .
  // And so many task as we need to watch! 

});

最后,而不是手动运行 gulp watch 任务,让我们配置一个默认任务:

gulp.task('default', ['run', 'watch']);

保存后,只需在终端中运行 gulp 命令。你会注意到一个 gulp 监视器总是在检查预配置的文件是否发生了任何变化!当然,它包括所有你的 .scss 文件!现在你可以更改和添加新的样式,并且可以自动在浏览器中看到反映,无需你自己执行任何命令。如果你想终止这个监视过程,只需按 Ctrl + C 来中止监视并返回到命令行。现在你真的有一个强大的任务自动化工具配置好了,并且准备使用!Aurelia CLI 预配置了 gulp task 活动,但了解其背后的工作原理非常重要;此外,如果你认为需要,你还可以修改此配置并添加自定义行为。

探索 CSS 框架

我们准备好开始编写我们的第一个 HTML 元素,并为应用程序添加样式。有一些常见元素可以在多个应用程序中重复使用:表格、网格、输入标签、选择等。我们可以定义自己的,但你应该记住,所有这些元素都需要标准化,并且从头开始定义可能是一项艰巨的任务。今天,我们有许多 HTML 库和模板来开始开发我们的视图层,并添加自定义行为以满足我们的需求。让我们探索最常用的 HTML、JS 和 CSS 库。

Bootstrap

Bootstrap 是最受欢迎和最完整的前端库之一。它由 HTML 模板、预定义的 CSS 类和 JavaScript 文件组成,为每个组件添加更动态的行为。由 Twitter 的 Mark Otto 和 Jacob Thornton 创建,并于 2011 年 8 月作为开源项目发布,Bootstrap 是第一个提供自定义元素和网格系统的库之一,用于设计响应式网页。响应式是什么意思?响应式网页设计是关于创建能够自动调整以在各种设备上看起来都很好的网站,从小型手机到大型桌面。

网格是这个框架最重要的方面。它定义了布局创建的基础。Bootstrap 根据屏幕宽度实现了五个层级或比例。根据您的需要,在超小、小、中、大或超大设备上自定义列的大小。对于从小型设备到大型设备都相同的网格,请使用.col.col-*类。当您需要一个特定大小的列时,指定一个编号类;否则,请随意坚持使用.col。让我们看看一个小屏幕的例子:

在这里,请记住以下几点:

  • .col-sm-1将适合屏幕宽度的 1/12

  • .col-sm-2将适合屏幕宽度的 1/6

  • .col-sm-4将适合屏幕宽度的 1/3

  • .col-sm-6将适合屏幕宽度的 1/2

  • .col-sm-12将适合整个屏幕宽度

同样,Bootstrap 包括大量预配置的类来简化我们 HTML 元素的排列,使我们的 Web 应用看起来有序且吸引人。我们真正喜欢 Bootstrap 的另一件事是其高度的可定制性。我们可以转换所有组件,添加自定义行为或样式,而不会与现有库产生任何冲突。一些元素定制的先进设计可能看起来像这样:

不关心目的,Bootstrap 是为了提供针对任何可能业务需求的高度可定制解决方案而创建的。让我们探索更多库!

材料设计

由谷歌提供支持,材料设计不仅仅是 CSS/JS 库。它是一个基于形状、阴影和转化的完整设计哲学。材料设计基于三个主要原则。

材料是隐喻

将你周围的所有空间视为一个运动系统。你可以触摸它,你可以感受它,并观察当你互动时它如何改变它们的方面。平面表面、纸张和颜色在我们的日常生活中很常见,Material 使用这些属性为最终用户创建一个直观且熟悉的界面,提供一大套动画而不违反物理规则。

粗体、图形和有意为之

令人愉悦的视觉体验,Material Design 不会侵扰或具有侵略性。它通过颜色、比例和空白空间实现基于意义的层次结构,邀请用户与网页界面互动。

动画赋予意义

动画尊重并强化了用户作为主要推动者的地位。主要用户操作是引发运动的转折点,从而改变整个设计。动作和反应规则,有助于集中注意力和保持连续性。

正如我们之前所说,Material 不是一个库,但许多库都是基于 Material 的,而目前最常用的之一是 Materialize。这个库提供了与其他库相同的功能,例如网格系统、预构建组件和自定义行为,不同之处在于所有这些都是在遵循 Material Design 原则的基础上创建的。让我们看看使用 Materialize 开发的网页示例:

正如你所见,Material 接口简单、干净、一目了然。

Semantic UI

邻居中新来的家伙,Semantic 带来了基于模式、手风琴元素、3D 变换、评分等功能的大量独特新特性。

"Semantic 通过创建 UI 的共享词汇来赋予设计师和开发者力量。"

  • Semantic UI 团队

为什么选择 Semantic?因为它提供了听起来非常自然的类名,而不是随机的类名,当然,它还描述了 CSS 类对 HTML 元素所执行的操作。

让我们看看实现与其他前端库的不同;在 Bootstrap 中,CSS 类的名称对人类阅读非常友好:

<div data-role="header">
    <a href="#"
       class="ui-btn-left ui-btn ui-btn-inline ui-mini ui-corner-all ui-btn-icon-left ui-icon-delete">Cancel</a>
    <h1>My App</h1>
    <button class="ui-btn-right ui-btn ui-btn-b ui-btn-inline ui-mini ui-corner-all ui-btn-icon-right ui-icon-check">
        Save
    </button>
</div>

使用 Semantic UI 库,类使用人类语言。这非常友好!编码更像是在写常规文本:

<div class="ui stackable inverted divided equal height stackable grid">

Semantic UI 提供了许多主题,并且它们很容易配置。例如,以下截图显示了使用 Semantic UI 开发的网页:

你可以在 semantic-ui.com 上了解更多关于这个出色库的信息。

所提到的库都是基于移动优先的。这意味着什么?让我们继续探索!

移动优先方法

移动优先是一种相对较新的网页设计方式,它通过始终从小型屏幕设备(如手机或平板电脑)开始,促进响应式设计。当你开始设计时,你必须对产品将在哪里显示有一个清晰的想法。

现在,是时候选择一个前端库来开始创建我们的第一个项目组件了。在我们看来,Material Design 可以给最终用户带来更自然的感觉,在移动设备上看起来也很棒(Android UI 基于 Material)。Bootstrap 也是一个不错的选择,在小屏幕上仍然看起来像普通的网页。对于 Semantic UI 也是如此,但这个库有过渡和动画,我们将会非常怀念。所以,让我们开始使用 Materialize 库配置我们的项目。

使用 Aurelia-Materialize 配置我们的项目

正如我们在上一节中提到的,Material Design 不是一个库。然而,许多库都是基于 Material 理念的,因此我们将选择 Materialize CSS。它们的组件看起来非常自然,最好的部分是 Aurelia 有自己的这个库的实现,称为 Aurelia-Materialize,这将极大地便利我们在开发过程和集成中的工作。

让我们打开我们创建的 FIFA 世界杯应用程序,并将终端设置在根目录;然后我们需要安装一些依赖项。按照以下顺序执行以下命令:

  • $ au install jquery

  • $ au install tslib

  • $ au install materialize-amd

  • $ au install aurelia-materialize-bridge

在我们的 index.html 文件中,让我们包含要导入到我们项目中的 Material Design 图标:

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

接下来,在我们的 main.js 文件中,我们需要配置我们的新插件:

aurelia.use.plugin('aurelia-materialize-bridge', b => b.useAll().preventWavesAttach());

b => b.useAll() 脚本允许我们将所有 Aurelia-materialize 组件加载到我们的项目中。如果您只需要其中的一些,您可以按照以下方式指定每个组件:

.plugin('aurelia-materialize-bridge', bridge => {
    bridge
        .useButton()
        .useCollapsible()
        .useModal()    
        .useTabs()   
        .useWaves().preventWavesAttach();
});

然后,我们需要将生成的 .css 文件添加到 index.html 文件中:

<link rel="stylesheet" href="styles/css/materialize.min.css">

由于我们在上一章中已经安装了 Aurelia CLI,我们正在使用这个功能来获取新的依赖项。请确保您至少有 0.32.0 版本。

您已完成!我们终于准备好开始开发我们的生成、配置和运行中的 Web 应用程序了。

到目前为止,您的应用程序文件夹应包含以下(或类似)的项目结构:

如果您正在使用 Aurelia-CLI 的早期版本(0.33.1),请阅读以下建议:

  • aurelia.json 文件的 vendor-bundle.js 配置的 prepend 部分添加 node_modules/jquery/dist/jquery.js,并从依赖项部分移除 jquery

  • aurelia.json 文件的 vendor-bundle.js 配置的 prepend 部分的末尾添加 node_modules/materialize-amd/dist/js/materialize.amd.js,并从依赖项部分移除 materialize-amd 配置。

摘要

在本章中,我们学习了如何使用 CSS 来样式化我们的 Aurelia 应用程序。我们还探讨了两个最流行的 CSS 预处理器——LESS 和 SASS。我们看到了这些预处理器如何帮助我们使用变量和扩展来开发更强大的样式表。

我们探讨了如何使用 Gulp 自动化任务。Aurelia CLI 使用 Gulp 来执行所有与之相关的任务。

最后,我们探讨了不同的 CSS 框架,并配置了 Google 的 Material Design 框架。我们使用 Material Design 创建了示例应用程序,并利用了最常见的 UI 元素。

在下一章中,我们将看到如何通过在我们的开发过程中采用测试驱动开发(Test-Driven-Development)来在我们的代码中应用测试。继续阅读!

第三章:测试和调试

测试是开发过程中最重要的阶段之一。无论您正在处理哪种类型的项目,如果您想交付高质量的应用程序并让用户满意,测试是关键。

想象一下,如果您的最喜欢的出租车应用程序让您的司机带您去错误的地方,或者您的行程价格比预期的高?这个尴尬的错误无疑会引发一系列灾难性事件,损害应用程序的声誉,更糟糕的是,公司的声誉。

学习如何测试我们的 Aurelia 应用程序以及如何正确应用测试驱动开发TDD)方法将提高您应用程序的质量,避免尴尬的错误,让您的用户更满意,并使您成为一名更好的开发者。因此,在本章中,我们将涵盖以下主题:

  • 测试的好处

  • TDD

  • Aurelia 测试框架

  • 练习测试

  • 调试我们的代码

测试的好处

测试纳入我们的开发流程,在项目的每一层都带来了许多好处。在我们开始编写测试应用程序的代码之前,让我们回顾一下良好的测试为以下方面带来的好处:

  • 开发团队

  • 项目

  • 组织

  • 用户

对于开发团队

如果您在一个有多名成员的团队中工作,您可能经历过这样的时刻:一名成员推送了一些更改,而应用程序没有按预期工作。不仅如此,如果团队没有避免破坏性更改的机制,其他人可能导致的代码冲突可能会降低我们应用程序的质量。

应用测试机制可以帮助我们避免代码中的潜在错误。建议您为应用程序的所有组件编写测试脚本。代码覆盖率百分比是开发团队为应用程序编写测试程度的指标。建议达到 100%的覆盖率;这样,您可以确保每次有人更改代码时,您不会破坏他人的代码。

对于项目

如果您在一个敏捷团队中工作,您可能熟悉两周或一个月的冲刺。当您在敏捷团队中工作时,尽可能自动化是非常重要的,自动化测试阶段将帮助您的团队快速交付成果。

无需人工干预的自动化测试要求您的团队在应用程序的所有阶段编写测试,例如以下这些:

  • 单元测试

  • 集成测试

  • UI 测试

  • 端到端测试

对于 Aurelia 应用程序,您将学习如何编写单元测试脚本和端到端测试。编写测试并自动化它们将提高我们团队的开发速度。

对于组织

保护您所在组织的声誉非常重要。尽可能多地测试我们组织的项目将提高其声誉并使其更加可靠。

有许多情况,大公司因为软件故障而损失了大量资金。如果这些应用程序在发布给用户之前进行了测试,所有这些错误本可以避免。因此,理解良好测试的重要性对于组织的各个方面至关重要。

对于用户来说

没有什么比向用户交付不能满足他们需求的应用程序更糟糕的了,更糟糕的是,应用程序充满了错误,质量普遍较差。仔细思考你向用户交付的内容。你编写的代码越多,你的用户就越满意。

测试驱动开发

这是一种改变开发者测试其应用程序代码方式的发展方法。以前,开发者实现了他们应用程序的所有业务逻辑,一旦一切都编码完成,他们就会编写测试。

编写测试脚本不是一件容易的任务;想象一下编写你应用程序所有业务逻辑所需的时间,以及你必须为所有可能的场景编写测试的事实,这需要你对系统本身的很多知识,而且如果你是唯一维护代码的开发者,复杂性只会增加。然而,幸运的是,有一个更好的方法来测试事物。TDD 由三个简单阶段组成:

图片

总是记住这三个颜色:红色、绿色和蓝色;它们代表 TDD 周期。处于红色阶段意味着你已经写下了你的代码应该做什么,但功能尚未实现,测试将失败。处于蓝色阶段,意味着你已经实现了代码,测试现在通过没有任何问题,但代码可能需要重构。最后,处于绿色阶段意味着代码和测试都已实现并通过,代码格式良好,任何开发者都容易阅读。

让我们通过编写一个简单的示例来详细说明每个步骤,以便更接近地观察 TDD 阶段。

让我们的代码失败

重要的是要知道,我们在这个阶段不会使用任何测试框架。本节的主要目标是了解 TDD 是如何工作的;在本章的后续部分,我们将对测试技术进行全面的介绍。

我们将使用 TDD 创建一个名为sum的附加函数,该函数将返回作为参数传递的两个数字的总和。首先,使用你选择的编辑器,创建一个名为testing.js的新文件,并添加以下代码:

const assert = require('assert');

function add(n1, n2) {
  return 0;
}

首先,我们导入assert模块以使用其equal函数。equal函数期望三个参数:

  • 当前要分析的价值或表达式

  • 预期值

  • 当当前值和预期值不相等时应该抛出的消息

如果断言失败,程序将被终止,你将在你的终端中看到断言失败的原因。

一旦我们导入了我们的assert模块,我们就继续创建我们的add函数。然而,这个函数不执行任何操作;这是因为我们的意图是首先编写测试并使其失败,然后我们将在下一个点中实现逻辑本身。在同一个testing.js文件中,在add函数下方添加以下代码,并添加测试用例:

let result = add(5, 5);
assert.equal( result, 10, "Should be 10");

console.log("Test passed!!");

现在我们有了测试用例,它将比较当前值——add(5, 5)存放在结果变量中——与预期值——10,如果它们不相等,将显示Should be 10错误消息,并且由于程序已经完成,下一个表达式——console.log——将不会执行。

是时候执行我们的测试了。使用你的终端,进入你创建testing.js文件的文件夹,并执行以下命令:

$ node testing.js

如果一切出错,你应该看到以下输出:

assert.js:81
  throw new assert.AssertionError({
  ^
AssertionError: Should be 10
    ...

好吧!现在是我们实现代码并通过测试的时候了。

实现代码

现在,让我们在add函数中实现代码。我们将声明一个result变量到函数中,用于存放传递给它的两个参数之间的加法运算。打开testing.js文件并应用以下更改:


const assert = require('assert');

function add(n1, n2) {

  const result = n1 + n2;

 return result;

}

var result = add(5, 5);
assert.equal(result, 10, "Should be 10");

console.log("Test passed!!");

现在我们已经实现了add函数的逻辑,下次我们运行测试时,我们期望assertion.equal函数不会失败,并且会显示Test passed!!消息。让我们试试看。执行以下命令:

$ node testing.js
 Test passed!!

正如你所见,测试通过了,现在显示Test passed!!消息。通过使用这种先编写测试然后实现的策略,我们可以 100%确信我们的应用程序正在做它应该做的事情。

让我们遵循最后一步来完成我们的 TDD 流程。

重构我们的代码

到目前为止,一切正常;我们已经编写了测试和实现,代码正在做我们期望的事情,看起来似乎没有剩下什么要做的。但是,作为一个好的开发者,我们应该寻找使我们的代码更易于阅读并减少代码行数的方法。此外,重构阶段用于更改一些奇怪的变量名,如果需要,还可以添加注释到我们的代码中。

打开你的testing.js文件,应用以下更改以使其更易于阅读:

var assert = require('assert');

function add(n1, n2) {
  return n1 + n2;
}

assert.equal(add(5, 5), 10, "Should be 10");

console.log("Test passed!!");

第一个更改是在add函数中。由于这是一个简单的加法操作,声明一个result变量是不必要的,因此我们返回加法结果在return函数中,使其更易于阅读。

最后,我们将add(5, 5)调用的结果传递给assert.equal函数,这样更容易知道你正在尝试测试什么。

正如你所见,TDD 易于实现但稍微有点难于采用;我们(作者们 😄)鼓励你在工作中采用并使用它。这将使你的生活更轻松,并帮助你成为一个更好的程序员,知道如何交付高质量的软件。

现在是时候看看 Aurelia 能提供什么了。继续阅读!

Aurelia 测试框架

Aurelia 由其他开源测试技术支持,这些技术共同帮助我们设置一个非常高效的开发环境。了解这些技术的根本原理将给我们一个清晰的思路,了解事情是如何运作的,并将为你提供解决未来可能出现的任何问题的知识。

在我们编写第一个 Aurelia 测试之前,让我们了解一下 JasmineJS 和 KarmaJS,这些是 JavaScript 生态系统为我们提供的最棒的测试框架,并且让我们使用 TDD 部分中实现的示例来了解如何使用 Jasmine 语法和断言辅助函数编写测试脚本。

学习 JasmineJS

如其网站详细说明,Jasmine 是一个用于测试 JavaScript 代码的行为驱动开发框架:它不依赖于任何其他 JavaScript 框架,它不需要 DOM,并且它有一个干净、明显的语法,这样你可以轻松编写测试。Jasmine 是一个易于学习的框架,它自带断言函数,因此我们不需要安装任何额外的断言库,例如 Chai。让我们开始安装它并探索构建块——套件和测试用例。

安装和配置

我们将使用 NPM 工具安装 Jasmine。打开你喜欢的终端并执行以下命令:

$ npm install jasmine --global

上述指令将 Jasmine 作为可执行程序安装,你可以在终端的任何文件夹中调用它。我们通过在npm install命令中使用--global标志来实现这一点。

一旦 Jasmine 安装完成,我们需要创建我们的项目示例骨架来练习使用 Jasmine。为此,我们需要做以下几步:

  1. 创建一个名为practice-jasmine的新文件夹

  2. 通过执行jasmine init命令初始化一个新的 Jasmine 项目

  3. 编写我们的测试脚本

因此,步骤清晰后,打开你的终端并执行以下命令:

$ mkdir practice-jasmine
$ cd practice-jasmine
$ jasmine init

一旦执行了jasmine init命令,它将为我们的项目创建以下文件夹结构:

└───spec 
    └───support
      └───jasmine.json

你可以通过安装tree程序并在practice-jasmine文件夹中执行tree命令来获取上述树状列表。

如你在项目文件夹的根目录中看到的,有一个spec文件夹,我们将在这里保存所有的测试脚本。在spec文件夹中,有一个support文件夹,其中为我们创建了一个jasmine.json文件。此文件包含 Jasmine 将用于查找我们的测试脚本并执行它们的全部配置。使用你选择的编辑器打开jasmine.json文件:

{
  "spec_dir": "spec",
  "spec_files": [
    "**/*[sS]pec.js"
  ],
  "helpers": [
    "helpers/**/*.js"
  ],
  "stopSpecOnExpectationFailure": false,
  "random": true
}

在此文件中需要考虑的重要属性是spec_dir,它指向包含我们的测试脚本的文件夹,以及spec_files,它声明一个正则表达式,告诉 Jasmine,以spec.jsSpec.js结尾的文件必须被视为测试脚本,并且必须被处理。

为了验证一切配置正确,在practice-jasmine根目录中执行以下命令:

$ jasmine 
Started

No specs found
Finished in 0.002 seconds

你应该看到显示消息No specs found,这意味着因为我们还没有编写任何测试,Jasmine 无法处理任何测试用例。此外,你还可以看到 Jasmine 执行你的测试所需的时间。

测试套件

遵循最佳实践,你必须将相似的可分组测试分开。例如,在我们的应用程序中,我们将编写代码来管理足球比赛和球队信息。比赛和球队是不同的领域,因此,我们应该将相关的比赛测试分组在一个套件中,并为球队重复同样的操作。

在我们创建了add函数的示例之后,我们可能正在为计算器应用程序实现这个函数,所以让我们以此为例来理解 Jasmine。在specs文件夹中创建calculator.spec.js文件,并编写以下代码:

describe("Calculator", function() {

   ...

});

我们已经创建了测试套件;为此,我们使用了describe函数,并将套件名称作为第一个参数传递,在这个例子中是Calculator,作为第二个参数,我们传递一个函数,该函数将包含我们的测试用例代码。

测试用例

为了编写我们应用程序的测试,我们需要考虑所有可能场景的测试用例。对于我们的计算器示例,我们将为四个主要算术运算——加法、乘法、减法和除法创建测试用例。测试用例是通过it函数创建的,如下所示:

describe("Calculator", function() {

   it("should return the addition ", function() {
 ...
 });

   it("should return the substraction ", function() {
 ...
 });

   it("should return the multiplication ", function() {
 ...
 })

   it("should return the division ", function() {
 ...
 })

});

在我们继续我们的旅程之前,我们应该有一些可以测试的内容。所以让我们创建一个Calculator对象来测试。在同一个calculator.spec.js文件中,在套件声明之前,添加以下代码,如下所示:

var Calculator = {
 add: function(n1, n2) {
 return n1 + n2;
 },
 substract: function(n1, n2) {
 return n1 - n2;
 },
 multiply: function(n1, n2) {
 return n1 * n2;
 },
 divide: function(n1, n2) {
 return n1 / n2;
 }
}

describe("Calculator", function() {
...

现在我们有了可以测试的内容,让我们了解一下如何在测试文件中编写断言。

期望

我们通过编写期望来测试我们代码的功能,这些期望不会超过我们对代码的正确实现所持有的简单假设。在我们正在工作的文件中,应用以下更改:

...

describe("Calculator", function() {

   it("should return the addition ", function() {
       expect(Calculator.add(1, 2)).toEqual(3);
   });

   it("should return the substraction ", function() {
       expect(Calculator.substract(1, 2)).toEqual(-1);
   });

   it("should return the multiplication ", function() {
       expect(Calculator.multiply(1, 2)).toEqual(2);
   })

    it("should return the division ", function() {
       expect(Calculator.divide(1, 2)).toEqual(0.5);
   })

});

我们已经创建了我们对代码的期望。例如,对于加法测试用例,我们期望add(1, 2)的结果返回3。我们使用辅助函数指定匹配操作,例如toEqual函数,正如其名称所示,如果调用加法函数的结果与期望值相同,则不会抛出任何异常。

现在我们已经完全实现了测试脚本,让我们执行它并看看控制台会显示什么。运行以下命令:

$ jasmine 
Started
....

4 specs, 0 failures
Finished in 0.006 seconds

现在显示的输出是找到了4 specs并成功执行,没有失败。现在是时候看看如果我们强制测试用例失败会发生什么了。替换加法测试用例中的以下期望语句:

...

it("should return the addition ", function() {
       expect(Calculator.add(1, 2)).toEqual(30);
});

...

一旦应用了更改,运行以下命令使我们的测试失败:

$ jasmine 

Started 
F... 

Failures: 
1) Calculator should return the addition 
 Message: 
 Expected 3 to equal 30\. 
 Stack: 
 Error: Expected 3 to equal 30\. 
 ...

4 specs, 1 failure 
Finished in 0.011 seconds 

让我们分析输出。首先要注意的是以下 F... 字符串,这意味着第一个测试失败了,其他 3 个(点)是正确的。你也可以阅读 Expected 3 to equal 30 的消息,了解我们的测试失败的原因,最后是测试摘要,显示找到了 4 个规范 但只发生了 1 个失败

这个测试的输出显示在 F... 字符串的开头失败的测试。这可能是开始或另一个顺序。这是因为 jasmine.json 文件中的随机属性被配置为 true,这意味着随机执行。如果你想要顺序执行你的测试,将随机属性更改为 false。

你已经看到了使用 Jasmine 框架测试我们的代码是多么容易。当然,你必须更多地了解它;我们真的想教你关于 Jasmine 的所有内容,但这超出了本书的范围。我们建议你访问官方网站 jasmine.github.io/

学习 KarmaJS

我们已经探索了 Jasmine,它基本上是一个由酷炫的语法和函数支持的测试框架,帮助我们编写应用程序的测试脚本;为了执行我们的测试,我们必须手动执行它们并等待直到完成,才能看到有多少测试通过,有多少测试失败。

Karma 是一个测试运行器。测试运行器是一个配置为查找我们的应用程序测试脚本、自动执行测试并导出测试结果的工具。因为我们正在使用 Aurelia 创建 Web 组件,我们需要在不同的浏览器中测试我们的应用程序,不同的浏览器有不同的特性,而由于 Web 浏览器在许多方面都不同,我们需要一种方法来测试我们的 Web 应用程序在所有可能的浏览器中,以确保我们的用户不会遇到任何与我们的应用程序相关的问题。

Karma 能够使用任何测试框架来实施测试。因为我们已经学习了 Jasmine,我们将使用它来编写我们的测试脚本,并且我们将使用 Karma 来拾取测试文件,执行它们,并导出结果。以下展示了流程:

我们将在本章后面看到 KarmaJS 的强大功能;只需记住,Aurelia 使用 Karma 作为其测试运行器,用于所有使用 Aurelia CLI 创建的项目,正如第一章 介绍 Aurelia 中讨论的那样。

安装 karma

我们将使用 NPM 安装 Karma 和其他依赖项。让我们开始创建一个名为 practice-karma 的练习文件夹并初始化我们的项目。运行以下命令:

$ mkdir practice-karma
$ cd practice-karma
$ npm init

我们使用 npm init 来创建一个新的模块;这将提示你一系列问题,并创建一个类似于以下的 package.json 文件:

{
  "name": "practice-karma",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

到目前为止,你应该有以下的文件夹结构:

practice-karma/
└── package.json

现在是时候安装 Karma 和我们使用 Karma 和 Jasmine 所需的依赖项了。在 practice-karma 文件夹中,运行以下命令:

$ npm install karma karma-jasmine jasmine-core karma-chrome-launcher --save-dev

前面的命令将安装使用 Karma 和 Jasmine 所需的依赖项,并将依赖项添加到我们的 package.json 文件中的 devDependencies 属性。打开您的 package.json 文件,您应该看到类似以下的内容:

{
  "name": "practice-karma",
  "version": "1.0.0",
  ...
  "devDependencies": {
 "jasmine-core": "².8.0",
 "karma": "¹.7.1",
 "karma-chrome-launcher": "².2.0",
 "karma-jasmine": "¹.1.1"
 }
}

由于我们必须使用 --save-dev 标志,所以依赖项列在 devDependencies 属性中;如果您使用 --save 代替,它将在 dependencies 属性中列出依赖项。

Karma 配置

现在 Karma 已经安装,我们需要配置以下内容:

  • Jasmine 作为测试工具

  • 包含应用代码的文件夹

  • 网络浏览器启动器

  • 报告器

设置所有前面的参数可能是一个非常耗时的工作,因此我们将使用 Karma 可执行文件来自动配置一切。在 practice-karma 文件夹中,运行以下命令:

$ karma init

这将提示您一系列问题;只需按回车键接受所有默认配置,一旦完成所有操作,就会创建一个 karma.conf.js 文件:

module.exports = function(config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    files: [
 ],
    exclude: [
    ],
    preprocessors: {
    },
    reporters: ['progress'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
    concurrency: Infinity
  })
}

我们需要指定用于测试过程中要使用的文件的模式。为此,将以下更改应用到 files: [] 属性:

...
files: [
   'specs/*.spec.js'
],
...

现在我们已经指定了所有以 .spec.js 结尾的文件都将由 karma 处理。让我们创建一个 specs 文件夹,并在其中创建一个 calculator.spec.js 文件:

$ mkdir specs
$ touch calculator.spec.js

您的项目结构应该类似于以下内容:

.
└── node_modules
├── package.json
├── karma.spec.js
└── specs
    └── calculator.spec.js

现在是时候通过创建一个测试示例来测试一下了。

测试示例

对于 Calculator 应用程序示例,让我们在根项目文件夹中创建一个 src 文件夹,并在其中创建 calculator.js 文件:

$ mkdir src
$ touch src/calculator.js

如您所见,我们没有在 specs/ 文件夹中保存 calculator.js 文件,因此我们需要配置 karma 来加载 src/ 文件夹中的文件。在 karma.conf.js 文件中应用以下更改:

...
files: [
      'specs/*.spec.js',
      'src/*.js'
],
...

现在,Karma 将在测试运行时加载 specssrc 文件夹中的所有文件。

让我们实现 calculator.js 文件的代码。使用您选择的编辑器打开 src/calculator.js 文件,并应用以下代码:

window.Calculator = {

  add: function(n1, n2) {
    return n1 + n2;
  },

  multiply: function(n1, n2) {
    return n1 * n2;
  }

}

如果您想使一个变量在全局范围内可访问,只需将其创建为 window 对象的一个属性。在这种情况下,我们使 Calculator 对象 全局化

现在,让我们编写我们的测试用例。打开 specs/calculator.spec.js 文件,并应用以下代码:

describe("Calculator Tests", function() {

  it("should return 10", function() {

    expect(window.Calculator.add(5, 5)).toBe(10);

  });

});

如果您注意到我们正在使用 Jasmine 测试框架来编写测试,那么前面的代码应该对您来说很熟悉。现在我们已经设置好一切并实现了代码,让我们继续通过启动测试来继续操作。

启动测试运行器

现在我们有了测试代码和 Calculator 对象,是时候启动 Karma 来执行我们的测试了。打开命令行,在 practice-karma 根目录下,执行以下命令:

./node_modules/karma/.bin/karma start karma.conf.js

前面的命令将输出大量日志,同时在你的 Chrome 浏览器中打开一个新窗口并执行最近启动的网页中的测试。这个页面是在 Chrome 中打开的,因为它在 karma.conf.js 中进行了配置。考虑以下示例:

$ ./node_modules/karma/bin/karma start karma.conf.js

10 12 2017 12:12:43.049:WARN [karma]: No captured browser, open http://localhost:9876/
10 12 2017 12:12:43.059:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
10 12 2017 12:12:43.060:INFO [launcher]: Launching browser Chrome with unlimited concurrency
10 12 2017 12:12:43.086:INFO [launcher]: Starting browser Chrome
10 12 2017 12:12:44.628:INFO [Chrome 62.0.3202 (Mac OS X 10.12.6)]: Connected on socket 8n0My3AYk-xKfu9sAAAA with id 38155723
Chrome 62.0.3202 (Mac OS X 10.12.6): Executed 1 of 1 SUCCESS (0 secs / 0.002 secChrome 62.0.3202 (Mac OS X 10.12.6): Executed 1 of 1 SUCCESS (0.007 secs / 0.002 secs)

注意到最后几行,我们的测试以成功的结果执行。启动的网页看起来类似于以下:

图片

如你所注意到的,运行前面的命令可能太长而难以记住;为了使我们的生活更简单,让我们配置 package.json 文件以配置 test 脚本来为我们执行此命令。打开 package.json 文件并应用以下更改:

{
  "name": "practice-karma",
  ...
  "scripts": {
    "test": "./node_modules/karma/bin/karma start karma.conf.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
   ...
  }
}

一旦配置完成,执行以下命令以运行测试:

$ npm test

就这样。使用 Karma 和 Jasmine 将为我们提供编写稳健测试所需的所有工具,这两种技术不仅限于 Web 开发。你可以在任何 JavaScript 项目中使用它们,例如用 Node.js 编写的后端。因此,现在是时候看看一个真实的 Aurelia 组件的例子了。继续阅读!

测试 Aurelia 组件

为了看到真实的测试示例,我们将创建一个简单的应用程序。这个应用程序将仅仅问候用户并显示当前正在学习的话题。这两项数据,用户名和话题,将被持久化为可绑定实体,我们将称这个组件为 info-box。我们将开发一个类似于以下模拟的应用程序:

图片

编码应用程序

我们将使用 Aurelia CLI 生成我们的 Aurelia 应用程序。Aurelia 配置项目以与 Karma 测试运行器和 Jasmine 测试框架一起工作,并使用 Chrome 作为默认的 Web 浏览器。

创建应用程序

为了创建我们的应用程序,打开你的终端,在首选的工作目录中运行以下命令并接受默认设置:

$ au new aurelia-testapp

前面的命令将创建一个名为 aurelia-testapp 的新目录;让我们进入这个文件夹,通过运行以下命令启动应用程序:

$ cd aurelia-testapp
$ au run --watch

...
Finished 'writeBundles'
Application Available At: http://localhost:9000
BrowserSync Available At: http://localhost:3001 

前面的命令将在开发服务器启动和 Aurelia 启动时输出大量日志。转到 http://localhost:9000;你应该会看到以下类似的内容:

图片

创建我们的组件

为了创建我们的 info-box 组件,我们将使用 Aurelia CLI。停止运行中的应用程序并执行以下命令。这将要求你输入目标文件夹;按 Enter 使用 src 作为默认值:

$ au generate component info-box

What sub-folder would you like to add it to?
If it doesn't exist it will be created for you.

Default folder is the source folder (src).

[.]>
Created info-box in the 'src' folder

此命令将创建两个文件,这两个文件共同定义了 info-box 组件;这些文件如下所示:

  • info-box.js:包含组件的视图模型

  • info-box.html:包含 HTML 视图模板

让我们实现我们的 info-box 组件。

实现 info-box 视图模型

打开 info-box.js 文件并应用以下更改以声明 usernametopic 属性:

export class InfoBox { 
  constructor() {
    this.username = 'Reader';
 this.topic = 'Testing and Debugging';
  }
}

实现信息框视图 HTML 模板

打开info-box.js文件,并应用以下更改以将属性绑定到 HTML 模板:

<template>
  <h1>Hello, ${username}!</h1>
  <p>You are learning: <b>${topic} </b></p>
</template>

渲染信息框组件

为了加载和渲染我们的组件,我们需要将其导入到应用程序组件中。打开app.html文件,并应用以下更改:

<template>
  <require from="info-box"></require> 
  <info-box></info-box>
</template>

首先,我们导入info-box组件,然后使用<info-box>标签语法使用它。完成更改后,通过运行au run --watch命令再次启动应用程序:

图片

编写测试

在我们开始编写测试之前,让我们清理test文件夹,通过从unit文件夹中删除app.spec.js文件。完成后,你应该有一个类似的文件夹结构:

.
├── aurelia-karma.js
└── unit
    └── setup.js

unit文件夹中创建info-box.spec.js文件,并添加以下代码:

 import {StageComponent} from 'aurelia-testing';
 import {bootstrap} from 'aurelia-bootstrapper';

 describe('Info-Box', () => {

   it('should render the username and topic', () => {

   });
 });

首先,我们从 Aurelia 框架导入两个我们将用于初始化info-box组件的对象。然后,我们声明我们的Info-Box测试套件,并声明一个测试用例。

注意我们使用的是特殊语法;我们使用() => {} ECMAScript 语法,而不是function() {}。为了对组件进行单元测试,我们需要执行以下步骤:

  1. 引入组件

  2. 测试组件

引入组件

我们需要创建组件,以便 Jasmine 可以使用它来应用测试。在info-box.spec.js文件中应用以下更改:

 import {StageComponent} from 'aurelia-testing';
 import {bootstrap} from 'aurelia-bootstrapper';

 describe('Info-Box', () => {

   it('should render the username and topic', done => {

     let component = StageComponent
 .withResources('info-box')
 .inView('<info-box></info-box>');

     component
 .create(bootstrap);

   });

 });

在我们引入组件之前,必须先创建它。我们使用StageComponent对象来实例化一个 Aurelia 组件;通过将组件名称作为withResources函数的参数来指定组件。最后,在inView函数中使用<info-box>元素来指定一个视图。

一旦我们定义了组件的骨架,我们就调用它的create函数并传递bootstrap对象。bootstrap对象包含在第一章“介绍 Aurelia”中讨论的main.js文件中指定的默认配置。

测试组件

现在我们已经创建了组件,我们需要对其进行测试。为此,我们依赖于传递给测试函数的done回调,以通知 Jasmine 测试已完成。如果我们不指定done参数,我们的测试将不会执行,因为我们在一个承诺中执行断言,如果我们不调用它,我们将得到超时错误,因为 Jasmine 将无法知道我们的测试何时完成。让我们对info-box.spec.js文件应用以下更改:

 import {StageComponent} from 'aurelia-testing';
 import {bootstrap} from 'aurelia-bootstrapper';

 describe('Info-Box', () => {

   it('should render the username and topic', done => {
     let component = StageComponent
               .withResources('info-box') 
               .inView('<info-box></info-box>');

     component
     .create(bootstrap)
     .then(() => {

 const h1 = component.element.querySelector('h1').innerHTML;
 const pa = component.element.querySelector('p').innerHTML;

 expect(h1).toBe('Hello, Reader!');
 expect(pa).toBe('You are learning: <b>Testing and Debugging </b>'); 
 done();

 })
 .catch(e => console.log(e.toString()));
   });
 });

你应该知道,如果你使用 Webpack,你可能需要从aurelia-pal模块中导入PLATFORM,以便按如下方式加载info-box资源——.withResourceS(PLATFORM.moduleName('info-box'))。有关 Webpack 的更多考虑,请访问官方网站aurelia.io/docs/build-systems/webpack

then 函数中,我们将使用 component.element.querySelector 函数访问 HTML 元素的回调传递给 info-box 组件,并使用元素的 innerHTML 属性来访问元素的值。

接下来,我们通过预期值比较元素的值,当所有 expect 语句执行完毕后,我们调用 done() 函数来告诉 Jasmine 我们已经完成了这个测试用例。

最后,一个 catch 回调被传递以打印测试过程中检测到的任何错误。一旦一切完成,请在您的终端中运行以下命令:

$ au karma

如果一切顺利,Chrome 网络浏览器应该已经打开,您应该在终端窗口中看到以下输出:

...
Starting 'karma'...
...
Chrome 62.0.3202 (Mac OS X 10.12.6): Executed 1 of 1 SUCCESS (0 secs / 0.105 secChrome 62.0.3202 (Mac OS X 10.12.6): Executed 1 of 1 SUCCESS (0.109 secs / 0.105 secs)
Finished 'karma'

就这些!现在您已经知道如何测试 Aurelia 组件了。一个小挑战是,在 TDD 循环中应用所学的所有内容。祝您玩得开心!

调试我们的代码

调试工具对于开发者来说至关重要。无论您使用的是哪种编程语言或框架,或者您是在前端还是后端项目中工作,调试都将始终存在于您的开发过程中。

现在,网络浏览器不仅仅只是服务器页面、缓存内容、保存收藏夹等等。它们是完整的网络开发工具,提供了调试我们的代码和应用程序性能的强大工具。

让我们看看我们如何使用我们最喜欢的网络浏览器来调试我们的代码。我们将以 Chrome 开发者工具为例。

重构我们的应用程序

首先,我们需要启动我们的应用程序并在网络浏览器中打开它。让我们使用我们的 aurelia-testapp*:

$ cd aurelia-testapp
$ au run --watch

应用程序运行起来后,转到 http://localhost:9000 来查看应用程序。我们将添加一个按钮,当按钮被按下时,我们将调试一些示例代码。打开 info-box.html 文件并应用以下更改:

<template>

  <button click.trigger="debugme()"> Debug Me!</button>

</template>

打开 info-box.js 文件并应用以下更改:

export class InfoBox { 

  debugme() {

 alert("YOU PRESSED THE DEBUGME BUTTON")

 }

}

返回您的浏览器应用程序,点击 Debug Me! 按钮,您应该在屏幕上看到以下内容:

图片

使用 Chrome 开发者工具进行调试

我们需要打开 Chrome 开发者工具。为此,请转到菜单中的视图 | 开发者 | 开发者工具选项,或者按 F12。您应该看到如下内容:

图片

在此窗口打开的情况下,让我们对我们的 info-box 文件进行一些更改,以告诉浏览器我们想要停止并调试我们的代码:

export class InfoBox { 

  debugme() {

    debugger;

    alert("YOU PRESSED THE DEBUGME BUTTON")

  }

}

返回您的应用程序并点击 Debug Me! 按钮。debugger; 指令将停止浏览器执行并进入浏览器调试模式:

图片

您可以使用开发者工具选项来导航您的代码,分析变量值,添加断点等。我个人更喜欢以这种方式调试我的代码。您可以使用 Node.js 命令行或其他类型的调试来尝试调试代码。调试工具将根据您正在工作的浏览器而有所不同。

现在我们已经做好了准备,并且知道如何样式化和测试 Aurelia 应用程序,是时候学习如何创建出色的 Aurelia 组件了。继续阅读!

摘要

在本章中,我们探讨了良好的测试给我们的公司、团队、产品和用户带来的好处。良好的测试将始终使我们的产品更好,让我们的用户更满意。

我们学习了如何将 TDD 应用到我们的软件开发过程中,以及它对我们应用程序质量的重要性和影响。您应该始终记住,TDD 由三个彩色阶段组成:红色阶段,使您的测试失败;蓝色阶段,使您的测试通过;最后是绿色阶段,重构和清理您的代码。

我们还了解了 Aurelia 用于开发的测试技术,并学习了如何独立使用它们。Jasmine 是测试框架,Karma 用作测试运行器。

我们使用 Aurelia 组件的真实测试示例进行了练习,并探索了一些调试选项。

现在我们已经做好了准备,并且知道如何样式化和测试 Aurelia 应用程序,是时候成为创建 Aurelia 组件的真正专家了。所以,继续阅读!

第四章:创建组件和模板

欢迎来到这本书的第二部分,并祝贺您来到这里!现在您已经了解了 JavaScript 编程的基本原则和技术,是时候深入了解了 Aurelia 这个奇妙框架提供的优势。本章的起点将是理解什么是组件以及我们如何通过应用程序中发生的事件来管理其生命周期。然后,我们将探讨依赖注入,这是一种由大多数流行框架(如 Java EE、Spring 和 Aurelia)使用的知名设计模式!我们的主要重点是解释我们如何管理我们的 DOM 并动态地在屏幕上显示数据,配置路由以访问应用程序中的某些功能,当然,当然,在我们的 FIFA 世界杯应用程序中应用所学的所有内容。本章将涵盖许多非常有用的概念,其中一些是:

  • 创建组件

  • 依赖注入模式

  • 组件的生命周期/事件

  • 数据绑定

  • Aurelia 路由器

  • 测试组件

让我们开始吧!

乐高组件

大多数现代前端 JavaScript 框架都提供了一些对基于组件的开发的支持。这是网络开发方向上一个极其重要的步骤。组件提供了一种编写小型部分的方法,这些部分具有一致的 API,可以轻松地作为更大屏幕、应用程序或系统的一部分进行编排。想象一下每个组件就像一个乐高积木——你可以在任何地方使用它,并且它将保持相同的形状和行为。

组件可以是一个 HTML 部分,一段 JavaScript 代码,一个服务,等等。任何可重用的部分都应该被视为一个组件。

一个小难题

让我们练习一下如何思考,并将一个应用程序抽象成几个组件。以下是一些带有一些部分的网页模板:

图片

现在,是时候思考了。

有多少部分是相似的?

  • 按钮非常相似;只是文本/颜色可以改变

  • 菜单选项可以是一个可重用的单个组件

  • 主页部分是相同的;只是内容不同

  • 标题可以与应用程序的主要部分解耦

你认为哪些部分可以在应用程序页面之间重用,看看:

  • 主页部分可以用作其他选项的容器

  • 按钮可以在所有应用程序部分之间共享

最后,但同样重要的是,当你提交时,你认为你需要刷新整个页面,还是只是某些部分?

更好的选择可能是只刷新真正需要刷新的部分。每个部分可以独立管理它们的数据及其检索方式。

当然,所有这些答案都取决于应用程序的业务规则,但原则始终相同。如果您发现应用程序中的一些部分可以独立于其他部分重用、重新加载、管理和维护,您应该将其解耦为单个组件。

一旦你定义了你的应用程序哪些部分将是组件,就是组织的时候了。你必须确定哪些组件将仅用于特定页面(可能是一个购物车页面的 Item 组件),有多少个将在整个应用程序中共享(一个在许多应用程序报告中使用的通用表格),最后,将它们按分离的组组织:

图片

现在,为每个组件创建一个文件夹;你应该记住,如果一个组件将是另一个组件的父组件,那么子组件文件夹应该创建在父组件内部,以指定所有权。始终记住,作为一个程序员,你的主要目标是让你的代码对其他开发者来说易于阅读和理解——这是一个好的质量指标!

图片

到目前为止,我们已经为我们的组件创建了文件夹结构。Aurelia 组件基本上由两个文件组成:HTML 模板,称为视图,渲染到 DOM 中。.js文件,称为视图模型,使用 ES Next 编写,并定义了行为并提供视图所需的数据。模板引擎,以及我们在以下章节中将详细解释的依赖注入DI),负责创建和强制组件的可预测生命周期。一旦组件被实例化,Aurelia 的数据绑定将这两部分连接起来,允许视图模型中的更改反映在视图中,反之亦然。这种关注点的分离使我们能够与设计师合作并提高我们的产品质量。让我们以一个组件为例来创建一个组件:

/**card-component.js**/

export class CardComponent {

  cardTitle;

  constructor(){
    this.cardTitle = 'Card component example'
  }

}

<!--card-component.html-->

<template>
  <div class="card" >

    <div class="card-header">
      <h2>${cardTitle}</h2>
    </div>
    <div class="card-body">
    </div>

  </div>
</template>

你应该记住一些关于命名组件的良好实践:

  • 使用破折号来命名你的组件。例如,<my-component><my-other-component>是有效的名称语法,而<my_component><myComponent><my_other_component>则不是。你必须保持这种表示法,因为 HTML 解析器会在自定义元素和常规元素之间进行区分。

  • 你不能注册一个已经存在的标签。

  • 自定义元素不是自闭合的。只有原生的 HTML 属性允许这个特性。确保你写上闭合标签(<my-component></my-component>)。

我们的第一个组件已经创建,硬编码,并且可以正常工作。等等….html模板是如何知道我的.js文件是正确的,用于检索数据的呢?Aurelia 遵循一个原则:约定优于配置。这意味着什么?如果我们为两个文件使用相同的名称,框架会自动将管理.html模板的 JavaScript 文件映射过来,我们不需要编写任何配置代码(与其他框架不同)。现在,是时候将其集成到我们的主页面上了。

我们只需要用<require>标签导入文件名。出于其他原因,这个标签将位于页面的顶部部分。然后,我们只需调用组件:

<!--main-template.html-->

<template>
 <require from="./components/card-component"></require> //Remember to add the close tag

 <div class="main-content">
     <card-component></card-component>
 </div>

</template>

启动你的应用程序,你将看到你的组件正在运行。在这种情况下,我们只定义了一个从.js文件渲染到模板的单个属性。这是一个非常基本的例子,所以不要担心,动作即将到来!

学习 DI 的工作原理

DI 基于控制反转模式。让我们来解释一下。

想象一下,如果我们创建一个没有 Aurelia 的 Web 应用程序。你将不得不手动实现类似的东西:

  1. 加载/实例化一个视图模型

  2. 加载/实例化一个视图

  3. 将视图绑定到视图模型

  4. 将视图添加到 DOM 中

  5. 处理用户点击链接。

  6. 解析 URL 散列,确定要加载/实例化的视图模型,检查当前视图是否可以停用,等等

  7. 重复使用

再次,并且很多次。如果没有 Aurelia,你将实现控制应用程序生命周期的逻辑,而不是你的应用程序业务逻辑和功能。

现在,让我们使用 Aurelia 创建一个。你不会在应用程序级别上编写任何配置代码,因为框架会为你完成这项工作。相反,你将专注于编写视图、视图模型、行为和路由,这些将体现你应用程序的定制逻辑和外观。Aurelia 反转了控制,处理应用程序生命周期,同时允许你定义自己的应用程序功能和行为。如何?通过生命周期钩子。

生命周期钩子是附加到视图模型上的可选方法。Aurelia 的路由器和模板引擎将在适当的时间调用这些方法,允许你控制特定的生命周期步骤。

我们将在接下来的章节中深入探讨所有这些方法;目前,我们只关注控制反转IoC)和 DI 功能。

Aurelia 使用 IoC 模式来减少构建应用程序所需的工作。你可以在应用程序启动/结束时使用可覆盖的约定和钩子来指定和控制它们。

DI 使用相同的模式来解决依赖项。依赖项是一个可以使用的对象,或者更具体地说,是一个服务。使用这种模式,你使该服务成为客户端对象状态的一部分,因为你传递了整个服务,而不是允许客户端构建或找到服务。

DI 需要一个注入器。这个注入器负责提供和构建服务对象,并在客户端的状态中定义它。客户端不允许直接调用注入器的代码。它只等待所有依赖项都得到满足。

两个模块是 Aurelia 中 DI 模式应用的关键推动者:

  • 依赖注入:一个可扩展且非常轻量级的 JavaScript DI 容器

  • 元数据:提供了一种在多种语言和格式中访问类型、注释和来源元数据的一致方式

为了说明 DI 是如何工作的,让我们定义一个典型的带有一些外部服务注入的视图模型类。代码应该像这样:

import CustomerService from './services/customer-service'

@inject(CustomerService)
export class CustomerComponent {

  constructor(customerService){
    this.customerService = customerService
  }

}

现在,让我们分析一下代码。这个视图模型是如何在运行时创建的?

Aurelia 负责每个元素的创建顺序,但它是如何工作的呢?首先,Aurelia 使用 DI 容器来实例化所有视图模型。正如我们之前所说的,客户端对象不会实例化或定位自己的依赖项。它们依赖于 Aurelia 作为构造函数参数提供依赖项。

这些依赖是如何被发现的?

在面向对象的语言(如 Java)中,DI 容器可以通过类型识别每个依赖项。在 Aurelia 的情况下,依赖项的实现是通过构造函数参数顺序列表确定的。在 JavaScript 中,我们可以将有关我们的组件或应用程序的各种信息作为元数据存储。我们没有机会定义基于类型的构造函数来定义我们的对象。为了处理这种情况,我们必须将此信息嵌入到类本身上,作为元数据

我们可以使用装饰器为我们的类添加一个定制的构造函数签名,基于将被 Aurelia 的 DI 容器消费的类型。这正是注解@injectCustomerService)在视图模型文件中所执行的操作。如果你是 TypeScript 用户,你可以使用emitDecoratorMetadata标志,它用于添加构造函数信息到我们的类中。只需将@autoInject()装饰器添加到你的类中;在这种情况下,构造函数参数类型是不需要的。

以这种方式编写的视图模型易于测试和模块化。你可以将一个大类拆分成小组件,并将它们注入以实现目标。记住,大类的维护很困难,并且很容易依赖于反模式意大利面代码

依赖项解析是一个递归过程。让我们解释一下——我们的客户视图模型依赖于CustomerService文件。当 DI 容器实例化CustomerComponent类时,它首先需要检索CustomerService实例或如果容器中不存在,则实例化一个。CustomerService可能有自己的依赖项,DI 容器将递归解析直到完整的依赖链被识别。

你可以拥有你需要的任何数量的注入依赖项。只需确保注入装饰器和构造函数相互匹配。

如果你没有使用 Babel 或 TypeScript 装饰器支持,你可以通过类中的静态方法提供注入元数据:

import {CustomerService} from 'backend/customer-service';
import {CommonAlerts} from 'resources/dialogs/common-dialogs';
import {EventAggregator} from 'aurelia-event-aggregator';

export class CustomerProfileScreen {

  static inject() { return [CustomerService, CommonAlerts, EventAggregator]; }

  constructor(customerService, alerts, ea) {
    this.customerService = customerService;
    this.alerts = alerts;
    this.ea = ea;
  }

}

支持静态方法和属性。注入装饰器只是自动设置静态属性。为什么使用它?只是为了使我们的语法更加优雅和易于理解。

管理组件的生命周期

正如我们之前所说的,Aurelia 提供了非常完整的生命周期事件方法来定制和改进我们应用程序的行为。以下是一个包含这些方法的列表:

export class ComponentLifecycleExample {

  retrievedData;

  constructor(service) {
    // Create and initialize your class object here...
    this.service = service;
  }

  created(owningView, myView) {
    // Invoked once the component is created...
  }

  bind(bindingContext, overrideContext) {
    // Invoked once the databinding is activated...
  }

  attached(argument) {
    // Invoked once the component is attached to the DOM...
    this.retrievedData = this.service.getData();
  }

  detached(argument) {
    // Invoked when component is detached from the dom
    this.retrievedData = null;
  }

  unbind(argument) {
    // Invoked when component is unbound...
  }

}

让我们探索脚本中呈现的每个方法:

constructor(): 这是第一个被调用的方法。它用于设置所有视图模型依赖项和其实例化所需的所有值。

constructor 方法可以用来实例化和初始化组件的属性,它们不一定需要预先声明:

constructor(){
  this.customerName = 'Default name'
  this.placeholderText = 'Insert customer name here'
}

此外,你也可以使用类方法来初始化变量:

constructor(){
  this.date = this.getCurrentDate()
}

getCurrentDate(){
  //Method implementation
}

created(owningView, myView): 接下来,会调用 created 方法。在这个时候,视图已经被创建,属于视图模型;它们与控制器相连。这个回调将接收 (owningView) 组件内部声明的视图。如果组件本身有视图,它将作为第二个参数传递,(myView)

bind(bindingContext, overrideContext): 在此时,绑定已经开始。如果视图模型有覆盖的 bind() 回调,它将在此时被调用。第一个参数代表组件的绑定上下文。第二个参数用于添加额外的上下文属性。

attached(): 当组件准备好使用时执行 attached 回调。这意味着它已经被实例化,并且其属性已经正确设置和计算。

如果你使用注入的服务方法,这个方法非常适合检索数据或设置属性。你可以配置不同的方式来加载数据,为用户显示加载警报,并提高用户体验。让我们看一个快速示例:

export class ComponentExample {

  dataList

  constructor(){
    // Constructor's code
  }

  attached(){
    this.showLoader(true);
    this.service.retrieveAllData()
                .then( data => {
                   this.dataList = data.getBody()
                   this.showLoader(false)
                   this.showAlert('Data retrieved correctly!!!')
                })
                .catch( error => {
                  console.log(error)
                  this.showLoader(false)
                  this.showAlert('Oops! We have some errors retrieving data!')
                })

  }

}

正如你所见,我们可以定义回退警报或方法来确保我们正确地处理错误(如果需要的话)。

detached(): 当组件将从 DOM 中移除时调用。与前面的方法不同,此方法在应用程序启动时不会执行。

与前面的例子一样,我们可以定义这个方法来恢复数据到之前的状态,删除本地存储数据等。

unbind(): 当组件被解绑时调用。

你应该记住,这些生命周期回调函数都是可选的。只需覆盖你真正需要的部分。执行顺序与前面提到的列表顺序相同。

使用 Aurelia 管理事件

我们之前解释了如何在组件生命周期中覆盖和捕获确定的事件和方法,但如果我们想在用户点击某个按钮或移动鼠标到一个部分时执行我们自己的方法,该怎么办?我们将开始 委托 事件。

事件委托的概念是一个有用的概念,其中事件处理器被附加到单个元素上,而不是 DOM 上的多个元素。这意味着什么?内存效率。它通过利用大多数 DOM 事件的 冒泡 特性,极大地减少了事件订阅的数量。

另一方面,我们有触发概念。类似但不相同。当你需要订阅不冒泡的事件(如 blur、focus、load 和 unload)时,你应该使用触发绑定。

一些例子如下所示:

  • 你需要禁用一个按钮、输入或其他元素

  • 元素的内容由其他元素组成(可重用组件)

用代码来说,可以这样解释:

<select change.delegate="myEventCallback($event)" ></select>

在你的视图模型中,你应该实现一个带有正确数量参数的方法,这样每次 <select> 元素发生变化时,事件就会被委托到你的自定义函数来处理它:

export class TriggerAndDelegateExample {

  myEventCallback(event){
    console.log(event)
  }

}

现在,让我们trigger相同的方法:

<div class="option-container" focus.trigger="myEventCallback($event)"></div>

注意,我们正在使用trigger绑定来捕获一个非冒泡事件。

在你的日常工作中,可能delegatetrigger就足够用来管理事件,但有些情况下,你需要了解一些更高级的功能来处理它们。想象一下,你正在集成第三方插件并需要与之交互内容。通常,triggerdelegate应该可以完成工作,但情况并非如此。

让我们来看一个例子:

<div class='my-plugin-container' click.delegate='onClickPluginContainer()'>
      <plugin-element></plugin-element>
</div>

但为什么?记住,你正在处理一个第三方插件,所以这将独立于container组件来管理其事件。也就是说,内部插件将在任何点击事件上调用event.stopPropagation()

那在这种情况下我们能做什么呢?别担心,你还有一个选择——capture命令:

<div class='my-plugin-container' click.capture='onClickPluginContainer()'>
  <plugin-element></plugin-element>
</div>

现在,方法将正确执行。再次强调,最重要的问题,为什么?这是因为有了capture命令,无论在容器内部是否调用event.stopPropagation()onClickPluginContainer()事件都会发生。

现在,在这个时候,你可能想知道“所以……我应该使用哪个命令?哪一个更好?”答案是简单的——这取决于你的需求。我们建议你使用delegate,因为这会提高你的应用程序性能。然后,只有在事件需要这样做的情况下才使用trigger,最后,如果你将处理第三方插件或你无法控制的元素,请使用capture,但请记住,这最后一个不是常用的,而且这不是你通常与浏览器事件一起工作的方式。

你可以在官方文档中找到更多关于委托和触发的信息:aurelia.io/docs/binding/delegate-vs-trigger/

数据绑定

Aurelia 有自己的数据绑定系统。让我们用一个例子来解释这一点。

你知道你需要为每个 Aurelia 组件定义一个视图和一个视图模型文件。绑定是将视图模型数据反映到视图中的过程,反之亦然。正如我们之前所说的,Aurelia 最美丽的特性之一是双向绑定框架,所以你不必担心视图或视图模型上的数据更新。

Aurelia 支持 HTML 和 SVG 属性到 JavaScript 表达式的绑定。绑定属性声明由三个部分组成:

attribute.commamnd = "expression"

让我们解释每一个:

attribute:指的是我们将应用到绑定的 HTML/SVG 属性。例如,一个输入标签可以定义以下属性:

<input  value="someValue" id="inputId"  />

valueid将是我们可以引用的属性。

command:在这里,你将使用 Aurelia 的一个绑定命令:

  • 一次性:数据单向流动,从视图模型到视图,仅此一次。

  • 到视图/单向:数据单向流动,从视图模型到视图。

  • 从视图到视图模型:数据单向流动,从视图到视图模型。

  • 双向:默认行为,数据从视图模型流向视图,反之亦然。

  • 绑定:自动选择绑定模式。它为表单控件使用双向绑定,为几乎所有其他内容使用到视图绑定。

让我们使用之前定义的相同输入元素作为例子:

<input  value.from-view="userInputValue" id.bind="editableId"  />
<input  value.one-time="defaultInputValue" id.one-way="generatedId"  />

第一个input元素使用from-view命令绑定用户在input元素中输入的任何内容,但这个值不能更改,也不能从view-model反映到view中。id属性使用双向绑定,因此这个id可以在视图层更新,并在视图模型中反映。第二个只绑定一次value属性,然后任何对这个值的更新都将被忽略。在id属性的情况下,它是由view-model文件生成的,并且从view的任何修改都不会反映到view-model中。

expression:最后一部分。通常是一个 JavaScript 表达式,用于反映view-model属性、计算属性等。再次,让我们使用相同的input元素作为例子:

<input  value.from-view="modelValue" id.bind="formName + randomNumber"  />

value属性只是将modelValue属性反映到视图中。id属性执行一个操作,将视图模型中生成的随机数附加到一个预定义属性中,并使用它作为单个值进行绑定。

就像事件管理部分,可能存在一些情况,你需要使用更高级的功能来获得预期的结果。通常,在开发自定义元素/属性时,你可能需要处理具有@bindable属性的情景。这些属性期望一个函数的引用,因此只需使用call绑定命令来声明并传递一个函数给bindable属性。对于这个用例,call命令比bind命令更优越,因为它会在正确的上下文中执行函数,确保它符合你的预期:

  <custom-element go.call="doSomething()"></custom-element>

go@bindable属性,doSomething()是你的view-model函数。

你可以为你的应用程序添加的一个额外功能是字符串插值。这些表达式允许将表达式的结果与文本进行混合。展示这一功能最好的方式是举一个例子。这里有两个带有数据绑定textcontentspan元素:

    <span textcontent.bind="'Hello' + name"></span>

    <span>Hello ${name}</span>

到目前为止,我们已经了解了 Aurelia 绑定引擎的基本概念。现在,让我们以更高级的方式使用这个伟大的功能来改进我们的应用程序!

在第二章“美化用户界面”中,我们探讨了向我们的应用程序添加 CSS 的一些方法,使其看起来更出色。然而,在你的日常工作中,你可能会遇到一些常见情况,这会让你“混合”一些功能。

这里有一个例子:

您正在编写一个仪表板页面,根据用户状态(活跃,非活跃),提交按钮应该看起来色彩丰富,或者只是禁用,具有不同的形状。

您可以使用字符串插值或.bind/.one-time来绑定元素的class属性:

<template>
  <button class="btn ${isActive ? 'btn-active' : 'blocked-btn'} submit"></button>
  <button class.bind="isActive ? 'btn-active' : 'blocked-btn'"></button>
  <button class.one-time="isActive ? 'btn-active' : 'blocked-btn'"></button>
</template>

使用三元运算,您可以告诉视图应该渲染哪个类。让我们分析第一个:

  • isActive指的是在视图中定义的一个布尔属性。

  • ?是三元运算符。如果条件为true,则使用第一个参数,在这种情况下,是'btn-active'类。

  • :代表条件的else部分。如果它评估为false,则使用:后面的第二个参数。

Aurelia 允许您使用外部 JavaScript 库。在其绑定系统中,它只支持在绑定表达式中添加或删除指定的类。

这样,其他代码添加的类(例如,classList.add(...))不会被移除。这种行为意味着会有轻微的成本,只有在基准测试或一些关键情况(如大列表的迭代)中才能感觉到。可以通过直接绑定到元素的className属性来替换默认行为,使用class-name.bind="....",或者使用class-name.one-time="..."可能是一个更好的选择;这样会更快。

与类类似,您可以直接将样式属性绑定到 DOM 中。请记住,直接在元素中定义样式是正确的,但使用类,您可以给元素添加更多标准化,并使维护变得容易。像其他 HTML 属性一样,您可以使用style.bind从您的view-model中检索style定义。

例如,让我们定义一个样式数组:

export class StyleExample {
  constructor() {
    this.styleAsString = 'color: red; background-color: blue';

    this.styleAsObject = {
      color: 'red',
      'background-color': 'blue'
    };
  }
}

然后,在view文件中,我们只需要绑定预定义的属性:

<template>
      <div style.bind="styleAsString"></div>
      <div style.bind="styleAsObject"></div>
</template>

您也可以使用字符串插值:

<div style="width: ${width}px; height: ${height}px;"></div>

然而,如果您需要与 Internet Explorer 和 Edge 兼容,这种语法将是非法的。在这种情况下,您必须使用css属性:

<div css="width: ${width}px; height: ${height}px;"></div>

绑定计算属性

有时在访问属性时返回一个动态计算值(后处理值)是可取的,或者您可能希望反映内部变量的状态,而不需要使用显式的方法调用。在 JavaScript 中,这可以通过使用 getter 函数来实现:

export class Developer {
  firstName = 'Erikson';
  lastName = 'Murrugarra';

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

这里没有技巧,你只需要绑定fullName属性。绑定系统将分析属性以及我们如何引用一个函数;在渲染计算值之前,它将处理所需的信息。这也被称为脏检查;它将不断观察某些属性是否更改其值,如果它对计算元素有影响,它将被重新评估和重新处理。听起来像是同一方法的多次执行?是的,你的 getter 函数将被调用很多次,大约每 120 毫秒一次。这不是问题,但如果我们有大量的计算属性或如果我们的 getter 函数稍微复杂一些,你应该考虑向绑定系统指示你想要观察哪些属性;在这个时候,避免了脏检查。这就是@computedFrom装饰器发挥作用的地方:

import {computedFrom} from 'aurelia-framework';

export class Developer {
  firstName = 'Erikson';
  lastName = 'Murrugarra';

  @computedFrom('firstName', 'lastName')
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

@computedFrom将告诉绑定系统哪些属性需要被观察。当这些表达式发生变化时,绑定系统将重新评估属性(执行 getter)。这消除了脏检查的需要,并可以提高性能。

值转换器

正如我们在接触这个主题之前所解释的,Aurelia 中的用户界面元素由两个文件组成:视图和视图模型对。视图是用纯 HTML 编写的,并渲染到 DOM 中。视图模型是用 JavaScript 编写的,为视图提供数据和行为。Aurelia 通过其强大的数据绑定引擎将这两个文件链接在一起,作为一个单一元素,允许视图模型中的更改反映在视图中,反之亦然。有时,视图模型显示的数据对于在 UI 中显示来说不是很好或难以理解。处理日期和数值是最常见的场景:

export class Example {

  constructor() {
    this.showRawData();
  }

  showRawData() {
    this.currentDate = new Date(); 
    this.someNumber = Math.random() * 1000000000;
  }

}

我们的视图应该看起来像这样:

<template>
      ${currentDate} <br/>
      ${someNumber}
</template>

这段代码将给我们当前的日期和一些随机数;嗯,这正是我们期望的,这也是可以接受的,但让我们看看这些数据是如何显示的:

Sun Dec 31 2017 18:04:45 GMT-0500 (-05)
936693540.3380567

这对于用户阅读来说肯定是不友好的。解决这个问题的酷解决方案可以是计算格式化的值并将它们作为view-model文件的属性公开。这是一个有效的方法,但请记住,我们正在给模型添加额外的属性和方法;这可能会在将来变得有些混乱,尤其是在需要保持格式化值与原始属性值同步时。幸运的是,Aurelia 有一个功能可以帮助我们处理这些情况。

最常见的选项是创建值转换器,将模型数据转换为视图可读的格式。到目前为止一切正常,但如果需要视图转换值以发送到view-model可接受的格式呢?

Aurelia 值转换器与其他语言的值转换器相当相似,例如 XAML。令人高兴的是,Aurelia 带来了一些显著的改进:

  • Aurelia 的 ValueConverter 接口使用两个方法:toViewfromView。这些方法定义了数据流动的方向。

  • Aurelia 值转换器方法可以接受多个参数。

  • Aurelia 允许你在一个属性中使用多个值转换器,只需使用管道(|)即可。

让我们通过一个例子来看看如何将我们的日期属性转换为更友好的可读值:

import moment from 'moment';

export class DateFormatValueConverter {

  toView(value) {
    return moment(value).format('M/D/YYYY h:mm:ss a');
  }

}

我们的 view-model 文件不会改变:

export class Example {

  constructor() {
    this.showRawData();
  }

  showRawData() {
    this.currentDate = new Date(); 
    this.someNumber = Math.random() * 1000000000;
  }

}

然而,我们的 view-model 文件在此时看起来会相当不同:

<template>
    <require from="./date-format"></require>
    ${currentDate | dateFormat} <br/> 
    ${someNumber} 
</template>

使用这个值转换器,我们将在屏幕上看到这个值:

12/31/2017 6:25:05 pm

这看起来好多了。再次,是时候提出更重要的问题——为什么?让我们检查我们做了什么。首先,我们创建了一个名为 DateFormatValueConverter 的值转换器类并实现了 toView 方法。Aurelia 将执行此方法并将模型值应用于屏幕上的数据之前。在转换用途上,我们使用了 MomentJS。接下来,我们已更新了 view 文件并添加了 <require> 标签以将我们的值转换器类导入将使用它的视图。

当框架处理资源时,它会检查类的元数据以确定资源类型(自定义元素、自定义属性、值转换器等)。元数据不是必需的,实际上,我们的值转换器没有暴露任何。如果你好奇,你必须注意一点——我们使用了 ValueConverter 后缀来命名我们的转换器类。再次,为什么?这是因为你必须记住 Aurelia 的一个基础是约定优于配置。这样,以 ValueConverter 结尾的名称将被假定为值转换器。

现在,我们将展示一个更高级的例子。让我们对我们的类转换器应用一些更改:

import moment from 'moment';

export class DateFormatValueConverter {

  toView(value, format) {
    return moment(value).format(format);
  }

}

view-model 文件仍然是相同的。现在,我们的模板文件将再次发生变化:

<template>
      <require from="./date-format"></require>

      ${currentDate | dateFormat:'M/D/YYYY h:mm:ss a'} <br/>
      ${currentDate | dateFormat:'MMMM Mo YYYY'} <br/>
      ${currentDate | dateFormat:'h:mm:ss a'} <br/>
</template>

现在,我们可以使用相同的值转换器类以不同的格式渲染数据,以满足我们的视图需求。

毫无疑问,我们已经涵盖了 Aurelia 绑定行为的一些最重要的功能,但,当然,还有更多方法和命令我们将在实践中看到。目前,我们准备过渡到另一个重要概念——路由。

路由和资源

路由是网络应用程序最重要的部分之一。我们的应用程序已经部署在 http://localhost:9000,但现在是时候开始定义我们资源的名称和地址了。首先,我们需要定义什么是资源。从概念上讲,资源是单个对象或元素相关的所有相关数据。例如,一个人资源可以具有姓名、地址、生日等字段。因此,扩展这一点,资源也可以是一系列人员。我们将深入讨论你的资源应该如何命名、组织以及调用,但在此刻,你只需要知道基础知识。在网络应用程序中,每个资源都有自己的地址。让我们看一个例子:

我们有一个带有一些联系人的地址簿:

  • http://localhost:9000/ 是服务器 URL,所有资源的基路径。通常与主页/欢迎页面相关,或者你首先定义的用户视图。页面有一个按钮可以查看所有我们的联系信息。

  • http://locahost:9000/persons 是与人员列表资源相关的 URL。在这里,我们将展示我们的通讯录中的人员列表。你可以选择一个来查看其联系详情。

  • http://locahost:9000/persons/p001 将指向具有 p001 ID 的人员。在这里,我们将能够看到其详情。如果你把这个 URL 给另一个用户,他们应该能够看到与你相同的数据,因为这个 URL 属于单一的联系——p001

  • http://locahost:9000/persons?search=p001 略有不同。想象一下,我们的联系列表由超过 500 人组成。你不认为用户通过 ID、姓名或最通用的参数来搜索他们会更方便吗?在这里,我们使用查询参数来表示我们的搜索条件;当然,我们仍在与我们的朋友 p001 一起工作。

现在,让我们配置我们的应用程序,使其为路由准备就绪。

到目前为止,我们已经在我们的应用程序中创建了一些组件。如果没有,不要担心,我们将在本章的最后部分有足够的时间来练习这一点。

现在,让我们向我们的 app.js 文件中添加一些代码。请记住,这个文件应该位于我们应用程序的基 src 文件夹中,因为现在它将代表我们所有应用程序的基本路由:

export class App {
  configureRouter(config, router) {
    this.router = router;
    config.title = 'Aurelia';
    config.map([
      { route: ['', 'home'],       name: 'home',       moduleId: 'home/index' },
      { route: 'users',            name: 'users',      moduleId: 'users/index', nav: true,           title: 'Users' },
      { route: 'users/:id/detail', name: 'userDetail', moduleId: 'users/detail' },
      { route: 'files/*path',      name: 'files',      moduleId: 'files/index', nav: false,    title: 'Files', href:'#files' }
    ]);
  }
}

让我们分析用于定义我们的路由的属性和方法:

  • configureRouter(config, router) 是框架在应用启动时将在基本 view-model 中评估的保留方法。参数引用自 aurelia-router 包中的 RouterRouterConfiguration。如果它们没有提供,框架将自动注入它们。

  • this.router = router 是对路由元素的引用,仅为了让我们能够从视图层(app.html)访问它,从而允许我们动态地构建导航菜单。

  • config.title 指的是在浏览器窗口中显示的应用程序标题。技术上,它应用于 HTML 文档 <head> 中的 <title> 元素。

  • config.map() 向路由器添加路由。尽管之前只展示了 routenamemoduleIdhrefnav,但还可以在 route 中包含其他属性。route 的接口名称为 RouteConfig。你也可以使用 config.mapRoute() 来添加单个 route

  • route 是与传入的 URL 片段匹配的模式。它可以是字符串或字符串数组。route 可以包含参数化路由或通配符。

现在,让我们分析我们创建的路由:

  • 在第一个 route 元素中,第一个标志 route 正在引用基本路径 ('') 和 home 路径。如果我们直接访问 http//:localhost:9000/http//:localhost:9000/home,应用程序将显示相同的页面。name 标志是 URL 标识符,可以直接从一个链接或 href 元素中调用。最后,我们需要通过 route 引用我们引用的文件;在这种情况下,组件位于 home/index,并将通过 moduleId 标志表示。

  • 第二个 URL 引用了 users 资源,但它有一些变化。nav 标志可以是布尔值或数字属性。当设置为 true 时,路由将被包含在路由器的导航模型中。当指定为数字时,该值将用于排序路由;这使得创建动态菜单或类似元素变得更容易。最后,title 标志将显示附加到浏览器窗口页面标题中的页面标题。

  • 第三种情况略有不同。我们可以在路由中间看到一个奇怪的参数,即 :id。这意味着 URL 签名的一部分将是动态的,你还记得我们的人 p001 吗?:id 参数将用于表示 p001 代码,并使该资源的 URL 独一无二。此外,在 view-model 文件中,我们将能够消费该参数并检索与其相关的数据。

  • 最后,我们看到 'files/*path'。通配符路由用于匹配路径的其余部分。href 标志是一个条件性可选属性。如果没有定义,则使用 route。如果 route 有段,则 href 是必需的,就像在文件的情况下一样,因为路由器不知道如何填充模式中参数化的部分。

可能会有一些情况,你需要一些额外的功能来处理它们。

例如,大小写敏感的路由;Aurelia 也解决了这个问题:

config.map([
          { route: ['', 'home'], name: 'home',  moduleId: 'home/index' },
          { route: 'users',      name: 'users', moduleId: 'users/index', nav: true, title: 'Users', caseSensitive: true }
]);

在这些情况下将使用 caseSensitive 标志。

另一种非常常见的情况是未知路由;Aurelia 有一种很好的处理方式:

config.map([
          { route: ['', 'home'], name: 'home',  moduleId: 'home/index' },
          { route: 'users',      name: 'users', moduleId: 'users/index', nav: true, title: 'Users' }
        ]);

config.mapUnknownRoutes('not-found');

config.mapUnknownRoutes() 方法将引用 'not-found' 组件模块。另一种方式是将其表示为一个函数:

const handleUnknownRoutes = (instruction) => {
      return { route: 'not-found', moduleId: 'not-found' };
}

config.mapUnknownRoutes(handleUnknownRoutes);

其他常见场景可能是重定向路由。这很简单——你只需要添加 redirect 标志并指定你想要显示的模块的引用:

config.map([
      { route: '', redirect: 'home' },
      { route: 'home', name: 'home', moduleId: 'home/index' }
]);

到目前为止,我们知道如何在 view-model 层面上配置路由,但视图呢?别担心,这将是我们的下一个主题。

所有这些配置都是在 app.js 文件中完成的,所以现在我们需要转到我们的 app.html 文件。在将路由属性添加到模板之前,你必须考虑一些事情。

通常,大多数 Web 应用程序使用基本布局。这可以由页眉、侧边菜单和视图内容组成。也就是说,唯一需要由路由器刷新和重新加载的元素是视图内容;页眉和菜单在整个应用程序中始终相同,因此我们需要在容器内部定义我们的路由元素;让我们看看代码:

<template>
  <div class="header">
    <header-component></header-component>
  </div>
  <div class="menu">
    <menu-component></menu-component>
  </div>
  <div class="main-content">
    <router-view></router-view>
  </div>
</template>

<router-view></router-view>是 Aurelia 路由器将用来渲染我们配置为路由的组件的 HTML 标志。图形表示如下:

图片

到目前为止,一切正常。然而,这是一个非常基础的途径;让我们探索一些高级方法来使我们的布局更加灵活和可配置。我们知道在 HTML 中定义的router-view元素总是与其父视图的视图模型中定义的路由配置方法中引用的一个或多个视图相关联。

要在router-view HTML 元素上指定布局,我们使用以下属性:

  • layout-view:通过文件名(带路径)指定要使用的布局视图

  • layout-model:指定传递给视图模型激活函数的模型参数

  • layout-view-model:指定与布局视图一起使用的moduleId

为了解释这一点,我们将实现一个完全与我们的app.html文件解耦的自定义布局页面:

<template>
      <div>
        <router-view layout-view="layout.html"></router-view>
      </div>
</template>

我们正在引用一个名为layout.html的文件。这个文件将包含我们的基本布局分布:

<template>
      <div class="left-content">
        <slot name="left-content"></slot>
      </div>
      <div class="right-content">
        <slot name="right-content"></slot>
      </div>
</template>

此外,请注意<slot>标签。这是一个将布局的一部分与引用其名称的视图的一部分关联的机制;在这种情况下,让我们创建一个具有自定义布局的home组件:

<template>
      <div slot="left-content">
        <home-header></home-header> 
      </div> 
      <div slot="right-content">
         <home-menu></home-menu> 
      </div>
</template>

在声明的槽之外的内容将不会被渲染。我们只剩下一个任务要做——配置路由:

config.map([
          { route: '', name: 'home', moduleId: 'home' }
]);

我们只需要声明路由并将其引用到主模块。布局将读取定义在其中的槽标签,并将渲染到主模板中。这样我们就可以根据我们访问的应用程序的路由来定制布局,一个用例可能有自定义菜单选项,同时显示某些路由。

为了让我们的路由器准备好工作,我们还需要覆盖一个东西——回退路由。想象一下,如果你的应用程序基于角色,如果用户不允许访问某些资源,他们应该被重定向到上一个位置。如果没有上一个位置怎么办?回退路由就派上用场了!

让代码展示它的魔力:

export class App {
      configureRouter(config, router) {
        this.router = router;
        config.title = 'Example';
        config.map([
          { route: ['', 'home'], name: 'home',  moduleId: 'home/index' },
          { route: 'users',      name: 'users', moduleId: 'users/index', nav: true, title: 'Users' }
        ]);

        config.fallbackRoute('users');
}

现在你已经了解了关于 Aurelia 路由器及其如何配置以改进应用程序的最重要特性。我们几乎准备好开始创建我们的 FIFA WC App 组件了。在上一章中,我们学习了测试、TDD 和调试。现在,是时候应用所学概念并测试我们的组件了。让我们编码!

测试我们的组件

测试是开发软件应用程序时最重要的步骤之一。在这个阶段,我们准备好开始创建组件、定义绑定行为和配置我们的路由。一切正常,但我们如何确保我们的组件按预期工作?在标记组件为完成并准备好进行 QA/生产环境之前,我们需要测试每个组件。

使用 Aurelia 的组件测试器,你将能够以隔离的方式测试你的组件,就像在一个迷你 Aurelia 应用程序中一样。在测试组件时应该做什么?评估预期数据,并通过生命周期断言数据绑定和行为。

首先,我们需要安装aurelia-testing包:

 npm install aurelia-testing

这个库基于流行的 BDD JavaScript 测试框架 Jasmine,它提供了测试结构和断言。如果你使用 Aurelia CLI 生成了你的应用程序,Jasmine 应该已经包含在内。

安装完成后,你可以开始编写你的第一个单元测试。我们将从一个简单的组件开始,该组件返回客户名称。

首先,让我们定义我们的View模板:

<template>
      <div class="custName">${custName}</div>
</template>

ViewModel文件中:

import {bindable} from 'aurelia-framework';

    export class CustomerComponent {
      @bindable custName;
}

我们组件应该可以工作。让我们验证一下。我们需要创建我们的测试文件:

import {StageComponent} from 'aurelia-testing';
import {bootstrap} from 'aurelia-bootstrapper';

describe('CustomerComponent', () => {
  let component;

  beforeEach(() => {
    component = StageComponent
      .withResources('customer-component')
      .inView('<customer-component cust-name.bind="custName"></customer-component>')
      .boundTo({ custName: 'Diego' });
  });

  it('should render first name', done => {
    component.create(bootstrap).then(() => {
      const nameElement = document.querySelector('.custName');
      expect(nameElement.innerHTML).toBe('Diego');
      done();
    }).catch(e => { console.log(e.toString()) });
  });

  afterEach(() => {
    component.dispose();
  });
});

好的,看起来没问题;它应该会通过。我们在做什么?

首先,我们从aurelia-testing导入StageComponent

import {StageComponent} from 'aurelia-testing';

StageComponent只是创建了一个新的ComponentTester类的实例,该类执行所有工作。接下来,StageComponent工厂将设置组件:

    component = StageComponent
      .withResources('src/customer-component')
      .inView('<customer-component cust-name.bind="custName"></customer-component>')
      .boundTo({ custName: 'Diego' });

StageComponent有一个属性—withResources()—它允许你使用流畅的 API 开始设置。类方法withResources非常有用,可以指定你将使用和注册哪些资源。如果你需要多个单一资源,只需使用一个字符串数组来注册所有这些资源。然后,inView方法允许我们提供我们需要运行的 HTML 代码。这是一个标准视图,你可以定义属性和其他内容,就像在我们的应用程序的真实组件中一样。最后,boundTo方法提供了一个测试viewModel,其中包含了在inView中配置的预定义数据。

在这个第一部分,设置是通过 Jasmine 的beforeEach()方法执行的,以便在存在多个测试的情况下重用相同的设置:

component.create(bootstrap).then(() => {
      const nameElement = document.querySelector('.custName');
      expect(nameElement.innerHTML).toBe('Diego');
      done();
    }).catch(e => { console.log(e.toString()) });

接下来,我们进入测试本身,即create()方法。create()方法将启动一切,并引导迷你 Aurelia 应用程序(它接收从之前导入的aurelia-bootstrapper库中导入的bootstrap组件);这个方法将使用standardConfiguration配置测试,将提供的资源注册为全局资源,启动应用程序,并最终渲染你的组件,这样你就可以断言预期的行为。在这种情况下,我们想要确保我们的custName属性在 HTML 中正确渲染,通过选择具有其类名的 div 标签。我们使用document.querySelector('.custName')来获取元素值并断言其innerHTMLDiego。接下来,我们调用 Jasmine 的done函数来告诉 Jasmine 测试已完成。调用done是必要的,因为create方法异步返回一个Promise。如果测试引发某些错误,catch()方法将被触发,并在控制台打印错误日志:

component.dispose();

最后,我们在我们的ComponentTester实例上调用dispose。这将清理 DOM,以便我们的下一个测试从一个干净的文档开始。

我们的第一项测试已经完成,猜猜看……它通过了!这是一个非常基础的例子,但我们已经学会了组件测试的基本部分以及如何将其包含在我们的应用程序中。现在,让我们探索更多高级功能。

测试组件生命周期

我们需要确保我们的数据按预期检索,同样,我们还需要断言我们的组件行为也进行得很好。为此,我们可以告诉创建的组件我们将手动处理生命周期方法;你会发现代码非常直观:

import {StageComponent} from 'aurelia-testing';
import {bootstrap} from 'aurelia-bootstrapper';

describe('CustomerComponent', () => {
  let component;

  beforeEach(() => {
    component = StageComponent
      .withResources('src/customer-component')
      .inView('<customer-component cust-name.bind="custName"></customer-component>')
      .boundTo({ custName: 'Diego' });
  });

  it('can manually handle life cycle', done => {
    let nameElement;

    component.manuallyHandleLifecycle().create()
      .then(() => {
        nameElement = document.querySelector('.custName');
        expect(nameElement.innerHTML).toBe(' ');
      })
      .then(() => component.bind())
      .then(() => {
        expect(nameElement.innerHTML).toBe('Foo bind');
      })
      .then(() => component.attached())
      .then(() => {
        expect(nameElement.innerHTML).toBe('Foo attached');
      })
      .then(() => component.detached())
      .then(() => component.unbind())
      .then(() => {
        expect(component.viewModel.custName).toBe(null);
      })
      .then(() => component.bind({ custName: 'Bar' }))
      .then(() => {
        expect(nameElement.innerHTML).toBe('Bar bind');
      })
      .then(() => component.attached())
      .then(() => {
        expect(nameElement.innerHTML).toBe('Bar attached');
      })
      .then(done)
      .catch(done);
  });

  afterEach(() => {
    component.dispose();
  });

});

导入的库仍然是相同的,我们组件元素的create()方法将启动应用程序,并为我们提供一个简单的方式来检查我们的生命周期方法响应;只需确保你按照它们执行的顺序调用它们。

那些依赖于外部服务的组件怎么办?别担心,你只需要在测试代码中添加一些额外的行,并创建一个模拟服务的类。

首先,我们的 Mock 类:

export class MockService {
      firstName;

      getFirstName() { return Promise.resolve(this.firstName);
}

我们的自测类将看起来像这样:

describe('MyComponent', () => {
      let component;
      let service = new MockService(); //Our created Mock

      beforeEach(() => {
        service.firstName = undefined; 

        component = StageComponent
          .withResources('src/component')
          .inView('<component></component>');

        component.bootstrap(aurelia => {
          aurelia.use.standardConfiguration();
          aurelia.container.registerInstance(Service, service); //Register our mock service instance to the current container instance
        });
      });

      it('should render first name', done => {
        service.firstName = 'Diego';

        component.create(bootstrap).then(() => {
          const nameElement = document.querySelector('.first-name');
          expect(nameElement.innerHTML).toBe('Diego');

          done();
        });
      });

      afterEach(() => {
        component.dispose();
      });
});

首先,我们声明我们的 mock 服务为一个全局变量。这将用于将其注入到 Aurelia 的容器上下文中;这样,组件就不会检测到真实服务类和我们的模拟服务之间的任何差异。还有一点你应该注意,在beforeEach()方法级别,我们将firstName属性声明为undefined;这只是为了使其可重用和可定制,以便根据每个测试的自身需求进行。记住,这个方法会为每个单元测试独立执行。

如果我需要定义一个更复杂的视图,评估我的组件的容器呢?很简单,你可以使用模板字面量:

import {StageComponent} from 'aurelia-testing';
import {bootstrap} from 'aurelia-bootstrapper';

describe('MyAttribute', () => {
  let component;

  beforeEach(() => {
    //Literal HTML syntax
    let view = `
          <div class="row">
            <div class="col-xs-12">
              <div my-attribute.bind="color">Diego</div>
            </div>
          </div>
        `;
    component = StageComponent
      .withResources('src/my-attribute')
      .inView(view)
      .boundTo(viewModel);
  });
  //...
});

你觉得怎么样?很简单,对吧?那很好!现在,我们已经完全准备好进入本章的精华部分;让我们将一切付诸实践!

是时候练习了!

是时候开始编码了!在这个时候,我们的应用程序已经创建并运行,预先配置为使用 SASS 作为 CSS 预处理器,并与 Aurelia Materialize 库集成,以遵循 Material Design 的良好设计实践。现在我们需要定义我们的布局。在此时,它将非常基础,并且随着应用程序的增长,我们开发的组件可以被改进和重构。

首先,我们需要访问我们的根文件夹应用程序;一旦进入,我们只需输入以下命令:

au run --watch 

接下来,我们打开我们最喜欢的浏览器窗口,它应该看起来像这样:

图片

现在,让我们创建我们的主布局。在src文件夹内,我们将创建一个名为 layout 的文件夹。在这个文件夹内,我们将创建两个子文件夹:header 和 menu:

  • ./src > layout > header >:在这里,我们将创建一个用于视图的 HTML 文件和一个用于视图模型的 JS 文件。这两个文件都叫做app-header

  • ./src > layout > menu >:与头部相同,这两个文件都将被命名为app-menu

我们的文件夹结构应该看起来像这样:

图片

首先,我们将创建app-header组件。让我们打开 HTML 文件并创建我们的navbar头部。就在这一点上,我们将看到 Aurelia Materialize 的功能:

<template>
  <md-navbar>
    <div class="margin-content">

      <ul class="hide-on-med-and-down right">
        <li md-waves><a href="#about">About</a></li>
        <li md-waves><a href="#map">Login</a></li>
      </ul>

    </div>
  </md-navbar>
</template>

<md-navbar>标签指的是 Aurelia Materialize 的navbar元素。这非常有帮助,因为组件已经创建好了;我们只需要调用它们,并开始定义我们希望向用户展示的方式。我们在此处不会创建任何 CSS 类。在我们的nav-bar中,我们正在创建两个导航选项,只是为了看看在浏览器上的样子。

我们的第一个组件创建完成后,是时候将其与主页面app.html进行整合了。首先,我们需要使用<require>标签调用创建的组件:

<require from="./layout/header/app-header"></require>

然后,我们只需要通过其文件名调用创建的组件:

<app-header></app-header>

现在,只需重新加载浏览器窗口,...就出现了一个错误!在这种情况下该怎么办?发生了什么?我们的好朋友,控制台,会告诉我们真相:

DEBUG [templating] importing resources for app.html Array [ "materialize-css/css/materialize.css", "layout/header/app-header" ]
vendor-bundle.js:14222:8 TypeError: target is undefined[Learn More]

让我们关注每行的最后部分。首先,错误发生在 Aurelia 引导程序导入和配置我们创建的组件时。最后一条消息告诉我们错误的原因:undefined target

考虑几分钟,可能会发生什么?我们知道你有足够的知识来告诉我们错误是什么。

准备好了吗?如果你注意的话,我们最近创建的app-header.jsview-model文件是完全空的。所以我们有视图,但这个视图没有指向任何东西,target是未定义的!为了解决这个错误,我们只需要声明组件名称并导出它:

export class AppHeader {

}

现在,让我们重新加载我们的浏览器:

图片

太棒了,对吧?放松,这只是开始。现在,是时候创建我们的菜单了。

我们选择的 Materialize 组件是固定的sidenav。然而,为了将其集成到我们的应用程序中,我们将合并到目前为止学到的某些技术和概念。首先,让我们编写我们的组件:

<template>

  <md-sidenav view-model.ref="sideNav" md-fixed="true" md-edge="left">
    <ul>
      <li md-waves><a>Option A</a></li>
      <li md-waves><a>This is better</a></li>
      <li md-waves><a>I want this</a></li>
      <li md-waves><a>Oops!</a></li>
    </ul>
  </md-sidenav>

</template>

类似地,使用<require>标签,我们将将其导入到我们的app.html文件中。如果我们只是调用sidenav菜单,我们将得到以下结果:

当然,我们不想隐藏我们的主要应用程序内容!是时候开始使用 CSS 来让app-menu为我们的团队服务了。

首先,让我们在我们的app.html页面上添加一些容器顺序。它应该是这样的:

<template>
  <require from="materialize-css/css/materialize.css"></require>
  <require from="./layout/header/app-header"></require>
  <require from="./layout/menu/app-menu"></require>

  <app-header></app-header>

  <main>

    <div class="row">

      <div class="col s12 m12 l12">
        <h1>${message}</h1>
      </div>

    </div>

    <app-menu></app-menu>

  </main>

</template>

我们只是将主要的内容,在这种情况下,消息属性,放入一个填充整个屏幕的容器中,无论分辨率如何。

如果我们运行应用程序,我们仍然会看到相同的结果。我们需要对我们的sidenav组件应用一些自定义 CSS 修改。是时候开始使用 SASS 了!

我们在依赖路径上有了 SASS,并且它已经准备好在我们的应用程序中使用,但让我们添加一些修改,以便使我们的文件分布对我们来说更容易理解。

前往aurelia_project文件夹,打开名为process-css.js的任务。

如果您还记得上一章中的 Gulp 任务自动化,您会发现代码非常熟悉。我们只需要添加一行:

export default function processCSS() {
  return gulp.src(project.cssProcessor.source)
    .pipe(sourcemaps.init())
    .pipe(sass().on('error', sass.logError))
    .pipe(gulp.dest('./')) //THIS LINE
    .pipe(build.bundle());
}

为什么这一行?我们想看到生成的 CSS 文件在我们的项目中,并从我们的index.html文件中导入它。再次问为什么?因为它直接在浏览器中使用此文件,如果需要修改或维护样式表,您的样式修改和调试将更容易。

然后,让我们创建我们的styles文件夹。这个文件夹应该直接位于我们的src文件夹中。它是否可以位于不同的位置?当然,但我们建议您首先检查您的aurelia.json文件。

如果您搜索cssProcessor任务,您将找到这个:

"cssProcessor": {
  "id": "sass",
  "displayName": "Sass",
  "fileExtension": ".scss",
  "source": "src/**/*.scss"
},

源属性指示我们的 scss 文件将位于哪个级别,并且默认情况下它们在哪里?是的,src/*whatever*/*.scss位置。您可以修改它,但就我们当前的目的而言,我们不需要。

然后,在我们的文件夹内,让我们创建我们的第一个名为_mainlayout.scss.scss文件。记住,_前缀是为了指示这个样式表将作为另一个样式表的一部分使用。我们只需要添加以下代码:


header, main, footer {
  padding-left: 300px;
}

md-navbar[md-fixed="true"] nav {
  padding-right: 300px;
}

md-sidenav {
  div {
    collapsible-body {
    }
    padding: 0;
  }
}

我们只是告诉我们的header和应用程序主体保持距离我们的应用程序菜单 300 像素。现在,是时候重新加载我们的浏览器了:

我们的基础布局完成了!猜猜看?是的,是时候添加路由了!

让我们将app.js文件的欢迎信息解耦。创建一个主页组件来渲染自定义消息;我们称之为app-home。现在,在您的app.html文件中的<h2>标签处,放置<router-view>标签。

在应用view-model文件中,删除constructor方法;这次我们不会使用它。然后,只需添加以下代码:

configureRouter(config, router) {
  this.router = router;
  config.title = 'FIFA WC 2018';
  config.map([
    { route: ['', 'home'],       name: 'home',       moduleId: 'home/app-home' },
  ]);
}

现在,让我们重新加载我们的浏览器。注意窗口标题;现在它反映了我们的应用名称!

图片

为了练习目的,我们现在就完成了!如果你注意到了,我们正在将许多关注点混合在我们的应用的一些基本方法中。现在,让我们给我们的代码添加额外的价值。你还记得我们硬编码的菜单选项吗?你不认为它应该是动态的吗?是的,我们正在讨论为我们的练习添加动态绑定!你在等什么,打开你的app-menu视图和view-model

让我们在view-model层创建一个字符串数组。我们将使用在图片中使用的相同选项:

export class AppMenu {

  menuOptions = [
    'Option A',
    'This is better',
    'I want this',
    'Oops!',
  ]

}

接下来,是魔法时刻。repeat命令将为我们做脏活。好吧,我们知道我们之前没有提到它;你还记得我们说在实现真实应用时我们将回顾许多新关注点吗?这是其中之一:

<template>

  <md-sidenav view-model.ref="sideNav" md-fixed="true" md-edge="left">
    <ul>

      <li repeat.for="option of menuOptions" md-waves><a>${option}</a></li>

    </ul>
  </md-sidenav>

</template>

这非常简单。现在我们真的完成了。我们的 FIFA WC 2018 应用已经准备好开始编写我们的业务服务和组件了!还有一件事悬而未决,那就是测试部分。我们不会在练习中涵盖它,因为在下一章中,我们将找到为我们的应用创建的一些更复杂的组件,并且我们将很高兴将测试应用到实际组件上。

摘要

你对 Aurelia 的了解现在真是令人惊叹!我们想要涵盖与组件创建和如何将你的业务场景抽象为一个数字应用相关的各个方面。你了解到每个组件都是整体的一部分,是可重用的,并允许你分离你的应用关注点。由于组件是你的应用的一个独立部分,它管理自己的生命周期;Aurelia 允许我们完全控制并配置事件,例如数据加载或组件销毁时的某些自定义行为。另一个非常有趣的事情是我们可以创建我们自己的事件,并且我们可以从视图层触发它们。

此外,你必须记住一个组件可以继承自其他组件,并且它们都有属性。记住 Aurelia 是一个双向绑定框架,所以所有这些属性都在视图和视图模型文件之间同步。我们还学习了如何实现值转换器和一些其他绑定行为来提高我们的应用性能并减少代码量,使我们的应用更轻量级和易于维护。一旦我们的组件创建完成,并且我们有我们的应用场景,就是时候通过动态路由将它们全部链接起来,定义用户工作流程并将动态属性传递给每个模板。最后但同样重要的是,我们了解了如何测试我们的应用组件,确保它们的功能性和生命周期行为。

您可以开始创建组件并探索 Aurelia Materialize 的库来定制您的应用程序。在随后的章节中,您会发现我们的应用程序非常先进,但请放心,我们不会解释我们没有解释过的内容。继续练习!

第五章:创建我们的 RESTful API

一个网络应用程序由不同的层组成;到目前为止,你一直在开发 FIFA 世界杯项目的客户端层。然而,仅仅向用户展示用户界面是不够的。我们需要处理一些业务逻辑,以便为用户提供适当的用户体验,例如订阅下一场比赛、检索用户喜爱球队的阵容、管理登录等。

为了拥有一个完整的网络应用程序,我们应该在我们的项目中实现以下服务器端缺失的部分:

  • RESTful API 层

  • 数据库层

从前,开发者通常在一个项目中实现所有层,这主要是因为采用了像 XAMPP 这样的流行开源 Web 平台,它只需几秒钟就能配置 PHP/MySQL 环境。因此,开发者通常使用 PHP 编写客户端网页,并在同一文件中添加 HTML 代码,将服务器端代码与客户端代码混合。

随着时间的推移,引入了新技术。现在,开发者将客户端和服务器端分离到不同的项目中,并且除了分离项目之外,这些层还部署在不同的域中。所有这一切都得益于新客户端和服务器端框架的引入。所有这些都带来了新的挑战——通信,因此现在需要一种方式来交换信息。RESTful API 成为在客户端和后端之间使用 HTTP 协议作为这些层之间数据传输手段进行通信的正确解决方案。

以下插图展示了创建 Web 应用的新方法:

图片

在本章中,我们将实现 RESTful API 层。为此,我们将使用 Node.js 和 JavaScript 构建一个健壮的 RESTful API。你还将学习概念以及如何使用 Node.js 之上的开源框架 ExpressJS 设计健壮的 API。我们还将探索新的 MEAN 栈,了解其背后的技术以及为什么我们决定将其命名为新的 MEAN 栈。最后,你将学习如何使用新技术改进你的 RESTful API,以改善团队的开发体验。

因此,我们将涵盖以下主题:

  • 理解 RESTful

  • 设计 API

  • 使用 Node.js 创建 API

  • 使用 ExpressJS 改进我们的 API

  • 编码我们的项目

理解 RESTful

在本节中,我们将介绍 HTTP 和CRUD创建检索更新删除)组件,这些组件构成了 RESTful 的基本构建块。这些组件共同使得在分布式环境中通过定义良好的 API 在不同应用程序之间进行通信成为可能。

理解 HTTP

每次您在网上导航时,您都在使用 HTTP。即使您在 Uber 上预订行程,您也在使用 HTTP。事实上,HTTP 几乎存在于您每天使用的所有应用程序中。

HTTP 基于客户端-服务器通信的原则。这意味着每次您想要访问信息或资源时,您都必须发送一个请求对象,服务器将发送所有请求的信息到一个响应对象中。以下图表解释了这种通信:

图片

因此,为了真正理解如何设计一个健壮的 HTTP RESTful API,我们需要了解 URL 的工作原理以及学习 HTTP 协议。

URL

URL 是应用程序通过定义良好结构的端点来提供对其信息资产访问的方式。例如,如果您想搜索秘鲁,您可能会通过网页浏览器访问以下 URL:www.google.com/search?q=Peru。让我们将之前的 URL 分成几个部分,并详细分析它:

图片

从图中,我们看到基本上有四个部分:

  • 协议:这是非安全连接的 HTTP 或安全连接的 HTTPS

  • 域名:将转换为服务器 IP 的已注册域名,其中包含此资源

  • 路径:这使我们能够将资源分成几个部分

  • 查询字符串:这是可选的,但允许我们提供额外的数据

我们将始终使用 URL 来访问托管在其他服务器上的资源。

动词

HTTP 动词是 RESTful API 背后的魔法。让我们用一个例子来理解 HTTP 动词是如何工作的。我们想要创建一个用于管理产品的 API;我们的第一个版本可能看起来像这样:

端点 HTTP 动词 目标
http://myapp/api/createProduct POST 创建产品
http://myapp/api/updateProduct/P1 POST 更新现有产品
http://myapp/api/listProducts GET 获取产品完整列表
http://myapp/api/viewProductDetails/P1 GET 获取单个产品
http://myapp/api/deleteProduct/P1 POST 删除一个产品

从前面的表中,您可以注意到我们需要记住五个端点,我们使用了两个 HTTP 动词:POST 和 GET。可以理解的是,每次我们想要检索信息时,都会使用 GET 动词,而要执行将修改我们系统中现有信息的操作时,会使用 POST 动词。

因此,让我们使用 HTTP 动词使我们的端点易于记忆。经过简单的重构后,我们的表格可能看起来如下所示:

端点 HTTP 动词 目标
http://myapp/api/products GET/POST 获取产品完整列表。创建新产品。
http://myapp/api/products/P1 GETDELETEPUT 检索单个产品。删除现有产品。修改现有产品。在这种情况下,你必须发送完整的文档,而不仅仅是已更改的字段。

现在,我们正在使用 HTTP 动词,并且我们已经将端点数量从五个减少到两个。HTTP 动词与每个请求一起发送到服务器,因此服务器可以使用它们来识别用户想要执行的操作,发送正确的端点和动词。

头部信息

头部信息包含了我们发送给服务器每个请求的附加信息。在开发过程中,你将更频繁地使用以下头部信息:

  • 内容类型 - Type:告诉服务器你期望哪种类型的响应

  • 接受 - Accept:告诉服务器客户端可以处理哪种类型的内容

  • 授权 - Authorization:发送一段信息以验证信息消费者的身份

当然,还有更多的头部信息。请参阅 developer.mozilla.org/es/docs/Web/HTTP/Headers 了解更多关于 HTTP 头部信息的内容。

主体

主体存在于请求和响应对象中。每次你想在你的数据库中创建新条目时,你都必须在 HTTP 请求的主体部分传递信息。主体部分中的数据被称为 有效载荷

CRUD 通过 HTTP

当我们讨论 HTTP 动词时,你可能已经注意到,每个动词都与一个 CRUD 操作相关联。基本上,这四个操作指的是所有数据库引擎执行的基本功能。

因此,将每个 HTTP 动词与其相应的 CRUD 操作相对应,我们将得到以下表格:

HTTP Verb CRUD Operation GOAL
POST 创建 在我们的应用程序中创建或插入一个新元素
GET 检索 从我们的应用程序中检索或读取元素
PUT 更新 更新或修改我们应用程序中的任何现有元素
DELETE 删除 从我们的应用程序中删除或移除任何现有元素

设计 API

现在我们已经清楚 HTTP 是什么以及其背后的构建块,我们需要设计一个友好且健壮的 API。这个做法的另一个优点是,我们将通过拥有一个定义良好的 API 来提高我们组织中的开发者体验。

API 文档是你在将要进行的每个 API 开发中必须应用的关键实践之一。有了文档,开发团队和 API 消费者都会了解 API 的全部功能,因为它们是以易于阅读的格式定义的。例如,一个组织中的财务团队和物流团队可以共享他们的 API 文档,并立即开始工作,因为他们现在知道了 API 的全部细节。

让我们通过了解 API 的第一步骤和其他设计概念来学习如何设计健壮的 API。

API 首选

API 首先是在开发新产品或服务时你应该牢记的最重要概念之一,你必须把它看作是程序员的用户界面。实际上,API 是你如何在分布式大系统中暴露产品功能的方式。

现在,像云应用和微服务这样的技术和方法正在被广泛应用,并提出了软件开发的新方式。使用微服务,你必须将你的应用程序拆分为不同的独立服务;每个服务包含你应用程序中一组特定的相关功能,例如以一些亚马逊 API 为例,我们可以有如下所示的内容:

如你所见,存在不同的服务需要交换信息和功能。所有这些都必须定义一个一致且自解释的 API,其他服务必须使用这个 API 来执行它们无法完成的操作,或者客户端应用程序可以直接调用 API 来访问某些信息。

API 首先的主要理解是,在你为系统实现任何代码或 UI 设计之前,你应该以这样的方式设计你的 API,即使你没有用户界面,开发者也应该能够导航并访问你的应用程序数据中的信息和功能。

API 设计

为了设计一个健壮、自解释、友好且易于使用的 API,我们将遵循多年来互联网大牛们所使用和实施的最佳实践。幸运的是,一些优秀的开发者和架构师已经定义了一套规则,我们将在本节中介绍。

路径作为名词

你是否见过一些包含完整句子来访问资源的 API 端点?以下是一些示例:

http://myshop.com/createNewProducthttp://myshop.com/deleteProduct
http://myshop.com/updateProduct http://myshop.com/getProductDetail/P01
http://myshop.com/getProductComments/P01

首先要记住的是,你永远不应该在你的端点中使用动词。相反,使用复数名词来指代你的 API 端点中的资源。例如,前面的例子可以被重构为以下内容:

REST 方法 - 不推荐 RESTful 方法 - 推荐
http://myshop.com/createNewProduct http://myshop.com/deleteProduct http://myshop.com/updateProduct http://myshop.com/products
http://myshop.com/getProductDetail/P01 http://myshop.com/products/P01
http://myshop.com/getProductComments/P01 http://myshop.com/products/P01/comments

一个好的做法是不将路径深度扩展超过三个路径。例如,使用以下方法:

http://myshop.com/products/P01/comments

不要这样做:

http://myshop.com/products/details/P01/comments/today

如果你想要扩展你的 API 以执行额外的操作,例如获取今天发布的第一个 10 条消息,请使用查询字符串而不是路径。考虑以下示例:

http://myshop.com/products/P01/comments?day=today&count=10

CRUD 的 HTTP 动词

现在你已经为你的 API 设计了干净的端点,是时候利用我们关于 HTTP 动词所学的知识了。你可能想知道,如果你使用相同的端点 /products,你如何区分创建、更新或删除产品?这就是我们使用 HTTP 动词的情况。例如,对于 /products 端点,我们将有以下动词:

端点 HTTP 动词 目标
/products POST 创建一个新的产品
/products PUT 更新现有产品
/products DELETE 删除现有产品

从前面的表中,你可以注意到关键的区别在于 HTTP 动词。简而言之,一个端点由一个路径和一个 HTTP 动词组成。

API 文档

当你在实际项目中工作时,你需要设计的端点数量将会更多。你需要一种方法来记住所有你的端点以及它们被设计的原因。同样,我们确信记录软件对于我们记住代码应该做什么非常重要;API 文档允许我们通过记录端点和其他附加元数据(如以下列出的)来告诉 API 消费者如何使用我们的 API。

  • 端点路径

  • HTTP 动词

  • 预期头信息

  • 预期体结构

  • 预期结果

有一个很棒的开源工具可以帮助你记录和分享你的 API 文档,它被称为 Swagger。我们鼓励你访问官方网站并尝试使用它:swagger.io

可能的文档可能只是一个简单的表格,包含请求和响应中使用的数据。例如,创建新产品的文档可能如下所示:

路径 产品
HTTP 方法 POST
预期结果 HTTP 200 OK 状态码
预期输入头信息 Content-type: application/json
预期输入体 {id: Integer, name: String, price: Decimal}
目标 使用此端点创建一个新产品
授权 需要授权令牌

在了解了如何设计自解释的 API 以及如何使用 HTTP 创建端点之后,让我们继续使用 Node.js 实现一个 RESTful API。

使用 Node.js 创建 API

是时候开始享受编码我们的 RESTful API 了。到目前为止,你已经看到了 API 背后的理论,这些知识对于理解以下部分非常重要。所以,你可能想知道为什么选择 Node.js?简短的答案是它很酷!哈哈,开个玩笑。Node.js 自 2009 年由 Ryan Dahl 创建以来已经发展演变了。Node.js 被世界各地的领先公司使用,如 LinkedIn、Facebook、Amazon 等。

Node.js 不限于 API 开发。实际上,你可以从命令行工具到 物联网IoT)应用创建任何类型的项目。因此,让我们开始学习 Node.js 的优势以及如何编写我们的 API。

Node.js 优势

Node.js 有很多优势;我们将在以下子主题中探索其中更重要的一些。

异步

在大多数编程语言中,I/O 操作是同步执行的。同步执行将阻塞程序流程,直到阻塞操作完成其执行。例如,以下 Python 代码是同步执行的,并阻塞了执行:

file_content = open("my_file.txt") // takes 10 seconds
file_content_2 = open("my_other_files.txt") // takes 20 seconds
print file_content
print file_content_2   

总执行时间大约为 30 秒。这是因为 I/O 阻塞操作。如果我们用时间线表示同步执行:

现在如果我们使用 JavaScript 以异步方式执行相同的操作:

open("my_file.txt", (file_content) => {
  console.log(file_content)
})

open("my_file_2.txt", (file_content_2) => {
  console.log(file_content_2)
})

从前面的代码中,你可以注意到我们使用回调来处理文件的内容。回调是在某些事件发生后被调用的函数。例如,(file_content)=> {}将在my_file.txt的内容准备好使用时被调用。

两个open()语句将同时执行。并行执行这些语句将帮助我们减少执行时间。例如,让我们看看这个异步代码的时间线执行:

现在执行时间将仅为 20 秒,并且,通过这种方式,我们已经提高了应用程序的性能。这是 Node.js 带来的关键优势之一。

单线程

每当一个新的用户请求到达后端服务器时,它将为该请求创建一个新的线程,这是不使用 Node.js 的后端服务器的经典行为。一旦服务器向用户发送响应,它就会释放线程。处理几个线程没问题,但想象一下,如果你需要同时处理数百万用户会发生什么?让我们用图表来展示这个问题:

Node.js 由于其单线程策略而没有这个问题。它不会为每个请求启动一个新的线程,而是将使用相同的主线程来处理所有请求,并由事件循环支持。以下图表描述了这种情况:

这种场景更适合 CPU 利用率;多个线程允许 Node.js 利用多核 CPU。然而,我们需要超过一个简单的章节来讨论它们。我们强烈建议您访问官方 Node.js 网站nodejs.org/en/

现在是时候编写一个简单的 HTTP 服务器并开始有趣的环节了!

简单 HTTP 服务器

是时候开始编码,看看我们如何使用 Node.js 实现一个简单的 HTTP 服务器了。所以,打开你的终端,并在你选择的目录中,创建一个名为my-server的新文件夹:

$ mkdir my-server
$ cd my-server

一旦你进入my-server文件夹,我们需要初始化一个 NPM 模块,所以运行以下命令:

$ npm init -y

{
 "name": "my-server",
 "version": "1.0.0",
 "main": "index.js",
 "scripts": {
 "test": "echo \"Error: no test specified\" && exit 1"
 },
 "keywords": [],
 "author": "",
 "license": "ISC",
 "description": ""
}

现在是时候通过在终端中执行 touch server.js 命令来创建 server.js 文件了。这个文件将包含我们服务器的代码。我们将开始导入 HTTP 模块:

const http = require('http')

我们使用内置的 require 函数导入任何模块,并定义 http 变量来存放模块引用。让我们通过编写以下代码来实现一个简单的处理器:

const myRequestHandler = (request, response) => {
    response.end('Hello From our Node.js Server !!')
}

如您所见,处理器只是一个声明了两个参数的函数:

  • request:用于读取客户端发送的信息

  • response:用于向客户端发送信息

我们的处理器正在使用 response 参数将友好的消息发送给客户端。现在是我们使用之前声明的 http 引用创建服务器实例的时候了:

const server = http.createServer(myRequestHandler)

我们正在创建一个不执行任何操作的空服务器。为了使我们的服务器变得有用,我们传递之前声明的 request 处理器 myRequestHandler。有了这个,每当某个客户端向我们的服务器发送 HTTP 请求时,我们的服务器就能够发送我们的 Hello 消息。为了完成我们的服务器实现,我们需要监听客户端请求:

server.listen(5000, () => {
    console.log("server is running on port 5000")
})

就这些了!现在我们有一个简单的 HTTP 服务器,执行 node server.js 命令来运行服务器。让我们测试一下。转到 localhost:5000,您应该看到以下内容:

图片

现在您已经知道如何使用 Node.js 内置的 HTTP 模块创建一个简单的 HTTP 服务器。然而,为了创建一个强大的 RESTful 后端,我们需要使用一个更复杂的框架。

在下一节中,我们将使用 Express.js 来增强我们的简单服务器。

使用 Express.js 优化我们的 API

Express.js 是一个基于 Node.js 的开源 Web 框架。我们可以使用 Node.js 的 HTTP 模块来实现我们的 REST API,但我们需要编写大量的代码来处理简单的用户请求。Express.js 非常灵活,并提供了一套功能,使我们能够创建健壮的 API。

编写我们的服务器

是时候创建我们的 FIFA 后端文件夹并开始 API 开发了。打开您的终端并运行以下命令:

$ mkdir wc-backend
$ cd wc-backend
$ npm init -y

{
 "name": "wc-backend",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "scripts": {
 "test": "echo \"Error: no test specified\" && exit 1"
 },
 "keywords": [],
 "author": "",
 "license": "ISC"
}

初始化完成后,让我们安装 Express.js。执行以下命令:

$ npm install --save express

接下来,在根目录中创建 server.js 文件,并编写以下代码:

const express = require('express')
const app = express()

app.use((req, res) => {
    res.send("Hello!")
})

app.listen(3000, () => {
    console.log('running on port: 3000')
})

我们开始导入 express 模块并将一个 express 应用实例化到 app 变量中。接下来,我们使用应用实例通过 app.use 函数配置一个简单的请求处理器。在这个函数中,我们传递另一个作为参数的函数,该函数有两个参数:请求和响应:reqres。为了发送一个简单的消息,我们使用 res 参数。

服务器应用实例配置完成后,我们通过调用其 listen 函数并传递它将监听新 HTTP 请求的 HTTP 端口来激活它:

$ node server.js

我们强烈建议你在开发中使用nodemon。nodemon 会在检测到源代码中的更改时自动重启你的 Node 应用程序。要安装 nodemon,只需执行npm install -g nodemon命令。要运行你的服务器,使用nodemon server.js命令。

让我们通过在浏览器中打开http://localhost:3000或使用 HTTP 客户端命令行工具来测试它。考虑以下示例:

$ curl http://localhost:3000

Hello!

到目前为止一切顺利!让我们定义一个路由路径来创建一个自解释的 API。将以下更改应用到server.js文件中:

...
app.use('/hello', (req, res) => {
    res.send("Hello!")
})
...

现在,前往http://localhost:3000/hello,你应该会看到相同的Hello!消息。你可以导航到http://localhost:3000来查看更改后的结果:

$ curl http://localhost:3000/hello

Hello!

使用路由

路由是我们 RESTful API 背后的魔法。如果你记得我们之前讨论 HTTP 动词时的情况,RESTful 是通过将 CRUD 操作与 HTTP 动词结合而成的。Express.js 使得定义这些 RESTful 方式变得简单。例如,打开server.js文件并应用以下更改:

...
app.get('/hello', (req, res) => {
    res.send("Hello!")
})
...

如你所见,我们将use更改为get。既然你这么聪明,你知道get指的是 GET HTTP 动词,所以让我们定义我们的队伍 API 的 RESTful 路由。在server.js中应用以下更改:

...

app.get('/teams', (req, res) => {
    res.send("*To retrieve the list of teams*")
})

app.post('/teams', (req, res) => {
    res.send("*To create a new team*")
})

app.put('/teams', (req, res) => {
    res.send("*To update an existing team*")
})

app.delete('/teams', (req, res) => {
    res.send("*To delete an existing team*")
})

...

一旦我们应用了这些更改,就是测试它们的时候了。在你的终端中运行以下命令:

$ curl -X POST http://localhost:3000/teams
 *To create a new team* 
$ curl -X GET http://localhost:3000/teams
  *To retrieve the list of teams*

太棒了!我们的 RESTful API 响应正确。注意,我们使用-X [HTTP Verb]来告诉 curl 我们想要为给定请求使用哪个 HTTP 动词。现在我们已经准备好了我们 API 的主要骨架,我们需要以一致的方式组织我们的项目,因为我们将会创建一组文件,并且应该始终组织我们的源代码。继续阅读!

编码我们的项目

到这里我们就位了!我们将实现我们项目的代码。我们的应用程序由三个领域组成:

  • 队伍:用于管理参加世界杯比赛的队伍信息

  • 比赛:用于管理比赛期间所有比赛的信息

  • 安全:用于管理用户信息和权限

队伍和比赛遵循相同的实现方式,只有一些小的变化。因此,在本节中,我们将编写实现Team领域的代码,并通过我们的 API 公开它,然后你可以自己实现Match领域的代码。当然,完整的源代码可以在github.com/Packt-Aurelia-Fullstack-Book/worldcup-app的 GitHub 仓库中找到。

那么,既然已经说到了这里。让我们开始吧!

我们的项目结构

让我们从创建我们源代码的项目结构开始。在你的工作目录中创建以下目录和文件,如下面的截图所示:

图片

src 文件夹包含三个子文件夹:modelsroutesconfig。在本章中,我们将仅使用 routes 文件夹和 teams-api.js 文件。其他两个将在下一章讨论数据库和 MongoDB 时进行探讨。

路由文件夹的目标是托管所有 API 路由声明。我们已在 server.js 文件中声明了我们的路由;如果我们正在处理一个小项目,这可能是个好主意,但对于将有很多路由声明的大项目来说,这并不是一个好主意。因此,一个很好的策略是按 API 功能分离路由;例如,teams-API、matches-API 和 auth-API。这样,我们将拥有更易于阅读和易于维护的代码。

实现 Teams API

在我们开始编码之前,我们必须首先设计我们的 API。以下表格包含了我们将要实现的 API 的文档:

路由 HTTP 方法 目标 响应
/teams GET 列出所有团队 Http 200 - OK
/teams POST 创建新的团队,请求体为{name: String} Http 201 - Created
/teams/:id GET 获取单个团队 Http 200 - OKHttp 404 - Not Found
/teams/:id PUT 更新现有团队 Http 200 - OKHttp 404 - Not Found
/teams/:id DELETE 删除现有团队 Http 200 - OKHttp 404 - Not Found

记住这一点,让我们开始有趣的环节!

配置 JSON 解析器

我们将首先配置我们的服务器以能够解析 JSON。这意味着 Express.js 将自动解析 HTTP 请求进出的数据。以下图表解释了这一点:

如您所见,所有请求都将被 JSON 解析器 截获。JSON 解析器 更为人所知的是 中间件。中间件只是一个简单的函数,它在另一个函数之前被处理。例如,GET /teams 函数应该是在每个请求中应该调用的唯一函数,但由于我们已配置了JSON 解析器GET /teams 将在JSON 解析器函数完成后被调用。

要在我们的代码中配置此功能,首先我们需要安装它。在终端中运行 npm install --save body-parser,并应用以下更改:

const express = require('express')
const bodyParser = require('body-parser')
const app = express()

app.use(bodyParser.json())

app.get('/teams', (req, res) => {

    const teams = [{ "name": "Peru" }, {"name": "Russia"}]

    res.json(teams)
})
...

首先,我们导入 body-parser 模块。然后,我们使用 app.use 函数配置应用程序使用我们的 bodyParser.json()。所有中间件都是通过调用此函数进行配置的。

最后,为了测试 JSON 解析器是否工作正常,我们定义了一个包含两个团队及其相应名称的 teams 变量。要发送 teams,我们使用 res.json 而不是 res.send。让我们通过执行以下命令来检查结果:

$ curl -X GET http://localhost:3000/teams

[{"name":"Peru"}, {"name":"Russia"}]

现在我们 API 能够接收和发送 JSON,让我们将路由移动到自己的文件中。

重构路由

我们之前在 routes 文件夹中创建了 teams-api.js 文件。打开该文件并应用以下更改:

const express = require('express')
const api = new express.Router()

let teams = [
    { id: 1, name: "Peru"},
    { id: 2, name: "Russia"}
]

首先,我们导入express模块。从这个模块中,我们声明了api变量,它是一个express.Router的实例。为了存储一些关于团队的假数据,我们创建了一个teams变量。我们将使用这个路由来配置我们的 CRUD/HTTP 处理器,如下所示:

api
  .route('/teams')
  .get((req, res) => {
    res.json(teams)
  })
  .post((req, res) => {

  })

app.listen(3000, () => {
...

使用api路由变量,我们定义'/teams'路径作为 HTTP 处理器的根路径。在get处理器中,我们只是发送团队的列表作为响应。

最后,我们需要将api路由导出以便在server.js文件中使用:

...
module.exports = api

一旦我们准备好了,打开server.js文件以应用以下更改,这将配置服务器使用此路由:

const express = require('express')
const bodyParser = require('body-parser')
const teamsApi = require('./src/routes/teams-api')
const app = express()

app.use(bodyParser.json())
app.use(teamsApi)

...

首先,我们从相对路径导入 Teams API 模块到我们的项目中,并使用app.use函数来配置我们的路由。让我们测试一下;在您的终端中运行以下命令:

$ curl -X GET localhost:3000/teams

[{"id": 1, "name":"Peru"},{"id": 2, "name":"Russia"}]

太棒了!现在我们的代码整洁,一切按预期工作。是时候编写一些代码来实现 POST、PUT 和 DELETE 处理器了。我们将使用假的teams变量在内存中添加数据,直到我们学习如何在下一章中使用真正的数据库。

创建团队

要创建一个团队,我们需要实现 POST 处理器。新团队的数据将通过 HTTP 请求的body参数发送。将以下更改应用到teams-api.js文件中:

...
api
  .route('/teams')
  ...
  .post((req, res) => {
    let team = req.body 
 teams.push(team) 
 res.status(201).json(team)
  })
  ...
...

首先,我们从req.body属性读取数据。然后,我们将新元素插入到teams数组中。最后,我们发送包含新团队的teams数组,并指定 HTTP 状态码 201,表示资源已创建。

您可以在en.wikipedia.org/wiki/List_of_HTTP_status_codes找到完整的 HTTP 状态码列表。

为了测试,我们将使用以下命令调用我们的 API:

$ curl -X POST -H "Content-Type: application/json" -d '{"id":3, "name": "Brasil"}' localhost:3000/teams

我们的命令这次有点奇怪。因为我们使用 JSON,我们必须明确告诉 HTTP 请求我们正在发送 JSON 数据,所以我们使用-H头选项。为了发送信息,我们使用-d数据选项。就这样!看起来很难,但实际上并不难。

现在,您可以使用 GET 方法列出所有团队并查看添加到我们列表中的新团队。

检索列表

我们已经有了列表处理器,但列表还不够。除了检索完整列表外,我们还需要检索单个团队。为此,我们需要添加一个新的 GET 路由并学习如何使用参数。将以下更改应用到teams-api.js中:

...
api
  .route('/teams')
...

api
  .route('/teams/:id')
  .get((req, res) => {
      const id = req.params.id

      for(let team of teams) {
          if (team.id == id)
            return res.json(team)
      }

      return res.status(404).send('team not found')
  })

module.exports = api

首先,我们声明了一个新的路由,它现在包含一个动态参数—/teams/:api。我们称之为动态参数,因为当然它可以取任何值,这些值将作为req.params对象的属性可用。请注意,你为参数使用的名称将被创建为一个属性,例如,在这个例子中是req.params.id

接下来,我们创建了一个简单的 for 循环,遍历完整的团队列表,寻找与路由参数中传递的相同 id 的团队。如果找到团队,我们通过调用 res.json(team) 语句发送团队。由于我们希望在发送响应后立即退出处理程序,我们使用 return 来退出处理程序。如果没有找到团队,我们发送一个错误消息,并将响应标记为 HTTP status 404,这意味着资源未找到。

最后,为了测试我们的实现,执行以下命令:

$ curl http://localhost:3000/teams/1
 {"id":1,"name":"Peru"}$ curl http://localhost:3000/teams/2
  {"id":2,"name":"Russia"}

请记住,默认情况下,curl 总是发送一个 -X GET 请求,如果没有明确定义 HTTP 动词。

让我们继续我们的最后两个实现,PUT 和 DELETE。

更新一个团队

更新过程是两个过程的组合——搜索团队和更新过程本身。在前一个实现中,我们编写了查找现有团队的代码。所以,让我们通过定义一个可以用于检索、更新和删除的函数来重用相同的代码。打开 teams-api.js 文件并应用以下更改:

...
let teams = [
    { id: 1, name: "Peru"},
    { id: 2, name: "Russia"}
]

function lookupTeamIndex(id) {
 for(var i = 0; i < teams.length; i++) {
 let team = teams[i]
 if (team.id == id)
 return i
 }
 return -1
}

api
  .route('/teams')
...

我们创建了一个 lookupTeam 函数,它期望 id 作为参数,如果找到,将返回一个有效的团队索引。否则,它将返回 -1。现在我们需要重构我们的处理程序以检索团队:

...
api
  .route('/teams/:id')
  .get((req, res) => {
      let id = req.params.id
      let index = lookupTeamIndex(id)

 if (index !== -1)
 return res.json(teams[index])

      return res.status(404).send('team not found')
  })
...

在完成这些之后,让我们实现我们的更新处理程序。在相同的 teams-api 文件中应用以下更改:

...
api
  .route('/teams/:id')
  .get((req, res) => {
     ...
  })
  .put((req, res) => {
 const id = req.params.id
 const index = lookupTeam(id)

 if (index !== -1) {
 const team = teams[index]

 team.name = req.body.name
 teams[index] = team

 return res.json(team)
 }

 return res.status(404).send('team not found')
 })
...

因此,我们定义了一个 .put 路由,并通过传递 id 参数来查找一个团队。如果返回一个有效的索引,我们将团队实例保存在 team 变量中,并通过从 request.body 对象中读取数据来应用更改到其 name 属性,最后发送更新后的团队。如果传递的 ID 没有有效的索引,我们返回一个 Not Found 消息。

执行以下命令来测试:

$ curl -X PUT -H "Content-Type: application/json" -d '{"name": "Brasil"}' localhost:3000/teams/999

Team not found

删除一个团队

delete 过程与 update 类似。首先,我们需要从 teams 数组中检索一个有效的索引,然后将其从中删除。打开 teams-api.js 文件并应用以下更改:

...
api
  .route('/teams/:id')
  .get((req, res) => {
     ...
  })
  .put((req, res) => {
     ...
  })
  .delete((req, res) => {
 const id = req.params.id
 const index = lookupTeam(id)
 const team = teams[index]

 if (index !== -1) {
 teams.splice(index, 1)
 return res.send(team)
 }

 return res.status(404).send('team not found')
 })
...

因此,我们定义了一个 .delete 路由,并通过传递 id 参数来查找一个团队。如果返回一个有效的索引,我们将团队实例保存在 team 变量中。接下来,我们使用 splice(index, 1) 表达式从数组中删除元素。最后,我们仅为了信息目的返回被删除的 team

我们完成了!我们已经实现了一个暴露 HTTP 处理程序的 RESTful API,用于我们的团队功能。我们需要对我们的路由和其他高级功能应用安全性。我们将在后续章节中学习所有这些内容。继续阅读!

摘要

在本章中,我们探索了 API 的世界,并了解了如何使用 HTTP 协议和 CRUD 操作来实现 API。这种伟大的组合使得信息交换比以前更容易。

我们还学习了如何使用 Node.js 创建 API,并看到了如何使用最流行的开源 Web 框架之一 Express.js 构建一个 RESTful API 是多么容易。

在下一章中,我们将把我们的 API 与 MongoDB 集成,以便将信息保存到真实的数据库中,而不是发送虚假数据。情况正在变得更好。

第六章:在 MongoDB 中存储我们的数据

你每天使用的所有应用程序都将它们的信息存储在数据库中。数据库允许你为用户提供最佳体验。想象一下,如果没有数据库,你的用户可能会遇到多么困难。例如,想象你购买了一部新的 iPhone,需要将你的 Instagram 账户中的所有联系人添加到你的新手机上。如果没有数据库,你需要复制所有联系人信息并将其本地添加到新设备上。这听起来可能很荒谬,但这就是没有数据库的世界可能的样子。

学习如何创建能够连接到数据库并存储信息的应用程序非常重要。因此,现在你将学习如何使用 MongoDB 作为你的 NoSQL 数据库,以及如何使用名为 Mongoose.js 的最受欢迎的库之一将我们的后端与之集成。

在本章中,我们将涵盖以下主题:

  • NoSQL 数据库

  • 介绍 MongoDB

  • MongooseJS

  • 将我们的 API 与 MongoDB 集成

NoSQL 数据库

全球的大公司都在使用 NoSQL 数据库来提供用户在使用应用程序时期望的速度;例如,Facebook、Amazon 和 Google 都在使用它们。然而,为什么这些数据库如此特别?为了回答这个问题,让我们看看 SQL 和 NoSQL 数据库之间的区别:

SQL 数据库 NoSQL 数据库
基于表格和硬结构化 不基于表格
鼓励规范化 鼓励非规范化
需要模式 无模式
快速 超快速
可扩展性难以实现 可扩展性极其容易实现

当然,还有更多差异和好处,但讨论所有优点及其背后的科学超出了本书的范围。

重要的是要知道,有不同类型的 NoSQL 数据库来解决不同类型的挑战。让我们来了解一下它们。

文档数据库

这是最受欢迎的数据库之一,归功于 MongoDB 和 CouchDB。这种类型的数据库以 JSON 格式存储信息。由于这是一个键值存储,你可以保存包含数组、嵌套文档和其他不同数据类型的复杂对象。例如,你可以在 JSON 文档中保存以下

{
  "identification" : "PE0022458197",
  "name": {
    "firstName": "Jack",
    "LastName": "Ma"
  },
  "age": 45,
  "addresses": [ 
    {"country": "Peru", "address": "MyTown PE#32"},
    {"country": "China", "address": "OtherTown CH#44"}
  ]
  ...
}

正如你所见,你不需要在不同的文档(或 SQL 数据库中的表)中存储地址;你可以在同一个文档(或 SQL 数据库中的表)中完美地存储它们。

介绍 MongoDB

正如其官方网站所述,MongoDB 是一个开源的文档数据库,它具有你想要的可扩展性和灵活性,以及你需要的查询和索引。

MongoDB 使用集合来持久化一组 JSON 文档,文档的模式可能会随时间变化,而不会影响集合中存储的其他文档。当您在处理具有不同角色和用户的复杂应用程序时,无模式特性非常出色。一个用户可能会使用一些字段,而其他用户可能需要一些其他字段,但不需要用 null 值填充未使用的字段。相反,不需要的字段不会持久化到 JSON 文档中。

安装 MongoDB

您可以在其官方网站 www.mongodb.com 上学习如何安装 MongoDB。我们强烈建议您在开发环境中使用 Docker 而不是在本地机器上安装 MongoDB。那么,让我们看看如何使用 Docker 容器安装 MongoDB。

首先,您需要从其官方网站下载 Docker,www.docker.com。下载完成后,继续安装。如果您在 MacOS 或 Windows 上工作,Docker 还会安装另一个名为 Kitematic 的工具。安装完成后,在容器部分查找 mongo。然后,点击创建。看看下面的截图:

图片

工具将下载镜像,一旦完成,它将运行 MongoDB 容器,并为我们提供 MongoDB 运行的宿主机和端口,如图所示:

图片

对于使用 Linux 的用户,执行以下命令以创建 MongoDB 容器:

> docker run -p 27017:27017 -v $(pwd)/data:data/db mongo

之前的命令将启动一个新的容器,并暴露其内部端口 27017,并将您的宿主机的 data 目录共享到容器中。通过这样做,我们可以将容器信息持久化到我们的宿主机文件系统中。

一旦我们的 MongoDB 数据库在 Docker 上运行,我们需要在 MongoDB 上进行一些实践。

CRUD 操作

在我们将 API 集成到 MongoDB 之前,了解如何直接与 MongoDB 交互非常重要。因此,让我们学习如何使用 MongoDB 命令行工具执行基本的 CRUD 操作。为此,我们需要访问 MongoDB 容器的终端,但使用 Kitematic 非常简单。在容器的工具栏中,点击 exec 选项。这将启动一个连接到 MongoDB 容器终端的终端窗口。

当它启动时,输入 mongo 以进入 MongoDB CLI。看看下面的截图:

图片

对于使用 Linux 的用户。您可以通过运行以下命令进入容器:

$ docker exec -it mongo sh

太棒了!现在我们可以开始使用 MongoDB 了。

创建文档

要插入文档,首先我们指定 collection 名称,并通过调用 insert 方法传递 JSON 文档,如下所示:

> db.collection_name.insert(JSON_Object)

让我们向 teams 集合插入第一个团队:

> db.teams.insert({"code": "PER", 
  ... "name": "Peru", 
  ... "ranking": 11, 
  ... "captain": "Paolo Guerreo", 
  ... "Trainer": "Ricardo Gareca", 
  ... "confederation": "Conmebol"})

WriteResult({ "nInserted" : 1 })

行首的三个点代表新行或Enter键。

如果插入执行正确,你应该会收到包含其nInserted属性中插入的文档数量的writeResult响应。

检索文档

要检索文档列表,我们使用find方法。例如,运行以下查询以检索完整的球队列表:

> db.teams.find()

{ "_id" : ObjectId("5a5cf1419afc8af268b9bb21"), "code" : "PER", "name" : "Peru", "ranking" : 11, "captain" : "Paolo Guerreo", "Trainer" : "Ricardo Gareca", "confederation" : "Conmebol" }

如你所见,一个新_id属性已被自动添加。这个属性被称为 JSON 文档的主键。这是一个自动生成的值,所以当你运行命令时,你将有一个不同的值。

随着时间的推移,球队的集合将不仅仅只有一个球队。那么,我们如何从集合中检索一个单独的球队呢?你还记得我们传递给find方法的{} JSON 对象吗?这个 JSON 对象用于查询集合。所以,如果我们想检索Peru队,我们必须执行以下查询:

> db.teams.find({"code": "PER"})

{ "_id" : ObjectId("5a5cf1419afc8af268b9bb21"), "code" : "PER", "name" : "Peru", "ranking" : 11, "captain" : "Paolo Guerreo", "Trainer" : "Ricardo Gareca", "confederation" : "Conmebol" }

注意,我们可以传递 JSON 文档中使用的任何字段。例如,你可以使用namerankingcaptain等等。

更新文档

要更新文档,我们使用updateOneupdateMany方法。例如,让我们更新Peru队的排名属性。执行以下代码:

> db.teams.updateOne({"code": "PER"}, {$set: {"ranking": 1}})

{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }

语法看起来有点奇怪,但并不是。你可能的第一个问题是为什么我们需要$set?我们需要$set来指定我们想要更新的字段。否则,你会替换整个文档。考虑以下示例:

首先,让我们列出我们的球队集合,以查看第一次更新的反映:

> db.teams.find({"code": "PER"})

{ "_id" : ObjectId("5a5cf1419afc8af268b9bb21"), "code" : "PER", "name" : "Peru", "ranking" : 1, "captain" : "Paolo Guerreo", "Trainer" : "Ricardo Gareca", "confederation" : "Conmebol" }

此外,是的,排名字段已更新为1。现在让我们尝试不使用$set运算符来更新此文档:

> db.teams.updateOne({"code": "PER"}, {"ranking": 1})

[thread1] Error: the update operation document must contain atomic operators :

注意,会抛出一个错误,并且没有进行任何更改。这对我们很有帮助,因为我们正在使用updateOne方法,但还有一个名为update的方法,如果我们不正确使用它,会给我们带来麻烦。例如,运行以下代码:

> db.teams.update({"code": "PER"}, {"ranking": 1})

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

在此刻,当你意识到你忘记了$set运算符时,你已经失去了你队的资料。尝试找到Peru队:

> db.teams.find({"code": "PER"})

没有显示结果。现在可能有一滴眼泪从你的脸颊滑落。有趣,对吧?

在更新或删除文档时要小心。这种类型的生产错误可能会让你失去职位。

删除文档

最后,要删除文档,我们使用deleteOnedeleteMany方法。例如,再次插入Peru队并运行以下代码:

> db.teams.deleteOne({"code": "PER"})

{ "acknowledged" : true, "deletedCount" : 1 }

太好了!现在我们处于良好的状态,可以学习 Mongoose 以及如何将我们的 API 与 MongoDB 集成。继续阅读!!

我们建议您使用 GUI 工具来探索您的 MongoDB 数据。我们使用Robo 3T或 Robomongo。您可以从其官方网站robomongo.org下载。

MongooseJS

Mongoose.js 是最受欢迎的 NPM 模块之一,用于将 Node.js 应用程序与 MongoDB 数据库集成。它提供了一种简单的方式来建模我们的应用程序数据,并附带不同的内置功能来验证、转换和查询我们的数据库,避免样板代码。

我们将使用我们在上一节中安装的 MongoDB 容器。我们需要的信息是 MongoDB 运行的主机和端口。这些信息在 Kitematic 工具的 Home/IP & Ports/Access URL 部分显示。例如,在我的情况下,这些是 localhost32768

图片

安装 Mongoose

要安装 Mongoose,我们将使用 NPM。因此,在你的终端中,导航到 wc-backend 项目并运行以下命令:

$ npm install --save mongoose

安装完成后,我们需要进入 src 文件夹并创建一个名为 config 的新文件夹。在 config 文件夹中,现在创建一个名为 mongoose-connection.js 的新文件:

$ cd src
$ mkdir config
$ touch config/mongoose-connection.js

现在我们已经安装了 Mongoose 并创建了配置文件,是时候编写一些代码来建立与 MongoDB 的连接了。

配置 Mongoose

这就是最精彩的部分开始的地方。因此,我们需要创建一个到 数据库 的连接;为此,打开 mongoose-connection.js 文件并应用以下更改:

const mongoose = require('mongoose')

mongoose.connect('mongodb://localhost:32768/wcDb')

mongoose.Promise = global.Promise

mongoose.connection.on('connected', () => {
  console.log('connection is ready')
})

mongoose.connection.on('error', () => {
  console.log(err)
})

首先,我们导入 Mongoose 模块并将其托管到 mongoose 常量中。然后,我们调用 connect 函数并传递连接 URL,使用 主机端口 指向我们的 MongoDB docker 容器。最后,我们告诉 mongoose 我们的数据库名称将是 wcDb。如果连接成功,将调用 connected 事件,并打印出 connection is ready 消息。让我们来测试一下;执行以下命令:

$ node src/config/mongoose-connection.js

connection is ready

太棒了!我们的 Node.js 模块能够使用 Mongoose 成功地与 MongoDB 建立连接。现在我们需要定义模式、模型和集合。继续阅读!

定义模式

要在我们的数据库中存储信息,我们需要创建一个模型,这个模型是基于初始模式定义创建的。这个模式定义包含了我们想要存储的信息的属性和数据类型。让我们为我们的团队集合定义模式。在同一个 mongoose-connection.js 文件中,添加以下代码:

...
const TeamSchema = new mongoose.Schema({
  name: String,
  ranking: Number,
  captain: String,
  trainer: String,
  confederation: String
})

如您所见,定义模式很简单。我们使用 mongoose.Schema 对象,并将我们想要为模式定义的字段作为 JSON 对象定义。

数据类型

如同其他数据库引擎,字段应该使用数据类型定义。以下都是有效的类型:

  • 字符串

  • 日期

  • 数字

  • 布尔值

  • 数组

  • ObjectId

  • 缓冲区

  • 混合

也许你对列出的几乎所有数据类型都很熟悉。Mixed数据类型基本上允许你定义一个字段,其值可以是任何数据类型。个人来说,我们不推荐使用这种数据类型,因为维护一个Mixed字段可能会变得困难,你可能需要编写样板代码来使用它。

验证

Mongoose 自带几个内置验证器。一些验证器存在于所有数据类型中,而一些则仅针对特定数据类型。例如,String字段将具有minmax验证器,但Boolean类型则没有。

让我们在Team模式中添加一些验证。打开mongoose-connection.js文件并应用以下更改:

...
const TeamSchema = new mongoose.Schema({
  name: {
    type: String,
    min: 3,
    max: 100,
    required: true,
    unique: true
  },
  ranking: {
    type: Number,
    min: 1
  },
  captain: {
    type: String,
    required: true
  },
  Trainer: {
    type: String,
    required: true
  },
  confederation: {
    type: String,
    required: true,
    uppercase: true
  }
})

现在,我们的模式看起来更专业,这将帮助我们验证在 MongoDB 中持久化之前的数据。大多数验证器都是自解释的。正如你可能已经注意到的,当你想要应用验证器时,声明字段的语法会略有变化;在这种情况下,应该传递一个 JavaScript 对象来定义数据类型和验证器。要了解更多关于验证器的信息,请访问mongoosejs.com/docs/validation.html

创建模型

现在我们有了我们的模式,是时候告诉 Mongoose 我们想要使用该模式来创建新对象,以便将它们发送到 MongoDB。为此,我们需要通过传递已定义的模式来创建一个模型。打开mongoose-connection.js文件并添加以下代码:

...
const Team = mongoose.model('team', TeamSchema)

信不信由你,我们只需要这一行代码就能将我们的模式与 MongoDB 接口。在这一行中,我们告诉 Mongoose 我们想要将我们的集合称为team。调用mongoose.model的结果将是一个对象,它是模型;我们将使用这个对象来创建新的实例。Team对象还包含内置的 CRUD 方法,因此我们将使用它们来创建对数据库的 CRUD 操作。

为了测试一下,让我们创建Peru团队并将其保存在我们的数据库中。在同一文件中,添加以下更改:

...
const peruTeam = new Team({
  name: 'Peru',
  ranking: 11,
  captain: 'Paolo Guerrero',
  Trainer: 'Ricardo Gareca',
  confederation: 'Conmebol'
})

peruTeam.save((err, data) => {
  if (err)
    throw err

  console.log("Team was created with the Id", data._id)
})

首先,我们使用之前创建的Team模型实例创建peruTeam对象。每个新实例都包含内置函数。调用内置的save函数将peruTeam保存在数据库中。定义了一个回调来处理操作的结果。如果一切顺利,将显示一个显示新生成 ID 的消息。因此,执行以下命令来测试一下:

$ node src/config/mongoose-connection.js

connection is ready
Team was created with the Id 5a5f8e5c34a28e049c026ed6

太棒了!现在我们准备好开始将我们的数据库模块和 RESTful API 进行集成。继续阅读吧!!

将我们的 API 与 MongoDB 集成

到这里就完成了!现在是时候实现我们的 Teams Rest 控制器了。为此,我们将开始解耦具有与 MongoDB 数据库通信逻辑的所有逻辑的 Team 模型。一旦 Team 模型重构完成,我们将在 Team Rest 控制器中开始实现代码以实现以下 CRUD 操作:

  • 列出所有团队

  • 创建新的团队

  • 更新现有团队

  • 删除团队

让我们动手解决吧!

解耦团队模型

我们在根项目目录中创建了一个 models 文件夹。在这个文件夹中,我们将创建我们应用程序的所有模型。开始在 src/models 文件夹中创建 team.js 文件:

$ touch src/models/team.js

记住我们使用 touch 命令创建一个新文件。然后,打开这个文件,并从 src/config/mongoose-connection.js 文件中剪切以下行并将它们复制到 src/models/team.js 文件中,如下所示:

const mongoose = require('mongoose')

const TeamSchema = new mongoose.Schema({
    name: {
      type: String,
      min: 3,
      max: 100,
      required: true,
      unique: true
    },
    ranking: {
      type: Number,
      min: 1
    },
    captain: {
      type: String,
      required: true
    },
    Trainer: {
      type: String,
      required: true
    },
    confederation: {
      type: String,
      required: true,
      uppercase: true
    }
  })

  module.exports = mongoose.model('team', TeamSchema)

我们只需要隔离 TeamSchema 定义,然后我们将导出 mongoose 创建的模型,以便稍后由 Rest 控制器访问。确保你的 src/config/mongoose-connection 文件看起来如下:

const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:32768/wcDb', { useMongoClient: true })
mongoose.Promise = global.Promise

mongoose.connection.on('connected',() => {
  console.log('connection is ready')
})

mongoose.connection.on('error', err => {
  console.log(err)
})

太好了!到目前为止,一切顺利。现在是时候实现我们的 Rest 控制器了。

实现 Rest 控制器

隔离业务逻辑始终是一个好的实践;因此,我们不会直接从 Rest 控制器调用模型。

连接应用

让我们先调用 mongoose-connection 模块来打开与 MongoDB 的连接。打开 server.js 文件并应用以下更改:

const express = require('express')
const bodyParser = require('body-parser')
const teamsApi = require('./src/routes/teams-api')
const mongooseConfig = require('./src/config/mongoose-connection')
const app = express()

app.use(bodyParser.json())
app.use(teamsApi)
...

这就是我们建立新连接所需的所有内容。mongoose-connection 文件包含打开 MongoDB 连接的逻辑,所以我们不需要编写更多代码。

创建一个新的团队

要创建一个新的团队,我们需要调用由 Mongoose 提供的模型内置方法。save 函数用于创建和更新任何模型的字段。因此,我们首先将开始将 Team 模型导入到 src/routes/teams-api.js 文件中,如下所示:

const express = require('express')
const api = express.Router()
const Team = require('../models/team')

let teams = [
    { id: 1, name: "Peru"},
    { id: 2, name: "Russia"}
]
...

现在我们已经使用 require 函数导入了模块并将其存储在 Team 常量中,我们可以用它来创建一个新的团队。让我们修改 Rest 控制器的 POST HTTP 方法:

...
api
  .route('/teams')
  .get((req, res) => {
    res.json(teams)
  })
  .post((req, res, next) => {
    let team = new Team(req.body)

 team.save()
 .then(data => res.json(data))
 .catch(err => next(err) )
  })
...

需要注意的第一个更改是函数中的 next 参数。此参数用于在 Mongoose 无法创建新团队时向 express 抛出错误。然后,我们创建一个新的团队,通过 req 对象的 body 参数调用 save 函数。save 函数返回一个 Promise,它只是一个异步调用,当成功完成时,将在 then 方法中返回新保存到 Team 的信息。一旦我们有了数据,我们就以 JSON 类型将信息发送给客户端。

让我们测试一下。首先,我们需要通过执行 node server.js 来启动服务器,然后我们将使用 cURL 来测试这个端点。打开你的终端并运行以下命令:

$ curl -X POST -H 'Content-type: application/json' -d '{"code": "GER", "name": "Germany", "ranking": 8, "captain": "Paolo Guerreo", "Trainer": "Ricardo Gareca", "confederation": "Conmebol"}' http://localhost:3000/teams

{"__v":0,"name":"Germany","ranking":8,"captain":"Paolo Guerreo","Trainer":"Ricardo Gareca","confederation":"CONMEBOL","_id":"5a662fbf728726072c6298fc"}

如果一切顺利,你应该会看到响应包含自动生成的 _id 属性的 JSON 对象。让我们看看再次运行它会发生什么:

$ curl -X POST -H 'Content-type: application/json' -d '{"code": "PER", "name": "Peru", "ranking": 11, "captain": "Paolo Guerreo", "Trainer": "Ricardo Gareca", "confederation": "Conmebol"}' 

http://localhost:3000/teams

MongoError: E11000 duplicate key error collection: wcDb.teams index: name_1 dup key: { :\"Peru\" }
...

现在我们收到一个丑陋的错误,说我们遇到了重复键错误。为什么会这样?让我们从我们之前定义的模型模式中找到答案。打开 src/models/team.js 文件:

const mongoose = require('mongoose')

const TeamSchema = new mongoose.Schema({
    name: {
      type: String,
      min: 3,
      max: 100,
      required: true,
      unique: true
    },

...

热狗!你找到了答案。我们遇到的问题是因为我们定义了 name 属性为 unique:true

我们需要在消息中修复一些内容。我们期望从 REST API 接收 JSON 响应,所以让我们在我们的后端配置一个全局异常处理器,将错误作为 JSON 对象发送,而不是一个丑陋且难以理解的 HTML 页面。打开server.js文件并应用以下更改:

const express = require('express')
const bodyParser = require('body-parser')
const teamsApi = require('./src/routes/teams-api')
const mongooseConfig = require('./src/config/mongoose-connection')
const app = express()

app.use(bodyParser.json())
app.use(teamsApi)

app.use((err, req, res, next) => {
 return res.status(500).send({ message: err.message })
})

app.listen(3000, () => {
    console.log('running on port: 3000')
})

我们定义了一个全局中间件,它是一个期望四个参数的函数:

  • err: 如果没有抛出错误,则包含null;否则,它是一个错误实例或另一个值

  • req: 客户端发送的源请求

  • res: 响应属性

  • next: Express.js 将调用的下一个操作的引用

如预期,所有错误都会返回一个不同于200的 HTTP 状态。在我们定义其他 CRUD 操作的正确状态之前,让我们默认使用status(500)。现在,让我们再次运行它并看看会发生什么:

$ curl -X POST -H 'Content-type: application/json' -d '{"code": "PER", "name": "Peru", "ranking": 11, "captain": "Paolo Guerreo", "Trainer": "Ricardo Gareca", "confederation": "Conmebol"}' http://localhost:3000/teams

{"error":"E11000 duplicate key error collection: wcDb.teams index: name_1 dup key: { : \"Peru\" }"}

如您所见,我们接收了一个包含单个error属性的 JSON 对象。太棒了!让我们继续学习如何检索完整的团队列表。

列出团队

要检索完整的团队列表,我们将使用 GET HTTP 方法。让我们先稍微清理一下我们的代码。到目前为止,我们一直在使用一个团队数组;我们不再需要它了,所以让我们将其删除。在src/routes/teams-api.js中,应用以下更改:

...

api
  .route('/teams')
  .get((req, res) => {
    // TODO
  })
  .post((req, res, next) => {
    let team = new Team(req.body)
    team.save()
      .then(data => res.json(data))
      .catch(err => next(err) )
  })

api
  .route('/teams/:id')
  .get((req, res) => {
      // TODO
  })
  .put((req, res) => {
    // TODO

  })
  .delete((req, res) => {
    // TODO
  })

...

现在我们已经清理了代码,添加以下更改以实现检索完整团队列表的逻辑:

...
api
  .route('/teams')
  .get((req, res, next) => {
    Team.find()
 .then(data => res.json(data))
 .catch(err => { next(err) })
  })
  .post((req, res, next) => {
    let team = new Team(req.body)
    team.save()
      .then(data => res.json(data))
      .catch(err => { next(err) } )
  })
...

首先,我们调用find方法以返回一个Promise,作为创建新团队时使用的save函数。由于它是一个Promise,我们将从数据库返回的数据接收进then函数,如果出现问题,它将在catch函数中返回一个错误。让我们测试它:

$ curl http://localhost:3000/teams

[{"_id":"5a662fbf728726072c6298fc","name":"Peru","ranking":11,"captain":"Paolo Guerreo","Trainer":"Ricardo Gareca","confederation":"CONMEBOL","__v":0}]

太棒了!现在我们能够使用我们刚刚实现的api检索团队列表。让我们继续!

查找一个单个团队

要查找单个团队,我们将使用findById内置方法,并将一个有效的 ID 传递给它。因此,应用以下更改:

api
  .route('/teams/:id')
  .get((req, res, next) => {
      let id = req.params.id

 Team.findById(id)
 .then(data => res.json(data))
 .catch(err => next(err))
  })
  .put((req, res) => {
    // TODO

  })
  .delete((req, res) => {
    // TODO
  })

首先,我们从req.params对象中提取 ID。请注意,我们不是使用/teams路由。相反,我们使用的是/teams/:id路由。这意味着 Express.js 将把id属性作为params对象中的一个元素注入。然后,我们调用findById方法并将响应发送给客户端。让我们测试它:

$ curl http://localhost:3000/teams/5a662fbf728726072c6298fc

{"_id":"5a662fbf728726072c6298fc","name":"Peru","ranking":11,"captain":"Paolo Guerreo","Trainer":"Ricardo Gareca","confederation":"CONMEBOL","__v":0}

太棒了!它正在工作。请注意,我们正在使用一个现有的 ID——5a662fbf728726072c6298fc。这个值对您来说将是不同的。要获取一个有效的值,只需调用/teams端点以列出所有您的团队,然后复制并替换_id属性中的值。

现在,如果我们传递一个无效的 ID 会发生什么?让我们测试它:

$ curl http://localhost:3000/teams/5a662fbf728726072c629233

null

现在检索到一个null值。根据我们的 API 文档,我们必须返回HTTP 404状态来表示未找到响应。因此,为了做到这一点,我们需要验证findById方法的结果,并在收到 null 响应时引发错误。请继续并应用以下更改:

...
api
  .route('/teams/:id')
  .get((req, res, next) => {
      let id = req.params.id
      Team.findById(id)
        .then(data => {
          if (data === null) {
 throw new Error("Team not found")
 }

          res.json(data)
        })
        .catch(err => { next(err) })
  })
  ...

现在这个实现中,如果我们收到一个null,我们将引发一个错误,这个错误将由我们的全局错误处理器处理,它会发送一个包含错误信息的 JSON 对象。让我们测试一下:

$ curl http://localhost:3000/teams/5a662fbf728726072c629233

{"error":"Team not found"}

最后,我们需要修改我们的错误处理器,将 HTTP 状态码更改为404。在server.js文件中,应用以下更改:

...
app.use((err, req, res, next) => {
    let status = 500

 if (err.message.match(/not found/)) {
 status = 404
 }

 return res.status(status).send({ error: err.message })
})
...

首先,我们声明一个STATUS变量并将其默认值设置为500。然后,我们应用正则表达式验证来检查消息是否包含not found字符串。如果是这样,STATUS将更改为404。所以,让我们再次测试它,在curl命令中添加-v标志来查看 HTTP 状态:

curl http://localhost:3000/teams/5a662fbf728726072c629233 -v
...
>
< HTTP/1.1 404 Bad Request
...
{"error":"Team not found"}

就这样!现在,我们准备好学习如何更新一个team了。

更新团队

要更新我们的teams,首先我们需要使用路径中提供的ID在我们的数据库中查找现有的团队。如果找到一个团队,我们将对team对象应用更改。所以,让我们开始添加以下代码:

api
  .route('/teams/:id')
  .get((req, res, next) => {
     ...
  })
  .put((req, res, next) => {
    let id = req.params.id 

 Team.findById(id)
 .then(data => {
 if (data == null) {
 throw new Error("Team not found")
 }
 return data
 })
 .then(team => {
 // We found the team. 
 // Code to update goes here!
 })
 .catch(err => next(err))

  })
  .delete((req, res) => {
    // TODO
  })
...

首先,我们从端点中提取传递的id。然后,我们调用findById方法来查找现有的团队。如果找到一个有效的团队,我们将有一个作为参数的team对象。否则,将抛出一个错误。

如您所见,我们需要向我们的逻辑中添加更多的代码行。现在,是时候从req.body对象中提取值并修改找到的team了:

...
  .put((req, res, next) => {
    let id = req.params.id
    let teamBody = req.body

    Team.findById(id)
      .then(data => {
        if (data == null) {
          throw new Error("Team not found")
        }
        return data
      })
      .then(team => {
         team.code = teamBody.code || team.code
 team.name = teamBody.name || team.name
 team.ranking = teamBody.ranking || team.ranking
 team.captain = teamBody.captain || team.captain
 team.trainer = teamBody.trainer || team.trainer
 team.confederation = teamBody.confederation || team.confederation

      })
      .catch(err => {
        next(err)
      })

  })
...

我们创建了一个teamBody变量来存储req.body数据。然后,我们将这些值转换为team对象。我们使用||运算符;如果这个值在请求体中发送,这个运算符将分配teamBody.code。否则,它将分配给team对象相同的值。通过这种方式,我们只能在值被发送时更改这些值。

现在,为了将其保存到数据库中,我们将调用保存方法,就像我们编写创建新Team的逻辑时做的那样:

... 
.put((req, res, next) => {
    let id = req.params.id
    let teamBody = req.body

    Team.findById(id)
      .then(data => {
        if (data == null) {
          throw new Error("Team not found")
        }
        return data
      })
      .then(team => {
        team.code = teamBody.code || team.code
        team.name = teamBody.name || team.name
        team.ranking = teamBody.ranking || team.ranking
        team.captain = teamBody.captain || team.captain
        team.trainer = teamBody.trainer || team.trainer
        team.confederation = teamBody.confederation || team.confederation

        return team.save()
      })
      .then(result => res.json(result))
      .catch(err => next(err))

  })
...

太棒了!现在我们已经实现了更新我们的Team实体的逻辑。让我们测试一下。执行以下命令:

$ curl -X PUT -H 'Content-type: application/json' -d '{"ranking": 1}' http://localhost:3000/teams/5a662fbf728726072c6298fc

{"_id":"5a662fbf728726072c6298fc","name":"Peru","ranking":1,"captain":"Paolo Guerreo","Trainer":"Ricardo Gareca","confederation":"CONMEBOL","__v":0}

极好!现在我们能够更新Team实体。然而,你有没有注意到我们的代码有点难以组织?我们正在使用多个Promise实体来查找和保存一个产品。如果我们需要执行更多的异步操作会发生什么?迟早,我们会有很多then指令的代码,这可能很难维护和理解。但是,别担心!async/await 来拯救这一天!继续阅读。

async/await 指令

async 和 await 是两个指令,它们可以帮助我们从Promise混乱中拯救我们的生命。这将允许我们使用异步语法编写异步代码。让我们组织我们的代码,看看它是如何工作的!

首先,我们需要为更新过程创建一个新的函数,如下所示:

...
const Team = require('../models/team')

const updateTeam = async (id, teamBody) => {
  try {
    let team = await Team.findById(id)

    if (team == null) throw new Error("Team not found")

    team.code = teamBody.code || team.code
    team.name = teamBody.name || team.name
    team.ranking = teamBody.ranking || team.ranking
    team.captain = teamBody.captain || team.captain
    team.trainer = teamBody.trainer || team.trainer
    team.confederation = teamBody.confederation || team.confederation

    team = await team.save()
    return team

  } catch (err) {
    throw err
  }
}

api
  .route('/teams')
...

在前面的代码中,首先要注意的是async关键字。这个关键字会将返回的结果包装成一个Promise,并允许我们使用await关键字。你无法在非异步函数中使用await关键字。await关键字将等待异步调用Team.findById(id)结束,并返回结果。当我们调用team.save()方法时,也会发生同样的事情。

使用 async-await 帮助我们避免Promise混乱。它为我们提供了一个可能看起来像异步执行的执行流程。

一旦我们定义了updateTeam异步函数,我们需要修改我们的PUT端点来调用这个新函数:

...
  })
  .put((req, res, next) => {
 updateTeam(req.params.id, req.body)
 .then(team => res.json(team))
 .catch(err => next(err))

  })
  .delete((req, res) => {
    // TODO
  })
...

我们说过async会将结果包装成一个Promise,因此要使用这个结果,我们需要使用thencatch方法来处理返回的Promise

就这样!现在我们准备好学习如何删除现有的对象了。

删除团队

删除一个团队非常简单。为此,我们将调用Team模型的内置remove方法。添加以下代码:

...
.delete((req, res, next) => {
    let id = req.params.id

 Team.remove({_id: id})
 .then(result => res.json(result))
 .catch(err => next(err))
  })

module.exports = api

现在,让我们测试一下。执行以下命令:

$ curl -X DELETE http://localhost:3000/teams/5a662fbf728726072c6298fc

{"n":1,"ok":1}

现在的输出略有不同;我们收到了一个包含两个参数的 JSON 对象:

  • n:被删除的文档数量

  • ok:如果操作成功则为1,如果不成功则为0

太棒了!现在我们有了管理团队的 Rest API,但我们的 API 中缺少一个关键部分,那就是安全性。我们将在后续章节中添加认证和授权层来使我们的 API 更安全。继续阅读!

概述

在本章中,你学习了什么是数据库以及 SQL 数据库和 NoSQL 数据库之间的区别。我们还实现了将信息持久化到 MongoDB 数据库中的管理团队的 API。

你还学习了关于 async/await 的内容,我们能够编写更易于阅读和维护的异步逻辑。

在下一章中,我们将探索 Aurelia 的高级功能,以将我们的 REST API 集成到我们的 Aurelia Web 应用程序中。

第七章:Aurelia 的高级功能

恭喜!终于,我们到了这里。欢迎来到本书第二部分的最后一章!在这个时候,我们真的确信你知道如何规划和开发,现在我们将添加一些酷炫的功能,使我们的应用程序更加有趣和可扩展。也许你在想如何共享一些属性或触发所有组件的事件,或者使你的应用程序对不同国家讲不同语言的人可理解。好吧,国际化是一个你将在本章中遇到的好概念。如果你需要执行转换日期、数字或货币的操作呢?好吧,这类情况(以及更多)在真实的应用程序中非常常见,所以你需要准备好处理它。你知道吗?我们有好消息!Aurelia 为每种情况(以及更多)都准备好了出色的解决方案。在本章中,你将学习以下主题:

  • 事件聚合器

  • 国际化

  • 记录

  • Aurelia 对话框

  • 值转换器

  • 自定义绑定行为

  • 验证器

  • 自定义属性

  • 计算属性

让我们从这个精彩的章节开始;我保证你会发现它非常有用和有趣。我们保证。

订阅和发布事件 – 事件聚合器来拯救!

在我们当前的应用程序中,我们有不同的组件和视图。其中一些需要从服务器检索数据,其他一些只需要处理为其他组件提供的数据,还有一些只是帮助我们的用户界面更加优雅和易于理解。好吧,到目前为止一切看起来都很好。请注意,到目前为止,我们的应用程序支持不同类型的处理——数据加载、转换以及如何显示。每一项都意味着不同的性能成本,因此可能比其他情况花费更长的时间。话虽如此,让我们描述一个常见的场景——用户进入我们的应用程序并导航到显示本月所有比赛的页面。有大量的数据需要检索,你需要计算今天和比赛日期之间的时间(针对每一个)。

所有这些操作的成本剩余时间将取决于服务器返回的数据量,所以你需要记住——当你设计一个应用程序时,要考虑到最极端的情况。

回到我们的应用程序案例,我们可以找到两种情况:

  • 最方便的情况是用户知道应用程序正在检索数据,并耐心地等待页面完全加载。老实说,根据我们的经验,这种场景代表了数字应用程序中常见用户行为的 5%。

  • 第二个且最可能的情况是一个没有耐心的用户,他们认为自己的互联网连接丢失,不断地刷新页面,或者按下某个按钮,或者更糟糕的是,永远离开我们的应用程序。

我们需要采取行动告诉用户“嘿!我正在处理某事,请稍等!”同时,阻止任何可以触发事件导致更多等待时间的按钮。我们确信你知道答案,那就是著名的加载条图标。

我们有两种实现它的方法:

  • 在每个视图/组件上放置一个加载条图标,并在其中管理其行为

  • 在我们的主应用程序模板中只放置一个加载条图标,并从其他组件调用它

可能你会想知道,我如何从一个子组件调用另一个事件?好吧,当然,没有 Aurelia,这可能是一个困难的任务,但幸运的是,情况不会是这样。Aurelia 附带了一个令人难以置信的、易于学习和理解的功能——事件聚合器。

就像选择使用任何东西一样,使用事件聚合器模块的决定应该由你的应用程序需求来决定。在大多数情况下,它是为了处理横切关注点

让我们概述一下什么是横切关注点。

如果有一些方法应该在应用程序/组件的生命周期中的某个事件被触发,并且与它没有任何关系,我们就是在谈论横切关注点。一些通用的例子如下所示:

  • 应用程序加载

  • 会话验证

  • 记录

配置事件聚合器

事件聚合器类不难理解。令人难以置信的是,它只有三个公开的方法。像任何其他 Aurelia 模块一样,你只需要在使用它之前将其导入并注入到你的视图模型中:

import { inject } from 'aurelia-framework';
import { EventAggregator } from 'aurelia-event-aggregator';

@inject(EventAggregator)
export class ExampleClass {
    constructor(EventAggregator) {
        this.ea = EventAggregator;
    }
}

现在,让我们详细探索事件聚合器的方法。

publish(event, data)

此方法允许你触发事件。记住,我们的EventAggregator将被放置在某个父组件中,所以你知道哪些组件将订阅它。因此,由于这个原因,这个方法没有特定的目标;它们只是触发到空间的事件,不管是否有零个或多个订阅者。

第一个参数是事件名称。你可以为这个选择任何名字,因为这是你的自定义事件。它将被用作标识符,以便从应用程序的外部组件调用它。在这种情况下,你可以使用EventAggregator来配置我们的加载条,所以按照它来命名是正确的。我们将称之为dataRetrievingEvent

第二个参数是你想要传递给事件的数据(例如提供一些数据),这是完全可选的。大多数情况下,它将是一个数组或数据对象。如果你愿意,甚至可以传递一个字符串值。然而,并不是所有事件都需要接收新数据。

我们将使用我们的EventAggregator类定义变量来调用此方法:

this.ea.publish('dataRetrievingEvent', {message: 'Loading...don't close the window!' 

这非常简单。我们已经配置并准备好了我们的第一个自定义事件,可以从应用程序中的任何组件调用它。

subscribe(event, callbackFunction)

如果我们在第一个方法中发布了一个事件,现在就是时候监听它了。第一个参数是我们想要订阅的事件名称,第二个参数是一个回调函数,可以用来获取事件发布者发送的值。这些数据可以是一个简单的字符串,或者,就像在我们的例子中,是一个对象。

就像前面的例子一样,我们可以在我们的组件中通过EventAggregator类实例访问这个方法:

let subscription = this.ea.subscribe('dataRetrievingEvent', response => {
    console.log(response);
    // This should yield: Object {message: "Loading...don't close the window!"}
});

我们定义了一个subscriber对象,这基本上是一个方法调用。这个对象将直接引用订阅的事件,并允许我们直接执行一个名为dispose的子函数。这个函数用于删除现有的订阅,通常用于我们的组件被销毁时。请记住,即使事件聚合器是一个很棒的功能,也会付出一点性能上的代价,所以不要滥用它。

这里有一个示例,当视图模型被分离时,订阅会被移除:

import { inject } from 'aurelia-framework';
import { EventAggregator } from 'aurelia-event-aggregator';

@inject(EventAggregator)
export class ExampleClass {
    constructor(EventAggregator) {
        this.ea = EventAggregator;
    }

    attached() {
        this.subscriber = this.ea.subscribe('dataRetrievingEvent', response => {
            console.log(response.message);
        });
    }

    detached() {
        this.subscriber.dispose();
    }
}

这是一个垃圾回收措施,确保您的应用程序不会使用它不再需要的资源。

subscribeOnce(event, callbackFunction)

这种方法与subscribe()方法完全相同,但只有一个区别——一旦回调被触发,它会自动取消订阅事件。也许在某些情况下,您会发现一些只需要一次性订阅的情况,比如我们的加载条。这个例子与subscribe()方法的例子非常相似:

attached() {
     this.subscriber = this.ea.subscribeOnce('dataRetrievingEvent', response => {
         console.log(response.message);
     });
}

我们确信您会发现这个功能非常有用。这只是开始!让我们探索其他令人惊叹的 API!

为我们的应用程序添加更多语言——国际化!

对于每个 Web 应用程序来说,国际化是更有趣(和有用)的功能之一。我们正在开发一个用于 2018 年俄罗斯 FIFA 世界杯的应用程序,因此,基于这一点,如果我们的应用程序能够支持全球所有用户的多种语言,那就太棒了。

为了这个目的,我们将使用官方的aurelia-i18n插件;这将用于获取当前应用程序的位置。这个插件基于i18next库,具有一些非常有趣的特性,如下列所示:

  • 翻译加载器

  • 语言检测

  • 产品本地化

  • 灵活性和可扩展性

最后一个是最重要的功能。考虑到可扩展性,您只需一个配置文件就可以为较小的项目实现国际化,如果您需要在较大的项目上实现它,只需创建多个翻译文件并根据用户的需求加载它们。

安装和配置

根据您选择的构建工具来创建和配置您的应用程序,您会发现许多安装和准备aurelia-i18n插件的方法。到目前为止,我们已经使用了 Aurelia CLI,但让我们探索一下如果我们选择了不同的选项,如何配置它,还记得吗?

  • Webpack

  • JSPM

  • Aurelia CLI

对于 Webpack 用户

我们需要从npm仓库检索插件,所以只需输入以下命令以获取最新版本:

 npm install aurelia-i18n --save

它将下载并保存插件到您的项目依赖项中。正如我们之前所说的,aurelia-i18n基于i18n框架,因此您应该安装一个后端插件。其中最著名的是i18next-xhr-backend,一个简单的用于浏览器的i18next后端。它是如何工作的?简单,使用XHR,它将从某些后端服务器加载资源。是的,您的翻译文件!如果您想使用内置的aurelia-loader后端,即 Aurelia 用来获取资源的相同插件,那么可以忽略下一步。

您现在知道该怎么做。

npm install i18next i18next-xhr-backend --save

我们几乎准备好了。在您的 Webpack 配置文件中,您必须在项目的 Aurelia 捆绑列表(在 Aurelia 部分)中添加aurelia-i18n

const coreBundles = {
    bootstrap: [/* many options here */],
    aurelia: [
      /* many options here too*/
      'aurelia-i18n' // add aurelia-i18n to the array
    ]
  }

JSPM 用户

与前两步类似,但我们将使用 JSPM 而不是已知的 NPM。首先,我们将下载aurelia-i18n插件:

 jspm install aurelia-i18n

对于后端,请输入以下内容:

jspm install npm:i18next-xhr-backend

由于我们将使用内置的aurelia-i18n-loader,因此我们不需要添加任何额外的配置。

Aurelia CLI 用户

与我们在 Webpack 用户部分解释的几乎相同,我们需要从npm仓库检索aurelia-i18n插件和后端服务器:

npm install aurelia-i18n --save
npm install i18next i18next-xhr-backend --save

现在,我们需要告诉我们的项目我们有一些新的依赖项需要配置。打开您的 Aurelia 配置文件(aurelia.json),查找dependencies部分。您必须添加以下条目:

{
    "name": "i18next",
    "path": "../node_modules/i18next/dist/umd",
    "main": "i18next"
  },
  {
    "name": "aurelia-i18n",
    "path": "../node_modules/aurelia-i18n/dist/amd",
    "main": "aurelia-i18n"
  },
  {
    "name": "i18next-xhr-backend",
    "path": "../node_modules/i18next-xhr-backend/dist/umd",
    "main": "i18nextXHRBackend"
  }

如果您的应用程序是使用Aurelia CLI 0.33.1创建的,您可以省略此最后一步。

最后,我们完成了插件的配置。现在,让我们将其配置到我们的应用程序中!

配置和定义我们的第一个翻译文件

我们的应用程序已准备好开始使用i18n框架。有一些步骤需要准备插件,让我们来探索它们。

我们需要做的第一件事是定位我们的index.html文件。它应该位于我们的主要根应用程序文件夹中。确保您的<body>部分与以下相同:

 <body aurelia-app="main">
      /* Some content */
 </body>

如果您是Webpack用户,请定位index.js文件而不是index.html

然后,在您的根应用程序位置,创建一个名为locales的文件夹。它将用于存储所有位置文件。为要支持的语言创建一个文件夹。然后,在每个文件夹内部,创建一个名为translation.json的文件。此文件将包含所有您的文本翻译结构,具体取决于您的应用程序。

您的应用程序文件夹结构必须如下所示:

图片

i18next基于预定义的事务文件模式工作。查看以下我们的 en-EN 事务文件示例:

 {
    "welcome": "Welcome to FIFA WC 18!",
    "user_male": "Mr.",
    "user_female": "Mss.",
    "time_remaining": "Time Remaining : {{time}}"
  }

现在,一个用于 es-ES 语言支持:

 {
    "welcome": "Bienvenido a FIFA WC 18!",
    "user_male": "Sr.",
    "user_female": "Sra.",
    "time_remaining": "Tiempo pendiente : {{time}}"
  }

所以这些只是为了示例目的,并且非常有用,可以帮助你理解这个插件的工作方式。现在,是时候配置插件后端了。你还记得我们创建的 src/main.js 文件来定义我们的配置函数吗?好吧,现在是打开这个文件并添加一些新配置的时候了。如果你还没有创建这个文件,这是一个很好的时机去做这件事。

对于选择 i18next-xhr-backend 支持的人来说,首先,打开 main.js 文件并找到 Aurelia 的配置部分。在文件的第一行,你必须导入以下文件:

import {I18N, TCustomAttribute} from 'aurelia-i18n';
import Backend from 'i18next-xhr-backend'; 

然后,创建一个新的插件管道:

aurelia.use
  .standardConfiguration()
  .plugin('aurelia-materialize-bridge', b => b.useAll())
  .plugin()/* <<<<  You must create a new plugin pipe*/
  .feature('resources');

现在,在新的插件管道内部,添加以下配置:

.plugin('aurelia-i18n', (instance) => {
            let aliases = ['t', 'i18n'];
            TCustomAttribute.configureAliases(aliases);
            instance.i18next.use(Backend);

            return instance.setup({
              backend: {                                  
                loadPath: './locales/{{lng}}/{{ns}}.json',
              },
              attributes: aliases,
              lng : 'es',
              fallbackLng : 'en',
              debug : false
            });
});

让我们简单解释一下我们在该文件中做了什么。

首先,我们需要配置我们的 i18n 别名,所以只需在简单的字符串数组中声明它们,并将它们作为参数传递给静态的 configureAliases() 方法。这将把定义的值映射到 <html> 标签中,以调用正确的值。现在可能听起来有点令人困惑,但别担心,你很快就能看到整个画面:

let aliases = ['t', 'i18n'];
TCustomAttribute.configureAliases(aliases);

接下来,我们将导入的后端插件(i18next-xhr-backend)注册到我们的 aurelia-i18n 实例中:

instance.i18next.use(Backend);

最后,我们需要添加一些配置。这完全基于 i18n 配置文档,所以你可以在 i18next.com/docs/options 找到更多关于这方面的信息。那个 promise 配置必须返回;正因为如此,我们在 instance.setup() 声明之前添加了 return 语句:

backend: {  // <-- configure backend
   loadPath: './locales/{{lng}}/{{ns}}.json', // <-- Our location files path
},

最后的选项用于映射后备语言、默认语言等:

attributes: aliases, <<-- Predefined aliases
lng : 'es', // <<-- Default language to use (overrides language detection).
fallbackLng : 'en',// <<-- Language to use is current location language is not available
debug : false // <<-- Log info level in console output

我们已经准备好开始使用这个插件了。做得好!

如果你是一个 Webpack 用户,别忘了在插件名称前加上 PLATFORM 前缀。例如:  .plugin(PLATFORM.moduleName('aurelia-i18n'), (instance) => {......});

使用插件 – 多语言支持!

要开始使用我们的文件,你必须告诉你的 ViewModel 组件使用哪种语言。我们将在构造函数方法中执行此操作;查看以下示例:

import {I18N} from 'aurelia-i18n';
import { inject } from 'aurelia-dependency-injection';

@inject(I18n) export class WelcomePageComponent { constructor(i18n) { this.i18n = i18n; this.i18n
 .setLocale('es-ES')
 .then( () => {}); } ... }

如果你想获取活动区域设置,很简单,将配置文件修改如下:

import {I18N} from 'aurelia-i18n';

@inject(I18n)
export class WelcomePageComponent {
     constructor(i18n) {
       this.i18n = i18n;
     }
      ...
}

setLocale() 类似,我们还有 getLocale() 方法。你可以通过输入以下内容来检索活动区域设置:

console.log(this.i18n.getLocale());

现在,在 HTML 文件中,我们只需要调用我们的翻译别名来映射我们在 translation.json 文件中定义的属性:

<h2 t="welcome">Welcome to FIFA WX 18</h2>

可选地,我们能够使用我们的第二个别名来映射值:

<h2 i18n="welcome">Welcome to FIFA WX 18</h2>

现在,你已经准备好开始为你的应用程序添加多语言支持了!使用这个插件还有很多其他高级提示可以帮助你获得最佳效果。我们将在下一个示例中展示其中之一。

想象一下,你需要在你翻译文件中映射<html>标签。这是可能的吗?是的。想象一下,你需要渲染一些长的测试(例如产品描述),并且需要加粗一些单词,比如价格或折扣。我们需要做什么?非常简单,只需添加你需要的 HTML 标签:

"time_remaining": "Time remaining : <b>{{time}}</b>"

现在,让我们在我们的View文件中使用它:

<label t="time_remaining">Time remaining : {{time}}</label>

如果你查看你的窗口,你会看到类似&lt;b&gt;bold&lt;/b&gt;的东西,不要害怕,这是正常的。这是因为我们没有设置正确的标记来正确解释我们的 HTML 标签。你必须知道,有四个主要属性可以添加自定义行为到我们的翻译文件变量:

  • [text]: 默认属性,将标签值转义为纯文本

  • [html]: 告诉我们的翻译文件“嘿,这包含 HTML 标签,将其渲染为它们!”

  • [append]: 将翻译添加到元素中已存在的当前内容(允许 HTML)

  • [prepend]: 将翻译添加到元素中已存在的当前内容之前(允许 HTML)

这个属性必须在我们翻译标识符键之前。你知道接下来要做什么:

<label t="[html]time_remaining">Time remaining : {{time}}</label>

太棒了?是的,确实如此。至于其他更高级的功能,我们非常确信你会喜欢i18n提供的所有选项。这就是这一章的全部内容吗?当然不是。让我们继续探索!

跟踪方法调用和用户行为——日志记录

作为开发者,你知道了解你的应用程序正在发生什么非常重要。一些关于用户点击、事件触发或错误消息的信息,有一个好的日志工具在你身边告诉你应用程序是否正常(或者真的很糟糕)是良好的实践。通常,开发者使用常见的console.log()语句,当它部署到 UAT 或生产环境时,他们会注释掉所有这些行。

Aurelia 知道这个功能有多重要,猜猜看——是的,它为此目的有自己的插件。让我们来探索一下!

配置你的日志管理器

默认情况下,Aurelia 已经在他们的依赖项中有了日志 API,所以你在这个时候不需要运行任何npm命令。当然,如果由于某种原因该库缺失,你知道如何处理它。

首先,我们需要创建一个文件来配置我们的日志级别。在resources文件夹中,创建一个名为custom-log-appender.js的文件。这个名字完全可选;你可以以最方便的方式命名它。

首先,让我们配置所有我们的日志级别:

export class CustomLogAppender {

  constructor(){}
  debug(logger, message, ...rest){
    console.debug(`DEBUG [${logger.id}] ${message}`, ...rest);
  }
  info(logger, message, ...rest){
    console.info(`INFO [${logger.id}] ${message}`, ...rest);
  }
  warn(logger, message, ...rest){
    console.warn(`WARN [${logger.id}] ${message}`, ...rest); 
  }
  error(logger, message, ...rest){
    console.error(`ERROR [${logger.id}] ${message}`, ...rest);
  }
}

我们几乎准备好了。现在,打开主配置文件(main.js)并从 Aurelia 导入日志依赖项:

import {LogManager} from 'aurelia-framework';

此外,导入我们最近创建的CustomLogAppender

import {CustomLogAppender} from './resources/custom-log-appender';

现在,使用你创建的CustomLogAppender配置 Aurelia 的LogManager

LogManager.addAppender(new CustomLogAppender());
LogManager.setLevel(LogManager.logLevel.debug);

查找configure()函数。只需添加一行:

export function configure(aurelia) {
  aurelia.use
    .standardConfiguration()
    .developmentLogging() // <-- Logging activated for development env!
    .plugin('aurelia-animator-css');

注意,这种配置将适用于所有环境(dev、test、prod)。通常,日志记录在开发阶段最常用于检测错误,所以让我们对之前的配置做一些改进:

首先,让我们创建一个名为 environment.js 的文件。这个文件将包含我们的当前激活环境:

//environment.js
export default {
  debug: true,
  testing: false
};

然后,我们需要将此文件导入我们的 src/main.js 文件:

import environment from './environment';

export function configure(aurelia) {
 aurelia.use
     .standardConfiguration()
     .plugin('aurelia-anumator-css');

if (environment.debug) { 
     aurelia.use.developmentLogging();
}
    .
    .
    .
}

你已经准备好开始使用日志记录器了!让我们打开一个 ViewModel 文件,开始记录内部发生的事情:

import {LogManager} from 'aurelia-framework';
let logger = LogManager.getLogger('homePage');
logger.debug('me');

export class HomePage() {
    activate(){
       logger.debug(“Enter to home page!!!”);
    }
}

这非常简单且非常有用。当然,我们还有更多特殊功能要展示给你。继续阅读吧!

模态配置 - Aurelia 对话框来拯救!

每个应用程序都需要向最终用户展示不同类型的信息。这些信息需要显示在单个页面上吗?不一定。最终用户非常熟悉 bootstrap 对话框(通常称为 Modal),这是一个自定义的 JavaScript alert() 元素。它更加优雅,更易于添加自定义行为,现在它可以用来显示警报信息,你也可以配置整个表单或确认对话框。在我们的 FIFA 世界杯应用程序中,Aurelia-materialize 插件已经为模态组件配置了这个功能,但让我们探索它是如何工作的以及我们如何可以改进它。让我们开始吧!

获取 Aurelia-dialog 插件

如果你使用的是 JSPM 管理器,请输入以下命令:

jspm install aurelia-dialog

否则,对于 Webpack / Aurelia CLI 用户,使用已知的 npm install 命令:

 npm install aurelia-dialog --save

记得将这个依赖项保存到你的 项目依赖项 部分。这非常重要,因为它将在最终应用程序构建中使用。

现在,让我们告诉我们的应用程序我们有一个新的插件。像之前看到的其他插件一样,打开你的 Aurelia 配置文件 (aurelia.json) 并添加一个新的插件部分:

{
    dependencies: [
    // Some content here
      {
        "name": "aurelia-dialog",
        "path": "../node_modules/aurelia-dialog/dist/amd",
        "main": "aurelia-dialog"
      }
    // Some content here too
    ]
  }

我们已经配置了我们的 index.html 文件以使用手动引导;如果没有,请确保它有一个包含 aurelia-app="main" 标签的 <body> 元素:

<body aurelia-app="main">
</body>

在你的应用程序配置文件 (main.js) 中,添加一个新的 plugin() 条目:

export function configure(aurelia) {
    aurelia.use
      .standardConfiguration()
      .developmentLogging()
      .plugin('aurelia-dialog'); // <<-- Add this plugin!

此外,如果你想给你的 modal 添加更定制的行为,你可以在 plugin() 管道中实现一些配置。可选地,你可以按照以下方式配置 aurelia-dialog 插件:

.plugin(PLATFORM.moduleName('aurelia-dialog'), config => { // <<-- PLATFORM.moduleName is mandatory if you are using webpack
        config.useDefaults();
        config.settings.lock = true;
        config.settings.centerHorizontalOnly = false;
        config.settings.startingZIndex = 5;
        config.settings.keyboard = true;
});

你已经准备好了!现在,是时候倾听我们的插件了!

将对话框组件添加到我们的应用程序中

让我们为我们的应用程序创建一个 UserRegister 模态。这看起来可能像这样:

import { inject } from 'aurelia-framework'
import { DialogController } from 'aurelia-dialog'

@inject(DialogController)
export class UserForm {
  user = { firstName: '',    
           lastName: '',
           age: 0
 };

  activate(user){
    user = user;
  }
}

这现在非常简单。记住,这个组件将是模态本身。模态是在主要内容之上显示的,所以现在我们将配置这个行为到我们的 Home 组件中。注意这个部分;它有点棘手,但我们确信你会觉得实现起来很容易。

首先,让我们从我们最近导入的 aurelia-dialog 插件中导入 DialogService

import {DialogService} from 'aurelia-dialog';

此外,让我们导入我们最近创建的 UserForm 组件:

import {UserForm} from './user-form';

现在,让我们配置对话框行为:

export class HomeComponent {
  static inject = [DialogService]; // <<-- Same as use the @inject annotation

  user = { firstName: 'Diego', lastName: 'Arguelles', age: 26 }

  constructor(dialogService) {
    this.dialogService = dialogService; <<-- We need to inject the service into our component
  }

  openModal(){
    this.dialogService.open({ viewModel: UserForm, model: this.user}).whenClosed(response => {
      if (!response.wasCancelled) {
        console.log('good - ', response.output);
      } else {
        console.log('bad');
      }
    });
  }
}

此外,<template> 文件需要包含一个 <button> 来触发 openModal() 方法:

<template>
   <button click.trigger = "openModal()">New user</button>
<template>

让我们解释一下 openModal() 方法:

首先,我们需要打开模态框。我们将为最近创建的用户对象设置一些默认值。此方法将返回一个 promise 对象。为什么?很简单,有了这个 promise,我们就能处理模态框内部触发的任何事件:

this.dialogService.open({ viewModel: UserForm, model: this.user}).then();

then() 语句内部,我们的 promise 将被定义为以下方式:

response => {
    //We will get the response value returned by the modal
    if (!response.wasCancelled) { 
        console.log('All OK - ', response.output); //Should output the recently created user info
    } else { 
        console.log('Something get wrong!'); 
    } console.log(response.output);
}

现在,让我们看看我们的 <template> 文件:

<template>
   <ux-dialog>
      <ai-dialog-body>
         <h2>User registration</h2>
         <input placeholder="User name" model.bind="user.firstName" />
         <input placeholder="User last name" model.bind="user.lastName" />
         <input placeholder="User age" model.bind="user.age" /> 
      </ai-dialog-body> 
      <ai-dialog-footer> 
         <button click.trigger = "controller.cancel()">Cancel</button> 
         <button click.trigger = "controller.ok(message)">Ok</button> 
      </ai-dialog-footer> 
   </ux-dialog> 
</template>

当然,我们可以自定义模态框的显示方式。例如,bootstrap 默认在模态框背景中添加 50% 的不透明度。为了得到相同的结果,将此 CSS 类包含在现有的或新的样式表中。根据你使用的 CSS 预处理器,如果需要,不要忘记导入:

ai-dialog-overlay.active {
      background-color: black;
      opacity: .5;
}

记住,我们正在覆盖来自 Aurelia 对话的现有类,所以你不需要在 <html> 组件中指定此类。

现在,你准备好使用动态对话框为你的应用程序添加更友好的行为。

动态值转换器 - 更少的代码,更多的功能

正如我们在本章开头所说的,我们的应用程序应该对全球所有用户都可用。你可以完全自由地根据你自己的目的来建模应用程序。也许你想实现需要付费才能访问的付费功能,因此你需要用用户的货币来表示成本。另一件好事是有一个自定义格式的日期,或者简单地添加一些数字转换、小数四舍五入等等。

你已经知道如何将 Aurelia ViewViewModel 组件之间的值绑定和插值。现在我们将看到如何改进数据绑定。加油!

问题 - 数据没有按照我们的需求暴露

我们可能遇到的一个常见问题是日期格式化。在其他代码语言,如 Java 中,你有一个像 SimpleDateFormat 这样的实用工具类,它将 Date() 对象转换为更友好的可读格式。在 JavaScript 中,我们有几个库来做这个工作,但它们并不容易调用。让我们看一个例子。

你在 ViewModel 组件中获取当前日期;然后,你将这个值传递到 View 层:

export class Example {
      constructor() {
        this.changeDate();
        setInterval(() => this.changeDate(), 3000); //<<-- This method will be executed each 3 seconds
      }

      changeDate() {
        this.currentDate = new Date(); //<<-- Get the current date
      }
}

在我们的 View 文件中,我们将 currentDate 的值映射为要显示的内容:

<template>
      ${currentDate}
</template>

当你运行示例时,你将在屏幕上看到以下输出:

Sun Feb 25 2018 14:06:37 GMT-0300 (-03) 

好的,我们可以做得更好;现在是时候调用我们的值转换器了,但究竟什么是值转换器?Aurelia 文档解释得很好:

"值转换器是一个类,其责任是将视图模型值转换为适合在视图中显示的值,反之亦然。"

话虽如此,让我们为了示例目的创建一个值转换器文件。由于我们正在处理 Date() 值,我们将使用 moment 插件。

如果你没有在依赖树中找到它,只需从 npm 存储库导入它:

npm install moment --save 

然后,首先,在我们的值转换器文件中导入这个库:

import moment from 'moment';

export class DateFormatValueConverter {
     toView(value) {
       return moment(value).format('M/D/YYYY h:mm:ss a');
     }
}

好的,现在让我们解释一下它是如何工作的:

  • 你知道 Aurelia 是一个基于约定的配置框架。换句话说,如果你以ValueConverter结尾命名这个类,框架将使用这个类作为自定义值转换器,而无需任何更多配置。

  • toView()方法是从 Aurelia 的ValueConverter接口继承的。它定义了数据流向的方向,如果是从ViewModelView,或者相反,你有fromView()方法。

  • 这些值转换方法可以接收多个参数。

现在,我们只需要像导入其他依赖项一样在我们的View文件中导入这个值转换器,使用<require>标签:

<template>
      <require from="./date-format"></require> <<-- Path to your value converter

现在,我们需要将转换器添加到我们的绑定语法中:

<template>
      <require from="./date-format"></require>
      ${currentDate | dateFormat} <br/> <<-- Name mapped for our value converter
</template>

现在,刷新你的浏览器窗口:

2/25/2018 2:25:36 pm

好多了,对吧?嗯,同样的动态也可以应用到数字格式转换器、货币等。让我们使例子更复杂一些——如果我们需要在整个应用程序中显示多种日期格式怎么办?我们应该为每个需要的格式定义一个值转换器文件吗?这是一个有效的选项,但不是最有效的方法。你还记得我们说过值转换器接口方法可以接收多个参数吗?那么,如果我们把日期格式也作为参数发送会怎样呢?让我们尝试看看会发生什么:

toView(value, format) {
        return moment(value).format(format);
}

如果没有提供,你可以指定一个默认格式:toView(value, format = 'M/D/YYYY'){ ... }

很好,我们的格式化器现在接受format模式作为参数。这不是 Aurelia 的魔法;这是因为我们正在使用一个很好的 JavaScript 库moment.js,它允许我们执行这类操作。

现在,在我们的View文件中,我们可以添加我们需要的任何时间格式:


${currentDate | dateFormat:'h:mm:ss a'} <br/>
${currentDate | dateFormat:'M/D/YYYY h:mm:ss a'} <br/>
${currentDate | dateFormat:'MMMM Mo YYYY'} <br/>

现在,看看你的浏览器窗口:

2:33:11 pm 
2/25/2018 2:33:11 pm 
February 2nd 2018 

非常好。现在,让我们看看一个更复杂但更常见用法的例子——数组排序。

你知道如何从一个后端服务检索数据;通常,这些数据作为array对象检索,并在View文件中显示为列表。这都很好,但如果我们需要根据某些属性对这些值进行排序怎么办?看看代码示例:

export class ArraySortingValueConverter {
      toView(array, config) {
        let sorter = (config.direction || 'ascending') === 'ascending' ? 1 : -1;
        return array.sort((a, b) => {
          return (a[config.propertyName] - b[config.propertyName]) * sorter;
        });
      }
}

我们在做什么?让我们来解释一下。

我们接收两个参数,一个是数组,另一个是配置属性。配置属性是一个包含两个值的对象:config.direction,它可以取这两个选项之一:升序或任何其他字符串。根据这个,排序器可以按递增的值1或按递减的-1进行降序排序。然后,在返回语句中,我们使用数组本身的排序函数,并将匿名函数作为参数发送,用于比较配置对象中映射的config.propertyName值。

这是我们从某些后端服务检索数据的方式:

import {HttpClient} from 'aurelia-http-client';

export class Example {
      users = [];
      activate() {
        return new HttpClient()
          .get('https://api.ourorganization.com/users')
          .then(response => this.users = response.content);
      }
}

目前还没有什么奇怪的地方。现在,让我们检查一下View文件:

<template>
      <require from="./array-sort"></require> <<-- Import your value converter

      <div class="row">
        <div class="col-sm-3" repeat.for="user of users | arraySorting: { propertyName: 'code', direction: 'descending' }">
            ${user.firstName}
          </a>
        </div>
      </div>
</template>

这很美。我们不需要添加任何 JavaScript 函数或奇怪的配置来开始使用这个非常有用的功能。

将自定义行为绑定到我们的应用程序

让我们继续探索 Aurelia 的特殊功能。在上一个部分中,我们看到了值转换器,并且很难不将这个功能与 Aurelia 框架的绑定引擎联系起来。也许你认为这两个功能有很多共同之处,好吧,实际上并不是很多。让我们开始解释绑定引擎是如何工作的。

Aurelia 框架中的视图资源可以分为四个类别:

  • 值转换器

  • 自定义属性

  • 自定义元素

  • 绑定行为

我们将只关注最后一个。这并不是因为其他的不重要,而是因为我们首先了解它是如何工作的,然后再探索其他类别会更好。不用担心,值转换器已经涵盖了,你将对这两个功能之间的区别有一个更清晰的认识。

值转换器在ViewViewModel(或反之)之间充当着桥梁拦截器的角色。绑定行为超越了这一点——它在整个组件生命周期中完全访问绑定实例。这使得我们可以修改绑定行为,例如修改绑定节流时间,或者添加对值更新的自定义。

记住,Aurelia 是一个双向绑定框架,所以你不需要担心ViewViewModel之间的数据同步。这是如何完成的?Aurelia 有一个预定义的节流机制,默认情况下每 200 毫秒更新一次值。可以更新吗?是的,Aurelia 让我们可以自由地根据我们的需求管理这个值。类似于value-converters的语法,我们需要在需要的地方调用绑定行为,即在<template>文件中:

    <input type="text" value.bind="query & throttle:850">

你注意到&符号了吗?这是与已涵盖的值转换器的第一个区别。当我们需要定义一个绑定行为时,我们使用&通配符告诉框架。还有一点你必须注意,我们可以向绑定行为发送参数。只需在行为声明后添加:符号并发送值即可。是否可以发送多个参数?是的。如何?看看例子:

<input type="text" value.bind="query & customBehavior:arg1:arg2:arg3">

此外,你可以在一个元素中声明多个行为:

${value | upperCase & throttle:800 & anotherBehavior:arg1:arg2}

同样,你可以在ViewViewModel之间定义更新时间间隔;你还有一个有趣的绑定行为称为防抖。我们可以将这个行为与节流放在同一类别中,但区别在于它不是计算更新时间,而是在指定的时间间隔内没有变化时,防止绑定更新。

你可能会觉得这个功能更有用;让我们通过一个实际用例来解释它。

在我们的 FIFA 世界杯应用程序中,最被需求的功能之一可能是一个搜索输入,更具体地说,是一个自动完成。你已经知道自动完成组件应该根据用户的输入值检索数据。当你开发这个功能时,最大的问题是“我们应该在何时触发自动完成的searchByKey()方法?在每次按键时?当输入长度大于23时?”。真的,这是一个难题;你的应用性能直接受到影响。

这是思考防抖的好时机。与其每次用户输入值时都触发searchByKey()方法,不如在用户输入搜索键后的一段时间内触发它:

<input type="text" value.bind="teamCountry & debounce:1000">

你还可以使用其他非常有用的绑定行为,比如oneTime。默认情况下,Aurelia 将预配置的双向绑定设置到每个ViewModel属性上。最大的问题是“我们是否真的需要在组件的每个属性上激活双向绑定?”大多数情况下,答案是“不”。这就是oneTime出现并成为我们的性能优化伙伴的地方。真的,它对应用性能有直接影响吗?是的。为了启用双重绑定,Aurelia 需要实现多个观察者来寻找组件属性中的任何变化。使用oneTime,我们只需告诉应用——将这个属性映射到我的视图并忘记它:

 <span>${score & oneTime}</span>

当然,还有许多更多的预定义绑定行为可以探索,但如果你记得,我们在第一个例子中使用了定义为customBehavior的一个绑定行为。你注意到这一点了吗?猜猜看,Aurelia 允许你定义自己的绑定行为,现在是时候学习了。

与自定义值转换器类似,你可以创建自定义绑定行为。查看以下示例:

export class DynamicExpressionBindingBehavior {  

  bind(binding, source, rawExpression) {
    console.log('Binding : '+rawExpression)
  }

  unbind(binding, source) {
    console.log('Unbinding ')
  }
}

就像最后的例子一样,让我们解释一下代码在做什么。

首先,你必须知道,与值转换器中的toView()fromView()方法类似,自定义绑定行为需要实现两个方法:bind(binding, src, expressions...)unbind(binding, src)

bind()方法中,我们正在操作作为参数传递的用户输入值。你必须知道的是,在bind()方法中,前两个参数是由 Aurelia 发送的。其他参数可以是一个或多个自定义参数;在这种情况下,rawExpresions

unbind()方法只是确保当我们的数据处理结束时,我们的绑定行为返回到正常状态。

看起来很简单?是的,例子看起来很简单,但实际实现会更难理解。别担心,Aurelia 框架通常提供的预定义绑定行为集合对于每个应用程序的目的来说已经足够了。

提高我们的应用程序表单 – 验证器

我们知道 Aurelia 的绑定引擎是如何工作的。我们也知道如何拦截和自定义绑定行为。我们还知道如何根据我们的需求在View-ViewModel层之间转换数据。只是还有一些事情悬而未决,aurelia-validation插件就是其中之一。当你需要用户提供的数据时,你必须预期任何情况。用户不了解你的应用。他会做任何他想做的事情,你需要为此做好准备。你需要确保用户提供的数据至少是后端服务期望的正确格式。你需要过滤正确的值,并向用户发送警报,告诉他们哪些值是错误的以及如何修复它们。我们经常需要站在用户的角度思考。软件开发不仅仅是编程,不仅仅是创建表单和存储/检索数据。我们需要使我们的应用具有容错性,就像我们之前说的那样,总是考虑最坏的情况。只需想象一下——你正在开发一个需要执行某些计算的应用。你有三个输入:abc;你需要计算总和。听起来很简单,对吧?我们有两个用户输入了以下内容:

User 1 : value for a)33; b)23; c)32

你按下提交按钮,得到正确的总和——88。很好,应用完成了它的目的。让我们看看第二个用户会输入什么:

User 2: value for a)49 b)34j c)12

正如你所见,最后一个用户错误地按下了j字符。它会依赖于应用错误吗?不。我们的操作将会执行,JavaScript 不是一种强类型语言,所以它会以以下方式操作:

49 + "34j" + 12 = "4934j12"

用户将看到这个值,我们确信他们永远不会再次使用你的应用。

准备战斗——获取验证插件

就像我们可能安装的其他插件一样,安装和配置步骤非常相似。如果你是第一次执行此操作,只需执行以下步骤:

如果你使用 NPM 作为包管理器,使用以下方法:

npm install aurelia-validation --save

或者,如果你是 JSPM 用户,使用以下方法:

jspm install aurelia-validation

现在,打开我们配置应用插件的main.js文件。在configure()函数中,添加新的插件:

export function configure(aurelia) {
    aurelia.use
      .standardConfiguration()
      .developmentLogging()
      .plugin('aurelia-validation'); // <<-- Add this plugin!

最后,打开你的aurelia.json文件并添加以下插件声明:

    {
      "name": "aurelia-validation",
      "path": "../node_modules/aurelia-validation/dist/amd",
      "main": "aurelia-validation"
    }

我们准备好了。让我们开始吧!

第一步——定义我们的规则

Aurelia 验证插件基于标准规则。我们需要使用ValidationRules类定义我们自己的规则集。这个类有一些静态方法,接收我们的值并验证输入值是否符合我们的要求。此外,一个验证规则必须有一个预定义的格式。我们将解释的第一个方法是ensure()

 ValidationRules.ensure('myValidatedProperty')

此方法接受一个参数,即我们想要验证的属性名。此外,如果你正在验证一个对象,你可以传递匿名函数作为参数:

 ValidationRules.ensure(u => u.firstName)

我们将要解释的第二种方法是 displayName()。这不是必需的,但如果需要在验证信息中以一种预定义的格式显示此属性,则很有用;考虑以下示例:

 ValidationRules.ensure(u => u.firstName).displayName('User name')
Error message: The user name is required.

最后,我们需要定义将应用于该字段的规则集;以下是一些最常用的规则:

  • required() 阻止用户提交空或空白值

  • matches(regex) 帮助我们确保输入值符合预定义的格式,这在日期字段中很常见

  • email() 是确保电子邮件格式正确的一种简单方法

  • minLength(length)maxLength(length) 验证字符串属性的长度

如果我们的用户名不能为空,验证规则将是这样的:

    ValidationRules.ensure('u => u.firstName').displayName('First name')
        .required().withMessage(`\${$firstName} cannot be blank.`);

你注意到有什么不同吗?是的,我们正在使用 withMessage() 方法来自定义我们的验证错误信息。事情变得更有趣了。

如果您需要这些验证规则仅适用于一个对象,请不要担心,Aurelia 已经解决了这个问题。您需要标记您想要应用规则的对象;示例是自解释的:

   // User.js class inside our models folder
    export class User {
      firstName = '';
      lastName = '';
    }

   export const UserRules = ValidationRules
      .ensure('firstName').required()
      .ensure('lastName').required()
      .on(User);

我们几乎准备好了。现在,我们需要使用最近创建的验证规则来配置我们的表单控制器:

import { inject, NewInstance } from 'aurelia-dependency-injection';
import { ValidationController } from 'aurelia-validation';
import { User, UserRules } from '../models/User'

@inject(NewInstance.of(ValidationController))
export class UserRegisterForm {
    constructor(userValidationController) {
      this.user = new User(); // 1
      this.formValidator = userValidationController; //2
      this.formValidator.addObject(this.user, UserRules); //3
    }
}

你可能想知道为什么我们需要这个 NewInstance.of() 语句?嗯,对于每个我们应用的验证规则,我们需要一个单独的控制器来验证它。所以,通过这个语句,我们只是确保创建了一个新的 ValidationController 实例。

现在让我们解释一下构造函数方法内部发生的事情:

  • 第 1 行:我们正在创建一个新的 User 对象实例,以便在我们的表单中使用其属性。

  • 第 2 行:我们将新的 ValidatorController 实例分配给我们的 formValidator 对象。

  • 第 3 行:我们告诉 formValidator,评估的对象是我们的用户实例,并将使用导入的 UserRules

配置我们的 formValidator 的另一种方法是定义 validate() 方法内部的属性和规则:

formValidator.validate({ object: user, propertyName: 'firstName', rules: myRules });

在我们的 submit() 方法中,我们只需要添加以下内容:

formValidator.validate()
      .then(result => {
        if (result.valid) {
        // validation succeeded
      } else {
        // validation failed
      }
});

最后,我们需要告诉我们的模板验证器将被放置在哪里:

    <input type="text" value.bind="user.firstName & validate">

    <input type="text" value.bind="user.lastName & validate">

第一个值将被传递给 ensure() 函数作为参数。嘿,等一下!我们需要指定错误信息将放置的位置!嗯,这真的很简单,我们会实现一个错误列表如下:

<form>
      <ul if.bind="formValidator.errors">
        <li repeat.for="error of formValidator.errors">
          ${error.message}
        </li>
      </ul>
</form>

或者,如果您想在错误的 input 元素旁边显示消息,您可以使用 <span> 标签和其他非常有趣的自定义属性:validation-errors

<div validation-errors.bind="firstNameErrors"> 
    <label for="firstName">First Name</label>
    <input type="text" class="form-control" id="firstName"
               placeholder="First Name"
               value.bind="user.firstName & validate">
    <span class="help-block" repeat.for="errorInfo of firstNameErrors">
       ${errorInfo.error.message}
    </span>
</div>

validation-errors 属性包含有关指定元素(在这种情况下为 firstNameErrors)的所有验证错误。

现在,开始在您的应用程序表单中添加验证规则吧!下一节见!

操作 DOM – 自定义属性

我们几乎完成了 Aurelia 最常用的高级功能。现在,是时候探索属于绑定引擎插件的其它类别了——自定义属性究竟是什么?让我们用一种非常简单的方式来解释——你知道 HTML 标签,例如 <div><input><span>。你也知道每个元素都有属性,如 classtypestyle。现在,我们可以添加更多属性来使元素更可定制,并添加更高级的行为。让我们看一个例子。

我们也看到了值转换器,但你难道不认为如果我们为任何元素实现一个自定义属性来执行这个操作会非常酷吗?考虑一下这个:

<label datetime=”format:YYYY-MM-DD HH:mm”>${match.date}</label>

此外,match.date 将是一个简单的 Date() JavaScript 对象,没有任何格式。为什么我们需要完成这个?请注意,我们确信,到这一点,了解基本的绑定概念,你会发现它非常容易。

首先,创建一个类来配置你的 customAttribute

import {customAttribute, bindable, inject} from 'aurelia-framework';
import moment from 'moment';

@inject(Element, moment); <<-- We already know how moment js works
@customAttribute('datetime'); <<-- The attribute name to refer it
@bindable('format'); <<-- The property we pass as parameter
export class Datetime {

    constructor(element, moment) {
        this.element = element;
        this.moment = moment;
    }

    bind() {
        this.element.innerHTML = moment(this.element.innerHTML).format(this.format);
    }
}

Element 正在做什么?很简单——它帮助我们指向我们想要应用自定义属性的正确元素。接下来,我们只需要将我们的文件调用到所需的模板中。

定义自定义属性的另一种非常有趣的方法是使用已知的约定优于配置功能:

import {bindable, inject} from 'aurelia-framework';
import moment from 'moment';

@inject(Element, moment);
export class DatetimeCustomAttribute {

    @bindable format; // <<-- The value property can also be placed inside the class declaration

    constructor(element, moment) {
        this.element = element;
        this.moment = moment;
    }

    bind() {
        this.element.innerHTML = moment(this.element.innerHTML).format(this.format);
    }
}

现在,在我们的 View 文件中:

<require from="./datetime"></require>
<label datetime="format:YYYY-MM-DD HH:mm">${match.date}</label>

如果我们需要这个自定义属性对所有我的应用程序组件都可用怎么办?好消息是——你可以在应用程序配置文件(main.js)中将它配置为全局资源。

识别 configure 方法并添加一个指向我们最近创建的自定义属性的全局资源:

export function configure(aurelia) {
    aurelia.globalResources(
        "./datetime"
    )
}

到目前为止,你必须觉得这些功能非常容易学习,这是因为你了解更多帮助理解框架工作原理的高级功能。最后一节再见!

理解计算属性的工作原理

欢迎来到本章的最后一节!你可以认为自己是一个具有强大前端技术知识的全栈程序员。如果你注意到,Aurelia 用于实现不同功能的理念是基于每个网络应用都需要处理的常见问题,无论它使用的是哪个框架。此外,作为开源工具,不同的插件基于其他工具,这些工具实际上支持其他框架插件,如 Angular。

现在,我们将解释的最后一个特性是关于计算属性。我们可以用一句话来总结它:

计算属性是在 ViewModel 层通过 JavaScript 函数预先处理的属性。

让我们看看一个非常简单的实际应用——你正在开发一个页面,该页面将 ${firstName}${lastName} 作为单个值——${completeName} 显示。

一个常见的解决方案是创建一个 JavaScript 函数来连接这两个值并将其映射到 ViewModel 属性。这是有效的,但 Aurelia 提供了一个更好的解决方案——aurelia-computed 插件。这个插件提高了数据绑定计算属性的性能。

你还记得我们在第一章中提到的getter/setter函数吗?现在是时候使用它们了。

此插件使用 Aurelia 的 JavaScript 解析器来解析属性的 getter 函数体,并检查结果以进行可观察性检查。如果 getter 函数是可观察的,则返回一个专门的观察者给 Aurelia 的绑定系统。当 getter 函数访问的属性发生变化时,观察者会发布更改事件。

让我们看看一个例子:

// "firstName" and "lastName" will be observed.
get completeName() {
  return `${this.firstName} ${this.lastName}`;
}

目前还没有什么特别之处。这个函数正在使用dirty-checking来绑定completeName计算属性。

为什么是脏的?这是因为观察者策略不会等待用于检索completeName属性的两个值所做的任何更改。这意味着 getter 函数将在你的组件生命周期中多次执行。我们应该将其视为一个问题吗?实际上不是,但如果你的应用程序变得更大,并且你有许多计算属性,你的性能可能会直接受到影响。那么,Aurelia 的解决方案是什么?只是一个注解——@computedFrom

import {computedFrom} from 'aurelia-framework';

    export class User {
      firstName = 'Diego';
      lastName = 'Arguelles';

      @computedFrom('firstName', 'lastName')
      get completeName() {
        return `${this.firstName} ${this.lastName}`;
      }
    }

Aurelia 的绑定系统会观察指定的属性,并在任何属性发生变化时重新评估绑定。aurelia-computed插件简单地自动化依赖识别,并且能够支持更复杂的场景,例如观察属性路径。

此功能的另一个常见用途是检索当前登录用户的数据。我们可以定义一个布尔属性来告诉我们组件用户是否已登录,并根据此信息显示真实用户名或只是访客

// "isLoggedIn", "user" and "user.userName" will be observed.
@computedFrom('userName')
get userName() {
  return this.isLoggedIn ? this.userName : '(Visitor)';
}

摘要

当然,这是一章非常广泛的章节。我们建议对认为更重要的主题进行第二次阅读,正如之前所说,记住如果你想对每个解释的功能有一个完整的理解,你必须进行研究并做概念验证,以获得最佳选项和体验。由于我们应用程序的性质,FIFA 世界杯是一个全球性事件,因此你需要使其对所有国家都可用,i18n帮助我们非常容易地处理国际化。如果你需要在组件之间共享属性或触发事件,EventAggregator是最佳选择。Aurelia 提供了一系列非常实用的绑定行为,使你的代码更加整洁、易于理解和维护。值转换器、验证器、计算属性只是允许开发者减少代码的几个绑定行为。记住,许多这些功能依赖于第三方库,所以不要忘记下载它们并将它们配置到你的项目包中。

我们真的确信这一章是最有趣的...直到现在!下一章见!

第八章:安全性

安全性对于你计划构建的每一个应用程序都至关重要。安全性是一个非常复杂的话题,应该考虑最佳实践和标准来分析和实施。开放网络应用安全项目OWASP)组织是一个全球性的非营利组织,专注于提高应用程序的安全性。

所有应用程序至少实现了一个简单的安全层,称为身份验证和授权层,它负责根据提供给应用程序的凭证限制某些访问和功能。尽管本章的重点是如何保护我们的 Aurelia 应用程序,但我们将实现一个简单的身份验证和授权 API 作为示例,以集成到我们的 Aurelia 网络应用程序中。

在本章中,我们将涵盖以下主题:

  • 理解 JSON Web Tokens

  • 自定义身份验证和授权

  • 介绍 Auth0

  • 使用 Auth0 进行社交登录

  • 单点登录

理解 JSON Web Tokens

我们实现了一个 RESTful API,该 API 为我们的 Aurelia 网络应用程序提供显示信息。这个 API 没有任何安全机制,所以如果任何恶意用户获取端点 URL,他们可以针对我们的应用程序运行恶意脚本并破坏我们的应用程序。因此,我们应该拒绝任何未经授权用户执行的管理操作。

我们的应用程序应该实现一个机制来管理用户的访问和权限。实现身份验证和授权有许多方法。对于我们的应用程序,我们将使用一个行业中的开放标准,称为JSON Web TokenJWT)。

JWT

JWTs 是一种开放、行业标准的 RFC 7519 方法,用于在双方之间安全地表示声明。我们使用它们的方式很简单。首先,我们通过提供用户名或密码来验证后端服务器。如果我们的凭证正确,后端服务器将生成一个包含应在客户端使用本地存储机制持久化的用户信息的令牌。这个 JWT 应该在每次请求中传递给服务器,以便服务器可以识别用户是谁以及该用户有什么权限;有了这些信息,服务器允许或拒绝用户请求。

让我们了解它是如何工作的。导航到 jwt.io/;向下滚动一点,你会找到一个类似于以下图像的示例部分:

图片

从右到左读取信息。在右侧,我们有解码部分,它包含三个部分:

  • 头部:包含用于加密令牌的算法信息

  • 负载:我们将定义并用于我们应用程序中的信息片段,例如,用户信息

  • 验证签名:令牌的签名;我们将定义一个 密钥值 来加密我们的令牌

在左侧的编码中,我们可以看到基于前面提到的三个部分信息的最终令牌加密结果。

如你所猜,这个加密值是在我们的后端服务器中计算的。每次用户登录应用程序时,都会将此令牌发送给用户。他们在客户端保存此令牌,然后在每次请求中使用 Authorization HTTP 头将其发送。让我们看看这一切是如何工作的。

自定义身份验证和授权

现在我们来了解应用安全背后的两个主要概念,这些概念你必须在你所有的项目中实现。

实现身份验证

身份验证是一个验证给定用户身份并检查用户是否有有效凭证访问我们的应用程序或后端 API 的过程。通过身份验证,我们限制了非我们应用程序成员的访问。

我们将创建一个基本的身份验证 API,因为本书的目标是向你展示如何保护你的 Aurelia 应用。我们不会深入探讨后端实现的细节。我们将创建一个硬编码的身份验证流程,但你可以使用 Mongoose 将其与数据库集成,如第六章“将我们的数据存储在 MongoDB”中所述。

因此,让我们动手实践。打开后端项目,在 routes 文件夹中创建一个名为 security-api.js 的新文件,并编写以下代码:

const express = require('express')
const api = express.Router()

const logIn = (username, password) => {
    // Logic Here    
}

api
  .route('/auth')
  .post((req, res, next) => {
    // Logic here
  })

module.exports = api

首先,我们导入 express 并创建 Router 类的一个实例。其次,我们定义一个名为 logIn 的函数,我们将在这个函数中实现用户认证和生成 JWT 的逻辑。然后,我们将定义一个 /auth 路由来处理 POST 端点。最后,我们将 API 导出以在主 server.js 文件中使用。

身份验证逻辑

让我们创建一个简单的身份验证逻辑。后端期望提供一个用户名和密码,其值为管理员。如果提供了这些值,它将返回一个有效的令牌;否则,它将返回 null。对以下代码进行以下更改:

const express = require('express')
const api = express.Router()

const logIn = (username, password) => {
    if (username === 'admin' && password === 'admin') {
const userData = {
 name: "Admin"
 }
 return generateToken(userData)
 }
 return null
}

api
...

代码非常直接。首先,我们在简单的 if 条件中比较值,并创建了一个 userData 对象,该对象将包含用户信息,在这种情况下只提供了一个名称值。最后,我们将调用一个 generateToken 函数并将 userData 传递给它以返回一个有效的令牌。

让我们实现 generateToken 函数。

生成令牌

我们将使用一个名为 jsonwebtoken 的 NPM 模块来生成 JWT。打开一个新的控制台,在 wc-backend 文件夹中输入以下命令来安装模块:

$ npm install jsonwebtoken --save

安装完成后,打开 security-api.js,然后按照以下方式导入我们的库:

const express = require('express')
const jwt = require('jsonwebtoken')
const api = express.Router()

...

在我们的文件中导入依赖项后,让我们实现 generateToken 函数。应用以下更改:

...
const logIn = (username, password) => {
    if (username == 'admin' && password == 'admin') {        
        let userData = {
            name: "Admin"
        }        
        return generateToken(userData)        
    } else {
        return null
    }    
}

const generateToken = userData => {
 return jwt.sign(userData, "s3cret", { expiresIn: '3h' })
} ...

就这样!让我们理解一下代码。我们调用jwt对象的sign函数来创建我们的令牌。我们将以下信息传递给该函数:

  • userData: 我们想要标记化的信息

  • secret: 用于加密和验证令牌的秘密值

  • expiration: 令牌的过期日期

现在,我们已经准备好了认证逻辑。为了完成我们的实现,我们必须通过我们的 REST 控制器提供此逻辑。

认证 REST 控制器

我们已经定义了负责将我们的逻辑作为 REST 端点提供的路由,所以唯一需要做的步骤就是调用我们的logIn函数。继续并应用以下更改:

...

const generateToken = (userData) => {
   return jwt.sign(userData, "s3cret", { expiresIn: '3h' })
}

api
  .route('/auth')
  .post((req, res, next) => {
    let { username, password } = req.body
 let token = logIn(username, password)
 if (token) {
 res.send(token)
 } else {
 next(new Error("Authentication failed"))
 }
  })

module.exports = api

首先,我们从req.body对象中提取usernamepassword。之后,我们调用logIn函数并将结果存储在token变量中。如果token不为空,我们通过调用res.send函数返回一个成功的响应。如果令牌为空,我们将一个Error对象传递给下一个参数,这将引发一个全局异常并返回一个失败的响应。

最后,我们必须修改server.js文件以将我们的 API 注册到 express 中,如下所示:

const express = require('express')
..
const seurityApi = require('./src/routes/security-api')
const mongooseConfig = require('./src/config/mongoose-connection')
const app = express()

app.use(bodyParser.json())
app.use(teamsApi)
app.use(seurityApi)
...

现在,我们已经准备好测试我们的实现了。在一个新的终端窗口中,运行以下curl命令:

$ curl -X POST -H "Content-type: application/json" -d '{"username":"admin", "password":"admin"}' localhost:3000/auth

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQWRtaW4iLCJpYXQiOjE1MTk1NzYwMDEsImV4cCI6MTUxOTU4NjgwMX0.4cNGYgz_BZZz5GEfN6MS3pkreGTkUBqJS1FZVC3_ew

如果一切实施得当,你应该会收到一个加密的 JWT 作为响应。

太棒了!现在是时候玩授权了。继续阅读!

实现授权

通过认证,我们确保我们的应用程序是由一个拥有有效凭证的授权人员使用的。在您未来构建的大多数应用程序中,您会发现有不同权限的用户。例如,一个学生可能有查看成绩的权限,但学生无法修改成绩。否则,一个教师可以更新成绩并访问学生无法访问的其他功能。

我们将使用另一个 NPM 模块express-jwt-permissions来实现授权。通过使用此模块,我们将能够以非常简单的方式实现授权。打开security-api.js文件并应用以下更改:


const logIn = (username, password) => {
    if (username == 'admin' && password == 'admin') {

        let userData = {
            name: "Admin",
            permissions: ["admin:create:match", "admin:update:scores"]
        }

        return generateToken(userData)

    } else {
        return null
    }

}

就这样!之前的库将检查 JWT 是否定义了permissions属性。如果是这样,我们将提取此信息并限制没有管理员权限的用户访问。在下一节中,我们将实现管理员 REST 控制器并查看我们是如何限制对更多细节的访问的。

话虽如此,我们已经准备好开始实施管理员 API 了!

创建管理员 API

为了理解授权是如何工作的,让我们实现一个基本的仅由我们网站管理员访问的 Admin API。我们的应用程序有两种主要类型的用户:

  • 正常: 此用户能够查看特色比赛和得分

  • 管理员: 此用户负责创建新比赛和更新得分

我们将在后端使用两个开源 NPM 模块来管理限制工作流程。以下图表更详细地解释了这个流程:

图片

所有的操作都是从用户请求开始的,该请求试图访问一个受限的端点。后端首先验证 HTTP 请求中是否存在有效的令牌;这个验证是由express-jwt模块执行的。其次,如果请求有一个有效的令牌,流程将检查该令牌是否有访问受限端点的有效权限;这个验证是由express-jwt-permissions执行的。如果请求有一个有效的令牌并且有权限,用户请求将能够访问受限端点并执行操作。

管理比赛

要创建一个比赛,我们需要创建一个有效的 Match 数据库模式。我们已经知道如何使用 Mongoose 来做这件事。让我们来做。

创建 Match 模式

src/models文件夹中创建match.js文件。然后,添加以下代码:

const mongoose = require('mongoose')

const MatchSchema = new mongoose.Schema({
    team_1: {
      type: String,        
      min: 3,
      max: 100,
      required: true
    },
   team_2: {
      type: String,
      min: 3,
      max: 100,
      required: true
    },
    score: {
      team_1: Number,
      team_2: Number
    }
})

module.exports = mongoose.model('match', MatchSchema)

我们定义了三个属性,前两个team_1team_2将存储两支参赛队伍的信息。比赛的score。这就是我们模型所需的所有内容。

创建 REST 控制器

让我们从在src/routes文件夹中创建一个名为admin-api.js的新文件开始。然后,编写以下代码:

const express = require('express')
const api = express.Router()

api
  .route('/admin/match/:id?')
  .post((req, res, next) => {

     // logic to create Match

  })
   .put((req, res, next) => {

     // logic to update Scores

  })

module.exports = api

你非常熟悉这种代码结构。首先,我们导入定义我们的 REST 控制器所需的模块。其次,我们创建一个/admin/match/:id?路由并定义创建新比赛的POST方法以及更新分数的另一个PUT方法。

注意路由定义;我们声明了一个名为:id的可选路径变量。为了使路径可选,我们在其名称后添加了?运算符。

到目前为止一切顺利。让我们来实现它们。

创建比赛

创建一个新的Match很简单。我们只需要导入Match模型并调用其内置的save方法,如下所示:

const express = require('express')
const api = express.Router()
const Match = require('../models/match')

api
  .route('/admin/match/:id?')
  .post((req, res, next) => {
      const match = new Match(req.body)
 match.save()
 .then(data => res.json(data))
 .catch(err => next(err) )
  })
   .put((req, res, next) => {

     // logic to update Match

  })

module.exports = api

首先,我们导入Match模型。然后,在POST方法中,我们创建一个新的Match对象并调用save函数。如果操作成功,我们通过res.json方法发送新的Match。为了测试我们的创建逻辑,我们需要配置server.js以使用我们的新 Admin API,如下所示:

const express = require('express')
...
const adminApi = require('./src/routes/admin-api')
const mongooseConfig = require('./src/config/mongoose-connection')
const app = express()

app.use(bodyParser.json())
app.use(teamsApi)
app.use(seurityApi)
app.use(adminApi)

app.use((err, req, res, next) => {

一旦我们应用了前面的更改,打开一个新的终端来测试一下:

$ curl -X POST -H "Content-type: application/json" -d '{"team_1": "Peru", "team_2": "Chile", "score": { "team_1": 20, "team_2": 0} }'  localhost:3000/admin/match/

{"__v":0,"team_1":"Peru","team_2":"Chile","_id":"5a94a2b8221bb505c92d801c","score":{"team_1":20,"team_2":0}}

太棒了!现在我们有了我们的创建逻辑,并且有一个很好的真实示例。

列出比赛

要列出我们的比赛,我们不需要安全措施,因为所有用户都应该能够获取应用程序中比赛的完整列表。所以让我们实现matches API。

因此,在src/routes文件夹中创建一个名为matches-api.js的新文件,并应用以下代码:

const express = require('express')
const api = express.Router()
const Match = require('../models/match')

api
  .route('/matches')
  .get((req, res, next) => {
     Match.find().exec()
        .then(matches => res.json(matches))
        .catch(err => next(err))
  })

module.exports = api

接下来,我们必须配置server.js文件以映射我们的Match API。在server.js文件中,应用以下更改:

...
const adminApi = require('./src/routes/admin-api')
const matchesApi = require('./src/routes/matches-api')
const mongooseConfig = require('./src/config/mongoose-connection')
const app = express()

app.use(bodyParser.json())
app.use(teamsApi)
app.use(seurityApi)
app.use(adminApi)
app.use(matchesApi)

...

太棒了!让我们来测试一下。打开一个终端窗口并执行以下命令:

$ curl localhost:3000/matches

[{"_id":"5a949f982c1fda05b8c5c00a","team_1":"Peru","team_2":"Chile","__v":0,"score":{"team_1":20,"team_2":0}}]

就这样!我们有了我们的公共Match API。

更新分数

要更新 score,我们需要查找现有比赛的 ID。如果找到一个有效的比赛,我们将应用更新。如果没有找到比赛,我们必须响应一个 404 not found HTTP 响应。否则,我们响应一个 Success 200 HTTP 响应。

让我们先创建 updateScore 函数,如下所示:

...
const Match = require('../models/match')

const updateScore = async (matchId, teamId) => {
 try {
 let match = await Match.findById(matchId)

 if (match == null) throw new Error("Match not found")

 if (teamId == 'team_1') {
 match.score.team_1++;
 } else {
 match.score.team_2++;
 }

 match = await match.save()
 return match

 } catch (err) {
 throw err
 }
}

api
  .route('/admin/match/:id?')
...

现在,让我们按照以下方式调用我们的 PUT HTTP 动词中的函数:

...
api
  .route('/admin/match/:id?')
  .post((req, res, next) => {
     ...
  })
api
  .route('/admin/match/scores/:id')
 .post((req, res, next) => {

 const matchId = req.params.id
 const teamId = req.body.teamId

 updateScore(matchId, teamId)
 .then(match => res.json(match))
 .catch(err => next(err))
 })
...

太棒了!让我们测试一下。执行以下 curl 命令来更新我们之前创建的比赛:

$ curl -X POST -H "Content-type: application/json" -d '{"teamId": "team_1" }'  localhost:3000/admin/match/scores/5a94a2b8221bb505c92d801c

{"_id":"5a94a2b8221bb505c92d801c","team_1":"Peru","team_2":"Chile","__v":0,"score":{"team_1":21,"team_2":0}}

注意,team_1(秘鲁队)现在在它的分数中有了 21 个进球,而智利队有 0 个。

太棒了!现在我们能够创建一个新的比赛并更新分数了。让我们使用 express-jwtexpress-jwt-permissions 来保护我们的 API。继续阅读!

保护 REST 控制器

现在是时候保护我们的 API 了。为了做到这一点,让我们首先安装我们的两个 NPM 模块。打开终端窗口并运行以下命令:

$ npm install --save express-jwt express-jwt-permissions

安装完成后,让我们验证我们的令牌。

验证令牌

每个 HTTP 请求都必须在授权头中发送 JWT。express-jwt 中间件将检查是否传递了有效令牌。如果是,请求将继续其流程,否则后端将响应一个未授权异常。

将以下更改应用到 admin-api.js 文件中:

...
const Match = require('../models/match')
const auth =require('express-jwt')

const updateScore = async (matchId, teamId) => {
  ...
}

api
  .route('/admin/match/:id?')
  .post(auth({ secret: 's3cret'}),
  (req, res, next) => {
    ...
  })
...

首先,我们开始导入 express-jwt 模块并创建一个新的常量 auth。其次,我们使用 auth 函数并传递一个具有 secret 属性的 JSON 对象;我们将使用与我们在 security-api.js 中签名令牌相同的密钥值:

return jwt.sign(userData, "s3cret", { expiresIn: '3h' })

auth 函数负责检查是否在授权头中传递了有效的令牌。让我们尝试不传递有效令牌创建一个新的 Match 实体。打开终端窗口并执行以下命令:

$ curl -X POST -H "Content-type: application/json" -d '{"team_1": "Peru", "team_2": "France", "score": { "team_1": 5, "team_2": 5} }'  localhost:3000/admin/match/ {"error":"No authorization token was found"}

如您所见,这次我们收到了一个错误消息,表示我们的后端 API 正在等待一个有效令牌,但没有提供。因此,为了应对这种情况,我们需要首先创建一个有效令牌。让我们通过调用我们的安全 API 并以管理员身份登录来创建一个有效令牌。执行以下命令:

$ curl -X POST -H "Content-type: application/json" -d '{"username":"admin", "password":"admin"}' localhost:3000/auth

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQWRtaW4iLCJpYXQiOjE1MTk2OTgyMjksImV4cCI6MTUxOTcwOTAyOX0.HQiz-NbBDBc9kVyBRNUeMsrDexEsk92WXoRyijNp1Rk 

为我创建了一个新的有效令牌。请注意,此令牌具有随机值,因此为您生成的令牌将完全不同。

一旦我们有了有效的令牌,我们再次尝试;然而,这次我们将使用 Authorization HTTP 头部传递令牌,如下所示:

$ curl -X POST -H "Content-type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQWRtaW4iLCJpYXQiOjE1MTk2OTgyMjksImV4cCI6MTUxOTcwOTAyOX0.HQiz-NbBDBc9kVyBRNUeMsrDexEsk92WXoRyijNp1Rk" -d '{"team_1": "Peru", "team_2": "France", "score": { "team_1": 5, "team_2": 5} }'  localhost:3000/admin/match/

{"__v":0,"team_1":"Peru","team_2":"France","_id":"5a94c2559e11b6089b2b265e","score":{"team_1":5,"team_2":5}}

太棒了!现在我们能够创建一个新的 Match。请注意我们传递令牌时使用的语法。我们使用了 Authorization: Bearer <token> 语法。

我们已经保护了我们的 API,但如果我们有两种不同类型的管理员怎么办?比如说,一组管理员只能添加 Match 实体,而另一组管理员只能更新分数。我们需要一种方式来管理这种角色分离。让我们学习权限是如何工作的。

验证权限

权限允许我们限制对一组资源的访问。你应该意识到,如果我们想保护我们的后端 API,仅进行身份验证是不够的。要实现权限,请打开admin-api.js文件并应用以下更改:

...
const auth =require('express-jwt')
const guard = require('express-jwt-permissions')()

const updateScore = async (matchId, teamId) => {
  ...
}

api
  .route('/admin/match/:id?')
  .post(auth({ secret: 's3cret'}),
    guard.check('admin:create:match'),
    (req, res, next) => {

      ...    

  })
...

首先,我们初始化一个guard常量。其次,我们调用guard.check;这个函数将在 JWT 中查找admin:create:match权限。记住,这些权限必须存在于令牌中。如果用户有权限,流程将继续,并创建新的Match。否则,我们将收到无法找到权限错误。

让我们尝试创建一个新的Match;执行以下命令:

$ curl -X POST -H "Content-type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQWRtaW4iLCJpYXQiOjE1MTk2OTgyMjksImV4cCI6MTUxOTcwOTAyOX0.HQiz-NbBDBc9kVyBRNUeMsrDexEsk92WXoRyijNp1Rk" -d '{"team_1": "Peru", "team_2": "Brasil", "score": { "team_1": 80, "team_2": 5} }'  localhost:3000/admin/match/

{"error":"Permission Denied"}

这很有趣!尽管我们已经进行了身份验证并传递了一个有效的令牌,但我们为什么不能创建Match?让我们看看用户令牌生成逻辑。打开security-api.js文件:

...
const logIn = (username, password) => {
    if (username == 'admin' && password == 'admin') {

        let userData = {
            name: "Admin"
        }

        return generateToken(userData)

    } else {
        return null
    }

}
...

正如你所见,我们的令牌没有定义权限。让我们通过添加正确的权限来解决这个问题:

...    
    let userData = {
       name: "Admin",
      permissions: ["admin:create:match"] 
    }
...

那就结束了。让我们再次登录以生成一个新的令牌,并测试一下。

首先,执行以下命令以生成一个有效的令牌:

$ curl -X POST -H "Content-type: application/json" -d '{"username":"admin", "password":"admin"}' localhost:3000/auth

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQWRtaW4iLCJpYXQiOjE1MTk2OTk4MzIsImV4cCI6MTUxOTcxMDYzMn0.cVTtJHcbQ2J76s6uRjuySCWq4dKXlNzAfInl0ZLgri 

太棒了!这个新的令牌包含了权限。接下来,让我们尝试再次通过传递这个新的令牌来创建新的Match

$ curl -X POST -H "Content-type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQWRtaW4iLCJpYXQiOjE1MTk2OTgyMjksImV4cCI6MTUxOTcwOTAyOX0.HQiz-NbBDBc9kVyBRNUeMsrDexEsk92WXoRyijNp1Rk" -d '{"team_1": "Peru", "team_2": "China", "score": { "team_1": 5, "team_2": 5} }'  localhost:3000/admin/match/

{"__v":0,"team_1":"Peru","team_2":"China","_id":"5a94c87987d2820a0d1931e7","score":{"team_1":5,"team_2":5}}

热狗!我们现在能够管理后端 API 中的身份验证和授权。当然,你可以通过在 MongoDB 数据库中保存用户集合并创建具有不同角色的不同用户来提高这种安全身份验证。此外,使用express-jwt-blacklist实现登出,以另一种方式,但出于本书的目的,我们对此基本实现感到满意。继续阅读!

介绍 Auth0

自己管理身份验证和授权可能变得非常困难。想象一下,你需要为 Web 应用程序、移动应用程序和桌面应用程序实现安全逻辑。甚至你的客户可能要求你将他们的应用程序集成到社交网络中,并使用多因素身份验证或无密码方法。尽管我们已经为我们的应用程序实现了安全措施,但我们鼓励你除非你正在创建一个非常简单的应用程序,否则不要自己编写安全代码。

因此,在本节中,我们将使用一个名为 Auth0(auth0.com)的流行服务来实现身份验证和授权。此服务将帮助我们增强我们的身份验证流程,例如:

  • 社交登录

  • 单点登录

  • 邮箱身份验证

  • 多因素

  • 无密码身份验证

  • 指纹登录

  • LDAP 集成

此外,Auth0 还提供监控和其他开箱即用的服务,这些服务将帮助我们管理我们的用户信息。

一个简单的例子

尽管我们为世界杯应用程序使用了自定义实现,但我们已经准备了一个简单的示例来向您展示如何使用 Auth0 与 Aurelia。您可以从 github.com/EriksonMurrugarra/AureliaAuth0 下载代码。

Auth0 实现了 JWT;这应该让您想起,因为我们使用 JWT 实现了我们的自定义 Auth0 实现。让我们首先在 Auth0 上创建一个免费账户。

创建账户

首先,导航到官方 Auth0 网站,auth0.com,并点击“注册”按钮:

图片

然后,填写表单中的电子邮件和密码,并点击“注册”按钮或使用您的社交网络账户:

图片

一旦完成注册过程,您应该会被重定向到您的管理仪表板。管理仪表板将允许您在几秒钟内配置可以实施的不同安全机制。让我们继续注册一个应用程序以生成一些有效的配置值,以便将我们的 Aurelia 应用程序连接到 Auth0。

注册 Auth0 客户端应用程序

如果您有与第三方服务提供商合作的经验,您可能会注意到,为了使用他们的服务,您必须注册一个应用程序以获取一些私钥,您将使用这些私钥来访问第三方提供商的资源。Auth0 也是如此;我们应该首先注册一个应用程序,然后使用生成的密钥来配置我们的应用程序。

在仪表板页面上,继续选择“应用程序”菜单并点击“创建客户端”按钮:

图片

点击“创建客户端”按钮后,填写以下表单中的应用程序名称,并选择客户端类型为单页应用程序;然后,点击“创建”:

图片

创建客户端后,将显示一个新的配置页面。导航到“设置”选项卡,您将看到以下配置值:

  • 名称:我们应用程序的名称

  • 域名:您在注册过程中之前注册的域名

  • 客户端 ID:一个独特的 ID,使您的应用程序独一无二

  • 客户端密钥:用于签署 Auth0 将生成的 JWT 的密钥值

  • 允许的回调 URL:当身份验证成功时,Auth0 将重定向到的 URL 列表

就这些。在我们探索应用程序的代码之前,让我们通过分析以下图表来了解 Auth0 如何管理身份验证:

图片

流程从用户想要登录应用程序时开始。你可能有一个导航栏,其中有一个按钮,点击后会触发一个 JavaScript 函数,该函数将调用 Auth0 JavaScript 登录函数。其次,用户将被重定向到内置的 Auth0 登录表单,并需要输入他们的凭据以进行注册。用户输入的凭据将由 Auth0 验证;如果 Auth0 找到具有提供的凭据的有效用户,它将生成一个有效的 JWT,并将其发送到用户/Aurelia 应用程序。此 JWT 将用于访问你的后端资源。请记住,你可以在后端使用客户端密钥属性来解密令牌。

探索我们的示例应用程序

首先,我们需要从 GitHub 仓库 github.com/EriksonMurrugarra/AureliaAuth0 下载源代码。让我们在你的首选文件夹中打开一个终端窗口,并运行以下命令:

$ cd /some/path
$ git clone https://github.com/EriksonMurrugarra/AureliaAuth0

下载源代码后,我们需要安装依赖项并运行应用程序。让我们进入源代码文件夹,并执行以下命令:

$ cd AureliaAuth0
$ npm install
...
$ au run --watch

Writing app-bundle.js...
Writing vendor-bundle.js...
Finished 'writeBundles'
Application Available At: http://localhost:9000
BrowserSync Available At: http://localhost:3001

让我们打开一个新的浏览器,并导航到 localhost:9000,这将显示一个简单的首页,在导航栏中有一个登录选项:

图片

太棒了!我们的应用程序已经启动并运行,但在成功登录之前,我们首先需要对其进行配置。请打开位于 src 文件夹中的以下 auth-service.js 文件。我们需要替换我们在 Auth0 上创建客户端应用程序时获得的配置值。在我们的例子中,这些值如下:

...  
auth0 = new auth0.WebAuth({
    domain: 'eriksonmurrugarra.auth0.com',
 clientID: 'LBmldq5O0XHPYz4SAyMr03ThgfMOiHs7',
 redirectUri: 'http://localhost:9000/callback',
 audience: 'https://eriksonmurrugarra.auth0.com/userinfo',
 responseType: 'token id_token',
 scope: 'openid'
  })
...

需要强调的是,redirectUri 应该在 Auth0 的应用程序设置中的允许回调 URL 中注册。

太棒了!保存更改并点击 LOG IN 按钮,将被重定向到以下页面:

图片

上一页由 Auth0 提供;你可能注意到 URL 已经更改。这意味着每次我们的用户被要求登录时,他们都会被重定向到 Auth0。由于你还没有创建任何用户账户,你首先需要注册。完成注册过程后,你将被重定向到主页:

图片

就这些了!现在你已经使用更安全的策略登录到应用程序。请记住,你永远不应该自己实现身份验证和授权。最佳实践是使用第三方服务。创建 Auth0 等服务的开发者肯定有多年创建比你自己更安全的身份验证和授权机制的经验。

使用 Auth0 进行社交登录

当人们面对任何注册表单时,背后有一个重要的真理。如果你的用户在他们的最喜欢的社交网络中已经填写了个人信息,而你让他们浪费有效时间再次填写这些信息,他们中的大多数都会讨厌你的应用程序。即使你试图为你的注册表单创造最佳的用户体验,他们也会避免使用它。那么,我们如何让我们的用户开心,并避免他们不得不进行这个可怕的注册过程呢?社交登录集成来拯救!

如果你需要将多个社交网络集成到你的应用程序中,实现社交登录可能是一项重复性的工作。为什么不使用一个现有的服务来帮助我们完成这个过程呢?Auth0 来拯救!

让我们在 3 分钟内实现社交登录。是的!你没看错,只需 3 分钟。请继续,导航到你的 Auth0 仪表板manage.auth0.com,然后导航到“连接/社交”菜单。接下来,激活你想要集成到认证流程中的社交网络,如下所示:

图片

如你所见,我已经为认证流程启用了 Google 和 Facebook。记住,你首先需要在 Facebook 和 Twitter 上注册一个应用程序,然后使用生成的密钥来配置社交登录方法。

我们几乎完成了。为了完成这个过程,我们只需要告诉我们的客户端应用程序我们想要启用社交登录。为此,让我们导航到“客户端”菜单,进入 MyAuth0App 客户端应用程序。然后,导航到“连接”标签,在社交部分启用 Facebook 和 Twitter,如下所示:

图片

一旦我们配置好了一切,下次当你的用户尝试登录应用程序时,他们将看到以下表单:

图片

太棒了!现在我们知道了如何集成我们的应用程序,让我们的用户能够登录我们的应用程序,并使用他们最喜欢的社交网络账户。

单点登录

如果你正在实施一个由不同分布式应用程序组成的大型企业解决方案,这些应用程序需要认证和授权,但需要使用相同的用户数据库,你将需要实现不同的流程来管理所有这些独立应用程序的认证。这种机制被称为单点登录SSO),它基本上会在你的任何应用程序中要求登录一次,并将重用生成的相同令牌在所有应用程序中。以下图表解释了这个流程:

图片

在前面的示意图中,有三个应用程序。让我们假设这三个不同的应用程序是由同一家公司开发的,员工使用这三个应用程序。想象一下,为了访问每个应用程序,员工必须使用不同的凭证登录到每个应用程序,或者他们可以选择为三个应用程序使用相同的用户名和密码。

如果这些应用程序共享相同用户信息,为什么我们的用户还需要再次登录到另一个应用程序呢?首先,用户将登录到服务器并检索一个有效的令牌。一旦第一个应用程序登录,它就可以将令牌作为 cookies 或存储在浏览器的LocalStorage中。当用户访问应用程序 02 时,应用应该检测到存在一个现有的令牌,并应该使用它来访问服务器,而无需请求凭证。

现在你已经了解了 SSO 的工作原理,你可以自由地自己实现 SSO 或使用外部服务。Auth0 对 SSO 提供了出色的支持。

摘要

在本章中,我们创建了一个自定义实现来管理认证和授权,以保护我们的 API 免受未经授权用户的使用。你已经看到,自己实现 Auth0 可能是一项艰巨的任务,并且需要比我们实现的多出更多的安全层。一个好的做法是在你的项目中使用外部服务来实现认证和授权。我们创建了一个简单的应用程序,它使用了一个最受欢迎的第三方服务,名为 Auth0。

我们还介绍了如何将社交登录集成到我们的应用程序中,但我们使用 Auth0 内置的社交连接功能来实现这一功能。你可以自己实现社交认证,但再次强调,最好将精力集中在你的应用程序登录上,而不是浪费时间实现那些可以迅速通过第三方服务实现的功能。

我们探讨了 SSO 在理论上的工作方式,并了解到它是一个简单的流程,即在所有不同的应用程序中重复使用用户令牌。

就这些!在下一章中,你将学习如何在 Aurelia 应用程序上应用端到端测试。继续阅读!

第九章:运行端到端测试

恭喜!你只需再迈出一小步,就能成为一名全栈应用开发者!目前,我们将停止讨论 Aurelia;你了解这个框架,并且对 JavaScript 作为编程语言的工作原理有很高的了解。现在,是时候扩展我们对全栈应用开发的了解。我们的 FIFA WC 2018 应用正在本地主机上运行,并且已经实现了一些单元测试。这足以确保它在 QA 或生产环境中工作吗?当然,不够。

单元测试非常重要,但它只能确保单个服务的正确功能。我们如何确保所有我们的应用程序(数据库、后端、前端以及任何其他外部服务)作为一个单一应用程序正确工作?这就是本章我们将学习的内容。

测试是当今所有开发者高度需求的一项技能。为什么?因为编程不再仅仅是编写代码,为了确保它在你的个人电脑上工作,你需要编写确保这种功能的代码,并且能够自动化测试过程以创建持续交付管道。

我们可以将测试阶段分为四个不同的级别,我们将在本章中逐一介绍。看看下面的图片:

图片

我们可以将这些层称为基本测试。一旦我们覆盖了所有三个层,我们就可以提交和推送我们的代码。这样就结束了?不。我们还有更高级的测试层,这些测试应该使用完全独立于我们应用程序的工具来执行:

  • 系统测试确保我们所有的业务用例都得到解决和满足。它就像黑盒测试,我们不知道每个方法或操作是如何执行的;我们只关心输入和输出。这种测试从前端层开始,模拟常见的用户操作,并期望一些已经通过后端 API 和外部服务处理过的数据。你必须知道系统测试可以在你的持续集成周期中自动化。

  • 验收测试由真实最终用户执行。他们确保你的系统将支持所有用户交互,评估时间、性能,以及一个非常重要的方面——你的应用程序的用户体验。通常,这种测试首先由产品所有者在称为用户验收测试(UAT)的外部环境中执行。

因此,我们可以总结测试阶段如下所示:

图片

我们将通过本章介绍每个测试阶段。我们已经在第四章的最后一节“创建组件和模板”中回顾了单元测试,这对于确保单个组件的功能很有用,但我们如何评估组件与其他外部服务之间的完整交互呢?是时候更进一步,学习集成测试了。但仅仅更进一步对我们来说还不够,所以让我们更深入地评估每个 Web 应用都非常重要但经常被忽视的一个方面:我们可以确保应用功能,但最终用户使用起来简单吗?UI 测试将给我们答案。最后,让我们为每个暴露给客户端消费的端点添加一些酷炫的文档,即使它是 Web 或移动端。我们将向您展示 Swagger 是如何工作的,以及它如何生成关于您的 API 的易于阅读的文档。本章涵盖的主题包括:

  • 集成测试

  • UI 测试

  • API 测试 - Swagger

不要跳过这一章;测试是应用开发生命周期中非常重要的一个部分。准备好了吗?开始吧!

集成测试 - 多个服务,一个应用

我们已经测试了所有功能。我们知道我们的服务运行正确,但我们的数据库连接和 SSO 呢?你能确认这些服务/依赖项运行正确吗?

集成测试通过验证系统的正确行为在应用开发周期中发挥着重要作用。让我们探索我们当前的应用架构:

图片

集成测试确保我们不同应用层之间的正确交互,在本例中,包括数据库连接、SSO 服务和前端应用。

为什么你应该做集成测试?好吧,如果还不清楚,这里有几个需要考虑的原因:

  • 容易与持续集成CI)周期中的日常构建集成。你的进度可以在任何时间进行测试。

  • 在开发/测试/用户验收测试/生产环境中易于测试。这很简单,就像运行你的应用一样。

  • 与端到端测试相比,测试运行得更快。

  • 允许你检测系统级问题。服务之间的通信、数据库连接等。

现在,让我们向现有的 Express 应用添加一些集成测试。

配置应用以进行集成测试

你唯一需要的前提是 Node 运行环境正确运行。在 JavaScript 中,我们有几个工具可以用来进行集成测试,例如以下这些:

  • SuperTest:其最佳特性是强大的文档;易于理解和实现,你只需写几行代码就可以开始测试你的应用。

  • Mocha:一个简单的 JavaScript 测试框架。它可以在浏览器或 Node 环境中执行。由于 Mocha 基于 JavaScript,它可以执行异步测试并生成非常有用的报告。

我们将使用这两个工具一起进行测试。

使用 SuperTest,你可以获得以下优势:

  • 你可以模拟多个用户交互,存储不同的凭证(令牌)以在用户之间切换。

  • 你不需要担心删除或添加模拟数据。SuperTest 将执行清理或添加数据到你的存储的操作。

  • 最有用的功能——所有这些测试都可以自动化并集成到你的 CI 流水线中。

SuperTest 对于提高生产力非常有帮助;它还提供了一种自然的方式来编写和测试你的代码;它非常直观且易于阅读。让我们来看看如何快速设置它来处理用户检索数据。

你在哪里可以运行 SuperTest 测试?基本上,你可以在任何你想要的服务器上运行它们。无论你是部署在本地 dev 服务器还是云提供商,SuperTests 都可以从它们中的任何一个执行,但你必须知道——SuperTest 包含它自己的 Express 服务器。这个服务器不应该一直运行,但使用一些外部工具如 nodemon,你可以在每次我们有所更改并需要测试时自动重启你的服务器。如果你不想运行所有测试,Mocha 的唯一指定器也是一个不错的解决方案。

首先,我们需要下载我们的依赖项:

npm i supertest mocha chai -s

Chai 允许我们选择以下前缀中的任何一个:shouldexpectassert。就像其他测试工具一样,它们在这里也都可用。

我们已经有了我们的 server.js 文件,所以你不需要在那里添加任何代码。是的,你不需要运行任何服务器,这是 SuperTest 最美的优势之一!

记住,我们的测试文件应该与应用程序文件分开。接下来,使用 touch tests.spec.js 命令创建你的 tests 文件,然后让我们添加一些代码:

const app = require('./server');
const chai = require('chai');
const request = require('supertest');

var expect = chai.expect;

describe('API Tests', () => {
  it('should return football teams', function(done) {
    request(app)
      .get('/teams')
      .end(function(err, res) {
        expect(res.statusCode).to.equal(200);
        done();
      });
  });
});

让我们解释一下代码在做什么。

我们正在导入我们的服务器、Chai 和 SuperTest。SuperTest 包含它自己的 .expect(),但我们正在使用 Chai 的语法。代码设置了一组 API 测试 并创建了一个测试来检查 /teams 端点是否返回状态码 200(OK)。请注意,done() 函数对于声明我们的异步测试已完成非常重要。当然,这是一个非常高级的测试,我们可以添加更多的断言,例如评估响应的内容等。为了说明目的,这个例子非常简单易懂,可以了解 SuperTest 的工作方式。

现在,让我们看看它是否工作。运行以下命令:

npm test

你应该得到这个:

> npm test

> integration-tests@1.0.0 test /Projects/worldcup-app
> mocha '**/*.spec.js'

  API tests
    ![](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/hsn-flstk-webdev-aurelia/img/2193f6d7-59ec-471d-a8b4-c860c8a792ed.png) should return football teams

  1 passing (41ms)

确保你已经在 package.json 文件的脚本部分正确配置了 test 命令。

模拟外部依赖项

好的,你可以编写一些集成测试。这够了吗?还不够。让我们再想想。我们真的需要连接到外部服务吗?如果它们宕机了怎么办?当然,我们的测试会失败,但不是因为某些应用程序错误。为了避免这种情况,我们将使用模拟(Mocks)。

模拟是一种技术,用于模拟某些对象/服务/组件,并在被调用时返回预定义的响应。我们不会连接到真实的服务,所以我们将使用 sinon.mock 为我们的 Teams 架构创建一个模拟模型,并测试预期的结果:

// Test will pass if we get all teams
        it("should return football teams",(done) => {
            var TeamMock = sinon.mock(Team);
            var expectedResult = {status: true, team: []};
            TeamMock.expects('find').yields(null, expectedResult);
            Team.find(function (err, result) {
                TeamMock.verify(); //Verifies the team and throws an exception if it's not met 
                TeamMock.restore(); //Restore the fake created to his initial state
                expect(result.status).to.be.true;
                done();
            });
        });

到目前为止一切正常。现在,让我们评估测试的另一个非常重要的方面,代码覆盖率。

确保在你的项目中下载了 sinon 并将其导入到当前的测试文件中,以便示例工作,就像你可能需要的任何外部模型一样。你可以在 sinonjs.org 找到更多关于 sinon 的信息。

计算代码覆盖率

由于我们的集成测试已经编写并运行,我们还有一件事要做。如果能看到我们的测试在测试覆盖率方面与我们的应用的关系,那将是一件很棒的事情。

让我们给应用添加代码覆盖率!

Istanbul 是一个非常著名的 JavaScript 代码覆盖率工具,它通过模块加载器钩子计算不同的指标,如语句质量、代码行数、函数使用和分支覆盖率,在运行测试时添加覆盖率,为我们提供有关应用程序的实时信息。它支持所有类型的 JavaScript 覆盖率用例,从单元测试到功能测试和浏览器测试。最好的部分是它具有可扩展性。

幸运的是,我们只需要安装 Istanbul 和 nyc(这是 istanbul 的命令行客户端)。我们只需这样做:

npm install istanbul --save-dev
npm install nyc --save-dev

然后,我们修改我们的 package.json 文件。这是为了向我们的 script 对象中添加带有覆盖率的测试:

"test-coverage": "nyc mocha ./spec.js"

然后,运行以下命令:

npm run test-coverage

你应该能够在控制台中看到你应用程序的覆盖率摘要。

我们的应用是否符合我们的业务需求?UI 测试

不要混淆,我们不会测试前端应用程序的功能。这已经通过我们的单元测试进行了测试,那么真正的 UI 测试是什么?嗯,这是一个非常长的讨论。我们可以配置多个测试套件,以便在任何时候执行,这将确保我们的端到端测试符合已经编写的业务需求。我们这是什么意思?单元和集成测试不能评估完整应用程序的所有区域,特别是与工作流程和可用性相关的区域。基本上,我们所有的自动化测试只能验证存在的代码。它们不能评估可能缺失的功能或与应用程序视觉元素相关的问题,以及我们的产品使用起来有多容易。这是 GUI 测试的真实价值,它从用户的角度而不是开发者的实际观点进行操作。通过从用户的角度分析应用程序,GUI 测试可以为项目团队提供足够的信息,以决定应用程序是否可以部署,或者我们需要重新组织一些功能以满足用户需求。

所以,这么说来,我是否总是需要一个手动验证功能的人?是的,也不是。这个主题真的很难以解释,因为可以引用不同的观点,在某些情况下,甚至可能相互矛盾。对于这些情况,我们将在下一节中关注一些非常有用的测试技术。

脚本测试

就像它的名字一样,基于软件测试人员编写的预脚本进行脚本测试,以检测应用程序是否在那一刻遗漏了某些功能。例如,一个脚本执行登录、保存一些数据,然后从另一个屏幕检索它。脚本定义了测试人员将用于评估每个屏幕的预定义数据以及预期的输出。然后,测试人员分析结果,并向应用程序开发团队报告发现的任何缺陷。脚本测试可以由人工手动执行,也可以通过 CI 工具支持测试自动化。

优点在于,你可以将工作分配给你的最有经验的开发者来编写脚本,以及初级开发者来运行脚本和分析数据,从而进行维护和学习业务需求。

缺点在于,如果你的用户界面频繁更改,维护起来会很困难。这种测试与你的业务代码高度耦合,因此如果业务代码发生变化,整个测试也应该随之改变。

探索性测试

在这种测试中,我们不会使用任何自动化脚本。这强制测试人员以普通用户的方式使用应用程序,并评估设计、我们的产品对最终用户来说有多容易、用户体验如何、替代工作流程等方面。测试人员可以识别与这些方面相关的任何失败,并向开发人员提供有价值的反馈。

由于解释性测试不使用脚本,仍然需要进行预先规划。在现实情况下,通常在基于会话的探索性测试中,测试团队为计划中的测试设定目标,并定义一个时间框架来在重点区域进行探索性测试。所有这些信息都包含在一个称为测试宪章的文档中。探索性测试的会话和结果在报告中记录,并在与整个团队的日常会议中审查。

优点在于,测试人员有更多的时间专注于实际的测试,因为准备测试用例和查看无聊的文档所需的时间减少了,这成为了一个不断挑战,即寻找更多问题和增加他们的业务知识。

这种测试的另一个缺点是它不是自动可执行的,当然也不能重复执行作为回归测试。此外,你可能需要具有深入理解你的业务需求的测试人员,而这通常很难找到。此外,在实时场景中,尝试通过探索性测试覆盖整个应用程序可能是不切实际的,因为我们不会找到足够了解产品所需知识的测试人员。

用户体验测试

在用户体验测试中,实际最终用户(或用户代表)评估应用程序的易用性、视觉外观以及满足其需求的能力。用户将在一个称为 UAT 的隔离环境中探索所有应用程序,该环境可以配置在本地服务器或某些云服务提供商上。您必须记住,产品部署的位置无关紧要,它应该代表与您的生产环境相同的条件。

不要将用户体验测试(UX)与用户验收测试(UAT)混淆。UAT 是一个测试级别,它验证给定的应用程序是否满足需求,仅关注业务用例。例如,在 UAT 中,您可以确保您的检索团队按钮工作正常并返回格式良好的正确数据。所以,这还不够吗?当然不够,因为您还不知道按钮是否放置得当,或者从最终用户的角度来看是否难以找到。

好的,现在我们了解了这种测试是如何工作的,我们应该应用哪一种?关于测试的所有决策都应该旨在最大化产品对最终用户的价值,即使是通过检测错误或意外功能,并确保功能性和可用性。为了实现这一目标,在实时情况下,我们需要结合所有不同类型的测试技术,这些技术在此期间已审查。

规划我们的测试——真理的时刻

规划是任何项目的非常重要的阶段,这也不例外。在开始编写用例之前,拥有一个识别可用于测试的资源并优先考虑要测试的应用程序区域的测试计划非常重要。有了这些信息,测试团队将能够创建测试场景、测试用例和测试脚本,并在测试宪章中记录。

这是我们测试计划应包含的结构示例:

  • 每个测试的明确日期

  • 需要的测试人员

  • 需要的资源,如服务器、环境、工具和云服务提供商,必须正确配置以开始测试

  • 目标应用程序环境,例如不同的屏幕分辨率、移动设备和支持的浏览器

  • 要测试的用户工作流程/导航

  • 将要使用的测试技术,包括脚本测试、探索性测试和用户体验测试

  • 测试目标,包括验收标准以确定测试是否通过或失败

此外,我们还可以根据我们的需求添加更多部分:

测试计划可以是文本文档、Excel 表格或测试管理工具,用于开发测试计划以支持分析和报告。有许多工具可供选择,其中一些可以下载到您的私有服务器,而另一些则可在云服务提供商上使用。

我们的 GUI 测试计划不应被视为完整的系统测试计划。您还可以考虑其他方面,如负载测试、安全性、备份、容错和恢复。

完成了,我们有了我们的计划!接下来是什么?确定我们的测试优先级。例如,首先,我们需要确保以下内容:

  • 视觉设计

  • 安全性

  • 用户体验

  • 合规性

  • 功能性

  • 性能

现在,是时候将其表示为整个团队都能理解的心智图,并执行所需的测试。查看以下示例:

图片

当我们在 Web 应用程序中导航时,最常见的测试区域是这些:

  • 与大多数常用浏览器不同版本的兼容性

  • 用户点击浏览器中的后退或刷新按钮时的行为

  • 用户使用书签或浏览器历史记录返回页面后的行为

  • 用户同时在 UAT 上打开多个浏览器窗口时的行为

定义常见场景

我们可以将测试场景定义为描述应用程序如何在特定实际情况下使用的简要声明,例如,“用户将能够使用有效的用户名和密码登录。”通常,测试场景是从开发文档(如需求或用户故事)中编写的,每个场景都有完成该任务所需的可接受标准。如果这些文档尚未创建,产品负责人应该编写它们,并定义不同的场景和可接受标准,以标记产品已完成。

场景是有用的,因为它们可以指导探索性测试,对 GUI 事件有一个良好的理解,而不限制测试团队遵循特定的程序。由于创建测试场景比编写完整的测试用例要快得多,因此场景在敏捷环境中被广泛使用。

如果在脚本测试中使用场景,它们可以作为编写测试用例的基础。

例如,前面提到的登录场景可以包含以下 GUI 事件的测试用例:

  • 用户输入有效的用户名和密码

  • 用户输入无效的用户名

  • 用户输入有效的用户名但无效的密码

  • 用户尝试重置密码

  • 用户反复点击提交按钮

图片

编写测试用例

通常,这份文档会从对要测试的 GUI 事件的简要描述开始,我们仍将使用登录尝试的例子。我们应该指定测试执行的条件和步骤。最后,我们需要评估测试的预期结果,并定义确定测试成功或失败的可接受标准。

你必须记住一些考虑因素,例如以下列出的:

  • 用户界面变化的频率

  • 当最终用户在应用程序中导航时,他们将有多少自由度

  • 如果你有的测试人员经验较少,他们可能需要更详细的程序

应包含哪些内容?这是个好问题。类似于我们的规划模板,我们需要将我们的可接受标准组织在一个文档中,以便我们可以检查和保存每个测试用例的演变。以下是一个非常基本的例子:

确保你为测试用例分离了测试数据非常重要。大多数情况下,问题是由项目中的验证错误产生的,因此开发团队必须知道哪些参数导致了这个错误。始终记住,应用程序开发是一个团队工作,所以你必须为你的同事提供所有便利,以获得最佳产品。

组织我们的测试 - 创建脚本

这是我们的测试中最详细的部分。在这里,我们将定义执行我们的测试的步骤和程序。创建足够的测试脚本以验证用户将通过 UAT 采取的路径。

总是记得记录输入数据和预期输出;当我们必须处理意外事件时,这将非常重要。

使用 Swagger 进行 API 测试

在最后一节中,我们专注于从前端开始对整个应用程序的端到端测试,但我们的 API 本身呢?记住,一个 API 可以用于服务许多客户端类型,如网页或移动。所以,你不认为确保这个功能独立于整个应用程序生命周期会很好吗?当然,我们知道你同意,我们心中有一个非常棒的解决方案来实现这个目标——Swagger。

Swagger 是一个规范和一组编写 RESTful API 的优秀工具。根据他们自己的网页定义:

"Swagger 是 OpenAPI 规范 (OAS) 的世界最大 API 开发者工具框架,它使整个 API 生命周期(从设计、文档到测试和部署)的开发成为可能。"

在 Swagger 的工具组中,我们可以找到以下工具:

  • Swagger Editor:这个工具将允许我们实时查看更新的文档。

  • Swagger Codegen:一个模板驱动的引擎,用于生成交互式文档。

  • Swagger UI:允许可视化 RESTful API,并检查输入和响应。因此,Swagger UI 从现有的 JSON 或 YAML 文档中创建交互式文档。

安装 Swagger

让我们开始获取我们最重要的依赖项,Swagger。记住,我们将使用 NPM 下载 Swagger,因为我们将在 ExpressJS API 上使用它。Swagger 根据您需要记录的平台有自己的实现。

有两种非常著名的 Swagger 实现可以与 ExpressJS 应用程序集成:

  • swagger-node-express

  • swagger-ui-express

swagger-node-express 是官方的 Swagger 模块,用于 Node。这个库的一些有趣(和不那么有趣)的特性如下:

  • Swagger API 的官方发行版。我们得到了一个积极致力于产品开发的组织的全面支持。

  • 它是开源的。

  • 附带 Swagger Editor 和 Swagger Codegen。

  • Swagger UI 需要插入到我们添加文档的代码中。

  • 由于文档相当匮乏,你需要阅读一些不同库的源代码来学习和理解用于配置 Swagger 的每个参数。

swagger-ui-express由社区支持,另一个优秀的开源选项。它是如何工作的?这个库为你的 Express.js 应用程序添加了一个中间件,该中间件提供与你的 Swagger 文档绑定的 Swagger UI。配置起来非常简单;你需要做的只是添加一个路由来托管 Swagger UI,无需手动复制任何内容。文档非常好,我们认为你需要的一切都应该在那里。

由于这个工具的功能性和简单性,我们决定使用这个库而不是其他任何选项来实现我们记录应用程序的主要目标。

要开始,我们需要将这个库添加到我们的当前项目中:

npm install -save swagger-ui-express

一旦将库添加到我们的项目中,我们需要配置一个路由来托管 Swagger UI。此外,我们还需要加载我们应用程序的 Swagger API 定义。在我们的应用程序中,Swagger API 定义是一个包含我们应用程序信息的 JSON 对象的单一文件。

要创建 Swagger API 定义,我们使用了 Swagger Editor。记住,你可以自由地使用 JSON 或 YAML 标记。让我们看看同一定义在不同格式下的示例:

在 JSON 中,操作如下:

{
    "swagger": "2.0",
    "info": {
        "version": "1.0.0",
        "title": "WorldCup API",
        "description": "A simple API to learn how to write FIFAWC Specification"
    },
    "schemes": [
        "http"
    ],
    "host": "localhost:3000",
    "basePath": "/",
    "paths": {
        "/teams": {
            "get": {
                "summary": "Gets team list",
                "description": "Returns a list containing all teams of the WorldCup.",
                "responses": {
                    "200": {
                        "description": "A list of Teams",
                        "schema": {
                            "type": "array",
                            "items": {
                                "teams": {
                                    "country": {
                                        "type": "string"
                                    },
                                    "trainer": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

在 YAML 中:

swagger: "2.0"

info:
  version: 1.0.0
  title: WorldCup API
  description: A simple API to learn how to write FIFAWC Specification

schemes:
  - https
host: simple.api
basePath: /doc

paths:
  /teams:
    get:
      summary: Gets teams list
      description: Returns a list containing all Teams of the WorldCup.
      responses:
        200:
          description: A list of Teams
          schema:
            type: array
            items:
              properties:
                country:
                  type: string
                trainer:
                  type: string

两者都非常易于阅读。

即使我们的规范文件是可以用任何文本编辑器编辑的文本,但现在我们有许多专门的工具来实现这一点,给我们提供了一些有用的功能,如语法验证、格式、自动完成参数等。编写规范文件的最佳选项是 Swagger Editor(是的,它自己的工具),一组强大的静态文件,允许你使用 YAML 语法编写和验证 Swagger 规范,并查看你的文件渲染后的样子。

创建的 Swagger API 定义将作为 JSON 对象存储在我们的应用程序的swagger.json文件中。此时,我们的设置脚本应该看起来像这样:

const swaggerUi = require('swagger-ui-express')
const swaggerDocument = require('./swagger.json'); //Our specification file

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

如你所见,第一个中间件是设置我们的 Swagger 服务器。这个中间件将返回托管 Swagger UI 所需的静态文件。第二个中间件是我们的设置函数,它将设置 Swagger UI 以使用我们在json/yml文件中预定义的用户参数。

当然,我们的文档 URL 是 http://localhost:3000/api-docs

摘要

你现在只需一步就能获得关于全栈应用程序及其相关内容的最大知识。正如我们之前所说,最重要的部分之一,也是区分程序员和全栈应用程序开发者的关键,是测试。这一章旨在易于理解,并在你的日常工作中高度适用,为你提供应用测试中最现代的概念和工具。

你需要记住,集成测试是确保所有组件协同工作良好的保证过程,例如第三方系统、外部数据库和异步进程。根据你使用的平台,你将拥有一个完整的生态系统来完成这项任务,并从你的应用程序中获得最佳价值。最好的部分是你可以在这个层面自动化所有测试,使你的部署过程更安全。有一件事你不能使用脚本进行测试,那就是应用程序的可用性。你需要真实的人类来与你的产品互动,在这个层面,不同的 UI 测试技术会提供帮助。记住,当你需要将产品推向市场时,用户体验是一个重要的差异化因素。

最后,所有不同的测试都需要被记录下来,以便其他开发者可以使用它们来改进你应用程序的更多功能。Swagger 是一个非常棒且简单的工具,可以生成详细的文档并与你的团队共享。

现在我们已经完全覆盖了测试阶段!你知道现在是时候了... 让我们部署吧!下一章见!

第十章:部署

现在我们知道了如何创建 Aurelia 应用程序,我们应该能够在自己的服务器上部署它们,或者如果你想利用其他大型公司的资源,为什么不使用云服务提供商来部署我们的应用程序呢?

在本章中,你将学习如何使用 Docker 和 NGINX 在你的自托管服务器上部署你的应用程序。此外,你还将学习如何在 Heroku 上部署应用程序以及地球上最好的云服务提供商之一——亚马逊网络服务AWS)。

我们将使用 Docker 在本地部署,因为这会使我们的部署具有可移植性,这样你就可以在自己的数据中心或云中部署你的应用程序。

话虽如此,在本章中,我们将涵盖以下主题:

  • 为我们的网站配置生产环境

  • 在你的服务器上部署

  • 在 Heroku 上部署

  • 在 AWS S3 存储桶上部署

为我们的网站配置生产环境

在我们将应用程序部署到生产之前,我们应该准备和配置它。让我们创建一个示例 Aurelia 应用程序并将其部署。打开一个新的终端窗口并运行以下命令以启动一个新的deployme项目:

$ au new deployme
 ...
 ...
 Happy Coding!

$ ls deployme/aurelia_project/environments
  dev.js  stage.js  prod.js

让我们导航到aurelia_project/environments文件夹。在这个文件夹中,你会找到以下文件,它们映射到特定的开发环境:

  • dev.js:包含开发阶段的配置。

  • stage.js:包含预发布阶段的配置。这个阶段也被称为质量保证QA)。

  • prod.js:包含生产阶段的配置。在这个阶段,我们的应用程序正在被我们的最终用户使用。

让我们打开dev.js文件并检查开发阶段配置的内容:

export default {
  debug: true,
  testing: true
};

配置文件是一个简单的 JSON 文件。正如你在dev.js文件中看到的那样,debugtesting属性为 true。这意味着在我们开发应用程序时,我们将能够调试和测试应用程序。让我们添加一个新的属性来看看它是如何工作的。在dev.js文件中,应用以下更改:

export default {
  debug: true,
  testing: true,
  appTitle: 'DeployMe [dev]'
};

现在,让我们打开prod.js文件并查看应用程序应该如何配置为生产环境:

export default {
  debug: false,
  testing: false
};

在生产中,我们不需要调试任何东西,我们也不测试我们的应用程序,所以我们必须禁用调试和测试,并将值设置为false。让我们创建appTitle属性,但这次使用生产环境的正确值:

export default {
  debug: false,
  testing: false,
  appTitle: 'DeployMe'
};

现在,让我们对app.js文件应用以下更改,从环境environment.js配置文件中读取appTitle

import environmentConfig from './environment'

export class App {
  constructor() {
    this.appTitle = environmentConfig.appTitle;
  }
}

现在,让我们对app.html文件应用以下更改:

<template>
  <h1>${appTitle}</h1>
  <p>
    Hello :)!
  </p>
</template>

一旦应用了更改,让我们运行应用程序,但这次让我们指定环境,以便查看应用程序的标题如何根据环境而变化。请运行以下命令以开发环境运行:

$ au run --watch --env dev
  ...
  Application Available At: http://localhost:9000

在你的浏览器中导航到http://localhost:9000,你应该会看到以下图像类似的内容:

图片

太棒了!现在让我们运行应用程序,但这次让我们将(--env)环境标志从dev更改为prod,以便创建生产版本:

$ au run --env prod
  ...
 Application Available At: http://localhost:9000

在您的浏览器中导航到http://localhost:9000,您应该会看到以下类似图像:

图片

太棒了!我们几乎完成了生产版本。在我们部署应用程序之前,我们需要创建代码的压缩版本。为此,执行以下命令:

$ au build --env prod

上述命令将在scripts文件夹中生成构建脚本,其中将包含准备就绪的 JavaScript 文件。这些文件被压缩以改善客户端浏览器的加载性能。我们将使用dist文件夹和index.html进行部署过程。基本上,Aurelia 应用程序的部署具有以下文件架构:

./
   - index.html
   - dist/
     - app-bundle.js
     - vendor-bundles.js

JavaScript 文件的详细信息在第一章的“Aurelia CLI 部分”中提供,介绍 Aurelia

太棒了!让我们学习如何部署我们的应用程序。继续阅读!

在自己的服务器上部署

到目前为止,我们已经准备好了生产文件,可以部署。典型的部署场景是当你想在本地服务器、本地计算机或内部企业服务器上部署你的 Web 应用程序时。这对那些想要管理和完全控制他们服务器的大公司来说是一种常见做法。所以,让我们学习如何使用 Docker 和 NGINX 作为我们的最佳盟友来完成这项工作。

创建我们的 NGINX 配置文件

NGINX 在我们部署中扮演的唯一角色是作为 Web 服务器,因此我们将为我们的服务器编写一个简单的配置文件。请前往项目根目录创建名为default.conf的文件:

server {
    listen 80;
    server_name localhost;

    location / {
 root /usr/share/nginx/html;
 index index.html index.htm;
 }

    error_page 500 502 503 504 /50x.html;
}

让我们了解这个配置文件的作用。首先,我们告诉 NGINX 在端口80listen。这意味着当我们想要访问我们的应用程序时,我们应该调用这个端口。其次,我们定义了 NGINX 将找到我们的应用程序文件的位置。我们必须将scripts文件夹和index.html文件复制到这个文件夹中。

太棒了!我们有了应用程序 Web 服务器的 NGINX 配置文件。让我们继续设置Dockerfile文件。

创建我们的 Dockerfile

我们的Dockerfile包含了构建 Docker 镜像的配方,其中包含了启动我们的 NGINX Web 服务器所需的所有配置,同时它还将包含必须复制到容器内的 Web 应用程序。请前往项目根目录创建名为Dockerfile的文件:

FROM nginx:alpine

COPY default.conf /etc/nginx/conf.d/default.conf

COPY index.html /usr/share/nginx/html/index.html
COPY dist /usr/share/nginx/html/scripts

是的,就是这样!这是一个简单的 Docker 文件,它使用nginx:alphine作为其基础镜像。我们COPY了包含 NGINX 配置文件的default.conf文件,最后我们COPY了我们的 Web 应用程序文件。

就这样!让我们构建我们的 Docker 镜像并运行我们的第一个容器。

在 Docker 上运行我们的应用程序

在我们能够运行我们的应用程序之前,我们需要使用我们的 Dockerfile 创建一个 Docker 镜像。打开一个新的终端并导航到项目根目录。一旦到达那里,运行以下命令来构建我们的 Docker 镜像:

$ cd /some/path/deployme
$ docker build -t mydeploymeapp .

Sending build context to Docker daemon 130.9MB
Step 1/4 : FROM nginx:alpine
Digest: sha256:17c4704e19a11cd47545fa3c17e6903fc88672021f7f907f212d6663baf6ab57
Status: Downloaded newer image for nginx:alpine
 ---> 91ce6206f9d8
Step 2/4 : COPY default.conf /etc/nginx/conf.d/default.conf
 ---> 0e744f0e2556
Step 3/4 : COPY index.html /usr/share/nginx/html/index.html
 ---> 092ad92d0d5c
Step 4/4 : COPY scripts /usr/share/nginx/html/scripts
 ---> 6d097542eec5
Successfully built 6d097542eec5
Successfully tagged mywebapp:latest 

我们使用 docker build 命令来构建一个新的镜像。-t 选项允许我们给我们的镜像命名,在这种情况下,我们的镜像名为 mydeploymeapp。注意最后一个命令参数中的点(.);docker build 命令使用 Dockerfile 来构建新的镜像。我们在命令的最后一个选项中指定了这个 Dockerfile 的路径;由于这个文件位于我们运行 docker build 的根目录中,我们应该使用 符号来指定当前文件夹,在这个例子中,它包含 Dockerfile

一旦构建过程完成,我们将看到 Successfully built 的消息。

现在,我们已经准备好启动我们的应用程序。运行以下命令以启动一个新的 mydeploymeapp Docker 容器:

$ docker run -p 8000:80 mywebapp

我们使用了 docker run 命令来启动一个新的容器,并通过 -p 选项将我们的主机 8000 端口映射到容器内的 NGINX 80 端口,该端口正在监听。最后一个参数是我们想要创建的 Docker 镜像,在这种情况下,是我们的应用程序镜像。

太棒了!我们的应用程序已经启动并运行。让我们导航到 http://localhost:8000,你应该会看到以下页面:

图片

就这样!我们已经成功使用 Docker 和 NGINX 在本地服务器上部署了我们的 Aurelia 应用程序。您可以在远程服务器上安装 Docker 并按照本节中相同的步骤安装任何 Aurelia 应用程序。

在接下来的章节中,我们将学习如何将应用程序部署到云端。继续阅读!

在 Heroku 上部署

是时候利用云服务并开始寻找新的方法了。在本节中,我们将了解如何将我们的 Aurelia 示例应用程序部署到 Heroku。在开始部署过程之前,我们将按照以下步骤进行操作:

  1. 创建 Heroku 账户

  2. 准备我们的应用程序

  3. 部署

让我们从第一步开始。

创建 Heroku 账户

导航至 heroku.com 并点击“免费注册”按钮。然后,填写您的账户信息。准备好后,我们需要安装 Heroku CLI。这个 CLI 将为我们提供一个易于使用的命令行工具,我们将使用它来部署我们的应用程序并在我们的应用程序上执行其他管理操作。

要下载 Heroku CLI,请导航至 devcenter.heroku.com/articles/heroku-cli。选择您的操作系统并遵循安装说明。安装完成后,打开一个新的终端窗口并执行以下命令以登录到您的 Heroku 工作空间:

$ heroku login

 Enter your Heroku credentials:
 Email: erikson.murrugarra@gmail.com
 Password: ***************
 Logged in as erikson.murrugarra@gmail.com

前面的命令将要求你提供在注册过程中使用的 emailpassword。提供你的凭证,如果一切正确,你将收到 Logged in as ... 的消息。

准备应用程序

Heroku,就像其他云服务提供商一样,不提供部署静态 HTML 文件的方式,因此我们需要使用另一种策略来部署我们的应用程序。我们将创建一个简单的 PHP 文件,它将作为入口点,其中包含一行代码来导入我们的 index.html 文件:

现在,在 dist 文件夹中,继续创建一个包含以下内容的 index.php 文件:

<? 
  include_once("index.html");

我们使用 invoke_once 函数将 index.html 页面作为主页面导入。

部署

一旦我们有了应用程序的静态文件和 index.php 入口点文件,让我们创建一个新的 Heroku 应用程序。导航到 dist 文件夹并运行以下命令来 init 一个新的 Git 仓库:

$ cd dist
$ git init
$ git add .
$ git commit -m "My Application commit"

我们使用 git add 后跟一个点(.)来将所有文件添加到暂存区。这意味着所有更改都将成为提交的候选内容。

一旦我们有了本地的 Git 仓库,让我们创建一个名为 mydeploymeapp 的新 Heroku 应用程序。执行以下命令:

$ heroku apps:create deploymeapp

 Creating ![](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/hsn-flstk-webdev-aurelia/img/ec09ec8a-6a1d-4cf7-b546-6fd7e06d8c15.png) deploymeapp... done
 https://deploymeapp.herokuapp.com/ | https://git.heroku.com/deploymeapp.git

我们使用 Heroku CLI 工具并调用 apps:create 选项来创建一个新的应用程序。你必须更改应用程序的名称,因为它们应该与其他应用程序的名称在全球范围内不同。一旦创建,Heroku 将响应并提供应用程序的 URL。

现在我们已经创建了应用程序,让我们将代码推送到 Heroku 创建的 Git 仓库以托管我们的应用程序代码并查看结果。执行以下命令:

$ git push heroku master

Counting objects: 3, done.
...
remote:
remote: -----> PHP app detected
remote:
...
done.
To https://git.heroku.com/mydeploymeapp.git
 15cb66e..72d4d2a master -> master

如你所见,Heroku 将自动识别我们正在部署一个 PHP 应用程序,并将部署我们的应用程序。让我们继续前进并导航到你的应用程序的 URL;我的网址是 mydeploymeapp.herokuapp.com

太棒了!现在我们知道了如何使用 Heroku 在 上部署我们的应用程序,让我们来看看我们如何使用另一个非常流行的云服务提供商来做这件事。我们将学习如何使用 Amazon Simple Storage ServiceAmazon S3)来部署我们的网站。继续阅读!

在 AWS S3 存储桶上部署

AWS 是全球最大的云服务提供商之一。我们将学习如何使用 AWS S3 部署我们的 Aurelia 应用程序。是的,我没有疯!我们将不安装任何服务器或 弹性云计算Elastic Cloud ComputeEC2)虚拟机实例来部署我们的应用程序。

上传文件

在我们开始这个过程之前,你需要在 AWS 上有一个账户。访问以下网址以创建你的免费账户—aws.amazon.com

一旦你完成了注册过程并登录到 AWS 控制台,请继续导航到 S3 服务仪表板,如下所示:

一旦加载了 S3 控制台,您将看到之前创建的所有 AWS 存储桶列表。如果没有,您将看到一个空列表。点击“创建存储桶”按钮创建一个新的存储桶,并以您的应用程序名称命名存储桶,然后点击“下一步”按钮接受默认设置:

图片

一旦创建,您应该会在列表中看到新的 S3 存储桶:

图片

现在进入您的存储桶,通过点击“添加文件”按钮并选择文件来上传index.html文件到存储桶。提交文件时,别忘了授予公共读取权限:

图片

一旦我们选择了想要在 S3 中部署的 Web 应用程序文件,我们应该将这些文件设置为公开,这样 S3 就能够公开提供文件,否则我们的用户将无法访问我们的 Web 应用程序文件。为此,在“管理公共权限”下拉列表中选择“授予此对象(们)公共读取访问权限”选项,如图所示:

图片

太棒了!要将内容上传到script文件夹,您需要在存储桶中创建一个脚本文件夹。为此,只需点击“创建文件夹”按钮。一旦创建了文件夹,请将app-bundle.jsvendor-bundle.js文件上传到 S3 脚本文件夹(别忘了授予公共权限)。

配置我们的 S3 存储桶以供网页使用

让我们配置我们的存储桶以充当网站存储桶。在这样做之前,让我们从我们的 Web 浏览器中访问我们的index.html文件。为此,选择index.html页面,并复制对象设置面板中出现的“链接”旁边的 URL:

图片

导航到提供的链接,您应该会看到您的 Web 应用程序正在运行:

图片

我们应该做最后一步。首先,请注意,URL 不友好,我们正在使用index.html文件访问 Web。这不是在 Amazon S3 上部署 Web 应用程序的正确方式。让我们配置我们的存储桶,使其明确成为 Web 存储桶。

让我们转到我们的存储桶中的“属性”选项卡,并选择静态 Web 主机选项。选择“使用此存储桶来托管网站”选项,将index.html作为索引页面,然后点击“保存”:

图片

太酷了!现在我们准备好了。复制 AWS 提供的新 URL 并导航到它,以查看 Web 应用程序正在运行:

图片

太棒了!您现在可以使用 S3 存储桶部署应用程序到 AWS,而无需提供任何虚拟服务器。恭喜您!

摘要

在本书的最后一章中,您学习了如何在自管理的或本地服务器上部署 Web 应用程序,以及如何利用云服务。

您学习了如何使用 Docker 来部署应用程序。使用 Docker 将帮助您在开发的各个阶段,所以我们强烈建议您购买一本 Docker 书籍,并立即开始学习它!

我们还介绍了如何在 Heroku 上部署应用程序。Heroku 是一个 平台即服务PaaS),它会为您管理和监控应用程序。因此,您无需担心基础设施;您只需关注创建出色的应用程序即可。

最后,您了解了如何使用 AWS S3 存储桶来部署应用程序,无需进行任何虚拟服务器配置。

Aurelia 正变得越来越受欢迎,并被重要组织采用;现在是成为专家并为 Aurelia 将在革命性未来扮演的重要角色做好准备的时候了。另一方面,您对 Docker 和云计算的了解还不是很深入。我们鼓励您探索这些在 IT 领域极为重要的技术。

话虽如此,我们祝愿您在接下来的冒险中一切顺利。

posted @ 2025-09-24 13:53  绝不原创的飞龙  阅读(27)  评论(0)    收藏  举报