当前位置: 首页 > 科技观察

教你如何在Vue3中封装一些有用的复合API

时间:2023-03-16 11:01:08 科技观察

在我个人看来,Hook和CompositionAPI的概念非常相似。事实上,React中可用的大部分Hook都可以再次使用Vue3重新实现。为了拼写方便,以下内容使用Hook而不是CompositionAPI。相关代码都放在了github[1]上。useRequest后台很容易用hook封装一组数据操作,比如下面的useBookimport{ref,onMounted}from'vue'functionfetchBookList(){returnnewPromise((resolve)=>{setTimeout(()=>{resolve([1,2,3])},1000)})}exportfunctionuseBook(){constlist=ref([])constloading=ref(false)constgetList=async()=>{loading.value=trueconstdata=awaitfetchBookList({page:1})loading.value=falselist.value=data}onMounted(()=>{getList()})return{list,loading,getList}}里面封装了获取资源和处理loading状态的逻辑,看起来它可以满足我们的需求。缺点是对于另外一个资源,我们好像需要写类似的模板代码,所以我们可以把这堆代码抽象出来,封装到useApi方法中,实现functionuseApi(api){constloading=ref(false)constresult=ref(null)consterror=ref(null)constfetchResource=(params)=>{loading.value=truereturnapi(params).then(data=>{//)按照约定,将api返回的结果直接复制到resultresult中。value=data}).catch(e=>{error.value=e}).finally(()=>{loading.value=false})}return{loading,error,result,fetchResource}}修改以上useBook方法functionuseBook2(){const{loading,error,result,fetchResource,}=useApi(fetchBookList)onMounted(()=>{fetchResource({page:1})})return{loading,error,list:result}}注意这是一个很通用的方法,假设现在需要封装其他请求,处理起来也很方便,不用一遍又一遍地重复处理加载和错误标志'success'}resolve(payload)},1000)})}functionuseUser(){const{loading,error,result,fetchResource,}=useApi((params)=>{//封装请求返回值returnfetchUserList(params)。then(res=>{console.log(res)if(res.code===200){returnres.data}return[]})})//...}思考处理网络请求是一个很常见的问题在前端工作中,处理上面列举的加载、错误处理等,还可以包括debounce、throttling、polling等各种情况,离开页面时取消未完成的请求,这些都可以进一步封装在useRequest中。useEventBusEventBus在多个组件之间进行事件通知的场景中比较有用。通过监听事件和触发事件,可以实现订阅者和发布者的解耦,实现一个常规的eventBusclassEventBus{constructor(){this.eventMap=newMap()}on(key,cb){lethandlers=this.eventMap.get(key)if(!handlers){handlers=[]}handlers.push(cb)this.eventMap.set(key,handlers)}off(key,cb){consthandlers=this.eventMap.get(key)if(!handlers)returnif(cb){constidx=handlers.indexOf(cb)idx>-1&&handlers.splice(idx,1)this.eventMap.set(key,handlers)}else{this.eventMap.delete(key)}}once(key,cb){consthandlers=[(payload)=>{cb(payload)this.off(key)}]this.eventMap.set(key,handlers)}emit(key,payload){consthandlers=this.eventMap.get(key)if(!Array.isArray(handlers))returnhandlers.forEach(handler=>{handler(payload)})}}我们在组件初始化时监听事件,在交互时触发事件。这些很容易理解;但是很容易忘记我们还需要在组件卸载的时候取消事件注册和释放相关资源所以可以封装一个useEventBus接口,将这些逻辑统一处理。由于组件卸载时需要注销相关事件,简单的实现思路是:只要在注册时(onandonce)收集相关事件和处理函数,然后在Cancel(off)收集的事件当onUnmounted所以我们可以劫持事件注册方法,并创建一个额外的eventMap来收集注册到当前接口//事件总线的事件,全局单例constbus=newEventBus()exportdefaultfunctionuseEventBus(){letinstance={eventMap:newMap(),//reuseeventBus事件收集逻辑on:bus.on,once:bus.once,//清除eventMapclear(){this.eventMap.forEach((list,key)=>{list.forEach(cb=>{bus.off(key,cb)})})eventMap.clear()}}leteventMap=newMap()//劫持两个监听方法收集当前组件对应事件conston=(key,cb)=>{instance.on(key,cb)bus.on(key,cb)}constonce=(key,cb)=>{instance.once(key,cb)bus.once(key,cb)}//组件卸载时取消相关事件卸载(()=>{instance.clear()})返回{on,once,off:bus.off.bind(bus),emit:bus。emit.bind(bus)}}这样,当price被unmounted时,组件注册的相关事件也会通过instance.clear被移除,比onUnmounted时手动取消每个组件方便多了。思考这个思路可以应用到很多组件卸载时需要进行清理操作的逻辑,比如:DOM事件注册addEventListener和removeEventListener定时器setTimeout和clearTimeout网络请求requestandabort从这个包中我们也可以看出一个很明显的组合API的优势:尽可能抽象出公共逻辑,而不用关注每个组件的具体细节。堆数据的方法封装在一起,比如下面的计数器函数useCounter(){constcount=ref(0)constdecrement=()=>{count.value--}constincrement=()=>{count.value++}return{count,decrement,increment}}这个useCounter暴露了获取当前值count,增加值decrement,减少值increment等数据和方法,然后可以愉快的在各个组件中实现计数器,在某些场景下,我们希望多个组件可以共享同一个计数器,而不是每个组件自己独立的计数器。一种情况是使用vuex等全局状态管理工具,然后修改useCounter的实现import{createStore}from'vuex'conststore=createStore({state:{count:0},mutations:{setCount(state,payload){state.count=payload}}})然后重新实现useCounterexportfunctionuseCounter2(){constcount=computed(()=>{returnstore.state.count})constdecrement=()=>{store.commit('setCount',count.value+1)}constincrement=()=>{store.commit('setCount',count.value+1)}return{count,decrement,increment}}显然,目前的useCounter2只是对state和mutations的封装store的,直接在组件中使用store也可以达到同样的效果,封装变得没有意义;另外,仅仅为了这个功能就在项目中添加一个vuex依赖,非常繁琐。基于这些问题,我们可以使用useModel来实现复用钩子状态的需求。整个思路也比较简单。使用Map保存钩子的状态。constmap=newWeakMap()exportdefaultfunctionuseModel(hook){if(!map.get(hook)){letans=hook()map.set(hook,ans)}returnmap.get(hook)}然后包装useCounterexportfunctionuseCounter3(){returnuseModel(useCounter)}//调用const{countinmultiplecomponents,decrement,increment}=useCounter3()//...const{count,decrement,increment}=useCounter3()这样每次调用useCounter3时,相同state被返回,多个组件实现了钩子的状态,它们之间共享。想一想userModel除了vuex和provide()/inject()之外,还提供了一种共享数据状态的方式,无需将所有状态放在一起或放在模块下就可以灵活地管理数据和操作数据。缺点是useCounter在没有与useModel一起打包时只是一个普通的钩子。在后期维护方面,我们很难判断某个状态是全局共享数据还是本地数据。因此,在使用useModel来处理hooks的共享状态时,一定要慎重考虑是否合适。useReducerredux的思想可以简单的概括为store维护全局state数据state,各个组件可以根据需要使用state中的数据,并监听state的变化。减速器接收动作并返回新状态。组件可以通过dispatch传递action来触发reducer状态更新,最后通知相关依赖更新数据。我们甚至可以hookredux的使用,类似functionreducer(state,action){//根据action进行处理//返回新的state}constinitialState={}const{state,dispatch}=useReducer(reducer,initialState);用Vue实现数据响应系统,我们甚至不需要实现任何发布订阅逻辑action格式为{type:string,payload:any}constdispatch=(action)=>{state.value=reducer(state.value,action)}return{state,dispatch}}然后实现一个useRedux负责投递reducer和actionimportuseReducerfrom'./index'functionreducer(state,action){switch(action.type){case"reset":returninitialState;case"increment":return{count:state.count+1};case"decrement":return{count:state.count-1};}}functionuseStore(){returnuseReducer(reducer,initialState);}我们希望维护一个全局的store,所以可以使用上面的useModelexportfunctionuseRedux(){returnuseModel(useStore);}和那么你可以在看起来和我们上面的useModel例子没什么区别,主要是因为它暴露了通用的dispatch方法,并且在reducer处维护了状态变化的逻辑,而不是在逻辑思维每个useCounter中维护和修改数据当然这个redux很简单,包括middleware,combineReducers,connect等方法都实现了,但是也给我们展示了redux最基本的数据流转过程useDebounce和useThrottle后台的很多前端业务场景需要处理throttling或者debounce场景。throttling函数和debounce函数本身并没有减少事件触发的次数,而是控制事件处理函数的执行,减少实际的逻辑处理过程,从而提高浏览器性能。表现。一个debounce的场景是:在搜索框中,根据用户输入的文本搜索相关内容,并以下拉的方式展示。由于输入是触发频率较高的事件,一般需要等到用户停止输出文本一段时间后,再请求接口查询数据。先实现最原始的业务逻辑.apply(context,args)},delay)}}exportfunctionuseAssociateSearch(){constkeyword=ref('')constsearch=()=>{console.log('search...',keyword.value)//模拟请求获取数据的接口}//watch(keyword,search)//原逻辑,请求watch(keyword,debounce(search,1000))//去抖,停止运行1秒后请求return{keyword}}然后引入类似useApi,我们可以把这个debounce的逻辑抽象出来,封装成一个通用的useDebounce的useDebounce实现似乎不需要我们编写任何额外的代码,just将debounce方法重命名为useDebounce。为了凑字数,我们还是修改一下,增加cancel方法导出函数useDebounce(cb,delay=100){consttimer=ref(null)lethandler=function(){clearTimeout(timer.value)letargs=参数,context=thistimer.value=setTimeout(()=>{cb.apply(context,args)},delay)}constcancel=()=>{clearTimeout(timer)timer.value=null}return{handler,cancel}}实现useThrottle节流和debounce的封装方式基本相同,只要知道throttle的实现,exportfunctionuseThrottle(cb,duration=100){letstart=+newDate()returnfunction(){letargs=argumentsletcontext=thisletnow=+newDate()if(now-start>=duration){cb.apply(context,args)start=now}}}思考从debounce/throttling的形式可以看出有些hook和我们的不一样以前的工具功能没有明确的界限。是统一hook所有代码,还是保留原有引入工具功能的风格,这是一个需要思考和实践的问题。总结本文主要展示了几种Hook封装思路和useRequest的简单实现,用于统一管理网络请求相关状态。无需在每次网络请求中重复处理loading、error等逻辑。useEventBus实现了当前组件监听的事件在组件卸载时自动取消,不需要重复写onUnmounted代码。这个想法也可以用于DOM事件、定时器和网络。请求的注册和注销等useModel实现了多个组件共享同一个hook状态,演示了除vuex和provide/inject函数外的跨组件共享数据的方案。useReducer使用hooks实现一个简单版的redux,使用useModel实现全局storeuseDebounce和useThrottle,实现debounce和throttling,思考hook代码风格和常规util代码风格,是否有必要hook一切把它放在github[3]上。由于只是为了展示思路和了解组合API的灵活使用,所以代码非常简单。如果大家发现有什么错误或者有其他的想法,欢迎指出,一起讨论。参考十大推荐ReactHook库[4]awesome-react-hooks[5]hooks-guide[6]ahooks[7]crooks[8]