demand

Previous single-page WEB applications were a bit stiff in terms of switching effects, and the page caching and updating experience was not as good as expected. Although vue’s keep-alive can cache components, some special requirements, such as caching newly opened pages (components), will destroy the cached pages (components) when clicking back, just like the effect experienced in the App. And it uses the same component, different routes, for things like opening product details. And continue to open the product details in the product details. In general routing configuration and component reuse does not seem to achieve this effect. It is also impossible to configure all the detail page routes.

A couple of questions.

There are several problems with implementing such a requirement.

  1. Simulate the effect of APP switching.
  2. Components reuse dynamic front-end routing.
  3. Pages (components) are cached and destroyed on demand.
  4. Cached pages (components) for data updates.
  5. Impact of browser forward and back buttons on front-end routing.
  6. Influence of mobile phone sliding gesture on front-end routing.

It’s almost there, though not quite there.

It is mainly based on vue vue-Router

Use vue-CLI directly for sample file building

This plugin is the “control switch effect” and “cache pages on demand” and “dynamic route management” functions

The sample configuration file is also required for full effect

Plugin address: vue-app-effect

Example configuration: Examples

Example demonstration: Demo

Here will not put the effect map directly scan the TWO-DIMENSIONAL code to experience the real wechat demo:

Hit “Star” if you think it helps.

Configuration guide

Installing a plug-in

$ npm install vue-app-effect -S
Copy the code

Configure the plug-in

The vue entry file main.js configures the plugin to attach a vnode-cache component, which is used in the same way as keep-alive. A $VueAppEffect object is also attached to the Window object to store some records of the operation route.

import VnodeCache from 'vue-app-effect'                         // Import plug-ins
import router from './router'                                   // A router is required

Vue.use(VnodeCache, {
  router,
  tabbar: ['/tabbar1'.'/tabbar2'.'/tabbar3'.'/tabbar4'].// Navigation route
  common: '/common'                                             // Public page routing
})
Copy the code

The routing configuration

Vue routing file router.js

/ / tabBar container
import TabCon from '@/Components/TabCon/index'
Vue.use(Router)
// On-demand configuration. Dynamic routes do not need to be added to routing groups
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
  }]
})
Copy the code

App. Vue configuration

Dynamically loaded routes and components need to be animated, cached on demand, and destroyed after page click back, using the plug-in’s cache component 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>
Copy the code
import TabBar from '@/ComponentsLayout/TabBar/index'
export default {
  name: 'App'.// It is recommended to include a name for each component
  components: {
    TabBar
  },
  data () {
    return {
      transitionName: ' '.// Toggle effect class name
      direction: ' '.// Forward or return action
      isTab: true           // Whether to display tabbar
    }
  },
  created () {
    // Listen for forward events
    this.$direction.on('forward', (direction) => {
      this.transitionName = direction.transitionName
      this.direction = direction.type
      this.isTab = direction.isTab      
    })
    // Listen for return events
    this.$direction.on('reverse', (direction) => {
      this.transitionName = direction.transitionName
      this.direction = direction.type
      this.isTab = direction.isTab
    })
  }
}
Copy the code

TabBar container configuration

Pages in TabBar need to be cached all the time, not in the effect of on-demand caching, and the switch has no sliding effect. Here we use keep-alive directly

<template>
  <div>
    <keep-alive>
      <router-view class="tab-router-view"></router-view>
    </keep-alive>
  </div>
</template>
Copy the code

Reuse Component Configuration

Reuse components need to be configured in router.js

// Components that need to be reused
import MovieDetail from '@/ComponentsDetails/MovieDetail/index'
import SingerDetail from '@/ComponentsDetails/SingerDetail/index'

// Re-use components for each dynamically registered route
Router.prototype.extends = {
  MovieDetail,
  SingerDetail
}
Copy the code

Jump to dynamic routing and load reusable components

methods: {
    goDetailMv (index, name) {  / / the refs
      // Create a new route
      let newPath = `/movie/${index}`
      let newRoute = [{
        path: newPath,
        name: newPath,
        component: {extends: this.$router.extends.MovieDetail}
      }]
      // Check whether the route exists
      let find = this.$router.options.routes.findIndex(item= > item.path === newPath)
      // There is no new route to add
      if (find === - 1) {
        this.$router.options.routes.push(newRoute[0])
        this.$router.addRoutes(newRoute)
      }
      // Then jump
      this.$router.replace({    
        name: newPath,
        params: { id: index, name: name }
      })
    }
}
Copy the code

Route jump method

This is a serious problem. Related to the overall effect of switching in each browser switch compatibility.Copy the code

Usually we use this.$router.push() to jump. This method adds a record to the browser’s history object, and the browser’s forward and back buttons take effect, inadvertently generating false route jumps. The most typical is Safari’s sidesaddle forward and back, which can affect the overall effect of the switch and occasionally cause confusion.

If not usedreplaceMethod.pushThen it will producehistoryHistory, the browser’s forward and back buttons will take effect.

