美文网首页
实践React

实践React

作者: 珍珠林 | 来源:发表于2017-05-20 00:50 被阅读0次

    我们用React官方推荐的Thinking in React的方式来开发一个Deskmark记事本程序。这个程序左边是文章列表,右边是预览和编辑。


    • 将原型图分割成不同组件

    在原型图上画方块,命名。组件原则,一个组件理想情况下应该只做一件事。如果发现它有过多的功能,就可以分割成更多的子组件。然后就很容易地建立起项目结构。
    将所有的组件放到components文件夹下,每个组件对应一个子文件夹,组件命名统一采用index.jsx,样式文件命名统一采用style.css。

    components/
      Deskmark(整个程序的框架)/
        index.jsx
        style.css
      CreateBar(新建按钮)/
      List(左侧文章列表)/
      ListItem(左侧列表中的每个条目)/
      ItemEditor(右侧文章编辑器,包含保存和取消按钮)/
      ItemShowLayer(右侧文章展示,包含编辑和删除两个按钮)/
    

    • 创造无状态函数式组件

    ListItem就是典型的例子,它什么都不关心,只接收一个属性、展示一条文章列表。

    /*
     * @file component Item
     */
    // 当声明一个组件的时候,采用下面的顺序规则
    
    // 加载依赖
    import React, { PropTypes } from 'react';
    
    // 属性验证
    const propTypes = {
      item: PropTypes.object.isRequired,
      onClick: PropTypes.func.isRequired,
    };
    
    // 组件主体,这里是stateless function,所以直接就是一个函数
    function ListItem({ item }) {
      // 返回JSX结构
      return (
        <a href="#" className="list-group-item item-component">
          <span className="label label-default label-pill pull-xs-right">{item.time}</span>
          {item.title}
        </a>
      );
    }
    
    // 添加验证
    ListItem.propTypes = propTypes;
    
    // 导出组件
    export default ListItem;
    

    同样,List组件也是无状态组件,它只是根据传入的数组展示列表而已,就像是组件组件,将ListItem组件循环输出:

    import ListItem from '../ListItem';
    ...
    function List({ items }) {
      // 循环插入子组件
      items = items.map(
        item => (
          <ListItem item={item} key={item.id} />
        )
      );
    
      return (
        <div className="list-component col-md-4 list-group">
          {items}
        </div>
      );
    }
    

    在循环展示子组件时,必需为每个自组件指定key值,可以保证重新渲染的效率,提高内部Diff算法的效率。

    左边的组件已经完成,再来创建右边的组件。右边有ItemShowLayer.jsx和ItemEditor.jsx两个组件。
    ItemShowLayer也没什么特殊,只是展示文章标题和内容。只不过要显示的是Markdown转换后的内容,所以需要装一个库来将Markdown格式转化为HTML文档格式:

    npm install marked --save
    
    // ItemShowLayer.jsx
    import marked from 'marked';
    ...
    function ItemShowLayer({ item }) {
      // 如果没有传入Item,直接返回一些静态的提示
      if(!item || !item.id) {
        return (
          <div className="col-md-8 item-show-layer-component">
            <div className="no-select">请选择左侧列表里面的文章</div>
          </div>
        );
      }
      // 将Markdown转换成HTML
      // 注意在渲染HTML代码时使用了描述过的JSX转义写法dangerouslySetInnerHTML
      let content = marked(item.content);
      return (
        <div className="col-md-8 item-show-layer-component">
          <div className="control-area">
            <button className="btn btn-primary">编辑</button>
            <button className="btn btn-danger">删除</button>
          </div>
          <h2>{item.title}</h2>
          <div className="item-text">
            <div dangerouslySetInnerHTML={{__html: content}} />
          </div>
        </div>
      );
    }
    

    现在,完成的组件还没有添加任何交互,所以上面的编辑和删除两个按钮只是先放在那里,没有触发任何事件。
    剩下的无状态组件就不一一写了,可以自己写一下ItemEditor的实现,只不过是一个input框和一个textarea而已。


    • 组合无状态组件
      新建一个Deskmark组件,作为整个程序的框架,利用一些数据把组件都展示出来,暂时不做任何交互。先不添加组件内部的state,因为交互可以改变组件的state,导致UI的重新渲染。
    // Deskmark.jsx
    render() {
      const items = [
        {
          "id": "6c84fb90-12c4-11e1-840d-7b25c5ee775a",
          "title": "Hello",
          "content": "# testing markdown",
          "time": 1458030208359
        }, {
          "id": "6c84fb90-12c4-11e1-840d-7b25c5ee775b",
          "title": "Hello2",
          "content": "# Hello world",
          "time": 1458030208359
        }
      ];
      
      return (
        <section className="deskmark-component">
          <div className="container">
            <div className="row">
              <CreateBar />
              <List items={items} />
            </div>
          </div>
        </section>
      );
    }
    

    右边是文章展示区,也可以切换成一个编辑器。暂且把这两个组件都添加到右边。

    ...
    return (
      const currentItem = items[0];
      <section className="deskmark-component">
        <div className="container">
          <div className="row">
            <CreateBar />
            <List items={items} />
            <ItemEditor item={currentItem} />
            <ItemShowLayer item={currentItem} />
          </div>
        </div>
      </section>
    );
    

    • 添加state的结构
        Deskmark组件是整个程序的框架,它控制了整个程序的状态。根据程序的静态版本思考一下,都需要什么状态来存储数据呢?state的设计原则是:尽量最简化,遵循DRY(Don't Repeat Yourself)的原则。
    • 需要一个数组来存储所有的文章。这一点没有异议,上面静态版本的组件其实已经采用这个结构来渲染组件。
    • 需要一个数据来展示已被选中的文章,并且展示在右边。最直观的方法是有一个对象保存展示的内容,就像这样{"id": "...", "title": "...", ...}。这样当然非常直观。那么再想想有没有更优解?选中的内容只是所有文章中的一项,其实不需要把这些数据全部复制下来,只需要保存一个索引,随时从文章列表中取出来就可以。这个索引就是每篇文章的ID,如此用一个selectId就可以表示当前选中的文章。
    • 还需要一个数据来表示编辑器状态,表示在编辑状态还是在浏览文章状态。那么狠容易想出用一个布尔值来表达:editing。

    经过这样的思考,不难得出整个程序的最后状态如下:

    this.state = {
      items: [],
      selected: null,
      editing: false
    }
    

    • 组件交互设计
        现在,静态组件和程序的state都已经确定,是时候添加交互了。根据原型图和组件传入的回调总结出的交互如下。
    • 文章的CRUD操作。1.创建文章(createItem),2.删除文章(deleteItem),3.更新文章(updateItem),4.选择文章(selectItem)。
    • 右侧状态栏切换。1.切换到编辑器状态(editItem),2.切换到文章展示状态(cancelEdit)

    现在把这些组件的交互操作都添加到Deskmark里

    // 安装一个用来生成uuid库
    npm install uuid --save
    
    import uuid from 'uuid';
    export default class Deskmark extends React.Component {
      ...
      constructor(props) {
        super(props);
        this.state = {
          items: [],
          selectId: null,
          editing: false
        };
      }
      saveItem(item) {
        // item是编辑器返回的对象,里面应该包括标题和内容
        // 当前的items state
        let items = this.state.items;
        item.id = uuid.v4();
        item.time = new Date().getTime();
        // 新的state
        items = [..items, item];
        // 更新新的state
        this.setState({
          items: items
        });
      }
    }
    

    需要注意的一点是,在构造函数中需要bind新建的方法,否则这个方法无法在render中使用。

    constructor(props) {
      ...
      this.saveItem = this.saveItem.bind(this);
    }
    

    这样就完成了第一个新增文章的方法,其实就是在state的items这个数组中添加一项。举一反三,其他的方法也就不难写了,这些方法只不过是各种各样对状态的操作:

    ...
    // 从左侧列表选择一篇文章
    // 将selectId置为选择文章的ID,并且将editing状态置为false
    selectItem(id) {
      if(id === this.state.selectedId) {
        return;
      }
      
      this.setState({
        selectedId: id,
        editing: false
      });
    }
    // 新建一篇文章
    createItem() {
      // 将editing状态置为true,并且selectedId为null,表示要创建一篇新的文章
      this.setState({
        selectedId: null,
        editing: true
      });
    }
    

    • 组合为最终版本
        现在已经有了静态组件,有了state,还添加了一系列交互操作。下面只要把交互操作和静态组件组合在一起就可以了。现在要做的就是将这些回调一一传入各个组件中,将JSX中的事件交互和这些回调联系起来。
    // Deskmark/index.jsx
    export default class Deskmark extends React.Component {
      ...
      render() {
        let { items, selectId, editing } = this.state;
        // 选出当前被选中的文章
        let selected = selectedId && items.find(item => item.id === selectedId);
        
        // 根据editing状态来决定是要显示ItemEditing组件还是ItemShowLayer组件,并且将回调方法都传入组件中
        let mainPart = editing
          ? <ItemEditor
               item={selected}
               onSave={this.saveItem}
               onCancel={this.cancelEdit}
             />
           : <ItemShowLayer
               item={selected}
               onEdit={this.editItem}
               onDelete={this.deleteItem}
             />;
        
        // 将交互回调添加到组建中
        return (
          <section className="deskmark-component">
            <div className="container">
              <CreateBar onClick={this.createIem} />
              <List items={this.state.items} onSelect={this.selectItem} />
              {mainPart}
            </div>
          </section>
        );
      }
      ...
    }
    

    回调已经传入组件,那么再来一一改造静态组件,让它们变得交互起来:

    // ItemShowLayer
    ...
    // 不要忘记把传入的回调加入到属性验证中
    const propTypes = {
      item: PropTypes.object,
      onEdit: PropTypes.func.isRequired,
      onDelete: PropTypes.func.isRequired,
    };
    
    function ItemShowLayer({ item, onEdit, onDelete }) {
      ...
      const content = marked(item.content);
      return (
        <div className="col-md-8 item-show-layer-component">
          <div className="control-area">
            <button onClick="{() => onEdit(item.id)}" className="btn btn-primary">编辑</button>
            <button onClick="{() => onDelete(item.id)}" className="btn btn-danger">删除</button>
          </div>
          <h2>{item.title}</h2>
          <div className="item-text">
            <div dangerouslySetInnerHTML={{ __html: content }} />
          </div>
        </div>
      );
      ...
    }
    

    再来改造一个稍微复杂一点的ItemEditor

    import React, { PropTypes } from 'react';
    
    const propTypes = {
      item: PropTypes.object,
      onSave: PropTypes.func.isRequired,
      onCancel: PropTypes.func.isRequired,
    };
    
    class ItemEditor extends React.Component {
      render() {
        const { onSave, onCancel } = this.props;
    
        const item = this.props.item || {
          title: '',
          content: '',
        };
        // 判断是否已经选择了selectId,渲染按钮不同的文本
        // let save = () => {
          onSave({
            ...item,
            // this.refs可以获取真实的DOM节点,从而取得value
            title: this.refs.title.value,
            content: this.refs.content.value,
          });
        };
        
        return (
          <div className="col-md-8 item-editor-component">
            <div className="control-area">
              <button onClick={save} className="btn btn-success">{saveText}</button>
              <button onClick={onCancel} className="btn secondary">取消</button>
            </div>
            <div className="edit-area">
              <input ref="title" placeholder="请填写标题" defaultValue={item.title} />
              <textarea ref="content" placeholder="请填写内容" defaultValue={item.content} />
            </div>
          </div>
        );
      }
    }
    
    ItemEditor.propTypes = propTypes;
    
    export default ItemEditor;
    

    到此完成了React的第一个项目,回顾下步骤:

    1. 先画出程序的Mockup图
    2. 将Mockup图划分成不同的组件
    3. 实现静态版本的程序和组件
    4. 将静态版本组合起来
    5. 考虑state的组成和实现
    6. 添加交互方法
    7. 将这些组合在一起,完成最终的版本

    相关文章

      网友评论

          本文标题:实践React

          本文链接:https://www.haomeiwen.com/subject/lgsuxxtx.html