hash 路由和 history 路由的原生 JS 实现

React 和 Vue 中都有 HashRouter 和 Router;本文用来理清路由的概念,并且使用原生 JS 实现 hash 路由 和history 路由;文章的最后是对 History 对象 和 window.history 的简要介绍,以便于理解 history 路由;

React-router GitHub仓库

Vue-router GitHub仓库

1、概述

在以前的多页应用(MPA)中,路由主要由后端实现,并且每次访问新内容都要重新刷新整个页面,这对服务端造成了性能压力,也影响浏览器上用户体验;

于是在现有的前端实践中,出现了前端路由的方案,前端路由使得每次获取新内容时只需要部分刷新页面,而不需要刷新整个页面,这也就构成了所谓单页应用(SPA)的概念;

在现有前端框架中都有 HashRouter 和 Router 两种前端路由实现方案,其中 HashRouter 是基于 hashchange 事件实现的(被称为hash路由),Router 是基于 history 对象实现的(被称为history路由);

hash路由在浏览器上的表现形式为 URL 为带 片段标识符(#)的 URL ;

history 路由在浏览器上的表现形式为 URL 和普通的远程资源的 URL(不带锚点#)的形式一致;

2、hash 路由的原生实现

hash URL 的特点:

  • URL 上带有锚点(#);
  • hash URL变化时浏览器不向服务器发送请求,页面不会跳转;
  • 通过 window.location.hash 可以获取和修改当前页面 URL 的 片段标识符

实现 hash 路由的基本思路:

  • 通过 hashchange 事件监听 hash 路由的变化;
  • 通过 xhr 对象获取服务器的资源;
  • 将资源渲染到页面上;

hash 路由的原生 JS 实现:

1、html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hash Router 和 history router 的实现</title>
    <style>
        .navbar {
            display:flex;
            justify-content:space-around;
            margin:0 auto;
            width:300px;
        }
        #container {
            width:400px;
            
            text-align: center;
            margin:0 auto;
            margin-top:100px;
        }
​
    </style>
</head>
<body>
    <div class="navbar">
        <div class="navbar_item"><a href="#/page1" class="page1">page1</a></div>
        <div class="navbar_item"><a href="#/page2" class="page2">page2</a></div>
        <div class="navbar_item"><a href="#/page3" class="page3">page3</a></div>
    </div>
​
    <div id="container"></div>
​
    <script type="text/javascript" src="./hashRouter.js"></script>
    
</body>
</html>

2、JS 文件

// hashRouter.js 
class HashRouter {
​
    constructor(dom) {
        this.hashObj = {};
​
        window.addEventListener('hashchange',(e) => {
​
            const path = e.newURL.split('#')[1];
​
            if(path && this.hashObj[path]) {
​
                const data = this.getData(path);
                this.appendData(dom,data);
​
            }
​
        })
    }
​
    // 注册路由
    register(...paths){
​
        paths.forEach(path => {
            this.hashObj[path] = path;
        })
​
    }
​
    // 获取数据
    getData(path) {
        const url = `https://www.fastmock.site/mock/ba90f15d75470108ed2c56997636d24d/routertest/api/getPage${path.slice(-1)}data`
        let xhr = new XMLHttpRequest();
        xhr.open('GET',url,false)
        xhr.send(null);
        if(xhr.status === 200) {
​
            return JSON.parse(xhr.responseText);
        }
        return {};
    }
​
    // 渲染数据
    appendData(dom,data) {
​
        let containerDom = document.querySelector(dom);
        containerDom.innerHTML = '';
​
        let titleDom = document.createElement('h1');
        titleDom.innerText = data.title; 
        let dataDom = document.createElement('div');
        dataDom.innerText = data.data;
        let descDom = document.createElement('div');
        descDom.innerText = data.desc;
​
        containerDom.append(titleDom,dataDom,descDom);
    }
}
​
const hashRouter = new HashRouter('#container');
hashRouter.register('/page1','/page2','/page3');

使用到的数据接口(由fastmock建立)


3、history 路由的原生实现

普通的远程资源的 URL 的特点:

  • URL 上不带有片段标识符(#);
  • URL 变化时浏览器会向服务器发送请求,重新渲染整个页面;
  • 通过 window.history 属性指向的 History 对象可以控制浏览器展示变化的 URL;

实现 history 路由的基本思路:

  • 监听 a 标签上访问普通URL的点击事件,阻止默认事件来阻止页面跳转和重新渲染;
  • 通过 window.history 改变浏览器显示的 URL;
  • 通过 xhr 对象获取服务器的资源;
  • 将资源渲染到页面上;

history 路由的原生 JS 实现:

1、html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hash Router 的实现</title>
    <style>
        .navbar {
            display:flex;
            justify-content:space-around;
            margin:0 auto;
            width:300px;
        }
        #container {
            width:400px;
            
            text-align: center;
            margin:0 auto;
            margin-top:100px;
        }
​
    </style>
</head>
<body>
    <div class="navbar">
        <div class="navbar_item"><a href="/page1" class="page1">page1</a></div>
        <div class="navbar_item"><a href="/page2" class="page2">page2</a></div>
        <div class="navbar_item"><a href="/page3" class="page3">page3</a></div>
    </div>
​
    <div id="container"></div>
​
    <script type="text/javascript" src="./historyRouter.js"></script>
    
</body>
</html>

2、JS 文件

// historyRouter.js
class HistoryRouter {
​
    constructor(dom) {
        this.historyObj = {};
        this.dom = dom;
        
        this.listenClickOfa();
        this.listenPopstate();
​
    }
​
    // 注册路由
    register(...paths){
​
        paths.forEach(path => {
            this.historyObj[path] = path;
        })
        
    }
​
    // 监听 a 标签上的click 事件
    listenClickOfa() {
​
        window.addEventListener('click',(e) => {
            const path = e.target.href;
            
            if(e.target.tagName === 'A' && path && this.historyObj['/' + path.split('/').slice(-1)]) {
                e.preventDefault();
​
                // 变换 URL
                this.updateUrl(path);
​
                // 获取数据
                const data = this.getData(path);
​
                // 渲染数据
                this.appendData(data);
​
            }
            
            
        })
    }
​
    // 获取数据
    getData(path) {
        const url = `https://www.fastmock.site/mock/ba90f15d75470108ed2c56997636d24d/routertest/api/getPage${path.slice(-1)}data`
        let xhr = new XMLHttpRequest();
        xhr.open('GET',url,false)
        xhr.send(null);
        if(xhr.status === 200) {
​
            return JSON.parse(xhr.responseText);
        }
        return {};
    }
​
    // 通过 window.history 切换浏览器显示的 url
    updateUrl(path) {
        window.history.pushState({path:path},null,path);
    }
​
​
    // 渲染数据
    appendData(data) {
       
        let containerDom = document.querySelector(this.dom);
        containerDom.innerHTML = '';
​
        let titleDom = document.createElement('h1');
        titleDom.innerText = data.title; 
        let dataDom = document.createElement('div');
        dataDom.innerText = data.data;
        let descDom = document.createElement('div');
        descDom.innerText = data.desc;
​
        containerDom.append(titleDom,dataDom,descDom);
    }
​
​
​
    // 通过监听 popstate 事件来处理 history.back()或 history.forward() 时的页面渲染
    listenPopstate() {
​
        window.addEventListener('popstate',e => {
            const data = this.getData(e.state.path);
            this.appendData(data);
        })
    }
​
}
​
​
const historyRouter = new HistoryRouter('#container');
historyRouter.register('/page1','/page2','/page3');

本文代码仓库:https://gitee.com/hotpotliuyu/myStudyForToBeAFrontendDeveloper.git




下面的内容是对 History 对象 和 window.history 的简要介绍;

4、History 对象

1、概述

window.history 属性指向一个 History 对象,它表示当前窗口的浏览历史;其中历史URL因为安全原因被浏览器设置为不可见,只显示 lengthscrollRestorationstate 三个属性;

window.history instanceof History; // true

History 对象的结构如下:

History {
    length: 5, // 此 History 对象有5个URL历史,但是浏览器默认对其隐藏
    scrollRestoration: "auto",
    state: {url: "66666"},
    [[Prototype]]: History
}

2、属性

  1. History.length

  2. History.state

    表示 History 中最上层网址的一些状态,使用 History.pushState()History.replaceState() 可更改 state;

eg:

window.history.length // 当前窗口访问过多少个网页
​
window.history.state // 当前窗口 History 对象的当前状态

3、方法

  1. History.forward

  2. History.back

  3. History.go

    eg:

    window.history.forward(); // 当前窗口移动到上一个网页
    window.history.back(); // 当前窗口移动到下一个网页
    window.history.go(num); // 当前窗口跳转到指定网页
    
  4. History.pushState(state,title,url)

    用于在 History 对象中添加一条记录;

    • state:一个与添加的记录相关联的的状态对象,会成为 History 对象中的 state 属性;
    • title:修改文档的 title 属性,但是现在的浏览器大多没有使用这个参数;
    • url:页面 url;

    eg:

    window.history.pushState({url:'http://hotpotliu.com'},null,'http://hotpotliu.com')
    
  5. History.replaceState(state,title,url)

    用来修改 History 对象的当前记录(History对象的最上层网页);

4、popstate 事件

每当同一个文档的浏览历史(即 history 对象)出现变化时,就会触发 popstate事件;

页面第一次加载的时候,浏览器不会触发 popstate 事件;

eg:

window.addEventListener('popstate',function(event){
    console.log(event.state);
    // event.state 为history对象中对应URL的state;可以通过 History.pushState() 和 History.replaceState() 修改
})




参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值