如何從 jQuery 遷移到 Next.js
已發表: 2022-03-10這篇文章得到了我們在 Netlify 的親愛的朋友的大力支持,他們是來自世界各地的一群令人難以置信的人才,並為 Web 開發人員提供了一個可以提高生產力的平台。 謝謝!
當 jQuery 在 2006 年出現時,許多開發人員和組織開始在他們的項目中採用它。 擴展和操作該庫提供的 DOM的可能性很大,而且我們還有許多插件可以為我們的頁面添加行為,以防我們需要執行 jQuery 主庫不支持的任務。 它為開發人員簡化了很多工作,並且在那一刻,它使 JavaScript 成為創建 Web 應用程序或單頁應用程序的強大語言。
jQuery 流行的結果在今天仍然是可以衡量的:世界上幾乎 80% 的最受歡迎的網站仍在使用它。 jQuery 如此受歡迎的一些原因是:
- 它支持 DOM 操作。
- 它提供 CSS 操作。
- 在所有網絡瀏覽器上都一樣。
- 它包裝了 HTML 事件方法。
- 輕鬆創建 AJAX 調用。
- 易於使用的效果和動畫。
多年來,JavaScript 發生了很大變化,並添加了一些我們過去沒有的特性。 隨著 ECMAScript 的重新定義和發展,jQuery 提供的一些功能被添加到標準 JavaScript 功能中,並被所有 Web 瀏覽器支持。 隨著這種情況的發生,不再需要jQuery 提供的一些行為,因為我們可以使用純 JavaScript 做同樣的事情。
另一方面,一種新的思考和設計用戶界面的方式開始出現。 React、Angular 或 Vue 等框架允許開發人員基於可重用的功能組件創建 Web 應用程序。 React,即,與“虛擬 DOM”一起工作,它是內存中的 DOM 表示,而jQuery 直接與 DOM 交互,以一種性能較低的方式。 此外,React 提供了很酷的特性來促進某些特性的開發,例如狀態管理。 隨著這種新方法和單頁應用程序開始獲得普及,許多開發人員開始在他們的 Web 應用程序項目中使用 React。
前端開發的發展甚至更多,在其他框架之上創建了框架。 例如,Next.js 就是這種情況。 您可能知道,它是一個開源 React 框架,提供生成靜態頁面、創建服務器端渲染頁面以及在同一個應用程序中組合這兩種類型的功能。 它還允許在同一個應用程序中創建無服務器 API。
有一個奇怪的場景:儘管這些前端框架這些年來越來越流行,jQuery 仍然被絕大多數網頁所採用。 發生這種情況的原因之一是使用 WordPress 的網站比例非常高,並且jQuery 包含在 CMS 中。 另一個原因是一些庫,比如 Bootstrap,依賴於 jQuery,並且有一些現成的模板使用它和它的插件。
但是,使用 jQuery 的網站數量如此之多的另一個原因是將完整的 Web 應用程序遷移到新框架的成本。 這並不容易,也不便宜,而且很耗時。 但是,最終,使用新的工具和技術會帶來很多好處:更廣泛的支持、社區幫助、更好的開發人員體驗以及讓人們更容易參與項目。
在很多場景中,我們不需要(或不想)遵循 React 或 Next.js 等框架強加給我們的架構,這沒關係。 然而,jQuery 是一個包含許多不再需要的代碼和功能的庫。 jQuery 提供的許多功能都可以使用現代 JavaScript 原生函數來完成,而且可能以更高效的方式實現。
讓我們討論一下如何停止使用 jQuery 並將我們的網站遷移到 React 或 Next.js Web 應用程序中。
定義遷移策略
我們需要圖書館嗎?
根據我們的 Web 應用程序的特性,我們甚至可能遇到不需要框架的情況。 如前所述,最新的 Web 標準版本包含了幾個 jQuery 特性(或至少一個非常相似的特性)。 所以,考慮到:
- jQuery 中的
$(selector)
模式可以替換為querySelectorAll()
。
而不是這樣做:
$("#someId");
我們可以做的:
document.querySelectorAll("#someId");
- 如果我們想操作 CSS 類,我們現在有屬性
Element.classList
。
而不是這樣做:
$(selector).addClass(className);
我們可以做的:
element.classList.add(className);
- 許多動畫可以直接使用 CSS 來完成,而不是實現 JavaScript。
而不是這樣做:
$(selector).fadeIn();
我們可以做的:
element.classList.add('show'); element.classList.remove('hide');
並應用一些 CSS 樣式:
.show { transition: opacity 400ms; } .hide { opacity: 0; }
- 如果我們想處理事件,我們現在有 addEventListener 函數。
而不是這樣做:
$(selector).on(eventName, eventHandler);
我們可以做的:
element.addEventListener(eventName, eventHandler);
- 除了使用 jQuery Ajax,我們可以使用
XMLHttpRequest
。
而不是這樣做:
$.ajax({ type: 'POST', url: '/the-url', data: data });
我們可以做的:
var request = new XMLHttpRequest(); request.open('POST', '/the-url', true); request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); request.send(data);
有關更多詳細信息,您可以查看這些 Vanilla JavaScript 代碼片段。
識別組件
如果我們在應用程序中使用 jQuery,我們應該有一些在 Web 服務器上生成的 HTML 內容,以及向頁面添加交互性的 JavaScript 代碼。 我們可能會在頁面加載時添加事件處理程序,以便在事件發生時操縱 DOM,可能會更新 CSS 或元素的樣式。 我們還可以調用後端服務來執行操作,這可能會影響頁面的 DOM,甚至重新加載它。
這個想法是重構頁面中的 JavaScript 代碼並構建 React 組件。 這將幫助我們加入相關代碼並組合元素,這些元素將成為更大組合的一部分。 通過這樣做,我們還可以更好地處理應用程序的狀態。 分析我們應用程序的前端,我們應該將其劃分為專用於某個任務的部分,以便我們可以基於它創建組件。
如果我們有一個按鈕:
<button>Click</button>
使用以下邏輯:
var $btnAction = $("#btn-action"); $btnAction.on("click", function() { alert("Button was clicked"); });
我們可以將其遷移到 React 組件:
import React from 'react'; function ButtonComponent() { let handleButtonClick = () => { alert('Button clicked!') } return <button onClick={handleButtonClick}>Click</button> }
但是我們還應該評估遷移過程將如何完成,因為我們的應用程序正在工作和使用,我們不想影響它(或者至少盡可能少地影響它)。
良好的遷移
一個好的遷移是將應用程序的所有部分完全遷移到新的框架或技術。 這將是我們應用程序的理想方案,因為我們將保持所有部分同步,並且我們將使用統一的工具和唯一的參考版本。
良好且完整的遷移通常包括對我們應用程序代碼的完全重寫,這是有道理的。 如果我們從頭開始構建應用程序,我們就有可能決定使用新代碼的方向。 我們可以對現有系統和工作流程使用全新的觀點,並使用我們目前所擁有的知識創建一個全新的應用程序,比我們第一次創建 Web 應用程序時所擁有的更完整。
但是完全重寫有一些問題。 首先,它需要很多時間。 應用程序越大,我們需要重寫它的時間就越多。 另一個問題是它需要的工作量和開發人員的數量。 而且,如果我們不進行漸進式遷移,我們必須考慮我們的應用程序將在多長時間內不可用。
通常,可以通過小型項目、不經常更改的項目或對我們的業務不那麼重要的應用程序來完成完整的重寫。
快速遷移
另一種方法是將應用程序分成多個部分。 我們逐部分遷移應用程序,並在它們準備好時發布這些部分。 因此,我們遷移了可供用戶使用的部分應用程序,並與我們現有的生產應用程序共存。
通過這種逐步遷移,我們以更快的方式向用戶交付項目的分離功能,因為我們不必等待重新編寫完整的應用程序。 我們還可以更快地從用戶那裡獲得反饋,這使我們能夠更早地檢測到錯誤或問題。
但是逐漸的遷移驅使我們擁有不同的工具、庫、依賴項和框架。 或者我們甚至可能不得不支持來自同一個工具的不同版本。 這種擴展支持可能會給我們的應用程序帶來衝突。
如果我們在全局範圍內應用策略,我們甚至可能會遇到問題,因為每個遷移的部分都可以以不同的方式工作,但會受到為我們的系統設置全局參數的代碼的影響。 這方面的一個例子是使用 CSS 樣式的級聯邏輯。
想像一下,我們在 Web 應用程序中使用不同版本的 jQuery,因為我們將新版本的功能添加到後來創建的模塊中。 將我們所有的應用程序遷移到更新版本的 jQuery 會有多複雜? 現在,想像一下同樣的場景,但是遷移到一個完全不同的框架,比如 Next.js。 這可能很複雜。
科學怪人遷移
Denys Mishunov 在 Smashing Magazine 上寫了一篇文章,提出了這兩種遷移想法的替代方案,試圖充分利用前兩種方法:科學怪人遷移。 它基於兩個主要組件的遷移過程:微服務和 Web 組件。
遷移過程包含一系列要遵循的步驟:
1. 識別微服務
根據我們的應用程序代碼,我們應該將其分成獨立的部分,專門用於一項小工作。 如果我們正在考慮使用 React 或 Next.js,我們可以將微服務的概念與我們擁有的不同組件聯繫起來。
讓我們以購物清單應用程序為例。 我們有一個要購買的東西的清單,以及一個向清單中添加更多東西的輸入。 因此,如果我們想將我們的應用程序分成小部分,我們可以考慮一個“項目列表”組件和一個“添加項目”。 這樣做,我們可以將與這些部分中的每一個相關的功能和標記分離到不同的 React 組件中。
為了證實組件是獨立的,我們應該能夠從應用程序中刪除其中一個,而其他的不應該受到影響。 如果我們在從服務中刪除標記和功能時遇到錯誤,則說明我們沒有正確識別組件,或者我們需要重構代碼的工作方式。
2. 允許主機到外星人訪問
“主機”是我們現有的應用程序。 “外星人”是我們將使用新框架開始創建的一個。 兩者都應該獨立工作,但我們應該提供從 Host 到 Alien 的訪問。 我們應該能夠在不破壞另一個應用程序的情況下部署這兩個應用程序中的任何一個,但保持它們之間的通信。
3. 編寫外星組件
使用新框架將 Host 應用程序中的服務重新寫入 Alien 應用程序。 組件應該遵循我們之前提到的相同的獨立性原則。
讓我們回到購物清單的例子。 我們確定了一個“添加項目”組件。 使用 jQuery,組件的標記將如下所示:
<input class="new-item" />
將項目添加到列表的 JavaScript/jQuery 代碼將如下所示:
var ENTER_KEY = 13; $('.new-item').on('keyup', function (e) { var $input = $(e.target); var val = $input.val().trim(); if (e.which !== ENTER_KEY || !val) { return; } // code to add the item to the list $input.val(''); });
取而代之的是,我們可以創建一個AddItem
React 組件:
import React from 'react' function AddItemInput({ defaultText }) { let [text, setText] = useState(defaultText) let handleSubmit = e => { e.preventDefault() if (e.which === 13) { setText(e.target.value.trim()) } } return <input type="text" value={text} onChange={(e) => setText(e.target.value)} onKeyDown={handleSubmit} /> }
4. 圍繞 Alien 服務編寫 Web Component Wrapper
創建一個包裝器組件,用於導入我們剛剛創建的 Alien 服務並呈現它。 這個想法是在 Host 應用程序和 Alien 應用程序之間建立一座橋樑。 請記住,我們可能需要一個包捆綁器來生成適用於當前應用程序的 JavaScript 代碼,因為我們需要復制新的 React 組件並使其工作。
按照購物清單示例,我們可以在 Host 項目中創建一個AddItem-wrapper.js
文件。 該文件將包含包裝我們已經創建的AddItem
組件的代碼,並使用它創建一個自定義元素:
import React from "../alien/node_modules/react"; import ReactDOM from "../alien/node_modules/react-dom"; import AddItem from "../alien/src/components/AddItem"; class FrankensteinWrapper extends HTMLElement { connectedCallback() { const appWrapper = document.createElement("div"); appWrapper.classList.add("grocerylistapp"); ... ReactDOM.render( <HeaderApp />, appWrapper ); … } } customElements.define("frankenstein-add-item-wrapper", FrankensteinWrapper);
我們應該從 Alien 應用程序文件夾中帶來必要的節點模塊和組件,因為我們需要導入它們以使組件工作。
5. 用 Web 組件替換主機服務
這個包裝器組件將替換 Host 應用程序中的包裝器組件,我們將開始使用它。 因此,生產中的應用程序將是 Host 組件和 Alien 包裝組件的混合體。
在我們的示例主機應用程序中,我們應該替換:
<input class="new-item" />
和
<frankenstein-add-item-wrapper></frankenstein-add-item-wrapper> ... <script type="module" src="js/AddItem-wrapper.js"></script>
6. 沖洗並重複
對每個已識別的微服務執行步驟 3、4 和 5。
7.切換到外星人
Host 現在是一個包裝器組件的集合,其中包括我們在 Alien 應用程序上創建的所有 Web 組件。 當我們轉換了所有已識別的微服務時,我們可以說 Alien 應用程序已完成並且所有服務都已遷移。 我們現在只需要將我們的用戶指向 Alien 應用程序。
Frankenstein Migration 方法結合了 Good 和 Fast 方法。 我們遷移完整的應用程序,但在完成後發布不同的組件。 因此,它們可以更快地使用並由用戶在生產中進行評估。
但是,我們必須考慮到,我們正在使用這種方法做一些過度工作。 如果我們想使用我們為 Alien 應用程序創建的組件,我們必須創建一個包裝器組件以包含在 Host 應用程序中。 這使我們花時間為這些包裝元素開發代碼。 此外,通過在我們的主機應用程序中使用它們,我們複製了代碼和依賴項的包含,並添加了會影響我們應用程序性能的代碼。
扼殺者應用
我們可以採取的另一種方法是傳統應用程序絞殺。 我們識別現有 Web 應用程序的邊緣,每當我們需要向應用程序添加功能時,我們都會使用更新的框架來完成,直到舊系統被“扼殺”。 這種方法可以幫助我們降低遷移應用程序時可以試驗的潛在風險。
要遵循這種方法,我們需要識別不同的組件,就像我們在科學怪人遷移中所做的那樣。 一旦我們將我們的應用程序分成不同的相關命令式代碼,我們將它們包裝在新的 React 組件中。 我們不添加任何額外的行為,我們只是創建渲染我們現有內容的 React 組件。
讓我們看一個示例以進行更多說明。 假設我們的應用程序中有這個 HTML 代碼:
<div class="accordion"> <div class="accordion-panel"> <h3 class="accordion-header">Item 1</h3> <div class="accordion-body">Text 1</div> </div> <div class="accordion-panel"> <h3 class="accordion-header">Item 2</h3> <div class="accordion-body">Text 2</div> </div> <div class="accordion-panel"> <h3 class="accordion-header">Item 3</h3> <div class="accordion-body">Text 3</div> </div>> </div>
還有這段 JavaScript 代碼(我們已經用新的 JavaScript 標準特性替換了 jQuery 函數)。
const accordions = document.querySelectorAll(".accordion"); for (const accordion of accordions) { const panels = accordion.querySelectorAll(".accordion-panel"); for (const panel of panels) { const head = panel.querySelector(".accordion-header"); head.addEventListener('click', () => { for (const otherPanel of panels) { if (otherPanel !== panel) { otherPanel.classList.remove('accordion-expanded'); } } panel.classList.toggle('accordion-expanded'); }); } }
這是 JavaScript 的accordion
組件的常見實現。 由於我們想在這裡介紹 React,我們需要用一個新的 React 組件包裝我們現有的代碼:
function Accordions() { useEffect(() => { const accordions = document.querySelectorAll(".accordion") for (const accordion of accordions) { const panels = accordion.querySelectorAll(".accordion-panel") for (const panel of panels) { const head = panel.querySelector(".accordion-header") head.addEventListener("click", () => { for (const otherPanel of panels) { if (otherPanel !== panel) { otherPanel.classList.remove("accordion-expanded") } } panel.classList.toggle("accordion-expanded") }); } } }, []) return null } ReactDOM.render(<Accordions />, document.createElement("div"))
該組件沒有添加任何新的行為或功能。 我們使用useEffect
是因為該組件已安裝在文檔中。 這就是函數返回 null 的原因,因為鉤子不需要返回組件。
因此,我們沒有向現有應用程序添加任何新功能,但我們在不改變其行為的情況下引入了 React。 從現在開始,每當我們向代碼添加新功能或更改時,我們都將使用更新的選定框架來完成。
客戶端渲染、服務器端渲染還是靜態生成?
Next.js 讓我們可以選擇如何呈現 Web 應用程序的每個頁面。 我們可以使用 React 已經為我們提供的客戶端渲染直接在用戶的瀏覽器中生成內容。 或者,我們可以使用服務器端渲染在服務器中渲染頁面內容。 最後,我們可以在構建時使用靜態生成來創建頁面內容。
在我們的應用程序中,在開始與任何 JavaScript 庫或框架交互之前,我們應該在頁面加載時加載和渲染代碼。 我們可能正在使用服務器端渲染編程語言或技術,例如 ASP.NET、PHP 或 Node.js。 我們可以利用 Next.js 的特性,將我們當前的渲染方法替換為Next.js 服務器端渲染方法。 這樣做,我們將所有行為保留在同一個項目中,該項目在我們選擇的框架的保護下工作。 此外,我們將主頁和 React 組件的邏輯保留在為我們的頁面生成所有需要的內容的相同代碼中。
讓我們以儀表板頁面為例。 我們可以在加載時在服務器中生成頁面的所有初始標記,而不必在用戶的 Web 瀏覽器中使用 React 生成它。
const DashboardPage = ({ user }) => { return ( <div> <h2>{user.name}</h2> // User data </div> ) } export const getServerSideProps = async ({ req, res, params }) => { return { props: { user: getUser(), }, } }, }) export default DashboardPage
如果我們在頁面加載時呈現的標記是可預測的並且基於我們可以在構建時檢索的數據,那麼靜態生成將是一個不錯的選擇。 在構建時生成靜態資產將使我們的應用程序更快、更安全、可擴展且更易於維護。 而且,如果我們需要在應用程序的頁面上生成動態內容,我們可以使用 React 的客戶端渲染從服務或數據源中檢索信息。
想像一下,我們有一個博客站點,其中包含許多博客文章。 如果我們使用靜態生成,我們可以在 Next.js 應用程序中創建一個通用的[blog-slug].js
文件,並添加以下代碼,我們將在構建時為我們的博客文章生成所有靜態頁面。
export const getStaticPaths = async () => { const blogPosts = await getBlogPosts() const paths = blogPosts.map(({ slug }) => ({ params: { slug, }, })) return { paths, fallback: false, } } export const getStaticProps = async ({ params }) => { const { slug } = params const blogPost = await getBlogPostBySlug(slug) return { props: { data: JSON.parse(JSON.stringify(blogPost)), }, } }
使用 API 路由創建 API
Next.js 提供的強大功能之一是創建 API 路由的可能性。 有了它們,我們可以使用 Node.js 創建自己的無服務器函數。 我們還可以安裝 NPM 包來擴展功能。 一個很酷的事情是我們的 API 將與我們的前端留在同一個項目/應用程序中,所以我們不會有任何 CORS 問題。
如果我們使用 jQuery AJAX 功能維護從我們的 Web 應用程序調用的 API,我們可以使用API Routes替換它們。 這樣做,我們會將應用程序的所有代碼庫保存在同一個存儲庫中,我們將使應用程序的部署更簡單。 如果我們使用第三方服務,我們可以使用 API 路由來“屏蔽”外部 URL。
我們可以有一個 API Route /pages/api/get/[id].js
來返回我們在頁面上使用的數據。
export default async (req, res) => { const { id } = req.query try { const data = getData(id) res.status(200).json(data) } catch (e) { res.status(500).json({ error: e.message }) } }
並從我們頁面的代碼中調用它。
const res = await fetch(`/api/get/${id}`, { method: 'GET', }) if (res.status === 200) { // Do something } else { console.error(await res.text()) }
部署到 Netlify
Netlify 是一個完整的平台,可用於自動化、管理、構建、測試、部署和託管 Web 應用程序。 它具有許多使現代 Web 應用程序開發更容易和更快的功能。 Netlify 的一些亮點是:
- 全球CDN託管平台,
- 無服務器功能支持,
- 基於 Github Pull Requests 部署預覽,
- 網絡掛鉤,
- 即時回滾,
- 基於角色的訪問控制。
Netlify 是管理和託管 Next.js 應用程序的絕佳平台,使用它部署 Web 應用程序非常簡單。
首先,我們需要在 Git 存儲庫中跟踪 Next.js 應用程序代碼。 Netlify 連接到 GitHub(或者我們更喜歡的 Git 平台),並且每當向分支(提交或拉取請求)引入更改時,都會觸發自動“構建和部署”任務。
一旦我們有了一個包含應用程序代碼的 Git 存儲庫,我們就需要為它創建一個“Netlify 站點”。 為此,我們有兩種選擇:
- 使用 Netlify CLI
在我們安裝 CLI (npm install -g netlify-cli
) 並登錄到我們的 Netlify 帳戶 (ntl login
) 後,我們可以轉到應用程序的根目錄,運行ntl init
並按照步驟操作。 - 使用 Netlify 網絡應用程序
我們應該去 https://app.netlify.com/start。 連接到我們的 Git 提供程序,從列表中選擇我們應用程序的存儲庫,配置一些構建選項,然後進行部署。
對於這兩種方法,我們都必須考慮到我們的構建命令將是next build
並且我們要部署的目錄是out
。
最後,自動安裝 Essential Next.js 插件,這將允許我們部署和使用 API 路由、動態路由和預覽模式。 就是這樣,我們的 Next.js 應用程序在快速穩定的 CDN 託管服務中啟動並運行。
結論
在本文中,我們使用 jQuery 庫評估了網站,並將它們與新的前端框架(如 React 和 Next.js)進行了比較。 我們定義瞭如何開始遷移,以防它使我們受益,遷移到更新的工具。 我們評估了不同的遷移策略,並看到了一些可以遷移到 Next.js Web 應用程序項目的場景示例。 最後,我們看到瞭如何將 Next.js 應用程序部署到 Netlify 並啟動並運行它。
進一步閱讀和資源
- 科學怪人遷移:與框架無關的方法
- 從 GitHub.com 前端移除 jQuery
- Next.js 入門
- 如何將 Next.js 站點部署到 Netlify
- Netlify 博客中的 Next.js 文章