很久都没有更新了,分享一点干货吧.
最近做一个小程序项目,特点是页面超多,开发周期紧,性能要求不高,都用小程序原生语法写的话估计要分包,这样就复杂了.于是项目采用了webview嵌套H5的方案,H5使用vue开发,周期短,开发效率高.
写H5页面不可避免的会遇到记录滚动位置的需求.常见使用场景是从列表页点击跳转至详情页,再返回列表页需要保持滚动位置不变.
这时候在H5中一般有2种解决方案
1.vue使用keepalive组件缓存当前页面组件.
优势:对业务代码侵入量少,可以自行决定页面是否需要缓存,对非body/root节点的自定义滚动容器有效
劣势:对body/root节点滚动不生效,局限性强,无法保证所有关联页面都是自定义滚动容器布局,页面切换后再返回原有的滚动条位置可能还是会错位.
2.实时记录滚动位置,保存在本地缓存中,再返回时候读取scrollTop设置回来.
优势:对某些简单类型页面适用
劣势:需要在具体的每个页面里基于特定的滚动容器做监听保存scrollTop.同时还存在分页情况下非首页返回时scrollTop无法还原,打个比方:列表页分页加载到第五页,假设单页记录高度为100,此时scrollTop=500,点击第五页其中一个元素跳转至详情页查看详情,在返回时默认只会查询第一页数据,这时候设置scrollTop=500就会失效,因为此时容器高度才100.当然你也可以记录下最新一次的页码,返回时一次性加载前五页数据,然后等页面渲染完成后再设置scrollTop=500,这样一来操作显得复杂很多,同时如果页面内还有其他一些变量也会丢失,除非花大力气把所有变量全部保存下来.
基于以上两点方案的局限性和复杂性,所以我在想至少在微信小程序框架是否能有一种更简单的方案能够实现我的目标?
在这里首先需要说明一下小程序原生页面开发的两个特点:
1.如果一个原生page页面内有webview,则webview会占满全屏,并且层级最高,连开发者调试工具也会被遮盖.
2.可以通过wx.navigateTo/wx.miniProgram.navigateTo 针对同一个原生页面多次打开生成多个独立的page实例,getCurrentPages()可以获取到当前小程序路由堆栈,返回由多个page实例构成的数组栈(注意是栈,新的在数组栈顶,老的在栈底)
3.小程序webview的src并不会同步H5内部实时url,假设初始src为"http://a.com/home",H5页面通过一系列操作后内部url已经变成"http://a.com/list/111",这时候小程序的src变量还是"http://a.com/home",并不会变成"http://a.com/list/111"
于是我想到可以通过新开一个webview page加载新H5页面,原有webview page保留,这时滚动位置自然也会一并保留下来.再返回时小程序会自动将新webview page销毁,这时候原有webview page又回到了前台.
举个例子:
H5有两个页面,列表页(/list)和详情页(/detail),列表页接口做了分页,上拉加载到第五页之后,点击第五页任意一条记录,这时候需要跳转至详情页之后再返回列表页保留之前的滚动位置.
小程序只有一个原生page,路径为/pages/liangshanbo/main,放了一个webview用于加载H5,默认加载列表页(/list),点击记录通过wx.miniProgram.navigateTo(path:"/pages/liangshanbo/main")新开了一个webview page加载详情页(/detail)(这时候小程序内维护了一个路由堆栈,里面有两条记录,都是基于同一个原生page生成的两个互不相干的page实例)
<!--pages/liangshanbo/main.wxml-->
<web-view src="{{src}}"></web-view>
滚动位置保留的问题时解决了.BUT,理想很丰满,现实很骨感.
实际业务场景中经常会遇到用户在详情页操作更新了状态.这时候返回至列表页数据就会不一致.那么如何在合适的时机更新列表中某一条数据呢?换句话也就是说列表页H5如何才能知道它回到了前台?只要知道回到前台,并且我们也知道点击的时具体哪一条记录,就可以请求该条记录最新数据定点更新它.
其中遇到一些技术难点,一般在H5中我们会通过判断document.hidden/document.visibilityState的变化来知道当前标签页是否转到后台不可见.但是在小程序的webview中这些方案全部都失效.
甚至我还猜测小程序webview page会不会因为转到后台从而暂时冻结JS线程的执行呢?如果可以的话,我们也可以通过打开新的webview page页面前在当前H5中执行setTimeout(()=>{updateRecord()},500)实现我们的目的.但实际并不如我所愿,转到后台上一个webview page H5页面JS线程一切正常.
原生page是可以通过onShow/onHide生命周期知道当前page状态的.我们需要一个原生page和webview H5实时通讯的机制通知H5,页面返回前台了.
终于回归主题了.这时候网上的一篇文章提醒了我,给前端路由加上hash是不会触发重新加载和渲染的,但是会触发hashChange.那么我们可以在详情页返回的时候给列表页加上一个hash不就可以通知列表页去更新数据了么?
no BB,show code:
/**
* 页面的初始数据
*/
data: {
referer: "",
src: "http://192.168.100.82:8080/home",
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
// referer:上一个H5页面的href,path,query:要打开的H5页面path,query
let { path = "", referer = "", query } = options;
path = decodeURIComponent(path);
referer = decodeURIComponent(referer);
query = (query && JSON.parse(decodeURIComponent(query))) || {};
// miniprogramHook标志位用于告诉H5当前在非首页原生webview page中运行,以达到点击任意前端路由自动新开原生webview page
// (业务需要点击跳转至任意非首页,小程序左上角出现返回按钮,有webview的原生页面自定义导航栏不生效,可忽略)
if (query.miniprogramHook == 1) {
this.setData({
referer,
src: `http://192.168.100.82:8080${path}?${qs.stringify(query)}`
})
}
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
// 在当前页面卸载前获取上一个页面的page实例
let prevPage = getCurrentPages().slice(-2, -1)[0]
// 由于上一个原生page的webview内部可能经过了多次跳转,src不会同步,所以这里需要拿上一个原生page的webview内部最新的url,也就是referer+hash(`#${Date.now()}`)更新上一个原生page的src
// 当上一个page实例回到前台时,由于src已经更新至webview内部url一致,并不会触发webview重新渲染,同时加上hash去触发前端路由的hashchange/beforeRouterUpdate回调
// hash 可以随便定义,这里Date.now()实现是因为列表页=>详情页可能存在多次打开返回的情况,对于同一个hash hashchange只会触发一次.为了能多次触发这里没有写死
if (this.data.referer) {
prevPage.setData({
src: this.data.referer + `#${Date.now()}`
})
}
},
// 全局beforeEach 钩子函数,如果需要打开新的webview page只需要在query增加mpTarget="blank"
router.beforeEach((to, from, next) => {
let isMiniprogram = sessionStorage.getItem("isMiniprogram");
// 判断是否在小程序webview环境内
if ( isMiniprogram === "1" && to.query.mpTarget === "blank") {
// mpTarget使命完成,删除
delete to.query.mpTarget;
// H5调用小程序API打开新的webview page实例
wx.miniProgram.navigateTo({
url: `/pages/liangshanbo/main?${qs.stringify({
query: JSON.stringify({ ...to.query, miniprogramHook: 1 }),
referer: location.href,
path: to.path,
})}`,
});
next(false);
} else {
next();
}
});
// H5列表页beforeRouteUpdate钩子函数可以监听到当前路由的hashChange
beforeRouteUpdate(to, from, next) {
// 更新数据
this.fetchCompanyInfo(this.selectedItem.uniformSCCode).then((data) => {
Object.assign(this.selectedItem, data);
});
// 用完hash将url重置到没有hash的状态
next(false);
},
该方案优缺点:
优势:1.对业务代码透明,侵入性弱.不需要记录滚动位置的页面可以完全不用实现beforeRouteUpdate
2.不限定前端H5框架
劣势:1.H5不能使用hash路由方案,不过现在基本都是history路由了,影响不大.
2.业务代码内也最好不用hash,避免和原生page 添加的hash冲突.至少最好不要在需要记录滚动位置的页面有hash操作.现在的前端路由传参都使用query/path了,问题也不大
最后给自己打个小广告:
本人八年前端开发经验,有多个vue/react项目开发经验,10个以上微信小程序开发经验,同时也熟悉后端node开发.欢迎各位大佬扔外包/兼职单子过来
支持一下: