vue3版本已经更新到了3.2,我觉得vuer都应该对vue3有所了解,尤其是其中的组合式API。本文将通过作者近期的重构实践,与大家一起探讨,怎么样才算是Vue3组合式API的最佳实践。
从一个登录界面谈起
笔者之前有个vue2的项目,使用的是vue全家桶,尤其是vuex。我就从这个项目里抽出一个最简单的模块,讲讲我近期准备进行的优化和重构,也和大家一起探讨一下。
这里就是这个登录界面,我随便找了个网图,简单的说就是一个输入框和一个按钮。
但是在老项目中我大概有这些逻辑,样式不属于主要问题,因此忽略.
// 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的文件夹下再新建一个文件,再写一个函数,然后绑定到界面上的按钮而已。
优势
- 高内聚,一个功能从开始到结束都在一个文件内,减少功能修改时跳转文件的次数。
- 新增功能时就是创建一个新函数,减少去操作别的地方那么多逻辑
- 页面上尽量使用setup,代码量减少,同时也不需要再页面里跳来跳去
劣势
- 结构混乱,以前我要找接口就是api文件夹,而现在api会分散个各个组合式API中
- 并没有完全解耦,有可能引用层级会更深。比如一个composable引用另一个composables
- 容易产生面条式代码
结语:这篇文章是对我昨天晚上写vue3的一些思考和回顾,也是想和大家一起讨论一下,如果我把组合式API都这样用,到底好不好?