JackyLove 的技术人生

人生艰难,唯有一技傍身才能慢慢走向通途。个人博客,记录生活,分享技术,记录成长。专注全栈技术,next技术。

第34章—实战篇ReactNotes项目介绍与创建

首次发表于 2024-03-22, 更新于 2024-03-22

前言

欢迎来到实战篇!基础篇的目标是带大家复习基础知识,以及用作使用手册,方便大家在以后的项目开发中查询 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 格式。

首页效果如下,界面分为两列,左侧是笔记列表,右侧是笔记内容:

image.png

点击左边的 New 按钮,可以增加一个 Note,增加后,左侧笔记列表也会同时更新:

React Notes 增加.gif

在编辑的时候,也可以删除一个 Note,删除后左侧笔记列表也会同时更新:

React Notes 删除.gif

可以对现有的 Note 进行修改:

React Notes 修改.gif

还可以在左侧用搜索框查找一个 Note:

React Notes 查找.gif

看起来效果是不是平平无奇?但是注意一点,在这个例子中,我们先在左侧笔记列表中展开了一个笔记,然后又新建了一个笔记,在新建后,左侧笔记列表刷新,但展开的笔记依然保持了之前的状态。

技术文档

现在我们要用 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,不妨改成使用路由的方式,想了下,应该有这样几个路由:

  1. 首页肯定是 /,点击左上角的 React Note Logo 会导航至首页 /
  2. 点击左侧笔记列表中的一项,导航至 /note/xxxx路由,渲染具体笔记内容
  3. 当点击 NEW 按钮的时候导航到 /note/edit路由上,点击 Done导航至刚创建的 /note/xxxx路由
  4. 导航至 /note/xxxx后,点击 EDIT 按钮,进入 /note/edit/xxxx 路由,点击 Done导航至刚修改的 /note/xxxx路由,点击 DELETE 导航至首页 /
  5. 当在左侧搜索框输入字符的时候,对应路由添加 ?q=searchText 参数

对应到 Next.js 的项目目录,至少要有这些文件:

next-react-notes                 
├─ app                                     
│  ├─ note                       
│  │  ├─ [id]                         
│  │  │  └─ page.js              
│  │  └─ edit                    
│  │     ├─ [id]                 
│  │     │  └─ page.js              
│  │     └─ page.js                        
│  ├─ layout.js                  
│  └─ page.js                                

考虑到左侧笔记列表出现在所有的路由中,我们将左侧的内容包括搜索栏和笔记列表,统一放在根布局 layout.js 中。

再者是组件划分,示意图如下:

截屏2023-12-14 下午4.08.38.png

左侧是 <Sidebar> 组件,子组件中有:

  1. <SidebarSearchField> 组件负责搜索框
  2. <EditButton> 组件负责添加按钮
  3. <SidebarNoteList> 组件负责笔记列表
    1. 再拆分为具体的 <SidebarNoteItem> 组件负责每一条具体的笔记内容

右侧是 <Note> 组件,子组件有:

  1. <EditButton> 组件负责编辑按钮
  2. <NoteEditor> 组件负责笔记的编辑界面
  3. <NotePreview> 组件负责笔记的预览界面

对项目有了大致的了解和规划,剩下的就让我们在项目里具体完善吧,现在开始动手吧。

开始项目

1. 创建项目

使用 create-next-app脚手架创建项目,运行:

npx create-next-app@latest

相关选择如下:

image.png

运行 npm run dev,打开 localhost: 3000开启项目:

image.png

2. 配置路径别名

为了让代码文件职责清晰,我们将组件统一放在根目录下的 components目录下,工具库放在根目录下的 lib目录下,为了方便引入,我们配置一下路径别名,修改 jsconfig.json

{
  "compilerOptions": {
    "paths": {
      "@/components/*": ["components/*"],
      "@/lib/*": ["lib/*"]
    }
  }
}

3. 修改根布局和根页面

修改 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>
    </>
  )
}

4. 引入所需样式和图片文件

