在 vue-router 跳转过程中保留页面组件的值状态

Vue
476
0
0
2022-07-13
标签   Vue基础

业务需求

我们需要在单页面应用中,在页面切换的过程中也保持某些输入框的状态。例如在页面A使用手机号搜索出了一个列表,从B页面切换回A页面时,还要保持手机号的输入和搜索结果。

页面生命周期

对于传统页面,一个页面的生命周期就是从加载网页到关闭或刷新网页,用户的操作全部都是在这个生命周期里,开发的时候考虑单个生命周期的状态及数据流动即可。页面切换会导致url变化,从而重载页面。

对于SPA项目,整个网站是一个页面,也是一个完整的生命周期。每个子页面各自的生命周期只是整体生命周期中事件循环的一部分。SPA项目使用路由来控制子页面切换,路由切换会导致锚点(有的项目则是url参数)变化,而不会重载整个页面。可以理解为SPA项目把页面切换变成了一个网页中的功能,开发者开发一个又一个“页面”则是当前页面下的一个组件。页面切换则是通过diff算法来改变页面上显示的元素来实现的。

方案

<keep-alive>

keep-alive是Vue官方提供的页面缓存组件,官方对此的解释是:

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。 当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。 在 2.2.0 及其更高版本中,activated 和 deactivated 将会在 <keep-alive> 树内的所有嵌套组件中触发。 主要用于保留组件状态或避免重新渲染

但是在实际使用过程中,keep-alive会导致在路由切换时传页面,搞得我的前端同事很痛苦。具体为什么串页面,以及keep-alive的运行原理,我没有仔细研究,这个是未来需要学习的一个点。而且在这个业务需求里,我们也不是想要完全保留页面的状态,而是保留某个组件的值。如果未来有某个需求,只想让页面中的某几个输入框保留状态,keep-alive的优势就一点都没有了,反而成了bug。于是这让我想到,有没有什么办法也能保留页面状态,而且是组件级别的。

v-directive

这就让我想到了Vue的自定义指令DirectiveDirective是写在每一个组件上的,可以根据自己写的逻辑,让这个组件的生命周期里实现不同的逻辑。

于是我想,能不能写一个指令,让所有带这个指令的组件在状态被更新的时候,都把当前值保存到store,在这个组件重新被创建时,都从store里取到值,并重新填回组件的绑定对象里。查了一下自定义指令的功能,这么干是可以的。干成功之后发现这玩意确实是可以的。

Directive 指令

官方文档

对于Vue的指令我们已经很熟悉了,平时常用的v-ifv-for都属于Vue指令。Vue对于指令的描述是:

在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

钩子函数

在Vue的文档中,我们直接复制过来钩子函数的内容:

bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。

在这次的开发中,我们用到了bindcomponentUpdated钩子。我们在组件更新状态时将新的值保存到store中,在页面重新被进入,组件在本次进入页面中第一次出现时从store中取值,如果有值则存入组件的状态里。

组件的全局唯一key

既然要全局保存一个组件的状态,我们则需要试图从directive提供的参数里找到能在全局代表一个唯一组件的值。这里就需要提到bindcomponentUpdated钩子提供的参数。

bind(el, binding, vnode);

componentUpdated(el, binding, vnode, oldVnode);

在一番console.log观察后,我发现vnode.data.model.expression是组件绑定值的表达式。例如有组件<input v-model="search.mobile"></input>,那么这个组件绑定值的表达式就是search.mobile。只要在一个页面中,data中的某一个值只被一个组件绑定,那么vnode.data.model.expression在当前页面就是唯一的。如果一个页面中data中的某个值被多个组件绑定,那也满足我们的需求——这多个组件共用一个状态,我们为这些个组件保留为同一个状态,这也合理。

在另一番console.log观察中,我发现vnode.child.$route.fullPath是这个组件所在的页面的路由值。如果当前页面路由值是全局唯一的,那么${vnode.child.$route.fullPath}_${vnode.data.model.expression}就是这个组件在全局的唯一标识。例如当前页面的路由是/home/page1,组件的v-modelsearch.mobile,此时唯一标识是/home/page1search.mobile,表示在页面/home/page1中,绑定了search.mobile的组件。如果想更人性化一些,可以使用md5或sha1等的方式,让这个唯一标识更像id一些,但我没有这样做,现在这样就足够了。

