JS 專題系列-前端路由

呂肥肥已存在發表於2019-01-29

1. 什麼是路由

路由是根據不同的 url 地址展示不同的內容或頁面

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

後來頁面越來越複雜,伺服器壓力越來越大,隨著 ajax(非同步重新整理技術) 的出現,頁面實現非 reload 就能重新整理資料,讓前端也可以控制 url 自行管理,前端路由由此而生。

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

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

1 Hash路由

我們經常在 url 中看到 #,這個 # 有兩種情況,一個是我們所謂的錨點,比如典型的回到頂部按鈕原理、Github 上各個標題之間的跳轉等,路由裡的 # 不叫錨點,我們稱之為 hash,大型框架的路由系統大多都是雜湊實現的。

我們需要一個根據監聽雜湊變化觸發的事件 —— hashchange 事件

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

我們用 window.location 處理雜湊的改變時不會重新渲染頁面,而是當作新頁面加到歷史記錄中,這樣我們跳轉頁面就可以在 hashchange 事件中註冊 ajax 從而改變頁面內容。

window.addEventListener('hashchange', function () {
  <!--這裡你可以寫你需要的程式碼-->
});
複製程式碼

2 History 路由

HTML5的History API 為瀏覽器的全域性history物件增加的擴充套件方法。

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

這兩個 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的變化

JS 專題系列-前端路由

注意:這裡的 url 不支援跨域,比如你在不是百度域名下輸入上面的程式碼。

JS 專題系列-前端路由

不過這種模式之前在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。

JS 專題系列-前端路由

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('請傳入函式型別的引數');
    }
  },

複製程式碼

好了,上面寫好了煉獄的主要程式碼,下面我們就可以看到對應的效果了。

JS 專題系列-前端路由

相關文章