文档章节

ReactJS开发入门

曾建凯
 曾建凯
发布于 2016/03/05 10:18
字数 3149
阅读 3680
收藏 105

ReactJS的确是一个伟大的东西,就其核心而言,就是一个可以和html混合编写的jsx格式,以及一个良好有序(加载时和更新时)、并和html渲染实时捆绑的状态机制。学习和使用ReactJS,你并不需要对你现有的代码架构做太大的调整,只要引入react.js和react-dom.js两个文件,就能开始了。

JSX之为物

首先,不要对jsx这个东西产生抗拒,如果你感到不爽,你完全可以直接使用js,只是你需要写很多嵌套的代码,比如如下:

var Test = React.createClass({
   displayName: 'Test',
   render: function () {
      return (
         <div className="ui form">
            <div className="field">
               <label>hello world</label>
            </div>
         </div>
      );
   }
});

实际上对应生成的是这样的js代码:

var Test = React.createClass({
	displayName: 'Test',
	render: function () {
		return React.createElement(
			'div',
			{ className: 'ui form' },
			React.createElement(
				'div',
				{ className: 'field' },
				React.createElement(
					'label',
					null,
					'hello world'
				)
			)
		);
	}
});

当然,如果你的html结构嵌套的更加复杂,对应的js,将变得更加恐怖。无论如何,html看起来还是更加直观的。

在jsx中,你可以使用如常见的模板语言一般,在html代码里面直接使用js的变量和函数。

var Test = React.createClass({
	displayName: 'Test',
	getInitialState: function() {
		return {
			title: '',
			list: [
				{ name: 'Jan' },
				{ name: 'Easy' }
			]
		};
	},
	render: function () {
		return (
			<div>
				<div className="ui header">
					{this.state.title}
				</div>
				<div className="ui list">
					{
						_.map(this.state.list, function(item) {
							return (
								<div className="item">{item.name}</div>
							);
						})
					}
				</div>
			</div>
		);
	}
});

哦,是的,一切都是你亲切而又熟悉的html,js,和className。

gulp实时编译JSX文件

JSX有如此这般的便利,唯一剩下的问题就是,如何将JSX进行转换。ReactJS官网提供了一种实时加载JSX的方式,但我自己觉得并不实用——毕竟现在前端类库,已经是从几个类库一跃到几十个类库了。通过gulp的watch是一个更好的选择。

严格意义上说,对react进行编译处理,只需要引入以下的三个库,就可以了:

npm install gulp gulp-babel babel-preset-react --save-dev

比如我当前的演示项目:

输入图片说明

编辑gulpfile.js,加入以下代码:

var gulp = require('gulp');
var babel = require('gulp-babel');

gulp.task('jsx', function () {
	gulp.src('./jsx/*.jsx')
		.pipe(babel({
			presets: ['react']
		}))
		.pipe(gulp.dest('./jsx'));
});

gulp.task('watch-jsx', ['jsx'], function () {
	gulp.watch([
		'./jsx/*.jsx'
	], ['jsx']);
});

然后在命令行执行gulp watch-jsx,这时每次你修改中/jsx目录下的*.jsx文件,并保存时,他自动就会将jsx转换为js文件。

JSX的事件处理

使用JSX的另外一个好处是,不再需要依赖jquery这种DOM的框架了。

JSX生成的对象实际上还是一个DOM实例,所以一切关于DOM原生的知识就又可以派上用场了,当然,除非你极度依赖于jquery什么的。其实在ReactJS里面,类如Mootools这种原生对象注入的类库,会有更大的用处,jquery反而一无是处。

var hello_world = React.createClass({
		displayName: 'hello_world.jsx',
		handleClick: function(event) {
			var link = event.target;
		},
		render: function () {
			return (
				<a className="hello" onClick={this.handleClick}>
					hello world
				</a>
			);
		}
	});

ReactJS 0.14以后,取消了事件过程内return false来中断事件,要事件中断,请使用e.preventDefaulte.stopPropagation,详细的可以参考官方文档:https://facebook.github.io/react/docs/events.html。

这里特别注意this的作用域问题。

var Test = React.createClass({
		displayName: 'Test',
		getInitialState: function() {
			return {
				title: '',
				list: [
					{ name: 'Jan' },
					{ name: 'Easy' }
				]
			};
		},
		handleClick: function() {

		},
		render: function () {
			return (
				<div>
					<div className="ui header">
						{this.state.title}
					</div>
					<div className="ui list">
						{
							_.map(this.state.list, function(item) {
								return (
									<a className="item" onClick={this.handleClick}>{item.name}</a>
								);
							})
						}
					</div>
				</div>
			);
		}
	});

