十二、遵循最佳实践并进一步开发 MERN

在本章中,我们将详细介绍在构建本书中的四个 MERN 应用时应用的一些最佳实践,以及本书中未应用的其他实践,但在实际应用中应考虑这些实践,以确保随着复杂性的增加而提高可靠性和可伸缩性。最后,我们总结了关于增强的建议,以及扩展所构建应用的步骤。

本章涵盖的主题包括以下内容:

  • 应用结构中模块化关注点的分离
  • 考虑 CSS 样式解决方案的选项
  • 使用选定视图的数据进行服务器端渲染
  • 对有状态组件和纯功能组件使用 ES6 类
  • 决定使用 Redux 或 Flux
  • 用于存储用户凭据的安全增强功能
  • 编写测试代码
  • 优化束大小
  • 如何向现有应用添加新功能

模块化关注点分离

在构建 MERN 堆栈应用时,我们在每个应用中遵循一个通用文件夹结构,它根据相关性和通用功能对代码进行划分和分组。在代码中创建这些较小且不同的部分背后的想法是确保每个部分解决一个单独的问题,以便可以重用各个部分,以及独立地开发和更新。

重新访问应用文件夹结构

更具体地说,在应用文件夹结构中,我们将客户端代码和服务器端代码分开,并在这两个部分中进一步细分。这使我们可以自由地独立设计和构建应用的前端和后端:

| mern_application/
  | -- client/
  | -- server/

clientserver部分中,我们将代码进一步划分为子文件夹,这些子文件夹映射到独特的功能,例如模型、控制器和服务器中的路由到特定功能,例如将与客户端用户相关的所有组件分组。

服务器端代码

在服务器端,我们根据功能划分代码,将定义业务模型的代码与实现路由逻辑的代码以及在这些路由上响应客户端请求的代码分开:

  | -- server/
    | --- controllers/
    | --- models/
    | --- routes/

在此结构中,每个文件夹包含具有特定用途的代码:

  • 模型:此文件夹用于在单独的文件中包含所有 Mongoose 模式模型定义,每个文件代表一个模型。
  • 路由:此文件夹包含允许客户端与服务器交互的所有路由-放置在单独的文件中,每个文件可能与 models 文件夹中的模型关联。
  • 控制器:包含所有控制器功能,这些功能定义了在定义的路由上响应传入请求的逻辑,分为与相关模型和路由文件相对应的单独文件。

正如整本书所展示的,服务器端代码的这些特定关注点分离允许我们通过添加所需的模型、路由和控制器文件来扩展为框架应用开发的服务器。

客户端代码

MERN 应用的客户端代码主要由 React 组件组成。为了以合理和可理解的方式组织组件代码和相关辅助代码,我们将代码分为与特征实体或唯一功能相关的文件夹:

  | -- client/
    | --- auth/
    | --- core/
    | --- post/
    | --- user/
    | --- componentFolderN/

在前面的结构中,我们将所有与 auth 相关的组件和助手代码放在auth文件夹中,将常见和基本组件,如HomeMenu组件放在core文件夹中,然后我们在各自的文件夹中为所有与 post 相关或与用户相关的组件制作postuser文件夹。

这种基于功能的组件分离和分组允许我们通过在客户机文件夹中添加新的功能相关组件代码文件夹(根据需要),为随后的每个应用扩展框架应用中的前端视图。

在本章的最后一节中,我们将进一步展示这种分离应用代码的模块化方法的优势,因为我们将概述可用于向本书中开发的任何现有应用添加新功能的通用工作流。

添加 CSS 样式

在本书中讨论应用的用户界面实现时,我们选择不关注应用的 CSS 样式代码的细节,而主要依赖于默认的材质 UI 样式。但是,考虑到实现任何用户界面都需要考虑样式化解决方案,我们将简要介绍一些可用的选项

在前端添加 CSS 样式时,有许多选项,每个选项都有优点和缺点。在本节中,我们将讨论两个最常见的选项,即外部样式表和内联样式,以及一种用 JavaScript 编写 CSS 的新方法,或者更具体地说是 JSS,它用于 Material UI 组件,因此也适用于本书中的应用。

外部样式表

