React学习(二)用React的方式思考

参考资料

React理念

React 最初的目的是使用 JavaScript 创建大型的,快速响应的网络应用。React的众多优点之一是它让你在编写代码的时候同时也在思考你的应用。我们通过一个简单的备忘录应用开发来理解react是怎样创建一个应用,并展示思考的过程

从模拟页面开始

TODOLIST

  • 页面中的数据模型为:
    {
    "color": "#ffffff",
    "text": "text",
    "time":"2017/2/3 11:20",
    "done": false
    }
    
  • 页面的功能为:
    • 支持添加新的备忘
    • 可切换备忘的“待做”和“已完成”状态,可设置颜色,可删除
    • 可根据颜色、时间对列表进行排序
    • 可分别查看“全部”、“待做”及“已完成”状态的备忘
    • 支持本地数据存储LocalStorage

把 UI 划分出组件层级

  • TodoApp: 整个应用的根组件,负责存储及处理应用的数据
  • Header:展示组件,表示应用的头部,包含应用的标题、新建备忘的内容输入框、时间输入框、对备忘录进行排序和过滤的操作栏
  • TodoList:展示组件,表示应用的列表部分
  • Memo:展示组件,表示单个备忘事项,由一个改变状态的复选框、改变备忘颜色的颜色选择器、备忘内容、备忘时间(及删除备忘按钮)组成
    TodoApp
  • UI划分的原则:组件的划分同样遵循单一功能原则。UI 和数据模型往往遵循着相同的信息架构,如果你的模型构建正确,你的 UI (以及你的组件结构)会被很好的映射。可以把UI划分成能准确表示你数据模型的一部分的组件就可以。

用 React 创建一个静态版本

  • 可以自顶向下或者自底向上构建应用。也就是,你可以从层级最高的组件开始构建(即 TodoApp开始)或层级最低的组件开始构建(Memo)。在较为简单的例子中,通常自顶向下更容易,而在较大的项目中,自底向上会更容易并且在你构建的时候有利于编写测试
  • 我们将交互与静态版本拆分来写,一方面,可以从一开始构建应用的大致层级结构;另一方面,交互往往涉及应用逻辑及数据流的思考
  • 在这步的最后,你会拥有一个用于呈现数据模型的可重用组件库。这些组件只会有 render() 方法。层级最高的组件会把数据模型作为 prop 传入。如果你改变你的基础数据模型并且再次调用 ReactDOM.render(), UI 会更新。这里可以看到React的单向数据流

定义 UI 状态的最小(但完整)表示/确定State的位置

  • 思考确定state的原则:
    • 它是通过 props 从父级传来的吗?如果是,他可能不是 state
    • 它随着时间推移不变吗?如果是,它可能不是 state
    • 你能够根据组件中任何其他的 state 或 props 把它计算出来吗?如果是,它不是 state
  • 确定state位置的原则:
    • 确定每一个需要这个 state 来渲染的组件
    • 找到一个公共所有者组件(一个在层级上高于所有其他需要这个 state 的组件的组件)
    • 这个公共所有者组件或另一个层级更高的组件应该拥有这个 state
    • 如果你没有找到可以拥有这个 state 的组件,创建一个仅用来保存状态的组件并把它加入比这个公共所有者组件层级更高的地方
  • 思考我们的TodoApp应用
    • Header中的输入框(文本、日期、时间)是可能改变,且不由props计算得到,因此我们为Header维护一个state
    • TodoList中的todo数组是从根应用传入的,且展示内容随着排序或者过滤内容可能发生改变,它不是state
    • Memo的是数据模型的最小结构,它展示的内容同样是由props决定的,不用props
    • TodoApp控制整个应用的状态和数据更新,它需要一个state维护应用的保持的数据项、排序条件、过滤条件

添加反向数据流

  • 在前面我们已经实现了单向数据流,我们需要通过底层的组件去触发根组件的状态改变以更新UI状态
  • Header->TodoApp:
    • 我们通过在添加按钮中调用回调函数,可以为添加列表项,并将数据更新到UI,并存储到本地
    • 将排序和过滤条件通过回调函数设置,可以重新排序和过滤列表项,更新展示
  • Memo->TodoList->TodoApp:
    • 将每个数据项中颜色,是否完成状态的改变通过回调函数重新设置,可以更新TodoApp的todos内容

