Redux教程1:環境搭建,初寫Redux


如果將React比喻成士兵的話,你的程序還需要一位將軍,去管理士兵(的狀態),而Redux恰好是一位好將軍,簡單高效;

相比起React的學習曲線,Redux的稍微平坦一些;本系列教程,將以“紅綠燈”為示例貫穿整個demo,希望能讓用戶快速理解&學習Redux。

強烈推薦 Redux 中文文檔,本redux教程所有的材料和思路都來源於此;

這個系列拆分成3篇文章,最后獲得的效果圖為:

result
(這個是gif圖,如果沒動畫請點擊在新窗口打開)

紅綠燈初始狀態是 #綠燈5s#,繼而循環 #黃燈3s# -> #紅燈7s# -> #綠燈5s# -> #黃燈3s# -> ...

1、Redux簡介

在Redux中,最為核心的概念就是 stateaction 、reducer 以及 store,單詞大家都懂,就是初學者不知道該怎么用。

以常見的紅路燈為例,將其應用到Redux中:

  • action:就是燈的變化,"紅變綠"等,用名詞表述
  • state:就是燈的名字,紅燈、綠燈等,用名詞表述
  • reducer:就是燈的變化規則,紅燈之后是綠燈等,用狀態轉移表述,歸根到底也是名詞
  • store:就像是交警,執行上述的交通規則;

簡單的說,Redux所想表達的就是這些內容,所以它的學習曲線不會很陡。對於程序員來講,閱讀代碼會比閱讀文字舒服,那么我們如何簡單地用redux實現。

2、文件夾結構

2.1、安裝依賴

創建符合redux風格的文件夾結構:

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

最終的文件夾結構為:

file structor

2.2、配置開發環境

這里需要啟用兩個服務器,一個是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安裝

3、開始Redux吧

環境搭建好了,我們就依據最開始的設定用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

3.1、Actions

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

3.2、Reducer

正所謂“不以規矩,不能方圓”,萬物的運作都要符合規律,Reducer 就是描述各狀態之間流轉的 規律

  • 當紅燈時,過n1秒會觸發 CHANGE_GREEN 事件,燈編程綠色的
  • 當綠燈時,過n2秒會觸發 CHANGE_YELLOW 事件,燈編程黃色的
  • 當黃燈時,過n3秒會觸發 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語句就是典型的用於表述 狀態轉移 邏輯的代碼結構,自己嘗試寫狀態機的同學應該深有體會;

3.3、Store

有了交規還不行,得有付諸具體行動的載體 —— 交通信號燈 才行,在 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 的必要規范了,接下來我們檢驗一下是否正如你所願;

此節中我們先簡單的實施一下,后續文章再補充細節

4、檢查能否運行

按照上面創建的一系列JS文件,你已經基於 Redux 完成了紅綠燈的規則效果,那怎么檢驗呢?

來,拿一個紅綠燈過來!

接通電源,給這個燈 發送 事件(類似於dom中的“觸發事件”),假設事件的 type 依次是 CHANGE_GREENCHANGE_GREEN,看看事件結束之后的狀態是否符合期望。

4.1、編寫demo文件

編寫 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());

4.2、編寫view模板

上面的都是 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>

4.3、查看結果

使用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是相比以前是一個重大升級,配置按模塊方式加載了;

5、總結

在繼續后面的章節之前,稍微整理一下上面的邏輯,使用圖表描述會更加清晰些:

light

這簡單的圖里面還涉及到 倒計時的狀態,此篇文章為減少復雜度,方便讀者快速理解Redux的基本概念,並不牽涉倒計時的狀態,后續文章示例自然會將車的狀態考慮進去;

將圖中的Action Reducer以及 Store 和上述代碼對照,一切都是那么合乎邏輯,自然而然;

本文更多的是講解如何快速上手Redux,並沒有對其中的語法和概念進行過多的解釋

  • 一方面是語法的解釋,中文文檔里面的解釋很全面,我沒有自信能夠超越它;
  • 另一方面讓新手對這些簡單的代碼中的陌生概念(諸如combineReducers dispatch等)產生疑惑,帶着問題來探索答案,加深印象

這里將上述操作流程大致繪制一下:

workflow

順帶提及一下Redux的三大原則,看一眼就好,后續用多了自然會記住:

three princel

最后,非常推薦redux庫,里面有很多示例可以參考,比如經典的 todos 例子:

git clone https://github.com/rackt/redux.git

cd redux/examples/todomvc
npm install
npm start

open http://localhost:3000/

該示例包含:

  • Redux 中使用兩個 reducer 的方法
  • 嵌套數據更新
  • 測試代碼

更多參考:Redux示例


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM