前端路由hash、history原理及簡單的實踐下

閱讀目錄html

一:什麼是路由?前端有哪些路由?他們有哪些特性?前端

路由是根據不一樣的url地址來顯示不一樣的頁面或內容的功能,這個概念很早是由後端提出的。
後端以前是這麼作的,當咱們訪問 http://xxx.abc.com/xx 的時候,大體流程能夠想象成這樣的:html5

1. 瀏覽器向服務器發出請求。
2. 服務器監聽到80端口,若是有請求過來,那麼就解析url地址。
3. 服務器根據客戶端的路由配置,而後就返回相應的信息(好比html字符串、json數據或圖片等)。
4. 瀏覽器根據數據包的 Content-Type來決定如何解析數據。node

如上就是後端路由最初始的實現方式,那麼既然有後端路由,那爲何還須要咱們前端路由呢?後端路由有一個很大的缺點就是每次路由切換的時候都須要去刷新頁面,而後發出ajax請求,而後將請求數據返回回來,那麼這樣每次路由切換都要刷新頁面對於用戶體驗來講就很差了。所以爲了提高用戶體驗,咱們前端路由就這樣產生了。它就能夠解決瀏覽器不會從新刷新了。webpack

在理解路由以前,咱們下面看下History API有哪些方法:
DOM window對象經過history對象提供了對當前會話瀏覽歷史的訪問,在html4中有以下方法:git

window.history.length: 返回當前會話瀏覽過的頁面數量。
window.history.go(?delta): 接收一個整數做爲參數,按照當前頁面在會話瀏覽歷史記錄中的位置進行移動。若是參數爲0、undefined、null、false 將刷新頁面,至關於執行window.location.reload()方法。若是參數大於瀏覽器瀏覽的數量,或小於瀏覽前的數量的話,什麼都不會作。
window.history.back(). 移動到上一頁。至關於點擊瀏覽器的後退按鈕,等價於 window.history.go(-1);
window.history.forward(). 移動到下一頁,至關於點擊瀏覽器的前進按鈕,等價於window.history.go(1).es6

在html5中,History API 新增了操做會話瀏覽歷史記錄的功能。以下新增的幾個方法:
window.history.state. 該參數是隻讀的,表示與會話瀏覽歷史的當前記錄相關聯的狀態對象。以下圖所示:github

window.history.pushState(data, title, ?url): 在會話瀏覽歷史記錄中添加一條記錄。web

window.history.replaceState(data, title, ?url): 該方法用法和history.pushState方法相似,可是該方法的含義是將修改會話瀏覽歷史的當前記錄,而不是新增一條記錄。也就是說把當前的瀏覽地址換成 replaceState以後的地址,可是瀏覽歷史記錄的總長度並無新增。ajax

注意:執行上面兩種方法後,url地址會發生改變。可是不會刷新頁面。所以有了這些基本知識後,咱們再來看下前端路由。

那麼前端路由也有2種模式,第一種是hash模式,第二種是history模式。咱們來分別看下這兩種知識點及區別以下:

1. hash模式

hash路由模式是這樣的:http://xxx.abc.com/#/xx。 有帶#號,後面就是hash值的變化。改變後面的hash值,它不會向服務器發出請求,所以也就不會刷新頁面。而且每次hash值發生改變的時候,會觸發hashchange事件。所以咱們能夠經過監聽該事件,來知道hash值發生了哪些變化。好比咱們能夠以下簡單的監聽:

function hashAndUpdate () { // todo 匹配 hash 作 dom 更新操做
} window.addEventListener('hashchange', hashAndUpdate);

咱們先來了解下location有哪些屬性,以下:

// 完整的url
location.href // 當前URL的協議,包括 :; 好比 https: 
location.protocol /* 主機名和端口號,若是端口號是80(http)或443(https), 那就會省略端口號,比兔 www.baidu.com:8080 */ location.host // 主機名:好比:www.baidu.com
location.hostname // 端口號;好比8080
location.port // url的路徑部分,從 / 開始; 好比 https://www.baidu.com/s?ie=utf-8,那麼 pathname = '/s'了
location.pathname // 查詢參數,從?開始;好比 https://www.baidu.com/s?ie=utf-8 那麼 search = '?ie=utf-8'
location.search // hash是頁面中的一個片斷,從 # 開始的,好比 https://www.baidu.com/#/a/b 那麼返回值就是:"#/a/b"
location.hash

