likes
comments
collection
share

从BUG到胜利:我的React Todos单例模式开发之路

作者站长头像
站长
· 阅读数 55

段子专栏

话说那是个风和日丽的下午,我,一个自封的“React大神”,决定挑战自我,用单例模式重构我的Todos应用。我自信满满,心想:“区区单例模式,还不手到擒来?”

我打开编辑器,手指飞快地敲击键盘,一行行代码如泉水般涌出。我仿佛能看到未来的自己,在众人面前炫耀这段精妙的单例模式代码,享受着崇拜的目光。

然而,理想很丰满,现实却骨感。当我满怀期待地运行应用时,屏幕上的错误信息如同晴天霹雳,让我瞬间清醒。我与BUG的战斗,正式拉开序幕。

“你这是什么鬼代码?”我对着电脑屏幕咆哮,“为什么你就是不肯乖乖工作呢?”我尝试了一切我能想到的方法,debug、搜索、重写...但每次修改后,问题似乎只是换了张面具,继续顽固地存在着。

正当我准备放弃,打算去养几只猫转移注意力时,突然灵光一闪,我发现了问题所在。原来,是我对单例模式的理解有些偏差,我把它当成了万能钥匙,却忘了每个锁都有自己的形状。

我深呼吸,调整策略,重新审视我的代码。这次,我小心翼翼地处理每一个细节,确保单例模式真正融入了我的Todos应用中,而不是简单地“贴上去”。

终于,在无数次的尝试和失败之后,我的Todos应用焕发出新的生命力。单例模式不再是那个令人头疼的怪物,而是成为了我的得力助手,让我的应用更加高效、稳定。

我坐在椅子上,看着屏幕上流畅运行的Todos列表,心中充满了成就感。“原来,与BUG的相爱相杀,也能如此美妙。”我微笑着对自己说,那一刻,我真正体会到了编程的魅力。

从此以后,我不仅学会了如何正确使用单例模式,更重要的是,我学会了在遇到困难时不轻言放弃,因为每一次挑战,都是成长的机会。

大致思路

我通过模块化的组件设计实现了高效的任务管理。App组件作为核心,负责全局状态的管理,利用本地存储确保数据的持久化。组件间通过props进行通信,TodoForm负责新任务的添加,TodoList遍历显示任务列表,而TodoItem则具体实现了每个任务的编辑、删除和完成状态切换功能。整体设计体现了良好的事件处理机制和清晰的组件划分。

一、组件化思维:我把整个项目分成了四个组件

在React里,组件是构建用户界面的基本单元,它们遵循“单一职责原则”,即每个组件负责渲染和管理一部分UI。在开发todos应用时,我首先将整个应用拆分为一个父组件App和三个主要子组件:TodoFormTodoListTodoItem

  • 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);
    }
}
  1. 单例模式实现

我在Storage类使用了单例模式,这是一种设计模式,保证一个类只有一个实例,并提供一个全局访问点。我是通过getInstance静态方法实现单例模式。

  • static getInstance():这是一个静态方法,意味着它可以不通过实例化类而直接被类调用。这个方法首先检查Storage.instance是否存在,如果不存在,则创建一个新的Storage实例并将其赋值给Storage.instance。之后,所有对该方法的调用都将返回同一个实例,确保了单例模式的要求。
  1. 封装localStorage方法
  • getItem(key):这个方法接受一个key参数,并调用localStorage.getItem(key)来获取存储在localStorage中的对应值。这是对localStorage API的直接封装,提供了更简洁的接口来访问存储的值。

  • setItem(key, value):这个方法接受一个key和一个value参数,并调用localStorage.setItem(key, value)来在localStorage中存储一个键值对。同样,这也是对原生API的封装,使得存储操作更加简洁。

  1. 运用 在 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 生命周期方法中,每当组件的 stateprops 更新时,都会将最新的 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值添加到待办事项列表中。

我还在构造器里写了两个函数handleChangehandleSubmithandleChange是一个事件处理器,它被绑定到<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。 下面是对代码的关键解析:

  1. 导入模块
import { Component } from "react";
import TodoItem from "./todoItem";
import "./css/todoList.css";
  1. 创建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数组以及三个方法toggleTododeleteTodoeditTodo。这些方法通常由父组件App传递下来,用于操作待办事项的完成状态、删除和编辑。
  • {todos.map((todo, index) => ... )}:遍历todos数组,对于每个todo项,创建一个TodoItem组件实例,并将其插入到虚拟DOM树中。key={index}为每个子元素提供唯一标识,帮助React更高效地更新和重渲染列表。

todoItem

TodoItem组件是一个功能组件,它接收一个todo对象以及一组处理待办事项的方法作为属性,能够根据当前状态显示或编辑待办事项的信息,并允许用户通过UI操作来改变待办事项的状态。这种设计使得组件具有高度的交互性和动态性,能够响应用户的行为并及时更新UI。

代码详解

  1. 导入模块
import { Component } from "react";
import "./css/todoItem.css";
  1. 创建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用于保存正在编辑的文本内容。
  1. 方法定义
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方法更新待办事项的文本。
  1. 渲染方法
render() {
    return (
        <li className={`todo-item ${completed ? "todo-item__completed" : ""}`}>
            {isEditing ? (
            // 编辑状态
            ) : (
            常态
            )}
        </li>
    );
}
  • render()方法中,根据isEditing状态的不同,组件渲染不同的UI:
    • 如果isEditingtrue,显示一个输入框供用户编辑待办事项的文本,以及一个“保存”按钮用于确认编辑。
    • 如果isEditingfalse,显示待办事项的文本、一个“编辑”按钮用于进入编辑模式,以及一个“删除”按钮用于删除该待办事项。文本旁边还有一个可点击的区域,用于切换待办事项的完成状态。

源码分享

想要的私信我

结语

编程,就像是一场旅行,充满了挑战与惊喜。在这个过程中,我们或许会遭遇重重困难,但正是这些困难,让我们不断成长,不断超越自我。希望我的故事能给你带来一些启示,愿你在编程的道路上越走越远,成为真正的编程高手!

转载自:https://juejin.cn/post/7387999151412281379
评论
请登录