@variojs/vue 0.0.1 → 0.0.2
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.js +1 -199
- package/dist/composable.js +1 -386
- package/dist/features/attrs-builder.js +1 -143
- 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.js +1 -171
- 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/dist/bindings.js
CHANGED
|
@@ -1,199 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Bidirectional binding handlers for Vue integration
|
|
3
|
-
*
|
|
4
|
-
* 功能:
|
|
5
|
-
* - v-model 双向绑定处理
|
|
6
|
-
* - 智能检测组件类型(原生元素 vs Vue 组件)
|
|
7
|
-
* - 自动适配不同的 model 协议(Vue 3 标准 vs 传统协议)
|
|
8
|
-
* - 支持自定义配置(可选)
|
|
9
|
-
*
|
|
10
|
-
* 设计原则:
|
|
11
|
-
* - 使用 vario-core 的路径工具,避免重复代码
|
|
12
|
-
* - 优先自动检测,减少配置需求
|
|
13
|
-
* - 支持 Vue 3 和传统协议
|
|
14
|
-
*/
|
|
15
|
-
import { resolveComponent } from 'vue';
|
|
16
|
-
import { getPathValue } from '@variojs/core';
|
|
17
|
-
/**
|
|
18
|
-
* 自定义配置注册表
|
|
19
|
-
*/
|
|
20
|
-
const customConfigs = new Map();
|
|
21
|
-
/**
|
|
22
|
-
* 原生表单元素
|
|
23
|
-
*/
|
|
24
|
-
const NATIVE_FORM_ELEMENTS = new Set(['input', 'textarea', 'select']);
|
|
25
|
-
/**
|
|
26
|
-
* 原生元素的事件映射
|
|
27
|
-
*/
|
|
28
|
-
const NATIVE_EVENT_MAP = {
|
|
29
|
-
input: 'input',
|
|
30
|
-
textarea: 'input',
|
|
31
|
-
select: 'change'
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Vue 3 标准 model 配置
|
|
35
|
-
*/
|
|
36
|
-
const VUE3_DEFAULT_CONFIG = {
|
|
37
|
-
prop: 'modelValue',
|
|
38
|
-
event: 'update:modelValue'
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* 获取组件的 model 配置
|
|
42
|
-
*/
|
|
43
|
-
function getModelConfig(componentType, component) {
|
|
44
|
-
// 1. 自定义配置优先
|
|
45
|
-
const custom = customConfigs.get(componentType);
|
|
46
|
-
if (custom)
|
|
47
|
-
return custom;
|
|
48
|
-
// 2. 原生表单元素
|
|
49
|
-
const lowerType = componentType.toLowerCase();
|
|
50
|
-
if (NATIVE_FORM_ELEMENTS.has(lowerType)) {
|
|
51
|
-
return {
|
|
52
|
-
prop: 'value',
|
|
53
|
-
event: NATIVE_EVENT_MAP[lowerType] || 'input'
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
// 3. 尝试从组件定义检测
|
|
57
|
-
if (component && typeof component === 'object') {
|
|
58
|
-
const comp = component;
|
|
59
|
-
const props = (comp.props || comp.__props || {});
|
|
60
|
-
// 检查 modelValue(Vue 3 标准)
|
|
61
|
-
if ('modelValue' in props || comp.__vModel) {
|
|
62
|
-
return VUE3_DEFAULT_CONFIG;
|
|
63
|
-
}
|
|
64
|
-
// 检查 value(传统协议)
|
|
65
|
-
if ('value' in props) {
|
|
66
|
-
const emits = (comp.emits || comp.__emits || []);
|
|
67
|
-
return {
|
|
68
|
-
prop: 'value',
|
|
69
|
-
event: emits.includes('update:value') ? 'update:value' : 'input'
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// 4. 默认 Vue 3 标准
|
|
74
|
-
return VUE3_DEFAULT_CONFIG;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* 转换事件名为 Vue 事件处理器格式
|
|
78
|
-
*/
|
|
79
|
-
function toEventHandlerName(event) {
|
|
80
|
-
if (event.startsWith('update:')) {
|
|
81
|
-
return `onUpdate:${event.slice(7)}`;
|
|
82
|
-
}
|
|
83
|
-
return `on${event.charAt(0).toUpperCase()}${event.slice(1)}`;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* 获取默认值(当值为 undefined 时)
|
|
87
|
-
* 确保双向绑定能够正常工作,即使状态中还没有这个字段
|
|
88
|
-
*/
|
|
89
|
-
function getDefaultValue(prop) {
|
|
90
|
-
// 对于输入框相关的 prop,默认为空字符串
|
|
91
|
-
if (prop === 'value' || prop === 'modelValue') {
|
|
92
|
-
return '';
|
|
93
|
-
}
|
|
94
|
-
// 对于复选框相关的 prop,默认为 false
|
|
95
|
-
if (prop === 'checked') {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
// 其他情况返回 undefined,让组件自己处理
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* 创建双向绑定配置
|
|
103
|
-
*
|
|
104
|
-
* @param componentType 组件类型名
|
|
105
|
-
* @param modelPath 模型路径(如 "user.name")
|
|
106
|
-
* @param ctx 运行时上下文
|
|
107
|
-
* @param component 组件对象(可选,用于自动检测)
|
|
108
|
-
* @param getState 获取响应式状态的函数(用于 Vue 响应式追踪)
|
|
109
|
-
* @param modelName 具名 model(可选,如 "checked", "value")
|
|
110
|
-
* @returns 包含 prop 和 event handler 的对象
|
|
111
|
-
*/
|
|
112
|
-
export function createModelBinding(componentType, modelPath, ctx, component, getState, modelName) {
|
|
113
|
-
// 尝试解析组件(如果未提供或者是字符串)
|
|
114
|
-
let resolvedComponent = component;
|
|
115
|
-
if (!resolvedComponent || typeof resolvedComponent === 'string') {
|
|
116
|
-
try {
|
|
117
|
-
const resolved = resolveComponent(componentType);
|
|
118
|
-
if (resolved && typeof resolved !== 'string') {
|
|
119
|
-
resolvedComponent = resolved;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
// 解析失败,使用默认配置
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// 获取配置(支持具名 model)
|
|
127
|
-
const config = modelName
|
|
128
|
-
? getNamedModelConfig(componentType, resolvedComponent, modelName)
|
|
129
|
-
: getModelConfig(componentType, resolvedComponent);
|
|
130
|
-
// 获取当前值
|
|
131
|
-
let value = getState
|
|
132
|
-
? getPathValue(getState(), modelPath)
|
|
133
|
-
: ctx._get(modelPath);
|
|
134
|
-
// 如果值是 undefined,自动初始化为合适的默认值
|
|
135
|
-
// 这确保了双向绑定能够正常工作,即使状态中还没有这个字段
|
|
136
|
-
if (value === undefined) {
|
|
137
|
-
const defaultValue = getDefaultValue(config.prop);
|
|
138
|
-
if (defaultValue !== undefined) {
|
|
139
|
-
// 立即初始化到状态中,确保响应式绑定正常工作
|
|
140
|
-
value = defaultValue;
|
|
141
|
-
// 调用 _set 初始化值,会触发 onStateChange 同步到 Vue 状态
|
|
142
|
-
ctx._set(modelPath, defaultValue);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
[config.prop]: value,
|
|
147
|
-
[toEventHandlerName(config.event)]: (newValue) => {
|
|
148
|
-
// 直接设置到运行时上下文,会触发 onStateChange 同步到 Vue 状态
|
|
149
|
-
ctx._set(modelPath, newValue);
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* 获取具名 model 配置(支持多 model)
|
|
155
|
-
*/
|
|
156
|
-
function getNamedModelConfig(componentType, component, modelName) {
|
|
157
|
-
// 1. 检查自定义配置
|
|
158
|
-
const customKey = `${componentType}:${modelName}`;
|
|
159
|
-
const custom = customConfigs.get(customKey);
|
|
160
|
-
if (custom)
|
|
161
|
-
return custom;
|
|
162
|
-
// 2. 尝试从组件定义检测
|
|
163
|
-
if (component && typeof component === 'object') {
|
|
164
|
-
const comp = component;
|
|
165
|
-
const props = (comp.props || {});
|
|
166
|
-
const emits = (comp.emits || []);
|
|
167
|
-
// Vue 3.4+ 多 model 支持
|
|
168
|
-
if (modelName in props) {
|
|
169
|
-
return {
|
|
170
|
-
prop: modelName,
|
|
171
|
-
event: emits.includes(`update:${modelName}`)
|
|
172
|
-
? `update:${modelName}`
|
|
173
|
-
: `update:${modelName}`
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
// 3. 默认规则:prop 为 modelName,event 为 update:modelName
|
|
178
|
-
return {
|
|
179
|
-
prop: modelName,
|
|
180
|
-
event: `update:${modelName}`
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* 注册自定义组件的 model 配置
|
|
185
|
-
* @param componentType 组件类型名
|
|
186
|
-
* @param config model 配置
|
|
187
|
-
* @param modelName 具名 model(可选)
|
|
188
|
-
*/
|
|
189
|
-
export function registerModelConfig(componentType, config, modelName) {
|
|
190
|
-
const key = modelName ? `${componentType}:${modelName}` : componentType;
|
|
191
|
-
customConfigs.set(key, config);
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* 清除自定义配置
|
|
195
|
-
*/
|
|
196
|
-
export function clearModelConfigs() {
|
|
197
|
-
customConfigs.clear();
|
|
198
|
-
}
|
|
199
|
-
//# sourceMappingURL=bindings.js.map
|
|
1
|
+
import{resolveComponent as l}from"vue";import{getPathValue as d}from"@variojs/core";const p=new Map,g=new Set(["input","textarea","select"]),v={input:"input",textarea:"input",select:"change"},a={prop:"modelValue",event:"update:modelValue"};function _(t,n){const e=p.get(t);if(e)return e;const i=t.toLowerCase();if(g.has(i))return{prop:"value",event:v[i]||"input"};if(n&&typeof n=="object"){const r=n,o=r.props||r.__props||{};if("modelValue"in o||r.__vModel)return a;if("value"in o)return{prop:"value",event:(r.emits||r.__emits||[]).includes("update:value")?"update:value":"input"}}return a}function V(t){return t.startsWith("update:")?`onUpdate:${t.slice(7)}`:`on${t.charAt(0).toUpperCase()}${t.slice(1)}`}function $(t){if(t==="value"||t==="modelValue")return"";if(t==="checked")return!1}function A(t,n,e,i,r,o){let s=i;if(!s||typeof s=="string")try{const u=l(t);u&&typeof u!="string"&&(s=u)}catch{}const f=o?C(t,s,o):_(t,s);let c=r?d(r(),n):e._get(n);if(c===void 0){const u=$(f.prop);u!==void 0&&(c=u,e._set(n,u))}return{[f.prop]:c,[V(f.event)]:u=>{e._set(n,u)}}}function C(t,n,e){const i=`${t}:${e}`,r=p.get(i);if(r)return r;if(n&&typeof n=="object"){const o=n,s=o.props||{},f=o.emits||[];if(e in s)return{prop:e,event:f.includes(`update:${e}`)?`update:${e}`:`update:${e}`}}return{prop:e,event:`update:${e}`}}function h(t,n,e){const i=e?`${t}:${e}`:t;p.set(i,n)}function w(){p.clear()}export{w as clearModelConfigs,A as createModelBinding,h as registerModelConfig};
|
package/dist/composable.js
CHANGED
|
@@ -1,386 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* useVario composable for Vue integration
|
|
3
|
-
*
|
|
4
|
-
* 设计目标:
|
|
5
|
-
* - Options 风格(state/computed/methods)零学习成本
|
|
6
|
-
* - Composition 风格透传 reactive/computed
|
|
7
|
-
* - 状态直接可访问(无需 .value)
|
|
8
|
-
* - 方法统一通过 $methods 调用
|
|
9
|
-
* - 生命周期:在使用 useVario 的组件中直接使用 Vue 的生命周期钩子
|
|
10
|
-
* (如 onMounted, onUnmounted 等),无需通过 options 传递
|
|
11
|
-
*
|
|
12
|
-
* 性能优化:
|
|
13
|
-
* - 1000+ 组件场景:避免在每个 useVario 中注册生命周期钩子
|
|
14
|
-
* - 生命周期钩子应在使用 useVario 的组件中直接使用,减少抽象层开销
|
|
15
|
-
*/
|
|
16
|
-
import { ref, reactive, computed, watch, nextTick, getCurrentInstance, h, isReactive } from 'vue';
|
|
17
|
-
import { createRuntimeContext, invalidateCache, setPathValue, ServiceError } from '@variojs/core';
|
|
18
|
-
import { VueRenderer } from './renderer.js';
|
|
19
|
-
import { registerModelConfig } from './bindings.js';
|
|
20
|
-
import { RefsRegistry } from './features/refs.js';
|
|
21
|
-
// 实现:使用箭头函数
|
|
22
|
-
export const useVario = (schema, options = {}) => {
|
|
23
|
-
const schemaRef = resolveSchema(schema);
|
|
24
|
-
// 获取组件实例(如果可用)
|
|
25
|
-
const instance = getCurrentInstance();
|
|
26
|
-
// 状态:自动包裹为响应式对象
|
|
27
|
-
// 如果传入的 state 已经是响应式的(通过 isReactive 检查),直接使用
|
|
28
|
-
// 否则用 reactive() 包裹
|
|
29
|
-
const reactiveState = (options.state
|
|
30
|
-
? (isReactive(options.state) ? options.state : reactive(options.state))
|
|
31
|
-
: reactive({}));
|
|
32
|
-
// 应用自定义绑定协议(可选)
|
|
33
|
-
applyBindingConfigs(options.modelBindings);
|
|
34
|
-
// 构建方法注册表(支持兼容命名)
|
|
35
|
-
const methodsRegistry = buildMethodsRegistry(options.methods, reactiveState);
|
|
36
|
-
// 同步锁,防止双向同步死循环
|
|
37
|
-
let syncing = false;
|
|
38
|
-
// 同步路径追踪,用于循环检测
|
|
39
|
-
const syncingPaths = new Set();
|
|
40
|
-
// 延迟引用 render 函数(因为 render 在 ctx 之后定义)
|
|
41
|
-
let renderFn = null;
|
|
42
|
-
const ctx = createRuntimeContext({}, {
|
|
43
|
-
onEmit: (event, data) => {
|
|
44
|
-
options.onEmit?.(event, data);
|
|
45
|
-
},
|
|
46
|
-
methods: methodsRegistry,
|
|
47
|
-
exprOptions: options.exprOptions,
|
|
48
|
-
onStateChange: ((path, value, runtimeCtx) => {
|
|
49
|
-
// 如果正在同步,跳过(防止循环)
|
|
50
|
-
if (syncing)
|
|
51
|
-
return;
|
|
52
|
-
// 循环检测:如果当前路径正在同步,跳过
|
|
53
|
-
if (syncingPaths.has(path))
|
|
54
|
-
return;
|
|
55
|
-
syncing = true;
|
|
56
|
-
syncingPaths.add(path);
|
|
57
|
-
try {
|
|
58
|
-
// 同步到 Vue 响应式状态(自动创建缺失的对象结构)
|
|
59
|
-
setPathValue(reactiveState, path, value, {
|
|
60
|
-
createObject: () => reactive({}),
|
|
61
|
-
createArray: () => reactive([]),
|
|
62
|
-
// 自动创建中间对象,确保路径存在
|
|
63
|
-
createIntermediate: true
|
|
64
|
-
});
|
|
65
|
-
// 缓存失效
|
|
66
|
-
invalidateCache(path, runtimeCtx);
|
|
67
|
-
// 触发重新渲染(确保 UI 及时更新)
|
|
68
|
-
if (renderFn)
|
|
69
|
-
nextTick(renderFn);
|
|
70
|
-
}
|
|
71
|
-
finally {
|
|
72
|
-
syncingPaths.delete(path);
|
|
73
|
-
syncing = false;
|
|
74
|
-
}
|
|
75
|
-
}),
|
|
76
|
-
createObject: () => reactive({}),
|
|
77
|
-
createArray: () => reactive([])
|
|
78
|
-
});
|
|
79
|
-
// 将初始状态写入 ctx(保持 runtime 与 Vue 一致)
|
|
80
|
-
// 注意:需要在 watch 之前初始化,避免触发不必要的 watch
|
|
81
|
-
// 遍历 reactiveState 的所有属性并同步到 ctx
|
|
82
|
-
// 同时,如果 state 为空,会自动在 onStateChange 中创建结构
|
|
83
|
-
for (const key in reactiveState) {
|
|
84
|
-
if (key.startsWith('$') || key.startsWith('_'))
|
|
85
|
-
continue;
|
|
86
|
-
const value = reactiveState[key];
|
|
87
|
-
ctx._set(key, value, { skipCallback: true });
|
|
88
|
-
}
|
|
89
|
-
// 计算属性(Options 风格)
|
|
90
|
-
if (options.computed) {
|
|
91
|
-
registerComputed(options.computed, reactiveState, ctx);
|
|
92
|
-
}
|
|
93
|
-
const ctxRef = ref(ctx);
|
|
94
|
-
// 规范化 modelPath 配置
|
|
95
|
-
const modelPathConfig = normalizeModelPathConfig(options.modelPath);
|
|
96
|
-
// 创建 refs 注册表(用于模板引用)
|
|
97
|
-
const refsRegistry = options.rendererOptions?.refsRegistry || new RefsRegistry();
|
|
98
|
-
const renderer = new VueRenderer({
|
|
99
|
-
...options.rendererOptions,
|
|
100
|
-
instance,
|
|
101
|
-
app: options.app ?? options.rendererOptions?.app,
|
|
102
|
-
components: options.components ?? options.rendererOptions?.components,
|
|
103
|
-
getState: () => reactiveState,
|
|
104
|
-
refsRegistry,
|
|
105
|
-
modelPath: modelPathConfig
|
|
106
|
-
});
|
|
107
|
-
const vnodeRef = ref(null);
|
|
108
|
-
const errorRef = ref(null);
|
|
109
|
-
const errorBoundaryEnabled = options.errorBoundary?.enabled !== false;
|
|
110
|
-
const render = () => {
|
|
111
|
-
// 清除之前的错误
|
|
112
|
-
if (errorRef.value && errorBoundaryEnabled) {
|
|
113
|
-
errorRef.value = null;
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
const currentSchema = schemaRef.value;
|
|
117
|
-
if (!isValidSchema(currentSchema)) {
|
|
118
|
-
vnodeRef.value = null;
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
vnodeRef.value = renderer.render(currentSchema, ctx);
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
125
|
-
// 错误边界处理
|
|
126
|
-
if (errorBoundaryEnabled) {
|
|
127
|
-
errorRef.value = err;
|
|
128
|
-
// 调用错误恢复回调
|
|
129
|
-
if (options.errorBoundary?.onRecover) {
|
|
130
|
-
try {
|
|
131
|
-
options.errorBoundary.onRecover(err);
|
|
132
|
-
}
|
|
133
|
-
catch (recoverError) {
|
|
134
|
-
console.warn('Error recovery callback failed:', recoverError);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// 显示错误UI
|
|
138
|
-
if (options.errorBoundary?.fallback) {
|
|
139
|
-
try {
|
|
140
|
-
vnodeRef.value = options.errorBoundary.fallback(err);
|
|
141
|
-
}
|
|
142
|
-
catch (fallbackError) {
|
|
143
|
-
// 回退到默认错误显示
|
|
144
|
-
vnodeRef.value = createDefaultErrorVNode(err);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
vnodeRef.value = createDefaultErrorVNode(err);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
// 错误边界未启用,使用原有逻辑
|
|
153
|
-
vnodeRef.value = null;
|
|
154
|
-
options.onError?.(err);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
/**
|
|
159
|
-
* 创建默认错误显示VNode
|
|
160
|
-
*/
|
|
161
|
-
function createDefaultErrorVNode(error) {
|
|
162
|
-
return h('div', {
|
|
163
|
-
style: {
|
|
164
|
-
padding: '20px',
|
|
165
|
-
border: '2px solid #f56565',
|
|
166
|
-
borderRadius: '4px',
|
|
167
|
-
backgroundColor: '#fff5f5',
|
|
168
|
-
color: '#c53030'
|
|
169
|
-
}
|
|
170
|
-
}, [
|
|
171
|
-
h('div', { style: { fontWeight: 'bold', marginBottom: '10px' } }, '渲染错误'),
|
|
172
|
-
h('div', { style: { marginBottom: '10px' } }, error.message),
|
|
173
|
-
h('button', {
|
|
174
|
-
onClick: () => {
|
|
175
|
-
errorRef.value = null;
|
|
176
|
-
render();
|
|
177
|
-
},
|
|
178
|
-
style: {
|
|
179
|
-
padding: '8px 16px',
|
|
180
|
-
backgroundColor: '#4299e1',
|
|
181
|
-
color: 'white',
|
|
182
|
-
border: 'none',
|
|
183
|
-
borderRadius: '4px',
|
|
184
|
-
cursor: 'pointer'
|
|
185
|
-
}
|
|
186
|
-
}, '重试')
|
|
187
|
-
]);
|
|
188
|
-
}
|
|
189
|
-
// 设置 renderFn 引用,供 onStateChange 使用
|
|
190
|
-
renderFn = render;
|
|
191
|
-
render();
|
|
192
|
-
// schema 变化重新渲染
|
|
193
|
-
watch(schemaRef, () => nextTick(render), { deep: true, immediate: false });
|
|
194
|
-
// 状态变更同步到 ctx(Options/Composition 都生效)
|
|
195
|
-
// 使用单独的 watchSyncing 标志,避免与 ctx._set 触发的 onStateChange 冲突
|
|
196
|
-
let watchSyncing = false;
|
|
197
|
-
watch(reactiveState, () => {
|
|
198
|
-
// 如果正在同步(watch 自身触发),跳过
|
|
199
|
-
if (watchSyncing)
|
|
200
|
-
return;
|
|
201
|
-
watchSyncing = true;
|
|
202
|
-
try {
|
|
203
|
-
syncStateToContext(reactiveState, ctx, { skipCallback: true });
|
|
204
|
-
// 清除表达式缓存(重要:当数组/对象内部变化时,引用不变但内容变了,需要重新求值)
|
|
205
|
-
// 遍历所有顶层属性,使其缓存失效
|
|
206
|
-
for (const key in reactiveState) {
|
|
207
|
-
if (key.startsWith('$') || key.startsWith('_'))
|
|
208
|
-
continue;
|
|
209
|
-
invalidateCache(key, ctx);
|
|
210
|
-
}
|
|
211
|
-
nextTick(render);
|
|
212
|
-
}
|
|
213
|
-
finally {
|
|
214
|
-
watchSyncing = false;
|
|
215
|
-
}
|
|
216
|
-
}, { deep: true, flush: 'post', immediate: false });
|
|
217
|
-
return {
|
|
218
|
-
vnode: vnodeRef,
|
|
219
|
-
state: reactiveState,
|
|
220
|
-
ctx: ctxRef,
|
|
221
|
-
refs: refsRegistry.getAll(),
|
|
222
|
-
error: errorRef,
|
|
223
|
-
/** 手动触发重新渲染(用于错误恢复) */
|
|
224
|
-
retry: () => {
|
|
225
|
-
errorRef.value = null;
|
|
226
|
-
render();
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
};
|
|
230
|
-
// ============================================================================
|
|
231
|
-
// 内部工具函数
|
|
232
|
-
// ============================================================================
|
|
233
|
-
function resolveSchema(schema) {
|
|
234
|
-
if (typeof schema === 'function') {
|
|
235
|
-
return computed(() => {
|
|
236
|
-
const result = schema();
|
|
237
|
-
if (result && typeof result === 'object' && 'value' in result && 'effect' in result) {
|
|
238
|
-
return result.value;
|
|
239
|
-
}
|
|
240
|
-
return result;
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
if (schema && typeof schema === 'object' && 'value' in schema && 'effect' in schema) {
|
|
244
|
-
return schema;
|
|
245
|
-
}
|
|
246
|
-
return computed(() => schema);
|
|
247
|
-
}
|
|
248
|
-
function isValidSchema(schema) {
|
|
249
|
-
return schema != null && typeof schema === 'object' && 'type' in schema;
|
|
250
|
-
}
|
|
251
|
-
function buildMethodsRegistry(methods, reactiveState) {
|
|
252
|
-
if (!methods)
|
|
253
|
-
return {};
|
|
254
|
-
const registry = {};
|
|
255
|
-
for (const [name, fn] of Object.entries(methods)) {
|
|
256
|
-
const handler = async (ctx, params) => {
|
|
257
|
-
try {
|
|
258
|
-
// 调用方法,自动处理同步/异步
|
|
259
|
-
// ctx 的类型是 RuntimeContext,但 MethodContext 需要 RuntimeContext<TState>
|
|
260
|
-
// 由于 RuntimeContext 是泛型类型,这里使用类型断言是安全的
|
|
261
|
-
const methodCtx = ctx;
|
|
262
|
-
const result = fn({ state: reactiveState, params, event: ctx.$event, ctx: methodCtx });
|
|
263
|
-
// 自动检测是否为 Promise
|
|
264
|
-
if (result && typeof result === 'object' && 'then' in result && typeof result.then === 'function') {
|
|
265
|
-
// 是 Promise,等待结果
|
|
266
|
-
return await result;
|
|
267
|
-
}
|
|
268
|
-
// 同步方法,直接返回
|
|
269
|
-
return result;
|
|
270
|
-
}
|
|
271
|
-
catch (error) {
|
|
272
|
-
// 错误处理:包装为 ServiceError
|
|
273
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
274
|
-
const originalError = error instanceof Error ? error : undefined;
|
|
275
|
-
// 如果已经是 ServiceError,直接抛出
|
|
276
|
-
if (error instanceof ServiceError) {
|
|
277
|
-
throw error;
|
|
278
|
-
}
|
|
279
|
-
// 包装为 ServiceError
|
|
280
|
-
throw new ServiceError(name, `Method execution failed: ${errorMessage}`, originalError, {
|
|
281
|
-
metadata: {
|
|
282
|
-
method: name,
|
|
283
|
-
params
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
// 注册多个别名以支持不同的调用方式
|
|
289
|
-
registry[name] = handler;
|
|
290
|
-
registry[`$methods.${name}`] = handler;
|
|
291
|
-
registry[`methods.${name}`] = handler;
|
|
292
|
-
registry[`services.${name}`] = handler;
|
|
293
|
-
}
|
|
294
|
-
return registry;
|
|
295
|
-
}
|
|
296
|
-
function registerComputed(computedDefs, reactiveState, ctx) {
|
|
297
|
-
Object.entries(computedDefs).forEach(([key, def]) => {
|
|
298
|
-
// 支持 Composition 风格(传入 ComputedRef)
|
|
299
|
-
let cVal;
|
|
300
|
-
if (def && typeof def === 'object' && 'value' in def && 'effect' in def) {
|
|
301
|
-
// 已经是 ComputedRef
|
|
302
|
-
cVal = def;
|
|
303
|
-
}
|
|
304
|
-
else {
|
|
305
|
-
// Options 风格:函数
|
|
306
|
-
const fn = def;
|
|
307
|
-
cVal = computed(() => fn(reactiveState));
|
|
308
|
-
}
|
|
309
|
-
// 监听计算属性变化,同步到运行时上下文
|
|
310
|
-
watch(cVal, (val) => {
|
|
311
|
-
const currentValue = ctx._get(key);
|
|
312
|
-
// 深度比较,避免不必要的更新
|
|
313
|
-
if (!isDeepEqual(currentValue, val)) {
|
|
314
|
-
ctx._set(key, val, { skipCallback: true });
|
|
315
|
-
}
|
|
316
|
-
}, { immediate: true });
|
|
317
|
-
// 将计算属性添加到 state(只读访问)
|
|
318
|
-
// 使用 configurable: true 允许覆盖已存在的属性
|
|
319
|
-
Object.defineProperty(reactiveState, key, {
|
|
320
|
-
get: () => cVal.value,
|
|
321
|
-
enumerable: true,
|
|
322
|
-
configurable: true // 允许覆盖,因为 computed 优先级高于初始值
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
function syncStateToContext(reactiveState, ctx, _options = {}) {
|
|
327
|
-
for (const key in reactiveState) {
|
|
328
|
-
if (key.startsWith('$') || key.startsWith('_'))
|
|
329
|
-
continue;
|
|
330
|
-
const value = reactiveState[key];
|
|
331
|
-
const currentValue = ctx._get(key);
|
|
332
|
-
// 深度比较,避免不必要的更新
|
|
333
|
-
if (!isDeepEqual(currentValue, value)) {
|
|
334
|
-
// 使用 skipCallback 避免触发 onStateChange(防止循环)
|
|
335
|
-
ctx._set(key, value, { skipCallback: true });
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* 深度比较两个值是否相等
|
|
341
|
-
*/
|
|
342
|
-
function isDeepEqual(a, b) {
|
|
343
|
-
if (a === b)
|
|
344
|
-
return true;
|
|
345
|
-
if (a == null || b == null)
|
|
346
|
-
return a === b;
|
|
347
|
-
if (typeof a !== typeof b)
|
|
348
|
-
return false;
|
|
349
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
350
|
-
if (a.length !== b.length)
|
|
351
|
-
return false;
|
|
352
|
-
return a.every((item, index) => isDeepEqual(item, b[index]));
|
|
353
|
-
}
|
|
354
|
-
if (typeof a === 'object' && typeof b === 'object') {
|
|
355
|
-
const keysA = Object.keys(a);
|
|
356
|
-
const keysB = Object.keys(b);
|
|
357
|
-
if (keysA.length !== keysB.length)
|
|
358
|
-
return false;
|
|
359
|
-
return keysA.every(key => isDeepEqual(a[key], b[key]));
|
|
360
|
-
}
|
|
361
|
-
return false;
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* 规范化 modelPath 配置
|
|
365
|
-
*/
|
|
366
|
-
function normalizeModelPathConfig(config) {
|
|
367
|
-
return {
|
|
368
|
-
separator: config?.separator ?? '.'
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
function applyBindingConfigs(bindings) {
|
|
372
|
-
if (!bindings)
|
|
373
|
-
return;
|
|
374
|
-
Object.entries(bindings).forEach(([key, config]) => {
|
|
375
|
-
if (!config)
|
|
376
|
-
return;
|
|
377
|
-
if (key.includes(':')) {
|
|
378
|
-
const [component, modelName] = key.split(':');
|
|
379
|
-
registerModelConfig(component, config, modelName);
|
|
380
|
-
}
|
|
381
|
-
else {
|
|
382
|
-
registerModelConfig(key, config);
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
//# sourceMappingURL=composable.js.map
|
|
1
|
+
import{ref as C,reactive as g,computed as x,watch as E,nextTick as R,getCurrentInstance as V,h as v,isReactive as P}from"vue";import{createRuntimeContext as W,invalidateCache as B,setPathValue as M,ServiceError as S}from"@variojs/core";import{VueRenderer as D}from"./renderer.js";import{registerModelConfig as _}from"./bindings.js";import{RefsRegistry as I}from"./features/refs.js";const X=(r,e={})=>{const a=N(r),o=V(),t=e.state?P(e.state)?e.state:g(e.state):g({});H(e.modelBindings);const c=q(e.methods,t);let u=!1;const f=new Set;let l=null;const i=W({},{onEmit:(n,s)=>{e.onEmit?.(n,s)},methods:c,exprOptions:e.exprOptions,onStateChange:(n,s,p)=>{if(!u&&!f.has(n)){u=!0,f.add(n);try{M(t,n,s,{createObject:()=>g({}),createArray:()=>g([]),createIntermediate:!0}),B(n,p),l&&R(l)}finally{f.delete(n),u=!1}}},createObject:()=>g({}),createArray:()=>g([])});for(const n in t){if(n.startsWith("$")||n.startsWith("_"))continue;const s=t[n];i._set(n,s,{skipCallback:!0})}e.computed&&z(e.computed,t,i);const b=C(i),$=G(e.modelPath),j=e.rendererOptions?.refsRegistry||new I,A=new D({...e.rendererOptions,instance:o,app:e.app??e.rendererOptions?.app,components:e.components??e.rendererOptions?.components,getState:()=>t,refsRegistry:j,modelPath:$}),d=C(null),y=C(null),O=e.errorBoundary?.enabled!==!1,m=()=>{y.value&&O&&(y.value=null);try{const n=a.value;if(!T(n)){d.value=null;return}d.value=A.render(n,i)}catch(n){const s=n instanceof Error?n:new Error(String(n));if(O){if(y.value=s,e.errorBoundary?.onRecover)try{e.errorBoundary.onRecover(s)}catch(p){console.warn("Error recovery callback failed:",p)}if(e.errorBoundary?.fallback)try{d.value=e.errorBoundary.fallback(s)}catch{d.value=w(s)}else d.value=w(s)}else d.value=null,e.onError?.(s)}};function w(n){return v("div",{style:{padding:"20px",border:"2px solid #f56565",borderRadius:"4px",backgroundColor:"#fff5f5",color:"#c53030"}},[v("div",{style:{fontWeight:"bold",marginBottom:"10px"}},"\u6E32\u67D3\u9519\u8BEF"),v("div",{style:{marginBottom:"10px"}},n.message),v("button",{onClick:()=>{y.value=null,m()},style:{padding:"8px 16px",backgroundColor:"#4299e1",color:"white",border:"none",borderRadius:"4px",cursor:"pointer"}},"\u91CD\u8BD5")])}l=m,m(),E(a,()=>R(m),{deep:!0,immediate:!1});let k=!1;return E(t,()=>{if(!k){k=!0;try{F(t,i,{skipCallback:!0});for(const n in t)n.startsWith("$")||n.startsWith("_")||B(n,i);R(m)}finally{k=!1}}},{deep:!0,flush:"post",immediate:!1}),{vnode:d,state:t,ctx:b,refs:j.getAll(),error:y,retry:()=>{y.value=null,m()}}};function N(r){return typeof r=="function"?x(()=>{const e=r();return e&&typeof e=="object"&&"value"in e&&"effect"in e?e.value:e}):r&&typeof r=="object"&&"value"in r&&"effect"in r?r:x(()=>r)}function T(r){return r!=null&&typeof r=="object"&&"type"in r}function q(r,e){if(!r)return{};const a={};for(const[o,t]of Object.entries(r)){const c=async(u,f)=>{try{const l=u,i=t({state:e,params:f,event:u.$event,ctx:l});return i&&typeof i=="object"&&"then"in i&&typeof i.then=="function"?await i:i}catch(l){const i=l instanceof Error?l.message:String(l),b=l instanceof Error?l:void 0;throw l instanceof S?l:new S(o,`Method execution failed: ${i}`,b,{metadata:{method:o,params:f}})}};a[o]=c,a[`$methods.${o}`]=c,a[`methods.${o}`]=c,a[`services.${o}`]=c}return a}function z(r,e,a){Object.entries(r).forEach(([o,t])=>{let c;if(t&&typeof t=="object"&&"value"in t&&"effect"in t)c=t;else{const u=t;c=x(()=>u(e))}E(c,u=>{const f=a._get(o);h(f,u)||a._set(o,u,{skipCallback:!0})},{immediate:!0}),Object.defineProperty(e,o,{get:()=>c.value,enumerable:!0,configurable:!0})})}function F(r,e,a={}){for(const o in r){if(o.startsWith("$")||o.startsWith("_"))continue;const t=r[o],c=e._get(o);h(c,t)||e._set(o,t,{skipCallback:!0})}}function h(r,e){if(r===e)return!0;if(r==null||e==null)return r===e;if(typeof r!=typeof e)return!1;if(Array.isArray(r)&&Array.isArray(e))return r.length!==e.length?!1:r.every((a,o)=>h(a,e[o]));if(typeof r=="object"&&typeof e=="object"){const a=Object.keys(r),o=Object.keys(e);return a.length!==o.length?!1:a.every(t=>h(r[t],e[t]))}return!1}function G(r){return{separator:r?.separator??"."}}function H(r){r&&Object.entries(r).forEach(([e,a])=>{if(a)if(e.includes(":")){const[o,t]=e.split(":");_(o,a,t)}else _(e,a)})}export{X as useVario};
|