自己動手實現一個前端路由

靖風行發表於2018-11-18

單頁面應用利用了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'))
}
複製程式碼

有位同學留言要實現路由懶載入,參考vue的實現方式,這裡貼出來,希望大家多提意見:

  /**
   * 懶載入路由元件控制器
   * @param {string} name 
   */
  controller(name) {
    // import 函式會返回一個 Promise物件,屬於es7範疇,需要配合babel的syntax-dynamic-import外掛使用
    var Component = ()=>import('../'+ name);
    Component().then(resp=>{
      var controller = new resp.default($('#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[index]==this.currentURLlist[index]) {
      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

相關文章