react项目开发-路由优化(前三篇续)
目前的项目架构,大家都能看到,有路由跳转的地方都是写死的url,如<Link to={'/aaa'}>,push({pathname:'/login'}),这种方式呢,没有问题,但是这样写死不太好,不便于后期维护,
比如以后要改下路径,除了config.js中需要改一次,代码全篇幅都需要改一次,好累;name该怎么办呢?以下我们就来处理一下路由的优化!
优化思路:
最好能在一个地方去维护这些路径地址,也就是config.js中,name我们就需要把config中的menuGlobal存储一份到全局配置的store中,这样将来项目其他地方有用到路径的,均可以从tore中取出来使用即可。
那这里我们就考虑,存在store中的这路由数据,应该是什么结构呢,没错,我们使用immutable数据的Map形式,易操作,简单直观(之前我们在menuGlobal总预留的id和pid这里就要用到了)。具体如下:
1 修改utils/config.js如下:
import {OrderedSet,Map,fromJS} from 'immutable'
const menuGlobal=[
{
id:'login',
pid:'0',
name:'登录',
icon:'user',
path: '/login',
models: () => [import('../models/login')], //models可多个
component: () => import('../routes/login'),
},
{
id:'home',
pid:'0',
name:'首页',
icon:'user',
path: '/',
models: () => [import('../models/home')], //models可多个
component: () => import('../routes/home'),
},
{
id:'aaa',
pid:'0',
name:'aaa页',
icon:'user',
path: '/aaa',
models: () => [import('../models/aaa')], //models可多个
component: () => import('../routes/AAA'),
},
{
id:'bbb',
pid:'0',
name:'bbb页',
icon:'user',
path: '/aaa/bbb',
models: () => [import('../models/bbb')], //models可多个
component: () => import('../routes/BBB'),
},
{
id:'ccc',
pid:'0',
name:'ccc页',
icon:'user',
path: '/ccc',
models: () => [import('../models/ccc')], //models可多个
component: () => import('../routes/CCC'),
},
];
/**
* 封装路由数据,利用id和pid的关联性处理
*/
const menuMap = (() => {
let byId = Map();
let byPid = Map();
menuGlobal.map(item => {
byId = byId.set(item.id, fromJS(item));
byPid = byPid.update(item.pid, obj => obj ? obj.add(item.id) : OrderedSet([item.id]))
});
return Map({
byId,
byPid
});
})();
export default {
menuGlobal,
menuMap
}
2 修改models/app.js如下:
import {Map, fromJS} from 'immutable';
import {routerRedux} from 'dva/router';
import {config} from '../utils';
const {menuMap} = config;
const initState = Map({
i18n: 'zh_CN',
token:null,
locationPathname:null,
menu:menuMap
})
export default {
namespace: 'app',
state:initState,
subscriptions: {
setup({ dispatch, history }) {
},
setupHistory ({ dispatch, history }) {
history.listen((location) => {
dispatch({
type: 'updateLocation',
payload: {
locationPathname: location.pathname
},
});
dispatch({
type: 'updateToken',
payload: {
token: window.sessionStorage.getItem('token')
},
})
})
},
},
effects: {
* changeLang ({
payload: {value},
}, { put }) {
yield put({ type: 'updateLang', payload: {value}});
},
* updateLocation ({
payload
}, {put, select}) {
yield put({type: 'updateStore', payload});
},
* updateToken ({
payload
}, {put, select}) {
yield put({type: 'updateStore', payload});
},
* loginOk ({
payload
}, {put, select}) {
window.sessionStorage.setItem('token',payload.token);
yield put(routerRedux.push({
pathname: '/'
}));
},
* logout ({
payload
}, {put, select}) {
window.sessionStorage.removeItem('token');
window.location.href='/login';
},
},
reducers: {
updateLang (state,{payload:{value}}) {
return state.set('i18n',value);
},
updateStore (state, { payload }) {
return payload?state.mergeDeep(fromJS(payload)):initState
},
},
};
以上修改做了两件事:
1封装menuMap数据结构
2存储在app的model中menu对象
效果如下:
接下来,我们就可以在任何地方获取并使用它了
(1)在组件中使用,需要connect数据 menu:app.get('menu'),以routes/home/index.js为例,修改如下:
import React, {Component} from 'react';
import {connect} from 'dva'
import {Link} from 'dva/router'
import {injectIntl} from 'react-intl'
import {Row, Col, Form, Button} from 'antd'
import classnames from 'classnames';
import styles from './index.less';
class Home extends Component{
render(){
const {menu} = this.props;
const aaaUrl=menu.getIn(['byId','aaa','path']);
return(
<Row>
<Col className={classnames(styles.home)}>
欢迎您,来到首页
</Col>
<Col>
<Link to={aaaUrl}><Button>去AAA页面</Button></Link>
</Col>
</Row>
)
}
}
export default connect(({
app
})=>({
menu:app.get('menu')
}))(injectIntl(Form.create()(Home)))
(2)在models中使用,需要select数据 yield select(_=>_.app.getIn(['menu','byId','home','path'])), 以models/app.js为例,修改loginOk如下:
import {Map, fromJS} from 'immutable';
import {routerRedux} from 'dva/router';
import {config} from '../utils';
const {menuMap} = config;
const initState = Map({
i18n: 'zh_CN',
token:null,
locationPathname:null,
menu:menuMap
})
export default {
namespace: 'app',
state:initState,
subscriptions: {
setup({ dispatch, history }) {
},
setupHistory ({ dispatch, history }) {
history.listen((location) => {
dispatch({
type: 'updateLocation',
payload: {
locationPathname: location.pathname
},
});
dispatch({
type: 'updateToken',
payload: {
token: window.sessionStorage.getItem('token')
},
})
})
},
},
effects: {
* changeLang ({
payload: {value},
}, { put }) {
yield put({ type: 'updateLang', payload: {value}});
},
* updateLocation ({
payload
}, {put, select}) {
yield put({type: 'updateStore', payload});
},
* updateToken ({
payload
}, {put, select}) {
yield put({type: 'updateStore', payload});
},
* loginOk ({
payload
}, {put, select}) {
const homeUrl = yield select(_=>_.app.getIn(['menu','byId','home','path']))
window.sessionStorage.setItem('token',payload.token);
yield put(routerRedux.push({
pathname: homeUrl
}));
},
* logout ({
payload
}, {put, select}) {
window.sessionStorage.removeItem('token');
window.location.href='/login';
},
},
reducers: {
updateLang (state,{payload:{value}}) {
return state.set('i18n',value);
},
updateStore (state, { payload }) {
return payload?state.mergeDeep(fromJS(payload)):initState
},
},
};
至此,刷新下试试,一切正常吧!
下面给大家推荐一款路由正则匹配的工具:path-to-regexp,用来更简易的操作路径匹配问题,有兴趣的同学可以自行学习。
在首页增加一个路由,用作编辑和新增页面,修改utils/config.js如下:
import {OrderedSet,Map,fromJS} from 'immutable'
const menuGlobal=[
{
id:'login',
pid:'0',
name:'登录',
icon:'user',
path: '/login',
models: () => [import('../models/login')], //models可多个
component: () => import('../routes/login'),
},
{
id:'home',
pid:'0',
name:'首页',
icon:'user',
path: '/',
models: () => [import('../models/home')], //models可多个
component: () => import('../routes/home'),
},
{
id:'home-edit',
pid:'home',
name:'首页-编辑和新增',
icon:'user',
path: '/edit/:id?',
models: () => [import('../models/home')], //models可多个
component: () => import('../routes/home/edit'),
},
{
id:'aaa',
pid:'0',
name:'aaa页',
icon:'user',
path: '/aaa',
models: () => [import('../models/aaa')], //models可多个
component: () => import('../routes/AAA'),
},
{
id:'bbb',
pid:'0',
name:'bbb页',
icon:'user',
path: '/aaa/bbb',
models: () => [import('../models/bbb')], //models可多个
component: () => import('../routes/BBB'),
},
{
id:'ccc',
pid:'0',
name:'ccc页',
icon:'user',
path: '/ccc',
models: () => [import('../models/ccc')], //models可多个
component: () => import('../routes/CCC'),
},
];
/**
* 封装路由数据,利用id和pid的关联性处理
*/
const menuMap = (() => {
let byId = Map();
let byPid = Map();
menuGlobal.map(item => {
byId = byId.set(item.id, fromJS(item));
byPid = byPid.update(item.pid, obj => obj ? obj.add(item.id) : OrderedSet([item.id]))
});
return Map({
byId,
byPid
});
})();
export default {
menuGlobal,
menuMap
}
对应增加组件routes/home/edit.js代码如下:
import React, {Component} from 'react';
import {connect} from 'dva'
import {Link} from 'dva/router'
import {injectIntl} from 'react-intl'
import {Row, Col, Form, Button} from 'antd'
import classnames from 'classnames';
import styles from './index.less';
class Home extends Component{
goBack=()=>{
const {dispatch} = this.props;
dispatch({
type:'app/goBack'
})
}
render(){
const {menu,match} = this.props;
const id=match.params.id;
return(
<Row>
<Col className={classnames(styles.home)}>
欢迎您,来到home<span style={{fontSize:'24px'}}>{id?`编辑${id}`:`新增`}</span>页面
</Col>
<Col>
<Button onClick={this.goBack}>返回</Button>
</Col>
</Row>
)
}
}
export default connect(({
app
})=>({
menu:app.get('menu')
}))(injectIntl(Form.create()(Home)))
这里用到了返回上一页,只需要在models/app.js中增加effects,用于返回上一页
* goBack ({
payload
}, {put, select}) {
yield put(routerRedux.goBack());
},
修改routes/home/index.js,灵活使用pathToRegexp.compile(homeEditUrl)({id:1}) 如下:
import React, {Component} from 'react';
import {connect} from 'dva'
import {Link} from 'dva/router'
import {injectIntl} from 'react-intl'
import {Row, Col, Form, Button} from 'antd'
import classnames from 'classnames';
import pathToRegexp from 'path-to-regexp'
import styles from './index.less';
class Home extends Component{
render(){
const {menu} = this.props;
const aaaUrl=menu.getIn(['byId','aaa','path']);
const homeEditUrl=menu.getIn(['byId','home-edit','path']);
return(
<Row>
<Col className={classnames(styles.home)}>
欢迎您,来到首页
</Col>
<Col>
<Link to={aaaUrl}><Button>去AAA页面</Button></Link>
<Link to={pathToRegexp.compile(homeEditUrl)()}><Button>新增</Button></Link>
<Link to={pathToRegexp.compile(homeEditUrl)({id:1})}><Button>编辑(id=1)</Button></Link>
<Link to={pathToRegexp.compile(homeEditUrl)({id:2})}><Button>编辑(id=2)</Button></Link>
<Link to={pathToRegexp.compile(homeEditUrl)({id:123})}><Button>编辑(id=123)</Button></Link>
</Col>
</Row>
)
}
}
export default connect(({
app
})=>({
menu:app.get('menu')
}))(injectIntl(Form.create()(Home)))
然后修改models/home.js,灵活使用pathToRegexp(homeUrl).exec(pathname) 如下:
import pathToRegexp from 'path-to-regexp'
export default {
namespace: 'home',
state: {
name:'这是home的model'
},
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname, query }) => {
dispatch({type: 'dataInit', payload: {pathname}});
});
},
},
effects: {
* dataInit({payload: {pathname}}, {put,call,select}){
const homeUrl = yield select(_=>_.app.getIn(['menu','byId','home','path']));
const homeEditUrl = yield select(_=>_.app.getIn(['menu','byId','home-edit','path']));
if(pathToRegexp(homeUrl).exec(pathname)){
console.log('home页面执行')
}else if(pathToRegexp(homeEditUrl.substring(0,homeEditUrl.lastIndexOf('?')-4)).exec(pathname)){
console.log('home-新增页面执行')
}else if(pathToRegexp(homeEditUrl.substring(0,homeEditUrl.lastIndexOf('?'))).exec(pathname)){
console.log('home-编辑页面执行')
}
},
},
reducers: {
},
};
好了,我们的预期是:
(1)访问https://localhost:9999/,控制台输出“home页面执行”
(2)访问https://localhost:9999/edit,控制台输出“home-新增页面执行”
(3)访问https://localhost:9999/edit/1,控制台输出“home-编辑页面执行”
下面看看效果:
哦了,就是这个样子!
再给大家介绍一个常用的工具react-helmet,什么东西呢,直观翻译‘头盔’,用在react组件中,就是用来处理页面的head部分构成的,有兴趣的同学自行学习。安装:cnpm i react-helmet --save
先来看看现在的页面title:
增加/layout/layout.js文件,代码:
import {connect} from 'dva';
import React from 'react';
import pathToRegexp from 'path-to-regexp'
import Helmet from 'react-helmet';
const Layout=({ children,dispatch,menu,locationPathname })=>{
const menuList=menu.getIn(['byId']).toList();
let menuName='';
menuList.map(item=>{
if(pathToRegexp(item.get('path')).exec(locationPathname)){
menuName = item.get('name');
}
});
return (
<React.Fragment>
<Helmet>
<title>
{menuName}
</title>
</Helmet>
{children}
</React.Fragment>
);
}
export default connect(({
app
})=>({
menu:app.get('menu'),
locationPathname:app.get('locationPathname'),
}))(Layout)
修改/layout/auth.js 如下:
import {connect} from 'dva';
import React from 'react';
import Layout from './layout';
const Auth=({ children,dispatch,token,locationPathname })=>{
if(!token&&locationPathname!='/login'){
dispatch({
type:'app/logout'
})
}else if(token&&locationPathname=='/login'){
dispatch({
type:'app/loginOk',
payload:{
token:token
}
})
}
return (
<Layout>
{children}
</Layout>
);
}
export default connect(({
app
})=>({
token:app.get('token'),
locationPathname:app.get('locationPathname'),
}))(Auth)
OK,我们再来看页面title:
此时,title显示的已经是menuGlobal中配置的name值了。