Page 路由基本用法

page 路由

从pages文件夹中的文件导出的每个 React 组件都是一个页面。

页面的路由跟它们的文件名是相关联的。举个例子:

  1. pages/index.js关联的是/
  2. pages/posts/first-post.js关联到/posts/first-post.js路由

路由跳转:

jsx
1import Link from 'next/link'
2;<Link href="/posts/first-post">this page!</Link>

Link组件允许在两个页面间进行客户端导航。

客户端导航意味着页面会的变化是用 JavaScript 进行的,这比浏览器默认的导航更快。

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

这样可以确保即使您有数百个页面,主页也能快速加载。

只加载您请求的页面的代码也意味着页面将被隔离。如果某个页面抛出错误,那么应用程序的其余部分仍然可以工作。

此外,在 Next.js 的生产版本中,每当链接组件出现在浏览器的视口中时,Next.js 都会自动在后台预取链接页面的代码。

当你点击链接时,目标页面的代码已经在后台加载!

静态资源

当静态资源放在public文件夹内时,在public内的资源可以被pages里的文件从根目录中引入。

例如,在 public文件夹内建立一个images文件夹并放入图片,则可以这么引入:

js
1import Image from 'next/image'
2;<Image src="/images/profile.jpg" alt="Your Name" />

对于图片资源,nextjs 提供了Image组件来处理以下功能:

  • 图片响应不同的屏幕尺寸
  • 用三方工具对图片资源进行优化
  • 仅在图像进入视口时加载图像

next/image是HTML<img>元素的扩展,它允许在浏览器支持的情况下,以 WebP 等现代格式调整、优化和提供图像。这样可以避免将大型图像运送到视口较小的设备。它还允许 Next.js 自动采用未来的图像格式,并将其提供给支持这些格式的浏览器。

meta 数据

如果我们想要在页面中定义 meta 数据,例如<title>标签,我们可以使用Head组件。

jsx
1import Head from 'next/head'
2;<Head>
3  <title>Create Next App</title>
4  <link rel="icon" href="/favicon.ico" />
5</Head>

<Head>是用来替代<head>标签的。

在 pages里定义的每个文件里都可以用<Head>标签,当加载到该页面时,nextjs 就能够把<Head>里面的 meta 数据给注入进去。

引入第三方script脚本外链的方式则是用Script组件:

jsx
1import Script from 'next/script'
2export default function FirstPost() {
3  return (
4    <>
5      <Head>
6        <title>First Post</title>
7      </Head>
8      <Script
9        src="https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.7/dayjs.min.js"
10        strategy="lazyOnload"
11        onLoad={() => console.log(`script loaded correctly, window.dayjs has been populated`)}
12      />
13      <h1>First Post</h1>
14      <h2>
15        <Link href="/">← Back to home</Link>
16      </h2>
17    </>
18  )
19}

Script组件定义了一些附加属性:

  1. strategy控制第三方脚本何时加载。lazyOnload告诉 Next.js 在浏览器空闲时间延迟加载这个特定的脚本。
  2. onLoad用于在脚本完成加载后立即运行任何 JavaScript 代码

上面的代码会将 script 里的内容引进页面中,输入window.dayjs看到这个全局变量就表示引入成功了。

css

如果想要设置全局 css,则可以在pages/_app.tsx中引入,例如:

js
1// pages/_app.tsx
2import 'src/styles/globals.css'

_app.tsx 除了加载全局 css 外,还可以在这里处理全局 state。

除了_app.tsx外,任何其他的地方都不能引入全局 css

其次是 CSS Modules,nextjs 强制要求 css 的文件名格式为:.module.css

下面有一个 layout.module.css 文件

css
1.container {
2  padding: 0 1rem;
3}

在组件中使用实例:

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

使用这种方式引入的className会自动加入 hash 值,类似layout_container_2t4v2这样的,所以并不需要担心类名重复问题。

同时,Nextjs 的代码分割功能对 css 模块也有作用,它会让每个页面都加载最少的 css。

这些操作会在 Nextjs 构建时进行,并且生成 css 文件并且在加载到页面中。

Pre-rendering

