Ryan Wang's Blog

Ryan Wang

无刷新加载下一页方案解析

2018-12-06

前段时间移植了一个Ghost的主题的时候(就是我现在用的这个),发现这个下拉加载下一页特别有意思,只用了短短几行代码,且后端没有重写请求方法,就轻而易举的实现了,于是乎就简单分析了一下。以供有需要的人参考一下。

之前是这样做的

我之前做这种无刷新加载下一页都是用的字符串拼接,把html代码和新的数据拼接起来然后append进去。这种方法虽然可以实现功能,但是有很多缺点,比如如果修改了页面的样式,那么很可能就要重新写拼接的字符串,而且整个代码看起来非常不好看(我有代码洁癖)。就像下面代码一样,宛如一坨shit...

$.each(data,function (n,value) {
	$('.post-list').append('' +
			'<div class="col-lg-4 col-md-6 col-sm-6 col-xs-12 post-list-item" data-aos="fade-up">\n' +
			'<div class="post-item-main">\n' +
			'<a href="/archives/'+value.postUrl+'">\n' +
			'<div class="post-item-thumbnail lazy" style="background-image: url(/halo/source/img/pic12.jpg)"></div>\n' +
			'</a>\n' +
			'<div class="post-item-info">\n' +
			'<div class="post-info-title">\n' +
			'<a href="/archives/'+value.postUrl+'"><span>'+value.postTitle+'</span></a><br>\n' +
			'</div>\n' +
			'<div>\n' +
			'<span class="post-info-desc">\n' +
			''+value.postSummary+'...' +
			'</span>\n' +
			'</div>\n' +
			'<div class="post-info-other" style="text-align: right">\n' +
			'<a href="#">MORE></a>\n' +
			'</div>\n' +
			'</div>\n' +
			'</div>\n' +
			'</div>'
	);
});

新学到的方法

废话少说,先上代码,地址https://github.com/TryGhost/Casper/blob/master/assets/js/infinitescroll.js

/* global maxPages */
var maxPages = parseInt(totalPages);

// Code snippet inspired by https://github.com/douglasrodrigues5/ghost-blog-infinite-scroll
$(function ($) {
    var currentPage = 1;
    var pathname = window.location.pathname;
    var $document = $(document);
    var $result = $('.post-feed');
    var buffer = 300;

    var ticking = false;
    var isLoading = false;

    var lastScrollY = window.scrollY;
    var lastWindowHeight = window.innerHeight;
    var lastDocumentHeight = $document.height();

    function onScroll() {
        lastScrollY = window.scrollY;
        requestTick();
    }

    function onResize() {
        lastWindowHeight = window.innerHeight;
        lastDocumentHeight = $document.height();
        requestTick();
    }

    function requestTick() {
        if (!ticking) {
            requestAnimationFrame(infiniteScroll);
        }
        ticking = true;
    }

    function sanitizePathname(path) {
        var paginationRegex = /(?:page\/)(\d)(?:\/)$/i;

        // remove hash params from path
        path = path.replace(/#(.*)$/g, '').replace('////g', '/');

        // remove pagination from the path and replace the current pages
        // with the actual requested page. E. g. `/page/3/` indicates that
        // the user actually requested page 3, so we should request page 4
        // next, unless it's the last page already.
        if (path.match(paginationRegex)) {
            currentPage = parseInt(path.match(paginationRegex)[1]);

            path = path.replace(paginationRegex, '');
        }

        return path;
    }

    function infiniteScroll() {
        // sanitize the pathname from possible pagination or hash params
        pathname = sanitizePathname(pathname);

        // return if already loading
        if (isLoading) {
            return;
        }

        // return if not scroll to the bottom
        if (lastScrollY + lastWindowHeight <= lastDocumentHeight - buffer) {
            ticking = false;
            return;
        }

        /**
        * maxPages is defined in default.hbs and is the value
        * of the amount of pagination pages.
        * If we reached the last page or are past it,
        * we return and disable the listeners.
        */
        if (currentPage >= maxPages) {
            window.removeEventListener('scroll', onScroll, {passive: true});
            window.removeEventListener('resize', onResize);
            return;
        }

        isLoading = true;

        // next page
        currentPage += 1;

        // Load more
        var nextPage = pathname + 'page/' + currentPage + '/';

        $.get(nextPage, function (content) {
            var parse = document.createRange().createContextualFragment(content);
            var posts = parse.querySelectorAll('.post');
            if (posts.length) {
                [].forEach.call(posts, function (post) {
                    $result[0].appendChild(post);
                });
            }
        }).fail(function (xhr) {
            // 404 indicates we've run out of pages
            if (xhr.status === 404) {
                window.removeEventListener('scroll', onScroll, {passive: true});
                window.removeEventListener('resize', onResize);
            }
        }).always(function () {
            lastDocumentHeight = $document.height();
            isLoading = false;
            ticking = false;
        });
    }

    window.addEventListener('scroll', onScroll, {passive: true});
    window.addEventListener('resize', onResize);

    infiniteScroll();
});

我们知道的是,很多CMS系统,博客系统(Wordpress,typecho,Hexo等)的下一页路径几乎都是/page/页码,所以上面的方法几乎可以通用,就算不是,改改也是可以的。好了,下面详细来解析一下上面的代码吧。

获取总页数

在最上面有一行代码var maxPages = parseInt(totalPages); 是用于定义总页数的,不知道?那么想办法后端返回一个吧,这个有什么用呢?看下面:

if (currentPage >= maxPages) {
	window.removeEventListener('scroll', onScroll, {passive: true});
	window.removeEventListener('resize', onResize);
	return;
}

就是说当当前页面大于等于总页数的的时候就停止执行下面的代码,而下面的代码则是加载下一页的,所以说需要有这个判断,当获取到最后一页的时候就停止获取。

加载下一页

可以看到,方法的最上面定义了一个变量var currentPage = 1;,这个变量代表着第一页的页码,当页面滑动到最底部的时候就会调用infiniteScroll方法,并且给currentPage加上一,然后就请求下一页的数据。此时的请求路径也就是/page/2

解析请求的路径

我们看到这一段:

$.get(nextPage, function (content) {
	var parse = document.createRange().createContextualFragment(content);
	var posts = parse.querySelectorAll('.post');
	if (posts.length) {
		[].forEach.call(posts, function (post) {
			$result[0].appendChild(post);
		});
	}
})

使用get方式请求下一页的数据,需要注意的是,虽然是使用ajax请求的,但是是请求的下一页整个页面的内容,所以请求获得的数据便是整个页面的html代码。所以这里使用到了querySelectorAll这个方法,截取全部以post类的html代码,并存入posts变量中,然后在遍历posts,得到单个文章列表项,最后再追加到页面中,整个过程一气呵成。

总结

整个获取下一页数据的基本流程为:

  1. 判断页面是否滑动到底部,如已滑动到底部,则调用加载页面的方法,并把currentPage在原基础上加1,并需要判断当前页码是否大于总页数,如大于,则不调用加载页面的方法。
  2. 使用ajax请求下一页的数据。
  3. 解析请求下一页所获得的数据,并截取所有以post类的html代码。注意:并非所有单个文章列表项的class都为post,请按实际情况来。
  4. 追加到当前页面的文章列表中。