本文节选自李晋华撰写的《React前端技术与工程实践》一书,由电子工业出版社出版。
作者:李晋华,信息系统架构师和技术顾问。多年从事军事物流信息系统研发工作和相关教学工作。在后勤信息化领域承担多项重点项目的研发工作。曾获军队科技进步奖二等奖。在系统架构设计、系统集成和前端交互设计等方面具有丰富的实战经验。
责编:陈秋歌,寻求报道或者投稿请发邮件至chenqg#csdn.net,或加微信:Rachel_qg。
了解更多前沿技术资讯,获取深度技术文章推荐,请关注CSDN研发频道微博。
在实际前端开发中,我们往往还会遇到一些具有共性的问题,针对其中的很多问题业界也提供相应的解决方案或工具。本文围绕实际工程问题列出了一些常用的技巧和工具,便于读者引用。
由于React的事件是面向虚拟DOM的,因此,有些真实的浏览器DOM事件是React未提供的。如果要在React中响应这样的事件,需要在componentDidMount中获取对应的真实DOM元素绑定事件,并在componentWillUnmount中取消事件绑定。这种技巧也常用于React与jQuery结合使用时。参见下面的示例:
var Comp = React.createClass({
getInitialState: function() {
return {windowWidth: window.innerWidth};
},
onWindowResize: function(e) {
this.setState({windowWidth: window.innerWidth});
},
componentDidMount: function() {
window.addEventListener('resize', this. onWindowResize);
},
componentWillUnmount: function() {
window.removeEventListener('resize', this. onWindowResize);
},
render: function() {
return 当前window宽度: {this.state.windowWidth};
}
});
React.render( , document.body);
通过AJAX加载数据是一个很普遍的场景。在React组件中如何通过AJAX请求来加载数据呢?首先,AJAX请求的源URL应该通过props传入;其次,最好在componentDidMount函数中加载数据。加载成功,将数据存储在state中后,通过调用setState来触发渲染更新界面。
注意:
AJAX通常是一个异步请求,也就是说,即使componentDidMount函数调用完毕,数据也不会马上就获得,浏览器会在数据完全到达后才调用AJAX中所设定的回调函数,有时间差。因此当响应数据、更新state前,需要先通过this.isMounted() 来检测组件的状态是否已经mounted。
下面是利用GitHub网站提供的API接口获取某个用户近况信息的例子。
var UserGist = React.createClass({
getInitialState: function() {
return {
username: '',
lastGistUrl: ''
};
},
componentDidMount: function() {
$.get(this.props.source, function(result) {
var lastGist = result[0];
if (this.isMounted()) {
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url
});
}
}.bind(this));
},
render: function() {
return (
<div>
{this.state.username}'s last gist is
<a href={this.state.lastGistUrl}>herea>.
div>
);
}
});
React.render(
<UserGist source="https://api.github.com/users/octocat/gists" />,
mountNode
);
使用jQuery库所提供的ajax请求$.ajax
函数数据也存在一些问题,如兼容性问题就很令人头疼。React推荐使用fetch库,其在API接口层面和jQuery类似,读者可以自行搜索相关资料,熟悉 $.ajax
可以很快上手。
在React中,ref属性是特定用途的属性,将ref属性绑定到渲染函数中输出的任何组件上,就可以获得对应的组件实例,通过这个实例可以获得对应的实际DOM元素。
ref字符串属性
React通常在组件上使用一个字符串识别符来作为ref属性。在渲染函数中给要渲染的子组件增加ref属性,如:
在其他地方,如事件处理代码中,通过this.refs访问真正的组件。
var input = this.refs.myInput;
var inputValue = input.value;
完整示例:
var MyComp = React.createClass({
getInitialState: function() {
return {userInput: ''};
},
handleChange: function(e) {
this.setState({userInput: e.target.value});
},
clearAndFocusInput: function() {
// 清空输入的数据
this.setState({userInput: ''}, function() {
// 此处获得真实DOM并获得焦点
this.refs.theInput.getDOMNode().focus();
});
},
render: function() {
return (
this.clearAndFocusInput}>
单击获得焦点并清空
"theInput"
value={this.state.userInput}
onChange={this.handleChange}
/>
);
}
});
在这个例子中,MyComp组件的渲染函数渲染一个组件,组件的实例通过this.refs.theInput获取,这个过程由React自动填充。值得注意的是:ref属性对于React组件获得的是组件实例,而对于HTML获得的则是对应的真实浏览器DOM元素。对于前者,我们可以直接调用组件在类定义中公开的任何成员函数。
对于复合组件来说,这个引用会指向一个组件类的实例,进而可以调用任何该类定义的方法。如果需要访问该组件类对应的实际DOM节点,可以用ReactDOM.findDOMNode来找到实际的节点。不过并不推荐这种做法,因为这样做绝大多数情况下都会打破封装性,并增加了对真实浏览器DOM的依赖,一般都能找到更清晰的模式,使只在React模型中编写代码就能达到同样的效果。
ref回调函数属性
React组件的ref属性也可以是一个回调函数,并且这个回调函数会在组件被挂载后立刻被执行。回调函数的参数对应组件实例的引用,这个回调函数可以立即使用组件,或者保存这个引用便于以后使用。上面的例子使用ref回调函数改写之后如下:
var MyComp = React.createClass({
getInitialState: function() {
return {userInput: ''};
},
handleChange: function(e) {
this.setState({userInput: e.target.value});
},
clearAndFocusInput: function() {
// 清空输入的数据
this.setState({userInput: ''}, function() {
// 此处获得真实DOM并获得焦点
this.theInput.focus();
});
},
render: function() {
return (
this.clearAndFocusInput}>
单击获得焦点并清空
this.theInput=compInstance}
value={this.state.userInput}
onChange={this.handleChange}
/>
);
}
});
一旦引用的组件被卸载,或者ref属性本身发生了变化,原有的ref会再次被调用,此时参数为null,这样可以防止内存泄露。如果和上面的例子一样使用内联的函数表达式,那么React每次更新(即调用渲染函数)时,都会得到不同的函数对象,之后每次更新时ref回调函数都会被调用两次:前一次参数是null,后一次是具体的组件实例。
注意:
classNames介绍
在实际应用中,经常会遇到根据某些状态增加或更改组件属性中类名的情况,例如Bootstrap中的动态行为往往是由不同的class来控制,这就经常需要对class进行修改。看看下面的这段代码:
var classString = 'content';
if (this.state.isBgRed) {
classString = classString + ' bg-red';
} else {
classString = classString + ' bg-green';
}
这里实现了根据isBgRed状态切换不同class项的效果,但这样的代码语义不太清晰,也不容易维护,一旦状态控制变量变多,这样的代码将会变成一团乱麻。为了更好地满足这种类似的class动态切换的需求,可以使用classNames工具。针对这个问题以前还出现过classSets工具,功能与classNames相似,现已废弃不用。
使用classNames工具首先要导入这个模块:
import classNames from 'classnames'
然后就可正常使用 classNames()函数了。前面的代码用 classNames 工具重写如下:
let isBgRed = this.state.isBgRed;
let classes = classNames ({' bg-red ':isBgRed, ' bg-green ': ! isBgRed });
classNames用法
classNames()函数可以接受任意数量的参数,并将参数连接成一个class字符串,参数可以是一个字符串或者一个对象。如果参数是对象,则对象的属性名(属性值为false、0、null或undefine的属性除外)也会加入到结果class字符串中。看下面的示例:
classNames('foo', 'bar'); // 结果为“foo bar”
classNames('foo', { bar: true }); //结果为“foo bar”
classNames({ 'foo-bar': true }); // 结果为“foo-bar”
classNames({ 'foo-bar': false }); //结果为空
classNames({ foo: true }, { bar: true }); //结果为“foo bar”
classNames({ foo: true, bar: true }); // 结果为“foo bar”
// 多个不同类型的参数示例
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // 结果为“foo bar baz quux”
// 忽略参数的示例
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // 结果为“bar 1”
如果参数是数组,则数组中的元素也被当作参数进行处理:
var arr = ['b', { c: true, d: false }];
classNames('a', arr); // 结果为“a b c”
在ES 6中使用动态的classNames
ES 6为JavaScript新标准语法,如果在工程中用到了ES 6语法(需要使用Babel进行转译),那么也可以结合ES 6中的动态属性特性来使用classNames。如下面的例子:
let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true });
多类名去重
当多个参数进行合并时,可能会遇到类名有重复的情况,这就需要进行去重处理。classNames模块中的dedupe模块包含这方面的功能。dedupe是classNames的增强版本,但性能要差些,因此,如非确实有去重需求,没必要使用它。这里只对其进行简要介绍。
要使用dedupe,首先需要引入模块:
var classNames = require('classnames/dedupe');
或者在html文件中加入:
<script src="js/dedupe.js" type="text/JavaScript">script>
使用方法与classnames一样,只是额外增加了自动去重的功能:
classNames('foo', 'foo', 'bar'); // 结果为“foo bar”
classNames('foo', { foo: false, bar: true }); // 结果为“bar”
Immutable.js介绍
JavaScript中的对象一般是可变的(Mutable),作为参数传递时使用的是引用。当声明一个对象的引用变量时,该引用变量直接指向原对象,如:
var foo={a: 1};
var bar=foo; //此时bar与foo为同一对象
bar.b = 2; //此时foo.b也为2
其实这样的语义是不清晰的,可能我们只是想复制一个与foo一样的对象,结果却改变了原对象。尽管这样的共享变量机制可以节约内存,但在复杂场景下却往往是造成各种怪异问题的根源。Pete Hunt说,共享的可变状态是罪恶之源(Shared mutable state is the root of all evil)。一般使用对象复制来避开这个问题,但马上就会遇到是浅复制还是深复制的选择,把问题复杂化了。使用同样来自名门facebook公司的Immutable模块来管理这样的数据是更好的选择。
使用Immutable来管理的数据对象具有只能创建不能更改的特性。对Immutable数据对象的任何修改、添加、删除操作都会返回一个新的Immutable对象,同时原对象依然可用且不变,这称为持久化数据结构(Persistent Data Structure)。为了避免深度复制,Immutable使用了被称为结构共享(Structural Sharing)的策略,如果对象树中只有一个节点发生了变化,则只修改受到影响的父节点对它的引用,其他节点则与原对象共享,从而避免了深度复制带来的内存开销。
基于这样的策略,Immutable还具有很多优秀的特性,比如支持回撤、并发安全、与函数式编程天然一致等,可以说Immutable是对数据管理的革新,其将产生更广泛的影响。如React就提倡把this.state当作只可创建不能更改的Immutable,Redux也推荐搭配使用Immutable来管理state数据等。
对于Immutable的具体用法,官网上有详细的文档。限于篇幅,本书主要列举了Immutable一些常用的、实用的用法,我们需要重点关注的是Immutable提供的持久化List、Map等用法,部分代码引自官方网站(http://facebook.github.io/ immutable-js/)。
Immutable基本用法
1.创建Immutable对象
import Immutable from 'immutable';
// 支持数据嵌套的创建方式
var imtArray = Immutable.fromJS([1, {2,3}]) //根据数组创建
var imtObj = Immutable.fromJS({a: 1, b:[2,3]}) //根据JavaScript对象创建
// 不支持数据嵌套的创建方式
var imtList = Immutable.List([1,2]) //根据数组创建,支持数据
var imtMap = Immutable.Map({a: 1}) //根据JavaScript对象创建
2.从Immutable对象中提取JavaScript对象
var jsObject = immutableData.toJS(); // 提取JavaScript对象
var jsArray = imtArray.toArray(); // 提取数组对象
Immutable对象比较
Immutable对象的比较有两种方式,一种是引用比较,一种是值比较。
1.引用比较
引用比较用来识别两个对象是不是同一个对象。使用操作符===进行比较,由于比较的是内存地址,所以速度很快。
let map1 = Immutable.Map({a:1, b:2, c:3});
let map2 = Immutable.Map({a:1, b:2, c:3});
map1 === map2; // 结果为false
因为只比较内存地址,即使两个不同的对象内容一样也被认为是不相等的。如果需要进行内容上的比较,可以使用值比较的方式。
2.值比较
值比较用于判断两个数据对象的值是否相等,使用Immutable.is()函数进行比较:
Immutable.is(immutableObjectA, immutableObjectB);
Immutable.is()函数实质上比较的是两个对象的 hashCode 或 valueOf(对于JavaScript对象)。由于Immutable使用持久化数据结构存储对象,只要两个对象的hashCode值相等,两个对象的值就是一样的,有效地避开了对对象值的深度遍历,非常高效。
Immutable List用法
(1)创建Immutable List。
Immutable.List(); //生成空的Immutable List对象
Immutable.List([1,2]); //生成Immutable数组对象,不支持嵌套
Immutable.fromJS([1,{2,3}]); //生成Immutable数组对象
(2)查看List的大小。
immutableA.size
immutableA.count()
(3)判断是否是List。
Immutable.List.isList(x);
(4)在React组件中判断propTypes是否是List的写法。
React.PropTypes.instanceOf(Immutable.List).isRequired
(5)获取List索引的元素。
immutableData.get(0);
immutableData.get(-1); //当索引值为负数时为反向索引
(6)访问嵌套数组中的数据。
var imtNestedData = Imutable.fromJS({a: {b: {c:3}}});
imtNestedData.getIn(['a', 'b', 'c']); //结果为3
(7)更新List,其实就是根据原来的List对象创建一个新的List对象。
immutableA = Immutable.fromJS([0, 0, [1, 2]]);
immutableB = immutableA.set(1, 1);
immutableC = immutableB.update(1, (x) -> x + 1);
immutableC = immutableB.updateIn([2, 1], (x) -> x + 1);
(8)针对List的排序方法有 sort 和 sortBy。
immutableData.sort(function(a, b){
if a < b then return -1;
if a > b then return 1;
return 0;
});
immutableData.sortBy((x) -> x);
(9)遍历。
immutableData.forEach(function(a, b){
// 此处进行遍历操作
return true; //如果返回值为false则终止遍历
});
(10)检索List中的元素。
immutableData.find((x) -> x > 1); // 返回第一个匹配的元素
immutableData.filter((x) -> x > 1); // 返回匹配的元素的数组
immutableData.filterNot((x) -> x <= 1); // 返回不匹配的所有元素的数组
Immutable Map用法
(1)创建Immutable Map。
Immutable.Map(); // 创建空的Immutable Map
Immutable.Map({a: 1}); // 根据对象创建Immutable Map
Immutable.fromJS({a: 1}); // 根据对象创建Immutable Map
(2)判断Map,写法和List类似。
Immutable.Map.isMap(obj);
(3)获取Map中的数据。
immutableData.get('a');
通过getIn访问嵌套的Map中属性a值对象中b属性的值。
immutableData.getIn(['a', 'b']);
(4)更新对象。
immutableB = immutableA.set('a', 1);
immutableB = immutableA.setIn(['a', 'b'], 1);
immutableB = immutableA.update('a', (x) -> x + 1);
immutableB = immutableA.updateIn(['a', 'b'], (x) -> x + 1);
合并对象。
immutableB = immutableA.merge(immutableC);
(5)Map的检索,与List相似。
data = Immutable.fromJS({a: 1, b: 2});
data.filter((value, key) -> value is 1);
判断属性是否存在要先转换为JavaScript的原生对象,再判断:
immutableData = Immutable.fromJS({key: null});
immutableData.has('key');
(6)分别获取key的数组和value的数组。
immutableData.keySeq();
immutableData.valueSeq();
毋庸置疑,jQuery仍然是当前主流的Java工具库。尽管React很优秀,但它毕竟是新生事物,和庞大的jQuery生态资源相比,React就显得很不足了。即使使用React,我们也会因为各种各样的原因,再将它和jQuery结合到一起使用。如何延续jQuery的资源又结合React的技术呢?围绕这个问题,我们首先来总结一下React与jQuery的区别。
React与jQuery的区别
基于以上考虑,我们很难不做改动地将jQuery组件变为React组件。因此,我们重点考虑如何在jQuery中使用React,以及如何在React中使用jQuery。
在React中使用jQuery
要在React中使用jQuery的功能,首先要获得组件所对应的真实浏览器DOM元素,这点通过ref属性可以实现;其次要注意jQuery事件系统与React的不同,重点需要关注componentDidMount和componentWillUnmount函数,并在这两个函数内适配jQuery的生命周期。以下代码片段引自React官方示例。
var BootstrapModal = React.createClass({
componentDidMount: function() {
var rootElem = this.refs.root; //获得真实的浏览器DOM节点
// 调用jQuery方法,将节点内容转换为模态对话框
$(rootElem).modal({backdrop: 'static', keyboard: false, show: false});
// 绑定jQuery事件,注意到root组件并没有在React中绑定事件
$( rootElem).on('hidden.bs.modal', this.handleHidden);
},
componentWillUnmount: function() {
$(this.refs.root).off('hidden.bs.modal', this.handleHidden);
},
close: function() {
$(this.refs.root).modal('hide');
},
open: function() {
$(this.refs.root).modal('show');
},
render: function() {
var confirmButton = null;
var cancelButton = null;
if (this.props.confirm) {
confirmButton = (
this.handleConfirm}
className="btn-primary">
{this.props.confirm}
);
}
if (this.props.cancel) {
cancelButton = (
this.handleCancel} className= "btn-default">
{this.props.cancel}
);
}
return (
"modal fade" ref="root">
"modal-dialog">
"modal-content">
"modal-header">
{this.props.title}
"modal-body">
{this.props.children}
"modal-footer">
{cancelButton}
{confirmButton}
);
},
handleCancel: function() {
if (this.props.onCancel) {
this.props.onCancel();
}
},
handleConfirm: function() {
if (this.props.onConfirm) {
this.props.onConfirm();
}
},
handleHidden: function() {
if (this.props.onHidden) {
this.props.onHidden();
}
}
});
在jQuery中使用React
在jQuery中使用React与在HTML中使用React没有什么不同,我们只要声明一个HTML标签(通常是div)作为React组件的容器,在初始化阶段调用ReactDOM.render()函数将组件挂接到该标签下即可。如下面的例子:
ReactDOM.render(
React.createElement(HelloComponent, null),
document.getElementById('reactContainer')
);
点击订购:《React前端技术与工程实践》
欢迎加入“CSDN前端开发者”群,与更多专家、技术同行进行热点、难点技术交流。请扫描以下二维码加群主微信,申请入群,务必注明「姓名+公司+职位」。