- 小程序内完美实现吸顶粘附功能(媲美 美团小程序的吸顶效果)如丝滑一般
利用css属性完美解决吸顶粘附效果(ios&android都适用),分享一下心得: tips:不需要调用onPageScroll 方法 去监听滚动距离!!! 1、给吸顶元素添加css属性 position: sticky;2、给元素设置距离顶部吸附的距离 top: 0px; (这个top值你可以根据实际情况动态设置)3、然后完事了啦啦啦啦啦啦啦啦~by the way:附上简单的代码片段------>https://developers.weixin.qq.com/s/pR8QKjmE7igq
2020-04-02 - 模块化——加速小程序开发
阅读基础:有小程序项目经验,有查阅官方文档习惯的小伙伴 随着公司小程序项目日益繁多,仅仅靠着官方提供的框架、组件、API,已经远远不能满足项目高效迭代的要求了,于是我们组内萌生了对小程序进行模块化的想法。 实际项目中我们对小程序模块化已经涉及各个模块,我总结一下,从三个方向跟大家分享我们不一样的模块化思路:[代码]Page+[代码],[代码]basePage[代码],[代码]适配层[代码]。 Page+:赋予页面更多的功能 [代码]Page()[代码]作为页面的入口,我们可以通过对其入参对象的封装实现:生命周期的改造、全局状态管理和新增页面功能。 官方删除了小程序分享回调 complete,一起来尝试将其恢复吧。一般我们的逻辑是这样的: [代码]// pages/index/index.js Page({ // 数据初始化 data: { shareFlag: false, //页面是否处于分享中 shareComplete: false //分享回调事件 }, // onShow 生命周期 onShow: function () { const { shareFlag, shareComplete } = this.data if( shareFlag ){ this.data.shareFlag = false //变量不涉及页面渲染,不使用 setData shareComplete && shareComplete() } }, // 分享事件 onShareAppMessage: function () { let shareInfo = { title: '分享测试标题', path: '', complete: function () { console.log('页面分享成功啦~') } } this.data.shareFlag = true this.data.shareComplete = typeof (shareInfo.complete) == 'function' ? shareInfo.complete : false return shareInfo } }) [代码] 在单页面内实现分享回调这样操作是可行的,如果多页面、多项目都要实现该功能,重复拷贝代码,则显格外得繁琐。 我们来将这个功能抽离封装一下吧。 [代码]// pages/index/index.js import PagePlus from './pagePlus.js' PagePlus({ // 分享事件 onShareAppMessage: function () { return { title: '分享测试标题', path: '', complete: function () { console.log('页面分享成功啦~') } } } }) [代码] [代码]// pages/index/pagePlus.js const PagePlus = (pageObj) => { const _onShow = pageObj.onShow, _onShareAppMessage = pageObj.onShareAppMessage, _data = { shareStatus: false, //页面是否处于分享中 shareComplete: false //分享回调事件 } Object.assign(_data, pageObj.data) delete pageObj.data pageObj.onShow = function () { typeof _onShow == 'function' && _onShow.apply(this) const { shareStatus, shareComplete } = this.data if (shareStatus) { this.data.shareStatus = false //变量不涉及页面渲染,不使用 setData shareComplete && shareComplete() } } pageObj.onShareAppMessage = function () { const shareInfo = typeof _onShareAppMessage == 'function' && _onShareAppMessage.apply(this) this.data.shareStatus = true shareInfo && (this.data.shareComplete = shareInfo.complete) return shareInfo } Page({ data: _data, ...pageObj }) } export default PagePlus [代码] 我们来增加一个新的生命周期回调——[代码]onReshow[代码](页面非首次显示回调,常用于详情页操作影响上一页列表数据的场景)。 [代码]// pages/index/index.js import PagePlus from './pagePlus.js' PagePlus({ // 监听页面非首次显示 onReshow: function(){ console.log('onReshow lifeCallBack') }, onShareAppMessage: function () { return { title: '分享测试标题', path: '', complete: function () { console.log('页面分享成功啦~') } } } }) [代码] [代码]// pages/index/pagePlus.js class BasePage{ data = { pagePlus: { shareStatus: false, //页面是否处于分享中 shareComplete: false, //分享回调事件 firstEnter: true //第一次进入页面 } } methods = { onShow: this.onShow, onShareAppMessage: this.onShareAppMessage, onReshow: this.onReshow } onShow(){ const { shareStatus, shareComplete, firstEnter } = this.data.pagePlus if (firstEnter) { this.data.pagePlus.firstEnter = false } else { this.onReshow() } if (shareStatus) { this.data.pagePlus.shareStatus = false shareComplete && shareComplete() } } onShareAppMessage(shareInfo){ this.data.pagePlus.shareStatus = true shareInfo && (this.data.pagePlus.shareComplete = shareInfo.complete) } } const PagePlus = (pageObj) => { const basePage = new BasePage() for (var i in basePage.methods) { basePage.methods[i] = (() => { const key = i const _temFn = basePage.methods[key] return function () { if (key == 'onShareAppMessage') { const shareInfo = typeof pageObj[key] == 'function' && pageObj[key].apply(this, arguments) _temFn.apply(this, [shareInfo]) return shareInfo } typeof pageObj[key] == 'function' && pageObj[key].apply(this, arguments) typeof _temFn == 'function' && _temFn.apply(this, arguments) } })() } Object.assign(basePage.data, pageObj.data) delete pageObj.data Page({ data: basePage.data, ...pageObj, ...basePage.methods }) } export default PagePlus [代码] 自此,我们修改了原生的生命周期回调和增加了新的生命周期回调。当然我们还能为 Page+ 赋予更多的功能,例如: [代码]页面刷新[代码]:下拉自动刷新当前页。 [代码]定时器自动清除[代码]:离开页面时,自动清除页面执行的定时器。 [代码]全局状态管理[代码]:页面间数据共享,相关数据关联的组件即时渲染更新。 相关的代码实现,大家可以自己思考一下怎么实现;我的实现细节,如果大家感兴趣的话就在下方给我留言吧,你们的回复是我更新的动力哦。 basePage:公共 Component 管理器 小程序页面彼此独立,使用 Component 都需要各自引用,为了实现页面公共 Component 的统一管理,这个时候就可以引入 basePage 的概念:以 basePage 作为父组件,其他公共 Component 作为子组件,页面通过 basePage 对公共 Component 进行管理。 实现原理 1、定义一个 Component ,作为 basePage 。 2、每个页面统一引用 basePage ,且规定页面的元素都需要写到 <basePage/> 标签内部 。 3、通过 basePage 引用页面公共的 Component ,并进行业务逻辑编辑。 实现细节 实际使用过程中,我发现有两个问题: 1、Page 和 basePage 通信是非常频繁的,需要通过 WXML 数据绑定和 triggerEvent 触发事件,略显麻烦。 2、setTimeout、webSocket 等后台进程,可能触发[代码]非当前显示页面[代码]的渲染更新,而绝大部分情况,我们只需要[代码]当前显示页面[代码]的渲染更新。 针对这两种场景的优化,我们可以把当前显示页面的 basePage 实例对象赋值到 global 的某个具体变量;每当 Page 触发 show 生命周期回调的时候,我们就对这个变量赋值的实例对象进行更新,这样我们就可以通过 global 的变量直接操作当前显示页面的 basePage 了。 部分代码示例 [代码]{ "文件路径": "pages/index/index.json", "usingComponents": { "basePage": "../../components/basePage/index" } } [代码] [代码]<!--pages/index/index.wxml--> <basePage> <!-- 页面元素 --> </basePage> [代码] [代码]// components/basePage/index.js Component({ /** * Component 所在页面的生命周期函数 */ pageLifetimes: { show: function () { global.basePage = this }, hide: function () { global.basePage = null } } }) [代码] [代码]{ "文件路径": "components/basePage/index.json", "说明": "在此处统一引入页面公共的 Component", "component": true, "usingComponents": {} } [代码] [代码]<!--components/basePage/index.wxml--> <slot /> [代码] 适配层:让代码适应更多的场景 如果你的项目对代码后续维护、迭代和可移植性有较高需求,或者需要多项目并行,这个时候通过适配层去调用各个功能模块就显得尤为重要。适配层方面我做的还是比较粗糙的,如果有建议欢迎指出。 适配层的时机 项目不是 bugfix 级别的迭代,都有适配层设计的必要。 如果是[代码]新项目[代码],心底不认为自己是“咸鱼”而是代码的“亲爹”,[代码]适配层完全可以作为标配[代码]去实现;这就是展现自己对代码全局观的时候了,把自己对代码的理解都用适配层去诠释吧。 如果是[代码]旧项目迭代[代码],在项目排期允许的情况下,尽可能理解原代码的基本实现细节;对比新的项目是要束手束脚一些,适配层的设计要在[代码]尽可能少改变原有代码[代码]的情况下进行;如果排期比紧急,适配层的完整实现[代码]可以在几个版本迭代中逐步实现[代码]。 模块设计必须高内聚低耦合 如果功能模块的设计过于松散、耦合复杂,这就意味着适配层将需要做各种兼容,这和适配层设计的初衷背道而驰,不做也罢。 配置文件 如果你的代码有移植性要求,为这些不同环境准备对应的配置文件吧,配置文件可以通过自制脚手架实现,也可以粗暴地手动替换,在保证尽可能不出错的情况下实现即可。 功能模块的入口 所有整合的功能模块都需要通过适配层进行调用,适配层就是你的“王之财宝”。 规范 && 文档 适配层是从代码的全局考虑,如果是项目是分工完成,项目的开发人员都需要遵守适配层规范进行代码开发;文档我一直都认为都是非常必要的,但还是经常会懈怠,没有进行完整的文档编写,但我基本会在所有项目成员都能理解适配层的情况下,进行简单的口头说明。 因为开心说一些废话 一次需求迭代中,几乎涉及手头上的所有小程序项目;刚好就在需求前的半个月,我们小组完成了对所有项目模块化改造;虽然需求来得很急,我们还是很完美的实现了。毕竟[代码]模块化之前,每个项目的改造都是独立的工作量;模块化之后,就只有适配层迭代的工作量了[代码]。不过真是辛苦了测试小伙伴,因为对所有项目进行模块化改造,意味着测试小伙伴对所有项目进行回归测试,感谢测试小伙伴,比心! 这篇文章,对 Page+ 的具体实现展示比较详细,感觉对 basePage 和适配层讲的都比较偏概念。毕竟这部分内容都和业务逻辑联系比较紧密,很难抽象深入讲解。刚好还有假期还有一段时间,如果自己还有时间就再写一篇关于最近项目的模块化剖析吧,哈哈。
2019-10-09 - 如何用小程序实现类原生APP下一条无限刷体验
1.背景 如今信息流业务是各大互联网公司争先抢占的一个大面包,为了提高用户的后续消费,产品想出了各种各样的方法,例如在微视中,用户可以无限上拉出下一条视频;在知乎中,也可以无限上拉出下一条回答。这样的操作方式用户体验更好,后续消费也更多。最近几年的时间,微信小程序已经从一颗小小的萌芽成长为参天大树,形成了较大规模的生态,小程序也拥有了一个很大的流量入口。 2.demo体验 那如何才能在小程序中实现类原生APP效果的下一条无限刷体验? 这篇文章详细记录了下一条无限刷效果的实现原理,以及细节和体验优化,并将相关代码抽象成一个微信小程序代码片段,有需要的同学可查看demo源码。 线上效果请用微信扫码体验: [图片] 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a 3.实现原理 出于性能和兼容性考虑,我们尽量采用小程序官方提供的原生组件来实现下一条无限刷效果。我们发现,可以将无限上拉下一篇的文章看作一个竖向滚动的轮播图,又由于每一篇文章的内容长度高于一屏幕高度,所以需要实现文章内部可滚动,以及文章之间可以上拉和下拉切换的功能。 在多次尝试后,我们最终采用了在[代码]<swiper>[代码]组件内部嵌套一个[代码]<scroll-view>[代码]组件的方式实现,利用[代码]<swiper>[代码]组件来实现文章之间上拉和下拉切换的功能,利用[代码]<scroll-view>[代码]来实现一篇文章内部可上下滚动的功能。 所以页面的dom结构如下所示: [代码]<swiper class='scroll-swiper' circular="{{false}}" vertical="{{true}}" bindchange="bindChange" skip-hidden-item-layout="{{true}}" duration="{{500}}" easing-function="easeInCubic" > <block wx:for="{{articleData}}"> <swiper-item> <scroll-view scroll-top="0" scroll-with-animation="{{false}}" scroll-y > content </scroll-view> </swiper-item> </block> </swiper> [代码] 4.性能优化 我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。例如减少代码包体积,使用分包,渲染性能优化等。下面主要讲一下渲染性能优化。 4.1 dom优化 由于页面需要无限上拉刷新,所以要在[代码]<swiper>[代码]组件中不断的增加[代码]<swiper-item>[代码],这样必然会导致页面的dom节点成倍数的增加,最后非常卡顿。 为了优化页面的dom节点,我们利用[代码]<swiper>[代码]的[代码]current[代码]和[代码]<swiper-item>[代码]的[代码]index[代码]来做优化,控制是否渲染dom节点。首先,仅当[代码]index <= current + 1[代码]时渲染[代码]<swiper-item>[代码],也就是页面中最多预先加载出下一条,而不是将接口返回的所有后续数据都渲染出来;其次,对于用户已经消费过的之前的[代码]<swiper-item>[代码],不能直接销毁dom节点,否则会导致[代码]<swiper>[代码]的[代码]current[代码]值出现错乱,但是我们可以控制是否渲染[代码]<swiper-item>[代码]内部的子节点,我们设置了仅当[代码]current <= index + 1 && index -1 <= current[代码]时才会渲染[代码]<swiper-item>[代码]中的内容,也就是仅渲染当先文章,及上一篇和下一篇的文章内容,其他文章的dom节点都被销毁了。 这样,无论用户上拉刷新了多少次,页面中最多只会渲染3篇文章的内容,避免了因为上拉次数太多导致的页面卡顿。 4.2 分页时setData的优化 setData工作原理 [图片] 小程序的视图层目前使用[代码]WebView[代码]作为渲染载体,而逻辑层是由独立的 [代码]JavascriptCore[代码] 作为运行环境。在架构上,[代码]WebView[代码] 和 [代码]JavascriptCore[代码] 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 [代码]evaluateJavascript[代码] 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 [代码]JS[代码] 脚本,再通过执行 [代码]JS[代码] 脚本的形式传递到两边独立环境。 而 [代码]evaluateJavascript[代码] 的执行会受很多方面的影响,数据到达视图层并不是实时的。 每次 [代码]setData[代码] 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关。 [代码]setData[代码] 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。 [代码]setData[代码] 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。 避免不当使用setData [代码]data[代码] 应仅包括与页面渲染相关的数据,其他数据可绑定在this上。使用 [代码]data[代码] 在方法间共享数据,会增加 setData 传输的数据量,。 使用 [代码]setData[代码] 传输大量数据,通讯耗时与数据正相关,页面更新延迟可能造成页面更新开销增加。仅传输页面中发生变化的数据,使用 [代码]setData[代码] 的特殊 [代码]key[代码] 实现局部更新。 避免不必要的 [代码]setData[代码],避免短时间内频繁调用 [代码]setData[代码],对连续的setData调用进行合并。不然会导致操作卡顿,交互延迟,阻塞通信,页面渲染延迟。 避免在后台页面进行 [代码]setData[代码],这样会抢占前台页面的渲染资源。可将页面切入后台后的[代码]setData[代码]调用延迟到页面重新展示时执行。 优化示例 无限上拉刷新的数据会采用分页接口的形式,分多次请求回来。在使用分页接口拉取到下一刷的数据后,我们需要调用[代码]setData[代码]将数据写进[代码]data[代码]的[代码]articleData[代码]中,这个[代码]articleData[代码]是一个数组,里面存放着所有的文章数据,数据量十分庞大,如果直接[代码]setData[代码]会增加通讯耗时和页面更新开销,导致操作卡顿,交互延迟。 为了避免这个问题,我们将[代码]articleData[代码]改进为一个二维数组,每一次[代码]setData[代码]通过分页的 [代码]cachedCount[代码]标识来实现局部更新,具体代码如下: [代码]this.setData({ [`articleData[${cachedCount}]`]: [...data], cachedCount: cachedCount + 1, }) [代码] [代码]articleData[代码]的结构如下: [图片] 4.3 体验优化 解决了操作卡顿,交互延迟等问题,我们还需要对动画和交互的体验进行优化,以达到类原生APP效果的体验。 在文章间上拉切换时,我们使用了[代码]<swiper>[代码]组件自带的动画效果,并通过设置[代码]duration[代码]和[代码]easing-function[代码]来优化滚动细节和动画。 当用户阅读文章到底部时,会提示下一篇文章的标题等信息,而在页面上拉时,由于下一篇文章的内容已经加载出来了,这样在滑动过程中会出现两个重复的标题。为了避免这种情况出现,我们通过一个占满屏幕宽高的空白[代码]<view>[代码]来将下一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]hidden="{{index !== current && index !== current + 1}}"[代码]来隐藏这个空白[代码]<view>[代码],并对这个空白[代码]<view>[代码]的高度变化增加动画,来实现下一篇文章从屏幕底部滚动到屏幕顶部的效果: [代码].fake-scroll { height: 100%; width: 100%; transition: height 0.3s cubic-bezier(0.167,0.167,0.4,1); } [代码] [图片] 而当用户想要上拉查看之前阅读过的文章时,我们需要给用户一个“下滑查看上一条”提示,所以也可以采用同上的方式,通过一个占满屏幕宽高的提示语[代码]<view>[代码]来将上一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]wx:if="{{index + 1 === current}}"[代码]来隐藏这个提示语[代码]<view>[代码],并对这个提示语[代码]<view>[代码]的透明度变化增加动画,来实现下拉时提示“下滑查看上一条”的效果: [代码].fake-previous { height: 100%; width: 100%; opacity: 0; transition: opacity 1s ease-in; } .fake-previous.show-fake-previous { opacity: 1; } [代码] 至此,这个类原生APP效果的下一条无限刷体验的需求的所有要点和细节都已实现。 记录在此,欢迎交流和讨论。 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a
2019-06-25 - 我个人总结的小程序字体解决方案
最近在用到小程序的字体,用起是真的有一丝烦躁,各种莫名其妙的bug。下面做一些总结,免得大家犯同样的错误,同样也希望官方能出一个详细的字体解决说明。 问题原因: 首先使用字体无非就是用font-family,在网页上是有很多很好的探讨的文章。但是在小程序中,我们并不知道font-family支持哪些字体,据我这一周的查找,官方也没有详细的说明。 因此如果我们想使用某种字体,最好的方法只有通过font-face外部引入字体。 但是小程序是不支持引入本地的字体文件,即font-face中的src属性的值不能是本地路径。否则会报do-not-use-local-path错: [图片] 因此我所知道的最终的解决方案就有两种 1.引入网络路径 2.通过base64编码引入 ①引入网络路径的方法是将你的字体文件放在服务器中,然后通过网络路径引入,需要注意跨域的问题(可能因为我把字体文件放在腾讯云的对象存储中了,因此没有专门处理跨域也没有问题,但是不排除你可能会遇到跨域问题)。使用这种方法时要注意字体文件不能太大,具体限制我也不清楚。这里我只测试过3M的中文字体,会报ERR_CACHE_MISS[图片],这个情况下,虽然在开发工具上显示的字体是正确的,但是预览到真机上就直接表现为字体文件没有加载的效果。(另外,即使你的字体文件很小,也会在 清除缓存后第一次编译 报这个错,但这种情况下预览到真机上是没有问题的) ②第二种方式就是通过base64编码引入。使用这种方法时需要注意一点,很多转base64编码的工具(例如我这里使用的font-min)会自动帮你兼容ie,生成的代码像这样:[图片] 在网页上这样用是没有问题的,还兼容其他浏览器。 但是在小程序中是不行的,必须把多余的部分全部删除,只留base64那一行,不然在开发工具上显示直接没有加载字体的效果,像这样: [图片] 另外据说base64编码不能太大,否则也会出错;这里由于我的需求只是需要几个固定文字,因此生成的base64很小,没有遇到这个问题。 以上就是我在处理小程序字体时遇到的问题总结,希望大家遇到时能少走弯路; 总结的不对的地方,欢迎大家讨论指正。
2018-05-08