<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一行代碼,搞定瀏覽器數(shù)據(jù)庫 IndexedDB

          共 14278字,需瀏覽 29分鐘

           ·

          2021-03-01 01:41

          前言

          2021 年,如果你的前端應(yīng)用,需要在瀏覽器上保存數(shù)據(jù),有三個主流方案可以選擇:

          • Cookie:上古時代就已存在,但能應(yīng)用的業(yè)務(wù)場景非常有限
          • LocalStorage:使用簡單靈活,但是容量只有 10Mb,且不適合儲存結(jié)構(gòu)化數(shù)據(jù)
          • IndexedDB:算得上真正意義上的數(shù)據(jù)庫,但坑異常多,使用麻煩,古老的 API 設(shè)計放在現(xiàn)代前端工程中總有種格格不入的感覺

          我在大三的時候,曾經(jīng)用 IndexedDB 寫過一個背單詞 App,當(dāng)時就有把 IndexedDB 封裝一遍的想法,但是由于學(xué)業(yè)緊張,后來就擱置了

          最近,我終于有了空閑時間,于是撿起了當(dāng)年的想法,開始嘗試用 TypeScriptIndexedDB 封裝一遍,把坑一個個填上,做成一個開發(fā)者友好的庫,并開源出來,上傳至 npm

          拍腦袋后,我決定把這個項目命名為 Godb.js

          Godb.js

          Godb.js 的出現(xiàn),讓你即使你不了解瀏覽器數(shù)據(jù)庫 IndexedDB,也能把它用的行云流水,從而把關(guān)注點放到業(yè)務(wù)上面去

          畢竟要用好 IndexedDB,你需要翻無數(shù)遍 MDN,而 Godb 替你吃透了 MDN,從而讓你把 IndexedDB 用的更好的同時,操作還更簡單了

          當(dāng)前項目處于 Alpha 階段(版本 0.3.x),意味著之后隨時可能會有 breaking changes,在正式版(1.0.0 及以后)發(fā)布之前,不要把這個項目用到任何嚴(yán)肅的場景下

          項目GitHub:https://github.com/chenstarx/Godb.js

          如果覺得不錯的話就點個 Star 吧~

          項目完整文檔與官網(wǎng)正在緊張開發(fā)中,現(xiàn)階段可以通過下面的 demo 來嘗鮮

          安裝

          首先需要安裝,這里默認(rèn)你使用了 webpack、gulp 等打包工具,或在 vue、react 等項目中

          npm install godb

          在第一個正式版發(fā)布后,還會提供 CDN 的引入方式,敬請期待~

          簡單上手

          操作非常簡單,增、刪、改、查各只需要一行代碼:

          import Godb from 'godb';

          const testDB = new Godb('testDB');
          const user = testDB.table('user');

          const data = {
            name'luke',
            age22
          };

          user.add(data) // 增
            .then(id => user.get(id)) // 查,等價于 user.get({ id: id })
            .then(luke => user.put({ ...luke, age23 })) // 改
            .then(id => user.delete(id)); // 刪

          這里注意增刪改查四個方法在 Promise.then 的返回值:

          • Table.get() 返回的是完整數(shù)據(jù)
          • Table.add()Table.put() 返回的是 id(也可以返回完整數(shù)據(jù),評論區(qū)留言討論吧~)
          • Table.delete() 不返回數(shù)據(jù)(返回 undefined

          第二點需要注意的就是,put(obj) 方法中的 obj 需要包含 id,否則就等價于 add(obj)

          上面的 demo 中,get 得到的 luke 對象包含 id,因此是修改操作

          之后會引入一個 update 方法來改進(jìn)這個問題

          也可以一次性添加多條數(shù)據(jù)

          const data = [
              {
                  name'luke',
                  age22
              },
              {
                  name'elaine',
                  age23
              }
          ];

          user.addMany(data)
            .then(() => user.consoleTable());

          addMany(data) 方法:

          • 嚴(yán)格按照 data 的順序添加
          • 返回 id 的數(shù)組,與 data 順序一致

          之所以單獨寫個 addMany,而不在 add 里加一個判斷數(shù)組的邏輯,是因為用戶想要的可能就是添加一個數(shù)組到數(shù)據(jù)庫

          注意:addManyadd 不要同步調(diào)用,如果在 addMany 正在執(zhí)行時調(diào)用 add,可能會導(dǎo)致數(shù)據(jù)庫里的順序不符合預(yù)期,請在 addMany 的回調(diào)完成后再調(diào)用 add

          Table.consoleTable()

          這里用了一個 Table.consoleTable() 的方法,它會在瀏覽器的控制臺打印出下面的內(nèi)容:

          這里的 (index) 就是 id

          雖然 chrome 開發(fā)者工具內(nèi)就能看到表內(nèi)所有數(shù)據(jù),但這個方法好處是可以在需要的時候打印出數(shù)據(jù),方便 debug

          注意:這個方法是異步的,因為需要在數(shù)據(jù)庫里把數(shù)據(jù)庫取出來;異步意味著緊接在它后面的代碼,可能會在打印出結(jié)果之前執(zhí)行,如果不希望出現(xiàn)這種情況,使用 awaitPromise.then 即可

          Table.find()

          如果你想在數(shù)據(jù)庫中查找數(shù)據(jù),還可以使用 Table.find() 方法:

          const data = [
              {
                  name'luke',
                  age22
              },
              {
                  name'elaine',
                  age23
              }
          ];

          user.addMany(data)
            .then(() => {
              user.find((item) => {
               return item.age > 22;
              })
                .then((data) => console.log(data)) // { name: 'luke', age: 23 }
            });

          Table.find(fn) 接受一個函數(shù) fn 作為參數(shù),這個函數(shù)的返回值為 truefalse

          這個方法在內(nèi)部會從頭遍歷整個表(使用 IndexedDB 的 Cursor),然后把每一次的結(jié)果放進(jìn) fn 執(zhí)行,如果 fn 的返回值為 true(也可以是 1 這樣的等價于 true 的值),就返回當(dāng)前的結(jié)果,停止遍歷

          這個方法只會返回第一個滿足條件的值,如果需要返回所有滿足條件的值,請使用 Table.findAll(),用法與 Table.find() 一致,但是會返回一個數(shù)組,包含所有滿足條件的值

          Schema

          如果你希望數(shù)據(jù)庫的結(jié)構(gòu)更嚴(yán)格一點,也可以添加 schema

          import Godb from 'godb';

          // 定義數(shù)據(jù)庫結(jié)構(gòu)
          const schema = {
              // user 表:
              user: {
                  // user 表的字段:
                  name: {
                      typeString,
                      uniquetrue // 指定 name 字段在表里唯一
                  },
                  ageNumber
              }
          }

          const testDB = new Godb('testDB', schema);
          const user = testDB.table('user');

          const luke1 = {
              name'luke'
              age22
          };

          const luke2 = {
              name'luke'
              age19
          };

          user.add(luke1) // 沒問題
            .then(() => user.get({ name'luke' })) // 定義schema后,就可以用 id 以外的字段獲取到數(shù)據(jù)了
            .then(() => user.add(luke2)) // 報錯,name 重復(fù)了

          如上面的例子

          • 定義了 schema,因此 get() 中可以使用 id 以外的字段搜索了,否則只能傳入 id
          • 指定了 user.name 這一項是唯一的,因此無法添加重復(fù)的 name

          Table.get() 與 Table.find() 區(qū)別

          get() 使用數(shù)據(jù)庫索引搜索,性能更高,而 find() 是遍歷全表,使用上靈活

          關(guān)于 schema:

          部分同學(xué)或許會發(fā)現(xiàn),上面定義 schema 的方式有點眼熟,沒錯,正是參考了 mongoose

          • 定義數(shù)據(jù)庫的字段時,可以只指明數(shù)據(jù)類型,如上面的 age: Number
          • 也可以使用一個對象,里面除了定義數(shù)據(jù)類型 type,也指明這個字段是不是唯一的(unique: true),之后會添加更多可選屬性,如用來指定字段默認(rèn)值的 default,和指向別的表的索引 ref

          不定義 Schema 時,Godb 使用起來就像 MongoDB 一樣,可以靈活添加數(shù)據(jù);區(qū)別是 Mongodb 中,每條數(shù)據(jù)的唯一標(biāo)識符是 _id,而 Godbid

          雖然這樣做的問題是,IndexedDB 畢竟還是結(jié)構(gòu)化的,用戶使用不規(guī)范的話(如每次添加的數(shù)據(jù)結(jié)構(gòu)都不一樣),久而久之可能會使得數(shù)據(jù)庫的字段特別多,且不同數(shù)據(jù)中沒用到的字段都是空的,導(dǎo)致浪費,影響性能

          定義 Schema 后,Godb 使用起來就像 MySQL 一樣,如果添加 Schema 沒有的字段,或者是字段類型不符合定義,會報錯(在寫文檔的時候還沒有實現(xiàn)這個功能,即使 Schema 不符合也能加,下個版本會安排上)

          因此推薦在項目中,定義好 schema,這樣不管是維護(hù)性上,還是性能上,都要更勝一籌

          另一個使用 await 的 CRUD demo:

          import Godb from 'godb';

          const schema = {
            user: {
              name: {
                typeString,
                uniquetrue
              },
              ageNumber
            }
          };

          const db = new Godb('testDB', schema);
          const user = db.table('user');

          crud();

          async function crud({

            // 增:
            await user.addMany([
              {
                name'luke',
                age22
              },
              {
                name'elaine',
                age23
              }
            ]);

            console.log('add user: luke');
            // await 非必須,這里是為了防止打印順序不出錯
            await user.consoleTable();

            // 查:
            const luke = await user.get({ name'luke' });
            // const luke = await user.get(2); // 等價于:
            // const luke = await user.get({ id: 2 });

            // 改:
            luke.age = 23;
            await user.put(luke);

            console.log('update: set luke.age to 23');
            await user.consoleTable();

            // 刪:
            await user.delete({ name'luke' });

            console.log('delete user: luke');
            await user.consoleTable();

          }

          上面這段 demo,會在控制臺打印出下面的內(nèi)容:

          API 設(shè)計

          因為「連接數(shù)據(jù)庫」和「連接表」這兩個操作是異步的,在設(shè)計之初,曾經(jīng)有兩個 API 方案,區(qū)別在于:要不要把這兩個操作,做為異步 API 提供給用戶

          這里討論的不是「API 如何命名」這樣的細(xì)節(jié),而是「API 的使用方式」,因為這會直接影響到用戶使用 Godb 時的業(yè)務(wù)代碼編寫方式

          以連接數(shù)據(jù)庫 -> 添加一條數(shù)據(jù)的過程為例

          設(shè)計一:提供異步特性

          GitHub 上大多數(shù)開源的 IndexedDB 封裝庫都是這么做的

          import Godb from 'godb';

          // 連接數(shù)據(jù)庫是異步的
          Godb.open('testDB')
              .then(testDB => testDB.table('user')) // 連接表也需要異步
              .then(user => {
                  user.add({
                      name'luke',
                      age22
                  });
              });
          });

          這樣的優(yōu)點是,工作流程一目了然,畢竟對數(shù)據(jù)庫的操作,要放在連接數(shù)據(jù)庫之后

          但是,這種設(shè)計不適合工程化的前端項目!

          因為,所有增刪改查等操作,都需要用戶,手動放到連接完成的異步回調(diào)之后,否則無法知道操作時有沒有連上數(shù)據(jù)庫和表

          導(dǎo)致每次需要操作數(shù)據(jù)庫時,都要先打開數(shù)據(jù)庫一遍數(shù)據(jù)庫,才能繼續(xù)

          即使你預(yù)先定義一個全局的連接,你在之后想要使用它時,如果不包一層 Promise,是無法確定數(shù)據(jù)庫和表,在使用時有沒有連接上的

          以 Vue 為例,如果你在全局環(huán)境(比如 Vuex)定義了一個連接:

          import Godb from 'godb';

          new Vuex.Store({
            state: {
              godbawait Godb.open('testDB'// 不加 await 返回的就是 Promise 了
            }
          });

          這樣,在 Vue 的任何一個組件中,我們都能訪問到 Godb 實例

          問題來了,在你的組件中,如果你想在組件初始化時,比如 createdmounted 這樣的鉤子函數(shù)中(React 中就是 ComponentDidMount),去訪問數(shù)據(jù)庫:

          new Vue({
             mounted() {
                 const godb = this.$store.state.godb; // 從全局環(huán)境取出連接
                 godb.table('user')
                     .then(user => {
                         user.add({
                             name'luke',
                             age22
                         }); // user is undefined!
                     });
             }
          });

          你會發(fā)現(xiàn),如果這個組件在 App 初始化時就被加載,在組件 mounted 函數(shù)觸發(fā)時,本地數(shù)據(jù)庫可能根本就沒有連接上?。ㄟB接數(shù)據(jù)庫這樣的操作,最典型的執(zhí)行場景就是在組件加載時)

          解決辦法是,在每一個需要操作數(shù)據(jù)庫的地方,都定義一個連接:

          import Godb from 'godb';

          new Vue({
              mounted() {
                  Godb.open('testDB')
                    .then(testDB => testDB.table('user'))
                    .then(user => {
                        user.add({
                            name'luke',
                            age22
                        });
                    });
              }
          });

          這樣不僅代碼又臭又長,性能低下(每次操作都需要先連接),在需要連接本地數(shù)據(jù)庫的組件多了后,維護(hù)起來更是一場噩夢

          簡而言之,就是這個方案,在工程化前端的不同組件中,需要在每次操作之前,都連一遍數(shù)據(jù)庫,否則無法確保組件加載時,已經(jīng)連接上了 IndexedDB

          設(shè)計二:隱藏連接的異步特性

          我最終采用了這個方案,對開發(fā)者而言,甚至感覺不到「連接數(shù)據(jù)庫」和「連接表」這兩個操作是異步的

          const testDB = new Godb('testDB');
          const user = testDB.table('user');

          user.add({
              name'luke',
              age22
          }).then(id => console.log(id));

          這樣使用上非常自然,開發(fā)者并不需要關(guān)心操作時有沒有連上數(shù)據(jù)庫和表,只需要在操作后的回調(diào)內(nèi)寫好自己的邏輯就可以

          但是,這個方案的缺點就是開發(fā)起來比較麻煩(嘿嘿,麻煩自己,方便用戶)

          因為 new Codb('testDB') 內(nèi)部的連接數(shù)據(jù)庫的操作,實際上是異步的(因為 IndexedDB 的原生 API 就是異步的設(shè)計)

          在連接數(shù)據(jù)庫的操作發(fā)出去后,即使還沒連接上,下面的 testDB.table('user')user.add() 也會先開始執(zhí)行

          也就是說,之后的「獲取 user 表」 和 「添加一條數(shù)據(jù)」實際上會先于「連上數(shù)據(jù)庫」這個過程執(zhí)行,如果實現(xiàn)該 API 設(shè)計時未處理這個問題,上面的示例代碼肯定會報錯

          而要處理這個問題,我用到了下面兩個方法:

          • 在每次需要連上數(shù)據(jù)庫的操作中(比如 add()),先拿到數(shù)據(jù)庫的連接,再進(jìn)行操作
          • 使用隊列 Queue,在還未連接時,把需要連接數(shù)據(jù)庫的操作放進(jìn)隊列,等連接完成,再執(zhí)行該隊列

          具體而言,就是

          • Godb 的 class 中定義一個 getDB(callback),用來獲取 IndexedDB 連接實例
          • 增刪改查中,都調(diào)用 getDB,在 callback 獲取到 IndexedDB 的連接實例后再進(jìn)行操作
          • getDB 中使用一個隊列,如果數(shù)據(jù)庫還沒連接上,就把 callback 放進(jìn)隊列,在連接上后,執(zhí)行這個隊列中的函數(shù)
          • 連接完成時,直接把 IndexedDB 連接實例傳進(jìn) callback 執(zhí)行即可

          在調(diào)用 getDB 時,可能有三種狀態(tài)(其實還有個數(shù)據(jù)庫已關(guān)閉的狀態(tài),這里不討論):

          1. 剛初始化,未發(fā)起和 IndexedDB 的連接
          2. 正在連接 IndexedDB,但還未連上
          3. 已經(jīng)連上,此時已經(jīng)有 IndexedDB 的連接實例

          第一種狀態(tài)只在第一次執(zhí)行 getDB 時觸發(fā),因為一旦嘗試建立連接就進(jìn)入下一個狀態(tài)了;第一次執(zhí)行被我放到了 Godb 類的構(gòu)造函數(shù)中

          第三種狀態(tài)時,也就是已經(jīng)連上數(shù)據(jù)庫后,直接把連接實例傳進(jìn) callback 執(zhí)行即可

          關(guān)鍵是處理第二種狀態(tài),此時正在連接數(shù)據(jù)庫,但還未連上,無法進(jìn)行增刪改查:

          const testDB = new Godb('testDB');
          const user = testDB.table('user');

          user.add({ name'luke' }); // 此時數(shù)據(jù)庫正在連接,還未連上
          user.add({ name'elaine' }); // 此時數(shù)據(jù)庫正在連接,還未連上

          testDB.onOpened = () => { // 數(shù)據(jù)庫連接成功的回調(diào)
              user.add({ name'lucas' }); // 此時已連接
          }

          上面的例子,頭兩個 add 操作時其實數(shù)據(jù)庫并未連接上

          那要如何操作,才能保證正常添加,并且 lukeelainelucas 進(jìn)入數(shù)據(jù)庫的順序和代碼一致呢?

          答案是使用隊列 Queue,把兩個 add 操作加進(jìn)隊列,在連接成功時,按先進(jìn)先出的順序執(zhí)行

          這樣,用戶就不需要關(guān)心,操作時數(shù)據(jù)庫是否已經(jīng)連上了(注意增刪改查有異步回調(diào),在回調(diào)里可以知道是否操作成功),Godb 幫你在幕后做好了這一切

          注意之所以使用 callback 而不是 Promise,是因為 JS 中的回調(diào)既可以是異步的,也可以是同步的

          而連接成功,已經(jīng)有連接實例后,直接同步返回連接實例更好,沒必要再使用異步

          還是以 Vue 為例,如果我們在 Vuex(全局變量)中添加連接實例:

          import Godb from 'godb';

          new Vuex.Store({
              state: {
            godbnew Godb('testDB')
              }
          });

          這樣,在所有組件中,我們都可以使用同一個連接實例:

          new Vue({
              computed: {
                  // 把全局實例變?yōu)榻M件屬性
                  godb() {
                      return this.$store.state.godb;
                  }
              },
              mounted() {
                  this.godb.table('user').add({
                      name'luke',
                      age22
                  }).then(id => console.log(id));
              }
          });

          總結(jié)這個方案的優(yōu)點:

          • 性能更高(可以全局共享一個連接實例)
          • 代碼更簡潔
          • 最關(guān)鍵的,心智負(fù)擔(dān)低了很多!

          缺點:Godb 開發(fā)更麻煩,不是簡單把 IndexedDB 包一層 Promise 就行

          因此,我最終采用了這個方案,畢竟麻煩我一個,方便你我他,優(yōu)點遠(yuǎn)遠(yuǎn)蓋過了缺點

          如果對實現(xiàn)好奇的話,可以去閱讀源碼,當(dāng)前只是實現(xiàn)了基本的 CRUD,源碼暫時還不復(fù)雜

          近期待辦

          在把基本的 CRUD 完成后,我就寫下了這篇文章,讓大家來嘗嘗鮮

          而接下來要做的事其實非常多,近期我會完成下面的開發(fā):

          • [ ] Table.update():更好的更新數(shù)據(jù)的方案
          • [ ] 全局錯誤處理,目前代碼里 throw 的 Error 其實是沒被處理的
          • [ ] 如果定義了 Schema,那就在所有 Table 的方法執(zhí)行前都檢查 Schema
          • [ ] 如果定義了 Schema,保證數(shù)據(jù)庫的結(jié)構(gòu)和 Schema 一致

          如果你有任何建議或意見,請在評論區(qū)留言,我會認(rèn)證讀每一個反饋

          如果覺得這個項目有意思,歡迎給文章點贊,歡迎來 GitHub 點個 star~

          https://github.com/chenstarx/Godb.js

          最后

          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          歡迎關(guān)注「前端瓶子君」,回復(fù)「算法」,加入前端算法源碼編程群,每日一刷(工作日),每題瓶子君都會很認(rèn)真的解答喲
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對你有幫助,在看」是最大的支持
          》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持
          瀏覽 43
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  波多野吉衣高清无码 | 欧美猛男操欧美美女 | 新www视频亚洲 | 亚洲天堂不卡一区二区三区 | 国产区视频在线 |