00_悦

教程 | React

00_悦 · 2017-08-30翻译 · 6014阅读 原文链接

#

教程

我们来创建一个简单实用可以放到你的博客里面评论框,Disqus、LiveFyre、Facebook提供了最简单版本的实时评论。

我们会提供:

  • 所有评论的展示

  • 提交评论的表单

  • 提供用户后台的hooks

还会有一些的巧妙的特性:

  • 优化的评论:评论会在保存到服务器之前就展示到列表里面,所以看起来更快。

  • 实时更新:其他用户的评论会实时弹入评论列表里。

  • Markdown格式:用户可以采用Markdown来格式化文本。

跳过直接查看源代码? #

都在GitHub上

启一个server #

为了开始本教程,我们先来启一个server。这仅作为我们获取和保存数据的API端。为了尽可能简单,我们已经用许多脚本语言搭建了简单的server,能满足我们的需求。查看资源或者下载zip包,里面包含了运行所需的所有文件

为了简单,在服务端用JSON 文件作为数据库。在生产环境不会这么用,但这样可以轻松模拟你对API的各种使用。只要启动server,它就能支持我们的API端,同时也支持静态页面。

启动项目#

对于本教程,怎么简单怎么来。在前面讨论的server包里包含了我们将要处理的HTML文件。在常用的编辑器中打开public/index.html。如下所示:




<!DOCTYPE html>

<html>

  <head>

    <meta charset="utf-8" />

    <title>React Tutorial</title>

    ``<script src="https://npmcdn.com/react@15.3.1/dist/react.js">``</script>

    ``<script src="https://npmcdn.com/react-dom@15.3.1/dist/react-dom.js">``</script>

    ``<script src="https://npmcdn.com/babel-core@5.8.38/browser.min.js">``</script>

    ``<script src="https://npmcdn.com/jquery@3.1.0/dist/jquery.min.js">``</script>

    ``<script src="https://npmcdn.com/remarkable@1.6.2/dist/remarkable.min.js">``</script>

  </head>

  <body>

    <div></div>

    ``&lt;script type="text/babel" src="scripts/example.js"&gt;``&lt;/script&gt;

    ``&lt;script type="text/babel"&gt;``

      // To get started with this tutorial running your own code, simply remove

      // the script tag loading scripts/example.js and start writing code here.

    &lt;/script&gt;

  &lt;/body&gt;

&lt;/html&gt;

接下来的教程,我们开始在脚本标签中编写JavaScript。由于没有使用任何高级的实时刷新技术,所以你需要在保存之后刷新浏览器来查看更新。按照你的进度,在浏览器中打开http://localhost:3000(服务器启动之后)。如果不做任何修改,第一次加载页面你会看到我们将要完成的产品成品。开始开发时,删掉前面的&lt;script&gt;标签就可以继续了。

> 注意:

> 我们添加了jQuery,因为想要简化后面AJAX请求的代码。不过这个对于React 并强制性的。

第一个评论 #

React是关于模块化、组件组合的。在我们的评论框案例中,组件结构如下:


- CommentBox

  - CommentList

    - Comment

  - CommentForm

我们先来创建CommentBox组件,就是简单的<div>


// tutorial1.js

var CommentBox = React.createClass({

  render: function() {

    return (

      <div>

        Hello, world! I am a CommentBox.

      </div>

    );

  }

});

ReactDOM.render(

  &lt;CommentBox /&gt;,

  document.getElementById('content')

);

请注意,原生HTML元素名以小写字母开头,而自定义的React类名以大写字母开头。

JSX语法#

首先,你会注意到JavaScript 中类似XML的语法。我们会用一个简单的预编译来把这个语法糖转换为纯JavaScript:


// tutorial1-raw.js

var CommentBox = React.createClass({displayName: 'CommentBox',

  render: function() {

    return (

      React.createElement('div', {className: "commentBox"},

        "Hello, world! I am a CommentBox."

      )

    );

  }

});

ReactDOM.render(

  React.createElement(CommentBox, null),

  document.getElementById('content')

);

