网络埋伏纪事

React 初学者教程 13:用 React 创建一个简单的 Todo List 应用

网络埋伏纪事 · 2016-10-28翻译 · 1250阅读 原文链接

概述:通过学习如何创建经典的 Todo List 应用,将所有学过的 React 技巧投入到实战中。

如果说创建 “Hello, World!” 示例是庆祝你开始涉足 React,那么创建一个经典的 Todo List 应用是庆祝你接近掌握 React!在本教程中,我们要把已经学习过的很多概念和技术综合在一起,创建一个如下的应用:

这个 Todo List 应用的工作方式很简单。在文本框中键入任务,按下 Add(或者回车)。在提交了任务之后,你会看到它作为一个条目出现。你可以继续添加任务到附加条目,并让它们都显示出来:

很简单,对吧?在如下的小节中,我们将从头开始创建这个应用,详细学习在此过程中一切是如何发生的。

开始

首先创建一个新 HTML 文档。在其中添加如下内容:

<!DOCTYPE html>
<html>

<head>
  <title>React! React! React!</title>
  <script src="https://fb.me/react-15.1.0.js"></script>
  <script src="https://fb.me/react-dom-15.1.0.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>

  <style>

  </style>
</head>

<body>

  <div id="container">

  </div>

  <script type="text/babel">
    var destination = document.querySelector("#container");

    ReactDOM.render(
      <div>
        Hello!
      </div>,
      destination
    );
  </script>
</body>

</html>

如果在浏览器中预览,会看到 “Hello!” 出现。下面,我们开始创建 Todo List 应用。

创建 UI

首先让 UI 元素跑起来。这个应用的 UI 并不复杂。我们打算先让输入框和按钮显示出来。用 divformbutton 元素就可以搞定。

所有元素都会放在一个 TodoList 组件中。现在在 ReactDOM.render 方法中添加如下代码:

var TodoList = React.createClass({
  render: function() {
      return (
        <div className="todoListMain">
          <div className="header">
            <form>
              <input placeholder="enter task">
              </input>
              <button type="submit">add</button>
            </form>
          </div>
        </div>
      );
    }
});

ReactDOM.render 方法内,我们需要调用新添加的 TodoList 组件以渲染它。继续,将已有的 JSX 用如下代码替换:

ReactDOM.render(
  <div>
    <TodoList/>
  </div>,
  destination
);

存盘,然后在浏览器中预览。你会看到如下的界面:

如果你对你看到的感到惊讶,花几分钟来看看我们在 TodoList 组件中定义的 JSX。这里应该没有什么令人吃惊的东西。我们只是定义了几个看起来很无聊的 HTML 元素。说到这个,我们来引入一些 CSS,让 HTML 元素看起来没那么无聊。

style 元素内,添加如下 CSS 代码:

body {
  padding: 50px;
  background-color: #66CCFF;
  font-family: sans-serif;
}
.todoListMain .header input {
  padding: 10px;
  font-size: 16px;
  border: 2px solid #FFF;
}
.todoListMain .header button {
  padding: 10px;
  font-size: 16px;
  margin: 10px;
  background-color: #0066FF;
  color: #FFF;
  border: 2px solid #0066FF;
}

.todoListMain .header button:hover {
  background-color: #003399;
  border: 2px solid #003399;
  cursor: pointer;
}

添加完上述代码后,预览一下应用。因为我们的 HTML 元素设置有对应的 className 值,所以 CSS 就起作用了,我们的示例在浏览器中就是如下的样子:

此时,我们的应用看起来很不错,但是还不能做什么。下一节我们开始让应用实际做点事情。

创建功能

Todo List 应用功能的实际实现并不难。我们先对它如何工作做个概述。最重要的数据是在文本框中输入的文本。每次输入一些文本,然后提交表单时,该文本就会显示在列表中以前你提交的文本之下。

这一切只需要利用 React 的状态功能就可以实现。在 state 对象内,我们有一个数组负责存储输入的文本:

