使用Webpack和Gulp構建ReactJS應用 javascript
前言 css
本文一共四個部分,包括,研究背景,ReactJS入門,相關工具和技術,具體實施和註解。主要是使用ReactJS和ES6進行開發,結合webpack,gulp,babel等工具,嘗試構建一個簡單的ReactJS應用,同時,在數據庫服務端交互上用了jquery的ajax,在樣式上,引入了React-bootstrap. 本文所述是對一個結合組件化,自動構建和打包,可與數據庫服務端交互的ReactJS項目的初級研究。 html
一.背景 前端
咱們都知道不少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等概念。 html5
二.ReactJS入門 java
1. ReactJS特色 node
1.1 Declarative
React和Html對頁面上的元素的定義方式同樣,是申明式的,Html是經過原生的標籤去申明element和它們的屬性,而React是經過xml(jsx)來申明UI組件的使用的。對於UI的渲染,用React.createElement和React.render方法實現頁面元素渲染。 react
eg.
(1)定義組件類
class User extends React.Component{//定義組件類User jquery
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} />
);}
} webpack
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