了解前端路由,实现一个mini-react-router
了解前端路由,实现一个mini-react-router
前言
自从React、Vue、Angular框架盛行之后,大部分的前端工程都从由传统的多页面应用转型成单页面应用。且不讨论单页面应用和多页面应用的优势劣势,前端路由已经成为前端开发者和面试官们绕不过去话题。
为什么要有前端路由?
在单页面应用中,我们仅在首次加载的时候向服务端一次性拉取所有的静态资源。后续的页面跳转是不会再向服务端请求资源的 ,仅以切换组件重新渲染页面达到页面跳转的目的。所以我们前端就需要自己维护一套url和组件展示的对应关系来确定页面上需要展示什么组件,这就是前端路由。react-router
、vue-router
就是用来管理前端路由的前端路由库。

前端路由的实现核心原理
前端路由主要有两种模式histroy
模式和hash
模式。
history
模式的路由长这样:https://baidu.com/foo
hash
模式的路由长这样:https://baidu.com/#/foo
history模式原理
路由改变
其实history模式就是调用浏览器提供的history.pushState
这个API。该方法的作用是在历史记录中新增一条记录,同时改变浏览器地址栏的url,但是不刷新页面!
/**
* @params state. state 代表状态对象,这让我们可以给每个路由记录创建自己的状态,并且它还会序列化后保存在用户的磁盘上
*/
history.pushState(state, title[, url])
监听路由
同一个文档的 history
对象出现变化时,就会触发 popstate
事件 history.pushState
可以使浏览器地址改变,但是无需刷新页面。
window.addEventListener('popstate',function(e){
/* 监听改变 */
})
Tips:用
history.pushState()
或者history.replaceState()
不会触发popstate
事件。popstate
事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮或者调用history.back()、history.forward()、history.go()
方法。
hash模式原理
通过location.hash = 'foo'
这样的语法来改变,路径就会从https://www.baidu.com
改变成https://www.baidu.com/#foo
。
通过window.addEventListener('hashChange')
这个时间,就可以监听到hash值的变化,但是也不刷新页面!
react-router、react-router-dom、history库
history
库对浏览器原生的history对象进行了封装,是react-router
的核心库。
react-router
里封装了Router、Route、Switch
等核心组件,实现了从路由改变到组件更新的核心功能。是react-router-dom
的核心库。
react-router-dom
,在react-router
的基础上添加了用于跳转的Link
组件。除此之外还提供了history模式下的BrowserHistory
和hash模式下的HashRouter
组件。
实现一个mini-react-router
经过分析,一个mini-react-router应该具有以下功能:
- 路由切换的时候,不能刷新浏览器
- 路由切换的时候,被BrowserHistory包裹的组件需要监听到路由切换事件
- Route中需要在监听到路由发生变化的同时,拿到当前的路由信息,以此来判断是否将当前组件渲染出来
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
// import { BrowserRouter as Router, Route } from "./react-router-dom/index";
import Home from "./pages/Home";
import Login from "./pages/Login";
import Admin from "./pages/Admin";
// import Home from './pages/Home';
function App() {
return (
<Router>
<Route path="/home" component={Home} />
<Route path="/login" component={Login} />
<Route path="/admin" component={Admin} />
</Router>
);
}
export default App;
演示效果:
实现BrowserRouter
- 首先 BrowserRouter 在初始化的时候就需要监听
popState
事件,并且卸载的时候移除监听(注意,这个popState
不会响应js触发的history.pushState
,后面我们会讲到)。 - 在用户点击前进后退按钮的时候会触发
handlePopstate
函数,我们在函数内部通过location获取到路由名称pathname
,然后更新对应的state - 其次,BrowserRouter 的内部会维护一个
path
用来和浏览器上的path保持同步,同时需要将这个path往下透传给Route
(因为考虑到Route
层级可能过深,这里使用context)
export function BrowserRouter(props) {
// 首次渲染,获取到对应的路由
const [path, setPath] = useState(() => {
const { pathname } = window.location;
return pathname || "/";
});
// popState触发的时候更新path。(这个popState只能监听到浏览器的前进后退事件,通过js - history.pushState触发的监听不到)
const handlePopstate = (event) => {
const { pathname } = window.location;
setPath(pathname);
return () => {
window.removeEventListener("popstate", handlePopstate);
};
};
// 用来ui上点击的跳转
const push = (path) => {
// 这个push需要做两件事情:1. 调用 history.pushState API 改变浏览器上的路由 2. 改变browserHistory内部的路由
// tips: 通过 hisotry.pushState 没发触发 'popstate' 事件。所以改变内部路由需要手动 setPath();
window.history.pushState({ path }, "", path);
setPath(path);
};
const goBack = function () {
window.history.go(-1);
};
useEffect(() => {
window.addEventListener("popState", () => handlePopstate);
}, []);
// 将路由path通过 Context 传递给子组件
return (
<RouterContext.Provider
value={{
path,
history: {
push,
goBack,
},
}}
>
{props.children}
</RouterContext.Provider>
);
}
实现Route
- Route的逻辑非常简单,当path和当前的path匹配的时候,就展示出来,否则就返回null
- Route内部去消费Context传递下来的path和history(这也就是为什么挂载在Route上的组件的props中是有history对象的)
export function Route(props) {
console.log("Route props", props);
// 接受连个参数 要渲染的组件:component. 展示的路径:path
const { component: Component, path: componentPath } = props;
return (
<RouterContext.Consumer>
{({ path, history }) => {
// 只有路径匹配上的时候才展示对应的组件
return componentPath === path ? <Component history={history} /> : null;
}}
</RouterContext.Consumer>
);
}
实现Link
- 我们知道,Link实际上是用来做前端路由跳转的。调用的时候传一个
to
表示要跳转的路径 - 我们Link内部需要拿到history对象,使用提前封装好的push函数进行跳转。保证即改变了浏览器的路由、又保证内部的组件重新根据最新的path重新渲染。
export function Link(props) {
const { to, children } = props;
const goToPage = (history) => {
history.push(to);
};
return (
<RouterContext.Consumer>
{/* 这里的history是通过 BrowserHistory 传递下来的 */}
{({ path, history }) => {
// 只有路径匹配上的时候才展示对应的组件
return React.createElement('div', {
history,
onClick: () => {
goToPage(history)
},
}, children)
}}
</RouterContext.Consumer>
);
}
完整代码
import React, { useState, useEffect, createContext } from "react";
const RouterContext = createContext();
export function Link(props) {
const { to, children } = props;
const goToPage = (history) => {
history.push(to);
};
return (
<RouterContext.Consumer>
{/* 这里的history是通过 BrowserHistory 传递下来的 */}
{({ path, history }) => {
// 只有路径匹配上的时候才展示对应的组件
return React.createElement('div', {
history,
onClick: () => {
goToPage(history)
},
}, children)
}}
</RouterContext.Consumer>
);
}
export function Route(props) {
console.log("Route props", props);
// 接受连个参数 要渲染的组件:component. 展示的路径:path
const { component: Component, path: componentPath } = props;
return (
<RouterContext.Consumer>
{({ path, history }) => {
// 只有路径匹配上的时候才展示对应的组件
return componentPath === path ? <Component history={history} /> : null;
}}
</RouterContext.Consumer>
);
}
export function BrowserRouter(props) {
// 首次渲染,获取到对应的路由
const [path, setPath] = useState(() => {
const { pathname } = window.location;
return pathname || "/";
});
// popState触发的时候更新path。(这个popState只能监听到浏览器的前进后退事件,通过js - history.pushState触发的监听不到)
const handlePopstate = (event) => {
const { pathname } = window.location;
setPath(pathname);
return () => {
window.removeEventListener("popstate", handlePopstate);
};
};
// 用来ui上点击的跳转
const push = (path) => {
// 这个push需要做两件事情:1. 调用 history.pushState API 改变浏览器上的路由 2. 改变browserHistory内部的路由
// tips: 通过 hisotry.pushState 没发触发 'popstate' 事件。所以改变内部路由需要手动 setPath();
window.history.pushState({ path }, "", path);
setPath(path);
};
const goBack = function () {
window.history.go(-1);
};
useEffect(() => {
window.addEventListener("popState", () => handlePopstate);
}, []);
// 将路由path通过 Context 传递给子组件
return (
<RouterContext.Provider
value={{
path,
history: {
push,
goBack,
},
}}
>
{props.children}
</RouterContext.Provider>
);
}
在线演示地址
参考链接
转载自:https://juejin.cn/post/7166638351125053470