每次这个 items 数组用你提交的新文本更新时,我们就用新提交的文本更新你看到的界面。剩下的工作只是围绕着设置事件和事件处理器,以确保提交表单,知道什么文本要添加到 items 数组。在如下的小节中,我们打算将你在这里看到的所有文字转换为 React 喜欢的 JavaScript 和 JSX。

初始化 state 对象

我们要做的第一件事情,是用负责存储所有提交了的文本的数组来初始化 state 对象。在 TodoList 组件中,添加如下高亮度行:

var TodoList = React.createClass({
  getInitialState: function() {
    return {
      items: []
    };
  },
  render: function() {
      return (
        <div className="todoListMain">
          <div className="header">
            <form>
              <input placeholder="enter task">
              </input>
              <button type="submit">add</button>
            </form>
          </div>
        </div>
      );
    }
});

这里我们所做的是,指定在组件渲染前要被调用的 getInitialState 生命周期方法。在该方法内,我们创建了一个 items 空数组。此后,在该组件内的任何地方,我们都可以通过 this.state.items 访问这个数组。

处理表单提交

当按下 Add 按钮或者在键盘上敲回车键后,我们将新条目添加到 “todo” 列表。这种行为通常是内置给 HTML,浏览器知道如何处理。我们不必为处理回车键盘或者监听 Add 按钮按下来写任何特殊代码,只需要考虑一件事情,即处理表单提交后发生了什么。

要这样做,我们监听表单元素上的 onSubmit 事件即可。该事件是在表单提交时触发,包括敲回车键或者捣鼓带有 type 属性为 submit 的任何元素。当表单被提交,并且该事件被监听到时,我们会需要调用一个事件处理器。我们给这个事件处理器命名为 addItem

把这些放在一起,在 TodoList 组件的 render 函数内,作出如下高亮度修改:

render: function() {
  return (
    <div className="todoListMain">
      <div className="header">
        <form onSubmit={this.addItem}>
          <input placeholder="enter task">
          </input>
          <button type="submit">add</button>
        </form>
      </div>
    </div>
  );
}

就像我们希望做的那样,我们只是把 form 元素的 onSubmit 事件链接到 addItem 事件处理器。这个事件处理器还不存在,所以我们添加如下高亮度行:

var TodoList = React.createClass({
  getInitialState: function() {
    return {
      items: []
    };
  },
  addItem: function(e) {

  },
  render: function() {
    return (
      <div className="todoListMain">
        <div className="header">
          <form onSubmit={this.addItem}>
            <input placeholder="enter task">
            </input>
            <button type="submit">add</button>
          </form>
        </div>
      </div>
    );
  }
});

现在 addItem 事件处理器/函数 还不能做什么,但是重要的是它已经存在了。下一步,我们就就让它能做点事情。

填充 state 对象

现在,TodoLilst 组件的 state 对象包含了 items 数组。我们要做的是用你在文本框中输入的文本填充这个数组。这意味着我们需要一种在 React 内访问 input 元素的方法。我们打算要做的方法是,在 input 元素上设置一个 ref 属性,并且将该引用存储到要生成的 HTML 元素上。

TodoList 组件的 render 方法内,添加如下行:

render: function() {
    return (
      <div className="todoListMain">
        <div className="header">
          <form onSubmit={this.addItem}>
            <input ref={(a) => this._inputElement = a} placeholder="enter task">
            </input>
            <button type="submit">add</button>
          </form>
        </div>
      </div>
    );
  }

在该组件挂载之后,这段高亮度代码立即运行,_inputElement 属性就会存储一个对生成的 input 元素的引用。这样,我们就可以将这个元素看作是在非 React 世界中用 querySelector 或者相似的函数找到的任何 DOM 元素。下一步我们就要填充 items 数组了。

继续,修改 addItem 方法,添加如下代码:

addItem: function(e) {
  var itemArray = this.state.items;

  itemArray.push(
    {
      text: this._inputElement.value,
      key: Date.now()
    }
  );

  this.setState({
    items: itemArray
  });

  this._inputElement.value = "";

  e.preventDefault();
}

这看起来跟很多我们刚添加的代码一样,但是我们这里所做的是,把我们早前明确的目标变成 JavaScript,即用输入框的文本填充 items 数组。我们更详细地来看看这段代码。

我们做的第一件事情是创建一个名为 itemArray 的数组,用来存储一个对 state 对象的 items 属性的引用:

var itemArray = this.state.items;

有了这个数组后,就把来自输入框的最近提交的文本添加到该数组:

itemArray.push(
  {
    text: this._inputElement.value,
    key: Date.now()
  }
);

注意,我们并非添加来自 input 元素的文本条目,而是添加一个由 textkey 属性组成的对象。text 属性存储 input 元素的文本值。key 属性存储当前时间。这听起来像要做古怪的事情,但是回忆一下在 《在 React 中从数据到 UI 的旅行》这一章,目标是让这个 key 值对于每个被提交的条目是唯一的。这是很重要的,因为我们将在这个数组中使用数据,最终生成一些 UI 元素。这个 key 值会被 React 用来唯一识别每个生成的 UI 元素,所以,通过用 Date.now() 生成 key,我们就可以确保某种程度的唯一性。因为这是一个很重要的细节,所以我们将会在稍后会重访所有这些。

不管怎样,回到正轨,处理了 itemArray 后,剩下的就是设置 state 对象的 items 属性为该数组:

this.setState({
  items: itemArray
});

到这里差不多完了!在这个方法中我们最后要做的一件事情如下:

e.preventDefault();

preventDefault 方法确保不会传播 onSubmit 事件超过此界限。我们这样做的原因有点难以理解,但是它确保:提交表单时所有想要做的是调用 addItem 方法。如果不中止事件传播,我们的应用将会在提交表单时会根据需要正确调用 addItem它还会触发浏览器的默认 POST 行为 - 这显然是我们不想要的。通过阻止 onSubmit 事件跑到此界限以外,我们就得到了我们想要的行为,即调用 addItem 方法,而没有不想要的副作用,比如不需要的会刷新页面的 POST 动作。

显示任务

到这里我们就快结束了。我们要做的最后一件事情是可视化当前 state 对象的 items 数组中的任务。包括创建一个全新的 TodoItems 组件,传递一些 props,使用 map 函数,和其它一些让人兴奋的事情:

不管怎样,我们要做的第一件事情就是定义 TodoItems 组件。在 TodoList 组件定义的代码上面,继续添加如下代码:

var TodoItems = React.createClass({
  render: function() {

  }
});

下一步,是在 TodoList 组件的 render 方法中调用该组件。不仅如此,我们还要指定一个 prop,将 TodoList 组件中包含 items 数组的 state 对象传递进来。这些事情做起来很简单,继续在 TodoList 组件的 render 方法中添加如下高亮度行:

render: function() {
  return (
    <div className="todoListMain">
      <div className="header">
        <form onSubmit={this.addItem}>
          <input ref={(a) => this._inputElement = a}
                 placeholder="enter task">
          </input>
          <button type="submit">add</button>
        </form>
      </div>
      <TodoItems entries={this.state.items}/>
    </div>
  );
}

这里我们实例化了 TodoItems 组件,并将 state 对象的 items 属性传递给该组件的 entries 属性。此时如果在浏览器中预览,什么都还看不到。TodoItems 组件准备要渲染,并且它可以访问提交的所有任务。唯一的问题是,它实际上什么事都没做,下面我们再校正。

回到 TodoItems 组件,我们要做的第一件事情就是创建一个新变量,来存储传递进来的任务数组。

var TodoItems = React.createClass({
  render: function() {
    var todoEntries = this.props.entries; 
  }
});

