打造一个统一浏览器存储库(统一封装localStorage、sessionStorage、Cookie 和 IndexedDB)

58 阅读2分钟

本文记录了如何从零开始开发一个统一的浏览器存储库 yu-storage,支持 localStorage、sessionStorage、Cookie 和 IndexedDB,并成功发布到 npm 的完整过程。

📖 前言

在前端开发中,我们经常需要处理各种浏览器存储:

  • localStorage - 持久化存储
  • sessionStorage - 会话级存储
  • Cookie - 带过期时间的存储
  • IndexedDB - 大容量存储

每个存储 API 都不相同,使用起来很麻烦。基于大佬的想法,实现此功能。

yu-storage/
├── src/
│   ├── base-storage.js      # 基础存储抽象类
│   ├── local-storage.js     # localStorage 实现
│   ├── session-storage.js   # sessionStorage 实现
│   ├── cookie-storage.js    # Cookie 实现
│   ├── indexeddb-storage.js # IndexedDB 实现
│   └── yu-storage.js        # 主入口文件
├── dist/                    # 构建输出
├── example.html            # 使用示例
├── test.html              # 功能测试
└── README.md              # 文档

🔧 核心实现

1. localStorage 实现

export class LocalStorage extends BaseStorage {
  constructor() {
    super('localStorage');
  }

  isAvailable() {
    try {
      const test = '__localStorage_test__';
      localStorage.setItem(test, test);
      localStorage.removeItem(test);
      return true;
    } catch (e) {
      return false;
    }
  }

  set(key, value, options = {}) {
    if (!this.isAvailable()) {
      console.error('localStorage is not available');
      return false;
    }

    try {
      const data = JSON.stringify(value);
      localStorage.setItem(key, data);
      return true;
    } catch (e) {
      console.error('Error setting localStorage item:', e);
      return false;
    }
  }

  get(key, options = {}) {
    if (!this.isAvailable()) {
      console.error('localStorage is not available');
      return null;
    }

    try {
      const data = localStorage.getItem(key);
      if (data === null) {
        return null;
      }
      return JSON.parse(data);
    } catch (e) {
      console.error('Error getting localStorage item:', e);
      return null;
    }
  }

  // ... 其他方法
}

2. Cookie 实现(重点难点)

Cookie 的实现是最复杂的,需要处理路径、域名、过期时间等参数:

export class CookieStorage extends BaseStorage {
  set(key, value, options = {}) {
    if (!this.isAvailable()) {
      console.error('Cookie is not available');
      return false;
    }

    try {
      const {
        expires = 365,
        path = window.location.pathname || '/',
        domain = '',
        secure = false,
        sameSite = 'Lax'
      } = options;

      // 将值序列化为 JSON 字符串
      const serializedValue = JSON.stringify(value);
      let cookie = `${encodeURIComponent(key)}=${encodeURIComponent(serializedValue)}`;

      if (expires) {
        const date = new Date();
        date.setTime(date.getTime() + (expires * 24 * 60 * 60 * 1000));
        cookie += `; expires=${date.toUTCString()}`;
      }

      if (path) {
        cookie += `; path=${path}`;
      }

      if (domain) {
        cookie += `; domain=${domain}`;
      }

      if (secure) {
        cookie += '; secure';
      }

      if (sameSite) {
        cookie += `; samesite=${sameSite}`;
      }

      document.cookie = cookie;
      return true;
    } catch (e) {
      console.error('Error setting cookie:', e);
      return false;
    }
  }

  get(key, options = {}) {
    if (!this.isAvailable()) {
      console.error('Cookie is not available');
      return null;
    }

    try {
      const name = `${encodeURIComponent(key)}=`;
      const decodedCookie = decodeURIComponent(document.cookie);
      const ca = decodedCookie.split(';');

      for (let i = 0; i < ca.length; i++) {
        let c = ca[i];
        while (c.charAt(0) === ' ') {
          c = c.substring(1);
        }
        if (c.indexOf(name) === 0) {
          const value = c.substring(name.length, c.length);
          // 尝试解析 JSON,如果失败则返回原始字符串
          try {
            return JSON.parse(value);
          } catch (e) {
            return value;
          }
        }
      }
      return null;
    } catch (e) {
      console.error('Error getting cookie:', e);
      return null;
    }
  }
}

