整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

实现一个前端路由,如何实现浏览器的前进与后退?

实现一个前端路由,如何实现浏览器的前进与后退?

. 需求

如果要你实现一个前端路由,应该如何实现浏览器的前进与后退 ?

2. 问题

首先浏览器中主要有这几个限制,让前端不能随意的操作浏览器的浏览纪录:

?没有提供监听前进后退的事件。?不允许开发者读取浏览纪录,也就是 js 读取不了浏览纪录。?用户可以手动输入地址,或使用浏览器提供的前进后退来改变 url。

所以要实现一个自定义路由,解决方案是自己维护一份路由历史的记录,从而区分 前进、刷新、回退。

下面介绍具体的方法。

3. 方法

目前笔者知道的方法有两种,一种是 在数组后面进行增加与删除,另外一种是 利用栈的后进先出原理

我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。

3.1 在数组最后进行 增加与删除

通过监听路由的变化事件 hashchange,与路由的第一次加载事件 load ,判断如下情况:

?url 存在于浏览记录中即为后退,后退时,把当前路由后面的浏览记录删除。?url 不存在于浏览记录中即为前进,前进时,往数组里面 push 当前的路由。?url 在浏览记录的末端即为刷新,刷新时,不对路由数组做任何操作。

另外,应用的路由路径中可能允许相同的路由出现多次(例如 A -> B -> A),所以给每个路由添加一个 key 值来区分相同路由的不同实例。

注意:这个浏览记录需要存储在 sessionStorage 中,这样用户刷新后浏览记录也可以恢复。

笔者之前实现的 用原生 js 实现的轻量级路由 ,就是用这种方法实现的,具体代码如下:

// 路由构造函数	
function Router() {	
        this.routes={}; //保存注册的所有路由	
        this.routerViewId="#routerView"; // 路由挂载点 	
        this.stackPages=true; // 多级页面缓存	
        this.history=[]; // 路由历史	
}	
 
	
Router.prototype={	
        init: function(config) {	
            var self=this;	
            //页面首次加载 匹配路由	
            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-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)	
            }	
            // 如果不需要页面缓存功能,每次都是刷新操作	
            if (!this.stackPages) {	
                this.historyFlag='forward'	
            }	
            window.sessionStorage[nameStr]=JSON.stringify(this.history)	
        },	
    }


3.2 利用栈的 后进者先出,先进者后出 原理

在说第二个方法之前,先来弄明白栈的定义与后进者先出,先进者后出原理。

3.2.1 定义

栈的特点:后进者先出,先进者后出

举一个生活中的例子说明:就是一摞叠在一起的盘子。我们平时放盘子的时候,都是从下往上一个一个放;取的时候,我们也是从上往下一个一个地依次取,不能从中间任意抽出。

因为栈的后进者先出,先进者后出的特点,所以只能栈一端进行插入和删除操作。这也和第一个方法的原理有异曲同工之妙。


下面用 JavaScript 来实现一个顺序栈:


// 基于数组实现的顺序栈	
class ArrayStack {	
  constructor(n) {	
      this.items=[];  // 数组	
      this.count=0;   // 栈中元素个数	
      this.n=n;       // 栈的大小	
  }	
 
	
  // 入栈操作	
  push(item) {	
    // 数组空间不够了,直接返回 false,入栈失败。	
    if (this.count===this.n) return false;	
    // 将 item 放到下标为 count 的位置,并且 count 加一	
    this.items[this.count]=item;	
    ++this.count;	
    return true;	
  }	
 
	
  // 出栈操作	
  pop() {	
    // 栈为空,则直接返回 null	
    if (this.count==0) return null;	
    // 返回下标为 count-1 的数组元素,并且栈中元素个数 count 减一	
    let tmp=items[this.count-1];	
    --this.count;	
    return tmp;	
  }	
}


其实 JavaScript 中,数组是自动扩容的,并不需要指定数组的大小,也就是栈的大小 n 可以不指定的。

3.2.2 应用

栈的经典应用: 函数调用栈

操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。为了让你更好地理解,我们一块来看下这段代码的执行过程。



function add(x, y) {	
   let sum=0;	
   sum=x + y;	
   return sum;	
}	
 
	
function main() {	
   let a=1; 	
   let ret=0;	
   let res=0;	
   ret=add(3, 5);	
   res=a + ret;	
   console.log("res: ", res);	
   reuturn 0;	
}	
 
	

上面代码也很简单,就是执行 main 函数求和,main 函数里面又调用了 add 函数,先调用的先进入栈。

执行过程如下:

3.2.3 实现浏览器的前进、后退

第二个方法就是:用两个栈实现浏览器的前进、后退功能。

我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。

比如你顺序查看了 a,b,c 三个页面,我们就依次把 a,b,c 压入栈,这个时候,两个栈的数据如下:

当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是这个样子:

这个时候你又想看页面 b,于是你又点击前进按钮回到 b 页面,我们就把 b 再从栈 Y 中出栈,放入栈 X 中。此时两个栈的数据是这个样子:

这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据这个样子:

如果用代码来实现,会是怎样的呢 ?各位可以想一下。

其实就是在第一个方法的代码里面, 添加多一份路由历史纪录的数组即可,对这两份历史纪录的操作如上面示例图所示即可,也就是对数组的增加和删除操作而已, 这里就不展开了。

其中第二个方法与参考了 王争老师的 数据结构与算法之美。

接:https://blog.csdn.net/u011518749/article/details/102774408

作者:山月行

们知道,在使用浏览器的后退和前进按钮时,浏览器会使用缓存来优化您的页面加载过程。它显着改善了用户的浏览体验——尤其是那些网络或设备较慢的用户。作为 Web 开发人员,了解如何在所有浏览器中针对这种缓存优化页面至关重要,这样您的用户才能获得收益。

回退前进缓存是一种内存缓存,用于在用户导航离开时存储页面(包括 JavaScript 堆)的完整快照。 整个页面都在内存中,如果用户决定返回,浏览器可以快速轻松地恢复它。比如当点击返回时,在那一刻,该缓存可以对上一个页面的加载速度产生很大的影响:

下面是有缓存和没缓存的加载回退页面的过程:

  • 无缓存:发起一个新请求以加载前一个页面,并且根据该页面针对重复访问的优化程度,浏览器可能必须重新下载、重新解析和重新执行部分(或全部)它刚刚下载的资源。
  • 有缓存:加载本质上是即时的,因为整个页面都可以从内存中恢复,根本不用去网络

回退前进缓存是如何工作的

回退前进缓存使用的“缓存”不同于 HTTP 缓存(它对于加快重复导航也很有用)。 回退前进缓存是在内存缓存中整个页面的快照(包括 JavaScript 堆),而 HTTP 缓存仅包含对先前发出的请求的响应。由于加载页面所需的所有请求都可以从 HTTP 缓存中完成是非常罕见的,因此使用回退前进缓存恢复的重复访问总是比优化得最好的回退前进缓存导航更快。

然而,在内存中创建页面快照涉及到如何最好地保存正在进行的代码方面的一些复杂性。例如,当页面在回退前进缓存中时,如何处理超时的 setTimeout() 调用?

答案是浏览器暂停运行任何挂起的计时器或未解决的Promise——就是说会挂起所有任务队列中的任务——并在当从回退前进缓存恢复页面时恢复处理任务。

在某些情况下,这是相当低的风险(例如,setTimeout或Promise),但在其他情况下,它可能会导致非常混乱或意外的行为。例如,如果浏览器暂停了作为 IndexedDB 事务的一部分所需的任务,它可能会影响同一源中的其他打开的选项卡(因为多个选项卡可以同时访问相同的 IndexedDB 数据库)。因此,浏览器通常不会尝试在 IndexedDB 事务的中间缓存页面或使用可能影响其他页面的 API。

可以观察回退前进缓存的API

虽然回退前进缓存是浏览器自动进行的优化,但对于开发人员来说,知道它何时发生仍然很重要,这样他们就可以优化他们的页面并相应地调整任何指标或性能测量。