它是可选的,不过我们发现JSX语法比纯JavaScript更好用。阅读更多JSX语法文章

接下来#

我们将JavaScript对象的一些方法加到React.createClass()上,来创建一个新的React组件。其中,最重要的方法是render,它返回一个React组件树并最终将其渲染成HTML。

<div>标签并不是实际的DOM节点;它是Reactdiv组件的实例。你可以把它看作是React知道如何处理的标记或者数据片段。React是安全的。我们不生成HTML字符串,所以默认就有XSS防护。

你不用返回基础的HTML。只需要返回你(或者别人)创建的组件树就行。这就使得React是可组合的:可维护性前端开发的宗旨。

ReactDOM.render()实例化根组件,启动框架,将标记注入作为第二个参数提供的原始DOM元素中。

ReactDOM模块暴露了DOM的特定方法,而React具有不同平台(比如React Native)上React共享的核心工具。

ReactDOM.render留到本教程脚本的最后是很重要的。ReactDOM.render只能在复合组件被定义好之后才能调用。

受控组件#

让我们来构建CommentListCommentForm的架子,再次使用简单的div。把这两个组件放到你的文件中,保持现有CommentBox的声明和ReactDOM.render的调用:


// tutorial2.js

var CommentList = React.createClass({

  render: function() {

    return (

      <div>

        Hello, world! I am a CommentList.

      </div>

    );

  }

});

var CommentForm = React.createClass({

  render: function() {

    return (

      <div>

        Hello, world! I am a CommentForm.

      </div>

    );

  }

});

接下来,用这些新的组件来更新CommentBox


// tutorial3.js

var CommentBox = React.createClass({

  render: function() {

    return (

      <div>

        <h1>Comments</h1>

        &lt;CommentList /&gt;

        &lt;CommentForm /&gt;

      </div>

    );

  }

});

注意我们是如何组合HTML标签和所创建的组件的。HTML组件就是常规的React组件,就像你定义的其他组件一样,不过有一点不一样。JSX编译器会自动把HTML标签重写为React.createElement(tagName)表达式,并保留其他内容。这是为了防止全局命名空间的污染。

使用props #

我们来创建Comment组件,它依赖于父组件传进来的数据。父组件传递的数据可以当作子组件的一个可用“属性”。这些“属性”可以通过this.props来访问。通过props,我们可以获取CommentList传给Comment的数据,并用来渲染一些标记:


// tutorial4.js

var Comment = React.createClass({

  render: function() {

    return (

      <div>

        <h2>

          {this.props.author}

        </h2>

        {this.props.children}

      </div>

    );

  }

});

通过JSX中大括号里面的JavaScript表达式(作为属性或者child),你可以把文本或者React组件放到树里面。我们通过this.props上的键来访问传递到组件的命名属性,通过this.props.children来访问嵌套的元素。

组件特性#

现在我们定义好了Comment组件,想要传给它作者姓名和评论文字。这允许我们对于每条评论复用代码。现在,让我们在CommentList中添加一些评论:


// tutorial5.js

var CommentList = React.createClass({

  render: function() {

    return (

      <div>

        &lt;Comment author="Pete Hunt"&gt;This is one comment&lt;/Comment&gt;

        &lt;Comment author="Jordan Walke"&gt;This is *another* comment&lt;/Comment&gt;

      </div>

    );

  }

});

注意,我们已经从父组件CommentList传递了一些数据给子组件Comment了。例如,我们传了Pete Hunt(通过属性)以及This is one comment(通过类XML的子节点)给第一个Comment。如上所述,Comment组件可以通过this.props.authorthis.props.children来访问这些“属性”。

添加Markdown #

Markdown是一种格式化文本的简单方法。例如,星号里的文字会被强调。

在本教程中,我们使用了第三方库remarkable,它可以把Markdown文本转换为原始HTML。我们已经将这个库和原始标记一起包含在页面中了,现在可以直接使用。让我们将评论文本转换为Markdown并输出:


// tutorial6.js

var Comment = React.createClass({

  render: function() {

    var md = new Remarkable();

    return (

      <div>

        <h2>

          {this.props.author}

        </h2>

        {md.render(this.props.children.toString())}

      </div>

    );

  }

});

