官方文档 Getting Started | Next.js (nextjs.org)
安装
npx create-next-app@latest --ts
# or
yarn create next-app --typescript
# or
pnpm create next-app --ts
运行
npm run dev
项目结构
文件 | 内容 |
---|---|
pages | 页面文件 |
pages/api | api 数据接口 |
public | 静态资源文件 |
styles | 样式文件 |
next-env.d.ts | 确保 typescript 支持 |
next.config.ts | next 配置文件 |
路由
nextjs 有一个基于页面概念的文件系统路由器,存放在 pages 下.js
, .jsx
, .ts
, .tsx
文件都将作为组件,即文件路径 → 页面路由,例如这里的 index.tsx 映射为 index,pages/about.js
将映射为 /about
。
同时还支持动态路由,创建pages/user/[id].tsx
文件,然后访问user/1
,user/2
import { useRouter } from 'next/router'
const User = () => {
const router = useRouter()
const { id } = router.query
return <div>User id:{id} </div>
}
export default User
此时访问 http://localhost:3000/user/1 便可得到 User ID: 1
在 router 对象下没有 param 属性,都是存放在 query 参数中,例如访问 user/1?username=kuizuo,此时的 query 值为 {username: 'kuizuo', id: '2'}
不过这里有个比较有意思的点,如果你在上方代码中使用 console.log 打印 query 的话,在 vscode 中会打印出空对象{}
,而在浏览器中会打印一次空对象,一次真实的 query 对象(并且打印两遍)
数据渲染
如果你打开控制台,查看所返回的页面,你会发现响应中只有 User id:,这不就和 react 的 CSR(客户端)渲染没有区别吗,是的,确实是这样。因为上一部分的代码,并且从输出 query 也可以看的出来而不是 SSR(服务端)渲染。首先我要展示一下两者渲染的代码
CSR 客户端渲染
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
const User = () => {
const router = useRouter()
const { id } = router.query
const [data, setData] = useState({
username: '',
email: '',
})
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
.then(res => res.json())
.then(data => {
setData(data)
})
.catch(err => {})
}, [id])
return (
<div>
<p>username:{data.username} </p>
<p>email:{data.email} </p>
</div>
)
}
export default User
经常写 react 的肯定对上面的代码不陌生,前端向后端发送数据请求,接受到数据后赋值给 data,然后渲染出来。因为请求数据是需要耗时的,所以在页面显示完之后,会停顿一会在显示出数据(主要是我这边没写 loadding),并且由于 id 并不是第一时间获取到的(从上面的 id)。
从这里来看,客户端渲染不仅要获取页面组件,还要请求数据,最终再通过 js 渲染出来
SSR 服务端渲染
next 中服务端渲染需要用到 getServerSideProps 函数,而后端的数据获取都是在该函数内来获取,并通过 prop 传入给前端组件中,来看实际代码
const User = ({ data }: { data: any }) => {
return (
<div>
<p>username:{data.username} </p>
<p>email:{data.email} </p>
</div>
)
}
export default User
export async function getServerSideProps(context: { query: { id: any } }) {
const { id } = context.query // 这里context.param也能获取到id
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
const data = await res.json()
return {
props: {
data,
},
}
}
如果从页面显示来看,确实没什么区别,但如果打开控制台就能发现诸多不同。
首先就是请求的页面,是直接包含数据,相当于返回你一个页面,而在客户端渲染则是返回一个组件,需要自己去请求数据来展示。
同时查看控制台中的 Fetch/XHR 的是看不到请求的数据,因为这些数据并不是由前端发 送的,而是由后端发送的(故不受跨域请求的限制)。
从这就能看出客户端渲染与服务端渲染的的区别了。
SSG 静态生成
不过还没完,还有一个静态生成,先来看看代码。
const User = ({ data }: { data: any }) => {
return (
<div>
<p>username:{data.username} </p>
<p>email:{data.email} </p>
</div>
)
}
export default User
export async function getStaticProps(context: { params: { id: any } }) {
const { id } = context.params
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
const data = await res.json()
return {
props: {
data,
},
}
}
export async function getStaticPaths() {
return {
paths: new Array(20).fill(0).map((a, i) => ({ params: { id: String(i + 1) } })),
fallback: 'blocking',
}
}
主要是 getServerSideProps 替换成 getStaticProps,同时增加了一个 getStaticPaths 用于生成静态页面的,而上面的 getStaticPaths 表示生成 id 1 到 20 的页面,那假设如果我访问 id 为 21 的 user 呢?由于这里设置fallback: 'blocking'
,所以还是会走服务端渲染的那一部分。但如果设置fallback: fasle
,访问 user/21 就会提示 404。
通俗点来说就就是生成一系列静态页面,不需要服务端处理,所以返回的速度更快,其缺点其实也比较明显,数据的任何更改都需要在服务端重新构建,而服务端渲染则是可以动态处理数据,不需要完全重建。
ISR 增量式静态生成
不做过多介绍,详看文档 Data Fetching: Incremental Static Regeneration | Next.js (nextjs.org)
api 接口
上面的数据都是调用 JSONPlaceholder 所提供的虚拟数据,在 next 中要提供数据接口的话,只需要在 pages/api 下编写即可,生成的路由规则和组件一样。例如 pages/api/hello.ts 映射为 api/hello ,浏览器访问http://localhost:3000/api/hello 就可以得到{"name": "John Doe"}
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
res.status(200).json({ name: 'John Doe' })
}
这里的 req、res 就是同大部分 node 后端框架一样,而这里的写法与 serverless 一致(这里应该就是 serverless)。
上述是 get 请求,那 post 请求呢?无论什么 http 请求方法都将在 handler 处理,通过 req.method 来获取请求方法,要区分的话可以通过如下代码。
export default function handler(req, res) {
if (req.method === 'POST') {
// Process a POST request
} else {
// Handle any other HTTP method
}
}
写一个简单的 CRUD
既然知道了上述的一些作用,不妨来个熟悉的 CRUD。这里以文章 post 为例
这里数据端使用的时 sqlite,配置不做展示,只展示主要核心功能
import type { NextApiRequest, NextApiResponse } from 'next'
import db from '../../../lib/db'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
try {
switch (req.method) {
case 'GET':
db.all(`select * from post`, (err, rows) => {
res.status(200).json(rows)
})
break
case 'POST':
const { title, content } = req.body
db.get(`insert into post(title, content) values(?, ?)`, [title, content], (err, rows) => {
res.status(200).json(rows)
})
break
}
} catch (error) {
res.status(500).end()
}
}
import type { NextApiRequest, NextApiResponse } from 'next'
import db from '../../../lib/db'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query
const { title, content } = req.body
try {
switch (req.method) {
case 'GET':
db.get(`select * from post where id=$id`, { $id: id }, (err, rows) => {
res.status(200).json(rows)
})
break
case 'Put':
db.get(
`update post set title=?,content=? where id=?`,
[title, content, id],
(err, rows) => {
res.status(200).json(rows)
},
)
case 'DELETE':
db.get(`delete from post where id=$id`, { $id: id }, (err, rows) => {
res.status(200).json(rows)
})
}
} catch (error) {
res.status(500).end()
}
}
这里为了符合 RESTFUL 风格,所以 post 下编写了两个文件,这时候请求http://localhost:3000/api/post 就能获取到所有文章数据,基本的 CRUD 也就实现了。
这里写 sql 是真滴繁琐,没使用 str 或是 typeorm 主要是不想把这个 demo 搞得太复杂,实际项目还是用上比较好。
当然这里只是作为后端 api 接口的演示,至于前端的展示与编写就和普通前端开发没啥大的区别。基本后端框架能做的,next 能做后端很多事情,更多的使用还是作为接口转发,中间件等,毕竟 Next 主要的强项还是服务端渲染的能力。