@mantou/gem 2.2.14 → 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.
Files changed (82) hide show
  1. package/dist/gem.js +7 -7
  2. package/dist/gem.mjs +7 -7
  3. package/elements/base/link.d.ts.map +1 -1
  4. package/elements/base/link.js +6 -3
  5. package/elements/base/link.js.map +1 -1
  6. package/elements/base/modal-factory.d.ts +3 -2
  7. package/elements/base/modal-factory.d.ts.map +1 -1
  8. package/elements/base/route.d.ts +8 -3
  9. package/elements/base/route.d.ts.map +1 -1
  10. package/elements/base/route.js +84 -44
  11. package/elements/base/route.js.map +1 -1
  12. package/elements/base/use.d.ts.map +1 -1
  13. package/elements/base/use.js +11 -0
  14. package/elements/base/use.js.map +1 -1
  15. package/helper/hmr.d.ts +3 -2
  16. package/helper/hmr.d.ts.map +1 -1
  17. package/helper/hmr.js +95 -23
  18. package/helper/hmr.js.map +1 -1
  19. package/helper/i18n.d.ts +2 -1
  20. package/helper/i18n.d.ts.map +1 -1
  21. package/helper/i18n.js +9 -1
  22. package/helper/i18n.js.map +1 -1
  23. package/helper/theme.d.ts +8 -4
  24. package/helper/theme.d.ts.map +1 -1
  25. package/helper/theme.js +15 -10
  26. package/helper/theme.js.map +1 -1
  27. package/lib/decorators.d.ts.map +1 -1
  28. package/lib/decorators.js +31 -46
  29. package/lib/decorators.js.map +1 -1
  30. package/lib/history.d.ts +10 -7
  31. package/lib/history.d.ts.map +1 -1
  32. package/lib/history.js +37 -51
  33. package/lib/history.js.map +1 -1
  34. package/lib/lit-html.d.ts +15 -0
  35. package/lib/lit-html.d.ts.map +1 -1
  36. package/lib/lit-html.js +3 -1
  37. package/lib/lit-html.js.map +1 -1
  38. package/lib/reactive.d.ts +6 -2
  39. package/lib/reactive.d.ts.map +1 -1
  40. package/lib/reactive.js +114 -118
  41. package/lib/reactive.js.map +1 -1
  42. package/lib/server.d.ts +26 -0
  43. package/lib/server.d.ts.map +1 -0
  44. package/lib/server.js +45 -0
  45. package/lib/server.js.map +1 -0
  46. package/lib/store.d.ts +4 -1
  47. package/lib/store.d.ts.map +1 -1
  48. package/lib/store.js +24 -9
  49. package/lib/store.js.map +1 -1
  50. package/lib/utils.d.ts +2 -2
  51. package/lib/utils.d.ts.map +1 -1
  52. package/lib/utils.js +20 -7
  53. package/lib/utils.js.map +1 -1
  54. package/lib/version.d.ts +1 -1
  55. package/lib/version.d.ts.map +1 -1
  56. package/lib/version.js +1 -1
  57. package/lib/version.js.map +1 -1
  58. package/package.json +3 -4
  59. package/src/elements/base/link.ts +5 -3
  60. package/src/elements/base/route.ts +84 -49
  61. package/src/elements/base/use.ts +11 -0
  62. package/src/helper/hmr.ts +105 -37
  63. package/src/helper/i18n.ts +12 -1
  64. package/src/helper/theme.ts +19 -10
  65. package/src/lib/decorators.ts +38 -51
  66. package/src/lib/history.ts +37 -46
  67. package/src/lib/lit-html.ts +3 -1
  68. package/src/lib/reactive.ts +115 -124
  69. package/src/lib/server.ts +59 -0
  70. package/src/lib/store.ts +24 -9
  71. package/src/lib/utils.ts +22 -7
  72. package/src/lib/version.ts +1 -1
  73. package/src/test/elements/gesture.test.ts +13 -5
  74. package/src/test/elements/route.test.ts +102 -0
  75. package/src/test/elements/title.test.ts +7 -0
  76. package/src/test/gem-element/advance.test.ts +19 -0
  77. package/src/test/gem-element/basic.test.ts +33 -0
  78. package/src/test/history.test.ts +19 -0
  79. package/src/test/store.test.ts +12 -0
  80. package/src/test/theme.test.ts +8 -1
  81. package/src/test/utils.test.ts +6 -0
  82. package/src/test/utils.ts +56 -0