location.href

咱們經過改變location.href來改變對應的url,看看是否會刷新頁面,咱們作以下測試能夠看到,使用location.href 改變url後並不會刷新頁面,以下代碼在控制檯中演示:

location.hash

改變hash不會觸發頁面跳轉,由於hash連接是當前頁面中的某個片斷,因此若是hash有變化,那麼頁面將會滾動到hash所鏈接的位置。可是頁面中若是不存在hash對應的片斷,則沒有任何效果。好比 a連接。這和 window.history.pushState方法相似,都是不刷新頁面的狀況下更改url。以下也能夠看到操做並無刷新url,以下演示:

hash 和 pushState 對比有以下缺點:

1. hash只能修改url的片斷標識符的部分。而且必須從#號開始,可是pushState且能修改路徑、查詢參數和片斷標識符。pushState比hash更符合前端路由的訪問方式,更加優雅(由於不帶#號)。

2. hash必須和原先的值不一樣,才能新增會話瀏覽歷史的記錄,可是pushState能夠新增相同的url的記錄,以下所示:

1.1 使用hashchange事件來監聽url hash的改變

咱們來演示下,咱們使用node啓動一個服務,而後有一個index.html頁面,該頁面引入了一個js文件,該js文件有以下js代碼:

window.addEventListener('hashchange', function(e) { console.log(e) });

如上代碼就是監聽hash值發生變化的事件,而後咱們訪問該index.html頁面後,而後在控制檯中,作以下操做,以下圖演示:

如上能夠看到;無論咱們是經過location接口直接改變hash值,仍是咱們經過history直接前進或後退操做(改變hash變化),咱們均可以看到都能經過 hashchange該事件進行監聽到url hash的改變。而且不會刷新頁面。

2. history模式

HTML5的History API爲瀏覽器的全局history對象增長了該擴展方法。它是一個瀏覽器的一個接口,在window對象中提供了onpopstate事件來監聽歷史棧的改變,只要歷史棧有信息發生改變的話,就會觸發該事件。提供了以下事件:

window.addEventListener('popstate', function(e) { console.log(e) });

history提供了兩個操做歷史棧的API: history.pushState 和 history.replaceState

history.pushState(data[,title][,url]); // 向歷史記錄中追加一條記錄

history.replaceState(data[,title][,url]); // 替換當前頁在歷史記錄中的信息。

如上html5中新增了上面這兩個方法,該兩個方法也能夠改變url,頁面也不會從新刷新。下面咱們也能夠來作個demo,來監聽下popstate事件,如今在我js裏面放入以下js代碼:

window.addEventListener('popstate', function(e) { console.log(e) });

而後咱們訪問頁面,以下所示:

如上圖所示,咱們使用location.hash, history.go(-1), history.pushState 等方法操做都會觸發 popstate 事件,而且瀏覽器的url地址也會跟着改變。只會改變url地址,且不會從新刷新頁面。

hash模式的特色:

hash模式在瀏覽器地址欄中url有#號這樣的,好比(http://localhost:3001/#/a). # 後面的內容不會傳給服務端,也就是說不會從新刷新頁面。而且路由切換的時候也不會從新加載頁面。

history模式的特色:

瀏覽器地址沒有#, 好比(http://localhost:3001/a); 它也同樣不會刷新頁面的。可是url地址會改變。

二:如何實現簡單的hash路由?

實現hash路由須要知足以下基本條件:
1. url中hash值的改變,並不會從新加載頁面。
2. hash值的改變會在瀏覽器的訪問歷史中增長一條記錄,咱們能夠經過瀏覽器的後退,前進按鈕控制hash值的切換。
3. 咱們能夠經過hashchange事件,監聽到hash值的變化,從而加載不一樣的頁面顯示。

觸發hash值的變化有2種方法:

第一種是經過a標籤,設置href屬性,當點擊a標籤以後,地址欄會改變,同時會觸發hashchange事件。好比以下a連接:
<a href="#/test1">測試hash1</a>

第二種是經過js直接賦值給location.hash,也會改變url,觸發hashchange事件。
location.hash = '#/test1';

所以咱們下面能夠實現一個簡單的demo,html代碼以下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>hash路由demo</title>
</head>
<body>
  <ul>
    <li><a href="#/">我是主頁</a></li>
    <li><a href="#/a">我是a頁面</a></li>
    <li><a href="#/b">我是b頁面</a></li>
  </ul>