在根布局里我们引用了 style.cssstyle.css里声明了所有的样式,但这个文件不需要我们自己写,因为原 Demo 里就已经将所有的样式写到了一个 style.css 文件,我们只需要将这个文件拷贝到 app目录下即可。

这个项目里还会用到一些图片,我们将原 Demo 里 public 目录下的 5 张 SVG 图片:checkmark.svgchevron-down.svgchevron-up.svgcross.svglogo.svg 拷贝到 public目录下。

5. 第一步完成!

如果步骤正确的话,此时再访问 http://localhost:3000/应该效果如下:

image.png

是不是有原 Demo 的样子了?

数据请求

现在我们来处理数据的问题,正如之前所说,为了方便起见,我们使用 Redis 做数据库。简单介绍一下 Redis,它是一个高性能的 key-value 数据库,是现在最受欢迎的 NoSQL 数据库之一,常用于缓存、计数器、消息队列系统、排行榜等场景。

使用 Redis 很简单,一共分为三步:

1. 安装 Redis

macOS 安装 redis 很简单,按照官网安装说明,使用 Homebrew 安装即可:

brew install redis

Windows 安装略微复杂一点,因为我手边没有 Windows 电脑,就不提供安装方法了,教程很多。

2. 启动 Redis

运行以下命令,如果出现下图界面即表示运行成功:

redis-server

image.png

3. 项目引入 Redis

在项目里使用 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 个用于前后端交互的接口,分别是:

  1. 获取所有笔记的 getAllNotes,这里我们做了一个特殊处理,如果为空,就插入 3 条事先定义的笔记数据
  2. 添加笔记的 addNote
  3. 更新笔记的 updateNote
  4. 获取笔记的 updateNote
  5. 删除笔记的 delNote

其中我们使用了 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 还没有写入这些数据):

image.png

其中,key 值用的是创建笔记时的时间戳,value 值是具体的笔记数据,分为 3 个字段,分别是 titlecontentupdateTime

Sidebar 组件

现在让我们用此数据接口来写左侧的笔记列表吧!

1. 笔记列表

修改 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/应该效果如下:

image.png

我们已经成功的获取了 Redis 数据库中的数据,然后服务端渲染到了页面上。

现在在 Medis 中应该已经可以查看到写入的数据:

image.png

现在你在 Medis 中修改下数据,http://localhost:3000/刷新后也会展示出来。

2. 时间处理库

现在你会发现,左侧笔记列表中的时间展示非常“难看”,为此我们需要一个将时间格式化的库,这里我们选择大家经常会用到的 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>
}

时间效果展示如下:

image.png

是不是好看多了?但其实效果并不重要,重要的是我们引用了 day.js 这个库。我们引入 day.js 的 SidebarNoteList 组件使用的是服务端渲染,这意味着 day.js 的代码并不会被打包到客户端的 bundle 中。我们查看开发者工具中的源代码:

截屏2023-12-14 下午10.56.02.png

你会发现 node_modules 并没有 day.js,但如果你现在在 SidebarNoteList 组件的顶部添加 'use client',声明为客户端组件,你会发现立刻就多了 day.js:

截屏2023-12-14 下午10.59.07.png

3. 最佳实践:多用服务端组件

这就是使用 React Server Compoent 的好处之一,服务端组件的代码不会打包到客户端的 bundle 中:

image.png

总结

那么今天的内容就结束了,本篇我们大致知道了要做的项目内容,并新建了 Next.js 项目,学会了用 Redis 做个简易的数据库,最后通过引入时间处理库,了解了使用 React Server Component 的一个优势。

本篇的代码我已经上传到代码仓库的 Day1 分支:https://github.com/mqyqingfeng/next-react-notes-demo/tree/day1,直接使用的时候不要忘记在本地开启 Redis。

参考链接

  1. https://github.com/reactjs/server-components-demo
  2. https://www.youtube.com/watch?v=TQQPAU21ZUw&t=15s&ab_channel=MetaOpenSource
  3. https://redis.io/docs/install/install-redis/install-redis-on-mac-os/
© Copyright 2025 JackyLove 的技术人生. Powered with by CreativeDesignsGuru