@@ -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, ssrHooks } 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,
@@ -25,13 +27,9 @@ export function setEngine(h: typeof Lit.html, r: typeof Lit.render) {
25
27
  render = r;
26
28
  }
27
29
 
28
- let hydrationEngine = (_value: unknown, _container: HTMLElement | DocumentFragment): Lit.RootPart | undefined => {
29
- return;
30
- };
31
-
32
- export function setHydrationEngine(engine: typeof hydrationEngine) {
33
- hydrationEngine = engine;
34
- }
30
+ /** 渲染元素内容,SSR 时由 `ssrHooks.render` 接管 */
31
+ export const _renderRoot = (value: unknown, container: HTMLElement | DocumentFragment) =>
32
+ ssrHooks.render?.(value, container) || render(value, container);
35
33
 
36
34
  const { assign, defineProperty } = Object;
37
35
 
@@ -40,8 +38,6 @@ defineProperty(Symbol, 'metadata', { value: Symbol.for('Symbol.metadata') });
40
38
 
41
39
  const execCallback = (fun: any) => typeof fun === 'function' && fun();
42
40
 
43
- const ssrNativeQueueMicrotask = (globalThis as any).nQM;
44
-
45
41
  // 读取构造样式表,跨多个 gem 工作
46
42
  export const SheetToken = Symbol.for('gem@sheetToken');
47
43
  export type SheetTokenType = typeof SheetToken;
@@ -51,14 +47,15 @@ export const UpdateToken = Symbol.for('gem@update');
51
47
  const BoundaryCSSState = 'gem-style-boundary';
52
48
  const ElementCSSState = 'gem-element';
53
49
 
54
- /**@internal */
50
+ /** 渲染出错时在元素上派发,`@fallback` 处理时取消,SSR 时 `@server` 方法出错也会派发 */
55
51
  export const _RenderErrorEvent = 'gem@render-error';
56
52
 
57
53
  // fix modal-factory type error
58
54
  const updateTokenAlias = UpdateToken;
59
55
 
60
- const rootStyleSheetInfo = new WeakMap<Document | ShadowRoot, Map<CSSStyleSheet, number>>();
61
- const rootUpdateFnMap = new WeakMap<Map<CSSStyleSheet, number>, () => void>();
56
+ // 渲染根中 light DOM 元素使用的样式表和使用次数,以及更新渲染根样式表的函数
57
+ type RootSheets = { counts: Map<CSSStyleSheet, number>; update: () => void };
58
+ const rootSheetsMap = new WeakMap<Document | ShadowRoot, RootSheets>();
62
59
 
63
60
  class GemCSSStyleSheet extends CSSStyleSheet {
64
61
  #ele?: HTMLStyleElement;
@@ -88,45 +85,37 @@ class GemCSSSheet {
88
85
  this.#content = v;
89
86
  }
90
87
 
91
- // 不需要 GC
92
- #record = new Map<any, GemCSSStyleSheet>();
93
- #used = new Map<GemCSSStyleSheet, string>();
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
+
94
95
  getStyle(host?: HTMLElement, isEleTheme?: boolean) {
95
96
  const isLight = host && !(host as GemElement).internals?.shadowRoot;
96
-
97
- // 对同一类 dom 只使用同一个样式表
98
97
  const key = isLight ? host.constructor : this;
99
- if (!this.#record.has(key)) {
100
- const sheet = new GemCSSStyleSheet({ media: this.#media });
101
- this.#record.set(key, sheet);
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);
102
112
  }
103
-
104
- const sheet = this.#record.get(key)!;
105
-
106
- // 只执行一次
107
- if (!this.#used.has(sheet)) {
108
- let style = this.#content;
109
- let scope = '';
110
- if (isLight) {
111
- // light dom 嵌套时需要选择子元素
112
- // `> *` 实际上是多范围?是否存在性能问题
113
- scope = isEleTheme ? '@scope' : `@scope (${host.tagName}) to (:state(${BoundaryCSSState}) > *)`;
114
- // 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用
115
- // 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `appleCSSStyleSheet`)
116
- style = `${scope}{ ${style} }`;
117
- }
118
- sheet.replaceSync(style);
119
- this.#used.set(sheet, scope);
120
- }
121
-
122
- return sheet;
113
+ return item.sheet;
123
114
  }
124
115
 
125
116
  // 一般用于主题更新
126
117
  updateStyle() {
127
- this.#used.forEach((scope, sheet) => {
128
- sheet.replaceSync(scope ? `${scope}{${this.#content}}` : this.#content);
129
- });
118
+ this.#sheets.forEach((item) => this.#apply(item));
130
119
  }
131
120
  }