在这里就是调用remarkable库。我们需要把React包装的文本this.props.children转换为remarkable能读懂的原生字符串,所以这里显式调用了toString()

但是有个问题!在浏览器中渲染出来的评论看起来像这样"<p>This is<em>another</em>comment</p>"。我们希望这些标签都实际渲染成HTML。

React保护你不受XSS攻击。有一种方法可以避开,但是框架会警告你不要使用它:


// tutorial7.js

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>

        <h2>

          {this.props.author}

        </h2>

        <span />

      </div>

    );

  }

});

这是一个特殊的API,它故意让插入原生HTML变得困难,但是对于remarkable,我们可以利用好这个后门。

记住:如果使用这个特性,依赖remarkable才是安全的。在这种情况下,remarkable会自动把HTML标记和不安全的链接从输出中剔除。

挂载数据模型#

到目前为止,我们已经直接把评论插入了源码中。现在让我们把JSON数据添加到评论列表中。数据最终从服务器获取,但现在,先写在源码里面:


// tutorial8.js

var data = [

  {id: 1, author: "Pete Hunt", text: "This is one comment"},

  {id: 2, author: "Jordan Walke", text: "This is *another* comment"}

];

我们需要用模块化的方式把这些数据引入CommentList。修改CommentBoxReactDOM.render()调用,通过props把数据传给CommentList


// tutorial9.js

var CommentBox = React.createClass({

  render: function() {

    return (

      <div>

        <h1>Comments</h1>

        &lt;CommentList data={this.props.data} /&gt;

        &lt;CommentForm /&gt;

      </div>

    );

  }

});

ReactDOM.render(

  &lt;CommentBox data={data} /&gt;,

  document.getElementById('content')

);

现在就可以在CommentList中使用数据了,让我们动态渲染评论:


// tutorial10.js

var CommentList = React.createClass({

  render: function() {

    var commentNodes = this.props.data.map(function(comment) {

      return (

        &lt;Comment author={comment.author} key={comment.id}&gt;

          {comment.text}

        &lt;/Comment&gt;

      );

    });

    return (

      <div>

        {commentNodes}

      </div>

    );

  }

});

就这么简单!

获取服务器数据#

用从服务器获取的动态数据来替换写在代码里面的数据。我们删除假数据,用URL来获取:


// tutorial11.js

ReactDOM.render(

  &lt;CommentBox url="/api/comments" /&gt;,

  document.getElementById('content')

);

现在这个组件和以前有点不同,因为它会重新渲染自身。该组件在服务器请求返回之前都没有数据,请求返回时组件需要渲染新的评论。

注意:代码在这步没有工作。

响应式state #

到目前为止,基于props,每个组件都渲染过一次。props是不变的:它们是从父组件传来的,是父组件“所有”。为了实现交互,我们在组件中引入了可变的statethis.state是组件私有的,可以通过调用this.setState()来改变。当state更新了,组件自身就会重新渲染。

render()方法是作为this.propsthis.state函数的声明编写的。框架会保证UI始终和输入保持一致。

当从服务器获取了数据,我们会更新已有的评论数据。给CommentBox组件添加一个评论数据的数组来作为它的state:


// tutorial12.js

var CommentBox = React.createClass({

  getInitialState: function() {

    return {data: []};

  },

  render: function() {

    return (

      <div>

        <h1>Comments</h1>

        &lt;CommentList data={this.state.data} /&gt;

        &lt;CommentForm /&gt;

      </div>

    );

  }

});

getInitialState()在组件的生命周期内仅执行一次,设置组件的初始状态。

更新state #

在组件首次创建的时候,我们希望从服务器GET一些JSON数据,并通过更新state来反映最新数据。使用jQuery来向服务器发异步请求获取需要的数据。数据已经包含在你启动的服务器上了(基于comments.json文件),因此一旦获取到,this.state.data就会如下所示:


[

  {"id": "1", "author": "Pete Hunt", "text": "This is one comment"},

  {"id": "2", "author": "Jordan Walke", "text": "This is *another* comment"}

]