这里是一个错误的示范,this.handleClick在_.map的闭包中,实际上指向了另外一个空间——所以,这个例子就算你点爆了你的浏览器,this.handleClick也是不会触发的。

这个问题,又存在一个反向的情况,即假定你绑定了某个本地变量为某个实例,在onClick这里调用这个实例的某个方法句柄。当onClick被触发的时候,他能成功执行到这个方法,但是本地对象却变成了这个DOM元素,而不是你预想的那个实例。这其实是对象冒充,他调用这个方法的时候指定了call或apply的主体执行对象。所以这里值得注意,但同时他又是一个可以使用各种奇淫巧技的入口,你懂的——执行方法主体虽然绑定了,然而局部上下文环境却是生效的,噢噢噢我不能说得太多。

状态初始化

状态(state)是ReactJS的精华之所在,基于这个状态,不但是一个React实例的数据载体,他还可以起到名副其实的控制状态,并且,通过更新状态,会触发React实例的ComponentUpdate,从而进行局部的重新渲染。

基本使用,你可以将状态视为数据的载体,在render的过程中使用。

var Test = React.createClass({
		displayName: 'Test',
		getDefaultProps: function() {
			return {
				name: ''
			};
		},
		getInitialState: function() {
			return {
				name: this.props.name
			};
		},
		render: function () {
			return (
				<div>
					<div className="ui header">
						{this.state.name}
					</div>
				</div>
			);
		}
	});

	var element = ReactDOM.render(<Test name="hello" />, document.getElementById('hello'));

上述这是一个较为完整的示例。

首先创建了一个React Class(实际上是React Component)。

<Test name="hello" />,创建了一个React Element,到目前为止,这还是一个虚拟的元素。虚拟元素并不具有Class实例的方法。

在执行了ReactDOM.render所返回的实例,才是将React Element添加进DOM树,并正式返回一个React Class的实例,这时,React的所有相关方法才开始生效。

在React Classs中:

getDefaultProps方法,用于声明一个React 实例的默认属性,这里我声明了一个name。而在创建React Element时,我实际设置了name=hello。这个getDefaultProps方法,保证了你的React实例默认的属性结构,以减少一些繁琐的判断。

getInitialState方法,是获取初始化的状态值,这里我将state.name取值于props.name,当然这未必是一个推荐的做法。

变更状态

你可以使用setState和replaceState的方法,来对React实例进行状态的变更。如上的例子:

element.setState({ name: 'Tom' });

页面显示的内容会随着你执行setState而发生变化,当然这是一个不太高明的例子。

状态的变更,是会触发组件重新执行render的(所以说组件内的局部渲染并不准确)。这是一个非常透明的机制,他除了能简单的控制输出数据的变化,还可以起到组件的状态化切换的效果。

比如我们经常做登录的登录界面:

var LoginForm = React.createClass({
		displayName: 'LoginForm',
		getInitialState: function() {
			return {
				isPost: false,
				account: '',
				password: ''
			};
		},
		handleSubmit: function() {
			if (!this.state.isPost) {
				this.setState({ isPost: true }); // 注意,这里设置了状态
			}
		},
		render: function () {
			return (
				<form method="post" onSubmit={this.handleSubmit}>
					<label>账号</label>
					<input type="text" value={this.state.account} disabled={this.state.isPost} />  

					<label>密码</label>
					<input type="password" value={this.state.password} disabled={this.state.isPost} />  

					<button disabled={this.state.isPost}>登录</button>
					{this.state.isPost ? 

'登录中,请稍候!'
 : ''}
				</form>
			);
		}
	});

这是一个非常简单的示例,当用户提交表单的时候,会触发更新状态isPost为true,当状态变更以后,所有输入的控件都会切换到disabled,并且在表单底部,会出现登录中,请稍候!提示。

接口函数

componentWillMount,一个组件被装载前(ReactDOM),会触发,只会被执行一次。

componentDidMount,一个组件装载完成后,触发,只会被执行一次。

componentWillReceiveProps(nextProps),当一个组件接收到新的props的时候会被触发,这个其实就是用于当已经渲染了一个组件,需要更新这个组件的初始化参数时可使用这个接口(通过<Test name="" />的方式)。

shouldComponentUpdate(nextProps, nextState),这是判断接口,必须返回true或false,这个接口是用来判断组件是否该触发更新。

componentWillUpdate(nextProps, nextState),每次接收到新的props或state时,会触发,但是不包含初始化渲染的时候。官网特别提示,不要在这个函数内执行setState,他会触发循环更新……

componentDidUpdate(prevProps, prevState),当状态更新已经写入DOM元素后,触发执行。

componentWillUnmount(),当一个DOM元素被卸载的时候触发。

补完整示例

为了方便内部示例传阅,补一个完整示例。

