最近項目使用react全家桶(react+redux+react-router(v4)+react-router-redux(v5-alpah.6)+react-router-config(v1-beta.4))來開發網站,使用過程當中遇到這樣或者那樣的問題,也在sg上面提了好多問題,如:如何監聽路由切換?componentWillReceiveProps渲染死循環?Route嵌套路由致使問題等等,有興趣的能夠直接打開個人問題看看。vue
如今我來分析和總結一下,如何處理這些問題,以及如何去使用。
首先如何結合redux+react-router+react-router-redux+react-router-config,這也是重點所在,下面代碼已經給出了,並且官網也有例子,你們能夠參考一下,我如今只講些重點,細節我就很少說了,若是有問題能夠加我建的羣進去提問:464696550react
重點1:thunk和react-router-redux中間件的使用ios
const middleWares = [thunkMiddleware, historyRouterMiddleware]; let store = createStore(combineReducers({ ...reducers, router: routerReducer }), applyMiddleware(...middleWares));
沒錯,你可使用es6語法...middleWares這樣寫在applyMiddleware函數參數裏面,還有由於routerReducer也會加載到store裏面,因此你能夠這樣使用在createStore裏面添加combineReducers把你的相關reducers和router:routerReducer結合加進去,特別注意的是:在reducers文件裏面就不要用combineReducer不然會報錯,像個人reducers文件以下:es6
reducers/index.jsvue-router
import user from './user' import layout from './layout' import historyRouter from './historyRouter' const reducers = { user, layout, historyRouter }; export default reducers
重點2:如何監聽路由變化
這也是我在使用過程當中遇到最大的坑,百度了好多也sg上面提問你們都說是經過componentWillReceiveProps事件來監聽,並且要放在根路由組件上面,後來在react-router-config上面發現也是經過componentWillReceiveProps來監聽,下面是個人具體代碼你們能夠參考下:redux
componentWillReceiveProps(nextProps) { const navigated = nextProps.location !== this.props.previousLocation; const { location } = this.props; if (navigated) { this.props.dispatch(setPreviousLocation(location)); this.routerChanged(location.pathname); } }
重點3:Route路由不要用嵌套的方式
一開始我對react-router不熟悉感受就是沒有vue-router用得方便,並且它是經過jsx的形式來呈現的,因此我一開始在頁面佈局上是這樣作的,首先使用Route來定義一個路由,而後該路由組件裏面我再定義多個Route,但是這樣會發現有不少問題,尤爲是「/」路由,因此我在頁面佈局的時候把公共樣式 抽取出來作成一個單獨組件來使用,這樣就防止Route嵌套Route帶來的問題,下面就是些具體的代碼:axios
<Route exact path="/" component={Index}/> <Route exact path="/explore" component={Explore}/> <Route exact path="/user" component={User}/> <Route exact path="/mysites" component={MySites}/>
值得注意的是上面的Index、Explore、User、和MySites這4個組件裏面會用到Layout組件,這個Layout組件就是我抽取出來的。react-router
重點4:推薦使用react-router-config來加載路由不要使用jsx方式
這也是官網的react-router-config庫提供的功能,因此感受上更像vue-router了,這樣很方便管理,以下就是個人路由代碼:app
router/index.js框架
import Login from '../containers/Login' import Register from '../containers/Register' import VerifyEmail from '../containers/VerifyEmail' import NoMatch from '../containers/NoMatch' import Explore from '../containers/Explore' import MySites from '../containers/MySites' import User from '../containers/User' import Index from '../containers/Index' const routes = [ { path: '/', component: Index, exact: true, }, { path: '/explore', exact: true, component: Explore }, { path: '/user', exact: true, component: User }, { path: '/mysites', exact: true, component: MySites }, { path: '/account/register', exact: true, component: Register }, { path: '/account/login', exact: true, component: Login }, { path: '/email/verify/:token', exact: true, component: VerifyEmail }, { component: NoMatch }, ]; export default routes;
這是我項目的目錄結構,很是典型的結構沒有什麼特別的地方,由於還在不斷學習react因此有什麼不規範或者有什麼更好的建議能夠跟我交流討論,我都很是歡迎。
下面這段代碼就是具體講如何結合幾個框架了,你們能夠看看參考參考,我只列出幾個比較重要的文件和組件代碼。
app.js
import React from 'react' import {createStore, combineReducers, applyMiddleware} from 'redux' import {Provider} from 'react-redux' import thunkMiddleware from 'redux-thunk' import createHistory from 'history/createBrowserHistory' import {ConnectedRouter, routerReducer, routerMiddleware, push} from 'react-router-redux' import {matchRoutes, renderRoutes} from 'react-router-config' import {Switch} from 'react-router-dom' import reducers from './reducers' import routes from './router' const history = createHistory(); const historyRouterMiddleware = routerMiddleware(history); const middleWares = [thunkMiddleware, historyRouterMiddleware]; let store = createStore(combineReducers({ ...reducers, router: routerReducer }), applyMiddleware(...middleWares)); const App = () => ( <Provider store={store}> <ConnectedRouter history={history}> <Switch> {renderRoutes(routes)} </Switch> </ConnectedRouter> </Provider> ); export default App
components/Layout.js
import React from 'react'; import PropTypes from 'prop-types'; import { withStyles } from 'material-ui/styles'; import classNames from 'classnames'; import Topbar from '../components/layout/Topbar' import Sidebar from '../components/layout/Sidebar' import {Route,withRouter} from 'react-router-dom' import {getUserInfo} from '../utils' import VerifyEmailDlg from './Dialog/VerifyEmailDlg' import SitesSidebar from '../components/layout/SitesSidebar' import {connect} from 'react-redux'; import {setBodyWidth,setIsMySites,setPreviousLocation} from '../actions' const styles = theme => ({ root: { width: '100%', height: '100%', zIndex: 1, overflow: 'hidden', }, appFrame: { display: 'flex', width: '100%', height: '100%', }, content: { position: 'absolute', top: '56px', right: 0, left: 0, bottom: 0, marginLeft:0, flexGrow: 1, boxSizing:'border-box', backgroundColor: '#fff', padding:0, transition: theme.transitions.create('margin', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, }), height: 'calc(100% - 56px)' }, contentShift: { marginLeft: '240px', transition: theme.transitions.create('margin', { easing: theme.transitions.easing.easeOut, duration: theme.transitions.duration.enteringScreen, }), }, }); class Layout extends React.Component { constructor(props){ super(props); this.state = { showVerifyEmailDig:false, user:null }; } componentWillReceiveProps(nextProps) { const navigated = nextProps.location !== this.props.previousLocation; const { location } = this.props; if (navigated) { this.props.dispatch(setPreviousLocation(location)); this.routerChanged(location.pathname); } } componentWillMount(){ window.addEventListener('resize', this.resizeListener); this.resizeListener(); //this.showVerifyEmailDialog(); } componentWillUnmount(){ window.removeEventListener('resize', this.resizeListener); } //監聽路由變化 routerChanged(pathname){ if (pathname.indexOf('mysites')!=-1){ this.props.dispatch(setIsMySites(true)); }else{ this.props.dispatch(setIsMySites(false)); } }; resizeListener=()=> { let bodyWidth = document.body.offsetWidth; this.props.dispatch(setBodyWidth(bodyWidth)); }; showVerifyEmailDialog=()=>{ let that = this; getUserInfo(function(isLogin,user){ if (isLogin&&user.is_active==0){ that.setState({ showVerifyEmailDig:true, user:user }) } }); }; render() { const { classes,width,showSiteSidebar,sideBarInfo } = this.props; return ( <div className={classes.root}> <VerifyEmailDlg onClose={()=>{ this.setState({ showVerifyEmailDig:false }) }} showVerifyEmailDig={this.state.showVerifyEmailDig} user={this.state.user}/> <div className={classes.appFrame}> <Topbar showSearchBar={width>1024}/> <Sidebar/> {showSiteSidebar ? <SitesSidebar/>:null} <main className={classNames(classes.content,((showSiteSidebar||sideBarInfo.open)&&width>1024) && classes.contentShift)}> {this.props.children} </main> </div> </div> ); } } const mapStateToProps = state => { return { column:state.layout.column, width:state.layout.bodyWidth, sideBarInfo:state.layout.sideBarInfo, showSiteSidebar:state.layout.showSiteSidebar, isMySites:state.layout.isMySites, previousLocation:state.historyRouter.previousLocation } }; export default withRouter(connect(mapStateToProps)(withStyles(styles)(Layout)));
actions/index.js
import axios from 'axios' export const LOGIN_INFO = 'LOGIN_INFO'; export const LOGOUT='LOGOUT'; export const SET_COLUMN = 'SET_COLUMN'; export const SET_BODY_WIDTH='SET_BODY_WIDTH'; export const TOGGLE_SIDEBAR='TOGGLE_SIDEBAR'; export const SET_SIDEBAR = 'SET_SIDEBAR'; export const SET_IS_MY_SITES='SET_IS_MY_SITES'; export const SET_PREVIOUS_LOCATION='SET_PREVIOUS_LOCATION'; export function getLoginAction(){ return (dispatch,state)=>{ axios.get('/user/validateUser') .then((res)=>{ console.log('axios:/user/validateUser'); console.log(res); dispatch({ type:LOGIN_INFO, isLogin:res.data.isLogin, user:res.data.user }) }).catch((err)=>{ }); } } export function logout() { return (dispatch,state)=>{ axios.get('/user/logout').then((res)=>{ if (res.data.success){ console.log('logout success'); dispatch({ type:LOGOUT, isLogin:false }) } }) } } export const setColumn=column=>({ type:SET_COLUMN, column }); export const setBodyWidth=width=>({ type:SET_BODY_WIDTH, width }); export const toggleSideBar = sideBarInfo=>({ type:TOGGLE_SIDEBAR }); export const setSidebar = sideBarInfo=>({ type:SET_SIDEBAR, sideBarInfo }); export const setIsMySites = isMySites=>({ type:SET_IS_MY_SITES, isMySites }); export const setPreviousLocation = location=>({ type:SET_PREVIOUS_LOCATION, location });
containers/Index.js
import React from 'react' import PropTypes from 'prop-types'; import {withStyles} from 'material-ui/styles' import classNames from 'classnames' import Grid from 'material-ui/Grid'; import AppBar from 'material-ui/AppBar' import Tabbar from '../components/Tabbar' import Layout from '../components/Layout' import {connect} from 'react-redux'; function TabContainer(props) { return <div style={{ padding: 20 }}>{props.children}</div>; } class Index extends React.Component{ constructor(props){ super(props); this.state={ tabIndex:0, } } onTabChanged=(index)=>{ this.setState({ tabIndex: index }); }; render(){ const { classes,column } = this.props; const tabs = [{ id:1,name:'關注者動態' },{ id:2,name:'個人動態' },{ id:3,name:'所有動態' }]; return( <Layout> <Grid container spacing={0} justify={'center'}> <Grid item xs={12}> <AppBar position="static" color="default" className={classNames(classes.appBar)}> <Grid container spacing={0} justify={'center'}> <Grid item xs={column}> <Tabbar tabs={tabs} onTabChanged={this.onTabChanged} index={this.state.tabIndex} /> </Grid> </Grid> </AppBar> </Grid> <Grid item xs={column}> {this.state.tabIndex === 0 && <TabContainer>sdfsf </TabContainer>} {this.state.tabIndex === 1 && <TabContainer>{'Item Two'}</TabContainer>} {this.state.tabIndex === 2 && <TabContainer>{'Item Three'}</TabContainer>} </Grid> </Grid> </Layout> ) } } const mapStateToProps = state => { return { column:state.layout.column } }; const styles = theme =>({ appBar: { boxShadow:'none !important' }, }); export default connect(mapStateToProps)(withStyles(styles)(Index))
reducers/layout.js
import { SET_COLUMN, SET_BODY_WIDTH, TOGGLE_SIDEBAR, SET_SIDEBAR, SET_IS_MY_SITES } from '../actions' let layoutInfo={ column:11, bodyWidth:0, isMySites:false, showSiteSidebar:false, sideBarInfo:{ open: true, type:'persistent',//temporary, } }; const layout = (state=layoutInfo,action)=>{ switch (action.type){ case SET_COLUMN: console.log('SET_COLUMN'); return { ...state, column:action.column }; case SET_BODY_WIDTH: console.log('SET_BODY_WIDTH'); let isPersistent = !state.isMySites && action.width>1024; return{ ...state, bodyWidth:action.width, column:state.sideBarInfo.open?11:10, showSiteSidebar:action.width>1024&&state.isMySites, sideBarInfo:{ open:isPersistent, type:isPersistent?'persistent':'temporary' } }; case TOGGLE_SIDEBAR: console.log('TOGGLE_SIDEBAR'); let isOpen = !state.sideBarInfo.open; return{ ...state, column:isOpen?11:10, sideBarInfo:{ ...state.sideBarInfo, open:isOpen } }; case SET_SIDEBAR: console.log('SET_SIDEBAR'); return{ ...state, sideBarInfo:action.sideBarInfo }; case SET_IS_MY_SITES: console.log('SET_IS_MY_SITES'); let showSideBar =state.bodyWidth>1024&&!action.isMySites; return { ...state, isMySites:action.isMySites, showSiteSidebar:state.bodyWidth>1024&&action.isMySites, sideBarInfo:{ open:showSideBar, type:showSideBar?'persistent':'temporary' } }; default: return state; } };