Vue 全家桶仿原生App切換效果和頁面緩存實踐

需求

在以前作的 WEB 單頁應用在切換效果上有些生硬,並且頁面的緩存和更新在體驗上達不到預期的效果。雖然 vue 的 keep-alive 能達到將組件進行緩存,可是在作一些特殊的需求的時候,如把新打開的頁面(組件)進行緩存,當點擊返回的時候就將該緩存的頁面(組件)進行銷燬,就像模擬 App 中體驗的效果同樣,並且在相似於打開商品詳情的時候是使用的同一個組件,不一樣的路由。而且在商品詳情中繼續打開商品詳情。在通常的路由配置和組件複用的貌似達不到這種效果。並且也不可能將全部的詳情頁路由進行配置。css

幾個問題。

要實現這麼一個需求就遇到了如下幾個問題。html

  1. 模擬 app 切換的效果。
  2. 組件複用動態前端路由。
  3. 頁面(組件)按需求進行緩存和銷燬。
  4. 緩存的頁面(組件)進行數據更新。
  5. 瀏覽器前進後退按鈕對前端路由的影響。
  6. 手機端滑動手勢對前端路由的影響。

最終仍是差很少實現了這個效果,雖然不是很完善。前端

主要是基於 vue vue-routervue

直接使用的 vue-cli 進行示例文件構建node

這插件是 【控制切換效果】 和 【按需緩存頁面】 以及 【動態路由管理】 功能ios

具體須要實現完整的效果還須要參考示例配置文件git

插件地址: vue-app-effectgithub

示例配置: Examplesvue-router

示例演示: Demovuex

這裏就不放效果圖了直接掃二維碼真實體驗 微信演示:

若是以爲有用的話,記得點個 Star 。

配置指南

安裝插件

$ npm install vue-app-effect -S
複製代碼

配置插件

vue 入口文件 main.js 配置插件後 就會附加一個 vnode-cache 緩存組件,用法和 keep-alive 同樣。 另外還會在 window 對象上掛上一個 $VueAppEffect 對象,用於存儲操做路由的一些記錄。

import VnodeCache from 'vue-app-effect'                         // 引入插件
import router from './router'                                   // 必需要有 router

Vue.use(VnodeCache, {
  router,
  tabbar: ['/tabbar1', '/tabbar2', '/tabbar3', '/tabbar4'],     // 導航路由
  common: '/common'                                             // 公共頁面路由
})
複製代碼

路由配置

vue 路由文件 router.js

// tabBar 容器
import TabCon from '@/Components/TabCon/index'
Vue.use(Router)
// 按需配置,動態路由不須要配置入路由組
export default new Router({
  routes: [{
    path: '/',
    component: TabCon,
    redirect: '/tabbar1',
    children: [ {
      path: '/tabbar1',
      name: '/tabbar1',
      component: Movie
    }, {
      path: '/tabbar2',
      name: '/tabbar2',
      component: Singer
    }, {
      path: '/tabbar3',
      name: '/tabbar3',
      component: Rank
    }, {
      path: '/tabbar4',
      name: '/tabbar4',
      component: Song
    }]
  }, {
    path: '/common',
    name: '/common',
    component: Common
  }]
})
複製代碼

App.vue 配置

動態加載的路由和組件須要有動畫效果,並且是按需緩存,頁面點擊返回後銷燬組件,使用插件的緩存組件 vnode-cache

<template>
  <div id="app">
    <transition :name="transitionName" :css="!!direction">
      <vnode-cache>
        <router-view class="router-view"></router-view>
      </vnode-cache>
    </transition>
    <TabBar v-show="isTab"></TabBar>
  </div>
</template>
複製代碼
import TabBar from '@/ComponentsLayout/TabBar/index'
export default {
  name: 'App',              // 每一個組件建議帶上名字
  components: {
    TabBar
  },
  data () {
    return {
      transitionName: '',   // 切換效果類名
      direction: '',        // 前進仍是返回動做
      isTab: true           // 是否顯示 tabbar
    }
  },
  created () {
    // 監聽前進事件
    this.$direction.on('forward', (direction) => {
      this.transitionName = direction.transitionName
      this.direction = direction.type
      this.isTab = direction.isTab      
    })
    // 監聽返回事件
    this.$direction.on('reverse', (direction) => {
      this.transitionName = direction.transitionName
      this.direction = direction.type
      this.isTab = direction.isTab
    })
  }
}
複製代碼

