别再写换皮 Options 了!Vue3 Setup 真正的用法的是这3步升级

很多人迁移 Vue3 后,写的 Setup 只是 Options API 的换皮------看似整洁,实则依然无序膨胀。其实 Composition API 的核心不是 Setup 语法,而是按功能组织代码!今天从一个详情页出发,带你3步升级,真正吃透它。

从简单的详情页开始

假设我们有一个页面:

  • 展示 业务详情
  • 展示 用户信息
  • 有一个 确认操作(confirm)

在 Vue2 里我们会这样写:

  • mounted 里拉数据
  • methods 里写 confirm
  • data 里存 loading

迁移到 Vue.js 3 后,很多人会写成这样:

ts 复制代码
setup() {
  const route = useRoute()

  const detail = ref(null)
  const queryDetailLoading = ref(false)
  const user = ref(null)
  const confirmLoading = ref(false)

  async function queryDetail(id: string) {
    queryDetailLoading.value = true
    detail.value = await api.getDetail(id)
    queryDetailLoading.value = false
  }

  async function queryUser(id: string) {
    user.value = await api.getUser(id)
  }

  async function confirm(id: string) {
    confirmLoading.value = true
    await api.confirm(id)
    confirmLoading.value = false
  }

  onMounted(() => {
    queryDetail(route.params.id)
    queryUser(route.params.id)
  })

  return {
    detail,
    queryDetailLoading,
    user,
    confirmLoading,
    confirm
  }
}

看起来没问题,甚至代码还很整洁:

  • 定义外部变量
  • 定义内部变量
  • 定义内部方法
  • 在生命周期中触发初始化操作

但是,代码的组织度好像没有变化?上面的代码好像还是很容易无序膨胀,出现几千行的 .vue 文件?应该怎么抽象?

Options 到 Composition 的优势在哪?

Vue 官网有张对比图:

很多人以为它表达的是:

代码更清晰了。

但它真正表达的是:

代码组织维度发生了变化。

Options API 是:

  • data
  • computed
  • methods
  • watch

按类型组织。

而 Composition API,可以按照:

  • 详情模块
  • 用户模块
  • 操作模块

功能模块组织代码。

升级1:按功能模块组织代码

先把同一职责的代码写在一起。

ts 复制代码
setup() {
  const route = useRoute()

  // ===== 详情模块 =====
  const detail = ref(null)
  const queryDetailLoading = ref(false)

  async function queryDetail(id: string) {
    queryDetailLoading.value = true
    detail.value = await api.getDetail(id)
    queryDetailLoading.value = false
  }

  onMounted(() => {
    queryDetail(route.params.id)
  })

  // ===== 用户模块 =====
  const user = ref(null)

  async function queryUser(id: string) {
    user.value = await api.getUser(id)
  }

  onMounted(() => {
    queryUser(route.params.id)
  })

  // ===== confirm 模块 =====
  const confirmLoading = ref(false)

  async function confirm(id: string) {
    confirmLoading.value = true
    await api.confirm(id)
    confirmLoading.value = false
  }

  return {
    detail,
    queryDetailLoading,
    user,
    confirmLoading,
    confirm
  }
}

这里有个 Vue 2 很容易忽略的思维定式:

在 Vue 3 中,可以有多个生命周期钩子,可以写多个 onMounted

这一刻,setup 不再是一个"大仓库",而是一个"功能组合器"。

生命周期不再是"一个入口",它可以属于不同功能块。

这样的代码组织,才是官网对比图中的样子。

升级2:拆分 Setup,useXxx 的诞生

在 setup 中拆分功能块后,可以很自然地将各个功能块拆分出 setup。

比如对于详情模块,输入是 route.params.id,输出是 detailqueryDetailLoading

ts 复制代码
  // ===== 详情模块 =====
  const detail = ref(null)
  const queryDetailLoading = ref(false)

  async function queryDetail(id: string) {
    queryDetailLoading.value = true
    detail.value = await api.getDetail(id)
    queryDetailLoading.value = false
  }

  onMounted(() => {
    queryDetail(route.params.id)
  })

重构为

ts 复制代码
function useDetail(id: string) {  
  const detail = ref(null)
  const queryDetailLoading = ref(false)
  
  async function queryDetail(id: string) {
    queryDetailLoading.value = true
    detail.value = await api.getDetail(id)
    queryDetailLoading.value = false
  }

  onMounted(() => queryDetail(id))
  
  return {  
    detail,  
    queryDetailLoading  
  }
}

注意不要将 const route = useRoute() 抽到 useDetail 中,保持 useDetail 依据 id 获取数据的单一职责。 Composition 的抽象边界应该围绕"数据输入输出",而不是围绕"框架能力"。

于是 setup 成为

js 复制代码
setup() {
  const route = useRoute()

  // ===== 详情模块 =====
  const { detail, queryDetailLoading } = useDetail(route.params.id)
  
  // ===== 用户模块 =====
  const { user } = useUser(route.params.id)

  // ===== confirm 模块 =====
  const { confirm, confirmLoading } = useConfirm()

  return {
    detail,
    queryDetailLoading,
    user,
    confirmLoading,
    confirm
  }
}

