没接触过jQuery甚至更久远年代的前端可能对路由不会有太多的其他看法,但是在我刚入行前端的那个年代页面切换还在用location.href,也没啥封装的概念,更别谈组件化,能做个公有css封装已经顶天了。自从接触了第一个vue(脚手架)项目简直是从东海风车村直接到新世界,中间都不用去香波地群岛;各种概念一股脑的迎面而来,工程化,webpack,less,sass,stylus,SPA,组件化...而对我印象最深的就是路由了。记得当时对路由的这种只改变浏览器地址参数不刷新页面而改变展示内容的行为是非常新奇的,作为好学的我(应付面试)当时特地去研究了一下,然而当时能力有限只知道一个大概,直到差不多三年前我才把路由的原理搞明白,与大家分享一下。如有不对欢迎指出;

什么是路由

在了解什么是路由我们首先要知道什么是SPA 单页面应用SPA 单页面应用是指一个项目只有一个页面(html),所有的交互与组件切换都在这个页面中呈现;而应对不同的呈现方式我们处理的方式也是不同的,在一般情况下我们直接运用组件的切换就能完成,但是如果遇到一些状态需要保存(防止刷新后丢失与前进后退状态保存)我们就需要用到路由;简单来说路由就是SPA页面中通过URL地址栏中地址不同页面组件呈现不同;也就是URL地址栏地址与组件之间的对应关系

未命名文件 (1).png

Hash路由

Hash路由的工作原理是通过浏览器地址栏Hash值变化来展现不同对应组件,这时候就有靓仔要问了,那啥是Hash值啊?

我们来看一张图

协议.png

一般来说浏览器地址栏的Hash值指的是浏览器地址#后面的部分(包括#);

那么路由是如何监听地址栏Hash值变化的呢?

当我们Hash值变化的时候会触发一个onhashchange事件,我们只需要在这个事件中监听hash值变化渲染对应的组件即可;

Hash路由简单实现

为了更好的了解Hash路由工作原理,接下来我们用原生JS手写一个超简单的Hash路由

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    <li onclick="router.push('/dg')">冬瓜</li>
    <li onclick="router.push('/xg')">西瓜</li>
    <li onclick="router.push('/ng')">南瓜</li>
    <li onclick="router.push('/bg')">北瓜</li>
</ul>
<div id="app"></div>
<script>
    //获取页面渲染容器
    const app = document.getElementById("app");
    class Router {
        constructor () {
            this.routes = {}//用于保存定义好的路由
            this.addListener()//初始化路由监听
            this.init()
        }
        //页面初始化
        init(){
            setTimeout(()=>{
                const routeKeys = Object.keys(this.routes)//获取定义好的路由key值
                const currentKey = window.location.hash.slice(1)//获取当前hash值
                if (routeKeys.includes (currentKey)) {//判断当前hash值是否有匹配若有则进行更新没有则跳转到#/
                    this.routeAction()
                }else{
                    window.location.href = `#/`;
                }
            })
        }
        //添加onhashchange监听
        addListener(){
            window.addEventListener("hashchange", (e)=>{
                this.routeAction(e)
            })
        }
        //获取将要渲染的组件并渲染
        routeAction(){
            const action = this.routes[window.location.hash?.slice(1)||'/' ];
            action&&action()
        }
        //创建路由(新增)
        createRoute(path,callback=()=>{}){
            this.routes[path] = callback
        }
        //路由push方法,用于跳转
        push(url){
            location.href = `#${url}`;
        }
    }

    const router = new Router()

    //创建路由
    router.createRoute('/dg', ()=>{
        app.innerHTML="冬瓜"
    })
    router.createRoute('/xg', ()=> {
        app.innerHTML="西瓜"
    })
    router.createRoute('/ng', ()=> {
        app.innerHTML="南瓜"
    })
    router.createRoute('/bg', ()=> {
        app.innerHTML="北瓜"
    })
</script>
</body>
</html>

在这里可能就有靓仔就会问了为啥location.href = #${url}; 这种方式页面不会刷新,我想大家在开发的时候多多少少用过锚点,Hash路由正是利用锚点改变Url地址栏的Hash值不会进行跳转(页面刷新)实现的

History路由

感官上History路由Hash路由的区别仅仅是少了一个#但是它们两者运用原理与实现方式却截然不同,在之前我们已经知道Hash路由是通过浏览器地址栏的Hash改变不会刷新页面实现的,而当我们在浏览器中使用location.href='/xx'页面是会被刷新的;那么History路由是怎么实现地址栏改变页面不会被刷新的呢?

在H5在History中为我们提供了几个新的API与事件

History.pushState()

描述

