最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue3你还在用Vuex?一个“函数式”状态管理的新思路

    正文概述 掘金(浅末)   2021-02-12   716

    vue3已经出来挺长一段时间了,我最近也在公司的一个项目中充分使用了vue3的特性。相比vue2,vue3的整个的编码方式有不小变化,如果要写出简洁优雅的代码,可能还是需要一定的时间去摸索。在摸索的过程中,需要面对的其中一个问题就是:vue3我怎么去更好的进行状态管理?

    在vue2时代,官方给我们提供了现成的状态管理工具vuex,它的使用方式借鉴了react生态的redux,定义一个状态变量,然后再定义它的getter, setter,以及异步变更action方法。总的来说,这套方案满足日常业务开发是完全没有问题的——只不过写起来稍显繁琐。

    然而,现在已经进入vue3时代,vuex的弊端就更加明显。首当其冲的第一点:不能很好地贴合typescript。vue3已经用ts重写,充分发挥了ts类型系统的作用;vuex目前整个设计来看,我在使用状态变更方法的时候,传入的居然都是字符串名?!看看官方文档中的示例:

    actions: {
      async actionA ({ commit }) {
        commit('gotData', await getData())
      },
      async actionB ({ dispatch, commit }) {
        await dispatch('actionA') // 等待 actionA 完成
        commit('gotOtherData', await getOtherData())
      }
    }
    

    这样完全没法做到智能化的类型提示,对重度依赖ts的本人来说,真的无法接受。

    vuex还有第2点弊端:啰嗦的语法,以及和vue3函数式风格api的割裂——虽然相比第一点倒还不算太大的问题,但用起来还是觉得膈应。总之对我而言,有了这两大弊端基本就宣告了vuex这套方案的死刑。

    于是,我在实际项目中开始摸索一套较为合理的状态管理方案。我将目光转移到了vue3自有的一套api:provide/inject。

    文档中的解释是,在父组件中调用provide函数,第一个参数传入字符串token,第二个参数传入子组件需要访问的对象。接着在子组件在调用inject函数,传入同样一个token,就能拿到该对象值了。代码如下:

    // Parent.vue
    import { provide } from 'vue';
    
    provide('person', {name: 'bob', age: 20});
    
    
    //Child.vue
    import { inject } from 'vue';
    const person = inject('person');
    

    这是最简单的示例,告诉我们怎么通过provide/inect在父子组件中传值。初看之下好像并没多大卵用,因为传下去的只是个普通对象啊,并不具备响应式更新的能力。但是,请记住,我们现在使用的是vue3,想要有响应式能力,我们传ref/reactive对象就可以了嘛!于是把代码稍微改改:

    (父组件)

    // Parent.vue
    <script lang="ts" setup>
      import { provide, reactive } from "vue";
    
      const person = reactive({name: 'bob', age:32});
      provide('person', person);
     </script>
    
    <template>
      <div>
        <child></child>
      </div>
    </template>
    

    (子组件)

    // Child.vue
    <script lang="ts" setup>
    import { inject, onMounted } from 'vue';
    
      const person = inject('person');
      onMounted(() => {
        person.age = 25;
      })
     </script>
    
    <template>
      <div>
        我叫{{person.name}} 我的年龄:{{person.age}}
      </div>
    </template>
    

    在父组件,通过provide提供了一个reactive响应式对象;然后在子组件通过inject注入该对象。在子组件修改对象的age属性,视图就会响应式更新!同样的,如果child组件也有自己的子组件,调用inject同样有效。这点我就不多讲了,毕竟这对古老的api在vue2时代就已经存在,只不过在vue3,他俩终于不再鸡肋,反而是可堪大用!

    现在,问题似乎已经得到解决。有了provide/inject和ref/reactive配合,父子组件/兄弟组件共享状态的问题已经迎刃而解。但随着业务的深入,我陷入了沉思,这个方案还是有严重问题,需要抢救。主要体现在两点:

    第一点,我们的provide方法,要传的参数仍然是个字符串啊!

    provide/inject目前的设计是,他们之间是靠一个字符串(或symbol)来建立暗号的,暗号对上了,我就把相应的值给你。可要是业务复杂了,暗号多了,一不留神其中一个拼写错误,那是不是得找花眼?而且,这种字符串传参大法仍然没有很好的类型提示,看看我在vscode 编辑器中写的代码:

    Vue3你还在用Vuex?一个“函数式”状态管理的新思路

    person下的age和name属性,直接有一条红线提示:类型“unkown”上不存在age属性。为啥,因为inject函数本身是需要传泛型的,如果不传,系统就会认为inject返回的对象类型位unkown。

    也就是讲,我每次调用inject方法,还得手动写个类型声明?像下面这样:

    const person = inject<{name: string; age: string}>('person');
    

    不好意思,typescript不是这样用的。

    第二点:直接在provide中传一个响应式对象,缺少封装性和逻辑复用能力。

    现在回头看看vuex,虽然写法挺啰嗦,类型提示不友好,但是它提供的是整套状态管理方案,它使得组件之间不仅共享状态,还能复用一系列更改状态的逻辑方法。mutation action干的就是这个事。

    再看看我们先前的做法:直接往下传reactive对象,至于状态更改的逻辑,还是写在组件里了,这就很不科学。这样一想,我们是不是得在单独一个文件,建一个大点的对象,把状态和状态变更逻辑都包含进去?比如:

    //person.ts
    const personStore = {
      state: reactive({
       name: bob,
       age: 20
      }),
      setAge(n: number) {
        this.state.name = n;
      }
    }
    

    看上去有点vuex那味了,而且还用上了vue3致力于抛弃掉的this。怎么看都极其不优雅。甚至还做不到class面向对象的初始化能力和封装性。毕竟有些方法可能只是内部调用,并不希望全部暴露出去。而且在编码过程中我发现自己封装的通用hook函数不能很方便地在这种状态对象中使用。总而言之,在vue3整个框架内内显得水土不服。

    提了这么多问题,那我们该怎么办?就这样妥协下去,早点完成业务功能然后下班?

    欸。。等等,我们从头捋一遍,回到最初的问题——尤雨溪为什么要发布变化如此之大的vue3? 我们为什么又非得从vue2切换到vue3?

    官方的解释是,使用composition api,能得到更强的逻辑复用能力。意思就是,以前相当多的业务逻辑写在组件里,现在可以很方便的抽象出去,单独放在一个函数内了。官方文档还给了个示例:

    // src/composables/useRepositoryNameSearch.js
    
    import { ref, computed } from 'vue'
    
    export default function useRepositoryNameSearch(repositories) {
      const searchQuery = ref('')
      const repositoriesMatchingSearchQuery = computed(() => {
        return repositories.value.filter(repository => {
          return repository.name.includes(searchQuery.value)
        })
      })
    
      return {
        searchQuery,
        repositoriesMatchingSearchQuery
      }
    }
    

    上面是将一个搜索功能抽象为组合式函数的示例,当然我更愿意和react统一称为hook函数,在hook函数内部,我们可以使用组件生命周期钩子函数如onMounted,也就是可以通过hook函数执行一些初始化逻辑而不是非得在组件内。与此同时,在hook中定义的响应式变量可以return出去给组件访问。如果hook内部做出了变更操作,组件视图也会进行相应更新。

    这就是vue3在逻辑复用方面的强大能力。但是,这跟状态管理有啥关系呢?

    关系太大了。

    思考到现在,结论已经很明显。而且我觉得这很可能是vue3官方的一个疏漏,只着重宣传hook函数(官方文档叫组合式函数)的逻辑复用能力,却不提这套方案用来做状态管理也是极为自然的。大概由于vuex/redux这类方案对开发者的影响太深,所以一提到状态管理,就总是以他们为起点开始思考,最终搞出来的也只是个变种版的redux。其实,有了hook这样的编码方式,状态管理的问题就注定被其染指。现在,是时候抛弃vuex这类跟函数式风格及其不搭的思想包袱了。

    想一想,每个组件使用一次hook函数,函数都会调用一次从而形成全新的执行上下文和闭包。若hook函数f返回响应式变量 x,那么组件A, B分别使用hook函数f,他们得到的只是一个专属于自己的变量x。可是,在某些业务场景下,我想让A和B共享同一个变量x怎么办?

    provide/inject这对cp又重新登场了。

    思考如下业务场景:我写了一个hook函数,里面全是与用户相关的逻辑:比如用户信息的修改以及登录与注销。一般情况下,跟用户相关的状态和逻辑有可能在各处组件都能用到,显而易见,它应该是全局唯一的,如果是每个组件单独去使用这个hook函数,就没法共享用户相关的状态变量。

    export function useUserInfo() {
      const userInfo = reactive({ });
      
      const login = (data) => {...};
      const logout = () => {...};
      const editUserInfo => (data) => {};
      
      return {
       userInfo,
       login,
       logout,
       editUserInfo
      }
    }
    

    那么直接在根组件调用provide,将userHook函数传入会怎样?

    //app.vue
    
    <script>
    import {provide} from 'vue';
    import {useUserInfo} from '@/hooks/userUserInfo';
    
    provide('user', useUserInfo())
    </script>
    

    恭喜你发现了华点!现在一切都豁然开朗了:有了provide之后,可以在login组件使用inject访问useUserInfo函数返回的对象,调用该对象的login方法;在logOut组件调用该对象返回的logOut方法,在userInfo组件使用返回的userInfo变量渲染用户信息。一切都是那么自然,只要任意一处执行了态变更的逻辑,所有相关组件都能响应更新。

    视图以外,皆是HOOK。

    vue3时代,这就是我的编程理念。组件只负责访问hook返回的响应式变量丢给模板,其余的事情比如业务逻辑,状态管理,全是hook的事情。这就是视图与逻辑与状态的彻底分离。所以说,react hook 和vue3推出之后,前端时代真的变了(当然,作为angular的铁粉,我认为angular 3年前已经走到这一步,可惜来得太早成为了先烈)。

    当然,讲了这么久,状态管理的最终方案是有了,那就是**回归hook。**如果希望hook内部的状态与逻辑在多个组件内共享,那只需要在hook的基础上加上一个provide/inject;如果你希望hook函数在每个组件都生成全新的状态,像以前那样组件内照常使用就行。这样一想,我们的解决办法好像并没有做什么新的动作,相比vuex那一套简单明了多了。所以说,hook出现之后,状态管理的问题已经从根本上被消解了。

    但是等等……ts类型提示的问题好像还是没解决啊。其实解决这个问题只需要对provide/inject进行一层封装就好了。废话不多说,直接上我在项目实践中写的代码:

    //定义一个用于状态共享的hook函数的标准接口
    export interface FunctionalStore<T extends object> {
      (...args: any[]): T;
      token?: symbol;
      root?: T;
    }
    
    //对原生provide进行封装
    
    //由于inject函数只会从父组件开始查找,所以useProvider默认返回hook函数的调用结果,以防同组件层级需要使用
    export function useProvider<T extends object>(func: FunctionalStore<T>): T {
      !func.token && (func.token = Symbol('functional store'));
        const depends = func();
        provide(func.token, depends);
        return depends;
    }
    
    // 可以一次传入多个hook函数, 统一管理
    export function useProviders(...funcs: FunctionalStore<any>[]) {
      funcs.forEach( func => {
        !func.token && (func.token = Symbol('functional store'));
        provide(func.token, func());
      });
    }
    
    //对原生inject进行封装
    
    type InjectType = 'root' | 'optional';
    
    //接收第二个参数,'root'表示直接全局使用;optional表示可选注入,防止父组件的provide并未传入相关hook
    export function useInjector<T extends object>(func: FunctionalStore<T>, type?: InjectType) {
      const token = func.token;
      const root = func.root;
    
      switch(type) {    
        case 'optional':
          return inject<T>(token) || func.root || null;
        case 'root':
          if(!func.root) func.root = func();      
          return func.root;
        default:      
          if(inject(token)) {
            return inject<T>(token)
          };
          if(root) return func.root;
          throw new Error(`状态钩子函数${func.name}未在上层组件通过调用useProvider提供`);
      }   
    }
    

    以上,就是我基于vue3 provide/inject 实现的状态管理方案,只有2个基本api,useProvider和useInjector,具有完全的hook函数能力,以及完备的自动类型提示。请注意这两函数传入的都是hook函数本身而不是字符串token。token我是直接挂在函数的属性里了,省去了编码时手动填token的动作。

    下面是我在公司项目实际使用的案例:

    Vue3你还在用Vuex?一个“函数式”状态管理的新思路

    app.vue根组件一次性传入5个hook函数

    Vue3你还在用Vuex?一个“函数式”状态管理的新思路

    其中一个hook函数的简单实现(useState是对vue ref的封装,为了看起来像react ...)

    Vue3你还在用Vuex?一个“函数式”状态管理的新思路

    在子组件注入对应的hook函数,可以看到编辑器的类型提示(完全不用手动申明类型)

    因为这个项目业务不算复杂,所以看上去并没有很好体现这套方案的强大之处。当然可伸缩性也是它的一个优点,项目不复杂也随便用。随着业务扩展,我认为这套方案也是可以一直hold住整个项目的。现在我们可以设想一个应用场景,来看看hook+provide/inject能做到什么地步。

    vue3开发中,想必很多人会基于组合api写一个通用的ajax请求hook函数,一些针对请求的通用逻辑都放在这里面,类似下面的代码:

    export function useRequest<R>(url: string, option: any) {
      const res = ref<R>(null);
      const status = ref('pending');
    
      const checkHttpStatus = (status: number) => {
        /** 处理错误status状态码的相关逻辑 */
      }
    
     onMounted(() => {
      fetch(url, option).then( response => {
        if(response.ok) {
          res.value = response.body;
          status.value = 'success'
        } else {
          checkHttpStatus(response.status);
          status.value = 'failed'
        }    
      });
     })
    
     return {
       res,
       status   
     }
    }
    

    以上是一个比较简单的通用请求函数,在组件内或其他hook内使用时自动发送请求。但在很多场景下,这点代码是难以hold整个业务的。比如常见的,对请求拦截进行权限认证。

    那么怎么办呢,直接在useRequest里面加入权限验证的相关代码?这样耦合太严重。单独写在一个函数内然后调用?嗯,可以。但是,如果我们的视角抬得更高一点,这个useRequest hook想要在公司通用库中被多个项目使用,这样直接调用权限验证函数好像不太行,每个项目的业务逻辑都不一样啊。

    provide/inect再再次登场!

    首先,我们先定一个暗号token,用来表示我们的useRequest需要的请求拦截功能,如下:

    export const  HTTP_INTERCEPT = Symbol('intercept');
    

    现在,如果你的一个同事正在使用你写的useRequest方法,并且想使用请求拦截功能。那么,他首先需要写一个拦截函数,接着把引入上面的暗号HTTP_INTERCEPT,赋值给函数的token属性(请参考我上面定义过的一个functionalStore接口!)

    //httpIntercept.ts
    export function httpIntercept() {
      const auth = useAuth();
      const intercept = () => {
        /** 通过auth来判断,useAuth你就当是该项目存在的一个hook  返回一个Promise */
       return new Promise( resolve => { ... })
      }
    
      return  intercept  
    }
    
    httpIntercept.token = HTTP_INTERCEPT
    

    接着在app.vue通过useProvider提供该拦截函数:

    //app.vue
    <script>
      useProvider(httpIntercept);
    </script>
    

    好了,接下来我们的useRequest需要改造一下

    export function useRequest<R>(url: string, option: any) {
      const res = ref<R>(null);
      const status = ref('pending');
     
     //注意第二个参数optional,表示拦截器可选,上层组件如果没有提供拦截器函数则默认返回null
      const intercept  = useInjector(HTTP_INTERCEPT, 'optional');
    
      const checkHttpStatus = (status: number) => {
        /** 处理错误status状态码的相关逻辑 */
      }
    
      const request = () => {
        fetch(url, option).then( response => {
          if(response.ok) {
            res.value = response.body;
            status.value = 'success'
          } else {
            checkHttpStatus(response.status);
            status.value = 'failed'
          }
          
        });
      }
    
     onMounted(() => {
      if(!intercept) {
        request()
      } else {
        intercept().then( _ => {
          request()
        })
      }
      
     })
    
     return {
       res,
       status   
     }
    }
    

    可以看到,结合了provide/inect的hook函数,简直是将代码的复用和解耦发挥到了极致!状态管理,不过是顺带而来的便利罢了。


    起源地下载网 » Vue3你还在用Vuex?一个“函数式”状态管理的新思路

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元