# 👉 前端路由实现

# 什么是前端路由

在 SPA 当中,路由描述了 URL 和对应页面的映射关系,单向映射。

现在单页面应用能够模拟多页面应用的效果,归功于其前端路由机制。现在前端路由有两种形式:Hash / History。

实现核心:

  1. 如何改变 URL 却不引起页面刷新?
  2. 如何检测 URL 变化了?

# hash

#,原来常用于锚点导航至页面具体元素。hash 是基于监听 hashchange 事件实现。通过监听 url 变化,可以触发相应 hashchange 回调函数。

# hash 实现思路:

  1. 监听 load 事件,第一次进入页面/刷新时,需要主动触发一次 onHashChange 事件,保证页面能够正常显示;
  2. 监听 hashChange,触发 onHashChange,更新视图;
<ul>
    <li><a href="#/">home</a></li>
    <li><a href="#/about">about</a></li>
    <li><a href="#/topics">topics</a></li>
</ul>
<div id="routeView"></div>

<script type="text/javascript">
    function myRouter() {
        this.routersMap = {};
        this.currentUrl = "";

        this.add = function(path, callback) {
            this.routersMap[path] = callback;
        };

        this.init = function() {
            window.addEventListener(
                "DOMContentLoaded",
                this.updateView(),
                false
            );
            window.addEventListener("hashchange", this.updateView(), false);
        };

        this.updateView = function() {
            this.currentUrl = location.hash.slice(1) || "/";
            this.routersMap[this.currentUrl] &&
                this.routersMap[this.currentUrl]();
        };
    }

    let routerView = document.querySelector("#routeView");
    const router = new myRouter();
    router.add("/about", function() {
        routerView.innerText = "about";
    });
    router.add("/topics", function() {
        routerView.innerText = "topics";
    });
    router.add("/", function() {
        routerView.innerText = "home";
    });

    router.init();
</script>

# history

HTML5 的 History API 为浏览器的全局 history 对象增加的扩展方法。

History 路由是基于 HTML5 规范,在 HTML5 规范中提供了history.pushStatehistory.replaceState来进行路由控制。

# history 的属性:

history.state:获取当页 state 信息(页面间可以通过 state 传递信息)
hstory.length:会话历史记录数目

# history 的方法:

  • history.back / history.forward / history.go(0/1) 0 代表刷新半页,-1 后退一页,以上三个方法能触发 popState

  • history.pushState(state [,title] [,url]):向历史记录里增加一条记录

    history.replaceState(state [,title] [,url]):替换当前页在历史的记录

    这两个方法不会刷新页面,也不会触发 popState 回调

    参数说明如下:
    state:存储 JSON 字符串,可以用在 popstate 事件中
    title:现在大多浏览器忽略这个参数,直接用 null 代替
    url:任意有效的 URL,用于更新浏览器的地址栏

    history.pushState({}, null, '/about')  时候,页面 url 会从  http://xxxx/  跳转到  http://xxxx/about  可以在改变 url 的同时,并不会刷新页面。

# history 实现思路

  1. 监听 window.popstate,触发 handlePopstate,更新视图(点击浏览器按钮和 js 调用 foward/go/back 会触发 popstate)

  2. 监听 load 事件,第一次进入页面/刷新时,需要主动触发 handlLoad 事件;

  3. handlLoad 事件中,对所有有 href 属性的 a 标签进行 click 事件监听,拦截原来 default 事件(跳转链接并刷新页面)
    调用 window.history.pushState 更新路由(不刷新页面)和 handlePopstate 更新路由对应的试图。

<script type="text/javascript">
    function myRouter() {
        this.routersMap = {};
        this.currentUrl = "";

        this.add = function(path, callback) {
            this.routersMap[path] = callback;
        };

        // 不同的地方在于 init 初始化函数,首先需要获取所有特殊的链接标签,然后监听点击事件,并阻止其默认事件,触发 history.pushState 以及更新相应的视图。
        this.init = function() {
            window.addEventListener("DOMContentLoaded", this.load(), false);
            window.addEventListener("popstate", this.updateView(), false);
        };

        this.load = function() {
            this.updateView();

            const _this = this;

            // 拦截 <a> 标签点击事件默认行为,点击时使用 pushState 修改URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
            const aLinks = document.querySelectorAll("a[href]");
            for (let i = 0; i < aLinks.length; i++) {
                const link = aLinks[i];
                link.addEventListener("click", function(e) {
                    e.preventDefault();

                    window.history.pushState(
                        { state: link.getAttribute("href") },
                        "",
                        link.getAttribute("href")
                    );
                    _this.updateView();
                });
            }
        };

        this.updateView = function() {
            console.log(1, location.pathname);
            this.currentUrl = location.pathname || "/";
            this.routersMap[this.currentUrl] &&
                this.routersMap[this.currentUrl]();
        };

        this.getRouterPath = function() {
            return window.location.pathname || "/";
        };

        this.pushRoute = function(path) {
            // 因为pushState不会触发popState,所以需要手动updateView
            window.history.pushState(null, null, path);
            this.updateView();
        };

        this.go = function(delta) {
            window.history.go(delta);
        };

        this.push = function() {
            window.history.go(1);
        };

        this.back = function() {
            window.history.back();
        };

        this.replace = function(path) {
            // 因为replaceState不会触发popState,所以需要手动updateView
            window.history.replaceState(null, null, path);
            this.updateView();
        };
    }

    const router = new myRouter();
    router.init();

    router.add("/about", function() {
        document.getElementById("content").innerHTML = "about";
    });
    router.add("/topics", function() {
        document.getElementById("content").innerHTML = "topics";
    });
    router.add("/", function() {
        document.getElementById("content").innerHTML = "home";
    });
</script>