把不同路由对应不同的内容或页面的任务交给前端来做。
本质是监听URL变化,然后匹配路由规则,显示相应的页面,并且无需刷新页面。
目前前端使用的路由有2种实现方式:Hash模式,History模式
Hash模式
像www.testpage.com/#/ 这样的就是一个hash URL,当#后面的哈希值发生变化时,可以通过hashChange事件来监听URL变化,从而跳转页面。无论hash值怎么变化,服务端接收到的URL请求永远是www.testpage.com。Hash模式相对来说简单,兼容性也更好。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<a href="#view1">view1</a>
<a href="#view2">view2</a>
<a href="#view3">view3</a>
<div id="app"></div>
</body>
<script>
function hashChangeHandler() {
console.log(window.location.hash); // 获取#hash
document.getElementById('app').innerHTML = window.location.hash
}
window.addEventListener('hashchange',hashChangeHandler)
</script>
</html>
History模式
History模式是HTML5新推出的功能,主要使用history.pushState和history.replaceState改变URL。
通过History模式改变URL同样不会引起页面的刷新,只会更新浏览器的历史纪录。
当用户做出浏览器动作时(如点击后退按钮),会触发popState事件.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="nav">
<li class="js-news">news</li>
<li class="js-shopping">shopping</li>
<li class="js-music">music</li>
</ul>
<div id="app"></div>
</body>
<script>
const nav = document.getElementById('nav')
nav.addEventListener('click',function(e) {
const target = e.target
switch (target.className) {
case 'js-news':
history.pushState("新闻","",location.href+"/news")
break;
case 'js-shopping':
history.pushState("购物","",location.href+"/shopping")
break
default:
history.pushState("音乐","",location.href+"/music")
break;
}
})
// 回退或者前进时触发
function popStateEvent(e) {
console.log('popStateEvent');
console.log(e);
}
window.addEventListener('popstate',popStateEvent)
</script>
</html>
npm install --save react-router-dom (npm i -S react-router-dom)
react-router中奉行一切皆组件的思想,路由器Router,链接Link,路由Route,独占Switch,重定向Redirect都以组件形式存在。
Browser Router
基于HTML5提供的history API(pushState,replaceState和popstate事件)来保持UI和URL同步。
History Router
使用URL的hash部分(window.location.hash)来保持UI和URL同步
Link
提供声明式的,可访问的导航。
Redirect
渲染<Redirect>使导航到一个新的地址。这个新的地址会覆盖history栈中当前的地址。类似服务端(HTTP 3XX) 的重定向。
Route
Route的path属性与某个location匹配时呈现一些UI
Switch
渲染与该地址匹配的第一个子节点<Route>或者<Redirect>
withRouter
通过withRouter高阶组件访问history对象的属性和最近的<Route>的match.
当路由渲染时,withRouter会将已更新的match,location ,history 属性传递给被包裹的组件。
1、Route渲染内容的三种方式
Route渲染优先级: children > component > render
三者能接收到同样的route props,包含match,location,history。当不匹配的时候,children的match为null。
这三种方式互斥,只能选用一种。
children:func . 不管location是否匹配都会被渲染。
render:func 只有当location匹配时才会渲染,以函数形式调用。
注意:要把route props传递下去,否则在组件中要用withRouter高阶组件包裹才能获取到。
component:component 只有当location匹配时才会渲染。
注意:渲染component的时候会调用React.createElement.如果使用匿名函数调用,每次都会生成一个新的匿名函数,导致组件重复卸载/挂载
<Router>
<ul>
<li><Link to="/">home</Link></li>
<li><Link to="/list">list</Link></li>
<li><Link to="/detail">detail</Link></li>
</ul>
<Route path="/" component={Home} />
<Route
exact
path="/list"
// 三种渲染方式互斥,只能选择其一
children={(props) => <List {...props}/>}
component={Home}
render={(props) => <List {...props} />}
// 以下是错误示范,会导致组件不断卸载、挂载
// component={() => <Home /> }
/>
<Route path="/detail" component={ Detail}/>
<Route component={ NotFound}/>
</Router>
2、Route props
①、history对象,通常具有以下属性和方法。
可以通过如下方式获取history对象:
②、location对象:代表应用程序现在在哪,它曾经在哪,你想让它去哪。可以通过如下方式获取
③、match对象:包含有关如何匹配URL的信息,match对象包含以下属性。
可以通过如下方式获取match对象
3、hooks API(react-router-dom 5*版本后添加 且 仅在函数组件中使用)
useHistory:获取history对象
useLocation:获取location对象
useRouteMatch:获取match对象
useParams:获取动态路由参数
1) 定义路由
<Route path="/detail/:id" component={ Detail}/>
2)定义导航
<ul>
<li><Link to="/detail/123">123</Link></li>
<li><Link to="/detail/456">456</Link></li>
<li><Link to="/detail/789">789</Link></li>
</ul>
3)创建Detail组件接收参数
import React from 'react'
import { useHistory, useLocation, useParams, useRouteMatch } from 'react-router'
const Detail = () => {
const { goBack} = useHistory() // 获取history对象
// const location = useLocation() // 获取location对象
// const match = useRouteMatch() // 获取match对象
const params = useParams() // 获取路由参数
return <div>
<p>detail page</p>
<p>id is { params.id}</p>
<button onClick={() => {goBack() }}>返回上一页</button>
</div>
}
export default Detail
(1)定义路由和导航 app.js (主要用到render渲染)
<Router>
<ul>
<li><Link to="/">home</Link></li>
<li><Link to="/list">list</Link></li>
<li><Link to="/music">music</Link></li>
</ul>
<Switch>
<Route path="/" exact component={Home} />
<Route
exact
path="/list"
component={List}
/>
{/* 动态路由 */}
<Route exact path="/detail/:id" component={Detail} />
{/* 路由嵌套 */}
<Route path="/music" render={(props) => <Music>
<Route {...props} exact path="/music" component={Classic} />
<Route {...props} path="/music/pop" component={ PopMusic} />
</Music>} />
{/* 404 */}
<Route component={NotFound} />
</Switch>
</Router>
(2)嵌套路由的页面 music.js (props.children接收)
import React from 'react';
import { Link} from 'react-router-dom'
const Music = props => {
return (
<div>
<p>This is Music Page</p>
<ul>
<li><Link to="/music">jump to classcic</Link></li>
<li><Link to="/music/pop">jump to pop</Link></li>
</ul>
{props.children}
</div>
);
};
export default Music;
思路:使用高阶组件包装Route使其具有权限判断功能。
做个小例子:假设有首页,用户中心,登录 3个页面;用户中心页面只有登录了才能查看。没登录的要重定向至登录页。登录态保存在store中。
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import store from './store'
import { Provider} from 'react-redux'
ReactDOM.render(
<React.StrictMode>
<Provider store={ store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
app.js
import React from 'react'
import {
BrowserRouter as Router,
Route,
Link,
Switch,
Redirect
} from 'react-router-dom'
import Home from './pages/Home'
import UseCenter from './pages/Usercenter'
import Login from './pages/Login'
import NotFound from './pages/NotFound'
import PriviateRoute from './router/PriviateRoute'
import { useSelector} from 'react-redux'
function App () {
const { isLogin } = useSelector(({ loginReducer}) => loginReducer)
return <div className="App">
<Router>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/userCenter">用户中心</Link></li>
<li><Link to="/login">登录</Link></li>
</ul>
<Switch>
<Route path="/" exact component={Home} />
{/* <Route path="/userCenter" component={UseCenter} /> */}
<PriviateRoute isLogin={ isLogin} path="/userCenter" component={UseCenter} />
<Route path="/login" component={ Login}/>
{/* 404 */}
<Route component={NotFound} />
</Switch>
</Router>
</div>
}
export default App;
router/priviateRoute.js (路由守卫)
import React from 'react'
import { Redirect, Route} from 'react-router-dom'
/**
* 路由守卫
已登录:该去哪去哪
未登录:重定向至登录页,并且携带参数,以便登录后自动跳转到原来的页面
*/
function PriviateRoute ({ isLogin,component:Component,...rest}) {
return <Route
{...rest}
render={(props) =>
isLogin ? <Component {...props}/>
: <Redirect to={{ pathname: '/login', state: {from:props.location.pathname}}
} />
} />
}
export default PriviateRoute
store/index.js
import { createStore,combineReducers} from 'redux'
import { loginReducer } from './reducer'
const store = createStore(
combineReducers({loginReducer})
)
export default store
store/action.js
export const IS_LOADING = "IS_LOADING"
export const LOGIN_SUCCESS = "LOGIN_SUCCESS"
export const LOGIN_FAILED = "LOGIN_FAILED"
export const LOGOUT_SUCCESS = "LOGOUT_SUCCESS"
export const LOGOUT_FAILED = "LOGOUT_FAILED"
store/reducer.js
import { IS_LOADING, LOGIN_FAILED, LOGIN_SUCCESS, LOGOUT_SUCCESS } from './action'
const defaultLoginState = {
isLogin: false,
userInfo: { id: null, name: '' },
loading: false,
err: {msg:''}
}
export const loginReducer = (state = defaultLoginState, { type, payload }) => {
switch (type) {
case IS_LOADING:
return {...state,loading:true}
case LOGIN_SUCCESS:
return { ...state, isLogin: true,loading:false, userInfo: {...payload}}
case LOGIN_FAILED:
return { ...state, ...defaultLoginState,...payload}
case LOGOUT_SUCCESS:
return {...state,isLogin:false,loading:false}
default:
return state
}
}
Login.jsx
import React from 'react'
import { useSelector,useDispatch} from 'react-redux'
import { Redirect, useLocation } from 'react-router'
const Login = (props) => {
const dispatch = useDispatch()
const { isLogin, loading, err, userInfo } = useSelector(({ loginReducer }) => loginReducer)
const location = useLocation()
const { from = "/" } = location.state || {}
const login = () => {
dispatch({ type: 'LOGIN_SUCCESS', payload: { id:1,name:'Amy'}})
}
if (isLogin) {
return <Redirect to={from}/>
}
return <div>
<p>登录页</p>
<button onClick={ login}>{ loading?"...loading":"登录"}</button>
</div>
}
export default Login
UserCenter.jsx
import React from 'react'
import { useSelector} from 'react-redux'
const Usercenter = (props) => {
const { userInfo} = useSelector(({ loginReducer }) => loginReducer)
return <div>
<p>用户中心</p>
<p>{ userInfo.name},欢迎您</p>
</div>
}
export default Usercenter
当然,实际项目中登录态肯定都是通过接口获取的,上例主要是为了演示路由守卫。
对于处理异步请求,推荐使用react-redux中间件。
预计下一篇文章比较redux-thunk,redux-saga.