</body>
</html>

而後咱們hash.js 代碼以下:

class HashRouter { constructor() { // 存儲hash與callback鍵值對
    this.routes = {}; // 保存當前的hash
    this.currentHash = ''; // 綁定事件
    const hashChangeUrl = this.hashChangeUrl.bind(this); // 頁面加載事件
    window.addEventListener('load', hashChangeUrl, false); // 監聽hashchange事件
    window.addEventListener('hashchange', hashChangeUrl, false); } // path路徑和callback函數對應起來,而且使用 上面的this.routes存儲起來
 route(path, callback) { this.routes[path] = callback || function() {}; } hashChangeUrl() { /* 獲取當前的hash值 location.hash 獲取的值爲:"#/a, 所以 location.hash.slice(1) = '/a' 這樣的 */
    this.currentHash = location.hash.slice(1) || '/'; // 執行當前hash對應的callback函數
    this.routes[this.currentHash](); } } // 初始化
const Router = new HashRouter(); const body = document.querySelector('body'); const changeColor = function(color) { body.style.backgroundColor = color; }; // 註冊函數
Router.route('/', () => { changeColor('red'); }); Router.route('/a', () => { changeColor('green'); }); Router.route('/b', () => { changeColor('#CDDC39'); }); 

如上就是一個很是簡化的hash路由了,首先咱們代碼也是很是的簡化(我相信你們都能看懂),首先如上js代碼有一個route函數,該函數的做用就是初始化對應的路由和函數進行綁定起來,把他們保存到 this.routes 對象裏面去,而後使用 hashchange 事件進行監聽,若是觸發了該事件,就找到該路由,而後觸發對應的函數便可。咱們點擊某個a連接就會調用對應的函數,或者咱們能夠在控制檯中使用 location.hash = '/b'; 來改變值也會觸發的。
查看效果

github源碼查看

三:如何實現簡單的history路由?

 代碼以下:

class HistoryRoutes { constructor() { // 保存對應鍵和函數
    this.routes = {}; // 監聽popstate事件
    window.addEventListener('popstate', (e) => { const path = this.getState(); this.routes[path] && this.routes[path](); }); } // 獲取路由路徑
 getState() { const path = window.location.pathname; return path ? path : '/'; } route(path, callback) { this.routes[path] = callback || function() {}; } init(path) { history.replaceState(null, null, path); this.routes[path] && this.routes[path](); } go(path) { history.pushState(null, null, path); this.routes[path] && this.routes[path](); } } window.Router = new HistoryRoutes(); console.log(location.pathname); Router.init(location.pathname); const body = document.querySelector('body'); const changeColor = function(color) { body.style.backgroundColor = color; }; // 註冊函數
Router.route('/', () => { changeColor('red'); }); Router.route('/a', () => { changeColor('green'); }); Router.route('/b', () => { changeColor('#CDDC39'); }); const ul = document.querySelector('ul'); ul.addEventListener('click', e => { console.log(e.target); if (e.target.tagName === 'A') { e.preventDefault(); Router.go(e.target.getAttribute('href')); } });

查看效果

github源碼查看

四:hash和history路由一塊兒實現

 首先看下項目目錄結構以下:

|----項目demo |  |--- .babelrc # 解決es6語法問題 |  |--- node_modules # 全部依賴的包 |  |--- dist           # 打包後的頁面  訪問該頁面使用:http://0.0.0.0:7799/dist
|  |--- js |  | |--- base.js |  | |--- hash.js |  | |--- history.js |  | |--- routerList.js |  | |--- index.js |  |--- package.json # 依賴的包文件 |  |--- webpack.config.js # webpack打包文件 |  |--- index.html    # html 頁面

index.html 頁面以下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>hash+history路由demo</title>
</head>
<body>
  <div id="app"></div>
  <ul class="list">
    <li><a href="/">我是主頁</a></li>
    <li><a href="/hash">我是hash頁面</a></li>
    <li><a href="/history">我是history頁面</a></li>
  </ul>
</body>
</html>

js/base.js 代碼以下:

const ELEMENT = document.querySelector('#app'); export class BaseRouter { constructor(list) { this.list = list; } render(state) { let ele = this.list.find(ele => ele.path === state); ele = ele ? ele : this.list.find(ele => ele.path === '*'); ELEMENT.innerText = ele.component; } }

