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的變化
注意:這裡的 url 不支援跨域,比如你在不是百度域名下輸入上面的程式碼。
不過這種模式之前在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。
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('請傳入函式型別的引數');
}
},
複製程式碼
好了,上面寫好了煉獄的主要程式碼,下面我們就可以看到對應的效果了。