自己動(dòng)手實(shí)現(xiàn)一個(gè)前端路由

單頁(yè)面應(yīng)用利用了JavaScript動(dòng)態(tài)變換網(wǎng)頁(yè)內(nèi)容,避免了頁(yè)面重載;路由則提供了瀏覽器地址變化,網(wǎng)頁(yè)內(nèi)容也跟隨變化,兩者結(jié)合起來(lái)則為我們提供了體驗(yàn)良好的單頁(yè)面web應(yīng)用

前端路由實(shí)現(xiàn)方式

路由需要實(shí)現(xiàn)三個(gè)功能:

? ①瀏覽器地址變化,切換頁(yè)面;

? ②點(diǎn)擊瀏覽器【后退】帝嗡、【前進(jìn)】按鈕担神,網(wǎng)頁(yè)內(nèi)容跟隨變化孤里;

? ③刷新瀏覽器,網(wǎng)頁(yè)加載當(dāng)前路由對(duì)應(yīng)內(nèi)容

在單頁(yè)面web網(wǎng)頁(yè)中,單純的瀏覽器地址改變,網(wǎng)頁(yè)不會(huì)重載,如單純的hash網(wǎng)址改變網(wǎng)頁(yè)不會(huì)變化,因此我們的路由主要是通過(guò)監(jiān)聽(tīng)事件,并利用js實(shí)現(xiàn)動(dòng)態(tài)改變網(wǎng)頁(yè)內(nèi)容,有兩種實(shí)現(xiàn)方式:

hash路由: 監(jiān)聽(tīng)瀏覽器地址hash值變化,執(zhí)行相應(yīng)的js切換網(wǎng)頁(yè)
history路由: 利用history API實(shí)現(xiàn)url地址改變,網(wǎng)頁(yè)內(nèi)容改變

hash路由

首先定義一個(gè)Router類(lèi)

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()時(shí)監(jiān)聽(tīng)load,hashchange兩個(gè)事件:

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

瀏覽器地址hash值變化直接通過(guò)a標(biāo)簽鏈接實(shí)現(xiàn)

<nav id="nav" class="nav-tab">
  <ul class='tab'>
    <li><a class='nav-item' href="#/index">首頁(yè)</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">狀態(tài)管理</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值變化后,回調(diào)方法:

/**
 * hash路由刷新執(zhí)行
 */
hashRefresh() {
  // 獲取當(dāng)前路徑,去掉查詢字符串,默認(rèn)'/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) {
  // 獲得相應(yīng)組件
  var Component = require('../' + name).default;
  // 判斷是否已經(jīng)配置掛載元素,默認(rèn)為$('#main')
  var controller = new Component($('#main'))
}

考慮到存在多級(jí)頁(yè)面嵌套路由的存在,需要對(duì)嵌套路由進(jìn)行處理:

  • 直接子頁(yè)面路由時(shí),按父路由到子路由的順序加載頁(yè)面
  • 父頁(yè)面已經(jīng)加載,再加載子頁(yè)面時(shí),父頁(yè)面保留,只加載子頁(yè)面

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

hashRefresh() {
  // 獲取當(dāng)前路徑,去掉查詢字符串,默認(rèn)'/index'
  var currentURL = location.hash.slice(1).split('?')[0] || '/index';  
  // 多級(jí)鏈接拆分為數(shù)組,遍歷依次加載
  this.currentURLlist = currentURL.slice(1).split('/')
  this.url = ""
  this.currentURLlist.forEach((item, index) => {
    // 導(dǎo)航菜單激活顯示
    if (index === 0) {
      this.navActive(item)
    }
    this.url += "/" + item
    this.name = this.routes[this.url]
    // 404頁(yè)面處理
    if (!this.name) {
      location.href = '#/error'
      return false
    }
    // 對(duì)于嵌套路由的處理
    if (this.oldURL && this.oldURL[0]==this.currentURLlist[0]) {
      this.handleSubRouter(item,index)
    } else {
      this.controller(this.name)
    }
  });
  // 記錄鏈接數(shù)組,后續(xù)處理子級(jí)組件
  this.oldURL = JSON.parse(JSON.stringify(this.currentURLlist))
}
/**
  * 處理嵌套路由
  * @param {string} item 鏈接list中當(dāng)前項(xiàng)
  * @param {number} index 鏈接list中當(dāng)前索引
  */
handleSubRouter(item,index){
  // 新路由是舊路由的子級(jí)
  if (this.oldURL.length < this.currentURLlist.length) {
    // 相同路由部分不重新加載
    if (item !== this.oldURL[index]) {
      this.controller(this.name)
    }
  }
  // 新路由是舊路由的父級(jí)
  if (this.oldURL.length > this.currentURLlist.length) {
    var len = Math.min(this.oldURL.length, this.currentURLlist.length)
    // 只重新加載最后一個(gè)路由
    if (index == len - 1) {
      this.controller(this.name)
    }
  }
}               

這樣,一個(gè)hash路由組件就實(shí)現(xiàn)了廓旬。

使用時(shí),只需new一個(gè)Router實(shí)例即可:new Router({mode:'hash'})

history 路由

window.history屬性指向 History 對(duì)象,是瀏覽器的一個(gè)屬性,表示當(dāng)前窗口的瀏覽歷史,History 對(duì)象保存了當(dāng)前窗口訪問(wèn)過(guò)的所有頁(yè)面地址。更多了解History對(duì)象,可參考阮一峰老師的介紹: History 對(duì)象

webpack開(kāi)發(fā)環(huán)境下,需要在devServer對(duì)象添加以下配置:

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

history路由主要是通過(guò)history.pushState()方法向?yàn)g覽記錄中添加一條歷史記錄,并同時(shí)觸發(fā)js回調(diào)加載頁(yè)面

當(dāng)【前進(jìn)】、【后退】時(shí)薄扁,會(huì)觸發(fā)history.popstate 事件,加載history.state中存放的路徑

history路由實(shí)現(xiàn)與hash路由的步驟類(lèi)似,由于需要配置路由模式切換,頁(yè)面中所有的a鏈接都采用了hash類(lèi)型鏈接,history路由初始化時(shí),需要攔截a標(biāo)簽的默認(rèn)跳轉(zhuǎn):

  /**
   * history模式劫持 a鏈接
   */
  bindLink() {
    $('#nav').on('click', 'a.nav-item', this.handleLink.bind(this))
  }
 /**
   * history 處理a鏈接
   * @param  e 當(dāng)前對(duì)象Event
   */
  handleLink(e) {
    e.preventDefault();
    // 獲取元素路徑屬性
    let href = $(e.target).attr('href')
    // 對(duì)非路由鏈接直接跳轉(zhuǎn)
    if (href.slice(0, 1) !== '#') {
      window.location.href = href
    } else {
      let path = href.slice(1)
      history.pushState({
        path: path
      }, null, path)
      // 加載相應(yīng)頁(yè)面
      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));

瀏覽是【前進(jìn)】或【后退】時(shí),觸發(fā)popstate事件,執(zhí)行回調(diào)函數(shù)

/**
  * history模式刷新頁(yè)面
  * @param  e  當(dāng)前對(duì)象Event
  */
historyRefresh(e) {
  const state = e.state || {}
  const path = state.path.split('?')[0] || null
  if (path) {
    this.loadView(path)
  }
}

history路由模式首次加載頁(yè)面時(shí),可以默認(rèn)一個(gè)頁(yè)面,這時(shí)可以用history.replaceState方法

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

對(duì)于404頁(yè)面的處理,也類(lèi)似

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

點(diǎn)擊預(yù)覽

更多源碼請(qǐng)?jiān)L問(wèn)Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瞎领,一起剝皮案震驚了整個(gè)濱河市泌辫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌九默,老刑警劉巖震放,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異驼修,居然都是意外死亡殿遂,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)乙各,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)墨礁,“玉大人,你說(shuō)我怎么就攤上這事耳峦《骶玻” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵蹲坷,是天一觀的道長(zhǎng)驶乾。 經(jīng)常有香客問(wèn)我,道長(zhǎng)循签,這世上最難降的妖魔是什么级乐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮县匠,結(jié)果婚禮上风科,老公的妹妹穿的比我還像新娘。我一直安慰自己乞旦,他們只是感情好贼穆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著杆查,像睡著了一般扮惦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上亲桦,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天崖蜜,我揣著相機(jī)與錄音浊仆,去河邊找鬼。 笑死豫领,一個(gè)胖子當(dāng)著我的面吹牛抡柿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播等恐,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼洲劣,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了课蔬?” 一聲冷哼從身側(cè)響起囱稽,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎二跋,沒(méi)想到半個(gè)月后战惊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡扎即,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年吞获,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谚鄙。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡各拷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出闷营,到底是詐尸還是另有隱情烤黍,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布傻盟,位于F島的核電站蚊荣,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏莫杈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一奢入、第九天 我趴在偏房一處隱蔽的房頂上張望筝闹。 院中可真熱鬧,春花似錦腥光、人聲如沸关顷。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)议双。三九已至,卻和暖如春捉片,著一層夾襖步出監(jiān)牢的瞬間平痰,已是汗流浹背汞舱。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宗雇,地道東北人昂芜。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像赔蒲,于是被迫代替她去往敵國(guó)和親泌神。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 前言 用過(guò)現(xiàn)代前端框架的同學(xué)舞虱,對(duì)前端路由一定不陌生, vue, react, angular 都有自己的 rout...
    noahlam閱讀 474評(píng)論 0 1
  • 一欢际、什么是前端路由 在web開(kāi)發(fā)的過(guò)程中,路由的使用是必不可少的矾兜,這里的路由不是指我們?nèi)粘I钪械穆酚善魉鹎鳎?..
    Zeroacexy閱讀 14,129評(píng)論 0 55
  • 通常 SPA 中前端路由有2種實(shí)現(xiàn)方式: window.history location.hash 下面就來(lái)介紹下...
    好奇男孩閱讀 1,173評(píng)論 0 18
  • 介紹 vue-router是一個(gè)vue插件。其實(shí)質(zhì)是在location.hash焕刮、location.replace...
    AmazRan閱讀 1,551評(píng)論 0 6
  • 在這個(gè)話題為王的時(shí)代配并,大佬們隔三差五總要鬧出點(diǎn)動(dòng)靜括荡,譬如這兩天京東就走在了風(fēng)口浪尖。6月7日溉旋,京東股價(jià)開(kāi)盤(pán)一路下跌...
    張美月閱讀 10,481評(píng)論 0 0