Skip to main content

One post tagged with "seo"

View All Tags

· 10 min read

简介

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>
)
}
Scrolling to an id

<Link>的默认行为其实和a标签是一样的,如果href中有#id,会自动scroll

定义一个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>
</>
)
}