外部样式表允许我们在单独的文件中定义 CSS 规则,这些文件可以注入到必要的视图中。在外部样式表中以这种方式放置 CSS 样式一度被认为是更好的做法,因为它强制了样式和内容的分离,允许重用,并且在为每个组件创建单独的 CSS 文件时也保持模块化。

然而,随着 web 开发技术的不断发展,这种方法不再能够满足更好的 CSS 组织和性能的需求。例如,在使用 React 组件开发前端视图时使用外部样式表会限制对基于组件状态更新样式的控制。此外,为 React 应用加载外部 CSS 需要额外的带有css-loaderstyle-loader的网页包配置。

当应用增长并共享多个样式表时,也无法避免选择器冲突,因为 CSS 只有一个全局名称空间。因此,尽管外部样式表对于简单和琐碎的应用来说已经足够了,但随着应用的增长,使用 CSS 的其他选项变得更加相关。

内联样式

内联 CSS 是一种定义的样式,直接应用于视图中的各个元素。尽管这解决了外部样式表所面临的一些问题,例如消除选择器冲突和允许依赖于状态的样式,但它带走了可重用性并引入了一些自身的问题,例如限制可以应用的 CSS 特性。

对于不断增长的应用,仅对基于 React 的前端使用内联 CSS 具有重要的限制,例如性能差,因为在每次渲染时都会重新计算所有内联样式,并且内联样式比开始时的类名慢。

在某些情况下,内联 CSS 似乎是一个简单的修复方法,但对于整体使用来说并不是一个好的选择。

JSS

JSS 允许我们以声明的方式使用 JavaScript 编写 CSS 样式。这也意味着 JavaScript 的所有特性现在都可用于编写 CSS,从而可以编写可重用和可维护的样式代码。

JSS 是一个 JS-to-CSS 编译器,它接受 JS 对象,其中键表示类名,值表示相应的 CSS 规则,然后生成 CSS 和作用域类名。

通过这种方式,JSS 在将 JSON 表示编译为 CSS 时默认生成唯一的类名,从而消除了外部样式表面临的选择器冲突的机会。此外,与内联样式不同,使用 JSS 定义的 CSS 规则可以跨多个元素共享,并且所有 CSS 特性都可以在定义中使用。

Material UI 使用 JSS 为其组件设置样式,因此我们使用 JSS 将 Material UI 主题和自定义 CSS 应用于为所有应用中的前端视图开发的组件。

使用数据的选择性服务器端渲染

当我们在第 4 章中开发基础骨架应用的前端时,添加了一个 React 前端来完成 MERN时,我们集成了基本的服务器端渲染,当请求到达服务器时,能够直接从浏览器地址栏加载客户端路由。在这个 SSR 实现中,在呈现反作用组件服务器端时,我们没有考虑从数据库中加载显示数据的组件的数据。只有当客户端 JavaScript 在服务器端呈现标记的初始加载之后接管时,数据才会加载到这些组件中。

我们确实更新了这个实现,在 MERN Mediastream 应用中添加了服务器端呈现,其中包含第 9 章定制媒体播放器和改进 SEO中讨论的各个媒体详细信息页面的数据。在本例中,我们决定通过向 React 前端的服务器端生成的标记中注入数据来呈现具有数据的特定视图。这种仅对特定视图使用数据的选择性服务器端渲染背后的推理可以基于所讨论视图的某些期望行为。

什么时候 SSR 与数据相关?

在应用中的所有反应视图中实现具有数据的服务器端渲染可能变得复杂,并且当需要考虑客户端认证或由多个数据源组成的视图时,需要额外的工作。在许多情况下,如果视图不需要使用数据进行服务器端渲染,则可能没有必要处理这些复杂性。为了判断视图是否需要由服务器呈现数据,请针对特定视图回答以下问题以做出决定:

  • 当浏览器中可能没有 JavaScript 时,在视图的初始加载中显示数据是否重要?
  • 视图及其数据是否需要对 SEO 友好?

从可用性的角度来看,在页面的初始加载中加载数据可能是相关的,因此它实际上取决于特定视图的用例。对于 SEO,使用数据的服务器端呈现将使搜索引擎更容易访问视图中的数据内容,因此,如果这对所讨论的视图至关重要,那么使用数据添加服务器端呈现是一个好主意。

