weifuwu 0.55.3 → 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 +221 -17
- package/dist/client/app.d.ts +3 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +337 -87
- 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 +30 -1
- package/dist/components/index.js +56 -18
- package/dist/dev/index.js +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +300 -2
- 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 +6 -3
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;
|
|
@@ -628,6 +681,192 @@ function callRefCleanup(input) {
|
|
|
628
681
|
vnode._remoteEl = void 0;
|
|
629
682
|
}
|
|
630
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
|
+
}
|
|
631
870
|
|
|
632
871
|
// src/client/app.ts
|
|
633
872
|
function createApp() {
|
|
@@ -724,7 +963,7 @@ function createApp() {
|
|
|
724
963
|
middlewares.push(mw);
|
|
725
964
|
return app;
|
|
726
965
|
},
|
|
727
|
-
async mount(rootSelector, RootComponent) {
|
|
966
|
+
async mount(rootSelector, RootComponent, options) {
|
|
728
967
|
rootComponent = RootComponent;
|
|
729
968
|
for (const mw of middlewares) {
|
|
730
969
|
ctx = await mw(ctx);
|
|
@@ -732,7 +971,36 @@ function createApp() {
|
|
|
732
971
|
const el = typeof rootSelector === "string" ? document.querySelector(rootSelector) : rootSelector;
|
|
733
972
|
if (!el) throw new Error(`mount target not found: ${rootSelector}`);
|
|
734
973
|
container = el;
|
|
735
|
-
|
|
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
|
+
};
|
|
736
1004
|
ctx.ui = {
|
|
737
1005
|
_selfId: "_wf_root",
|
|
738
1006
|
// ── ctx 版本号(供三态 skip 判定) ──
|
|
@@ -852,16 +1120,16 @@ function createApp() {
|
|
|
852
1120
|
* pos 是稳定对象,render 闭包直接读取 top/left;
|
|
853
1121
|
* 滚动/resize 时自动重算并定向刷新;打开弹层瞬间调用 pos.refresh()。
|
|
854
1122
|
*/
|
|
855
|
-
usePopupPosition: function(
|
|
1123
|
+
usePopupPosition: function(options2) {
|
|
856
1124
|
const selfId = getSelfId(this);
|
|
857
1125
|
const pos = { top: 0, left: 0, refresh: () => {
|
|
858
1126
|
} };
|
|
859
1127
|
if (!selfId) return pos;
|
|
860
1128
|
const tracker = {
|
|
861
1129
|
pos,
|
|
862
|
-
getEl:
|
|
863
|
-
isOpen:
|
|
864
|
-
compute:
|
|
1130
|
+
getEl: options2.el,
|
|
1131
|
+
isOpen: options2.isOpen,
|
|
1132
|
+
compute: options2.compute
|
|
865
1133
|
};
|
|
866
1134
|
_popupTrackers.set(selfId, tracker);
|
|
867
1135
|
if (!_popupListenersReady) {
|
|
@@ -896,10 +1164,14 @@ function createApp() {
|
|
|
896
1164
|
oldVNode = wrapComponent(RootComponent, ctx);
|
|
897
1165
|
oldVNode._id = "_wf_root";
|
|
898
1166
|
oldVNode._parentNode = container;
|
|
899
|
-
oldVNode._refNode = null;
|
|
1167
|
+
oldVNode._refNode = hydrating ? container.firstChild : null;
|
|
900
1168
|
idRegistry.set("_wf_root", oldVNode);
|
|
901
|
-
|
|
902
|
-
|
|
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
|
+
}
|
|
903
1175
|
oldVNode._refNode = container.firstChild;
|
|
904
1176
|
_rendering = false;
|
|
905
1177
|
flushDirtyBatch();
|
|
@@ -921,52 +1193,8 @@ function createApp() {
|
|
|
921
1193
|
function wrapComponent(Comp, _ctx) {
|
|
922
1194
|
return { type: Comp, props: {}, key: void 0 };
|
|
923
1195
|
}
|
|
924
|
-
function createReactiveState(dirty) {
|
|
925
|
-
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
926
|
-
const reactive = (target) => {
|
|
927
|
-
if (target === null || typeof target !== "object") return target;
|
|
928
|
-
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
929
|
-
const proxy = new Proxy(target, {
|
|
930
|
-
set(target2, key, value) {
|
|
931
|
-
const old = Reflect.get(target2, key);
|
|
932
|
-
if (old === value) return true;
|
|
933
|
-
Reflect.set(target2, key, value);
|
|
934
|
-
dirty();
|
|
935
|
-
return true;
|
|
936
|
-
},
|
|
937
|
-
get(target2, key) {
|
|
938
|
-
const value = Reflect.get(target2, key);
|
|
939
|
-
if (typeof value === "object" && value !== null) return reactive(value);
|
|
940
|
-
return value;
|
|
941
|
-
},
|
|
942
|
-
deleteProperty(target2, key) {
|
|
943
|
-
if (Reflect.has(target2, key)) {
|
|
944
|
-
Reflect.deleteProperty(target2, key);
|
|
945
|
-
dirty();
|
|
946
|
-
}
|
|
947
|
-
return true;
|
|
948
|
-
}
|
|
949
|
-
});
|
|
950
|
-
proxyCache.set(target, proxy);
|
|
951
|
-
return proxy;
|
|
952
|
-
};
|
|
953
|
-
return reactive({});
|
|
954
|
-
}
|
|
955
1196
|
|
|
956
|
-
// src/client/
|
|
957
|
-
function flattenRoutes(routes, basePath = "", chain = []) {
|
|
958
|
-
const result = [];
|
|
959
|
-
for (const route of routes) {
|
|
960
|
-
const fullPath = joinPaths(basePath, route.path);
|
|
961
|
-
const { re, keys } = compilePath(fullPath);
|
|
962
|
-
const fullChain = [...chain, route];
|
|
963
|
-
result.push({ re, keys, def: route, chain: fullChain });
|
|
964
|
-
if (route.children) {
|
|
965
|
-
result.push(...flattenRoutes(route.children, fullPath, fullChain));
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return result;
|
|
969
|
-
}
|
|
1197
|
+
// src/client/route-match.ts
|
|
970
1198
|
function joinPaths(a, b) {
|
|
971
1199
|
if (!b || b === "/") return a || "/";
|
|
972
1200
|
const left = a.endsWith("/") ? a.slice(0, -1) : a;
|
|
@@ -981,6 +1209,19 @@ function compilePath(path) {
|
|
|
981
1209
|
}).replace(/\*/g, ".*");
|
|
982
1210
|
return { re: new RegExp(`^${reStr}$`), keys };
|
|
983
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
|
+
}
|
|
984
1225
|
function matchRoute(path, routes) {
|
|
985
1226
|
let best = null;
|
|
986
1227
|
for (const fr of routes) {
|
|
@@ -990,6 +1231,18 @@ function matchRoute(path, routes) {
|
|
|
990
1231
|
}
|
|
991
1232
|
return best;
|
|
992
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
|
|
993
1246
|
function router(opts) {
|
|
994
1247
|
const flatRoutes = flattenRoutes(opts.routes);
|
|
995
1248
|
const mode = opts.mode || "history";
|
|
@@ -1003,17 +1256,10 @@ function router(opts) {
|
|
|
1003
1256
|
function resolve(path) {
|
|
1004
1257
|
const match = matchRoute(path, flatRoutes);
|
|
1005
1258
|
if (match) {
|
|
1006
|
-
const params = {};
|
|
1007
|
-
const m = path.match(match.re);
|
|
1008
|
-
if (m) {
|
|
1009
|
-
for (let i = 0; i < match.keys.length; i++) {
|
|
1010
|
-
params[match.keys[i]] = decodeURIComponent(m[i + 1]);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
1259
|
const last = match.chain[match.chain.length - 1];
|
|
1014
1260
|
return {
|
|
1015
1261
|
path: match.def.path,
|
|
1016
|
-
params,
|
|
1262
|
+
params: extractParams(path, match),
|
|
1017
1263
|
query: Object.fromEntries(new URLSearchParams(window.location.search)),
|
|
1018
1264
|
chain: match.chain,
|
|
1019
1265
|
title: last?.title ?? ""
|
|
@@ -1033,6 +1279,7 @@ function router(opts) {
|
|
|
1033
1279
|
if (resolved.title) document.title = resolved.title;
|
|
1034
1280
|
if (!ctx.app) ctx.app = {};
|
|
1035
1281
|
ctx.app.navigate = (path) => {
|
|
1282
|
+
clearAsyncComponentCache();
|
|
1036
1283
|
if (mode === "hash") {
|
|
1037
1284
|
window.location.hash = "#" + path;
|
|
1038
1285
|
const resolved2 = resolve(path);
|
|
@@ -1047,6 +1294,7 @@ function router(opts) {
|
|
|
1047
1294
|
ctx.ui?.render();
|
|
1048
1295
|
};
|
|
1049
1296
|
const onPop = () => {
|
|
1297
|
+
clearAsyncComponentCache();
|
|
1050
1298
|
const resolved2 = resolve(getPath());
|
|
1051
1299
|
ctx.route = resolved2;
|
|
1052
1300
|
if (ctx.route?.title) document.title = ctx.route.title;
|
|
@@ -1595,6 +1843,7 @@ export {
|
|
|
1595
1843
|
Portal,
|
|
1596
1844
|
RouteView,
|
|
1597
1845
|
api,
|
|
1846
|
+
asyncComponent,
|
|
1598
1847
|
auth,
|
|
1599
1848
|
computeFixedPos,
|
|
1600
1849
|
computeFixedPosRect,
|
|
@@ -1604,6 +1853,7 @@ export {
|
|
|
1604
1853
|
extendCtx,
|
|
1605
1854
|
h,
|
|
1606
1855
|
i18n,
|
|
1856
|
+
isAsyncComponent,
|
|
1607
1857
|
jsx,
|
|
1608
1858
|
jsxDEV,
|
|
1609
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;
|
package/dist/client/vnode.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* --jsxImportSource=weifuwu/client
|
|
8
8
|
*/
|
|
9
9
|
import type { WfuiContext } from './types.ts';
|
|
10
|
-
export type VNodeType = string | Component | typeof Fragment | typeof Portal;
|
|
10
|
+
export type VNodeType = string | Component | AsyncComponent | typeof Fragment | typeof Portal;
|
|
11
11
|
export interface VNode {
|
|
12
12
|
type: VNodeType;
|
|
13
13
|
props: Record<string, any>;
|
|
@@ -37,6 +37,35 @@ export interface VNode {
|
|
|
37
37
|
* P = props 类型(JSX 自动推断),C = 组件依赖的 ctx 注入(如 ApiInjected & RouteInjected)
|
|
38
38
|
*/
|
|
39
39
|
export type Component<P = {}, C extends object = {}> = (initProps: P, ctx: WfuiContext & C) => ((props: P) => VNode | null) | null;
|
|
40
|
+
/**
|
|
41
|
+
* 异步组件工厂(形态 C):async (ctx) => (initProps, ctx) => (props) => VNode
|
|
42
|
+
*
|
|
43
|
+
* 工厂层(async,只执行一次并缓存):
|
|
44
|
+
* - 数据声明:const data = await ctx.data.get(key, fetcher)
|
|
45
|
+
* - 代码分割:const { default: def } = await import('./view.tsx')
|
|
46
|
+
* - 异步初始化:模块加载、共享资源
|
|
47
|
+
*
|
|
48
|
+
* 返回标准的 Component——mount/render 保持同步,异步只发生在工厂边界。
|
|
49
|
+
* 服务端遍历器与客户端渲染器都 await 工厂;更新路径(_render 缓存)不碰工厂。
|
|
50
|
+
*
|
|
51
|
+
* 必须用 asyncComponent() 包装以标记(渲染器据此区分调用约定):
|
|
52
|
+
* Component → Comp(initProps, ctx)
|
|
53
|
+
* AsyncComponent → await Comp(ctx) → Component
|
|
54
|
+
*/
|
|
55
|
+
export type AsyncComponent<C extends object = {}, P = {}> = (ctx: WfuiContext & C) => Promise<Component<P, C>>;
|
|
56
|
+
/**
|
|
57
|
+
* 包装异步组件工厂。
|
|
58
|
+
*
|
|
59
|
+
* ```tsx
|
|
60
|
+
* const UserProfile = asyncComponent(async (ctx) => {
|
|
61
|
+
* const user = await ctx.data.get(`/api/user/${ctx.params.id}`)
|
|
62
|
+
* return (initProps, ctx) => (props) => h('div', {}, user.name)
|
|
63
|
+
* })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function asyncComponent<C extends object = {}, P = {}>(factory: AsyncComponent<C, P>): AsyncComponent<C, P>;
|
|
67
|
+
/** 判定一个组件类型是否为 async 工厂(asyncComponent 包装过) */
|
|
68
|
+
export declare function isAsyncComponent(type: any): type is AsyncComponent;
|
|
40
69
|
export declare const Fragment: unique symbol;
|
|
41
70
|
/** Portal — 将子 VNode 渲染到 document.body 下的独立容器 */
|
|
42
71
|
export declare const Portal: unique symbol;
|