阅读 store.js

139 阅读5分钟

前言

store.js 库整体代码偏实用性,结构清晰、功能稳定、API 精简,所以学习一下,以及看下这类库的 commit 重构的一个过程。整体结构中最重要的其实是 store-engine,其他就是扩展的能力接口:

store.js.svg

目录

  • store.js 的接口设计
// 首先会定义一个对象
var storeAPI = {}

// 这个对象中包含了所有的开放 api
var storeAPI = {
  version: '',
  enabled: false,

  // 访问相关 API
  set(key, value) { },
  get(key, defaultValue) { },
  remove(key) { },
  clearAll() { },
  each() { },

  // 扩展性相关 API
  addPlugin() { },
  createStore() { },
  namespace() { },
  hasNamespace() { },
}

其中,访问相关的 API 会依赖具体的 storage 的实现,如果要实现一个 storage,必须满足以下接口即可:

// 官方的写法
module.exports = {
  name: 'customStorage',
  read: read,
  write: write,
  each: each,
  remove: remove,
  clearAll: clearAll,
}

// 然后再依次实现接口即可,比如 read
function read(key) {
  // 为什么需要运行时实时获取对象 localStorage() 方式
  // #190 <https://github.com/marcuswestin/store.js/issues/190>
  return customStorage().getItem(key)
}

