React 示例(二)
原文:
zh.annas-archive.org/md5/b5a25aa2a02a14cf60f32ae6c400d782译者:飞龙
第五章:Chapter 5. Mixins and the DOM
In the previous chapter, we took a deep dive into React Forms. We took a look at building multiple components and interactivity between them, Controller and Uncontrolled Components, building Forms and Form elements, and Form events and handlers for the events. We build a form to capture cart-checkout flow and orders being placed in a multi-step form.
In this chapter, we will focus on abstracting content using mixins and touch upon DOM handling.
Here, we will cover the following points:
-
Mixins
-
PureRender mixin
-
React and the DOM
-
Refs
At the end of this chapter, we will be able to abstract and reuse logic across our components and learn how to handle DOM from within the components.
Back at the office
The duo was back at work. Mike entered with a cup of coffee. It was morning and the office had just started to buzz.
"So Shawn, we did a lot of complex forms stuff last time. Our cart flow is now complete. However, now we have been asked to add a timeout to the cart. We need to show a timer to the user that they need to checkout and complete the order in 15 minutes."
"Any idea how we can do this?"
"Umm, maintain a state for timer and keep updating every second? Take some action when the timer hits zero."
"Right! We will use intervals to reduce the timeout values and keep updating our views to display the timer. As we have been storing the form data in a single place, our Bookstore component, let's go ahead and add a state value that will track this timeout value. Let's change our initial state to something similar to the following:"
getInitialState() {
return ({currentStep: 1, formValues: {}, cartTimeout: 60 * 15});
}
"60 X 15, that's 15 minutes in seconds value. We will also need to add a method to keep updating this state so that we can use it freely from here as well as the child components."
updateCartTimeout(timeout){
this.setState({cartTimeout: timeout});
}
"Cool."
"Now, what we will do is define what are called as mixins."
"Mixins?"
"Yeah, mixins allow us to share a code across components. Let's take a look at how we are going to use it before moving ahead."
var SetIntervalMixin = {
componentWillMount: function() {
this.intervals = [];
},
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount: function() {
this.intervals.map(clearInterval);
}
};
module.exports = SetIntervalMixin;
"所以我们在这里做的 nothing much but defining an object. We will see how we use it in our components."
"As you can see, what we are trying to achieve here is add a way to track all our interval handlers, as follows:"
componentWillMount: function() {
this.intervals = [];
}
"Here, we are first initializing an array to hold instances to intervals that we will be creating. Next, we will define a method that can be used to define new intervals, as follows:"
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
}
"Got it. I see the last bit is defining the componentWillUnmount method and we have already defined componentWillMount; but this isn't a React component. Why do we have these method here?"
"Oh right. Let's take a look at the following method first:"
componentWillUnmount: function() {
this.intervals.map(clearInterval);
}
"What this method does is clean up the intervals, which we might have created, before we unmount our component."
"Got it."
"Now, as you mentioned, we have two life cycle methods here—componentWillMount and componentWillUnmount."
"当我们开始在组件中使用这个功能时,它们就像我们组件中其他类似的生命周期方法一样被调用。"
"哦,很好。这两个方法都会被调用吗?”肖恩问。
"没错。现在我们已经定义了 mixin,让我们开始使用它!"
"我们首先想开始使用这个功能的地方是在交付详情页。这就像做以下事情一样简单:"
var DeliveryDetails = React.createClass({
…
mixins: [SetIntervalMixin]
…
"太棒了,接下来我们希望开始使用这个功能来存储cartTimeout值并更新它们。你能定义一个 mixin 来完成这个任务吗?”迈克问道。
"好的,我将首先定义一个方法来递减购物车计时器,这将保持更新状态。接下来,我们需要实际设置超时,以便每隔一段时间调用该方法,使其每秒调用一次以递减时间?"
"没错,让我们看看你会怎么做。"
var CartTimeoutMixin = {
componentWillMount: function () {
this.setInterval(this.decrementCartTimer, 1000);
},
decrementCartTimer(){
if (this.state.cartTimeout == 0) {
this.props.alertCartTimeout();
return;
}
this.setState({cartTimeout: this.state.cartTimeout - 1});
},
};
"太好了,这正是我们需要的。但我们遗漏了一部分;我们需要能够将这个信息发送回父组件以存储我们在这里更新的计时器值。"
"我们还将注意从父组件传递当前计时器的状态到子组件。"
"哦,对了。"
"让我们回到父组件,开始传递购物车计时器值给子组件。现在我们的渲染方法应该看起来像这样:"
……
render() {
switch (this.state.currentStep) {
case 1:
return <BookList updateFormData={this.updateFormData}/>;
case 2:
return <ShippingDetails updateFormData={this.updateFormData}
cartTimeout={this.state.cartTimeout}
updateCartTimeout={this.updateCartTimeout} />;
case 3:
return <DeliveryDetails updateFormData={this.updateFormData}
cartTimeout={this.state.cartTimeout}
updateCartTimeout={this.updateCartTimeout} />;
……
"请注意,我们在这里传递了updateCartTimeout方法。这是我们将在 mixin 中开始使用的东西。"
"接下来,我们将更新DeliveryDetails组件以开始存储cartTimeout值。"
getInitialState() {
return { deliveryOption: 'Primary', cartTimeout: this.props.cartTimeout };
}
"有了这个设置,我们现在可以设置交付选项页的渲染方法,现在应该看起来像以下这样:"
render() {
var minutes = Math.floor(this.state.cartTimeout / 60);
var seconds = this.state.cartTimeout - minutes * 60;
return (
<div>
<h1>Choose your delivery options here.</h1>
<div style={{width:200}}>
<form onSubmit={this.handleSubmit}>
<div className="radio">
<label>
<input type="radio"
checked={this.state.deliveryOption === "Primary"}
value="Primary"
onChange={this.handleChange} />
Primary -- Next day delivery
</label>
</div>
<div className="radio">
<label>
<input type=e"radio"
checked={this.state.deliveryOption === "Normal"}
value="Normal"
onChange={this.handleChange} />
Normal -- 3-4 days
</label>
</div>
<button className="btn btn-success">
Submit
</button>
</form>
</div>
<div className="well">
<span className="glyphicon glyphicon-time" aria-hidden="true"></span> You have {minutes} Minutes, {seconds} Seconds, before confirming order
</div>
</div>
);
}
"我们还需要开始使用CartMixin,所以我们的mixins导入应该看起来像以下这样:"
…
mixins: [SetIntervalMixin, CartTimeoutMixin],
…
"很好,让我看看现在的运输信息看起来怎么样。"
"它工作了!”肖恩兴奋地说。
"太棒了。记住,肖恩,现在我们需要在切换到其他页面时将信息传递回父组件。"
"是的,我们应该将其添加到使用了 mixin 的组件中?"
"更好的是,让我们将以下代码添加到 mixin 中:"
….
componentWillUnmount(){
this.props.updateCartTimeout(this.state.cartTimeout);
}
….
"现在我们的 mixin 应该看起来像以下这样:"
var CartTimeoutMixin = {
componentWillMount: function () {
this.setInterval(this.decrementCartTimer, 1000);
},
decrementCartTimer(){
if (this.state.cartTimeout == 0) {
this.props.alertCartTimeout();
return;
}
this.setState({cartTimeout: this.state.cartTimeout - 1});
},
componentWillUnmount(){
this.props.updateCartTimeout(this.state.cartTimeout);
}
};
module.exports = CartTimeoutMixin;
"我们的 mixin 现在将在组件卸载时更新当前的购物车值。"
"我们遗漏了一件事,它是这个 mixin 的一部分。当计时器达到零时,我们调用this.props.alertCartTimeout()。"
"我们将在父组件上定义这个,并传递它以便从子组件调用,如下所示:"
alertCartTimeout(){
this.setState({currentStep: 10});
},
"然后更新我们的渲染方法,以便在达到超时步骤时进行处理,如下所示:"
render() {
switch (this.state.currentStep) {
case 1:
return <BookList updateFormData={this.updateFormData}/>;
case 2:
return <ShippingDetails updateFormData={this.updateFormData}
cartTimeout={this.state.cartTimeout}
updateCartTimeout={this.updateCartTimeout}
alertCartTimeout={this.alertCartTimeout}/>;
case 3:
return <DeliveryDetails updateFormData={this.updateFormData}
cartTimeout={this.state.cartTimeout}
updateCartTimeout={this.updateCartTimeout}
alertCartTimeout={this.alertCartTimeout}/>;
case 4:
return <Confirmation data={this.state.formValues}
updateFormData={this.updateFormData}
cartTimeout={this.state.cartTimeout}/>;
case 5:
return <Success data={this.state.formValues} cartTimeout={this.state.cartTimeout}/>;
case 10:
/* Handle the case of Cart timeout */
return <div><h2>Your cart timed out, Please try again!</h2></div>;
default:
return <BookList updateFormData={this.updateFormData}/>;
}
}
"让我们看看完成它后DeliveryDetails组件看起来怎么样:"
import React from 'react';
import SetIntervalMixin from './mixins/set_interval_mixin'
import CartTimeoutMixin from './mixins/cart_timeout_mixin'
var DeliveryDetails = React.createClass({
propTypes: {
alertCartTimeout: React.PropTypes.func.isRequired,
updateCartTimeout: React.PropTypes.func.isRequired,
cartTimeout: React.PropTypes.number.isRequired
},
mixins: [SetIntervalMixin, CartTimeoutMixin],
getInitialState() {
return { deliveryOption: 'Primary', cartTimeout: this.props.cartTimeout };
},
componentWillReceiveProps(newProps){
this.setState({cartTimeout: newProps.cartTimeout});
},
handleChange(event) {
this.setState({ deliveryOption: event.target.value});
},
handleSubmit(event) {
event.preventDefault();
this.props.updateFormData(this.state);
},
render() {
var minutes = Math.floor(this.state.cartTimeout / 60);
var seconds = this.state.cartTimeout - minutes * 60;
return (
<div>
<h1>Choose your delivery options here.</h1>
<div style={{width:200}}>
<form onSubmit={this.handleSubmit}>
<div className="radio">
<label>
<input type="radio"
checked={this.state.deliveryOption === "Primary"}
value="Primary"
onChange={this.handleChange} />
Primary -- Next day delivery
</label>
</div>
<div className="radio">
<label>
<input type="radio"
checked={this.state.deliveryOption === "Normal"}
value="Normal"
onChange={this.handleChange} />
Normal -- 3-4 days
</label>
</div>
<button className="btn btn-success">
Submit
</button>
</form>
</div>
<div className='well'>
<span className="glyphicon glyphicon-time" aria-hidden="true"></span> You have {minutes} Minutes, {seconds} Seconds, before confirming order
</div>
</div>
);
}
});
module.exports = DeliveryDetails;
"我们还将更新我们的ShippingDetails组件,使其看起来像以下这样:"
import React from 'react';
import SetIntervalMixin from './mixins/set_interval_mixine'
import CartTimeoutMixin from './mixins/cart_timeout_mixin'
var ShippingDetails = React.createClass({
propTypes: {
alertCartTimeout:React.PropTypes.func.isRequired,
updateCartTimeout: React.PropTypes.func.isRequired,
cartTimeout: React.PropTypes.number.isRequired
},
mixins: [SetIntervalMixin, CartTimeoutMixin],
getInitialState() {
return {fullName: '', contactNumber: '', shippingAddress: '', error: false, cartTimeout: this.props.cartTimeout};
},
_renderError() {
if (this.state.error) {
return (
<div className=e"alert alert-danger">
{this.state.error}
</div>
);
}
},
_validateInput() {
…..
},
handleSubmit(event) {
….
},
handleChange(event, attribute) {
var newState = this.state;
newState[attribute] = event.target.value;
this.setState(newState);
console.log(this.state);
},
render() {
var errorMessage = this._renderError();
var minutes = Math.floor(this.state.cartTimeout / 60);
var seconds = this.state.cartTimeout - minutes * 60;
return (
<div>
<h1>Enter your shipping information.</h1>
…..
<div className='well'>
<span className="glyphicon glyphicon-time" aria-hidden="true"></span> You have {minutes} Minutes, {seconds} Seconds, before confirming order
</div>
</div>
);
}
});
module.exports = ShippingDetails;
"现在它应该看起来像以下截图所示:"
"太棒了,”肖恩兴奋地说。
"在超时的情况下,我们有简单的显示:"
添加模态框
"好吧,这工作得很好,”迈克继续说。
"然而,目前这有点笨拙。超时后,用户无法进行任何操作。我们可以添加一个弹出窗口来通知用户。而不是显示错误页面,让我们显示一个带有警告的模态框,并将用户重定向到第一页,这样用户就可以重新开始流程。我们可以使用 Bootstrap 模态框来实现这一点。"
"明白了。你想让我试一试吗?"肖恩问道。
"继续!"
"让我先从设置模态框开始。我将使用一个简单的 Bootstrap 模态框来显示它。完成之后,我需要从alertCartTimeout调用模态框的显示。我想我还需要设置显示第一页和重置表单数据。"
"正确。"
"这就是模态框将呈现的样子"
import React from 'react';
var ModalAlertTimeout = React.createClass({
render() {
return (
<div className="modal fade" ref='timeoutModal'>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 className="modal-title">Timeout</h4>
</div>
<div className="modal-body">
<p>The cart has timed-out. Please try again!</p>
</div>
</div>
</div>
</div>
);
}
});
module.exports = ModalAlertTimeout;
"很好。接下来,你将更新Bookstore组件的alertCartTimeout方法。"
"是的,我在 body 中添加了一个新的空 HTML 元素,ID 为modalAlertTimeout。这将用于显示新的模态框并在其上方挂载组件。我还将更改警告超时方法如下:"
alertCartTimeout(){
React.render(<ModalAlertTimeout />, document.getElementById('modalAlertTimeout'));
this.setState({currentStep: 1, formValues: {}, cartTimeout: 1});
}
"啊,让我们看看这个做了什么"迈克继续说,检查肖恩所做的更改。
"肖恩,看起来超时将我们带到了第一页,但没有显示模态警告"
"哦,对了。我们仍然需要从 Bootstrap 调用模态框的显示。"
"正确。让我来处理这个问题,肖恩。在我们的ModalAlertTimeout中,我们将在组件成功挂载后添加一个方法调用以显示模态框,如下所示:"
componentDidMount(){
setTimeout(()=> {
let timeoutModal = this.refs.timeoutModal.getDOMNode();
$(timeoutModal).modal('show');
}, 100);
}
"啊,我明白了我们在这里做了一些 DOM 操作。"
"是的,让我过一遍它们。"
引用
"我想我们之前用过这个,”肖恩问道。"
"是的。引用的作用是给我们一个引用组件某部分的句柄。我们在表单中做过这个。在这里,我们使用它来获取对模态框的引用,这样我们就可以在上面调用modal()方法。"
"这将反过来显示模态框。"
"现在,注意我们是如何使用getDOMNode()方法的。"
"是的。它做什么?"
"getDOMNode()方法帮助我们获取渲染 React 元素的底层 DOM 节点。在我们的例子中,我们想在 DOM 节点上调用一个方法。"
"当我们调用this.refs.timeoutModal时,它返回给我们一个组件的引用对象。"
"这与实际的 DOM 组件不同。它实际上是一个 React 包装的对象。为了获取底层的 DOM 对象,我们调用了getDOMNode()。"
"明白了。"
"接下来,我们将所有这些包裹在一个setTimeout调用中,这样我们就可以在 React 组件成功渲染并且模态框内容存在于页面上时调用它。"
"最后,我们调用了$(timeoutModal).modal('show')来调用模态框!"
"让我们看看我们的模态框现在看起来怎么样。"
import React from 'react';
var ModalAlertTimeout = React.createClass({
componentDidMount(){
setTimeout(()=> {
let timeoutModal = this.refs.timeoutModal.getDOMNode();
$(timeoutModal).modal('show');
}, 100);
},
render() {
return (
<div className="modal fade" ref='timeoutModal'>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 className="modal-title">Timeout</h4>
</div>
<div className="modal-body">
<p>The cart has timed-out. Please try again!</p>
</div>
</div>
</div>
</div>
);
}
});
module.exports = ModalAlertTimeout;
"让我们看看现在看起来怎么样。"
"既然我们正在讨论这个,还有一个关于 DOM 的问题。我们可以调用getDOMNode()来获取当前组件的节点。因此,我们可以简单地调用this.getDOMNode(),这将返回给我们一个元素!"
"好的,让我们这么做。当有人关闭模态时,我们将卸载模态,以便在第二次渲染时重新调用它。"
"让我们定义一个回调方法来完成这个任务,如下:"
unMountComponent () {
React.unmountComponentAtNode(this.getDOMNode().parentNode);
}
"最后,我们将设置一个回调函数在模态关闭时执行,如下:"
$(timeoutModal).on('hidden.bs.modal', this.unMountComponent);
"这样一来,我们就完成了!当模态隐藏时,组件将会卸载。"
"注意我们是如何在 DOM 节点上使用parentNode属性来隐藏模态的。这有助于我们获取包含 React 元素的容器,并使用它来移除模态。"
"太好了。这真是一次很好的复习。谢谢 Mike!"
随着这些,这对搭档回到了检查他们刚刚所做的各种更改。
摘要
在本章中,我们探讨了重构组件。我们学习了如何利用混入(mixins)提取相似的功能,以便在组件间无缝使用。我们还研究了 DOM 交互,使用 refs 以及从组件中执行相关的 DOM 操作。
在下一章中,我们将探讨 React 在服务器端的运行方式。我们将看到 React 如何允许我们在服务器上渲染和处理组件,以预渲染 HTML,这对于多个原因来说是有用的。我们还将研究这如何影响 React 组件的生命周期。
第六章。React 在服务器端
在上一章中,我们查看了对组件的重构。我们看到了如何使用混入(mixins)提取相似的功能,以便在组件之间无缝使用。我们还查看了一下 DOM 交互,使用 refs 以及从组件中相关的 DOM 操作。
在本章中,我们将探讨 React 在服务器端的运行方式。React 允许我们在服务器上渲染和处理组件,以预渲染 HTML,这有几个原因很有用。我们还将看看这如何影响 React 组件的生命周期。
在本章中,我们将涵盖以下要点:
-
服务器端渲染
-
渲染函数
-
服务器端组件生命周期
在本章结束时,我们将能够开始在服务器端使用 React 组件,并理解其与服务器端的交互和影响。
让 React 在服务器上渲染
"嘿,肖恩!" 迈克带着一杯咖啡闯入他们的工作场所,吓了肖恩一跳。
"早上好,迈克,”肖恩回答道。
阳光在肖恩的桌子上闪耀,他们开始讨论即将开始的新项目。
"肖恩,我从卡拉那里刚刚得知,我们有一个新的项目需要承担。客户要求我们为我们的 Open Library 项目构建一个简单的搜索页面。"
肖恩和迈克之前构建了一个应用程序,用于显示来自openlibrary.com API 的最新更改。他们现在将基于 Open Library 的搜索 API 构建一个搜索应用程序。
"太棒了,”迈克对此兴奋不已。他已经非常喜欢在 React 上工作了。
"肖恩,对于这个项目,我们将探讨如何在服务器上使用 React 的选项。"
到目前为止,我们一直在页面加载后手动挂载我们的组件。直到组件被渲染,页面都没有任何来自组件的 HTML 内容。
"让我们看看我们如何在服务器上完成这项工作,以便在页面完全加载后预先生成 HTML。"
"明白了。那么,服务器端组件的渲染有什么好处呢?"
"这有几个有用的原因。其中一个原因是我们在服务器上生成内容。这对于 SEO 目的和更好的搜索引擎索引很有用。"
"由于内容是在服务器上生成的,第一次渲染将立即显示页面,而不是等待页面加载完全完成后才正确渲染组件。"
"这也帮助我们避免了页面加载时的闪烁效果。还有其他这样的优点我们可以利用,我们将在稍后探讨,”迈克解释道。
"好。那我们就开始吧。"
"好的!对于这个项目,让我们从一个入门 Webpack 项目开始,以管理我们的代码。对于服务器端元素,我们将使用 Express JS。我们在这里不会做任何复杂的事情,我们只是简单地从 Express JS 中暴露一个路由,并渲染一个包含我们的组件的.ejs视图。"
"这样的入门级项目示例可以在webpack.github.io/网站上找到,"迈克告知。
"很好,我想我们也会在客户端/服务器端划分代码?"
"是的。让我们将它们放在/app目录下以组成我们的组件,/client用于客户端特定的代码,/server用于我们/src目录中的服务器端代码,"迈克继续说道。
"接下来,我们将在/app/server目录中设置server.js文件。"
import path from 'path';
import Express from 'express';
var app = Express();
var server;
const PATH_STYLES = path.resolve(__dirname, '../client/styles');
const PATH_DIST = path.resolve(__dirname, '../../dist');
app.use('/styles', Express.static(PATH_STYLES));
app.use(Express.static(PATH_DIST));
app.get('/', (req, res) => {
res.sendFile(path.resolve(__dirname, '../client/index.html'));
});
server = app.listen(process.env.PORT || 3000, () => {
var port = server.address().port;
console.log('Server is listening at %s', port);
});
"这是一个相当标准的 Express 应用程序设置。我们指定了要使用的样式,静态资源路径等等。"
"对于我们的路由,我们通过这样做来简单地暴露根/:"
app.get('/', (req, res) => {
res.sendFile(path.resolve(__dirname, '../client/index.html'));
});
"我们要求 Express 在请求根目录时服务index.html文件。在我们的index.js文件中,我们将将其传递给 node 以运行应用程序,我们只需简单地暴露我们刚刚编写的服务器模块。"
require('babel/register');
module.exports = require('./server');
"迈克,为什么在这里需要babel/register?"
"哦,对了。在这里,我们引入 Babel(babeljs.io/)将我们的文件转换为浏览器兼容的格式。我们正在使用一些 JavaScript ES2015 语法的好处。Babel 通过语法转换器帮助我们添加对 JavaScript 最新版本的支持。这允许我们使用目前浏览器不支持的最新的 JavaScript 语法。"
"有了这个设置,我们将定义我们的index.html如下:"
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Search</title>
<link href="styles/main.css" rel="stylesheet" />
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
</head>
<body>
<div id="app"></div>
<script src="img/bundle.js"></script>
</body>
</html>
"这里没有什么特别的。我们只是在顶部定义一个 div,我们将在其上渲染 React 组件。"
"此外,请注意,我们已经包含了添加 Bootstrap 和 Font Awesome 支持到我们的应用程序的文件链接。"
"接下来,在客户端渲染处理方面,我们将做"
// file: scr/client/scripts/client.js
import App from '../../app';
var attachElement = document.getElementById('app');
var state = {};
var app;
// Create new app and attach to element
app = new App({ state: state});
app.renderToDOM(attachElement);
"最后,让我们在移动到我们的实际组件之前看看这里定义的App类是如何使用的。"
import React from 'react/addons';
import AppRoot from './components/AppRoot';
class App {
constructor(options) {
this.state = options.state;
}
render(element) {
var appRootElement = React.createElement(AppRoot, {
state: this.state
});
// render to DOM
if (element) {
React.render(appRootElement, element);
return;
}
// render to string
return React.renderToString(appRootElement);
}
renderToDOM(element) {
if (!element) {
new Error('App.renderToDOM: element is required');
}
this.render(element);
}
renderToString() {
return this.render();
}
}
export default App;
"哇,这需要消化很多东西,"肖恩叹了口气。
"哈哈!给它一些时间。我们在这里所做的只是处理我们的渲染逻辑。如果我们向这个类传递一个元素,内容将被渲染到它上面;否则,我们将返回渲染后的字符串版本。注意我们是如何使用React.renderToString来实现相同功能的。让我们先完成这个,然后我们将在使用它来在服务器请求上渲染内容时再次回顾它。"
"简而言之,我们只是要求 React 接收一个组件的状态,渲染它,并将render()方法将渲染的内容作为字符串返回。"
"然后我们将开始定义我们的根容器组件。"
require("jquery");
import React from 'react/addons';
import SearchPage from './SearchPage'
var AppRoot = React.createClass({
propTypes: {
state: React.PropTypes.object.isRequired
},
render()
{
return <SearchPage/>;
}
})
;
export default AppRoot;
"在这里,我们简单地定义一个容器来存放我们的主组件,并引入所有依赖。让我们接下来构建我们的搜索组件。"
"太棒了。我想我可以处理这个。看起来这只是一个简单的组件?"
"是的。继续吧,"迈克回应道。
"好的,我明白了我们需要从 Open Library API 端点获取数据。"
openlibrary.org/search.json?page=1&q=searchTerm
"在这里,q 查询参数将是搜索词。一个示例响应看起来像:"
{
"start": 0,
"num_found": 6,
"numFound": 6,
"docs": [
{
"title_suggest": "Automatic search term variant generation for document retrieval",
"edition_key": [
..
],
…
],
"author_name": [
..
..}]
}
"没错," Mike 补充说。
"我想我会从根据 start、num_found 和 docs 字段定义初始状态开始," Shawn 说
"好的。"
getInitialState(){
return {docs: [], numFound: 0, num_found: 0, start: 0, searchCompleted: false, searching: false}
}
"我还添加了两个其他状态,我将保持它们:searchCompleted 以知道当前搜索操作是否已完成,以及 searching 以知道我们目前正在搜索某物。"
"酷。让我们看看下一个渲染方法," Mike 继续说。
"让我先在 render 方法中添加搜索框。"
render() {
let tabStyles = {paddingTop: '5%'};
return (
<div className='container'>
<div className="row" style={tabStyles}>
<div className="col-lg-8 col-lg-offset-2">
<div className="input-group">
<input type="text" className="form-control" placeholder="Search for Projects..." ref='searchInput'/>
<span className="input-group-btn">
<button className="btn btn-default" type="button" onClick={this.performSearch}>Go!</button>
</span>
</div>
</div>
</div>
</div>
);
},
"我们现在应该有一个搜索框的显示。"
"接下来,我们将添加 performSearch 方法,它基于用户输入的搜索词启动搜索。"
performSearch(){
let searchTerm = $(this.refs.searchInput.getDOMNode()).val();
this.openLibrarySearch(searchTerm);
this.setState({searchCompleted: false, searching: true});
},
"在这里,我们只是获取用户输入的搜索词,并将其传递给 openLibrarySearch 方法,它将实际执行搜索。然后,我们更新状态,表明我们现在正在积极执行搜索。"
"现在让我们完成搜索功能。"
openLibrarySearch(searchTerm){
let openlibraryURI = `https://openlibrary.org/search.json?page=1&q=${searchTerm}}`;
fetch(openlibraryURI)
.then(this.parseJSON)
.then(this.updateState)
.catch(function (ex) {
console.log('Parsing failed', ex)
})
}
"啊,好,Shawn,你正在使用 fetch 而不是常规的 Ajax!"
"嗯,是的。我一直在使用 github.com/github/fetch 作为 window.fetch 规范的 polyfill。"
"不错,不是吗?它支持简单且干净的 API,如 Ajax,以及统一的获取 API。"
在获取某些资源或请求完成后,回调会通过 then 方法执行。注意,我们还在构建 URI 时使用了 ES2015 字符串字面量," Shawn 补充说。
"酷。看起来你正在获取资源,然后将其传递给 parseJSON 以解析并从响应体中返回 JSON 结果。然后,我们是否在它之上更新状态?"
"是的,让我定义那些"
parseJSON(response) {
return response.json();
},
// response.json() is returning the JSON content from the response.
updateState(json){
this.setState({
...json,
searchCompleted: true,
searching: false
});
},
"在获取最终响应后,我正在更新和设置返回的结果状态,以及更新我们的 searchCompleted 和 searching 状态,以表明搜索工作已完成。"
"啊,好,Shawn,我看到你已经开始采用并使用 JS Next! 的新特性,比如 spread 操作符。"
"哈哈,是的。我已经爱上了这些。我正在使用这个来合并 JSON 结果的属性和我想添加的新键,并构建一个新的对象。这也会使用我们之前看到的 Object.assign 以类似的方式完成。"
Object.assign({}, json, {searchCompleted: true, searching: false} )
"这样,我们是在构建一个新的对象,而不是修改先前的对象。"
"Shawn 很棒,Mike 愉快地知道 Shawn 正在掌握新事物。"
"最后,让我添加加载动作显示,以显示加载器图标和实际结果的显示。现在渲染方法将看起来像这样。"
render() {
let tabStyles = {paddingTop: '5%'};
return (
<div className='container'>
<div className="row" style={tabStyles}>
<div className="col-lg-8 col-lg-offset-2">
<div className="input-group">
<input type="text" className="form-control" placeholder="Search for Projects..." ref='searchInput'/>
<span className="input-group-btn">
<button className="btn btn-default" type="button" onClick={this.performSearch}>Go!</button>
</span>
</div>
</div>
</div>
{ (() => {
if (this.state.searching) {
return this.renderSearching();
}
return this.state.searchCompleted ? this.renderSearchElements() : <div/>
})()}
</div>
);
},
"在这里,我们正在检查搜索操作当前的状态。基于此,我们正在显示实际内容、结果或空 div 元素的加载器。"
"让我定义元素的加载和渲染。"
renderSearching(){
return <div className="row">
<div className="col-lg-8 col-lg-offset-2">
<div className='text-center'><i className="fa fa-spinner fa-pulse fa-5x"></i></div>
</div>
</div>;
},
"这将定义旋转按钮的显示,以指示正在加载。"
renderSearchElements(){
return (
<div className="row">
<div className="col-lg-8 col-lg-offset-2">
<span className='text-center'>Total Results: {this.state.numFound}</span>
<table className="table table-stripped">
<thead>
<th>Title</th>
<th>Title suggest</th>
<th>Author</th>
<th>Edition</th>
</thead>
<tbody>
{this.renderDocs(this.state.docs)}
</tbody>
</table>
</div>
</div>
);
},
renderDocs(docs){
return docs.map((doc) => {
console.log(doc);
return <tr key={doc.cover_edition_key}>
<td>{doc.title}</td>
<td>{doc.title_suggest}</td>
<td>{(doc.author_name || []).join(', ')}</td>
<td>{doc.edition_count}</td>
</tr>
})
},
"添加这个之后,搜索操作应该会显示一个类似这样的加载器。"
显示的结果将如下所示:
"完成的SearchPage组件如下:"
import React from 'react';
var SearchPage = React.createClass({
getInitialState(){
return {docs: [], numFound: 0, num_found: 0, start: 0, searchCompleted: false, searching: false}
},
render() {
let tabStyles = {paddingTop: '5%'};
return (
<div className='container'>
<div className="row" style={tabStyles}>
<div className="col-lg-8 col-lg-offset-2">
<div className="input-group">
<input type="text" className="form-control" placeholder="Search for Projects..." ref='searchInput'/>
<span className="input-group-btn">
<button className="btn btn-default" type="button" onClick={this.performSearch}>Go!</button>
</span>
</div>
</div>
</div>
{ (() => {
if (this.state.searching) {
return this.renderSearching();
}
return this.state.searchCompleted ? this.renderSearchElements() : <div/>
})()}
</div>
);
},
renderSearching(){
return <div className="row">
<div className="col-lg-8 col-lg-offset-2">
<div className='text-center'><i className="fa fa-spinner fa-pulse fa-5x"></i></div>
</div>
</div>;
},
renderSearchElements(){
return (
<div className="row">
<div className="col-lg-8 col-lg-offset-2">
<span className='text-center'>Total Results: {this.state.numFound}</span>
<table className="table table-stripped">
<thead>
<th>Title</th>
<th>Title suggest</th>
<th>Author</th>
<th>Edition</th>
</thead>
<tbody>
{this.renderDocs(this.state.docs)}
</tbody>
</table>
</div>
</div>
);
},
renderDocs(docs){
return docs.map((doc) => {
console.log(doc);
return <tr key={doc.cover_edition_key}>
<td>{doc.title}</td>
<td>{doc.title_suggest}</td>
<td>{(doc.author_name || []).join(', ')}</td>
<td>{doc.edition_count}</td>
</tr>
})
},
performSearch(){
let searchTerm = $(this.refs.searchInput.getDOMNode()).val();
this.openLibrarySearch(searchTerm);
this.setState({searchCompleted: false, searching: true});
},
parseJSON(response) { return response.json(); },
updateState(json){
this.setState({
...json,
searchCompleted: true,
searching: false
});
},
openLibrarySearch(searchTerm){
let openlibraryURI = `https://openlibrary.org/search.json?page=1&q=${searchTerm}}`;
fetch(openlibraryURI)
.then(this.parseJSON)
.then(this.updateState)
.catch(function (ex) {
console.log('Parsing failed', ex)
})
}
});
module.exports = SearchPage;
"如果你注意到,我使用立即调用的函数添加了一个if语句来显示搜索图标渲染,如下所示:"
{ (() => {
if (this.state.searching) {
return this.renderSearching();
}
return this.state.searchCompleted ? this.renderSearchElements() : <div/>
})()}
"在这里,我们使用了()=>{}语法首先定义函数,然后立即调用它(()=>{}))(),返回我们在渲染期间需要显示的内容。"
"干得好,肖恩!" 迈克对肖恩取得的进展感到高兴。
"这很方便,当我们想在渲染本身中添加简单的逻辑开关时,而不是定义新的方法,"迈克继续说。
在服务器上
"现在肖恩,让我们在服务器上预渲染组件。这意味着从 React 组件创建一个 HTML 元素,并在第一次页面加载时本身渲染其内容。目前,元素的加载由客户端代码处理。"
app.renderToDOM(attachElement);
"而不是这样,我们将在 Express 动作本身中渲染 React 元素。"
"首先,让我们设置一个.ejs视图来显示我们的 HTML 内容以及动态生成的 React 内容。"
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Search</title>
<link href="styles/main.css" rel="stylesheet" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
</head>
<body>
<div id="app">
<%- reactOutput %>
</div>
<script src="img/bundle.js"></script>
</body>
</html>
"在这里,我们将reactOutput作为变量传递给视图进行渲染。"
"我们现在将修改server.js文件,以包含所需的组件和 React 进行渲染。"
import AppRoot from '../app/components/AppRoot'
import React from 'react/addons';
"我们的动作将变为:"
app.get('/', (req, res) => {
var reactAppContent = React.renderToString(<AppRoot state={{} }/>);
console.log(reactAppContent);
res.render(path.resolve(__dirname, '../client/index.ejs'), {reactOutput: reactAppContent});
});
"我们的最终服务器代码将如下所示。"
import path from 'path';
import Express from 'express';
import AppRoot from '../app/components/AppRoot'
import React from 'react/addons';
var app = Express();
var server;
const PATH_STYLES = path.resolve(__dirname, '../client/styles');
const PATH_DIST = path.resolve(__dirname, '../../dist');
app.use('/styles', Express.static(PATH_STYLES));
app.use(Express.static(PATH_DIST));
app.get('/', (req, res) => {
var reactAppContent = React.renderToString(<AppRoot state={{} }/>);
console.log(reactAppContent);
res.render(path.resolve(__dirname, '../client/index.ejs'), {reactOutput: reactAppContent});
});
server = app.listen(process.env.PORT || 3000, () => {
var port = server.address().port;
console.log('Server is listening at %s', port);
});
"这里就是了!我们正在使用 React 的renderToString方法来渲染一个组件,如果需要,可以传递任何状态,以伴随它。"
摘要
在本章中,我们探讨了如何使用Express.js的帮助,将服务器端渲染与 React 结合使用。我们从一个客户端 React 组件开始,最后使用 React API 提供的方法将其替换为服务器端渲染。
在下一章中,我们将探讨 React 插件,用于执行双向绑定、类名操作、组件克隆、不可变辅助工具和 PureRenderMixin,同时继续在本章中构建的搜索项目。
第七章。React 插件
在上一章中,我们学习了如何在服务器端使用 React。我们了解了在服务器端使用 React 时 React 组件的预渲染以及组件生命周期的变化。我们还看到了如何使用 Express.js 调用 React 的服务器端 API。
在本章中,我们将探讨 React 插件——这些不是 React 核心库的一部分的实用程序包,但它们使开发过程变得有趣和愉快。我们将学习使用不可变辅助工具、组件克隆和测试实用工具。我们不会涵盖其他插件,如Animation、Perf和PureRenderMixin。这些插件将在下一章中介绍。
在本章中,我们将涵盖以下主题:
-
开始学习 React 插件
-
不可变辅助工具
-
克隆 React 组件
-
测试辅助工具
开始学习插件
在完成关于在服务器端使用 React 的上一项目之后,迈克的团队在开始下一个项目之前有一些空闲时间。迈克决定利用这段时间来学习 React 插件。
"肖恩,我们有了一些空闲时间。让我们利用这段时间开始学习 React 插件。"
"什么是 React 插件?它们与 React 核心库有关吗?"肖恩问道。
"React 插件是 React 核心库之外的实用模块。然而,它们得到了 React 团队的认可。在未来,其中一些可能会被包含在 React 核心中。这些库提供了编写不可变代码的辅助工具、测试 React 应用的实用工具以及衡量和改进 React 应用性能的方法。"迈克解释道。
"每个插件都有自己的 npm 包,这使得使用变得非常简单。例如,要使用 Update 插件,我们需要安装并引入其 npm 包。"
$ npm install react-addons-update --save
// src/App.js
import Update from 'react-addons-update';
不可变辅助工具
"肖恩,随着我们学习插件,让我们给我们的应用添加排序功能,这样用户就可以按标题排序书籍。我已经添加了所需的标记。"
"你能尝试编写当用户点击标题时进行排序的代码吗?"马克问道。
"这就是它。我引入了排序状态来指示排序方向——升序或降序。"
// Updated getInitialState function of App component
// src/App.js
getInitialState(){
return { books: [],
totalBooks: 0,
searchCompleted: false,
searching: false,
sorting: 'asc' };
}
"当用户点击标题时,它将使用 sort-by npm 包按升序或降序对现有状态中存储的书籍进行排序,并使用排序后的书籍更新状态。"
import sortBy from 'sort-by';
_sortByTitle() {
let sortByAttribute = this.state.sorting === 'asc' ? "title" : "-title";
let unsortedBooks = this.state.books;
let sortedBooks = unsortedBooks.sort(sortBy(sortByAttribute));
this.setState({ books: sortedBooks,
sorting: this._toggleSorting() });
},
_toggleSorting() {
return this.state.sorting === 'asc' ? 'desc' : 'asc';
}
"肖恩,这是功能性的;然而,它并不遵循 React 的方式。React 假设状态对象是不可变的。当我们为unsortedBooks赋值时,我们正在使用现有状态中对书籍的引用。"
let unsortedBooks = this.state.books;
"稍后,我们将unsortedBooks转换为sortedBooks;然而,作为副作用,我们也在修改this.state的当前值。"
_sortByTitle() {
let sortByAttribute = this.state.sorting === 'asc' ? "title" : "-title";
let unsortedBooks = this.state.books;
console.log("Before sorting :");
console.log(this.state.books[0].title);
let sortedBooks = unsortedBooks.sort(sortBy(sortByAttribute));
console.log("After sorting :");
console.log(this.state.books[0].title);
// this.setState({ books: sortedBooks,
sorting: this._toggleSorting() });
},
"正如你所见,即使我们注释掉了对 this.setState的调用,我们的当前状态仍然被修改了。"马克解释道。
"这可以通过使用 ES6 中的 Object.assign 轻易地修复。我们可以简单地创建一个新的数组,并将 this.state.books 的当前值复制到其中。然后我们可以对新的数组进行排序,并使用新的排序数组调用 setState。" 肖恩告知。
注意
Object.assign 方法将多个源对象的所有可枚举属性值复制到目标对象。更多详细信息可以在以下链接找到:developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign。
_sortByTitle() {
let sortByAttribute = this.state.sorting === 'asc' ? "title" : "-title";
let unsortedBooks = Object.assign([], this.state.books);
console.log("Before sorting :");
console.log(this.state.books[0].title);
let sortedBooks = unsortedBooks.sort(sortBy(sortByAttribute));
console.log("After sorting :");
console.log(this.state.books[0].title);
this.setState({ books: sortedBooks,
sorting: this._toggleSorting() });
}
"是的。这行得通。但是 Object.assign 方法将创建 this.state.books 的浅拷贝。它将创建一个新的 unsortedBooks 对象,然而,它仍然会在 unsortedBooks 中使用 this.state.books 的相同引用。假设,由于某种原因,我们想要将所有书籍的标题转换为大写字母,那么我们可能会意外地变异 this.state," 迈克解释道。
_sortByTitle() {
let sortByAttribute = this.state.sorting === 'asc' ? "title" : "-title";
let unsortedBooks = Object.assign([], this.state.books);
unsortedBooks.map((book) => book.title = book.title.toUpperCase());
console.log("unsortedBooks");
console.log(unsortedBooks[0].title);
console.log("this.state.books");
console.log(this.state.books[0].title);
}
"正如您所看到的,即使使用了 Object.assign,this.state.books 仍然发生了变异。实际上,这与 React 本身无关。这是由于 JavaScript 传递数组和对象引用的方式造成的。然而,由于这个原因,如果我们深层次的状态中有数组和对象,就很难防止变异。" 迈克进一步解释道。
"我们是否总是必须执行深拷贝状态对象以确保安全?" 肖恩问道。
"嗯,深拷贝可能很昂贵,有时在深层次嵌套的状态中很难实现。幸运的是,React 提供了带有不可变辅助工具的 Update 插件,我们可以使用它来解决这个问题的。" 迈克补充道。
"在使用不可变辅助工具时,我们需要回答以下三个问题:"
-
需要更改什么?
-
需要在哪里更改?
-
需要如何更改?
"在这种情况下,我们需要更改 this.state 以显示排序后的书籍。"
"第二个问题是,this.state 内部应该在哪个位置发生变异?变异应该发生在 this.state.books 上。"
"第三个问题是变异应该如何发生?我们是打算删除某些内容,还是添加新的元素,或者重构现有元素?在这种情况下,我们想要根据某些标准对元素进行排序。"
"Update 插件接受两个参数。第一个参数是我们想要变异的对象。第二个参数告诉我们第一个参数中变异应该发生在哪里以及如何进行。在这种情况下,我们想要变异 this.state。在 this.state 中,我们想要用排序后的书籍更新书籍。因此,我们的代码将类似于以下内容:"
Update(this.state, { books: { sortedBooks }})
可用命令
Update 插件提供了不同的命令来执行数组和对象的变异。这些命令的语法受到了 MongoDB 查询语言的启发。
"大多数这些命令都操作数组对象,允许我们向数组中推入一个数组或从数组中 unshift 一个元素。它还支持使用 set 命令完全替换目标。除此之外,它还提供了 merge 命令来合并新键与现有对象。" 迈克解释道。
"肖恩,这个插件提供的我最喜欢的命令是 apply。它接受一个函数,并将该函数应用于我们想要修改的目标的当前值。它为在复杂数据结构中做出更改提供了更多的灵活性。这也是你下一个任务的提示。尝试使用它来按标题对书籍进行排序,而不进行修改。" 迈克提出了挑战。
"当然。给你," 肖恩说。
// src/App.js
import Update from 'react-addons-update';
_sortByTitle() {
let sortByAttribute = this.state.sorting === 'asc' ? "title" : "-title";
console.log("Before sorting");
console.log(this.state.books[0].title);
let newState = Update(this.state,
{ books: { $apply: (books) => { return books.sort(sortBy(sortByAttribute)) } },
sorting: { $apply: (sorting) => { return sorting === 'asc' ? 'desc' : 'asc' } } });
console.log("After sorting");
console.log(this.state.books[0].title);
this.setState(newState);
}
"太好了,肖恩。使用 Update 插件使得管理复杂状态变得非常容易,而实际上并没有修改它。"
小贴士
查阅facebook.github.io/immutable-js/docs/以获取在 JavaScript 中使用不可变数据结构的完整解决方案。
克隆组件
"肖恩,在 React 中,props 也是不可变的。在大多数情况下,子组件只是使用父组件传递的 props。然而,有时我们在渲染子组件之前想要扩展传入的 props 以添加一些新数据。这是一个典型的用例,用于更新样式和 CSS 类。React 提供了一个插件来克隆组件并扩展其 props。它被称为cloneWithProps插件。" 迈克说。
"迈克,这个插件已经过时了。我以前看过它,React 已经将其弃用。它还与子组件的 refs 未传递给新克隆的子组件等问题有关。" 肖恩通知说。
"确实如此。然而,React 还有一个顶级的React.cloneElement API 方法,它允许我们克隆并扩展一个组件。它有一个非常简单的 API,并且可以用作 cloneWithProps 插件的替代品。" 迈克解释道。
React.cloneElement(element, props, …children);
"这个方法可以用来克隆给定的元素,并将新属性与现有属性合并。它用新子元素替换现有子元素。因此,在处理子组件时,我们必须记住这一点。"
"cloneElement函数还会将旧子组件的 ref 传递给新克隆的组件。因此,如果我们有任何基于 refs 的回调,它们在克隆后仍然会继续工作。"
"肖恩,你的下一个挑战来了。我们在我们的应用中显示书籍列表。实际上,在我们的所有应用中,我们都显示其他事物的列表,如产品、项目等。我们希望在所有这些列表中显示带有交替颜色的行,而不是只有白色背景。因为这个功能的代码将在所有应用中都是相同的,所以我正在考虑创建一个单独的组件,该组件将接受行作为输入,并以交替颜色渲染它们。这样的组件可以用于我们的所有应用。我认为你可以使用React.cloneElement来完成这个任务。" 迈克解释了下一个任务。
"将这个功能提取为一个单独的组件听起来是个好主意。我们几乎在所有应用中都需要它。昨天我们的 QA 团队抱怨我们的搜索应用缺少颜色。" 肖恩回忆道。
"让我们添加一些交替颜色吧。" 迈克轻声笑了。
"首先,让我们看看我们目前是如何显示书籍的。"
// src/App.js
render() {
let tabStyles = {paddingTop: '5%'};
return (
<div className='container'>
<div className="row" style={tabStyles}>
<div className="col-lg-8 col-lg-offset-2">
<h4>Open Library | Search any book you want!</h4>
<div className="input-group">
<input type="text" className="form-control" placeholder="Search books..." ref='searchInput'/>
<span className="input-group-btn">
<button className="btn btn-default" type="button" onClick={this._performSearch}>Go!</button>
</span>
</div>
</div>
</div>
{this._displaySearchResults()}
</div>
);
},
_displaySearchResults() {
if(this.state.searching) {
return <Spinner />;
} else if(this.state.searchCompleted) {
return (
<BookList
searchCount={this.state.totalBooks}
_sortByTitle={this._sortByTitle}>
{this._renderBooks()}
</BookList>
);
}
}
_renderBooks() {
return this.state.books.map((book, idx) => {
return (
<BookRow key={idx}
title={book.title}
author_name={book.author_name}
edition_count={book.edition_count} />
);
})
},
})
}
"BookList 组件只是按照原样渲染传递给它的行,因为它使用了 this.props.children。"
// BookList component
var BookList = React.createClass({
render() {
return (
<div className="row">
<div className="col-lg-8 col-lg-offset-2">
<span className='text-center'>
Total Results: {this.props.searchCount}
</span>
<table className="table table-stripped">
<thead>
<tr>
<th><a href="#" onClick={this.props._sortByTitle}>Title</a></th>
<th>Author</th>
<th>No. of Editions</th>
</tr>
</thead>
<tbody>
{this.props.children}
</tbody>
</table>
</div>
</div>
);
}
});
"迈克,我打算将这个组件命名为 RowAlternator。RowAlternator 组件将获取动态的子行数组,并以交替颜色渲染它们。我们也可以向 RowAlternator 传递多个颜色。这样,使用这个组件的客户端代码就可以控制它们想要使用的交替颜色。"
"听起来不错,肖恩。我认为现在这个 API 已经足够了。"
// RowAlternator component
import React from 'react';
var RowAlternator = React.createClass({
propTypes: {
firstColor: React.PropTypes.string,
secondColor: React.PropTypes.string
},
render() {
return (
<tbody>
{ this.props.children.map((row, idx) => {
if (idx %2 == 0) {
return React.cloneElement(row, { style: { background: this.props.firstColor }});
} else {
return React.cloneElement(row, { style: { background: this.props.secondColor }});
}
})
}
</tbody>
)
}
});
module.exports = RowAlternator;
"由于我们不知道在 RowAlternator 中我们会得到多少子元素,所以我们只是遍历所有元素并使用交替颜色设置样式。我们在这里也使用了 React.cloneElement 来克隆传递的子元素,并使用适当的背景颜色扩展其样式属性。"
"现在让我们更改 BookList 组件,以便使用 RowAlternator。"
// BookList component
import RowAlternator from '../src/RowAlternator';
var BookList = React.createClass({
render() {
return (
<div className="row">
<div className="col-lg-8 col-lg-offset-2">
<span className='text-center'>
Total Results: {this.props.searchCount}
</span>
<table className="table table-stripped">
<thead>
<tr>
<th><a href="#" onClick={this.props._sortByTitle}>Title</a></th>
<th>Author</th>
<th>No. of Editions</th>
</tr>
</thead>
<RowAlternator firstColor="white" secondColor="lightgrey">
{this.props.children}
</RowAlternator>
</table>
</div>
</div>
);
}
});
"我们已经准备好了。现在列表显示了我们所想要的交替颜色,如下面的图片所示:"
"太好了,肖恩。正如你已经注意到的,当我们构建一个具有动态子元素的组件时,使用 React.cloneElement 是有意义的,在这些子元素的渲染方法上我们没有控制权,但基于某些标准想要扩展它们的属性。" 迈克很高兴。
测试 React 应用程序的帮助器
"肖恩,我们还没有为我们的应用添加任何测试,但是现在是时候开始慢慢添加测试覆盖率了。有了 Jest 库和测试实用工具插件,设置和开始测试 React 应用程序变得非常简单。" 马克解释了下一个任务。
"我听说过 Jest。它不是 Facebook 的一个测试库吗?" 肖恩问道。
设置 Jest
"是的。它是建立在 Jasmine 之上的。设置起来非常简单。首先,我们必须安装 jest-cli 和 babel-jest 包。"
npm install jest-cli --save-dev
npm install babel-jest –-save-dev
"之后,我们需要配置 package.json,如下所示:"
{
...
"scripts": {
"test": "jest"
},
"jest": {
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react",
"<rootDir>/node_modules/react-dom",
"<rootDir>/node_modules/react-addons-test-utils",
"<rootDir>/node_modules/fbjs"
],
"testFileExtensions": ["es6", "js", "jsx"],
"moduleFileExtensions": ["js", "json", "es6"]
}
...
}
"默认情况下,Jest 模拟所有模块,但是在这里我们告诉 Jest 不要模拟 React 和相关库。我们还指定了 Jest 将识别为测试文件的测试文件扩展名。"
"创建一个 __test__ 文件夹,我们将在这里添加我们的测试。Jest 将从这个文件夹中的文件运行测试。让我们也添加一个空文件。我们必须确保文件以 -test.js 结尾,这样 Jest 才能将其识别为测试文件。" 迈克解释道。
mkdir __test__
touch __test__/app-test.js
"现在让我们验证我们是否可以从命令行运行测试。"
$ npm test
> react-addons-examples@0.0.1 test /Users/prathamesh/Projects/sources/reactjs-by-example/chapter7
> jest
Using Jest CLI v0.7.1
PASS __tests__/app-test.js (0.007s)
备注
你应该看到一个与前面输出类似的输出。它可能会根据 Jest 版本而变化。如有任何问题,请咨询facebook.github.io/jest/docs/getting-started.html以设置 Jest。
"肖恩,我们已经准备好使用 Jest 了。现在是时候开始编写测试了。我们将测试顶层App组件是否正确挂载。然而,首先,我们需要更多地了解 Jest 的使用方法,"迈克说。
"默认情况下,Jest 模拟了测试文件中需要的所有模块。Jest 这样做是为了隔离正在测试的模块与其他所有模块。在这种情况下,我们想测试App组件。如果我们只是导入它,那么 Jest 将提供App的模拟版本。"
// app-test.js
const App = require('App'); // Mocked by Jest
"因为我们想测试App组件本身,所以我们需要指定 Jest 不要模拟它。Jest 提供了jest.dontmock()函数来实现这个目的。"
// app-test.js
jest.dontmock('./../src/App'); // Tells Jest not to mock App.
const App = require('App');
注意
查阅facebook.github.io/jest/docs/automatic-mocking.html以获取 Jest 自动模拟功能的更多详细信息。
"接下来,我们将添加 React 和 TestUtils 插件的导入语句。"
// app-test.js
jest.dontMock('../src/App');
const App = require('../src/App');
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
"TestUtils 插件提供了渲染组件、在渲染组件中查找子组件以及模拟渲染组件上的事件的实用函数。它有助于测试 React 组件的结构和行为。"迈克补充道。
测试 React 组件的结构
"我们将从renderIntoDocument函数开始。这个函数将给定的组件渲染到文档中的一个分离的 DOM 节点中。它返回ReactComponent,可以用于进一步的测试。"
// app-test.js
describe('App', () => {
it('mounts successfully', () => {
let app = TestUtils.renderIntoDocument(<App />);
expect(app.state.books).toEqual([]);
expect(app.state.searching).toEqual(false);
})
});
"我们在 DOM 中渲染了App组件,并断言初始书籍和搜索状态被正确设置。"迈克解释道。
"迈克,这太棒了。我们不是在测试真实的 DOM,而是在测试 React 组件。"
"是的。TestUtils 插件附带了一些查找方法,可以用来在给定的组件树中查找子组件。它们对于查找子组件,如输入框和提交按钮,并模拟点击事件或更改事件很有用。"
-
findAllInRenderedTree(tree, predicate function):这对于在给定的树中找到返回谓词函数真值的所有组件很有用。 -
scryRenderedDOMComponentsWithClass(tree, className):这对于找到具有给定类名的所有 DOM 组件很有用。 -
findRenderedDOMComponentWithClass(tree, className):与找到具有给定类的所有 DOM 组件不同,此方法期望只有一个这样的组件存在。如果有多个具有给定类名的组件,它将抛出异常。 -
scryRenderedDOMComponentsWithTag(tree, tagName):它与根据类名查找 DOM 组件类似,但是,它基于给定的标签名来查找组件。 -
findRenderedDOMComponentWithTag(tree, tagName): 与查找具有给定标签的所有组件不同,它期望只有一个这样的组件存在。如果存在多个此类组件,它也会抛出异常。 -
scryRenderedComponentsWithType(tree, componentType): 此方法查找所有具有给定类型的组件。这对于查找用户创建的所有复合组件非常有用。 -
findRenderedComponentWithType (tree, componentType): 这与所有之前的查找方法类似。如果存在具有给定类型的多个组件,它将引发异常。
测试 React 组件的行为
"让我们使用这些函数来断言,当用户输入搜索词并点击提交按钮时,搜索书籍开始。我们将通过模拟用户输入搜索词的事件来实现这一点。"迈克说。
// app-test.js
it('starts searching when user enters search term and clicks submit', () => {
let app = TestUtils.renderIntoDocument(<App />);
let inputNode = TestUtils.findRenderedDOMComponentWithTag(app, 'input');
inputNode.value = "Dan Brown";
TestUtils.Simulate.change(inputNode);
let submitButton = TestUtils.findRenderedDOMComponentWithTag(app, 'button');
TestUtils.Simulate.click(submitButton);
expect(app.state.searching).toEqual(true);
expect(app.state.searchCompleted).toEqual(false);
})
"我们渲染App组件,找到输入节点,将输入节点的值设置为搜索词,并使用TestUtils.Simulate()模拟更改事件。这个函数模拟在 DOM 节点上给定事件的派发。Simulate函数为 React 理解的所有事件都有一个方法。因此,我们可以模拟所有事件,如更改、点击等。我们可以使用这种方法来测试用户行为。"迈克解释道。
"明白了。因此,在更改搜索词后,我们点击提交按钮并验证状态是否按预期更新。"肖恩说道。
"是的,肖恩。现在,你能检查一下用户点击提交按钮后是否显示 Spinner 吗?你可以使用我们之前讨论过的查找方法之一。"迈克解释了下一个任务。
"是的。在点击提交按钮后,组件状态发生变化后,我们可以搜索渲染的组件树,以查看 Spinner 组件是否存在。"。
// app-test.js
// __tests__/app-test.js
import Spinner from './../src/Spinner';
it('starts searching when user enters search term and clicks submit', () => {
let app = TestUtils.renderIntoDocument(<App />);
let inputNode = TestUtils.findRenderedDOMComponentWithTag(app, 'input');
inputNode.value = "Dan Brown";
TestUtils.Simulate.change(inputNode);
let submitButton = TestUtils.findRenderedDOMComponentWithTag(app, 'button');
TestUtils.Simulate.click(submitButton);
expect(app.state.searching).toEqual(true);
expect(app.state.searchCompleted).toEqual(false);
let spinner = TestUtils.findRenderedComponentWithType(app, Spinner);
expect(spinner).toBeTruthy();
}),
"我们在这里使用TestUtils.findRenderedComponentWithType来检查 Spinner 是否存在于由App组件渲染的树中。然而,在添加此断言之前,我们需要在测试文件顶部导入 Spinner 组件,因为findRenderedComponentWithType期望第二个参数是一个 React 组件。"。
"非常好,肖恩。正如你所看到的,使用TestUtils.Simulate和finder方法,React 组件的测试行为变得非常简单。"迈克解释道。
注意
注意,我们没有添加测试来异步从 Open Library API 加载书籍,因为这超出了本章的范围。
浅渲染
“肖恩,TestUtils 还提供了一种使用浅渲染以隔离方式测试组件的方法。浅渲染允许我们渲染顶级组件并断言其渲染方法的返回值。它不会渲染子组件或实例化它们。因此,我们的测试不受子组件行为的影响。与之前的方法不同,浅渲染不需要 DOM,”迈克解释道。
let renderer = TestUtils.createRenderer();
“这创建了一个浅渲染器对象,我们将在这个对象中渲染我们想要测试的组件。浅渲染器有一个类似于ReactDOM.render的render方法,可以用来渲染组件。”
let renderer = TestUtils.createRenderer();
let result = renderer.render(<App />);
“在调用渲染方法之后,我们应该调用renderer.getRenderedOutput,它返回渲染组件的浅渲染输出。我们可以在getRenderedOutput的输出上开始断言关于组件的事实。”
“让我们看看从getRenderedOutput得到的输出。”
let renderer = TestUtils.createRenderer();
let result = renderer.render(<App />);
result = renderer.getRenderOutput();
console.log(result);
// Output of console.log(result)
Object {
'$$typeof': Symbol(react.element),
type: 'div',
key: null,
ref: null,
props:
Object {
className: 'container',
children: Array [ [Object], undefined ] },
_owner: null,
_store: Object {} }
“正如你所见,基于渲染输出,我们可以断言关于当前组件 props 的事实。然而,如果我们想测试关于子组件的任何事情,我们需要通过this.props.children[0].props.children[1].props.children显式地访问它们。”
“这使得使用浅渲染测试子组件的行为变得困难。然而,由于浅渲染不受子组件的影响,它对于以隔离方式测试小型组件是有用的,”迈克说。
摘要
在本章中,我们首先了解了 React 插件及其使用方法。我们使用了插件提供的不可变辅助工具和测试实用函数。我们还探讨了如何克隆组件。
在下一章中,我们将使我们的 React 应用更加高效。你将学习到可以提升 React 应用性能的插件。具体来说,你将学习如何测量我们应用的性能以及 React 如何在不改变大部分 UI 的情况下进行更快的更新。
让我们的 React 应用更快吧!
第八章. React 应用的性能
在上一章中,我们学习了如何使用各种 React 插件。我们看到了从不可变辅助工具到测试工具的各种插件。
在本章中,我们将探讨可以提高我们 React 应用性能的 React 性能工具。特别是,我们将使用 PERF 插件、PureRenderMixin和shouldComponentUpdate。我们还将探讨在使用 React 提供的性能工具时需要考虑的一些陷阱。
React 应用的性能
"嗨,迈克,我今天有几个问题想问你。周末我一直在思考我们的搜索应用。你有时间讨论一下吗?"肖恩问道。
"当然,但让我先喝点咖啡。好的,我现在准备好了。开始吧!"迈克说。
"我对 React 应用的性能有几个问题。我知道 React 在状态变化时重新渲染组件树方面做得非常好。React 使我能很容易地理解和推理我的代码。然而,这不会影响性能吗?重新渲染看起来是一个非常昂贵的操作,尤其是在重新渲染大型组件树时。"肖恩问道。
"肖恩,重新渲染可能会很昂贵。然而,React 在这方面很聪明。它只渲染发生变化的部分。它不需要重新渲染页面上的一切。它也在尽量减少 DOM 操作方面很聪明。"
"这是怎么可能的?它是怎么知道页面哪一部分发生了变化?它不是依赖于用户交互或传入的状态和属性吗?"肖恩质疑道。
"虚拟 DOM 在这里发挥了作用。它完成了跟踪变化和帮助 React 对真实 DOM 进行最小更改的所有繁重工作。"迈克解释道。
虚拟 DOM
"肖恩,React 使用虚拟 DOM 来跟踪真实 DOM 中的变化。它的概念非常容易理解。React 始终在内存中保留实际 DOM 表示的副本。每当某些状态操作发生变化时,它都会计算一个新的 DOM 副本,该副本将使用新的状态和属性生成。然后它计算原始虚拟 DOM 副本和新虚拟 DOM 副本之间的差异。这个差异导致对真实 DOM 进行最小操作,可以将当前 DOM 带到新的阶段。这样,React 在发生变化时不需要进行重大更改。"迈克解释道。
"但是,差异计算不是很昂贵吗?"肖恩问道。
"与实际的 DOM 操作相比,它并不昂贵。DOM 操作总是昂贵的。虚拟 DOM 的比较发生在 JavaScript 代码中,所以它总是比手动 DOM 操作快。"迈克说。
"这种方法的优势之一是,一旦 React 知道需要在 DOM 上执行哪些操作,它就会一次性完成。因此,当我们渲染 100 个元素的列表时,而不是逐个添加元素,React 将执行最少的 DOM 操作来在页面上插入这 100 个元素。"迈克解释道。
"我印象深刻!"肖恩惊呼。
"你们会更加印象深刻。让我实际展示一下我的意思。让我们使用来自 React 的 PERF 插件,并实际看到我们讨论的内容。"
The PERF 插件
"让我们从安装 PERF 插件开始。"
$ npm install react-addons-perf --save-dev
"我们只需要在开发模式下使用这个插件。这是一个需要记住的重要点,因为在生产中,我们不需要调试信息,因为它可能会使我们的应用程序变慢。"迈克告知。
"肖恩,PERF 插件可以用来查看 React 对 DOM 做了哪些更改,它在渲染我们的应用程序时在哪里花费时间,它是否在渲染过程中浪费了一些时间,等等。然后,我们可以使用这些信息来提高应用程序的性能。"迈克说。
"让我们首先将 PERF 插件暴露为一个全局对象。当我们的应用程序运行时,我们可以使用它在浏览器控制台中查看 React 根据用户交互所做的更改。"迈克解释道。
// index.js
import ReactDOM from 'react-dom';
import React from 'react';
import App from './App';
import Perf from 'react-addons-perf';
window.Perf = Perf;
ReactDOM.render(<App />, document.getElementById('rootElement'));
"我们已经将 PERF 插件导入到我们的 index.js 文件中,这是应用程序的起点。我们可以在浏览器控制台中访问 Perf 对象,因为我们已经将其附加到 window.Perf。"迈克补充道。
"PERF 插件附带了一些方法,可以帮助我们了解当某些内容发生变化时 React 如何处理 DOM。让我们测量一些性能统计数据。我们将通过在浏览器控制台中调用 Perf.start() 来开始测量过程。之后,我们将与应用程序进行交互。我们将输入一个查询以搜索一本书,点击提交,搜索结果将显示出来。我们将通过在浏览器控制台中调用 Perf.stop() 来停止性能测量。之后,让我们分析我们收集到的信息。"迈克解释了整个过程。
"让我们搜索丹·布朗写的书籍。"
"一旦结果显示出来,我们就停止性能测量。"
React 执行的 DOM 操作
"肖恩,PERF 插件可以显示 React 执行了哪些 DOM 操作。让我们看看 React 对丹·布朗的书籍列表进行了哪些操作。"迈克说。
"Perf.printDOM() 方法告诉我们 React 执行的 DOM 操作。它只做了两次设置 innerHTML 调用。第一次是渲染加载指示器,第二次是渲染行列表。在这之间,我们看到一个移除调用,这应该是加载指示器从页面上移除的时候。"
"哇,这个方法看起来非常实用,因为它可以告诉我们 React 是否在某种程度上做了额外的 DOM 操作。" 肖恩说。
"是的,但还有更多工具可以用来分析性能。让我们看看 React 渲染每个组件需要多少时间。这可以通过使用Perf.printInclusive()来实现。" 迈克解释道。
渲染所有组件所需的时间
"这个方法打印了渲染所有组件所需的总时间。这还包括处理属性、设置初始状态以及调用componentDidMount和componentWillMount所需的时间。"
"因此,如果我们在这其中的某个钩子中有一些耗时操作,它将影响printInclusive函数显示的输出,对吧?" 肖恩问道。
"正是如此。尽管 PERF 插件提供了另一种方法——printExclusive()——它可以在不使用这些钩子的情况下打印渲染所需的时间,这些钩子用于安装应用程序。"
"但是迈克,这些方法对于检测 React 的性能并不那么有帮助。我了解了所有发生的事情的总体情况,但它并没有告诉我如何优化哪个部分。" 肖恩问道。
React 浪费的时间
"肖恩,PERF 插件还可以告诉我们 React 浪费了多少时间以及在哪里。这有助于确定我们可以进一步优化的应用程序的部分。" 迈克说。
"什么是浪费时间?"
"当 React 重新渲染组件树时,一些组件可能不会从它们的前一个表示形式中改变。然而,如果它们再次渲染,那么 React 在渲染相同的输出上就浪费了时间。PERF 插件可以跟踪所有这些时间,并给我们一个 React 浪费了时间渲染相同输出的总结。让我们看看这个实际操作。" 迈克说。
"PERF 插件告诉我们,它浪费了时间在两次重新渲染Form组件上,但Form组件中没有任何变化,因此,它只是按照原样重新渲染了一切。" 迈克解释道。
"让我们看看Form组件,了解为什么会发生这种情况。"
// src/Form.js
import React from 'react';
export default React.createClass({
getInitialState() {
return { searchTerm: '' };
},
_submitForm() {
this.props.performSearch(this.state.searchTerm);
},
render() {
return (
<div className="row" style={this.props.style}>
<div>
<div className="input-group">
<input type="text"
className="form-control input-lg"
placeholder="Search books..."
onChange={(event) => { this.setState({searchTerm: event.target.value}) }}/>
<span className="input-group-btn">
<button className="btn btn-primary btn-lg"
type="button"
onClick={this._submitForm}>
Go!
</button>
</span>
</div>
</div>
</div>
)
}
})
"肖恩,Form组件的渲染不依赖于状态或属性。无论状态和属性如何,它都会渲染相同的输出。然而,当用户在输入框中输入字符时,我们会更新其状态。因此,React 会重新渲染它。实际上,重新渲染的输出并没有任何变化。因此,PERF 插件正在抱怨浪费了时间。" 迈克解释道。
"这是有用的信息,但这看起来像是一种微不足道的浪费,对吧?" 肖恩问道。
"同意。让我们做一些更改,这样我就可以向您展示 React 如何在实际上不应该的情况下浪费大量时间重新渲染相同的输出。" 迈克说。
"目前,我们只显示 Open Library API 返回的前 100 个搜索结果。让我们更改我们的代码,在同一页面上显示所有结果。"
// src/App.js
getInitialState() {
return { books: [],
totalBooks: 0,
offset: 100,
searching: false,
sorting: 'asc',
page: 1,
searchTerm: '',
totalPages: 1
};
}
"我引入了一个新的状态来保存搜索词、从开放图书馆获取的总页数以及当前正在获取的页码。"
"现在,我们想要从 API 默认获取同一页的所有结果。API 通过numFounds属性返回查询找到的书籍总数。基于这个,我们需要找到需要从 API 获取的总页数。"
"此外,每次最多返回 100 条记录,这些记录我们已经存储在state.offset中了。"
totalPages = response.numFound / this.state.offset + 1;
"一旦我们得到总页数,我们需要继续请求下一页的搜索结果,直到所有页面都被获取。你想要尝试让它工作吗?" 迈克问道。
"当然。" 肖恩说。
// src/App.js
// Called when user hits "Go" button.
_performSearch(searchTerm) {
this.setState({searching: true, searchTerm: searchTerm});
this._searchOpenLibrary(searchTerm);
},
_searchOpenLibrary(searchTerm) {
let openlibraryURI = `https://openlibrary.org/search.json?q=${searchTerm}&page=${this.state.page}`;
this._fetchData(openlibraryURI).then(this._updateState);
},
// called with the response received from open library API
_updateState(response) {
let jsonResponse = response;
let newBooks = this.state.books.concat(jsonResponse.docs);
let totalPages = jsonResponse.numFound / this.state.offset + 1;
let nextPage = this.state.page + 1;
this.setState({
books: newBooks,
totalBooks: jsonResponse.numFound,
page: nextPage,
totalPages: totalPages
} this._searchAgain);
},
// Keep searching until all pages are fetched.
_searchAgain() {
if (this.state.page > this.state.totalPages) {
this.setState({searching: false});
} else {
this._searchOpenLibrary(this.state.searchTerm);
}
}
"我将 API URL 更改为包含页面参数。每次从 API 收到响应时,我们都会用新的页面更新状态。我们还更新this.state.books以包括新获取的书籍。然后,在this.setState调用的回调中调用_searchAgain函数,以确保它是setState调用设置的下一页的正确值。" 肖恩解释说。
"很好,这是一个重要的要点,要记住不要在this.setState()调用之外调用_searchAgain函数,因为它可能会在setState()完成之前执行。"
"因为如果我们在外面调用它,_searchAgain函数可能会使用错误的this.state.page值。然而,由于你已经在回调中将_searchAgain函数传递给了setState,所以这种情况不可能发生。" 迈克解释道。
"_searchAgain函数会一直获取结果,直到所有页面都完成。这样,我们将在页面上显示所有搜索结果,而不仅仅是前 100 条。" 肖恩通知说。
"这正是我想要的。做得好。让我清理一下渲染方法,这样旋转器就会始终显示在底部。" 迈克说。
// src/App.js
render() {
let style = {paddingTop: '5%'};
return (
<div className='container'>
<Header style={style}></Header>
<Form style={style}
performSearch={this._performSearch}>
</Form>
{this.state.totalBooks > 0 ?
<BookList
searchCount={this.state.totalBooks}
_sortByTitle={this._sortByTitle}>
{this._renderBooks()}
</BookList>
: null }
{ this.state.searching ? <Spinner /> : null }
</div>
);
}
"这将确保在所有结果都显示之前,旋转器会一直显示。好的,都完成了。现在让我们再次测量性能。" 迈克说。
"哇,浪费的时间增加了很多!丹·布朗发布了新书吗?比我们上次看到的时间多了这么多?" 肖恩说。
"哈哈,我认为他刚才并没有发布新书。每次从下一页获取书籍时,我们都将它们添加到现有的书籍中,并从下一页开始获取书籍。然而,之前页面的书籍渲染并没有任何变化。因为我们把所有状态都保存在顶层的App组件中,所以每当它的状态发生变化时,App下的整个组件树都会重新渲染。因此,BookList会再次渲染。反过来,所有的BookRows也会再次渲染。这导致在重复渲染之前页面的相同BookRow组件上浪费了大量的时间。" 迈克说道。
"所以每次我们从新页面获取书籍时,包括已经在页面上存在的所有书籍都会再次重新渲染?我认为在这种情况下,仅仅将新的书籍行添加到现有列表中会更好。"肖恩说。
"别担心。我们可以轻松地消除这种不必要的浪费时间。React 为我们提供了一个用于短路重新渲染过程的钩子。它是shouldComponentUpdate。"
应该使用shouldComponentUpdate钩子
"肖恩,shouldComponentUpdate是一个钩子,它告诉 React 是否重新渲染组件。它不会在组件的初始渲染时被调用。然而,每当组件即将接收新的状态或属性时,shouldComponentUpdate都会在那时被调用。如果这个函数的返回值是true,那么 React 将重新渲染组件。然而,如果返回值是false,那么 React 将不会在下次调用之前重新渲染组件。在这种情况下,componentWillUpdate和componentDidUpdate钩子也不会被调用。"迈克解释道。
"很好。那么我们的代码为什么浪费了这么多时间?React 不应该使用这个钩子来优化它,并且不应该反复重新渲染相同的BookRow组件吗?"肖恩问道。
"默认情况下,shouldComponentUpdate总是返回true。React 这样做是为了避免微小的错误。我们的代码中可能有可变的状态或属性,这会使shouldComponentUpdate返回假阳性。它可能在应该返回true时返回false,导致组件在应该重新渲染时没有重新渲染。因此,React 将实现shouldComponentUpdate的责任放在了开发者的手中。"迈克说。
"让我们尝试自己使用shouldComponentUpdate来减少在重新渲染相同的BookRow组件上浪费的时间。"迈克补充道。
"这是我们目前的BookRow组件:"
// src/BookRow.js
import React from 'react';
export default React.createClass({
render() {
return(
<tr style={this.props.style}>
<td><h4>#{this.props.index}</h4></td>
<td><h4>{this.props.title}</h4></td>
<td><h4>{(this.props.author_name || []).join(', ')}</h4></td>
<td><h4>{this.props.edition_count}</h4></td>
</tr>
);
}
});
"让我们添加shouldComponentUpdate以减少不必要的重新渲染。"
// src/BookRow.js
import React from 'react';
export default React.createClass({
shouldComponentUpdate(nextProps, nextState) {
return nextProps.title !== this.props.title ||
nextProps.author_name !== this.props.author_name ||
nextProps.edition_count !== this.props.edition_count;
},
render() {
return(
<tr style={this.props.style}>
<td><h4>#{this.props.index}</h4></td>
<td><h4>{this.props.title}</h4></td>
<td><h4>{(this.props.author_name || []).join(', ')}</h4></td>
<td><h4>{this.props.edition_count}</h4></td>
</tr>
);
}
});
shouldComponentUpdate钩子接收nextProps和nextState作为参数,并且我们可以将它们与当前的状态或属性进行比较,以做出是否返回true或false的决定。
"在这里,我们正在检查标题、作者姓名或版次是否已更改。如果这些属性中的任何一个已更改,那么我们将返回true。然而,如果所有这些都没有更改,那么我们将返回false。因此,如果没有任何属性更改,组件将不会重新渲染。由于BookRow组件只依赖于属性,我们根本不必担心状态变化。"迈克补充道。
"现在,再次测量性能,看看我们是否有所改进。"
"太棒了,我们完全消除了BookRow组件重新渲染所花费的时间。然而,我们还可以做得更好。看起来我们也可以消除重新渲染Form和Header组件的时间,根据前面的结果。它们是静态组件。因此,它们根本不应该重新渲染。肖恩,这是你的下一个挑战。"
"知道了。"
// src/Header.js
import React from 'react';
export default React.createClass({
shouldComponentUpdate(nextProps, nextState) {
return false;
},
render() {
return (
<div className="row" style={this.props.style}>
<div className="col-lg-8 col-lg-offset-2">
<h1>Open Library | Search any book you want!</h1>
</div>
</div>
)
}
})
// src/Form.js
import React from 'react';
export default React.createClass({
getInitialState() {
return { searchTerm: '' };
},
shouldComponentUpdate(nextProps, nextState) {
return false;
},
_submitForm() {
this.props.performSearch(this.state.searchTerm);
},
render() {
return (
<div className="row" style={this.props.style}>
<div>
<div className="input-group">
<input type="text"
className="form-control input-lg"
placeholder="Search books..."
onChange={(event) => { this.setState({searchTerm: event.target.value}) }}/>
<span className="input-group-btn">
<button className="btn btn-primary btn-lg"
type="button"
onClick={this._submitForm}>
Go!
</button>
</span>
</div>
</div>
</div>
)
}
})
"迈克,我们可以简单地从shouldComponentUpdate中返回false,对于Header和Form组件,因为它们在渲染时根本不依赖于状态或 props!"
"完美的发现,肖恩。记下这些不依赖于任何东西的静态组件。它们是告诉 React 不比较它们的虚拟 DOM 的完美目标,因为它们根本不需要重新渲染。"迈克通知说。
"没错。我会密切关注 UI 中可以提取成更小组件的这些静态部分。"肖恩说。
"现在让我们看看在进行这些改进之后,是否消除了更多浪费的时间。"
"酷!我们消除了重新渲染相同的Header和Form组件所浪费的时间。"迈克说。
"太棒了!让我也尝试一下消除BookList和RowAlternator上花费的时间。"肖恩通知道。
"等等,肖恩。在我们做这件事之前,我想讨论一下shouldComponentUpdate的一个替代方案。"
PureRenderMixin
"肖恩,PureRenderMixin是一个可以作为shouldComponentUpdate替代品使用的附加组件。在底层,它使用shouldComponentUpdate并比较当前和下一个 props 和 state。让我们在我们的代码中试试它。当然,首先我们需要安装这个附加组件。"迈克说。
$ npm install react-addons-pure-render-mixin
// src/Header.js
import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
export default React.createClass({
mixins: [PureRenderMixin],
..
..
})
// src/Form.js
import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
export default React.createClass({
mixins: [PureRenderMixin],
..
..
}
})
"肖恩,现在我们用过了PureRenderMixin,来看看浪费的时间吧。"迈克说。
"哦,情况变得更糟了。PureRenderMixin函数又把重新渲染Form和Header组件所浪费的时间加回去了。怎么了,迈克?"肖恩问道。
"冷静点!我要解释一下为什么会这样。PureRenderMixin会将当前的 props 和 state 与下一个 props 和 state 进行比较,但它进行的是浅比较。因此,如果我们传递包含对象和数组的 state 或 props,即使它们内容相同,浅比较也不会返回 true。"迈克解释道。
"然而,我们在哪里将任何复杂对象或数组传递给Header和Form组件呢?我们只是传递了书籍数据,如作者的名字、版次等。我们没有向Header传递任何东西,PureRenderMixin怎么会失败呢?"肖恩问道。
"你忘了从App组件传递给Header和Form组件的样式属性。"迈克提醒说。
// src/App.js
render() {
let style = {paddingTop: '5%'};
return (
<div className='container'>
<Header style={style}></Header>
<Form style={style}
performSearch={this._performSearch}>
</Form>
..
..
</div>
)}
"每次App重新渲染时,都会创建一个新的样式对象,并通过 props 发送给Header和Form。"
The PureRenderMixin anti pattern
PureRenderMixin内部实现了shouldComponentUpdate,如下所示:
var ReactComponentWithPureRenderMixin = {
shouldComponentUpdate: function(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
},
};
shallowCompare函数也是 React 提供的一个附加功能,是一个比较当前状态和 props 与下一个状态和 props 的辅助函数。它基本上实现了与PureRenderMixin相同的功能,但由于它是一个函数,可以直接使用,而不是使用PureRenderMixin。当我们使用 ES6 类与 React 一起使用时,这尤其必要。" 迈克解释说。
"迈克,所以浅比较是PureRenderMixin未能检测到下一个 props 没有变化的原因吗?" 肖恩问。
"是的。shallowCompare只是遍历正在比较的对象的键,当每个对象中键的值不是严格相等时返回false。因此,如果我们传递简单的 props,如下所示,那么shallowCompare将正确地确定不需要重新渲染:"
// shallowCompare will detect correctly that props are not changed.
{ author_name: "Dan Brown",
edition_count: "20",
title: "Angels and Demons" }
"然而,如果道具是一个对象或数组,它将立即失败。"
{ author_name: "Dan Brown",
edition_count: "20",
title: "Angels and Demons",
style: { paddingTop: '%5' } }
"尽管PureRenderMixin为我们节省了一些代码行,但它可能不会像我们预期的那样始终有效。特别是当我们有可变状态、对象或数组作为 props 时。" 迈克说。
"明白了。所以当我们有嵌套状态或 props 时,我们可以编写自己的shouldComponentUpdate函数吗?" 肖恩问道。
"是的。PureRenderMixin和shallowCompare对于具有简单 props 和 states 的简单组件来说很好,但我们在使用它时应该小心。" 迈克说。
注意
由于各种原因,在 React 世界中不建议使用混入。在此处查看PureRenderMixin模式的替代方法 - github.com/gaearon/react-pure-render。
不可变数据
"迈克,不过我有一个问题。尽管如此,为什么PureRenderMixin最初要执行浅比较呢?它不应该执行深度比较,以便我们始终有更好的性能吗?" 肖恩对PureRenderMixin不太满意。
"嗯,这里有一个原因。浅比较非常便宜。它不花太多时间。深度比较总是昂贵的。因此,PureRenderMixin执行浅比较,这对于大多数简单用例来说已经足够好了。" 迈克说。
"然而,React 确实为我们提供了一个选项,可以定义我们自己的shouldComponentUpdate版本,就像我们之前看到的。我们只需从shouldComponentUpdate返回false就可以完全短路重新渲染过程,或者我们只需比较我们组件所需的那部分 props。"
"没错,就像我们为BookRow组件编写shouldComponentUpdate一样?" 肖恩问。
// src/BookRow.js
export default React.createClass({
shouldComponentUpdate(nextProps, nextState) {
return nextProps.title !== this.props.title ||
nextProps.author_name !== this.props.author_name ||
nextProps.edition_count !== this.props.edition_count;
},
render() {
return(
<tr style={this.props.style}>
..
</tr>
);
}
});
"确实如此,肖恩。如果需要,你还可以根据组件的需求进行深度比较。"
// custom deep comparison as per requirement
shouldComponentUpdate(nextProps, nextState) {
return nextProps.book.review === props.book.review;
}
"肖恩,我们还有另一个选择,那就是使用不可变数据。比较不可变数据非常简单,因为它总是会创建新的数据或对象,而不是修改现有的对象。"
// pseudo code
book_ids = [1, 2, 3]
new_book_ids = book_ids.push(4)
book_ids === new_book_ids # false
"因此,我们只需要比较新对象的引用与旧对象的引用,当值相等时它们总是相同的,当值不相等时它们总是不同的。因此,如果我们使用不可变数据作为我们的 props 和 state,那么PureRenderMixin将按预期工作。" 迈克说道。
注意
检查facebook.github.io/immutable-js/,作为使用不可变数据作为 state 和 props 的选项。
摘要
在本章中,你了解了 React 提供的性能工具以及如何使用它们。我们使用了 PERF 插件:shouldComponentUpdate和PureRenderMixin。我们还看到了在尝试提高我们应用性能时需要关注的区域。我们还研究了在提高性能时可能遇到的陷阱,特别是与PureRenderMixin相关。最后,我们讨论了不可变数据的重要性和优势。
在下一章中,我们将使用 React Router 和 Flux 详细查看 React 的数据模型。你将学习如何使用 React 与其他框架如 Backbone 一起使用。