通过这种方式,各个 composition 有自己的职责,setup 负责视图层数据的聚合,你再不会写出流水账式的代码。

升级3:从生命周期驱动到数据驱动

到这里,其实我们已经完成了"功能拆分"。

但还有一个更重要的转变:setup 不应该围绕生命周期组织,而应该围绕数据变化组织。

比如经常遇到的问题:如果路由参数变化要怎么做呢?

实际这里的逻辑是,当 id 变化时,重新获取 detail

ts 复制代码
function useDetail(id: MaybeRefOrGetter<string>) {  
  // ...

  watch(() => toValue(id), () => queryDetail(toValue(id)), { immediate: true })
  
  return {  
    detail,  
    queryDetailLoading  
  }
}
js 复制代码
setup() {
  const route = useRoute()

  // ===== 详情模块 =====
  const { detail, queryDetailLoading } = useDetail(() => route.params.id)
  
  // ...

  return {
    detail,
    queryDetailLoading,
    // ...
  }
}

如果使用了 将 props 传递给路由组件,还可以将 route 统一到组件标准的 props 操作:

js 复制代码
setup(props) {
  // ===== 详情模块 =====
  const { detail, queryDetailLoading } = useDetail(() => props.id)
  
  // ...

  return {
    detail,
    queryDetailLoading,
    // ...
  }
}

回顾:Composition 的升级到底在哪?

升级路径其实很自然:

  1. 在 setup 内按功能组织
  2. 将功能抽离 setup 方便共享
  3. 通过 watch 将生命周期驱动转向数据驱动

Vue3 没让代码变乱。它提供了按功能组织代码的能力。

它的真正价值,不在于 setup,而在于它允许我们按"业务模型"组织代码,而不是按"框架结构"组织代码。

这可以让我们写出更内聚,更单一职责的代码。

在 Vue 2 中想达成这种能力需要通过 mixin 的方式,但 mixin 是通过在 this 上动态添加属性的方式进行的,这导致 mixin 在类型推导上极其困难,极度依赖对实现细节的了解。

升级加餐:不拆 setup,也能简单

如果你已经接受"按功能组织 + 数据驱动副作用"这个思路,那么其实可以再进一步,把这些模式固化下来。

在很多场景下,setup 中的内容没有复用的必要,单独抽到其它文件中有点大材小用。

有没有不拆分,还能保证各功能块高度内聚的写法?我写了 vue-asyncx 用于解决这个问题。

ts 复制代码
setup(props) {
  // ===== 详情模块 =====
  const detail = ref(null)
  const queryDetailLoading = ref(false)

  async function queryDetail(id: string) {
    queryDetailLoading.value = true
    detail.value = await api.getDetail(id)
    queryDetailLoading.value = false
  }

  watch(() => props.id, () => queryDetail(props.id), { immediate: true })

  // ===== 用户模块 =====
  const user = ref(null)

  async function queryUser(id: string) {
    user.value = await api.getUser(id)
  }

  watch(() => props.id, () => queryUser(props.id), { immediate: true })

  // ===== confirm 模块 =====
  const confirmLoading = ref(false)

  async function confirm(id: string) {
    confirmLoading.value = true
    await api.confirm(id)
    confirmLoading.value = false
  }

  return {
    detail,
    queryDetailLoading,
    user,
    confirmLoading,
    confirm
  }
}

可以重构为

ts 复制代码
import { useAsyncData, useAsync } from 'vue-asyncx'

setup(props) {
  // ===== 详情模块 =====
  const { 
    detail, 
    queryDetailLoading 
  } = useAsyncData('detail', () => api.getDetail(props.id), {
    watch: () => props.id, immediate: true
  })

  // ===== 用户模块 =====
  const { user } = useAsyncData('user', () => api.getUser(props.id), {
    watch: () => props.id, immediate: true
  })

  // ===== confirm 模块 =====
  const { confirm, confirmLoading } = useAsync('confirm', (id: string) => api.confirm(id))

  return {
    detail,
    queryDetailLoading,
    user,
    confirmLoading,
    confirm
  }
}

核心代码从19行减少到10行,代码量减少接近 50%,而语义化、组织性不丢失。

更多详细用法,见:早点下班:在 Vue3 中少写 40%+ 的异步代码

相关推荐
我有满天星辰37 分钟前
Vue 3 响应式双雄:ref 与 reactive 完全指南
前端·javascript·vue.js
leoZ23112 小时前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
荒诞英雄15 小时前
从 17MB Vendor 大包到按需加载:Vite 多入口 Vue 组件库首屏优化实践
vue.js·vite
jsonbro15 小时前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
Cobyte17 小时前
23.Vue Vapor 组件 attrs 的实现
前端·javascript·vue.js
斯特凡今天也很帅18 小时前
xml页面可以打开,不报错,但是显示数据的地方也不显示,我是怎么解决的
xml·vue.js
柯克七七20 小时前
给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了
前端·vue.js·typescript
自然 醒1 天前
前端如何实现在线预览office常见文件功能?
前端·vue.js
肉肉不吃 肉1 天前
前端调试跨域如何解决
前端·vue.js
卓怡学长1 天前
w268基于springboot + vue 物流系统
java·数据库·vue.js·spring boot·spring·intellij-idea