@taicode/common-web 1.1.10 → 1.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/output/catalyst/alert.jsx +55 -0
- package/output/catalyst/auth-layout.jsx +7 -0
- package/output/catalyst/avatar.jsx +45 -0
- package/output/catalyst/badge.jsx +53 -0
- package/output/catalyst/button.jsx +187 -0
- package/output/catalyst/checkbox.jsx +105 -0
- package/output/catalyst/combobox.jsx +120 -0
- package/output/catalyst/description-list.jsx +24 -0
- package/output/catalyst/dialog.jsx +55 -0
- package/output/catalyst/divider.jsx +16 -0
- package/output/catalyst/dropdown.jsx +102 -0
- package/output/catalyst/fieldset.jsx +41 -0
- package/output/catalyst/heading.jsx +22 -0
- package/output/catalyst/input.jsx +73 -0
- package/output/catalyst/link.jsx +14 -0
- package/output/catalyst/listbox.jsx +120 -0
- package/output/catalyst/navbar.jsx +67 -0
- package/output/catalyst/pagination.jsx +52 -0
- package/output/catalyst/radio.jsx +103 -0
- package/output/catalyst/select.jsx +59 -0
- package/output/catalyst/sidebar-layout.jsx +58 -0
- package/output/catalyst/sidebar.jsx +85 -0
- package/output/catalyst/stacked-layout.jsx +55 -0
- package/output/catalyst/switch.jsx +161 -0
- package/output/catalyst/table.jsx +68 -0
- package/output/catalyst/text.jsx +29 -0
- package/output/catalyst/textarea.jsx +49 -0
- package/output/helpers/cache-api/cache-api.d.ts +13 -0
- package/output/helpers/cache-api/cache-api.d.ts.map +1 -0
- package/output/helpers/cache-api/cache-api.js +114 -0
- package/output/helpers/cache-api/cache-api.test.d.ts +2 -0
- package/output/helpers/cache-api/cache-api.test.d.ts.map +1 -0
- package/output/helpers/cache-api/cache-api.test.js +348 -0
- package/output/helpers/cache-api/index.d.ts +2 -0
- package/output/helpers/cache-api/index.d.ts.map +1 -0
- package/output/helpers/cache-api/index.js +1 -0
- package/output/helpers/service/index.d.ts +1 -0
- package/output/helpers/service/index.d.ts.map +1 -0
- package/output/helpers/service/index.js +1 -0
- package/output/helpers/service/service.d.ts +5 -0
- package/output/helpers/service/service.d.ts.map +1 -0
- package/output/helpers/service/service.js +2 -0
- package/output/helpers/side-cache/index.d.ts +2 -0
- package/output/helpers/side-cache/index.d.ts.map +1 -0
- package/output/helpers/side-cache/index.js +1 -0
- package/output/helpers/side-cache/side-cache.d.ts +10 -0
- package/output/helpers/side-cache/side-cache.d.ts.map +1 -0
- package/output/helpers/side-cache/side-cache.js +137 -0
- package/output/helpers/side-cache/side-cache.test.d.ts +2 -0
- package/output/helpers/side-cache/side-cache.test.d.ts.map +1 -0
- package/output/helpers/side-cache/side-cache.test.js +179 -0
- package/output/helpers/use-observer/index.d.ts +2 -0
- package/output/helpers/use-observer/index.d.ts.map +1 -0
- package/output/helpers/use-observer/index.js +1 -0
- package/output/helpers/use-observer/use-observer.d.ts +3 -0
- package/output/helpers/use-observer/use-observer.d.ts.map +1 -0
- package/output/helpers/use-observer/use-observer.js +16 -0
- package/output/helpers/use-observer/use-observer.test.d.ts +2 -0
- package/output/helpers/use-observer/use-observer.test.d.ts.map +1 -0
- package/output/helpers/use-observer/use-observer.test.jsx +134 -0
- package/output/service/service.d.ts +98 -14
- package/output/service/service.d.ts.map +1 -1
- package/output/service/service.js +65 -10
- package/output/service/service.test.jsx +367 -0
- package/output/toaster/index.d.ts +6 -0
- package/output/toaster/index.d.ts.map +1 -1
- package/output/toaster/index.js +37 -21
- package/output/use-observer/use-observer.test.jsx +134 -0
- package/package.json +7 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
36
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
37
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
38
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
39
|
+
};
|
|
40
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
41
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
42
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
43
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
44
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
45
|
+
};
|
|
46
|
+
import { action, computed, observable, runInAction } from 'mobx';
|
|
47
|
+
function isPromiseLike(value) {
|
|
48
|
+
return value != null && typeof value.then === 'function';
|
|
49
|
+
}
|
|
50
|
+
let SideCache = (() => {
|
|
51
|
+
var _a, _SideCache_emptyFlag_accessor_storage, _SideCache_currentKey_accessor_storage, _SideCache_cache_accessor_storage;
|
|
52
|
+
var _b, _c;
|
|
53
|
+
let _instanceExtraInitializers = [];
|
|
54
|
+
let _emptyFlag_decorators;
|
|
55
|
+
let _emptyFlag_initializers = [];
|
|
56
|
+
let _emptyFlag_extraInitializers = [];
|
|
57
|
+
let _currentKey_decorators;
|
|
58
|
+
let _currentKey_initializers = [];
|
|
59
|
+
let _currentKey_extraInitializers = [];
|
|
60
|
+
let _cache_decorators;
|
|
61
|
+
let _cache_initializers = [];
|
|
62
|
+
let _cache_extraInitializers = [];
|
|
63
|
+
let _get_value_decorators;
|
|
64
|
+
let _get_empty_decorators;
|
|
65
|
+
let _handle_decorators;
|
|
66
|
+
return _a = class SideCache {
|
|
67
|
+
get emptyFlag() { return __classPrivateFieldGet(this, _SideCache_emptyFlag_accessor_storage, "f"); }
|
|
68
|
+
set emptyFlag(value) { __classPrivateFieldSet(this, _SideCache_emptyFlag_accessor_storage, value, "f"); }
|
|
69
|
+
get currentKey() { return __classPrivateFieldGet(this, _SideCache_currentKey_accessor_storage, "f"); }
|
|
70
|
+
set currentKey(value) { __classPrivateFieldSet(this, _SideCache_currentKey_accessor_storage, value, "f"); }
|
|
71
|
+
get cache() { return __classPrivateFieldGet(this, _SideCache_cache_accessor_storage, "f"); }
|
|
72
|
+
set cache(value) { __classPrivateFieldSet(this, _SideCache_cache_accessor_storage, value, "f"); }
|
|
73
|
+
get value() {
|
|
74
|
+
if (this.currentKey == null)
|
|
75
|
+
return undefined;
|
|
76
|
+
const cacheItem = this.cache[this.currentKey];
|
|
77
|
+
return cacheItem ? cacheItem.data : undefined;
|
|
78
|
+
}
|
|
79
|
+
get empty() {
|
|
80
|
+
return this.emptyFlag;
|
|
81
|
+
}
|
|
82
|
+
handle(key, func) {
|
|
83
|
+
var _b;
|
|
84
|
+
// 不需要跨设备、软件一致,所以直接用 JSON.stringify
|
|
85
|
+
// 但需要处理 JSON.stringify(undefined) 返回 undefined 的情况
|
|
86
|
+
const keyStringify = (_b = JSON.stringify(key)) !== null && _b !== void 0 ? _b : 'undefined';
|
|
87
|
+
// 这会让外面立马可以拿到之前缓存的值,如果存在的话
|
|
88
|
+
runInAction(() => this.currentKey = keyStringify);
|
|
89
|
+
const funcReturn = func();
|
|
90
|
+
if (isPromiseLike(funcReturn)) {
|
|
91
|
+
return funcReturn.then(result => runInAction(() => {
|
|
92
|
+
this.emptyFlag = false;
|
|
93
|
+
this.cache = Object.assign(Object.assign({}, this.cache), { [keyStringify]: {
|
|
94
|
+
data: result,
|
|
95
|
+
createTime: new Date().toISOString()
|
|
96
|
+
} });
|
|
97
|
+
return result;
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
runInAction(() => {
|
|
101
|
+
this.emptyFlag = false;
|
|
102
|
+
this.cache = Object.assign(Object.assign({}, this.cache), { [keyStringify]: {
|
|
103
|
+
data: funcReturn,
|
|
104
|
+
createTime: new Date().toISOString()
|
|
105
|
+
} });
|
|
106
|
+
});
|
|
107
|
+
return funcReturn;
|
|
108
|
+
}
|
|
109
|
+
constructor() {
|
|
110
|
+
_SideCache_emptyFlag_accessor_storage.set(this, (__runInitializers(this, _instanceExtraInitializers), __runInitializers(this, _emptyFlag_initializers, true)));
|
|
111
|
+
_SideCache_currentKey_accessor_storage.set(this, (__runInitializers(this, _emptyFlag_extraInitializers), __runInitializers(this, _currentKey_initializers, void 0)));
|
|
112
|
+
_SideCache_cache_accessor_storage.set(this, (__runInitializers(this, _currentKey_extraInitializers), __runInitializers(this, _cache_initializers, {})));
|
|
113
|
+
__runInitializers(this, _cache_extraInitializers);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
_SideCache_emptyFlag_accessor_storage = new WeakMap(),
|
|
117
|
+
_SideCache_currentKey_accessor_storage = new WeakMap(),
|
|
118
|
+
_SideCache_cache_accessor_storage = new WeakMap(),
|
|
119
|
+
(() => {
|
|
120
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
121
|
+
_emptyFlag_decorators = [observable];
|
|
122
|
+
_currentKey_decorators = [observable];
|
|
123
|
+
_cache_decorators = [(_b = observable).ref.bind(_b)];
|
|
124
|
+
_get_value_decorators = [computed];
|
|
125
|
+
_get_empty_decorators = [computed];
|
|
126
|
+
_handle_decorators = [(_c = action).bound.bind(_c)];
|
|
127
|
+
__esDecorate(_a, null, _emptyFlag_decorators, { kind: "accessor", name: "emptyFlag", static: false, private: false, access: { has: obj => "emptyFlag" in obj, get: obj => obj.emptyFlag, set: (obj, value) => { obj.emptyFlag = value; } }, metadata: _metadata }, _emptyFlag_initializers, _emptyFlag_extraInitializers);
|
|
128
|
+
__esDecorate(_a, null, _currentKey_decorators, { kind: "accessor", name: "currentKey", static: false, private: false, access: { has: obj => "currentKey" in obj, get: obj => obj.currentKey, set: (obj, value) => { obj.currentKey = value; } }, metadata: _metadata }, _currentKey_initializers, _currentKey_extraInitializers);
|
|
129
|
+
__esDecorate(_a, null, _cache_decorators, { kind: "accessor", name: "cache", static: false, private: false, access: { has: obj => "cache" in obj, get: obj => obj.cache, set: (obj, value) => { obj.cache = value; } }, metadata: _metadata }, _cache_initializers, _cache_extraInitializers);
|
|
130
|
+
__esDecorate(_a, null, _get_value_decorators, { kind: "getter", name: "value", static: false, private: false, access: { has: obj => "value" in obj, get: obj => obj.value }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
131
|
+
__esDecorate(_a, null, _get_empty_decorators, { kind: "getter", name: "empty", static: false, private: false, access: { has: obj => "empty" in obj, get: obj => obj.empty }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
132
|
+
__esDecorate(_a, null, _handle_decorators, { kind: "method", name: "handle", static: false, private: false, access: { has: obj => "handle" in obj, get: obj => obj.handle }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
133
|
+
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
134
|
+
})(),
|
|
135
|
+
_a;
|
|
136
|
+
})();
|
|
137
|
+
export { SideCache };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"side-cache.test.d.ts","sourceRoot":"","sources":["../../../source/helpers/side-cache/side-cache.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { SideCache } from './side-cache';
|
|
3
|
+
describe('SideCache', () => {
|
|
4
|
+
let cache;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
cache = new SideCache();
|
|
7
|
+
});
|
|
8
|
+
describe('初始状态', () => {
|
|
9
|
+
it('应该初始化为空状态', () => {
|
|
10
|
+
expect(cache.empty).toBe(true);
|
|
11
|
+
expect(cache.value).toBe(undefined);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe('同步缓存', () => {
|
|
15
|
+
it('应该能够缓存同步函数的结果', () => {
|
|
16
|
+
const mockFunc = vi.fn(() => 'test-result');
|
|
17
|
+
const key = 'test-key';
|
|
18
|
+
const result = cache.handle(key, mockFunc);
|
|
19
|
+
expect(result).toBe('test-result');
|
|
20
|
+
expect(mockFunc).toHaveBeenCalledTimes(1);
|
|
21
|
+
expect(cache.empty).toBe(false);
|
|
22
|
+
expect(cache.value).toBe('test-result');
|
|
23
|
+
});
|
|
24
|
+
it('应该对相同 key 返回缓存的结果', () => {
|
|
25
|
+
const mockFunc = vi.fn(() => 'cached-result');
|
|
26
|
+
const key = 'same-key';
|
|
27
|
+
// 第一次调用
|
|
28
|
+
const result1 = cache.handle(key, mockFunc);
|
|
29
|
+
expect(result1).toBe('cached-result');
|
|
30
|
+
expect(mockFunc).toHaveBeenCalledTimes(1);
|
|
31
|
+
// 第二次调用相同 key,应该直接返回缓存
|
|
32
|
+
const result2 = cache.handle(key, mockFunc);
|
|
33
|
+
expect(result2).toBe('cached-result');
|
|
34
|
+
expect(mockFunc).toHaveBeenCalledTimes(2); // 函数仍会被调用,但会更新缓存
|
|
35
|
+
expect(cache.value).toBe('cached-result');
|
|
36
|
+
});
|
|
37
|
+
it('应该对不同 key 分别缓存', () => {
|
|
38
|
+
const mockFunc1 = vi.fn(() => 'result1');
|
|
39
|
+
const mockFunc2 = vi.fn(() => 'result2');
|
|
40
|
+
cache.handle('key1', mockFunc1);
|
|
41
|
+
expect(cache.value).toBe('result1');
|
|
42
|
+
cache.handle('key2', mockFunc2);
|
|
43
|
+
expect(cache.value).toBe('result2');
|
|
44
|
+
// 切换回 key1
|
|
45
|
+
cache.handle('key1', mockFunc1);
|
|
46
|
+
expect(cache.value).toBe('result1');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('异步缓存', () => {
|
|
50
|
+
it('应该能够缓存异步函数的结果', async () => {
|
|
51
|
+
const mockAsyncFunc = vi.fn(async () => 'async-result');
|
|
52
|
+
const key = 'async-key';
|
|
53
|
+
const promise = cache.handle(key, mockAsyncFunc);
|
|
54
|
+
expect(promise).toBeInstanceOf(Promise);
|
|
55
|
+
const result = await promise;
|
|
56
|
+
expect(result).toBe('async-result');
|
|
57
|
+
expect(mockAsyncFunc).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(cache.empty).toBe(false);
|
|
59
|
+
expect(cache.value).toBe('async-result');
|
|
60
|
+
});
|
|
61
|
+
it('应该在异步函数执行期间立即设置 currentKey', async () => {
|
|
62
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
63
|
+
const mockAsyncFunc = vi.fn(async () => {
|
|
64
|
+
await delay(10);
|
|
65
|
+
return 'delayed-result';
|
|
66
|
+
});
|
|
67
|
+
const key = 'delayed-key';
|
|
68
|
+
// 在异步函数开始执行时,currentKey 应该已经设置
|
|
69
|
+
const promise = cache.handle(key, mockAsyncFunc);
|
|
70
|
+
// 此时异步函数还在执行,但 currentKey 已设置
|
|
71
|
+
// 如果之前有缓存,value 应该能立即获取到
|
|
72
|
+
const result = await promise;
|
|
73
|
+
expect(result).toBe('delayed-result');
|
|
74
|
+
expect(cache.value).toBe('delayed-result');
|
|
75
|
+
});
|
|
76
|
+
it('应该处理异步函数的错误', async () => {
|
|
77
|
+
const error = new Error('Async error');
|
|
78
|
+
const mockAsyncFunc = vi.fn(async () => {
|
|
79
|
+
throw error;
|
|
80
|
+
});
|
|
81
|
+
const key = 'error-key';
|
|
82
|
+
await expect(cache.handle(key, mockAsyncFunc)).rejects.toThrow('Async error');
|
|
83
|
+
expect(mockAsyncFunc).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('key 序列化', () => {
|
|
87
|
+
it('应该正确处理不同类型的 key', () => {
|
|
88
|
+
const mockFunc = vi.fn(() => 'result');
|
|
89
|
+
// 字符串 key
|
|
90
|
+
cache.handle('string-key', mockFunc);
|
|
91
|
+
expect(cache.value).toBe('result');
|
|
92
|
+
// 数字 key
|
|
93
|
+
cache.handle(123, mockFunc);
|
|
94
|
+
expect(cache.value).toBe('result');
|
|
95
|
+
// 对象 key
|
|
96
|
+
cache.handle({ id: 1, name: 'test' }, mockFunc);
|
|
97
|
+
expect(cache.value).toBe('result');
|
|
98
|
+
// 数组 key
|
|
99
|
+
cache.handle([1, 2, 3], mockFunc);
|
|
100
|
+
expect(cache.value).toBe('result');
|
|
101
|
+
});
|
|
102
|
+
it('相同内容的对象 key 应该被视为相同', () => {
|
|
103
|
+
const mockFunc = vi.fn(() => 'object-result');
|
|
104
|
+
const key1 = { id: 1, name: 'test' };
|
|
105
|
+
const key2 = { id: 1, name: 'test' };
|
|
106
|
+
cache.handle(key1, mockFunc);
|
|
107
|
+
const result1 = cache.value;
|
|
108
|
+
cache.handle(key2, mockFunc);
|
|
109
|
+
const result2 = cache.value;
|
|
110
|
+
expect(result1).toBe(result2);
|
|
111
|
+
expect(result1).toBe('object-result');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('缓存时间戳', () => {
|
|
115
|
+
it('应该为缓存项添加创建时间', () => {
|
|
116
|
+
const mockFunc = vi.fn(() => 'timestamped-result');
|
|
117
|
+
const key = 'timestamp-key';
|
|
118
|
+
const beforeTime = new Date().toISOString();
|
|
119
|
+
cache.handle(key, mockFunc);
|
|
120
|
+
const afterTime = new Date().toISOString();
|
|
121
|
+
// 访问私有属性进行测试(仅用于测试目的)
|
|
122
|
+
const cacheData = cache.cache;
|
|
123
|
+
const cachedItem = cacheData[JSON.stringify(key)];
|
|
124
|
+
expect(cachedItem).toBeDefined();
|
|
125
|
+
expect(cachedItem.data).toBe('timestamped-result');
|
|
126
|
+
expect(cachedItem.createTime).toBeDefined();
|
|
127
|
+
expect(typeof cachedItem.createTime).toBe('string');
|
|
128
|
+
// 验证时间戳在合理范围内
|
|
129
|
+
expect(cachedItem.createTime >= beforeTime).toBe(true);
|
|
130
|
+
expect(cachedItem.createTime <= afterTime).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('边界情况', () => {
|
|
134
|
+
it('应该处理 null 和 undefined key', () => {
|
|
135
|
+
const mockFunc1 = vi.fn(() => 'null-key-result');
|
|
136
|
+
const mockFunc2 = vi.fn(() => 'undefined-key-result');
|
|
137
|
+
cache.handle(null, mockFunc1);
|
|
138
|
+
expect(cache.value).toBe('null-key-result');
|
|
139
|
+
cache.handle(undefined, mockFunc2);
|
|
140
|
+
expect(cache.value).toBe('undefined-key-result');
|
|
141
|
+
});
|
|
142
|
+
it('应该处理返回 null 或 undefined 的函数', () => {
|
|
143
|
+
const nullFunc = vi.fn(() => null);
|
|
144
|
+
const undefinedFunc = vi.fn(() => undefined);
|
|
145
|
+
cache.handle('null-key', nullFunc);
|
|
146
|
+
expect(cache.value).toBe(null);
|
|
147
|
+
expect(cache.empty).toBe(false);
|
|
148
|
+
cache.handle('undefined-key', undefinedFunc);
|
|
149
|
+
expect(cache.value).toBe(undefined);
|
|
150
|
+
expect(cache.empty).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
it('应该处理返回 Promise.resolve(null) 的异步函数', async () => {
|
|
153
|
+
const nullAsyncFunc = vi.fn(async () => null);
|
|
154
|
+
const result = await cache.handle('async-null', nullAsyncFunc);
|
|
155
|
+
expect(result).toBe(null);
|
|
156
|
+
expect(cache.value).toBe(null);
|
|
157
|
+
expect(cache.empty).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('类型检查', () => {
|
|
161
|
+
it('应该正确识别 Promise-like 对象', async () => {
|
|
162
|
+
let thenCallback;
|
|
163
|
+
const promiseLike = {
|
|
164
|
+
then: vi.fn((callback) => {
|
|
165
|
+
thenCallback = callback;
|
|
166
|
+
return promiseLike;
|
|
167
|
+
})
|
|
168
|
+
};
|
|
169
|
+
const mockFunc = vi.fn(() => promiseLike);
|
|
170
|
+
const result = cache.handle('promise-like', mockFunc);
|
|
171
|
+
expect(result).toBe(promiseLike);
|
|
172
|
+
expect(promiseLike.then).toHaveBeenCalled();
|
|
173
|
+
// 模拟 Promise 完成
|
|
174
|
+
if (thenCallback) {
|
|
175
|
+
thenCallback('resolved-value');
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../source/helpers/use-observer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useObserver } from './use-observer';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-observer.d.ts","sourceRoot":"","sources":["../../../source/helpers/use-observer/use-observer.ts"],"names":[],"mappings":"AAGA,wBAAgB,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,CAAC,CAAA;AAClD,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { reaction } from 'mobx';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
export function useObserver(initialValue, selector) {
|
|
4
|
+
const [, refresh] = useState({});
|
|
5
|
+
React.useEffect(() => {
|
|
6
|
+
if (initialValue == null)
|
|
7
|
+
return;
|
|
8
|
+
if (selector == null)
|
|
9
|
+
return;
|
|
10
|
+
return reaction(() => selector(initialValue), () => refresh({}));
|
|
11
|
+
}, [selector, initialValue]);
|
|
12
|
+
if (selector == null) {
|
|
13
|
+
return initialValue;
|
|
14
|
+
}
|
|
15
|
+
return selector(initialValue);
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-observer.test.d.ts","sourceRoot":"","sources":["../../../source/helpers/use-observer/use-observer.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import { observable, action } from 'mobx';
|
|
4
|
+
import { useObserver } from './use-observer';
|
|
5
|
+
describe('useObserver', () => {
|
|
6
|
+
it('应该返回初始值当没有selector时', () => {
|
|
7
|
+
const initialValue = '初始值';
|
|
8
|
+
const { result } = renderHook(() => useObserver(initialValue));
|
|
9
|
+
expect(result.current).toBe(initialValue);
|
|
10
|
+
});
|
|
11
|
+
it('应该使用selector转换值', () => {
|
|
12
|
+
const initialValue = { count: 5 };
|
|
13
|
+
const selector = (value) => value.count * 2;
|
|
14
|
+
const { result } = renderHook(() => useObserver(initialValue, selector));
|
|
15
|
+
expect(result.current).toBe(10);
|
|
16
|
+
});
|
|
17
|
+
it('应该在观察对象变化时重新渲染', async () => {
|
|
18
|
+
const observableStore = observable({
|
|
19
|
+
count: 0,
|
|
20
|
+
increment: action(() => {
|
|
21
|
+
observableStore.count++;
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
const { result } = renderHook(() => useObserver(observableStore, (store) => store.count));
|
|
25
|
+
expect(result.current).toBe(0);
|
|
26
|
+
// 改变observable的值
|
|
27
|
+
act(() => {
|
|
28
|
+
observableStore.increment();
|
|
29
|
+
});
|
|
30
|
+
// 等待React更新
|
|
31
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
32
|
+
expect(result.current).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
it('应该处理复杂的selector函数', () => {
|
|
35
|
+
const store = observable({
|
|
36
|
+
users: [
|
|
37
|
+
{ id: 1, name: '张三', active: true },
|
|
38
|
+
{ id: 2, name: '李四', active: false },
|
|
39
|
+
{ id: 3, name: '王五', active: true }
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
const selector = (store) => store.users.filter((user) => user.active).map((user) => user.name);
|
|
43
|
+
const { result } = renderHook(() => useObserver(store, selector));
|
|
44
|
+
expect(result.current).toEqual(['张三', '王五']);
|
|
45
|
+
});
|
|
46
|
+
it('应该在observable数组变化时重新渲染', async () => {
|
|
47
|
+
const store = observable({
|
|
48
|
+
items: ['项目1', '项目2'],
|
|
49
|
+
addItem: action((item) => {
|
|
50
|
+
store.items.push(item);
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
const { result } = renderHook(() => useObserver(store, (store) => store.items.length));
|
|
54
|
+
expect(result.current).toBe(2);
|
|
55
|
+
act(() => {
|
|
56
|
+
store.addItem('项目3');
|
|
57
|
+
});
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
59
|
+
expect(result.current).toBe(3);
|
|
60
|
+
});
|
|
61
|
+
it('应该处理null值的初始值', () => {
|
|
62
|
+
const { result } = renderHook(() => useObserver(null));
|
|
63
|
+
expect(result.current).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
it('应该处理undefined值的初始值', () => {
|
|
66
|
+
const { result } = renderHook(() => useObserver(undefined));
|
|
67
|
+
expect(result.current).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
it('应该在selector为null时返回原值', () => {
|
|
70
|
+
const initialValue = { data: '测试数据' };
|
|
71
|
+
const { result } = renderHook(() => useObserver(initialValue, null));
|
|
72
|
+
expect(result.current).toBe(initialValue);
|
|
73
|
+
});
|
|
74
|
+
it('应该处理嵌套observable对象', async () => {
|
|
75
|
+
const store = observable({
|
|
76
|
+
user: {
|
|
77
|
+
profile: {
|
|
78
|
+
name: '测试用户',
|
|
79
|
+
email: 'test@example.com'
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
updateName: action((name) => {
|
|
83
|
+
store.user.profile.name = name;
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
const { result } = renderHook(() => useObserver(store, (store) => store.user.profile.name));
|
|
87
|
+
expect(result.current).toBe('测试用户');
|
|
88
|
+
act(() => {
|
|
89
|
+
store.updateName('新用户名');
|
|
90
|
+
});
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
92
|
+
expect(result.current).toBe('新用户名');
|
|
93
|
+
});
|
|
94
|
+
it('应该处理多个observable属性的变化', async () => {
|
|
95
|
+
const store = observable({
|
|
96
|
+
firstName: '张',
|
|
97
|
+
lastName: '三',
|
|
98
|
+
updateFirstName: action((name) => {
|
|
99
|
+
store.firstName = name;
|
|
100
|
+
}),
|
|
101
|
+
updateLastName: action((name) => {
|
|
102
|
+
store.lastName = name;
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
const { result } = renderHook(() => useObserver(store, (store) => `${store.firstName}${store.lastName}`));
|
|
106
|
+
expect(result.current).toBe('张三');
|
|
107
|
+
act(() => {
|
|
108
|
+
store.updateFirstName('李');
|
|
109
|
+
});
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
111
|
+
expect(result.current).toBe('李三');
|
|
112
|
+
act(() => {
|
|
113
|
+
store.updateLastName('四');
|
|
114
|
+
});
|
|
115
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
116
|
+
expect(result.current).toBe('李四');
|
|
117
|
+
});
|
|
118
|
+
it('应该在组件卸载时清理reaction', () => {
|
|
119
|
+
const store = observable({ count: 0 });
|
|
120
|
+
const { unmount } = renderHook(() => useObserver(store, (store) => store.count));
|
|
121
|
+
// 卸载组件应该不会抛出错误
|
|
122
|
+
expect(() => unmount()).not.toThrow();
|
|
123
|
+
});
|
|
124
|
+
it('应该处理boolean类型的返回值', () => {
|
|
125
|
+
const store = observable({ isActive: true });
|
|
126
|
+
const { result } = renderHook(() => useObserver(store, (store) => store.isActive));
|
|
127
|
+
expect(result.current).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it('应该处理数字类型的返回值', () => {
|
|
130
|
+
const store = observable({ count: 42 });
|
|
131
|
+
const { result } = renderHook(() => useObserver(store, (store) => store.count));
|
|
132
|
+
expect(result.current).toBe(42);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -35,13 +35,18 @@
|
|
|
35
35
|
import { Token, Container, Provider } from '@needle-di/core';
|
|
36
36
|
import React from 'react';
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* 使用服务 Hook
|
|
39
39
|
*
|
|
40
40
|
* 该 Hook 从 DI 容器中获取指定的服务实例,支持可选的 selector 函数来选择需要的数据。
|
|
41
41
|
* 当选择的数据发生变化时,组件会自动重新渲染。
|
|
42
42
|
*
|
|
43
|
+
* 该 Hook 可以获取三种类型的服务:
|
|
44
|
+
* 1. 全局服务:通过 ServiceProvider 类注入的服务
|
|
45
|
+
* 2. 值注入服务:通过 ServiceProvider 值注入({ provide, useValue })的服务实例
|
|
46
|
+
* 3. 父级容器中的任何服务:支持多层嵌套的 ServiceProvider
|
|
47
|
+
*
|
|
43
48
|
* 特性:
|
|
44
|
-
* -
|
|
49
|
+
* - 从容器获取服务实例
|
|
45
50
|
* - 支持 Mobx 响应式更新
|
|
46
51
|
* - 支持可选 selector,不传时监听整个服务的所有变化
|
|
47
52
|
* - 智能检测 selector 返回值,优化响应式监听策略
|
|
@@ -56,19 +61,49 @@ import React from 'react';
|
|
|
56
61
|
*
|
|
57
62
|
* @example
|
|
58
63
|
* ```tsx
|
|
59
|
-
* //
|
|
64
|
+
* // 场景 1: 使用全局服务(类注入)
|
|
65
|
+
* // App.tsx
|
|
66
|
+
* <ServiceProvider services={[UserService]}>
|
|
67
|
+
* <UserList />
|
|
68
|
+
* </ServiceProvider>
|
|
69
|
+
*
|
|
70
|
+
* // UserList.tsx
|
|
60
71
|
* const users = useService(UserService, service => service.users)
|
|
72
|
+
* const userService = useService(UserService) // 监听整个服务的所有变化
|
|
61
73
|
*
|
|
62
|
-
* //
|
|
63
|
-
*
|
|
74
|
+
* // 场景 2: 使用值注入的共享服务实例
|
|
75
|
+
* // UserPage.tsx
|
|
76
|
+
* function UserPage() {
|
|
77
|
+
* const localService = useMemo(() => {
|
|
78
|
+
* const service = new LocalUserService()
|
|
79
|
+
* service.init()
|
|
80
|
+
* return service
|
|
81
|
+
* }, [])
|
|
64
82
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
83
|
+
* return (
|
|
84
|
+
* <ServiceProvider services={[
|
|
85
|
+
* { provide: LocalUserService, useValue: localService }
|
|
86
|
+
* ]}>
|
|
87
|
+
* <UserList />
|
|
88
|
+
* <UserDetail />
|
|
89
|
+
* </ServiceProvider>
|
|
90
|
+
* )
|
|
91
|
+
* }
|
|
67
92
|
*
|
|
68
|
-
* //
|
|
69
|
-
*
|
|
93
|
+
* // UserList.tsx 和 UserDetail.tsx 共享同一个 localService 实例
|
|
94
|
+
* function UserList() {
|
|
95
|
+
* const users = useService(LocalUserService, s => s.users)
|
|
96
|
+
* return <div>{users.length} users</div>
|
|
97
|
+
* }
|
|
98
|
+
*
|
|
99
|
+
* function UserDetail() {
|
|
100
|
+
* const service = useService(LocalUserService)
|
|
101
|
+
* return <div>{service.currentUser?.name}</div>
|
|
102
|
+
* }
|
|
70
103
|
*
|
|
71
|
-
* //
|
|
104
|
+
* // 场景 3: 其他 selector 使用方式
|
|
105
|
+
* const service = useService(UserService, s => s) // 智能检测,自动优化响应式监听
|
|
106
|
+
* const currentUser = useService(UserService, service => service.currentUser)
|
|
72
107
|
* const userCount = useService(UserService, service => service.users.length)
|
|
73
108
|
* ```
|
|
74
109
|
*/
|
|
@@ -153,7 +188,12 @@ export declare function useLocalService<T, U>(ServiceClass: new (...args: any[])
|
|
|
153
188
|
* ServiceProvider 组件的属性接口
|
|
154
189
|
*/
|
|
155
190
|
interface ServiceProviderProps {
|
|
156
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* 要注册到容器中的服务提供者数组
|
|
193
|
+
* 支持两种注入方式:
|
|
194
|
+
* 1. 类注入:直接传入服务类,ServiceProvider 会自动实例化并初始化
|
|
195
|
+
* 2. 值注入:传入 { provide: Token, useValue: instance },直接注入已有实例
|
|
196
|
+
*/
|
|
157
197
|
services: Provider<unknown>[];
|
|
158
198
|
/** 子组件 */
|
|
159
199
|
children: React.ReactNode;
|
|
@@ -167,12 +207,14 @@ interface ServiceProviderProps {
|
|
|
167
207
|
* 特性:
|
|
168
208
|
* - 自动复用父级容器,避免重复创建
|
|
169
209
|
* - 支持容器层级结构,子容器可以覆盖父容器的服务
|
|
170
|
-
* -
|
|
210
|
+
* - 支持两种注入方式:类注入和值注入
|
|
211
|
+
* - 自动初始化实现了 Service 接口的服务(仅限类注入)
|
|
171
212
|
*
|
|
172
213
|
* @param props 组件属性
|
|
173
214
|
*
|
|
174
215
|
* @example
|
|
175
216
|
* ```tsx
|
|
217
|
+
* // 方式 1: 类注入 - 自动实例化和初始化
|
|
176
218
|
* function App() {
|
|
177
219
|
* return (
|
|
178
220
|
* <ServiceProvider services={[UserService, ProductService]}>
|
|
@@ -186,11 +228,53 @@ interface ServiceProviderProps {
|
|
|
186
228
|
* )
|
|
187
229
|
* }
|
|
188
230
|
*
|
|
189
|
-
* //
|
|
231
|
+
* // 方式 2: 值注入 - 在多个组件间共享局部服务实例
|
|
190
232
|
* function UserPage() {
|
|
233
|
+
* // 创建局部服务实例
|
|
234
|
+
* const localUserService = useMemo(() => {
|
|
235
|
+
* const service = new LocalUserService()
|
|
236
|
+
* service.init() // 手动初始化
|
|
237
|
+
* return service
|
|
238
|
+
* }, [])
|
|
239
|
+
*
|
|
191
240
|
* return (
|
|
192
|
-
* <ServiceProvider
|
|
241
|
+
* <ServiceProvider
|
|
242
|
+
* services={[
|
|
243
|
+
* { provide: LocalUserService, useValue: localUserService }
|
|
244
|
+
* ]}
|
|
245
|
+
* >
|
|
193
246
|
* <UserList />
|
|
247
|
+
* <UserDetail />
|
|
248
|
+
* </ServiceProvider>
|
|
249
|
+
* )
|
|
250
|
+
* }
|
|
251
|
+
*
|
|
252
|
+
* // UserList 和 UserDetail 共享同一个 localUserService 实例
|
|
253
|
+
* function UserList() {
|
|
254
|
+
* const users = useService(LocalUserService, s => s.users)
|
|
255
|
+
* return <div>{users.length} users</div>
|
|
256
|
+
* }
|
|
257
|
+
*
|
|
258
|
+
* function UserDetail() {
|
|
259
|
+
* const service = useService(LocalUserService)
|
|
260
|
+
* return <div>{service.currentUser?.name}</div>
|
|
261
|
+
* }
|
|
262
|
+
*
|
|
263
|
+
* // 方式 3: 混合使用类注入和值注入
|
|
264
|
+
* function App() {
|
|
265
|
+
* const configService = useMemo(() => {
|
|
266
|
+
* const config = new ConfigService()
|
|
267
|
+
* config.loadConfig() // 预配置
|
|
268
|
+
* return config
|
|
269
|
+
* }, [])
|
|
270
|
+
*
|
|
271
|
+
* return (
|
|
272
|
+
* <ServiceProvider services={[
|
|
273
|
+
* UserService, // 类注入 - 自动实例化和初始化
|
|
274
|
+
* ApiService, // 类注入
|
|
275
|
+
* { provide: ConfigService, useValue: configService } // 值注入 - 使用预配置的实例
|
|
276
|
+
* ]}>
|
|
277
|
+
* <Router />
|
|
194
278
|
* </ServiceProvider>
|
|
195
279
|
* )
|
|
196
280
|
* }
|