效果图:
项目地址:https://github.com/biaochenxuying/route
效果体验地址:
1. 滑动效果: https://biaochenxuying.github.io/route/index.html
2. 淡入淡出效果: https://biaochenxuying.github.io/route/index2.html
因为我司的 H 5 的项目是用原生 js 写的,要用到路由,但是现在好用的路由都是和某些框架绑定在一起的,比如 vue-router ,framework7 的路由;但是又没必要为了一个路由功能而加入一套框架,现在自己写一个轻量级的路由。
现在前端的路由实现一般有两种,一种是 Hash 路由,另外一种是 History 路由。
History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。
前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于 history.go(-1).
Note: 当浏览器会话历史记录处于第一页时调用此方法没有效果,而且也不会报错。
在浏览器历史记录里前往下一页,用户可点击浏览器左上角的前进按钮模拟此方法. 等价于 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 路由切换。
这个代码是网上的:
首先前端用 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 标签
列表1
//2. 标签加 js 调用方法
首页
// 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();
完整代码:https://github.com/biaochenxuying/route/blob/master/js/route.js
内容占位
内容占位
参考项目:https://github.com/kliuj/spa-routers
项目地址:https://github.com/biaochenxuying/route
博客常更地址1 :https://github.com/biaochenxuying/blog
博客常更地址2 :http://biaochenxuying.cn/main.html
足足一个多月没有更新文章了,因为项目太紧,加班加班啊,趁着在家有空,赶紧写下这篇干货,免得忘记了,希望对大家有所帮助。
如果您觉得这篇文章不错或者对你有所帮助,请点个赞,谢谢。
微信公众号: BiaoChenXuYing
分享 前端、后端开发等相关的技术文章,热点资源,随想随感,全栈程序员的成长之路。