React是Facebook公司推出的前端组件化解决方案,目的在于解决前端开发中存在的各个痛点。目前,前端框架与库层出不穷,形成了异常繁荣的局面,那么Facebook为何还要重复造轮子呢?究其原因,Facebook认为现有的前端解决方案都不是很好(甚至Facebook认为MVC本身也是有问题的),无法解决自己在实际开发中面临的种种问题,于是自己就开发出了React并将其开源;同时,基于React,Facebook又推出了React Native,旨在使用前端开发者熟悉的JavaScript等技术来开发原生App,实现一套代码运行在iOS与Android等移动平台上。一经推出,React与React Native就得到了开发者的极大关注,短时间内其在GitHub上就获得了大量的关注,目前也是前端开发领域最火热的技术之一。基于这一点,本文将会介绍React开发入门知识,通过一个实际可运行的案例带领大家一步步掌握React开发的步骤,厘清React开发的各项知识点,同时对于开发过程中所用的工具有一定的认识和掌握。
开发IDE
同时,文中案例的开发是在JetBrains公司的WebStorm 2016.2版本(也是目前WebStorm的最新版)下进行的。WebStorm是一款非常强大的前端开发者一站式工具,内置了前端开发者日常所用的大多数工具的支持,而且提供了强大的代码提示、自动完成、代码高亮等特性,是一个开箱机用的优秀工具。对WebStorm感兴趣的读者可以参考我之前所写的一篇文章。
前端框架分析
值得一提的是,目前的前端开发领域非常繁荣,各种工具、库、理念层出不穷,再也不是前几年jQuery一个库打天下的境况了。随着前端开发领域的持续发展,这一局面一定还会继续下去,我个人认为目前前端的流行趋势是这样的:
- React:由Facebook推出,通过单向数据绑定、虚拟DOM以及强大的生态圈(Flux、Redux等等),再配合WebPack等工具,掀起了前端的一阵旋风,也是各大公司前端所热烈追逐的技术,目前呈现出非常强劲的发展势头,更新频率也非常快速。
- Angular:由Google推出,通过双向数据绑定以及一整套完善的指令与库实现了前端的一站式开发,目前也是SPA(Single Page Application,单页面应用)的重要选择之一。不久之后将会发布的Angular 2.0将会成为Angular的一个重要里程碑,新版本增加了诸多重要功能与特性,值得每一个前端开发者重点掌握。
- jQuery:老牌的前端开发工具库,目前依然得到了广泛的应用。jQuery已经流行很多年了,而且在可预计的未来还将一直流行下去。它简化了前端开发者的日常工作,通过简洁的函数封装实现了DOM操作、CSS操作、Ajax调用、各种动画效果,浏览器兼容性等诸多功能,是最受前端开发者青睐的工具之一。而且,jQuery还可以很好地与React及Angular进行集成,便于复用组织内已有的基础设施,增强效率。
本文将会重点关注于React,通过一个实际可运行的示例来一步步演示React的开发过程,同时还会给出关于工具、开发等的一些最佳实践。
本文所选取的示例来自于React官网,不过进行了一定程度的增强和完善,更加便于React新手学习;同时,对于工具的使用也给出了一些建议。
开发工具
虽然本文介绍的是React前端开发,不过为了保持示例的完整性,文中同时给出了后端代码,这样学习者就可以直接在本机启动服务器运行示例了。该示例虽然不大,但使用的工具还是不少的,希望大家能一步步跟着我的步伐操练起来。
本文所使用的主要工具与库如下所示:
- 开发机器:MacBook Pro
- 开发工具:Web Storm 2016.2版本
- Node
- Express
- nvm
- npm
- React
- jQuery
- JSON
值得一提的是,虽然可以通过任何文本编辑器来实现本文的示例,但我这里选取的是JetBrains公司的WebStorm作为开发工具,目的在于提升效率,让开发者将注意力放在React本身,而不是工具的各项配置上。WebStorm提供了开箱机用的强大功能,是一款不可多得的前端开发伴侣。
项目简介
本文将会带领读者使用React实现一个简单的留言系统,使用者通过输入自己的姓名与评论内容来发布评论。评论发布完毕后可以显示出评论列表;此外,程序还将通过轮询的方式在不刷新页面的情况下自动获取其他评论者的评论内容。这就是本应用的主题功能。由于本文主要突出React的使用介绍,因此对于样式等方面几乎没有做任何优化。该系统实际运行的样子如下图所示:
项目开发
首先需要安装项目所用的工具,该项目的后端采用Node进行开发,因此需要先安装Node。
直接安装Node是非常简单的事情,在Mac平台上只需从Node官网(https://nodejs.org)下载Node的安装包即可双击安装,同时还会自动安装npm(Node的包管理器)。目前Node的最新版本是6.3.1。不过这样安装Node存在一个严重的问题:Node现在的发展速度非常快,版本更迭也非常频繁,可能安装不就之后Node就发布了新的版本,这时如果要体验Node的最新版特性就变得比较困难了。因为一方面要保留老的Node版本供系统开发所用,另一方面还想要尝试Node的新特性。那该怎么办呢?答案就是使用nvm(Node Version Manager)。
nvm是一个优秀的Node版本管理器,可以使多个Node版本在同一台电脑上共存并且互不影响,而且还能轻松实现各个版本的Node切换。此外,它还支持查询本地安装的各个Node版本,支持查询远程发布的所有Node版本与iojs(之前从Node分裂出来的一个版本,不过后来Node与iojs又合并了)版本,并且安装也是非常便捷的。
安装nvm:
[plain] view plain copy print?
- curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.3/install.sh | bash
只需通过上述一行命令即可在Mac上安装nvm。
安装完毕后在Terminal中输入命令:nvm help即可列出nvm支持的各项命令,比如说:
- nvm --version:显示nvm版本
- nvm ls:列出本地安装的所有Node版本
- nvm ls-remote:列出远程所有的Node与iojs版本
- nvm current:显示当前激活的Node版本
- nvm install v0.10.32:安装v0.10.32这个Node版本
- nvm use 0.10:使用0.10这个Node版本
- nvm alias default 0.10.32:将0.10.32这一版本作为默认使用的Node版本
以上只列出了nvm众多功能的一些重要功能,感兴趣的读者可以自行安装nvm并查看命令。对于我们这个示例来说,我们安装最新版本的Node即可,命令如下所示:
[plain] view plain copy print?
- nvm install v6.3.1
上述命令同时还会自动安装v6.3.1版本的Node所对应的npm,安装完毕后输入命令:
[plain] view plain copy print?
- nvm alias default v6.3.1
上述命令用于将v6.3.1这一版本作为系统默认使用的Node版本。至此为止,Node与npm就安装完毕了。
接下来,打开WebStorm,新建工程并命名为:react-tutorial,如下图所示(这是完整的工程文件目录结构):
在工程中新建文件package.json(Node的包管理描述文件),输入项目所需用到的依赖以及项目名字等基本信息,如下代码所示:
[plain] view plain copy print?
- {
- "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库。
然后在项目所在目录下执行命令:
[plain] view plain copy print?
- npm install
这时,npm会根据package.json的文件内容自动解析依赖并下载到项目目录的node_modules下面,如下图所示:
在项目目录下新建目录public,然后在public目录下新建两个子目录:css与scripts,分别用于存放项目所用的CSS文件与JavaScript文件。
在项目根目录下新建文件server.js,在server.js文件中编写如下代码:
[javascript] view plain copy print?
- 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') + '/');
- });
该文件主要有两个作用:
- 作为服务器启动,启动后监听端口3000
- 作为API服务者,向外提供接口/api/comments
该文件是一个典型的Nodejs服务器文件,使用到了目前Nodejs领域流行的Express框架(Koa是另外一个流行的的服务器框架,是由Express框架的原班人马开发的,感兴趣的读者也可以了解一下);此外,读者可以看到,该文件还向外提供了一个接口/api/comments,同时提供了两种调用方式,分别是get方式与post方式,这实际上是一个典型的RESTFul接口,针对评论这一资源提供两种调用方式:get用于查询评论,post则用于发表评论。同时,应用为了简化,将新的评论保存到了comments.json文件中。
另外值得一提的是,对于每一个评论都会有一个唯一的主键,这里的主键生成方式采用了uuid模块的方法,用于生成全局唯一的uuid标识符作为每一条新评论的主键。
通过如下命令来启动node server:
[plain] view plain copy print?
- node server
服务器启动后即会监听3000端口的访问。
确保服务器启动没有任何异常信息后,使用ctrl+c来关闭服务器。
在public目录下的css目录中新建一个CSS文件base.css,其内容如下所示:
[plain] view plain copy print?
- 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,输入如下内容:
[html] view plain copy print?
- <!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文件,下面分别介绍:
- react.js:React的核心文件
- react-dom.js:React针对浏览器DOM的文件,它实现了虚拟DOM与浏览器真实DOM之间的映射与转换,并实现了diff dom算法
- browser.js:实现React的JSX语法到原生JavaScript的转换
- jquery.min.js:jQuery的依赖文件,这里通过jQuery向服务器端发送Ajax请求
- remarkable.min.js:实现评论的markdown功能,这样就可以通过markdown语法发送评论了
这里值得重点关注的是前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文件如下代码清单所示:
[javascript] view plain copy print?
- 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的深度内容介绍。