一起来看看响应式会话存储的实现原理

334 阅读6分钟

前言

在日常开发中,我们总会遇到组件或者工具函数复用的问题,如果使用的是相关框架,例如vue/react,则还会遇到公共hooks的复用问题。如果我们将这些公共的函数封装出来,然后发布成一个npm包,就可以在任意项目当中通过安装npm包来使用,今天我就以ew-responsive-store为例,来谈谈如何实现并发布一个完整的npm包。

构建工程环境

构建工程环境通常需要使用rollup.js来构建,而vite内置就是使用rollup.js,因此我们可以选择vite工具来搭建工程环境。

根据vite官网文档,我们可以使用如下命令来搭建一个工程环境。

pnpm create vite [项目名] --template [技术栈]

这里,我们只是实现一个简单的ts函数,因此我们只需要选择原生ts的技术栈即可。即如下:

pnpm create vite ew-responsive-store --template vanilla-ts

修改配置

修改vite配置

工程环境初始化完成,由于我们要使用库模式,而vite刚好提供这个配置,因此我们在根目录下新建一个vite.config.ts,并写上如下配置代码。如下所示:

import { defineConfig } from "vite";

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    lib: {
      entry: "src/core/core",
      name: "ew-responsive-data-store",
      fileName: (format) => `index.${format}.min.js`,
    },
    rollupOptions: {
      external: ['@vue/reactivity']
    }
  }
});

其中lib的entry需要指定打包构建入口,我的入口是src/core/core,包名name叫做ew-responsive-data-store,fileName指定构建的文件名。rollupOptions的external指定忽略构建的包,这里我使用了@vue/reactivity的响应式包,但实际构建是不需要的,需要用户自行引入,因此在这里配置忽略打包,这样也可以减少包的体积。

修改ts配置

由于我们要构建ts类型文件(即.d.ts文件),方便用户在使用该包时有完整的类型推导,因此我们还需要修改tsconfig.json的配置,在tsconfig.json中增加如下配置:

{
  "compilerOptions": {
    //...
    "declaration": true,
    "declarationDir": "./typings",
    "emitDeclarationOnly": true,
     //...
  },
  // ...
}

其中declaration为true代表生成ts定义文件,declarationDir设置输出ts定义文件的目录,为当前根目录下的typings目录,这里指定的目录名是typings,在package.json当中指定typings配置的时候需要与之相同。emitDeclarationOnly为true,则表示只构建xx.d.ts文件,然后emit配置我们需要删除,因为不能和以上的三个配置共存。

修改package.json配置

package.json新增如下配置:

{
  // ...
  "main": "dist/index.umd.min.js",
  "module": "dist/index.es.min.js",
  "typings": "./typings/core/core.d.ts",
  "scripts": {
    "dev": "vite",
    "build": "tsc vite build",
    "preview": "vite preview",
    "tsc": "tsc",
    "release": "npm publish --access public"
  },
  //...
}

主要新增/修改了main、module、typings、scipts的配置,其中main指定用户通过umd模块引入该库的时候导入的构建文件,module则指定通过es模块来导入使用时导入的构建文件,typings指定的则是ts推导导入的ts类型文件。同样的我们增加了tsc命令,用于编译构建ts文件,这里我们在根目录配置了tsconfig.json,因此我们不需要tsc命令后跟一些配置命令,事实上如果我们需要执行构建某个目录下的tsconfig.json时,可以使用tsc --project xx/xxx/tsconfig.json命令,这个命令通常用在多包项目环境中构建ts文件,同样的我们增加了release命令,也就是发布npm包的命令。

编码时刻

在src目录下新建core目录,然后新建core.ts文件,源码主要分成了3个部分。如下:

  1. utils.ts: 使用到的工具函数。
  2. enum.ts: 定义枚举。
  3. core.ts: 核心函数的源码。

枚举

枚举代码主要定义了会话存储的种类以及解析字符串的种类,代码如下所示:

export enum StoreType {
    LOCAL = 'localStorage',
    SESSION = 'sessionStorage',
}

export enum parseStrType {
    EVAL = 'eval',
    JSON = 'json',
}

1. StoreType 枚举

  • StoreType 是一个枚举(enum),包含两个值:
    • LOCAL: 值为 'localStorage',对应浏览器的 localStorage API。localStorage 是一种持久存储机制,数据会在浏览器关闭后依然存在,除非显式删除。
    • SESSION: 值为 'sessionStorage',对应浏览器的 sessionStorage API。sessionStorage 是一种会话存储机制,数据只会在当前浏览器标签页的会话期间有效,浏览器关闭后数据会被清除。
  • 枚举 StoreType 用来标识我们希望使用哪种会话存储方式。

2. parseStrType 枚举

  • parseStrType 是一个枚举(enum),包含两个值:
    • EVAL: 字符串解析使用 eval 方法(字符串转代码执行)。
    • JSON: 字符串解析使用 JSON.parse 方法(将字符串转换为 JSON 对象)。
  • 枚举 parseStrType 用来标识我们希望使用哪种解析方式。

工具函数

第一个版本,工具函数只包含了parseStr函数,而在0.0.1-beta.4版本,我又新增了isValidJSONisStorageEnabled函数。

下面我们分别来看看这3个工具函数的实现。

parseStr函数

我们先来看函数代码如下所示:

import { parseStrType } from "./enum";

export const parseStr = <T>(
    str: string,
    type: parseStrType = parseStrType.JSON,
) => {
    const parseMethod = {
        [parseStrType.EVAL]: <T>(v: string): T => new Function(`return ${v}`)(),
        [parseStrType.JSON]: JSON.parse,
    };
    let res: T | null = null;
    try {
        const method = parseMethod[type];
        if (method) {
            res = method(str);
        }
    } catch (error) {
        console.error(`[rds error]:parse data error,that is ${error}!`);
    }
    return res;
};

这段代码定义了一个类型安全的 parseStr 函数,用于解析字符串数据。根据传入的类型,它支持两种解析方式:使用 eval 或者 JSON.parse。我们逐步分析代码的每个部分。

1. parseStr 函数
export const parseStr = <T>(
    str: string,
    type: parseStrType = parseStrType.JSON,
) => { ... }
  • parseStr 是一个泛型函数(<T>),可以根据输入的类型返回不同的解析结果类型。
  • str: string:传入一个字符串,表示需要解析的目标字符串。
  • type: parseStrType = parseStrType.JSON:这是一个可选的参数,决定解析方式。默认使用 JSON.parse 解析。
  • 返回值类型是 T | null,即泛型类型 Tnullnull 作为默认值,意味着如果解析失败,返回 null
2. parseMethod 对象
const parseMethod = {
    [parseStrType.EVAL]: <T>(v: string): T => new Function(`return ${v}`)(),
    [parseStrType.JSON]: JSON.parse,
};
  • parseMethod 是一个对象,其中:
    • parseStrType.EVAL:对应的值是一个匿名函数,这个函数使用 new Function() 创建一个新的函数并执行 eval,它将字符串 v 转换为 JavaScript 表达式并执行。new Function('return ' + v)() 可以将传入的字符串作为 JavaScript 表达式返回。
    • parseStrType.JSON:对应的值是内建的 JSON.parse,用来解析 JSON 字符串。
3. try...catch 代码块
let res: T | null = null;
try {
    const method = parseMethod[type];
    if (method) {
        res = method(str);
    }
} catch (error) {
    console.error(`[rds error]:parse data error,that is ${error}!`);
}
  • res 用来存储解析后的结果,初始化为 null
  • try 块内:
    • const method = parseMethod[type]:根据传入的 typeparseMethod 对象中获取对应的解析方法。
    • if (method):如果方法存在,调用该方法(method(str))并将返回结果存储在 res 中。
  • 如果在解析过程中出现错误(例如 eval 抛出错误,或者 JSON.parse 失败),会被 catch 捕获,并输出错误信息。