Nextjs 最重要的功能是 Pre-rendering(预渲染),所有页面都默认使用预渲染,这意味着 Nextjs 会提前给每个页面生成 HTML 而不是全部使用 JavaScript 做客户端渲染。这对性能以及 SEO 有好处。

每个生成的 HTML 都与该页面下最小化所需的 JavaScript 相关联。当页面在浏览器中加载后,它对应的 JavaScript 代码会启动并且使页面变得可交互(这个过程叫做 hydration,中文翻译是水合)。

Nextjs 给出了两张图来对比 预渲染与非预渲染的不同:

预渲染

img

  • 预渲染的页面被展示
  • React 组件被初始化完成,产生水合作用——让页面变得可交互(比如加上了各种事件)

非预渲染

img

  • 页面没有内容
  • React 组件被初始化完成并且页面变得可交互(Hydration)

SSG 和 SSR

Nextjs 有两种方式的预渲染分别是 Static Generation(简称 SSG)和 Server-side Rendering(简称 SSR)。

他们的区别在于:

  • Static Generation:在 build 时生成所有 HTML 页面。

    img

  • Server-side Rendering:在前端页面请求时才生成对应的 HTML 页面。

img

在开发模式下,每个请求都会预呈现页面。这也适用于静态生成,使其更易于开发。在投入生产时,静态生成将在构建时发生一次,而不是在每次请求时都发生。

选择 SSG 还是 SSR?

Next.js 能够让用户选择该页面下到底是用 SSG 还是 SSR,我们可以混合使用 SSG 或者 SSR。

img

推荐尽可能地使用 SSG 模式因为页面能够只被构建一次然后被保存到 CDN 中,这样能让页面在请求下变得更快。

如果我们需要在用户请求前就把页面准备好,那么就使用 SSG。

另一方面,如果用户请求前我们没办法把内容准备好的话,那么就只能使用 SSR。

SSR 会慢一点,但预渲染的页面将始终是最新的。或者,您可以跳过预渲染,使用客户端 JavaScript 来填充频繁更新的数据。

fetching data

不管是 SSG 还是 SSR,都需要数据来页面内容。前面说了 SSG 在构建时把 HTML 页面给打包好,SSR 是在用户请求时才把页面生成出来然后呈现给用户,所以两者之间处理数据的方式肯定也不同:

  • SSG 将数据提前打包到文件系统中,build 的页面会执行 JavaScript 从文件系统中获取数据。
  • SSR 是动态数据,需要在生成页面前从数据库获取好数据,然后塞到生成的页面中。

getStaticProps

在导出一个 page 组件时,我们还可以同样导出一个async函数名字叫getStaticProps。

  • getStaticProps会在生产环境构建时执行
  • 在函数里面,我们可以获取额外的数据并且把它作为 props 发送给 page
js
1export default function Home(props) { ... }
2
3export async function getStaticProps() {
4  // Get external data from the file system, API, DB, etc.
5  const data = ...
6
7  // The value of the `props` key will be
8  //  passed to the `Home` component
9  return {
10    props: ...
11  }
12}

getStaticProps函数相当于我们告诉 Nextjs:预渲染该页面时,在生产环境打包前调用该方法获取额外的数据。

getStaticProps仅在服务端执行,所以我们不但可以从文件系统中获取数据,还可以从数据库、API 等地方获取数据。

js
1export async function getSortedPostsData() {
2  // Instead of the file system,
3  // fetch post data from an external API endpoint
4  const res = await fetch('..')
5  return res.json()
6}
js
1import someDatabaseSDK from 'someDatabaseSDK'
2
3const databaseClient = someDatabaseSDK.createClient(...)
4
5export async function getSortedPostsData() {
6  // Instead of the file system,
7  // fetch post data from a database
8  return databaseClient.query('SELECT posts...')
9}

由于 SSG 在构建时仅发生一次,因此它不适合频繁更新或者根据每个用户请求不同的数据。

如果数据频繁发生变化,那么就需要使用 SSR。

getServerSideProps

如果我们希望在用户请求时获取数据,那么就得用Server-side Rendering。