// tutorial13.js

var CommentBox = React.createClass({

  getInitialState: function() {

    return {data: []};

  },

  componentDidMount: 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)

    });

  },

  render: function() {

    return (

      <div>

        <h1>Comments</h1>

        &lt;CommentList data={this.state.data} /&gt;

        &lt;CommentForm /&gt;

      </div>

    );

  }

});

这里,componentDidMount是在组件第一次渲染完成后React自动调用的方法。动态更新的关键是调用this.setState()。我们用从服务器获取到的数据来替换原来的评论数组,UI会自动更新。由于这种反应,添加实时更新只是一个很小的改变。在这里使用了简单的轮询,你也可以使用WebSockets或者其他技术。


// tutorial14.js

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);

  },

  render: function() {

    return (

      <div>

        <h1>Comments</h1>

        &lt;CommentList data={this.state.data} /&gt;

        &lt;CommentForm /&gt;

      </div>

    );

  }

});

ReactDOM.render(

  &lt;CommentBox url="/api/comments" pollInterval={2000} /&gt;,

  document.getElementById('content')

);

我们在这里所做的就是把AJAX的调用分离成一个单独的方法,在组件首次加载以及后面每2秒调用一次。试着在你的浏览器中运行代码并修改comments.json文件(服务器同一目录中);2秒钟,修改就能看到了。

添加新评论#

现在我们来创建表单。CommentForm组件应该知道用户的名称和评论文字,然后向服务器发送请求保存评论。


// tutorial15.js

var CommentForm = React.createClass({

  render: function() {

    return (

      &lt;form className="commentForm"&gt;

        &lt;input type="text" placeholder="Your name" /&gt;

        &lt;input type="text" placeholder="Say something..." /&gt;

        &lt;input type="submit" value="Post" /&gt;

      &lt;/form&gt;

    );

  }

});

受控组件#

传统的DOM中,input元素被渲染,是浏览器来管理它的状态(渲染它的value)。因此,实际DOM的状态与组件的状态是不同的。视图的状态和组件的状态是不同的,这并不理想。在React中,组件始终反映视图的state,而不仅仅只是在初始化的时候。

我们用this.state来保存用户的输入。定义一个初始只包含authortext两个空字符串属性的state。在&lt;input&gt;元素中,我们设置了value来反映组件的state,然后给它们添加onChange事件。这些具有value集合的&lt;input&gt;元素称为受控组件。在Forms article上阅读更多受控组件的文章。


// tutorial16.js

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 (

      &lt;form className="commentForm"&gt;

        &lt;input

          type="text"

          placeholder="Your name"

          value={this.state.author}

          onChange={this.handleAuthorChange}

        /&gt;

        &lt;input

          type="text"

          placeholder="Say something..."

          value={this.state.text}

          onChange={this.handleTextChange}

        /&gt;

        &lt;input type="submit" value="Post" /&gt;

      &lt;/form&gt;

    );

  }

});

事件#

React约定使用驼峰命名来给组件添加事件句柄。我们给这两个&lt;input&gt;元素添加了onChange事件。现在,当用户在&lt;input&gt;中输入文本时就会触发onChange的回调,组件的state就改变了。然后,input元素的渲染值将被更新来反映当前组件的state

(精明的读者可能会奇怪事件句柄像描述那样工作,因为方法的引用并没有明确的绑定到this。那是因为React.createClass(...)自动将每个方法绑定到组件的实例上,避免了显式绑定的需要。)

提交表单#

我们让表单能够有交互。当用户提交表单之后,应该清除它并向服务器提交请求,然后刷新评论列表。首先,监听表单的提交并且进行清除。


