likes
comments
collection
share

从零实现一个NextJS14+React18 服务端渲染功能

作者站长头像
站长
· 阅读数 63

前言

SSR工作原理

服务端渲染(SSR,Server-Side Rendering)是一种在服务器上生成网页内容并将其发送到客户端的技术。与客户端渲染(CSR,Client-Side Rendering)不同,SSR 在服务器端完成 HTML 的生成,并将完整的 HTML 发送到客户端,从而使页面在浏览器中立即显示内容。工作原理:

  1. 用户请求:用户通过浏览器请求一个 URL。
  2. 服务器处理:服务器接收到请求,执行代码来生成页面的 HTML。这包括处理数据获取、模板渲染等。
  3. 生成 HTML:服务器生成完整的 HTML 页面。
  4. 发送响应:服务器将生成的 HTML 页面发送回浏览器。
  5. 浏览器渲染:浏览器接收到 HTML 并立即渲染页面,同时下载和执行任何必要的 JavaScript 以实现交互功能。

SSR的优势

  1. 更快的初始加载
  • 预渲染内容:SSR 提供了一个完整的 HTML 页面,用户在加载页面时可以立即看到内容,而不必等待 JavaScript 加载和执行。
  • 感知性能提升:用户感知到的页面加载时间更短,因为他们可以立即看到页面内容。
  1. 更好的搜索引擎优化(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在完成功能开发之后再回补类型定义,可能开发体验会好一些。

从零实现一个NextJS14+React18 服务端渲染功能

修改页面的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的限制点比较多,主要有:

  1. 不能使用React的生命周期和一些Hooks

React的一些生命周期函数,比如类组件的componentDidMountcomponentDidUpdatecomponentWillUnmount,以及函数组件的useEffectuseLayoutEffect,都不能在服务端Server Component组件中使用。

  1. 不能使用浏览器专属的API

浏览器专属的API,比如windowdocumentlocalStorage,alert等,都不能使用。

  1. 事件处理函数

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.

从零实现一个NextJS14+React18 服务端渲染功能

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页面 从零实现一个NextJS14+React18 服务端渲染功能 然后复制一段列表内容,在网页源文件中查找,看能否匹配到,结果如下: 从零实现一个NextJS14+React18 服务端渲染功能 可以看到,确实是服务端渲染。

页面交互事件

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;

可以看到,页面可以正常跳转。

从零实现一个NextJS14+React18 服务端渲染功能

设置有的页面走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
评论
请登录