localForage:管理脱机浏览器存储

2020年12月30日11:17:05 发表评论 34 次浏览

本文概述

使用以下命令缓存基于静态文件的资源缓存API因此, 大多数人认为渐进式Web应用程序(PWA)就是要解决这些静态页面而无需连接互联网的情况。但是, PWA可以通过浏览器存储实现更多功能。借助浏览器存储, 我们可以构建功能齐全的交互式脱机应用程序, 这些应用程序可以:

  • 当互联网连接不畅或没有连接时, 将用户输入(例如向日历Web应用程序添加新任务)存储在浏览器中, 然后在检测到连接后立即同步到远程数据库。这样, 即使用户从其他设备登录, 也将提供最新数据
  • 存储API响应数据, 以便更快地进行初始加载, 然后按照过时的重新验证策略, 在恢复连接时进行任何更改时, 稍后进行更新
  • 在服务器端存储很少更改的API数据, 因此应用程序不必始终对这些数据进行服务器调用

针对此类需求, 浏览器提供了两种主要的存储机制, 它们是持久性的并且可以脱机访问:localStorage和IndexedDB。

但是, 这两个有两个缺点。

使用localStorage作为离线存储机制

一方面, 通过简单的获取和设置API, localStorage非常易于使用。但是, 它只能存储字符串类型的数据。这意味着必须将其他数据类型转换为带有JSON.stringify()当存储, 然后转换回JSON.parse()从存储中读取时, 这不是很安全。另外, localStorage提供的存储空间非常有限, 大约为5MB, 是同步的, 无法从Web Worker访问, 因此不支持后台同步。

使用IndexedDB作为脱机存储机制

另一方面, IndexedDB几乎正是我们所需要的。它是异步的, 因此不会阻塞主线程。它接受不同的数据类型, 包括Blob, 文件和图像, 根据用户的磁盘空间和操作系统的不同, 其存储上限有时会高达1GB, 并且可以由Web Worker访问。

但是, 使用本机indexedDB API完全是一场噩梦。有时, 你只需要编写10行以上的代码即可存储数据。

什么是本地饲料?

这是哪里本地饲料(如果你不熟悉离线存储的概念, 建议你阅读这个在继续之前)。

用其创建者的话来说, localForage已对离线存储进行了改进。通过方便地在其上提供抽象层, 它使使用脱机存储更加容易。它结合了indexedDB的灵活性和简单的类似于localStorage的异步API。这意味着我们必须使用大家都喜欢的async-await语法。

localForage旨在将数据存储在indexedDB中, 如果不支持indexedDB, 则退回到localStorage。 (尽管这样做可能会产生相当大的性能和存储方面的副作用, 因为所有数据都将在保存时进行序列化, 并且只有可以通过以下方式转换为字符串的数据:JSON.stringify()将被保存)。

因为indexedDB当前是所有主要浏览器均支持, 并且, 如果你要构建PWA, 则较旧的浏览器将不支持你需要使用的大多数功能, 因此你可以直接决定仅支持现代的浏览器。

我们为制作了一个自定义演示.
不完全是。点击这里查看.

localForage:管理脱机浏览器存储1

在我目前的公司中, 我们仅为我们的PWA产品支持Chrome, PWA产品是适用于小型企业的一体化SaaS平台。我们使用localForage来存储无论互联网连接如何都应该可用的数据, 因为该应用程序主要供销售代理商使用, 他们有时可能会在互联网连接不良的非常规场所使用。

在本文的其余部分, 我将指导你完成如何设置localForage并对其执行基本CRUD操作的方式, 就像处理其他任何数据库一样。我们将编写功能来构建假设的CRM销售应用程序的一部分, 以供销售代表整理潜在的客户联系详细信息(通常是在可能存在连接问题的地方)。让我们开始吧。

1.设置HTML页面

首先, 我们建立一个简单的HTML页面, 其中包含一个用于收集客户数据的表格和一个显示所有客户详细信息的表格:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LocalForage Demo</title>

</head>
<body>
    <div>
       <label>Client Name:
           <input id="clientName" type="text" name='clientName' value="">
       </label>
       <label>Phone Number:
           <input id="clientPhone" type="number" name='clientPhone' value="">
       </label>
       <label>Needs:
           <input id="clientNeed" type="text" name='clientNeed' value="">
       </label>
       <button type="submit" id="submit">Save</button>
    </div>

    <div>
        <table>
            <thead>
                <tr>
                    <th>Client Name</th>
                    <th>Phone Number</th>
                    <th>Needs</th>
                    <th>Actions</th>
                    <th>Actions</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>

            </tbody>
        </table>
    </div>

   <script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.9.0/localforage.min.js"></script>
   <script src="index.js"></script>
</body>
</html>

为了使本教程框架和工具不可知, 我们将通过CDN链接使用localForage, 但你可以选择通过npm或yarn安装它。

接下来, 让我们对离线数据库进行一些配置:

//in index.js
const ContactTable = localforage.createInstance({
    name: "CRMApp", storeName: "ContactTable"
});

