使用 Next.js 重建大型电子商务网站(案例研究)

已发表: 2022-03-10
快速总结↬我们使用 Next.js 从更传统的集成电子商务平台切换到无头平台。 以下是使用 Next.js 重建大型电子商务网站时学到的最重要的经验教训。

在我们的 Unplatform 公司,几十年来我们一直在构建电子商务网站。 这些年来,我们已经看到技术堆栈从带有一些小的 JavaScript 和 CSS 的服务器渲染页面发展到成熟的 JavaScript 应用程序。

我们用于电子商务网站的平台基于 ASP.NET,当访问者开始期待更多交互时,我们为前端添加了 React。 尽管将像 ASP.NET 这样的服务器 Web 框架的概念与像 React 这样的客户端 Web 框架的概念混合在一起会使事情变得更加复杂,但我们对这个解决方案非常满意。 直到我们与流量最高的客户一起投入生产。 从我们上线的那一刻起,我们就遇到了性能问题。 Core Web Vitals 很重要,在电子商务中更是如此。 在德勤的这项研究:毫秒赚百万中,研究人员分析了 37 个不同品牌的移动网站数据。 结果,他们发现 0.1 秒的性能提升可以带来 10% 的转化率提升。

为了缓解性能问题,我们不得不添加大量(未预算的)额外服务器,并且不得不在反向代理上积极缓存页面。 这甚至要求我们禁用网站的部分功能。 我们最终得到了一个非常复杂、昂贵的解决方案,在某些情况下只是静态地提供一些页面。

显然,这感觉不对,直到我们发现了 Next.js 。 Next.js 是一个基于 React 的 Web 框架,它允许您静态生成页面,但您仍然可以使用服务器端渲染,非常适合电子商务。 它可以托管在 Vercel 或 Netlify 等 CDN 上,从而降低延迟。 Vercel 和 Netlify 还使用无服务器功能进行服务器端渲染,这是最有效的横向扩展方式。

挑战

使用 Next.js 进行开发令人惊叹,但肯定存在一些挑战。 Next.js 的开发者体验是您只需要体验的东西。 您编写的代码会立即在您的浏览器中可视化,并且生产力会飞速发展。 这也是一种风险,因为您很容易过于关注生产力而忽视代码的可维护性。 随着时间的推移,这和 JavaScript 的无类型特性会导致代码库的退化。 错误数量增加,生产力开始下降。

运行时方面也可能具有挑战性。 代码中最小的更改可能会导致性能和其他核心 Web 生命值下降。 此外,不小心使用服务器端渲染可能会导致意外的服务成本。

让我们仔细看看我们在克服这些挑战方面的经验教训。

  1. 模块化你的代码库
  2. 整理和格式化您的代码
  3. 使用打字稿
  4. 计划绩效和衡量绩效
  5. 将性能检查添加到您的质量门
  6. 添加自动化测试
  7. 积极管理你的依赖
  8. 使用日志聚合服务
  9. Next.js 的重写功能支持增量采用
跳跃后更多! 继续往下看↓

经验教训:模块化您的代码库

像 Next.js 这样的前端框架让现在很容易上手。 您只需运行 npx create-next-app 即可开始编码。 但是如果你不小心,开始敲代码而不考虑设计,你最终可能会得到一个大泥球。

当您运行npx create-next-app时,您将拥有如下的文件夹结构(这也是大多数示例的结构):

 /public logo.gif /src /lib /hooks useForm.js /api content.js /components Header.js Layout.js /pages Index.js

我们开始使用相同的结构。 我们在 components 文件夹中有一些用于更大组件的子文件夹,但大多数组件都在根组件文件夹中。 这种方法没有任何问题,对于较小的项目也很好。 然而,随着我们项目的发展,对组件及其使用位置的推理变得越来越困难。 我们甚至发现了根本不再使用的组件! 它还推波助澜,因为没有明确的指导说明哪些代码应该依赖于其他哪些代码。

为了解决这个问题,我们决定重构代码库并按功能模块(有点像 NPM 模块)而不是技术概念对代码进行分组

 /src /modules /catalog /components productblock.js /checkout /api cartservice.js /components cart.js

在这个小例子中,有一个结帐模块和一个目录模块。 以这种方式对代码进行分组可以提高可发现性:仅通过查看文件夹结构,您就可以确切地知道代码库中有哪些功能以及在哪里可以找到它。 它还使推理依赖关系变得容易得多。 在以前的情况下,组件之间存在很多依赖关系。 我们在结帐时收到了更改的拉取请求,这也影响了目录组件。 这增加了合并冲突的数量,并使更改变得更加困难。

