MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue Keep-Alive 如何结合路由实现页面缓存功能

2021-01-123.4k 阅读

Vue Keep - Alive 基础介绍

在 Vue 开发中,keep - alive 是一个内置组件,它的主要作用是在组件切换过程中,将被切换掉的组件保留在内存中,而不是销毁它们。这意味着当组件再次被切换回来时,不会重新创建组件实例,而是直接从内存中复用,从而提升性能和用户体验。

从本质上来说,keep - alive 是通过 Vue 的抽象组件特性实现的。它自身不会渲染成一个 DOM 元素,也不会出现在父组件链中。当 keep - alive 包裹的组件被切换时,keep - alive 会将这些组件实例缓存起来。

来看一个简单的示例:

<template>
  <div id="app">
    <keep - alive>
      <component :is="currentComponent"></component>
    </keep - alive>
    <button @click="changeComponent">切换组件</button>
  </div>
</template>

<script>
import ComponentA from './components/ComponentA.vue';
import ComponentB from './components/ComponentB.vue';

export default {
  data() {
    return {
      currentComponent: 'ComponentA'
    };
  },
  components: {
    ComponentA,
    ComponentB
  },
  methods: {
    changeComponent() {
      this.currentComponent = this.currentComponent === 'ComponentA'? 'ComponentB' : 'ComponentA';
    }
  }
};
</script>

在这个例子中,keep - alive 包裹了动态组件 component,通过点击按钮切换 currentComponent,被切换掉的组件会被缓存,再次切换回来时不会重新创建。

Keep - Alive 的生命周期变化

当组件被 keep - alive 包裹时,其生命周期会发生一些变化。原本在组件销毁时触发的 beforeDestroydestroyed 钩子函数不会再触发,取而代之的是 deactivated 钩子函数,当组件从 DOM 中移除但被缓存时会触发这个钩子函数。而当组件被重新激活,也就是从缓存中取出重新渲染到 DOM 中时,会触发 activated 钩子函数。

例如,对于被 keep - alive 包裹的组件 ComponentA

<template>
  <div>
    <p>这是 ComponentA</p>
  </div>
</template>

<script>
export default {
  activated() {
    console.log('ComponentA 被激活');
  },
  deactivated() {
    console.log('ComponentA 被缓存');
  }
};
</script>

这样我们就可以在 activated 钩子函数中执行一些组件重新激活时需要的操作,比如重新获取数据等;在 deactivated 钩子函数中执行一些缓存前的清理操作,如取消定时器等。

路由与页面缓存的关系

在单页面应用(SPA)中,路由控制着页面的切换。通常情况下,当我们通过路由切换页面时,原页面组件会被销毁,新页面组件会被创建。但在一些场景下,我们希望某些页面在切换离开后能够被缓存,再次访问时直接从缓存中恢复,这就需要结合 keep - alive 与路由来实现。

比如一个电商应用,用户浏览商品列表页后进入商品详情页,当用户再返回商品列表页时,如果商品列表页能被缓存,就可以避免重新加载商品数据,提高页面响应速度,提升用户体验。

结合路由实现页面缓存的方式

全局路由守卫结合 Keep - Alive

一种常见的方式是利用全局路由守卫来判断哪些页面需要被缓存。首先,在路由配置中,我们可以给需要缓存的路由添加一个自定义元信息,例如 keepAlive: true

import Vue from 'vue';
import Router from 'vue - router';
import Home from './views/Home.vue';
import About from './views/About.vue';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
      meta: { keepAlive: true }
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
});

然后,在全局路由守卫 beforeEach 中进行判断:

router.beforeEach((to, from, next) => {
  if (to.meta.keepAlive) {
    // 这里可以添加一些进入需要缓存页面的额外逻辑
    next();
  } else {
    next();
  }
});

接下来,在应用的主布局文件(通常是 App.vue)中,使用 keep - alive 结合动态组件来根据路由切换页面,并实现缓存功能。

<template>
  <div id="app">
    <keep - alive>
      <router - view v - if="$route.meta.keepAlive"></router - view>
    </keep - alive>
    <router - view v - if="!$route.meta.keepAlive"></router - view>
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

在这个例子中,当路由切换到带有 keepAlive: true 元信息的页面时,该页面组件会被 keep - alive 缓存;而没有这个元信息的页面组件则正常创建和销毁。

基于路由视图嵌套实现页面缓存

