localForage:离线存储,更上一层楼

Web 应用已经具备了像保存大型数据集和二进制文件之类的离线功能。您甚至可以做一些事情,例如 缓存 MP3 文件。浏览器技术可以离线存储大量数据。但是,问题在于,用于实现此目的的技术选择是碎片化的。

localStorage 为您提供了非常基本的数据存储,但它速度很慢,无法处理二进制 Blob。IndexedDBWebSQL 是异步的、快速的,并且支持大型数据集,但它们的 API 并不直观。即使这样,IndexedDBWebSQL 也并非所有主要浏览器供应商都支持,而且这种情况在短期内似乎不会改变。

如果您需要编写一个支持离线功能的 Web 应用,并且不知道从哪里开始,那么这篇文章适合您。如果您曾经尝试过开始使用离线支持,但它让您头晕眼花,那么这篇文章也适合您。Mozilla 创建了一个名为 localForage 的库,它使在任何浏览器中离线存储数据变得更加轻松。

around 是一款我编写的 HTML5 Foursquare 客户端,它帮助我克服了离线存储的一些痛点。我们仍然将逐步介绍如何使用 localForage,但对于那些喜欢通过查看代码来学习的人来说,这里有一些源代码。

localForage 是一个 JavaScript 库,它使用非常简单的 localStorage APIlocalStorage 本质上为您提供了 get、set、remove、clear 和 length 的功能,但它还添加了

  • 带有回调的异步 API
  • IndexedDBWebSQLlocalStorage 驱动程序(自动管理;将为您加载最佳驱动程序)
  • Blob 和任意类型支持,因此您可以存储图像、文件等。
  • 对 ES6 Promise 的支持

包含 IndexedDBWebSQL 支持,使您可以为 Web 应用存储比单独使用 localStorage 更多的数据。它们的 API 的非阻塞特性通过不在 get/set 调用上挂起主线程来提高应用速度。对 promise 的支持使编写没有回调堆的 JavaScript 成为一种乐趣。当然,如果您喜欢回调,localForage 也支持回调。

说够了,给我看看它怎么工作吧!

传统 localStorage API 在许多方面实际上是非常不错的;它使用起来很简单,不会强制使用复杂的数据结构,并且不需要任何样板代码。如果您想在一个应用中保存一些配置信息,您只需要编写以下代码:

// Our config values we want to store offline.
var config = {
    fullName: document.getElementById('name').getAttribute('value'),
    userId: document.getElementById('id').getAttribute('value')
};

// Let's save it for the next time we load the app.
localStorage.setItem('config', JSON.stringify(config));

// The next time we load the app, we can do:
var config = JSON.parse(localStorage.getItem('config'));

请注意,我们需要将值以字符串形式保存在 localStorage 中,因此在与之交互时,我们将转换为/从 JSON 进行转换。

这看起来非常简单,但您会立即注意到 localStorage 的几个问题

  1. 它是同步的。我们会等到数据从磁盘读取并解析后,无论它有多大。这会降低应用的响应速度。这在移动设备上尤其糟糕;主线程将被暂停,直到数据被获取,这会使您的应用看起来很慢,甚至无响应。

  2. 它只支持字符串。您注意到我们必须使用 JSON.parseJSON.stringify 吗?这是因为 localStorage 只支持 JavaScript 字符串的值。没有数字、布尔值、Blob 等。这使得存储数字或数组很麻烦,并且实际上使得存储 Blob 变得不可能(或者至少非常麻烦和缓慢)。

使用 localForage 的更好方法

localForage 通过使用异步 API 但使用 localStorage 的 API 来解决这两个问题。将使用 IndexedDB 与 localForage 比较,以查看同一部分数据

IndexedDB 代码

// IndexedDB.
var db;
var dbName = "dataspace";

var users = [ {id: 1, fullName: 'Matt'}, {id: 2, fullName: 'Bob'} ];

var request = indexedDB.open(dbName, 2);

