实践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. 将这些组合在一起,完成最终的版本
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

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