用 React 编写一个笔记应用程序

这篇文章会教大家用 React 编写一个笔记应用程序。用户可以创建、编辑、和切换 Markdown 笔记。

1. nanoid

nanoid 是一个轻量级和安全的唯一字符串ID生成器,常用于JavaScript环境中生成随机、唯一的字符串ID,如数据库主键、会话ID、文件名等场景。

2. React-split

react-split 是一个 React 组件库,它提供了一个简单易用的界面来分割面板,允许用户通过拖动来调整面板的大小。这个库非常适合需要在界面上展示可调整大小的多个区域的场景,比如编辑器、IDE、仪表板等。

javascript 复制代码
import Split from "react-split";

<Split>
  <div>Panel 1</div>
  <div>Panel 2</div>
</Split>

3. react-mde

react-mde 是一个用于 React 的 Markdown 编辑器组件,它基于 Draft.js 构建,提供了一个功能强大且可扩展的界面,允许用户以 Markdown 语法或可视化方式编辑文本。

bash 复制代码
npm install react-mde --legacy-peer-deps

4. showdown

Showdown 是一个 JavaScript 库,用于将 Markdown 文本转换为 HTML。它是一个广泛使用的库,因为它兼容性好、速度快,且易于集成和使用。

5. App.jsx

这是一个用 React 编写的笔记应用程序的主组件 App。用户可以创建、编辑、和切换 Markdown 笔记。它使用了以下功能与库:

  • React:实现状态管理和组件化。
  • nanoid:生成唯一的 ID,用于每个笔记。
  • react-split:实现分屏布局(侧边栏和编辑器)。
  • 自定义组件:
    -- Sidebar:显示笔记列表和用于切换当前笔记。
    -- Editor:用于编辑当前笔记的内容。

主要功能

  1. 创建新笔记
  2. 更新笔记内容
  3. 切换当前笔记
  4. 显示分屏布局
  5. 无笔记时显示提示
javascript 复制代码
import React from "react";
import { nanoid } from "nanoid";
import Split from "react-split";
import Sidebar from "../public/components/Sidebar";
import Editor from "../public/components/Editor";
  • React:提供 useState 和其他功能,用于创建 React 组件。
  • nanoid:生成唯一的 ID,用于标识每条笔记。
  • Split:用于实现分屏布局(侧边栏 + 编辑器)。
  • Sidebar 和 Editor 是自定义的子组件,分别处理笔记列表和笔记编辑。

状态管理

javascript 复制代码
const [notes, setNotes] = React.useState([]);
const [currentNoteId, setCurrentNoteId] = React.useState(
  (notes[0] && notes[0].id) || ""
);
  • notes:存储所有笔记的状态,初始值为空数组。
  • currentNoteId:存储当前笔记的 ID,初始值为第一个笔记的 ID(若无笔记,则为空字符串)。

创建新笔记

javascript 复制代码
function createNewNote() {
  const newNote = {
    id: nanoid(),
    body: "# Type your markdown note's title here"
  };
  setNotes(prevNotes => [newNote, ...prevNotes]);
  setCurrentNoteId(newNote.id);
}
  • 使用 nanoid 生成一个唯一的 ID。
  • 创建一个新笔记对象,默认内容为 "# Type your markdown note's title here"。
  • 将新笔记添加到 notes 的顶部。
  • 更新 currentNoteId 为新笔记的 ID

更新笔记

javascript 复制代码
function updateNote(text) {
  setNotes(oldNotes => oldNotes.map(oldNote => {
    return oldNote.id === currentNoteId
      ? { ...oldNote, body: text }
      : oldNote;
  }));
}
  • 遍历 notes,检查每条笔记的 ID。
  • 如果某条笔记的 ID 与 currentNoteId 相同,则更新其 body 内容。
  • 使用 ...oldNote 保留笔记其他的属性,更新 body

查找当前笔记

javascript 复制代码
function findCurrentNote() {
  return notes.find(note => {
    return note.id === currentNoteId;
  }) || notes[0];
}
  • 查找与 currentNoteId 匹配的笔记。
  • 如果没有匹配到,返回第一条笔记(默认情况)。
