65.9K
CodeProject 正在变化。 阅读更多。
Home

如何用indexedDB做一些神奇的事情

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (17投票s)

2014年3月26日

Apache

12分钟阅读

viewsIcon

91462

downloadIcon

565

探索indexedDB的隐藏功能!

引言

使用纯粹的 IndexedDB API,您可以实现不区分大小写的搜索、逻辑 OR 操作、键值集匹配等等。本文将展示如何访问人们不知道其存在的 IndexedDB 隐藏功能!在我编写一个名为 Dexie.js 的 IndexedDB 库时,我发现了 IDBCursor 的隐藏功能,因此我想与公众分享我的算法和发现。我将这些算法称为“Dexie 算法”,本文揭示了它们的工作原理。文章末尾有一个完整的 HTML 片段,您可以复制并粘贴到浏览器中进行测试。

背景

如果您需要在浏览器中本地存储可索引的数据库,IndexedDB 是最具前瞻性的 API。W3C 已放弃 WebSQL 标准化工作,转而支持 IndexedDB。好消息是,您已经可以在所有现代浏览器中使用它:IE10+、Opera、Firefox、Chrome、Android 和 IE Mobile,并且在不同实现之间非常稳定。您甚至可以通过 indexedDB polyfill 在 Safari 桌面版和 iOS Safari 中使用它,该 polyfill 使用 WebSQL 来实现标准。大约一年前(2013 年),微软积极向所有 Windows 7 和 Windows 8 客户端推送了 IE11。因此,如果您决定基于 IndexedDB 构建应用程序,只会错过那些少数使用 Windows XP 的用户。总而言之:IndexedDB 可以在任何现代浏览器上运行,并且我们可以很快假设它存在于任何客户端上。

IndexedDB 的优势

IndexedDB 是一个具有完整事务支持、读/写锁定和索引的 NoSQL 数据库。其事务支持使其在所有 Web 用例中都能稳定工作,例如页面重新加载、多个标签页打开、加载具有更新模式的新版 Web 应用等……

表被称为对象存储(Object Stores),其中包含 JavaScript 对象而不是记录。在每个对象存储中,您可以存放 JavaScript 对象,而无需指定它们可能具有的成员(列名)。如果您想按主键以外的其他键来搜索对象,则需要指定索引。使用索引,您可以按特定键(如 firstName、lastName 等)查询对象。

由于其简单的索引 API,浏览器供应商可以“从头开始”实现它,而无需使用现有的 SQL 库(如 SQLite)来满足标准。

事务、数据库版本控制和初始化是 IndexedDB 最令人印象深刻的健壮性所在——它经过深思熟虑和精心设计。初始设置表、填充初始数据以及在版本之间升级是 IndexedDB 的核心内容。如果没有这种深思熟虑的架构,这些对于 Web 应用开发者来说可能会很麻烦。通过要求 Web 应用开发者从一开始就考虑升级,它不仅能让应用程序存储客户端数据,还能确保应用程序在将来需要添加表或索引,或更改数据架构时仍能正常运行。

IndexedDB 的局限性

与 SQL 数据库相比,存在许多局限性(没有外键、存储过程、视图等),但最重要的是查询支持有限。

IndexedDB 开箱即用只能执行以下搜索操作

  1. IDBKeyRange.only() - 精确匹配(区分大小写!)
  2. IDBKeyRange.bound() - 查找键在给定范围内的所有对象
  3. IDBKeyRange.upperBound() - 查找键小于给定键的所有对象
  4. IDBKeyRange.lowerBound() - 查找键大于给定键的所有对象

如上所示,似乎没有办法执行以下操作:

  • 不区分大小写的搜索
  • 查找键为("a", "b", "c" 或 "d")中任意一个的所有对象(SQL 'IN')
  • 查找包含给定子字符串的键(SQL LIKE)
  • 逻辑 OR 或 AND
  • 等等……(列表可能很长……)

让 IndexedDB 强大的技巧

我将展示的技巧都不需要您存储元数据或挂钩到数据库操作。它们可以与任何已使用的 IndexedDB 数据库一起工作。例如,您无需存储字符串的小写版本即可进行不区分大小写的匹配。

那么,让我们开始展示 Dexie.js 所做的技巧以及我的实现方式吧。

支持键匹配任意 [a, b 或 c](SQL IN (a,b,c))

