NextJS学习笔记
初始篇-Next.jsCLI
创建项目
使用 create-next-app脚手架
npx create-next-app@latest
yarn create next-app
pnpm create next-app运行项目
查看项目根目录 package.json 文件的代码:
"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 目录下创建一个文件就可以了,以前的目录结构类似于:
└── pages
├── index.js
├── about.js
└── more.js这种方式有一个弊端,那就是 pages 目录的所有 js 文件都会被当成路由文件,这就导致比如组件不能写在 pages 目录下,这就不符合开发者的使用习惯。
升级为新的 App Router 后,现在的目录结构类似于:
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!) 在上图这个例子中:
app/page.js对应路由/app/dashboard/page.js对应路由/dashboardapp/dashboard/settings/page.js对应路由/dashboard/settingsanalytics目录下因为没有page.js文件,所以没有对应的路由。这个文件可以被用于存放组件、样式表、图片或者其他文件。
当然不止 .js文件,Next.js 默认是支持 React、TypeScript 的,所以 .js、.jsx、.tsx 都是可以的。
定义布局(Layouts)
目录结构
└── app
└── dashboard
├── layout.js
├── page.js
└── settings.js
└── page.js相关代码如下:
// app/dashboard/layout.js
export default function DashboardLayout({
children,
}) {
return (
<section>
<nav>nav</nav>
{children}
</section>
)
}// app/dashboard/page.js
export default function Page() {
return <h1>Hello, Dashboard!</h1>
}你可以发现:同一文件夹下如果有 layout.js 和 page.js,page 会作为 children 参数传入 layout。换句话说,layout 会包裹同层级的 page。
// 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。它会应用于所有的路由。除此之外,这个布局还有点特殊。
// 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>
)
}其中:
app目录必须包含根布局,也就是app/layout.js这个文件是必需的。- 根布局必须包含
html和body标签,其他布局不能包含这些标签。如果你要更改这些标签,不推荐直接修改 - 你可以使用[路由组]创建多个根布局。
- 默认根布局是[服务端组件],且不能设置为客户端组件。
定义模板(Templates)
用法跟布局一模一样。它们最大的区别就是状态的保持。如果同一目录下既有 template.js 也有 layout.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的代码如下:
// app/dashboard/loading.js
export default function DashboardLoading() {
return <>Loading dashboard...</>
}同级的 page.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文件。
// /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>
}特殊文件的层级问题
目录结构
├── layout.js
├── template.js
├── error.js
├── loading.js
├── noy-found.js
└── page.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。
链接与导航
<Link>组件
Next.js 的<Link>组件是一个拓展了原生 HTML <a> 标签的内置组件,用来实现预获取(prefetching) 和客户端路由导航。这是 Next.js 中路由导航的主要和推荐方式。
import Link from 'next/link'
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>
}支持路由链接动态渲染:
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)。示例代码如下:
'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:
// next/link
<Link href="/dashboard" scroll={false}>
Dashboard
</Link>// useRouter
import { useRouter } from 'next/navigation'
const router = useRouter()
router.push('/dashboard', { scroll: false })