用于观察回退前进缓存的主要事件是页面转换事件——pageshowpagehide——它们在回退前进缓存存在的时候就已经存在,并且在当今使用的几乎所有浏览器中都得到了支持。较新的页面生命周期事件(freeze和resume)也会在页面进出回退前进缓存时以及在某些其他情况下调度。

观察何时从回退前进缓存恢复页面:

pageshow 事件在页面初始加载时以及页面从缓存恢复时的 load 事件之后立即触发。 pageshow 事件有一个persisted属性,如果页面从缓存恢复,则该属性为 true(如果不是,则为 false)。 您可以使用持久化属性来区分常规页面加载和缓存恢复。 例如:

观察页面什么时候进入回退前进缓存

pagehide 是 pageshow 事件的对应事件。 pageshow 事件在页面正常加载或从缓存恢复时触发。 pagehide 事件在页面正常卸载或浏览器尝试将其放入缓存时触发。

pagehide 事件也有一个persisted属性,如果它是假的,那么你可以确信页面不会进入回退前进缓存。 但是,如果persisted 属性为真,则不能保证页面会被缓存。 这意味着浏览器打算缓存该页面,但可能存在无法缓存的因素。下面会解释几种原因,导致它可能仍然不能进入缓存并被丢弃。

为回退前进缓存优化你的页面

并非所有页面都存储在回退前进缓存中,即使页面确实存储在那里,它也不会无限期地留在那里。 开发人员必须了解是什么使页面符合(和不符合)使用缓存的条件,以最大限度地提高其缓存命中率。以下部分概述了使浏览器尽可能缓存您的页面的最佳实践。

不要使用 unload 事件

在所有浏览器中优化回退前进缓存的最重要方法是永远不要使用 unload 事件。

unload 事件对浏览器来说是有问题的在 unload 事件触发后页面将不会继续存在。这提出了一个挑战,因为其中许多页面也是在假设用户导航离开时会触发 unload 事件的假设下构建的,这不再是真的(并且很长一段时间都不是真的)。所以浏览器面临两难境地,他们必须在可以改善用户体验的东西之间做出选择——但也可能会冒着破坏页面的风险。

如果 Chrome 和 Firefox 添加了卸载侦听器,则选择使页面不符合回退前进缓存的条件,这样风险较小,但也会取消许多页面被缓存的资格。 Safari 将尝试使用 unload 事件侦听器缓存某些页面,但为了减少潜在的损坏,它不会在用户导航离开时运行 unload 事件,这使得该事件非常不可靠。

不要使用 unload 事件,而是使用 pagehide 事件。 pagehide 事件在当前触发 unload 事件的所有情况下触发,并且当页面放入回退前进缓存时也会触发。

事实上,Lighthouse v6.2.0 添加了一个 no-unload-listeners 审计,如果他们的页面上的任何 JavaScript(包括来自第三方库的 JavaScript)添加了 unload 事件监听器,它将警告开发人员。

正确的做法:

避免 window.opener 引用

在某些浏览器(包括基于 Chromium 的浏览器)中,如果页面是使用 window.open() 或(在 88 版之前基于 Chromium 的浏览器中)从带有 target=_blank 的链接打开的——没有指定 rel="noopener"——那么 打开页面将引用打开页面的窗口对象。

除了存在安全风险之外,具有非空 window.opener 引用的页面不能安全地放入缓存,因为这可能会破坏任何试图访问它的页面。

因此,最好尽可能避免使用 rel="noopener" 创建 window.opener 引用。 如果您的站点需要打开一个窗口并通过 window.postMessage() 或直接引用 window 对象来控制它,则打开的窗口和打开器都没有资格使用缓存。

始终在用户导航离开之前关闭打开的连接

如上所述,当页面被放入缓存时,所有计划的 JavaScript 任务都会暂停,然后在页面从缓存中取出时恢复。如果这些计划好的 JavaScript 任务仅访问 DOM API(或仅与当前页面隔离的其他 API),那么在页面对用户不可见时暂停这些任务不会导致任何问题。

