使用 Next.js 创建 React 同构应用

1,529 阅读13分钟

使用场景

  • 必须使用打包程序(例如 webpack)打包代码,并使用 Babel 等编译器进行代码转换。
  • 你需要针对生产环境进行优化,例如代码拆分。
  • 你可能需要对一些页面进行预先渲染以提高页面性能和 SEO。你可能还希望使用服务器端渲染或客户端渲染。
  • 你可能必须编写一些服务器端代码才能将 React 应用程序连接到数据存储。

优势

一.   约定式路由

Next.js 会根据 pages 目录下的文件结构自动生成对应路由

在 Next.js 中,一个页面就是在 pages 目录下被导出的一个 React 组件。

pages 根据其目录下的 文件名与路由进行关联,例如:

  • pages/index.js 与路由的 / (根路径) 关联
  • pages/post/first-post.js 与路由的 /posts/first-post 关联

二.   跳转页面-Link

在 Next.js 中, 我们使用 Link 标签 包裹 a 标签 实现页面跳转

import Link from 'next/link'

Read <Link href="/posts/first-post"><a>this page!</a></Link>

Client-Side Navigation(客户端导航)

Link   组件可在同一 Next.js 应用程序中的两个页面之间启用  client-side navigation

client-side navigation 意味着使用 JavaScript 代码 进行页面跳转,这比浏览器执行的默认导航要快。

Linka 标签的区别

<a>   跳转会刷新整个页面, <Link>   则不会

Code splitting and prefetching(代码分割和预加载)

Next.js 会自动进行代码拆分,因此每个页面仅加载该页面所需的内容。 这意味着在呈现主页时,最初不会提供其他页面的代码。

这样可以确保即使你添加数百页也可以快速加载主页。

仅加载你请求的页面的代码也意味着页面被隔离。 如果某个页面抛出错误,则该应用程序的其余部分仍将正常工作。

此外,在 Next.js 的生产版本中,只要 Link 组件出现在浏览器的视口中,Next.js 就会自动在后台预加载接页面的代码。 当你单击链接时,目标页面的代码将已经在后台加载,并且页面转换将很快完成!

总结

Next.js 通过,code splitting(代码分离) , client-side navigation(客户端导航), and prefetching (in production)(预加载)   自动优化你的应用程序以获得最佳性能。

你可以直接在  pages 目录下 创建文件(自动生成路由),并使用内置的 Link 组件。 而不需要其他路由库。

注意: 如果需要链接到 Next.js 应用程序外部的外部页面,需使用 a

Learn <a href="https://www.nextjs.cn">Next.js!</a>

如果你需要添加诸如 className 之类的属性,请将其添加到 a 标签而不是 Link 标签中。 下面一个例子。

// Example: Adding className with <Link>
import Link from "next/link";

export default function LinkClassnameExample() {
  // To add attributes like className, target, rel, etc.
  // add them to the <a> tag, not to the <Link> tag.
  return (
    <Link href='/'>
            
      <a className='foo' target='_blank' rel='noopener noreferrer'>
        Hello World
      </a>
    </Link>
  );
}

三. 资源、元数据和 CSS

在这一节你可以了解到

  • 如何在 Next.js 中添加静态文件(eg:图片)
  • 如何为每个页面自定义 head
  • 如何 在  pages/_app.js   中添加全局样式
  • 如何用 CSS module 构建一个 样式可重用的 React 组件

3.1 Next.js 会把 public 目录当做根路径直接读取静态资源

比如 public 文件夹下有一个 logo.svg 图片 可以像如下使用

<img src='/logo.svg' alt='Vercel Logo' className='logo' />

3.2 自定义文档 head

作用:可以为单页面应用的每一个子页面定义不同的 metaData, title, link 等等

<Head>
  <title>6666p</title>
  <link rel='icon' href='/favicon.ico' />
</Head>

3.3 CSS Styling

Next.js 内部使用一个叫 styled-jsx 的 “CSS-in-JS” 库

