Initial commit
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# Windychen Utils 项目开发记录
|
||||||
|
|
||||||
|
**日期**: 2026-05-25
|
||||||
|
**项目**: windychen-untils (v1.0.0)
|
||||||
|
**类型**: 轻量级 JavaScript 工具库
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、已完成功能模块
|
||||||
|
|
||||||
|
### 1. Object 扩展 (`object.js`)
|
||||||
|
|
||||||
|
| 方法 | 功能 | 特性 |
|
||||||
|
|------|------|------|
|
||||||
|
| `getPro(path, defaultValue?)` | 安全获取嵌套属性 | 支持点分隔路径、特殊键名(含`.`)、混合路径、默认值回退 |
|
||||||
|
| `setPro(path, value)` | 设置嵌套路径值 | 自动创建中间对象,支持特殊键名 |
|
||||||
|
|
||||||
|
**路径格式示例:**
|
||||||
|
```javascript
|
||||||
|
obj.getPro('a.b.c'); // 普通点分隔
|
||||||
|
obj.getPro("['x.y'].z"); // 特殊键名
|
||||||
|
obj.getPro("['a.b'].c['d']"); // 混合嵌套
|
||||||
|
```
|
||||||
|
|
||||||
|
**优化点:**
|
||||||
|
- 提取 `_parsePath()` 公共函数消除重复代码
|
||||||
|
- 使用正则 `\[['"]([^'"]+)['"]\]|[^.\[\]]+` 统一解析所有 key 格式
|
||||||
|
- 最终代码量:26 行(原 47 行,精简 45%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Array 扩展 (`array.js`)
|
||||||
|
|
||||||
|
6 个增强方法,均支持三种调用方式:
|
||||||
|
|
||||||
|
| 方法 | 对应原生 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `findPro(a, b?)` | find() | 查找首个匹配项 |
|
||||||
|
| `filterPro(a, b?)` | filter() | 过滤所有匹配项 |
|
||||||
|
| `findIndexPro(a, b?)` | findIndex() | 返回索引 |
|
||||||
|
| `findLastPro(a, b?)` | findLast() | 从末尾查找 |
|
||||||
|
| **somePro(a, b?)** | some() | 任一满足 |
|
||||||
|
| **everyPro(a, b?)** | every() | 全部满足 |
|
||||||
|
|
||||||
|
**三种调用模式:**
|
||||||
|
```javascript
|
||||||
|
// 模式1: 回调函数
|
||||||
|
arr.findPro(item => item.id > 1);
|
||||||
|
|
||||||
|
// 模式2: 单值精确匹配
|
||||||
|
['aa','bb'].findPro('bb');
|
||||||
|
|
||||||
|
// 模式3: key-value 匹配(支持 getPro 嵌套路径)
|
||||||
|
arr.findPro('user.info.name', 'Alice');
|
||||||
|
```
|
||||||
|
|
||||||
|
**优化点:**
|
||||||
|
- 提取 `_resolveMatcher()` 统一参数判断逻辑
|
||||||
|
- 循环生成 6 个 Pro 方法,零重复代码
|
||||||
|
- key-value 模式接入 `getPro` 支持深层路径访问
|
||||||
|
- 处理 null/undefined 边界(`item != null && ...`)
|
||||||
|
- 最终代码量:12 行(原 64 行,精简 81%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Storage 存储 (`storage.js`)
|
||||||
|
|
||||||
|
浏览器环境专用:
|
||||||
|
|
||||||
|
| 类 | 存储后端 | 特点 |
|
||||||
|
|----|---------|------|
|
||||||
|
| `IDBStorage` | IndexedDB | 大容量存储 |
|
||||||
|
| `ILocalStorage` | localStorage + Map 缓存 | 兼容性好,启动时全量加载 |
|
||||||
|
|
||||||
|
**自动选择策略:**
|
||||||
|
```javascript
|
||||||
|
window.indexedDB ? IDBStorage : (localStorage ? ILocalStorage : null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**API:**
|
||||||
|
- `storage.set(key, value)` → Promise
|
||||||
|
- `storage.get(key, initValue)` → Promise
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、工程化配置
|
||||||
|
|
||||||
|
### 构建系统 (Webpack)
|
||||||
|
|
||||||
|
**配置文件**: `webpack.config.js`(单文件,多模式)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # 全部 (CJS + ESM + UMD)
|
||||||
|
npm run build:cjs # CommonJS 格式
|
||||||
|
npm run build:esm # ES Module 格式
|
||||||
|
npm run build:umd # UMD 浏览器格式
|
||||||
|
npm run dev # 开发模式
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出产物:**
|
||||||
|
|
||||||
|
| 格式 | 文件 | 入口字段 |
|
||||||
|
|------|------|---------|
|
||||||
|
| CJS | dist/windychen-utils.cjs.js | main |
|
||||||
|
| ESM | dist/windychen-utils.esm.js | module |
|
||||||
|
| UMD | dist/windychen-utils.js | browser |
|
||||||
|
|
||||||
|
**演进过程:**
|
||||||
|
1. 初始创建 3 个独立 webpack 配置文件
|
||||||
|
2. 发现 `.esm.js` 后缀被 webpack-cli 误识别为 esm 加载器
|
||||||
|
3. 重命名解决加载异常
|
||||||
|
4. 发现 `experiments` 应在顶层而非 output 内
|
||||||
|
5. **最终收敛为单文件 + --env 参数方案**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
e:\project\utils\
|
||||||
|
├── index.js # 统一入口,聚合各模块
|
||||||
|
├── object.js # Object 原型扩展 (getPro/setPro)
|
||||||
|
├── array.js # Array 原型扩展 (6个Pro方法)
|
||||||
|
├── storage.js # 浏览器存储封装 (IndexedDB/LS)
|
||||||
|
├── webpack.config.js # 打包配置 (单文件多模式)
|
||||||
|
├── package.json # 项目元信息 & 脚本
|
||||||
|
├── .gitignore # Git 忽略规则
|
||||||
|
├── test.js # Node/Bun 端测试
|
||||||
|
├── index.html # 浏览器端测试页面
|
||||||
|
├── README.md # 项目文档
|
||||||
|
└── dist/ # 构建产物输出目录
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、测试体系
|
||||||
|
|
||||||
|
### Node.js / Bun 测试 (`test.js`)
|
||||||
|
- 直接 require 源码运行
|
||||||
|
- 彩色终端输出 (✓/✗)
|
||||||
|
- 覆盖全部 API 的核心场景
|
||||||
|
- 失败时 process.exit(1)
|
||||||
|
- 命令: `npm test`
|
||||||
|
|
||||||
|
### 浏览器测试 (`index.html`)
|
||||||
|
- 引入 dist/windychen-utils.js (UMD)
|
||||||
|
- 深色主题 UI 展示结果
|
||||||
|
- 绿色通过 / 红色失败
|
||||||
|
- 命令: `npm run build:umd && open index.html`
|
||||||
|
|
||||||
|
**已修复 Bug:**
|
||||||
|
- array.js 中 `_resolveMatcher` 未处理 item 为 null/undefined 导致的 TypeError
|
||||||
|
- 修复: `item != null && item.getPro(...)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、关键决策记录
|
||||||
|
|
||||||
|
1. **Prototype 扩展方式**: 直接扩展 Object/Array 原型,使用时无需导入,全局可用
|
||||||
|
2. **路径解析设计**: 正则统一处理普通键和特殊键(含点号),避免分支逻辑
|
||||||
|
3. **Array Pro 方法设计**: 参数重载(function/string+value/value)覆盖常见使用场景
|
||||||
|
4. **Storage 自动降级**: IndexedDB → localStorage → null,最大化兼容性
|
||||||
|
5. **Webpack 单配置**: 通过 --env format 参数控制输出,避免维护多个配置文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、待改进方向
|
||||||
|
|
||||||
|
- [ ] 添加单元测试框架(如 Jest/Vitest)
|
||||||
|
- [ ] 支持 TypeScript 类型声明 (.d.ts)
|
||||||
|
- [ ] Storage 模块添加 delete/clear 方法
|
||||||
|
- [ ] 考虑添加更多工具方法(日期、字符串、数字等)
|
||||||
|
- [ ] CI/CD 自动化构建与发布
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Windychen Utils
|
||||||
|
|
||||||
|
轻量级 JavaScript/TypeScript 工具库,提供对象、数组、存储等常用扩展方法。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install windychen-untils
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
### Node.js / Bun.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
require('windychen-untils');
|
||||||
|
|
||||||
|
// Object 扩展
|
||||||
|
const obj = { a: { b: { c: 1 } } };
|
||||||
|
obj.getPro('a.b.c'); // 1
|
||||||
|
obj.setPro('x.y.z', 'hello');
|
||||||
|
|
||||||
|
// Array 扩展
|
||||||
|
[{ id: '1' }, { id: '2' }].findPro('id', '2');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 浏览器
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="dist/windychen-utils.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ES Module
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import from 'windychen-untils/dist/windychen-utils.esm.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 参考
|
||||||
|
|
||||||
|
### Object
|
||||||
|
|
||||||
|
| 方法 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `getPro(path, default?)` | 安全获取嵌套属性,支持特殊键名 | `obj.getPro('a.b.c')` / `obj.getPro("['x.y'].z")` |
|
||||||
|
| `setPro(path, value)` | 设置嵌套路径值,自动创建中间对象 | `obj.setPro('foo.bar', 1)` |
|
||||||
|
|
||||||
|
**路径格式支持:**
|
||||||
|
- 普通点分隔:`'a.b.c'`
|
||||||
|
- 特殊键名:`"['key.with.dot']"`
|
||||||
|
- 混合:`"['a.b'].c['d.e']"`
|
||||||
|
|
||||||
|
### Array
|
||||||
|
|
||||||
|
| 方法 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `findPro(fn \| key, val?)` | 增强查找 | `arr.findPro('id', '2')` |
|
||||||
|
| `filterPro(fn \| key, val?)` | 增强过滤 | `arr.filterPro('user.name', 'Alice')` |
|
||||||
|
| `findIndexPro(fn \| key, val?)` | 增强索引查找 | `arr.findIndexPro('status', 1)` |
|
||||||
|
| `findLastPro(fn \| key, val?)` | 从末尾查找 | `arr.findLastPro('type', 'b')` |
|
||||||
|
| **somePro(fn \| key, val?)** | 任一匹配 | `arr.somePro('active', true)` |
|
||||||
|
| **everyPro(fn \| key, val?)** | 全部匹配 | `arr.everyPro('valid', true)` |
|
||||||
|
|
||||||
|
**三种调用方式:**
|
||||||
|
```javascript
|
||||||
|
// 回调函数(同原生)
|
||||||
|
arr.findPro(item => item.id > 1);
|
||||||
|
|
||||||
|
// 单值精确匹配
|
||||||
|
['aa', 'bb'].findPro('bb');
|
||||||
|
|
||||||
|
// key-value 匹配(支持嵌套路径)
|
||||||
|
[{ user: { name: 'Bob' } }].findPro('user.name', 'Bob');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage(浏览器环境)
|
||||||
|
|
||||||
|
自动选择存储后端:
|
||||||
|
- 支持 IndexedDB → 使用 `IDBStorage`
|
||||||
|
- 仅支持 localStorage → 使用 `ILocalStorage`(内存缓存)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import storage from 'windychen-untils';
|
||||||
|
|
||||||
|
await storage.set('key', { data: 123 });
|
||||||
|
await storage.get('key', null); // { data: 123 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # 打包全部 (CJS + ESM + UMD)
|
||||||
|
npm run build:cjs # CommonJS
|
||||||
|
npm run build:esm # ES Module
|
||||||
|
npm run build:umd # UMD (浏览器)
|
||||||
|
npm test # 运行测试
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
ISC
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 解析 Pro 方法的匹配器参数
|
||||||
|
* @param {(Function|string|*)} a - 回调函数、属性键、或单值
|
||||||
|
* @param {*=} b - 属性值(与 a 配合使用)
|
||||||
|
* @returns {function(*): boolean} 匹配函数
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const _resolveMatcher = (a, b) => {
|
||||||
|
if (typeof a === 'function') return a;
|
||||||
|
if (typeof a === 'string' && b !== undefined) return (item) => item != null && item.getPro(a) === b;
|
||||||
|
return (item) => item === a;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @private @type {string[]} 支持增强方法的原始方法列表 */
|
||||||
|
const _methods = ['find', 'filter', 'findIndex', 'findLast', 'some', 'every'];
|
||||||
|
|
||||||
|
_methods.forEach((method) => {
|
||||||
|
/**
|
||||||
|
* 增强版数组遍历方法,支持三种调用模式
|
||||||
|
*
|
||||||
|
* **模式1 - 回调函数**(同原生方法)
|
||||||
|
* @example
|
||||||
|
* arr.findPro(item => item.id > 1)
|
||||||
|
* arr.filterPro(item => item.active)
|
||||||
|
*
|
||||||
|
* **模式2 - 单值精确匹配**
|
||||||
|
* @example
|
||||||
|
* ['aa','bb','cc'].findPro('bb')
|
||||||
|
* [1,2,2,3].filterPro(2)
|
||||||
|
*
|
||||||
|
* **模式3 - key-value 匹配**(支持嵌套路径,通过 getPro 访问)
|
||||||
|
* @example
|
||||||
|
* [{ user: { name: 'Bob' } }].findPro('user.name', 'Bob')
|
||||||
|
* arr.filterPro('status', 1)
|
||||||
|
*
|
||||||
|
* @name ${method}Pro
|
||||||
|
* @memberof Array.prototype
|
||||||
|
* @method
|
||||||
|
* @param {(Function|string|*)} a - 回调函数、属性键、或单值
|
||||||
|
* @param {*=} [b] - 属性值(当 a 为 string 时使用)
|
||||||
|
* @returns {*} 返回结果同对应原生方法
|
||||||
|
*/
|
||||||
|
Array.prototype[`${method}Pro`] = function (a, b) {
|
||||||
|
return this[method](_resolveMatcher(a, b));
|
||||||
|
};
|
||||||
|
});
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Windychen Utils - 浏览器测试</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: monospace; padding: 20px; background: #1a1a2e; color: #eee; }
|
||||||
|
.pass { color: #4ade80; }
|
||||||
|
.fail { color: #f87171; }
|
||||||
|
.group { margin: 16px 0; font-weight: bold; color: #60a5fa; }
|
||||||
|
h1 { border-bottom: 2px solid #3b82f6; padding-bottom: 10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Windychen Utils 测试结果</h1>
|
||||||
|
<div id="output"></div>
|
||||||
|
|
||||||
|
<script src="dist/windychen-utils.js"></script>
|
||||||
|
<script>
|
||||||
|
const $ = document.getElementById.bind(document);
|
||||||
|
let passed = 0, failed = 0;
|
||||||
|
|
||||||
|
function assert(desc, condition) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = condition ? 'pass' : 'fail';
|
||||||
|
el.textContent = `${condition ? '✓' : '✗'} ${desc}`;
|
||||||
|
$('output').appendChild(el);
|
||||||
|
condition ? passed++ : failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function group(name) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'group';
|
||||||
|
el.textContent = `\n─── ${name} ───`;
|
||||||
|
$('output').appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Object.getPro 测试 =====
|
||||||
|
group('Object.getPro');
|
||||||
|
const obj = { a: { b: { c: 1 } }, 'x.y': { z: 2 } };
|
||||||
|
|
||||||
|
assert("getPro('a.b.c') === 1", obj.getPro('a.b.c') === 1);
|
||||||
|
assert("getPro('a.b.d', 'default') === 'default'", obj.getPro('a.b.d', 'default') === 'default');
|
||||||
|
assert("getPro(\"['x.y'].z\") === 2", obj.getPro("['x.y'].z") === 2);
|
||||||
|
assert("getPro('missing', 42) === 42", obj.getPro('missing', 42) === 42);
|
||||||
|
|
||||||
|
// ===== Object.setPro 测试 =====
|
||||||
|
group('Object.setPro');
|
||||||
|
const obj2 = {};
|
||||||
|
obj2.setPro('foo.bar.baz', 'hello');
|
||||||
|
assert("setPro 嵌套赋值", obj2.foo.bar.baz === 'hello');
|
||||||
|
|
||||||
|
obj2.setPro("['a.b'].c", 99);
|
||||||
|
assert("setPro 特殊键名", obj2['a.b'].c === 99);
|
||||||
|
|
||||||
|
// ===== Object.getTypeString 测试 =====
|
||||||
|
group('Object.getTypeString');
|
||||||
|
assert("getTypeString('hello') === 'String'", Object.getTypeString('hello') === 'String');
|
||||||
|
assert("getTypeString(123) === 'Number'", Object.getTypeString(123) === 'Number');
|
||||||
|
assert("getTypeString([1,2]) === 'Array'", Object.getTypeString([1,2]) === 'Array');
|
||||||
|
assert("getTypeString(null) === 'Null'", Object.getTypeString(null) === 'Null');
|
||||||
|
assert("getTypeString(undefined) === 'Undefined'", Object.getTypeString(undefined) === 'Undefined');
|
||||||
|
assert("getTypeString({}) === 'Object'", Object.getTypeString({}) === 'Object');
|
||||||
|
assert("getTypeString(true) === 'Boolean'", Object.getTypeString(true) === 'Boolean');
|
||||||
|
|
||||||
|
// ===== Array.findPro 测试 =====
|
||||||
|
group('Array.findPro');
|
||||||
|
const arr = [
|
||||||
|
{ user: { name: 'Alice' } },
|
||||||
|
{ user: { name: 'Bob' } },
|
||||||
|
null,
|
||||||
|
{ user: { name: 'Charlie' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
assert("findPro 回调", [1, 2, 3].findPro(t => t > 1) === 2);
|
||||||
|
assert("findPro key-value", arr.findPro('user.name', 'Bob')?.user?.name === 'Bob');
|
||||||
|
assert("findPro 单值", ['aa', 'bb', 'cc'].findPro('cc') === 'cc');
|
||||||
|
|
||||||
|
// ===== Array.filterPro 测试 =====
|
||||||
|
group('Array.filterPro');
|
||||||
|
assert("filterPro key-value", arr.filterPro('user.name', 'Alice').length === 1);
|
||||||
|
assert("filterPro 单值", [1, 2, 2, 3].filterPro(2).length === 2);
|
||||||
|
|
||||||
|
// ===== Array.findIndexPro 测试 =====
|
||||||
|
group('Array.findIndexPro');
|
||||||
|
assert("findIndexPro", arr.findIndexPro('user.name', 'Charlie') === 3);
|
||||||
|
|
||||||
|
// ===== Array.findLastPro 测试 =====
|
||||||
|
group('Array.findLastPro');
|
||||||
|
assert("findLastPro", arr.findLastPro('user.name', 'Bob')?.user?.name === 'Bob');
|
||||||
|
|
||||||
|
// ===== Array.somePro / everyPro 测试 =====
|
||||||
|
group('Array.somePro / everyPro');
|
||||||
|
assert("somePro", arr.somePro('user.name', 'Alice'));
|
||||||
|
assert("everyPro", !arr.everyPro('user.name', 'Alice'));
|
||||||
|
|
||||||
|
// ===== Storage 测试 =====
|
||||||
|
group('Storage');
|
||||||
|
const storage = new window.IStorage();
|
||||||
|
assert("IStorage 实例存在", storage != null);
|
||||||
|
|
||||||
|
(async function () {
|
||||||
|
await storage.set('test_key', { data: 123, name: 'hello' });
|
||||||
|
assert("storage.set 执行成功", true);
|
||||||
|
|
||||||
|
const val = await storage.get('test_key', null);
|
||||||
|
assert("storage.get 返回值正确", val && val.data === 123 && val.name === 'hello');
|
||||||
|
|
||||||
|
const missing = await storage.get('_not_exist_', 'default');
|
||||||
|
assert("storage.get 缺失键返回默认值", missing === 'default');
|
||||||
|
|
||||||
|
// 汇总
|
||||||
|
const summary = document.createElement('h2');
|
||||||
|
summary.style.marginTop = '20px';
|
||||||
|
summary.innerHTML = `共 ${passed + failed} 项,<span class="pass">${passed} 通过</span>,<span class="fail">${failed} 失败</span>`;
|
||||||
|
$('output').appendChild(summary);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
require('./object');
|
||||||
|
require('./array');
|
||||||
|
|
||||||
|
try { require('./storage'); } catch (e) {} // 仅浏览器环境生效
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 获取值的类型字符串
|
||||||
|
* @param {*} val - 任意值
|
||||||
|
* @returns {string} 类型名称,如 'String' 'Number' 'Array' 'Null' 'Undefined'
|
||||||
|
* @example
|
||||||
|
* Object.getTypeString('hello') // 'String'
|
||||||
|
* Object.getTypeString(123) // 'Number'
|
||||||
|
* Object.getTypeString(null) // 'Null'
|
||||||
|
*/
|
||||||
|
Object.getTypeString = function (val) {
|
||||||
|
return Object.prototype.toString.call(val).replace(/\[object |]/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析路径字符串为键数组
|
||||||
|
* @param {string} path - 路径字符串,支持点分隔、特殊键名、混合格式
|
||||||
|
* @returns {string[]} 键数组
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const _parsePath = (path) => {
|
||||||
|
const keys = [];
|
||||||
|
const keyRegex = /\[['"]([^'"]+)['"]\]|[^.\[\]]+/g;
|
||||||
|
let match;
|
||||||
|
while ((match = keyRegex.exec(path)) !== null) {
|
||||||
|
keys.push(match[1] || match[0]);
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全获取对象嵌套属性
|
||||||
|
* @param {string} path - 属性路径,支持多种格式:
|
||||||
|
* - 普通点分隔: 'a.b.c'
|
||||||
|
* - 特殊键名: "['key.with.dot']"
|
||||||
|
* - 混合: "['a.b'].c['d.e']"
|
||||||
|
* @param {*=} defaultValue - 路径不存在或值为 undefined 时返回的默认值
|
||||||
|
* @returns {*} 找到的属性值或默认值
|
||||||
|
* @example
|
||||||
|
* { a: { b: { c: 1 } } }.getPro('a.b.c') // 1
|
||||||
|
* { a: { b: { c: 1 } } }.getPro('a.x.y', 'default') // 'default'
|
||||||
|
* { 'x.y': { z: 2 } }.getPro("['x.y'].z") // 2
|
||||||
|
*/
|
||||||
|
Object.prototype.getPro = function (path, defaultValue) {
|
||||||
|
let result = this;
|
||||||
|
for (const key of _parsePath(path)) {
|
||||||
|
if (result == null) return defaultValue;
|
||||||
|
result = result[key];
|
||||||
|
}
|
||||||
|
return result !== undefined ? result : defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置对象嵌套路径值(自动创建中间对象)
|
||||||
|
* @param {string} path - 属性路径,格式同 getPro
|
||||||
|
* @param {*} value - 要设置的值
|
||||||
|
* @returns {void}
|
||||||
|
* @example
|
||||||
|
* const obj = {};
|
||||||
|
* obj.setPro('a.b.c', 1); // obj => { a: { b: { c: 1 } } }
|
||||||
|
* obj.setPro("['x.y'].z", 2); // obj => { a: {...}, 'x.y': { z: 2 } }
|
||||||
|
*/
|
||||||
|
Object.prototype.setPro = function (path, value) {
|
||||||
|
const keys = _parsePath(path);
|
||||||
|
let target = this;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
if (target[keys[i]] == null || typeof target[keys[i]] !== 'object') {
|
||||||
|
target[keys[i]] = {};
|
||||||
|
}
|
||||||
|
target = target[keys[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
target[keys[keys.length - 1]] = value;
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "windychen-untils",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "dist/windychen-utils.cjs.js",
|
||||||
|
"module": "dist/windychen-utils.esm.js",
|
||||||
|
"browser": "dist/windychen-utils.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"build:cjs": "webpack --mode production --env format=cjs",
|
||||||
|
"build:esm": "webpack --mode production --env format=esm",
|
||||||
|
"build:umd": "webpack --mode production --env format=umd",
|
||||||
|
"dev": "webpack --mode development",
|
||||||
|
"test": "node test.js"
|
||||||
|
},
|
||||||
|
"author": "windychen",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"webpack": "^5.90.0",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
/** @type {Function} 日志输出 */
|
||||||
|
var log = console.warn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断值是否为 String 类型
|
||||||
|
* @param {*} str - 待检测值
|
||||||
|
* @returns {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
var isString = function (str) { return Object.getTypeString(str) === 'String'; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IndexedDB 存储实现(大容量存储优先选择)
|
||||||
|
*
|
||||||
|
* @class IDBStorage
|
||||||
|
* @example
|
||||||
|
* const storage = new IDBStorage('myDb', 'myStore', 1);
|
||||||
|
* await storage.set('key', { data: 123 });
|
||||||
|
* await storage.get('key', null);
|
||||||
|
*/
|
||||||
|
class IDBStorage {
|
||||||
|
/**
|
||||||
|
* 创建 IndexedDB 存储实例
|
||||||
|
* @param {string} [dbName='dbName'] - 数据库名称
|
||||||
|
* @param {string} [storeName='storeName'] - 对象存储名称
|
||||||
|
* @param {number} [version=1] - 数据库版本号
|
||||||
|
*/
|
||||||
|
constructor(dbName, storeName, version) {
|
||||||
|
/** @type {number} 数据库版本号 */
|
||||||
|
this.version = version || 1;
|
||||||
|
/** @type {string} 数据库名称 */
|
||||||
|
this.dbName = dbName || 'dbName';
|
||||||
|
/** @type {string} 对象存储名称 */
|
||||||
|
this.storeName = storeName || 'storeName';
|
||||||
|
|
||||||
|
var request = indexedDB.open(this.dbName, this.version);
|
||||||
|
/** @type {Promise<IDBDatabase>} 数据库连接 Promise */
|
||||||
|
this.requestPromise = new Promise(function (resolve, reject) {
|
||||||
|
request.onupgradeneeded = function (e) {
|
||||||
|
if (e && e.target && !request.result.objectStoreNames.contains(this.storeName)) {
|
||||||
|
request.result.createObjectStore(this.storeName, { keyPath: 'key' })
|
||||||
|
.createIndex('key', 'key', { unique: false });
|
||||||
|
}
|
||||||
|
log('[Index][open]-upgrade');
|
||||||
|
resolve(request.result);
|
||||||
|
}.bind(this);
|
||||||
|
request.onsuccess = function () { log('[IndexDB][open]-success'); resolve(request.result); };
|
||||||
|
request.onerror = function (e) { log('[IndexDB][open]-error', 2); reject(e); };
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取存储值
|
||||||
|
* @template T
|
||||||
|
* @param {string} key - 键名
|
||||||
|
* @param {T} initValue - 默认值(未找到时返回)
|
||||||
|
* @returns {Promise<T>} 存储的值或默认值
|
||||||
|
*/
|
||||||
|
get(key, initValue) {
|
||||||
|
var _this = this;
|
||||||
|
return new Promise(async function (resolve) {
|
||||||
|
var db = await _this.requestPromise;
|
||||||
|
var transaction = db.transaction(_this.storeName, 'readonly');
|
||||||
|
var objectStore = transaction.objectStore(_this.storeName);
|
||||||
|
var req = objectStore.get(key);
|
||||||
|
req.onsuccess = function () { resolve(req.result ? req.result.value : (initValue || null)); };
|
||||||
|
req.onerror = function (e) { log('[indexDB][get]-error', 2, e); resolve(initValue || null); };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置存储值
|
||||||
|
* @template T
|
||||||
|
* @param {string} key - 键名
|
||||||
|
* @param {T} value - 要存储的值
|
||||||
|
* @returns {Promise<T>} 返回写入的值
|
||||||
|
*/
|
||||||
|
set(key, value) {
|
||||||
|
var _this = this;
|
||||||
|
return new Promise(async function (resolve) {
|
||||||
|
var db = await _this.requestPromise;
|
||||||
|
var transaction = db.transaction(_this.storeName, 'readwrite');
|
||||||
|
var objectStore = transaction.objectStore(_this.storeName);
|
||||||
|
var req = objectStore.put({ key: key, value: value });
|
||||||
|
req.onsuccess = function () { resolve(value); };
|
||||||
|
req.onerror = function (e) { log('[indexDB][set]-error', 2, e); resolve(value); };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* localStorage 存储实现(兼容性好,带内存缓存)
|
||||||
|
*
|
||||||
|
* @class ILocalStorage
|
||||||
|
* @example
|
||||||
|
* const storage = new ILocalStorage();
|
||||||
|
* await storage.set('key', { data: 456 });
|
||||||
|
* await storage.get('key', null);
|
||||||
|
*/
|
||||||
|
class ILocalStorage {
|
||||||
|
constructor() {
|
||||||
|
/** @type {Map<string, *>} 内存缓存 */
|
||||||
|
this.cache = new Map();
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 localStorage 全量加载到缓存 */
|
||||||
|
load() {
|
||||||
|
var _this = this;
|
||||||
|
Object.entries(localStorage).forEach(function (entry) {
|
||||||
|
var key = entry[0], value = entry[1];
|
||||||
|
try {
|
||||||
|
_this.cache.set(key, JSON.parse(value));
|
||||||
|
} catch (error) {
|
||||||
|
log('[localStorage][load]-error', 2, error, key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从缓存获取值
|
||||||
|
* @template T
|
||||||
|
* @param {string} key - 键名
|
||||||
|
* @param {T} initValue - 默认值
|
||||||
|
* @returns {Promise<T>} 缓存的值或默认值
|
||||||
|
*/
|
||||||
|
get(key, initValue) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
resolve(this.cache.get(key) !== undefined ? this.cache.get(key) : initValue);
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入 localStorage 并更新缓存
|
||||||
|
* @template T
|
||||||
|
* @param {string} key - 键名
|
||||||
|
* @param {T} value - 要存储的值
|
||||||
|
* @returns {Promise<T>} 返回写入的值
|
||||||
|
*/
|
||||||
|
set(key, value) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
var _value = isString(value) ? value : '';
|
||||||
|
try {
|
||||||
|
_value = JSON.stringify(value);
|
||||||
|
} catch (e) {
|
||||||
|
log('[localStorage][set]-error', 2, e);
|
||||||
|
}
|
||||||
|
localStorage.setItem(key, _value);
|
||||||
|
this.cache.set(key, _value);
|
||||||
|
resolve(value);
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 浏览器环境自动选择存储方式
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
/**
|
||||||
|
* 当前环境可用的存储类(IDBStorage 或 ILocalStorage)
|
||||||
|
* @type {Function|null}
|
||||||
|
* @global
|
||||||
|
*/
|
||||||
|
window.IStorage = window.indexedDB ? IDBStorage : (window.localStorage ? ILocalStorage : null);
|
||||||
|
if (typeof module !== 'undefined') {
|
||||||
|
module.exports = new window.IStorage();
|
||||||
|
}
|
||||||
|
} else if (typeof module !== 'undefined') {
|
||||||
|
module.exports = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
require('./index');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
function assert(desc, condition) {
|
||||||
|
if (condition) {
|
||||||
|
console.log(`\x1b[32m ✓ ${desc}\x1b[0m`);
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
console.log(`\x1b[31m ✗ ${desc}\x1b[0m`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function group(name) {
|
||||||
|
console.log(`\n\x1b[36m─── ${name} ───\x1b[0m`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Object.getPro =====
|
||||||
|
group('Object.getPro');
|
||||||
|
const obj = { a: { b: { c: 1 } }, 'x.y': { z: 2 } };
|
||||||
|
assert("getPro('a.b.c') === 1", obj.getPro('a.b.c') === 1);
|
||||||
|
assert("getPro('a.b.d', 'default') === 'default'", obj.getPro('a.b.d', 'default') === 'default');
|
||||||
|
assert("getPro(\"['x.y'].z\") === 2", obj.getPro("['x.y'].z") === 2);
|
||||||
|
assert("getPro('missing', 42) === 42", obj.getPro('missing', 42) === 42);
|
||||||
|
|
||||||
|
// ===== Object.setPro =====
|
||||||
|
group('Object.setPro');
|
||||||
|
const obj2 = {};
|
||||||
|
obj2.setPro('foo.bar.baz', 'hello');
|
||||||
|
assert("setPro 嵌套赋值", obj2.foo.bar.baz === 'hello');
|
||||||
|
obj2.setPro("['a.b'].c", 99);
|
||||||
|
assert("setPro 特殊键名", obj2['a.b'].c === 99);
|
||||||
|
|
||||||
|
// ===== Array Pro 方法 =====
|
||||||
|
const arr = [
|
||||||
|
{ user: { name: 'Alice' } },
|
||||||
|
{ user: { name: 'Bob' } },
|
||||||
|
null,
|
||||||
|
{ user: { name: 'Charlie' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
group('Array.findPro');
|
||||||
|
assert("回调 findPro(t=>t>1)", [1, 2, 3].findPro((t) => t > 1) === 2);
|
||||||
|
assert("key-value findPro('user.name','Bob')", arr.findPro('user.name', 'Bob').user.name === 'Bob');
|
||||||
|
assert("单值 findPro('cc')", ['aa', 'bb', 'cc'].findPro('cc') === 'cc');
|
||||||
|
|
||||||
|
group('Array.filterPro');
|
||||||
|
assert("key-value filterPro", arr.filterPro('user.name', 'Alice').length === 1);
|
||||||
|
assert("单值 filterPro(2)", [1, 2, 2, 3].filterPro(2).length === 2);
|
||||||
|
|
||||||
|
group('Array.findIndexPro');
|
||||||
|
assert("findIndexPro", arr.findIndexPro('user.name', 'Charlie') === 3);
|
||||||
|
|
||||||
|
group('Array.findLastPro');
|
||||||
|
assert("findLastPro", arr.findLastPro('user.name', 'Bob').user.name === 'Bob');
|
||||||
|
|
||||||
|
group('Array.somePro / everyPro');
|
||||||
|
assert("somePro", arr.somePro('user.name', 'Alice') === true);
|
||||||
|
assert("everyPro false", arr.everyPro('user.name', 'Alice') === false);
|
||||||
|
assert("everyPro true", ['a', 'a'].everyPro('a') === true);
|
||||||
|
|
||||||
|
// 汇总
|
||||||
|
console.log(`\n\x1b[33m共 ${passed + failed} 项,${passed} 通过,${failed} 失败\x1b[0m`);
|
||||||
|
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const configs = {
|
||||||
|
cjs: {
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'windychen-utils.cjs.js',
|
||||||
|
libraryTarget: 'commonjs2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
esm: {
|
||||||
|
experiments: { outputModule: true },
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'windychen-utils.esm.js',
|
||||||
|
libraryTarget: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
umd: {
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'windychen-utils.js',
|
||||||
|
library: { name: 'WindychenUtils', type: 'umd', export: 'default' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = (env = {}) => {
|
||||||
|
const format = env.format;
|
||||||
|
|
||||||
|
if (!format || format === 'all') {
|
||||||
|
return Object.keys(configs).map((key) => ({
|
||||||
|
name: key,
|
||||||
|
entry: './index.js',
|
||||||
|
...configs[key],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configs[format]) {
|
||||||
|
throw new Error(`不支持的格式: ${format},可选值: cjs, esm, umd`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entry: './index.js', ...configs[format] };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user