1、歷史
路由最早興起於後端,路由就是:使用者端發起一個HTTP請求,後臺用一個函式進行處理,即 函式 => 路由。到後來前端出現ajax,頁面可以進行區域性重新整理,出現SPA應用。
2、SPA
單頁面應用僅在初始化時,載入頁面相應的HTML、CSS、JavaScript。一旦頁面載入完成,SPA不會因為使用者的操作,而進行頁面的重新載入和跳轉,而是利用路由機制在實現HTML的切換,UI與使用者互動,避免重複的載入。
優點:
- 使用者體驗好、快、內容改變不需要重新載入頁面,避免了不必要的跳轉和重複渲染。並不需要處理HTML檔案的請求,這樣節約了HTTP傳送延遲。
- 基於上面一點,SPA相對伺服器壓力小很多。
- 前後端職責分離,架構清楚,前端進行互動邏輯,後端服務資料處理
缺點:
- 初始化時耗時比較多,為實現單頁面web應用功能及顯示效果,需要將JavaScript和CSS統一載入,雖然可以部分按需載入。
- SPA為單頁面,但是瀏覽器的前進後臺不能使用,所有頁面的切換需要自己建立一個堆疊進行管理。
- 所有內容都在一個頁面,SEO天然處於弱勢。
3、前端路由
為了解決SPA應用的缺點,前端路由出現。前端路由結合SPA更新檢視但是瀏覽器不重新整理頁面,只是重新渲染部門子頁面,載入速度快,頁面反應靈活等優點實現。
目前瀏覽器前端路由實現的方式有兩種
- URL #hash
- H5的history interface
4、URL #hash
在最開始H5還沒有流行其他的時候,SPA採用URL 的hash值作為錨點,獲取錨點值,監聽其改變,在進行對應的子頁面渲染。window物件有一個專門監聽hash變化的時候,那就是onhashchange。 監聽到URL的變化,我們如何進行頁面的載入了。當頁面傳送改變了,在執行頁面載入有三種方式 查詢節點內容並改變。 使用import匯出js檔案,js檔案export模板字串。 利用Ajax載入不同hash的HTML模組。
4.1 查詢節點內容並改變
<h1 id="page"></h1>
<a href="#/page1">page1</a>
<a href="#/page2">page2</a>
<a href="#/page3">page3</a>
<script>
window.addEventListener('hashchange', e => {
e.preventDefault();
document.querySelector('#page').innerHTML = location.hash;
})
</script>
複製程式碼
4.2 import方法
const str = `
<div>
我是import進來的JS檔案
</div>
`
export default str
複製程式碼
<body>
<h1 id="page"></h1>
<a href="#/page1">page1</a>
<a href="#/page2">page2</a>
<a href="#/page3">page3</a>
<script type="module">
import demo1 from './import.js'
document.querySelector('#page').innerHTML = demo1;
window.addEventListener('hashchange', e => {
e.preventDefault();
document.querySelector('#page').innerHTML = location.hash;
})
</script>
</body>
複製程式碼
4.3 Ajax請求,例如使用jQuery封裝好的ajax請求方式。
<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>
複製程式碼
4.4 通用方式
<body>
<h1 id="page">空白頁</h1>
<a href="#/page1">page1</a>
<a href="#/page2">page2</a>
<a href="#/page3">page3</a>
</body>
<script type="text/javascript">
import demo1 from './import.js';
// 建立路由類
class Router {
constructor() {
// 路由物件
this.routes = {};
// 當前URL
this.curUrl = '';
}
// 根據RUL或者對應URL的回撥函式
route(path, callback = () => {}) {
this.routes[path] = callback;
}
// 重新整理
refresh() {
// 當前hash值
this.curUrl = window.location.hash.slice(1) || '/';
// 執行方法
this.routes[this.curUrl] && this.routes[this.curUrl]();
}
// 初始化監聽函式
initPage() {
window.addEventListener('load', this.refresh.bind(this), false);
window.addEventListener('hashchange', this.refresh.bind(this), false);
}
}
// 例項化路由
window.router = new Router();
// 初始化
window.router.init();
// 獲取節點
const content = document.getElementById('page');
// 直接改變
Router.route('/page1', () => {
content.innerHTML = 'page1'
});
// import改變
Router.route('/page2', () => {
content.innerHTML = demo1
});
// AJAX改變
Router.route('/page3', () => {
$.ajax({
url: './demo2.html',
success: (res) => {
content.innerHTML = res
}
})
});
</script>
複製程式碼
5、h5 interface
5.1 向歷史記錄棧中新增記錄:pushState(state, title, url);
- state: 一個 JS 物件(不大於640kB),主要用於在 popstate 事件中作為引數被獲取。如果不需要這個物件,此處可以填 null
- title: 新頁面的標題,部分瀏覽器(比如 Firefox )忽略此引數,因此一般為 null
- url: 新歷史記錄的地址,可為頁面地址,也可為一個錨點值,新 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 事件中獲取
// 不會觸發頁面重新整理
// 這個方法僅僅是新增了一條最新記錄
複製程式碼
5.2 改變當前歷史記錄而不是新增新紀錄:replaceState(state, title, url);
PS:
將URL設定為錨點時將不回觸發hashchange。
URL設定為不同域名、埠號、協議,將會報錯,這是由於瀏覽器的同源策略的限制。
複製程式碼
5.3 popState(state, title, url);
當瀏覽器歷史棧出現變化時,就會觸發popState事件。
PS:
呼叫pushState和replaceState時不會觸發popState,只有使用者點選前進後退
或者使用JavaScript的go、back、forword方法時才會觸發,並且對於不同檔案
的切換也是不會觸發的
複製程式碼
h5 interface 手寫一個路由類
<body>
<h1 id="page">空白頁</h1>
<a class="route" href="#/page1">page1</a>
<a class="route" href="#/page2">page2</a>
<a class="route" href="#/page3">page3</a>
</body>
<script type="text/javascript">
import demo1 from './import.js';
// 建立路由類
class Router {
constructor() {
// 路由物件
this.routes = {};
// 當前URL
this.curUrl = '';
}
// 根據RUL或者對應URL的回撥函式
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.curUrl] && this.routes[this.curUrl](type);
}
// 初始化監聽函式
initPage() {
window.addEventListener('load', () => {
// 獲取當前 URL 路徑
this.curUrl = location.href.slice(location.href.indexOf('/', 8));
this.refresh(this.curUrl, 2)
}, false);
window.addEventListener('popstate', () => {
this.curUrl = history.state.path;
this.refresh(this.curUrl, 2);
}, false);
const links = document.querySelectorAll('.route')
links.forEach((item) => {
// 覆蓋 a 標籤的 click 事件,防止預設跳轉行為
item.onclick = (e) => {
e.preventDefault();
// 獲取修改之後的 URL
this.curUrl = e.target.getAttribute('href');
// 渲染
this.refresh(this.curUrl, 2);
}
});
}
}
// 例項化路由
window.router = new Router();
// 初始化
window.router.init();
// 獲取節點
const content = document.getElementById('page');
// 直接改變
Router.route('/page1', () => {
content.innerHTML = 'page1'
});
// import改變
Router.route('/page2', () => {
content.innerHTML = demo1
});
// AJAX改變
Router.route('/page3', () => {
$.ajax({
url: './demo2.html',
success: (res) => {
content.innerHTML = res
}
})
});
</script>
複製程式碼
6、Vue-router的路由實現
專案地址:github.com/vuejs/vue-r… 先看一段Vue-router原始碼(其他已省略)
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
複製程式碼
我們發現在Vue-router中,history例項和mode的型別有關。不同的型別實現不一樣。 首先我們來看看HTML5History原始碼實現(/src/index.js)
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.replace(location, resolve, reject)
})
} else {
this.history.replace(location, onComplete, onAbort)
}
}
go (n: number) {
this.history.go(n)
}
back () {
this.go(-1)
}
forward () {
this.go(1)
}
複製程式碼
我們發現其實我們在日常專案中使用的push和replace方法就是呼叫的this.history中的方法,也就是 HTML5History類的方法,我們找到HTML5History這個類。在這裡/src/history/html5.js
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
複製程式碼
在這個HTML5History類中定義了push和replace方法,原來如此,在內部的使用又使用了上文提到 h5 interface的pushState和replaceState,哈哈哈,其實這裡的pushState和replaceState並不是h5 interface的pushState和replaceState。而是呼叫了import { pushState, replaceState, supportsPushState } from '../util/push-state'中的pushState和replaceState。來看看push-state原始檔的實現。
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
複製程式碼
在這裡才是真正的使用了h5 interface的pushState和replaceState,真是兜兜轉轉一圈終於找到了呀
總結:
router 例項呼叫的 push 實際是 history 的方法,通過 mode 來確定匹配 history
的實現方案,從程式碼中我們看到,push 呼叫了 src/util/push-state.js 中被改寫過
的 pushState 的方法,改寫過的方法會根據傳入的引數 replace?: boolean 來進行
判斷呼叫 pushState 還是 replaceState ,同時做了錯誤捕獲,如果,history 無刷
新修改訪問路徑失敗,則呼叫 window.location.replace(url) ,有重新整理的切換使用者訪
問地址 ,同理 pushState 也是這樣。這裡的 transitionTo 方法主要的作用是做檢視
的跟新及路由跳轉監測,如果 url 沒有變化(訪問地址切換失敗的情況),在
transitionTo 方法內部還會呼叫一個 ensureURL 方法,來修改 url。
transitionTo 方法中應用的父方法比較多,這裡不做長篇贅述。
複製程式碼
其他兩種方式hash模式abstract模式內部其實也是比較簡單的,我這裡就不一一為大家來解析原始碼。