shayloyuki

科技是第一生产力

 

刷新后记忆上一次的查询参数、页面位置

需求

目前页面缓存机制是 keep-alive,即点击之前页签,页面不刷新。这会导致:数据不是最新的,即在页签 A 操作数据后,点击之前打开的页签 B,页签 B 的数据仍然是旧的。

需求:再次点击页签 B 时,根据之前的查询参数(包括页码)、树节点、屏幕高度(下文统称为“查询数据”)刷新页面,即实现静默刷新。

解决

第一步:封装 vuex

queryData 默认配置

image

封装 queryData.js 模块

@/store/modules/queryData.js
import defaultQuery from "@/queryData";

const { queryParams, scrollY } = defaultQuery;

const storageQuery = JSON.parse(localStorage.getItem("query-data")) || "";

const state = {
  queryParams: storageQuery.queryParams || queryParams,
  scrollY: storageQuery.scrollY || scrollY,
};

const mutations = {
  CHANGE_QUERY: (state, { key, value }) => {
    if (state.hasOwnProperty(key)) {
      state[key] = { ...state[key], ...value };
    }
  },
  DEL_QUERY: (state, path) => {
    Object.keys(state).forEach((key) => {
      if (state[key].hasOwnProperty([path])) {
        delete state[key][path];
      }
    });
  },
  DEL_ALL_QUERY: (state) => {
    state = {};
  },
  DEL_OTHER_QUERY: (state, path) => {
    Object.keys(state).forEach((key) => {
      state[key] = { [path]: state[key][path] };
    });
  },
};

const actions = {
  // 修改查询数据
  changeQuery({ commit }, data) {
    commit("CHANGE_QUERY", data);
  },
  // 删除某个页签的查询数据
  delQuery({ commit }, path) {
    commit("DEL_QUERY", path);
  },
  // 删除所有页签的查询数据
  delAllQuery({ commit }) {
    commit("DEL_ALL_QUERY");
  },
  // 删除除了该页签外的,其他页签的查询数据
  delOtherQuery({ commit }, path) {
    commit("DEL_OTHER_QUERY", path);
  },
  // 删除该页签以左/右的页签的查询数据
  delDirectionQuery({ commit, rootGetters }) {
    const queryPaths = Object.keys(state.scrollY);
    const remainPaths = rootGetters.visitedViews.map((e) => e.path);
    const needDelPaths = queryPaths.filter((ele) => !remainPaths.includes(ele));
    if (remainPaths.length === 1) {
      commit("DEL_OTHER_QUERY", remainPaths[0]);
    } else if (needDelPaths.length) {
      needDelPaths.forEach((e) => {
        commit("DEL_QUERY", e);
      });
    }
  },
};

export default {
  namespaced: true,
  state,
  mutations,
  actions,
};

queryData.js 引入 @/store/index.js

image


第二步:封装 mixin

作用:

  1. 避免重复请求;
  2. 存储查询参数和滚动条位置、跳转到上次的滚动位置;
  3. 提取公共部分,减少代码量。

vue 原本运作规则(使用 keep-alive 缓存的组件):

  1. 首次进入页面、或者使用 this.$tab.refreshPage() 刷新页面:触发 created 和 activated,造成重复请求;
  2. 切换页签再回到该页:触发 activated;
  3. 点击浏览器刷新按钮:触发 created,不会触发 activated;

解决:让情形1 变为:触发 created,不触发 activated。

使用方法:
使用本 mixin 的组件,create 钩子中调用 createdFn,activated 钩子中调用 activatedFn,且必须要有 initData() 方法加载数据(内部加载数据完毕后,要调用 saveQueryParamsFn 存储查询参数)。

