基于Redux架构的单页运用开辟总结

体系架构引见

本项目开辟基于 React + Redux + React-Route 框架,运用 webpack 举行模块化构建,前端编写言语是 JavaScript ES6,运用 babel举行转换。

|--- 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最好同伴,非此货莫属!真的很壮大,很好用。

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支撑一个简朴的设置项:

new webpack.optimize.UglifyJsPlugin()

这是一种简朴而有用的方法来优化你的webapp。而webpack还供应了modules 和 chunks ids 来分辨他们俩。运用下面的设置项,webpack就能够比对id的运用频次和散布来得出最短的id分配给运用频次高的模块。

new webpack.optimize.OccurenceOrderPlugin()

进口文件关于文件大小有较高的优先级(进口文件紧缩优化率只管的好)

去重

假如你运用了一些有着很酷的依靠树的库,那末它能够存在一些文件是反复的。webpack能够找到这些文件并去重。这保证了反复的代码不被大包到bundle文件内里去,取而代之的是运行时要求一个封装的函数。不会影响语义

new webpack.optimize.DedupePlugin()

这个功用能够会增添进口模块的一些花消

关于chunks的优化

当coding的时刻,你能够已增加了许多支解点来按需加载。但编译完了以后你发明有太多微小的模块造成了很大的HTTP消耗。荣幸的是Webpack能够处置惩罚这个题目,你能够做下面两件事变来兼并一些要求:

  • Limit the maximum chunk count with

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

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

    原文作者:jafeney
    原文地址: https://segmentfault.com/a/1190000006067018
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