从某种程度来说,调用 pushState() 和 window.location = "#foo"基本上一样,他们都会在当前的 document 中创建和激活一个新的历史记录。但是 pushState() 有以下优势:

  • 新的 URL 可以是任何和当前 URL 同源的 URL。但是设置 window.location 只会在你只设置锚的时候才会使当前的 URL。
  • 非强制修改 URL。相反,设置 window.location = "#foo"; 仅仅会在锚的值不是 #foo 情况下创建一条新的历史记录。
  • 可以在新的历史记录中关联任何数据。window.location = "#foo"形式的操作,你只可以将所需数据写入锚的字符串中。

注意: pushState() 不会造成 hashchange 事件调用,即使新的 URL 和之前的 URL 只是锚的数据不同。

语法

history.pushState(state, title[, url])

参数

state

  • 状态对象是一个 JavaScript 对象,它与pushState()创建的新历史记录条目相关联。每当用户导航到新状态时,都会触发popstate事件,并且该事件的状态属性包含历史记录条目的状态对象的副本。 状态对象可以是任何可以序列化的对象。

title

  • 可忽略一般不传,为JS保留参数

url 可选

  • 新历史记录条目的 URL 由此参数指定。请注意,浏览器不会在调用 pushState() 之后尝试加载此 URL,但可能会稍后尝试加载 URL,例如在用户重新启动浏览器之后。新的 URL 不必是绝对的。如果是相对的,则相对于当前 URL 进行解析。新网址必须与当前网址相同 origin;否则,pushState()将引发异常。如果未指定此参数,则将其设置为文档的当前 URL。

History.replaceState()

描述

History.pushState()类似,但是不会在document中保留或者创建历史记录(但是会修改记录)

语法

history.replaceState(stateObj, title[, url]);

参数

stateObj

  • 状态对象是一个 JavaScript 对象,它与传递给 replaceState 方法的历史记录实体相关联。

title

  • 可忽略一般不传,为JS保留参数

url 可选

  • 历史记录实体的 URL. 新的 URL 跟当前的 URL 必须是同源; 否则 replaceState 抛出一个异常。

popstate事件

描述

每当激活同一文档中不同的历史记录条目时,popstate 事件就会在对应的 window 对象上触发。如果当前处于激活状态的历史记录条目是由 history.pushState() 方法创建的或者是由 history.replaceState() 方法修改的,则 popstate 事件的 state 属性包含了这个历史记录条目的 state 对象的一个拷贝。

语法

window.onpopstate = funcRef;

备注

无论是history.pushState()还是history.replaceState()只会影响历史记录的创建和修改,并不会触发popstate事件,popstate事件只会在浏览器的前进后退或者history.back()等情况下触发;

History简单实现

简单用原生JS实现一个超简单的History路由

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    <li onclick="router.push('/dg')">冬瓜</li>
    <li onclick="router.push('/ng')">南瓜</li>
    <li onclick="router.push('/xg')">西瓜</li>
    <li onclick="router.push('/bg')">北瓜</li>
</ul>
<div id="app">

</div>
<script>
    const app = document.getElementById("app");
    class Router{
        constructor () {
            this.routes = {}
            this.addListener()
            this.init()
        }
        //路由初始化操作
        init(){
            setTimeout(()=>{
                const fn = this.routes[location.pathname]
                fn?fn(): history.replaceState({ path:'/' }, null, '/')
            })
        }
        //监听popstate并渲染相应组件
        addListener(){
            addEventListener('popstate',e=>{
                const fn = this.routes[e?.state?.path]
                fn&&fn()
            })
        }

        //创建路由(新增)
        createRoute(path, callback) {
            this.routes[path] = callback || function() {}
        }

        //路由跳转触发回调
        push(path) {
            //修改路径,不进行跳转
            history.pushState({ path }, null, path)
            this.routes[path] && this.routes[path]()
        }
    }

    const router = new Router()
    //创建路由
    router.createRoute('/dg', ()=>{
        app.innerHTML="冬瓜"
    })
    router.createRoute('/xg', ()=> {
        app.innerHTML="西瓜"
    })
    router.createRoute('/ng', ()=> {
        app.innerHTML="南瓜"
    })
    router.createRoute('/bg', ()=> {
        app.innerHTML="北瓜"
    })
</script>
</body>
</html>

参考文档 MDN

使用History路由需要注意的点

不管是History.pushState还是History.replaceState都只能在有服务的情况下运行(存在域名端口号)不然这两个傲娇的API是无法调用的,在使用History路由时我们还需要对服务器进行配置,不然刷新会使我们的页面丢失

总结

路由是SPA页面中通过URL地址栏变化做到页面无刷新并通过URL地址栏的hash值(hash路由)或者pathname(history路由)不同来对应页面的组件渲染


龙骑士尹道长
58 声望2 粉丝

什么都不会,光会写bug