Ionic 開發之 Ionic Storage 詳解

  • 2019 年 11 月 6 日
  • 筆記

Ionic Storage 是一款基於 localForage 用於 Ionic 應用程序的簡單 「鍵-值」 存儲模塊,支持 SQLite 開箱即用。該工具可以根據平台自動選擇最佳的存儲引擎,而不用用戶關係具體的使用細節。模塊內存儲引擎的默認選擇順序是 SQLite,IndexedDB,WebSQL 和 LocalStorage。

在原生應用程序環境中運行時,存儲方式會優先使用 SQLite 的原因,是因為它最穩定和最廣泛使用的文件數據之一,並且避免了諸如 localStorage 和 IndexedDB 之類的一些陷阱,比如在低磁盤空間的情況下會自動清理數據。在實際開發中,如果你想執行任意 SQL 查詢,你可以直接使用 Ionic Native SQLite 插件。

接下來,我們先來介紹一下 Ionic Storage 的安轉與使用。

安裝與使用

首先,如果你想使用 SQLite,請先安裝 cordova-sqlite-storage 插件:

$ ionic cordova plugin add cordova-sqlite-storage

接下來,安裝 @Ionic/storage:

$ npm install --save @ionic/storage

然後,導入 IonicStorageModule 並把它添加到根模塊 NgModule 的 imports 列表中:

