前端路由

484 阅读6分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

路由的概念

其实早先年没有前端路由的概念,只是随着前端的发展,才有了前端路由。(鲁迅说的)

最早做模版引擎开发的时候,会看到这样的页面url: http://test.xxx.help.cn/bbs/sage.php 或者是http://home.xxx.cn/bbs/sage.html像这样以.php或者是.html的路径,就是通过服务器端渲染,直接返回一个页面。虽然这样利于SEO搜索,但是代码不方便维护,也会加大服务器压力。

ajax和 SPA

前端路由发展起来离不开ajax的普及。在http0.9(更早)时代都是直接返回一个html页面。用户的每次更新操作都需要重新刷新页面,及其影响交互体验。随着网络的发展,迫切需要一种方案来改善这种情况。 有了 Ajax 后,用户交互就不用每次都刷新页面,体验带来了极大的提升。随着就有了单页面应用SPA。

什么是SPA?

简单点来说;在SPA应用中,只有一个hTML页面,在HTML页面中包含一个app,也就是占位符,页面的切换就是app内的切换,也就是view的切换。如图:

1626170999317_BD6DD7A6-03A3-42C0-B77B-F5C66E97B0B4.png

通过view的切换可以起到页面的改变。但是有问题: 单单是通过js和html配合实现,页面url没有变化,对浏览器没有任何影响,浏览器无法记住用户对操作。用户的回退,前进操作都无效,很尴尬。而且url没有变化的话,一个页面只有一个url,也不利于SEO。蛋疼。。 为了解决以上问题,引出了前端路由的概念。其实只要做到可以监听url变化,同时不会发送请求,就可以完成一个简单的前端路由。 重点来啦,介绍实现前端路由的两种方式:hash和history。

hash模式

hash最早的路由解决方法,www.baidu/#/xx/xx 通过改变锚点#后面的hash值来实现url的改变,并且不会刷新页面。 通过window.location.hash可以获取到hash值,然后监听hashChange事件。当hasChange事件触发的时候更新对应的hash值,就可以实现前端路由功能。好的,不多bb。我们来实现一下:

实现hash

我们的想法是维护一个对象,用来储存path(用来匹配hash), view(匹配到hash的时候渲染)。

    class MyHashRouter {
           constructor() {
               this.router = {}; //存储注册路由 
           }
           window.addEventListener('hashChange', this.loadView.bind(this), false);
    }

很简单,通过监听hashChange事件的改变,来触发loadView(通过路由匹配view)。注意一下router对象:

    router {
        path: "/XX",  // 路由的路径
        callBack: () => {} // 一个执行函数(用来显示view)
    }

实现一下注册路由:

    registerRouter(path, callBack = ()=>{}) {
        this.router[path] = callBack; 
                    // 一个路由对应一个view(一个萝卜一个坑)
    }

要注意一个问题:path如果不存在的时候,我们要默认为/,也就是首页。

    registerIndexRouter(callBack = () => {}) {
        this.router['index'] = callBack;   // index默认为首页
    }

ok!简单的路由注册完成。现在我们要考虑一下路由切换view切换问题。通过a标签实现url的hash改变,在hash改变的同时我们获取到hash,通过hash来匹配到我们注册的路由。代码走起:!

    loadView() {
        let hash = window.location.hash.slice(1); // 去除#号
        let cb;  // 对应执行函数(view)
        hash ? cb = this.router[hash] : cb = this.router.index;
        // hash存在:找对应的view,否则对应index
        cb.call(this);  // 执行
    }