一点背景:要遍历表中的所有记录,您可以调用 IDBObjectStore 或 IDBIndex 实例上的 openCursor()。对于每条记录,您都会通过 onsuccess 回调函数获得通知。

// Start a transaction to operate on your 'friends' table
var transaction = db.transaction(["friends"]); // (db is an instance of IDBDatabase)
 
// Get the 'name' index from your 'friends' table.
var index = trans.objectStore("friends").index("name");
 
// Start iteration by opening a cursor
var cursorReq = index.openCursor();
 
// When any records is found, you's get notified by the 'success' event
req.onsuccess = function (e) {
    var cursor = e.target.result;
    if (cursor) {
        // We have a record in cursor.value
        console.log(JSON.stringify(cursor.value));
        cursor.continue();
    } else {
        // Iteration complete
    }
};

您可以指定要前进到的下一个键的值,而不是仅仅调用 cursor.continue()。所以如果我们写

 cursor.continue("David"); 

...下一个 onsuccess 将会有一个光标定位在第一个键值为“David”的记录上,如果该键存在的话。规范要求光标必须定位在排序顺序上与索引相同、等于或大于指定键的第一条记录上

假设我们要查找所有名字为“David”、“Daniel”或“Zlatan”的朋友。我们要做的第一件事就是对我们的集合进行排序(sort()),得到 ["Daniel", "David", "Zlatan"](n 在 v 之前)。然后我们这样做:

  1. 调用 cursor.continue("Daniel")
  2. onsuccess:检查 cursor.key == "Daniel"。如果是,则将 cursor.value 包含在结果中,并调用 cursor.continue()(不带参数)以查找更多 David。
  3. 调用 cursor.continue("David")
  4. onsuccess:检查 cursor.key == "David"。如果是,则将 cursor.value 包含在结果中,并调用 cursor.continue()(不带参数)以查找更多 David。
  5. 调用 cursor.continue("Zlatan")
  6. onsuccess:检查 cursor.key == "Zlatan"。如果是,则将 cursor.value 包含在结果中,并调用 cursor.continue()(不带参数)以查找更多 Zlatan。否则,我们可以通过不调用 cursor.continue() 来停止迭代,因为我们知道不会找到更多结果。

算法如下:

function equalsAnyOf(keysToFind, onfound, onfinish) {
    var set = keysToFind.sort();
    var i = 0;
    var cursorReq = index.openCursor(); // Assume 'index' exists in a parent closure scope
    cursorReq.onsuccess = function (event) {
        var cursor = event.target.result;
        if (!cursor) { onfinish(); return; }
        var key = cursor.key;
        while (key > set[i]) {
            // The cursor has passed beyond this key. Check next.
            ++i;
            if (i === set.length) {
                // There is no next. Stop searching.
                onfinish();
                return;
            }
        }
        if (key === set[i]) {
            // The current cursor value should be included and we should continue
            // a single step in case next item has the same key or possibly our
            // next key in set.
            onfound(cursor.value);
            cursor.continue();
        } else {
            // cursor.key not yet at set[i]. Forward cursor to the next key to hunt for.
            cursor.continue(set[i]);
        }
    };
    return c;
}

不区分大小写的搜索

不区分大小写的搜索是许多人期望数据库支持的另一项功能。人们对 IndexedDB 存在普遍的误解,他们认为需要将字符串小写存储两次才能进行不区分大小写的搜索。原因如下:

Dexie.js 使用类似于上面所示的 SQL 'IN' 算法的 IDBCursor.continue(key) 方法来实现不区分大小写的搜索,但算法需要更多的复杂性。

假设我们需要在“friends”表中不区分大小写地查找“david”。“David”或“david”都应该被找到。显然,我们可以创建一个包含“david”所有可能的组合格式的数组,然后使用上面的 SQL 'IN' 算法。然而,组合的数量会随着我们要查找的键的长度呈指数级增长。但我们可以使用一个技巧;由于 cursor.continue() 会定位到排序顺序中的下一条记录,这会揭示我们可以跳过哪些组合,当我们定位到一个不匹配的键时。

我们所做的是搜索“David”可能出现的最低值,即“DAVID”,因为大写 Unicode 字符的值小于其对应的小写版本。如果数据库中没有“DAVID”,我们将定位到最小但大于“DAVID”的键。现在,而不是测试下一个 David 的组合(这将是“DAVId”),我们首先检查我们定位到的键,并从该键派生出下一个要搜索的“david”变体。