在这里, 我们正在使用localForagecreateInstance创建新数据库的方法(客户关系管理), 还有一家商店(联系表)用于我们的应用。 localForage中的存储就像indexedDB中的对象存储一样, 可以将其视为数据库中的单个表。

对于要存储的不同类别的数据, 最好有不同的存储。如果你不这样做, 默认情况下, localForage将建立一个名为" localforage"的数据库和一个名为" keyvaluepairs"的存储, 它将为你存储所有数据。

2.添加新客户

现在我们已经完成了基本设置, 让我们开始编写CRUD函数。例如, 当销售代表在输入中输入新的客户详细信息并单击保存按钮时, 我们需要从所有输入中获取值并将其结构化为对象。

const getClientDetails = () => {
    const clientName = document.getElementById("clientName").value;
    const clientPhone = document.getElementById("clientPhone").value;
    const clientNeed = document.getElementById("clientNeed").value;

    return {
        clientName: clientName, clientPhone: clientPhone, clientNeed: clientNeed, }
}

然后, 我们需要将新的客户详细信息对象存储在浏览器存储中, 并将其显示在表上:

const addInput = async () => {
    const inputValues = getClientDetails();
    const dbLength = await ContactTable.length();
    let id = dbLength === 0 ? 1 : dbLength + 1;
    try{
        let row = `<tr id="${id}">
                        <td>${inputValues.clientName}</td>
                        <td>${inputValues.clientPhone}</td>
                        <td>${inputValues.clientNeed}</td>
                        <td><button class="edit">Edit</button></td>
                        <td><button class="delete"> Delete</button></td>
                    </tr>`
        document.querySelector('tbody').insertAdjacentHTML('afterbegin', row);
        await ContactTable.setItem(id, inputValues);
        alert('contact added successfully');
    }catch(e){
        console.log(err.message);
    }}
document.getElementById('submit').addEventListener('click', async(e) => {
    e.preventDefault();
    await addInput();
})

随着addInput功能, 当点击保存按钮时, 我们称为getClientDetails函数来获取客户详细信息对象, 然后使用ES6模板文字将新的包含这些详细信息的行添加到HTML表格的顶部。

我们也在使用localForage的长度生成唯一ID的方法, 该ID将用于检索单个条目。最后, 我们使用setItem将客户详细信息对象添加到的方法contactTable商店。

你可以打开chrome开发工具的应用程序窗格以查看其运行情况:

Chrome开发者工具页面

3.加载所有客户数据

接下来, 每当刷新, 重新加载或在另一个选项卡上打开应用程序时, 我们都需要能够读取所有客户详细信息数据并将其显示在表上:

const loadContactsFromStorage = async () => {
    try {
        await ContactTable.iterate((value, key, iterationNumber) => {
            let newContact = `<tr id="${key}">
                                <td>${value.clientName}</td>
                                <td>${value.clientPhone}</td>
                                <td>${value.clientNeed}</td>
                                <td><button class="edit">Edit</button></td>
                                <td><button class="delete"> Delete</button></td>
                                <td><button class="view">View</button></td>
                           </tr>`
            document.querySelector('tbody').insertAdjacentHTML('afterbegin', newContact);
        })
    } catch (err) {
        console.log(err)
    }

}

window.addEventListener('load', async() => {
    await loadContactsFromStorage();
})

在这里我们使用localForage重复遍历脱机数据库中所有条目的方法。的重复方法采用一个回调函数, 该函数每次迭代都调用, 就像ES6地图功能。

该回调函数将迭代和迭代编号中当前数据的值和键作为参数接收。我们使用模板文字将每个对象键的值添加到表中。之后, 我们监听load事件并调用loadContactsFromStorage.

这是一个使用length属性检查存储中是否存在数据的好地方(如果用户通过新设备访问应用程序), 然后尝试从远程数据库加载数据并将其存储在浏览器存储中以供脱机访问。

4.删除客户

下一个功能是使销售代表能够从表格中删除客户的联系方式。这意味着单击删除按钮时, 我们需要从应用程序界面以及离线数据库中删除联系人。

const deleteContact = async(e) => {
    const row = e.target.parentElement.parentElement;
    const key = row.id;
    row.remove();
    try{
        await ContactTable.removeItem(key);
        alert('Contact deleted successfully');
    }catch(err){
        console.log(err.message);
    }

}

window.addEventListener('load', async() => {
     await loadContactsFromStorage();
     document.querySelectorAll('.delete').forEach(button =>{
        button.addEventListener('click', async(e) => {
          await deleteContact(e);
        })
    });
  })

在这里, 我们使用的是HTMLparentElement属性以获取包含单击的删除按钮的行。然后, 我们使用HTML去掉从UI中删除该行的方法。

要从离线存储中删除, 我们使用localForage除去项目方法, 将目标行的ID作为键传递。我们还将点击事件监听器附加到加载事件下所有加载按钮的下方, loadContactFromStorage功能, 因为必须先将行加载到DOM中, 然后才能附加事件监听器。

5.更新客户

