Hash Router 实现
实现思路:监听路由 hash 的变化,调用路由对应的回调函数。
实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
class HashRouter { constructor() { this.routes = {}; this.currentUrl = ""; this._listenLoadAndHashChange(); }
_listenLoadAndHashChange() { window.addEventListener("load", () => this.refresh(), false); window.addEventListener("hashchange", () => this.refresh(), false); }
refresh() { this.currentUrl = window.location.hash.slice(1);
const callback = this.routes[this.currentUrl]; if (typeof callback === "function") { callback(); } }
registerRoute(path, callback) { this.routes[path] = typeof callback === "function" ? callback : () => {}; } }
|
使用例子(分为 js 和 html)
在 js 中:
- 初始化 hash router
- 注册路由对应的回调函数(回调函数中进行页面的渲染等逻辑)
1 2 3 4 5 6 7 8 9 10 11 12
| const hashRouter = new HashRouter();
hashRouter.registerRoute("/", () => { console.log("加载 #/ 对应的页面逻辑"); document.querySelector("body").style.backgroundColor = "white"; });
hashRouter.registerRoute("/blue", () => { console.log("加载 #/blue 对应的页面逻辑"); document.querySelector("body").style.backgroundColor = "blue"; });
|
在 html 中:当点击「首页」或者「blue 页面」链接时,执行上方绑定的回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <!DOCTYPE html> <html> <head> <title>Parcel Sandbox</title> <meta charset="UTF-8" /> <script src="./script.js"></script> </head>
<body> <ul> <li><a href="#/">首页</a></li> <li><a href="#/blue">blue页面</a></li> </ul> </body> </html>
|
History Router 实现
这里模拟 react-router-dom 的 API,封装自己的<Link />
和<Router />
。
在 Hash Router 中,通过监听 hashchange 和 load 的事件,可以响应所有的路由 hash 变化。在 History Router 中,history 模式单页路由和 hash 模式的单页路由类似,都是通过事件回调,来监听路由(hash 或者 url)的变化,进而进行路由的渲染工作。
但是对于 history 模式的路由,要考虑的情况有些复杂:
- 通过 js 代码,直接调用方法,进行跳转
- 用户点击 a 标签,进行跳转
- 用户点击浏览器的前进/后退按钮
但在 history 路由中,情况有些不同。根据上面三种情况,解决方案细节分别是:
- 暴露内置的 js 方法,用来进行全部组件的重新渲染(匹配当前路由的组件才会重新渲染)
- 包装浏览器的 a 标签,阻止 a 标签点击的默认行为,而是执行全部组件的重新渲染(匹配当前路由的组件才会重新渲染)
- 前进/后退会触发 popstate 事件,在事件回调中执行全部组件的重新渲染(匹配当前路由的组件才会重新渲染)
代码实现:分别实现了<MiniHistoryRoute />
和<MiniHistoryLink />
这 2 个函数组件,以及 1 个用于 js 路由跳转的push()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
import React, { useEffect, useState } from "react";
const strictMatch = (path) => window.location.pathname === path;
const routeForceUpdateMap = {};
const forceUpdateRoutes = () => { Reflect.ownKeys(routeForceUpdateMap).forEach((route) => routeForceUpdateMap[route]() ); };
window.addEventListener("popstate", () => { console.log("触发 popstate"); forceUpdateRoutes(); });
export const MiniHistoryRoute = (props) => { const { path, component } = props || {};
const [_, forceUpdate] = useState();
useEffect(() => { routeForceUpdateMap[path] = () => forceUpdate(Date.now());
return () => { delete routeForceUpdateMap[path]; }; }, []);
return strictMatch(path) ? React.createElement(component) : null; };
export const MiniHistoryLink = (props) => { const { to } = props;
const handleLinkClick = (event) => { event.preventDefault(); window.history.pushState({}, null, to); forceUpdateRoutes(); };
return ( <a href={to} target="_self" onClick={handleLinkClick}> {props.children} </a> ); };
export const push = (to) => { window.history.pushState({}, null, to); forceUpdateRoutes(); };
|
使用案例:当点击 a 链接的时候,会渲染组件 A;同理,b 和 c 链接渲染对应的 B 和 C 组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import React from "react"; import ReactDOM from "react-dom"; import { MiniHistoryRoute, MiniHistoryLink } from "./mini-history-router";
const A = () => <div>A</div>;
const B = () => <div>B</div>;
const C = () => <div>C</div>;
const App = () => { return ( <> <ul> <li> <MiniHistoryLink to="/a">a</MiniHistoryLink> </li> <li> <MiniHistoryLink to="/b">b</MiniHistoryLink> </li> <li> <MiniHistoryLink to="/c">c</MiniHistoryLink> </li> </ul> <MiniHistoryRoute path="/" component={A} /> <MiniHistoryRoute path="/a" component={A} /> <MiniHistoryRoute path="/b" component={B} /> <MiniHistoryRoute path="/c" component={C} /> </> ); };
const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
|
题外话:怎么在 react hooks 中强制重新更新组件?
1、在 Class 时代,可以通过调用实例上的 forceUpdate()方法,来触发组件的强制更新
2、在 Hooks 时代,组件状态的改变,就会触发更新。所以通过改变状态属性即可。
参考链接