NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (2024)

原文:zh.annas-archive.org/md5/2FC862C6AE287FE2ADCD470958CE8295

译者:飞龙

协议:CC BY-NC-SA 4.0

随着 ECMAscript 6 的出现,Node.JS 的可用性在未来有很大的发展空间,并且已经在今天得到了实现。学习 es6 语法糖的需求以及包含大多数跨技术特性的需求,激励了不同的技术社区学习 JavaScript。

Node.js 的高性能和可扩展性以及名为 MongoDB 的开源 NoSQL 数据库解决方案适用于轻松构建快速、可扩展的网络应用程序。这种组合使得管理任何形式的数据变得简单,并确保其交付速度。

本书旨在提供使用 Node.JS 和 MongoDB 构建等同服务器端渲染 Web 应用程序的不同方面。本书还指导我们使用 hapi.js 创建可配置的 Node.JS 服务器,并学习使用 Angular 4 开发单页前端应用程序。

本书将首先介绍您建立开发环境所需的基础知识,并对新的 ECMAscript 与传统 JavaScript 的不同进行比较研究。一旦基础就绪,我们将快速浏览必要的步骤,使主要应用程序服务器运行起来,并学习 Node.JS 核心。

此外,我们将通过使用控制器和 ViewModels 来生成可重用代码,从而减少开发时间。开发以学习适当的测试概念以及如何自动化测试以实现可重用性和可维护性而结束。

在本书结束时,您将与 JavaScript 生态系统连接,并了解流行的 JavaScript 前端和后端框架。

第一章,欢迎来到全栈 JavaScript,介绍了 Node.js 和 MongoDB。除此之外,它还将解释您将使用本书构建的应用程序的整体架构。

第二章,启动和运行,解释了如何为 Node.js 和 MongoDB 设置开发环境。您还将通过编写一个示例应用程序并运行它来验证一切是否设置正确。

第三章,Node 和 MongoDB 基础,是关于学习 JavaScript 的基础知识。此外,还介绍了 NodeJS 的需要了解的概念以及 MongoDB 上的基本 CRUD 操作。

第四章,介绍 Express,向您介绍了 Express 框架及其各个组件。它还指导您如何组织使用该框架构建的基本应用程序。它还将详细介绍 Express 的 MVC 组件。

第五章,使用 Handlebars 进行模板化,向您介绍了使用模板引擎和 handlebars 的概念。此外,它还向您展示了如何在应用程序中使用 handlebars 作为模板引擎。

第六章,控制器和视图模型,向您展示了如何将构建的示例应用程序的代码组织到 Express 框架的控制器和视图中。它将通过介绍将代码分离到各种模块并利用 Express 框架来间接介绍 MVS 概念。

第七章,使用 MongoDB 持久化数据,向您展示了如何从正在构建的 Node.js 应用程序连接到 MongoDB 服务器。它还将向您介绍 ODM 的概念,最流行的是 Mongoose。

第八章,创建 RESTful API,向您介绍了 RESTful API。它还向您展示了 RESTful 包装器对应用程序的重要性。然后,它将教您如何将当前应用程序更改为基于 REST API 的应用程序。

第九章,测试您的代码,向您展示为什么需要将测试与应用程序结合,并且还会提到您在本章编写的代码的可测试性需要注意的事项。

第十章,使用基于云的服务部署,讨论了托管您正在构建的 Node.js MongoDB 应用程序的选项。它还比较了市场上可用的各种 PaaS 解决方案。

第十一章,流行的 Node.js Web 框架,介绍了除了 Express 之外在 Node.js 上可用的各种 Web 框架,您将在本书中用于构建应用程序。您将分析各种 Web 框架,如 Meteor、Sails、Koa、Hapi 和 Flatiron。您还将通过创建 API 服务器更详细地学习一种独特类型的框架,即 hapi.js。

第十二章,使用流行的前端框架创建单页应用程序,提供了单页应用程序与流行的前端框架(如 backbone.js、ember.js、react.js 和 Angular)的比较研究。您将详细了解一种流行的框架--Angular4。此外,您还将分析流行的前端方面,如可用的自动化工具和转译器。

本书只需要对 JavaScript 和 HTML 有基本的了解。然而,本书的设计也有助于具有基本编程知识和跨平台开发人员学习 JavaScript 及其框架的初学者。

本书适用于具有以下标准的 JavaScript 开发人员:

  • 那些想要学习后端 JavaScript 的人

  • 那些了解 es5 并希望从新的 ECMAscript 开始的人

  • 那些具有 JavaScript 中级知识并希望探索新框架,如 Angular 2、hapi 和 Express 的人

最后,本书适用于任何渴望学习 JavaScript 并希望在 Node.js 和 MongoDB 中构建交互式 Web 应用程序的跨平台开发人员。

在本书中,您将找到许多文本样式,用以区分不同类型的信息。以下是一些示例以及它们的含义解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:

"在上述情况中,setTimeout()方法由 JavaScript(Node.js)API 提供。"

代码块设置如下:

var http = require('http');http.createServer(function(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World\n');}).listen(8080, 'localhost');console.log('Server running at http://localhost:8080'); 

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

app.engine('Handlebars', exphbs.create({ defaultLayout: 'main', layoutsDir: app.get('views') + '/layouts', partialsDir: [app.get('views') + '/partials'], helpers: { timeago: (timestamp)=> { return moment(timestamp).startOf('minute').fromNow(); } } }).engine); 

任何命令行输入或输出都以以下方式书写:

$ sudo apt-get install python-software-properties $ sudo curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - $ sudo apt-get install nodejs

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如

例如,在菜单或对话框中出现的文本如下所示:

Mac 上一个很好的替代品是 iTerm2。

警告或重要提示以此框出现。

提示和技巧显示如下。

曾经只被认为是为网页添加增强功能和小部件的语言,现在已经发展成了一个完整的生态系统。截至 2017 年的调查(insights.stackoverflow.com/survey/2017),它是 stackoverflow 上使用量最大的语言,有大约一百万个与之相关的问题标签。有大量的框架和环境可以让 JavaScript 几乎在任何地方运行。我相信阿特伍德定律说得最好:

“任何可以用 JavaScript 编写的应用程序最终都将用 JavaScript 编写!”

尽管这句话可以追溯到 2007 年,但它在今天仍然是真实的。你不仅可以使用 JavaScript 开发完整的单页应用程序,比如 Gmail,还可以看到我们如何在本书的后续章节中使用它来实现以下项目:

  • 完全使用 Node.js 和 Express.js 来支持后端

  • 使用诸如 MongoDB 之类的强大的文档导向数据库来持久化数据

  • 使用 Handlebars.js 编写动态 HTML 页面

  • 使用 Heroku 和 Amazon Web Services(AWS)等服务将整个项目部署到云端

有了 Node.js 的引入,JavaScript 正式进入了以前甚至不可能的方向。现在,你可以在服务器上使用 JavaScript,也可以用它来开发完整的企业级应用程序。当你将这一点与 MongoDB 及其基于 JSON 的数据的强大功能结合起来时,你可以在应用程序的每一层中使用 JavaScript。

让我们快速了解一些 Node.js 和 MongoDB 的基本概念,这将有助于你理解本书后续章节的内容。

人们在初次接触 Node.js 时最容易混淆的一件事是,要理解它究竟是什么。它是一个完全不同的语言吗,它只是 JavaScript 的一个框架,还是其他什么东西?Node.js 绝对不是一种新语言,它也不仅仅是 JavaScript 的一个框架。它可以被看作是建立在 Google 的 V8 引擎之上的 JavaScript 运行环境。因此,它为我们提供了一个上下文,我们可以在任何可以安装 Node.js 的平台上编写 JavaScript 代码。任何地方!

现在,稍微了解一下它的历史!2009 年,Ryan Dahl 在 JSConf 上做了一个演讲,彻底改变了 JavaScript。在他的演讲中,他向 JavaScript 社区介绍了 Node.js。在大约 45 分钟的演讲后,他得到了观众的起立鼓掌。他在 Flickr 上看到了一个简单的文件上传进度条后,受到启发,决定写 Node.js。他意识到该网站正在以错误的方式处理整个过程,他决定必须有更好的解决方案。

现在让我们快速了解一下 Node.js 的特点,看看它与其他服务器端编程语言有何不同。

V8 引擎是由 Google 开发的,并于 2008 年开源。众所周知,JavaScript 是一种解释性语言,它不像编译语言那样高效,因为代码的每一行在执行时都会被逐行解释。V8 引擎带来了一个高效的模型,其中 JavaScript 代码首先被解释,然后编译成机器级代码。

新的 V8 5.9 发布了一个稳定版本,引入了 TurboFan 编译器,提供了性能和大规模优化的好处。它还推出了 Ignition 解释器,对于所有大小的设备如服务器或 IOT 设备等,它都非常高效,因为它的内存占用范围不同。由于内存占用低,它可以快速启动应用程序。我们可以在以下链接中研究基准测试:goo.gl/B15xB2

通过两个强大的更新,v8 团队还在开发 Orinoco,这是一个基于并行和并发压缩机制的垃圾收集器。

这样的高性能和有希望的结果是将 node 8(LTS)的发布日期从 2018 年 5 月推迟到 2018 年 10 月的原因。目前我们正在使用非 LTS 版本的 node 8。它为使用 node v4.x.x 及以上版本的用户提供了干净的替代,没有破损的库。版本 8 还具有各种内置功能,如缓冲区改进和内置的 promisify 方法等。我们可以在以下链接中学习它们:goo.gl/kMySCS

随着 Web 的出现,传统的 JavaScript 旨在在浏览器中添加简单的功能和最小的运行时。因此,JavaScript 被保持为单线程脚本语言。现在,为了对单线程模型有一个简要的了解,让我们考虑以下图表:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (1)

单线程模型在执行上下文中创建一个单一的调用栈。在前面的代码中,当函数getData()被调用时,该函数被推入堆栈以便按顺序执行。

在 Node.js 的上下文中,JavaScript 是基础脚本语言,因此 Node.js 是单线程的。您可能会问,单线程模型如何帮助?典型的 PHP、ASP.NET、Ruby 或基于 Java 的服务器遵循的模型是每个客户端请求都会导致实例化一个新的线程甚至一个进程。

当涉及到 Node.js 时,请求在同一个线程上运行,共享资源。一个经常被问到的问题是,使用这样的模型会有什么优势?要理解这一点,我们应该了解 Node.js 试图解决的问题。它试图在单个线程上进行异步处理,以提供更高的性能和可伸缩性,以处理太多的网络流量的应用程序。想象一下处理数百万并发请求的 Web 应用程序;如果服务器为每个进来的请求创建一个新的线程,它将消耗大量资源,我们最终将不得不添加更多的服务器来增加应用程序的可伸缩性。

单线程的异步处理模型在先前的上下文中有其优势,您可以使用更少的服务器端资源处理更多的并发请求。然而,这种方法也有其缺点;Node(默认情况下)不会利用服务器上可用的 CPU 核心数量,而不使用额外的模块,如pm2

Node.js 是单线程的这一点并不意味着它在内部不使用线程。只是开发人员和代码的执行上下文对 Node.js 内部使用的线程模型没有控制权。

如果您对线程和进程的概念不熟悉,我建议您阅读一些关于这些主题的初步文章。还有很多 YouTube 视频也是关于同样的主题。

以下参考资料可以作为一个起点:

www.cs.ucsb.edu/~rich/class/cs170/notes/IntroThreads/

Node.js 最强大的特性之一是它既是事件驱动的,又是异步的。那么,异步模型是如何工作的呢?想象一下你有一段代码,在第 n 行有一个耗时的操作。当这段代码被执行时,后面的行会发生什么?在正常的同步编程模型中,后面的行将不得不等到该行的操作完成。异步模型会以不同的方式处理这种情况。

让我们通过以下代码和图表来可视化这种情况:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (2)

在前面的情况下,setTimeout()方法由 JavaScript(Node.js)API 提供。因此,这个方法被认为是同步的,并在不同的执行上下文中执行。根据setTimeout()的功能,它在指定的持续时间后执行回调函数,在我们的例子中是三秒后。

此外,当前的执行永远不会被阻塞以完成一个进程。当 Node.js API 确定事件的完成已被触发时,它将立即执行你的回调函数。

在典型的同步编程语言中,执行前面的代码将产生以下输出:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (3)

如果你仍然对学习 JavaScript 中的异步模型和回调概念感兴趣,Mozilla 开发者网络MDN)有许多文章详细解释了这些概念。

使用 Node.js 编写应用程序真的很愉快,当你意识到你可以随时使用的大量信息和工具时!使用 Node.js 内置的包管理器 npm,你可以找到成千上万的模块,只需几次按键就可以安装和在应用程序中使用!Node.js 成功的最大原因之一是 npm,它是最好的包管理器之一,学习曲线非常小。如果这是你第一次接触的包管理器,你应该觉得自己很幸运!

在一个普通的月份,npm 处理的下载量超过 10 亿次,目前有大约 15 万个包可供下载。你可以通过访问www.npmjs.com来查看可用模块的库。在你的应用程序中下载和安装任何模块就像执行以下命令一样简单:

npm install package 

你写了一个想要与世界分享的模块吗?你可以使用 npm 打包并将其轻松上传到www.npmjs.org的公共注册表中!如果你不确定安装的模块如何工作,源代码就在你的项目的node_modules/文件夹中等待探索!

npm 中的模块版本遵循语义化版本控制,例如major.minor.patch的顺序。

在开发 Web 应用程序时,你总是需要对 UI 进行验证,客户端和服务器两端都需要进行验证,因为客户端验证对于更好的 UI 体验是必需的,而服务器端验证则是为了更好地保护应用程序的安全。想想两种不同的语言在行动:你将在服务器和客户端两端实现相同的逻辑。使用 Node.js,你可以考虑在服务器和客户端之间共享通用函数,大大减少代码重复。

曾经尝试过优化从模板引擎(如 Underscore)加载的单页应用程序SPA)的客户端组件的加载时间吗?你会考虑一种方法,可以同时在服务器和客户端共享模板的渲染;有些人称之为混合模板。

Node.js 比其他任何服务器端技术更好地解决了客户端模板重复的问题,只是因为我们可以在服务器和客户端同时使用相同的 JS 模板框架和模板。

如果你对这一点持轻视态度,它解决的问题不仅仅是在服务器和客户端重用验证或模板的问题。想想正在构建的 SPA;你将需要在客户端 MV*框架中实现服务器端模型的子集。现在,想想在客户端和服务器上共享模板、模型和控制器子集。我们正在解决更高级别的代码冗余情景。

Node.js 不仅仅是用于在服务器端编写 JavaScript。是的,我们之前已经讨论过这一点。Node.js 为 JavaScript 代码在任何可以安装的地方工作设置了环境。它可以是创建命令行工具的强大解决方案,也可以是完全功能的本地运行应用程序,与 Web 或浏览器无关。Grunt.js 就是一个由 Node 驱动的命令行工具的很好例子,许多 Web 开发人员每天都在使用它来自动化任务,如构建过程、编译 CoffeeScript、启动 Node.js 服务器、运行测试等。

除了命令行工具,Node.js 在硬件领域也越来越受欢迎,尤其是 Node.js 机器人运动。Johnny-FiveCylon.js是两个流行的 Node.js 库,用于提供与机器人工作的框架。只需在 YouTube 上搜索 Node.js 机器人,你就会看到很多例子。此外,你可能正在使用一个基于 Node.js 开发的文本编辑器。GitHub 的开源编辑器 Atom 就是一个很好的例子。

Node.js 产生的一个重要原因是支持实时 Web 应用程序。Node.js 有几个专为实时 Web 应用程序构建的框架非常受欢迎:Socket.ioSock.JS。这些框架使构建即时协作应用程序(如 Google Drive 和 Mozilla 的 together.js)变得非常简单。在现代浏览器引入 WebSockets 之前,这是通过长轮询实现的,这对于实时体验来说并不是一个很好的解决方案。虽然 WebSockets 是现代浏览器中支持的功能,但Socket.io充当了一个框架,还为旧版浏览器提供了无缝的回退实现。

如果您需要了解更多关于在应用程序中使用 WebSockets 的信息,这是 MDN 上一个很好的资源,您可以探索一下:

developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications

除了 Node.js 强大的非阻塞异步特性之外,它还通过核心模块提供了强大的网络和文件系统工具。使用 Node.js 的网络模块,您可以创建接受网络连接并通过流和管道进行通信的服务器和客户端应用程序。Node 包含一个名为fs或文件系统的模块,它完全负责对文件执行的所有读写操作。它还利用了 Node 的流特性来执行这些操作。

根据功能单元划分应用程序称为微服务。每个微服务都成为自包含的部署单元。Node.js 基于通用 JS 模块模式,提供了应用程序结构的模块化。这种模式用于创建微服务。随着功能的增加,微服务的数量也在增加。为了管理这些服务,Node.js 生态系统提供了强大的库,如pm2。因此,它使应用程序的元素能够单独更新和扩展。

随着物联网IoT)的出现,Node.js 生态系统为各种设备(如传感器、信标、可穿戴设备等)提供了惊人的库支持。Node.js 被认为是管理这些设备发出的请求的理想技术,通过其强大的流和非阻塞 I/O 支撑。像 Arduino、Raspberry Pi 等流行的物联网板变种有 300 多个 Node.js 包。构建数据密集型、实时应用程序的开发人员通常会发现 Node.js 是一个自然的选择。

要看轻量级 Node.js 可以做到什么,让我们看一下启动 HTTP 服务器并向浏览器发送 Hello World 的示例代码:

var http = require('http');http.createServer(function(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World\n');}).listen(8080, 'localhost');console.log('Server running at http://localhost:8080'); 

只需几行基本的代码就可以编写一个完整的 Node.js 应用程序。使用简单的 Node.js app.js命令运行它将启动一个监听端口 8080 的 HTTP 服务器。将任何浏览器指向http://localhost:8080,您将在屏幕上看到简单的输出 Hello World!虽然这个示例应用程序实际上并没有做任何有用的事情,但它应该让您一窥使用 Node.js 编写 Web 应用程序时所拥有的强大功能。如果您还没有设置初始的 Node.js 开发环境,我们将在下一章中讨论它。

您可能听说过美国心理学家亚伯拉罕·马斯洛的这句谚语:

“如果你手中只有一把锤子,那么任何东西看起来都像钉子!”

在这种情况下,这是有道理的。Node.js 不是一种可以依赖解决您打算解决的所有应用程序问题的技术,如果选择不明智,使用它的决定将适得其反。Node.js 非常适合预期处理大量并发连接的应用程序。此外,应该注意,它最适合每个传入请求需要非常少的 CPU 周期的应用程序。这意味着,如果您打算在请求时执行计算密集型任务,它将阻塞事件循环,从而影响 Web 服务器同时处理的其他请求。Node.js 非常适合实时 Web 应用程序,如聊天室、协作工具、在线游戏等。因此,在决定是否使用 Node.js 时,我们应该认真分析应用程序的上下文,并弄清楚 Node.js 是否真的适合应用程序的上下文。

很难详细讨论 Node.js 的用例。然而,以下 Stack Overflow 主题有效地做到了这一点,我强烈建议您阅读这篇帖子上的答案,如果您对 Node.js 的用例更感兴趣:stackoverflow.com/questions/5062614/how-to-decide-when-to-use-node-js.

由于我们已经简要介绍了 Node.js 的概念和特性,现在让我们来看看 NoSQL 和 MongoDB 方面。

让我们从探讨一个问题的答案开始:什么是 NoSQL 数据库?NoSQL 是数据库技术的常见术语,它偏离了传统的关系数据库管理系统(RDBMS)概念。这些数据库解决方案偏离 RDBMS 数据库标准的常见原因是为了实现和设定比传统 RDBMS 解决方案更好的可用性和分区能力标准。

为了向您介绍这个概念,我们应该看一下布鲁尔定理,也就是 CAP 定理:

分布式计算系统不可能同时提供以下三项保证:一致性、可用性和分区容错性。

传统的 RDBMS 解决方案在一致性方面表现良好,但在提供更好的可用性(数据读取)和分区能力方面会有所妥协。大多数 NoSQL 解决方案已经朝着这个方向发展,以实现更好的数据可用性和分区。

由于这是任何偏离 RDBMS 解决方案(如 MySQL、PostgreSQL 等)概念的数据库技术的常见术语,NoSQL 数据库有各种子集。最流行的 NoSQL 子集包括文档存储、键值存储和基于图的数据库解决方案。我们将要尝试的 MongoDB 属于文档存储类别。除了 MongoDB 之外,市场上还有许多其他 NoSQL 解决方案,如 Cassandra、Redis、Neo4j、HBase 等。

正如我们在前面的段落中讨论的,MongoDB 属于 NoSQL 数据库的文档存储类别。MongoDB 由 10gen 积极开发,该公司已更名为 MongoDB Inc. MongoDB 是开源的,其源代码可在 GitHub 等各种平台上获得。

我们将看一下 MongoDB 的以下各种特性:

  • JSON 友好的数据库

  • 无模式化设计

  • 各种性能方面

MongoDB 之所以如此受欢迎的一个最重要的原因是它是一个 JSON 友好的数据库。这意味着文档以 JavaScript 对象的形式存储和检索。在内部,这些 JSON 数据在持久化到系统时会转换为 BSON 格式。因此,这提供了极大的灵活性,我们可以在客户端、服务器和最终数据库中使用相同的数据格式。

MongoDB 集合(表)中的典型文档(记录)可能如下所示:

$ mongo > db.contacts.find({email: 'jason@kroltech.com'}).pretty() { "email" : "jason@kroltech.com", "phone" : "123-456-7890", "gravatar" : "751e957d48e31841ff15d8fa0f1b0acf", "_id" : ObjectId("52fad824392f58ac2452c992"), "name" : { "first" : "Jason", "last" : "Krol" }, "__v" : 0 } 

在检查前面的输出后,我们可以看到一个名为_id的关键字。这是一个必须被编码为二进制 JSON objectID(BSON)的 MongoDB ID。如果编码失败,MongoDB 将无法检索或更新对象。

MongoDB 的另一个重要特性是其无模式化的特性。在关系型数据库中,您需要提前定义存储的数据的确切结构,这被称为模式。这意味着您必须定义表中每个字段的确切列数、长度和数据类型,并且每个字段必须始终符合该确切的一组标准。Mongo 提供了一种灵活的特性,使得您存储到数据库中的文档不需要遵循任何模式,除非开发人员通过应用程序级别强制执行它。这使得 MongoDB 非常适合基于敏捷开发,因为您可以在应用程序模式上进行即时修改。

除了友好的 JavaScript 特性之外,MongoDB 和 Node.js 之间的另一个相似之处是,MongoDB 也是为高并发应用程序和大量读操作而设计的。

MongoDB 还引入了分片的概念,这使得可以水平和垂直扩展数据库。如果应用程序所有者需要增加数据库的能力,他们可以在堆栈中添加更多的机器。这是一个相对于投资于单台机器的 RAM 来说更便宜的选择,而这将是关系型数据库解决方案的情况。

索引化的过程创建了一个称为索引的值列表,用于选择的字段。这些索引用于查询更大的数据块。使用索引可以加快数据检索速度和性能。MongoDB 客户端提供了各种方法,比如ensureIndex,只有在索引不存在时才创建索引。

此外,MongoDB 还有各种命令来允许对数据进行聚合,比如分组、计数和返回不同的值。

我们讨论的所有优点都会对一致性产生一定影响,因为 MongoDB 不严格遵守 ACID 事务等关系型数据库标准。此外,如果您最终创建了一个可能需要太多 JOIN 操作的数据模型,那么 MongoDB 可能不适合,因为它并不是设计用于太多的聚合,尽管聚合是可能通过 MongoDB 聚合框架实现的。MongoDB 可能适合也可能不适合您的应用程序。在做出决定之前,您应该真正权衡每种技术的利弊,以确定哪种技术适合您。

Node.js 和 MongoDB 在开发社区中都非常受欢迎和活跃。这对企业也是如此。财富 500 强中一些最大的公司已经完全采用 Node.js 来支持他们的 Web 应用程序。

这在很大程度上是由于 Node.js 的异步特性,使其成为高流量、高 I/O 应用程序的绝佳选择,例如电子商务网站和移动应用程序。

以下是一些正在使用 Node.js 的大公司的小列表:

  • 贝宝

  • 领英

  • eBay

  • 沃尔玛

  • 雅虎!

  • 微软

  • 道琼斯

  • 优步

  • 纽约时报

MongoDB 在企业领域的使用同样令人印象深刻和广泛,越来越多的公司采用这一领先的 NoSQL 数据库服务器。以下是一些正在使用 MongoDB 的大公司的小列表:

  • 思科

  • Craigslist 公司

  • 福布斯

  • FourSquare

  • 财捷通

  • 麦克菲

  • MTV

  • 大都会人寿

  • 旭通飞

  • 安德玛

本书的其余部分将是一次引导之旅,带领您完成一个完整的数据驱动网站的创建过程。我们创建的网站将涵盖典型大型 Web 开发项目的几乎所有方面。该应用程序将使用一种名为 Express 的流行 Node.js 框架进行开发,并将使用 MongoDB 持久化数据。在最初的几章中,我们将涵盖涉及启动服务器核心并提供内容所涉及的基础工作。这包括配置您的环境,以便您可以使用 Node.js 和 MongoDB,并对这两种技术的核心概念进行基本介绍。然后,我们将从头开始编写一个由 ExpressJS 驱动的 Web 服务器,该服务器将处理为网站提供所有必要文件。然后,我们将使用 Handlebars 模板引擎来提供静态和动态 HTML 网页。更深入地进行,我们将通过添加数据层使应用程序持久化,网站的记录将通过 MongoDB 服务器保存和检索。

我们将介绍如何编写 RESTful API,以便其他人可以与您的应用程序进行交互。最后,我们将深入了解如何为您的所有代码编写和执行测试。以下部分提供了摘要。

最后,我们将进行一个简短的旁观,检查一些越来越受欢迎的前端技术,这些技术在编写单页应用程序时变得越来越受欢迎。这些技术包括 Backbone.js、Angular 和 Ember.js。

最后但同样重要的是,我们将详细介绍如何使用 Heroku 和亚马逊 Web 服务等流行的基于云的托管服务将您的新网站部署到互联网上。

在本章中,我们回顾了本书其余部分可以期待的内容。我们讨论了 JavaScript 目前令人惊叹的状态,以及它如何可以用于支持 Web 应用程序的整个堆栈。虽然您一开始就不需要任何说服,但我希望您对开始使用 Node.js 和 MongoDB 编写 Web 应用程序感到兴奋并准备好了!

接下来,我们将设置您的开发环境,并让您使用 Node.js、MongoDB 和 npm,并编写并启动一个使用 MongoDB 的快速 Node.js 应用程序!

在本章中,我们将介绍设置开发环境所需的必要步骤。这些步骤包括以下内容:

  • 在您的计算机上安装 Node.js

  • 在您的计算机上安装 MongoDB

  • 验证一切是否设置正确

仔细遵循这些部分,因为我们需要在跳转到实际编码的章节之前,开发环境已经准备就绪。在本书的其余部分中,我们将假定您使用的是 Mac OS X、Linux 或 Windows 7/Windows 8。您还需要在计算机上拥有超级用户和/或管理员权限,因为您将安装 Node 和 MongoDB 服务器。本章之后的代码和示例将是与操作系统无关的,并且应该在任何环境中工作,只要您提前采取了我概述的准备步骤。

您需要一个合适的文本编辑器来编写和编辑代码。虽然您选择的任何文本编辑器都可以满足此目的,但选择一个更好的文本编辑器将极大地提高您的生产力。Sublime Text 3 似乎是目前最受欢迎的文本编辑器,无论在哪个平台上。这是一个简单、轻量级的编辑器,由全球开发人员提供了无限的插件。如果您使用的是 Windows 机器,那么Notepad++也是一个不错的选择。此外,还有基于 JavaScript 的开源编辑器,如 Atom 和 Brackets,也值得一试。

最后,您需要访问命令行。Linux 和 Mac 可以通过终端程序访问命令行。Mac 上一个很好的替代品是 iTerm2 (iterm2.com)。对于 Windows,默认的命令行程序可以工作,但不是最好的。那里一个很好的替代品是 ConEmu (conemu.codeplex.com)。

在本书的其余部分,每当我提到命令行或命令提示符时,它看起来像下面这样:

$ command -parameters -etc

可以通过访问官方 Node 网站并访问下载部分轻松获取 Node.js 安装程序。一旦进入那里,请确保根据您的操作系统和 CPU(32 位或 64 位)下载正确的版本。作为替代方案,您还可以使用特定于操作系统的软件包管理器进行安装。根据您使用的操作系统,只需跳转到特定的子部分,以获取有关要遵循的步骤的更多详细信息。

您可以通过以下链接跳转到 Node.js 下载部分:nodejs.org/en/download

Node 网站上有一个专门为 OS X 设计的通用安装程序。

我们需要按照以下步骤在 Mac 上安装 Node.js:

  1. 访问 Node.js 官方网站的下载页面,如前所述,单击 Mac OS X 安装程序,这与处理器类型(32 位或 64 位)无关。

  2. 下载完成后,双击.pkg文件,这将启动 Node 安装程序。

  3. 按照向导的每一步进行操作,这应该是相当简单易懂的。

此外,如果您已安装了 OS X 软件包管理器之一,则无需手动下载安装程序。

您可以通过各自的软件包管理器安装 Node.js。

  • 使用 Homebrew 进行安装:
 brew install node
  • 使用 Mac ports 进行安装:
 port install nodejs

通过安装程序或软件包管理器安装 Node.js 时,将包括 npm。因此,我们不需要单独安装它。

在 Windows 上安装 Node.js,我们将按照以下步骤进行:

  1. 我们需要确定您的处理器类型,32 位还是 64 位。您可以通过在命令提示符下执行以下命令来执行此操作:
 $ wmic os get osarchitecture

输出如下:

 OSArchiecture 64-bit
  1. 根据此命令的结果下载安装程序。

  2. 下载完成后,双击.msi文件,这将启动 Node 安装程序。

  3. 按照向导的每一步进行操作。

  4. 当您到达自定义设置屏幕时,您应该注意安装向导不仅会安装 Node.js 运行时,还会安装 npm 软件包管理器,并配置路径变量。

  5. 因此,一旦安装完成,Node 和 npm 可以在任何文件夹中通过命令行执行。

此外,如果您安装了任何 Windows 软件包管理器,则无需手动下载安装程序。您可以通过相应的软件包管理器安装 Node.js:

  • 使用 chocolatey:
 cinst nodejs.install
  • 使用scoop
 scoop install nodejs

由于 Linux 有许多不同的版本和发行版,安装 Node 并不那么简单。但是,如果您一开始就在运行 Linux,那么您很清楚这一点,并且可能对一些额外的步骤感到满意。

Joyent 在如何使用许多不同的软件包管理器选项在 Linux 上安装 Node 的出色 wiki。这涵盖了几乎所有流行的debrpm-based 软件包管理器。您可以通过访问github.com/joyent/node/wiki/Installing-Node.js-via-package-manager阅读该 wiki。

以 Ubuntu 14.04 和之前版本为例,安装 Node 的步骤如下:

$ sudo apt-get install python-software-properties $ sudo curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - $ sudo apt-get install nodejs

完成这些步骤后,Node 和 npm 应该已安装在您的系统上。

现在 Node 已经安装在您的系统上,让我们运行一个快速测试,以确保一切正常运行。

通过终端程序访问命令行并执行以下命令:

 $ node --version v8.4.3 $ npm --version 5.3.0

假设您的 Node 安装成功,您应该在屏幕上看到安装的版本号作为输出,就在您执行的命令下面。

您的版本号很可能比之前打印的要新。

您还可以启动 Node repl,这是一个命令行 shell,可以让您直接执行 JavaScript:

 $ node > console.log('Hello world!') Hello World! Undefined [press Ctrl-C twice to exit] 

您需要确保将浏览器指向 Node 的在线文档并将其加为书签,因为它无疑会成为您经常访问的资源。您不一定要逐个阅读每个部分,但一旦开始在 Node.js 中编写代码,您将需要经常参考此文档,以更多地了解 Node.js 公开的 API。该文档可在nodejs.org/api/上找到。

还可以查看 npm 注册表,网址为npmjs.com,在那里您可以找到数以万计的 Node 开发人员可用的模块。

MongoDB 也可以通过访问官方 MongoDB 网站并从www.MongoDB.org/downloads访问下载部分轻松下载。在那里,请务必根据您的操作系统和 CPU(32 位或 64 位)下载正确的版本。

对于 Windows 用户,您可以选择下载 MSI 安装程序文件,这将使安装更简单。

根据您下载的 MongoDB 版本,您将需要在以下部分中用适当的版本号替换<version>

如果您使用 Homebrew 软件包管理器,可以使用以下两个命令安装 MongoDB:

 $ brew update $ brew install MongoDB

本章的其余部分假设您没有使用 Homebrew,并且需要手动安装 MongoDB。如果您通过 Homebrew 安装 MongoDB,可以直接转到确认成功安装 MongoDB部分。

下载完成后,打开并提取.tgz文件的内容。您需要将提取的内容移动到目标文件夹/MongoDB。您可以通过查找器或命令行执行此操作,具体取决于您的喜好,如下所示:

 $ mkdir -p /MongoDB $ cd ~/Downloads $ cp -R -n MongoDB-osx-x86_64-2.4.9/ MongoDB 

您需要确保 MongoDB 二进制文件的位置已配置在您的环境路径中,以便您可以从任何工作目录执行MongoDB和 Mongo。要做到这一点,编辑您家目录(~/)中的.profile文件,并将 MongoDB 的位置追加到其中。您的.profile文件应该看起来像以下内容:

export PATH=~/bin:/some/of/my/stuff:/more/stuff:/MongoDB/bin:$PATH

如果您没有这行或完全缺少.bash_profile,您可以通过执行以下命令轻松创建一个:

 $ touch .bash_profile $ [edit] .bash_profile export PATH=$PATH:/MongoDB/bin

您很可能在前面的代码行中有比我更多的内容。重要的是在最后的$PATH之前添加:/MongoDB/bin:是不同路径之间的分隔符(因此您可能会将您的路径添加到现有列表的末尾,但在结尾的$PATH之前)。

在这里,mongod指的是您需要调用的 MongoDB 服务器实例,mongo指的是 Mongo shell,它将是您与数据库交互的控制台。

接下来,您需要创建一个默认的data文件夹,MongoDB 将用它来存储所有数据文档。从命令行执行以下操作:

 $ mkdir -p /data/db $ chown `id -u` /data/db

一旦文件已经正确解压到/MongoDB文件夹并且数据文件夹已创建,您可以通过从命令行执行以下命令来启动 MongoDB 数据库服务器:

 $ mongod

这应该会在服务器启动时输出一大堆日志语句,但最终会以以下结束:

2017-08-04T10:10:47.853+0530 I NETWORK [thread1] waiting for connections on port 27017

就是这样!您的 MongoDB 服务器已经启动并运行。您可以输入Ctrl-C来取消并关闭服务器。

完成下载后,MongoDB 网站将自动将您重定向到一个带有 Windows 快速入门指南链接的页面:

docs.MongoDB.org/manual/tutorial/install-MongoDB-on-windows/

强烈建议您遵循该指南,因为它将是最新的,并且通常会比我在这里提供的更详细。

解压已下载的 ZIP 文件到根目录c:\。默认情况下,这应该会解压一个名为MongoDB-osx-x86_64-2.4.9的文件夹。根据您用于解压的工具,您可以保持原样,也可以将目标文件夹更改为MongoDB。如果在解压过程中没有更改目标文件夹,完成后应该将文件夹重命名。无论哪种方式,确保解压出的文件位于名为c:\MongoDB的文件夹中。

接下来,您需要创建一个默认的data文件夹,MongoDB 将用它来存储所有数据文档。使用 Windows 资源管理器或命令提示符,您最熟悉的方式创建c:\data文件夹,然后使用以下命令创建c:\data\db

 $ md data $ md data\db

一旦文件已经正确解压到c:\MongoDB文件夹,并且数据文件夹随后创建,您可以通过从提示符执行以下命令来启动 MongoDB 数据库服务器:

$ c:\MongoDB\bin\mongod.exe

这应该会在服务器启动时输出一大堆日志语句,但最终会以以下结束:

2017-08-04T10:10:47.853+0530 I NETWORK [thread1] waiting for connections on port 27017

就是这样!您的 MongoDB 服务器已经启动并运行。您可以输入Ctrl-C来取消并关闭服务器。

再次,与 Windows 或 Mac 相比,我们将面临稍微更具挑战性的 Linux 安装过程。官方网站docs.MongoDB.org/manual/administration/install-on-linux/上有关于如何在许多不同的 Linux 发行版上安装 MongoDB 的详细说明。

我们将继续使用 Ubuntu 作为我们的首选版本,并使用 APT 软件包管理器进行安装:

 $ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 $ echo 'deb http://downloads-distro.MongoDB.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/MongoDB.list $ sudo apt-get update $ sudo apt-get install MongoDB-10gen

完成这些步骤后,MongoDB 应该已经安装并准备在您的系统上运行。在终端中执行以下命令以确保。这会启动 MongoDB 守护程序,并监听连接:

 $ mongod 2017-08-04T10:10:47.853+0530 I NETWORK [thread1] waiting for connections on port 27017

成功!您的 MongoDB 服务器已经运行。您可以输入Ctrl-C来取消并关闭服务器。

由于您正在开发本地开发机器而不是生产服务器,您不需要 MongoDB 服务器始终运行。这将是对您的机器不必要的负担,因为大部分时间您不会与服务器进行开发。因此,在本书的其余部分,每次启动期望连接到 MongoDB 服务器的代码时,您都需要手动启动服务器。如果您愿意,您当然可以配置 MongoDB 在本地作为服务运行并始终运行,但是如何配置超出了本章的范围。

现在 MongoDB 已经安装在您的系统上,让我们运行一个快速测试,确保一切正常运行。

通过终端程序访问命令行并执行以下命令:

 $ mongod --version db version v3.4.4 $ mongo --version MongoDB shell version v3.4.4 

假设您的 MongoDB 安装成功,您应该在屏幕上看到安装的版本号作为输出。

您的版本号很可能比之前打印的要更新。

您需要确保将浏览器指向 MongoDB 的在线文档,网址为docs.MongoDB.org/manual/,并将其加为书签,因为它无疑将成为您经常访问的资源。

现在您已经安装了所有内容并确认一切正常运行,您可以编写您的第一个快速应用程序,该应用程序将同时使用 Node 和 MongoDB。这将证明您的环境已经准备就绪,并且您已经准备好开始。此外,这将让您简单了解 Node 和 MongoDB 开发的世界!如果以下内容让您感到困惑或不合理,不要担心,本书的其余部分将会澄清一切!

首先,我们需要为我们的应用程序创建一个文件夹,该文件夹将包含此应用程序的特定代码,如下所示:

 $ mkdir testapp $ cd testapp

我们刚刚创建的testapp文件夹将是我们示例 Node 应用程序的根目录。虽然这不是必需的,但是创建package.json文件对我们的 Node 应用程序非常重要,这将包含有关应用程序的必要数据,如版本、名称、描述、开发和运行时依赖项。可以通过从testapp文件夹根目录发出以下命令来完成:

 $ npm init 

这个命令将会询问您一些问题,比如您新创建的应用的名称和版本号。您不需要一次填写所有细节,可以通过按下Enter跳过步骤,系统将使用默认值,稍后您可以更新。

在我们开始编写任何 Node.js 代码之前,我们需要使用npm安装我们的依赖项。由于这是一个基本应用程序,我们将使用它来测试我们的 Node.js 与 MongoDB 服务器的连接。因此,我们唯一需要的依赖模块是 Node.js 的原生 MongoDB 客户端。我们可以通过执行以下命令轻松安装:

 $ npm install MongoDB --save

npm安装 MongoDB 驱动程序后,您可以列出目录的内容,您会注意到一个新文件夹被创建,名为node_modules。这是所有 Node 模块的存储位置,当您从npm安装它们时,它们会存储在这里。在node_modules文件夹中,应该有一个名为MongoDB的单个文件夹。此外,您会注意到我们示例应用程序的package.json文件将被此新依赖项条目更新。

现在,让我们编写简单的应用程序代码来测试一下。这个应用程序基本上会连接到我们本地运行的 MongoDB 服务器,插入一些记录作为种子数据,然后提供输出,告诉我们数据是否被正确插入到 MongoDB 中。你可以通过以下 URL 下载代码的 Gist:bit.ly/1JpT8QL

使用你喜欢的编辑器,创建一个名为app.js的新文件,并将其保存到应用程序根目录,即testapp文件夹。只需将上面 Gist 的内容复制到app.js文件中。

现在,让我们逐个解释代码的每个部分在做什么。

 //require the mongoClient from MongoDB module var MongoClient = require('MongoDB').MongoClient; 

上面的一行需要我们通过npm安装的 MongoDB Node 驱动程序。这是 Node.js 中用于将外部文件依赖项引入当前上下文文件的必需约定。

我们将在接下来的章节中更详细地解释这一点。

//MongoDB configs var connectionUrl = 'MongoDB://localhost:27017/myproject', sampleCollection = 'chapters'; 

在上面的代码中,我们声明了要使用的数据库服务器信息和集合的变量。在这里,myproject是我们想要使用的数据库,chapters是集合。在 MongoDB 中,如果你引用并尝试使用一个不存在的集合,它将自动被创建。

下一步将是定义一些数据,我们可以将其插入到 MongoDB 中以验证一切是否正常。因此,我们在这里创建了一个章节的数组,可以将其插入到我们在前面步骤中设置的数据库和集合中:

//We need to insert these chapters into MongoDB var chapters = [{ 'Title': 'Snow Crash', 'Author': 'Neal Stephenson' },{ 'Title': 'Snow Crash', 'Author': 'Neal Stephenson' }]; 

现在,我们可以看一下其余的代码,我们将这些数据插入到 MongoDB 数据库中:

MongoClient.connect(connectionUrl, function(err, db) { console.log("Connected correctly to server"); // Get some collection var collection = db.collection(sampleCollection); collection.insert(chapters,function(error,result){ //here result will contain an array of records inserted if(!error) { console.log("Success :"+result.ops.length+" chapters inserted!"); } else { console.log("Some error was encountered!"); } db.close(); }); }); 

在这里,我们与 MongoDB 服务器建立连接,如果连接正常,db变量将拥有我们可以用于进一步操作的connection对象:

MongoClient.connect(url, function(err, db) { 

仔细看一下上面的代码-你还记得我们在第一章中学到的内容吗?我们在这里为我们进行的connection调用使用了一个callback。正如在第一章中讨论的,这个函数将被注册为一个callback,一旦连接尝试完成,就会被触发。连接完成后,这将由errordb对象触发,具体取决于我们是否能够建立正确的连接。因此,如果你看一下callback函数中的代码,我们在记录正确连接到服务器之前并没有检查是否有任何错误。现在,这就是你要在我们尝试运行这个应用程序时添加和检查的任务!看一下本节中以下代码块:

var collection = db.collection(sampleCollection); collection.insert(chapters,function(error,result){ 

这只是使用我们在连接调用中得到的db对象,并获取名为chapterscollection。记住,我们在代码开头将该值设置为sampleCollection。一旦我们获得了collection,我们就会进行insert调用,将我们在数组chapters中定义的章节放入其中。正如你所看到的,这个insert调用也是通过附加callback函数来进行的,这是一个异步调用。一旦 MongoDB 原生客户端中的代码完成了insert操作,这个callback函数就会被触发,而我们将其作为一个依赖项来使用。

接下来,我们将看一下我们传递给insert函数调用的callback函数中的代码:

if(!error) { console.log("Success :"+result.ops.length+" chapters inserted!"); } else { console.log("Some error was encountered!"); } db.close(); 

在这里,我们处理通过callback传递的值,以找出insert操作是否成功,以及已插入记录相关的数据。因此,我们检查是否有错误,如果没有,就继续打印插入的记录数。在这里,如果操作成功,结果数组将包含我们插入到 MongoDB 中的记录。

现在我们可以继续尝试运行这段代码,因为我们已经理解了它的作用。

一旦您将完整的代码保存到app.js中,就可以执行它并查看发生了什么。但是,在启动明显依赖于与 MongoDB 的连接的应用程序之前,您需要首先启动 MongoDB 守护程序实例:

 $ mongod

在 Windows 中,如果您尚未为mongod设置PATH变量,则在执行 MongoDB 时可能需要使用完整路径,即c:\MongoDB\bin\mongod.exe。对于您的需求,本书的其余部分将引用mongod命令,但您可能始终需要在每个实例中执行完整路径。

现在,要启动应用程序本身,请在app.js所在的root文件夹中执行以下命令:

 $ node app.js

当应用程序首次执行时,您应该会看到以下内容:

 Connected correctly to server Success :2 chapters inserted! 

让我们快速查看一下数据库本身,看看在应用程序执行过程中发生了什么。由于服务器目前正在运行,我们可以使用 Mongo shell 连接到它-这是 MongoDB 服务器的命令行界面。执行以下命令以使用 Mongo 连接到服务器并针对章节集合运行查询。正如您在即将看到的代码中,Mongo shell 最初连接到名为test的默认数据库。如果要切换到其他数据库,我们需要手动指定数据库名称:

 $ mongo MongoDB shell version: 2.4.8 connecting to: test > use myproject > show collections chapters system.indexes > db.chapters.find().pretty() 

在这里,pretty被用作命令的一部分,用于格式化find命令的结果。这仅在 shell 上下文中使用。它对 JSON 执行更多的美化任务。

您应该会看到类似以下输出的内容:

{ 'id' : ObjectId("5547e734cdf16a5ca59531a7"), 'Title': 'Snow Crash', 'Author': 'Neal Stephenson' }, { 'id' : ObjectId("5547e734cdf16a5ca59531a7"), 'Title': 'Snow Crash', 'Author': 'Neal Stephenson' } 

如果再次运行 Node 应用程序,记录将再次插入 Mongo 服务器。因此,如果重复执行命令多次,输出中将有更多的记录。在本章中,我们没有处理这种情况,因为我们打算只有特定的代码,这将足够简单易懂。

在本章中,我们花时间确保您的开发环境正确配置了 Node 运行环境和 MongoDB 服务器。在确保两者都正确安装后,我们编写了一个利用了这两种技术的基本应用程序。该应用程序连接到本地运行的 MongoDB 服务器,并插入了示例记录。

现在,繁琐但必要的设置和安装任务已经完成,我们可以继续一些有趣的事情并开始学习了!

在下一章中,我们将回顾 JavaScript 语言的入门知识,并了解 Node 的基础知识。然后,我们将使用 Mongo shell 回顾 MongoDB 的基本CRUDcreatereadupdatedelete)操作。

在我们深入研究并开始使用 Node 和 MongoDB 构建一个完整的 Web 应用程序之前,重温一些基础知识是很重要的。本章将为你提供一个关于语法和重要主题的速成课程。它分为两部分,前半部分侧重于 JavaScript 或 Node,后半部分涵盖 MongoDB。你将深入了解一些常见和强大的可用工具,并将回顾大量的示例代码,以便让你快速上手。

在本章中,我们将涵盖以下主题:

  • JavaScript 语言的基础知识

  • Node.js 的基础知识

  • Node 的包管理器 npm

  • MongoDB 的基础知识

在本章结束时,你应该对语法以及如何使用 Node 和 MongoDB 有扎实的理解。有很多内容需要涵盖,所以让我们开始吧。

正如我们所知,Node.js 不仅仅是另一种语言,而是 JavaScript。在编写浏览器上的 JavaScript 时使用的语言语法和工具将完全适用于服务器端。Node.js 具有一些仅在服务器上可用的附加工具,但语言和语法再次与 JavaScript 相同。我假设你对基本的 JavaScript 语法有一般的了解,但我会简要介绍一下 JavaScript 的语言,以防万一。

一般来说,JavaScript 在语法方面是一个相当简单的语言,你只需要了解一些重要的元素。

es6,或者 ECMAScript 2015,是 JavaScript 语言的更新,适用于所有类型、值、对象文字、属性、函数和程序语法。es6 的全新语义(类似于其他语言如 Java、C#等)使跨平台开发人员能够轻松学习 JavaScript。它不仅改进了语言的语法方面,还提供了新的内置工具,如 promises、proper tail calls、destructuring、modules 等。由于我们已经安装了 Node 版本 8,所有 ECMAScript 6 功能或 es2017 直至今都是包括在内的。如果你使用的是低于 4.3.2 版本的 Node,你将需要安装类似 babel.js 的转译工具。我们将通过逐步在代码中实现和进行比较研究来学习 es6。

在几乎任何编程语言中,你可以做的最基本的事情就是声明一个变量。与大多数其他语言不同,JavaScript 是一种动态类型的语言,这意味着当你声明一个变量时,它的值可以是任何类型,并且在其生命周期内可以改变。然而,相反,强类型语言规定,定义为string类型的变量必须始终是一个字符串,并且必须始终具有字符串的值。强类型特性包含在我们接下来要学习的 es6 中。目前,在 JavaScript 中声明一个变量,只需在变量名之前使用var关键字:

var myVariable; // declaring a variable with no value var myFirstName = "Jason"; var myLastName = "Krol"; var myFullName = myFirstName + ' ' + myLastName; // => Jason Krol 

前面的代码片段显示了我们如何声明变量并在声明时定义它们的初始值。+运算符用于字符串连接。

此外,我们使用驼峰命名法来命名变量。使用驼峰命名法并不是强制性的,但在面向对象的语言中,遵循驼峰命名法比基于下划线的方法更常见。

JavaScript 不会因为你忘记在每个语句的末尾加上分号而抱怨。相反,如果缺少适当的语句终止,它会尝试为你添加分号。这可能导致意想不到的结果。关于分号插入的规则在这篇文章中有解释:bclary.com/2004/11/07/#a-7.9.1

自 es6 引入了两个更多的变量声明关键字,即letconst,使 JavaScript 变得更加优雅。首先,让我们通过以下示例学习const

const loopOver = [1,2,3];

const的用法与var相同。用const声明变量会使其不可变,并且不能用于重新分配新的内容。

关于const关键字的另一个区别是,它并不意味着某物是常量,而是强调一次赋值。

通过添加以下行来测试它:

loopOver = [4,5,6];

它会抛出以下错误:

Uncaught TypeError: Assignment to constant variable

那么,为什么需要呢?对于程序员来说,推荐的做法是保持简单,这意味着使用一个变量来表示一个值。然而,我们之前讨论过变量的动态性,它有自己的优点,有时需要表示一个不可变的数据。比如存储一些服务器配置的凭据或 Node 包本身。用法可能有所不同,但都会遵循一次赋值的单一规则。

要学习let关键字,我们首先需要了解变量的作用域,这在下一节中有所涉及。

在 JavaScript 中理解变量的作用域非常重要,以更好地掌握这门语言。作用域可以被称为您的变量或函数存在的一个容器。与 Java 和其他流行的语言不同,JavaScript 遵循函数级作用域,而不是块级作用域(这在 es6 中引入)。这意味着您定义的变量将受限于其父函数绑定的作用域。

考虑以下代码片段:

var outer = 10; function myFunction() { var inner = 2; console.log(inner);// 2 console.log(outer);// 10 }myFunction();console.log(inner); 

当运行前述代码时,我们可以看到inner变量的作用域仅限于名为myFunction的父函数。它在外部是不可访问的,并且会提供一个referenceError通知。此外,外部作用域中的变量在函数作用域中是可用的,您无需额外的努力来访问它们,就像在前面的示例中看到的名为outer的变量一样。

在这种情况下需要讨论的一个重要事情是var关键字的使用。如果在声明新变量时漏掉了var,JavaScript 不会抱怨。但如果发生这种情况,情况可能会变得非常糟糕。请看以下例子:

(function (){ (function (){ a = 10; })(); })(); console.log(a);// 10 

在这里,由于在内部函数中跳过了var关键字和变量声明,JavaScript 认为应该在其父作用域中搜索该变量,然后将其附加到全局作用域,并最终使其在任何地方都可用。因此,为了避免代码中出现此类问题,通过 JSHint 等代码质量工具对代码进行检查总是很有用的。前面的代码结构可能会让你感到困惑,因为它使用了自调用函数来引入作用域。

现在,随着 es6 的到来,您可以在块级作用域中声明变量,而不仅仅是函数作用域。要理解块级作用域,让我们看下面的例子:

|

for(let i=0;i<loopOver.length;i++){console.log(`Iteration : ", i)}Console.log(`Let value of ${ i}`)

|

for(var i=0;i<loopOver.length;i++){console.log(`Iteration : ", i)}Console.log(`Let value of ${ i}`)

|

前述代码片段的唯一区别是变量i的声明。i变量在for循环块之外是不可访问的。

有关let的更多详细信息,请参考链接:developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/let

这就是关于变量作用域的全部内容。JavaScript 支持多种数据类型。让我们来看看它们。

数据类型是任何语言的基础。JavaScript 中可用的数据类型有

如下:

  • 数字

  • 字符串

  • 布尔

  • 对象

  • 空值

  • 未定义

  • 符号(es6 中新增)

在我们的代码中,我们声明的每个变量都包含属于前述类型的值。数字、字符串和布尔类型都很容易理解。这些属于语言支持的原始数据类型。在这里,一个重要的要点是要注意,JavaScript 在类型级别上没有整数或浮点数之间的区别。

数组、函数、正则表达式和日期等类型属于对象数据类型。

它们被认为是复合数据类型。因此,您定义的函数

在您的代码中也只是对象。

Null 和 undefined 是 JavaScript 支持的两种特殊类型。Null 指向

指向故意的非值,而 undefined 指向未初始化的值。因此,当您只声明变量并尚未使用值对其进行初始化时,变量将是未定义类型。最后但同样重要的是,es6 引入了一种新的原始数据类型符号。它们用于唯一的属性键和代表概念的常量。

我们没有在我们的书中使用它们,但是您可以访问以下链接以获取更多详细信息exploringjs.com/es6/ch_symbols.html

因此,在我们了解定义函数、数组和对象的各种方法之前,让我们先了解运算符和流程。

JavaScript 支持与 C 语言系列中的其他语言类似的控制结构。条件语句使用ifelse编写,并且可以使用else-if梯级将语句链接在一起。

var a = "some value"; if(a === "other value") { //do something } else if (a === "another value") { //do something } else { //do something } 

可以使用whiledo-whileforswitch语句编写控制语句。在编写 JavaScript 条件时,需要考虑的一个重要事项是了解什么等于true和/或false。大于或小于零的任何值,非 null 和非 undefined 都等于true。诸如0nullundefined字符串的字符串等于false

使用whiledo-whileforswitch语句的一些示例如下:

// for loop example var myVar = 0; for(let i = 0; i < 100; i += 1) { myVar = i; console.log(myVar); // => 0 1 ... 99 } // do while example var x = 0; do { x += 1; console.log(x); // => 1 2 ... 100 } while (x < 100); // while example while (x > 90) { x -= 1; console.log(x); // => 99 98 ... 90 } //switch example var x = 0; switch(x) { case 1 : console.log(""one""); break; case 2 : console.log("two""); break; default: console.log("none"); } // => "none" 

另一个重要的事情是要理解

使用=====进行比较。应该在何处使用==比较

变量的类型不是你关心的问题;如果还应该比较变量的数据类型,那么你应该选择===比较符号,如下面的代码所示:

const a = '5'; const b = 5; if(a == b) { //do something } if(a === b) { //do something } 

在代码片段中,第一个条件评估为 true,而第二个条件不是。因此,在编写代码时,始终更安全地依赖严格的(===)相等检查作为最佳实践。

在批准应用程序之前,建议始终通过诸如 JSHint 之类的代码质量工具运行代码。您可以通过诸如 Grunt 之类的任务运行器自动运行代码质量检查,以便每次我们更改代码时,代码质量工具都会运行并显示代码编写中是否存在任何潜在问题。

在 JavaScript 对象中,我们创建的数组甚至函数都属于相同的数据类型:Object。声明对象是一个非常简单的过程:

var myObject = {}; // that's it! 

您可以向此对象添加任何类型的属性或属性。这意味着您可以将数组、函数甚至其他对象添加为此对象的属性。向此对象添加新属性可以通过以下两种方式之一完成:

var person = {}; person.firstName = 'Jason'; // via dot operator person['lastName'] = 'Krol'; // via square brackets 

让我们看一个例子,我们将数组和函数添加为此对象的属性:

var person = {}; person.firstName = 'Jason'; // properties person.lastName = 'Krol'; person.fullName = function() { // methods return this.firstName + ' ' + this.lastName; }; person.colors = ['red', 'blue', 'green']; // array property 

您可以在前面的代码中看到,我们定义了一个名为person的基本对象,并为其分配了一些属性和一个函数。重要的是要注意在fullName函数中使用了this关键字。this关键字指的是函数所属的对象。因此,通过this关键字,函数将能够访问其所属对象的其他属性。

除了在对象创建后添加属性的方法之外,我们还可以在创建对象时将初始对象属性附加为其一部分,如下所示:

// define properties during declaration var book = { title: 'Web Development with MongoDB and NodeJS', author: 'Jason Krol', publisher: 'Packt Publishing' }; console.log(book.title); // => Web Development with MongoDB and NodeJS book.pageCount = 150; // add new properties 

在前面的示例中,我们创建对象时没有指定它们应该由哪个类创建,而是使用{}。因此,这将导致从Object基类创建此新对象,其他复合类型(如数组和函数)都是从该基类扩展的。因此,当您使用{}时,它等同于一个新的Object()

在这里,我们通过使用对象字面量{}创建的对象是Object类的实例。要为我们的应用程序定义自定义类,我们需要使用函数和原型。Mozilla 在developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript上提供了一个相当不错的教程,介绍了整个要点。es6 通过添加各种功能增强了对象属性:

首先,最重要的是属性简写。现在,使用 es6,我们可以使用变量分配属性。让我们使用以下示例来理解这一点:

let publisher = 'Packt Publishing';let book = { publisher };console.log(book.publisher);

在前面的片段中,变量值隐式分配给对象属性,声明对象时无需指定属性。

下一个令人惊叹的功能是计算对象字面量中属性键的属性。要了解此功能,让我们向前面的对象添加一个名为book的属性。

let edition = 3;let book = {publisher,[ `Whats new in ${edition} ? `] : "es6 and other improvisation"}

es6 向我们介绍了一个最期待的功能之一,称为模板文字。您可能已经注意到在前面的片段中使用了${}占位符的一种插值操作。这只是一个字符串中变量的连接,而不使用任何运算符,例如+。模板文字增强了 JavaScript 中的可读性功能,这是非常重要的。有关更多信息,请访问链接developer.mozilla.org/en/docs/Web/JavaScript/Reference/Template_literals

运行前面的代码后,我们注意到 es6 使我们能够使用方括号计算属性名称的任何计算。最后,我们可以遵循object属性中所有函数的方法表示的优雅特性。这可以在以下示例中看到:

var person = { firstName : 'Jason', lastName : 'Krol', // properties fullName() { // method notation return this.firstName + ' ' + this.lastName; } }; 

始终记住,对象只是内存位置的地址,而不是实际存储。例如,firstName: 'Jason'存储在内存位置person.firstName的地址中。到目前为止,我们已经了解了称为变量的单个存储点,让我们进一步学习多个存储点。

在 JavaScript 中,数组的工作方式与几乎任何其他语言中的工作方式相同。它们是从零开始索引的,您可以将变量声明为空数组或预填充数组。您可以操作数组中的项目,并且数组的长度不固定:

var favFoods = ['pizza', 'cheeseburgers', 'french fries']; var stuff = []; // empty array var moreStuff = new Array(); // empty array var firstFood = favFoods[0]; // => pizza// array functions: favFoods.push('salad'); // add new item// => ['pizza', 'cheeseburgers', 'french fries', 'salad'] favFoods.pop(); // remove the last item // => ['pizza', 'cheeseburgers', 'french fries'] var first = favFoods.shift(); // remove the first item // => first = 'pizza'; // => favFoods = ['cheeseburgers', 'french fries'] 

更准确地说,您可以将数组视为基本Object类的扩展子类,具有Array函数的额外实现。

在 JavaScript 中,函数是头等公民。这意味着function本身是一个对象,因此可以将其视为对象,并将其与基本Object类扩展为具有属性和附加函数。我们将看到许多情况下,我们将函数作为参数传递给其他函数,并从其他函数调用中返回函数。

在这里,我们将采用标准函数(在本例中为myFunction)。我们将为此函数分配一个timesRun属性,就像在执行任何其他对象时一样,并查看如何稍后引用该属性:

var myFunction = function() { if(this.timesRun) this.timesRun += 1; else this.timesRun = 1; // do some actual work console.log(this.timesRun); }; myFunction(); // => 1; myFunction(); // => 2; myFunction(); // => 3; 

正如我们在前面的示例中所看到的,使用 var 关键字,我们可以以与变量相同的方式定义函数:

function sayHello() { console.log('Hello!');}// or var sayHello = function() { console.log('Hello!');};

在前面的示例代码中,两种方法几乎是相同的。第一种方法是定义函数的最常见方式,称为命名函数方法。这里讨论的第二种方法是函数表达式方法,其中您将未命名函数分配为变量的引用并保持其未命名。

这两种方法之间最重要的区别与一个叫做 JavaScript hoisting 的概念有关。基本上,不同之处在于当你采用函数表达式策略时,函数在其定义语句执行之前将不会在其包含的范围内可用。在命名函数方法中,无论你在哪个位置定义它,该函数都将在其包含的范围内可用,如下面的代码所示:

one();//will display Hello two();//will trigger error as its definition is yet to happen. function one() { console.log('Hello!'); } var two = function() { console.log('Hello!'); }; two ();//will display Hello 

在前面的示例代码片段中,function one可以从其父范围的任何地方调用。但是在其表达式被评估之前,function two将不可用。

JavaScript hoisting 是指在脚本执行之前,JS 解释器将函数定义和变量声明移动到包含范围的顶部的过程。因此,在命名函数的前一个案例中,定义被移动到了范围的顶部。然而,对于函数表达式,只有变量的声明移动到了范围的顶部,将其设置为未定义,直到脚本中实际执行的那一点。你可以在code.tutsplus.com/tutorials/JavaScript-hoisting-explained--net-15092上阅读更多关于 hoisting 的概念。

通常,你需要使用一个临时函数,你不一定想提前声明。在这种情况下,你可以使用匿名函数,它只是在需要时声明的函数。这类似于我们之前探讨的函数表达式上下文,唯一的区别是该函数没有分配给一个变量,因此没有办法在以后引用它。匿名函数最常见的用法是当它们被定义为另一个函数的参数时(尤其是当用作回调时)。

使用匿名函数(即使你没有意识到它)的最常见的地方之一是与setTimeoutsetInterval一起使用。这两个标准的 JavaScript 函数将在指定的延迟时间(以毫秒为单位)后执行代码,或者在指定的延迟时间后重复执行代码。以下是其中一个setTimeout的示例,使用了内联的匿名函数:

console.log('Hello...'); setTimeout(function() { console.log('World!'); }, 5000); // => Hello... // (5000 milliseconds i.e. 5 second delay) // => World! 

你可以看到匿名函数作为第一个参数传递给了setTimeout,因为setTimeout需要一个函数。如果你愿意,你可以提前声明函数作为变量,并将其传递给setTimeout,而不是内联的匿名函数:

var sayWorld = function() { console.log('World!'); } setTimeout(sayWorld, 5000); // (5 second delay) // => World! 

匿名函数只是作为一个干净的内联一次性函数。

回调很重要,因为 JavaScript 最强大(也最令人困惑)的特性之一是它是异步的。这意味着每一行都是按顺序执行的,但它不会等待可能需要更长时间的代码(即使是按设计)。我们在第一章中通过一个例子探讨了这一点,当时我们正在研究 Node.js 的异步特性。

Mozilla 有一个关于 JavaScript 概念的详细教程,我们建议你在完成本章后阅读一次。该教程包括高级概念,比如闭包,这些概念由于主题的深度而没有在本章中涵盖。因此,请参考 Mozilla 开发网络文章developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript

JavaScript 对象表示法JSON)是处理 JavaScript 以及大多数其他语言和网络服务中的数据时使用的标准语法。JSON 的基本原则是它看起来与标准的 JavaScript 对象完全相同,只有一些严格的例外:

  • JSON 是纯文本。没有带属性的数据类型;也就是说,日期值被存储为字符串等等

  • 所有名称和字符串值必须用双引号括起来

  • 属性中不能包含函数

让我们快速看一下一个标准的 JSON 对象:

{ "title": "This is the title", "description": "Here is where the description would be", "page-count": 150, "authors": [ { "name": "John Smith" }, { "name": "Jane Doe" }, { "name": "Andrea Johnson" } ], "id": "1234-567-89012345" } 

如果您对 XML 有所了解,JSON 有些类似,只是它更容易阅读和理解。正如 ECMA 所描述的那样,“JSON 是一种文本格式,可以促进所有编程语言之间的结构化数据交换”。

在了解 JavaScript 的基础知识之后,让我们专注于 Node 的一些基础知识。我们将从理解 node.js 核心架构开始。不同的 node 特性的重要性在于它的架构和工作方式。让我们在下一节仔细研究它。

Web 应用程序通常遵循由客户端、Web 服务器和数据源组成的三层 Web 架构。在我们的上下文中,我们使用 Node.js 创建了一个 Web 应用服务器。正如我们在第一章中讨论的那样,欢迎来到全栈 JavaScript 中,Node.js 遵循单线程的架构模型。为了减少内存泄漏并在编写代码时理解异步性,我们需要了解 Node.js 的工作原理。

以下图表描述了代码的可视化表示:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (4)

每个处理组件按以下顺序进行排列:

  1. 客户端发送请求(考虑一个 HTTP 请求)。

  2. Chrome 的 v8 引擎是一个即时JIT)编译器。一旦服务器接收到请求,v8 将 JavaScript 代码转换为机器代码。

  3. Node.js 核心中的 C++ API 为其他系统级组件提供了绑定。绑定基本上是一个包装库,使得用一种语言编写的代码可以与用另一种语言编写的代码进行通信。这个 API 负责发出一个事件。

  4. 一旦事件被触发,它就被存储在事件队列中。

  5. 事件循环负责从队列中获取事件并在调用堆栈中执行它。

  6. 如果一个事件需要执行异步操作,比如使用数据库文件,它会切换执行上下文到另一个工作线程并执行。这是由 libuv 完成的。libuv 库负责处理系统中事件生命周期的异步行为。它是用 C 编写的。它维护一个线程池来处理诸如 I/O 和网络相关操作的异步请求。

  7. 一旦异步操作完成,它返回回调。回调保持在事件队列中,直到调用堆栈为空。

  8. 一旦调用堆栈为空,事件循环就会从事件队列中提取回调并在调用堆栈中执行它。

  9. 最终,事件将数据返回给 Node API。

  10. 在每个循环中,它执行单个操作。虽然操作是顺序执行的,但这个单线程的机械化事件循环非常快,以至于提供了并发的错觉。单个线程可以利用系统的单个核心;因此,它提供了更好的性能和最小的响应时间给客户端。

在其核心,Node 最强大的功能之一是它是事件驱动的。这意味着你在 Node 中编写的几乎所有代码都是以响应事件的方式编写的,或者是自身触发事件(进而触发其他代码监听该事件)。

让我们来看一下我们将在后面的章节中编写的处理使用 Mongoose 连接到 MongoDB 服务器的代码,Mongoose 是一个流行的 Node.js MongoDB 对象文档映射ODM)模块:

mongoose.connect('); mongoose.connection.on('open', function() { console.log("Connected to Mongoose..."); }); 

首先,我们告诉我们的 mongoose 对象连接到作为参数传递给函数的服务器。连接可能需要一段时间,但我们无法知道需要多长时间。因此,我们绑定了一个监听器到 mongoose.connection 对象的 open 事件上。通过使用 on 关键字,我们指示当 mongoose.connection 对象触发 open 事件时,执行作为参数传递的匿名函数。

早些时候,我们在浏览器中使用 setTimeout 来回顾异步 JavaScript 代码的概念;这些原则在 Node 的世界中更为强大。由于您可能会对不同的 REST API 服务、数据库服务器和其他任何内容进行许多网络相关的连接,因此很重要的是,您的代码可以平稳执行,并且在每个服务响应时都有适当的回调使用。

为了使代码尽可能模块化和可重用,Node 使用了一个模块系统,允许您更好地组织代码。基本前提是,您编写一个满足单一关注点的代码,并使用 module.exports(或简单地 exports)将此代码导出为服务于该单一目的的模块。每当您需要在代码库的其他地方使用该代码时,您将需要该模块:

// ** file: dowork.js module.exports = { doWork: function(param1, param2) { return param1 + param2; } } // ** file: testing.js var worker = require('./dowork'); // note: no .js in the file var something = 1; var somethingElse = 2; var newVal = worker.doWork(something, somethingElse); console.log(newVal); // => 3 

使用这个系统,可以简单地在许多其他文件中重用模块(在本例中是 dowork 模块)中的功能。此外,模块的各个文件充当私有命名空间,这意味着每个文件定义一个模块并且被单独执行。在模块文件中声明和使用的任何变量都是私有的,不会暴露给通过 require() 使用模块的任何代码。开发人员可以控制模块的哪一部分将被导出。这种模块的实现被称为commonJs模块模式。

在我们总结 Node.js 中的模块系统之前,我们需要了解 require 关键字。require 关键字接受文件地址作为字符串,并将其提供给 JavaScript 引擎编译为 Module._load 方法。Module._load 方法首次执行时,实际上是从导出的文件中加载,并且进一步进行缓存。缓存是为了减少文件读取次数,可以显著加快应用程序的速度。此外,当下次加载模块时,它会从缓存中提供已加载模块的实例。这允许在项目中共享模块,并保持单例状态。最后,Module._load 方法返回所述文件的 module.exports 属性在其各自的执行中。

模块系统也可以无限扩展。在您的模块中,您可以要求其他模块,依此类推。在导入时要确保不会导致所谓的循环依赖。

循环依赖是指模块直接或间接地要求自身的情况。我们可以从以下链接的讨论中了解更多:

stackoverflow.com/questions/10869276/how-to-deal-with-cyclic-dependencies-in-node-js

Node.js 核心实际上有数百个模块可供您在编写应用程序时使用。这些模块已经编译成二进制,并在 Node.js 源代码中定义。其中包括以下内容:

  • 事件

  • 文件系统

与其他语言一样,Node.js 核心还提供了使用fs模块与文件系统交互的能力。该模块配备了不同的方法,用于同步和异步地执行文件的不同操作。参考第一章。欢迎来到全栈 JavaScript,了解更多关于同步和异步的区别。fs的同步方法以关键字 Sync 结尾,例如readFileSync。要深入了解模块,请参考以下链接:nodejs.org/api/fs.html

HTTP 模块是 Node.js 核心中最重要的模块之一。HTTP 模块提供了实现 HTTP 客户端和服务器的功能。

以下是创建基本服务器和客户端所需的最小代码:

HTTP 服务器HTTP 客户端

|

const http = require('http');//create a server objecthttp.createServer((req, res)=>{ res.write('Hello Readers!'); //write a response to the client res.end(); //end the response}).listen(3000); //the server object listens on port 3000

|

const http = require('http');http.get({ hostname: 'localhost', port: 3000, path: '/'}, (res) => { res.setEncoding('utf8'); res.on('data', (chunk)=>{ console.log(`BODY: ${chunk}`); });});

|

考虑到前面的代码,一旦模块被引入,我们就可以使用 HTTP 对象的实例来创建服务器或请求另一端的服务器。createServer方法需要一个回调作为参数。每当服务器受到 HTTP 请求时,都会调用这个callback。此外,它还提供一个响应对象作为参数,以便相应地处理返回的响应。

前面的 HTTP 模块是使用 net 模块连接的。根据 node.js api 的文档,net 模块提供了用于创建基于流的 TCP 或 IPC 服务器的异步网络 API。这是 Node 的核心编译二进制库之一,它与内部 C 库 libuv 交互。libuv 库负责处理异步请求,如 I/O 和网络相关操作。最好的参考文档是 Node 自己的文档:nodejs.org/api/net.html

流是核心模块中最重要的模块之一。简单来说,流是从特定来源接收的数据流的小数据块。在接收端,它可能既没有所有的流数据,也不必一次性将其全部放入内存。这使我们能够使用有限的资源处理大量数据。我们可以通过 Dominic Denicola 提供的类比来形象地描述流。根据他的说法:

"流是异步可迭代对象,就像数组是同步可迭代对象一样"。

考虑到我们需要在进行多次读写操作的环境中读取大文件数据。在这种情况下,流提供了一个强大的抽象来处理低级 I/O 系统调用,同时提供性能优势。

内部流模块不应直接使用,以避免在 Node 版本之间发生行为变化。但是,我们可以在 npm 上使用可读流等包装模块。

尽管在我们的书的上下文中并未广泛使用流,但它是 Node.js 核心的一个支柱特性,被其内部模块使用,并一直是 Node.js 生态系统的重要组成部分。要了解更多关于流的信息,请访问以下链接:community.risingstack.com/the-definitive-guide-to-object-streams-in-node-js/

一定要查看 Node 的在线文档:nodejs.org/api,以查看 Node 核心中可用模块的完整列表,并查看大量示例代码和解释。

Node 中的模块系统非常强大,使用其他开发者编写的第三方模块非常简单。Node 包含了自己的包管理器npm,它是一个注册表,目前包含了超过 475,000 个用 Node 编写的模块。这些模块完全开源,并且可以通过几个简短的命令让你使用。此外,你也可以通过 npm 发布你自己的个人模块,并允许世界上的任何人使用你的功能!

假设你想要在你的项目中(我们在本书后面会使用的)包含一个流行的 web 框架express。下载一个模块并在你的代码中使用它只需要两个简单的步骤:

 $ npm install express // ** file: usingnpm.js var express = require('express'); 

就是这样!真的,就是这么简单!从你的项目所在的文件夹的命令行中,只需要执行npm install package-name,这个包就会从 npm 下载并存储在你的项目中的一个叫做node_modules的文件夹中。如果你浏览node_modules文件夹,你会发现一个你安装的包的文件夹,在这个文件夹中,你会找到这个包本身的原始源代码。一旦这个包被下载,使用require()在你的代码中就会变得非常简单。

有时候你可能想要全局安装一个 Node 包,比如说,当你使用一个叫做 Grunt.js 的流行命令行构建工具的时候。要全局安装一个 npm 包,只需要包含-g或者--global标志,这个模块就会被安装为一个全局可执行文件。当全局安装 npm 包时,这个包的源文件并不会存储在特定项目的node_modules文件夹中,而是存储在你机器的系统目录下的node_modules文件夹中。

npm 的一个非常强大的特性是它允许其他开发者快速、简单、一致地在他们的本地环境中启动你的代码。Node 项目通常包括一个特殊的文件叫做package.json,其中包含了关于项目的信息以及项目依赖的所有 npm 包的列表。拥有你本地代码副本的开发者只需要执行npm install就可以通过这个文件下载并在本地安装每个依赖。

如果你想要安装的依赖被保存到package.json文件中,npm install标志--save或者--save-dev是必需的。如果你正在开始一个新项目,不想手动创建一个package.json文件,你可以简单地执行npm init并回答几个快速的问题来快速设置一个默认的package.json文件。在init期间,如果你想的话可以留空每个问题并接受默认值:

 $ npm init $ npm install express --save $ npm install grunt --save-dev $ cat package.json { "name": "chapter3", "version": "0.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "express": "³.5.1" }, "devDependencies": { "grunt": "⁰.4.4" } }

dependenciesdevDependencies部分列出了expressgrunt。这两个部分的区别在于,dependencies部分对于应用程序的正常运行是绝对关键的,而devDependencies部分只包含了在项目开发过程中需要安装的包(比如 Grunt 用于各种构建步骤、测试框架等)。如果你对包版本中的^符号的使用感到困惑,它用于更新依赖到最新的次要版本或者补丁版本(第二个或第三个数字)。¹.2.3将匹配任何 1.x.x 版本,包括 1.3.0,但不会包括 2.0.0。所以,在我们的例子中,³.5.1的 Express.js 将寻找最新的 express.js 的次要版本,但不会接受 4.0.0,因为这是一个主要版本。

由于 MongoDB 主要由 JavaScript 驱动,Mongo shell 充当了一个 JavaScript 环境。除了能够执行常规的 Mongo 查询之外,你还可以执行标准的 JavaScript 语句。在 JavaScript 入门中提到的大部分内容同样适用于 Mongo shell。

在这一节中,我们将主要关注通过 Mongo shell 执行标准 CRUD 操作的各种方法。

要访问 Mongo shell,只需从任何终端执行mongo。Mongo shell 需要mongod服务器当前正在运行并且可用于机器,因为它的第一件事就是连接到服务器。使用以下命令访问 Mongo shell:

 $ mongo MongoDB shell version: 2.4.5 connecting to: test >

默认情况下,当您首次启动 Mongo 时,您会连接到本地服务器,并设置为使用test数据库。要显示服务器上所有数据库的列表,请使用以下命令:

 > show dbs

要切换到show dbs输出中列出的任何数据库,请使用以下命令:

 > use chapter3 switched to db chapter3

值得注意的是,如果您在一个不存在的数据库上使用use

将自动创建一个。如果您正在使用现有数据库,并且想要查看数据库中的集合列表,请执行以下命令:

 > show collections

在我chapter3数据库的情况下,由于它是自动生成的新数据库,我没有现有的集合。MongoDB 中的集合类似于关系数据库中的表。

由于我们正在使用chapter3数据库,这是一个全新的数据库,目前里面没有集合。您可以通过简单地引用一个新的集合名称和db对象来使用任何集合(表):

> db.newCollection.find() > 

在空集合上执行find操作只会返回空。让我们插入一些数据,这样我们就可以尝试一些查询:

> db.newCollection.insert({ name: 'Jason Krol', website: 'http://kroltech.com' }) > db.newCollection.find().pretty() { "_id" : ObjectId("5338b749dc8738babbb5a45a"), "name" : "Jason Krol", "website" : "http://kroltech.com" } 

在我们执行简单的插入(基本上是一个 JavaScript JSON 对象)之后,我们将在集合上执行另一个find操作,并且返回我们的新记录,这次还添加了一个额外的_id字段。_id字段是 Mongo 用来跟踪每个文档(记录)的唯一标识符的方法。我们还在find()的末尾链接了pretty()函数,这样可以更好地输出结果。

继续插入一些记录,这样您就有一些数据可以在下一节进行查询时使用。

在 MongoDB 集合中查询和搜索文档非常简单。仅使用没有参数的find()函数将返回集合中的每个文档。为了缩小搜索结果,您可以提供一个JSON对象作为第一个参数,其中包含尽可能多或尽可能少的特定信息以匹配,如下面的代码所示:

> db.newCollection.find({ name: 'Jason Krol' }) { "_id" : ObjectId("533dfb9433519b9339d3d9e1"), "name" : "Jason Krol", "website" : "http://kroltech.com" }

您可以包含额外的参数来使搜索更精确:

> db.newCollection.find({ name: 'Jason Krol', website: 'http://kroltech.com'}){ "_id" : ObjectId("533dfb9433519b9339d3d9e1"), "name" : "Jason Krol", "website" : "http://kroltech.com" }

对于每个结果集,每个字段都包含在内。如果您只想返回特定的一组字段,您可以将map作为find()的第二个参数包括:

> db.newCollection.find({ name: 'Jason Krol' }, { name: true }) { "_id" : ObjectId("533dfb9433519b9339d3d9e1"), "name" : "Jason Krol" }> db.newCollection.find({ name: 'Jason Krol' }, { name: true, _id: false }) { "name" : "Jason Krol" } 

_id字段将始终默认包含,除非您明确声明不想包含它。

此外,您可以使用查询运算符来搜索范围内的内容。这些包括大于(或等于)和小于(或等于)。如果您想对作业集合执行搜索,并且想要找到每个分数在 B 范围内(80-89)的文档,您可以执行以下搜索:

> db.homework_scores.find({ score: { $gte: 80, $lt: 90 } }) 

最后,您可以在执行搜索时使用regex来返回多个匹配的文档:

> db.newCollection.find({ name: { $regex: 'Krol'} }) 

前面的查询将返回包含单词Krol的每个文档。您可以使用regex语句进行高级查询。

如果您知道您将在查询中返回多个文档,并且只想要第一个结果,请使用findOne()代替常规的find()操作。

要更新记录,请使用update()函数,但将查找查询作为第一个参数包括:

> db.newCollection.update({ name: 'Jason Krol' }, { website: 'http://jasonkrol.com' })

这里有一个小问题。如果你执行一个新的find({ name: 'Jason Krol' })操作,会发生一些奇怪的事情。没有返回数据。发生了什么?好吧,update()函数的第二个参数实际上是完整文档的新版本。因此,你只想要更新website字段,实际发生的是找到的文档被新版本替换,新版本只包含website字段。重申一下,之所以会发生这种情况,是因为在 NoSQL(如 MongoDB)中,文档没有固定数量的字段(如关系数据库)。要解决这个问题,你应该使用$set运算符。

> db.newCollection.update({ name: 'Jason Krol' }, { $set: { website: 'http://jasonkrol.com'} })

也许有一天你想要更新一个文档,但文档本身可能存在,也可能不存在。当文档不存在时,如果你想根据提供的更新值立即创建一个新文档,会发生什么?好吧,有一个很方便的函数专门用于这个目的。将{upsert: true}作为update()函数的第三个参数传递:

> db.newCollection.update({ name: 'Joe Smith' }, { name: 'Joe Smith', website: 'http://google.com' }, { upsert: true })

如果我们有一个name字段匹配Joe Smith的文档,website

字段将被更新(并且name字段将被保留)。但是,如果我们没有

匹配的文档,将自动创建一个新文档。

删除文档的工作方式几乎与find()完全相同,只是不是查找和返回结果,而是删除与搜索条件匹配的文档:

> db.newCollection.remove({ name: 'Jason Krol' }) 

如果你想要核心选项,你可以使用drop()函数,它将删除集合中的每个文档:

> db.newCollection.drop() 

对于 JavaScript 的进一步学习,我建议你查看以下一些资源:

  • Mozilla 开发者网络位于developer.mozilla.org/en-US/docs/Web/JavaScript

  • Secrets of the JavaScript NinjaJohn ResigBear BibeaultManning

  • Learning JavaScript Design PatternsAddy OsmaniO'Reilly

  • JavaScript: The Good PartsDouglas CrockfordO'Reilly

Node API 在线文档将是你全面了解 Node 核心模块中所有可用内容的最佳选择。Node API 文档可以在nodejs.org/api找到。

此外,有一个很棒的网站,教你使用实际的编程问题来学习 Node。这些练习的重点是理解 Node 的工作原理,并深入了解流、异步 I/O、promises 等基本知识。Node school 可以在nodeschool.io找到。

最后,MongoDB 的创建者提供了一个令人惊叹的 7-8 周在线培训和认证计划,完全免费,你将学到成为真正的 MongoDB 大师所需的一切。这可以在 MongoDB 大学的university.mongodb.com找到。

现在是时候深入进入并开始编写一些真正的代码了!

在本章中,你快速学习了 JavaScript、Node.js 和 MongoDB 的基础知识。此外,你还了解了 Node 的包管理器 npm。为了进一步学习,提供了 JavaScript、Node.js 和 MongoDB 的额外资源。

在下一章中,你将使用 Express.js 编写你的第一个 Node web 服务器,并开始创建一个完整的 Web 应用程序。

当我们需要构建一个完整的 Web 应用程序时,从头开始编写整个应用程序并不是最佳的方法。我们可以使用一个维护良好、编写良好的 Web 应用程序框架来构建我们的应用程序,以减少开发工作量并提高可维护性。

在本章中,我们将涵盖以下主题:

  • 探索 Express.js Web 应用程序框架

  • 探索 Express.js 的各种元素

  • 使用 Express 开发必要的代码来引导 Web 应用程序

简而言之,Web 框架使得开发 Web 应用程序变得更容易。考虑将常用功能分解为可重用模块的方面。这正是框架所做的。它们带有许多可重用模块,并强制执行代码的标准结构,以便全世界的开发人员更容易地浏览和理解应用程序。

除了所有这些优点之外,Web 框架大多由全世界的开发人员维护。因此,开发人员将新的 bug 修复和底层语言的功能整合到框架版本中的工作量最小化,我们只需要升级应用程序使用的框架版本。因此,使用 Web 框架构建 Web 应用程序为开发和维护阶段带来了许多优势。

我们将在整本书中使用的 Express.js 框架是基于模型-视图-控制器MVC)的 Web 应用程序框架。MVC 只是一种架构设计模式:

  • 模型:模型用于表示 Web 应用程序的数据或实体。

它更接近实例,这些实例存储应用程序的数据,通常是数据库或 Web 服务。

  • 视图:视图负责将应用程序呈现给最终用户。因此,视图可以被视为应用程序的呈现层。

  • 控制器:现在,你可能想知道控制器在 Web 应用程序中的作用。控制器的作用就是将模型与相应的视图粘合在一起,并负责处理用户对应用程序中特定 Web 页面的请求。

如果你第一次听到这个概念,可能会有点难以理解。但是在阅读完本章之后,我们会向你展示各种例子,让你逐渐熟悉这些概念。

正如它在主页上完美描述的那样,Express 是一个最小化和灵活的 Node.js

Web 应用程序框架,提供了一套强大的功能,用于构建单页、多页和混合 Web 应用程序。换句话说,它提供了所有你需要的工具和基本构建块,只需编写很少的代码就可以让 Web 服务器运行起来。它让你专注于编写你的应用程序,而不用担心基本功能的细节。

Express 框架是最流行的基于 Node 的 Web 框架之一,也是npm中最流行的包之一。它是基于 Sinatra Web 框架构建的,在 Ruby 世界中非常流行。有很多跨语言的框架都受到 Sinatra 简单性的启发,比如 PHP 的 Laravel 框架。因此,Express 是 Node.js 世界中基于 Sinatra 的 Web 框架。

如果你看一段代码示例,Express 的最基本实现之一,你会发现启动 Web 服务器是多么容易,例如:

const express = require('express'); const app = express(); app.get('/', (req, res)=>{ res.send('Hello World'); }); app.listen(3300); 

Express 的美妙之处在于它使得构建和维护网站的服务器代码变得简单。

从本章开始,我们将构建一个完整的 Web 应用程序。

我们将要构建的 Web 应用程序将是一个流行的社交图片分享网站imgur.com的克隆。我们将称我们的网站为imgPloadr.io

网站的要求如下:

  • 主页将允许访问者上传图片,并浏览已上传的图片,这些图片将按从新到旧的顺序进行排序。

  • 每个上传的图片将通过自己的页面呈现,显示其标题、描述和大图像。访问者将能够喜欢图片并发表评论。

  • 一个一致共享的侧边栏将在两个页面上可见,并展示有关网站的一般统计信息,最受欢迎的图片和最近的评论。

该网站将使用 Bootstrap,以便具有漂亮的专业设计,并且在任何设备上都能响应。

以下屏幕截图是完成网站的主页:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (5)

以下屏幕截图是网站上图片的详细页面:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (6)

在编写任何代码之前,我们希望确保您已经正确设置了项目文件夹,并具有正确的文件夹结构来存放您将要创建的各种文件。首先创建一个新的项目文件夹,并为其命名。然后,在该文件夹内,创建额外的文件夹以匹配以下结构:

/(project root) ---/helpers ---/controllers ---/public ------/css ------/img ------/js ------/upload ---/server ---/Views ------/layouts ------/partials 

这些文件夹中的每一个都将包含我们在本章和本书的其余部分中编写的重要模块。

如果您通过 Yeoman 使用基于 Express 的生成器,您将获得必要的文件夹结构和依赖项与样板代码导入。然而,由于我们的意图是了解这个框架,我们将跳过这一步。访问yeoman.io/了解更多关于Yeoman功能的信息。

您需要一个package.json文件用于这个项目,创建这个文件的最简单方法是从项目文件夹的根目录执行以下命令:

$ npm init 

在提示时回答每个问题,或者简单地重复按Enter接受默认值。现在,让我们通过npm安装 Express 及其必要的依赖项:

$ npm install express morgan body-parser cookie-parser method-override errorhandler express-handlebars --save 

这将在node_modules文件夹中安装 Express 框架,并且还将在package.json文件的依赖项部分中添加 Express。请注意,在撰写本书时,Express.js 处于其 4.x.x 版本。在这里,您可以看到,Express 是一个完全解耦的框架,它本身并不带有很多打包的模块。相反,您可以找出应用程序的依赖关系,并随时插入和拔出应用程序。如果您从一开始就一直关注 Express 的发展,您一定会注意到这些变化是作为 Express 4.x.x 版本的一部分引入的。在此版本之前,Express 通常会随附许多内置模块。在这里,我们与 Express 一起安装的模块是我们在构建完整 Web 应用程序时应用程序具有的各种依赖项。我们将在本章的后面部分讨论每个模块的使用。

安装 Express 和必要的依赖项之后,开发应用程序的下一步将是创建一个文件,该文件将作为应用程序的默认入口点。我们将执行此文件来启动我们的 Web 应用程序,并且它将包含必要的代码来要求依赖模块,并在开发服务器上监听指定的端口。

我们暂时将入口点文件命名为server.js,并且保持它非常简洁,以便内容相当自解释。在这个文件中执行的任何主要逻辑实际上将被延迟到其他文件中托管的外部模块中。

server.js中我们无法做任何事情之前,我们需要引入一些我们将要使用的模块,特别是 Express:

const express = require('express'); // config = require('./server/configure'); let app = express(); 

在前面的代码中,我们将express模块分配给express变量。config模块实际上将是我们自己编写的模块,但目前由于它不存在,我们将保留该行的注释。最后,我们将声明一个名为app的变量,这实际上是 Express 框架在执行时返回的内容。这个app对象驱动我们整个app应用程序,这就是它如此巧妙地命名的原因。

在本章和本书的其余部分中,我可能会在示例中包含已注释的代码(以//开头的代码)。这样,当我们使用已注释的行作为参考点时,或者当我们通过简单取消注释代码来启用这些功能时,跟随将会更容易。

接下来,我们将通过app.set()函数在app对象中设置一些简单的设置。这些设置实际上只是为了定义一些我们可以在代码的其余部分中使用的应用级常量,以便我们可以方便地使用它们作为快捷方式:

app.set('port', process.env.PORT || 3300); app.set('Views', `${__dirname}/Views`); // app = config(app); 

代码解释如下:

  • 前面代码的前两行使用了 Node 中的内置常量。process.env.PORT常量是设置在实际机器上的环境设置,用于服务器的默认端口值。如果在机器上没有设置端口值,我们将硬编码一个默认值3300来代替使用。

  • 之后,我们将我们的 Views(HTML 模板)的位置设置为

${__dirname}'/Views,或者使用另一个 Node 常量,/Views

在当前工作目录中的文件夹。

  • 代码的第三行引用了尚未编写的config模块,因此该行被注释掉了。

  • 最后但并非最不重要的是,我们将使用我们的app对象创建一个 HTTP 服务器,并告诉它监听连接:

app.get('/', (req, res) => { res.send('Hello World');});app.listen(app.get('port'), () => { console.log(`Server up: http://localhost:${app.get('port')}`);});

在这里,我们在我们的应用程序中设置了一个路由,以响应Hello World消息。如果任何用户请求我们应用程序的根目录,它将会响应一个Hello World消息。代码的最后部分是在我们的应用程序上调用listen()函数,告诉它要监听哪个端口,并传入一个简单的匿名回调函数,一旦服务器启动并监听,就会执行一个简单的console.log()消息。就是这样!再次确保将此文件保存为项目根目录下的server.js。您已经准备好运行您的服务器,看看它是否正常工作。

让我们来测试一下您的服务器的运行情况:

$ node server.jsServer up: http://localhost:3300 

太棒了!到目前为止,您的服务器实际上并没有做任何伟大的事情。尝试将浏览器指向http://localhost:3300。您应该会收到一个非常基本的消息,上面写着Hello World!如果您请求端口上的任何其他路由,例如http://localhost:3300/,它将会响应一个无法获取的响应。这是因为您还没有配置任何路由或任何实际逻辑在您的服务器中,来处理特定的请求,只有一个对/默认路由的GET请求。

在设置路由之前,我们应该了解 Express 中间件的概念,这对于理解我们应用程序的自定义依赖模块如何与我们的正常应用程序流集成是至关重要的。

您可以在运行服务器之前,直接从命令行设置任意数量的环境变量,执行类似以下命令的操作:

$ PORT=5500 node server.jsServer up: http://localhost:5500 

您还可以在环境设置中永久设置环境变量。通常可以通过编辑您的.profile文件或等效文件来完成此操作。

Express 提供的最强大的功能之一是中间件的概念。中间件背后的思想是,它就像一个过滤器堆栈,每个对服务器的请求都会通过。每个请求都会经过每个过滤器,并且每个过滤器可以对请求执行特定任务,然后再传递到下一个过滤器。

为了更好地理解,这里是中间件的图解视图:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (7)

通常,这些过滤器用于诸如 cookie 解析、表单字段处理、会话处理、身份验证、错误处理和日志记录等任务。清单不胜枚举。您可以使用数百个第三方模块,也可以简单地编写自己的自定义中间件。

毫无疑问,总有一天你会想要编写自己的自定义中间件,除了Connect或任何其他第三方提供的现有中间件。在 Node 中编写自定义中间件之前,习惯性地首先搜索www.npmjs.org/,因为很有可能其他人已经完成了这项工作。

编写自定义中间件非常简单。在使用 Express 框架时,它记录了各种类型的中间件,我们可以简单地将其分类为两种类型,即应用程序级和基于路由的中间件。

以下是应用程序级中间件的超级基本示例:

app.use((err, req, res, next)=> { // do whatever you want here, alter req, alter res, throw err etc. return next(); });

app.use函数允许我们注册为中间件。在基本级别上,它是一个在http.createServer方法中接收请求时调用的函数。此外,我们需要编写一个接受四个参数的函数:errreqresnext

  • 第一个参数是一个错误对象,如果在您的中间件运行之前有任何堆栈错误,该错误将被传递给您的中间件,以便您可以相应地处理它。这是一个可选参数;因此,如果对特定功能的实现不需要错误处理,我们可以跳过它。

  • 你已经熟悉了reqres参数,已经编写了你的路由。

  • 第四个参数实际上是一个回调的引用。这个next参数是中间件堆栈能够像堆栈一样运行的方式,每个执行并确保管道中的下一个中间件通过next返回和调用。

app.use方法还接受第一个参数作为路由或端点。这形成了之前提到的第二种中间件类型,称为基于路由的中间件。以下是语法:

app.use('/get_data', (err, req, res, next)=>{ console.log('Hello world!') return next(); }, (err, req, res, next)=>{ console.log('Hello world Again!') return next();});

因此,这表明我们不是将中间件应用于所有传入的请求,而是将其特定于一个路由并调用路由匹配。

在编写自定义中间件时唯一要记住的重要事情是你有正确的参数并且返回next()。其余完全取决于你!

中间件被调用的顺序非常重要。再次使用过滤器的概念,作为通过每个过滤器的请求,您要确保它们按正确的顺序执行其职责。一个很好的例子是在会话处理程序之前实现 cookie 解析器,因为会话通常依赖于 cookie 来在请求之间维护与用户的状态。

中间件顺序重要的另一个很好的例子涉及错误处理。如果你的任何中间件遇到错误,它们将简单地将该错误传递给堆栈中的下一个中间件。如果最后一个中间件,无论是什么,都不能优雅地处理该错误,它基本上会显示在你的应用程序中作为堆栈跟踪(这是不好的)。将错误处理程序配置为最后一个中间件之一就像是在说“如果一切都失败,并且在以前的中间件的任何时候发生故障,请优雅地处理它。”

我们已经安装的各种依赖项将被集成到我们的代码中作为中间件。我们将通过config模块来执行这个集成各种中间件的任务,因为它将帮助我们使server.js文件更加精简,并增加代码的可读性。

由于我们保持server.js文件非常简洁,因此在配置服务器时仍需要相当多的逻辑。为此,我们将使用一个名为configure的自定义模块。首先,在server文件夹中创建一个configure.js文件。当我们首次安装 Express 时,我们已经安装了自定义依赖项。

现在模块已安装并准备好使用,让我们开始编写configure.js文件。首先,像我们的任何模块一样,我们将声明我们的依赖项:

const path = require('path'), routes = require('./routes'), exphbs = require('express-handlebars'),), express = require('express'), bodyParser = require('body-parser'), cookieParser = require('cookie-parser'), morgan = require('morgan'), methodOverride = require('method-override'), errorHandler = require('errorhandler'); module.exports = (app)=>{ app.use(morgan('dev')); app.use(bodyParser.urlencoded({'extended':true})); app.use(bodyparser.json()); app.use(methodOverride()); app.use(cookieParser('some-secret-value-here')); routes(app);//moving the routes to routes folder. app.use('/public/', express.static(path.join(__dirname, '../public'))); if ('development' === app.get('env')) { app.use(errorHandler()); } return app; }; 

在前面的代码中,我们为我们自定义的configure模块中将要使用的每个模块声明了const。然后,我们定义了实际将由此代码文件导出的模块,更具体地说是一个接受我们的app对象作为参数的函数,并返回相同对象(在我们对其进行一些配置修改后)。

您应该看到我们需要 Connect,它实际上是 Express.js 的核心依赖项之一,默认安装。Connect 是一个流行的第三方中间件框架,我们将在本章后面更多地了解它。

让我们来看看我们在前面的代码中使用的每个 Connect 中间件:

  • morgan:这是负责记录日志的模块。这对调试您的 Node 服务器非常有帮助。

  • bodyParser:这有助于方便打包通过浏览器的 HTML 表单提交的任何表单字段。通过POST请求提交的表单字段将通过req.body属性可用。

  • methodOverride:对于不正确支持 REST HTTP 动词的旧浏览器,如UPDATEPUTmethodOverride中间件允许使用特殊的隐藏输入字段来伪造它。

  • cookieParser:这允许发送和接收 cookie。

  • errorHandler:这处理整个中间件过程中发生的任何错误。通常,您会编写自己的自定义errorHandler,可能会呈现默认的 404 HTML 页面,将错误记录到数据存储中,等等。

  • handlebars:这是我们将与视图一起使用的模板引擎。我们将在接下来的部分中更多地解释如何集成它。

routes(app)行是 Express 的一个特殊组件,表示您实际上正在使用路由器与服务器,您可以响应GETPOSTPUTUPDATE等请求。由于您正在使用 Express 路由器作为最后一个中间件之一,我们还将在下一节中定义实际的路由。

最后,express.static()中间件用于从预定义的静态资源目录向浏览器呈现静态内容文件。这很重要,这样服务器可以提供静态文件,如.js.css图像regular.html,以及您可能需要提供的任何其他文件。静态中间件将从 public 目录提供任何静态文件,就像以下代码一样:

http://localhost:3300/public/js/somescript.jshttp://localhost:3300/public/img/main_logo.jpg

重要的是,您的静态中间件在app.router()之后定义,这样静态资产不会意外地优先于您可能已定义的匹配路由。

现在您的configure.js文件已经完成,您可以从主server.js文件中调用它了。如果您还记得,我们在configure模块中包含了两行被注释掉的代码。现在是时候取消注释这两行了,这样当您运行服务器时,您的configure模块将发挥作用。这两行现在应该是这样的:

config = require('./server/configure'), app = config(app); 

通过执行server.js节点再次启动服务器,一切应该仍然运行顺利。现在,是时候在我们的应用程序中加入更多路由了,除了我们之前添加的Hello World路由。

到目前为止,你有你的server.js文件和一个configure模块,用于连接应用程序所需的所有中间件。下一步是实现适当的路由器和必要的控制器。

路由将是应用程序中每个可用 URL 路径的映射。服务器上的每个路由都对应于控制器中的一个函数。这是我们正在编写的特定应用程序的路由表:

GET /(index) - home.index (render the homepage of the site) GET /images/image_id - image.index (render the page for a specific image)POST /images - image.create (when a user submits and uploads a new image)POST /images/image_id/like - image.like (when a user clicks the Like button)POST /images/image_id/comment - image.comment (when a user posts a comment)

你可以看到我们处理了两个不同的GET请求和三个不同的POST请求。此外,我们有两个主要的控制器:homeimage。控制器实际上只是具有不同函数定义的模块,这些函数与相应的路由相匹配。正如前面指出的,它们在 MVC 设计模式中被称为控制器。通常,每个路由都对应一个控制器。这个控制器很可能会渲染一个视图,而这个视图很可能会有自己的模型(在视图中显示的任何数据)。

让我们将我们的路由写成一个与所述表格匹配的模块。首先,在server文件夹中创建一个routes.js文件。routes文件将会非常简单,它所需的唯一依赖将是我们定义的控制器:

const express = require('express'), router = express.Router(), home = require('../controllers/home'), image = require('../controllers/image'); module.exports = (app)=>{ router.get('/', home.index); router.get('/images/:image_id', image.index); router.post('/images', image.create); router.post('/images/:image_id/like', image.like); router.post('/images/:image_id/comment', image.comment); app.use(router); }; 

我们立即声明一个router变量,并要求controllers文件夹来分配每个应用程序路由(我们还没有创建这些文件,但接下来就要创建了)。在这里,我们将每个路由分配给控制器中的相应函数。然后,我们导出一个模块,当单独调用时,将所有这些路由附加到app实例上。

路由的第一个参数是路由本身的字符串值,它可以包含变量值作为子路径。你可以看到第二个router.get,我们分配了一个路由值/images/:image_id,它基本上等同于浏览器地址栏中的/image/ANYVALUE。当我们编写image.index控制器时,你将看到如何检索:image_id的值并在controller函数内部使用它。

路由的第二个参数是一个回调函数。你可以完全忽略使用控制器的想法,只需将回调定义为内联匿名函数;然而,随着你的路由增长,这个文件会变得越来越大,代码会开始变得混乱。将代码分解成尽可能多的小而可管理的模块总是一个很好的做法,以保持自己的理智!

前两个router.get路由是典型的路由,当访问者将他们的浏览器指向yourdomain.com/routepath时会被调用——浏览器默认发送GET请求到服务器。另外三个router.post路由被定义为处理浏览器向服务器发出的请求,通常通过 HTML 表单提交完成。

有了所有我们定义的路由,现在让我们创建匹配的控制器。在controllers文件夹中,创建home.jsimage.js文件。home.js文件非常基本:

module.exports = { index(req, res){ res.send('The home:index controller'); } }; 

使用这个模块,我们实际上是在导出一个对象,该对象具有一个名为index的单个函数。indexfunction签名是使用 Express 的每个路由所需的签名。第一个参数是一个请求对象,第二个参数是一个响应对象。浏览器发送到服务器的请求的每个具体细节都可以通过请求对象获得。

此外,请求对象将使用之前声明的所有中间件进行修改。你将使用响应对象向客户端发送响应——这可能是一个渲染的 HTML 页面、静态资产、JSON 数据、错误,或者你确定的任何内容。目前,我们的控制器只是简单地响应一个简单的文本,这样你就可以看到它们都在工作。

让我们创建一个图像控制器,其中有更多的函数。编辑/controllers/image.js文件并插入以下代码:

module.exports = { index(req, res) { res.send(`The image:index controller ${req.params.image_id}`); }, create(req, res) { res.send('The image:create POST controller'); }, like (req, res) { res.send('The image:like POST controller'); }, comment(req, res) { res.send('The image:comment POST controller'); } }; 

在这里,我们定义了index函数,就像我们在主控制器中所做的那样,只是我们还将显示image_id,这是在执行此控制器函数时在路由中设置的。params属性是通过urlencoded功能添加到request对象中的,这是 body parser 模块的一部分!

请注意,控制器目前不需要任何依赖项(文件顶部没有定义require声明)。随着我们实际完善控制器函数并开始执行诸如将记录插入我们的 MongoDB 数据库和使用其他第三方npm模块等操作,这将发生改变。

现在你的控制器已经创建并准备好使用,你只需要激活你的路由。为了做到这一点,我们将在我们的configure.js文件中插入最后一行代码,就在return app;行的上方:

routes(app); 

不要忘记在文件顶部取消注释routes = require('./routes')这一行。我们在这里做的是使用我们定义的routes模块,并执行initialize函数,这将通过我们的app对象实际连接我们的路由。我们需要注释掉我们刚刚移动到routes中的冗余代码,它仍然存在于server.js中。

作为迄今为止你已经创建的每个文件的总结,这里列出了不间断的文件,这样你就可以查看完整的代码:

首先,我们需要用server.js启动

const express = require('express'); const config = require('./server/configure'); let app = express(); app.set('port', process.env.PORT || 3300); app.set('Views', `${ __dirname }/Views`); app = config(app); //commenting out following snippet that is not required // app.get('/', function(req, res){ // res.send('Hello World'); // }); const server = app.listen(app.get('port'), ()=>{ console.log(`Server up: http://localhost:${ app.get('port')}`); }); 

接下来,我们将使用server/configure.js配置服务器:

const path = require('path'), routes = require('./routes'), exphbs = require('express-handlebars'), express = require('express'), bodyParser = require('body-parser'), cookieParser = require('cookie-parser'), morgan = require('morgan'), methodOverride = require('method-override'), errorHandler = require('errorhandler'); module.exports = (app)=>{ app.use(morgan('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true})); app.use(methodOverride()); app.use(cookieParser('some-secret-value-here')); routes(app); app.use('/public/', express.static(path.join(__dirname, '../public'))); if ('development' === app.get('env')) { app.use(errorHandler()); } return app; }; 

然后,我们在server/routes.js中定义了我们的路由:

const express = require('express'), router = express.Router(), home = require('../controllers/home'), image = require('../controllers/image'); module.exports = (app)=>{ router.get('/', home.index); router.get('/images/:image_id', image.index); router.post('/images', image.create); router.post('/images/:image_id/like', image.like); router.post('/images/:image_id/comment', image.comment); app.use(router); }; 

最后,我们将使用controllers/home.js定义我们的控制器:

module.exports = { index(req, res) { res.send('The home:index controller'); } }; 

此外,我们还将使用controllers/image.js来定义我们的控制器:

module.exports = { index(req, res) { res.send(`The image:index controller ${req.params.image_id}`); }, create(req, res) { res.send('The image:create POST controller'); }, like (req, res) { res.send('The image:like POST controller'); }, comment(req, res) { res.send('The image:comment POST controller'); } }; 

让我们最后一次启动服务器并检查是否一切正常。

执行server.js节点,并且这次将浏览器指向http://localhost:3300。现在,你应该在浏览器中看到一些响应。转到http://localhost:3300/images/testing123。你应该在屏幕上看到以下消息:

 The image:index controller testing123 

默认情况下,Express 可以愉快地呈现静态 HTML 文档并将其返回给客户端。但是,除非你正在构建一个纯静态的、内容驱动的网站,这是可疑的,否则你很可能希望动态地呈现你的 HTML。也就是说,你希望在页面被请求时动态生成 HTML 的部分,也许使用循环、条件语句、数据驱动的内容等等。为了呈现动态 HTML 页面,你需要使用一个渲染引擎。

这就是 Handlebars 的用武之地。这个渲染引擎得名是因为它用于显示数据的语法,即双大括号{{}}。使用 Handlebars,你可以在你的 HTML 页面中有根据传递给它的数据在运行时确定的部分。考虑以下例子:

<div> <p>Hello there {{ name }}! Todays date is {{ timestamp }}</p> </div> 

访问者浏览器上实际的 HTML 将是:

<div> <p>Hello there Jason! Todays date is Sun Apr 13</p> </div> 

我们在configure模块中要处理的第一件事是将 Handlebars 注册为默认的视图渲染引擎。在configure.js文件中,在return(app);行的上方,你应该插入以下代码:

app.engine('handlebars', exphbs.create({ defaultLayout: 'main', layoutsDir: `${app.get('Views')}/layouts`, partialsDir: [`${app.get('Views') }/partials`] }).engine); app.set('View engine', 'handlebars'); 

首先,使用传入configure函数的 Express app对象,通过调用appengine函数来定义我们选择的渲染引擎。engine函数的第一个参数是渲染引擎应该寻找的文件扩展名,即handlebars

第二个参数通过调用express-hbs模块的create函数来构建引擎。这个create函数以一个options对象作为参数,这个options对象为我们的服务器定义了许多常量。最重要的是,我们将定义哪个布局是我们的默认布局,以及我们的布局将存储在哪里。如果您还记得,在server.js中,我们使用app.set来设置我们的appViews属性,指向当前工作目录+/Views。当我们配置渲染引擎的选项时,就会使用这个设置。您会注意到partialsDir属性使用了一个数组(只有一个项)和一个layoutsDir的单个字符串值。这两种方法是可以互换的,我只是想演示您可以有多个部分目录,它可以只是一个字符串值的数组。

有了这个设置,我们的服务器现在知道,每当我们尝试呈现具有handlebars文件扩展名的 HTML 页面时,它将使用 Handlebars 引擎执行呈现。这意味着我们需要确保在我们的动态 HTML 页面中使用 Handlebars 特定的语法。

在下一章中,我们将学习更多关于 Handlebars 以及如何编写动态 HTML 页面的知识。

使用.handlebars作为文件扩展名纯粹是个人选择。有些人更喜欢.hbs,如果你愿意,你可以使用任何你喜欢的东西。只需确保app.engine()函数中的第一个参数和app.set('View engine')函数中的第二个参数是相同的。

要了解 Express.js 提供的许多模板引擎,请查看此链接github.com/expressjs/express/wiki#template-engines

在本章中,我们学习了 Node 的 Express Web 框架,并使用 Express 编写了一个基本的 Web 服务器,这将成为我们在本书的其余部分中构建的图片上传网站的基础。

您编写的 Web 服务器处理特定路由的请求,使用控制器处理这些路由的逻辑,并支持典型 Web 服务器应具备的所有标准要求。

在下一章中,我们将介绍 Handlebars 模板引擎,以编写网站所需的每个动态 HTML 页面。此外,我们将更新图像和主页控制器,以包含必要的逻辑,以正确呈现这些 HTML 页面。

JavaScript 模板引擎是 node.js 被证明是一种同构技术的原因。它在 node.js 中添加了服务器端 UI 渲染功能,刺激了其客户端渲染性能。此外,它还简化了客户端代码的维护,因为它与服务器端代码紧密耦合。让我们在本章中更清楚地了解这一点,通过探索以下主题:

  • Handlebars 模板框架

  • 开发构建应用程序呈现层所需的模板的步骤

在我们开始探索 Handlebars 的功能之前,我们需要了解模板框架通常做什么。

正如我们已经知道的,MVC 应用程序框架将应用程序特定代码分为模型、视图和控制器。控制器应该处理将适当的数据绑定到其相关视图以生成传入 Web 应用程序请求的输出的任务。因此,视图应该独立于数据,只包含与数据呈现相关的代码,这将主要是 HTML。除了 HTML,视图还需要包含呈现逻辑,这将是在通过控制器传递给它们的数据上编写的条件。然后,模板框架在这种情况下的主要任务是使嵌入呈现逻辑的过程更简单和可读。此外,它们试图将视图分隔成更易理解的子组件。

模板解决方案通常可以分为客户端和服务器端模板解决方案。我们构建的 Web 应用程序通常遵循服务器端或客户端模板方法,或两者的混合。

想象一种情况,即 Web 应用程序在加载页面后,通过 AJAX 调用 API 并得到 JSON 响应。它将如何将接收到的数据呈现到相应的 HTML 中?在这种情况下,需要客户端模板来保持我们的 JavaScript 代码整洁,否则我们将不得不在 JavaScript 代码中放入太多难以阅读的 HTML 代码作为字符串。客户端模板框架允许我们将页面组件对应的模板放在标记内的特定标记中,并在必要时通过 JavaScript 代码呈现它们。采用客户端方法的常见缺点是它对页面的初始渲染时间产生的影响。

使用客户端模板的另一个重要优势是,它有助于将模板工作从服务器转移到客户端。这有助于大大减少服务器的计算负载,因为模板逻辑仅在浏览器中通过 JavaScript 执行。

服务器端模板是我们在将 HTML 响应发送回 Web 浏览器之前通过调用相应的视图来呈现标记的地方。这是我们将通过 Handlebars 在本章中探讨的内容。

有许多不同的渲染引擎可用于 Node 和 Express。其中最受欢迎的是 Jade、EJS 和 Handlebars。本书将探讨的特定引擎是 Handlebars.js。

Handlebars是一个非常简单和易于使用的模板框架。它的工作原理是在模板内插入数据。要了解 Handlebars 的概述,请考虑以下块图:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (8)

在这里,编译方法接受 HTML 表达式模板,并生成一个带有参数的函数。

让我们来看看编写 Handlebars 模板的基本语法。

假设以下 JavaScript 对象被传递给 Handlebars 模板:

 let context = { name: 'World' }; 

模板文件本身将包含以下标记:

 let source = `<div> Hello {{ name }}! </div>` 

前面的标记包含name作为 HTML 表达式,将由其上下文对象插值。

我们需要编写使其工作的 JavaScript 方法,如下所示:

 let template = Handlebars.compile(source); let output = template(context);

此输出变量将包含以下 HTML 代码:

 <div> Hello World! </div>

当然,你可以做的远不止这些!Handlebars 还支持条件语句:

 let model = { name: 'World', description: 'This will only appear because its set.' }; <div> Hello {{ name }}!<br/><br/> {{#if description}} <p>{{description}}</p> {{/if}} </div> 

使用if块助手,如前面的代码所示,您可以检查真条件,并且只在条件为真时显示 HTML 和/或数据。或者,您可以使用unless助手来执行相反的操作,只有在条件为假时显示 HTML:

 let model = { name: 'World' }; <div> Hello {{ name }}!<br/><br/> {{#unless description}} <p>NOTE: no description found.</p> {{/if}} </div> 

您可以像在其他编程语言中使用条件if/else一样使用ifelse,以及unless。就是这样!这些就是我们需要了解的所有基础知识,以便恢复我们的应用程序。

视图是我们称之为 HTML 页面的东西。它们被称为视图是因为 MVC 设计模式。正如我们已经讨论过的,模型是将显示在页面上的数据,视图是页面本身,控制器是在模型和视图之间通信的大脑。

我们的特定应用程序将需要两个视图。第一个视图是主页,第二个视图是图像页面。

以下部分的 HTML 严重依赖于 Bootstrap,这是 Twitter 创建的流行 HTML 框架,它提供了一套标准的用户界面元素。这些包括按钮、字体、布局网格、颜色方案等等。使用 Bootstrap 不仅可以让我们以一个漂亮干净的 UI 呈现我们的应用程序,还可以使其具有响应性,并且在任何查看它的设备上都能正确显示。您可以通过访问getbootstrap.com了解更多关于 Bootstrap 的信息。

让我们从创建主页视图开始。在views文件夹中创建一个新文件,命名为index.Handlebars,并插入以下 HTML 代码:

 <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title"> Upload an Image </h3> </div> 

我们将文件命名为index.Handlebars的原因纯粹是个人选择,但也基于网络上的常见命名约定。通常,作为任何网站的根文件的 HTML 页面被命名为index.whatever.php.aspx.html等)。再次强调,这只是一个常见的约定,不是您必须特别遵守的东西。

创建一个基本的 HTML 表单,并将method设置为postaction设置为/images。确保设置表单的enctype属性,因为我们将上传文件以及通过表单字段提交数据:

 <form method="post" action="/images" enctype="multipart/form- data"> <div class="panel-body form-horizontal"> <div class="form-group col-md-12"> <label class="col-sm-2 control-label" for="file">Browse:</label>

在这里,我们包含了一个标准的 HTML 输入字段,用于上传文件:

 <div class="col-md-10"> <input class="form-control" type="file" name="file" id="file"> </div> </div> <div class="form-group col-md-12"> <label class="col-md-2 control-label" for="title">Title:</label> <div class="col-md-10"> 

另一个标准的 HTML 输入字段用于文件的标题可以是用户想要的任何内容,如下面的代码所示:

 <input class="form-control" type="text" name="title"> </div> </div> <div class="form-group col-md-12"> <label class="col-md-2 control-label" for="description">Description:</label> <div class="col-md-10"> 

用于描述的标准 HTML textarea输入字段如下:

 <textarea class="form-control" name="description" rows="2"></textarea> </div> </div> <div class="form-group col-md-12"> <div class="col-md-12 text-right"> 

提供了一个标准的 HTML 按钮,将表单提交到服务器。使用 Bootstrap 类,我们提供了btnbtn-success,使其看起来像一个具有默认成功颜色(绿色)的 Bootstrap 风格按钮:

 <button type="submit" id="login-btn" class="btn btn-success" type="button"> <i class="fa fa-cloud-upload "> </i> Upload Image</button> </div> </div> </div> </form> </div>

在上传表单部分之后,我们将显示上传到网站的最新图像列表。请参考以下代码片段中的each块。它是 Handlebars 支持的关键字,用于循环遍历提供给模板的数据,以便重用 HTML 块。我们将在以下代码中详细讨论这一点:

<div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> Newest Images </h3> </div> <div class="panel-body"> {{#each images}} <div class="col-md-4 text-center" style="padding-bottom: 1em;"><a href="/images/{{ uniqueId }}"><img src="img/{{filename}}" alt="{{title}}" style="width: 175px; height: 175px;" class="img- thumbnail"></a></div> {{/each}} </div> </div> 

主页 HTML 代码中有两个重要部分。第一个是

我们定义的表单将是用户上传图像到网站的主要方式。由于我们将接受图像文件以及图像的详细信息(标题、描述等),因此我们需要确保表单设置为接受多部分数据。我们还将表单操作设置为指向我们在routesimage控制器模块中早些时候定义的/images路由。当用户完成表单并单击提交按钮时,表单将向http://localhost:3300/images发送POST请求,我们的路由器将捕获该请求并将其转发到我们的image控制器。从那里,image控制器将处理数据并将其保存到数据库,将图像文件保存到文件系统,并重定向到图像详细信息页面。我们将在下一章中编写这个逻辑。目前,如果您提交表单,实际上不会发生任何事情。

在主页上的主要图像上传表单下面,我们还有一个部分,使用each执行 Handlebars 循环,并遍历图像集合,将每个图像显示为缩略图,并提供指向图像页面的链接。images集合将从我们的home控制器中填充。在这里需要注意的是,在 Handlebars 模板的{{#each}}循环中,您的上下文会发生变化。也就是说,您用于访问each内部数据的路径现在基于集合中的每个项目。在这里,我们的对象将绑定到视图,将具有一个图像集合,图像集合中的每个项目将具有uniqueidfilenametitle属性。完成主页视图后,让我们设置图像页面的视图。

views文件夹中创建另一个文件,并将其命名为image.Handlebars。这个文件将具有更多的功能,所以我将它分成几部分,以便您可以审查每个部分。首先,插入以下代码块:

<div class="panel panel-primary"> <div class="panel-heading"> <h2 class="panel-title">{{ image.title }}</h2> </div> <div class="panel-body"> <p>{{ image.description }}</p> <div class="col-md-12 text-center"> <img src="img/{{image.filename}}" class="img-thumbnail"> </div> </div> <div class="panel-footer"> <div class="row"> <div class="col-md-8"> <button class="btn btn-success" id="btn-like" data-id="{{ image.uniqueId }}"><i class="fa fa- heart"> Like</i></button> <strong class="likes-count">{{ image.likes }} </strong> &nbsp; - &nbsp; <i class="fa fa-eye"> </i> <strong>{{ image.views }}</strong> &nbsp; - &nbsp; Posted: <em class="text-muted">{{ timeago image.timestamp }}</em> </div> </div> </div> </div> 

这段代码块定义了将在特定图像页面上显示的大部分内容。此页面的viewModel将由一个image对象组成,该对象具有在整个代码中使用的各种属性,例如titledescriptionfilenamelikesviews以及图像上传的时间戳。

您可能已经注意到代码中与{{ timeago image.timestamp }}时间戳相关的语法略有不同。那实际上是一个 Handlebars 助手。

这是一个我们即将编写的自定义函数,它将执行一些特殊的字符串格式化,具体来说,将时间戳字符串转换为一段时间前的时间(即 2 天前,12 小时前,15 分钟前等)。

我们希望允许用户对图像发表评论,因此让我们包括一个简单的表单:

<div class="panel panel-default"> <div class="panel-heading"> <div class="row"> <div class="col-md-8"> <strong class="panel-title">Comments</strong> </div> <div class="col-md-4 text-right"> <button class="btn btn-default btn-sm" id="btn- comment" data-id="{{ image.uniqueId }}"> <i class="fa fa-comments-o"> Post Comment...</i></button> </div> </div> </div> <div class="panel-body"> <blockquote id="post-comment"> <div class="row"> 

接下来是另一个标准的 HTML 表单,其中设置了方法和操作。该表单允许用户通过标准的 HTML 输入字段输入他们的姓名、电子邮件地址和评论。还提供了另一个提交按钮来保存评论:

 <form method="post" action="/images/{{ image.uniqueId }}/comment"> <div class="form-group col-sm-12"> <label class="col-sm-2 control-label" for="name">Name:</label> <div class="col-sm-10"> <input class="form-control" type="text" name="name"> </div> </div> <div class="form-group col-sm-12"> <label class="col-sm-2 control-label" for="email">Email:</label> <div class="col-sm-10"> <input class="form-control" type="text" name="email"> </div> </div> <div class="form-group col-sm-12"> <label class="col-sm-2 control-label" for="comment">Comment:</label> <div class="col-sm-10"> <textarea class="form-control" name="comment" rows="2"></textarea> </div> </div> <div class="form-group col-sm-12"> <div class="col-sm-12 text-right"> <button type="submit" id="comment-btn" class="btn btn-success" type="button"> <i class="fa fa-comment"></i> Post</button> </div> </div> </form> </div> </blockquote> 

评论的表单操作设置为/images/{{ image.uniqueid }}/comment。同样,如果您回忆起我们设置的路由,我们特意定义了一个路由来处理这个问题。

最后,我们希望显示已提交的任何评论。我们的viewModel包括评论集合以及图像详细信息,因此我们可以简单地使用 Handlebars 的#each块助手遍历该集合:

 <ul class="media-list"> {{#each comments}} <li class="media"> <a class="pull-left" href="#"> <img class="media-object img-circle" src="img/> {{gravatar}}?d=monsterid&s=45"> </a> <div class="media-body"> {{ comment }} <br/><strong class="media-heading">{{ name }}</strong> <small class="text-muted">{{ timeago timestamp }}</small> </div> </li> {{/each}} </ul> </div> </div> 

就像我们在主页上执行的循环一样,用于显示图像集合,这里我们只是遍历comments集合中的每条评论,并显示评论和字符串格式化的时间戳(再次使用我们的timeago全局助手)。我们还使用 Gravatar 来显示已评论用户的通用头像图像(假设他们已经提供了他们的电子邮件地址)。

Gravatar 是由wordpress.com/提供的一项服务,允许通过用户的电子邮件地址提供用户的个人资料图像。许多流行的网络服务依赖于 Gravatar,作为一种快速简便的方式来显示用户的个人资料图像,而无需支持此功能的额外功能。您可以在gravatar.com了解更多关于 Gravatar 的信息。

到目前为止,我们为我们的网站创建了两个特定视图:一个用于主页,一个用于图像的详细信息。然而,没有一致的 UI 将这两个页面联系在一起。我们没有一致的导航或标志。没有带有标准版权或其他信息的通用页脚。

通常,对于您创建的任何网站,您都希望有某种标准布局或主模板,每个页面都将使用。此布局通常包括网站标志和标题、主导航、侧边栏(如果有)、页脚。在每个网页上包含布局的 HTML 代码是不好的做法,因为如果您想对主布局进行最小的更改,结果将不得不编辑每个网页。幸运的是,Handlebars 有助于减少使用布局文件的工作量。

让我们通过在views/layouts文件夹中创建一个名为main.Handlebars的新文件来为我们的应用程序创建一个布局文件,并将以下 HTML 代码插入其中:

<!DOCTYPE HTML> <html lang="en"> <head> <title>imgPloadr.io</title> <link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/ bootstrap.min.css" rel="stylesheet"> <link href="//netdna.bootstrapcdn.com/font-awesome/ 4.0.3/css/font-awesome.min.css" rel="stylesheet"> <link rel="stylesheet" type="text/css" href="/public/css/styles.css"> </head> <body> <div class="page-header"> <div class="container"> <div class="col-md-6"> <h1><a href="/">imgPloadr.io</a></h1> </div> </div> </div> <div class="container"> <div class="row"> <div class="col-sm-8"> {{{ body }}} </div> <div class="col-sm-4"> {{> stats this }} {{> popular this }} {{> comments this }} </div> </div> </div> <div style="border-top: solid 1px #eee; padding-top: 1em;"> <div class="container"> <div class="row"> <div class="col-sm-12 text-center"> <p class="text-muted">imgPloadr.io | &copy; Copyright 2017, All Rights Reserved</p> <p class="text-center"> <i class="fa fa-twitter-square fa-2x text-primary"></i> <i class="fa fa-facebook-square fa-2x text-primary"></i> </p> </div> </div> </div> </div> <script src="img/jquery.min.js"></script> <script type="text/javascript" src="img/scripts.js"></script> </body> </html> 

前面的大部分代码只是 HTML,其中大部分使用 Bootstrap 来实际布局页面以及其他一些与 UI 相关的元素。最重要的部分是中间的突出部分,其中包含{{{ body }}}和下面的几行,因为它们涉及使用 Handlebars。

{{{ body }}}是 Handlebars 中专门用于布局的保留字。基本上,我们要说的是,我们渲染的任何页面都将其内容插入到定义{{{ body }}}的区域。如果您回忆一下我们之前创建的configure模块,当我们设置 Handlebars 作为我们的渲染引擎时,我们定义了我们的默认布局文件。在{{{ body }}}周围使用{{{}}}的略显奇怪的用法是因为 Handlebars 在使用{{}}时默认转义 HTML。由于我们的视图主要包含 HTML,我们希望保持其完整性,因此我们使用{{{}}}

另外三行使用{{ > ... }}渲染 Handlebars 部分,这些部分就像共享的 HTML 代码块,接下来将介绍。

到目前为止,我们创建了一个视图,它作为特定页面的大部分 HTML,以及一个布局,它作为网站每个页面上一致部分的包装。接下来,让我们来看看创建部分,这些部分实际上只是我们可以重用并注入到我们的布局或视图中的小视图。

部分是在网站中创建可重用组件并减少代码重复的绝佳方式。考虑一下我们应用程序中的评论。我们定义了一个 HTML 表单,用户可以使用它提交评论,但是如果我们想允许用户从网站的许多不同区域发布评论,该怎么办?这种情况是将我们的评论表单移到自己的部分中的绝佳候选,并且只需在任何我们想要显示评论表单的地方包含该部分。

对于这个应用程序,我们特别使用部分来处理主要布局中的侧边栏。

对于每个视图的viewModel,我们将包含一个名为sidebar的 JavaScript 对象

其中将包含侧边栏部分中找到的统计数据、热门图片和最近评论的特定数据。

让我们为每个部分创建 HTML。首先,在views/partials/路径中创建一个名为stats.Handlebars的文件,并包含以下 HTML 代码:

<div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> Stats </h3> </div> <div class="panel-body"> <div class="row"> <div class="col-md-2 text-left">Images:</div> <div class="col-md-10 text-right">{{ sidebar.stats.images }}</div> </div> <div class="row"> <div class="col-md-2 text-left">Comments:</div> <div class="col-md-10 text-right">{{ sidebar.stats.comments }}</div> </div> <div class="row"> <div class="col-md-2 text-left">Views:</div> <div class="col-md-10 text-right">{{ sidebar.stats.views }}</div> </div> <div class="row"> <div class="col-md-2 text-left">Likes:</div> <div class="col-md-10 text-right">{{ sidebar.stats.likes }}</div> </div> </div> </div> 

接下来,创建views/partials/popular.Handlebars,并将以下 HTML 代码插入其中:

<div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> Most Popular </h3> </div> <div class="panel-body"> {{#each sidebar.popular}} <div class="col-md-4 text-center" style="padding- bottom: .5em;"> <a href="/images/{{uniqueId}}"><img src="img/{{filename}}" style="width: 75px; height: 75px;" class="img-thumbnail"></a> </div> {{/each}} </div> </div> 

最后,创建views/partials/comments.Handlebars并将以下 HTML 代码插入其中:

<div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> Latest Comments </h3> </div> <div class="panel-body"> <ul class="media-list"> {{#each sidebar.comments}} <li class="media"> <a class="pull-left" href="/images/{{ image.uniqueId }}"> <img class="media-object" width="45" height="45" src="img/> {{ image.filename }}"> </a> <div class="media-body"> {{comment}}<br/> <strong class="media-heading"> {{name}}</strong> <small class="text-muted"> {{timeago timestamp }}</small> </div> </li> {{/each}} </ul> </div> </div> 

Handlebars 支持助手的概念,这些助手是您可以编写的特殊自定义函数,用于在模板中执行一些特殊逻辑。这将鼓励开发人员将视图中存在的常见呈现逻辑迁移到助手中并重用它们,从而大大提高视图的可读性。一个很好的助手示例是我们一直在使用的日期字符串格式化程序。助手可以全局注册并提供给每个模板文件,或者可以针对每个视图进行定义,并作为viewModel的一部分根据需要传递给模板。

首先,让我们创建一个全局助手,它将适用于我们渲染的每个 Handlebars 模板。我们将创建的全局助手将用于格式化时间戳,以便根据事件发生多久以前的时间来表达。我们将在整个应用程序中使用它,例如评论和图像时间戳。

我们需要做的第一件事是更新我们的server/configure.js模块,在那里我们最初将 Handlebars 配置为我们的渲染引擎。我们将添加以下部分来定义我们的助手:

app.engine('Handlebars', exphbs.create({ defaultLayout: 'main', layoutsDir: app.get('views') + '/layouts', partialsDir: [app.get('views') + '/partials'], helpers: { timeago: (timestamp)=> { return moment(timestamp).startOf('minute').fromNow(); } } }).engine); 

从我们添加的附加代码中可以看出(在前面的代码中突出显示),我们在create()的配置选项中定义了helpers属性。在helpers属性内,我们可以定义任意数量的函数。在这种情况下,我们定义了一个简单的timeago函数,实际上使用了另一个名为momentnpm模块。moment模块是一个用于执行多种不同类型的日期字符串格式化的优秀库。由于我们正在使用一个新模块,我们需要确保在我们的configure模块顶部执行require()

const path = require('path'), routes = require('./routes'), exphbs = require('express-Handlebars'), bodyParser = require('body-parser'), cookieParser = require('cookie-parser'), morgan = require('morgan'), methodOverride = require('method-override'), errorHandler = require('errorhandler'), moment = require('moment'); 

此外,我们需要通过npm实际安装它:

 $ npm install moment --save

全局定义助手很好,因为它们适用于渲染的每个视图,但有时您可能只需要为单个视图内部定义一个助手。在这种情况下,您可以在调用res.render()时将助手与模型本身一起包含,如以下代码所示:

var viewModel = { name: 'Jason', helpers: { timeago: (timestamp) =>{ return 'a long time ago!'; } } }; res.render('index', viewModel); 

我们不仅定义了一个可以从此视图的模型对象中专门使用的自定义助手,而且在这种特定情况下,我们还覆盖了现有的timeago全局助手,使用了略有不同但完全有效的版本。

让我们花一点时间快速回顾一下我们到目前为止所做的事情。到目前为止,我们已经完成了以下工作:

  • 我们创建了index.Handlebarsimage.Handlebars,这是应用程序的两个主要页面的视图

  • 我们创建了layouts/main.handelbars,这是应用程序中每个页面的主要布局文件

  • 我们创建了partials/comments.Handlebarspopular.Handlebarsstats.Handlebars

  • 我们创建了一个全局timeago Handlebars 助手

到目前为止,一切都很好;但是,这些视图实际上没有做任何事情,也没有接收任何viewModels,甚至在运行应用程序时也不会出现!让我们对我们的控制器进行一些快速的小修改,以使我们的视图正确渲染。

打开/controllers/home.js以编辑home控制器模块。

更新该文件的内容,使其看起来与以下代码块完全相同:

module.exports = { index: (req, res)=> { res.render('index'); } }; 

我们不再执行res.send,它只发送简单的响应,而是调用res.render并将要渲染的模板文件的名称作为唯一参数传递(目前)。使用我们在configure模块中定义的默认值,index文件将从我们的views文件夹加载。同样,还使用默认值,我们在configure模块中配置了将应用于此视图的默认布局main

让我们也更新image控制器以执行相同的操作。编辑/controllers/image.js并更改index函数,使其看起来与以下代码块完全相同:

index: (req, res)=> { res.render('image'); }, 

就是这样!让我们启动服务器,在浏览器中打开应用程序,看看

它看起来:

 $ npm start $ open http://localhost:3300 $ open http://localhost:3300/images/1

前面的命令使用npm start来启动应用程序。请注意,此命令仅在package.json文件中配置了应用程序入口文件时才有效。如果这不起作用,那么您必须在package.json中设置主属性,并将其设置为server.js文件。另外,作为替代方案,您可以通过使用node server.js手动调用server.js文件。

成功!希望您看到的东西与主页的以下截图非常相似:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (9)

此外,如果您向特定图像提供一个随机的 URL,例如,

http://localhost:3300/images/1,您应该看到以下截图:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (10)

在本章中,介绍了 Handlebars 模板渲染引擎,

随后我们回顾了创建动态 HTML 时使用的语法

我们为应用程序创建了主布局以及主页和图像页视图。我们在布局中包含了侧边栏的部分视图,并创建了一个全局 Handlebars 助手来显示自定义格式的日期。

尽管当前没有任何视图显示任何数据(因为我们还没有将模型传递给视图),但您可以看到事情开始顺利进行!在下一章中,我们将连接每个页面控制器中的实际逻辑,并构建模型对象,以便在屏幕上看到一些实际内容。

直到目前为止,我们为应用程序编写的控制器非常基础。它们最初只是发送文本响应给客户端的简单任务。在前一章中,我们更新了控制器,使它们呈现 HTML 视图,并将 HTML 代码发送给客户端,而不是简单的文本响应。控制器的主要工作是作为一个实体,其中包含使所有必要决定以正确呈现响应给客户端的逻辑。在我们的情况下,这意味着检索和/或生成页面完全显示所需的数据。

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

  • 修改控制器,以便它们生成数据模型并将其传递给视图

  • 包括支持上传和保存图像文件的逻辑

  • 更新控制器以实际呈现动态 HTML

  • 包括为生成网站统计数据的部分添加辅助程序

  • 通过迭代 UI 来改进可用性,使用 jQuery

控制器可以被定义为一个实体,负责操作模型并使用从相应模型接收到的数据启动视图渲染过程。在我们迄今为止开发的代码中,我们可以看到 express 路由器实例被用来将函数绑定到相应的路由。这些函数就是控制器。

在我们的路由器中创建的每个路由,都需要以下两个参数:

  • 第一个参数是路由本身的字符串,即/images/:image_id

  • 第二个参数是在访问该路由时将执行的控制器函数

对于任何与图像有关的路由,我们依赖于图像控制器。同样,任何与主页有关的路由都依赖于主页控制器,依此类推。

我们在应用程序中定义控制器的步骤纯粹是组织性的,并基于个人偏好。我们将控制器创建为模块,以便我们的路由器不会成为一个冗长混乱的代码堆。我们本可以将所有逻辑直接包含在路由中的函数中,但这样会导致组织混乱,并且后期维护起来会非常难读。

由于我们的示例应用程序相当小,目前只有两个控制器:主页和图像。这些控制器的责任是为我们的 HTML 页面构建适当的视图模型,并实际呈现页面。任何需要执行每个页面并构建视图模型的逻辑都将通过我们的控制器完成。

鉴于我们的应用程序中只有一个 HTML 视图,我们需要将数据附加到该页面,以便渲染的模板可以以这样的方式包含,即页面的动态区域被真实内容替换。为此,我们需要生成一个视图模型。在渲染过程中,模板引擎将解析模板本身,并寻找特殊的语法,指示特定部分应在运行时用视图模型中的值替换。我们在前一章中探索 Handlebars 模板框架时看到了这样的例子。可以将其视为 HTML 模板的一个花哨的运行时查找和替换--查找变量并用视图模型中存储的值替换它们。

这个过程发生在服务器端,结果只作为应用程序接收到的 HTTP 请求的响应发送。

视图模型通常只是一个可以传递给模板的 JavaScript 对象。模板包含了我们渲染页面所需的所有必要逻辑。模板引擎的任务是通过处理与相关模型关联的模板来生成相应的 HTML。页面的视图模型通常包含渲染该页面的特定内容部分所需的所有数据。以我们的应用程序为例,特定图片页面的视图模型可能包含图片的标题、描述、显示图片所需的信息,以及各种统计数据,如点赞数、浏览量和评论集合。视图模型可以简单也可以复杂。

这里使用术语视图模型来指代模型的数据形式,它将与模板绑定,通过任何模板框架呈现 HTML。

如果您看一下我们当前的主页控制器(controllers/home.js),您会发现index函数几乎没有任何代码:

res.render('index');

我们想要做的第一件事是使用示例数据构建一个基本的视图模型,以便我们可以看到我们的视图模型在工作。用以下更新后的代码替换那个单独的res.render调用:

const ViewModel = { images: [ images: [{ uniqueId: 1, title: 'Sample Image 1', description: '', filename: 'sample1.jpg', Views: 0, likes: 0, timestamp: Date.now() }, { uniqueId: 2, title: 'Sample Image 2', description: '', filename: 'sample2.jpg', Views: 0, likes: 0, timestamp: Date.now() }, { uniqueId: 3, title: 'Sample Image 3', description: '', filename: 'sample3.jpg', Views: 0, likes: 0, timestamp: Date.now() }, { uniqueId: 4, title: 'Sample Image 4', description: '', filename: 'sample4.jpg', Views: 0, likes: 0, timestamp: Date.now() }] };res.render('index', ViewModel);

在上面的代码中,我们构建了一个基本的 JavaScript 对象集合。我们声明的常量称为ViewModel,但实际上这个常量的名称并不重要,可以是任何你想要的。const ViewModel是一个包含一个名为images的属性的对象,images本身是一个数组。

images数组包含四张示例图片,每张图片都有一些基本属性--最明显的属性是在决定我们想要每张图片的哪种信息时决定的。集合中的每张图片都有一个uniqueIdtitledescriptionfilenameViewslikes counttimestamp属性。

设置好我们的ViewModel后,我们只需将其作为res.render调用的第二个参数传递。在渲染View时这样做可以使其中的数据对View本身可用。现在,如果您回忆一下我们为主页index.Handlebars视图编写的一些模板代码,我们有一个{{#each images}}循环,遍历了传递给模板的视图模型的图片集合中的每张图片。再次查看我们创建的视图模型,我们看到它只有一个名为images的属性。Handlebars 循环内的 HTML 代码将明确引用images数组中每张图片的uniqueIdfilenametitle属性。

保存更改到主控制器,再次启动您的应用程序,并导航到localhost:3300。您应该在最新图片部分看到现在出现在主页上的四张图片(尽管,正如您在下面的截图中所看到的,这些图片仍然是损坏的,因为我们实际上并没有创建任何图片文件):

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (11)

主页有一个相当简单的控制器和视图模型,您可能已经注意到侧边栏仍然完全空白。我们将在本章稍后讨论侧边栏。

让我们为图片页面创建控制器和视图模型。图片的控制器会更复杂一些,因为我们将编写处理通过主页表单上传和保存图片文件的逻辑。

图片控制器中的index函数看起来几乎与主页控制器中的index函数相同。唯一的区别是,我们不是生成一个images数组,而是为单个图片构建一个ViewModel。然而,这个图片的ViewModel将比主页上的更详细,因为我们正在构建一个更详细的图片视图页面(而不是主页上的缩略图集合)。最值得注意的是图片的评论数组。

再次查看我们的controllers/image.js文件中原始的index函数,我们可以看到简单的现有res.render代码行:

res.render('image');

我们希望用以下代码替换这行,使用一个ViewModel和一个更新后的res.render语句:

const ViewModel = { image: { uniqueId: 1, title: 'Sample Image 1', description: 'This is a sample.', filename: 'sample1.jpg', Views: 0, likes: 0, timestamp: Date.now() }, comments: [{ image_id: 1, email: 'test@testing.com', name: 'Test Tester', gravatar: 'http://lorempixel.com/75/75/animals/1', comment: 'This is a test comment...', timestamp: Date.now() }, { image_id: 1, email: 'test@testing.com', name: 'Test Tester', gravatar: 'http://lorempixel.com/75/75/animals/2', comment: 'Another followup comment!', timestamp: Date.now() }]};res.render('image', ViewModel);

在这里,我们再次声明一个新的ViewModel常量--这次有一个包含单个图片属性的image属性。除了image属性之外,还有一个comments属性,它是一个comment对象的数组。你可以看到每个评论都有特定于每张图片的评论的各种属性。这个 JavaScript 对象实际上是我们的真实数据一旦包含了连接我们的应用程序到 MongoDB 的逻辑后最终会看起来的一个相当不错的预览。

在构建了我们的示例image对象及其评论集合之后,我们将其传递给我们的res.render调用,从而直接将这个新的ViewModel发送到我们图片的 Handlebars 模板。同样,如果你查看image.Handlebars文件中的 HTML 代码,你可以看到ViewModel的每个属性在哪里显示。

再次运行应用程序,确保我们的图片页面显示正常:

$ node server.js

一旦应用程序运行并在浏览器中启动,点击主页上最新图片部分列出的任何一张图片。

这应该带你到一个单独的图片页面,你会看到类似以下截图所示的页面:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (12)

请注意,标题、描述、喜欢、浏览次数和时间戳(转换为用户可读的不同格式)现在都出现在页面上。此外,你还可以看到图片附近列出了一些评论!

我们接下来需要在我们的图片控制器中实现的下一个功能是处理用户在主页上提交图片上传的逻辑。即使表单在我们应用的主页上,我们决定将处理上传的逻辑放在图片控制器中,因为从逻辑上讲,这是最合理的(因为这个功能主要与图片有关,而不是特定于主页)。这纯粹是个人决定,你可以将逻辑放在任何你喜欢的地方。

你应该注意到主页上表单的 HTML 的动作设置为/images,方法是post。这与我们之前设置的路由完全匹配,我们监听/images路由的post,并调用图片控制器的create函数。

我们的图片控制器中的create函数将有一些关键的责任:

  • 它应该为图片生成一个唯一的文件名,这也将作为标识符

  • 它应该将上传的文件保存到文件系统,并确保它是一个图片文件

  • 最后,一旦任务完成,它应该将控制重定向到image/image_id路由,以显示实际的图片

由于我们将在这个函数中使用文件系统,我们需要包含一些来自 Node.js 核心模块的模块,特别是文件系统(fs)和路径(path)模块。

在我们开始为上传图片部分添加必要的代码之前,我们需要对应用程序的配置进行一些小修复。此外,我们还需要在配置文件中添加一个额外的模块来支持文件上传,即multer。使用以下命令将其作为应用程序的依赖项添加到我们的应用程序中:

npm install multer --save

现在,转到配置文件server/configure.js并通过require引入它:

multer = require('multer');

您可以将此放在文件中最初需要的模块下。然后,在 Handlebars 引擎方法下插入以下片段:

app.use(multer({ dest: path.join(__dirname, 'public/upload/temp')}));

现在,我们的上传操作将正常工作,如预期的那样。

让我们首先编辑controllers/image.js文件,并在文件顶部插入两个新的 require 语句:

const fs = require('fs'),path = require('path');

接下来,获取create函数的原始代码:

res.send('The image:create POST controller');res.redirect('/images/1');

用以下代码替换原始代码:

const saveImage = function() {// to do...};saveImage();

在这里,我们创建了一个名为saveImage的函数,并在声明后立即执行它。这可能看起来有点奇怪,但原因将在下一章中实现数据库调用时变得清晰。主要原因是我们将重复调用saveImage,以确保我们生成的唯一标识符实际上是唯一的,并且在数据库中不存在作为先前保存的图像标识符。

让我们先来审查将插入saveImage函数中的代码的细分(替换// to do...注释)。我将为这个函数的每一行代码进行解释,然后在最后给出整个代码块:

let possible = 'abcdefghijklmnopqrstuvwxyz0123456789',imgUrl = '';

我们需要生成一个随机的六位字母数字字符串,以表示图像的唯一标识符。这个标识符将类似于其他提供唯一链接的网站(例如bit.ly)的短链接。为此,我们首先提供一个可能字符的字符串,用于生成随机字符串:

for(let i=0; i < 6; i+=1) { imgUrl += possible.charAt(Math.floor(Math.random() *possible.length));}

然后,循环六次,并从可能字符的字符串中随机选择一个字符,在每个循环中将其附加。在这个for循环结束时,我们应该有一个由六个随机字母和/或数字组成的字符串,例如a8bd73

const tempPath = req.files.file.path,ext = path.extname(req.files.file.name).toLowerCase(),targetPath = path.resolve(`./public/upload/${imgUrl}${ ext}`);

在这里,我们声明了三个常量:我们上传的文件将被临时存储的位置,上传的文件的文件扩展名(例如.png.jpg等),以及上传的图像应最终驻留的目的地。

对于后两个变量,我们将使用 path 节点模块,该模块在处理文件名和路径以及从文件中获取信息(例如文件扩展名)时非常有效。接下来,我们将把图像从临时上传路径移动到最终目的地:

if (ext === '.png' || ext === '.jpg' || ext === '.jpeg' || ext === '.gif') { fs.rename(tempPath, targetPath, (err) => { if (err) throw err; res.redirect(`/images/ ${imgUrl}`); });} else { fs.unlink(tempPath, () => { if (err) throw err; res.json(500, { error: 'Only image files are allowed.' }); });}

前面的代码执行了一些验证。具体来说,它进行了检查,以确保上传的文件扩展名与允许的扩展名列表匹配,即已知的图像文件类型。如果上传了有效的图像文件,它将通过filesystemrename函数从temp文件夹中移动。请注意,filesystem(fs)rename函数接受三个参数:原始文件,新文件和callback函数。

callback函数在rename完成后执行。如果节点不是这样工作的(总是依赖于callback函数),那么很可能您的代码将在rename函数执行后立即执行,并尝试针对尚不存在的文件进行操作(即rename函数甚至没有完成其工作)。使用callback函数,我们有效地告诉节点,一旦文件的rename完成并且文件准备好并放置在应该放置的位置,那么它可以执行callback函数中的代码。

随后的else条件处理了上传的文件无效的情况(即不是图像),因此我们调用 fs 模块的unlink函数,该函数将删除原始文件(从上传到的temp目录)然后发送一个简单的JSON 500带有错误消息。

这是完整的saveImage函数(再次,以下代码将替换之前的// to do...):

const possible = 'abcdefghijklmnopqrstuvwxyz0123456789', imgUrl = '';for (let i = 0; i < 6; i += 1) { imgUrl += possible.charAt(Math.floor(Math.random() * possible.length));}const tempPath = req.files.file.path, ext = path.extname(req.files.file.name).toLowerCase(), targetPath = path.resolve(`./public/upload/${ imgUrl }${ ext }`);if (ext === '.png' || ext === '.jpg' || ext === '.jpeg' || ext === '.gif') { fs.rename(tempPath, targetPath, (err) => { if (err) throw err; res.redirect('/images/${ext}'); });} else { fs.unlink(tempPath, () => { if (err) throw err; res.json(500, { error: 'Only image files are allowed.' }); });}

有了这个新代码,我们现在可以通过主页上的表单成功上传图像文件。启动应用程序并在浏览器中打开它,然后尝试一下。在那里,点击主表单中的浏览按钮,然后从计算机中选择一个image文件。如果成功,该image文件应该存在于项目的public/upload文件夹中,并带有一个新的随机文件名。

确保您在项目中创建了public/upload文件夹,否则在尝试将文件写入不存在的位置时会出现运行时错误。根据您的操作系统和安全访问权限,可能需要在文件夹上设置写入权限。

上传表单完成并且create控制器函数完成其工作后,它将重定向您到已上传图像的个别图像页面。

到目前为止,我们渲染的每个页面都完美地显示它们的ViewModel数据,但是那个讨厌的侧边栏仍然是空白的。我们将通过将它们实现为辅助模块来解决这个问题,为侧边栏内容创建一些模块。这些辅助模块将被我们应用程序的各个部分重复使用,并不一定属于controller文件夹或server文件夹。因此,我们将创建一个名为helpers的新主目录,并将这些模块存储在那里。

由于我们只是将临时装置数据加载到我们的ViewModels中,一旦我们实现 MongoDB,我们在helperscontrollers中设置的数据都将被实际的实时数据替换;我们将在下一章中实现这一点。

首先,我们将为整个侧边栏创建一个模块。这个模块将负责调用多个其他模块来填充每个侧边栏部分的ViewModel。由于我们将为每个页面的自己的ViewModel填充特定于侧边栏的数据,因此侧边栏模块的函数将接受原始的ViewModel作为参数。这样我们就可以为每个页面的现有ViewModel附加数据。

在这里,我们将附加一个侧边栏属性(这是一个 JavaScript 对象),其中包含侧边栏各个部分的属性。

首先创建一个名为helpers/sidebar.js的文件,并插入以下代码:

const Stats = require('./stats'), Images = require('./images'), Comments = require('./comments');module.exports = (ViewModel, callback) => { ViewModel.sidebar = { stats: Stats(), popular: Images.popular(), comments: Comments.newest() }; callback(ViewModel);};

在前面的代码中,您可以看到您首先需要为侧边栏的每个部分创建一个模块。显示侧边栏的任何给定页面的现有ViewModel是函数的第一个参数。我们向ViewModel添加了一个侧边栏属性,并通过调用每个侧边栏部分的模块为每个属性设置了值。最后,我们执行了作为侧边栏模块的第二个参数传递的callback。这个callback是一个匿名函数,我们将用它来执行 HTML 页面的渲染。

让我们更新主页和图像控制器,包括调用侧边栏模块,并将每个页面的 HTML 模板的渲染推迟到侧边栏模块的callback中。

编辑controllers/home.js,考虑以下代码行:

res.render('index', ViewModel);

用这个新的代码块替换它:

sidebar(ViewModel, (ViewModel) => { res.render('index', ViewModel);});

controllers/image.js文件进行完全相同的更改,将index替换为image

sidebar(ViewModel, (ViewModel) => { res.render('image', ViewModel);});

再次注意,我们执行了侧边栏模块,并将现有的ViewModel作为第一个参数传递,并将一个基本的匿名函数作为第二个参数的callback。我们等待渲染 HTML 视图,直到sidebar完成填充ViewModel。这是因为 Node.js 的异步特性。假设我们以以下方式编写代码:

sidebar(ViewModel);res.render('index', ViewModel);

在这里,很可能res.render语句会在sidebar甚至完成任何工作之前执行。一旦我们在下一章中引入 MongoDB,这将变得非常重要。另外,由于我们现在在每个控制器中使用sidebar模块,请确保在两个控制器的顶部require它,包括以下代码:

const sidebar = require('../helpers/sidebar');

现在我们的“侧边栏”模块已经完成,并且从两个控制器中调用,让我们通过创建所需的每个子模块来完成“侧边栏”。

统计模块将显示有关我们应用程序的一些随机统计信息。具体来说,它将显示整个网站的“图片”、“评论”、“浏览量”和“喜欢”总数。

创建helpers/stats.js文件,并插入以下代码:

module.exports = () => { const stats = { images: 0, comments: 0, Views: 0, likes: 0 }; return stats;};

这个模块非常基础,它所做的就是创建一个标准的 JavaScript 对象,其中包含一些属性用于各种统计信息,每个属性最初都设置为 0。

“图片”模块负责返回各种图片集合。最初,我们将创建一个“热门”函数,用于返回网站上最受欢迎的图片集合。最初,这个集合将只是一个包含示例固定数据的“图片”对象数组。

创建helpers/images.js文件,并插入以下代码:

module.exports = { popular() { let images = [{ uniqueId: 1, title: 'Sample Image 1', description: '', filename: 'sample1.jpg', Views: 0, likes: 0, timestamp: Date.now() }, { uniqueId: 2, title: 'Sample Image 2', description: '', filename: 'sample2.jpg', Views: 0, likes: 0, timestamp: Date.now() }, { uniqueId: 3, title: 'Sample Image 3', description: '', filename: 'sample3.jpg', Views: 0, likes: 0, timestamp: Date.now() }, { uniqueId: 4, title: 'Sample Image 4', description: '', filename: 'sample4.jpg', Views: 0, likes: 0, timestamp: Date.now() } ]; return images; }};

与图片的“辅助”模块类似,“评论”模块将返回最新发布到网站的评论集合。一个特别感兴趣的想法是每条评论也附有一张图片,这样在显示评论列表时可以显示每条评论的实际图片作为缩略图(否则,当我们看到一系列没有相关图片的评论时,我们会失去上下文)。

创建helpers/comments.js文件,并插入以下代码:

module.exports = { newest() { let comments = [{ image_id: 1, email: 'test@testing.com', name: 'Test Tester', gravatar: 'http://lorempixel.com/75/75/animals/1', comment: 'This is a test comment...', timestamp: Date.now(), image: { uniqueId: 1, title: 'Sample Image 1', description: '', filename: 'sample1.jpg', Views: 0, likes: 0, timestamp: Date.now } }, { image_id: 1, email: 'test@testing.com', name: Test Tester ', gravatar: 'http://lorempixel.com/75/75/animals/2', comment: 'Another followup comment!', timestamp: Date.now(), image: { uniqueId: 1, title: 'Sample Image 1', description: '', filename: 'sample1.jpg', Views: 0, likes: 0, timestamp: Date.now } }]; return comments; }};

同样,这只是一个基本的 JavaScript 对象数组,每个评论都有一些属性,其中之一是实际图片及其属性(“图片”属性应该看起来很熟悉,因为它与图片的“辅助”模块中的项目相同)。

现在我们的“侧边栏”模块已经完成,以及它的依赖子模块“统计”、“图片”和“评论”,是时候再次测试我们的应用程序了。启动节点服务器,并在浏览器中打开应用程序。

您现在应该在主页和图片着陆页上看到带有内容的“侧边栏”。

现在我们的应用程序运行得相当不错,实际上可以与之交互,是时候退后一步,看看我们可能改进的一些方面了。

一个领域是图片页面上的发表评论表单。我认为这个表单总是可见并不是必要的,而是应该只在有人想要发表评论时才可用。

此外,我希望“喜欢”按钮不必向服务器提交完整的表单并导致整个页面重新加载(就像主页上上传图片时的表单一样)。我们将使用 jQuery 向服务器提交 AJAX 调用来处理喜欢,并实时发送和检索数据,而无需重新加载页面。

为了进行这些调整,我们需要在页面上引入少量 JavaScript 以增加一些交互性。为了使事情变得更容易,我们将使用流行的 jQuery JavaScript 库来轻松创建交互功能。

jQuery 已经存在多年,并在前端开发中爆炸性流行。它允许您非常轻松地操纵文档对象模型DOM),即任何页面的 HTML 结构,您将在下一节中看到。您可以在jquery.com了解更多关于 jQuery 的信息。

您可能没有注意到,在为main.Handlebars布局文件提供的 HTML 代码中,已经包含了作为外部script标签的 jQuery(引用托管在 CDN 上的 jQuery)。此外,还包括了一个本地的scripts.js标签,我们将在其中放置我们自定义的 jQuery JavaScript 代码,用于对 UI 进行的更改。当您查看main.Handlebars的最底部时,您可以看到以下代码:

<script src="img/jquery.min.js"></script><script type="text/javascript" src="img/scripts.js"></script>

第一个脚本标签指向谷歌的内容交付网络CDN),这意味着我们不必担心将该文件与我们的代码一起托管。然而,第二个文件是我们自己的文件,因此我们需要确保它存在。

CDN 是一种从全球分布的缓存服务器网络中传递文件的方法。一般来说,这意味着通过区域性更近的下载源以及改进的缓存,网页访问者经常下载的文件(如 jQuery)可以更快地加载。如果多个网站使用相同的 CDN URL 来托管 jQuery,例如,那么有理由认为访问您网站的访问者在访问以前的不相关网站时可能已经下载了 jQuery。因此,您的网站将加载得更快。

创建public/js/scripts.js文件,并插入以下代码:

$(function(){// to do...});

这是一个标准的代码块,几乎每次有人使用 jQuery 时都会看到。这段代码执行了一个匿名函数,该函数位于$() jQuery 包装器内,这是写下面代码的简写:

$(document).ready(function(){// to do...});

前面的代码基本上意味着callback函数将在页面完全加载和准备好之前等待执行。这很重要,因为我们不希望将 UI 事件处理程序和/或效果应用于实际上尚不存在的 DOM 元素,因为页面仍在加载。这也是为什么main.Handlebars布局文件中的脚本标签是页面的最后一行的另一个原因;这样它们就是最后加载的,确保文档已经完全下载并准备好被操作。

首先,让我们解决post-comment功能。我们希望默认隐藏评论表单,然后仅在用户点击图像下的“发布评论”按钮时显示它(在“喜欢”按钮右侧)。在callback函数中插入以下代码,该函数存在// to do...注释处:

$('#post-comment').hide();$('#btn-comment').on('click', function(event) { event.preventDefault(); $('#post-comment').show();});

第一行代码在具有post-comment ID 的 HTML div 上执行hide函数。然后,我们立即在具有btn-comment ID 的 HTML 按钮上应用事件处理程序。我们应用的事件处理程序是onClick,因为我们希望它在用户点击该按钮时执行我们提供的匿名函数。该函数简单地阻止默认行为(特定元素的默认行为;在本例中是按钮)然后调用显示 jQuery 函数,该函数显示先前隐藏的post-comment div。event.preventDefault()部分很重要,因为如果我们不包括它,点击按钮的操作将执行浏览器期望执行的操作,并尝试同时执行我们的自定义 JavaScript 函数。如果我们不包括这个,很可能我们的 UI 会表现得不太理想。一个很好的例子是,如果您想要覆盖标准 HTML 链接的默认行为,您可以分配一个onClick事件处理程序并做任何您想做的事情。但是,如果您不执行event.preventDefault(),浏览器将发送用户到该链接的 HREF,而不管您的代码试图做什么。

现在,让我们添加一些代码来处理“喜欢”按钮的功能。我们将为按钮添加一个事件处理程序,方式与我们为“发布评论”按钮做的方式相同,使用 jQuery 的.on函数。在您之前添加的代码之后,将以下附加代码块插入ready语句内:

$('#btn-like').on('click', function(event) { event.preventDefault(); let imgId = $(this).data('id'); $.post('/images/' + imgId + '/like').done(function(data) { $('.likes-count').text(data.likes); });});

前面的代码将一个onClick事件处理程序附加到btn-like按钮。事件处理程序首先从“喜欢”按钮中检索data('id')属性(通过image.Handlebars HTML 模板代码和ViewModel分配),然后执行 jQuery AJAX post 到/images/:image_id/like路由。回想一下我们 Node server/routes.js文件中的以下行:

app.post('/images/:image_id/like', image.like);

一旦完成了该 AJAX 调用,将执行另一个匿名的“回调”函数,该函数将更改具有likes-count类的 HTML 元素的文本,并用从 AJAX 调用返回的数据替换它--在这种情况下,喜欢的总数已更新(通常情况下,它将是之前的总数加一)。

为了测试这个功能,我们需要在image控制器的like函数中实现一些固定数据。编辑controllers/image.js,在like函数内,用以下代码替换现有的res.send函数调用:

like(req, res) { res.json({ likes: 1 });},

所有这段代码做的就是向客户端返回 JSON,其中包含一个带有值为1的 likes 属性的简单对象。在下一章中,当我们向应用程序引入 MongoDB 时,我们将更新此代码,以实际增加喜欢的数量并返回所喜欢的图像的真实值。

经过所有这些变化,您应该能够重新启动节点服务器并在浏览器中打开网站。单击主页上的任何图像以查看图像页面,然后单击“喜欢”按钮,以将其从0更改为1。不要忘记查看新的“发布评论”按钮;单击此按钮应该会显示评论表单。

在本章的开始,我们有一些基本的 HTML 页面通过我们的应用程序显示在浏览器中,但它们没有任何内容和逻辑。我们实现了每个控制器的逻辑,并讨论了 ViewModel 以及如何填充页面内容。

除了通过 ViewModel 在我们的页面上显示内容之外,我们还实现了处理上传和保存图像文件到本地文件系统的代码。

我们稍微调整了 UI,使用 jQuery 包含了一些微妙的增强功能,通过显示评论表单,并使用 AJAX 来跟踪喜欢,而不是依赖完整的页面回发。

现在,我们已经为我们的 ViewModels 和 controllers 奠定了基础,让我们使用 MongoDB 将它们全部联系在一起,并开始使用真实数据。在下一章中,我们将再次更新 controllers,这次实现从 MongoDB 服务器读取和保存数据的逻辑。

几乎现在为 Web 编写的任何应用程序,如果其用户之间的交互不是永久保存的话,那么高度交互式的应用程序的价值就会受到限制。您必须将您的应用程序与适当的数据库集成以解决这个问题。想象一种情况,您的应用程序的所有数据(注册用户、订单交易和社交互动)都存储在服务器的临时内存中。一旦服务器关闭或重新启动,您的应用程序数据将全部丢失。依赖数据库永久存储这些数据对于任何动态应用程序都至关重要。

在本章中,将涵盖以下主题:

  • 连接到 MongoDB

  • Mongoose 简介

  • 模式和模型

  • 在我们的控制器中添加 CRUD

在上一章中,我们编写并考虑了我们应用程序的实际逻辑。构建我们应用程序的下一步是将其连接到数据库,以便我们用户的交互和数据可以被永久保存和检索。从技术上讲,我们可以通过将数据存储在内存中来解决这个问题,但是一旦我们的 Web 服务器重新启动或崩溃,所有这些数据都将丢失。如果没有将我们的应用程序连接到数据库服务器以持久保存数据,访问者交互的每个输入都将过时。如果没有某种数据库服务器来存储我们的数据,我们每天与之交互的大多数网站甚至都不会存在。

以下是我们的数据将如何在我们的应用程序中为每个访问者交互持久化的一般分解:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (13)

考虑前面的图表,它反映了 Web 应用程序请求的典型生命周期:

  1. 一个访问者通过他们的网络浏览器提交请求来查看我们应用程序上的页面。

  2. Node.js 服务器接收到这个请求并查询 MongoDB 服务器是否有任何数据。

  3. MongoDB 服务器将查询到的数据返回给我们的 Node.js 服务器。

  4. Node.js 服务器获取数据,将其构建成视图模型,然后将渲染后的 HTML 页面发送回浏览器。

  5. 网络浏览器接收到来自我们 Node.js 服务器的响应并渲染 HTML。

  6. 这个循环通常会为每个访问者的每次交互重复。

为了本书的目的,我们使用 MongoDB 作为我们的主要数据存储,但现实情况是我们可以使用以下任何一种来存储数据:MySQL,PostgreSQL,MS SQL,文件系统等等。

在我们正式将 MongoDB 实现到我们的实际应用程序之前,让我们先看一些从 Node.js 内部连接到 MongoDB 服务器的基本示例。

创建一个新的项目文件夹来存储一些样本代码进行实验。我会把我的文件夹称为mongotest。在这个文件夹里,创建一个名为test.js的新文件。在这个文件中,我们将玩一些代码来测试如何连接到 MongoDB,以及如何插入和检索一些数据。从 Node.js 连接到 MongoDB 服务器的第一件事是要求一个mongodb模块。

为了开始,切换到新的mongotest文件夹并使用npm安装mongodb模块:

 $ cd mongotest $ npm install mongodb

不要被模块的名称所困惑。mongodb npm 模块并不是 MongoDB 本身,而是一个第三方 npm 模块,可以方便地从 Node.js 内部与 MongoDB 服务器通信。此外,因为这只是一个用于实验的示例项目,所以我们不需要在npm install中使用--save标志,因为我们不维护package.json文件。

现在mongodb模块已经安装,我们可以在我们的实验文件中使用它。启动你的编辑器,创建一个名为test.js的文件。将以下代码块插入其中:

const MongoClient = require('mongodb').MongoClient;MongoClient.connect('mongodb://localhost:27017/mongotest', (err, db)=>{ console.log('Connected to MongoDB!'); db.close(); });

执行上述代码应该在你的屏幕上记录“Connected to MongoDB!”。

您首先注意到的是我们需要mongodb模块,但我们特别使用模块的MongoClient组件。该组件是我们实际用于主动打开与 MongoDB 服务器的连接的接口。使用MongoClient,我们将mongodb://localhost:27017/mongotest字符串 URL 作为第一个参数传递给我们的本地服务器。请注意,URL 中的路径指向服务器,然后是数据库本身的名称。

请确保在本章节的持续时间内在另一个终端中运行本地的 MongoDB 服务器实例。为此,打开一个命令行终端窗口并执行$ mongod。您的服务器应该启动并在屏幕上记录信息,最后显示[initandlisten] waiting for connections on port 27017

您可能会发现当运行应用程序时,您会收到类似以下代码的堆栈跟踪错误:

events.js:72

thrower; // Unhandled 'error' event

错误:无法连接到[localhost:27017]。如果发生这种情况,您应该意识到它无法连接到端口 27017 上的localhost,这是我们本地mongod服务器运行的端口。

一旦我们与数据库服务器建立了活动连接,就好像我们在运行 Mongo shell 命令一样。MongoClient 回调函数返回一个数据库连接对象(我们在代码中命名为db,但可以命名为任何东西),这与我们在 Mongo shell 中使用的对象完全相同,当我们执行use <databasename>时。知道这一点,此时我们可以使用db对象做任何我们可以通过 Mongo shell 做的事情。语法略有不同,但思想基本相同。

让我们通过向集合插入记录来测试我们的新db对象:

const MongoClient = require('mongodb').MongoClient;MongoClient.connect('mongodb://localhost:27017/mongotest', (err, db)=>{ console.log('Connected to MongoDB!'); var collection = db.collection('testing'); collection.insert({'title': 'Snowcrash'}, (err, docs)=>{ /** * on successful insertion, log to the screen * the new collection's details: **/ console.log(`${docs.ops.length} records inserted.`); console.log(`${docs.ops[0]._id} - ${docs.ops[0].title}`); db.close(); });});

在前面的代码中,我们建立了与数据库的连接,并在连接完成后执行了一个“回调”。该“回调”接收两个参数,第二个参数是db对象本身。使用db对象,我们可以获取我们想要使用的集合。在这种情况下,我们将该集合保存为一个变量,以便在代码的其余部分更容易地使用它。使用collection变量,我们执行一个简单的insert命令,并将要插入到数据库中的 JSON 对象作为第一个参数传递。

“回调”函数在insert接受两个参数后执行,第二个参数是受命令影响的文档数组;在这种情况下,它是我们插入的文档数组。一旦insert完成并且我们在“回调”函数内部,我们记录一些数据。您可以看到docs.ops数组的长度为 1,因为我们只插入了一个文档。此外,您可以看到数组中的单个文档是我们插入的文档,尽管现在它有一个额外的_id字段,因为 MongoDB 会自动处理。

通过将findOne函数调用添加到刚刚插入的文档中,让我们稍微证明一下我们的代码。将test.js中的代码更改为以下示例:

const MongoClient = require('mongodb').MongoClient;MongoClient.connect('mongodb://localhost:27017/mongotest', (err, db)=>{ console.log('Connected to MongoDB!'); var collection = db.collection('testing'); collection.insert({'title': 'Snowcrash'}, (err, docs)=>{ console.log(`${docs.ops.length} records inserted.`); console.log(`${docs.ops[0]._id} - ${docs.ops[0].title}`); collection.findOne({title: 'Snowcrash'}, (err, doc)=>{ console.log(`${doc._id} - ${doc.title}`); db.close(); }); });});

在这段代码中,我们以与之前完全相同的方式插入记录;只是这一次,

我们对title执行findOnefindOne函数接受一个要匹配的 JSON 对象(这可以是您想要的精确或宽松)作为其第一个参数。在findOne之后执行的“回调”函数将包含找到的单个文档作为其第二个参数。如果我们执行了find操作,我们将根据搜索条件收到一组匹配的文档。

上述代码的输出应该是:

 $ node test.js Connected to MongoDB! 1 record inserted. 538bc3c1a39448868f7013b4 - Snowcrash 538bc3c1a39448868f7013b4 - Snowcrash

在你的输出中,你可能会注意到insert报告的_id参数与findOne中的不匹配。这很可能是多次运行代码的结果,导致插入了多个具有相同标题的记录。findOne函数将返回第一个找到的文档,没有特定的顺序,所以返回的文档可能不是最后一个插入的。

现在你已经基本了解了如何轻松地从 Node.js 连接和与 MongoDB 服务器通信,让我们来看看如何以一种不那么原始的方式来使用 MongoDB。

虽然直接使用mongodb模块很好,但它也有点原始,并且缺乏我们在使用 Express 等框架时所期望的开发者友好性。Mongoose是一个很棒的第三方框架,使得与 MongoDB 的工作变得轻而易举。它是 Node.js 的优雅的mongodb对象建模。

基本上,这意味着 Mongoose 赋予我们组织数据库的能力,使用模式(也称为模型定义),并为我们的模型提供强大的功能,如验证、虚拟属性等。Mongoose 是一个很棒的工具,因为它使得在 MongoDB 中处理集合和文档变得更加优雅。Mongoose 的原始mongodb模块是 Mongoose 的一个依赖,因此你可以将 Mongoose 看作是mongodb的一个包装器,就像 Express 是 Node.js 的一个包装器一样——两者都抽象了很多原始的感觉,并为你提供了更容易直接使用的工具。

需要注意的是,Mongoose 仍然是 MongoDB,所以你熟悉和习惯的一切都基本上是一样的;只是语法会稍微改变。这意味着我们从 MongoDB 中熟悉和喜爱的查询、插入和更新在 Mongoose 中也可以完美地工作。

我们需要做的第一件事是安装 Mongoose,以便在我们的mongotest项目中使用:

 $ npm install mongoose

安装完成后,让我们来看一下 Mongoose 提供的一些功能,我们将利用这些功能来简化开发依赖于 MongoDB 数据库的应用程序时的工作。

在 Mongoose 中,模式是我们用来定义模型的。将模式想象成用于创建模型的蓝图。使用模式,你可以定义比 MongoDB 模型的简单蓝图更多的内容。你还可以利用 Mongoose 默认提供的内置验证,添加静态方法、虚拟属性等。

在为模型定义模式时,我们首先要做的是构建我们认为对于特定文档我们将需要的每个字段的列表。字段由类型定义,并且你所期望的标准数据类型都是可用的,还有一些其他的:

  • String:这种类型存储字符串值。

  • Number:这种类型存储一个数字值,并带有限制。

  • Date:这种类型保存了一个datetime对象。

  • Buffer:这种类型提供了二进制数据的存储。例如,它可以包括图像或任何其他文件。

  • Boolean:这种类型用于存储布尔(true/false)值。

  • Mixed:这基本上是一个可以包含任何内容的非结构化对象。在存储 JSON 类型数据或任意数据时要考虑这一点。

可以是任何 JSON 表示。它不需要预定义。

  • ObjectID:当你想要在字段中存储另一个文档的 ObjectID 时,通常会使用这个。例如,在定义关系时。

  • Array:这是其他模式(模型)的集合。

这是一个基本的 Mongoose 模式定义的例子:

const mongoose = require('mongoose'), Schema = mongoose.Schema;var Account = new Schema({ username: { type: String }, date_created: { type: Date, default: Date.now }, visits: { type: Number, default: 0 }, active: { type: Boolean, default: false }}); 

在这里,我们为 Account 集合定义了模式。我们首先 require Mongoose,然后在我们的模块中使用 mongoose.Schema 定义了一个 Schema 对象。我们通过创建一个新的 Schema 实例来定义模式,构造函数对象定义了模式。定义中的每个字段都是一个基本的 JavaScript 对象,具有其类型,然后是一个可选的默认值。

在 Mongoose 中,模型是可以实例化的类(由模式定义)。

使用模式,我们定义 models,然后将它们用作常规 JavaScript 对象。

好处是 model 对象有额外的优势,即由 Mongoose 支持,因此还包括保存、查找、创建和删除等功能。让我们来看看如何使用模式定义模型,然后实例化模型并使用它。在您的实验文件夹 mongotest/test2.js 中添加另一个文件 test2.js,并在其中包含以下代码块:

const mongoose = require('mongoose'), Schema = mongoose.Schema;mongoose.connect('mongodb://localhost:27017/mongotest');mongoose.connection.on('open', function() { console.log('Mongoose connected.');});var Account = new Schema({ username: { type: String }, date_created: { type: Date, default: Date.now }, visits: { type: Number, default: 0 }, active: { type: Boolean, default: false }});var AccountModel = mongoose.model('Account', Account);var newUser = new AccountModel({ username: 'randomUser' });console.log(newUser.username);console.log(newUser.date_created);console.log(newUser.visits);console.log(newUser.active);

运行上述代码应该会得到类似以下的结果:

 $ node test2.js randomUser Mon Jun 02 2014 13:23:28 GMT-0400 (EDT) 0 false

当您使用新文档并且想要创建一个新实例、填充其值,然后将其保存到数据库时,创建新模型是很好的:

var AccountModel = mongoose.model('Account', Account);var newUser = new AccountModel({ username: 'randomUser'});newUser.save();

mongoose 模型上调用 .save 将触发对 MongoDB 的命令

将执行必要的 insertupdate 语句来更新服务器。当您切换到 mongo shell 时,您会看到新用户确实已保存到数据库中:

> use mongotestswitched to db mongotest> db.accounts.find(){ "username" : "randomUser", "_id" : ObjectId("538cb4cafa7c430000070f66"), "active" : false, "visits" : 0, "date_created" : ISODate("2014-06-02T17:30:50.330Z"), "__v" : 0 }

在模型上不调用 .save(),模型的更改实际上不会持久保存到数据库中。在您的 Node 代码中使用 Mongoose 模型就是这样——代码。您必须在模型上执行 MongoDB 函数,才能与数据库服务器进行任何实际的通信。

您可以使用 AccountModel 执行 find 操作,并根据一些检索条件返回一组 AccountModel 对象,这些条件从 MongoDB 数据库中检索结果。

// assuming our collection has the following 4 records: // { username: 'randomUser1', age: 21 } // { username: 'randomUser2', age: 25 } // { username: 'randomUser3', age: 18 } // { username: 'randomUser4', age: 32 } AccountModel.find({ age: { $gt: 18, $lt: 30 } }, function(err, accounts) { console.log(accounts.length); // => 2 console.log(accounts[0].username); // => randomUser1 mongoose.connection.close();});

在这里,我们使用标准的 MongoDB 命令 $gt$lt 来传递查询参数以查找文档(也就是查找年龄在 18 到 30 之间的文档)。find 执行后的回调函数引用了一个 accounts 数组,这是从 MongoDB 查询返回的 AccountModel 对象的集合。作为良好的管理方式,我们在完成后关闭了与 MongoDB 服务器的连接。

Mongoose 的核心概念之一是它在顶部强制执行模式

无模式设计,例如 MongoDB。这样做,我们获得了许多新功能,包括内置验证。默认情况下,每个模式类型都有一个内置的必需验证器。此外,数字有 minmax 验证器,字符串有枚举和匹配验证器。还可以通过模式定义自定义验证器。让我们简要看一下我们之前示例中添加的一些验证:

var Account = new Schema({ username: { type: String, required: true }, date_created: { type: Date, default: Date.now }, visits: { type: Number, default: 0 }, active: { type: Boolean, default: false }, age: { type: Number, required: true, min: 13, max: 120 }}); 

我们在模式中添加的验证是 username 参数现在是必需的,并且我们包含了一个名为 age 的新字段,它是一个必须在 13120(年)之间的数字。如果任一值不符合验证要求(即 username 为空或 age 小于 13 或大于 120),则会抛出错误。

每当调用模型的 .save() 函数时,验证将自动触发;但是,您还可以通过调用模型的 .validate() 函数并使用 callback 手动验证来处理响应。在上面的示例基础上,添加以下代码,将从定义的模式创建一个新的 mongoose 模型:

var AccountModel = mongoose.model('Account', Account);var newUser = new AccountModel({ username: 'randomUser', age: 11 });newUser.validate(function(err) { console.log(err);});// the same error would occur if we executed: // newUser.save(); 

运行上述代码应该会将以下错误记录到屏幕上:

{ message: 'Validation failed', name: 'ValidationError', errors: { age: { message: 'Path ' age ' (11) is less than minimum allowed value (13).', name: 'ValidatorError', path: 'age', type: 'min', value: 11 } }} 

您可以看到从 validate 返回的 error 对象非常有用,并提供了大量信息,可以在验证模型并将错误消息返回给用户时提供帮助。

验证是为什么在 Node 中始终接受error对象作为任何callback函数的第一个参数是非常重要的一个很好的例子。同样重要的是,您检查error对象并适当处理它。

Schema足够灵活,以便您可以轻松地向其添加自定义的静态方法,然后这些方法将对由该Schema定义的所有模型可用。静态方法非常适合添加您知道您将要在大多数模型中使用的辅助工具和函数。让我们从之前的简单年龄查询中重构它,使其成为一个静态方法并且更加灵活:

var Account = new Schema({ username: { type: String }, date_created: { type: Date, default: Date.now }, visits: { type: Number, default: 0 }, active: { type: Boolean, default: false }, age: { type: Number, required: true, min: 13, max: 120 }});Account.statics.findByAgeRange = function(min, max, callback) { this.find({ age: { $gt: min, $lte: max } }, callback);};var AccountModel = mongoose.model('Account', Account);AccountModel.findByAgeRange(18, 30, function(err, accounts) { console.log(accounts.length); // => 2 });

静态方法非常容易实现,一旦您开始充分利用它们,它们将使您的模型更加强大!

虚拟属性就是它们听起来的样子——虚假的属性,实际上并不存在于您的 MongoDB 文档中,但您可以通过组合其他真实属性来伪造它们。最明显的虚拟属性的例子是fullname字段,当只有firstnamelastname是 MongoDB 集合中的实际字段时。对于fullname,您只需返回模型的名字和姓氏组合成一个字符串,并将其标记为fullname

// assuming the Account schema has firstname and lastname defined: Account.virtual('fullname') .get(function() { return this.firstname + ' ' + this.lastname; }) .set(function(fullname) { var parts = fullname.split(' '); this.firstname = parts[0]; this.lastname = parts[1]; });

我们调用.get().set()函数。虽然不需要同时提供两者,但这是相当常见的。

在这个例子中,我们的get()函数只是执行基本的字符串连接并返回一个新值。我们的.set()函数执行相反的操作,将字符串在空格上拆分,然后将模型的firstnamelastname字段值分别赋值给每个结果。您可以看到,如果有人尝试使用 Dr. Kenneth Noisewater 这样的值来设置模型的fullname,那么.set()的实现就有点不稳定。

重要的是要注意,虚拟属性不会持久保存到 MongoDB,因为它们不是文档或集合中的真实字段。

Mongoose 还有很多功能可以使用,我们只是刚刚触及了表面。幸运的是,它有一个相当深入的指南,您可以在以下链接中参考:mongoosejs.com/docs/guide.html

一定要花时间审查 Mongoose 文档,以便您熟悉所有可用的强大工具和选项。

这就结束了我们对 Mongoose 模型、模式和验证的介绍。接下来,让我们重新回到我们的主要应用程序,并编写我们将用来替换现有样本ViewModels并与 Mongoose 连接的模式和模型。

在开始下一节之前,读者可能已经注意到我们并没有在所有情况下使用箭头函数作为callbacks的替代品。这是因为我们将广泛使用函数的上下文(this)。在实现上,箭头函数和常规函数在上下文方面有所不同。不同的是,函数的上下文(this)不取决于它在哪里声明,而是取决于它从哪里调用。为了理解这一点,让我们考虑以下例子:

function getData() { console.log(this.a); // global }getData.a = 'hello';var a = 'world';getData(); 

运行前面的片段,我们将得到输出'world';这是因为 foo 函数在全局范围内调用,因此上下文是全局的,我们接收到全局变量的值。为了限制这种行为,我们可以使用bind方法或 es6 箭头函数。

现在,我们将前面的get virtual方法转换如下:

Account.virtual('fullname') .get(()=>{ return this.firstname + ' ' + this.lastname; }) 

在前面的箭头函数中的上下文不会引用Account模式,因此将得到未定义。为了避免这种行为,我们将继续使用常规函数。了解有关箭头函数的词法作用域机制的更多信息,请访问以下链接:goo.gl/bXvFRM。有关如何使用不同方法处理此问题的更多详细信息,请访问以下链接:github.com/Automattic/mongoose/issues/5057

使用 Mongoose 连接到 MongoDB 服务器的行为几乎与我们之前使用mongodb模块时使用的方法相同。

首先,我们需要确保安装了 Mongoose。在这一点上,我们将在我们的主应用程序中使用 Mongoose,因此我们希望在主项目目录中安装它,并更新package.json文件。使用您的命令行终端程序,更改位置到您的projects文件夹,并通过npm安装 Mongoose,确保使用--save标志以更新package.json文件:

 $ cd ~/projects/imgPloadr $ npm install mongoose --save

安装了 Mongoose 并更新了项目的package.json文件后,我们准备打开与 MongoDB 服务器的连接。对于我们的应用程序,我们将在应用程序启动时打开与 MongoDB 服务器的连接,并在应用程序的整个生命周期内保持与数据库服务器的开放连接。让我们编辑server.js文件,包括我们需要的连接代码。

首先,在文件的顶部包含 Mongoose:

cons express = require('express'), config = require('./server/configure'), mongoose = require('mongoose'); var app = express(); 

然后,在app = config(app);行后面插入以下代码:

mongoose.connect('mongodb://localhost/imgPloadr', { useMongoClient : true }); mongoose.connection.on('open',()=>{ console.log('Mongoose connected.'); }); 

就是这样!这几行简单的代码就可以打开与 MongoDB 服务器的连接,我们的应用程序已经准备好开始与数据库通信。我们传递给 Mongoose 的connect函数的参数是一个 URL 字符串,指向我们本地运行的 MongoDB 服务器,并带有我们要使用的集合的路径。useMongoClient用于选择新的连接方法作为默认方法,但一旦我们开始使用 Mongo 版本 4.11.0 及以上版本,它将被弃用。此外,我们为mongoose.connection对象的open事件添加了一个事件侦听器,当它触发时,我们只需记录一个输出消息,即数据库服务器已连接。

为了我们正在构建的应用程序,我们将只有两种不同的唯一模式和相关模型:Image模型和Comment模型。如果我们将这个应用程序投入生产并真正构建它,具有所有必要功能,我们还希望有更多的模型。

首先,在项目中创建一个名为models的新目录,我们将在这里存储每个模型的 Node.js 模块。在此目录中创建三个文件,命名为image.jscomment.jsindex.js。让我们首先看一下Image模型。将以下代码块复制到models/image.js文件中:

const mongoose = require('mongoose'), Schema = mongoose.Schema, path = require('path');const ImageSchema = new Schema({ title: { type: String }, description: { type: String }, filename: { type: String }, views: { type: Number, 'default': 0 }, likes: { type: Number, 'default': 0 }, timestamp: { type: Date, 'default': Date.now }});ImageSchema.virtual('uniqueId').get(function() { return this.filename.replace(path.extname(this.filename), '');});module.exports = mongoose.model('Image', ImageSchema); 

首先,我们使用各种字段定义了我们的ImageSchema,这些字段将存储在 MongoDB 中的每个图像。我们创建了一个uniqueIdvirtual属性,它只是去掉文件扩展名的文件名。由于我们希望我们的Image模型在应用程序的其余部分中可用,我们使用module.exports导出它。请注意,我们正在导出模型,而不是模式(因为模式本身对我们来说相当无用)。让我们为评论设置一个类似的模型。将以下代码块复制到models/comment.js文件中:

const mongoose = require('mongoose'), Schema = mongoose.Schema, ObjectId = Schema.ObjectId; const CommentSchema = new Schema({ image_id: { type: ObjectId }, email: { type: String }, name: { type: String }, gravatar: { type: String }, comment: { type: String }, timestamp: { type: Date, 'default': Date.now } }); CommentSchema.virtual('image') .set(function(image){ this._image = image; }).get(function() { return this._image; }); module.exports = mongoose.model('Comment', CommentSchema);

有几件重要的事情需要注意这个模型。首先,我们有

一个名为image_id的字段,其类型为ObjectId。我们将使用这个

用于存储commentimage之间关系的字段。

存储在此字段中的ObjectId是来自 MongoDB 的相关图像文档的_id

我们还在comment模式上定义了virtual,标记为image,我们为其提供了 getter 和 setter。当我们在控制器中稍后检索评论时,image虚拟属性将是我们附加相关图像的方式。对于每条评论,我们将遍历并查找其关联的图像,并将该image对象作为评论的属性附加上去。

处理集合的名称

您使用单数形式命名您的模型,Mongoose 会识别这一点,并使用复数形式的模型名称创建您的集合。因此,在 MongoDB 中定义为Image的模型将具有名为images的集合。Mongoose 会尝试智能处理这一点;但是,在 MongoDB 中定义为Person的模型将具有名为people的对应集合,依此类推。(是的,octopus 会导致 octopi!)

在我们的项目中,还有一个我们尚未涉及的models文件夹中的最后一个文件。index.js文件在 Node.js 中的任何文件夹中都充当其中模块的index文件。这是按照惯例的,所以如果您不想遵循这个规则,也是可以的。

由于我们的models文件夹将包含许多不同的文件,每个文件都是我们模型中的一个独特模块,如果我们可以在单个require语句中包含所有模型,那将是很好的。使用index.js文件,我们也可以很容易地做到这一点。将以下代码块复制到models/index.js文件中:

module.exports = { 'Image': require('./image'), 'Comment': require('./comment') };

models目录中的index.js文件只是定义了一个 JavaScript 对象,其中包含我们目录中每个模块的名称-值对。我们手动维护这个对象,但这是概念的最简单实现。现在,由于这个基本文件,我们可以在应用程序的任何地方执行require('./models'),并知道我们通过该模块拥有每个模型的字典。要引用该模块中的特定模型,我们只需将特定模型作为模块的属性引用。如果我们只想在应用程序的某个地方要求特定的模型,我们也可以轻松地执行require('./models/image')!稍后您将看到更多内容,这将变得更加清晰。

因为我们的两个模型是如此密切相关,所以我们通常会在整个应用程序中始终使用require('./models')来要求models字典。

CRUD代表创建、读取、更新和删除。现在我们的模式已经定义好,我们的模型也准备就绪,我们需要通过在必要时更新我们的控制器,使用各种 CRUD 方法在整个应用程序中开始使用它们。直到这一点,我们的控制器只包含了占位符或假数据,因此我们可以证明我们的控制器正在工作,并且我们的视图模型已经连接到我们的模板。我们开发的下一个逻辑步骤是直接从 MongoDB 中填充我们的视图模型。如果我们可以直接将我们的 Mongoose 模型传递给我们的模板作为viewModel本身,那将更好。

如果您回忆一下第六章“更新主控制器”部分,控制器和视图模型,我们最初创建了viewModel,其中包含了主控制器中的一组 JavaScript 对象,这些对象只是占位符数据:

var viewModel = { images: [ { uniqueId: 1, title: 'Sample Image 1', description: '', filename: 'sample1.jpg', views: 0, likes: 0, timestamp: Date.now() }, { uniqueId: 2, title: 'Sample Image 2', description: '', filename: 'sample2.jpg', views: 0, likes: 0, timestamp: Date.now() }, { uniqueId: 3, title: 'Sample Image 3', description: '', filename: 'sample3.jpg', views: 0, likes: 0, timestamp: Date.now() }, { uniqueId: 4, title: 'Sample Image 4', description: '', filename: 'sample4.jpg', views: 0, likes: 0, timestamp: Date.now() } ] }; 

我们将用一个非常简化的版本替换viewModel,然后从我们的 Mongoose 模型中填充它们的真实数据:

var viewModel = { images: []}; 

在我们可以用真实数据填充viewModel之前,我们首先需要确保我们的主控制器可以使用我们的模型。为此,我们必须要求models模块。将其包含在controllers/home.js文件的顶部:

const sidebar = require('../helpers/sidebar'), ImageModel = require('../models').Image;

我们本可以要求完整的models模块,并且可以访问Comment模型和Image模型;然而,对于主页,我们实际上只需要使用Image模型。现在,我们的mongoose模型对于主页控制器是可用的,我们可以执行find操作来检索最新图片的列表,以便在主页上显示。用这段代码更新你的主页控制器中现有的sidebar()调用:

ImageModel.find({}, {}, { sort: { timestamp: -1 } }, (err, images) => { if (err) { throw err; } viewModel.images = images; sidebar(viewModel, (viewModel) => { res.render('index', viewModel); }); }); 

使用ImageModel,我们执行了一个 MongoDB 的find查询,但是我们没有为实际查询提供任何具体信息(一个空的 JavaScript 对象),这意味着它将返回每个文档。第二个参数也是一个空的 JavaScript 对象,这意味着我们没有指定如何映射结果,因此将返回完整的模式。第三个参数是一个options对象,我们可以在其中指定诸如sort字段和顺序之类的东西。在这个特定的查询中,我们按照时间戳的降序检索了图像集合中的每个图像(升序的顺序将具有1而不是-1的值)。

在成功执行对 MongoDB 数据库服务器的find查询后执行的callback函数将返回一个error对象和一个匹配模型的images数组;在我们的情况下,它是数据库中的每个图像。使用从查询返回的数组,我们简单地通过其images属性将其附加到我们的viewModel上。然后,我们调用我们的sidebar函数,就像我们之前做的那样。

在这一点上,我们不再用固定数据填充viewModel,而是用我们的 MongooseImage模型执行基本的find查询时返回的数据来填充它。应用程序的主页现在正式是数据驱动的。以下是整个controllers/home.js文件的概述:

const sidebar = require('../helpers/sidebar'), ImageModel = require('../models').Image; module.exports = { index: (req, res)=>{ var viewModel = { images: [] }; ImageModel.find({}, {}, { sort: { timestamp: -1 }}, (err, images)=>{ if (err) { throw err; } viewModel.images = images; sidebar(viewModel, (viewModel)=>{ res.render('index', viewModel); }); }); } }; 

如果你运行应用程序并在浏览器中打开它,你实际上在主页上看不到任何东西。这是因为我们实际上还没有插入任何数据。接下来就要来了。但是,请注意页面本身仍然可以工作,你不会收到任何错误。这是因为 MongoDB 只是从ImageModelfind中返回一个空数组,Handlebars 主页模板可以很好地处理,因为它对空数组执行了each操作,所以在主页上显示零张图片。

image控制器是我们应用程序中最大的组件。它包含了大部分,如果不是全部,支撑我们应用程序的逻辑。这包括显示图像的所有细节,处理图像的上传,以及处理喜欢和评论。在这个控制器中有很多内容要涵盖,所以让我们按每个部分来分解。

我们image控制器中index函数的主要责任是检索单个特定图像的详细信息,并通过其viewModel显示出来。除了实际图像的详细信息之外,图像的评论也以列表的形式显示在页面上。每当查看图像时,我们还需要更新图像的查看次数,并将其增加一次。

首先编辑controllers/image.js文件,并在顶部更新所需模块的列表,包括我们的models模块:

const fs = require('fs'), path = require('path'), sidebar = require('../helpers/sidebar'), Models = require('../models');

我们还希望将viewModel简化到最基本的形式,就像我们在主页控制器中所做的那样。用这个新的、更轻的版本替换现有的viewModel对象变量:

var viewModel = { image: {}, comments: [] }; 

在定义一个空的viewModel之后,让我们在Image模型上包含一个find调用,以便我们可以通过其filename查找特定的图像:

Models.Image.findOne({ filename: { $regex: req.params.image_id } }, (err, image) => { if (err) { throw err; } if (image) { // to do... } else { res.redirect('/'); } }); 

在上述代码中,我们使用Models模块的Image模型并执行findOne,它与find相同,只会返回单个文档(匹配或不匹配),而不是find返回的数组。按照惯例,我们在回调的第二个参数中使用单数变量名,而不是复数变量名,这样我们作为开发人员就可以轻松地知道我们是在处理单个对象还是一组对象/集合。

我们提供的query对象作为第一个参数匹配了 MongoDB 的regex过滤器中的图像文档的filename字段,并将其与req.params.image_id进行比较,这是 URL 中参数的值,如我们的routes文件中定义的那样。图像页面的 URL 将始终是http://localhost:3300/images/abcdefg,其中abcdefg将是req.params.image_id的值。如果您还记得,我们在上传图像时在create函数中随机生成这个值。

在确保我们的err对象不为空的情况下,我们然后检查我们的image对象也不为空。如果它不为空,这意味着从 MongoDB 返回了一个模型;因此,我们找到了我们的图像,一切正常。如果没有返回image模型,因为我们尝试搜索不存在的文件名的图像,我们只需将用户重定向回主页。

现在让我们通过在// to do...占位符注释的区域插入以下行来填充我们的viewModel

image.views = image.views + 1; viewModel.image = image; image.save(); 

我们将从findOne返回的image模型附加到我们的viewModel.image属性上,但在此之前,我们需要将该模型的views属性增加1(以便在加载页面时表示实际的加一次查看)。由于我们修改了模型(通过增加其视图计数),我们需要确保将其保存回 MongoDB,因此我们调用模型的save函数。

现在viewModel已经使用image模型和视图更新了

计数已经增加并保存,我们需要检索与图像关联的评论列表。让我们在之前的image.save();之后立即插入一些代码块:

Models.Comment.find({ image_id: image._id }, {}, { sort: { 'timestamp': 1 } }, (err, comments) => { if (err) { throw err; } viewModel.comments = comments; sidebar(viewModel, (viewModel) => { res.render('image', viewModel); }); });

使用我们的Comment模型上的find,我们可以将包含我们查询的对象作为第一个参数传递;在这种情况下,我们指定要获取所有image_id字段等于主image模型的_id属性的所有评论

我们之前附加到viewModel上的。

这段代码可能看起来有点奇怪,所以让我们详细解释一下。请记住,从原始的Models.Image.findOne()调用返回的image对象在整个callback函数的范围内都是可用的。无论我们嵌套多深的callback函数,我们始终可以访问原始的image模型。因此,我们可以在Model.Comment.find()执行时触发的callback函数内访问它及其属性。

一旦进入Commentfind回调,我们将返回的comments数组附加到我们的viewModel,然后像我们第一次打开控制器并开始编辑这个index函数时一样调用我们的sidebar函数。

回顾一下,在controllers/image.js文件中完全更新后,整个index函数如下:

index: (req, res)=>{ var viewModel = { image: {}, comments: [] }; Models.Image.findOne({ filename: { $regex: req.params.image_id } }, (err, image)=>{ if (err) { throw err; } if (image) { image.views = image.views + 1; viewModel.image = image; image.save(); Models.Comment.find( { image_id: image._id}, {}, { sort: { 'timestamp': 1 }}, (err, comments)=>{ viewModel.comments = comments; sidebar(viewModel, (viewModel)=>{ res.render('image', viewModel); }); } ); } else { res.redirect('/'); } }); }

让我们快速回顾一下index控制器的责任和任务:

  1. 创建一个新的空viewModel对象。

  2. 创建findOne imagefindOneimage模型,其中文件名是与 URLimage_id参数匹配的正则表达式。

  3. 将找到的image的视图增加一次。

  4. 将找到的image模型附加到viewModel

  5. 保存image模型,因为它的view已经更新。

  6. 查找所有image_id属性等于_id的评论

原始的image模型。

  1. 将找到的comments数组附加到viewModel

  2. 使用sidebar渲染页面,传入viewModel

callback函数。

我们已经在我们的create函数中放置了处理随机命名和上传图像文件的功能。现在,我们需要将该信息保存到 MongoDB 中以供上传的图像使用。

让我们更新controllers/images.js:create中原始的saveImage函数,并包括将其与数据库联系起来的功能。

我们对saveImage函数的目标是双重的。首先,我们要确保

我们永远不会将具有与数据库中已存在的图像相同的随机生成文件名的图像保存到数据库中。其次,我们希望确保只有在成功上传、重命名和保存到文件系统后才将图像插入到数据库中。我们将对现有代码进行两处修改以实现这一点。

第一个修改是用find对随机生成的文件名进行大部分逻辑包装,如果从 MongoDB 返回任何文档作为匹配项,就重新开始这个过程,并重复这个过程直到实现真正的唯一文件名。执行搜索的代码如下:

Models.Image.find({ filename: imgUrl }, (err, images)=>{ if (images.length> 0) { saveImage(); } else { // do all the existing work... } });

如果从find返回的images数组的长度大于零,则意味着至少找到一个具有与我们的随机for循环生成的相同文件名的图像。如果是这种情况,我们希望再次调用saveImage,这将重复整个过程(随机生成一个新名称并在数据库中执行find)。我们通过先定义saveImage函数作为变量来实现这一点,这样在saveImage函数本身内部,我们可以通过调用原始变量作为函数来再次执行它。

调用自身的函数称为递归函数。

最初,create函数的最后一步是在文件系统重命名完成时触发的callback中将访问者重定向到图像的页面。这是我们希望创建一个新的 Mongooseimage模型的地方。我们应该只在数据库服务器完成保存图像时才重定向(再次依赖于callback函数)。考虑原始函数中的以下行:假设从find中没有返回图像,这意味着我们已经为我们的图像生成了一个真正独特的文件名,并且我们可以安全地重命名文件并将其上传到服务器,以及在数据库中保存记录:

res.redirect('/images/${ imgUrl}'); 

用这个新的代码块替换这个:

var newImg = new Models.Image({ title: req.body.title, description: req.body.description, filename: imgUrl + ext});newImg.save((err, image) => { console.log('Successfully inserted image: ' + image.filename); res.redirect(`/images/${image.uniqueId}`);}); 

在这里,我们创建一个全新的Image模型,并通过其构造函数传入默认值。titledescription字段直接从通过req.body和表单字段名称(.title.description)传入的值设置。filename参数的构建方式与我们最初设置其重命名目的地的方式相同,只是我们不包括路径和目录名称,只包括随机生成的文件名和图像的原始扩展名。

我们调用模型的.save()函数(就像我们之前在index controller函数中更新图像的views属性时所做的那样)。save函数在其callback中接受第二个参数,这将是其更新版本。一旦save完成并且图像已插入到 MongoDB 数据库中,我们就会重定向到图像的页面。callback返回其更新版本的原因是因为 MongoDB 自动包含其他信息,如_id

作为审查和合理检查,这是controllers/image.js:createsaveImage函数的完整代码,新添加的代码已经清晰地标出:

var saveImage = function() { var possible = 'abcdefghijklmnopqrstuvwxyz0123456789', imgUrl = ''; for (var i = 0; i < 6; i += 1) { imgUrl += possible.charAt(Math.floor(Math.random() * possible.length)); } /* Start new code: */ // search for an image with the same filename by performing a find: Models.Image.find({ filename: imgUrl }, (err, images) => { if (images.length > 0) { // if a matching image was found, try again (start over): saveImage(); } else { /* end new code:*/ var tempPath = req.files.file.path, ext = path.extname(req.files.file.name).toLowerCase(), targetPath = path.resolve(`./public/upload/${imgUrl}${ext}`); if (ext === '.png' || ext === '.jpg' || ext === '.jpeg' || ext === '.gif') { fs.rename(tempPath, targetPath, (err) => { if (err) { throw err; } /* Start new code: */ // create a new Image model, populate its details: var newImg = new Models.Image({ title: req.body.title, filename: imgUrl + ext, description: req.body.description }); // and save the new Image newImg.save((err, image) => { res.redirect(`/images/${image.uniqueId}`); }); /* End new code: */ }); } else { fs.unlink(tempPath, () => { if (err) { throw err; } res.json(500, { error: 'Only image files are allowed.' }); }); } /* Start new code: */ } }); /* End new code: */};saveImage();

不要忘记在函数定义后立即执行saveImage();否则,什么也不会发生!

到目前为止,我们已经将大部分关键功能与 MongoDB 集成起来,我们的应用程序应该真的感觉像是在一起了。让我们进行一次测试运行,确保到目前为止我们的所有端点都在工作。启动应用程序并在浏览器中打开它:

 $ node server.js Server up: http://localhost:3300 Mongoose connected.

打开浏览器,将其指向http://localhost:3300,你应该看到你的应用程序正在运行,如下面的截图所示:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (14)

继续使用首页上的表单,在你的电脑上搜索一张图片文件并选择它。提供标题和描述的输入,然后点击“上传图片”按钮。你应该直接进入图片页面,显示你上传的图片的详细信息:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (15)

返回首页,你现在应该在“最新图片”部分看到你的新图片显示出来了:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (16)

接下来,让我们为“喜欢”按钮添加支持。记住我们的“喜欢”按钮的工作方式有点不同。它使用了带有 jQuery 的 AJAX,这样数据可以实时发送和接收,而不用重新加载整个页面。用户的体验是无缝和愉快的,因为他们不会失去页面上的滚动位置,也不会遇到其他令人不快的与 UI 相关的问题。

“喜欢”按钮点击的端点是/images/:image_id/like,所以我们将使用 URL 中的image_id的值来在 MongoDB 中找到并检索图片,将其likes值增加1,然后返回图片的新总点赞数(以便 UI 可以更新为新值)。

目前,controllers/image.js中的like函数只是简单地用一个硬编码值1做 JSON 响应:

res.json({likes: 1}); 

让我们用新的代码替换原始代码,这段代码将使用 Mongoose 的Image模型来查找一个文件名与通过 URL 传递的image_id匹配的图片:

Models.Image.findOne({ filename: { $regex: req.params.image_id } }, (err, image) => { if (!err && image) { image.likes = image.likes + 1; image.save((err) => { if (err) { res.json(err); } else { res.json({ likes: image.likes }); } }); } }); 

假设callback函数从查询中接收到一个有效的image模型响应,我们将增加它的likes属性,然后由于模型被修改,我们需要执行它的save函数。在save函数的callback中,我们发送一个 JSON 响应,其中包含图片点赞的实际当前值,返回给浏览器。

有时,我们会在 JavaScript 中使用简写,写出类似以下的内容:

if (!err && image)

在前面的示例中的if语句中,我们在说,如果err对象是false(即null),并且image对象是true(即不是null),那么我们就可以继续了!

有了这段代码,你可以再次运行应用程序,通过查看你之前上传的图片并点击“喜欢”按钮来测试。如果成功,按钮旁边的计数器应该增加一。刷新页面,点赞数应该保持为新值。

插入评论的工作方式几乎与给图片点赞的方式完全相同。唯一的区别是我们创建了一个新的comment模型,而不是更新一个image模型。我们在comment函数中原来的代码是:

res.send('The image:comment POST controller'); 

让我们用一些代码来替换这段文字,代码将再次通过image_id在 URL 中找到图片,但这一次,我们不是更新它的点赞,而是创建一个新的评论,并将评论的image_id值赋予我们当前查看的图片的_id(这是为了将评论与图片关联起来,使其实际上属于一张图片)。用以下代码块替换controllers/image.js中的整个comment函数:

Models.Image.findOne({ filename: { $regex: req.params.image_id } }, (err, image) => { if (!err && image) { var newComment = new Models.Comment(req.body); newComment.gravatar = md5(newComment.email); newComment.image_id = image._id; newComment.save((err, comment) => { if (err) { throw err; } res.redirect(`/images/${image.uniqueId}#${comment._id}`); }); } else { res.redirect('/'); } }); 

在这里,你可以看到我们正在使用与like函数相同的代码来查询 MongoDB,并从 URL 中找到与filename匹配的图片。

假设一个有效的图片作为匹配返回,我们创建一个名为newComment的新的comment对象,并将整个 HTML 表单主体传递给构造函数。这有点作弊,因为我们的 HTML 表单恰好使用了与comment模型相同的名称和结构的form字段。如果你对req.body对象执行console.log操作,你会看到类似以下的内容:

{ name: 'Jason Krol', email: 'jason@kroltech.com', comment: 'This is what a comment looks like?!' } 

这与我们手动构建的内容完全相同,所以我们只是采取了一种捷径,将整个内容传递进去!之后,我们更新了newComment模型的一些属性。首先,我们手动设置了一个gravatar属性,这是我们将存储评论者的电子邮件地址的 MD5 哈希值,以便我们可以检索他们的 Gravatar 个人资料图片。Gravatar 是一个根据用户的电子邮件地址存储个人资料图片的通用头像服务。然而,他们用于每个个人资料的唯一 ID 是一个 MD5 哈希值,这就是为什么我们必须存储该值。

由于我们依赖第三方的 MD5 模块,我们需要确保它已经安装在我们的项目中,并且作为一个依赖保存在我们的package.json文件中。从你的项目根文件夹中,执行以下命令:

 $ npm install md5 --save

此外,我们需要在controllers/image.js文件中要求该模块

在顶部,以及我们需要的其他模块一起:

const fs = require('fs'), path = require('path'), sidebar = require('../helpers/sidebar'), Models = require('../models'), md5 = require('md5'); 

最后,我们将newCommentimage_id属性设置为函数开始时找到的图片的_id属性。然后,我们调用comment模型的.save()函数,并将用户重定向回图片页面。为了方便起见,我们将新评论的_id附加到 URL 中,这样当页面加载时,它将自动滚动到刚刚发布的用户评论处。

有了这个功能,继续启动应用程序并在浏览器中打开它。访问你上传的任何图片的图片页面,并发表评论。一旦评论发布并页面重新加载,你应该在图片下看到类似以下截图的东西:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (17)

我们本可以选择使用 jQuery 和 AJAX 来处理评论,就像我们处理“喜欢”按钮一样。然而,这会引入更多的复杂性,因为如果我们这样做,我们将需要一种稍微复杂的方式来显示插入的评论到屏幕上。这将涉及大量依赖 jQuery 来进行一些高级的 DOM 操作,以便在使用 AJAX 发布评论后显示评论。

在后面的章节中,当我们回顾单页应用程序时,我们将简要介绍一些执行这种功能以及其他高级功能的 JavaScript 框架,这些功能包括image控制器的代码和功能。

让我们快速回顾一下我们对这个控制器所做的所有更改:

  • 我们更新了index函数,从 MongoDB 中检索图片,并用image模型的细节填充viewModel。我们还发现

所有与该图片相关的评论都已经插入,并将这些评论的数组附加到viewModel中。

  • 我们调整了create函数,一旦成功重命名并保存到文件系统中,就会将新的image模型插入到数据库中。

  • like函数已经更新,实际上增加了图片的likes属性的值,并将该值保存到数据库中,同时通过 JSON 响应返回新的值。

  • 现在通过comment函数为特定的图片插入评论。不仅插入了一个comment模型到数据库中,而且还找到了对应的图片,并将image模型的_id值附加到评论中,以巩固关系。

拼图的最后一块,也是我们需要与 MongoDB 连接的最后一个领域是侧边栏。为此,我们需要更新之前创建的每个助手。我们为大多数编写代码的助手将使用本章中已经涵盖的概念和功能。但是,在我们查看代码之前,我想专注于一个新概念的补充。

JavaScript 本质上是异步的,毫无疑问,总会有这样的时候,我们需要一种处理同时执行多个不同异步函数的方法。这里的一个大问题是,如果我们尝试在 MongoDB 服务器上执行三个不同的查询,我们如何知道这三个在移动并处理结果之前何时完成?到目前为止,我们一直在依赖一个单一的 callback 函数,这对于单个调用非常有效。我们如何将单一的 callback 函数分配给多个异步调用呢?

答案是,我们无法直接做到。您可以使用许多嵌套的 callbacks 来实现这一点,但这通常被认为是不良实践,并且会显著降低代码的可读性。但是,您可以使用一个专门为此需求设计的第三方模块。async 是一个强大的 Node 模块,可以通过 npm 下载和安装,提供了许多非常有用的实用程序函数,都旨在在处理一系列异步函数时提供帮助。我们将在本章中使用的两个函数是 seriesparallelseries 函数允许我们按顺序执行异步函数,每个函数等待前一个函数完成,然后在最后执行一个单一的 callback 函数。parallel 函数允许我们执行相反的操作——同时执行多个异步函数,等待它们全部完成,然后在最后一个函数完成时执行一个单一的 callback 函数。

一个单一的 callback 函数如何处理多个不同异步函数的响应?答案是:

通过接受每个函数的响应数组作为参数!

由于我们将在项目中使用 async,让我们通过 npm 安装它,并确保我们的 package.json 文件也得到更新。在您的 project 文件夹的根目录中,从命令行执行以下操作:

 $ npm install --save async

让我们来看看我们的助手之一 comments 助手中 async 的第一个用法。最初,helpers/comments.js 是一个模块,其中有一个 newest 函数,返回一个包含一些示例评论的固定数据数组。

我们将完全删除此代码,而是查询 MongoDB 的 newest 评论并将其作为数组返回。首先清除 comment 助手模块,并从头开始(请注意,我们向 newest 函数添加了一个新的 callback 参数):

var models = require('../models'), async = require('async'); module.exports = { newest: (callback)=>{ // to do... } }; 

请注意,我们在文件顶部添加了额外的 require 语句,用于我们的 modelsasync。在 newest 函数中,让我们用代码替换 // to do... 注释,查询 MongoDB 并找到最近的五条评论:

models.Comment.find({}, {}, { limit: 5, sort: { 'timestamp': -1 } }, (err, comments) => { // to do - attach an image to each comment... }); 

请注意,find 查询中的第一个参数是一个空的 JavaScript 对象,这意味着我们将检索数据库中的每条评论。然而,对于第三个参数,我们使用 limitsort,以便将返回的记录数量限制为五条,并按照 timestamp 降序排序查询。

现在我们有了一个评论数组,我们理想情况下希望返回每条评论所属的图片。通常,这可以通过在 MongoDB 中使用 aggregate 查询来连接不同的集合(例如 SQL 中的 JOIN)来实现。我们将在下一章中更详细地了解 aggregate

为了我们代码的目的,我们将分别查询 MongoDB 以获取每个评论并检索与评论的image_id值相关联的图像。

首先,让我们定义一个函数,该函数将查询 MongoDB 并检索并附加image模型到comment模型:

var attachImage = (comment, next) => { models.Image.findOne({ _id: comment.image_id }, (err, image) => { if (err) throw err; comment.image = image; next(err); });}; 

该函数将接受一个comment模型作为第一个参数,并接受一个名为nextcallback函数作为第二个参数。将next回调作为第二个参数是很重要的,因为这是async能够运行的关键。想象一下,下一个callback就像一个链环。由于同一个函数将被调用用于集合中的每个项目,因此需要一种方法来将这些调用链接在一起。这是通过callback来执行的。

基本上,每次对数组中的项目调用callback时,它都会执行其工作,然后执行相同的callback以处理数组中的下一个项目,依此类推,这就是为什么我们将回调函数参数命名为next的原因。

这个函数的另一个重要元素是,当我们将image模型附加到评论的image属性时,我们使用了之前在主评论模式中设置的virtual属性。如果你还记得,当我们设置image属性时,实际上是在设置私有的_image属性。同样,当我们获取image属性时,实际上是在检索私有的_image属性。

在我们定义了attachImage函数之后,我们需要使用asynceach函数将该函数应用于comments集合中的每个项目:

async.each(comments, attachImage, (err) => { if (err) throw err; callback(err, comments); });

asynceach函数将循环遍历第一个参数中集合中的每个项目,并将每个项目作为第二个参数中的callback函数的参数发送。第三个参数是final callback函数,一旦整个系列与集合完成,就会执行。在这种情况下,评论数组中的每个评论将单独传递给attachImage函数。当整个集合被迭代完毕时,将执行final callback,这基本上触发了作为其唯一参数传递给newest函数的第一个callback函数。哇,这真是一大堆!让我们试着再详细解释一下,这样就会更有意义一些:

  • comment助手模块的newest函数接受一个名为callback的参数- -这是将被调用的函数

一旦整个函数中的所有工作都完成了。

  • newest函数的第一件事是找到最新的五条评论。

并将它们作为数组返回给一个匿名定义的内联函数。

  • 首先,我们定义一个函数并将其存储在名为attachImage的变量中。

  • attachImage函数接受两个参数:一个单独的评论模型和一个我们命名为next的回调函数。

  • attachImage函数将查询 MongoDB 以找到与

commentimage_id属性相同的_id

将传递给它的作为第一个参数。

  • 一旦找到该图像,它将通过其image属性附加到评论中,然后执行下一个callback函数。

  • 我们使用async.each循环遍历作为each的第一个参数传递的comments数组中的每个评论。

  • attachImage函数作为第二个参数传递,这是将为评论数组中的每个评论调用的函数。

  • 最后,定义一个内联匿名函数,该函数将在对评论集合中的最后一项进行迭代后执行。这个内联函数本身只接受一个error对象作为参数。假设comments集合的每次迭代都成功,这个函数将在没有错误的情况下执行。在这个函数内部,我们执行了原始函数callback,这个函数是newest函数的唯一参数,并且callback被调用时,新更新的评论数组作为它的第二个参数。

好了,最困难的部分已经过去了!你已经完成了关于async模块的速成课程,并且希望你毫发无损地度过了!为了安全起见,这里是helpers/comments.js模块文件的完整代码:

/* jshint node: true */ "use strict" var models = require('../models'), async = require('async'); module.exports = { newest: (callback)=>{ models.Comment.find({}, {}, { limit: 5, sort: { 'timestamp': -1 } }, (err, comments)=>{ //console.log("COCOCO"); //console.log(comments); var attachImage = (comment, next)=>{ models.Image.findOne({ _id : comment.image_id}, (err, image)=>{ if (err) throw err; comment.image = image; next(err); }); }; async.each(comments, attachImage, (err)=>{ if (err) throw err; callback(err, comments); }); }); } };

到处都是回调函数!

此时,可能会因为我们处理的callbacks数量而感到有些困惑。问题的一部分是我们一直在使用的术语。任何作为参数传递并且只在满足特定条件后执行的函数,通常作为原始函数的最终结果,被称为callback。JavaScript 的常规约定是在参数中直接使用变量名callback来标记callback函数,以便清晰明了。当你阅读代码时,这样做非常好,但当你解释代码并引用一个名为callback的函数时,这样做就不太好了!

好了!当然,肯定有一个陷阱,对吧!?嗯,有点。因为我们在Comments助手模块中引入了async,所以现在我们需要在sidebar助手中也引入它。这是因为我们的Comments助手现在真的是异步的,所以任何使用我们的Comments模块的东西都需要处理这一点。就我们目前的sidebar模块而言,它只是期望comments助手模块立即返回一个数组;因此,它并不期望必须等待实际数据。因此,如果我们按照现在的代码运行,我们的comments侧边栏将保持空白(因为侧边栏在comments模块内的 MongoDB 调用甚至完成之前就已经渲染了页面)。让我们通过更新我们的sidebar助手模块来解决这个问题,以便也使用async

首先,让我们编辑helpers/sidebar.js文件,并用稍微修改过的版本替换其整个内容,该版本使用了async.parallel

const Stats = require('./stats'), Images = require('./images'), Comments = require('./comments'), async = require('async');module.exports = (viewModel, callback) => { async.parallel([ (next) => { next(null, Stats()); }, (next) => { next(null, Images.popular()); }, (next) => { Comments.newest(next); } ], (err, results) => { viewModel.sidebar = { stats: results[0], popular: results[1], comments: results[2] }; callback(viewModel); });};

我们做的第一件事是确保在文件顶部包含async作为必需的模块。在主要的exports函数内部,我们基本上包装了现有的代码,并将其集成到async.parallel中,以便稍后可以轻松地对其进行调整,因为我们更新了sidebar助手的每个部分。由于我们目前只完成了comments助手模块,所以只有这个模块实际上已经被更改了。其他的StatsImages.popular调用被强制使用async.parallel,即使现在这样做并不太合理。一旦这两个部分在下一节变得更加异步,这样做就会有意义。

asyncparallel函数的工作方式与我们之前使用的each函数类似。主要区别在于parallel不是通过集合循环执行相同的函数,而是同时执行一系列独特的函数。如果仔细观察,可以看到parallel的第一个参数实际上是一个数组,数组中的每个项都是一个独特的函数。数组中的每个函数都接受一个next回调参数函数,在每个函数结束时执行。next回调的第二个参数是函数本身内部执行的工作结果。在StatsImages.popular的情况下,这两个函数只是立即返回值,没有异步调用其他任何东西,因此我们只是期望通过直接执行它们来返回结果。

但是,正如您在Comments.newest部分中所看到的,我们将next回调函数作为参数传递,因为我们希望其执行被推迟到最后一秒(直到Comments.newest完成所有工作)。一旦调用了next回调函数,它就会传递所有工作的结果。

parallel函数的最后一个参数是一个内联函数,它接受一个结果数组作为其第二个参数。这个数组是从第一个参数中的每个函数返回的结果的集合。您可以看到,当我们现在构建viewModel时,我们是在results数组中引用索引。索引顺序是原始数组中定义的函数的顺序。我们知道第一个函数是检索Stats,第二个函数是检索Images.popular,第三个函数是检索Comments.newest。因此,我们可以可靠地将results[0]分配给viewModel.Stats,依此类推。作为参考,这是sidebar模块中viewModel定义的原始样子:

viewModel.sidebar = { stats: Stats(), popular: Images.popular(), comments: Comments.newest()}; 

您可以将其与使用async的更新版本进行比较:

viewModel.sidebar = { stats: results[0], popular: results[1], comments: results[2]}; 

现在侧边栏已经设置好,可以正确处理辅助模块(以及最终将是)异步的,我们可以运行应用程序并测试它,以确保我们的侧边栏正确显示网站上最近五条评论。运行应用程序并在浏览器中启动它。如果您还没有对图像发布任何评论,请立即这样做,以便您可以在侧边栏中看到这些评论以及它们所属的图像的缩略图。

到目前为止,我们已经涵盖并实施了大量对我们应用程序的更改。可以理解,您可能会遇到一些问题,因此让我们快速检查一下,确保我们没有漏掉任何可能阻止您的应用程序正常运行的问题:

  • 确保您已安装本章所需的所有模块,并将它们保存到您的package.json文件中。这包括mongooseasyncmd5

  • 确保每个模块文件的顶部都要求适当的依赖模块。

  • 确保每当运行应用程序时,在另一个终端实例中启动mongod

  • 如果有疑问,当 Node 在终端中失败时,注意堆栈跟踪输出,通常很明显出了什么问题。

它还会给出错误模块的文件名和行号。

  • 当一切都失败时,到处执行console.log

接下来,让我们也更新stats辅助模块,以便使用它并行,这样我们就可以为应用程序获取一些真实的统计数据。

stats辅助模块的主要责任是为我们的应用程序收集一些总数。这些stats是关于上传的图片总数、评论总数、所有图片的总浏览量和所有图片的总点赞数等。你可能首先会认为我们将查询 MongoDB 以获取所有图片,并循环遍历每张图片以跟踪所有的浏览量和总数。这是一种方法,但效率很低。幸运的是,MongoDB 有一些内置功能,可以轻松生成这些类型的值。

由于我们将要对 MongoDB 进行多次调用,我们将依赖

async.parallel函数中,就像我们在sidebar模块中所做的那样。原始的helpers/stats.js文件非常简单,所以让我们完全用这个使用parallel的新版本替换该文件:

const models = require('../models'), async = require('async');module.exports = (callback) => { async.parallel([ (next) => { next(null, 0); }, (next) => { next(null, 0); }, (next) => { next(null, 0); }, (next) => { next(null, 0); } ], (err, results) => { callback(null, { images: results[0], comments: results[1], views: results[2], likes: results[3] }); });}; 

这段代码完全做了模块最初的事情,只是多了一点冗长!我很确定我们不想永远只返回0作为我们所有统计数据,因为那将是相当无用和令人印象深刻的,至少可以这么说!让我们更新each函数,正确地查询 MongoDB 并获取一些统计数据。查看最后一个函数中回调中返回的对象,我们可以看到我们已经定义了并行执行的函数的顺序。让我们从图片开始。将第一个函数中的next(null, 0);行替换为以下代码片段:

models.Image.count({}, next); 

简单!只需使用 MongoDB 的count方法找到与任何条件匹配的图片集合中文档的总数(第一个参数)。然后,我们只需将next函数作为callback传递,因为巧合的是,参数签名是匹配的。如果我们不想在这里使用简写,我们可以以长方式编写,如下所示:

models.Image.count({}, (err, total) => { next(err, total);}); 

然而,当你不必要的时候,谁会想要输入所有这些!让我们对并行数组中的第二个函数做同样的事情,用于总评论。将第二个函数中的next(null, 0);行替换为以下代码行:

models.Comment.count({}, next); 

再次,这真是小菜一碟!

现在,接下来的两个函数将有些不同,但它们几乎是相同的。我们想要用next获取每张图片的总viewslikes。我们不能使用 MongoDB 的count方法,因为它只计算集合中的单个文档。我们需要使用 MongoDB 的aggregate功能。

使用aggregate,我们可以执行数学运算,比如$sum,来为我们计算结果。将第三个函数中的next(null, 0);行替换为以下代码片段:

models.Image.aggregate({ $group: { _id: '1', viewsTotal: { $sum: '$views' } }}, (err, result) => { var viewsTotal = 0; if (result.length > 0) { viewsTotal += result[0].viewsTotal; } next(null, viewsTotal);}); 

使用 MongoDB 的aggregate函数,我们告诉 MongoDB 将每个文档分组在一起,并将它们的所有视图总和到一个名为viewsTotal的新字段中。返回给callback函数的结果集合是一个具有_idviewsTotal字段的文档数组。在这种情况下,结果数组将只包含一个具有总数的文档,因为我们在aggregate功能中并不那么巧妙。如果集合中根本没有图片,我们需要处理并相应地进行检查。最后,使用实际的viewsTotal值调用next回调函数。

让我们使用完全相同的功能来统计所有图片的likes。将并行中的第四个和最后一个函数中的next(null, 0)行代码替换为以下代码片段:

models.Image.aggregate({ $group: { _id: '1', likesTotal: { $sum: '$likes' } }}, (err, result) => { var likesTotal = 0; if (result.length > 0) { likesTotal += result[0].likesTotal; } next(null, likesTotal);});

现在sidebar辅助模块已经更新,并且完全具有async.parallel功能,让我们对sidebar模块进行微小调整,以确保我们正确调用Stats模块,以便它正确地异步执行。helpers/sidebar.js中的原始代码行是:

next(null, Stats()); 

用这个稍微不同的版本替换那行代码:

Stats(next); 

最后但并非最不重要的是,让我们来处理图像侧边栏的最受欢迎的辅助模块。

同样,原始的helpers/images.js文件大部分都是填充了固定数据和相当无用的占位符代码。让我们用这个实际上相当温和的新版本替换整个文件,与所有其他辅助模块相比。

var models = require('../models');module.exports = { popular: (callback) => { models.Image.find({}, {}, { limit: 9, sort: { likes: -1 } }, (err, images) => { if (err) throw err; callback(null, images); }); }};

到目前为止,这段代码对你来说应该很熟悉。我们只是查询 MongoDB,并通过按总数对图像进行排序,例如按降序计数,然后将结果限制为九个文档,找到了最受欢迎的九张图像。

让我们再次编辑helpers/sidebar.js文件,以包括对Images.popular函数的更新调用。考虑原始代码:

next(null, Images.popular()); 

用以下稍微更新的版本替换这个:

Images.popular(callback);

现在侧边栏已经完全完成并且完全动态。没有任何固定数据或占位符变量。运行应用程序应该产生一个功能齐全的网站,所有我们要实现的功能都完美地运行!试一试,确保它正常工作。

到目前为止,我认为我们的应用程序非常棒,但有一些东西让我感到不满。在测试期间,我一直在创建各种新图像并将它们上传到应用程序,但现在开始变得有点混乱和凌乱。

我意识到最明显的缺失是删除图像的能力!

实际上,我故意省略了这个功能,这样我们就可以利用这个机会来整合一个完全新的功能,几乎触及应用程序的每个领域。这个看似简单的添加实际上需要以下更改:

  • 更新routes.js以包括处理Delete请求的新路由

  • 更新controllers/image.js以包括路由的新功能

  • 这不仅应该从数据库中删除图像,还应该删除文件和所有相关评论

  • 更新image.handlebars HTML 模板以包括一个删除按钮

  • 使用 AJAX 处理程序更新public/js/scripts.js文件以处理删除按钮

为了添加这个新功能,我们需要更新的第一件事是主routes列表。在这里,我们将添加一个处理delete功能并指向image控制器内的函数的新端点。编辑server/routes.js文件并插入以下新代码行:

router.delete('/images/:image_id', image.remove);

现在我们已经添加了一个新的路由,我们需要创建它使用的控制器函数作为它的callback(image.remove)。编辑controllers/image.js并在现有的comment: function(req, res){}操作之后添加以下新的函数代码(不要忘记在comment函数之后添加一个逗号,因为你正在添加一个新的函数):

remove: (req, res) => { Models.Image.findOne({ filename: { $regex: req.params.image_id } }, (err, image) => { if (err) { throw err; } fs.unlink(path.resolve(`./public/upload/${image.filename}`), (err) => { if (err) { throw err; } Models.Comment.remove({ image_id: image._id }, (err) => { image.remove((err) => { if (!err) { res.json(true); } else { res.json(false); } }); }); }); });} 

这个函数执行四个主要功能(因此,使用callbacks嵌套了四层深--我们可以在这里使用 async 的series方法来防止疯狂的嵌套)。第一项任务是找到我们要删除的图像。一旦找到该图像,应删除与图像关联的文件。接下来,找到与图像关联的评论并删除它们。一旦它们被删除,最后一步是删除图像本身。假设所有这些都成功了,只需向浏览器发送一个true布尔 JSON 响应。

现在我们有了支持删除图像的routecontroller函数,我们需要一种方法让 UI 发送请求。最明显的解决方案是在页面的某个地方添加一个删除按钮。编辑views/image.handlebars文件,在现有的 HTML 之后,我们有了 Like 按钮,添加一个新的 HTML 用于删除按钮:

<div class="col-md-8"> <button class="btnbtn-success" id="btn-like" ... // existing HTML for Like button and misc details </div> <div class="col-md-4 text-right"> <button class="btnbtn-danger" id="btn-delete" data-id="{{ image.uniqueId }}"> <i class="fafa-times"></i> </button> </div> 

在这里,我们只包括一个新的div,它使用 Bootstrap 设置为四个右对齐的列。这里的 UI 是,喜欢按钮和统计数据是行的最左边部分,删除按钮(来自 Font Awesome 的 X 图标)位于同一行的最右边(由于我们使用 Bootstrap 的危险颜色类,所以是红色的)。

最后,我们将通过实现类似于“喜欢”按钮的代码来将所有内容联系在一起,在按钮被点击时向服务器发送带有 URL 和图像 ID 的 AJAX“删除”方法。为了安全起见,我们显示一个标准的 JavaScript 确认对话框,以确保按钮不是意外点击的。

假设服务器响应一个true值,我们将把按钮变成绿色,并将图标更改为一个带有“已删除!”字样的复选标记。编辑public/js/scripts.js并在现有代码之后插入以下代码块(确保将新代码插入到$(function(){ ... })jQuery 函数内):

$('#btn-delete').on('click', function(event) { event.preventDefault(); var $this = $(this); var remove = confirm('Are you sure you want to delete this image ? '); if (remove) { var imgId = $(this).data('id'); $.ajax({ url: '/images/' + imgId, type: 'DELETE' }).done(function(result) { if (result) { $this.removeClass('btn-danger').addClass('btn-success '); $this.find('i').removeClass('fa -times ').addClass('fa - check '); $this.append('<span> Deleted!</span>'); } }); }}); 

让我们通过启动应用程序、在浏览器中加载它、找到一个我们不再需要的图像并查看它的图像页面来测试这个全新的功能。

删除按钮现在应该显示出来了。

在这一点上,我们一直在构建的应用程序几乎完成了!在我们对项目进行任何迭代并继续构建它并使其准备投入生产之前,我们可能应该考虑一些重构和/或一般改进。我个人会看一下需要重构和/或重写以改进应用程序性能和整体健康状况的一些领域如下:

  • 我可能会重新考虑在控制器中直接与模型一起工作这么多,而是创建一个实用程序,我可以在其中包装大部分噪音,并依赖于更基本的 CRUD 调用我的模型,并仅提供一个callback。这在image控制器中最为明显,包括likecommentremove

  • 在我们编写的项目中实际上没有验证,这主要是为了简洁。实际上,我们应该在用户界面上的任何输入字段上包含验证。验证应该在前端通过 jQuery 或普通的原始 JavaScript 以及在后端通过 Node 上提供。验证应该保护用户免受提交无效和/或恶意代码(即 XSS 或跨站点脚本)的影响。

  • 目前,我们的应用程序对一般公众开放,这意味着任何访问者都可以上传图像以及删除它们!在我们的应用程序中包含用户身份验证过程将是相当简单的。Passport.js 是一个很好的第三方模块,可以将用户身份验证集成到 Node.js 应用程序中。

  • 不要附加图像到评论的目的边栏(newest评论),我们应该考虑使用 MongoDB 创建更强大的聚合查询,以从 MongoDB 直接检索包含图像的评论的混合集合。

这一章是一个庞然大物,但也是完成我们的应用程序并拥有一个完全动态、数据库驱动的 Node.js 应用程序的最后一块拼图。祝贺你走到了这一步并坚持下来!你正在成为一个真正的全栈 JavaScript 开发人员。

在下一章中,我们将暂时离开我们的应用程序,看看如何使用 Node.js 处理 REST API。

现在你的应用程序已经完成并准备好展示给世界,你可以开始考虑让它变得更受欢迎。如果你想允许外部系统以一种方式访问你的数据,使它们可以大规模地向你的网站插入数据,而不需要用户访问实际的网站呢?

一个几乎立刻想到的例子是,另一个网站的用户,比如www.facebook.com,可以上传一张图片到 Facebook,并且它会自动上传到你的网站上。

使这种情景成为可能的唯一方法是通过提供一个 API 给你的数据,并且给外部开发者访问一套工具的代码,使他们可以执行操作而不需要与实际的网页进行交互。

在这一章中,我们将回顾以下主题:

  • 介绍 RESTful API

  • 安装一些基本工具

  • 创建一个基本的 API 服务器和示例 JSON 数据

  • 响应GET请求

  • 使用POSTPUT更新数据

  • 使用DELETE删除数据

  • 从 Node.js 消费外部 API

应用程序编程接口API)是一个系统提供的一组工具,使不相关的系统或软件有能力相互交互。通常,当开发人员编写将与封闭的外部软件系统交互的软件时,他们会使用 API。外部软件系统提供 API 作为所有开发人员可以使用的一套标准工具。许多流行的社交网络网站提供开发人员访问 API 的权限,以构建支持这些网站的工具。最明显的例子是 Facebook 和 Twitter。它们都有一个强大的 API,为开发人员提供了直接处理数据和构建插件的能力,而不需要被授予完全访问权限,作为一般的安全预防措施。

正如你在本章中所看到的,提供自己的 API 不仅相当简单,而且还赋予你提供用户访问你的数据的权力。你还可以放心地知道,你完全控制着你可以授予的访问级别,你可以使哪些数据集只读,以及可以插入和更新哪些数据。

表述性状态转移REST)是一种通过 HTTP 进行 CRUD 的花哨方式。这意味着,当你使用 REST API 时,你有一种统一的方式,使用简单的 HTTP URL 和一组标准的 HTTP 动词来创建、读取和更新数据。REST API 的最基本形式将在 URL 上接受 HTTP 动词之一,并作为响应返回某种数据。

通常,REST API 的GET请求总是会返回某种数据,比如 JSON、XML、HTML 或纯文本。对 RESTful API URL 的POSTPUT请求将接受数据以创建或更新。RESTful API 的 URL 被称为端点,当使用这些端点时,通常说你在消费它们。在与 REST API 交互时使用的标准 HTTP 动词包括:

  • GET:这是检索数据

  • POST:这是提交新记录的数据

  • PUT:这是提交数据以更新现有记录

  • PATCH:这是提交日期以更新现有记录的特定部分

  • DELETE:这会删除特定记录

通常,RESTful API 端点以一种模仿数据模型并具有语义 URL 的方式进行定义。这意味着,例如,要请求模型列表,你将访问/models的 API 端点。同样,要通过其 ID 检索特定模型,你将在端点 URL 中包含它,如/models/:Id

一些示例 RESTful API 端点 URL 如下:

  • GET http://myapi.com/v1/accounts:这将返回一个账户列表

  • GET http://myapi.com/v1/accounts/1:这将返回一个单一账户

通过Id: 1

  • POST http://myapi.com/v1/accounts:这将创建一个新账户

(数据作为请求的一部分提交)

  • PUT http://myapi.com/v1/accounts/1: 这将更新现有的帐户

通过Id: 1提交的帐户(作为请求的一部分提交的数据)

  • GET http://myapi.com/v1/accounts/1/orders: 这将返回帐户Id: 1的订单列表

  • GET http://myapi.com/v1/accounts/1/orders/21345: 这将返回帐户Id: 1的单个订单的详细信息,订单Id: 21345

URL 端点匹配此模式并不是必需的;这只是常见的约定。

在开始之前,有一些工具可以使您在直接使用 API 时更加轻松。其中一个工具就是称为 Postman REST Client 的工具,它是一个可以直接在浏览器中运行或作为独立的打包应用程序运行的 Google Chrome 应用程序。使用此工具,您可以轻松地向任何您想要的端点发出任何类型的请求。该工具提供了许多有用且强大的功能,非常易于使用,而且最重要的是,免费!

Postman REST Client 可以以两种不同的方式安装,但都需要安装并在您的系统上运行 Google Chrome。安装该应用程序的最简单方法是访问 Chrome 网络商店chrome.google.com/webstore/category/apps

搜索 Postman REST Client,将返回多个结果。有常规的 Postman REST Client,它作为内置到浏览器中的应用程序运行,还有一个单独的 Postman REST Client(打包应用程序),它作为独立应用程序在您的系统中运行,并在自己的专用窗口中运行。继续安装您的首选项。如果您将应用程序安装为独立的打包应用程序,将会在您的停靠栏或任务栏上添加一个启动图标。如果您将其安装为常规浏览器应用程序,可以通过在 Google Chrome 中打开一个新标签页,转到应用程序,并找到 Postman REST Client 图标来启动它。

安装并启动应用程序后,您应该看到类似以下截图的输出:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (18)

使用 Postman REST Client,我们能够向任何我们想要的端点提交 REST API 调用,以及修改请求的类型。然后,我们可以完全访问从 API 返回的数据,以及可能发生的任何错误。要测试 API 调用,请在“在此输入请求 URL”字段中输入您最喜欢的网站的 URL,并将其旁边的下拉菜单保留为GET。这将模仿您访问网站时浏览器执行的标准GET请求。单击蓝色的发送按钮。请求被发送,并且响应显示在屏幕的下半部分。

在下面的截图中,我向kroltech.com发送了一个简单的GET请求,并返回了 HTML。

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (19)

如果我们将此 URL 更改为我的网站的 RSS 源 URL,您可以看到返回的 XML:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (20)

XML 视图具有更多功能,因为它公开了右侧的侧边栏,让您一览 XML 数据的树结构。不仅如此,您现在还可以看到我们迄今为止所做的请求的历史记录,沿左侧边栏。当我们执行更高级的POSTPUT请求并且不想在测试端点时重复数据设置时,这将非常有用。

这是一个示例 API 端点,我向其提交了一个GET请求,返回其响应中的 JSON 数据:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (21)

使用 Postman Client 向返回 JSON 的端点发出 API 调用的一个非常好的功能是,它以非常好的格式解析和显示 JSON,并且数据中的每个节点都是可展开和可折叠的。

这个应用程序非常直观,所以确保你花一些时间玩耍和尝试不同类型的调用不同的 URL。

还有一个工具我想和你谈谈(虽然非常小),但实际上是一个非常重要的事情。JSONView 谷歌浏览器扩展程序是一个非常小的插件,它将立即通过浏览器将任何 JSONView 直接转换为更可用的 JSON 树(就像在 Postman 客户端中一样)。这是在安装 JSONView 之前指向返回 JSON 的 URL 的示例:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (22)

在安装了 JSONView 之后,这就是相同的 URL:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (23)

你应该像安装 Postman REST Client 一样安装 JSONView 谷歌浏览器扩展程序:访问 Chrome 网上应用店,搜索 JSONView

现在你已经有了能够轻松处理和测试 API 端点的工具,让我们来看看如何编写自己的端点并处理不同的请求类型。

让我们使用 Express 创建一个超级基本的 Node.js 服务器,我们将使用它来创建我们自己的 API。然后,我们可以使用 Postman REST Client 发送测试到 API,看看它是如何工作的。在一个新的项目工作空间中,首先安装我们需要的 npm 模块,以便让我们的服务器运行起来:

 $ npm init $ npm install --save express body-parser underscore

现在,这个项目的 package.json 文件已经初始化并安装了模块,让我们创建一个基本的服务器文件来引导 Express 服务器。创建一个名为 server.js 的文件,并插入以下代码块:

const express = require('express'), bodyParser = require('body-parser'), _ = require('underscore'), json = require('./movies.json'), app = express(); app.set('port', process.env.PORT || 3500); app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.json()); let router = new express.Router(); // TO DO: Setup endpoints ... app.use('/', router); const server = app.listen(app.get('port'), ()=>{ console.log(`Server up: http://localhost:${app.get('port')}`); }); 

这对你来说应该看起来很熟悉。在 server.js 文件中,我们引入了 express、body-parser 和 underscore 模块。我们还引入了一个名为 movies.json 的文件,接下来我们将创建它。

在我们引入模块之后,我们使用最少量的配置来设置 Express 服务器的标准配置,以支持 API 服务器所需的最少配置。请注意,我们没有设置 Handlebars 作为视图渲染引擎,因为我们不打算使用这个服务器来渲染任何 HTML,只是纯粹的 JSON 响应。

让我们创建一个临时数据存储的示例 movies.json 文件(尽管我们为了演示目的构建的 API 实际上不会在应用程序的生命周期之外保留数据):

[{ "Id": "1", "Title": "Aliens", "Director": "James Cameron", "Year": "1986", "Rating": "8.5" }, { "Id": "2", "Title": "Big Trouble in Little China", "Director": "John Carpenter", "Year": "1986", "Rating": "7.3" }, { "Id": "3", "Title": "Killer Klowns from Outer Space", "Director": "Stephen Chiodo", "Year": "1988", "Rating": "6.0" }, { "Id": "4", "Title": "Heat", "Director": "Michael Mann", "Year": "1995", "Rating": "8.3" }, { "Id": "5", "Title": "The Raid: Redemption", "Director": "Gareth Evans", "Year": "2011", "Rating": "7.6" }] 

这只是一个非常简单的 JSON 电影列表。随意用你喜欢的内容填充它。启动服务器以确保你没有收到任何错误(请注意,我们还没有设置任何路由,所以如果你尝试通过浏览器加载它,它实际上不会做任何事情):

 $ node server.js Server up: http://localhost:3500

添加简单的 GET 请求支持非常简单,你已经在我们构建的应用程序中见过这个。这是一些响应 GET 请求并返回简单 JavaScript 对象作为 JSON 的示例代码。在我们有 // TO DO: Setup endpoints ... 注释等待的 routes 部分插入以下代码:

router.get('/test', (req, res)=>{ var data = { name: 'Jason Krol', website: 'http://kroltech.com' }; res.json(data); }); 

就像我们在第五章中设置了 viewModel 一样,使用 Handlebars 进行模板化,我们创建一个基本的 JavaScript 对象,然后可以直接使用 res.json 发送作为 JSON 响应,而不是 res.render。让我们稍微调整一下这个函数,并将它更改为响应根 URL(即 /)路由的 GET 请求,并从我们的 movies 文件返回 JSON 数据。在之前添加的 /test 路由之后添加这个新路由:

router.get('/', (req, res)=>res.json(json)); 

在 Express 中,res(响应)对象有一些不同的方法来将数据发送回浏览器。这些方法最终都会回退到基本的send方法,其中包括header信息,statusCodes等。res.jsonres.jsonp将自动将 JavaScript 对象格式化为 JSON,然后使用res.send发送它们。res.render将以字符串形式呈现模板视图,然后也使用res.send发送它。

有了这段代码,如果我们启动server.js文件,服务器将监听/URL 路由的GET请求,并响应我们电影集合的 JSON 数据。让我们首先使用 Postman REST 客户端工具进行测试:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (24)

GET请求很好,因为我们可以很容易地通过浏览器拉取相同的 URL 并获得相同的结果:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (25)

然而,我们将使用 Postman 进行剩余的端点测试,因为使用浏览器发送POSTPUT请求有点困难。

当我们希望允许使用我们的 API 插入或更新数据时,我们需要接受来自不同 HTTP 动词的请求。在插入新数据时,POST动词是接受数据并知道它是用于插入的首选方法。让我们看一下接受POST请求和数据的代码,将记录插入到我们的集合中,并返回更新的 JSON。

在之前为GET添加的路由之后插入以下代码块:

router.post('/', (req, res)=>{ // insert the new item into the collection if(req.body.Id && req.body.Title && req.body.Director && req.body.Year && req.body.Rating) { json.push(req.body); res.json(json); } else { res.json(500, { error: 'There was an error!' }); } }); 

POST函数中,我们首先要做的是检查确保所需字段与实际请求一起提交。假设我们的数据检查通过,并且所有必需字段都被考虑在内(在我们的情况下,每个字段),我们将整个req.body对象按原样插入数组中,使用数组的push函数。如果请求中没有提交任何必需字段,我们将返回一个 500 错误消息。让我们使用 Postman REST 客户端向相同的端点提交一个POST请求。(不要忘记确保你的 API 服务器正在使用 node server.js运行。):

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (26)

首先,我们提交了一个没有数据的POST请求,所以你可以清楚地看到返回的 500 错误响应:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (27)

接下来,我们在 Postman 中使用x-www-form-urlencoded选项提供了实际数据,并提供了每个名称/值对的一些新的自定义数据。你可以从结果中看到状态是 200,这是成功的,并且更新的 JSON 数据作为结果返回。在浏览器中重新加载主GET端点,可以看到我们原始的电影集合中添加了新的电影:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (28)

PUT请求几乎以完全相同的方式工作,除了传统上,数据的Id属性处理方式有点不同。在我们的例子中,我们将要求Id属性作为 URL 的一部分,并且不接受它作为提交的数据参数(因为通常update函数不会改变正在更新的对象的实际Id)。在之前添加的POST路由之后,插入以下代码用于PUT路由:

router.put('/:id', (req, res)=>{ // update the item in the collection if(req.params.id && req.body.Title && req.body.Director && req.body.Year && req.body.Rating) { _.each(json, (elem, index)=>{ // find and update: if (elem.Id === req.params.id) { elem.Title = req.body.Title; elem.Director = req.body.Director; elem.Year = req.body.Year; elem.Rating = req.body.Rating; } }); res.json(json); } else { res.json(500, { error: 'There was an error!' }); } }); 

这段代码再次验证了提交的数据中是否包含所需的字段。然后,它执行一个_.each循环(使用underscore模块)来查看电影集合,并找到其Id参数与 URL 参数中的Id匹配的项目。假设有匹配项,那么相应对象的个别字段将使用请求中发送的新值进行更新。一旦循环完成,更新后的 JSON 数据将作为响应发送回来。同样,在POST请求中,如果缺少任何必需的字段,将返回一个简单的 500 错误消息。以下截图展示了成功的PUT请求更新现有记录:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (29)

在 Postman 的响应中,包括将值1作为Id参数放入 URL 中,作为x-www-form-urlencoded值提供要更新的个别字段,最后作为PUT发送,显示我们电影集合中的原始项目现在是原始的 Alien(而不是 Aliens,它的续集,正如我们最初的情况)。

我们在不同的 REST API HTTP 动词的旋风之旅中的最后一站是DELETE。发送DELETE请求应该做的事情应该不会让人感到意外。让我们添加另一个接受DELETE请求并从我们的电影集合中删除项目的路由。以下是处理DELETE请求的代码,应该放在先前PUT的现有代码块之后:

router.delete('/:id', (req, res)=>{ let indexToDel = -1; _.each(json, (elem, index)=>{ if (elem.Id === req.params.id) { indexToDel = index; } }); if (~indexToDel) { json.splice(indexToDel, 1); } res.json(json); }); 

这段代码将循环遍历电影集合,并通过比较Id的值找到匹配的项目。如果找到匹配项,匹配项目的数组index将保持,直到循环结束。使用array.splice函数,我们可以删除特定索引处的数组项。一旦通过删除请求的项目更新了数据,JSON 数据将被返回。请注意,在以下截图中,返回的更新后的 JSON 实际上不再显示我们删除的原始第二项:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (30)

JavaScript 中的~使用!这是一点点 JavaScript 黑魔法!在 JavaScript 中,波浪号(~)将对一个值进行位翻转。换句话说,它将取一个值并返回该值的负值加一,即~n === -(n+1)。通常,波浪号与返回-1作为假响应的函数一起使用。通过在-1上使用~,您将其转换为0。如果您在 JavaScript 中对-1执行布尔检查,它将返回true。您会发现~主要与indexOf函数和jQuery$.inArray()一起使用;两者都将-1作为false响应。

本章中定义的所有端点都非常基础,其中大多数在生产环境中不应该出现!每当您有一个接受除GET请求以外的任何内容的 API 时,您都需要确保执行非常严格的验证和身份验证规则。毕竟,您基本上是直接向用户提供对您数据的访问权限。

毫无疑问,总有一天您想要直接从 Node.js 代码中使用 API。也许您自己的 API 端点需要首先从某个与之无关的第三方 API 中获取数据,然后再发送响应。无论原因是什么,通过使用一个名为request的流行和知名的npm模块,可以相对容易地发送请求到外部 API 端点并接收响应。request模块是由 Mikeal Rogers 编写的,目前是第三受欢迎(也是最可靠的)npm模块,仅次于asyncunderscore

请求基本上是一个超级简单的 HTTP 客户端,所以到目前为止你用 Postman REST Client 所做的一切基本上都是Request可以做的,只是结果数据在你的 Node 代码中可用,以及响应状态码和/或错误(如果有的话)。

让我们做一个巧妙的技巧,实际上消耗我们自己的端点,就好像它是某个第三方外部 API 一样。首先,我们需要确保我们已经安装了request并且可以在我们的应用程序中包含它:

 $ npm install --save request

接下来,编辑server.js,确保你包含request作为一个必需的模块

在文件的开头:

const express = require('express'), bodyParser = require('body-parser'), _ = require('underscore'), json = require('./movies.json'), app = express(), request = require('request'); 

现在,让我们在现有路由之后添加一个新的端点,这将是通过对/external-api发出GET请求在我们的服务器中可访问的一个端点。然而,这个端点实际上将消耗另一个服务器上的另一个端点,但是出于这个例子的目的,另一个服务器实际上是我们当前正在运行的相同服务器!

request模块接受一个带有许多不同参数和设置的选项对象,但对于这个特定的例子,我们只关心其中的一些。我们将传递一个具有我们要消耗的端点的 URL 设置的对象。在发出请求并收到响应后,我们希望执行一个内联的callback函数。

server.js中现有的routes列表之后放置以下代码块:

router.get('/external-api', (req, res)=>{ request.get({ uri: `http://localhost:${(process.env.PORT || 3500)}` }, (error, response, body)=>{ if (error) { throw error; } var movies = []; _.each(JSON.parse(body), (elem, index)=>{ movies.push({ Title: elem.Title, Rating: elem.Rating }); }); res.json(_.sortBy(movies, 'Rating').reverse()); }); }); 

callback函数接受三个参数:errorresponsebodyresponse对象就像 Express 处理的任何其他响应一样,具有各种参数。第三个参数body是我们真正感兴趣的。它将包含我们调用的端点请求的实际结果。在这种情况下,它是我们之前定义的主GET路由返回的 JSON 数据,其中包含我们自己的电影列表。重要的是要注意,从请求返回的数据是作为字符串返回的。我们需要使用JSON.parse将该字符串转换为实际可用的 JSON 数据。

我们操纵了从请求返回的数据以满足我们的需求。在这个例子中,我们拿到了电影的主列表,只返回了一个由每部电影的TitleRating组成的新集合,并按照最高分数对结果进行排序。

通过将浏览器指向http://localhost:3500/external-api来加载这个新的端点,你可以看到新转换的 JSON 输出显示在屏幕上。

让我们看一个更真实的例子。假设我们想为我们收藏中的每部电影显示一系列相似的电影,但我们想在www.imdb.com等地方查找这些数据。下面是一个示例代码,它将向 IMDB 的 JSON API 发送一个GET请求,特别是针对单词aliens,并返回按TitleYear列出的相关电影。继续在external-api的先前路由之后放置这个代码块:

router.get('/imdb', (req, res)=>{ //console.log("err1") request.get({ uri: 'http://sg.media-imdb.com/suggests/a/aliens.json' }, (err, response, body)=>{ let data = body.substring(body.indexOf('(')+1); data = JSON.parse(data.substring(0,data.length-1)); let related = []; _.each(data.d, (movie, index)=>{ related.push({ Title: movie.l, Year: movie.y, Poster: movie.i ? movie.i[0] : '' }); }); res.json(related); }); }); 

如果我们在浏览器中查看这个新的端点,我们可以看到从我们的/imdb端点返回的 JSON 数据实际上是从一些其他 API 端点检索和返回数据:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (31)

我正在使用的 IMDB 的 JSON 端点实际上并不是来自他们的 API,而是当你在主搜索框中输入时他们在主页上使用的。这实际上并不是使用他们的数据的最合适的方式,但更多的是一个示例。实际上,要使用他们的 API(就像大多数其他 API 一样),你需要注册并获取一个 API 密钥,这样他们才能正确地跟踪你每天或每小时请求了多少数据。出于这个原因,大多数 API 都需要你使用私钥。

在本章中,我们简要介绍了 API 的一般工作原理,语义 URL 路径和参数的 RESTful API 方法,以及如何创建一个简单的 API。

我们使用 Postman REST Client 与 API 交互,通过消耗端点和测试不同类型的请求方法(GETPOSTPUT等)来进行测试。您还学会了如何使用第三方节点模块request来消耗外部 API 端点。

在下一章中,我们将重新访问我们的原始应用程序,通过在 Node.js 中引入测试来实施最佳实践。我们将研究流行的测试框架,并为应用程序编写测试,以证明我们的代码按预期工作。

到目前为止,我们在编写代码时基本上是凭着感觉在进行。我们实际上无法知道代码是否有效,直到在实际浏览器中测试它。

在本章中,我们将涵盖以下主题:

  • 使用 Mocha 测试框架运行测试

  • 使用 Chai.js 断言库编写测试

  • 使用 Sinon 和 Proxyquire 进行间谍和存根

  • 编写您的第一个测试

  • 测试您的应用程序

测试对于确保您的代码正常运行非常重要,但它们也非常适用于防止由于您对某些无辜的代码进行了微小更改而突然出现新的意外错误。

让我们首先看一下我们将用于运行和编写测试的各种工具和库。在我们实际开始编写真正的测试之前,我们需要掌握三个主要概念。

第一个是测试运行器,或者我们用来运行测试套件的框架。

大多数框架都遵循测试驱动开发TDD),其过程依赖以下步骤:

  1. 它定义了一个单元测试。

  2. 它实现了该单元。

  3. 它执行测试并验证测试是否通过。

第二个概念是断言库本身——我们用来编写测试的语言。使用断言语言的特殊版本来逐步设计和构建功能块,以期望的行为为指导,称为行为驱动开发BDD)。

对于 TDD 和 BDD,我们可以使用 Mocha 测试框架;但是,我们将使用一个名为Chai.js的特殊断言库来编写断言。

最后,我们将看一下间谍和存根的概念,它们是我们代码的某些部分的虚假代表,当我们需要跟踪函数调用以确保预期的行为时,我们会依赖它们。

在为应用程序编写测试时,通常会按模块特定的批次编写它们。这些批次被称为套件或规范。每个套件通常包含一批以几乎与应用程序本身相似的方式组织的测试。对于 Node,这个想法也是一样的,我们编写的每个测试套件都将针对一个单独的模块。您将需要测试的模块,并为模块的每个部分编写一系列测试。

由于您将有许多不同的测试文件来测试应用程序的每个组件,您需要一种快速执行所有测试的方法。这就是测试运行器的作用。我们决定使用的测试运行器称为 Mocha。您可以像安装其他npm包一样全局安装 Mocha,如下所示:

 $ npm install -g mocha

在 Linux 或 OS X 上安装时可能需要安全权限,可以简单地在npm之前使用sudo

一旦安装完成,Mocha 命令行工具就可以使用了。只需在命令行中执行mocha,就会使用一些默认选项执行测试运行。

测试运行器将查找名为test的文件夹和其中的任何.js文件。在我们的情况下,我们实际上还没有设置任何测试,因此仅执行mocha将不会有任何效果;相反,它会抛出以下错误:

 cannot resolve path

当 Mocha 测试运行器找到.js文件时,它会像执行任何其他 Node 文件一样执行它们,只是它会在文件中查找一些特定的关键字。

以下是典型测试块的一些示例代码:

const expect = require('chai').expect; describe('The code', ()=>{ beforeEach(()=>{ // optional preparation for each test }); afterEach(()=>{ // optional cleanup after each test }); it('should test something', ()=>{ const something = 1; // here we "expect" some condition to declare our test // in this case, we expect the variable to exist // more on the assertion syntax a little later expect(something).to.exist; }); it('should test something_else', ()=>{ const something_else = false; // now we test a different variable against its value // and expect that value to equal false expect(something_else).to.equal(false); }); }); 

Mocha 首先要扫描文件的是describe块。describe块是定义单行中特定测试用例组的一种方式。您可以在test文件中有许多describe块,并且每个describe块可以有许多具体测试。此外,describe块可以嵌套得很深,以更好地组织您的测试。

一旦找到一个describe块,其中还会执行一些其他项目。会检查beforeEachafterEach块,看是否有任何需要在每次测试执行之前执行的预测试工作。同样,在测试之间需要进行任何清理工作也可以在afterEach块中处理。

这两个块都是可选的,因此不是必需的。如果您需要实例化一个对象进行测试,您可以使用beforeEach块。这样,无论测试可能对对象进行了什么更改,都将被重置,并且不会无意中影响任何其他测试。同样,您在测试期间对任何其他相关对象所做的任何更改都可以在afterEach块中重置。

describe块内,使用it语句定义单独的测试。在每个it语句中,通常认为包括一个单独的expect来断言实际测试(尽管您可以包括尽可能多的expect函数调用,但由于只有一个it,它仍然被认为是单个测试)。

在编写测试套件时,我们使用 BDD 风格的语法,这样我们的测试就可以像用户故事一样阅读。使用前面的测试片段,您可以将测试读作代码应该测试某事代码应该测试其他事情。实际上,如果我们运行前面的测试,我们会看到以下输出:

 The code should test something should test something_else 2 passing (5ms)

正如您在前面的单元测试示例中看到的,我们使用特殊块来定义我们的测试组,但在定义实际的单独测试时使用了不同的语言。这些测试被称为断言,我们选择使用Chai.js库。这纯粹是个人偏好,因为存在许多不同的断言库。每个库基本上都是做同样的事情,只是在编写测试的语法和风格上略有不同。由于Chai.js是项目特定的并且基于个人偏好,我们将其安装为项目依赖项(而不是全局安装)。此外,由于我们的测试实际上并不是应用程序运行所必需的,我们将在package.json文件的devDependencies列表中包含Chai.js

在开发者的机器上执行npm install将会将所有包安装到正常的依赖项下,以及package.json中的devDependencies。当环境变为生产环境时,我们需要执行npm install --prod来指定环境。

这将帮助npmpackage.json中将包安装到依赖项而不是devDependencies下。为了将Chai.js作为devDependency包含在我们的项目中,我们将在执行npm安装时使用--save-dev标志而不是--save

 $ npm install --save-dev chai

Chai 本身有几种不同风格的 API 可以在编写测试时使用。我们将使用 BDD API 来编写测试,它使用expectshould。还有一个更多的 TDD 风格的 assert API。使用expect/should的 BDD 风格的好处是可以链式调用断言方法来提高测试的可读性。

您可以通过访问以下维基百科页面了解更多关于 BDD 和 TDD 的信息:

en.wikipedia.org/wiki/Behavior-driven_development

使用Chai.js的 BDD 断言 API 提供了许多方法,比如tobeis等等。它们没有测试能力,但可以提高断言的可读性。所有的获取器都列在chaijs.com/api/bdd/上。

所有这些获取器都将遵循一个expect()语句,并且可以与not结合,以便在需要时将断言取反。

前面的获取器与chai断言方法相结合,比如okequalwithin等,以确定测试的结果。所有这些方法都列在chaijs.com/api/assert/中。

让我们开始构建简单的断言。chai提供了三种不同的断言风格:expectshouldassert。考虑以下简单的例子:

const chai = require('chai'); const expect = chai.expect; const should = chai.should(); const assert = chai.assert; const animals = { pets: [ 'dog', 'cat', 'mouse' ] }; const foo = 'bar'; expect(foo).to.be.a('string').and.equal('bar'); expect(animals).to.have.property('pets').with.length(4); animals.should.have.property('pets').with.length(4); assert.equal(foo, 'bar', 'Foo equal bar'); 

正如你所看到的,expect/should函数是基于自描述语言链的。两者在声明方式上有所不同——expect函数提供了链的起点,而should接口则扩展了Object.prototype

assert接口提供了简单但强大的 TDD 风格断言。除了前面的例子产生的深度相等断言,还有异常测试和实例可用。要进行更深入的学习,请参考 Chai 文档chaijs.com/api

如果没有一种简单的方法来监视函数并知道它们何时被调用,测试代码将变得非常困难。此外,当调用你的函数之一时,知道传递给它的参数和返回的内容也是很好的。在测试中,spy是一个特殊的占位符函数,当你想要检查特定的函数是否/何时被调用时,它会替换现有的函数。当调用函数时,间谍会跟踪一些属性,并且它们还可以通过原始函数的预期功能。Sinon.js库提供了spystub功能,并且非常全面。要了解这个强大框架提供的不同选项的完整列表,我强烈建议你花一些时间阅读文档sinonjs.org/docs

由于我们将在测试中使用Sinon.js,我们应该将其安装为另一个devDependency,与我们使用Chai.js时完全相同。此外,我们还应该安装sinon-chai助手,它提供了额外的chai断言动词,专门用于与 Sinon 一起使用:

 $ npm install --save-dev sinon sinon-chai

包含sinon-chai允许我们编写特殊的断言,比如to.be.calledWith,这在仅使用chai时是无法使用的。

想象一下,你有一个简单地将两个数字相加并返回总和的函数:

let sum = (a, b) => { return a + b;}let doWork = () => { console.log("asdasd") const x = 1, y = 2; console.log(sum(x, y));} 

在为doWork函数编写测试时,我们希望断言sum函数是否被调用。我们并不一定关心函数做什么,或者它是否起作用;我们只是想确保——因为doWork依赖于sum——它实际上调用了function()函数。在这种情况下,我们唯一能确定的方式是如果我们有一种方法来监视sum函数并知道它是否被调用。使用spy,我们可以做到这一点:

const chai = require('chai');const expect = chai.expect; const sinon = require("sinon"); const sinonChai = require("sinon-chai"); chai.use(sinonChai); describe('doWork', ()=>{ let sum; it('should call sum', ()=>{ sum = sinon.spy(); doWork(); expect(sum).to.be.calledWith(1,2); }); }); 

在前面的场景中,sum函数被替换为spy函数。因此它的实际功能将不再存在。如果我们想要确保sum函数不仅被监视,而且仍然按照我们的期望工作,我们需要在sinon.spy()后面添加.andCallThrough()

describe('doWork', ()=>{ let sum; console.log = sinon.spy(); it('should call sum', ()=>{ sum = sinon.spy().andCallThrough(); doWork(); expect(sum).to.be.calledWith(1,2); expect(console.log).to.be.calledWith(3); }); }); 

请注意,通过在我们的sum间谍上包含andCallThrough,我们不仅能够监视它并断言它是否被调用,还能够监视console.log函数并断言它是否被调用并返回sum返回的正确值。

spy通常只是一个函数的观察者,并且只报告函数是否被调用,而stub允许你在测试执行期间为函数提供自定义功能。测试存根被称为预编程行为函数,用于测试应用程序中作为模块依赖项的包装样板代码。

stub视为超级间谍,它报告与spy相同的事情,但也执行您想要的特定任务。使用相同的示例,让我们将sum函数存根为始终返回相同的值:

it('should console.log sum response', ()=>{ // replace the existing sum function with a new stub, // a generic function that does exactly what we specify // in this case always just return the number 2 sum = sinon.stub(()=>{ return 2; }); // lets replace the standard console.log function // with a spy console.log = sinon.spy(); // call our doWork function (which itself uses console.log) doWork(); // and if doWork executed the way its supposed to, console.log // should have been called and the parameter 2 passed to it expect(console.log).to.be.calledWith(2); }); 

当函数执行可能产生意外结果,并且您只想为测试目的强制响应时,存根函数是很好的。当您进行 TDD 并且正在针对尚未编写的函数进行测试时,存根也很方便。

在同一模块内编写测试时,间谍和存根非常有用,但是当您需要监视或存根另一个 Node 模块中所需的模块时,情况就会变得有点棘手。幸运的是,有一个名为Proxyquire的工具,它将允许您存根从您的代码中所需的模块。

检查以下代码示例:

// google.js const request = require('request'), sinon = require("sinon"), log = sinon.spy();module.exports =()=>{ request('http://www.google.com', (err, res, body)=>{ log(body); }); } 

您可以看到我们需要request模块。request模块接受两个参数,其中一个是callback函数。事情开始变得棘手的地方就在这里。在这种情况下,我们将如何实现间谍和/或存根?此外,我们如何防止我们的测试明确地发出网络调用以获取google.com?如果我们运行测试时google.com宕机(哈!)会怎么样?

为了能够监视request模块,我们需要一种拦截实际require并附加我们自己的存根版本的request的方法。request模块实际上是一个您想要存根的模块的很好的例子,因为request用于进行网络调用,这是您希望确保您的测试永远不会真正执行的操作。您不希望您的测试依赖外部资源,例如网络连接或依赖从实际请求返回的数据。

使用 Proxyquire,我们实际上可以设置我们的测试,以便它们拦截require模块,并用我们自己的存根替换执行的内容。以下是针对我们之前创建的模块编写的测试文件的示例:

//google.spy.jsconst sinon = require("sinon"),proxyquire = require('proxyquire'),log = sinon.spy(), requestStub = sinon.stub().callsArgWith(1, null, null, 'google.com'), google = proxyquire('./google', { 'request': requestStub }); describe('google module', ()=>{ beforeEach(()=>{ google(); }); it('should request google.com', ()=>{ expect(reqstub).to.be.called(); }); it('should log google body', ()=>{ expect(callback).to.be.calledWith(null, null, 'google.com'); }); }); 

测试套件的第一件事是设置一个spy和一个通用的stub函数,该函数将用作request模块。然后,我们包括我们的google模块,但我们使用proxyquire而不是典型的require模块。使用proxyquire,我们传递模块的路径,方式与require相同,只是第二个参数是在该模块中所需的模块,以及要在其位置使用的stub函数。

在每个测试之前,我们将执行原始的google模块,并断言我们的stub实际上被调用。此外,我们断言log间谍被调用,并且使用从request模块返回的任何数据。由于我们控制该模块,因此我们可以测试确实,当请求发送到http://google.com时,返回了字符串google.com(我们确切知道这不是真的--不仅如此,我们还知道从未发送网络调用到www.google.com)。

我们正在使用stub的特殊功能,该功能允许我们执行特定参数到存根函数,假设它是callback函数。在这里,我们使用callsArgWith,并将参数index(从零开始)包括为第一个参数;在这种情况下,传递给请求的两个参数中的一个,第一个(索引 0)是 URL 本身,第二个(索引 1)是callback函数。使用callsArgWith,我们可以执行callback函数并具体提供其参数,例如nullnull和一个字符串。像Sinon.jsChai.js一样,proxyquire也需要作为devDependency包含在我们的项目中。

 $ npm install --save-dev proxyquire

到目前为止,我们看到的所有测试代码都只是演示和示例,我们实际上还没有运行任何测试。让我们设置应用程序的基本结构,以便我们可以开始编写真正的测试。

首先要做的是设置一个文件夹结构,用来存放所有的测试。考虑以下步骤:

  1. 在应用程序项目文件夹的根目录中,创建一个名为tests的文件夹。

  2. tests文件夹中,创建三个更多的文件夹,分别为controllersmodelsserver

/(existing app root) tests/ ----/controllers/ ----/models/ ----/server/ 

在我们开始为应用程序编写测试之前,有一些额外的开销需要我们准备好以准备进行测试。为了处理这些开销,我们将编写一个testhelper文件,它将被包含并与我们通过 Mocha 执行的每个测试文件一起运行。

tests文件夹中创建一个名为testhelper.js的文件,并插入以下代码块:

const chai = require('chai'), sinon = require('sinon'), sinonChai = require('sinon-chai'); global.expect = chai.expect; global.sinon = sinon; chai.use(sinonChai); 

这是我们通常需要在每一个测试文件的顶部包含的代码;但是,通过将其包含在一个单独的文件中,我们可以指示 Mocha 自动要求每个运行的测试文件包含这个文件。文件本身只包括chaisinon模块,并定义了一些全局变量作为我们测试编写的快捷方式。此外,它指示chai使用sinonChai模块,以便我们的语法得到扩展,我们可以编写 Sinon 特定的 Chai 断言。实际运行我们的测试套件的命令如下:

 $ mocha -r tests/testhelper.js -R spec tests/**/*.test.js

记住我们之前全局安装了 Mocha,这样我们就可以从任何地方执行mocha命令。

根据前面命令中测试的路径,假设该命令将从应用项目文件夹的根目录执行。-r标志指示 Mocha 要求testhelper.js模块。-R标志是定义测试报告输出样式的选项。我们选择使用spec样式,它以嵌套缩进样式列出我们的报告,每个describeit语句,以及通过测试的绿色复选标记。最后一个参数是我们test文件的路径;在这种情况下,我们提供了通配符,以便所有的测试都会运行。

Mocha 有几种不同的报告样式可供选择。这包括点(每个测试重复一个点)、列表、进度(百分比条)、JSON 和 spec。其中比较有趣的,尽管有点无用,是-R nyan报告样式。

让我们写一个快速的样本测试,以确保我们的项目设置正确。在tests文件夹中,创建一个名为mocha.test.js的新文件,并包含以下代码:

describe('Mocha', ()=>{ 'use strict'; beforeEach(()=>{}); describe('First Test', ()=>{ it('should assert 1 equals 1', ()=>{ expect(1).to.eql(1); }); });});

前面的测试非常简单,只是断言1等于1。保存这个文件,再次运行Mocha测试命令,你应该会得到以下输出:

 $ mocha -r tests/testhelper.js -R spec tests/mocha.test.js Mocha First Test should assert 1 equals 1 1 passing (5ms)

你可能会觉得记住和执行Mocha的那个冗长而复杂的命令很烦人和令人沮丧。幸运的是,有一个相当简单的解决方案。编辑应用程序中的package.json文件,并添加以下部分:

"scripts": { "start": "node server.js", "test": "mocha -r tests/testhelper.js -R spec tests/**/*.test.js" }, 

通过在package.json文件中进行这个调整,现在你可以简单地在命令行中执行npm test作为一个快速简便的快捷方式。这是package.json文件的一个标准约定,所以任何开发人员都会知道如何简单地执行npm test

 $ npm test > chapter9@0.0.0 test /Users/jasonk/repos/nodebook/chapter9 > mocha -r tests/testhelper.js -R spec tests/**/*.test.js Mocha First Test should assert 1 equals 1 1 passing (5ms)

现在我们的项目已经设置好了,可以正确运行和执行测试,让我们开始为应用程序编写一些真正的测试。

在解决了所有这些背景信息之后,让我们专注于为我们构建的应用程序编写一些真正的测试。在接下来的几节中,我们将为应用程序的路由、服务器、模型和控制器编写测试。

让我们慢慢开始,先看看我们应用程序中最基本的文件之一,routes.js文件。这个文件只是定义了应用程序应该响应的路由数量。这将是最容易编写测试的文件之一。

由于routes.js文件位于我们主应用程序中的server文件夹中,让我们将其相应的测试文件放在类似的位置。在tests/server文件夹中,创建一个名为routes.test.js的文件。由于routes.test.js文件将测试我们的routes.js文件的功能,我们需要它require相同的模块。

test/server/routes.test.js中包含以下代码:

const home = require('../../controllers/home'), image = require('../../controllers/image'), routes = require('../../server/routes'); 

请注意,路径不同,因为我们从test/server文件夹中require模块,但我们还需要require特定于应用程序的模块。另外,请注意,除了我们原始的routes.js文件需要的模块之外,我们还需要require routes模块本身。否则,如果没有包含它,我们将无法测试模块的功能。接下来,让我们设置测试套件的结构并创建一些spy。在tests/server/routes.test.js中的先前代码之后包括这个新的代码块:

describe('Routes',()=>{ let app = { get: sinon.spy(), post: sinon.spy(), delete: sinon.spy() }; beforeEach(()=>{ routes.initialize(app); }); // to do: write tests... }); 

如果您还记得,routes模块的initialize函数接受一个参数,即app对象。在我们的测试中,我们将app定义为一个简单的匿名对象,有三个函数-- getpostdelete;每个都是一个spy。我们包括一个beforeEach块,在每次测试运行之前执行initialize函数。

现在,让我们包括一些测试。首先,我们将测试GET端点是否正确配置。在// to do: write tests...注释之后,放置以下代码块:

describe('GETs',()=>{ it('should handle /', function(){ expect(app.get).to.be.calledWith('/', home.index); }); it('should handle /images/:image_id', ()=>{ expect(app.get).to.be.calledWith('/images/:image_id', image.index); }); }); 

然后,测试POST端点:

describe('POSTs', ()=>{ it('should handle /images', ()=>{ expect(app.post).to.be.calledWith('/images', image.create); }); it('should handle /images/:image_id/like', ()=>{ expect(app.post).to.be.calledWith('/images/:image_id/like', image.like); }); it('should handle /images/:image_id/comment', ()=>{ expect(app.post).to.be.calledWith('/images/:image_id/comment', image.comment); });}); 

最后,测试DELETE端点:

describe('DELETEs', ()=>{ it('should handle /images/:image_id', ()=>{ expect(app.delete).to.be.calledWith('/images/:image_id', image.remove); });}); 

这些测试都断言了同一件事,即app对象的相应getpostdelete函数是否针对每个路由使用了正确的参数。我们能够针对参数进行测试,因为我们使用的app对象是一个spy

如果您运行mocha命令来执行测试套件,您应该会看到以下输出:

 $ npm test Routes GETs should handle / should handle /images/:image_id POSTs should handle /images should handle /images/:image_id/like should handle /images/:image_id/comment DELETEs should handle /images/:image_id 6 passing (14ms)

测试server.js文件将与我们的其他文件略有不同。该文件作为我们应用程序的根运行,因此它不导出任何我们可以直接测试的模块或对象。由于我们使用server.js启动服务器,我们需要模拟从我们的代码启动服务器。我们将创建一个名为server的函数,它将使用proxyquire需要server.js文件,并对它需要的每个模块进行存根。执行server()函数将与从命令行执行node server.js完全相同。文件中的所有代码都将通过该函数执行,然后我们可以使用proxyquire中的stub对每个调用进行测试。

tests/server/文件夹中创建名为server.test.js的文件,并插入以下代码块:

let proxyquire, expressStub, configStub, mongooseStub, app, server = function() { proxyquire('../../server', { 'express': expressStub, './server/configure': configStub, 'mongoose': mongooseStub }); }; describe('Server',()=>{ beforeEach(()=>{ proxyquire = require('proxyquire'), app = { set: sinon.spy(), get: sinon.stub().returns(3300), listen: sinon.spy() }, expressStub = sinon.stub().returns(app), configStub = sinon.stub().returns(app), mongooseStub = { connect: sinon.spy(), connection: { on: sinon.spy() } }; delete process.env.PORT; }); // to do: write tests... }); 

在为我们的服务器运行每个测试之前,我们重置服务器的所有主要组件的存根。这些存根包括app对象、expressconfigmongoose。我们对这些模块进行存根,因为我们想要对它们进行spy(并且我们使用存根是因为其中一些需要返回我们将在文件中使用的对象)。现在我们已经准备好了所有的spy和我们的app对象框架,我们可以开始测试我们代码的主要功能。

我们需要检查以下条件是否通过:

创建一个应用程序

  • 视图目录已设置

  • 端口已设置并且可以配置和/或设置为默认值

  • 应用程序本身已配置(config已调用)

  • Mongoose 连接到数据库 URI 字符串

  • 应用程序本身已启动

用以下代码块替换之前代码中的// to do: write tests...注释:

describe('Bootstrapping', ()=>{ it('should create the app', ()=>{ server(); expect(expressStub).to.be.called; }); it('should set the views', ()=>{ server(); expect(app.set.secondCall.args[0]).to.equal('views'); }); it('should configure the app', ()=>{ server(); expect(configStub).to.be.calledWith(app); }); it('should connect with mongoose', ()=>{ server(); expect(mongooseStub.connect).to.be.calledWith(sinon.match.string); }); it('should launch the app', ()=>{ server(); expect(app.get).to.be.calledWith('port'); expect(app.listen).to.be.calledWith(3300, sinon.match.func); });}); 

在前面的一组测试中,我们测试了服务器的引导,这些都是最初在server.js中运行的所有功能。测试的名称相当不言自明。我们检查app对象的各种方法,确保它们被调用和/或传递了正确的参数。对于测试,我们希望测试特定类型的参数是否被调用,而不是参数值的确切内容;我们使用 Sinon 的匹配元素,这使得我们的测试可以更加通用。我们不希望在测试中硬编码 MongoDB URI 字符串,因为这只是我们需要维护的另一个地方--尽管如果您希望测试非常严格(即确切地断言传递了确切的 URI 字符串),您完全可以这样做。

在第二组测试中,我们希望确保端口已设置,默认为3300,并且可以通过使用节点环境变量进行更改:

describe('Port', ()=>{ it('should be set', ()=>{ server(); expect(app.set.firstCall.args[0]).to.equal('port'); }); it('should default to 3300', ()=>{ server(); expect(app.set.firstCall.args[1]).to.equal(3300); }); it('should be configurable', ()=>{ process.env.PORT = '5500'; server(); expect(app.set.firstCall.args[1]).to.equal('5500'); });}); 

有了这些测试,再次运行npm test命令,您应该会得到以下输出:

 $ npm test Server Bootstrapping should create the app (364ms) should set the views should configure the app should connect with mongoose should launch the app Port should be set should default to 3300 should be configurable

在测试我们的模型时,我们希望包括model模块本身,然后针对它编写测试。这里最简单的解决方案是创建一个测试model对象,然后断言该模型具有我们期望的所有字段,以及我们可能创建的任何虚拟属性。

创建tests/models/image.test.js文件,并插入以下代码:

let ImageModel = require('../../models/image'); describe('Image Model',()=>{ var image; it('should have a mongoose schema',()=>{ expect(ImageModel.schema).to.be.defined; }); beforeEach(()=>{ image = new ImageModel({ title: 'Test', description: 'Testing', filename: 'testfile.jpg' }); }); // to do: write tests... }); 

首先,我们使用require包含ImageModel(注意require语句的路径)。我们运行的第一个测试是确保ImageModel具有一个 mongoose 模式属性。在这个测试之后,我们定义了beforeEach块,我们将依赖于这个块进行我们余下的测试。在每个测试之前,我们都希望实例化一个新的ImageModel对象,以便我们可以进行测试。我们可以在beforeEach块中执行此操作,以确保我们在每个测试中都处理一个新的对象,并且它没有被先前运行的任何测试所污染。还要注意的是,第一个测试和beforeEach块的顺序实际上并不重要,因为beforeEach块将在其父describe函数中的每个测试之前运行,无论它是以何种顺序定义的。

包括以下一组测试,替换占位符// to do: write tests...的注释:

describe('Schema', ()=>{ it('should have a title string', ()=>{ expect(image.title).to.be.defined; }); it('should have a description string', ()=>{ expect(image.description).to.be.defined; }); it('should have a filename string', ()=>{ expect(image.filename).to.be.defined; }); it('should have a views number default to 0', ()=>{ expect(image.views).to.be.defined; expect(image.views).to.equal(0); }); it('should have a likes number default to 0', ()=>{ expect(image.likes).to.be.defined; expect(image.likes).to.equal(0); }); it('should have a timestamp date', ()=>{ expect(image.timestamp).to.be.defined; });}); 

在这里,我们将检查确保我们期望的ImageModel实例具有的每个属性都已定义。对于已设置默认值的属性,我们还检查确保默认值也已设置。

接下来,我们将对我们期望ImageModel具有的virtuals进行测试,并验证它们是否按预期工作:

describe('Virtuals', ()=>{ describe('uniqueId', ()=>{ it('should be defined', ()=>{ expect(image.uniqueId).to.be.defined; }); it('should get filename without extension', ()=>{ expect(image.uniqueId).to.equal('testfile'); }); });}); 

在测试uniqueId虚拟属性时,它应该返回image模型的文件名,但不包括扩展名。由于beforeEach定义了我们的image模型,文件名为testfile.jpg,我们可以通过测试断言uniqueId返回的值等于testfile(不包括扩展名的文件名)。

运行我们的模型测试应该提供以下结果:

 $ npm test Image Model should have a mongoose schema Schema should have a title string should have a description string should have a filename string should have a views number default to 0 should have a likes number default to 0 should have a timestamp date Virtuals uniqueId should be defined should get filename without extension

最后,让我们来看看image控制器,特别是对主要的index函数进行测试。由于index函数需要做很多工作并执行许多不同的任务,测试文件将大量使用存根和间谍。在任何测试之前,我们需要声明一些全局变量供我们的测试使用,并设置所有我们的stub、间谍和占位符对象以供proxyquire使用。然后,我们使用proxyquire来引入实际的图像控制器。创建一个名为tests/controllers/image.test.js的文件,并插入以下代码:

let proxyquire = require('proxyquire'), callback = sinon.spy(), sidebarStub = sinon.stub(), fsStub = {}, pathStub = {}, md5Stub = {}, ModelsStub = { Image: { findOne: sinon.spy() }, Comment: { find: sinon.spy() } }, image = proxyquire('../../controllers/image', { '../helpers/sidebar': sidebarStub, '../models': ModelsStub, 'fs': fsStub, 'path': pathStub, 'md5': md5Stub }), res = {}, req = {}, testImage = {}; 

通过这段代码,我们定义了许多全局变量,如间谍、存根或空占位符 JavaScript 对象。一旦我们的stub准备好了,我们将调用proxyquire来包含我们的image控制器(确保image控制器中的所需模块实际上被我们各种stub和间谍替换)。现在,所有我们的全局变量、stub和间谍都准备好了,让我们包含一些测试。

在上述代码块之后包含以下代码:

describe('Image Controller', function(){ beforeEach(()=>{ res = { render: sinon.spy(), json: sinon.spy(), redirect: sinon.spy() }; req.params = { image_id: 'testing' }; testImage = { _id: 1, title: 'Test Image', views: 0, likes: 0, save: sinon.spy() }; }); // to do: write tests... }); 

再次,我们将使用beforeEach块为我们的测试构建一些设置。这会在res对象的每个函数上设置间谍,包括 render、JSON 和 redirect(这些函数在image控制器中都被使用)。我们通过设置req.params对象的image_id属性来伪造查询字符串参数。最后,我们将创建一个测试image对象,该对象将被我们的假 mongooseimage模型存根使用,以模拟从 MongoDB 返回的数据库对象:

describe('Index',()=>{ it('should be defined', ()=>{ expect(image.index).to.be.defined; }); it('should call Models.Image.findOne', ()=>{ ModelsStub.Image.findOne = sinon.spy(); image.index(req, res); expect(ModelsStub.Image.findOne).to.be.called; }); it('should find Image by parameter id', ()=>{ ModelsStub.Image.findOne = sinon.spy(); image.index(req, res); expect(ModelsStub.Image.findOne).to.be.calledWith( { filename: { $regex: 'testing' } }, sinon.match.func ); }); // to do: write more tests... }); 

我们运行的第一个测试是确保index函数实际存在。在index函数中,发生的第一个动作是通过Models.Image.findOne函数找到image模型。为了测试该函数,我们首先需要将其设置为spy。我们之所以在这里而不是在beforeEach中这样做,是因为我们可能希望在每个测试中findOne方法的行为略有不同,所以我们不希望为所有测试设置严格的规则。

为了模拟GET调用被发布到我们的服务器,并且我们的图像index控制器函数被调用,我们可以手动触发该函数。我们使用image.index(req, res)并传入我们的假请求和响应对象(在beforeEach函数中定义为全局变量并存根)。

由于ModelsStub.Image.findOne是一个间谍,我们可以测试它是否被调用,然后分别测试它是否被调用时使用了我们期望的参数。在findOne的情况下,第二个参数是一个回调函数,我们不关心或不想测试包含的非常具体的函数,而只是确保包含了一个实际的函数。为此,我们可以使用 Sinon 的匹配器 API,并指定一个 func 或函数作为第二个参数。

这组tests测试了当找到图像并从findOne函数返回时执行的代码。

describe('with found image model', ()=>{ beforeEach(function(){ ModelsStub.Image.findOne = sinon.stub().callsArgWith(1,null,testImage); }); it('should incremement views by 1 and save', ()=>{ image.index(req, res); expect(testImage.views).to.equal(1); expect(testImage.save).to.be.called; }); it('should find related comments', ()=>{ image.index(req, res); expect(ModelsStub.Comment.find).to.be.calledWith( {image_id: 1}, {}, { sort: { 'timestamp': 1 }}, sinon.match.func ); }); it('should execute sidebar', ()=>{ ModelsStub.Comment.find = sinon.stub().callsArgWith(3, null, [1,2,3]); image.index(req, res); expect(sidebarStub).to.be.calledWith( {image: testImage, comments: [1,2,3]}, sinon.match.func); }); it('should render image template with image and comments', ()=>{ ModelsStub.Comment.find = sinon.stub().callsArgWith(3, null, [1,2,3]); sidebarStub.callsArgWith(1, {image: testImage, comments: [1,2,3]}); image.index(req, res); expect(res.render).to.be.calledWith('image', {image: testImage, comments: [1,2,3]}); });}); 

在这里你会注意到的第一件事是,在这些测试中findOne不再是一个间谍,而是一个存根,它将手动触发作为第二个参数提供的回调函数。被触发的回调函数将包含我们的测试image模型。通过这个存根,我们模拟了通过findOne实际进行了数据库调用,并且返回了一个有效的image模型。然后,我们可以测试在主回调中执行的其余代码。我们使用Comment.find调用进行类似的设置。

当执行sidebarStub时,我们使用callsArgWith Sinon 函数,该函数触发最初包含的回调函数。在该回调函数中,我们将假的viewModel作为参数包含进去。

一旦sidebarStub完成其工作,我们期望res.render已被调用,并且我们指定了我们期望它被调用的确切参数。

运行image控制器的测试应该产生以下输出:

 $ npm test Image Controller Index should be defined should call Models.Image.findOne should find Image by parameter id with found image model should incremement views by 1 and save should find related comments should execute sidebar should render image template with image and comments

如果有疑问,编写测试时最安全的做法是对所有内容进行间谍,对其他所有内容进行存根。总会有时候你希望一个函数自然执行;在这种情况下,不要动它。最终,您永远不希望您的测试依赖于任何其他系统,包括数据库服务器、其他网络服务器、其他 API 等。您只想测试您自己的代码是否有效,仅此而已。如果您的代码预期调用 API,请对实际调用进行间谍,并断言您的代码尝试进行调用。同样,通过存根伪造服务器的响应,并确保您的代码正确处理响应。

检查代码中的依赖项最简单的方法是停止任何其他服务的运行(本地节点应用程序等),甚至可能禁用网络连接。如果您的测试超时或在意外的地方失败,很可能是因为您错过了需要在途中进行间谍或存根的函数。

在编写测试时不要陷入兔子洞。很容易被带入并开始测试可以安全假定正在工作的功能。一个例子是编写测试以确保第三方模块的正确执行。如果不是您编写的模块,请不要测试它。不要担心编写测试来证明模块是否按照其应有的方式工作。

要了解有关编写 JavaScript 特定的 TDD 的更多信息,我强烈建议您阅读 Christian Johansen 的巨著:Test-Driven JavaScript Development。这本书内容丰富,涵盖了与 TDD 相关的大量信息。在某些圈子里,TDD 确实是一种生活方式,它将定义您编写代码的风格。

没有 Gulp,测试自动化从未如此简单。Gulp 是一个开源的 JavaScript 库,提供高效的构建创建过程,并充当任务运行器。我们将使用 Gulp 通过终端中的单个命令来自动化我们的单元测试。

让我们首先使用以下命令安装所有必需的软件包:

npm install gulp-cli -gnpm install gulp --save-devtouch test/gulpfile.jsgulp --help

请注意,您可能需要 root 访问权限来安装gulp-cli的全局软件包。在这种情况下使用sudo,例如sudo npm install gulp-cli -g。我们使用--save-dev在本地安装 Gulp 并将其保存为package.json中的开发依赖项。

此外,我们在test目录中创建了一个 Gulp 文件。现在,要test我们应用程序的目录并确保我们有以下文件结构:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (32)

安装所需的软件包并创建了 Gulp 文件后,让我们开始编写一些代码,如下所示:

var gulp = require('gulp');gulp.task('default', function() {console.log("Lets start the automation!")});

返回终端,运行 Gulp,您将收到以下输出:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (33)

Gulp 更快,更简单;为什么呢?Gulp 使用 node.js 流将数据块通过一系列管道插件传递。这加速了内存中的处理操作,并在任务的最后执行写操作。

让我们准备扩大学习 Gulp 的范围,并自动化我们在前几节中涵盖的单元测试。我们将首先安装其他所需的 npm 软件包。请注意,我们需要在project文件夹中安装它们,而不是在test文件夹中。因此,让我们使用cd..回到上一步,并确保您位于项目的根目录,然后运行以下命令:

npm install gulp-mocha --save-dev

gulp-mocha是运行mocha测试文件的插件。现在,让我们修改我们的 Gulp 文件并添加一些 es6 调料,如下所示:

const gulp = require('gulp');const gulpMocha = require('gulp-mocha')gulp.task('test-helper',()=>gulp.src('./testhelper.js'))gulp.task('test-server', ['test-helper'],()=>{return gulp.src('./server/server.test.js').pipe(gulpMocha())});

现在,运行gulp test-server以获得以下输出:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (34)

让我们讨论上述代码的工作原理。首先,我们创建了test-helper任务,它在内存中读取testhelper.js文件,除了存储测试server.test.js所需的全局变量之外什么也不做。

我们使用 mocha 作为一个框架来编写测试用例。gulpMocha插件通过读取server.test.js文件并将输出传输到终端来在内存中运行测试用例。要详细了解gulp-mocha,请访问github.com/sindresorhus/gulp-mocha链接。

注意如何写入依赖项的语法结构(如果需要)。让我们通过编写一个额外的任务来澄清添加依赖的方式:

gulp.task('test-routes', ['test-helper', 'test-server'],()=>{return gulp.src('./server/routes.test.js').pipe(gulpMocha())});

这次我们将运行gulp test-routes

现在,可能会有一个关于管理这么多任务的问题。Gulp 也提供了一种解决方案,可以一次性自动化所有任务。让我们向文件中添加以下片段:

gulp.task('build', ['test-helper', 'test-server','test-routes'])

运行上述命令;Gulp build和单元测试的自动化都已经完成。此外,我们可以添加所有的控制器和相应的模型,以实现基于项目的测试用例自动化。

这绝对是一个关于测试的速成课程,但基础已经奠定,我希望你对可以用来编写自己的测试的工具链有一个扎实的理解。相信这套强大的工具组合,你将很快编写出牢固的代码!

编写测试的最终目标是实现 100%的代码覆盖率,并且为你编写的每一行代码都存在单元测试。从这里开始,真正的测试是转向 TDD,这要求你在任何代码存在之前先编写测试。显然,对不存在的代码进行测试将会失败,所以你需要编写尽量少的代码来使其通过,并重复这个过程!

在下一章中,我们将看看一些云端托管选项,以便让你的应用程序在线上运行起来。

不可避免地,您会希望您一直在构建的应用程序在线并且对世界可用,无论您是想在开发过程中在线托管您的应用程序,还是在应用程序完成并准备投入生产时。目前有许多不同的托管选项可供 Node.js 和基于 MongoDB 的应用程序使用,在本章中,我们将介绍一些不同的热门服务的部署方式。

在这一章中,我们将涵盖以下内容:

  • 云与传统 Web 托管

  • Git 源代码控制的介绍

  • 使用 Nodejitsu 部署应用程序

  • 使用 Heroku 部署应用程序

  • 使用 Amazon Web Services 部署应用程序

  • 使用 Microsoft Azure 部署应用程序

  • 对 DigitalOcean 的简要介绍

如果您之前有网站托管的经验,我将称之为传统托管,您可能对使用 FTP 上传网页文件到托管提供商的过程非常熟悉。在传统 Web 托管中,服务提供商通常为每个用户提供共享空间,每个用户都配置有自己的公共文件夹,用于存放网页文件。在这种情况下,每个客户都托管相同类型的网站,他们的文件都存储在同一台 Web 服务器上并由其提供服务。

传统的 Web 托管成本相对较低,因为单个 Web 服务器可以托管成百上千个个人网站。传统托管通常存在扩展性问题,因为如果您的网站需要更多的资源,它需要被迁移到另一台服务器(具有更多硬件),并且在此迁移过程中可能会出现潜在的停机时间。作为一个副作用,如果与您的网站位于同一服务器上的网站对硬件要求特别高,那么该服务器上的每个网站都可能会受到影响。

使用基于云的托管,每个网站或服务的实例都托管在自己的虚拟专用服务器(VPS)上。当客户上传其网站的副本时,该网站在其自己的隔离环境中运行,并且该环境专门设计用于仅运行该网站。虚拟专用服务器是服务器的实例,通常都同时在同一硬件上运行。由于其隔离性质,VPS 的扩展性非常好,因为只需更改硬件分配的设置,服务器就会重新启动。如果您的 VPS 托管在与其他 VPS 相同的硬件上,并且它们正在经历高流量峰值,您的网站不会因 VPS 的隔离性质而受到影响。

云的美妙之处在于可以获得的服务级别和数量变化很大。对于运行您的 Web 应用程序的基本托管计划,您可以使用许多被视为平台即服务(PaaS)的服务。这是一种为您提供托管和运行 Web 应用程序的平台。随着规模和复杂性的增加,您可以转向提供整个基于云的数据中心的基础设施即服务(IaaS)提供商。

您可以通过阅读一篇详细的文章了解 IaaS、PaaS 和软件即服务(SaaS)之间的区别,该文章可在www.rackspace.com/knowledge_center/whitepaper/understanding-the-cloud-computing-stack-saas-paas-iaas上找到。

基于云的托管成本可能会有很大的变化,因为它们非常可扩展。您的成本可能会在一个月内发生剧烈波动,这取决于您对资源的需求(即,在一个月中需求更高的时间和/或像 HackerNews 或 Reddit 这样的大型社交媒体的点击)。另一方面,如果您对服务器的需求非常小,您通常可以免费获得云托管!

传统的 Web 托管服务提供商包括 GoDaddy、Dreamhost、1&1、HostGator 和 Network Solutions。热门的基于云的托管选项包括 Nodejitsu(PaaS)、Heroku(PaaS)、Amazon Web Services(IaaS)、Microsoft Azure(IaaS)和 Digital Ocean。

对于传统的托管提供商,连接到服务器并上传文件的标准方法是使用文件传输协议FTP)。您可以使用任何标准的 FTP 软件进行连接,并将文件副本推送到服务器,这些更改将在访问您的网站 URL 时立即反映在线。对于基于云的托管提供商,标准通常是使用 Git 源代码控制。Git 是一种源代码控制技术,允许您跟踪项目源代码的更改和历史,以及提供与多个开发人员轻松协作的简便方法。目前最受欢迎的 Git 在线代码存储库提供商是www.github.com

我们将使用 Git 来跟踪我们的应用项目源代码,并将我们的代码推送到各种云托管提供商。当您使用 Git 推送代码时,您实际上是将所有或仅更改版本的代码传输到在线存储库(例如,Git 和www.github.com相对容易进入,但可能看起来令人生畏和复杂)。如果您对 Git 和/或GitHub.com不熟悉,我强烈建议您花点时间通过查看以下指南来熟悉:

指南将带您了解以下概念:

  • 下载和安装 Git

  • github.com注册帐户

  • 使用github.com对您的计算机进行身份验证并创建您的第一个存储库

  • 将项目源代码提交到存储库

一旦您将项目源代码配置为本地 Git 存储库,并且所有代码都提交到主分支,就可以继续阅读以下各节。

现在,您已经将项目设置为本地 GitHub 存储库,是时候将该代码上线了!接下来的各节将分别介绍将应用程序部署到几个不同的热门基于云的托管提供商的过程。

请随意探索和尝试每个提供商,因为大多数都有免费或相对便宜的计划。每个提供商都有其优势和劣势,所以我将由您决定哪个适合您的特定需求。我们介绍的服务没有特定的顺序。

为了本章的目的,我将一贯地将我的应用命名为imgploadr;但是,您的应用名称需要不同和独特。在本章中,无论我何时提到imgploadr,您都应该用您自己应用的独特名称替换它。

要开始使用 Nodejitsu,请访问www.nodejitsu.com并首先注册一个免费帐户。在提供您的电子邮件地址、用户名和密码后,您将看到一个定价计划页面,您可以在该页面配置您的服务。如果您只想创建免费帐户并进行实验,只需单击“不,谢谢”按钮,注册过程就完成了。然后,只需单击右上角的“登录”按钮即可登录并转到您的应用程序仪表板。

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (35)

将您的应用程序部署到 Nodejitsu 将需要一个新的命令行界面工具;具体来说,是jitsu CLI。单击大蓝色的使用 jitsu 部署应用程序按钮将带您到此工具的www.github.com存储库。您可以跳过这一步,只需使用以下npm命令手动安装 CLI:

 $ sudo npm install -g-g install jitsu

安装npm包时,有时需要使用sudo命令来全局安装(使用-g标志)。取决于您所使用的机器的访问级别,您可能需要或者不需要包括sudo

现在jitsu CLI 已安装,您可以使用这个方便的工具来登录到您的 Nodejitsu 帐户,创建一个应用程序,并部署您的项目代码。首先,让我们登录:

$ jitsu login info: Welcome to Nodejitsu info: jitsu v0.13.18, node v0.10.26 info: It worked if it ends with Nodejitsu ok info: Executing command login help: An activated nodejitsu account is required to login help: To create a new account use the jitsu signup command prompt: username: jkat98 prompt: password: info: Authenticated as jkat98 info: Nodejitsu ok 

您可以看到,在成功提供用户名和密码后,您现在已经通过 Nodejitsu 进行了身份验证,准备好开始了。

在我们实际部署应用程序之前,我们需要在 Nodejitsu 仪表板中配置 MongoDB 数据库。切换回浏览器,在 Nodejitsu 应用程序仪表板上,通过单击数据库选项卡切换部分。

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (36)

通过单击大的 MongoHQ 按钮,让我们选择 MongoHQ 来满足我们的需求。您将被提示为新数据库命名,然后它将在屏幕底部的“您的数据库”部分列出。我们需要的重要部分是连接字符串,旁边有一个方便的复制链接,可以将其复制到剪贴板。

编辑server.js文件,并更新mongoose.connect行以使用您为 Nodejitsu 数据库复制的新连接字符串:

[/server.js] mongoose.connect('YOUR_NODEJITSU_CONNECTION_STRING_HERE'); mongoose.connection.on('open', ()=>{ console.log('Mongoose connected.'); }); 

唯一剩下的就是打开终端,切换到项目主目录,并执行以下命令来打包您的应用程序并将其推送到 Nodejitsu:

$ jitsu deploy info: Welcome to Nodejitsu jkat98 info: jitsu v0.13.18, node v0.10.26 info: It worked if it ends with Nodejitsu ok info: Executing command deploy warn: warn: The package.json file is missing required fields: warn: warn: Subdomain name warn: warn: Prompting user for required fields. warn: Press ^C at any time to quit. warn: prompt: Subdomain name: (jkat98-imgploadr) imgploadr warn: About to write /Users/jasonk/repos/nodebook/imgploadr/package.json ... (a lot of npm install output) ... info: Done creating snapshot 0.0.1 info: Updating app myapp info: Activating snapshot 0.0.1 for myapp info: Starting app myapp info: App myapp is now started info: http://imgploadr.nodejitsu.com on Port 80 info: Nodejitsu ok

执行jitsu deploy后,CLI 首先会提示您确认在www.nodejitsu.com域名下的子域名是什么。随意更改为您喜欢的内容(它将检查确认可用性)。然后,它会对您的package.json文件进行一些微小的修改,具体包括使用您提供的任何值包括subdomain选项。最后,它会上传您的源代码并执行远程npm install操作。假设一切顺利,应用程序应该已部署,并且 URL 的确认应该输出到屏幕上。随意在浏览器中打开该 URL 以查看在线应用程序!

现在,您还可以看到应用程序在应用程序仪表板中列出:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (37)

现在应用程序已成功上传,通过其 URL 启动它,并尝试上传一个新图像进行测试运行。您应该注意到的第一件事是,尝试上传图像失败,并显示一个相当无用的错误(您可以通过从应用程序仪表板的日志选项卡访问以下错误):

400 Error: ENOENT, open '/opt/run/snapshot/package/public/upload/temp/72118-89rld0.png 

这个错误远非有用!基本上,这里发生的是应用程序试图上传并保存图像到实际上并不存在的temp文件夹!我们需要向我们的应用程序添加一小段代码来检查这种情况,并在必要时创建文件夹。

编辑server/configure.js文件,并在routes(app);return app;之间插入以下代码片段:

// Ensure the temporary upload folders exist fs.mkdir(path.join(__dirname, '../public/upload'), (err)=>{ console.log(err); fs.mkdir(path.join(__dirname, '../public/upload/temp'), (err)=>{ console.log(err); }); }); 

在这里,我们使用文件系统fs模块来创建父upload文件夹和temp子文件夹。也不要忘记在文件顶部require fs模块:

const connect = require('connect'), path = require('path'), routes = require('./routes'), exphbs = require('express3-handlebars'), moment = require('moment'), fs = require('fs'); 

有一个名为node-mkdirpnpm模块,它将执行递归的mkdir,基本上实现了我们在前面示例中调用的双重mkdir。我之所以没有包括它,是为了简洁起见,不包括额外的安装模块、要求它并不必要地使用它的指示。更多信息可以在www.npmjs.org/package/mkdirp找到。

在对代码进行了上述更改后,你需要再次部署你的应用程序。只需执行另一个jitsu deploy,你的代码的新副本将被上传到你的实例:

$ jitsu deploy 

再次打开你的应用程序 URL,这次你应该能够与应用程序进行交互并成功上传新的图片!恭喜,你已成功部署了你的应用程序,现在它正在使用 Nodejitsu 托管服务在线运行!

另一个流行的基于云的 Node.js 应用程序托管提供商是www.Heroku.com。Heroku 与其他提供商的一个不同之处在于其提供的强大附加组件的数量。任何你能想象到的你的应用程序需要的服务都可以作为附加组件使用,包括数据存储、搜索、日志和分析、电子邮件和短信、工作和排队、监控和媒体。这些附加组件可以快速而轻松地添加到你的服务中,并集成到你的应用程序中。

与 Nodejitsu 一样,Heroku 允许你注册一个免费帐户,并在其沙箱定价计划范围内工作。这些计划是免费的,但在带宽、处理能力等方面有限。大多数,如果不是全部,附加组件通常也提供某种免费的沙箱或基于试用的计划。与 Nodejitsu 一样,我们将在 Heroku 应用程序中使用的附加组件之一是 MongoHQ,一个基于云的 MongoDB 服务提供商。

首先,去heroku.com注册一个免费帐户。虽然注册不需要信用卡,但是为了在你的应用程序中包含任何附加组件,你必须在文件中提供信用卡(即使你不选择扩展服务,也不会被收费)。注册后,点击确认电子邮件中的链接并提供密码;你将看到你的应用程序仪表板。

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (38)

请注意,你需要做的第一件事是下载 Heroku Toolbelt(与 Nodejitsu 的jitsu CLI 类似)。点击下载按钮下载并安装 Toolbelt。Toolbelt 是一个专门用于创建和部署应用程序到 Heroku 的 CLI,并提供了heroku命令。

安装 Toolbelt 后,打开命令行终端并切换到项目的根目录。然后执行以下命令

登录 Heroku:

 $ heroku login Enter your Heroku credentials. Email: jkat98@gmail.com Password (typing will be hidden): Authentication successful.

现在你已经登录,可以直接向 Heroku 发出命令了

帐户并使用这些命令来创建应用程序,安装附加组件并部署你的项目。

你需要做的第一件事是创建一个新的应用程序。通过在命令行中执行heroku create来完成:

 $ heroku create Creating secret-shore-2839... done, stack is cedar http://secret-shore-2839.herokuapp.com/ | git@heroku.com:secret- shore-2839.git

创建应用程序后,Heroku 会随机分配一个唯一的名称;在我的情况下是secret-shore-2839(不过不用担心,这很容易改变):

 $ heroku apps:rename imgploadr --app secret-shore-2839 Renaming secret-shore-2839 to imgploadr... done http://imgploadr.herokuapp.com/ | git@heroku.com:imgploadr.git Don't forget to update your Git remotes on any local checkouts.

让我们接下来解决最后一部分。Heroku 依赖于你机器上的 Git 源代码控制,以便将你的项目源代码推送到服务器,而不像 Nodejitsu 那样使用自己的文件传输机制。假设你之前按照关于 Git 和www.github.com的说明进行了操作,你的项目源代码应该已经准备就绪并提交到主分支,准备好了。接下来我们需要做的是在你的机器上为 Git 添加一个指向 Heroku 的新远程。

让我们从git init开始,在当前工作目录中初始化git,然后执行以下命令为 Heroku 创建一个新的远程:

 $ git remote add heroku git@heroku.com:imgploadr.git

在将源代码推送到 Heroku 帐户之前,我们需要处理一些事情。

在您的 Heroku 服务器上运行应用程序之前,需要一个特殊的文件。这个文件称为Procfile,它专门包含启动应用程序所需的命令。在项目的根目录中创建一个名为Procfile(无扩展名)的新文件,并包含以下行:

 web: node server.js 

就是这样!有了那个文件,Heroku 将使用该命令启动您的应用程序。现在您已经设置了Procfile并且您的项目源代码已准备就绪,只剩下一件事要做--安装 MongoHQ 附加组件并配置您的应用程序以使用它:

 $ heroku addons:create mongohq --app imgploadr Adding mongohq on imgploadr... done, v3 (free) Use 'heroku addons:docs mongohq' to view documentation.

添加了 MongoHQ 附加组件后,您现在可以配置数据库本身并检索连接字符串(就像您之前在 Nodejitsu 中所做的那样)。访问您的heroku.com应用程序仪表板,它应该看起来像以下截图:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (39)

应用程序的仪表板屏幕是获取应用程序快照和快速查看当前成本的好地方。由于我正在为我的应用程序和附加组件使用沙箱和/或免费计划,我的当前预计月费用为$0.00。但是,如果您需要更多的功能,您可以快速轻松地扩展您的应用程序。请注意,您也可以快速轻松地将您的月费用飙升到天际!(将所有内容扩展到最大,我能够将我的预计费用提高到大约每月$60,000!)。

要配置您的 MongoHQ 数据库,只需在应用程序仪表板的附加组件部分下点击 MongoHQ 链接:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (40)

点击 Collections 标签下方的带有齿轮图标的 Admin 标签。点击 Users 标签,并提供应用程序将用于连接 MongoHQ 数据库的用户名和密码。这将创建具有安全密码的imgploadrdb用户名。添加新用户后,切换回概述标签并复制 Mongo URI 字符串。

就像在 Nodejitsu 中一样,编辑项目中的server.js文件,并用刚刚复制的新 URI 替换mongoose.connect字符串。编辑字符串,并根据您刚刚创建的新用户帐户的情况,用适当的值替换<username><password>server.jsmongoose.connect代码应如图所示:

mongoose.connect('mongodb://imgploadrdb:password@kahana.mongohq.co m:10089/app26'); mongoose.connection.on('open', ()=>{ console.log('Mongoose connected.'); });

由于您刚刚对项目的源代码进行了更改,因此需要记住将这些更改提交到 Git 存储库的主分支,以便它们可以上传到 Heroku。执行以下命令,将这些更改永久提交到您的源代码并将代码上传到 Heroku 服务器:

 $ git commit -am "Update mongoose connection string" $ git push heroku master Initializing repository, done. Counting objects: 50, done. Delta compression using up to 8 threads. Compressing objects: 100% (43/43), done. Writing objects: 100% (50/50), 182.80 KiB | 0 bytes/s, done. Total 50 (delta 3), reused 0 (delta 0) ... npm install output ... To git@heroku.com:imgploadr.git * [new branch] master -> master

将应用程序启动的最后一步是创建服务器的实例(基本上相当于打开它)。要做到这一点,执行以下命令:

 $ heroku ps:scale web=1 --app imgploadr Scaling dynos... done, now running web at 1:1X. $ heroku open Opening imgploadr... done

成功!希望您的浏览器已启动并且您的网站正在运行。继续,尝试上传一张图片!由于我们在 Nodejitsu 部署期间发现的错误,这个应用程序的更新版本应该可以正常工作。

虽然使用 Heroku 部署似乎比 Nodejitsu 更复杂,这可能是因为它使用 Git 源代码控制来促进项目文件的传输。此外,由于 Heroku 在扩展和附加组件的功能方面非常灵活,因此 Toolbelt CLI 更加强大。

虽然 Nodejitsu 和 Heroku 可以被认为是开发人员级别的服务提供商,因为它们是 PaaS,但亚马逊网络服务(和微软 Azure)将被认为是企业级服务,因为它们更像是 IaaS。AWS 和 Azure 提供的选项和服务的数量是令人震惊的。这绝对是顶级服务,像我们这样托管应用程序就像用火箭筒打苍蝇一样!

AWS 确实提供了自己的 NoSQL 数据库,称为 DynamoDB,但是对于我们的目的,我们希望继续使用 MongoDB 并在我们的应用程序中使用 Mongoose。为此,我们可以使用第三方 MongoDB 提供商。如果你还记得,当我们最初设置 Nodejitsu 时,列出的一个 MongoDB 提供商是 MongoLab。MongoLab 提供MongoDB 作为服务,这意味着我们可以使用它的服务来托管我们的 MongoDB 数据库,但使用 AWS 的所有功能来托管我们的 Node.js 应用程序(这与 Nodejitsu 和 Heroku 已经发生的情况并没有太大不同;它们只是更好地简化了这个过程)。请记住,AWS 是一个 IaaS 提供商,所以你也可以创建另一个服务器实例并自己安装 MongoDB,并将其用作数据源。但是,这略微超出了本章的范围。

为了在 AWS 中使用 MongoLab,我们首先需要在mlab.com/上注册一个新帐户并创建 AWS 数据库订阅。注册新帐户并使用他们通过电子邮件发送给你的链接进行激活后,你可以创建你的第一个数据库订阅。

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (41)

从你的主仪表板上,点击创建新按钮(带闪电图标)

闪电图标)。

从创建新订阅页面,配置以下设置:

  • 云提供商:亚马逊网络服务

  • 位置:你喜欢的任何地区

  • 计划:选择单节点(开发)

  • 选择沙盒(共享/免费)

  • MongoDB 版本:2.4.x

  • 数据库名称:anything_you_want(我选择了imgploadr

  • 确认价格为每月$0

  • 点击创建新的 MongoDB 部署

回到你的主仪表板,你现在应该看到你的新数据库已经创建并准备就绪。我们需要做的下一件事是创建一个用户帐户,我们的应用程序将用它来连接服务器。点击主仪表板上列出的数据库,然后选择用户选项卡。提供一个新的用户名和密码。添加新用户帐户后,复制位于屏幕顶部的 URI(只有在添加用户后才会出现)以mongodb://开头。

现在你有了新的 URI 连接字符串,我们需要更新server.js以在mongoose.connect中包含这个新的连接字符串。编辑文件并使用以下代码进行更新:

mongoose.connect('mongodb://imgploadrdb:password@ds061248.mongolab .com:61248/imgploadr'); mongoose.connection.on('open', ()=>{ console.log('Mongoose connected.'); }); 

确保用 MongoLab 仪表板上创建的用户帐户的适当信息替换<username><password>

将我们的应用程序代码更新为指向新的 MongoLab 数据库连接字符串后,我们需要将项目文件压缩,以便可以通过 AWS 仪表板上传。从你计算机的文件浏览器中,找到包含所有应用程序源代码文件的项目根目录,选择它们所有,右键单击它们以添加到存档或 ZIP 文件中。ZIP 文件的名称可以是任何你选择的。需要注意的一点是,你不应该在这个 ZIP 文件中包含node_modules文件夹(最简单的解决方案可能是直接删除整个文件夹)。如果你需要更多信息,AWS 在线文档有一个关于创建 ZIP 文件的很好的介绍(docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features.deployment.source.html)。

一旦你的源代码已经更新为使用新的 MongoLab 连接字符串,并且你已经创建了整个项目的 ZIP 文件(不包括node_modules文件夹),你就可以创建新的 AWS 应用程序并部署你的应用程序了。

如果您还没有亚马逊帐户,您需要一个才能使用他们的 AWS 服务。将浏览器指向aws.amazon.com,然后点击注册(即使您已经有亚马逊帐户)。在随后的屏幕上,您可以使用现有的亚马逊帐户登录或注册一个新帐户。注册并登录后,您将看到 AWS 提供的整套云服务。

我们感兴趣的主要服务是弹性 Beanstalk(位于部署和管理下,带有绿色图标):

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (42)

从此屏幕,点击右上角的创建新应用程序链接。随后的屏幕将引导您完成一个多步向导过程,在其中您将配置应用程序所在的环境。在适当的情况下配置以下设置:

  • 应用程序信息:

  • 应用程序名称:任何你想要的

  • 环境类型:

  • 环境层:Web 服务器

  • 预定义配置:Node.js

  • 环境类型:负载均衡自动扩展

  • 应用程序版本:

  • 上传您自己的(选择之前创建的 ZIP 文件)

  • 环境信息:

  • 环境名称:任何你想要的

  • 环境 URL:任何你想要的(这是您应用程序的子域)

  • 配置详情:

  • 实例类型:t1.micro

其余字段可以留空或使用它们的默认值

  • 环境标签:跳过此步骤;对于此应用程序是不必要的

最后一步是审查配置设置,然后启动环境(点击蓝色的 Launch 按钮)。

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (43)

弹性 Beanstalk 可能需要几分钟来配置和启动您的环境

应用程序,所以您可能需要耐心等待。环境正式启动并且应用程序在线后,继续打开您的应用程序(点击页面顶部的链接)并进行测试运行。假设一切按计划进行,您的应用程序应该已经启动并且应该正常运行!

微软的 Azure 服务与亚马逊的 AWS 非常相似。两者都可以被视为企业级服务,并且都提供了极大的灵活性和功能,具有非常流畅的用户界面。令人惊讶的是,尽管它是微软产品,您也可以使用 Azure 启动 Linux 环境的实例,以及托管您的 Node.js 和 MongoDB 应用程序。

您需要的第一件事,就像任何其他服务一样,是在azure.microsoft.com注册帐户。如果您有一个现有的 Microsoft Live 登录,您可以使用它;否则,您可以相当容易地注册一个新帐户。一旦您登录到 Azure 服务,您将首先看到的是您的主要仪表板。左边的图标是 Azure 提供的各种服务和选项。

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (44)

点击左下角的+NEW 图标将呈现给您

可以用来添加任何新服务的主要对话框。对于我们的目的,我们希望

添加网站:

  1. 选择计算、网站和从库中选择。

  2. 从众多的库选项中选择 Node JS Empty Site。这将创建必要的环境,以便您有一个可以放置应用程序的地方。

  3. 在随后的屏幕上,提供您应用程序的 URL。

  4. 将其余字段保留为默认值。

  5. 点击对勾图标完成设置过程,您的网站将被创建。

  6. 下一步是设置数据库服务器。与 AWS 或 Nodejitsu 类似,我们将再次选择 MongoLab 作为我们的数据库服务提供商。

  7. 再次点击+NEW 图标,选择 Store,并浏览列表,直到找到并选择 MongoLab。

  8. 点击下一个箭头并浏览各种计划。对于我们的需求,我们将保留 Sandbox 选项(因为它是免费的)。

  9. 为您的数据库提供一个名称;在我的情况下,我输入了imgploadrdb

  10. 再次单击下一步以查看和确认计划和每月价格(应为每月$0.00)。

  11. 最后,单击复选标志图标以购买这个新的订阅计划。

几秒钟后,您应该会回到仪表板,在那里您将看到网站和数据库应用服务的条目:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (45)

现在数据库已经创建并准备就绪,我们需要在应用程序中包含其连接字符串,然后才能上传我们的代码:

  1. 单击数据库行以选择它并转到其概述。

  2. 屏幕底部将包含一些图标,其中一个标有连接信息(并且有一个看起来像>i 的图标)。单击该图标,会弹出一个模态窗口,其中包含您的新 MongoLab 数据库服务器的连接字符串 URI。

  3. 将该 URI 复制到剪贴板。

  4. 编辑本地应用程序中的server.js,并用刚刚复制的新字符串替换mongoose.connect连接字符串。无需更新usernamepassword,因为 Azure 已经使用以下代码为您处理了这个问题:

mongoose.connect('mongodb://your_specific_azure_ mongolab_uri'); mongoose.connection.on('open', ()=>{ console.log('Mongoose connected.'); });

一旦更改完成,保存文件,并不要忘记使用 Git 更新您的本地 Git 存储库,因为在下一节中我们将使用 Git 将您的代码推送到 Azure(就像我们之前在 Heroku 上做的那样):

 $ git commit -am "Azure connection string"

回到 Azure 仪表板,在所有项目列表中单击 Web Site(或使用左侧工具栏上的图标筛选网站)。从概述屏幕中,找到朝向底部的集成源控制部分,并单击设置从源控制进行部署的链接。以下屏幕截图显示了此时您应该看到的内容:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (46)

选择本地 Git 存储库,然后通过单击下一个箭头图标继续。

接下来的屏幕将呈现如何将本地代码推送到刚刚为您的 Azure 网站创建的远程 Git 存储库的说明。要点是添加一个指向 Azure 存储库的新 Git 远程(就像我们之前在 Heroku 上做的那样),然后推送您的代码:

 $ git remote add azure SPECIFIC_URL_FOR_YOUR_SERVER $ git push azure master 

当您的代码开始在git push命令之后推送时,您应该注意到 Azure 仪表板中的 Git 信息屏幕会实时更新。从命令行中,您将看到大量远程npm install输出。完成后,Azure 仪表板中的部署历史将更新,显示最后一次活动部署的信息。

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (47)

现在,您的代码已部署到 Azure 网站,并且您的网站连接字符串指向您的 MongoLab Azure 应用服务,您已经准备好测试网站运行情况了。通过将浏览器指向yourappname.azurewebsites.net来启动它。Azure 做了很多正确的事情(UI/UX),并且提供了一些非常强大的选项和扩展功能!快速浏览网站仪表板(上述屏幕截图),您会发现有很多事情正在进行。

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (48)

有许多不同的配置选项,以及健康监控和一般信息(FTP 设置,网站 URL,使用度量等),所以请随意查看和探索。

我想要提到并简要介绍的最后一个服务是 Digital Ocean - digitalocean.com。Digital Ocean 是一个真正的虚拟专用服务器VPS)服务提供商,是一个让您尽可能接近底层的服务的很好的例子。这意味着 Digital Ocean 实际上并没有其他我们看到的服务所提供的所有花里胡哨的功能。然而,Digital Ocean 提供的是对您所创建的 Linux 服务器实例的直接、未经过滤的访问,在这种情况下被称为Droplets

Digital Ocean 允许您快速启动新的 Linux 虚拟服务器实例。他们提供非常有竞争力的价格,如果您需要快速获取 Linux 服务器,因为您只需要短时间内的一个,或者您想要启动自己的 Linux 服务器,用于托管生产环境,那么他们是一个很好的选择。唯一的缺点(如果我不得不这样说的话)是您必须对 Linux 非常熟悉,特别是对服务器管理和相关责任。

您可以在新的 Droplet 上使用 Git 非常容易地克隆您的项目,但新 Droplet 的实际原始性的一个例子是,Git 不会默认安装在服务器上。您需要在克隆存储库之前手动安装 Git。取决于您在创建新 Droplet 时决定克隆哪个镜像,您可能还需要安装和配置 Node.js 以及 MongoDB。幸运的是,Digital Ocean 在创建新服务器时提供了许多预定义的服务器供您选择,其中包括MongoDB,Express,AngularNode.jsMEAN)堆栈。除此之外,实际上启动您的应用程序只会在您当前登录的会话期间运行;一旦您退出登录,您的应用程序就会关闭。您需要进一步管理服务器,配置您的应用程序以作为服务运行。

Digital Ocean 允许您直接使用网站内的控制台访问工具连接到您的服务器,或者通过在自己的计算机上的终端直接使用 SSH 连接:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (49)

我之所以提到 Digital Ocean,是因为很多人会觉得这种原始的力量非常令人耳目一新,并且希望自己动手配置和维护他们的服务器。Digital Ocean 是一个很棒的服务,但并不适合每个人。我之所以特别想谈论它,是因为我觉得它完善了我们迄今为止所涵盖的服务列表。

我们已经涵盖了基于云的托管服务提供商的整个范围,并介绍了配置您的服务和部署项目代码。Nodejitsu 和 Heroku 是更多面向开发人员的优秀服务,并通过非常易于访问和流畅的用户界面赋予他们很大的权力。亚马逊和微软,作为行业巨头,代表了您可以期望的企业级服务提供商的实力和复杂性。Digital Ocean 是一个无花俏、接近底层的基于云的 VPS 提供商,牺牲了花里胡哨的功能,以换取对服务器的原始和直接访问。

我们涵盖的所有托管选项都很棒,但并不是唯一的选择。它们只是一个样本,但它们展示了云的力量!在几分钟内,几乎没有成本,您就可以配置一个环境,并让您的网站在线运行!

在下一章中,我们将介绍单页应用程序的概念以及流行的客户端开发框架和工具。

在本书中,我们专注于使用 Express.js 作为我们的首选 Web 框架,主要是因为它是 Node.js 最流行的 Web 开发框架之一。它已经存在了相当长的时间,并且被广泛使用。然而,还有许多可供选择的替代框架,我想向您介绍。其中一些框架比 Express.js 更强大和稳健,而另一些则与之相当,或者功能稍微少一些。

在本章中,我们将简要介绍以下框架:

  • Koa

  • Meteor

  • Sails

  • Flatiron

  • total.js

  • loopback

  • Hapi

最后,我们将使用 Hapi 中的一个框架来构建一个服务器 API。这个服务器 API 将在下一章中由 Angular 4 构建的客户端应用程序使用。构建这个 Web 应用程序的整个目的是研究如何根据项目选择框架,以及不同的框架有不同的特点,但都建立在 Node.js 的共同平台上。

Koa是由创建 Express.js 的同一团队设计的新的 Web 框架。Koa 的目标是更小、更有表现力,以及更坚固的 Web 应用程序基础。Express 框架的创建者 T J Holowaychuk 也是 Koa 的创建者,你可以看到它将大部分的功能都集中在生成器上,这是其他流行编程语言中的特性,比如 Python、C#和 Ruby。生成器是在 ECMAScript 6 中引入到 JavaScript 中的。生成器可以防止在 Node.js 开发过程中常见的回调地狱。Koa 具有轻量级的架构,因此它不包含任何中间件;相反,它将实现某些功能的选择留给开发人员。

有关 Koa 和示例实现的更多信息可以在其网站以及github.com/koajs/koa上找到。

Meteor是一个简单而完整的 Web 框架,旨在让任何技能水平的开发人员能够在较短的时间内构建强大的 Web 应用程序。

它具有一个方便的 CLI 工具,可以快速搭建新项目。

Meteor 提供了一些核心项目/库,例如 blaze、DDP、livequery 等,具有统一的构建系统。这简化了整个开发过程,并提供了一致的开发者体验。

Meteor 旨在通过在服务器端提供分布式数据协议和在客户端端提供透明的反应式渲染来构建实时应用程序。有关更多详细信息,请访问meteor.com/features

该框架的另一个显著特点是其广泛的包系统,名为atmosphere,其中包含了大多数常见应用程序的模块

用例。

它正在迅速获得关注,并且每天都变得越来越受欢迎。目前,它的 GitHub 存储库已经拥有超过 38,000 个星标!

有关 Meteor 的更多信息可以在其网站以及其官方 GitHub 存储库github.com/meteor/meteor上找到。

Sails是另一个用于使用 Node.js 构建 Web 应用程序的出色的 MVC 框架

有时会将自己与 Ruby on Rails 进行比较。与 Meteor 不同,Sails 是数据库无关的,因此您选择哪种数据存储方式并不重要。Sails 包括一些方便的脚手架工具,例如自动生成 RESTful API 的工具。Socket.io

一个用于 Node.js 的实时通信框架,内置在 Sails 中,因此,在应用程序中包含实时功能应该是轻而易举的。Sails 具有一些不错的生产级自动化功能,通常需要由诸如 Grunt.js 或 Gulp 之类的工具来处理(包括前端 CSS 和 JavaScript 的最小化和捆绑)。Sails 还包括应用程序的基本安全性和基于角色的身份验证,如果您需要该级别的功能。与 Express 相比,Sails 可以被认为是一个更全面的企业级框架,因为它几乎具有像 Rails 这样的流行框架的每个功能。Sails 网站位于sailsjs.com

有关 Sails 的更多信息可以在其网站上找到,以及其官方 GitHub 存储库github.com/balderdashy/sails

Flatiron是另一个 Node.js MVC Web 应用程序框架。Flatiron 与其他框架的不同之处在于其基于包的方法。由于它赋予了决定框架应该包含多少或多少的权力和自由,开发人员可以挑选并选择他们想要使用并包含在项目中的包。它通过提供一个强大的 ODM 来处理大部分基本数据管理职责和 CRUD,从而为您处理大部分繁重的工作。

有关 Flatiron 的更多信息可以在其网站上找到,以及其官方 GitHub 存储库github.com/flatiron/flatiron

另一个 Node.js HMVC 框架是 total.js。正如其名称所示,它提供了从 Web 应用程序到 IOT 应用程序的全面解决方案。你说一个功能,total.js都有;这就是total.js的特点。它支持大量功能,如图像处理、工作者、生成器、静态文件处理、站点地图、缓存机制、SMTP 等等。

减少使用第三方模块的需求。它在过去三年中得到了强大的社区支持,并且再次成为一个可以在功能开发的各个方面超越其他框架的强大竞争者。

关注所有更新的链接:www.totaljs.com/

IBM 和 StrongLoop 设计了最强大的现代 Node 框架之一,名为LoopBack。启动 API 服务器所需的工作量很小。LoopBack 内部有一个名为 API 资源管理器的客户端,它记录 API 并同时提供 API 测试。它是 Sails 框架的强有力竞争者,具有就绪的结构,并且在需要时完全可配置。它具有访问控制列表ACL)、移动客户端 SDK、基于约定的配置编码,当然还有 IBM 支持的团队,将长期维护项目。

您可以在以下链接开始使用 LoopBack:loopback.io/

Hapi是沃尔玛在线移动网站背后团队的成果。构建该网站的团队开发了一套丰富的 Node.js 实用程序和库,可以在Spumko umbrella下找到。考虑到沃尔玛网站在任何给定日子都会收到大量流量,沃尔玛实验室的团队在涉及 Node.js 开发和最佳实践时无疑是游刃有余。Hapi 是从现实世界的试错中诞生的 Web 框架。Hapi 网站位于hapijs.com

有关 Hapi 的更多信息可以在其网站上找到,以及其官方 GitHub 存储库github.com/spumko/hapi。在下一节中,我们将在 Hapi 框架中实现一组 API。

在之前的章节中,我们学习并实现了 Express 框架。Express 社区将其称为最简档的框架,因此它提供了性能优势。对于构建任何应用程序,选择正确的框架是应用程序可扩展性的最重要因素之一。在 Hapi 的情况下,它具有不同的路由机制,通过其可配置的代码模式提高了应用程序的性能。开发人员始终建议考虑框架提供的所有优势和劣势,以及应用程序的功能实现和长期目标。让我们通过一个小型原型来了解 Hapi 框架。

以下步骤提供了使用电话簿示例逐步学习 Hapi 框架实现的经验。建议在阅读时进行编码以获得更好的学习效果。

创建一个名为 phone book-API 的目录,并通过cd phonebook-api导航到该目录。使用npm init初始化一个 node 项目,并完成npm提供的问卷调查。使用以下命令安装 Hapi 框架:

npm install hapi --save

首先要编写的文件必须是一个server文件,所以让我们创建一个server.js。使用hapi框架启动server所需的最小代码如下:

const hapi = require('hapi');const server = new hapi.Server();server.connection({ host: 'localhost', port: 8000, routes: { cors: true }});// Start the serverserver.start((err) => { if (err) { throw err; } console.log('Server running at:', server.info.uri);});

在审查了前面的代码之后,我们可以观察到hapi通过首先配置所有必需的数据来启动其服务器。它以主机和端口作为输入,然后最终启动服务器。如果我们将其与 express 进行比较,express 首先需要一个回调作为输入,然后才是监听部分。

下一个重要的步骤是创建路由。在任何框架中实现路由时,始终建议遵循模块化,以便长期维护代码。话虽如此,让我们创建一个routes.js文件。由于我们不打算使用诸如 MongoDB 或 MySQL 之类的数据库,让我们为支持数据源创建一个名为phonebook.jsonjson文件。让我们在json文件中创建以下数据:

{ "list": [ { "phone_no": 1212345678, "name": "Batman" }, { "phone_no": 1235678910, "name": "Superman" }, { "phone_no": 9393939393, "name": "Flash" }]}

我们的 API 目录结构如下:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (50)

hapi的配置代码模式随处可见,甚至用于创建路由。让我们通过在下面的片段中添加一个简单的GET方法和它的处理程序来理解它:

let phonebook = require('./phonebook');module.exports = [{ method: 'GET', path: '/phone/list', config: { handler(request, reply) { reply({ message: "phonebook of superheroes", data: phonebook.list }); } }}]

上面的片段显示了创建路由所需的最小配置。它包括request方法,可以是'GET''POST'等;用于 URL 导航目的的 URL 路径;以及包含请求处理程序的config属性。此处理程序用于在收到请求时编写各种业务逻辑。

现在,在server.js中包含路由文件,并在服务器启动之前将路由分配给hapi服务器。因此,总结一下,在server.js中有以下代码:

const hapi = require('hapi');const server = new hapi.Server();const routes = require('./routes');server.connection({ host: 'localhost', port: 8000, routes: { cors: true }});//Add the routesserver.route(routes);// Start the serverserver.start((err) => { if (err) { throw err; } console.log('Server running at:', server.info.uri);});

让我们在浏览器中访问路由并查看响应:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (51)

同样,我们可以添加、更新和删除电话簿中的条目。我们的routes.js将如下所示:

let phonebook = require('./phonebook');module.exports = [{ method: 'GET', path: '/phone/list', config: { handler(request, reply) { reply({ message: "phonebook of superheroes", data: phonebook.list }); } }}, { method: 'POST', path: '/phone/add', config: { handler(request, reply) { let payload = request.payload; phonebook.list.unshift(payload); reply({ message: "Added successfully", data: phonebook.list }); } }}, { method: 'PUT', path: '/phone/{phno}', config: { handler(request, reply) { let payload = request.payload; let phno = request.params.phno; var notFound = []; for (let i = phonebook.list.length - 1; i >= 0; i--) { if (phonebook.list[i].phone_no == phno) { phonebook.list[i].name = payload.name; reply(phonebook.list); return; } else { notFound.push(i); } } if (notFound.length == phonebook.list.length) { reply('not Found'); return; } } }}, { method: 'DELETE', path: '/phone/{phno}', config: { handler(request, reply) { let phno = request.params.phno; var notFound = []; for (let i = phonebook.list.length - 1; i >= 0; i--) { if (phonebook.list[i].phone_no == phno) { phonebook.list.splice(i, 1); reply({ message: "Delete successfully", data: phonebook.list }); return; } else { notFound.push(i); } } if (notFound.length == phonebook.list.length) { reply('not Found'); return; } } }}];

我们需要使用浏览器扩展来测试前面的 REST API。POSTMAN 是 REST API 调用的流行扩展之一。请参考第八章,了解 POSTMAN 的详细信息。

哇!我们的服务器 API 已经准备就绪。在下一章中,我们将通过创建一个前端应用程序来使用这些 API 调用。

尽管我们在本书中专门使用了 Express.js,但在使用 Node.js 创建 Web 应用程序时还有许多其他选项可供选择。我们研究了

本章介绍了其中一些选项,包括 Meteor、Sails、Hapi、Koa 和 Flatiron。每个框架都有其自身的优势和劣势,以及对 Web 应用程序所需的标准功能的独特方法。

就是这样,伙计们!我希望使用 Node.js 和 MongoDB 构建 Web 应用程序的不同方面能够带领读者以渐进的方式学习和开发一个令人惊叹的想法。嗯,这只是个开始。我建议您关注您自己应用程序中将要使用的所有技术或库的开发者社区。

使用 Node.js 进行 Web 开发的美妙之处在于如何完成单个任务没有意见的短缺。MVC 框架也不例外,从本章可以看出,有很多功能强大且功能丰富的框架可供选择。

在本章中,我们将从前端的角度来看待 Web 应用程序开发,特别是单页应用程序SPA),也称为厚客户端应用程序。使用 SPA,大部分的呈现层被卸载到浏览器中,浏览器负责呈现页面、处理导航,并向 API 发出数据调用。

在本章中,我们将涵盖:

  • 为什么要使用前端框架,比如 Backbone.js、Ember.js 或 Angular.js?

  • 单页应用程序到底是什么?

  • 受欢迎的前端开发工具,如 Grunt、Gulp、Browserify、SAAS 和 Handlebars

  • 前端的测试驱动开发

我们使用框架来提高我们的生产力,让我们保持理智,并且通常来说,让我们的开发过程更加愉快。在本书的大部分章节中,我们使用了 Node.js 的 Express.js MVC 框架。这个框架允许我们组织我们的代码,并且它将大量样板代码抽象出来,释放出我们的时间来专注于我们的自定义业务逻辑。同样的情况也适用于应用程序的前端。任何复杂的代码最终都需要得到适当的组织,我们需要使用一套标准的可重用工具来完成常见的任务。Express.js 在编写我们的 Node.js 后端代码时让我们的生活变得轻松。同样,也有许多受欢迎的前端框架可以依赖。

复杂的 Web 应用程序的当前趋势是模拟桌面应用程序,并远离传统网站的感觉。对于传统网站,与服务器的每次交互都需要一个完整的页面后退,这使得一个完整的往返。随着我们的 Web 应用程序变得更加复杂,发送和从服务器检索数据的需求增加了。

如果每次都依赖完整的页面后退,我们需要方便其中一个请求;我们的应用程序会感觉迟钝和无响应,因为用户将不得不等待每个请求的完整往返。用户现在对他们的应用程序有更多的需求,如果你考虑一下我们编写的应用程序,点赞按钮就是一个完美的例子。只是因为我们想要增加一个计数器,就必须向服务器发送完整的页面后退,这似乎是很多不必要的开销。幸运的是,我们能够很容易地使用 jQuery 和 AJAX 来纠正这一点。这是单页应用程序如何工作的一个完美例子(只是在一个更大的规模上)。

第一个突出的单页应用程序的一个很好的例子是谷歌的 Gmail。Gmail 为您提供了一个类似于 Microsoft Outlook 或任何传统基于桌面的电子邮件客户端的界面。用户与应用程序的交互感觉就像与桌面应用程序一样响应——页面永远不会重新加载,您可以轻松切换应用程序中的窗格和选项卡,并且数据在实时中不断刷新和更新。

创建 SPA 通常涉及将单个 HTML 页面作为应用程序的源,该页面加载所有必要的 JavaScript 以触发一系列事件,包括:

  • 引导应用程序:这意味着通过 AJAX 连接到服务器以下载必要的启动数据

  • 根据用户操作呈现屏幕:这意味着监视用户触发的事件并操作 DOM,以便隐藏、显示或重绘应用程序的部分,从而模拟桌面应用程序的感觉

  • 与服务器通信:这意味着使用 AJAX 不断地向服务器发送和接收数据,从而保持通过浏览器的有状态连接的幻觉

在决定为下一个大型前端项目选择哪个前端框架时,决策过程可能会让人不知所措!跟踪所有不同的框架以及每个框架的利弊似乎是徒劳的练习。幸运的是,人们已经回应了这一呼吁,有一个方便的网站不仅演示了几乎每个框架编写的相同应用程序,而且还为每个框架提供了完整的注释源代码!

TodoMVC 项目,todomvc.com,是一个专注于创建一个简单的单页面待办事项应用程序的网站,使用了每个经过验证的 JavaScript MVC 框架编写;甚至有一个用原生 JavaScript 编写的!因此,跳入一个框架的最简单方法是查看其 TodoMVC 代码示例:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (52)

一定要花些时间查看网站并深入了解每个特色框架。通过以完全不同的方式编写相同的代码,您可以对不同的框架有一个很好的感觉;没有两个是相同的,最终,您需要评估并找出您更喜欢哪个以及原因。

为了简洁起见,我将专注于我个人喜欢并认为处于当前领先地位的三个。

Backbone.js是一个非常轻量级(生产中为 6.5 KB)的 MV*框架,已经存在了几年。它拥有一个非常庞大的用户群,并且许多大型网络应用程序都是使用这个框架编写的:

  • 今日美国

  • Hulu

  • 领英

  • Trello

  • Disqus

  • 可汗学院

  • 沃尔玛移动

如果您熟悉 jQuery 并且已经使用它一段时间,并且想要开始改进您的代码组织和模块化,那么 Backbone.js 是一个很好的起点。此外,Backbone.js 需要 jQuery 并且与之紧密集成,因此在您逐渐进入这个前端开发的新世界时,这就少了一件要担心学习的事情。

Backbone.js 的基本思想是模型、集合、视图和路由器。看一下以下几点:

  • 模型是存储和管理应用程序中所有数据的基本元素

  • 集合存储模型

  • 视图将 HTML 呈现到屏幕上,从模型和集合中检索动态数据

  • 路由器为应用程序的 URL 提供动力,使每个应用程序的各个部分都有自己独特的 URL(实际上不加载实时 URL),最终将整个应用程序联系在一起

由于 Backbone.js 非常轻量级,可以非常快速地组合一个非常小而简单的示例代码集:

var Person = Backbone.Model.extend(); var PersonView = Backbone.View.extend({ tag: 'div', render: function() { var html = [ this.model.get('name'), '<br/>', this.model.get('website') ].join(''); this.$el.html(html); return this; } }); var person = new Person({ name: 'Jason Krol', website: 'http://kroltech.com' }), view = new PersonView({ model: person }); $('body').append(view.render().el); 

需要注意的一件事是,Backbone.js 本质上是如此轻量级,以至于它不包括您期望立即使用的大多数功能。正如您在前面的代码中所看到的,在我们创建的View对象中,我们必须提供一个手动为我们呈现 HTML 的render函数。因此,许多人都对 Backbone.js 望而却步,但其他人则因其为开发人员提供的原始力量和灵活性而拥抱它。

传统上,您不会像在前面的示例中那样将所有代码放入单个文件中。您将把模型、集合和视图组织到单独的文件夹中,就像我们在 Node.js 应用程序中组织代码一样。将所有代码捆绑在一起将是构建工具的工作(这将在本章后面讨论)。

您可以通过访问其官方网站backbonejs.org了解更多关于 Backbone.js 的信息。还要不要忘了在 TodoMVC 网站上检查待办应用程序的 Backbone.js 实现!

我在 GitHub 上维护了一个存储库,其中有一个使用我们在本书中涵盖的完整堆栈的样板 Web 应用程序的完整代码,以及用于前端的 Backbone.js 和 Marionette。欢迎访问github.com/jkat98/benm(Backbone、Express、Node 和 MongoDB)。

Ember.js 自称为创建雄心勃勃的 Web 应用程序的框架。Ember 的目标是针对相当大规模的单页应用程序,因此使用它构建非常简单的东西可能看起来有些大材小用,但这当然是可行的。一个公平的评估是查看 Ember 库的生产文件大小,大约为 90 KB(而 Backbone.js 为 6.5 KB)。也就是说,如果你正在构建一个非常强大的、代码量很大的东西,那么额外的 90 KB 对你来说可能并不是什么大问题。

以下是一个使用 Ember.js 的非常小的示例应用程序:

var App = Ember.Application.create(), movies = [{ title: "Big Trouble in Little China", year: "1986" }, { title: "Aliens", year: "1986" }]; App.IndexRoute = Ember.Route.extend({ model: function() { return movies; } }); <script type="text/x-handlebars" data-template-name="index"> {{#each}} {{title}} - {{year}}<br/> {{/each}} </script> 

Ember.js 的代码看起来与 Backbone.js 的代码有些相似,因此并不奇怪,许多经验丰富的 Backbone.js 开发人员发现随着对更强大解决方案的需求增加,他们开始迁移到 Ember.js。Ember.js 使用熟悉的项目,包括视图、模型、集合和路由,以及一个Application对象。

此外,Ember.js 具有组件功能,这是它更强大和受欢迎的功能之一。通过组件,你可以创建小型、模块化、可重用的 HTML 组件,根据需要将其插入到应用程序中。使用组件,你基本上可以创建自己的自定义 HTML 标签,其外观和行为完全按照你定义的方式进行定义,并且可以在整个应用程序中轻松重用。

使用 Ember.js 开发完全遵循约定。与 Backbone.js 不同,Ember.js 试图消除大量样板代码,并为你做出一些假设。因此,你需要以一定的方式进行操作,控制器、视图和路由需要遵循一定的命名约定。

Ember.js 网站提供了令人难以置信的在线文档和入门指南。如果你对 Ember.js 想了解更多,请访问emberjs.com/guides/。另外,不要忘记查看 TodoMVC 的实现!

从未有过如此激烈的竞争来适应新的 JavaScript 技术。现在是 JavaScript 的最佳时机。Facebook 团队有一个强大的竞争者,名为React.js。因此,与 Angular 等其他 MVC 框架不同,React.js只是 Model-View-Controller 中的视图。它轻巧且渲染速度惊人。代码的基本封装单元称为组件。这些组件组合在一起,形成一个完整的 UI。让我们使用 es6 类为打招呼的示例创建一个简单的组件,如下所示:

class Greet extends React.Component { render() { this.props.user = this.props.user || 'All'; return ( < div > < h4 > Greeting to { this.props.user }! < /h4> < /div>); }}

前面的片段创建了一个简单的类来向用户打招呼。我们扩展了React.component以提供其公共方法和变量在Greet类中的访问权限。每个创建的组件都需要一个render方法,其中包含了相应组件的 html 模板。props是一个公共变量,用于从容器传递数据到子组件,依此类推。

现在,我们需要在 HTML 文档中加载组件。为了做到这一点,我们需要在React.js库全局提供的reactDOMAPI 下注册。可以按照以下方式完成:

ReactDOM.render( <Greet user="Developers" />, document.getElementById('root'));

ReactDOM对象的render方法需要两个参数。首先是根节点,其次是根元素。根节点是用于声明不同父组件的主机元素。根元素<div id="root" />写在我们的index.html的 body 中,用于承载React组件的整个视图。

reactDOM负责创建虚拟 DOM 并观察每个组件的任何更改。只重新渲染已更改/操作的部分,保持其他组件不变。这提高了应用程序的性能。我们所了解的组件中的更改也称为组件的状态,由不同的库(如reflux.jsredux.js)维护。另一个重要的特性是使用 props 进行单向绑定。props 只是将数据传递给视图的一种方式。一旦搭建和数据流设置好,React 项目就能提供出色的可扩展代码和复杂项目的易维护性。React.js 上有大量的项目列表github.com/facebook/react/wiki/sites-using-react。一旦你熟悉了 React web 应用程序,你就可以轻松切换到 React Native,创建令人惊叹的原生移动应用程序。

Angular 之所以如此火爆,主要是因为它是由谷歌(开源)构建的。Angular 基本上就像是给 HTML 加上了类固醇。您创建的应用程序和页面使用我们都习惯的常规 HTML,但它们包括许多新的和自定义指令,扩展了 HTML 的核心功能,赋予了它强大的新功能。

Angular 的另一个伟大特性是,它是由一群经验丰富的非 Web 开发人员构建的,因为它经过了严格测试并支持依赖注入。这是一个框架,它不会让创建复杂的 Web 应用程序感觉像传统的 Web 开发。

然而,不要误解;JavaScript 在 Angular 的开发中仍然扮演着重要角色。新的 Angular 2.0,现在是 4.0,已经成为当今最广泛使用的框架之一。它不仅引入了 TypeScript 来实现代码的语法模块化,还提供了新的语义,如组件(而不是控制器)、管道、生命周期钩子等功能。让我们通过实现一个简单的客户端应用程序来更多了解 Angular,该应用程序使用了上一章中创建的 phone-API。

与 Angular.js 不同,Angular 2.0 是一个完整的框架,而不是一个可包含的单个文件。这个框架带有许多功能,如 rxJs、TypeScript、systemJs 等,提供了代码的集成开发。Angular 团队提供了简单的步骤来设置 Angular 2.0 种子应用程序。然而,开发人员也可以手动创建 Angular 项目所需的最小文件列表,尽管这并不被推荐,而且也很耗时。对于我们的客户原型,我们将按照angular.io提供的 Angular 快速入门步骤,然后将我们的功能集成到其中。让我们按照这些步骤进行。

前往angular.io/guide/quickstart并按照三个步骤进行操作:

  1. 安装Angular/cli。使用以下命令进行全局安装:
npm install -g @Angular/cli
  1. 使用以下命令创建一个名为phonebook-app的新项目:
ng new phonebook-app
  1. 通过cd phonebook-app进入phonebook-app目录。最后,使用ng serve --open来启动应用程序。ng serve命令用于在开发过程中监视更改、转译和重建应用程序。--open是一个可选的命令行参数,用于在浏览器选项卡中打开应用程序。在这里,如果出现错误,请确保通过npm install命令重新安装 npm 包。

谷歌对转译的定义是将一种语言的源代码转换为另一种具有相似抽象级别的语言的过程。简单来说,这意味着我们将使用 TypeScript 编写代码,然后将其转换为 JavaScript(因为它在浏览器中运行)。

解释项目中的每个文件超出了本书的范围,但是,我们主要关注 Angular 的基本构建块以便开始。我们目前的主要关注点是src/app目录。在创建组件之前,让我们为我们的应用程序添加 Twitter 的 bootstrap 链接以进行基本样式设置.在我们的src目录中,我们有index.html。在header标签中插入以下 HTML 代码:

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

TypeScript 是 JavaScript 的超集,它编译成干净的 JavaScript 输出。

正如其名称所示,TypeScript 意味着类型语言,这要求我们在代码中声明数据类型。这可以在不同的语言中看到,比如 Java、C#等。在 TypeScript 中,变量的声明使用冒号注释,如下所示:

let name : string = "Bruno";

其次,JavaScript 包含大多数(但不是全部)面向对象的特性。因此,它可以在语义上实现,但在语法上没有规定。例如,面向对象编程的一个重要特性是封装;让我们来比较一下:

TypeScript 代码JavaScript 代码(ES5)

|

class GreetTheWorld { greet() { return "Hello World"; } }

|

var GreetTheWorld = (function () { function GreetTheWorld() { } GreetTheWorld.prototype.greet = function () { return "Hello World"; }; return GreetTheWorld; }());

|

最终,使用 TypeScript 作为其主要脚本语言的框架将其编译(转换)为 es5 JavaScript。新的 es6 充当了 es5 和 TypeScript 之间的桥梁。到目前为止,es6 已逐渐将 TypeScript 功能实现到 JavaScript 中,例如类。了解这一点,我们将很容易理解在 Angular 框架中使用 TypeScript 的用法。要进行更深入的学习,我们可以参考www.TypeScriptlang.org/docs/home.html

Angular 2.0 提供了基于组件的方法来实现代码的重用性和可维护性。通过将每个功能组件化,可以轻松复制或重用它们。让我们为项目创建一个框架,以便我们可以从项目中挑选出这些组件:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (53)

在我们的项目结构中,我们已经有app.component.ts作为我们的组件文件。我们已经在组件元数据的selector属性中指定了我们的app-root组件。这个文件将用于处理根或父元素。根据前面的框架,这个文件包含了app-root组件:

import { Component } from '@Angular/core';@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css']})export class AppComponent {}

确保每个组件都附加了一个类,这样我们就可以导出一个component类,并在app.modules.ts中将其注册为 Angular 组件。为了简洁起见,我们遵循标准的命名约定,即将类名的第一个字母大写,然后导入作为组件的 Angular 类前缀。因此,在 app module.ts文件中,我们有以下代码:

import { BrowserModule } from '@Angular/platform-browser';import { NgModule } from '@Angular/core';import { AppComponent } from './app.component';@NgModule({ declarations: [AppComponent], imports: [BrowserModule], providers: [], bootstrap: [AppComponent]})export class AppModule {}

一旦在NgModule中注册,我们需要将根组件传递给 bootstrap 方法/属性。Bootstrap 处理加载到我们决定使用的平台上。

根据设计,在应用组件中,我们只有一个轮廓,它将作为其他组件的容器,因此,让我们创建一个模板。创建一个带有 HTML 代码的文件app.component.html和其相关的 css 文件,如下所示:

app.component.html

<div class="outter"><!-- <subscribe></subscribe>--><!-- <list></list> --></div>

app.component.css

.outter { padding: 20 px; border: solid 2 px# AAA; width: 200 px;}

运行此代码后,我们在浏览器中得到了一个简单的轮廓。使用ng serve来运行应用程序。

为了简要了解数据流,让我们比较一下 MVC 是如何在 Angular 中实现的。考虑以下图表:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (54)

该图包含了 Angular 2.0 中的基本块,而不是所有的特性。该模型包含静态数据或同步数据类,这些类在组件中被扩展或导入。组件充当包含业务逻辑的控制器。每个组件都有元数据,它将其与模板连接起来。最后,数据绑定到component类变量,并在模板中可用。现在,对于我们的应用程序,我们需要显示联系人数据列表;因此,我们将在新的组件文件list.component.ts中使用数据绑定的这一特性。该文件提供了在电话簿中列出联系人的逻辑。始终遵循包含组件的三个步骤。

  1. 为列表组件创建元数据和类以及其模板。因此,我们有list.component.ts,其中包含以下代码:
import { Component } from '@Angular/core';@Component({ selector: 'list', templateUrl: './list.component.html'})export class ListComponent { public phoneList = [] constructor() { this.phoneList = [{ name: 'Superman', phno: 1234567890 }, { name: 'Batman', phno: 2222222890 }] }} 

它还包括其模板list.component.html

<div> <div class="list-group"> <div class="list-group-item list-group-item-info"> Contact list </div> <a href="#" *ngFor="let data of phoneList;" class="list-group-item"> {{data.name}} <span class="badge">{{data.phone_no}}</span> </a> </div><div>
  1. 将组件包含在app.module中,并在声明属性中注册,如下所示:
import { ListComponent } from './list.component';@NgModule({ declarations: [ AppComponent, ListComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent]})
  1. 第三步是包含用于呈现组件的模板。我们已经有了我们的根组件root-app。因此,取消app.component.htm中的list标记。可以按照以下方式完成:
<div class="outter"><!-- <subscribe></subscribe>--> <list>&lt;/list></div>

上述片段包含绑定到phoneList属性的静态数据,因此相应地呈现,这是一个同步操作。现在,如果我们需要显示异步数据怎么办?让我们消耗我们的电话簿 API 并为我们的客户端创建一个 HTTP 服务,以便我们可以显示异步数据。让我们创建一个名为phonebook.service.ts的服务文件。该文件包含了获取和设置数据所需的所有 HTTP request 方法。现在,让我们创建一个getContactlist方法,它将获取服务器上存在的所有联系人数据。所需的代码如下:

import { Injectable } from '@Angular/core';import { HttpClient } from '@Angular/common/http';import 'rxjs/add/operator/map'@Injectable()export class PhonebookService { constructor(private http: HttpClient) {} getContactList() { return this.http.get('http://localhost:8000/phone/list') }}

在这里,我们导入了HttpClient服务,用于从浏览器发出 XML HTTP 请求。Injectable函数是必需的,以使此文件成为服务提供者。在可注入的元数据中没有提供元数据,因为 Angular 本身将发出元数据。接下来,我们将通过在组件的constructor中注入它来使用此服务。我们在构造函数中传递了一个参数化值,这被称为依赖注入。

将当前代码修改为list.components.ts的以下代码:

import { Component } from '@Angular/core';import { PhonebookService } from './phonebook.service';@Component({ selector: 'list', templateUrl: './list.component.html'})export class ListComponent { public phoneList = [] constructor(private _pbService: PhonebookService) { this._pbService.getContactList() .subscribe((response) => { this.phoneList = response['data']; }) }}

最后,我们需要在app.modules的提供者下注册服务,因为我们在整个应用程序中都在使用它。在这一步,我们需要在app.modules中有以下代码:

import {HttpClientModule} from '@Angular/common/http';import {PhonebookService} from './phonebook.service';

并将其包含在ngModule的提供者列表中:

@NgModule({ declarations: [ ListComponent, AppComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [PhonebookService], bootstrap: [AppComponent]})

使用Httpclient服务,我们首先需要在NgModules中注册httpModule

还要确保我们的电话簿 API 节点服务器已经在运行。如果没有,请使用npm start启动它,一旦我们启动ng serve --open,我们将从服务器接收超级英雄联系人列表。

最后但同样重要的是,我们需要创建一个名为add的表单组件,用于添加新的电话记录。在 Angular 中,有两种构建表单的方式:模板驱动表单和响应式表单。由于响应式表单是 Angular 的一个新特性,我们将实现它。简而言之,响应式表单通过代码逻辑控制表单指令,而不是在部分中。让我们通过为我们的电话簿应用程序实现它来检查它。

要创建的新组件文件是add.component.ts及其.html文件,如下所示:

import { Component, Output, EventEmitter} from '@Angular/core';import { PhonebookService} from './phonebook.service';import { FormControl, FormGroup, Validators} from '@Angular/forms';@Component({ selector: 'add', templateUrl: './add.component.html'})export class AddComponent { @Output() onAdded = new EventEmitter < Object > (); public newRecordForm = new FormGroup({ name: new FormControl(), phone_no: new FormControl(0, [Validators.required, Validators.minLength(10)]) }); constructor(private _pbService: PhonebookService) { this.resetForm(); } resetForm() { this.newRecordForm.reset(); } onSubmit() { if (this.newRecordForm.valid) { const payload = this.newRecordForm.value; this._pbService.postContactList(payload).subscribe((response) => { let newListData = response['data']; this.onAdded.emit(newListData); this.resetForm(); }) } }}

在上述代码中,您可能会注意到一些 Angular 的新特性。它们的解释如下:

  • FormControlFormGroup:导入FormControl,它是一个指令,接受模型数据作为输入,并创建表单元素的实例。这些表单元素可以使用FormGroup进行聚合或分组。

  • Validators:类验证器提供了验证表单元素的方法。它们作为第二个可选参数传递给FormControl,以配置元素的验证。根据应用的条件,它设置一个布尔属性作为有效。

  • OutputEventEmitter将在后面解释。所以一旦我们的代码准备好了,让我们创建其模板如下:

<form [formGroup]="newRecordForm" (ngSubmit)="onSubmit()" novalidate> <div class="form-group"> <label class="center-block">Name: <input class="form-control" formControlName="name"> </label> <label class="center-block">Phone no: <input class="form-control" formControlName="phone_no" ngClass=""> </label> </div> <input type="submit" name="submit"></form>

在上述代码中,我们可以调查formGroup是否是一个以newRecordForm作为输入的指令。我们有一个名为ngSubmit的事件处理程序,其中包含一个名为onSubmit的公共方法。这个方法负责将联系人的详细信息保存到我们的服务器上。这里的最后一个要求是将newRecordForm的属性映射到formControlName,以便代码逻辑映射到模板中的适当元素。像form-control这样的类用于 HTML 元素,只是基本设计的 bootstrap 类。

现在,我们需要在app.component.html中再添加一个更改:

<div class="container"> <add (onAdded)="onAddedData($event)"></add> <list [phoneList]="listData"></list></div>

我们还需要在app.component.ts中添加一个更改,如下所示:

export class AppComponent { public listData = []; onAddedData(newListData: any) { this.listData = newListData; }}

这种变化是为了在两个组件之间进行通信。这种通信是为了什么?

当我们在add组件中添加新记录时,它需要将新添加的数据发送到列表组件,但不能直接绑定,因此我们使用@output将数据绑定回父组件。EventEmitter用于通过应用模板中的绑定发出响应数据,如下所示:

<add (onAdded)="onAddedData($event)"></add>

在这里,应用组件充当它们之间的桥梁。一旦父组件在AddedData方法中接收到数据,它通过listData变量的@input绑定与其子列表组件进行通信。

观察浏览器中的变化;form组件向列表中添加新数据,我们的电话簿应用程序已经准备好进行第一个原型。

前端框架最近已经带有了一些宗教色彩。发表关于特定框架的负面评论或批评,很可能会遭到支持者的抨击。同样,对特定框架进行积极的讨论,也很可能会遭到关于不同框架如何更好地处理相同主题的攻击。在决定哪种框架适合您和/或您的项目时,通常会涉及个人偏好。在 TodoMVC 网站上展示的每个框架都可以清楚地以其独特的方式实现相同的目标。花一些时间来评估一下,并自行决定!

由于单页应用程序的复杂性,前端开发人员需要熟悉许多日常甚至有时是每分钟的任务的工具套件。

构建工具就是它听起来的样子——用于构建应用程序的工具。当前端开发人员创建和维护应用程序时,可能需要重复执行一些任务,每次文件更改和保存时都需要。使用构建工具,开发人员可以通过将责任转移到可以监视文件更改并执行所需的任意数量任务的自动化任务管理器来释放时间和精力。这些任务可能包括以下任意数量的任务:

  • 串联

  • 缩小

  • 丑化和混淆

  • 操纵

  • 依赖安装和准备

  • 自定义脚本触发

  • 并发观察者

  • 服务器启动

  • 测试自动化

今天一些更受欢迎的构建工具包括 Grunt、Gulp 和 Broccoli。Grunt.js 已经存在多年,并且在开发社区中非常成熟。Gulp 和 Broccoli 相对较新,但迅速获得了认可,并且与 Grunt 的工作方式有所不同。使用 Grunt,您可以使用配置文件定义和管理任务,而使用 Gulp 和 Broccoli,您可以编写 Node.js 代码并使用流的原始力量。许多开发人员发现使用 Grunt 的配置文件相当混乱和令人沮丧,并且发现使用 Gulp 是一种令人耳目一新的改变。但是,很难否认 Grunt 的历史和流行程度。

这三个都是功能丰富的插件生态系统,可以帮助您自动化构建过程中几乎一切和任何事情。

以下是典型 Grunt build命令的示例输出:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (55)

在典型的单页面应用程序中,构建管理器可以负责下载和安装依赖项,将多个 JavaScript 文件合并为单个文件,编译和 shimming Browserify 模块,对 JavaScript 文件进行语法错误的 linting,将 LESS 文件转换为生产就绪的 CSS 文件,将文件复制到运行时目标,监视文件以重复任何任务,并最后,在代码更改时运行适当的测试-所有这些都可以通过单个命令完成!

Grunt 可以使用npm进行安装,并且应该全局安装。执行以下命令在您的机器上安装 Grunt CLI:

 $ npm install -g grunt-cli

有关更多信息,请参考官方 Grunt.js 网站上的入门指南gruntjs.com/getting-started

此外,还可以查看 Gulp 和 Broccoli,获取更多信息:

实际上有数百万个 JavaScript 库可用,可以帮助您处理从 DOM 操作(jquery)到时间戳格式化(moment.js)的一切。管理这些库和依赖项有时可能会有点麻烦。对于前端,首选的依赖管理器是 Bower.io。

Bower 的工作方式与 npm 几乎完全相同;它管理bower.json文件中的软件包。在前端工作时(例如,您需要一个已知的 JavaScript 库或插件,例如 underscore),只需执行bower install underscore,JavaScript 文件将下载到项目中的本地bower_components文件夹中。从那里,您可以通过更新构建过程或简单地复制文件并在 HTML 中包含脚本标签来自动包含这些脚本;然后,您就可以开始了。

Bower 可以使用 npm 进行安装,并且应该全局安装。执行以下命令在您的机器上安装 Bower:

 $ npm install -g bower $ bower install jquery bower cached git://github.com/jquery/jquery.git#2.1.0 bower validate 2.1.0 against git://github.com/jquery/jquery.git#* bower new version for git://github.com/jquery/jquery.git#* bower resolve git://github.com/jquery/jquery.git#* bower download https://github.com/jquery/jquery/archive/2.1.1.tar.gz bower extract jquery#* archive.tar.gz bower resolved git://github.com/jquery/jquery.git#2.1.1 bower install jquery#2.1.1 jquery#2.1.1 bower_components/jquery

访问 Bower.io 网站(bower.io),了解更多信息,以及可以通过bower install安装的完整脚本目录。

在编写大型 JavaScript 应用程序时,关键是保持源代码组织良好和结构合理。不幸的是,JavaScript 并不天生很好地支持模块化代码的概念。为了解决这个问题,存在两个流行的库,允许您编写模块化代码,并且只依赖于每个代码片段中需要的模块。

前端设计模式的必读资源是 Addy Osmandi 的Learning JavaScript Design Patterns,您可以通过访问以下 URL 免费阅读:

addyosmani.com/resources/essentialjsdesignpatterns/book/

Require.js 和 Browserify 是当今最流行的两种模块加载器。每种都有非常独特的语法和自己的一套好处。Require.js 遵循异步模块定义,这意味着每段代码都需要定义自己的依赖关系。就我个人而言,我以前使用过 Require.js,并且最近发现我真的很喜欢使用 Browserify。Browserify 的一个优势是它使用与 Node.js 相同的模块化模式;因此,使用 Browserify 编写前端代码与使用 Node 的感觉相同。您在前端使用module.exportsrequire,而且如果在同一个应用程序中在 Node 和前端之间来回切换,您不必担心语法上下文切换。

与之前提到的流行 MVC 框架之一结合使用模块加载器几乎是必需的,因为两者像花生酱和果冻一样搭配得很好!

有关更多信息,请访问以下链接:

幸运的是,我们已经在本书的整个过程中涵盖了 HTML 模板渲染引擎的概念。这些主题和概念直接转移到前端应用程序。在浏览器中有许多不同的 HTML 模板引擎可供选择。

许多模板引擎将基于 mustache,这意味着它们使用{{}}进行合并变量。Handlebars 目前是我个人最喜欢的,主要是因为它在应用程序的后端和前端都能很好地工作,我真的很喜欢使用它的帮助程序。Underscore.js 具有内置的lite模板渲染引擎,可与 Backbone.js 一起使用,但其语法使用<%%>(与经典 ASP 或 ASP.net MVC Razor 语法非常相似)。通常,大多数前端 MVC 框架允许您自定义模板渲染引擎并使用任何您想要的引擎。例如,Backbone.js 可以很容易地设置为使用 Handlebars.js,而不是默认使用 Underscore.js。

以下是当前可用的一些前端模板渲染引擎的简单示例列表:

其中一些将在后端和前端都起作用。

在 CSS 文件中使用变量和逻辑的想法听起来像是梦想成真,对吧?我们还没有完全实现(至少在浏览器中);但是,在我们的构建步骤中有一些工具可以让我们在 CSS 文件中使用变量和逻辑并对其进行编译。LESS 和 SASS 是目前最流行的两种 CSS 转译器。它们的行为几乎相同,只是在语法和功能上略有不同。最大的区别是 LESS 是使用 JavaScript 编写的

Node 使用 JavaScript,而 SASS 使用 Ruby;因此,它们在您的计算机上运行需要不同的要求。

以下是一个 SASS 样式表文件示例:

$sprite-bg:url("/images/editor/sprite-msg-bg.png"); @mixin radius($radius) { -moz-border-radius: $radius; -webkit-border-radius: $radius; -ms-border-radius: $radius; border-radius: $radius; } .upload-button { border-bottom: solid 2px #005A8B; background: transparent $sprite-bg no-repeat; @include radius(4px); cursor: pointer; } #step-status { color:#dbdbdb; font-size:14px; span.active { color:#1e8acb; } &.basic-adjust, &.message-editor { width: 525px; } .icon { height:65px; width: 50px; margin:auto; } } @import "alerts"; @import "attachments"; @import "codemirror"; @import "drafts"; 

从示例代码中可以看出,我们有一些通常在常规 CSS 文件中不起作用的新元素。其中一些包括:

  • 为整个样式表定义自定义变量

  • 定义 mixin,作为可重用样式的伪函数(带有动态参数)

  • 在我们的样式定义中定义 mixin 和变量

  • 使用父/子关系嵌套样式

当使用 LESS(或在示例代码中使用 SASS)转译前面的代码时,输出是一个符合所有正常浏览器规则和语法的标准.css样式表。

有关 LESS 和 SASS 的更多信息,请查看以下链接:

开发复杂的前端应用程序与任何其他软件应用程序并无二致。代码将会复杂而强大,因此写测试以及实践测试驱动开发是理所当然的。前端的测试框架和语言的可用性与任何其他语言一样强大。我们在本书中用于测试我们编写的 Node.js 代码的所有工具和概念也可以直接用于前端。

考虑用于测试前端 JavaScript 的其他一些工具:

我想指出的一件事是,测试前端代码时,通常测试运行程序希望在浏览器窗口中运行。这很好,也很合理,但在现实世界中,自动化测试或使用 TDD 快速执行测试套件时,每次测试套件运行时都要打开浏览器窗口可能有点痛苦。PhantomJS 是一种可用的 无头 浏览器,非常适合在这种情况下使用。无头浏览器简单地意味着它是一个可以在命令行中运行的浏览器,只存在于内存中,没有实际的界面(像典型的浏览器)。

您可以轻松地配置 Karma,使用 PhantomJS 而不是您选择的浏览器来启动测试套件。当使用 PhantomJS 作为您的浏览器时,您的测试在后台执行,只报告错误。以下是使用 Karma 使用 PhantomJS 运行的测试套件的示例输出:

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (56)

这是一个关于在进行典型的 Web 开发时使用的一些最常见的前端工具和框架的快速介绍。我们看了一下 TodoMVC 项目,并回顾了三种流行的 JavaScript 框架,用于构建强大和复杂的前端应用程序。

诸如 Grunt.js、Gulp 和 Broccoli 等流行的构建工具帮助开发人员通过自动化许多需要在每次文件修改时发生的重复任务来简化其工作流程。从将脚本连接成单个文件,到最小化和压缩,再到执行自动化测试套件,任务运行程序可以配置为处理几乎所有事情!

我们看了一下两种流行的 CSS 转译器 LESS 和 SASS,并看到它们如何通过使用混合、变量和嵌套使得创建和管理 CSS 样式表变得动态化。

最后,您了解了 PhantomJS,无头浏览器,并在运行前端测试时使用它,以便可以使用像 Karma 这样的测试运行程序从命令行快速轻松地执行测试。

NodeJS-MongoDB-Web-开发-全- - 绝不原创的飞龙 - 博客园 (2024)
Top Articles
Latest Posts
Article information

Author: Ms. Lucile Johns

Last Updated:

Views: 6267

Rating: 4 / 5 (41 voted)

Reviews: 88% of readers found this page helpful

Author information

Name: Ms. Lucile Johns

Birthday: 1999-11-16

Address: Suite 237 56046 Walsh Coves, West Enid, VT 46557

Phone: +59115435987187

Job: Education Supervisor

Hobby: Genealogy, Stone skipping, Skydiving, Nordic skating, Couponing, Coloring, Gardening

Introduction: My name is Ms. Lucile Johns, I am a successful, friendly, friendly, homely, adventurous, handsome, delightful person who loves writing and wants to share my knowledge and understanding with you.