Skip to content

NextJS学习笔记 ​

初始篇-Next.jsCLI ​

创建项目 ​

使用 create-next-app脚手架

shell
npx create-next-app@latest
yarn create next-app
pnpm create next-app

运行项目 ​

查看项目根目录 package.json 文件的代码:

js
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },

我们可以看到脚本命令有 dev、build、start、lint,分别对应开发、构建、运行、代码检查。

开发的时候使用 npm run dev。部署的时候先使用 npm run build 构建生产代码,再执行 npm run start 运行生产项目。运行 npm run lint 则会执行 ESLint 语法检查。

路由篇-AppRouter ​

Pages Router & App Router ​

以前我们声明一个路由,只用在 pages 目录下创建一个文件就可以了,以前的目录结构类似于:

js
└── pages
    ├── index.js
    ├── about.js
    └── more.js

这种方式有一个弊端,那就是 pages 目录的所有 js 文件都会被当成路由文件,这就导致比如组件不能写在 pages 目录下,这就不符合开发者的使用习惯。

升级为新的 App Router 后,现在的目录结构类似于:

js
src/
└── app
    ├── page.js 
    ├── layout.js
    ├── template.js
    ├── loading.js
    ├── error.js
    └── not-found.js
    ├── about
    │   └── page.js
    └── more
        └── page.js

如果你想使用 Pages Router,只需要在 src 目录下创建一个 pages 文件夹或者在根目录下创建一个 pages文件夹。其中的 JS 文件会被视为 Pages Router 进行处理。 但是要注意,虽然两者可以共存,但 App Router 的优先级要高于 Pages Router。而且如果两者解析为同一个 URL,会导致构建错误。

定义路由(Routes) ​

首先是定义路由,文件夹被用来定义路由。每个文件夹都代表一个对应到 URL 片段的路由片段。创建嵌套的路由,只需要创建嵌套的文件夹。举个例子, app/dashboard/settings目录对应的路由地址就是 /dashboard/settings:

定义页面(Pages) ​

那如何保证这个路由可以被访问呢?你需要创建一个特殊的名为 page.js 的文件。至于为什么叫 page.js呢?除了 page 有“页面”这个含义之外,你可以理解为这是一种约定或者规范。(如果你是 Next.js 的开发者,你也可以约定为 index.js甚至 yayu.js!) image.png 在上图这个例子中:

  • app/page.js 对应路由 /
  • app/dashboard/page.js 对应路由 /dashboard
  • app/dashboard/settings/page.js 对应路由/dashboard/settings
  • analytics 目录下因为没有 page.js 文件,所以没有对应的路由。这个文件可以被用于存放组件、样式表、图片或者其他文件。

当然不止 .js文件,Next.js 默认是支持 React、TypeScript 的,所以 .js、.jsx、.tsx 都是可以的。

定义布局(Layouts) ​

目录结构

js
└── app
    └── dashboard
        ├── layout.js
        ├── page.js
        └── settings.js
            └── page.js

相关代码如下:

js
// app/dashboard/layout.js
export default function DashboardLayout({
  children,
}) {
  return (
    <section>
      <nav>nav</nav>
      {children}
    </section>
  )
}
js
// app/dashboard/page.js
export default function Page() {
  return <h1>Hello, Dashboard!</h1>
}

你可以发现:同一文件夹下如果有 layout.js 和 page.js,page 会作为 children 参数传入 layout。换句话说,layout 会包裹同层级的 page。

js
// app/dashboard/settings/page.js
export default function Page() {
  return <h1>Hello, Settings!</h1>
}

你可以发现:布局是支持嵌套的,app/dashboard/settings/page.js 会使用 app/layout.js 和 app/dashboard/layout.js 两个布局中的内容

根布局(Root Layout) ​

布局支持嵌套,最顶层的布局我们称之为根布局(Root Layout),也就是 app/layout.js。它会应用于所有的路由。除此之外,这个布局还有点特殊。

js
// app/layout.js
import './globals.css'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  )
}

其中:

  1. app 目录必须包含根布局,也就是 app/layout.js 这个文件是必需的。
  2. 根布局必须包含 html 和 body标签,其他布局不能包含这些标签。如果你要更改这些标签,不推荐直接修改
  3. 你可以使用[路由组]创建多个根布局。
  4. 默认根布局是[服务端组件],且不能设置为客户端组件。

定义模板(Templates) ​

用法跟布局一模一样。它们最大的区别就是状态的保持。如果同一目录下既有 template.js 也有 layout.js,最后的输出效果如下:

