@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.
Files changed (66) 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 +1 -1
  7. package/elements/base/modal-factory.d.ts.map +1 -1
  8. package/elements/base/use.d.ts.map +1 -1
  9. package/elements/base/use.js +11 -0
  10. package/elements/base/use.js.map +1 -1
  11. package/helper/hmr.d.ts +3 -2
  12. package/helper/hmr.d.ts.map +1 -1
  13. package/helper/hmr.js +95 -23
  14. package/helper/hmr.js.map +1 -1
  15. package/helper/i18n.d.ts +2 -1
  16. package/helper/i18n.d.ts.map +1 -1
  17. package/helper/i18n.js +9 -1
  18. package/helper/i18n.js.map +1 -1
  19. package/helper/theme.d.ts.map +1 -1
  20. package/helper/theme.js +3 -1
  21. package/helper/theme.js.map +1 -1
  22. package/lib/decorators.d.ts.map +1 -1
  23. package/lib/decorators.js +4 -3
  24. package/lib/decorators.js.map +1 -1
  25. package/lib/history.d.ts +10 -7
  26. package/lib/history.d.ts.map +1 -1
  27. package/lib/history.js +20 -16
  28. package/lib/history.js.map +1 -1
  29. package/lib/lit-html.d.ts +15 -0
  30. package/lib/lit-html.d.ts.map +1 -1
  31. package/lib/lit-html.js +3 -1
  32. package/lib/lit-html.js.map +1 -1
  33. package/lib/reactive.d.ts +4 -1
  34. package/lib/reactive.d.ts.map +1 -1
  35. package/lib/reactive.js +41 -18
  36. package/lib/reactive.js.map +1 -1
  37. package/lib/server.d.ts +14 -0
  38. package/lib/server.d.ts.map +1 -0
  39. package/lib/server.js +43 -0
  40. package/lib/server.js.map +1 -0
  41. package/lib/store.d.ts +10 -1
  42. package/lib/store.d.ts.map +1 -1
  43. package/lib/store.js +36 -9
  44. package/lib/store.js.map +1 -1
  45. package/lib/utils.d.ts +2 -2
  46. package/lib/utils.d.ts.map +1 -1
  47. package/lib/utils.js +20 -7
  48. package/lib/utils.js.map +1 -1
  49. package/lib/version.d.ts +1 -1
  50. package/lib/version.js +1 -1
  51. package/package.json +1 -1
  52. package/src/elements/base/link.ts +5 -3
  53. package/src/elements/base/use.ts +11 -0
  54. package/src/helper/hmr.ts +105 -37
  55. package/src/helper/i18n.ts +12 -1
  56. package/src/helper/theme.ts +3 -1
  57. package/src/lib/decorators.ts +4 -3
  58. package/src/lib/history.ts +20 -16
  59. package/src/lib/lit-html.ts +3 -1
  60. package/src/lib/reactive.ts +40 -18
  61. package/src/lib/server.ts +45 -0
  62. package/src/lib/store.ts +40 -9
  63. package/src/lib/utils.ts +22 -7
  64. package/src/lib/version.ts +1 -1
  65. package/src/test/gem-element/basic.test.ts +20 -0
  66. package/src/test/store.test.ts +12 -0
@@ -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
- if (CSS.supports(`font:-apple-system-body`)) return theme(access.get(this).apply(this));
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
@@ -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, GemElement, render, UpdateToken } from './reactive';
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, ({ detail }: CustomEvent) => {
304
- render(access.get.call(this, this).apply(this, [detail]), this.internals.shadowRoot || this);
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
  };
@@ -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 paramsMap = new Map<string, HistoryParams>();
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
- paramsMap.get(store.$key) ||
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
- paramsMap.set(state.$key, params);
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
- paramsMap.set(state.$key, params);
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(paramsMap.get(store.$key)!, { path: getInternalPath(location.pathname) });
156
+ Object.assign(this.getParams(), { path: getInternalPath(location.pathname) });
153
157
  }
154
158
  getParams() {
155
- return paramsMap.get(store.$key)!;
159
+ return getParamsMap().get(store.$key)!;
156
160
  }
157
161
  updateParams(params: UpdateHistoryParams) {
158
- Object.assign(paramsMap.get(store.$key)!, params);
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
- paramsMap.get(store.$key)?.close?.();
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
- paramsMap.set(store.$key, params);
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
- paramsMap.set(store.$key, params);
235
+ getParamsMap().set(store.$key, params);
232
236
  }
233
237
 
234
238
  connect(gemTitleStore, () => {
235
- const params = paramsMap.get(store.$key);
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 (!paramsMap.has(newState.$key)) {
273
+ if (!getParamsMap().has(newState.$key)) {
270
274
  const { pathname, search, hash } = location;
271
- paramsMap.set(newState.$key, {
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
- paramsMap.get(newState.$key)?.open?.();
291
+ getParamsMap().get(newState.$key)?.open?.();
288
292
  } else if (prevState.$hasCloseHandle) {
289
- const prevParams = paramsMap.get(prevState.$key);
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();
@@ -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
@@ -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
- export function setHydrationEngine(engine: typeof hydrationEngine) {
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)!, ssrNativeQueueMicrotask);
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
- tick(timeStamp);
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 (!ssrNativeQueueMicrotask && ele.#isMounted && effect && !effect.initialized) {
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
- assign(state, payload);
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, ssrNativeQueueMicrotask);
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
- const r = temp !== null ? temp : html`<style>${isLight ? '@scope' : ':host'}{&{display:none!important;}}</style>`;
433
- hydrationEngine(r, this.#renderRoot) || render(r, this.#renderRoot);
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
- this.dispatchEvent(new CustomEvent(_RenderErrorEvent, { bubbles: true, composed: true, detail: err }));
436
- throw err;
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
- if (this.#metadata.noBlocking) {
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
- #connectedCallback = async () => {
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
- addMicrotask(this.#initEffect);
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 { addMicrotask, createUpdater, type Updater } from './utils';
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
- 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
+ // 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 listeners = _StoreListenerMap.get(store);
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
- 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.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
+ });
@@ -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
+ });