4. 返回值
return res;
  • 如果解析成功,res 将是解析后的结果(类型为 T)。
  • 如果解析失败,res 将是 null,并且错误信息已被打印。
总结

这段代码实现了一个通用的字符串解析函数,支持两种解析方式:

  1. eval 解析:通过动态创建并执行 JavaScript 代码来解析字符串。需要谨慎使用,因为 eval 可以执行恶意代码,可能带来安全风险。
  2. JSON.parse 解析:将字符串解析为 JSON 对象,适用于标准的 JSON 数据。

该函数通过 type 参数选择解析方式,并通过 try...catch 来捕获解析错误。返回值是解析后的结果,类型为泛型 T,或者在解析失败时返回 null

示例使用
const jsonStr = '{"name": "John", "age": 30}';
const evalStr = '({name: "John", age: 30})';

const parsedJson = parseStr(jsonStr, parseStrType.JSON); // { name: "John", age: 30 }
const parsedEval = parseStr(evalStr, parseStrType.EVAL); // { name: "John", age: 30 }
  • parseStr 会根据传入的 type 参数,选择使用 JSON.parseeval 来解析字符串。

isValidJSON函数

该函数代码如下所示:

export const isValidJSON = (val: string) => {
    try {
        const res = JSON.parse(val);
        return res !== null;
    } catch (error) {
        return false;
    }
};

这段代码定义了一个名为 isValidJSON 的函数,用于判断一个字符串是否是有效的 JSON 格式。我们来逐行解读这段代码:

1. 函数定义
export const isValidJSON = (val: string) => { ... }
  • export:将 isValidJSON 函数导出,意味着它可以在其他模块中被导入使用。
  • const:声明一个常量 isValidJSON,它是一个箭头函数(也称为匿名函数)。
  • (val: string):函数的参数是一个名为 val 的字符串,表示我们要验证的输入值。
  • =>:箭头函数的语法,表示定义一个简洁的函数。
2. try-catch 语句
try {
    const res = JSON.parse(val);
    return res !== null;
} catch (error) {
    return false;
}
  • try块:

    • JSON.parse(val):尝试将输入的字符串 val 转换成 JavaScript 对象。JSON.parse 是 JavaScript 内置的函数,用于解析 JSON 格式的字符串并返回对应的 JavaScript 对象。如果 val 是一个有效的 JSON 字符串,JSON.parse 将成功执行,并返回一个 JavaScript 对象。
    • return res !== null;:判断解析后的结果 res 是否为 nullJSON.parse 将字符串 'null' 转换为 JavaScript 中的 null,但有效的 JSON 字符串应该是包含数据的对象或数组。如果 resnull,函数将返回 false,否则返回 true
  • catch 块:

    • 如果 val 不是有效的 JSON 字符串,JSON.parse(val) 会抛出异常,这时会跳转到 catch 块。
    • return false;:在 catch 块中,如果解析失败(即输入的字符串不是有效的 JSON 格式),函数将返回 false
3. 总结功能
  • isValidJSON 函数:用于判断输入的字符串是否是有效的 JSON。
    • 如果字符串是有效的 JSON,且不等于 null,则返回 true
    • 如果字符串不是有效的 JSON(例如格式不正确或不是 JSON 字符串),或者是 null,则返回 false
4. 使用场景

这个函数可以用于检查一个字符串是否能被成功解析为 JSON 对象,常见于:

  • 验证用户输入的数据是否符合 JSON 格式。
  • 检查从服务器接收到的数据是否为有效的 JSON 字符串。
5. 示例:
console.log(isValidJSON('{"name": "John", "age": 30}')); // true
console.log(isValidJSON('{"name": "John", "age": }'));   // false
console.log(isValidJSON('null')); // false
console.log(isValidJSON('[]'));   // true