TabBar 容器配置

TabBar 裏面的頁面須要一直被緩存下來,並不在按需緩存的效果中,並且切換也沒有滑動效果。這裏直接使用 keep-alive

<template>
  <div>
    <keep-alive>
      <router-view class="tab-router-view"></router-view>
    </keep-alive>
  </div>
</template>
複製代碼

複用組件配置

複用組件須要在 router.js 中進行配置

// 須要被複用的組件
import MovieDetail from '@/ComponentsDetails/MovieDetail/index'
import SingerDetail from '@/ComponentsDetails/SingerDetail/index'

// 每一個動態註冊的路由重複使用的組件
Router.prototype.extends = {
  MovieDetail,
  SingerDetail
}
複製代碼

跳轉到動態路由而且加載複用組件時候

methods: {
    goDetailMv (index, name) {  // 傳參
      // 建立一個新路由
      let newPath = `/movie/${index}`
      let newRoute = [{
        path: newPath,
        name: newPath,
        component: {extends: this.$router.extends.MovieDetail}
      }]
      // 判斷路由是否存在
      let find = this.$router.options.routes.findIndex(item => item.path === newPath)
      // 不存在 添加一個新路由
      if (find === -1) {
        this.$router.options.routes.push(newRoute[0])
        this.$router.addRoutes(newRoute)
      }
      // 而後跳轉
      this.$router.replace({    
        name: newPath,
        params: { id: index, name: name }
      })
    }
}
複製代碼

路由跳轉的方法

這是一個很嚴肅的問題。關係到整個效果切換在各個瀏覽器中的切換兼容。
複製代碼

一般 咱們都是使用 this.$router.push() 去跳轉,這跳轉方法會給 瀏覽器的 history 對象中添加記錄,因而瀏覽器的前進和後退按鈕就會生效,會在無心間產生一些錯誤的路由跳轉操做。 最典型的就是 safari 的側滑前進和返回功能,會影響整個切換的效果,偶爾會致使錯亂。

若是不使用 replace 方法而使用 push 的話就會產生 history 歷史記錄,瀏覽器的前進後退按鈕會生效。

解決方法就是 不使用 this.$router.push() 去作產生瀏覽器的 history 記錄。使用this.$router.replace() 這個方法去跳轉,不會給瀏覽器的 history中添加記錄,就不會有上面由於前進後退產生的問題。這樣就犧牲了部分瀏覽器的特性,可是在微信瀏覽器中就不會顯示底部兩個前進後退按鈕。這也是一種補償吧,大多數的移動網站在微信瀏覽器中出現的次數仍是比較多的。 固然沒有瀏覽器的後退按鈕,那麼返回功能就集中在應用中的後退按鈕上,如下是使用 this.$router.replace() 推薦的返回按鈕寫法。

<div class="back-btn">
  <div @click="back"></div>
</div>
複製代碼
methods: {
    back () {
      window.$VueAppEffect.paths.pop()
      this.$router.replace({
        name: window.$VueAppEffect.paths.concat([]).pop()  // 不影響原對象取到要返回的路由
      })
    }
}
複製代碼

在導航器中也推薦使用 replace 方式

<template>
  <div id="tab-bar">
    <div class="container border-half-top">
      <router-link class="bar" :to="'/movie'" replace>  <!--this.$router.replace() 聲明式寫法 -->
        <div class="button"></div>
      </router-link>
      <router-link class="bar" :to="'/singer'" replace> <!--this.$router.replace() 聲明式寫法 -->
        <div class="button"></div>
      </router-link>
      <router-link class="bar" :to="'/rank'" replace>   <!--this.$router.replace() 聲明式寫法 -->
        <div class="button"></div>
      </router-link>
      <router-link class="bar" :to="'/song'" replace>   <!--this.$router.replace() 聲明式寫法 -->
        <div class="button"></div>
      </router-link>
    </div>
  </div>
</template>
複製代碼

佈局結構配置

佈局結構直接影響切換效果。

佈局結構請參考 示例配置: Examples 中的 css 這裏就不寫了。

切換效果

能夠從新覆蓋一下樣式,可是類名不能改變

