Next入门篇
Next是什么?
是基于React的应用层框架,提供了很多内置功能,能快速的构建Web应用程序。 包括的功能有:
- 内置路由
- 静态和服务端渲染
- TS
- 完善的打包配置
简单使用
官方提供了一个create-next-app脚手架工具来使用,可以快速创建一个Next项目。
自动创建项目
IMPORTANT
node版本要求:18.17以上
npx create-next-app@latest其他各种包管理工具都提供了模板:
yarn create next-apppnpm create next-app然后按照提示进行选择,就可以创建一个Next项目。 
另外还有很多的实例模板,可以在github仓库中看到。通过这些实例模板,可以学习Next如何在各种环境中使用的。
可以通过--example参数直接使用这些模板。
npx create-next-app --example [example-name] [project-name]手动创建项目
通过手动创建一个Next项目,可以更好的学习Next,以及它到底依赖了哪些东西。
- 创建项目文件夹
mkdir next-demo- 进入文件夹安装相关依赖
pnpm i next@latest react@latest react-dom@latest- 修改
package.json
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},- 添加文件
app/page.js以及app/layout.js
在新的Next版本中,已经从page路由转成了app路由,所以需要有一个app文件夹
- layout.js:提供布局文件,容器
- page.js:提供页面文件
还有一些其他的文件,在后面路由章节会介绍
比如上面的访问/路径时,会先进入layout.js渲染,然后再渲染page.js
Next CLI常见命令
dev:启动一个本地服务,就可以看到页面了build:打包服务,不过在next: 14.2.4版本发现,该命令和dev命令好像有冲突,需要停掉dev命令start:查看打包后的文件lint:执行lint
路由介绍(App Router)
从一开始page router到现在的app router。在next中路由并没有单独的配置文件,而是通过文件路径来配置路由的,另外也可以通过文件来处理常见的页面状态。
NOTE
从 v13.4 之后,App Router成了默认的路由方案。 当然,Page Router也是兼容的,App Router的优先级要高一些,并且如果两者路由一致,会有冲突,导致构建错误。
. src
├─ app
│ ├─ page.js
│ ├─ layout.js
│ ├─ demo
│ │ ├─ about
│ │ │ ├─ page.js
│ │ ├─ page.js
│ │ ├─ loading.js
│ │ ├─ error.js
│ │ ├─ layout.js
│ │ ├─ template.js首先介绍一下这几个有着特殊名称的文件作用:
layout.xx:布局作用,相当于一个容器,会自动将同级的page.xx作为children传入。顶级的layout是必须要有的,而且必须要有html以及body标签。page.xx:页面主体,类比index.xx的作用,作为页面的主体内容。template.xx:和layout.xx相同的作用,也会将同级page.xx作为children传入。但是路由切换时并不会保留页面状态,同时顶级不是必须的。loading.xx:提供loading状态,配合Suspense实现的,一般是page.xx中导出的是async函数,而不是普通的函数。error.xx:提供错误状态,配合ErrorBoundary实现的,但是测试时好像开发环境还是会抛出错误。顶级的话是global-error.xx,报错会替换掉layout,所以顶级的error也需要有html/bodynot-found.xx:404的页面,有一个默认的。
NOTE
如果文件夹内没有page.xx的话,该文件夹不作为路由,而是用来存放其他文件。
如果全都有的话,编译之后就是:
export default () => {
return (
<Layout>
<Template>
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<ErrorBoundary fallback={<NotFound />}>
<Page />
</ErrorBoundary>
</Suspense>
</ErrorBoundary>
</Template>
</Layout>
)
}这里路由的层级就是文件的层级:访问/就是src/app/page.js;访问/demo就是src/app/demo/page.js;访问/demo/about就是src/app/demo/about/page.js;
Layout的使用
起到一个容器作用,需要注意如果在根目录的话,该文件是必须要有的,并且必须要要包含html、body标签。 page.xx文件会作为children传入到该文件中。
Template的使用
也是起到一个容器作用,但是路由切换时状态会重置,以下例子:
layout.xx中有一些路由跳转的链接,跳转到子集路由,template.xx中有自己的状态,可以发现路由切换后,template的状态重置了,而layout.xx中的状态没有重置。每次路由变化时,template都会重新创建,所以一般用在某些和路由切换相关联的场景,比如切换路由时来执行一些上报记录之类的操作。
// layout.tsx
'use client'
import Link from 'next/link'
import { FC, PropsWithChildren, memo, useState } from 'react'
const Layout: FC<PropsWithChildren> = ({ children }) => {
const [count, setCount] = useState(0)
return (
<div className='w-[200px] border-[1px] border-[#ccc] rounded-[4px]'>
<div>
<Link href='/about' className="w-[80px] h-[30px] p-[4px] border-[1px] border-[#ccc]">about</Link>
<br />
<Link href='/about/detail' className="w-[80px] h-[30px] p-[4px] border-[1px] border-[#ccc]">detail</Link>
</div>
{children}
<div>layout count: {count}</div>
<button className='border-[1px] border-[red] p-[4px]' onClick={() => setCount(d => d += 1)}>layout count + 1</button>
</div>
)
}
export default memo(Layout)// template.tsx
'use client'
import { FC, PropsWithChildren, memo, useState } from 'react'
const Template: FC<PropsWithChildren> = ({ children }) => {
const [count, setCount] = useState(0)
return (
<>
{children}
<div>tempalte count: {count}</div>
<button className='border-[1px] border-[red] p-[4px]' onClick={() => setCount(d => d += 1)}>template count + 1</button>
</>
)
}
export default memo(Template)具体的代码看该仓库的
next/demo1分支
其他状态页面的使用
这个状态页面包括loading.xx、error.xx、not-found.xx,这几个都是向上查找的,本级没有就用父级的。
Loading的使用
是使用Suspence组件包裹的,所以page.xx需要是异步函数,如下代码:
import { FC, memo } from 'react'
const getData: () => Promise<string> = async() => {
return await new Promise((resolve) => setTimeout(() => {
resolve('ji')
}, 3000))
}
const User: FC = async () => {
const userName = await getData()
return (
<>
<h1>User page.</h1>
<div className=' font-bold'>userName: {userName}</div>
</>
)
}
export default memo(User)当访问该路由时,就会从本路由中找loading.xx作为Suspence的fallback,如果没有就会用父级的loading.xx。
Error的使用
是使用ErrorBoundary组件包裹的,这里的是在Layout以及Template之下的,所以本级的error.xx是无法捕获到layout.xx以及template.xx中的错误,需要在父级路由的error.xx中捕获。
NOTE
这里个ErrorBoundary组件并不是React内置的,应该是Next内置的,具体源码没有看。
所以还提供了一个global-error.xx文件,用来捕获根路由的layou.xx以及template.xx错误,这里作为根layout的fallback,所以也必须要有html以及body标签。
NotFound的使用
not-found.xx文件有一个默认的,触发时机如下:
- 执行内置的
notFound函数 - 没有找到对应的路由
所以开发时可以通过手动执行notFound函数来触发not-found.xx。如下代码:
import { notFound } from 'next/navigation'
import { FC, memo } from 'react'
const getData: () => Promise<string> = async() => {
return await new Promise((resolve) => setTimeout(() => {
resolve(Math.random() > 0.5 ? 'ji' : '')
}, 1000))
}
const User: FC = async () => {
const userName = await getData()
if(!userName) notFound()
return (
<>
<h1>User page.</h1>
<div className='font-bold'>userName: {userName}</div>
<div className='h-[30px] p-[4px] border-[1px] border-[#ccc] rounded-[6px]'>refresh userName</div>
</>
)
}
export default memo(User)路由切换
即SPA中的跳转到页面其他地方的链接,不会渲染整个页面,只加载需要的部分。Next中有4种方式来导航:
- 提供的
<Link>组件 - 客户端组件中的
useRouterhook - 服务端组件中的
redirect函数 - 浏览器原生的
History API
Link组件
基本用法如下:
import Link from 'next/link'
export default () => <Link href='/test'>跳转到test</Link>其中的href是支持动态的。
还可以通过usePathname获取当前的pathname:
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export default () => {
const pathname = usePathname()
return (
<>
<h1>当前页面的pathname:{pathname}</h1>
<Link href='/test'>跳转到test</Link>
</>
)
}另外路由跳转有一些滚动条位置问题,默认是滚动到顶部的,可以通过配置scroll来解决:
// 滚动条不会改变,即保持当前页面的滚动位置
<Link href="/dashboard" scroll={false}>
Dashboard
</Link>useRouter hook
客户端组件use client中使用
'use client'
import { useRoute } from 'next/navigation'
export default () => {
const route = useRoute()
return <div onClick={() => route.push('/test')}>
跳转
</div>
}redirect函数
服务端组件中使用
原生的History API
即history.pushState/replaceState
路由文件的写法
动态路由Dynamic Route
因为next中采用的是文件名称来进行路由配置的,动态路由需要使用[xxx]将文件名称包裹起来,包裹起来的部分会作为params参数传递给layout.xx/page.xx文件。
IMPORTANT
注意文件名不要是特殊的名称,比如react、route等
同级下多个动态路由,只会有一个生效,一般是先创建的那个。
[folderName]
这种写法只能访问下一级
比如文件目录:
app
- route
- page.js
- [id]
- page.js访问/route/123会进入到[id]/page.js中,但是访问/route/123/abc就会404
传入组件(对应的page.js文件内容)的params.id是字符串
[...folderName]
访问多级路由
以下文件目录:
app
- route
- page.js
- [...id]
- page.js访问/route/123会进入到[...id]/page.js中
访问/route/123/abc也会进入到[...id]/page.js中
此时组件中的params.id是字符串数组
[[...folderName]]
会匹配所有的子级路由,包括自己。
app
- route
- [[...id]]
- page.js和[...folderName]的区别在于,访问/route时也会进入[[...id]]/page.js中。
IMPORTANT
特别注意这种写法,不能有默认的page.js,否则会404
路由组Route Groups (folderName)
在Next中一般的文件名称都会被映射到URL中,可以通过路由组阻止这种行为。
一般用这个按照逻辑对文件分组。
使用时只需要用(folderName)即可。
比如以下文件:
- app
- (group1)
- layout.js
- detail
- page.js # /detail
- detail1
- page.js # /detail1
- (group2)
- layout.js
- about
- page.js # /about访问/detail和/about即可,进行了逻辑上的划分。
路由组只是阻止了将文件名映射到URL,其他的如layout.xx/error.xx/loading.xx还是可以用的,比如上面的/detail和/detail1用的都是layout.js。
需要注意的时,如果替代的是根布局,那么就需要包括html和body元素。
- 路由分组仅在逻辑上对文件内容划分,没有实际意义。
- 不要有相同的路径,比如
(group1)/about/page.js和(group2)/about/page.js,会报错。 - 跨
layout的导航会导致页面重新加载,上述文件中的/about跳转到/detail会让页面重新加载
平行路由Parallel Route @folderName
从名称中可以知道这个是用来干嘛的,即同一个layout中渲染多个页面的。
如下文件:
- app
- route
- page.js
- layout.js
- @some1
- page.js
- @some2
- page.js会自动将@some1/page.js和@some2/page.js作为参数传递到route/layout.js中:
export default ({ children, frontend, backend }) => {
return (
<>
route平行路由测试
{children}
{frontend}
{backend}
</>
)
}需要注意的是,要有一个page.js,否则会404。page.js相当于是
@children/page.js。
可以用于条件渲染,比如根据某个条件才显示出some1,否则显示some2。
这样写也能单独使用loading.xx等,让每一个路由都有自己的状态,这样就更像是子模块而非路由了。
平行路由让复杂页面拆分的更加简单了,将这些平行路由当做子模块来理解可能会更好。
default.js
平行路由虽然使用子路由使得复杂页面更加的方便拆分,但是也带来了一些问题。
- 热更新可能会卡死,需要重启服务
- 如果平行路由有子路由,直接访问子路由可能会导致404
- app
- route
- page.js
- layout.js
- @frontend
- show
- page.js
- page.js
- @backend
- page.js上面的文件,直接访问/route可以看到页面,如果在layout中有个Link,跳转到/route/show,直接点击跳转frontend区域会显示出show/page.js的内容。但是如果直接在导航栏输入/route/show进行跳转,就会404。
这是因为Next中导航的跳转(软导航)和直接刷新的跳转(硬导航)行为是不太一样的,Link导航时,执行部分渲染,如果其他的没有匹配上,就不会变化。但直接输入URL进行跳转,就无法确定其他部分和当前URL不匹配的该如何渲染,所以就渲染了404。
实际上,直接输入URL跳转,比如/route/show,会去查找app/route/show/page.js、app/route/@frontend/show/page.js、app/route/@backend/show/page.js。
为此,Next提供了default.js,当出现上述的无法匹配问题,就会渲染default.js,如果没有default.js,就会渲染404。
将上述文件目录改为:
- app
- route
- page.js
- default.js
- layout.js
- @frontend
- show
- page.js
- page.js
- @backend
- default.js
- page.js在app/route和app/route/@backend下新增了个default.js,这样直接通过URL访问/route/show时,就会渲染出对应的default.js内容。
拦截路由Intercepting Route (.)
即通过Link跳转时,可以进行拦截,展示A页面。如果是直接通过URL来访问,则展示B页面。
拦截路由可以让用户即预览了详情内容,又没有打断用户的浏览体验 留在了当前页面。
使用
使用拦截路由时,需要文件夹以(..)开头:
(.)同级(..)上一级(..)(..)上上一级(...)根目录
IMPORTANT
匹配的是路由的层级 而非文件夹层级,比如那些路由组/平行路由 就不会匹配。
如以下文件:
- app
- stop
- @modal
- (.)photo
- page.js
- photo
- page.js
- page.js
- layout.js在app/stop/page.js中通过Link跳转到/stop/photo,会先进入app/stop/@modal/(.)photo/page.js页面,刷新之后才展示app/stop/photo/page.js
路由写法总结
上述有好几种路由的方式,这里总结一下:
- 通过
[]包裹的文件夹名称,作为动态路由 - 通过
()包裹的文件夹名称,作为路由组,仅作逻辑上的划分 - 通过
@前缀的文件夹名称,作为平行路由,也不会被映射到URL上 - 通过
(.)等前缀的文件夹名称,作为拦截路由,是以路由层级而非文件层级。