路由是根據不一樣的 url 地址展現不一樣的內容或頁面javascript
早期的路由都是後端直接根據 url 來 reload 頁面實現的,即後端控制路由。css
後來頁面愈來愈複雜,服務器壓力愈來愈大,隨着 ajax(異步刷新技術) 的出現,頁面實現非 reload 就能刷新數據,讓前端也能夠控制 url 自行管理,前端路由由此而生。html
單頁面應用的實現,就是由於有了前端路由這個概念。前端
咱們常常在 url 中看到 #,這個 # 有兩種狀況,一個是咱們所謂的錨點,好比典型的回到頂部按鈕原理、Github 上各個標題之間的跳轉等,路由裏的 # 不叫錨點,咱們稱之爲 hash,大型框架的路由系統大多都是哈希實現的。vue
咱們須要一個根據監聽哈希變化觸發的事件 —— hashchange 事件java
window對象提供了onhashchange事件來監聽hash值的改變,一旦url中的hash值發生改變,便會觸發該事件。react
咱們用 window.location 處理哈希的改變時不會從新渲染頁面,而是看成新頁面加到歷史記錄中,這樣咱們跳轉頁面就能夠在 hashchange 事件中註冊 ajax 從而改變頁面內容。webpack
window.addEventListener('hashchange', function () {
<!--這裏你能夠寫你須要的代碼-->
});
複製代碼
HTML5的History API 爲瀏覽器的全局history對象增長的擴展方法。nginx
重點說其中的兩個新增的API history.pushState 和 history.replaceStateweb
這兩個 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;
}
複製代碼
接下來會一步一步來說解怎麼樣寫一個前端路由。
也就是把咱們的知識轉爲技能的過程。
上面咱們也看到了路由是根據不一樣的 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。
看好了demo,是否是火燒眉毛想實現一個路由了,那就讓咱們一塊兒來一步一步實現它吧。這裏給他取個名-煉獄,主要是方便下文的指代。
這裏我是仿造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文件)
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) {
}
}
複製代碼
上面已經列出來煉獄的總體代碼框架,下面咱們就來對每個函數進行編寫。
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('請傳入函數類型的參數');
}
},
複製代碼
好了,上面寫好了煉獄的主要代碼,下面咱們就能夠看到對應的效果了。