这样一个简单的hash路由就是完成了。但是要注意个问题:如果匹配的路由没有匹配到,默认跳转到404页面。

    hashNotFound() {
        if (!this.router.hasOwnProperty(hash) {
             cb = this.routers['404'] || function(){};
        }
    }

code如下:

    class MyHashRouter {
        constructor(){
            this.router = {};
            window.addEventListener('hashChange', this.loadView.bind(this), false);
        }
        registerIndexRouter(callBack = () => {}) {
            this.router['index'] = callBack;
        }
        registerRouter(path, callBack = () => {}) {
            this.router[path] = callBack;
        }
        registerNotFound(callback = () => {}){
            this.routers['404'] = callback;   // 注册404
        }
        loadView() {
            let hash = window.location.hash.slice(1);
            let cb;
            if (!hash) {
                cb = this.router.index;
            }
            else if (!this.router.hasOwnProperty(hash) && hash) {
                cb = this.router['404'];
            }
            else {
                cb = this.router.hash;
            }
            cb.call(this);
        }
    }

我们可以简单的玩一下:

            let router = new MyHashRouter();
            let app = document.getElementById('app');

            //首页
            router.registerIndexRouter(()=> app.innerHTML = '首页');

            //注册其他视图
            router.registerRouter('/path1',() => app.innerHTML = 'view1');
            router.registerRouter('/path2',()=> app.innerHTML = 'view2');
            router.registerRouter('/path3',()=> app.innerHTML = 'view3');
            router.registerRouter('/path4',()=> app.innerHTML = 'view4');
            router.registerNotFound(()=> app.innerHTML = '页面未找到'); // 404
            router.loadView();  //加载视图

history模式

history api

DOM window 对象通过 history 对象提供了对浏览器的会话历史的访问(不要与 WebExtensions history搞混了)。它暴露了很多有用的方法和属性,允许你在用户浏览历史中向前和向后跳转,同时——从HTML5开始——提供了对history栈中内容的操作。 --MDN

                window.history.back()   ===  浏览器回退
                window.history.forward()  === 浏览器前进
                window.history.go(-1) === 向后移动一个页面  === back()
                window.history.go(1) === 向前移动一个页面 === forward()
                go(2/-2) ===  跳转到 history 中指定的一个点

在HTML5中新增加属性history.pushState()和history.replaceState()它们分别可以添加和修改历史记录条目。注意:这个方法配合window.onpopstate使用

history.pushState(state, title, url) 和 history.replaceState(state, title, url) 接收三个参数:

  1. state: 合法的 Javascript 对象,可以用在 popstate 事件中
  2. null(。。。)
  3. url:任意有效的 URL,用于更新浏览器的地址栏

pushState()会把现有的url保存在栈里。调用 pushState() 与 设置 window.location = "#foo" 类似,二者都会在当前页面创建并激活新的历史记录。但是相比之下pushState有如下的优点:

  1. 新的 URL 可以是与当前URL同源的任意URL。
  2. 如果你不想改URL,就不用改。相反,设置 window.location = "#foo";在当前哈希不是 #foo 时, 才能创建新的历史记录项。

replaceState() 会将历史记录中的当前页面历史替换为url。 ok!现在我们知道了,replaceState()和pushState()可以在修改url的同时,不进行页面刷新。所以我们可以使用history这两个属性实现前端路由。 这里有个问题:

在hash模式中,我们通过监听hashChange事件,来完成url改变,change对应的view。但是在history模式中,我们没有办法监听history。咋整呀!!!

可以这样处理,我们通过监听所有会引起history改变的方法,将这些方法拦截,监听。这样就可以做到变相拦截history了。

在HTML5中有以下的方式会触发history改变:

  1. a标签点击跳转
  2. 浏览器回退,前进事件。
  3. 触发了pushState()和replaceState()方法

ok,现在我们来简单实现一下history模式。 注册router对象和之前的hash模式都是基本一样,这里就不过多BB了。主要是拦截可以触发history改变的方法。

通用处理路径和view

没什么好说的,path改变view改变。

    commonDealPath(path) {
        let view;
        if(!this.router.hasOwnProperty(path)){
            view = this.routers['404'] || function(){};
        }
        else{
            view = this.routers[path];   //有对应path
        }
        view.call(this);
    }

全局监听a链接

注意两点:1. 获取a标签的href属性的value。 2. 阻止a标签的默认事件。

    listenerAClick() {
        window.addEventListener('click', (e) => {
           if (e && e.target) {
               let target = e.target;  // 获取click事件
               if(target.tagName.toUpperCase() === 'A' && target.getAttribute('href')){
                   let path = target.getAttribute('href')); // 获取a标签的href属性。
                   history.pushState({path}, null, path);   // 通过pushState改变url。
                   this.commonDealPath(path);  // 触发commonDealPath
               }
           }
        }
    }

监听浏览器事件

监听浏览器前进和后退,也就是监听popState事件。

    listenPop() {
        window.addEventListener('popState', () => {
            let state = e.state || {};
            let path = state.path || "";
            this.commonDealPath(path);  // 触发commonDealPath
        }, false)
    }

拦截pushState()和replaceState()

单独处理一下,这两个方法。监听的同时触发view改变。

    pathpush(path) {
        history.pushState({path},null,path);
        this.commonDealPath(path);
    }
    
    pathReplace(path) {
        history.replaceState({path},null,path);
        this.commonDealPath(path);
    }

ok!简单的histroy模式就完成了!一句话:监听触发history改变的方法,然后change view。

兼容性

hash:

1626233547630_F6AE9DE8-1AAB-4B43-95C9-6B174E9023FF.png

history:

1626233729418_7E3C2F26-0F85-4195-8F43-39E0B3139ECD.png

hash的兼容性要好于hsitory的。

最后

简单的分析了一波hash和history。下期分享一下vue的router和react的router。感谢!!