📅 我们继续 50 个小项目挑战!------NotesApp组件
仓库地址:https://gitee.com/hhm-hhm/50days50projects.git
搭配 TailwindCSS 和 marked 库,构建一个支持 Markdown 渲染的笔记应用。该应用允许用户添加、编辑、删除笔记,并且内容会自动保存到 localStorage 中。每个笔记支持实时 Markdown 预览,提供良好的交互体验。
🌀 组件目标
- 支持创建多个笔记卡片
- 每个笔记可切换"编辑"与"预览"模式
- 实时渲染 Markdown 内容
- 支持删除笔记
- 自动保存内容到
localStorage - 使用
TailwindCSS快速构建 UI 样式
🔧 NotesApp.tsx组件实现
TypeScript
import React, { useState, useEffect } from 'react'
import { marked } from 'marked'
// 定义笔记类型
interface Note {
text: string
editing: boolean
}
const NotesApp: React.FC = () => {
// 初始化状态:从 localStorage 读取
const [notes, setNotes] = useState<Note[]>(() => {
const saved = localStorage.getItem('notes')
if (saved) {
try {
const texts = JSON.parse(saved) as string[]
return texts.map((text) => ({ text, editing: false }))
} catch (e) {
console.warn('Failed to parse notes from localStorage', e)
return []
}
}
return []
})
// 自动保存到 localStorage
useEffect(() => {
const texts = notes.map((note) => note.text)
localStorage.setItem('notes', JSON.stringify(texts))
}, [notes])
// 添加新笔记
const addNote = () => {
setNotes((prev) => [...prev, { text: '', editing: true }])
}
// 删除笔记
const deleteNote = (index: number) => {
setNotes((prev) => prev.filter((_, i) => i !== index))
}
// 切换编辑模式
const toggleEdit = (index: number) => {
setNotes((prev) =>
prev.map((note, i) => (i === index ? { ...note, editing: !note.editing } : note))
)
}
// 更新笔记内容(由 textarea 的 onChange 触发)
const updateNote = (index: number, newText: string) => {
setNotes((prev) => prev.map((note, i) => (i === index ? { ...note, text: newText } : note)))
}
// 渲染 Markdown
const renderMarkdown = (text: string): string => {
return marked(text, { sanitize: true }) // 启用 sanitize 防 XSS(marked v4+ 已弃用,见下方说明)
}
return (
<>
<div className="fixed top-5 left-20 z-100 text-2xl text-red-500">
CSDN@Hao_Harrision
</div>
{/* 顶部固定添加按钮 */}
<div className="fixed top-4 right-4 z-50">
<button
onClick={addNote}
className="cursor-pointer rounded bg-lime-500 px-4 py-2 text-white shadow transition hover:bg-lime-600 active:scale-95"
aria-label="添加笔记">
<i className="fas fa-plus mr-2"></i>
添加笔记
</button>
</div>
{/* 笔记列表 */}
<div className="flex flex-wrap p-16">
{notes.map((note, index) => (
<div
key={index}
className="m-6 flex h-[400px] w-[400px] flex-col overflow-y-auto bg-white shadow-lg">
{/* 工具栏 */}
<div className="flex justify-end bg-lime-500 p-2">
<button
className="ml-2 cursor-pointer text-lg text-white"
onClick={() => toggleEdit(index)}
aria-label="编辑">
<i className="fas fa-edit"></i>
</button>
<button
className="ml-2 cursor-pointer text-lg text-white"
onClick={() => deleteNote(index)}
aria-label="删除">
<i className="fas fa-trash-alt"></i>
</button>
</div>
{/* 展示模式:渲染 Markdown */}
{!note.editing ? (
<div
className="prose prose-sm max-w-none p-5"
dangerouslySetInnerHTML={{ __html: renderMarkdown(note.text) }}
/>
) : (
/* 编辑模式:textarea */
<textarea
value={note.text}
onChange={(e) => updateNote(index, e.target.value)}
className="w-full flex-1 resize-none border-none p-5 font-sans text-base outline-none"
autoFocus
/>
)}
</div>
))}
</div>
</>
)
}
export default NotesApp
🔄 核心转换对照表
| 功能 | Vue 3 | React + TS |
|---|---|---|
| 状态 | ref() |
useState() |
| 事件传参 | @change="fn('x')" |
onChange={() => fn('x')} |
| 条件类名 | :class="[...]" |
模板字符串 + 三元表达式 |
| 逻辑复用 | 无(重复代码) | 提取 renderToggle 函数 |
| 类型检查 | 无(除非用 TSX) | 完整 TypeScript 支持 |
1. 状态管理:reactive → useState
| Vue | React |
|---|---|
reactive([...]) |
useState<Note[]>(...) |
| 响应式数组 | 不可变更新(使用 map, filter 等) |
✅ React 中必须通过 setNotes 返回新数组,不能直接修改原数组。
2. 初始化:惰性初始化 + 错误处理
TypeScript
const [notes, setNotes] = useState<Note[]>(() => {
// 只在首次渲染执行
const saved = localStorage.getItem('notes');
if (saved) {
try {
return JSON.parse(saved).map(...);
} catch (e) { /* fallback */ }
}
return [];
});
✅ 使用 惰性初始化函数 避免重复解析;
✅ 添加 try/catch 防止 localStorage 数据损坏导致崩溃。
3. 自动保存:watchEffect → useEffect
TypeScript
useEffect(() => {
localStorage.setItem('notes', JSON.stringify(notes.map(n => n.text)));
}, [notes]);
✅ 依赖 notes,每次变化自动保存;
✅ 仅保存 text 字段(与 Vue 行为一致)。
4. 不可变更新(关键!)
Vue 可直接修改 notes[index].editing = true,但 React 必须返回新对象:
TypeScript
// 切换编辑
setNotes(prev =>
prev.map((note, i) =>
i === index ? { ...note, editing: !note.editing } : note
)
);
// 更新内容
setNotes(prev =>
prev.map((note, i) =>
i === index ? { ...note, text: newText } : note
)
);
// 删除
setNotes(prev => prev.filter((_, i) => i !== index));
✅ 所有操作都保持状态不可变,符合 React 原则。
5. Markdown 渲染:
v-html → dangerouslySetInnerHTML
TypeScript
<div dangerouslySetInnerHTML={{ __html: renderMarkdown(note.text) }} />
⚠️ React 禁止直接插入 HTML ,必须显式使用 dangerouslySetInnerHTML;
✅ 与 v-html 功能等效,但需自行处理 XSS 风险。
6. 事件处理:参数传递
TypeScript
onClick={() => deleteNote(index)}
onChange={(e) => updateNote(index, e.target.value)}
✅ 使用箭头函数捕获索引和事件值。
7. 编辑模式优化
- 添加
autoFocus到<textarea>,提升 UX; - 使用
aria-label提升无障碍访问。
8. Tailwind 样式完全兼容
-
所有类名(
fixed,top-4,bg-lime-500,prose,shadow-lg等)直接复用; -
prose类来自 Tailwind Typography,需安装插件:bashnpm install -D @tailwindcss/typography并在
tailwind.config.js中启用:TypeScriptplugins: [require('@tailwindcss/typography')],
9. TypeScript 类型安全
- 定义
Note接口; - 所有函数参数、状态、返回值均有明确类型;
- 防止运行时错误。
✅ 最终效果
- 页面右上角固定"添加笔记"按钮;
- 每个笔记支持 编辑 ↔ 预览 切换;
- 预览使用 Markdown 渲染(支持标题、列表、代码等);
- 所有笔记 自动持久化到 localStorage;
- 支持删除、实时编辑;
- 代码类型安全、结构清晰、符合 React 最佳实践。
🎨 TailwindCSS 样式重点讲解
| 类名 | 作用 |
|---|---|
fixed, top-4, right-4, z-50 |
固定在右上角的添加按钮 |
bg-lime-500, text-white, hover:bg-lime-600 |
按钮的背景色、悬停效果 |
transition, active:scale-95 |
添加按钮的动画反馈 |
flex, flex-wrap, p-16 |
布局容器,支持响应式排版 |
m-6, h-[400px], w-[400px] |
每个笔记卡片的大小和间距 |
overflow-y-auto |
支持滚动查看长内容 |
bg-white, shadow-lg |
卡片背景和阴影效果 |
bg-lime-500, p-2 |
工具栏背景和内边距 |
text-lg, text-white |
图标按钮的样式 |
v-html 渲染区:prose, prose-sm, max-w-none |
使用 Tailwind 的 prose 类美化 Markdown 渲染结果 |
textarea 区域:resize-none, border-none, outline-none |
移除默认边框和调整大小功能,提升编辑体验 |
| [🎯 TailwindCSS 样式说明] |
🦌 路由组件 + 常量定义
router/index.tsx 中 children数组中添加子路由
TypeScript
{
path: '/',
element: <App />,
children: [
...
{
path: '/NotesApp',
lazy: () =>
import('@/projects/NotesApp').then((mod) => ({
Component: mod.default,
})),
},
],
},
constants/index.tsx 添加组件预览常量
TypeScript
import demo33Img from '@/assets/pic-demo/demo-33.png'
省略部分....
export const projectList: ProjectItem[] = [
省略部分....
{
id: 33,
title: 'Notes App',
image: demo33Img,
link: 'NotesApp',
},
]
🚀 小结
通过这篇文章,我们使用 React19 、TailwindCSS 和 marked 构建了一个功能完善的 Markdown 笔记应用。它不仅支持添加、编辑、删除笔记,还能自动保存内容,并实时渲染 Markdown。该应用结构清晰,易于扩展,是一个非常适合初学者和进阶者学习的项目。
✅ 支持导出为 Markdown 文件,将当前笔记导出为 .md 文件下载。
✅ 根据关键词搜索笔记内容。
✅ 允许用户通过拖拽重新排列笔记顺序。
✅ 支持暗色/亮色主题切换。
📅 明日预告: 我们将完成AnimatedCountdown组件,非常有意思的倒计时动画组件。🚀
感谢阅读,欢迎点赞、收藏和分享 😊
原文链接:https://blog.csdn.net/qq_44808710/article/details/149447129
每天造一个轮子,码力暴涨不是梦!🚀