业务需求
我们需要在单页面应用中,在页面切换的过程中也保持某些输入框的状态。例如在页面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的自定义指令Directive
。Directive
是写在每一个组件上的,可以根据自己写的逻辑,让这个组件的生命周期里实现不同的逻辑。
于是我想,能不能写一个指令,让所有带这个指令的组件在状态被更新的时候,都把当前值保存到store
,在这个组件重新被创建时,都从store
里取到值,并重新填回组件的绑定对象里。查了一下自定义指令的功能,这么干是可以的。干成功之后发现这玩意确实是可以的。
干
Directive 指令
对于Vue的指令我们已经很熟悉了,平时常用的v-if
、v-for
都属于Vue指令。Vue对于指令的描述是:
在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
钩子函数
在Vue的文档中,我们直接复制过来钩子函数的内容:
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
componentUpdated
:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind
:只调用一次,指令与元素解绑时调用。
在这次的开发中,我们用到了bind
和componentUpdated
钩子。我们在组件更新状态时将新的值保存到store中,在页面重新被进入,组件在本次进入页面中第一次出现时从store中取值,如果有值则存入组件的状态里。
组件的全局唯一key
既然要全局保存一个组件的状态,我们则需要试图从directive提供的参数里找到能在全局代表一个唯一组件的值。这里就需要提到bind
和componentUpdated
钩子提供的参数。
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-model
是search.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)
十分炫酷。