从简单的详情页开始

假设我们有一个页面:

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

在 Vue2 里我们会这样写:

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

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

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:按功能模块组织代码

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

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 很容易忽略的思维定式:

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

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

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

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

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

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

  // ===== 详情模块 =====
  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)
  })

重构为

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  
  }
}

于是 setup 成为

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

function useDetail(id: MaybeRefOrGetter<string>) {  
  // ...

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

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

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

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

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

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

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

升级路径其实很自然:

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

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

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

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

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

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

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

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

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
  }
}

可以重构为

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%+ 的异步代码

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com