《React官方文档》之教程Tutorial

原文链接 译者:jella77

教程Tutorial

 我们建立一个简单但实际的评论框,Disqus, LiveFyre或Facebook可以提供实时评论,评论框可以放在一个博客中。

我们提供:

  • 可以看到所有评论的视图
  • 提交评论的表单
  • 通过Hooks可以自定义后端

它有如下特点:

  • 优化的评论: 评论将会在保存到服务器上之前就出现在列表中,这样看上去非常快。
  • 实时更新: 其他用户的评论会实时的被放到评论界面。
  • Markdown格式: 用户可以使用Markdown来格式化他们的文本。

想要跳过这些只看源码?

都在GitHub上

运行一个服务器

首先,我们需要一个运行的服务器。它将作为API端口来获取并保存数据。为了简便,我们用脚本语言创建一个服务端。你可以看源文件 或者 下载zip文件 。

服务器使用一个JSON文件作为数据库。 在实际的产品中你不会这样用,但是这样可以简单的模拟出当你使用一个API是你可能做的事情。一旦你启动服务器, 它将会支持API端口并且为我们需要的静态页面提供服务。

开始

本教程中我们尽量简化。 上文提到的包中包含一个HTML文件。 在你最喜欢的编辑器中打开public/index.html,可以看到:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>React Tutorial</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/babel" src="scripts/example.js"></script>
    <script type="text/babel">
      // To get started with this tutorial running your own code, simply remove
      // the script tag loading scripts/example.js and start writing code here.
    </script>
  </body>
</html>

本教程余下的部份,我们将通过JavaScript语言完成<script>标签中的内容。我们没有任何高级的livereload(自动重新加载),因此你需要在保存修改刷新浏览器。启动服务后在浏览器中打开http://localhost:3000。当你未做任何修改加载在这个页面时,你将看到我们想要构建的成品。删掉前缀为<script>的标签就可以开始了。

注意:

我们在这里用到了jQuery因为我们想要简化ajax调用,但这在React中不是必要的。

你的第一个组件

React就是将各种组件模块化组合到一起。我们的评论框例子中需要以下组件结构:

- 评论框CommentBox
  - 评论列表CommentList
    - 每条评论Comment
  - 可提交的评论表单CommentForm

首先,构建评论框组件,这就是个简单的<div>:

// tutorial1.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});
ReactDOM.render(
  <CommentBox />,
  document.getElementById('content')
);

注意纯TML元素名都是以小写字母开头,然而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节点,他们是React的div组件实例。你可以把这些当作React知道如何处理的数据或者标识。React是安全的。我们并不产生HTML字符串,所以XSS保护是默认的。

你可以不返回基本的HTML,可以返回你或者他人建立的一个组件树。这使得React是可组合的(可维护前端的一个关键原则)。

ReactDOM.render() 实例化根节点组件,开始框架,将标识注入到原生的DOM中,作为第二个参数。

通过在不同平台上共享的React核心工具,ReactDOM方法显示出特定的DOM方法 (e.g., React Native)。

需要注意的是在本教程中ReactDOM.render要在js文件的底部。ReactDOM.render只有在符合组件被定义后才可以被调用。

复合组件

下面我们构建一个评论列表和评论表单的框架,用到的同样是简单的<div>。将这两个组件添加到你的文件中,保持评论框CommentBox定义的存在以及ReactDOM.render的调用:

// tutorial2.js
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>
    );
  }
});

接下来,评论框使用这两个新组件:

// tutorial3.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList />
        <CommentForm />
      </div>
    );
  }
});

注意我们是怎样将HTML标签和我们构造的组件结合到一起的。HTML组件就是常规的React组件,和你自己定义的就只有首字母大小写的区别。 JSX编译器可以自动的将HTML标签重写到React.createElement(标签名)表达式中而不管其他的,这将避免全局命名空间被污染。

使用属性

我们创建Comment组件,它将依赖于它的父组件传递给它的数据。 对于子组件来说,从父组件传递来的数据可以像一个属性一样被获取。这些属性通过this.props得到。使用属性我们可以读取从CommentList传递给Comment的数据,并且渲染标识:

// tutorial4.js
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {this.props.children}
      </div>
    );
  }
});

通过将JavaScript表达式包含在JSX中(作为属性或者子节点),你可以将文本或者React组件放在树中。我们通过传给组件的this.props 及其他像this.props.children的键来获取值。我们获取传递给组件的属性传递给组件,比如this.props的键或者其他被大括号包起来的比如this.props.children。

组件属性

既然我们定义了Comment 组件,我们就希望利用它来传递作者名和评论内容。这将使得我们对每一段评论重用同样的代码。现在让我们为我们的CommentList添加一些评论:

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

注意到我们已经从父组件CommentList 向子组件Comment传递数据。例如:我们将名字Pete Hunt (通过author属性) 和评论内容This is one comment (通过类XML的子节点)传递给第一个Comment。如上所述,Comment组件将通过this.props.author和this.props.children获取属性。

添加Markdown

Markdown是一个简单的方式来格式化你的文本。例如星号围绕的文本将会被强调。

在本教程中我们使用一个第三方库marked,它将把Markdown文本转化为纯HTML.。我们为页面引用这个库然后直接使用,把文本转化为Markdown并输出:

