React 全栈项目(一)
原文:
zh.annas-archive.org/md5/05F04F9004AE49378ED0525C32CB85EB译者:飞龙
前言
本书探讨了通过将 React 的力量与经过行业测试的服务器端技术(如 Node、Express 和 MongoDB)相结合,开发全栈 JavaScript Web 应用的潜力。JavaScript 领域已经快速增长了一段时间。在这个主题上有大量的选择和资源可用,当你需要从这些经常变化的部分中进行选择、了解它们并使它们一起工作来构建自己的 Web 应用时,很容易迷失。为了解决这一痛点,本书采用了一种实用的方法,帮助您设置和构建使用这种流行的 JavaScript 堆栈的各种工作应用程序。
本书适合的读者
本书面向有一些 React 经验但没有涉及 Node、Express 和 MongoDB 的全栈开发经验的 JavaScript 开发人员,他们希望获得实用的指南,以开始使用这种堆栈构建不同类型的 Web 应用程序。
本书涵盖的内容
第一章,《使用 MERN 释放 React 应用程序》,介绍了 MERN 堆栈技术和本书中开发的应用程序。我们将讨论使用 React、Node、Express 和 MongoDB 开发 Web 应用程序的背景和相关性。
第二章,《准备开发环境》,帮助设置 MERN 堆栈技术以进行开发。我们将探索必要的开发工具,安装 Node、MongoDB、Express、React 和其他所需的库,然后运行代码来检查设置。
第三章,《使用 MongoDB、Express 和 Node 构建后端》,实现了一个骨架 MERN 应用的后端。我们将构建一个独立的服务器端应用程序,其中包括 MongoDB、Express 和 Node,用于存储用户详细信息,并具有用于用户身份验证和 CRUD 操作的 API。
第四章,《添加 React 前端以完成 MERN》,通过集成 React 前端完成了 MERN 骨架应用程序。我们将使用 React 视图实现一个可与服务器上的用户 CRUD 和 auth API 进行交互的工作前端。
第五章,“从简单的社交媒体应用开始”,通过扩展骨架应用程序构建了一个社交媒体应用程序。我们将通过实现社交媒体功能来探索 MERN 堆栈的能力,例如帖子分享、点赞和评论;关注朋友;以及聚合新闻源。
第六章,“通过在线市场锻炼新的 MERN 技能”,在在线市场应用程序中实现了基本功能。我们将实现与买卖相关的功能,支持卖家账户、产品列表和按类别搜索产品。
第七章,“扩展订单和支付的市场”,进一步构建了市场应用程序,包括购物车、订单管理和支付处理。我们将添加购物车功能,允许用户使用购物车中的商品下订单。我们还将集成 Stripe 以收集和处理付款。
第八章,“构建媒体流应用程序”,使用 MongoDB GridFS 实现媒体上传和流媒体。我们将开始构建一个基本的媒体流应用程序,允许注册用户上传视频文件,这些文件将存储在 MongoDB 上并流回,以便观众可以在简单的 React 媒体播放器中播放每个视频。
第九章,“定制媒体播放器和改善 SEO”,通过定制媒体播放器和自动播放媒体列表来升级媒体查看功能。我们将在默认的 React 媒体播放器上实现定制控件,添加可以自动播放的播放列表,并通过为媒体详细信息添加有选择的服务器端渲染和数据来改善 SEO。
第十章,“开发基于 Web 的 VR 游戏”,使用 React 360 开发了一个用于 Web 的 3D 虚拟现实游戏。我们将探索 React 360 的 3D 和 VR 功能,并构建一个简单的基于 Web 的 VR 游戏。
第十一章,使用 MERN 使 VR 游戏动态化,通过扩展 MERN 骨架应用程序并集成 React 360,构建了一个动态的 VR 游戏应用程序。我们将实现一个游戏数据模型,允许用户创建自己的 VR 游戏,并将动态游戏数据与使用 React 360 开发的游戏相结合。
第十二章,遵循最佳实践并进一步开发 MERN,反思了前几章的教训,并提出了进一步基于 MERN 的应用程序开发的改进建议。我们将扩展一些已经应用的最佳实践,比如应用程序结构中的模块化,其他应该应用的实践,比如编写测试代码,以及可能的改进,比如优化捆绑大小。
为了充分利用本书
本书的内容组织假定您熟悉基本的基于 Web 的技术,了解 JavaScript 中的编程构造,并对 React 应用程序的工作原理有一般了解。在阅读本书时,您将了解这些概念在使用 React、Node、Express 和 MongoDB 构建完整的 Web 应用程序时是如何结合在一起的。
为了在阅读各章节时最大限度地提高学习体验,建议您并行运行相关应用程序代码的关联版本,并使用每章提供的相关说明。
下载示例代码文件
您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
-
登录或注册www.packtpub.com。
-
选择“支持”选项卡。
-
单击“代码下载和勘误”。
-
在搜索框中输入书名,然后按照屏幕上的说明操作。
下载文件后,请确保使用以下最新版本的解压缩或提取文件夹:
-
Windows 上的 WinRAR/7-Zip
-
Mac 上的 Zipeg/iZip/UnRarX
-
Linux 上的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Full-Stack-React-Projects。如果代码有更新,将在现有的 GitHub 存储库上进行更新。
我们还有来自我们丰富书籍和视频目录的其他代码包,可在**github.com/PacktPublishing/**上找到。去看看吧!
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:"将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。"
代码块设置如下:
import path from 'path'
const CURRENT_WORKING_DIR = process.cwd()
app.use('/dist', express.static(path.join(CURRENT_WORKING_DIR, 'dist')))
当我们希望引起您对代码块的特定部分的注意时,相关行或项将以粗体显示:
{
"presets": [
"env",
"stage-2",
"react"
],
"plugins": [
"react-hot-loader/babel"
]
}
任何命令行输入或输出都将按照以下方式书写:
npm install babel-preset-react --save-dev
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中以这种方式出现。例如:"从管理面板中选择系统信息。"
警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。
第一章:使用 MERN 释放 React 应用程序
React 可能已经为前端 Web 开发开辟了新的领域,并改变了我们编写 JavaScript 用户界面的方式,但我们仍然需要一个坚实的后端来构建完整的 Web 应用程序。尽管在选择后端技术时有很多选择,但使用完整的 JavaScript 堆栈的好处和吸引力是不可否认的,特别是当有像 Node、Express 和 MongoDB 这样强大且被广泛采用的后端技术时。将 React 的潜力与这些经过行业测试的服务器端技术相结合,可以在开发现实世界 Web 应用程序时创造多样的可能性。
本书将指导您进行基于 MERN 的 Web 开发设置,以构建不同复杂性的实际 Web 应用程序。
在深入开发这些 Web 应用程序之前,我们将在本章中回答以下问题,以便为使用 MERN 设置背景:
-
什么是 MERN 堆栈?
-
为什么 MERN 如今仍然相关?
-
MERN 何时适合开发 Web 应用程序?
-
这本书如何组织以帮助掌握 MERN?
MERN 堆栈
MongoDB、Express、React 和 Node 一起被用来构建 Web 应用程序,并组成了 MERN 堆栈。在这个组合中,Node 和 Express 将 Web 后端绑在一起,MongoDB 作为 NoSQL 数据库,React 构建用户看到并与之交互的前端。
这四种技术都是免费的、开源的、跨平台的,基于 JavaScript,并得到了广泛的社区和行业支持。每种技术都有一套独特的属性,当它们集成在一起时,就可以构建一个简单但有效的完整 JavaScript 堆栈,用于 Web 开发。
Node
Node 是建立在 Chrome 的 V8 JavaScript 引擎上的 JavaScript 运行时环境。Node 使得在服务器端使用 JavaScript 来构建各种工具和应用成为可能,而不再局限于浏览器内的先前用例。
Node 具有事件驱动的架构,能够进行异步、非阻塞的 I/O。它独特的非阻塞 I/O 模型消除了等待请求的方式。这使得构建可扩展和轻量级的实时 Web 应用程序成为可能,可以高效地处理许多请求。
Node 的默认包管理系统,Node 包管理器或 npm,已捆绑在 Node 安装中。Npm 提供了数十万个由世界各地的开发人员构建的可重用 Node 包,并自称目前是世界上最大的开源库生态系统。
在nodejs.org/en/了解更多关于 Node,并浏览可用的 npm 模块在www.npmjs.com/。
Express
Express 是一个用于构建带有 Node 服务器的 Web 应用程序和 API 的基本框架。它提供了一层简单的基本 Web 应用程序功能,可以补充 Node。
在使用 Node 开发的任何 Web 应用程序中,Express 可以用作路由和中间件 Web 框架,具有自己的最小功能-Express 应用程序本质上是一系列中间件函数调用。
中间件函数是具有对 HTTP 请求和响应对象的访问权限,以及 Web 应用程序请求-响应周期中的下一个中间件函数的访问权限的函数。
可以将几乎任何兼容的中间件插入到请求处理链中,几乎可以按任何顺序进行,使 Express 非常灵活易用。
在expressjs.com上了解 Express.js 的可能性。
MongoDB
在决定用于任何应用程序的 NoSQL 数据库时,MongoDB 是首选。它是一个面向文档的数据库,可以将数据存储在灵活的类 JSON 文档中。这意味着字段可以在文档之间变化,并且数据模型可以随着应用程序要求的变化而随时间演变。
将高可用性和可扩展性放在首位的应用程序受益于 MongoDB 的分布式架构功能。它内置支持高可用性,使用分片进行水平扩展,并且可以跨地理分布进行多数据中心的可扩展性。
MongoDB 具有表达丰富的查询语言,可以进行即席查询,索引以实现快速查找,并提供实时聚合,从而提供了强大的访问和分析数据的方式,即使数据量呈指数级增长,也能保持性能。
在www.mongodb.com/上探索 MongoDB 的功能和服务。
React
React 是一个声明式的、基于组件的 JavaScript 库,用于构建用户界面。它的声明式和模块化特性使开发人员能够轻松创建和维护可重用、交互式和复杂的用户界面。
如果使用 React 构建,显示大量变化数据的大型应用程序可以快速响应,因为它会在特定数据更改时高效地更新和渲染正确的 UI 组件。React 通过其对虚拟 DOM 的显著实现进行高效渲染,这使其与其他处理页面更新的 Web UI 库有所区别,后者直接在浏览器的 DOM 中进行昂贵的操作。
使用 React 开发用户界面也迫使前端程序员编写合理和模块化的代码,这些代码是可重用的,更容易调试、测试和扩展。
在reactjs.org/上查看有关 React 的资源。
由于所有四种技术都是基于 JavaScript 的,它们本质上都是为集成进行了优化。然而,实际上如何将它们组合在一起形成 MERN 堆栈可能会根据应用程序要求和开发者偏好而有所不同,使 MERN 可以根据特定需求进行定制和扩展。
MERN 的相关性
自 JavaScript 诞生以来,它已经走过了很长的路,而且它还在不断发展。MERN 堆栈技术挑战了现状,并为 JavaScript 的可能性开辟了新的领域。但是,当涉及到开发需要可持续的真实应用程序时,选择 MERN 是否是一个值得的选择呢?以下简要概述了选择 MERN 作为下一个 Web 应用程序的强有力理由。
技术堆栈的一致性
由于 JavaScript 一直在使用,开发人员不需要频繁学习和切换到使用非常不同的技术。这也促进了在不同部分的 Web 应用程序上工作的团队之间更好的沟通和理解。
学习、开发、部署和扩展所需的时间更少
技术堆栈的一致性也使学习和使用 MERN 变得更加容易,减少了采用新堆栈的开销和开发工作的时间。一旦建立了 MERN 应用程序的工作基础并建立了工作流程,复制、进一步开发和扩展任何应用程序就需要更少的工作量。
在行业中被广泛采用
基于其需求,各种规模的组织一直在采用此堆栈中的技术,因为他们可以更快地构建应用程序,处理高度多样化的需求,并在规模上更有效地管理应用程序。
社区支持和增长
围绕非常流行的 MERN 堆栈技术的开发者社区非常多样化,并且定期增长。由于有很多人不断使用、修复、更新,并愿意帮助发展这些技术,支持系统在可预见的未来将保持强大。这些技术将继续得到维护,并且在文档、附加库和技术支持方面很可能会提供资源。
使用这些技术的便利性和好处已经被广泛认可。由于继续采用和适应的知名公司,以及不断增加的为代码库做出贡献、提供支持和创建资源的人数,MERN 堆栈中的技术将在很长一段时间内继续保持相关性。
MERN 应用范围
考虑到每种技术的独特特性,以及通过集成其他技术来扩展此堆栈的功能的便利性,可以使用此堆栈构建的应用程序范围实际上非常广泛。
如今,网络应用程序默认应该是丰富的客户端应用程序,具有沉浸式、互动性,并且在性能和可用性上不会逊色。MERN 的优势组合使其非常适合开发满足这些方面和需求的网络应用程序。
此外,一些技术的新颖和即将推出的特性,例如使用 Node 进行低级操作操作、使用 MongoDB GridFS 进行大文件流传输功能,以及使用 React 360 在网络上实现虚拟现实功能,使得可以使用 MERN 构建更复杂和独特的应用程序。
挑选 MERN 技术中的特定功能,并论述为什么这些功能不适用于某些应用可能看起来是合理的。但考虑到 MERN 堆栈如何灵活地组合和扩展,这些问题可以在 MERN 中逐案解决。在本书中,我们将演示在构建应用程序时如何考虑特定要求和需求。
本书中开发的 MERN 应用程序
为了展示 MERN 的广泛可能性以及如何轻松开始构建具有不同功能的 Web 应用程序,本书将展示日常使用的 Web 应用程序以及复杂和罕见的 Web 体验:
上述截图展示了本书其余部分开发的四个不同的 MERN 应用程序
社交媒体平台
对于第一个 MERN 应用程序,我们将构建一个受 Twitter 和 Facebook 启发的基本社交媒体应用程序。这个社交媒体平台将实现诸如帖子分享、点赞和评论、关注朋友以及聚合新闻源等简单功能。
在线市场
各种类型的电子商务 Web 应用程序在互联网上随处可见,而且这些应用程序在短期内不会过时。使用 MERN,我们将构建一个在线市场应用程序,涵盖核心方面,如支持卖家账户、产品列表、顾客购物车和支付处理。
媒体流应用程序
为了测试一些高级的 MERN 功能,下一个选择是更加沉浸式的应用程序,比如媒体流应用程序。受 Netflix 和 YouTube 的功能启发,该应用程序将实现内容上传和查看功能,为内容提供者提供媒体内容上传功能,并为观众提供实时内容流。
Web 的 VR 游戏
React 360 的发布使得将 Web VR 功能应用于 React 用户界面成为可能。我们将探索如何在 MERN 中使用 React 360 创建罕见的 Web 体验,通过组合基本的虚拟现实游戏应用程序。用户将能够制作和玩 VR 游戏,每个游戏都将有动画的 VR 对象,玩家可以收集以完成游戏。
书的结构
这本书旨在帮助那些对 MERN 堆栈有零到一些经验的 JavaScript 开发人员,设置并开始开发不同复杂性的 Web 应用程序。它包括构建和运行不同应用程序的指南,以及代码片段和关键概念的解释。
这本书分为五个部分,从基础到高级主题逐步展开,带领你一路构建 MERN,然后利用它开发具有简单到复杂功能的不同应用程序,同时演示如何根据应用程序要求扩展 MERN 堆栈的功能。
开始使用 MERN
第一章,释放 MERN 的 React 应用程序和第二章,准备开发环境为在 MERN 堆栈中开发 Web 应用程序设定了背景,并指导您设置开发环境。
从头开始构建 MERN——一个骨架应用程序
第三章,使用 MongoDB、Express 和 Node 构建后端和第四章,添加 React 前端以完成 MERN展示了如何将 MERN 堆栈技术结合起来形成一个具有最少和基本功能的骨架 Web 应用程序。这个骨架 MERN 应用程序作为本书其余部分开发的四个主要应用程序的基础。
使用 MERN 开发基本 Web 应用程序
在这一部分,您将通过构建两个真实世界的应用程序——一个简单的社交媒体平台(第五章),从一个简单的社交媒体应用开始,和一个在线市场(第六章),通过在线市场锻炼新的 MERN 技能和第七章,扩展订单和支付的市场来熟悉 MERN 堆栈 Web 应用程序的核心属性。
深入复杂的 MERN 应用
第八章,构建媒体流应用程序,第九章,自定义媒体播放器和改善 SEO,第十章,开发基于 Web 的 VR 游戏和第十一章,使用 MERN 使 VR 游戏动态展示了这个堆栈如何用于开发具有更复杂和沉浸式功能的应用程序,例如使用 React 360 进行媒体流和虚拟现实。
继续前进与 MERN
最后第十二章,遵循最佳实践并进一步开发 MERN总结了前面的章节和应用程序,通过扩展最佳实践来开发成功的 MERN 应用程序,提出改进建议和进一步发展。
您可以根据自己的经验水平和偏好,选择是否按照规定的顺序使用本书。对于一个对 MERN 非常陌生的开发人员,可以按照本书中的路径进行。对于更有经验的 JS 开发人员,从零开始构建 MERN - 一个骨架应用程序 部分的章节将是开始设置基础应用程序的好地方,然后选择任何四个应用程序进行构建和扩展。
充分利用本书
这本书的内容是以实践为导向的,涵盖了构建每个 MERN 应用程序所需的实施步骤、代码和相关概念。建议您不要仅仅试图通读章节,而是应该并行运行相关代码,并在阅读书中的解释时浏览应用程序的功能。
讨论代码实现的章节将指向包含完整代码及其运行说明的 GitHub 存储库。您可以在阅读章节之前拉取代码、安装并运行它:
您可以考虑按照本书中概述的推荐步骤来实施:
-
在深入讨论章节中的实施细节之前,从相关的 GitHub 存储库中拉取代码
-
按照代码的说明安装和运行应用程序
-
在阅读相关章节中的功能描述时,浏览正在运行的应用程序的功能
-
在开发模式下运行代码并在编辑器中打开后,参考书中的步骤和解释,以更深入地理解实施细节
本书旨在为每个应用程序提供快速的入门指南和工作代码。您可以根据需要对此代码进行实验、改进和扩展。为了获得积极的学习体验,鼓励您在遵循本书的同时重构和修改代码。在一些示例中,本书选择冗长的代码而不是简洁和更清晰的代码,因为对于新手来说更容易理解。在一些其他实现中,本书坚持使用更广泛使用的传统约定,而不是现代和即将到来的 JavaScript 约定。这样做是为了在您自行研究讨论的技术和概念时,最小化参考在线资源和文档时的差异。本书中代码可以更新的这些实例,是探索和发展超出本书涵盖范围的技能的好机会。
摘要
在本章中,我们了解了在 MERN 堆栈中开发 Web 应用程序的背景,以及本书将如何帮助您使用该堆栈进行开发。
MERN 堆栈项目集成了 MongoDB、Express、React 和 Node,用于构建 Web 应用程序。该堆栈中的每种技术在 Web 开发领域都取得了相关进展。这些技术被广泛采用,并在不断壮大的社区支持下不断改进。可以开发具有不同需求的 MERN 应用程序,从日常使用的应用程序到更复杂的 Web 体验。本书中的实用导向方法可用于从基础到高级的 MERN 技能成长,或者直接开始构建更复杂的应用程序。
在下一章中,我们将开始为 MERN 应用程序开发做准备,通过设置开发环境。
第二章:准备开发环境
在使用 MERN 堆栈构建应用程序之前,我们首先需要准备每种技术的开发环境,以及用于辅助开发和调试的工具。本章将指导您了解工作空间选项、基本开发工具、如何在工作空间中设置 MERN 技术以及检查此设置的实际代码步骤。
我们将涵盖以下主题:
-
工作空间选项
-
代码编辑器
-
Chrome 开发者工具
-
Git 设置
-
MongoDB 设置
-
Node 设置
-
npm 模块以完成 MERN 堆栈
-
用于检查 MERN 设置的代码
选择开发工具
在选择基本开发工具(如文本编辑器或 IDE、版本控制软件甚至开发工作空间本身)时有很多选择。在本节中,我们将介绍与 MERN Web 开发相关的选项和建议,以便您可以根据个人偏好做出明智的决定。
工作空间选项
在本地计算机上开发是程序员中最常见的做法,但随着诸如 Cloud9(aws.amazon.com/cloud9/?origin=c9io)等优秀的云开发服务的出现,现在可以同时使用本地和云端。您可以使用 MERN 技术设置本地工作空间,并且在本书的其余部分将假定为这种情况,但您也可以选择在配备了 Node 开发的云服务中运行和开发代码。
本地和云开发
您可以选择同时使用这两种类型的工作空间,以享受在本地工作的好处,而不必担心带宽/互联网问题,并在没有您喜爱的本地计算机时远程工作。为此,您可以使用 Git 对代码进行版本控制,将最新代码存储在 GitHub 或 BitBucket 等远程 Git 托管服务上,然后在所有工作空间中共享相同的代码。
IDE 或文本编辑器
大多数云开发环境都将集成源代码编辑器。但是对于您的本地工作空间,您可以根据自己作为程序员的偏好选择任何编辑器,然后为 MERN 开发进行自定义。例如,以下流行选项都可以根据需要进行自定义:
-
Atom(
atom.io/):GitHub 的免费开源文本编辑器,有许多其他开发人员提供的与 MERN 堆栈相关的包可用 -
SublimeText(www.sublimetext.com/):一款专有的跨平台文… MERN 堆栈相关的软件包可用,支持 JavaScript 开发
-
Visual Studio Code(code.visualstudio.com/):微软开发的功能丰富… Web 应用程序开发工作流程,包括对 MERN 堆栈技术的支持
-
WebStorm(www.jetbrains.com/webstorm/):… JetBrains 开发的全功能 JavaScript IDE,支持基于 MERN 堆栈的开发
Chrome 开发者工具
加载、查看和调试前端是 Web 开发过程中非常关键的一部分。Chrome 开发者工具是 Chrome 浏览器的一部分,具有许多出色的功能,允许调试、测试和实验前端代码,以及 UI 的外观、响应和性能。此外,React 开发者工具扩展可作为 Chrome 插件使用,并将 React 调试工具添加到 Chrome 开发者工具中。
Git
任何开发工作流程都不完整,如果没有版本控制系统来跟踪代码更改、共享代码和协作。多年来,Git 已成为许多开发人员的事实标准版本控制系统,并且是最广泛使用的分布式源代码管理工具。在本书中,Git 将主要帮助跟踪进度,因为我们逐步构建每个应用程序。
安装
要开始使用 Git,首先根据您的系统规格在本地计算机或基于云的开发环境上安装它。有关下载和安装最新 Git 的相关说明,以及使用 Git 命令的文档,可在以下网址找到:git-scm.com/downloads。
远程 Git 托管服务
基于云的 Git 存储库托管服务,如 GitHub 和 BitBucket,有助于在工作空间和部署环境之间共享最新的代码,并备份代码。这些服务提供了许多有用的功能,以帮助代码管理和开发工作流程。要开始使用,您可以创建一个帐户,并为您的代码库设置远程存储库。
所有这些基本工具将丰富您的 Web 开发工作流程,并在您完成工作区的必要设置并开始构建 MERN 应用程序后提高生产力。
设置 MERN 技术栈
MERN 技术栈正在开发和升级,因此在撰写本书时,我们使用的是最新的稳定版本。大多数这些技术的安装指南取决于工作区的系统环境,因此本节指向所有相关的安装资源,并且也作为设置完全功能的 MERN 技术栈的指南。
MongoDB
在向 MERN 应用程序添加任何数据库功能之前,必须在开发环境中设置并运行 MongoDB。在撰写本文时,MongoDB 的当前稳定版本是 3.6.3,本书中用于开发应用程序的是 MongoDB Community Edition 的这个版本。本节的其余部分提供了有关如何安装和运行 MongoDB 的资源。
安装
您需要在工作区安装并启动 MongoDB,以便在开发中使用它。MongoDB 的安装和启动过程取决于工作区的规格:
-
云开发服务将有其自己的安装和设置 MongoDB 的说明。例如,Cloud9 的操作步骤可以在此找到:
community.c9.io/t/setting-up-mongodb/1717。 -
在本地机器上安装的指南详见:
docs.mongodb.com/manual/installation/。
运行 mongo shell
mongo shell 是 MongoDB 的交互式工具,是熟悉 MongoDB 操作的好地方。一旦安装并运行了 MongoDB,您可以在命令行上运行 mongo shell。在 mongo shell 中,您可以尝试查询和更新数据以及执行管理操作的命令。
Node
MERN 应用程序的后端服务器实现依赖于 Node 和 npm。在撰写本文时,8.11.1 是最新的稳定 Node 版本,并且附带 npm 版本 5.6.0。然而,npm 的最新版本是 5.8.0,因此在安装 Node 后,需要根据下一节的讨论升级 npm。
安装
Node 可以通过直接下载、安装程序或 Node 版本管理器进行安装。
-
您可以通过直接下载源代码或针对您的工作平台特定的预构建安装程序来安装 Node。下载地址为nodejs.org/en/download。
-
云开发服务可能已经预装了 Node,比如 Cloud9,或者会有特定的添加和更新 Node 的说明。
要测试安装是否成功,可以打开命令行并运行node -v来查看它是否正确返回版本号。
升级 npm 版本
为了安装 npm 版本 5.8.0,可以从命令行运行以下安装命令,并使用**npm -v**检查版本:
npm install -g npm@5.8.0
npm -v
使用 nvm 进行 Node 版本管理
如果您需要为不同的项目维护多个 Node 和 npm 版本,nvm 是一个有用的命令行工具,可以在同一工作空间中安装和管理不同的版本。您必须单独安装 nvm。设置说明可以在github.com/creationix/…找到。
MERN 的 npm 模块
其余的 MERN 堆栈技术都可以作为 npm 模块使用,并且可以通过npm install添加到每个项目中。这些包括关键模块,如 React 和 Express,这些模块是运行每个 MERN 应用程序所必需的,还有在开发过程中将需要的模块。在本节中,我们列出并讨论这些模块,然后在下一节中看如何在一个工作项目中使用这些模块。
关键模块
为了集成 MERN 堆栈技术并运行您的应用程序,我们将需要以下 npm 模块:
-
React:要开始使用 React,我们将需要两个模块:
-
react -
react-dom -
Express:要在代码中使用 Express,您需要
express模块 -
MongoDB:要在 Node 应用程序中使用 MongoDB,还需要添加驱动程序,该驱动程序可作为名为
mongodb的 npm 模块使用
devDependency 模块
为了在 MERN 应用程序的开发过程中保持一致性,我们将在整个堆栈中使用 JavaScript ES6。因此,为了辅助开发过程,我们将使用以下额外的 npm 模块来编译和捆绑代码,并在开发过程中更新代码时自动重新加载服务器和浏览器应用程序:
-
Babel 模块用于将 ES6 和 JSX 转换为适合所有浏览器的 JavaScript。需要的模块来使 Babel 工作的有:
-
babel-core -
babel-loader用于使用 Webpack 转换 JavaScript 文件 -
babel-preset-env,babel-preset-react和babel-preset-stage-2用于支持 React,最新的 JS 功能以及一些 stage-x 功能,例如声明目前未在babel-preset-env下覆盖的类字段 -
Webpack 模块将帮助捆绑编译后的 JavaScript,用于客户端和服务器端代码。需要使 Webpack 工作的模块有:
-
webpack -
webpack-cli用于运行 Webpack 命令 -
webpack-node-externals在 Webpack 打包时忽略外部 Node 模块文件 -
webpack-dev-middleware在开发过程中通过连接的服务器提供从 Webpack 发出的文件 -
webpack-hot-middleware将热模块重新加载添加到现有服务器中,通过将浏览器客户端连接到 Webpack 服务器,并在开发过程中接收代码更改的更新 -
nodemon在开发过程中监视服务器端的更改,以便重新加载服务器以使更改生效。 -
react-hot-loader用于加快客户端的开发。每当 React 前端中的文件更改时,react-hot-loader使浏览器应用程序能够在不重新捆绑整个前端代码的情况下更新。
尽管react-hot-loader旨在帮助开发流程,但安装此模块作为常规依赖项而不是 devDependency 是安全的。它会自动确保在生产中禁用热重新加载,并且占用空间很小。
检查您的开发设置
在这一部分,我们将逐步进行开发工作流程,并编写代码,以确保环境正确设置以开始开发和运行 MERN 应用程序。
我们将在以下文件夹结构中生成这些项目文件以运行一个简单的设置项目:
| mern-simplesetup/
| -- client/
| --- HelloWorld.js
| --- main.js
| -- server/
| --- devBundle.js
| --- server.js
| -- .babelrc
| -- nodemon.json
| -- package.json
| -- template.js
| -- webpack.config.client.js
| -- webpack.config.client.production.js
| -- webpack.config.server.js
本节讨论的代码可在 GitHub 的存储库中找到:github.com/shamahoque/…。您可以克隆此代码,并在本章的其余部分中阅读代码解释时运行它。
初始化 package.json 并安装 npm 模块
我们将首先使用 npm 安装所有必需的模块。在每个项目文件夹中添加package.json文件以维护、记录和共享 MERN 应用程序中使用的 npm 模块是最佳实践。package.json文件将包含有关应用程序的元信息,以及列出模块依赖项。
按照以下步骤生成package.json文件,修改它,并用它来安装 npm 模块:
-
npm init: 从命令行进入项目文件夹,运行npm init。您将被问及一系列问题,然后将自动生成一个package.json文件,其中包含您的答案。 -
dependencies: 在编辑器中打开package.json,修改 JSON 对象,添加关键模块和react-hot-loader作为常规的dependencies。
在代码块之前提到的文件路径表示项目目录中代码的位置。本书始终遵循这一约定,以提供更好的上下文和指导,让您能够跟着代码进行学习。
mern-simplesetup/package.json:
"dependencies": {
"express": "⁴.16.3",
"mongodb": "³.0.7",
"react": "¹⁶.3.2",
"react-dom": "¹⁶.3.2",
"react-hot-loader": "⁴.1.2"
}
devDependencies: 进一步修改package.json,添加以下在开发过程中所需的 npm 模块作为devDependencies。
mern-simplesetup/package.json:
"devDependencies": {
"babel-core": "⁶.26.2",
"babel-loader": "⁷.1.4",
"babel-preset-env": "¹.6.1",
"babel-preset-react": "⁶.24.1",
"babel-preset-stage-2": "⁶.24.1",
"nodemon": "¹.17.3",
"webpack": "⁴.6.0",
"webpack-cli": "².0.15",
"webpack-dev-middleware": "³.1.2",
"webpack-hot-middleware": "².22.1",
"webpack-node-externals": "¹.7.2"
}
npm install: 保存package.json,然后从命令行运行npm install,以获取并添加所有这些模块到您的项目中。
配置 Babel、Webpack 和 Nodemon
在我们开始编写 Web 应用程序之前,我们需要配置 Babel、Webpack 和 Nodemon,在开发过程中编译、打包和自动重新加载代码更改。
Babel
在项目文件夹中创建一个.babelrc文件,并添加以下 JSON,其中指定了presets和plugins。
mern-simplesetup/.babelrc:
{
"presets": [
"env",
"stage-2"
"react"
],
"plugins": [
"react-hot-loader/babel"
]
}
react-hot-loader/babel插件是由react-hot-loader模块需要编译React组件。
Webpack
我们将不得不为捆绑客户端和服务器端代码以及生产代码分别配置 Webpack。在项目文件夹中创建webpack.config.client.js、webpack.config.server.js和webpack.config.client.production.js文件。所有三个文件都将具有以下代码结构:
const path = require('path')
const webpack = require('webpack')
const CURRENT_WORKING_DIR = process.cwd()
const config = { ... }
module.exports = config
config JSON 对象的值将根据客户端或服务器端代码以及开发与生产代码而有所不同。
用于开发的客户端 Webpack 配置
在您的webpack.config.client.js文件中更新config对象如下,以配置 Webpack 在开发过程中捆绑和热加载 React 代码。
mern-simplesetup/webpack.config.client.js:
const config = {
name: "browser",
mode: "development",
devtool: 'eval-source-map',
entry: [
'react-hot-loader/patch',
'webpack-hot-middleware/client?reload=true',
path.join(CURRENT_WORKING_DIR, 'client/main.js')
],
output: {
path: path.join(CURRENT_WORKING_DIR , '/dist'),
filename: 'bundle.js',
publicPath: '/dist/'
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
'babel-loader'
]
}
]
}, plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
}
-
mode将process.env.NODE_ENV设置为给定值,并告诉 Webpack 相应地使用其内置的优化。如果没有明确设置,它默认为值'production'。也可以通过命令行通过将值作为 CLI 参数传递来设置。 -
devtool指定了如何生成源映射,如果有的话。通常,源映射提供了一种将压缩文件中的代码映射回源文件中的原始位置以帮助调试的方法。 -
entry指定了 Webpack 开始打包的入口文件,在这种情况下是client文件夹中的main.js文件。 -
output指定了打包代码的输出路径,在这种情况下设置为dist/bundle.js。 -
publicPath允许指定应用程序中所有资产的基本路径。 -
module设置了用于转译的文件扩展名的正则规则,以及要排除的文件夹。这里要使用的转译工具是babel-loader。 -
HotModuleReplacementPlugin启用了react-hot-loader的热模块替换。 -
NoEmitOnErrorsPlugin允许在编译错误时跳过输出。
服务器端 Webpack 配置
修改代码以要求nodeExternals,并在webpack.config.server.js文件中更新config对象以配置 Webpack 用于打包服务器端代码。
mern-simplesetup/webpack.config.server.js:
const config = {
name: "server",
entry: [ path.join(CURRENT_WORKING_DIR , './server/server.js') ],
target: "node",
output: {
path: path.join(CURRENT_WORKING_DIR , '/dist/'),
filename: "server.generated.js",
publicPath: '/dist/',
libraryTarget: "commonjs2"
},
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [ 'babel-loader' ]
}
]
}
}
mode选项在这里没有明确设置,但在运行 Webpack 命令时,将根据开发或生产的需要进行传递。
Webpack 从server.js文件夹开始打包,然后将打包后的代码输出到dist文件夹中的server.generated.js文件中。
用于生产的客户端 Webpack 配置
为了准备客户端代码用于生产,更新webpack.config.client.production.js文件中的config对象与以下代码。
mern-simplesetup/webpack.config.client.production.js:
const config = {
mode: "production",
entry: [
path.join(CURRENT_WORKING_DIR, 'client/main.js')
],
output: {
path: path.join(CURRENT_WORKING_DIR , '/dist'),
filename: 'bundle.js',
publicPath: "/dist/"
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
'babel-loader'
]
}
]
}
}
这将配置 Webpack 用于打包用于生产模式的 React 代码,其中将不再需要热重载插件或调试配置。
Nodemon
在项目文件夹中创建一个nodemon.js文件,并添加以下配置。
mern-simplesetup/nodemon.js
{
"verbose": false,
"watch": [ "./server" ],
"exec": "webpack --mode=development --config
webpack.config.server.js
&& node ./dist/server.generated.js"
}
这个配置将设置nodemon在开发过程中监视服务器文件的更改,然后根据需要执行编译和构建命令。
使用 React 的前端视图
为了开始开发前端,首先在项目文件夹中创建一个名为template.js的根模板文件,它将使用React组件来渲染 HTML。
mern-simplesetup/template.js:
export default () => {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MERN Kickstart</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="/dist/bundle.js">
</script>
</body>
</html>`
}
当服务器收到对根 URL 的请求时,这个 HTML 模板将在浏览器中被渲染,ID 为"root"的div元素将包含我们的React组件。
接下来,创建一个client文件夹,我们将在其中添加两个 React 文件,main.js和HelloWorld.js。
main.js文件简单地在 HTML 文档的div元素中渲染顶层入口React组件。
mern-simplesetup/client/main.js:
import React from 'react'
import { render } from 'react-dom'
import HelloWorld from './HelloWorld'
render(<HelloWorld/>, document.getElementById('root'))
在这种情况下,入口React组件是从HelloWorld.js导入的HelloWorld组件。
HelloWorld.js包含一个基本的HelloWorld组件,它被热导出以在开发过程中使用react-hot-loader进行热重载。
mern-simplesetup/client/HelloWorld.js:
import React, { Component } from 'react'
import { hot } from 'react-hot-loader'
class HelloWorld extends Component {
render() {
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
}
export default hot(module)(HelloWorld)
为了在服务器收到对根 URL 的请求时在浏览器中看到React组件被渲染,我们需要使用 Webpack 和 Babel 设置来编译和打包这段代码,并添加服务器端代码来响应根路由请求并返回打包后的代码。
使用 Express 和 Node 构建服务器
在项目文件夹中,创建一个名为server的文件夹,并添加一个名为server.js的文件来设置服务器。然后,添加另一个名为devBundle.js的文件,它将在开发模式下使用 Webpack 配置来编译 React 代码。
Express 应用程序
在server.js中,我们首先将添加代码来导入express模块,以初始化一个 Express 应用程序。
mern-simplesetup/server/server.js:
import express from 'express'
const app = express()
然后我们将使用这个 Express 应用程序来构建出 Node 服务器应用程序的其余部分。
在开发过程中打包 React 应用程序
为了保持开发流程简单,我们将初始化 Webpack 来在运行服务器时编译客户端代码。在devBundle.js中,我们将设置一个编译方法,它接受 Express 应用程序并配置它来使用 Webpack 中间件来编译、打包和提供代码,以及在开发模式下启用热重载。
mern-simplesetup/server/devBundle.js:
import webpack from 'webpack'
import webpackMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import webpackConfig from './../webpack.config.client.js'
const compile = (app) => {
if(process.env.NODE_ENV == "development"){
const compiler = webpack(webpackConfig)
const middleware = webpackMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath
})
app.use(middleware)
app.use(webpackHotMiddleware(compiler))
}
}
export default {
compile
}
我们将在开发模式下通过在server.js中添加以下行来调用这个编译方法。
mern-simplesetup/server/server.js:
**import devBundle from './devBundle'**
const app = express()
**devBundle.compile(app)**
这两行突出显示的代码仅用于开发模式,在构建应用程序代码以进行生产时应将其注释掉。在开发模式下,当执行这些行时,Webpack 将编译和捆绑 React 代码并将其放置在dist/bundle.js中。
从 dist 文件夹中提供静态文件
Webpack 将在开发模式和生产模式下编译客户端代码,然后将捆绑文件放置在dist文件夹中。为了使这些静态文件在客户端请求时可用,我们将在server.js中添加以下代码来从dist/folder中提供静态文件。
mern-simplesetup/server/server.js:
import path from 'path'
const CURRENT_WORKING_DIR = process.cwd()
app.use('/dist', express.static(path.join(CURRENT_WORKING_DIR, 'dist')))
在根目录渲染模板
当服务器在根 URL / 处收到请求时,我们将在浏览器中呈现template.js。在server.js中,向 Express 应用程序添加以下路由处理代码,以接收在/处的 GET 请求。
mern-simplesetup/server/server.js:
import template from './../template'
app.get('/', (req, res) => {
res.status(200).send(template())
})
最后,添加服务器代码以侦听指定端口的传入请求。
mern-simplesetup/server/server.js:
let port = process.env.PORT || 3000
app.listen(port, function onStart(err) {
if (err) {
console.log(err)
}
console.info('Server started on port %s.', port)
})
将服务器连接到 MongoDB
要将 Node 服务器连接到 MongoDB,请在server.js中添加以下代码,并确保您的工作区中正在运行 MongoDB。
mern-simplesetup/server/server.js:
import { MongoClient } from 'mongodb'
const url = process.env.MONGODB_URI || 'mongodb://localhost:27017/mernSimpleSetup'
MongoClient.connect(url, (err, db)=>{
console.log("Connected successfully to mongodb server")
db.close()
})
在此代码示例中,MongoClient是连接到运行中的MongoDB实例的驱动程序,使用其url,并允许我们在后端实现与数据库相关的代码。
运行 npm 脚本
更新package.json文件,添加以下 npm 运行脚本以进行开发和生产。
mern-simplesetup/package.json:
"scripts": {
"development": "nodemon",
"build": "webpack --config webpack.config.client.production.js
&& webpack --mode=production --config
webpack.config.server.js",
"start": "NODE_ENV=production node ./dist/server.generated.js"
}
-
npm run development:此命令将启动 Nodemon、Webpack 和服务器以进行开发 -
npm run build:这将为生产模式生成客户端和服务器代码包(在运行此脚本之前,请确保从server.js中删除devBundle.compile代码) -
npm run start:此命令将在生产环境中运行捆绑代码
实时开发和调试
要运行到目前为止开发的代码,并确保一切正常运行,可以按照以下步骤进行:
-
从命令行运行应用程序:
npm run development。 -
在浏览器中加载:在浏览器中打开根 URL,即
http://localhost:3000,如果您正在使用本地机器设置。您应该看到一个标题为 MERN Kickstart 的页面,上面只显示 Hello World!。 -
开发代码并调试实时更改:将
HelloWorld.js组件文本中的'Hello World!'更改为'hello'。保存更改以在浏览器中看到即时更新,并检查命令行输出以查看bundle.js是否未重新创建。类似地,当您更改服务器端代码时,您也可以看到即时更新,从而提高开发效率。
如果您已经走到了这一步,恭喜您,您已经准备好开始开发令人兴奋的 MERN 应用程序了。
总结
在本章中,我们讨论了开发工具选项以及如何安装 MERN 技术,然后编写了代码来检查开发环境是否设置正确。
我们首先看了推荐的工作区、IDE、版本控制软件和适用于 Web 开发的浏览器选项。您可以根据自己作为开发人员的偏好从这些选项中进行选择。
接下来,我们首先安装 MongoDB、Node 和 npm,然后使用 npm 添加其余所需的库,从而设置了 MERN 堆栈技术。
在继续编写代码以检查此设置之前,我们配置了 Webpack 和 Babel 以在开发期间编译和捆绑代码,并构建生产就绪的代码。我们了解到,在在浏览器上打开 MERN 应用程序之前,有必要编译用于开发 MERN 应用程序的 ES6 和 JSX 代码。
此外,我们通过为前端开发包括 React Hot Loader,为后端开发配置 Nodemon,并在开发期间运行服务器时编译客户端和服务器端代码的方式,使开发流程更加高效。
在下一章中,我们将使用此设置开始构建一个骨架 MERN 应用程序,该应用程序将作为功能齐全应用程序的基础。
第三章:使用 MongoDB、Express 和 Node 构建后端
在大多数 Web 应用程序的开发过程中,存在常见任务、基本功能和实现代码的重复。这本书中开发的 MERN 应用程序也是如此。考虑到这些相似之处,我们将首先为一个骨架 MERN 应用程序奠定基础,该应用程序可以轻松修改和扩展,以实现各种 MERN 应用程序。
在本章中,我们将涵盖以下主题,并从 MERN 骨架的后端实现开始,使用 Node、Express 和 MongoDB:
-
MERN 应用程序中的用户 CRUD 和 auth
-
使用 Express 服务器处理 HTTP 请求
-
使用 Mongoose 模式进行用户模型
-
用户 CRUD 和 auth 的 API
-
用 JWT 进行受保护路由的身份验证
-
运行后端代码并检查 API
骨架应用程序概述
骨架应用程序将封装基本功能和一个在大多数 MERN 应用程序中重复的工作流程。我们将构建骨架本质上作为一个基本但完全功能的 MERN Web 应用程序,具有用户创建(CRUD)和身份验证-授权(auth)功能,这也将展示如何开发、组织和运行使用这个堆栈构建的一般 Web 应用程序的代码。目标是保持骨架尽可能简单,以便易于扩展,并可用作开发不同 MERN 应用程序的基础应用程序。
功能分解
在骨架应用程序中,我们将添加以下用例,其中包括用户 CRUD 和 auth 功能的实现:
-
注册:用户可以通过使用电子邮件地址注册创建新帐户
-
用户列表:任何访问者都可以看到所有注册用户的列表
-
身份验证:注册用户可以登录和退出
-
受保护的用户资料:只有注册用户可以在登录后查看个人用户详细信息
-
授权用户编辑和删除:只有注册和经过身份验证的用户才能编辑或删除自己的用户帐户详细信息
本章重点-后端
在本章中,我们将专注于使用 Node、Express 和 MongoDB 构建骨架应用程序的工作后端。完成的后端将是一个独立的服务器端应用程序,可以处理 HTTP 请求以创建用户、列出所有用户,并在考虑用户身份验证和授权的情况下查看、更新或删除数据库中的用户。
用户模型
用户模型将定义要存储在 MongoDB 数据库中的用户详细信息,并处理与用户相关的业务逻辑,如密码加密和用户数据验证。这个骨架版本的用户模型将是基本的,支持以下属性:
| 字段名称 | 类型 | 描述 |
|---|---|---|
name | String | 存储用户姓名的必需字段 |
email | String | 必需的唯一字段,用于存储用户的电子邮件并标识每个帐户(每个唯一电子邮件只允许一个帐户) |
password | String | 用于身份验证的必需字段,数据库将存储加密后的密码而不是实际字符串,以确保安全性 |
created | Date | 当创建新用户帐户时自动生成的时间戳 |
updated | Date | 当现有用户详细信息更新时自动生成的时间戳 |
用户 CRUD 的 API 端点
为了在用户数据库上启用和处理用户 CRUD 操作,后端将实现并公开 API 端点,供前端在视图中使用,如下所示:
| 操作 | API 路由 | HTTP 方法 |
|---|---|---|
| 创建用户 | /api/users | POST |
| 列出所有用户 | /api/users | GET |
| 获取用户 | /api/users/:userId | GET |
| 更新用户 | /api/users/:userId | PUT |
| 删除用户 | /api/users/:userId | DELETE |
| 用户登录 | /auth/signin | POST |
| 用户退出登录(可选) | /auth/signout | GET |
其中一些用户 CRUD 操作将具有受保护的访问权限,这将要求请求的客户端进行身份验证、授权或两者都要求。最后两个路由用于身份验证,将允许用户登录和退出登录。
使用 JSON Web Tokens 进行身份验证
为了根据骨架特性限制和保护对用户 API 端点的访问,后端需要整合身份验证和授权机制。在实现 Web 应用程序的用户身份验证时有许多选择。最常见和经过时间考验的选项是使用会话在客户端和服务器端存储用户状态。但是,一种较新的方法是使用JSON Web Token(JWT)作为无状态身份验证机制,不需要在服务器端存储用户状态。
这两种方法在相关的真实用例中都有优势。然而,为了简化本书中的代码,并且因为它与 MERN 堆栈和我们的示例应用程序配合得很好,我们将使用 JWT 进行身份验证实现。此外,本书还将在未来章节中提出安全增强选项。
JWT 的工作原理
当用户成功使用其凭据登录时,服务器端会生成一个使用秘钥和唯一用户详细信息签名的 JWT。然后,将此令牌返回给请求的客户端,以便在本地保存,可以保存在localStorage、sessionStorage或浏览器的 cookie 中,从根本上将维护用户状态的责任交给客户端:
对于成功登录后进行的 HTTP 请求,特别是对受保护且具有受限访问权限的 API 端点的请求,客户端必须将此令牌附加到请求中。更具体地说,JSON Web Token必须包含在请求的Authorization头部中作为Bearer:
Authorization: Bearer <JSON Web Token>
当服务器收到对受保护的 API 端点的请求时,它会检查请求的Authorization头部是否包含有效的 JWT,然后验证签名以识别发送者,并确保请求数据未被损坏。如果令牌有效,则请求的客户端将被授予对关联操作或资源的访问权限,否则将返回授权错误。
在骨架应用程序中,当用户使用电子邮件和密码登录时,后端将使用仅在服务器上可用的秘钥生成带有用户 ID 的签名 JWT。然后,当用户尝试查看任何用户配置文件、更新其帐户详细信息或删除其用户帐户时,将需要此令牌进行验证。
实现用户模型来存储和验证用户数据,然后将其与 API 集成以基于 JWT 执行 CRUD 操作,将产生一个功能齐全的独立后端。在本章的其余部分,我们将看看如何在 MERN 堆栈和设置中实现这一点。
实现骨架后端
为了开始开发 MERN 骨架的后端部分,我们将首先设置项目文件夹,安装和配置必要的 npm 模块,然后准备运行脚本以帮助开发和运行代码。然后,我们将逐步通过代码实现用户模型、API 端点和基于 JWT 的身份验证,以满足我们之前为面向用户的功能定义的规范。
本章讨论的代码以及完整的骨架应用程序的代码可在 GitHub 的存储库github.com/shamahoque/…中找到。仅后端的代码可在同一存储库的名为mern-skeleton-backend的分支中找到。您可以克隆此代码,并在阅读本章其余部分的代码解释时运行应用程序。
文件夹和文件结构
以下文件夹结构仅显示与 MERN 骨架后端相关的文件。有了这些文件,我们将生成一个功能齐全的独立服务器端应用程序:
| mern_skeleton/
| -- config/
| --- config.js
| -- server/
| --- controllers/
| ---- auth.controller.js
| ---- user.controller.js
| --- helpers/
| ---- dbErrorHandler.js
| --- models/
| ---- user.model.js
| --- routes/
| ---- auth.routes.js
| ---- user.routes.js
| --- express.js
| --- server.js
| -- .babelrc
| -- nodemon.json
| -- package.json
| -- template.js
| -- webpack.config.server.js
这个结构将在下一章进一步扩展,在那里我们通过添加React前端来完成骨架应用程序。
项目设置
如果开发环境已经设置好,我们可以初始化 MERN 项目以开始开发后端。首先,我们将在项目文件夹中初始化package.json,配置和安装开发依赖项,设置用于代码的配置变量,并更新package.json以帮助开发和运行代码的运行脚本。
初始化package.json
我们需要一个package.json文件来存储有关项目的元信息,列出模块依赖项及其版本号,并定义运行脚本。要在项目文件夹中初始化package.json文件,请从命令行转到项目文件夹并运行npm init,然后按照说明添加必要的细节。有了package.json文件后,我们可以继续设置和开发,并在代码实现过程中根据需要更新文件。
开发依赖项
为了开始开发并运行后端服务器代码,我们将配置和安装 Babel、Webpack 和 Nodemon,如第二章中所讨论的那样,对于仅后端,进行一些微小的调整。
Babel
由于我们将使用 ES6 编写后端代码,我们将配置和安装 Babel 模块来转换 ES6。
首先,在.babelrc文件中配置 Babel,使用最新 JS 特性的预设和一些目前未在babel-preset-env下覆盖的 stage-x 特性。
mern-skeleton/.babelrc:
{
"presets": [
"env",
"stage-2"
]
}
接下来,我们从命令行安装 Babel 模块作为devDependencies:
npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-stage-2
一旦模块安装完成,您会注意到package.json文件中的devDependencies列表已更新。
Webpack
我们需要 Webpack 使用 Babel 编译和捆绑服务器端代码,并且对于配置,我们可以使用在第二章中讨论的相同的webpack.config.server.js。
从命令行运行以下命令来安装webpack,webpack-cli和webpack-node-externals模块:
npm install --save-dev webpack webpack-cli webpack-node-externals
这将安装 Webpack 模块并更新package.json文件。
Nodemon
为了在开发过程中更新代码时自动重新启动 Node 服务器,我们将使用 Nodemon 来监视服务器代码的更改。我们可以使用与第二章中讨论的相同的安装和配置指南,准备开发环境。
配置变量
在config/config.js文件中,我们将定义一些与服务器端配置相关的变量,这些变量将在代码中使用,但不应作为最佳实践硬编码,也是出于安全目的。
mern-skeleton/config/config.js:
const config = {
env: process.env.NODE_ENV || 'development',
port: process.env.PORT || 3000,
jwtSecret: process.env.JWT_SECRET || "YOUR_secret_key",
mongoUri: process.env.MONGODB_URI ||
process.env.MONGO_HOST ||
'mongodb://' + (process.env.IP || 'localhost') + ':' +
(process.env.MONGO_PORT || '27017') +
'/mernproject'
}
export default config
定义的配置变量有:
-
env:区分开发和生产模式 -
端口:定义服务器的监听端口 -
jwtSecret:用于签署 JWT 的秘钥 -
mongoUri:项目的 MongoDB 数据库位置
运行脚本
为了在开发后端代码时运行服务器,我们可以从package.json文件中的npm run development脚本开始。对于完整的骨架应用程序,我们将使用第二章中定义的相同的运行脚本,准备开发环境。
mern-skeleton/package.json:
"scripts": {
"development": "nodemon"
}
npm run development:从项目文件夹的命令行中运行此命令基本上会根据nodemon.js中的配置启动 Nodemon。配置指示 Nodemon 监视服务器文件的更新,并在更新时重新构建文件,然后重新启动服务器,以便立即使用更改。
准备服务器
在本节中,我们将集成 Express、Node 和 MongoDB,以在开始实现特定于用户的功能之前运行完全配置的服务器。
配置 Express
要使用 Express,我们将首先安装 Express,然后在server/express.js文件中添加和配置它。
从命令行运行以下命令来安装带有--save标志的express模块,以便package.json文件会自动更新:
npm install express --save
一旦 Express 安装完成,我们可以将其导入到express.js文件中,并根据需要进行配置,并使其对整个应用程序可用。
mern-skeleton/server/express.js:
import express from 'express'
const app = express()
/*... configure express ... */
export default app
为了正确处理 HTTP 请求并提供响应,我们将使用以下模块来配置 Express:
-
body-parser:用于处理流式请求对象解析复杂性的主体解析中间件,以便我们可以通过在请求主体中交换 JSON 来简化浏览器-服务器通信: -
安装
body-parser模块:npm install body-parser --save -
配置 Express:
bodyParser.json()和bodyParser.urlencoded({ extended: true }) -
cookie-parser:用于解析和设置请求对象中的 cookie 的 cookie 解析中间件:
安装cookie-parser模块:npm install cookie-parser --save
压缩:压缩中间件,将尝试压缩所有通过中间件传递的请求的响应主体:
安装compression模块:npm install compression --save
头盔:一组中间件函数,通过设置各种 HTTP 头部来帮助保护 Express 应用程序:
安装头盔模块:npm install helmet --save
cors:中间件以启用CORS(跨源资源共享):
安装cors模块:npm install cors --save
在安装了上述模块之后,我们可以更新express.js来导入这些模块并在导出到服务器其余代码中使用之前配置 Express 应用程序。
更新后的mern-skeleton/server/express.js代码应该如下所示:
import express from 'express'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import compress from 'compression'
import cors from 'cors'
import helmet from 'helmet'
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(cookieParser())
app.use(compress())
app.use(helmet())
app.use(cors())
export default app
启动服务器
通过配置 Express 应用程序来接受 HTTP 请求,我们可以继续使用它来实现服务器以监听传入的请求。
在mern-skeleton/server/server.js文件中,添加以下代码来实现服务器:
import config from './../config/config'
import app from './express'
app.listen(config.port, (err) => {
if (err) {
console.log(err)
}
console.info('Server started on port %s.', config.port)
})
我们首先导入配置变量来设置服务器将监听的端口号,然后配置 Express 应用程序以启动服务器。
要使此代码运行并继续开发,现在可以从命令行运行npm run development。如果代码没有错误,服务器应该开始运行,并且 Nodemon 会监视代码更改。
设置 Mongoose 并连接到 MongoDB
我们将使用Mongoose模块来在此骨架中实现用户模型,以及我们 MERN 应用程序的所有未来数据模型。在这里,我们将首先配置 Mongoose,并利用它来定义与 MongoDB 数据库的连接。
首先,要安装mongoose模块,请运行以下命令:
npm install mongoose --save
然后,更新server.js文件以导入mongoose模块,配置它以使用原生的 ES6 promises,并最终使用它来处理与项目的 MongoDB 数据库的连接。
mern-skeleton/server/server.js:
import mongoose from 'mongoose'
mongoose.Promise = global.Promise
mongoose.connect(config.mongoUri)
mongoose.connection.on('error', () => {
throw new Error(`unable to connect to database: ${mongoUri}`)
})
如果您在开发中运行代码,则保存此更新应重新启动现在已集成了 Mongoose 和 MongoDB 的服务器。
Mongoose 是一个 MongoDB 对象建模工具,它提供了基于模式的解决方案来对应用程序数据进行建模。它包括内置的类型转换、验证、查询构建和业务逻辑钩子。在此后端堆栈中使用 Mongoose 提供了对 MongoDB 的更高层次的功能,包括将对象模型映射到数据库文档。因此,使用 Node 和 MongoDB 后端进行开发变得更简单和更高效。要了解有关 Mongoose 的更多信息,请访问mongoosejs.com。
在根 URL 上提供 HTML 模板
现在,具有 Node、Express 和 MongoDB 功能的服务器正在运行,我们可以扩展它以响应根 URL/的传入请求并提供 HTML 模板。
在template.js文件中,添加一个 JS 函数,该函数返回一个简单的 HTML 文档,该文档将在浏览器屏幕上呈现Hello World。
mern-skeleton/template.js:
export default () => {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MERN Skeleton</title>
</head>
<body>
<div id="root">Hello World</div>
</body>
</html>`
}
要在根 URL 上提供此模板,请更新express.js文件以导入此模板,并在对'/'路由的 GET 请求的响应中发送它。
mern-skeleton/server/express.js:
import Template from './../template'
...
app.get('/', (req, res) => {
res.status(200).send(Template())
})
...
通过这个更新,在浏览器中打开根 URL 应该显示“Hello World”在页面上呈现。
如果您在本地机器上运行代码,根 URL 将是http://localhost:3000/。
用户模型
我们将在server/models/user.model.js文件中实现用户模型,使用 Mongoose 来定义包含必要用户数据字段的模式,为字段添加内置验证,并整合密码加密、认证和自定义验证等业务逻辑。
我们将首先导入mongoose模块,并使用它来生成一个UserSchema。
mern-skeleton/server/models/user.model.js:
import mongoose from 'mongoose'
const UserSchema = new mongoose.Schema({ … })
mongoose.Schema()函数以模式定义对象作为参数,生成一个新的 Mongoose 模式对象,可以在后端代码的其余部分中使用。
用户模式定义
生成新的 Mongoose 模式所需的用户模式定义对象将声明所有用户数据字段和相关属性。
名称
name字段是一个必填字段,类型为String。
mern-skeleton/server/models/user.model.js:
name: {
type: String,
trim: true,
required: 'Name is required'
},
电子邮件
email字段是一个必填字段,类型为String,必须匹配有效的电子邮件格式,并且在用户集合中也必须是“唯一”的。
mern-skeleton/server/models/user.model.js:
email: {
type: String,
trim: true,
unique: 'Email already exists',
match: [/.+\@.+\..+/, 'Please fill a valid email address'],
required: 'Email is required'
},
创建和更新时间戳
created和updated字段是Date值,将被程序生成以记录用户创建和更新的时间戳。
mern-skeleton/server/models/user.model.js:
created: {
type: Date,
default: Date.now
},
updated: Date,
哈希密码和盐
hashed_password和salt字段代表我们将用于认证的加密用户密码。
mern-skeleton/server/models/user.model.js:
hashed_password: {
type: String,
required: "Password is required"
},
salt: String
出于安全目的,实际密码字符串不会直接存储在数据库中,而是单独处理。
用于认证的密码
密码字段对于在任何应用程序中提供安全用户认证非常重要,它需要作为用户模型的一部分进行加密、验证和安全认证。
作为虚拟字段
用户提供的password字符串不会直接存储在用户文档中。相反,它被处理为一个“虚拟”字段。
mern-skeleton/server/models/user.model.js:
UserSchema
.virtual('password')
.set(function(password) {
this._password = password
this.salt = this.makeSalt()
this.hashed_password = this.encryptPassword(password)
})
.get(function() {
return this._password
})
当在用户创建或更新时接收到password值时,它将被加密为一个新的哈希值,并设置为hashed_password字段,以及在salt字段中设置salt值。
加密和认证
加密逻辑和盐生成逻辑,用于生成代表password值的hashed_password和salt值,被定义为UserSchema方法。
mern-skeleton/server/models/user.model.js:
UserSchema.methods = {
authenticate: function(plainText) {
return this.encryptPassword(plainText) === this.hashed_password
},
encryptPassword: function(password) {
if (!password) return ''
try {
return crypto
.createHmac('sha1', this.salt)
.update(password)
.digest('hex')
} catch (err) {
return ''
}
},
makeSalt: function() {
return Math.round((new Date().valueOf() * Math.random())) + ''
}
}
此外,authenticate方法也被定义为UserSchema方法,用于在用户提供的密码必须进行验证以进行登录时使用。
Node 中的crypto模块用于将用户提供的密码字符串加密为带有随机生成的salt值的hashed_password。当用户详细信息在创建或更新时保存到数据库中,hashed_password和 salt 将存储在用户文档中。在用户登录时,需要hashed_password和 salt 值来匹配和验证提供的密码字符串,使用之前定义的authenticate方法。
密码字段验证
为了在最终用户选择的实际密码字符串上添加验证约束,我们需要添加自定义验证逻辑并将其与模式中的hashed_password字段关联起来。
mern-skeleton/server/models/user.model.js:
UserSchema.path('hashed_password').validate(function(v) {
if (this._password && this._password.length < 6) {
this.invalidate('password', 'Password must be at least 6 characters.')
}
if (this.isNew && !this._password) {
this.invalidate('password', 'Password is required')
}
}, null)
为了确保在创建新用户或更新现有密码时确实提供了密码值,并且长度至少为六个字符,我们添加了自定义验证以在 Mongoose 尝试存储hashed_password值之前检查密码值。如果验证失败,逻辑将返回相关的错误消息。
一旦UserSchema被定义,并且所有与密码相关的业务逻辑都像之前讨论的那样被添加,我们最终可以在user.model.js文件的底部导出模式,以便在后端代码的其他部分中使用它。
mern-skeleton/server/models/user.model.js:
export default mongoose.model('User', UserSchema)
Mongoose 错误处理
向用户模式字段添加的验证约束将在将用户数据保存到数据库时引发错误消息。为了处理这些验证错误以及我们向数据库查询时可能引发的其他错误,我们将定义一个辅助方法来返回相关的错误消息,以便在请求-响应周期中适当地传播。
我们将在server/helpers/dbErrorHandler.js文件中添加getErrorMessage辅助方法。该方法将解析并返回与使用 Mongoose 查询 MongoDB 时发生的特定验证错误或其他错误相关联的错误消息。
mern-skeleton/server/helpers/dbErrorHandler.js:
const getErrorMessage = (err) => {
let message = ''
if (err.code) {
switch (err.code) {
case 11000:
case 11001:
message = getUniqueErrorMessage(err)
break
default:
message = 'Something went wrong'
}
} else {
for (let errName in err.errors) {
if (err.errors[errName].message)
message = err.errors[errName].message
}
}
return message
}
export default {getErrorMessage}
不是因为 Mongoose 验证器违规而抛出的错误将包含错误代码,并且在某些情况下需要以不同方式处理。例如,由于违反唯一约束而导致的错误将返回一个与 Mongoose 验证错误不同的错误对象。唯一选项不是验证器,而是用于构建 MongoDB 唯一索引的便捷助手,因此我们将添加另一个getUniqueErrorMessage方法来解析与唯一约束相关的错误对象,并构造适当的错误消息。
mern-skeleton/server/helpers/dbErrorHandler.js:
const getUniqueErrorMessage = (err) => {
let output
try {
let fieldName =
err.message.substring(err.message.lastIndexOf('.$') + 2,
err.message.lastIndexOf('_1'))
output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) +
' already exists'
} catch (ex) {
output = 'Unique field already exists'
}
return output
}
通过使用从此辅助文件导出的getErrorMessage函数,我们将在处理 Mongoose 执行的用户 CRUD 操作引发的错误时添加有意义的错误消息。
用户 CRUD API
Express 应用程序公开的用户 API 端点将允许前端对根据用户模型生成的文档执行 CRUD 操作。为了实现这些工作端点,我们将编写 Express 路由和相应的控制器回调函数,当这些声明的路由收到 HTTP 请求时应该执行这些函数。在本节中,我们将看看这些端点在没有任何身份验证限制的情况下如何工作。
用户 API 路由将在server/routes/user.routes.js中使用 Express 路由器声明,然后挂载到我们在server/express.js中配置的 Express 应用程序上。
mern-skeleton/server/express.js:
import userRoutes from './routes/user.routes'
...
app.use('/', userRoutes)
...
用户路由
user.routes.js文件中定义的用户路由将使用express.Router()来声明具有相关 HTTP 方法的路由路径,并分配应该在服务器接收到这些请求时调用的相应控制器函数。
我们将通过以下方式保持用户路由简单:
-
/api/users用于: -
使用 GET 列出用户
-
使用 POST 创建新用户
-
/api/users/:userId用于: -
使用 GET 获取用户
-
使用 PUT 更新用户
-
使用 DELETE 删除用户
生成的user.routes.js代码将如下所示(不包括需要为受保护的路由添加的身份验证考虑)。
mern-skeleton/server/routes/user.routes.js:
import express from 'express'
import userCtrl from '../controllers/user.controller'
const router = express.Router()
router.route('/api/users')
.get(userCtrl.list)
.post(userCtrl.create)
router.route('/api/users/:userId')
.get(userCtrl.read)
.put(userCtrl.update)
.delete(userCtrl.remove)
router.param('userId', userCtrl.userByID)
export default router
用户控制器
server/controllers/user.controller.js文件将包含在前面的用户路由声明中使用的控制器方法,作为服务器接收到路由请求时的回调。
user.controller.js文件将具有以下结构:
import User from '../models/user.model'
import _ from 'lodash'
import errorHandler from './error.controller'
const create = (req, res, next) => { … }
const list = (req, res) => { … }
const userByID = (req, res, next, id) => { … }
const read = (req, res) => { … }
const update = (req, res, next) => { … }
const remove = (req, res, next) => { … }
export default { create, userByID, read, list, remove, update }
控制器将使用errorHandler助手来在 Mongoose 发生错误时响应路由请求并提供有意义的消息。在更新具有更改值的现有用户时,它还将使用一个名为lodash的模块。
lodash是一个 JavaScript 库,提供常见编程任务的实用函数,包括对数组和对象的操作。要安装lodash,请从命令行运行npm install lodash --save。
先前定义的每个控制器函数都与路由请求相关,并将根据每个 API 用例进行详细说明。
创建新用户
创建新用户的 API 端点在以下路由中声明。
mern-skeleton/server/routes/user.routes.js:
router.route('/api/users').post(userCtrl.create)
当 Express 应用程序在'/api/users'处收到 POST 请求时,它会调用控制器中定义的create函数。
mern-skeleton/server/controllers/user.controller.js:
const create = (req, res, next) => {
const user = new User(req.body)
user.save((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
res.status(200).json({
message: "Successfully signed up!"
})
})
}
此函数使用从前端收到的用户 JSON 对象在req.body中创建新用户。user.save尝试在 Mongoose 对数据进行验证检查后将新用户保存到数据库中,因此将向请求的客户端返回错误或成功响应。
列出所有用户
获取所有用户的 API 端点在以下路由中声明。
mern-skeleton/server/routes/user.routes.js:
router.route('/api/users').get(userCtrl.list)
当 Express 应用程序在'/api/users'处收到 GET 请求时,它会执行list控制器函数。
mern-skeleton/server/controllers/user.controller.js:
const list = (req, res) => {
User.find((err, users) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
res.json(users)
}).select('name email updated created')
}
list控制器函数从数据库中找到所有用户,仅填充结果用户列表中的名称、电子邮件、创建和更新字段,然后将这些用户列表作为 JSON 对象数组返回给请求的客户端。
按 ID 加载用户以进行读取、更新或删除
读取、更新和删除的所有三个 API 端点都需要根据正在访问的用户的用户 ID 从数据库中检索用户。在响应特定的读取、更新或删除请求之前,我们将编程 Express 路由器执行此操作。
加载
每当 Express 应用程序收到与路径中包含:userId参数匹配的路由的请求时,该应用程序将首先执行userByID控制器函数,然后传播到传入请求特定的next函数。
mern-skeleton/server/routes/user.routes.js:
router.param('userId', userCtrl.userByID)
userByID控制器函数使用:userId参数中的值来查询数据库的_id,并加载匹配用户的详细信息。
mern-skeleton/server/controllers/user.controller.js:
const userByID = (req, res, next, id) => {
User.findById(id).exec((err, user) => {
if (err || !user)
return res.status('400').json({
error: "User not found"
})
req.profile = user
next()
})
}
如果在数据库中找到匹配的用户,则用户对象将附加到请求对象的profile键中。然后,使用next()中间件将控制传播到下一个相关的控制器函数。例如,如果原始请求是读取用户配置文件,则userById中的next()调用将转到read控制器函数。
阅读
声明了读取单个用户数据的 API 端点在以下路由中。
mern-skeleton/server/routes/user.routes.js:
router.route('/api/users/:userId').get(userCtrl.read)
当 Express 应用程序在'/api/users/:userId'接收到 GET 请求时,它执行userByID控制器函数,通过参数中的userId值加载用户,然后执行read控制器函数。
mern-skeleton/server/controllers/user.controller.js:
const read = (req, res) => {
req.profile.hashed_password = undefined
req.profile.salt = undefined
return res.json(req.profile)
}
read函数从req.profile中检索用户详细信息,并在将用户对象发送到请求客户端的响应之前删除敏感信息,如hashed_password和salt值。
更新
声明了更新单个用户的 API 端点在以下路由中。
mern-skeleton/server/routes/user.routes.js:
router.route('/api/users/:userId').put(userCtrl.update)
当 Express 应用程序在'/api/users/:userId'接收到 PUT 请求时,类似于read,它首先加载具有:userId参数值的用户,然后执行update控制器函数。
mern-skeleton/server/controllers/user.controller.js:
const update = (req, res, next) => {
let user = req.profile
user = _.extend(user, req.body)
user.updated = Date.now()
user.save((err) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
user.hashed_password = undefined
user.salt = undefined
res.json(user)
})
}
update函数从req.profile中检索用户详细信息,然后使用lodash模块来扩展和合并请求体中的更改以更新用户数据。在将此更新后的用户保存到数据库之前,updated字段将填充为当前日期以反映最后更新的时间戳。成功保存此更新后,更新后的用户对象将通过删除敏感数据,如hashed_password和salt,然后将用户对象发送到请求客户端的响应中。
删除
声明了删除用户的 API 端点在以下路由中。
mern-skeleton/server/routes/user.routes.js:
router.route('/api/users/:userId').delete(userCtrl.remove)
当 Express 应用程序在'/api/users/:userId'接收到 DELETE 请求时,类似于读取和更新,它首先通过 ID 加载用户,然后执行remove控制器函数。
mern-skeleton/server/controllers/user.controller.js:
const remove = (req, res, next) => {
let user = req.profile
user.remove((err, deletedUser) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
deletedUser.hashed_password = undefined
deletedUser.salt = undefined
res.json(deletedUser)
})
}
remove函数从req.profile中检索用户,并使用remove()查询从数据库中删除用户。成功删除后,将在响应中返回已删除的用户对象。
到目前为止,通过实现 API 端点,任何客户端都可以对用户模型执行 CRUD 操作,但我们希望通过认证和授权来限制对其中一些操作的访问。
用户认证和受保护的路由
为了限制用户操作(如用户资料查看、用户更新和用户删除),我们将实现 JWT 的登录认证,然后保护和授权读取、更新和删除路由。
登录和登出的认证相关 API 端点将在server/routes/auth.routes.js中声明,然后挂载到server/express.js中的 Express 应用程序上。
mern-skeleton/server/express.js:
import authRoutes from './routes/auth.routes'
...
app.use('/', authRoutes)
...
认证路由
使用express.Router()在auth.routes.js文件中定义了两个认证 API,以声明具有相关 HTTP 方法的路由路径,并分配了应在收到这些路由的请求时调用的相应认证控制器函数。
认证路由如下:
-
'/auth/signin':使用电子邮件和密码进行用户认证的 POST 请求 -
'/auth/signout':GET 请求以清除包含在登录后设置在响应对象上的 JWT 的 cookie
生成的mern-skeleton/server/routes/auth.routes.js文件将如下所示:
import express from 'express'
import authCtrl from '../controllers/auth.controller'
const router = express.Router()
router.route('/auth/signin')
.post(authCtrl.signin)
router.route('/auth/signout')
.get(authCtrl.signout)
export default router
认证控制器
server/controllers/auth.controller.js中的认证控制器函数不仅处理登录和登出路由的请求,还提供 JWT 和express-jwt功能,以启用受保护的用户 API 端点的认证和授权。
auth.controller.js文件将具有以下结构:
import User from '../models/user.model'
import jwt from 'jsonwebtoken'
import expressJwt from 'express-jwt'
import config from './../../config/config'
const signin = (req, res) => { … }
const signout = (req, res) => { … }
const requireSignin = …
const hasAuthorization = (req, res) => { … }
export default { signin, signout, requireSignin, hasAuthorization }
以下详细说明了四个控制器函数,以展示后端如何使用 JSON Web Tokens 实现用户认证。
登录
在以下路由中声明了用于登录用户的 API 端点。
mern-skeleton/server/routes/auth.routes.js:
router.route('/auth/signin').post(authCtrl.signin)
当 Express 应用程序在'/auth/signin'收到 POST 请求时,它会执行signin控制器函数。
mern-skeleton/server/controllers/auth.controller.js:
const signin = (req, res) => {
User.findOne({
"email": req.body.email
}, (err, user) => {
if (err || !user)
return res.status('401').json({
error: "User not found"
})
if (!user.authenticate(req.body.password)) {
return res.status('401').send({
error: "Email and password don't match."
})
}
const token = jwt.sign({
_id: user._id
}, config.jwtSecret)
res.cookie("t", token, {
expire: new Date() + 9999
})
return res.json({
token,
user: {_id: user._id, name: user.name, email: user.email}
})
})
}
POST请求对象在req.body中接收电子邮件和密码。该电子邮件用于从数据库中检索匹配的用户。然后,UserSchema中定义的密码验证方法用于验证从客户端req.body中接收的密码。
如果密码成功验证,JWT 模块将用秘密密钥和用户的_id值生成一个签名的 JWT。
安装jsonwebtoken模块,通过在命令行中运行npm install jsonwebtoken --save来使其在导入此控制器时可用。
然后,签名的 JWT 将与用户详细信息一起返回给经过身份验证的客户端。可选地,我们还可以将令牌设置为响应对象中的 cookie,以便在客户端选择 JWT 存储的情况下可用。在客户端,当从服务器请求受保护的路由时,必须将此令牌附加为Authorization头。
登出
在以下路由中声明了用于登出用户的 API 端点。
mern-skeleton/server/routes/auth.routes.js:
router.route('/auth/signout').get(authCtrl.signout)
当 Express 应用程序在'/auth/signout'处收到 GET 请求时,它会执行signout控制器函数。
mern-skeleton/server/controllers/auth.controller.js:
const signout = (req, res) => {
res.clearCookie("t")
return res.status('200').json({
message: "signed out"
})
}
signout函数清除包含签名 JWT 的响应 cookie。这是一个可选的端点,如果前端根本不使用 cookie,则对身份验证没有真正必要。使用 JWT,用户状态存储是客户端的责任,并且除了 cookie 之外,客户端存储的选择有多种选项。在登出时,客户端需要在客户端删除令牌,以确立用户不再经过身份验证。
使用 express-jwt 保护路由
为了保护对读取、更新和删除路由的访问,服务器需要检查请求的客户端是否真的是经过身份验证和授权的用户。
在访问受保护的路由时,我们将使用express-jwt模块来检查请求用户是否已登录并具有有效的 JWT。
express-jwt模块是验证 JSON Web 令牌的中间件。运行npm install express-jwt --save来安装express-jwt。
要求登录
auth.controller.js中的requireSignin方法使用express-jwt来验证传入请求的Authorization头中是否有有效的 JWT。如果令牌有效,它会将经过验证的用户 ID 附加在请求对象的'auth'键中,否则会抛出身份验证错误。
mern-skeleton/server/controllers/auth.controller.js:
const requireSignin = expressJwt({
secret: config.jwtSecret,
userProperty: 'auth'
})
我们可以将requireSignin添加到任何应受保护免受未经身份验证访问的路由。
授权已登录用户
对于一些受保护的路由,如更新和删除,除了检查身份验证外,我们还希望确保请求的用户只能更新或删除自己的用户信息。为了实现这一点,在auth.controller.js中定义的hasAuthorization函数在允许相应的 CRUD 控制器函数继续之前,检查经过身份验证的用户是否与正在更新或删除的用户相同。
mern-skeleton/server/controllers/auth.controller.js:
const hasAuthorization = (req, res, next) => {
const authorized = req.profile && req.auth && req.profile._id ==
req.auth._id
if (!(authorized)) {
return res.status('403').json({
error: "User is not authorized"
})
}
next()
}
req.auth对象由express-jwt在身份验证验证后的requireSignin中填充,而req.profile由user.controller.js中的userByID函数填充。我们将在需要身份验证和授权的路由中添加hasAuthorization函数。
保护用户路由
我们将在需要受到身份验证和授权保护的用户路由声明中添加requireSignin和hasAuthorization。
更新user.routes.js中的读取、更新和删除路由如下。
mern-skeleton/server/routes/user.routes.js:
import authCtrl from '../controllers/auth.controller'
...
router.route('/api/users/:userId')
.get(authCtrl.requireSignin, userCtrl.read)
.put(authCtrl.requireSignin, authCtrl.hasAuthorization,
userCtrl.update)
.delete(authCtrl.requireSignin, authCtrl.hasAuthorization,
userCtrl.remove)
...
只需要身份验证验证的用户信息读取路由,而更新和删除路由在执行这些 CRUD 操作之前应检查身份验证和授权。
对于 express-jwt 的身份验证错误处理
处理由express-jwt抛出的与验证传入请求中的 JWT 令牌相关的错误时,我们需要在 Express 应用程序配置中添加以下错误捕获代码,该配置位于mern-skeleton/server/express.js中的代码末尾,在挂载路由之后并在导出应用程序之前:
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
res.status(401).json({"error" : err.name + ": " + err.message})
}
})
当令牌由于某种原因无法验证时,express-jwt会抛出一个名为UnauthorizedError的错误。我们在这里捕获此错误,以便向请求的客户端返回401状态。
通过实施用户身份验证来保护路由,我们已经涵盖了骨架 MERN 应用程序的所有期望功能。在下一节中,我们将看看如何在不实施前端的情况下检查这个独立后端是否按预期运行。
检查独立后端
在选择用于检查后端 API 的工具时,有许多选项,从命令行工具 curl(github.com/curl/curl)到 Advanced REST Client(chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo)—一个具有交互式用户界面的 Chrome 扩展程序应用。
要检查本章实现的 API,首先从命令行运行服务器,并使用这些工具之一请求路由。如果您在本地计算机上运行代码,则根 URL 为http://localhost:3000/。
使用 ARC,我们将展示检查实现的 API 端点的五个用例的预期行为。
创建新用户
首先,我们将使用/api/users的 POST 请求创建一个新用户,并在请求体中传递名称、电子邮件和密码值。当用户在数据库中成功创建且没有任何验证错误时,我们将看到一个 200 OK 的成功消息,如下面的屏幕截图所示:
获取用户列表
我们可以通过对/api/users进行GET请求来查看数据库中是否有新用户。响应应包含存储在数据库中的所有用户对象的数组:
尝试获取单个用户
接下来,我们将尝试在未登录的情况下访问受保护的 API。对任何一个用户进行GET请求将返回 401 未经授权,例如,在以下示例中,对/api/users/5a1c7ead1a692aa19c3e7b33的GET请求将返回 401:
登录
为了能够访问受保护的路由,我们将使用第一个示例中创建的用户的凭据进行登录。要登录,需要在/auth/signin发送带有电子邮件和密码的 POST 请求。成功登录后,服务器将返回一个签名的 JWT 和用户详细信息。我们将需要这个令牌来访问获取单个用户的受保护路由:
成功获取单个用户
使用登录后收到的令牌,我们现在可以访问之前失败的受保护路由。在向/api/users/5a1c7ead1a692aa19c3e7b33发出 GET 请求时,令牌以 Bearer 方案设置在Authorization标头中,这次用户对象成功返回。
总结
在本章中,我们使用 Node、Express 和 MongoDB 开发了一个完全独立的服务器端应用程序,涵盖了 MERN 骨架应用程序的第一部分。在后端,我们实现了以下功能:
-
用 Mongoose 实现的用于存储用户数据的用户模型
-
使用 Express 实现的用户 API 端点执行 CRUD 操作
-
使用 JWT 和
express-jwt实现受保护路由的用户认证
我们还通过配置 Webpack 编译 ES6 代码和 Nodemon 在代码更改时重新启动服务器来设置开发流程。最后,我们使用 Chrome 的高级 Rest API 客户端扩展应用程序检查了 API 的实现。
我们现在准备在下一章中扩展这个后端应用程序代码,添加 React 前端,并完成 MERN 骨架应用程序。
第四章:添加 React 前端以完成 MERN
没有前端的 Web 应用程序是不完整的。这是用户与之交互的部分,对于任何 Web 体验都至关重要。在本章中,我们将使用 React 为我们在上一章开始构建的 MERN 骨架应用程序的后端实现的基本用户和认证功能添加交互式用户界面。
我们将涵盖以下主题,以添加一个可工作的前端并完成 MERN 骨架应用程序:
-
骨架的前端特性
-
使用 React、React Router 和 Material-UI 进行开发设置
-
后端用户 API 集成
-
认证集成
-
主页、用户、注册、登录、用户资料、编辑和删除视图
-
导航菜单
-
基本的服务器端渲染
骨架前端
为了完全实现在第三章的功能拆分部分中讨论的骨架应用程序功能,即使用 MongoDB、Express 和 Node 构建后端,我们将向基本应用程序添加以下用户界面组件:
-
主页:在根 URL 上呈现的视图,欢迎用户访问 Web 应用程序
-
用户列表页面:获取并显示数据库中所有用户列表的视图,并链接到单个用户资料
-
注册页面:一个带有用户注册表单的视图,允许新用户创建用户账户,并在成功创建后将他们重定向到登录页面
-
登录页面:带有登录表单的视图,允许现有用户登录,以便他们可以访问受保护的视图和操作
-
个人资料页面:获取并显示单个用户信息的组件,只有已登录用户才能访问,并且还包含编辑和删除选项,仅当已登录用户查看自己的个人资料时才可见
-
编辑个人资料页面:一个表单,获取用户的信息,允许他们编辑信息,并且仅当已登录用户尝试编辑自己的个人资料时才可访问
-
删除用户组件:一个选项,允许已登录用户在确认意图后删除自己的个人资料
-
菜单导航栏:列出所有可用和相关的视图的组件,还帮助指示用户在应用程序中的当前位置
以下 React 组件树图显示了我们将开发的所有 React 组件,以构建出这个基本应用程序的视图:
MainRouter将是根 React 组件,其中包含应用程序中的所有其他自定义 React 视图。Home,Signup,Signin,Users,Profile和EditProfile将在使用 React Router 声明的各个路由上呈现,而Menu组件将在所有这些视图中呈现,DeleteUser将成为Profile视图的一部分。
本章讨论的代码以及完整的骨架代码都可以在 GitHub 的存储库中找到,网址为github.com/shamahoque/…。您可以克隆此代码,并在本章的其余部分中阅读代码解释时运行应用程序。
文件夹和文件结构
以下文件夹结构显示了要添加到骨架中的新文件夹和文件,以完成具有 React 前端的骨架:
| mern_skeleton/
| -- client/
| --- assets/
| ---- images/
| --- auth/
| ---- api-auth.js
| ---- auth-helper.js
| ---- PrivateRoute.js
| ---- Signin.js
| --- core/
| ---- Home.js
| ---- Menu.js
| --- user/
| ---- api-user.js
| ---- DeleteUser.js
| ---- EditProfile.js
| ---- Profile.js
| ---- Signup.js
| ---- Users.js
| --- App.js
| --- main.js
| --- MainRouter.js
| -- server/
| --- devBundle.js
| -- webpack.config.client.js
| -- webpack.config.client.production.js
客户端文件夹将包含 React 组件,辅助程序和前端资产,例如图像和 CSS。除了这个文件夹和用于编译和捆绑客户端代码的 Webpack 配置之外,我们还将修改一些其他现有文件,以整合完整的骨架。
为 React 开发设置
在我们可以在现有的骨架代码库中开始使用 React 进行开发之前,我们首先需要添加配置来编译和捆绑前端代码,添加构建交互式界面所需的与 React 相关的依赖项,并在 MERN 开发流程中将所有这些联系在一起。
配置 Babel 和 Webpack
为了在开发期间编译和捆绑客户端代码并在生产环境中运行它,我们将更新 Babel 和 Webpack 的配置。
Babel
为了编译 React,首先安装 Babel 的 React 预设模块作为开发依赖项:
npm install babel-preset-react --save-dev
然后,更新.babelrc以包括该模块,并根据需要配置react-hot-loader Babel 插件。
mern-skeleton/.babelrc:
{
"presets": [
"env",
"stage-2",
"react"
],
"plugins": [
"react-hot-loader/babel"
]
}
Webpack
在使用 Babel 编译后捆绑客户端代码,并为更快的开发启用react-hot-loader,安装以下模块:
npm install --save-dev webpack-dev-middleware webpack-hot-middleware file-loader
npm install --save react-hot-loader
然后,为了配置前端开发的 Webpack 并构建生产捆绑包,我们将添加一个webpack.config.client.js文件和一个webpack.config.client.production.js文件,其中包含与第二章中描述的相同配置代码,准备开发环境。
加载 Webpack 中间件进行开发
在开发过程中,当我们运行服务器时,Express 应用程序应加载与客户端代码设置的配置相关的 Webpack 中间件,以便集成前端和后端开发工作流程。为了实现这一点,我们将使用第二章中讨论的devBundle.js文件,准备开发环境,设置一个compile方法,该方法接受 Express 应用程序并配置它使用 Webpack 中间件。server文件夹中的devBundle.js将如下所示。
mern-skeleton/server/devBundle.js:
import config from './../config/config'
import webpack from 'webpack'
import webpackMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import webpackConfig from './../webpack.config.client.js'
const compile = (app) => {
if(config.env === "development"){
const compiler = webpack(webpackConfig)
const middleware = webpackMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath
})
app.use(middleware)
app.use(webpackHotMiddleware(compiler))
}
}
export default {
compile
}
然后,通过添加以下突出显示的行,导入并调用express.js中的compile方法,仅在开发时添加。
mern-skeleton/server/express.js:
**import devBundle from './devBundle'**
const app = express()
**devBundle.compile(app)**
这两行突出显示的代码仅用于开发模式,在构建生产代码时应将其注释掉。此代码将在 Express 应用程序以开发模式运行时导入中间件和 Webpack 配置,然后启动 Webpack 编译和捆绑客户端代码。捆绑后的代码将放置在dist文件夹中。
使用 Express 提供静态文件
为了确保 Express 服务器正确处理对静态文件(如 CSS 文件、图像或捆绑的客户端 JS)的请求,我们将通过在express.js中添加以下配置来配置它从dist文件夹中提供静态文件。
mern-skeleton/server/express.js:
import path from 'path'
const CURRENT_WORKING_DIR = process.cwd()
app.use('/dist', express.static(path.join(CURRENT_WORKING_DIR, 'dist')))
更新模板以加载捆绑的脚本
为了在 HTML 视图中添加捆绑的前端代码,我们将更新template.js文件,将脚本文件从dist文件夹添加到<body>标签的末尾。
mern-skeleton/template.js:
...
<body>
<div id="root"></div>
**<script type="text/javascript" src="/dist/bundle.js"></script>**
</body>
添加 React 依赖项
前端视图将主要使用 React 实现。此外,为了实现客户端路由,我们将使用 React Router,并且为了增强用户体验,使其看起来更加流畅,我们将使用 Material-UI。
React
在本书中,我们将使用 React 16 来编写前端代码。要开始编写React组件代码,我们需要安装以下模块作为常规依赖项:
npm install --save react react-dom
React Router
React Router 提供了一组导航组件,可以在 React 应用程序的前端进行路由。为了利用声明式路由并拥有可书签的 URL 路由,我们将添加以下 React Router 模块:
npm install --save react-router react-router-dom
Material-UI
为了保持我们的 MERN 应用程序中的 UI 简洁,而不过多涉及 UI 设计和实现,我们将利用Material-UI库。它提供了可立即使用和可定制的React组件,实现了谷歌的材料设计。要开始使用 Material-UI 组件制作前端,我们需要安装以下模块:
npm install --save material-ui@1.0.0-beta.43 material-ui-icons
在撰写本文时,Material-UI 的最新预发布版本是1.0.0-beta.43,建议安装此确切版本,以确保示例项目的代码不会中断。
将Roboto字体按照 Material-UI 的建议添加,并使用Material-UI图标,我们将在template.js文件的 HTML 文档的<head>部分中添加相关的样式链接:
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
随着开发配置的全部设置和必要的 React 模块添加到代码库中,我们现在可以开始实现自定义的 React 组件。
实现 React 视图
一个功能齐全的前端应该将 React 组件与后端 API 集成,并允许用户根据授权在应用程序中无缝导航。为了演示如何为这个 MERN 骨架实现一个功能齐全的前端视图,我们将从详细说明如何在根路由处呈现主页组件开始,然后涵盖后端 API 和用户认证集成,然后突出实现剩余视图组件的独特方面。
呈现主页
在根路由处实现和呈现一个工作的Home组件的过程也将暴露骨架中前端代码的基本结构。我们将从顶级入口组件开始,该组件包含整个 React 应用程序,并呈现链接应用程序中所有 React 组件的主路由器组件。
在main.js的入口点
客户端文件夹中的client/main.js文件将是渲染完整 React 应用程序的入口点。在这段代码中,我们导入将包含完整前端并将其呈现到在template.js中指定的 HTML 文档中的div元素的根或顶级 React 组件。
mern-skeleton/client/main.js:
import React from 'react'
import { render } from 'react-dom'
import App from './App'
render(<App/>, document.getElementById('root'))
根 React 组件
定义应用程序前端所有组件的顶层 React 组件在client/App.js文件中。在这个文件中,我们配置 React 应用程序以使用定制的 Material-UI 主题渲染视图组件,启用前端路由,并确保 React Hot Loader 可以在我们开发组件时立即加载更改。
定制 Material-UI 主题
可以使用MuiThemeProvider组件轻松定制 Material-UI 主题,并通过在createMuiTheme()中配置自定义值来设置主题变量。
mern-skeleton/client/App.js:
import {MuiThemeProvider, createMuiTheme} from 'material-ui/styles'
import {indigo, pink} from 'material-ui/colors'
const theme = createMuiTheme({
palette: {
primary: {
light: '#757de8',
main: '#3f51b5',
dark: '#002984',
contrastText: '#fff',
},
secondary: {
light: '#ff79b0',
main: '#ff4081',
dark: '#c60055',
contrastText: '#000',
},
openTitle: indigo['400'],
protectedTitle: pink['400'],
type: 'light'
}
})
对于骨架,我们只需进行最少的定制,通过将一些颜色值设置为 UI 中使用的值。在这里生成的主题变量将传递给我们构建的所有组件,并在其中可用。
用 MUI 主题和 BrowserRouter 包装根组件
我们创建的自定义 React 组件将通过MainRouter组件中指定的前端路由进行访问。基本上,这个组件包含了为应用程序开发的所有自定义视图。在App.js中定义根组件时,我们使用MuiThemeProvider将MainRouter组件包装起来,以便让它可以访问 Material-UI 主题,并使用BrowserRouter启用 React Router 的前端路由。之前定义的自定义主题变量作为 prop 传递给MuiThemeProvider,使主题在所有自定义 React 组件中可用。
mern-skeleton/client/App.js:
import React from 'react'
import MainRouter from './MainRouter'
import {BrowserRouter} from 'react-router-dom'
const App = () => (
<BrowserRouter>
<MuiThemeProvider theme={theme}>
<MainRouter/>
</MuiThemeProvider>
</BrowserRouter>
)
将根组件标记为热导出
在App.js中的最后一行代码导出App组件使用react-hot-loader中的hot模块将根组件标记为hot。这将在开发过程中启用 React 组件的实时重新加载。
mern-skeleton/client/App.js:
import { hot } from 'react-hot-loader'
...
export default hot(module)(App)
对于我们的 MERN 应用程序,在这一点之后,我们不需要太多更改main.js和App.js的代码,可以继续通过在MainRouter组件中注入新组件来构建 React 应用程序的其余部分。
向 MainRouter 添加主页路由
MainRouter.js代码将帮助根据应用程序中的路由或位置渲染我们的自定义 React 组件。在这个第一个版本中,我们只会添加根路由来渲染Home组件。
mern-skeleton/client/MainRouter.js:
import React, {Component} from 'react'
import {Route, Switch} from 'react-router-dom'
import Home from './core/Home'
class MainRouter extends Component {
render() {
return (<div>
<Switch>
<Route exact path="/" component={Home}/>
</Switch>
</div>)
}
}
export default MainRouter
随着我们开发更多的视图组件,我们将更新MainRouter以在Switch组件中为新组件添加路由。
React Router 中的Switch组件专门用于呈现路由。换句话说,它只呈现与请求的路由路径匹配的第一个子组件。而不在Switch中嵌套时,每个Route组件在路径匹配时都会进行包容性渲染。例如,对'/'的请求也会匹配'/contact'的路由。
Home 组件
当用户访问根路由时,Home组件将在浏览器上呈现,并且我们将使用 Material-UI 组件来组合它。以下屏幕截图显示了Home组件和稍后在本章中作为独立组件实现的Menu组件,以提供应用程序中的导航:
Home组件和其他视图组件将按照通用的代码结构在浏览器中呈现给用户进行交互,该结构包含以下部分,按照给定的顺序。
导入
组件文件将从 React、Material-UI、React Router 模块、图像、CSS、API fetch 和我们代码中的 auth helpers 中导入所需的特定组件。例如,在Home.js中的Home组件代码中,我们使用以下导入。
mern-skeleton/client/core/Home.js:
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {withStyles} from 'material-ui/styles'
import Card, {CardContent, CardMedia} from 'material-ui/Card'
import Typography from 'material-ui/Typography'
import seashellImg from './../assets/images/seashell.jpg'
图像文件保存在client/assets/images/文件夹中,并被导入/添加到Home组件中。
样式声明
在导入之后,我们将根据需要使用Material-UI主题变量来定义 CSS 样式,以便对组件中的元素进行样式设置。对于Home.js中的Home组件,我们有以下样式。
mern-skeleton/client/core/Home.js:
const styles = theme => ({
card: {
maxWidth: 600,
margin: 'auto',
marginTop: theme.spacing.unit * 5
},
title: {
padding:`${theme.spacing.unit * 3}px ${theme.spacing.unit * 2.5}px
${theme.spacing.unit * 2}px`,
color: theme.palette.text.secondary
},
media: {
minHeight: 330
}
})
在这里定义的 JSS 样式对象将被注入到组件中,并用于对组件中的元素进行样式设置,就像下面的Home组件定义中所示。
Material-UI 使用 JSS,这是一种 CSS-in-JS 的样式解决方案,用于向组件添加样式。JSS 使用 JavaScript 作为描述样式的语言。本书不会详细介绍 CSS 和样式实现,而是更多地依赖于 Material-UI 组件的默认外观和感觉。要了解更多关于 JSS 的信息,请访问cssinjs.org/?v=v9.8.1。要了解如何自定义Material-UI组件样式的示例,请查看 Material-UI 文档material-ui-next.com/。
组件定义
在组件定义中,我们将组合组件的内容和行为。Home组件将包含一个 Material-UI 的Card,其中包括一个标题、一个图像和一个标题,所有这些都使用之前定义的类进行样式设置,并作为 props 传递进来。
mern-skeleton/client/core/Home.js:
class Home extends Component {
render() {
const {classes} = this.props
return (
<div>
<Card className={classes.card}>
<Typography type="headline" component="h2" className=
{classes.title}>
Home Page
</Typography>
<CardMedia className={classes.media} image={seashellImg}
title="Unicorn Shells"/>
<CardContent>
<Typography type="body1" component="p">
Welcome to the Mern Skeleton home page
</Typography>
</CardContent>
</Card>
</div>
)
}
}
PropTypes 验证
为了验证将样式声明作为 props 注入到组件中的要求,我们向已定义的组件添加了PropTypes要求验证器。
mern-skeleton/client/core/Home.js:
Home.propTypes = {
classes: PropTypes.object.isRequired
}
导出组件
最后,在组件文件的最后一行代码中,我们将使用Material-UI中的withStyles导出组件并传递定义的样式。像这样使用withStyles创建了一个具有对定义样式对象的访问权限的Higher-order component (HOC)。
mern-skeleton/client/core/Home.js:
export default withStyles(styles)(Home)
导出的组件现在可以在其他组件中进行组合使用,就像我们在之前讨论的MainRouter组件中的路由中使用Home组件一样。
在我们的 MERN 应用程序中要实现的其他视图组件将遵循相同的结构。在本书的其余部分,我们将主要关注组件定义,突出已实现组件的独特方面。
捆绑图像资源
我们导入到Home组件视图中的静态图像文件也必须与编译后的 JS 代码一起包含在捆绑包中,以便代码可以访问和加载它。为了实现这一点,我们需要更新 Webpack 配置文件,添加一个模块规则来加载、捆绑和发射图像文件到输出目录中,该目录包含编译后的前端和后端代码。
更新webpack.config.client.js,webpack.config.server.js和webpack.config.client.production.js文件,在使用babel-loader后添加以下模块规则:
[ …
{
test: /\.(ttf|eot|svg|gif|jpg|png)(\?[\s\S]+)?$/,
use: 'file-loader'
}
]
这个模块规则使用 Webpack 的file-loader npm 模块,需要安装为开发依赖,如下所示:
npm install --save-dev file-loader
运行并在浏览器中打开
到目前为止,客户端代码可以运行,以在根 URL 的浏览器中查看Home组件。要运行应用程序,请使用以下命令:
npm run development
然后,在浏览器中打开根 URL(http://localhost:3000)以查看Home组件。
这里开发的Home组件是一个基本的视图组件,没有交互功能,不需要使用后端 API 来进行用户 CRUD 或身份验证。然而,我们骨架前端的其余视图组件将需要后端 API 和身份验证。
后端 API 集成
用户应该能够使用前端视图根据身份验证和授权从数据库中获取和修改用户数据。为了实现这些功能,React 组件将使用 Fetch API 访问后端暴露的 API 端点。
Fetch API 是一个较新的标准,用于发出类似于XMLHttpRequest(XHR)的网络请求,但使用 promise,从而实现了更简单和更清晰的 API。要了解有关 Fetch API 的更多信息,请访问developer.mozilla.org/en-US/docs/Web/API/Fetch_API。
用户 CRUD 的获取
在client/user/api-user.js文件中,我们将添加用于访问每个用户 CRUD API 端点的方法,React 组件可以使用这些方法与服务器和数据库交换用户数据。
创建用户
create方法将从视图组件获取用户数据,使用fetch进行POST调用,在后端创建一个新用户,最后将来自服务器的响应作为一个 promise 返回给组件。
mern-skeleton/client/user/api-user.js:
const create = (user) => {
return fetch('/api/users/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
})
.then((response) => {
return response.json()
}).catch((err) => console.log(err))
}
列出用户
list方法将使用 fetch 进行GET调用,以检索数据库中的所有用户,然后将来自服务器的响应作为 promise 返回给组件。
mern-skeleton/client/user/api-user.js:
const list = () => {
return fetch('/api/users/', {
method: 'GET',
}).then(response => {
return response.json()
}).catch((err) => console.log(err))
}
读取用户配置文件
read方法将使用 fetch 进行GET调用,按 ID 检索特定用户。由于这是一个受保护的路由,除了将用户 ID 作为参数传递之外,请求组件还必须提供有效的凭据,这种情况下将是成功登录后收到的有效 JWT。
mern-skeleton/client/user/api-user.js:
const read = (params, credentials) => {
return fetch('/api/users/' + params.userId, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
}
}).then((response) => {
return response.json()
}).catch((err) => console.log(err))
}
JWT 附加到GET fetch 调用中的Authorization标头,使用Bearer方案,然后将来自服务器的响应作为 promise 返回给组件。
更新用户数据
update方法将从视图组件获取特定用户的更改用户数据,然后使用fetch进行PUT调用,更新后端现有用户。这也是一个受保护的路由,需要有效的 JWT 作为凭据。
mern-skeleton/client/user/api-user.js:
const update = (params, credentials, user) => {
return fetch('/api/users/' + params.userId, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: JSON.stringify(user)
}).then((response) => {
return response.json()
}).catch((err) => {
console.log(err)
})
}
删除用户
remove方法将允许视图组件使用 fetch 来删除数据库中的特定用户,发出DELETE调用。同样,这是一个受保护的路由,将需要有效的 JWT 作为凭据,类似于read和update方法。服务器对删除请求的响应将以 promise 的形式返回给组件。
mern-skeleton/client/user/api-user.js:
const remove = (params, credentials) => {
return fetch('/api/users/' + params.userId, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
}
}).then((response) => {
return response.json()
}).catch((err) => {
console.log(err)
})
}
最后,将用户 API 辅助方法导出,以便根据需要被导入和使用 React 组件。
mern-skeleton/client/user/api-user.js:
export { create, list, read, update, remove }
用于认证 API 的 fetch
为了将服务器的认证 API 端点与前端 React 组件集成,我们将在client/auth/api-auth.js文件中添加用于获取登录和登出 API 端点的方法。
登录
signin方法将从视图组件获取用户登录数据,然后使用fetch发出POST调用来验证后端的用户。服务器的响应将以 promise 的形式返回给组件,其中可能包含 JWT 如果登录成功的话。
mern-skeleton/client/user/api-auth.js:
const signin = (user) => {
return fetch('/auth/signin/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(user)
})
.then((response) => {
return response.json()
}).catch((err) => console.log(err))
}
登出
signout方法将使用 fetch 来向服务器的 signout API 端点发出 GET 调用。
mern-skeleton/client/user/api-auth.js:
const signout = () => {
return fetch('/auth/signout/', {
method: 'GET',
}).then(response => {
return response.json()
}).catch((err) => console.log(err))
}
在api-auth.js文件的末尾,导出signin和signout方法。
mern-skeleton/client/user/api-auth.js:
export { signin, signout }
有了这些 API fetch 方法,React 前端可以完全访问后端可用的端点。
前端的认证
如前一章所讨论的,使用 JWT 实现认证将责任转移到客户端来管理和存储用户认证状态。为此,我们需要编写代码,允许客户端存储从服务器成功登录时收到的 JWT,在访问受保护的路由时使其可用,当用户退出时删除或使令牌无效,并且还根据用户认证状态限制前端的视图和组件访问。
使用 React Router 文档中的认证工作流示例,我们将编写辅助方法来管理组件之间的认证状态,并且还将使用自定义的PrivateRoute组件来向前端添加受保护的路由。
管理认证状态
在client/auth/auth-helper.js中,我们将定义以下辅助方法来从客户端sessionStorage中存储和检索 JWT 凭据,并在用户退出时清除sessionStorage:
authenticate(jwt, cb): 在成功登录时保存凭据:
authenticate(jwt, cb) {
if(typeof window !== "undefined")
sessionStorage.setItem('jwt', JSON.stringify(jwt))
cb()
}
isAuthenticated(): 如果已登录,则检索凭据:
isAuthenticated() {
if (typeof window == "undefined")
return false
if (sessionStorage.getItem('jwt'))
return JSON.parse(sessionStorage.getItem('jwt'))
else
return false
}
signout(cb): 删除凭据并退出登录:
signout(cb) {
if(typeof window !== "undefined")
sessionStorage.removeItem('jwt')
cb()
signout().then((data) => {
document.cookie = "t=; expires=Thu, 01 Jan 1970 00:00:00
UTC; path=/;"
})
}
使用这里定义的方法,我们构建的 React 组件将能够检查和管理用户认证状态,以限制前端的访问,就像在自定义的PrivateRoute中所示的那样。
PrivateRoute 组件
client/auth/PrivateRoute.js中定义了PrivateRoute组件,如 React Router 文档中的认证流程示例所示。它将允许我们声明受保护的路由,以便前端根据用户认证限制视图访问。
mern-skeleton/client/auth/PrivateRoute.js:
import React, { Component } from 'react'
import { Route, Redirect } from 'react-router-dom'
import auth from './auth-helper'
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
auth.isAuthenticated() ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/signin',
state: { from: props.location }
}}/>
)
)}/>
)
export default PrivateRoute
在PrivateRoute中呈现的组件只有在用户经过认证时才会加载,否则用户将被重定向到Signin组件。
随着后端 API 的集成,和认证管理辅助方法在组件中准备就绪,我们可以开始构建剩余的视图组件。
用户和认证组件
本节中描述的 React 组件完成了骨架定义的交互功能,允许用户查看、创建和修改存储在数据库中的用户数据,同时考虑认证限制。对于以下每个组件,我们将介绍每个组件的独特方面,以及如何将组件添加到应用程序中的MainRouter中。
Users 组件
client/user/Users.js中的Users组件显示了从数据库中获取的所有用户的名称,并将每个名称链接到用户配置文件。任何访问应用程序的访问者都可以查看此组件,并且将在路径'/users'上呈现:
在组件定义中,我们首先使用空数组初始化状态。
mern-skeleton/client/user/Users.js:
class Users extends Component {
state = { users: [] }
...
接下来,在componentDidMount中,我们使用api-user.js中的list方法,从后端获取用户列表,并通过更新状态将用户数据加载到组件中。
mern-skeleton/client/user/Users.js:
componentDidMount = () => {
list().then((data) => {
if (data.error)
console.log(data.error)
else
this.setState({users: data})
})
}
render函数包含Users组件的实际视图内容,并与 Material-UI 组件(如Paper、List和ListItems)组合在一起。这些元素使用定义的 CSS 进行样式化,并作为 props 传递。
mern-skeleton/client/user/Users.js:
render() {
const {classes} = this.props
return (
<Paper className={classes.root} elevation={4}>
<Typography type="title" className={classes.title}>
All Users
</Typography>
<List dense>
{this.state.users.map(function(item, i) {
return <Link to={"/user/" + item._id} key={i}>
<ListItem button="button">
<ListItemAvatar>
<Avatar>
<Person/>
</Avatar>
</ListItemAvatar>
<ListItemText primary={item.name}/>
<ListItemSecondaryAction>
<IconButton>
<ArrowForward/>
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</Link>
})}
</List>
</Paper>
)
}
为了生成每个列表项,我们使用 map 函数遍历状态中的用户数组。
要将此Users组件添加到 React 应用程序中,我们需要使用Route更新MainRouter组件,在'/users'路径处呈现此组件。在Home路由之后,在Switch组件内添加Route。
mern-skeleton/client/MainRouter.js:
<Route path="/users" component={Users}/>
要在浏览器中看到此视图呈现,可以暂时在Home组件中添加一个Link组件,以路由到Users组件:
<Link to="/users">Users</Link>
注册组件
client/user/Signup.js中的Signup组件向用户呈现一个带有名称、电子邮件和密码字段的表单,用于在'/signup'路径上注册。
在组件定义中,我们首先使用空输入字段值,空错误消息和将对话框打开变量设置为 false 来初始化状态。
mern-skeleton/client/user/Signup.js:
constructor() {
state = { name: '', password: '', email: '', open: false, error: '' }
...
我们还定义了两个处理函数,当输入值更改或单击提交按钮时将被调用。handleChange函数获取输入字段中输入的新值,并将其设置为state。
mern-skeleton/client/user/Signup.js:
handleChange = name => event => {
this.setState({[name]: event.target.value})
}
当表单提交时,将调用clickSubmit函数。它从状态中获取输入值,并调用create获取方法来注册用户。然后,根据服务器的响应,要么显示错误消息,要么显示成功对话框。
mern-skeleton/client/user/Signup.js:
clickSubmit = () => {
const user = {
name: this.state.name || undefined,
email: this.state.email || undefined,
password: this.state.password || undefined
}
create(user).then((data) => {
if (data.error)
this.setState({error: data.error})
else
this.setState({error: '', open: true})
})
}
在render函数中,我们使用诸如来自 Material-UI 的TextField等组件来组成和样式化注册视图中的表单组件。
mern-skeleton/client/user/Signup.js:
render() {
const {classes} = this.props
return (<div>
<Card className={classes.card}>
<CardContent>
<Typography type="headline" component="h2"
className={classes.title}>
Sign Up
</Typography>
<TextField id="name" label="Name"
className={classes.textField}
value={this.state.name}
onChange={this.handleChange('name')}
margin="normal"/> <br/>
<TextField id="email" type="email" label="Email"
className={classes.textField} value=
{this.state.email}
onChange={this.handleChange('email')}
margin="normal"/><br/>
<TextField id="password" type="password"
label="Password" className={classes.textField}
value={this.state.password}
onChange={this.handleChange('password')}
margin="normal"/><br/>
{this.state.error && ( <Typography component="p"
color="error">
<Icon color="error"
className={classes.error}>error</Icon>
{this.state.error}</Typography>)}
</CardContent>
<CardActions>
<Button color="primary" raised="raised"
onClick={this.clickSubmit}
className={classes.submit}>Submit</Button>
</CardActions>
</Card>
<Dialog> ... </Dialog>
</div>)
}
渲染还包含一个错误消息块,以及一个Dialog组件,根据服务器的注册响应条件渲染。Signup.js中的Dialog组件组成如下。
mern-skeleton/client/user/Signup.js:
<Dialog open={this.state.open} disableBackdropClick={true}>
<DialogTitle>New Account</DialogTitle>
<DialogContent>
<DialogContentText>
New account successfully created.
</DialogContentText>
</DialogContent>
<DialogActions>
<Link to="/signin">
<Button color="primary" autoFocus="autoFocus" variant="raised">
Sign In
</Button>
</Link>
</DialogActions>
</Dialog>
成功创建帐户后,用户将收到确认,并被要求使用此Dialog组件登录,该组件链接到Signin组件:
要将Signup组件添加到应用程序中,在Switch组件中添加以下Route到MainRouter中。
mern-skeleton/client/MainRouter.js:
<Route path="/signup" component={Signup}/>
这将在'/signup'处呈现Signup视图。
登录组件
client/auth/Signin.js中的Signin组件也是一个只有电子邮件和密码字段的登录表单。该组件与Signup组件非常相似,并将在'/signin'路径下呈现。主要区别在于成功登录后重定向和接收 JWT 的存储实现:
对于重定向,我们将使用 React Router 中的Redirect组件。首先,在状态中将redirectToReferrer值初始化为false,并与其他字段一起使用:
mern-skeleton/client/auth/Signin.js:
class Signin extends Component {
state = { email: '', password: '', error: '', redirectToReferrer: false }
...
当用户成功提交表单并且接收到 JWT 存储在sessionStorage中时,redirectToReferrer应设置为true。为了存储 JWT 并在之后重定向,我们将调用auth-helper.js中定义的authenticate()方法。这段代码将放在clickSubmit()函数中,在表单提交时调用。
mern-skeleton/client/auth/Signin.js:
clickSubmit = () => {
const user = {
email: this.state.email || undefined,
password: this.state.password || undefined
}
signin(user).then((data) => {
if (data.error) {
this.setState({error: data.error})
} else {
auth.authenticate(data, () => {
this.setState({redirectToReferrer: true})
})
}
})
}
基于redirectToReferrer值的条件,重定向将在render函数中的Redirect组件中发生。在返回之前,在 render 函数中添加重定向代码如下:
mern-skeleton/client/auth/Signin.js:
render() {
const {classes} = this.props
const {from} = this.props.location.state || {
from: {pathname: '/' }
}
const {redirectToReferrer} = this.state
if (redirectToReferrer)
return (<Redirect to={from}/>)
return (...)
}
}
如果渲染Redirect组件,将会将应用程序带到上次的位置或根目录下的Home组件。
返回将包含类似于Signup的表单元素,只有email和password字段,条件错误消息和submit按钮。
要将Signin组件添加到应用程序中,在Switch组件的MainRouter中添加以下路由。
mern-skeleton/client/MainRouter.js:
<Route path="/signin" component={Signin}/>
这将在"/signin"处呈现Signin组件。
Profile 组件
client/user/Profile.js中的Profile组件在'/user/:userId'路径中显示单个用户的信息,其中userId参数表示特定用户的 ID:
只有在用户登录后,才能从服务器获取此配置文件信息,并且为了验证这一点,组件必须向read获取调用提供 JWT,否则用户应该被重定向到登录视图。
在Profile组件定义中,我们首先需要用空用户初始化状态,并将redirectToSignin设置为false。
mern-skeleton/client/user/Profile.js:
class Profile extends Component {
constructor({match}) {
super()
this.state = { user: '', redirectToSignin: false }
this.match = match
} ...
我们还需要访问由Route组件传递的匹配 props,其中将包含:userId参数值,并且在组件挂载时可以作为this.match.params.userId进行访问。
Profile组件应在路由中的userId参数更改时获取用户信息并呈现它。然而,当应用程序从一个配置文件视图切换到另一个配置文件视图时,只是路由路径中的参数更改,React 组件不会重新挂载。相反,它会在componentWillReceiveProps中传递新的 props。为了确保组件在路由参数更新时加载相关用户信息,我们将在init()函数中放置read获取调用,然后可以在componentDidMount和componentWillReceiveProps中调用它。
mern-skeleton/client/user/Profile.js:
init = (userId) => {
const jwt = auth.isAuthenticated()
read({
userId: userId
}, {t: jwt.token}).then((data) => {
if (data.error)
this.setState({redirectToSignin: true})
else
this.setState({user: data})
})
}
init(userId)函数接受userId值,并调用读取用户获取方法。由于此方法还需要凭据来授权登录用户,因此 JWT 是使用auth-helper.js中的isAuthenticated方法从sessionStorage中检索的。一旦服务器响应,要么更新状态与用户信息,要么将视图重定向到登录视图。
init函数在componentDidMount和componentWillReceiveProps中被调用,并传入相关的userId值作为参数,以便在组件中获取和加载正确的用户信息。
mern-skeleton/client/user/Profile.js:
componentDidMount = () => {
this.init(this.match.params.userId)
}
componentWillReceiveProps = (props) => {
this.init(props.match.params.userId)
}
在render函数中,我们设置了条件重定向到登录视图,并返回Profile视图的内容:
mern-skeleton/client/user/Profile.js:
render() {
const {classes} = this.props
const redirectToSignin = this.state.redirectToSignin
if (redirectToSignin)
return <Redirect to='/signin'/>
return (...)
}
如果当前登录的用户正在查看另一个用户的配置文件,则render函数将返回Profile视图,并包含以下元素。
mern-skeleton/client/user/Profile.js:
<div>
<Paper className={classes.root} elevation={4}>
<Typography type="title" className={classes.title}> Profile </Typography>
<List dense>
<ListItem>
<ListItemAvatar>
<Avatar>
<Person/>
</Avatar>
</ListItemAvatar>
<ListItemText primary={this.state.user.name}
secondary={this.state.user.email}/>
</ListItem>
<Divider/>
<ListItem>
<ListItemText primary={"Joined: " +
(new Date(this.state.user.created)).toDateString()}/>
</ListItem>
</List>
</Paper>
</div>
但是,如果当前登录的用户正在查看自己的配置文件,则可以在Profile组件中看到编辑和删除选项,如下截图所示:
要实现此功能,在Profile中的第一个ListItem组件中添加一个包含Edit按钮和DeleteUser组件的ListItemSecondaryAction组件,根据当前用户是否查看自己的配置文件来有条件地呈现。
mern-skeleton/client/user/Profile.js:
{ auth.isAuthenticated().user && auth.isAuthenticated().user._id == this.state.user._id &&
(<ListItemSecondaryAction>
<Link to={"/user/edit/" + this.state.user._id}>
<IconButton color="primary">
<Edit/>
</IconButton>
</Link>
<DeleteUser userId={this.state.user._id}/>
</ListItemSecondaryAction>)}
Edit按钮将路由到EditProfile组件,此处使用的自定义DeleteUser组件将处理传递给它的userId的删除操作。
要将Profile组件添加到应用程序中,请将Route添加到Switch组件中的MainRouter中。
mern-skeleton/client/MainRouter.js:
<Route path="/user/:userId" component={Profile}/>
EditProfile 组件
client/user/EditProfile.js中的EditProfile组件在实现上与Signup和Profile组件都有相似之处。它将允许授权用户在类似注册表单的表单中编辑自己的个人资料信息:
在'/user/edit/:userId'加载时,组件将通过验证 JWT 以获取 ID 的用户信息,然后使用接收到的用户信息加载表单。表单将允许用户仅编辑和提交更改的信息到update fetch 调用,并在成功更新后将用户重定向到具有更新信息的Profile视图。
EditProfile将以与Profile组件相同的方式加载用户信息,通过在componentDidMount中使用read从this.match.params获取userId参数,并使用auth.isAuthenticated的凭据。表单视图将具有与Signup组件相同的元素,输入值在更改时更新状态。
在表单提交时,组件将使用userId、JWT 和更新后的用户数据调用update fetch 方法。
mern-skeleton/client/user/EditProfile.js:
clickSubmit = () => {
const jwt = auth.isAuthenticated()
const user = {
name: this.state.name || undefined,
email: this.state.email || undefined,
password: this.state.password || undefined
}
update({
userId: this.match.params.userId
}, {
t: jwt.token
}, user).then((data) => {
if (data.error) {
this.setState({error: data.error})
} else {
this.setState({'userId': data._id, 'redirectToProfile': true})
}
})
}
根据服务器的响应,用户将要么看到错误消息,要么在渲染函数中使用以下Redirect组件重定向到更新后的 Profile 页面。
mern-skeleton/client/user/EditProfile.js:
if (this.state.redirectToProfile)
return (<Redirect to={'/user/' + this.state.userId}/>)
要将EditProfile组件添加到应用程序中,这次我们将使用PrivateRoute,以限制用户未登录时根本不加载组件。在MainRouter中的放置顺序也很重要。
mern-skeleton/client/MainRouter.js:
<Switch>
... <PrivateRoute path="/user/edit/:userId" component={EditProfile}/><>
<Route path="/user/:userId" component={Profile}/>
</Switch>
具有路径'/user/edit/:userId'的路由需要放置在具有路径'/user/:userId'的路由之前,以便在请求此路由时,编辑路径首先在 Switch 组件中独占匹配,不会与Profile路由混淆。
DeleteUser 组件
client/user/DeleteUser.js中的DeleteUser组件基本上是一个按钮,我们将其添加到 Profile 视图中,当点击时会打开一个要求用户确认delete操作的Dialog组件:
该组件首先使用open设置为false来初始化Dialog组件的状态,并且还将redirect设置为false,因此首先不会被渲染。
mern-skeleton/client/user/DeleteUser.js:
class DeleteUser extends Component {
state = { redirect: false, open: false }
...
接下来,我们需要处理打开和关闭dialog按钮的方法。当用户点击delete按钮时,对话框将被打开。
mern-skeleton/client/user/DeleteUser.js:
clickButton = () => {
this.setState({open: true})
}
当用户在对话框上点击cancel时,对话框将被关闭。
mern-skeleton/client/user/DeleteUser.js:
handleRequestClose = () => {
this.setState({open: false})
}
该组件将从Profile组件中作为属性传递的userId,这是调用remove fetch 方法所需的,同时还需要 JWT,用户在对话框中确认delete操作后。
mern-skeleton/client/user/DeleteUser.js:
deleteAccount = () => {
const jwt = auth.isAuthenticated()
remove({
userId: this.props.userId
}, {t: jwt.token}).then((data) => {
if (data.error) {
console.log(data.error)
} else {
auth.signout(() => console.log('deleted'))
this.setState({redirect: true})
}
})
}
确认后,deleteAccount函数使用来自属性的userId和来自isAuthenticated的 JWT 调用remove fetch 方法。在服务器成功删除后,用户将被注销并重定向到主页视图。
渲染函数包含对主页视图的条件性Redirect,并返回DeleteUser组件元素、DeleteIcon按钮和确认Dialog:
mern-skeleton/client/user/DeleteUser.js:
render() {
const redirect = this.state.redirect
if (redirect) {
return <Redirect to='/'/>
}
return (<span>
<IconButton aria-label="Delete" onClick={this.clickButton}
color="secondary">
<DeleteIcon/>
</IconButton>
<Dialog open={this.state.open} onClose={this.handleRequestClose}>
<DialogTitle>{"Delete Account"}</DialogTitle>
<DialogContent>
<DialogContentText>
Confirm to delete your account.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={this.handleRequestClose} color="primary">
Cancel
</Button>
<Button onClick={this.deleteAccount} color="secondary"
autoFocus="autoFocus">
Confirm
</Button>
</DialogActions>
</Dialog>
</span>)
}
DeleteUser将userId作为属性传递,用于delete fetch 调用,因此我们为所需的属性userId添加了propType检查。
mern-skeleton/client/user/DeleteUser.js:
DeleteUser.propTypes = {
userId: PropTypes.string.isRequired
}
由于我们在Profile组件中使用DeleteUser组件,所以当Profile添加到MainRouter中时,它将被添加到应用视图中。
菜单组件
Menu组件将作为整个前端应用程序的导航栏,提供到所有可用视图的链接,并指示应用程序中的当前位置。
为了实现这些导航栏功能,我们将使用 React Router 中的 HOC withRouter来访问历史对象的属性。Menu组件中的以下代码仅添加了标题、与根路由相关联的Home图标以及与'/users'路由相关联的Users按钮。
mern-skeleton/client/core/Menu.js:
const Menu = withRouter(({history}) => (<div>
<AppBar position="static">
<Toolbar>
<Typography type="title" color="inherit">
MERN Skeleton
</Typography>
<Link to="/">
<IconButton aria-label="Home" style={isActive(history, "/")}>
<HomeIcon/>
</IconButton>
</Link>
<Link to="/users">
<Button style={isActive(history, "/users")}>Users</Button>
</Link>
</Toolbar>
</AppBar>
</div>))
为了指示应用程序的当前位置在Menu上,我们将通过条件性地改变颜色来突出显示与当前位置路径匹配的链接。
mern-skeleton/client/core/Menu.js:
const isActive = (history, path) => {
if (history.location.pathname == path)
return {color: '#ff4081'}
else
return {color: '#ffffff'}
}
isActive函数用于在Menu中为按钮应用颜色,如下所示:
style={isActive(history, "/users")}
剩下的链接,如 SIGN IN、SIGN UP、MY PROFILE 和 SIGN OUT,将根据用户是否已登录显示在Menu上:
例如,当用户未登录时,注册和登录的链接应该只显示在菜单上。因此,我们需要在Menu组件中添加它,并在Users按钮之后加上条件。
mern-skeleton/client/core/Menu.js:
{!auth.isAuthenticated() && (<span>
<Link to="/signup">
<Button style={isActive(history, "/signup")}> Sign Up </Button>
</Link>
<Link to="/signin">
<Button style={isActive(history, "/signin")}> Sign In </Button>
</Link>
</span>)}
类似地,只有当用户已登录时,MY PROFILE链接和SIGN OUT按钮才应该显示在菜单上,并且应该根据这个条件检查添加到Menu组件中。
mern-skeleton/client/core/Menu.js:
{auth.isAuthenticated() && (<span>
<Link to={"/user/" + auth.isAuthenticated().user._id}>
<Button style={isActive(history, "/user/" + auth.isAuthenticated().user._id)}>
My Profile
</Button>
</Link>
<Button color="inherit"
onClick={() => { auth.signout(() => history.push('/')) }}>
Sign out
</Button>
</span>)}
MY PROFILE按钮使用已登录用户的信息链接到用户自己的个人资料,并且SIGN OUT按钮在点击时调用auth.signout()方法。当用户已登录时,菜单将如下所示:
为了在所有视图中显示Menu导航栏,我们需要在MainRouter中添加它,放在所有其他路由之前,并且在Switch组件之外。
mern-skeleton/client/MainRouter.js:
<Menu/>
<Switch>
…
</Switch>
当在路由上访问组件时,这将使Menu组件呈现在所有其他组件的顶部。
骨架前端已经完整,包括所有必要的组件,以便用户可以在后端注册、查看和修改用户数据,并考虑到认证和授权限制。然而,目前还不能直接在浏览器地址栏中访问前端路由,只能在前端视图内部链接时访问。为了在骨架应用程序中实现此功能,我们需要实现基本的服务器端渲染。
基本的服务器端渲染
目前,当 React Router 路由或路径名直接输入到浏览器地址栏,或者刷新不在根路径的视图时,URL 无法工作。这是因为服务器无法识别 React Router 路由。我们需要在后端实现基本的服务器端渲染,以便服务器在收到对前端路由的请求时能够响应。
在服务器接收到前端路由的请求时,我们需要根据 React Router 和 Material-UI 组件在服务器端正确渲染相关的 React 组件。
React 应用程序服务器端渲染的基本思想是使用react-dom中的renderToString方法将根 React 组件转换为标记字符串,并将其附加到服务器在接收到请求时渲染的模板上。
在express.js中,我们将用代码替换对'/'的GET请求返回template.js的代码,该代码在接收到任何传入的 GET 请求时,生成相关 React 组件的服务器端渲染标记,并将此标记添加到模板中。此代码将具有以下结构:
app.get('*', (req, res) => {
// 1\. Prepare Material-UI styles
// 2\. Generate markup with renderToString
// 3\. Return template with markup and CSS styles in the response
})
用于服务器端渲染的模块
为了实现基本的服务器端渲染,我们需要将以下 React、React Router 和 Material-UI 特定模块导入到服务器代码中。在我们的代码结构中,这些模块将被导入到server/express.js中:
- React 模块:用于渲染 React 组件和使用
renderToString:
import React from 'react'
import ReactDOMServer from 'react-dom/server'
- Router 模块:
StaticRouter是一个无状态路由器,它接受请求的 URL 以匹配前端路由和MainRouter组件,这是我们前端的根组件:
import StaticRouter from 'react-router-dom/StaticRouter'
import MainRouter from './../client/MainRouter'
- Material-UI 模块:以下模块将帮助基于前端使用的 Material-UI 主题为前端组件生成 CSS 样式:
import { SheetsRegistry } from 'react-jss/lib/jss'
import JssProvider from 'react-jss/lib/JssProvider'
import { MuiThemeProvider, createMuiTheme, createGenerateClassName } from 'material-ui/styles'
import { indigo, pink } from 'material-ui/colors'
有了这些模块,我们可以准备、生成和返回服务器端渲染的前端代码。
为 SSR 准备 Material-UI 样式
当服务器接收到任何请求时,在响应包含 React 视图的生成标记之前,我们需要准备应该添加到标记中的 CSS 样式,以便 UI 在初始渲染时不会中断。
mern-skeleton/server/express.js:
const sheetsRegistry = new SheetsRegistry()
const theme = createMuiTheme({
palette: {
primary: {
light: '#757de8',
main: '#3f51b5',
dark: '#002984',
contrastText: '#fff',
},
secondary: {
light: '#ff79b0',
main: '#ff4081',
dark: '#c60055',
contrastText: '#000',
},
openTitle: indigo['400'],
protectedTitle: pink['400'],
type: 'light'
},
})
const generateClassName = createGenerateClassName()
为了注入 Material-UI 样式,在每个请求上,我们首先生成一个新的SheetsRegistry和 MUI 主题实例,与前端代码中使用的相匹配。
生成标记
使用renderToString的目的是生成要响应请求的用户显示的 React 组件的 HTML 字符串版本:
mern-skeleton/server/express.js:
const context = {}
const markup = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<JssProvider registry={sheetsRegistry} generateClassName=
{generateClassName}>
<MuiThemeProvider theme={theme} sheetsManager={new Map()}>
<MainRouter/>
</MuiThemeProvider>
</JssProvider>
</StaticRouter>
)
客户端应用程序的根组件MainRouter被 Material-UI 主题和 JSS 包裹,以提供MainRouter子组件所需的样式属性。在这里使用无状态的StaticRouter代替客户端使用的BrowserRouter,来包裹MainRouter并提供在实现客户端组件时使用的路由属性。基于这些值,例如请求的location路由和作为属性传递给包装组件的主题,renderToString将返回包含相关视图的标记。
发送包含标记和 CSS 的模板
一旦生成了标记,我们首先检查组件中是否有渲染的redirect,以便在标记中发送。如果没有重定向,那么我们从sheetsRegistry生成 CSS 字符串,并在响应中发送带有标记和注入的 CSS 的模板。
mern-skeleton/server/express.js:
if (context.url) {
return res.redirect(303, context.url)
}
const css = sheetsRegistry.toString()
res.status(200).send(Template({
markup: markup,
css: css
}))
在组件中渲染重定向的一个例子是尝试通过服务器端渲染访问PrivateRoute时。由于服务器端无法从客户端的sessionStorage访问 auth 令牌,PrivateRoute中的重定向将被渲染。在这种情况下,context.url将具有'/signin'路由,因此不会尝试渲染PrivateRoute组件,而是重定向到'/signin'路由。
更新 template.js
在服务器上生成的标记和 CSS 必须添加到template.js的 HTML 代码中,以便在服务器渲染模板时加载。
mern-skeleton/template.js:
export default ({markup, css}) => {
return `...
<div id="root">${markup}</div>
<style id="jss-server-side">${css}</style>
...`
}
更新 MainRouter
一旦在服务器端渲染的代码到达浏览器,并且前端脚本接管后,我们需要在主组件挂载时移除服务器端注入的 CSS。这将完全控制 React 应用程序的渲染权力交给客户端:
mern-skeleton/client/MainRouter.js:
componentDidMount() {
const jssStyles = document.getElementById('jss-server-side')
if (jssStyles && jssStyles.parentNode)
jssStyles.parentNode.removeChild(jssStyles)
}
用 hydrate 代替 render
现在 React 组件将在服务器端渲染,我们可以更新main.js代码,使用ReactDOM.hydrate()代替ReactDOM.render():
import React from 'react'
import { hydrate } from 'react-dom'
import App from './App'
hydrate(<App/>, document.getElementById('root'))
hydrate函数用于给已由ReactDOMServer渲染的 HTML 内容进行水合。这意味着服务器端渲染的标记将被保留,只有当 React 在浏览器中接管时才会附加事件处理程序,从而使初始加载性能更好。
通过实现基本的服务器端渲染,服务器现在可以正确处理浏览器地址栏对前端路由的直接请求,从而可以将 React 前端视图加入书签。
这里开发的骨架 MERN 应用程序现在是一个具有基本用户功能的完全功能的 MERN Web 应用程序。我们可以扩展这个骨架中的代码,为不同的应用程序添加各种功能。
总结
在本章中,我们通过添加一个工作的 React 前端完成了 MERN 骨架应用程序,包括前端路由和 React 视图的基本服务器端渲染。
我们首先更新了开发流程,以包括用于 React 视图的客户端代码捆绑。我们更新了 Webpack 和 Babel 的配置以编译 React 代码,并讨论了如何从 Express 应用程序加载配置的 Webpack 中间件,以便在开发过程中从一个地方启动服务器端和客户端代码的编译。
在更新开发流程并构建前端之前,我们添加了相关的 React 依赖项,以及用于前端路由的 React Router 和用于在骨架应用程序的用户界面中使用现有组件的 Material-UI。
然后,我们实现了顶层根 React 组件,并集成了 React Router,这使我们能够添加用于导航的客户端路由。使用这些路由,我们加载了使用 Material-UI 组件开发的自定义 React 组件,以构成骨架应用程序的用户界面。
为了使这些 React 视图能够与从后端获取的数据动态交互,我们使用 Fetch API 连接到后端用户 API。然后,我们使用sessionStorage存储用户特定的细节和从服务器成功登录时获取的 JWT,还通过使用PrivateRoute组件限制对某些视图的访问来在前端视图上实现身份验证和授权。
最后,我们修改了服务器代码,实现了基本的服务器端渲染,允许在服务器识别到传入请求实际上是针对 React 路由时,在浏览器中直接加载经服务器端渲染的标记。
在下一章中,我们将利用开发这个基本的 MERN 应用程序时学到的概念,扩展骨架应用程序的代码,构建一个功能齐全的社交媒体应用程序。