request.onerror = function(event) {
    // Handle errors.
};
request.onupgradeneeded = function(event) {
    db = event.target.result;

    var objectStore = db.createObjectStore("users", { keyPath: "id" });

    objectStore.createIndex("fullName", "fullName", { unique: false });

    objectStore.transaction.oncomplete = function(event) {
        var userObjectStore = db.transaction("users", "readwrite").objectStore("users");
    }
};

// Once the database is created, let's add our user to it...

var transaction = db.transaction(["users"], "readwrite");

// Do something when all the data is added to the database.
transaction.oncomplete = function(event) {
    console.log("All done!");
};

transaction.onerror = function(event) {
    // Don't forget to handle errors!
};

var objectStore = transaction.objectStore("users");

for (var i in users) {
    var request = objectStore.add(users[i]);
    request.onsuccess = function(event) {
        // Contains our user info.
        console.log(event.target.result);
    };
}

WebSQL 不会那么冗长,但它仍然需要相当多的样板代码。使用 localForage,您可以编写以下代码

localForage 代码

// Save our users.
var users = [ {id: 1, fullName: 'Matt'}, {id: 2, fullName: 'Bob'} ];
localForage.setItem('users', users, function(result) {
    console.log(result);
});

这比以前少了一些工作。

除字符串以外的数据

假设您想下载用户的个人资料图片以供您的应用使用,并将其缓存以供离线使用。使用 localForage 保存二进制数据很容易

// We'll download the user's photo with AJAX.
var request = new XMLHttpRequest();

// Let's get the first user's photo.
request.open('GET', "/users/1/profile_picture.jpg", true);
request.responseType = 'arraybuffer';

// When the AJAX state changes, save the photo locally.
request.addEventListener('readystatechange', function() {
    if (request.readyState === 4) { // readyState DONE
        // We store the binary data as-is; this wouldn't work with localStorage.
        localForage.setItem('user_1_photo', request.response, function() {
            // Photo has been saved, do whatever happens next!
        });
    }
});

request.send()

下次我们只需要三行代码就可以从 localForage 中获取照片

localForage.getItem('user_1_photo', function(photo) {
    // Create a data URI or something to put the photo in an img tag or similar.
    console.log(photo);
});

回调和 promise

如果您不喜欢在代码中使用回调,可以使用 ES6 Promise 而不是 localForage 中的回调参数。让我们从上一个示例中获取该照片,但使用 promise 而不是回调

localForage.getItem('user_1_photo').then(function(photo) {
    // Create a data URI or something to put the photo in an  tag or similar.
    console.log(photo);
});

诚然,这是一个有点牵强的例子,但 around 有一些 真实代码,如果您有兴趣了解该库在日常使用中的情况。

跨浏览器支持

localForage 支持所有现代浏览器IndexedDB 在除 Safari 之外的所有现代浏览器中可用(IE 10+、IE Mobile 10+、Firefox 10+、Firefox for Android 25+、Chrome 23+、Chrome for Android 32+ 和 Opera 15+)。同时,Android 浏览器(2.1+)和 Safari 使用 WebSQL

在最坏的情况下,localForage 将退回到 localStorage,因此您至少可以离线存储基本数据(虽然不是 blob,而且速度更慢)。它至少会负责自动将您的数据转换为/从 JSON 字符串进行转换,这就是 localStorage 需要数据存储的方式。

在 GitHub 上了解更多关于 localForage 的信息,如果您想看到该库做更多的事情,请提交问题!

关于 Matthew Riley MacPherson

Matthew Riley MacPherson(又名 tofumatt)是一位生活在 Pythonista 世界中的 Rubyist。他来自加拿大,所以您会在他的文章中发现很多奇怪的拼写(比如“colour”或“labour”)。他对漂亮的代码、优质咖啡和非常快的摩托车有着浓厚的兴趣。查看他的 GitHub 代码在 Twitter 上与他谈论摩托车

Matthew Riley MacPherson 的更多文章...

关于 Robert Nyman [荣誉编辑]

