目标是希望写出符合规范的组件。
先从文档开始吧,具体的心得等开发一段时间后再整理
文档给我们介绍构建混合应用程序需要了解的概念。
原文:Building Your Application: Rendering | Next.js (nextjs.org)
渲染(Rendering)
React 和 Next.js 允许您创建混合式 Web 应用程序,组件可以在服务器端或客户端进行渲染。 我们需要了解这些渲染环境、策略和运行时之间的区别。
需要了解的三个概念:
- 程序代码可以在以下环境中执行:服务器和客户端
- 当用户访问应用程序或与应用程序交互时的生命周期:请求-响应
- 服务器和客户端代码的网络边界
渲染环境
客户端和服务器
- 客户端是指用户设备上的浏览器,它向服务器发送应用程序代码请求。然后,它将服务器的响应转换为用户界面。
- 服务器是指服务器上应用程序代码、接收客户端请求并发回适当响应。
请求-响应生命周期(Request-Response Lifecycle)
- User Action: 用户操作:用户与web应用程序进行交互。这可以是点击链接、提交表格,或者直接在浏览器的地址栏中键入URL。
- HTTP Request: HTTP请求:客户端向服务器发送一个HTTP请求,其中包含有关正在请求的资源、正在使用的方法(如GET、POST)以及必要时的其他数据的必要信息。
- Server: 服务器:服务器处理请求并使用适当的资源进行响应。这个过程可能需要几个步骤,如路由、获取数据等。
- HTTP Response: HTTP响应:处理完请求后,服务器会向客户端发回HTTP响应。此响应包含一个状态代码(它告诉客户端请求是否成功)和请求的资源(例如HTML、CSS、JavaScript、静态资产等)。
- Client: 客户端:客户端解析资源以呈现用户界面。
- User Action: 用户操作:一旦呈现了用户界面,用户就可以与之交互,整个过程就会重新开始。 构建混合web应用程序的一个主要部分是决定如何在生命周期中划分工作,以及将网络边界放置在哪里。
到这告诉我们从用户点击或输入到页面呈现的整个过程中发生的事情,以及涉及的组件和技术。为后面定位组件属于客户端还是服务端做准备。
网络边界(Network Boundary)
网络边界是一个概念上的分界线,帮助我们区分这些不同的环境。例如,当我们在浏览器中点击一个按钮并发送请求到服务器时,这个请求就跨过了客户端和服务器的网络边界。
- 我们可以根据需要决定哪些功能在客户端执行,哪些在服务器执行。这种选择定义了客户端和服务器的网络边界。这样做的目的是为了优化性能、用户体验和数据安全性。
- 为了更有效地管理代码和执行流程,将工作分为两个部分:一部分是在服务器上执行的(称为服务器模块图),另一部分是在客户端执行的(称为客户端模块图)。模块图是一种描述应用程序中各个部分如何相互连接和依赖的关系图。
- 其中,"use client"约定是告诉React某些功能只在客户端执行。同样地,"use server"约定则是告诉React某些计算在服务器上执行。这些约定有助于保持代码的清晰和组织性。
构建混合应用程序(Building Hybrid Applications)
- 单向代码流(Unidirectional Code Flow) :指的是在应用程序中,代码的执行和数据的流动是单向的。在响应过程中,代码和数据从服务器流向客户端,而不是在两个方向上来回流动。
- 网络边界(Network Boundary) :指的是在客户端和服务器之间进行数据传输时需要遵守的规则和限制。这通常涉及到网络请求和响应的格式、安全性、性能等方面的考虑。
- 客户端(Client)和服务器(Server) :在客户端-服务器架构中,客户端是发起请求并接收响应的一方,而服务器是处理请求并发送响应的一方。
- 组件树(Component Tree) :在前端开发中,组件树指的是由多个组件按照层级关系组成的结构。每个组件都可能有子组件,形成一个树状结构。
建议我们采用单向代码流的思想来处理服务器和客户端之间的交互。应该首先考虑在服务器上执行哪些操作,然后将结果发送到客户端,并在客户端上进行渲染和交互。这种方法有助于简化代码结构,提高应用程序的可维护性和性能。
在客户端和服务器之间进行数据传输时需要遵守的规则和限制。这有助于确保应用程序的安全性和稳定性。
服务器组件(Server Components)
首先介绍了三种不同的服务器渲染策略和服务端渲染的好处
服务器呈现的好处
- 数据获取:服务器组件允许您将数据获取移动到离数据源更近的服务器。这可以通过减少提取渲染所需数据所需的时间以及客户端需要发出的请求数量来提高性能。
- 安全性:服务器组件允许您在服务器上保留敏感数据和逻辑,如令牌和API密钥,而不会将它们暴露给客户端。
- 缓存:通过在服务器上渲染,可以缓存结果,并在后续请求和用户之间重用。这可以通过减少每个请求的渲染和数据提取量来提高性能并降低成本。
- 流式处理:服务器组件允许您将渲染工作拆分为块,并在它们准备就绪时将它们流式处理到客户端。这允许用户更早地查看页面的部分内容,而无需等待整个页面在服务器上呈现。
在Next.js中使用服务器组件
默认情况下,Next.js使用服务器组件。自己选择使用客户端组件。 这是一种特殊类型的React组件,它在服务器上执行并渲染,而不是在用户的浏览器上。服务器组件有助于减少发送到客户端的代码量,提高首屏加载速度。
服务器组件是如何呈现的?
- React的API :使用React的API,用于构建和管理用户界面,将React组件转换成HTML、CSS和JavaScript的过程,以便在浏览器中显示。React服务器组件有效载荷(React Server Component Payload, RSC Payload) :这是一个紧凑的二进制表示,包含了服务器组件渲染树的结果。它被用来在客户端上更新浏览器的DOM(文档对象模型)。
- 再通过client component进行呈现
- React服务器组件有效负载用于协调客户端和服务器组件树,并更新DOM。
- 通过[hydrate]进行协调
What is the React Server Component Payload (RSC)?什么是React服务器组件有效负载(RSC)?
RSC Payload是呈现的React Server Components树的紧凑二进制表示。React在客户端上使用它来更新浏览器的DOM。RSC有效载荷包含:
- 服务器组件的渲染结果
- 客户端组件应在何处呈现的占位符及其JavaScript文件的引用
- 从服务器组件传递到客户端组件的任何道具
服务器呈现策略
Static Rendering (Default)
网页内容在后台渲染,并且结果被缓存以供后续使用。这意味着对于给定的路由,渲染的结果在一段时间内是固定的,不会因用户的不同而有所变化。
数据重新验证:这是一个过程,其中服务器检查先前缓存的数据是否仍然有效或是否需要更新。如果数据仍然有效,服务器可能会跳过重新渲染,并直接使用缓存的版本。
Dynamic Rendering 动态渲染
动态渲染是指在用户请求时,根据特定用户的需求或条件实时生成和展示网页内容的过程。与静态渲染(预先生成并存储网页内容)不同,动态渲染允许为每个用户提供定制化的内容。 切换到动态路由 在渲染过程中,如果发现动态函数或未缓存的数据请求,Next.js将切换到动态渲染整个路由。
Dynamic Functions动态函数 | Data数据 | Route路线 |
---|---|---|
No不 | Cached缓存 | Statically Rendered静态渲染 |
Yes对 | Cached缓存 | Dynamically Rendered动态渲染 |
No不 | Not Cached未缓存 | Dynamically Rendered动态渲染 |
Yes对 | Not Cached未缓存 | Dynamically Rendered动态渲染 |
动态函数
cookies()
和headers()
函数:当在Next.js的服务器组件中使用这些函数时,它们会导致整个路由在每次请求时都进行动态渲染。这意味着,每次有人请求该路由时,Next.js都会重新生成页面内容,以确保它包含最新的cookie和标头信息。searchParams
:这是一个属性,而不是一个函数。当你在一个Next.js页面组件中使用它时,它允许你访问当前URL的查询参数(即URL中"?"后面的部分)。和上述函数一样,使用searchParams
也会使页面在每次请求时都进行动态渲染。- 无论使用上述哪个功能,整个路由或页面都会在请求时进行动态渲染。这确保了内容总是最新的,但也可能增加服务器的负载,因为每次请求都需要重新生成内容。
Streaming 流动
流式处理使您能够从服务器逐步渲染UI
- 服务器上的工作(如数据处理、内容生成等)被分成多个小块。每完成一个小块,就立即将其发送到客户端,而不是等待所有工作都完成后再发送。
- Next.js的路由器默认支持流式处理。
可以使用loading.js
和带有React Suspense的UI组件开始流式传输路由段
客户端组件(Client Components)
客户端上进行渲染工作有几个好处,包括:
- 客户端组件可以使用状态、效果和事件侦听器,这意味着它们可以向用户提供即时反馈并更新UI。
- 可以使用浏览器API,如地理位置或本地存储。
在Next.js中使用客户端组件
要使用客户端组件,可以在导入的文件顶部添加React"use Client"指令。
通过这个指令,可以定义一个边界,使得在这个边界内的所有代码和组件只在客户端运行。
也是告诉Next.js这个文件及其导入的所有模块都属于客户端捆绑包。
客户端组件是如何呈现的?
- 当用户首次访问应用程序时,Next.js使用React的API在服务器上渲染一个静态HTML预览。
- 服务器上的React将服务器组件渲染为RSC Payload,其中包含对客户端组件的引用。
- Next.js使用这个RSC Payload和客户端组件的JavaScript指令在服务器上生成HTML。
- 生成的HTML被发送到客户端,并立即显示为一个非交互式的初始预览。
- 在客户端上,RSC Payload用于协调客户端和服务器组件树,并更新DOM。
服务器和客户端组合模式
以下是服务器和客户端组件的不同用例的快速摘要:
What do you need to do? | Server Component | Client Component |
---|---|---|
Fetch data(获取数据) | ✅ | ❌ |
Access backend resources (directly) | ✅ | ❌ |
Keep sensitive information on the server (access tokens, API keys, etc) | ✅ | ❌ |
Keep large dependencies on the server / Reduce client-side JavaScript | ✅ | ❌ |
Add interactivity and event listeners (onClick(), onChange(), etc) | ❌ | ✅ |
Use State and Lifecycle Effects (useState(), useReducer(), useEffect(), etc) | ❌ | ✅ |
Use browser-only APIs | ❌ | ✅ |
Use custom hooks that depend on state, effects, or browser-only APIs | ❌ | ✅ |
Use react.dev/reference/r... | ❌ | ✅ |
Server Component Patterns服务器组件模式
Sharing data between components组件之间共享数据
可以使用fetch或React的缓存函数在需要的组件中提取相同的数据,而不用使用React Context(在服务器上不可用),也不用担心对相同的数据发出重复请求。
因为React扩展了fetch以自动存储数据请求,并且在fetch不可用时可以使用缓存功能。
Keeping Server-only Code out of the Client Environment 将仅服务器代码排除在客户端环境之外
由于JavaScript模块可以在服务器组件模块和客户端组件模块之间共享,因此原本只打算在服务器上运行的代码可能会偷偷进入客户端。
javascript
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
乍一看,getData似乎同时适用于服务器和客户端。但是,此函数包含一个API_KEY,编写此函数的目的是只在服务器上执行。
因此,即使可以在客户端上导入并执行getData(),它也无法按预期工作。虽然公开变量会使函数在客户端上工作,但您可能不想向客户端公开敏感信息。
为了防止客户端意外使用服务器代码,如果其他开发人员意外地将其中一个模块导入到客户端组件中,我们可以使用仅限服务器的包来给他们带来构建时错误。
javascript
npm install server-only
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
Using Third-party Packages and Providers使用第三方包
由于服务器组件是一个新的React功能,生态系统中的第三方包和提供商才刚刚开始将"使用客户端"指令添加到使用仅客户端功能(如useState、useEffect和createContext)的组件中。
可以自己加
javascript
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
Using Context Providers使用上下文
由于服务器组件不支持React上下文,因此尝试在应用程序的根目录下创建上下文将导致错误:
javascript
import { createContext } from 'react'
// createContext is not supported in Server Components
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
要解决此问题,请创建上下文并用provider包裹
javascript
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({
children,
}: {
children: React.ReactNode
}) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
Client Components客户端组件
Moving Client Components Down the Tree向下移动树中的客户端组件
为了减少客户端JavaScript捆绑包的大小,我们建议将客户端组件向下移动到组件树中。
javascript
// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
Interleaving Server and Client Components交错使用服务器和客户端组件
- 在请求-响应生命周期中,代码会从服务器移动到客户端。如果您需要在客户端访问服务器上的数据或资源,您将向服务器发出新的请求,而不是来回切换。
- 当向服务器发出新请求时,将首先呈现所有服务器组件,包括嵌套在客户端组件中的组件。呈现的结果(RSC Payload)将包含对客户端组件位置的引用。然后,在客户端上,React使用RSC Payload将服务器和客户端组件协调到一个树中。
- 由于客户端组件是在服务器组件之后呈现的,因此无法将服务器组件导入客户端组件模块(因为这需要向服务器返回新请求)。相反,您可以将服务器组件作为props传递给客户端组件。请参阅以下不支持的模式和支持的模式部分。
Unsupported Pattern: Importing Server Components into Client Components不支持的模式:将服务器组件导入客户端组件
不支持以下模式。无法将服务器组件导入客户端组件:
javascript
'use client'
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}
Supported Pattern: Passing Server Components to Client Components as Props 支持的模式:将服务器组件作为道具传递给客户端组件
javascript
'use client'
import { useState } from 'react'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
在父服务器组件中,您可以导入和,并将作为的子级传递
javascript
// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
Edge and Node.js Runtimes
Node | Serverless | Edge | |
---|---|---|---|
Cold Boot | / | Normal | Low |
HTTP Streaming | Yes | Yes | Yes |
IO | All | All | fetch |
Scalability | / | High | Highest |
Security | Normal | High | High |
Latency | Normal | Low | Lowest |
npm Packages | All | All | A smaller subset |
Static Rendering | Yes | Yes | No |
Dynamic Rendering | Yes | Yes | Yes |
Data Revalidation w/ fetch |
Yes | Yes | Yes |
Edge Runtime
在Next.js中,轻量级Edge Runtime是可用Node.js API的子集。
Edge Runtime并不支持所有Node.js API,这意味着一些npm包可能无法工作。例如,"找不到模块:无法解决'fs'"或类似错误。如果您需要使用这些API或包,我们建议您使用Node.js运行时。
Node.js Runtime
使用Node.js运行时可以访问所有Node.js API以及所有依赖它们的npm包。但是,它的启动速度不如使用Edge运行时的路由快。
大概过了一遍文档有了一些基础的概念:代码是从服务端流向客户端,了解了请求的生命周期,混合使用客户端组件和服务端组件的模式,还有运行时的环境。
在开发过程中熟悉起来吧。