如何从 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 文章