【译】使用 HTML5 History API 调节浏览器地址栏 URubiconL

by admin on 2019年12月16日

History API 与浏览器历史堆栈管理

2016/07/25 · HTML5 ·
History,
HTML5,
浏览器

本文作者: 伯乐在线 –
欲休
。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

移动端开发在某些场景中有着特殊需求,如为了提高用户体验和加快响应速度,常常在部分工程采用SPA架构。传统的单页应用基于url的hash值进行路由,这种实现不存在兼容性问题,但是缺点也有–针对不支持onhashchange属性的IE6-7需要设置定时器不断检查hash值改变,性能上并不是很友好。

而如今,在移动端开发中HTML5规范给我们提供了一个History接口,使用该接口可以自由操纵历史记录。本文并不详细介绍History接口,而是探究History接口如何影响浏览器历史堆栈,并且利用这个规律应用到具体的实际业务中,提出两种历史记录保存策略,使路由逻辑更清晰,让SPA更容易。

原文:抱歉找不到了

  HTML5 History
API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL。这个功能很有用,例如通过一段JavaScript代码局部加载页面的内容,你希望通过改变当前页面的URL来反应出页面内容的变化,这时该功能可以派上用场。

历史

History API回顾

HTML5 History
API包括2个方法:history.pushState()和history.replaceState(),和1个事件:window.onpopstate。

HTML5 的 History API 可以让开发者不刷新整个页面就可以修改网站的
URL。这在使用 JavaScript 加载一个页面的一部分的时候尤其有用,用
JavaScript 新加载的这一部分内容是明显不同的, 因而有必要使用一个新的
URL(原文: This is particularly useful for loading portions of a page
with JavaScript, such that the content is significantly different and
warrants a new URL.)。

  举个例子,当用户从首页进入帮助页面时,我们通过Ajax来加载帮助页面的内容。然后这个用户又转到产品页面,我们需要再一次通过Ajax请求来替换页面的内容。当用户想分享页面的URL时,通过History
API,我们可以改变页面的URL来反应内容的修改,这样不管是用户分享还是保存的URL都能和页面的内容对应起来。

经节:你们这追求公义、寻求耶和华的,当听我言!你们要追想被凿而出的盘石,被挖而出的岩穴。

pushState

history.pushState(stateObject, title, url),包括三个参数。

第一个参数用于存储该url对应的状态对象,该对象可在onpopstate事件中获取,也可在history对象中获取。

第二个参数是标题,目前浏览器并未实现。

第三个参数则是设定的url。一般设置为相对路径,如果设置为绝对路径时需要保证同源。

pushState函数向浏览器的历史堆栈压入一个url为设定值的记录,并改变历史堆栈的当前指针至栈顶。

>
在这里笔者使用历史堆栈和当前指针,用以说明浏览器对历史记录的管理策略。文档中并没有使用这样的词汇,笔者为了更形象的介绍接口对浏览器历史记录的影响,使用这样的描述,如有不当之处请及时指出(不过目前以这套模型为基础的逻辑实现中并未出现悖论)。

这里有一个例子,我们假设一个人从一个网站的 Homepage 前往访问 Help
页面。我们使用 Ajax 加载 Help 页面的内容。那个用户随后又访问 Product
页面,而我们还是用 Ajax 加载并替换内容。然后,用户想分享这个页面(即
Product 页面)的 URL。通过 History API,我们本可以(we could have
been)随着用户的访问而正确地修改页面的
URL,所以他们看见(或分享或保存)的 URL 是相关的并且正确的。

基本知识

  要查看这个API提供了哪些功能非常简单,打开浏览器的Developer
Tools工具面板,然后在console中输入history。如果你的浏览器支持History
API,你将会看到这个对象下面附带了很多方法。

图片 1

  注意其中的pushStatereplaceState这两个方法。我们可以在console中进行一些简单的测试,来看看当我们使用这两个方法时URL会发生什么变化。稍后我们将分析这两个方法中的所有参数,现在我们只需要关注最后一个参数:

history.replaceState(null, null, 'hello');

  上面代码中的replaceState方法改变了当前页面的URL,在后面添加了一个’/hello’。不过并没有发出任何request请求,当前窗口仍然停留在之前的页面。不过这里有个问题,当你点击浏览器的后退按钮时,页面并不会回退到我们通过replaceState方法修改之前的那个URL,而是直接回退到了上一个页面(即我们进入到这个页面之前的那个页面)。这是因为replaceState方法不会修改浏览器的history,它只是简单地替换了地址栏中的URL。

  要解决这个问题我们需要使用pushState方法:

history.pushState(null, null, 'hello');

  现在再点击浏览器的后退按钮,你会发现它和你预想的效果一样。因为pushState方法将我们传给它的URL添加到浏览器的history中,从而改变了浏览器的history。假如我们将另外一个完整的站点URL传递给它会发生什么情况呢?例如我们在baidu.com的首页进行测试,然后在console中输入下面的内容。

history.pushState(null, null, 'https://twitter.com/hello');

  浏览器会报错。因为传递给pushState方法的URl必须和当前页面的URL属于同一个源(即不能跨域),否则会有很大的安全漏洞,开发人员可能会借用该功能来欺骗用户,让他们觉得自己是在访问一个完全不同的站点,而事实并非如此。

  来看看传递给pushState方法的所有参数:

history.pushState([data], [title], [url]);
  1. 第一个参数用来传递我们需要的数据,当页面的状态发生变化时我们可以接收到该数据。如用户点击浏览器的后退和向前按钮。需要注意的是在Firefox中只允许传递最多640K的数据。
  2. 第二个参数title是一个字符串,不过截止到目前,几乎所有的浏览器都忽略该参数。
  3. 最后一个参数是我们想要替换的URL。

身为基督徒,我们永远不要忽视自己所承继的产业。了解自己所承继的产业,帮助我们了解自己的身分与神的带领。

replaceState

该接口与pushState参数相同,含义也相同。唯一的区别在于replaceState是替换浏览器历史堆栈的当前历史记录为设定的url。需要注意的是,replaceState不会改动浏览器历史堆栈的当前指针。

基础原理

要查看这个 API 的特征,只需要打开 the Developer
Tools(即浏览器的开发者工具),在控制台输入(type
into)history。如果你选择的浏览器支持这个
API,那么我们将会看到许多隶属于这个对象的方法(a host of methods
attached to this object):

History {length: 2, state: null, scrollRestoration: "auto"}
    length:2
    scrollRestoration:"auto"
    state:null
    __proto__:History
        back:back()
        constructor:History()
        forward:forward()
        go:go()
        length:(...)
        get length:()
        pushState:pushState()
        replaceState:replaceState()
        scrollRestoration:(...)
        get scrollRestoration:()
        set scrollRestoration:()
        state:(...)
        get state:()
        Symbol(Symbol.toStringTag):"History"
        __proto__:Object

我们只对说明中的pushStatereplaceState感兴趣。回到控制台,我们可以试验一下这些方法并看看当我们使用这些方法的时候
URL
发生什么变化。我们稍后将覆盖其他参数,但现在我们所要使用的是最后的这个参数:

history.replaceState(null, null, 'hello');

尽管没有请求资源,窗口也保持着同一个页面,但上面的replaceState方法用/hello作为地址栏中
URL 的结尾(原文:The replaceState method above switches out the URL in
the address bar with ‘/hello’ despite no assets being requested and the
window remaining on the same page.
)。这里还有一个问题。当点击浏览器上的返回按钮的时候,我们会发现我们并没有返回到这篇文章的
URL
,而是回到我们之前所停留的页面。这是因为replaceState没有处理浏览器的历史,它只是简单地替换地址栏中当前的
URL。

要解决(fix)这个问题,我们需要使用pushState方法来代替:

history.pushState(null, null, 'hello');

现在,如果我们点击浏览器的返回按钮,我们会发现它会像我们所希望的一样运行,因为pushState已经修改了我们的历史(记录)以包含我们刚刚所传递进去的
URL。这很有趣,但如果我们做一些不那么直接的尝试并且假装当前的 URL
一直都不是“css-tricks.com”而是完完全全是另一个网站,之后会发生什么呢?

history.pushState(null, null, 'https://twitter.com/hello');

这会抛出一个异常,因为 URL 必须与当前的 URL 同源(has to be of the
same origin as the current
one),否则,我们可能冒着重大的安全漏洞风险(原文:we might risk major
security flaws
)并且是开发者能够欺骗人们让他们完全相信自己是在一个不同的网站(give
developers the ability to fool people into believing they were on a
different website altogether)。

回头看看被传入到这个函数中的其他参数,我们可以总结如下:

history.pushState([data], [title], [url]);
  1. 如果网页的状态改变了,例如:每当有人按下浏览器中的返回或前进按钮,我们将需要的数据就是第一个参数。需要注意的是:在
    Firefox 中,这个数据被限制为 640k 个字符。
  2. title是第二个参数,他可以是一个字符串,但截至本文写作时间(2015年3月9日),所有浏览器都简单地忽略它。
  3. 最后的这个参数就是我们希望出现在地址栏的 URL。

简单回顾一下

  这些History
API最主要的功能就是不重新加载页面。以往我们只能通过改变window.location的值来修改当前页面的URL,不过这会导致整个页面被重新加载。如果你修改的只是URL中的hash,则不会导致页面被刷新。 

  使用旧的hashbang方法可以改变页面的URL而不刷新页面。著名的Twitter就是使用的该方法,不过也广受诟病,毕竟hash在location中并不被作为一个真正的资源来对待。

  作为History
API的早期支持者,Twitter后来抛弃了传统的hashbang方法。在2012年,Twitter的团队介绍了他们的新方法,并列出了其中的一些问题同时还详细地介绍了各浏览器应该如何实现该规范。

以色列人是拥有丰盛产业的民族。这个国家始于亚伯拉罕与撒拉的信心,他们的后世跟随着以撒、雅各和约瑟这些信心领袖的脚踪。神丰富地祝福祂的百姓,使他们繁荣。神爱他们,领他们出埃及,进入祂所赏赐的富饶之地。神在历史上行了一些最威严可畏的奇迹,来创建自己的国度。神不断提供坚强的领袖给以色列民族,如:摩西、约书亚、基甸、底波拉、撒母耳、大卫和所罗门。祂差遣大能先知给以色列人,如:以利亚、以赛亚和耶利米。不幸地,在以赛亚时代,以色列百姓已经忘记自己所承继的产业。他们像属灵的乞丐,不像庞大产业的继承者,也不像皇家祭司的成员。

onpopstate

该事件是window的属性。该事件会在调用浏览器的前进、后退以及执行history.forward、history.back、和history.go触发,因为这些操作有一个共性,即修改了历史堆栈的当前指针。在不改变document的前提下,一旦当前指针改变则会触发onpopstate事件。

简史(A Quick History)

这些 history API 最值得注意的是,它们不会重新加载页面。在过去,改变 URL
的唯一方法是window.locaton,但它总是会重新加载页面。
除非,如果你所改变的只是hash(就像
点击<a href="#target">link</a>不会重新加载页面 一样)(原文:Except,
if all you changed was the hash)。

这与旧的 hashbang 方法相通,hashbang 方法可以改变 URL
而不刷新整个页面。总所周知,Twitter
曾经这样子做并因此而在很大程度上收到了批评(hash
不是一个真的资源地址)。

Twitter 放弃了使用这个方法,并且成为 history API 最早的支持者之一。

图片 2

Paste_Image.png

一个使用pushState和Ajax的例子

  在该示例中,我们希望用户通过我们的网站找到电影捉鬼敢死队(一部美国电影)中的演员。当用户选择一个图片时,我们需要在下方显示该演员对应的文字描述,同时给该图片一个被选中的效果。当点击后退按钮时,页面应该切换到上一个被选中的图片状态,同时图片下方的文字也要一并切换。当点击前进按钮时也一样。

  这里有一个效果图:

图片 3

  这个示例的HTML代码非常简单:div.gallery中包含了所有的链接,每个链接里有一个图片。接下来我们放置了一个空的div.content,用来存放当演员图片被点击时显示在下放的文字。

<div class="gallery">
  <a href="https://cdn.css-tricks.com/peter.html">
    <img src="bill.png" alt="Peter" class="peter" data-name="peter">
  </a>
  <a href="https://cdn.css-tricks.com/ray.html">
    <img src="ray.png" alt="Ray" class="ray" data-name="ray">
  </a>
  <a href="https://cdn.css-tricks.com/egon.html">
    <img src="egon.png" alt="Egon" class="egon" data-name="egon">
  </a>
  <a href="https://cdn.css-tricks.com/winston.html">
    <img src="winston.png" alt="Winston" class="winston" data-name="winston">
  </a>
</div>

<p class="selected">Ghostbusters</p>
<p class="highlight"></p>

<div class="content"></div>

  如果没有JavaScript该页面仍然可以正常工作,点击图片可以跳转到对应的页面,然后点击后退按钮也可以回到之前的页面。这是为了考虑页面的可访问行和优雅降级。

  接下来我们要添加JavaScript代码了。我们通过event
propagation
给div.gallery元素中的每一个link添加一个事件处理程序,像这样:

var container = document.querySelector('.gallery');

container.addEventListener('click', function(e) {
  if (e.target != e.currentTarget) {
    e.preventDefault();
    // e.target is the image inside the link we just clicked.
  }
  e.stopPropagation();
}, false);

  在if语句中,我们获取到被选中图片的data-name属性的值,然后将’.html’添加到后面拼成一个要访问的页面地址,并将其作为第三个参数传递给pushState方法(不过在真实的例子中我们可能会在Ajax请求成功之后才会去修改URL)。

var data = e.target.getAttribute('data-name'),
url = data + ".html";
history.pushState(null, null, url);

// 此处更改当前的classes样式
// 然后使用data变量的值更新
// 并通过Ajax请求.content元素的内容
// 最后再更新当前文档的title

(当然,此处我们也可以直接使用link的href属性的值)

  我将真实代码中的内容都替换成注释了,这样我们可以只关注pushState方法的使用。

  现在我们点击图片,URL和Ajax请求的内容会被自动更新,但是当我们点击后退按钮时并不会回退到之前选中的演员图片。这里我们还需要在用户点击后退和前进按钮时使用另外一个Ajax请求来更新内容,并再一次使用pushState方法来更新页面的URL。

  我们使用pushState方法中的第一个参数(其中的state)来保存状态信息:

history.pushState(data, null, url);

  上面代码中的data参数在popstate事件触发时可以被获取到。当浏览器的后退和前进按钮被点击时会触发popstate事件。

window.addEventListener('popstate', function(e) {
  // e.state表示上一个被点击的图片的data-attribute
});

  我们可以通过该参数传递一些我们需要的信息,例如在该示例中我们将之前选中的捉鬼敢死队的演员作为参数传递给requestContent方法,在该方法中,我们使用jQuery的load方法进行一次Ajax请求。

function requestContent(file) {
  $('.content').load(file + ' .content');
}

window.addEventListener('popstate', function(e) {
  var character = e.state;

  if (character == null) {
    removeCurrentClass();
    textWrapper.innerHTML = " ";
    content.innerHTML = " ";
    document.title = defaultTitle;
  } else {
      updateText(character);
      requestContent(character + ".html");
      addCurrentClass(character);
      document.title = "Ghostbuster | " + character;
  }
});

  如果用户点击了演员Ray的图片,event
listener会被触发,然后在pushState事件中保存图片的data属性的值。当用户点击另外一个图片,并点击了浏览器的后退按钮,此时popstate事件会被触发,从而重新加载ray.html页面。

  这意味着什么呢?当我们点击一个演员的图片然后将被更改的URL分享出去,用户访问这个URL时对应的HTML文件会被自动加载进来。这会带来一些更好的用户体验,并保证了URL和页面内容的一致性从而减少了因此而带给用户的一些困惑。

  上面的示例只是简单地通过jQuery来动态加载内容,我们当然也可以在pushState方法中传递一些更加复杂的对象。不过这个例子已经能足够说明问题并帮助我们开始学习如何使用History
API的功能。我们先要学会走,然后才能跑。

你的属灵产业,远比以赛亚时代以色列百姓所承继的更富裕。你的属灵前辈,包括耶稣的肉身母亲马利亚、施洗约翰、十二使徒、使徒保罗,以及许许多多历世历代的圣徒。更重要的是,你有创始成终的耶稣作你的榜样。你也许拥有可追溯数代的家族信心史。

History API与业务实践

最常见的单页应用场景:列表页、商品详情页以及其内部的其他链接入口如图片页、评论页及其推荐其他商品详情页。以上提到的已经涉及到了4个单独业务逻辑页面(推荐的商品可复用商品详情页逻辑),分别是:列表、详情、图片详情和评论。将这4个页面合并到一个页面中,这就是最简单的SPA。为了用户的良好体验,必须设计合理的交互逻辑,最直观的就是浏览器(或手机app、微信公众号)的后退前进必须合乎业务逻辑特点。因此,这就涉及到了History
API的使用,也牵扯到浏览器的历史记录管理。

图片 4