这是 Dexie 的算法:

function findIgnoreCaseAlgorithm(needle, onfound, onfinish) {
    // needle: The string to search for
    // onfound: A function to call for each found item
    // onfinsish: A function to call when we're finshed searching.
    var upperNeedle = needle.toUpperCase();
    var lowerNeedle = needle.toLowerCase();
    var cursorReq = index.openCursor(); // 'index' (IDBIndex) must exist in closure scope
    cursorReq.onsuccess = function (event) {
        var cursor = event.target.result;
        if (!cursor) {
            // No more data to iterate over - call onfinish()
            onfinish();
            return;
        }
        var key = cursor.key;
        if (typeof (key) !== 'string') {
            // Just in case we stumble on data that isnt what we expect -
            // toLowerCase() wont work on this object. Check next.
            cursor.continue();
            return;
        }
        var lowerKey = key.toLowerCase();
        if (lowerKey === lowerNeedle) {
            onfound(cursor.value); // Notify caller we found somethin
            cursor.continue(); // Check next record, it might match too!
        } else {
            // Derive least possible casing to appear after key in sort order
            var nextNeedle = nextCasing(key, lowerKey, upperNeedle, lowerNeedle);
            if (nextNeedle) {
                cursor.continue(nextNeedle);
            } else {
                // No more possible case combinations to look for.
                // Call onfinish() and dont call cursor.continue() anymore.
                onfinish();
            }
        }
    };
 
    function nextCasing(key, lowerKey) {
        var length = Math.min(key.length, lowerNeedle.length);
        var llp = -1; // "llp = least lowerable position"

        // Iterate through the most common first chars for cursor.key and needle.
        for (var i = 0; i < length; ++i) {
            var lwrKeyChar = lowerKey[i];
            if (lwrKeyChar !== lowerNeedle[i]) {
                // The char at position i differs between the found key and needle being
                // looked for when just doing case insensitive match.
                // Now check how they differ and how to trace next casing from this:
                if (key[i] < upperNeedle[i]) {
                    // We could just append the UPPER version of the key we're looking for
                    // since found key is less than that.
                    return key.substr(0, i) + upperNeedle[i] + upperNeedle.substr(i + 1);
                }
                if (key[i] < lowerNeedle[i]) {
                    // Found key is between lower and upper version. Lets first append a
                    // lowercase char and the rest as uppercase.
                    return key.substr(0, i) + lowerNeedle[i] + upperNeedle.substr(i + 1);
                }
                if (llp >= 0) {
                    // Found key is beyond this key. Need to rewind to last lowerable
                    // position and return key + 1 lowercase char + uppercase rest.
                    return key.substr(0, llp) + lowerKey[llp] + upperNeedle.substr(llp + 1)
                }
                // There are no lowerable positions - all chars are already lowercase
                // (or non-lowerable chars such as space, periods etc)
                return null;
            }
            if (key[i] < lwrKeyChar) {
                // Making lowercase of this char would make it appear after key.
                // Therefore set llp = i.</span>
                llp = i; 
        }
        // All first common chars of found key and the key we're looking for are equal
        // when ignoring case.
        if (length < lowerNeedle.length) {
            // key was shorter than needle, meaning that we may look for key + UPPERCASE
            // version of the rest of needle.
            return key + upperNeedle.substr(key.length);
        }
        // Found key was longer than the key we're looking for
        if (llp < 0) {
            // ...and there is no way to make key we're looking for appear after found key.
            return null;
        } else {
            // There is a position of a char, that if we make that char lowercase,
            // needle will become greater than found key.
            return key.substr(0, llp) + lowerNeedle[llp] + upperNeedle.substr(llp + 1);
        }
    }
}
 

搜索以X开头的字符串,不区分大小写。

在 Dexie 中,不区分大小写的 LIKE '%str' 查询使用以下直接语法编写:

 db.friends.where("name").startsWithIgnoreCase("da")  

结果是一个 Collection,当使用 toArray() 或 each() 方法执行时,它将返回“David”和“Daniel”对象。

实现方式如下:

  • 执行与上面相同的 findIgnoreCaseAlgorithm,但不要检查 (lowerKey === lowerNeedle) {...},而是执行 if (lowerKey.indexOf(lowerNeedle) == 0) { ... }
 cursorReq.onsuccess = function (event) {
        var cursor = event.target.result;
        if (!cursor) {
            // No more data to iterate over - call onfinish()
            onfinish();
            return;
        }
        var key = cursor.key;
        if (typeof (key) !== 'string') {
            // Just in case we stumble on data that isnt what we expect -
            // toLowerCase() wont work on this object. Check next.
            cursor.continue();
            return;
        }
        var lowerKey = key.toLowerCase();
        if (lowerKey.indexOf(lowerNeedle) === 0) {
            onfound(cursor.value); // Notify caller we found somethin
            cursor.continue(); // Check next record, it might match too!
        } else {
            // Derive least possible casing to appear after key in sort order
            var nextNeedle = nextCasing(key, lowerKey, upperNeedle, lowerNeedle);
            if (nextNeedle) {
                cursor.continue(nextNeedle);
            } else {
                // No more possible case combinations to look for.
                // Call onfinish() and dont call cursor.continue() anymore.
                onfinish();
            }
        }
    }; 

注意:上面的代码片段只是算法的一部分。请参阅不区分大小写的搜索算法,其中包含了其余内容。

逻辑或

IndexedDB 不支持逻辑 OR。您一次只能指定一个键范围。但是,它确实支持并行运行多个查询——即使在使用同一事务时(只要查询是只读查询)。即使查询不并行运行,它也能工作,但性能会稍差。Dexie.js 中的 OR 操作已通过 Chrome、IE、Firefox 和 Opera 进行单元测试。

除了并行执行两个查询之外,我们只需要做的是去除重复项。为此,我们可以使用基于闭包的已找到主键集。每当任一并行查询匹配到一个新记录时,它首先检查该集合是否已包含。如果未包含,则调用 onfound 处理该条目,并将 set[primKey] = true,这样如果另一查询找到相同的条目,它将静默地忽略调用 onfound()。

要使用 Dexie.js 访问此算法,您可以键入以下内容:

 db.friends.where('name').equalsIgnoreCase('david').or('shoeSize').above(40).toArray(fn) 

实现方式如下。下面的代码片段是一个简化版本,仅支持两个标准 IDBKeyRange 查询的逻辑 OR。使用 Dexie,您可以进行任意数量的 OR 操作,以及任何标准或扩展操作,如 equalsIgnoreCase()。

function logical_or(index1, keyRange1, index2, keyRange2, onfound, onfinish) {
    var openCursorRequest1 = index1.openCursor(keyRange1);
    var openCursorRequest2 = index2.openCursor(keyRange2);
 
    assert(index1.objectStore === index2.objectStore); // OR can only be done on same store
    var primKey = index1.objectStore.keyPath;
    
    var set = {};
    var resolved = 0;
 
    function complete() {
        if (++resolved === 2) onfinish();
    }
 
    function union(item) {
        var key = JSON.stringify(item[primKey]);
        if (!set.hasOwnProperty(key)) {
            set[key] = true;
            onfound(item);
        }
    }
 
    openCursorRequest1.onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor) {
            union(cursor.value);
        } else {
            complete();
        }
    }
 
    openCursorRequest2.onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor) {
            union(cursor.value);
        } else {
            complete();
        }
    }
}

当使用并行 OR 执行时,排序顺序将无效。部分原因是两个不同的查询在具有不同排序顺序的不同索引上执行,但主要原因是这两个查询由浏览器后台线程并行运行,我们无法知道哪个 onsuccess 比另一个先调用。然而,这可以通过实现 onfound 将项推送到数组,并在 onfinish 时使用任何请求的排序顺序对其进行排序来解决。

 var index1 = transaction.objectStore("friends").index("name");
 var index2 = transaction.objectStire("friends").index("shoeSize");
 var keyRange1 = IDBKeyRange.bound ("Da", "Da\uffff");
 var keyRange2 = IDBKeyRange.lowerBound (40, true);
 //
 // SELECT * FROM friends WHERE name LIKE 'Da%' OR shoeSize > 40 ORDERBY shoeSize;
 //
 var result = [];
 logical_or (index1, keyRange1, index2, keyRange2, function (friend) {
     result.push(friend);
 }, function () {
     result.sort (function (a,b) { return a.shoeSize - b.shoeSize; });
 }); 

逻辑与

逻辑 AND 部分可以通过使用复合索引在 IndexedDB 中实现。与许多其他数据库一样,IndexedDB 支持创建不仅仅是“name”或“shoeSize”的索引,而是“name”和“shoeSize”的组合。

