業務需求
我們需要在單頁面應用中,在頁面切換的過程中也保持某些輸入框的狀態。例如在頁面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)
十分炫酷。
相關地址
本作品採用《CC 協議》,轉載必須註明作者和本文連結