从BUG到胜利:我的React Todos单例模式开发之路
段子专栏
话说那是个风和日丽的下午,我,一个自封的“React大神”,决定挑战自我,用单例模式重构我的Todos应用。我自信满满,心想:“区区单例模式,还不手到擒来?”
我打开编辑器,手指飞快地敲击键盘,一行行代码如泉水般涌出。我仿佛能看到未来的自己,在众人面前炫耀这段精妙的单例模式代码,享受着崇拜的目光。
然而,理想很丰满,现实却骨感。当我满怀期待地运行应用时,屏幕上的错误信息如同晴天霹雳,让我瞬间清醒。我与BUG的战斗,正式拉开序幕。
“你这是什么鬼代码?”我对着电脑屏幕咆哮,“为什么你就是不肯乖乖工作呢?”我尝试了一切我能想到的方法,debug、搜索、重写...但每次修改后,问题似乎只是换了张面具,继续顽固地存在着。
正当我准备放弃,打算去养几只猫转移注意力时,突然灵光一闪,我发现了问题所在。原来,是我对单例模式的理解有些偏差,我把它当成了万能钥匙,却忘了每个锁都有自己的形状。
我深呼吸,调整策略,重新审视我的代码。这次,我小心翼翼地处理每一个细节,确保单例模式真正融入了我的Todos应用中,而不是简单地“贴上去”。
终于,在无数次的尝试和失败之后,我的Todos应用焕发出新的生命力。单例模式不再是那个令人头疼的怪物,而是成为了我的得力助手,让我的应用更加高效、稳定。
我坐在椅子上,看着屏幕上流畅运行的Todos列表,心中充满了成就感。“原来,与BUG的相爱相杀,也能如此美妙。”我微笑着对自己说,那一刻,我真正体会到了编程的魅力。
从此以后,我不仅学会了如何正确使用单例模式,更重要的是,我学会了在遇到困难时不轻言放弃,因为每一次挑战,都是成长的机会。
大致思路
我通过模块化的组件设计实现了高效的任务管理。App
组件作为核心,负责全局状态的管理,利用本地存储确保数据的持久化。组件间通过props进行通信,TodoForm
负责新任务的添加,TodoList
遍历显示任务列表,而TodoItem
则具体实现了每个任务的编辑、删除和完成状态切换功能。整体设计体现了良好的事件处理机制和清晰的组件划分。
一、组件化思维:我把整个项目分成了四个组件
在React里,组件是构建用户界面的基本单元,它们遵循“单一职责原则”,即每个组件负责渲染和管理一部分UI。在开发todos应用时,我首先将整个应用拆分为一个父组件App
和三个主要子组件:TodoForm
、TodoList
、TodoItem
。
- App:负责管理应用的状态并将其传递给子组件。在App中,我将处理主要的逻辑——添加、删除和编辑todos。并且把子组件渲染到页面上。
- TodoForm:负责收集用户输入的新todo项,通过表单的形式,用户可以轻松添加新的事项。
- TodoList:作为容器组件,它接收来自父组件的数据(todos数组),并将数据映射为多个
TodoItem
组件。 - TodoItem:展示单个todo项,支持编辑、删除和完成状态切换。
二、数据管理
在React中,数据管理是至关重要的。我将数据(todos数组)保存在最高层级的组件——App
中,这使得数据管理更加集中化。通过props将数据传递给子组件,子组件可以通过回调函数与父组件通信,实现数据的更新。
为了确保数据状态的一致性,我特别注意了表单组件的管理。在TodoForm
中,我将输入框的值与组件状态inputText
绑定,确保任何界面操作都能立即反映在状态中,反之亦然。
三、循环列表
在TodoList
组件中,我利用了React的JSX语法,通过map
函数遍历todos数组,动态生成TodoItem
组件。但因为React的虚拟DOM机制会智能地比较前后两次渲染的差异,所以我只更新必要的部分,避免了不必要的重渲染,不仅简化了代码,还提高了应用的响应速度。
四、利用单例模式持久储存
为了实现todos的持久化存储,我在项目中引入了单例模式的Storage
类。这个类封装了对浏览器localStorage
的访问,确保在整个应用中只有一个实例管理数据的读写。通过getInstance
方法获取单例实例,我能够在App
组件的componentDidUpdate
生命周期钩子中,将更新后的todos数组存储到localStorage
中,实现了数据的持久化。
代码实现】
1. App.jsx
首先,我们先导入必要的模块
import { Component } from "react";
import TodoForm from "./components/todoForm";
import TodoList from "./components/todoList";
import "./App.css";
import Storage from "./utils/storage";
然后创建了一个App类
class App extends Component {
constructor(props) {
super(props);
this.state = {
todos: []
};
}
- 在这里我使用到了es6的class语法糖创建了App去继承基类Component,它是React中的基础组件类,提供了构建用户界面的基本功能。相比于传统的构造函数和原型链继承方式,class语法糖的语法更清晰、更接近面向对象语言,类和其方法可以很好地封装数据和行为,且继承更简单。
- 并且,我在构造器里创建了state状态来动态渲染数据
最后 利用jsx语法糖渲染写的页面
render() {
const { todos } = this.state;
return (
<div className="todo-app">
<h1 className="todo-app__title">Todo List</h1>
<TodoForm addTodo={this.addTodo} />
<TodoList todos={todos} deleteTodo={this.deleteTodo} toggleTodo={this.toggleTodo} editTodo={this.editTodo} />
</div>
);
}
利用单例模式持久化存储
为了持久的存储数据,我在src文件夹下建立一个utils文件夹,在里面存储Storage,封装了一个单例模式来存储todos
/**
* @func 基于localStorage封装Storage类,单例模式
* @author kl
* @date 2024-07-03
*/
class Storage {
constructor() {}
static getInstance() {
if (!Storage.instance) {
Storage.instance = new Storage();
}
return Storage.instance;
}
getItem(key) {
return localStorage.getItem(key);
}
setItem(key, value) {
localStorage.setItem(key, value);
}
}
- 单例模式实现
我在Storage
类使用了单例模式,这是一种设计模式,保证一个类只有一个实例,并提供一个全局访问点。我是通过getInstance
静态方法实现单例模式。
static getInstance()
:这是一个静态方法,意味着它可以不通过实例化类而直接被类调用。这个方法首先检查Storage.instance
是否存在,如果不存在,则创建一个新的Storage
实例并将其赋值给Storage.instance
。之后,所有对该方法的调用都将返回同一个实例,确保了单例模式的要求。
- 封装
localStorage
方法
-
getItem(key)
:这个方法接受一个key
参数,并调用localStorage.getItem(key)
来获取存储在localStorage
中的对应值。这是对localStorage
API的直接封装,提供了更简洁的接口来访问存储的值。 -
setItem(key, value)
:这个方法接受一个key
和一个value
参数,并调用localStorage.setItem(key, value)
来在localStorage
中存储一个键值对。同样,这也是对原生API的封装,使得存储操作更加简洁。
- 运用
在
App.js
中,先导入了Storage
模块
import Storage from "./utils/storage";
const instance = Storage.getInstance();
Storage.getInstance()
方法确保了在整个应用程序中只有一个 Storage
类的实例被创建和使用。这是单例模式的核心特点,即保证一个类只有一个实例,并提供一个访问它的全局访问点。
在 App
组件的构造函数中,使用 instance.getItem("todos")
从本地存储中获取 todos
数据,并将其解析为 JavaScript 对象。如果本地存储中没有 todos
数据,则使用空数组初始化 todos
。
constructor(props) {
super(props);
const saveTodos = JSON.parse(instance.getItem("todos")) || [];
this.state = {
todos: saveTodos
};
}
在 componentDidUpdate
生命周期方法中,每当组件的 state
或 props
更新时,都会将最新的 todos
数组存储到本地存储中。这是通过调用 instance.setItem("todos", JSON.stringify(this.state.todos))
实现的。
componentDidUpdate() {
instance.setItem("todos", JSON.stringify(this.state.todos));
}
这样,即使页面刷新或重新加载,todos
数据也不会丢失,因为它已经被持久化到本地存储中。用户每次打开应用时,都能看到上次保存的 todos
。
通过这种方式,单例模式在应用程序中确保了 Storage
实例的唯一性,并提供了一种简便的方法来持久化 todos
数据,使得 App
组件能够专注于其他逻辑,而不需要直接处理本地存储的细节。
todoForm.jsx
TodoForm
负责新任务的添加,我们只需要在这里把输入框和提交按钮完成即可,既然有提交事件,我选择在这里使用表单
render() {
return (
<form className="todo-form" onSubmit={ this.handleSubmit }>
<input type="text" placeholder="Enter your todo" value={inputText} className="todo-form__input" onChange={this.handleChange}/>
<button type="submit" className="todo-form__button">Add</button>
</form>
);
}
输入框通过value属性和onChange事件处理器与组件状态相连,确保用户输入的内容能够实时反映在组件状态中。提交按钮则用于触发待办事项的添加逻辑,通常在handleSubmit方法中实现,比如将当前inputText值添加到待办事项列表中。
我还在构造器里写了两个函数handleChange
、handleSubmit
,handleChange
是一个事件处理器,它被绑定到<input>
元素的onChange
事件上。每当用户在输入框中键入或修改文本时,这个函数就会被调用。
handleSubmit
函数处理表单的提交事件,当用户点击“提交”按钮时调用。
handleSubmit = (e) => {
e.preventDefault();
if(this.state.inputText.trim()) {
this.props.addTodo(this.state.inputText);
this.setState({
inputText: " "
});
}
}
- e.preventDefault();阻止了表单的默认提交行为
- if(this.state.inputText.trim())检查inputText是否非空(即用户输入了内容)。trim()方法用于移除字符串两端的空白字符,确保即使用户输入了只有空格的文本,也不会被视为有效输入。
- this.props.addTodo(this.state.inputText);如果输入有效,就调用从父组件传入的addTodo方法,将当前inputText的值作为参数传递给它,这通常用于向待办事项列表中添加一项。
- this.setState({ inputText: " " });清空inputText,将其值设为空字符串,这样用户在添加完待办事项后,输入框会自动清空,准备接受下一次输入。
todoList.jsx
在这个模块就比较简单了,只需要处理循环渲染todoItem
然后把App传进来的数据和方法传递给todoItem
。
下面是对代码的关键解析:
- 导入模块
import { Component } from "react";
import TodoItem from "./todoItem";
import "./css/todoList.css";
- 创建
TodoList
组件
class TodoList extends Component {
render() {
const { todos, toggleTodo, deleteTodo, editTodo } = this.props;
return (
<ul className="todo-list">
{todos.map((todo, index) => (
<TodoItem key={index} index={index} todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo} editTodo={editTodo} />
))}
</ul>
);
}
}
const { todos, toggleTodo, deleteTodo, editTodo } = this.props;
:从组件的属性props
中解构出todos
数组以及三个方法toggleTodo
、deleteTodo
和editTodo
。这些方法通常由父组件App
传递下来,用于操作待办事项的完成状态、删除和编辑。{todos.map((todo, index) => ... )}
:遍历todos
数组,对于每个todo
项,创建一个TodoItem
组件实例,并将其插入到虚拟DOM树中。key={index}
为每个子元素提供唯一标识,帮助React更高效地更新和重渲染列表。
todoItem
TodoItem
组件是一个功能组件,它接收一个todo
对象以及一组处理待办事项的方法作为属性,能够根据当前状态显示或编辑待办事项的信息,并允许用户通过UI操作来改变待办事项的状态。这种设计使得组件具有高度的交互性和动态性,能够响应用户的行为并及时更新UI。
代码详解
- 导入模块
import { Component } from "react";
import "./css/todoItem.css";
- 创建
TodoItem
组件
class TodoItem extends Component {
constructor(props) {
super(props);
this.state = {
isEditing: false,
editText: this.props.todo.text
};
}
// ... 其他方法 ...
}
class TodoItem extends Component
:定义一个名为TodoItem
的React组件,它继承自Component
。constructor(props)
:组件的构造函数,初始化组件的状态。isEditing
用于跟踪当前条目是否处于编辑模式,editText
用于保存正在编辑的文本内容。
- 方法定义
handleEditChange = (e) => {
this.setState({
editText: e.target.value
});
}
handleEditChange
:当用户在编辑模式下的输入框中输入或修改文本时调用,更新editText
状态。
handleEditSave = () => {
const { index, editTodo } = this.props;
const { editText } = this.state;
this.setState({
isEditing: false
});
editTodo(index, editText);
}
handleEditSave
:当用户点击“保存”按钮时调用,更新状态以退出编辑模式,并调用editTodo
方法更新待办事项的文本。
- 渲染方法
render() {
return (
<li className={`todo-item ${completed ? "todo-item__completed" : ""}`}>
{isEditing ? (
// 编辑状态
) : (
常态
)}
</li>
);
}
- 在
render()
方法中,根据isEditing
状态的不同,组件渲染不同的UI:- 如果
isEditing
为true
,显示一个输入框供用户编辑待办事项的文本,以及一个“保存”按钮用于确认编辑。 - 如果
isEditing
为false
,显示待办事项的文本、一个“编辑”按钮用于进入编辑模式,以及一个“删除”按钮用于删除该待办事项。文本旁边还有一个可点击的区域,用于切换待办事项的完成状态。
- 如果
源码分享
想要的私信我
结语
编程,就像是一场旅行,充满了挑战与惊喜。在这个过程中,我们或许会遭遇重重困难,但正是这些困难,让我们不断成长,不断超越自我。希望我的故事能给你带来一些启示,愿你在编程的道路上越走越远,成为真正的编程高手!
转载自:https://juejin.cn/post/7387999151412281379