Webpack&React (五) 实现一个简单的便签应用

原文链接
因为我们以经有了一个好的开发环境,所以我们可能真正的做一些东西了.我们的目标是做一个简单的便签应用.它会有一些基本操作.我们会从头开始写并会遇到一些麻烦,之所以这样做是我了让你了解为什么诸如Flux,等架构是需要的.

初始化数据模型

通常开始设计应用程序的好的方式是从数据开始的.我们将有一个模型列表像下面这样:

[
  {
    id: '4a068c42-75b2-4ae2-bd0d-284b4abbb8f0',
    task: 'Learn Webpack'
  },
  {
    id: '4e81fc6e-bfb6-419b-93e5-0242fb6f3f6a',
    task: 'Learn React'
  },
  {
    id: '11bbffc8-5891-4b45-b9ea-5c99aadf870f',
    task: 'Do laundry'
  }
];

每个便签对象都包含我们需要的数据,包括id和我们要执行的task.最后将扩展数据定义包含便签的颜色和它的作者.

App中使用数据

设置App

现在我们知道如何处理id并知道我们需要何种数据类型.我们需要连接数据模型到App.一种简单的方式是直接加入数据到render()中.这不是有效率的,但是可以让我们快速开始.下面看在React怎么实现它:

app/components/App.jsx

leanpub-start-insert
import uuid from 'node-uuid';
leanpub-end-insert
import React from 'react';
leanpub-start-delete
import Note from './Note.jsx';
leanpub-end-delete

export default class App extends React.Component {
  render() {
leanpub-start-insert
    const notes = [
      {
        id: uuid.v4(),
        task: 'Learn Webpack'
      },
      {
        id: uuid.v4(),
        task: 'Learn React'
      },
      {
        id: uuid.v4(),
        task: 'Do laundry'
      }
    ];
leanpub-end-insert

leanpub-start-delete
    return <Note />;
leanpub-end-delete
leanpub-start-insert
    return (
      <div>
        <ul>{notes.map(note =>
          <li key={note.id}>{note.task}</li>
        )}</ul>
      </div>
    );
leanpub-end-insert
  }
}

