实践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 (
    
      {item.time}
      {item.title}
    
  );
}

// 添加验证
ListItem.propTypes = propTypes;

// 导出组件
export default ListItem;

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

import ListItem from '../ListItem';
...
function List({ items }) {
  // 循环插入子组件
  items = items.map(
    item => (
      
    )
  );

  return (
    
{items}
); }

在循环展示子组件时,必需为每个自组件指定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 (
      
请选择左侧列表里面的文章
); } // 将Markdown转换成HTML // 注意在渲染HTML代码时使用了描述过的JSX转义写法dangerouslySetInnerHTML let content = marked(item.content); return (

{item.title}

); }

现在,完成的组件还没有添加任何交互,所以上面的编辑和删除两个按钮只是先放在那里,没有触发任何事件。
剩下的无状态组件就不一一写了,可以自己写一下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 (
    
); }

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

...
return (
  const currentItem = items[0];
  
);

  • 添加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
      ? 
       : ;
    
    // 将交互回调添加到组建中
    return (
      
{mainPart}
); } ... }

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

// 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 (
    

{item.title}

); ... }

再来改造一个稍微复杂一点的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 (