效果圖:javascript
項目地址:github.com/biaochenxuy…css
效果體驗地址:html
1. 滑動效果: https://biaochenxuying.github.io/route/index.html前端
2. 淡入淡出效果: https://biaochenxuying.github.io/route/index2.htmlvue
由於我司的 H 5 的項目是用原生 js 寫的,要用到路由,可是如今好用的路由都是和某些框架綁定在一塊兒的,好比 vue-router ,framework7 的路由;可是又不必爲了一個路由功能而加入一套框架,如今本身寫一個輕量級的路由。java
如今前端的路由實現通常有兩種,一種是 Hash 路由,另一種是 History 路由。git
History 接口容許操做瀏覽器的曾經在標籤頁或者框架裏訪問的會話歷史記錄。程序員
前往上一頁, 用戶可點擊瀏覽器左上角的返回按鈕模擬此方法. 等價於 history.go(-1).github
Note: 當瀏覽器會話歷史記錄處於第一頁時調用此方法沒有效果,並且也不會報錯。vue-router
在瀏覽器歷史記錄裏前往下一頁,用戶可點擊瀏覽器左上角的前進按鈕模擬此方法. 等價於 history.go(1).
Note: 當瀏覽器歷史棧處於最頂端時( 當前頁面處於最後一頁時 )調用此方法沒有效果也不報錯。
經過當前頁面的相對位置從瀏覽器歷史記錄( 會話記錄 )加載頁面。好比:參數爲 -1的時候爲上一頁,參數爲 1 的時候爲下一頁. 當整數參數超出界限時 ( 譯者注:原文爲 When integerDelta is out of bounds ),例如: 若是當前頁爲第一頁,前面已經沒有頁面了,我傳參的值爲 -1,那麼這個方法沒有任何效果也不會報錯。調用沒有參數的 go() 方法或者不是整數的參數時也沒有效果。( 這點與支持字符串做爲 url 參數的 IE 有點不一樣)。
這兩個 API 都接收三個參數,分別是
a. 狀態對象(state object) — 一個JavaScript對象,與用 pushState() 方法建立的新歷史記錄條目關聯。不管什麼時候用戶導航到新建立的狀態,popstate 事件都會被觸發,而且事件對象的state 屬性都包含歷史記錄條目的狀態對象的拷貝。
b. 標題(title) — FireFox 瀏覽器目前會忽略該參數,雖然之後可能會用上。考慮到將來可能會對該方法進行修改,傳一個空字符串會比較安全。或者,你也能夠傳入一個簡短的標題,標明將要進入的狀態。
c. 地址(URL) — 新的歷史記錄條目的地址。瀏覽器不會在調用 pushState() 方法後加載該地址,但以後,可能會試圖加載,例如用戶重啓瀏覽器。新的 URL 不必定是絕對路徑;若是是相對路徑,它將以當前 URL 爲基準;傳入的 URL 與當前 URL 應該是同源的,不然,pushState() 會拋出異常。該參數是可選的;不指定的話則爲文檔當前 URL。
相同之處: 是兩個 API 都會操做瀏覽器的歷史記錄,而不會引發頁面的刷新。
不一樣之處在於: pushState 會增長一條新的歷史記錄,而 replaceState 則會替換當前的歷史記錄。
例子:
原本的路由
http://biaochenxuying.cn/
複製代碼
執行:
window.history.pushState(null, null, "http://biaochenxuying.cn/home");
複製代碼
路由變成了:
http://biaochenxuying.cn/home
複製代碼
詳情介紹請看:MDN
咱們常常在 url 中看到 #,這個 # 有兩種狀況,一個是咱們所謂的錨點,好比典型的回到頂部按鈕原理、Github 上各個標題之間的跳轉等,可是路由裏的 # 不叫錨點,咱們稱之爲 hash。
如今的前端主流框架的路由實現方式都會採用 Hash 路由,本項目採用的也是。
當 hash 值發生改變的時候,咱們能夠經過 hashchange 事件監聽到,從而在回調函數裏面觸發某些方法。
先看個簡單版的 原生 js 模擬 Vue 路由切換。
這個代碼是網上的:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="author" content="">
<title>原生模擬 Vue 路由切換</title>
<style type="text/css">
.router_box,
#router-view {
max-width: 1000px;
margin: 50px auto;
padding: 0 20px;
}
.router_box>a {
padding: 0 10px;
color: #42b983;
}
</style>
</head>
<body>
<div class="router_box">
<a href="/home" class="router">主頁</a>
<a href="/news" class="router">新聞</a>
<a href="/team" class="router">團隊</a>
<a href="/about" class="router">關於</a>
</div>
<div id="router-view"></div>
<script type="text/javascript">
function Vue(parameters) {
let vue = {};
vue.routes = parameters.routes || [];
vue.init = function() {
document.querySelectorAll(".router").forEach((item, index) => {
item.addEventListener("click", function(e) {
let event = e || window.event;
event.preventDefault();
window.location.hash = this.getAttribute("href");
}, false);
});
window.addEventListener("hashchange", () => {
vue.routerChange();
});
vue.routerChange();
};
vue.routerChange = () => {
let nowHash = window.location.hash;
let index = vue.routes.findIndex((item, index) => {
return nowHash == ('#' + item.path);
});
if (index >= 0) {
document.querySelector("#router-view").innerHTML = vue.routes[index].component;
} else {
let defaultIndex = vue.routes.findIndex((item, index) => {
return item.path == '*';
});
if (defaultIndex >= 0) {
window.location.hash = vue.routes[defaultIndex].redirect;
}
}
};
vue.init();
}
new Vue({
routes: [{
path: '/home',
component: "<h1>主頁</h1><a href='https://github.com/biaochenxuying'>https://github.com/biaochenxuying</a>"
}, {
path: '/news',
component: "<h1>新聞</h1><a href='http://biaochenxuying.cn/main.html'>http://biaochenxuying.cn/main.html</a>"
}, {
path: '/team',
component: '<h1>團隊</h1><h4>全棧修煉</h4>'
}, {
path: '/about',
component: '<h1>關於</h1><h4>關注公衆號:BiaoChenXuYing</h4><p>分享 WEB 全棧開發等相關的技術文章,熱點資源,全棧程序員的成長之路。</p>'
}, {
path: '*',
redirect: '/home'
}]
});
</script>
</body>
</html>
複製代碼
首先前端用 js 實現路由的緩存功能是很難的,但像 vue-router 那種還好,由於有 vue 框架和虛擬 dom 的技術,能夠保存當前頁面的數據。
要作緩存功能,首先要知道瀏覽器的 前進、刷新、回退 這三個操做。
可是瀏覽器中主要有這幾個限制:
因此要自定義路由,解決方案是本身維護一份路由歷史的記錄,存在一個數組裏面,從而區分 前進、刷新、回退。
另外,應用的路由路徑中可能容許相同的路由出現屢次(例如 A -> B -> A),因此給每一個路由添加一個 key 值來區分相同路由的不一樣實例。
這個瀏覽記錄須要存儲在 sessionStorage 中,這樣用戶刷新後瀏覽記錄也能夠恢復。
像 vue-router 那樣,提供了一個 router-link 組件來導航,而我這個框架也提供了一個 linkTo 的方法。
// 生成不一樣的 key
function genKey() {
var t = 'xxxxxxxx'
return t.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0
var v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
// 初始化跳轉方法
window.linkTo = function(path) {
if (path.indexOf("?") !== -1) {
window.location.hash = path + '&key=' + genKey()
} else {
window.location.hash = path + '?key=' + genKey()
}
}
複製代碼
用法:
//1. 直接用 a 標籤
<a href='#/list' >列表1</a>
//2. 標籤加 js 調用方法
<div onclick='linkTo(\"#/home\")'>首頁</div>
// 3. js 調用觸發
linkTo("#/list")
複製代碼
定義好要用到的變量
function Router() {
this.routes = {}; //保存註冊的全部路由
this.beforeFun = null; //切換前
this.afterFun = null; // 切換後
this.routerViewId = "#routerView"; // 路由掛載點
this.redirectRoute = null; // 路由重定向的 hash
this.stackPages = true; // 多級頁面緩存
this.routerMap = []; // 路由遍歷
this.historyFlag = '' // 路由狀態,前進,回退,刷新
this.history = []; // 路由歷史
this.animationName = "slide" // 頁面切換時的動畫
}
複製代碼
包括:初始化、註冊路由、歷史記錄、切換頁面、切換頁面的動畫、切換以前的鉤子、切換以後的鉤子、滾動位置的處理,緩存。
Router.prototype = {
init: function(config) {
var self = this;
this.routerMap = config ? config.routes : this.routerMap
this.routerViewId = config ? config.routerViewId : this.routerViewId
this.stackPages = config ? config.stackPages : this.stackPages
var name = document.querySelector('#routerView').getAttribute('data-animationName')
if (name) {
this.animationName = name
}
this.animationName = config ? config.animationName : this.animationName
if (!this.routerMap.length) {
var selector = this.routerViewId + " .page"
var pages = document.querySelectorAll(selector)
for (var i = 0; i < pages.length; i++) {
var page = pages[i];
var hash = page.getAttribute('data-hash')
var name = hash.substr(1)
var item = {
path: hash,
name: name,
callback: util.closure(name)
}
this.routerMap.push(item)
}
}
this.map()
// 初始化跳轉方法
window.linkTo = function(path) {
console.log('path :', path)
if (path.indexOf("?") !== -1) {
window.location.hash = path + '&key=' + util.genKey()
} else {
window.location.hash = path + '?key=' + util.genKey()
}
}
//頁面首次加載 匹配路由
window.addEventListener('load', function(event) {
// console.log('load', event);
self.historyChange(event)
}, false)
//路由切換
window.addEventListener('hashchange', function(event) {
// console.log('hashchange', event);
self.historyChange(event)
}, false)
},
// 路由歷史紀錄變化
historyChange: function(event) {
var currentHash = util.getParamsUrl();
var nameStr = "router-" + (this.routerViewId) + "-history"
this.history = window.sessionStorage[nameStr] ? JSON.parse(window.sessionStorage[nameStr]) : []
var back = false,
refresh = false,
forward = false,
index = 0,
len = this.history.length;
for (var i = 0; i < len; i++) {
var h = this.history[i];
if (h.hash === currentHash.path && h.key === currentHash.query.key) {
index = i
if (i === len - 1) {
refresh = true
} else {
back = true
}
break;
} else {
forward = true
}
}
if (back) {
this.historyFlag = 'back'
this.history.length = index + 1
} else if (refresh) {
this.historyFlag = 'refresh'
} else {
this.historyFlag = 'forward'
var item = {
key: currentHash.query.key,
hash: currentHash.path,
query: currentHash.query
}
this.history.push(item)
}
console.log('historyFlag :', this.historyFlag)
// console.log('history :', this.history)
if (!this.stackPages) {
this.historyFlag = 'forward'
}
window.sessionStorage[nameStr] = JSON.stringify(this.history)
this.urlChange()
},
// 切換頁面
changeView: function(currentHash) {
var pages = document.getElementsByClassName('page')
var previousPage = document.getElementsByClassName('current')[0]
var currentPage = null
var currHash = null
for (var i = 0; i < pages.length; i++) {
var page = pages[i];
var hash = page.getAttribute('data-hash')
page.setAttribute('class', "page")
if (hash === currentHash.path) {
currHash = hash
currentPage = page
}
}
var enterName = 'enter-' + this.animationName
var leaveName = 'leave-' + this.animationName
if (this.historyFlag === 'back') {
util.addClass(currentPage, 'current')
if (previousPage) {
util.addClass(previousPage, leaveName)
}
setTimeout(function() {
if (previousPage) {
util.removeClass(previousPage, leaveName)
}
}, 250);
} else if (this.historyFlag === 'forward' || this.historyFlag === 'refresh') {
if (previousPage) {
util.addClass(previousPage, "current")
}
util.addClass(currentPage, enterName)
setTimeout(function() {
if (previousPage) {
util.removeClass(previousPage, "current")
}
util.removeClass(currentPage, enterName)
util.addClass(currentPage, 'current')
}, 350);
// 前進和刷新都執行回調 與 初始滾動位置爲 0
currentPage.scrollTop = 0
this.routes[currHash].callback ? this.routes[currHash].callback(currentHash) : null
}
this.afterFun ? this.afterFun(currentHash) : null
},
//路由處理
urlChange: function() {
var currentHash = util.getParamsUrl();
if (this.routes[currentHash.path]) {
var self = this;
if (this.beforeFun) {
this.beforeFun({
to: {
path: currentHash.path,
query: currentHash.query
},
next: function() {
self.changeView(currentHash)
}
})
} else {
this.changeView(currentHash)
}
} else {
//不存在的地址,重定向到默認頁面
location.hash = this.redirectRoute
}
},
//路由註冊
map: function() {
for (var i = 0; i < this.routerMap.length; i++) {
var route = this.routerMap[i]
if (route.name === "redirect") {
this.redirectRoute = route.path
} else {
this.redirectRoute = this.routerMap[0].path
}
var newPath = route.path
var path = newPath.replace(/\s*/g, ""); //過濾空格
this.routes[path] = {
callback: route.callback, //回調
}
}
},
//切換以前的鉤子
beforeEach: function(callback) {
if (Object.prototype.toString.call(callback) === '[object Function]') {
this.beforeFun = callback;
} else {
console.trace('路由切換前鉤子函數不正確')
}
},
//切換成功以後的鉤子
afterEach: function(callback) {
if (Object.prototype.toString.call(callback) === '[object Function]') {
this.afterFun = callback;
} else {
console.trace('路由切換後回調函數不正確')
}
}
}
複製代碼
window.Router = Router;
window.router = new Router();
複製代碼
<script type="text/javascript">
var config = {
routerViewId: 'routerView', // 路由切換的掛載點 id
stackPages: true, // 多級頁面緩存
animationName: "slide", // 切換頁面時的動畫
routes: [{
path: "/home",
name: "home",
callback: function(route) {
console.log('home:', route)
var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>首頁</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/list\")'>列表</a></div><div class='height'>內容佔位</div>"
document.querySelector("#home").innerHTML = str
}
}, {
path: "/list",
name: "list",
callback: function(route) {
console.log('list:', route)
var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>列表</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail\")'>詳情</a></div>"
document.querySelector("#list").innerHTML = str
}
}, {
path: "/detail",
name: "detail",
callback: function(route) {
console.log('detail:', route)
var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>詳情</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail2\")'>詳情 2</a></div><div class='height'>內容佔位</div>"
document.querySelector("#detail").innerHTML = str
}
}, {
path: "/detail2",
name: "detail2",
callback: function(route) {
console.log('detail2:', route)
var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>詳情 2</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/home\")'>首頁</a></div>"
document.querySelector("#detail2").innerHTML = str
}
}]
}
//初始化路由
router.init(config)
router.beforeEach(function(transition) {
console.log('切換之 前 dosomething', transition)
setTimeout(function() {
//模擬切換以前延遲,好比說作個異步登陸信息驗證
transition.next()
}, 100)
})
router.afterEach(function(transition) {
console.log("切換之 後 dosomething", transition)
})
</script>
複製代碼
<div id="routerView" data-animationName="slide">
<div class="page" data-hash="/home">
<div class="page-content">
<div id="home"></div>
<script type="text/javascript">
window.home = function(route) {
console.log('home:', route)
// var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>首頁</h2> <input type='text'> <div><a href='#/list' >列表1</div></div><div class='height'>內容佔位</div>"
var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>首頁</h2> <input type='text'> <div><div href='javascript:void(0);' onclick='linkTo(\"#/list\")'>列表</div></div><div class='height'>內容佔位</div>"
document.querySelector("#home").innerHTML = str
}
</script>
</div>
</div>
<div class="page" data-hash="/list">
<div class="page-content">
<div id="list"></div>
<div style="height: 700px;border: solid 1px red;background-color: #eee;margin-top: 20px;">內容佔位</div>
<script type="text/javascript">
window.list = function(route) {
console.log('list:', route)
var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>列表</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail\")'>詳情</a></div>"
document.querySelector("#list").innerHTML = str
}
</script>
</div>
</div>
<div class="page" data-hash="/detail">
<div class="page-content">
<div id="detail"></div>
<script type="text/javascript">
window.detail = function(route) {
console.log('detail:', route)
var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>詳情</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail2\")'>詳情 2</a></div><div class='height'>內容佔位</div>"
document.querySelector("#detail").innerHTML = str
}
</script>
</div>
</div>
<div class="page" data-hash="/detail2">
<div class="page-content">
<div id="detail2"></div>
<div style="height: 700px;border: solid 1px red;background-color: pink;margin-top: 20px;">內容佔位</div>
<script type="text/javascript">
window.detail2 = function(route) {
console.log('detail2:', route)
var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>詳情 2</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/home\")'>首頁</a></div>"
document.querySelector("#detail2").innerHTML = str
}
</script>
</div>
</div>
</div>
<script type="text/javascript" src="./js/route.js"></script>
<script type="text/javascript">
router.init()
router.beforeEach(function(transition) {
console.log('切換之 前 dosomething', transition)
setTimeout(function() {
//模擬切換以前延遲,好比說作個異步登陸信息驗證
transition.next()
}, 100)
})
router.afterEach(function(transition) {
console.log("切換之 後 dosomething", transition)
})
</script>
複製代碼
足足一個多月沒有更新文章了,由於項目太緊,加班加班啊,趁着在家有空,趕忙寫下這篇乾貨,省得忘記了,但願對你們有所幫助。
若是您以爲這篇文章不錯或者對你有所幫助,請點個贊,謝謝。