封装 initDataMixin.js
// @/mixins/initDataMixin.js
const initDataMixin = {
  data() {
    return {
      // 判断是否是第一次进入页面
      isFirstEnter: false,
    };
  },
  computed: {
    path() {
      return this.$route.path;
    },
    storeQueryParams() {
      return this.$store.state.queryData.queryParams[this.path] || {};
    },
  },
  deactivated() {
    this.isFirstEnter = false;
  },
  // 跳转路由之前,存储滚动条位置
  beforeRouteLeave(to, from, next) {
    this.$store.dispatch("queryData/changeQuery", {
      key: "scrollY",
      value: {
        [from.path]: window.scrollY,
      },
    });
    next();
  },
  methods: {
    // 供使用本 mixin 的组件的 created 钩子中调用
    createdFn() {
      this.isFirstEnter = true;
      this.initDataPage();
    },
    // 供使用本 mixin 的组件的 activated 钩子中调用
    activatedFn() {
      if (!this.isFirstEnter) {
        this.initDataPage();
      }
      this.isFirstEnter = false;
    },
    // 供使用本 mixin 的组件:必须有 initData() 方法加载数据
    async initDataPage() {
      await this.initData();
      // 跳转到上次的滚动位置
      window.scrollTo(0, this.$store.state.queryData.scrollY[this.path] || 0);
    },
    // 保存查询参数
    saveQueryParamsFn() {
      this.$store.dispatch("queryData/changeQuery", {
        key: "queryParams",
        value: {
          [this.path]: this.queryParams,
        },
      });
    },
  },
};

export default initDataMixin;

第三步:页面中引入 initDataMixin

pageA 页面读取查询参数
<template>
<!-- 模板代码 -->
</template>

<script>
import initDataMixin from "@/mixins/initDataMixin.js";

export default {
	name: 'PageA',
	mixins: [initDataMixin],
	computed: {
		// 查询参数
		queryParams: {
		  get() {
		    // this.storeQueryParams 是 initDataMixin 中的计算属性
			if (JSON.stringify(this.storeQueryParams) === "{}") {
			  return {
				pageNum: 1,
				pageSize: 14,
				projectName: "",
			  };
			} else return this.storeQueryParams;
		  },
		  set(val) {
			this.$store.dispatch("queryData/changeQuery", {
			  key: "queryParams",
			  value: {
			    // this.path 是 initDataMixin 中的计算属性路由地址
				[this.path]: val,
			  },
			});
		  },
		}
	},
	created() {
		// this.createdFn() 是 initDataMixin 中的方法,会调用 本组件的 initData() 方法
		this.createdFn();
	},
	activated() {
		// this.activatedFn() 是 initDataMixin 中的方法,会调用 本组件的 initData() 方法
		this.activatedFn();
	},
	methods: {
		// 根据 initDataMixin,本组件必须要有 initData() 方法
		initData() {
			this.loadPage();
		},
		// 加载页面:查询数据
		loadPage() {
			this.loading = true;
			project.myProject(this.queryParams).then((response) => {
				this.projectList = response.rows;
				this.total = response.total;
				this.loading = false;
			});
			// this.saveQueryParamsFn() 是 initDataMixin 中的方法,作用是存储查询参数
			this.saveQueryParamsFn();
		}
	}
}
</script>

注意点

刷新怎样避免重复请求

对于使用了 keep-alive 缓存的页面,若 created 和 activated 中都请求了数据,那么刷新时存在二次重复请求的问题。如下所示:

vue 原本运作规则(使用 keep-alive 缓存的组件):

  1. 首次进入页面、或者使用 router.replace() 刷新页面:触发 created 和 activated,造成重复请求;
  2. 切换页签再回到该页:触发 activated;
  3. 点击浏览器刷新按钮:触发 created,不会触发 activated;

解决方法参考链接:https://github.com/PanJiaChen/vue-element-admin/issues/3620

这里封装 initDataMixin.js就是为了解决刷新时发送两次请求的问题。用 mixin 全局混入,减少代码量。定义 isFirstEnter 判断是否是第一次进入页面:

    // 供使用本 mixin 的组件的 created 钩子中调用
    createdFn() {
      this.isFirstEnter = true;
      this.initDataPage();
    },
    // 供使用本 mixin 的组件的 activated 钩子中调用
    activatedFn() {
      if (!this.isFirstEnter) {
        this.initDataPage();
      }
      this.isFirstEnter = false;
    },

怎样跳转到之前的滚动轴位置

根据 scrollBehavior 可实现:记住之前的滚动轴位置。

但是 savedPosition 参数只适用于点击浏览器的前进/后退按钮,对于此处切换页签不适用。

因此,需要在跳转路由前存储滚动轴位置。

在什么时机存储查询数据

存储查询数据的时机

有两个时机:

  1. 离开本页签时(即点击其他页签时);
  2. 发送查询请求后

时机1最为节省性能,但是有个问题:首次打开页签,或者点击浏览器按钮刷新时后,页面上的输入框输入后,会出现延迟显示、输入卡顿。见此链接:bug记录:输入框延迟、卡顿

