欢迎来到实战篇!基础篇的目标是带大家复习基础知识,以及用作使用手册,方便大家在以后的项目开发中查询 API 用法,属于这本小册的“赠送面积”。从本篇起就进入小册的正式内容了。
我们的第一个实战项目是 React Notes,因为 Next.js v14 基于 React Server Component 构建的 App Router,而 React Server Component 的起源是 2020 年 12 月 21 日 React 官方发布的关于 React Server Components 的介绍文章。
这篇文章同时配上了由 Dan Abramov 和 Lauren Tan 两位 React 团队的工程师分享的长约 1h 的演讲和 Demo,详细的介绍了 React Server Components 的出现背景和使用方式(这是这个 Demo 的一个线上工程,你可以在这个地址上调试学习)。
当时这个 Demo 就是 React Notes,实战篇的第一个项目从这个“起源 Demo”开始讲起,既是一种追溯致敬,也是为了帮助大家在实战中体会 React Server Component 的特性和优势,毕竟当时 React 的工程师写了这个 Demo 用于新特性的展示,自然是要覆盖它的各种用法和特性。
这个 Demo 中的 Server 是自己写的,数据库用的是 PostgreSQL,如果要本地预览原本的 Demo 效果,参照 Demo 的介绍,本地安装 PostgreSQL,创建数据库,连接数据库,再运行项目即可成功开启。这里具体的实现步骤就不多讲了,反正我们的实战篇会用 Next.js 重新实现这个项目。
先让我介绍下 React Notes 的项目效果,正如它的名字表明的那样,这是一个笔记系统,可以增删改查笔记,笔记支持 markdown 格式。
首页效果如下,界面分为两列,左侧是笔记列表,右侧是笔记内容:
点击左边的 New
按钮,可以增加一个 Note,增加后,左侧笔记列表也会同时更新:
在编辑的时候,也可以删除一个 Note,删除后左侧笔记列表也会同时更新:
可以对现有的 Note 进行修改:
还可以在左侧用搜索框查找一个 Note:
看起来效果是不是平平无奇?但是注意一点,在这个例子中,我们先在左侧笔记列表中展开了一个笔记,然后又新建了一个笔记,在新建后,左侧笔记列表刷新,但展开的笔记依然保持了之前的状态。
现在我们要用 Next.js 实现这个项目,该怎么实现呢?
首先是技术选型,Next.js 的 App Router 自然是要用的,TypeScript 为了减少代码展示量就不使用了,ESLint 要使用,用于校验代码,Tailwind CSS 不需要,因为重写样式浪费时间,我们直接导入原 Demo 的样式文件即可。
后端数据库选择什么都可以,不过考虑到初期大家对 Next.js 尤其是 App Router 的使用不太习惯,再加上数据库的安装和使用也需要额外学习,我们先集中学习如何写好 Next.js 项目,数据方面先使用模拟数据来实现。
那么新的问题来了,怎么写模拟数据呢?第一种方式是在代码里直接写入数据。第二种方式是使用比如 faskMock 这样的工具生成静态接口。但是我们毕竟要做增删改查,无论是直接写数据还是静态接口都难以实现真的对数据源进行修改,所以最后我想了下,干脆用 Redis 做好了,作为经典的 NoSQL 数据库,使用起来也很方便。等 Next.js 部分完成学习之后,我们再替换为其他数据库。(其实我还试了用维格表做数据库,但维格表接口有每秒最多 2 次的限制,于是就放弃了)
其次是路由分析,原 Demo 中都是在 localhost:4000
下实现的,各种操作并不会产生路由变化,但既然我们用了 Next.js,不妨改成使用路由的方式,想了下,应该有这样几个路由:
/
,点击左上角的 React Note Logo 会导航至首页 /
/note/xxxx
路由,渲染具体笔记内容NEW
按钮的时候导航到 /note/edit
路由上,点击 Done
导航至刚创建的 /note/xxxx
路由/note/xxxx
后,点击 EDIT
按钮,进入 /note/edit/xxxx
路由,点击 Done
导航至刚修改的 /note/xxxx
路由,点击 DELETE
导航至首页 /
?q=searchText
参数对应到 Next.js 的项目目录,至少要有这些文件:
next-react-notes
├─ app
│ ├─ note
│ │ ├─ [id]
│ │ │ └─ page.js
│ │ └─ edit
│ │ ├─ [id]
│ │ │ └─ page.js
│ │ └─ page.js
│ ├─ layout.js
│ └─ page.js
考虑到左侧笔记列表出现在所有的路由中,我们将左侧的内容包括搜索栏和笔记列表,统一放在根布局 layout.js
中。
再者是组件划分,示意图如下:
左侧是 <Sidebar>
组件,子组件中有:
<SidebarSearchField>
组件负责搜索框<EditButton>
组件负责添加按钮<SidebarNoteList>
组件负责笔记列表
<SidebarNoteItem>
组件负责每一条具体的笔记内容右侧是 <Note>
组件,子组件有:
<EditButton>
组件负责编辑按钮<NoteEditor>
组件负责笔记的编辑界面<NotePreview>
组件负责笔记的预览界面对项目有了大致的了解和规划,剩下的就让我们在项目里具体完善吧,现在开始动手吧。
使用 create-next-app
脚手架创建项目,运行:
npx create-next-app@latest
相关选择如下:
运行 npm run dev
,打开 localhost: 3000
开启项目:
为了让代码文件职责清晰,我们将组件统一放在根目录下的 components
目录下,工具库放在根目录下的 lib
目录下,为了方便引入,我们配置一下路径别名,修改 jsconfig.json
:
{
"compilerOptions": {
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"]
}
}
}
修改 app/page.js
:
// app/page.js
export default async function Page() {
return (
<div className="note--empty-state">
<span className="note-text--empty-state">
Click a note on the left to view something! 🥺
</span>
</div>
)
}
修改 app/layout.js
:
import './style.css'
import Sidebar from '@/components/Sidebar'
export default async function RootLayout({
children
}) {
return (
<html lang="en">
<body>
<div className="container">
<div className="main">
<Sidebar />
<section className="col note-viewer">{children}</section>
</div>
</div>
</body>
</html>
)
}
在 /components
下新建一个名为 Sidebar.js
的文件,代码为:
import React from 'react'
import Link from 'next/link'
export default async function Sidebar() {
return (
<>
<section className="col sidebar">
<Link href={'/'} className="link--unstyled">
<section className="sidebar-header">
<img
className="logo"
src="/logo.svg"
width="22px"
height="20px"
alt=""
role="presentation"
/>
<strong>React Notes</strong>
</section>
</Link>
<section className="sidebar-menu" role="menubar">
{/* SideSearchField */}
</section>
<nav>
{/* SidebarNoteList */}
</nav>
</section>
</>
)
}
在根布局里我们引用了 style.css
,style.css
里声明了所有的样式,但这个文件不需要我们自己写,因为原 Demo 里就已经将所有的样式写到了一个 style.css 文件,我们只需要将这个文件拷贝到 app
目录下即可。
这个项目里还会用到一些图片,我们将原 Demo 里 public 目录下的 5 张 SVG 图片:checkmark.svg
、chevron-down.svg
、chevron-up.svg
、cross.svg
、logo.svg
拷贝到 public
目录下。
如果步骤正确的话,此时再访问 http://localhost:3000/
应该效果如下:
是不是有原 Demo 的样子了?
现在我们来处理数据的问题,正如之前所说,为了方便起见,我们使用 Redis 做数据库。简单介绍一下 Redis,它是一个高性能的 key-value 数据库,是现在最受欢迎的 NoSQL 数据库之一,常用于缓存、计数器、消息队列系统、排行榜等场景。
使用 Redis 很简单,一共分为三步:
macOS 安装 redis 很简单,按照官网安装说明,使用 Homebrew 安装即可:
brew install redis
Windows 安装略微复杂一点,因为我手边没有 Windows 电脑,就不提供安装方法了,教程很多。
运行以下命令,如果出现下图界面即表示运行成功:
redis-server
在项目里使用 redis 的时候,我们借助 ioredis 这个库,安装 ioredis:
npm install ioredis
在根目录下新建一个 lib
文件夹,在 lib
下新建一个名为 redis.js
的文件,代码如下:
import Redis from 'ioredis'
const redis = new Redis()
const initialData = {
"1702459181837": '{"title":"sunt aut","content":"quia et suscipit suscipit recusandae","updateTime":"2023-12-13T09:19:48.837Z"}',
"1702459182837": '{"title":"qui est","content":"est rerum tempore vitae sequi sint","updateTime":"2023-12-13T09:19:48.837Z"}',
"1702459188837": '{"title":"ea molestias","content":"et iusto sed quo iure","updateTime":"2023-12-13T09:19:48.837Z"}'
}
export async function getAllNotes() {
const data = await redis.hgetall("notes");
if (Object.keys(data).length == 0) {
await redis.hset("notes", initialData);
}
return await redis.hgetall("notes")
}
export async function addNote(data) {
const uuid = Date.now().toString();
await redis.hset("notes", [uuid], data);
return uuid
}
export async function updateNote(uuid, data) {
await redis.hset("notes", [uuid], data);
}
export async function getNote(uuid) {
return JSON.parse(await redis.hget("notes", uuid));
}
export async function delNote(uuid) {
return redis.hdel("notes", uuid)
}
export default redis
这块代码并不复杂,我们导出了 5 个函数,表示 5 个用于前后端交互的接口,分别是:
其中我们使用了 ioredis 的 hash 结构(ioredis 提供了相关写法示例和 API 说明)。也就是说,我们在 redis 服务器中存储的数据大概长这样:
{
"1702459181837": '{"title":"sunt aut","content":"quia et suscipit suscipit recusandae","updateTime":"2023-12-13T09:19:48.837Z"}',
"1702459182837": '{"title":"qui est","content":"est rerum tempore vitae sequi sint","updateTime":"2023-12-13T09:19:48.837Z"}',
"1702459188837": '{"title":"ea molestias","content":"et iusto sed quo iure","updateTime":"2023-12-13T09:19:48.837Z"}'
}
使用 macOS 的同学可以再下载一个 Medis,用于查看 Redis 中的数据(当然此时 Redis 还没有写入这些数据):
其中,key 值用的是创建笔记时的时间戳,value 值是具体的笔记数据,分为 3 个字段,分别是 title
、content
、updateTime
。
现在让我们用此数据接口来写左侧的笔记列表吧!
修改 components/Sidebar.js
:
import React from 'react'
import Link from 'next/link'
import { getAllNotes } from '@/lib/redis';
import SidebarNoteList from '@/components/SidebarNoteList';
export default async function Sidebar() {
const notes = await getAllNotes()
return (
<>
<section className="col sidebar">
<Link href={'/'} className="link--unstyled">
<section className="sidebar-header">
<img
className="logo"
src="/logo.svg"
width="22px"
height="20px"
alt=""
role="presentation"
/>
<strong>React Notes</strong>
</section>
</Link>
<section className="sidebar-menu" role="menubar">
{/* SideSearchField */}
</section>
<nav>
<SidebarNoteList notes={notes} />
</nav>
</section>
</>
)
}
在代码中,我们将笔记列表抽成了单独的 components/SidebarNoteList.js
组件,代码如下:
export default async function NoteList({ notes }) {
const arr = Object.entries(notes);
if (arr.length == 0) {
return <div className="notes-empty">
{'No notes created yet!'}
</div>
}
return <ul className="notes-list">
{arr.map(([noteId, note]) => {
const { title, updateTime } = JSON.parse(note);
return <li key={noteId}>
<header className="sidebar-note-header">
<strong>{title}</strong>
<small>{updateTime}</small>
</header>
</li>
})}
</ul>
}
如果步骤正确的话,此时再访问 http://localhost:3000/
应该效果如下:
我们已经成功的获取了 Redis 数据库中的数据,然后服务端渲染到了页面上。
现在在 Medis 中应该已经可以查看到写入的数据:
现在你在 Medis 中修改下数据,http://localhost:3000/
刷新后也会展示出来。
现在你会发现,左侧笔记列表中的时间展示非常“难看”,为此我们需要一个将时间格式化的库,这里我们选择大家经常会用到的 Day.js,安装一下:
npm install dayjs
修改 SidebarNoteList.js
:
import dayjs from 'dayjs';
export default async function NoteList({ notes }) {
const arr = Object.entries(notes);
if (arr.length == 0) {
return <div className="notes-empty">
{'No notes created yet!'}
</div>
}
return <ul className="notes-list">
{arr.map(([noteId, note]) => {
const { title, updateTime } = JSON.parse(note);
return <li key={noteId}>
<header className="sidebar-note-header">
<strong>{title}</strong>
<small>{dayjs(updateTime).format('YYYY-MM-DD hh:mm:ss')}</small>
</header>
</li>
})}
</ul>
}
时间效果展示如下:
是不是好看多了?但其实效果并不重要,重要的是我们引用了 day.js
这个库。我们引入 day.js
的 SidebarNoteList 组件使用的是服务端渲染,这意味着 day.js
的代码并不会被打包到客户端的 bundle 中。我们查看开发者工具中的源代码:
你会发现 node_modules 并没有 day.js,但如果你现在在 SidebarNoteList 组件的顶部添加 'use client'
,声明为客户端组件,你会发现立刻就多了 day.js:
这就是使用 React Server Compoent 的好处之一,服务端组件的代码不会打包到客户端的 bundle 中:
那么今天的内容就结束了,本篇我们大致知道了要做的项目内容,并新建了 Next.js 项目,学会了用 Redis 做个简易的数据库,最后通过引入时间处理库,了解了使用 React Server Component 的一个优势。
本篇的代码我已经上传到代码仓库的 Day1 分支:https://github.com/mqyqingfeng/next-react-notes-demo/tree/day1,直接使用的时候不要忘记在本地开启 Redis。