總所周知,隨着前端應用的業務功能起來越複雜,用戶對於使用體驗的要求愈來愈高,單面(SPA
)成爲前端應用的主流形式。而大型單頁應用最顯著特色之一就是採用的前端路由跳轉子頁面系統,經過改變頁面的URL
,在不從新請求頁面的狀況下,更新頁面視圖。javascript
更新視圖可是瀏覽器不從新渲染整個頁面,只是從新渲染部分子頁面,加載速度快,頁面反應靈活,這是 SPA
的優點,這也是前端路由原理的核心,這會給人一種彷彿在操做 APP
同樣的感受,目前在瀏覽器環境中實現這一功能的方式主要有兩種:css
URL
的 hash(#)
H5
新增方法 History interface
URL
的Hash(#)
在 H5
尚未流行開來時,通常 SPA
都採用 url
的 hash(#)
做爲錨點,獲取到 # 以後的值,並監聽其改變,再進行渲染對應的子頁面。網易雲音樂官網就是利用的此技術。html
例如,你的地址爲http://localhost:8888/#/abc
那麼利用 location.hash
輸出的內容就爲 #/abc
。前端
那麼我就先從 location
這個對象提及。vue
先來看看location
的官方屬性有哪些java
屬性 | 描述 |
---|---|
hash | 設置或返回從 # 開始的 URL (錨) |
host | 設置或返回主機名和當前 URL 的端口號 |
hostname | 設置或返回當前 URL 的主機名 |
href | 設置或返回完整的 URL |
pathname | 設置或返回當前 URL 的路徑部分 |
port | 設置或返回當前 URL 的端口號 |
protocol | 設置或返回當前 URL 的協議 |
search | 設置或返回從 ? 開始的 URL 部分 |
由上表格能夠知道,咱們能夠輕易的獲取到 # 以後的部分,那麼拿到這個部分咱們怎麼監聽其變化以及對應的子頁面進行改變呢?jquery
window
對象中有一個事件是專門監聽hash
的變化,那就是onhashchange
,首先咱們須要監聽此事件:git
<body>
<h1 id="id"></h1>
<a href="#/id1">id1</a>
<a href="#/id2">id2</a>
<a href="#/id3">id3</a>
</body>
<script> window.addEventListener('hashchange', e => { e.preventDefault() document.querySelector('#id').innerHTML = location.hash }) </script>
複製代碼
可見此時咱們已經徹底監聽到了 URL
的變化,頁面上的內容也對應改變了。 那麼,該如何載入不一樣的頁面呢,目前來講有三種方式:github
import
一個 JS
文件,文件內部 export
模版字符串AJAX
加載對應的 HTML
模版第一種方式已經演示過,不過這種方式侷限性太大,下面我會演示另外兩種方式加載頁面。ajax
import
方式定義一個 JS
文件,名爲 demo1.js
,在裏面輸入內容:
const str = ` <div> 我是import進來的JS文件 </div> `
export default str
複製代碼
在主文件裏 import
進來,並進行測試(使用 Chrome
必定要使用服務器開啓,或者直接用火狐打開):
<body>
<h1 id="id"></h1>
<a href="#/id1">id1</a>
<a href="#/id2">id2</a>
<a href="#/id3">id3</a>
</body>
<!-- 在 HTML 導入文件記得要加上 type="module" -->
<script type="module"> import demo1 from './demo1.js' document.querySelector('#id').innerHTML = demo1 window.addEventListener('hashchange', e => { e.preventDefault() document.querySelector('#id').innerHTML = location.hash }) </script>
複製代碼
可見導入文件已經生效,目前大部分框架編譯事後是採用相似此種方式處理。
例如,vue
框架,.vue
文件是一個自定義的文件類型,用類 HTML
語法描述一個 Vue
組件。每一個 .vue
文件包含三種類型的頂級語言塊 <template>
, <script>
和 <style>
,vue-loader
會解析文件,提取每一個語言塊,若有必要會經過其它 loader
處理,最後將他們組裝成一個 CommonJS
模塊,module.exports
出一個 Vue.js
組件對象。。
AJAX
方式本篇文章是詳解路由機制,AJAX
就直接採用 JQuery
這個輪子。
定義一個 HTML
文件,名爲 demo2.html
,在裏面寫入一些內容(因爲主頁面已經有head
,body
等根標籤,此文件只需寫入須要替換的標籤):
<div>
我是AJAX加載進來的HTML文件
</div>
複製代碼
咱們在主文件裏寫入,並進行測試:
<body>
<h1 id="id"></h1>
<a href="#/id1">id1</a>
<a href="#/id2">id2</a>
<a href="#/id3">id3</a>
</body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="module"> // import demo1 from './demo1.js' // document.querySelector('#id').innerHTML = demo1 $.ajax({ url: './demo2.html', success: (res) => { document.querySelector('#id').innerHTML = res } }) window.addEventListener('hashchange', e => { e.preventDefault() document.querySelector('#id').innerHTML = location.hash }) </script>
複製代碼
可見,利用 AJAX
加載進來的文件也已經生效。
既然加載不一樣頁面的內容都已經生效,那麼只須要包裝一下咱們的監聽,利用觀察者模式封裝路由的變化:
<body>
<h1 id="id">我是空白頁</h1>
<a href="#/id1">id1</a>
<a href="#/id2">id2</a>
<a href="#/id3">id3</a>
</body>
<script type="module"> import demo1 from './demo1.js' // 建立一個 newRouter 類 class newRouter { // 初始化路由信息 constructor() { this.routes = {}; this.currentUrl = ''; } // 傳入 URL 以及 根據 URL 對應的回調函數 route(path, callback = () => {}) { this.routes[path] = callback; } // 切割 hash,渲染頁面 refresh() { this.currentUrl = location.hash.slice(1) || '/'; this.routes[this.currentUrl] && this.routes[this.currentUrl](); } // 初始化 init() { window.addEventListener('load', this.refresh.bind(this), false); window.addEventListener('hashchange', this.refresh.bind(this), false); } } // new 一個 Router 實例 window.Router = new newRouter(); // 路由實例初始化 window.Router.init(); // 獲取關鍵節點 var content = document.querySelector('#id'); Router.route('/id1', () => { content.innerHTML = 'id1' }); Router.route('/id2', () => { content.innerHTML = demo1 }); Router.route('/id3', () => { $.ajax({ url: './demo2.html', success: (res) => { content.innerHTML = res } }) }); </script>
複製代碼
效果以下:
至此,利用 hash(#)
進行前端路由管理都已實現。
H5
新增方法 History interface
上面使用的 hash
法實現路由當然不錯,可是問題就是實在太醜~ 若是在微信或者其餘不顯示 URL
的 APP
中使用,倒也無所謂,可是若是在通常的瀏覽器中使用就會遇到問題了。
由此,H5
的 History
模式,解決了這一問題。
在 H5
以前, History
僅僅只有一下幾個 API
:
API | 說明 |
---|---|
back() |
回退到上次訪問的 URL (與瀏覽器點擊後退按鈕相同) |
forward() |
前進到回退以前的 URL (與瀏覽器點擊向前按鈕相同) |
go(n) |
n 接收一個整數,移動到該整數指定的頁面,好比go(1) 至關於forward() ,go(-1) 至關於 back() ,go(0) 至關於刷新當前頁面 |
若是移動的位置超出了訪問歷史的邊界,以上三個方法並不報錯,而是靜默失敗。
然而,到了 H5
的時代,新的 H5
則賦予了其更多的新特性:
默認狀況下,瀏覽器會緩存當前會話頁面,這樣當下一個頁面點擊後退按鈕,或前一個頁面點擊前進按鈕,瀏覽器便會從緩存中提取並加載此頁面,這個特性被稱爲「往返緩存」。
PS: 此緩存會保留頁面數據、DOM和js狀態,其實是將整個頁面無缺完好地保留。
瀏覽器支持度: IE10+
JS
對象(不大於640kB),主要用於在 popstate
事件中做爲參數被獲取。若是不須要這個對象,此處能夠填 null
null
url
必須與當前 url
處於同一個域,不然將拋出異常,此參數若沒有特別標註,會被設爲當前文檔 url
栗子:
// 如今是 localhost/1.html
const stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');
// 瀏覽器地址欄將當即變成 localhost/2.html
// 但!!!
// 不會跳轉到 2.html
// 不會檢查 2.html 是否存在
// 不會在 popstate 事件中獲取
// 不會觸發頁面刷新
// 這個方法僅僅是添加了一條最新記錄
複製代碼
除此以外,仍有幾點須要注意:
url
設爲錨點值時不會觸發 hashchange
XSS
、 CSRF
等攻擊方式瀏覽器支持度: IE10+
pushstate
popstate
瀏覽器支持度: IE10+
state
。定義:每當同一個文檔的瀏覽歷史(即 history
對象)出現變化時,就會觸發 popstate
事件。
注意:若僅僅調用 pushState
方法或 replaceState
方法 ,並不會觸發該事件,只有用戶點擊瀏覽器倒退按鈕和前進按鈕,或者使用 JavaScript
調用 back
、 forward
、 go
方法時纔會觸發。另外,該事件只針對同一個文檔,若是瀏覽歷史的切換,致使加載不一樣的文檔,該事件也不會觸發。
栗子:
window.onpopstate= (event) => {
  console.log(event.state) //當前歷史記錄的state對象
}
複製代碼
瞭解了這麼多內容,那麼就讓咱們開始實現 History
模式的路由吧!
咱們將上面的 HTML
稍稍改造下,請你們耐心分析:
<body>
<h1 id="id">我是空白頁</h1>
<a class="route" href="/id1">id1</a>
<a class="route" href="/id2">id2</a>
<a class="route" href="/id3">id3</a>
</body>
複製代碼
import demo1 from './demo1.js'
// 建立一個 newRouter 類
class newRouter {
// 初始化路由信息
constructor() {
this.routes = {};
this.currentUrl = '';
}
route(path, callback) {
this.routes[path] = (type) => {
if (type === 1) history.pushState( { path }, path, path );
if (type === 2) history.replaceState( { path }, path, path );
callback()
};
}
refresh(path, type) {
this.routes[this.currentUrl] && this.routes[this.currentUrl](type);
}
init() {
window.addEventListener('load', () => {
// 獲取當前 URL 路徑
this.currentUrl = location.href.slice(location.href.indexOf('/', 8))
this.refresh(this.currentUrl, 2)
}, false);
window.addEventListener('popstate', () => {
this.currentUrl = history.state.path
this.refresh(this.currentUrl, 2)
}, false);
const links = document.querySelectorAll('.route')
links.forEach((item) => {
// 覆蓋 a 標籤的 click 事件,防止默認跳轉行爲
item.onclick = (e) => {
e.preventDefault()
// 獲取修改以後的 URL
this.currentUrl = e.target.getAttribute('href')
// 渲染
this.refresh(this.currentUrl, 2)
}
})
}
}
// new 一個 Router 實例
window.Router = new newRouter();
// 實例初始化
window.Router.init();
// 獲取關鍵節點
var content = document.querySelector('#id');
Router.route('/id1', () => {
content.innerHTML = 'id1'
});
Router.route('/id2', () => {
content.innerHTML = demo1
});
Router.route('/id3', () => {
$.ajax({
url: './demo2.html',
success: (res) => {
content.innerHTML = res
}
})
});
複製代碼
演示圖以下所示:
通常場景下,hash
和 history
均可以,除非你更在乎顏值,#
符號夾雜在 URL
裏看起來確實有些不太美麗。 另外,根據 Mozilla Develop Network 的介紹,調用 history.pushState()
相比於直接修改 hash
,存在如下優點:
pushState()
設置的新 URL
能夠是與當前 URL
同源的任意 URL
;而 hash
只可修改 #
後面的部分,所以只能設置與當前 URL
同文檔的 URL
pushState()
設置的新 URL
能夠與當前 URL
如出一轍,這樣也會把記錄添加到棧中;而 hash
設置的新值必須與原來不同纔會觸發動做將記錄添加到棧中pushState()
經過 stateObject
參數能夠添加任意類型的數據到記錄中;而 hash
只可添加短字符串;pushState()
可額外設置 title
屬性供後續使用。這麼一看 history
模式充滿了 happy,感受徹底能夠替代 hash
模式,但其實 history
也不是樣樣都好,雖然在瀏覽器裏遊刃有餘,但真要經過 URL
向後端發起 HTTP
請求時,二者的差別就來了。尤爲在用戶手動輸入 URL
後回車,或者刷新(重啓)瀏覽器的時候。
hash
模式下,僅 hash
符號以前的內容會被包含在請求中,如 http://www.qqq.com
,所以對於後端來講,即便沒有作到對路由的全覆蓋,也不會返回 404
錯誤。history
模式下,前端的 URL
必須和實際向後端發起請求的 URL
一致,如 http://www.qqq.com/book/id
。若是後端缺乏對 /book/id
的路由處理,將返回 404
錯誤。Vue-Router
官網裏如此描述:「不過這種模式要玩好,還須要後臺配置支持……因此呢,你要在服務端增長一個覆蓋全部狀況的候選資源:若是 URL
匹配不到任何靜態資源,則應該返回同一個 index.html
頁面,這個頁面就是你 app
依賴的頁面。」Apache
或 Nginx
)進行簡單的路由配置,同時搭配前端路由的 404
頁面支持。最後很差意思推廣一下我基於 Taro
框架寫的組件庫:MP-ColorUI。
能夠順手 star 一下我就很開心啦,謝謝你們。