简介
next.js一款非常优秀的react服务端渲染(SEO)、全栈式框架, github star 高达 108K
主要特性:
- 文件系统路由:支持布局、嵌套路由、加载状态、错误处理等
- 渲染:客户端渲染,服务端渲染,混合渲染
- 数据获取:
fetch()
API 配合async/await
- css:支持module css,CSS-in-JS,Tailwind CSS
- 优化:服务端可以使用Image, Fonts, Script来提升用户体验
- tyscript支持
- 可供参考的API
需要注意的是next.js有2套文档:App Router docs 和 Pages Router docs,中文网版本的只提到了后者
在英文文档处,可以通过点击下面的下拉按钮快速切换文档
next.js是有学习门槛的,你需要有react基础,如果你还不知道什么是react,可以看下官方推荐的教程:
安装
系统要求:
- Node.js 16.8 or later.
- 支持 Macos, windows, linux
# 快速构建项目
$ npx create-next-app@latest
# or 在当前目录创建
$ npx create-next-app@latest .
# 启动
$ npm run dev
可以看到next.js的启动是非常快的,接着浏览器访问 http://localhost:3000 就可以看到效果了
next.js使用Turbopack打包,网上有人晒出其比webpack快700倍,比vite快10倍
文件结构:
什么时候用服务端渲染,什么时候用客户端渲染
默认情况下app目录下的所有文件都是服务端渲染,除非你用"use client"
在文件的第一行声明
官方给出了一个表格:
可以看到极少情况下,只有当与客户端交互时才会用到客户端渲染,eg:js事件,useXXX
相关的 hook api,使用浏览器特有的api,使用react class component
APP路由
路由必须定义在app目录下方可生效
路由定义
定义路由的语法很简单 就是一个普通的js组件,值得注意的是不需要引入react, eg:
export default function Page() {
return <h1>Hello, Next.js</h1>
}
//or
export default () => (
return <h1>Hello, Next.js</h1>
)
路由约定
上面的 路由约定 是什么意思呢?
以Routing files为例,这些files都必须是react组件,后缀名可以是js jsx或者tsx
比如你想定义一个从浏览器 http://127.0.0.1:3000/movie
就可以访问到的路由,那么你的组件结构必须是: app/movie/page.js[x]
or app/movie/page.ts[x]
folder就是路由的segment
又再比如你想定义一个从浏览器 http://127.0.0.1:3000/fav/movie
就可以访问到的路由,那么你的组件结构必须是: app/fav/movie/page.js[x]
or app/fav/movie/page.ts[x]
路由就像文件系统一样,清晰明了,next.js
通过约定自动注册路由,免去了我们注册路由的麻烦
看上去是不是很简单,值得注意的是只有名为page.js的文件才是可见的
打个比方,我在app/moive
目录下定义了3个js
如果我在浏览器访问 http://127.0.0.1:3000/fav/movie
是访问不到其中任意一个js组件的,只会显示404,除非我们再在movie.js里定义一个page.js组件
Routing files 写的很清楚,如果你想定义loading组件,只需定义一个loading.js
; 如果想重写404,只需定义一个not-found.js
即可, 后面的file从字面就可以看的出来用途,就不再一一列举了
动态路由
什么是动态路由呢?打个比方,通过id访问博客,前面的http://127.0.0.1:3000
我就省略了
假如有类似的url
- /blog/111
- /blog/222
- /blog/333
我们不可能也绝不能傻傻的定义以下这种组件(id是数据库的bid)
- /blog/111/page.js
- /blog/222/page.js
- /blog/333/page.js
那么如何定义动态路由呢?
看说明:
[folder] Dynamic route segment
我们只要给folder加个[]
即可将folder变为一个slot,这个slot承载的就是一个变量
可以看到成功访问页面
动态路由如何获取参数呢
next.js组件会自动接收一个封装好slot的params对象
我们修改下代码:
再次访问页面
可以看到已成功获取动态id
路由导航
有2种方式可以实现路由导航
<Link>
组件useRouter
hook
//`<Link>`组件
import Link from 'next/link'
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>
}
'use client'
//`useRouter` hook
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}
<Link>
的默认行为其实和a标签是一样的,如果href中有#id
,会自动scroll
Checking Active Links
定义一个Navigation
组件,用于渲染<Link/>
(Navigation的位置可以随意放,官网给的demo是app/ui/Navigation.js
)
import { usePathname } from 'next/navigation'
import Link from 'next/link'
export function Navigation({ navLinks }) {
const pathname = usePathname()
return (
<>
{navLinks.map((link) => {
const isActive = pathname.startsWith(link.href)
// console.log("url:",link.href)
// console.log("is active:", isActive)
// 这里会随着服务重启只会执行一次
return (
<Link
className={isActive ? 'text-blue-500' : 'text-black'}
href={link.href}
key={link.name}
>
{link.name}
</Link>
)
})}
</>
)
}
使用<Navigation/>
"use client"
//这里必须使用"use client" directive
//因为Navigation使用了life cylcle effects: usePathname()
import { Navigation } from '../../ui/Navigation'
const navLinks = [
{
name: "movie",
href: "/movie"
},
{
name: "news",
href: "/news"
}
]
export default function Header() {
return (
<div className='flex gap-2'>
<Navigation navLinks={navLinks} />
</div>
)
}
来看看效果:
Route Groups
app里的folder通常会被影射成url path,如果你不想folder被url映射,可以使用(folder)
来标记,这样next.js就会忽略它 - 这就是Route Group
有了Route Group,你可以随意添加项目名 公司名 组织名 而不会影响路由
来看下面的例子(marking shop都会被忽略):
loading ui & streaming
example(注意Suspense的使用方法,不能包含多个页面/组件):
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}
Data Fetching
Next.js官方建议如果发送http请求,不管在什么情况下都请在server组件中,使用fetch() API 配合async/await
在server端发送http请求最大的好处就是你不用担心跨域的问题,而且还可以很好的屏蔽后端接口,让db更加安全
example:
async function getData() {
const res = await fetch('https://api.example.com/...')
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
// Recommendation: handle errors
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
默认情况下, fetch 会强制进行缓存,只要是重复请求,再次请求不会真实的去服务端查询,而是直接使用缓存,这样可以大大的加快页面的渲染速度
fetch('https://...') // cache: 'force-cache' is the default
如果希望每次请求的数据都是最新的,可以加上 cache: 'no-store'
option.
fetch('https://...', { cache: 'no-store' })
Revalidating Data
要定期重新验证缓存数据,可以使用 fetch() 中的 next.revalidate 选项来设置资源的缓存生命周期(以秒为单位)。
fetch('https://...', { next: { revalidate: 10 } }) //超过10s会跳过缓存去真实服务器获取数据
Parallel Data Fetching
有时候需要同时请求多个接口:
import Albums from './albums'
async function getArtist(username) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getArtistAlbums(username) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({ params: { username } }) {
// Initiate both requests in parallel
const artistData = getArtist(username)
const albumsData = getArtistAlbums(username)
// Wait for the promises to resolve
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
)
}