@mantou/gem 2.2.14 → 2.2.15
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/dist/gem.js +7 -7
- package/dist/gem.mjs +7 -7
- package/elements/base/link.d.ts.map +1 -1
- package/elements/base/link.js +6 -3
- package/elements/base/link.js.map +1 -1
- package/elements/base/modal-factory.d.ts +1 -1
- package/elements/base/modal-factory.d.ts.map +1 -1
- package/elements/base/use.d.ts.map +1 -1
- package/elements/base/use.js +11 -0
- package/elements/base/use.js.map +1 -1
- package/helper/hmr.d.ts +3 -2
- package/helper/hmr.d.ts.map +1 -1
- package/helper/hmr.js +95 -23
- package/helper/hmr.js.map +1 -1
- package/helper/i18n.d.ts +2 -1
- package/helper/i18n.d.ts.map +1 -1
- package/helper/i18n.js +9 -1
- package/helper/i18n.js.map +1 -1
- package/helper/theme.d.ts.map +1 -1
- package/helper/theme.js +3 -1
- package/helper/theme.js.map +1 -1
- package/lib/decorators.d.ts.map +1 -1
- package/lib/decorators.js +4 -3
- package/lib/decorators.js.map +1 -1
- package/lib/history.d.ts +10 -7
- package/lib/history.d.ts.map +1 -1
- package/lib/history.js +20 -16
- package/lib/history.js.map +1 -1
- package/lib/lit-html.d.ts +15 -0
- package/lib/lit-html.d.ts.map +1 -1
- package/lib/lit-html.js +3 -1
- package/lib/lit-html.js.map +1 -1
- package/lib/reactive.d.ts +4 -1
- package/lib/reactive.d.ts.map +1 -1
- package/lib/reactive.js +41 -18
- package/lib/reactive.js.map +1 -1
- package/lib/server.d.ts +14 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +43 -0
- package/lib/server.js.map +1 -0
- package/lib/store.d.ts +10 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +36 -9
- package/lib/store.js.map +1 -1
- package/lib/utils.d.ts +2 -2
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +20 -7
- package/lib/utils.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/elements/base/link.ts +5 -3
- package/src/elements/base/use.ts +11 -0
- package/src/helper/hmr.ts +105 -37
- package/src/helper/i18n.ts +12 -1
- package/src/helper/theme.ts +3 -1
- package/src/lib/decorators.ts +4 -3
- package/src/lib/history.ts +20 -16
- package/src/lib/lit-html.ts +3 -1
- package/src/lib/reactive.ts +40 -18
- package/src/lib/server.ts +45 -0
- package/src/lib/store.ts +40 -9
- package/src/lib/utils.ts +22 -7
- package/src/lib/version.ts +1 -1
- package/src/test/gem-element/basic.test.ts +20 -0
- package/src/test/store.test.ts +12 -0
package/src/helper/theme.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GemElement, Sheet } from '../lib/reactive';
|
|
2
2
|
import { css, SheetToken } from '../lib/reactive';
|
|
3
|
+
import { isServer } from '../lib/server';
|
|
3
4
|
import type { Store } from '../lib/store';
|
|
4
5
|
import { connect, createStore } from '../lib/store';
|
|
5
6
|
import type { Updater } from '../lib/utils';
|
|
@@ -118,7 +119,8 @@ export function createDecoratorTheme<T extends Record<string, unknown>>(themeObj
|
|
|
118
119
|
this.memo(
|
|
119
120
|
() => {
|
|
120
121
|
// https://bugs.webkit.org/show_bug.cgi?id=326327
|
|
121
|
-
|
|
122
|
+
// SSR 需要更新样式表文本
|
|
123
|
+
if (isServer || CSS.supports(`font:-apple-system-body`)) return theme(access.get(this).apply(this));
|
|
122
124
|
const next = access.get(this).apply(this);
|
|
123
125
|
const rule = style.cssRules[0] as CSSStyleRule;
|
|
124
126
|
const eleTarget = this.shadowRoot
|
package/src/lib/decorators.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { TemplateResult } from './lit-html';
|
|
4
4
|
import type { Metadata, Sheet } from './reactive';
|
|
5
|
-
import { _createTemplate, _RenderErrorEvent,
|
|
5
|
+
import { _createTemplate, _RenderErrorEvent, _renderRoot, GemElement, UpdateToken } from './reactive';
|
|
6
6
|
import type { Store } from './store';
|
|
7
7
|
import { camelToKebabCase, GemError, PropProxyMap } from './utils';
|
|
8
8
|
|
|
@@ -300,8 +300,9 @@ export function template<T extends GemElement, V extends () => TemplateResult |
|
|
|
300
300
|
export function fallback<T extends GemElement, V extends (err: any) => TemplateResult | null | undefined>() {
|
|
301
301
|
return (_: any, { addInitializer, access }: ClassFieldDecoratorContext<T, V> | ClassMethodDecoratorContext<T, V>) => {
|
|
302
302
|
addInitializer(function (this: T) {
|
|
303
|
-
this.addEventListener(_RenderErrorEvent, (
|
|
304
|
-
|
|
303
|
+
this.addEventListener(_RenderErrorEvent, (evt: CustomEvent) => {
|
|
304
|
+
evt.preventDefault();
|
|
305
|
+
_renderRoot(access.get.call(this, this).apply(this, [evt.detail]), this.internals.shadowRoot || this);
|
|
305
306
|
});
|
|
306
307
|
});
|
|
307
308
|
};
|
package/src/lib/history.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// https://github.com/WICG/navigation-api#stakeholder-feedback
|
|
2
2
|
|
|
3
|
+
import { getRequestValue } from './server';
|
|
3
4
|
import { connect, createStore } from './store';
|
|
4
5
|
import { absoluteLocation, cleanObject, GemError, QueryString } from './utils';
|
|
5
6
|
|
|
@@ -45,7 +46,11 @@ type HistoryParams = Omit<UpdateHistoryParams, 'query'> & {
|
|
|
45
46
|
hash: string;
|
|
46
47
|
};
|
|
47
48
|
|
|
48
|
-
const
|
|
49
|
+
const globalParamsMap = new Map<string, HistoryParams>();
|
|
50
|
+
// SSR 时每个请求使用独立的记录,请求结束后释放
|
|
51
|
+
// 从全局复制初始记录(设置 basePath 等操作需要它),记录会被原地修改,所以也要复制
|
|
52
|
+
const copyParamsMap = () => new Map([...globalParamsMap].map(([k, v]) => [k, { ...v }]));
|
|
53
|
+
const getParamsMap = () => getRequestValue(globalParamsMap, copyParamsMap);
|
|
49
54
|
|
|
50
55
|
function validData(data: any) {
|
|
51
56
|
const { $key, $hasCloseHandle, $hasOpenHandle, $hasShouldCloseHandle } = data || {};
|
|
@@ -71,8 +76,7 @@ function dispatchBeforeChangeEvent() {
|
|
|
71
76
|
|
|
72
77
|
function normalizeParams(params: UpdateHistoryParams): HistoryParams {
|
|
73
78
|
const current =
|
|
74
|
-
|
|
75
|
-
({ path: getInternalPath(location.pathname), query: new QueryString() } as HistoryParams);
|
|
79
|
+
gemHistory.getParams() || ({ path: getInternalPath(location.pathname), query: new QueryString() } as HistoryParams);
|
|
76
80
|
// 没提供 path 使用当前 path
|
|
77
81
|
const path = params.path ? absoluteLocation(current.path, params.path) : getInternalPath(location.pathname);
|
|
78
82
|
// 没提供 query 又没有提供 path 时使用当前 search
|
|
@@ -100,7 +104,7 @@ function updateHistory(p: UpdateHistoryParams, native: typeof nativeHistory.push
|
|
|
100
104
|
$title: title,
|
|
101
105
|
...data,
|
|
102
106
|
};
|
|
103
|
-
|
|
107
|
+
getParamsMap().set(state.$key, params);
|
|
104
108
|
dispatchBeforeChangeEvent();
|
|
105
109
|
cleanObject(store);
|
|
106
110
|
store(state);
|
|
@@ -120,7 +124,7 @@ function updateHistoryByNative(data: any, title: string, originUrl: string, nati
|
|
|
120
124
|
};
|
|
121
125
|
const { pathname, search, hash } = new URL(originUrl, location.origin + location.pathname);
|
|
122
126
|
const params = normalizeParams({ path: pathname, query: new QueryString(search), hash, title, data });
|
|
123
|
-
|
|
127
|
+
getParamsMap().set(state.$key, params);
|
|
124
128
|
dispatchBeforeChangeEvent();
|
|
125
129
|
cleanObject(store);
|
|
126
130
|
store(state);
|
|
@@ -149,13 +153,13 @@ class GemHistory extends EventTarget {
|
|
|
149
153
|
// 应用初始化的时候设置
|
|
150
154
|
gemBasePathStore({ basePath });
|
|
151
155
|
// paramsMap 更新后 ui 才会更新
|
|
152
|
-
Object.assign(
|
|
156
|
+
Object.assign(this.getParams(), { path: getInternalPath(location.pathname) });
|
|
153
157
|
}
|
|
154
158
|
getParams() {
|
|
155
|
-
return
|
|
159
|
+
return getParamsMap().get(store.$key)!;
|
|
156
160
|
}
|
|
157
161
|
updateParams(params: UpdateHistoryParams) {
|
|
158
|
-
Object.assign(
|
|
162
|
+
Object.assign(this.getParams(), params);
|
|
159
163
|
store();
|
|
160
164
|
}
|
|
161
165
|
push(params: UpdateHistoryParams) {
|
|
@@ -166,7 +170,7 @@ class GemHistory extends EventTarget {
|
|
|
166
170
|
}
|
|
167
171
|
pushIgnoreCloseHandle(params: UpdateHistoryParams) {
|
|
168
172
|
if (store.$hasCloseHandle) {
|
|
169
|
-
|
|
173
|
+
this.getParams()?.close?.();
|
|
170
174
|
this.replace(params);
|
|
171
175
|
} else {
|
|
172
176
|
this.push(params);
|
|
@@ -218,7 +222,7 @@ if (!globalThis._GEMHISTORY) {
|
|
|
218
222
|
} else if (nativeHistory.state.$hasCloseHandle) {
|
|
219
223
|
store(nativeHistory.state);
|
|
220
224
|
const params = normalizeParams({ title: document.title });
|
|
221
|
-
|
|
225
|
+
getParamsMap().set(store.$key, params);
|
|
222
226
|
// 有 handle 返回键的页面刷新需要清除返回 handler
|
|
223
227
|
gemHistory.back();
|
|
224
228
|
} else {
|
|
@@ -228,11 +232,11 @@ if (!globalThis._GEMHISTORY) {
|
|
|
228
232
|
$key: getKey(),
|
|
229
233
|
...(nativeHistory.state || {}),
|
|
230
234
|
});
|
|
231
|
-
|
|
235
|
+
getParamsMap().set(store.$key, params);
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
connect(gemTitleStore, () => {
|
|
235
|
-
const params =
|
|
239
|
+
const params = gemHistory.getParams();
|
|
236
240
|
if (params) {
|
|
237
241
|
params.title = gemTitleStore.title;
|
|
238
242
|
}
|
|
@@ -266,9 +270,9 @@ if (!globalThis._GEMHISTORY) {
|
|
|
266
270
|
// 刷新后再导航需要从当前 state 中构建 params
|
|
267
271
|
// 理论上该条历史记录中不会出现事件处理器
|
|
268
272
|
// 子 app 导航也需要从 state 中构建 params
|
|
269
|
-
if (!
|
|
273
|
+
if (!getParamsMap().has(newState.$key)) {
|
|
270
274
|
const { pathname, search, hash } = location;
|
|
271
|
-
|
|
275
|
+
getParamsMap().set(newState.$key, {
|
|
272
276
|
path: getInternalPath(pathname),
|
|
273
277
|
query: new QueryString(search),
|
|
274
278
|
hash: decodeURIComponent(hash),
|
|
@@ -284,9 +288,9 @@ if (!globalThis._GEMHISTORY) {
|
|
|
284
288
|
// 返回键关闭的 modal 能前进键重新打开
|
|
285
289
|
// 刷新后不能工作:刷新后 historyItem 中只有 url
|
|
286
290
|
// 能在子 app 中工作
|
|
287
|
-
|
|
291
|
+
getParamsMap().get(newState.$key)?.open?.();
|
|
288
292
|
} else if (prevState.$hasCloseHandle) {
|
|
289
|
-
const prevParams =
|
|
293
|
+
const prevParams = getParamsMap().get(prevState.$key);
|
|
290
294
|
const closeHandle = prevParams?.close;
|
|
291
295
|
const shouldCloseHandle = prevParams?.shouldClose;
|
|
292
296
|
const notAllowClose = shouldCloseHandle && !shouldCloseHandle();
|
package/src/lib/lit-html.ts
CHANGED
|
@@ -245,8 +245,10 @@ function trustFromTemplateString(tsa: TemplateStringsArray, stringFromTSA: strin
|
|
|
245
245
|
* @return Array containing `[html, attrNames]` (array returned for terseness,
|
|
246
246
|
* to avoid object fields since this code is shared with non-minified SSR
|
|
247
247
|
* code)
|
|
248
|
+
*
|
|
249
|
+
* Gem Note: export for SSR
|
|
248
250
|
*/
|
|
249
|
-
const getTemplateHtml = (strings: TemplateStringsArray, type: ResultType): [TrustedHTML, Array<string>] => {
|
|
251
|
+
export const getTemplateHtml = (strings: TemplateStringsArray, type: ResultType): [TrustedHTML, Array<string>] => {
|
|
250
252
|
// Insert makers into the template HTML to represent the position of
|
|
251
253
|
// bindings. The following code scans the template strings to determine the
|
|
252
254
|
// syntactic position of the bindings. They can be in text position, where
|
package/src/lib/reactive.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// biome-ignore-all lint/plugin/no_this_effect: 兼容生命周期
|
|
2
2
|
|
|
3
3
|
import type * as Lit from './lit-html';
|
|
4
|
+
import { isServer } from './server';
|
|
4
5
|
import type { Store } from './store';
|
|
5
6
|
import { connect } from './store';
|
|
6
7
|
import {
|
|
7
8
|
addListener,
|
|
8
9
|
addMicrotask,
|
|
10
|
+
assignState,
|
|
9
11
|
createUpdater,
|
|
10
12
|
GemError,
|
|
11
13
|
isArrayChange,
|
|
@@ -29,10 +31,18 @@ let hydrationEngine = (_value: unknown, _container: HTMLElement | DocumentFragme
|
|
|
29
31
|
return;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
// 在元素首次渲染前恢复 SSR 的状态
|
|
35
|
+
let restoreState: ((ele: GemElement) => void) | undefined;
|
|
36
|
+
|
|
37
|
+
export function setHydrationEngine(engine: typeof hydrationEngine, restore?: typeof restoreState) {
|
|
33
38
|
hydrationEngine = engine;
|
|
39
|
+
restoreState = restore;
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
/** 渲染元素内容,SSR 时由水合引擎接管 */
|
|
43
|
+
export const _renderRoot = (value: unknown, container: HTMLElement | DocumentFragment) =>
|
|
44
|
+
hydrationEngine(value, container) || render(value, container);
|
|
45
|
+
|
|
36
46
|
const { assign, defineProperty } = Object;
|
|
37
47
|
|
|
38
48
|
// https://github.com/tc39/proposal-decorator-metadata
|
|
@@ -40,8 +50,6 @@ defineProperty(Symbol, 'metadata', { value: Symbol.for('Symbol.metadata') });
|
|
|
40
50
|
|
|
41
51
|
const execCallback = (fun: any) => typeof fun === 'function' && fun();
|
|
42
52
|
|
|
43
|
-
const ssrNativeQueueMicrotask = (globalThis as any).nQM;
|
|
44
|
-
|
|
45
53
|
// 读取构造样式表,跨多个 gem 工作
|
|
46
54
|
export const SheetToken = Symbol.for('gem@sheetToken');
|
|
47
55
|
export type SheetTokenType = typeof SheetToken;
|
|
@@ -186,7 +194,7 @@ const updateStyleSheets = (map: Map<CSSStyleSheet, number>, sheets: CSSStyleShee
|
|
|
186
194
|
if (needUpdate) {
|
|
187
195
|
// 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新
|
|
188
196
|
// 在 SSR 中应该区别 effect 强制执行
|
|
189
|
-
addMicrotask(rootUpdateFnMap.get(map)
|
|
197
|
+
addMicrotask(rootUpdateFnMap.get(map)!);
|
|
190
198
|
}
|
|
191
199
|
};
|
|
192
200
|
|
|
@@ -286,10 +294,14 @@ const noBlockingTaskList = new LinkedList<() => void>();
|
|
|
286
294
|
const tick = (timeStamp = performance.now()) => {
|
|
287
295
|
if (performance.now() > timeStamp + 50) return requestAnimationFrame(tick);
|
|
288
296
|
const task = noBlockingTaskList.get();
|
|
289
|
-
if (task)
|
|
297
|
+
if (!task) return;
|
|
298
|
+
// 出错时继续执行后面的任务,否则剩下的任务不会再被调度
|
|
299
|
+
try {
|
|
290
300
|
task();
|
|
291
|
-
|
|
301
|
+
} catch (err) {
|
|
302
|
+
reportError(err);
|
|
292
303
|
}
|
|
304
|
+
tick(timeStamp);
|
|
293
305
|
};
|
|
294
306
|
noBlockingTaskList.addEventListener('start', () => addMicrotask(tick));
|
|
295
307
|
|
|
@@ -340,14 +352,14 @@ export abstract class GemElement extends HTMLElement {
|
|
|
340
352
|
const effect = ele.#effectList.at(0);
|
|
341
353
|
// https://github.com/mantou132/gem/issues/203
|
|
342
354
|
// SSR 不会执行 initEffect,应该跳过
|
|
343
|
-
if (!
|
|
355
|
+
if (!isServer && ele.#isMounted && effect && !effect.initialized) {
|
|
344
356
|
throw new GemError(`Don't set state sync before insert the DOM`);
|
|
345
357
|
}
|
|
346
|
-
|
|
358
|
+
assignState(state, payload);
|
|
347
359
|
// 避免无限刷新
|
|
348
360
|
// 挂载前 set state 不应该触发更新
|
|
349
361
|
// SSR 中渲染 <gem-route>,其他情况的 SSR 应该也可以执行
|
|
350
|
-
if (!ele.#rendering && ele.#isMounted) addMicrotask(ele.#update
|
|
362
|
+
if (!ele.#rendering && ele.#isMounted) addMicrotask(ele.#update);
|
|
351
363
|
});
|
|
352
364
|
ele.#internals.stateList.push(state);
|
|
353
365
|
return state;
|
|
@@ -429,11 +441,17 @@ export abstract class GemElement extends HTMLElement {
|
|
|
429
441
|
const isLight = this.#renderRoot === this;
|
|
430
442
|
const temp = item ? item.render() : isLight ? undefined : html`<slot></slot>`;
|
|
431
443
|
if (temp === undefined) return;
|
|
432
|
-
|
|
433
|
-
|
|
444
|
+
// `<style>` 中不使用绑定,SSR 水合不支持
|
|
445
|
+
const r =
|
|
446
|
+
temp ??
|
|
447
|
+
(isLight
|
|
448
|
+
? html`<style>@scope{&{display:none!important;}}</style>`
|
|
449
|
+
: html`<style>:host{&{display:none!important;}}</style>`);
|
|
450
|
+
_renderRoot(r, this.#renderRoot);
|
|
434
451
|
} catch (err) {
|
|
435
|
-
|
|
436
|
-
|
|
452
|
+
const evt = new CustomEvent(_RenderErrorEvent, { bubbles: true, composed: true, cancelable: true, detail: err });
|
|
453
|
+
// `@fallback` 处理了错误
|
|
454
|
+
if (this.dispatchEvent(evt)) throw err;
|
|
437
455
|
} finally {
|
|
438
456
|
this.#rendering = false;
|
|
439
457
|
}
|
|
@@ -447,12 +465,13 @@ export abstract class GemElement extends HTMLElement {
|
|
|
447
465
|
(item || !this.#renderList.length)
|
|
448
466
|
) {
|
|
449
467
|
this.#render(item);
|
|
450
|
-
addMicrotask(this.#execEffect);
|
|
468
|
+
if (!isServer) addMicrotask(this.#execEffect);
|
|
451
469
|
}
|
|
452
470
|
};
|
|
453
471
|
|
|
454
472
|
#update = () => {
|
|
455
|
-
|
|
473
|
+
// SSR 需要同步渲染
|
|
474
|
+
if (this.#metadata.noBlocking && !isServer) {
|
|
456
475
|
noBlockingTaskList.add(this.#updateCallback);
|
|
457
476
|
} else {
|
|
458
477
|
this.#updateCallback();
|
|
@@ -506,7 +525,8 @@ export abstract class GemElement extends HTMLElement {
|
|
|
506
525
|
};
|
|
507
526
|
|
|
508
527
|
#disconnectStore?: (() => void)[];
|
|
509
|
-
|
|
528
|
+
// 同步执行,SSR 需要捕获渲染错误
|
|
529
|
+
#connectedCallback = () => {
|
|
510
530
|
if (this.#isAppendReason) {
|
|
511
531
|
this.#isAppendReason = false;
|
|
512
532
|
// https://github.com/mantou132/gem/issues/202
|
|
@@ -526,12 +546,14 @@ export abstract class GemElement extends HTMLElement {
|
|
|
526
546
|
if (!mode && this.#renderList.length && !penetrable) this.#internals.states.add(BoundaryCSSState);
|
|
527
547
|
|
|
528
548
|
this.#disconnectStore = observedStores?.map((store) => connect(store, this.#update));
|
|
549
|
+
restoreState?.(this);
|
|
529
550
|
this.#render(this.#renderItem);
|
|
530
551
|
this.#isMounted = true;
|
|
531
552
|
this.#clearStyle = this.#prepareStyle();
|
|
532
553
|
// 等待所有元素的样式被应用,再执行回调
|
|
533
554
|
// 这让 mounted 和 effect 回调和其他回调一样保持一样的异步行为
|
|
534
|
-
|
|
555
|
+
// SSR 不执行副作用
|
|
556
|
+
if (!isServer) addMicrotask(this.#initEffect);
|
|
535
557
|
};
|
|
536
558
|
|
|
537
559
|
/**
|
|
@@ -540,7 +562,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
540
562
|
* use `@mounted`
|
|
541
563
|
*/
|
|
542
564
|
connectedCallback(): Unique {
|
|
543
|
-
if (this.#metadata.noBlocking) {
|
|
565
|
+
if (this.#metadata.noBlocking && !isServer) {
|
|
544
566
|
noBlockingTaskList.add(this.#connectedCallback);
|
|
545
567
|
} else {
|
|
546
568
|
this.#connectedCallback();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SSR 相关的功能都在这里
|
|
2
|
+
//
|
|
3
|
+
// `@mantou/gem-ssr` 在加载 Gem 前提供 `__GEM_SSR__`,返回当前请求的作用域,
|
|
4
|
+
// 客户端构建时 `unplugin-gem` 把 `globalThis.__GEM_SSR__` 替换成 `undefined`,SSR 代码会被压缩移除
|
|
5
|
+
|
|
6
|
+
const { get, set, has, deleteProperty, defineProperty, getOwnPropertyDescriptor, ownKeys } = Reflect;
|
|
7
|
+
|
|
8
|
+
const getRequestScope: (() => WeakMap<object, any> | undefined) | undefined = (globalThis as any).__GEM_SSR__;
|
|
9
|
+
|
|
10
|
+
/** 服务端渲染时不执行副作用,同步更新 */
|
|
11
|
+
export const isServer = !!getRequestScope;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SSR 时返回当前请求中 `key` 对应的值,第一次访问时使用 `init` 创建,
|
|
15
|
+
* 不在请求中(包括客户端)时返回 `key` 本身
|
|
16
|
+
*/
|
|
17
|
+
export const getRequestValue: <T extends object>(key: T, init: () => T) => T = getRequestScope
|
|
18
|
+
? (key, init) => {
|
|
19
|
+
const scope = getRequestScope();
|
|
20
|
+
if (!scope) return key;
|
|
21
|
+
if (!scope.has(key)) scope.set(key, init());
|
|
22
|
+
return scope.get(key);
|
|
23
|
+
}
|
|
24
|
+
: (key) => key;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* SSR 时对象的读写转到当前请求的副本上,副本在第一次访问时从原对象浅复制
|
|
28
|
+
*
|
|
29
|
+
* 嵌套的对象没有复制,原地修改会影响所有请求,应该替换顶层属性,例如 `store({ list: [...store.list, item] })`
|
|
30
|
+
*/
|
|
31
|
+
export const isolate: <T extends object>(target: T) => T = getRequestScope
|
|
32
|
+
? (target) => {
|
|
33
|
+
const copy = () => ({ ...target });
|
|
34
|
+
const current = () => getRequestValue(target, copy);
|
|
35
|
+
return new Proxy(target, {
|
|
36
|
+
get: (_, key) => get(current(), key),
|
|
37
|
+
set: (_, key, value) => set(current(), key, value),
|
|
38
|
+
has: (_, key) => has(current(), key),
|
|
39
|
+
deleteProperty: (_, key) => deleteProperty(current(), key),
|
|
40
|
+
defineProperty: (_, key, descriptor) => defineProperty(current(), key, descriptor),
|
|
41
|
+
getOwnPropertyDescriptor: (_, key) => getOwnPropertyDescriptor(current(), key),
|
|
42
|
+
ownKeys: () => ownKeys(current()),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
: (target) => target;
|
package/src/lib/store.ts
CHANGED
|
@@ -1,22 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getRequestValue, isolate, isServer } from './server';
|
|
2
|
+
import { addMicrotask, assignState, createUpdater, type Updater } from './utils';
|
|
2
3
|
|
|
3
4
|
export type Store<T = any> = Updater<T>;
|
|
4
5
|
|
|
5
6
|
/**@internal */
|
|
6
7
|
export const _StoreListenerMap = new WeakMap<Store<any>, Set<() => void>>();
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const createListeners = () => new Set<() => void>();
|
|
10
|
+
|
|
11
|
+
// SSR 时当前请求中更新过的有 key 的 store,渲染后输出到页面中
|
|
12
|
+
const updatedStores = new Map<string, Store<any>>();
|
|
13
|
+
|
|
14
|
+
/** SSR 时当前请求中更新过的有 key 的 store,供 `@mantou/gem-ssr` 使用 */
|
|
15
|
+
export const _getUpdatedStores = () => getRequestValue(updatedStores, () => new Map());
|
|
16
|
+
|
|
17
|
+
// 客户端读取服务端渲染时 store 的值
|
|
18
|
+
let restoreStore: ((key: string) => object | undefined) | undefined;
|
|
19
|
+
|
|
20
|
+
/** 设置客户端恢复 store 的方法,供 `@mantou/gem-ssr` 使用 */
|
|
21
|
+
export function _setStoreRestorer(restorer: typeof restoreStore) {
|
|
22
|
+
restoreStore = restorer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 创建 store,`key` 用来在 SSR 时同步服务端的值,`swc-plugin-gem` 的 `ssr` 选项会为模块顶层的 store 自动添加
|
|
27
|
+
*/
|
|
28
|
+
export function createStore<T = any>(originStore: T, key?: string) {
|
|
29
|
+
const listeners = createListeners();
|
|
30
|
+
// SSR 时每个请求使用独立的值
|
|
31
|
+
const store = isolate(
|
|
32
|
+
createUpdater(originStore, (value?: Partial<T>) => {
|
|
33
|
+
assignState(store, value);
|
|
34
|
+
listeners.forEach(addMicrotask);
|
|
35
|
+
if (!isServer) return;
|
|
36
|
+
// SSR 时请求中添加的监听器只属于当前请求,其他请求更新 store 时不会触发
|
|
37
|
+
getRequestValue(listeners, createListeners).forEach(addMicrotask);
|
|
38
|
+
const updated = _getUpdatedStores();
|
|
39
|
+
// 不在请求中时更新的是全局的值,客户端也一样
|
|
40
|
+
if (key && updated !== updatedStores) updated.set(key, store);
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
if (key && restoreStore) assignState(store, restoreStore(key));
|
|
44
|
+
_StoreListenerMap.set(store, listeners);
|
|
15
45
|
return store;
|
|
16
46
|
}
|
|
17
47
|
|
|
18
48
|
export function connect(store: Store<any>, func: () => void) {
|
|
19
|
-
const
|
|
49
|
+
const storeListeners = _StoreListenerMap.get(store);
|
|
50
|
+
const listeners = storeListeners && getRequestValue(storeListeners, createListeners);
|
|
20
51
|
listeners?.add(func);
|
|
21
52
|
return () => listeners?.delete(func);
|
|
22
53
|
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { getRequestValue } from './server';
|
|
2
|
+
|
|
3
|
+
const { assign, fromEntries, entries, keys, defineProperty, hasOwn } = Object;
|
|
2
4
|
|
|
3
5
|
// 用原生 Chrome 找不到调用栈
|
|
4
6
|
function queueMicrotask(cb: () => void) {
|
|
@@ -6,13 +8,16 @@ function queueMicrotask(cb: () => void) {
|
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
const microtaskSet = new Set<() => void>();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const createMicrotaskSet = () => new Set<() => void>();
|
|
12
|
+
export function addMicrotask(func: () => void) {
|
|
13
|
+
// SSR 时不同请求的同一个函数都需要执行
|
|
14
|
+
const taskSet = getRequestValue(microtaskSet, createMicrotaskSet);
|
|
15
|
+
if (taskSet.has(func)) return;
|
|
16
|
+
queueMicrotask(() => {
|
|
17
|
+
taskSet.delete(func);
|
|
13
18
|
func();
|
|
14
19
|
});
|
|
15
|
-
|
|
20
|
+
taskSet.add(func);
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
type OverrideFn = {
|
|
@@ -29,6 +34,16 @@ type OverrideFn = {
|
|
|
29
34
|
[Symbol.metadata]: never;
|
|
30
35
|
};
|
|
31
36
|
|
|
37
|
+
// 删除后继承的 `name` `length` 只读,直接赋值会报错,需要先定义成自有属性
|
|
38
|
+
export function assignState(state: any, payload: any) {
|
|
39
|
+
for (const key of ['name', 'length']) {
|
|
40
|
+
if (payload && hasOwn(payload, key)) {
|
|
41
|
+
defineProperty(state, key, { value: payload[key], writable: true, enumerable: true, configurable: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
assign(state, payload);
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
// 注意 typeof state === 'function' 但是没有 Function 的方法和属性
|
|
33
48
|
export function createUpdater<T, Fn = (payload?: Partial<T>) => any>(initState: T, fn: Fn) {
|
|
34
49
|
const state: any = fn;
|
|
@@ -36,7 +51,7 @@ export function createUpdater<T, Fn = (payload?: Partial<T>) => any>(initState:
|
|
|
36
51
|
// setPrototypeOf(state, null);
|
|
37
52
|
delete state.name;
|
|
38
53
|
delete state.length;
|
|
39
|
-
|
|
54
|
+
assignState(state, initState);
|
|
40
55
|
// ts 不允许直接 Omit 掉 Fn 上的属性
|
|
41
56
|
// 只能利用 ts plugin 移除多余的自动完成项
|
|
42
57
|
return state as Fn & Omit<OverrideFn, keyof T> & T;
|
package/src/lib/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.2.
|
|
1
|
+
export const version = '2.2.15';
|
|
@@ -161,3 +161,23 @@ describe('基本 gem element 测试', () => {
|
|
|
161
161
|
expect(el.renderCount).to.equal(2);
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
|
+
|
|
165
|
+
@customElement('gem-name-state')
|
|
166
|
+
class GemNameStateElement extends GemElement {
|
|
167
|
+
state = createState({ name: 'guest' });
|
|
168
|
+
|
|
169
|
+
render() {
|
|
170
|
+
return html`${this.state.name}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
describe('state `name` 字段', () => {
|
|
175
|
+
it('create and update state', async () => {
|
|
176
|
+
const el: GemNameStateElement = await fixture(html`<gem-name-state></gem-name-state>`);
|
|
177
|
+
expect(el.textContent).to.equal('guest');
|
|
178
|
+
el.state({ name: 'admin' });
|
|
179
|
+
await Promise.resolve();
|
|
180
|
+
expect({ ...el.state }).to.deep.equal({ name: 'admin' });
|
|
181
|
+
expect(el.textContent).to.equal('admin');
|
|
182
|
+
});
|
|
183
|
+
});
|
package/src/test/store.test.ts
CHANGED
|
@@ -25,3 +25,15 @@ describe('store 测试', () => {
|
|
|
25
25
|
expect(flag).to.equal(true);
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
describe('store `name` `length` 字段', () => {
|
|
30
|
+
it('create and update store', () => {
|
|
31
|
+
const store = createStore({ name: 'guest', length: 1 });
|
|
32
|
+
expect({ ...store }).to.deep.equal({ name: 'guest', length: 1 });
|
|
33
|
+
store({ name: 'admin', length: 2 });
|
|
34
|
+
expect({ ...store }).to.deep.equal({ name: 'admin', length: 2 });
|
|
35
|
+
const store2 = createStore<{ name?: string }>({});
|
|
36
|
+
store2({ name: 'guest' });
|
|
37
|
+
expect(store2.name).to.equal('guest');
|
|
38
|
+
});
|
|
39
|
+
});
|