上图为具体的逻辑示意图。在列表页,点击其中一个商品,这里是商品1,进入详情页。详情页包括了该商品的轮播图、商品的图片详情入口、评论入口和推荐的其他商品入口。接下来进行如下操作:进入图片详情页,后退至详情页再进入评论页;后退至商品1详情页再由推荐商品入口进入商品9详情页,同样在商品9详情页进入图片详情页和评论页,再后退至商品9详情页;由推荐商品入口进入商品34详情页,再进行类似操作。最后保证在商品34图片详情页或评论页可以顺利后退至最初的商品列表页。

>
上文中加粗的“后退”,意味着使用浏览器后退按钮,或者使用手机自带的返回,再或者使用页面上提供的后退按钮。

这样一个很细小的需求,但是一旦真正放手去做却不是那么容易。仅仅根据History
API的2个函数和1个事件去盲目的尝试实现,这属于盲人摸象,鲁棒性不高。不清楚浏览器的历史记录管理策略,不了解当前页面的历史记录数量,此种情况若要实现上述场景就有些麻烦。所以在具体动手写业务代码之前,需要搞懂History的pushState和replaceState具体如何影响历史记录栈。

下一步

  如果我们想大范围地使用这种技术,我们应该考虑使用一些专有的工具,例如pjax。 它是一个jQuery的插件,使用它可以大大提高我们同时使用Ajax和pushState方法进行开发的速度,不过它只支持那些使用History
API接口的现代浏览器。

  History
JS
可以兼容旧浏览器,对于不支持History
API接口的浏览器,它依然使用旧的URL hash的方式来实现同样的功能。

你是否看到神整个救赎计划?神的计划包括你,诚如这个计划包括了历世历代的基督徒。神要你参与祂这份已在历世历代展开的救赎大工,拯救失丧的灵魂。你今日的顺服会成为后世信心的遗产,让后世可跟随你的榜样。

探究浏览器历史记录策略与History API的关系

由于浏览器并未针对每个页面的历史记录提供具体访问的接口,因此所有的测试都是黑盒。但是在移动端的中,大都是webkit内核,其webcore的具体实现也都相近,因此该节得出的结论完全可以在移动端使用。

尽管无法访问当前页的历史记录栈,但是浏览器却提供了history.length属性,它标明了当前历史记录栈的个数。该值会帮助我们更好地分析History
API对历史记录栈的影响。

图片 5

上图为测试实例。其中白色箭头意味着点击该链接并执行pushState操作(即操作1),黑色箭头则执行浏览器后退,红色的圆点为历史记录栈中的当前指针,而每个项则为历史记录栈,历史记录的个数则为其子项的数量。

初始在第一个搜索列表页,执行操作1后历史堆栈数量增加,当前指针上移一位至26788.html;
同理在执行3次操作1,历史堆栈递增3个,当前指针仍在栈顶,即78099.html;
此后进行浏览器后退,历史堆栈数量不变,当前指针下移一位至8819.html;
在此处再执行操作1,栈顶元素改变,当前指针移至栈顶,历史堆栈数量不变;
继续执行操作1,栈顶元素改变,指针移至栈顶,历史堆栈数量加一;
执行浏览器后退,栈顶元素不变,指针下移一位至8128.html,历史堆栈数量不变;
执行浏览器后退,栈顶元素不变,指针下移一位至8819.html,历史堆栈数量不变;
执行浏览器后退,栈顶元素不变,指针下移一位至8128.html,历史堆栈数量不变;
执行浏览器后退,栈顶元素不变,指针下移一位至26788.html,历史堆栈数量不变;
执行操作1,栈顶元素变为9721.html,指针上移至栈顶,历史堆栈数量变为3;
执行操作1,栈顶元素变为8387.html,指针上移至栈顶,历史堆栈数量变为4;
执行浏览器后退,栈顶元素不变,指针下移一位至9721.html,历史堆栈数量不变;
执行浏览器后退,栈顶元素不变,指针下移一位至26788.html,历史堆栈数量不变;
执行浏览器后退,栈顶元素不变,指针下移一位至search.html,历史堆栈数量不变;
执行操作1,栈顶元素变为xxx.html,指针上移至栈顶,历史堆栈数量变为2; …

至此,实验结束。虽然这里仅仅列出了这一个测试用例,但是其实笔者做了更多更复杂的测试,并且平台涉及了pc和移动端的浏览器、微信和原生webview,结果都一样。这一系列测试说明了很多问题,总结之一句话则是:

浏览器针对每个页面维护一个History栈。执行pushState函数可压入设定的url至栈顶,同时修改当前指针;
当执行back操作时,history栈大小并不会改变(history.length不变),仅仅移动当前指针的位置;
若当前指针在history栈的中间位置(非栈顶),此时执行pushState会改变history栈的大小。
总结pushState的规律,可发现当前指针在history栈顶部时执行pushState,会增加history栈大小;若current指针不在栈顶则会在当前指针所在位置添加项。执行back操作并不修改history栈大小,因此可以通过back和forward在当前大小的history栈中自由移动。

掌握这个规律,就知道如何维护历史记录,就知道在什么状态下需要pushState。回到最初的需求,产品经理规定从商品34的评论页,按后退按钮可以到达最初的列表页,但是他并没有详细规定如何后退。在这里就会有2中实现方式:

  • 每一次后退,会回到上次的访问地方。如,在商品34的评论页,会后退至商品34的详情页,再后退则会回到商品9的详情页,直至回到列表页。
  • 总共维护三层历史记录,第一层(栈底)为列表页,第二层为详情页,第三层(栈顶)为评论页或图片详情页。在该种实现下,由商品34的评论页第一次后退至商品34的详情页,第二次后退至列表页。

针对第一种,其实实现最为简单,因为这完全是由浏览器默认控制历史记录堆栈,而我们只需在合适的时机调用pushState将url插入到堆栈,然后在onpopstate处理函数中监听对应的时间即可:

window.addEventListener(‘popstate’, function (e) {
console.log(‘popstate’) // 后退(前进)至商品详情页,异步加载数据并渲染
if(e.state && e.state.indexOf(‘/shop/sku/’) !== -1){
ajaxDetail(e.state,true); }else //
后退(前进)至评论页,异步加载数据渲染 if(e.state &&
e.state.indexOf(‘/shop/comment/commentList.html’) !== -1){
ajaxComment(e.state,true); }else //
后退(前进)至图片详情页,异步加载数据渲染 if(e.state &&
e.state.indexOf(‘/shop/item/pictext/’) !== -1){ ajaxPic(e.state,true);
}else // 后退(前进)至列表页,隐藏浮层 if(e.state &&
e.state.indexOf(‘/search/’) !== -1){ // 隐藏spa的浮层
$(‘.spa-container’).css(‘zIndex’,’-1′); } });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
window.addEventListener(‘popstate’, function (e) {
    
    console.log(‘popstate’)
    // 后退(前进)至商品详情页,异步加载数据并渲染
    if(e.state && e.state.indexOf(‘/shop/sku/’) !== -1){
      ajaxDetail(e.state,true);  
    }else
    // 后退(前进)至评论页,异步加载数据渲染
    if(e.state && e.state.indexOf(‘/shop/comment/commentList.html’) !== -1){
      ajaxComment(e.state,true);
    }else
    // 后退(前进)至图片详情页,异步加载数据渲染
    if(e.state && e.state.indexOf(‘/shop/item/pictext/’) !== -1){
      ajaxPic(e.state,true);
    }else
    // 后退(前进)至列表页,隐藏浮层
    if(e.state && e.state.indexOf(‘/search/’) !== -1){
      // 隐藏spa的浮层
      $(‘.spa-container’).css(‘zIndex’,’-1′);
    }
    
  });

针对第二种实现,则是本文的重点。毕竟,由浏览器默认维护的历史堆栈在某些业务场景中并不匹配,因此需要开发者自己维护一个历史记录栈。在本次实现中,由于总共涉及4张页面的显示,因此我们设定了3层历史堆栈,这很好理解。

为了构建这样的历史记录栈,在主页面(即列表页)中需要额外添加两条历史记录。这是由于默认打开列表页时,当前页面的url已加入历史记录栈中,

function push(state){ history.pushState(state, null, location.pathname +
location.search); } // ‘abc’用于标示初始列表页
history.replaceState(‘abc’,null,location.pathname + location.search) //
压入两条历史记录 push(); push();

1
2
3
4
5
6
7
8
9
function push(state){
    history.pushState(state, null, location.pathname + location.search);
  }
  // ‘abc’用于标示初始列表页
  history.replaceState(‘abc’,null,location.pathname + location.search)
  
  // 压入两条历史记录
  push();
  push();

这样,打开列表页后就会创建3个历史记录,并且这3个历史记录的url都为列表页的url,这与后面的操作并无影响。

