从Tutorial入门——react官网评论框练习总结

作为一个新手,绝不好高骛远。
而踏踏实实的一个重要表现,就是走一走要扭头看一看。
通过这个评论框练习,差不多结束入门阶段,特此总结。
注:此为融汇阶段,需要一定基础。
声明:转载请注明出处
我的一些话:并不是什么高手,有什么不完备的地方,请尽情轰炸我,蹂躏我吧!
文章相关——偏基础,偏练手性质,高手直接略过吧,不然是浪费时间呢

[TOC]——简书还没有目录么???

分析下这个小小的评论框

大概长这样

接下来我们来分析一下如何来做

抽象组件

整个评论系统可以看做一个大组件;评论显示列表框,评论输入框是第二层组件;评论显示列表中的每个评论是第三层组件。

  • 评论系统
    • 评论列表
      • 评论
    • 评论录入
Paste_Image.png

但是,在最初抽象组件时,可能这样搞

  • 评论系统
  • 评论
  • 评论录入

由于组件渲染最外层,只能一个标签,所以,对于评论来说,多个评论放一块,也应当组成一个组件。

也就是说,在抽象过程中,组合也是抽象的一部分。

功能分析

一下子有序的分析所有的功能真的不容易,应由根及叶(就像react官网上教的一样。)

  • 最根本的功能——
    用户输入评论、
    评论列表显示评论。
    很简单的描述,并不具体,为了很好的实现它,我们需要更深层的分析
    • 用户输入评论
      用户评论牵扯到评论录入表单组件,每一条评论应对应于一个数据对象
{
  id: '',
  author: '',
  text: ''
}

而这个数据对象,很明显,将是贯穿前后。
仅对于输入评论来说:输入用户名,输入评论内容,点击post按钮后,通过Ajax传输数据对象并请求服务器,服务器写入后台。
以上是基本功能,我们还需要一些更好的体验
点击post按钮后,清空Input内的内容。

Paste_Image.png

  • 评论列表显示评论
    显示评论,牵扯到评论列表组件和评论组件。依然是上边的数据对象
    能想到的功能就是
    当页面打开时候,从后台拿到数据,并逐个创建评论并插入到评论列表;
    当有用户更新评论时,能够实时显示;
    当有新评论添加时,能实时显示。
    一些更好的功能(react教程里的,我没想到这个):
    可以识别markdown语法,并正确显示。

以上,就是所有的分析,然我们开始实现它!

具体实现

  • 评论系统
    • 评论列表
      • 评论
    • 评论录入
      react标榜的是组件化开发,那么在依据react的开发过程也应当是基于组件开始的,那么就从组件开始吧。(按照react官网教程的结构,我觉得有必要加入我自己的理解,用更菜鸟一点的语言再说一说,这部分结束,后边有我自己从不同角度的理解)

官网流程——布蕾布蕾版

开始

准备工作

官网教程中文
英文貌似没有了。
项目源码github地址

Paste_Image.png

用git down下来,目录如图

别的不多说了,仅仅提一下server.js,模拟的后台,提供了一个api,

Paste_Image.png

这里定义了服务端口号,本地3000端口


Paste_Image.png

在script文件夹创建一个js文件,修改index.html中script的地址

最后,在项目目录运行
npm install安装项目依赖,package.json中查看
node server.js运行我们的后台
浏览器打开:http://localhost:3000/

准备工作结束
(接下来主要一步一步放代码,详情可以去官网看,不难)
以下代码,都写在自己新建的js

编写组件框架
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});
var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        Hello, world! I am a CommentList.
      </div>
    );
  }
});
var CommentForm = React.createClass({
  render: function() {
    return (
      <div className="commentForm">
        Hello, world! I am a CommentForm.
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox />,
  document.getElementById('content')
);
串联组件

让我们建立组件之间的联系

// 修改评论系统组件,链接其他组件
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList />
        <CommentForm />
      </div>
    );
  }
});
//添加评论组件
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {this.props.children}
      </div>
    );
  }
});
//将评论组件添加到评论列表组件中
// tutorial5.js
var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        <Comment author="Pete Hunt">This is one comment</Comment>
        <Comment author="Jordan Walke">This is *another* comment</Comment>
      </div>
    );
  }
});
//为了识别markdown语法,我们修改评论组件
var Comment = React.createClass({
  render: function() {
    var md = new Remarkable();
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {md.render(this.props.children.toString())}
      </div>
    );
  }
});
//由于react保护我们避免xss攻击,所以,以上修改没有实际效果,所以,
//对comment组件做如下修改

