前言
store.js 库整体代码偏实用性,结构清晰、功能稳定、API 精简,所以学习一下,以及看下这类库的 commit 重构的一个过程。整体结构中最重要的其实是 store-engine,其他就是扩展的能力接口:
目录
- 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 测试
- engine 测试
-
// 插件测试主要是通过统一的调用 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;
}
}