使用Webpack和Gulp构建ReactJS应用
前言
本文一共四个部分,包括,研究背景,ReactJS入门,相关工具和技术,具体实施和注解。主要是使用ReactJS和ES6进行开发,结合webpack,gulp,babel等工具,尝试构建一个简单的ReactJS应用,同时,在数据库服务端交互上用了jquery的ajax,在样式上,引入了React-bootstrap. 本文所述是对一个结合组件化,自动构建和打包,可与数据库服务端交互的ReactJS项目的初级研究。
一.背景
我们都知道很多MVVM或者MVC的前端开发框架,Angular,Vue,Knockout等,那React是一个什么样的框架呢?这里所说的ReactJS只是用来构建前端UI组件的一个js库,可以理解为它只是view层的库,如官方的说法:REACT IS A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES。所以我们学习React时,就需要先抛开MVVM,Two-way Binding,Model,template等概念。
二.ReactJS入门
1. ReactJS特点
1.1 Declarative
React和Html对页面上的元素的定义方式一样,是申明式的,Html是通过原生的标签去申明element和它们的属性,而React是通过xml(jsx)来申明UI组件的使用的。对于UI的渲染,用React.createElement和React.render方法实现页面元素渲染。
eg.
(1)定义组件类
class User extends React.Component{//定义组件类User
shouldComponentUpdate(){
return React.addons.PureRenderMixin.shouldComponentUpdate.apply(this, arguments);//使用高级组件PureRenderMixin来管理当前组件是否更新;
}
render(){
return (
< span className='user help-block text-warning bg-warning'>
{this.props.user.userName} ;{this.props.user.email}
< /span>
);
} }
User.propTypes = {
user:React.PropTypes.object.isRequired
};
export default User;
(2)在其他组件中import该组件并使用
import User from '../user/user';
class AppRoot extends React.Component{
shouldComponentUpdate () {
return React.addons.PureRenderMixin.shouldComponentUpdate.apply(this, arguments);
}
render(){
return (
//在JSX此处申明User组件实例;
< User user={this.props.state.user} />
);}
}
1.2 Component-Based
基于React的开发要求尽量将独立功能的模块写成具有独自的状态和运算逻辑的组件。这些组件能和其他组件协调工作,即设计的组件需要能和其父组件,子组件和其他组件之间进行信息共享。此处需要注意,组件化可以得到可复用的组件,但是ReactJS组件化的目标不是复用,而是分治,即UI组件自己维护自己的状态和数据流。不可以为了复用而导致组件内部逻辑过于臃肿。
1.3 单向数据流
React中,父组件到子组件或者子组件到父组件之间的数据是单向流动的。在一次UI渲染中,数据从顶层按照树形的结构往每个分支组件流动的同时,去产生VDOM树。最后再将VDOM渲染到document。
单个组件的更新是状态驱动的,即React发现当前组件的state变化时,去执行shouldComponentUpdate方法,如果需要更新,React会重新构建整个DOM树,和上一次的DOM树进行比较,以合适的方法对UI上变化的部分进行update。如何比较新的Dom和上一次的DOM,去最快的更新VDOM,React采用了分层对比,基于key(Note:React渲染到页面的元素都会有id属性)对比,深度优先遍历等算法。
Note:(1)虚拟DOM是document tree在内存中的一个映射,React根据组件的申明和组件的初始状态(props,state)将DOM tree添加到VD,并管理起来,state或者props变化的时候,React去比较dom node和vnode的变化来决定如何最快的更新VD和刷新页面。
(2)Virtual DOM的核心在于对原生javascript中document.createDocumentFragment()方法和DocumentFragment对象的应用。避免过多的document的直接操作。
1.4 Learn Once, Write Anywhere
React组件可以node从服务端渲染,也可以直接再浏览器端渲染,同时React Native开发的应用也可以使用同样的组件。
2.React组件的生命周期管理
生命周期,指一个React组件在渲染,挂载,更新,到卸载整个过程。以下通过一个每点击三次更新一次点击次数的demo来详细分析React组件的生命周期管理。
class Counter extends React.Component{
constructor(props, context) { super(props, context);
this.state = { count: this.props.initialCount }; }
handleClick () { this.setState({count:this.state.count + 1}); }, componentWillMount(){
} shouldComponentUpdate (nextProps, nextState){ if((nextState.count % 3) === 0){ return true; }else{ return false; } }
componentWillUpdate(){ console.log(this.state.count); }
componentDidUpdate(){ console.log(this.state.count); }
componentWillMount(){
},
componentDidMount(){ console.log(this.props); this.setState({count:this.props.initialCount + 1}); }
render(){ return <div><input onClick={this.handleClick} type='submit' value='add' /><span>{this.state.count}</span></div>; }
ReactDOM.render(<Counter initialCount={0} />,document.getElementById("app-counter")); | 申明Counter类,该类继承自 React.Component;
构造函数中调用父类的构造方法;
初始化state;
响应点击事件,更新组件的state。
该方法在组件被服务端渲染的时候和将要被浏览器渲染到页面上时,均会执行一次。
组件的state更新时,react默认会更新UI上对应的字段,该方法在state更新之后执行,如果返回false则不更新UI,React将不再计算DOM的变化,不再刷新页面。
shouldComponentUpdate()返回true之后执行。
React刷新页面之后执行。
组件在服务端渲染时执行,在浏览器挂载之前也执行。
组件在浏览器挂载之后执行,在服务端渲染时不执行。
初始化state之后,React根据render方法将组件挂载到页面上。
渲染组件到 id为app-counter的元素下。 |
3.JSX
JSX即javascript xml,将xml标签写在javascript中,但实际上,在js运行时,React会将jsx中的xml解释成js代码执行。ReactJS有两种方式创建element,
一是React.createElement方法,二是React官方推荐的jsx,因为jsx使节点之间的层次关系看起来很清晰,就像html文档树一样。
class UserInfo extends React.Component{ constructor(props, context) { super(props, context);
this.state = { users: [{ id:1, firsrName:'xx', latName:'yy', age:10, sex:'male' } }]; } render(){ let greeting = 'Hello '; let users = this.state.users; function getFullName(u){ return u.firstName+'.'+ u.lastName; } return ( <span className='text-info'>Total:{ users.length}</span> <ul>
users.map((item,index)=>{ return(
<li key={index}>
<span className='text-info'>{ item.lastName}</span>
<SexSeletor sex={ item.sex} />
<span>{if(item.age<18){'young'}}</span> <span>{getFullName(item)}}</span> </li> );}); </ul> ); } } |
render方法中定义的对象和当前组件的state与props可在标签中使用。{}内使用js 表达式。
将users.map方法返回的element添加到ul中。
建议为list类的元素增加key属性,该属性可帮React做DOM树的遍历和比较。
使用className代替html5中的class。Text-info是React-Bootstrap的样式
使用自定义component.
使用js表达式 使用function. |
4.组件之间通信
在React中,数据从root组件往各个分支节点流动,父子组件之间的通信往往通过引用对方的属性或者调用对方传递给自己的方法来实现。
4.1父组件到子组件/子组件到父组件
parent.js
class Parent extends React.Component{ constructor(props, context) { super(props, context); this.state = { userName :'test' }; } onTextChange:function(event){ this.setState({ userName: event.target.value }) }
onChildChanged(newName) { this.setState({ userName : newName }); } render(){ return (<Son userName ={this.state.userName } callbackParent={this.onChildChanged} />); } } |
定义onTextChanged 方法。Input的change事件发生时,更新parent组件的state,react根据这个state的变化去更新引用该state的子组件。此处实现父组件到子组件的信息传递。
定义onChildChanged 方法。
将onChildChanged方法传递给子组件。 |
son.js
class Son extends React.Component{ constructor(props, context) { super(props, context); this.state = { userName:'test' };} onUserNameChange:function(event){ this.props.callbackParent(event.target.value); } render(){ return (<span>username:<input value={this.props.userName} type='text' onChange={this. onUserNameChange } /></span>); } } |
子组件调用父组件传递的 callbackParent方法。 此处实现子组件向父组件传递信息; |
图 1 .父子组件通信
4.2 非相邻层次组件之间的通信
需要通信的组件之间没有嵌套关系或者层次过深,则可以引入PubSubJS 库,通过
PubSub.subscribe('message',callback(topic,data))和
PubSub.publish(' message',params)两个方法来实现组件间交流。
component1.js
class Component1 extends React.Component{ constructor(props, context) { super(props, context); this.state = { userName:'test' }; } componentDidMount: function () { this.pubsub_token = PubSub.subscribe('message', function (topic, data) { this.setState({ userName : data }); }.bind(this)); }, componentWillUnmount: function () { PubSub.unsubscribe(this.pubsub_token); } } |
这里定义了message订阅和对应的回调函数。并维护在当前组件
当组件从将要从document卸载时,删除订阅。 |
component2.js
class Component2 extends React.Component{ constructor(props, context) { super(props, context); this.state = { name:'test' }; } onChange: function (event) { this.setState({name: event.target.value }); PubSub.publish('message', this.state.name); }, render: function() { return <input onChange={this. onChange } value= {this.state.name} / >; }} |
数据变化时发布message。 |
三.相关工具和技术
3.1 ES6即ES2015
javascript的新标准。
3.2 webpack
前端应用模块管理工具 管理和加载依赖模块。使用各种loader去加载各类资源(js,css,img)
3.3 gulp
基于gulp插件管理任务,以帮助项目的自动化构建。
常用的gulp API:
3.3.1 gulp.task('clean',deps,fn);
eg,删除dist下的.js和.css文件:
gulp.task('clean', function(cb) {
del(['dist/.js','dist/.css'], cb)
});
3.3.2 gulp.src([]),gulp.dest(),.pipe();
eg,打包指定的css
gulp.task('minifyCss', function() {
return gulp.src(['./src/client/styles/.css','./src/client/styles/external/.css'])
.pipe(minifycss())//使用minificss处理gulp.src产生的文件流
.pipe(concat('styles.css'))//使用concat处理pipe中的文件流
.pipe(minifycss())//再次压缩
.pipe(rename({suffix: '.min'}))//重命名文件
.pipe(gulp.dest('./dist'));//写入文件的路径;
});
3.3.3 gulp.watch('js/**/.js',fn)
用来监视文件的变化,当文件发生变化后,我们可以利用它来执行相应的任务,例如文件压缩等.
3.4 some gulp plugins
uglify,代码压缩和混淆
minifycss,最小化css文件,gulp-imagemin,压缩jpg,png,gif等图片
concat,合并打包文件,rename,重命名文件,del,删除文件
webpack,模块打包工具
nodemon,管理node的启动和关闭
四.实施
4.1 目标
这个项目标是尝试React+ES6构建一个简单的web应用,其中应包含React组件,jsx,React-Bootstrap,jQuery Promise等相关知识或工具的实验,并使用webpack,gulp等工具实现打包,使用nodejs作为服务器,把程序运行起来。
4.2 项目结构
| webpack打包配置文件
libs:带插件的Reactjs和外部js库
通过npm安装到当前项目的node modules,包括gulp plugins和功能需要的js库 app:app源代码 client:通过res.sendFile()发送到客户端的资源 server:node服务端代码 babellrc:babel(ES6配置文件) gulpfile:引入gulp插件并定义task package:npm安装的js库的信息 |
4.3 构建过程
| Application 文件夹 所有的组件 Root 组件 Cart组件 Cart的每一个项定义成一个组件
用户的联系人组件
用户信息组件
定义一个User Model,其中包含user的方法 定义公用的工具类的方法(一般与具体业务无关) 加载AppRoot组件 App入口 客户端资源 客户端入口,同时也是webpack打包的入口 发送到客户端的页面 申明server,指定服务器端口,资源相对路径; 响应客户端请求 |
(1).安装nodejs. http://nodejs.cn/
(2).在WebStorm中搭建如4.2所示的项目结构
(3).从本地nodejs目录下通过cmd进入MyReactApp文件夹
安装如下模块($ npm install 'module name' –save)
react,
express,处理客户端请求
react-bootstrap,UI样式集合
babel-core, babel-loader, babel-preset-react, ES6/ES6-React解释器
jquery,此处用他的ajax方法,当然也可用async等异步模块提供promise
(4).定义各个组件
eg. appRoot.js
import React from 'react'; //在root组件引入各个子模块; import UserInfo from '../user-info/userInfo'; import Cart from '../cart/cart'; import Contact from '../contact/contact'; import UserModel from '../../models/user';
class AppRoot extends React.Component{ shouldComponentUpdate () { // 在当前组件的state或props变化时,使用React.addons.PureRenderMixin插件来决定是否重新渲染UI. return React.addons.PureRenderMixin.shouldComponentUpdate.apply(this, arguments); } render(){ let userModel = new UserModel(); return ( //.appRoot在styles.css中定义 <div className='appRoot'>
//申明各个组件 <UserInfo user={this.props.state.user} /> <Contact user={this.props.state.user} usersPromise={userModel.getUsers()} /> <Cart cart={this.props.state.cart} /> </div> ); } } export default AppRoot; |
(5). 安装如下模块
webpack, 打包各个模块
path, resolve相对路径
gulp,基于node stream的task管理工具
gulp-babel, gulp-react, gulp-webpack
gulp-uglify,用于压缩和混淆js代码
gulp-minify-css, 压缩css
gulp-nodemon,管理node server的启动
gulp-rename, 用于重命名打包文件
gulp-concat,连接文件
(6). 配置webpack config
webpack.config.js
var path = require('path'); var webpack = require('webpack'); module.exports = { /*配置externals的模块 1.这一类模块需要单独打包到一个js中,和应用组件的打包文件分开。 2.webpack通过import指令发现以下模块时,会通过window['moduleName']去引用这一类模块。如:window.jQuery,window.React. 3.单独打包外部library到dist目录下,在开发中,只需要打包修改的js或者css到dist供客户端访问即可。尤其是在dev-debug的要求下,这样打包更快,更容易区分开外部library和业务组件。如果发布到生产环境,可以省去externals,以减少客户端加载过程中请求的资源个数,但包含外部library的打包会比较慢。 */ externals: { 'react': 'window.React', 'jquery':'window.jQuery' }, entry:{ /* 这里配置了一个打包入口,webpack通过识别import指令,去找到该入口下的所有依赖模块和子模块的依赖模块,将所有模块打包,并且建立引用关系。 如果是一个多页面的应用,此处也可以配置多个入口。 */ application: path.resolve(__dirname,'../src/client/scripts/client.js'), //react: path.resolve(__dirname,'../libs/react/dist/react.js'), //utility: path.resolve(__dirname,'../src/app/utils/appUtilities.js'), }, output: { /* 配置打包之后的输出路径。 */ path: path.resolve(__dirname, '../dist'), /* 使用entry的名称命名输出文件.此处打包预期得到:application.js文件。 */ filename: '[name].js' }, resolve: { /* 配置可打包的被import的文件类型。 */ extensions: ['', '.js', '.jsx'] }, module: { loaders: [{ /* 配置程序语言解释器模块。 */ test: /\.js$/, loader: 'babel-loader' }, { test: /\.jsx$/, loader: 'babel-loader!jsx-loader?harmony' }] }, plugins: [ /* 此处配置模块之间的公共文件,将打包到common.js,在dev-debug时,同样可以注释改行配置,一简少打包时间消耗。 */ new webpack.optimize.CommonsChunkPlugin('common','common.js',['react',' utility']), new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.js', Infinity), /* 配置代码压缩插件 */ new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) ] }; |
(7). 配置.babelrc
{ /*在使用babel的项目根目下,配置该文件*/ "presets": [ /*需要编译的语言,es2015即es6 需要解释jsx,则增加'react' 配置项 此处stage-0 包含了对所有es6和最新es(es7)提案中的部分功能的解释 eg. do语句 do { if(role == 'admin') { <AdminComponent/>; }else if(role == 'user') { <UserComponent/>; }else { <GuestComponent/>; } } }*/ "es2015", "react", "stage-0" ] } |
(8). 定义gulp tasks
var gulp = require('gulp'), jshint = require('gulp-jshint'), uglify = require('gulp-uglify'), webpack = require('gulp-webpack'), rename = require('gulp-rename'), del = require('del'), react = require('gulp-react'), babel = require('gulp-babel'), nodemon = require('gulp-nodemon'), minifycss = require('gulp-minify-css'), concat = require('gulp-concat');
var webpackConfig = require('./config/webpack.config'); gulp.task("webpack", function() { return gulp .src('./src') .pipe(webpack(webpackConfig)) .pipe(uglify({ compress: true, preserveComments: 'none' })) .pipe(gulp.dest('./dist')); });
gulp.task('clean', function(cb) { del(['dist/*.js','dist/*.css'], cb) });
gulp.task('startServer_buildAll', ['bundleLibs','minifyCss','webpack'], function () { nodemon({ script: './src/server/index.js', ext: 'js' }) .on('start',function(){ console.log('starting server...'); }) .on('restart', function () { console.log('restarted!'); }); });
gulp.task('startServer_buildOnlyComponent', ['webpack'], function () { nodemon({ script: './src/server/index.js', ext: 'js' }) .on('start',function(){ console.log('starting server...'); }) .on('restart', function () { console.log('restarted!'); }); });
gulp.task('startServer_buildOnlyCss', ['minifyCss'], function () { nodemon({ script: './src/server/index.js', ext: 'js' }) .on('start',function(){ console.log('starting server...'); }) .on('restart', function () { console.log('restarted!'); }); });
gulp.task('minifyCss', function() { return gulp.src(['./src/client/styles/*.css', './src/client/styles/external/*.css']) .pipe(minifycss()) .pipe(concat('styles.css')) .pipe(minifycss()) .pipe(rename({suffix: '.min'})) .pipe(gulp.dest('./dist')); });
gulp.task('bundleLibs', function(){ return gulp.src(['./libs/react-with-addons.min.js', './libs/jquery.min.js']) .pipe(concat('libs.js')) .pipe(rename({suffix: '.min'})) .pipe(gulp.dest('./dist')); }); |
引入webpack.config, 定义webpack task.
打包文件发布到dist文件夹
清除dist下的打包文件的task
在完成外部library的打包,css打包,webpack 任务之后,启动server.
在dev-debug时,如果只有component文件发生改变,可以不清除dist文件夹,在启动server前只启动webpack任务
原理同上,启动服务之前,只打包变动了的css,节省测试等待时间。
打包所有css文件到./dist/styles.min.css
打包js库,在开发环境,可以只执行一次,不需要每次build应用都打包 |
Task定义完成,在gulp task explorer 中通过双击执行选中的任务。
图 2 .Gulp 任务管理器
图 3 .执行打包后的文件夹
(9). 开发客户端index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>My React App</title> <link href="styles/styles.min.css" rel="stylesheet" /> </head> <body> <div id="app"></div> <script src="libs.min.js"></script> <script src="vendor.js"></script> <script src="common.js"></script> <script src="application.js"></script> </body> </html> |
在发送到服务端的页面中链如打包css
链入打包js |
(10). 开发express server程序
Server/index.js:
require('babel-register'); module.exports = require('./server'); | 我们将通过node server 将index.html发送到客户端
Note. nodejs服务端对es6的支持度比客户端(浏览器)好很多,只需要在程序入口模块中引入'babel-register' ,就可以在其子程序中使用ES6 |
Server/server.js
import Express from "express"; import path from 'path';
let app = Express(); let server;
const PATH_STYLES = path.resolve(__dirname, '../../dist'); const PATH_DIST = path.resolve(__dirname, '../../dist');
app.use('/styles', Express.static(PATH_STYLES)); app.use(Express.static(PATH_DIST));
app.get('/', (req, res) => { res.sendFile(path.resolve(__dirname, '../client/index.html')); });
server = app.listen(process.env.PORT || 3001, () => { let port = server.address().port; console.log('Server is listening at %s', port); }); |
申明一个express app
服务端css路径
Js资源路径
相应客户端请求,发送index.html到浏览器
定义端口,服务端访问的url为:http://127.0.0.1:3001/ |
4.4 项目运行
双击startServer_buildAll任务,即在library打包,css打包和压缩,webpack 打包react 组件后启动server。通过:http://127.0.0.1:3001/ 即可访问。
Note. 以下是配置exnternals和external library不分离的打包效果的对比。这个项目开发是在一台只分配了2G内存和2个processor的虚拟机上进行的,机器上运行的webstorm,比较占内存。在高一些的配置的机器上,相信打包回更流畅,更快。
图 4 . 不分离js 库和应用组件的打包
图 5 . webpack.config增加externals配置
4.5 问题和启示
这个学习项目中还需要继续研究或改进的方面有:
1.项目结构需要调整以适应具有更多功能,更多组件的应用的开发,同时需要更进一步的设计组件和组件之间的关系。
2.webpack的配置上,可以尝试深入研究更多的plugin,以提高打包效率
3.研究如何将本地应用发布到远程服务器。
4.研究react中可用的更轻量级的Jquery之外的实现异步的组件,async等
….
Attachment.
| UserInfo组件Contact 组件
异步从另外一台服务器上读取的contact json. http://myapps.eastus.cloudapp.azure.com:8899/users/findAll
Cart 组件
每一行为一个CartItem组件的实例 |
图 6. 运行截图
该项目Github 地址:https://github.com/wangzhongchunongithub/ReactJS_Starter