在这个示例中:

  • {"name": "John", "age": 30} 是一个有效的 JSON 字符串,返回 true
  • {"name": "John", "age": } 是一个无效的 JSON 字符串(缺少值),返回 false
  • 'null' 作为字符串是无效的 JSON,返回 false(注意:null 作为 JavaScript 值是合法的,但在此作为字符串检查时是无效的)。
  • '[]' 是一个有效的空数组,返回 true

isStorageEnabled函数

该函数代码如下所示:

export const isStorageEnabled = (storage: Storage) => {
    try {
        const key = `__storage__test`;
        storage.setItem(key, '');
        storage.removeItem(key);
        return true;
    } catch (e) {
        return false;
    }
};

这段代码定义了一个名为 isStorageEnabled 的函数,旨在检测浏览器的存储(如 localStoragesessionStorage)是否可用。我们来逐行解读这段代码。

1. 函数定义
export const isStorageEnabled = (storage: Storage) => { ... }
  • export:表示该函数是一个模块的导出,可以在其他模块中导入和使用。
  • const:定义了一个常量 isStorageEnabled,并将其赋值为一个箭头函数。
  • (storage: Storage):函数的参数是一个 Storage 类型的对象,表示 Web 存储(例如 localStoragesessionStorage)。这个参数使得该函数可以检查任意类型的 Web 存储。
  • =>:箭头函数语法,用于定义一个匿名函数。
2. 函数主体
try {
    const key = `__storage__test`;
    storage.setItem(key, '');
    storage.removeItem(key);
    return true;
} catch (e) {
    return false;
}
try 块:
  • const key = '__storage__test';:定义了一个名为 key 的字符串常量,值为 '__storage__test',这个键将用于测试存储是否可用。
  • storage.setItem(key, '');:调用 storage 对象的 setItem 方法,尝试将空字符串存入 storage 中,使用 key 作为存储的键。这个操作测试了浏览器的存储是否支持写入。
  • storage.removeItem(key);:调用 storage 对象的 removeItem 方法,移除刚才存入的键值对。这个操作测试了存储是否支持删除数据。

如果没有抛出任何异常,那么 setItemremoveItem 都成功执行,说明存储功能是可用的。

  • return true;:如果 try 块中的代码没有抛出异常,返回 true,表示存储可用。
catch 块:
  • catch (e):如果在 try 块中发生任何错误(例如 storage 不支持某些操作或被禁用),就会跳到 catch 块执行。
  • return false;:在 catch 块中返回 false,表示存储不可用。
3. 总结
  • 功能:isStorageEnabled 函数的作用是检测浏览器是否支持 Web 存储(如 localStoragesessionStorage),并且存储功能是否正常工作。
    • 它通过尝试存储和删除一个临时项来验证存储是否可用。
    • 如果没有错误发生,则认为存储是可用的,返回 true
    • 如果在操作存储时发生异常,则返回 false,表示存储不可用。
4. 使用场景

此函数常用于在 Web 应用中检查 localStoragesessionStorage 是否可用,特别是在以下情况时:

  • 检测浏览器是否禁用了存储(例如在隐私模式下或禁用缓存的情况下)。
  • 在尝试使用浏览器存储之前,确保可以安全地进行读写操作。
  • 在实现需要本地存储支持的功能(如用户设置、会话数据存储等)时,先进行存储可用性的检测。
5. 示例使用:
if (isStorageEnabled(localStorage)) {
    localStorage.setItem('username', 'JohnDoe');
} else {
    console.log('LocalStorage is not available');
}

在这个例子中:

  • 如果 localStorage 可用,则将用户名存入 localStorage
  • 如果 localStorage 不可用(例如浏览器禁用了该功能),则会输出一条提示信息。

核心源码

核心源码如下所示:

