路由是跟据不同的url地址展示不同的页面
后端路由
服务端解析url地址,发送数据,但是每一次切换都需要重新刷新页面,对用户体验不好
前端路由
用户在点击切换路由的时候不需要重新刷新页面,路由由前端维护,分为hash路由和history路由,带#号的为hash路由,hash路由兼容性比较好
history路由
H5新增了window.history.pushstate 和window.history.replaceState
hash路由
hashchange事件来监听url hash值的改变
react-router
架构
Route 拿到context 中的location 和history 给 component, 然后根据路径 path 和pathname 生出一个match 也传给component
如何实现
HashRouter
新建一个文件夹,react-router-dom, 在index.js 文件中导出 HashRouter,
export { default as HashRouter } from './HashRouter';
export { default as BrowserRouter } from './BrowserRouter'
然后来写HashRouter.js 文件
实际上引用的还是Router组件,通过createHashHistory 创建hash的history对象,传给Router
// HashRouter.js
import { Router } from '../react-router'
import { createHashHistory } from 'history'
const HashRouter = (props) => {
// 创建一个history对象,创建一个history对象,但是是使用hash实现
let hashHistory = createHashHistory()
return (
<Router history={hashHistory}>
{props.children}
</Router>
)
}
export default HashRouter
新建一个文件夹,react-router
在Router传值给Route的时候,一共传了三个对象,history,location, match, 通过context的方法传递给下面的Route。在这里我们写一个RouterContext.js文件,用于所有组件都可以引用到这个context
// RouterContext.js
import React from 'react'
const RouterContext = React.createContext();
export default RouterContext;
然后来实现Router
// Router.js
import React, {useState} from 'react'
import RouterContext from './RouterContext'
const Router = (props) => {
const { history } = props; // 可能是hashHistory 可能是 browserHistory
const [location, setLocation] = useState(history.location)
// 监听历史对象中的路径变化,当路径发生变化后执行回调函数
// 返回一个location 参数就是最新的对象
history.listen(location => setLocation(location))
// 里面有三个值
const value = {
history, // 这个里面的location和外面的是一样的
// location 代表当前路径 url 地址
// location: {pathname:'', state: undefined, search: '', hash:"", }
// match: {
// isExact: false,
// path: '/',
// url:'/'
// }
location,
}
return (
<RouterContext.Provider value = {value}>
{props.children}
</RouterContext.Provider>
)
}
export default Router
最后我们来实现Route进行渲染不同的组件 这里判断传给route的path路径和location.pathname(当前地址中的url地址)匹不匹配,匹配就渲染组件
// Route.js
import React, {useContext} from 'react'
import RouterContext from './RouterContext'
const Route = (props) => {
const { history, location, match } = useContext(RouterContext);
const { path, component: RouterComponent, exact } = props;
let renderElement = null;
// 将history和location传给组件
let renderProps = {
history,
location
}
if (path === location.pathname) {
renderElement = < RouterComponent {...renderProps} />
}
return renderElement
}
export default Route;
最后我们来实现 createHashHistory 这个方法, 用来创建出的 hash路由下面的 history 对象
在history文件夹下面新建一个createHashHistory.js 文件 用来实现history
// createHashHistory.js
const createHashHistory = () => {
let action;
let listenerList = [];
let historyStack = [];
let historyIndex = -1;
let state;
// 用于监听到location改变后修改location
const listen = (handleLocation) => {
// hadleLocation是一个改变location的函数
listenerList.push(handleLocation)
return () => {
const index = listenerList.indexOf(handleLocation)
listenerList.splice(index, 1);
}
}
const handelHashChange = () => {
// 当前的路径
let pathname = window.location.hash.slice(1);
// 将新的action和pathname赋值给histor.action
Object.assign(history,{
action,
location: {pathname, state}
})
if (!action || action === 'PUSH') {
historyStack[++historyIndex] = history.location
} else if (action === 'REPLACE') {
historyStack[historyIndex] = history.location
}
listenerList.forEach(handleLocation => handleLocation(history.location))
}
window.addEventListener('hashchange', handelHashChange)
const push = (newPath, newState) => {
action = 'PUSH';
let pathname;
if (typeof newPath === 'object') {
state = newPath.state;
pathname = newPath.pathname;
} else {
pathname = newPath;
state = newState;
}
window.location.hash = pathname;
}
const replace = (newPath, nextState) => {
action = 'REPLACE'
let pathname;
if (typeof newPath === 'object') {
state = newPath.state;
pathname = newPath.pathname
} else {
pathname = newPath;
state = nextState
}
window.location.hash = pathname;
}
const go = (n) => {
action = 'POP'
historyIndex += n;
let newLocation = historyStack[historyIndex];
state = newLocation.state;
window.location.hash = newLocation.pathname;
}
const goBack = () => {
go(-1)
}
const goForward = () => {
go(1)
}
// 自己创建的一个对象,类似于window.history,用于hash路由,两者没有关系
let history = {
listen, //监听
action,
// 给location一个默认值
location: {pathname:'/', state:undefined},
go,
goBack,
goForward,
push,
replace,
}
// 当url输入的地址和当前地址是一样的时候,相当于刷新页面,这个时候是不会触发到hashchange事件的因为hash值没有变,所以需要判断一下,手动调用一下这个事件
if (window.location.hash) {
handelHashChange()
} else {
window.location.hash = "/"
}
// 返回自己创建的history对象
return history
}
export default createHashHistory;
现在我们已经简单实现了,来看一下效果吧
historyRouter
和hashRouter一样,我们在react-router-dom文件夹新建一个BrowserRouter.js文件,然后导出
// BrowserRouter.js
import { Router } from '../react-router'
import createBrowserRouter from '../historya/createBrowserRouter'
const BrowserRouter = (props) => {
let BrowserRouter = createBrowserRouter()
return (
<Router history={BrowserRouter}>
{props.children}
</Router>
)
}
export default BrowserRouter;
然后来实现createBrowserRouter
createBrowserRouter和createhashRouter差别不多,就是关于历史栈是history里面内部维护的,当点击浏览器返回箭头或者调用go,goback的时候就会触发popstate,监听popstate来设置最新的history,更新渲染组件
// createBrowserRouter.js
const createBrowserRouter = () => {
let action;
let state;
let listenerList = [];
let globalHistory = window.history;
const listen = (HandellisterLocation) => {
listenerList.push(HandellisterLocation);
return () => {
const index = listenerList.indexOf(HandellisterLocation);
listenerList.splice(index, 1)
}
}
const changeState = (newLocaation) => {
Object.assign(history, newLocaation);
listenerList.forEach(HandellisterLocation => HandellisterLocation(history.location))
}
const push = (newPath, newState) => {
action = 'PUSH'
let pathname;
if (typeof newPath === 'object') {
pathname = newPath.pathname
state = newPath.pathname
} else {
pathname = newPath;
state = newState;
}
globalHistory.pushState(state, null, pathname)
changeState({action, location: {pathname, state}})
}
// 当回退或者前进的时候会执行,这个监听是浏览器自带的默认支持的
window.onpopstate = () => {
changeState({
action:'POP',
location: {pathname : window.location.pathname, state}
})
}
const go = (n) => {
window.history.go(n)
}
const goBack = () => {
go(-1);
}
const goForward = () => {
go(1)
}
let history = {
action: "POP", // 当前最后一个动作是什么
location: {pathname: window.location.pathname, state: window.location.state},
go,
push,
goBack,
goForward,
listen,
isBrowser:true
}
return history
}
export default createBrowserRouter;
然后我们将hashRouter换成BrowserRouter看下效果
现在history路由也可以愉快玩耍了(图片里面的hashRouter忘记改为historyRouter!😂)
接下来我们来实现 Switch Redirect withRouter www.jianshu.com/p/8d3cf411a…
Switch 实现 (待修改)
在react-router中新建一个Switch.js文件
没有switch会全部匹配一遍,switch包裹后,只要有匹配的就不会继续匹配了
import React from 'react'
import RouterContext from './RouterContext'
import matchPath from './mathPath';
class Switch extends React.Component {
static contextType = RouterContext
render() {
const {location} = this.context;
let element, match;
React.Children.forEach(this.props.children, child => {
// 判断一个元素是不是一个合法的元素
if (!match && React.isValidElement(child)) { // 如果还没有任何一个元素匹配上
element = child;
match = matchPath(location.pathname, child.props)
}
})
return match ? React.cloneElement(element, {computedMatch: match}) : null;
}
}
export default Switch
Redirect 实现
在react-router中新建一个Redirect.js文件
<Redirect to="/Weclome" />
import { useContext } from 'react'
import LifeCycle from './LifeCycle'
import RouterContext from './RouterContext'
const Redirect = ({to}) => {
const { history } = useContext(RouterContext)
return (
<LifeCycle onMount={() => history.push(to)}></LifeCycle>
)
}
export default Redirect
withRouter 实现
在react-router中新建一个withRouter.js文件
withRouter 作用是将一个组件包裹进去,然后 history,location,match三个对象就会被放到这个组件的props属性中,如果我们某一个点击的不是router,但是在点击的时候需要跳转到一个页面,这个时候就要withRouter来做,例如现在的app,点击li的时候,拿不到histroy.push,所以需要withRouter包裹
// import { Route,
// } from 'react-router-dom'
import { HashRouter, Route } from './react-router-dom'
// import { HashRouter, Route, withRouter } from 'react-router-dom'
import Hello from './component/Hello'
import Weclome from './component/Weclome'
import Word from './component/Word'
const App = (props) => {
console.log(props, 'App的props') // {} 用withRouter包裹后就有值
const handlePush = () => {
console.log(window.history, '---->handlePush')
// 如果用window.history.pushState虽然路径变了,但是不会刷新组件,所以还是需要withRouter
window.history.pushState(null, null, '/Weclome')
}
return (
<>
<li onClick={handlePush}>Hello</li>
<li>word</li>
<li>Weclome</li>
<div>
-----------------------------------
</div>
<Route path="/" component={Hello}></Route>
<Route path="/Weclome" component={Weclome}></Route>
<Route path="/Word" component={Word}></Route>
</>
)
}
export default App;
我们来实现一下withRouter 草鸡简单直接将context的值传给组件就可
import React from 'react'
import RouterContext from './RouterContext'
function withRouter(OldComponent) {
return props => {
return (
<RouterContext.Consumer>
{
value => {
return <OldComponent {...props} {...value} />
}
}
</RouterContext.Consumer>
)
}
}
export default withRouter
实现效果
// app.js
import { HashRouter, Route, withRouter } from './react-router-dom'
// 中间省略
export default withRouter(App); //包裹一下即可
LifeCycle 实现
import React, { useEffect } from 'react'
const LifeCycle = (props) => {
useEffect(() => {
props.onMount && props.onMount()
return () => {
props.onUnmount && props.onUnmount()
}
},[])
return null;
}
export default LifeCycle
在react-router-dom 中还有 Link, NavLink 组件方法,我们也来实现以下
Link 实现
在react-router-dom中新建一个Link.js文件
import React, { useContext } from 'react'
import RouterContext from '../react-router/RouterContext'
const Link = (props) => {
const {history} = useContext(RouterContext)
const handleToLink = (e) => {
e.preventDefault()
history.push(props.to)
}
return (
<a {...props} onClick={handleToLink}>
{props.children}
</a>
)
}
export default Link
使用
<Link to="/Word"> to_word </Link>
NavLink 实现 (待修改)
在react-router-dom中新建一个NavLink.js文件
import React from 'react'
import Link from './Link'
import RouterContext, {} from '../react-router/RouterContext'
function NavLink(props) {
let context = React.useContext(RouterContext);
let {pathname} = context.location;
const {
to, // 匹配的路径
className:classNameProp="", // 原生类名
style: styleProp = {}, // 原始的行内样式对象
activeClassName="",
activeStyle={},
children,
exact
} = props;
// 匹配当前的路径和自己to路径 是否匹配
let isActive = matchPath(pathname, {path: to, exact});
let className = isActive? joinClassnames(classNameProp, activeClassName): classNameProp;
let style = isActive ? {...styleProp, ...activeStyle}: classNameProp;
let linkProps = {
className,
style,
to,
children
}
return <Link {...linkProps} />;
}
function joinClassnames(...classNames) {
// 把空的类名过滤掉
return classNames.filter(c => c).join(' ');
}
export default NavLink;
直接判断路径想等需要优化,使用mathPath
import pathToRegexp from 'path-to-regexp';
const cache = {};
function compilePath(path, options) {
let cacheKey = path + JSON.stringify(options)
if (cache[cacheKey]) return cache[cacheKey]
const keys = []; // 处理路径参数的
const regexp = pathToRegexp(path, keys, options);
let result = { keys, regexp }
cache[cacheKey] = result;
return result
}
/**
*
* @param {*} pathname 浏览器真实的路径
* @param {*} options Route组件的属性
* sensitive 是否大小写敏感
*/
function matchPath(pathname, options={}) {
let { path="/", exact= false, strict= false, sensitive=fasle } = options;
let { keys, regexp } = compilePath(path, {end: exact, strict, sensitive})
const match = regexp.exec(pathname)
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
// 如果要求精确,但是不精确,也返回null
if (exact && !isExact) return null;
return {
path, // Route里的path路径
url, // 浏览器地址中的url
isExact, // 是否精确匹配
params:keys.reduce((memo, key, index)=>{
memo[key.name] = values[index];
return memo
},{})
}
}
export default matchPath
- 指定一个route组件要渲染的内容有三种方式
- 1。component属性,值是一个组件的类型,他不能写定义的逻辑
-
- render属性,他是一个函数,如果路径匹配的话,就要渲染它这个函数的返回值‘
- 3, children属性,他也是一个函数
- render 是匹配才渲染,不匹配不渲染
- childern 不管匹配不匹配都渲染
// Route
import React from 'react'
import { Router } from 'react-router';
import RouterContext from './RouterContext'
import matchPath from './mathPath';
/**
* 获取context的值
* 匹配路由规则里面的path是否和当前地址中的url地址是不是相等,如果相等
* 就渲染component,不相等就不渲染
*/l
class Route extends React.Component {
static contextType = RouterContext
render() {
const { history, location } = this.context
const { exact, path, component: RouteComponent, computedMatch, render } = this.props;
// 在这里直接设置相等不合理,应该使用正则,这里用到 path-to-regexp库
// const match = location.patchname === path;
const match = computedMatch? computedMatch : matchPath(location.patchname, this.props)
let renderElement = null;
let routeProps = {
history,
location,
}
if (match) { //路径匹配才会进来
routeProps.match = match;
if (RouteComponent) {
renderElement = <RouteComponent {...routeProps} />;
} else if (render) {
renderElement = render(routeProps);
}
}
return renderElement
}
}
export default Route;
Prompt 实现(待修改)
import React from 'react'
import RouterContext from './RouterContext'
import lifeCyle from './Lifecycle'
import LifeCycle from './Lifecycle';
/**
*
* @param {*} when 布尔值,表示要不要阻止跳转
*
* @returns message 函数,表示要阻止的时候显示什么提示信息
*/
function Prompt({when, message}) {
return (
<RouterContext.Consumer>
{
value => {
if (!when) return null;
const block = value.history.block;
return (
<LifeCycle
onMount={self => { // self 是lifeCylcles的实例,this
self.release = block(message)
}}
onUnmount={self => self.release()}
/>
)
}
}
</RouterContext.Consumer>
)
}
export default Prompt
使用
<Prompt
when={this.state.isBlocking}
message={
location => `请问你是否真的要跳转到${location.pathname}?`
}
/>