history api_如何使用History Web API建立无限滚动体验

history api

在本教程中,我们将增强我们的History Web API技能。 我们将在Web上构建一个UX模式,该模式将受到同样的喜爱和厌恶: 无限滚动

无限滚动是一种界面模式,可在我们到达给定网页的末尾时加载新内容。 如果进行周到的实施,无限滚动可以保留用户的参与度。 一些最佳示例出现在Facebook,Twitter和Pinterest等社交平台上。

但是,如果您对挑战的想法感到兴奋,请系紧安全带,做好准备,然后开始吧!

建立演示网站

我们的网站是静态博客。 您可以从纯HTML生成它,也可以利用静态网站生成器(例如JekyllMiddlemanHexo) 。 本教程的演示如下所示:

关于HTML结构,有几件事需要引起您的注意。

<!-- site header -->
<div class="content" id="main">
	<article class="article" id="article-5" data-article-id="5">
		<!-- content -->
	</article>
<!-- site footer -->
  1. 从上面的代码片段中可以看到,文章应该包装在具有唯一IDHTML元素中。 您可以使用divsection元素,而在命名该元素的id没有任何限制。
  2. 同样,在商品本身上,您将需要添加一个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

该插件将使我们能够通过OptionsSettings引入灵活性。

观察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 );
  1. 在代码的第一部分中,我们根据JavaScript的通用命名约定将插件名称keepScrolling指定为“ camel case”。 我们还有一个变量defaults ,它将包含插件的默认设置。
  2. 接下来,我们有插件的主要功能Plugin() 。 可以将此功能与“构造函数”进行比较,在这种情况下,该“构造函数”将初始化插件并将默认设置与实例化插件时传递的所有设置合并。
  3. 第三部分是我们将组成自己的函数以提供无限滚动功能的地方。
  4. 最后,第四部分是将整个内容包装到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"
		}]
	});
} );

无限滚动现在应该可以运行了。

警告:后退按钮

在本教程中,我们建立了无限的滚动体验。 我们通常在QuartzTechInAsia等新闻网站以及许多移动应用程序中看到的东西。

尽管已证明这是保持用户参与度的有效方法,但它也有一个缺点:它破坏了浏览器中的“返回”按钮。 当您单击按钮时,它不会总是准确地将您滚动回到上一个访问的内容或页面。

网站以各种方式解决此问题; 例如, Quartz会将您重定向到引用的 URL。 您以前访问过的URL,但不是通过Web History API记录的URL。 TechInAsia只会带您回到首页。

结语

本教程篇幅很长,涵盖了很多内容! 其中一些很容易理解,而有些则可能不那么容易消化。 为了提供帮助,我整理了一份参考文献清单作为本文的补充。

最后, 查看完整的源代码查看演示

翻译自: https://webdesign.tutsplus.com/tutorials/how-to-build-an-infinite-scroll-experience-with-the-history-web-api--cms-26631

history api

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值