对有状态组件和纯功能组件使用 ES6 类

在使用 React 组件构建 UI 时,使用更多无状态功能组件组合视图可以使前端代码易于管理、干净和测试。但有些组件需要状态或生命周期挂钩,而不仅仅是纯粹的表示组件。在本节中,我们将介绍如何构建有状态和无状态的功能组件,何时使用一个或另一个,以及使用频率。

将组件与 ES6 类进行反应

使用 ES6 类定义的 React 组件可以访问生命周期方法,this关键字,并且在构建有状态组件时可以使用setState管理状态。有状态组件允许我们构建交互式组件,这些组件可以管理状态中不断变化的数据,并传播需要在 UI 中应用的任何业务逻辑。一般来说,对于复杂的 UI,有状态组件应该是更高级别的容器组件,用于管理它们所组成的较小的无状态功能组件的状态。

将组件作为纯函数进行反应

React 组件可以使用 ES6 类语法定义为无状态功能组件,也可以定义为纯函数。其主要思想是无状态组件不修改状态并接收道具。

以下代码使用 ES6 类语法定义无状态组件:

class Greeting extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1> 
  }
}

也可以使用 JavaScript 纯函数定义,如下所示:

function Greeting(props) {
  return <h1>Hello, {props.name}</h1> 
}

当给定相同的输入时,纯函数总是给出相同的输出,而没有任何副作用。将 React 组件建模为纯函数将强制创建更小、定义更明确、自包含的组件,这些组件强调 UI 而不是业务逻辑,因为这些组件中没有状态操纵。这些类型的组件是可组合的、可重用的,并且易于调试和测试。

使用有状态组件和无状态功能组件设计 UI

在考虑 UI 的组件组合时,将根组件或父组件设计为有状态组件,其中包含子组件或仅接收道具且无法操作状态的可组合组件。所有使用setState的状态更改操作和生命周期问题将由根组件或父组件处理。

在为本书开发的应用中,混合了有状态的高级组件和较小的无状态组件。例如,在 MERN 社交应用中,Profile组件修改无状态子组件的状态,例如FollowProfileButtonFollowGrid组件。可以将本书中开发的一些较大组件重构为更小、更独立的组件,在扩展应用以包含更多功能之前,应该考虑这一点。

可以应用于新组件设计或重构现有组件的主要收获是,随着 React 应用的增长和变得更加复杂,最好将更多无状态功能组件添加到负责管理其内部组件状态的高级有状态组件中。

使用 Redux 或 Flux

当 React 应用开始增长并变得更加复杂时,管理组件之间的通信可能会出现问题。使用 React 时,通信的方式是将值和回调函数作为道具传递给子组件。但是,如果回调必须通过很多中间组件,那么这可能会很乏味。随着 React 应用的发展,为了解决这些与状态通信和管理相关的问题,人们转向使用 React 与库和体系结构模式,如 Redux 和 Flux。

在本书的范围之外,深入研究与 ReDux 库或 FLUX 架构集成的细节,但读者可以考虑这些选项以供其不断增长的 Mern 应用,同时请记住以下几点:

  • Redux 和 Flux 利用了从中心位置强制 React 应用中改变状态的模式。避免在可管理规模的 React 应用中使用 Redux 或 Flux 的一个技巧是将组件树中的所有状态更改向上移动到父组件。
  • 较小的应用在没有 Flux 或 Redux 的情况下也能正常工作。

You can learn more about using React with Redux at https://redux.js.org/, and Flux at facebook.github.io/flux/.

加强安全

在为本书开发的 MERN 应用中,我们使用 JSON Web 令牌作为身份验证机制,并在用户集合中存储哈希密码,从而简化了与身份验证相关的安全实现。在本节中,我们将回顾这些选择并指出可能的增强。

JSON web 令牌–客户端或服务器端存储

