基于 Next.js 的 SSR/SSG 方案了解一下?

开发 前端
服务端渲染(SSR,Server Side Render)与客户端渲染(CSR,Client Side Render)的核心区分点简单来说就是完整的 HTML 文档在服务端还是浏览器里组装完成。

[[442187]]

本文主要是讲讲如何使用 Next.js 框架实现服务端渲染,将有效提升网页的 SEO 和首屏渲染速度,说不定哪天就用上了,是吧!

一、服务端渲染(SSR)

服务端渲染(SSR,Server Side Render)与客户端渲染(CSR,Client Side Render)的核心区分点简单来说就是完整的 HTML 文档在服务端还是浏览器里组装完成。

SSR 的另一概念是同构渲染,可以看看知乎中的讨论:什么是前端的同构渲染?[1]

同构渲染简单来说就是一份代码,服务端先通过服务端渲染(SSR),生成 HTML 以及初始化数据,客户端拿到代码和初始化数据后,通过对 HTML 的 DOM 进行 patch 和事件绑定对 DOM 进行客户端激活(client-side hydration),该整体过程叫同构渲染。

SSR 的原理,本文就不再赘述了,感兴趣的朋友推荐阅读这篇文章:《彻底理解服务端渲染 - SSR原理》

二、Next.js

Next.js[2] 是一款用于生产环境的 React 框架,无需配置,默认提供了生产环境所需所有功能的最佳开发实践:支持静态渲染和服务端渲染、支持 TypeScript、智能打包、路由预加载等功能。

与此同时,Next.js 还提供了如下开箱即用的 SDK 辅助开发 Web 应用:

阅读过 SSR 原理一文可看到配置支持服务端渲染还是挺麻烦的,但借助 Next.js,可以很轻松的上手改造支持现有 Web 应用服务端渲染。

是否采用服务端渲染还得综合考虑收益,服务端渲染毕竟会增加服务器的计算开销,稳定性相较于 CSR 差一些。

三、创建 Next.js 应用

初始化一个 Next.js 应用可以直接通过脚手架快速完成:

npx create-next-app@latest --ts 
or 
yarn create next-app --typescript 
  • 1.
  • 2.
  • 3.

中途会要求输入项目名,并自动安装所需的模块

执行 yarn dev 后需要手动再浏览器打开网址:http://localhost:3000 ,即可看到如下页面:

首页的内容对应 ./pages/index.tsx 文件

初始的目录结构如下:


├── pages // 采用约定式路由(文件系统路由) 
│   ├── _app.tsx 
│   ├── api // API 目录 
|      ├── hello.ts 
│   └── index.tsx // 首页 
├── public // 公共资源 
│   ├── favicon.ico 
│   └── vercel.svg 
├── styles // 样式 
│   ├── Home.module.css 
│   └── globals.css 
├── next-env.d.ts // Next 相关的 TS 定义 
├── next.config.js // Next.js 自定义配置 
├── node_modules 
├── package.json 
├── tsconfig.json 
├── README.md 
└── yarn.lock 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

四、页面路由

通常我们的 Web 应用是多页面、多路由的,因此会涉及到在各个页面之间跳转,因此有必要熟悉 Next.js 的路由使用方式。

上述讲到了 Next.js 是约定式路由,基于文件系统,对应到 ./pages 目录下,当添加页面文件到 ./pages 目录,Next.js 会自动识别并将对应文件注册的路由上

4.1 索引路由

Next.js 会自动将文件夹内的 “index” 文件注册为文件夹的主页

4.2 嵌套路由

Next.js 支持嵌套文件的路由,如果您创建嵌套文件夹结构,文件仍将自动以相同方式路由解析。

4.3 动态参数路由

常见于比如博客的文章详情页面,文章的 id 是动态变化的,Next.js 中可以使用中括号解析到对应的命名参数

更多关于动态路由的解析可参阅:https://nextjs.org/docs/routing/dynamic-routes

