Имитация корзины семейства Vue, встроенный эффект переключения приложений и практика кэширования страниц

Vue.js Vuex vue-router

нужно

Одностраничное веб-приложение, сделанное ранее, немного туповата в эффекте переключения, а кеширование и обновление страницы не достигают ожидаемого эффекта в опыте. Хотя вьюkeep-aliveКомпонент может быть кэширован, но при выполнении некоторых особых требований, таких как кэширование вновь открытой страницы (компонента), когда вы нажимаете, чтобы вернуться, кэшированная страница (компонент) будет уничтожена, как и при имитации приложения. один и тот же компонент используется при открытии сведений о продукте, но используются разные маршруты. И продолжайте открывать сведения о продукте в сведениях о продукте. Кажется, что этот эффект недостижим при общей конфигурации маршрутизации и повторном использовании компонентов. И невозможно настроить все маршруты страницы сведений.

Некоторые проблемы.

Для достижения такого требования встречаются следующие проблемы.

  1. Имитирует эффект переключения приложений.
  2. Компоненты повторно используют динамическую интерфейсную маршрутизацию.
  3. Страницы (компоненты) кэшируются и уничтожаются по требованию.
  4. Кэшированные страницы (компоненты) для обновления данных.
  5. Влияние кнопок браузера вперед и назад на интерфейсную маршрутизацию.
  6. Влияние жестов промахов мобильных телефонов на передней маршрутизации.

В итоге такой эффект почти достигается, хотя и не идеальный.

в основном на основеvue vue-router

непосредственно используетсяvue-cliСделайте пример сборки файла

Этот подключаемый модуль имеет функции [Управление эффектами переключения], [Кэширование страниц по запросу] и [Управление динамической маршрутизацией].

Для достижения полного эффекта необходимо обратиться к образцу конфигурационного файла

Адрес плагина:vue-app-effect

Пример конфигурации:Examples

Пример демо:Demo

Я не буду размещать здесь рендеры, просто отсканируйте QR-код, чтобы увидеть реальную демонстрацию WeChat:

Не забудьте щелкнуть звездочкой, если считаете это полезным.

Руководство по настройке

Установить плагин

$ npm install vue-app-effect -S

Настроить плагин

входной файл vuemain.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'                                             // 公共页面路由
})

конфигурация маршрутизации

файл маршрутизации vuerouter.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Если вы добавите записи в , у вас не будет вышеперечисленных проблем, вызванных перемоткой вперед и назад. Это приносит в жертву функции некоторых браузеров, но две кнопки «вперед» и «назад» внизу не будут отображаться в браузере WeChat. Это тоже своеобразная компенсация: большинство мобильных сайтов чаще появляются в браузерах WeChat. Конечно, в браузере нет кнопки «Назад», поэтому функция «Назад» сосредоточена на кнопке «Назад» в приложении.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()  // 不影响原对象取到要返回的路由
      })
    }
}

Метод замены также рекомендуется в навигаторе

<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>

Конфигурация структуры макета

Структура макета напрямую влияет на эффект перехода.

Пожалуйста, обратитесь к примеру конфигурации для структуры макета:Examplescss в нем тут писаться не будет.

эффект переключения

Вы можете переопределить стиль, но имя класса изменить нельзя.

.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      // 组件

Затем он будет отображаться как индекс в инструментах разработки.


В следующем разделе описывается, как добиться этого эффекта.

Процесс реализации

Проблема 1: Нужна память для хранения текущей загруженной истории маршрутов

строить планы:существуетvuxв исходном коде передается вvuexизstoreзарегистрировать модуль вwindow.sessionStorageХранить записи данных в . охрана на маршрутеrouter.beforeEach() router.afterEach()Выполните маршрутизацию вперед и назад, а затем передайтеbusВыполните статус отправки события, чтобы динамически дать<transition>Компонент добавляет сверхэффект css.

решить:использоватьstoreа такжеwindow.sessionStorageЧувствуя себя немного хлопотно, вот прямое использование глобальногоwindowОбъект монтирует на него объект управления состоянием$VueAppEffectИспользуется для хранения некоторых записей, созданных во время операции.

Дизайн маршрутной памяти

Поскольку программа всегда запускается в браузере, вы можете напрямую монтировать объект на объект окна, что просто и удобно.

window.$VueAppEffect = {
  '/movie/23':1,                    // 新增动态路由名称,值为层级
  // '/play':999999,                // 公共组件,层级为最高级。不计入 count 默认无
  'count':1,                        // 新增路由总量
  'paths':['/movie','/movie/23'],   // 给返回按钮使用的路由记录默认会将导航路由的期中一个添加在最前。
}

Вопрос 2. Компонент кэша необходимо перепроектировать для динамического кэширования и уничтожения компонента в соответствии с текущим состоянием.

решить:реализовать<keep-alive>Компонент с той же функцией, компонент будет динамически уничтожать и кэшировать содержимое в соответствии с записью операции.

абстрактный компонент

Эта вещь выглядит как пара тегов, как компонент, но она не отображает фактический дом.Есть два часто используемых<keep-alive> <transition>Внутри выглядит вот так

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

Абстрактные компоненты также имеют функции жизненного цикла, но нет частей html и частей css, и естьrender()метод, этот метод в основном возвращает результат обработки.

Базовый класс VNode

Смотрите эту статью об этомБазовый класс VNode

Создайте абстрактный компонент

Разделите компоненты в файл, а затем создайте индексный файл.

├── 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)
    }
  }
}

Затем реализуйте запись операции мониторинга защиты маршрута (часть в комментарии /* */ выше), определите, загружается она или возвращается, и отправляйте события через шину.


// 处理路由当前的执行方法和 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. Здесь он в основном реализует активное уничтожение компонентов в соответствии с событиями, отправляемыми шиной.

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.

// 插入 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)

это заканчивается здесь Обработка браузера вперед и назад была написана в конфигурации.Вот несколько плагинов прокрутки окон, рекомендуемых для лучшего взаимодействия для достижения эффекта приложения приложения, обновления при выпадающем меню, загрузки подтягивания и т. д.

better-scrollОбъем относительно большой, функция относительно полная, а эффект хороший.

vue-scroller

iscroll

Суммировать

На самом деле это использование защиты маршрутизации и BUS, а также настраиваемых компонентов кэша для динамического управления процессами настройки маршрута. Это делается для улучшения взаимодействия с пользователем одностраничных приложений, особенно в браузере WeChat, системе iOS. Две стрелки появляются в в нижней части окна History History. Реализация эффекта хендовера позволяет одной странице быть более похожей на WebApp.