在列表页中打开详情页,需要做额外的处理。由于按照我们设计的历史记录栈,第二层应该为详情页,而此时在初始化后,历史记录栈的当前指针已指向栈顶元素,因此需要将当前指针下移一位。这里就需要history.back来完成。

$(‘.item-list’).on(‘click’,’a’,handler); // 异步加载详情数据 var handler
= function(e,isScrollXClick){ var a = this;
ajaxDetail($(a).attr(‘href’),isScrollXClick); return false; }; var
isScrollXClick; /** * @params: url 请求路径 isScrollXClick:
是否点击推荐商品 * */ var ajaxDetail = function(url,isScrollXClick){
$.ajax({ url: ‘/api’ + url, success: function(data){ … …
if(!isScrollXClick){ console.log(‘I am back!’) // 在代码中进行back or
forward并不会立即出发popstate事件,以v8引擎为例,在执行back之后 //
的大概18us之后会触发事件,而此时如果立即通过replaceState修改url则会造成失败,修改的是
// history stack栈顶的url. // 这里通过异步执行replaceState兼容
history.back(); } // 异步触发 setTimeout(function(){
history.replaceState(url, null, url); }) //
针对推荐栏的商品,循环绑定事件,此处用事件代理优化
$(‘#J_PDSlider’).on(‘click’,’a’,function(e){ isScrollXClick = 1;
handler.call(this,e,isScrollXClick); return false; }); }, error:
function(xhr, type){ alert(‘Ajax error!’) } }) };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
$(‘.item-list’).on(‘click’,’a’,handler);
 
// 异步加载详情数据
var handler = function(e,isScrollXClick){
    var a = this;
    ajaxDetail($(a).attr(‘href’),isScrollXClick);
    return false;
};
 
var isScrollXClick;
  /**
   * @params: url 请求路径 isScrollXClick: 是否点击推荐商品
   *
   */
  var ajaxDetail = function(url,isScrollXClick){
 
     $.ajax({
      url: ‘/api’ + url,
      success: function(data){
        …
        …
        if(!isScrollXClick){
          console.log(‘I am back!’)
 
          // 在代码中进行back or forward并不会立即出发popstate事件,以v8引擎为例,在执行back之后
          // 的大概18us之后会触发事件,而此时如果立即通过replaceState修改url则会造成失败,修改的是
          // history stack栈顶的url.
          
          // 这里通过异步执行replaceState兼容
          history.back();      
          
        }
          
        // 异步触发
        setTimeout(function(){
          history.replaceState(url, null, url);
        })
 
        // 针对推荐栏的商品,循环绑定事件,此处用事件代理优化
        $(‘#J_PDSlider’).on(‘click’,’a’,function(e){
          isScrollXClick = 1;
          handler.call(this,e,isScrollXClick);
          return false;
        });
      },
      error: function(xhr, type){
        alert(‘Ajax error!’)
      }
     })
  };

在此处实现,通过isScrollXClick变量判断是否点击的是推荐商品,如果不是则需要执行back操作,下移指针。此时指针是指在第二层,但是浏览器和第二层历史记录的url仍为初始化设定的url,因此需要修改,在这里异步修改当前url。

之所以异步执行replaceState,是由于webkit触发popState事件决定的。在代码中执行history.back
或者history.forward,并不会立即返回,也不会立即触发popState事件。由于没有阅读webkit的源码,因此无从推测执行back或者forward后具体需要额外做什么操作,它们之间有着10us级别的间隔,因此此处必须使用setTimeout实现异步改变url。

在具体开发过程中,这个问题困扰着笔者好几天,终于在一次调试过程中发现浏览器url的变动,才联想到可能是由事件触发的时间差导致。

对于图片详情和评论的逻辑处理,则和上文类似,无需多言。

最后一次后退需要回到列表页,而在初始化阶段我们给列表页设置了state为“abc”,特殊的标示该路由,因此在popState事件处理中,我们就可以根据该项回到初始页:

window.addEventListener(‘popstate’, function (e) { if(e.state &&
e.state.indexOf(‘/shop/sku/’) !== -1){ ajaxDetail(e.state,true); }else
if(e.state && e.state.indexOf(‘abc’) !== -1){ // 隐藏spa的浮层
$(‘.spa-container’).css(‘zIndex’,’-1′); push(); push(); } });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window.addEventListener(‘popstate’, function (e) {
 
    if(e.state && e.state.indexOf(‘/shop/sku/’) !== -1){
      ajaxDetail(e.state,true);  
    }else if(e.state && e.state.indexOf(‘abc’) !== -1){
      // 隐藏spa的浮层
      $(‘.spa-container’).css(‘zIndex’,’-1′);
      
      
      push();
      push();
    }
    
    
  });