// tutorial6.js
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {marked(this.props.children.toString())}
      </div>
    );
  }
});

我们现在所做的就是调用marked库。我们需要把this.props.children从React包裹的文本转化为一个字符串,因此我们显式的调用toString()。

但仍然有问题!我们渲染好的评论在显示器中这样显示:”<p>This is<em>another</em> comment</p>“。我们想把这些标签真正的渲染成HTML。

这是因为React会防止 XSS攻击。有种方法可以解决这个问题但建议尽量不这么做:

// tutorial7.js

var Comment = React.createClass({
  rawMarkup: function() {
    var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
    return { __html: rawMarkup };
  },

  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={this.rawMarkup()} />
      </div>
    );
  }
});

这个特殊的API使得插入纯HTML变得困难,但是为了marked也不得不使用这个伎俩。

记住: 使用这个要确保marked是安全的。在这里我们传递sanitize: true 来告诉marked不保留任何的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()调用来将数据通过属性传入到CommentList

// tutorial9.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox data={data} />,
  document.getElementById('content')
);

既然在CommentList中数据可得,我们可以动态渲染评论:

// tutorial10.js
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>
    );
  }
});

这就可以了!

从服务器取数据

下面我们从服务器动态获取数据来代替固化的代码。我们去掉data属性并以一个URL来代替取数据:

// tutorial11.js
ReactDOM.render(
  <CommentBox url="/api/comments" />,
  document.getElementById('content')
);

这个组件不同于前面的那些因为它必须重新渲染它自己。服务器端响应之前组件不会有任何数据,这时间段组件不会渲染新的评论。

注意:代码在这个阶段是不能工作的。

反应状态

到目前为止,每个组件都可以基于自己的属性渲染自己。 属性props是不可改变的: 它们来自父节点并且属于父节点。为了实现交互,我们为组件引入一个可变的状态。 this.state是组件私有的并且可以通过调用this.setState()被改变。当状态state更新时,组件也会重新渲染。

render()方法就像this.propsthis.state函数一样以声明形式写入。框架却表UI始终与输入一致。

当服务器端取数据,我们可以修改我们已有的评论。下面我们为 CommentBox组件增加一组数据作为它的状态:

// tutorial12.js
var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

getInitialState()在组件生命周期中被执行并且建立组件初始状态。

更新状态

当组件一开始被创建时,我们想要从服务器端获取JSON,并且更新状态反映到最新的数据中。我们将使用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 className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

这里, componentDidMount是个在组件第一次被渲染后自动被React调用的方法。动态更新的关键是调用this.setState()。我们用来自服务器的评论组替代旧的评论组,并且UI可以自动更新。 由于这个反应特性,添加实时更新只需要有也很小的改变。我们在这里使用的是简单的轮询,但是你可以使用WebSocket或者其他技术。

// 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 className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

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

这里我们做的是将AJAX调用移到一个单独的方法中,并且在组件第一次被加载时以及之后每隔两秒时被调用。区运行你的浏览器并且修改 comments.json文件 (在你服务端的同一个目录下),两秒钟后你就可以看到变化!

增加新评论

现在我们建立表单。我们的 CommentForm 组件应该获取用户的姓名和评论文本,并且发送一个请求给服务端将评论保存下来。

// tutorial15.js
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>
    );
  }
});

控制组件

传统的DOM,input元素是由浏览器管理其状态(它渲染的值)。结果DOM的状态和组件的状态不同。视图状态与组件状态不同,这不是理想的。在React中,组件状态和视图状态始终一样,不仅限于初始化时相同。

因此我们使用this.state来保存用户的输入。我们定义初始状态state,并赋予两个属性作者名author和评论文本text并置空。在我们的<input>元素中,我们使用value属性并反映组件的状态 state,另外附onChange事件。 这些 含有待赋值的<input>元素叫做控制组件。更多关于控制组件的内容可以参考表单

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

事件

React为组件附上事件并用骆驼拼写法命名。我们为两个<input>元素附上 onChange 事件。现在作为用户在<input>区域输入文本,被附上的 onChange被激活,组件状态被改变。随后,<input>元素渲染的值将会被刷新,当前组件状态state也随之改变。

提交表单

下面我们让表单交互化。当用户提交表单,我们应该清空表单,并向服务端发送请求,刷新评论列表。首先我们要坚监听表单提交事件并清空它。

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

我们为表单附上onSubmit句柄,这样表单被有效填写并提交后表单区域会立即清空。

调用 preventDefault() 来避免浏览器默认提交表单的动作。

作为属性回调

当一个用户提交一个评论,我们需要刷新列表把这个新评论放进来。在 CommentBox中完成这个逻辑非常合理,因为CommentBox拥有评论列表的状态。

我们需要从子组件中将数据传给父组件。我们在父组件 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 className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

既然 CommentBox 已经让 CommentForm通过onCommentSubmit属性获得回调,当用户提交表单时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 (
      <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>
    );
  }
});

现在回调已经在了,我们要去做的就是提交给服务端并刷新列表:

// 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 className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </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 className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});

恭喜!

你刚刚通过这样简单的几步完成了一个评论框。更多内容见为什么使用React,更加深入可参考API reference。好运!

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/114829.html

(0)
上一篇 2021年8月27日
下一篇 2021年8月27日

相关推荐

发表回复

登录后才能评论