[chrome拡張機能]求む!storageのいい感じのラップ方法


chrome拡張機能でstorageをラップしたい

要約

 chrome拡張機能を開発する時って、コールバックばっかりでしんどいですよね。特にstorageみたいな使う頻度が高くなりそうなapiは、できればpromise化して使いやすくしたいです。(拡張機能でにそこまでやる必要ある?みたいな話もありますが)
 この記事ではstorageをクラスとしてまとめてみたのですが、もうちょっといい感じにならないものかと思い記事にしてみた次第です。ご意見いただけたらうれしいです。
 勿論、どう使うかによっても色々考え方変わると思いますが、一旦ある程度汎用的に使えるものを想定して下記クラスを作成しています。

前提情報

・storageのデータ保存方法は所謂key-value方式。
・一個のオブジェクトへ保存していくイメージ。
※他にも色々制約などがありますが下記リンクにて

公式リファレンス
わかりやすい説明

raz的な実装イメージ

storageのinterface・抽象クラス

storage.ts
export type StorageType = 'local' | 'sync';

export interface StorageInterFace<T> {
  readonly key: string;
  initializeData(): T;
}

export interface StorageData {
  isInitialized: boolean;
}

export abstract class Storage<T extends StorageData> implements StorageInterFace<T> {
  private storage: chrome.storage.LocalStorageArea | chrome.storage.SyncStorageArea;
  readonly key: string;
  protected constructor(key: string, saveType?: StorageType) {
    this.key = key;
    this.storage = saveType === 'sync' ? chrome.storage.sync : chrome.storage.local;
  }

  save = async (data: T) => {
    // keyのつけ外しをこのクラス内でやってあげることで、データの扱いが無茶苦茶やり易いはず。
    const saveObject = {
      [this.key]: data,
    };
    return await new Promise((resolve, reject) => {
      this.storage.set(saveObject, () => {
        if (chrome.runtime.lastError) {
          reject(chrome.runtime.lastError);
        }
      });
    });
  };

  load = async (): Promise<T> => {
    return await new Promise((resolve, reject) => {
      this.storage.get(this.key, async (result) => {
        if (chrome.runtime.lastError) {
          reject(chrome.runtime.lastError);
        }
        // 普通のSaasとかと違ってDB(storage)の環境設定とかがあらかじめできるわけではないので、
        // 初回データアクセス時はundifinedの判定と初期データの作成をしてあげる必要があります。
        const data = result[this.key] as T;
        if (!data?.isInitialized) {
          const initData = this.initializeData();
          await this.save(initData); 
          resolve(initData);
        }
        resolve(result[this.key] as T);
      });
    });
  };

  abstract initializeData(): T;
}

実装クラス(一例)

config.ts
import { StorageData, StorageType, Storage } from './Storage';

interface ConfigData extends StorageData {
  storageType: StorageType;
}

const config_key = 'custum_tabGroup_config';

export class ConfigStorage extends Storage<ConfigData> {
  // initをstatic化しているのは他のkeyの値を参照したいケースがあると思っているため。
  // (例えばメインの大きめなデータはlocalに保存するかどうか設定として持っておくかどうかを選べる様にするとかがある想定です。)
  static init = () => new ConfigStorage(config_key, 'sync');

  initializeData = (): ConfigData => ({ isInitialized: true, storageType: 'sync' });
}

宣伝

本当に大した機能じゃないですが、拡張機能リリースしたのでよかったらみてみてください。
githubもpublicにしてみました。
https://github.com/R-Az/custum_tabGroup
https://chrome.google.com/webstore/detail/tabgroupcustoms/aiidfmkcfamdjancnfkppnbhakahcndm?hl=ja