文章

手搓 React Router 简易源码

手搓 React Router 简易源码

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
/*
 * @Author: dongyuanxin
 * @Date: 2021-01-07 23:15:50
 * @Github: https://github.com/dongyuanxin/blog
 * @Blog: https://dongyuanxin.github.io/
 * @Description: react-router hash形式实现
 */

class HashRouter {
    constructor() {
        this.routes = {};
        this.currentUrl = "";
        this._listenLoadAndHashChange();
    }

    /**
     * 为什么要监听load事件?
     * 当页面初次进入的时候,会触发load事件。之后路由上hash值改变,会触发hashchange事件。
     */
    _listenLoadAndHashChange() {
        window.addEventListener("load", () => this.refresh(), false);
        window.addEventListener("hashchange", () => this.refresh(), false);
    }

    /**
     * 匹配当前路由,执行对应路由的回调函数,来进行页面渲染的操作
     */
    refresh() {
        // 切掉hash上的#字符:'#123' => '123'
        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
/*
 * @Author: dongyuanxin
 * @Date: 2021-01-08 19:09:34
 * @Github: https://github.com/dongyuanxin/blog
 * @Blog: https://dongyuanxin.github.io/
 * @Description: 封装自己的Router和Link组件
 */
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();

    // 在第一次渲染的时候,将此路由的强制刷新函数保存下来
    // 以便在路由变化的时候调用,从而触发当前MiniHistoryRoute的更新
    useEffect(() => {
        routeForceUpdateMap[path] = () => forceUpdate(Date.now());

        return () => {
            delete routeForceUpdateMap[path];
        };
    }, []);

    // 为了方便演示,这里仅支持最简单的严格匹配
    return strictMatch(path) ? React.createElement(component) : null;
};

export const MiniHistoryLink = (props) => {
    const { to } = props;

    // 拦截 a 标签的默认操作
    // 调用 pushState 将url推入浏览器路由栈,并且强制重新渲染
    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 时代,组件状态的改变,就会触发更新。所以通过改变状态属性即可。

参考链接

本文由作者按照 CC BY 4.0 进行授权