刷新后记忆上一次的查询参数、页面位置
需求
目前页面缓存机制是 keep-alive,即点击之前页签,页面不刷新。这会导致:数据不是最新的,即在页签 A 操作数据后,点击之前打开的页签 B,页签 B 的数据仍然是旧的。
需求:再次点击页签 B 时,根据之前的查询参数(包括页码)、树节点、屏幕高度(下文统称为“查询数据”)刷新页面,即实现静默刷新。
解决
第一步:封装 vuex
queryData 默认配置
封装 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
第二步:封装 mixin
作用:
- 避免重复请求;
- 存储查询参数和滚动条位置、跳转到上次的滚动位置;
- 提取公共部分,减少代码量。
vue 原本运作规则(使用 keep-alive 缓存的组件):
- 首次进入页面、或者使用 this.$tab.refreshPage() 刷新页面:触发 created 和 activated,造成重复请求;
- 切换页签再回到该页:触发 activated;
- 点击浏览器刷新按钮:触发 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 缓存的组件):
- 首次进入页面、或者使用
router.replace()
刷新页面:触发 created 和 activated,造成重复请求; - 切换页签再回到该页:触发 activated;
- 点击浏览器刷新按钮:触发 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最为节省性能,但是有个问题:首次打开页签,或者点击浏览器按钮刷新时后,页面上的输入框输入后,会出现延迟显示、输入卡顿。见此链接: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();
},
}
总结
- 在发送查询请求后,存储查询参数;
- 在跳转路由前,存储滚动轴位置。
不同路由的查询数据,怎样避免混淆
打开多个页签时,都要记忆各自的查询数据。因此要避免数据混淆。
如下图所示:queryParams
和 scrollY
的值都是对象,属性名为路由 path
,属性值为查询参数或滚动条位置。
删除页签,需要清除对应路由的查询数据
若依 $tab 对象 中关闭页签的方法,实际上是调用了 vuex 中 tagsView.js
中的方法来清除打开的页签。如下图所示:
因此,要在这一步清除对应路由的查询数据:
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 中找出两个数组不同的元素
参考链接
更新
查询数据中新增树信息:当前选中的树节点id、树展开的节点数组。
使用的vue页面:
本文来自博客园,作者:shayloyuki,转载请注明原文链接:https://www.cnblogs.com/shayloyuki/p/17970436
posted on 2024-02-18 14:58 shayloyuki 阅读(138) 评论(0) 编辑 收藏 举报