JS 專題系列-前端路由

1. 什麼是路由

路由是根據不一樣的 url 地址展現不一樣的內容或頁面javascript

早期的路由都是後端直接根據 url 來 reload 頁面實現的,即後端控制路由。css

後來頁面愈來愈複雜,服務器壓力愈來愈大,隨着 ajax(異步刷新技術) 的出現,頁面實現非 reload 就能刷新數據,讓前端也能夠控制 url 自行管理,前端路由由此而生。html

單頁面應用的實現,就是由於有了前端路由這個概念。前端

2. 前端路由的兩種實現原理

1 Hash路由

咱們常常在 url 中看到 #,這個 # 有兩種狀況,一個是咱們所謂的錨點,好比典型的回到頂部按鈕原理、Github 上各個標題之間的跳轉等,路由裏的 # 不叫錨點,咱們稱之爲 hash,大型框架的路由系統大多都是哈希實現的。vue

咱們須要一個根據監聽哈希變化觸發的事件 —— hashchange 事件java

window對象提供了onhashchange事件來監聽hash值的改變,一旦url中的hash值發生改變,便會觸發該事件。react

咱們用 window.location 處理哈希的改變時不會從新渲染頁面,而是看成新頁面加到歷史記錄中,這樣咱們跳轉頁面就能夠在 hashchange 事件中註冊 ajax 從而改變頁面內容。webpack

window.addEventListener('hashchange', function () {
  <!--這裏你能夠寫你須要的代碼-->
});
複製代碼

2 History 路由

HTML5的History API 爲瀏覽器的全局history對象增長的擴展方法。nginx

重點說其中的兩個新增的API history.pushState 和 history.replaceStateweb

這兩個 API 都接收三個參數,分別是

狀態對象(state object) — 一個JavaScript對象,與用pushState()方法建立的新歷史記錄條目關聯。不管什麼時候用戶導航到新建立的狀態,popstate事件都會被觸發,而且事件對象的state屬性都包含歷史記錄條目的狀態對象的拷貝。

標題(title) — FireFox瀏覽器目前會忽略該參數,雖然之後可能會用上。考慮到將來可能會對該方法進行修改,傳一個空字符串會比較安全。或者,你也能夠傳入一個簡短的標題,標明將要進入的狀態。

地址(URL) — 新的歷史記錄條目的地址。瀏覽器不會在調用pushState()方法後加載該地址,但以後,可能會試圖加載,例如用戶重啓瀏覽器。新的URL不必定是絕對路徑;若是是相對路徑,它將以當前URL爲基準;傳入的URL與當前URL應該是同源的,不然,pushState()會拋出異常。該參數是可選的;不指定的話則爲文檔當前URL。

咱們在控制檯輸入

window.history.pushState(null, null, "https://www.baidu.com/?name=lvpangpang");

能夠看到瀏覽器url的變化

注意:這裏的 url 不支持跨域,好比你在不是百度域名下輸入上面的代碼。

不過這種模式以前在vue或者react裏面選擇了這種模式,發現一刷新頁面就會到月球。

緣由是由於history模式的url是真實的url,服務器會對url的文件路徑進行資源查找,找不到資源就會返回404。說的通俗一點就是這種模式會被服務器識別,會作出相應的處理。

對於這種404的問題,咱們有不少解決方式。

A 配置webpack(開發環境)

historyApiFallback:{
    index:'/index.html'//index.html爲當前目錄建立的template.html
}
複製代碼

B 配置ngnix(生產環境)

location /{
    root   /data/nginx/html;
    index  index.html index.htm;
    error_page 404 /index.html;
}
複製代碼

3. 路由demo

接下來會一步一步來說解怎麼樣寫一個前端路由。

也就是把咱們的知識轉爲技能的過程。

上面咱們也看到了路由是根據不一樣的 url 地址展現不一樣的內容或頁面。對於前端路由來講就是根據不一樣的url地址展現不一樣的內容。

因而有了下面這版代碼。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
<div id="root">
  <a href="#/index">首頁</a>
  <a href="#/list">列表</a>
</div>
<script>
const root = document.querySelector('#root');
window.onhashchange = function (e) {
  var hash = window.location.hash.substr(1);
  if(hash === '/index') {
    root.innerHTML = '這是index組件';
  }
  if (hash === '/list') {
    root.innerHTML = '這是list組件';
  }
}
</script>
</body>
</html>
複製代碼

上面只能說是一個小demo,爲了讓咱們能最直觀地感覺到前端路由。此次爲了能有更好的效果,特地引入了gif。

4. 路由js版

看好了demo,是否是火燒眉毛想實現一個路由了,那就讓咱們一塊兒來一步一步實現它吧。這裏給他取個名-煉獄,主要是方便下文的指代。

4.1 煉獄的參數配置

這裏我是仿造vue,react裏面的路由配置的,默認是一個路由對象數組。