最适合我们的解决方案是将模块之间的依赖关系保持在最低限度(如果您真的需要依赖关系,请确保它是单向的)并引入将所有内容联系在一起的“项目”级别:

 /src /modules /common /atoms /lib /catalog /components productblock.js /checkout /api cartservice.js /components cart.js /search /project /layout /components /templates productdetail.js cart.js /pages cart.js

此解决方案的视觉概述:

模块化项目示例概述
模块化项目示例概述(大预览)

项目级别包含电子商务站点和页面模板的布局代码。 在 Next.js 中,页面组件是一种约定,会产生一个物理页面。 根据我们的经验,这些页面通常需要重用相同的实现,这就是我们引入“页面模板”概念的原因。 页面模板使用来自不同模块的组件,例如,产品详细信息页面模板将使用目录中的组件来显示产品信息,但也会使用结帐模块中的添加到购物车组件。

我们还有一个通用模块,因为还有一些代码需要功能模块重用。 它包含简单的原子,这些原子是用于提供一致的外观和感觉的 React 组件。 它还包含基础设施代码,想想某些通用的反应钩子或 GraphQL 客户端代码。

警告请确保公共模块中的代码是稳定的,并且在添加代码之前要三思而后行,以防止代码缠结。

微前端

在更大的解决方案中或与不同的团队合作时,将应用程序进一步拆分为所谓的微前端是有意义的。 简而言之,这意味着将应用程序进一步拆分为多个独立托管在不同 URL 上的物理应用程序。 例如: checkout.mydomain.com和 catalog.mydomain.com。 然后,它们由充当代理的不同应用程序集成。

Next.js 的重写功能对此非常有用,所谓的多区域支持像这样使用它。

多区域设置示例
多区域设置示例(大预览)

多区域的好处是每个区域都管理自己的依赖关系。 它还可以更轻松地逐步发展代码库:如果 Next.js 或 React 的新版本发布,您可以逐个升级区域,而不必一次升级整个代码库。 在多团队组织中,这可以大大减少团队之间的依赖关系。

延伸阅读

  • “Next.js 项目结构”,Yannick Wittwer,Medium
  • “关于以灵活高效的方式构建 Next.js 项目的 2021 年指南,”Vadorequest,Dev.to。
  • “微前端”,Michael Geers

经验教训:Lint 和格式化您的代码

这是我们在早期项目中学到的:如果您与多人在同一个代码库中工作并且不使用格式化程序,您的代码很快就会变得非常不一致。 即使您正在使用编码约定并进行审查,您很快就会开始注意到不同的编码风格,从而给代码留下杂乱无章的印象。

linter 将检查您的代码是否存在潜在问题,格式化程序将确保代码以一致的方式格式化。 我们使用 ESLint & prettier 并认为它们很棒。 您不必考虑编码风格,减少开发过程中的认知负担。