store.createIndex('nameAndShoeSize', ['name', 'shoeSize']);  

此索引首先按名称排序,如果两个记录具有相同的名称,则第二个排序顺序为鞋码。使用此类索引时,键将被视为两个复合键的数组。通过使用 IDBKeyRange.only(['David', 40]),您可以查找 name 为 'David' AND shoeSize 为 40 的记录。但是,它不像通用的 AND,它只是提供了在精确匹配时进行 AND 的可能性。您不能执行WHERE name = 'David' AND shoeSize > 40。可以说,它的行为就像您创建了一个新字段,将“name”和“shoeSize”连接成一个字符串。另一个缺点是并非所有浏览器都支持复合索引——特别是 IE10/IE11。

除了通过 JS 过滤器应用于迭代之外,没有其他技巧可以 emulated 一个真正的通用逻辑 AND。这是您能做的最好的。并且只要选择一个好的第一手过滤器,您就可以对迭代应用手动过滤器,这并不过分。请记住,使用 IndexedDB 时,数据库与客户端运行在同一台机器上,因此没有网络消耗需要优化。这样做的优点是,您可以拥有编写复杂过滤器——任何 JS 中可能的表达式——的所有能力。只需记住选择一个好的第一手索引来过滤掉大部分结果。

考虑以下 SQL 查询:

SELECT * FROM friends WHERE name='David' AND shoeSize > 40 

让数据库过滤掉 name='David',然后由我们的 JavaScript 过滤器处理 (shoeSize > 40) 会更明智,因为名字为 'David' 的朋友数量很可能少于鞋码大于 40 的朋友数量。

这是使用 Dexie.js 的一个示例(包括引导数据库):

var db = new Dexie("MyDB");
db.version(1).stores({friends: "++id,name,shoeSize"});
db.open(); 
db.friends.where('name')
          .equalsIgnoreCase('david')
          .and(function(friend) { return friend.shoeSize > 40; })
          .toArray (function (friendsArray) {
              console.log(JSON.stringify(friendsArray));
          }); 

……或者,如果您更愿意使用复合索引,您可以在 Dexie.js 中这样做:

var db = new Dexie("MyDB");
db.version(1).stores({friends: "++id,name,shoeSize,[name+shoeSize]"});
db.open();
db.friends.where('[name+shoeSize]')
          .equals(['David',43])
          .toArray (function (friendsArray) {
              console.log(JSON.stringify(friendsArray));
          });  

但是,如果您需要针对 IE,请不要使用它们——IE10 或 IE11 都不支持。

搜索以X开头的字符串(SQL LIKE 'prefix%')

匹配字符串键的前缀是使用 IndexedDB 可以做的最直接的技巧。它并非 Dexie.js 所独有,其他库也支持它。但是,为了完整起见,这里是实现方法:只需使用 IDBKeyRange.bound,其中 lowerBound 是前缀,upperBound 是一个包含前缀所有可能后缀的字符串。最简单的方法是附加一个具有最高可能值的字符:'\uffff'。

IDBKeyRange.bound(prefix, prefix + '\uffff', false, false)  

更多技巧...

受本文中算法的启发,还可以执行 Dexie.js 支持的其他查询,例如:

  • startsWithAnyOf([str1, str2, strN...])
  • SQL IN 忽略大小写
  • betweenAnyOfRanges([from, to], [from2, to2], ...)
  • ...

使用代码

本文中的代码片段仅用于概念演示。文章的目的是只展示 Dexie.js 中使用的概念和算法的简化版本。

在您自己的浏览器中测试!

要查看算法的实际效果,只需将以下片段粘贴到一个空的 HTML 文件中,并下载 Dexie.js。如果使用 IE,请确保在 HTTP 或 HTTPS URL 上运行该页面。如果使用 Safari,请下载 indexedDB polyfill 并将其包含在 HTML 中。请注意,在使用 Safari polyfill 时,OR 操作无法按预期工作(这是 polyfill 中需要修复的问题)。