//路由配置
const routes = [{
  path: '/index',
  url: 'js/index.js'
}, {
  path: '/list',
  url: 'js/list.js'
}, {
  path: '/detail',
  url: 'js/detail.js'
}];
var router = new Router(routes);
複製代碼

能夠看到上面的路由配置是否是和vue以及react很像呢。只不過這裏的url指向的是js文件而不是組件(其實組件也是js文件,一個組件包含html, css, js ,最終都會被編譯到一個js文件)

4.1 煉獄的總體框架

function Router(opts = []) {
  
}
Router.prototype = {
  init: function () {
    
  },

  // 路由註冊
  initRouter: function () {
   
  },

  // 解析url獲取路徑以及對應參數數組化
  getParamsUrl: function () {
    
  },

  // 路由處理
  urlChange: function () {
    
  },

  // 渲染視圖(執行匹配到的js代碼)
  render: function (currentHash) {
    
  },

  // 單個路由註冊
  map: function (item) {
    
  },

  // 切換前
  beforeEach: function (callback) {
    
  },

  // 切換後
  afterEach: function (callback) {
    
  },

  // 路由異步懶加載js文件
  asyncFun: function (file, transition) {
    
  }

}
複製代碼

4.1 煉獄的內部解刨

上面已經列出來煉獄的總體代碼框架,下面咱們就來對每個函數進行編寫。

A init函數

這是煉獄插件在被調用的時候就會執行的方式,固然是用來註冊路由以及綁定對應的路由切換事件的。

init() {
    var oThis = this;

    // 註冊路由
    this.initRouter();
    
    // 頁面加載匹配路由
    window.addEventListener('load', function () {
      oThis.urlChange();
    });
    
    // 路由切換
    window.addEventListener('hashchange', function () {
      oThis.urlChange();
    });
}
    
}
複製代碼

B initRouter函數+map函數

註冊路由,做用就是將路由對象數組參數在初始化的時候就作好路由匹配,好比/index路由對應/js/index.js。

// 路由註冊
initRouter: function() {
    var opts = this.opts;
    opts.forEach((item, index) => {
      this.map(item);
    });
}

// 單個路由註冊
map: function (item) {
    path = item.path.replace(/\s*/g, '');// 過濾空格
    this.routers[path] = {
      callback: (transition) => {
        return this.asyncFun(item.url, transition);
      }, // 回調
      fn: null // 緩存對應的js文件
    }
}

複製代碼

this.routers用來存儲路由對象,執行每個路由的callback函數就是加載對應的js文件。

每個router對象裏面的fn函數的做用是已經加載過的js文件,能夠作到加載一次屢次使用,在路由切換的時候。

C asyncFun函數

這個函數的做用是異步加載目標js文件。原理就是利用手動生成javascript標籤動態插入頁面。固然在加載真實js文件前須要作一個判斷,目標js是否已經加載過。

// 路由異步懶加載js文件
 asyncFun: function (file, transition) {
    // console.log(transition);
    var oThis = this,
      routers = this.routers;

    // 判斷是否走緩存
    if (routers[transition.path].fn) {
      oThis.afterFun && oThis.afterFun(transition)
      routers[transition.path].fn(transition)
    } else {
      var _body = document.getElementsByTagName('body')[0];
      var scriptEle = document.createElement('script');
      scriptEle.type = 'text/javascript';
      scriptEle.src = file;
      scriptEle.async = true;
      SPA_RESOLVE_INIT = null;
      scriptEle.onload = function () {
        oThis.afterFun && oThis.afterFun(transition)
        routers[transition.path].fn = SPA_RESOLVE_INIT;
        routers[transition.path].fn(transition)
      }
      _body.appendChild(scriptEle);
    }
 }
複製代碼

D render函數

看名字都知道這個函數的主要做用就是渲染頁面,在這裏也就是執行加載路由對應的js文件。這裏作了一個判斷,若是存在路由守護的話則走路由守護。

// 渲染視圖(執行匹配到的js代碼)
  render: function (currentHash) {
    var oThis = this;
    // 全局路由守護
    if (oThis.beforeFun) {
      oThis.beforeFun({
        to: {
          path: currentHash.path,
          query: currentHash.query
        },
        next: function () {
          // 執行目標路由對應的js代碼(至關因而組件渲染)
          oThis.routers[currentHash.path].callback.call(oThis, currentHash)
        }
      });
    } else {
      oThis.routers[currentHash.path].callback.call(oThis, currentHash);
    }
  }
複製代碼

E beforeEach函數

路由守護函數,在這裏能夠作一些好比登陸權限判斷的事情,這一點是否是和vue-router的全局路由守護很像呢。

// 切換前
  beforeEach: function (callback) {
    if (Object.prototype.toString.call(callback) === '[object Function]') {
      this.beforeFun = callback;
    } else {
      console.trace('請傳入函數類型的參數');
    }
  },

複製代碼

好了,上面寫好了煉獄的主要代碼,下面咱們就能夠看到對應的效果了。

相關文章
相關標籤/搜索