import { IonicStorageModule } from '@ionic/storage';    @NgModule({    declarations: [      // ...    ],    imports: [      BrowserModule,      IonicModule.forRoot(MyApp),      IonicStorageModule.forRoot()    ],    bootstrap: [IonicApp],    entryComponents: [      // ...    ],    providers: [      // ...    ]  })  export class AppModule {}

之後,你就可以在頁面或組件中注入 Storage 服務:

import { Component } from '@angular/core';    import { NavController } from 'ionic-angular';    import { Storage } from '@ionic/storage';    @Component({    selector: 'page-home',    templateUrl: 'home.html'  })  export class HomePage {      constructor(      public navCtrl: NavController,      public storage: Storage) {    }  }

為了確保進行數據操作時,存儲系統已經初始化完成。你可以在使用前調用 Storage.ready() 方法,不過該方法僅在 1.1.7 以上的版本才支持:

this.storage.ready().then((db) => {  });

若需要保存數據,則可以調用 set(key, value) 方法:

this.storage.set('name', 'semlinker');

若想要獲取上面已存儲的 name 信息,你可以調用 get(key) 方法:

this.storage.get('name').then((name) => {    console.log('Me: Hey, ' + name + '! You have a very nice name.');    console.log('You: Thanks! I got it for my birthday.');  });

當然,要移除已存儲的項目,可以使用 remove(key) 方法:

this.storage.remove('name').then(() => {    console.log('Name item has been removed');  });

下面我們繼續來介紹如何配置 IonicStorageModule 模塊。

配置 Storage

你可以使用特定的存儲引擎優先級配置存儲引擎,也可以將自定義配置項配置為 localForage。更多的選項,請參閱 localForage 配置文檔:https://github.com/localForage/localForage#configuration

注意:任何自定義配置將與默認配置合併

import { IonicStorageModule } from '@ionic/storage';    @NgModule({    declarations: [...],    imports: [      IonicStorageModule.forRoot({        name: '__mydb',        driverOrder: ['indexeddb', 'sqlite', 'websql']      })    ],    bootstrap: [...],    entryComponents: [...],     providers: [...]  })  export class AppModule { }

IonicStorageModule 源碼分析

IonicStorageModule 定義

import { NgModule, ModuleWithProviders } from '@angular/core';  import {    getDefaultConfig,    provideStorage,    Storage,    StorageConfig,    StorageConfigToken  } from './storage';    export { StorageConfig, StorageConfigToken, Storage };    @NgModule()  export class IonicStorageModule {    static forRoot(storageConfig: StorageConfig = null): ModuleWithProviders {      return {        ngModule: IonicStorageModule,        providers: [          { provide: StorageConfigToken, useValue: storageConfig },          {            provide: Storage,            useFactory: provideStorage,            deps: [StorageConfigToken]          }        ]      };    }  }

在前面的配置章節,我們通過調用 forRoot() 方法進行初始化操作:

IonicStorageModule.forRoot({    name: '__mydb',    driverOrder: ['indexeddb', 'sqlite', 'websql']  })

storageConfig 對象除了包含 name 和 driverOrder 屬性外,還支持其它的屬性,StorageConfig 接口的定義如下:

export interface StorageConfig {    name?: string;    version?: number;    size?: number;    storeName?: string;    description?: string;    driverOrder?: string[]; // 存儲引擎的應用順序    dbKey?: string;  }

在 IonicStorageModule 模塊內,配置了兩個 provider:

providers: [    { provide: StorageConfigToken, useValue: storageConfig },    {      provide: Storage,      useFactory: provideStorage,      deps: [StorageConfigToken]    }  ]

第一個 provider 使用 useValue 的方式進行註冊,這裡 StorageConfigToken 的定義如下:

export const StorageConfigToken = new InjectionToken<any>(    'STORAGE_CONFIG_TOKEN'  );

第二個 provider 使用 useFactory 的方式進行註冊,對應的工廠函數 provideStorage 定義如下:

export function provideStorage(storageConfig: StorageConfig): Storage {    const config = !!storageConfig ? storageConfig : getDefaultConfig();    return new Storage(config);  }

provideStorage 函數內部,會先判斷 storageConfig 是否有效,如果無效的時候,會使用通過調用 getDefaultConfig() 方法獲取默認的配置:

export function getDefaultConfig() {    return {      name: '_ionicstorage',      storeName: '_ionickv',      dbKey: '_ionickey',      driverOrder: ['sqlite', 'indexeddb', 'websql', 'localstorage']    };  }

下面我們來分析一下最核心的 Storage 類。

Storage 類

Storage 構造函數

import { Injectable, InjectionToken, Optional } from '@angular/core';    import LocalForage from 'localforage';    import CordovaSQLiteDriver from 'localforage-cordovasqlitedriver';    export class Storage {    private _dbPromise: Promise<LocalForage>;    private _driver: string = null; //      constructor(config: StorageConfig) {      this._dbPromise = new Promise((resolve, reject) => {        let db: LocalForage;          const defaultConfig = getDefaultConfig(); // 獲取默認配置        const actualConfig = Object.assign(defaultConfig, config || {}); // 合併配置          LocalForage.defineDriver(CordovaSQLiteDriver)          .then(() => {            db = LocalForage.createInstance(actualConfig); // 創建db實例          })          .then(() =>            db.setDriver(this._getDriverOrder(actualConfig.driverOrder)) // 設置數據庫驅動          )          .then(() => {            this._driver = db.driver(); // 返回最終使用的驅動類型            resolve(db);          })          .catch(reason => reject(reason));      });    }  }

上面代碼中,在調用 db.setDriver() 方法時,會調用內部的 _getDriverOrder() 方法轉換成相應的驅動:

private _getDriverOrder(driverOrder) {      return driverOrder.map(driver => {        switch (driver) {          case 'sqlite':            return CordovaSQLiteDriver._driver;          case 'indexeddb':            return LocalForage.INDEXEDDB;          case 'websql':            return LocalForage.WEBSQL;          case 'localstorage':            return LocalForage.LOCALSTORAGE;        }      });  }    // https://github.com/localForage/localForage/blob/master/src/localforage.js  const DefaultDrivers = {      INDEXEDDB: idbDriver,      WEBSQL: websqlDriver,      LOCALSTORAGE: localstorageDriver  };

Storage 成員方法

在繼續分析之前,我們先來大致瀏覽一下 Storage 類中定義的成員方法:

  • driver() —— 返回 string 或 null,表示正在使用驅動的名稱;
  • ready() —— 返回 Promise<LocalForage> 對象,當存儲初始化完成後會進入 resolved 狀態;
  • get(key) —— 獲取與給定鍵相關聯的值,返回 Promise 對象;
  • set(key, value) —— 設置給定鍵的值,返回 Promise 對象;
  • remove(key) —— 刪除與此鍵關聯的值,返回 Promise 對象;
  • clear() —— 清除整個鍵值存儲,返回 Promise 對象;
  • length() —— 獲取已存儲對象的個數,返回 Promise 對象;
  • keys() —— 返回用存儲中的所有鍵,返回 Promise 對象;
  • forEach(iteratorCallback) —— 迭代每個鍵值對,返回 Promise 對象:
    • iteratorCallback —— (value, key, iterationNumber)

driver() 和 ready() 方法的實現很簡單:

get driver(): string | null { // 私有屬性 _driver getter 方法    return this._driver;  }    ready(): Promise<LocalForage> {    return this._dbPromise; //  _dbPromise: Promise<LocalForage>;  }

下面來看一下我們常用的 get、set 和 remove 等方法:

// 獲取與給定鍵相關聯的值,返回 Promise 對象  get(key: string): Promise<any> {    return this._dbPromise.then(db => db.getItem(key));  }    // 設置給定鍵的值,返回 Promise 對象  set(key: string, value: any): Promise<any> {    return this._dbPromise.then(db => db.setItem(key, value));  }    // 刪除與此鍵關聯的值,返回 Promise 對象  remove(key: string): Promise<any> {    return this._dbPromise.then(db => db.removeItem(key));  }    // 清除整個鍵值存儲,返回 Promise 對象  clear(): Promise<void> {    return this._dbPromise.then(db => db.clear());  }

最後我們來看一下剩下的三個方法:

// 獲取已存儲項的個數,返回 Promise 對象  length(): Promise<number> {    return this._dbPromise.then(db => db.length());  }    // 返回用存儲中的所有鍵,返回 Promise 對象  keys(): Promise<string[]> {    return this._dbPromise.then(db => db.keys());  }    // 迭代每個鍵值對,返回 Promise 對象  forEach(      iteratorCallback: (value: any, key: string, iterationNumber: Number) => any    ): Promise<void> {      return this._dbPromise.then(db => db.iterate(iteratorCallback));  }

總結

其實分析完 IonicStorageModule 模塊,我們會發現它的內部實現並不複雜。它只是對 localForage 的 API 進行簡單的封裝,實際的存儲功能還是交由 localForage 來完成,感興趣的小夥伴可以研究一下。實際的開發過程中,在數據存儲時,我們可能還會涉及數據響應式、數據加密、數據壓縮、數據遷移和備份,有上述需求的同學,可以了解一下 rxdb 這個庫。