The solution is not to use this.$router.push() to generate the browser’s history. Use this.$router.replace() to jump, and no record will be added to the browser’s history. This sacrifices some of the browser’s features, but the bottom two forward and back buttons are not displayed in the wechat browser. This is also a kind of compensation, most mobile websites appear in the wechat browser more times. Of course, there is no browser back button, so the return function is focused on the back button in the app. Here’s how to write the back button using this.$router.replace().

<div class="back-btn">
  <div @click="back"></div>
</div>
Copy the code
methods: {
    back () {
      window.$VueAppEffect.paths.pop()
      this.$router.replace({
        name: window.$VueAppEffect.paths.concat([]).pop()  // Does not affect the original object to fetch the route to return}}})Copy the code

Replace is also recommended in the navigator

<template>
  <div id="tab-bar">
    <div class="container border-half-top">
      <router-link class="bar" :to="'/movie'" replace>  <! --this.$router. Replace () declarative -->
        <div class="button"></div>
      </router-link>
      <router-link class="bar" :to="'/singer'" replace> <! --this.$router. Replace () declarative -->
        <div class="button"></div>
      </router-link>
      <router-link class="bar" :to="'/rank'" replace>   <! --this.$router. Replace () declarative -->
        <div class="button"></div>
      </router-link>
      <router-link class="bar" :to="'/song'" replace>   <! --this.$router. Replace () declarative -->
        <div class="button"></div>
      </router-link>
    </div>
  </div>
</template>
Copy the code

Layout structure configuration

The layout structure directly affects the switching effect.

For the layout structure, see the CSS in Examples: Examples.

Switching effect

You can override the style, but the class name cannot be changed

.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); }Copy the code

The component withnameThe benefits of

The name of the component can be effectively displayed in the development tool

If there is no name, the name of the current component is displayed. Example:

├ ─ ─ Movie │ └ ─ ─ index. The vue / / component ├ ─ ─ Singer │ └ ─ ─ index. The vue / / componentCopy the code

Then in the development tool, it will be displayed as Index


The following section describes how to achieve this effect

The implementation process

Problem 1: A memory is required to store the currently loaded route history

Solution: in vUX source is by registering a module in vuex store, and then storing data records in window.sessionStorage. Router.beforeeach () router.aftereach () determines the route forward or backward, and dynamically adds a CSS transition effect to the
component via bus event submission status.

$VueAppEffect ($VueAppEffect); $VueAppEffect ($VueAppEffect); $VueAppEffect ($VueAppEffect);

Design routing memory

Since the program is always running in the browser, you can simply mount an object on the Window object.

window.$VueAppEffect = {
  '/movie/23':1.// Add the dynamic route name. The value is hierarchy
  // '/play':999999, // Public component, highest level. Does not count default none
  'count':1.// Total number of new routes
  'paths': ['/movie'.'/movie/23'].// The route record used for the back button by default will add the intermediate navigation route first.
}
Copy the code

Problem two: a cache component needs to be redesigned to dynamically cache and destroy components based on the current state.

Solution: Implement a component similar to

that will dynamically destroy and cache content based on action records.

Abstract component

This thing looks like a pair of tags like a component, but it doesn’t render the actual DOM. There are usually two

that look something like this inside

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

Abstract components also have lifecycle functions but no HTML or CSS parts, and have a render() method, which basically returns a processing result.

VNode base class

See this article for VNode base classes

Create an abstract component

Separate the component into a file, and then create an index file

├─ SRC │ ├─ ├─ ├─ vnode-cache.js // ├─ ├─ vnode-cache.js //Copy the code

To set up the index. Js

import VnodeCache from './vnode-cache'
export default {
  install: (Vue, {router, tabbar, common=' ' } = {}) = > {
  // The router and navigation route configuration arrays are required to determine parameter integrity
  if(! router || ! tabbar) {console.error('vue-app-effect need options: router, tabbar')
    return
  }
  
  // Listen for page active refresh, active refresh is equal to reload app
  window.addEventListener('load', () => {
    router.replace({path: '/'})})// Create a status record object
  window.$VueAppEffect = {
    'count':0.'paths':[]
  }
  // Reconfigure if there are public pages
  if(common){                                   
    window.$VueAppEffect[common] = 9999999
  }
  
  // Use bus for event dispatch and listening
  const bus = new Vue()
  
  /** * Determine whether the current route load method is push or replace * Load and * destroy components according to router guard router.beforeeach () router.aftereach () And bus is used to send events for loading and destroying components to distribute * additional processing of the content returned by touch events **/
  
  // Mount the vnode-cache component
  Vue.component('vnode-cache', VnodeCache(bus, tabbar))
  Vue.direction = Vue.prototype.$direction = {
    on: (event, callback) = > {
      bus.$on(event, callback)
    }
  }
}
Copy the code

Then implement the route guard to monitor the operation record (above /* */ comments), determine whether it is loaded or returned, and send events through bus.


// Handles the route's current execution method and the ios slide-back event
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)
  }
})
// Forward and backward judgment
router.beforeEach((to, from, next) = >{
  // If the link is external, jump directly
  if (/\/http/.test(to.path)) {
    window.location.href = to.path
    return
  }
  // Not in the case of external chain
  let toIndex = Number(window.$VueAppEffect[to.path])       // Get the routing hierarchy to go
  let fromIndex = Number(window.$VueAppEffect[from.path])   // The resulting routing hierarchy
  fromIndex = fromIndex ? fromIndex : 0
  // Enter the new route to determine whether it is tabBar
  let toIsTabBar = tabbar.findIndex(item= > item === to.path)
  // Instead of entering the tabBar route --------------------------
  if (toIsTabBar === - 1) {
    // A level greater than 0 is not a navigation level
    if (toIndex > 0) {
      // Determine whether to return
      if (toIndex > fromIndex) { // Not return
        bus.$emit('forward', {type:'forward'.isTab:false.transitionName:'vue-app-effect-in'
        })
        window.$VueAppEffect.paths.push(to.path)
      } else {                  / / is returned
        // Check if it is ios swipe left to return
        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'}}})/ / is returned
    } 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)
    }
  / / is entering the tabbar routing -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  } else {
    // Delete the current tabbar route
    window.$VueAppEffect.paths.pop()
    // Check if it is ios swipe left to return
    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
})