javascript 复制代码
return (
  <main>
    {notes.length > 0 ? (
      <Split
        sizes={[30, 70]}
        direction="horizontal"
        className="split"
      >
        <Sidebar
          notes={notes}
          currentNote={findCurrentNote()}
          setCurrentNoteId={setCurrentNoteId}
          newNote={createNewNote}
        />
        {currentNoteId && notes.length > 0 && (
          <Editor
            currentNote={findCurrentNote()}
            updateNote={updateNote}
          />
        )}
      </Split>
    ) : (
      <div className="no-notes">
        <h1>You have no notes</h1>
        <button
          className="first-note"
          onClick={createNewNote}
        >
          Create one now
        </button>
      </div>
    )}
  </main>
);

有笔记时的渲染:

  • 使用 Split 实现水平分屏,30% 分给侧边栏(Sidebar),70% 分给编辑器(Editor)。
  • Sidebar:显示笔记列表,并支持切换当前笔记或创建新笔记。
  • Editor:编辑当前笔记的内容。

无笔记时的渲染:

  • 显示提示文字 You have no notes
  • 提供一个按钮 Create one now,单击后调用 createNewNote 创建第一条笔记。

6. Sidebar.jsx

这是一个 React 组件 Sidebar,用于显示笔记的侧边栏列表,并允许用户切换当前笔记或创建新笔记。此组件通过 props 接收父组件传递的状态和函数来实现动态渲染和交互。

javascript 复制代码
import React from "react";

export default function Sidebar(props) {
  • React:用于支持 React 的 JSX 语法。
  • export default:将 Sidebar 组件作为默认导出,使其可被其他模块导入。

Sidebar 是一个无状态函数组件,接收 props 参数(即父组件传递的数据)。

生成笔记元素

javascript 复制代码
const noteElements = props.notes.map((note, index) => (
    <div key={note.id}>
        <div
            className={`title ${
                note.id === props.currentNote.id ? "selected-note" : ""  
            }`}
            onClick={() => props.setCurrentNoteId(note.id)}
        >
            <h4 className="text-snippet">Note {index + 1}</h4>
        </div>
    </div>
));
  1. props.notes
  • 父组件传入的笔记数组,每个笔记对象包含 idbody 属性。
  1. props.notes.map()
  • 遍历笔记数组,为每条笔记生成一个 JSX 元素。
  • key={note.id}:为每个顶层元素提供唯一的 key 属性,以优化 React 的渲染性能。
  1. 条件渲染类名:
  • className={title ${note.id === props.currentNote.id ? "selected-note" : ""}}
  • 如果当前笔记的 id 与 props.currentNote.id 匹配,添加 selected-note 类名。
  • 用于高亮显示当前选中的笔记。
  1. 点击事件:
  • onClick={() => props.setCurrentNoteId(note.id)}
  • 点击笔记时调用父组件传入的 setCurrentNoteId 方法,更新当前笔记的 id。
  1. 显示笔记编号:
  • <h4 className="text-snippet">Note {index + 1}</h4>:动态显示笔记的编号(从 1 开始)。

侧边栏结构

javascript 复制代码
return (
    <section className="pane sidebar">
        <div className="sidebar--header">
            <h3>Notes</h3>
            <button 
                className="new-note"
                onClick={props.newNote}
            >+</button>
        </div>
        {noteElements}
    </section>
);
  1. 侧边栏头部:
  • <div className="sidebar--header">:包含标题和创建按钮。
  • 标题:<h3>Notes</h3>
  • 创建按钮:
  • <button className="new-note" onClick={props.newNote}>+</button>
    点击按钮调用父组件传入的 newNote 方法,创建一条新笔记。
  1. 笔记列表:
  • {noteElements}:动态渲染生成的笔记列表。

组件渲染逻辑总结

  1. 笔记渲染:
  • 遍历 props.notes,生成笔记列表。
  • 为选中的笔记添加 selected-note 类名。
  • 点击笔记切换当前选中的笔记。
  1. 笔记创建:
  • 提供一个按钮,点击后调用 props.newNote 创建新笔记。
  1. 动态更新:
  • 通过 props 与父组件共享状态,每次父组件状态更新时,Sidebar 会自动重新渲染。

7. Editor.jsx

此代码定义了一个名为 Editor 的 React 函数组件,用于实现 Markdown 文本编辑器。它通过 ReactMde(React Markdown Editor)库提供一个多功能的 Markdown 编辑和预览界面。

javascript 复制代码
import React from "react"
import ReactMde from "react-mde"
import 'react-mde/lib/styles/css/react-mde-all.css';
import Showdown from "showdown"
  1. React:
  • 导入 React 库,支持 JSX 和组件开发。
  1. ReactMde:
  • 导入 React Markdown Editor,用于渲染 Markdown 编辑器。
  • ReactMde 提供了内置的编辑和预览功能。
  1. 样式文件:
  • 导入 react-mde 的样式文件,应用默认的编辑器样式。
  1. Showdown:
  • 导入 Showdown 库,用于将 Markdown 文本转换为 HTML。
javascript 复制代码
export default function Editor({ currentNote, updateNote }) {
    const [selectedTab, setSelectedTab] = React.useState("write")
  1. 函数组件:
  • 定义一个函数组件 Editor,接收两个 props:
  • currentNote:当前笔记对象(包含 body 属性)。
  • updateNote:更新笔记内容的回调函数。
  1. 状态管理:
  • 使用 React 的 useState Hook 管理当前编辑器的选项卡状态:
  • selectedTab 的初始值为 "write",表示当前默认显示"编辑"模式。
javascript 复制代码
const convertor = new Showdown.Converter({
    tables: true,
    simplifiedAutoLink: true,
    strikethrough: true,
    tasklists: true,
})
  1. 创建 Showdown.Converter 实例:
  • 配置 Markdown 转换器,使其支持以下扩展功能:
  • tables: true:支持表格语法。
  • simplifiedAutoLink: true:自动将 URL 转换为超链接。
  • strikethrough: true:支持 删除线 语法。
  • tasklists: true:支持任务列表
  1. 作用:将 Markdown 文本转换为 HTML,以便在预览模式中显示。
javascript 复制代码
return (
    <section className="pane editor">
        <ReactMde
            value={currentNote.body}
            onChange={updateNote}
            selectedTab={selectedTab}
            onTabChange={setSelectedTab}
            generateMarkdownPreview={(markdown) => 
                Promise.resolve(convertor.makeHtml(markdown))
            }
            minEditorHeight={80}
            heightUnits="vh"
        />
    </section>
)

ReactMde 编辑器:

  • value:绑定到 currentNote.body,表示当前笔记的内容。
  • onChange:当用户在编辑器中输入内容时,调用 updateNote 更新父组件的状态。
  • selectedTab:当前选中的选项卡("write" 或 "preview")。
  • onTabChange:切换选项卡时调用,更新 selectedTab 的状态。
  • generateMarkdownPreview:预览时调用,将 Markdown 文本转换为 HTML。使用 convertor.makeHtml(markdown) 完成转换,返回一个 Promise。
  • minEditorHeightheightUnits:设置编辑器的最小高度为 80,单位为 vh(视口高度)。

父组件如何与 Editor 交互

  • currentNote:父组件将当前笔记对象传递给 Editor。通过 currentNote.body 显示当前笔记的内容。
  • updateNote:父组件提供回调函数,用于更新笔记内容。

8. index.css

css 复制代码
* {
  box-sizing: border-box;
}

1.* 通配符选择器:

  • 设置所有元素的 box-sizing 为 border-box。
  • 效果:元素的 width 和 height 属性包括内边距(padding)和边框(border),避免计算宽高时的复杂性。
css 复制代码
body {
  margin: 0;
  padding: 0;
  font-family: 'Karla', sans-serif;
}
  1. body 标签:
  • 移除默认的外边距(margin)和内边距(padding)。
  • 设置全局字体为 Karla,使用无衬线字体(sans-serif)作为后备。
css 复制代码
button:focus {
  outline: none;
}
  1. 按钮样式:移除按钮在获得焦点(focus)时的默认轮廓(outline)。
css 复制代码
.ql-editor p,
.ql-editor.ql-blank::before {
  font-size: 1.3em;
  font-weight: 100;
}

ql-editor

  • 专为 ReactMde(Markdown 编辑器)定义。
  • 调整段落文字大小(1.3em)和字体粗细(100,非常细的字体)。
css 复制代码
.pane {
  overflow-y: auto;
}

.pane

  • 应用于侧边栏和编辑器容器。
  • 启用垂直滚动(overflow-y: auto),以显示超出容器高度的内容。
css 复制代码
.sidebar {
  width: 20%;
  height: 100vh;
}

.sidebar

  • 设置侧边栏宽度为 20%,高度占满整个视口(100vh)。
css 复制代码
.editor {
  width: 80%;
  height: 100vh;
}

.editor

  • 设置编辑器宽度为 80%,高度占满整个视口。
css 复制代码
.sidebar--header {
  display: flex;
  justify-content: space-around;
  align-items: center;
}

.sidebar--header

侧边栏标题区域:

  • 使用 flex 布局。
  • 水平排列子元素,且均匀分布(justify-content: space-around)。
  • 垂直居中对齐(align-items: center)。
css 复制代码
.new-note, .first-note {
  cursor: pointer;
  background-color: #4A4E74;
  border: none;
  color: white;
  border-radius: 3px;
}

两类按钮(新建笔记 new-note 和初始笔记 first-note):

  • 指针悬停变成手型(cursor: pointer)。
  • 背景色为深紫色(#4A4E74),字体颜色为白色。
  • 圆角(border-radius: 3px)。
css 复制代码
.title {
  overflow: hidden;
  width: 100%;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

单个笔记标题的样式:

  • 超出宽度的内容隐藏(overflow: hidden)。
  • 使用 flex 布局,子元素之间均匀分布。
css 复制代码
.selected-note {
  background-color: #4A4E74;
}
.selected-note .text-snippet {
  color: white;
  font-weight: 700;
}

当前选中的笔记高亮:

  • 背景色变成深紫色。
  • 子元素 .text-snippet 的字体颜色变成白色,且加粗(font-weight: 700)。
css 复制代码
.gutter {
  background-color: #eee;
  background-repeat: no-repeat;
  background-position: 50%;
}

.gutter

  • 分割侧边栏和编辑器的拖拽条。
  • 设置背景色为浅灰色。
css 复制代码
.gutter.gutter-horizontal {
  background-image: url('data:image/png;base64,...');
}
.gutter.gutter-horizontal:hover {
  cursor: col-resize;
}

.gutter-horizontal

水平分割条的样式:

  • 使用 base64 图片作为背景。
  • 鼠标悬停时,显示水平调整光标(col-resize)。
css 复制代码
.no-notes {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: whitesmoke;
}

当没有任何笔记时的提示区域:

  • 宽度和高度占满视口。
  • 子元素垂直排列(flex-direction: column),水平和垂直居中。
  • 背景色为浅白色(whitesmoke)。
相关推荐
gxn_mmf3 分钟前
典籍知识问答重新生成和消息修改Bug修改
前端·bug
hj10435 分钟前
【fastadmin开发实战】在前端页面中使用bootstraptable以及表格中实现文件上传
前端
乌夷14 分钟前
axios结合AbortController取消文件上传
开发语言·前端·javascript
毫秒AI获客19 分钟前
小红书多账号运营效率优化:技术方案与自动化实践
笔记
菜一头包27 分钟前
c++ std库中的文件操作学习笔记
c++·笔记·学习
晓晓莺歌37 分钟前
图片的require问题
前端
码农黛兮_461 小时前
CSS3 基础知识、原理及与CSS的区别
前端·css·css3
水银嘻嘻1 小时前
web 自动化之 Unittest 四大组件
运维·前端·自动化
(((φ(◎ロ◎;)φ)))牵丝戏安2 小时前
根据输入的数据渲染柱形图
前端·css·css3·js
程序猿阿伟2 小时前
《React Native与Flutter:社交应用中用户行为分析与埋点统计的深度剖析》
flutter·react native·react.js