它使你可以在 React 组件中编写 CSS ,并且 CSS 样式将受到限制(其他组件不会受到影响)。

Next.js 内置了对 styled-jsx 的支持,但是你也可以使用其他流行的 CSS-in-JS 库,例如 styled-components

方式一: CSS-in-JS

<style jsx>{`
 …

`}</style>

方式二: 编写和导入 CSS

Next.js 具有对 CSS 和 Sass 的内置支持,你可以直接导入 .css 和 .scss 文件。

在本文中,我们也会讨论如何在 Next.js 中编写和导入 CSS 文件。 我们还将讨论 Next.js 对 CSS 模块和 Sass 的内置支持。

3.4 Layout Component

Layout.js

在 根路径 新建 components 文件夹,进入文件夹, 新建 layout.js

export default function Layout({ children }) {
  return <div>{children}</div>;
}

使用方式:

// /pages目录下
import Link from "next/link";
import Head from "next/head";

import Layout from "../../components/layout";

export default function FirstPost() {
  return (
    <Layout>
      <h1>First Post</h1>
    </Layout>
  );
}

添加 CSS

使用 CSS Modules 的方式 为 react 组件添加样式

Important: To use CSS Modules, the CSS file name must end with .module.css.

components 目录下 新建 layout.module.css 文件,内容如下

.container {
  max-width: 36rem;
  padding: 0 1rem;
  margin: 3rem auto 6rem;
}

在 layout.js 中使用

import styles from "./layout.module.css";

export default function Layout({ children }) {
  return <div className={styles.container}>{children}</div>;
}

自动生成全局唯一的 className

这就是 CSS Module 所做的:自动生成唯一的类名。 只要使用 CSS Module,就不必担心类名冲突。

此外,Next.js 的代码拆分功能也可以在 CSS 模块上使用。 它确保为每个页面加载最少的 CSS。 这降低代码打包后的大小。

CSS 模块是在构建时从 JavaScript 捆绑包中提取的,并生成.css 文件,这些文件由 Next.js 自动加载。

3.5 全局样式

CSS Module 是应用于组件级别的样式,但是如果你想每个页面都加载一样的 CSS , Next.js 同样支持,

styles/global.css

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
    Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  line-height: 1.6;
  font-size: 18px;
}

* {
  box-sizing: border-box;
}

