服务器组件与客户端代码完全兼容
服务器组件与客户端代码完全兼容,这意味着客户端组件和服务器组件可以在同一个 React 树中进行渲染。通过将大部分应用程序代码移到服务器上,服务器组件有助于防止客户端数据获取的瀑布效应,快速解决服务器端的数据依赖关系。
在传统的客户端渲染中,组件使用 React Suspense
来“暂停”
其渲染过程(并显示回退状态),同时等待异步工作完成。通过服务器组件,数据获取和渲染都在服务器上进行,因此 Suspense
也会在服务器端管理等待期,从而缩短总的往返时间,加快回退和完成页面的渲染。
需要注意的是,客户端组件在初始加载时仍然进行服务器端渲染(SSR
)。服务器组件模型并不取代 SSR 或 Suspense,而是与它们一起工作,根据需要为用户提供应用程序的所有部分。
在使用
Next.js
和React
服务器组件时,数据获取和 UI 渲染可以在同一个组件中完成。此外,服务器操作(Server Actions)为用户提供了在页面上的JavaScript
加载之前与服务器端数据进行交互的方式。
9. 如何使用Next.js和MongoDB构建课程列表页面
现在让我们用Next.js
构建一个使用RSC
的应用程序。
因此,我们现在将构建一个课程列表页面,以展示我们如何在Next.js
中创建服务器组件,以及它与客户端组件的不同之处。
请注意,我们不会在这里深入学习
Next.js
或MongoDB
。我们只是将这个应用程序作为一个示例,来教我们RSC
的工作原理以及它们与客户端组件的区别。
首先,让我们将课程数据添加到数据存储中。对于这个应用程序,我使用了MongoDB
。下面的图像显示添加了三个课程的三个文档。
接下来,我们将创建一个实用函数来建立与MongoDB
的连接。这是一个通用的代码,我们可以在任何基于JavaScript
的项目中使用它,以使用Mongoose
和MongoDB URI
连接到MongoDB
import mongoose from "mongoose"; export async function dbConnect(): Promise<any> { try { const conn = await mongoose.connect(String(process.env.MONGO_DB_URI)); console.log(`Database connected : ${conn.connection.host}`); return conn; } catch (err) { console.error(err); } }
现在,我们需要创建与MongoDB
文档相对应的模型
(modal
)。由于我们处理的是课程数据,这是与之对应的模型:
import mongoose, { Schema } from "mongoose"; const schema = new Schema({ name: { required: true, type: String }, description: { required: true, type: String }, cover: { required: true, type: String }, rating: { required: true, type: Number }, price: { required: true, type: Number }, createdOn: { type: { type: Date, default: Date.now } }, link: { required: true, type: String }, type: { required: true, type: String }, comments: { required: false, type: [{ body: String, date: Date }] } }); export const courses = mongoose.models.course ?? mongoose.model("course", schema);
通过
Next.js App Router
,所有的组件默认都是服务器组件。这意味着它们位于靠近服务器的位置,并且可以访问我们的服务器生态系统。
下面的代码是一个常规的Next.js
组件,但具有一个特殊功能:我们可以在组件中直接获取数据库连接,并直接查询数据,而无需经过任何状态和效果管理。
从该组件中记录的任何内容都不会被记录到我们的浏览器控制台,因为这是一个服务器组件。我们可以在服务器控制台中查看日志(我们可以使用yarn dev
命令启动服务器的终端)。
由于与数据库的交互是异步的,我们在进行调用时使用await
关键字,并在组件上使用async
关键字。在接收到响应后,我们将其作为属性传递给子组件。
import { dbConnect } from '@/services/mongo' import { courses } from '@/models/courseModel' import { addCourseToDB } from './actions/add-course' import AddCourse from './components/AddCourse' import CourseList from './components/CourseList' export default async function Home() { // 建立MongoDB链接 await dbConnect(); //获取所有的数据信息 const allCourses = await courses.find().select( ["name", "cover", "rating"]); // 在服务器终端中打印显示数据 console.log({allCourses}) return ( <main> <div> <h1>Courses</h1> <AddCourse addCourseToDB={addCourseToDB} /> <CourseList allCourses={allCourses} /> </div> </main> ) }
Home组件包含:
- 一个标题
- 一个组件(
AddCourse
),用于包装一个添加课程的按钮 - 一个组件(
CourseList
),用于将课程显示为列表。
我们知道,服务器组件可以同时渲染客户端和服务器组件。AddCourse
组件需要用户交互,即用户需要点击按钮来添加课程。所以它不能是服务器组件.
因此,让我们为AddCourse
创建一个客户端组件
。通过Next.js App Router
,默认情况下,所有组件都是服务器组件。
如果我们想创建一个客户端组件,我们必须在组件顶部(甚至在任何导入语句之前)使用名为
use client
的指令来明确创建一个客户端组件。
客户端组件- AddCourse
'use client' import { useState } from 'react'; import Modal from './Modal'; import AddCourseForm from "./AddCourseForm"; export default function AddCourse({ addCourseToDB, }: { addCourseToDB: (data: any) => Promise<void> }) { const [showAddModal, setShowAddModal] = useState(false); const add = async(data: any) => { await addCourseToDB(data); setShowAddModal(false); } return ( <> <button onClick={() => setShowAddModal(true)} > Add Course </button> <Modal shouldShow={showAddModal} body={ <AddCourseForm saveAction={add} cancelAction={() => setShowAddModal(false)} />} /> </> ) }
服务器组件 -CourseList
CourseList
组件不需要任何事件处理程序,因此我们可以将其保持为服务器组件。
import Image from 'next/image' import Link from 'next/link' export default function CourseList(courseList: any) { const allCourses = courseList.allCourses; return( <div> { allCourses.map((course: any) => <Link key={course['_id']} href={`/courses/${course['_id']}`}> <div> <Image src={course.cover} width={200} height={200} alt={course.name} /> <h2>{course.name}</h2> <p>{course.rating}</p> </div> </Link> )} </div> ) }
我们打开浏览器开发工具的Sources
选项卡,以确定客户端上下载了什么,服务器上留下了什么。我们在这里是看不到page.tsx
文件或CourseList.tsx
文件信息。这是因为这些是服务器组件,它们永远不会成为我们的客户端捆绑包的一部分。
我们只会看到我们在应用程序中明确标记为客户端组件的组件。
在
Next.js App Router
中,所有获取的数据现在默认为静态数据,在构建时渲染。然而,这可以很容易地改变:Next.js
扩展了fetch
选项对象,以提供缓存和重新验证规则的灵活性。我们可以使用
{next: {revalidate: number}}
选项以设置的时间间隔或在后端更改发生时刷新静态数据(增量静态再生成),而{cache: 'no-store'}
选项可以在动态数据的fetch
请求中传递(服务器端渲染)。
总结
总结一下:
React
服务器组件具有后端访问权限,无需进行任何网络往返。- 我们可以通过使用
RSC
来避免网络瀑布问题。 React
服务器组件支持自动代码拆分,并通过零捆绑大小提高应用程序的性能。- 由于这些组件位于服务器端,它们无法访问客户端端的事件处理程序、状态和效果。这意味着我们不能使用任何事件处理程序或
React
钩子,如useState
、useReducer
和useEffect
。 React
服务器组件可以导入并渲染客户端组件,但反之则不成立。但我们可以将服务器组件作为props
传递给客户端组件。RSC
并不意味着取代客户端组件。健康的应用程序同时使用服务器组件来进行动态数据获取以及客户端组件来实现丰富的交互性。挑战在于确定何时使用每种组件。
后记
分享是一种态度。
参考资料:
- understanding-react-server-components
- react-server-components-for-beginners/
- how-to-use-react-server-components/
- what-even-are-react-server-components/
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。