不使用v-model的麻烦组件

到这里还有一个问题。对于普通的使用v-model来绑定值的组件来说,vnode.data.model.expression可以直接拿到v-model里的表达式,我们这个方法还好用。如果是element-ui里的pagination这样的组件,绑定值是用:page.sync这样的属性来实现的,我们的表达式就不管用了。所以对于非v-model绑定的组件,我们提供了state-key属性。在这种组件中使用指令的时候,需要额外使用state-key来指定绑定的值。比如:

<pagination  v-stateful  state-key="page.page"  :page.sync="page.page" />

在实际使用过程中,我们发现这类组件不光有state-key的问题,由于不是使用v-model更新的状态,就算是我们用指令把数据塞到page.sync绑定的值里,组件的显示状态也不会变。也就是说会有可能当时页面已经被指令翻到第4页了,但页面上显示的还是1。为了解决这个问题,我们尝试了Vue.$forceUpdate,但是不管用,问题出在组件的:key上。如果使用指令改动了这个组件的绑定值,也需要更新这个组件对应的:key值,这样才会更新这个组件的显示状态。

<pagination  v-stateful  state-key="page.page"  :page.sync="page.page"  :key="randnum" />

data() {
    return {
        randnum:  1
    }},

methods: {
    switchPage() {
        // ....
        randnum  ++
    }
}

这里其实有改进的余地,可以让指令的代码不用污染到具体的业务组件。可以在state中创建一个randnum的对象,每个页面的全局唯一key都是这个对象下的子对象。对于这些“麻烦组件”,让它们的key去绑定这个store里的值,在指令更新了组件的值之后,再去更新对应的randnum就好了。

// $store.state

{
    stateful: {
        randnum: {
            "/home/login_mobile": 1,
            "/home/login_passwd": 1,
            "/home/dashboard_mobile": 1,
            "/home/dashboard_gender": 1,
        }
    }
}

componentUpdated(el, binding, vnode, oldVnode) {
    // ...
    vnode.context.$store.state.stateful.randnum[vnodeKey] ++
}

上面说的“改进的余地”实际还没有改进,现在的项目中还是在每个页面里为这些麻烦组件在data里单独声明一个随机数变量,有机会一定把这些麻烦分子都干掉。

起个名

既然这个自定义指令让使用它的组件都更富有状态,甚至可以抵抗路由切换,那么我们就叫它stateful吧。那么对应的指令就是v-stateful了。

这个名字在前面的实例代码里也出现过好多次了。

全局绑定

指令开发好了,被我放在了@/directive/stateful里,在main.js里全局声明一下,就能在整个项目的任何地方都使用v-stateful来应用这个命令了。

// @/main.js

import  stateful  from  '@/directive/Stateful/stateful.js'

Vue.directive('stateful', stateful)

// @/views/*.vue

<pagination  v-stateful  state-key="page.page"  :page.sync="page.page"  :key="randnum" />

NPM

既然是一个独立的功能,能不能不让它出现在我们代码的业务目录里,而是像某一个独立的模块一样,被yarn安装在node_modules里。于是我想,能不能把它发布到npm上。

先去npm的官网注册个账号,然后去github上创建个仓库,把自己的代码组织一下。

/

---- src

-------- stateful.js

---- index.js

---- package.json

/src/stateful.js就是刚刚写完的完整的业务代码,index.js负责导出。在加上其他乱七八糟的文件(.gitignore、readme),就可以准备发布了。发布过程没什么含金量,我就罗列了。

$ yarn init

$ npm login -d

# 在这输入在npm官网注册的用户名、密码、邮箱

# 而且邮箱是公开的

$ npm publish

如果输出绿色的话,不出意外你就可以在npm仓库里搜到自己的成果了。

发布成功后,我们把@/src/directive里的代码删掉,使用yarn安装自己的库,从库中引入。

$ yarn add statefulvue

import  stateful  from  'statefulvue'

Vue.directive('stateful', stateful)

十分炫酷。