通过 JWT 身份验证机制,客户端负责维护用户状态。用户登录后,服务器发送的令牌由客户端代码在浏览器存储器上存储维护,如sessionStorage。因此,当用户注销或需要注销时,客户端代码也可以通过删除令牌来使令牌无效。这种机制适用于大多数需要最少身份验证以保护对资源的访问的应用。但是,对于可能需要跟踪用户登录、注销以及让服务器知道特定令牌不再对登录有效的情况,仅客户端处理令牌是不够的。

对于这些情况,讨论的在客户端处理 JWT 令牌的实现也可以扩展到服务器端的存储。在跟踪失效令牌的特定情况下,服务器可以维护 MongoDB 集合以存储这些失效令牌作为参考,这与在服务器端存储会话数据的方式有些类似。

需要注意的是,在大多数情况下,在客户端和服务器端存储和维护与身份验证相关的信息可能过于繁琐。因此,这完全取决于具体用例和要考虑的相关权衡。

保护密码存储

在用户集合中存储用于身份验证的用户凭据时,我们确保用户提供的原始密码字符串不会直接存储在数据库中。相反,我们使用节点中的crypto模块生成了密码散列和 salt 值。

在我们的应用的user.model.js中,我们定义了以下函数来生成散列的passwordsalt值:

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())) + '' 
  }

在这个实现中,每当用户输入密码登录时,salt 就会生成一个哈希。如果生成的哈希与存储的哈希匹配,则密码正确,否则密码错误。因此,为了检查密码是否正确,salt 是必需的,因此它与用户详细信息一起存储在数据库中以及散列。

这是保护存储用于用户身份验证的密码的标准做法,但如果特定应用的安全需求需要,还可以探索其他高级方法。可以考虑的一些选项包括多迭代散列方法、其他安全散列算法、限制每个用户帐户的登录尝试,以及带有附加步骤(如回答安全问题或输入安全代码)的多级身份验证。

编写测试代码

尽管讨论和编写测试代码超出了本书的范围,但它对于开发可靠的软件至关重要。在本节中,首先我们将介绍可用于测试 MERN 应用不同部分的测试工具。然后,为了帮助开始编写本书中开发的 MERN 应用的测试代码,我们还将讨论一个真实的例子,从第 5 章开始,从一个简单的社交媒体应用开始,向 MERN 社交应用添加客户端测试。

开玩笑地测试

Jest 是一个针对 JavaScript 的综合测试框架。虽然它更常见于测试 React 组件,但它可以用于任何 JavaScript 库或框架的通用测试。在 Jest 中的众多 JavaScript 测试解决方案中,它提供了对模拟和快照测试的支持,并附带了断言库,Jest 中的测试是以行为驱动开发BDD风格编写的。除了测试 React 组件外,Jest 还可以根据需要为基于 Node Express Mongoose 的后端编写测试代码。因此,为 MERN 应用添加测试代码是一个可靠的测试选项。

To learn more about Jest, read the docs at https://facebook.github.io/jest/docs/en/getting-started.html.

向 MERN 社交应用添加测试

使用 Jest,我们将向 MERN 社交应用添加客户端测试,并演示如何开始向 MERN 应用添加测试。

在编写测试代码之前,首先我们将安装必要的软件包,定义测试运行脚本,并为测试代码创建一个tests文件夹,以进行测试。

安装软件包

编写测试代码和运行测试需要以下 npm 包:

  • jest:包括 jest 测试框架
  • babel jest:为 jest 编译 JS 代码
  • react test renderer:在不使用浏览器的情况下,对 react DOM 呈现的 DOM 树进行快照

要将这些软件包安装为devDependencies,请从命令行运行以下npm install命令:

npm install --save-dev jest babel-jest react-test-renderer

定义运行测试的脚本

为了运行测试代码,我们将更新package.json中定义的运行脚本,添加一个运行测试的脚本jest

  "scripts": {
    "test": "jest"
  }

在命令行中,如果我们运行npm run test,它将提示 Jest 在应用文件夹中查找测试代码并运行测试。

添加测试文件夹

要在 MERN 社交应用中添加客户端测试,我们将在客户端文件夹中创建一个名为tests的文件夹,其中将包含与测试 React 组件相关的测试文件。当运行 test 命令时,Jest 将在这些文件中查找测试代码。