.vue-app-effect-out-enter-active,
.vue-app-effect-out-leave-active,
.vue-app-effect-in-enter-active,
.vue-app-effect-in-leave-active {
  will-change: transform;
  transition: all 500ms cubic-bezier(0.075, 0.82, 0.165, 1) ;
  bottom: 50px;
  top: 0;
  position: absolute;
  backface-visibility: hidden;
  perspective: 1000;
}
.vue-app-effect-out-enter {
  opacity: 0;
  transform: translate3d(-70%, 0, 0);
}
.vue-app-effect-out-leave-active {
  opacity: 0 ;
  transform: translate3d(70%, 0, 0);
}
.vue-app-effect-in-enter {
  opacity: 0;
  transform: translate3d(70%, 0, 0);
}
.vue-app-effect-in-leave-active {
  opacity: 0;
  transform: translate3d(-70%, 0, 0);
}
複製代碼

組件帶 name 的好處

能在開發工具中有效的顯示組件的 name

若是沒有 name 會顯示當前組件的文件名 例如:

├── Movie          
│   └── index.vue      // 組件
├── Singer          
│   └── index.vue      // 組件
複製代碼

那麼在開發工具中都會顯示爲 Index


如下部分是描述如何實現該效果

實現過程

問題一:須要一個存儲器來存儲當前加載的路由歷史記錄

方案:vux 的源碼中是經過 在 vuexstore 中註冊一個模塊,而後在 window.sessionStorage 中存儲數據記錄。在路由守衛 router.beforeEach() router.afterEach() 進行路由前進後退判斷, 而後經過 bus 進行事件提交狀態來動態的給 <transition> 組件添加一個css的過分效果。

解決:storewindow.sessionStorage 感受有些麻煩,這裏直接採用全局 window 對象在上面掛載一個狀態管理的對象 $VueAppEffect 用來存儲操做中產生的一些記錄。

設計路由存儲器

由於程序始終是運行在瀏覽器中,能夠直接在 window 對象上掛載一個對象便可,簡單方便。

window.$VueAppEffect = {
  '/movie/23':1,                    // 新增動態路由名稱,值爲層級
  // '/play':999999, // 公共組件,層級爲最高級。不計入 count 默認無
  'count':1,                        // 新增路由總量
  'paths':['/movie','/movie/23'],   // 給返回按鈕使用的路由記錄默認會將導航路由的期中一個添加在最前。
}
複製代碼

問題二:須要從新設計一個緩存組件,根據當前狀態動態緩存和銷燬組件。

解決: 實現一個相似於<keep-alive> 同樣功能的組件,該組件會根據操做記錄動態的銷燬和緩存內容。

抽象組件

這個東西的看起來跟組件同樣是一對標籤,可是它不會渲染出實際的 dom 經常使用的有兩個<keep-alive> <transition> 內部具體樣子大概是這樣的

name: '',
abstract: true,
props: {},
data() {
  return {}
},
computed: {},
methods: {},
created () {},
destroyed () {},
render () {}
複製代碼

抽象組件也有生命週期函數 可是沒有html部分和css部分,並且有一個render() 方法, 這個方法主要是返回一個處理結果。

VNode基類

關於這個看能夠看這篇文章 VNode基類

建立一個抽象組件

將組件單獨成一個文件,而後再創建一個index文件

├── src          
│   └── index.js            // 入口安裝文件
│   └── vnode-cache.js      // 組件文件
複製代碼

先創建 index.js

