本章我们将使用 NextJS 开发一个小型的博客系统,你将学习到如何从远程服务器获取数据以及数据过滤、NextJS 常用组件的用法、NextJS 的自动路由等功能。
我们使用json placeholder提供的模拟数据来实现一个小型的、只读的博客系统(因为这个网站虽然提供了写操作的API,但它只是单纯的返回状态码,并不会把数据真实的写入服务器)。
开发之前,我们先熟悉一下 API 及数据结构
json placeholder API | 说明 |
---|---|
/posts | 100条文章的列表 |
/posts/:id | 指定ID的文章详情 |
/posts/:id/comments | 指定ID的文章的评论 |
/users | 10个用户的列表 |
/users/:id | 指定ID的用户详情 |
模块
模块 | 说明 |
---|---|
首页 | 分成两部分:上面部分显示用户列表;下半部分显示文章列表 |
文章详情 | 显示文章内容及该文章的作者和所有评论 |
用户详情 | 显示该用户的详情信息及发表的文章 |
实现
目录结构
使用客户端渲染实现
本节代码:axum-rs-next-lite-blog-client-side-rendering,你可以先点开这个链接,既能看到执行结果,又能方便的查看代码。
首页
import { useState, useEffect } from 'react';
import Head from 'next/head';
import axios from 'axios';
import Link from 'next/link';
import PostList from '../components/PostList';
export default function Home() {
const [userList, setUserList] = useState([]); // 第 8 行
const [postList, setPostList] = useState([]); // 第 9 行
useEffect(() => { // 第10行
Promise.all([ // 第11行
axios.get('https://jsonplaceholder.typicode.com/users'),
axios.get('https://jsonplaceholder.typicode.com/posts'),
]).then(([{ data: userListRemote }, { data: postListRemote }]) => { // 第14行
setUserList(userListRemote);
setPostList(postListRemote);
});
}, []); // 第18行
return (
<>
<Head> {/* 第21行*/}
<title>AXUM中文网</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head> {/* 第24行*/}
<main>
<section>
<h2>用户列表</h2>
<ul>
{userList.map((user) => ( {/* 第29行*/}
<li key={`user-item-${user.id}`}>
<Link href={`/users/${user.id}`}>{user.name}</Link>
<small>({user.username})</small>
</li>
))} {/* 第34行*/}
</ul>
</section>
<section>
<h2>最新文章</h2>
<PostList userList={userList} postList={postList} limit={5} /> {/* 第39行*/}
</section>
</main>
</>
);
}
-
第10~18行:使用
useEffect
hook模拟compontentDidMount
生命周期,从远程服务器获取数据- 第11行:由于我们要同时获取两个接口的数据,所以这里使用
Promise.all
来并发地发起两个 HTTP 请求 - 第14行:这里我们结合了数组解构和对象解构的写法
- 第11行:由于我们要同时获取两个接口的数据,所以这里使用
-
第21~24行:设置所需要的
Head
-
第39行:将
userList
和postList
传递给PostList
组件思考:代码中还有一个额外的
limit
,可以用来控制显示的条数,你有思路实现吗。👉 可以使用Array.slice()
文章列表、文章项组件
// components/PostList.jsx
import PostItem from './PostItem';
export default function PostList({ userList, postList }) {
return (
<ul>
{postList.map((post) => {
const author = userList.find((u) => u.id === post.userId); {/* 第8行*/}
return <PostItem key={post.id} post={post} author={author} />;
})}
</ul>
);
}
// components/PostItem.jsx
import Link from 'next/link';
export default function PostItem({ post, author }) {
return (
<li>
<Link href={`/posts/${post.id}`}>{post.title}</Link> - by{' '}
<Link href={`/users/${author.id}`}>{author.name}</Link>
</li>
);
}
除了第8行,其它没什么值得说的:
- 第8行,使用
Array.find()
来查找符合条件的元素。此例中,是要在所有用户列表中,查找当前文章的作者。
文章详情页
import axios from 'axios';
import Link from 'next/link';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router'; // 第4行
export default function PostDetail() {
const { id } = useRouter().query; // 第7行
const [post, setPost] = useState({});
const [comments, setComments] = useState([]);
const [author, setAuthor] = useState({});
useEffect(() => {
Promise.all([ //第12行
axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`),
axios.get(`https://jsonplaceholder.typicode.com/posts/${id}/comments`),
]).then(([{ data: post }, { data: comments }]) => {
setPost(post);
setComments(comments);
axios // 第18行
.get(`https://jsonplaceholder.typicode.com/users/${post.userId}`)
.then(({ data: author }) => {
setAuthor(author);
});//第22行
}); // 第23行
}, []);
return (
<div>
<h1>{post.title}</h1>
<div>
作者:<Link href={`/users/${author.id}`}>{author.name}</Link> {/*第29行*/}
</div>
<p>{post.body}</p>
<hr />
<h3>评论列表</h3>
{comments.map((c) => (
<div key={`comments-${post.id}-${c.id}`}>
<div>
<a href={`mailto:${c.email}`}>{c.name}</a> 说:{/*第37行*/}
</div>
<div>{c.body}</div>
</div>
))}
</div>
);
}
- 第4行:导入
useRouter
hook - 第7行:通过
useRoter
hook,获取动态路由中的参数 - 第12~23行:从远程服务端获取数据
- 第12行:并行发起2个 HTTP 请求,当这两个 HTTP请求成功返回之后,设置状态并在18行又一次发起 HTTP 请求
- 第18行:由于这个请求依赖于12行发起的 HTTP请求的返回结果,所以这里要在上一步HTTP请求拿到结果后再发一次 HTTP 请求
- 第29行:使用
Link
组件跳转到指定路由 - 第37行:直接使用原始的 HTML 的
<a>
,这里因为这个链接并不是路由跳转,所以不能使用Link
用户详情页
import axios from 'axios';
import PostItem from '../../components/PostItem';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
export default function UserDetail() {
const { id } = useRouter().query;
const [user, setUser] = useState({});
const [postList, setPostList] = useState([]);
useEffect(() => {
Promise.all([
axios.get(`https://jsonplaceholder.typicode.com/users/${id}`),
axios.get('https://jsonplaceholder.typicode.com/posts'),
]).then(([{ data: user }, { data: postList }]) => {
setUser(user);
setPostList(postList);
});
}, []);
return (
<div>
<h1>用户详情</h1>
<div>用户名:{user.username}</div>
<div>姓名:{user.name}</div>
<div>邮箱:{user.email}</div>
<div>
{/* 第26行 */}地址:{user.address?.street} {user.address?.suite}, {user.address?.city}{' '}
({user.address?.zipcode})
</div>
<div>电话:{user.phone}</div>
<div>
网站:
<a href="http://{user.website}" target="_blank"> {/* 第32行 */}
{user.website}
</a>
</div>
<hr />
<div>发表的文章:</div>
<ul>
{postList
.filter((post) => post.userId === user.id) {/* 第40行 */}
.map((post) => (
<PostItem post={post} author={user} /> {/* 第42行 */}
))}
</ul>
</div>
);
}
- 第26行:为了确保在未拿到 HTTP 请求的结果之前,页面能正常渲染,这里使用
?
对user.address
进行容错处理 - 第32行:
- 因为是链接到外部地址,所以直接使用
<a>
标签 - 这里存在一个笔误,请读者自行修改:
<a href={...}
——这里应该用字符串模板拼接
- 因为是链接到外部地址,所以直接使用
- 第40行:使用
Array.filter()
对文章列表进行过滤:此处只需要该用户发表的文章 - 第42行:复用
PostItem
——这里漏了key
,请读者自行修改:<PostItem key={...} ... />
页顶
import Link from 'next/link';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return (
<> {/*第6行*/}
<div> {/*第7行*/}
<Link href="/">首页</Link> {/*第8行*/}
</div>{/*第9行*/}
<Component {...pageProps} />
</>{/*第11行*/}
);
}
export default MyApp;
- 为了让每个页面都能快速回到首页,我们在
pages/_app.js
中添加了一个类似于导航栏的组件- 在布局时,这是一种很常用的方式
- 为了满足 React 只能有一个根元素的规范,第6行添加了
<>
——还记得它是什么吗?不记得的话,找找以前的章节吧。
使用服务端渲染实现
本节代码:axum-rs-next-lite-blog,你可以先点开这个链接,既能看到执行结果,又能方便的查看代码。
服务端渲染和客户端渲染几乎一样,除了获取数据的方式和获取动态路由的参数不同。我们以文章详情为例进行分析。其它内容请点击上面的链接直接查看源码。
import axios from 'axios';
import Link from 'next/link';
export default function PostDetail({ post, comments, author }) { // 第4行
return (
<div>
<h1>{post.title}</h1>
<div>
作者:<Link href={`/users/${author.id}`}>{author.name}</Link>
</div>
<p>{post.body}</p>
<hr />
<h3>评论列表</h3>
{comments.map((c) => (
<div key={`comments-${post.id}-${c.id}`}>
<div>
<a href={`mailto:${c.email}`}>{c.name}</a> 说:
</div>
<div>{c.body}</div>
</div>
))}
</div>
);
}
export async function getServerSideProps({ params }) { // 第26行
const { id } = params; // 第27行
const [{ data: post }, { data: comments }] = await Promise.all([
axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`),
axios.get(`https://jsonplaceholder.typicode.com/posts/${id}/comments`),
]);
const { data: author } = await axios.get(
`https://jsonplaceholder.typicode.com/users/${post.userId}`
);
return { props: { post, comments, author } };
}
- 第4行:服务端渲染不需要使用
useEffect
来获取数据了,而是由第26行定义的getServerSideProps()
特殊函数返回- 它只要在形参里声明、接收即可
- 它连ID都不用获取:
- 一是不需要
- 二是 如果确实需要,还是可以通过
getServerSideProps()
来传递
- 第26行:
getServerSideProps({params})
函数- 它可以接收参数,其中就有
params
,nextjs 会自动把动态路由里的参数通过它传递过来
- 它可以接收参数,其中就有
- 第27行:从
params
里获取动态路由里的id
参数 - 在页面组件里(第4行开始),还是可以像客户端渲染那样,通过
useRouter
hook 获取动态路由里的参数 - 但是在
getServerSideProps()
里,无法使用useRouter
hook,必须通过params
接收
试一试
-
参照页顶“首页”链接的方法,将
pages/index.js
下的<Head>
组件放到pages/_app.js
里,以便让给所有页面统一设置Head
。 -
参照“文章列表”的方式,试着将“用户列表”也封装成组件
-
让组件目录更规范化,比如:
- components -- 组件目录 - Post - Items.jsx -- 列表中,单个文章组件 - List.jsx -- 文章列表组件
本章小结
如何获取动态路由中的参数
方式一:所有渲染模式都能用,且客户端渲染模式只能用这种方式
使用 useRouter
hook获取。
方式二:服务端渲染和静态网站生成
在 getServerSideProps/getStaticProps
函数里,通过 params
接收。
如何同时(并发的)发起多个 HTTP 请求
使用 Promise.all
可以并发地执行多个异步任务。
对于外部链接,NextJS可能会有警告
可以按照它的警告信息,给<a>
加上noreferrer
等内容。
Tailwind 在召唤你
通过本章的示例,你应该能感受到 NextJS (包括 React)开发的爽快感觉了,回头看一下本章案例——好丑呀。不要慌,马上进入 Tailwind 的学习,有了它之后,不愁它不能美化你的网页,只恨你没有那么多创意。
我和 Tailwind 在下一章等你。