Next.js学习笔记

635 阅读7分钟

Next.js学习笔记

环境搭建

npx create-next-app@latest,会自动为你设置所有内容,下载没有下载的依赖

可以加上 --typescript来使用typescript

使用npm run dev来启动开发环境服务器,自动运行在3000端口

项目目录结构&初步认知

image-20220518153753224.png

其中pages是存放具体页面的地方

目前感受到的一个好处是pages自动配置了路由

比如这里我写了一个about.jsx,我并不需要配置路由,nex.tjs直接配置了路由,我可以直接访问http://localhost:3000/about

同时next.js可以使用动态路由pages/posts/[id].js => post/1 ,post/2 ...

同时next.js支持 静态文件服务,在public中的文件可以被直接访问到,比如我们可以直接访问 http://localhost:3000/favicon.ico

对于路由不匹配的情况,也自动进行了捕获和处理

image-20220518154157357.png

静态渲染

next.js一个突出的优点就是它可以静态渲染页面:如果一个页面使用了 静态生成,在 构建时(build time) 将生成此页面对应的 HTML 文件 。这意味着在生产环境中,运行 next build 时将生成该页面对应的 HTML 文件。然后,此 HTML 文件将在每个页面请求时被重用,还可以被 CDN 缓存。

每个生成的 HTML 文件都与该页面所需的最少 JavaScript 代码相关联。当浏览器加载一个 page(页面)时,其 JavaScript 代码将运行并使页面完全具有交互性。(此过程称为 水合(hydration) 。)这可以带来更好的性能和 搜索引擎优化效果。

我的理解就是next.js预先将一个page,即一个react组件所需的所有资源(组件中的jsx代码、组件依赖的css样式、包括组件引入的其他custom hooks或者子组件)渲染成一个静态的html文件。这自然能够更好的提升性能:毕竟只需要一个html文件,比之前的网络请求减少太多了;同时搜索引擎等爬虫也更好找到,毕竟是静态的html,而不是动态的js。

那么这个静态的页面怎么样使用动态的数据(比如需要从服务器端获取的)甚至动态的路由呢(比如post/1)呢?

next.js提供了两个异步函数用来获取动态数据:**getStaticProps用于获取页面内容依赖的数据,而getStaticPaths**用来获取路由依赖的动态数据

我们可以通过官方的例子来看这两个函数的作用:假设现在有一个博客首页,里面需要从服务器端获取博客标题渲染成一个列表,同时需要点击具体的博客进入博客阅读页面。

我们有一个Blog组件,其中需要使用posts这个参数,它是由具体的博客组成的数组,而博客中有idtitle两个属性。

我们用getStaticPathshttps://.../posts发送请求获得了posts数据,使用其中的id属性组成paths数组,这个数组就是动态路由依赖的数组。

getStaticProps也是类似的获得了post数据并直接返回给了Blog组件进行使用。

Blog组件中,我们使用map语法取出每一篇post,使用<Link>进行路由跳转,在路由跳转中使用动态路由:/blog/${encodeURIComponent(post.slug)}

