基於Redux架構的單頁應用開發總結

系統架構介紹

本項目開發基於 React + Redux + React-Route 框架,利用 webpack 進行模塊化構建,前端編寫語言是 JavaScript ES6,利用 babel進行轉換。javascript

|--- project
        |--- build                    // 項目打包編譯目錄
        |--- src                      // 項目開發的源代碼
            |--- actions              // redux的動做
            |--- components           // redux的組件
            |--- containers           // redux的容器  
            |--- images               // 靜態圖片
            |--- mixins               // 通用的函數庫
            |--- reducers             // redux的store操做
            |--- configureStore.js    // redux的store映射
            |--- index.js             // 頁面入口
            |--- routes.js            // 路由配置
        |--- index.html               // 入口文件
        |--- .babelrc                 // babel配置
        |--- main.js                  // webkit打包的殼子
        |--- package.json             // 包信息
        |--- webpack.config.js        // webpack配置文件
        |--- readme.md
"dependencies": {
    "babel-polyfill": "^6.7.4",
    "base-64": "^0.1.0",
    "immutable": "^3.7.6",
    "isomorphic-fetch": "^2.2.1",
    "moment": "^2.13.0",
    "normalizr": "^2.0.1",
    "react": "^0.14.8",
    "react-datetimepicker": "^2.0.0",
    "react-dom": "^0.14.8",
    "react-redux": "^4.4.1",
    "react-redux-spinner": "^0.4.0",
    "react-router": "^2.0.1",
    "react-router-redux": "^4.0.1",
    "redux": "^3.3.1",
    "redux-immutablejs": "0.0.8",
    "redux-logger": "^2.6.1",
    "redux-thunk": "^2.0.1"
  },
  "devDependencies": {
    "babel-core": "^6.7.5",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0",
    "babel-preset-stage-1": "^6.5.0",
    "css-loader": "^0.23.1",
    "file-loader": "^0.8.5",
    "img-loader": "^1.2.2",
    "less": "^2.6.1",
    "less-loader": "^2.2.3",
    "mocha": "^2.4.5",
    "style-loader": "^0.13.1",
    "url-loader": "^0.5.7",
    "webpack": "^1.12.14"
  }

webpack配置

也算是實際體驗了一把webpack,不得不說,論React最佳搭檔,非此貨莫屬!真的很強大,很好用。css

var webpack = require('webpack');   // 引入webpack模塊
var path = require('path');         // 引入node的path模塊
var nodeModulesPath = path.join(__dirname, '/node_modules');  // 設置node_modules目錄