有时候,我们可能有更复杂的页面结构,需要对部分路由视图进行缓存。这可以通过路由视图的嵌套来实现。

假设我们有一个 Parent.vue 组件作为父级路由视图,其中包含两个子路由视图 Child1.vueChild2.vue,并且我们希望 Child1.vue 被缓存。 首先,路由配置如下:

const router = new VueRouter({
  routes: [
    {
      path: '/parent',
      component: Parent,
      children: [
        {
          path: 'child1',
          component: Child1,
          meta: { keepAlive: true }
        },
        {
          path: 'child2',
          component: Child2
        }
      ]
    }
  ]
});

然后,在 Parent.vue 中:

<template>
  <div>
    <keep - alive>
      <router - view v - if="$route.meta.keepAlive"></router - view>
    </keep - alive>
    <router - view v - if="!$route.meta.keepAlive"></router - view>
  </div>
</template>

<script>
export default {
  name: 'Parent'
};
</script>

这样,当在 Parent.vue 的子路由之间切换时,Child1.vue 会被缓存,而 Child2.vue 正常创建和销毁。

使用 include 和 exclude 属性

keep - alive 组件提供了 includeexclude 属性,通过这两个属性可以更灵活地控制哪些组件需要被缓存。include 属性指定只有名称匹配的组件会被缓存,exclude 属性指定名称匹配的组件不会被缓存。

在路由配置中,我们可以结合组件的 name 来使用。假设我们有 Page1.vuePage2.vue 两个组件,并且希望 Page1.vue 被缓存。

<template>
  <div id="app">
    <keep - alive :include="['Page1']">
      <router - view></router - view>
    </keep - alive>
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

Page1.vuePage2.vue 中,需要设置 name 属性:

<template>
  <div>
    <p>这是 Page1</p>
  </div>
</template>

<script>
export default {
  name: 'Page1'
};
</script>
<template>
  <div>
    <p>这是 Page2</p>
  </div>
</template>

<script>
export default {
  name: 'Page2'
};
</script>

这样,当路由切换到 Page1.vue 时,它会被缓存,而切换到 Page2.vue 时不会被缓存。

页面缓存中的数据处理

当页面被缓存后,其中的数据状态也会被保留。但在某些情况下,我们可能需要在页面重新激活时更新数据。例如,在商品列表页被缓存后,当用户再次进入该页面时,可能希望重新获取最新的商品数据。

我们可以利用 activated 钩子函数来实现数据更新。假设在 GoodsList.vue 组件中有一个获取商品列表数据的方法 fetchGoodsList

<template>
  <div>
    <ul>
      <li v - for="(good, index) in goodsList" :key="index">{{ good.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      goodsList: []
    };
  },
  activated() {
    this.fetchGoodsList();
  },
  methods: {
    async fetchGoodsList() {
      // 假设这里是异步获取商品列表数据的逻辑
      const response = await axios.get('/api/goodsList');
      this.goodsList = response.data;
    }
  }
};
</script>

这样,当 GoodsList.vue 组件被重新激活时,会调用 fetchGoodsList 方法重新获取最新的数据。

另外,在页面被缓存期间,如果有外部数据变化需要反映到缓存页面中,我们可以通过事件总线或者 Vuex 等状态管理工具来实现。

比如使用事件总线,在 GoodsList.vue 中监听一个自定义事件 goodsDataUpdated

<template>
  <div>
    <ul>
      <li v - for="(good, index) in goodsList" :key="index">{{ good.name }}</li>
    </ul>
  </div>
</template>

<script>
import Vue from 'vue';

export default {
  data() {
    return {
      goodsList: []
    };
  },
  created() {
    Vue.$on('goodsDataUpdated', () => {
      this.fetchGoodsList();
    });
  },
  methods: {
    async fetchGoodsList() {
      const response = await axios.get('/api/goodsList');
      this.goodsList = response.data;
    }
  }
};
</script>

然后在其他地方,当商品数据发生变化时,通过事件总线触发 goodsDataUpdated 事件:

Vue.$emit('goodsDataUpdated');

这样就可以在缓存页面不重新创建的情况下更新数据。

结合 Keep - Alive 和路由的性能优化

在实现页面缓存功能时,性能优化是一个重要的考虑因素。虽然 keep - alive 可以避免组件的重复创建和销毁,提升性能,但如果使用不当,也可能带来一些问题。