function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.id)}`}>
            <a>{post.title}</a>
          </Link>
        </li>
      ))}
    </ul>
  )
}
​
​
// 此函数在构建时被调用
export async function getStaticPaths() {
  // 调用外部 API 获取博文列表
  const res = await fetch('https://.../posts')
  const posts = await res.json()
​
  // 据博文列表生成所有需要预渲染的路径
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))
​
  // 仅会在build时重新渲染paths
  // { fallback: false } 表明未匹配到的路由将会返回404
  // { fallback: 'blocking' } 表明会直接不处理(静默取消)未匹配到的路由
  return { paths, fallback: false }
}
​
​
// 此函数在构建时被调用
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

而在具体的博客页面,我们可以使用router.query来获取路由的信息并使用,比如在post/[id]/index.js中,可能有这样的组件结构:

我们用useRouter这个next.js提供的custom hook中的router中的router.query获得query参数,即路由参数,并用es6的解构语法提取出id渲染在页面中。

import { useRouter } from 'next/router'
import Link from 'next/link'const Post = () => {
  const router = useRouter()
  const { id } = router.query
​
  return (
      ...
      <h1>Post: {id}</h1>
      ...
  )
}
​
export default Post

getStaticPorps 进阶操作

动态更新

如果须要动态更新已经渲染好的经静态页面,可以用revaliate来表明更新的频率:

export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()
​
  return {
    props: {
      posts,
    },
    // Next.js 会尝试在每10秒为新的请求重新生成页面
    revalidate: 10, // 单位是秒
  }
}

读取文件

有些时候我们需要从静态文件中读取数据,比如静态博客(直接上传md文件即可生成页面,比如hexo等),这时我们就需要在getStaticProps中去获取静态文件中的数据,这时我们可以使用fs这个库

import { promises as fs } from 'fs'
import path from 'path'
​
...
​
export async function getStaticProps() {
  const postsDirectory = path.join(process.cwd(), 'posts')
  const filenames = await fs.readdir(postsDirectory)
​
  const posts = filenames.map(async (filename) => {
    const filePath = path.join(postsDirectory, filename)
    const fileContents = await fs.readFile(filePath, 'utf8')
​
    // 通常还需要进一步转换这些fileContents,比如md转HTMl
    
    return {
      filename,
      content: fileContents,
    }
  })
  return {
    props: {
      posts: await Promise.all(posts),
    },
  }
}

getStaticPaths 进阶操作

除了直接匹配单个动态路由,我们还可以匹配多个动态路由、不同层级的路由甚至兜底的路由。

多个动态路由

考虑pages/posts/[id].js匹配到posts/1posts/2等,自然会想到是否可以更进一步匹配某个动态路由下的动态路由呢?答案当然是可以的,比如我们可以使用pages/posts/[postId]/[commentId]去匹配posts/1/2等,但是这时params应该同时包含postIdcommentId

...
const paths = posts.map((post) => ({
    params: { 
        postId: post.id,
        commentId: post.comment.id
    },
}))
​
return { paths, fallback: false }

不同层级的路由

有时候我们并不知道需要匹配多少层级,比如上面的例子:我们匹配了文章,还匹配了文章下的评论。我们可以用slug来实现同时匹配多层层级的需求

slug意为鼻涕虫,想象它拖在地上缓慢行走的样子,就能理解为什么是同时匹配多层,我个人喜欢理解成“惰性”

必须我们可以使用pages/[...slug]来接收所有路由参数,同时我们需要在params中包含一个slug数组。比如当slug数组是['foo', 'bar']时,Next.js会静态生成/foo/bar

兜底路由

如果使用假值作为路由参数,会自动被/捕获,实现兜底。假值包括null,[],undefined,false,比如说在slug数组中如果为空或者直接设置slug:false,那么就会返回首页,即/

定制错误页面

Next.js可以非常方便的定制错误页面,只需要在pages文件夹下新建[状态码名].js即可,比如我们可以新建pages/404.js来定制状态码为404时的页面。

export default function Custom404() {
    return <h1>定制404页面,同样也可以定制其他状态码如500</h1>
  }

api路由

可以在pages/api下构建自己的api文件,这些文件只会在服务端文件包中存在,build之后是不会被包含的。

一个典型的api路由是这样的:

我们需要两个参数:

req用来接收:一个http.IncomingMessage实例,以及一些 预先构建的中间件,比如可以用req.method来判断请求方法。关于http.IncomingMessage的具体api可以参考amery2010.gitbooks.io/nodejs-api-…

而res用来接收一个http.ServerResponse实例,以及一些 辅助函数。比如这里使用的res.statusres.json

export default function handler(req, res) {
  res.status(200).json({ name: 'John Doe' })
}
​

部署

Next.js 可以部署到任何支持 Node.js 的托管提供商处。确保你的 package.json 文件中设置了 package.json 文件中存在 "build""start" 两个构建命令。

执行 next build 命令将在 .next 文件夹中构建出用于生产环境的应用程序。构建之后,执行 next start 命令启动一个支持 混合页面(hybrid pages) 的 Node.js 服务程序,该服务程序将同时服务于静态生成的页面和服务器端渲染的页面。

而如果想要将 Next.js 应用程序导出为静态 HTML,可以使用命令next export来导出静态页面,页面会被导出到一个名为out的文件夹中