React是Facebook公司推出的前端组件化解决方案,目的在于解决前端开发中存在的各个痛点。目前,前端框架与库层出不穷,形成了异常繁荣的局面,那么Facebook为何还要重复造轮子呢?究其原因,Facebook认为现有的前端解决方案都不是很好(甚至Facebook认为MVC本身也是有问题的),无法解决自己在实际开发中面临的种种问题,于是自己就开发出了React并将其开源;同时,基于React,Facebook又推出了React Native,旨在使用前端开发者熟悉的JavaScript等技术来开发原生App,实现一套代码运行在iOS与Android等移动平台上。一经推出,React与React Native就得到了开发者的极大关注,短时间内其在GitHub上就获得了大量的关注,目前也是前端开发领域最火热的技术之一。基于这一点,本文将会介绍React开发入门知识,通过一个实际可运行的案例带领大家一步步掌握React开发的步骤,厘清React开发的各项知识点,同时对于开发过程中所用的工具有一定的认识和掌握。
本文将会重点关注于React,通过一个实际可运行的示例来一步步演示React的开发过程,同时还会给出关于工具、开发等的一些最佳实践。
本文所选取的示例 来自于React官网,不过进行了一定程度的增强和完善,更加便于React新手学习;同时,对于工具的使用也给出了一些建议。
虽然本文介绍的是React前端开发,不过为了保持示例的完整性,文中同时给出了后端代码,这样学习者就可以直接在本机启动服务器运行示例了。该示例虽然不大,但使用的工具还是不少的,希望大家能一步步跟着我的步伐操练起来。
本文所使用的主要工具与库如下所示:
首先需要安装项目所用的工具,该项目的后端采用Node进行开发,因此需要先安装Node。
安装nvm:
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.3/install.sh | bash
只需通过上述一行命令即可在Mac上安装nvm。
安装完毕后在Terminal中输入命令:nvm help即可列出nvm支持的各项命令,比如说:
nvm install v6.3.1
上述命令同时还会自动安装v6.3.1版本的Node所对应的npm,安装完毕后输入命令:
nvm alias default v6.3.1
{ "name": "React_Tutorial", "version": "0.1.1", "private": true, "main": "server.js", "dependencies": { "body-parser": "^1.4.3", "express": "^4.4.5", "uuid": "^2.0.0" } }
我们这个项目使用到了Express框架、body-parser以及用于生成uuid的uuid库。
然后在项目所在目录下执行命令:
npm install
在项目目录下新建目录public,然后在public目录下新建两个子目录:css与scripts,分别用于存放项目所用的CSS文件与JavaScript文件。
在项目根目录下新建文件server.js,在server.js文件中编写如下代码:
var fs = require('fs'); var path = require('path'); var express = require('express'); var bodyParser = require('body-parser'); var uuid = require('uuid'); var app = express(); var COMMENTS_FILE = path.join(__dirname, 'comments.json'); 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.use(function(req, res, next) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Cache-Control', 'no-cache'); next(); }); app.get('/api/comments', function(req, res) { fs.readFile(COMMENTS_FILE, function(err, data) { if (err) { console.error(err); process.exit(1); } res.json(JSON.parse(data)); }); }); app.post('/api/comments', function(req, res) { fs.readFile(COMMENTS_FILE, function(err, data) { if (err) { console.error(err); process.exit(1); } var comments = JSON.parse(data); var newComment = { id: uuid.v4(), author: req.body.author, text: req.body.text, }; comments.push(newComment); fs.writeFile(COMMENTS_FILE, JSON.stringify(comments, null, 4), function(err) { if (err) { console.error(err); process.exit(1); } res.json(comments); }); }); }); app.listen(app.get('port'), function() { console.log('Server started: http://localhost:' + app.get('port') + '/'); });
该文件主要有两个作用:
该文件是一个典型的Nodejs服务器文件,使用到了目前Nodejs领域流行的Express框架(Koa是另外一个流行的的服务器框架,是由Express框架的原班人马开发的,感兴趣的读者也可以了解一下);此外,读者可以看到,该文件还向外提供了一个接口/api/comments,同时提供了两种调用方式,分别是get方式与post方式,这实际上是一个典型的RESTFul接口,针对评论这一资源提供两种调用方式:get用于查询评论,post则用于发表评论。同时,应用为了简化,将新的评论保存到了comments.json文件中。
另外值得一提的是,对于每一个评论都会有一个唯一的主键,这里的主键生成方式采用了uuid模块的方法,用于生成全局唯一的uuid标识符作为每一条新评论的主键。
通过如下命令来启动node server:
node server
服务器启动后即会监听3000端口的访问。
确保服务器启动没有任何异常信息后,使用ctrl+c来关闭服务器。
在public目录下的css目录中新建一个CSS文件base.css,其内容如下所示:
body { background: #fff; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 15px; line-height: 1.7; margin: 0; padding: 30px; } a { color: #4183c4; text-decoration: none; } a:hover { text-decoration: underline; } code { background-color: #f8f8f8; border: 1px solid #ddd; border-radius: 3px; font-family: "Bitstream Vera Sans Mono", Consolas, Courier, monospace; font-size: 12px; margin: 0 2px; padding: 0 5px; } h1, h2, h3, h4 { font-weight: bold; margin: 0 0 15px; padding: 0; } h1 { border-bottom: 1px solid #ddd; font-size: 2.5em; } h2 { border-bottom: 1px solid #eee; font-size: 2em; } h3 { font-size: 1.5em; } h4 { border-bottom: 1px solid #eee; font-size: 1.2em; } p, ul { margin: 15px 0; } ul { padding-left: 30px; }
该CSS文件的内容都是一些基本的样式信息,这里不再赘述。
下面进入到本文最为关键与核心的部分——React。
在public目录中新建文件index.html,输入如下内容:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>React Tutorial</title> <!-- Not present in the tutorial. Just for basic styling. --> <link rel="stylesheet" href="css/base.css" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.2.0/react.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.2.0/react-dom.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.6.16/browser.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/remarkable/1.6.2/remarkable.min.js"></script> </head> <body> <div id="contentContainer"></div> <script type="text/babel" src="scripts/test.js"></script> </body> </html>
从中可以看到,该文件基本上算是一个空的html文件,只是引入了一些外部js文件与css文件,这正是React编程的方式。
除了引入方才创建的base.css文件外,该文件在head部分还引入了5个js文件,下面分别介绍:
这里值得重点关注的是前3个文件,react.js与react-dom.js是我们使用React所必须的两个文件;另外,由于React建议使用JSX语法来编写组件声明,而JSX需要在浏览器端转换为原生的JavaScript文件,因此需要一个转换工具,而browser.js文件就是起到这个作用的;jquery.min.js与remarkable.min.js则是针对于本项目所需的功能而引入的两个文件。
下面来编写本项目所需的最后一个文件。在public目录的scripts目录下新建文件test.js。
React是基于组件化开发的,因此在一开始我们需要先设计好页面的组件以及组件之间的关系。下面是页面运行时的截图:
从图中可以看到,该页面实际上由几个部分构成:
综上所述,该页面的组件构成与包含关系应该如下图所示:
接下来就需要定义各个组件了,test.js文件如下代码清单所示:
var Comment = React.createClass({ rawMarkup: function() { var md = new Remarkable(); var rawMarkup = md.render(this.props.children.toString()); return { __html: rawMarkup }; }, render: function() { return ( <div className="comment"> <h4 className="commentAuthor"> {this.props.author} 说: <span dangerouslySetInnerHTML={this.rawMarkup()} /> </h4> </div> ); } }); 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; 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> ); } }); 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> ); } }); 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="昵称" value={this.state.author} onChange={this.handleAuthorChange} /> <input type="text" placeholder="评论内容" value={this.state.text} onChange={this.handleTextChange} /> <input type="submit" value="提交评论" /> </form> ); } }); ReactDOM.render( <CommentBox url="/api/comments" pollInterval={3000} />, document.getElementById('contentContainer') );
从上面的代码中我们可以看到,系统一共定义了4个组件,分别是Comment、CommentBox、CommentList与CommentForm,最下面则通过ReactDOM的render方法将CommentBox组件插入到外层容器contentContainer中。
在上述代码中,我们与服务器之间的异步通信使用了jQuery,实际上也可以使用其他方式,React对于这一点并没有任何限制。而组件之间的包含关系则是CommentList包含了Comment、CommentBox包含了CommentList与CommentForm。最后则通过ReactDOM的render方法将CommentBox插入到了外层容器中。
上述代码中定义组件的方式使用了React.createClass方法,这是React提供的定义组件的一般方法,每一个组件都需要提供一个render方法,用于指定组件的渲染方式与包含关系,这里使用了React 的JSX语法。实际上,也可以通过原生的JavaScript来实现,不过React官方强烈推荐使用JSX语法,因为它简洁、可读性好,同时类似于XML语法,使用起来非常直观方便,感兴趣的读者可以到React官网阅读JSX语法指南,还是比较简单的。
另外,在ReactDOM的render方法中,我们为CommentBox组件指定了属性pollInterval,值为3000,这表示每隔3秒钟会向服务器发起一个异步请求,用于获取最新的评论列表。实际上,这里可以通过WebSocket来实现,效率更好,同时也省去了轮询的烦恼,这一步可以由读者自行实现。
数据的存储我们使用comments.json文件,由于本教程主要讲解React的使用,因此存储这块就没有使用数据库,实际情况下,这部分应该使用诸如MongoDB之类的数据库来实现,也是比较容易的。如果使用MongoDB,那么可以使用Mongoose,这是个面向Nodejs的MongoDB ODM(Object-Document Mapping,对象文档映射)框架,可以实现领域模型与数据库文档之间的映射,使用起来非常方便。
本文主要起到React入门的作用,目的在于通过一个实际可运行的示例来演示React的基本用法,并未涉及到React的深层次知识,比如说Flux、Redux、WebPack与React整合等等。
学习是需要循序渐进的,只有入门了才能进一步深入下去,希望读者在学习完本文后能够开启React的学习之旅,我也将在后面为大家带来React的深度内容介绍。