React 19已经到来,它带来了复杂而灵活的新功能、难以置信的优化,以及一种全新的思考应用程序的方式。
但在构建营销网站时,React 19的功能是否过于工程化?或者这些功能是否值得付出努力?
在过去的18个月里,这些功能已经在React Canary和Next.js App Router中慢慢推出。在过去的18个月里,我们一直在文档和营销网站上使用它们。所以我们的团队有一些看法。
让我们来谈谈React 19的新功能,以及它们如何使React开发的未来光明,即使你只是在写一个营销网站。
服务器组件(Server Components)
所以我提到了“一种全新的思考应用程序的方式”。这个新范式的最大部分是服务器组件。你可能听说过它们——它们在React社区引起了一场大地震。如果熟悉,请跳到下一个副标题。否则,这里是一个快速的回顾:
什么是服务器组件?(快速版)
很久以前,当我们编写React时,我们会将React代码发送到客户端,该代码将在客户端生成HTML。现在,我们有两种类型的组件。客户端组件的工作方式与React过去的方式相同——在客户端上。新的服务器组件在服务器上运行并在服务器上生成HTML1。换句话说,我们可以选择哪些代码在客户端运行,哪些代码可以留在服务器上。通常,需要交互性(即,响应用户输入并随时间变化)的东西会被发送到客户端。通常,可以运行一次就再也不需要运行的东西,如或,会留在服务器上。
服务器组件不仅可以让你选择代码运行的位置。服务器组件还让你直接在组件中获取数据。以前,你得运行特定框架的函数,如getServerSideProps或构建某种框架GraphQL数据层。现在呢?简单又方便。你的评论组件需要与你的评论数据库通信吗?把你的评论数据库代码就放在你的评论组件里。
使用服务器组件数据获取还有一个额外的神奇技巧。假设你的评论数据库非常慢。以前,整个页面都必须等待数据库调用完成。现在,如果你把你的评论组件包装在Suspense中,它将在准备好后流式传输到你的客户端。换句话说,用户可以立即开始阅读你的页面,当你的评论数据库最终完成它的工作时,那个评论组件可以稍后弹出。
蜜月期后的服务器组件生活
服务器组件给人的第一印象是难以置信的。当我们第一次实现它们时,我们的页面捆绑包大小立即减少了20%-90%。这很疯狂。但这说得通——我们的营销网站和文档网站大部分是静态内容,像段落标签、标题、突出的代码块和markdown加载器。这些组件不需要响应用户交互。这意味着它们可以留在服务器上。光这一点就足以让服务器组件成为营销网站值得考虑的选项。
话虽如此,服务器组件也要求你攀登陡峭的学习曲线。蜜月期过后,现实是,很难一下子把所有事情都记在脑子里。很难避免混合客户端和服务器组件的陷阱。学习曲线如此陡峭,以至于我不得不写一篇4000字的博客文章 只是让我自己搞清楚。如果你也在攀登这个曲线,你应该看看那篇文章。
不过,既然我们已经攀登了这个曲线,我想关注的是服务器组件现在为我们做了什么。服务器组件是如何改变我们编写网站的?
服务器组件的未来是光明的
现在我们已经将营销和文档网站迁移到服务器组件,我们可以编写以前不可能的新组件。
很早以前,我们就利用了服务器组件的新数据获取超能力。例如:我们的整个文档页面必须等待更改日志侧边栏从我们的数据库中获取数据,即使该侧边栏通常一开始就在屏幕外。
export default async function Page() {
const changelogData = await getChangelogData()
/* 所有这些都必须等待changelogData加载 */
return (
<Layout>
<Sidebar>
{/* 其他侧边栏内容 */}
<Changelog data={changelogData} />
</Sidebar>
{/* 其他页面内容 */}
</Layout>
)
}
现在,不是让整个页面等待更改日志数据获取,我们把那个获取移到了组件中,并将该组件包装在Suspense中。
import { Suspense } from 'react';
/* 我们已经把数据获取移到了更改日志组件中 */
async function ChangelogWithDataFetching() {
const changelogData = await getChangelogData()
return <Changelog data={changelogData} />
}
/*
...并包装了那个组件在Suspense中。
用户可以立即看到页面,而更改日志组件正在加载
*/
export default function Page() {
return (
<Layout>
<Sidebar>
{/* 其他侧边栏内容 */}
<Suspense fallback={<ChangelogPlaceholder />}>
<ChangelogWithDataFetching />
</Suspense>
</Sidebar>
{/* 其他页面内容 */}
</Layout>
)
}
这种优化只是我们服务器组件之旅的开始。随着我们不断构建,我们意识到有些组件,比如我们营销页面上的定价组件,需要它们自己的数据库文档。以前,如果我们在页面上添加了定价组件,我们还必须在页面上或其他地方的API端点添加定价组件的文档检索。现在,由于定价组件获取自己的数据,我们可以直接放入组件并继续前进。服务器端数据获取过去是与组件分开的;现在它在同一个地方。
对一些人来说,React鼓励我们将逻辑、标记和样式都放在同一个文件中是有争议的。对一些人来说,那种单文件方法违反了我们许多程序员所珍视的“关注点分离”原则。然而,正如Cristiano Rastelli 用他们标志性的图表论证的那样,React组件仍然在“分离关注点”,但通过上下文而不是技术。React组件在它们最好的时候,可以让你在一个地方看到它们所有的东西。将服务器端数据添加到我们的组件中扩展了这一点,并为Rastelli的图表增加了另一层:
一旦这种思考方式变得根深蒂固,我们开始思考我们以前绝不会写的组件。以我们的新VideoGlossaryHoverCard组件为例。我们经常不得不谈论一些普通人不需要立即知道的视频术语,比如HDCP和LL-HLS。我们想在悬停它们时添加这些术语的定义。这是完成组件的样子:
这是它的内部工作原理。首先,我写了一个VideoGlossaryHoverCard服务器组件:给定一个术语链接,查询数据库中的该术语并在悬停卡片中显示它。(难道不甜蜜吗,我们可以在一个地方完成所有这些工作?)。接下来,我在我们的链接组件中添加了一个条件:如果你检测到一个视频术语链接,显示视频术语悬停卡片。(并且悬停卡片包装在Suspense中,以确保这个组件不会拖慢主要体验的速度。)
import 'server-only';
import { Suspense } from 'react';
import NextLink from 'next/link';
import VideoGlossaryHoverCard from './VideoGlossaryHoverCard';
export default function Link({ href, ...rest }) {
const linkComponent = <BaseNextLink href={href} {...rest} ref={ref} />;
if (href?.includes('/video-glossary/')) {
return (
<Suspense fallback={linkComponent}>
<VideoGlossaryHoverCard href={href}>{linkComponent}</VideoGlossaryHoverCard>
</Suspense>
);
}
return linkComponent;
}
让我强调一下,如果在这个之前,这个组件会是一个灾难性的痛苦。如果我们想在客户端执行这项工作,我们不得不编写一个完整的API端点,然后在VideoGlossaryHoverCard中的useEffect中查询该端点,并完成所有相关的样板工作。我们都已经做过,我们都厌倦了。或者,想象一下在页面级别在服务器上完成这项工作!我们必须浏览我们的标记,并检测页面上每一个视频术语,然后对我们的数据库进行查询以获取所有这些术语,然后向下传递属性将这些术语传递到链接……然后……呃。不。
一个简单的const glossaryTerm = await getGlossaryTerm(href)。一个简单的组件。所有的样板工作都很少,都在一个地方。我喜欢它。一旦你开始用服务器组件思考,你就再也回不去了。
动作
简而言之,如果你以一种特殊的方式2调用一个异步函数,你将免费获得三样东西。首先(也是最重要的),你可以获得一个isPending状态。不再需要自己管理setIsPending。其次,动作与React的原生错误边界特性相关联。这意味着当出现问题时,你可以轻松地向用户显示错误状态。最后,动作与一个名为useOptimistic的新hook一起工作,它让你在操作完成之前向用户显示操作的结果。例如,如果你点击一个🩷按钮,你可以在API成功响应之前显示一个可爱的💖动画。
当React为你处理所有这些时,如此多的样板工作就消失了。如果你想看看,可以查看React在他们的博客上提供的优秀示例。但动作只是样板工作融化冰山的一角。我们发现对于营销网站来说,真正改变游戏规则的是服务器动作。
用服务器动作整合一切
服务器动作为样板工作融化冰山增添了最后一层蛋糕。(我承认:这些类比已经失控了。)服务器动作也融化了编写API端点的样板工作。
让我们想象一下,你想在你的网站上构建一个小的“联系支持”框。现在,你得编写一个后端处理评论的端点。然后你需要为你的网站编写一个小表单,带有一个文本区域;当那个表单提交时,你需要将那些数据POST到你的新端点,并管理所有那些待处理和乐观的UI和错误处理。
但是等等!动作管理待处理和乐观的UI和错误处理……这只留下了那个烦人的API端点。这就是服务器动作的用武之地。与其编写一个API端点,你编写一个服务器动作。然后,与其通过POST使用fetch与那个API端点交互,你就像调用一个函数一样调用你的服务器动作。
让我们来看一个服务器动作的实际例子。通过在文件顶部放置“use server”,你告诉你的打包器“嘿,将这个变成一个服务器动作。将这个变成一个我的客户端可以POST的端点。”
"use server"
// "use server"指令告诉React使其成为自己的捆绑包
// 可以作为服务器动作调用。
// 在幕后,React为你将其变成自己的API端点。
export async function saveContactFormAction(formData) {
const name = formData.get('name')
const text = formData.get('text')
try {
await saveToDatabase(name, text)
return { message: 'Text saved successfully' }
} catch (error) {
return { message: error.message }
}
}
这与API端点不太不同……但一旦我们到达前端,魔法就开始了。我们可以直接导入那个服务器动作并将其传递给表单的action属性。然后,为了获得我提到的轻松isPending状态,我们利用React的新useActionState钩子。
"use client"
import { useActionState } from "react"
import saveContactFormAction from "./actions"
const initialState = { message: '' }
export default function ContactForm() {
const [response, formAction, isPending] = useActionState(saveContactForm)
return (
<form action={formAction}>
<label>Name: <input type="text" name="name" required /></label>
<label>Text: <textarea name="text" required /></label>
<button type="submit" disabled={isPending}>
{isPending ? "Submitting..." : "Submit"}
</button>
<p aria-live="polite">{response?.message}</p>
</form>
)
没有setIsLoading。没有fetch('/api/contact', { method: 'POST' }).只是调用一个函数并获得所有其他东西免费。这只是一个简单的例子;还有更多,请务必查看这个Next.js文档。
服务器动作的未来似乎是光明的
我们已经使用服务器组件比服务器动作更长的时间了。在服务器组件一年多之后,我们开始看到像我们之前谈到的视频术语悬停框这样的酷炫新模式。我认为我们也看到服务器动作开始发生这种情况。
在上面的例子中,我们用一个包含成功和消息的对象响应动作。你知道还有什么是对象吗?服务器组件只是对象。如果我们用服务器组件响应我们的服务器动作呢?例如,这是我们上周在更改日志中添加的一个酷炫的分页功能:
这个新的分页功能在我们的更改日志中非常容易用服务器动作编写
这是背后令人难以置信的简单动作:
'use server'
import { getPosts } from './data'
import Posts from './posts'
export async function getPostPageAction(formData) {
const page = formData.get('page')
const posts = await getPosts(page)
return <Posts posts={posts} />
}
然后我们在更改日志的底部添加了一个按钮。当动作响应时,按钮被服务器动作中的新帖子替换:
'use client';
import { useActionState } from 'react';
import Button from 'components/button';
import { getPostPageAction } from './actions';
const defaultComponent = null;
export default function LoadPostPageButton({ page }) {
const [component, formAction, isPending] = useActionState(getPostPageAction, defaultComponent);
return component || (
<form action={formAction}>
<input type="hidden" name="page" value={page} />
<Button cta disabled={isPending} type="submit">
{isPending ? 'Loading...' : 'Load more'}
</Button>
</form>
);
}
这就是编写这种分页交互的简单性。没有客户端数据管理。几乎没有代码被运送到客户端。没有API端点。没有待处理状态管理。只是一个很棒的新的用户体验。
React的未来是光明的
React 19还有很多东西即将到来,这将改善我们作为Web开发人员的生活。React正在为文档元数据、样式表、异步脚本标签和预加载其他资源添加组件级支持。早些时候,我们谈到了React在让你在同一个地方查看有关组件的所有内容时表现最佳。这些新功能让你可以为组件的更多相关功能进行协作。
还有一些更高级的功能,我真的很兴奋!更少和更好的水合错误,上下文和转发引用的更好的开发体验,甚至不要开始谈论一流的Web组件支持——我们非常兴奋,Dylan写了一篇完整的博客文章。
我可以继续,但是,哦,看看字数。也许是时候开始总结了。
技术市场研究中有一个概念叫做高德纳炒作周期。它展示了一项新技术进入市场时的爆炸性增长,因为我们试图将其应用于世界上的每一个问题。然后,随着我们对这项技术的熟悉,首先我们对它不能解决世界上所有的问题感到失望。然后,随着这项技术成为老朋友,我们开始看到技术的本质:对于一组特定问题来说是一个非常好用的工具。
特别是对于服务器组件,蜜月期非常明亮,它的捆绑包更小,新的数据加载也很时尚。然后,我们经历了幻灭的低谷,因为我们意识到服务器组件的开发体验具有挑战性。
现在呢?我们正在学习如何思考和教授这些新概念。我们开始看到它们真正的潜力。我们作为Web开发人员的时间这么多都花在将数据发送到客户端,以及将数据从客户端发送回服务器上。我们工作中这么多都是围绕这两个任务的样板工作。React 19看到了这一点,并融化了样板工作。React 19是一个伟大的工具,它使服务器和客户端之间的通信变得更容易,所以我们可以更有生产力。