自己动手实现一个前端路由

单页面应用利用了JavaScript动态变换网页内容,避免了页面重载;路由则提供了浏览器地址变化,网页内容也跟随变化,两者结合起来则为我们提供了体验良好的单页面web应用

前端路由实现方式

路由需要实现三个功能:

​ ①浏览器地址变化,切换页面;

​ ②点击浏览器【后退】、【前进】按钮,网页内容跟随变化;

​ ③刷新浏览器,网页加载当前路由对应内容

在单页面web网页中,单纯的浏览器地址改变,网页不会重载,如单纯的hash网址改变网页不会变化,因此我们的路由主要是通过监听事件,并利用js实现动态改变网页内容,有两种实现方式:

hash路由: 监听浏览器地址hash值变化,执行相应的js切换网页
history路由: 利用history API实现url地址改变,网页内容改变

hash路由

首先定义一个Router

class Router {
  constructor(obj) {
    // 路由模式
    this.mode = obj.mode
    // 配置路由
    this.routes = {
      '/index'              : 'views/index/index',
      '/index/detail'       : 'views/index/detail/detail',
      '/index/detail/more'  : 'views/index/detail/more/more',
      '/subscribe'          : 'views/subscribe/subscribe',
      '/proxy'              : 'views/proxy/proxy',
      '/state'              : 'views/state/stateDemo',
      '/state/sub'          : 'views/state/components/subState',
      '/dom'                : 'views/visualDom/visualDom',
      '/error'              : 'views/error/error'
    }
    this.init()
  }
}

路由初始化init()时监听load,hashchange两个事件:

window.addEventListener('load', this.hashRefresh.bind(this), false);
window.addEventListener('hashchange', this.hashRefresh.bind(this), false);

浏览器地址hash值变化直接通过a标签链接实现

<nav id="nav" class="nav-tab">
  <ul class='tab'>
    <li><a class='nav-item' href="#/index">首页</a></li>
    <li><a class='nav-item' href="#/subscribe">观察者</a></li>
    <li><a class='nav-item' href="#/proxy">代理</a></li>
    <li><a class='nav-item' href="#/state">状态管理</a></li>
    <li><a class='nav-item' href="#/dom">虚拟DOM</a></li>
  </ul>
</nav>
<div id="container" class='container'>
  <div id="main" class='main'></div>
</div>

hash值变化后,回调方法:

/**
 * hash路由刷新执行
 */
hashRefresh() {
  // 获取当前路径,去掉查询字符串,默认'/index'
  var currentURL = location.hash.slice(1).split('?')[0] || '/index';
  this.name = this.routes[this.currentURL]
  this.controller(this.name)
}
/**
  * 组件控制器
  * @param {string} name 
  */
controller(name) {
  // 获得相应组件
  var Component = require('../' + name).default;
  // 判断是否已经配置挂载元素,默认为$('#main')
  var controller = new Component($('#main'))
}

考虑到存在多级页面嵌套路由的存在,需要对嵌套路由进行处理:

  • 直接子页面路由时,按父路由到子路由的顺序加载页面
  • 父页面已经加载,再加载子页面时,父页面保留,只加载子页面

改造后的路由刷新方法为:

hashRefresh() {
  // 获取当前路径,去掉查询字符串,默认'/index'
  var currentURL = location.hash.slice(1).split('?')[0] || '/index';  
  // 多级链接拆分为数组,遍历依次加载
  this.currentURLlist = currentURL.slice(1).split('/')
  this.url = ""
  this.currentURLlist.forEach((item, index) => {
    // 导航菜单激活显示
    if (index === 0) {
      this.navActive(item)
    }
    this.url += "/" + item
    this.name = this.routes[this.url]
    // 404页面处理
    if (!this.name) {
      location.href = '#/error'
      return false
    }
    // 对于嵌套路由的处理
    if (this.oldURL && this.oldURL[0]==this.currentURLlist[0]) {
      this.handleSubRouter(item,index)
    } else {
      this.controller(this.name)
    }
  });
  // 记录链接数组,后续处理子级组件
  this.oldURL = JSON.parse(JSON.stringify(this.currentURLlist))
}
/**
  * 处理嵌套路由
  * @param {string} item 链接list中当前项
  * @param {number} index 链接list中当前索引
  */
handleSubRouter(item,index){
  // 新路由是旧路由的子级
  if (this.oldURL.length < this.currentURLlist.length) {
    // 相同路由部分不重新加载
    if (item !== this.oldURL[index]) {
      this.controller(this.name)
    }
  }
  // 新路由是旧路由的父级
  if (this.oldURL.length > this.currentURLlist.length) {
    var len = Math.min(this.oldURL.length, this.currentURLlist.length)
    // 只重新加载最后一个路由
    if (index == len - 1) {
      this.controller(this.name)
    }
  }
}               

这样,一个hash路由组件就实现了。

使用时,只需new一个Router实例即可:new Router({mode:'hash'})

history 路由

window.history属性指向 History 对象,是浏览器的一个属性,表示当前窗口的浏览历史,History 对象保存了当前窗口访问过的所有页面地址。更多了解History对象,可参考阮一峰老师的介绍: History 对象

webpack开发环境下,需要在devServer对象添加以下配置:

historyApiFallback: {
  rewrites: [
    { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
  ],
}

history路由主要是通过history.pushState()方法向浏览记录中添加一条历史记录,并同时触发js回调加载页面

当【前进】、【后退】时,会触发history.popstate 事件,加载history.state中存放的路径

history路由实现与hash路由的步骤类似,由于需要配置路由模式切换,页面中所有的a链接都采用了hash类型链接,history路由初始化时,需要拦截a标签的默认跳转:

  /**
   * history模式劫持 a链接
   */
  bindLink() {
    $('#nav').on('click', 'a.nav-item', this.handleLink.bind(this))
  }
 /**
   * history 处理a链接
   * @param  e 当前对象Event
   */
  handleLink(e) {
    e.preventDefault();
    // 获取元素路径属性
    let href = $(e.target).attr('href')
    // 对非路由链接直接跳转
    if (href.slice(0, 1) !== '#') {
      window.location.href = href
    } else {
      let path = href.slice(1)
      history.pushState({
        path: path
      }, null, path)
      // 加载相应页面
      this.loadView(path.split('?')[0])
    }
  }

history路由初始化需要绑定loadpopstate事件

this.bindLink()
window.addEventListener('load', this.loadView.bind(this, location.pathname));
window.addEventListener('popstate', this.historyRefresh.bind(this));

浏览是【前进】或【后退】时,触发popstate事件,执行回调函数

/**
  * history模式刷新页面
  * @param  e  当前对象Event
  */
historyRefresh(e) {
  const state = e.state || {}
  const path = state.path.split('?')[0] || null
  if (path) {
    this.loadView(path)
  }
}

history路由模式首次加载页面时,可以默认一个页面,这时可以用history.replaceState方法

if (this.mode === 'history' && currentURL === '/') {
  history.replaceState({path: '/'}, null, '/')
  currentURL = '/index'
}

对于404页面的处理,也类似

history.replaceState({path: '/error'}, null, '/error')
this.loadView('/error')

点击预览

更多源码请访问Github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 前言 用过现代前端框架的同学,对前端路由一定不陌生, vue, react, angular 都有自己的 rout...
    noahlam阅读 458评论 0 1
  • 一、什么是前端路由 在web开发的过程中,路由的使用是必不可少的,这里的路由不是指我们日常生活中的路由器,但是...
    Zeroacexy阅读 14,108评论 0 55
  • 通常 SPA 中前端路由有2种实现方式: window.history location.hash 下面就来介绍下...
    好奇男孩阅读 1,155评论 0 18
  • 介绍 vue-router是一个vue插件。其实质是在location.hash、location.replace...
    AmazRan阅读 1,541评论 0 6
  • 在这个话题为王的时代,大佬们隔三差五总要闹出点动静,譬如这两天京东就走在了风口浪尖。6月7日,京东股价开盘一路下跌...
    张美月阅读 10,476评论 0 0