3. IndexedDB 实现(异步处理)

IndexedDB 是异步的,需要返回 Promise:

export class IndexedDBStorage extends BaseStorage {
  constructor() {
    super('indexedDB');
    this.dbName = 'YuStorageDB';
    this.storeName = 'YuStorageStore';
    this.db = null;
  }

  async initIndexedDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName);
        }
      };
    });
  }

  async set(key, value, options = {}) {
    if (!this.isAvailable()) {
      console.error('IndexedDB is not available');
      return false;
    }

    try {
      if (!this.db) {
        await this.initIndexedDB();
      }

      return new Promise((resolve, reject) => {
        const transaction = this.db.transaction([this.storeName], 'readwrite');
        const store = transaction.objectStore(this.storeName);
        const request = store.put(value, key);

        request.onsuccess = () => resolve(true);
        request.onerror = () => reject(request.error);
      });
    } catch (e) {
      console.error('Error setting IndexedDB item:', e);
      return false;
    }
  }

  // ... 其他异步方法
}

4. 统一入口类

class YuStorage {
  constructor(defaultType = 'local') {
    this.types = {
      local: 'localStorage',
      session: 'sessionStorage',
      cookie: 'cookie',
      indexeddb: 'indexedDB'
    };

    this.storages = {
      [this.types.local]: new LocalStorage(),
      [this.types.session]: new SessionStorage(),
      [this.types.cookie]: new CookieStorage(),
      [this.types.indexeddb]: new IndexedDBStorage()
    };

    this.defaultType = this.types[defaultType] || this.types.local;
  }

  set(key, value, options = {}) {
    const storage = this.getStorage(options.type);
    return storage.set(key, value, options);
  }

  get(key, options = {}) {
    const storage = this.getStorage(options.type);
    return storage.get(key, options);
  }

  remove(key, options = {}) {
    const storage = this.getStorage(options.type);
    return storage.remove(key, options);
  }

  clear(options = {}) {
    const storage = this.getStorage(options.type);
    return storage.clear(options);
  }

  keys(options = {}) {
    const storage = this.getStorage(options.type);
    return storage.keys(options);
  }

  getStorage(type) {
    const storageType = this.types[type] || this.defaultType;
    const storage = this.storages[storageType];
    
    if (!storage) {
      throw new Error(`Unsupported storage type: ${type}`);
    }

    if (!storage.isAvailable()) {
      console.warn(`Storage type ${storageType} is not available, falling back to localStorage`);
      return this.storages[this.types.local];
    }

    return storage;
  }
}

🧪 测试与调试

遇到的问题

  1. Cookie 在 file:// 协议下不可用
    • 问题:直接打开 HTML 文件时 Cookie 无法工作
    • 解决:使用 HTTP 服务器进行测试

📦 发布到 npm

# 检查登录状态
npm whoami

# 检查包名是否可用
npm view yu-storage

# 发布到 npm
npm publish

🎉 使用效果

安装

npm install yu-storage

使用示例

import yuStorage from 'yu-storage';

// 使用默认存储(localStorage)
yuStorage.set('name', '张三');
const name = yuStorage.get('name');
console.log(name); // 张三

// 指定存储类型
yuStorage.set('theme', 'dark', { type: 'session' });
yuStorage.set('token', 'abc123', { type: 'cookie', expires: 7 });
yuStorage.set('settings', { lang: 'zh' }, { type: 'indexeddb' });

// 获取数据
const theme = yuStorage.get('theme', { type: 'session' });
const token = yuStorage.get('token', { type: 'cookie' });
const settings = await yuStorage.get('settings', { type: 'indexeddb' });

🔗 相关链接

如果觉得有用,请给个 ⭐️ 支持一下!