前言

最近用iview-admin做后台管理系统,遇到了个问题,列表页面点击进入详情页面编辑,编辑完成自动跳转到列表页,需要页面重新刷新请求列表(后管就是这么简单粗暴),然并卵~,再次回到列表页页面不会重新刷新;问题很好解决,因为iview-admin的路由设置会默认缓存页面notCache:false,因此我直接设置notCache:true就ok了,页面可以重新刷新。但是我觉得这个问题简单也不简单;
vue-router的切换不同于传统的页面的切换,路由之间的切换,其实就是组件之间的切换,不是真正的页面切换。这也会导致一个问题,就是引用相同组件的时候,会导致该组件无法更新,也就是我们口中的页面无法更新的问题了,这是根源;
所以参考网上一些文章加上自己之前遇到过得一些相似问题小结一下;

针对不同的需求分为两部分:

一、需求:页面不刷新

场景:(vue-cli生成的项目)

  • 商品列表页点击进入详情页,再次返回列表页会重新请求接口数据,重新渲染DOM,同时如果有滚动条,滚动条返回顶部;需求: 列表页进入详情页,再次返回列表页不刷新,同时滚动条位置不变。
  • 同上;需求: 列表页进入编辑页,编辑完成返回列表页,不刷新全部列表,只针对该条数据进行刷新,同时滚动条位置不变。

分析问题:

  • 列表页进入详情页,再返回重新请求接口数据,重新渲染DOM,这样不仅白白浪费一次请求消耗性能,而且也会影响用户的体验,尤其是移动端项目。
  • 针对滚动条问题,用户浏览到下面列表出现滚动条后,再次返回滚动条回到最顶部,也很影响用户体验;

解决问题:

  • <keep-alive>容器组件,将页面缓存起来;<keep-alive>是Vue的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
  • 使用路由跳转时:beforeRouteLeave和beforeRouteEnter 来保存滚动条的位置;(beforeRouterLeave必须写在有配置路由的页面上才有效的)
  • 当编辑列表详情时,记录列表行的 index 和 ID,保存返回到列表页时用当前数据刷新列表行数据;

具体代码:

  • 在路由配置页(router.js)中设置页面是否缓存以为滚动条的位置(不一定是body的滚动条也可能是某一个容器的滚动条)
routes: [
	{
	    path: 'index/query',
	    component: ()=>import('@/components/data_center/xxx/index.vue'),
	    meta: {
	        keepAlive: true, // 是否缓存   
	        scollTopPosition: 0 // 滚动条位置
	    }
	}
]
  • 在整个页面框架(App.vue)中需要进行是否缓存的设置
<template>
  <div id="app">
    <keep-alive>
          <!-- 如果当前打开页面的路由中 keepAlive: true (开启了缓存时) -->
          <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
        
    <!-- 如果当前打开页面的路由中 没有 或者为 keepAlive: false (关闭缓存时[默认就是false]) -->
    <router-view v-if="!$route.meta.keepAlive"></router-view>
  </div>
</template>
  • 在product.vue页面中,添加beforeRouteEnter和beforeRouteLeave两个监听方法,在离开该页时记录scollTopPosition,当从详情页返回时,再把记录的位置赋给对应容器的滚动条
// 不!能!获取组件实例 `this`
beforeRouteEnter(to, from, next) {
    next(vm => {
          if (from.path === "xxx") {
            document.getElementById('home_query').scrollTop = to.meta.scollTopPosition;
          }
    });
},
beforeRouteLeave(to, from, next) {
    if(from.meta.keepAlive) {
         from.meta.scollTopPosition = document.getElementById('home_query').scrollTop;
    }
    next();
}
  • 当编辑列表页返回时,一般就是根据$index和id来用新数据刷新之前的旧数据;
vm.$set(vm.array,index,newValue)
vm.array.splice(index,1,newValue)

PS: 如何在vue中实时查看滚动条,滚动的位置的值?

