不必把 Vue3 写成“麻花”:从状态碎片到对象协作,重新理解 Zova 的前端心智模型

Vue 3 的响应式依然是底座;Zova 希望解决的,是复杂业务中状态、行为、依赖与生命周期如何拥有清晰归属的问题。

Vue 3 很成功。它足够轻、足够灵活,响应式也足够优雅;React、Angular 也都在持续演进。于是很多开发者会问:Vue 未来还会凭什么保持竞争力?

我认为,一个值得关注的方向并不只是继续讨论"状态管理库该选哪个",而是回到一个更基础的问题:

当一个真实业务逐渐复杂时,我们究竟应该如何组织状态、行为,以及它们之间的关系?

在大量 Vue 3 项目中,我反复看到一种熟悉的写法:

ts 复制代码
const {
  list,
  loading,
  fetchList,
  createItem,
  removeItem,
} = useTodo();

刚开始看,这很简洁。但页面大一点、业务复杂一点,问题就会慢慢浮现:

  • 一个组件同时使用多个 composable,解构变量越堆越多;
  • listloadingsubmitreset 来自哪里,要不停上下翻找;
  • 变量命名开始互相冲突,只能加前缀:userListorderListtodoList
  • 为了避免解构后丢失响应式,又要记住 toRefsstoreToRefscomputed 等规则;
  • Pinia、provide/inject、composable、组件局部 ref/reactive 各自解决一部分问题,但开发者需要自行拼出整体架构。

这并不是说解构语法不能用,更不是说 Vue 3 的组合式 API 有问题。恰恰相反,它非常强大。

真正的问题是:当业务复杂度上升时,代码容易从"按对象协作"滑向"按变量拼装"。

而变量一旦散开,状态的归属、行为的边界、生命周期和依赖关系,也就容易跟着散开。

这正是 Cabloy/Zova 想解决的事情。


Vue 的响应式很好,但业务代码不该只剩一地变量

先澄清一个容易引发争论的观点:

Vue 并不是一个"必须面向对象"的框架;Vue 3 的组合式 API 也不是错误的架构方式。

Vue 3 给我们的核心能力,是一套出色的响应式系统。你可以用函数组织代码,也可以用对象组织代码;可以使用 refreactive,也可以把它们封装在 composable、store 或其他抽象中。

问题不在于"函数式还是面向对象"谁更先进,而在于:

你的业务状态和业务行为,是否有清晰、稳定、可理解的归属?

例如,一个待办事项页面通常有:

  • 列表查询、加载状态、错误状态;
  • 新建、修改、删除等操作;
  • 查询缓存与操作后的刷新策略;
  • 页面输入状态;
  • 页面渲染逻辑;
  • 可能还有权限、路由参数、跨页面共享状态。

如果这些内容只是不断从各个 composable 中解构出来,最终页面很容易变成这样:

ts 复制代码
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 中,可以这样写:

tsx 复制代码
@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),不需要再返回一组值,也不需要在使用处解构:

ts 复制代码
const { count, increment } = useCounter();

而是直接使用:

ts 复制代码
this.count
this.increment()

这是一种很朴素、却很重要的差异:状态不再漂浮在函数返回值中,而是明确地属于一个对象。

当然,count 能响应式更新,并不是因为"类字段天然响应式"。

真实机制是:Zova 会把 Controller 作为容器管理的 Bean 创建,并在底层接入 Vue 的响应式能力。因此,当 this.count++ 执行后,依赖它的渲染会像普通 Vue 响应式代码一样更新。

换句话说:

面向对象负责组织代码,Vue 3 负责让对象中的状态具备响应式。


不要到处解构,让依赖关系保留在代码里

很多复杂页面的真正难点,不在 UI,而在多个业务能力之间的协作。

例如,待办页面需要一个"待办数据模型"。在传统 Vue 项目里,它可能是:

  • 一个 composable;
  • 一个 Pinia store;
  • 一组 API 函数加若干 ref
  • 或者以上几种方式的混合。

在 Zova 中,可以把它明确为一个 Model:

ts 复制代码
@Model()
export class ModelTodo extends BeanModelBase {
  findAll() {
    return this.$useStateData({
      queryKey: ['list'],
      queryFn: async () => {
        return await this.scope.api.todo.findAll();
      },
    });
  }

