前言
在日常开发中,我们总会遇到组件或者工具函数复用的问题,如果使用的是相关框架,例如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个部分。如下:
- utils.ts: 使用到的工具函数。
- enum.ts: 定义枚举。
- core.ts: 核心函数的源码。
枚举
枚举代码主要定义了会话存储的种类以及解析字符串的种类,代码如下所示:
export enum StoreType {
LOCAL = 'localStorage',
SESSION = 'sessionStorage',
}
export enum parseStrType {
EVAL = 'eval',
JSON = 'json',
}
1. StoreType 枚举
StoreType是一个枚举(enum),包含两个值:LOCAL: 值为'localStorage',对应浏览器的localStorageAPI。localStorage是一种持久存储机制,数据会在浏览器关闭后依然存在,除非显式删除。SESSION: 值为'sessionStorage',对应浏览器的sessionStorageAPI。sessionStorage是一种会话存储机制,数据只会在当前浏览器标签页的会话期间有效,浏览器关闭后数据会被清除。
- 枚举
StoreType用来标识我们希望使用哪种会话存储方式。
2. parseStrType 枚举
parseStrType是一个枚举(enum),包含两个值:EVAL: 字符串解析使用eval方法(字符串转代码执行)。JSON: 字符串解析使用JSON.parse方法(将字符串转换为 JSON 对象)。
- 枚举
parseStrType用来标识我们希望使用哪种解析方式。
工具函数
第一个版本,工具函数只包含了parseStr函数,而在0.0.1-beta.4版本,我又新增了isValidJSON和isStorageEnabled函数。
下面我们分别来看看这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,即泛型类型T或null。null作为默认值,意味着如果解析失败,返回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]:根据传入的type从parseMethod对象中获取对应的解析方法。if (method):如果方法存在,调用该方法(method(str))并将返回结果存储在res中。
- 如果在解析过程中出现错误(例如
eval抛出错误,或者JSON.parse失败),会被catch捕获,并输出错误信息。
4. 返回值
return res;
- 如果解析成功,
res将是解析后的结果(类型为T)。 - 如果解析失败,
res将是null,并且错误信息已被打印。
总结
这段代码实现了一个通用的字符串解析函数,支持两种解析方式:
eval解析:通过动态创建并执行 JavaScript 代码来解析字符串。需要谨慎使用,因为eval可以执行恶意代码,可能带来安全风险。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.parse或eval来解析字符串。
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是否为null。JSON.parse将字符串'null'转换为 JavaScript 中的null,但有效的 JSON 字符串应该是包含数据的对象或数组。如果res是null,函数将返回false,否则返回true。
-
catch块:- 如果
val不是有效的 JSON 字符串,JSON.parse(val)会抛出异常,这时会跳转到catch块。 return false;:在catch块中,如果解析失败(即输入的字符串不是有效的 JSON 格式),函数将返回false。
- 如果
3. 总结功能
isValidJSON函数:用于判断输入的字符串是否是有效的 JSON。- 如果字符串是有效的 JSON,且不等于
null,则返回true。 - 如果字符串不是有效的 JSON(例如格式不正确或不是 JSON 字符串),或者是
null,则返回false。
- 如果字符串是有效的 JSON,且不等于
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 的函数,旨在检测浏览器的存储(如 localStorage 或 sessionStorage)是否可用。我们来逐行解读这段代码。
1. 函数定义
export const isStorageEnabled = (storage: Storage) => { ... }
export:表示该函数是一个模块的导出,可以在其他模块中导入和使用。const:定义了一个常量isStorageEnabled,并将其赋值为一个箭头函数。(storage: Storage):函数的参数是一个Storage类型的对象,表示 Web 存储(例如localStorage或sessionStorage)。这个参数使得该函数可以检查任意类型的 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方法,移除刚才存入的键值对。这个操作测试了存储是否支持删除数据。
如果没有抛出任何异常,那么 setItem 和 removeItem 都成功执行,说明存储功能是可用的。
return true;:如果try块中的代码没有抛出异常,返回true,表示存储可用。
catch 块:
catch (e):如果在try块中发生任何错误(例如storage不支持某些操作或被禁用),就会跳到catch块执行。return false;:在catch块中返回false,表示存储不可用。
3. 总结
- 功能:
isStorageEnabled函数的作用是检测浏览器是否支持 Web 存储(如localStorage或sessionStorage),并且存储功能是否正常工作。- 它通过尝试存储和删除一个临时项来验证存储是否可用。
- 如果没有错误发生,则认为存储是可用的,返回
true。 - 如果在操作存储时发生异常,则返回
false,表示存储不可用。
4. 使用场景
此函数常用于在 Web 应用中检查 localStorage 或 sessionStorage 是否可用,特别是在以下情况时:
- 检测浏览器是否禁用了存储(例如在隐私模式下或禁用缓存的情况下)。
- 在尝试使用浏览器存储之前,确保可以安全地进行读写操作。
- 在实现需要本地存储支持的功能(如用户设置、会话数据存储等)时,先进行存储可用性的检测。
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 组件的状态与浏览器的本地存储(localStorage 或 sessionStorage)进行同步。它支持通过 options 配置项来定制存储类型、监视器的设置等。我们来逐行分析一下代码的功能和实现。
1. 导入模块
import { WatchOptions, ref, watch } from '@vue/reactivity';
import { StoreType } from './enum';
import { isStorageEnabled, isValidJSON, parseStr } from './utils';
ref和watch:从@vue/reactivity中导入,这两个是 Vue 3 的组合式 API,ref用于创建响应式数据,watch用于监听响应式数据的变化。StoreType:从./enum中导入一个枚举类型,StoreType用来指示存储类型(localStorage或sessionStorage)。isStorageEnabled、isValidJSON和parseStr:从./utils中导入了三个辅助函数,分别用于检测存储是否可用、验证字符串是否为有效 JSON 以及解析字符串为指定类型。
2. StoreOptions 接口
export interface StoreOptions extends WatchOptions {
storage?: StoreType;
}
StoreOptions:该接口扩展了WatchOptions,用于接收存储配置的选项。它允许通过storage属性来指定使用的存储类型(localStorage或sessionStorage)。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:存储项的键名,用于在localStorage或sessionStorage中检索或保存数据。initialValue: T:存储项的初始值,若该值在存储中不存在或无效,则使用这个值。options: StoreOptions:存储的配置选项,支持以下字段:storage:指定存储类型,默认为StoreType.LOCAL(即使用localStorage)。immediate和deep:watch的选项,控制是否立即执行回调以及是否深度监听。
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 }
- 导出
parseStr、isStorageEnabled和isValidJSON这三个工具函数,使得它们可以在其他地方被重用。
总结
useStorage 函数是一个非常实用的 Vue 3 组合式 API,目的是将 Vue 的响应式数据与浏览器的存储(localStorage 或 sessionStorage)进行同步。它的核心功能包括:
- 自动加载存储数据:如果存储中存在有效的值,则加载该值作为初始值,否则使用默认的初始值。
- 自动保存数据:每当响应式数据
value发生变化时,它会自动将新的值保存到浏览器存储中。 - 支持存储类型切换:通过
options可以选择使用localStorage或sessionStorage,并且支持传入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生成dist和typings目录,然后执行npm run release即可发布npm包。
特别注意: 发布npm包时一定要注意登陆自己的账号。
总结
ew-responsive-store 是一个简单但功能强大的库,它通过封装 localStorage 或 sessionStorage 数据存储,使得这些数据变得响应式,从而简化了开发中的数据管理和状态同步。其简洁的 API 和小巧的体积,使其可以轻松集成到各种前端框架中,甚至是原生 JavaScript 项目。通过配置不同的参数,你可以灵活控制存储类型、监听机制等,极大提高了开发效率。
源码地址:GitHub - ew-responsive-store
如果你觉得这个包对你有帮助,请不吝点赞并分享给更多开发者!