手把手教你用 React 搓一个 TodoList 待办清单(Vite+Bun+Stylus)

306 阅读4分钟

在本文中,我们将从零开始搭建一个基于 Vite + React + Stylus + Bun 的 TodoList(待办事项)应用。

成品展示:

todolist.gif

一、项目初始化流程

1. 创建项目

我们使用 Vite 快速创建一个 React 项目:

npm init vite
cd todo-list

这将生成一个基础的 React 项目结构,包含必要的入口文件和配置。

2. 使用 Bun 安装依赖

Bun 是新一代 JavaScript 运行时和包管理器,速度快于 npm 和 pnpm。

如果是多人协作的中大型项目还是用pnpm稳健一点,小编使用bun完全是想体验一下。这里附上传送门:Bun — 快速的一体化 JavaScript 运行时

安装 Bun(通过 npm)

如果你尚未安装 Bun,可以通过 npm 安装:

npm install -g bun

然后进入项目目录,使用 Bun 安装依赖:

bun install

Bun 的优势:极快的依赖安装速度,内置打包工具,未来可能替代 Node.js。

3. 添加 Stylus 支持

Stylus 是一种 CSS 预处理器,语法简洁,支持变量、嵌套、混合等高级功能。

安装 Stylus:

bun add stylus --dev

Vite 默认支持 .styl 文件,只需引入即可生效,无需额外配置插件。

例如,在入口文件中引入全局样式:

// src/index.jsx
import './global.styl'

结论:是的,这种方式是 Vite 对 Stylus 的原生支持方式,无需额外配置即可全局生效。


二、项目结构概览

todo-list/
├── public/
├── src/
│   ├── components/
│   │   ├── TodoForm.jsx
│   │   ├── TodoItem.jsx
│   │   └── TodoList.jsx
│   ├── hooks/
│   │   └── useTodos.jsx
│   ├── App.jsx
│   ├── index.jsx
│   └── global.styl  
├── package.json
└── vite.config.js

三、React 18 入口配置(index.jsx)

Vite 默认生成的是 main.jsx,但我们使用 React 18 推荐的新入口方式 createRoot 替代旧的 ReactDOM.render()

// src/index.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import './global.styl'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
)

知识点解析:

  • StrictMode: 检测潜在问题,如不安全生命周期、废弃 API。
  • createRoot: React 18 新增方法,支持并发模式。
  • ./global.styl: 全局样式文件,自动生效。
  • ./index.css: 可以删除或保留。

四、组件实现详解(含注释)

1. App.jsx

import { Todos } from './components/Todos'

export default function App() {
  return (
    <div>
      <h1>React TodoList</h1>
      <Todos /> {/* 主组件,整合所有子组件 */}
    </div>
  )
}

2. TodoForm.jsx

import { useState } from 'react'

export const TodoForm = ({ onAddTodo }) => {
  const [text, setText] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    const trimmedText = text.trim()
    if (!trimmedText) return // 防止空内容提交
    onAddTodo(trimmedText)
    setText('')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="输入任务内容"
      />
      <button type="submit">添加</button>
    </form>
  )
}

负责新增待办项,绑定输入状态并触发父组件回调。


3. TodoItem.jsx

export const TodoItem = ({ todo, onToggle, onDelete }) => {
  const { id, text, isCompleted } = todo

  return (
    <div className="todo-item">
      <input
        type="checkbox"
        checked={isCompleted}
        onChange={onToggle} // 切换完成状态
      />
      <span className={isCompleted ? 'completed' : ''}>{text}</span>
      <button onClick={onDelete}>删除</button>
    </div>
  )
}

展示单个待办项,并提供完成/删除操作。


4. TodoList.jsx

import { TodoItem } from './TodoItem'

export const TodoList = ({ todos, onToggle, onDelete }) => {
  return (
    <div className="todo-list">
      {todos.length > 0 ? (
        todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={() => onToggle(todo.id)} // 包装为函数传递id
            onDelete={() => onDelete(todo.id)} // 同上
          />
        ))
      ) : (
        <p>暂无待办事项</p>
      )}
    </div>
  )
}

渲染所有待办项列表,判断是否为空。


5. Todos.jsx

import { useTodos } from '../hooks/useTodos'

export const Todos = () => {
  const { todos, addTodo, toggleTodo, deleteTodo } = useTodos()

  return (
    <div>
      <h2>我的待办事项</h2>
      <TodoForm onAddTodo={addTodo} />
      <TodoList
        todos={todos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
      />
    </div>
  )
}

整合表单与列表,是整个 TodoList 的主组件。


五、自定义 Hook:useTodos

import { useState, useEffect } from 'react'

export const useTodos = () => {
  const [todos, setTodos] = useState(
    JSON.parse(localStorage.getItem('todos')) || []
  )

  const addTodo = (text) => {
    setTodos([
      ...todos,
      { id: Date.now(), text, isCompleted: false }
    ])
  }

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id
        ? { ...todo, isCompleted: !todo.isCompleted }
        : todo
    ))
  }

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }

  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos))
  }, [todos])

  return { todos, addTodo, toggleTodo, deleteTodo }
}

封装状态逻辑,使得 UI 更加清晰,也便于复用。


六、数据存储:localStorage

localStorage 是浏览器提供的本地存储机制,用于保存键值对数据,刷新页面后不会丢失。

示例:

localStorage.setItem('todos', JSON.stringify(todos))
const storedTodos = JSON.parse(localStorage.getItem('todos'))

优势:

  • 数据持久化,页面刷新不丢失
  • 不需要服务器参与
  • 易于使用,适合小型项目的数据存储

注意:不适合敏感数据或大量数据存储。

刷新后不丢失,查看方式如图所示 image.png

七、Bun 与 pnpm、npm 的简单对比

特性npmpnpmBun
安装速度较快极快
包管理器原生 npm硬链接优化内置 JS 运行时
构建能力支持打包、构建
安装命令npm installpnpm installbun install
启动命令npm run devpnpm devbun dev

✅bun不只是包管理器,它野心很大,但本篇仅把它当作包管理器用


八、项目亮点与难点分析

项目亮点

亮点描述
Vite 构建工具快速启动、热更新,提升开发体验
Bun 包管理器极速安装依赖,内置 JS 运行时
Stylus 样式方案支持变量、嵌套、混合等高级特性
useTodos 自定义 Hook封装状态逻辑,组件更聚焦 UI
localStorage 持久化数据刷新不丢失,用户体验更佳
路径别名优化减少路径嵌套,提高代码可读性

项目难点

难点描述
跨组件通信props 层层传递,可考虑用 useContext 或 Zustand 优化
状态共享限制当前未使用全局状态管理,大型项目中可能需引入 Redux/Zustand

九、结语

本项目完整实现了:

  • 使用 Vite + React + Stylus + Bun 构建现代前端应用
  • 组件结构清晰,职责分明
  • 使用 localStorage 实现数据持久化
  • 自定义 Hook 提升代码复用性和可维护性
  • Stylus 的快速集成与全局样式导入
  • Bun 的使用方式与性能优势