a {
  color: #0070f3;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

img {
  max-width: 100%;
  display: block;
}

pages/_app.js

在 pages 目录下 创建 _app.js文件,并加入如下内容

import "../styles/global.css";

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

四. 预渲染和数据获取     

这里你可以了解到

  • Next.js 的 预渲染功能
  • 预渲染的两种方式/形式: 静态生成, 服务端渲染
  • 静态生成 方式 在 两种场景下的使用
    • 需要获取外部数据数据-getStaticProps
    • 不需要获取外部数据

4.1 Pre-rendering(预渲染)

在讨论数据获取之前,让我们谈谈 Next.js 中最重要的概念之一:预渲染。

默认情况下,Next.js 会预渲染每个页面。

这意味着 Next.js 会预先为每个页面生成 HTML,而不是全部由客户端 JavaScript 完成。

预渲染可以带来更好的性能和 SEO。

每个生成的 HTML 都与该页面所需的最少 JavaScript 代码相关联。 当浏览器加载页面时,其 JavaScript 代码将运行并使页面完全具有交互性。 (此过程称为 hydration。)

4.1.1 检查预渲染是否生效

你可以通过执行以下步骤来检查预渲染是否生效:

  1. 在浏览器中禁用 JavaScript(在 Chrome 中是这样),然后…
  2. 刷新浏览器当前页面, 尝试访问此页面。

你应该看到你的应用是在没有 JavaScript 的情况下呈现的。 如果界面展现出较完整的 HTML, 那么说明该网站已将 web 应用预先渲染为静态 HTML,从而使无需运行 JavaScript 即可查看应用界面。

如果你的应用程序是普通的 React.js 应用程序(没有 Next.js),则不会进行预渲染,因此,如果禁用 JavaScript,你将无法看到该 React 应用的正常界面。 例如: 此网站是用普通 React.js 构建的

  1. 禁用 JavaScript 并刷新访问同一页面。
  2. 界面显示“你需要启用 JavaScript 才能运行此应用程序。” 这是因为该应用程序未预先呈现为静态 HTML。

4.1.2 总结: 预渲染 VS 没有预渲染(单页面应用-客户端渲染)

pre-rendering.png

访问浏览器之前, 预先渲染出了 HTML。 在浏览器端访问某一个页面时,再通过 JS 绑定 交互事件!

no-pre-rendering.png

渲染 HTML, 绑定交互事件 都在 浏览器端访问页面时通过 js 进行

4.1.3 预渲染的两种方式

Next.js 具有两种形式的预渲染: 静态生成(Static Generation)服务器端渲染(Server-side Rendering)

这两种方式的不同之处在于为 page(页面)生成 HTML 页面的 时机

static-generation.png

server-side-rendering.png

在开发环境中(当你 执行 npm run dev yarn dev),每个页面的预渲染形式是 服务端渲染 - 尽管给页面设置了 "静态生成"的方式

重要的是,Next.js 允许你为每个页面  选择  预渲染的方式。你可以创建一个 “混合渲染” 的 Next.js 应用程序:对大多数页面使用“静态生成”,同时对其它页面使用“服务器端渲染”。 per-page-basis.png

4.2 使用静态生成和 服务端渲染的时机

我们建议你尽可能使用 静态生成 ,因为你的所有 page(页面)都可以只构建一次并托管到 CDN 上,这比让服务器根据每个页面请求来渲染页面快得多。 还可以对多种类型的页面使用“静态生成”,包括:

  • 营销页面
  • 博客文章
  • 电商产品列表
  • 帮助和文档

你应该问问自己:“我可以在用户请求之前预先渲染此页面吗?” 如果答案是肯定的,则应选择“静态生成”。

另一方面,如果你无法在用户请求之前预渲染页面,则“静态生成” 不是 一个好主意。这也许是因为你的页面需要显示频繁更新的数据,并且页面内容会随着每个请求而变化。

在这种情况下,你可以执行以下任一操作:

  • 将“静态生成”与 客户端渲染 一起使用:你可以跳过页面某些部分的预渲染,然后使用客户端 JavaScript 来填充它们。要了解有关此方法的更多信息,请查看 获取数据 章节的文档。
  • 使用 服务器端渲染: Next.js 针对每个页面的请求进行预渲染。由于 CDN 无法缓存该页面,因此速度会较慢,但是预渲染的页面将始终是最新的。我们将在下面讨论这种方法。

4.2 以静态生成的方式获取数据(在打包构建时获取数据)

静态生成 可以有数据也可以没有数据。

到目前为止,我们创建的所有页面都不需要提取外部数据。 在为应用程序构建应用程序时,将自动静态生成这些页面。

static-generation-without-data.png

但是,对于某些页面,你可能必须先获取一些外部数据才能渲染 HTML。 也许你需要在构建时访问文件系统,获取外部 API 或查询数据库。 Next.js 开箱即用地支持这种情况-带有数据的静态生成。

static-generation-with-data.png

4.2.1 在静态生成的方式下 通过 getStaticProps 获取数据

要在预渲染时获取此数据,Next.js 允许你从同一文件 export(导出) 一个名为 getStaticPropsasync(异步) 函数。(当你导出一个 React 组件时, 也可以导出一个 getStaticProps 异步函数)

  • 该函数在构建时被调用,
  • 并允许你在预渲染时将获取的数据作为 props 参数传递给页面。
function Blog({ posts }) {
  // Render posts...
}

// 此函数在构建时被调用
export async function getStaticProps() {
  // 调用外部 API 获取博文列表
  const res = await fetch("https://.../posts");
  const posts = await res.json();

  // 通过返回 { props: posts } 对象,Blog 模块
  // 在构建时将接收到 `posts` 参数
  return {
    props: {
      posts,
    },
  };
}

export default Blog;

getStaticProps 函数会告诉 Next.js:“嘿,此页面具有某些数据依赖关系-因此,在构建时预渲染此页面时,请确保先请求数据!”

Note: In development mode, getStaticProps runs on each request instead.

4.2.2 添加 接口请求代码

pages/index.js 加入以下代码
import Link from "next/link";

import Layout from "../components/layout";
import { getSortedPostsData } from "../lib/posts";

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      <section>
        <ul>
          {allPostsData.map(({ id, title }) => (
            <li key={id}>
              <Link href={`/posts/${id}`}>
                <a>{title}</a>
              </Link>
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  );
}

export async function getStaticProps() {
  const allPostsData = getSortedPostsData();
  return {
    props: {
      allPostsData,
    },
  };
}
新建 lib/posts.js 文件,加入如下内容

lib/posts.js 中,我们实现了getSortedPostsData,从其他来源(例如外部 API)获取数据

export async function getSortedPostsData() {
  // Instead of the file system,
  // fetch post data from an external API endpoint
  const res = await fetch("..");
  return res.json();
}

注意:Next.js 在客户端和服务器上均会自动导入fetch()。 你不需要手动导入它。

除此之外,你还可以直接查询数据库。 因为getStaticProps 仅在服务器端运行。 它永远不会在客户端运行。 它甚至不会包含针对浏览器 打包后的 JS 代码 中。 这意味着你可以编写诸如直接数据库查询之类的代码,而无需将其发送到浏览器。

4.3 开发环境与生产环境的区别

  • 在开发环境中( npm run devyarn dev), getStaticProps 函数 每次发起请求时都会执行
  • 在生产环境中, getStaticProps 只在构建打包时会执行

由于它是在构建时运行的,因此你将无法使用仅在请求时间内可用的数据,例如 URL 查询参数或 HTTP 头。

4.4 只允许在页面(Page) 内使用

getStaticProps只能从页面导出。 你无法从非页面文件中导出它。 出现此限制的原因之一是,在渲染页面之前,React 需要拥有所有必需的数据。

4.5 如果我需要在请求时获取数据怎么办?

如果你无法在用户请求之前预渲染页面,则不建议静态生成的方式。 也许你的页面显示了频繁更新的数据,并且页面内容在每次请求时都会更改。 在这种情况下,你可以尝试服务器端渲染或跳过预渲染。 **

4.3 以服务端渲染的方式获取数据(在每次请求时获取数据)

如果你不想在 构建时,而想在请求页面时获取数据, 你可以使用** 服务端渲染** server-side-rendering-with-data.png 使用 服务端渲染,你需要在页面中 导出一个 getServerSideProps

export async function getServerSideProps(context) {
  return {
    props: {
      // props for your component
    },
  };
}

因为 getServerSideProps 只在请求被执行,因此他的 参数(context)中 可以取到一些 请求参数

getServerSideProps 使用的时机

使用场景: 必须请求到数据后,再进行预渲染.

到第一个字节(TTFB)的时间将比getStaticProps慢,因为服务器必须在每个请求上计算结果,并且如果没有额外的配置,则 CDN 不能缓存结果。

4.4 客户端渲染

如果你不需要在预渲染之前获取数据, 你可以使用客户端渲染,

静态生成(预渲染)页面的不需要外部数据的部分。 页面加载后,使用 JavaScript 从客户端获取外部数据并填充其余部分。

client-side-rendering.png

例如,此方法适用于用户仪表板页面。 由于信息中心是一个私有的,特定于用户的页面,因此 SEO 无关紧要,并且该页面无需预先呈现。 数据经常更新,这需要获取请求时数据。

4.5 SWR

Next.js 背后的团队创建了一个名为SWR 的 React Hook 来进行数据获取。 如果你要在客户端获取数据,我们强烈建议你这样做。 它处理缓存,重新验证,焦点跟踪,间隔重新获取等等。 我们不会在这里介绍详细信息,但是这里是一个示例用法:

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>;
}