history api
在本教程中,我们将增强我们的History Web API技能。 我们将在Web上构建一个UX模式,该模式将受到同样的喜爱和厌恶: 无限滚动 。
无限滚动是一种界面模式,可在我们到达给定网页的末尾时加载新内容。 如果进行周到的实施,无限滚动可以保留用户的参与度。 一些最佳示例出现在Facebook,Twitter和Pinterest等社交平台上。
但是,如果您对挑战的想法感到兴奋,请系紧安全带,做好准备,然后开始吧!
建立演示网站
我们的网站是静态博客。 您可以从纯HTML生成它,也可以利用静态网站生成器(例如Jekyll , Middleman或Hexo) 。 本教程的演示如下所示:
关于HTML结构,有几件事需要引起您的注意。
<!-- site header -->
<div class="content" id="main">
<article class="article" id="article-5" data-article-id="5">
<!-- content -->
</article>
<!-- site footer -->
- 从上面的代码片段中可以看到,文章应该包装在具有唯一IDHTML元素中。 您可以使用
div
或section
元素,而在命名该元素的id
没有任何限制。 - 同样,在商品本身上,您将需要添加一个
data-article-id
属性,其中包含该商品的相应id
号。
随意详细说明网站样式; 使其色彩更丰富,更具吸引力或添加更多内容。
加载JavaScript
首先,按以下顺序将以下JavaScript库加载到博客的每个页面。
- jquery.js :我们将用于选择元素,添加新内容,添加新类以及执行AJAX请求的库。
- history.js :一个polyfill ,可填充浏览器的本机历史API。
我们的自定义jQuery插件
除了这两个之外,我们还需要加载自己JavaScript文件,在这里我们可以编写脚本来执行无限滚动 。 我们将采用的方法是将JavaScript包装到jQuery插件中,而不是像之前的教程中那样直接编写它。
我们将从jQuery Plugin Boilerplate开始该插件。 这类似于HTML5 Boilerplate,它提供了用于构建jQuery插件的模板,模式和最佳实践的集合。
下载Boilerplate ,将其放置在网站上所有JavaScript文件所在的目录中(例如/assets/js/
),并将文件重命名为“ keepscrolling.jquery.js”(此名称由Finding Nemo的Dory和她著名的台词“ 继续游泳 ”)。
assets/js
├── keepscrolling.jquery.js
├── keepscrolling.jquery.js.map
└── src
└── keepscrolling.jquery.js
该插件将使我们能够通过Options或Settings引入灵活性。
观察jQuery插件结构
编写jQuery插件需要稍微不同的思维方式,因此在添加任何代码之前,我们将首先检查jQuery插件的结构。 如下所示,我将代码分为四个部分:
;( function( $, window, document, undefined ) {
"use strict";
// 1.
var pluginName = "keepScrolling",
defaults = {};
// 2.
function Plugin ( element, options ) {
this.element = element;
this.settings = $.extend( {}, defaults, options );
this._defaults = defaults;
this._name = pluginName;
this.init();
}
// 3.
$.extend( Plugin.prototype, {
init: function() {
console.log( "Plugin initialized" );
},
} );
// 4.
$.fn[ pluginName ] = function( options ) {
return this.each( function() {
if ( !$.data( this, "plugin_" + pluginName ) ) {
$.data( this, "plugin_" +
pluginName, new Plugin( this, options ) );
}
} );
};
} )( jQuery, window, document );
- 在代码的第一部分中,我们根据JavaScript的通用命名约定将插件名称
keepScrolling
指定为“ camel case”。 我们还有一个变量defaults
,它将包含插件的默认设置。 - 接下来,我们有插件的主要功能
Plugin()
。 可以将此功能与“构造函数”进行比较,在这种情况下,该“构造函数”将初始化插件并将默认设置与实例化插件时传递的所有设置合并。 - 第三部分是我们将组成自己的函数以提供无限滚动功能的地方。
- 最后,第四部分是将整个内容包装到jQuery插件中的部分。
设置好所有这些之后,我们现在就可以编写我们JavaScript了。 我们首先定义插件的默认选项。
选项
;( function( $, window, document, undefined ) {
"use strict";
var pluginName = "keepScrolling",
defaults = {
floor: null,
article: null,
data: {}
};
...
} )( jQuery, window, document );
如您在上面看到的,我们设置了三个选项:
-
floor
:一个ID选择器(例如#floor
或#footer
,我们将其视为网站或内容的结尾。 通常,它将是站点页脚。 -
article
:包装文章的类选择器。 -
data
:由于我们无权访问任何外部API(我们的网站是静态的),因此我们需要以JSON格式传递文章数据的集合,例如文章URL,ID和标题,作为此选项参数。
功能
这里我们有init()
。 在此功能中,我们将添加许多必须在站点初始化期间立即运行的功能。 例如,我们选择现场楼层。
$.extend( Plugin.prototype, {
// The `init()` function.
init: function() {
this.siteFloor = $( this.settings.floor ); // select the element set as the site floor.
},
} );
还有一些函数将在初始化之外运行。 我们添加这些函数来创建并在init
函数之后添加它们。
我们将编写的第一组函数是用于检索或返回“事物”的函数; 字符串,对象或数字中的任何内容均可在插件中的其他所有功能中重复使用。 这些内容包括:
在页面上获取所有文章:
/**
* Find and returns list of articles on the page.
* @return {jQuery Object} List of selected articles.
*/
getArticles: function() {
return $( this.element ).find( this.settings.article );
},
获取文章地址。 在WordPress中,这通常称为“ post slug”。
/**
* Returns the article Address.
* @param {Integer} i The article index.
* @return {String} The article address, e.g. `post-two.html`
*/
getArticleAddr: function( i ) {
var href = window.location.href;
var root = href.substr( 0, href.lastIndexOf( "/" ) );
return root + "/" + this.settings.data[ i ].address + ".html";
},
获取下一个要检索的文章ID和地址。
/**
* Return the "next" article.
* @return {Object} The `id` and `url` of the next article.
*/
getNextArticle: function() {
// Select the last article.
var $last = this.getArticles().last();
var articlePrevURL;
/**
* This is a simplified way to determine the content ID.
*
* Herein, we substract the last post ID by `1`.
* Ideally, we should be calling call an API endpoint, for example:
* https://www.techinasia.com/wp-json/techinasia/2.0/posts/329951/previous/
*/
var articleID = $last.data( "article-id" );
var articlePrevID = parseInt( articleID, 10 ) - 1; // Previous ID
// Loop into the Option `data`, and get the correspending Address.
for ( var i = this.settings.data.length - 1; i >= 0; i-- ) {
if ( this.settings.data[ i ].id === articlePrevID ) {
articlePrevURL = this.getArticleAddr( i ) ;
}
}
return {
id: articlePrevID,
url: articlePrevURL
};
},
以下是该插件的实用程序功能; 负责执行一项特定“事情”的功能。 这些包括:
告知元素是否正在进入视口的函数。 我们主要使用它来判断在视口中是否可见定义的站点“地板”。
/**
* Detect whether the target element is visible.
* https://stackoverflow.com/q/123999/
*
* @return {Boolean} `true` if the element in viewport, and `false` if not.
*/
isVisible: function() {
if ( target instanceof jQuery ) {
target = target[ 0 ];
}
var rect = target.getBoundingClientRect();
return rect.bottom > 0 &&
rect.right > 0 &&
rect.left < ( window.innerWidth || document.documentElement.clientWidth ) &&
rect.top < ( window.innerHeight || document.documentElement.clientHeight );
},
暂停执行功能的功能; 被称为反跳 。 如前所述,我们将处理频繁发生的用户滚动活动。 因此, scroll
事件内的功能将在用户滚动之后频繁运行,这将使站点上的滚动体验变慢或变慢。
上面的反跳功能将减少执行频率。 在用户停止滚动之前,它将在运行功能之前通过wait
参数wait
指定的时间。
/**
* Returns a function, that, as long as it continues to be invoked, will not b
* triggered.
* The function will be called after it stops being called for N milliseconds.
* If immediate is passed, trigger the function on the leading edge, instead of
* the trailing.
*
* @link https://davidwalsh.name/function-debounce
* @link http://underscorejs.org/docs/underscore.html#section-83
*
* @param {Function} func Function to debounce
* @param {Integer} wait The time in ms before the Function run
* @param {Boolean} immediate
* @return {Void}
*/
isDebounced: function( func, wait, immediate ) {
var timeout;
return function() {
var context = this,
args = arguments;
var later = function() {
timeout = null;
if ( !immediate ) {
func.apply( context, args );
}
};
var callNow = immediate && !timeout;
clearTimeout( timeout );
timeout = setTimeout( later, wait );
if ( callNow ) {
func.apply( context, args );
}
};
},
确定是进行还是中止操作的功能。
/**
* Whether to proceed ( or not to ) fetching a new article.
* @return {Boolean} [description]
*/
isProceed: function() {
if ( articleFetching // check if we are currently fetching a new content.
|| articleEnding // check if no more article to load.
|| !this.isVisible( this.siteFloor ) // check if the defined "floor" is visible.
) {
return;
}
if ( this.getNextArticle().id <= 0 ) {
articleEnding = true;
return;
}
return true;
},
我们将使用前面的实用程序函数isProceed()
来检查是否满足所有条件以继续提取新内容。 如果是这样,则随后的函数将运行,获取新内容并将其附加在上一篇文章之后。
/**
* Function to fetch and append a new article.
* @return {Void}
*/
fetch: function() {
// Shall proceed or not?
if ( !this.isProceed() ) {
return;
}
var main = this.element;
var $articleLast = this.getArticles().last();
$.ajax( {
url: this.getNextArticle().url,
type: "GET",
dataType: "html",
beforeSend: function() {
articleFetching = true;
}
} )
/**
* When the request is complete and it successly
* retrieves the content, we append the content.
*/
.done( function( res ) {
$articleLast
.after( function() {
if ( !res ) {
return;
}
return $( res ).find( "#" + main.id ).html();
} );
} )
/**
* When the function is complete, whether it `fail` or `done`,
* always set the `articleFetching` to false.
* It specifies that we are done fetching the new content.
*/
.always( function() {
articleFetching = false;
} );
},
在init
添加此功能。 因此,该函数将在插件初始化后立即运行,然后在满足条件时检索新内容。
init: function() {
this.siteFloor = $( this.settings.floor ); // select the element set as the site floor.
this.fetch();
},
接下来,我们将添加一个功能,以使用History Web API更改浏览器历史记录。 这个特定功能比我们之前的功能要复杂得多。 这里最棘手的部分是,我们究竟何时应该在用户滚动,文档标题以及URL期间更改历史记录。 以下是帮助简化该功能背后概念的说明:
从图中可以看到,我们有三行:“屋顶线”,“中线”和“地板线”,它们说明了文章在视口中的位置。 该图显示,第一篇文章的底部以及第二篇文章的顶部现在处于中间位置。 它并没有具体说明用户在看哪篇文章的意图。 是第一篇文章还是第二篇文章? 因此,当两篇文章都位于此位置时,我们将不会更改浏览器历史记录。
当文章顶部到达“屋顶线”时,我们将历史记录记录到后续帖子中,因为它占据了视口的大部分可见部分。
类似地,我们记录前一个帖子的底部到达“底线”时的历史,因为它现在占据了视口的大部分可见部分。
这些是您需要添加的“ while”代码:
init: function() {
this.roofLine = Math.ceil( window.innerHeight * 0.4 ); // set the roofLine;
this.siteFloor = $( this.settings.floor );
this.fetch();
},
/**
* Change the browser history.
* @return {Void}
*/
history: function() {
if ( !window.History.enabled ) {
return;
}
this.getArticles()
.each( function( index, article ) {
var scrollTop = $( window ).scrollTop();
var articleOffset = Math.floor( article.offsetTop - scrollTop );
if ( articleOffset > this.threshold ) {
return;
}
var articleFloor = ( article.clientHeight - ( this.threshold * 1.4 ) );
articleFloor = Math.floor( articleFloor * -1 );
if ( articleOffset < articleFloor ) {
return;
}
var articleID = $( article ).data( "article-id" );
articleID = parseInt( articleID, 10 );
var articleIndex;
for ( var i = this.settings.data.length - 1; i >= 0; i-- ) {
if ( this.settings.data[ i ].id === articleID ) {
articleIndex = i;
}
}
var articleURL = this.getArticleAddr( articleIndex );
if ( window.location.href !== articleURL ) {
var articleTitle = this.settings.data[ articleIndex ].title;
window.History.pushState( null, articleTitle, articleURL );
}
}.bind( this ) );
},
最后,我们创建一个函数,当用户滚动页面时将运行fetch()
和history()
。 为此,我们创建了一个名为scroller()
的新函数,并在插件初始化时运行它。
/**
* Functions to run during the scroll.
* @return {Void}
*/
scroller: function() {
window.addEventListener( "scroll", this.isDebounced( function() {
this.fetch();
this.history();
}, 300 ).bind( this ), false );
}
正如您在上面看到的那样,我们将这些动作进行反跳,因为执行AJAX和更改浏览器历史记录都是一项昂贵的操作。
添加内容占位符
这是可选的,但是为了尊重用户体验,建议您这样做。 占位符向用户提供反馈,表示正在发布新文章。
首先,我们创建占位符模板。 通常,这种模板放在站点页脚之后。
<script type="text/template" id="tmpl-placeholder">
<div class="placeholder placeholder--article" id="placeholder-article">
<div class="container">
<div class="placeholder__header animated">
<h1></h1>
</div>
<div>
<p class="placeholder__p-1 animated"></p>
<p class="placeholder__p-2 animated"></p>
</div>
</div>
</div>
</script>
请记住,占位符文章及其结构应类似于您博客的真实内容。 相应地调整HTML结构。
占位符样式更简单。 它包含所有布局的基本样式,如实际文章,模拟加载感的动画@keyframe
以及切换可见性的样式(占位符最初是隐藏的;仅当父元素具有fetching
时才显示类)。
.placeholder {
color: @gray-light;
padding-top: 60px;
padding-bottom: 60px;
border-top: 6px solid @gray-lighter;
display: none;
.fetching & {
display: block;
}
p {
display: block;
height: 20px;
background: @gray-light;
}
&__header {
animation-delay:.1s;
h1 {
height: 30px;
background-color: @gray-light;
}
}
&__p-1 {
animation-delay:.2s;
width: 80%;
}
&__p-2 {
animation-delay:.3s;
width: 70%;
}
}
然后,我们更新几行以在AJAX请求期间显示占位符,如下所示。
/**
* Initialize.
* @return {Void}
*/
init: function() {
this.roofLine = Math.ceil( window.innerHeight * 0.4 );
this.siteFloor = $( this.settings.floor );
this.addPlaceholder();
this.fetch();
this.scroller();
},
/**
* Append the addPlaceholder.
* Placeholder is used to indicate a new post is being loaded.
* @return {Void}
*/
addPlaceholder: function() {
var tmplPlaceholder = document.getElementById( "tmpl-placeholder" );
tmplPlaceholder = tmplPlaceholder.innerHTML;
$( this.element ).append( tmplPlaceholder );
},
/**
* Function to fetch and append a new article.
* @return {Void}
*/
fetch: function() {
...
// Select the element wrapping the article.
var main = this.element;
$.ajax( {
...
beforeSend: function() {
...
// Add the 'fetching' class.
$( main ).addClass( function() {
return "fetching";
} );
}
} )
...
.always( function() {
...
// Remove the 'fetching' class.
$( main ).removeClass( function() {
return "fetching";
} );
} );
这就是我们处理占位符的方式! 我们的插件已完成,是时候部署插件了。
部署方式
部署插件非常简单。 我们指定包装博客文章的元素,并使用选项集调用插件,如下所示。
$( document ).ready( function() {
$( "#main" ).keepScrolling({
floor: "#footer",
article: ".article",
data : [{
"id": 1,
"address": "post-one",
"title": "Post One"
}, {
"id": 2,
"address": "post-two",
"title": "Post Two"
}, {
"id": 3,
"address": "post-three",
"title": "Post Three"
}, {
"id": 4,
"address": "post-four",
"title": "Post Four"
}, {
"id": 5,
"address": "post-five",
"title": "Post Five"
}]
});
} );
无限滚动现在应该可以运行了。
警告:后退按钮
在本教程中,我们建立了无限的滚动体验。 我们通常在Quartz , TechInAsia等新闻网站以及许多移动应用程序中看到的东西。
尽管已证明这是保持用户参与度的有效方法,但它也有一个缺点:它破坏了浏览器中的“返回”按钮。 当您单击按钮时,它不会总是准确地将您滚动回到上一个访问的内容或页面。
网站以各种方式解决此问题; 例如, Quartz会将您重定向到引用的 URL。 您以前访问过的URL,但不是通过Web History API记录的URL。 TechInAsia只会带您回到首页。
结语
本教程篇幅很长,涵盖了很多内容! 其中一些很容易理解,而有些则可能不那么容易消化。 为了提供帮助,我整理了一份参考文献清单作为本文的补充。
history api