仅需在 page 页面中使用getServerSideProps即可:

js
1export async function getServerSideProps(context) {
2  return {
3    props: {
4      // props for your component
5    },
6  }
7}

由于getServerSideProps函数会在请求时执行,所以有一个context参数包含请求的描述参数。

只有在需要预呈现必须在请求时提取数据的页面时,才应使用 getServerSideProps。

当然,如果对 SEO 要求没那么高的话,就可以使用Client-side Rendering,页面加载完成后才发送请求。

SWR

Next.js 背后的团队创建了一个叫 SWR 的库来帮助请求数据,可以用来在客户端渲染时获取数据,它会处理缓存、间隔重新触发等功能。

js
1import useSWR from 'swr'
2
3function Profile() {
4  const { data, error } = useSWR('/api/user', fetch)
5
6  if (error) return <div>failed to load</div>
7  if (!data) return <div>loading...</div>
8  return <div>hello {data.name}!</div>
9}

Dynamic Routes

getStaticPaths

如果一个页面使用动态路由,并且使用getStaticProps,需要用到getStaticPaths这个方法。

跟getStaticProps一样,getStaticPaths也只能在构建时执行一次。

该方法可以处理静态的路径,并且将某些参数传递给getStaticProps。

getStaticPaths内可以处理:

  • 数据库数据
  • 文件系统
  • ...

以文件系统为例,在 pages 目录下建[id].tsx文件,并且创建三个方法:

js
1import Layout from '../../components/layout'
2
3export default function Post() {
4  return <Layout>...</Layout>
5}
6
7export async function getStaticPaths() {
8  // Return a list of possible value for id
9}
10
11export async function getStaticProps({ params }) {
12  // Fetch necessary data for the blog post using params.id
13}

在 getStaticPaths中,我们可以获取文件系统的路径,例如在src/docs下,分别放了两个md文件(内容随意)。

然后就用fs.readdirSync方法来获取这个目录下所有的文件,这里将处理逻辑放在getAllPostIds里。

js
1export function getAllPostIds() {
2  const fileNames = fs.readdirSync(postsDirectory)
3  console.log('🚀🚀🚀🚀🚀 - file: posts.ts:10 - getAllPostIds - fileNames:', fileNames)
4  // Returns an array that looks like this:
5  // [
6  //   {
7  //     params: {
8  //       id: '文件 a'
9  //     }
10  //   },
11  //   {
12  //     params: {
13  //       id: '文件 b'
14  //     }
15  //   }
16  // ]
17  return fileNames.map((fileName) => ({
18    params: {
19      id: fileName.replace(/\.md$/, ''),
20    },
21  }))
22}

需要注意的是,这里的返回值需要是一个对象的数组。

接着将getAllPostIds放到getStaticPaths中。

js
1export async function getStaticPaths() {
2  // Return a list of possible value for id
3  const paths = getAllPostIds()
4  return {
5    paths,
6    fallback: false,
7  }
8}

使用getStaticPaths返回两个属性,分别是paths和fallback。

nextjs会遍历 paths,然后将每个对象里的属性传递给getStaticProps。

下面处理getStaticProps的逻辑:

js
1export async function getStaticProps({ params }) {
2  const postData = await getPostData(params.id)
3  return {
4    props: {
5      postData,
6    },
7  }
8}

path的 params属性已经透传到getStaticProps中了,我们可以用它来处理获取数据的逻辑。

这里的getPostData方法是拷贝了官方文档中的逻辑,用remark和remark-html来将 markdown 数据解析成 html。

js
1const postsDirectory = path.join(process.cwd(), 'src', 'docs')
2
3export async function getPostData(id: string) {
4  const fullPath = path.join(postsDirectory, `${id}.md`)
5  const fileContents = fs.readFileSync(fullPath, 'utf8')
6
7  // Use gray-matter to parse the post metadata section
8  const matterResult = matter(fileContents)
9
10  // Use remark to convert markdown into HTML string
11  const processedContent = await remark().use(html).process(matterResult.content)
12  const contentHtml = processedContent.toString()
13
14  // Combine the data with the id and contentHtml
15  return {
16    id,
17    contentHtml,
18    ...matterResult.data,
19  }
20}