4.4 路由跳转

之前有提到 Next.js 中的路由预加载功能,需借助 Next.js 提供的 next/link,写法如下:

<Link href="/blog/hello-world"
  <a>第一篇文章</a> 
</Link> 
  • 1.
  • 2.
  • 3.

应用页面之间的跳转,可以用 标签包裹。

属性 href 的值是跳转页面的路径字符串或 URL 对象:

import Link from 'next/link' 
 
function Articles({ articles }) { 
  return ( 
    <ul> 
      {articles.map((article) => ( 
        <li key={article.id}> 
          <Link 
            href={{ 
              pathname: '/article/[slug]'
              query: { slug: article.slug }, 
            }} 
          > 
            <a>{article.title}</a> 
          </Link> 
        </li> 
      ))} 
    </ul> 
  ) 

 
export default Articles 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

如有需要对路由通过 js 跳转,则可以通过 Next.js 提供的 next/router[3] 中的 useRouter[4] Hook。

4.5 代码拆分和预加载

通过 Next.js 的路由功能,可以自动完成页面按需加载当前页面所需的代码,同时会自动预加载页面中属于自身应用的链接。

这意味着在呈现主页时,最初不会提供其他页面的代码,同时可确保即使您有数百个页面,主页也能按需快速加载。

仅加载您请求的页面的代码也意味着页面变得独立,如果某个页面抛出错误,应用程序的其余部分仍然可以工作。

在 Next.js 的生产版本中,每当 Link 组件出现在浏览器的视口中时,Next.js 都会在后台自动预取链接页面的代码。当您单击链接时,目标页面的代码已在后台加载,页面转换将近乎即时。

五、静态资源

所有静态资源都可以放到 ./public 目录下,Next.js 会自动为其中的文件注册路由,按照文件系统的方式,与 Page 的路由类似。

5.1 图片元素

一般网页中的图片写法如下:

<img src="/images/logo.png" alt="logo" /> 
  • 1.

但这种写法会需要开发者手动去优化,比如按需加载、错误处理等。

Next.js 考虑到这点,为了减轻开发者负担,于是提供了 next/image[5],开箱即用。

这里其实可以借鉴一下,别的项目中为了业务统一处理图片,可以封装一个 Image 组件,提升研发效率。

import Image from 'next/image' 
 
const YourComponent = () => ( 
  <Image 
    src="/images/profile.jpg" // 图片文件路径 
    height={144} // 具有正确纵横比的所需尺寸 
    width={144} 
    alt="Image Alt" 
  /> 

 
export default YourComponent; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

5.2 Meta 数据

网页的 Meta 数据,也就是在 html->head 标签中的内容

Next.js 提供了 next/head[6] 用于声明式编写网页的 head 内容。

import Link from 'next/link' 
import Head from 'next/head' 
 
export default function FirstPost() { 
  return ( 
    <> 
      <Head> 
        <meta charset="UTF-8" /> 
        <title>First Post</title> 
        <link rel="shortcut icon" href="/favicon.ico" /> 
        <meta name="keywords" content="网页关键词" /> 
        <meta name="description" content="网页描述" /> 
        <meta name="author" content="DYBOY,dyboy2017@qq.com" /> 
        <meta name="version" content="1.0" /> 
        <link rel="stylesheet" href="//at.alicdn.com/t/font_2319527_hng3o947ocv.css" /> 
        <link rel="stylesheet" href="/style/fancybox.css" /> 
        <link rel="stylesheet" href="/style/app.css" /> 
        <script src="/scripts/jquery.js"></script> 
      </Head> 
      <h1>First Post</h1> 
      <h2> 
        <Link href="/"
          <a>Back to home</a> 
        </Link> 
      </h2> 
    </> 
  ) 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

此外,若我们有需要修改 的诉求时,可创建pages/_document.js 文件,并通过“自定义文档[7]”的方式继承并统一改造所有网页输出的公共内容。

5.3 JS 脚本文件

例如我们使用了三方库 Jquery,虽然可以直接在 组件中直接写:

<script src="/scripts/jquery.js"></script> 
  • 1.

但是,这种方式包含脚本并不能明确说明何时加载同一页面上获取的其他 JavaScript 代码。如果某个特定脚本会阻塞渲染并且会延迟页面内容的加载,则会显着影响性能。

因此,可以通过 next/script[8] 来优化

import Link from 'next/link' 
import Head from 'next/head' 
import Script from 'next/script' 
 
export default function FirstPost() { 
  return ( 
    <> 
      <Head> 
        <title>First Post</title> 
      </Head> 
      <Script 
        src="/scripts/jquery.js" 
        strategy="lazyOnload" // 设置 js 加载的方式 
        onLoad={() => 
          // js 脚本文件加载完成后的回调函数 
          console.log(`script loaded correctly`) 
        } 
      /> 
      <h1>First Post</h1> 
      <h2> 
        <Link href="/"
          <a>返回首页</a> 
        </Link> 
      </h2> 
    </> 
  ) 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

 5.4 CSS 文件

Next.js 已经内置支持了 CSS 和 SASS,允许开发者引入 .css 和 .sass 文件方式引入样式文件,同时还支持 Tailwind CSS。

需要手动安装 SASS 模块

yarn add sass 
  • 1.

默认还支持 CSS-in-JS,借助 styled-jsx[9] 这个模块,可以直接在 React 组件中直接写 CSS,同时限制作用域,不会影响其他组件。

如果需要 CSS 模块化[10],那么 CSS 文件命名应当为 *.module.css 格式。

import styles from './layout.module.css' 
 
export default function Layout({ children }) { 
  return <div className={styles.container}>{children}</div> 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

全局 CSS 注入,则在根目录的 ./styles 目录编写,同时也仅在 ./pages/_app.tsx 文件中引入全局样式文件

import '../styles/globals.css' // 引入全局样式 
import type { AppProps } from 'next/app' 
 
function MyApp({ Component, pageProps }: AppProps) { 
  return <Component {...pageProps} /> 

 
export default MyApp 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

Next.js 使用 PostCSS[11] 编译 CSS,自定义配置 PostCSS 的方式可参考:【自定义 PostCSS 配置[12]】

六、预渲染和数据获取

Next.js 支持:

  • 在服务端预渲染
  • 静态页面生成和服务端渲染
  • 有数据和无数据的静态生成
  • 一些预定义的方法(生命周期函数)注入数据

6.1 预渲染

默认情况下,Next.js 预渲染每个页面。这意味着 Next.js 会提前为每个页面生成 HTML,预渲染可以带来更好的性能和SEO。

每个生成的 HTML 都与该页面所需的最少 JavaScript 代码相关联。当浏览器加载页面时,其 JavaScript 代码会运行并使页面完全交互。

预渲染和无预渲染的对比如下:

 6.2 静态生成和服务端渲染

Next.js 支持两种形式的预渲染方式:静态生成和服务端渲染

  • 静态生成: 在构建时生成 HTML 的预渲染方法。然后在每个请求上重用预渲染的 HTML。
  • 服务器端渲染: 在每个请求上生成 HTML 的预渲染方法。

6.3 获取数据

(1)静态生成时获取数据

在服务端构建生成静态页面之前,有时候需要获取一些数据,可以借助 getStaticProps 方法。

在页面组件内,同时导出一个 getStaticProps 方法:

export default function HomePage(props) { ... } 
 
// 导出异步获取数据方法 
export async function getStaticProps() { 
  // 获取数据,例如从数据库、API、文件等 
  const data = ... 
 
  // 返回的参数将会按照 key 值赋值到 HomePage 组件的同名入参中 
  return { 
    props: ... 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

注意,仅在页面组件内导出该方法

(2)服务端渲染时获取数据

比如用户的个人中心页面,该页面时不需要 SEO 优化的,其数据通常需要实时更新获取,因此采用 SSR 的方式,而 SSR 在服务端获取数据可以借助 getServerSideProps 方法

和构建时获取数据方法类似:

export default function HomePage(props) { ... } 
 
// 导出异步获取数据方法 
export async function getServerSideProps() { 
  // 获取数据,例如从数据库、API、文件等 
  const data = ... 
 
  // 返回的参数将会按照 key 值赋值到 HomePage 组件的同名入参中 
  return { 
    props: ... 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

(3)客户端渲染时获取数据

如果不需要“预渲染”时候获取数据,即不需要“静态生成”和“服务端渲染”的时候获取数据,则可以在对应页面组件代内,编写网络请求相关代码。

Next.js 团队提供了一个基于 React Hooks 的 useSWR 钩子,推荐使用,该钩子会处理缓存、重新验证、焦点跟踪、间隔重新获取等。

一个简单的示例如下:

import useSWR from 'swr' 
 
function Profile() { 
  const { data, error } = useSWR('/api/user'fetch
 
  if (error) return <div>failed to load</div> 
  if (!data) return <div>loading...</div> 
  return <div>hello {data.name}!</div> 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

和一些封装的请求 Hooks 类似,useSWR 还支持自定义请求库,默认使用的是 fetch 的 pollyfill 模块(unfetch[13]),提供的中文官方的文档也非常清晰,地址:https://swr.vercel.app/zh-CN/docs/getting-started

七、动态路由

上面讲到了预渲染,如果是动态路由的预渲染该如何处理?这里需要依赖方法 getStaticPaths 获得动态路由需要生成页面列表。

// ./pages/post/[id].tsx 
import Layout from '../../components/layout' 
 
export default function Post({id, article}) { 
  return ( 
      <Layout> 
          <Head> 
              <title>{article.title}</title> 
          </Head> 
          {article.title} 
          <br /> 
          {id} 
          <br /> 
          {article.date
      </Layout> 
   ) 

 
export async function getStaticPaths() { 
  // 返回所有可能的文章 id 所对应的列表 
    const paths = [ 
      { 
        params: { 
          id: 'ssg-SSR' 
        } 
      }, 
      { 
        params: { 
          id: 'pre-rendering' 
        } 
      } 
    ] 
     
    return { 
        paths, 
        fallback: false, // 如果在 paths 中 id 找不到对应值,则指向 404 页面 
    } 

 
export async function getStaticProps({ params }) { 
  // 通过 params.id 获取必要的文章数据  
  // parmas 即路由中的参数对象 
  const article = getContentById(parmas.id); 
  return { 
      props: { 
          id, 
          artcile, 
      } 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.

关于 fallback 可以参阅:fallback props[14]

自建一个 404 页面,文件路径为:./pages/404.tsx

export default function Custom404() { 
  return <h1>404 - Page Not Found</h1> 

  • 1.
  • 2.
  • 3.

八、BFF API

在初始化的目录结构中的 ./pages/api/hello.ts 文件,就是一个 API 页面,他的路由和页面路由相同

import type { NextApiRequest, NextApiResponse } from 'next' 
 
type Data = { 
  name: string 

 
export default function handler( 
  req: NextApiRequest, 
  res: NextApiResponse<Data> 
) { 
  res.status(200).json({ name'John Doe' }) 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

在 ./pages/api/ 目录下,前端开发者编写人意的 API 应用,也就是被称为 Serverless Functions,类似于字节的“轻服务[15]”

九、部署

官方推荐使用 Vercel[16] 来完成一键自动化构建部署

首先执行构建,构建时候会自动做相关优化

yarn build 
// 实际执行 
next build 
  • 1.
  • 2.
  • 3.

然后是启动服务:

yarn start 
// 实际执行 
next start -p 8080 
  • 1.
  • 2.
  • 3.

在生产环境,再用 PM2[17] 管理守护进程

然后使用 Nginx 作为网关,配置域名,SSL,映射到本地 8080 端口即可。

拓展更多

Next.js 还有更多细节和 API,需要深入了解的小伙伴可以参阅:Next.js API文档[18]

除了 Next.js,还有 Razzle.js[19] 也可以学习下。

推荐阅读:

  • 《彻底理解服务端渲染 - SSR原理》
  • 《魅族官网基于 next.js 重构实践总结与分享[20]》
  • 《SWR - 用于数据请求的 React Hooks 库[21]》
  • 《react 服务端(SSR) 框架next.js开发个人网站分享[22]》
  • 《Next.js 应用开发实践[23]》

总结

通过对 Next.js 的初步上手使用,SSR 确实有助于提升用户的体验,比如一些文档网站、官网、营销网页,个人非常推荐这种方式,但其缺点也很明显,服务端的稳定性会有所降低,稳定性可以通过增加成本提高,相较于其优点,还是值得投入的!

Next.js 把一些生产配置初始化就构建完成,对于开发者来说,开箱即用的感觉真的太棒了!

参考资料

[1]什么是前端的同构渲染?: https://www.zhihu.com/question/325952676

[2]Next.js: https://nextjs.org/

[3]next/router: https://nextjs.org/docs/api-reference/next/link

[4]useRouter: https://nextjs.org/docs/api-reference/next/router#userouter

[5]next/image: https://nextjs.org/docs/api-reference/next/image

[6]next/head: https://nextjs.org/docs/api-reference/next/head

[7]自定义文档: https://nextjs.org/docs/advanced-features/custom-document

[8]next/script: https://nextjs.org/docs/api-reference/next/script

[9]styled-jsx: https://github.com/vercel/styled-jsx

[10]CSS 模块化: https://nextjs.org/docs/basic-features/built-in-css-support#adding-component-level-css

[11]PostCSS: https://postcss.org/

[12]自定义配置 PostCSS: https://nextjs.org/docs/advanced-features/customizing-postcss-config

[13]unfetch: https://www.npmjs.com/package/unfetch

[14]fallback props: https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required

[15]轻服务: https://qingfuwu.cn/

[16]Vercel: https://vercel.com/

[17]PM2: https://pm2.keymetrics.io/

[18]Next.js API文档: https://nextjs.org/docs/api-reference/cli

[19]Razzle.js: https://razzlejs.org/

[20]魅族官网基于 next.js 重构实践总结与分享: https://zhuanlan.zhihu.com/p/113853235

[21]SWR - 用于数据请求的 React Hooks 库: https://swr.vercel.app/zh-CN/docs/getting-started

[22]react 服务端(SSR) 框架next.js开发个人网站分享: http://www.liuweibo.cn/p/206

[23]Next.js 应用开发实践: https://nextjs-in-action-cn.taonan.lu/

【编辑推荐】

 

责任编辑:姜华 来源: DYBOY
相关推荐

2023-01-03 08:00:00

2020-12-04 09:26:13

SSR 框架企业级

2024-04-03 13:27:28

Next.js扩展项目

2025-02-03 00:00:35

2025-03-06 00:00:00

2024-05-09 09:01:03

2024-09-20 15:37:02

2024-11-25 07:39:48

2025-01-17 09:29:42

2024-12-13 08:37:32

2024-09-04 10:27:53

2024-04-28 10:56:34

Next.jsWeb应用搜索引擎优化

2022-02-22 20:48:48

RemixNext.js框架

2022-08-02 09:00:00

开发Web工具

2024-01-25 09:04:25

2020-02-10 14:26:10

GitHub代码仓库

2024-02-05 11:55:41

Next.js开发URL

2024-03-05 19:17:37

2022-09-14 15:11:06

Linux防火墙

2024-03-04 07:33:39

RemixReact框架
点赞
收藏

51CTO技术栈公众号