使用 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 文檔