module.exports = {
    // 配置入口(此處定義了雙入口)
    entry: {
        bundle: './src/index',
        vendor: ['react', 'react-dom', 'redux']
    },
    // 配置輸出目錄
    output: {
        path: path.join(__dirname, '/build'),
        publicPath: "/assets/",
        filename: 'bundle.js'
    },
    module: {
        noParse: [
            path.join(nodeModulesPath, '/react/dist/react.min'),
            path.join(nodeModulesPath, '/react-dom/dist/react-dom.min'),
            path.join(nodeModulesPath, '/redux/dist/redux.min'),
        ],
        // 加載器
        loaders: [
            // less加載器
            { test: /\.less$/, loader: 'style!css!less' },
            // babel加載器
            { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' },
            // 圖片加載器(圖片超過8k會自動轉base64格式)
            { test: /\.(gif|jpg|png)$/, loader: "url?limit=8192&name=images/[name].[hash].[ext]"},
            // 加載icon字體文件
            { test: /\.(woff|svg|eot|ttf)$/, loader: 'url?limit=50000&name=fonts/[name].[hash].[ext]'}
        ]
    },
    // 外部依賴(不會打包到bundle.js裏)
    externals: { 
        'citys': 'Citys'
    },
    // 插件
    plugins: [
        //new webpack.HotModuleReplacementPlugin(),  // 版本上線時開啓
        new webpack.DefinePlugin({
            // 定義生產環境
            "process.env": {
                NODE_ENV: JSON.stringify("production")
            }
        }),
        //new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }), // 版本上線時開啓
        // 公共部分會被抽離到vendor.js裏
        new webpack.optimize.CommonsChunkPlugin('vendor',  'vendor.js'),
        // 比對id的使用頻率和分佈來得出最短的id分配給使用頻率高的模塊
        new webpack.optimize.OccurenceOrderPlugin(),
        // 容許錯誤不打斷程序
        new webpack.NoErrorsPlugin()
    ],
};

延伸-Webpack性能優化

最小化

爲了瘦身你的js(還有你的css,若是你用到css-loader的話)webpack支持一個簡單的配置項:html

new webpack.optimize.UglifyJsPlugin()

這是一種簡單而有效的方法來優化你的webapp。而webpack還提供了modules 和 chunks ids 來區分他們倆。利用下面的配置項,webpack就可以比對id的使用頻率和分佈來得出最短的id分配給使用頻率高的模塊。前端

new webpack.optimize.OccurenceOrderPlugin()

入口文件對於文件大小有較高的優先級(入口文件壓縮優化率儘可能的好)java

去重

若是你使用了一些有着很酷的依賴樹的庫,那麼它可能存在一些文件是重複的。webpack能夠找到這些文件並去重。這保證了重複的代碼不被大包到bundle文件裏面去,取而代之的是運行時請求一個封裝的函數。不會影響語義node

new webpack.optimize.DedupePlugin()

這個功能可能會增長入口模塊的一些花銷react

對於chunks的優化

當coding的時候,你可能已經添加了許多分割點來按需加載。但編譯完了以後你發現有太多細小的模塊形成了很大的HTTP損耗。幸運的是Webpack能夠處理這個問題,你能夠作下面兩件事情來合併一些請求:webpack

  • Limit the maximum chunk count withgit

new webpack.optimize.LimitChunkCountPlugin({maxChunks: 15})
  • Limit the minimum chunk size withes6

new webpack.optimize.MinChunkSizePlugin({minChunkSize: 10000})

Webpack經過合併來管理這些異步加載的模塊(合併更多的時候發生在當前這個chunk有複用的地方)。文件只要在入口頁面加載的時候沒有被引入,那麼就不會被合併到chunk裏面去。

單頁

Webpack 是爲單頁應用量身定作的 你能夠把app拆成不少chunk,這些chunk由路由來加載。入口模塊僅僅包含路由和一些庫,沒有別的內容。這麼作在用戶經過導航瀏覽表現很好,可是初始化頁面加載的時候你須要2個網絡請求:一個是請求路由,一個是加載當前內容。

若是你利用HTML5的HistoryAPI 來讓URL影響當前內容頁的話。你的服務器能夠知道那個內容頁面將被客戶端請求。爲了節約請求數,服務端能夠把要請求的內容模塊放到響應頭裏面:以script標籤的形式來添加,瀏覽器將並行的加載這倆請求。

<script src="entry-chunk.js" type="text/javascript" charset="utf-8"></script>
<script src="3.chunk.js" type="text/javascript" charset="utf-8"></script>

你能夠從build stas裏面提取出chunk的filename (stats-webpack-plugin )

多頁

當編譯一個多頁面的app時,你想要在頁面之間共享一些代碼。這在webpack看來很簡單的:只須要和多個入口文件一塊兒編譯就好

webpack p1=./page1 p2=./page2 p3=./page3 [name].entry-chunk.js
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3"
    },
    output: {
        filename: "[name].entry.chunk.js"
    }
}

由上面能夠產出多個入口文件

p1.entry.chunk.js, p2.entry.chunk.js and p3.entry.chunk.js

可是能夠增長一個chunk來共享她們中的一些代碼。 若是你的chunks有一些公用的modules,那我推薦一個很酷的插件CommonsChunkPlugin,它能辨別共用模塊並把他們放倒一個文件裏面去。你須要在你的頁面裏添加兩個script標籤來分別引入入口文件和共用模塊文件。

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3"
    },
    output: {
        filename: "[name].entry.chunk.js"
    },
    plugins: [
        new CommonsChunkPlugin("commons.chunk.js")
    ]
}

由上面能夠產出入口文件

p1.entry.chunk.js, p2.entry.chunk.js and p3.entry.chunk.js

和共用文件

commons.chunk.js