Mozilla Hacks 的技术布道师和编辑。关于 HTML5、JavaScript 和开放网络进行演讲和博客。Robert 是 HTML5 和开放网络的坚定支持者,自 1999 年以来一直从事 Web 前端开发工作 - 在瑞典和纽约市。他经常在 http://robertnyman.com 上写博客,并且喜欢旅行和结识朋友。

Robert Nyman [荣誉编辑] 的更多文章...

关于 Angelina Fabbro

我是一名来自加拿大不列颠哥伦比亚省温哥华的开发者,在 Mozilla 工作,担任 Firefox OS 的技术布道师和开发者倡导者。我喜欢 JavaScript、Web 组件、Node.js、移动应用开发,以及这个我一直喜欢待的地方,叫做万维网。哦,还有,别忘了 Firefox OS。在我的业余时间,我会上唱歌课、玩万智牌、教人编程,以及与科学家合作,以促进程序员与科学家之间更好的互动。

Angelina Fabbro 的更多文章...


33 条评论

  1. Lucas Holmquist

    这看起来真的很酷,
    我们在 AeroGear 中也有类似的东西,它被称为 datamanager,在我们的 JS 库中:https://github.com/aerogear/aerogear-js

    2014 年 2 月 12 日 上午 07:02

  2. Joe Larson

    不错!现在添加命名空间还来得及吗? https://github.com/joelarson4/LSNS

    2014 年 2 月 12 日 下午 07:34

    1. tofumatt

      这个库目前还在发展中,因为它还没有完全实现我想要在稳定版 1.0 中发布的所有功能。它可以很好地处理这些简单的用例,但如果你想看到更多功能的实现,请在 GitHub 上提交一个问题,我们可以讨论一下。

      2014 年 2 月 12 日 下午 09:17

  3. Patrik

    如何实现持久性存储?也就是说,即使浏览器关闭或电脑关机,数据也不会被清除的存储。

    2014 年 2 月 12 日 下午 08:13

    1. tofumatt

      所有使用的存储库都是持久性的;例如: MDN 明确提到 localStorage 是持久性的.

      2014 年 2 月 12 日 下午 09:22

  4. Felipe N. Moura

    真的很酷!
    帮助很多,使用和实现回退都变得更简单!

    跨域一直是个问题...
    有没有可能使用某种代理?

    2014 年 2 月 12 日 下午 08:20

  5. Matěj Cepl

    高保真度并不是任何事物的最佳示例,我认为…… https://travis-ci.org/mcepl/high-fidelity :( 而且是的,我非常希望为我的 Peak 拥有一个可用的播客接收器。到目前为止,https://github.com/colinfrei/Podcast/ 是我们最好的选择。

    2014 年 2 月 12 日 下午 09:06

    1. tofumatt

      Podcasts 应用程序的测试失败与其作为糟糕示例无关,更多的是因为我以一种奇怪的方式编写测试,这些测试在 Travis 上运行不佳(值得一提的是,在本地运行这些测试实际上是有效的)。

      此外,高保真度的代码启发了 localForage,但没有直接使用该库。

      2014 年 2 月 12 日 下午 09:18

      1. Matěj Cepl

        不仅是 Travis-CI 的问题。当我在我的 Peak 上运行 High-fidelity(使用 1.1hd ... 任何更新的版本都没有手机连接;https://bugzilla.mozilla.org/show_bug.cgi?id=924999)时,我得到的只是一个黑屏,没有任何其他内容。因此,不幸的是,尽管 Podcast 对我来说很糟糕,但它是最好的播客接收器(或者在电脑上使用 bashpodder 并通过 USB 线缆进行 rsync)。

        2014 年 2 月 16 日 下午 13:20

  6. Stu

    我想知道一个简单的文件系统类型 API 是否可行 - 这些文件不会存在于客户端本身,而是在某种虚拟空间中,每个应用程序一个?

    [可能这些文件会存在于一个带有文件系统的镜像文件中]。

    2014 年 2 月 12 日 下午 09:20

    1. tofumatt

      有一个正在开发中的 文件系统 API,但它还没有被广泛使用,并且涵盖了更小众的用例。当它更好地标准化时,它将成为存储文件比 localForage 更好的选择,当然。

      这个库的目标之一是提供强大的跨浏览器支持,因为我认为这是一个好的离线存储 API 所缺少的东西。

      2014 年 2 月 12 日 下午 09:26

  7. Mindaugas J.

    不错的解决方案。我们最近在扩展程序中正好需要这样的功能。localStorage API 非常简单,我们已经有了它的异步包装器,但问题是存储限制。 https://github.com/jensarps/IDBWrapper 是我最接近的,它在使用 IDB 的同时保持了简单性。

    显然,Safari 需要一个 IDB polyfill。

    我认为现在是时候尝试 localForage 了 :)

    2014 年 2 月 12 日 下午 09:23

    1. tofumatt

      我一直很困扰 Safari 的缺乏支持,我尝试过的 IndexedDB polyfills 最好也只能解决 CSP 问题。请告诉我进展如何,不要犹豫提交问题!

      2014 年 2 月 12 日 下午 09:28

      1. Mindaugas J.

        所以从一开始,localForage 就存在明显的缺陷。所有问题都与命名空间有关。首先,你不允许使用自定义的数据库名称。我看到 GitHub 上已经提交了一个关于此问题的 issue。其次,即使在同一个应用程序中,也习惯于使用 localStorage 为键名添加命名空间,例如 'myapp.foo.bar.baz'。由于异步迭代所有键很麻烦(发现另一个已提交的 issue),因此至少能够清除任何命名空间中的所有键会很好,例如 myapp.foo.bar.*、myapp.foo.* 或整个 myapp.*。当然,有一个 clear() 方法,但它没什么用,因为它只能清空整个数据库,这也让我们回到了第一个问题。

        以下是我使用 IDBWrapper 实现它的方法: http://pastebin.com/VBhRQDir

        2014 年 2 月 27 日 下午 09:23

  8. Fawad Hassan

    这和 lawnchair 类似吗?
    http://brian.io/lawnchair/

    2014 年 2 月 12 日 下午 11:43

    1. tofumatt

      它在某些方面看起来很相似,是的。它似乎更像是一个模块化系统,但它看起来不错。

      2014 年 2 月 12 日 下午 12:22

  9. James

    我有点困惑。你列出的支持的浏览器是:“IE 10+、IE Mobile 10+、Firefox 10+、Firefox for Android 25+、Chrome 23+、Chrome for Android 32+ 和 Opera 15+。”

    但紧接着你写道:“在最坏的情况下,localForage 将回退到 localStorage”,而根据 https://caniuse.cn/#feat=namevalue-storage,localStorage 支持 IE 8+、Firefox 3.5+、Chrome 4+、Opera 10.5+ 和 Safari 4+。

    所以哪个是对的?

    2014 年 2 月 12 日 下午 12:06

    1. tofumatt

      抱歉,我应该说明一下:这些浏览器支持异步存储。但你说得对:任何支持 localStorage 的浏览器都是受支持的。

      2014 年 2 月 12 日 下午 12:14

  10. Brock

    这正是我一直希望 localStorage 的工作方式。干得好。

    2014 年 2 月 12 日 下午 16:42

  11. Ido Green

    干得好!
    我很乐意看到它也涵盖文件系统 API,并且只在浏览器支持的情况下使用它。
    感谢你的辛勤工作!

    2014 年 2 月 14 日 上午 03:38

    1. tofumatt

      这可能是我最终会实现的功能,因为存储图像或 MP3 等内容使用更适合此任务的 API 会更方便。

      2014 年 2 月 14 日 下午 11:23

  12. Sumeet

    干得好!
    我正在准备一个提案,招标文件的要求是必须有一个具有离线功能的 Web 应用程序。离线功能是针对一个需要大量数据录入流程的应用程序,其中有大约 5 个章节、大量网格和数百个问题。这种数据录入流程需要通过 Web 界面来实现,以确保来自互联网连接不可靠的国家的用户能够离线完成大部分数据录入工作(可能需要几天甚至几周),然后当他们准备好录入的数据时,应用程序就应该能够将这些数据同步回主数据库...…

    你认为 HTML5 和 localForage 已经成熟到可以处理这种复杂且数据量大的离线数据了吗?

    谢谢,此致
    Sumeet

    2014 年 2 月 14 日 上午 05:11

    1. tofumatt

      localForage 相当稳定,并且有大量测试,所以我认为你应该没问题。当然,像这样的开源软件实际上没有提供任何担保,但我认为你应该没问题。

      2014 年 2 月 14 日 下午 11:22

  13. Glintch

    太好了!正是我要找的东西。但当使用 localStorage 时,存储大小仍然限制在 5MB 吗?

    2014 年 2 月 14 日 上午 06:00

    1. tofumatt

      的确如此,虽然我们对此无能为力。幸运的是,大多数浏览器首先会使用非 localStorage 驱动程序,所以你基本是安全的!

      2014 年 2 月 14 日 下午 11:20

      1. Glintch

        很高兴知道。谢谢。

        2014 年 2 月 14 日 下午 12:26

  14. Sam

    看来 localStorage 从一开始就没有设计/实现好,因为它很慢(尤其是在 [Android] 平板电脑上)并且会阻塞。然而,API 比 IndexedDB 更简洁,所以他们在这一点上做得很好。

    2014 年 2 月 14 日 下午 10:34

  15. Agustin Lopez

    一个问题... 我刚刚下载了它,并试用了它,但注意到 Safari 弹出一个消息,询问是否允许网站使用 10MB。

    这发生在该库中使用的默认 DB_SIZE ...

    var DB_SIZE=5*1024*1024;

    但如果我手动将其设置为更小的尺寸,就不会发生这种情况...

    var DB_SIZE=4.5*1024*1024;

    这是正确的吗?我不想让用户看到询问此权限的弹出窗口,我认为 5MB 可能是 Safari 用于询问权限的限制。

    2014 年 2 月 16 日 上午 09:04

    1. tofumatt

      看起来 5MB 就是限制,是的,而略小于这个限制是可以的。我今天会推送一个更改来修复这个错误。

      2014 年 2 月 16 日 上午 10:01

  16. Agustin Lopez

    还有一个问题...

    如何从存储中删除项目?我们必须将其设置为 null 还是有类似 ...

    localforage.deleteItem 或类似方法?

    提前感谢。

    2014 年 2 月 16 日 下午 13:44

  17. Simon

    加油!Matt,我正在研究构建第一个希望成为许多移动端的离线应用程序,这些应用程序使用的是浏览器而不是原生应用程序,而且我刚刚发现移动 Safari 与其他浏览器世界的差异 :) 所以任何像这样能够平衡浏览器竞争环境的东西都非常棒 !!!!!

    2014 年 2 月 17 日 上午 02:21

  18. Andrea Giammarchi

    感谢你创建了我 2012 年 6 月提出的、并在当时发布在 GitHub 上的方案: https://github.com/WebReflection/db#asyncstorage–a-developer-friendly-asynchronous-storage 我相信现在 Mozilla 已经谈论过它了,开发者会更多地使用它。

    我还想知道你是否也计划像我的旧解决方案一样使 API 避免冲突,这样 library-A 执行的 `storage.clear()` 不会破坏可能来自 library-B 的 50 MB 数据。

    在我的情况下,我使用了存储名称作为数据库操作的内部命名空间,因此命名空间数据库的创建也将是异步的(或者在你的情况下,是基于承诺的)。

    此致

    2014 年 2 月 18 日 下午 14:23

  19. Dheeraj

    有没有简单的方法可以插入第三方驱动程序?Lawnchair (http://brian.io/lawnchair/) 允许使用适配器。这在混合移动应用程序中使用 PhoneGap 插件时是必需的。

    2014 年 2 月 19 日 下午 23:18

本文评论已关闭。