在 vue-router 跳轉過程中保留頁面元件的值狀態

thebestxt發表於2021-10-21

業務需求

我們需要在單頁面應用中,在頁面切換的過程中也保持某些輸入框的狀態。例如在頁面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)

十分炫酷。

相關地址

npm地址

github原始碼

本作品採用《CC 協議》,轉載必須註明作者和本文連結
從前從前,有個人愛你很久

相關文章