@mantou/gem 2.2.15 → 2.3.1

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 (90) hide show
  1. package/dist/gem.js +7 -7
  2. package/dist/gem.mjs +7 -7
  3. package/elements/base/dialog.d.ts.map +1 -1
  4. package/elements/base/link.d.ts.map +1 -1
  5. package/elements/base/link.js +1 -1
  6. package/elements/base/link.js.map +1 -1
  7. package/elements/base/modal-factory.d.ts +20 -510
  8. package/elements/base/modal-factory.d.ts.map +1 -1
  9. package/elements/base/modal-factory.js +3 -2
  10. package/elements/base/modal-factory.js.map +1 -1
  11. package/elements/base/reflect.d.ts +7 -4
  12. package/elements/base/reflect.d.ts.map +1 -1
  13. package/elements/base/reflect.js +30 -4
  14. package/elements/base/reflect.js.map +1 -1
  15. package/elements/base/route.d.ts +11 -9
  16. package/elements/base/route.d.ts.map +1 -1
  17. package/elements/base/route.js +84 -44
  18. package/elements/base/route.js.map +1 -1
  19. package/elements/base/title.d.ts.map +1 -1
  20. package/elements/base/title.js +1 -1
  21. package/elements/base/title.js.map +1 -1
  22. package/helper/hmr.js.map +1 -1
  23. package/helper/i18n.d.ts +21 -17
  24. package/helper/i18n.d.ts.map +1 -1
  25. package/helper/i18n.js +172 -116
  26. package/helper/i18n.js.map +1 -1
  27. package/helper/logger.d.ts.map +1 -1
  28. package/helper/mediaquery.d.ts.map +1 -1
  29. package/helper/request.d.ts +7 -1
  30. package/helper/request.d.ts.map +1 -1
  31. package/helper/request.js.map +1 -1
  32. package/helper/theme.d.ts +8 -4
  33. package/helper/theme.d.ts.map +1 -1
  34. package/helper/theme.js +12 -9
  35. package/helper/theme.js.map +1 -1
  36. package/lib/decorators.d.ts +3 -2
  37. package/lib/decorators.d.ts.map +1 -1
  38. package/lib/decorators.js +29 -44
  39. package/lib/decorators.js.map +1 -1
  40. package/lib/directive.d.ts.map +1 -1
  41. package/lib/history.d.ts +46 -42
  42. package/lib/history.d.ts.map +1 -1
  43. package/lib/history.js +19 -37
  44. package/lib/history.js.map +1 -1
  45. package/lib/lit-html.d.ts.map +1 -1
  46. package/lib/lit-html.js.map +1 -1
  47. package/lib/reactive.d.ts +7 -9
  48. package/lib/reactive.d.ts.map +1 -1
  49. package/lib/reactive.js +86 -115
  50. package/lib/reactive.js.map +1 -1
  51. package/lib/repeat.d.ts +1 -1
  52. package/lib/repeat.d.ts.map +1 -1
  53. package/lib/repeat.js.map +1 -1
  54. package/lib/server.d.ts +12 -0
  55. package/lib/server.d.ts.map +1 -1
  56. package/lib/server.js +2 -0
  57. package/lib/server.js.map +1 -1
  58. package/lib/store.d.ts +0 -6
  59. package/lib/store.d.ts.map +1 -1
  60. package/lib/store.js +5 -17
  61. package/lib/store.js.map +1 -1
  62. package/lib/utils.d.ts.map +1 -1
  63. package/lib/version.d.ts +1 -1
  64. package/lib/version.d.ts.map +1 -1
  65. package/lib/version.js +1 -1
  66. package/lib/version.js.map +1 -1
  67. package/package.json +7 -8
  68. package/src/elements/base/modal-factory.ts +22 -6
  69. package/src/elements/base/reflect.ts +30 -4
  70. package/src/elements/base/route.ts +80 -50
  71. package/src/helper/i18n.ts +184 -127
  72. package/src/helper/theme.ts +16 -9
  73. package/src/lib/decorators.ts +36 -49
  74. package/src/lib/history.ts +19 -32
  75. package/src/lib/reactive.ts +90 -126
  76. package/src/lib/server.ts +14 -0
  77. package/src/lib/store.ts +3 -19
  78. package/src/lib/version.ts +1 -1
  79. package/src/test/elements/gesture.test.ts +13 -5
  80. package/src/test/elements/link.test.ts +12 -2
  81. package/src/test/elements/reflect.test.ts +20 -1
  82. package/src/test/elements/route.test.ts +102 -0
  83. package/src/test/elements/title.test.ts +7 -0
  84. package/src/test/elements/unsafe.test.ts +22 -0
  85. package/src/test/gem-element/advance.test.ts +106 -0
  86. package/src/test/gem-element/basic.test.ts +13 -0
  87. package/src/test/history.test.ts +19 -0
  88. package/src/test/theme.test.ts +8 -1
  89. package/src/test/utils.test.ts +10 -0
  90. package/src/test/utils.ts +56 -0
@@ -52,13 +52,14 @@ const globalParamsMap = new Map<string, HistoryParams>();
52
52
  const copyParamsMap = () => new Map([...globalParamsMap].map(([k, v]) => [k, { ...v }]));
53
53
  const getParamsMap = () => getRequestValue(globalParamsMap, copyParamsMap);
54
54
 
55
+ // 内部状态字段,不能由外部设置
56
+ const internalStateKeys = ['$key', '$hasCloseHandle', '$hasOpenHandle', '$hasShouldCloseHandle'] as const;
57
+
55
58
  function validData(data: any) {
56
- const { $key, $hasCloseHandle, $hasOpenHandle, $hasShouldCloseHandle } = data || {};
57
- if (store.$key === $key) return;
58
- if ($key) throw new GemError('`$key` is not allowed');
59
- if ($hasCloseHandle) throw new GemError('`$hasCloseHandle` is not allowed');
60
- if ($hasOpenHandle) throw new GemError('`$hasOpenHandle` is not allowed');
61
- if ($hasShouldCloseHandle) throw new GemError('`$hasShouldCloseHandle` is not allowed');
59
+ if (store.$key === data?.$key) return;
60
+ for (const key of internalStateKeys) {
61
+ if (data?.[key]) throw new GemError(`\`${key}\` is not allowed`);
62
+ }
62
63
  }
63
64
 
64
65
  function getUrlBarPath(internalPath: string) {
@@ -70,13 +71,17 @@ function getInternalPath(urlBarPath: string) {
70
71
  return urlBarPath.replace(new RegExp(`^${gemHistory.basePath}/`), '/');
71
72
  }
72
73
 
73
- function dispatchBeforeChangeEvent() {
74
+ // 切换到新的历史记录状态
75
+ function changeState(state: HistoryState) {
74
76
  gemHistory.dispatchEvent(new CustomEvent('beforechange'));
77
+ cleanObject(store);
78
+ store(state);
75
79
  }
76
80
 
77
81
  function normalizeParams(params: UpdateHistoryParams): HistoryParams {
78
82
  const current =
79
- gemHistory.getParams() || ({ path: getInternalPath(location.pathname), query: new QueryString() } as HistoryParams);
83
+ gemHistory.getParams() ||
84
+ ({ path: getInternalPath(location.pathname), query: new QueryString(), title: document.title } as HistoryParams);
80
85
  // 没提供 path 使用当前 path
81
86
  const path = params.path ? absoluteLocation(current.path, params.path) : getInternalPath(location.pathname);
82
87
  // 没提供 query 又没有提供 path 时使用当前 search
@@ -84,8 +89,9 @@ function normalizeParams(params: UpdateHistoryParams): HistoryParams {
84
89
  const queryChanged = String(newQueryObject) !== String(current.query);
85
90
  const query = queryChanged ? newQueryObject : current.query;
86
91
  const urlChanged = path !== current.path || queryChanged;
87
- // 没提供 title 又没有改变 URL 时使用当前 document.title
88
- const title = params.title || (urlChanged ? '' : document.title);
92
+ // 没提供 title 又没有改变 URL 时使用当前 title
93
+ // 不使用 document.title,它可能已被格式化(例如添加了网站名后缀)
94
+ const title = params.title || (urlChanged ? '' : current.title);
89
95
  const statusChanged = params.close || params.data || params.open || params.shouldClose;
90
96
  // 没提供 hash 又没有改变 URL 仅仅是状态更新时使用当前 hash
91
97
  const hash = decodeURIComponent(params.hash ?? (!urlChanged && statusChanged ? location.hash : ''));
@@ -105,9 +111,7 @@ function updateHistory(p: UpdateHistoryParams, native: typeof nativeHistory.push
105
111
  ...data,
106
112
  };
107
113
  getParamsMap().set(state.$key, params);
108
- dispatchBeforeChangeEvent();
109
- cleanObject(store);
110
- store(state);
114
+ changeState(state);
111
115
  const url = getUrlBarPath(path) + new QueryString(query) + hash;
112
116
  const prevHash = decodeURIComponent(location.hash);
113
117
  native(state, title, url);
@@ -116,23 +120,8 @@ function updateHistory(p: UpdateHistoryParams, native: typeof nativeHistory.push
116
120
 
117
121
  // 跨框架时,调用者对 basePath 无感知
118
122
  function updateHistoryByNative(data: any, title: string, originUrl: string, native: typeof nativeHistory.pushState) {
119
- validData(data);
120
- const state = {
121
- $key: getKey(),
122
- $title: title,
123
- ...(data || {}),
124
- };
125
123
  const { pathname, search, hash } = new URL(originUrl, location.origin + location.pathname);
126
- const params = normalizeParams({ path: pathname, query: new QueryString(search), hash, title, data });
127
- getParamsMap().set(state.$key, params);
128
- dispatchBeforeChangeEvent();
129
- cleanObject(store);
130
- store(state);
131
- const url = getUrlBarPath(pathname) + params.query + hash;
132
- const prevHash = location.hash;
133
- native(state, title, url);
134
- // `location.hash` 和 `hash` 都已经进行了 url 编码,可以直接进行相等判断
135
- if (prevHash !== hash) dispatchEvent(new CustomEvent('hashchange'));
124
+ updateHistory({ path: pathname, query: search, hash, title, data }, native);
136
125
  }
137
126
 
138
127
  const gemBasePathStore = createStore({
@@ -313,9 +302,7 @@ if (!globalThis._GEMHISTORY) {
313
302
  }
314
303
  }
315
304
 
316
- dispatchBeforeChangeEvent();
317
- cleanObject(store);
318
- store(newState);
305
+ changeState(newState);
319
306
  });
320
307
  }
321
308
 
@@ -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
- let hydrationEngine = (_value: unknown, _container: HTMLElement | DocumentFragment): Lit.RootPart | undefined => {
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
- hydrationEngine(value, container) || render(value, container);
32
+ ssrHooks.render?.(value, container) || render(value, container);
45
33
 
46
34
  const { assign, defineProperty } = Object;
47
35
 
@@ -59,14 +47,12 @@ export const UpdateToken = Symbol.for('gem@update');
59
47
  const BoundaryCSSState = 'gem-style-boundary';
60
48
  const ElementCSSState = 'gem-element';
61
49
 
62
- /**@internal */
50
+ /** 渲染出错时在元素上派发,`@fallback` 处理时取消,SSR 时 `@server` 方法出错也会派发 */
63
51
  export const _RenderErrorEvent = 'gem@render-error';
64
52
 
65
- // fix modal-factory type error
66
- const updateTokenAlias = UpdateToken;
67
-
68
- const rootStyleSheetInfo = new WeakMap<Document | ShadowRoot, Map<CSSStyleSheet, number>>();
69
- const rootUpdateFnMap = new WeakMap<Map<CSSStyleSheet, number>, () => void>();
53
+ // 渲染根中 light DOM 元素使用的样式表和使用次数,以及更新渲染根样式表的函数
54
+ type RootSheets = { counts: Map<CSSStyleSheet, number>; update: () => void };
55
+ const rootSheetsMap = new WeakMap<Document | ShadowRoot, RootSheets>();
70
56
 
71
57
  class GemCSSStyleSheet extends CSSStyleSheet {
72
58
  #ele?: HTMLStyleElement;
@@ -96,45 +82,37 @@ class GemCSSSheet {
96
82
  this.#content = v;
97
83
  }
98
84
 
99
- // 不需要 GC
100
- #record = new Map<any, GemCSSStyleSheet>();
101
- #used = new Map<GemCSSStyleSheet, string>();
85
+ // 对同一类 dom 只使用同一个样式表,key 是元素类或者 `this`,不需要 GC
86
+ #sheets = new Map<any, { sheet: GemCSSStyleSheet; scope: string }>();
87
+
88
+ #apply({ sheet, scope }: { sheet: GemCSSStyleSheet; scope: string }) {
89
+ sheet.replaceSync(scope ? `${scope}{ ${this.#content} }` : this.#content);
90
+ }
91
+
102
92
  getStyle(host?: HTMLElement, isEleTheme?: boolean) {
103
93
  const isLight = host && !(host as GemElement).internals?.shadowRoot;
104
-
105
- // 对同一类 dom 只使用同一个样式表
106
94
  const key = isLight ? host.constructor : this;
107
- if (!this.#record.has(key)) {
108
- const sheet = new GemCSSStyleSheet({ media: this.#media });
109
- this.#record.set(key, sheet);
110
- }
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);
95
+ let item = this.#sheets.get(key);
96
+ if (!item) {
97
+ // light dom 嵌套时需要选择子元素
98
+ // `> *` 实际上是多范围?是否存在性能问题
99
+ // 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用
100
+ // 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `adoptRootStyleSheets`)
101
+ const scope = !isLight
102
+ ? ''
103
+ : isEleTheme
104
+ ? '@scope'
105
+ : `@scope (${host.tagName}) to (:state(${BoundaryCSSState}) > *)`;
106
+ item = { sheet: new GemCSSStyleSheet({ media: this.#media }), scope };
107
+ this.#sheets.set(key, item);
108
+ this.#apply(item);
128
109
  }
129
-
130
- return sheet;
110
+ return item.sheet;
131
111
  }
132
112
 
133
113
  // 一般用于主题更新
134
114
  updateStyle() {
135
- this.#used.forEach((scope, sheet) => {
136
- sheet.replaceSync(scope ? `${scope}{${this.#content}}` : this.#content);
137
- });
115
+ this.#sheets.forEach((item) => this.#apply(item));
138
116
  }
139
117
  }
140
118
 
@@ -182,40 +160,38 @@ export function css<T extends Record<string, string>>(
182
160
  return sheet as Sheet<T>;
183
161
  }
184
162
 
185
- const updateStyleSheets = (map: Map<CSSStyleSheet, number>, sheets: CSSStyleSheet[], value: number) => {
163
+ const updateRootSheets = ({ counts, update }: RootSheets, sheets: CSSStyleSheet[], value: number) => {
186
164
  let needUpdate = false;
187
165
  sheets.forEach((e) => {
188
- const count = map.get(e) || 0;
166
+ const count = counts.get(e) || 0;
189
167
  needUpdate ||= (value === 1 && count === 0) || (value === -1 && count === 1);
190
168
  // count 为 0 时不立即删除,以便更新时识别是否外部样式
191
169
  // 但是只要挂载 document 的样式表就不会被 GC 了, 是否要在闲时 GC?
192
- map.set(e, count + value);
170
+ counts.set(e, count + value);
193
171
  });
194
- if (needUpdate) {
195
- // 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新
196
- // 在 SSR 中应该区别 effect 强制执行
197
- addMicrotask(rootUpdateFnMap.get(map)!);
198
- }
172
+ // 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新
173
+ // 在 SSR 中应该区别 effect 强制执行
174
+ if (needUpdate) addMicrotask(update);
199
175
  };
200
176
 
201
- const appleCSSStyleSheet = (ele: HTMLElement, sheets: CSSStyleSheet[]) => {
177
+ /** light DOM 元素的样式表添加到渲染根中,返回移除函数 */
178
+ const adoptRootStyleSheets = (ele: HTMLElement, sheets: CSSStyleSheet[]) => {
202
179
  const root = ele.getRootNode() as ShadowRoot | Document;
203
- if (!rootStyleSheetInfo.has(root)) {
204
- const map = new Map<CSSStyleSheet, number>();
205
- rootStyleSheetInfo.set(root, map);
206
- rootUpdateFnMap.set(map, () => {
180
+ let rootSheets = rootSheetsMap.get(root);
181
+ if (!rootSheets) {
182
+ const counts = new Map<CSSStyleSheet, number>();
183
+ const update = () => {
207
184
  // 先找到外部样式表
208
- const newSheets = (root.adoptedStyleSheets || []).filter((e) => !map.has(e));
209
- map.forEach((count, sheet) => count && newSheets.push(sheet));
185
+ const newSheets = (root.adoptedStyleSheets || []).filter((e) => !counts.has(e));
186
+ counts.forEach((count, sheet) => count && newSheets.push(sheet));
210
187
  // 外层元素的样式要放到最后,以提升优先级,但是只考虑第一次出现样式表的位置
211
188
  root.adoptedStyleSheets = newSheets.reverse();
212
- });
189
+ };
190
+ rootSheets = { counts, update };
191
+ rootSheetsMap.set(root, rootSheets);
213
192
  }
214
-
215
- const map = rootStyleSheetInfo.get(root)!;
216
-
217
- updateStyleSheets(map, sheets, 1);
218
- return () => updateStyleSheets(map, sheets, -1);
193
+ updateRootSheets(rootSheets, sheets, 1);
194
+ return () => updateRootSheets(rootSheets, sheets, -1);
219
195
  };
220
196
 
221
197
  /**必须使用在字段中,否则会读取到错误的实例 */
@@ -232,23 +208,25 @@ type EffectItem<T> = {
232
208
  preCallback?: any;
233
209
  };
234
210
 
235
- function execEffectList(list?: EffectItem<any>[]) {
236
- list?.forEach((effectItem) => {
237
- const { callback, getDep, values, preCallback } = effectItem;
238
- const newValues = getDep?.();
239
- if (!getDep || !values || isArrayChange(values, newValues)) {
240
- execCallback(preCallback);
241
- effectItem.preCallback = callback(newValues, values);
242
- effectItem.values = newValues;
243
- // 更新时也要设置,避免异步的 `#initEffect` 重复执行
244
- effectItem.initialized = true;
245
- }
211
+ function runEffect<T>(item: EffectItem<T>, values: T) {
212
+ execCallback(item.preCallback);
213
+ item.preCallback = item.callback(values, item.values);
214
+ item.values = values;
215
+ // 更新时也要设置,避免异步的 `#initEffect` 重复执行
216
+ item.initialized = true;
217
+ }
218
+
219
+ function execEffectList(list: EffectItem<any>[]) {
220
+ list.forEach((item) => {
221
+ const values = item.getDep?.();
222
+ if (!item.getDep || !item.values || isArrayChange(item.values, values)) runEffect(item, values);
246
223
  });
247
224
  }
248
225
 
249
226
  function clearEffect(list: EffectItem<any>[]) {
250
227
  return list.filter((e) => {
251
228
  execCallback(e.preCallback);
229
+ e.preCallback = undefined;
252
230
  e.initialized = false;
253
231
  e.values = undefined;
254
232
  return e.fixed;
@@ -277,6 +255,8 @@ export type Metadata = Partial<ShadowRootInit> & {
277
255
  >;
278
256
  // 实例化时使用到,DevTools 需要读取
279
257
  observedStores?: Store<any>[];
258
+ /** attribute 对应的字段名,用于 `setAttribute` 等方法设置字段 */
259
+ attrProps?: Record<string, string>;
280
260
  adoptedStyleSheets?: Sheet<unknown>[];
281
261
  // 以下静态字段仅供外部读取,没有实际作用
282
262
  observedProperties?: string[];
@@ -285,8 +265,6 @@ export type Metadata = Partial<ShadowRootInit> & {
285
265
  definedCSSStates?: string[];
286
266
  definedParts?: string[];
287
267
  definedSlots?: string[];
288
- /**@deprecated GemDevtools 在使用*/
289
- definedRefs?: string[];
290
268
  };
291
269
 
292
270
  // global render task pool
@@ -340,9 +318,8 @@ export abstract class GemElement extends HTMLElement {
340
318
  #rendering?: boolean;
341
319
  #clearStyle?: (() => void) | undefined;
342
320
 
343
- [updateTokenAlias]() {
344
- // 避免 `connectedCallback` 中的 property 赋值造成多余更新
345
- if (this.#isMounted) addMicrotask(this.#update);
321
+ [UpdateToken]() {
322
+ this.#scheduleUpdate();
346
323
  }
347
324
 
348
325
  static {
@@ -357,9 +334,8 @@ export abstract class GemElement extends HTMLElement {
357
334
  }
358
335
  assignState(state, payload);
359
336
  // 避免无限刷新
360
- // 挂载前 set state 不应该触发更新
361
337
  // SSR 中渲染 <gem-route>,其他情况的 SSR 应该也可以执行
362
- if (!ele.#rendering && ele.#isMounted) addMicrotask(ele.#update);
338
+ if (!ele.#rendering) ele.#scheduleUpdate();
363
339
  });
364
340
  ele.#internals.stateList.push(state);
365
341
  return state;
@@ -378,6 +354,10 @@ export abstract class GemElement extends HTMLElement {
378
354
  // 这里认为 rest 不会有自定义字段和 ShadowRootInit 参数冲突
379
355
  this.#renderRoot = !mode ? this : this.shadowRoot || this.attachShadow({ mode, delegatesFocus, ...rest });
380
356
  this.#internals = this.attachInternals() as GemElement['internals'];
357
+ // 定义前添加的 ShadowRoot 对 `ElementInternals` 不可见,例如客户端填充了服务端内容的延时视图元素
358
+ if (this.#renderRoot !== this && !this.#internals.shadowRoot) {
359
+ defineProperty(this.#internals, 'shadowRoot', { value: this.#renderRoot });
360
+ }
381
361
  this.#internals.stateList = [];
382
362
  this.#internals.sheets = [];
383
363
 
@@ -423,10 +403,6 @@ export abstract class GemElement extends HTMLElement {
423
403
  execEffectList(this.#effectList);
424
404
  };
425
405
 
426
- #execMemo = () => {
427
- execEffectList(this.#memoList);
428
- };
429
-
430
406
  /**
431
407
  * - 条件渲染
432
408
  * - 没有 `render` 时显示子内容
@@ -437,7 +413,7 @@ export abstract class GemElement extends HTMLElement {
437
413
  #render = (item?: RenderItem) => {
438
414
  try {
439
415
  this.#rendering = true;
440
- this.#execMemo();
416
+ execEffectList(this.#memoList);
441
417
  const isLight = this.#renderRoot === this;
442
418
  const temp = item ? item.render() : isLight ? undefined : html`<slot></slot>`;
443
419
  if (temp === undefined) return;
@@ -469,15 +445,22 @@ export abstract class GemElement extends HTMLElement {
469
445
  }
470
446
  };
471
447
 
472
- #update = () => {
473
- // SSR 需要同步渲染
448
+ // `@async()` 的元素在空闲时执行,SSR 需要同步执行
449
+ #schedule = (task: () => void) => {
474
450
  if (this.#metadata.noBlocking && !isServer) {
475
- noBlockingTaskList.add(this.#updateCallback);
451
+ noBlockingTaskList.add(task);
476
452
  } else {
477
- this.#updateCallback();
453
+ task();
478
454
  }
479
455
  };
480
456
 
457
+ #update = () => this.#schedule(this.#updateCallback);
458
+
459
+ // 挂载前(例如 `connectedCallback` 中的 property 赋值)不需要更新,挂载时会渲染
460
+ #scheduleUpdate = () => {
461
+ if (this.#isMounted) addMicrotask(this.#update);
462
+ };
463
+
481
464
  #prepareStyle = () => {
482
465
  const { adoptedStyleSheets = [] } = this.#metadata;
483
466
  const { shadowRoot, sheets } = this.#internals;
@@ -487,7 +470,7 @@ export abstract class GemElement extends HTMLElement {
487
470
  shadowRoot.adoptedStyleSheets = clsSheets.concat(sheets);
488
471
  } else {
489
472
  this.prepend(...sheets.map((e) => e.element));
490
- return appleCSSStyleSheet(this, clsSheets);
473
+ return adoptRootStyleSheets(this, clsSheets);
491
474
  }
492
475
  };
493
476
 
@@ -495,14 +478,7 @@ export abstract class GemElement extends HTMLElement {
495
478
  * 元素挂载后执行还未初始化的副作用
496
479
  * */
497
480
  #initEffect = () => {
498
- this.#effectList?.forEach((effectItem) => {
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
- });
481
+ this.#effectList.forEach((item) => !item.initialized && runEffect(item, item.getDep?.()));
506
482
  };
507
483
 
508
484
  /** @lifecycle 将来会彻底移除,现在做兼容实现*/ willMount?(): void | Promise<void>;
@@ -534,6 +510,7 @@ export abstract class GemElement extends HTMLElement {
534
510
  this.#clearStyle = this.#prepareStyle();
535
511
  return;
536
512
  }
513
+ if (ssrHooks.connect?.(this, this.#connectedCallback)) return;
537
514
  this.#notInCons = true;
538
515
  this.#compat();
539
516
 
@@ -546,7 +523,6 @@ export abstract class GemElement extends HTMLElement {
546
523
  if (!mode && this.#renderList.length && !penetrable) this.#internals.states.add(BoundaryCSSState);
547
524
 
548
525
  this.#disconnectStore = observedStores?.map((store) => connect(store, this.#update));
549
- restoreState?.(this);
550
526
  this.#render(this.#renderItem);
551
527
  this.#isMounted = true;
552
528
  this.#clearStyle = this.#prepareStyle();
@@ -562,11 +538,7 @@ export abstract class GemElement extends HTMLElement {
562
538
  * use `@mounted`
563
539
  */
564
540
  connectedCallback(): Unique {
565
- if (this.#metadata.noBlocking && !isServer) {
566
- noBlockingTaskList.add(this.#connectedCallback);
567
- } else {
568
- this.#connectedCallback();
569
- }
541
+ this.#schedule(this.#connectedCallback);
570
542
  return undefined as unknown as Unique;
571
543
  }
572
544
 
@@ -606,17 +578,9 @@ export abstract class GemElement extends HTMLElement {
606
578
  * ```
607
579
  * */
608
580
  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
- };
581
+ const effectItem: EffectItem<K> = { callback, getDep, fixed: !this.#notInCons };
615
582
  // 已挂载时立即执行副作用,未挂载时等挂载后执行
616
- if (this.#isMounted) {
617
- effectItem.values = getDep?.() as K;
618
- effectItem.preCallback = callback(effectItem.values);
619
- }
583
+ if (this.#isMounted) runEffect(effectItem, getDep?.() as K);
620
584
  this.#effectList.push(effectItem);
621
585
  };
622
586
 
@@ -649,7 +613,7 @@ export abstract class GemElement extends HTMLElement {
649
613
  * @helper
650
614
  * async
651
615
  */
652
- update = () => addMicrotask(this.#update);
616
+ update = () => this.#scheduleUpdate();
653
617
  }
654
618
 
655
619
  // 用来禁止 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
- const updated = _getUpdatedStores();
39
- // 不在请求中时更新的是全局的值,客户端也一样
40
- if (key && updated !== updatedStores) updated.set(key, store);
24
+ if (key) ssrHooks.updateStore?.(key, store);
41
25
  }),
42
26
  );
43
- if (key && restoreStore) assignState(store, restoreStore(key));
27
+ if (key) assignState(store, ssrHooks.createStore?.(key));
44
28
  _StoreListenerMap.set(store, listeners);
45
29
  return store;
46
30
  }
@@ -1 +1 @@
1
- export const version = '2.2.15';
1
+ export const version = '2.3.1';
@@ -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]);
@@ -1,6 +1,7 @@
1
- import type { GemActiveLinkElement } from '../../elements/link';
1
+ import type { GemActiveLinkElement, GemLinkElement } from '../../elements/link';
2
2
  import { html } from '../../lib/element';
3
- import { expect, fixture } from '../utils';
3
+ import { history } from '../../lib/history';
4
+ import { expect, fixture, nextFrame } from '../utils';
4
5
 
5
6
  import '../../elements/link';
6
7
 
@@ -11,4 +12,13 @@ describe('GemLinkElement', () => {
11
12
  );
12
13
  expect(ele.active).to.equal(true);
13
14
  });
15
+ it('basePath', async () => {
16
+ const ele: GemLinkElement = await fixture(html`<gem-link href="/a">a</gem-link>`);
17
+ const getHref = () => new URL(ele.shadowRoot!.querySelector('a')!.href).pathname;
18
+ expect(getHref()).to.equal('/a');
19
+ history.basePath = '/base';
20
+ await nextFrame();
21
+ expect(getHref()).to.equal('/base/a');
22
+ history.basePath = '';
23
+ });
14
24
  });