<!DOCTYPE html>
<html>
<head>
    <title>Extended indexedDB queries using Dexie.js</title>
    <script type="text/javascript" src="Dexie.js"></script>
    <script>
        //
        // App global database instance and schema
        //
        var db = new Dexie("MyDB");
        db.version(1).stores({
            friends: "++id,name,shoeSize"
        });
        db.open().catch(function (e) {
            log("Error opening database: " + e, "error");
        });
 
        //
        // Populate some data
        //
        function populateSomeData() {
            log("Populating some data", "heading");
            return db.transaction("rw", db.friends, function (friends) {
                friends.clear();
                friends.add({ name: "David", shoeSize: 43 });
                friends.add({ name: "Ylva", shoeSize: 37 });
                friends.add({ name: "Jon", shoeSize: 44 });
                friends.add({ name: "Måns", shoeSize: 42 });
                friends.add({ name: "Daniel", shoeSize: 39 });
                friends.add({ name: "Nils", shoeSize: 45 });
                friends.add({ name: "Zlatan", shoeSize: 47 });
                // Log data from DB:
                friends.orderBy('name').each(function (friend) {
                    log(JSON.stringify(friend));
                });
            }).catch(function (e) {
                log(e, "error");
            });
        }
 
        //
        // Examples
        //
        function equalsAnyOf() {
            log("db.friends.where('name').anyOf('David', 'Zlatan', 'Daniel')", "heading");
            return db.friends.where('name').anyOf('David', 'Zlatan', 'Daniel')
                             .each(function (friend) {
                                 log(JSON.stringify(friend));
                             });
        }
 
        function equalsIgnoreCase() {
            log("db.friends.where('name').equalsIgnoreCase('david')", "heading");
            return db.friends.where('name').equalsIgnoreCase('david')
                             .each(function (friend) {
                                 log(JSON.stringify(friend));
                             });
        }
 
        function startsWithIgnoreCase() {
            log("db.friends.where('name').startsWithIgnoreCase('da')", "heading");
            return db.friends.where('name').startsWithIgnoreCase('da')
                             .each(function (friend) {
                                 log(JSON.stringify(friend));
                             });
        }
 
        function logicalOR() {
            log("db.friends.where('name').startsWithIgnoreCase('da').or('shoeSize').below(40)", "heading");
            return db.friends.where('name').startsWithIgnoreCase('da')
                             .or('shoeSize').below(40)
                             .each(function (friend) {
                                 log(JSON.stringify(friend));
                             });
        }
 
        function logicalAND() {
            log("db.friends.where('name').startsWithIgnoreCase('da').and(function (friend) { return friend.shoeSize > 40; })", "heading");
            return db.friends.where('name').startsWithIgnoreCase('da')
                .and(function (friend) { return friend.shoeSize > 40; })
                .each(function (friend) {
                    log(JSON.stringify(friend));
                });
        }
 
        //
        // Helpers
        //
        function log(txt, clazz) {
            var li = document.createElement('li');
            li.textContent = txt.toString();
            if (clazz) li.className = clazz;
            document.getElementById('log').appendChild(li);
        }
 
        function runSamples() {
            populateSomeData()
                .then(equalsAnyOf)
                .then(equalsIgnoreCase)
                .then(startsWithIgnoreCase)
                .then(logicalOR)
                .then(logicalAND)
            .catch(function (e) {
                log(e, "error");
            });
        }
 
    </script>
    <style>
        li {list-style-type:none;}
        .error { color: red; }
        .heading { color: #808080; margin-top: 12px;}
    </style>
</head>
<body onload="runSamples();">
    <ul id="log"></ul>
</body>
</html> 

关注点

所有这些算法都是在我编写 IndexedDB 包装器 Dexie.js 时发明的。我最初的意图只是为我正在开发的产品创建一个体面的包装器库。

我给这个包装器库起的最早的名字是 StraightForwardDB / sfdb.js——因为我想要一个最小化的 API——但又能像原生 API 一样处理所有版本控制和健壮性。API 的很大一部分受到了 .NET 的 Linq2SQL 的启发。在此过程中,我了解了 IndexedDB 工作原理的细节,当我学习 IDBCursor 接口时,我偶然发现了不区分大小写的搜索和集合匹配的可能性。令我惊讶的是,我在网上找不到任何其他不区分大小写的搜索实现。这启发我写了这篇文章,展示了 IndexedDB 的可能性。

历史

3 月 16 日:初稿

3 月 26 日:文章发布。

3 月 27 日:更新了示例文件 - Dexie.min.js 中的 bug 修复

© . All rights reserved.