在頁面中要首先加載 commons.chunk.js 在加載xx.entry.chunk.js 你能夠出實話不少個commons chunks ,經過選擇不一樣的入口文件。而且你能夠堆疊使用這些commons chunks。

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3",
        ap1: "./admin/page1",
        ap2: "./admin/page2"
    },
    output: {
        filename: "[name].js"
    },
    plugins: [
        new CommonsChunkPlugin("admin-commons.js", ["ap1", "ap2"]),
        new CommonsChunkPlugin("commons.js", ["p1", "p2", "admin-commons.js"])
    ]
};

輸出結果:

page1.html: commons.js, p1.js
page2.html: commons.js, p2.js
page3.html: p3.js
admin-page1.html: commons.js, admin-commons.js, ap1.js
admin-page2.html: commons.js, admin-commons.js, ap2.js

另外你能夠將多個共用文件打包到一個共用文件中。

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        commons: "./entry-for-the-commons-chunk"
    },
    plugins: [
        new CommonsChunkPlugin("commons", "commons.js")
    ]
};

關於less的組織

做爲一個後端出身的前端工程師,寫簡單的css實在沒有那種代碼可配置和結構化的快感。因此引入less是個不錯的選擇,不管是針對代碼後期的管理,仍是提升代碼的複用能力。

global.less

這個是全局均可以調用的方法庫,我習慣把 項目的配色、各類字號、用於引入混出的方法等寫在這裏,其餘container頁面經過@import方式引入它,就可使用裏面的東西。不過定義它時要注意如下兩點:

  • 第一,這個less裏只能存放變量和方法,less編譯時會忽略它們,只在調用它們的地方纔編譯成css。因此爲了防止代碼重複,請不要在這裏直接定義樣式,而是用一個方法把它們包起來,表示一個用途。

  • 第二,這個less裏的方法若是是針對某些具體標籤訂義樣式的,只能初始化一次,建議在單頁的入口container裏作,這樣好維護。好比reset()(頁面標籤樣式初始化),這個方法放在入口containerlogin.less裏調用且全局只調用一次。

下面是個人global.less 經常使用的一些模塊

/**
 * @desc 一些全局的less
 * @createDate 2016-05-16
 * @author Jafeney <692270687@qq.com>
 **/

// 全局配色
@g-color-active: #ff634d;  //活躍狀態的背景色(橘紅色)
@g-color-info: #53b2ea;    //通常用途的背景色(淺藍色)
@g-color-primary: #459df5; //主要用途的背景色 (深藍色)
@g-color-warning: #f7cec8; //用於提示的背景色 (橘紅色較淺)
@g-color-success: #98cf07; //成功狀態的背景色 (綠色)
@g-color-fail: #c21f16;    //失敗狀態的背景色 (紅色)
@g-color-danger: #ff634d;  //用於警示的背景色 (橘紅色)
@g-color-light: #fde2e1;   //高飽合度淡色的背景色(橘紅)

// 全局尺寸
@g-text-default: 14px;
@g-text-sm: 12px;
@g-text-lg: 18px;

// 全局使用的自定義icon(這樣寫的好處是webpack打包時自動轉base64)
@g-icon-logo: url("../images/logo.png");
@g-icon-logoBlack: url("../images/logoBlack.png");
@g-icon-phone: url("../images/phone.png");
@g-icon-message: url("../images/message.png");
@g-icon-help: url("../images/help.png");
@g-icon-down: url("../images/down.png");
@g-icon-top: url("../images/top.png");
@g-icon-home: url("../images/home.png");
@g-icon-order: url("../images/order.png");
@g-icon-cart: url("../images/cart.png");
@g-icon-source: url("../images/source.png");
@g-icon-business: url("../images/business.png");
@g-icon-finance: url("../images/finance.png");
@g-icon-account: url("../images/account.png");
// ....

// 背景色
@g-color-grey1: #2a2f33;   //黑色
@g-color-grey2: #363b3f;   //深灰色
@g-color-grey3: #e5e5e5;   //灰色
@g-color-grey4: #efefef;   //淺灰色
@g-color-grey5: #f9f9f9;   //很淺
@g-color-grey6: #ffffff;   //白色

// 全局邊框
@g-border-default: #e6eaed;
@g-border-active: #53b2ea;
@g-border-light: #f7dfde;

// 經常使用的border-box盒子模型
.border-box() {
    box-sizing: border-box;
    -ms-box-sizing: border-box;
    -moz-box-sizing: border-box;
    -o-box-sizing: border-box;
    -webkit-box-sizing: border-box;
}

// 模擬按鈕效果
.btn() {
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -o-user-select: none;

    &:hover {
        opacity: .8;
    }

    &.disabled {
        &:hover {
            opacity: 1;
            cursor: not-allowed;
        }
    }
}

// 超出部分處理
.text-overflow() {
    overflow: hidden;
    text-overflow: ellipsis;
    -o-text-overflow: ellipsis;
    -webkit-text-overflow: ellipsis;
    -moz-text-overflow: ellipsis;
    white-space: nowrap;
}

// reset styles
.reset() {
// ....
}

// 一些原子class
.atom() {
    .cp {
        cursor: pointer;
    }
    .ml-5 {
        margin-left: 5px;
    }
    .mr-5 {
        margin-right: 5px;
    }
    .ml-5p {
        margin-left: 5%;
    }
    .mr-5p {
        margin-right: 5%;
    }
    .mt-5 {
        margin-top: 5px;
    }

    .txt-center {
        text-align: center;
    }
    .txt-left {
        text-align: left;
    }
    .txt-right {
        text-align: right;
    }
    .fr {
        float: right;
    }
    .fl {
        float: left;
    }
}

component的less

爲了下降組件的耦合性,每一個組件的less必須單獨寫,樣式跟着組件走,一個組件一個less,不要有其餘依賴,保證組件的高移植能力。
並且組件應該針對用途提供幾套樣式方案,好比button組件,咱們能夠針對顏色提供不一樣的樣式,以樣式組合的方式提供給外部使用。

// 下面的變量能夠針對不一樣的需求進行配置
@color-primary: #459df5; 
@color-warning: #f7cec8; 
@color-success: #98cf07; 
@color-fail: #c21f16;    

.btn {
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -o-user-select: none;
    display: inline-block;
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    -ms-box-sizing: border-box;
    -moz-box-sizing: border-box;
    -o-box-sizing: border-box;
    text-align: center;
    
    // 鼠標放上時
    &:hover {
        opacity: .8;
    }
    
    // 按鈕不可用時
    &.disabled {
        &:hover {
            opacity: 1;
            cursor: not-allowed;
        }
    }
    
    // 填充式按鈕
    &.full {
        color: #fff;
        &.primary {
            background-color:  @color-primary;
            border: 1px solid @color-primary;
        }
        // ....
    }

    // 邊框式按鈕 
    &.border {
       background-color:  #fff;
       &.primary {
            color: @color-primary;
            border: 1px solid @color-primary;
        }
        // ...
    }
}

container的less

同上,每一個container一個less文件,能夠複用的模塊儘可能封裝成component,而不是偷懶複製幾行樣式過來,這樣雖然方便一時,但隨着項目的迭代,後期的冗餘代碼會多得超出你的想象。
若是遵循組件化的設計思想,你會發現container裏其實只有一些佈局和尺寸定義相關的代碼,很是容易維護。

這是大型項目的設計要領,除此以外就是大局觀的培養,這點尤其重要,項目一拿來不要立刻就動手寫頁面,而是應該多花些時間在代碼的設計上,把全局的東西剝離出來,越細越好;把可複用的模塊設計成組件,思考組件的拓展性和不一樣的用途,記住—— 結構上儘可能減小依賴關係,保持組件的獨立性,而用途上多考慮功能的聚合,即所謂的低耦合高聚合。

不過實際項目不可能每一個組件都是獨立存在的,有時咱們爲了進一步減小代碼量,會把一些經常使用的組件整合成一個大組件來使用,即複合組件。因此每一個項目實際上存在一級組件(獨立)和二級組件(複合)。一級組件能夠隨意遷移,而二級組件是針對實際場景而生的,二者並無好壞之分,一切都爲了高效地生產代碼,存在即合理。

關於React的組織

本項目的React代碼都用JavaScript的ES6風格編寫,代碼很是地優雅,並且語言自身支持模塊化,不再用依賴BrowserifyRequireJS等工具了,很是爽。若是你不會ES6,建議去翻一翻阮一峯老師的《ES6標準入門》

入口

入口模塊index.js放在src的根目錄,是外部調用的入口。

import React from 'react'
import { render } from 'react-dom'
// 引入redux
import { Provider } from 'react-redux'
// 引入router
import { Router, hashHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import routes from './routes'
import configureStore from './configureStore'

const store = configureStore(hashHistory)  // 路由的store
const history = syncHistoryWithStore(hashHistory, store) // 路由的歷史紀錄(會寫入到瀏覽器的歷史紀錄)

render(
  (
  <Provider store={store}>
    <Router history={history} routes={routes} />
  </Provider>
  ), document.getElementById('root')
)

路由

這裏主要應用了react-route組件來製做哈希路由,使用方式很簡單,和ReactNative裏的Navigator組件相似。

import React from 'react'
import { Route } from 'react-router'

import Manager from './containers/manager'

import Login from './containers/Login/'
import Register from './containers/Register/'
import Password from './containers/Password/'
import Dashboard from './containers/Dashboard/'

const routes = (
  <Route>
    <Route path="" component={Manager}>                                // 主容器
        <Route path="/" component={Dashboard} />                       // 儀表盤
        // .... 各模塊的container
    </Route>
    <Route path="login" component={Login} />                           // 登陸
    <Route path="register" component={Register} />                     // 註冊
    <Route path="password" component={Password} />                     // 找回密碼
  </Route>
)

export default routes

瞭解action、store、reducer

從調用關係來看以下所示:

store.dispatch(action) --> reducer(state, action) --> final state

來個實際的例子:

// reducer方法, 傳入的參數有兩個
// state: 當前的state
// action: 當前觸發的行爲, {type: 'xx'}
// 返回值: 新的state
var reducer = function(state, action){
    switch (action.type) {
        case 'add_todo':
            return state.concat(action.text);
        default:
            return state;
    }
};

// 建立store, 傳入兩個參數
// 參數1: reducer 用來修改state
// 參數2(可選): [], 默認的state值,若是不傳, 則爲undefined
var store = redux.createStore(reducer, []);

// 經過 store.getState() 能夠獲取當前store的狀態(state)
// 默認的值是 createStore 傳入的第二個參數
console.log('state is: ' + store.getState());  // state is:

// 經過 store.dispatch(action) 來達到修改 state 的目的
// 注意: 在redux裏,惟一可以修改state的方法,就是經過 store.dispatch(action)
store.dispatch({type: 'add_todo', text: '讀書'});
// 打印出修改後的state
console.log('state is: ' + store.getState());  // state is: 讀書

store.dispatch({type: 'add_todo', text: '寫做'});
console.log('state is: ' + store.getState());  // state is: 讀書,寫做

store、reducer、action關聯

store:對flux有了解的同窗應該有所瞭解,store在這裏表明的是數據模型,內部維護了一個state變量,用例描述應用的狀態。store有兩個核心方法,分別是getState、dispatch。前者用來獲取store的狀態(state),後者用來修改store的狀態。

// 建立store, 傳入兩個參數
// 參數1: reducer 用來修改state
// 參數2(可選): [], 默認的state值,若是不傳, 則爲undefined
var store = redux.createStore(reducer, []);

// 經過 store.getState() 能夠獲取當前store的狀態(state)
// 默認的值是 createStore 傳入的第二個參數
console.log('state is: ' + store.getState());  // state is:

// 經過 store.dispatch(action) 來達到修改 state 的目的
// 注意: 在redux裏,惟一可以修改state的方法,就是經過 store.dispatch(action)
store.dispatch({type: 'add_todo', text: '讀書'});

action:對行爲(如用戶行爲)的抽象,在redux裏是一個普通的js對象。redux對action的約定比較弱,除了一點,action必須有一個type字段來標識這個行爲的類型。因此,下面的都是合法的action

{type:'add_todo', text:'讀書'}
{type:'add_todo', text:'寫做'}
{type:'add_todo', text:'睡覺', time:'晚上'}

reducer:一個普通的函數,用來修改store的狀態。傳入兩個參數 state、action。其中,state爲當前的狀態(可經過store.getState()得到),而action爲當前觸發的行爲(經過store.dispatch(action)調用觸發)。reducer(state, action) 返回的值,就是store最新的state值。

// reducer方法, 傳入的參數有兩個
// state: 當前的state
// action: 當前觸發的行爲, {type: 'xx'}
// 返回值: 新的state
var reducer = function(state, action){
    switch (action.type) {
        case 'add_todo':
            return state.concat(action.text);
        default:
            return state;
    }
}

React式編程思惟

在沒有遁入React以前,我是一個DOM操做控,不管是jQuery仍是zepto,我在頁面交互的實現上用的最多的就是DOM操做,把複雜的交互一步一步經過選擇器和事件委託綁定到document上,而後逐個連貫起來。

$(document).on('event', 'element', function(e){
    e.preventDefault();
    var that = this;
    var parent = $(this).parent();
    var siblings = $(this).siblings();
    var children = $(this).children();
    // .....
});

這是jQuery式的編程思惟,React和它大相徑庭。React的設計是基於組件化的,每一個組件經過生命週期維護統一的statestate改變,組件便update,從新觸發render,即從新渲染頁面。而這個過程操做的實際上是內存裏的虛擬DOM,而不是真正的DOM節點,加上其內部的差別更新算法,因此性能上比傳統的DOM操做要好。

舉個簡單的例子:

如今要實現一個模態組件,若是用jQuery式的編程思惟,很習慣這麼寫:

/**
 * @desc 全局模態窗口
 **/
var $ = window.$;
var modal = {
    confirm: function(opts) {
        var title = opts.title || '提示',
            content = opts.content || '提示內容',
            callback = opts.callback;
        var newNode = [
            '<div class="mask" id="J_mask">',
                '<div class="modal-box">',
                    '<h2>',
                        title,
                    '</h2>',
                    '<p>',
                        content,
                    '</p>',
                    '<div class="mask-btns">',
                        '<span id="J_cancel">取消</span>',
                        '<span id="J_confirm">肯定</span>',
                    '</div>',
                '</div>',
            '</div>',
        ].join('');
        $('#J_mask').remove();
        $('body').append(newNode);

        $('#J_cancel').on('click', function() {
            $('#J_mask').remove();
        });

        $('#J_confirm').on('click', function() {
            if (typeof callback === 'function') {
                callback();
            }
            $('#J_mask').remove();
        });
    }
};
module.exports = modal;

而後在頁面的JavaScript裏經過選擇器觸發模態和傳遞參數。

var Modal = require('modal');
var $ = window.$;
var app = (function() {
    var init = function() {
        eventBind();
    };
    var eventBind = function() {
        $(document).on('click', '#btnShowModal', function() {
            Modal.confirm({
                title: '提示',
                content: '你好!世界',
                callback: function() {
                    console.log('Hello World');
                }
            });
        });
    };
    init();
})();

若是採用React式的編程思惟,它應該是這樣的:

/**
 * @desc 全局模態組件 Component
 * @author Jafeney
 * @createDate 2016-05-17
 * */
import React, { Component } from 'react'
import './index.less'

class Modal extends Component {
    constructor() {
        super()
        this.state = {
            jsMask: 'mask hidden'
        }
    }
    show() {
        this.setState({
            jsMask: 'mask'
        })
    }
    close() {
        this.setState({
            jsMask: 'mask hidden'
        })
    }
    confirm() {
        this.props.onConfirm && this.props.onConfirm()
    }
     render() {
         return (
             <div className={this.state.jsMask}>
                <div className="modal-box" style={this.props.style}>
                    <div className="header">
                        <h3>{ this.props.title }</h3>
                        <span className="icon-remove closed-mask" onClick={()=>this.close()}></span>
                    </div>
                    <div className="content">
                        { this.props.children }
                    </div>
                    <div className="mask-btns">
                        <span className="btn-full-danger" onClick={()=>this.confirm()}>{ this.props.confirmText || '肯定' }</span>
                        { this.props.showCancel && (<span className="btn-border-danger" onClick={()=>this.close()}>取消</span>) }
                    </div>
                </div>
             </div>
         );
     }
}
export default Modal

而後在containerrender()函數裏經過標籤的方式引入,並經過點擊觸發。

import {React, component} from 'react'; 
import Modal from 'Modal';

class App extends Component {
    render() {
       <div>
            <button onClick = {()=> {this.refs.modal.show()}}
            <Modal title={"提示"} 
                   style={{width: 420, height: 200}}
                   ref={(ref)=> this.modal = ref} 
                   onConfirm={()=>this.onModalConfirm()}>
                   <p className="tips">Hello world!</p>
            </Modal>
       </div>
    }
}

export default App

你會發現,上面的代碼並無刻意地操做某個DOM元素的樣式,而是經過改變組件的state去觸發自身的渲染函數。換句話說,咱們不須要寫繁瑣的DOM操做,而是靠改變組件的state控制組件的交互和各類變化。這種思惟方式的好處等你熟悉React以後天然會明白,能夠大大地減小後期的代碼量。

優化渲染

前面提到組件的state改變即觸發render()React內部雖然作了一些算法上的優化,可是咱們能夠結合Immutable作進一步的渲染優化,讓頁面更新渲染速度變得更快。

/**
 * @desc PureRender 優化渲染
 **/

import React, { Component } from 'react'
import Immutable from 'immutable';

export default {
    // 深度比較
    deepCompare: (self, nextProps, nextState) => {
        return !Immutable.is(self.props, nextProps) || !Immutable.is(self.state, nextState)
     },
    // 阻止不必的渲染
    loadDetection: (reducers=[])=> {
        for (let r of reducers) {
            if (!r.get('preload')) return (<div />)
        }
    }
}

這樣咱們在containerrender()函數裏就能夠調用它進行渲染優化

import React, { Component } from 'react'
import PureRenderMixin from '../../mixins/PureRender';

class App extends Component { 
    render() {
        let { actions, account, accountLogs, bankBind } = this.props;
        // 數據導入檢測
        let error = PureRenderMixin.loadDetection([account, accountLogs, bankBind])
        // 若是和上次沒有差別就阻止組件從新渲染
        if (error) return error   
        return (
            <div>
                // something ...
            </div>
        );
    }
}

全局模塊的處理

其實Redux最大的做用就是有效減小代碼量,把繁瑣的操做經過 action ----> reducer ----> store 進行抽象,最後維護統一的state。對於頁面的全局模塊,簡單地封裝成mixin來調用仍是不夠的,好比全局的request模塊,下面介紹如何用Redux進行改造。

首先在types.js裏進行聲明:

// request
export const REQUEST_PEDDING = 'REQUEST_PEDDING';
export const REQUEST_DONE = 'REQUEST_DONE';
export const REQUEST_ERROR = 'REQUEST_ERROR';
export const REQUEST_CLEAN = 'REQUEST_CLEAN';
export const REQUEST_SUCCESS = 'REQUEST_SUCCESS';

而後編寫action:

/**
 * @desc 網絡請求模塊的actions
 **/

// fetch 須要使用 Promise 的 polyfill
import {
  pendingTask, // The action key for modifying loading state
  begin, // The action value if a "long" running task begun
  end // The action value if a "long" running task ended
} from 'react-redux-spinner';
import 'babel-polyfill'
import fetch from 'isomorphic-fetch'
import Immutable from 'immutable'
import * as CONFIG from './config';   //請求的配置文件
import * as TYPES from './types';

export function request(route, params, dispatch, success=null, error=null, { method='GET', headers={}, body=null } = {}) {
  dispatch({type: TYPES.REQUEST_PEDDING, [ pendingTask ]: begin})
  // 處理query
  const p = params ? '?' + Object.entries(params).map( (i)=> `${i[0]}=${encodeURI(i[1])}` ).join('&') : ''
  const uri = `${ CONFIG.API_URI }${ route }${ p }`
  let data = {method: method, headers: headers}
  if (method!='GET') data.body = body
  fetch(uri, data)
    .then((response) => {
      dispatch({type: TYPES.REQUEST_DONE, [ pendingTask ]: end})
      return response.json()
    })
    .then((data) => {
      if (String(data.code) == '0') {
        if (method !== 'GET' ) dispatch({type: TYPES.REQUEST_SUCCESS});
        success && success(data);
      } else {
        console.log(data.error)
        dispatch({type: TYPES.REQUEST_ERROR, ...data})
        error && error(data)
      }
    })
    .catch((error) => {
        console.warn(error)
    })
}

export function requestClean() {
  return { type: TYPES.REQUEST_CLEAN }
}

而後編寫對應的reducer操做state

import Immutable from 'immutable';
import * as TYPES from '../actions/types';
import { createReducer } from 'redux-immutablejs'

export default createReducer(Immutable.fromJS({status: null, error: null}), {
  [TYPES.REQUEST_ERROR]: (state, action) => {
    return state.merge({
        status: 'error',
        code: action.code,
        error: Immutable.fromJS(action.error),
    })
  },
  [TYPES.REQUEST_CLEAN]: (state, action) => {
    return state.merge({
        status: null,
        error: null,
    })
  },
  [TYPES.REQUEST_SUCCESS]: (state, action) => {
    return state.merge({
        status: 'success',
        error: null,
    })
  }
})

而後在reducersindex.js裏對外暴露接口

export request from './request'

爲何要作這一步呢?由於咱們須要在configureStore.js裏利用combineReducers對全部的reducer進行進一步的結合處理:

import { createStore, combineReducers, compose, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import * as reducers from './reducers'
import { routerReducer, routerMiddleware } from 'react-router-redux'
import { pendingTasksReducer } from 'react-redux-spinner'

export default function configureStore(history, initialState) {
  const reducer = combineReducers({
    ...reducers,
    routing: routerReducer,
    pendingTasks: pendingTasksReducer,
  })
  const store = createStore(
    reducer,
    initialState,
    compose(
      applyMiddleware(
        thunkMiddleware,
        routerMiddleware(history) 
      )
    )
  )
  return store
}

接下來就能夠在container裏使用了,好比登陸模塊:

/**
 * @desc 登陸模塊 container
 * @createDate 2016-05-16
 * @author Jafeney<692270687@qq.com>
 **/
import React, { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { replace } from 'react-router-redux'
import { login } from '../../actions/user'
import { requestClean } from '../../actions/request'
import CheckUserMixin from '../../mixins/CheckUser'
import PureRenderMixin from '../../mixins/PureRender'
import '../style.less';

class Login extends Component {
    constructor() {
        super()
    }
    shouldComponentUpdate(nextProps, nextState) {
        // 若是已經登陸不觸發深度比較
        if (nextProps.user.getIn(['login', 'status'])=='logged') {
            this.toMain()
            return true
        }
        return PureRenderMixin.deepCompare(this, nextProps, nextState)
    }
    // 檢查登陸態
    componentDidMount() {
        let { user } = this.props;
        if (CheckUserMixin.isLogged(user)) this.toMain()
    }
    // 初始化頁面
    toMain() {
        this.props.actions.replace('/')
        this.props.actions.requestClean()
    }
    // 執行登陸
    login() {
        const userName = this.refs['J_username'].value, password = this.refs['J_password'].value
        if (userName && password) {
            this.props.actions.login({username: userName, password: password})
        }
    }
    // 綁定回車事件
    onEnter(event) {
        var e = event || window.event || arguments.callee.caller.arguments[0];
        if(e && e.keyCode==13) { // enter 鍵
             this.login()
        }
    }
    render() {
        let { user } = this.props
        return (
            <div className="wrapper" onKeyPress={()=>this.onEnter()}>
                <div className="containers">
                    <div className="logo"></div>
                    <div className="content">
                        <div className="header">會員登陸</div>
                        <div className="mainer">
                            <div className="input-group">
                                <input ref="J_username" type="text" placeholder="手機號碼" className="input" />
                                <label className="check-info" ref="J_username-check"></label>
                            </div>
                            <div className="input-group">
                                <input ref="J_password" type="password" placeholder="登陸密碼" className="input" />
                                <label className="check-info" ref="J_password-check"></label>
                            </div>
                            <div className="input-group">
                                <span ref="J_login" onClick={()=>this.login()} className="login-btn">登陸</span>
                                <span className="login-info">
                                    <a ref="J_register" href="#/register" className="register">免費註冊</a> |
                                    <a ref="J_forget" href="#/password" className="forget">忘記密碼 ?</a>
                                </span>
                            </div>
                            <div className="form-error">
                                { user.getIn(['login', 'error', 'message']) }
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        )
    }
}

// 下面是redux的核心方法
function mapStateToProps(state) {
    return {
        user: state.user
    }
}
function mapDispatchToProps(dispatch) {
    return { actions: bindActionCreators({ login, requestClean, replace }, dispatch) }
}
export default connect(mapStateToProps, mapDispatchToProps)(Login)

注意:經過以上方式,在組件內部actions裏掛載的方法就能夠經過this.props取得了。

參考


@歡迎關注個人 github我的博客 -Jafeney

相關文章
相關標籤/搜索