在上面我们使用了React许多特性.下面是对它的解:

  • <ul>{notes.map(note => ...}</ul> - {}让我们混入JavaScript语法到JSX中.map反回一个列表li给React进行渲染.
  • <li key={note.id}>{note.task}</li> - 为了让React有序的展现元素,我们使用key属性,这个属性是重要的且key值是唯一的否则React不能找出正确的方式展现它.如果没有设置这个属性.React将会给出一个警告,更多信息看Multiple Components

如果你运行了应用.你能看到这些便签.虽然不好看,但我们已经迈出了第一步.
这里写图片描述

增加新条目到列表

增加更多条目到列表是我们下一步开发的好的开始.每个React组件可以维护内部的状态state.在这个例子中state指的是我们刚刚定义的数据模型.我们通过React的setState修改这个状态.React最终会调用render()方法并更新用户界面.这让我们可以实现比如增加新项目的交互.

随着你程序的发展.你需要小心的处理这个状态.这就是为什么专门开发对状态管理的解决方案.它们是你专注于React组件的开发而不用担心状态的问题.

组件可能保持一些自已的状态.举例子是由状态相关的用户界面.想一下下拉列组件可能想控制它自已的可见状态.我们将讨论状态管理以更好的开发这个应用.

如果我们使用基于类的方式定义组件.我们可以在构造函数construcotr中初始化状态.它是一个特定的方法当实例初始化时会被调用.在这个例子中我们能加入初始数据和组件state在它里面.

app/components/App.jsx

...

export default class App extends React.Component {
leanpub-start-insert
  constructor(props) {
    super(props);

    this.state = {
      notes: [
        {
          id: uuid.v4(),
          task: 'Learn Webpack'
        },
        {
          id: uuid.v4(),
          task: 'Learn React'
        },
        {
          id: uuid.v4(),
          task: 'Do laundry'
        }
      ]
    };
  }
leanpub-end-insert
  render() {
leanpub-start-delete
    const notes = [
      {
        id: uuid.v4(),
        task: 'Learn Webpack'
      },
      {
        id: uuid.v4(),
        task: 'Learn React'
      },
      {
        id: uuid.v4(),
        task: 'Do laundry'
      }
    ];
leanpub-end-delete
leanpub-start-insert
    const notes = this.state.notes;
leanpub-end-insert

    ...
  }
}

刷新浏览器后,会看到和之前相同的结果.我们现在可以使用setState改变状态.

定义addNote处理程序

现在我们可以通过自定义方法来修改状态.在App中简单的增加一个按钮,通过点击它来增加一个新的条目到组件状态.如前面所说的.React会得到这个改变并刷新用户界面.

app/components/App.jsx

...

export default class App extends React.Component {
  constructor(props) {
    ...
  }
  render() {
    const notes = this.state.notes;

    return (
      <div>
leanpub-start-insert
        <button onClick={this.addNote}>+</button>
leanpub-end-insert
        <ul>{notes.map(note =>
          <li key={note.id}>{note.task}</li>
        )}</ul>
      </div>
    );
  }
leanpub-start-insert
  // We are using an experimental feature known as property
  // initializer here. It allows us to bind the method `this`
  // to point at our *App* instance.
  //
  // Alternatively we could `bind` at `constructor` using
  // a line, such as this.addNote = this.addNote.bind(this);
  addNote = () => {
    // It would be possible to write this in an imperative style.
    // I.e., through `this.state.notes.push` and then
    // `this.setState({notes: this.state.notes})` to commit.
    //
    // I tend to favor functional style whenever that makes sense.
    // Even though it might take more code sometimes, I feel
    // the benefits (easy to reason about, no side effects)
    // more than make up for it.
    //
    // Libraries, such as Immutable.js, go a notch further.
    this.setState({
      notes: this.state.notes.concat([{
        id: uuid.v4(),
        task: 'New task'
      }])
    });
  };
leanpub-end-insert
}

如果可以与后台交互,我们能触发一个操作并获取这个反回值.

现在刷新浏览器点击按钮你应该看到加入了一个新项目到列表:
这里写图片描述

我们现在还缺少两个重要的功能: 修改和删除.在这之前,是扩展我们组件层级的好时机.这会为我们以后增加新功能带来便利.使用React常常像这样的,开发一个组件一段时间后,你会拆分这个组件.

this.setState可设置第二个参数像这样:this.setState({...}, () => console.log('set state!')),这会在setState正确完成后,通知到你的一个回调函数.
也可以使用[...this.state.notes, {id: uuid.v4(), task: 'New task'}]达到同样的效果.
autobind-decorator可以为我们自动绑定类或方法到当前对象.这样我们就不用写()=>{}箭头表达式来附加this

改进组件层级

我们现在,基于一个组件的开发,要增加一个便签集合到它里面是复杂的.并会产生许多重复代码.

幸运的是.我们可以通过建立更多的组件模型来解决这个问题.并可以提高代码复用.理想情况下.我们可以在多个不同程序中共享我们的组件.

便签集合如感觉像是一个组件.我们建模它为Notes,我们还可以从中分离出Note概念. 这样的设置让我们得到三个层级如:

  • App - App 保存应用程序的状态并处理上层逻辑.
  • Notes - Notes是一个中间件.并负责渲染Note组件.
  • Note - Note 是我们应用程序的主力,编辑和删除会在这里触发,这些操作将级联到App反应给用户界面.

提取Note

第一步我们将提取Note,Note组件将需要接收task作为prop,并渲染它.在JSX中看起来像<Note task="task goes here" />.

除了state,props是另一个你将大量使用的概念.它描述了一个组件的外部接口.

一个基于函数的组件,将接收props作为它的第一个参数.我们能提取特定的属性通过ES6 destructuring syntax, 基于函数的组件是render()它本身. 与基于类的组件相比限制很多,但对于简单的展现是适合的,比如这个.

app/components/Note.jsx

import React from 'react';

export default ({task}) => <div>{task}</div>;

连接Note和App

现在我们有一个接收task属性的简单组件.为了更接近我们理想的层级结构,我们在App中连接它.

app/components/App.jsx

import uuid from 'node-uuid';
import React from 'react';
leanpub-start-insert
import Note from './Note.jsx';
leanpub-end-insert

export default class App extends React.Component {
  constructor(props) {
    ...
  }
  render() {
    const notes = this.state.notes;

    return (
      <div>
        <button onClick={this.addNote}>+</button>
        <ul>{notes.map(note =>
leanpub-start-delete
          <li key={note.id}>{note.task}</li>
leanpub-end-delete
leanpub-start-insert
          <li key={note.id}>
            <Note task={note.task} />
          </li>
leanpub-end-insert
        )}</ul>
      </div>
    );
  }
  ...
}

应用程序看起来还是与之前一样.为了达到我们追求的结构.我们应当做更多的调整继续提取Notes.

提取Notes

提取Notes是简单的.它是属于App的一个部分.它与Note有点儿像.如:

app/components/Notes.jsx

import React from 'react';
import Note from './Note.jsx';

export default ({notes}) => {
  return (
    <ul>{notes.map(note =>
      <li key={note.id}>
        <Note task={note.task} />
      </li>
    )}</ul>
  );
}

此外.我们需要在我们的App中使用这个新定义的组定.

app/components/App.jsx

import uuid from 'node-uuid';
import React from 'react';
leanpub-start-delete
import Note from './Note.jsx';
leanpub-end-delete
leanpub-start-insert
import Notes from './Notes.jsx';
leanpub-end-insert

export default class App extends React.Component {
  constructor(props) {
    ...
  }
  render() {
    const notes = this.state.notes;

    return (
      <div>
        <button onClick={this.addNote}>+</button>
leanpub-start-delete
        <ul>{notes.map(note =>
          <li key={note.id}>
            <Note task={note.task} />
          </li>
        )}</ul>
leanpub-end-delete
leanpub-start-insert
        <Notes notes={notes} />
leanpub-end-insert
      </div>
    );
  }
  addNote = () => {
    this.setState({
      notes: this.state.notes.concat([{
        id: uuid.v4(),
        task: 'New task'
      }])
    });
  };
}

运行结果仍与之前相同.但结构要比之前好很多.现在我们可以继续往应用程序中增加新功能了.

编辑Notes

为了可以编辑Notes,我们应该设置一些钩子,理论上应该发生接下来的事.

  1. 用户点击一个Note.
  2. Note自身展示为一个输入框.并显示当前值.
  3. 用户确让修改(触发blur事件或按下回车).
  4. Note渲染这个新值.

意思是Note需要追踪它的editing状态.此外.当(task)改变时我们需要传递这个值,App需要更新它的状态.因此我们需要一些函数.

跟踪Note editing状态

正如早期的App,我发现在需要在Note中处理状态,所以基于函数的组件是不够的.我们需要改写它到基于类的.我们会跟据用户的行为改变editing状态,最后渲染它.请看如下代码:

app/components/Note.jsx

import React from 'react';

export default class Note extends React.Component {
  constructor(props) {
    super(props);

    // Track `editing` state.
    this.state = {
      editing: false
    };
  }
  render() {
    // Render the component differently based on state.
    if(this.state.editing) {
      return this.renderEdit();
    }

    return this.renderNote();
  }
  renderEdit = () => {
    // We deal with blur and input handlers here. These map to DOM events.
    // We also set selection to input end using a callback at a ref.
    // It gets triggered after the component is mounted.
    //
    // We could also use a string reference (i.e., `ref="input") and
    // then refer to the element in question later in the code. This
    // would allow us to use the underlying DOM API through
    // this.refs.input. This can be useful when combined with
    // React lifecycle hooks.
    return <input type="text"
      ref={
        (e) => e ? e.selectionStart = this.props.task.length : null
      }
      autoFocus={true}
      defaultValue={this.props.task}
      onBlur={this.finishEdit}
      onKeyPress={this.checkEnter} />;
  };
  renderNote = () => {
    // If the user clicks a normal note, trigger editing logic.
    return <div onClick={this.edit}>{this.props.task}</div>;
  };
  edit = () => {
    // Enter edit mode.
    this.setState({
      editing: true
    });
  };
  checkEnter = (e) => {
    // The user hit *enter*, let's finish up.
    if(e.key === 'Enter') {
      this.finishEdit(e);
    }
  };
  finishEdit = (e) => {
    // `Note` will trigger an optional `onEdit` callback once it
    // has a new value. We will use this to communicate the change to
    // `App`.
    //
    // A smarter way to deal with the default value would be to set
    // it through `defaultProps`.
    //
    // See the *Typing with React* chapter for more information.
    const value = e.target.value;

    if(this.props.onEdit) {
      this.props.onEdit(value);

      // Exit edit mode.
      this.setState({
        editing: false
      });
    }
  };
}

如果你现在尝试编辑Note,会看到一个输入框并可以输入.因为我们还没有写onEdit函数.所以不能保存我们的输入.下一步我们要获取我们的输入值并且更新App状态,让它完整的工作.

建议命名回调函数时使用on前缀.可以与其它属性区分开并使你的代码变得更整洁

Note状态改变的传播

考虑到我们当前的页务逻辑是在App中, 我们同样能处理onEdit在那里.另一种选择是可以加入这个逻辑到Notes级别,这样做使得addNote存在问题,因为这个功能不属于Notes范围.因此我们在App级别管理应用状态.

为了使onEdit工作,我们要得到他的输出和包装这个结果到App.此外我们需要知道到底哪一个Note被修改了好做相应的改变.这能通过数据绑定实现.看下图所示:
这里写图片描述

onEdit定义在App级别.我们需要通过Notes传递onEdit.所以我们需要修改两个文件中的代码.如下所示:

app/components/App.jsx

import uuid from 'node-uuid';
import React from 'react';
import Notes from './Notes.jsx';

export default class App extends React.Component {
  constructor(props) {
    ...
  }
  render() {
    const notes = this.state.notes;

    return (
      <div>
        <button onClick={this.addNote}>+</button>
leanpub-start-delete
        <Notes notes={notes} />
leanpub-end-delete
leanpub-start-insert
        <Notes notes={notes} onEdit={this.editNote} />
leanpub-end-insert
      </div>
    );
  }
  addNote = () => {
    ...
  };
leanpub-start-insert
  editNote = (id, task) => {
    // Don't modify if trying set an empty value
    if(!task.trim()) {
      return;
    }

    const notes = this.state.notes.map(note => {
      if(note.id === id && task) {
        note.task = task;
      }

      return note;
    });

    this.setState({notes});
  };
leanpub-end-insert
}

要使其工作.我们仍需要修改Notes.它需要bind相应的便签id,可以通过在bind第一个参数后面添加绑定的值.当触发回调时,这些值会被附加到回调函数的参数中.

app/components/Notes.jsx

import React from 'react';
import Note from './Note.jsx';

export default ({notes, onEdit}) => {
  return (
    <ul>{notes.map(note =>
      <li key={note.id}>
        <Note
          task={note.task}
          onEdit={onEdit.bind(null, note.id)} />
      </li>
    )}</ul>
  );
}

现在你可以刷新并编辑Note了.

当前的设计不是完美的.如何让新创建的便签直接可以编辑?考虑到Note封装state,我们没有简单的方法从外部访问它,这种情况我们会在接下来的章节中实现.
这里写图片描述

删除节点

我们还缺少一个重要的功能,可以让我们能删除便签.我们会为每个节点增加一个按钮并使用它实现这个功能.

开始.我们需要在App中定义一些逻辑.删除节点可以通过查找一个Note基于它的id.当删除后.排除这个Note并修改我们的状态.

和之前一样.它会有三个改变.在App中定义逻辑,在Notes中绑定id,最后触发逻辑在Note中. 我们使用filter在删除逻辑:

app/components/App.jsx

import uuid from 'node-uuid';
import React from 'react';
import Notes from './Notes.jsx';

export default class App extends React.Component {
  ...
  render() {
    const notes = this.state.notes;

    return (
      <div>
        <button onClick={this.addNote}>+</button>
leanpub-start-delete
        <Notes notes={notes} onEdit={this.editNote} />
leanpub-end-delete
leanpub-start-insert
        <Notes notes={notes}
          onEdit={this.editNote}
          onDelete={this.deleteNote} />
leanpub-end-insert
      </div>
    );
  }
leanpub-start-insert
  deleteNote = (id) => {
    this.setState({
      notes: this.state.notes.filter(note => note.id !== id)
    });
  };
leanpub-end-insert
  ...
}

Notes修改为如下:

app/components/Notes.jsx

import React from 'react';
import Note from './Note.jsx';

leanpub-start-delete
export default ({notes, onEdit}) => {
leanpub-end-delete
leanpub-start-insert
export default ({notes, onEdit, onDelete}) => {
leanpub-end-insert
  return (
    <ul>{notes.map(note =>
      <li key={note.id}>
leanpub-start-delete
        <Note
          task={note.task}
          onEdit={onEdit.bind(null, note.id)} />
leanpub-end-delete
leanpub-start-insert
        <Note
          task={note.task}
          onEdit={onEdit.bind(null, note.id)}
          onDelete={onDelete.bind(null, note.id)} />
leanpub-end-insert
      </li>
    )}</ul>
  );
}

最后在Note中增加删作按钮.并写触发函数onDelete:
app/components/Note.jsx

...

export default class Note extends React.Component {
  ...
  renderNote = () => {
    // If the user clicks a normal note, trigger editing logic.
leanpub-start-delete
    return <div onClick={this.edit}>{this.props.task}</div>;
leanpub-end-delete
leanpub-start-insert
    const onDelete = this.props.onDelete;

    return (
      <div onClick={this.edit}>
        <span>{this.props.task}</span>
        {onDelete ? this.renderDelete() : null }
      </div>
    );
leanpub-end-insert
  };
leanpub-start-insert
  renderDelete = () => {
    return <button onClick={this.props.onDelete}>x</button>;
  };
leanpub-end-insert
  ...
}

最后刷新浏览器点击删除按钮看一下效果.
这里写图片描述

你可能需要需要刷新浏览器.使用 CTRL/CMD-R 组合键.

附加样式

我们现在的应用程序是不漂亮的.我们可以基于类定义的方式写一些CSS并附加到我们的组件上. 在Styling React章我们会讨论其它更好的方式.

附加类到组件

app/components/App.jsx

import uuid from 'node-uuid';
import React from 'react';
import Notes from './Notes.jsx';

export default class App extends React.Component {
  ...
  render() {
    const notes = this.state.notes;

    return (
      <div>
leanpub-start-delete
        <button onClick={this.addNote}>+</button>
leanpub-end-delete
leanpub-start-insert
        <button className="add-note" onClick={this.addNote}>+</button>
leanpub-end-insert
        <Notes notes={notes}
          onEdit={this.editNote}
          onDelete={this.deleteNote} />
      </div>
    );
  }
  ...
}

app/components/Notes.jsx

import React from 'react';
import Note from './Note.jsx';

export default ({notes, onEdit, onDelete}) => {
  return (
leanpub-start-delete
    <ul>{notes.map(note =>
leanpub-end-delete
leanpub-start-insert
    <ul className="notes">{notes.map(note =>
leanpub-end-insert
leanpub-start-delete
      <li key={note.id}>
leanpub-end-delete
leanpub-start-insert
      <li className="note" key={note.id}>
leanpub-end-insert
        <Note
          task={note.task}
          onEdit={onEdit.bind(null, note.id)}
          onDelete={onDelete.bind(null, note.id)} />
      </li>
    )}</ul>
  );
}

app/components/Note.jsx

import React from 'react';

export default class Note extends React.Component {
  ...
  renderNote = () => {
    const onDelete = this.props.onDelete;

    return (
      <div onClick={this.edit}>
leanpub-start-delete
        <span>{this.props.task}</span>
leanpub-end-delete
leanpub-start-insert
        <span className="task">{this.props.task}</span>
leanpub-end-insert
        {onDelete ? this.renderDelete() : null }
      </div>
    );
  };
  renderDelete = () => {
leanpub-start-delete
    return <button onClick={this.props.onDelete}>x</button>;
leanpub-end-delete
leanpub-start-insert
    return <button
      className="delete-note"
      onClick={this.props.onDelete}>x</button>;
leanpub-end-insert
  };
  ...
}

CSS组件

第一步是为了摆脱可怕的serif 字体.

app/main.css

body {
  background: cornsilk;
  font-family: sans-serif;
}

下一步是使Notes摆脱列表前的圆点儿.

app/main.css

.add-note {
  background-color: #fdfdfd;
  border: 1px solid #ccc;
}

.notes {
  margin: 0.5em;
  padding-left: 0;

  max-width: 10em;
  list-style: none;
}

这里写图片描述

为了使个别的Notes突出我们可以使用:

app/main.css

.note {
  margin-bottom: 0.5em;
  padding: 0.5em;

  background-color: #fdfdfd;
  box-shadow: 0 0 0.3em 0.03em rgba(0, 0, 0, 0.3);
}
.note:hover {
  box-shadow: 0 0 0.3em 0.03em rgba(0, 0, 0, 0.7);

  transition: 0.6s;
}

.note .task {
  /* force to use inline-block so that it gets minimum height */
  display: inline-block;
}

在交互过程中加入了动画显示阴影效果.这可以让用户知道鼠标具体在哪个便签上面.这个效果在基于触摸的界面中不能工作.但在桌面应用中效果不错.

最后我们让鼠标在Note上时才显示删除按钮.其它时候隐藏它.同样在触摸时将失效.

app/main.css

.note .delete-note {
  float: right;

  padding: 0;

  background-color: #fdfdfd;
  border: none;

  cursor: pointer;

  visibility: hidden;
}
.note:hover .delete-note {
  visibility: visible;
}

这里写图片描述

理解React组件

理解propsstate是怎么工作很重要.组件的生命周期是另一个关键概念,虽然我们上面已经提及过它,但是还是需要更详细的理解.通过应用这些概念在您的应用程序,您可以实现React中的大多数任务.React支持下面这些生命周期钩子:

  • componentWillMount() 任何渲染中的组件将触发这个事件一次.一种方式使用它是异步加载数据并通过setState强制渲染组件.
  • componentDidMount() 渲染完成后触发.在这里能访问DOM.你可以使用它封装一个jQuery插件在组件内.
  • componentWillReceiveProps(object nextProps) 当组件接收新属性时触发.例如,基于你接收到的属性修改你的组件的状态.
  • shouldComponentUpdate(object nextProps, object nextState) 允许您优化渲染.如果你查看这个属性和状态并决定不需要更新,则反回false.
  • componentWillUpdate(object nextProps, object nextState)shouldComponentUpdate之后且render()前触发.在这里不能使用setState,但你能设置类属性.
  • componentDidUpdate()渲染之后触发.在这里能修改DOM.这里能使用其它代码库工作.
  • componentWillUnmount()仅在一个组件是从 DOM 中卸下前被触发,这是执行清理的理想场所(例如移除运行中的定时器,自定义的DOM等).

除了生命周期的钩子.如果你使用React.createClass你需要意识到其它的属性和方法.

  • displayName - 它是更可取的设置displayName方式.这将改善debug信息.对于ES6的类.这是自动从类名派生出的.
  • getInitialState() - 在基于类的方式通过constructor做相同的事情.
  • getDefaultProps() - 在基于类的定义中可以在constructor设置.
  • mixins - mixins包含一个数组应用于组件中.
  • statics - statics包含了组件中的静态属性和方法.

React组件约定

我更喜欢先写constructor,其次生命周期事件,render(),最后是render()使用的方法.我喜欢这种自上而下的方式.

最后,你会找到适合你的习惯和工作方式.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值