vue單頁應用前進重新整理後退不重新整理方案探討
引言
前端webapp應用為了追求類似於native模式的細緻體驗,總是在不斷的在向native的體驗靠攏;比如本文即將要說到的功能,native由於是多頁應用,新頁面可以啟用一個的新的webview來開啟,後退其實是關閉當前webview,其上一個webview就自然顯示出來;但是在單頁的webapp應用中,所有內容其實是在一個頁面中展示的,不存在多頁的情況,這時就需要前端開發來想辦法實現相應的體驗效果。
首先需要說明一下,本文所說的前進重新整理後退不重新整理是指元件是否重新渲染,比如列表A頁面,點選其中的每一項進入詳情B頁面,然後從B頁面後退到列表A頁面時,A頁面沒有重新渲染,也沒有重新發送ajax請求。下面,我們就來說說在vue的單頁應用中,實現前進重新整理後退不重新整理的一些實現方案,其他的方案大家可以一起補充。
keep-alive方案
ofollow,noindex" target="_blank">keep-alive 是vue官方提供的一種快取元件例項的方法,vue官網對其用法的介紹:
<keep-alive>
包裹動態元件時,會快取不活動的元件例項,而不是銷燬它們。
正如vue官網的介紹,我們在開發中就可以使用他這一點來快取後退不用重新整理的路由元件。具體的實現思路如下。
1、模板中使用keep-alive來快取對應的路由元件
在app.vue模板中改寫 <router-view>
,具體可以這樣:
<keep-alive> <router-view v-if="$route.meta.keepAlive"> <!-- 這裡是會被快取的檢視元件,比如列表A頁面 --> </router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"> <!-- 這裡是不被快取的檢視元件,比如詳情B頁面--> </router-view>
這種方式需要通過vue 路由元資訊 的配合,當然也可以像下面這樣:
<keep-alive include="A"> <router-view> <!-- 只有路徑匹配到的檢視元件,如上面的列表A頁面會被快取! --> </router-view> </keep-alive>
這種方式缺點是:
需要事先知道路由元件的**name**值,這在大型專案中不是一個特別好的選擇。
2、在路由配置檔案中配置路由元資訊
下面以第一種模板方式來展開介紹。對應上面模板檔案中的路由元資料配置如下:
routes: [{ path: '/', name: 'home', component: Home, meta: { keepAlive: false //此元件不需要被快取 } }, { path: '/list', name: 'list', component: List, meta: { keepAlive: true //此元件需要被快取 } }, { path: '/detail', name: 'detail', component: Detail, meta: { keepAlive: false // 此元件需要被快取 } } ]
3、在keep-alive元件提供 activated
鉤子函式實現資料更新邏輯
需要強調的是keep-alive元件(這裡是指keep-alive包裹的路由元件,下同)與一個vue元件是有區別的,vue的具體生命週期函式可以參考 這裡 ;而 keep-alive
元件,除了正常vue元件提供的生命週期之外,其額外新增了2個跟 keep-alive
相關的鉤子函式:
- activated : 快取的元件再次進入時會觸發
- deactivated : 快取的元件離開時會觸發
既然keep-alive元件提供了這麼多生命週期函式鉤子,那麼這些鉤子函式具體的執行順序是怎樣的呢?
第一次進入keep-alive元件時,其生命週期執行順序:
beforeRouteEnter --> created --> mounted --> activated --> deactivated
非首次進入時,其生命週期執行順序:
beforeRouteEnter -->activated --> deactivated
可以看到,非首次進入keep-alive元件時,正常的vue元件生命週期函式是不會在執行,而會執行keep-alive新增的兩個週期鉤子函式。同時也可以看出離開keep-alive元件時其destroy周期函式並沒有執行,從側面證明快取元件並沒有銷燬。根據介紹,我們可以:
通過利用keep-alive提供 activated
鉤子函式來決定是否進行ajax請求來更新元件,以及 deactivated
鉤子函式來重置頁面相關狀態。
keep-alive實現後推不重新整理的方案,有一些地方需要特別注意:
- keep-alive元件的更新時機要有清晰的認知
意思就是在開發過程中需要知道後退不重新整理元件雖然不重新渲染,但是要知道元件資料在什麼情況下需要重新發送ajax請求來獲取資料,從而更新元件。
就拿上面的A、B頁面來說,我們需要知道列表A頁面對應的keep-alive元件在什麼時候進行更新,因為進入A頁面的入口可以是從B頁面後退而來,也可能從其他頁面前進而來;當然需要對這兩種不同情況需要加以區分,否則A頁面的資料就一直是第一次快取過的資料。
這篇文章 給出了一種解決方案:
首先,在每個路由元資訊meta中新增一個isBack欄位,用來解決beforeRouterEnter不能直接訪問vue例項。
... { path: '/list', name: 'list', component: List, meta: { keepAlive: true, //此元件需要被快取 isBack: false } } ...
然後,藉助 beforeRouteEnter
鉤子函式來判斷頁面來源:
beforeRouteEnter(to, from, next) { if(from.name === 'detail') { //判斷是從哪個路由過來的,若是detail頁面不需要重新整理獲取新資料,直接用之前快取的資料即可 to.meta.isBack = true; } next(); },
最後,需要藉助keep-alive提供鉤子函式 activated
來完成是否更新:
activated() { if(!this.$route.meta.isBack) { // 如果isBack是false,表明需要獲取新資料,否則就不再請求,直接使用快取的資料 this.getData(); // ajax獲取資料方法 } // 恢復成預設的false,避免isBack一直是true,導致下次無法獲取資料 this.$route.meta.isBack = false },
- 在 keep-alive元件前進的頁面重新整理導致keep-alive元件狀態丟失
繼續以上面的A、B頁面為例,在進入詳情B頁面後,然後重新整理,這時列表A頁面的快取的資料都丟失了,由於上面的判斷規則也會導致不會重新獲取資料。所以對於這種問題,還需要額外加一些判斷條件。由於keep-alive第一次進入時會執行 created 方法,所以利用這點加一個標識來加以判斷:
//第一次進入keep-alive路由元件時 created() { this.isFirstEnter = true; // 只有第一次進入或者重新整理頁面後才會執行此鉤子函式,使用keep-alive後(2+次)進入不會再執行此鉤子函式 },
activated鉤子函式也需要增加對應的判斷:
activated() { if(!this.$route.meta.isBack || this.isFirstEnter){ // 如果isBack是false,表明需要獲取新資料,否則就不再請求,直接使用快取的資料 // 如果isFirstEnter是true,表明是第一次進入此頁面或使用者重新整理了頁面,需獲取新資料 this.data = ''// 把資料清空,可以稍微避免讓使用者看到之前快取的資料 this.getData(); } // 恢復成預設的false,避免isBack一直是true,導致下次無法獲取資料 this.$route.meta.isBack=false // 恢復成預設的false,避免isBack一直是true,導致每次都獲取新資料 this.isFirstEnter=false; },
- 快取過多keep-alive元件,因常駐記憶體會導致記憶體佔用過多
這是一個特別需要注意的問題,尤其是當整個系統或者系統大部分頁面都使用keep-alive來快取元件時,由於其是快取在記憶體中的,若不加處理,記憶體堆積越來越大,導致系統卡頓。正確的解決方案是: 需要及時銷燬掉記憶體快取的元件 。
具體可以參考: vue issue#6509 和 記一次vue 的keep-alive踩坑之路 兩篇文章的實現思路。
巢狀路由
巢狀路由具體的實現可以參考 官網 ,這種方案也是一種解決思路。下面以一個具體的例子(如下圖所示)來說一下實現的具體過程。
正如上圖所示,一個下單頁面有6處跳出當前頁面檢視規則、協議或者修改具體某些內容的頁面,因為這6項依賴這個訂單頁,那麼可以使用路由巢狀來實現這種後退不重新整理的過程,下單頁作為父路由,其他跳轉項可以作為其子路由。具體步驟:
1、配置路由資訊
{ path: '/order', component: Order, children: [ { path: 'invoice', component: Invoice }, { path: 'contact', component: Contact }, { path: 'costrule', component: CostRule }, { path: 'refundrule', component: RefundRule },{ path: 'useragreement', component: UserAgreement },{ path: 'payrule', component: PayRule } ] }
2、在下單頁Order元件模板中配置路由巢狀。
<div class="safe-area-pb"> <purchase /> <router-view /> </div>
這樣,通過下單頁進入其他頁面比如進入修改聯絡人資訊頁面,那麼路由從 /order 進入到 /order/contact ,修改完成後回退會回到父路由 /order 中,完成後推不重新整理的功能。
當然,正如上面所說的,巢狀路由方案只是一種可選擇方案,有其對應的使用場景;另外,使用過程還需要注意以下幾點:
**1、進入子路由後,若是在子路由強制重新整理後,父子路由的元件都會重新渲染,執行各自路由元件的生命週期;父路由中設定相關邏輯都會執行。
**2、子路由若被其他頁面共用,這時進入子路由時會觸發第一點的情況,所以最好子路由是父路由獨佔的。
component元件配合路由方案
這種方案主要是利用vue提供的動態路由元件 component 來實現,頁面元件的切換不再根據路由path來決定,而是根據不同的業務邏輯載入不同的動態元件。具體的實現可以參考這篇文章解決方案第6點部分: 非同步載入的業務線如何動態註冊路由? 。同樣,同步路由也可以使用動態路由來完成對應後退不重新整理功能。這不過這種方式的使用場景更急侷限。
總結
上面提供的3種解決方案,第一種方案大家都比較熟悉,後面兩種可能相對來說就比較陌生。它們只是解決同一問題的不同解決方案,想必還有其他的解決方案本人沒有想到,有其他更好方案的可以一起探討。
參考
1、 滴滴 webapp 5.0 Vue 2.0 重構經驗分享