问题描述
在电商项目中,通过商品列表进入商品详情是很常见的一个业务场景,在H5的开发过程中,有以下两种情况:
-
第一种: 列表页面为H5页面,商品详情页是app端原生页,通过jsbridge交互而进入商品详情
-
第二种:列表页面和商品详情页都是H5页面,我们的技术栈是用vue去实现的,通过vue-router进入详情的H5
在以上两种场景中,第一种模式并不需要我们去做额外的一些处理,直接返回webview,H5页面在不做页面可见性事件的干预下,不会做任何刷新;
第二种场景,则有点区别,通过vue- router跳转,然后通过浏览器的回退事件返回到列表的,列表页会重新走生命周期,并且自动定位到顶部;
解决思路和代码
vue项目的解决方案,自然是通过keep-alive 缓存页面数据,结合scrollBehavior,定位到页面滚动位置;具体处理代码如下:
示例代码为vue3 的代码
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "Home",
component: () => import(/* webpackChunkName: "Home" */ "../views/Home.vue"),
meta: {
title: "列表",
keepAlive: true
}
},
// 商品详情
{
path: "/productDetail",
name: "ProductDetail",
component: () =>
import(/* webpackChunkName: "Home" */ "../views/productDetail.vue"),
meta: {
title: "详情",
showNavigationbar: false,
keepAlive: false
}
}
];
function scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
}
if (from.meta.keepAlive) {
from.meta.savedPosition = document.body.scrollTop // 此处为记录的滚动条位置
}
from.meta.savedPosition = document.body.scrollTop
return { x: 0, y: to.meta.savedPosition || 0 }
}
const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior
});
export default router;
app.vue
// 使用keep-alive 包裹路由组件
<template>
<router-view v-slot="{ Component }">
<keep-alive>
<<img src="component" alt="" width="50%" />
:is="Component"
:key="$route.name"
v-if="$route.meta.keepAlive"
/>
</keep-alive>
<component
:is="Component"
:key="$route.name"
v-if="!$route.meta.keepAlive"
/>
</router-view>
</template>
存在的问题
以上则可以解决列表页面的定位问题,但是有一点需要注意的是就是,这里的scrollBehavior更改的页面document的滚动条的高度;
而我们的页面布局是下面这样的:
如果要将页面的滚动提升到document,那么顶部和底部的两块fix的区域则需要使用,position: fixed; 定位在页面上;而fixed的布局在IOS会存在很多滚动上的体验问题;
最终方案
根据上面的解决思路,在不改变页面布局的情况下,最终使用keep-alive 缓存页面,scrollBehavior,控制的是documen的滚动,如果需要进行页面内部某一个块的滚动,则需要结合element.scrollTop,(⚠️:element.scrollTop在12.2机型上不生效,需要引入对应的polyfill)
具体步骤:
- 使用keep-alive 缓存页面
- 引入scrollTo 的polyfill
- 在点击商品进入商品详情的位置,记录当前点击的位置,并缓存在本地,然后在onActivated中去除这个位置,并将相关的scroll 的容器滚动到这个位置;
- 高度位置的数据使用完成之后,需要清除本地的数据;
完整代码
route.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "Home",
component: () => import(/* webpackChunkName: "Home" */ "../views/Home.vue"),
meta: {
title: "列表",
keepAlive: true
}
},
// 商品详情
{
path: "/productDetail",
name: "ProductDetail",
component: () =>
import(/* webpackChunkName: "Home" */ "../views/productDetail.vue"),
meta: {
title: "商祥",
keepAlive: false
}
}
];
const router = createRouter({
history: createWebHashHistory(),
routes
});
export default router;
app.vue
<template>
<router-view v-slot="{ Component }">
<keep-alive>
<component
:is="Component"
:key="$route.name"
v-if="$route.meta.keepAlive"
/>
</keep-alive>
<component
:is="Component"
:key="$route.name"
v-if="!$route.meta.keepAlive"
/>
</router-view>
</template>
scrollToPolyfill.js
(function(doc, win, el) {
/**
* @description scrollTo/scrollBy的polyfill
* IE9+
*/
if (!win.scrollTo) {
win.scrollTo = function(x, y) {
win.pageXOffset = x;
win.pageYOffset = y;
};
}
if (!win.scrollBy) {
win.scrollBy = function(x, y) {
win.pageXOffset += x;
win.pageYOffset += y;
};
}
if (!doc.body.scrollTo) {
el.prototype.scrollTo = function(x, y) {
this.scrollLeft = x;
this.scrollTop = y;
};
}
if (!doc.body.scrollBy) {
el.prototype.scrollBy = function(x, y) {
this.scrollLeft += x;
this.scrollTop += y;
};
}
})(document, window, Element);
main.js
import "@/utils/scrollToPolyfill.js";
useScroll : 位置记录 & 滚动方法
import { ref, reactive, onMounted, onBeforeMount, Ref } from "vue";
import { useRoute } from "vue-router";
interface ScrollOptions {
setPosition: (type: number) => void;
setScrollTop: (type: number) => void;
restPosition: () => void;
scrollRef: Ref;
}
export default function(): ScrollOptions {
const scrollRef = ref(null);
const route = useRoute();
const setPosition = (type = TYPE.SCROLL_TYPE.HOME) => {
const top = scrollRef?.value?.scrollTop
if (route.meta.keepAlive) {
window.sessionStorage.setItem("TOC_TOP_POSITION", top);
}
};
const setScrollTop = () => {
try {
const top = window.sessionStorage.getItem("TOC_TOP_POSITION");
if (top) {
scrollRef?.value?.scrollTo({
top: parseFloat(top) || 0
});
restPosition();
}
} catch (e) {
console.log(e);
}
};
const restPosition = () => {
window.sessionStorage.removeItem("TOC_TOP_POSITION");
};
return {
scrollRef,
setPosition,
setScrollTop,
restPosition
};
}
Home.vue
<container ref="scrollRef" ></container>
// 点击进入详情页
handleToProductDetail() {
setPosition()
}
onActivated(() => {
setScrollTop();
});