因此,要采用时机2:在发送查询请求后,存储查询参数。如下所示:

  /* === initDataMixin.js === */
  beforeRouteLeave(to, from, next) {
    // 跳转路由之前,存储滚动条位置
    this.$store.dispatch("queryData/changeQuery", {
      key: "scrollY",
      value: {
        [from.path]: window.scrollY,
      },
    });
    next();
  },
  methods: {
    // 保存查询参数
    saveQueryParamsFn() {
      this.$store.dispatch("queryData/changeQuery", {
        key: "queryParams",
        value: {
          [this.path]: this.queryParams,
        },
      });
    },
  },
/* === 使用 initDataMixin 的 vue 页面  === */
methods: {
    // 根据 initDataMixin,必须要有 initData()
    initData() {
      this.loadPage();
    },
    // 加载页面
    loadPage() {
      // 请求查询接口数据
      // ……
      // 存储查询参数
      this.saveQueryParamsFn();
    },
}

总结

  1. 在发送查询请求后,存储查询参数;
  2. 在跳转路由前,存储滚动轴位置。

不同路由的查询数据,怎样避免混淆

打开多个页签时,都要记忆各自的查询数据。因此要避免数据混淆。

如下图所示:queryParamsscrollY 的值都是对象,属性名为路由 path,属性值为查询参数或滚动条位置。

image

image

删除页签,需要清除对应路由的查询数据

若依 $tab 对象 中关闭页签的方法,实际上是调用了 vuex 中 tagsView.js 中的方法来清除打开的页签。如下图所示:

image

因此,要在这一步清除对应路由的查询数据:

image

src/store/modules/queryData.js
import defaultQuery from "@/queryData";

const { queryParams, scrollY } = defaultQuery;

const storageQuery = JSON.parse(localStorage.getItem("query-data")) || "";

const state = {
  queryParams: storageQuery.queryParams || queryParams,
  scrollY: storageQuery.scrollY || scrollY,
};

const mutations = {
  CHANGE_QUERY: (state, { key, value }) => {
    if (state.hasOwnProperty(key)) {
      state[key] = { ...state[key], ...value };
    }
  },
  DEL_QUERY: (state, path) => {
    Object.keys(state).forEach((key) => {
      if (state[key].hasOwnProperty([path])) {
        delete state[key][path];
      }
    });
  },
  DEL_ALL_QUERY: (state) => {
    state = {};
  },
  DEL_OTHER_QUERY: (state, path) => {
    Object.keys(state).forEach((key) => {
      state[key] = { [path]: state[key][path] };
    });
  },
};

const actions = {
  // 修改查询数据
  changeQuery({ commit }, data) {
    commit("CHANGE_QUERY", data);
  },
  // 删除某个页签的查询数据
  delQuery({ commit }, path) {
    commit("DEL_QUERY", path);
  },
  // 删除所有页签的查询数据
  delAllQuery({ commit }) {
    commit("DEL_ALL_QUERY");
  },
  // 删除除了该页签外的,其他页签的查询数据
  delOtherQuery({ commit }, path) {
    commit("DEL_OTHER_QUERY", path);
  },
  // 删除该页签以左/右的页签的查询数据
  delDirectionQuery({ commit, rootGetters }) {
    const queryPaths = Object.keys(state.scrollY);
    const remainPaths = rootGetters.visitedViews.map((e) => e.path);
    const needDelPaths = queryPaths.filter((ele) => !remainPaths.includes(ele));
    if (remainPaths.length === 1) {
      commit("DEL_OTHER_QUERY", remainPaths[0]);
    } else if (needDelPaths.length) {
      needDelPaths.forEach((e) => {
        commit("DEL_QUERY", e);
      });
    }
  },
};

export default {
  namespaced: true,
  state,
  mutations,
  actions,
};

vuex 中不同模块如何相互调用

在模块 A 中调用模块 B 的 方法:

const actions = {
  delCachedView({ commit, state, dispatch }, view) {
    return new Promise((resolve) => {
      commit("DEL_CACHED_VIEW", view);
      // 调用另一个模块,清除关闭页签的查询数据
      dispatch("queryData/delQuery", view.path, { root: true });
      resolve([...state.cachedViews]);
    });
  },
}

在模块 B 中获取模块 A 的 state:

const actions = {
  // 删除该页签以左/右的页签的查询数据
  delDirectionQuery({ commit, rootGetters }) {
    const queryPaths = Object.keys(state.scrollY);
    const remainPaths = rootGetters.visitedViews.map((e) => e.path);
    const needDelPaths = queryPaths.filter((ele) => !remainPaths.includes(ele));
    if (remainPaths.length === 1) {
      commit("DEL_OTHER_QUERY", remainPaths[0]);
    } else if (needDelPaths.length) {
      needDelPaths.forEach((e) => {
        commit("DEL_QUERY", e);
      });
    }
  },
}

总结

rootState 获取其他模块 state

rootGetters获取其他模块 getter

参考 vuex 中各个模块间互相调用 actions、mutations、state

mutation 中怎样使用 commit 和 dispatch

const mutations = {
  DEL_QUERY: (state, path) => {
    Object.keys(state).forEach((key) => {
      if (state[key].hasOwnProperty([path])) {
        delete state[key][path];
      }
    });
  },
  DEL_OTHER_QUERY: (state, path) => {
    Object.keys(state).forEach((key) => {
      state[key] = { [path]: state[key][path] };
    });
  },
  DEL_DIRECTION_QUERY: (state, path) => {
	// 根据条件判断,若符合则调用 commit("DEL_QUERY", path) 或者 dispatch("delQuery", path)
	// ……
	// 否则,调用 commit("DEL_OTHER_QUERY", path) 或者 dispatch("delOtherQuery", path)
	// ……
  },
};

const actions = {
  // 删除某个页签的查询数据
  delQuery({ commit }, path) {
    commit("DEL_QUERY", path);
  },
  // 删除除了该页签外的,其他页签的查询数据
  delOtherQuery({ commit }, path) {
    commit("DEL_OTHER_QUERY", path);
  },
    // 删除该页签以左/右的页签的查询数据
  delDirectionQuery({ commit }, path) {
    commit("DEL_DIRECTION_QUERY", path);
  },
}

但是 mutations 中不能 commit 和 dispatch。因此,可以把分条件处理移到 actions中,如下图所示:

const mutations = {
  DEL_QUERY: (state, path) => {
    Object.keys(state).forEach((key) => {
      if (state[key].hasOwnProperty([path])) {
        delete state[key][path];
      }
    });
  },
  DEL_OTHER_QUERY: (state, path) => {
    Object.keys(state).forEach((key) => {
      state[key] = { [path]: state[key][path] };
    });
  },
};

const actions = {
  // 删除某个页签的查询数据
  delQuery({ commit }, path) {
    commit("DEL_QUERY", path);
  },
  // 删除除了该页签外的,其他页签的查询数据
  delOtherQuery({ commit }, path) {
    commit("DEL_OTHER_QUERY", path);
  },
  // 删除该页签以左/右的页签的查询数据
  delDirectionQuery({ commit, rootGetters }) {
    const queryPaths = Object.keys(state.scrollY);
    const remainPaths = rootGetters.visitedViews.map((e) => e.path);
    const needDelPaths = queryPaths.filter((ele) => !remainPaths.includes(ele));
    if (remainPaths.length === 1) {
      commit("DEL_OTHER_QUERY", remainPaths[0]);
    } else if (needDelPaths.length) {
      needDelPaths.forEach((e) => {
        commit("DEL_QUERY", e);
      });
    }
  },
}

vuex 中 state 直接赋值无效

const mutations = {
  DEL_OTHER_QUERY: (state, path) => {
    // const obj = {
    //   queryParams: {
    //     [path]: state.queryParams[path],
    //   },
    //   scrollY: {
    //     [path]: state.scrollY[path],
    //   },
    // };
    // console.log({ obj });
    // state = obj; // 直接赋值无效
    Object.keys(state).forEach((key) => {
      state[key] = { [path]: state[key][path] };
    });
  },
};

js 中找出两个数组不同的元素

参考链接

  1. js找出两个数组不同的元素
  2. 操作删除对象中的某一个key键
  3. Vue 中 route.path 和 route.fullPath 的区别
  4. LocalStorage 怎么清除

更新

查询数据中新增树信息:当前选中的树节点id、树展开的节点数组。

image

image

image

使用的vue页面:

image

image

posted on 2024-02-18 14:58  shayloyuki  阅读(138)  评论(0编辑  收藏  举报

导航