// tutorial17.js

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});

  },

  handleSubmit: function(e) {

    e.preventDefault();

    var author = this.state.author.trim();

    var text = this.state.text.trim();

    if (!text || !author) {

      return;

    }

    // TODO: send request to the server

    this.setState({author: '', text: ''});

  },

  render: function() {

    return (

      &lt;form className="commentForm" onSubmit={this.handleSubmit}&gt;

        &lt;input

          type="text"

          placeholder="Your name"

          value={this.state.author}

          onChange={this.handleAuthorChange}

        /&gt;

        &lt;input

          type="text"

          placeholder="Say something..."

          value={this.state.text}

          onChange={this.handleTextChange}

        /&gt;

        &lt;input type="submit" value="Post" /&gt;

      &lt;/form&gt;

    );

  }

});

给form添加了一个onSubmit,在表单进行有效提交的时候清除表单字段。

在事件处理中调用preventDefault()来阻止浏览器提交表单的默认操作。

作为props的回调#

当用户提交评论时,我们需要刷新评论列表使之包含新的评论。在CommentBox中执行这些逻辑是有道理的,因为CommentBox包含了展示评论列表的state。

我们需要把子组件的数据传递给父级。在父组件的render方法中向子组件传一个新的回调(handleCommentSubmit),并将其绑定到子组件的onCommentSubmit事件中。不管什么时候事件触发,都会执行回调:


// tutorial18.js

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)

    });

  },

  handleCommentSubmit: function(comment) {

    // TODO: submit to the server and refresh the list

  },

  getInitialState: function() {

    return {data: []};

  },

  componentDidMount: function() {

    this.loadCommentsFromServer();

    setInterval(this.loadCommentsFromServer, this.props.pollInterval);

  },

  render: function() {

    return (

      <div>

        <h1>Comments</h1>

        &lt;CommentList data={this.state.data} /&gt;

        &lt;CommentForm onCommentSubmit={this.handleCommentSubmit} /&gt;

      </div>

    );

  }

});

现在CommentBox已经通过prop的onCommentSubmit使回调在CommentForm中可用。CommentForm可以在用户提交表单的时候调用回调函数:


// tutorial19.js

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});

  },

  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: ''});

  },

  render: function() {

    return (

      &lt;form className="commentForm" onSubmit={this.handleSubmit}&gt;

        &lt;input

          type="text"

          placeholder="Your name"

          value={this.state.author}

          onChange={this.handleAuthorChange}

        /&gt;

        &lt;input

          type="text"

          placeholder="Say something..."

          value={this.state.text}

          onChange={this.handleTextChange}

        /&gt;

        &lt;input type="submit" value="Post" /&gt;

      &lt;/form&gt;

    );

  }

});

现在回调函数已经好了,我们要做的就是把数据提交到服务器并刷新列表:


// tutorial20.js

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)

    });

  },

  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)

    });

  },

  getInitialState: function() {

    return {data: []};

  },

  componentDidMount: function() {

    this.loadCommentsFromServer();

    setInterval(this.loadCommentsFromServer, this.props.pollInterval);

  },

  render: function() {

    return (

      <div>

        <h1>Comments</h1>

        &lt;CommentList data={this.state.data} /&gt;

        &lt;CommentForm onCommentSubmit={this.handleCommentSubmit} /&gt;

      </div>

    );

  }

});

优化:积极更新#

现在,应用程序的功能完成了,但是必须等待请求完成才能把评论展示到列表中,感觉有点慢。我们可以主动添加评论到列表中,使应用感觉更快。


// tutorial21.js

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)

    });

  },

  handleCommentSubmit: function(comment) {

    var comments = this.state.data;

    // Optimistically set an id on the new comment. It will be replaced by an

    // id generated by the server. In a production application you would likely

    // not use Date.now() for this and would have a more robust system in place.

    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)

    });

  },

  getInitialState: function() {

    return {data: []};

  },

  componentDidMount: function() {

    this.loadCommentsFromServer();

    setInterval(this.loadCommentsFromServer, this.props.pollInterval);

  },

  render: function() {

    return (

      <div>

        <h1>Comments</h1>

        &lt;CommentList data={this.state.data} /&gt;

        &lt;CommentForm onCommentSubmit={this.handleCommentSubmit} /&gt;

      </div>

    );

  }

});

恭喜! #

你通过简单的几个步骤创建了一个评论框。了解更多为何使用React,或者深入学习API参考,开始hacking!祝你好运!

相关文章