若是將React比喻成士兵的話,你的程序還須要一位將軍,去管理士兵(的狀態),而Redux剛好是一位好將軍,簡單高效;css
相比起React的學習曲線,Redux的稍微平坦一些;本系列教程,將以「紅綠燈」爲示例貫穿整個demo,但願能讓用戶快速理解&學習Redux。
html
強烈推薦 Redux 中文文檔,本redux教程全部的材料和思路都來源於此;前端
這個系列拆分紅3篇文章,最後得到的效果圖爲:java
紅綠燈初始狀態是 #綠燈5s#,繼而循環 #黃燈3s# -> #紅燈7s# -> #綠燈5s# -> #黃燈3s# -> ...react
在Redux中,最爲核心的概念就是 state
、action
、reducer
以及 store
,單詞你們都懂,就是初學者不知道該怎麼用。webpack
以常見的紅路燈爲例,將其應用到Redux中:git
action
:就是燈的變化,"紅變綠"等,用名詞表述state
:就是燈的名字,紅燈、綠燈等,用名詞表述reducer
:就是燈的變化規則,紅燈以後是綠燈等,用狀態轉移表述,歸根到底也是名詞store
:就像是交警,執行上述的交通規則;簡單的說,Redux所想表達的就是這些內容,因此它的學習曲線不會很陡。對於程序員來說,閱讀代碼會比閱讀文字舒服,那麼咱們如何簡單地用redux實現。程序員
建立符合redux風格的文件夾結構:github
mkdir actions constants components layouts reducers stores tests views touch server.js index.js webpack.config.js
這些文件夾結構也是借鑑自官網redux的todos示例;
而後安裝依賴:
npm init npm install --save koa koa-handlebars koa-router react react-dom react-redux classnames npm install --save-dev webpack webpack-dev-server webpack-hot-middleware babel-core babel-loader babel-plugin-react-transform style-loader less-loader css-loader extract-text-webpack-plugin babel-preset-es2015 babel-preset-react
最終的文件夾結構爲:
這裏須要啓用兩個服務器,一個是webpack服務器,專門用於轉換代碼;另一個是web應用服務器,響應客戶端的請求;
var path = require('path'); var fs = require('fs'); var webpack = require('webpack'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); // 遍歷目錄 var searchDir = ['components','app']; // 須要webpack打包的目錄 var entry = {}; searchDir.forEach(function(dir){ var srcBasePath = path.join(__dirname, './', dir); var files = fs.readdirSync(srcBasePath); var ignore = ['.DS_Store']; // 忽略某些文件夾 files.map(function (file) { if (ignore.indexOf(file) < 0) { entry[dir+'/'+file] = path.join(srcBasePath, file, 'index.js'); var demofile = path.join(srcBasePath, file, 'demo.js'); if(fs.existsSync(demofile)){ entry[dir+'/'+file + '/demo'] = demofile; } var reduxfile = path.join(srcBasePath, file, 'redux.js'); if(fs.existsSync(reduxfile)){ entry[dir+'/'+file + '/redux'] = reduxfile; } } }); }); Object.keys(entry).forEach(function (key) { entry[key] = [entry[key], 'webpack-hot-middleware/client']; }); module.exports = { devtool:'cheap-module-eval-source-map', entry :entry, output:{ path:path.join(__dirname,'dist'), filename:'[name].js', publicPath:'/static/' }, plugins:[ new ExtractTextPlugin("[name]/index.css"), new webpack.optimize.OccurenceOrderPlugin(), new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ], module:{ loaders:[{ test:/\.js$/, loader:'babel-loader', exclude:/node_modules/, include:__dirname, query:{ presets: ['es2015','react'] } },{ test: /\.less$/, loader: ExtractTextPlugin.extract('style-loader','css-loader!less-loader'), exclude: /node_modules/ }] } }
注意Babel 6的配置和上一個版本有很大的不一樣;詳見:Setting up Babel 6
這是webpack配置項,接下來專門起一個nodejs程序提供webpack服務:
server.js
:
var webpack = require('webpack'); var WebpackDevServer = require('webpack-dev-server'); var config = require('./webpack.config'); new WebpackDevServer(webpack(config), { publicPath: config.output.publicPath, hot: true, stats: { colors: true }, historyApiFallback: true }).listen(3009, 'localhost', function (err, result) { if (err) { console.log(err); } console.log('Listening at localhost:3009'); });
此webpack服務專門用於整合前端資源,順帶使用babel轉換ES6的JS代碼;這裏的含義就很少說了,能夠參考之前的文章 Webpack;
使用koa做爲web服務器,由於測試因此比較簡單,用了最基本的代碼快速搭建:
var koa = require('koa'); var router = require('koa-router')(); var handlebars = require("koa-handlebars"); var app = koa(); var port = 3000; // 使用handlerbars做爲模板文件 app.use(handlebars({ defaultLayout: "index" })); // 定義路由 router.get("/", function *(next) { yield this.render('index',{ title:'Redux示例-交通燈', name:'交通燈示例' }); }); // 定義應用路由 router.get('app','/app/:name',function*(next){ this.appName = this.params.name || 'index'; // 應用名字 yield this.render('app/'+this.appName,{ title:'應用', filename:this.appName }); }) // 定義demo路由 router.get('demo','/:name/:type', function *(next) { this.demoName = this.params.name || 'demo'; // 獲取demo名稱 this.demoType = this.params.type; // 獲取文件類型,'demo' 或者 'redux' yield this.render(this.demoName + '/index',{ title:'示例', filename:this.demoType }); }); // 啓用路由 app .use(router.routes()) .use(router.allowedMethods()); // 監聽端口 app.listen(port, function(error) { if (error) { console.error(error) } else { console.info("==> 監聽端口 %s. 請在瀏覽器裏打開 http://localhost:%s/.", port, port) } })
好了,在命令行裏開啓這兩個服務吧:
nodemon server.js & nodemon --harmony index.js
在package.json中的scripts
增長一條配置:"start": "nodemon server.js & nodemon --harmony index.js"
,之後就可使用 npm start
命令同時啓動兩個服務了;
這裏使用了nodemon
應用程序,方便修改後快速啓動,該程序可經過npm install -g nodemon
安裝
環境搭建好了,咱們就依據最開始的設定用redux搭建紅綠燈示例。
先建立所須要的文件:
mkdir actions/light reducers/light stores/light components/light touch constants/TrafficLight.js actions/light/index.js reducers/light/index.js stores/light/index.js components/light/redux.js
Action 本質是 JavaScript 普通對象 action 內必須使用一個 字符串類型 的 type
字段來表示將要執行的動做。多數狀況下,type 會被定義成字符串常量。當應用規模愈來愈大時,建議使用單獨的模塊或文件來存放 action。
在 constants/TrafficLight.js
中定義actions的名稱,使用 const
修飾防止被修改:
export const CHANGE_GREEN = 'CHANGE_GREEN' export const CHANGE_YELLOW = 'CHANGE_YELLOW' export const CHANGE_RED = 'CHANGE_RED'
而後在 actions/light/index.js
文件,定義 Actions 對象:
import * as lights from '../../constants/TrafficLight' export function changeGreen(){ return {type:lights.CHANGE_GREEN} } export function changeYellow(){ return {type:lights.CHANGE_YELLOW} } export function changeRed(){ return {type:lights.CHANGE_RED} }
這裏的 {type:lights.CHANGE_GREEN} 等就是Redux的 action對象(就是這麼簡單....), 而對應的 changeGreen
方法則稱爲 action建立函數 ;
詳細的概念及做用請參考Redux的中文文檔 Actions
正所謂「不以規矩,不能方圓」,萬物的運做都要符合規律,Reducer 就是描述各狀態之間流轉的 規律:
CHANGE_GREEN
事件,燈編程綠色的CHANGE_YELLOW
事件,燈編程黃色的CHANGE_RED
事件,燈編程紅色的繼續在 reducers/light/index.js
文件,描述不一樣等之間的轉移:
import {CHANGE_GREEN, CHANGE_YELLOW, CHANGE_RED} from '../../constants/TrafficLight' // 定義初始化狀態,初始化狀態是常量 // 初始狀態是紅燈 const initState = { color:'red', time:'7' // 持續時間20ms } // 定義燈轉換的reducer函數 export default function light(state=initState,action){ switch(action.type){ case CHANGE_GREEN: return { color:'green', time:'5' } case CHANGE_YELLOW: return { color:'yellow', time:'3' } case CHANGE_RED: return Object.assign({},initState); default: return state } }
這裏的switch
語句就是典型的用於表述 狀態轉移 邏輯的代碼結構,本身嘗試寫狀態機的同窗應該深有體會;
有了交規還不行,得有付諸具體行動的載體 —— 交通訊號燈 才行,在 stores/light/index.js
:
import {createStore} from 'redux' import lightReducer from '../../reducers/light/' export default function lightStore(initState){ return createStore(lightReducer,initState); // 初始化建立 }
關鍵就那句createStore
函數,接受 reducer
(交通規則)和 initState
(初始狀態,燈的初始狀態是紅燈)做爲參數;
這裏的 「交通訊號燈」 也是一種類別,並非具體指 「燈」 —— 額,但願你能理解我想表述的...
自此,恭喜你你已經成功實施了 Redux 的必要規範了,接下來咱們檢驗一下是否正如你所願;
此節中咱們先簡單的實施一下,後續文章再補充細節
按照上面建立的一系列JS文件,你已經基於 Redux
完成了紅綠燈的規則效果,那怎麼檢驗呢?
來,拿一個紅綠燈過來!
接通電源,給這個燈 發送 事件(相似於dom中的「觸發事件」),假設事件的 type
依次是 CHANGE_GREEN 、CHANGE_GREEN,看看事件結束以後的狀態是否符合指望。
編寫 components/light/demo.js
:
import lightStore from '../../stores/light' import {changeGreen, changeYellow, changeRed} from '../../actions/light' let store = lightStore(); let unsubscribe = store.subscribe(() = console.log(store.getState()) ); store.dispatch(changeGreen()); store.dispatch(changeYellow()); store.dispatch(changeRed());
上面的都是 redux
的功能代碼,如今爲了方便在瀏覽器查看,使用 koa
搭建一個簡單的服務器;使用handlerbars
做爲模板引擎,使用下列方式建立模板和視圖
在 layouts/index.hbs
中編寫母模板,其中的 {@body}
是留給子模板填充的
<!DOCTYPE html> <html> <head> <title{{title}}</title> </head> <body> {@body} </body> </html>
在views/light/index.hbs
中編寫子模板內容,程序會自動將裏面的內容自動替換上述模板中的 {@body}
佔位符:
<link rel="stylesheet" href="http://localhost:3009/static/components/light/index.css"> <h1交通燈示例</h1> <div id="demo"</div> <script src="http://localhost:3009/static/components/light/{{filename}}.js"</script>
使用npm start
開啓兩個服務,在瀏覽器URL裏輸入 http://localhost:3000/light/demo
,打開console
,你將看到如下字符串:
Object {color: "green", time: "5"} Object {color: "yellow", time: "3"} Object {color: "red", time: "7"}
你get到了什麼?全程你都沒有涉及到紅綠燈的UI,但彷彿卻有紅綠燈的即視感,狀態徹底可控可預見!redux 其實就是幫你實現了一套狀態機,且邏輯清晰。因爲不涉及UI,因此很是也很利於單元測試。
若是啓動的時候 webpack 報錯:You may need an appropriate loader to handle this file type ,請見use Webpack with Babel to compile ES6 assets, 這裏的解決方案,由於Babel 6是相比之前是一個重大升級,配置按模塊方式加載了;
## 五、總結
在繼續後面的章節以前,稍微整理一下上面的邏輯,使用圖表描述會更加清晰些:
這簡單的圖裏面還涉及到 倒計時的狀態
,此篇文章爲減小複雜度,方便讀者快速理解Redux的基本概念,並不牽涉倒計時的狀態,後續文章示例天然會將車的狀態考慮進去;
將圖中的Action
Reducer
以及 Store
和上述代碼對照,一切都是那麼合乎邏輯,天然而然;
本文更多的是講解如何快速上手Redux,並無對其中的語法和概念進行過多的解釋
combineReducers
dispatch
等)產生疑惑,帶着問題來探索答案,加深印象這裏將上述操做流程大體繪製一下:
順帶說起一下Redux的三大原則,看一眼就好,後續用多了天然會記住:
最後,很是推薦redux
庫,裏面有不少示例能夠參考,好比經典的 todos
例子:
git clone https://github.com/rackt/redux.git cd redux/examples/todomvc npm install npm start open http://localhost:3000/
該示例包含:
更多參考:Redux示例