- 教你解决showLoading 和 showToast显示异常的问题
问题描述 当wx.showLoading 和 wx.showToast 混合使用时,showLoading和showToast会相互覆盖对方,调用hideLoading时也会将toast内容进行隐藏。 触发场景 当我们给一个网络请求增加Loading态时,如果同时存在多个请求(A和B),如果A请求失败需要将错误信息以Toast形式展示,B请求完成后又调用了wx.hideLoading来结束Loading态,此时Toast也会立即消失,不符合展示一段时间后再隐藏的预期。 解决思路 这个问题的出现,其实是因为小程序将Toast和Loading放到同一层渲染引起的,而且缺乏一个优先级判断,也没有提供Toast、Loading是否正在显示的接口供业务侧判断。所以实现的方案是我们自己实现这套逻辑,可以使用Object.defineProperty方法重新定义原生API,业务使用方式不需要任何修改。 代码参考 [代码]// 注意此代码应该在调用原生api之前执行 let isShowLoading = false; let isShowToast = false; const { showLoading, hideLoading, showToast, hideToast } = wx; Object.defineProperty(wx, 'showLoading', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowToast) { // Toast优先级更高 return; } isShowLoading = true; console.log('--------showLoading--------') return showLoading.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'hideLoading', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowToast) { // Toast优先级更高 return; } isShowLoading = false; console.log('--------hideLoading--------') return hideLoading.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'showToast', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { if (isShowLoading) { // Toast优先级更高 wx.hideLoading(); } isShowToast = true; console.error('--------showToast--------') return showToast.apply(this, param); // 原样移交函数参数和this } }); Object.defineProperty(wx, 'hideToast', { configurable: true, // 是否可以配置 enumerable: true, // 是否可迭代 writable: true, // 是否可重写 value(...param) { isShowToast = false; console.error('--------hideToast--------') return hideToast.apply(this, param); // 原样移交函数参数和this } }); [代码] 调整后展示逻辑为: 优先级:Toast>Loading,如果Toast正在显示,调用showLoading、hideLoading将无效 调用showToast时,如果Loading正在显示,则先调用 wx.hideLoading 隐藏Loading
2019-10-30 - 微信小程序答题页——swiper渲染优化及swiper分页实现
前言 swiper的加载太多问题,网上资料好像没有一个特别明确的,就拿这个答题页,来讲讲我的解决方案 这里实现了如下功能和细节: 保证swiper-item的数量固定,加载大量数据时,大大优化渲染效率记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验示例动图 [图片] 截图 [图片] [图片] 问题原因 当swiper-item数量很多的时候,会出现性能问题 我实现了一个答题小程序,在一次性加载100个swipe-item的时候,低端手机页面渲染时间达到了2000多ms 也就是说在进入答题页的时候,会卡顿2秒多去加载这100个swiper-item 思考问题 那我们能不能让他先加载一部分,然后滑动以后再去改变item的数据,让swiper一直保持一定量的swiper-item? 注意到官方文档有这么两个属性可以利用,我们可以开启衔接滑动,然后再bindchange方法中去修改data [图片] 1、保证swiper-item的数量固定,加载大量数据时,优化渲染效率 假设我们请求到的数据的为list,实际渲染的数据为swiperList 我们现在给他就固定3个swiper-item,前后滑动的时候去替换数据 正向滑动的时候去替换滑动后的下一页数据,反向滑动的时候去替换滑动后的上一页数据 当我们知道了要替换的条件,我们便可以去替换数据了 但是我们应该考虑到临界值的问题,如果当前页是list第一项和最后一项该怎么办,向左向右滑是不是得禁止啊 这边是判断没数据会让它再弹回去 2、记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页 有很多时候,我们是从某一项直接进来的,比如说上次答题答到了第五题,我这次进来要直接做第六题 那么我们需要去初始化这个swiperList,让它当前页、上一页、下一页都有数据 3、答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验 从答题卡选择index,那就不仅仅是滑动上下页了,它可以跳转到任何页,所以也采用类似初始化swiperList的方法 swiper切换动画我这边是默认250ms,但是发现有时候从答题卡点击回来,你在答题卡点击的下一项不知道会从左还是从右滑过来 体验真的很差,一开始不知道怎么禁掉动画,其实在跳转到答题卡页的时候把duration设为0就可以了 然后在答题卡页的unload方法中恢复 关键点: 在固定3个swiper-item的同时,要保证我们可以有办法来替代微信自带swiper的current属性和change方法 swiper-limited-load使用方法及说明: 将components中的swiper-limited-load复制到您的项目中在需要的页面引用此组件,并且创建自己的自定义组件item-view在初始化数据时,为你的list的每一项指定index属性具体可以参照项目目录start-swiper-limited-load中的用法说明:其它属性和swiper无异,你们可以自己单独添加你们需要的属性总结 一开始很头疼,为什么微信小程序提供的这个swiper,没去考虑这方面 然后在网上和社区找也没有一个特别好的解决方案。 后来想想,遇到需求就静下来解决吧。 项目地址:https://github.com/pengboboer/swiper-limited-load 如果错误,欢迎指出。 如有新的需求也可以提出来,如果有时间的话,我会帮你们完善。 如果能帮到你们,记得给一个star,谢谢。 ---补充 有很多朋友在评论区提到了分页的需求,抽时间写了一个分页的Demo和大家分享一下。 还是以答题为例,比如我们一共有500条数据,一页20条,可能需要如下功能,乍一看不就加了个分页,挺简单的,其实实现起来挺麻烦的,下面说一下思路和一些需要特别注意的点: 1、从其他页面跳转到答题页时,不光只能默认在第一题,可以是任意一题,比如第80题。 跳转到任意一题,那么需要我们根据index算出该数据在第几页,然后需要请求该页数据,最后显示对应的index。我的思路更注重用户体验,不可能是上滑或者下滑才开始去请求数据,一定是要用户滑动前提前请求好数据。所以起码要保证左右两侧在初始化那一刻都有数据。如果此题和它的上一题下一题都在同一页,那么我们只需要请求一页数据(第15题,那么只需请求第1页数据)。如果此题和它的上一题或者下一题不在同一页,那么我们可能需要请求两页数据。(第20题,那么需要请求第1页和第2页数据) 2、左滑、右滑没数据时,都可以加载新数据。直到滑到第一题或者最后一题。 如果我们初始化时是第24题,那么我们左滑到第21题时,就应该去请求第一页的数据。那么用户在看完21题时,再滑到20题,可能就根本不会感知到通过网络请求了数据。但是如果用户此刻滑动特别快:滑到21题时请求了网络,请求还没成功,就又向左滑了。那么我们需要限制用户的滑动,给用户一个提示:数据正在加载中。 3、从答题卡点击任意一题可以跳转到相应的题目,并且左右滑动显示正常数据 比如我们初始化是跳转到了第80题,不一会点击答题卡又要跳转到200题,一会又跳转到150题。各种无序操作,你也不知道用户要往哪里点。 一开始是想着维护一个主list,点到哪道题往list中添加这道题所在的当页的数据,但是还得判断这一页或者左滑右滑请求新一页的数据得往list的哪个位置添加。这来回来去乱七八糟的判断就很麻烦了,很容易出bug。而且list长度太长了以后insert的性能也不好。 后来就去想,要不答题卡点击任意一题都清空旧的list,然后请求新的数据,左右滑动没数据了再请求新的数据呗。但是这样很浪费资源,并且用户体验也不好,用户已经从第1题答到第200题了,这时用户从答题卡选择了一个25题,还得重新请求网络。而且200道题的数据都没了,那再选个26题,再重新请求网络?网络有延时不说,还浪费资源。 最后转念一想,这时候就需要弄一个缓存了。所以最终的解决方法就出来了:我们维护一个map,在网络请求成功后,在map中保存对应页的数据,同时我们维护一个主list来显示对应的题目。当我们在答题卡选择某一题目,就清空list,然后判断map中有没有该页的数据,如果有就直接拿来,没有就再去网络请求。这个处理方式,写法相对来说简单,不需要乱七八糟的判断,也不浪费资源,用户体验也很不错。 总结 以上就是一些思路和要注意的地方。这个Demo断断续续花了好几天时间写出来的。可能我说的比较啰嗦比较细,只是想让需要用到这个分页Demo的同学能理解我是如何实现的。 如果觉得能帮到你,记得给一个star,谢谢。同时如果这个demo有bug或者你们有新想法,欢迎提出来。
2021-01-07 - 已解决。小程序获取手机号时,checkSession通过但是获取手机号解密失败
一开始我的处理方式是在页面直接用checkSession,我的session_key是在index.js登录的时候保存到storage,这里check回调的是“success”。 但是把此时storage里面的session_key结合授权按钮的参数去进行解密是失败的,需要在当前的Page再登陆一次才能成功。 不推荐把session_key存放在缓存。所以以上做法直接跳过。 最后参考了一个朋友的做法,在Page onLoad的时候执行一次wx.login(),然后拿到新的session_key,再用此时的新key去解密就通了。或者改为请求解密之前执行一次登录,据说出问题的概率还是很大 结尾补充:最后一种方法还有个问题要考虑,就是最好执行获取手机号之前再checkSession一下(尽管没啥用)。 问题源头,由于这个函数在校验session_key的时候,无论是过期的key还是新的key都是success,所以有了之后一些列的问题,session_key的状态没法把控 [代码]Page({ data: { currentSessionKey: null }, onLoad: function(options) { /* do something*/ const here = this; // 执行登录确保session_key在线 wx.login({ success(res) { if (res.code) { // call()是我自己基于wx.request封装的一个请求函数工具,这里通过后端发送登录请求获得openid const data = call(userLogin, { code: res.code }); data.then(obj => { if (!obj.error) { here.setData({ currentSessionKey: obj.result.session_key }) } }); } }, fail(error) { throw error; } }); }, // 点击按钮获取手机号权限并解析<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber" bindtap='doMyAction'>获取手机号</button> getPhoneNumber: function (e) { const { encryptedData, iv } = e.detail; const options = { encryptedData: encryptedData, iv: iv, sessionKey: this.data.currentSessionKey }; here.doGetPhone(options); }, doMyAction: function() { // 还可以做一些事情 }, doGetPhone: function (options) { const { sessionKey, encryptedData, iv } = options; const here = this; // 向服务器请求解密 wx.request({ // 这里是解密用的接口 url: 'https://xxx.com/python/decrypt', method: 'POST', data: { sessionKey: sessionKey, encryptedData: encryptedData, iv: iv }, success(res) { // 最终获取到用户数据,国家代号前缀、不带前缀的手机号。默认是不带前缀 const { countryCode, purePhoneNumber } = res.data; here.pageForward(countryCode, purePhoneNumber); }, fail(error) { console.log(error); here.pageForward(); } }) }, pageForward: function(countryCode, purePhoneNumber) { // 获取成功后我是跳转到另一个页面 wx.navigateTo({ url: `/pages/person/index?phone=${purePhoneNumber}` }) } }) [代码]
2020-09-15 - 各种路由情况下使用 getCurrentPages 可能存在的陷阱
废话不多说,直接上微信API。 PageObject[] getCurrentPages()获取当前页面栈。数组中第一个元素为首页,最后一个元素为当前页面。 注意: 不要尝试修改页面栈,会导致路由以及页面状态错误。 不要在 [代码]App.onLaunch[代码] 的时候调用 [代码]getCurrentPages[代码],此时 [代码]page[代码] 还没有生成。 这是微信官方对 getCurrentPages 做的解析和使用注意点,相信大家都很熟悉了。文档中明确指出 不要在 [代码]App.onLaunch[代码] 的时候调用 [代码]getCurrentPages[代码],此时 [代码]page[代码] 还没有生成。 那么是不是只要我们不再 App.onLaunch 中使用,getCurrentPages 就会按照我们的预期来工作呢?其实并不尽然。 我在这里为了演示,写了两个页面 First Page 和 Second Page。First Page 可以跳转到 Second Page , Second Page 也可以回退到 First Page. [图片][图片][图片][图片] 并且在两个页面的 onShow 方法中调用了 [代码]getCurrentPages[代码] 方法。 onShow(){ let pages = getCurrentPages(); console.log('------------------- First Page onShow方法中获取栈内页面'); console.log(pages); }, 重新运行小程序,从 First Page 跳转到 Second Page ,观察控制台日志 [图片][图片] 可以看到在 First Page 的 onShow 方法中打印的包含一个对象H的数组, 在 Second Page 的 onShow 方法中打印出了包含两个对象H的数组,H 可以理解为Page实例对象的名称。这和我们预期的是一样的。 接下来我们举个特别点的栗子,怎么特别呢?这回我们的栗子没用糖炒,炒栗子的伙计把“路由”错当成糖了。那我们就来看看当栗子,啊...,不,应该是 [代码]getCurrentPages[代码] 遇到“路由”后会变成什么味儿呢? 首先我们来看看微信小程序API中都有哪些路由 [图片][图片] 没错就是这几个控制小程序页面跳转的全局函数。我们在这里尝试一下最常用的两个 navigateTo 和 navigateBack,其他的同理。 下面我们来看看 First Page 的跳转方法代码: /** * 跳转到 Second Page */ navigateToSecondPage() { wx.navigateTo({ url: '/other/other', }) let pages = getCurrentPages(); console.log('------------------- First Page navigateToSecondPage方法中获取栈内页面'); console.log(pages); }, 我们重新运行一下小程序后点击 First Page 中 跳转到Second Page的按钮,观察一下控制台的日志输出情况 [图片][图片] 哎哎哎......?怎么哪里好像不对呢?炒栗子的伙计说在点击按钮的时候我明明往锅里加了一个 Page 实例啊,为什么我放进去后没有马上看见呢?完了这下炒出来的栗子又变味了。 我们知道 navigateTo 方法肯定是向页面栈里又加入了一个新页面实例的,那么我们去 Second Page 中的 onLoad 方法中看看,有几个 Page 实例: [图片][图片] 哎呦,在Second Page 中明明就是两个,怎么在跳转执行后没有看到呢?伙计想是不是自己太心急了,于是他在加入第二个Page后并没有马上去看而是出去抽了根烟。 /** * 跳转到 Second Page */ navigateToSecondPage() { wx.navigateTo({ url: '/other/other', }) let timerId = setTimeout(function(){ let pages = getCurrentPages(); console.log('------------------- First Page navigateToSecondPage 1S later 方法中获取栈内页面'); console.log(pages); }, 1000); }, 抽完烟后回来发现又正常了 [图片][图片] 点击完跳转按钮1S后,第二个Page实例奇迹般地出现了。伙计有点兴奋啊。于是决定了以后碰到 [代码]getCurrentPages[代码] 和 路由就出去抽根烟,啊...... ,不不不,是等会再看。可是这并不是万全之策。我们都知道 setTimeOut 的运行机制,它是在当前任务栈内的任务执行完毕后再执行 setTimeOut 里任务。这个任务的执行开始时间是当前任务栈内所有的任务执行完成的耗时 + setTimeOut 的延迟时间,也就是说是一个不确定的时间,换个说法就是伙计出去抽根烟回来不一定能看见第二个Page实例。总感觉不是那么安全,那怎么办呢? 我们仔细阅读 navigateTo 的使用文档就会发现,它除了可以指定跳转路径(url)外,还可以添加三个回调方法。 [图片][图片] 那么现在我们根据使用文档来试着修改一下我们查看 page 实例的时间。我们等到跳转成功后就立马去查看。 /** * 跳转到 Second Page */ navigateToSecondPage() { wx.navigateTo({ url: '/other/other', success: function(){ let pages = getCurrentPages(); console.log('------------------- First Page navigateTo Success 方法中获取栈内页面'); console.log(pages); } }) }, 重新运行一下程序: [图片][图片] 好像这个success回调也不是很靠谱,还是得等一小会儿才能看见第二个Page实例。 这是 navigateTo ,下面我们来看看 navigateBack 。 navigateBack(){ let pages = getCurrentPages(); console.log('------------------- Second Page navigateBack 执行之前获取栈内页面'); console.log(pages); if(pages.length > 1){ wx.navigateBack({ delta: 1, success: function(){ console.log('-------------------Second Page navigateBack Success获取栈内页面'); let pages1 = getCurrentPages(); console.log(pages1); } }); } }, 我们清空控制台日志,然后点击 Second Page 的 返回 First Page 按钮 [图片][图片] 我们可以看到 wx.navigateBack 的 success 函数中可以拿到准备的页面堆栈数据。当然你也可以出去抽根烟等会回来再看数据。 所以我们根据上面的Demo可以得出两个小结论: 1 wx.navigateTo 执行之后只能通过延时的方法去获取准确的页面堆栈数据,具体延时多少看你炒栗子的经验喽。 2 wx.navigateBack 执行后可以通过延时和回调方法进行获取页面堆栈数据。 至于其他的路由的情况就不啰嗦那么多了,结论如下: 3 wx.switchTab 执行后可以通过延时和回调方法进行获取页面堆栈数据, 同 wx.navigateBack。 4 wx.reLaunch 执行之后只能通过延时的方法去获取准确的页面堆栈数据, 同 wx.navigateTo。 5 wx.redirectTo 没有办法在执行wx.redirectTo的页面内获取准确的页面堆栈数据。 怎么样,有没有感觉很 因吹丝汀 啊..... 好了,大概就这些了。 重要的事情说三遍: 尽量不要在执行完路由函数后立即调用 getCurrentPages 函数! 尽量不要在执行完路由函数后立即调用 getCurrentPages 函数! 尽量不要在执行完路由函数后立即调用 getCurrentPages 函数! 不然你会吃到怪味的栗子,希望可以帮到大家,如果哪些地方写的不对或不够准确,请大家批评指正。
2019-12-12 - 微信小程序设置过滤器将时间戳转化为日期字符串
在微信开发小程序时,后台传入的日期数据可能是时间戳 而不是日期 , 或者需要把日期转换成时间戳来做出相应的处理时,我们将用到时间戳和日期的相互转换. WXS是专供WXML调用的有独立作用域的JS模块(不是全功能的JS,感觉有所限制),可以在在视图层对数据进行格式化处理,起到过滤器的作用。 js文件 Page({ data: { timestamp: 1522117395730 }}) wxml文件wiz_code_mirror>/wiz_code_mirror> <wxs module="m1"> function format(ts) { var d = getDate(ts) return [d.getFullYear(), d.getMonth()+1, d.getDate()].join('-') + ' ' + [d.getHours(), d.getMinutes(), d.getSeconds()].join(':') } module.exports.format = format</wxs><view>{{ timestamp }}</view><view>{{ m1.format(timestamp) }}</view> 注意:wxs 获取当前日期不可以使用 new Date(),而需要使用 getDate() 代替。 wiz_tmp_tag style="display: none;">/wiz_tmp_tag> 更多分享请移步我的个人博客: https://tianxintiandisheng.github.io/
2018-08-28 - Expected updated data but get first ?
最近突然出现Expected updated data but get first rendering data报错,没有具体位置信息,无法提供相关的代码片段,这个问题如何解决,测试过两个空白页跳转也会出现此问题
2019-09-06 - mpvue开发音频类小程序踩坑和建议
前言 这是我第一次开发小程序,开发的产品是音频类的,在大佬的建议下采用了[代码]mpvue[代码],一周时间把功能都做出来,由于不太熟悉mpvue和微信小程序,足足用了一周时间来改bug才出来一个能用的版本,在这里整理分享下我开发时遇到的一些问题和给出一些建议。 [图片] 在[代码]Linux[代码]上开发小程序 在公司电脑装了双系统,日常用的是[代码]Ubuntu[代码]系统,Linux或Mac的开发环境对前端相对来说会友好一些。微信小程序官方的开发者工具只有[代码]Windows[代码]和[代码]Mac[代码]版本,所以这就尴尬了。 不过还好,发现已经有大神在GitHub上做了Linux的支持,推荐给大家:Linux微信web开发者工具。 根据教程安装使用即可,使用时就用[代码]./bin/wxdt[代码]命令打开。不过用了几天后面觉得不太方便,就索性切回Windows系统用官方最新的版本了。 封装wx.request为Promise [代码]wx.request[代码]用于发起http请求,但平时习惯了Promise的写法,所以还是封装一下这个方法为Promise的形式。 我看很多小程序会使用fly这个库。 但个人觉得发起请求不需要那么强大的功能,小程序本身就应该是一个轻量级的东西,引入一个库可能会导致项目打包变大,可能让小程序更卡,所以本着能自己写就自己写吧的心态,索性自己封装一下算了。 在[代码]src/utils[代码],新建一个[代码]request.js[代码]: [代码]const apiUrl = 'https://your server.com/api/' const request = (apiName, reqData, isShowLoading = true) => { // 某些请求可能不需要显示loading if (isShowLoading) { wx.showLoading({ title: '正在努力加载中', mask: true }) } return new Promise(function (resolve, reject) { wx.request({ url: apiUrl + apiName, method: 'POST', data: reqData, header: { 'content-type': 'application/json' // 默认值 }, success (res) { if (res.data.code === 0) { // 与后端约定code=0时才是正常的 resolve(res) } else { reject(res) } }, fail (err) { reject(err) }, complete (res) { wx.hideLoading() } }) }) } export default request [代码] 当然这是个简化版的,我实际项目中还会在初始化时加入一些[代码]token[代码]之类的参数,大家能看明白是这样封装成Promise的就可以啦。 使用vant-weapp 小程序已经支持了npm安装,但不太会弄。还是按网上方法,将项目clone下来放进static目录下。 [代码]git clone https://github.com/youzan/vant-weapp.git [代码] 然后将[代码]vant-weapp[代码]的[代码]dist[代码]目录拷贝到项目的static目录下(尽可能精简,删掉一些奇奇怪怪的如[代码].github[代码]的东西,所以直接使用dist目录),改名为[代码]vant[代码](也可以不改名)。全局使用时,可以在[代码]app.json[代码]引入: [代码] "usingComponents": { "van-button": "/static/vant/button/index", "van-field": "/static/vant/field/index" }, [代码] 注意:需要打开微信开发者工具中的ES6转ES5功能 一开始以为使用起来和web端的没啥差别,但没想到那么麻烦。比如:在vue中是可以使用[代码]v-model[代码]的,但在mpvue中的小程序中不能使用,只能 [代码]<van-field :value="password" type="password" @change="pwdChange" input-class="myClass" /> [代码] 而且不能随意灵活添加class修改组件的样式,需要vant组件支持提供外部样式才可修改,比如上面的[代码]van-field[代码]是通过[代码]input-class[代码]来添加样式控制的,很不方便。而且某些内部样式由于没有外部样式表,根本改不了。 综上: 在微信小程序使用第三方组件库不太方便,样式修改比较麻烦,如果产品是有UI设计时,尽量不使用,有时候自己实现样式可能更快,而且项目体积更小。 使用vuex mpvue官方的快速模板中是将vuex放在[代码]counter[代码] 这个page目录下,可能习惯了vue官方写法的很多同学(包括我)不太喜欢,所以最好就改为vuex官方的写法。 在src目录下建一个[代码]store[代码]的文件夹,分别建以下文件: [图片] 项目不太复杂时不建议使用modules,使用起来比较麻烦。 贴一下[代码]index.js[代码]的代码,其他的[代码]actions.js[代码],[代码]getters.js[代码]按官方的写法就好啦。 [代码]import Vue from 'vue' import Vuex from 'vuex' import * as actions from './actions' import * as getters from './getters' import state from './state' import mutations from './mutations' import createLogger from 'vuex/dist/logger' Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' export default new Vuex.Store({ actions, getters, state, mutations, strict: debug, plugins: debug ? [createLogger()] : [] }) [代码] [代码]vuex/dist/logger[代码]是vuex在开发环境可以自动打印日志的工具,debug比较方便,建议使用。 然后在[代码]src/main.js[代码]引入: [代码]import Vue from 'vue' import App from './App' import store from '@/store' Vue.config.productionTip = false App.mpType = 'app' Vue.prototype.$store = store const app = new Vue({ store }) app.$mount() [代码] 这样就可以在项目中正常使用啦,完全支持[代码]mapState[代码],[代码]mapActions[代码],[代码]mapGetters[代码]的写法,比如在[代码]pages/index/index.vue[代码]中使用: [代码]<script> import { mapState, mapActions } from 'vuex' export default { computed: { ...mapState(['myAudio']) }, methods: { ...mapActions(['myActions']) }, created () { this.myActions() //调用vuex中的方法 } } </script> [代码] 踩坑指南 其实大多数坑可能是mpvue的,很多情况也是自己不熟悉小程序生命周期导致的一些奇奇怪怪的bug。 mpvue是支持小程序原生组件的 mpvue会将[代码]div[代码]编译为小程序中的[代码]view[代码]。一开始我不了解,以为用了mpvue后就不能使用小程序原生支持的组件了,比如[代码]swiper[代码],[代码]scroll-view[代码]等,小程序是支持的,可以放心使用哈哈。 npm run build后样式丢失 本来在开发环境正常的,然后准备发版[代码]npm run build[代码]后发现样式丢失了。然后重新[代码]npm start[代码]排查问题,样式还是丢失的。内心此时是mmp的:npm run build丢失就算了,我没改什么东西重新npm start后为什么还是丢失,之前还是正常的呀? 刚开始怀疑是缓存什么的问题,删掉的dist目录,重启开发者工具,甚至重启电脑都试了一下,这是我遇到的超级诡异的问题之一。 冷静下来想到:之前的版本是正常的,一定是新版本引入了什么导致了打包样式的丢失。于是回滚版本一个个build排查问题,最后找到了原因:在一个page中引入了其他page,即在页面中import另一个页面。 在我这里的具体例子是:我在[代码]pages/index/index.vue[代码] 中想做底部共用一个tabbar,页面根据tabbar的值来显示对应的子级页面:[代码]pages/page1/index.vue[代码]和[代码]pages/page2/index.vue[代码]。 所以我将这两个页面当做子组件来引入了:[代码]import Page1 from '@/pages/page1'[代码],一开始没有问题,等重启项目,或者build后就发现样式丢失了。 这可能是mpvue打包机制的一个限制,即[代码]页面不能将另一个页面当子组件来引用[代码],否则会导致样式丢失。 背景音频的src无法读取 项目中希望用户退出小程序后依然能播放音频,所以用到了背景音频的api: wx.getBackgroundAudioManager()。 [代码]this.audio = wx.getBackgroundAudioManager() this.audio.src = 'http://ws.stream.qqmusic.qq.com/M500001VfvsJ21xFqb.mp3?guid=ffffffff82def4af4b12b3cd9337d5e7&uin=346897220&vkey=6292F51E1E384E061FF02C31F716658E5C81F5594D561F2E88B854E81CAAB7806D5E4F103E55D33C16F3FAC506D1AB172DE8600B37E43FAD&fromtag=46' this.audio.title = '此时此刻' //注意必填 this.audio.epname = '此时此刻' this.audio.singer = '许巍' this.audio.coverImgUrl = 'http://y.gtimg.cn/music/photo_new/T002R300x300M000003rsKF44GyaSk.jpg?max_age=2592000' [代码] [代码]title[代码]和[代码]src[代码]赋值后会直接播放音频,后面的几个属性建议也填上,因为播放背景音频时微信是有个界面需要封面图和歌手名称等的。 如果想要获取当前正在播放的音频src,本来以为通过[代码]this.audio.src[代码]来获取就可以了但是有bug。 在开发者工具中是可以正常获取的,即开发时是没问题的,但在真机上返回的是[代码]undefined[代码],因此不能用[代码]this.audio.src[代码]来获取当前播放的音频url,得用一个变量来存这个数据。 直接使用音频的currentTime可能渲染不及时 currentTime用于显示当前的播放进度,但我用在子组件中时经常更新不及时,打印是正常的,但试图渲染不及时,有时候需要点击一下才能重新渲染,这可能是mpvue使用时才会遇到。 所以建议还是项目自身维护一套背景音频的变量比较好一点,比如放在[代码]vuex[代码]中。监听[代码]BackgroundAudioManager.onTimeUpdate()[代码]方法每次赋值到自身维护的变量中。 音频的onCanplay方法不一定每个音频都会触发 一开始我监听在[代码]onCanplay[代码]方法,将音频的时长信息[代码]duration[代码]赋值到vuex中存起来,但发现[代码]onCanplay[代码]有时候是不会触发的,比如重新赋值src播放下一首时,很尴尬。 所以不要太依赖onCanplay这个方法,还好目前直接使用[代码]audio.duration[代码]好像不会出现像上面的[代码]currentTime[代码]渲染不及时的问题,所以就这样用着先。 音频播放结束,即onStop后,不能再通过audio.play()的方法重新播放,得重新赋值src 正常来说,音频播放结束后,音频的src是不变的,再次[代码]play()[代码]应该是可以的。但在小程序中偏偏不行,得重新赋值src才能重新播放,这应该是小程序的一个bug。。。 所以需要判断一下暂停和停止的情况,用不同的办法播放。正常来说,音频暂停时[代码]currentTime[代码]是不为0的,而结束时[代码]currentTime[代码]会为0。 所以可以通过[代码]currentTime[代码](最好是自己维护的变量)来判断暂停和停止的情况:如果currentTime不为0,表示是暂停的情况,可以用[代码]play()[代码],如果小于等于0,则重新赋值src播放: [代码]if (currentTime) { this.audio.play() } else { this.audio.src = 'xx.mp3' } [代码] mpvue不支持直接在template上直接绑定函数 这个是mpvue文档上有写的,不过一开始并不是很理解,也踩坑了,所以在这里提一下,避免不知道的同学踩坑找半天。 [代码]<template> <div v-for="(item, index) in list" :key="index">{{ formatItem(item) }}</div> </template> <script> export default { data () { return{ list: [1, 2, 3] } }, methods: { formatItem (item) { return `我是${item}` } } } </script> [代码] 上面的代码应该是日常vue中比较常用的,就是将数据传参给方法做一些处理,这个在mpvue中是不支持的,会被编译成一个空字符串。 小程序中可放心使用css3的一些特性 比如高斯模糊 [代码]filter: blur(50px); [代码] 如果要使用动画,尽量用[代码]css[代码]动画代替[代码]wx.createAnimation[代码] 在实际使用时,[代码]wx.createAnimation[代码]做动画其实很卡,性能很差,所以在需要使用动画时,建议尽量使用css做动画。 在小程序中是支持css动画的,[代码]transition[代码],[代码]animation[代码],[代码]@keyframes[代码]这些特性都支持。 比如做一个div一直旋转的动画,大家可以对比一下两个版本: [代码]wx.createAnimation[代码]版本 原理:通过setInterval()不断更新div的旋转位置 [代码]<template> <div class="cover" :animation="animationData"></div> </template> <script> export default { data () { return { animationData: '', animation: '', rotateCount: 0, timer: '' } }, components: { }, methods: { startRotate () { this.timer = setInterval(() => { this.rotateAni(++this.rotateCount) }, 100) }, rotateAni (n) { if (!this.animation) { return } // 每100毫秒旋转10度 this.animation.rotate(10 * n).step() this.animationData = this.animation.export() } }, onShow () { // 页面从隐藏到显示时才执行 if (!this.animation) { this.animation = wx.createAnimation() this.startRotate() } }, onReady () { // 第一次初始化时会执行 if (!this.animation) { this.animation = wx.createAnimation() this.starRotate() } }, onHide () { // 页面隐藏时会执行,避免频繁的setData操作,将定时器停掉 this.timer && clearInterval(this.timer) }, beforeDestroy () { // 页面卸载,也停掉定时器 this.timer && clearInterval(this.timer) } } </script> <style scoped lang="scss"> .cover { left: 20px; bottom: 70px; border-radius: 50%; background: #fff; position: absolute; width: 50px; height: 50px; background: rgba(0, 0, 0, 0.2); box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.5); overflow: hidden; z-index: 10000; } </style> [代码] 使用css的[代码]@keyframes[代码]做旋转动画 [代码]<template> <div class="cover" :style="coverStyle"></div> </template> <script> export default { } </script> <style scoped lang="scss"> // 定义一个动画名为 rotate @keyframes rotate { 0%, 100% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .cover { left: 20px; bottom: 70px; border-radius: 50%; background: #fff; position: absolute; width: 50px; height: 50px; background: rgba(0, 0, 0, 0.2); box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.5); overflow: hidden; z-index: 10000; // 使用动画 animation: rotate 4s linear infinite; } </style> [代码] 用js写的动画需要控制好setInterval的间隔时间和旋转角度,比较难调。而用css写动画很简单,性能比js好,代码量也很少。 使用css动画时建议开启硬件加速 为了动画更流畅,想尽办法做优化,虽然不知道有没效果,反正用了再说[手动滑稽]。 可以用will-change和transform: translate3d(0,0,0)开启硬件加速。我也不太会用,具体用法大家自行百度Google。 [代码]will-change: auto; transform: translate3d(0, 0, 0); [代码] iPhoneX需要底部导航条预留34px(68rpx)的高度。 由于小程序中不能设置[代码]viewport-fit=cover[代码],所以也就没有web中的安全区域说法,目前主流的做法是通过[代码]wx.getSystemInfoSync()[代码]判断是否是ipx,若是则给页面底部撑高34px。 [代码]const res = wx.getSystemInfoSync() if (res.model.indexOf('iPhone X') >= 0) { this.isIpx = true } [代码] 注意是用[代码]res.model.indexOf('iPhone X')[代码],在开发者工具的iPhone X中,model是全等于[代码]iPhone X[代码]的,但在真机中往往拿到的值是[代码]iPhone X GZxxx[代码],即后面可能会带一串东西,所以用[代码]indexOf[代码]才是比较稳的,而且对[代码]iPhone XR[代码]等机型也适用。 由于还有其他安卓机的全面屏,不太可能一一判断,而且某些安卓全面屏是没有用iPhone底部的工具条的(不存在冲突的情况),所以我们只判断iPhone X的情况就可以了,其他全面屏就不需要给底部预留了。 至于全面屏布局的适配,需要用[代码]flex[代码]布局或者获取屏幕宽高来慢慢调了,建议最好用flex布局自适应处理。 for循环中的子组件click事件无法触发 [代码]Page -> 父组件 -> 子组件[代码],在子组件click后[代码]$emit[代码]一个事件出来,发现无法触发。 这个bug一开始没有出现,但偶然[代码]npm run build[代码]出现的,然后排查原因,后面即使回滚所有版本再npm start也还会出现。好像不触发则已,一发就不可收拾,这又是一个大坑,搜issue和加群问人,当晚下班回家研究到1点多都没有解决。 第二天继续研究,感觉可能是框架的原因,最后尝试升级一下mpvue版本,没想到就正常了。直接使用quick-strat项目的[代码]mpvue[代码]版本是 2.0.0,[代码]mpvue[代码]和[代码]mpvue-template-compiler[代码]升级到最新[代码]2.0.6[代码]就解决了。 事后查看mpvue版本记录,果然是框架本身原因,并且找到了issue。 npm run build后代码报错,再build一次可能报另一些错 解决: 没找到原因,可能是引入vant导致的,打包时丢失了部分文件。多build几次,或者重启下小程序开发者工具就正常了。 mpvue中created() 钩子会在页面初始化时全部一起触发,尽量不要用 小程序生命周期的理解 进入已销毁的page组件时依次触发: onLoad,onShow,onReady,beforeMount,mounted 第一次进入已销毁的子组件时依次触发: onLoad,onReady,beforeMount,mounted 第二次进入已销毁的子组件时依次触发: onLoad,onShow,onReady 再次进入 未被销毁的page组件、子组件时只触发: onShow mpvue文档中建议尽量不要使用小程序的生命周期,这个应该是为了让项目更好地适应支付宝小程序和头条小程序等,所以才这样建议大家尽量不要使用某一个小程序自身的api。 如果你们的小程序只是微信小程序(不考虑兼容其他平台小程序),我建议直接用小程序的生命周期,而不要用mpvue的生命周期,坑太多了。比如mpvue的created周期,初始化时所有的page都会执行,所以created这个周期是不能用了。 onUnload不触发 小程序中与平常web开发不同的是,它的页面会被缓存。举个例子: 从[代码]page1[代码]跳转到[代码]page2[代码],再从[代码]page2[代码]返回[代码]page1[代码],此时的[代码]page1[代码]还没销毁,不会触发[代码]onLoad[代码]再重新渲染,而是直接使用之前的数据。从性能上来说,单纯的返回不应该再请求api获取数据重新渲染,这是对的,符合我们的预期。 而有时候,从[代码]page2[代码]返回[代码]page1[代码]时,我们希望[代码]page1[代码]是重新获取数据渲染的。比如在[代码]page2[代码]做了一个退出登录的操作,此时再返回[代码]page1[代码]时,还是会看到之前的数据。实际上我们的预期是:由于已经退出登录了,[代码]page1[代码]的数据应该被销毁了。 在平常的web开发中,遇到上面的问题,我们可能是不管缓存,每次返回[代码]page1[代码]都再次请求api渲染最新的数据,牺牲掉部分性能从而保证逻辑的正确性。 在mpvue中我也尝试这样干了:想在[代码]page1[代码]的[代码]onUnload()[代码]生命周期中销毁数据,但是没有成功。即使在[代码]page2[代码]退出登录时,采用[代码]wx.reLaunch()[代码]重新刷一遍,[代码]page1[代码]的[代码]onUnload()[代码]生命周期也没有执行。所以[代码]onUnload()[代码]是有可能不执行的,建议慎用。 最后还是得想办法做到在[代码]page2[代码]控制[代码]page1[代码]的数据销毁或保留。想到这里,[代码]vuex[代码]就不自觉浮现在眼前了,如果page1的数据是通过vuex来控制的,那么我在page2就可以用vuex来灵活管理其他页面的数据了。 如果page2做退出登录操作时,就让page1的数据销毁,如果是不退出登录正常返回,page1的数据还是正常,做到灵活控制。 个人平时web开发很少用[代码]vuex[代码],因为项目比较简单不用那么复杂的全局数据传递。但在小程序中,建议全局使用[代码]vuex[代码]来控制所有数据(当然是得根据需求来用)。 总结 第一次开发小程序就直接上了mpvue,可能有些坑已经很多同学总结过了,有些坑可能是不熟悉而导致的,但自己没有去踩过一遍可能不够深刻。 有两种坑会比较难啃: 框架本身的问题,如mpvue2.0.0出现的子组件无法触发事件的问题。 开发者工具和真机运行环境不一致导致的坑。 遇到真机和开发者工具不一致的情况,可按以下步骤排查: 有可能是缓存,可以杀掉之前的版本再跑起来 手机微信版本太低,可能api不支持,用[代码]wx.canIUse[代码]打印一下 手机端某些属性不支持读取,比如上面的[代码]this.audio.src[代码],可以在真机打印调试一下 代码在手机端运行有报错,可以在手机端开启调试,看一下log 微信设计上的坑,百度下是否有相关的案例和解决办法 而遇到mpvue框架的问题可以: 去搜一下[代码]mpvue[代码]的issue看有没相关解决办法 尽量使用最新版本的框架,可能某些问题已经修复了的。实在解决不了的,建议想办法绕过,换一种方法来实现。 希望对大家有所帮助。
2019-03-12