基于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()
(页面标签样式初始化),这个方法放在入口container
的login.less
里调用且全局只调用一次。
下面是我的global.less
常用的一些模块
/** * @desc 一些全局的less * @createDate 2016-05-16 * @author Jafeney <[email protected]> **/ // 全局配色 @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风格编写,代码非常地优雅,而且语言自身支持模块化,再也不用依赖Browserify
、RequireJS
等工具了,非常爽。如果你不会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
的设计是基于组件化的,每个组件通过生命周期维护统一的state
,state
改变,组件便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
然后在container
的render()
函数里通过标签的方式引入,并通过点击触发。
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 />) } } }
这样我们在container
的render()
函数里就可以调用它进行渲染优化
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, }) } })
然后在reducers
的index.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<[email protected]> **/ 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
取得了。
参考
《Redux系列01:从一个简单例子了解action、store、reducer》
@欢迎关注我的 github
和 个人博客 -Jafeney