React 全栈项目第二版(一)
原文:
zh.annas-archive.org/md5/35c59f78351aeb34721c43c78c53c92a译者:飞龙
前言
本书通过结合 React 的强大功能和经过行业验证的服务端技术(如 Node.js、Express 和 MongoDB)来探讨全栈 JavaScript Web 应用的开发。JavaScript 领域已经迅速发展了一段时间。在有关全栈 JavaScript Web 应用方面,有大量的选项和资源可用,当你需要从这些经常变化的实体中选择、了解它们并使它们协同工作以构建自己的 Web 应用时,很容易感到迷茫。为了解决这个痛点,本书采用了一种实用方法,帮助你使用流行的 MERN 框架搭建和构建各种工作应用。
本书面向的对象
本书面向的是可能使用过 React 但全栈开发(涉及 Node.js、Express 和 MongoDB)经验有限的 JavaScript 开发者。
本书涵盖的内容
第一章,利用 MERN 释放 React 应用的力量,介绍了 MERN 框架技术以及本书将要开发的应用。我们将讨论使用 React、Node.js、Express 和 MongoDB 开发 Web 应用。
第二章,准备开发环境,帮助你设置 MERN 框架技术以进行开发。我们将探讨基本开发工具;安装 Node.js、MongoDB、Express、React 以及任何其他必需的库;然后运行代码以检查设置。
第三章,使用 MongoDB、Express 和 Node 构建后端,实现了 MERN 框架应用的骨架后端。我们将构建一个使用 MongoDB、Express 和 Node.js 的独立服务器端应用,存储用户详细信息,并提供用户认证和 CRUD 操作的 API。
第四章,添加 React 前端以完善 MERN,通过集成 React 前端来完善 MERN 框架应用。我们将实现一个具有 React 视图的前端,用于与服务器上的用户 CRUD 操作和认证 API 交互。
第五章,将骨架扩展为社交媒体应用,通过扩展骨架应用来构建一个社交媒体应用。我们将通过实现社交媒体功能(如帖子分享、点赞、评论、关注朋友和聚合新闻源)来探索 MERN 框架的能力。
第六章,构建基于 Web 的课堂应用,专注于通过扩展 MERN 框架骨架应用来构建一个简单的在线课堂应用。这个课堂应用将支持多个用户角色,添加课程内容和课程,学生注册,进度跟踪以及课程注册统计。
第七章,使用在线市场锻炼 MERN 技能,利用 MERN 框架技术开发在线市场应用的基本功能。我们将实现买卖相关功能,包括卖家账户、产品列表和按类别搜索产品。
第八章,扩展市场以支持订单和支付,专注于通过实现买家添加产品到购物车、结账和下订单的能力,以及卖家管理这些订单和从市场应用中处理支付的能力来扩展我们在上一章中构建的在线市场。我们还将集成 Stripe 以收集和处理支付。
第九章,为市场添加实时竞价功能,专注于教你如何使用 MERN 框架技术以及 Socket.IO,轻松集成全栈应用中的实时行为。我们将通过在 MERN 市场应用中集成具有实时竞价功能的拍卖功能来实现这一点。
第十章,将数据可视化集成到支出跟踪应用中,专注于使用 MERN 框架技术以及 Victory——一个用于 React 的图表库,以轻松集成全栈应用中的数据可视化功能。我们将扩展 MERN 框架基础应用以构建一个支出跟踪应用,该应用将包含用户随时间记录的支出数据处理和可视化功能。
第十一章,构建媒体流应用,专注于扩展 MERN 框架基础应用以构建一个使用 MongoDB GridFS 的媒体上传和流应用。我们将从构建一个基本的媒体流应用开始,允许注册用户上传视频文件,这些文件将被存储在 MongoDB 中并通过流回放,以便观众可以在简单的 React 媒体播放器中播放每个视频。
第十二章,定制媒体播放器和优化 SEO,通过定制媒体播放器和自动播放媒体列表升级我们的媒体应用观看能力。我们将实现自定义控制默认 React 媒体播放器,添加一个可以自动播放的播放列表,并通过添加仅针对媒体详情视图的数据的选区服务器端渲染来优化媒体详情的 SEO。
第十三章,开发基于 Web 的 VR 游戏,使用 React 360 开发一个三维虚拟现实(VR)游戏。我们将探索 React 360 的三维和 VR 功能,并构建一个简单的基于 Web 的 VR 游戏。
第十四章,使用 MERN 使 VR 游戏动态化,您将通过扩展 MERN 骨架应用程序并集成 React 360 来构建一个动态的 VR 游戏应用程序。我们将实现一个游戏数据模型,使用户能够创建自己的 VR 游戏,并将动态游戏数据与使用 React 360 开发的游戏相结合。
第十五章,遵循最佳实践并进一步开发 MERN,回顾了前几章学到的经验教训,并提出了进一步基于 MERN 的应用程序开发的改进建议。我们将扩展一些已经应用的最佳实践,例如应用程序结构中的模块化,其他应该应用的做法,例如编写测试代码,以及可能的改进,例如优化包大小。
为了充分利用这本书
本书假设您熟悉基本的网络技术,了解 JavaScript 中的编程结构,并对 React 应用程序的工作原理有一个大致的了解。随着您阅读本书,您将发现这些概念如何在构建使用 React 16.13.1、Node.js 13.12.0、Express 4.17.1 和 MongoDB 4.2.5 的完整功能网络应用程序时结合在一起。
为了在阅读章节时最大限度地提高您的学习体验,建议您并行运行相关应用程序代码,保持指定的包版本,并使用每章中提供的相关说明。
如果您使用的是这本书的数字版,我们建议您自己输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。
下载示例代码文件
您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择“支持”标签。
-
点击“代码下载”。
-
在搜索框中输入书名,并遵循屏幕上的说明。
文件下载完成后,请确保使用最新版本的以下软件解压或提取文件夹:
-
Windows 版的 WinRAR/7-Zip
-
Mac 版的 Zipeg/iZip/UnRarX
-
Linux 版的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Full-Stack-React-Projects-Second-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在 github.com/PacktPublishing/ 上找到。查看它们吧!
使用的约定
在本书中使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理。以下是一个示例:“将下载的 WebStorm-10*.dmg 磁盘映像文件作为系统中的另一个磁盘挂载。”
代码块按以下方式设置:
addItem(item, cb) {
let cart = []
if (typeof window !== "undefined") {
if (localStorage.getItem('cart')) {
cart = JSON.parse(localStorage.getItem('cart'))
}
cart.push({
product: item,
quantity: 1,
shop: item.shop._id
})
localStorage.setItem('cart', JSON.stringify(cart))
cb()
}
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
<Grid container spacing={24}>
<Grid item xs={6} sm={6}>
<CartItems checkout={checkout}
setCheckout={showCheckout}/>
</Grid>
{checkout &&
<Grid item xs={6} sm={6}>
<Checkout/>
</Grid>}
</Grid>
任何命令行输入或输出都按以下方式编写:
yarn add --dev @babel/preset-react
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从管理面板中选择系统信息。”
警告或重要提示看起来像这样。
小技巧和技巧看起来像这样。
联系我们
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过 customercare@packtpub.com 邮箱联系我们。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/err…,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 联系我们,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
有关 Packt 的更多信息,请访问 packt.com。
第一章:使用 MERN 入门
在这部分,我们将对 MERN 进行介绍,并概述其不同组件。此外,在您开始使用这些技术开发完整的 Web 应用程序之前,您将了解如何正确配置您的开发环境。
本节包括以下章节:
-
第一章,利用 MERN 解放 React 应用程序
-
第二章,准备开发环境
第二章:使用 MERN 激活 React 应用程序
React 可能已经为前端网络开发开辟了新的领域,并改变了我们编写 JavaScript 用户界面的方式,但我们需要一个坚实的后端来构建完整的网络应用程序。虽然在选择后端技术时有很多选择,但使用全 JavaScript 栈的好处和吸引力是无可否认的,尤其是在有像 Node、Express 和 MongoDB 这样强大且广泛采用的后端技术时。将 React 的潜力与这些经过行业检验的服务端技术相结合,在开发实际网络应用程序时创造了多样化的可能性。本书将引导您通过设置基于 MERN(即 MongoDB, Express.js, React, 和 Node.js)的 Web 开发,到构建不同复杂度的实际网络应用程序。
在深入开发这些网络应用程序之前,我们将在本章中回答以下问题,以便您能够有效地使用这本书来获取全栈开发技能,并了解选择 MERN 栈构建应用程序背后的背景:
-
第二版有哪些新内容?
-
这本书是如何组织来帮助掌握 MERN 的?
-
MERN 栈是什么?
-
为什么 MERN 在今天仍然相关?
-
在什么情况下 MERN 是开发网络应用程序的好选择?
第二版有哪些新内容?
MERN 栈技术以及整个全栈开发生态系统正在随着行业采用和使用的增加而持续增长和改进。在本版中,我们考虑了这些新发展,并更新了第一版中的所有应用程序和相应的代码库。
我们使用每个技术、库和模块的最新版本和约定,这些技术、库和模块对于开发相关的设置和功能实现都是必需的。此外,我们还强调了使用这些技术升级中的新功能,例如 React Hooks 和 JavaScript 中的 async/await。
为了展示 MERN 栈更多的可能性,我们更新了现有的市场应用程序,添加了更高级的功能,例如实时竞标。我们还添加了两个新项目,一个基于网络的教室应用程序和一个具有数据可视化功能的支出跟踪应用程序。
为了更好地理解本书涵盖的内容和概念,我们扩展了解释并提供了一些最新资源的线索,这些资源可能有助于您更深入地掌握并提高您的学习体验。
除了涵盖 MERN 技术的最新更新并提供详细的解释外,本书中的概念和项目组织旨在帮助您从简单到高级主题灵活学习。在下一节中,我们将讨论本书的结构以及您如何根据自己的偏好和经验水平来利用它。
书的结构
本书旨在帮助那些对 MERN 堆栈有一定经验或无经验的 JavaScript 开发者设置并开始开发不同复杂度的 Web 应用程序。它包括构建和运行不同应用程序的指南,辅以代码片段和关键概念的说明。
本书分为五个部分,从基础知识到高级主题逐步深入,带您从零开始构建 MERN,然后使用它开发具有简单到复杂功能的不同应用程序,同时展示如何根据应用程序的需求扩展 MERN 堆栈的功能。
开始使用 MERN
第一章,利用 MERN 解放 React 应用程序,以及 第二章,准备开发环境,为在 MERN 堆栈中开发 Web 应用程序设定了背景,并指导您设置开发环境。
从零开始构建 MERN 应用程序
第三章,使用 MongoDB、Express 和 Node 构建 Backend,和 第四章,添加 React 前端以完成 MERN,向您展示如何将 MERN 堆栈技术结合起来,形成一个具有最小和基本功能的骨架 Web 应用程序。第五章将骨架扩展成社交媒体应用程序,展示了这个骨架 MERN 应用程序如何作为一个基础,并容易被扩展来构建一个简单的社交媒体平台。这种扩展和自定义基础应用程序的能力将在本书其余部分开发的其他应用程序中得到应用。
使用 MERN 开发 Web 应用程序
在本部分,您将通过构建两个真实世界的应用程序来更加熟悉 MERN 堆栈 Web 应用程序的核心属性——一个基于 Web 的课堂应用程序在 第六章构建基于 Web 的课堂应用程序,以及一个功能丰富的在线市场在 第七章使用在线市场锻炼 MERN 技能,第八章扩展市场以支持订单和支付,和 第九章向市场添加实时竞标功能。
向复杂 MERN 应用程序迈进
第十章,将数据可视化集成到支出跟踪应用中,第十一章,构建媒体流应用,第十二章,定制媒体播放器和改进 SEO,第十三章*,开发基于 Web 的 VR 游戏*,和第十四章,使用 MERN 使 VR 游戏动态化,展示了如何使用 React 360 利用这个堆栈开发具有更复杂和沉浸式功能的应用,如数据可视化、媒体流和虚拟现实(VR)。
MERN 的进一步发展
最后,第十五章,遵循最佳实践并进一步开发 MERN,通过扩展遵循以成功构建 MERN 应用的最佳实践,提出改进和进一步发展的建议。
您可以根据自己的经验水平和喜好选择不按规定的顺序使用这本书。对于刚开始接触 MERN 的开发者,可以遵循书中的路径。对于经验更丰富的 JavaScript 开发者,从零开始构建 MERN部分中的章节将是设置基础应用的好起点,然后可以选择任何六个应用进行构建和扩展。
这种结构是为了使具有不同背景的开发者能够进行实践学习。为了最大限度地实现这一目标,我们建议采用一种实用的方法来跟随书中的材料,如下一节更详细地描述。
充分利用本书
本书的内容以实用为导向,涵盖了构建每个 MERN 应用相关的实现步骤、代码和概念。然而,大多数代码解释将引用可能包含更多行代码的文件中的特定代码片段,这些片段构成了完整且可工作的应用代码。
简单来说,强烈建议您不要只是阅读章节,而应该并行运行相关的代码,并在阅读书中的解释的同时浏览应用的功能。
讨论代码实现的章节将指向包含完整代码的 GitHub 仓库,并提供了如何运行代码的说明。您可以在阅读章节之前拉取代码、安装它并运行它:
您应该考虑这里概述的推荐步骤来遵循本书中的实现:
-
在深入讨论章节中讨论的实现细节之前,从相关的 GitHub 仓库中拉取代码。
-
按照代码中的说明安装并运行应用。
-
在阅读相关章节中的功能描述时,浏览运行中应用程序的功能。
-
当代码以开发模式运行并在编辑器中打开时,参考书中的步骤和解释,以获得对实现的更深入理解。
本书旨在为每个应用程序提供快速入门,并提供工作代码。您可以按需进行实验、改进和扩展此代码。为了获得积极的 学习体验,鼓励您在遵循本书的同时重构和修改代码。在某些示例中,本书选择使用冗长的代码而非简洁且更干净的代码,因为对于初学者来说,冗长的代码更容易理解。在另一些实现中,本书坚持使用更广泛使用的传统约定,而不是现代和即将到来的 JavaScript 约定。这样做是为了最小化在您自己研究讨论的技术和概念时,参考在线资源和文档时的差异。这些代码可以更新的实例,是探索和提升超出本书涵盖范围技能的好机会。
现在,您应该对本书的预期内容以及如何充分利用其内容和结构有一个整体的认识,随着我们继续讨论 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/。
然而,npm并不是您可用的唯一包管理系统。Yarn 是由 Facebook 开发的一个较新的包管理器,近年来越来越受欢迎。它可以作为npm的替代品使用,可以访问npm注册表中的所有相同模块,以及npm尚未提供的更多功能。
在yarnpkg.com了解 Yarn 及其功能。
Node 将使我们能够构建和运行完整的全栈 JavaScript 应用程序。然而,为了实现具有 API 路由等特定于 Web 应用程序的功能的可扩展服务器端应用程序,我们将在 Node 之上使用 Express 模块。
Express
Express 是一个简单的服务器端 Web 框架,用于使用 Node 构建 Web 应用程序。它通过提供 HTTP 实用方法和中间件功能,为 Node 添加了一层基本的 Web 应用程序功能。
从一般意义上讲,任何应用程序中的中间件功能使不同的组件能够添加在一起协同工作。在服务器端 Web 应用程序框架的特定上下文中,中间件函数可以访问 HTTP 请求-响应管道,这意味着可以访问请求-响应对象以及 Web 应用程序请求-响应周期中的下一个中间件函数。
在任何使用 Node 开发的 Web 应用程序中,Express 可以用作 API 路由和中间件 Web 框架。几乎可以将任何兼容的中间件插入到请求处理链中,几乎可以以任何顺序,这使得 Express 非常灵活。
在expressjs.com了解 Express.js 能实现什么。
在我们将要开发的基于 MERN 的应用程序中,Express 可以用于在服务器端处理 API 路由,向客户端提供静态文件,通过认证集成限制对资源的访问,实现错误处理,以及本质上添加任何中间件包,以根据需要扩展 Web 应用程序的功能。
在任何完整的 Web 应用程序中,数据存储系统都是一个关键功能。Express 模块没有定义要求或对将数据库集成到 Node-Express Web 应用程序施加限制。因此,这为您提供了选择任何数据库选项的灵活性,无论是关系型数据库(如 PostgreSQL)还是 NoSQL 数据库(如 MongoDB)。
MongoDB
在为任何应用程序选择 NoSQL 数据库时,MongoDB 是首选。它是一个面向文档的数据库,以灵活的、类似 JSON 的文档存储数据。这意味着字段可以因文档而异,数据模型可以根据不断变化的应用程序需求随时间演变。
重视可用性和可伸缩性的应用程序可以从 MongoDB 的分布式架构特性中受益。它内置了对高可用性的支持,使用分片进行水平扩展,以及跨地理分布的多数据中心可伸缩性。
MongoDB 拥有强大的查询语言,能够实现即席查询、快速查找的索引以及实时聚合,这些功能提供了强大的数据访问和分析方式,即使在数据量呈指数级增长的情况下也能保持性能。
在www.mongodb.com/探索 MongoDB 的功能和服务。
选择 MongoDB 作为 Node 和 Express Web 应用程序的数据库,将使您拥有一个完全基于 JavaScript 的独立服务器端应用程序。这将为您留下集成客户端界面的选择,该界面可能使用兼容的前端库(如 React)构建,以完成全栈应用程序。
React
React 是一个用于构建用户界面的声明式和组件化 JavaScript 库。其声明性和模块化特性使得开发者能够轻松创建和维护可重用、交互式和复杂用户界面。
如果使用 React 构建,大量显示动态数据的大型应用程序可以快速且响应灵敏,因为它会高效地更新和渲染仅在特定数据发生变化时所需的用户界面组件。React 通过其虚拟 DOM 的显著实现进行这种高效的渲染,这使得 React 与其他直接在浏览器 DOM 中进行昂贵操作处理页面更新的其他 Web 用户界面库区分开来。
使用 React 开发用户界面也迫使前端程序员编写逻辑清晰、模块化的代码,这些代码可重用且易于调试、测试和扩展。
查看reactjs.org/上的 React 资源。
由于所有四种技术都是基于 JavaScript 的,这些技术本身就是为了集成而优化的。然而,在实际应用中,如何将这些技术组合起来形成 MERN 栈,这取决于应用需求和开发者的偏好,这使得 MERN 栈可以根据特定需求进行定制和扩展。这个栈是否适合你的下一个全栈 Web 项目,不仅取决于它能否满足你的需求,还取决于它在行业中的表现以及这些技术的发展方向。
MERN 的相关性
自从 JavaScript 诞生以来,它已经走得很远,并且一直在不断发展。MERN 栈技术挑战了现状,并为 JavaScript 的可能性开辟了新的天地。但是,当涉及到开发需要可持续性的实际应用时,它是一个值得的选择吗?以下几节简要概述了选择 MERN 作为下一个 Web 应用程序的理由。
技术栈的一致性
由于 JavaScript 被广泛使用,开发者不需要频繁地学习和转换以与非常不同的技术一起工作。这也使得在负责 Web 应用程序不同部分的团队之间进行更好的沟通和理解成为可能。
学习、开发、部署和扩展所需的时间更少
整个技术栈的一致性也使得学习和使用 MERN 变得容易,减少了采用新栈的 overhead 和开发工作产品的耗时。一旦 MERN 应用程序的工作基础建立并确立了工作流程,复制、进一步开发或扩展任何应用程序所需的努力就会减少。
在行业中广泛采用
所有规模的组织都在根据他们的需求采用这个栈中的技术,因为它们可以更快地构建应用程序,处理高度多样化的需求,并在规模上更有效地管理应用程序。
社区支持和增长
围绕非常流行的 MERN 栈技术的开发者社区非常多样化,并且正在不断增长。由于很多人持续使用、修复、更新并愿意帮助这些技术发展,可预见的未来支持系统将保持强大。这些技术将继续得到维护,并且很可能在文档、附加库和技术支持方面提供资源。
使用这些技术的便捷性和好处已经得到了广泛的认可。由于继续采用和适应这些技术的知名公司,以及不断有更多的人为代码库做出贡献、提供支持和创建资源,MERN 栈中的技术将继续在未来一段时间内保持相关性。
为了确定这个广泛采用的堆栈是否满足您项目的特定要求,您可以探索使用这一组技术可能实现的功能范围。在下一节中,我们将突出介绍这个堆栈的一些方面,以及本书示例应用程序的几个功能,以展示这些技术所提供的多样化选项。
MERN 应用程序的范围
考虑到每个技术所具有的独特特性,以及通过整合其他技术轻松扩展此堆栈功能性的便利性,使用此堆栈可以构建的应用程序范围实际上非常广泛。
现在,Web 应用程序默认预期是丰富的客户端应用程序,它们具有沉浸式、交互式,并且在性能或可用性方面不会有所欠缺。MERN 技术的优势组合使其非常适合开发满足这些特定方面和需求的应用程序。
此外,一些这些技术的创新和即将推出的特性,例如 Node 的底层操作操作、MongoDB GridFS 的大文件流能力,以及使用 React 360 在 Web 上实现 VR 功能,使得使用 MERN 构建更加复杂和独特的应用程序成为可能。
可能有人会挑选 MERN 技术中的特定功能,并争论为什么它们不适合某些应用程序。然而,鉴于 MERN 堆栈的灵活性和可扩展性,这些担忧可以在 MERN 中逐个案例解决。在本书中,我们将演示如何面对构建应用程序中的特定要求和需求时进行此类考虑。
本书将介绍在 MERN 框架下开发的应用程序
为了展示 MERN 的广泛可能性以及如何轻松地构建具有不同功能的 Web 应用程序,本书将展示日常使用的 Web 应用程序以及复杂和罕见的 Web 体验。
社交媒体平台
对于第一个 MERN 应用程序,我们将构建一个受 Twitter 和 Facebook 启发的基本社交媒体应用程序,如下所示:
这个社交媒体平台将实现简单的功能,如帖子分享、点赞和评论、关注朋友以及聚合新闻源。
基于 Web 的教室应用程序
远程或在线学习现在是常见的做法,讲师和学生都利用互联网连接在在线平台上进行教学和学习。我们将使用 MERN 实现一个简单的基于 Web 的教室应用程序,其外观如下所示:
这个教室将具备一些功能,允许讲师添加包含课程的课程,同时学生可以报名参加这些课程并跟踪他们的进度。
在线市场
互联网上充满了各种电子商务 Web 应用程序,它们不会很快过时。使用 MERN,我们将构建一个具有从基础到高级电子商务功能的综合在线市场应用程序。以下截图显示了市场完成的首页,包括产品列表:
这个市场应用的功能将涵盖支持卖家账户、产品列表、客户购物车、支付处理、订单管理和实时竞标能力等方面。
支出跟踪应用
将数据可视化添加到任何数据密集型应用可以显著提高其价值。我们将通过扩展 MERN 来添加支出跟踪应用,以展示你如何在全栈 MERN 应用程序中集成数据可视化功能,包括图表和图形。以下截图显示了支出跟踪应用完成的首页,概述了用户的当前支出:
使用这个应用程序,用户将能够跟踪他们的日常支出。应用程序将随着时间的推移添加支出。然后,应用程序将提取数据模式,为用户提供一个视觉表示,展示他们的支出习惯随时间如何发展。
媒体流应用
为了测试一些高级 MERN 功能,一个更沉浸式的应用,如媒体流应用,是下一个选择。以下截图显示了包含添加到该平台的热门视频列表的首页视图,这些功能灵感来自 Netflix 和 YouTube:
在这个媒体流应用中,我们将实现内容上传和查看功能,为内容提供者提供媒体内容上传功能,并为观众提供实时内容流。
VR 网页游戏
通过使用建立在 React 之上的框架,如 React 360,可以将 Web VR 和 3D 功能应用于 React 的用户界面。我们将探讨如何在 MERN 中使用 React 360 创建独特的 Web 体验,通过构建一个基本的 VR 游戏应用来展示,如下面的截图所示:
用户将能够玩 VR 游戏,并使用这个基于 Web 的应用程序制作自己的游戏。每个游戏都将有动画 VR 对象放置在 360 度的世界中,玩家将需要找到并收集这些对象以完成游戏。
随着书中这些多样化应用的实现,你将学会如何结合、扩展和使用 MERN 堆栈技术来构建全栈 Web 应用程序,并揭示你自己的全栈项目多样化的选择。
摘要
在本章中,我们了解了在 MERN 技术栈中开发 Web 应用的背景,以及这本书将如何帮助您使用这个技术栈进行开发。MERN 技术栈项目集成了 MongoDB、Express、React 和 Node 来构建 Web 应用。这个技术栈中的每一种技术都在 Web 开发领域取得了相关的发展。这些技术被广泛采用,并在不断增长的社区支持下持续改进。使用具有不同需求的 MERN 应用开发是可能的,从日常使用的应用到更复杂的 Web 体验。这本书的实用导向方法可用于从基础到高级提升 MERN 技能,或者直接用于构建更复杂的应用。
在下一章中,我们将开始为 MERN 应用开发做准备,通过学习如何使用每种 MERN 技术栈来设置开发环境,并编写一个 MERN 入门应用的代码,以确保您的系统设置正确。
第三章:准备开发环境
在使用 MERN 堆栈构建应用程序之前,我们首先需要为每种技术以及辅助开发和调试的工具准备开发环境。使用此堆栈需要您使不同的技术和工具协同工作,鉴于有关此主题的许多选项和资源,弄清楚这一切如何结合在一起可能是一项艰巨的任务。本章将指导您了解工作空间选项、基本开发工具、如何在您的工空间中设置 MERN 技术,以及如何通过实际代码检查此设置。
我们将涵盖以下主题:
-
选择开发工具
-
设置 MERN 堆栈技术
-
检查您的开发设置
选择开发工具
在选择基本开发工具时,如文本编辑器或 IDE、版本控制软件,甚至开发工作空间本身时,有很多选项可供选择。在本节中,我们将讨论与 MERN 堆栈相关的选项和建议,以便您可以根据个人偏好做出明智的选择。
工作空间选项
在本地机器上进行开发是程序员中的一种常见做法,但随着良好的云端和远程开发服务的出现,例如 AWS Cloud9 (aws.amazon.com/cloud9/?origin=c9io) 和 Visual Studio Code 的远程开发扩展 (code.visualstudio.com/docs/remote),您可以使用 MERN 技术设置您的本地工作空间(本书余下部分将假设这种情况),但您也可以选择在为 Node 开发配备的云端服务中运行和开发代码。
本地与云端开发
您可以选择同时使用这两种类型的工作空间,以享受在本地工作的好处,无需担心带宽/互联网问题,并在您没有物理上拥有您最喜欢的本地机器时远程工作。为此,您可以使用 Git 进行版本控制您的代码,将最新的代码存储在远程 Git 托管服务(如 GitHub 或 BitBucket)上,然后在所有工作空间中共享相同的代码。在您的工空间中,您可以从许多可用的选项中选择您喜欢的 IDE 来编写代码,其中一些将在下文中讨论。
IDE 或文本编辑器
大多数云端开发环境都会集成源代码编辑器,但针对您的本地工作空间,您可以根据自己的编程偏好选择任何一种,然后对其进行定制以适应 MERN 开发。例如,以下流行的选项可以根据需要定制:
-
Visual Studio Code (
code.visualstudio.com/):Microsoft 开发的特性丰富的源代码编辑器,广泛支持现代网络应用程序开发工作流程,包括对 MERN 堆栈技术的支持 -
Atom (
atom.io/):一个免费的、开源的文本编辑器,由 GitHub 提供,有许多其他开发者提供的与 MERN 堆栈相关的包 -
SublimeText (
www.sublimetext.com/):一个专有、跨平台的文本编辑器,也提供了许多与 MERN 堆栈相关的包,以及 JavaScript 开发支持 -
WebStorm (
www.jetbrains.com/webstorm/):JetBrains 开发的完整 JavaScript IDE,支持 MERN 堆栈开发
您还有其他可用的编辑器,但除了关注每个编辑器能提供什么之外,选择一个适合您、能够实现高效工作流程并且与其他必要的网络应用程序开发工具良好集成的编辑器同样重要。
Chrome 开发者工具
加载、查看和调试前端是网络开发过程中的一个非常重要的部分。Chrome DevTools(developers.google.com/web/tools/chrome-devtools),它是 Chrome 浏览器的一部分,拥有许多出色的功能,允许调试、测试和实验前端代码以及 UI 的外观、感觉、响应性和性能。此外,React Developer Tools 扩展作为 Chrome 插件提供,它为 Chrome DevTools 添加了 React 调试工具。
在您的开发工作流程中利用此类工具可以帮助您更好地理解代码,并有效地构建应用程序。同样,将代码版本控制与 Git 等工具集成可以提高您作为开发者的生产力和效率。
Git
任何开发工作流程如果没有一个能够跟踪代码更改、代码共享和协作的版本控制系统都是不完整的。多年来,Git 已成为许多开发者的首选版本控制系统,并且是最广泛使用的分布式源代码管理工具。在本书的代码开发中,Git 将主要帮助我们跟踪在构建每个应用程序的步骤中的进度。
安装
要开始使用 Git,首先根据您的系统规格在本地机器或云开发环境中安装它。有关下载和安装 Git 最新版本的说明以及使用 Git 命令的文档,可以在git-scm.com/downloads找到。
远程 Git 托管服务
基于云的 Git 仓库托管服务,如 GitHub 和 BitBucket,有助于在工作和部署环境中共享您的最新代码,并备份您的代码。这些服务包含了许多有用的功能,以帮助进行代码管理和开发工作流程。要开始使用,您可以创建一个账户并为您的代码库设置远程仓库。
所有这些基本工具都将帮助丰富您的 Web 开发工作流程并提高生产力。一旦您根据下一节中的讨论在您的空间中完成必要的设置,我们将继续前进并开始构建 MERN 应用程序。
设置 MERN 技术栈
随着本书的编写,MERN 技术栈正在开发和升级,因此对于本书中展示的工作,我们使用撰写本文时的最新稳定版本。这些技术的大多数安装指南取决于您的工作空间系统环境,因此本节指向所有相关的安装资源,并作为设置一个完全运行的 MERN 技术栈的指南。
MongoDB
在将任何数据库功能添加到 MERN 应用程序之前,MongoDB 必须设置好、运行良好,并且对您的开发环境可访问。在撰写本文时,MongoDB 的当前稳定版本是 4.2.0,本书中的应用程序开发使用的是这个版本的 MongoDB 社区版。本节其余部分提供了有关如何安装和运行 MongoDB 的资源。
安装
在您可以使用 MongoDB 进行开发之前,您需要在您的空间中安装并启动 MongoDB。MongoDB 的安装和启动过程取决于您的工作空间规格:
-
云开发服务将有自己的安装和设置 MongoDB 的说明。例如,AWS Cloud9 的如何操作步骤可以在
docs.c9.io/docs/setup-a-database#mongodb找到。 -
您可以在
docs.mongodb.com/manual/administration/install-community找到 MongoDB 在本地机器上的安装指南。
在您成功安装 MongoDB 到您的空间并使其运行后,您可以使用mongo shell与之交互。
运行 mongo shell
MongoDB shell 是 MongoDB 安装的一部分,是一个交互式工具,用于开始熟悉 MongoDB 操作。一旦 MongoDB 安装并运行,你就可以在命令行上运行 mongo shell。在 mongo shell 中,你可以使用命令查询和更新数据,并执行管理操作。
你也可以跳过 MongoDB 的本地安装,而是使用 MongoDB Atlas 在云中部署 MongoDB 数据库(www.mongodb.com/cloud/atlas)。它是一个全球云数据库服务,可以用于向现代应用程序添加完全管理的 MongoDB 数据库。
MERN 开发的下一个核心组件是 Node.js,这是完成剩余的 MERN 设置所必需的。
Node.js
MERN 应用程序的后端服务器实现依赖于 Node.js。在撰写本文时,13.12.0 是可用的最新稳定版 Node.js,书中代码也已在最新夜间构建的 14.0.0 版本上进行了测试。你选择的 Node.js 版本将捆绑 npm 作为包管理器。根据你选择 npm 或 Yarn 作为 MERN 项目的包管理器,你可以带或不带 npm 安装 Node.js。
安装
Node.js 可以通过直接下载、安装程序或 Node 版本管理器进行安装:
-
你可以通过直接下载源代码或针对你的工作空间平台预构建的安装程序来安装 Node.js。下载可在nodejs.org/en/download找到。
-
云开发服务可能预装了 Node.js,例如 AWS Cloud9 就是这样,或者会有添加和更新它的具体说明。
要测试安装是否成功,你可以打开命令行并运行node -v以查看它是否正确返回版本号。
使用 nvm 进行 Node 版本管理
如果你需要为不同的项目维护多个版本的 Node.js 和 npm,nvm 是一个有用的命令行工具,可以在同一工作空间上安装和管理不同版本。你必须单独安装 nvm。设置说明可以在github.com/creationix/…找到。
在你的系统上设置 Node.js 后,你可以使用 npm 或 Yarn 等包管理器开始集成 MERN 堆栈的其余部分。
Yarn 包管理器
Yarn 是一个相对较新的 JavaScript 依赖项包管理器,可以用作 npm 的替代品。它是一个快速、可靠且安全的依赖项管理器,提供了一系列额外的功能,包括离线模式,可以在没有互联网连接的情况下重新安装包,并支持多个包注册表,如 npmjs.com 和 Bower。
我们将使用 Yarn(v1.22.4)来管理本书中项目的 Node 模块和包。在使用 Yarn 之前,您需要在您的开发空间中安装它。根据您的操作系统及其版本,安装 Yarn 有多种方法。
要了解更多关于在您的开发空间中安装 Yarn 的选项,请访问 yarnpkg.com/lang/en/docs/install。
安装 Yarn 后,它可以用来添加其他必要的模块,包括 Express 和 React。
MERN 模块
剩余的 MERN 堆栈技术都可作为 Node.js 包模块提供,并可以使用 Yarn 添加到每个项目中。这包括运行每个 MERN 应用程序所需的关键模块,如 React 和 Express,以及开发期间必要的模块。在本节中,我们列出并讨论这些模块,然后在下一节中查看如何在实际项目中使用这些模块。
关键模块
要集成 MERN 堆栈技术并运行您的应用程序,我们需要以下模块:
-
React:要开始使用 React,我们需要两个模块:
-
react -
react-dom
-
-
Express:要在您的代码中使用 Express,您需要
express模块。 -
MongoDB:要直接在 Node 应用程序中使用 MongoDB,您需要添加驱动程序,该驱动程序作为名为
mongodb的 Node 模块提供。
这些关键模块将生成全栈 Web 应用程序,但我们需要一些额外的模块来帮助开发和应用代码的生成。
开发依赖模块
为了在整个 MERN 应用程序开发过程中保持一致性,我们将在客户端和服务器端实现中使用 ES6 和更高版本的新的 JavaScript 语法。因此,为了帮助开发过程,我们将使用以下附加模块来编译和打包代码,并在开发过程中代码更新时自动重新加载服务器和浏览器应用程序:
-
Babel 模块需要将 ES6 和 JSX 转换为适用于所有浏览器的合适 JavaScript。要使 Babel 工作所需的模块如下:
-
@babel/core -
babel-loader -
用于使用 Webpack 转换 JavaScript 文件
-
@babel/preset-env和@babel/preset-react以提供对 React 和最新 JavaScript 特性的支持
-
-
Webpack 模块将帮助打包编译后的 JavaScript,包括客户端和服务器端代码。要使 Webpack 工作所需的模块如下:
-
-
webpack。 -
webpack-cli用于运行 Webpack 命令。 -
webpack-node-externals用于在 Webpack 打包时忽略外部 Node.js 模块文件。 -
webpack-dev-middleware用于在代码开发期间通过连接的服务器提供 Webpack 输出的文件。 -
webpack-hot-middleware通过将浏览器客户端连接到 Webpack 服务器并在开发过程中接收代码更改时的更新,将热模块重载添加到现有服务器中。 -
nodemon用于在开发期间监视服务器端更改,以便服务器可以重新加载以使更改生效。 -
react-hot-loader用于客户端上的快速开发。每当 React 前端中的文件发生变化时,react-hot-loader都会启用浏览器应用程序更新,而无需重新捆绑整个前端代码。 -
@hot-loader/react-dom用于启用对 React hooks 的热重载支持。它本质上替换了相同版本的react-dom包,但增加了额外的补丁以支持热重载。
-
虽然 react-hot-loader 旨在帮助开发流程,但将其作为常规依赖项而不是开发依赖项安装是安全的。它将自动确保在生产环境中禁用热重载,并且占用空间最小。
在安装并准备好使用必要的 MERN 堆栈技术和相关工具后,在下一节中,我们将使用此工具集编写代码以确认您的开发空间是否正确设置以开始开发基于 MERN 的 Web 应用程序。
检查您的开发设置
在本节中,我们将通过开发工作流程,逐步编写代码以确保环境正确设置以开始开发和运行 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/PacktPublishing/Full-Stack-React-Projects-Second-Edition/tree/master/Chapter02/mern-simplesetup。您可以将此代码克隆并运行,同时您将通读本章其余部分的代码解释。
我们将配置文件留在根目录中,并将应用程序代码组织到客户端和服务器端文件夹中。client 文件夹将包含前端代码,而 server 文件夹将包含后端代码。在本节的其余部分,我们将生成这些文件,并实现前端和后端代码以构建一个可工作的全栈 Web 应用程序。
初始化 package.json 和安装 Node.js 模块
我们将首先使用 Yarn 安装所有必需的模块。在项目文件夹中添加 package.json 文件是一种最佳实践,以维护、记录和共享在 MERN 应用程序中使用的 Node.js 模块。package.json 文件将包含有关应用程序的元信息,并列出模块依赖项。
执行以下要点中概述的步骤以生成 package.json 文件,修改它,并使用它来安装模块:
-
yarn init:从命令行进入您的项目文件夹,并运行yarn init。您将回答一系列问题以收集有关项目的元信息,例如名称、许可证和作者。然后,将自动生成一个包含您答案的package.json文件。 -
dependencies:在编辑器中打开package.json,并修改JSON对象,添加键模块和react-hot-loader作为常规依赖项:
在代码块之前提到的文件路径表示代码在项目目录中的位置。本书中一直保持此约定,以便在跟随代码时提供更好的上下文和指导。
mern-simplesetup/package.json:
"dependencies": {
"@hot-loader/react-dom": "16.13.0",
"express": "4.17.1",
"mongodb": "3.5.5",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-hot-loader": "4.12.20"
}
devDependencies:进一步修改package.json,将开发期间所需的以下 Node 模块作为devDependencies添加:
mern-simplesetup/package.json:
"devDependencies": {
"@babel/core": "7.9.0",
"@babel/preset-env": "7.9.0",
"@babel/preset-react": "7.9.4",
"babel-loader": "8.1.0",
"nodemon": "2.0.2",
"webpack": "4.42.1",
"webpack-cli": "3.3.11",
"webpack-dev-middleware": "3.7.2",
"webpack-hot-middleware": "2.25.0",
"webpack-node-externals": "1.7.2"
}
- yarn:保存
package.json,然后从命令行运行yarn命令以获取并添加所有这些模块到你的项目中。
在安装并添加所有必要的模块后,接下来我们将添加配置以编译和运行应用程序代码。
配置 Babel、Webpack 和 Nodemon
在我们开始编写网络应用程序代码之前,我们需要配置 Babel、Webpack 和 Nodemon 以在开发期间编译、捆绑和自动重新加载代码更改。
Babel
在你的项目文件夹中创建一个 .babelrc 文件,并添加以下带有 presets 和 plugins 的 JSON,指定如下:
mern-simplesetup/.babelrc:
{
"presets": [
["@babel/preset-env",
{
"targets": {
"node": "current"
}
}
],
"@babel/preset-react"
],
"plugins": [
"react-hot-loader/babel"
]
}
在此配置中,我们指定需要 Babel 将最新的 JavaScript 语法进行转译,支持 Node.js 环境中的代码以及 React/JSX 代码。react-hot-loader 模块需要 react-hot-loader/babel 插件来编译 React 组件。
Webpack
我们将不得不配置 Webpack 以捆绑客户端和服务器代码,并为生产环境单独捆绑客户端代码。在你的项目文件夹中创建 webpack.config.client.js、webpack.config.server.js 和 webpack.config.client.production.js 文件。所有三个文件都将具有以下代码结构,从导入开始,然后是 config 对象的定义,最后是定义的 config 对象的导出:
const path = require('path')
const webpack = require('webpack')
const CURRENT_WORKING_DIR = process.cwd()
const config = { ... }
module.exports = config
config JSON 对象将根据客户端或服务器端代码以及开发与生产代码的特定值而有所不同。在以下各节中,我们将突出显示每个配置实例中的相关属性。
或者,你也可以使用交互式门户 Generate Custom Webpack Configuration 在 generatewebpackconfig.netlify.com/ 或使用命令行的 Webpack-cli 的 init 命令生成 Webpack 配置。
开发时的客户端 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: [
'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()
],
resolve: {
alias: {
'react-dom': '@hot-loader/react-dom'
}
}
}
在 config 对象中突出显示的键和值将确定 Webpack 如何捆绑代码以及捆绑代码将放置的位置:
-
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 别名,将
react-dom引用指向@hot-loader/react-dom版本。
应用程序的客户端代码将从bundle.js中的捆绑代码在浏览器中加载。
Webpack 还提供了其他配置选项,可以根据你的代码和捆绑规范按需使用,正如我们将在探索服务器端特定捆绑时看到的。
服务器端 Webpack 配置
修改代码以要求nodeExternals,并在你的webpack.config.server.js文件中使用以下代码更新config对象,以配置 Webpack 进行服务器端代码的捆绑:
mern-simplesetup/webpack.config.server.js:
const nodeExternals = require('webpack-node-externals')
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所在的server文件夹开始捆绑,然后在dist文件夹中的server.generated.js中输出捆绑代码。在捆绑过程中,将假设 CommonJS 环境,因为我们已在libraryTarget中指定了commonjs2,因此输出将被分配给module.exports。
我们将使用生成的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
在你的项目文件夹中创建一个 nodemon.json 文件,并添加以下配置:
mern-simplesetup/nodemon.json:
{
"verbose": false,
"watch": [ "./server" ],
"exec": "webpack --mode=development --config
webpack.config.server.js
&& node ./dist/server.generated.js"
}
此配置将设置 nodemon 在开发期间监视服务器文件中的更改,然后根据需要执行编译和构建命令。我们可以开始编写一个简单的全栈 Web 应用程序的代码,以查看这些配置的实际效果。
使用 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="img/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 组件:
mern-simplesetup/client/HelloWorld.js:
import React from 'react'
import { hot } from 'react-hot-loader'
const HelloWorld = () => {
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
export default hot(module)(HelloWorld)
HelloWorld.js 包含一个基本的 HelloWorld React 组件,它通过 react-hot-loader 在开发期间启用热重载进行热导出。
要在服务器接收到对根 URL 的请求时在浏览器中看到渲染的 React 组件,我们需要使用 Webpack 和 Babel 设置来编译和打包此代码,并添加响应根路由请求的打包代码的服务器端代码。我们将在下一节中实现此服务器端代码。
使用 Express 和 Node 的服务器
在项目文件夹中,创建一个名为 server 的文件夹,并添加一个名为 server.js 的文件,该文件将设置服务器。然后,添加另一个名为 devBundle.js 的文件,该文件将帮助在开发模式下使用 Webpack 配置编译 React 代码。在接下来的章节中,我们将实现 Node-Express 应用程序,该应用程序启动客户端代码打包,启动服务器,设置路径以向客户端提供静态资产,并在对根路由发起 GET 请求时在模板中渲染 React 视图。
Express 应用程序
在 server.js 中,我们首先将添加代码以导入 express 模块,以便初始化一个 Express 应用程序:
mern-simplesetup/server/server.js:
import express from 'express'
const app = express()
然后,我们将使用这个 Express 应用程序来构建 Node 服务器应用程序的其余部分。
在开发期间打包 React 应用程序
为了保持开发流程简单,我们将初始化 Webpack,在服务器运行时编译客户端代码。在devBundle.js中,我们将设置一个compile方法,该方法接受 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文件夹中提供静态文件:
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')))
这将配置 Express 应用,当请求的路由以/dist开头时,从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())
})
最后,配置 Express 应用以启动一个监听指定端口的请求的服务器:
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)
})
使用此代码,当服务器运行时,它将能够接受根路由的请求并在浏览器中渲染带有“Hello World”文本的 React 视图。这个全栈实现中唯一缺少的部分是与数据库的连接,我们将在下一节中添加。
将服务器连接到 MongoDB
为了将你的 Node 服务器连接到 MongoDB,请将以下代码添加到server.js中,并确保你在工作区中运行 MongoDB 或者你有云 MongoDB 数据库实例的 URL:
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是使用其 URL 连接到运行中的 MongoDB 实例的驱动程序。它允许我们在后端实现数据库相关代码。这完成了使用 MERN 设置的这个简单 Web 应用程序的全栈集成,最终我们可以运行此代码以查看应用程序实时工作。
运行脚本
为了运行应用程序,我们将更新package.json文件,为开发和生产添加以下运行脚本:
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"
}
让我们看看代码:
-
yarn development:此命令将启动 Nodemon、Webpack 和服务器以进行开发。 -
yarn build:这将生成生产模式下的客户端和服务器代码包(在运行此脚本之前,请确保从server.js中删除devBundle.compile代码)。 -
yarn start:此命令将在生产中运行打包后的代码。
您可以使用以下命令来运行应用程序,无论是您在开发应用程序时进行调试,还是当应用程序准备在生产环境中上线时。
实时开发和调试
要运行到目前为止开发的代码,并确保一切正常工作,您可以按照以下步骤进行:
-
从命令行运行应用程序:
yarn development。 -
在浏览器中加载:在浏览器中打开根 URL,如果您使用的是本地机器,则为
http://localhost:3000。您应该看到一个标题为 MERN Kickstart 的页面,该页面仅显示 Hello World!。 -
实时开发代码和调试:将
HelloWorld.js组件的文本从"Hello World!"更改为仅"hello"。保存更改后,您可以在浏览器中看到即时更新,并且还可以检查命令行输出以确认bundle.js没有被重新创建。同样,当您更改服务器端代码时,您也可以看到即时更新,这可以在开发过程中提高生产力。
如果您已经走到这一步,恭喜您!您已经准备好开始开发令人兴奋的 MERN 应用程序了。
摘要
在本章中,我们讨论了开发工具选项以及如何安装 MERN 技术,然后我们编写了代码来检查开发环境是否正确设置。
我们首先查看适合 Web 开发的推荐工作区、IDE、版本控制软件和浏览器选项。您可以根据作为开发者的个人喜好从中选择。
接下来,我们首先安装 MongoDB、Node 和 Yarn,然后使用 Yarn 添加剩余所需库,从而设置 MERN 堆栈技术。
在编写代码以检查此设置之前,我们已配置 Webpack 和 Babel 在开发期间编译和打包代码,并构建生产就绪代码。我们了解到,在将应用程序打开到浏览器之前,有必要编译用于开发 MERN 应用程序的 ES6 和 JSX 代码。
此外,我们还通过包括 React Hot Loader 用于前端开发、配置 Nodemon 用于后端开发,以及在开发期间运行服务器时使用一条命令编译客户端和服务器代码,使开发流程更高效。
在下一章中,我们将使用这个设置开始构建一个作为全功能应用程序基础的骨架 MERN 应用程序。
第四章:从零开始构建 MERN
在本部分,我们从零开始构建一个全栈 MERN 应用程序,并展示如何轻松扩展以开发第一个示例应用程序。
本节包含以下章节:
-
第三章,使用 MongoDB、Express 和 Node 构建后端
-
第四章,添加 React 前端以完成 MERN
-
第五章,将骨架扩展成社交媒体应用程序
第五章:使用 MongoDB、Express 和 Node 构建后端
在开发不同的网络应用程序时,你会发现过程中有常见的任务、基本功能和重复的实现代码。对于本书中将开发的 MERN 应用程序也是如此。考虑到这些相似之处,我们首先将建立一个可轻松修改和扩展以实现各种 MERN 应用程序的框架 MERN 应用程序的基础。
在本章中,我们将涵盖以下主题,并从使用 Node、Express 和 MongoDB 实现的 MERN 框架的后端实现开始:
-
框架应用程序概述
-
后端代码设置
-
使用 Mongoose 的用户模型
-
使用 Express 的用户 CRUD API 端点
-
使用 JSON Web Tokens 进行用户认证
-
运行后端代码并检查 API
框架应用程序概述
框架应用程序将封装基本功能和大多数 MERN 应用程序中重复的工作流程。我们将构建一个基本但功能齐全的 MERN 网络应用程序,具有用户创建、读取、更新、删除(CRUD)和认证-授权(auth)功能;这还将展示如何使用此堆栈开发、组织和运行通用网络应用程序的代码。目标是使框架尽可能简单,以便易于扩展,并可作为开发不同 MERN 应用程序的基础应用程序。
功能分解
在框架应用程序中,我们将添加以下用例,包括用户 CRUD 和 auth 功能实现:
-
注册:用户可以通过使用电子邮件地址创建新账户进行注册。
-
用户列表:任何访客都可以看到所有注册用户的列表。
-
认证:注册用户可以登录和登出。
-
受保护的用户资料:只有注册用户在登录后才能查看个人用户详情。
-
授权用户编辑和删除:只有注册并认证的用户可以编辑或删除自己的用户账户详情。
通过这些功能,我们将拥有一个简单的运行中的网络应用程序,支持用户账户。我们将从后端实现开始构建这个基本网络应用程序,然后集成 React 前端以完成全栈。
定义后端组件
在本章中,我们将专注于使用 Node、Express 和 MongoDB 构建一个工作后端框架,该后端将是一个独立的服务器端应用程序,可以处理创建用户、列出所有用户、在数据库中查看、更新或删除用户等 HTTP 请求,同时考虑用户认证和授权。
用户模型
用户模型将定义要存储在 MongoDB 数据库中的用户详情,并处理与用户相关的业务逻辑,如密码加密和用户数据验证。此版本的用户模型将是基本的,支持以下属性:
| 字段名称 | 类型 | 描述 |
|---|---|---|
name | 字符串 | 存储用户名称的必填字段。 |
email | 字符串 | 必需的唯一字段,用于存储用户的电子邮件并识别每个账户(每个唯一电子邮件地址仅允许一个账户)。 |
password | 字符串 | 认证所需的字段。出于安全目的,数据库将存储加密后的密码,而不是实际字符串。 |
created | 日期 | 创建新用户账户时自动生成的时间戳。 |
updated | 日期 | 更新现有用户详细信息时自动生成的时间戳。 |
当我们通过扩展此骨架构建应用程序时,我们可以根据需要添加更多字段。但以这些字段开始将足以识别唯一的用户账户,并且还可以实现与用户 CRUD 操作相关的功能。
用户 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 操作中的一些将具有受保护访问权限,这要求请求客户端进行认证、授权或两者兼而有之,具体取决于功能规范。表中最后两个路由用于认证,将允许用户登录和登出。对于本书中开发的应用程序,我们将使用 JWT 机制来实现这些认证功能,如下一节中更详细地讨论。
使用 JSON Web Tokens 进行认证
为了根据骨架功能限制和保护对用户 API 端点的访问,后端需要整合认证和授权机制。在实现 Web 应用程序的用户认证方面有许多选择。最常见且经过时间考验的选项是在客户端和服务器端使用会话来存储用户状态。但一种较新的方法是使用 JSON Web Token(JWT)作为无状态认证机制,它不需要在服务器端存储用户状态。
这两种方法在相关的现实世界用例中都有其优势。然而,为了保持本书中的代码简单,并且因为它与 MERN 栈和我们的示例应用程序配合良好,我们将使用 JWT 进行认证实现。
JWT 的工作原理
在深入到 MERN 栈中 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 骨架的后端部分,我们将设置项目文件夹,安装和配置必要的 Node 模块,然后准备运行脚本以帮助开发和运行代码。然后,我们将逐步分析代码以实现一个工作的 Express 服务器、带有 Mongoose 的用户模型、Express 路由器上的 API 端点以及基于 JWT 的身份验证,以满足我们之前定义的用户功能规格。
本章将讨论的代码以及完整的骨架应用程序可在 GitHub 上找到 https://github.com/PacktPublishing/Full-Stack-React-Projects-Second-Edition/tree/master/Chapter03%20and%2004/mern-skeleton 。仅后端代码可在同一存储库的 mern2-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
| -- yarn.lock
我们将把配置文件放在根目录下,把与后端相关的代码放在 server 文件夹中。在 server 文件夹内,我们将后端代码划分为包含模型、控制器、路由、辅助函数和通用服务器端代码的模块。这个文件夹结构将在下一章中进一步扩展,届时我们将通过添加 React 前端来完成骨架应用程序。
初始化项目
如果你的开发环境已经设置好,你可以初始化 MERN 项目以开始开发后端。首先,我们将从项目文件夹中初始化 package.json,配置和安装任何开发依赖项,设置代码中使用的配置变量,并在 package.json 中更新运行脚本以帮助开发和运行代码。
添加 package.json
我们需要一个 package.json 文件来存储关于项目的元信息,列出带有版本号的模块依赖项,并定义运行脚本。要在项目文件夹中初始化 package.json 文件,从命令行进入项目文件夹,运行 yarn init,然后按照指示添加必要的详细信息。创建 package.json 后,我们可以继续设置和开发,并在代码实现过程中需要更多模块时更新该文件。
开发依赖项
为了开始开发过程并运行后端服务器代码,我们将配置和安装 Babel、Webpack 和 Nodemon,如第二章所述,准备开发环境,并对后端进行一些小的调整。
Babel
由于我们将在后端代码中使用 ES6+ 和最新的 JS 特性,我们将安装和配置 Babel 模块将 ES6+ 转换为较旧的 JS 版本,以便与正在使用的 Node 版本兼容。
首先,我们将在 .babelrc 文件中配置 Babel,使用最新的 JS 特性预设,并指定当前版本的 Node 作为目标环境。
mern-skeleton/.babelrc:
{
"presets": [
["@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
将 targets.node 设置为 current 指示 Babel 编译针对当前版本的 Node,并允许我们在后端代码中使用 async/await 等表达式。
接下来,我们需要从命令行安装 Babel 模块作为 devDependencies:
yarn add --dev @babel/core babel-loader @babel/preset-env
一旦完成模块安装,你将注意到 package.json 文件中的 devDependencies 列表已经更新。
Webpack
我们需要 Webpack 来使用 Babel 编译和打包服务器端代码。对于配置,我们可以使用第二章中讨论的相同的 webpack.config.server.js。
从命令行运行以下命令以安装webpack、webpack-cli和webpack-node-externals模块:
yarn add --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:用于区分开发和生产模式 -
port:用于定义服务器的监听端口 -
jwtSecret:用于签名 JWT 的秘密密钥 -
mongoUri:项目 MongoDB 数据库实例的位置
这些变量将使我们能够从单个文件更改值并在后端代码中使用它。接下来,我们将添加运行脚本,这将允许我们运行和调试后端实现。
运行脚本
要在开发后端代码时运行服务器,我们可以从package.json文件中的yarn development脚本开始。对于完整的骨架应用程序,我们将使用我们在第二章“准备开发环境”中定义的相同运行脚本。
mern-skeleton/package.json:
"scripts": {
"development": "nodemon"
}
添加此脚本后,从项目文件夹中运行命令行中的yarn development将基本上根据nodemon.json中的配置启动 Nodemon。配置指示 Nodemon 监控服务器文件以进行更新,并在更新时重新构建文件,然后重启服务器,以便更改立即可用。我们将从实现具有此配置的工作服务器开始。
准备服务器
在本节中,我们将在开始实现用户特定功能之前,将 Express、Node 和 MongoDB 集成在一起,以运行一个完全配置的服务器。
配置 Express
要使用 Express,我们将安装它,然后在server/express.js文件中添加和配置它。
从命令行运行以下命令以安装express模块并自动更新package.json文件:
yarn add express
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 来简化浏览器-服务器通信。要安装此模块,请在命令行中运行yarn add body-parser。然后,使用bodyParser.json()和bodyParser.urlencoded({ extended: true })配置 Express 应用。 -
cookie-parser: 解析和设置请求对象中的 Cookie 的解析中间件。要安装cookie-parser模块,请在命令行中运行yarn add cookie-parser。 -
compression: 尝试压缩所有通过中间件传输的请求响应体的压缩中间件。要安装compression模块,请在命令行中运行yarn add compression。 -
helmet: 通过设置各种 HTTP 头来帮助保护 Express 应用的中间件集合。要安装helmet模块,请在命令行中运行yarn add helmet。 -
cors: 允许跨源资源共享(CORS)的中间件。要安装cors模块,请在命令行中运行yarn add cors。
在安装了前面的模块之后,我们可以更新 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 请求的信息,为此我们首先需要使用此应用启动一个服务器。
启动服务器
配置 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 应用以启动服务器。要运行此代码并继续开发,我们可以在命令行中运行 yarn development。如果代码没有错误,服务器应该会启动运行,Nodemon 会监控代码更改。接下来,我们将更新此服务器代码以集成数据库连接。
设置 Mongoose 并连接到 MongoDB
我们将使用 mongoose 模块来实现此骨架中的用户模型,以及所有未来 MERN 应用的数据模型。在这里,我们将首先配置 Mongoose 并利用它来定义与 MongoDB 数据库的连接。
首先,要安装 mongoose 模块,请运行以下命令:
yarn add mongoose
然后,更新 server.js 文件以导入 mongoose 模块,配置它以使用原生 ES6 promises,并最终使用它来处理项目与 MongoDB 数据库的连接。
mern-skeleton/server/server.js:
import mongoose from 'mongoose'
mongoose.Promise = global.Promise
mongoose.connect(config.mongoUri, { useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true } )
mongoose.connection.on('error', () => {
throw new Error(`unable to connect to database: ${mongoUri}`)
})
如果你正在开发环境中运行代码,并且 MongoDB 也正在运行,保存此更新应该可以成功重启服务器,现在该服务器已集成 Mongoose 和 MongoDB。
Mongoose 是一个 MongoDB 对象建模工具,它提供了一个基于模式的解决方案来建模应用程序数据。它包括内置的类型转换、验证、查询构建和业务逻辑钩子。使用 Mongoose 与此后端堆栈一起提供 MongoDB 之上的更高层,具有更多功能,包括将对象模型映射到数据库文档。这使得使用 Node 和 MongoDB 后端进行开发更加简单和高效。要了解更多关于 Mongoose 的信息,请访问 mongoosejs.com。
在配置了 Express 应用程序、数据库集成 Mongoose 以及准备就绪的监听服务器后,我们可以添加代码以从后端加载 HTML 视图。
在根 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/。
到目前为止,我们可以构建在基于 Node-Express-和 MongoDB 的后端服务器上,以添加特定于用户的功能。
实现用户模型
我们将在 server/models/user.model.js 文件中实现用户模型,并使用 Mongoose 定义包含必要用户数据字段的模式。我们这样做是为了可以对字段添加内置验证,并集成如密码加密、身份验证和自定义验证等业务逻辑。
我们将首先导入 mongoose 模块,并使用它来生成一个 UserSchema,它将包含模式定义和与用户相关的业务逻辑,以构成用户模型。这个用户模型将被导出,以便其余的后端代码可以使用它。
mern-skeleton/server/models/user.model.js:
import mongoose from 'mongoose'
const UserSchema = new mongoose.Schema({ … })
export default mongoose.model('User', UserSchema)
mongoose.Schema() 函数接受一个模式定义对象作为参数,以生成一个新的 Mongoose 模式对象,该对象将指定集合中每个文档的属性或结构。在我们添加任何业务逻辑代码以完成用户模型之前,我们将讨论用户集合的模式定义。
用户模式定义
需要生成新 Mongoose 模式的用户模式定义对象将声明所有用户数据字段和相关属性。该模式将记录与用户相关的信息,包括名称、电子邮件、创建时间和最后更新时间戳、哈希密码以及相关的唯一密码盐。我们将在下一节中详细说明这些属性,展示每个字段如何在用户模式代码中定义。
名称
名称字段是String类型的必填字段。
mern-skeleton/server/models/user.model.js:
name: {
type: String,
trim: true,
required: 'Name is required'
},
此字段将存储用户的名称。
邮箱
电子邮件字段是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,
这些Date值将程序化生成,以记录用户创建和用户数据更新的时间戳。
哈希密码和盐
hashed_password和salt字段代表用于认证的加密用户密码。
mern-skeleton/server/models/user.model.js:
hashed_password: {
type: String,
required: "Password is required"
},
salt: String
为了安全起见,实际的密码字符串不会直接存储在数据库中,并且将单独处理,如下一节所述。
用于认证的密码
密码字段对于在任何应用程序中提供安全用户身份验证至关重要,每个用户密码都需要作为用户模型的一部分进行加密、验证和安全的认证。
将密码字符串作为虚拟字段处理
用户提供的密码字符串不会直接存储在用户文档中。相反,它被处理为一个虚拟字段。
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
})
当在用户创建或更新时接收到密码值时,它将被加密成一个新的哈希值,并设置到hashed_password字段,同时与salt字段中的唯一盐值一起。
加密和认证
用于生成表示密码值的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())) + ''
}
}
UserSchema方法可用于提供以下功能:
-
authenticate:此方法被调用以验证登录尝试,通过将用户提供的密码文本与数据库中特定用户的hashed_password进行匹配。 -
encryptPassword:此方法用于使用 Node 的crypto模块从纯文本密码和唯一的salt值生成加密哈希。 -
makeSalt:此方法使用执行时的当前时间戳和Math.random()生成一个唯一且随机的盐值。
crypto模块提供了各种加密功能,包括一些标准的加密哈希算法。在我们的代码中,我们使用 SHA1 哈希算法和crypto中的createHmac来从密码文本和salt对生成加密的 HMAC 哈希。
哈希算法为相同的输入值生成相同的哈希。但为了确保两个用户不会因为偶然使用相同的密码文本而得到相同的哈希密码,我们在为每个用户生成哈希密码之前,将每个密码与一个唯一的salt值配对。这也会使猜测所使用的哈希算法变得困难,因为看似相同的用户输入生成了不同的哈希。
这些UserSchema方法用于将用户提供的密码字符串加密成带有随机生成的salt值的hashed_password。当用户详细信息在创建或更新时保存到数据库中时,hashed_password和salt存储在用户文档中。为了匹配和验证用户在登录期间提供的密码字符串,需要这两个值。我们还应该确保用户一开始就选择一个强大的密码字符串,这可以通过添加自定义验证到护照字段来实现。
密码字段验证
为了向最终用户选择的实际密码字符串添加验证约束,我们需要添加自定义验证逻辑并将其与模式中的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以及所有与密码相关的业务逻辑,完成了用户模型实现。现在,我们可以导入并使用这个用户模型在其他后端代码的部分。但在我们开始使用这个模型来扩展后端功能之前,我们将添加一个辅助模块,这样我们就可以解析可读的 Mongoose 错误消息,这些错误消息是在对模式验证进行操作时抛出的。
Mongoose 错误处理
当用户数据保存到数据库中时,如果违反了添加到用户模式字段中的验证约束,将抛出错误消息。为了处理这些验证错误以及数据库在查询时可能抛出的其他错误,我们将定义一个辅助方法,该方法将返回可以在请求-响应周期中适当传播的相关错误消息。
我们将在 server/helpers/dbErrorHandler.js 文件中添加 getErrorMessage 辅助方法。此方法将解析并返回与特定验证错误或其他在查询 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 验证器违规而未抛出的错误将包含一个相关的错误 code。在某些情况下,这些错误需要不同的处理方式。例如,由于违反 unique 约束而引起的错误将返回一个与 Mongoose 验证错误不同的错误对象。unique 选项不是一个验证器,而是构建 MongoDB unique 索引的方便助手,因此我们将添加另一个 getUniqueErrorMessage 方法来解析与 unique 约束相关的错误对象并构建适当的错误信息。
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 操作抛出的错误时添加有意义的错误信息。
用户模型完成后,我们可以执行与使用我们将在下一节中开发的用户 API 实现用户 CRUD 功能相关的 Mongoose 操作。
添加用户 CRUD API
由 Express 应用程序公开的用户 API 端点将允许前端根据用户模型生成文档执行 CRUD 操作。为了实现这些工作端点,我们将编写 Express 路由和相应的控制器回调函数,这些函数应在针对这些声明的路由传入 HTTP 请求时执行。在本节中,我们将查看这些端点在没有任何认证限制的情况下是如何工作的。
我们将使用 Express 路由器在 server/routes/user.routes.js 中声明用户 API 路由,然后将其挂载到我们在 server/express.js 中配置的 Express 应用程序上。
mern-skeleton/server/express.js:
import userRoutes from './routes/user.routes'
...
app.use('/', userRoutes)
...
所有路由和 API 端点,例如我们接下来将声明的特定用户路由,都需要挂载到 Express 应用程序上,以便从客户端访问。
用户路由
在 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
除了声明与用户 CRUD 操作对应的 API 端点外,我们还将配置 Express 路由器,以便它通过执行 userByID 控制器函数来处理请求路由中的 userId 参数。
当服务器接收到这些定义的路由中的每个请求时,相应的控制器函数会被调用。我们将在下一小节中定义这些控制器方法的功能,并将其从 user.controller.js 文件中导出。
用户控制器
server/controllers/user.controller.js 文件将包含用于先前用户路由声明的控制器方法的定义,这些方法作为回调在服务器接收到路由请求时执行。
user.controller.js 文件将具有以下结构:
import User from '../models/user.model'
import extend from 'lodash/extend'
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,请在命令行中运行 yarn add lodash。
我们定义的每个控制器函数都与一个路由请求相关联,并将根据每个 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 = async (req, res) => {
const user = new User(req.body)
try {
await user.save()
return res.status(200).json({
message: "Successfully signed up!"
})
} catch (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
此函数使用从前端 POST 请求中接收到的用户 JSON 对象在 req.body 内创建新用户。调用 user.save 尝试在 Mongoose 对数据进行验证检查后,将新用户保存到数据库中。因此,会向请求客户端返回错误或成功响应。
create 函数被定义为使用 **async** 关键字的一个异步函数,允许我们使用 **await** 与 user.save() 一起,它返回一个 Promise。在 **async** 函数内部使用 **await** 关键字会导致此函数在执行下一行代码之前等待返回的 Promise 解决。如果 Promise 拒绝,则会抛出错误并在 catch 块中捕获。
Async/await 是 ES8 的一个新增功能,它允许我们以看似顺序或同步的方式编写异步 JavaScript 代码。对于处理异步行为,如访问数据库的控制器函数,我们将使用 async/await 语法来实现它们。
类似地,在下一节中,我们将使用 async/await 来实现控制器函数,以便在查询数据库后列出所有用户。
列出所有用户
获取所有用户的 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 = async (req, res) => {
try {
let users = await User.find().select('name email updated created')
res.json(users)
} catch (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
list 控制器函数从数据库中查找所有用户,仅在结果用户列表中填充 name、email、created 和 updated 字段,然后将此用户列表作为 JSON 对象数组返回给请求客户端。
剩余的 CRUD 操作,包括读取、更新和删除单个用户,需要我们首先通过 ID 获取特定的用户。在下文中,我们将实现控制器函数,以便从数据库中检索单个用户,以响应相应的请求,返回用户、更新用户或删除用户。
通过 ID 加载用户以读取、更新或删除
读取、更新和删除的所有三个 API 端点都需要根据被访问用户的用户 ID 从数据库中加载用户。我们将编程 Express 路由器首先执行此操作,然后再响应特定的读取、更新或删除请求。
加载
当 Express 应用程序收到一个请求,该请求与包含 :userId 参数的路径匹配时,应用程序将执行 userByID 控制器函数,该函数获取并加载用户到 Express 请求对象中,然后再将其传播到特定于请求的 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 = async (req, res, next, id) => {
try {
let user = await User.findById(id)
if (!user)
return res.status('400').json({
error: "User not found"
})
req.profile = user
next()
} catch (err) {
return res.status('400').json({
error: "Could not retrieve user"
})
}
}
如果在数据库中找到匹配的用户,则将用户对象附加到请求对象的 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 请求时,类似于读取,它会加载具有:userId参数值的用户,然后在执行update控制器函数之前。
mern-skeleton/server/controllers/user.controller.js:
const update = async (req, res) => {
try {
let user = req.profile
user = extend(user, req.body)
user.updated = Date.now()
await user.save()
user.hashed_password = undefined
user.salt = undefined
res.json(user)
} catch (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
update函数从req.profile中检索用户详细信息,然后使用lodash模块扩展并合并请求体中传入的更改以更新用户数据。在将此更新后的用户保存到数据库之前,updated字段被填充为当前日期,以反映最后更新的时间戳。在成功保存此更新后,更新后的用户对象通过删除敏感数据(如hashed_password和salt)来清理,然后在响应中发送给请求客户端。删除用户的最终用户控制器函数的实现与update函数类似,具体细节将在下一节中说明。
删除
删除用户的 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 = async (req, res) => {
try {
let user = req.profile
let deletedUser = await user.remove()
deletedUser.hashed_password = undefined
deletedUser.salt = undefined
res.json(deletedUser)
} catch (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
}
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)
...
这将使我们在auth.routes.js中定义的路由从客户端可访问。
认证路由
两个认证 API 在auth.routes.js文件中定义,使用express.Router()声明路由路径和相关 HTTP 方法。它们还被分配了相应的控制器函数,当接收到这些路由的请求时应该调用这些函数。
认证路由如下:
-
'/auth/signin':使用电子邮件和密码进行用户认证的 POST 请求 -
'/auth/signout':登录后,在响应对象上设置的 JWT 包含的 cookie 的 GET 请求用于清除
生成的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
对 signin 路由的 POST 请求和对 signout 路由的 GET 请求将调用在 auth.controller.js 文件中定义的相应控制器函数,如下一节所述。
认证控制器
server/controllers/auth.controller.js 中的认证控制器函数不仅会处理对 signin 和 signout 路由的请求,还会提供 JWT 和 express-jwt 功能,以启用受保护用户 API 端点的认证和授权。
mern-skeleton/server/controllers/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 实现用户认证。我们将从下一节的 signin 控制器函数开始。
登录
在以下路由中声明了用于登录用户的 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 = async (req, res) => {
try {
let user = await User.findOne({ "email": req.body.email })
if (!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
}
})
} catch (err) {
return res.status('401').json({ error: "Could not sign in" })
}
}
POST 请求对象在 req.body 中接收电子邮件和密码。这个电子邮件用于从数据库中检索匹配的用户。然后,使用在 UserSchema 中定义的密码认证方法来验证从客户端接收到的 req.body 中的密码。
如果密码验证成功,JWT 模块将使用密钥和用户 _id 值生成一个签名的 JWT。
通过在命令行中运行 yarn add jsonwebtoken 来安装 jsonwebtoken 模块,使其可用于此控制器。
然后,将签名的 JWT 返回给认证的客户端,并附带用户的详细信息。可选地,我们还可以将令牌设置为响应对象中的 cookie,以便在客户端(如果选择 cookie 作为 JWT 存储形式)可用。在客户端,此令牌必须在请求受保护路由时附加为 Authorization 标头。要注销用户,客户端可以简单地根据其存储方式删除此令牌。在下一节中,我们将学习如何使用 signout API 端点清除包含令牌的 cookie。
注销
在以下路由中声明了用于注销用户的 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,用户状态存储是客户端的责任,除了 cookies 之外,客户端还有多种存储选项。在注销时,客户端需要在客户端删除令牌,以确认用户不再经过身份验证。在服务器端,我们可以使用和验证在登录时生成的令牌来保护不应未经有效身份验证而访问的路由。在下一节中,我们将学习如何使用 JWT 实现这些受保护的路由。
使用 express-jwt 保护路由
为了保护对读取、更新和删除路由的访问,服务器需要检查请求客户端是否是经过身份验证和授权的用户。
当访问受保护的路由时,为了检查请求用户是否已登录并且具有有效的 JWT,我们将使用express-jwt模块。
express-jwt模块是一个中间件,用于验证 JSON Web Tokens。通过命令行运行yarn add express-jwt来安装express-jwt。
保护用户路由
我们将定义两个认证控制器方法,分别称为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 操作之前应该检查身份验证和授权。我们将在下一节中探讨requireSignin方法的实现,该方法用于检查身份验证,以及hasAuthorization方法的实现,该方法用于检查授权。
要求登录
auth.controller.js中的requireSignin方法使用express-jwt来验证传入的请求是否在Authorization头中包含有效的 JWT。如果令牌有效,它将在请求对象中添加一个带有'auth'键的已验证用户 ID;否则,它将抛出一个身份验证错误。
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()
}
在requireSignin认证验证后,express-jwt填充了req.auth对象,而req.profile则由user.controller.js中的userByID函数填充。我们将向需要认证和授权的路由添加hasAuthorization函数。
express-jwt的认证错误处理
为了处理express-jwt在尝试验证传入请求中的 JWT 令牌时抛出的认证相关错误,我们需要在mern-skeleton/server/express.js中的 Express 应用程序配置中添加以下错误捕获代码,在代码末尾,在路由挂载之后和应用程序导出之前:
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
res.status(401).json({"error" : err.name + ": " + err.message})
}else if (err) {
res.status(400).json({"error" : err.name + ": " + err.message})
console.log(err)
}
})
当由于某种原因令牌无法验证时,express-jwt会抛出一个名为UnauthorizedError的错误。我们在这里捕获这个错误,向请求客户端返回401状态。如果在这里生成并捕获其他服务器端错误,我们也会添加一个要发送的响应。
在为保护路由实现用户认证后,我们已经涵盖了我们的 MERN 应用程序骨架所需的所有工作后端功能。在下一节中,我们将探讨如何检查这个独立后端是否按预期工作,而不需要实现前端。
检查独立后端
在选择用于检查后端 API 的工具时,有多种选择,从命令行工具 curl(github.com/curl/curl)到高级 REST 客户端(ARC)(chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo),这是一个具有交互式用户界面的 Chrome 扩展应用程序。
要检查本章中实现的 API,首先,从命令行启动服务器,并使用这些工具之一请求路由。如果您在本地机器上运行代码,根 URL 是http://localhost:3000/。
使用 ARC,我们将展示五个用例的预期行为,以便我们可以检查实现的 API 端点。
创建新用户
首先,我们将通过/api/users POST 请求创建一个新用户,并在请求体中传递名称、电子邮件和密码值。当用户在数据库中成功创建且没有验证错误时,我们将看到如下截图所示的 200 OK 成功消息:
您也可以尝试发送具有无效名称、电子邮件和密码值的相同请求,以检查后端是否返回了相关的错误消息。接下来,我们将通过调用列表用户 API 来检查用户是否已成功创建并存储在数据库中。
获取用户列表
我们可以通过向/api/users发送一个获取所有用户的GET请求来查看新用户是否在数据库中。响应应包含存储在数据库中的所有用户对象的数组:
注意返回的用户对象只显示了_id、name、email和created字段的值,而没有显示salt或hashed_password值,这些值也存在于数据库中实际存储的文档中。请求只检索我们在列表控制器方法中指定的 Mongoose find查询中指定的所选字段。在获取单个用户时也存在这种省略。
尝试获取单个用户
接下来,我们将尝试在不先登录的情况下访问受保护的 API。读取任何用户的GET请求将返回 401 未授权错误,如下例所示。在这里,向/api/users/5a1c7ead1a692aa19c3e7b33发送的GET请求返回 401 错误:
为了使这个请求返回包含用户详情的成功响应,需要在请求头中提供一个有效的授权令牌。我们可以通过成功调用登录请求来生成一个有效的令牌。
登录
为了能够访问受保护的路由,我们将使用第一个示例中创建的用户凭据进行登录。为了登录,发送一个 POST 请求到/auth/signin,请求体中包含电子邮件和密码,如下面的截图所示:
登录成功后,服务器返回一个签名过的 JWT 和用户详情。我们需要这个令牌来访问用于获取单个用户的受保护路由。
成功获取单个用户
使用登录后收到的令牌,我们现在可以访问之前失败的受保护路由。在向/api/users/5a1c7ead1a692aa19c3e7b33发送 GET 请求时,令牌被设置为Authorization头部的 Bearer 方案中。这次,成功返回了用户对象:
如本节所示,使用 ARC,您还可以检查其他 API 端点的更新和删除用户实现。所有这些 API 端点按预期工作,我们就有了一个完整的 MERN 应用程序后端。
摘要
在本章中,我们使用 Node、Express 和 MongoDB 开发了一个完全功能独立的后端应用程序,并涵盖了 MERN 框架应用程序骨架的第一部分。在后端,我们实现了用于存储用户数据的用户模型,使用 Mongoose 实现;实现了执行 CRUD 操作的 API 端点,使用 Express 实现;以及实现了用于受保护路由的用户认证,使用 JWT 和express-jwt实现。
我们还通过配置 Webpack 来设置开发流程,以便使用 Babel 编译 ES6+代码。我们还配置了 Nodemon,以便在代码更改时重启服务器。最后,我们使用 Chrome 的高级 Rest API 客户端扩展应用程序检查了 API 的实现。
现在,我们已经准备好扩展这个后端应用程序代码并添加 React 前端,这将完成 MERN 框架的应用程序。我们将在下一章中这样做。
第六章:将 React 前端添加到完成 MERN
一个 Web 应用程序没有前端是不完整的。这是用户与之交互的部分,对于任何 Web 体验都至关重要。在本章中,我们将使用 React 为 MERN 骨架应用程序后端已实现的基本用户和认证功能添加交互式用户界面,我们在上一章中开始构建该应用程序。这个功能性的前端将添加连接到后端 API 的 React 组件,并允许用户根据授权在应用程序中无缝导航。到本章结束时,你将学会如何轻松地将 React 客户端与 Node-Express-MongoDB 服务器端集成,以创建全栈 Web 应用程序。
在本章中,我们将涵盖以下主题:
-
骨架的前端特性
-
使用 React、React Router 和 Material-UI 设置开发环境
-
使用 React 渲染主页
-
后端用户 API 集成
-
限制访问的认证集成
-
用户列表、个人资料、编辑、删除、注册和登录 UI 以完成用户前端
-
基本服务器端渲染
定义骨架应用程序的前端
为了完全实现我们在第三章“使用 MongoDB、Express 和 Node 构建后端”的“功能分解”部分中讨论的骨架应用程序功能,我们将向我们的基础应用程序添加以下用户界面组件:
-
主页:在根 URL 上渲染的视图,用于欢迎用户访问 Web 应用程序。
-
注册页面:一个带有用户注册表单的视图,允许新用户创建用户账户,并在成功创建后将其重定向到登录页面。
-
登录页面:一个带有登录表单的视图,允许现有用户登录,以便他们可以访问受保护的视图和操作。
-
用户列表页面:一个视图,用于从数据库中检索并显示所有用户的列表,并且还链接到单个用户个人资料。
-
个人资料页面:一个组件,用于检索并显示单个用户的信息。这仅对已登录用户可用,并且还包含编辑和删除选项,这些选项仅在已登录用户查看自己的个人资料时可见。
-
编辑个人资料页面:一个表单,用于检索用户信息以预填充表单字段。这允许用户编辑信息,并且此表单仅对尝试编辑自己个人资料的已登录用户可用。
-
删除用户组件:一个选项,允许已登录用户在确认其意图后删除自己的个人资料。
-
菜单导航栏:一个组件,列出所有对用户可用和相关的视图,并有助于指示用户在应用程序中的当前位置。
下面的 React 组件树图显示了我们将开发的所有 React 组件,以构建此基础应用程序的视图:
MainRouter将是主要的 React 组件。它包含应用程序中的所有其他自定义 React 视图。首页、注册、登录、用户、个人资料和编辑个人资料将在 React Router 声明的单独路由上渲染,而菜单组件将在所有这些视图中渲染。删除用户将是个人资料视图的一部分。
本章讨论的代码以及完整的骨架,可在 GitHub 上找到:github.com/PacktPublishing/Full-Stack-React-Projects-Second-Edition/tree/master/Chapter03%20and%2004/mern-skeleton。您可以克隆此代码,并在阅读本章其余部分的代码解释时运行应用程序。
为了实现这些前端 React 视图,我们不得不扩展现有的项目代码,这些代码包含 MERN 骨架的独立服务器应用程序。接下来,我们将简要查看构成这个前端并完成全栈骨架应用程序代码所需的文件。
文件夹和文件结构
以下文件夹结构显示了需要添加到我们在上一章开始实现的基础项目中的新文件夹和文件,以便使用 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
| --- theme.js
| -- server/
| --- devBundle.js
| -- webpack.config.client.js
| -- webpack.config.client.production.js
client文件夹将包含 React 组件、辅助工具和前端资产,如图片和 CSS。除了这个文件夹和用于编译和打包客户端代码的 Webpack 配置文件之外,我们还将修改一些其他现有的文件,以完成本章中完整骨架应用程序的集成。
在我们开始实现具体的前端功能之前,我们需要通过安装必要的模块并添加编译、打包和加载 React 视图的配置来为 React 开发做好准备。我们将在下一节中介绍这些设置步骤。
设置 React 开发环境
在我们可以在现有的骨架代码库中使用 React 进行开发之前,我们需要添加配置以编译和打包前端代码,添加构建交互式界面所需的 React 相关依赖项,并在 MERN 开发流程中将这一切结合起来。
为了实现这一点,我们将添加前端配置以编译、打包和热重载代码的 Babel、Webpack 和 React Hot Loader。接下来,我们将修改服务器代码,以便在一条命令中启动前端和后端的代码打包,使开发流程简单化。然后,我们将进一步更新代码,以便在应用程序在浏览器中运行时从服务器提供打包后的代码。最后,我们将通过安装启动前端实现所需的 React 依赖项来完成设置。
配置 Babel 和 Webpack
在代码可以在浏览器中运行之前,我们需要编译和捆绑我们将编写的 React 代码以实现前端。为了在开发期间运行并捆绑客户端代码,以及为生产捆绑,我们将更新 Babel 和 Webpack 的配置。然后,我们将配置 Express 应用程序以一个命令来启动前端和后端代码捆绑,这样在开发期间只需启动服务器就可以使整个堆栈准备好运行和测试。
Babel
要编译 React,首先,通过在命令行中运行以下命令将 Babel React 预设模块作为开发依赖项安装:
yarn add --dev @babel/preset-react
然后,使用以下代码更新.babelrc。这将包括模块,并配置react-hot-loader Babel 插件,这是react-hot-loader模块所需的。
mern-skeleton/.babelrc:
{
"presets": [
["@babel/preset-env",
{
"targets": {
"node": "current"
}
}
],
"@babel/preset-react"
],
"plugins": [
"react-hot-loader/babel"
]
}
要使用此更新的 Babel 配置,我们需要更新 Webpack 配置,我们将在下一节中查看。
Webpack
在使用 Babel 编译客户端代码后捆绑它,以及启用react-hot-loader以加快开发速度,通过在命令行中运行以下命令安装以下模块:
yarn add -dev webpack-dev-middleware webpack-hot-middleware file-loader
yarn add react-hot-loader @hot-loader/react-dom
然后,为了配置 Webpack 以进行前端开发并构建生产包,我们将添加一个webpack.config.client.js文件和一个webpack.config.client.production.js文件,其中包含我们在第二章中描述的相同配置代码,准备开发环境。
在配置好 Webpack 并准备好捆绑前端 React 代码后,接下来,我们将添加一些可以在我们的开发流程中使用的代码。这将使全栈开发过程无缝。
加载开发 Webpack 中间件
在开发期间,当我们运行服务器时,Express 应用程序还应加载与前端相关的 Webpack 中间件,这与为客户端代码设置的配置相匹配,以便前端和后端开发工作流程集成。为此,我们将使用我们在第二章中讨论的devBundle.js文件,准备开发环境,以设置一个接受 Express 应用程序并配置它使用 Webpack 中间件的compile方法。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
}
在此方法中,Webpack 中间件使用在webpack.config.client.js中设置的值,并使用 Webpack Hot Middleware 从服务器端启用热重载。
最后,我们需要在express.js中导入并调用这个compile方法,通过添加以下突出显示的行,但仅在开发期间。
mern-skeleton/server/express.js:
import devBundle from './devBundle'
const app = express()
devBundle.compile(app)
这两条加粗的行仅用于开发模式,在构建生产代码时应将其注释掉。当 Express 应用以开发模式运行时,添加此代码将导入中间件以及客户端 Webpack 配置。然后,它将启动 Webpack 来编译和打包客户端代码,并启用热重载。
打包的代码将被放置在dist文件夹中。这些代码将用于渲染视图。接下来,我们将配置 Express 服务器应用,使其从dist文件夹提供静态文件。这将确保打包的 React 代码可以在浏览器中加载。
加载打包的前端代码
我们将在浏览器中看到的渲染的前端视图将加载自dist文件夹中的打包文件。为了将这些打包文件添加到包含我们前端代码的 HTML 视图中,我们需要配置 Express 应用,使其提供静态文件,这些文件不是由服务器端代码动态生成的。
使用 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')))
在此配置到位后,当 Express 应用接收到以/dist开头的路由请求时,它将知道在返回资源之前在dist文件夹中查找请求的静态资源。现在,我们可以在前端加载dist文件夹中的打包文件。
更新模板以加载打包的脚本
为了将打包的前端代码添加到 HTML 中以便渲染我们的 React 前端,我们将更新template.js文件,使其将dist文件夹中的脚本文件添加到<body>标签的末尾。
mern-skeleton/template.js:
...
<body>
<div id="root"></div>
<script type="text/javascript" src="img/bundle.js"></script>
</body>
当我们在服务器运行时访问根 URL '/' 时,此脚本标签将在浏览器中加载我们的 React 前端代码。我们已经准备好看到这一效果,并可以开始安装将添加 React 视图的依赖项。
添加 React 依赖项
我们骨架应用的前端视图将主要使用 React 来实现。此外,为了启用客户端路由,我们将使用 React Router,并且为了通过简洁的外观和感觉提升用户体验,我们将使用 Material-UI。为了添加这些库,我们将在本节中安装以下模块:
-
核心 React 模块:
react和react-dom -
React Router 模块:
react-router和react-router-dom -
Material-UI 模块:
@material-ui/core和@material-ui/icons
React
在整本书中,我们将使用 React 来编写前端代码。为了开始编写 React 组件代码,我们需要将以下模块作为常规依赖项安装:
yarn add react react-dom
这些是实施基于 React 的 Web 前端所必需的核心 React 库模块。通过添加其他附加模块,我们将在 React 之上添加更多功能。
React Router
React Router 提供了一组导航组件,这些组件使 React 应用程序在前端实现路由功能。我们将添加以下 React Router 模块:
yarn add react-router react-router-dom
这些模块将使我们能够利用声明式路由,并在前端拥有可书签的 URL 路由。
Material-UI
为了保持我们的 MERN 应用程序的 UI 简洁,而不深入 UI 设计和实现,我们将利用 Material-UI 库。它提供了现成可使用且可定制的 React 组件,实现了谷歌的材质设计。为了开始使用 Material-UI 组件来构建前端,我们需要安装以下模块:
yarn add @material-ui/core @material-ui/icons
在撰写本文时,Material-UI 的最新版本是4.9.8。建议您安装此确切版本,以确保示例项目的代码不会出错。
为了添加 Material-UI 推荐的Roboto字体并使用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 组件,从主页开始。这个页面应该作为完整应用程序的第一个视图加载。
渲染主页视图
为了展示如何实现这个 MERN 框架的前端功能,我们将首先详细说明如何在应用程序的根路由上渲染一个简单的首页,然后再介绍后端 API 集成、用户认证集成,以及在本章的其余部分实现其他视图组件。
在根路由上实现和渲染一个工作的Home组件的过程,也将揭示前端代码骨架的基本结构。我们将从包含整个 React 应用的顶级入口组件开始,该组件渲染主路由组件,它将应用程序中的所有 React 组件链接起来。
在接下来的章节中,我们将开始实施 React 前端。首先,我们将添加根 React 组件,该组件与 React Router 和 Material-UI 集成,并配置为热重载。我们还将学习如何自定义 Material-UI 主题,并使主题对所有组件可用。最后,我们将实现并加载代表主页的 React 组件,从而展示如何在应用程序中添加和渲染 React 视图。
入口点在 main.js
客户端文件夹中的client/main.js文件将是渲染完整 React 应用程序的入口点,如客户端 Webpack 配置对象中已指示的那样。在client/main.js中,我们导入包含整个前端的最顶层或最高级 React 组件,并将其渲染到 HTML 文档中template.js指定的具有'root' ID 的div元素中。
mern-skeleton/client/main.js:
import React from 'react'
import { render } from 'react-dom'
import App from './App'
render(<App/>, document.getElementById('root'))
在这里,最高级的根 React 组件是App组件,它正在 HTML 中被渲染。App组件在client/App.js中定义,如下一小节所述。
根 React 组件
将包含应用程序前端所有组件的最高级 React 组件定义在client/App.js文件中。在这个文件中,我们配置 React 应用程序,使其使用定制的 Material-UI 主题渲染视图组件,启用前端路由,并确保 React Hot Loader 可以在我们开发组件时即时加载更改。
在以下章节中,我们将添加代码来自定义主题,使此主题和 React Router 功能可供我们的 React 组件使用,并配置用于热加载的根组件。
自定义 Material-UI 主题
可以使用ThemeProvider组件轻松地自定义 Material-UI 主题。它还可以用于在createMuiTheme()中配置主题变量的自定义值。我们将使用createMuiTheme在client/theme.js中为骨架应用程序定义一个自定义主题,然后将其导出,以便在App组件中使用。
mern-skeleton/client/theme.js:
import { createMuiTheme } from '@material-ui/core/styles'
import { pink } from '@material-ui/core/colors'
const theme = createMuiTheme({
typography: {
useNextVariants: true,
},
palette: {
primary: {
light: '#5c67a3',
main: '#3f4771',
dark: '#2e355b',
contrastText: '#fff',
},
secondary: {
light: '#ff79b0',
main: '#ff4081',
dark: '#c60055',
contrastText: '#000',
},
openTitle: '#3f4771',
protectedTitle: pink['400'],
type: 'light'
}
})
export default theme
对于骨架,我们只通过设置一些用于 UI 的颜色值进行最小化定制。这里生成的主题变量将被传递到,并在我们构建的所有组件中使用。
使用 ThemeProvider 和 BrowserRouter 包装根组件
我们将创建以构成用户界面的自定义 React 组件将通过MainRouter组件中指定的前端路由来访问。本质上,此组件包含了为应用程序开发的所有自定义视图,需要提供主题值和路由功能。此组件将是根App组件中的核心组件,该组件在以下代码中定义。
mern-skeleton/client/App.js:
import React from 'react'
import MainRouter from './MainRouter'
import {BrowserRouter} from 'react-router-dom'
import { ThemeProvider } from '@material-ui/styles'
import theme from './theme'
const App = () => {
return (
<BrowserRouter>
<ThemeProvider theme={theme}>
<MainRouter/>
</ThemeProvider>
</BrowserRouter>
)}
在App.js中定义此根组件时,我们将MainRouter组件包裹在ThemeProvider中,使其能够访问 Material-UI 主题,以及BrowserRouter,它通过 React Router 启用前端路由。我们之前定义的自定义主题变量作为属性传递给ThemeProvider,使主题可在所有自定义 React 组件中使用。最后,在App.js文件中,我们需要导出此App组件,以便可以在main.js中导入和使用。
标记根组件为热导出
App.js中的最后一行代码,用于导出App组件,使用了react-hot-loader的高阶组件(HOC)hot模块来标记根组件为hot。
mern-skeleton/client/App.js:
import { hot } from 'react-hot-loader'
const App = () => { ... }
export default hot(module)(App)
以这种方式标记App组件为hot实际上在开发期间启用了我们的 React 组件的实时重新加载。
在这一点之后,对于我们的 MERN 应用,我们不需要太多地更改main.js和App.js代码,我们可以通过将新组件注入到MainRouter组件中继续构建 React 应用的其余部分,这正是我们将在下一节中做的。
向 MainRouter 添加主页路由
MainRouter.js代码将帮助我们根据应用中的路由或位置渲染自定义 React 组件。在这个第一个版本中,我们只会添加根路由以渲染Home组件。
mern-skeleton/client/MainRouter.js:
import React from 'react'
import {Route, Switch} from 'react-router-dom'
import Home from './core/Home'
const MainRouter = () => {
return ( <div>
<Switch>
<Route exact path="/" component={Home}/>
</Switch>
</div>
)
}
export default MainRouter
随着我们开发更多的视图组件,我们将更新MainRouter并在Switch组件内添加新组件的路由。
React Router 中的Switch组件渲染一个特定的路由。换句话说,它只渲染与请求的路由路径匹配的第一个子组件。另一方面,如果没有嵌套在Switch中,当有路径匹配时,每个Route组件都会全面渲染;例如,对'/'的请求也匹配'/contact'路由。
我们在MainRouter中添加此路由的Home组件需要被定义和导出,我们将在下一节中这样做。
Home组件
Home组件将是包含骨架应用主页视图的 React 组件。当用户访问根路由时,它将在浏览器中渲染,我们将使用 Material-UI 组件来组合它。
以下截图显示了Home组件,以及将在本章后面作为单独组件实现的Menu组件,该组件将提供跨应用导航:
将在浏览器中渲染并供用户交互的Home组件和其他视图组件将遵循一个常见的代码结构,包含以下部分(按顺序):
-
导入构建组件所需的库、模块和文件
-
样式声明,用于定义组件元素的特定 CSS 样式
-
定义 React 组件的函数
在本书中,随着我们开发代表前端视图的新 React 组件,我们将主要关注 React 组件定义部分。但为了我们的第一次实现,我们将详细阐述所有这些部分以介绍必要的结构。
导入
对于每个 React 组件实现,我们需要导入实现代码中使用的库、模块和文件。组件文件将首先从 React、Material-UI、React Router 模块、图片、CSS、API 获取以及我们代码中的认证助手导入,具体取决于特定组件的需求。例如,对于Home.js中的Home组件代码,我们使用以下导入。
mern-skeleton/client/core/Home.js:
import React from 'react'
import { makeStyles } from '@material-ui/core/styles'
import Card from '@material-ui/core/Card'
import CardContent from '@material-ui/core/CardContent'
import CardMedia from '@material-ui/core/CardMedia'
import Typography from '@material-ui/core/Typography'
import unicornbikeImg from './../assets/images/unicornbike.jpg'
图片文件保存在client/assets/images/文件夹中,并导入以便将其添加到Home组件中。这些导入将帮助我们构建组件并定义组件中使用的样式。
样式声明
在导入之后,我们将通过利用Material-UI主题变量和makeStyles(这是由Material-UI提供的自定义 React Hook API)来定义所需的 CSS 样式,以通过Material-UI主题变量和makeStyles来设置组件中的元素样式。
Hooks 是 React 的新特性。Hooks 是函数,使得在函数组件中可以使用 React 状态和生命周期特性,而无需编写一个类来定义组件。React 提供了一些内置的 Hooks,但根据需要我们也可以构建自定义 Hooks 以在不同组件间重用有状态的行为。要了解更多关于 React Hooks 的信息,请访问reactjs.org/docs/hooks-…。
对于Home.js中的Home组件,我们有以下样式。
mern-skeleton/client/core/Home.js:
const useStyles = makeStyles(theme => ({
card: {
maxWidth: 600,
margin: 'auto',
marginTop: theme.spacing(5)
},
title: {
padding:`${theme.spacing(3)}px ${theme.spacing(2.5)}px ${theme.spacing(2)}px`,
color: theme.palette.openTitle
},
media: {
minHeight: 400
}
}))
在这里定义的 JSS 样式对象将通过makeStyles Hook 返回的 Hook 注入到组件中。makeStyles Hook API 接受一个函数作为参数,并提供访问我们自定义主题变量的权限,我们可以在定义样式时使用这些变量。
Material-UI 使用 JSS,这是一个用于向组件添加样式的 CSS-in-JS 样式解决方案。JSS 使用 JavaScript 作为描述样式的语言。本书不会详细涵盖 CSS 和样式实现。它将主要依赖于 Material-UI 组件的默认外观和感觉。要了解更多关于 JSS 的信息,请访问cssinjs.org/?v=v9.8.1。有关如何自定义Material-UI组件样式的示例,请查看 Material-UI 文档material-ui.com/。
我们可以使用这些生成的样式来设置组件中的元素样式,如下面的Home组件定义所示。
组件定义
在编写定义组件的函数时,我们将组合组件的内容和行为。Home组件将包含一个带有标题、图片和说明的 Material-UI Card,所有这些都将使用我们之前定义并调用useStyles() Hook 返回的样式进行样式化。
mern-skeleton/client/core/Home.js:
export default function Home(){
const classes = useStyles()
return (
<Card className={classes.card}>
<Typography variant="h6" className={classes.title}>
Home Page
</Typography>
<CardMedia className={classes.media}
image={unicornbikeImg} title="Unicorn Bicycle"/>
<CardContent>
<Typography variant="body2" component="p">
Welcome to the MERN Skeleton home page.
</Typography>
</CardContent>
</Card>
)
}
在前面的代码中,我们定义并导出了一个名为Home的函数组件。现在,这个导出的组件可以在其他组件内部进行组合。正如我们之前讨论的那样,我们已经在MainRouter组件中的一个路由中导入了此Home组件。
在本书的整个过程中,我们将定义所有我们的 React 组件为函数组件。我们将利用 React Hooks,这是 React 的新增功能,来添加状态和生命周期特性,而不是使用类定义来实现相同的功能。
我们将在我们的 MERN 应用程序中实现的其他视图组件将遵循相同的结构。在本书的剩余部分,我们将主要关注组件定义,突出实现组件的独特方面。
我们几乎准备好运行此代码以在前端渲染主页组件。但在那之前,我们需要更新 Webpack 配置,以便我们可以捆绑和显示图像。
捆绑图像资源
我们导入到Home组件视图中的静态图像文件也必须包含在与其他编译 JS 代码捆绑的捆绑包中,以便代码可以访问和加载它。为了启用此功能,我们需要更新 Webpack 配置文件,并添加一个模块规则来加载、捆绑并将图像文件输出到dist目录,该目录包含编译的前端和后端代码。
更新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节点模块,需要将其作为开发依赖项安装,如下所示:
yarn add --dev file-loader
添加了此图像捆绑配置后,当运行应用程序时,主页组件应成功渲染图像。
在浏览器中运行和打开
到目前为止的客户端代码可以运行,这样我们就可以在根 URL 中查看Home组件。要运行应用程序,请使用以下命令:
yarn 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 组件可以使用这些方法根据需要与服务器和数据库交换用户数据。在接下来的章节中,我们将探讨这些方法的实现以及它们如何对应到每个 CRUD 端点。
创建用户
create方法将从视图组件获取用户数据,我们将在那里调用此方法。然后,它将使用fetch在创建 API 路由'/api/users'上发起一个POST调用,以在后端使用提供的数据创建一个新用户。
mern-skeleton/client/user/api-user.js:
const create = async (user) => {
try {
let response = await fetch('/api/users/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
})
return await response.json()
} catch(err) {
console.log(err)
}
}
最后,在这个方法中,我们作为 promise 返回来自服务器的响应。因此,调用此方法的组件可以使用这个 promise 来适当地处理响应,具体取决于从服务器返回的内容。同样,我们将在下一节中实现list方法。
列出用户
list方法将使用fetch发起一个GET调用,以检索数据库中的所有用户,然后作为 promise 将服务器的响应返回给组件。
mern-skeleton/client/user/api-user.js:
const list = async (signal) => {
try {
let response = await fetch('/api/users/', {
method: 'GET',
signal: signal,
})
return await response.json()
} catch(err) {
console.log(err)
}
}
如果返回的 promise 成功解析,将给组件提供一个包含从数据库检索到的用户对象的数组。在单个用户读取的情况下,我们将处理单个用户对象,如下所示。
阅读用户资料
read方法将使用fetch发起一个GET调用,通过 ID 检索特定用户。由于这是一个受保护的路由,除了传递用户 ID 作为参数外,请求的组件还必须提供有效的凭据,在这种情况下,将是一个在成功登录后收到的有效 JWT。
mern-skeleton/client/user/api-user.js:
const read = async (params, credentials, signal) => {
try {
let response = await fetch('/api/users/' + params.userId, {
method: 'GET',
signal: signal,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
}
})
return await response.json()
} catch(err) {
console.log(err)
}
}
JWT 通过Bearer方案附加到GET fetch 调用中的Authorization头,然后服务器响应作为 promise 返回给组件。当这个 promise 解析时,它将要么给组件提供特定用户的用户详情,要么通知访问权限仅限于认证用户。同样,更新的用户 API 方法也需要为 fetch 调用传递有效的 JWT 凭据,如下一节所示。
更新用户数据
update方法将获取特定用户的更改后的用户数据,然后使用fetch发起一个PUT调用,以更新后端中现有的用户。这也是一个受保护的路由,将需要有效的 JWT 作为凭据。
mern-skeleton/client/user/api-user.js:
const update = async (params, credentials, user) => {
try {
let response = await fetch('/api/users/' + params.userId, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: JSON.stringify(user)
})
return await response.json()
} catch(err) {
console.log(err)
}
}
正如我们看到的其他 fetch 调用一样,此方法也将返回一个包含服务器对用户更新请求响应的承诺。在最后一个方法中,我们将学习如何调用用户删除 API。
删除用户
remove 方法将允许视图组件从数据库中删除特定用户,并使用 fetch 发起一个 DELETE 请求。这同样是一个受保护的路线,需要有效的 JWT 作为凭证,类似于 read 和 update 方法。
mern-skeleton/client/user/api-user.js:
const remove = async (params, credentials) => {
try {
let response = await fetch('/api/users/' + params.userId, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
}
})
return await response.json()
} catch(err) {
console.log(err)
}
}
服务器对删除请求的响应将以承诺的形式返回到组件中,就像其他方法一样。
在这五个辅助方法中,我们已经涵盖了所有与用户 CRUD 相关的后端实现的 API 端点调用。最后,我们可以如下从 api-user.js 文件导出这些方法。
mern-skeleton/client/user/api-user.js:
export { create, list, read, update, remove }
这些用户 CRUD 方法现在可以根据需要由 React 组件导入和使用。接下来,我们将实现类似的辅助方法以集成与认证相关的 API 端点。
为认证 API 获取
为了将服务器端的认证 API 端点与前端 React 组件集成,我们将在 client/auth/api-auth.js 文件中添加获取登录和注销 API 端点的方法。让我们来看看它们。
登录
signin 方法将从视图组件获取用户登录数据,然后使用 fetch 发起一个 POST 请求以验证用户与后端。
mern-skeleton/client/auth/api-auth.js:
const signin = async (user) => {
try {
let response = await fetch('/auth/signin/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(user)
})
return await response.json()
} catch(err) {
console.log(err)
}
}
服务器响应将以承诺的形式返回到组件中,如果登录成功,可能会提供 JWT。调用此方法的组件需要适当地处理响应,例如将接收到的 JWT 本地存储,以便在从前端调用其他受保护 API 路由时使用。我们将在本章后面实现 登录 视图时查看此实现。
用户成功登录后,我们还想在用户注销时调用注销 API。接下来将讨论注销 API 的调用。
注销
我们将在 api-auth.js 中添加一个 signout 方法,它将使用 fetch 发起一个 GET 请求到服务器的注销 API 端点。
mern-skeleton/client/auth/api-auth.js:
const signout = async () => {
try {
let response = await fetch('/auth/signout/', { method: 'GET' })
return await response.json()
} catch(err) {
console.log(err)
}
}
此方法也将返回一个承诺,以通知组件 API 请求是否成功。
在 api-auth.js 文件的末尾,我们将导出 signin 和 signout 方法。
mern-skeleton/client/auth/api-auth.js:
export { signin, signout }
现在,这些方法可以被导入到相关的 React 组件中,以便我们可以实现用户登录和注销功能。
在添加了这些 API 获取方法之后,React 前端可以完全访问我们在后端提供的端点。在我们开始在 React 组件中使用这些方法之前,我们将探讨如何在前端维护用户认证状态。
在前端添加认证
正如我们在上一章中讨论的,使用 JWT 实现认证将管理用户认证状态的责任转移给了客户端。为此,我们需要编写代码,允许客户端在成功登录后存储从服务器接收到的 JWT,在访问受保护的路由时使其可用,当用户登出时删除或使令牌无效,并且根据用户认证状态限制对视图和组件的前端访问。
使用 React Router 文档中的认证工作流程示例,在以下章节中,我们将编写辅助方法来管理组件间的认证状态,并使用自定义的 PrivateRoute 组件将受保护的路由添加到 MERN 骨架应用程序的前端。
管理认证状态
为了管理应用程序前端中的认证状态,前端需要能够存储、检索和删除在用户成功登录时从服务器接收到的认证凭据。在我们的 MERN 应用程序中,我们将使用浏览器的 sessionStorage 作为存储选项来存储 JWT 认证凭据。
或者,您可以使用 localStorage 而不是 sessionStorage 来存储 JWT 凭据。使用 sessionStorage,用户认证状态将仅在当前窗口标签中记住。使用 localStorage,用户认证状态将在浏览器的标签间记住。
在 client/auth/auth-helper.js 中,我们将定义以下章节中讨论的辅助方法,以从客户端的 sessionStorage 中存储和检索 JWT 凭据,并在用户登出时清除 sessionStorage。
保存凭据
为了在成功登录后保存从服务器接收到的 JWT 凭据,我们使用 authenticate 方法,该方法定义如下。
mern-skeleton/client/auth/auth-helper.js:
authenticate(jwt, cb) {
if(typeof window !== "undefined")
sessionStorage.setItem('jwt', JSON.stringify(jwt))
cb()
}
authenticate 方法接受 JWT 凭据 jwt 和一个回调函数 cb 作为参数。在确保 window 已定义后,即在确保此代码在浏览器中运行并因此可以访问 sessionStorage 后,它将凭据存储在 sessionStorage 中。然后,它执行传入的回调函数。此回调将允许组件——在我们的例子中,是调用登录的组件——定义在成功登录并存储凭据后应执行的操作。接下来,我们将讨论让我们访问这些存储凭据的方法。
检索凭据
在我们的前端组件中,我们需要检索存储的凭据以检查当前用户是否已登录。在 isAuthenticated() 方法中,我们可以从 sessionStorage 中检索这些凭据。
mern-skeleton/client/auth/auth-helper.js:
isAuthenticated() {
if (typeof window == "undefined")
return false
if (sessionStorage.getItem('jwt'))
return JSON.parse(sessionStorage.getItem('jwt'))
else
return false
}
对isAuthenticated()的调用将返回存储的凭据或false,这取决于是否在sessionStorage中找到了凭据。在存储中找到凭据意味着用户已登录,而没有找到凭据则意味着用户未登录。我们还将添加一个方法,允许我们在登录用户从应用程序注销时从存储中删除凭据。
删除凭据
当用户成功从应用程序注销时,我们希望从sessionStorage中清除存储的 JWT 凭据。这可以通过调用以下代码中定义的clearJWT方法来实现。
mern-skeleton/client/auth/auth-helper.js:
clearJWT(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=/;"
})
}
这个clearJWT方法接受一个回调函数作为参数,并从sessionStorage中移除 JWT 凭据。传入的cb()函数允许启动signout功能的组件决定在成功注销后应该发生什么。
clearJWT方法还使用了我们在api-auth.js中定义的signout方法来调用后端中的注销 API。如果我们使用cookies而不是sessionStorage来存储凭据,那么对这个 API 调用的响应将是我们清除 cookie 的地方,如前面的代码所示。使用注销 API 调用是可选的,因为这取决于是否使用 cookie 作为凭据存储机制。
使用这三个方法,我们现在有了在客户端存储、检索和删除 JWT 凭据的方法。使用这些方法,我们为前端构建的 React 组件将能够检查和管理用户认证状态,以限制前端访问,如下一节中自定义PrivateRoute组件所示。
PrivateRoute组件
文件中的代码定义了PrivateRoute组件,如reacttraining.com/react-router/web/example/auth-workflow中的认证流程示例所示,该示例可在 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中要渲染的组件只有在用户认证时才会加载,这是通过调用isAuthenticated方法确定的;否则,用户将被重定向到Signin组件。我们将在PrivateRoute中加载应有限制访问的组件,例如用户配置文件组件,这将确保只有认证用户才能查看用户配置文件页面。
在将后端 API 集成并准备好在组件中使用认证管理辅助方法后,我们现在可以开始构建剩余的视图组件,这些组件利用这些方法并完成前端。
完成用户前端
本节中将要描述的 React 组件通过允许用户根据认证限制查看、创建和修改存储在数据库中的用户数据,从而完成我们为骨架定义的交互式功能。我们将实现的组件如下:
-
Users: 从数据库获取并显示所有用户 -
Signup: 显示一个表单,允许新用户注册 -
Signin: 显示一个表单,允许现有用户登录 -
Profile: 在从数据库检索后显示特定用户的详细信息 -
EditProfile: 显示特定用户的详细信息,并允许授权用户更新这些信息 -
DeleteUser: 允许授权用户从应用程序中删除他们的账户 -
Menu: 为应用程序中的每个视图添加一个通用的导航栏
对于这些组件中的每一个,我们将讨论它们的独特之处,以及如何在MainRouter中添加它们。
用户组件
client/user/Users.js中的Users组件显示了从数据库中检索到的所有用户的名称,并将每个名称链接到用户个人资料。以下组件可以被应用程序的任何访客查看,并在'/users'路由上渲染:
在组件定义中,类似于我们实现Home组件的方式,我们定义并导出一个函数组件。在这个组件中,我们首先使用一个空的用户数组初始化状态。
mern-skeleton/client/user/Users.js:
export default function Users() {
...
const [users, setUsers] = useState([])
...
}
我们使用内置的 React 钩子useState给这个函数组件添加状态。通过调用这个钩子,我们实际上声明了一个名为users的状态变量,可以通过调用setUsers来更新它,并将users的初始值设置为[]。
使用内置的useState钩子允许我们在 React 中给函数组件添加状态行为。调用它将声明一个状态变量,类似于在类组件定义中使用this.state。传递给useState的参数是这个变量的初始值——换句话说,初始状态。调用useState返回当前状态和一个更新状态值的函数,这类似于类定义中的this.setState。
在初始化users状态后,接下来我们将使用另一个内置的 React 钩子useEffect从后端获取用户列表并更新状态中的users值。
useEffect钩子用于替代我们本应在 React 类中使用的componentDidMount、componentDidUpdate和componentWillUnmount生命周期方法。在函数组件中使用此钩子允许我们执行副作用,例如从后端获取数据。默认情况下,React 在每次渲染后(包括第一次渲染)都会运行使用useEffect定义的效果。但我们可以指示效果仅在状态发生变化时重新运行。可选地,我们还可以定义在效果之后如何清理,例如,在组件卸载时执行取消 fetch 信号等操作,以避免内存泄漏。
在我们的Users组件中,我们使用useEffect来调用api-user.js辅助方法中的list方法。这将从后端获取用户列表,并通过更新状态将用户数据加载到组件中。
mern-skeleton/client/user/Users.js:
useEffect(() => {
const abortController = new AbortController()
const signal = abortController.signal
list(signal).then((data) => {
if (data && data.error) {
console.log(data.error)
} else {
setUsers(data)
}
})
return function cleanup(){
abortController.abort()
}
}, [])
在此效果中,我们还添加了一个清理函数,在组件卸载时取消 fetch 调用。为了将信号与 fetch 调用关联起来,我们使用 AbortController Web API,这允许我们根据需要取消 DOM 请求。
在此useEffect钩子的第二个参数中,我们传递一个空数组,以便此效果清理只在组件挂载和卸载时运行一次,而不是在每次渲染后。
最后,在Users函数组件的返回值中,我们添加实际视图内容。视图由 Material-UI 组件组成,如Paper、List和ListItem。这些元素使用makeStyles钩子定义和提供的 CSS 进行样式化,与Home组件中的方式相同。
mern-skeleton/client/user/Users.js:
return (
<Paper className={classes.root} elevation={4}>
<Typography variant="h6" className={classes.title}>
All Users
</Typography>
<List dense>
{users.map((item, i) => {
return <Link to={"/user/" + item._id} key={i}>
<ListItem button>
<ListItemAvatar>
<Avatar>
<Person/>
</Avatar>
</ListItemAvatar>
<ListItemText primary={item.name}/>
<ListItemSecondaryAction>
<IconButton>
<ArrowForward/>
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</Link>
})
}
</List>
</Paper>
)
在这种观点下,为了生成每个列表项,我们使用map函数遍历状态中的用户数组。每个列表项都会渲染从用户数组中访问的每个项目的单个用户名称。
要将此Users组件添加到 React 应用程序中,我们需要更新MainRouter组件,添加一个在'/users'路径上渲染此组件的Route。在Home路由之后,将Route添加到Switch组件内部。
mern-skeleton/client/MainRouter.js:
<Route path="/users" component={Users}/>
要在浏览器中看到此视图的渲染效果,您可以暂时将Link组件添加到Home组件中,以便能够路由到Users组件:
<Link to="/users">Users</Link>
在浏览器中渲染根路由的首页视图后,点击此链接将显示我们在本节中实现的Users组件。我们将在下一节中类似地实现其他 React 组件,从Signup组件开始。
注册组件
client/user/Signup.js中的Signup组件向用户展示一个包含姓名、电子邮件和密码字段的表单,用于在'/signup'路径上注册,如下截图所示:
在组件定义中,我们使用 useState 钩子初始化状态,使用空的输入字段值、空的错误消息,并将对话框打开变量设置为 false。
mern-skeleton/client/user/Signup.js:
export default function Signup() {
...
const [values, setValues] = useState({
name: '',
password: '',
email: '',
open: false,
error: ''
})
...
}
我们还定义了两个处理函数,用于在输入值更改或点击提交按钮时调用。handleChange 函数接受输入字段中输入的新值,并将其设置为状态。
mern-skeleton/client/user/Signup.js:
const handleChange = name => event => {
setValues({ ...values, [name]: event.target.value })
}
当表单提交时,会调用 clickSubmit 函数。该函数从状态中获取输入值,并调用 create fetch 方法在后端注册用户。然后,根据服务器的响应,要么显示错误消息,要么显示成功对话框。
mern-skeleton/client/user/Signup.js:
const clickSubmit = () => {
const user = {
name: values.name || undefined,
email: values.email || undefined,
password: values.password || undefined
}
create(user).then((data) => {
if (data.error) {
setValues({ ...values, error: data.error})
} else {
setValues({ ...values, error: '', open: true})
}
})
}
在 return 函数中,我们使用 Material-UI 的 TextField 等组件组合和样式化注册视图中的表单组件。
mern-skeleton/client/user/Signup.js:
return (
<div>
<Card className={classes.card}>
<CardContent>
<Typography variant="h6" className={classes.title}>
Sign Up
</Typography>
<TextField id="name" label="Name"
className={classes.textField}
value={values.name} onChange={handleChange('name')}
margin="normal"/>
<br/>
<TextField id="email" type="email" label="Email"
className={classes.textField}
value={values.email} onChange={handleChange('email')}
margin="normal"/>
<br/>
<TextField id="password" type="password" label="Password"
className={classes.textField} value={values.password}
onChange={handleChange('password')} margin="normal"/>
<br/>
{
values.error && (<Typography component="p" color="error">
<Icon color="error" className={classes.error}>error</Icon>
{values.error}</Typography>)
}
</CardContent>
<CardActions>
<Button color="primary" variant="contained" onClick={clickSubmit}
className={classes.submit}>Submit</Button>
</CardActions>
</Card>
</div>
)
此返回值还包含一个错误消息块,以及一个根据服务器注册响应条件渲染的 Dialog 组件。如果服务器返回错误,则添加到表单下方的错误块(我们在前面的代码中实现),将在视图中渲染相应的错误消息。如果服务器返回成功响应,则将渲染一个 Dialog 组件。
Signup.js 中的 Dialog 组件如下组成。
mern-skeleton/client/user/Signup.js:
<Dialog open={values.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="contained">
Sign In
</Button>
</Link>
</DialogActions>
</Dialog>
在成功创建账户后,用户会收到确认信息,并被要求使用此 Dialog 组件进行登录,该组件链接到 Signin 组件,如下截图所示:
要将 Signup 组件添加到应用中,请将以下 Route 添加到 Switch 组件中的 MainRouter。
mern-skeleton/client/MainRouter.js:
<Route path="/signup" component={Signup}/>
这将在 '/signup' 路径上渲染 Signup 视图。同样,我们将接下来实现 Signin 组件。
登录组件
client/auth/Signin.js 中的 Signin 组件也是一个仅包含电子邮件和密码字段的登录表单。该组件与 Signup 组件非常相似,将在 '/signin' 路径上渲染。关键区别在于成功登录后的重定向实现以及存储接收到的 JWT 凭证。渲染的 Signin 组件如下截图所示:
对于重定向,我们将使用 React Router 的 Redirect 组件。首先,在状态中将 redirectToReferrer 值初始化为 false,与其他字段一起:
mern-skeleton/client/auth/Signin.js:
export default function Signin(props) {
const [values, setValues] = useState({
email: '',
password: '',
error: '',
redirectToReferrer: false
})
}
Signin函数将接受包含 React Router 变量的 props 作为参数。我们将使用这些变量进行重定向。当用户在提交表单后成功登录,并且收到的 JWT 存储在sessionStorage中时,redirectToReferrer应设置为true。为了存储 JWT 并在之后进行重定向,我们将调用在auth-helper.js中定义的authenticate()方法。这种实现将放在clickSubmit()函数中,以便在表单提交时调用。
mern-skeleton/client/auth/Signin.js:
const clickSubmit = () => {
const user = {
email: values.email || undefined,
password: values.password || undefined
}
signin(user).then((data) => {
if (data.error) {
setValues({ ...values, error: data.error})
} else {
auth.authenticate(data, () => {
setValues({ ...values, error: '',redirectToReferrer: true})
})
}
})
}
重定向将根据redirectToReferrer的值条件性地发生,使用 React Router 中的Redirect组件。我们将在函数的返回块之前添加重定向代码,如下所示。
mern-skeleton/client/auth/Signin.js:
const {from} = props.location.state || {
from: {
pathname: '/'
}
}
const {redirectToReferrer} = values
if (redirectToReferrer) {
return (<Redirect to={from}/>)
}
如果渲染了Redirect组件,它将带应用程序到在 props 中接收到的最后一个位置,或者到根目录的Home组件。
函数返回的代码在此处未显示,因为它与Signup中的代码非常相似。它将包含相同的表单元素,只是email和password字段,一个条件性错误消息,以及submit按钮。
要将Signin组件添加到应用程序中,请将以下 Route 添加到MainRouter中的Switch组件。
mern-skeleton/client/MainRouter.js:
<Route path="/signin" component={Signin}/>
这将在"/signin"处渲染Signin组件,并且可以在 Home 组件中链接,类似于Signup组件,以便在浏览器中查看。接下来,我们将实现个人资料视图以显示单个用户的详细信息。
Profile组件
client/user/Profile.js中的Profile组件在'/user/:userId'路径的视图中显示单个用户的信息,其中userId参数代表特定用户的 ID。完成的Profile将显示用户详细信息,并条件性地显示编辑/删除选项。以下截图显示了当当前浏览的用户正在查看其他用户的个人资料而不是自己的个人资料时,Profile是如何渲染的:
如果用户已登录,则可以从服务器获取此配置文件信息。为了验证这一点,组件必须向read获取调用提供 JWT 凭证;否则,用户应重定向到登录视图。
在Profile组件定义中,我们需要使用空用户初始化状态,并将redirectToSignin设置为false。
mern-skeleton/client/user/Profile.js:
export default function Profile({ match }) {
...
const [user, setUser] = useState({})
const [redirectToSignin, setRedirectToSignin] = useState(false)
...
}
我们还需要获取由Route组件传递的match props 的访问权限,它将包含一个:userId参数值。这可以通过match.params.userId访问。
Profile组件应获取用户信息并使用这些详细信息渲染视图。为了实现这一点,我们将使用useEffect钩子,就像我们在Users组件中所做的那样。
mern-skeleton/client/user/Profile.js:
useEffect(() => {
const abortController = new AbortController()
const signal = abortController.signal
const jwt = auth.isAuthenticated()
read({
userId: match.params.userId
}, {t: jwt.token}, signal).then((data) => {
if (data && data.error) {
setRedirectToSignin(true)
} else {
setUser(data)
}
})
return function cleanup(){
abortController.abort()
}
}, [match.params.userId])
此效果使用match.params.userId值并调用read用户获取方法。由于此方法还需要凭据来授权已登录的用户,因此使用auth-helper.js中的isAuthenticated方法从sessionStorage检索 JWT,并将其传递给read调用。
一旦服务器响应,要么将状态更新为用户信息,要么如果当前用户未认证,将视图重定向到登录视图。我们还在此效果钩子中添加了一个清理函数,以便在组件卸载时中止获取信号。
此效果仅在路由中的userId参数更改时需要重新运行,例如,当应用程序从一个个人资料视图切换到另一个视图时。为了确保在userId值更新时此效果重新运行,我们将在useEffect的第二个参数中添加[match.params.userId]。
如果当前用户未认证,我们将设置条件重定向到登录视图。
mern-skeleton/client/user/Profile.js
if (redirectToSignin) {
return <Redirect to='/signin'/>
}
如果当前登录的用户正在查看另一个用户的个人资料,该函数将返回包含以下元素的Profile视图。
mern-skeleton/client/user/Profile.js:
return (
<Paper className={classes.root} elevation={4}>
<Typography variant="h6" className={classes.title}>
Profile
</Typography>
<List dense>
<ListItem>
<ListItemAvatar>
<Avatar>
<Person/>
</Avatar>
</ListItemAvatar>
<ListItemText primary={user.name} secondary={user.email}/>
</ListItem>
<Divider/>
<ListItem>
<ListItemText primary={"Joined: " + (
new Date(user.created)).toDateString()}/>
</ListItem>
</List>
</Paper>
)
然而,如果当前登录的用户正在查看自己的个人资料,他们将在Profile组件中看到编辑和删除选项,如下面的截图所示:
要实现此功能,在Profile中的第一个ListItem组件中添加一个包含Edit按钮和DeleteUser组件的ListItemSecondaryAction组件,该组件将根据当前用户是否查看自己的个人资料有条件地渲染。
mern-skeleton/client/user/Profile.js:
{ auth.isAuthenticated().user && auth.isAuthenticated().user._id == user._id &&
(<ListItemSecondaryAction>
<Link to={"/user/edit/" + user._id}>
<IconButton aria-label="Edit" color="primary">
<Edit/>
</IconButton>
</Link>
<DeleteUser userId={user._id}/>
</ListItemSecondaryAction>)
}
Edit按钮将路由到EditProfile组件,而自定义的DeleteUser组件将处理删除操作,并将userId作为 prop 传递给它。
要将Profile组件添加到应用程序中,请将Route添加到Switch组件中的MainRouter。
mern-skeleton/client/MainRouter.js:
<Route path="/user/:userId" component={Profile}/>
要在浏览器中访问此路由并渲染包含用户详情的Profile,链接中应包含有效的用户 ID。在下一节中,我们将使用相同的方法检索单个用户详情并在组件中渲染它来实现编辑个人资料视图。
EditProfile 组件
client/user/EditProfile.js中的EditProfile组件在其实现上与Signup和Profile组件有相似之处。它允许授权用户以与注册表单类似的形式编辑自己的个人资料信息,如下面的截图所示:
在'/user/edit/:userId'加载时,组件将在验证 JWT 进行身份验证后,使用其 ID 获取用户信息,然后加载带有接收到的用户信息的表单。表单将允许用户编辑并提交仅更改的信息到update获取调用,并且在更新成功后,将用户重定向到带有更新信息的Profile视图。
EditProfile将以与Profile组件相同的方式加载数据,即通过在useEffect中使用match.params中的userId参数进行read获取。它将从auth.isAuthenticated中收集凭证。表单视图将包含与Signup组件相同的元素,当输入值发生变化时,它们将在状态中更新。
在表单提交时,组件将调用带有userId、JWT 和更新后的用户数据的update获取方法。
mern-skeleton/client/user/EditProfile.js:
const clickSubmit = () => {
const jwt = auth.isAuthenticated()
const user = {
name: values.name || undefined,
email: values.email || undefined,
password: values.password || undefined
}
update({
userId: match.params.userId
}, {
t: jwt.token
}, user).then((data) => {
if (data && data.error) {
setValues({...values, error: data.error})
} else {
setValues({...values, userId: data._id, redirectToProfile: true})
}
})
}
根据服务器的响应,用户将看到错误消息或使用Redirect组件重定向到更新后的个人资料页面,如下所示。
mern-skeleton/client/user/EditProfile.js:
if (values.redirectToProfile) {
return (<Redirect to={'/user/' + values.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路由混淆。
在添加了此个人资料编辑视图后,我们只剩下用户删除 UI 实现需要完成用户相关的前端。
DeleteUser 组件
client/user/DeleteUser.js中的DeleteUser组件基本上是一个按钮,我们将将其添加到个人资料视图中,当点击时,将打开一个Dialog组件,提示用户确认delete操作,如下面的截图所示:
此组件将Dialog组件的open状态初始化为false,以及将redirect设置为false,以便它不会首先渲染。
mern-skeleton/client/user/DeleteUser.js:
export default function DeleteUser(props) {
...
const [open, setOpen] = useState(false)
const [redirect, setRedirect] = useState(false)
...
}
DeleteUser组件也将从父组件接收属性。在这种情况下,属性将包含从Profile组件发送的userId。
接下来,我们需要一些处理方法来打开和关闭dialog按钮。当用户点击delete按钮时,对话框将被打开。
mern-skeleton/client/user/DeleteUser.js:
const clickButton = () => {
setOpen(true)
}
当用户在对话框中点击cancel时,对话框将被关闭。
mern-skeleton/client/user/DeleteUser.js:
const handleRequestClose = () => {
setOpen(false)
}
组件将能够访问从 Profile 组件作为属性传递的 userId,这是调用 remove 获取方法所需的,以及用户在对话框中确认删除操作后的 JWT 凭据。
mern-skeleton/client/user/DeleteUser.js:
const deleteAccount = () => {
const jwt = auth.isAuthenticated()
remove({
userId: props.userId
}, {t: jwt.token}).then((data) => {
if (data && data.error) {
console.log(data.error)
} else {
auth.clearJWT(() => console.log('deleted'))
setRedirect(true)
}
})
}
在确认后,deleteAccount 函数调用带有从属性中获取的 userId 和来自 isAuthenticated 的 JWT 的 remove 获取方法。在删除成功后,用户将被注销并重定向到主页视图。React Router 的 Redirect 组件用于将当前用户重定向到主页视图,如下所示:
if (redirect) {
return <Redirect to='/'/>
}
组件函数返回 DeleteUser 组件元素,包括一个 DeleteIcon 按钮和确认 Dialog。
mern-skeleton/client/user/DeleteUser.js:
return (<span>
<IconButton aria-label="Delete"
onClick={clickButton} color="secondary">
<DeleteIcon/>
</IconButton>
<Dialog open={open} onClose={handleRequestClose}>
<DialogTitle>{"Delete Account"}</DialogTitle>
<DialogContent>
<DialogContentText>
Confirm to delete your account.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleRequestClose} color="primary">
Cancel
</Button>
<Button onClick={deleteAccount}
color="secondary" autoFocus="autoFocus">
Confirm
</Button>
</DialogActions>
</Dialog>
</span>)
DeleteUser 接收 userId 作为属性,用于在 delete 获取调用中使用,因此我们需要为这个 React 组件添加一个必需的属性验证检查。我们将在下一步进行此操作。
使用 PropTypes 验证属性
为了验证将 userId 作为属性注入到组件中的必要性,我们将在定义的组件中添加 PropTypes 验证器。
mern-skeleton/client/user/DeleteUser.js:
DeleteUser.propTypes = {
userId: PropTypes.string.isRequired
}
由于我们在 Profile 组件中使用了 DeleteUser 组件,因此当在 MainRouter 中添加 Profile 时,它会被添加到应用程序视图中。
在添加了删除用户界面之后,我们现在有一个包含所有 React 组件视图的前端,以便完成骨架应用程序的功能。但是,我们仍然需要一个公共导航 UI 来将这些视图连接起来,并使前端用户能够轻松访问每个视图。在下一节中,我们将实现这个导航菜单组件。
菜单组件
Menu 组件将通过提供对所有可用视图的链接以及指示用户在应用程序中的当前位置,作为前端应用程序中的导航栏工作。
为了实现这些导航栏功能,我们将使用 React Router 的 HOC withRouter 来获取对 history 对象属性的访问权限。以下 Menu 组件中的代码仅添加了标题、链接到根路由的“主页”图标以及链接到 '/users' 路由的“用户”按钮。
mern-skeleton/client/core/Menu.js:
const Menu = withRouter(({history}) => (
<AppBar position="static">
<Toolbar>
<Typography variant="h6" 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>))
为了在 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")}
根据用户是否已登录,以下链接(如“登录”、“注册”、“我的资料”和“注销”)将显示在 Menu 中。以下截图显示了用户未登录时菜单的渲染方式:
例如,只有当用户未登录时,注册和登录的链接才应出现在菜单中。因此,我们需要在Users按钮之后添加它到Menu组件,并使用条件。
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.clearJWT(() => history.push('/')) }}>
Sign out
</Button>
</span>)
}
例如,MY PROFILE 按钮使用已登录用户的信息链接到用户的个人资料,而 SIGN OUT 按钮在点击时调用auth.clearJWT()方法。当用户已登录时,Menu将如下所示:
要使Menu导航栏在所有视图中都显示,我们需要在所有其他路由之前,并在Switch组件外部将其添加到MainRouter。
mern-skeleton/client/MainRouter.js:
<Menu/>
<Switch>
…
</Switch>
这将使Menu组件在访问相应路由时渲染在其他所有组件之上。
前端骨架现在已完整,并包含所有必要的组件,允许用户在考虑身份验证和授权限制的情况下,在后台注册、查看和修改用户数据。然而,目前还不能直接在浏览器地址栏中访问前端路由;这些路由只能在前端视图中链接时访问。为了在骨架应用程序中启用此功能,我们需要实现基本的服务器端渲染。
实现基本的服务器端渲染
目前,当 React Router 路由或路径名直接输入到浏览器地址栏中,或者当非根路径的视图刷新时,URL 不起作用。这是因为服务器无法识别我们在前端定义的 React Router 路由。我们必须在后台实现基本的服务器端渲染,以便服务器在接收到对前端路由的请求时能够响应。
为了在服务器接收到对前端路由的请求时正确渲染相关 React 组件,我们需要在客户端 JS 准备好接管渲染之前,首先在服务器端根据 React Router 和 Material-UI 组件生成 React 组件。
服务器端渲染 React 应用程序的基本思想是使用react-dom中的renderToString方法将根 React 组件转换为标记字符串。然后,我们可以将其附加到服务器在接收到请求时渲染的模板上。
在express.js中,我们将替换响应'/'的GET请求返回template.js的代码,用接收任何传入的 GET 请求时生成一些服务器端渲染的标记和相应 React 组件树的 CSS 的代码替换。此更新后的代码将实现以下功能:
app.get('*', (req, res) => {
// 1\. Generate CSS styles using Material-UI's ServerStyleSheets
// 2\. Use renderToString to generate markup which renders
components specific to the route requested
// 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'
- 路由模块:
StaticRouter是一个无状态的路由器,它接受请求的 URL 以匹配在MainRouter组件中声明的前端路由。MainRouter是我们前端中的根组件。
import StaticRouter from 'react-router-dom/StaticRouter'
import MainRouter from './../client/MainRouter'
- Material-UI 模块和自定义主题:以下模块将帮助根据前端使用的样式和 Material-UI 主题生成前端组件的 CSS 样式:
import { ServerStyleSheets, ThemeProvider } from '@material-ui/styles'
import theme from './../client/theme'
使用这些模块,我们可以准备、生成并返回服务器端渲染的前端代码,正如我们接下来将要讨论的。
生成 CSS 和标记
要在服务器端生成表示 React 前端视图的 CSS 和标记,我们将使用 Material-UI 的ServerStyleSheets和 React 的renderToString。
在 Express 应用接收到的每个请求中,我们将创建一个新的ServerStyleSheets实例。然后,我们将使用renderToString调用中的服务器端收集器渲染相关的 React 树,最终返回要向用户显示的 React 视图的关联标记或 HTML 字符串版本。
以下代码将在 Express 应用接收到的每个 GET 请求上执行。
mern-skeleton/server/express.js:
const sheets = new ServerStyleSheets()
const context = {}
const markup = ReactDOMServer.renderToString(
sheets.collect(
<StaticRouter location={req.url} context={context}>
<ThemeProvider theme={theme}>
<MainRouter />
</ThemeProvider>
</StaticRouter>
)
)
在渲染 React 树时,客户端应用的根组件MainRouter被 Material-UI 的ThemeProvider包装,以提供MainRouter子组件所需的样式 props。在这里使用无状态的StaticRouter而不是客户端上使用的BrowserRouter,是为了包装MainRouter并提供用于实现客户端组件的路由 props。
基于这些值,例如作为 props 传递给包装组件的请求的location路由和theme,renderToString将返回包含相关视图的标记。
发送带有标记和 CSS 的模板
一旦生成了标记,我们需要检查组件中是否生成了要发送在标记中的redirect。如果没有生成重定向,那么我们使用sheets.toString从sheets获取 CSS 字符串,并在响应中发送带有注入的标记和 CSS 的Template,如下面的代码所示。
mern-skeleton/server/express.js:
if (context.url) {
return res.redirect(303, context.url)
}
const css = sheets.toString()
res.status(200).send(Template({
markup: markup,
css: css
}))
当我们尝试通过服务器端渲染访问 PrivateRoute 时,会渲染重定向的例子。由于服务器端无法从浏览器的 sessionStorage 中访问认证令牌,PrivateRoute 中的重定向将会渲染。在这种情况下,context.url 的值将是 '/signin' 路由,因此,而不是尝试渲染 PrivateRoute 组件,它将重定向到 '/signin' 路由。
这完成了我们需要添加到服务器端的代码,以启用 React 视图的简单服务器端渲染。接下来,我们需要更新前端,使其能够集成并渲染由服务器生成的代码。
更新 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>
...`
}
这将在前端脚本准备好接管之前在浏览器中加载服务器生成的代码。在下一节中,我们将学习前端脚本需要如何处理从服务器端渲染代码的接管。
更新 App.js
一旦服务器端渲染的代码到达浏览器,并且前端脚本接管,我们需要在根 React 组件挂载时移除服务器端注入的 CSS,使用 useEffect 钩子。
mern-skeleton/client/App.js:
React.useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side')
if (jssStyles) {
jssStyles.parentNode.removeChild(jssStyles)
}
}, [])
这将使客户端能够完全控制 React 应用的渲染。为了确保这种转移高效进行,我们需要更新 ReactDOM 渲染视图的方式。
替换渲染为激活
现在,由于 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 网络应用程序,具有基本用户功能。我们可以扩展这个骨架中的代码,为不同的应用程序添加各种功能。
摘要
在本章中,我们通过添加一个可工作的 React 前端,包括前端路由和基本的 React 视图服务器端渲染,完成了 MERN 骨架应用程序。
我们首先更新了开发流程,使其包括用于 React 视图的客户端代码打包。我们更新了 Webpack 和 Babel 的配置,以编译 React 代码,并讨论了如何在开发过程中从一个地方加载配置的 Webpack 中间件,以启动服务器端和客户端代码的编译。
在更新开发流程并构建前端之前,我们添加了相关的 React 依赖项,包括用于前端路由的 React Router 和 Material-UI,以便在骨架应用的用户界面中使用它们现有的组件。
然后,我们实现了顶级根 React 组件,并集成了 React Router,这使得我们能够添加客户端路由以进行导航。使用这些路由,我们加载了我们使用 Material-UI 组件开发的自定义 React 组件,以构成骨架应用的用户界面。
为了使这些 React 视图能够动态地与从后端获取的数据进行交互,我们使用了 Fetch API 来连接到后端用户 API。然后,我们在前端视图中集成了认证和授权。我们使用 sessionStorage 来存储用户特定的详细信息,以及从服务器成功登录后获取的 JWT,以及通过使用 PrivateRoute 组件限制对某些视图的访问。
最后,我们修改了服务器代码,以便我们可以实现基本的服务器端渲染,这允许我们在服务器识别到传入的请求实际上是为 React 路由时,直接在浏览器中加载前端路由的标记。
现在,你应该能够实现并集成一个基于 React 的前端,该前端结合了客户端路由和独立服务器应用程序的认证管理。
在下一章中,我们将使用本章学到的概念来扩展骨架应用程序代码,以便我们可以构建一个功能齐全的社交媒体应用程序。