如果回到初始页,隐藏浮层,同时在执行2次push操作。根据上节发现的规律,在初始页执行2次push操作,会在当前指针位置重新添加2个历史记录,当前指针指向栈顶元素,历史记录栈的数量不变,仍为3。这样就完成了简单的由开发者自定义维护历史堆栈的spa系统。

有关URLs

  这里我特别引用了Kyle Neath有关URLs的说明:

URLs是一个通用的概念,它可以工作在Firefox, Chrome, Safari, Internet
Explorer, curl, wget,甚至在你的iPhone,
Android以及便签纸上。它是web中的一个通用的语法。不要认为这是理所当然的。任何一个稍微懂点技术的用户都可以浏览你的应用的90%以上的部分而不用去刻意记住那些URL的结构。要实现这样的效果,你需要考虑URLs的实用性。

  这意味着不论你想要进行什么样的hacks或性能优化,作为web开发人员,你应该注重URL。而随着HTML5
History API的帮助,我们可以轻松地解决诸如上述示例中的一些问题。

History

回顾

之所以会写这篇文章完全是出于偶然,由于实际项目的各种需求我们不应该仅仅将眼光停留在使用API的层面上。另外,在开发过程中遇到难以解决的问题,需要提出各种合理的设想并用详实的实验证明,在得到相对应的结论后需要利用该结论去例证其他场景,这样才能确保解决方案的可靠性。目前网络上或者书籍中并未提供任何手动维护历史记录堆栈的方法,也未明确指出History
API与浏览器历史记录之间如何影响,因此本文对于旨在利用History
API实现spa的开发者而言还是有些指导意义的。

打赏支持我写出更多好文章,谢谢!

打赏作者

常见问题

  • 将Ajax请求的地址嵌入到a标记的href属性中通常是个不错的主意。
  • 确保在JavaScript的click事件处理程序中return
    true,这样当有人使用中键点击或命令点击时不会导致程序被意外覆盖。

Listen to Me, you who follow after righteousness, You whoseek the LORD:
Look to the rock from which you were hewn, And to the hole ofthe pit
from which you were dug.

打赏支持我写出更多好文章,谢谢!

图片 6

1 赞 7 收藏
评论

补充

  • Mozilla有关操作浏览器history的文档
  • Ajax示例集锦Dive into
    HTML5
  • Twitter有关pushState的实现

As Christians, we ought never tooverlook our heritage. An awareness of
our Christian heritage helps us tounderstand our identity, and it gives
us a sense of where God is leading us.The Israelites had a rich
heritage. Their nation began as a result of Abrahamand Sarah’s
faithfulness. The generations that followed included Isaac, Jacob,and
Joseph as their faithful leaders. God richly blessed His people and
madethem prosper. God continued to show favor on the Israelites by
leading them outof Egypt into a prosperous land of their own. God
established His nationthrough some of the most awesome miracles in
history. God continued to providestrong leaders, such as Moses, Joshua,
Gideon, Deborah, Samuel, David, andSolomon. He sent mighty prophets such
as Elijah, Isaiah, and Jeremiah.Unfortunately, in Isaiah’s day, God’s
people had reached a point where they hadforgotten their heritage. They
lived as spiritual paupers rather than as heirsto a rich heritage and
members of a royal priesthood.

关于作者:欲休

图片 7

前端自由人
个人主页 ·
我的文章 ·
1 ·
 

图片 8

浏览器支持

Chrome Safari Firefox Opera IE Android iOS
31+ 7.1+ 34+ 11.50+ 10+ 4.3+ 7.1+

原文地址:

Your spiritual heritage is evenricher than that of Isaiah’s generation.
Your spiritual ancestors include Marythe mother of Jesus, John the
Baptist, the disciples, the apostle Paul, and ahost of saints down
through the ages. Even more important, you look to Jesus asthe author
and finisher of your faith . You may have a familyhistory of
faithfulness that goes back several generations.

Do you see the full picture of God’sredemptive work? God’s plan involves
you, just as it has included eachChristian throughout the centuries. God
wants you to participate in Hiscontinuing work to redeem a lost world.
Your obedience today will provide alegacy of faithfulness to the
generations that follow.

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图