摘要:实现原理现在前端的路由实现一般有两种,一种是路由,另外一种是路由。现在的前端主流框架的路由实现方式都会采用路由,本项目采用的也是。当值发生改变的时候,我们可以通过事件监听到,从而在回调函数里面触发某些方法。
效果图:
项目地址:https://github.com/biaochenxuying/route
效果体验地址:
1. 滑动效果: https://biaochenxuying.github.io/route/index.html
2. 淡入淡出效果: https://biaochenxuying.github.io/route/index2.html
1. 需求因为我司的 H 5 的项目是用原生 js 写的,要用到路由,但是现在好用的路由都是和某些框架绑定在一起的,比如 vue-router ,framework7 的路由;但是又没必要为了一个路由功能而加入一套框架,现在自己写一个轻量级的路由。
2. 实现原理现在前端的路由实现一般有两种,一种是 Hash 路由,另外一种是 History 路由。
2.1 History 路由History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。
属性History.length 是一个只读属性,返回当前 session 中的 history 个数,包含当前页面在内。举个例子,对于新开一个 tab 加载的页面当前属性返回值 1 。
History.state 返回一个表示历史堆栈顶部的状态的值。这是一种可以不必等待 popstate 事件而查看状态而的方式。
方法History.back()
前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于 history.go(-1).
Note: 当浏览器会话历史记录处于第一页时调用此方法没有效果,而且也不会报错。
History.forward()
在浏览器历史记录里前往下一页,用户可点击浏览器左上角的前进按钮模拟此方法. 等价于 history.go(1).
Note: 当浏览器历史栈处于最顶端时( 当前页面处于最后一页时 )调用此方法没有效果也不报错。
History.go(n)
通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面。比如:参数为 -1的时候为上一页,参数为 1 的时候为下一页. 当整数参数超出界限时 ( 译者注:原文为 When integerDelta is out of bounds ),例如: 如果当前页为第一页,前面已经没有页面了,我传参的值为 -1,那么这个方法没有任何效果也不会报错。调用没有参数的 go() 方法或者不是整数的参数时也没有效果。( 这点与支持字符串作为 url 参数的 IE 有点不同)。
history.pushState() 和 history.replaceState()
这两个 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
2.2 Hash 路由我们经常在 url 中看到 #,这个 # 有两种情况,一个是我们所谓的锚点,比如典型的回到顶部按钮原理、Github 上各个标题之间的跳转等,但是路由里的 # 不叫锚点,我们称之为 hash。
现在的前端主流框架的路由实现方式都会采用 Hash 路由,本项目采用的也是。
当 hash 值发生改变的时候,我们可以通过 hashchange 事件监听到,从而在回调函数里面触发某些方法。
3. 代码实现 3.1 简单版 - 单页面路由先看个简单版的 原生 js 模拟 Vue 路由切换。
原理监听 hashchange ,hash 改变的时候,根据当前的 hash 匹配相应的 html 内容,然后用 innerHTML 把 html 内容放进 router-view 里面。
这个代码是网上的:
3.2 复杂版 - 内联页面版,带缓存功能原生模拟 Vue 路由切换
首先前端用 js 实现路由的缓存功能是很难的,但像 vue-router 那种还好,因为有 vue 框架和虚拟 dom 的技术,可以保存当前页面的数据。
要做缓存功能,首先要知道浏览器的 前进、刷新、回退 这三个操作。
但是浏览器中主要有这几个限制:
没有提供监听前进后退的事件
不允许开发者读取浏览记录
用户可以手动输入地址,或使用浏览器提供的前进后退来改变 url
所以要自定义路由,解决方案是自己维护一份路由历史的记录,存在一个数组里面,从而区分 前进、刷新、回退。
url 存在于浏览记录中即为后退,后退时,把当前路由后面的浏览记录删除。
url 不存在于浏览记录中即为前进,前进时,往数组里面 push 当前的路由。
url 在浏览记录的末端即为刷新,刷新时,不对路由数组做任何操作。
另外,应用的路由路径中可能允许相同的路由出现多次(例如 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/biaochenxu...
callback 是切换页面后,执行的回调
参考项目:https://github.com/kliuj/spa-...
5. 最后项目地址:https://github.com/biaochenxuying/route
博客常更地址1 :https://github.com/biaochenxuying/blog
博客常更地址2 :http://biaochenxuying.cn/main.html
足足一个多月没有更新文章了,因为项目太紧,加班加班啊,趁着在家有空,赶紧写下这篇干货,免得忘记了,希望对大家有所帮助。
如果您觉得这篇文章不错或者对你有所帮助,请点个赞,谢谢。
对 全栈修炼 有兴趣的朋友可以扫下方二维码关注我的公众号
我会不定期更新有价值的内容,长期运营。
关注公众号并回复 福利 可领取免费学习资料,福利详情请猛戳: Python、Java、Linux、Go、node、vue、react、javaScript
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/108955.html
摘要:什么是前后端同构明确三个概念后端渲染指传统的或的渲染机制前端渲染指使用来渲染页面大部分内容,代表是现在流行的单页面应用同构渲染指前后端共用,首次渲染时使用来直出。 什么是前后端同构 明确三个概念:「后端渲染」指传统的 ASP、Java 或 PHP 的渲染机制;「前端渲染」指使用 JS 来渲染页面大部分内容,代表是现在流行的 SPA 单页面应用;「同构渲染」指前后端共用 JS,首次渲染时...
摘要:如何实现前端路由要实现前端路由,需要解决两个核心如何改变却不引起页面刷新如何检测变化了下面分别使用和两种实现方式回答上面的两个核心问题。 原文链接:github.com/whinc/blog/… 在单页应用如此流行的今天,曾经令人惊叹的前端路由已经成为各大框架的基础标配,每个框架都提供了强大的路由功能,导致路由实现变的复杂。想要搞懂路由内部实现还是有些困难的,但是如果只想了解路由实现基本...
摘要:使用值来作路由。原生应用本身就是多页的场景,页面间状态的隔离比共享更重要一些。使用开发的是原生应用,页面栈的管理使用的也是原生的特性,没有但是有模块可以实现页面的前进和后退等操作。 系列文章的目录在 ? 这里 (由于 我比较懒 最近一段时间在忙其他事,系列文章拖了好久终于又更新了。。。) 什么是 vue-router ? vue-router 官方文档 vue-router 是针对 V...
摘要:这里借鉴了一下的处理方式,我们把单独模块的包装成一个函数,提供一个全局的回调方法,加载完成时候再调用回调函数。 前端路由实现之 #hash 先上github项目地址: spa-routers运行效果图showImg(https://segmentfault.com/img/bVFi7l?w=581&h=312); 背景介绍 用了许多前端框架来做spa应用,比如说backbone,ang...
阅读 2231·2021-09-23 11:52
阅读 1898·2021-09-02 15:41
阅读 3018·2019-08-30 10:47
阅读 1982·2019-08-29 17:14
阅读 2332·2019-08-29 16:16
阅读 3191·2019-08-28 18:29
阅读 3418·2019-08-26 13:30
阅读 2609·2019-08-26 10:49