Appearance
实现history
本节目标:理解“实现history”的核心思路,并能把它用于实际开发或面试表达。
说明:原 OneNote 中有图片引用,但图片未包含在导出目录中;本页保留了可用的文字与代码内容。
Src\index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
ReactDOM.render(
<Router>
<Route path="/" component={Home} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />
</Router>,
document.getElementById('root')
);其中,react-router-dom中导出了两种router,HashRouter和
BrowserRouter:
Src\react-router-dom\index.js
export * from '../react-router';
export{ default as HashRouter } from
'./HashRouter';
export{ default as BrowserRouter } from
'./BrowserRouter';对于第一行代码,我们导出了react-router中的所有导出:
Src\react-router\index.js
export{ default as Router } from
'./Router';
export{ default as Route } from
'./Route';
export{ default as __RouterContext } from
'./RouterContext';所以,react-router-dom中包含了react-router的内容; 在入口文件的render方法中,我们使用了Router和Route组件;
我们看一下
Src\react-router-dom\BrowserRouter.js
importReact from
'react';
import{ Router } from
'../react-router';
import{ createHashHistory } from
'../history';
classHashRouter extends
React.Component{history =
createHashHistory()//HashRouter的history实例属性会指向用hash实现的历史对象
render(){return (
<Routerhistory={this.history}>{this.props.children}</Router> `)`
}
}
exportdefault
HashRouter;
/*** createHashHistory和createBrowserHistory* 都 会反回一个history对象,对象的方法和API是完全 相同 的,只是内闻的实现原理不一样 */
这里面又用到了
Src\react-router\Router.js
import React from 'react';
import RouterContext from './RouterContext';
class Router extends React.Component {
constructor(props) {
super(props);
this.state = {
location: props.history.location
}
//监听历史对象路径变化,如果路径发生变化的话执行回调
this.unlisten = props.history.listen((location) => {
this.setState({ location })
});
}
componentWillUnmount() {
this.unlisten && this.unlisten();
}
render() {
let value = { history: this.props.history, location: this.state.location };
return (
<RouterContext.Provider value={value}>
{this.props.children}
</RouterContext.Provider>
)
}
}
export default Router;react-router-dom\HashRouter.js
import React from 'react';
import { Router } from '../react-router';
import { createBrowserHistory } from '../history';
class BrowserRouter extends React.Component {
history = createBrowserHistory()//HashRouter的history实例属性会指向用hash实现的历史对象
render() {
return (
<Router history={this.history}>
{this.props.children}
</Router>
)
}
}
export default BrowserRouter;我们要使用Router时,会给它传入的props中,存在一个history属性,它是由我们的createBrowserHistory()方法创建的; 其实BrowserRouter和HashRouter的区别在于,它们render返回中的Router的props中的history是由不同的方法创建的,分别是createBrowserHistory和createHashHistory;
Src\react-router\RouterContent.js
import React from 'react';
export default React.createContext({});分别了解一下这两个方法返回的history的异同; 进入到
Src\history\createBrowserHistory.js
function createBrowserHistory() {// react-router-dom用于浏览器环境,利用浏览器的location对象和history对象进行页面前进后退及跳转,及手动修改location值;
const globalHistory = window.history;
let listeners = [];//存放所有的监听函数
let state;
function listen(listener) {
listeners.push(listener);// 返回取消监听的函数
return () => {// 过滤掉当时监听的回调函数
listeners = listeners.filter(item => item != listener);
}
}
function go(n) {
globalHistory.go(n);
} function goBack() {
go(-1);
} function goForward() {
go(1);
}// 按前进后退按钮或使用js调用以上3个跳转方法,都会触发popState事件
// 默认情况下state值为空,如果使用pushState或replaceState方法,可以定制State
window.addEventListener('popstate', () => {//TODO
let location = { state: globalHistory.state, pathname: window.location.pathname };
//当路径改变之后应该让history的监听函数执行,重新刷新组件
notify({ action: "POP", location });
}); function notify(newState) {
//把newState上的属性赋值到history对象上
Object.assign(history, newState);
history.length = globalHistory.length;//路由历史栈中历史条目的长度
listeners.forEach(listener => listener(history.location));//通知监听函数执行,参数是新的
location
} function push(pathname, nextState) {//TODO
const action = 'PUSH';//action表示是由于什么样的动作引起了路径的变更
if (typeof pathname === 'object') {
state = pathname.state;
pathname = pathname.pathname;
} else {
state = nextState;//TODO
}
globalHistory.pushState(state, null, pathname);//我们已经跳转路径
let location = { state, pathname };
notify({ action, location });
} const history = {
action: 'POP',
go,
goBack,
goForward,
push,
listen,
location: { pathname: window.location.pathname, state: window.location.state }
}
return history;
}
export default createBrowserHistory;新建
src\history\index.js
export { default as createHashHistory } from './createHashHistory';
export { default as createBrowserHistory } from './createBrowserHistory';Src\history\createHashHistory.js
/**
* hash不能使用 浏览器的history对象了
* @returns
*/
function createHashHistory() {
let stack = [];//类似于历史栈 里面存放都是路径
let index = -1;//栈的指针,默认是
-1
let action = 'POP';//动作
let state;//最新的状态
let listeners = [];//监听函数的数组
function listen(listener) {
listeners.push(listener);
return () => {
listeners = listeners.filter(item => item != listener);
}
} //当hash发生变化的话,会执行回调
window.addEventListener('hashchange', hashChangeHandler); let hashChangeHandler = () => {
//取出最新的hash值
let pathname = window.location.hash.slice(1);
Object.assign(history, { action, location: { pathname, state } });
if (action === 'PUSH') {//说明是调用push方法,需要往历史栈中添加新的条目
stack[++index] = history.location;
}
listeners.forEach(listener => listener(history.location));
} function go(n) {
action = 'POP';
index += n;//更改栈顶的指针
let nextLocation = stack[index];//取出指定索引对应的路径对象
state = nextLocation.state;//取出此location对应的状态 // 以下语句可直接修改hash值,从而触发onhashchange事件 // 网址也可以通过window.location=xx来进行修改
window.location.hash = nextLocation.pathname;
} function push(pathname, nextState) {
action = 'PUSH';
if (typeof pathname === 'object') {
state = pathname.state;
pathname = pathname.pathname
} else {
state = nextState;
}
window.location.hash = pathname;
} function goBack() {
go(-1);
}
function goForward() {
go(1);
} const history = {
action: 'POP',
go,
goBack,
goForward,
push,
listen,
location: {},
location: { pathname: '/', state: undefined }
}
if (window.location.hash) {//如果初始的情况下,如果hash是有值的
action = 'PUSH';
hashChangeHandler();
} else {
window.location.hash = '/';
}
return history;
}
export default createHashHistory;再看看我们的 Src\react-router\Router.js 在构造函数中,我们已经进行了location的监听;
import React from 'react';
import RouterContext from './RouterContext';
class Router extends React.Component {
constructor(props) {
super(props);
this.state = {
location: props.history.location
}
//监听历史对象路径变化,如果路径发生变化的话执行回调
this.unlisten = props.history.listen((location) => {
this.setState({ location })
});
}
componentWillUnmount() {
this.unlisten && this.unlisten();
}
render() {
let value = { history: this.props.history, location: this.state.location };
return (
<RouterContext.Provider value={value}>
{this.props.children}
</RouterContext.Provider>
)
}
}
export default Router;Src\react-router\route.js
import React from 'react';
import RouterContext from './RouterContext';
import matchPath from './matchPath';
class Route extends React.Component {
static contextType = RouterContext;
render() {
const { history, location } = this.context;
const { path, component: RouteComponent, exact = false } = this.props;
//const match = exact?location.pathname===path:location.pathname.startsWith(path);// /user /user
const match = matchPath(location.pathname, this.props);
const routeProps = { history, location };
let renderElement = null;// null也一个合法的react渲染节点 代表我们render的返顺值,代表此组件将要渲染的内容
if (match) {
routeProps.match = match;
//React.createElement(RouteComponent,routeProps);
renderElement = <RouteComponent {...routeProps} />
}
return renderElement
}
}
export default Route;Src\react-router\matchPath.js
import pathToRegexp from 'path-to-regexp';
function compilePath(path, options) {
const keys = [];
const regexp = pathToRegexp(path, keys, options);
return { regexp, keys };
}
function matchPath(pathname, options = {}) {
const { path = "/", exact = false, strict = false, sensitive = false } = options;
const { regexp, keys } = compilePath(path, { end: exact, strict, sensitive });
const match = regexp.exec(pathname);
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;//
if (exact && !isExact) return null;//如果希望精确,但其实不精确返回确
return {
path,//Route里的path属性
url,//正则匹配到的浏览器的pathname部分
isExact,//是否实现了精确匹配
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index]
return memo;
}, {})
}
}
/**
*浏览器的
pathname /user/1
* path /user
* match是能匹配上的
* exact=true;
* /user/1不完全 相等
/user表示非精确匹配
*
*
*
*
* Home path = /
* location.pathname /user
*匹配的部分就是
/
* / === /user不相等就是
false
*/
export default matchPath;