mounted() {
    // 监听window中滚动条的位置
    window.addEventListener('scroll', function(){
     let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
      console.log(scrollTop);
    });
 
    // 监听指定某个元表滚动条的位置
    document.querySelector('#listBox').addEventListener('scroll', function(){
      let scrollTop = this.pageYOffset || this.scrollTop;
      console.log(scrollTop);
}

二、需求:页面刷新

在解决这个问题之前先说一下 <keep-alive>这个组件,其实在上面页面后退不刷新的解决方案里面会用这个组件也是最重要的;

keep-alive理解:
在平常开发中,有部分组件没有必要多次初始化,这时,我们需要将组件进行持久化,使组件的状态维持不变,在下一次展示时,也不会进行重新初始化组件。
也就是说,kee-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免DOM重新渲染 。也就是所谓的组件缓存。
开启keep-alive之后页面生命周期钩子函数执行顺序:页面第一次进入created—mounted—activated,退出时触发deactivated。当再次进入时,只触发activated;

场景:

  • 在路由配置里面设置了keep-alive的页面里面,返回页面或者再次this.route.push跳转时页面不刷新,一般我们用的vue-cli开发项目,会设置keep-alive,而如果用类似iview-admin这样类似的框架时,会有专门的配置方法配置路由缓存,比如:notCache
  • 同一路由携带不同参数,本质上是重用相同的组件实例,默认在跳转路由时会采用缓存策略,并不会刷新当前路由组件,因此不会调用组件的生命周期挂钩

解决问题:

  • 如果因为设置了keep-alive导致页面不刷新,可以针对该页面关闭keep-alive
meta: {
  keepAlive: false, // 是否缓存   
 }
 // iview-admin
  meta: {
    icon: 'md-apps',
    title: '动态详情',
    hideInMenu: true,
    notCache: true //关闭缓存
   },
  • 可以使用activated周期函数代替mounted函数,把列表页的请求接口的方法放到activated里面
  • 监听路由变化(不推荐、用户体验不好)

官方提供的解决方案为要对同一组件更改做出反应,监听$route的变化,或者使用使用2.2中引入的beforeRouteUpdate
导航卫士,调用对应的方法。

// 监控路由中的数据变化
watch: {
    $route(to, from) {
      if (this.$route.query.id) {
        console.log( "获取页面数据" );
      }
    }
},


// 或者使用beforeRouteUpdate 导航守卫监听路由变化
beforeRouteUpdate(to, from, next) {
    console.log(this.$route.query.id);
    if (this.$route.query.id) {
      console.info("获取页面数据");
    }
    next();
},

注意:

  1. 该方案可以在监听方法中完成在vue生命周期初始化过程中的业务逻辑,可以实现页面重新加载数据的效果,但是滚动条不会重置,因此需要对滚动条进行重置处理;
  2. 当页面刷新后,此时会认为路由并未发生改变,虽然此时watch中的$route或者beforeRouteUpdate 路由监听均视作无变化,但是会正常执行生命周期,因此不存在刷新出错的问题;
  3. 只有在这种情况下严格按照官网:对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,beforeRouteUpdate才起作用
  • 给路由添加唯一key

如果想强制刷新,可以在根路由上为其分配一个唯一key。采用$route.fullpath作为其唯一key。这样vue就回认为内部路由每个都是不同的路由,在跳转时便会强制刷新组件。

<!-- App.vue根组件代码 -->
<template>
  <div class="app">
      <div class="slide">
          <ul>
              <li><router-link to="/page1/freddy">freddy</router-link></li>	
              <li><router-link to="/page1/nick">nick</router-link></li>	
              <li><router-link to="/page1/mike">mike</router-link></li>	
          </ul>	
      </div>
      <div class="content">
      	   <router-view :key="key"></router-view>
      </div>
  </div>
</template>
 
<script>
    export default{
	data(){
	    return {}
	},
	computed:{
	    key(){
	        return this.$route.path + Math.random();
	    }
	}
    }
</script>

这时候路由就会更新了。不过这也就意味着需要把每个都绑定一个key值。如果我从page1跳到page2不同组件的话,我其实是不用担心组件更新问题的。

  • 使用provide和inject结合使用 - 利用v-if原理重载路由

<router-view v-if="routerAlive"></router-view>增加一个不同v-if值,来先摧毁,然后再重新创建起到刷新页面的效果。

<!-- App.vue根组件代码 -->
<template>
  <div class="app">
      <div class="slide">
          <ul>
              <li><router-link to="/page1/freddy" @click.native="routerRefresh">freddy</router-link></li>	
              <li><router-link to="/page1/nick" @click.native="routerRefresh">nick</router-link></li>	
              <li><router-link to="/page1/mike" @click.native="routerRefresh">mike</router-link></li>	
          </ul>	
      </div>
      <div class="content">
      	   <router-view v-if="routerAlive"></router-view>
      </div>
  </div>
</template>
 
<script>
    export default{
	data(){
	    return {
		routerAlive:true
	    }
	},
	methods:{
	    routerRefresh(){
		this.routerAlive = false;
		this.$nextTick(()=>{
		    this.routerAlive = true;
	        });
	    }
	}
    }
</script>
  1. 因为router-link组件有取消点击事件,这里的.native就是为了触发组件原生标签中的事件。
  2. this.$nextTick(()=>{}) 的用法是等this.routerAlive = false; 触发后再执行 this.routerAlive = true; 从而起到摧毁再创建的效果。

provide和inject

<!-- App.vue根组件代码 -->
<template>
  <div class="app">
      <div class="slide">
          <ul>
              <li><router-link to="/page1/freddy" >freddy</router-link></li>	
              <li><router-link to="/page1/nick" >nick</router-link></li>	
              <li><router-link to="/page1/mike" >mike</router-link></li>	
          </ul>	
      </div>
      <div class="content">
      	   <router-view v-if="routerAlive"></router-view>
      </div>
  </div>
</template>
 
<script>
    export default{
	data(){
	    return {
		routerAlive:true
	    }
	},
	provide(){    //在父组件中创建属性
            return {
                routerRefresh: this.routerRefresh
            }
        },
	methods:{
	    routerRefresh(){
	        this.routerAlive = false;
		this.$nextTick(()=>{
		    this.routerAlive = true;
		});
	    }
	}
    }
</script>
<!-- 组件代码 -->
<template>
    <div class="page-1">
	    名字:<input type="text" v-model="value"><br/>
	    <button @click="linkToNick1">跳转到nick,不刷新路由</button>
	    <button @click="linkToNick2">跳转到nick,并刷新路由</button>
	    <br/>
	    <button @click="linkToSelf1">跳转到本身,不刷新路由</button>
	    <button @click="linkToSelf2">刷新本身</button>
    </div>
</template>
<script type="text/javascript">
    export default {
	name:'page1',
	inject:['routerRefresh'],   //在子组件中注入在父组件中出创建的属性
	mounted(){
	    this.value = this.$route.params.name;
	},
	data(){
	    return {
	        value:''
	    }
	},
	methods:{
	    linkToNick1(){
		this.$router.push('/page1/nick');
	    },
	    linkToSelf1(){
		this.$router.push('/page1/freddy');
	    },
	    linkToNick2(){
		this.$router.push('/page1/nick');
		this.routerRefresh();
	    },
	    linkToSelf2(){
		this.routerRefresh();
	    }
	}
    }
</script>

三、总结

  1. 针对路由跳转的问题还需要分清是同一路由,还是同一路由不同参数,还是不同路由。
  2. 对keep-alive的理解使用;
  3. 路由回退最好用this.$router.go(-1)this.$router.back()
  4. keep-alive只对SPA页面有用,传统的多页面不起作用。

本文参考:参考