React 路由实现的两种方式
实现机制
HashRouter: 基于window.onhashchange实现BrowserRouter:基于HTML5 popstate event实现
相同点:
- 都是一个
Router组件容器,通过创建上下文的方式,传递如下数据给后代组件使用; - 都需要在组件挂载时机
componentDidMount/useEffect,订阅 路由变化 的函数,修改相应的 路径 匹配渲染对应的组件
不同点:
HashRouter监听的是hashchange事件BrowserRouter监听的是pushstate&popstate事件
注意:
HTML5中并没有pushstate原生事件, 需要自行实现。
下文中实现的方式均为 Functional Component 函数式组件
准备上下文
RouterContext.js
import { createContext } from 'react';
export default createContext();
HashRouter 实现
当 一个窗口的 hash (URL 中 # 后面的部分)改变时就会触发 hashchange 事件
import React, { useEffect, useState } from 'react';
import RouterContext from './RouterContext';
export default function () {
const location = {
pathname: window.location.hash.slice(1) || '/',
state: null,
}
let locationState = null;
let [ initialLocation, setInitialLocation ] = useState(location);
useEffect(() => {
window.addEventListener('hashchange', () => {
setInitialLocation({
...initialLocation,
pathname: window.location.hash.slice(1) || '/',
state: locationState,
})
})
window.location.hash = window.location.hash || '#/';
// 赋值时加 #, 取值时无 #
});
const history = {
location: initialLocation,
push(to) {
if (history.prompt) {
const target = typeof to === 'string' ? { pathname: to } :to;
const yes = window.confirm(prompt(target));
if (!yes) return;
}
if (typeof to === 'object') {
const { pathname, state } = to;
locationState = state;
window.location.hash = pathname;
} else {
window.location.hash = to;
}
},
block(prmopt) {
history.prompt = prompt;
},
unblock() {
history.prompt = null;
},
}
const routerValue = {
history,
location: initialLocation
};
return (
<RouterContext value = { routerValue }></RouterContext>
)
}
BrowserRouter 实现
调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件. popstate 事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用 history.back()、history.forward()、history.go()方法).
import RouterContext from "./RouterContext"
import React, { useEffect, useState } from "react"
export default function (props) {
let location = {
pathname: window.location.pathname,
state: null,
}
let [ initialLocation, setInitialLocation ] = useState( location );
useEffect(() => {
window.onpushstate = (state, pathname) => {
setInitialLocation({
...initialLocation,
pathname,
state,
})
}
window.onpopstate = (event) => {
setInitialLocation({
...initialLocation,
pathname: window.location.pathname,
state: event.state,
})
}
});
const globalHistory = window.history;
let history = {
location: initialLocation,
push(to) {
if (history.prompt) {
let target = typeof to === 'string' ? { pathname: to } : to;
let yes = window.confirm(history.prompt(target));
if (!yes) return;
}
if (typeof to === 'object') {
let { pathname, state } = to;
globalHistory.pushState(state, null, pathname);
} else {
globalHistory.pushState(null, null, to);
}
},
block(prompt) {
history.prompt = prompt;
},
unblock() {
history.prompt = null;
}
}
const routerValue = {
history,
location: initialLocation,
};
return (
<RouterContext.Provider value={ routerValue }>
{ props.children }
</RouterContext.Provider>
)
}
重写 pushState 方法
方法体内部增加 onpushstate 触发事件
index.html
<script>
!( history => {
const pushState = history.pushState;
history.pushState = function (state, title, url) {
if (typeof window.onpushstate === 'function') {
window.onpushState(state, url);
}
pushState.apply(history, arguments);
}
})(window.history)
</script>