├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── mock-server └── api │ └── fetchSampleData.json ├── package.json ├── ppt ├── img │ ├── category.jpeg │ ├── doRedux.jpg │ ├── initRedux.jpg │ ├── mvc.jpeg │ ├── mvc2.jpeg │ ├── mvc3.png │ ├── react-redux.jpg │ ├── reactredux1.jpg │ ├── reactredux2.jpg │ ├── redux-logger.jpeg │ ├── reduxAction2.jpg │ ├── reduxReducer1.jpg │ ├── reduxReducer3.jpg │ └── reduxmiddleware2.jpg ├── redux-1.md └── redux-2.md ├── scripts ├── dev.js ├── dev │ └── index.js └── webpack.config.js ├── src ├── originRedux │ ├── entries │ │ └── redux.js │ ├── originRedux.js │ └── originRedux.pcss ├── originReduxAction │ ├── actions │ │ └── number.js │ ├── configs │ │ └── action.js │ ├── entries │ │ └── reduxaction.js │ ├── originRedux.pcss │ └── originReduxAction.js ├── originReduxCombineReducer │ ├── actions │ │ ├── alert.js │ │ ├── index.js │ │ └── number.js │ ├── configs │ │ └── action.js │ ├── entries │ │ └── reduxcombinereducer.js │ ├── lib │ │ └── common.js │ ├── originRedux.pcss │ ├── originReduxCombineReducer.js │ └── reducers │ │ ├── alert.js │ │ ├── index.js │ │ └── number.js ├── originReduxReducer │ ├── actions │ │ └── number.js │ ├── configs │ │ └── action.js │ ├── entries │ │ └── reduxreducer.js │ ├── originRedux.pcss │ ├── originReduxReducer.js │ └── reducers │ │ └── number.js ├── originReduxStore │ ├── actions │ │ ├── alert.js │ │ ├── index.js │ │ └── number.js │ ├── configs │ │ └── action.js │ ├── entries │ │ └── reduxstore.js │ ├── lib │ │ └── common.js │ ├── originRedux.pcss │ ├── originReduxStore.js │ └── reducers │ │ ├── alert.js │ │ ├── index.js │ │ └── number.js ├── reactRedux │ ├── actions │ │ ├── alert.js │ │ ├── index.js │ │ └── number.js │ ├── components │ │ ├── alert │ │ │ ├── alert.js │ │ │ └── alert.pcss │ │ └── number │ │ │ ├── number.js │ │ │ └── number.pcss │ ├── configs │ │ └── action.js │ ├── containers │ │ └── sample │ │ │ ├── sample.js │ │ │ └── sample.pcss │ ├── entries │ │ └── reactRedux.js │ ├── lib │ │ ├── Provider.js │ │ ├── common.js │ │ └── connect.js │ └── reducers │ │ ├── alert.js │ │ ├── index.js │ │ └── number.js ├── reactReduxAsync │ ├── actions │ │ ├── alert.js │ │ ├── fetchData.js │ │ ├── index.js │ │ └── number.js │ ├── components │ │ ├── alert │ │ │ ├── alert.js │ │ │ └── alert.pcss │ │ ├── fetchData │ │ │ ├── fetchData.js │ │ │ └── fetchData.pcss │ │ ├── loading │ │ │ ├── images │ │ │ │ └── page-loading.gif │ │ │ ├── loading.js │ │ │ └── loading.pcss │ │ └── number │ │ │ ├── number.js │ │ │ └── number.pcss │ ├── configs │ │ └── action.js │ ├── containers │ │ └── sample │ │ │ ├── sample.js │ │ │ └── sample.pcss │ ├── entries │ │ └── reactReduxAsync.js │ ├── lib │ │ └── common.js │ └── reducers │ │ ├── alert.js │ │ ├── fetchData.js │ │ ├── index.js │ │ └── number.js ├── reactReduxMiddleware │ ├── actions │ │ ├── alert.js │ │ ├── index.js │ │ └── number.js │ ├── components │ │ ├── alert │ │ │ ├── alert.js │ │ │ └── alert.pcss │ │ └── number │ │ │ ├── number.js │ │ │ └── number.pcss │ ├── configs │ │ └── action.js │ ├── containers │ │ └── sample │ │ │ ├── sample.js │ │ │ └── sample.pcss │ ├── entries │ │ └── reactReduxMiddleware.js │ ├── lib │ │ ├── common.js │ │ └── middleware.js │ └── reducers │ │ ├── alert.js │ │ ├── index.js │ │ └── number.js └── reactReduxSaga │ ├── actions │ ├── alert.js │ ├── fetchData.js │ ├── index.js │ └── number.js │ ├── api │ └── index.js │ ├── components │ ├── alert │ │ ├── alert.js │ │ └── alert.pcss │ ├── fetchData │ │ ├── fetchData.js │ │ └── fetchData.pcss │ ├── loading │ │ ├── images │ │ │ └── page-loading.gif │ │ ├── loading.js │ │ └── loading.pcss │ └── number │ │ ├── number.js │ │ └── number.pcss │ ├── configs │ └── action.js │ ├── containers │ └── sample │ │ ├── sample.js │ │ └── sample.pcss │ ├── entries │ └── reactReduxSaga.js │ ├── io.js │ ├── lib │ └── common.js │ ├── proc.js │ ├── reducers │ ├── alert.js │ ├── fetchData.js │ ├── index.js │ └── number.js │ ├── saga-middleware-backup.js │ ├── saga-middleware.js │ ├── sagas-backup.js │ ├── sagas.js │ └── utils.js └── template └── index.html /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-0" 6 | ], 7 | "plugins": ["transform-runtime"] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | curly_bracket_next_line = false 11 | spaces_around_operators = true 12 | indent_brace_style = 1tbs 13 | 14 | [*.js] 15 | quote_type = single 16 | 17 | [*.{html,less,css,json}] 18 | quote_type = double 19 | 20 | [package.json] 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-davinci", 3 | "rules": { 4 | "import/no-unresolved": "off", 5 | "no-prototype-builtins": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules 4 | npm-debug.log* 5 | yarn.lock 6 | yarn-error.log* 7 | /build 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.1 2 | 3 | - 将结合 `react` 的章节拆成:用法和原理两部分。用法部分在第一次课上介绍,原理在第二次课上介绍 4 | - `ppt` 里中间件增加 `redux-logger` 的示例页 5 | 6 | # 1.0.0 7 | 8 | - 初版发布 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zhangxinlin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-demo 2 | 3 | This project is for learning Redux 4 | 5 | ### insall 6 | 7 | yarn insall 8 | 9 | ### watch ppt 10 | 11 | yarn start 12 | 13 | ### run code 14 | 15 | yarn run dev 16 | 17 | ### Chapter 1 originRedux 18 | 19 | all redux in single JS file 20 | 21 | http://0.0.0.0:9999/redux.html 22 | 23 | ### Chapter 2 originReduxAction 24 | 25 | separate action from others 26 | 27 | http://0.0.0.0:9999/reduxaction.html 28 | 29 | ### Chapter 3 originReduxReducer & originReduxCombineReducer 30 | 31 | separate reducer from others 32 | 33 | http://0.0.0.0:9999/reduxreducer.html 34 | 35 | learn combineReducers 36 | 37 | http://0.0.0.0:9999/reduxcombinereducer.html 38 | 39 | ### Chapter 4 originReduxStore 40 | 41 | learn createStore compose 42 | 43 | http://0.0.0.0:9999/reduxstore.html 44 | 45 | ### Chapter 5 reactRedux 46 | 47 | learn react-redux 48 | 49 | http://0.0.0.0:9999/reactredux.html 50 | 51 | ### Chapter 6 reactReduxMiddleware 52 | 53 | learn middleware 54 | 55 | http://0.0.0.0:9999/reactreduxmiddleware.html 56 | 57 | ### Chapter 7 reactReduxAsync 58 | 59 | learn asynchronous operation by redux-thunk 60 | 61 | http://0.0.0.0:9999/reactreduxasync.html 62 | 63 | ### Chapter 8 reactReduxSaga 64 | 65 | learn asynchronous operation by redux-saga 66 | 67 | http://0.0.0.0:9999/reactreduxsaga.html 68 | -------------------------------------------------------------------------------- /mock-server/api/fetchSampleData.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "msg": { 4 | "msg": "Fetched Redux Async sample data successfully!" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux", 3 | "version": "1.0.0", 4 | "description": "react redex demo", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "lint": "eslint ./src", 8 | "start": "nodeppt start -p 9998 -d ppt -w", 9 | "dev": "cross-env NODE_ENV=development node ./scripts/dev.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/JackZhangXL/react-redux-demo.git" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "redux" 18 | ], 19 | "author": "JackZhang", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/JackZhangXL/react-redux-demo/issues" 23 | }, 24 | "homepage": "https://github.com/JackZhangXL/react-redux-demo#readme", 25 | "devDependencies": { 26 | "antd": "^2.10.0", 27 | "babel-core": "^6.24.1", 28 | "babel-loader": "^7.0.0", 29 | "babel-plugin-transform-runtime": "^6.23.0", 30 | "babel-polyfill": "^6.23.0", 31 | "babel-preset-es2015": "^6.24.1", 32 | "babel-preset-react": "^6.24.1", 33 | "babel-preset-stage-0": "^6.24.1", 34 | "babel-register": "^6.24.1", 35 | "cross-env": "^4.0.0", 36 | "css-loader": "^0.28.1", 37 | "eslint": "^3.19.0", 38 | "eslint-config-davinci": "^1.0.2", 39 | "eslint-loader": "^1.7.1", 40 | "extract-text-webpack-plugin": "^2.1.0", 41 | "file-loader": "^0.11.1", 42 | "fs-promise": "^2.0.2", 43 | "glob": "^7.1.1", 44 | "html-webpack-plugin": "^2.28.0", 45 | "nodeppt": "^1.4.2", 46 | "open": "^0.0.5", 47 | "postcss-cssnext": "^2.10.0", 48 | "postcss-import": "^9.1.0", 49 | "postcss-loader": "^1.3.3", 50 | "precss": "^1.4.0", 51 | "prop-types": "^15.5.10", 52 | "react": "^15.5.4", 53 | "react-dom": "^15.5.4", 54 | "react-hot-loader": "3.0.0-beta.6", 55 | "react-redux": "^5.0.4", 56 | "redux": "^3.6.0", 57 | "redux-logger": "^3.0.1", 58 | "redux-saga": "^1.0.2", 59 | "redux-thunk": "^2.2.0", 60 | "style-loader": "^0.17.0", 61 | "url-loader": "^0.5.8", 62 | "webpack": "^2.4.1", 63 | "webpack-dev-server": "^2.4.5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ppt/img/category.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/category.jpeg -------------------------------------------------------------------------------- /ppt/img/doRedux.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/doRedux.jpg -------------------------------------------------------------------------------- /ppt/img/initRedux.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/initRedux.jpg -------------------------------------------------------------------------------- /ppt/img/mvc.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/mvc.jpeg -------------------------------------------------------------------------------- /ppt/img/mvc2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/mvc2.jpeg -------------------------------------------------------------------------------- /ppt/img/mvc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/mvc3.png -------------------------------------------------------------------------------- /ppt/img/react-redux.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/react-redux.jpg -------------------------------------------------------------------------------- /ppt/img/reactredux1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/reactredux1.jpg -------------------------------------------------------------------------------- /ppt/img/reactredux2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/reactredux2.jpg -------------------------------------------------------------------------------- /ppt/img/redux-logger.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/redux-logger.jpeg -------------------------------------------------------------------------------- /ppt/img/reduxAction2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/reduxAction2.jpg -------------------------------------------------------------------------------- /ppt/img/reduxReducer1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/reduxReducer1.jpg -------------------------------------------------------------------------------- /ppt/img/reduxReducer3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/reduxReducer3.jpg -------------------------------------------------------------------------------- /ppt/img/reduxmiddleware2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/ppt/img/reduxmiddleware2.jpg -------------------------------------------------------------------------------- /ppt/redux-1.md: -------------------------------------------------------------------------------- 1 | title: Redux (1) 2 | speaker: JackZhang 3 | theme: dark 4 | transition: move 5 | 6 | [slide] 7 | 8 | # Redux介绍 9 | JackZhang 10 | 11 | [slide] 12 | 13 | # Part 1 14 | 15 | - 概述 16 | 17 | - Action 18 | 19 | - Reducer 20 | 21 | - Store 22 | 23 | - 结合React(用法) 24 | 25 | # Part 2 26 | 27 | - 结合React(原理) 28 | 29 | - 中间件 30 | 31 | - 异步Action 32 | 33 | [slide] 34 | 35 | # Part 1 36 | 37 | - 概述 38 | 39 | - Action 40 | 41 | - Reducer 42 | 43 | - Store 44 | 45 | - 结合React(用法) 46 | 47 | [slide] 48 | 49 | # Why Flux? 50 | 51 | 传统 MVC 框架 52 | 53 | ![initRedux](../img/mvc.jpeg) 54 | 55 | [slide] 56 | 57 | # Why Flux? 58 | 59 | 最终几乎肯定失控 60 | 61 | ![initRedux](../img/mvc2.jpeg) 62 | 63 | [slide] 64 | 65 | # Why Flux? 66 | 67 | 单向数据流(禁止 view 直接对话 model) 68 | 69 | ![initRedux](../img/mvc3.png) 70 | 71 | [slide] 72 | 73 | # Why Redux? 74 | 75 | Redux 是 Flux 的一种实现 76 | 77 | 替你管理难以维护的 state 78 | 79 | 让 state 的变化可控 80 | 81 | [slide] 82 | 83 | # 三大原则 84 | 85 | - 单一数据源 Store 86 | - 只能通过 Dispatch Action 来修改 state 87 | - 使用 Reducer 纯函数来执行修改 state 88 | 89 | [slide] 90 | 91 | # 初始态流转图 92 | 93 | ![initRedux](../img/initRedux.jpg) 94 | 95 | [slide] 96 | 97 | # 更新态流转图 98 | 99 | ![doRedux](../img/doRedux.jpg) 100 | 101 | [slide] 102 | 103 | ![initRedux](../img/initRedux.jpg) 104 | 105 | [例子 originRedux](http://0.0.0.0:9999/redux.html) 106 | 107 | [slide] 108 | 109 | # Part 1 110 | 111 | - 概述 112 | 113 | - Action 114 | 115 | - Reducer 116 | 117 | - Store 118 | 119 | - 结合React(用法) 120 | 121 | [slide] 122 | 123 | # 什么是Action? 124 | 125 | 描述已发生事件,能携带数据的plain object 126 | 127 | [slide] 128 | 129 | # Action的作用 130 | 131 | - 告诉Reducer发生了什么事 132 | - 携带数据 133 | 134 | [slide] 135 | 136 | # 典型的Action 137 | 138 | - 必须有type属性,用于告知Reducer发生了什么事 139 | 140 | ``` JavaScript 141 | // 例1 142 | { 143 | type: 'ADD_TODO', 144 | payload: { 145 | text: 'Do something.' 146 | } 147 | } 148 | 149 | // 例2 150 | { 151 | type: 'ADD_TODO', 152 | payload: new Error(), 153 | error: true 154 | } 155 | ``` 156 | 157 | [参考 Flux 标准](https://github.com/acdlite/flux-standard-action) 158 | 159 | [slide] 160 | 161 | # Action Creator 162 | 163 | 创建Action的函数 164 | 165 | ``` JavaScript 166 | // 太麻烦,不好维护 167 | store.dispatch({ 168 | type: constant.INCREMENT, 169 | }); 170 | 171 | /*************/ 172 | 173 | // Action Creator 174 | const incrementNum = () => ({ 175 | type: constant.INCREMENT, 176 | }); 177 | 178 | store.dispatch(incrementNum()); 179 | ``` 180 | [slide] 181 | 182 | 目录结构 183 | 184 | ![reduxAction](../img/reduxAction2.jpg) 185 | 186 | [例子 Action](http://0.0.0.0:9999/reduxaction.html) 187 | 188 | [slide] 189 | 190 | # Part 1 191 | 192 | - 概述 193 | 194 | - Action 195 | 196 | - Reducer 197 | 198 | - Store 199 | 200 | - 结合React(用法) 201 | 202 | [slide] 203 | 204 | # 什么是Reducer? 205 | 206 | Reducer是个纯函数,执行计算,返回新的state 207 | 208 | [slide] 209 | 210 | # Reducer的作用 211 | 212 | - 返回计算后的新的state 213 | 214 | - 参数:旧的state和Action 215 | 216 | - 返回值:新的state 217 | 218 | ```JavaScript 219 | (state, action) => newState 220 | ``` 221 | 222 | [slide] 223 | 224 | # 两个注意点 225 | 226 | 227 | - 首次执行Redux时,需要给state一个初始值(初始化时,Redux会自动执行一次Reducer,此时state是undefined,我们应该初始化state) 228 | 229 | - Reducer每次更新状态时需要一个新的state,因此不要直接修改旧的state参数,而是将旧state参数复制一份,在副本上修改值,返回这个副本 230 | 231 | ```JavaScript 232 | if (typeof state === 'undefined') { 233 | return initialState 234 | } 235 | 236 | ... 237 | return { 238 | ...state, 239 | // 更新state中的值 240 | }; 241 | ``` 242 | 243 | [slide] 244 | 245 | 目录结构 246 | 247 | ![reduxReducer](../img/reduxReducer1.jpg) 248 | 249 | [例子 Reducer](http://0.0.0.0:9999/reduxreducer.html) 250 | 251 | [slide] 252 | 253 | # Reducer Creator 254 | 255 | - 取代switch-case,将default封装在函数内 256 | 257 | ```JavaScript 258 | export const createReducer = (initialState, handlers) => { 259 | return (state = initialState, action) => { 260 | if (handlers.hasOwnProperty(action.type)) { 261 | return handlers[action.type](state, action); 262 | } else { 263 | return state; 264 | } 265 | } 266 | }; 267 | ``` 268 | 269 | [slide] 270 | 271 | # 如何切分业务数据? 272 | 273 | - 用combineReducers将多个Reducer合并成一个Reducer 274 | 275 | ```JavaScript 276 | combineReducers({ 277 | myReducer1, 278 | myReducer2, 279 | }); 280 | ``` 281 | 282 | [slide] 283 | 284 | # 源代码 285 | 286 | ```JavaScript 287 | export const combineReducers = (reducers) => { 288 | const reducerKeys = Object.keys(reducers); 289 | return (state = {}, action) => { 290 | const nextState = {}; 291 | for (let i = 0; i < reducerKeys.length; i++) { 292 | const key = reducerKeys[i]; 293 | const reducer = reducers[key]; 294 | nextState[key] = reducer(state[key], action); 295 | } 296 | return { 297 | ...state, 298 | ...nextState, 299 | }; 300 | }; 301 | }; 302 | ``` 303 | 304 | [slide] 305 | 306 | 目录结构 307 | 308 | ![reduxCombineReducer](../img/reduxReducer3.jpg) 309 | 310 | [例子 CombineReducer](http://0.0.0.0:9999/reduxcombinereducer.html) 311 | 312 | [slide] 313 | 314 | # Part 1 315 | 316 | - 概述 317 | 318 | - Action 319 | 320 | - Reducer 321 | 322 | - Store 323 | 324 | - 结合React(用法) 325 | 326 | [slide] 327 | 328 | # 什么是Store? 329 | 330 | 由createStore创建,提供getState,dispatch,subscribe方法,内部存储数据state的仓库 331 | 332 | [slide] 333 | 334 | # 创建Store 335 | 336 | ``` JavaScript 337 | createStore(reducer, [initialState], [enhancer]) 338 | ``` 339 | 340 | - 可选参数initialState用于初始化state 341 | 342 | - 可选参数enhancer是一个高阶函数,用于增强Store 343 | 344 | ``` JavaScript 345 | import { createStore, applyMiddleware, compose } from 'redux'; 346 | import { createLogger } from 'redux-logger'; 347 | 348 | const logger = createLogger(); 349 | const store = createStore(reducer, compose( 350 | applyMiddleware(logger), 351 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), // eslint-disable-line 352 | )); 353 | ``` 354 | 355 | [slide] 356 | 357 | # getState() 358 | 359 | - 获取Store里存储的state 360 | 361 | ``` JavaScript 362 | store.getState().changeNumber.number 363 | ``` 364 | 365 | [slide] 366 | 367 | # dispatch(action) 368 | 369 | - 派发Action,通知Reducer去更新state 370 | 371 | ``` JavaScript 372 | store.dispatch(actions.number.incrementNum()); 373 | ``` 374 | 375 | [slide] 376 | 377 | # subscribe(listener) 378 | 379 | - 注册回调函数,当state发生变化时,会自动触发回调函数 380 | 381 | ``` JavaScript 382 | const update = () => { 383 | // 更新 view 384 | }; 385 | 386 | store.subscribe(update); 387 | ``` 388 | 389 | - 该方法的返回值也是一个函数对象,调用后可以取消注册的回调函数 390 | 391 | ``` JavaScript 392 | const update = () => { 393 | // 更新 view 394 | }; 395 | 396 | const cancelUpdate = store.subscribe(update); 397 | 398 | 399 | ``` 400 | 401 | [slide] 402 | 403 | # 简易版createStore 404 | 405 | ``` JavaScript 406 | export const createStore = (reducer) => { 407 | let state = {}; 408 | const listeners = []; 409 | const getState = () => state; 410 | const dispatch = (action) => { 411 | state = reducer(state, action); 412 | listeners.forEach((listener) => listener()); 413 | }; 414 | const subscribe = (listener) => listeners.push(listener); 415 | 416 | return { 417 | getState, 418 | dispatch, 419 | subscribe, 420 | }; 421 | }; 422 | ``` 423 | 424 | [slide] 425 | 426 | [例子 Store](http://0.0.0.0:9999/reduxstore.html) 427 | 428 | [slide] 429 | 430 | # Part 1 431 | 432 | - 概述 433 | 434 | - Action 435 | 436 | - Reducer 437 | 438 | - Store 439 | 440 | - 结合React(用法) 441 | 442 | [slide] 443 | 444 | - container目录里的组件需要关心Redux 445 | 446 | - component目录里的组件仅做展示用,不需要关心Redux 447 | 448 | 目录结构 449 | 450 | ![reactredux](../img/reactredux1.jpg) 451 | 452 | [例子 react-redux](http://0.0.0.0:9999/reactredux.html) 453 | 454 | [slide] 455 | 456 | # react-redux包 457 | 458 | - `````` 459 | - connect 460 | 461 | [slide] 462 | 463 | # `````` 464 | 465 | - 将入口组件包进去(被包进的组件及子组件才能访问到Store,才能使用connect方法) 466 | 467 | ``` JavaScript 468 | import { Provider } from 'react-redux'; // 引入 react-redux 469 | 470 | …… 471 | render( 472 | 473 | 474 | , 475 | document.getElementById('app'), 476 | ); 477 | ``` 478 | 479 | [slide] 480 | 481 | # connect 482 | 483 | ``` 484 | connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) 485 | ``` 486 | 487 | ``` JavaScript 488 | const mapStateToProps = (state) => { 489 | return { 490 | number: state.changeNumber.number, 491 | showAlert: state.toggleAlert.showAlert, 492 | }; 493 | }; 494 | 495 | const mapDispatchToProps = { 496 | incrementNum: action.number.incrementNum, 497 | decrementNum: action.number.decrementNum, 498 | clearNum: action.number.clearNum, 499 | toggleAlert: action.alert.toggleAlert, 500 | }; 501 | 502 | export default connect( 503 | mapStateToProps, 504 | mapDispatchToProps, 505 | )(Sample); 506 | ``` 507 | 508 | [slide] 509 | 510 | # mapStateToProps 511 | 512 | - connect方法的第一个参数mapStateToProps是一个function(负责输入) 513 | - 作用是将Store里的state变成组件的props。当state更新时,会同步更新组件的props,触发组件的render方法 514 | - function返回值是一个key-value的plain object 515 | 516 | ``` JavaScript 517 | const mapStateToProps = (state) => { 518 | return { 519 | number: state.changeNumber.number, 520 | showAlert: state.toggleAlert.showAlert, 521 | }; 522 | }; 523 | ``` 524 | - 如果mapStateToProps为空(即设成()=>({})),那Store里的任何更新就不会触发组件的render方法。 525 | 526 | [slide] 527 | 528 | # mapDispatchToProps 529 | 530 | - connect方法的第二个参数mapDispatchToProps可以是一个object也可以是一个function(负责输出) 531 | - 作用是将```dispatch(action)```绑定到组件的props上,这样组件就能派发Action,更新state了 532 | 533 | [slide] 534 | 535 | # object型mapDispatchToProps 536 | 537 | - 是一个key-value的plain object 538 | - key是组件props 539 | - value是一个Action creator 540 | 541 | ``` JavaScript 542 | const mapDispatchToProps = { 543 | incrementNum: action.number.incrementNum, 544 | decrementNum: action.number.decrementNum, 545 | clearNum: action.number.clearNum, 546 | toggleAlert: action.alert.toggleAlert, 547 | }; 548 | ``` 549 | - 这样就能在组件中通过```this.props.incrementNum()```方式来dispatch Action出去 550 | - 但为何不是```dispatch(this.props.incrementNum())```?? 551 | 552 | [slide] 553 | 554 | # function型mapDispatchToProps 555 | 556 | - 是一个function 557 | - 参数是dispatch方法 558 | - 返回值是object型mapDispatchToProps 559 | 560 | ``` JavaScript 561 | import { bindActionCreators } from 'redux'; 562 | 563 | const mapDispatchToProps2 = (dispatch, ownProps) => { 564 | return { 565 | incrementNum: bindActionCreators(action.number.incrementNum, dispatch), 566 | decrementNum: bindActionCreators(action.number.decrementNum, dispatch), 567 | clearNum: bindActionCreators(action.number.clearNum, dispatch), 568 | toggleAlert: bindActionCreators(action.alert.toggleAlert, dispatch), 569 | }; 570 | }; 571 | ``` 572 | - 解释了上一页的疑问。其实dispatch已经被封装进去了,因此你不必手动写dispatch了 573 | 574 | [slide] 575 | 576 | # mergeProps 577 | 578 | - 经过conncet的组件的props有3个来源: 579 | - 1.由mapStateToProps将state映射成的props 580 | - 2.由mapDispatchToProps将```dispatch(action)```映射成的props 581 | - 3.组件自身的props。 582 | 583 | mergeProps的参数分别对应了上面3个来源,作用是整合这些props 584 | 585 | (例如过滤掉不需要的props,重新组织props,根据ownProps绑定不同的stateProps和dispatchProps等) 586 | 587 | [slide] 588 | 589 | 例如过滤掉不需要的props: 590 | 591 | ``` JavaScript 592 | const mergeProps = (stateProps, dispatchProps, ownProps) => { 593 | return { 594 | ...ownProps, 595 | ...stateProps, 596 | incrementNum: dispatchProps.incrementNum, // 只输出incrementNum 597 | }; 598 | }; 599 | 600 | export default connect( 601 | mapStateToProps, 602 | mapDispatchToProps, 603 | mergeProps, 604 | )(Sample); 605 | ``` 606 | 607 | [slide] 608 | 609 | 例如重新组织props: 610 | 611 | ``` JavaScript 612 | const mergeProps = (stateProps, dispatchProps, ownProps) => { 613 | return { 614 | ...ownProps, 615 | state: stateProps, 616 | actions: { 617 | ...dispatchProps, 618 | ...ownProps.actions, 619 | }, 620 | }; 621 | }; 622 | 623 | export default connect( 624 | mapStateToProps, 625 | mapDispatchToProps, 626 | mergeProps, 627 | )(Sample); 628 | ``` 629 | 630 | [slide] 631 | 632 | # 总结 633 | 634 | - react-redux一共就一个组件和一个API: 635 | 636 | - ``````用于包裹React组件,被包裹的组件可以使用connect方法。 637 | 638 | - conncet方法用于将组件绑定Redux。第一个参数负责输入,将state映射成组件props。第二个参数负责输出,将```dispatch(action)```映射成组件props。第三个参数用于整合props。第四个参数可以做一些优化,具体见官网。 639 | 640 | [slide] 641 | 642 | # THE END 643 | 644 | ### THANK YOU 645 | -------------------------------------------------------------------------------- /ppt/redux-2.md: -------------------------------------------------------------------------------- 1 | title: Redux (2) 2 | speaker: JackZhang 3 | theme: dark 4 | transition: move 5 | 6 | [slide] 7 | 8 | # Redux介绍 9 | JackZhang 10 | 11 | [slide] 12 | 13 | # Part 1 14 | 15 | - 概述 16 | 17 | - Action 18 | 19 | - Reducer 20 | 21 | - Store 22 | 23 | - 结合React(用法) 24 | 25 | # Part 2 26 | 27 | - 结合React(原理) 28 | 29 | - 中间件 30 | 31 | - 异步Action 32 | 33 | [slide] 34 | 35 | # Part 2 36 | 37 | - 结合React(原理) 38 | 39 | - 中间件 40 | 41 | - 异步Action 42 | 43 | [slide] 44 | 45 | # react-redux的实现原理 46 | 47 | - React里有个全局变量context,可用将组件间共享的数据放到context里 48 | 49 | - 优点是:所有组件都可以随时访问到context里共享的值,免去了数据层层传递的麻烦 50 | 51 | - 缺点是:全局变量意味着所有人都可以随意修改它,导致不可控。而且和React组件化设计思想不符合 52 | 53 | [slide] 54 | 55 | # context能和react-redux完美结合 56 | 57 | - Redux设计思想就是单一数据源,集中维护state。(context天生就是唯一数据源) 58 | 59 | - Redux设计思想就是不允许随意修改state,这样数据存到context里,也无法随意修改数据 60 | 61 | - context成了一个可控的唯一的全局变量,完美! 62 | 63 | [slide] 64 | 65 | # Provider组件的实现原理 66 | 67 | - 将store保存进context,让子组件可以访问到context里的Store 68 | 69 | ``` JavaScript 70 | import React, { Component } from 'react'; 71 | import PropTypes from 'prop-types'; 72 | 73 | export default class Provider extends Component { 74 | static childContextTypes = { 75 | store: PropTypes.object, 76 | }; 77 | 78 | getChildContext = () => { 79 | return { store: this.props.store, }; 80 | }; 81 | 82 | render() { 83 | return (
{this.props.children}
); 84 | } 85 | } 86 | ``` 87 | 88 | [slide] 89 | 90 | # connect高阶组件目的 91 | 92 | - 被``````包裹的子组件能访问到context里的store 93 | 94 | ``` JavaScript 95 | export class myComponent extends Component { 96 | ... 97 | static contextTypes = { 98 | store: PropTypes.object 99 | } 100 | ... 101 | } 102 | ``` 103 | 104 | - 但这样每个组件里都要写上述代码太麻烦了,用HOC高阶组件来消除重复代码 105 | 106 | [slide] 107 | 108 | # connect高阶组件示意图 109 | 110 |
111 | 112 |
113 | 114 | [slide] 115 | 116 | # connect高阶组件实现一 117 | 118 | - 第一步:内部封装掉了每个组件都要写的访问context的代码 119 | 120 | ``` JavaScript 121 | import React, { Component } from 'react'; 122 | import PropTypes from 'prop-types'; 123 | 124 | const connect = (WrappedComponent) => { 125 | class Connect extends Component { 126 | static contextTypes = { 127 | store: PropTypes.object, 128 | }; 129 | 130 | render() { 131 | return (); 132 | } 133 | } 134 | 135 | return Connect; 136 | }; 137 | 138 | export default connect; 139 | ``` 140 | 141 | [slide] 142 | 143 | # connect高阶组件实现二 144 | 145 | - 第二步:参数mapStateToProps封装掉组件从context中取Store的代码 146 | 147 | ``` JavaScript 148 | const connect = (mapStateToProps) => (WrappedComponent) => { 149 | class Connect extends Component { 150 | ... 151 | render() { 152 | const { store } = this.context; 153 | const stateProps = mapStateToProps(store.getState()); 154 | return (); 155 | } 156 | } 157 | 158 | return Connect; 159 | }; 160 | ``` 161 | 162 | [slide] 163 | 164 | # connect高阶组件实现三 165 | 166 | - 第三步:参数mapDispatchToProps封装掉组件往context里更新Store的代码 167 | 168 | ``` JavaScript 169 | export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { 170 | class Connect extends Component { 171 | ... 172 | render() { 173 | const { store } = this.context; 174 | const stateProps = mapStateToProps(store.getState()); 175 | const dispatchProps = mapDispatchToProps(store.dispatch); 176 | 177 | const finalProps = { 178 | ...stateProps, 179 | ...dispatchProps, 180 | }; 181 | 182 | return (); 183 | } 184 | } 185 | 186 | return Connect 187 | } 188 | ``` 189 | 190 | [slide] 191 | 192 | # connect高阶组件实现四 193 | 194 | - 第四步:封装掉subscribe,当store变化,刷新组件的props,触发组件的render方法 195 | 196 | ``` JavaScript 197 | export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { 198 | class Connect extends Component { 199 | ... 200 | constructor() { 201 | super(); 202 | this.state = { finalProps: {} }; 203 | } 204 | 205 | componentWillMount() { 206 | const { store } = this.context; 207 | this.updateProps(); 208 | store.subscribe(this.updateProps); 209 | } 210 | 211 | updateProps = () => { 212 | const { store } = this.context; 213 | const stateProps = mapStateToProps(store.getState()); 214 | const dispatchProps = mapDispatchToProps(store.dispatch); 215 | 216 | const finalProps = { 217 | ...stateProps, 218 | ...dispatchProps, 219 | ...this.props, 220 | }; 221 | 222 | this.setState({ 223 | finalProps, 224 | }); 225 | }; 226 | ... 227 | } 228 | 229 | return Connect 230 | } 231 | ``` 232 | 233 | [slide] 234 | 235 | # connect高阶组件实现完整版 236 | 237 | ``` JavaScript 238 | import React, { Component } from 'react'; 239 | import PropTypes from 'prop-types'; 240 | 241 | const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { 242 | class Connect extends Component { 243 | static contextTypes = { 244 | store: PropTypes.object, 245 | }; 246 | 247 | constructor() { 248 | super(); 249 | this.state = { finalProps: {} }; 250 | } 251 | 252 | componentWillMount() { 253 | const { store } = this.context; 254 | this.updateProps(); 255 | store.subscribe(this.updateProps); 256 | } 257 | 258 | updateProps = () => { 259 | const { store } = this.context; 260 | const stateProps = mapStateToProps(store.getState()); 261 | const dispatchProps = mapDispatchToProps(store.dispatch); 262 | 263 | const finalProps = { 264 | ...stateProps, 265 | ...dispatchProps, 266 | ...this.props, 267 | }; 268 | 269 | this.setState({ 270 | finalProps, 271 | }); 272 | }; 273 | 274 | render() { 275 | const { finalProps } = this.state; 276 | return (); 277 | } 278 | } 279 | 280 | return Connect; 281 | }; 282 | 283 | export default connect; 284 | ``` 285 | 286 | [slide] 287 | 288 | # connect高阶组件示意图(回顾) 289 | 290 |
291 | 292 |
293 | 294 | [slide] 295 | 296 | # 总结(回顾) 297 | 298 | - react-redux一共就一个组件和一个API: 299 | 300 | - ``````用于在入口处包裹需要用到React的组件。本质上是将store放入context里 301 | 302 | - conncet方法用于将组件绑定Redux。本质上是HOC,封装掉了每个组件都要写的板式代码,增加了功能。 303 | 304 | - react-redux的高封装性让开发者感知不到context的存在,甚至感知不到Store的getState,subscribe,dispatch的存在。只要connect一下,数据一变就自动刷新React组件,非常方便。 305 | 306 | [slide] 307 | 308 | # Part 2 309 | 310 | - 结合React 311 | 312 | - 中间件 313 | 314 | - 异步Action 315 | 316 | [slide] 317 | 318 | # 中间件的作用 319 | 320 | - 在流程中插入功能 321 | 322 | - 要满足两个特性:一是扩展功能,二是可以被链式调用。 323 | 324 | [slide] 325 | 326 | # 例子 327 | 328 | - redux-logger中间件 329 | 330 | ![redux-logger](../img/redux-logger.jpeg) 331 | 332 | [slide] 333 | 334 | # 需求:打印log 335 | 336 | - 需求:自动打印出Action对象和更新前后的state,便于调试和追踪数据变化流 337 | 338 | ``` JavaScript 339 | console.log('current state: ', store.getState()); 340 | console.log('dispatching', action); 341 | store.dispatch(action); 342 | console.log('next state', store.getState()); 343 | ``` 344 | 345 | - 我们需要封装打印log的代码,否则让程序员在每个dispatch的地方写log是不可接受的 346 | 347 | [slide] 348 | 349 | # 打印log的中间件 350 | 351 | ``` JavaScript 352 | const preDispatch = store.dispatch; 353 | 354 | store.dispatch = (action) => { 355 | console.log('current state: ', store.getState()); 356 | console.log('action: ', action); 357 | preDispatch(action); 358 | console.log('next state: ', store.getState()); 359 | }; 360 | ``` 361 | 362 | - 上述就是增强版store.dispatch,这就是Redux的中间件 363 | 364 | [slide] 365 | 366 | # log中间件为何只能封装到dispatch里? 367 | 368 | - ~~Action~~ (HOW?plain object) 369 | - ~~Action Creator~~ (WHERE?not updated) 370 | - ~~Reducer~~ (OK...But not pure) 371 | - dispatch 372 | 373 | - Redux的中间件本质上就是增强dispatch 374 | 375 | [slide] 376 | 377 | # 多个中间件 378 | 379 | - 例如将上述打印logger的中间件拆成两个 380 | 381 | ``` JavaScript 382 | // 只打印出 Action 383 | export const loggerAction = (store) => { 384 | return (dispatch) => { 385 | return (action) => { 386 | console.log('action: ', action); 387 | dispatch(action); 388 | }; 389 | }; 390 | }; 391 | 392 | // 只打印出 更新后的state 393 | export const loggerState = (store) => { 394 | return (dispatch) => { 395 | return (action) => { 396 | console.log('current state: ', store.getState()); 397 | dispatch(action); 398 | console.log('next state', store.getState()); 399 | }; 400 | }; 401 | }; 402 | ``` 403 | 404 | [slide] 405 | 406 | # 多个中间件就像洋葱圈 407 | 408 | ![reduxmiddleware](../img/reduxmiddleware2.jpg) 409 | 410 | [slide] 411 | 412 | # 更优雅的链式调用 413 | 414 | - 前面的例子里中间件已经实现了链式调用,前一个中间件增强过的store作为参数传递给下一个中间件。但用起来不够优雅 415 | 416 | - Redux提供applyMiddleware方法,允许将所有中间件作为参数传递进去。我们来自己实现这个方法 417 | 418 | [slide] 419 | 420 | # applyMiddleware 421 | 422 | ``` JavaScript 423 | export const applyMiddleware = (store, middlewares) => { 424 | let dispatch = store.dispatch; 425 | middlewares.forEach((middleware) => { 426 | dispatch = middleware(store)(dispatch); 427 | }); 428 | 429 | return { 430 | ...store, 431 | dispatch, 432 | }; 433 | }; 434 | 435 | let store = createStore(reducer); 436 | store = applyMiddleware(store, [loggerAction, loggerState]); 437 | ``` 438 | 439 | [slide] 440 | 441 | # applyMiddleware官方版 442 | 443 | - 官方版的applyMiddleware将第一个参数store也被精简掉了 444 | 445 | ``` JavaScript 446 | export const applyMiddlewarePlus = (...middlewares) => (createStore) => (reducer) => { 447 | const store = createStore(reducer); 448 | 449 | let dispatch = store.dispatch; 450 | middlewares.forEach((middleware) => { 451 | dispatch = middleware(store)(dispatch); 452 | }); 453 | 454 | return { 455 | ...store, 456 | dispatch, 457 | }; 458 | }; 459 | ``` 460 | 461 | [slide] 462 | 463 | # 总结 464 | 465 | - Redux里的中间件就是增强Store.dispatch功能 466 | 467 | - 开发中间件,需要支持链式调用 468 | 469 | [slide] 470 | 471 | # Part 2 472 | 473 | - 结合React 474 | 475 | - 中间件 476 | 477 | - 异步Action 478 | 479 | [slide] 480 | 481 | # 什么是异步Action 482 | 483 | - 需要异步操作时(ajax请求,读取文件等),你需要异步Action 484 | 485 | - Action本质是plain object,不存在同步异步的概念。所谓异步Action,本质上是打包一系列Action动作 486 | 487 | [slide] 488 | 489 | # redux-thunk中间件 490 | 491 | - 例如网络请求数据: 492 | 493 | - (1)dispatch出请求服务器数据的Action 494 | 495 | - (2)等待服务器响应 496 | 497 | - (3)dispatch出结果Action(携带服务器返回了的数据或异常)去更新state 498 | 499 | - redux-thunk中间件的作用:将这三步封装到Action Creator里,以实现打包一系列Action动作的目的 500 | 501 | [slide] 502 | 503 | # redux-thunk实现方式 504 | 505 | - 常规的Action creator返回一个Action,但redux-thunk,允许你的Action creator还可以返回一个```function(dispatch, getState)``` 506 | 507 | ``` JavaScript 508 | function createThunkMiddleware(extraArgument) { 509 | return function (_ref) { 510 | var dispatch = _ref.dispatch, 511 | getState = _ref.getState; 512 | return function (next) { 513 | return function (action) { 514 | if (typeof action === 'function') { 515 | return action(dispatch, getState, extraArgument); 516 | } 517 | 518 | return next(action); 519 | }; 520 | }; 521 | }; 522 | } 523 | ``` 524 | 525 | [slide] 526 | 527 | # 实现网络请求的异步Action 528 | 529 | - 第一步dispatch出请求服务器数据的Action 530 | 531 | ``` JavaScript 532 | const requestData = () => ({ 533 | type: constant.REQUEST_DATA, 534 | }); 535 | ``` 536 | 537 | [slide] 538 | 539 | # 实现网络请求的异步Action 540 | 541 | - 第二步dispatch出结果Action(携带服务器返回了的数据或异常)去更新state 542 | 543 | ``` JavaScript 544 | const receiveData = (data) => ({ 545 | type: constant.RECEIVE_DATA, 546 | data: data.msg, 547 | }); 548 | ``` 549 | 550 | [slide] 551 | 552 | # 实现网络请求的异步Action 553 | 554 | - 第三步用redux-thunk将这两步连起来 555 | 556 | ``` JavaScript 557 | const doFetchData = () => (dispatch) => { 558 | dispatch(requestData()); 559 | return fetch('./api/fetchSampleData.json') 560 | .then((response) => response.json()) 561 | .then((json) => dispatch(receiveData(json))); 562 | }; 563 | 564 | export default { 565 | fetchDataAction: () => (dispatch) => { 566 | return dispatch(doFetchData()); 567 | }, 568 | }; 569 | ``` 570 | 571 | [slide] 572 | 573 | # 实现网络请求的异步Action 574 | 575 | - 完整版(加上了请求未完成不允许连续请求,减少服务器开销的逻辑) 576 | 577 | ``` JavaScript 578 | import fetch from 'isomorphic-fetch'; 579 | import * as constant from '../configs/action'; 580 | import { sleep } from '../lib/common'; 581 | 582 | const requestData = () => ({ 583 | type: constant.REQUEST_DATA, 584 | }); 585 | 586 | const receiveData = (data) => ({ 587 | type: constant.RECEIVE_DATA, 588 | data: data.msg, 589 | }); 590 | 591 | const doFetchData = () => async(dispatch) => { 592 | dispatch(requestData()); 593 | await sleep(1000); // Just 4 mock 594 | return fetch('./api/fetchSampleData.json') 595 | .then((response) => response.json()) 596 | .then((json) => dispatch(receiveData(json))); 597 | }; 598 | 599 | const canFetchData = (state) => { 600 | return !state.fetchData.fetching; 601 | }; 602 | 603 | export default { 604 | fetchDataAction: () => (dispatch, getState) => { 605 | if (canFetchData(getState())) { 606 | return dispatch(doFetchData()); 607 | } 608 | return Promise.resolve(); 609 | }, 610 | }; 611 | ``` 612 | 613 | [slide] 614 | 615 | # Part 1 616 | 617 | - 概述(三大原则,初始态流转图,更新态流转图) 618 | 619 | - Action(Action,Action Creator) 620 | 621 | - Reducer(纯函数,Reducer Creator,combineReducers) 622 | 623 | - Store(createStore,getState,dispatch,subscribe) 624 | 625 | # Part 2 626 | 627 | - 结合React(Provider,connect,用context实现原理,HOC) 628 | 629 | - 中间件(洋葱圈式增强dispatch,实现链式调用) 630 | 631 | - 异步Action(redux-thunk) 632 | 633 | [slide] 634 | 635 | # 脚手架目录结构 636 | 637 |
638 | 639 |
640 | 641 | [slide] 642 | 643 |
644 | 645 |
646 | 647 | # THE END 648 | 649 | ### THANK YOU 650 | -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | // require('babel-polyfill'); 2 | require('babel-register')(); 3 | require('./dev/index'); 4 | -------------------------------------------------------------------------------- /scripts/dev/index.js: -------------------------------------------------------------------------------- 1 | import open from 'open'; 2 | import webpack from 'webpack'; 3 | import WebpackDevServer from 'webpack-dev-server'; 4 | import path from 'path'; 5 | import webpackConfig from '../webpack.config'; 6 | 7 | const compiler = webpack(webpackConfig); 8 | 9 | const SRC = path.join(process.cwd(), 'src'); 10 | const MOCK_SERVER = path.join(process.cwd(), 'mock-server'); 11 | 12 | const devServerOptions = { 13 | contentBase: [ 14 | SRC, 15 | MOCK_SERVER, 16 | ], 17 | hot: true, 18 | historyApiFallback: true, 19 | stats: 'verbose', 20 | }; 21 | 22 | const server = new WebpackDevServer(compiler, devServerOptions); 23 | let opened = false; 24 | 25 | const openBrowser = () => { 26 | const address = server.listeningApp.address(); 27 | const url = `http://${address.address}:${address.port}`; 28 | console.log(` server started: ${url}`); 29 | open(`${url}/redux.html`); 30 | }; 31 | 32 | compiler.plugin('done', () => { 33 | if (!opened) { 34 | opened = true; 35 | openBrowser(); 36 | } 37 | }); 38 | 39 | const startServer = new Promise((resolve, reject) => { 40 | server.listen(webpackConfig.devServer.port, webpackConfig.devServer.host, (err) => { 41 | if (err) { 42 | reject(err); 43 | } else { 44 | resolve(); 45 | } 46 | }); 47 | }); 48 | 49 | const devServer = async() => { 50 | await startServer; 51 | 52 | const stdIn = process.stdin; 53 | stdIn.setEncoding('utf8'); 54 | stdIn.on('data', openBrowser); 55 | }; 56 | 57 | devServer().catch((ex) => { 58 | console.error(ex); 59 | }); 60 | 61 | -------------------------------------------------------------------------------- /scripts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const glob = require('glob'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | // const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | const PostcssImport = require('postcss-import'); 8 | const precss = require('precss'); 9 | const cssnext = require('postcss-cssnext'); 10 | 11 | const ENV_DEVELOPMENT = 'development'; 12 | const ENV_PRODUCTION = 'production'; 13 | const HOST = process.env.HOST || `0.0.0.0`; 14 | const PORT = process.env.PORT || 9999; 15 | 16 | const SRC = path.join(process.cwd(), 'src'); 17 | const BUILD = path.join(process.cwd(), 'build'); 18 | const TEMPLATE = path.join(process.cwd(), 'template'); 19 | const ENTRIES = path.join(SRC, 'entries'); 20 | const BASEDIR = path.join(__dirname, '..'); 21 | 22 | const NODE_ENV = process.env.NODE_ENV || 'production'; 23 | 24 | const config = { 25 | // entry: { 26 | // common: [ 27 | // // 'babel-polyfill', 28 | // 'react', 29 | // 'react-dom' 30 | // ] 31 | // }, 32 | entry: {}, 33 | output: { 34 | path: `${BUILD}`, 35 | filename: '[name]', 36 | publicPath: './' 37 | }, 38 | devtool: 'eval', 39 | module: { 40 | loaders: [ 41 | { 42 | test: /\.js$/, 43 | exclude: /node_modules/, 44 | use: ['babel-loader', 'react-hot-loader/webpack'], 45 | }, 46 | { 47 | test: /\.(png|jpg|gif)$/, 48 | loader: 'url-loader?limit=2048000', 49 | }, 50 | { 51 | test: /\.css$/, 52 | use: ['style-loader', 'css-loader'], 53 | }, 54 | { 55 | test: /\.pcss$/, 56 | use: [ 57 | 'style-loader', 58 | 'css-loader', 59 | { 60 | loader: 'postcss-loader', 61 | options: { 62 | plugins: function() { 63 | return [ 64 | PostcssImport(), 65 | precss, 66 | cssnext, 67 | ]; 68 | }, 69 | }, 70 | }, 71 | ], 72 | }, 73 | { 74 | enforce: 'pre', 75 | test: /\.js$/, 76 | include: [ 77 | path.resolve(BASEDIR, 'src'), 78 | ], 79 | use: 'eslint-loader', 80 | } 81 | ] 82 | }, 83 | plugins: [ 84 | // new ExtractTextPlugin(`${ENTRIES}/[name]-[hash].pcss`), 85 | // new webpack.optimize.CommonsChunkPlugin('common', 'js/common.js'), 86 | // new webpack.NamedModulesPlugin(), 87 | ], 88 | }; 89 | 90 | 91 | const entryFileNameList = glob.sync(path.join(SRC, '*/entries') + '/*.js'); 92 | entryFileNameList.forEach((item) => { 93 | const { 94 | entry, 95 | plugins 96 | } = config; 97 | 98 | let fileName = path.basename(item, '.js'); 99 | entry[fileName] = [`${item}`]; 100 | console.log('entry[item]: ', entry[fileName]); 101 | 102 | entry[fileName].unshift(`webpack-dev-server/client?http://${HOST}:${PORT}`); 103 | entry[fileName].unshift(`webpack/hot/log-apply-result`); 104 | 105 | // hot reload 106 | entry[fileName].unshift(`webpack/hot/only-dev-server`); 107 | entry[fileName].unshift(`react-hot-loader/patch`); 108 | 109 | const htmlName = fileName.toLowerCase(); 110 | plugins.push( 111 | new HtmlWebpackPlugin({ 112 | template: `${TEMPLATE}/index.html`, 113 | filename: `${htmlName}.html`, 114 | hash: false, 115 | inject: 'body', 116 | chunks: [ 117 | // 'common', 118 | fileName, 119 | ] 120 | }) 121 | ); 122 | }); 123 | 124 | switch (NODE_ENV) { 125 | case ENV_DEVELOPMENT: 126 | console.log('dev start'); 127 | config.output.publicPath = '/'; 128 | config.devServer = { 129 | historyApiFallback: true, 130 | hot: true, 131 | inline: true, 132 | progress: true, 133 | stats: { 134 | colors: true, 135 | hash: false, 136 | version: false, 137 | timings: false, 138 | assets: false, 139 | chunks: false, 140 | modules: false, 141 | reasons: false, 142 | children: false, 143 | source: false, 144 | errors: true, 145 | errorDetails: true, 146 | warnings: true, 147 | publicPath: false 148 | }, 149 | host: HOST, 150 | port: PORT 151 | }; 152 | 153 | config.plugins.push( 154 | new webpack.HotModuleReplacementPlugin() 155 | ); 156 | break; 157 | // case ENV_PRODUCTION: 158 | // config.plugins.push( 159 | // new webpack.optimize.UglifyJsPlugin({ 160 | // compress: { 161 | // warnings: false 162 | // } 163 | // }), 164 | // new webpack.DefinePlugin({ 165 | // 'process.env': { 166 | // NODE_ENV: JSON.stringify('production') 167 | // } 168 | // }) 169 | // ); 170 | // break; 171 | default: 172 | break; 173 | } 174 | 175 | module.exports = config; 176 | -------------------------------------------------------------------------------- /src/originRedux/entries/redux.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import Demo from '../originRedux'; 5 | 6 | render( 7 | 8 | 9 | , 10 | document.getElementById('app'), 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('../originRedux', () => { 15 | const newDemo = require('../originRedux').default; 16 | render( 17 | 18 | {React.createElement(newDemo)} 19 | , 20 | document.getElementById('app'), 21 | ); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/originRedux/originRedux.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { createStore } from 'redux'; 3 | import { Button } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import './originRedux.pcss'; 6 | 7 | const reducer = (state, action) => { 8 | if (typeof state === 'undefined') { 9 | return 0; 10 | } 11 | 12 | switch (action.type) { 13 | case 'INCREMENT': 14 | return state + 1; 15 | case 'DECREMENT': 16 | return state - 1; 17 | case 'CLEAR_NUM': 18 | return 0; 19 | default: 20 | return state; 21 | } 22 | }; 23 | 24 | const store = createStore(reducer); 25 | 26 | const update = () => { 27 | const valueEl = document.getElementsByClassName('numValue'); 28 | valueEl[0].innerHTML = store.getState().toString(); 29 | }; 30 | 31 | store.subscribe(update); 32 | 33 | export default class Number extends Component { 34 | 35 | addNum = () => { 36 | store.dispatch({ type: 'INCREMENT' }); 37 | }; 38 | 39 | minusNum = () => { 40 | store.dispatch({ type: 'DECREMENT' }); 41 | }; 42 | 43 | clearNum = () => { 44 | store.dispatch({ type: 'CLEAR_NUM' }); 45 | }; 46 | 47 | render() { 48 | return ( 49 |
50 |