import { WatchOptions, ref, watch } from '@vue/reactivity';
import { StoreType } from './enum';
import { isStorageEnabled, isValidJSON, parseStr } from './utils';
export interface StoreOptions extends WatchOptions {
  storage?: StoreType;
}
export function useStorage<T>(
  key: string,
  initialValue: T,
  options: StoreOptions = {
    storage: StoreType.LOCAL,
    immediate: true,
    deep: true
  }
) {
  const { storage = StoreType.LOCAL, immediate = true, deep = true, ...rest } = options;
  const currentStorage =
    storage === StoreType.LOCAL ? localStorage : sessionStorage;
  if (!isStorageEnabled(currentStorage)) {
    throw new Error(`[rds error]:${currentStorage} is not enabled!`)
  }
  const storedValue = currentStorage.getItem(key);
  const data = storedValue && isValidJSON(storedValue) ? parseStr<T>(storedValue)! : initialValue;  
  const value = ref(data);
  value.value = data;
  watch(value, (newValue) => {
    currentStorage.setItem(key, JSON.stringify(newValue));
  }, { immediate, deep, ...rest });

  return value;
}
export { parseStr, isStorageEnabled, isValidJSON }

这段代码定义了一个名为 useStorage 的 Vue 组合式 API 函数,用于将 Vue 组件的状态与浏览器的本地存储(localStoragesessionStorage)进行同步。它支持通过 options 配置项来定制存储类型、监视器的设置等。我们来逐行分析一下代码的功能和实现。

1. 导入模块

import { WatchOptions, ref, watch } from '@vue/reactivity';
import { StoreType } from './enum';
import { isStorageEnabled, isValidJSON, parseStr } from './utils';
  • refwatch:从 @vue/reactivity 中导入,这两个是 Vue 3 的组合式 API,ref 用于创建响应式数据,watch 用于监听响应式数据的变化。
  • StoreType:从 ./enum 中导入一个枚举类型,StoreType 用来指示存储类型(localStoragesessionStorage)。
  • isStorageEnabledisValidJSONparseStr:从 ./utils 中导入了三个辅助函数,分别用于检测存储是否可用、验证字符串是否为有效 JSON 以及解析字符串为指定类型。

2. StoreOptions 接口

export interface StoreOptions extends WatchOptions {
  storage?: StoreType;
}
  • StoreOptions:该接口扩展了 WatchOptions,用于接收存储配置的选项。它允许通过 storage 属性来指定使用的存储类型(localStoragesessionStorage)。
  • storage?: StoreType:可选的 storage 属性,默认使用 StoreType.LOCAL

3. useStorage 函数定义

export function useStorage<T>(
  key: string,
  initialValue: T,
  options: StoreOptions = {
    storage: StoreType.LOCAL,
    immediate: true,
    deep: true
  }
) {
  const { storage = StoreType.LOCAL, immediate = true, deep = true, ...rest } = options;
  • key: string:存储项的键名,用于在 localStoragesessionStorage 中检索或保存数据。
  • initialValue: T:存储项的初始值,若该值在存储中不存在或无效,则使用这个值。
  • options: StoreOptions:存储的配置选项,支持以下字段:
    • storage:指定存储类型,默认为 StoreType.LOCAL(即使用 localStorage)。
    • immediatedeepwatch 的选项,控制是否立即执行回调以及是否深度监听。
  const currentStorage =
    storage === StoreType.LOCAL ? localStorage : sessionStorage;
  • 通过 storage 判断当前使用的是 localStorage 还是 sessionStorage,并将其赋值给 currentStorage

4. 存储可用性检查

  if (!isStorageEnabled(currentStorage)) {
    throw new Error(`[rds error]:${currentStorage} is not enabled!`)
  }
  • 使用 isStorageEnabled(currentStorage) 函数检查当前的存储是否可用。如果不可用,则抛出错误,提醒用户存储不可用。

5. 获取存储的值

  const storedValue = currentStorage.getItem(key);
  const data = storedValue && isValidJSON(storedValue) ? parseStr<T>(storedValue)! : initialValue;  
  • 通过 currentStorage.getItem(key) 获取存储中的值(如果有)。
  • 如果值存在,并且是有效的 JSON 格式(通过 isValidJSON 验证),则使用 parseStr<T>(storedValue) 解析该值。
  • 如果存储中没有值或者值格式无效,则使用 initialValue 作为默认值。

6. 创建响应式数据并设置初始值

  const value = ref(data);
  value.value = data;
  • 使用 ref(data) 创建一个响应式的 value,并将存储的值或初始值赋给它。ref 是 Vue 3 中用于创建响应式数据的方法。

7. 监听数据变化并更新存储

  watch(value, (newValue) => {
    currentStorage.setItem(key, JSON.stringify(newValue));
  }, { immediate, deep, ...rest });
  • 使用 watch 监听 value 的变化。每当 value 发生变化时,就将新的值通过 JSON.stringify(newValue) 序列化为 JSON 字符串,并使用 currentStorage.setItem(key, ...) 更新到存储中。
  • watch 的配置项包括:
    • immediate:是否在数据变化前立即执行回调。
    • deep:是否进行深度监听(即对象或数组内部属性的变化也会被监听)。

8. 返回值

  return value;
}
  • 最后,返回 value,即一个响应式数据对象,Vue 组件可以通过它访问和修改存储中的值。

9. 导出工具函数

export { parseStr, isStorageEnabled, isValidJSON }
  • 导出 parseStrisStorageEnabledisValidJSON 这三个工具函数,使得它们可以在其他地方被重用。

总结

useStorage 函数是一个非常实用的 Vue 3 组合式 API,目的是将 Vue 的响应式数据与浏览器的存储(localStoragesessionStorage)进行同步。它的核心功能包括:

  • 自动加载存储数据:如果存储中存在有效的值,则加载该值作为初始值,否则使用默认的初始值。
  • 自动保存数据:每当响应式数据 value 发生变化时,它会自动将新的值保存到浏览器存储中。
  • 支持存储类型切换:通过 options 可以选择使用 localStoragesessionStorage,并且支持传入 watch 选项来控制数据监听的行为。

使用示例

const userData = useStorage('user', { name: 'John Doe', age: 30 });

watch(() => userData.value, (newData) => {
  console.log('User data updated:', newData);
});

在这个示例中:

  • useStorage 会将 user 的数据与 localStorage(默认)或 sessionStorage 同步。
  • userData 发生变化时,它会自动更新到存储中,同时可以通过 watch 监听 userData 的变化。

发布npm包

然后,我们还需要创建一个忽略不需要发布到npm包中的文件配置,新建一个.npmignore文件,指定如下不需要发布到npm的文件/目录。如下所示:

node_modules/*
.gitignore
vite.config.ts
tests/*
public/*
pnpm-lock.yaml
index.html
vitest.config.ts
vite.config.ts
src/*
tsconfig.json

然后执行npm login登陆自己的npm账号,注意这里需要二次验证,因此需要下载一个app来验证,相关教程可以搜索网上的教程,这不在本文的范畴。

最后,我们只需要执行npm run build生成disttypings目录,然后执行npm run release即可发布npm包。

特别注意: 发布npm包时一定要注意登陆自己的账号。

总结

ew-responsive-store 是一个简单但功能强大的库,它通过封装 localStoragesessionStorage 数据存储,使得这些数据变得响应式,从而简化了开发中的数据管理和状态同步。其简洁的 API 和小巧的体积,使其可以轻松集成到各种前端框架中,甚至是原生 JavaScript 项目。通过配置不同的参数,你可以灵活控制存储类型、监听机制等,极大提高了开发效率。

源码地址:GitHub - ew-responsive-store

如果你觉得这个包对你有帮助,请不吝点赞并分享给更多开发者!