@variojs/vue 0.0.1 → 0.0.4
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 +53 -848
- package/dist/bindings.d.ts +2 -1
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +1 -199
- package/dist/bindings.js.map +1 -1
- package/dist/composable.js +1 -386
- package/dist/features/attrs-builder.d.ts.map +1 -1
- package/dist/features/attrs-builder.js +1 -143
- package/dist/features/attrs-builder.js.map +1 -1
- package/dist/features/children-resolver.js +1 -171
- package/dist/features/component-resolver.js +1 -110
- package/dist/features/computed.js +1 -161
- package/dist/features/event-handler.js +1 -103
- package/dist/features/expression-evaluator.js +1 -28
- package/dist/features/lifecycle-wrapper.js +1 -102
- package/dist/features/loop-handler.js +1 -168
- package/dist/features/path-resolver.d.ts +4 -0
- package/dist/features/path-resolver.d.ts.map +1 -1
- package/dist/features/path-resolver.js +1 -171
- package/dist/features/path-resolver.js.map +1 -1
- package/dist/features/provide-inject.js +1 -139
- package/dist/features/refs.js +1 -113
- package/dist/features/teleport.js +1 -32
- package/dist/features/validators.js +1 -297
- package/dist/features/watch.js +1 -154
- package/dist/index.js +1 -24
- package/dist/renderer.js +1 -244
- package/dist/types.js +0 -16
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -1,915 +1,120 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# 🎨 @variojs/vue
|
|
2
2
|
|
|
3
|
-
Vario Vue
|
|
3
|
+
Vario Vue 渲染器 - 深度集成 Vue 3 的 Schema 渲染器
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 特点
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **Schema 只负责结构**:Schema 定义 UI 结构、属性绑定、事件映射
|
|
12
|
-
- **Vue 负责响应式逻辑**:computed、watch 等响应式特性使用 Vue 原生 API
|
|
13
|
-
- **声明映射而非重新实现**:ref、生命周期、provide/inject 等通过声明映射到 Vue 原生 API
|
|
14
|
-
- **自动响应式同步**:state 自动包裹为响应式对象,支持层级依赖收集
|
|
7
|
+
- 🚀 **深度集成**:完整支持 Vue 3 Composition API
|
|
8
|
+
- 📦 **声明式 Schema**:JSON Schema 定义 UI,简洁易维护
|
|
9
|
+
- 🔄 **自动响应式**:状态自动包裹为响应式,支持层级依赖收集
|
|
10
|
+
- 🎯 **Vue 特性支持**:ref、生命周期、provide/inject、teleport 等
|
|
15
11
|
|
|
16
12
|
## 安装
|
|
17
13
|
|
|
18
14
|
```bash
|
|
19
|
-
npm install @
|
|
15
|
+
npm install @variojs/vue
|
|
20
16
|
# 或
|
|
21
|
-
pnpm add @
|
|
17
|
+
pnpm add @variojs/vue
|
|
22
18
|
```
|
|
23
19
|
|
|
20
|
+
依赖的 `@variojs/core`、`@variojs/schema` 和 `vue` 会自动安装。
|
|
21
|
+
|
|
24
22
|
## 快速开始
|
|
25
23
|
|
|
26
24
|
```typescript
|
|
27
|
-
import { useVario } from '@
|
|
28
|
-
import type { VueSchemaNode } from '@
|
|
25
|
+
import { useVario } from '@variojs/vue'
|
|
26
|
+
import type { VueSchemaNode } from '@variojs/vue'
|
|
29
27
|
|
|
30
28
|
const schema: VueSchemaNode = {
|
|
31
29
|
type: 'div',
|
|
32
30
|
children: [
|
|
33
31
|
{
|
|
34
|
-
type: '
|
|
32
|
+
type: 'ElInput',
|
|
35
33
|
model: 'name',
|
|
36
34
|
props: { placeholder: '请输入姓名' }
|
|
37
35
|
},
|
|
38
36
|
{
|
|
39
37
|
type: 'div',
|
|
40
38
|
children: '{{ name }}'
|
|
41
|
-
}
|
|
42
|
-
]
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export default {
|
|
46
|
-
setup() {
|
|
47
|
-
const { vnode, state } = useVario(schema, {
|
|
48
|
-
state: {
|
|
49
|
-
name: ''
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
return { vnode, state }
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## 核心概念
|
|
59
|
-
|
|
60
|
-
### 状态管理
|
|
61
|
-
|
|
62
|
-
`useVario` 会自动将传入的 `state` 包裹为 Vue 的响应式对象(`reactive`),并建立与运行时上下文(`RuntimeContext`)的双向同步。
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
const { state, ctx } = useVario(schema, {
|
|
66
|
-
state: {
|
|
67
|
-
user: {
|
|
68
|
-
name: 'John',
|
|
69
|
-
age: 30
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
// state 是响应式的
|
|
75
|
-
state.user.name = 'Jane' // ✅ Vue 会自动追踪变化
|
|
76
|
-
|
|
77
|
-
// 也可以通过 ctx 访问
|
|
78
|
-
ctx._get('user.name') // 'Jane'
|
|
79
|
-
ctx._set('user.age', 31) // ✅ 会同步到 state.user.age
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### Model 双向绑定与依赖收集
|
|
83
|
-
|
|
84
|
-
**关键特性:会根据层级自动收集依赖**
|
|
85
|
-
|
|
86
|
-
当你在 Schema 中使用 `model` 双向绑定时,Vario 会:
|
|
87
|
-
|
|
88
|
-
1. **自动追踪嵌套路径**:使用 Vue 的 Proxy 响应式系统追踪深层属性访问
|
|
89
|
-
2. **双向同步**:`ctx._set` 的变化会通过 `onStateChange` 同步到 `reactiveState`
|
|
90
|
-
3. **层级依赖收集**:Vue 会自动追踪所有访问的嵌套路径
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
const schema: VueSchemaNode = {
|
|
94
|
-
type: 'div',
|
|
95
|
-
children: [
|
|
96
|
-
{
|
|
97
|
-
type: 'input',
|
|
98
|
-
model: 'user.profile.name', // 深层路径
|
|
99
|
-
props: { placeholder: '姓名' }
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
type: 'input',
|
|
103
|
-
model: 'user.profile.email', // 另一个深层路径
|
|
104
|
-
props: { placeholder: '邮箱' }
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
type: 'div',
|
|
108
|
-
children: '{{ user.profile.name }} - {{ user.profile.email }}'
|
|
109
|
-
}
|
|
110
|
-
]
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const { state } = useVario(schema, {
|
|
114
|
-
state: {
|
|
115
|
-
user: {
|
|
116
|
-
profile: {
|
|
117
|
-
name: '',
|
|
118
|
-
email: ''
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
// 当用户在输入框中输入时:
|
|
125
|
-
// 1. 触发 update:modelValue 事件
|
|
126
|
-
// 2. 调用 ctx._set('user.profile.name', newValue)
|
|
127
|
-
// 3. 触发 onStateChange 回调
|
|
128
|
-
// 4. 通过 setPathValue 更新 reactiveState.user.profile.name
|
|
129
|
-
// 5. Vue 检测到变化,自动更新所有依赖该路径的组件
|
|
130
|
-
// - 包括显示 {{ user.profile.name }} 的 div
|
|
131
|
-
// - 包括绑定 model: 'user.profile.name' 的 input(值同步)
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
**依赖收集机制详解**:
|
|
135
|
-
|
|
136
|
-
```typescript
|
|
137
|
-
// 在 bindings.ts 中
|
|
138
|
-
const value = getPathValue(getState(), modelPath)
|
|
139
|
-
// getState() 返回 reactiveState(Vue 响应式对象)
|
|
140
|
-
// 当 Vue 组件访问 reactiveState.user.profile.name 时
|
|
141
|
-
// Vue 的 Proxy 会自动追踪这个嵌套路径的依赖
|
|
142
|
-
|
|
143
|
-
// 在 composable.ts 中
|
|
144
|
-
onStateChange: (path: string, value: unknown) => {
|
|
145
|
-
setPathValue(
|
|
146
|
-
reactiveState,
|
|
147
|
-
path, // 'user.profile.name'
|
|
148
|
-
value,
|
|
149
|
-
{
|
|
150
|
-
createObject: () => reactive({}), // 中间对象也是响应式的
|
|
151
|
-
createArray: () => reactive([])
|
|
152
|
-
}
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
// 这样确保了整个路径链都是响应式的
|
|
156
|
-
// reactiveState.user → reactive
|
|
157
|
-
// reactiveState.user.profile → reactive
|
|
158
|
-
// reactiveState.user.profile.name → 值
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
**示例:深层嵌套绑定**
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
const schema: VueSchemaNode = {
|
|
165
|
-
type: 'div',
|
|
166
|
-
children: [
|
|
167
|
-
{
|
|
168
|
-
type: 'input',
|
|
169
|
-
model: 'form.user.profile.contact.phone', // 4 层嵌套
|
|
170
|
-
props: { placeholder: '电话' }
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
type: 'div',
|
|
174
|
-
children: '{{ form.user.profile.contact.phone }}'
|
|
175
|
-
}
|
|
176
|
-
]
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const { state } = useVario(schema, {
|
|
180
|
-
state: {
|
|
181
|
-
form: {
|
|
182
|
-
user: {
|
|
183
|
-
profile: {
|
|
184
|
-
contact: {
|
|
185
|
-
phone: ''
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
// ✅ 所有层级都会自动收集依赖
|
|
194
|
-
// ✅ 修改 state.form.user.profile.contact.phone 会触发更新
|
|
195
|
-
// ✅ 在输入框中输入会同步到 state
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
#### 自动路径解析与状态创建
|
|
199
|
-
|
|
200
|
-
Vario 支持智能的 model 路径解析,可以自动拼接扁平路径,并自动创建缺失的状态结构。行为完全由 **model 的写法** 决定。
|
|
201
|
-
|
|
202
|
-
**model 的几种形式**
|
|
203
|
-
|
|
204
|
-
| 写法 | 是否压栈 | 是否在本节点绑定 | 典型用途 |
|
|
205
|
-
|------|----------|------------------|----------|
|
|
206
|
-
| **字符串路径** `model: "form"` | 压栈 | 绑定 | 普通表单项、嵌套路径根 |
|
|
207
|
-
| **字符串** `model: "."` | 不压栈(用现有栈) | 栈非空时绑定到「当前栈路径」 | 循环中绑定到 `items[0]`、`items[1]` 等数组元素本身 |
|
|
208
|
-
| **对象** `model: { path: "form", scope: true }` | 压栈 `path` | **不**绑定 | 仅作层级的容器(如表单根、区块) |
|
|
209
|
-
|
|
210
|
-
子节点的扁平路径会自动与父级路径栈(来自字符串 `model` 或 `model: { path, scope: true }`)拼接:
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
const schema: VueSchemaNode = {
|
|
214
|
-
type: 'div',
|
|
215
|
-
model: { path: 'form', scope: true }, // 父级仅作作用域,不绑定
|
|
216
|
-
children: [
|
|
217
|
-
{
|
|
218
|
-
type: 'input',
|
|
219
|
-
model: 'name' // 扁平路径 → 自动变成 form.name
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
type: 'input',
|
|
223
|
-
model: 'email' // 扁平路径 → 自动变成 form.email
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
type: 'div',
|
|
227
|
-
model: { path: 'user', scope: true }, // 再压一层 user,不绑定
|
|
228
|
-
children: [
|
|
229
|
-
{
|
|
230
|
-
type: 'input',
|
|
231
|
-
model: 'age' // 扁平路径 → 自动变成 form.user.age
|
|
232
|
-
}
|
|
233
|
-
]
|
|
234
|
-
}
|
|
235
|
-
]
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const { state } = useVario(schema, {
|
|
239
|
-
state: {} // 空状态,会自动创建结构
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
// ✅ state.form.name 会被自动创建
|
|
243
|
-
// ✅ state.form.email 会被自动创建
|
|
244
|
-
// ✅ state.form.user.age 会被自动创建
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
**使用 scope 的容器(仅作用域、不绑定)**
|
|
248
|
-
|
|
249
|
-
希望某节点只提供路径层级、自身不参与绑定时,使用对象形式并设置 `scope: true`:
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
const schema: VueSchemaNode = {
|
|
253
|
-
type: 'form',
|
|
254
|
-
model: { path: 'form', scope: true }, // 仅作用域,不绑定
|
|
255
|
-
children: [
|
|
256
|
-
{ type: 'input', model: 'name' }, // → form.name
|
|
257
|
-
{ type: 'input', model: 'email' } // → form.email
|
|
258
|
-
]
|
|
259
|
-
}
|
|
260
|
-
// ✅ 根节点不会对 state.form 做 v-model,仅子节点绑定 form.name / form.email
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
**明确路径**
|
|
264
|
-
|
|
265
|
-
使用完整路径时,不会进行拼接,直接使用。容器建议用 `scope`,避免对 div 建立 v-model:
|
|
266
|
-
|
|
267
|
-
```typescript
|
|
268
|
-
const schema: VueSchemaNode = {
|
|
269
|
-
type: 'div',
|
|
270
|
-
model: { path: 'form', scope: true }, // 容器用 scope,不绑定
|
|
271
|
-
children: [
|
|
272
|
-
{
|
|
273
|
-
type: 'input',
|
|
274
|
-
model: 'form.user.name' // 明确路径,直接使用
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
type: 'input',
|
|
278
|
-
model: 'name' // 扁平路径,拼接成 form.name
|
|
279
|
-
}
|
|
280
|
-
]
|
|
281
|
-
}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
**数组访问语法 `[]`**
|
|
285
|
-
|
|
286
|
-
使用 `[]` 语法可以明确指定数组访问,系统会自动创建数组结构:
|
|
287
|
-
|
|
288
|
-
```typescript
|
|
289
|
-
const schema: VueSchemaNode = {
|
|
290
|
-
type: 'div',
|
|
291
|
-
children: [
|
|
292
|
-
{
|
|
293
|
-
type: 'input',
|
|
294
|
-
model: 'users[0].name' // 使用 [] 语法明确数组访问
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
type: 'input',
|
|
298
|
-
model: 'users[1].email' // 另一个数组项
|
|
299
|
-
}
|
|
300
|
-
]
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const { state } = useVario(schema, {
|
|
304
|
-
state: {} // 空状态
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
// ✅ state.users 会被自动创建为数组
|
|
308
|
-
// ✅ state.users[0].name 会被自动创建
|
|
309
|
-
// ✅ state.users[1].email 会被自动创建
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
**嵌套数组和对象**
|
|
313
|
-
|
|
314
|
-
支持复杂的嵌套结构:
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
const schema: VueSchemaNode = {
|
|
318
|
-
type: 'div',
|
|
319
|
-
children: [
|
|
320
|
-
{
|
|
321
|
-
type: 'input',
|
|
322
|
-
model: 'data.users[0].profile.tags[1]' // 混合嵌套
|
|
323
|
-
}
|
|
324
|
-
]
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ✅ 自动创建结构:
|
|
328
|
-
// state.data (对象)
|
|
329
|
-
// → state.data.users (数组)
|
|
330
|
-
// → state.data.users[0] (对象)
|
|
331
|
-
// → state.data.users[0].profile (对象)
|
|
332
|
-
// → state.data.users[0].profile.tags (数组)
|
|
333
|
-
// → state.data.users[0].profile.tags[1] (值)
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
**循环中的自动路径解析**
|
|
337
|
-
|
|
338
|
-
在循环中,扁平路径会自动拼接循环索引。循环层用 `scope` 提供数组路径,避免对容器做 v-model:
|
|
339
|
-
|
|
340
|
-
```typescript
|
|
341
|
-
const schema: VueSchemaNode = {
|
|
342
|
-
type: 'div',
|
|
343
|
-
model: { path: 'users', scope: true }, // 循环层用 scope,不绑定
|
|
344
|
-
loop: {
|
|
345
|
-
items: '{{ users }}',
|
|
346
|
-
itemKey: 'user'
|
|
347
|
-
},
|
|
348
|
-
children: [
|
|
349
|
-
{
|
|
350
|
-
type: 'input',
|
|
351
|
-
model: 'name' // 扁平路径 → 自动变成 users[0].name, users[1].name...
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
type: 'input',
|
|
355
|
-
model: 'age' // 扁平路径 → 自动变成 users[0].age, users[1].age...
|
|
356
|
-
}
|
|
357
|
-
]
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const { state } = useVario(schema, {
|
|
361
|
-
state: {
|
|
362
|
-
users: [
|
|
363
|
-
{ name: 'John', age: 30 },
|
|
364
|
-
{ name: 'Jane', age: 25 }
|
|
365
|
-
]
|
|
366
|
-
}
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
// ✅ 每个循环项都会正确绑定到对应的数组索引
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
**绑定到数组元素本身(`model: "."`)**
|
|
373
|
-
|
|
374
|
-
当希望「子组件输入值」直接作为数组元素(`state.items[0]`、`state.items[1]` 为字符串等原始值),而不是 `items[i].xxx` 的对象的字段时,在循环内使用 **`model: "."`**,表示绑定到当前路径栈对应的路径(即当前循环项在 state 中的位置):
|
|
375
|
-
|
|
376
|
-
```typescript
|
|
377
|
-
const schema: VueSchemaNode = {
|
|
378
|
-
type: 'div',
|
|
379
|
-
loop: { items: 'items', itemKey: 'it' },
|
|
380
|
-
model: { path: 'items', scope: true },
|
|
381
|
-
children: [
|
|
382
|
-
{ type: 'input', model: '.' } // 绑定到 items[0]、items[1] 本身
|
|
383
|
-
]
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const { state } = useVario(schema, {
|
|
387
|
-
state: { items: ['', ''] }
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
// 第 1 行输入 "a" → state.items[0] === 'a'
|
|
391
|
-
// 第 2 行输入 "b" → state.items[1] === 'b'
|
|
392
|
-
// state.items 为字符串数组,不是对象数组
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
使用 `model: "."` 时,当前路径栈必须非空(例如在带 `model: { path, scope: true }` 的循环下)才会创建绑定。
|
|
396
|
-
|
|
397
|
-
**循环中嵌套的 model 路径栈**
|
|
398
|
-
|
|
399
|
-
循环中可以嵌套多层;每层容器用 `scope` 只提供路径、不绑定:
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
const schema: VueSchemaNode = {
|
|
403
|
-
type: 'div',
|
|
404
|
-
model: { path: 'data', scope: true }, // 容器用 scope
|
|
405
|
-
loop: {
|
|
406
|
-
items: '{{ data.items }}',
|
|
407
|
-
itemKey: 'item'
|
|
408
|
-
},
|
|
409
|
-
children: [
|
|
410
|
-
{
|
|
411
|
-
type: 'div',
|
|
412
|
-
model: { path: 'item.profile', scope: true }, // 相对循环项压栈,不绑定
|
|
413
|
-
children: [
|
|
414
|
-
{
|
|
415
|
-
type: 'input',
|
|
416
|
-
model: 'name' // 扁平路径 → data.items[0].profile.name
|
|
417
|
-
}
|
|
418
|
-
]
|
|
419
|
-
}
|
|
420
|
-
]
|
|
421
|
-
}
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
**modelPath 配置选项**
|
|
425
|
-
|
|
426
|
-
仅保留与格式相关的配置(如路径分隔符),路径解析与绑定行为由 model 写法决定,无需开关:
|
|
427
|
-
|
|
428
|
-
```typescript
|
|
429
|
-
const { state } = useVario(schema, {
|
|
430
|
-
modelPath: {
|
|
431
|
-
separator: '.' // 路径分隔符(默认 '.')
|
|
432
|
-
},
|
|
433
|
-
state: {}
|
|
434
|
-
})
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
**智能类型推断**
|
|
438
|
-
|
|
439
|
-
系统会根据路径结构自动推断并创建数组或对象:
|
|
440
|
-
|
|
441
|
-
- 使用 `[]` 语法或路径中包含数字索引 → 创建数组
|
|
442
|
-
- 使用 `.` 语法或字符串键 → 创建对象
|
|
443
|
-
- 默认情况下,纯数字段(如 `users.0.name`)会被视为数组索引
|
|
444
|
-
|
|
445
|
-
```typescript
|
|
446
|
-
// 自动创建数组
|
|
447
|
-
model: 'users[0].name' → state.users = []
|
|
448
|
-
|
|
449
|
-
// 自动创建对象
|
|
450
|
-
model: 'user.profile.name' → state.user = {}, state.user.profile = {}
|
|
451
|
-
|
|
452
|
-
// 混合结构
|
|
453
|
-
model: 'data.users[0].profile.tags[1]'
|
|
454
|
-
→ state.data = {}
|
|
455
|
-
→ state.data.users = []
|
|
456
|
-
→ state.data.users[0] = {}
|
|
457
|
-
→ state.data.users[0].profile = {}
|
|
458
|
-
→ state.data.users[0].profile.tags = []
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
**路径语法与 model 写法总结**
|
|
462
|
-
|
|
463
|
-
| 类型 | 示例 | 说明 | 自动创建 |
|
|
464
|
-
|------|------|------|---------|
|
|
465
|
-
| 扁平路径 | `model: "name"` | 单段路径,自动拼接父级路径栈 | ✅ 对象 |
|
|
466
|
-
| 明确路径 | `model: "form.user.name"` | 完整路径,直接使用 | ✅ 对象 |
|
|
467
|
-
| 当前栈路径 | `model: "."` | 绑定到当前路径栈(循环中即 `items[0]`、`items[1]` 等元素本身);栈非空时有效 | ✅ 按栈推断 |
|
|
468
|
-
| 作用域(不绑定) | `model: { path: "form", scope: true }` | 仅压栈,本节点不绑定;子节点扁平路径拼在 `path` 下 | — |
|
|
469
|
-
| 数组访问 `[]` | `model: "users[0].name"` | 明确数组索引访问 | ✅ 数组 |
|
|
470
|
-
| 动态索引 `[]` | `model: "users[].name"` | 循环中自动填充索引 | ✅ 数组 |
|
|
471
|
-
| 点语法数字 | `model: "users.0.name"` | 纯数字段视为数组索引 | ✅ 数组 |
|
|
472
|
-
| 混合嵌套 | `model: "data[0].items[1].value"` | 数组和对象混合 | ✅ 自动推断 |
|
|
473
|
-
|
|
474
|
-
**最佳实践**
|
|
475
|
-
|
|
476
|
-
1. **使用 `[]` 语法明确数组访问**:`users[0].name` 比 `users.0.name` 更清晰
|
|
477
|
-
2. **扁平路径用于层级结构**:在父级定义 `model` 后,子级使用扁平路径自动拼接
|
|
478
|
-
3. **明确路径用于跨层级访问**:需要访问非直接父级时使用完整路径
|
|
479
|
-
4. **循环中利用自动索引**:在循环中使用扁平路径,系统会自动添加索引
|
|
480
|
-
|
|
481
|
-
### 表达式系统
|
|
482
|
-
|
|
483
|
-
Schema 中支持表达式语法,使用 `{{ }}` 包裹:
|
|
484
|
-
|
|
485
|
-
```typescript
|
|
486
|
-
const schema: VueSchemaNode = {
|
|
487
|
-
type: 'div',
|
|
488
|
-
children: [
|
|
489
|
-
{
|
|
490
|
-
type: 'div',
|
|
491
|
-
children: '{{ firstName + " " + lastName }}' // 字符串拼接
|
|
492
|
-
},
|
|
493
|
-
{
|
|
494
|
-
type: 'div',
|
|
495
|
-
children: '{{ count * 2 }}' // 数学运算
|
|
496
|
-
},
|
|
497
|
-
{
|
|
498
|
-
type: 'div',
|
|
499
|
-
show: 'count > 10', // 条件显示
|
|
500
|
-
children: 'Count is greater than 10'
|
|
501
|
-
}
|
|
502
|
-
]
|
|
503
|
-
}
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
表达式会在 Vue 的响应式上下文中求值,自动追踪依赖。
|
|
507
|
-
|
|
508
|
-
## Vue 特性集成
|
|
509
|
-
|
|
510
|
-
### Ref 模板引用
|
|
511
|
-
|
|
512
|
-
在 Schema 中声明 `ref`,通过 `useVario` 返回的 `refs` 对象访问组件实例。
|
|
513
|
-
|
|
514
|
-
```typescript
|
|
515
|
-
const schema: VueSchemaNode = {
|
|
516
|
-
type: 'div',
|
|
517
|
-
children: [
|
|
518
|
-
{
|
|
519
|
-
type: 'ElInput',
|
|
520
|
-
ref: 'inputRef', // 声明 ref
|
|
521
|
-
props: {
|
|
522
|
-
modelValue: '{{ inputValue }}'
|
|
523
|
-
}
|
|
524
39
|
},
|
|
525
40
|
{
|
|
526
41
|
type: 'ElButton',
|
|
527
42
|
events: {
|
|
528
43
|
click: {
|
|
529
44
|
type: 'call',
|
|
530
|
-
method: '
|
|
45
|
+
method: 'handleClick'
|
|
531
46
|
}
|
|
532
47
|
},
|
|
533
|
-
children: '
|
|
534
|
-
}
|
|
535
|
-
]
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const { refs, methods } = useVario(schema, {
|
|
539
|
-
state: { inputValue: '' },
|
|
540
|
-
methods: {
|
|
541
|
-
focusInput: () => {
|
|
542
|
-
refs.inputRef.value?.focus() // ✅ 访问组件实例
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
})
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
### 生命周期钩子
|
|
549
|
-
|
|
550
|
-
在 Schema 中声明生命周期钩子方法名,精确控制每个组件的生命周期。
|
|
551
|
-
|
|
552
|
-
```typescript
|
|
553
|
-
const schema: VueSchemaNode = {
|
|
554
|
-
type: 'div',
|
|
555
|
-
onMounted: 'initData', // 挂载后
|
|
556
|
-
onUnmounted: 'cleanup', // 卸载前
|
|
557
|
-
onUpdated: 'onUpdate', // 更新后
|
|
558
|
-
onBeforeMount: 'beforeInit', // 挂载前
|
|
559
|
-
onBeforeUnmount: 'beforeCleanup', // 卸载前
|
|
560
|
-
onBeforeUpdate: 'beforeUpdate', // 更新前
|
|
561
|
-
children: 'Content'
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const { methods } = useVario(schema, {
|
|
565
|
-
state: {},
|
|
566
|
-
methods: {
|
|
567
|
-
initData: ({ state, ctx }) => {
|
|
568
|
-
console.log('组件已挂载')
|
|
569
|
-
// 初始化逻辑
|
|
570
|
-
},
|
|
571
|
-
cleanup: ({ state, ctx }) => {
|
|
572
|
-
console.log('组件即将卸载')
|
|
573
|
-
// 清理逻辑
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
})
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
**注意**:生命周期钩子映射到 Vue 原生 API,不是重新实现。
|
|
580
|
-
|
|
581
|
-
### Provide/Inject 依赖注入
|
|
582
|
-
|
|
583
|
-
在 Schema 中声明 `provide` 和 `inject`,实现组件间的数据传递。
|
|
584
|
-
|
|
585
|
-
```typescript
|
|
586
|
-
// 父组件 Schema
|
|
587
|
-
const parentSchema: VueSchemaNode = {
|
|
588
|
-
type: 'div',
|
|
589
|
-
provide: {
|
|
590
|
-
theme: 'dark',
|
|
591
|
-
locale: 'currentLocale', // 表达式:从状态读取
|
|
592
|
-
apiUrl: 'https://api.example.com' // 静态值
|
|
593
|
-
},
|
|
594
|
-
children: [
|
|
595
|
-
{
|
|
596
|
-
type: 'ChildComponent'
|
|
48
|
+
children: '点击'
|
|
597
49
|
}
|
|
598
50
|
]
|
|
599
51
|
}
|
|
600
52
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
// 或对象形式
|
|
607
|
-
// inject: {
|
|
608
|
-
// myTheme: 'theme',
|
|
609
|
-
// appLocale: { from: 'locale', default: 'en-US' }
|
|
610
|
-
// },
|
|
611
|
-
children: [
|
|
612
|
-
{
|
|
613
|
-
type: 'div',
|
|
614
|
-
children: 'Theme: {{ theme }}, Locale: {{ locale }}'
|
|
615
|
-
}
|
|
616
|
-
]
|
|
617
|
-
}
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
### Teleport 传送
|
|
621
|
-
|
|
622
|
-
将组件传送到指定的 DOM 节点。
|
|
623
|
-
|
|
624
|
-
```typescript
|
|
625
|
-
const schema: VueSchemaNode = {
|
|
626
|
-
type: 'div',
|
|
627
|
-
children: [
|
|
628
|
-
{
|
|
629
|
-
type: 'ElDialog',
|
|
630
|
-
teleport: 'body', // 传送到 body
|
|
631
|
-
props: {
|
|
632
|
-
modelValue: '{{ dialogVisible }}'
|
|
53
|
+
export default {
|
|
54
|
+
setup() {
|
|
55
|
+
const { vnode, state, methods } = useVario(schema, {
|
|
56
|
+
state: {
|
|
57
|
+
name: ''
|
|
633
58
|
},
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
```typescript
|
|
645
|
-
const schema: VueSchemaNode = {
|
|
646
|
-
type: 'div',
|
|
647
|
-
transition: 'fade', // 简单形式
|
|
648
|
-
// 或完整配置
|
|
649
|
-
// transition: {
|
|
650
|
-
// name: 'fade',
|
|
651
|
-
// appear: true,
|
|
652
|
-
// mode: 'out-in',
|
|
653
|
-
// duration: { enter: 300, leave: 200 }
|
|
654
|
-
// },
|
|
655
|
-
children: 'Content'
|
|
59
|
+
methods: {
|
|
60
|
+
handleClick: ({ state, ctx }) => {
|
|
61
|
+
console.log('Clicked', state.name)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return { vnode, state }
|
|
67
|
+
}
|
|
656
68
|
}
|
|
657
69
|
```
|
|
658
70
|
|
|
659
|
-
|
|
71
|
+
## 核心特性
|
|
660
72
|
|
|
661
|
-
|
|
73
|
+
### 双向绑定
|
|
662
74
|
|
|
663
75
|
```typescript
|
|
664
|
-
|
|
665
|
-
type: '
|
|
666
|
-
|
|
667
|
-
// 或完整配置
|
|
668
|
-
// keepAlive: {
|
|
669
|
-
// include: 'ComponentA',
|
|
670
|
-
// exclude: 'ComponentB',
|
|
671
|
-
// max: 10
|
|
672
|
-
// },
|
|
673
|
-
children: 'Content'
|
|
76
|
+
{
|
|
77
|
+
type: 'ElInput',
|
|
78
|
+
model: 'user.name' // 自动创建响应式绑定
|
|
674
79
|
}
|
|
675
80
|
```
|
|
676
81
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
**重要**:`computed` 和 `watch` **不在 Schema 中定义**,应该在 Vue 组件中使用原生 API 定义。
|
|
680
|
-
|
|
681
|
-
### Computed 计算属性
|
|
82
|
+
### 表达式
|
|
682
83
|
|
|
683
84
|
```typescript
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const schema: VueSchemaNode = {
|
|
85
|
+
{
|
|
687
86
|
type: 'div',
|
|
688
|
-
children:
|
|
689
|
-
|
|
690
|
-
type: 'div',
|
|
691
|
-
children: '{{ fullName }}' // 使用 computed
|
|
692
|
-
}
|
|
693
|
-
]
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const state = reactive({
|
|
697
|
-
firstName: 'John',
|
|
698
|
-
lastName: 'Doe'
|
|
699
|
-
})
|
|
700
|
-
|
|
701
|
-
// ✅ 使用 Vue 原生 computed
|
|
702
|
-
const fullName = computed(() => state.firstName + ' ' + state.lastName)
|
|
703
|
-
|
|
704
|
-
const { vnode } = useVario(schema, {
|
|
705
|
-
state,
|
|
706
|
-
computed: {
|
|
707
|
-
fullName // 传入 Vue ComputedRef
|
|
708
|
-
}
|
|
709
|
-
})
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
### Watch 监听器
|
|
713
|
-
|
|
714
|
-
```typescript
|
|
715
|
-
import { watch } from 'vue'
|
|
716
|
-
|
|
717
|
-
const state = reactive({
|
|
718
|
-
count: 0,
|
|
719
|
-
user: { name: 'John' }
|
|
720
|
-
})
|
|
721
|
-
|
|
722
|
-
// ✅ 使用 Vue 原生 watch
|
|
723
|
-
watch(() => state.count, (newVal, oldVal) => {
|
|
724
|
-
console.log('Count changed:', newVal, '->', oldVal)
|
|
725
|
-
})
|
|
726
|
-
|
|
727
|
-
watch(() => state.user.name, (newVal) => {
|
|
728
|
-
console.log('User name changed:', newVal)
|
|
729
|
-
}, { immediate: true, deep: true })
|
|
730
|
-
|
|
731
|
-
const { vnode } = useVario(schema, {
|
|
732
|
-
state
|
|
733
|
-
})
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
**为什么不在 Schema 中定义?**
|
|
737
|
-
|
|
738
|
-
- 避免重新实现 Vue 的响应式系统
|
|
739
|
-
- 利用 Vue 的编译优化
|
|
740
|
-
- 保持与 Vue 生态的一致性
|
|
741
|
-
- 更好的类型推导和 IDE 支持
|
|
742
|
-
|
|
743
|
-
## 控制流
|
|
744
|
-
|
|
745
|
-
### 条件渲染
|
|
746
|
-
|
|
747
|
-
```typescript
|
|
748
|
-
const schema: VueSchemaNode = {
|
|
749
|
-
type: 'div',
|
|
750
|
-
cond: 'isVisible', // 条件渲染(v-if)
|
|
751
|
-
// 或
|
|
752
|
-
show: 'isVisible', // 条件显示(v-show)
|
|
753
|
-
children: 'Content'
|
|
87
|
+
children: '{{ firstName + " " + lastName }}',
|
|
88
|
+
show: 'count > 10'
|
|
754
89
|
}
|
|
755
90
|
```
|
|
756
91
|
|
|
757
92
|
### 循环渲染
|
|
758
93
|
|
|
759
94
|
```typescript
|
|
760
|
-
|
|
95
|
+
{
|
|
761
96
|
type: 'div',
|
|
762
97
|
loop: {
|
|
763
|
-
items: '{{ userList }}',
|
|
764
|
-
itemKey: 'item'
|
|
765
|
-
indexKey: 'index' // 索引变量名(可选)
|
|
766
|
-
},
|
|
767
|
-
children: [
|
|
768
|
-
{
|
|
769
|
-
type: 'div',
|
|
770
|
-
children: '{{ index + 1 }}. {{ item.name }}'
|
|
771
|
-
}
|
|
772
|
-
]
|
|
773
|
-
}
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
## 事件处理
|
|
777
|
-
|
|
778
|
-
```typescript
|
|
779
|
-
const schema: VueSchemaNode = {
|
|
780
|
-
type: 'ElButton',
|
|
781
|
-
events: {
|
|
782
|
-
click: {
|
|
783
|
-
type: 'call',
|
|
784
|
-
method: 'handleClick'
|
|
785
|
-
}
|
|
98
|
+
items: '{{ userList }}',
|
|
99
|
+
itemKey: 'item'
|
|
786
100
|
},
|
|
787
|
-
children: '
|
|
101
|
+
children: '{{ item.name }}'
|
|
788
102
|
}
|
|
789
|
-
|
|
790
|
-
const { methods } = useVario(schema, {
|
|
791
|
-
state: {},
|
|
792
|
-
methods: {
|
|
793
|
-
handleClick: ({ state, ctx, event }) => {
|
|
794
|
-
console.log('Button clicked', event)
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
})
|
|
798
103
|
```
|
|
799
104
|
|
|
800
|
-
|
|
105
|
+
### Vue 特性
|
|
801
106
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
schema: Schema<TState> | (() => Schema<TState>) | ComputedRef<Schema<TState>>,
|
|
807
|
-
options?: UseVarioOptions<TState>
|
|
808
|
-
): UseVarioResult<TState>
|
|
809
|
-
```
|
|
810
|
-
|
|
811
|
-
#### Options
|
|
812
|
-
|
|
813
|
-
```typescript
|
|
814
|
-
interface UseVarioOptions<TState> {
|
|
815
|
-
/** 初始状态(会自动包裹为响应式对象) */
|
|
816
|
-
state?: TState
|
|
817
|
-
|
|
818
|
-
/** 计算属性(传入 Vue ComputedRef) */
|
|
819
|
-
computed?: Record<string, ((state: TState) => any) | ComputedRef<any>>
|
|
820
|
-
|
|
821
|
-
/** 方法定义 */
|
|
822
|
-
methods?: Record<string, (context: MethodContext<TState>) => any>
|
|
823
|
-
|
|
824
|
-
/** 表达式选项 */
|
|
825
|
-
exprOptions?: ExpressionOptions
|
|
826
|
-
|
|
827
|
-
/** 自定义 model 绑定配置 */
|
|
828
|
-
modelBindings?: Record<string, ModelConfig>
|
|
829
|
-
|
|
830
|
-
/** Model 路径解析配置(可选,仅与格式相关如 separator) */
|
|
831
|
-
modelPath?: {
|
|
832
|
-
/** 路径分隔符(默认 '.') */
|
|
833
|
-
separator?: string
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
/** 渲染器选项 */
|
|
837
|
-
rendererOptions?: VueRendererOptions
|
|
838
|
-
|
|
839
|
-
/** 错误边界配置 */
|
|
840
|
-
errorBoundary?: {
|
|
841
|
-
enabled?: boolean
|
|
842
|
-
fallback?: (error: Error) => VNode
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
/** 事件发射器 */
|
|
846
|
-
onEmit?: (event: string, data?: unknown) => void
|
|
847
|
-
}
|
|
848
|
-
```
|
|
849
|
-
|
|
850
|
-
#### 返回值
|
|
851
|
-
|
|
852
|
-
```typescript
|
|
853
|
-
interface UseVarioResult<TState> {
|
|
854
|
-
/** 渲染的 VNode */
|
|
855
|
-
vnode: Ref<VNode | null>
|
|
856
|
-
|
|
857
|
-
/** 响应式状态(自动包裹) */
|
|
858
|
-
state: TState
|
|
859
|
-
|
|
860
|
-
/** 运行时上下文 */
|
|
861
|
-
ctx: Ref<RuntimeContext<TState>>
|
|
862
|
-
|
|
863
|
-
/** 模板引用集合 */
|
|
864
|
-
refs: Record<string, Ref<any>>
|
|
865
|
-
|
|
866
|
-
/** 当前错误(如果有) */
|
|
867
|
-
error: Ref<Error | null>
|
|
868
|
-
|
|
869
|
-
/** 手动触发重新渲染 */
|
|
870
|
-
retry: () => void
|
|
871
|
-
}
|
|
872
|
-
```
|
|
873
|
-
|
|
874
|
-
## 性能优化
|
|
875
|
-
|
|
876
|
-
### 大规模渲染
|
|
877
|
-
|
|
878
|
-
- **组件解析缓存**:相同类型的组件只解析一次
|
|
879
|
-
- **静态属性缓存**:静态属性会被缓存,避免重复计算
|
|
880
|
-
- **事件处理器缓存**:事件处理器会被缓存,避免重复创建
|
|
881
|
-
- **循环上下文池**:复用循环上下文对象,减少内存分配
|
|
882
|
-
|
|
883
|
-
### 响应式优化
|
|
884
|
-
|
|
885
|
-
- **深度比较**:状态同步时进行深度比较,避免不必要的更新
|
|
886
|
-
- **路径追踪**:使用路径追踪避免循环同步
|
|
887
|
-
- **批量更新**:使用 `nextTick` 批量处理状态变化
|
|
888
|
-
|
|
889
|
-
### 最佳实践
|
|
890
|
-
|
|
891
|
-
1. **使用虚拟滚动**:对于大型列表(> 1000 项),使用虚拟滚动组件
|
|
892
|
-
2. **静态节点标记**:对于不会变化的节点,考虑使用静态提升
|
|
893
|
-
3. **合理使用 computed**:在组件层面定义 computed,利用 Vue 的缓存机制
|
|
894
|
-
4. **避免深层嵌套**:虽然支持深层嵌套,但建议控制在 3-4 层以内
|
|
895
|
-
|
|
896
|
-
## 类型支持
|
|
897
|
-
|
|
898
|
-
```typescript
|
|
899
|
-
import type { VueSchemaNode } from '@vario/vue'
|
|
900
|
-
|
|
901
|
-
// VueSchemaNode 扩展了 SchemaNode,添加了 Vue 特有属性
|
|
902
|
-
const schema: VueSchemaNode = {
|
|
903
|
-
type: 'div',
|
|
904
|
-
ref: 'container',
|
|
905
|
-
onMounted: 'init',
|
|
906
|
-
// ... 其他属性
|
|
907
|
-
}
|
|
908
|
-
```
|
|
107
|
+
- **Ref 模板引用**:`ref: 'inputRef'`
|
|
108
|
+
- **生命周期**:`onMounted: 'initData'`
|
|
109
|
+
- **Provide/Inject**:`provide: { theme: 'dark' }`
|
|
110
|
+
- **Teleport**:`teleport: 'body'`
|
|
909
111
|
|
|
910
|
-
##
|
|
112
|
+
## 优势
|
|
911
113
|
|
|
912
|
-
|
|
114
|
+
- ✅ **类型推导**:完整的 TypeScript 类型支持
|
|
115
|
+
- ✅ **性能优化**:组件解析缓存、静态属性缓存、循环上下文池
|
|
116
|
+
- ✅ **自动同步**:状态与运行时上下文双向同步
|
|
117
|
+
- ✅ **Vue 原生**:computed、watch 使用 Vue 原生 API
|
|
913
118
|
|
|
914
119
|
## 许可证
|
|
915
120
|
|