本例的测试用例是对Post组件的测试,而Post组件的测试将添加到tests文件夹中名为post.test.js的文件中。

测试用例

我们将编写一个测试来检查帖子上的delete按钮是否只有在登录用户也是帖子的创建者时才可见。这意味着,如果经过身份验证的用户的user._id值与正在呈现的 Post 数据的postedby值相同,则删除按钮将仅是 Post 视图的一部分。

添加测试

为了实现此测试用例,我们将添加代码,其中包含以下内容:

  • 为 post 和auth对象定义虚拟数据
  • 模仿auth-helper.js
  • 定义测试,并在测试定义内
    • 声明postauth变量
    • 将模拟的isAuthenticated方法的返回值设置为虚拟auth对象
    • 使用renderer.create创建Post组件,将所需的虚拟道具传递并包裹在MemoryRouter中,以提供与react-router相关的道具
    • 生成并匹配快照

post.test.js中包含此特定测试所述步骤的代码如下:

import auth from './../auth/auth-helper.js'
import Post from './../post/Post.js'
import React from 'react'
import renderer from 'react-test-renderer'
import { MemoryRouter } from 'react-router-dom'

jest.mock('./../auth/auth-helper.js') 

const dummyPostObject = {"_id":"5a3cb2399bcc621874d7e42f",
                         "postedBy":{"_id":"5a3cb1779bcc621874d7e428",
                         "name":"Joe"}, "text":"hey!",
                         "created":"2017-12-22T07:20:25.611Z",
                         "comments":[], "likes":[]} 
const dummyAuthObject = {user: {"_id":"5a3cb1779bcc621874d7e428",
                                "name":"Joe",
                                "email":"abc@def.com"}} 

test('delete option visible only to authorized user', () => {
  const post = dummyPostObject 
  const auth = dummyAuthObject 

  auth.isAuthenticated.mockReturnValue(auth) 

  const component = renderer.create(
     <MemoryRouter>
         <Post post={post} key={post._id} ></Post>
     </MemoryRouter>
  ) 

  let tree = component.toJSON() 
  expect(tree).toMatchSnapshot() 
}) 

生成正确 Post 视图的快照

第一次运行此测试时,我们将为它提供生成 Post 视图的正确快照所需的值。当 auth 对象的user._id等于 post 对象的postedBy值时,此测试用例的正确快照将包含 delete 按钮。第一次运行测试时生成的此快照将用于将来执行测试时的比较。

Snapshot testing in Jest basically records snapshots of rendered component structures to compare them to future renderings. When the recorded snapshot and the current rendering don’t match, the test fails, indicating that something has changed.

运行并检查测试

在我们刚刚添加到post.test.js的代码中,虚拟auth对象和post对象指的是同一个用户,因此在命令行中运行此测试将提示 Jest 生成包含删除选项的快照,并通过测试。

要运行测试,请从命令行转到项目文件夹:

npm run test

测试输出将显示测试通过:

当该测试首次成功运行时,生成的记录快照将自动添加到tests文件夹中的_snapshots_文件夹中。此快照表示视图中呈现“删除”按钮的状态,因为经过身份验证的用户也是帖子的创建者

现在,我们可以检查当组件由非帖子创建者的经过身份验证的用户呈现时,测试是否实际失败。要执行此检查,我们将通过更改user._id以不匹配postedBy值来更新虚拟数据对象,然后再次运行测试。这将给我们一个失败的测试,因为当前渲染将不再具有记录的快照中存在的删除按钮。

如下图中的测试日志所示,测试失败,并表明呈现的树与记录的快照不匹配,因为代表 delete 按钮的元素在接收到的值中丢失:

通过这个屏幕截图,我们有一个客户端测试,用于检查登录用户是否可以查看其帖子上的delete按钮。使用此设置,可以利用 Jest 的功能为 MERN 应用添加更多测试。

编写测试代码将使您开发的应用可靠,并有助于确保代码质量。提高和维护代码质量的另一个良好实践是在项目中使用 linting 工具。Linting 工具对代码执行静态分析,以发现违反指定规则和准则的问题模式或行为。JavaScript 项目中的 Linting 代码可以提高代码的整体可读性,还可以帮助在代码执行之前发现语法错误。对于基于 MERN 的项目中的 linting,您可以探索 ESLint,它是一个 JavaScript linting 实用程序,允许开发人员创建自己的 linting 规则。

