@mantou/gem 2.2.15 → 2.3.0
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/modal-factory.d.ts +2 -1
- package/elements/base/modal-factory.d.ts.map +1 -1
- package/elements/base/route.d.ts +8 -3
- package/elements/base/route.d.ts.map +1 -1
- package/elements/base/route.js +84 -44
- package/elements/base/route.js.map +1 -1
- package/helper/theme.d.ts +8 -4
- package/helper/theme.d.ts.map +1 -1
- package/helper/theme.js +12 -9
- package/helper/theme.js.map +1 -1
- package/lib/decorators.d.ts.map +1 -1
- package/lib/decorators.js +27 -43
- package/lib/decorators.js.map +1 -1
- package/lib/history.d.ts.map +1 -1
- package/lib/history.js +19 -37
- package/lib/history.js.map +1 -1
- package/lib/reactive.d.ts +6 -5
- package/lib/reactive.d.ts.map +1 -1
- package/lib/reactive.js +85 -112
- package/lib/reactive.js.map +1 -1
- package/lib/server.d.ts +12 -0
- package/lib/server.d.ts.map +1 -1
- package/lib/server.js +2 -0
- package/lib/server.js.map +1 -1
- package/lib/store.d.ts +0 -6
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +5 -17
- package/lib/store.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.d.ts.map +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/package.json +3 -4
- package/src/elements/base/route.ts +84 -49
- package/src/helper/theme.ts +16 -9
- package/src/lib/decorators.ts +34 -48
- package/src/lib/history.ts +19 -32
- package/src/lib/reactive.ts +89 -120
- package/src/lib/server.ts +14 -0
- package/src/lib/store.ts +3 -19
- package/src/lib/version.ts +1 -1
- package/src/test/elements/gesture.test.ts +13 -5
- package/src/test/elements/route.test.ts +102 -0
- package/src/test/elements/title.test.ts +7 -0
- package/src/test/gem-element/advance.test.ts +19 -0
- package/src/test/gem-element/basic.test.ts +13 -0
- package/src/test/history.test.ts +19 -0
- package/src/test/theme.test.ts +8 -1
- package/src/test/utils.test.ts +6 -0
- package/src/test/utils.ts +56 -0
package/src/lib/reactive.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
+
import { isServer, ssrHooks } from './server';
|
|
5
5
|
import type { Store } from './store';
|
|
6
6
|
import { connect } from './store';
|
|
7
7
|
import {
|
|
@@ -27,21 +27,9 @@ export function setEngine(h: typeof Lit.html, r: typeof Lit.render) {
|
|
|
27
27
|
render = r;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
return;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// 在元素首次渲染前恢复 SSR 的状态
|
|
35
|
-
let restoreState: ((ele: GemElement) => void) | undefined;
|
|
36
|
-
|
|
37
|
-
export function setHydrationEngine(engine: typeof hydrationEngine, restore?: typeof restoreState) {
|
|
38
|
-
hydrationEngine = engine;
|
|
39
|
-
restoreState = restore;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 渲染元素内容,SSR 时由水合引擎接管 */
|
|
30
|
+
/** 渲染元素内容,SSR 时由 `ssrHooks.render` 接管 */
|
|
43
31
|
export const _renderRoot = (value: unknown, container: HTMLElement | DocumentFragment) =>
|
|
44
|
-
|
|
32
|
+
ssrHooks.render?.(value, container) || render(value, container);
|
|
45
33
|
|
|
46
34
|
const { assign, defineProperty } = Object;
|
|
47
35
|
|
|
@@ -59,14 +47,15 @@ export const UpdateToken = Symbol.for('gem@update');
|
|
|
59
47
|
const BoundaryCSSState = 'gem-style-boundary';
|
|
60
48
|
const ElementCSSState = 'gem-element';
|
|
61
49
|
|
|
62
|
-
|
|
50
|
+
/** 渲染出错时在元素上派发,`@fallback` 处理时取消,SSR 时 `@server` 方法出错也会派发 */
|
|
63
51
|
export const _RenderErrorEvent = 'gem@render-error';
|
|
64
52
|
|
|
65
53
|
// fix modal-factory type error
|
|
66
54
|
const updateTokenAlias = UpdateToken;
|
|
67
55
|
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
// 渲染根中 light DOM 元素使用的样式表和使用次数,以及更新渲染根样式表的函数
|
|
57
|
+
type RootSheets = { counts: Map<CSSStyleSheet, number>; update: () => void };
|
|
58
|
+
const rootSheetsMap = new WeakMap<Document | ShadowRoot, RootSheets>();
|
|
70
59
|
|
|
71
60
|
class GemCSSStyleSheet extends CSSStyleSheet {
|
|
72
61
|
#ele?: HTMLStyleElement;
|
|
@@ -96,45 +85,37 @@ class GemCSSSheet {
|
|
|
96
85
|
this.#content = v;
|
|
97
86
|
}
|
|
98
87
|
|
|
99
|
-
//
|
|
100
|
-
#
|
|
101
|
-
|
|
88
|
+
// 对同一类 dom 只使用同一个样式表,key 是元素类或者 `this`,不需要 GC
|
|
89
|
+
#sheets = new Map<any, { sheet: GemCSSStyleSheet; scope: string }>();
|
|
90
|
+
|
|
91
|
+
#apply({ sheet, scope }: { sheet: GemCSSStyleSheet; scope: string }) {
|
|
92
|
+
sheet.replaceSync(scope ? `${scope}{ ${this.#content} }` : this.#content);
|
|
93
|
+
}
|
|
94
|
+
|
|
102
95
|
getStyle(host?: HTMLElement, isEleTheme?: boolean) {
|
|
103
96
|
const isLight = host && !(host as GemElement).internals?.shadowRoot;
|
|
104
|
-
|
|
105
|
-
// 对同一类 dom 只使用同一个样式表
|
|
106
97
|
const key = isLight ? host.constructor : this;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
let item = this.#sheets.get(key);
|
|
99
|
+
if (!item) {
|
|
100
|
+
// light dom 嵌套时需要选择子元素
|
|
101
|
+
// `> *` 实际上是多范围?是否存在性能问题
|
|
102
|
+
// 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用
|
|
103
|
+
// 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `adoptRootStyleSheets`)
|
|
104
|
+
const scope = !isLight
|
|
105
|
+
? ''
|
|
106
|
+
: isEleTheme
|
|
107
|
+
? '@scope'
|
|
108
|
+
: `@scope (${host.tagName}) to (:state(${BoundaryCSSState}) > *)`;
|
|
109
|
+
item = { sheet: new GemCSSStyleSheet({ media: this.#media }), scope };
|
|
110
|
+
this.#sheets.set(key, item);
|
|
111
|
+
this.#apply(item);
|
|
110
112
|
}
|
|
111
|
-
|
|
112
|
-
const sheet = this.#record.get(key)!;
|
|
113
|
-
|
|
114
|
-
// 只执行一次
|
|
115
|
-
if (!this.#used.has(sheet)) {
|
|
116
|
-
let style = this.#content;
|
|
117
|
-
let scope = '';
|
|
118
|
-
if (isLight) {
|
|
119
|
-
// light dom 嵌套时需要选择子元素
|
|
120
|
-
// `> *` 实际上是多范围?是否存在性能问题
|
|
121
|
-
scope = isEleTheme ? '@scope' : `@scope (${host.tagName}) to (:state(${BoundaryCSSState}) > *)`;
|
|
122
|
-
// 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用
|
|
123
|
-
// 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `appleCSSStyleSheet`)
|
|
124
|
-
style = `${scope}{ ${style} }`;
|
|
125
|
-
}
|
|
126
|
-
sheet.replaceSync(style);
|
|
127
|
-
this.#used.set(sheet, scope);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return sheet;
|
|
113
|
+
return item.sheet;
|
|
131
114
|
}
|
|
132
115
|
|
|
133
116
|
// 一般用于主题更新
|
|
134
117
|
updateStyle() {
|
|
135
|
-
this.#
|
|
136
|
-
sheet.replaceSync(scope ? `${scope}{${this.#content}}` : this.#content);
|
|
137
|
-
});
|
|
118
|
+
this.#sheets.forEach((item) => this.#apply(item));
|
|
138
119
|
}
|
|
139
120
|
}
|
|
140
121
|
|
|
@@ -182,40 +163,38 @@ export function css<T extends Record<string, string>>(
|
|
|
182
163
|
return sheet as Sheet<T>;
|
|
183
164
|
}
|
|
184
165
|
|
|
185
|
-
const
|
|
166
|
+
const updateRootSheets = ({ counts, update }: RootSheets, sheets: CSSStyleSheet[], value: number) => {
|
|
186
167
|
let needUpdate = false;
|
|
187
168
|
sheets.forEach((e) => {
|
|
188
|
-
const count =
|
|
169
|
+
const count = counts.get(e) || 0;
|
|
189
170
|
needUpdate ||= (value === 1 && count === 0) || (value === -1 && count === 1);
|
|
190
171
|
// count 为 0 时不立即删除,以便更新时识别是否外部样式
|
|
191
172
|
// 但是只要挂载 document 的样式表就不会被 GC 了, 是否要在闲时 GC?
|
|
192
|
-
|
|
173
|
+
counts.set(e, count + value);
|
|
193
174
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
addMicrotask(rootUpdateFnMap.get(map)!);
|
|
198
|
-
}
|
|
175
|
+
// 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新
|
|
176
|
+
// 在 SSR 中应该区别 effect 强制执行
|
|
177
|
+
if (needUpdate) addMicrotask(update);
|
|
199
178
|
};
|
|
200
179
|
|
|
201
|
-
|
|
180
|
+
/** light DOM 元素的样式表添加到渲染根中,返回移除函数 */
|
|
181
|
+
const adoptRootStyleSheets = (ele: HTMLElement, sheets: CSSStyleSheet[]) => {
|
|
202
182
|
const root = ele.getRootNode() as ShadowRoot | Document;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
183
|
+
let rootSheets = rootSheetsMap.get(root);
|
|
184
|
+
if (!rootSheets) {
|
|
185
|
+
const counts = new Map<CSSStyleSheet, number>();
|
|
186
|
+
const update = () => {
|
|
207
187
|
// 先找到外部样式表
|
|
208
|
-
const newSheets = (root.adoptedStyleSheets || []).filter((e) => !
|
|
209
|
-
|
|
188
|
+
const newSheets = (root.adoptedStyleSheets || []).filter((e) => !counts.has(e));
|
|
189
|
+
counts.forEach((count, sheet) => count && newSheets.push(sheet));
|
|
210
190
|
// 外层元素的样式要放到最后,以提升优先级,但是只考虑第一次出现样式表的位置
|
|
211
191
|
root.adoptedStyleSheets = newSheets.reverse();
|
|
212
|
-
}
|
|
192
|
+
};
|
|
193
|
+
rootSheets = { counts, update };
|
|
194
|
+
rootSheetsMap.set(root, rootSheets);
|
|
213
195
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
updateStyleSheets(map, sheets, 1);
|
|
218
|
-
return () => updateStyleSheets(map, sheets, -1);
|
|
196
|
+
updateRootSheets(rootSheets, sheets, 1);
|
|
197
|
+
return () => updateRootSheets(rootSheets, sheets, -1);
|
|
219
198
|
};
|
|
220
199
|
|
|
221
200
|
/**必须使用在字段中,否则会读取到错误的实例 */
|
|
@@ -232,23 +211,25 @@ type EffectItem<T> = {
|
|
|
232
211
|
preCallback?: any;
|
|
233
212
|
};
|
|
234
213
|
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
214
|
+
function runEffect<T>(item: EffectItem<T>, values: T) {
|
|
215
|
+
execCallback(item.preCallback);
|
|
216
|
+
item.preCallback = item.callback(values, item.values);
|
|
217
|
+
item.values = values;
|
|
218
|
+
// 更新时也要设置,避免异步的 `#initEffect` 重复执行
|
|
219
|
+
item.initialized = true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function execEffectList(list: EffectItem<any>[]) {
|
|
223
|
+
list.forEach((item) => {
|
|
224
|
+
const values = item.getDep?.();
|
|
225
|
+
if (!item.getDep || !item.values || isArrayChange(item.values, values)) runEffect(item, values);
|
|
246
226
|
});
|
|
247
227
|
}
|
|
248
228
|
|
|
249
229
|
function clearEffect(list: EffectItem<any>[]) {
|
|
250
230
|
return list.filter((e) => {
|
|
251
231
|
execCallback(e.preCallback);
|
|
232
|
+
e.preCallback = undefined;
|
|
252
233
|
e.initialized = false;
|
|
253
234
|
e.values = undefined;
|
|
254
235
|
return e.fixed;
|
|
@@ -277,6 +258,8 @@ export type Metadata = Partial<ShadowRootInit> & {
|
|
|
277
258
|
>;
|
|
278
259
|
// 实例化时使用到,DevTools 需要读取
|
|
279
260
|
observedStores?: Store<any>[];
|
|
261
|
+
/** attribute 对应的字段名,用于 `setAttribute` 等方法设置字段 */
|
|
262
|
+
attrProps?: Record<string, string>;
|
|
280
263
|
adoptedStyleSheets?: Sheet<unknown>[];
|
|
281
264
|
// 以下静态字段仅供外部读取,没有实际作用
|
|
282
265
|
observedProperties?: string[];
|
|
@@ -341,8 +324,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
341
324
|
#clearStyle?: (() => void) | undefined;
|
|
342
325
|
|
|
343
326
|
[updateTokenAlias]() {
|
|
344
|
-
|
|
345
|
-
if (this.#isMounted) addMicrotask(this.#update);
|
|
327
|
+
this.#scheduleUpdate();
|
|
346
328
|
}
|
|
347
329
|
|
|
348
330
|
static {
|
|
@@ -357,9 +339,8 @@ export abstract class GemElement extends HTMLElement {
|
|
|
357
339
|
}
|
|
358
340
|
assignState(state, payload);
|
|
359
341
|
// 避免无限刷新
|
|
360
|
-
// 挂载前 set state 不应该触发更新
|
|
361
342
|
// SSR 中渲染 <gem-route>,其他情况的 SSR 应该也可以执行
|
|
362
|
-
if (!ele.#rendering
|
|
343
|
+
if (!ele.#rendering) ele.#scheduleUpdate();
|
|
363
344
|
});
|
|
364
345
|
ele.#internals.stateList.push(state);
|
|
365
346
|
return state;
|
|
@@ -378,6 +359,10 @@ export abstract class GemElement extends HTMLElement {
|
|
|
378
359
|
// 这里认为 rest 不会有自定义字段和 ShadowRootInit 参数冲突
|
|
379
360
|
this.#renderRoot = !mode ? this : this.shadowRoot || this.attachShadow({ mode, delegatesFocus, ...rest });
|
|
380
361
|
this.#internals = this.attachInternals() as GemElement['internals'];
|
|
362
|
+
// 定义前添加的 ShadowRoot 对 `ElementInternals` 不可见,例如客户端填充了服务端内容的延时视图元素
|
|
363
|
+
if (this.#renderRoot !== this && !this.#internals.shadowRoot) {
|
|
364
|
+
defineProperty(this.#internals, 'shadowRoot', { value: this.#renderRoot });
|
|
365
|
+
}
|
|
381
366
|
this.#internals.stateList = [];
|
|
382
367
|
this.#internals.sheets = [];
|
|
383
368
|
|
|
@@ -423,10 +408,6 @@ export abstract class GemElement extends HTMLElement {
|
|
|
423
408
|
execEffectList(this.#effectList);
|
|
424
409
|
};
|
|
425
410
|
|
|
426
|
-
#execMemo = () => {
|
|
427
|
-
execEffectList(this.#memoList);
|
|
428
|
-
};
|
|
429
|
-
|
|
430
411
|
/**
|
|
431
412
|
* - 条件渲染
|
|
432
413
|
* - 没有 `render` 时显示子内容
|
|
@@ -437,7 +418,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
437
418
|
#render = (item?: RenderItem) => {
|
|
438
419
|
try {
|
|
439
420
|
this.#rendering = true;
|
|
440
|
-
this.#
|
|
421
|
+
execEffectList(this.#memoList);
|
|
441
422
|
const isLight = this.#renderRoot === this;
|
|
442
423
|
const temp = item ? item.render() : isLight ? undefined : html`<slot></slot>`;
|
|
443
424
|
if (temp === undefined) return;
|
|
@@ -469,15 +450,22 @@ export abstract class GemElement extends HTMLElement {
|
|
|
469
450
|
}
|
|
470
451
|
};
|
|
471
452
|
|
|
472
|
-
|
|
473
|
-
|
|
453
|
+
// `@async()` 的元素在空闲时执行,SSR 需要同步执行
|
|
454
|
+
#schedule = (task: () => void) => {
|
|
474
455
|
if (this.#metadata.noBlocking && !isServer) {
|
|
475
|
-
noBlockingTaskList.add(
|
|
456
|
+
noBlockingTaskList.add(task);
|
|
476
457
|
} else {
|
|
477
|
-
|
|
458
|
+
task();
|
|
478
459
|
}
|
|
479
460
|
};
|
|
480
461
|
|
|
462
|
+
#update = () => this.#schedule(this.#updateCallback);
|
|
463
|
+
|
|
464
|
+
// 挂载前(例如 `connectedCallback` 中的 property 赋值)不需要更新,挂载时会渲染
|
|
465
|
+
#scheduleUpdate = () => {
|
|
466
|
+
if (this.#isMounted) addMicrotask(this.#update);
|
|
467
|
+
};
|
|
468
|
+
|
|
481
469
|
#prepareStyle = () => {
|
|
482
470
|
const { adoptedStyleSheets = [] } = this.#metadata;
|
|
483
471
|
const { shadowRoot, sheets } = this.#internals;
|
|
@@ -487,7 +475,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
487
475
|
shadowRoot.adoptedStyleSheets = clsSheets.concat(sheets);
|
|
488
476
|
} else {
|
|
489
477
|
this.prepend(...sheets.map((e) => e.element));
|
|
490
|
-
return
|
|
478
|
+
return adoptRootStyleSheets(this, clsSheets);
|
|
491
479
|
}
|
|
492
480
|
};
|
|
493
481
|
|
|
@@ -495,14 +483,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
495
483
|
* 元素挂载后执行还未初始化的副作用
|
|
496
484
|
* */
|
|
497
485
|
#initEffect = () => {
|
|
498
|
-
this.#effectList
|
|
499
|
-
const { callback, getDep, initialized } = effectItem;
|
|
500
|
-
if (!initialized) {
|
|
501
|
-
effectItem.values = getDep?.();
|
|
502
|
-
effectItem.preCallback = callback(effectItem.values);
|
|
503
|
-
effectItem.initialized = true;
|
|
504
|
-
}
|
|
505
|
-
});
|
|
486
|
+
this.#effectList.forEach((item) => !item.initialized && runEffect(item, item.getDep?.()));
|
|
506
487
|
};
|
|
507
488
|
|
|
508
489
|
/** @lifecycle 将来会彻底移除,现在做兼容实现*/ willMount?(): void | Promise<void>;
|
|
@@ -534,6 +515,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
534
515
|
this.#clearStyle = this.#prepareStyle();
|
|
535
516
|
return;
|
|
536
517
|
}
|
|
518
|
+
if (ssrHooks.connect?.(this, this.#connectedCallback)) return;
|
|
537
519
|
this.#notInCons = true;
|
|
538
520
|
this.#compat();
|
|
539
521
|
|
|
@@ -546,7 +528,6 @@ export abstract class GemElement extends HTMLElement {
|
|
|
546
528
|
if (!mode && this.#renderList.length && !penetrable) this.#internals.states.add(BoundaryCSSState);
|
|
547
529
|
|
|
548
530
|
this.#disconnectStore = observedStores?.map((store) => connect(store, this.#update));
|
|
549
|
-
restoreState?.(this);
|
|
550
531
|
this.#render(this.#renderItem);
|
|
551
532
|
this.#isMounted = true;
|
|
552
533
|
this.#clearStyle = this.#prepareStyle();
|
|
@@ -562,11 +543,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
562
543
|
* use `@mounted`
|
|
563
544
|
*/
|
|
564
545
|
connectedCallback(): Unique {
|
|
565
|
-
|
|
566
|
-
noBlockingTaskList.add(this.#connectedCallback);
|
|
567
|
-
} else {
|
|
568
|
-
this.#connectedCallback();
|
|
569
|
-
}
|
|
546
|
+
this.#schedule(this.#connectedCallback);
|
|
570
547
|
return undefined as unknown as Unique;
|
|
571
548
|
}
|
|
572
549
|
|
|
@@ -606,17 +583,9 @@ export abstract class GemElement extends HTMLElement {
|
|
|
606
583
|
* ```
|
|
607
584
|
* */
|
|
608
585
|
effect = <K = any[] | undefined>(callback: EffectCallback<K>, getDep?: K extends any[] ? () => K : undefined) => {
|
|
609
|
-
const effectItem: EffectItem<K> = {
|
|
610
|
-
callback,
|
|
611
|
-
getDep,
|
|
612
|
-
initialized: this.#isMounted,
|
|
613
|
-
fixed: !this.#notInCons,
|
|
614
|
-
};
|
|
586
|
+
const effectItem: EffectItem<K> = { callback, getDep, fixed: !this.#notInCons };
|
|
615
587
|
// 已挂载时立即执行副作用,未挂载时等挂载后执行
|
|
616
|
-
if (this.#isMounted)
|
|
617
|
-
effectItem.values = getDep?.() as K;
|
|
618
|
-
effectItem.preCallback = callback(effectItem.values);
|
|
619
|
-
}
|
|
588
|
+
if (this.#isMounted) runEffect(effectItem, getDep?.() as K);
|
|
620
589
|
this.#effectList.push(effectItem);
|
|
621
590
|
};
|
|
622
591
|
|
|
@@ -649,7 +618,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
649
618
|
* @helper
|
|
650
619
|
* async
|
|
651
620
|
*/
|
|
652
|
-
update = () =>
|
|
621
|
+
update = () => this.#scheduleUpdate();
|
|
653
622
|
}
|
|
654
623
|
|
|
655
624
|
// 用来禁止 connectCallback 方法被覆盖而破坏 GemElement 的功能
|
package/src/lib/server.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// `@mantou/gem-ssr` 在加载 Gem 前提供 `__GEM_SSR__`,返回当前请求的作用域,
|
|
4
4
|
// 客户端构建时 `unplugin-gem` 把 `globalThis.__GEM_SSR__` 替换成 `undefined`,SSR 代码会被压缩移除
|
|
5
5
|
|
|
6
|
+
import type { GemElement } from './reactive';
|
|
7
|
+
|
|
6
8
|
const { get, set, has, deleteProperty, defineProperty, getOwnPropertyDescriptor, ownKeys } = Reflect;
|
|
7
9
|
|
|
8
10
|
const getRequestScope: (() => WeakMap<object, any> | undefined) | undefined = (globalThis as any).__GEM_SSR__;
|
|
@@ -10,6 +12,18 @@ const getRequestScope: (() => WeakMap<object, any> | undefined) | undefined = (g
|
|
|
10
12
|
/** 服务端渲染时不执行副作用,同步更新 */
|
|
11
13
|
export const isServer = !!getRequestScope;
|
|
12
14
|
|
|
15
|
+
/** SSR 接入 Gem 的钩子,`@mantou/gem-ssr` 在服务端和客户端分别设置 */
|
|
16
|
+
export const ssrHooks: {
|
|
17
|
+
/** 渲染元素内容,返回 `true` 时跳过渲染:服务端收集模版,客户端水合 */
|
|
18
|
+
render?: (value: unknown, container: HTMLElement | DocumentFragment) => boolean;
|
|
19
|
+
/** 元素挂载前调用,返回 `true` 时推迟挂载,之后调用 `connect` 挂载:客户端恢复状态,等待父模版水合 */
|
|
20
|
+
connect?: (ele: GemElement, connect: () => void) => boolean;
|
|
21
|
+
/** 创建有 key 的 store,返回初始值:客户端恢复服务端的值 */
|
|
22
|
+
createStore?: (key: string) => object | undefined;
|
|
23
|
+
/** 更新有 key 的 store:服务端记录,渲染后输出到页面中 */
|
|
24
|
+
updateStore?: (key: string, store: object) => void;
|
|
25
|
+
} = {};
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* SSR 时返回当前请求中 `key` 对应的值,第一次访问时使用 `init` 创建,
|
|
15
29
|
* 不在请求中(包括客户端)时返回 `key` 本身
|
package/src/lib/store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRequestValue, isolate, isServer } from './server';
|
|
1
|
+
import { getRequestValue, isolate, isServer, ssrHooks } from './server';
|
|
2
2
|
import { addMicrotask, assignState, createUpdater, type Updater } from './utils';
|
|
3
3
|
|
|
4
4
|
export type Store<T = any> = Updater<T>;
|
|
@@ -8,20 +8,6 @@ export const _StoreListenerMap = new WeakMap<Store<any>, Set<() => void>>();
|
|
|
8
8
|
|
|
9
9
|
const createListeners = () => new Set<() => void>();
|
|
10
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
11
|
/**
|
|
26
12
|
* 创建 store,`key` 用来在 SSR 时同步服务端的值,`swc-plugin-gem` 的 `ssr` 选项会为模块顶层的 store 自动添加
|
|
27
13
|
*/
|
|
@@ -35,12 +21,10 @@ export function createStore<T = any>(originStore: T, key?: string) {
|
|
|
35
21
|
if (!isServer) return;
|
|
36
22
|
// SSR 时请求中添加的监听器只属于当前请求,其他请求更新 store 时不会触发
|
|
37
23
|
getRequestValue(listeners, createListeners).forEach(addMicrotask);
|
|
38
|
-
|
|
39
|
-
// 不在请求中时更新的是全局的值,客户端也一样
|
|
40
|
-
if (key && updated !== updatedStores) updated.set(key, store);
|
|
24
|
+
if (key) ssrHooks.updateStore?.(key, store);
|
|
41
25
|
}),
|
|
42
26
|
);
|
|
43
|
-
if (key
|
|
27
|
+
if (key) assignState(store, ssrHooks.createStore?.(key));
|
|
44
28
|
_StoreListenerMap.set(store, listeners);
|
|
45
29
|
return store;
|
|
46
30
|
}
|
package/src/lib/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.3.0';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EndEventDetail, GemGestureElement, PanEventDetail, SwipeEventDetail } from '../../elements/gesture';
|
|
2
2
|
import { html } from '../../lib/element';
|
|
3
|
-
import { aTimeout, expect, fixture } from '../utils';
|
|
3
|
+
import { aTimeout, createFakeTimers, expect, fixture } from '../utils';
|
|
4
4
|
|
|
5
5
|
import '../../elements/gesture';
|
|
6
6
|
|
|
@@ -20,6 +20,12 @@ function pointer(type: string, [timeStamp, clientX, clientY = 0]: Sample, pointe
|
|
|
20
20
|
return event;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const clock = createFakeTimers();
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
clock.uninstall();
|
|
27
|
+
});
|
|
28
|
+
|
|
23
29
|
async function setupGesture(touchAction = '') {
|
|
24
30
|
const el: GemGestureElement = await fixture(html`<gem-gesture touch-action=${touchAction}></gem-gesture>`);
|
|
25
31
|
const captured = new Set<number>();
|
|
@@ -43,9 +49,9 @@ describe('GemGestureElement end', () => {
|
|
|
43
49
|
const { el, dispatch } = await setupGesture();
|
|
44
50
|
const ends: EndEventDetail[] = [];
|
|
45
51
|
el.addEventListener('end', (evt: CustomEvent<EndEventDetail>) => ends.push(evt.detail));
|
|
46
|
-
|
|
52
|
+
clock.install();
|
|
47
53
|
dispatch('pointerdown', [0, 0]);
|
|
48
|
-
|
|
54
|
+
clock.tick(251);
|
|
49
55
|
const event = pointer(type, [300, 10, 20]);
|
|
50
56
|
el.dispatchEvent(event);
|
|
51
57
|
await Promise.resolve();
|
|
@@ -68,7 +74,9 @@ describe('GemGestureElement end', () => {
|
|
|
68
74
|
el.addEventListener('end', (evt: CustomEvent<EndEventDetail>) => resolve(evt.detail), { once: true });
|
|
69
75
|
});
|
|
70
76
|
el.addEventListener('press', () => dispatch('pointerup', [300, 0]), { once: true });
|
|
77
|
+
clock.install();
|
|
71
78
|
dispatch('pointerdown', [0, 0]);
|
|
79
|
+
clock.tick(251);
|
|
72
80
|
expect((await ended).pressed).to.equal(true);
|
|
73
81
|
});
|
|
74
82
|
});
|
|
@@ -330,9 +338,9 @@ describe('GemGestureElement swipe', () => {
|
|
|
330
338
|
|
|
331
339
|
it('does not swipe after a long press', async () => {
|
|
332
340
|
const { el, swipes, dispatch } = await setupGesture();
|
|
333
|
-
|
|
341
|
+
clock.install();
|
|
334
342
|
dispatch('pointerdown', [0, 0]);
|
|
335
|
-
|
|
343
|
+
clock.tick(251);
|
|
336
344
|
dispatch('pointermove', [300, 0]);
|
|
337
345
|
dispatch('pointermove', [340, 60]);
|
|
338
346
|
dispatch('pointerup', [350, 60]);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { GemRouteElement } from '../../elements/route';
|
|
2
|
+
import { html } from '../../lib/element';
|
|
3
|
+
import { history } from '../../lib/history';
|
|
4
|
+
import { expect, fixture, nextFrame, waitForPopState } from '../utils';
|
|
5
|
+
|
|
6
|
+
import '../../elements/route';
|
|
7
|
+
|
|
8
|
+
const routes = [{ pattern: '*', content: html`<div style="height: 1000px"></div>` }];
|
|
9
|
+
|
|
10
|
+
// 滚动位置在路由渲染后更新,恢复位置可能晚一个微任务,等待 `routechange` 后已经更新
|
|
11
|
+
const rendered = (route: GemRouteElement) =>
|
|
12
|
+
new Promise((resolve) => route.addEventListener('routechange', resolve, { once: true }));
|
|
13
|
+
|
|
14
|
+
let container: HTMLElement;
|
|
15
|
+
|
|
16
|
+
async function createRoute() {
|
|
17
|
+
container = await fixture(
|
|
18
|
+
html`<div style="height: 100px; overflow: auto"><gem-route .routes=${routes}></gem-route></div>`,
|
|
19
|
+
);
|
|
20
|
+
return container.querySelector<GemRouteElement>('gem-route')!;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function navigate(route: GemRouteElement, action: () => void) {
|
|
24
|
+
const promise = rendered(route);
|
|
25
|
+
action();
|
|
26
|
+
await promise;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('<gem-route> scrollContainer', () => {
|
|
30
|
+
// 所有路由共享滚动位置,避免之前的路由记录位置
|
|
31
|
+
afterEach(() => container.remove());
|
|
32
|
+
|
|
33
|
+
it('导航时滚动到顶部,后退时恢复位置,同一条历史记录重新渲染时保持位置', async () => {
|
|
34
|
+
const route = await createRoute();
|
|
35
|
+
route.scrollContainer = container;
|
|
36
|
+
await nextFrame();
|
|
37
|
+
await navigate(route, () => history.push({ path: '/scroll-a' }));
|
|
38
|
+
container.scrollTop = 300;
|
|
39
|
+
await navigate(route, () => history.push({ path: '/scroll-b' }));
|
|
40
|
+
expect(container.scrollTop).to.equal(0);
|
|
41
|
+
container.scrollTop = 100;
|
|
42
|
+
await navigate(route, () => (route.key = 1));
|
|
43
|
+
expect(container.scrollTop).to.equal(100);
|
|
44
|
+
await navigate(route, () => waitForPopState(() => history.back()));
|
|
45
|
+
expect(container.scrollTop).to.equal(300);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('前进后退时位置未知则滚动到顶部', async () => {
|
|
49
|
+
const route = await createRoute();
|
|
50
|
+
await navigate(route, () => history.push({ path: '/scroll-c' }));
|
|
51
|
+
await navigate(route, () => history.push({ path: '/scroll-d' }));
|
|
52
|
+
route.scrollContainer = container;
|
|
53
|
+
await nextFrame();
|
|
54
|
+
container.scrollTop = 50;
|
|
55
|
+
await navigate(route, () => waitForPopState(() => history.back()));
|
|
56
|
+
expect(container.scrollTop).to.equal(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('前进后退时位置未知则保持文档的滚动位置', async () => {
|
|
60
|
+
// 避免浏览器恢复滚动位置
|
|
61
|
+
window.history.scrollRestoration = 'manual';
|
|
62
|
+
document.body.style.minHeight = '3000px';
|
|
63
|
+
const route = await createRoute();
|
|
64
|
+
await navigate(route, () => history.push({ path: '/scroll-f' }));
|
|
65
|
+
await navigate(route, () => history.push({ path: '/scroll-g' }));
|
|
66
|
+
route.scrollContainer = document.scrollingElement!;
|
|
67
|
+
await nextFrame();
|
|
68
|
+
scrollTo(0, 50);
|
|
69
|
+
await navigate(route, () => waitForPopState(() => history.back()));
|
|
70
|
+
expect(scrollY).to.equal(50);
|
|
71
|
+
scrollTo(0, 0);
|
|
72
|
+
document.body.style.minHeight = '';
|
|
73
|
+
window.history.scrollRestoration = 'auto';
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('恢复位置前等待 scrollWait', async () => {
|
|
77
|
+
const route = await createRoute();
|
|
78
|
+
route.scrollContainer = container;
|
|
79
|
+
let resolve = () => {};
|
|
80
|
+
const ready = new Promise<void>((r) => (resolve = r));
|
|
81
|
+
route.scrollWait = () => ready;
|
|
82
|
+
await nextFrame();
|
|
83
|
+
await navigate(route, () => history.push({ path: '/scroll-h' }));
|
|
84
|
+
container.scrollTop = 300;
|
|
85
|
+
await navigate(route, () => history.push({ path: '/scroll-i' }));
|
|
86
|
+
expect(container.scrollTop).to.equal(0);
|
|
87
|
+
await navigate(route, () => waitForPopState(() => history.back()));
|
|
88
|
+
expect(container.scrollTop).to.equal(0);
|
|
89
|
+
resolve();
|
|
90
|
+
await ready;
|
|
91
|
+
expect(container.scrollTop).to.equal(300);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('滚动容器为空时不滚动', async () => {
|
|
95
|
+
const route = await createRoute();
|
|
96
|
+
route.scrollContainer = () => null;
|
|
97
|
+
await nextFrame();
|
|
98
|
+
container.scrollTop = 80;
|
|
99
|
+
await navigate(route, () => history.push({ path: '/scroll-e' }));
|
|
100
|
+
expect(container.scrollTop).to.equal(80);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -21,5 +21,12 @@ describe('GemTitleElement', () => {
|
|
|
21
21
|
await nextFrame();
|
|
22
22
|
expect(document.title).to.equal('prefix-Page');
|
|
23
23
|
expect(ele.shadowRoot!.textContent).to.equal('Page');
|
|
24
|
+
|
|
25
|
+
// 只改变 hash 时保持标题,不重复添加前缀
|
|
26
|
+
location.hash = '#a';
|
|
27
|
+
await nextFrame();
|
|
28
|
+
await nextFrame();
|
|
29
|
+
expect(document.title).to.equal('prefix-Page');
|
|
30
|
+
expect(ele.shadowRoot!.textContent).to.equal('Page');
|
|
24
31
|
});
|
|
25
32
|
});
|