weifuwu 0.50.0 → 0.51.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.
- package/README.md +130 -52
- package/dist/client/app.d.ts +9 -3
- package/dist/client/index.js +223 -120
- package/dist/client/render.d.ts +1 -0
- package/dist/client/router.d.ts +0 -1
- package/dist/client/types.d.ts +11 -5
- package/dist/client/vnode.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install weifuwu
|
|
|
16
16
|
|
|
17
17
|
**两阶段组件模型** — 组件 = `(initProps, ctx) => (props) => VNode`。外层函数只执行一次(mount),内层函数每次状态/props 变化时执行(render)。无 class、无 `this`、无 Hook。
|
|
18
18
|
|
|
19
|
-
**Proxy 驱动渲染** — `ctx.ui.$()` 返回深度 Proxy,`$.x = val`
|
|
19
|
+
**Proxy 驱动渲染** — `ctx.ui.$()` 返回深度 Proxy,`$.x = val` 自动触发当前组件的 VDOM patch。也支持手动 `ctx.ui.render()` 精确控制渲染时机。无需手动调用 `useState`/`useEffect`。
|
|
20
20
|
|
|
21
21
|
**中间件注入一切** — 后端和前端共用同一理念:中间件向 `ctx` 注入能力(`ctx.sql` / `ctx.redis` / `ctx.api` / `ctx.auth` / `ctx.i18n` 等),Handler/组件从 `ctx` 读取。
|
|
22
22
|
|
|
@@ -66,6 +66,86 @@ createApp()
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
+
## CDN 快速原型(零构建、纯 HTML)
|
|
70
|
+
|
|
71
|
+
不需要 Node.js 或构建工具,直接在浏览器中用 CDN 使用 weifuwu。创建一个 `.html` 文件即可开始,适合快速原型、Codepen、简单的演示页面。
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<!-- cdn-counter.html -->
|
|
75
|
+
<!doctype html>
|
|
76
|
+
<html lang="zh-CN">
|
|
77
|
+
<head>
|
|
78
|
+
<meta charset="UTF-8" />
|
|
79
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
80
|
+
<title>Weifuwu CDN 示例</title>
|
|
81
|
+
|
|
82
|
+
<!-- 组件样式(可选,如只用 weifuwu/client 则不需要) -->
|
|
83
|
+
<link
|
|
84
|
+
rel="stylesheet"
|
|
85
|
+
href="https://unpkg.com/weifuwu@0.51.0/dist/components/style.css"
|
|
86
|
+
/>
|
|
87
|
+
</head>
|
|
88
|
+
<body>
|
|
89
|
+
<div id="root"></div>
|
|
90
|
+
|
|
91
|
+
<!-- Import Map — 将 weifuwu 包名映射到 CDN 地址 -->
|
|
92
|
+
<script type="importmap">
|
|
93
|
+
{
|
|
94
|
+
"imports": {
|
|
95
|
+
"weifuwu/client": "https://unpkg.com/weifuwu@0.51.0/dist/client/index.js",
|
|
96
|
+
"weifuwu/components": "https://unpkg.com/weifuwu@0.51.0/dist/components/index.js"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<script type="module">
|
|
102
|
+
import { createApp, h } from 'weifuwu/client'
|
|
103
|
+
import { Card, Button, Badge } from 'weifuwu/components'
|
|
104
|
+
|
|
105
|
+
// 组件 = (initProps, ctx) => (props) => VNode
|
|
106
|
+
const Counter = (_props, ctx) => {
|
|
107
|
+
const $ = ctx.ui.$()
|
|
108
|
+
$.count = 0 // mount 初始化
|
|
109
|
+
|
|
110
|
+
return () =>
|
|
111
|
+
h(Card, { variant: 'default', padding: 'lg' },
|
|
112
|
+
h('h2', { style: { textAlign: 'center', margin: 0 } }, '⚡ Weifuwu'),
|
|
113
|
+
h('div', { style: { fontSize: '4rem', fontWeight: 600, textAlign: 'center' } },
|
|
114
|
+
String($.count)),
|
|
115
|
+
h('div', { style: { textAlign: 'center', marginTop: '1rem' } },
|
|
116
|
+
h(Badge, {
|
|
117
|
+
variant: $.count % 2 === 0 ? 'success' : 'warning'
|
|
118
|
+
}, $.count % 2 === 0 ? '偶数' : '奇数')),
|
|
119
|
+
h('hr', { style: { margin: '1rem 0', border: 'none', borderTop: '1px solid #eee' } }),
|
|
120
|
+
h('div', { style: { display: 'flex', gap: '0.5rem', justifyContent: 'center' } },
|
|
121
|
+
h(Button, { variant: 'secondary', onClick: () => $.count-- }, '➖ 减 1'),
|
|
122
|
+
h(Button, { variant: 'danger', onClick: () => $.count = 0 }, '↺ 重置'),
|
|
123
|
+
h(Button, { variant: 'primary', onClick: () => $.count++ }, '➕ 加 1'),
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
createApp().mount('#root', Counter)
|
|
129
|
+
</script>
|
|
130
|
+
</body>
|
|
131
|
+
</html>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
将此 HTML 保存到本地用浏览器打开即可运行。完整的 CDN 示例见 [`apps/html/test.html`](./apps/html/test.html)。
|
|
135
|
+
|
|
136
|
+
### CDN 资源地址说明
|
|
137
|
+
|
|
138
|
+
| 资源 | CDN 地址 | 说明 |
|
|
139
|
+
|------|---------|------|
|
|
140
|
+
| `weifuwu/client` | `https://unpkg.com/weifuwu@0.51.0/dist/client/index.js` | 客户端核心(createApp, h, 路由, 状态管理等) |
|
|
141
|
+
| `weifuwu/components` | `https://unpkg.com/weifuwu@0.51.0/dist/components/index.js` | 41 个 UI 组件(Button, Card, Table, Modal 等) |
|
|
142
|
+
| 组件样式 | `https://unpkg.com/weifuwu@0.51.0/dist/components/style.css` | 组件 CSS + 72 个主题 Token + 35 个布局原语 |
|
|
143
|
+
| 独立布局系统 | `https://unpkg.com/weifuwu@0.51.0/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
|
|
144
|
+
|
|
145
|
+
> 提示:将 `@0.51.0` 替换为 `@latest` 始终使用最新版,或固定版本避免意外变更。
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
69
149
|
## 模块总览
|
|
70
150
|
|
|
71
151
|
| 导入路径 | 模块 | 用途 | 依赖 |
|
|
@@ -116,7 +196,7 @@ createApp()
|
|
|
116
196
|
|------|------|------|
|
|
117
197
|
| 注入 | 中间件注入 ctx.field | 中间件注入 ctx.field |
|
|
118
198
|
| 读取 | handler 读取 ctx | 组件读取 ctx |
|
|
119
|
-
| 渲染 | 返回 Response | `ctx.ui.render()` / `ctx.ui.dirty()`
|
|
199
|
+
| 渲染 | 返回 Response | `ctx.ui.render()` / `ctx.ui.dirty()` / `$.x = val` 触发局部 VDOM patch |
|
|
120
200
|
|
|
121
201
|
### Closeable 接口
|
|
122
202
|
|
|
@@ -812,11 +892,14 @@ h('div', { class: 'x' }, child1, child2)
|
|
|
812
892
|
|
|
813
893
|
### Render 机制总览
|
|
814
894
|
|
|
815
|
-
| API | 触发时机 | 渲染方式 | 使用场景 |
|
|
816
|
-
|
|
817
|
-
| `$.x = val` | 赋值后自动 | 微任务批量(异步) | **日常 UI 状态** —
|
|
818
|
-
| `ctx.ui.dirty()` | 主动调用 | 微任务批量(异步) | **绕过 Proxy 后手动标记**
|
|
819
|
-
| `ctx.ui.render()` | 主动调用 | 立即同步 | **需要立即拿到最新 DOM** — DOM
|
|
895
|
+
| API | 触发时机 | 渲染方式 | 作用域 | 使用场景 |
|
|
896
|
+
|------|---------|---------|--------|---------|
|
|
897
|
+
| `$.x = val` | 赋值后自动 | 微任务批量(异步) | 当前组件 | **日常 UI 状态** — 表单输入、切换开关、异步数据加载等 |
|
|
898
|
+
| `ctx.ui.dirty()` | 主动调用 | 微任务批量(异步) | 当前/指定 | **绕过 Proxy 后手动标记** |
|
|
899
|
+
| `ctx.ui.render()` | 主动调用 | 立即同步 | 当前/指定 | **需要立即拿到最新 DOM** — DOM 测量、动画触发 |
|
|
900
|
+
| `ctx.ui.render(['id'])` | 主动调用 | 立即同步 | 指定组件 | **跨组件精准刷新** — 全局事件、Portal 远程控制 |
|
|
901
|
+
|
|
902
|
+
`render()` 和 `dirty()` 无参 = 当前组件,传参 = 指定组件列表。三套 API 同一 scope 机制。
|
|
820
903
|
|
|
821
904
|
### 闭包变量 + `ctx.ui.render()`(简单场景)
|
|
822
905
|
|
|
@@ -862,23 +945,13 @@ const FormPage: Component = (_init, ctx) => {
|
|
|
862
945
|
- 不需要触发渲染的内部缓存(用闭包变量 `let`)
|
|
863
946
|
- 简单组件只有一两个状态变量(闭包变量 + `render()` 更轻量)
|
|
864
947
|
|
|
865
|
-
### `ctx.ui.dirty()` —
|
|
866
|
-
|
|
867
|
-
当你绕过 Proxy 直接操作底层数据后,调用 `dirty()` 通知框架在下个微任务批量重渲染:
|
|
868
|
-
|
|
869
|
-
```tsx
|
|
870
|
-
// 实际场景:在 mount 阶段需要手动触发渲染
|
|
871
|
-
// mount 期间 $.x = val 自动静默(不触发渲染)
|
|
872
|
-
$.initialized = true
|
|
873
|
-
// 如果非要在这里触发渲染,需要手动调用 dirty():
|
|
874
|
-
ctx.ui.dirty()
|
|
875
|
-
```
|
|
948
|
+
### `ctx.ui.dirty()` — 异步标记脏
|
|
876
949
|
|
|
877
|
-
|
|
950
|
+
异步版本,无参 = 当前组件,传参 = 指定组件列表。多次调用合并为一次微任务渲染。`$` 内部就是调 `dirty()`。
|
|
878
951
|
|
|
879
952
|
### `ctx.ui.render()` — 同步强制渲染
|
|
880
953
|
|
|
881
|
-
与 `dirty()` 的微任务批量不同,`render()` 是**同步执行**的。调用后立即执行 VDOM diff + patch,DOM
|
|
954
|
+
与 `dirty()` 的微任务批量不同,`render()` 是**同步执行**的。调用后立即执行 VDOM diff + patch,DOM 立刻更新。无参时只刷新当前组件,传参时可精准刷新指定组件。
|
|
882
955
|
|
|
883
956
|
**何时必须用 `render()`**:
|
|
884
957
|
|
|
@@ -913,55 +986,57 @@ onClick: () => {
|
|
|
913
986
|
### 三种方式速查
|
|
914
987
|
|
|
915
988
|
```tsx
|
|
916
|
-
//
|
|
989
|
+
// 自动:$.x = val — 微任务批量,绑定当前组件
|
|
917
990
|
const $ = ctx.ui.$()
|
|
918
991
|
$.count++
|
|
919
|
-
$.name = 'hello' //
|
|
992
|
+
$.name = 'hello' // 多次赋值合并为一次渲染
|
|
920
993
|
|
|
921
|
-
//
|
|
994
|
+
// 手动:ctx.ui.render() — 同步,无参=当前,传参=指定
|
|
922
995
|
let count = 0
|
|
923
996
|
count++
|
|
924
997
|
ctx.ui.render() // DOM 立刻更新
|
|
998
|
+
ctx.ui.render(['stats']) // 精准刷新指定组件
|
|
925
999
|
|
|
926
|
-
//
|
|
1000
|
+
// 异步:ctx.ui.dirty() — 微任务批量,同 render() 作用域
|
|
1001
|
+
ctx.ui.dirty()
|
|
1002
|
+
ctx.ui.dirty(['stats']) // 批处理合并
|
|
927
1003
|
```
|
|
928
1004
|
|
|
929
1005
|
**性能说明**:
|
|
930
|
-
- `$.x = val` 和 `dirty()`
|
|
931
|
-
- `render()` 每次调用都触发一次完整 diff/patch
|
|
1006
|
+
- `$.x = val` 和 `dirty()` 都是微任务批量合并
|
|
1007
|
+
- `render()` 每次调用都触发一次完整 diff/patch
|
|
1008
|
+
- 三个入口同一套 scope 机制,不想要的渲染不触发
|
|
1009
|
+
|
|
1010
|
+
### 实践建议
|
|
932
1011
|
|
|
933
|
-
|
|
1012
|
+
**组件库**(可分享组件)推荐手动模式:
|
|
1013
|
+
|
|
1014
|
+
```tsx
|
|
1015
|
+
const DatePicker = (_init, ctx) => {
|
|
1016
|
+
let show = false // let 不触发渲染
|
|
1017
|
+
return (props) =>
|
|
1018
|
+
h('input', {
|
|
1019
|
+
onClick: () => { show = true; ctx.ui.render() }
|
|
1020
|
+
})
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
934
1023
|
|
|
935
|
-
|
|
1024
|
+
行为只由 `render()` 显式控制,不依赖 `$`,测试中 `render()` 直接 mock 为空函数。
|
|
936
1025
|
|
|
937
|
-
|
|
1026
|
+
**业务层**推荐自动模式:
|
|
938
1027
|
|
|
939
1028
|
```tsx
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
return {
|
|
945
|
-
add(item: ToastItem) {
|
|
946
|
-
items = [...items, item]
|
|
947
|
-
ctx.ui.render() // 显式同步渲染,确保 DOM 立即可见
|
|
948
|
-
},
|
|
949
|
-
remove(id: string) {
|
|
950
|
-
items = items.filter(i => i.id !== id)
|
|
951
|
-
ctx.ui.dirty() // 显式标记脏,下个微任务批量渲染
|
|
952
|
-
},
|
|
953
|
-
render: (props) =>
|
|
954
|
-
h('div', { class: 'toast-container' },
|
|
955
|
-
items.map(item => h('div', { key: item.id }, item.msg))
|
|
956
|
-
),
|
|
957
|
-
}
|
|
1029
|
+
const OrderPage = (_init, ctx) => {
|
|
1030
|
+
const $ = ctx.ui.$
|
|
1031
|
+
$.orders = [] // $ 赋值自动触发渲染
|
|
1032
|
+
$.loading = false
|
|
1033
|
+
return (props) => h('div', {}, $.loading ? h(Spinner) : h(OrderList, { orders: $.orders }))
|
|
958
1034
|
}
|
|
959
1035
|
```
|
|
960
1036
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
- 消费方不需要知道组件内部用 `$` 还是闭包,只需调用 API
|
|
1037
|
+
省事、安全、`$` 绑定所属组件不波及兄弟。
|
|
1038
|
+
|
|
1039
|
+
同一个组件内可以按变量混用两种模式:需要渲染的用 `$`,不需要的用 `let`。
|
|
965
1040
|
|
|
966
1041
|
---
|
|
967
1042
|
|
|
@@ -1282,7 +1357,7 @@ createApp()
|
|
|
1282
1357
|
|
|
1283
1358
|
// 运行时切换语言
|
|
1284
1359
|
ctx.i18n?.setLocale('en-US')
|
|
1285
|
-
// →
|
|
1360
|
+
// → 自动触发根组件重渲染(所有组件使用新语言文案)
|
|
1286
1361
|
```
|
|
1287
1362
|
|
|
1288
1363
|
| I18nOptions | 类型 | 默认值 | 说明 |
|
|
@@ -1574,6 +1649,9 @@ props 变化 ──────────────────────
|
|
|
1574
1649
|
| `onmounted` | `ref` 的 `if (el)` 分支 |
|
|
1575
1650
|
| `onunmount` | `ref` 的 `else` 分支 |
|
|
1576
1651
|
| `onupdate` | render 内层函数收新 props 自行比较 |
|
|
1652
|
+
| `全局刷新` | `ctx.ui.render(['_wf_root'])` |
|
|
1653
|
+
| `局部刷新` | `ctx.ui.render()` 或 `$.x = val` |
|
|
1654
|
+
| `跨组件刷新` | `ctx.ui.selfId('name')` + `render(['name'])` |
|
|
1577
1655
|
|
|
1578
1656
|
## 组件列表
|
|
1579
1657
|
|
package/dist/client/app.d.ts
CHANGED
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
* createApp() → app.use(mw) → app.mount('#root', RootComponent)
|
|
5
5
|
*
|
|
6
6
|
* ctx.ui 在 mount 时注入:
|
|
7
|
-
* ctx.ui.render()
|
|
8
|
-
* ctx.ui.
|
|
9
|
-
* ctx.ui
|
|
7
|
+
* ctx.ui.render() 同步刷新当前组件
|
|
8
|
+
* ctx.ui.render(['#id']) 同步刷新指定组件
|
|
9
|
+
* ctx.ui.dirty() 异步刷新当前组件(微任务批处理)
|
|
10
|
+
* ctx.ui.$() 响应式状态容器($.x = val 自动 dirty)
|
|
11
|
+
*
|
|
12
|
+
* render / dirty / $ 通过 prototype chain 实现组件级 scope:
|
|
13
|
+
* 每个组件 mount 时创建 childCtx.ui = Object.create(ctx.ui)
|
|
14
|
+
* 并设置 childCtx.ui._selfId = 组件 ID
|
|
15
|
+
* render() 无参时从 this._selfId 取当前组件 ID
|
|
10
16
|
*/
|
|
11
17
|
import type { WfuiContext, AppMiddleware } from './types.ts';
|
|
12
18
|
import type { Component } from './vnode.ts';
|
package/dist/client/index.js
CHANGED
|
@@ -39,6 +39,8 @@ function createPortal(children, portalKey) {
|
|
|
39
39
|
// src/client/render.ts
|
|
40
40
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
41
41
|
var SVG_TAGS = /* @__PURE__ */ new Set(["svg", "path", "circle", "line", "rect", "text", "g", "polyline", "polygon", "ellipse", "defs", "use", "clipPath", "mask", "linearGradient", "radialGradient", "stop", "tspan"]);
|
|
42
|
+
var _idCounter = 0;
|
|
43
|
+
var idRegistry = /* @__PURE__ */ new Map();
|
|
42
44
|
function render(input, ctx) {
|
|
43
45
|
return renderValue(input, ctx);
|
|
44
46
|
}
|
|
@@ -76,7 +78,15 @@ function renderValue(v, ctx) {
|
|
|
76
78
|
} else {
|
|
77
79
|
const flatChildren = flattenChildren(vnode.props?.children);
|
|
78
80
|
for (const child of flatChildren) {
|
|
79
|
-
|
|
81
|
+
const childNode = renderValue(child, ctx);
|
|
82
|
+
el.appendChild(childNode);
|
|
83
|
+
if (child && typeof child === "object" && typeof child.type === "function") {
|
|
84
|
+
const childVNode = child;
|
|
85
|
+
if (!childVNode._parentNode) {
|
|
86
|
+
childVNode._parentNode = el;
|
|
87
|
+
childVNode._refNode = childNode;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
92
|
if (selectValue !== void 0) {
|
|
@@ -89,9 +99,17 @@ function renderValue(v, ctx) {
|
|
|
89
99
|
function renderComponent(Comp, props, vnode, ctx) {
|
|
90
100
|
;
|
|
91
101
|
ctx.ui = ctx.ui ?? {};
|
|
102
|
+
if (!vnode._id) {
|
|
103
|
+
vnode._id = `_wf_${_idCounter++}`;
|
|
104
|
+
idRegistry.set(vnode._id, vnode);
|
|
105
|
+
}
|
|
106
|
+
const childCtx = Object.create(ctx);
|
|
107
|
+
childCtx.ui = Object.create(ctx.ui);
|
|
108
|
+
childCtx.ui._selfId = vnode._id;
|
|
109
|
+
childCtx.ui._selfVNode = vnode;
|
|
92
110
|
let childVNode;
|
|
93
111
|
try {
|
|
94
|
-
childVNode = Comp(props,
|
|
112
|
+
childVNode = Comp(props, childCtx);
|
|
95
113
|
if (typeof childVNode !== "function") {
|
|
96
114
|
throw new Error(
|
|
97
115
|
`Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
@@ -114,7 +132,12 @@ function renderComponent(Comp, props, vnode, ctx) {
|
|
|
114
132
|
return document.createTextNode("");
|
|
115
133
|
}
|
|
116
134
|
vnode._child = childVNode;
|
|
117
|
-
|
|
135
|
+
const domNode = renderValue(childVNode, childCtx);
|
|
136
|
+
if (!vnode._refNode) {
|
|
137
|
+
;
|
|
138
|
+
vnode._refNode = domNode;
|
|
139
|
+
}
|
|
140
|
+
return domNode;
|
|
118
141
|
}
|
|
119
142
|
function renderArray(arr, ctx) {
|
|
120
143
|
const frag = document.createDocumentFragment();
|
|
@@ -236,14 +259,23 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
|
|
|
236
259
|
const oldV = oldInput;
|
|
237
260
|
if (typeof newV.type === "function") {
|
|
238
261
|
const comp = newV.type;
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
if (oldV._render) {
|
|
263
|
+
newV._render = oldV._render;
|
|
264
|
+
newV._id = oldV._id;
|
|
265
|
+
if (newV._id) idRegistry.set(newV._id, newV);
|
|
266
|
+
}
|
|
267
|
+
newV._parentNode = parent;
|
|
268
|
+
newV._refNode = oldNode;
|
|
269
|
+
const childCtx = Object.create(ctx);
|
|
270
|
+
childCtx.ui = Object.create(ctx.ui);
|
|
271
|
+
childCtx.ui._selfId = newV._id;
|
|
272
|
+
childCtx.ui._selfVNode = newV;
|
|
241
273
|
let childNew;
|
|
242
274
|
try {
|
|
243
275
|
if (typeof newV._render === "function") {
|
|
244
276
|
childNew = newV._render(newV.props);
|
|
245
277
|
} else {
|
|
246
|
-
childNew = comp(newV.props,
|
|
278
|
+
childNew = comp(newV.props, childCtx);
|
|
247
279
|
if (typeof childNew === "function") {
|
|
248
280
|
newV._render = childNew;
|
|
249
281
|
childNew = childNew(newV.props);
|
|
@@ -261,7 +293,7 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
|
|
|
261
293
|
}
|
|
262
294
|
const _prevChild = oldV._child;
|
|
263
295
|
newV._child = childNew;
|
|
264
|
-
return patchValue(parent, oldNode, _prevChild, childNew,
|
|
296
|
+
return patchValue(parent, oldNode, _prevChild, childNew, childCtx);
|
|
265
297
|
}
|
|
266
298
|
if (newV.type === Fragment) {
|
|
267
299
|
patchChildren(parent, oldV, newV, ctx);
|
|
@@ -494,6 +526,186 @@ function callRefCleanup(input) {
|
|
|
494
526
|
}
|
|
495
527
|
}
|
|
496
528
|
|
|
529
|
+
// src/client/app.ts
|
|
530
|
+
function createApp() {
|
|
531
|
+
const middlewares = [];
|
|
532
|
+
let ctx = {};
|
|
533
|
+
let container = null;
|
|
534
|
+
let rootComponent = null;
|
|
535
|
+
let oldVNode = null;
|
|
536
|
+
let _rendering = false;
|
|
537
|
+
let _dirtyBatch = /* @__PURE__ */ new Set();
|
|
538
|
+
let _dirtyScheduled = false;
|
|
539
|
+
function renderByIds(ids) {
|
|
540
|
+
if (_rendering) return;
|
|
541
|
+
_rendering = true;
|
|
542
|
+
for (const id of ids) {
|
|
543
|
+
const vnode = idRegistry.get(id);
|
|
544
|
+
if (!vnode || !vnode._render) continue;
|
|
545
|
+
const oldChild = vnode._child;
|
|
546
|
+
const newChild = vnode._render(vnode.props);
|
|
547
|
+
vnode._child = newChild;
|
|
548
|
+
if (!vnode._parentNode && vnode._refNode) {
|
|
549
|
+
;
|
|
550
|
+
vnode._parentNode = vnode._refNode.parentNode;
|
|
551
|
+
}
|
|
552
|
+
if (vnode._parentNode) {
|
|
553
|
+
const newNode = patchValue(
|
|
554
|
+
vnode._parentNode,
|
|
555
|
+
vnode._refNode ?? null,
|
|
556
|
+
oldChild,
|
|
557
|
+
newChild,
|
|
558
|
+
ctx
|
|
559
|
+
);
|
|
560
|
+
if (newNode && newNode !== vnode._refNode) {
|
|
561
|
+
vnode._refNode = newNode;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
_rendering = false;
|
|
566
|
+
flushDirtyBatch();
|
|
567
|
+
}
|
|
568
|
+
function flushDirtyBatch() {
|
|
569
|
+
if (_dirtyBatch.size > 0 && !_dirtyScheduled) {
|
|
570
|
+
_dirtyScheduled = true;
|
|
571
|
+
queueMicrotask(() => {
|
|
572
|
+
_dirtyScheduled = false;
|
|
573
|
+
const batch = [..._dirtyBatch];
|
|
574
|
+
_dirtyBatch.clear();
|
|
575
|
+
if (batch.length > 0) renderByIds(batch);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function getSelfId(uiObj) {
|
|
580
|
+
return uiObj?._selfId ?? ctx.ui?._selfId;
|
|
581
|
+
}
|
|
582
|
+
const app = {
|
|
583
|
+
get ctx() {
|
|
584
|
+
return ctx;
|
|
585
|
+
},
|
|
586
|
+
use(mw) {
|
|
587
|
+
middlewares.push(mw);
|
|
588
|
+
return app;
|
|
589
|
+
},
|
|
590
|
+
async mount(rootSelector, RootComponent) {
|
|
591
|
+
rootComponent = RootComponent;
|
|
592
|
+
for (const mw of middlewares) {
|
|
593
|
+
ctx = await mw(ctx);
|
|
594
|
+
}
|
|
595
|
+
const el = typeof rootSelector === "string" ? document.querySelector(rootSelector) : rootSelector;
|
|
596
|
+
if (!el) throw new Error(`mount target not found: ${rootSelector}`);
|
|
597
|
+
container = el;
|
|
598
|
+
container.innerHTML = "";
|
|
599
|
+
ctx.ui = {
|
|
600
|
+
_selfId: "_wf_root",
|
|
601
|
+
/** 同步刷新(无参 = 当前组件,传参 = 指定组件列表) */
|
|
602
|
+
render: function(ids) {
|
|
603
|
+
if (!ids || ids.length === 0) {
|
|
604
|
+
const selfId = getSelfId(this);
|
|
605
|
+
if (selfId) ids = [selfId];
|
|
606
|
+
else return;
|
|
607
|
+
}
|
|
608
|
+
renderByIds(ids);
|
|
609
|
+
},
|
|
610
|
+
/** 异步刷新(微任务批处理,无参 = 当前组件) */
|
|
611
|
+
dirty: function(ids) {
|
|
612
|
+
if (_rendering) return;
|
|
613
|
+
if (!ids || ids.length === 0) {
|
|
614
|
+
const selfId = getSelfId(this);
|
|
615
|
+
if (selfId) ids = [selfId];
|
|
616
|
+
else return;
|
|
617
|
+
}
|
|
618
|
+
for (const id of ids) {
|
|
619
|
+
if (id) _dirtyBatch.add(id);
|
|
620
|
+
}
|
|
621
|
+
if (!_dirtyScheduled) {
|
|
622
|
+
_dirtyScheduled = true;
|
|
623
|
+
queueMicrotask(() => {
|
|
624
|
+
_dirtyScheduled = false;
|
|
625
|
+
const batch = [..._dirtyBatch];
|
|
626
|
+
_dirtyBatch.clear();
|
|
627
|
+
if (batch.length > 0) renderByIds(batch);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
/** 创建响应式状态容器:$.x = val 自动触发 dirty() */
|
|
632
|
+
$: function() {
|
|
633
|
+
const selfId = getSelfId(this);
|
|
634
|
+
return createReactiveState(() => {
|
|
635
|
+
if (selfId) ctx.ui.dirty([selfId]);
|
|
636
|
+
});
|
|
637
|
+
},
|
|
638
|
+
/** 注册组件实例的自定义 ID(用于跨组件精准刷新) */
|
|
639
|
+
selfId: function(name) {
|
|
640
|
+
if (typeof name !== "string" || !name) {
|
|
641
|
+
throw new Error(`[weifuwu] selfId requires a non-empty string, got ${typeof name}`);
|
|
642
|
+
}
|
|
643
|
+
if (idRegistry.has(name)) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
`[weifuwu] Duplicate component ID: "${name}". Each component must have a unique custom ID.`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
const vnode = this._selfVNode;
|
|
649
|
+
if (!vnode) return;
|
|
650
|
+
vnode._customId = name;
|
|
651
|
+
idRegistry.set(name, vnode);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
_rendering = true;
|
|
655
|
+
oldVNode = wrapComponent(RootComponent, ctx);
|
|
656
|
+
oldVNode._id = "_wf_root";
|
|
657
|
+
oldVNode._parentNode = container;
|
|
658
|
+
oldVNode._refNode = null;
|
|
659
|
+
idRegistry.set("_wf_root", oldVNode);
|
|
660
|
+
const node = render(oldVNode, ctx);
|
|
661
|
+
if (node instanceof Node) container.appendChild(node);
|
|
662
|
+
oldVNode._refNode = container.firstChild;
|
|
663
|
+
_rendering = false;
|
|
664
|
+
flushDirtyBatch();
|
|
665
|
+
},
|
|
666
|
+
destroy() {
|
|
667
|
+
if (container) container.innerHTML = "";
|
|
668
|
+
container = null;
|
|
669
|
+
ctx = {};
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
return app;
|
|
673
|
+
}
|
|
674
|
+
function wrapComponent(Comp, _ctx) {
|
|
675
|
+
return { type: Comp, props: {}, key: void 0 };
|
|
676
|
+
}
|
|
677
|
+
function createReactiveState(dirty) {
|
|
678
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
679
|
+
const reactive = (target) => {
|
|
680
|
+
if (target === null || typeof target !== "object") return target;
|
|
681
|
+
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
682
|
+
const proxy = new Proxy(target, {
|
|
683
|
+
set(target2, key, value) {
|
|
684
|
+
const old = Reflect.get(target2, key);
|
|
685
|
+
if (old === value) return true;
|
|
686
|
+
Reflect.set(target2, key, value);
|
|
687
|
+
dirty();
|
|
688
|
+
return true;
|
|
689
|
+
},
|
|
690
|
+
get(target2, key) {
|
|
691
|
+
const value = Reflect.get(target2, key);
|
|
692
|
+
if (typeof value === "object" && value !== null) return reactive(value);
|
|
693
|
+
return value;
|
|
694
|
+
},
|
|
695
|
+
deleteProperty(target2, key) {
|
|
696
|
+
if (Reflect.has(target2, key)) {
|
|
697
|
+
Reflect.deleteProperty(target2, key);
|
|
698
|
+
dirty();
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
proxyCache.set(target, proxy);
|
|
704
|
+
return proxy;
|
|
705
|
+
};
|
|
706
|
+
return reactive({});
|
|
707
|
+
}
|
|
708
|
+
|
|
497
709
|
// src/client/router.ts
|
|
498
710
|
function flattenRoutes(routes, basePath = "", chain = []) {
|
|
499
711
|
const result = [];
|
|
@@ -508,7 +720,6 @@ function flattenRoutes(routes, basePath = "", chain = []) {
|
|
|
508
720
|
}
|
|
509
721
|
return result;
|
|
510
722
|
}
|
|
511
|
-
var layoutDepth = /* @__PURE__ */ new WeakMap();
|
|
512
723
|
function joinPaths(a, b) {
|
|
513
724
|
if (!b || b === "/") return a || "/";
|
|
514
725
|
const left = a.endsWith("/") ? a.slice(0, -1) : a;
|
|
@@ -595,128 +806,20 @@ function router(opts) {
|
|
|
595
806
|
};
|
|
596
807
|
}
|
|
597
808
|
function RouteView(_props, ctx) {
|
|
809
|
+
const _depth = ctx.route?._rvDepth ?? 0;
|
|
598
810
|
return () => {
|
|
599
811
|
const route = ctx.route;
|
|
600
|
-
if (!route?.chain?.length) return null;
|
|
601
|
-
const
|
|
602
|
-
const depth = layoutDepth.get(ctxAny) ?? 0;
|
|
603
|
-
if (depth >= route.chain.length) return null;
|
|
604
|
-
const def = route.chain[depth];
|
|
812
|
+
if (!route?.chain?.length || _depth >= route.chain.length) return null;
|
|
813
|
+
const def = route.chain[_depth];
|
|
605
814
|
const Comp = def.layout ?? def.component;
|
|
606
815
|
if (!Comp) return null;
|
|
607
816
|
if (def.layout) {
|
|
608
|
-
|
|
817
|
+
route._rvDepth = _depth + 1;
|
|
609
818
|
}
|
|
610
819
|
return { type: Comp, props: {}, key: void 0 };
|
|
611
820
|
};
|
|
612
821
|
}
|
|
613
822
|
|
|
614
|
-
// src/client/app.ts
|
|
615
|
-
function createApp() {
|
|
616
|
-
const middlewares = [];
|
|
617
|
-
let ctx = {};
|
|
618
|
-
let container = null;
|
|
619
|
-
let rootComponent = null;
|
|
620
|
-
let oldVNode = null;
|
|
621
|
-
let rendered = false;
|
|
622
|
-
const app = {
|
|
623
|
-
get ctx() {
|
|
624
|
-
return ctx;
|
|
625
|
-
},
|
|
626
|
-
use(mw) {
|
|
627
|
-
middlewares.push(mw);
|
|
628
|
-
return app;
|
|
629
|
-
},
|
|
630
|
-
async mount(rootSelector, RootComponent) {
|
|
631
|
-
rootComponent = RootComponent;
|
|
632
|
-
for (const mw of middlewares) {
|
|
633
|
-
ctx = await mw(ctx);
|
|
634
|
-
}
|
|
635
|
-
const el = typeof rootSelector === "string" ? document.querySelector(rootSelector) : rootSelector;
|
|
636
|
-
if (!el) throw new Error(`mount target not found: ${rootSelector}`);
|
|
637
|
-
container = el;
|
|
638
|
-
container.innerHTML = "";
|
|
639
|
-
let _dirty = false;
|
|
640
|
-
let _rendering = false;
|
|
641
|
-
const doRender = () => {
|
|
642
|
-
if (_rendering || !container || !rootComponent || !oldVNode) return;
|
|
643
|
-
_rendering = true;
|
|
644
|
-
layoutDepth.delete(ctx);
|
|
645
|
-
const newVNode = wrapComponent(rootComponent, ctx);
|
|
646
|
-
const oldNode = container.firstChild;
|
|
647
|
-
if (oldNode) {
|
|
648
|
-
patchValue(container, oldNode, oldVNode, newVNode, ctx);
|
|
649
|
-
}
|
|
650
|
-
oldVNode = newVNode;
|
|
651
|
-
_rendering = false;
|
|
652
|
-
};
|
|
653
|
-
const scheduleRender = () => {
|
|
654
|
-
if (_dirty || _rendering) return;
|
|
655
|
-
_dirty = true;
|
|
656
|
-
queueMicrotask(() => {
|
|
657
|
-
if (!_dirty) return;
|
|
658
|
-
_dirty = false;
|
|
659
|
-
doRender();
|
|
660
|
-
});
|
|
661
|
-
};
|
|
662
|
-
ctx.ui = {
|
|
663
|
-
/** 立即同步渲染 */
|
|
664
|
-
render: doRender,
|
|
665
|
-
/** 标记脏状态,下个微任务批量渲染 */
|
|
666
|
-
dirty: scheduleRender,
|
|
667
|
-
/** 创建响应式状态容器:$.x = val 自动触发 dirty()(仅事件/timer 中生效) */
|
|
668
|
-
$: () => createReactiveState(scheduleRender)
|
|
669
|
-
};
|
|
670
|
-
_rendering = true;
|
|
671
|
-
oldVNode = wrapComponent(RootComponent, ctx);
|
|
672
|
-
const node = render(oldVNode, ctx);
|
|
673
|
-
if (node instanceof Node) container.appendChild(node);
|
|
674
|
-
_rendering = false;
|
|
675
|
-
rendered = true;
|
|
676
|
-
},
|
|
677
|
-
destroy() {
|
|
678
|
-
if (container) container.innerHTML = "";
|
|
679
|
-
container = null;
|
|
680
|
-
ctx = {};
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
return app;
|
|
684
|
-
}
|
|
685
|
-
function wrapComponent(Comp, _ctx) {
|
|
686
|
-
return { type: Comp, props: {}, key: void 0 };
|
|
687
|
-
}
|
|
688
|
-
function createReactiveState(dirty) {
|
|
689
|
-
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
690
|
-
const reactive = (target) => {
|
|
691
|
-
if (target === null || typeof target !== "object") return target;
|
|
692
|
-
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
693
|
-
const proxy = new Proxy(target, {
|
|
694
|
-
set(target2, key, value) {
|
|
695
|
-
const old = Reflect.get(target2, key);
|
|
696
|
-
if (old === value) return true;
|
|
697
|
-
Reflect.set(target2, key, value);
|
|
698
|
-
dirty();
|
|
699
|
-
return true;
|
|
700
|
-
},
|
|
701
|
-
get(target2, key) {
|
|
702
|
-
const value = Reflect.get(target2, key);
|
|
703
|
-
if (typeof value === "object" && value !== null) return reactive(value);
|
|
704
|
-
return value;
|
|
705
|
-
},
|
|
706
|
-
deleteProperty(target2, key) {
|
|
707
|
-
if (Reflect.has(target2, key)) {
|
|
708
|
-
Reflect.deleteProperty(target2, key);
|
|
709
|
-
dirty();
|
|
710
|
-
}
|
|
711
|
-
return true;
|
|
712
|
-
}
|
|
713
|
-
});
|
|
714
|
-
proxyCache.set(target, proxy);
|
|
715
|
-
return proxy;
|
|
716
|
-
};
|
|
717
|
-
return reactive({});
|
|
718
|
-
}
|
|
719
|
-
|
|
720
823
|
// src/client/types.ts
|
|
721
824
|
function extendCtx(ctx, fields) {
|
|
722
825
|
return Object.assign(Object.create(ctx), fields);
|
package/dist/client/render.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { VNode } from './vnode.ts';
|
|
14
14
|
import type { WfuiContext } from './types.ts';
|
|
15
|
+
export declare const idRegistry: Map<string, VNode>;
|
|
15
16
|
export declare function render(input: any, ctx: WfuiContext): Node;
|
|
16
17
|
export declare function patchValue(parent: Node, oldNode: Node | null, oldInput: any, newInput: any, ctx: WfuiContext): Node | null;
|
|
17
18
|
export declare function mountVNode(container: Element, vnode: VNode, ctx: WfuiContext): void;
|
package/dist/client/router.d.ts
CHANGED
|
@@ -9,6 +9,5 @@ export interface RouterOptions {
|
|
|
9
9
|
routes: RouteDef[];
|
|
10
10
|
notFound?: (props: any, ctx: WfuiContext) => any;
|
|
11
11
|
}
|
|
12
|
-
export declare const layoutDepth: WeakMap<any, number>;
|
|
13
12
|
export declare function router(opts: RouterOptions): AppMiddleware;
|
|
14
13
|
export declare function RouteView(_props: {}, ctx: WfuiContext): () => any;
|
package/dist/client/types.d.ts
CHANGED
|
@@ -6,12 +6,18 @@ export interface WfuiContext {
|
|
|
6
6
|
[key: string]: unknown;
|
|
7
7
|
/** UI 框架能力(由 createApp.mount 注入) */
|
|
8
8
|
ui: {
|
|
9
|
-
/**
|
|
10
|
-
render: () => void;
|
|
11
|
-
/**
|
|
12
|
-
dirty: () => void;
|
|
13
|
-
/** 创建响应式状态容器:$.x = val 自动触发 dirty()
|
|
9
|
+
/** 触发组件重渲染(同步,无参 = 当前组件) */
|
|
10
|
+
render: (ids?: string[]) => void;
|
|
11
|
+
/** 异步触发组件重渲染(微任务批处理,无参 = 当前组件) */
|
|
12
|
+
dirty: (ids?: string[]) => void;
|
|
13
|
+
/** 创建响应式状态容器:$.x = val 自动触发 dirty() */
|
|
14
14
|
$: () => Record<string, any>;
|
|
15
|
+
/** 注册组件实例的自定义语义 ID,同名冲突抛错 */
|
|
16
|
+
selfId: (name: string) => void;
|
|
17
|
+
/** 当前组件实例 ID(仅供内部使用,通过 ctx 扩展注入) */
|
|
18
|
+
_selfId?: string;
|
|
19
|
+
/** 当前组件 VNode 引用(仅供内部使用,通过 ctx 扩展注入) */
|
|
20
|
+
_selfVNode?: any;
|
|
15
21
|
};
|
|
16
22
|
/** 路由(由 router 中间件注入) */
|
|
17
23
|
route?: {
|
package/dist/client/vnode.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface VNode {
|
|
|
19
19
|
_portalEl?: HTMLDivElement | undefined;
|
|
20
20
|
/** 两阶段组件的 render 函数(mount 返回的函数) */
|
|
21
21
|
_render?: (props: any) => VNode | null;
|
|
22
|
+
/** 组件实例 ID(如 '_wf_0') */
|
|
23
|
+
_id?: string;
|
|
24
|
+
/** 组件输出的 DOM 父节点 */
|
|
25
|
+
_parentNode?: Node;
|
|
26
|
+
/** 组件输出的第一个 DOM 节点 */
|
|
27
|
+
_refNode?: Node | null;
|
|
22
28
|
}
|
|
23
29
|
export type Component<P = {}> = (initProps: P, ctx: WfuiContext) => ((props: P) => VNode | null) | null;
|
|
24
30
|
export declare const Fragment: unique symbol;
|