客户的电话号码或需求很可能会改变。我们希望销售代表能够编辑和更新客户的详细信息, 即使他们处于离线状态也是如此。为此, 单击编辑按钮时, 我们需要显示一个模式, 该模式将具有一个表单, 该表单的输入包含目标个人客户详细信息和一个更新按钮。单击模式中的更新按钮后, 我们需要在脱机数据库以及表中更新目标客户详细信息条目。

首先, 向HTML添加模式:

<div id="modal">
        <div class="backdrop">
            <div class="form">
                <label><span>Client Name:</span>
                    <input id="editName" type="text" name='editName' value="">
                </label>
                <label><span>Phone Number:</span>
                    <input id="editPhone" type="number" name='editPhone' value="">
                </label>
                <label><span>Needs:</span>
                    <input id="editNeed" type="text" name='editNeed' value="">
                </label>
                <button type="submit" id="update">Update</button>
            </div>
        </div>

    </div>

我们还添加一些CSS来设置模式样式并在初始加载时将其隐藏:

css

#modal{
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  display: none;
}

#modal .backdrop{
    background-color: rgba(43, 40, 40, 0.5);
    width: 100vw;
    height: 100vh;
}

#modal .form{
    background-color: #fff;
    padding: 40px;
    border: 1px solid blue;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 1;
}

现在, 让我们编写一个函数来处理更新任务的第一部分, 即单击编辑按钮时检索信息:

let updateKey;

const viewContact = async(e) => {
    updateKey =  e.target.parentElement.parentElement.id;

    const editName = document.getElementById("editName");
    const editPhone = document.getElementById("editPhone");
    const editNeed = document.getElementById("editNeed");

    try{
        const contact = await ContactTable.getItem(updateKey);

        editName.value = contact.clientName;
        editPhone.value = contact.clientPhone;
        editNeed.value = contact.clientNeed;

        document.getElementById("modal").style.display= "block";

    }catch(err){
        console.log(err.message);
    }

}

window.addEventListener('load', async() => {
    await loadContactsFromStorage();
    ....
    document.querySelectorAll('.edit').forEach(button =>{
        button.addEventListener('click', async(e) => {
          await viewContact(e);
        })
    })
})

在这里, 我们创建一个全局变量:updateKey因为我们将在多种功能中使用它。的viewContact函数获取包含单击按钮的表行的ID。

然后, 它使用localForagegetItem从单个键值对条目检索值的方法, 该条目与脱机数据库中提供的键匹配。然后, 它将返回的值显示为模态输入的值, 并将模态显示设置为块。

出于与上一步相同的原因, 我们还将点击事件监听器附加到了加载事件中的所有编辑按钮上。

接下来, 我们需要编写一个函数来处理更新任务的第二部分, 该任务在单击模式上的更新按钮时发生:

const updateContact = async() => {

    try{
        const updatedClient ={
            clientName: document.getElementById("editName").value, clientPhone: document.getElementById("editPhone").value, clientNeed: document.getElementById("editNeed").value, }

        let updatedRow = `<tr id="${updateKey}">
                        <td>${updatedClient.clientName}</td>
                        <td>${updatedClient.clientPhone}</td>
                        <td>${updatedClient.clientNeed}</td>
                        <td><button class="edit">Edit</button></td>
                        <td><button class="delete"> Delete</button></td>
                    </tr>`

        document.getElementById(`${updateKey}`).remove();
        document.querySelector('tbody').insertAdjacentHTML('afterbegin', updatedRow);

        await ContactTable.setItem(updateKey, updatedClient);

        document.getElementById("modal").style.display= "none";

        alert('Contact updated successfully');
    }catch(err){
        console.log(err.message);
    }
}


document.getElementById('update').addEventListener('click', async(e) => {
    e.preventDefault();
    await updateContact();
})

的updateContact函数从模态输入中获取更新的客户详细信息, 然后使用模板文字用这些详细信息形成HTML表格行。然后, 在从表中删除具有先前数据的行后, 将创建的行追加到表的开头。

ew!拍拍自己的背部。你已经使用localForage成功构建了功能齐全的离线CRUD应用。

我们的代码中有一些重复, 以帮助理解。你可以尝试将其干燥。另外, 你可以在此查看完整的代码库github仓库并观看现场演示这里.

下一步是实现后台同步到存储流中。根据你决定使用的远程数据库, 你的处理方式将有所不同。不用担心, 我在下面添加了一些文章来帮助你入门。

如果你希望看到一个演示项目, 并且将此处讨论的所有概念付诸实践, 请查看此内容。费用和收入跟踪器PWA我之前用indexedDB构建的。它完全脱机工作。

看看controller.js文件, 第56行和sw.js(第98行), 了解如何实现与远程Firebase数据库的后台同步。

总结

在本文中, 我向你介绍了浏览器离线存储的强大功能和令人难以置信的实用性, 以及localForage库如何使它们的使用和管理变得更加容易。我还向你展示了如何使用localForage在浏览器存储上执行基本的CRUD功能。现在, 用这些新获得的知识来构建一些很棒的东西!

一盏木

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: