在JavaScript/浏览器中缓存jQuery AJAX响应

107
我想在JavaScript/浏览器中启用ajax响应的缓存。
jquery.ajax docs中得知:
默认情况下,请求总是发出,但浏览器可能会从其缓存中提供结果。要禁止使用缓存结果,请将cache设置为false。要使请求在自上次请求以来未修改资产时报告失败,请将ifModified设置为true。
然而,这两者都没有解决强制缓存的问题。
动机: 我希望将$.ajax({...})调用放在我的初始化函数中,其中一些请求相同的url。有时我需要调用其中一个初始化函数,有时我需要调用几个。
因此,如果已经加载了特定的url,我希望将对服务器的请求最小化。
我可以自己编写解决方案(有些困难!),但我想知道是否有标准方法来实现这一点。

我认为跟踪已经加载的URL并将结果存储在该列表中并不难。然后,在进行AJAX调用之前,您可以检查您的URL。你就拥有了自己的基本缓存。 - user1864610
你可以在服务器响应中添加缓存控制和过期头,这样只有在你配置的超时时间之后才会调用你的服务器。 - hereandnow78
很长,但你能帮我理解为什么需要在ajax请求中使用缓存吗?(可能用通俗易懂的语言) - Asish
为什么内置的浏览器缓存不够好并不清楚。 - Martlark
6个回答

159

cache:true 只适用于 GET 和 HEAD 请求。

你可以像你所说的那样使用类似以下的方法来自己实现缓存:

var localCache = {
    data: {},
    remove: function (url) {
        delete localCache.data[url];
    },
    exist: function (url) {
        return localCache.data.hasOwnProperty(url) && localCache.data[url] !== null;
    },
    get: function (url) {
        console.log('Getting in cache for url' + url);
        return localCache.data[url];
    },
    set: function (url, cachedData, callback) {
        localCache.remove(url);
        localCache.data[url] = cachedData;
        if ($.isFunction(callback)) callback(cachedData);
    }
};

$(function () {
    var url = '/echo/jsonp/';
    $('#ajaxButton').click(function (e) {
        $.ajax({
            url: url,
            data: {
                test: 'value'
            },
            cache: true,
            beforeSend: function () {
                if (localCache.exist(url)) {
                    doSomething(localCache.get(url));
                    return false;
                }
                return true;
            },
            complete: function (jqXHR, textStatus) {
                localCache.set(url, jqXHR, doSomething);
            }
        });
    });
});

function doSomething(data) {
    console.log(data);
}

这里有一个可用的演示

编辑:随着这篇文章变得流行,如果您想要管理超时缓存并且不必担心在$.ajax()中出现的所有麻烦,以下是更好的答案,我使用了$.ajaxPrefilter()。现在只需设置{cache: true}即可正确处理缓存:

var localCache = {
    /**
     * timeout for cache in millis
     * @type {number}
     */
    timeout: 30000,
    /** 
     * @type {{_: number, data: {}}}
     **/
    data: {},
    remove: function (url) {
        delete localCache.data[url];
    },
    exist: function (url) {
        return !!localCache.data[url] && ((new Date().getTime() - localCache.data[url]._) < localCache.timeout);
    },
    get: function (url) {
        console.log('Getting in cache for url' + url);
        return localCache.data[url].data;
    },
    set: function (url, cachedData, callback) {
        localCache.remove(url);
        localCache.data[url] = {
            _: new Date().getTime(),
            data: cachedData
        };
        if ($.isFunction(callback)) callback(cachedData);
    }
};

$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
    if (options.cache) {
        var complete = originalOptions.complete || $.noop,
            url = originalOptions.url;
        //remove jQuery cache as we have our own localCache
        options.cache = false;
        options.beforeSend = function () {
            if (localCache.exist(url)) {
                complete(localCache.get(url));
                return false;
            }
            return true;
        };
        options.complete = function (data, textStatus) {
            localCache.set(url, data, complete);
        };
    }
});

$(function () {
    var url = '/echo/jsonp/';
    $('#ajaxButton').click(function (e) {
        $.ajax({
            url: url,
            data: {
                test: 'value'
            },
            cache: true,
            complete: doSomething
        });
    });
});

function doSomething(data) {
    console.log(data);
}

这里有示例代码 注意,使用$.Deferred会无法工作

下面是一份有缺陷但能够工作的使用deferred的实现:

var localCache = {
    /**
     * timeout for cache in millis
     * @type {number}
     */
    timeout: 30000,
    /** 
     * @type {{_: number, data: {}}}
     **/
    data: {},
    remove: function (url) {
        delete localCache.data[url];
    },
    exist: function (url) {
        return !!localCache.data[url] && ((new Date().getTime() - localCache.data[url]._) < localCache.timeout);
    },
    get: function (url) {
        console.log('Getting in cache for url' + url);
        return localCache.data[url].data;
    },
    set: function (url, cachedData, callback) {
        localCache.remove(url);
        localCache.data[url] = {
            _: new Date().getTime(),
            data: cachedData
        };
        if ($.isFunction(callback)) callback(cachedData);
    }
};

$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
    if (options.cache) {
        //Here is our identifier for the cache. Maybe have a better, safer ID (it depends on the object string representation here) ?
        // on $.ajax call we could also set an ID in originalOptions
        var id = originalOptions.url+ JSON.stringify(originalOptions.data);
        options.cache = false;
        options.beforeSend = function () {
            if (!localCache.exist(id)) {
                jqXHR.promise().done(function (data, textStatus) {
                    localCache.set(id, data);
                });
            }
            return true;
        };

    }
});

$.ajaxTransport("+*", function (options, originalOptions, jqXHR, headers, completeCallback) {

    //same here, careful because options.url has already been through jQuery processing
    var id = originalOptions.url+ JSON.stringify(originalOptions.data);

    options.cache = false;

    if (localCache.exist(id)) {
        return {
            send: function (headers, completeCallback) {
                completeCallback(200, "OK", localCache.get(id));
            },
            abort: function () {
                /* abort code, nothing needed here I guess... */
            }
        };
    }
});

$(function () {
    var url = '/echo/jsonp/';
    $('#ajaxButton').click(function (e) {
        $.ajax({
            url: url,
            data: {
                test: 'value'
            },
            cache: true
        }).done(function (data, status, jq) {
            console.debug({
                data: data,
                status: status,
                jqXHR: jq
            });
        });
    });
});

这里是Fiddle。 一些问题,我们的缓存ID取决于json2库JSON对象的表示。

使用控制台视图(F12)或FireBug查看缓存生成的一些日志。


你在 localCache.set 函数上放回调的原因是什么?为什么不在设置缓存后直接执行 doSomething(jqXHR) - cammil
2
有什么建议可以改进这个代码以支持使用 $.ajax 作为 Promise?从 beforeSend 返回 false 将取消请求(这是应该的),因此现有的 $.ajax(...).done(function(response) {...}).fail(...) 现在停止工作,因为调用了 fail 而不是 done... 我宁愿不重写它们所有 :) - franck102
@franck102 哦,是啊,我还没有想到那个。我会在这个周末做些工作并更新答案。 - TecHunter
2
@TecHunter 非常感谢您的帮助。还有三个小改进可以做。首先,如果同时对同一资源进行多个请求,则所有请求都会错过缓存。为了解决这个问题,您可能需要将某个“id”的缓存设置为挂起状态,并推迟发送后续请求的结果,直到第一个请求返回。其次,您可能希望缓存请求的错误结果,以便所有请求相同的资源都获得相同的结果。 - mjhm
2
@TecHunter - 不错的解决方案,我使用了这个解决方案并进行了一个重要的更改。如果缓存对象在其他函数中被修改,那么会导致问题,因此在设置和获取缓存对象时,我返回该对象的副本,如下所示:`localCache.data[url] = { _: new Date().getTime(), data: _.cloneDeep(cachedData, true) };_.cloneDeep(localCache.data[url].data, true)` - Bharat Patil
显示剩余23条评论

14

我正在寻找一种为我的PhoneGap应用程序存储缓存的方法,我发现@TecHunter的答案非常好,但是它是使用localCache完成的。

我发现并知道localStorage是另一种替代方法来缓存通过ajax调用返回的数据。因此,我创建了一个演示,使用localStorage将帮助其他可能想要使用localStorage而不是localCache进行缓存的人。

Ajax调用:

$.ajax({
    type: "POST",
    dataType: 'json',
    contentType: "application/json; charset=utf-8",
    url: url,
    data: '{"Id":"' + Id + '"}',
    cache: true, //It must "true" if you want to cache else "false"
    //async: false,
    success: function (data) {
        var resData = JSON.parse(data);
        var Info = resData.Info;
        if (Info) {
            customerName = Info.FirstName;
        }
    },
    error: function (xhr, textStatus, error) {
        alert("Error Happened!");
    }
});

将数据存储到本地存储中:

$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
if (options.cache) {
    var success = originalOptions.success || $.noop,
        url = originalOptions.url;

    options.cache = false; //remove jQuery cache as we have our own localStorage
    options.beforeSend = function () {
        if (localStorage.getItem(url)) {
            success(localStorage.getItem(url));
            return false;
        }
        return true;
    };
    options.success = function (data, textStatus) {
        var responseData = JSON.stringify(data.responseJSON);
        localStorage.setItem(url, responseData);
        if ($.isFunction(success)) success(responseJSON); //call back to original ajax call
    };
}
});

如果你想要移除localStorage,可以在任何需要的地方使用以下语句:

localStorage.removeItem("Info");

希望这能帮助其他人!


嗨,在 localStorage.removeItem("Info"); 中,关于 "info",它是指 URL 吗? - vsogrimen
@vsogrimen info 是用于将数据存储在本地存储中的对象。 - immayankmodi
1
一直收到“responseJSON未定义”的错误。有什么办法可以解决吗?(我的数据类型是HTML) - Nikita 웃
@CreativeMind,请移除responseJSON,并使用“var responseData = JSON.stringify(data);”代替,对于success(data)也做同样的更改。 - Unicco
我更喜欢使用localStorage,因为我需要在多个请求之间进行缓存。 - KeitelDOG

10
现代浏览器都提供了存储API,您可以使用它们(localStorage或sessionStorage)来保存数据。您只需要在接收到响应后将其存储到浏览器存储中。然后下次找到相同的调用时,搜索是否已经保存了响应。如果是,则从那里返回响应;如果没有,则进行新的调用。Smartjax插件也做类似的事情;但由于您的要求仅是保存调用响应,因此您可以在jQuery ajax成功函数中编写代码以保存响应。在进行调用之前,只需检查响应是否已保存即可。

我在IndexedDB中保存了响应,有没有办法检查IndexedDB?另外,如果我使用Session Storage,那么有没有办法使用jQuery检查是否存在响应。我不能包含除jQuery之外的任何库。谢谢。 - Abhishek Aggarwal

9
如果我理解了你的问题,这里是解决方案:
    $.ajaxSetup({ cache: true});

对于特定的调用

 $.ajax({
        url: ...,
        type: "GET",
        cache: false,           
        ...
    });

如果你想要相反的效果(针对特定调用进行缓存),你可以在开头设置为false,然后对于特定的调用设置为true。

2
那恰好相反。 - TecHunter
它对我有效,谢谢!奇怪的是我的页面默认没有启用缓存。 - Timur Nurlygayanov
1
它将被缓存多长时间? - Pardeep Jain

2

虽然这是一个老问题,但我的解决方案有些不同。

我正在编写一个单页 Web 应用程序,它会不断地通过用户触发的 ajax 调用,并且为了使它更加困难,还需要使用除 jQuery 之外的其他方法(如 dojo,原生 xhr 等)的库。我为其中一个自己的库编写了一个插件,以尽可能高效地缓存 ajax 请求,并以一种适用于所有主要浏览器的方式实现,无论使用哪个库来进行 ajax 调用。

该解决方案使用jSQL(由我编写 - 一种在 JavaScript 中编写的客户端持久性 SQL 实现,它使用 indexeddb 和其他 DOM 存储方法),并与另一个名为XHRCreep(由我编写)的库捆绑在一起,后者是本机 XHR 对象的完全重写。

要实施,您只需要在页面中包含插件,在此处

有两个选项:

jSQL.xhrCache.max_time = 60;

设置缓存的最大过期时间,以分钟为单位。任何超过此时间的缓存响应都会重新请求。默认值为1小时。

jSQL.xhrCache.logging = true;

当设置为true时,模拟的XHR调用将在控制台中显示以进行调试。

您可以通过以下方式清除任何给定页面上的缓存

jSQL.tables = {}; jSQL.persist();

我在 GitHub 和官方网站上找不到你的插件。请更新你的回答,我需要你的插件 :) 我是你在 GitHub 上的追随者。干得好 ;) @occams-razor - Alican Kablan

-1
        function getDatas() {
            let cacheKey = 'memories';

            if (cacheKey in localStorage) {
                let datas = JSON.parse(localStorage.getItem(cacheKey));

                // if expired
                if (datas['expires'] < Date.now()) {
                    localStorage.removeItem(cacheKey);

                    getDatas()
                } else {
                    setDatas(datas);
                }
            } else {
                $.ajax({
                    "dataType": "json",
                    "success": function(datas, textStatus, jqXHR) {
                        let today = new Date();

                        datas['expires'] = today.setDate(today.getDate() + 7) // expires in next 7 days

                        setDatas(datas);

                        localStorage.setItem(cacheKey, JSON.stringify(datas));
                    },
                    "url": "http://localhost/phunsanit/snippets/PHP/json.json_encode.php",
                });
            }
        }

        function setDatas(datas) {
            // display json as text
            $('#datasA').text(JSON.stringify(datas));

            // your code here
           ....

        }

        // call
        getDatas();

在此输入链接描述


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接