资讯专栏INFORMATION COLUMN

基于Redux架构的单页应用开发总结

fish / 2942人阅读

摘要:系统架构介绍本项目开发基于框架,利用进行模块化构建,前端编写语言是,利用进行转换。单页是为单页应用量身定做的你可以把拆成很多,这些由路由来加载。前者用来获取的状态,后者用来修改的状态。

系统架构介绍

本项目开发基于 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标签的形式来添加,浏览器将并行的加载这俩请求。


你可以从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(
  (
  
    
  
  ), 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 = (
  
                                    // 主容器
                               // 仪表盘
        // .... 各模块的container
    
                               // 登录
                         // 注册
                         // 找回密码
  
)

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 = [
            "
", "", "
", ].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 (
             

{ this.props.title }

this.close()}>
{ this.props.children }
this.confirm()}>{ this.props.confirmText || "确定" } { this.props.showCancel && (this.close()}>取消) }
); } } export default Modal

然后在containerrender()函数里通过标签的方式引入,并通过点击触发。

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

class App extends Component {
    render() {
       
} } 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 (
) } } }

这样我们在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 (
            
// something ...
); } }
全局模块的处理

其实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 (
            
this.onEnter()}>
会员登录
this.login()} className="login-btn">登录 免费注册 | 忘记密码 ?
{ user.getIn(["login", "error", "message"]) }
) } } // 下面是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取得了。

参考

《webpack 性能优化》

《Redux系列01:从一个简单例子了解action、store、reducer》

@欢迎关注我的 github 和 个人博客 -Jafeney

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86453.html

相关文章

  • 单页应用开发总结

    摘要:本文想通过自己这一年的单页应用开发经验,来对的开发做一个总结。但是要知道,现如今页面都比较复杂,一般的单页应用都需要一个可靠的数据流去处理,否则在日后维护方面会难度巨大。 本文想通过自己这一年的单页应用开发经验,来对SPA的开发做一个总结。 页面开发模式 通常我们在开发页面时,都会拿到一份设计图,假设我们拿到一份这样的设计图 showImg(https://segmentfault.c...

    zzbo 评论0 收藏0
  • 专治前端焦虑的学习方案

    摘要:不过今天我希望能够更进一步,不仅仅再抱怨现状,而是从我个人的角度来给出一个逐步深入学习生态圈的方案。最后,我还是想提到下对于的好的学习方法就是回顾参照各种各样的代码库,学习人家的用法与实践。 本文翻译自A-Study-Plan-To-Cure-JavaScript-Fatigue。笔者看到里面的几张配图着实漂亮,顺手翻译了一波。本文从属于笔者的Web Frontend Introduc...

    codeGoogle 评论0 收藏0
  • 我为什么从Redux迁移到了Mobx

    摘要:需要注意的是,在中,需要把数据声明为。同时还提供了运行时的类型安全检查。在利用了,使异步操作可以在一个函数内完成并且可以被追踪。例如在中,数组并不是一个,而是一个类的对象,这是为了能监听到数据下标的赋值。 Redux是一个数据管理层,被广泛用于管理复杂应用的数据。但是实际使用中,Redux的表现差强人意,可以说是不好用。而同时,社区也出现了一些数据管理的方案,Mobx就是其中之一。 R...

    DevYK 评论0 收藏0
  • 【译】Redux 还是 Mobx,让我来解决你的困惑!

    摘要:我现在写的这些是为了解决和这两个状态管理库之间的困惑。这甚至是危险的,因为这部分人将无法体验和这些库所要解决的问题。这肯定是要第一时间解决的问题。函数式编程是不断上升的范式,但对于大部分开发者来说是新奇的。规模持续增长的应 原文地址:Redux or MobX: An attempt to dissolve the Confusion 原文作者:rwieruch 我在去年大量的使用...

    txgcwm 评论0 收藏0

发表评论

0条评论

fish

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<