在單頁應用上,前端路由並不陌生。單頁應用是指在瀏覽器中運行的應用,在使用期間頁面不會重新加載。
基本原理:以 hash 形式(也可以使用 History API 來處理)為例,當 url 的 hash 發生改變時,觸發 hashchange 注冊的回調,回調中去進行不同的操作,進行不同的內容的展示。
基於hash的前端路由優點是:能兼容低版本的瀏覽器。
history 是 HTML5 才有的新 API,可以用來操作瀏覽器的 session history (會話歷史)。
從性能和用戶體驗的層面來比較的話,后端路由每次訪問一個新頁面的時候都要向服務器發送請求,然后服務器再響應請求,這個過程肯定會有延遲。而前端路由在訪問一個新頁面的時候僅僅是變換了一下路徑而已,沒有了網絡延遲,對於用戶體驗來說會有相當大的提升。
SPA的核心即是前端路由。何為路由呢?說的通俗點就是網址,比如https://www.talkingcoder.com/article/6333760306895988699;專業點就是每次GET或者POST等請求,在服務端有一個專門的正則配置列表,然后匹配到具體的一條路徑后,分發到不同的Controller,然后進行各種操作后,最終將html或數據返回給前端,這就完成了一次IO。當然,目前絕大多數的網站都是這種后端路由,也就是多頁面的,這樣的好處有很多,比如頁面可以在服務端渲染好直接返回給瀏覽器,不用等待前端加載任何js和css就可以直接顯示網頁內容,再比如對SEO的友好等。那SPA的缺點也是很明顯的,就是模板是由后端來維護或改寫。前端開發者需要安裝整套的后端服務,必要還得學習像PHP或Java這些非前端語言來改寫html結構,所以html和數據、邏輯混為一談,維護起來即臃腫也麻煩。然后就有了前后端分離的開發模式,后端只提供API來返回數據,前端通過Ajax獲取到數據后,再用一定的方式渲染到頁面里,這么做的優點就是前后端做的事情分的很清楚,后端專注在數據上,前端專注在交互和可視化上,從此前后搭配,干活不累,如果今后再開發移動App,那就正好能使用一套API了,當然缺點也很明顯,就是首屏渲染需要時間來加載css和js。這種開發模式被很多公司認同,也出現了很多前端技術棧,比如以jQuery+artTemplate+Seajs(requirejs)+gulp為主的開發模式所謂是萬金油了。在Node.js出現后,這種現象有了改善,就是所謂的大前端,得益於Node.js和JavaScript的語言特性,html模板可以完全由前端來控制,同步或異步渲染完全由前端自由決定,並且由前端維護一套模板,這就是為什么在服務端使用artTemplate、React以及即將推出的Vue2.0原因了。那說了這么多,到底怎樣算是SPA呢,其實就是在前后端分離的基礎上,加一層前端路由。
前端路由,即由前端來維護一個路由規則。實現有兩種,一種是利用url的hash,就是常說的錨點(#),JS通過hashChange事件來監聽url的改變,IE7及以下需要用輪詢;另一種就是HTML5的History模式,它使url看起來像普通網站那樣,以"/"分割,沒有#,但頁面並沒有跳轉,不過使用這種模式需要服務端支持,服務端在接收到所有的請求后,都指向同一個html文件,不然會出現404。所以,SPA只有一個html,整個網站所有的內容都在這一個html里,通過js來處理。
前端路由的優點有很多,比如頁面持久性,像大部分音樂網站,你都可以在播放歌曲的同時,跳轉到別的頁面而音樂沒有中斷,再比如前后端徹底分離。前端路由的框架,通用的有Director,更多還是結合具體框架來用,比如Angular的ngRouter,React的ReactRouter,以及我們后面用到的Vue的vue-router。這也帶來了新的開發模式:MVC和MVVM。如今前端也可以MVC了,這也是為什么那么多搞Java的鍾愛於Angular。
開發一個前端路由,主要考慮到頁面的可插拔、頁面的生命周期、內存管理等。
(function() {
window.Router = function() {
var self = this;
self.hashList = {}; /* 路由表 */
self.index = null;
self.key = '!';
window.onhashchange = function() {
self.reload();
};
};
/**
* 添加路由,如果路由已經存在則會覆蓋
* @param addr: 地址
* @param callback: 回調函數,調用回調函數的時候同時也會傳入相應參數
*/
Router.prototype.add = function(addr, callback) {
var self = this;
self.hashList[addr] = callback;
};
/**
* 刪除路由
* @param addr: 地址
*/
Router.prototype.remove = function(addr) {
var self = this;
delete self.hashList[addr];
};
/**
* 設置主頁地址
* @param index: 主頁地址
*/
Router.prototype.setIndex = function(index) {
var self = this;
self.index = index;
};
/**
* 跳轉到指定地址
* @param addr: 地址值
*/
Router.prototype.go = function(addr) {
var self = this;
window.location.hash = '#' + self.key + addr;
};
/**
* 重載頁面
*/
Router.prototype.reload = function() {
var self = this;
var hash = window.location.hash.replace('#' + self.key, '');
var addr = hash.split('/')[0];
var cb = getCb(addr, self.hashList);
if(cb != false) {
var arr = hash.split('/');
arr.shift();
cb.apply(self, arr);
} else {
self.index && self.go(self.index);
}
};
/**
* 開始路由,實際上只是為了當直接訪問路由路由地址的時候能夠及時調用回調
*/
Router.prototype.start = function() {
var self = this;
self.reload();
}
/**
* 獲取callback
* @return false or callback
*/
function getCb(addr, hashList) {
for(var key in hashList) {
if(key == addr) {
return hashList[key]
}
}
return false;
}
})();
每次hash的變化我們還可以通過onhashchange事件【核心+精髓】來監聽,然后做出相應的處理。。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<script type="text/javascript" src="./router.js"></script>
<style type="text/css">
</style>
</head>
<body>
<a href="#!index">go to index</a><br />
<a href="#!search/SB/shi/ni">go to search</a>
</body>
<script type="text/javascript">
window.onload = function() {
var router = new Router();
router.add('index', function() {
alert('current page: index');
});
router.add('search', function(wd, sortType, sortBy) {
alert('current page: search' + '\nwd: ' + wd + '\nsortType: ' + sortType + '\nsortBy: ' + sortBy);
});
router.setIndex('index');
router.start();
};
</script>
</html>