但是,如果这些任务连接到的 API 也可以从同一来源的其他页面访问(例如:IndexedDB、Web Locks、WebSockets 等),这可能会出现问题,因为暂停这些任务可能会阻止其他选项卡中的代码运行.

因此,某些浏览器在以下情况下不会尝试将页面放入缓存

  • 具有打开 IndexedDB 连接的页面
  • 正在进行 fetch() 或 XMLHttpRequest 的页面
  • 具有打开的 WebSocket 或 WebRTC 连接的页面

如果您的页面使用这些 API 中的任何一个,最好在 pagehide 或 freeze 事件期间始终关闭连接并删除或断开观察者。这将允许浏览器安全地缓存页面,而不会影响其他打开的选项卡。然后,如果页面从缓存恢复,您可以重新打开或重新连接到这些 API(在 pageshow 或 resume 事件中)。

以下示例显示了如何在使用 IndexedDB 时通过关闭 pagehide 事件侦听器中的打开连接来确保您的页面符合缓存条件:

缓存恢复后更新陈旧或敏感数据

如果您的站点保留用户状态(尤其是任何敏感的用户信息),则需要在从回退前进缓存恢复页面后更新或清除该数据。例如,如果用户导航到结帐页面然后更新他们的购物车,如果从缓存恢复过时的页面,则返回导航可能会显示过时的信息。

另一个更关键的示例是,如果用户在公共计算机上退出站点,并且下一个用户单击后退按钮。 这可能会暴露用户在注销时认为已清除的私人数据。为避免此类情况,如果 event.persisted 为 true,则最好在 pageshow 事件之后始终更新页面。

以下代码检查 pageshow 事件中是否存在特定于站点的 cookie,如果未找到 cookie,则重新加载:

测试一下保证你的页面会被缓存

Chrome DevTools 可以帮助您测试您的页面,以确保它们针对回退前进缓存进行了优化,并确定可能阻止它们符合条件的任何问题。

要测试特定页面,请在 Chrome 中导航到该页面,然后在 DevTools 中转到 Application > Back-forward Cache。 接下来单击 Run Test 按钮,DevTools 将尝试导航并返回以确定页面是否可以从回退前进缓存恢复。

如果成功,则会显示从缓存恢复成功,失败也有提示,并告诉你原因。

成功

比如百度使用了unload导致缓存失败 :)

结语:

回退前进缓存对提高性能,及用户体验很有帮助。大家要写正确的代码来使自己的页面尽量被缓存,这样你们的网站才能大大获益。

近在工作中遇到一个bug,将word转换成html,转换成功之后在浏览器中打开其中图片不显示,使用img标签,src指定图片相对地址又是能显示的,排除图片问题。

网上能搜索到的demo

打开转码之后的html代码发现,生成的是vml图片标签,这个在IE9以后就不支持了,更别说现在的主流浏览器了。

生成的html中使用的是vml标签

将这个跟大佬分析分析,各种文档一查,咔咔咔大致分析出问题所在。原来jacob使用的是word本身自带的功能,相当于把word打开另存为html,于是手动将word转为html试了一下,果然效果与代码转换一致,这时候注意到word另存为时有一个web选项,里面有个使用vml渲染图片默认是选中的,去掉这个选项,再次生成,图片正常显示。



到这里基本已经确定了问题的解决思路,另存为时不勾选这个选项,那么问题来了,怎么利用jacob操作另存为时去掉这个选项呢,想去搜搜看jacob相关的文档,结果不知道是不是因为这个很老了,网上大多数都是demo,根本没有相关的文档可看,Github上也是只言片语,根本无从查起。

jacob github 地址:https://github.com/joval/jacob

微软官网文档

官方文档连接:https://docs.microsoft.com/zh-cn/office/vba/api/word.weboptions.relyonvml

微软官网查询相关文档,发现其实是可以关闭的,于是代码变成这样

关闭relyOnVml

再次运行程序,这次转出来的html就能在浏览器打开了。

总结,在这次解决问题的过程中,学会了往更深层次去想问题,找对方向,迎难而上。

记录一下这个问题解决的经验,也希望能帮到同样遇到这个问题的人。