从零实现一个NextJS14+React18 服务端渲染功能
前言
SSR工作原理
服务端渲染(SSR,Server-Side Rendering)是一种在服务器上生成网页内容并将其发送到客户端的技术。与客户端渲染(CSR,Client-Side Rendering)不同,SSR 在服务器端完成 HTML 的生成,并将完整的 HTML 发送到客户端,从而使页面在浏览器中立即显示内容。工作原理:
- 用户请求:用户通过浏览器请求一个 URL。
- 服务器处理:服务器接收到请求,执行代码来生成页面的 HTML。这包括处理数据获取、模板渲染等。
- 生成 HTML:服务器生成完整的 HTML 页面。
- 发送响应:服务器将生成的 HTML 页面发送回浏览器。
- 浏览器渲染:浏览器接收到 HTML 并立即渲染页面,同时下载和执行任何必要的 JavaScript 以实现交互功能。
SSR的优势
- 更快的初始加载:
- 预渲染内容:SSR 提供了一个完整的 HTML 页面,用户在加载页面时可以立即看到内容,而不必等待 JavaScript 加载和执行。
- 感知性能提升:用户感知到的页面加载时间更短,因为他们可以立即看到页面内容。
- 更好的搜索引擎优化(SEO) :
- 可爬取的内容:搜索引擎可以直接爬取和索引完整的 HTML 内容,从而提高页面在搜索引擎结果中的可见性。
- 元数据设置:可以为每个页面动态生成元数据(如标题、描述等),提高 SEO 效果。
SSR渲染方案选择
基于React框架实现SSR,主要有两种方式:
Nextjs
特点:
- 框架集成:Next.js 是一个基于 React 的框架,内置了 SSR 支持,提供了简单的配置和强大的功能。
- 数据获取:通过自定义方法获取服务器端渲染数据。
- 路由:内置文件系统路由,易于使用。
- 优化:内置代码分割、预加载和自动优化。
优点:
- 易于上手,特别适合使用 React 的团队。
- 丰富的生态系统和社区支持。
- 内置的 SEO 优化和性能提升功能。
缺点:
- 依赖于 React,对于非 React 项目可能不合适。
- 部分高级功能可能需要复杂的配置。
手动搭建服务端渲染(Express)
特点:
- 自定义实现:使用 Express 作为服务器框架,自行实现 SSR 逻辑。
- 灵活性:完全自定义的实现,适合复杂的业务需求。
优点:
- 高度灵活,可以根据需求完全定制。
- 可以与现有的 Node.js 生态系统无缝集成。
缺点:
- 开发复杂度高,需要手动处理 SSR 逻辑和优化。
- 缺乏框架的标准化功能,需要更多的开发和维护工作。
React 官方推荐使用Nextjs来配置 SSR 页面, 那我们就按照官方推荐的技术栈,实战一下。
SSR功能实现
我们用NextJS写一个简单的SSR demo
,练练手。有实践经验后,再去改造复杂的实际项目。先用脚手架,生成一个NextJS项目。
npx create-next-app@latest my-next-ssr
cd my-next-ssr
用脚手架生成NextJS项目时,会有许多问询项。自己根据实际情况选择。这里笔者没有选择TypeScript,是因为TS在开发阶段挺烦人的,编辑器会调试许多警告,看着人心烦意乱。个人感觉,TS在完成功能开发之后再回补类型定义,可能开发体验会好一些。
修改页面的title和描述
最新的NextJS,修改页面title的方法与以前不一样,通过定义metadata对象进行修改。另外,顶级src\app\layout.js
路由的 Layout 是必须的,用于定义 <html>
和 <body>
标签。,其它层级可选。
export const metadata = {
title: "my-next-ssr",
description: "从0搭建一个next.js服务端渲染项目",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Server Component和Client Component的区别
写SSR渲染页面之前,先要做3个铺垫。第一个铺垫是Server Component和Client Component的区别。Server Component 的用途侧重点是获取数据,并一次性渲染出结果。Client Component 侧重点是渲染完成以后的页面交互。Server Component的限制点比较多,主要有:
- 不能使用React的生命周期和一些Hooks
React的一些生命周期函数,比如类组件的componentDidMount
,componentDidUpdate
和componentWillUnmount
,以及函数组件的useEffect
和useLayoutEffect
,都不能在服务端Server Component组件中使用。
- 不能使用浏览器专属的API
浏览器专属的API,比如window
,document
,localStorage
,alert
等,都不能使用。
- 事件处理函数
Server Component组件不会执行事件处理函数,也不会触发任何事件。
Next.js 默认所有的组件都是 Server Component 服务端组件,要声明为客户端组件,需要在组件的定义文件顶部加声明。
"use client";
// ...
流式渲染
第二个铺垫是流式渲染,就是把 HTML 分块通过网络传输,然后客户端收到分块后逐步渲染,提升页面打开时的用户体验。通常是利用HTTP/1.1
中的分块传输编码(Chunked transfer encoding)机制。比原始的renderToString
有着更短的 TFFB 时间(发出页面请求到接收到应答数据第一个字节所花费的毫秒数)。流式传输的 HTML 也不会阻塞注水过程。如果 JavaScript 早于 HTML 加载完成,React 就会开始对已完成的 HTML 部分注水。流式渲染的实现方法:
import { Suspense } from "react";
import { Spin } from "antd";
import {HomePageClientA,HomePageClientB} from "@/components/home-client";
export default function Posts() {
return (
<h1>首页</h1>
<Suspense fallback={<Spin tip="加载中..." />}>
<HomePageClientA initialData={xxx} />
</Suspense>
<Suspense fallback={<Spin tip="加载中..." />}>
<HomePageClientB initialData={xxx} />
</Suspense>
)
}
刚开始被 Suspense 的 HomePageClientA和 HomePageClientA组件还没准备好时,会先返回占位的Spin。哪个组件先完成数据填充,哪个组件会先展示。
API编写方法
第三个铺垫是api的编写方式。api目录应该定义在app目录下,而不是和app目录同级。api下每个文件夹的名称就是api的请求路径,api的逻辑的具体实现文件要统一命名为route.js.
api具体业务逻辑实现文件,导出的方法名是Http的请求方式。如GET,POST,PUT,DELETE
这些。
// src\app\api\avatar-list\route.js
const list = [
{
title: "Ant Design Title 1",
},
{
title: "Ant Design Title 2",
},
{
title: "Ant Design Title 3",
},
{
title: "Ant Design Title 4",
},
];
export async function GET(request) {
return new Response(JSON.stringify(list), {
headers: { "Content-Type": "application/json" },
});
}
在Server Component
和在Client Component
中调用时的请求URL写法差异。在Server Component
中调用时,要这样写,要先在根目录下新增一个.env
文件,定义好网络请求的基础路径。
URL="http://localhost:3000"
不加process.env.URL
的话,请求会报404.
async function getData() {
const result = await fetch(process.env.URL + "/api/avatar-list", { method: "GET" });
if (result.ok) {
return result.json();
}
return [];
}
在客户端调用的话,请求地址无需加基础路径。
await fetch("/api/avatar-list", { method: "GET" });
SSR页面编写
我们在NextJS的入口页,编写SSR页面逻辑。 写一段获取模拟列表数据,将数据填充到页面的功能。然后查看网页源代码,是否包含填充数据,如果包含,说明就是想实现的SSR渲染效果。
// app/page.js
import { Suspense } from "react";
import { Spin } from "antd";
import HomePageClient from "@/components/home-client";
// 获取模拟数据
async function getData() {
// console.log({ url: process.env.URL + "/api/avatar-list" });
const result = await fetch(process.env.URL + "/api/avatar-list", { method: "GET" });
if (result.ok) {
return result.json();
}
return [];
}
const HomePage = async () => {
const res = await getData();
return (
<>
<h1>首页</h1>
<Suspense fallback={<Spin tip="加载中..." />}>
<HomePageClient initialData={res.data} />
</Suspense>
</>
);
};
export default HomePage;
app/page.js中引用的HomePageClient组件逻辑是接收传入的列表数据,遍历生成列表。
// components/HomePageClient.js
"use client";
import { Divider, List, Avatar, Space } from "antd";
const HomePageClient = ({ initialData = [] }) => {
return (
<Space
direction="vertical"
size="middle"
style={{
display: "flex",
}}>
<Divider orientation="left">Default Size</Divider>
<List
itemLayout="horizontal"
dataSource={avatarList}
renderItem={(item, index) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar src={`https://api.dicebear.com/7.x/miniavs/svg?seed=${index}`} />}
title={<a href="https://ant.design">{item.title}</a>}
description="Ant Design, a design language for background applications, is refined by Ant UED Team"
/>
</List.Item>
)}
/>
</Space>
);
};
export default HomePageClient;
编写完之后,用下面的命令启动项目
yarn dev
打开http://localhost:3000/
访问SSR页面
然后复制一段列表内容,在网页源文件中查找,看能否匹配到,结果如下:
可以看到,确实是服务端渲染。
页面交互事件
SSR页面一般需要在客户端进行水合作用,页面才有交互效果。而使用NextJS框架的话,这个动作是隐式完成的。不需要调用React水合API,我们写一个鼠标点击页面跳转功能,验证一下dom事件能否正常执行。
// components/HomePageClient.js
"use client";
import { Divider, List, Avatar, Button, Input, Space } from "antd";
import { useRouter } from "next/navigation";
// ...
const HomePageClient = ({ initialData = [] }) => {
const router = useRouter();
// ...
const handleClick = () => {
router.push("/other");
};
return (
<Space
direction="vertical"
size="middle"
style={{
display: "flex",
}}>
<Button type="primary" onClick={handleClick}>
跳转测试
</Button>
</Space>
);
};
export default HomePageClient;
可以看到,页面可以正常跳转。
设置有的页面走CSR渲染
上文提到,Server Components
限制比较多。所以我们只想对个别页面进行SSR改造,大多数页面还是想复用以前的写法,那如何让有些页面执行CSR渲染呢? 其实也很简单,只需使用dynamic方法加载组件,在传入的参数中,设置ssr属性为false即可。示例如下:
// app/other/page.js
import dynamic from "next/dynamic";
import React from "react";
// 对于其它页面,使用动态导入来禁用 SSR,并确保这些页面只在客户端渲染。
const OtherPageComponent = dynamic(() => import("@/components/other"), {
ssr: false,
});
const OtherPage = () => {
return (
<div>
<h1>其它页面</h1>
<OtherPageComponent />
</div>
);
};
export default OtherPage;
这样这个页面访问的时候执行的就是浏览器渲染。
最后
发现NextJS发展挺快, 在写此文的过程中,参考的一些网上NextJS+SSR文章的好多示例,已经不适用。比如说页面标题用Head
组件设置,事件交互要调用水合函数,路由跳转,给组件填充数据等。如果你是初学者,就和现在的我一样,若是采用NextJS14+React18
实现服务端渲染功能,本文一定能让你少走一些弯路。本文的示例项目已经上传到码云,欢迎学下载习与交流。
转载自:https://juejin.cn/post/7389650993359192090