  create() {
    return this.$useMutationData({
      mutationKey: ['create'],
      mutationFn: async body => {
        return await this.scope.api.todo.create(body);
      },
      onSuccess: () => {
        this.$invalidateQueries({ queryKey: ['list'] });
      },
    });
  }
}

这个 ModelTodo 不只是"请求 API 的工具类"。

它拥有一组完整而内聚的职责:

  • 待办事项查询;
  • 查询缓存的身份;
  • 创建操作;
  • 创建过程的状态;
  • 创建成功后的列表失效与刷新策略。

然后,在页面 Controller 中注入它:

tsx 复制代码
@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>
    );
  }
}

这里有一个很重要的阅读体验:

ts 复制代码
this.$$modelTodo.findAll()
this.$$modelTodo.create()

你一眼就能知道:

  • 这些能力来自 ModelTodo
  • 页面依赖待办模型;
  • 查询与写入逻辑没有散落在页面各处;
  • 后续想查看缓存、请求、失效策略时,有明确的对象可以进入。

这就是依赖注入带来的价值:它不仅仅是"少写一个 import",而是让对象之间的依赖关系变得显式、稳定、可追踪。


Pinia、provide/inject、composable:不是三套"状态管理",而是同一个问题的不同碎片

Vue 社区里常见的状态组织方式包括:

  • 组件内部的 ref / reactive
  • composable;
  • Pinia;
  • provide/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 并不要求一开始就拆出很多类。一个小页面完全可以只有一个 Controller:

tsx 复制代码
@Controller()
export class ControllerPageProfile extends BeanControllerPageBase {
  nickname = '';

  save() {
    // 保存昵称
  }

  protected render() {
    return (
      <input
        value={this.nickname}
        onInput={event => {
          this.nickname = event.target.value;
        }}
      />
    );
  }
}

只有当职责真的开始增长时,再逐步拆分:

  1. 页面变复杂:拆出 Render Bean;
  2. 样式变复杂:拆出 Style Bean;
  3. 业务能力可复用:拆出 Service Bean;
  4. 数据需要缓存、复用、持久化或 SSR 协调:拆出 Model。

这不是为了"面向对象而面向对象",而是让代码随着业务自然生长。

一个好的架构不应该要求开发者一开始就设计出一棵完美的类图;它应该允许开发者从简单开始,并在复杂度真正出现时,有一条清晰的演进路径。


TSX 让"渲染"和"行为"靠得更近

Zova 使用 TSX 作为主要渲染表达方式。

这并不意味着模板语法不好。Vue 模板对简单页面非常友好,也有很成熟的生态。

但在业务 UI 逐渐复杂时,TSX 有一个很实际的优势:渲染逻辑与 TypeScript 逻辑使用同一种语言表达。

例如条件、循环、局部变量、事件处理、组件组合,都可以自然地写在同一段代码中:

tsx 复制代码
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 的响应式依然熟悉,但组织复杂业务时,代码突然不再像"麻花"了。


延伸阅读

相关推荐
Vuji2 小时前
MCP: 一条 tool 调用链路的旅程
前端·人工智能
万少2 小时前
DeepSeek-V4-Flash 正式版上线了,但这 3 个坑我帮你提前踩了
前端·javascript·后端
明月_清风2 小时前
🚀 Palantir Foundry 本体论实战:当 Ontology 从"知识图谱"进化为"企业操作系统"
前端·后端
驳是2 小时前
入坑 Nginx,看这一篇就够了
前端
宁风NF3 小时前
JavaScript:网络请求与前端通信
开发语言·前端·javascript·网络·学习·ecmascript
明月_清风3 小时前
从概念到代码:用 Ontology 构建你的第一个知识图谱
前端·后端
程序员黑豆3 小时前
鸿蒙应用开发:AppStorage 全局状态存储用法教程
前端·harmonyos
猫猫不是喵喵.3 小时前
Vue3 中 computed 计算属性与 watch、watchEffect 监听
前端·javascript·vue.js
En^_^Joy3 小时前
Vue项目创建与入口配置全攻略
前端·vue.js·arcgis