mkdir test_reactjs
npm init // 然后一路回车
npm install gulp gulp-babel babel-preset-react --save-dev
bower init // 一路回车
bower install react --save

这样基础的环境已经足够了,编辑gulpfile.js

var gulp = require('gulp');
var babel = require('gulp-babel');

gulp.task('jsx', function () {
	gulp.src('./jsx/*.jsx')
		.pipe(babel({
			presets: ['react']
		}))
		.pipe(gulp.dest('./jsx'));
});

gulp.task('watch-jsx', ['jsx'], function () {
	gulp.watch([
		'./jsx/*.jsx'
	], ['jsx']);
});

添加一个html文件

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<script type="text/javascript" src="bower_components/react/react.js"></script>
	<script type="text/javascript" src="bower_components/react/react-dom.js"></script>
</head>
<body>
<div id="hello"></div>
<script type="text/javascript" src="jsx/LoginForm.js"></script>
</body>
</html>

然后加入/jsx/LoginForm.jsx

var LoginForm = React.createClass({
	displayName: 'LoginForm',
	getInitialState: function() {
		return {
			isPost: false,
			account: '',
			password: ''
		};
	},
	handleSubmit: function(event) {
		event.preventDefault();
		if (!this.state.isPost) {
			this.setState({ isPost: true }); // 注意,这里设置了状态
		}
	},
	handleChange: function(event) {
		var target = event.target;
		var dataField = target.getAttribute('data-field'), updateData = {};
		updateData[dataField] = target.value;
		this.setState(updateData)
	},
	render: function () {
		return (
			<form method="post" onSubmit={this.handleSubmit}>
				<label>账号</label>
				<input type="text" value={this.state.account} data-field="account" onChange={this.handleChange} disabled={this.state.isPost} />  

				<label>密码</label>
				<input type="password" value={this.state.password} data-field="password" onChange={this.handleChange} disabled={this.state.isPost} />  

				<button disabled={this.state.isPost}>登录</button>
				{this.state.isPost ? 

登录中,请稍候!
 : ''}
			</form>
		);
	}
});

ReactDOM.render(<LoginForm />, document.getElementById('hello'));

最后执行gulp jsx,刷新页面,你就能看到效果了。

执行gulp watch-jsx,你就可以一边修改一边预览了。

个人经验

至此,你已经基本了解了ReactJS的内容,可以开始尝试写一些自己的组件了。

官网的例子呢,显得十分高大上,比如getting start那个Comment Box,组件内套着组件。

但从实际出发,这里有一个开发者经常容易出现的问题,就是过度封装,最后导致自己在不同的组件中不停的切换调试,而找不到问题的根源。而在React里,最显著的问题就是props和state之间的转换问题,当你调用<Component prop="value" />将参数传入时,是props,你还需要再处理从props到state之间的转换,也就是当你有更多封装,你需要关注到底这个组件的props是不是有效的转换为state了,这是一个很繁琐的问题,随着你封装的越具体,调试的复杂度和维度随之增长,真的得不偿失。经常会发生,某个组件处理不得当,导致某个部分无法实时更新数据。

所以,个人建议,最好的做法是,对你的界面做一个整体规划,整体框架性渲染,统一在一个类中,在这个框架中,划分出具体的区域,然后将不同的组件加载渲染这些对应的区域中区,而不要想着在一个框架内,嵌入不同的组件,借助一个整体的state去联动多个嵌套组件的update,那样你要调试的工作量实在太大了,真是调到天荒地老。

比如,我的项目中,规划出Page和Component两个逻辑分区,Page表述的是大框架,Page更新,就全页都更新。而Component则是具体的组件,并且我为组件写了特别的加载方法,他不是通过嵌入在Page的html代码中的方式来加载的,而是通过特定的事件触发,来找到对应的Container来装载的,这样有助于我更灵活和准确的来调试Page和Component。

补充的说,用ReactJS开发,尤其是如果你是全部用ReactJS来开发的话(比如我手头的一个项目,因为需要用WebSocket做一些通信,我希望页面本身维持不刷新),其实开发的情况就和开发手机App无差别了,因为所有的数据你都需要本地获取一次。如何优化异步加载获取数据的合理性就变得更加重要,而ReactJS并不提供这方面的解决方案。于是乎,我用了一天的时间,封装了一个非常简易的前端Model,这个Model只是用于装载数据和维持数据实例的检索,而不关心具体的数据逻辑(数据逻辑还是交给后端处理吧),这方面前端做的再精细也枉然。

不得不说,ReactJS真的是一个非常伟大的东西,他的确解放了前端的开发工作量,基于ReactJS封装的一个组件,重用性远比用html或者别的什么机制写成的方式输出的组件,模板那些就更别提了。非常推荐前端,乃至后端的开发人员都尝试