实践React

我们用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. 将这些组合在一起,完成最终的版本
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 175,843评论 25 709
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,777评论 19 139
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 14,302评论 4 61
  • 17种 k线图形态 一、出水芙蓉 形态特征: 1、一根大阳线上穿三条均线,均线多头排列 2、成交量需放大 二、单针...
    捉牛股阅读 4,884评论 0 0
  • 如果拿战争作比喻,就很容易理解“兵力强大”的作用。假设每个士兵的射中率是一样的,几个来回下来,从数学的角度,人数多...
    杨蓉Dorothy阅读 11,230评论 0 0