最近有个需求,移动端网站,列表上拉加载,点击详情后返回,每次都固定返回到顶部,感觉这样不够人性化,希望固定到进列表前的页面,于是简单实现了一下。

这里有两个问题

1、数据都是异步的

2、只有返回的时候定位(刷新正常回顶部)

简单的实现思路及主要代码

为了方便二次加载,异步数据每次缓存到本地,同时拦截页面所有链接,在即将跳转的时候记录当前页面或者滚动元素的scrollTop值,下次进页面判断是否返回进来的,如果是直接进来或者刷新,则重新请求,如果为返回,则直接使用已缓存数据迅速加载后使用已缓存的scrollTop值定位到进详情页之前的位置,同时清除值,即scrollTop值的缓存仅一次有效。

判断浏览器返回关键代码如下:

var navigationType = window.performance.navigation.type;

var navigation = {
    enter: navigationType === 0,
    refresh: navigationType === 1,
    back: navigationType === 2,
    type: navigationType
}

navigation.back为true的时候则为页面返回进入,当然这里兼容性并不是很好,但由于是移动端,普遍WebView内核,再加上个别浏览器如QQ、UC等返回实则直接使用缓存,触发不了JS运行,所以并不存在很大的问题,经测试Android、iOS皆可。

本地缓存,使用localStorage,但由于数据量可能比较大,这里对localStorage做了类似cookie的时效性封装,默认数据缓存30分钟,如果超时在取数据时直接返回null并移除已失效缓存,以此来降低缓存占用率,同时,每次进入页面实例化缓存封装的同时,异步扫描一次所有缓存,无效的自动移除。

至于输出,可以根据需要,对数据进行预处理或者干脆存储html,进入页面后首先对缓存进行加载,之后再做事件绑定等动作。由于我一开始上拉加载的页面都统一使用一个自己事先封装的组件,回调传回数据进行业务渲染,为了方便,我在组件内对数据进行缓存和重载,外部保持原有的业务渲染逻辑,只是每次请求数据前先获取缓存,存在则直接通过回调传回缓存数据,每次回调执行完毕后触发scrollTop,由于scrollTop传值大于当前页面长度情况,页面会滚动到底部,于是又会根据原有封装的上拉加载调用下一页的数据,如果存储的是第二页中某个位置,则可能出现第一次加载完滚动到底部立即加载第二页,然后继续触发scrollTop向底部滚动到目标位置,同理,如果存储的是第三、第四页也会持续向下滚动,于是,这里就出现了一个没法解决的问题,这样依次加载几页效果还好,如果十页、二十页,那么会有明显的滚动效果,不是很理想,由于页面数量多,且都使用了现成封装组件,这样改动量最小就暂时这样做了,最佳方案还是每次存储已加载的所有数据,甚至直接html,这样返回时一次性输出,滚动效果会达到最大限度的消除,但弊端是要在存储前对数据干涉,输出时又要对原有的输出进行干涉,大家可以权衡利弊。当然,大家也可以根据具体需要,将这种方案进一步封装,缓存的输出保持不影响原有程序,这里仅说出我的简单思路,不再赘述。

另外,如果30分钟后用户才触发缓存,这样的情况可以根据业务需要,适当增加缓存时效或者干脆做永久缓存,和坐标的存储、使用一样,使用一次直接清除,第一次之后每次进页面异步请求后默默将新数据存入缓存(如果数据变动快的话)。

注:这里不是也sessionStorage主要是因为实际应用中发现个别浏览器sessionStorage存在无故丢失甚至直接不可用等情况,所以直接采用localStorage

本地缓存简单实现代码如下:

/**
 * Created by William.Wei on 2016/5/5.
 * 参考js.cookie插件api,实现类似cookie的expires
 * var store = new Store([local|session])
 * store.set('key',{expires:1})
 * store.get('key')
 * store.getJSON('key')
 * store.remove('key')
 */
define(function (require, exports, module) {

    var global = window,
        encode = encodeURIComponent,
        decode = decodeURIComponent,
        expiresMap = {
            d: 86400000,
            h: 3600000,
            m: 60000,
            s: 1000
        },
        support = {
            local: global.localStorage,
            session: global.sessionStorage
        };

    function compare(oldData, newData) {
        return oldData === newData;
    }

    function setValue(key, value, attributes) {
        var store = this.store;
        if (typeof value === 'undefined' && attributes.expires === -1) {
            try {
                store.removeItem(encode(key));
            } catch (e) {
            }
            return;
        }

        attributes = $.extend(true, {}, this.defaults, attributes);

        if (typeof attributes.expires === 'number') {
            var expires = new Date();
            var unit = expiresMap[attributes.unit] || 1;
            expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * unit);
            attributes.expires = expires;
        }

        try {
            var result = JSON.stringify(value);
            if (/^[\{\[]/.test(result)) {
                value = result;
            }
        } catch (e) {
        }

        if (attributes.compare && ($.isFunction(attributes.compare) ? attributes.compare : compare)(getValue.call(this, key), value)) {
            return false;
        }

        value = {v: value};

        if (attributes.expires) {
            value.t = attributes.expires.getTime();
        }

        try {
            store.setItem(encode(key), JSON.stringify(value));
        } catch (e) {
        }
        return true;
    }

    function parse(key, value) {
        try {
            var temp = JSON.parse(value);
            if (!temp.t || temp.t > +new Date()) {
                value = temp.v;
                if (this.json) {
                    value = JSON.parse(value);
                }
            } else {
                //无效移除
                setValue.call(this, key, undefined, {expires: -1});
                return;
            }
        } catch (e) {
            value = undefined;
        }
        return value;
    }

    function getValue(key) {
        var value,
            store = this.store;
        if (key) {
            value = parse.call(this, key, store.getItem(encode(key)));
        } else {
            value = {};
            var len = store.length,
                tempKey;
            for (var i = 0; i < len; i++) {
                tempKey = store.key(i);
                value[tempKey] = parse.call(this, decode(tempKey), store.getItem(tempKey));
            }
        }
        return value;
    }

    function Store(type) {
        this.store = support[type || 'local'];
        //延迟1s清理失效脏数据
        setTimeout(function () {
            for (var i = 0; i < this.store.length; i++) {
                this.get(this.store.key(i));
            }
        }.bind(this), 1000);
    }

    Store.prototype = {
        defaults: {
            expires: null
        },
        get: getValue,
        set: setValue,
        getJSON: function () {
            return getValue.apply({
                store: this.store,
                json: true
            }, arguments);
        },
        remove: function (key, attributes) {
            setValue.call(this, key, undefined, $.extend(true, {}, attributes, {
                expires: -1
            }));
        },
        clear: function () {
            this.store.clear();
        }
    };

    module.exports = Store;
});