origin Redux

51 | Current Number: 0 52 |
53 | 54 | 55 | 56 |
57 |
58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/originRedux/originRedux.pcss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | margin: 20px; 3 | font-size: 18px; 4 | .numValue { 5 | color: red; 6 | } 7 | .numBtn { 8 | margin: 10px; 9 | &:first-child { 10 | margin-left: 0; 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/originReduxAction/actions/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export const incrementNum = () => ({ 4 | type: constant.INCREMENT, 5 | }); 6 | 7 | export const decrementNum = () => ({ 8 | type: constant.DECREMENT, 9 | }); 10 | 11 | export const clearNum = () => ({ 12 | type: constant.CLEAR_NUM, 13 | }); 14 | -------------------------------------------------------------------------------- /src/originReduxAction/configs/action.js: -------------------------------------------------------------------------------- 1 | // action.type 常量 2 | export const INCREMENT = 'INCREMENT'; 3 | export const DECREMENT = 'DECREMENT'; 4 | export const CLEAR_NUM = 'CLEAR_NUM'; 5 | -------------------------------------------------------------------------------- /src/originReduxAction/entries/reduxaction.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import Demo from '../originReduxAction'; 5 | 6 | render( 7 | 8 | 9 | , 10 | document.getElementById('app'), 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('../originReduxAction', () => { 15 | const newDemo = require('../originReduxAction').default; 16 | render( 17 | 18 | {React.createElement(newDemo)} 19 | , 20 | document.getElementById('app'), 21 | ); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/originReduxAction/originRedux.pcss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | margin: 20px; 3 | font-size: 18px; 4 | .numValue { 5 | color: red; 6 | } 7 | .numBtn { 8 | margin: 10px; 9 | &:first-child { 10 | margin-left: 0; 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/originReduxAction/originReduxAction.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { createStore } from 'redux'; 3 | import { Button } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import * as constant from './configs/action'; 6 | import * as actions from './actions/number'; 7 | import './originRedux.pcss'; 8 | 9 | const reducer = (state, action) => { 10 | if (typeof state === 'undefined') { 11 | return 0; 12 | } 13 | 14 | switch (action.type) { 15 | case constant.INCREMENT: 16 | return state + 1; 17 | case constant.DECREMENT: 18 | return state - 1; 19 | case constant.CLEAR_NUM: 20 | return 0; 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | const store = createStore(reducer); 27 | 28 | const update = () => { 29 | const valueEl = document.getElementsByClassName('numValue'); 30 | valueEl[0].innerHTML = store.getState().toString(); 31 | }; 32 | 33 | store.subscribe(update); 34 | 35 | export default class Number extends Component { 36 | 37 | addNum = () => { 38 | store.dispatch(actions.incrementNum()); 39 | }; 40 | 41 | minusNum = () => { 42 | store.dispatch(actions.decrementNum()); 43 | }; 44 | 45 | clearNum = () => { 46 | store.dispatch(actions.clearNum()); 47 | }; 48 | 49 | render() { 50 | return ( 51 |
52 |

origin Redux action

53 | Current Number: 0 54 |
55 | 56 | 57 | 58 |
59 |
60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/actions/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | toggleAlert: () => ({ 5 | type: constant.TOGGLE_ALERT, 6 | }), 7 | }; 8 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/actions/index.js: -------------------------------------------------------------------------------- 1 | import number from './number'; 2 | import alert from './alert'; 3 | 4 | export default { 5 | number, 6 | alert, 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/actions/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | incrementNum: () => ({ 5 | type: constant.INCREMENT, 6 | }), 7 | decrementNum: () => ({ 8 | type: constant.DECREMENT, 9 | }), 10 | clearNum: () => ({ 11 | type: constant.CLEAR_NUM, 12 | }), 13 | }; 14 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/configs/action.js: -------------------------------------------------------------------------------- 1 | // action.type 常量 2 | export const INCREMENT = 'INCREMENT'; 3 | export const DECREMENT = 'DECREMENT'; 4 | export const CLEAR_NUM = 'CLEAR_NUM'; 5 | export const TOGGLE_ALERT = 'TOGGLE_ALERT'; 6 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/entries/reduxcombinereducer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import Demo from '../originReduxCombineReducer'; 5 | 6 | render( 7 | 8 | 9 | , 10 | document.getElementById('app'), 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('../originReduxCombineReducer', () => { 15 | const newDemo = require('../originReduxCombineReducer').default; 16 | render( 17 | 18 | {React.createElement(newDemo)} 19 | , 20 | document.getElementById('app'), 21 | ); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/lib/common.js: -------------------------------------------------------------------------------- 1 | export const createReducer = (initialState, handlers) => { 2 | return (state = initialState, action) => { 3 | if (handlers.hasOwnProperty(action.type)) { 4 | return handlers[action.type](state, action); 5 | } 6 | return state; 7 | }; 8 | }; 9 | 10 | export const combineReducers = (reducers) => { 11 | const reducerKeys = Object.keys(reducers); 12 | return (state = {}, action) => { 13 | const nextState = {}; 14 | for (let i = 0; i < reducerKeys.length; i++) { 15 | const key = reducerKeys[i]; 16 | const reducer = reducers[key]; 17 | nextState[key] = reducer(state[key], action); 18 | } 19 | return { 20 | ...state, 21 | ...nextState, 22 | }; 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/originRedux.pcss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | margin: 20px; 3 | font-size: 18px; 4 | .numValue { 5 | color: red; 6 | } 7 | .numBtn { 8 | margin: 10px; 9 | &:first-child { 10 | margin-left: 0; 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/originReduxCombineReducer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { createStore, compose } from 'redux'; 3 | import { Button, Alert } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import reducer from './reducers/index'; 6 | import actions from './actions/index'; 7 | import './originRedux.pcss'; 8 | 9 | const store = createStore(reducer, compose( 10 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), // eslint-disable-line 11 | )); 12 | 13 | const update = () => { 14 | const valueEl = document.getElementsByClassName('numValue'); 15 | valueEl[0].innerHTML = store.getState().changeNumber.number; 16 | 17 | const alertEl = document.getElementsByClassName('alert'); 18 | if (store.getState().toggleAlert.showAlert) { 19 | alertEl[0].style.display = 'block'; 20 | } else { 21 | alertEl[0].style.display = 'none'; 22 | } 23 | }; 24 | 25 | store.subscribe(update); 26 | 27 | export default class Number extends Component { 28 | 29 | addNum = () => { 30 | store.dispatch(actions.number.incrementNum()); 31 | }; 32 | 33 | minusNum = () => { 34 | store.dispatch(actions.number.decrementNum()); 35 | }; 36 | 37 | clearNum = () => { 38 | store.dispatch(actions.number.clearNum()); 39 | }; 40 | 41 | toggleAlert = () => { 42 | store.dispatch(actions.alert.toggleAlert()); 43 | }; 44 | 45 | render() { 46 | return ( 47 |
48 |

origin Redux combine reducer

49 | Current Number: 0 50 |
51 | 52 | 53 | 54 |
55 |
56 | 57 | 58 |
59 |
60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/reducers/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | showAlert: false, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.TOGGLE_ALERT]: (state, action) => { 10 | return { 11 | ...state, 12 | showAlert: !state.showAlert, 13 | }; 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | // import { combineReducers } from '../lib/common'; 3 | import changeNumber from './number'; 4 | import toggleAlert from './alert'; 5 | 6 | export default combineReducers({ 7 | changeNumber, 8 | toggleAlert, 9 | }); 10 | -------------------------------------------------------------------------------- /src/originReduxCombineReducer/reducers/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | number: 0, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.INCREMENT]: (state, action) => { 10 | return { 11 | ...state, 12 | number: state.number + 1, 13 | }; 14 | }, 15 | [constant.DECREMENT]: (state, action) => { 16 | return { 17 | ...state, 18 | number: state.number - 1, 19 | }; 20 | }, 21 | [constant.CLEAR_NUM]: (state, action) => { 22 | return { 23 | ...state, 24 | number: 0, 25 | }; 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/originReduxReducer/actions/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export const incrementNum = () => ({ 4 | type: constant.INCREMENT, 5 | }); 6 | 7 | export const decrementNum = () => ({ 8 | type: constant.DECREMENT, 9 | }); 10 | 11 | export const clearNum = () => ({ 12 | type: constant.CLEAR_NUM, 13 | }); 14 | -------------------------------------------------------------------------------- /src/originReduxReducer/configs/action.js: -------------------------------------------------------------------------------- 1 | // action.type 常量 2 | export const INCREMENT = 'INCREMENT'; 3 | export const DECREMENT = 'DECREMENT'; 4 | export const CLEAR_NUM = 'CLEAR_NUM'; 5 | -------------------------------------------------------------------------------- /src/originReduxReducer/entries/reduxreducer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import Demo from '../originReduxReducer'; 5 | 6 | render( 7 | 8 | 9 | , 10 | document.getElementById('app'), 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('../originReduxReducer', () => { 15 | const newDemo = require('../originReduxReducer').default; 16 | render( 17 | 18 | {React.createElement(newDemo)} 19 | , 20 | document.getElementById('app'), 21 | ); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/originReduxReducer/originRedux.pcss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | margin: 20px; 3 | font-size: 18px; 4 | .numValue { 5 | color: red; 6 | } 7 | .numBtn { 8 | margin: 10px; 9 | &:first-child { 10 | margin-left: 0; 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/originReduxReducer/originReduxReducer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { createStore } from 'redux'; 3 | import { Button } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import reducer from './reducers/number'; 6 | import * as actions from './actions/number'; 7 | import './originRedux.pcss'; 8 | 9 | const store = createStore(reducer); 10 | 11 | const update = () => { 12 | const valueEl = document.getElementsByClassName('numValue'); 13 | valueEl[0].innerHTML = store.getState().number; 14 | }; 15 | 16 | store.subscribe(update); 17 | 18 | export default class Number extends Component { 19 | 20 | addNum = () => { 21 | store.dispatch(actions.incrementNum()); 22 | }; 23 | 24 | minusNum = () => { 25 | store.dispatch(actions.decrementNum()); 26 | }; 27 | 28 | clearNum = () => { 29 | store.dispatch(actions.clearNum()); 30 | }; 31 | 32 | render() { 33 | return ( 34 |
35 |

origin Redux reducer

36 | Current Number: 0 37 |
38 | 39 | 40 | 41 |
42 |
43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/originReduxReducer/reducers/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | const initialState = { 4 | number: 0, 5 | }; 6 | 7 | export default (state = initialState, action) => { 8 | switch (action.type) { 9 | case constant.INCREMENT: 10 | return { 11 | ...state, 12 | number: state.number + 1, 13 | }; 14 | case constant.DECREMENT: 15 | return { 16 | ...state, 17 | number: state.number - 1, 18 | }; 19 | case constant.CLEAR_NUM: 20 | return { 21 | ...state, 22 | number: 0, 23 | }; 24 | default: 25 | return state; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/originReduxStore/actions/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | toggleAlert: () => ({ 5 | type: constant.TOGGLE_ALERT, 6 | }), 7 | }; 8 | -------------------------------------------------------------------------------- /src/originReduxStore/actions/index.js: -------------------------------------------------------------------------------- 1 | import number from './number'; 2 | import alert from './alert'; 3 | 4 | export default { 5 | number, 6 | alert, 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /src/originReduxStore/actions/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | incrementNum: () => ({ 5 | type: constant.INCREMENT, 6 | }), 7 | decrementNum: () => ({ 8 | type: constant.DECREMENT, 9 | }), 10 | clearNum: () => ({ 11 | type: constant.CLEAR_NUM, 12 | }), 13 | }; 14 | -------------------------------------------------------------------------------- /src/originReduxStore/configs/action.js: -------------------------------------------------------------------------------- 1 | // action.type 常量 2 | export const INCREMENT = 'INCREMENT'; 3 | export const DECREMENT = 'DECREMENT'; 4 | export const CLEAR_NUM = 'CLEAR_NUM'; 5 | export const TOGGLE_ALERT = 'TOGGLE_ALERT'; 6 | -------------------------------------------------------------------------------- /src/originReduxStore/entries/reduxstore.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import Demo from '../originReduxStore'; 5 | 6 | render( 7 | 8 | 9 | , 10 | document.getElementById('app'), 11 | ); 12 | 13 | if (module.hot) { 14 | module.hot.accept('../originReduxStore', () => { 15 | const newDemo = require('../originReduxStore').default; 16 | render( 17 | 18 | {React.createElement(newDemo)} 19 | , 20 | document.getElementById('app'), 21 | ); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/originReduxStore/lib/common.js: -------------------------------------------------------------------------------- 1 | export const createReducer = (initialState, handlers) => { 2 | return (state = initialState, action) => { 3 | if (handlers.hasOwnProperty(action.type)) { 4 | return handlers[action.type](state, action); 5 | } 6 | return state; 7 | }; 8 | }; 9 | 10 | export const createStore = (reducer) => { 11 | let state = {}; 12 | const listeners = []; 13 | const getState = () => state; 14 | const dispatch = (action) => { 15 | state = reducer(state, action); 16 | listeners.forEach((listener) => listener()); 17 | }; 18 | const subscribe = (listener) => listeners.push(listener); 19 | 20 | return { 21 | getState, 22 | dispatch, 23 | subscribe, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/originReduxStore/originRedux.pcss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | margin: 20px; 3 | font-size: 18px; 4 | .numValue { 5 | color: red; 6 | } 7 | .numBtn { 8 | margin: 10px; 9 | &:first-child { 10 | margin-left: 0; 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/originReduxStore/originReduxStore.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { createStore, applyMiddleware, compose } from 'redux'; 3 | // import { applyMiddleware, compose } from 'redux'; 4 | import { createLogger } from 'redux-logger'; 5 | import { Button, Alert } from 'antd'; 6 | import 'antd/dist/antd.css'; 7 | import reducer from './reducers/index'; 8 | import actions from './actions/index'; 9 | // import { createStore } from '../lib/common'; 10 | import './originRedux.pcss'; 11 | 12 | const logger = createLogger(); 13 | const store = createStore(reducer, compose( 14 | applyMiddleware(logger), 15 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), // eslint-disable-line 16 | )); 17 | 18 | // const store = createStore(reducer); 19 | 20 | const update = () => { 21 | const valueEl = document.getElementsByClassName('numValue'); 22 | valueEl[0].innerHTML = store.getState().changeNumber.number; 23 | 24 | const alertEl = document.getElementsByClassName('alert'); 25 | if (store.getState().toggleAlert.showAlert) { 26 | alertEl[0].style.display = 'block'; 27 | } else { 28 | alertEl[0].style.display = 'none'; 29 | } 30 | }; 31 | 32 | const cancelUpdate = store.subscribe(update); 33 | 34 | export default class Number extends Component { 35 | 36 | addNum = () => { 37 | store.dispatch(actions.number.incrementNum()); 38 | }; 39 | 40 | minusNum = () => { 41 | store.dispatch(actions.number.decrementNum()); 42 | }; 43 | 44 | clearNum = () => { 45 | store.dispatch(actions.number.clearNum()); 46 | }; 47 | 48 | toggleAlert = () => { 49 | store.dispatch(actions.alert.toggleAlert()); 50 | }; 51 | 52 | render() { 53 | return ( 54 |
55 |

origin Redux store

56 | Current Number: 0 57 |
58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 |
69 |
70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/originReduxStore/reducers/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | showAlert: false, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.TOGGLE_ALERT]: (state, action) => { 10 | return { 11 | ...state, 12 | showAlert: !state.showAlert, 13 | }; 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/originReduxStore/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import changeNumber from './number'; 3 | import toggleAlert from './alert'; 4 | 5 | export default combineReducers({ 6 | changeNumber, 7 | toggleAlert, 8 | }); 9 | -------------------------------------------------------------------------------- /src/originReduxStore/reducers/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | number: 0, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.INCREMENT]: (state, action) => { 10 | return { 11 | ...state, 12 | number: state.number + 1, 13 | }; 14 | }, 15 | [constant.DECREMENT]: (state, action) => { 16 | return { 17 | ...state, 18 | number: state.number - 1, 19 | }; 20 | }, 21 | [constant.CLEAR_NUM]: (state, action) => { 22 | return { 23 | ...state, 24 | number: 0, 25 | }; 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/reactRedux/actions/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | toggleAlert: () => ({ 5 | type: constant.TOGGLE_ALERT, 6 | }), 7 | }; 8 | -------------------------------------------------------------------------------- /src/reactRedux/actions/index.js: -------------------------------------------------------------------------------- 1 | import number from './number'; 2 | import alert from './alert'; 3 | 4 | export default { 5 | number, 6 | alert, 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /src/reactRedux/actions/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | incrementNum: () => ({ 5 | type: constant.INCREMENT, 6 | }), 7 | decrementNum: () => ({ 8 | type: constant.DECREMENT, 9 | }), 10 | clearNum: () => ({ 11 | type: constant.CLEAR_NUM, 12 | }), 13 | }; 14 | -------------------------------------------------------------------------------- /src/reactRedux/components/alert/alert.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Button, Alert } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import './alert.pcss'; 6 | 7 | export default class AlertComponent extends Component { 8 | render() { 9 | const { 10 | showAlert, 11 | handleClickAlert, 12 | } = this.props; 13 | 14 | return ( 15 |
16 | 17 | { 18 | showAlert ? : null 19 | } 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/reactRedux/components/alert/alert.pcss: -------------------------------------------------------------------------------- 1 | .numBtn { 2 | margin: 10px; 3 | &:first-child { 4 | margin-left: 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/reactRedux/components/number/number.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Button } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import './number.pcss'; 6 | 7 | export default class Number extends Component { 8 | 9 | render() { 10 | const { 11 | value, 12 | handleClickAdd, 13 | handleClickMinux, 14 | handleClickClear, 15 | } = this.props; 16 | 17 | return ( 18 |
19 | Current Number: {value} 20 |
21 | 22 | 23 | 24 |
25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/reactRedux/components/number/number.pcss: -------------------------------------------------------------------------------- 1 | .numValue { 2 | color: red; 3 | } 4 | 5 | .numBtn { 6 | margin: 10px; 7 | &:first-child { 8 | margin-left: 0; 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/reactRedux/configs/action.js: -------------------------------------------------------------------------------- 1 | // action.type 常量 2 | export const INCREMENT = 'INCREMENT'; 3 | export const DECREMENT = 'DECREMENT'; 4 | export const CLEAR_NUM = 'CLEAR_NUM'; 5 | export const TOGGLE_ALERT = 'TOGGLE_ALERT'; 6 | -------------------------------------------------------------------------------- /src/reactRedux/containers/sample/sample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | // import { bindActionCreators } from '../../lib/common'; 4 | import { render } from 'react-dom'; 5 | import { connect } from 'react-redux'; 6 | // import connect from '../../lib/connect'; 7 | import action from '../../actions/index'; 8 | import NumberComponent from '../../components/number/number'; 9 | import AlertComponent from '../../components/alert/alert'; 10 | import './sample.pcss'; 11 | 12 | class Sample extends Component { 13 | 14 | handleClickAdd = () => { 15 | this.props.incrementNum(); 16 | }; 17 | 18 | handleClickMinux = () => { 19 | this.props.decrementNum(); 20 | }; 21 | 22 | handleClickClear = () => { 23 | this.props.clearNum(); 24 | }; 25 | 26 | handleClickAlert = () => { 27 | this.props.toggleAlert(); 28 | }; 29 | 30 | render() { 31 | const { 32 | number, 33 | showAlert, 34 | } = this.props; 35 | 36 | return ( 37 |
38 |

recat redux

39 | 45 | 49 |
50 | ); 51 | } 52 | } 53 | 54 | const mapStateToProps = (state) => { 55 | return { 56 | number: state.changeNumber.number, 57 | showAlert: state.toggleAlert.showAlert, 58 | }; 59 | }; 60 | 61 | // const mapDispatchToProps = (dispatch) => { 62 | // return { 63 | // incrementNum: () => dispatch(action.number.incrementNum()), 64 | // decrementNum: () => dispatch(action.number.decrementNum()), 65 | // clearNum: () => dispatch(action.number.clearNum()), 66 | // toggleAlert: () => dispatch(action.alert.toggleAlert()), 67 | // }; 68 | // }; 69 | 70 | // const mapDispatchToProps = (dispatch) => { 71 | // return { 72 | // incrementNum: bindActionCreators(action.number.incrementNum, dispatch), 73 | // decrementNum: bindActionCreators(action.number.decrementNum, dispatch), 74 | // clearNum: bindActionCreators(action.number.clearNum, dispatch), 75 | // toggleAlert: bindActionCreators(action.alert.toggleAlert, dispatch), 76 | // }; 77 | // }; 78 | 79 | const mapDispatchToProps = { 80 | incrementNum: action.number.incrementNum, 81 | decrementNum: action.number.decrementNum, 82 | clearNum: action.number.clearNum, 83 | toggleAlert: action.alert.toggleAlert, 84 | }; 85 | 86 | export default connect( 87 | mapStateToProps, 88 | mapDispatchToProps, 89 | )(Sample); 90 | 91 | // const mergeProps = (stateProps, dispatchProps, ownProps) => { 92 | // return { 93 | // ...ownProps, 94 | // ...stateProps, 95 | // incrementNum: dispatchProps.incrementNum, // 只暴露出 incrementNum 96 | // }; 97 | // }; 98 | // 99 | // const mergeProps = (stateProps, dispatchProps, ownProps) => { 100 | // return { 101 | // ...ownProps, 102 | // state: stateProps, 103 | // actions: { 104 | // ...dispatchProps, 105 | // ...ownProps.actions, 106 | // }, 107 | // }; 108 | // }; 109 | // 110 | // export default connect( 111 | // mapStateToProps, 112 | // mapDispatchToProps, 113 | // mergeProps, 114 | // )(Sample); 115 | -------------------------------------------------------------------------------- /src/reactRedux/containers/sample/sample.pcss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | margin: 20px; 3 | font-size: 18px; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/reactRedux/entries/reactRedux.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import { createStore, applyMiddleware, compose } from 'redux'; 5 | import { createLogger } from 'redux-logger'; 6 | import { Provider } from 'react-redux'; 7 | // import Provider from '../lib/Provider'; 8 | import reducer from '../reducers/index'; 9 | import Sample from '../containers/sample/sample'; 10 | 11 | const logger = createLogger(); 12 | const store = createStore(reducer, compose( 13 | applyMiddleware(logger), 14 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), // eslint-disable-line 15 | )); 16 | 17 | render( 18 | 19 | 20 | 21 | 22 | , 23 | document.getElementById('app'), 24 | ); 25 | 26 | if (module.hot) { 27 | module.hot.accept('../containers/sample/sample', () => { 28 | const newDemo = require('../containers/sample/sample').default; 29 | render( 30 | 31 | 32 | {React.createElement(newDemo)} 33 | 34 | , 35 | document.getElementById('app'), 36 | ); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/reactRedux/lib/Provider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class Provider extends Component { 5 | static childContextTypes = { 6 | store: PropTypes.object, 7 | }; 8 | 9 | getChildContext = () => { 10 | return { store: this.props.store }; 11 | }; 12 | 13 | render() { 14 | return (
{this.props.children}
); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/reactRedux/lib/common.js: -------------------------------------------------------------------------------- 1 | export const createReducer = (initialState, handlers) => { 2 | return (state = initialState, action) => { 3 | if (handlers.hasOwnProperty(action.type)) { 4 | return handlers[action.type](state, action); 5 | } 6 | return state; 7 | }; 8 | }; 9 | 10 | export const createStore = (reducer) => { 11 | let state = {}; 12 | const listeners = []; 13 | const getState = () => state; 14 | const dispatch = (action) => { 15 | state = reducer(state, action); 16 | listeners.forEach((listener) => listener()); 17 | }; 18 | const subscribe = (listener) => listeners.push(listener); 19 | 20 | return { 21 | getState, 22 | dispatch, 23 | subscribe, 24 | }; 25 | }; 26 | 27 | export const bindActionCreators = (actionCreators, dispatch) => { 28 | return () => dispatch(actionCreators()); 29 | }; 30 | -------------------------------------------------------------------------------- /src/reactRedux/lib/connect.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { 5 | class Connect extends Component { 6 | static contextTypes = { 7 | store: PropTypes.object, 8 | }; 9 | 10 | constructor() { 11 | super(); 12 | this.state = { finalProps: {} }; 13 | // this.finalDispatchToProps = {}; 14 | } 15 | 16 | componentWillMount() { 17 | const { store } = this.context; 18 | // const tempDispatchToProps = {}; 19 | // if (typeof mapDispatchToProps === 'object') { 20 | // const keys = Object.keys(mapDispatchToProps); 21 | // keys.forEach((key) => { 22 | // const actionCreator = mapDispatchToProps[key]; 23 | // tempDispatchToProps[key] = () => store.dispatch(actionCreator()); 24 | // }); 25 | // this.finalDispatchToProps = () => tempDispatchToProps; 26 | // } else { 27 | // this.finalDispatchToProps = () => mapDispatchToProps(store.dispatch); 28 | // } 29 | this.updateProps(); 30 | store.subscribe(this.updateProps); 31 | } 32 | 33 | updateProps = () => { 34 | const { store } = this.context; 35 | const stateProps = mapStateToProps(store.getState()); 36 | const dispatchProps = mapDispatchToProps(store.dispatch); 37 | // const dispatchProps = this.finalDispatchToProps(); 38 | 39 | const finalProps = { 40 | ...stateProps, 41 | ...dispatchProps, 42 | ...this.props, 43 | }; 44 | 45 | this.setState({ 46 | finalProps, 47 | }); 48 | }; 49 | 50 | render() { 51 | const { finalProps } = this.state; 52 | return (); 53 | } 54 | } 55 | 56 | return Connect; 57 | }; 58 | 59 | export default connect; 60 | -------------------------------------------------------------------------------- /src/reactRedux/reducers/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | showAlert: false, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.TOGGLE_ALERT]: (state, action) => { 10 | return { 11 | ...state, 12 | showAlert: !state.showAlert, 13 | }; 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/reactRedux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import changeNumber from './number'; 3 | import toggleAlert from './alert'; 4 | 5 | export default combineReducers({ 6 | changeNumber, 7 | toggleAlert, 8 | }); 9 | -------------------------------------------------------------------------------- /src/reactRedux/reducers/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | number: 0, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.INCREMENT]: (state, action) => { 10 | return { 11 | ...state, 12 | number: state.number + 1, 13 | }; 14 | }, 15 | [constant.DECREMENT]: (state, action) => { 16 | return { 17 | ...state, 18 | number: state.number - 1, 19 | }; 20 | }, 21 | [constant.CLEAR_NUM]: (state, action) => { 22 | return { 23 | ...state, 24 | number: 0, 25 | }; 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/reactReduxAsync/actions/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | toggleAlert: () => ({ 5 | type: constant.TOGGLE_ALERT, 6 | }), 7 | }; 8 | -------------------------------------------------------------------------------- /src/reactReduxAsync/actions/fetchData.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import * as constant from '../configs/action'; 3 | import { sleep } from '../lib/common'; 4 | 5 | const requestData = () => ({ 6 | type: constant.REQUEST_DATA, 7 | }); 8 | 9 | const receiveData = (data) => ({ 10 | type: constant.RECEIVE_DATA, 11 | data: data.msg, 12 | }); 13 | 14 | const doFetchData = () => async(dispatch) => { 15 | dispatch(requestData()); 16 | await sleep(1000); // Just 4 mock 17 | return fetch('./api/fetchSampleData.json') 18 | .then((response) => response.json()) 19 | .then((json) => dispatch(receiveData(json))); 20 | }; 21 | 22 | const canFetchData = (state) => { 23 | return !state.fetchData.fetching; 24 | }; 25 | 26 | export default { 27 | fetchDataAction: () => (dispatch, getState) => { 28 | if (canFetchData(getState())) { 29 | return dispatch(doFetchData()); 30 | } 31 | return Promise.resolve(); 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/reactReduxAsync/actions/index.js: -------------------------------------------------------------------------------- 1 | import number from './number'; 2 | import alert from './alert'; 3 | import fetchDataAction from './fetchData'; 4 | 5 | export default { 6 | number, 7 | alert, 8 | fetchDataAction, 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /src/reactReduxAsync/actions/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | incrementNum: () => ({ 5 | type: constant.INCREMENT, 6 | }), 7 | decrementNum: () => ({ 8 | type: constant.DECREMENT, 9 | }), 10 | clearNum: () => ({ 11 | type: constant.CLEAR_NUM, 12 | }), 13 | }; 14 | -------------------------------------------------------------------------------- /src/reactReduxAsync/components/alert/alert.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Button, Alert } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import './alert.pcss'; 6 | 7 | export default class AlertComponent extends Component { 8 | render() { 9 | const { 10 | showAlert, 11 | handleClickAlert, 12 | } = this.props; 13 | 14 | return ( 15 |
16 | 17 | { 18 | showAlert ? : null 19 | } 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/reactReduxAsync/components/alert/alert.pcss: -------------------------------------------------------------------------------- 1 | .numBtn { 2 | margin: 10px; 3 | &:first-child { 4 | margin-left: 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/reactReduxAsync/components/fetchData/fetchData.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Button } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import LoadingComponent from '../loading/loading'; 6 | import './fetchData.pcss'; 7 | 8 | export default class FetchDataComponent extends Component { 9 | render() { 10 | const { 11 | showloading, 12 | handleClickFetchData, 13 | } = this.props; 14 | 15 | return ( 16 |
17 | 18 | 19 |
20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/reactReduxAsync/components/fetchData/fetchData.pcss: -------------------------------------------------------------------------------- 1 | .numBtn { 2 | margin: 10px; 3 | &:first-child { 4 | margin-left: 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/reactReduxAsync/components/loading/images/page-loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/src/reactReduxAsync/components/loading/images/page-loading.gif -------------------------------------------------------------------------------- /src/reactReduxAsync/components/loading/loading.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './loading.pcss'; 3 | 4 | export default class Loading extends Component { 5 | render() { 6 | const { 7 | show, 8 | } = this.props; 9 | 10 | return ( 11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/reactReduxAsync/components/loading/loading.pcss: -------------------------------------------------------------------------------- 1 | .loading { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | background-color: rgba(0, 0, 0, 0.3); 6 | background-image: url(./images/page-loading.gif); 7 | background-size: 50px; 8 | background-repeat: no-repeat; 9 | background-position: center; 10 | width: 100%; 11 | height: 100%; 12 | z-index: 11; 13 | } 14 | 15 | .hide { 16 | display: none; 17 | } 18 | -------------------------------------------------------------------------------- /src/reactReduxAsync/components/number/number.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Button } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import './number.pcss'; 6 | 7 | export default class Number extends Component { 8 | 9 | render() { 10 | const { 11 | value, 12 | handleClickAdd, 13 | handleClickMinux, 14 | handleClickClear, 15 | } = this.props; 16 | 17 | return ( 18 |
19 | Current Number: {value} 20 |
21 | 22 | 23 | 24 |
25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/reactReduxAsync/components/number/number.pcss: -------------------------------------------------------------------------------- 1 | .numValue { 2 | color: red; 3 | } 4 | 5 | .numBtn { 6 | margin: 10px; 7 | &:first-child { 8 | margin-left: 0; 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/reactReduxAsync/configs/action.js: -------------------------------------------------------------------------------- 1 | // action.type 常量 2 | export const INCREMENT = 'INCREMENT'; 3 | export const DECREMENT = 'DECREMENT'; 4 | export const CLEAR_NUM = 'CLEAR_NUM'; 5 | export const TOGGLE_ALERT = 'TOGGLE_ALERT'; 6 | export const REQUEST_DATA = 'REQUEST_DATA'; 7 | export const RECEIVE_DATA = 'RECEIVE_DATA'; 8 | -------------------------------------------------------------------------------- /src/reactReduxAsync/containers/sample/sample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { connect } from 'react-redux'; 4 | import action from '../../actions/index'; 5 | import NumberComponent from '../../components/number/number'; 6 | import AlertComponent from '../../components/alert/alert'; 7 | import FetchDataComponent from '../../components/fetchData/fetchData'; 8 | import './sample.pcss'; 9 | 10 | class Sample extends Component { 11 | 12 | handleClickAdd = () => { 13 | this.props.incrementNum(); 14 | }; 15 | 16 | handleClickMinux = () => { 17 | this.props.decrementNum(); 18 | }; 19 | 20 | handleClickClear = () => { 21 | this.props.clearNum(); 22 | }; 23 | 24 | handleClickAlert = () => { 25 | this.props.toggleAlert(); 26 | }; 27 | 28 | handleClickFetchData = () => { 29 | this.props.fetchDataAction(); 30 | }; 31 | 32 | render() { 33 | const { 34 | number, 35 | showAlert, 36 | fetching, 37 | data, 38 | } = this.props; 39 | 40 | return ( 41 |
42 |

recat redux async

43 | 49 | 53 | 57 |

{ data !== null ? data.msg : '' }

58 |
59 | ); 60 | } 61 | } 62 | 63 | export default connect((state) => ({ 64 | number: state.changeNumber.number, 65 | showAlert: state.toggleAlert.showAlert, 66 | fetching: state.fetchData.fetching, 67 | data: state.fetchData.data, 68 | }), { 69 | incrementNum: action.number.incrementNum, 70 | decrementNum: action.number.decrementNum, 71 | clearNum: action.number.clearNum, 72 | toggleAlert: action.alert.toggleAlert, 73 | fetchDataAction: action.fetchDataAction.fetchDataAction, 74 | })(Sample); 75 | -------------------------------------------------------------------------------- /src/reactReduxAsync/containers/sample/sample.pcss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | margin: 20px; 3 | font-size: 18px; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/reactReduxAsync/entries/reactReduxAsync.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import { createStore, applyMiddleware, compose } from 'redux'; 5 | import thunk from 'redux-thunk'; 6 | // import { thunk } from '../lib/common'; 7 | import { createLogger } from 'redux-logger'; 8 | import { Provider } from 'react-redux'; 9 | import reducer from '../reducers/index'; 10 | import Sample from '../containers/sample/sample'; 11 | 12 | const logger = createLogger(); 13 | const store = createStore(reducer, compose( 14 | applyMiddleware(thunk, logger), 15 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), // eslint-disable-line 16 | )); 17 | 18 | render( 19 | 20 | 21 | 22 | 23 | , 24 | document.getElementById('app'), 25 | ); 26 | 27 | if (module.hot) { 28 | module.hot.accept('../containers/sample/sample', () => { 29 | const newDemo = require('../containers/sample/sample').default; 30 | render( 31 | 32 | 33 | {React.createElement(newDemo)} 34 | 35 | , 36 | document.getElementById('app'), 37 | ); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/reactReduxAsync/lib/common.js: -------------------------------------------------------------------------------- 1 | export const createReducer = (initialState, handlers) => { 2 | return (state = initialState, action) => { 3 | if (handlers.hasOwnProperty(action.type)) { 4 | return handlers[action.type](state, action); 5 | } 6 | return state; 7 | }; 8 | }; 9 | 10 | export const createStore = (reducer) => { 11 | let state = {}; 12 | const listeners = []; 13 | const getState = () => state; 14 | const dispatch = (action) => { 15 | state = reducer(state, action); 16 | listeners.forEach((listener) => listener()); 17 | }; 18 | const subscribe = (listener) => listeners.push(listener); 19 | 20 | return { 21 | getState, 22 | dispatch, 23 | subscribe, 24 | }; 25 | }; 26 | 27 | export const sleep = (timeout) => { 28 | return new Promise((resolve) => { 29 | setTimeout(resolve, timeout); 30 | }); 31 | }; 32 | 33 | export const thunk = (store) => (dispatch) => (action) => { 34 | if (typeof action === 'function') { 35 | return action(store.dispatch, store.getState); 36 | } 37 | return dispatch(action); 38 | }; 39 | -------------------------------------------------------------------------------- /src/reactReduxAsync/reducers/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | showAlert: false, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.TOGGLE_ALERT]: (state, action) => { 10 | return { 11 | ...state, 12 | showAlert: !state.showAlert, 13 | }; 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/reactReduxAsync/reducers/fetchData.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | fetching: false, 6 | data: null, 7 | }; 8 | 9 | export default createReducer(initialState, { 10 | [constant.REQUEST_DATA]: (state, action) => { 11 | return { 12 | ...state, 13 | fetching: true, 14 | }; 15 | }, 16 | [constant.RECEIVE_DATA]: (state, action) => { 17 | return { 18 | ...state, 19 | fetching: false, 20 | data: action.data, 21 | }; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/reactReduxAsync/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import changeNumber from './number'; 3 | import toggleAlert from './alert'; 4 | import fetchData from './fetchData'; 5 | 6 | export default combineReducers({ 7 | changeNumber, 8 | toggleAlert, 9 | fetchData, 10 | }); 11 | -------------------------------------------------------------------------------- /src/reactReduxAsync/reducers/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | number: 0, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.INCREMENT]: (state, action) => { 10 | return { 11 | ...state, 12 | number: state.number + 1, 13 | }; 14 | }, 15 | [constant.DECREMENT]: (state, action) => { 16 | return { 17 | ...state, 18 | number: state.number - 1, 19 | }; 20 | }, 21 | [constant.CLEAR_NUM]: (state, action) => { 22 | return { 23 | ...state, 24 | number: 0, 25 | }; 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/actions/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | toggleAlert: () => ({ 5 | type: constant.TOGGLE_ALERT, 6 | }), 7 | }; 8 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/actions/index.js: -------------------------------------------------------------------------------- 1 | import number from './number'; 2 | import alert from './alert'; 3 | 4 | export default { 5 | number, 6 | alert, 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/actions/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | incrementNum: () => ({ 5 | type: constant.INCREMENT, 6 | }), 7 | decrementNum: () => ({ 8 | type: constant.DECREMENT, 9 | }), 10 | clearNum: () => ({ 11 | type: constant.CLEAR_NUM, 12 | }), 13 | }; 14 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/components/alert/alert.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Button, Alert } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import './alert.pcss'; 6 | 7 | export default class AlertComponent extends Component { 8 | render() { 9 | const { 10 | showAlert, 11 | handleClickAlert, 12 | } = this.props; 13 | 14 | return ( 15 |
16 | 17 | { 18 | showAlert ? : null 19 | } 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/components/alert/alert.pcss: -------------------------------------------------------------------------------- 1 | .numBtn { 2 | margin: 10px; 3 | &:first-child { 4 | margin-left: 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/components/number/number.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Button } from 'antd'; 4 | import 'antd/dist/antd.css'; 5 | import './number.pcss'; 6 | 7 | export default class Number extends Component { 8 | 9 | render() { 10 | const { 11 | value, 12 | handleClickAdd, 13 | handleClickMinux, 14 | handleClickClear, 15 | } = this.props; 16 | 17 | return ( 18 |
19 | Current Number: {value} 20 |
21 | 22 | 23 | 24 |
25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/components/number/number.pcss: -------------------------------------------------------------------------------- 1 | .numValue { 2 | color: red; 3 | } 4 | 5 | .numBtn { 6 | margin: 10px; 7 | &:first-child { 8 | margin-left: 0; 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/configs/action.js: -------------------------------------------------------------------------------- 1 | // action.type 常量 2 | export const INCREMENT = 'INCREMENT'; 3 | export const DECREMENT = 'DECREMENT'; 4 | export const CLEAR_NUM = 'CLEAR_NUM'; 5 | export const TOGGLE_ALERT = 'TOGGLE_ALERT'; 6 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/containers/sample/sample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { render } from 'react-dom'; 4 | import { connect } from 'react-redux'; 5 | import action from '../../actions/index'; 6 | import NumberComponent from '../../components/number/number'; 7 | import AlertComponent from '../../components/alert/alert'; 8 | import './sample.pcss'; 9 | 10 | class Sample extends Component { 11 | 12 | // static contextTypes = { 13 | // store: PropTypes.object, 14 | // }; 15 | 16 | handleClickAdd = () => { 17 | // console.log('prevState: ', this.context.store.getState()); 18 | // console.log('action: ', action.number.incrementNum()); 19 | this.props.incrementNum(); 20 | // console.log('currentState: ', this.context.store.getState()); 21 | }; 22 | 23 | handleClickMinux = () => { 24 | this.props.decrementNum(); 25 | }; 26 | 27 | handleClickClear = () => { 28 | this.props.clearNum(); 29 | }; 30 | 31 | handleClickAlert = () => { 32 | this.props.toggleAlert(); 33 | }; 34 | 35 | render() { 36 | const { 37 | number, 38 | showAlert, 39 | } = this.props; 40 | 41 | return ( 42 |
43 |

recat redux middleware

44 | 50 | 54 |
55 | ); 56 | } 57 | } 58 | 59 | const mapStateToProps = (state) => { 60 | return { 61 | number: state.changeNumber.number, 62 | showAlert: state.toggleAlert.showAlert, 63 | }; 64 | }; 65 | 66 | const mapDispatchToProps = { 67 | incrementNum: action.number.incrementNum, 68 | decrementNum: action.number.decrementNum, 69 | clearNum: action.number.clearNum, 70 | toggleAlert: action.alert.toggleAlert, 71 | }; 72 | 73 | export default connect( 74 | mapStateToProps, 75 | mapDispatchToProps, 76 | )(Sample); 77 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/containers/sample/sample.pcss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | margin: 20px; 3 | font-size: 18px; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/entries/reactReduxMiddleware.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import { createStore } from 'redux'; 5 | import { Provider } from 'react-redux'; 6 | import reducer from '../reducers/index'; 7 | import Sample from '../containers/sample/sample'; 8 | import { 9 | middleware1, 10 | middleware2, 11 | loggerAction, 12 | loggerState, 13 | applyMiddleware, 14 | applyMiddlewarePlus, 15 | compose, 16 | } from '../lib/middleware'; 17 | 18 | // Step0 - 手写 console.log 19 | // const store = createStore(reducer); 20 | 21 | // Step1 - 自定义中间件 22 | // const store = createStore(reducer); 23 | // store.dispatch = middleware1(store.dispatch); 24 | // store.dispatch = middleware2(store.dispatch); 25 | 26 | // Step2 - 用 applyMiddleware 将中间件更优雅地链接起来 27 | // let store = createStore(reducer); 28 | // store = applyMiddleware(store, [loggerAction, loggerState]); 29 | 30 | // Step3 - 更优雅的 applyMiddleware 31 | // const store = applyMiddlewarePlus(loggerAction, loggerState)(createStore)(reducer); 32 | 33 | // Step4 - 增加 compose 绑定插件功能 34 | const store = createStore(reducer, compose( 35 | applyMiddlewarePlus(loggerAction, loggerState), 36 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), // eslint-disable-line 37 | )); 38 | 39 | render( 40 | 41 | 42 | 43 | 44 | , 45 | document.getElementById('app'), 46 | ); 47 | 48 | if (module.hot) { 49 | module.hot.accept('../containers/sample/sample', () => { 50 | const newDemo = require('../containers/sample/sample').default; 51 | render( 52 | 53 | 54 | {React.createElement(newDemo)} 55 | 56 | , 57 | document.getElementById('app'), 58 | ); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/lib/common.js: -------------------------------------------------------------------------------- 1 | export const createReducer = (initialState, handlers) => { 2 | return (state = initialState, action) => { 3 | if (handlers.hasOwnProperty(action.type)) { 4 | return handlers[action.type](state, action); 5 | } 6 | return state; 7 | }; 8 | }; 9 | 10 | export const createStore = (reducer) => { 11 | let state = {}; 12 | const listeners = []; 13 | const getState = () => state; 14 | const dispatch = (action) => { 15 | state = reducer(state, action); 16 | listeners.forEach((listener) => listener()); 17 | }; 18 | const subscribe = (listener) => listeners.push(listener); 19 | 20 | return { 21 | getState, 22 | dispatch, 23 | subscribe, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/lib/middleware.js: -------------------------------------------------------------------------------- 1 | // Step1 自定义中间件 2 | export const middleware1 = (dispatch) => { 3 | return (action) => { 4 | console.log('first middleware'); 5 | dispatch(action); 6 | }; 7 | }; 8 | 9 | export const middleware2 = (dispatch) => { 10 | return (action) => { 11 | console.log('second middleware'); 12 | dispatch(action); 13 | }; 14 | }; 15 | 16 | // Step2 - 用 applyMiddleware 将中间件更优雅地链接起来 17 | // // 只打印出 Action 18 | // export const loggerAction = (store) => { 19 | // return (dispatch) => { 20 | // return (action) => { 21 | // console.log('action: ', action); 22 | // dispatch(action); 23 | // }; 24 | // }; 25 | // }; 26 | // 27 | // // 只打印出 更新后的state 28 | // export const loggerState = (store) => { 29 | // return (dispatch) => { 30 | // return (action) => { 31 | // console.log('current state: ', store.getState()); 32 | // dispatch(action); 33 | // console.log('next state', store.getState()); 34 | // }; 35 | // }; 36 | // }; 37 | 38 | /* es6 写法 */ 39 | // 只打印出 Action 40 | export const loggerAction = (store) => (dispatch) => (action) => { 41 | console.log('action: ', action); 42 | dispatch(action); 43 | }; 44 | 45 | // 只打印出 更新后的state 46 | export const loggerState = (store) => (dispatch) => (action) => { 47 | console.log('current state: ', store.getState()); 48 | dispatch(action); 49 | console.log('next state', store.getState()); 50 | }; 51 | 52 | export const applyMiddleware = (store, middlewares) => { 53 | let dispatch = store.dispatch; 54 | middlewares.forEach((middleware) => { 55 | dispatch = middleware(store)(dispatch); 56 | }); 57 | 58 | return { 59 | ...store, 60 | dispatch, 61 | }; 62 | }; 63 | 64 | // Step3 - 更优雅的 applyMiddleware 65 | export const applyMiddlewarePlus = (...middlewares) => (createStore) => (reducer) => { 66 | const store = createStore(reducer); 67 | 68 | let dispatch = store.dispatch; 69 | middlewares.forEach((middleware) => { 70 | dispatch = middleware(store)(dispatch); 71 | }); 72 | 73 | return { 74 | ...store, 75 | dispatch, 76 | }; 77 | }; 78 | 79 | // Step4 - 增加 compose 绑定插件功能 80 | export const compose = (...funcs) => { 81 | return funcs.reduce((a, b) => (...args) => a(b(...args))); 82 | }; 83 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/reducers/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | showAlert: false, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.TOGGLE_ALERT]: (state, action) => { 10 | return { 11 | ...state, 12 | showAlert: !state.showAlert, 13 | }; 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import changeNumber from './number'; 3 | import toggleAlert from './alert'; 4 | 5 | export default combineReducers({ 6 | changeNumber, 7 | toggleAlert, 8 | }); 9 | -------------------------------------------------------------------------------- /src/reactReduxMiddleware/reducers/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | number: 0, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.INCREMENT]: (state, action) => { 10 | return { 11 | ...state, 12 | number: state.number + 1, 13 | }; 14 | }, 15 | [constant.DECREMENT]: (state, action) => { 16 | return { 17 | ...state, 18 | number: state.number - 1, 19 | }; 20 | }, 21 | [constant.CLEAR_NUM]: (state, action) => { 22 | return { 23 | ...state, 24 | number: 0, 25 | }; 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/reactReduxSaga/actions/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | toggleAlert: () => ({ 5 | type: constant.TOGGLE_ALERT, 6 | }), 7 | }; 8 | -------------------------------------------------------------------------------- /src/reactReduxSaga/actions/fetchData.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | fetchDataAction: (data) => ({ 5 | type: constant.FETCH_DATA, 6 | data, 7 | }), 8 | }; 9 | -------------------------------------------------------------------------------- /src/reactReduxSaga/actions/index.js: -------------------------------------------------------------------------------- 1 | import number from './number'; 2 | import alert from './alert'; 3 | import fetchDataAction from './fetchData'; 4 | 5 | export default { 6 | number, 7 | alert, 8 | fetchDataAction, 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /src/reactReduxSaga/actions/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | 3 | export default { 4 | incrementNum: () => ({ 5 | type: constant.INCREMENT, 6 | }), 7 | decrementNum: () => ({ 8 | type: constant.DECREMENT, 9 | }), 10 | clearNum: () => ({ 11 | type: constant.CLEAR_NUM, 12 | }), 13 | }; 14 | -------------------------------------------------------------------------------- /src/reactReduxSaga/api/index.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | export const fetchDataApi = async(data) => { 4 | return fetch(`./api/fetchSampleData.json?num=${data}`) 5 | .then((response) => response.json()); 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /src/reactReduxSaga/components/alert/alert.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Alert } from 'antd'; 3 | import 'antd/dist/antd.css'; 4 | import './alert.pcss'; 5 | 6 | export default class AlertComponent extends Component { 7 | render() { 8 | const { 9 | showAlert, 10 | handleClickAlert, 11 | } = this.props; 12 | 13 | return ( 14 |
15 | 16 | { 17 | showAlert ? : null 18 | } 19 |
20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/reactReduxSaga/components/alert/alert.pcss: -------------------------------------------------------------------------------- 1 | .numBtn { 2 | margin: 10px; 3 | &:first-child { 4 | margin-left: 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/reactReduxSaga/components/fetchData/fetchData.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'antd'; 3 | import 'antd/dist/antd.css'; 4 | import LoadingComponent from '../loading/loading'; 5 | import './fetchData.pcss'; 6 | 7 | export default class FetchDataComponent extends Component { 8 | render() { 9 | const { 10 | showloading, 11 | handleClickFetchData, 12 | } = this.props; 13 | 14 | return ( 15 |
16 | 17 | 18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/reactReduxSaga/components/fetchData/fetchData.pcss: -------------------------------------------------------------------------------- 1 | .numBtn { 2 | margin: 10px; 3 | &:first-child { 4 | margin-left: 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/reactReduxSaga/components/loading/images/page-loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/src/reactReduxSaga/components/loading/images/page-loading.gif -------------------------------------------------------------------------------- /src/reactReduxSaga/components/loading/loading.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './loading.pcss'; 3 | 4 | export default class Loading extends Component { 5 | render() { 6 | const { 7 | show, 8 | } = this.props; 9 | 10 | return ( 11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/reactReduxSaga/components/loading/loading.pcss: -------------------------------------------------------------------------------- 1 | .loading { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | background-color: rgba(0, 0, 0, 0.3); 6 | background-image: url(./images/page-loading.gif); 7 | background-size: 50px; 8 | background-repeat: no-repeat; 9 | background-position: center; 10 | width: 100%; 11 | height: 100%; 12 | z-index: 11; 13 | } 14 | 15 | .hide { 16 | display: none; 17 | } 18 | -------------------------------------------------------------------------------- /src/reactReduxSaga/components/number/number.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'antd'; 3 | import 'antd/dist/antd.css'; 4 | import './number.pcss'; 5 | 6 | export default class Number extends Component { 7 | 8 | render() { 9 | const { 10 | value, 11 | handleClickAdd, 12 | handleClickMinux, 13 | handleClickClear, 14 | } = this.props; 15 | 16 | return ( 17 |
18 | Current Number: {value} 19 |
20 | 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/reactReduxSaga/components/number/number.pcss: -------------------------------------------------------------------------------- 1 | .numValue { 2 | color: red; 3 | } 4 | 5 | .numBtn { 6 | margin: 10px; 7 | &:first-child { 8 | margin-left: 0; 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/reactReduxSaga/configs/action.js: -------------------------------------------------------------------------------- 1 | // action.type 常量 2 | export const INCREMENT = 'INCREMENT'; 3 | export const DECREMENT = 'DECREMENT'; 4 | export const CLEAR_NUM = 'CLEAR_NUM'; 5 | export const TOGGLE_ALERT = 'TOGGLE_ALERT'; 6 | export const REQUEST_DATA = 'REQUEST_DATA'; 7 | export const RECEIVE_DATA = 'RECEIVE_DATA'; 8 | export const FETCH_DATA = 'FETCH_DATA'; 9 | export const FETCH_DATA_FAILED = 'FETCH_DATA_FAILED'; 10 | export const TOGGLE_ALERT_ASYNC = 'TOGGLE_ALERT_ASYNC'; 11 | -------------------------------------------------------------------------------- /src/reactReduxSaga/containers/sample/sample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import action from '../../actions/index'; 4 | import NumberComponent from '../../components/number/number'; 5 | import AlertComponent from '../../components/alert/alert'; 6 | import FetchDataComponent from '../../components/fetchData/fetchData'; 7 | import './sample.pcss'; 8 | 9 | class Sample extends Component { 10 | 11 | handleClickAdd = () => { 12 | this.props.incrementNum(); 13 | }; 14 | 15 | handleClickMinux = () => { 16 | this.props.decrementNum(); 17 | }; 18 | 19 | handleClickClear = () => { 20 | this.props.clearNum(); 21 | }; 22 | 23 | handleClickAlert = () => { 24 | this.props.toggleAlert(); 25 | }; 26 | 27 | handleClickFetchData = () => { 28 | this.props.fetchDataAction(1); 29 | }; 30 | 31 | render() { 32 | const { 33 | number, 34 | showAlert, 35 | fetching, 36 | data, 37 | } = this.props; 38 | 39 | return ( 40 |
41 |

recat redux saga

42 | 48 | 52 | 56 |

{ data !== null ? data.msg : '' }

57 |
58 | ); 59 | } 60 | } 61 | 62 | export default connect((state) => ({ 63 | number: state.changeNumber.number, 64 | showAlert: state.toggleAlert.showAlert, 65 | fetching: state.fetchData.fetching, 66 | data: state.fetchData.data, 67 | }), { 68 | incrementNum: action.number.incrementNum, 69 | decrementNum: action.number.decrementNum, 70 | clearNum: action.number.clearNum, 71 | toggleAlert: action.alert.toggleAlert, 72 | fetchDataAction: action.fetchDataAction.fetchDataAction, 73 | })(Sample); 74 | -------------------------------------------------------------------------------- /src/reactReduxSaga/containers/sample/sample.pcss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | margin: 20px; 3 | font-size: 18px; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/reactReduxSaga/entries/reactReduxSaga.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import { createStore, applyMiddleware, compose } from 'redux'; 5 | // import { createLogger } from 'redux-logger'; 6 | import createSagaMiddleware from 'redux-saga'; 7 | import { Provider } from 'react-redux'; 8 | import reducer from '../reducers/index'; 9 | import Sample from '../containers/sample/sample'; 10 | import rootSaga from '../sagas'; 11 | 12 | // const logger = createLogger(); 13 | const saga = createSagaMiddleware(); 14 | 15 | const store = createStore(reducer, compose( 16 | applyMiddleware(saga), 17 | window.devToolsExtension ? window.devToolsExtension() : (f) => f, 18 | )); 19 | 20 | saga.run(rootSaga); 21 | 22 | render( 23 | 24 | 25 | 26 | 27 | , 28 | document.getElementById('app'), 29 | ); 30 | 31 | if (module.hot) { 32 | module.hot.accept('../containers/sample/sample', () => { 33 | const newDemo = require('../containers/sample/sample').default; 34 | render( 35 | 36 | 37 | {React.createElement(newDemo)} 38 | 39 | , 40 | document.getElementById('app'), 41 | ); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/reactReduxSaga/io.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/src/reactReduxSaga/io.js -------------------------------------------------------------------------------- /src/reactReduxSaga/lib/common.js: -------------------------------------------------------------------------------- 1 | export const createReducer = (initialState, handlers) => { 2 | return (state = initialState, action) => { 3 | if (handlers.hasOwnProperty(action.type)) { 4 | return handlers[action.type](state, action); 5 | } 6 | return state; 7 | }; 8 | }; 9 | 10 | export const sleep = (timeout) => { 11 | return new Promise((resolve) => { 12 | setTimeout(resolve, timeout); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/reactReduxSaga/proc.js: -------------------------------------------------------------------------------- 1 | import { is } from './utils' 2 | import { as, matcher } from './io' 3 | 4 | export const NOT_ITERATOR_ERROR = "proc argument must be an iterator" 5 | 6 | export default function proc(iterator, subscribe=()=>()=>{}, dispatch=()=>{}) { 7 | 8 | if(!is.iterator(iterator)) 9 | throw new Error(NOT_ITERATOR_ERROR) 10 | 11 | let deferredInput, deferredEnd 12 | const canThrow = is.throw(iterator) 13 | 14 | const endP = new Promise((resolve, reject) => deferredEnd = {resolve, reject}) 15 | 16 | const unsubscribe = subscribe(input => { 17 | if(deferredInput && deferredInput.match(input)) 18 | deferredInput.resolve(input) 19 | }) 20 | 21 | next() 22 | return endP 23 | 24 | function next(arg, isError) { 25 | deferredInput = null 26 | try { 27 | if(isError && !canThrow) 28 | throw arg 29 | const result = isError ? iterator.throw(arg) : iterator.next(arg) 30 | 31 | if(!result.done) 32 | runEffect(result.value).then(next, err => next(err, true)) 33 | else { 34 | unsubscribe() 35 | deferredEnd.resolve(result.value) 36 | } 37 | } catch(err) { 38 | unsubscribe() 39 | deferredEnd.reject(err) 40 | } 41 | } 42 | 43 | function runEffect(effect) { 44 | let data 45 | return ( 46 | is.promise(effect) ? effect 47 | : is.array(effect) ? Promise.all(effect.map(runEffect)) 48 | : is.iterator(effect) ? proc(effect, subscribe, dispatch) 49 | 50 | : (data = as.take(effect)) ? runTakeEffect(data) 51 | : (data = as.put(effect)) ? Promise.resolve(dispatch(data)) 52 | : (data = as.race(effect)) ? runRaceEffect(data) 53 | : (data = as.call(effect)) ? data.fn(...data.args) 54 | : (data = as.cps(effect)) ? runCPSEffect(data) 55 | 56 | : /* resolve anything else */ Promise.resolve(effect) 57 | ) 58 | } 59 | 60 | function runTakeEffect(pattern) { 61 | return new Promise(resolve => { 62 | deferredInput = { resolve, match : matcher(pattern), pattern } 63 | }) 64 | } 65 | 66 | function runCPSEffect(cps) { 67 | return new Promise((resolve, reject) => { 68 | cps.fn(...[ 69 | ...cps.args, 70 | (err, res) => is.undef(err) ? resolve(res) : reject(err) 71 | ]) 72 | }) 73 | } 74 | 75 | function runRaceEffect(effects) { 76 | return Promise.race( 77 | Object.keys(effects) 78 | .map(key => 79 | runEffect(effects[key]) 80 | .then( result => ({ [key]: result }), 81 | error => Promise.reject({ [key]: error })) 82 | ) 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/reactReduxSaga/reducers/alert.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | showAlert: false, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.TOGGLE_ALERT_ASYNC]: (state, action) => { 10 | return { 11 | ...state, 12 | showAlert: !state.showAlert, 13 | }; 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/reactReduxSaga/reducers/fetchData.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | fetching: false, 6 | data: null, 7 | }; 8 | 9 | export default createReducer(initialState, { 10 | [constant.REQUEST_DATA]: (state, action) => { 11 | return { 12 | ...state, 13 | fetching: true, 14 | }; 15 | }, 16 | [constant.RECEIVE_DATA]: (state, action) => { 17 | return { 18 | ...state, 19 | fetching: false, 20 | data: action.data.msg, 21 | }; 22 | }, 23 | [constant.FETCH_DATA_FAILED]: (state, action) => { 24 | return { 25 | ...state, 26 | fetching: false, 27 | data: { msg: action.errMsg }, 28 | }; 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /src/reactReduxSaga/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import changeNumber from './number'; 3 | import toggleAlert from './alert'; 4 | import fetchData from './fetchData'; 5 | 6 | export default combineReducers({ 7 | changeNumber, 8 | toggleAlert, 9 | fetchData, 10 | }); 11 | -------------------------------------------------------------------------------- /src/reactReduxSaga/reducers/number.js: -------------------------------------------------------------------------------- 1 | import * as constant from '../configs/action'; 2 | import { createReducer } from '../lib/common'; 3 | 4 | const initialState = { 5 | number: 0, 6 | }; 7 | 8 | export default createReducer(initialState, { 9 | [constant.INCREMENT]: (state, action) => { 10 | return { 11 | ...state, 12 | number: state.number + 1, 13 | }; 14 | }, 15 | [constant.DECREMENT]: (state, action) => { 16 | return { 17 | ...state, 18 | number: state.number - 1, 19 | }; 20 | }, 21 | [constant.CLEAR_NUM]: (state, action) => { 22 | return { 23 | ...state, 24 | number: 0, 25 | }; 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/reactReduxSaga/saga-middleware-backup.js: -------------------------------------------------------------------------------- 1 | function isGenerator(fn) { 2 | return fn.constructor.name === 'GeneratorFunction'; 3 | } 4 | 5 | export default function sagaMiddleware(saga) { 6 | if (!isGenerator(saga)) { 7 | throw new Error('saga必须是个generator方法'); 8 | } 9 | 10 | return ({ getState, dispatch }) => (next) => (action) => { 11 | const result = next(action); 12 | const gen = saga(getState, action); 13 | 14 | function iterate(generator) { 15 | function step(arg, isError) { 16 | const { value: effect, done } = isError ? generator.throw(arg) : generator.next(arg); 17 | // retreives next action/effect 18 | if (!done) { 19 | let response; 20 | if (typeof effect === 'function') { 21 | response = effect(); 22 | } else if (Array.isArray(effect) && typeof effect[0] === 'function') { 23 | response = effect[0](...effect.slice(1)); 24 | } else { 25 | response = dispatch(effect); 26 | } 27 | 28 | Promise.resolve(response).then(step, (err) => step(err, true)); 29 | } 30 | } 31 | step(); 32 | } 33 | 34 | iterate(gen); 35 | 36 | return result; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/reactReduxSaga/saga-middleware.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackZhangXL/react-redux-demo/81bfd927e4f786e6cbaed06b2e9306a5070ee3b9/src/reactReduxSaga/saga-middleware.js -------------------------------------------------------------------------------- /src/reactReduxSaga/sagas-backup.js: -------------------------------------------------------------------------------- 1 | import { call, put, takeEvery, takeLatest, all, select, take } from 'redux-saga/effects'; 2 | import { fetchDataApi } from './api/index'; 3 | import * as constant from './configs/action'; 4 | import { sleep } from './lib/common'; 5 | 6 | function* alertAsync(action) { 7 | yield sleep(1000); 8 | yield put({ type: constant.TOGGLE_ALERT_ASYNC }); 9 | } 10 | 11 | function* fetchData(action) { // 参数是 actionCreator 创建的 action 12 | try { 13 | yield put({ type: constant.REQUEST_DATA }); 14 | yield sleep(1000); 15 | const data = yield call(fetchDataApi, action.data); 16 | yield put({ type: constant.RECEIVE_DATA, data }); 17 | } catch (e) { 18 | yield put({ type: constant.FETCH_DATA_FAILED, errMsg: e.message }); 19 | } 20 | } 21 | 22 | export function* watchAlertAsync() { 23 | yield takeEvery(constant.TOGGLE_ALERT, alertAsync); 24 | } 25 | 26 | export function* watchFetchData() { 27 | yield takeEvery(constant.FETCH_DATA, fetchData); 28 | } 29 | 30 | function* watchAndLog() { 31 | while (true) { 32 | const action = yield take('*'); 33 | const state = yield select(); 34 | 35 | console.log('action', action); 36 | console.log('state after', state); 37 | } 38 | // 和上面是等价的 39 | // yield takeEvery('*', function* logger(action) { 40 | // const state = yield select(); 41 | // 42 | // console.log('action', action); 43 | // console.log('state after', state); 44 | // }); 45 | } 46 | 47 | export default function* rootSaga() { 48 | yield all([ 49 | watchAndLog(), 50 | watchAlertAsync(), 51 | watchFetchData(), 52 | ]); 53 | } 54 | -------------------------------------------------------------------------------- /src/reactReduxSaga/sagas.js: -------------------------------------------------------------------------------- 1 | import { call, put, takeEvery, takeLatest, all, select, take } from 'redux-saga/effects'; 2 | import { fetchDataApi } from './api/index'; 3 | import * as constant from './configs/action'; 4 | import { sleep } from './lib/common'; 5 | 6 | function* alertAsync(action) { 7 | yield sleep(1000); 8 | yield put({ type: constant.TOGGLE_ALERT_ASYNC }); 9 | } 10 | 11 | function* fetchData(action) { // 参数是 actionCreator 创建的 action 12 | try { 13 | yield put({ type: constant.REQUEST_DATA }); 14 | yield sleep(1000); 15 | const data = yield call(fetchDataApi, action.data); 16 | yield put({ type: constant.RECEIVE_DATA, data }); 17 | } catch (e) { 18 | yield put({ type: constant.FETCH_DATA_FAILED, errMsg: e.message }); 19 | } 20 | } 21 | 22 | export function* watchAlertAsync() { 23 | yield takeEvery(constant.TOGGLE_ALERT, alertAsync); 24 | } 25 | 26 | export function* watchFetchData() { 27 | yield takeEvery(constant.FETCH_DATA, fetchData); 28 | } 29 | 30 | function* watchAndLog() { 31 | while (true) { 32 | const action = yield take('*'); 33 | const state = yield select(); 34 | 35 | console.log('action', action); 36 | console.log('state after', state); 37 | } 38 | // 和上面是等价的 39 | // yield takeEvery('*', function* logger(action) { 40 | // const state = yield select(); 41 | // 42 | // console.log('action', action); 43 | // console.log('state after', state); 44 | // }); 45 | } 46 | 47 | export default function* rootSaga() { 48 | yield all([ 49 | watchAndLog(), 50 | watchAlertAsync(), 51 | watchFetchData(), 52 | ]); 53 | } 54 | -------------------------------------------------------------------------------- /src/reactReduxSaga/utils.js: -------------------------------------------------------------------------------- 1 | 2 | export const kTrue = () => true 3 | export const noop = () => {} 4 | 5 | export const is = { 6 | undef : v => v === null || v === undefined, 7 | func : f => typeof f === 'function', 8 | array : Array.isArray, 9 | promise : p => p && typeof p.then === 'function', 10 | generator : g => g.constructor.name === 'GeneratorFunction', 11 | iterator : it => it && typeof it.next === 'function', 12 | throw : it => it && typeof it.throw === 'function' 13 | } 14 | 15 | export function remove(array, item) { 16 | const index = array.indexOf(item) 17 | if(index >= 0) 18 | array.splice(index, 1) 19 | } 20 | -------------------------------------------------------------------------------- /template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux 7 | 8 | 9 |
10 | 11 | 12 | --------------------------------------------------------------------------------