幸运的是,Next.js 11 现在支持开箱即用的 ESLint (https://nextjs.org/blog/next-11),通过运行 npx next lint 可以非常容易地进行设置。 这可以为您节省大量时间,因为它带有 Next.js 的默认配置。 例如,它已经为 React 配置了 ESLint 扩展。 更好的是,它带有一个新的 Next.js 特定扩展,甚至可以发现代码中可能影响应用程序核心 Web 生命力的问题! 在后面的段落中,我们将讨论质量门,它可以帮助您防止将代码推送到意外伤害您的 Core Web Vitals 的产品。 此扩展为您提供更快的反馈,使其成为一个很好的补充。

延伸阅读

  • “ESLint”,Next.js 文档
  • “ESLint”官方网站

经验教训:使用 TypeScript

随着组件的修改和重构,我们注意到一些组件 props 不再使用。 此外,在某些情况下,由于传递给组件的道具类型缺失或不正确,我们遇到了错误。

TypeScript 是 JavaScript 的超集并添加了类型,它允许编译器静态检查您的代码,有点像类固醇上的 linter。

在项目开始时,我们并没有真正看到添加 TypeScript 的价值。 我们觉得这只是一个不必要的抽象。 但是,我们的一位同事对 TypeScript 有很好的体验,并说服我们尝试一下。 幸运的是,Next.js 具有很好的 TypeScript 开箱即用支持,TypeScript 允许您逐步将其添加到您的解决方案中。 这意味着您不必一次性重写或转换整个代码库,但您可以立即开始使用它并慢慢转换其余代码库。

一旦我们开始将组件迁移到 TypeScript,我们立即发现将错误值传递给组件和函数的问题。 此外,开发者反馈循环变短了,您在浏览器中运行应用程序之前会收到问题通知。 我们发现的另一大好处是它使重构代码变得更加容易:更容易查看代码在哪里使用,并且您可以立即发现未使用的组件道具和代码。 简而言之,TypeScript 的好处:

  1. 减少错误的数量
  2. 使重构代码更容易
  3. 代码变得更容易阅读

延伸阅读

  • “TypeScript”,Next.js 文档
  • TypeScript,官方网站

经验教训:计划绩效和衡量绩效

Next.js 支持不同类型的预渲染:静态生成和服务器端渲染。 为了获得最佳性能,建议使用在构建期间发生的静态生成,但这并不总是可行的。 想想包含库存信息的产品详细信息页面。 这种信息经常变化,每次运行构建都不能很好地扩展。 幸运的是,Next.js 还支持一种称为增量静态重新生成 (ISR) 的模式,该模式仍然静态生成页面,但每隔 x 秒在后台生成一个新页面。 我们了解到,这种模型非常适合大型应用程序。 性能仍然很好,它比服务器端渲染需要更少的 CPU 时间,并且减少了构建时间:页面仅在第一个请求时生成。 对于您添加的每个页面,您应该考虑所需的呈现类型。 一、看能不能用静态生成; 如果没有,请使用增量静态再生,如果这也不可行,您仍然可以使用服务器端渲染。

Next.js 会根据页面上是否缺少getServerSidePropsgetInitialProps方法来自动确定渲染的类型。 很容易出错,这可能导致页面在服务器上呈现,而不是静态生成。 Next.js 构建的输出准确地显示了哪个页面使用什么类型的渲染,所以一定要检查一下。 它还有助于监控生产并跟踪页面的性能和所涉及的 CPU 时间。 大多数托管服务提供商会根据 CPU 时间向您收费,这有助于防止出现任何令人不快的意外。 我将在“经验教训:使用日志聚合服务”段落中描述我们如何监控这一点。

捆绑大小

为了获得良好的性能,最小化包大小是至关重要的。 Next.js 有很多开箱即用的功能,例如自动代码拆分。 这将确保只为每个页面加载所需的 JavaScript 和 CSS。 它还为客户端和服务器生成不同的捆绑包。 但是,重要的是要密切注意这些。 例如,如果您以错误的方式导入 JavaScript 模块,服务器 JavaScript 可能会最终出现在客户端包中,从而大大增加客户端包的大小并损害性能。 添加 NPM 依赖项也会极大地影响包大小。

幸运的是,Next.js 带有一个包分析器,可以让您深入了解哪些代码占用了包的哪些部分。

webpack 包分析器显示包中包的大小
webpack 包分析器显示包中包的大小(大预览)

延伸阅读

  • “Next.js + Webpack Bundle Analyzer”,Vercel,GitHub
  • “数据获取”,Next.js 文档

经验教训:将性能检查添加到您的质量门

使用 Next.js 的一大好处是能够静态生成页面并能够将应用程序部署到边缘 (CDN),这应该会带来出色的性能和 Web Vitals。 我们了解到,即使使用像 Next.js 这样的出色技术,获得并保持出色的灯塔分数也非常困难。 有好几次,在我们对生产环境进行了一些更改后,灯塔分数显着下降。 为了收回控制权,我们在质量门中添加了自动灯塔测试。 通过这个 Github Action,您可以自动将灯塔测试添加到您的拉取请求中。 我们使用 Vercel,每次创建拉取请求时,Vercel 都会将其部署到预览 URL,然后我们使用 Github 操作针对此部署运行灯塔测试。

Github 拉取请求上的灯塔结果示例
Github Pull Request 上的灯塔结果示例(大预览)

如果您不想自己设置 GitHub 操作,或者您想更进一步,您还可以考虑使用第三方性能监控服务,例如 DebugBear。 Vercel 还提供了一项分析功能,可测量生产部署的核心 Web Vitals。 Vercel Analytics 实际上从访问者的设备中收集测量值,因此这些分数实际上是访问者所体验的。 在撰写本文时,Vercel Analytics 仅适用于生产部署。

经验教训:添加自动化测试

当代码库变大时,很难确定您的代码更改是否破坏了现有功能。 根据我们的经验,拥有一套良好的端到端测试作为安全网至关重要。 即使你有一个小项目,当你至少进行一些基本的冒烟测试时,它也可以让你的生活变得更加轻松。 为此,我们一直在使用 Cypress,并且非常喜欢它。 使用 Netlify 或 Vercel 在临时环境中自动部署 Pull 请求并运行 E2E 测试的组合是无价的。

我们使用cypress-io/GitHub-action针对我们的拉取请求自动运行 cypress 测试。 根据您正在构建的软件类型,使用 Enzyme 或 JEST 进行更精细的测试可能很有价值。 权衡是这些与您的代码更紧密地耦合并且需要更多的维护。

Github 拉取请求的自动检查示例
Github Pull Request 的自动检查示例(大预览)

经验教训:积极管理您的依赖关系

在维护大型 Next.js 代码库时,管理依赖项变得非常耗时,但非常重要。 NPM 让添加包变得如此简单,而且现在似乎所有东西都有一个包。 回想起来,很多时候当我们引入新的错误或性能下降时,这与新的或更新的 NPM 包有关。

因此,在安装软件包之前,您应该始终问自己以下问题:

  • 包裹的质量如何?
  • 添加此包对我的捆绑包大小意味着什么?
  • 这个包真的有必要还是有替代品?
  • 该软件包是否仍在积极维护?

为了保持包的大小并尽量减少维护这些依赖项所需的工作量,保持依赖项的数量尽可能少是很重要的。 当您维护软件时,您未来的自己会为此感谢您。

提示导入成本 VSCode 扩展会自动显示导入包的大小。

跟上 Next.js 版本

跟上 Next.js 和 React 很重要。 它不仅可以让您访问新功能,而且新版本还将包括错误修复和针对潜在安全问题的修复。 幸运的是,Next.js 通过提供 Codemods (https://nextjs.org/docs/advanced-features/codemods) 使升级变得非常容易。这些是自动更新代码的自动代码转换。

更新依赖

出于同样的原因,保持 Next.js 和 React 版本是真实的很重要; 更新其他依赖项也很重要。 Github 的dependabot (https://github.com/dependabot) 可以在这里真正提供帮助。 它将自动创建具有更新依赖项的拉取请求。 但是,更新依赖项可能会破坏事情,因此在这里进行自动化的端到端测试确实可以挽救生命。

经验教训:使用日志聚合服务

为了确保应用程序正常运行并抢先发现问题,我们发现配置日志聚合服务是绝对必要的。 Vercel 允许您登录并查看日志,但这些是实时流式传输的,不会持久化。 它也不支持配置警报和通知。

一些例外情况可能需要很长时间才能浮出水面。 例如,我们为特定页面配置了 Stale-While-Revalidate。 在某些时候,我们注意到页面没有被刷新并且旧数据正在被提供。 检查 Vercel 日志后,我们发现在页面的后台渲染过程中发生了异常。 通过使用日志聚合服务并配置异常警报,我们可以更快地发现这一点。

日志聚合服务还可用于监控 Vercel 定价计划的限制。 Vercel 的使用页面也为您提供了这方面的见解,但是使用日志聚合服务可以让您在达到某个阈值时添加通知。 预防胜于治疗,尤其是在计费方面。

Vercel 提供了许多与日志聚合服务的开箱即用集成,包括 Datadog、Logtail、Logalert、Sentry 等。

在 Datadog 中查看 Next.js 请求日志
在 Datadog 中查看 Next.js 请求日志(大预览)

延伸阅读

  • “整合”,韦尔塞尔

经验教训:Next.js 的重写功能支持增量采用

除非当前网站存在严重问题,否则不会有很多客户会为重写整个网站而兴奋。 但是,如果您可以从仅重建对 Web Vitals 而言最重要的页面开始呢? 这正是我们为另一位客户所做的。 我们不会重建整个网站,而是只重建对 SEO 和转换最重要的页面。 在这种情况下,产品详细信息和类别页面。 通过使用 Next.js 重建那些,性能大大提高。

Next.js 重写功能对此非常有用。 我们构建了一个新的 Next.js 前端,其中包含目录页面并将其部署到 CDN。 所有其他现有页面都由 Next.js 重写到现有网站。 通过这种方式,您可以以省力或低风险的方式开始享受 Next.js 网站的好处。

延伸阅读

  • “重写”,Next.js 文档

下一步是什么?

当我们发布该项目的第一个版本并开始进行认真的性能测试时,我们对结果感到非常兴奋。 不仅页面响应时间和 Web Vitals 比以前好很多,而且运营成本也只是以前的一小部分。 Next.js 和 JAMStack 通常允许您以最具成本效益的方式进行横向扩展。

从更面向后端的架构切换到 Next.js 之类的架构是一大步。 学习曲线可能非常陡峭,最初,一些团队成员真的觉得自己超出了他们的舒适区。 我们所做的小调整,从本文中吸取的经验教训,确实对此有所帮助。 此外,Next.js 的开发体验也极大地提高了生产力。 开发者反馈周期非常短!

延伸阅读

  • “进入生产阶段”,Next.js 文档