var Comment = React.createClass({
  rawMarkup: function() {
    var md = new Remarkable();
    var rawMarkup = md.render(this.props.children.toString());
    return { __html: rawMarkup };
  },

  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={this.rawMarkup()} />
      </div>
    );
  }
});
数据
//通过CommentBox组件state挂载数据
var CommentBox = React.createClass({
 getInitialState: function() {
    return {data: []};
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox data={data} />,
  document.getElementById('content')
);
//commentList中使用数据
var CommentList = React.createClass({
  render: function() {
    var commentNodes = this.props.data.map(function(comment) {
      return (
        <Comment author={comment.author} key={comment.id}>
          {comment.text}
        </Comment>
      );
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
});

要从服务器获取数据了!

//写下我们的接口
ReactDOM.render(
  <CommentBox url="/api/comments" />,
  document.getElementById('content')
);

点开comments.json文件,会看到我们的`数据库`,此时,照着上边改点有个人特色的,人名或话

//编写请求数据方法
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
    //通过一个简单的定时器实现不停的检查,当然也可以使用WebSockets等技术
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox url="/api/comments" pollInterval={2000} />,
  document.getElementById('content')
);

修改一下comments.json中的数据,观察网页变化吧

表单组件
//完善表单
var CommentForm = React.createClass({
  render: function() {
    return (
      <form className="commentForm">
        <input type="text" placeholder="Your name" />
        <input type="text" placeholder="Say something..." />
        <input type="submit" value="Post" />
      </form>
    );
  }
});
//添加方法
var CommentForm = React.createClass({
  getInitialState: function() {
    return {author: '', text: ''};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  render: function() {
    return (
      <form className="commentForm">
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
});
//提交表单事件
//在CommentForm组件中,添加或修改

//添加方法
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.state.author.trim();
    var text = this.state.text.trim();
    if (!text || !author) {
      return;
    }
    // TODO: 待会在这添加一个向服务器发送请求的方法
    this.setState({author: '', text: ''});
  },

//部分修改
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
});
//使用事件处理程序实现子到父组建通信
var CommentBox = React.createClass({
···
//添加
  handleCommentSubmit: function(comment) {
    // TODO: 等会再这编写提交服务器并刷新的具体实现
  },
···
//部分修改
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});
//用户提交表单时,调用回调函数
var CommentForm = React.createClass({
 ···
//部分修改
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.state.author.trim();
    var text = this.state.text.trim();
    if (!text || !author) {
      return;
    }
    this.props.onCommentSubmit({author: author, text: text});
    this.setState({author: '', text: ''});
  },
···
});
//提交服务器,并刷新的功能实现
var CommentBox = React.createClass({
 ···
//添加方法实现
  handleCommentSubmit: function(comment) {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: comment,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
 ···
});
优化

我们提交表单,再等刷新表单后获取数据,再显示到页面上,显然速度很慢。
我们做的优化是,本地提交后,根据本地缓存的评论数据对象(带本地添加的新平路)直接加载,等从服务器请求数据后,再进行一次加载。
因为react是否刷新页面取决于虚拟dom和真实dom的区别,如果从服务器请求数据后,再次加载时,发现没有变化,那么久不会刷新页面。这样做,性能显然就更高

var CommentBox = React.createClass({
  ···
//部分修改
  handleCommentSubmit: function(comment) {
    var comments = this.state.data;
    // 这个id=Date.now(),其实就是模拟一个为评论添加id的过程
    // 在下边细说
    comment.id = Date.now();
    var newComments = comments.concat([comment]);
    this.setState({data: newComments});
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: comment,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        this.setState({data: comments});
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
 ···
});

在数据库中,文章,用户,评论这些数据,都应该有一个id,
比如:一个文章对应一个作者,对应一堆评论。
在比如:一个评论,对应一个发布者,对应一篇文章

结束!

我的理解

我觉得理解一个项目,应当理解这几方面

  1. 通信(数据流向)
  2. Atom间通信(react的Atom是组件)
  3. 功能
  4. 事件
  5. 方法

他们并不是分立的,而是糅合在一起,我觉得清晰的理解他们是一个难点。
应该从根基着手,最终落脚于功能。比如react,从组件着手,落脚于设计组件,组合组件构成的各种功能。通信是针,数据是线,功能是目标。

对于这个评论框来说:

  • 数据流向
    评论(data)数据对象,贯穿上下前后(顶层组件到底层组件,前、后端到处窜)。
    在顶层组件state保存,一层一层通过props传入底层组件,最后反应到屏幕上。
    从form组件录入,传入后台保存。
    通过轮询,从后台拿到最新数据,反应到屏幕上
    这样的数据流向,与react的单向数据流特性有关。
  • 功能方法
    我们分析功能方法:
    需要请求服务器数据,就有了loadCommentsFromServer()方法,在首次加载和轮询时,我们都用它
    需要提交数据,handleCommentSubmit()被设置在父组件上,通过props传递引用,进入到表单组件。而子组件的handleSubmit()在点击按钮时候,调用handleCommentSubmit(),并清空表单。由于在调用时候并没有改变this,所以,此时handleCommentSubmit()的上下文还是在父组件中,那么通过它调用this.setState实现优化。
    handleSubmit()的数据从哪来?从input的handleAuthorChange()``handleTextChange()事件处理函数来。
通信方法图.png

复杂情况

然而,这个小项目,并不复杂,在很多复杂的情况下,我们要清楚数据流向和功能方法之间的关系,如何配合。

通信

React组件之间通信,主要由组件的关系决定

  • 父组件——>子组件
  • 子组件——>父组件
  • 无嵌套关系
    虽然只是初学者,但我也讲讲我的想法

父传子

- 利用props
太简单嘛,就像上边咱们的做法
但是这里有个问题,如果层数太多,怎么办。
可以想象,100层,一层一层往里边传,绝对是很慢(当然,只是我想当然,没测试过♪(*))。
怎么解决,还清楚

子传父

我觉得一定会有这种情况,比如购物网站,购物车显示当前购物车里商品个数。
如图


购物车示意图.png

我们可以从子传给父,然后父render,那么怎么办呢。
事件处理函数(核心,就是传递事件处理程序的引用)!从上边的例子中,我们也能看出,没错,就是handleCommentSubmit()方法
对于购物车例子我们可以这样
购物车组件:
某数量方法number(data)
子组件通过props引用方法,比如:子—setNumber={this.number}
在子上下文中,设置事件处理程序onChange={this.change},当数量改变时候,调用,setNumber(‘具体数字’)传入具体数量。由于传递的是引用,number方法的this并没有变,所以,可以在number()中,根据传入的n,设置父组件的state,也就实现了子传父。

Paste_Image.png

这里有一点要记录,就是react事件系统
它并不是用的原生事件,而是自己封装实现的react事件,(注意大写onClick,不是onclick,(#‵′)真坑,查了半个小时没查出来)
react事件系统可以查阅英文官网中文官网
其实上边那种方式,就像事件委托一样,仔细想想,复用事件处理程序其实也可这么写的。

不过,由于知识有限,目前还没有理解这里边的作用域链关系,有没有前辈不吝指导一下= ̄ω ̄=

无关系

似乎要用到设计模式。。。信号模式?发布订阅模式?
好吧,最直观还是兄弟1——>父亲——>兄弟2 o(╯□╰)o

关于发布订阅模式,我是这样理解的,有一个中继站,它可以注册话题,如果一个人在这个话题下发布(publish)文章,其他订阅(subscribe)话题的人,就能收到消息。但是这个中继站怎么做,我还不会,也没太研究过,不敢乱说。

es6也有传递信息的方式,es6没学精,不敢乱说,移步链接点我,ES6 中的生成器函数介绍

复杂情况就带此结束吧。

评论框结构总结

  • 评论系统
    <CommentBox url="/api/comments" pollInterval={3000} />
    HTML结构:

<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data}/>
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>

state: 
        data[] 评论数据
porps:
        url 请求服务器api接口地址
        pollInterval 循环检查变化的间隔时间    
`loadCommentsFromServer`: 从服务器获取评论的方法
`handleCommentSubmit`: 点击提交的方法
    - 评论列表
    `<CommentList />`:
    HTML结构:
    <div className="commentList">
        {commentNodes}
        commentNodes[i]的HTML结构
        <Comment author={item.author} key={item.id}>
            {item.text}
        </Comment>
    </div>
    props: 
                data 从父组件(CommentBox)state中的data数组而来,表示评论数据
    `commentNode`: 一个个<Comment>组件构成的数组。

        - 评论
        `<Comment />`
        HTML结构:
        <div className="comment">
            <h2 className="commentAuthor">
                {this.props.author}
            </h2>
            <span dangerouslySetInnerHTML={this.rawMarkup()}></span>
        </div>
        props: 
                    author 
                    key 
                    这俩都是从父组件(CommentList)传过来,数据对象
        rawMarkup: markdown的处理
    - 评论录入
    `<CommentForm />`:
    HTML结构:
    <form className="commentForm" onSubmit={this.handleSubmit}>
        <input type="text" placeholder="Your name"
            value={this.state.author}
            onChange={this.handleAuthorChange}
        />
        <input type="text" placeholder="说点啥呗..."
            value={this.state.text}
            onChange={this.handleTextChange}
        />
        <input type="submit" value="post" />
    </form>
    props: 
            onCommentSubmit  父组件的handleCommentSubmit()传给了它。
    state:
            author: 用户名
            text:评论内容
    `handleAuthorChange`: 用户名输入框改变触发的方法
    `handleTextChange`: 评论内容框改变触发的方法
    `handleSubmit`: 点击提交后触发的方法

放一张在sublime上写的,看着清晰一些。

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/4765228-aaa4bccb57d7aca9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)





## 后记

终于写完了,挺长,能看到最后不容易,大兄弟们握个手!
如有错误,请一定赐教


## 参考资料

推荐阅读更多精彩内容