一个完整的例子

  • TodoApp
    class TodoApp extends Component {
    constructor() {
      super()
      this.state = loadData() || {
        todos: [],
        filter: '全部',
        order: 'time',
        reverse: false
      }
    }
    render() {
      let filter = this.state.filter
      let items = sortArrayByProps(
        this.state.todos.filter(memo => {
          return filter === '全部'
            ? true
            : filter === '已完成'
            ? memo.done
            : !memo.done
        }),
        this.state.order,
        this.state.reverse
      )
      return (
        <div className="todo-app">
          <Header
            onClickAdd={this.handleAdd.bind(this)}
            onChangeOrder={this.handleChangeOrder.bind(this)}
            onChangeFilter={this.handleChangeFilter.bind(this)}
          />
          <TodoList
            title={filter}
            items={items}
            onToggleState={this.handleToggleState.bind(this)}
            onChangeColor={this.handleChangeColor.bind(this)}
            onDelete={this.handleDelete.bind(this)}
          />
        </div>
      )
    }
    handleAdd(text, date, time) {
      this.setState(
        {
          todos: this.state.todos.concat([
            {
              color: '#ffffff',
              text,
              time: date + ' ' + time,
              done: false
            }
          ])
        },
        () => saveData(this.state)
      )
    }
    handleToggleState(memo) {
      memo.done = !memo.done
      this.setState({}, () => saveData(this.state))
    }
    handleChangeColor(memo, color) {
      memo.color = color
      this.setState({}, () => saveData(this.state))
    }
    handleDelete(memo) {
      let todos = this.state.todos
      todos.splice(todos.indexOf(memo), 1)
      this.setState({todos: todos}, () => saveData(this.state))
    }
    handleChangeOrder(field) {
      this.setState({reverse: !this.state.reverse})
      this.setState({order: field}, () => saveData(this.state))
    }
    handleChangeFilter(filter) {
      this.setState({filter}, () => saveData(this.state))
    }
    }
    
  • Header
    class Header extends Component {
    constructor(props) {
      super(props)
      this.state = {
        value: '',
        date: '2000-01-01',
        time: '00:00'
      }
    }
    handleAdd() {
      this.props.onClickAdd(this.state.value, this.state.date, this.state.time);
    }
    handleTextChange(event) {
      this.setState({
        value: event.target.value
      })
    }
    handleDateChange(event) {
      this.setState({
        date: event.target.value
      })
    }
    handleTimeChange(event) {
      this.setState({
        time: event.target.value
      })
    }
    render() {
      return (
        <div className="header">
          <h3 className="title">备忘录</h3>
          <button
            type="button"
            disabled={!this.state.value}
            onClick={this.handleAdd.bind(this)}
          >添加</button>
          <div className="input-wrapper">
            <input
              value={this.state.value}
              type="text"
              name="text"
              onChange={this.handleTextChange.bind(this)}
            />
            <input
              type="date"
              className="date"
              name="date"
              value={this.state.date}
              onChange={this.handleDateChange.bind(this)}
            />
            <input
              type="time"
              className="time"
              name="time"
              value={this.state.time}
              onChange={this.handleTimeChange.bind(this)}
            />
          </div>
          <div className="bar">
            <strong>排序</strong>
            <span onClick={() => this.props.onChangeOrder('color')}>颜色</span>
            <span onClick={() => this.props.onChangeOrder('time')}>时间</span>
            <strong style={{marginLeft: 20}}>过滤</strong>
            <span onClick={() => this.props.onChangeFilter('全部')}>全部</span>
            <span onClick={() => this.props.onChangeFilter('待做')}>待做</span>
            <span onClick={() => this.props.onChangeFilter('已完成')}>已完成</span>
          </div>
        </div>
      )
    }
    }
    
  • TodoList
    const TodoList = props => {
    let todoList = props.items.map((memo, index) => (
      <Memo
        memo={memo}
        key={index}
        onToggleState={props.onToggleState}
        onChangeColor={props.onChangeColor}
        onDelete={props.onDelete}
      />
    ))
    return (
      <div className="todo-list-wrapper">
        <div>{`${props.title} ${props.items.length}`}</div>
        <div className="todo-list">{todoList}</div>
      </div>
    )
    }
    
  • Memo
    const Memo = props => {
    let memo = props.memo
    let classNames = 'todo-item' + (memo.done ? ' done' : '')
    return (
      <div className={classNames}>
        <input
          type="checkbox"
          checked={memo.done}
          onChange={() => props.onToggleState(memo)}
        />
        <input
          type="color"
          className="color"
          value={memo.color}
          onChange={e => props.onChangeColor(memo, e.target.value)}
        />
        <span className="text">{memo.text}</span>
        <span className="pull-right del" onClick={() => props.onDelete(memo)}>
          X
        </span>
        <a className="pull-right" href="void javascript 0">
          {new Date(memo.time).toLocaleString()}
        </a>
      </div>
    )
    }
    

 上一篇
React学习(三)更多特性 React学习(三)更多特性
props.children使代码更加优雅 当需要编写一个容器类组件,我们需要把内容传入容器,一个通用写法是class Container extends React.Component { render(){ return(
2019-02-12
下一篇 
React学习(一)基础语法 React学习(一)基础语法
开始 安装脚手架npx create-react-app <project-name> 目录 manifest.json 允许将站点添加至主屏幕(support) name: {string} 应用名称,用于安装横幅、启动画面
2019-02-10
  目录