import VnodeCache from './vnode-cache'
export default {
  install: (Vue, {router, tabbar, common='' } = {}) => {
  // 判斷參數的完整性 必需要有 router 和導航路由配置數組
  if (!router || !tabbar) {
    console.error('vue-app-effect need options: router, tabbar')
    return
  }
  
  // 監聽頁面主動刷新,主動刷新等於從新載入 app
  window.addEventListener('load', () => {
    router.replace({path: '/'})
  })
  
  // 建立狀態記錄對象 
  window.$VueAppEffect = {
    'count':0,
    'paths':[]
  }
  // 若是有公共頁面再配置
  if(common){                                   
    window.$VueAppEffect[common] = 9999999
  }
  
  // 利用 bus 進行事件派發和監聽
  const bus = new Vue()
  
  /** * 判斷當前路由加載執行的方法爲 push 仍是 replace * 根據路由守衛 router.beforeEach() router.afterEach() 進行加載和 * 銷燬組件的判斷,而且使用 bus 進行發送加載和銷燬組件的事件派發 * 額外處理觸摸事件返回的內容 **/
  
  // 掛載 vnode-cache 組件
  Vue.component('vnode-cache', VnodeCache(bus, tabbar))
  Vue.direction = Vue.prototype.$direction = {
    on: (event, callback) => {
      bus.$on(event, callback)
    }
  }
}
複製代碼

而後實現路由守衛監測操做記錄(上面/* */ 註釋中的部分),判斷是不是加載或者返回,並經過 bus 進行事件派發。

// 處理路由當前的執行方法和 ios 側滑返回事件
let isPush = false
let endTime = Date.now()
let methods = ['push', 'go', 'replace', 'forward', 'back']
document.addEventListener('touchend', () => {
  endTime = Date.now()
})
methods.forEach(key => {
  let method = router[key].bind(router)
  router[key] = function (...args) {
    isPush = true
    method.apply(null, args)
  }
})
// 前進與後退判斷
router.beforeEach((to, from, next)=>{
  // 若是是外鏈直接跳轉
  if (/\/http/.test(to.path)) {
    window.location.href = to.path
    return
  }
  // 不是外鏈的狀況下
  let toIndex = Number(window.$VueAppEffect[to.path])       // 獲得去的路由層級
  let fromIndex = Number(window.$VueAppEffect[from.path])   // 獲得來的路由層級
  fromIndex = fromIndex ? fromIndex : 0
  // 進入新路由 判斷是否爲 tabBar
  let toIsTabBar = tabbar.findIndex(item => item === to.path)
  // 不是進入 tabBar 路由 --------------------------
  if (toIsTabBar === -1) {
    // 層級大於0 即非導航層級
    if (toIndex > 0) {
      // 判斷是否是返回
      if (toIndex > fromIndex) { // 不是返回
        bus.$emit('forward',{
            type:'forward',
            isTab:false,
            transitionName:'vue-app-effect-in'
        })
        window.$VueAppEffect.paths.push(to.path)
      } else {                  // 是返回
        // 判斷是不是ios左滑返回
        if (!isPush && (Date.now() - endTime) < 377) {  
          bus.$emit('reverse', { 
            type:'', 
            isTab:false, 
            transitionName:'vue-app-effect-out'
          })
        } else {
          bus.$emit('reverse', { 
            type:'reverse', 
            isTab:false, 
            transitionName:'vue-app-effect-out'
          })
        }
      }
    // 是返回
    } else {
      let count = ++ window.$VueAppEffect.count
      window.$VueAppEffect.count = count
      window.$VueAppEffect[to.path] = count
      bus.$emit('forward', { 
        type:'forward', 
        isTab:false, 
        transitionName:'vue-app-effect-in'
      })
      window.$VueAppEffect.paths.push(to.path)
    }
  // 是進入 tabbar 路由 ---------------------------------------
  } else {
    // 先刪除當前的 tabbar 路由
    window.$VueAppEffect.paths.pop()
    // 判斷是不是ios左滑返回
    if (!isPush && (Date.now() - endTime) < 377) {
      bus.$emit('reverse', { 
        type:'', 
        isTab:true, 
        transitionName:'vue-app-effect-out'
      })
    } else {
      bus.$emit('reverse', { 
        type:'reverse', 
        isTab:true, 
        transitionName:'vue-app-effect-out'
      })
    }
    window.$VueAppEffect.paths.push(to.path)
  }
  next()
})

router.afterEach(function () {
  isPush = false
})

// 掛載 vnode-cache 組件
複製代碼

最後實現 vnode-cache.js 這裏主要實現了 根據 bus 派發的事件主動銷燬組件。

export default (bus,tabbar) => {
  return {
    name: 'vnode-cache',
    abstract: true,
    props: {},
    data: () {
      return {
        routerLen: 0,       // 當前路由總量
        tabBar: tabbar,     // 導航路由數組
        route: {},          // 須要被監測的路由對象
        to: {},             // 當前跳轉的路由
        from: {},           // 上一個路由
        paths: []           // 記錄路由操做記錄數組
      }
    },
    // 檢測路由的變化,記錄上一個和當前路由並保存路由的全路徑作爲標識。
    watch: {                
      route (to, from) {
        this.to = to
        this.from = from
        let find = this.tabBar.findIndex(item => item === this.$route.fullPath)
        if (find === -1) {
          this.paths.push(to.fullPath)              // 不是tabbar就保存下來
          this.paths = [...new Set(this.paths)]     // 去重
        }
      }
    },
    // 建立緩存對象集
    created () {                                            
      this.cache = {}
      this.routerLen = this.$router.options.routes.length   // 保存 route 長度
      this.route = this.$route                              // 保存route
      this.to = this.$route                                 // 保存route
      bus.$on('reverse', () => { this.reverse() })          // 監聽返回事件並執行對應操做
    },
    // 組件被銷燬清除全部緩存
    destroyed () {                                          
      for (const key in this.cache) {
        const vnode = this.cache[key]
        vnode && vnode.componentInstance.$destroy()
      }
    },
    methods: {
      // 返回操做的時候清除上一個路由的組件緩存
      reverse () {
        let beforePath = this.paths.pop()
        let routes = this.$router.options.routes
        // 查詢是否是導航路由
        let isTabBar = this.tabBar.findIndex(item => item === this.$route.fullPath)
        // 查詢當前路由在路由列表中的位置
        let routerIndex = routes.findIndex(item => item.path === beforePath)
        // 當不是導航路由,而且不是默認配置路由 清除對應歷史記錄 
        if (isTabBar === -1 && routerIndex >= this.routerLen) {
          delete  window.$VueAppEffect[beforePath]
          window.$VueAppEffect.count -= 1
        }
        // 當不是導航的時候 刪除上一個緩存
        let key = isTabBar === -1 ? this.$route.fullPath : ''
        if (this.cache[key]) {
          this.cache[beforePath].componentInstance.$destroy()
          delete this.cache[beforePath]
        }
      }
    },
    // 緩存 vnode
    render () {
      this.router = this.$route 
      // 獲得 vnode
      const vnode = this.$slots.default ? this.$slots.default[0] : null
      // 若是 vnode 存在
      if (vnode) {
        // tabbar判斷若是是 直接保存/tab-bar
        let findTo = this.tabBar.findIndex(item => item === this.$route.fullPath)
        let key = findTo === -1 ? this.$route.fullPath : '/tab-bar'
        // 判斷是否緩存過了
        if (this.cache[key]) {
          vnode.componentInstance = this.cache[key].componentInstance
        } else {
          this.cache[key] = vnode
        }
        vnode.data.keepAlive = true
      }
      return vnode
    }
  }
}
複製代碼

最後是將 css 效果代碼直接打包進了 index.js 文件中,這裏偷了個懶,由於代碼不是不少,因此只有使用的是 js 動態建立 style 標籤的方式

// 插入 transition 效果文件 偷懶不用改打包文件---------------------
const CSS = ` .vue-app-effect-out-enter-active, .vue-app-effect-out-leave-active, .vue-app-effect-in-enter-active, .vue-app-effect-in-leave-active { will-change: transform; transition: all 500ms cubic-bezier(0.075, 0.82, 0.165, 1) ; bottom: 50px; top: 0; position: absolute; backface-visibility: hidden; perspective: 1000; } .vue-app-effect-out-enter { opacity: 0; transform: translate3d(-70%, 0, 0); } .vue-app-effect-out-leave-active { opacity: 0 ; transform: translate3d(70%, 0, 0); } .vue-app-effect-in-enter { opacity: 0; transform: translate3d(70%, 0, 0); } .vue-app-effect-in-leave-active { opacity: 0; transform: translate3d(-70%, 0, 0); }`
let head = document.head || document.getElementsByTagName('head')[0]
let style = document.createElement('style')
style.type = 'text/css'
if (style.styleSheet){ 
  style.styleSheet.cssText = CSS; 
}else { 
  style.appendChild(document.createTextNode(CSS))
} 
head.appendChild(style)
複製代碼

到這裏就結束了 關於瀏覽器前進後退等方式的處理已經再配置中寫出,這裏推薦幾款窗口滾動插件,更好的配合實現 app 的應用效果,下拉刷新,上拉加載等。

better-scroll 體積比較大,功能比較全,效果還好

vue-scroller

iscroll

總結

這個其實就是利用 路由守衛,和 bus 以及 自定義一個緩存組件進行動態管理路由的配合配置過程,作這個的目的也就是爲了提升單頁應用的用戶體驗度,特別是再微信瀏覽器中,ios 系統下操做 history 歷史記錄窗口底部會出現兩個箭頭。切換效果的實現 讓單頁應用更像 WebApp 。

相關文章
相關標籤/搜索