优化束大小

随着 MERN 应用的开发和发展,使用 Webpack 生成的捆绑包的大小也会增长,特别是在使用大型第三方库的情况下。较大的包大小将影响性能并增加应用的初始加载时间。我们可以对代码进行更改,以确保最终不会出现大型捆绑包,还可以利用 Webpack4 中打包的功能来帮助优化捆绑包。在本节中,我们将重点介绍一些关键概念,这些概念可以让我们控制生成更小的捆绑包和减少加载时间。

在进入代码以更新包大小优化之前,您还可以熟悉默认的优化选项,这些选项现在是 Webpack4 的一部分。在 MERN 应用中,我们使用mode配置来利用开发和生产模式的默认设置。要获得可用选项的概述,请访问查看本文 https://medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a

代码拆分

我们可以使用 Webpack 支持的代码拆分功能,根据用户当前的需要延迟加载部分应用代码,而不是一次在一个包中加载所有代码。在我们修改应用代码以引入代码拆分后,Webpack 可以创建多个包,而不是一个大包。这些捆绑包可以在运行时动态加载,从而提高应用的性能。

要了解有关 Webpack 中的代码拆分支持以及如何对设置和配置进行必要更改的更多信息,请查看文档中的指南,网址为https://webpack.js.org/guides/code-splitting/

有几种方法可以为应用代码引入代码拆分,但为此您将遇到的最重要的语法是动态的import()。在下一节中,我们将了解如何在 MERN 应用中使用import()

动态导入()

动态import()是常规导入的一个新的类似函数的版本,它支持动态加载 JS 模块。使用import(moduleSpecifier)将返回请求模块的模块名称空间对象的承诺。当使用常规静态导入时,我们在代码顶部导入一个模块,然后在代码中使用它:

import {  convert } from './metric'
...
console.log(convert('km', 'miles', 202))

如果我们在开始时使用动态import()而不是添加静态导入,代码将如下所示:

import('./metric').then({ convert } => { 
    console.log( convert('km', 'miles', 202) ) 
})

这允许在代码需要时导入和加载模块。捆绑应用代码时,Webpack 会将对import()的调用视为拆分点,并通过将请求的模块及其子模块放置在主捆绑包的单独块中,自动开始代码拆分。

为了通过在给定组件上应用代码拆分来优化前端 React 代码的捆绑,我们需要将动态import()与 React Loadable—一个用于加载承诺组件的高阶组件配对。例如,我们将查看第 7 章中开发的购物车扩展订单和支付市场。在构建 cart 的界面时,我们通过导入Checkout组件并将其添加到视图中,组成了Cart组件,如下所示:

import Checkout from './Checkout'
class Cart extends Component {
    ...
    render(){
        ...
        <Checkout/>
    }
    ...
}

为了在这里引入代码拆分,动态导入Checkout组件,我们可以用Loadable签出代替开始时的静态导入,如下代码所示:

import Loadable from 'react-loadable'
const Checkout = Loadable({
  loader: () => import('./Checkout'),
  loading: () => <div>Loading...</div>,
})

进行此更改并再次使用 Webpack 构建代码将生成一个大小减小的bundle.js文件,并生成另一个表示拆分代码的较小捆绑文件,该文件现在仅在呈现Cart组件时加载。

使用这种机制,我们可以根据需要在应用代码中应用代码拆分。需要记住的是,有效的代码拆分将取决于正确使用它,并将其应用于代码中的正确位置,这将有助于优化资源负载优先级

Route-based code splitting can be an effective approach for introducing code splitting in React apps that use routes to load components in the view. To learn more about implementing code splitting, specifically with React Router, check out the article at https://tylermcginnis.com/react-router-code-splitting/.

扩展应用

在本书的所有章节中,当我们开发每个应用时,我们通过扩展现有代码添加了一些功能,这些功能是通过一个通用的、可重复的步骤来实现的。在最后一节中,我们将回顾这些步骤,并为向当前版本的应用添加更多功能制定指导方针。

