最近學習react redux,先前看過了幾本書和一些博客之類的,感受還不錯,好比《深刻淺出react和redux》,《React全棧++Redux+Flux+webpack+Babel整合開發》,《React與Redux開發實例精解》, 我的以爲《深刻淺出react和redux》這本說講的面比較全, 可是 不少都是走馬觀花, 不怎麼深刻。這裏簡單記錄一個redux 的demo, 主要方便之後本身查看,首先運行界面以下:html
項目結構以下:node
這裏咱們一共有2個組件,都在components文件夾下面,Picker.js實現以下:react
import React from 'react'; import PropTypes from 'prop-types'; function Picker({value, onChange, options}) { return( <span> <h1>{value}</h1> <select onChange={e=>onChange(e.target.value)} value={value}> {options.map(o=> <option value={o} key={o}>{o}</option> )} </select> </span> ); } Picker.propTypes = { options:PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, value:PropTypes.string.isRequired, onChange:PropTypes.func.isRequired, }; export default Picker;這裏的onChange事件是傳遞進來的,最終就是要觸發一個action,Posts.js的實現以下:
import React from 'react'; import PropTypes from 'prop-types'; function Posts ({posts}){ return( <ul> { posts.map((p,i)=> <li key={i}>{p.title}</li> ) } </ul> ); } Posts.propTypes = { posts:PropTypes.array.isRequired, }; export default Posts;
import fetch from 'isomorphic-fetch'; export const REQUEST_POSTS = 'REQUEST_POSTS'; export const RECEIVE_POSTS = 'RECEIVE_POSTS'; export const SELECT_REDDIT = 'SELECT_REDDIT'; export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT'; export function selectReddit(reddit){ return{ type:SELECT_REDDIT, reddit, }; } export function invalidateReddit(reddit){ return { type:INVALIDATE_REDDIT, reddit, }; } export function requestPosts(reddit){ return { type:REQUEST_POSTS, reddit, }; } export function receivePosts(reddit,json){ return{ type:RECEIVE_POSTS, reddit, posts:json.data.children.map(x=>x.data), receivedAt:Date.now(), }; } function fetchPosts(reddit){ return dispatch=>{ dispatch(requestPosts); return fetch(`https://www.reddit.com/r/${reddit}.json`) .then(r=>r.json()) .then(j=>dispatch(receivePosts(reddit,j))); } } function shouldFetchPosts(state,reddit){ const posts = state.postsByReddit[reddit]; if(!posts){ return true; } if(posts.isFetching){ return false; } return posts.didInvalidate; } export function fetchPostsIfNeeded(reddit){ return (dispatch,getState)=>{ if(shouldFetchPosts(getState(),reddit)){ return dispatch(fetchPosts(reddit)); } return null; }; }
主要是暴露出selectReddit,invalidateReddit,requestPosts,receivePosts和fetchPostsIfNeeded幾個action,而fetchPostsIfNeeded纔是主要的,首先調用shouldFetchPosts方法來檢查是否須要獲取數據, 若是是的話就調用fetchPosts方法,而fetchPosts方法返回的是一個function,這裏個人項目使用了redux-thunk, 看看redux-thunk的實現以下:webpack
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
因此在fetchPostsIfNeeded中的dispatch(fetchPosts(reddit)) 最終會進入到redux-thunk裏面,fetchPosts(reddit)返回的是一個function以下,因此這裏會進入這個action裏面,也就是 return action(dispatch, getState, extraArgument);web
function fetchPosts(reddit){ return dispatch=>{ dispatch(requestPosts); return fetch(`https://www.reddit.com/r/${reddit}.json`) .then(r=>r.json()) .then(j=>dispatch(receivePosts(reddit,j))); } }
因此在fetchPosts裏面的dispatch參數就是redux-thunk裏面return action(dispatch, getState, extraArgument) 的dispatch。express
在這裏的function裏面, 通常發起http請求前有一個 狀態改變(dispatch(requestPosts);), http請求成功後有一個 狀態改變(dispatch(receivePosts(reddit,j))),失敗也會有狀態改變(這裏忽律失敗的case)json
接下來看看containers\App.jsredux
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { selectReddit, fetchPostsIfNeeded, invalidateReddit } from '../actions'; import Picker from '../components/Picker'; import Posts from '../components/Posts'; class App extends Component{ constructor(props){ super(props); this.handleChange=this.handleChange.bind(this); this.handleRefreshClick=this.handleRefreshClick.bind(this); } componentDidMount(){ console.log('執行componentDidMount'); const { dispatch, selectedReddit } = this.props; dispatch(fetchPostsIfNeeded(selectedReddit)); } componentWillReceiveProps(nextProps){ console.log('執行componentWillReceiveProps', nextProps); if(nextProps.selectedReddit !==this.props.selectedReddit) { const { dispatch, selectedReddit } = this.props; dispatch(fetchPostsIfNeeded(selectedReddit)); } } handleChange(nextReddit){ this.props.dispatch(selectReddit(nextReddit)); } handleRefreshClick(e){ e.preventDefault(); const {dispatch, selectedReddit } = this.props; dispatch(invalidateReddit(selectedReddit)); dispatch(fetchPostsIfNeeded(selectedReddit)); } render(){ const { selectedReddit, posts, isFetching, lastUpdated } = this.props; const isEmpty = posts.length === 0; const message = isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>; return( <div> <Picker value={selectedReddit} onChange={this.handleChange} options={['reactjs', 'frontend']} /> <p> { lastUpdated && <span>Last updated at {new Date(lastUpdated).toLocaleDateString()}.</span>} {!isFetching && <a href="#" onClick={this.handleRefreshClick}>Refresh</a>} </p> {isEmpty?message:<div style={{opacity:isFetching?0.5:1}}><Posts posts={posts}/></div>} </div> ); } } App.propTypes = { selectedReddit:PropTypes.string.isRequired, posts:PropTypes.array.isRequired, isFetching:PropTypes.bool.isRequired, lastUpdated:PropTypes.number, dispatch:PropTypes.func.isRequired }; function mapStateToProps(state){ const { selectedReddit, postsByReddit } = state; const { isFetching, lastUpdated, items: posts,} = postsByReddit[selectedReddit] || { isFetching: true, items: [], }; return { selectedReddit, posts, isFetching, lastUpdated, }; } export default connect(mapStateToProps)(App);
只是要注意一下componentDidMount方法裏面是調用dispatch(fetchPostsIfNeeded(selectedReddit));的,頁面加載後就發送默認的http請求。babel
在來看看reducers\index.js:app
import {combineReducers} from 'redux'; import {SELECT_REDDIT,INVALIDATE_REDDIT,REQUEST_POSTS,RECEIVE_POSTS} from '../actions'; function selectedReddit (state='reactjs',action) { switch(action.type){ case SELECT_REDDIT: return action.reddit; default: return state === undefined ? "" : state; } } function posts(state= { isFetching: false, didInvalidate: false,items: [],}, action){ switch(action.type){ case INVALIDATE_REDDIT: return Object.assign({},state,{didInvalidate:true}); case REQUEST_POSTS: return Object.assign({},state,{isFetching:true,didInvalidate:false}); case RECEIVE_POSTS: return Object.assign({},state,{isFetching:false,didInvalidate:false,items:action.posts,lastUpdated:action.receivedAt}); default: return state; } } function postsByReddit(state={},action){ switch(action.type){ case INVALIDATE_REDDIT: case RECEIVE_POSTS: case REQUEST_POSTS: return Object.assign({},state,{[action.reddit]:posts(state[action.reddit],action)}); default: return state === undefined ? {} : state; } } const rootReducer =combineReducers({postsByReddit,selectedReddit}); export default rootReducer;
這裏用combineReducers來合併了postsByReddit和selectedReddit兩個Reducers,因此每一個action都會進入這2個Reducers(也不知道個人理解是否正確),好比action type 是INVALIDATE_REDDIT,selectedReddit 什麼都不處理,直接返回state,然而postsByReddit會返回咱們須要的state。 還有就是通過combineReducers合併後的數據,原先postsByReddit須要的state如今就只能經過state.postsByReddit來獲取了。
還有你們主要到了沒有, 這裏有return state === undefined ? "" : state; 這樣的寫法, 那是combineReducers在初始化的時候會傳遞undefined ,combineReducers->assertReducerShape的實現以下:
因此默認的state傳遞的是undefined,而咱們的reducer也是沒有處理ActionTypes.INIT的
如今來看看store/configureStore.js
import {createStore,applyMiddleware,compose} from 'redux'; import thunkMiddleware from 'redux-thunk'; import logger from 'redux-logger'; import rootReducer from '../reducers'; const store=createStore(rootReducer,initialState,compose( applyMiddleware(thunkMiddleware,logger), window.devToolsExtension? window.devToolsExtension():f=>f )); if(module.hot){ module.hot.accept('../reducers',()=>{ const nextRootReducer=require('../reducers').default; store.replaceReducer(nextRootReducer); }); } return store; }
module.hot實在啓用了熱跟新後才能夠訪問的。
index.js實現:
import 'babel-polyfill'; import React from 'react'; import {render} from 'react-dom'; import {Provider} from 'react-redux'; import App from './containers/App'; import configureStore from './store/configureStore'; const store=configureStore(); render( <Provider store={store}> <App /> </Provider> ,document.getElementById('root') );
server.js實現:
var webpack = require('webpack'); var webpackDevMiddleware = require('webpack-dev-middleware'); var webpackHotMiddleware = require('webpack-hot-middleware'); var config = require('./webpack.config'); var app= new (require('express'))(); var port= 3000; var compiler = webpack(config); app.use(webpackDevMiddleware(compiler, {noInfo:true, publicPath:config.output.publicPath})); app.use(webpackHotMiddleware(compiler)); app.get("/",function(req,res){ res.sendFile(__dirname+'/index.html'); }); app.listen(port,function(error){ if(error){ console.error(error); } else{ console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) } });
package.json文件以下:
{ "name": "react-demo", "version": "1.0.0", "main": "index.js", "scripts": { "start":"node server.js" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "babel-core": "^6.26.3", "babel-loader": "^7.1.5", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "babel-preset-react-hmre": "^1.1.1", "expect": "^23.6.0", "express": "^4.16.3", "node-libs-browser": "^2.1.0", "webpack": "^4.20.2", "webpack-dev-middleware": "^3.4.0", "webpack-hot-middleware": "^2.24.2" }, "dependencies": { "babel-polyfill": "^6.26.0", "isomorphic-fetch": "^2.2.1", "react": "^16.5.2", "react-dom": "^16.5.2", "react-redux": "^5.0.7", "redux": "^4.0.0", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0" } }
webpack.config.js:
var path = require("path"); var webpack= require('webpack'); module.exports = { devtool:'cheap-module-eval-source-map', entry: ['webpack-hot-middleware/client','./index.js'], output : { path:path.join(__dirname,'dist'), filename:"bundle.js", publicPath:'/static/' }, module:{ rules:[ { test:/\.js$/, loaders:['babel-loader'] , exclude:'/node_modules/', include:__dirname } ] }, plugins:[ new webpack.optimize.OccurrenceOrderPlugin(), new webpack.HotModuleReplacementPlugin() ] }
index.html:
<!DOCTYPE html> <html> <head> <title>Redux async example</title> </head> <body> <div id="root"> </div> <script src="/static/bundle.js"></script> </body> </html>
.babelrc:
{ "presets": [ "es2015","react" ], "env":{ "development":{ "presets":["react-hmre"] } } }