【vue3】这样写Composition Api 真的可以吗?

1,969 阅读4分钟

vue3版本已经更新到了3.2,我觉得vuer都应该对vue3有所了解,尤其是其中的组合式API。本文将通过作者近期的重构实践,与大家一起探讨,怎么样才算是Vue3组合式API的最佳实践。

从一个登录界面谈起

笔者之前有个vue2的项目,使用的是vue全家桶,尤其是vuex。我就从这个项目里抽出一个最简单的模块,讲讲我近期准备进行的优化和重构,也和大家一起探讨一下。

这里就是这个登录界面,我随便找了个网图,简单的说就是一个输入框和一个按钮。

image.png

但是在老项目中我大概有这些逻辑,样式不属于主要问题,因此忽略.

// login.vue
<template>
    <div class="flex flex-col justify-center items-center">
     <input ref="input" :value="usename" @change="changeUsername" @click="callFocus">
     <button ref="btn" @click="loginByUsername">log in</button>
    </div>
</template>
<script>
import {mapActions} from "vuex"
import {Toast} from 'vant'
export default {
  name: "LoginByName",
  data () {
    return {
      username:''
    }
  },
mounted () {
  const scrollIntoView = () => {
    // 输入法弹出式把界面上移
    setTimeout(() =>{
      this.$refs.btn.scrollIntoView(false)
    },10)
}
  this.$refs.input.addEventListener('focus',scrollIntoView)
  this.$once('hook:beforeDestroy',()=>{
    this.$refs.input.removeEventListener('focus',scrollIntoView)
  })
  this.trackByAdjust('some_event')
  this.trackByTa('some_event')
},
methods: {
  ...mapActions(['trackByAdjust','getUserInfo','trackByTa']),

changeUsername(e){
  // 首尾去空格再转小写
  this.username = e.target.value.replace(/^\s+|\s+$/gm, '').toLowerCase()
},
  callFocus(){
   // 修复iOS偶尔点击没有聚焦
    this.$refs.input.focus()
},
async loginByUsername(){
    if(this.username === ''){
      return Toast('username is required')
    }
    // 判断非法用户名
    if(!/^(?!.*..)(?!.*.$)[^\W][\w.]{0,29}$/.test(this.username)){
      this.showWrongName = true
      return
    }
    this.trackByTa('xxx',{login_click_position:'username'})
    await this.getUserInfo(this.username)
    // 后续进行一些操作
  }
}
</script>

从我这里逻辑可以看出来,大概分为两部分:登录的逻辑,和页面显示输入的逻辑。由于登录后的数据要在整个SPA中共享,因此这里登录时请求接口是放到vuex中,这个函数大概又是这样。

// api/index.js
import axios from 'axios'
import md5 from 'crypto-js/md5'
import enc_hex from 'crypto-js/enc-hex'

export const getData(username){
  // 这里会有接口参数加密,我以md5为例
 const ts = new Date().getTime()
 const secret = md5(username+ts).toString(enc_hex).slice(0,10)
 return axios({
     method:'get',
     url:'xxx.com/api/login',
     params:{ts,secret}
 })
}
// store/action.js
import {getData} from '@/api'
export default {
    async getUserInfo({state,commit,dispatch},username){
     // 处理一些逻辑
     commit('resetUser')
     const res = await getData(username)
     if(res.data.code === 200){
         if(state.xxx){
          commit('xxx',res.xx)
         } else {
            commit('aaa',res.xxx) 
         }
         return res.data
     } else {
      // 登录失败
      return false
     }
     
    }
}

其实mutation里还有一堆逻辑,为了节省篇幅我暂时忽略。哪怕写到现在,其实仅仅是一个登录界面已经有很多代码了,而且很分散。

想象一下,现在接口的加密方式变了,返回值也新增了一个字段,需要显示在界面上,那么我需要怎么修改呢?

  • 先修改/api/index.js中调用axios的相关代码,修改加密方法。
  • 修改/store/action.js
  • 修改/store/mutation.js
  • 修改/store/state.js
  • 可能需要修改utils/index.js
  • 再打开组件 login.vue,修改loginByUsername这个函数还有template中的显示。

随着功能的增加,每次修改都变得极为复杂,而复杂就代表着出错。

没错,这个结构基本是学花裤衩的管理模板,但凡是重度vuex的使用者,我觉得结构也差不了多少。

vue3重构的设想

这次正好有一些时间,打算将整个项目用vue3重构,因此最近一直在思考怎么样使用组合式API,昨天晚上我开始动手写,突然想到,组合式API不就是要把逻辑整合,按功能或者按照关注点分离。

当我把关注点想象为整个登录过程时,那么我就可以把整个逻辑放到一个函数中

import axios from 'axios'
import md5 from 'crypto-js/md5'
import enc_hex from 'crypto-js/enc-hex'
import useAppStore from '../store' // pinia 
import {useTaTrack} from '@/composables/useNameLogin'
const getData(username){
  // 这里会有接口参数加密,我以md5为例
 const ts = new Date().getTime()
 const secret = md5(username+ts).toString(enc_hex).slice(0,10)
 return axios({
     method:'get',
     url:'xxx.com/api/login',
     params:{ts,secret}
 })
}
export default async (username) => {
 if(username === ''){
      return {code:400,msg:'username is required'}
    }
    // 判断非法用户名
    if(!/^(?!.*..)(?!.*.$)[^\W][\w.]{0,29}$/.test(this.username)){
      this.showWrongName = true
      return
    }
    
    const res = await getUserInfo(this.username)
    // 后续进行一些操作
    if(res.data,code === 200){
        useTaTrack('login_success')
      // 返回值存入store,供其他组件页面使用
        const store = useAppStore()
        store.$patch({
          userInfo:res.data
        })
        // 省略n多处理store的逻辑
      return res.data
    }
    return false

  }
}

因此当我使用上方这个函数的时候,login.vue就会变成这样。

// login.vue
<template>
    <div class="flex flex-col justify-center items-center">
     <input ref="input" :value="usename" @change="changeUsername(e)" @click="callFocus">
     <button ref="btn" @click="handleClick">log in</button>
    </div>
</template>
<script> setup>
import {ref,onMounted,onBeforeUnmount,unref} from 'vue'
import useNameLogin from '@/composables/useNameLogin'
import {useAdjust,useTaTrack} from '@/composables/useTrack' // 事件打点
const input = ref(null) // 此处类似于vue2的$refs
const btn = ref(null)
const username = ref('')
const changeUsername = (e)=>{
  if(e.target.value!==username.value){
    username.value = e.target.value.toLowerCase()
  }
}
const handleClick = async ()=>{
    useTaTrack('click_login_button',{login_click_position:'username'})
    const res = await useNameLogin(unref(username))
    if(res){
     username.value = ''
     // do sth
    }
}
const scrollIntoView = () => {
  setTimeout(() =>{
    btn.value && btn.value.scrollIntoView(false)
  },100)
}

onMounted(() => {
  useAdjust('some_event')
  useTaTrack('some_event')
  input.value.addEventListener('focus',scrollIntoView)
})

onBeforeUnmount(()=>{
  input.value.removeEventListener('focus',scrollIntoView)
})


</script>

如果我现在在做上面提到那个需求:登录时接口加密方式变了,返回值也新增了一个字段,需要显示在界面上。

我只需要

  • 修改composables/useNameLogin.js中相关逻辑
  • 修改store/index.js,这里是pinia创建的store
  • 可能需要修改utils/index.js
  • 修改login.vue中显示相关的逻辑

从这个简单的例子来看,组件的代码量减少很多,整体上结构会更加简单。而且,当我下一次再新增一个按钮的时候,其实也只是在composables的文件夹下再新建一个文件,再写一个函数,然后绑定到界面上的按钮而已。

优势

  1. 高内聚,一个功能从开始到结束都在一个文件内,减少功能修改时跳转文件的次数。
  2. 新增功能时就是创建一个新函数,减少去操作别的地方那么多逻辑
  3. 页面上尽量使用setup,代码量减少,同时也不需要再页面里跳来跳去

劣势

  1. 结构混乱,以前我要找接口就是api文件夹,而现在api会分散个各个组合式API中
  2. 并没有完全解耦,有可能引用层级会更深。比如一个composable引用另一个composables
  3. 容易产生面条式代码

结语:这篇文章是对我昨天晚上写vue3的一些思考和回顾,也是想和大家一起讨论一下,如果我把组合式API都这样用,到底好不好?