Facebook 发布的React框架采用了与其他前端框架不同的设计思路,下面通过官网的例子来进一步学习这个开源框架,看看能给前端设计带来怎样的惊喜。
一、准备工作
1、先从官网http://facebook.github.io/react/下载React(版本)压缩包,解压后有readme.md文件和两个文件夹,框架的js文件在build文件夹中,主要会用到react.js和JSXTransformer.js这两个文件。
2、例子需要与服务器端交互,可选择很多(python, ruby,php和js),我们可以根据自己的语言习惯自由选择,这里会用到node.js和npm。node.js和npm的安装请参考其他文章,百度一搜一大把。
3、IDE,文本编辑器,推荐webstorm。
4、项目的本地文件夹结构
项目文件夹(React_Post)
|------public(放置js, css,html等项目静态文件)
|--------javascript
|----------react.js
|---------JSXTransformer.js
index.html
|------package.json
|------server.js
|------comments.json
二、建立服务器
如果对node.js、express非常熟悉,这一部分可以一带而过。
1、编辑package.json
{
"name": "react-tutorial",
"version": "0.0.0",
"description": "Code from the React tutorial.",
"main": "server.js",
"dependencies": {
"body-parser": "^1.4.3",
"express": "^4.4.5"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "https://github.com/reactjs/react-tutorial.git"
},
"keywords": [
"react",
"tutorial",
"comment",
"example"
],
"author": "petehunt",
"bugs": {
"url": "https://github.com/reactjs/react-tutorial/issues"
},
"homepage": "https://github.com/reactjs/react-tutorial",
"engines" : {
"node" : "0.12.x"
}
}
package.json文件是node.js项目启动的配置文件,声明项目需要的依赖包。
2、编辑server.js
var fs = require('fs');
var path = require('path');
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
app.set('port', (process.env.PORT || 3000));
app.use('/', express.static(path.join(__dirname, 'public')));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.get('/comments.json', function(req, res) {
fs.readFile('comments.json', function(err, data) {
res.setHeader('Content-Type', 'application/json');
res.send(data);
});
});
app.post('/comments.json', function(req, res) {
fs.readFile('comments.json', function(err, data) {
var comments = JSON.parse(data);
comments.push(req.body);
fs.writeFile('comments.json', JSON.stringify(comments, null, 4), function(err) {
res.setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'no-cache');
res.send(JSON.stringify(comments));
});
});
});
app.listen(app.get('port'), function() {
console.log('Server started: http://localhost:' + app.get('port') + '/');
});
在命令提示模式下,进入到项目根目录,执行命令npm install,程序会根据package.json的配置开始下载并安装依赖模块(记得联网),命令完成后,项目根目录会产生一个node_modules文件夹,对应模块已经安装完成。
还是在命令提示符的项目根目录下执行命令node server.js。如果一切没有问题,服务器已经启动,端口3000。默认public文件夹为web根目录。打开浏览器(开发模式下推荐使用firefox或chrome)输入http://localhost:3000,就可以看到public中的index.html了。注意这里还有一个comments.json 的get和post请求。这里先不展开,下文会用到这个文件。
三、搭建前端应用
前端的搭建全部在index.html这个文件中完成。这个应用模拟实时聊天或者也可以叫blog的实时评价雏形。项目没有定义css外观,主要从数据和UI控制方面介绍React的特点。废话不多说,代码如下:
第一步:应用骨架index.html
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Hello React</title>
<script src="javasript/react.js"></script>
<script src="javasript/JSXTransformer.js"></script>
<script src="http://cdn.bootcss.com/jquery/2.1.3/jquery.js"></script> </head>
<body>
<div id="content"></div><!--这里的div用于附加UI-->
<script type="text/jsx">
// 在这里输入代码,用于UI结构创建、事件绑定、数据绑定和渲染表现
</script>
</body>
</html>
载入React和JSXTransformer。jquery主要用于发起ajax请求,也需要嵌入,这里使用了bootcss的CDN服务,这样就不用把jquery下载到本地,但需要联网,当然也可以下载到本地加载。
需要注意不同于一般的js书写方法,script标签的type属性值为text/jsx,这里用到了jsx的语法。如果不加声明的话,编译无法通过。JSXTransformer就是用于jsx语法转译的,仅适用于开发环境,如果用于最终产品,需要对jsx单独编译转为js,再嵌回html。这样就不用再加载JSXTransformer了。更为深入的细节请参考官方文档。
第二步:建立UI结构
简单地说,React不针对整体页面结构,而非常适合于UI(用户交互组件)结构(比页面结构更细的粒度)的分割、组合和重用。本例中的UI结构如下图:
CommentBox为整个组件的容器,组件结构是类似于XML,因此有且只有一个根元素,如果设计组件的过程出现了多个并行关系的父元素,那么就不符合React的设计思路,需要分拆和重构。
Step1:对应上边的组件关系,先写出父元素CommentBox的创建和渲染。
var CommentBox=React.createClass({
render:function(){
return(
<div className="CommentBox">
<h1>这个是CommentBox</h1>
</div>
);
}
});
React.render(
<CommentBox/>,
document.getElementByID('content')
);
代码解读:使用React写代码,个人感觉最好从根元素开始创建,然后逐步叠加子元素,要不随着元素增加,会把自己搞个晕头转向。
createClass方法从字面意思看就是创建一个类。这个类中有方法和属性,其中的render方法定义了进行渲染的DOM元素(XMLish,与xml相似的,有且只有一个根元素,标签配对)。还有大量的其他内建方法和属性(比如mixins,类似于依赖注入)。React.render是实际执行和嵌入dom元素的方法,这里嵌入了CommnetBox,附加在id为content的div元素之后。
在浏览器输入http://localhost:3000,可以看到输出结果。
Step2:定义子元素,改写父元素
var CommentList=React.createClass({
render:function(){
return(
<div className="commnetList">这个是CommnetList</div>
);
}
});
var CommentForm=React.createClass({
render:function(){
return(
<div className="commentForm">这个是CommentForm</div>
);
}
});
var CommentBox=React.createClass({
render:function(){
return(
<div className=commentBox>
<h1>这个是CommentBox</h1>
<CommentList/>
<CommentForm/>
</div>
);
}
});
React.render(
<CommentBox/>,
document.getElementByID('content')
);
刷新浏览器,检查输出结果。CommentList和CommentForm元素已经嵌入CommentBox。
Step3: 使用props,定义Comment,改写CommentList
CommentList元素负责加载呈现多条Comment,在CommentList之上,再定义Comment元素,然后嵌入CommentList。
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}//jxf语法,获取父元素的数据
</h2>
{this.props.children}//jxf语法,获取父元素的数据
</div>
);
}
});
问题出来了,this.props是什么玩意儿?到这,基本上会把我们弄的一头雾水。一般的理解:React通过内建的pops属性进行数据传递,那么数据从哪来?从父元素来,单向传递。this可以简单地理解为父元素的实例。
Comment元素的父元素是CommentList,因此需要在CommentList里定义数据,向下(子元素)传递。改写CommentList。
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>
);
}
});
好了,刷新浏览器,会有两条comment。
Step4: 数据分离
到第三步,可以看到数据与表现耦合度太高,需要分离数据解耦。问题来了,数据定义在哪?如何传递?
在整个脚本的顶层独立定义数据,通过CommenBox传递。why? React从父元素通过pops属性进行数据单向(向下/子元素)传递。
var data = [
{author: "Pete Hunt", text: "This is one comment"},
{author: "Jordan Walke", text: "This is *another* comment"}
];
var Comment=React.createClass({
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}//jxf语法,获取父元素的数据
</h2>
{this.props.children}//jxf语法,获取父元素的数据
</div>
);
}
});
var CommentList=React.createClass({
render: function() {
var commentNodes = this.props.data.map(function (comment) {//通过map方法,先把comment组合为commentNodes组件,并向Comment传递数据。
return (
<Comment author={comment.author}>
{comment.text}
</Comment>
);
});
return(
<div className="commentList">
{commentNodes} //在CommentList中嵌入commentNodes组件
</div>
);
}
});
var CommentForm=React.createClass({
........
});
var CommentBox=React.createClass({
render:function(){
return(
<div className=commentBox>
<h1>这个是CommentBox</h1>
<CommentList data={this.props.data}/>
<CommentForm/>
</div>
);
}
});
React.render(
<CommentBox data={data}/>,
document.getElementById('content')
);
这一步的数据传递稍有点复杂,见下图:
Step5: 数据再次分离
到第四步,数据已经和表现进行了分离,但还是硬编码在脚本中,需要再次分离到json文件中解耦,用jquery的ajax异步读取数据。改写React.render中的<CommentBox/>,删除属性data,加入属性url并指向数据文件comments.json,这里把data属性变成了url,这个值代表的文件地址将传递给CommentBox组件,通过ajax调用获取数据。
根据server.js的配置,在项目根文件夹下新建comments.json,写入json数据。这时,我们不再需要脚本中的data定义,删除即可。提示:这里有个小坑,在脚本中定义的数据键值对,键名不加引号,但在json文件中的键名需要加引号。
React.render(
<CommentBox url="comments.json"/>,
document.getElementById('content')
);
到这一步,数据分离基本完成,但是还不能正常工作。
Step6: React的state
在完成所有的代码前,需要引入React的state的概念。听上去比较抽象,实际上state解决的问题就是一个组件数据的改变如何影响到其他组件的数据和视图。React把每个组件都视为具有一定状态的东西,当组件的状态(state)发生改变,React就会自动更行。
修改CommentBox组件,添加状态约束。
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
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>
);
}
});
组件中getInitialState方法(React内建方法,不要拼错哦)给定了CommentBox组件的初始状态,给该组件的state附加了一个属性名为data还未赋值的空数组。componentDidMount方法定义了当组件经过渲染真实地插入到DOM中可执行的代码。这里用到了jquery的ajax请求。当请求数据返回成功时,调用了React的setState方法,改变了组件的状态,那么React的事件驱动机制就会自动更新子元素CommentList的data属性,并向下传递新的数据。
这时,当我们单独的修改coments.json中的数据,页面并不会跟着一起变化。不像angluar.js提供的双向数据绑定,React提供的是单向绑定,React有自己的理由,详细的原因请参照官方文档。
例子中提供了一个方法,让我们定时(每隔2秒)的向服务器端发出ajax请求,用于自动更新页面,以模拟数据变化时带来的视图自动更新。继续修改CommentBox。
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
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);
//每隔2秒执行loadCommentsFromServer方法,改变state,传递数据,改变视图
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>
);
}
});
React.render(
<CommentBox url="comments.json" pollInterval={2000} />,//定义属性pollInterval为2秒,每隔2秒向服务器发起ajax请求
document.getElementById('content')
);
到目前为止,可以刷新浏览器,查看输出,在firebug中可以看到每隔一段时间就会发起http一个请求。单独修改commens.json文件也可以看到即时效果。
Step7: 完善CommentForm,让提交结果立刻反映在页面中
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>
);
}
});
创建了一个form、两个文本框和一个提交按钮。这个form用于向comments.json写入数据,并立刻反馈在页面中,所有的用户可以同时看到。增加页面提交的处理函数。
var CommentForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();//阻止submit默认提交行为
var author = React.findDOMNode(this.refs.author).value.trim();
var text = React.findDOMNode(this.refs.text).value.trim();
if (!text || !author) {
return;
}
//这里写入提交数据处理方法
React.findDOMNode(this.refs.author).value = '';
React.findDOMNode(this.refs.text).value = '';
return;
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
当从实际插入到页面DOM取值的时候,React提供了findDOMNode方法,并把DOM的引用(refs)作为传递参数,因此被引用的元素需要定义ref。这一步获取的数据如何插入到comments.json中呢?React建议把数据处理提交到父元素层面进行,把处理函数作为pops的属性传递给CommentForm。改写CommentBox和CommentForm。
CommentBox:
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
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) {//form 提交处理函数,post给comments.json文件,类似REST服务,具体产看server.js
$.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} />//把handleCommentSubmit通过onCommentSubmit传递给CommentForm使用
</div>
);
}
});
CommentForm:
var CommentForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var author = React.findDOMNode(this.refs.author).value.trim();
var text = React.findDOMNode(this.refs.text).value.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});//获得了onCommentSubmit处理函数,并执行数据插入操作。
React.findDOMNode(this.refs.author).value = '';
React.findDOMNode(this.refs.text).value = '';
return;
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
到这一步,基本完成了这个例子。可以在页面中尝试输入,查看结果。我们可以设想,comments.json需要多次IO,会带来性能上的损失。不可避免的会产生阻塞。React又给了提升性能的一招。在CommentBox异步提交处理前,先更新state,让结果快速呈现在我们面前。
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
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;//获取state
var newComments = comments.concat([comment]);//追加数据
this.setState({data: newComments});//改变state更新数据和视图
$.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} />//把handleCommentSubmit通过onCommentSubmit传递给CommentForm使用
</div>
);
}
});
这里有个坑,由于要修改comments.json文件,因此需要把comments.json文件权限修改为任何人可读写,如果文件拒绝写入,结果可能会失败。
可以在这个基础上继续扩展,尝试和Mongodb连接,加入安全认证,markdown支持等