比如,如果缓存的组件过多,会占用较多的内存,导致应用性能下降。因此,我们需要根据实际业务场景合理控制缓存的组件数量。可以通过 exclude 属性排除一些不需要缓存的组件,或者在适当的时候手动清除缓存。

另外,对于一些数据量较大的组件,在缓存期间可以考虑对数据进行优化。例如,将一些复杂的数据结构进行简化,只保留必要的信息。当组件重新激活时,再根据需要重新构建完整的数据结构。

以一个图表展示组件为例,假设该组件需要大量的数据来绘制图表。在缓存期间,我们可以只保留图表的基本配置信息和少量关键数据点,当组件重新激活时,再根据需要重新获取完整的数据。

<template>
  <div>
    <chart - component :config="chartConfig" :data="chartData"></chart - component>
  </div>
</template>

<script>
export default {
  data() {
    return {
      chartConfig: {},
      chartData: []
    };
  },
  deactivated() {
    // 简化数据,只保留基本配置和少量关键数据
    this.chartData = this.chartData.slice(0, 5);
  },
  activated() {
    // 重新获取完整数据
    this.fetchFullChartData();
  },
  methods: {
    async fetchFullChartData() {
      const response = await axios.get('/api/chartData');
      this.chartData = response.data;
    }
  }
};
</script>

这样可以在缓存期间减少内存占用,同时在组件重新激活时快速恢复完整功能。

注意事项与常见问题解决

  1. 组件状态问题:有时候,在组件被缓存后,可能会出现状态显示异常的情况。这通常是由于在 activated 钩子函数中没有正确恢复组件状态导致的。例如,一个表单组件在缓存前填写了部分内容,再次激活时希望保持这些填写状态。我们需要在 activated 钩子函数中根据缓存的数据来恢复表单状态。
<template>
  <form>
    <input v - model="formData.name" type="text">
    <input v - model="formData.age" type="number">
  </form>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        name: '',
        age: 0
      }
    };
  },
  activated() {
    // 假设这里从缓存中获取数据并恢复表单状态
    const cachedData = this.getCachedFormData();
    this.formData.name = cachedData.name;
    this.formData.age = cachedData.age;
  },
  methods: {
    getCachedFormData() {
      // 实际逻辑中,这里可能从本地存储或者其他缓存机制中获取数据
      return {
        name: '默认名称',
        age: 18
      };
    }
  }
};
</script>
  1. 动态组件与缓存:如果使用动态组件结合 keep - alive,需要注意动态组件的 key 属性。如果 key 不正确,可能会导致组件无法正确缓存或复用。key 应该设置为能唯一标识组件的属性,例如路由的 path 或者组件的 name 等。
<template>
  <keep - alive>
    <component :is="currentComponent" :key="$route.path"></component>
  </keep - alive>
</template>

<script>
export default {
  data() {
    return {
      currentComponent: ''
    };
  },
  methods: {
    changeComponent() {
      // 切换组件逻辑
    }
  }
};
</script>
  1. 缓存清除问题:在某些情况下,我们可能需要手动清除缓存。例如,当用户进行了某些操作后,相关页面的缓存已经不再有效。我们可以通过 this.$destroy() 方法手动销毁组件实例来清除缓存。但需要注意的是,这种方式比较粗暴,可能会导致一些副作用,所以在使用时需要谨慎。
// 在某个方法中手动清除缓存
clearCache() {
  const componentInstance = this.$children.find(c => c.$options.name === 'ComponentToClear');
  if (componentInstance) {
    componentInstance.$destroy();
  }
}
  1. 路由参数变化与缓存:当路由参数发生变化时,默认情况下,被 keep - alive 缓存的组件不会感知到参数的变化。如果我们希望组件能响应路由参数的变化,可以通过 watch 监听 $route 的变化。
<template>
  <div>
    <p>路由参数:{{ $route.params.id }}</p>
  </div>
</template>

<script>
export default {
  watch: {
    $route(to, from) {
      // 这里处理参数变化后的逻辑,例如重新获取数据
      this.fetchData(to.params.id);
    }
  },
  methods: {
    async fetchData(id) {
      const response = await axios.get(`/api/data/${id}`);
      // 更新组件数据
    }
  }
};
</script>

通过以上详细的介绍和示例,我们可以全面了解如何在 Vue 中结合 keep - alive 和路由实现页面缓存功能,并且掌握在实际应用中可能遇到的问题及解决方法,从而开发出性能更优、用户体验更好的前端应用。