js
<Layout>
  {/* 模板需要给一个唯一的 key */}
  <Template key={routeParam}>{children}</Template>
</Layout>

也就是说 layout 会包裹 template,template 又会包裹 page。

某些情况下,模板会比布局更适合:

  • 依赖于 useEffect 和 useState 的功能,比如记录页面访问数(维持状态就不会在路由切换时记录访问数了)、用户反馈表单(每次重新填写)等

  • 更改框架的默认行为,举个例子,布局内的 Suspense 只会在布局加载的时候展示一次 fallback UI,当切换页面的时候不会展示。但是使用模板,fallback 会在每次路由切换的时候展示

定义加载界面(Loading UI) ​

App Router 提供了用于展示加载界面的 loading.js。

这个功能的实现借助了 React 的Suspense API。关于 Suspense 的用法,可以查看 《React 之 Suspense》。它实现的效果就是当发生路由变化的时候,立刻展示 fallback UI,等加载完成后,展示数据。

loading.js的代码如下:

js
// app/dashboard/loading.js
export default function DashboardLoading() {
  return <>Loading dashboard...</>
}

同级的 page.js 代码如下:

js
// app/dashboard/page.js
async function getData() {
  await new Promise((resolve) => setTimeout(resolve, 3000))
  return {
    message: 'Hello, Dashboard!',
  }
}
export default async function DashboardPage(props) {
  const { message } = await getData()
  return <h1>{message}</h1>
}

loading.js 的实现原理是将 page.js和下面的 children 用 <Suspense> 包裹。因为page.js导出一个 async 函数,Suspense 得以捕获数据加载的 promise,借此实现了 loading 组件的关闭。

当然实现 loading 效果,不一定非导出一个 async 函数。也可以借助 React 的 use 函数。现在我们在 dashboard下新建一个 about目录,在其中新建 page.js文件。

js
// /dashboard/about/page.js
import { use } from 'react'

async function getData() {
  await new Promise((resolve) => setTimeout(resolve, 5000))
  return {
    message: 'Hello, About!',
  }
}

export default function Page() {
  const {message} = use(getData())
  return <h1>{message}</h1>
}

特殊文件的层级问题 ​

目录结构

js
  ├── layout.js
  ├── template.js
  ├── error.js
  ├── loading.js
  ├── noy-found.js
  └── page.js
js
   <Layout>
     <Template key={routeParam}>
        <ErrorBoundary fallback={<Error />}>
          <Suspense fallback={<Loading />}>
            <ErrorBoundary fallback={<NotFound />}>
              <Page />
            </ErrorBoundary>
          </Suspense>
        </ErrorBoundary>
      </Template>
    </Layout>

定义错误处理(Error Handling) ​

再讲讲特殊文件 error.js。顾名思义,用来创建发生错误时的展示 UI。

其实现借助了 React 的 Error Boundary 功能。简单来说,就是给 page.js 和 children 包了一层 ErrorBoundary。

链接与导航 ​

Next.js 的<Link>组件是一个拓展了原生 HTML <a> 标签的内置组件,用来实现预获取(prefetching) 和客户端路由导航。这是 Next.js 中路由导航的主要和推荐方式。

js
import Link from 'next/link'
 
export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}

支持路由链接动态渲染: ​

js
import Link from 'next/link'
 
export default function PostList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

获取当前路径名 ​

如果需要对当前链接进行判断,你可以使用 usePathname() ,它会读取当前 URL 的路径名(pathname)。示例代码如下:

js
'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export function Navigation({ navLinks }) {
 const pathname = usePathname()

 return (
   <>
     {navLinks.map((link) => {
       const isActive = pathname === link.href

       return (
         <Link
           className={isActive ? 'text-blue' : 'text-black'}
           href={link.href}
           key={link.name}
         >
           {link.name}
         </Link>
       )
     })}
   </>
 )
}

跳转行为设置 ​

App Router 的默认行为是滚动到新路由的顶部,或者在前进后退导航时维持之前的滚动距离。

如果你想要禁用这个行为,你可以给 <Link> 组件传递一个 scroll={false}属性,或者在使用 router.push和 router.replace的时候,设置 scroll: false:

js
// next/link
<Link href="/dashboard" scroll={false}>
  Dashboard
</Link>
js
// useRouter
import { useRouter } from 'next/navigation'
 
const router = useRouter()
 
router.push('/dashboard', { scroll: false })