我们从创建的 md 文件中获取到了 id(其实就是文件名)以及 解析后的 HTML。

再通过getStaticProps将数据传递给 page 组件

jsx
1export default function Post({ postData }) {
2  return (
3    <div>
4      {postData.title}
5      <br />
6      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
7    </div>
8  )
9}

动态路由就完成了。

Fallback

在getStaticPaths中,我们返回了两个值,分别是paths和fallback,fallback 可以有三个值:

  • false:只会在 build 的时候构建制定的页面,如果没有指定则 404

  • true:访问没有定义的页面时不会返回 404,但会传递一个isFallback状态给路由页面,并且在服务器中再调用一次getStaticProps方法,完成后,浏览器会收到JSON生成的数据并渲染出fallback页面。

    同时,Next.js 将此路径缓存到预渲染页面列表中,对同一路径的后续请求将提供第一次生成的页面,就像在构建时预呈现的其他页面一样。

    js
    1export default function Post({ postData }) {
    2  const router = useRouter()
    3  if (router.isFallback) {
    4    return <div>Loading...</div>
    5  }
    6  return (
    7    <div>
    8      {postData?.title}
    9      <br />
    10      <div dangerouslySetInnerHTML={{ __html: postData?.contentHtml }} />
    11    </div>
    12  )
    13}
    14
    15export async function getStaticProps({ params }) {
    16  // { params:'c' }
    17  let postData
    18  try {
    19    postData = await getPostData(params?.id)
    20  } catch (error) {
    21    postData = { title: 'error' }
    22  }
    23
    24  return {
    25    props: {
    26      postData,
    27    },
    28  }
    29}

    上面示例代码中,如果在前端页面访问/posts/c时,由于没有 c.md这个文件,所以会报错。

    上面使用一个错误处理对这种情况进行降级。

    在getStaticProps请求完成之前,页面中拿到的isFallback = true,故而会有一个 loading的效果。

  • block:访问没有定义的页面时不会返回 404,也不会传递 isFallback给路由页面。但会将页面生成 SSR 并返回,这种方式可能有点慢,但是也只生成一次,生成完后会缓存到预渲染列表中。

all Routes

把...符号添加到[]里,就可以捕获所有的路由。

pages/posts/[...id].js可以被/posts/a、/posts/a/b以及/posts/a/b/c等捕获。

如果要这么做的话,就需要在getStaticPaths中返回一个数组:

js
1return [
2  {
3    params: {
4      // Statically Generates /posts/a/b/c
5      id: ['a', 'b', 'c'],
6    },
7  },
8  //...
9]

在getStaticProps中获取的数据是这样的

js
1export async function getStaticProps({ params }) {
2  // params.id will be like ['a', 'b', 'c']
3}

404 页面

默认情况下,Nextjs 会提供 404 页面,如果想要自定义则需要在 page 中手动创建。

jsx
1// pages/404.js
2export default function Custom404() {
3  return <h1>404 - Page Not Found</h1>
4}

API 路由

示例

pages/api文件夹用来处理 API 路由的,在里面的文件以及函数能够被部署成类似于 Serverless 的 function,类似于Lambdas。

以下是示例。

ts
1// pages/api/hello
2import type { NextApiRequest, NextApiResponse } from 'next'
3
4type Data = {
5  text: string
6}
7
8export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
9  res.status(200).json({ text: 'Hello' })
10}

当前端请求/api/hello时,可以得到{ text: 'Hello' }

使用规则

不要在getStaticProps或者getStaticPaths中使用 API 路由。

因为这两个函数只会在 server 端运行而不会在浏览器端运行,同时也不会存在于浏览器JS bundle 中,所以我们应该直接在里面做数据库查询操作。

一个好的使用例子是处理 form 表单输入。

我们可以在前端页面创建表单输入框,然后把内容post到 API 路由中,由于 API 路由不会打包到前端的bundle中,所以我们可以直接写一些数据库操作,把数据保存到数据库里。

js
1export default function handler(req, res) {
2  const email = req.body.email
3  // Then save email to your database, etc...
4}