// 然后开放的 API 中就会调用对应的 [storage.read](<http://storage.read>) 等方法。
set(key, value)
{
  // 如果设置 undefined,认为需要删除
  if (value === undefined) return this.remove(key)

  // 会默认序列化值
  this.storage.write(key, this._serialize(value)

  return value
}
  • 如何实现插件机制
_addPlugin(plugin)
{
  // 1. 先判断是否重复注册过,浅比较,所以后续的变量是 mutation 的
  // 2. 这里只判断 plugin function 是否重新注册,但是不会判断 api 是否真实存在
  // 3. api 是支持 mixin 的
  const seenPlugin = pluck(this.plugins, seenPlugin => plugin === seenPlugin)

  // 调用过了就不处理了,可以重复 addPlugin(fn) addPlugin(fn)...
  if (seenPlugin) return

  // 2. 注册到 store this 上去
  // 缓存起来
  this.plugins.push(plugin)
  // 指定下 this
  var pluginProps = plugin.call(this)
  // 开始拷贝
  each(pluginProps, function (propValue, propName) {
   if (!isFunction(propValue)) { /* plugin 的属性必须是一个方法,所以不能扩展属性 */
   }

   self._assignPluginProps(propValue, propName)
  })
}

_assignPluginProps(propValue, propName)
{
  var oldFn = this[propName]

  this.propName = function pluginFn() {
   var self = this

   function super_fn() {
   } // 调用 oldFn

   var newArgs = [super_fn].concat(args)

   return propValue.apply(self, newArgs)
  }
}

// plugin 的数据结构
// 自定义结构,必须是一个方法,返回一个对象
// 然后会绑定到 store 实例的属性上
// 所以返回一个对象结构后,后面会通过 .call 来指定 this -> store
// example
function customPlugin() {
  return {
   api: api,
  }

  function api() {
  }
}

// 验证是否重复注册,主要是用 pluck 函数
function pluck(obj, fn) {
  if (isList(obj)) {
   for (var i = 0; i < obj.length; i++) {
    if (fn(obj[i], i)) return obj[i]
   }
  } else {
   for (var key in obj) {
    if (obj.hasOwn(key) && fn(obj[key], key)) {
     return obj[key]
    }
   }
  }
}
  • 实现 plugin
function dumpPlugin() {
  return {
   dump: dump,
  }

  function dump() {
   var res = {} // 打印的 json

   // this -> store
   this.each(function (val, key) {
    res[key] = val
   })

   return res
  }
}
  • plugin mixin 的情况
function customPlugin() {
  return {
   set: set,
  }

  function set(super_fn, key, value) {
   // 通过在这里调用来 mock 复制
   return super_fn()
  }
}
  • 如何实现自定义 store 机制
// 默认 dist/store.modern.js 进行注册
var engine = require('./src/store-engine')

engine.createStore([
  require('../storage/localStorage'),
], [])
  • 实现 storage
    • 依次支持的顺序
      • default: localStorage
require('./localStorage'),
require('./oldFF-globalStorage'),
require('./oldIE-userDataStorage'),
require('./cookieStorage'),
require('./sessionStorage'),
// 内存变量
require('./memoryStorage')

// 实现 storage 需要的数据结构
module.exports = {
  name: 'customStorage',
  read: read,
  write: write,
  remove: remove,
  clearAll: clearAll,
};

// issues 190,主要为了解决在隐私或者 iframe 某场景读取报错,这样在外面就可以捕获运行时异常
function customStorage() {
  return Global.customStorage;
}

interface StorageAPI {
  read(key: string): any;

  write(key: string, data: any): any;

  each(fn: (value: any, key: string) => any): void;

  remove(key: string): any;

  clearAll(): any;
}

// 最后注册即可,其实全局会按照 storages = [] 的顺序进行 support 检测后,注册成功一个可用
if (_testStorage(storage)) {
  this.storage = storage;
  // 所以能够通过 enable 是否为 true 来看 store.js 是否可用
  // 其实在实际情况下,都是 true,如果注册全部的话会有 memoryStorage.js 支持
  this.enable = true;
}
  • 特性支持

    • 如何实现 namespace
    // namespace 实现比较简单,主要需要注意 each 中对 key 的替换逻辑
    var _namespaceReg = new RegExp('^' + namespacePrefix)
    
    var _namespacePrefix = '<逻辑>'
    // 这里的逻辑主要是如果传入了 namespace 后也会添加公用的前缀,避免不同库撞 key
    var namespacePrefix = '__storejs_' + namespace;
    
    • 如何实现 store expire 过期机制
      // 主要通过容易一个 store 来记录过期时间,数据结构如下:
      var expirations = this.createStore(this.storage, null, this._namespacePrefix + namespace);
    
      // 复写所有 api, 比如
      module.exports = {
        set: expire_set, // 添加过期的逻辑
      };
    
      // 主要过期逻辑 2 部分:1. 验证是否过期,2. 过期的处理
      _checkExpire(key) {
        // 如果不设置过期,理论值是永久不过期
        var expiration = expirations.get(key, Number.MAX_VALUE);
    
        if (expiration < new Date().getTime()) {
          this.raw.remove(key); // 删除 store 中原始值
          this.expirations.remove(key); // 删除过期记录
        }
      }
    
      expire_get(super_fn, key)
      {
        // 避免撞 key 的情况,这种情况属于覆盖,这里做一个判断来实现是否使用过期的 plugin
        if (!this.hasNamespace(key)) {
          _checkExpire.call(this, key);
        }
      }
    
      // 使用的时候就有限制了,需要制定一个当前使用的相对时间 max-age
      store.set('key', 'value', new Date().getTime() + 1000); // 1s
    
    • 序列化和反序列化的实现
    var storeAPI = {
      _serialize(obj) {
        return JSON.stringify(obj);
      },
    
      _deserialize(value, defaultVal) {
        // 所有这里如果反序列化 0/'0'/'' 应该都会返回 defaultVal
        if (!value) return defaultVal;
    
        var val = '';
    
        try {
          val = JSON.parse(value);
        } catch (e) {
          val = defaultVal;
        }
    
        return val !== undefined ? val : defaultVal;
      },
    };
    
    • 如何测试浏览器能存储的大小
      • 不同浏览器存储的大小限制会不同,3MB 目前从数据来看是安全的,所以会有探测或者反复写入的需求,但是过大的需求应该使用浏览器 db 来做。

首先可以获取到当前 localStorage 存储的大小:stackoverflow.com/questions/4…

// copy 代码,有一个安全值:3
const localStorageSpace = () => {
  let allStrings = '';
  for (const key of Object.keys(window.localStorage)) {
    allStrings += window.localStorage[key];
  }
  return allStrings ? 3 + ((allStrings.length * 16) / (8 * 1024)) + ' KB' : 'Empty (0 KB)';
};
console.log(localStorageSpace())

// 发现还有现有 api 使用的代码
// 这个计算值更加精准,上面我们的代码计算出来剩余偏大
window.navigator.webkitTemporaryStorage.queryUsageAndQuota(function(used, remaining) { 
  console.log('Used space', used); 
  console.log('Remaining space', remaining); 
}, function(err) { 
  console.warn(err); 
});
  • 工程组织
    • issues 用例的管理

      • 主要通过 test/bugs 目录根据 gh-issue-id 进行管理
    • 自动化的脚本

      • [release.sh](<http://release.sh>) 这个还是不看他的…看 vite 的更好点
      • scripts/
        • engine 测试
          • 基础的用例,覆盖度不算高,虽然是该库最核心内容
          • 核心很少功能,主要实现组织 api 和测试功能
        • storage 测试
          • 通过统一的接口进行用例编写
        • plugins 测试
// 插件测试主要是通过统一的调用 api 进行
module.exports = {
	plugin: require('..'),
	setup() {
		// 该插件对应的用例
	},
};
  • 如何实现兼容性处理

store.js 通过尝试写入、读取、移除,功能性测试来判断是否兼容,主要代码是 _testStorage

function _testStorage(targetStorage) {
  try {
    // 尝试写入的测试字符串,这个其实是一个副作用,业务代码中不能重复
    var testStr = '__storejs__test__';

    targetStorage.write(testStr, testStr);

    var ok = (targetStorage.read(testStr) === testStr);
    targetStorage.remove(testStr);

    return ok;
  } catch (e) {
    return false;
  }
}