这篇文章会教大家用 React 编写一个笔记应用程序。用户可以创建、编辑、和切换 Markdown 笔记。
1. nanoid
nanoid 是一个轻量级和安全的唯一字符串ID生成器,常用于JavaScript环境中生成随机、唯一的字符串ID,如数据库主键、会话ID、文件名等场景。
2. React-split
react-split 是一个 React 组件库,它提供了一个简单易用的界面来分割面板,允许用户通过拖动来调整面板的大小。这个库非常适合需要在界面上展示可调整大小的多个区域的场景,比如编辑器、IDE、仪表板等。
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 语法或可视化方式编辑文本。
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:用于编辑当前笔记的内容。
主要功能
- 创建新笔记
- 更新笔记内容
- 切换当前笔记
- 显示分屏布局
- 无笔记时显示提示
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 是自定义的子组件,分别处理笔记列表和笔记编辑。
状态管理
const [notes, setNotes] = React.useState([]);
const [currentNoteId, setCurrentNoteId] = React.useState(
(notes[0] && notes[0].id) || ""
);
- notes:存储所有笔记的状态,初始值为空数组。
- currentNoteId:存储当前笔记的 ID,初始值为第一个笔记的 ID(若无笔记,则为空字符串)。
创建新笔记
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
。
更新笔记
function updateNote(text) {
setNotes(oldNotes => oldNotes.map(oldNote => {
return oldNote.id === currentNoteId
? { ...oldNote, body: text }
: oldNote;
}));
}
- 遍历
notes
,检查每条笔记的 ID。 - 如果某条笔记的 ID 与
currentNoteId
相同,则更新其body
内容。 - 使用
...oldNote
保留笔记其他的属性,更新body
。
查找当前笔记
function findCurrentNote() {
return notes.find(note => {
return note.id === currentNoteId;
}) || notes[0];
}
- 查找与 currentNoteId 匹配的笔记。
- 如果没有匹配到,返回第一条笔记(默认情况)。
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 接收父组件传递的状态和函数来实现动态渲染和交互。
import React from "react";
export default function Sidebar(props) {
- React:用于支持 React 的 JSX 语法。
- export default:将 Sidebar 组件作为默认导出,使其可被其他模块导入。
Sidebar 是一个无状态函数组件,接收 props 参数(即父组件传递的数据)。
生成笔记元素
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>
));
props.notes
:
- 父组件传入的笔记数组,每个笔记对象包含
id
和body
属性。
props.notes.map()
:
- 遍历笔记数组,为每条笔记生成一个 JSX 元素。
key={note.id}
:为每个顶层元素提供唯一的 key 属性,以优化 React 的渲染性能。
- 条件渲染类名:
className={title ${note.id === props.currentNote.id ? "selected-note" : ""}}
:- 如果当前笔记的 id 与 props.currentNote.id 匹配,添加 selected-note 类名。
- 用于高亮显示当前选中的笔记。
- 点击事件:
onClick={() => props.setCurrentNoteId(note.id)}
:- 点击笔记时调用父组件传入的 setCurrentNoteId 方法,更新当前笔记的 id。
- 显示笔记编号:
<h4 className="text-snippet">Note {index + 1}</h4>
:动态显示笔记的编号(从 1 开始)。
侧边栏结构
return (
<section className="pane sidebar">
<div className="sidebar--header">
<h3>Notes</h3>
<button
className="new-note"
onClick={props.newNote}
>+</button>
</div>
{noteElements}
</section>
);
- 侧边栏头部:
<div className="sidebar--header">
:包含标题和创建按钮。- 标题:
<h3>Notes</h3>
。 - 创建按钮:
<button className="new-note" onClick={props.newNote}>+</button>
:
点击按钮调用父组件传入的 newNote 方法,创建一条新笔记。
- 笔记列表:
{noteElements}
:动态渲染生成的笔记列表。
组件渲染逻辑总结
- 笔记渲染:
- 遍历
props.notes
,生成笔记列表。 - 为选中的笔记添加
selected-note
类名。 - 点击笔记切换当前选中的笔记。
- 笔记创建:
- 提供一个按钮,点击后调用
props.newNote
创建新笔记。
- 动态更新:
- 通过 props 与父组件共享状态,每次父组件状态更新时,Sidebar 会自动重新渲染。
7. Editor.jsx
此代码定义了一个名为 Editor
的 React 函数组件,用于实现 Markdown 文本编辑器。它通过 ReactMde
(React Markdown Editor)库提供一个多功能的 Markdown 编辑和预览界面。
import React from "react"
import ReactMde from "react-mde"
import 'react-mde/lib/styles/css/react-mde-all.css';
import Showdown from "showdown"
- React:
- 导入 React 库,支持 JSX 和组件开发。
- ReactMde:
- 导入
React Markdown Editor
,用于渲染 Markdown 编辑器。 ReactMde
提供了内置的编辑和预览功能。
- 样式文件:
- 导入
react-mde
的样式文件,应用默认的编辑器样式。
- Showdown:
- 导入
Showdown
库,用于将 Markdown 文本转换为 HTML。
export default function Editor({ currentNote, updateNote }) {
const [selectedTab, setSelectedTab] = React.useState("write")
- 函数组件:
- 定义一个函数组件 Editor,接收两个 props:
- currentNote:当前笔记对象(包含 body 属性)。
- updateNote:更新笔记内容的回调函数。
- 状态管理:
- 使用 React 的
useState
Hook 管理当前编辑器的选项卡状态: selectedTab
的初始值为 “write
”,表示当前默认显示“编辑”模式。
const convertor = new Showdown.Converter({
tables: true,
simplifiedAutoLink: true,
strikethrough: true,
tasklists: true,
})
- 创建 Showdown.Converter 实例:
- 配置 Markdown 转换器,使其支持以下扩展功能:
tables: true
:支持表格语法。simplifiedAutoLink: true
:自动将 URL 转换为超链接。strikethrough: true
:支持 删除线 语法。tasklists: true
:支持任务列表
- 作用:将 Markdown 文本转换为 HTML,以便在预览模式中显示。
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。minEditorHeight
和heightUnits
:设置编辑器的最小高度为 80,单位为 vh(视口高度)。
父组件如何与 Editor 交互
currentNote
:父组件将当前笔记对象传递给Editor
。通过currentNote.body
显示当前笔记的内容。updateNote
:父组件提供回调函数,用于更新笔记内容。
8. index.css
* {
box-sizing: border-box;
}
1.* 通配符选择器:
- 设置所有元素的 box-sizing 为 border-box。
- 效果:元素的 width 和 height 属性包括内边距(padding)和边框(border),避免计算宽高时的复杂性。
body {
margin: 0;
padding: 0;
font-family: 'Karla', sans-serif;
}
- body 标签:
- 移除默认的外边距(margin)和内边距(padding)。
- 设置全局字体为 Karla,使用无衬线字体(sans-serif)作为后备。
button:focus {
outline: none;
}
- 按钮样式:移除按钮在获得焦点(focus)时的默认轮廓(outline)。
.ql-editor p,
.ql-editor.ql-blank::before {
font-size: 1.3em;
font-weight: 100;
}
ql-editor
:
- 专为
ReactMde
(Markdown 编辑器)定义。 - 调整段落文字大小(
1.3em
)和字体粗细(100
,非常细的字体)。
.pane {
overflow-y: auto;
}
.pane
:
- 应用于侧边栏和编辑器容器。
- 启用垂直滚动(
overflow-y: auto
),以显示超出容器高度的内容。
.sidebar {
width: 20%;
height: 100vh;
}
.sidebar
:
- 设置侧边栏宽度为 20%,高度占满整个视口(100vh)。
.editor {
width: 80%;
height: 100vh;
}
.editor
:
- 设置编辑器宽度为 80%,高度占满整个视口。
.sidebar--header {
display: flex;
justify-content: space-around;
align-items: center;
}
.sidebar--header
:
侧边栏标题区域:
- 使用 flex 布局。
- 水平排列子元素,且均匀分布(justify-content: space-around)。
- 垂直居中对齐(align-items: center)。
.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
)。
.title {
overflow: hidden;
width: 100%;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
单个笔记标题的样式:
- 超出宽度的内容隐藏(
overflow: hidden
)。 - 使用
flex
布局,子元素之间均匀分布。
.selected-note {
background-color: #4A4E74;
}
.selected-note .text-snippet {
color: white;
font-weight: 700;
}
当前选中的笔记高亮:
- 背景色变成深紫色。
- 子元素
.text-snippet
的字体颜色变成白色,且加粗(font-weight: 700
)。
.gutter {
background-color: #eee;
background-repeat: no-repeat;
background-position: 50%;
}
.gutter
:
- 分割侧边栏和编辑器的拖拽条。
- 设置背景色为浅灰色。
.gutter.gutter-horizontal {
background-image: url('data:image/png;base64,...');
}
.gutter.gutter-horizontal:hover {
cursor: col-resize;
}
.gutter-horizontal
:
水平分割条的样式:
- 使用 base64 图片作为背景。
- 鼠标悬停时,显示水平调整光标(col-resize)。
.no-notes {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: whitesmoke;
}
当没有任何笔记时的提示区域:
- 宽度和高度占满视口。
- 子元素垂直排列(
flex-direction: column
),水平和垂直居中。 - 背景色为浅白色(
whitesmoke
)。