TypeScript:Example:Storage
LocalStorage와 SessionStorage를 적용할 수 있는 TypeScript 구현체 샘플.
index.ts
Vue플러그인으로 구현한다.
import VueInterface from 'vue';
import { PluginObject } from 'vue/types/plugin';
import PersistRecc from '@/persists/PersistRecc';
import PersistOptions from '@/persists/PersistOptions';
class PersistPlugin implements PluginObject<any> {
install(Vue: typeof VueInterface, options?: PersistOptions): void {
Vue.prototype.$persist = new PersistRecc(options);
}
}
const VuePersist = new PersistPlugin()
export default VuePersist;
이걸 Vue 플러그인으로 적용할 때:
MemoryStorage.ts
export default class MemoryStorage implements Storage {
private _dict: Map<string, string>;
constructor() {
this._dict = new Map<string, string>();
}
get length(): number {
return this._dict.size;
}
clear(): void {
this._dict.clear();
}
getItem(key: string): string | null {
const result = this._dict.get(key);
if (result === undefined) {
return null;
}
return result;
}
key(index: number): string | null {
if (0 <= index && index < this._dict.size) {
const keys = this._dict.keys();
while (true) {
if (index) {
keys.next();
--index;
} else {
return keys.next().value;
}
}
} else {
return null;
}
}
removeItem(key: string): void {
this._dict.delete(key);
}
setItem(key: string, value: string): void {
this._dict.set(key, value);
}
}
PersistBase.ts
import MemoryStorage from '@/persists/MemoryStorage';
import PersistOptions from '@/persists/PersistOptions';
export interface ListenerData {
key: string;
oldValue: any;
newValue: any;
storage: Storage;
url: string;
}
export type Listener = (ListenerData) => void;
export const STORAGE_TYPE_LOCAL = 'local';
export const STORAGE_TYPE_SESSION = 'session';
export const STORAGE_TYPE_MEMORY = 'memory';
export const DEFAULT_STORAGE_TYPE = STORAGE_TYPE_LOCAL;
export const DEFAULT_PREFIX = 'persist.';
export default class PersistBase {
private readonly _storage: Storage;
private readonly _prefix: string;
private _listeners: Map<string, Set<Listener>>;
constructor(options?: PersistOptions) {
const type = (options && options.type) ? options.type : DEFAULT_STORAGE_TYPE;
const prefix = (options && options.prefix) ? options.prefix : DEFAULT_PREFIX;
if (type == STORAGE_TYPE_LOCAL) {
this._storage = window.localStorage;
} else if (type == STORAGE_TYPE_SESSION) {
this._storage = window.sessionStorage;
} else if (type == STORAGE_TYPE_MEMORY) {
this._storage = new MemoryStorage();
} else {
throw new Error(`Unknown storage type: ${type}`);
}
this._prefix = prefix;
this._listeners = new Map<string, Set<Listener>>();
if (type == STORAGE_TYPE_LOCAL || type == STORAGE_TYPE_SESSION) {
if (window) {
if (window.addEventListener) {
window.addEventListener('storage', this.onStorage, false);
} else {
window['onstorage'] = this.onStorage;
}
} else {
console.warn('The `window` object does not exist.')
}
}
}
private onStorage(event: StorageEvent) {
if (!event || !event.key) {
return;
}
if (!event.key.startsWith(this._prefix)) {
return;
}
const localKey = event.key.substr(this._prefix.length);
const callbacks = this._listeners.get(localKey) as Set<Listener>;
if (!callbacks || callbacks.size == 0) {
return;
}
const _value = (data: string | null) => {
if (!data) {
return null;
}
try {
return JSON.parse(data).value;
} catch (error) {
return null;
}
};
callbacks.forEach((listener: Listener) => {
let data = {} as ListenerData;
data.key = localKey;
data.oldValue = _value(event.oldValue);
data.newValue = _value(event.newValue);
data.storage = this._storage;
data.url = event.url;
listener(data);
});
}
get length(): number {
return this._storage.length;
}
size(): number {
return this.length;
}
addListener(key: string, listener: Listener): void {
if (!this._listeners.has(key)) {
this._listeners.set(key, new Set<Listener>());
}
const callbacks = this._listeners.get(key) as Set<Listener>;
callbacks.add(listener);
}
removeListener(key: string, listener: Listener): void {
if (!this._listeners.has(key)) {
return;
}
const callbacks = this._listeners.get(key) as Set<Listener>;
callbacks.delete(listener);
}
set(key: string, value: any): void {
const stringifyValue = JSON.stringify({
value: value,
datetime: new Date().getTime(),
});
this._storage.setItem(this._prefix + key, stringifyValue);
}
get(key: string, defaultValue: any = undefined): any {
const value = this._storage.getItem(this._prefix + key);
if (value === null) {
return defaultValue;
}
try {
const data = JSON.parse(value);
return data.value;
} catch (error) {
return defaultValue;
}
}
remove(key: string): void {
this._storage.removeItem(this._prefix + key);
}
clear(): void {
if (!this.length) {
return;
}
for (const key of this.keys(true)) {
this._storage.removeItem(key);
}
}
keys(original = false): Array<string> {
let result = new Array<string>();
for (let i = 0; i < this.length; i++) {
const key = this._storage.key(i);
if (key !== null && key.startsWith(this._prefix)) {
if (original) {
result.push(key);
} else {
result.push(key.substr(this._prefix.length));
}
}
}
return result;
}
has(key: string): boolean {
const realKey = this._prefix + key;
for (let i = 0; i < this.length; i++) {
if (realKey == this._storage.key(i)) {
return true;
}
}
return false;
}
}
PersistOptions.ts
PersistRecc.ts
import PersistBase, { STORAGE_TYPE_LOCAL } from '@/persists/PersistBase';
import PersistOptions from '@/persists/PersistOptions';
export const DEFAULT_STORAGE_TYPE = STORAGE_TYPE_LOCAL;
export const DEFAULT_PREFIX = 'recc.';
const KEY_API_ORIGIN = "api.origin"
export default class PersistRecc extends PersistBase {
constructor(options?: PersistOptions) {
let o = {} as PersistOptions;
o.type = (options && options.type) ? options.type : DEFAULT_STORAGE_TYPE;
o.prefix = (options && options.prefix) ? options.prefix : DEFAULT_PREFIX;
super(options);
}
get apiOrigin(): string {
return super.get(KEY_API_ORIGIN, document.location.origin);
}
set apiOrigin(val: string) {
super.set(KEY_API_ORIGIN, val);
}
}
Jest Test Unit
MemoryStorage.spec.ts
import MemoryStorage from '@/persists/MemoryStorage';
describe('MemoryStorage', () => {
let storage = new MemoryStorage();
test('default', () => {
const key0 = 'key0';
const val0 = 'val0';
const key1 = 'key1';
const val1 = 'val1';
expect(storage.length).toEqual(0);
storage.setItem(key0, val0);
expect(storage.length).toEqual(1);
expect(storage.getItem(key0)).toEqual(val0);
expect(storage.key(0)).toEqual(key0)
storage.removeItem(key0);
expect(storage.length).toEqual(0);
storage.setItem(key1, val1);
expect(storage.length).toEqual(1);
storage.clear();
expect(storage.length).toEqual(0);
});
test('errors', () => {
const key0 = 'key0';
const val0 = 'val0';
const val1 = 'val1';
storage.setItem(key0, val0);
storage.setItem(key0, val1);
expect(storage.length).toEqual(1);
expect(storage.getItem(key0)).toEqual(val1);
storage.removeItem(key0);
expect(storage.length).toEqual(0);
storage.removeItem(key0);
expect(storage.length).toEqual(0);
expect(storage.getItem(key0)).toBeNull();
expect(storage.key(0)).toBeNull();
});
});
PersistBase.spec.ts
import PersistBase, { ListenerData } from '@/persists/PersistBase';
describe.each(['memory', 'local', 'session'])('PersistBase', (type) => {
let storage = new PersistBase({type: type, prefix: `test.${type}`});
test(`${type}-default`, () => {
const key0 = 'key0';
const val0 = 'val0';
const key1 = 'key1';
const val1 = 'val1';
expect(storage.length).toEqual(0);
storage.set(key0, val0);
expect(storage.length).toEqual(1);
expect(storage.get(key0)).toEqual(val0);
expect(storage.keys().length).toEqual(1)
expect(storage.keys()[0]).toEqual(key0)
storage.remove(key0);
expect(storage.length).toEqual(0);
storage.set(key1, val1);
expect(storage.length).toEqual(1);
storage.clear();
expect(storage.length).toEqual(0);
});
});