不必把 Vue3 写成“麻花”:从状态碎片到对象协作,重新理解 Zova 的前端心智模型
Vue 3 的响应式依然是底座Zova 希望解决的是复杂业务中状态、行为、依赖与生命周期如何拥有清晰归属的问题。Vue 3 很成功。它足够轻、足够灵活响应式也足够优雅React、Angular 也都在持续演进。于是很多开发者会问Vue 未来还会凭什么保持竞争力我认为一个值得关注的方向并不只是继续讨论“状态管理库该选哪个”而是回到一个更基础的问题当一个真实业务逐渐复杂时我们究竟应该如何组织状态、行为以及它们之间的关系在大量 Vue 3 项目中我反复看到一种熟悉的写法const{list,loading,fetchList,createItem,removeItem,}useTodo();刚开始看这很简洁。但页面大一点、业务复杂一点问题就会慢慢浮现一个组件同时使用多个 composable解构变量越堆越多list、loading、submit、reset来自哪里要不停上下翻找变量命名开始互相冲突只能加前缀userList、orderList、todoList为了避免解构后丢失响应式又要记住toRefs、storeToRefs、computed等规则Pinia、provide/inject、composable、组件局部ref/reactive各自解决一部分问题但开发者需要自行拼出整体架构。这并不是说解构语法不能用更不是说 Vue 3 的组合式 API 有问题。恰恰相反它非常强大。真正的问题是当业务复杂度上升时代码容易从“按对象协作”滑向“按变量拼装”。而变量一旦散开状态的归属、行为的边界、生命周期和依赖关系也就容易跟着散开。这正是 Cabloy/Zova 想解决的事情。Vue 的响应式很好但业务代码不该只剩一地变量先澄清一个容易引发争论的观点Vue 并不是一个“必须面向对象”的框架Vue 3 的组合式 API 也不是错误的架构方式。Vue 3 给我们的核心能力是一套出色的响应式系统。你可以用函数组织代码也可以用对象组织代码可以使用ref、reactive也可以把它们封装在 composable、store 或其他抽象中。问题不在于“函数式还是面向对象”谁更先进而在于你的业务状态和业务行为是否有清晰、稳定、可理解的归属例如一个待办事项页面通常有列表查询、加载状态、错误状态新建、修改、删除等操作查询缓存与操作后的刷新策略页面输入状态页面渲染逻辑可能还有权限、路由参数、跨页面共享状态。如果这些内容只是不断从各个 composable 中解构出来最终页面很容易变成这样const{todos,loading,refresh}useTodos();const{createTodo,creating}useCreateTodo();const{removeTodo,removing}useRemoveTodo();const{keyword,resetKeyword}useSearch();const{currentUser}useAuth();每一行单独看都没问题但放在一起时页面真正的结构已经不明显了。谁拥有待办列表创建成功后谁负责刷新列表loading是查询中的加载还是创建中的加载哪些状态应当跨页面保留哪些只属于当前页面如果另一个页面也需要待办列表是复制逻辑、抽公共 composable还是建一个 store这些问题不是语法问题而是状态与行为的架构问题。换一个视角让状态和行为重新回到对象中Zova 并不试图替换 Vue 3 的响应式能力。它做的是另一件事在 Vue 3 响应式之上引入 Controller、Bean、Model 与依赖注入让状态、行为、生命周期和依赖关系重新以“对象协作”的方式组织起来。可以把它概括为三个核心特性Vue 3 响应式 TSX 渲染 Angular 风格依赖注入注意这不是把 Angular 或 React 简单搬进 Vue而是让三者各自擅长的部分结合起来能力Zova 的选择响应式底座Vue 3渲染表达TSX对象组织与依赖协作IoC / Dependency Injection页面逻辑承载Controller可复用业务状态Model业务能力拆分Service / Bean这样开发者不需要先问“我要把这段逻辑写进哪个 composable”而可以先问一个更自然的问题这份状态和行为到底属于谁一个页面就是一个有职责的对象假设我们做一个最简单的计数器页面。在 Zova 中可以这样写Controller() export class ControllerPageCounter extends BeanControllerPageBase { count 0; get countText() { return 当前计数${this.count}; } increment() { this.count; } protected render() { return ( div div{this.countText}/div button onClick{() this.increment()} 加 1 /button /div ); } }这段代码的关键不在于“用了 class”而在于它非常直接地表达了业务含义count是这个页面拥有的状态increment()是这个页面提供的行为countText是从状态推导出来的展示数据render()描述页面如何呈现所有内容都围绕同一个对象展开。你不需要先创建ref(0)不需要再返回一组值也不需要在使用处解构const{count,increment}useCounter();而是直接使用this.countthis.increment()这是一种很朴素、却很重要的差异状态不再漂浮在函数返回值中而是明确地属于一个对象。当然count能响应式更新并不是因为“类字段天然响应式”。真实机制是Zova 会把 Controller 作为容器管理的 Bean 创建并在底层接入 Vue 的响应式能力。因此当this.count执行后依赖它的渲染会像普通 Vue 响应式代码一样更新。换句话说面向对象负责组织代码Vue 3 负责让对象中的状态具备响应式。不要到处解构让依赖关系保留在代码里很多复杂页面的真正难点不在 UI而在多个业务能力之间的协作。例如待办页面需要一个“待办数据模型”。在传统 Vue 项目里它可能是一个 composable一个 Pinia store一组 API 函数加若干ref或者以上几种方式的混合。在 Zova 中可以把它明确为一个 ModelModel()exportclassModelTodoextendsBeanModelBase{findAll(){returnthis.$useStateData({queryKey:[list],queryFn:async(){returnawaitthis.scope.api.todo.findAll();},});}create(){returnthis.$useMutationData({mutationKey:[create],mutationFn:asyncbody{returnawaitthis.scope.api.todo.create(body);},onSuccess:(){this.$invalidateQueries({queryKey:[list]});},});}}这个ModelTodo不只是“请求 API 的工具类”。它拥有一组完整而内聚的职责待办事项查询查询缓存的身份创建操作创建过程的状态创建成功后的列表失效与刷新策略。然后在页面 Controller 中注入它Controller() export class ControllerPageTodo extends BeanControllerPageBase { Use() $$modelTodo: ModelTodo; get queryTodos() { return this.$$modelTodo.findAll(); } async addTodo() { await this.$$modelTodo.create().mutateAsync({ title: 学习 Zova, }); } protected render() { const todos this.queryTodos.data ?? []; return ( div button onClick{() this.addTodo()} 新建待办 /button ul {todos.map(item ( li key{item.id}{item.title}/li ))} /ul /div ); } }这里有一个很重要的阅读体验this.$$modelTodo.findAll()this.$$modelTodo.create()你一眼就能知道这些能力来自ModelTodo页面依赖待办模型查询与写入逻辑没有散落在页面各处后续想查看缓存、请求、失效策略时有明确的对象可以进入。这就是依赖注入带来的价值它不仅仅是“少写一个 import”而是让对象之间的依赖关系变得显式、稳定、可追踪。Pinia、provide/inject、composable不是三套“状态管理”而是同一个问题的不同碎片Vue 社区里常见的状态组织方式包括组件内部的ref/reactivecomposablePiniaprovide/inject各类数据请求与缓存库。它们都很有价值也都适合特定场景。但从初学者的角度看它们容易形成一个困惑我到底该在什么时候用 composable什么时候用 Pinia什么时候 provide/inject什么时候又该把状态放在组件里Zova 提供的思路是先不急着从工具名称出发而是先从状态归属与生命周期出发。问题更适合的归属只在当前页面短暂存在的输入、开关、交互状态Page Controller可复用 UI 单元的状态与行为Component Controller跨多个页面复用的业务数据、查询、缓存、持久化状态Model可复用的业务能力或基础能力Service Bean某个组件树中的局部协作对象ctx作用域 Bean应用或 SSR 请求范围内的对象app作用域 Bean系统级、长生命周期对象sys作用域 Bean于是原来分散在多个生态工具中的概念可以逐渐被统一到一个心智模型中状态不是先决定放进哪个库而是先决定它属于哪个对象、活多久、被谁依赖。这会让架构讨论从“技术选型”回到“业务建模”。“对象化”不等于写 Java 式前端看到class、Controller()、Model()、Use()有些开发者会立刻担心这会不会变成过度设计会不会写成传统 Java 项目那种层层包装答案取决于我们怎么使用它。Zova 并不要求一开始就拆出很多类。一个小页面完全可以只有一个 ControllerController() export class ControllerPageProfile extends BeanControllerPageBase { nickname ; save() { // 保存昵称 } protected render() { return ( input value{this.nickname} onInput{event { this.nickname event.target.value; }} / ); } }只有当职责真的开始增长时再逐步拆分页面变复杂拆出 Render Bean样式变复杂拆出 Style Bean业务能力可复用拆出 Service Bean数据需要缓存、复用、持久化或 SSR 协调拆出 Model。这不是为了“面向对象而面向对象”而是让代码随着业务自然生长。一个好的架构不应该要求开发者一开始就设计出一棵完美的类图它应该允许开发者从简单开始并在复杂度真正出现时有一条清晰的演进路径。TSX 让“渲染”和“行为”靠得更近Zova 使用 TSX 作为主要渲染表达方式。这并不意味着模板语法不好。Vue 模板对简单页面非常友好也有很成熟的生态。但在业务 UI 逐渐复杂时TSX 有一个很实际的优势渲染逻辑与 TypeScript 逻辑使用同一种语言表达。例如条件、循环、局部变量、事件处理、组件组合都可以自然地写在同一段代码中protected render() { const query this.$$modelTodo.findAll(); if (query.isPending) { return div加载中……/div; } if (query.isError) { return div加载失败请稍后重试。/div; } return ( ul {query.data?.map(todo ( li key{todo.id} span{todo.title}/span button onClick{() this.removeTodo(todo.id)} 删除 /button /li ))} /ul ); }在这里数据从哪个 Model 来清楚点击后调用哪个行为清楚页面状态如何分支清楚TypeScript 类型如何流动也清楚。对于习惯了业务逻辑复杂、组件组合较多的开发者来说这种“逻辑—状态—渲染”在同一对象中的连续性会带来很强的掌控感。Zova 不是否定 Vue而是给 Vue 增加一套更完整的工程语言如果只看表面Zova 像是在 Vue 3 上增加了 class、装饰器、IoC 容器和 Model。但更准确地说它提供的是一套更统一的前端工程语言用Controller表达页面或组件的交互职责用Bean表达可组合、可注入的能力用Model表达有生命周期、有缓存、有失效策略的数据状态用IoC表达对象创建、作用域与依赖关系用Vue 3 响应式保持状态变化与 UI 更新的自然联动用TSX让渲染与逻辑在同一种语言中协作。所以Zova 并不是在说“Vue 3 的组合式 API 不够好。”它更像是在说“当项目从一个组件走向一套业务系统时我们需要的不只是组合函数还需要清晰的对象边界、依赖关系和状态归属。”写在最后从“怎么管理状态”到“状态属于谁”前端开发这些年一直在讨论状态管理。但很多时候我们问错了问题。我们经常问用 Pinia 还是 composable全局状态放哪里怎么避免 props drilling怎样让数据请求自动缓存这些问题当然重要但它们背后其实是同一个更根本的问题这份状态属于谁谁应该修改它谁依赖它它应该活多久当它变化时谁负责维护一致性当这些问题有了明确答案技术工具的选择往往就不再困难。这也是 Cabloy/Zova 想带来的体验不要让业务代码变成一堆被解构、被搬运、被拼装的变量。让状态回到对象让行为回到职责让依赖回到结构。如果你已经熟悉 Vue 3不妨尝试用 Zova 写一个小业务页面一个 Controller、一个 Model、几段 TSX。你可能会发现Vue 3 的响应式依然熟悉但组织复杂业务时代码突然不再像“麻花”了。延伸阅读Zova 前端基础从 Vue 开发者视角阅读 ZovaZova 与 Vue 3 对比IoC 与 Beans面向 Vue 开发者的状态架构Model 架构