扩展服务器代码

对于需要数据持久性和 API 以允许视图操作数据的特定功能,我们可以从扩展服务器代码和添加必要的模型、路由和控制器功能开始。

添加模型

对于特性的数据持久性方面,设计数据模型时考虑需要存储的字段和值。然后,在server/models文件夹中的单独文件中定义并导出此数据模型的 Mongoose 模式。

实现 API

接下来,设计与所需功能相关的 API,以便根据模型操作和访问将存储在数据库中的数据。

添加控制器

确定 API 后,在server/controllers文件夹中的单独文件中添加相应的控制器函数,以响应对这些 API 的请求。此文件中的控制器函数应访问和操作为此功能定义的模型的数据。

添加路由

要完成服务器端 API 的实现,需要在 Express app 上声明并装载相应的路由。在server/routes文件夹中的单独文件中,首先声明并导出这些 API 的路由,分配请求特定路由时应执行的相关控制器功能。然后,将这些新路由加载到server/express.js文件中的 Express 应用上,就像应用中的其他现有路由一样。

这将生成新的后端 API 的工作版本,可以从 REST API 客户端应用运行和检查,然后再继续为正在开发的功能构建和集成前端视图。

扩展客户端代码

在客户端,首先设计功能所需的视图,并确定这些视图将如何结合用户与功能相关数据的交互。然后添加 fetchapi 代码以与新的后端 API 集成,定义表示这些新视图的新组件,并更新现有代码以将这些新组件包含在应用的前端。

添加 API 获取方法

在“客户端”文件夹中,创建一个新文件夹,以容纳与正在开发的功能模块相关的组件和辅助代码。然后,要集成新的后端 API,请在此新组件文件夹中的单独文件中添加并导出相应的获取方法。

添加组件

在新文件夹中的单独文件中创建和导出表示所需特征视图的新构件。使用现有的 auth 助手方法将 auth 集成到这些新组件中。

加载新组件

为了将这些新组件合并到前端,需要将这些组件添加到现有组件中,或者按照其自己的客户端路线进行渲染。

更新前端路线

如果需要在各个路由上呈现这些新组件,请更新MainRouter.js代码以添加在给定 URL 路径上加载这些组件的新路由。

与现有组件集成

如果新零部件将成为现有视图的一部分,请将零部件导入现有零部件,以便根据需要将其添加到视图中。新组件也可以与现有组件集成,例如在Menu组件中,通过链接到使用单个路由添加的新组件。

组件集成并与后端连接后,新功能的实现就完成了。可以重复这些步骤,为应用添加新功能。

总结

在最后一章中,我们在本书中回顾并详细阐述了构建 MERN 应用时使用的一些最佳实践,强调了改进的领域,给出了解决应用增长时可能出现的问题的建议,并最终制定了继续在现有应用中开发更多功能的指导原则。

我们看到,模块化应用的代码结构有助于轻松扩展应用,选择使用 JS 而不是内联 CSS 和外部样式表可以保持样式代码的包含性和易用性,并且只根据需要实现特定视图的服务器端呈现可以避免代码中不必要的复杂性。

我们讨论了创建由更小、定义更明确的无状态功能组件组成的更少有状态组件的好处,以及在重构现有组件或设计新组件以扩展应用时如何应用这些好处。对于可能遇到跨数百个组件管理和通信状态问题的不断增长的应用,我们指出可以考虑使用 Redux 或 Flux 等选项来解决这些问题。

对于可能对更严格的安全实施有更高要求的应用,我们回顾了使用 JWT 和密码加密的用户身份验证的现有实现,并讨论了提高安全性的可能扩展。

我们使用 Jest 演示了如何将测试代码添加到 MERN 应用中,并讨论了良好实践(如编写测试代码和使用 linting 工具)如何在确保应用可靠性的同时提高代码质量。

我们还研究了捆绑包优化特性,如代码拆分,这些特性可以通过减少初始捆绑包大小和根据需要延迟加载部分应用来帮助提高性能。

最后,我们回顾并制定了本书中使用的 repeatabe 步骤,可以作为通过添加更多功能扩展 MERN 应用的指南。