132
121
 
@@ -174,40 +163,38 @@ export function css<T extends Record<string, string>>(
174
163
  return sheet as Sheet<T>;
175
164
  }
176
165
 
177
- const updateStyleSheets = (map: Map<CSSStyleSheet, number>, sheets: CSSStyleSheet[], value: number) => {
166
+ const updateRootSheets = ({ counts, update }: RootSheets, sheets: CSSStyleSheet[], value: number) => {
178
167
  let needUpdate = false;
179
168
  sheets.forEach((e) => {
180
- const count = map.get(e) || 0;
169
+ const count = counts.get(e) || 0;
181
170
  needUpdate ||= (value === 1 && count === 0) || (value === -1 && count === 1);
182
171
  // count 为 0 时不立即删除,以便更新时识别是否外部样式
183
172
  // 但是只要挂载 document 的样式表就不会被 GC 了, 是否要在闲时 GC?
184
- map.set(e, count + value);
173
+ counts.set(e, count + value);
185
174
  });
186
- if (needUpdate) {
187
- // 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新
188
- // 在 SSR 中应该区别 effect 强制执行
189
- addMicrotask(rootUpdateFnMap.get(map)!, ssrNativeQueueMicrotask);
190
- }
175
+ // 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新
176
+ // 在 SSR 中应该区别 effect 强制执行
177
+ if (needUpdate) addMicrotask(update);
191
178
  };
192
179
 
193
- const appleCSSStyleSheet = (ele: HTMLElement, sheets: CSSStyleSheet[]) => {
180
+ /** light DOM 元素的样式表添加到渲染根中,返回移除函数 */
181
+ const adoptRootStyleSheets = (ele: HTMLElement, sheets: CSSStyleSheet[]) => {
194
182
  const root = ele.getRootNode() as ShadowRoot | Document;
195
- if (!rootStyleSheetInfo.has(root)) {
196
- const map = new Map<CSSStyleSheet, number>();
197
- rootStyleSheetInfo.set(root, map);
198
- rootUpdateFnMap.set(map, () => {
183
+ let rootSheets = rootSheetsMap.get(root);
184
+ if (!rootSheets) {
185
+ const counts = new Map<CSSStyleSheet, number>();
186
+ const update = () => {
199
187
  // 先找到外部样式表
200
- const newSheets = (root.adoptedStyleSheets || []).filter((e) => !map.has(e));
201
- map.forEach((count, sheet) => count && newSheets.push(sheet));
188
+ const newSheets = (root.adoptedStyleSheets || []).filter((e) => !counts.has(e));
189
+ counts.forEach((count, sheet) => count && newSheets.push(sheet));
202
190
  // 外层元素的样式要放到最后,以提升优先级,但是只考虑第一次出现样式表的位置
203
191
  root.adoptedStyleSheets = newSheets.reverse();
204
- });
192
+ };
193
+ rootSheets = { counts, update };
194
+ rootSheetsMap.set(root, rootSheets);
205
195
  }
206
-
207
- const map = rootStyleSheetInfo.get(root)!;
208
-
209
- updateStyleSheets(map, sheets, 1);
210
- return () => updateStyleSheets(map, sheets, -1);
196
+ updateRootSheets(rootSheets, sheets, 1);
197
+ return () => updateRootSheets(rootSheets, sheets, -1);
211
198
  };
212
199
 
213
200
  /**必须使用在字段中,否则会读取到错误的实例 */
@@ -224,23 +211,25 @@ type EffectItem<T> = {
224
211
  preCallback?: any;
225
212
  };
226
213
 
227
- function execEffectList(list?: EffectItem<any>[]) {
228
- list?.forEach((effectItem) => {
229
- const { callback, getDep, values, preCallback } = effectItem;
230
- const newValues = getDep?.();
231
- if (!getDep || !values || isArrayChange(values, newValues)) {
232
- execCallback(preCallback);
233
- effectItem.preCallback = callback(newValues, values);
234
- effectItem.values = newValues;
235
- // 更新时也要设置,避免异步的 `#initEffect` 重复执行
236
- effectItem.initialized = true;
237
- }
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);
238
226
  });
239
227
  }
240
228
 
241
229
  function clearEffect(list: EffectItem<any>[]) {
242
230
  return list.filter((e) => {
243
231
  execCallback(e.preCallback);
232
+ e.preCallback = undefined;
244
233
  e.initialized = false;
245
234
  e.values = undefined;
246
235
  return e.fixed;
@@ -269,6 +258,8 @@ export type Metadata = Partial<ShadowRootInit> & {
269
258
  >;
270
259
  // 实例化时使用到,DevTools 需要读取
271
260
  observedStores?: Store<any>[];
261
+ /** attribute 对应的字段名,用于 `setAttribute` 等方法设置字段 */
262
+ attrProps?: Record<string, string>;
272
263
  adoptedStyleSheets?: Sheet<unknown>[];
273
264
  // 以下静态字段仅供外部读取,没有实际作用
274
265
  observedProperties?: string[];
@@ -286,10 +277,14 @@ const noBlockingTaskList = new LinkedList<() => void>();
286
277
  const tick = (timeStamp = performance.now()) => {
287
278
  if (performance.now() > timeStamp + 50) return requestAnimationFrame(tick);
288
279
  const task = noBlockingTaskList.get();
289
- if (task) {
280
+ if (!task) return;
281
+ // 出错时继续执行后面的任务,否则剩下的任务不会再被调度
282
+ try {
290
283
  task();
291
- tick(timeStamp);
284
+ } catch (err) {
285
+ reportError(err);
292
286
  }
287
+ tick(timeStamp);
293
288
  };
294
289
  noBlockingTaskList.addEventListener('start', () => addMicrotask(tick));
295
290
 
@@ -329,8 +324,7 @@ export abstract class GemElement extends HTMLElement {
329
324
  #clearStyle?: (() => void) | undefined;
330
325
 
331
326
  [updateTokenAlias]() {
332
- // 避免 `connectedCallback` 中的 property 赋值造成多余更新
333
- if (this.#isMounted) addMicrotask(this.#update);
327
+ this.#scheduleUpdate();
334
328
  }
335
329
 
336
330
  static {
@@ -340,14 +334,13 @@ export abstract class GemElement extends HTMLElement {
340
334
  const effect = ele.#effectList.at(0);
341
335
  // https://github.com/mantou132/gem/issues/203
342
336
  // SSR 不会执行 initEffect,应该跳过
343
- if (!ssrNativeQueueMicrotask && ele.#isMounted && effect && !effect.initialized) {
337
+ if (!isServer && ele.#isMounted && effect && !effect.initialized) {
344
338
  throw new GemError(`Don't set state sync before insert the DOM`);
345
339
  }
346
- assign(state, payload);
340
+ assignState(state, payload);
347
341
  // 避免无限刷新
348
- // 挂载前 set state 不应该触发更新
349
342
  // SSR 中渲染 <gem-route>,其他情况的 SSR 应该也可以执行
350
- if (!ele.#rendering && ele.#isMounted) addMicrotask(ele.#update, ssrNativeQueueMicrotask);
343
+ if (!ele.#rendering) ele.#scheduleUpdate();
351
344
  });
352
345
  ele.#internals.stateList.push(state);
353
346
  return state;
@@ -366,6 +359,10 @@ export abstract class GemElement extends HTMLElement {
366
359
  // 这里认为 rest 不会有自定义字段和 ShadowRootInit 参数冲突
367
360
  this.#renderRoot = !mode ? this : this.shadowRoot || this.attachShadow({ mode, delegatesFocus, ...rest });
368
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
+ }
369
366
  this.#internals.stateList = [];
370
367
  this.#internals.sheets = [];
371
368
 
@@ -411,10 +408,6 @@ export abstract class GemElement extends HTMLElement {
411
408
  execEffectList(this.#effectList);
412
409
  };
413
410
 
414
- #execMemo = () => {
415
- execEffectList(this.#memoList);
416
- };
417
-
418
411
  /**
419
412
  * - 条件渲染
420
413
  * - 没有 `render` 时显示子内容
@@ -425,15 +418,21 @@ export abstract class GemElement extends HTMLElement {
425
418
  #render = (item?: RenderItem) => {
426
419
  try {
427
420
  this.#rendering = true;
428
- this.#execMemo();
421
+ execEffectList(this.#memoList);
429
422
  const isLight = this.#renderRoot === this;
430
423
  const temp = item ? item.render() : isLight ? undefined : html`<slot></slot>`;
431
424
  if (temp === undefined) return;
432
- const r = temp !== null ? temp : html`<style>${isLight ? '@scope' : ':host'}{&{display:none!important;}}</style>`;
433
- hydrationEngine(r, this.#renderRoot) || render(r, this.#renderRoot);
425
+ // `<style>` 中不使用绑定,SSR 水合不支持
426
+ const r =
427
+ temp ??
428
+ (isLight
429
+ ? html`<style>@scope{&{display:none!important;}}</style>`
430
+ : html`<style>:host{&{display:none!important;}}</style>`);
431
+ _renderRoot(r, this.#renderRoot);
434
432
  } catch (err) {
435
- this.dispatchEvent(new CustomEvent(_RenderErrorEvent, { bubbles: true, composed: true, detail: err }));
436
- throw err;
433
+ const evt = new CustomEvent(_RenderErrorEvent, { bubbles: true, composed: true, cancelable: true, detail: err });
434
+ // `@fallback` 处理了错误
435
+ if (this.dispatchEvent(evt)) throw err;
437
436
  } finally {
438
437
  this.#rendering = false;
439
438
  }
@@ -447,18 +446,26 @@ export abstract class GemElement extends HTMLElement {
447
446
  (item || !this.#renderList.length)
448
447
  ) {
449
448
  this.#render(item);
450
- addMicrotask(this.#execEffect);
449
+ if (!isServer) addMicrotask(this.#execEffect);
451
450
  }
452
451
  };
453
452
 
454
- #update = () => {
455
- if (this.#metadata.noBlocking) {
456
- noBlockingTaskList.add(this.#updateCallback);
453
+ // `@async()` 的元素在空闲时执行,SSR 需要同步执行
454
+ #schedule = (task: () => void) => {
455
+ if (this.#metadata.noBlocking && !isServer) {
456
+ noBlockingTaskList.add(task);
457
457
  } else {
458
- this.#updateCallback();
458
+ task();
459
459
  }
460
460
  };
461
461
 
462
+ #update = () => this.#schedule(this.#updateCallback);
463
+
464
+ // 挂载前(例如 `connectedCallback` 中的 property 赋值)不需要更新,挂载时会渲染
465
+ #scheduleUpdate = () => {
466
+ if (this.#isMounted) addMicrotask(this.#update);
467
+ };
468
+
462
469
  #prepareStyle = () => {
463
470
  const { adoptedStyleSheets = [] } = this.#metadata;
464
471
  const { shadowRoot, sheets } = this.#internals;
@@ -468,7 +475,7 @@ export abstract class GemElement extends HTMLElement {
468
475
  shadowRoot.adoptedStyleSheets = clsSheets.concat(sheets);
469
476
  } else {
470
477
  this.prepend(...sheets.map((e) => e.element));
471
- return appleCSSStyleSheet(this, clsSheets);
478
+ return adoptRootStyleSheets(this, clsSheets);
472
479
  }
473
480
  };
474
481
 
@@ -476,14 +483,7 @@ export abstract class GemElement extends HTMLElement {
476
483
  * 元素挂载后执行还未初始化的副作用
477
484
  * */
478
485
  #initEffect = () => {
479
- this.#effectList?.forEach((effectItem) => {
480
- const { callback, getDep, initialized } = effectItem;
481
- if (!initialized) {
482
- effectItem.values = getDep?.();
483
- effectItem.preCallback = callback(effectItem.values);
484
- effectItem.initialized = true;
485
- }
486
- });
486
+ this.#effectList.forEach((item) => !item.initialized && runEffect(item, item.getDep?.()));
487
487
  };
488
488
 
489
489
  /** @lifecycle 将来会彻底移除,现在做兼容实现*/ willMount?(): void | Promise<void>;
@@ -506,7 +506,8 @@ export abstract class GemElement extends HTMLElement {
506
506
  };
507
507
 
508
508
  #disconnectStore?: (() => void)[];
509
- #connectedCallback = async () => {
509
+ // 同步执行,SSR 需要捕获渲染错误
510
+ #connectedCallback = () => {
510
511
  if (this.#isAppendReason) {
511
512
  this.#isAppendReason = false;
512
513
  // https://github.com/mantou132/gem/issues/202
@@ -514,6 +515,7 @@ export abstract class GemElement extends HTMLElement {
514
515
  this.#clearStyle = this.#prepareStyle();
515
516
  return;
516
517
  }
518
+ if (ssrHooks.connect?.(this, this.#connectedCallback)) return;
517
519
  this.#notInCons = true;
518
520
  this.#compat();
519
521
 
@@ -531,7 +533,8 @@ export abstract class GemElement extends HTMLElement {
531
533
  this.#clearStyle = this.#prepareStyle();
532
534
  // 等待所有元素的样式被应用,再执行回调
533
535
  // 这让 mounted 和 effect 回调和其他回调一样保持一样的异步行为
534
- addMicrotask(this.#initEffect);
536
+ // SSR 不执行副作用
537
+ if (!isServer) addMicrotask(this.#initEffect);
535
538
  };
536
539
 
537
540
  /**
@@ -540,11 +543,7 @@ export abstract class GemElement extends HTMLElement {
540
543
  * use `@mounted`
541
544
  */
542
545
  connectedCallback(): Unique {
543
- if (this.#metadata.noBlocking) {
544
- noBlockingTaskList.add(this.#connectedCallback);
545
- } else {
546
- this.#connectedCallback();
547
- }
546
+ this.#schedule(this.#connectedCallback);
548
547
  return undefined as unknown as Unique;
549
548
  }
550
549
 
@@ -584,17 +583,9 @@ export abstract class GemElement extends HTMLElement {
584
583
  * ```
585
584
  * */
586
585
  effect = <K = any[] | undefined>(callback: EffectCallback<K>, getDep?: K extends any[] ? () => K : undefined) => {
587
- const effectItem: EffectItem<K> = {
588
- callback,
589
- getDep,
590
- initialized: this.#isMounted,
591
- fixed: !this.#notInCons,
592
- };
586
+ const effectItem: EffectItem<K> = { callback, getDep, fixed: !this.#notInCons };
593
587
  // 已挂载时立即执行副作用,未挂载时等挂载后执行
594
- if (this.#isMounted) {
595
- effectItem.values = getDep?.() as K;
596
- effectItem.preCallback = callback(effectItem.values);
597
- }
588
+ if (this.#isMounted) runEffect(effectItem, getDep?.() as K);
598
589
  this.#effectList.push(effectItem);
599
590
  };
600
591
 
@@ -627,7 +618,7 @@ export abstract class GemElement extends HTMLElement {
627
618
  * @helper
628
619
  * async
629
620
  */
630
- update = () => addMicrotask(this.#update);
621
+ update = () => this.#scheduleUpdate();
631
622
  }
632
623
 
633
624
  // 用来禁止 connectCallback 方法被覆盖而破坏 GemElement 的功能
@@ -0,0 +1,59 @@
1
+ // SSR 相关的功能都在这里
2
+ //
3
+ // `@mantou/gem-ssr` 在加载 Gem 前提供 `__GEM_SSR__`,返回当前请求的作用域,
4
+ // 客户端构建时 `unplugin-gem` 把 `globalThis.__GEM_SSR__` 替换成 `undefined`,SSR 代码会被压缩移除
5
+
6
+ import type { GemElement } from './reactive';
7
+
8
+ const { get, set, has, deleteProperty, defineProperty, getOwnPropertyDescriptor, ownKeys } = Reflect;
9
+
10
+ const getRequestScope: (() => WeakMap<object, any> | undefined) | undefined = (globalThis as any).__GEM_SSR__;
11
+
12
+ /** 服务端渲染时不执行副作用,同步更新 */
13
+ export const isServer = !!getRequestScope;
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
+
27
+ /**
28
+ * SSR 时返回当前请求中 `key` 对应的值,第一次访问时使用 `init` 创建,
29
+ * 不在请求中(包括客户端)时返回 `key` 本身
30
+ */
31
+ export const getRequestValue: <T extends object>(key: T, init: () => T) => T = getRequestScope
32
+ ? (key, init) => {
33
+ const scope = getRequestScope();
34
+ if (!scope) return key;
35
+ if (!scope.has(key)) scope.set(key, init());
36
+ return scope.get(key);
37
+ }
38
+ : (key) => key;
39
+
40
+ /**
41
+ * SSR 时对象的读写转到当前请求的副本上,副本在第一次访问时从原对象浅复制
42
+ *
43
+ * 嵌套的对象没有复制,原地修改会影响所有请求,应该替换顶层属性,例如 `store({ list: [...store.list, item] })`
44
+ */
45
+ export const isolate: <T extends object>(target: T) => T = getRequestScope
46
+ ? (target) => {
47
+ const copy = () => ({ ...target });
48
+ const current = () => getRequestValue(target, copy);
49
+ return new Proxy(target, {
50
+ get: (_, key) => get(current(), key),
51
+ set: (_, key, value) => set(current(), key, value),
52
+ has: (_, key) => has(current(), key),
53
+ deleteProperty: (_, key) => deleteProperty(current(), key),
54
+ defineProperty: (_, key, descriptor) => defineProperty(current(), key, descriptor),
55
+ getOwnPropertyDescriptor: (_, key) => getOwnPropertyDescriptor(current(), key),
56
+ ownKeys: () => ownKeys(current()),
57
+ });
58
+ }
59
+ : (target) => target;
package/src/lib/store.ts CHANGED
@@ -1,22 +1,37 @@
1
- import { addMicrotask, createUpdater, type Updater } from './utils';
1
+ import { getRequestValue, isolate, isServer, ssrHooks } 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
- export function createStore<T = any>(originStore: T) {
9
- const store = createUpdater(originStore, (value?: Partial<T>) => {
10
- // biome-ignore lint/plugin/assign: 未知类型
11
- Object.assign(store, value);
12
- _StoreListenerMap.get(store)?.forEach((func) => addMicrotask(func));
13
- });
14
- _StoreListenerMap.set(store, new Set<() => void>());
9
+ const createListeners = () => new Set<() => void>();
10
+
11
+ /**
12
+ * 创建 store,`key` 用来在 SSR 时同步服务端的值,`swc-plugin-gem` 的 `ssr` 选项会为模块顶层的 store 自动添加
13
+ */
14
+ export function createStore<T = any>(originStore: T, key?: string) {
15
+ const listeners = createListeners();
16
+ // SSR 时每个请求使用独立的值
17
+ const store = isolate(
18
+ createUpdater(originStore, (value?: Partial<T>) => {
19
+ assignState(store, value);
20
+ listeners.forEach(addMicrotask);
21
+ if (!isServer) return;
22
+ // SSR 时请求中添加的监听器只属于当前请求,其他请求更新 store 时不会触发
23
+ getRequestValue(listeners, createListeners).forEach(addMicrotask);
24
+ if (key) ssrHooks.updateStore?.(key, store);
25
+ }),
26
+ );
27
+ if (key) assignState(store, ssrHooks.createStore?.(key));
28
+ _StoreListenerMap.set(store, listeners);
15
29
  return store;
16
30
  }
17
31
 
18
32
  export function connect(store: Store<any>, func: () => void) {
19
- const listeners = _StoreListenerMap.get(store);
33
+ const storeListeners = _StoreListenerMap.get(store);
34
+ const listeners = storeListeners && getRequestValue(storeListeners, createListeners);
20
35
  listeners?.add(func);
21
36
  return () => listeners?.delete(func);
22
37
  }
package/src/lib/utils.ts CHANGED
@@ -1,4 +1,6 @@
1
- const { assign, fromEntries, entries, keys } = Object;
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
- export function addMicrotask(func: () => void, method = queueMicrotask) {
10
- if (microtaskSet.has(func)) return;
11
- method(() => {
12
- microtaskSet.delete(func);
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
- microtaskSet.add(func);
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
- assign(state, initState);
54
+ assignState(state, initState);
40
55
  // ts 不允许直接 Omit 掉 Fn 上的属性
41
56
  // 只能利用 ts plugin 移除多余的自动完成项
42
57
  return state as Fn & Omit<OverrideFn, keyof T> & T;
@@ -1 +1 @@
1
- export const version = '2.2.14';
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
- const pressed = new Promise((resolve) => el.addEventListener('press', resolve, { once: true }));
52
+ clock.install();
47
53
  dispatch('pointerdown', [0, 0]);
48
- await pressed;
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
- const pressed = new Promise((resolve) => el.addEventListener('press', resolve, { once: true }));
341
+ clock.install();
334
342
  dispatch('pointerdown', [0, 0]);
335
- await pressed;
343
+ clock.tick(251);
336
344
  dispatch('pointermove', [300, 0]);
337
345
  dispatch('pointermove', [340, 60]);
338
346
  dispatch('pointerup', [350, 60]);