// Mount the vnode-cache component
Copy the code

Finally, vNode-cache. js is mainly implemented to actively destroy components according to the event distributed by bus.

export default (bus,tabbar) => {
  return {
    name: 'vnode-cache'.abstract: true.props: {},
    data: () {
      return {
        routerLen: 0.// Total number of current routes
        tabBar: tabbar,     // Navigate the route array
        route: {},          // The routing object to be monitored
        to: {},             // The current route
        from: {},           // Last route
        paths: []           // Record the routing operation record array}},// Detect route changes, record the last and current routes, and save the full path of the route as identification.
    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)              // Save if not tabbar
          this.paths = [...new Set(this.paths)]     / / to heavy}}},// Create a cache object set
    created () {                                            
      this.cache = {}
      this.routerLen = this.$router.options.routes.length   // Save the route length
      this.route = this.$route                              / / save the route
      this.to = this.$route                                 / / save the route
      bus.$on('reverse', () = > {this.reverse() })          // Listen for the return event and perform the corresponding action
    },
    // The component is destroyed to clear all caches
    destroyed () {                                          
      for (const key in this.cache) {
        const vnode = this.cache[key]
        vnode && vnode.componentInstance.$destroy()
      }
    },
    methods: {
      // Clear the component cache of the previous route when the return operation is performed
      reverse () {
        let beforePath = this.paths.pop()
        let routes = this.$router.options.routes
        // Query whether it is a navigation route
        let isTabBar = this.tabBar.findIndex(item= > item === this.$route.fullPath)
        // Query the position of the current route in the routing list
        let routerIndex = routes.findIndex(item= > item.path === beforePath)
        If the route is not a navigation route and is not configured by default, the history of the route is cleared
        if (isTabBar === - 1 && routerIndex >= this.routerLen) {
          delete  window.$VueAppEffect[beforePath]
          window.$VueAppEffect.count -= 1
        }
        // Delete the last cache when it is not navigation
        let key = isTabBar === - 1 ? this.$route.fullPath : ' '
        if (this.cache[key]) {
          this.cache[beforePath].componentInstance.$destroy()
          delete this.cache[beforePath]
        }
      }
    },
    / / the cache vnode
    render () {
      this.router = this.$route 
      / / get the vnode
      const vnode = this.$slots.default ? this.$slots.default[0] : null
      // If the vNode exists
      if (vnode) {
        // tabbar indicates whether to save /tab-bar directly
        let findTo = this.tabBar.findIndex(item= > item === this.$route.fullPath)
        let key = findTo === - 1 ? this.$route.fullPath : '/tab-bar'
        // Check whether the cache has been cached
        if (this.cache[key]) {
          vnode.componentInstance = this.cache[key].componentInstance
        } else {
          this.cache[key] = vnode
        }
        vnode.data.keepAlive = true
      }
      return vnode
    }
  }
}
Copy the code

Finally, the CSS effect code is packaged directly into the index.js file, which is a bit lazy, because there is not a lot of code, so only use js to create the style tag dynamically

/ / insert the transition effect files Lazy don't have to change packaging file -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
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)
Copy the code

Here is the end of the browser forward and backward processing has been written in the configuration, here recommend several window scrolling plug-in, better with the implementation of app application effect, pull down refresh, pull up loading, etc..

Better-scroll is relatively large in volume and has complete functions, so the effect is good

vue-scroller

iscroll

conclusion

In fact, this is the configuration process of using route guard, bus and a customized cache component to dynamically manage routes. The purpose of doing this isto improve the user experience of single-page application, especially in wechat browser, there will be two arrows at the bottom of the operation history window in ios system. The implementation of the toggle effect makes a single page application more like a WebApp.