这里我们添加了一个 todoEntries 变量,它存储基于 TodoList 组件的 this.state.items 的值传递进来的 entries 属性的值。现在,todoEntries 变量存储了一个数组,该数组包含了一堆对象,每个对象存储了一个任务和一个 key。剩下的就是创建用来显示数据的 HTML 元素了。

首先,添加如下高亮度代码行来创建 li 元素:

var TodoItems = React.createClass({
  render: function() {
    var todoEntries = this.props.entries;

    function createTasks(item) {
      return <li key={item.key}>{item.text}</li>
    }

    var listItems = todoEntries.map(createTasks);
  }
});

这里我们用 map 函数遍历 todoEntries 中的每一个元素,并调用 createTasks 函数来为每个条目创建一个 li 元素:

function createTasks(item) {
  return <li key={item.key}>{item.text}</li>
}

重申我们早前的观点,因为这些列表元素是动态创建的,我们需要通过指定 key 属性并给每个条目一个唯一的值,来帮助 React 记录它们。在最初存储任务的时候,我们就已经解决了这部分问题:

itemArray.push(
  {
    text: this._inputElement.value,
    key: Date.now()
  }
);

因为前面的计划,我们现在是将 key 属性赋值为 todoEntries 数组中已包含的每个条目的 item.key。li 元素的可见内容只是 item.text 存储的文本值。如果使用它不需要额外的解释。让人耳目一新,对不对?

总体来说,li 元素的集合是全部由 listItems 变量/数据来处理和存储。此时剩下的就是把数组中的列表元素渲染在屏幕上。添加如下高亮度代码行:

var TodoItems = React.createClass({
  render: function() {
    var todoEntries = this.props.entries;

    function createTasks(item) {
      return <li key={item.key}>{item.text}</li>
    }

    var listItems = todoEntries.map(createTasks);

    return (
      <ul className="theList">
        {listItems}
      </ul>
    );
  }
});

现在我们正在做的是返回一个 ul 元素,该元素的内容是由 listItems 存储的 li 元素。在添加上述代码后,保存文档并预览。键入几个任务后,你会看到如下界面:

我们的应用跑起来了。每个提交的任务显示在列表条目上。深呼吸,放松一下。这是个很棒的进展,我们只剩下一点小事情。

添加收尾工作

到这里,我们已近完成了!首先,我们现在所有的与最开始的示例看起来并不是一样的。任务列表看起来有点普通,但是我们可以加点 CSS 解决。在样式块中,在已有的样式规则下添加如下样式规则:

.todoListMain .theList {
  list-style: none;
  padding-left: 0;
  width: 255px;
}

.todoListMain .theList li {
  color: #333;
  background-color: rgba(255,255,255,.5);
  padding: 15px;
  margin-bottom: 15px;
  border-radius: 5px;
}

如果现在预览一下,会看到输入的任务和我们想要的一模一样:

现在,你注意到没有:在输入框中输入的内容在表单提交后不会消失。每次在提交一个任务后,你必须手动清除输入框。这是很烦人的,但是解决它很简单。在 TodoList 组件的 addItem 方法中,添加如下高亮度行:

addItem: function(e) {
  var itemArray = this.state.items;

  itemArray.push(
    {
      text: this._inputElement.value,
      key: Date.now()
    }
  );

  this.setState({
    items: itemArray
  });

  this._inputElement.value = "";

  e.preventDefault();
}

这里我们在表单被提交以及 addItem 方法被调用时,清除 input 元素的 value 属性。

总结

Todo 应用的功能相当简单,但是通过从头开始创建它,我们几乎涵盖了 React 带来的每个有意思的细节。更重要的是,我们创建了一个示例,来展示我们分别学习的不同概念是如何配合的。这才是重要的细节。现在,我有一个简单的问题:本教程中我们已经做的一切都理解了吗?

如果本教程中我们做过的一切都理解了,那么你可以骄傲地告诉你的朋友和家人你差不多掌握 React 了!如果你发现还有困惑的地方,建议重读前面的内容。

相关文章