Skip to content

原生JS实现hash路由 #119

Open
Open
@louzhedong

Description

熟悉React,vue等各类框架的同学也肯定都使用过他们各自的路由框架,用起来确实十分方便。对于单页应用来说,路由确实是一个非常重要的组成部分。众所周知,目前的路由可以分为hash-router和history-router。本文将用原生JS来实现一个简单的hash-router,功能包包括路由的切换已经历史记录的前进后退

实现原理

​ 在Javascript中有一个API,可以监听当前浏览器地址中hash的变化(即地址上#后面的部分),利用这一特点,当我们通过某些按钮改变地址的hash时,可以针对性地做出某些改变,这就是hash理由的主要实现原理

在线demo

具体的实现代码

// html
<main>
	<div class="nav">
		<ul class="nav-list">
			<li class="nav-item index active"><a href="#/index">推荐</a></li>
			<li class="nav-item rank"><a href="#/rank">排行榜</a></li>
			<li class="nav-item songList"><a href="#/songList">歌单</a></li>
			<li class="nav-item broadcast"><a href="#/broadcast">主播电台</a></li>
			<li class="nav-item newest"><a href="#/newest">最新音乐</a></li>
		</ul>
	</div>
	<div class="main-content">
		<div class="main-box index">推荐</div>
    <div class="main-box rank">排行榜</div>
    <div class="main-box songList">歌单</div>
    <div class="main-box broadcast">主播电台</div>
    <div class="main-box newest">最新音乐</div>
	</div>
</main>

<div class="nav-area">
	<div class="nav-area-back" onclick="handleRouterBack()">后退</div>
	<div class="nav-area-front" onclick="handleRouterFront()">前进</div>
</div>


// css
.nav-list {
  list-style: none;
  line-height: 45px;
  text-align: center;
}

.nav-list .nav-item {
  display: inline-block;
  margin: 0 30px;
}

.nav-list .nav-item:first-child {
  margin-left: 0;
}

.nav-list .nav-item:last-child {
  margin-right: 0;
}

.nav-list .nav-item a {
  text-decoration: none;
  color: #000;
  font-size: 14px;
  cursor: pointer;
}

.nav-list .nav-item.active a {
  color: #B92500;
}

.nav-list .nav-item a:hover {
  color: #B92500;
}

.main-content .main-box {
	display: none;
}

.main-content .main-box-show {
  display: block;
}

.nav-area {
	position: absolute;
	top: 20px;
	left:50px;
}

.nav-area-back, .nav-area-front {
	display: inline-block;
}

.nav-area .active {
  color: #C52C04;
	cursor: pointer;
}

// javascript

  function Router(callback) {
    this.routes = {};  // 消费者模式,将对应路径的执行函数存入一个map里
    this.routeHistory = [];  // 路由历史
    this.currentUrl = '';  // 当前的路由地址
    this.currentIndex = -1;  // 当前的路由序列号
    this.frontOrBack = false;  // 是否的点击前进后退造成的路由变化,此时不需要监听到路由变化函数
    this.callback = callback; // 每次路由改变后调用此回调函数,将currentIndex, routeHistory 传出去,将后续依赖路由的操作与路由本身解耦
  }

  // 注册路由对象,将对应路径的执行函数存入一个map里
  Router.prototype.route = function (path, callback) {
    this.routes[path] = callback || function () { };
  }

  // 监听路由变化
  Router.prototype.refersh = function () {
    if (this.frontOrBack) {  // 前进后退造成的路由变化,此时不需要改变routeHistory的数据
      this.frontOrBack = false;
    } else {
      this.currentUrl = location.hash.slice(1) || '/index';
      this.routeHistory = this.routeHistory.slice(0, this.currentIndex + 1);
      this.routeHistory.push(this.currentUrl);
      this.currentIndex++;
    }

    // 执行对应路由绑定的函数
    this.routes[this.currentUrl]();

    this.callback(this.currentIndex, this.routeHistory);
  }

  // 路由后退
  Router.prototype.back = function () {
    if (this.currentIndex > 0) {
      this.frontOrBack = true;
      this.currentIndex--;
      this.currentUrl = this.routeHistory[this.currentIndex];
      window.location.hash = this.currentUrl;
    }
  }

  // 路由前进
  Router.prototype.front = function () {
    const historyLength = this.routeHistory.length;
    if (this.currentIndex < historyLength - 1) {
      this.frontOrBack = true;
      this.currentIndex++;
      this.currentUrl = this.routeHistory[this.currentIndex];
      window.location.hash = this.currentUrl;
    }
  }

  Router.prototype.init = function () {
    window.addEventListener('load', this.refersh.bind(this), false);
    // 监听hash的变化
    window.addEventListener('hashchange', this.refersh.bind(this), false);
  }

const router = new Router((currentIndex, routeHistory) => {
	// 如果当前路由前面还有路由
  let backDom = document.querySelector('.nav-area-back');
  if (backDom) {
    if (currentIndex > 0) {
      backDom.classList.add('active');
    } else {
      backDom.classList.remove('active');
    }
  }

  // 如果当前路由后还有路由
  let frontDom = document.querySelector(".nav-area-front");
  if (frontDom) {
    if (currentIndex < routeHistory.length - 1) {
      frontDom.classList.add('active');
    } else {
      frontDom.classList.remove('active');
    }
  }
});

router.init();

const tabs = [
  {
    name: '推荐',
    path: '/index'
  },
  {
    name: '排行榜',
    path: '/rank'
  },
  {
    name: '歌单',
    path: '/songList'
  },
  {
    name: '主播电台',
    path: '/broadcast'
  },
  {
    name: '最新音乐',
    path: '/newest'
  }
]

tabs.forEach(item => {
  router.route(item.path, () => {
    const tabName = item.path.split('/')[1];
    const tabDom = document.querySelectorAll('.main-box');
    const navDom = document.querySelectorAll('.nav-item');
    const tabDomLength = tabDom.length;
    const navDomLength = navDom.length;
    for (let i = 0; i < tabDomLength; i++) {
      if (tabDom[i].classList.contains(tabName)) {
        tabDom[i].classList.add('main-box-show');
      } else {
        tabDom[i].classList.remove('main-box-show');
      }
    }
    for (let i = 0; i < navDomLength; i++) {
      if (navDom[i].classList.contains(tabName)) {
        navDom[i].classList.add('active');
      } else {
        navDom[i].classList.remove('active');
      }
    }
  })
})

function handleRouterBack() {
  router.back();
}

function handleRouterFront() {
  router.front();
}

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions