@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.
- package/dist/gem.js +7 -7
- package/dist/gem.mjs +7 -7
- package/elements/base/link.d.ts.map +1 -1
- package/elements/base/link.js +6 -3
- package/elements/base/link.js.map +1 -1
- package/elements/base/modal-factory.d.ts +3 -2
- 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/elements/base/use.d.ts.map +1 -1
- package/elements/base/use.js +11 -0
- package/elements/base/use.js.map +1 -1
- package/helper/hmr.d.ts +3 -2
- package/helper/hmr.d.ts.map +1 -1
- package/helper/hmr.js +95 -23
- package/helper/hmr.js.map +1 -1
- package/helper/i18n.d.ts +2 -1
- package/helper/i18n.d.ts.map +1 -1
- package/helper/i18n.js +9 -1
- package/helper/i18n.js.map +1 -1
- package/helper/theme.d.ts +8 -4
- package/helper/theme.d.ts.map +1 -1
- package/helper/theme.js +15 -10
- package/helper/theme.js.map +1 -1
- package/lib/decorators.d.ts.map +1 -1
- package/lib/decorators.js +31 -46
- package/lib/decorators.js.map +1 -1
- package/lib/history.d.ts +10 -7
- package/lib/history.d.ts.map +1 -1
- package/lib/history.js +37 -51
- package/lib/history.js.map +1 -1
- package/lib/lit-html.d.ts +15 -0
- package/lib/lit-html.d.ts.map +1 -1
- package/lib/lit-html.js +3 -1
- package/lib/lit-html.js.map +1 -1
- package/lib/reactive.d.ts +6 -2
- package/lib/reactive.d.ts.map +1 -1
- package/lib/reactive.js +114 -118
- package/lib/reactive.js.map +1 -1
- package/lib/server.d.ts +26 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +45 -0
- package/lib/server.js.map +1 -0
- package/lib/store.d.ts +4 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +24 -9
- package/lib/store.js.map +1 -1
- package/lib/utils.d.ts +2 -2
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +20 -7
- package/lib/utils.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.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/link.ts +5 -3
- package/src/elements/base/route.ts +84 -49
- package/src/elements/base/use.ts +11 -0
- package/src/helper/hmr.ts +105 -37
- package/src/helper/i18n.ts +12 -1
- package/src/helper/theme.ts +19 -10
- package/src/lib/decorators.ts +38 -51
- package/src/lib/history.ts +37 -46
- package/src/lib/lit-html.ts +3 -1
- package/src/lib/reactive.ts +115 -124
- package/src/lib/server.ts +59 -0
- package/src/lib/store.ts +24 -9
- package/src/lib/utils.ts +22 -7
- 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 +33 -0
- package/src/test/history.test.ts +19 -0
- package/src/test/store.test.ts +12 -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,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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
//
|
|
92
|
-
#
|
|
93
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.#
|
|
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
|
|
166
|
+
const updateRootSheets = ({ counts, update }: RootSheets, sheets: CSSStyleSheet[], value: number) => {
|
|
178
167
|
let needUpdate = false;
|
|
179
168
|
sheets.forEach((e) => {
|
|
180
|
-
const count =
|
|
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
|
-
|
|
173
|
+
counts.set(e, count + value);
|
|
185
174
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
180
|
+
/** light DOM 元素的样式表添加到渲染根中,返回移除函数 */
|
|
181
|
+
const adoptRootStyleSheets = (ele: HTMLElement, sheets: CSSStyleSheet[]) => {
|
|
194
182
|
const root = ele.getRootNode() as ShadowRoot | Document;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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) => !
|
|
201
|
-
|
|
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
|
-
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
340
|
+
assignState(state, payload);
|
|
347
341
|
// 避免无限刷新
|
|
348
|
-
// 挂载前 set state 不应该触发更新
|
|
349
342
|
// SSR 中渲染 <gem-route>,其他情况的 SSR 应该也可以执行
|
|
350
|
-
if (!ele.#rendering
|
|
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.#
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
453
|
+
// `@async()` 的元素在空闲时执行,SSR 需要同步执行
|
|
454
|
+
#schedule = (task: () => void) => {
|
|
455
|
+
if (this.#metadata.noBlocking && !isServer) {
|
|
456
|
+
noBlockingTaskList.add(task);
|
|
457
457
|
} else {
|
|
458
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = () =>
|
|
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 {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
1
|
+
import { getRequestValue } from './server';
|
|
2
|
+
|
|
3
|
+
const { assign, fromEntries, entries, keys, defineProperty, hasOwn } = Object;
|
|
2
4
|
|
|
3
5
|
// 用原生 Chrome 找不到调用栈
|
|
4
6
|
function queueMicrotask(cb: () => void) {
|
|
@@ -6,13 +8,16 @@ function queueMicrotask(cb: () => void) {
|
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
const microtaskSet = new Set<() => void>();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const createMicrotaskSet = () => new Set<() => void>();
|
|
12
|
+
export function addMicrotask(func: () => void) {
|
|
13
|
+
// SSR 时不同请求的同一个函数都需要执行
|
|
14
|
+
const taskSet = getRequestValue(microtaskSet, createMicrotaskSet);
|
|
15
|
+
if (taskSet.has(func)) return;
|
|
16
|
+
queueMicrotask(() => {
|
|
17
|
+
taskSet.delete(func);
|
|
13
18
|
func();
|
|
14
19
|
});
|
|
15
|
-
|
|
20
|
+
taskSet.add(func);
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
type OverrideFn = {
|
|
@@ -29,6 +34,16 @@ type OverrideFn = {
|
|
|
29
34
|
[Symbol.metadata]: never;
|
|
30
35
|
};
|
|
31
36
|
|
|
37
|
+
// 删除后继承的 `name` `length` 只读,直接赋值会报错,需要先定义成自有属性
|
|
38
|
+
export function assignState(state: any, payload: any) {
|
|
39
|
+
for (const key of ['name', 'length']) {
|
|
40
|
+
if (payload && hasOwn(payload, key)) {
|
|
41
|
+
defineProperty(state, key, { value: payload[key], writable: true, enumerable: true, configurable: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
assign(state, payload);
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
// 注意 typeof state === 'function' 但是没有 Function 的方法和属性
|
|
33
48
|
export function createUpdater<T, Fn = (payload?: Partial<T>) => any>(initState: T, fn: Fn) {
|
|
34
49
|
const state: any = fn;
|
|
@@ -36,7 +51,7 @@ export function createUpdater<T, Fn = (payload?: Partial<T>) => any>(initState:
|
|
|
36
51
|
// setPrototypeOf(state, null);
|
|
37
52
|
delete state.name;
|
|
38
53
|
delete state.length;
|
|
39
|
-
|
|
54
|
+
assignState(state, initState);
|
|
40
55
|
// ts 不允许直接 Omit 掉 Fn 上的属性
|
|
41
56
|
// 只能利用 ts plugin 移除多余的自动完成项
|
|
42
57
|
return state as Fn & Omit<OverrideFn, keyof T> & T;
|
package/src/lib/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
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]);
|