如上代碼base.js 該類就一個構造函數,在hash.js 或 history.js 會繼承該類,所以hash或history類都有該render() 方法。該render方法做用就是找到對應的路徑是否等於 routerlist中的path,若是找到的話,就把對應的component裏面的內容賦值給 id爲app的元素。
js/hash.js 代碼以下:

import { BaseRouter } from './base.js'; // hash路由繼承了BaseRouter
export class HashRouter extends BaseRouter { constructor(list) { super(list); this.handler(); // 監聽hash事件變化,而且從新渲染頁面
    window.addEventListener('hashchange', (e) => { this.handler(); }); } // 渲染
 handler() { const state = this.getState(); this.render(state); } // 獲取當前的hash
 getState() { const hash = window.location.hash; return hash ? hash.slice(1) : '/'; } // 獲取完整的url
 getUrl(path) { const href = window.location.href; const index = href.indexOf('#'); const base = index > -1 ? href.slice(0, index) : href; return `${base}#${path}`; } // hash值改變的話,實現壓入
 push(path) { window.location.hash = path; } // 替換功能
 replace(path) { window.location.replace(this.getUrl(path)); } // 模擬history.go 功能,實現前進/後退功能
 go(n) { window.history.go(n); } }

hash 代碼如上;該類裏面有hashchange事件監聽hash值的變化,若是變化的話就會調用 handler 函數,在執行該函數中的render方法以前,會先調用 getState 方法,該方法目的是獲取當前的hash。好比getState方法中使用location.hash 獲取的hash會是 '#/x' 這樣的,而後會返回 '/x'。
getUrl() 方法是獲取完整的url,能夠看如上代碼理解下便可,其餘的就是 push,replace,go方法。

js/history.js 代碼以下:

import { BaseRouter } from './base.js'; export class HistoryRouter extends BaseRouter { constructor(list) { super(list); this.handler(); // 監聽歷史棧變化,變化時候從新渲染頁面
    window.addEventListener('popstate', (e) => { this.handler(); }); } // 渲染
 handler() { const state = this.getState(); this.render(state); } // 獲取路由路徑
 getState() { const path = window.location.pathname; return path ? path : '/'; } /* pushState方法實現壓入功能,PushState不會觸發popstate事件, 所以咱們須要手動調用handler函數 */ push(path) { window.history.pushState(null, null, path); this.handler(); } /* pushState方法實現替換功能,replaceState不會觸發popstate事件, 所以咱們須要手動調用handler函數 */ replace(path) { window.history.replaceState(null, null, path); this.handler(); } go(num) { window.history.go(num); } };

代碼和hash.js 代碼相似。

js/routerList.js 代碼以下:

export const ROUTERLIST = [ { path: '/', name: 'index', component: '這是首頁' }, { path: '/hash', name: 'hash', component: '這是hash頁面' }, { path: '/history', name: 'history', component: '這是history頁面' }, { path: '*', component: '404頁面' } ];

js/index.js 代碼以下:

import { HashRouter } from './hash'; import { HistoryRouter } from './history'; import { ROUTERLIST } from './routerList'; // 路由模式,默認爲hash
const MODE = 'history'; class WebRouter { constructor({ mode = 'hash', routerList }) { this.router = mode === 'hash' ? new HashRouter(routerList) : new HistoryRouter(routerList); } push(path) { // 返回 this.router 所以有 hash或history中的push方法
    this.router.push(path); } replace(path) { this.router.replace(path); } go(num) { this.router.go(num); } } const webRouter = new WebRouter({ mode: MODE, routerList: ROUTERLIST }); document.querySelector('.list').addEventListener('click', e => { const event = e || window.event; event.preventDefault(); if (event.target.tagName === 'A') { const url = event.target.getAttribute('href'); !url.indexOf('/') ? webRouter.push(url) : webRouter.go(url); } });

如上就是全部的代碼了,仔細研究下看到這樣編寫代碼的好處,就是hash.js,和 history.js 代碼分離出來了,而且都有對應的方法,而後在index.js 初始化對應的代碼。最後使用dom點擊事件傳遞對應的路由進去。
查看效果

github源碼查看

注意:github源碼下載後會有一個dist文件夾,該文件是打包後的文件,也就是說咱們改完裏面的代碼後,先 npm run build 後生成該文件,而後咱們再運行下 npm run dev 後,使用 http://0.0.0.0:7799/dist/ 訪問頁面進行切換。

相關文章
相關標籤/搜索