weifuwu 0.55.2 → 0.56.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/README.md +263 -29
- package/dist/client/app.d.ts +3 -1
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.js +354 -88
- package/dist/client/reactive.d.ts +6 -0
- package/dist/client/render.d.ts +11 -0
- package/dist/client/route-match.d.ts +22 -0
- package/dist/client/types.d.ts +17 -0
- package/dist/client/vnode.d.ts +32 -1
- package/dist/components/Table/Table.d.ts +2 -0
- package/dist/components/ThemeSwitch/ThemeSwitch.d.ts +27 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +136 -19
- package/dist/components/style.css +285 -44
- package/dist/dev/index.js +23 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +320 -7
- package/dist/layout/weifuwu-layout.css +180 -15
- package/dist/ui/html-safe.d.ts +11 -0
- package/dist/ui/index.d.ts +11 -0
- package/dist/ui/ssr-page.d.ts +41 -0
- package/dist/ui/ssr.d.ts +35 -0
- package/package.json +7 -4
package/dist/client/index.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
// src/client/vnode.ts
|
|
2
|
+
var ASYNC_MARK = "__wfAsyncComponent";
|
|
3
|
+
function asyncComponent(factory) {
|
|
4
|
+
const fn = factory;
|
|
5
|
+
fn[ASYNC_MARK] = true;
|
|
6
|
+
return fn;
|
|
7
|
+
}
|
|
8
|
+
function isAsyncComponent(type) {
|
|
9
|
+
return typeof type === "function" && type?.[ASYNC_MARK] === true;
|
|
10
|
+
}
|
|
2
11
|
var Fragment = /* @__PURE__ */ Symbol("Fragment");
|
|
3
12
|
var Portal = /* @__PURE__ */ Symbol("Portal");
|
|
4
13
|
function jsx(type, props, key) {
|
|
@@ -56,10 +65,11 @@ function renderValue(v, ctx) {
|
|
|
56
65
|
}
|
|
57
66
|
if (vnode.type === Fragment) {
|
|
58
67
|
const frag = document.createDocumentFragment();
|
|
59
|
-
|
|
68
|
+
const children = vnode.props?.children == null ? [] : Array.isArray(vnode.props.children) ? vnode.props.children : [vnode.props.children];
|
|
69
|
+
for (const child of children) {
|
|
60
70
|
const node = renderValue(child, ctx);
|
|
61
71
|
if (node != null) frag.appendChild(node);
|
|
62
|
-
}
|
|
72
|
+
}
|
|
63
73
|
return frag;
|
|
64
74
|
}
|
|
65
75
|
if (typeof vnode.type === "function") {
|
|
@@ -102,6 +112,60 @@ function renderValue(v, ctx) {
|
|
|
102
112
|
if (typeof vnode.props?.ref === "function") vnode.props.ref(el);
|
|
103
113
|
return el;
|
|
104
114
|
}
|
|
115
|
+
var asyncFactoryCache = /* @__PURE__ */ new WeakMap();
|
|
116
|
+
function clearAsyncComponentCache() {
|
|
117
|
+
asyncFactoryCache = /* @__PURE__ */ new WeakMap();
|
|
118
|
+
}
|
|
119
|
+
function startAsyncFactory(Comp, ctx) {
|
|
120
|
+
const existing = asyncFactoryCache.get(Comp);
|
|
121
|
+
if (existing) return existing;
|
|
122
|
+
const entry = { promise: null };
|
|
123
|
+
entry.promise = Promise.resolve().then(() => Comp(ctx)).then((def) => {
|
|
124
|
+
if (typeof def !== "function") {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`asyncComponent factory <${Comp.name || "anonymous"}> must return a Component (initProps, ctx) => (props) => VNode.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
entry.resolved = def;
|
|
130
|
+
return def;
|
|
131
|
+
});
|
|
132
|
+
asyncFactoryCache.set(Comp, entry);
|
|
133
|
+
return entry;
|
|
134
|
+
}
|
|
135
|
+
async function resolveAsyncFactory(Comp, ctx) {
|
|
136
|
+
return startAsyncFactory(Comp, ctx).promise;
|
|
137
|
+
}
|
|
138
|
+
function resolveAsyncFactorySync(Comp) {
|
|
139
|
+
return asyncFactoryCache.get(Comp)?.resolved;
|
|
140
|
+
}
|
|
141
|
+
function scheduleFullReRender(ctx) {
|
|
142
|
+
const ui = ctx.ui;
|
|
143
|
+
if (ui && typeof ui.render === "function") ui.render(["_wf_root"]);
|
|
144
|
+
}
|
|
145
|
+
function mountComponent(Comp, props, vnode, ctx) {
|
|
146
|
+
let def;
|
|
147
|
+
if (isAsyncComponent(Comp)) {
|
|
148
|
+
def = resolveAsyncFactorySync(Comp);
|
|
149
|
+
if (!def) {
|
|
150
|
+
void startAsyncFactory(Comp, ctx).promise.then(
|
|
151
|
+
() => scheduleFullReRender(ctx),
|
|
152
|
+
() => {
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
def = Comp;
|
|
159
|
+
}
|
|
160
|
+
let childVNode = def(props, ctx);
|
|
161
|
+
if (typeof childVNode !== "function") {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
vnode._render = childVNode;
|
|
167
|
+
return childVNode(props);
|
|
168
|
+
}
|
|
105
169
|
function renderComponent(Comp, props, vnode, ctx) {
|
|
106
170
|
;
|
|
107
171
|
ctx.ui = ctx.ui ?? {};
|
|
@@ -116,14 +180,7 @@ function renderComponent(Comp, props, vnode, ctx) {
|
|
|
116
180
|
vnode._ctxVersion = childCtx.ui._ctxVersion ?? 0;
|
|
117
181
|
let childVNode;
|
|
118
182
|
try {
|
|
119
|
-
childVNode = Comp
|
|
120
|
-
if (typeof childVNode !== "function") {
|
|
121
|
-
throw new Error(
|
|
122
|
-
`Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
vnode._render = childVNode;
|
|
126
|
-
childVNode = childVNode(props);
|
|
183
|
+
childVNode = mountComponent(Comp, props, vnode, childCtx);
|
|
127
184
|
} catch (e) {
|
|
128
185
|
const errHandler = ctx.ui?._errorHandler;
|
|
129
186
|
if (errHandler) {
|
|
@@ -193,14 +250,6 @@ function patchPortal(oldV, newV, ctx) {
|
|
|
193
250
|
ensureKeys(oldChildren, newChildren);
|
|
194
251
|
patchKeyedChildren(sub, oldChildren, newChildren, ctx);
|
|
195
252
|
}
|
|
196
|
-
function forEach(children, fn) {
|
|
197
|
-
if (children == null) return;
|
|
198
|
-
if (Array.isArray(children)) {
|
|
199
|
-
children.forEach(fn);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
fn(children);
|
|
203
|
-
}
|
|
204
253
|
function flattenChildren(children) {
|
|
205
254
|
if (children == null) return [];
|
|
206
255
|
if (!Array.isArray(children)) return [children];
|
|
@@ -300,11 +349,7 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
|
|
|
300
349
|
if (typeof newV._render === "function") {
|
|
301
350
|
childNew = newV._render(newV.props);
|
|
302
351
|
} else {
|
|
303
|
-
childNew = comp
|
|
304
|
-
if (typeof childNew === "function") {
|
|
305
|
-
newV._render = childNew;
|
|
306
|
-
childNew = childNew(newV.props);
|
|
307
|
-
}
|
|
352
|
+
childNew = mountComponent(comp, newV.props, newV, childCtx);
|
|
308
353
|
}
|
|
309
354
|
} catch (e) {
|
|
310
355
|
const errHandler = ctx.ui?._errorHandler;
|
|
@@ -395,8 +440,8 @@ function patchProps(el, oldProps, newProps) {
|
|
|
395
440
|
if (newVal !== oldVal) el.innerHTML = String(newVal ?? "");
|
|
396
441
|
} else if (newVal !== oldVal) {
|
|
397
442
|
if (key === "class" || key === "className") {
|
|
398
|
-
if (el instanceof SVGElement) el.setAttribute("class",
|
|
399
|
-
else el.className =
|
|
443
|
+
if (el instanceof SVGElement) el.setAttribute("class", classToString(newVal));
|
|
444
|
+
else el.className = classToString(newVal);
|
|
400
445
|
} else if (key === "style" && typeof newVal === "object") {
|
|
401
446
|
const st = el.style;
|
|
402
447
|
for (const sk of Object.keys(newVal)) {
|
|
@@ -425,6 +470,14 @@ function patchProps(el, oldProps, newProps) {
|
|
|
425
470
|
}
|
|
426
471
|
}
|
|
427
472
|
}
|
|
473
|
+
function classToString(v) {
|
|
474
|
+
if (typeof v === "string") return v;
|
|
475
|
+
if (Array.isArray(v)) return v.filter(Boolean).join(" ");
|
|
476
|
+
if (v && typeof v === "object") {
|
|
477
|
+
return Object.entries(v).filter(([, b]) => b).map(([k]) => k).join(" ");
|
|
478
|
+
}
|
|
479
|
+
return "";
|
|
480
|
+
}
|
|
428
481
|
function getKey(input) {
|
|
429
482
|
if (input == null || typeof input !== "object") return void 0;
|
|
430
483
|
return input.key;
|
|
@@ -596,6 +649,15 @@ function cleanupPortalChildren(vnode) {
|
|
|
596
649
|
function callRefCleanup(input) {
|
|
597
650
|
if (input == null || typeof input !== "object") return;
|
|
598
651
|
const vnode = input;
|
|
652
|
+
if (vnode._id) {
|
|
653
|
+
if (vnode._customId) idRegistry.delete(vnode._customId);
|
|
654
|
+
idRegistry.delete(vnode._id);
|
|
655
|
+
vnode._id = void 0;
|
|
656
|
+
vnode._customId = void 0;
|
|
657
|
+
vnode._render = void 0;
|
|
658
|
+
vnode._parentNode = void 0;
|
|
659
|
+
vnode._refNode = void 0;
|
|
660
|
+
}
|
|
599
661
|
if (vnode._child != null) {
|
|
600
662
|
if (Array.isArray(vnode._child)) {
|
|
601
663
|
for (const child of vnode._child) {
|
|
@@ -619,6 +681,192 @@ function callRefCleanup(input) {
|
|
|
619
681
|
vnode._remoteEl = void 0;
|
|
620
682
|
}
|
|
621
683
|
}
|
|
684
|
+
function cursorAdvance(c) {
|
|
685
|
+
c.node = c.node ? c.node.nextSibling : null;
|
|
686
|
+
}
|
|
687
|
+
function cursorInsert(c, n) {
|
|
688
|
+
if (c.node && c.node.parentNode) c.node.parentNode.insertBefore(n, c.node);
|
|
689
|
+
else c.parent.appendChild(n);
|
|
690
|
+
}
|
|
691
|
+
function cursorReplace(c, n) {
|
|
692
|
+
if (c.node && c.node.parentNode) {
|
|
693
|
+
c.node.parentNode.replaceChild(n, c.node);
|
|
694
|
+
cursorAdvance(c);
|
|
695
|
+
} else {
|
|
696
|
+
c.parent.appendChild(n);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async function renderValueHydrating(v, ctx, c) {
|
|
700
|
+
if (v == null || typeof v === "boolean") return null;
|
|
701
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
702
|
+
const text = String(v);
|
|
703
|
+
if (c.node && c.node.nodeType === 3) {
|
|
704
|
+
if (c.node.textContent !== text) c.node.textContent = text;
|
|
705
|
+
cursorAdvance(c);
|
|
706
|
+
return c.node;
|
|
707
|
+
}
|
|
708
|
+
const tn = document.createTextNode(text);
|
|
709
|
+
cursorInsert(c, tn);
|
|
710
|
+
return tn;
|
|
711
|
+
}
|
|
712
|
+
if (Array.isArray(v)) {
|
|
713
|
+
let first = null;
|
|
714
|
+
for (const item of v) {
|
|
715
|
+
const n = await renderValueHydrating(item, ctx, c);
|
|
716
|
+
if (n != null && !first) first = n;
|
|
717
|
+
}
|
|
718
|
+
return first;
|
|
719
|
+
}
|
|
720
|
+
const vnode = v;
|
|
721
|
+
if (vnode.type === Portal || vnode.type === Fragment) {
|
|
722
|
+
const children = vnode.props?.children;
|
|
723
|
+
const arr = children == null ? [] : Array.isArray(children) ? children : [children];
|
|
724
|
+
let first = null;
|
|
725
|
+
for (const child of arr) {
|
|
726
|
+
const n = await renderValueHydrating(child, ctx, c);
|
|
727
|
+
if (n != null && !first) first = n;
|
|
728
|
+
}
|
|
729
|
+
return first;
|
|
730
|
+
}
|
|
731
|
+
if (typeof vnode.type === "function") {
|
|
732
|
+
return renderComponentHydrating(vnode, ctx, c);
|
|
733
|
+
}
|
|
734
|
+
const tag = vnode.type;
|
|
735
|
+
const props = vnode.props ?? {};
|
|
736
|
+
let el;
|
|
737
|
+
if (c.node && c.node.nodeType === 1 && c.node.tagName.toLowerCase() === tag.toLowerCase()) {
|
|
738
|
+
el = c.node;
|
|
739
|
+
cursorAdvance(c);
|
|
740
|
+
} else {
|
|
741
|
+
el = SVG_TAGS.has(tag) ? document.createElementNS(SVG_NS, tag) : document.createElement(tag);
|
|
742
|
+
cursorReplace(c, el);
|
|
743
|
+
}
|
|
744
|
+
vnode.el = el;
|
|
745
|
+
patchProps(el, null, props);
|
|
746
|
+
if ("innerHTML" in props) {
|
|
747
|
+
} else {
|
|
748
|
+
const childCursor = { parent: el, node: el.firstChild };
|
|
749
|
+
const children = flattenChildren(props.children);
|
|
750
|
+
for (const child of children) {
|
|
751
|
+
const n = await renderValueHydrating(child, ctx, childCursor);
|
|
752
|
+
if (n != null && n.parentNode !== el) el.appendChild(n);
|
|
753
|
+
if (child && typeof child === "object" && typeof child.type === "function") {
|
|
754
|
+
const childVNode = child;
|
|
755
|
+
if (!childVNode._parentNode) {
|
|
756
|
+
childVNode._parentNode = el;
|
|
757
|
+
childVNode._refNode = n;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
while (childCursor.node) {
|
|
762
|
+
const n = childCursor.node;
|
|
763
|
+
childCursor.node = n.nextSibling;
|
|
764
|
+
n.parentNode?.removeChild(n);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if ("value" in props && el instanceof HTMLSelectElement) {
|
|
768
|
+
;
|
|
769
|
+
el.value = String(props.value ?? "");
|
|
770
|
+
}
|
|
771
|
+
if (typeof props.ref === "function") props.ref(el);
|
|
772
|
+
return el;
|
|
773
|
+
}
|
|
774
|
+
async function renderComponentHydrating(vnode, ctx, c) {
|
|
775
|
+
;
|
|
776
|
+
ctx.ui = ctx.ui ?? {};
|
|
777
|
+
if (!vnode._id) {
|
|
778
|
+
vnode._id = `_wf_${_idCounter++}`;
|
|
779
|
+
idRegistry.set(vnode._id, vnode);
|
|
780
|
+
}
|
|
781
|
+
const childCtx = Object.create(ctx);
|
|
782
|
+
childCtx.ui = Object.create(ctx.ui);
|
|
783
|
+
childCtx.ui._selfId = vnode._id;
|
|
784
|
+
childCtx.ui._selfVNode = vnode;
|
|
785
|
+
vnode._ctxVersion = childCtx.ui._ctxVersion ?? 0;
|
|
786
|
+
const Comp = vnode.type;
|
|
787
|
+
let childVNode;
|
|
788
|
+
try {
|
|
789
|
+
let def;
|
|
790
|
+
if (isAsyncComponent(Comp)) {
|
|
791
|
+
def = await resolveAsyncFactory(Comp, childCtx);
|
|
792
|
+
} else {
|
|
793
|
+
def = Comp;
|
|
794
|
+
}
|
|
795
|
+
const renderFn = def(vnode.props ?? {}, childCtx);
|
|
796
|
+
if (typeof renderFn !== "function") {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
vnode._render = renderFn;
|
|
802
|
+
childVNode = renderFn(vnode.props ?? {});
|
|
803
|
+
} catch (e) {
|
|
804
|
+
const errHandler = ctx.ui?._errorHandler;
|
|
805
|
+
if (errHandler) {
|
|
806
|
+
errHandler(e);
|
|
807
|
+
childVNode = null;
|
|
808
|
+
} else {
|
|
809
|
+
console.error(
|
|
810
|
+
`[weifuwu] Component hydration error in <${Comp.name || "anonymous"}> (id: ${vnode._id ?? "?"})`,
|
|
811
|
+
e
|
|
812
|
+
);
|
|
813
|
+
childVNode = null;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (childVNode == null) {
|
|
817
|
+
vnode._child = null;
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
vnode._child = childVNode;
|
|
821
|
+
const domNode = await renderValueHydrating(childVNode, childCtx, c);
|
|
822
|
+
if (!vnode._refNode) {
|
|
823
|
+
;
|
|
824
|
+
vnode._refNode = domNode;
|
|
825
|
+
}
|
|
826
|
+
return domNode;
|
|
827
|
+
}
|
|
828
|
+
async function hydrateVNode(container, vnode, ctx) {
|
|
829
|
+
const cursor = { parent: container, node: container.firstChild };
|
|
830
|
+
await renderValueHydrating(vnode, ctx, cursor);
|
|
831
|
+
while (cursor.node) {
|
|
832
|
+
const n = cursor.node;
|
|
833
|
+
cursor.node = n.nextSibling;
|
|
834
|
+
n.parentNode?.removeChild(n);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/client/reactive.ts
|
|
839
|
+
function createReactiveState(dirty) {
|
|
840
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
841
|
+
const reactive = (target) => {
|
|
842
|
+
if (target === null || typeof target !== "object") return target;
|
|
843
|
+
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
844
|
+
const proxy = new Proxy(target, {
|
|
845
|
+
set(target2, key, value) {
|
|
846
|
+
const old = Reflect.get(target2, key);
|
|
847
|
+
if (old === value) return true;
|
|
848
|
+
Reflect.set(target2, key, value);
|
|
849
|
+
dirty();
|
|
850
|
+
return true;
|
|
851
|
+
},
|
|
852
|
+
get(target2, key) {
|
|
853
|
+
const value = Reflect.get(target2, key);
|
|
854
|
+
if (typeof value === "object" && value !== null) return reactive(value);
|
|
855
|
+
return value;
|
|
856
|
+
},
|
|
857
|
+
deleteProperty(target2, key) {
|
|
858
|
+
if (Reflect.has(target2, key)) {
|
|
859
|
+
Reflect.deleteProperty(target2, key);
|
|
860
|
+
dirty();
|
|
861
|
+
}
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
proxyCache.set(target, proxy);
|
|
866
|
+
return proxy;
|
|
867
|
+
};
|
|
868
|
+
return reactive({});
|
|
869
|
+
}
|
|
622
870
|
|
|
623
871
|
// src/client/app.ts
|
|
624
872
|
function createApp() {
|
|
@@ -715,7 +963,7 @@ function createApp() {
|
|
|
715
963
|
middlewares.push(mw);
|
|
716
964
|
return app;
|
|
717
965
|
},
|
|
718
|
-
async mount(rootSelector, RootComponent) {
|
|
966
|
+
async mount(rootSelector, RootComponent, options) {
|
|
719
967
|
rootComponent = RootComponent;
|
|
720
968
|
for (const mw of middlewares) {
|
|
721
969
|
ctx = await mw(ctx);
|
|
@@ -723,7 +971,36 @@ function createApp() {
|
|
|
723
971
|
const el = typeof rootSelector === "string" ? document.querySelector(rootSelector) : rootSelector;
|
|
724
972
|
if (!el) throw new Error(`mount target not found: ${rootSelector}`);
|
|
725
973
|
container = el;
|
|
726
|
-
|
|
974
|
+
const hydrating = !!options?.hydrate;
|
|
975
|
+
if (!hydrating) container.innerHTML = "";
|
|
976
|
+
const dataCache = /* @__PURE__ */ new Map();
|
|
977
|
+
const hydratedData = globalThis.__DATA__;
|
|
978
|
+
if (hydratedData && typeof hydratedData === "object") {
|
|
979
|
+
for (const [k, v] of Object.entries(hydratedData)) {
|
|
980
|
+
dataCache.set(k, { value: v });
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
;
|
|
984
|
+
ctx.data = {
|
|
985
|
+
async get(key, fetcher) {
|
|
986
|
+
const entry = dataCache.get(key);
|
|
987
|
+
if (entry && "value" in entry) return entry.value;
|
|
988
|
+
if (entry?.promise) return entry.promise;
|
|
989
|
+
if (!fetcher) return void 0;
|
|
990
|
+
const promise = Promise.resolve().then(() => fetcher()).then((val) => {
|
|
991
|
+
dataCache.set(key, { value: val });
|
|
992
|
+
return val;
|
|
993
|
+
});
|
|
994
|
+
dataCache.set(key, { promise });
|
|
995
|
+
return promise;
|
|
996
|
+
},
|
|
997
|
+
set(key, value) {
|
|
998
|
+
dataCache.set(key, { value });
|
|
999
|
+
},
|
|
1000
|
+
has(key) {
|
|
1001
|
+
return dataCache.has(key);
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
727
1004
|
ctx.ui = {
|
|
728
1005
|
_selfId: "_wf_root",
|
|
729
1006
|
// ── ctx 版本号(供三态 skip 判定) ──
|
|
@@ -843,16 +1120,16 @@ function createApp() {
|
|
|
843
1120
|
* pos 是稳定对象,render 闭包直接读取 top/left;
|
|
844
1121
|
* 滚动/resize 时自动重算并定向刷新;打开弹层瞬间调用 pos.refresh()。
|
|
845
1122
|
*/
|
|
846
|
-
usePopupPosition: function(
|
|
1123
|
+
usePopupPosition: function(options2) {
|
|
847
1124
|
const selfId = getSelfId(this);
|
|
848
1125
|
const pos = { top: 0, left: 0, refresh: () => {
|
|
849
1126
|
} };
|
|
850
1127
|
if (!selfId) return pos;
|
|
851
1128
|
const tracker = {
|
|
852
1129
|
pos,
|
|
853
|
-
getEl:
|
|
854
|
-
isOpen:
|
|
855
|
-
compute:
|
|
1130
|
+
getEl: options2.el,
|
|
1131
|
+
isOpen: options2.isOpen,
|
|
1132
|
+
compute: options2.compute
|
|
856
1133
|
};
|
|
857
1134
|
_popupTrackers.set(selfId, tracker);
|
|
858
1135
|
if (!_popupListenersReady) {
|
|
@@ -887,10 +1164,14 @@ function createApp() {
|
|
|
887
1164
|
oldVNode = wrapComponent(RootComponent, ctx);
|
|
888
1165
|
oldVNode._id = "_wf_root";
|
|
889
1166
|
oldVNode._parentNode = container;
|
|
890
|
-
oldVNode._refNode = null;
|
|
1167
|
+
oldVNode._refNode = hydrating ? container.firstChild : null;
|
|
891
1168
|
idRegistry.set("_wf_root", oldVNode);
|
|
892
|
-
|
|
893
|
-
|
|
1169
|
+
if (hydrating) {
|
|
1170
|
+
await hydrateVNode(container, oldVNode, ctx);
|
|
1171
|
+
} else {
|
|
1172
|
+
const node = render(oldVNode, ctx);
|
|
1173
|
+
if (node instanceof Node) container.appendChild(node);
|
|
1174
|
+
}
|
|
894
1175
|
oldVNode._refNode = container.firstChild;
|
|
895
1176
|
_rendering = false;
|
|
896
1177
|
flushDirtyBatch();
|
|
@@ -912,52 +1193,8 @@ function createApp() {
|
|
|
912
1193
|
function wrapComponent(Comp, _ctx) {
|
|
913
1194
|
return { type: Comp, props: {}, key: void 0 };
|
|
914
1195
|
}
|
|
915
|
-
function createReactiveState(dirty) {
|
|
916
|
-
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
917
|
-
const reactive = (target) => {
|
|
918
|
-
if (target === null || typeof target !== "object") return target;
|
|
919
|
-
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
920
|
-
const proxy = new Proxy(target, {
|
|
921
|
-
set(target2, key, value) {
|
|
922
|
-
const old = Reflect.get(target2, key);
|
|
923
|
-
if (old === value) return true;
|
|
924
|
-
Reflect.set(target2, key, value);
|
|
925
|
-
dirty();
|
|
926
|
-
return true;
|
|
927
|
-
},
|
|
928
|
-
get(target2, key) {
|
|
929
|
-
const value = Reflect.get(target2, key);
|
|
930
|
-
if (typeof value === "object" && value !== null) return reactive(value);
|
|
931
|
-
return value;
|
|
932
|
-
},
|
|
933
|
-
deleteProperty(target2, key) {
|
|
934
|
-
if (Reflect.has(target2, key)) {
|
|
935
|
-
Reflect.deleteProperty(target2, key);
|
|
936
|
-
dirty();
|
|
937
|
-
}
|
|
938
|
-
return true;
|
|
939
|
-
}
|
|
940
|
-
});
|
|
941
|
-
proxyCache.set(target, proxy);
|
|
942
|
-
return proxy;
|
|
943
|
-
};
|
|
944
|
-
return reactive({});
|
|
945
|
-
}
|
|
946
1196
|
|
|
947
|
-
// src/client/
|
|
948
|
-
function flattenRoutes(routes, basePath = "", chain = []) {
|
|
949
|
-
const result = [];
|
|
950
|
-
for (const route of routes) {
|
|
951
|
-
const fullPath = joinPaths(basePath, route.path);
|
|
952
|
-
const { re, keys } = compilePath(fullPath);
|
|
953
|
-
const fullChain = [...chain, route];
|
|
954
|
-
result.push({ re, keys, def: route, chain: fullChain });
|
|
955
|
-
if (route.children) {
|
|
956
|
-
result.push(...flattenRoutes(route.children, fullPath, fullChain));
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
return result;
|
|
960
|
-
}
|
|
1197
|
+
// src/client/route-match.ts
|
|
961
1198
|
function joinPaths(a, b) {
|
|
962
1199
|
if (!b || b === "/") return a || "/";
|
|
963
1200
|
const left = a.endsWith("/") ? a.slice(0, -1) : a;
|
|
@@ -972,6 +1209,19 @@ function compilePath(path) {
|
|
|
972
1209
|
}).replace(/\*/g, ".*");
|
|
973
1210
|
return { re: new RegExp(`^${reStr}$`), keys };
|
|
974
1211
|
}
|
|
1212
|
+
function flattenRoutes(routes, basePath = "", chain = []) {
|
|
1213
|
+
const result = [];
|
|
1214
|
+
for (const route of routes) {
|
|
1215
|
+
const fullPath = joinPaths(basePath, route.path);
|
|
1216
|
+
const { re, keys } = compilePath(fullPath);
|
|
1217
|
+
const fullChain = [...chain, route];
|
|
1218
|
+
result.push({ re, keys, def: route, chain: fullChain });
|
|
1219
|
+
if (route.children) {
|
|
1220
|
+
result.push(...flattenRoutes(route.children, fullPath, fullChain));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return result;
|
|
1224
|
+
}
|
|
975
1225
|
function matchRoute(path, routes) {
|
|
976
1226
|
let best = null;
|
|
977
1227
|
for (const fr of routes) {
|
|
@@ -981,6 +1231,18 @@ function matchRoute(path, routes) {
|
|
|
981
1231
|
}
|
|
982
1232
|
return best;
|
|
983
1233
|
}
|
|
1234
|
+
function extractParams(path, match) {
|
|
1235
|
+
const params = {};
|
|
1236
|
+
const m = path.match(match.re);
|
|
1237
|
+
if (m) {
|
|
1238
|
+
for (let i = 0; i < match.keys.length; i++) {
|
|
1239
|
+
params[match.keys[i]] = decodeURIComponent(m[i + 1]);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return params;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/client/router.ts
|
|
984
1246
|
function router(opts) {
|
|
985
1247
|
const flatRoutes = flattenRoutes(opts.routes);
|
|
986
1248
|
const mode = opts.mode || "history";
|
|
@@ -994,23 +1256,22 @@ function router(opts) {
|
|
|
994
1256
|
function resolve(path) {
|
|
995
1257
|
const match = matchRoute(path, flatRoutes);
|
|
996
1258
|
if (match) {
|
|
997
|
-
const params = {};
|
|
998
|
-
const m = path.match(match.re);
|
|
999
|
-
if (m) {
|
|
1000
|
-
for (let i = 0; i < match.keys.length; i++) {
|
|
1001
|
-
params[match.keys[i]] = decodeURIComponent(m[i + 1]);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
1259
|
const last = match.chain[match.chain.length - 1];
|
|
1005
1260
|
return {
|
|
1006
1261
|
path: match.def.path,
|
|
1007
|
-
params,
|
|
1262
|
+
params: extractParams(path, match),
|
|
1008
1263
|
query: Object.fromEntries(new URLSearchParams(window.location.search)),
|
|
1009
1264
|
chain: match.chain,
|
|
1010
1265
|
title: last?.title ?? ""
|
|
1011
1266
|
};
|
|
1012
1267
|
}
|
|
1013
|
-
return {
|
|
1268
|
+
return {
|
|
1269
|
+
path,
|
|
1270
|
+
params: {},
|
|
1271
|
+
query: Object.fromEntries(new URLSearchParams(window.location.search)),
|
|
1272
|
+
chain: opts.notFound ? [{ component: opts.notFound, title: "Not Found" }] : [],
|
|
1273
|
+
title: ""
|
|
1274
|
+
};
|
|
1014
1275
|
}
|
|
1015
1276
|
return (ctx) => {
|
|
1016
1277
|
const resolved = resolve(getPath());
|
|
@@ -1018,6 +1279,7 @@ function router(opts) {
|
|
|
1018
1279
|
if (resolved.title) document.title = resolved.title;
|
|
1019
1280
|
if (!ctx.app) ctx.app = {};
|
|
1020
1281
|
ctx.app.navigate = (path) => {
|
|
1282
|
+
clearAsyncComponentCache();
|
|
1021
1283
|
if (mode === "hash") {
|
|
1022
1284
|
window.location.hash = "#" + path;
|
|
1023
1285
|
const resolved2 = resolve(path);
|
|
@@ -1032,6 +1294,7 @@ function router(opts) {
|
|
|
1032
1294
|
ctx.ui?.render();
|
|
1033
1295
|
};
|
|
1034
1296
|
const onPop = () => {
|
|
1297
|
+
clearAsyncComponentCache();
|
|
1035
1298
|
const resolved2 = resolve(getPath());
|
|
1036
1299
|
ctx.route = resolved2;
|
|
1037
1300
|
if (ctx.route?.title) document.title = ctx.route.title;
|
|
@@ -1580,14 +1843,17 @@ export {
|
|
|
1580
1843
|
Portal,
|
|
1581
1844
|
RouteView,
|
|
1582
1845
|
api,
|
|
1846
|
+
asyncComponent,
|
|
1583
1847
|
auth,
|
|
1584
1848
|
computeFixedPos,
|
|
1849
|
+
computeFixedPosRect,
|
|
1585
1850
|
createApp,
|
|
1586
1851
|
createPortal,
|
|
1587
1852
|
enUS,
|
|
1588
1853
|
extendCtx,
|
|
1589
1854
|
h,
|
|
1590
1855
|
i18n,
|
|
1856
|
+
isAsyncComponent,
|
|
1591
1857
|
jsx,
|
|
1592
1858
|
jsxDEV,
|
|
1593
1859
|
jsxs,
|
package/dist/client/render.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ import type { VNode } from './vnode.ts';
|
|
|
14
14
|
import type { WfuiContext } from './types.ts';
|
|
15
15
|
export declare const idRegistry: Map<string, VNode>;
|
|
16
16
|
export declare function render(input: any, ctx: WfuiContext): Node | null;
|
|
17
|
+
/**
|
|
18
|
+
* 清空 async 工厂缓存。
|
|
19
|
+
* 页面上下文切换时调用(路由导航/登录登出)——工厂内 ctx.data.get 的 key 依赖 ctx(如 route.params),
|
|
20
|
+
* 上下文变化后旧缓存定义的数据已失效,需要让工厂以新 ctx 重新执行。
|
|
21
|
+
*/
|
|
22
|
+
export declare function clearAsyncComponentCache(): void;
|
|
17
23
|
/** 首次渲染 Portal:创建远程容器、渲染子节点(不返回占位节点) */
|
|
18
24
|
export declare function renderPortal(vnode: VNode, ctx: WfuiContext): void;
|
|
19
25
|
/** 更新 Portal:复用远程容器,patch 子节点(不操作父 DOM) */
|
|
@@ -22,3 +28,8 @@ export declare function patchValue(parent: Node, oldNode: Node | null, oldInput:
|
|
|
22
28
|
/** 通知 ref 清理 + Portal 子容器清理 */
|
|
23
29
|
export declare function callRefCleanup(input: any): void;
|
|
24
30
|
export declare function mountVNode(container: Element, vnode: VNode, ctx: WfuiContext): void;
|
|
31
|
+
/**
|
|
32
|
+
* Hydration 挂载入口:收养 container 内现有服务端 HTML。
|
|
33
|
+
* 渲染完收尾:删除服务端有、客户端没有的残留 DOM。
|
|
34
|
+
*/
|
|
35
|
+
export declare function hydrateVNode(container: Element, vnode: VNode, ctx: WfuiContext): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由匹配纯函数 — 客户端 router 与服务端 uiSsr 共用(无 DOM 依赖)
|
|
3
|
+
*
|
|
4
|
+
* 支持路径参数 :id、通配符 *、嵌套路由(children → chain)。
|
|
5
|
+
* 与后端 Router(Trie)独立——本模块服务于"前端路由表驱动的页面"。
|
|
6
|
+
*/
|
|
7
|
+
import type { RouteDef } from './types.ts';
|
|
8
|
+
export interface FlattenedRoute {
|
|
9
|
+
re: RegExp;
|
|
10
|
+
keys: string[];
|
|
11
|
+
def: RouteDef;
|
|
12
|
+
chain: RouteDef[];
|
|
13
|
+
}
|
|
14
|
+
export declare function joinPaths(a: string, b: string): string;
|
|
15
|
+
export declare function compilePath(path: string): {
|
|
16
|
+
re: RegExp;
|
|
17
|
+
keys: string[];
|
|
18
|
+
};
|
|
19
|
+
export declare function flattenRoutes(routes: RouteDef[], basePath?: string, chain?: RouteDef[]): FlattenedRoute[];
|
|
20
|
+
export declare function matchRoute(path: string, routes: FlattenedRoute[]): FlattenedRoute | null;
|
|
21
|
+
/** 从匹配结果提取路径参数 */
|
|
22
|
+
export declare function extractParams(path: string, match: FlattenedRoute): Record<string, string>;
|
package/dist/client/types.d.ts
CHANGED
|
@@ -70,6 +70,23 @@ export interface WfuiContext {
|
|
|
70
70
|
delete: <T = any>(url: string, opts?: any) => Promise<T>;
|
|
71
71
|
[key: string]: any;
|
|
72
72
|
};
|
|
73
|
+
/** 数据管道(由 createApp 注入):ctx.data.get(key, fetcher) */
|
|
74
|
+
data?: {
|
|
75
|
+
/**
|
|
76
|
+
* 获取数据:
|
|
77
|
+
* - SSR:服务端真 fetch,结果序列化进 __DATA__
|
|
78
|
+
* - hydration:从 __DATA__ 缓存同步命中(工厂 await 微任务即 resolve)
|
|
79
|
+
* - SPA:未命中则触发 fetcher,同 key 并发请求合并
|
|
80
|
+
*
|
|
81
|
+
* key 约定即 URL(`/api/posts/1`),天然唯一。
|
|
82
|
+
*/
|
|
83
|
+
get: <T = any>(key: string, fetcher?: () => Promise<T>) => Promise<T>;
|
|
84
|
+
/** 向缓存写入值(如 hydration 种子) */
|
|
85
|
+
set: (key: string, value: unknown) => void;
|
|
86
|
+
/** 是否存在缓存(未触发 fetch) */
|
|
87
|
+
has: (key: string) => boolean;
|
|
88
|
+
[key: string]: any;
|
|
89
|
+
};
|
|
73
90
|
/** 认证状态 */
|
|
74
91
|
auth?: {
|
|
75
92
|
token: string | null;
|