- 你不知道的小程序系列之生命周期执行顺序
再次开始之前先问几个问题: 你是否知道[代码]Page[代码]生命周期 与 [代码]pagelifetimes[代码] 生命周期执行顺序? 你是否知道[代码]behaviors[代码]中的生命周期与组件生命周期执行顺序? 你是否知道[代码]Page[代码]生命周期 与 组件[代码]pagelifetimes[代码]生命周期执行顺序? 要回答上面的问题,首先我们看看小程序生命周期有哪些: App onLaunch onShow onHide Page onLoad onShow onReady onHide onUnload Component created attached ready moved detached 想一下加载一个页面(包含组件)的加载顺序,按照直觉小程序加载顺序应该是这样的加载顺序(以下列子中[代码]Component[代码]都是同步组件): App(onLaunch) -> Page(onLoad) -> Component(created) 但其实并不然,小程序的加载顺序是这样的: 首先执行 [代码]App.onLaunch[代码] -> [代码]App.onShow[代码] 其次执行 [代码]Component.created[代码] -> [代码]Component.attached[代码] 再执行 [代码]Page.onLoad[代码] -> [代码]Page.onShow[代码] 最后 执行 [代码]Component.ready[代码] -> [代码]Page.onReady[代码] 其实也不难理解微信这么设计背后的逻辑,我们先看下官方的的生命周期: [图片] 可以看到,在页面[代码]onLoad[代码]之前会有页面[代码]create[代码]阶段,这其中就包含了组件的初始化,等组件初始化完成之后,才会执行页面的[代码]onLoad[代码], 之后页面[代码]ready[代码]事件也是在组件[代码]ready[代码]之后才触发的。 下面我们来看看 [代码]Behavior[代码], [代码]Behavior[代码] 与 [代码]Vue[代码]中的 [代码]mixin[代码] 类似,猜想下其中的执行顺序: Behavior.created => Component.created 测试下来和预期相符,其实在[代码]Vue[代码]的文档中有一段这样的描述: 另外,混入对象的钩子将在组件自身钩子之前调用。 这样的设计和主流设计保持一致。接下来我们看看 [代码]pageLifetimes[代码],有[代码]show[代码]和[代码]hide[代码]生命周期对应页面的展示与隐藏,预期的执行顺序: pageLifetime.show => Page.onShow 测试下来也和预期相符,那么我们可以推断出如下的结论: 当页面中包含组件时,组件的生命周期(包括pageLifetimes)总是优先于页面,[代码]Behaviors[代码]生命周期优先于组件的生命周期。但其实有个例外:页面退出堆栈,当页面[代码]unload[代码]时会执行如下顺序: Page.onUnload => Component.detached 看了以上的分析你应该知道了答案,最后做个总结(demo): [图片] 最后的最后布置个作业 异步组件(异步渲染的组件,通常是通过if条件判断是否渲染)的生命周期执行顺序是怎样的,pagelifetimes会不会执行?
2020-01-10 - 小程序生成海报经验分享
场景 由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有二维码的图片,然后引导用户下载图片到本地后再分享到朋友圈 遇到的难点 本来打算用canvas自己去画,但是在操作的过程中遇到两个问题 1、文本太长,需要换行的问题 技术方案 https://github.com/Kujiale-Mobile/Painter 演示 [图片] 以上文章参考并摘录自下面相关链接 小程序海报生成工具,可视化编辑直接生成代码使用,你的海报你自己做主 https://developers.weixin.qq.com/community/develop/article/doc/000e222d9bcc305c5739c718d56813 Painter 一款轻量级的小程序海报生成组件 https://developers.weixin.qq.com/community/develop/doc/000048447844f80b9107d64ab51006 利用云开发实现个性海报制作 https://developers.weixin.qq.com/community/develop/article/doc/000ea8817d0b504e08b921b275b413
2020-01-17 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - vant + recycle-view 常见长列表实战demo
recycle-view 常列表实战 昨天拿到一个需求 [图片] 分解需求: 自定义标题栏 搜索框禁用,做点击跳转到搜索详情页 分类,左右滑动 点击菜单展示所有分类 tabs吸顶 常列表 需求是老项目新增需求,前期类似的需求都是手写原生,后期用了vant Ui。维护好难-。-,标题栏统一处理忽略,接下来就是处理 vant ui 和 常列表,先对着原型撸个demo,代码片段在最后。 [图片] vant ui 之前打包已经2M多了,在用一个ui框架的话后期维护真的。。所以继续 vant ui 使用的组件 index.json [代码] // dropdown-menu 待定 { "usingComponents": { "recycle-view": "miniprogram-recycle-view/recycle-view", "recycle-item": "miniprogram-recycle-view/recycle-item", "van-tab": "@vant/weapp/tab/index", "van-tabs": "@vant/weapp/tabs/index", "van-dropdown-menu": "@vant/weapp/dropdown-menu/index", "van-dropdown-item": "@vant/weapp/dropdown-item/index", "van-icon": "@vant/weapp/icon/index", "van-popup": "@vant/weapp/popup/index", "van-search": "@vant/weapp/search/index" } } [代码] 代码片段:https://developers.weixin.qq.com/s/1ZmiCGm87qdb 删除了node_modules,太大了超过一M不能上传代码片段,会报错请忽略,介意的话 npm 重新安装走一遍。 总结 有点儿短~ 想水一篇文章的,发现没得写。。。水文
2019-12-17 - 小程序顶部导航栏,可滑动,可动态选中放大
最近在研究小程序顶部导航栏时,学到了一个不错的导航栏,今天就来分享给大家。 老规矩,先看效果图 [图片] 可以看到我们实现了如下功能 1,顶部导航栏 2,可以左右滑动的导航栏 3,选中条目放大 原理其实很简单,我这里把我研究后的源码发给大家吧。 wxml文件如下 [代码]<!-- 导航栏 --> <scroll-view scroll-x class="navbar" scroll-with-animation scroll-left="{{scrollLeft}}rpx"> <view class="nav-item" wx:for="{{tabs}}" wx:key="id" bindtap="tabSelect" data-id="{{index}}"> <view class="nav-text {{index==tabCur?'tab-on':''}}">{{item.name}}</view> </view> </scroll-view> [代码] wxss文件如下 [代码]/* 导航栏布局相关 */ .navbar { width: 100%; height: 90rpx; /* 文本不换行 */ white-space: nowrap; display: flex; box-sizing: border-box; border-bottom: 1rpx solid #eee; background: #fff; align-items: center; /* 固定在顶部 */ position: fixed; left: 0rpx; top: 0rpx; } .nav-item { padding-left: 25rpx; padding-right: 25rpx; height: 100%; display: inline-block; /* 普通文字大小 */ font-size: 28rpx; } .nav-text { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; letter-spacing: 4rpx; box-sizing: border-box; } .tab-on { color: #fbbd08; /* 选中放大 */ font-size: 38rpx !important; font-weight: 600; border-bottom: 4rpx solid #fbbd08 !important; } [代码] js文件如下 [代码]// pages/test2/test2.js Page({ data: { tabCur: 0, //默认选中 tabs: [{ name: '等待支付', id: 0 }, { name: '待发货', id: 1 }, { name: '待收货', id: 2 }, { name: '待签字', id: 3 }, { name: '待评价', id: 4 }, { name: '五星好评', id: 5 }, { name: '差评订单', id: 6 }, { name: '编程小石头', id: 8 }, { name: '小石头', id: 9 } ] }, //选择条目 tabSelect(e) { this.setData({ tabCur: e.currentTarget.dataset.id, scrollLeft: (e.currentTarget.dataset.id - 2) * 200 }) } }) [代码] 代码里注释很明白了,大家自己跟着多敲几遍就可以了。后面会更新更多小程序相关的知识,请持续关注。
2019-11-22 - 做了一个颜色选择器
edit at 11/12 代码传到了:https://github.com/eclipseglory/zasi-components , DEMO演示在文章结尾 小程序没有提供color-picker类似的组件,只能自己做。 可传统的RGB颜色选择器,真的腻了,而且在手机上也不是很操作,就跑网上搜了一圈,发现有一种圆环形的(基于HSV)我很喜欢: [图片] 我自诩对canvas2d和webgl很熟悉,做个这玩意儿很轻松,开始做!没想到痛苦开始了。 从上周5开始,一共做了三个版本: 1.纯canvas版本 2.canvas+组件版本 3.纯组件版本 纯canvas版本这个版本做了整整一天! [图片] 由于canvas绘制性能问题,特别是因为没有requestAnimationFrame可以调用,别说在真机上测试特别不流畅,就是在模拟器上也小卡小卡的。而且,在纯的canvas进行触摸定位等事件响应处理,计算起来太麻烦,bug不断,只能放弃了。 混合版本因为wxs模块是提供requestAnimationFrame接口的,所以我就想,使用canvas作为底部颜色环,上面就直接用view作为指针,这样,事件触发和处理比起纯canvas要简单得多,而且还能利用rAF回调页面接口去绘制其他canvas。 的确,我的想法得到了证实,这个混合版本比起第一个要流畅得多! 可就要完工的时候,我却发现,在真机上,cover-view的鼠标事件有很大问题,坐标值飘忽不定,也就是说拖动指针会发生鬼畜般的抖动!加上我不知道怎么debug到wxs模块中,于是跟个sb一样fix,找了半天也没找到问题在哪儿,直到我搜索时,返现有人也遇到和我一样的问题,我才安心了:这是小程序的问题。 动手改!既然cover-view有不行,那就不用它。 实际上canvas在该组件中的作用无非就是绘制一个圆环而已,如果我利用离屏canvas事先画好,然后保存成图片,再用image加载它,这样就可以避免使用canvas来显示圆环了,也就可以不用cover-view放到其顶部! 想法是好的,可是到了真机上,绘制保存出来的图片时好时坏: [图片] 只能放弃,又耽误我一天。 无canvas版本刚才说了,canvas在该组件中的作用,仅仅是绘制一个颜色环而已,除此之外真没什么用。 那我就用css模拟一个类似圆环就好了,精确到每一度一个颜色一点意义没有。 所以就利用css的background-image属性,做了4个四分之一圆弧,然后拼在一起,得到了一个彩色原版,再用一个小的view遮挡,让它们只露出一部分,圆环就做好了。 之前的代码都不用改,直接用新作的圆环views替换canvas的标签即可。主体框架和功能,不到一天就完成了,不得不说,比起纯的canvas绘制,要方便太多太多。 这是截图: [图片] 代码片段这里是 演示DEMO,要使用的话,复制里面的组件出来用就好。 有些代码我混淆过,但不耽误使用。 有问题找我
2019-11-12 - 小程序多端差异调研报告(微信,支付宝,头条,QQ)
已经使用uni-app开发并发布了一个跨端小程序啦,嘻嘻嘻! 🧐 须知 这是一份详细的小程序各特性各端真机调研对比报告 测试机:iPhone7 plus IOS 12.4.1 客户端:微信7.0.5,支付宝10.1.72,今日头条7.4.0,抖音8.1.0,QQ8.1.5.461 🚫️ 百度小程序只有商户才能注册,个人开发者无法注册,没有appid功能受限(如百度开发者工具无法使用预览功能导致无法真机测试),所以暂时不测百度小程序 用户信息授权 授权方式: 【头条】用户信息授权方式还停留在微信小程序第一版,即直接调用 getUserInfo 弹出授权弹窗,如果用户选择允许,则后续调用不再出弹窗,而是直接走 success 回调。如果用户选择取消,则后续调用也不再出弹窗,而是直接走 fail 回调 【微信】【QQ】【支付宝】则采用 button + 回调事件的方式调起授权弹窗,如果用户选择允许,则后续点击不再出弹窗,直接走回调。如果用户选择取消,则后续点击继续弹窗询问授权 授权信息清除方式: 【微信】删除小程序即可清除授权信息 【支付宝】我的-设置-安全设置-账号授权 【今日头条】我的-系统设置-清除缓存。【抖音】未找到清除方法 【QQ】未找到清除方法(据说开放小程序的QQ版本尚未灰度发布) 小程序登录 【微信】wx.login 【QQ】qq.login 基本同微信 【支付宝】my.getAuthCode 【头条】大致同微信,未找到模型文档 分享 行为: 【微信】直接调起聊天对话列表进行选择 【QQ】调起分享渠道列表: QQ好友 QQ空间 点右上角三个点调起的列表还有微信好友和朋友圈两个项,在微信中打开qq小程序是走中间页 【支付宝】调起分享渠道列表: 支付宝朋友圈 支付宝联系人 微信好友|QQ好友(保存支付宝生成的分享图片后打开支付宝扫码) 钉钉好友(中间页自动打开支付宝小程序,中间页不自动关闭) 新浪微博(中间页自动打开支付宝小程序,和钉钉一个中间页) 【头条】调起分享渠道列列表: 转发到头条 微信好友|微信朋友圈(生成口令,复制口令后打开今日头条弹出识别弹窗) QQ|QQ空间(打开中间页,点击打开(QQ空间点了没反应),出现另一个中间页,自动打开AppStore,再点打开调起今日头条,最后居然没打开那个小程序🥴!!!) 【抖音】调起分享渠道列列表: 多闪好友 微信好友|微信朋友圈|QQ好友|QQ空间(生成抖音码图片,打开抖音扫码识别) 【头条】webview的转发暂未支持: 【今日头条】能转发,但转发的链接点击后总是提示加载失败!也可能是小程序未发布的原因,扫uni-app官方demo进行 webview转发是能正常打开的 【抖音】不支持转发,右上角胶囊只有一个关闭按钮 跳转到其他小程序 【微信】支持(navigateToMiniProgramAppIdList + navigateToMiniProgram) 【QQ】支持 【头条】支持(navigateToMiniProgramAppIdList + navigateToMiniProgram) 【支付宝】支持(后台配置 + navigateToMiniProgram) 🚫️ ️QQ,支付宝和头条未真机验证,因为须要一个其他小程序的appId 客服会话 【微信】支持(button open-type=“contact”) 【QQ】支持,须用户加一个客服机器人为好友 【支付宝】支持(contact-button) 【头条】不支持。 支付 【微信】支持(调起微信支付) 【QQ】支持(调起QQ支付) 【支付宝】支持(调起支付宝支付) 【头条】支持(调起支付宝App进行支付) 🚫 ️QQ,支付宝和头条未真机验证,因为支付接口只有商户才有权限 地理位置 【微信】支持(须在app.json中配置permission字端),用户拒绝授权后再次调用不再出询问弹窗,而是直接走fail回调 【QQ】支持。真机行为同微信。QQ开发者工具上拒绝授权再次调用仍会出询问弹窗 【头条】支持,同微信 【支付宝】支持,用户拒绝授权后再次调用继续出询问弹窗 视频播放 【微信】支持 【QQ】支持 【头条】支持 【支付宝】支持?(uni-app里说支付宝不支持,支付宝文档也没找到video组件,但放在页面里video能正常渲染和播放,难道是昨天刚支持🤔) 复制文字 行为: 【微信】【QQ】复制成功后有一个默认的复制成功toast且无法控制 【支付宝】【头条】复制成功后没有toast 权限: 【支付宝】my.setClipboard 此功能仅支持企业支付宝账号。实际情况是:在IDE上个人账号是可以复制的,但在真机上调用就会报 [代码]ERROR 4: 无权调用该接口[代码] 错误 【微信】【QQ】【头条】无限制 打电话 【微信】【QQ】【支付宝】【头条】都支持 收货地址 【微信】支持 【QQ】不支持 【头条】支持(实测【今日头条】支持,【抖音】不支持) 【支付宝】支持。但仅商户才有使用权限。且目前 my.getAddress 接口暂不支持在开发者工具调试和真机调试,仅支持真机预览 相机/图片相关 拍照/相册选图片 【微信】【QQ】支持 【支付宝】支持。IDE上会弹一个相册授权询问弹窗,真机上并没有弹窗 【头条】支持。但会弹出两个询问弹窗(相机权限,相册权限) 拍摄/相册选视频 【微信】【QQ】支持 【支付宝】支持。IDE上会弹一个相册授权询问弹窗,真机上并没有弹窗。须调用 my.chooseVideo(文档未找到),uni.chooseVideo会报错 【头条】支持。但会弹出两个询问弹窗(相机权限,相册权限) ⚠️chooseVideo的maxDuration选项在【微信】和【支付宝】是只限制拍摄时长,在【头条】是同时限制相册选择视频时长和拍摄时长 图片预览 【微信】【QQ】【支付宝】【头条】都支持 保存图片到相册 【微信】【QQ】【头条】支持,弹窗仅询问一次 【支付宝】tt.saveImageToPhotosAlbum 在IDE上报错 [代码]tt.saveImageToPhotosAlbum is not a function[代码],在真机上报错 [代码]无权调用该接口[代码],文档未提及,猜测是仅商户可用,且不支持在开发者工具调试和真机调试,仅支持真机预览 接口返回值差异 getUserInfo【微信】【QQ】【支付宝 】【头条】 [代码]// 支付宝 { 'nickName': 'test', 'gender': 'm', 'city': '北京市', 'province': '北京' 'countryCode': 'CN', 'avatar': 'https:\/\/tfs.alipayobjects.com\/images\/partner\/T1_38eXnRiXXXXXXXX', 'code': '10000', 'msg': 'Success', } // 微信 { 'nickName': 'test', 'gender': 1, 'city': 'Xinxiang', 'province': 'Henan', 'country': 'China', 'avatarUrl': 'https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJCzUl7llykqrMLicpULvVfkbbL2bVDua4tI8ibjxq5E9ib1oPW3F4QazLIUdS2GsFMAGnrWSYjN05Ew/132' 'language': 'zh_CN', } // QQ { 'nickName': 'test', 'gender': 1, 'city': '新乡', 'province': '河南' 'country': '中国', 'avatarUrl': 'https://thirdqq.qlogo.cn/qqapp/1108100302/D64611B2AE700324589177922EEBA5F4/100', 'language': 'zh_CN', } // 头条系(今日头条,抖音,皮皮虾,西瓜视频分别取各自用户信息) { 'nickName': 'test', 'gender': 1, 'city': '新乡市', 'province': '河南省' 'country': '中国', 'avatarUrl': 'http://wx.qlogo.cn/mmhead/Q3auHgzwzM5uibSytRCXFs0Y3xSpdy12thibjWIoMrBIsf7FiaPp2ibnFg/0', 'language': '', } [代码] getSetting【微信】【支付宝 】【头条】【QQ】 [代码]// 微信 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html [ 'scope.userInfo', // 用户信息 'scope.userLocation', // 地理位置 'scope.address', // 通讯地址 'scope.record', // 录音功能 'scope.camera', // 摄像头 'scope.writePhotosAlbum', // 保存到相册 'scope.userLocationBackground', // 后台定位 'scope.invoiceTitle', // 发票抬头 'scope.invoice', // 获取发票 'scope.werun', // 微信运动步数 ] // 头条 https://developer.toutiao.com/dev/miniapp/uQjMy4CNyIjL0IjM [ 'scope.userInfo', // 用户信息 'scope.userLocation', // 地理位置 'scope.address', // 通讯地址 'scope.record', // 录音功能 'scope.camera', // 摄像头 'scope.album', // *保存到相册* ] // 支付宝 https://docs.alipay.com/mini/api/xmk3ml#-1 [ 'userInfo', // 用户信息 'location', // 地理位置 'audioRecord', // 录音功能 'camera', // 摄像头 'album', // 保存到相册 ] // QQ https://q.qq.com/wiki/develop/game/frame/open-ability/authorize.html [ 'scope.userInfo', // 用户信息 'scope.userLocation', // 地理位置 'scope.qqrun', // QQ运动步数 'scope.writePhotosAlbum', // 保存到相册 'scope.appMsgSubscribed', // 订阅消息 ] [代码] 主要入口 【微信】 首屏对话列表下拉 扫一扫 发现->小程序 搜索 【支付宝】 扫一扫 搜索 首页我的小程序 【今日头条】 我的->扫一扫 搜索 【抖音】 搜索->扫一扫 【QQ】 扫一扫 💣 头条小程序陷阱 目前仅在头条Android版本7.2.9及以上版本支持真机调试功能。iOS暂时不支持真机调试 抖音App的小程序上没有打开调试器选项,右上角胶囊只有一个关闭按钮 💣 支付宝小程序陷阱 my.getOpenUserInfo用于获取支付宝会员基础信息,只能在真机上调试,无法在 IDE 中调试,也就是只要有用户授权的页面都需要推送到真机上开发调试! 支付宝授权平台只返回tocken和uid,由开发者自己维护session有效期,[代码]checkSession[代码] 方法不可用 打开调试的调试器面板在调起用户授权弹窗时会消失,此时须使用真机调试 💣 uni-app 陷阱 uni.getSetting,文档上说【支付宝】支持,调用却报错 [代码]支付宝小程序,暂不支持getSetting[代码],而直接调支付宝的api my.getSetting 确是支持的 uni.chooseVideo,文档上说【支付宝】支持,调用却报错 [代码]支付宝小程序,暂不支持chooseVideo[代码],而直接调支付宝的api my.chooseVideo(文档未找到) 确是支持的 uni.chooseAddress,文档上说【支付宝】不支持,实际上是支持的,只是需要调用 my.getAddress,且仅商户才能使用 uni.getImageInfo,文档上说【头条】支持,调用却报错 [代码]头条小程序,暂不支持getImageInfo[代码],而直接调头条的api tt.getImageInfo 确是支持的 📌 TODO 模版消息 第三方插件 uni-app 跨端小程序风险点 后端接口。不同端的后端接口不一样,需要后端评估一下。举例:模版消息(微信|支付宝|头条);设计用户系统时需注意微信和QQ都有各自的openID和unionID,支付宝只有uid,头条只有openID;接入微信,QQ,支付宝支付时各种传参不一样 分享转发。支付宝,头条小程序分享至微信和QQ的主要方式是生成口令或者生成小程序码图片或者走中间页,导致传播路径较长 某些端重要功能缺失。举例:【头条】不支持客服会话。【抖音】不支持webview转发。【QQ】不支持收货地址 某些端api缺失,可能导致某些功能无法实现 第三方插件支持度
2019-11-02 - 借助云开发实现婚礼小程序
原文链接(一) 原文链接(二) 最近时间充裕,索性对原来仓促做给自己结婚用的小程序来个整体优化,相关内容和功能请查看相关两篇文章介绍。 这次优化主要功能有: 1、访客分页; 2、留言分页及搜索; 3、留言审核机制; 4、云开发相关资源存储的转移; 5、插屏广告的使用。 欢迎大家体验: [图片]
2019-10-24 - 【分享】小程序全景图片展示的几个方案
概述 以下方案均需要有全景照片后方可实现(自己拍的 or 网上下载)。 一、方案一:自建网页 自建网页,自己有服务器,可以用全景图转换器(如pano2vr)直接生成html代码,然后通过 webview 嵌入到小程序访问。 建议:图片可以放在七牛云或其他地方,CDN 能有效优化网页中全景图的打开速度(一般全景图片体积都是较大的)。 体验效果: [图片] 二、方案二:720yun 使用 720云,这也是大部分全景摄影社或爱好者最习惯用的平台了。他们也提供了小程序打开全景图的方案。但核心还是使用 webview,并且需要开通会员,具体参考: 建议:经费足的可以考虑一下这个方案,毕竟720云的操作和体验是真的是十分优秀的! 参考:小程序校验指南 | 720yun https://bbs2.720yun.com/article?id=687 [图片] 三、方案三:小程序插件 以上两种方案都是借助webview来实现,也就是说必须要企业或其他单位的主体才能使用。个人的小程序如果要实现全景,建议使用这位大佬写的小程序插件——wxPano。项目一直在不断更新中,而且还免费,很值得期待! 建议:①该插件限制全景图片分辨率需在2048*1024及以下,因此无法打开画质很高清的全景图片。②插件代码包超过1MB,对小程序打开速度有微小的影响。 链接:https://mp.weixin.qq.com/wxopen/pluginbasicprofile?action=intro&appid=wx386c038238531f87 [图片] 结语 以上来自我自己开发时的一些经验,欢迎前辈老师们补充。 也欢迎社区三连——点赞收藏关注!!
2019-10-24 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - 模块化——加速小程序开发
阅读基础:有小程序项目经验,有查阅官方文档习惯的小伙伴 随着公司小程序项目日益繁多,仅仅靠着官方提供的框架、组件、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 - 常见小程序优化方案总结
一、首次启动性能优化 1、首次打开一个小程序,用户一般会观察到如下图所示的三种状态 [图片] 这张图中的三种状态对应的都是什么呢?小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。 此时,微信会在背后完成几项工作:下载小程序代码包、加载小程序代码包、初始化小程序首页。下载到的小程序代码包不是小程序的源代码,而是编译、压缩、打包之后的代码包。 2、小程序加载的顺序 微信会在小程序启动前为小程序准备好通用的运行环境。这个运行环境包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。这样可以显著减少小程序的启动时间。 [图片] 通过这张图可以对比发现,小程序首次启动的 第一张图是资源准备(代码包下载);第二张图是业务代码的注入以及落地页首次渲染;第三张图是落地页数据请求时的loading态(部分小程序存在)。 3、优化方案 控制包大小:上传代码时要先进行压缩、静态图片资源除小的icon外其余放到cdn、无用代码清除; 分包加载:根据业务场景,将用户访问率高的页面放在主包里,将访问率低的页面放入子包里,按需加载; 分包预加载:在进入小程序某个页面时,由框架自动预下载可能需要的分包,提升进入后续分包页面时的启动速度。对于独立分包,也可以预下载主包。分包预下载 官方文档链接 独立分包技术:区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源;独立分包 官方文档链接 二、渲染性能优化 1、数据渲染优化 双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。 [图片] 页面初始化的时间大致由页面初始数据通信时间和初始渲染时间两部分构成。其中,数据通信的时间指数据从逻辑层开始组织数据到视图层完全接收完毕的时间,数据量小于64KB时总时长可以控制在30ms内。传输时间与数据量大体上呈现正相关关系,传输过大的数据将使这一时间显著增加。因而减少传输数据量是降低数据传输时间的有效方式。 [图片] 在数据传输时,逻辑层会执行一次JSON.stringify来去除掉setData数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将setData所设置的数据字段与data合并,使开发者可以用this.data读取到变更后的数据。因此,为了提升数据更新的性能,可以参考如下方法: 1.不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用; 2.数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用setData来设置这些数据; 3.与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下; 4.勿在后台页面去setData; 5.建议创建一个检测data大小的方法,如果超过64K可以打印报警日志提醒开发者; 2、长列表优化方案 无限下拉加载后会大数据量展现导致的性能问题,一个常见的方法在诸多C端都有使用,一句话说就是"只渲染所需的元素"。虚拟列表是按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。简而言之,虚拟列表指的就是「可视区域渲染」的列表。有三个概念需要了解一下: 滚动容器元素:一般情况下,滚动容器元素是 window 对象。然而,我们可以通过布局的方式,在某个页面中任意指定一个或者多个滚动容器元素。只要某个元素能在内部产生横向或者纵向的滚动,那这个元素就是滚动容器元素考虑每个列表项只是渲染一些纯文本。在本文中,只讨论元素的纵向滚动。 可滚动区域:滚动容器元素的内部内容区域。假设有 100 条数据,每个列表项的高度是 50,那么可滚动的区域的高度就是 100 * 50。可滚动区域当前的具体高度值一般可以通过(滚动容器)元素的 scrollHeight 属性获取。用户可以通过滚动来改变列表在可视区域的显示部分。 可视区域:滚动容器元素的视觉可见区域。如果容器元素是 window 对象,可视区域就是浏览器的视口大小(即视觉视口);如果容器元素是某个 div 元素,其高度是 300,右侧有纵向滚动条可以滚动,那么视觉可见的区域就是可视区域。 实现虚拟列表就是在处理用户滚动时,要改变列表在可视区域的渲染部分,其具体步骤如下: 计算当前可见区域起始数据的 startIndex 计算当前可见区域结束数据的 endIndex 计算当前可见区域的数据,并渲染到页面中 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上 计算 endIndex 对应的数据相对于可滚动区域最底部的偏移位置 endOffset,并设置到列表上 [图片] 虚拟列表的实现原理可以参考这篇文章:浅说虚拟列表的实现原理 3、长列表局部渲染技巧 在一个列表中,有n条数据,采用上拉加载更多的方式。假如这个时候想对其中某一个数据进行点赞操作,还能及时看到点赞的效果,可以采用setData全局刷新,点赞完成之后,重新获取数据,再次进行全局重新渲染,这样做的优点是:方便,快捷!缺点是:用户体验极其不好,当用户刷量100多条数据后,重新渲染量大会出现空白期(没有渲染过来)。 优化步骤: 1.将点赞的[代码]id[代码]传过去,知道点的是那一条数据, 将点赞的[代码]id[代码]传过去,知道点的是那一条数据 <view wx:if="{{!item.status}}" class=“btn” data-id="{{index}}" bindtap=“couponTap”>立即领取</view> 2.重新获取数据,查找相对应id的那条数据的下标([代码]index[代码]是不会改变的) 3.用setData进行局部刷新 this.setData({ list[index] : newList[index] }) 4、用户事件优化 视图层将事件反馈给逻辑层时,同样需要一个通信过程,通信的方向是从视图层到逻辑层。因为这个通信过程是异步的,会产生一定的延迟,延迟时间同样与传输的数据量正相关,数据量小于64KB时在30ms内。降低延迟时间的方法主要有两个。 1.去掉不必要的事件绑定(WXML中的bind和catch),从而减少通信的数据量和次数; 2.事件绑定时需要传输target和currentTarget的dataset,因而不要在节点的data前缀属性中放置过大的数据。 三、生命周期优化 1、异步请求,页面渲染需要的数据最好在onLoad时异步请求数据,不要在onReady时请求;非页面渲染需要的数据,尽量放在onReady生命周期去调用; 2、定时器、事件监听、播放组件、音视频组件等,在页面转入后台(onHide)或者销毁(onUnload)时应该中止掉; 四、图片静态资源预加载 在日常小程序的开发中,有很多的大图片是放置于cdn上的,在需要进行展示的时候,如果没有预加载有可能出现图片展示的不及时,造成不好的体验,所以如下方式实现了图片预加载的功能,可以封装成组件的形式。 实现思路是将图片添加进页面中,设置不可见,然后加载图片,实现一个预加载的功能。 1、添加模版文件: img-loader.wxml <template name=“img-loader”> <image mode=“aspectFill” wx:for="{{ imgLoadList }}" wx:key="*this" src="{{ item }}" data-src="{{ item }}" bindload="_imgOnLoad" binderror="_imgOnLoadError" style=“width:0;height:0;opacity:0” /> </template> 2、添加js文件:img-loader.js /** 图片预加载组件 */ class ImgLoader { /** 初始化方法,在页面的 onLoad 方法中调用,传入 Page 对象及图片加载完成的默认回调 */ constructor(pageContext, defaultCallback) { this.page = pageContext this.defaultCallback = defaultCallback || function () { } this.callbacks = {} this.imgInfo = {} [代码]this.page.data.imgLoadList = [] //下载队列 this.page._imgOnLoad = this._imgOnLoad.bind(this) this.page._imgOnLoadError = this._imgOnLoadError.bind(this) [代码] } /** 加载图片 @param {String} src 图片地址 @param {Function} callback 加载完成后的回调(可选),第一个参数个错误信息,第二个为图片信息 */ load(src, callback) { if (!src) return; [代码]let list = this.page.data.imgLoadList, imgInfo = this.imgInfo[src] if (callback) this.callbacks[src] = callback //已经加载成功过的,直接回调 if (imgInfo) { this._runCallback(null, { src: src, width: imgInfo.width, height: imgInfo.height }) //新的未在下载队列中的 } else if (list.indexOf(src) == -1) { list.push(src) this.page.setData({ 'imgLoadList': list }) } [代码] } _imgOnLoad(ev) { let src = ev.currentTarget.dataset.src, width = ev.detail.width, height = ev.detail.height [代码]//记录已下载图片的尺寸信息 this.imgInfo[src] = { width, height } this._removeFromLoadList(src) this._runCallback(null, { src, width, height }) [代码] } _imgOnLoadError(ev) { let src = ev.currentTarget.dataset.src this._removeFromLoadList(src) this._runCallback(‘Loading failed’, { src }) } //将图片从下载队列中移除 _removeFromLoadList(src) { let list = this.page.data.imgLoadList list.splice(list.indexOf(src), 1) this.page.setData({ ‘imgLoadList’: list }) } //执行回调 _runCallback(err, data) { let callback = this.callbacks[data.src] || this.defaultCallback callback(err, data) delete this.callbacks[data.src] } } module.exports = ImgLoader 3、在需要使用预加载功能的xxx.wxml页面中加入模版文件和使用代码: <import src="…/…/templates/img-loader.wxml"/> <template is=“img-loader” data="{{ imgLoadList }}"></template> 4、在需要使用预加载功能页面的xxx.js文件中引入文件和使用代码: import ImgLoader from ‘…/…/templates/img-loader.js’; let images = [ ‘http://cdn.weimob.com/saas/activity/bargain/images/arms/shoulie.png’, ‘http://cdn.weimob.com/saas/activity/bargain/images/arms/shandian.png’, ‘http://cdn.weimob.com/saas/activity/bargain/images/arms/fengbao.png’ ] //初始化图片预加载组件,并指定统一的加载完成回调 this.imgLoader = new ImgLoader(this, this.imageOnLoad.bind(this)); images.forEach(item => { this.imgLoader.load(item) }) 备注:如有错误请帮忙指出;如有侵权,请联系我们删除,谢谢!
2019-09-03 - 如何实现一个简单的swiper效果
简单的siwper效果,又是逛社区发现老哥提的想要该效果,那么有需要,咱们就得上啊: 效果图: [图片] 上代码: wxml [代码]<view class="container"> <swiper duration="200" previous-margin="140rpx" next-margin="140rpx" bindchange="currentHandle" circular="{{true}}" class="swiper-out"> <block wx:for="{{punchList}}" wx:key="*this"> <swiper-item class="swp-item {{current === index ?'active-item': ''}}"> <view class="slide-image" style=" background: url({{item.bannerUrl}}) no-repeat center center;background-size: 100% 100%;" id="{{index}}"></view> </swiper-item> </block> </swiper> <view class="swp-dot"> <view class="square-12 m-r-8 {{current === index ?'active': ''}}" wx:for="{{punchList}}" wx:key="{{index}}"></view> </view> </view> [代码] JS [代码]const app = getApp() Page({ data: { punchList: [{ "bannerUrl": "https://qiniu-image.qtshe.com/1536067857379_122.png" }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068379879_115.png", }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068319939_230.png", }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068074140_695.png", }, { "bannerUrl": "https://qiniu-image.qtshe.com/1536068213758_796.png", }], current: 0 }, currentHandle(e) { let { current } = e.detail this.setData({ current }) } }) [代码] wxss [代码].container { display: flex; flex-direction: column; align-items: center; justify-content: space-between; height: 100vh; } .slide-image { height: 600rpx; width: 400rpx; margin-top: 20rpx; overflow: hidden; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 10rpx; } .swiper-out { width: 750rpx; height: 660rpx; margin-top: 60rpx; } .active-item .slide-image { box-shadow: 0 5rpx 20rpx 3rpx rgba(0, 0, 0, 0.15); } .swp-item { width: 400rpx; display: flex; flex-direction: column; align-items: center; padding-top: 4rpx; opacity: 0.6; } .active-item { opacity: 1; } .swp-dot { display: flex; justify-content: center; flex: 1; margin-top: 18rpx; } .m-r-8 { margin-right: 8rpx; } .m-l-8 { margin-right: 8rpx; } .square-12 { width: 12rpx; height: 12rpx; background-color: #d8d8d8; border-radius: 6rpx; transition: width 0.2s linear; } .active { background-color: #3c3c3c; width: 36rpx; transition: width 0.2s linear; } [代码] 代码放完了,看下效果吧。代码片段如下: https://developers.weixin.qq.com/s/DCK6HJmw7kaZ
2019-12-19 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: Android、IOS手机对于页面title的展示不一致,安卓title的显示不居中 页面的title只支持纯文本级别的样式控制,不能够做更丰富的title效果 左上角的事件无法监听、定制 路由导航单一,只能够返回上一页,深层级页面的返回不够友好 探索 小程序自定义导航栏已开放许久>>了解一下,相信不少小伙伴已使用过这个功能,同时不少小伙伴也会发现一些坑: 机型多如牛毛:自定义导航栏高度在不同机型始终无法达到视觉上的统一 调皮的胶囊按钮:导航栏元素(文字,图标等)怎么也对不齐那该死的胶囊按钮 各种尺寸的全面屏,奇怪的刘海屏,简直要抓狂 一探究竟 为了搞明白原理,我先去翻了官方文档,>>飞机,点过去是不是很惊喜,很意外,通篇大文尽然只有最下方的一张图片与这个问题有关,并且啥也看不清,汗汗汗… 我特意找了一张图片来 [图片] 分析上图,我得到如下信息: Android跟iOS有差异,表现在顶部到胶囊按钮之间的距离差了6pt 胶囊按钮高度为32pt, iOS和Android一致 动手分析 我们写一个状态栏,通过wx.getSystemInfoSync().statusBarHeight设置高度 Android: [图片] iOS:[图片] 可以看出,iOS胶囊按钮与状态栏之间距离为:4px, Android为8px,是不是所有手机都是这种情况呢? 答案是:苹果手机确实都是4px,安卓大部分都是7和8 也会有其他的情况(可以自己打印getSystemInfo验证)如何快速便捷算出这个高度,请接着往下看 如何计算 导航栏分为状态栏和标题栏,只要能算出每台手机的导航栏高度问题就迎刃而解 导航栏高度 = 胶囊按钮高度 + 状态栏到胶囊按钮间距 * 2 + 状态栏高度 注:由于胶囊按钮是原生组件,为表现一致,其单位在各种手机中都为px,所以我们自定义导航栏的单位都必需是px(切记不能用rpx),才能完美适配。 解决问题 现在我们明白了原理,可以利用胶囊按钮的位置信息和statusBarHeight高度动态计算导航栏的高度,贴一个实现此功能最重要的方法 [代码]let systemInfo = wx.getSystemInfoSync(); let rect = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null; //胶囊按钮位置信息 wx.getMenuButtonBoundingClientRect(); let navBarHeight = (function() { //导航栏高度 let gap = rect.top - systemInfo.statusBarHeight; //动态计算每台手机状态栏到胶囊按钮间距 return 2 * gap + rect.height; })(); [代码] gap信息就是不同的手机其状态栏到胶囊按钮间距,具体更多代码实现和使用demo请移步下方代码仓库,代码中还会有输入框文字跳动解决办法,安卓手机输入框文字飞出解决办法,左侧按钮边框太粗解决办法等等 胶囊信息报错和获取不到 问题就在于 getMenuButtonBoundingClientRect 这个方法,在某些机子和环境下会报错或者获取不到,对于此种情况完美可以模拟一个胶囊位置出来 [代码]try { rect = Taro.getMenuButtonBoundingClientRect ? Taro.getMenuButtonBoundingClientRect() : null; if (rect === null) { throw 'getMenuButtonBoundingClientRect error'; } //取值为0的情况 if (!rect.width) { throw 'getMenuButtonBoundingClientRect error'; } } catch (error) { let gap = ''; //胶囊按钮上下间距 使导航内容居中 let width = 96; //胶囊的宽度,android大部分96,ios为88 if (systemInfo.platform === 'android') { gap = 8; width = 96; } else if (systemInfo.platform === 'devtools') { if (ios) { gap = 5.5; //开发工具中ios手机 } else { gap = 7.5; //开发工具中android和其他手机 } } else { gap = 4; width = 88; } if (!systemInfo.statusBarHeight) { //开启wifi的情况下修复statusBarHeight值获取不到 systemInfo.statusBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - 20; } rect = { //获取不到胶囊信息就自定义重置一个 bottom: systemInfo.statusBarHeight + gap + 32, height: 32, left: systemInfo.windowWidth - width - 10, right: systemInfo.windowWidth - 10, top: systemInfo.statusBarHeight + gap, width: width }; console.log('error', error); console.log('rect', rect); } [代码] 以上代码主要是借鉴了拼多多的默认值写法,android 机子中 gap 值大部分为 8,ios 都为 4,开发工具中 ios 为 5.5,android 为 7.5,这样处理之后自己模拟一个胶囊按钮的位置,这样在获取不到胶囊信息的情况下,可保证绝大多数机子完美显示导航头 吐槽 这么重要的问题,官方尽然没有提供解决方案…竟然提供了一张看不清的图片??? 网上有很多ios设置44,android设置48,还有根据不同的手机型号设置不同高度,通过长时间的开发和尝试,本人发现以上方案并不完美,并且bug很多 代码库 Taro组件gitHub地址详细用法请参考README 原生组件npm构建版本gitHub地址详细用法请参考README 原生组件简易版gitHub地址详细用法请参考README 由于本人精力有限,目前只计划发布维护好这2种组件,其他组件请自行修改代码,有问题请联系 备注 上方2种组件在最下方30多款手机测试情况表现良好 iPhone手机打电话和开热点导致导航栏样式错乱,问题已经解决啦,请去demo里测试,这里特别感谢moments网友提出的问题 本文章并无任何商业性质,如有侵权请联系本人修改或删除 文章少量部分内容是本人查询搜集而来 如有问题可以下方留言讨论,微信zhijunxh 比较 斗鱼: [图片] 虎牙: [图片] 微博: [图片] 酷狗: [图片] 知乎: [图片] [图片] 知乎是这里边做的最好的,但是我个人认为有几个可以优化的小问题 打电话或者开启热点导致样式错落,这也是大部门小程序的问题 导航栏下边距太小,看起来不舒服 搜索框距离2侧按钮组距离不对等 自定义返回和home按钮中的竖线颜色重了,并且感觉太粗 如果您看到了此篇文章,请赶快修改自己的代码,并运用在实践中吧 扫码体验我的小程序: [图片] 创作不易,如果对你有帮助,请移步Taro组件gitHub原生组件gitHub给个星星 star✨✨ 谢谢 测试信息 手机型号 胶囊位置信息 statusBarHeight 测试情况 iPhoneX 80 32 281 369 48 88 44 通过 iPhone8 plus 56 32 320 408 24 88 20 通过 iphone7 56 32 281 368 24 87 20 通过 iPhone6 plus 56 32 320 408 24 88 20 通过 iPhone6 56 32 281 368 24 87 20 通过 HUAWEI SLA-AL00 64 32 254 350 32 96 24 通过 HUAWEI VTR-AL00 64 32 254 350 32 96 24 通过 HUAWEI EVA-AL00 64 32 254 350 32 96 24 通过 HUAWEI EML-AL00 68 32 254 350 36 96 29 通过 HUAWEI VOG-AL00 65 32 254 350 33 96 25 通过 HUAWEI ATU-TL10 64 32 254 350 32 96 24 通过 HUAWEI SMARTISAN OS105 64 32 326 422 32 96 24 通过 XIAOMI MI6 59 28 265 352 31 87 23 通过 XIAOMI MI4LTE 60 32 254 350 28 96 20 通过 XIAOMI MIX3 74 32 287 383 42 96 35 通过 REDMI NOTE3 64 32 254 350 32 96 24 通过 REDMI NOTE4 64 32 254 350 32 96 24 通过 REDMI NOTE3 55 28 255 351 27 96 20 通过 REDMI 5plus 67 32 287 383 35 96 28 通过 MEIZU M571C 65 32 254 350 33 96 25 通过 MEIZU M6 NOTE 62 32 254 350 30 96 22 通过 MEIZU MX4 PRO 62 32 278 374 30 96 22 通过 OPPO A33 65 32 254 350 33 96 26 通过 OPPO R11 58 32 254 350 26 96 18 通过 VIVO Y55 64 32 254 350 32 96 24 通过 HONOR BLN-AL20 64 32 254 350 32 96 24 通过 HONOR NEM-AL10 59 28 265 352 31 87 24 通过 HONOR BND-AL10 64 32 254 350 32 96 24 通过 HONOR duk-al20 64 32 254 350 32 96 24 通过 SAMSUNG SM-G9550 64 32 305 401 32 96 24 通过 360 1801-A01 64 32 254 350 32 96 24 通过
2019-11-17 - 我的微信小程序制作的辛苦历程
跳一跳流行的时候,我知道了小程序,那个时候只是和同事在一起PK玩玩。半年后,因工作需要,用到小程序,我自己注册了一个账号,从此开启了我的小程序编程之路。 刚开始,很疯狂,连着轴的读了三遍小程序开发的文档。拿着微信小程序的demo看了又看,除了自带的获取用户头像外,然并卵,我还是什么都不会,就搁置起来。 我不喜欢前端编程,但是喜欢前端华丽的界面。在学习编程语言golang的时候,因golang官方的网站手机上无法打开,我在想能不能把它搬到手机上。开始有意向使用html5,但因域名服务器及不喜欢前端等原因放弃了。这个时候,云开发已经推出。仔细看了下,有云函数、云存储及云数据库等功能,我这么简单的逻辑肯定够用了。我开始构思怎么去实现我的想法。这次我是下定了决心,要做成一个出来看看(事后才知道,有些事情很难,但你铁了心去做时,还是可以实现的,个人观点,勿喷)。 构思之后: [图片] 操作流程: [图片] 最终实现的界面如下: [图片] [图片] 前端使用Weui库,Markdown解析使用Towxml库。内容资源复制的golang的playground,那些天全部手动复制golang网站上的内容到Markdown文件,然后再上传到云存储。等弄完之后,手都是酸的。Weui全部使用的模板,我真的对前端不感冒,想稍稍改下样式都做不了,所以就在weui demo的模板上改改内容。Towxml入手很简单,第一它是真的简单,第二我用到它的功能简单,第三他的demo我完全可以复制。就这样,敲敲打打,简陋的第一版就上线了。 在上线之后,我又新增了两个golang库,还是复制黏贴,我发现现在的模式这样做太累了。 先来看看我的代码 [图片] 没有看错,我就是那样写的。一个章节对应一个page,一个page对应一个file。手酸是有原因的。我新增一个章节,就得做如下操作,在书籍的data结构中添加章节数据,新建一个章节Page,建好之后,复制其它原有章节的代码,改下文件名称。再把写好的文件上传到云存储中。 手酸是活该的(哈哈)事后,我还在微信文档社区分享了我的经验 https://developers.weixin.qq.com/community/develop/article/doc/000082f9790060a3c5885a7f95d013 这里说第一个技巧,利用navigator的参数。 [图片] 上面的链接里面说的很详细,我就不再叙述。改完之后,我只需要在书籍data中添加我新的章节数据,然后上传章节的Markdown文件到云存储就可以了。改之后的小程序包小了很多。 现在的小程序还是有缺陷的,我只能添加golang内容,不能添加java内容。因为它逻辑上只有两层:章节->内容。不能添加新的书籍,所以小程序需要再改,改成3层,在现有的界面前再添加一个书籍列表,后端不变。 改造之后的界面如下: [图片] 第二层 [图片] 在上线之后,我发现一个新的问题,现在的小程序不能维护,不能在手机上添加内容,我有一个好的想法,或者一段自己感觉很厉害的代码,想分享给大家或想存储起来,我做不到,并且现在的内容是我复制的,网上一大片,除了做个搬运工,其它什么收获也没有。我现在的需求也在变化,我用它主要当工具来使用了,当我忘了某个类型写法时,上去看一眼,这时,感觉少了点东东,就是搜索,我又重新构建了一次。这次,前端使用漂亮的ColorUI。 这是最后实现的界面: [图片] [图片] [图片] 这次主要增加了关键的几个特性: 一、可搜索 章节内容可以根据关键字进行查询,查找自己感兴趣的内容。 二、可维护 可以添加内容,目前只能自己观看,若要分享,还需要到后台数据库更改状态才可以,这样也是为了保障内容的质量。 这次实现使用了云数据库。 在数据库中实现书籍-章节-内容之间的关系,不在像以前通过在小程序端写死了。增加了灵活性。 书籍,章节,内容之间的关系简单说下,书籍有标签,标签和现有编程语言对应,这个标签的作用就是在云存储中设置前缀文件夹的。章节绑定书籍ID,章节名称可以被搜索,章节ID为内容文件名。云存储中的文件路径就是标签+章节ID,文件内容是Markdown格式,暂不支持其它模式,Markdown中链接不起作用,图片需要用网络图片,且能访问才行。 因为没有文采,所以有点老太婆裹脚布 —— 又臭又长,构思了好多天,总感觉能写出很多东西,当落笔时,发现脑子里灰的。不管怎样,终于写完了。有志者事竟成,有了想法,Just do it!
2019-07-18 - 从0到1开发一个小程序cli脚手架(一)--创建页面/组件模版篇
github地址:https://github.com/jinxuanzheng01/xdk-cli 原文地址:https://www.yuque.com/docs/share/c6dddfdf-5a18-4024-8604-5e619cb9845d cli工具是什么? 在正文之前先大致描述下什么是cli工具,cli工具英文名command-line interface,也就是命令行交互接口,比较典型的几个case例如,create-react-app,vue-cli,具体可以去百度一下,下面gif是小打卡目前用的一套自动化发布工具🔧 [图片] 可以看到整个发布流程大致是以选择或默认项的形式实现,大致分析下面几步 选择打包形式 开发模式/debug模式/发布模式 设置版本号 填写发布信息 选择环境 是否提交版本commit 是不是非常无脑?是不是再也不用担心线上发错环境了?有了它就算不同项目间,就算一天发n次版本还需要担心什么呢? 当然除了简单的发布功能还,还可以做很多的事情,比如创建page/component模版等一些更多有趣的事情 为了节约版面就不贴图了,具体可以看下仓库 https://github.com/jinxuanzheng01/xdk-cli(目前该工具是从小打卡现有的cli库中抽离的部分功能) 明确痛点 也就是我为什么要做这么一个工具,其实最开始我只是为了解决一个问题,就是在整个发布流程中需要人工去改动/确认发布环境和版本信息,大致可以想象下把线下环境发布到线上的尴尬处境 后续发现从cli角度触发,很多东西都变得简单了,大致列了下: 环境变量切换(线上环境,线下环境) 创建启动模版,包括页面,组件 自动化发布 … 准备工作 本文会以快速创建页面模版文件为例教你怎么快速撸一个属于自己的cli工具 如果觉得自己做比较麻烦,可以clone下我的仓库自己改装下 需要了解的三方库 中间会用到一些第三方库 commander, 一个解析命令行命令和参数工具 inquirer,常用交互式命令行用户界面的集合 chalk,美化你的终端输出样式 fuzzy,字符串模糊匹配的插件,根据输入关键词进行模糊匹配 json-format,json美化/格式化工具 其他的一些小知识:比如path模块,fs模块,大家可以去node官网自行查看:https://nodejs.org/api/ 搭建开发环境 创建一个空文件夹,并且npm初始化, 并且创建一个index.js页面,这个index.js将作为你整个包的入口文件 [代码]npm init -y [代码] 安装上述的三方包,当然也可以后续按需安装,这样更能清楚每个包是做什么的 [代码] npm install @moyuyc/inquirer-autocomplete-prompt commander chalk commander fuzzy inquirer json-format --save [代码] 在package.json里添加bin字段, 将自定义的命令软连到全局环境,同时执行npm link创建链接,这里如果报错{code EACCES,errno:13,…},是因为权限不足,可以尝试sudo npm link [代码] "bin": { "cli-demo": "./index.js" } [代码] 在入口文件,index.js 行首加入一行[代码]#!/usr/bin/env node[代码]指定当前脚本由node.js进行解析 [代码]#!/usr/bin/env node // 指定运行环境 // 输出文本 console.log('Hello World!!!'); [代码] 这时可以在命令行中执行[代码]cli-demo[代码]验收一下成果了 [图片] ok,可以看到当在全局状态下输入自定义命令时,正确运行了入口文件,也就意味着的开发玩具已经搭建完成 Let‘ Go 整理逻辑 以快速创建页面模版文件为例,就需要考虑需要哪些逻辑: 设置页面名称 找到已有模版文件 copy到项目中 修改app.json 识别命令行 在刚才的[代码]Hello World!!![代码]环节,已经可以正确识别cli-demo,但是需要在一个cli工具中集成更多功能,可能需要有不同的执行策略,以git为例:[代码]git clone, git status,git push[代码],所以需要识别不同的命令和参数, 是时候就需要用到[代码]commander[代码]这个第三方包帮助解析命令行参数了,当然你也可以自己撸一个lib,本质上还是方便解析[代码]process.argv[代码] index.js (本质上这个js就是一个路由) [代码]#!/usr/bin/env node const version = require('./package').version; // 版本号 /* = package import -------------------------------------------------------------- */ const program = require('commander'); // 命令行解析 /* = task events -------------------------------------------------------------- */ const createProgramFs = require('./lib/create-program-fs'); // 创建项目文件 /* = config -------------------------------------------------------------- */ // 设置版本号 program.version(version, '-v, --version'); /* = deal receive command -------------------------------------------------------------- */ program .command('create') .description('创建页面或组件') .action((cmd, options) => createProgramFs(cmd)); /* 后续可以根据不同的命令进行不同的处理,可以简单的理解为路由 */ // program // .command('build [cli]') // .description('执行打包构建') // .action((cmd, env) => callback); /* = main entrance -------------------------------------------------------------- */ program.parse(process.argv) [代码] 这时候当键入[代码]cli-demo create[代码]时会自动执行createProgramFs createProgramFs.js [代码]module.exports = function () { console.log('Hi, create-program-fs.js'); }; [代码] 命令行输入 cli-demo create [图片] 可以看到已经成功的开辟出了一块独立的业务模块,后续就只需要依据需求填补相应的内容即可 创建交互命令 收到执行命令,这个时候按第一张图,是需要开始一系列QA(当然你也可以不做交互式,直接配置命令行参数),<br />引入三方包 [代码]inquirer[代码],来指定问题队列 [代码]const question = [ // 选择模式使用 page -> 创建页面 | component -> 创建组件 { type: 'list', name: 'mode', message: '选择想要创建的模版', choices: [ 'page', 'component', ] }, // 设置名称 { type: 'input', name: 'name', message: answer => `设置 ${answer.mode} 名称 (e.g: index):`, }, ]; module.exports = function() { // 问题执行 inquirer.prompt(question).then(answers => { console.log(answers); }); }; [代码] [图片]、 可以看到通过一系列QA交互,实际输出拿到的是一个json对象,第一步已完成 创建模版文件 创建一个存放模版文件的文件夹template,并准备好你希望的模版 [图片] 项目中使用模版文件 为了方便阅读,下面的代码,需要明确下面变量的定义, Config.dir_root = 命令行执行目录 Config.root = cli项目根目录 Config.appRoot = 小程序项目路径 Config.template = 模版目录 这里有两个点,一个是执行路径的问题,另一个是分包的问题,具体如下: 执行路径 这里一定要弄明白**__dirname, process.cwd()**的区别,同时还有一些小程序是自己搭的gulp/webpack,可能小程序项目是在src目录下,一定要分清楚 __dirname: 被执行js文件的绝对路径,一般在index.js执行时缓存起来作为项目的全局路径,比如找到template文件夹就会使用 [代码]${__dirname}/template[代码] process.cwd():当前命令行运行时的工作目录,比如在/Users/xuan/Documents/cli-demo 如果当前项目在src,或其他文件夹里怎么办?可以提供一个给用户项目中的配置文件,类似于gulpfile.js或是webpack.config.js的形式,内容例如(具体可以看git仓库) [代码]module.exports = { // 小程序路径 app: './src', // 模版文件夹 template: './template' }; [代码] 可以看到对象中app属性,可以指定你当前小程序项目的路径 分包 因为小程序的分包机制会导致页面实际路径与在主包的路径不相符,例如: 主包:pages/index/index 分包:pages/main_module/pages/habit_enlist/habit_enlist 解决这个问题一方面是要有页面创建要有一定的规范,统一格式,另一方面需要根据规则解析app.json,<br />上面的主包,分包路径差不多是我目前使用的规范 解析app.json [代码]// 获取app.json function getAppJson() { let appJsonRoot = path.join(Config.appRoot, '/app.json'); try { return require(appJsonRoot); }catch (e) { Log.error(`未找到app.json, 请检查当前文件目录是否正确,path: ${appJsonRoot}`); process.exit(1); // 异常退出 } } // 解析app.json let parseAppJson = () => { // app Json 原文件 let appJson = __Data__.appJson = getAppJson(); // 获取主包页面 appJson.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = ''); // 获取分包,页面列表 appJson.subPackages.forEach(item => { __Data__.appModuleList[getPathSubSting(item.root)] = item.root; item.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = item.root); }); }; // __Data__.appPagesList = 小程序全部页面 // __Data__.appModuleList = 小程序全部分包页面 // item结构 {util_module: 'pages/util_module/'},这么定义结构是为了方便后续取数 [代码] question队列里,增加删选分包的选项 [代码] // 设置page所属module { type: 'autocomplete', name: 'modulePath', message: 'Set page ownership module', choices: [], suggestOnly: false, source(answers, input) { // none 代表放在主包 return Promise.resolve(fuzzy.filter(input, ['none', ...Object.keys(__Data__.appModuleList)]).map(el => el.original)); }, filter(input) { if (input === 'none') { return ''; } return __Data__.appModuleList[input]; }, when(answer) { return answer.mode === 'page'; } } [代码] autocomplete类型本质上是个列表,但是可以进行模糊查询,非常方便,像小打卡有接近30个分包的情况下效果尤为明显 [图片] 有了文件名,有了分包路径,有了可供copy的模版,接下来就很简单了,把模版文件塞进项目就可以了,下面是一串从仓库里copy的代码,利用async/await很方便的写出一维代码,基本上的流程: 获取路径 -> 校验 -> 获取文件信息 -> 复制文件 -> 修改app.json -> 输出结果信息 [代码]async function createPage(name, modulePath = '') { // 获取模版文件路径 let templateRoot = path.join(Config.template, '/page'); if (!Util.checkFileIsExists(templateRoot)) { Log.error(`未找到模版文件, 请检查当前文件目录是否正确,path: ${templateRoot}`); return; } // 获取业务文件夹路径 let page_root = path.join(Config.appRoot, modulePath, '/pages', name); // 查看文件夹是否存在 let isExists = await Util.checkFileIsExists(page_root); if (isExists) { Log.error(`当前页面已存在,请重新确认, path: ` + page_root); return; } // 创建文件夹 await Util.createDir(page_root); // 获取文件列表 let files = await Util.readDir(templateRoot); // 复制文件 await Util.copyFilesArr(templateRoot, `${page_root}/${name}`, files); // 填充app.json await writePageAppJson(name, modulePath); // 成功提示 Log.success(`createPage success, path: ` + page_root); } [代码] 扩展 一个基本的快速创建页面模版的cli工具就这样完成,但是有可能需要更多的一些功能 自定义模版 比如说每个项目的模版都有可能不太一样,很大程度上需要根据项目进行定制,这时候可能就需要前文提到的给用户开放config文件的插槽了 项目中的config: [代码]// xdk.config.js module.exports = { // 小程序路径 app: './', // 模版文件夹 template: './template' }; // create-program-fs.js module.exports = function() { // 校验:当前是否存在配置文件 let customConfPath = `${Config.dir_root}/xdk.config.js`; if (!Util.checkFileIsExists(customConfPath)) { Log.error('当前项目尚未创建xdk.config.js文件'); return; } // 获取用户配置项 let {app, template = ''} = require(customConfPath); // 小程序目录 Config.appRoot = path.resolve(path.join(Config.dir_root, app)); // 模版文件目录(默认使用cli提供的默认模版,当config文件有设置template路径时,使用自定义路径) !!template && (Config.template = path.resolve(path.join(Config.dir_root, template)))); // 问题执行 inquirer.prompt(question).then(answers => { console.log(answers); }); }; [代码] 发布的npm仓库 目前从开发到调试本质上是在本地提供服务,利用npm link提供软连接到全局PATH,<br />其实也可以直接发到npm上,让其他使用的该cli的成员一建安装,比如npm install -g xxxxxxx 教程的话百度,google有很多,作者表示很懒,遇到问题下面留言吧。。 最后 可以看到整个功能逻辑相对于平时写的复杂的业务逻辑来说相对简单,主要是工具库的一些使用方面的东西,中间的难点可能就是node中概念性的一些东西,然而这些多看一下文档基本就可以解决 顺便预告下后续的话可能会更新一些如何利用cli工具做到自动化发布,版本号控制,环境变量切换,自动生成文档等一系列有趣的功能 下文地址: 《从0到1开发一个小程序cli脚手架(二) --版本发布/管理篇》
2019-08-05 - 小程序canvas生成海报图(易错点分析)
1,问:创建canvas 绘制图像,保存的图片不清晰? 答:这个问题大概就是canvas宽高太小。 2,问:绘制图像,图片绘制不上? 答:绘制图片的时候图片路径必须为本地路径,如果使用网络路径是不行的,我们只需要将网络路径下载到本地就行wx.downloadFile()。 3,问:绘制好的图像,图片保存不了? 答:我们绘制好的图像,要保存成图片,其实需要2步。 第1步:利用wx.canvasToTempFilePath()将canvas画布图像,生成图片。我们需要将wx.canvasToTempFilePath(),放到 ctx.draw(false, function() { wx.canvasToTempFilePath()生成的图片路径进行 }) 里面确保canvas已经将图像全部绘制完成。 第2步:将wx.canvasToTempFilePath()生成的图片路径进行 wx.saveImageToPhotosAlbum()保存到相册就ok。 以上只是个人遇到的问题,如有步骤不明确请在评论区提问。 谢谢大家的支持。
2019-07-03 - 您好!页面栈问题求解答
您好!页面栈最多10个 但 如果我有 1 2 3 4 5 6 7 8 9 10 11 个页面, 用 wx.navigateTo 方法实现跳转,到第10页面再想跳到11 页面,提示错误 navigateTo:fail webview count limit exceed 此时 如何跳到11页,还能让用户返回时能看到 10 9 8 7 6 这几个页面 12345 如何从页面栈中去除?
2019-02-18 - 你可能不知道的mpvue性能优化技巧
最近一直在折腾[代码]mpvue[代码]写的微信小程序的性能优化,分享下实战的过程。 先上个优化前后的图: [图片] 可以看到打包后的代码量从[代码]813KB[代码]减少到[代码]387KB[代码],Audits体验评分从[代码]B[代码]到[代码]A[代码],效果还是比较明显的。其实这个指标说明不了什么,而且轻易就可以做到,更重要的是优化小程序运行过程中的卡顿感,请耐心往下看。 常规优化 常规的Web端优化方法在小程序中也是适用的,而且不可忽视。 一、压缩图片 这一步最简单,但是容易被忽视。在tiny上在线压缩,然后下载替换即可。 [图片] 我这项目的压缩率高达[代码]72%[代码],可以说打包后的代码从[代码]813KB[代码]降到[代码]387KB[代码]大部分都是归功于压缩图片了。 二、移除无用的库 我之前在项目中使用了Vant Weapp,在[代码]static[代码]目录下引入了整个库,但实际上我只使用了[代码]button[代码],[代码]field[代码],[代码]dialog[代码]等几个组件,实在是没必要。 所以干脆移除掉了,微信小程序自身提供的[代码]button[代码],[代码]wx.showModal[代码]等一些组件基本可以满足需求,自己手写一下样式也不用花什么时间。 在这里建议大家,在微信小程序中,尽量避免使用过多的依赖库。 不要贪图方便而引入一些比较大的库,小程序不同于[代码]Web[代码],限制比较多,能自己写一下就尽量自己写一下吧。 小程序的优化 咱们首先得看一下官方优化建议,大多是围绕这个建议去做。 一、开启Vue.config._mpTrace = true 这个是[代码]mpvue[代码]性能优化的一个黑科技啊,可能大多数同学都不知道这个,我在官方文档都没有搜到到这个配置,我真的是服了。 我能找到这个配置也是Google机缘巧合下看到的,出处:mpvue重要更新,页面更新机制进行全面升级 具体做法是在[代码]/src/main.js[代码]添加[代码]Vue.config._mpTrace = true[代码],如: [代码]Vue.config._mpTrace = true Vue.config.productionTip = false App.mpType = 'app' [代码] 添加了[代码]Vue.config._mpTrace[代码]属性,这样就可以看到console里会打印每500ms更新的数据量。如图: [图片] 如果数据更新量很大,会明显感觉小程序运行卡顿,反之就流畅。因此我们可以根据这个指标,逐步找出性能瓶颈并解决掉。 二、精简data 1. 过滤api返回的冗余数据 后端的api可能是需要同时为iOS,Android,H5等提供服务的,往往会有些冗余的数据小程序是用不到的。比如api返回的一个[代码]文章列表[代码]数据有很多字段: [代码]this.articleList = [ { articleId: 1, desc: 'xxxxxx', author: 'fengxianqi', time: 'xxx', comments: [ { userId: 2, conent: 'xxx' } ] }, { articleId: 2 // ... }, // ... ] [代码] 假设我们在小程序中只需要用到列表中的部分字段,如果不对数据做处理,将整个[代码]articleList[代码]都[代码]setData[代码]进去,是不明智的。 小程序官方文档: 单次设置的数据不能超过1024kB,请尽量避免一次设置过多的数据。 可以看出,内存是很宝贵的,当[代码]articleList[代码]数据量非常大超过1M时,某些机型就会爆掉(我在iOS中遇到过很多次)。 因此,需要将接口返回的数据剔除掉不需要的,再[代码]setData[代码],回到我们上面的[代码]articleList[代码]例子,假设我们只需要用[代码]articleId[代码]和[代码]author[代码]这两个字段,可以这样: [代码]import { getArticleList } from '@/api/article' export default { data () { return { articleList: [] } } methods: { getList () { getArticleList().then(res => { let rawList = res.list this.articleList = this.simplifyArticleList(rawList) }) }, simplifyArticleList (list) { return list.map(item => { return { articleId: item.articleId, author: item.author // 需要哪些字段就加上哪些字段 } }) } } } [代码] 这里我们将返回的数据通过[代码]simplifyArticleList[代码]来精简数据,此时过滤后的[代码]articleList[代码]中的数据类似: [代码][ {articleId: 1, author: 'fengxianqi'}, {articleId: 2, author: 'others'} // ... ] [代码] 当然,如果你的需求中是所有数据都要用到(或者大部分数据),就没必要做一层精简了,收益不大。毕竟精简数据的函数中具体的字段,是会增加维护成本的。 PS: 在我个人的实际操作中,做数据过滤虽然增加了维护的成本,但一般收益都很大,因次这个方法比较推荐。 2. data()中只放需要的数据 [代码]import xx from 'xx.js' export default { data () { return { xx, otherXX: '2' } } } [代码] 有些同学可能会习惯将[代码]import[代码]的东西都先放进[代码]data[代码]中,再在[代码]methods[代码]中使用,在小程序中可能是个不好的习惯。 因为通过[代码]Vue.config._mpTrace = true[代码]在更新某个数据时,我对比放进data和不放进data中的两种情况会有差别。 所以我猜测可能是data是会一起更新的,比如只是想更新[代码]otherXX[代码]时,会同时将[代码]xx[代码]也一起合起来[代码]setData[代码]了。 3. 静态图片放进static 这个问题和上面的问题其实是一样的,有时候我们会通过[代码]import[代码]的方式引入,比如这样: [代码]<template> <img :src="UserIcon"> </template> <script> import UserIcon from '@/assets/images/user_icon.png' export default { data () { return { UserIcon } } } </script> [代码] 这样会导致打包后的代码,图片是[代码]base64[代码]形式(很长的一段字符串)存放在[代码]data[代码]中,不利于精简data。同时当该组件多个地方使用时,每个组件实例都会携带这一段很长的[代码]base64[代码]代码,进一步导致数据的冗余。 因此,建议将静态图片放到[代码]static[代码]目录下,这样引用: [代码]<template> <img src="/static/images/user_icon.png"> </template> [代码] 代码也更简洁清爽。 看一下做了上面操作的前后对比图,使用体验上也流畅了很多。 [图片] 三、swiper优化 小程序自身提供的[代码]swiper[代码]组件性能上不是很好,使用时要注意。参考着两个思路: 【优化】解决swiper渲染很多图片时的卡顿 想请教一下小程序swiper组件的问题 在我使用时,由于需求原因,动态删掉swiper-item的思路不可行(手滑时会造成抖动)。因此只能作罢。但仍然可以优化一下: 将未显示的[代码]swiper-item[代码]中的图片用[代码]v-if[代码]隐藏到,当判断到current时才显示,防止大量图片的渲染导致的性能问题。 四、vuex使用注意事项 我之前写过的一篇mpvue开发音频类小程序踩坑和建议里面有讲如何在小程序中使用[代码]vuex[代码]。但遇到了个比较严重的性能问题。 1. 问题描述 我开发的是一个音频类的小程序,所以需要将播放列表[代码]playList[代码],当前索引[代码]currentIndex[代码]和当前时长[代码]currentTime[代码]放进[代码]state.js[代码]中: [代码]const state = { currentIndex: 0, // playList当前索引 currentTime: 0, // 当前播放的进度 playList: [], // {title: '', url: '', singer: ''} } [代码] 每次用户点击播放音频时,都会先加载音频的播放列表[代码]playList[代码],然后播放时更新当前时长[代码]currentTime[代码],发现有时候播音频时整个小程序非常卡顿。 注意到,音频需每秒就得更新一次[代码]currentTime[代码],即每秒就做一次[代码]setData[代码]操作,稍微有些卡顿是可以理解的。但我发现是播放列表数据比较多时会特别卡,比如playList的长度是100条以上时。 2. 问题原因 我开启[代码]Vue.config._mpTrace = true[代码]后发现一个规律: 当[代码]palyList[代码]数据量小时,[代码]console[代码]显示造成的数据量更新数值比较小;当[代码]playList[代码]比较大时,[代码]console[代码]显示造成的数据量更新数值比较大。 PS: 我曾尝试将playList数据量增加到200条,每500ms的数据量更新达到800KB左右。 到这里基本可以确定一个事实就是:更新[代码]state[代码]中的任何一个字段,将导致整个[代码]state[代码]全量一起[代码]setData[代码]。在我这里的例子,虽然我每次只是更新[代码]currentTime[代码]这个字段的值,但依然导致将state中的其他字段如[代码]playList[代码],[代码]currentIndex[代码]都一起做了一次[代码]setData[代码]操作。 3. 解决问题 有两个思路: 精简state中保存的数据,即限制[代码]playList[代码]的数据不能太多,可将一些数据暂存在[代码]storage[代码]中 [代码]vuex[代码]采用[代码]Module[代码]的写法能改善这个问题,虽然使用时命名空间造成一定的麻烦。vuex传送门 一般情况下,推荐使用后者。我在项目中尝试使用了前者,同样能达到很好的效果,请继续看下面的分享。 五、善用storage 1.为什么说要善用storage 由于小程序的内存非常宝贵,占用内存过大会非常卡顿,因此最好尽可能少的将数据放到内存中,即[代码]vuex[代码]存的数据要尽可能少。而小程序的[代码]storage[代码]支持单个 key允许存储的最大数据长度为 [代码]1MB[代码],所有数据存储上限为 [代码]10MB[代码]。 所以可以将一些相对取用不频繁的数据放进[代码]storage[代码]中,需要时再将这些数据放进内存,从而缓解内存的紧张,有点类似Windows中[代码]虚拟内存[代码]的概念。 2.storage换内存的实例 这个例子讲的会有点啰嗦,真正能用到的朋友可以详细看下。 上面讲到[代码]playList[代码]数据量太多,播放一条音频时其实只需要最多保证3条数据在内存中即可,即[代码]上一首[代码],[代码]播放中的[代码],[代码]下一首[代码],我们可以将多余的播放列表存放在[代码]storage[代码]中。 PS: 为了保证更平滑地连续切换下一首,我们可以稍微保存多几条,比如我这里选择保存5条数据在vuex中,播放时始终保证当前播放的音频前后都有两条数据。 [代码]// 首次播放背景音频的方法 async function playAudio (audioId) { // 拿到播放列表,此时的playList最多只有5条数据。getPlayList方法看下面 const playList = await getPlayList(audioId) // 当前音频在vuex中的currentIndex const currentIndex = playList.findIndex(item => item.audioId === audioId) // 播放背景音频 this.audio = wx.getBackgroundAudioManager() this.audio.title = playList[currentIndex].title this.audio.src = playList[currentIndex].url // 通过mapActions将播放列表和currentIndex更新到vuex中 this.updateCurrentIndex(index) this.updatePlayList(playList) // updateCurrentIndex和updatePlayList是vuex写好的方法 } // 播放音频时获取播放列表的方法,将所有数据存在storage,然后返回当前音频的前后2条数据,保证最多5条数据 import { loadPlayList } from '@/api/audio' async function getPlayList (courseId, currentAudioId) { // 从api中请求得到播放列表 // loadPlayList是api的方法, courseId是获取列表的参数,表示当前课程下的播放列表 let rawList = await loadPlayList(courseId) // simplifyPlayList过滤掉一些字段 const list = this.simplifyPlayList(rawList) // 将列表存到storage中 wx.setStorage({ key: 'playList', data: list }) return subPlayList(list, currentAudioId) } [代码] 重点是[代码]subPlayList[代码]方法,这个方法保证了拿到的播放列表是最多5条数据。 [代码]function subPlayList(playList, currentAudioId) { let tempArr = [...playList] const count = 5 // 保持vuex中最多5条数据 const middle = parseInt(count / 2) // 中点的索引 const len = tempArr.length // 如果整个原始的播放列表本来就少于5条数据,说明不需要裁剪,直接返回 if (len <= count) { return tempArr } // 找到当前要播放的音频的所在位置 const index = tempArr.findIndex(item => item.audioId === currentAudioId) // 截取当前音频的前后两条数据 tempArr = tempArr.splice(Math.max(0, Math.min(len - count, index - middle)), count) return tempArr } [代码] [代码]tempArr.splice(Math.max(0, index - middle), count)[代码]可能有些同学比较难理解,需要仔细琢磨一下。假设[代码]playList[代码]有10条数据: 当前音频是列表中的第1条(索引是0),截取前5个:[代码]playList.splice(0, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]0[代码],没有[代码]上一首[代码],有4个[代码]下一首[代码] 当前音频是列表中的第2条(索引是1),截取前5个:[代码]playList.splice(0, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]1[代码],有1个[代码]上一首[代码],3个[代码]下一首[代码] 当前音频是列表中的第3条(索引是2),截取前5个:[代码]playList.splice(0, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]2[代码],有2个[代码]上一首[代码],2个[代码]下一首[代码] 当前音频是列表中的第4条(索引是3),截取第1到6个:[代码]playList.splice(1, 5)[代码] ,此时[代码]currentAudio[代码]在这5个数据的索引是[代码]2[代码],有2个[代码]上一首[代码],2个[代码]下一首[代码] 当前音频是列表中的第5条(索引是4),截取第2到7个:[代码]playList.splice(2, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]2[代码],有2个[代码]上一首[代码],2个[代码]下一首[代码] … 当前音频是列表中的第9条(索引是[代码]8[代码]),截取后5个:[代码]playList.splice(4, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]3[代码],有3个[代码]上一首[代码],1个[代码]下一首[代码] 当前音频是列表中的最后1条(索引是[代码]9[代码]),截取后的5个:[代码]playList.splice(4, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]4[代码],有4个[代码]上一首[代码],没有[代码]下一首[代码] 有点啰嗦,感兴趣的同学仔细琢磨下,无论当前音频在哪,都始终保证了拿到当前音频前后的最多5条数据。 接下来就是维护播放上一首或下一首时保证当前vuex中的[代码]playList[代码]始终是包含当前音频的前后2条。 播放下一首 [代码]function playNextAudio() { const nextIndex = this.currentIndex + 1 if (nextIndex < this.playList.length) { // 没有超出数组长度,说明在vuex的列表中,可以直接播放 this.audio = wx.getBackgroundAudioManager() this.audio.src = this.playList[nextIndex].url this.audio.title = this.playList[nextIndex].title this.updateCurrentIndex(nextIndex) // 当判断到已经到vuex的playList的边界了,重新从storage中拿数据补充到playList if (nextIndex === this.playList.length - 1 || nextIndex === 0) { // 拿到只有当前音频前后最多5条数据的列表 const newList = getPlayList(this.playList[nextIndex].courseId, this.playList[nextIndex].audioId) // 当前音频在这5条数据中的索引 const index = newList.findIndex(item => item.audioId === this.playList[nextIndex].audioId) // 更新到vuex this.updateCurrentIndex(index) this.updatePlayList(newList) } } } [代码] 这里的[代码]getPlayList[代码]方法是上面讲过的,本来是从api中直接获取的,为了避免每次都从api直接获取,所以需要改一下,先读storage,若无则从api获取: [代码]import { loadPlayList } from '@/api/audio' async function getPlayList (courseId, currentAudioId) { // 先从缓存列表中拿 const playList = wx.getStorageSync('playList') if (playList && playList.length > 0 && courseId === playList[0].courseId) { // 命中缓存,则从直接返回 return subPlayList(playList, currentAudioId) } else { // 没有命中缓存,则从api中获取 const list = await loadPlayList(courseId) wx.setStorage({ key: 'playList', data: list }) return subPlayList(list, currentAudioId) } } [代码] 播放上一首也是同理,就不赘述了。 PS: 将vuex中的数据精简后,我所做的小程序在播放音频时刷其他页面已经非常流畅啦,效果非常好。 六、动画优化 这个问题在mpvue开发音频类小程序踩坑和建议已经讲过了,感兴趣的可以移步看一眼,这里只写下概述: 如果要使用动画,尽量用css动画代替wx.createAnimation 使用css动画时建议开启硬件加速 最后 大致总结一下上面所讲的几个要点: 开发时打开[代码]Vue.config._mpTrace = true[代码]。 谨慎引入第三方库,权衡收益。 添加数据到data中时要克制,能精简尽量精简。 图片记得要压缩,图片在显示时才渲染。 vuex保持数据精简,必要时可先存storage。 性能优化是一个永不止步的话题,我也还在摸索,不足之处还请大家指点和分享。 欢迎关注,会持续分享前端实战中遇到的一些问题和解决办法。
2019-05-15 - DatePicker 年月日时分秒 任你选
DatePicker 微信上的时间选择,有的时候你会发现,你不能同时选择日期和时间,而且时间不能选到秒。DatePicker让你想选什么选什么… Mode DatePicker分为四个mode:YMDhms(年月日时分秒)、YMD(年月日)、MD(月日)、hm(时分)。 我自己觉得用起来很爽快。 效果图 mode:YMDhms (年月日时分秒) [图片] mode:YMD(年月日) [图片] mode:MD (月日) [图片] mode:hm (时分) [图片] gitHub地址
2019-05-11 - 永远对微信小程序保持尊重——小程序心得体会和开发经验
开篇 我第一次接触小程序时,还清楚的记得是在2017年的春节期间。当我升级最新版的微信后,开始摸索着新的功能变化。在发现页有一个叫小程序的入口,点进去有一列的“应用”。当我打开一个叫“亲戚关系计算器”的小程序后,简单的使用,然后退出,再去寻找其他的小程序,猫眼电影,自选股,滴滴出行…… 当我尝试着去探索更多小程序的过程中,我突然发现,微信正在发展为一个超级的应用流量入口。而微信中的小程序也可以轻松的坐拥10亿的可用用户人群。 那时候过年,我做的唯一一件事情就是细细的读了小程序的开发文档,心想他可以具有多大的活力与动力,能够引发多大的改变。而我能不能适应他,去随着他的发展,带动自己的腾飞。 很遗憾,当时的小程序并不对个人开发者开放,没有办法注册一个小程序,只能在开发者工具上写一些小的应用,去熟悉小程序的开发。 3月27日,小程序重要更新,其中之一就是支持个人开发者注册小程序。那时候的我,在学校上大一,用一个运营公众号的微信号注册了第一个小程序,并做了一些实验性的开发,并上线。 从这个过程中,我开始细细的体会小程序的优势以及不足,小程序适用于那些领域,小程序适合怎么推广。小程序适合那些行业领域的应用。现在看来,其实从小程序提供的能力,就可以依稀端详出小程序所致力的场景与应用领域。 2018年1月,在我历经半年多的思考和衡量下,做出第一款真正属于自己的小程序——GS比赛计分。去尝试探索线上小程序和线下场景的交融,在这过程中有顾虑,有大胆创新,但都为我更深层次理解小程序有很大的帮助。 目前,自己已经做过10余款小程序,除了GS比赛计分开发时间很长,其他的小程序都是一个月左右的时间完工。比如2019年2月的高校课程助手;2019年3月的数据查询助手;2019年4月的数据汇总助手。 我接下来会从小程序的需求分析与应用设计,小程序的开发,小程序的运营3篇来讲述我对于微信小程序的独自见解与经验。希望能为更多的学生开发者有所启发和借鉴。 需求分析与应用设计 要清楚的认识到小程序对自己需求的最大赋能,需要从最初去理解小程序的定位,微信团队对于小程序的定义是这样的: 微信小程序是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播,同时具有出色的使用体验。 通过对小程序提供能力的分析,不难看出。小程序相对于APP来说,在降低开发门槛的同时能够满足最普遍的应用需求,适合生活服务类线下商铺以及非刚需低频应用领域。使得微信通过小程序作为生态的建立者和维护者,赋能商家和应用者。以一种生态触角的状态来迅速的捕捉最大化生态红利。 当清楚的理解小程序的定位后,那么就需要合理的筛选需求并进行应用的功能设计。我将会使用【GS比赛计分】作为例子进行分析说明。 1.用实质的问题引出明确需求,并确定解决问题的功能边界 首先思考的是,自己需要做的功能都有哪些,使用人群是谁?使用场景是什么?需要数据的实时性吗? 一开始设计【GS比赛计分】的时候,目的是为了解决现在的中小型比赛使用人工计分的失效性差,出错率高,人工和时间成本昂贵的问题。程序的目的是将比赛计分的相关人员用互联网工具联系起来,以提升计分效率。 [图片] 当明晰了产品目标后,开始考虑使用人群。一般来看,大多数的比赛计分都是由两种角色构成的,一个是评委(分数确定者),另一个是工作人员(分数汇总者)。 传统的计分过程是通过现场工作人员将评委的分数以纸质的形式送达计分人员。在这一过程中就有大量的成本浪费了(一个是人力成本,另一个是时间成本)。另外,计分人员以Excel或计算器和笔记的形式进行比分汇总,在这一过程中又存在大量的成本浪费(人力成本和时间成本甚至还有错误风险)。 [图片] 所以横观所有的主观评分的线下比赛,无一例外都存在比赛结束后有长时间的节目或视频热场环节,其实这都是在为人工计分的缓慢争取时间。 所以,【GS比赛计分】的使用人群和使用场景就确定下来了,与传统人工计分的过程类似,只不过需要互联网赋能,解决时间和人力成本。需要数据的实时交互。 【GS比赛计分】作为解决现象问题的工具,首先需要做的就是不要违背现象中事件的发生次序。所以功能完全参照比赛计分过程来设计。具体功能如下,提交成绩,撤回成绩,弃权处理,实时的选手分数,实时的选手名次,清楚评委数据,解绑评委,重启比赛,结束比赛,比赛内置会话。 2.对程序平台进行横向比对,明确小程序的优势和劣势 当程序的功能确定好后,不要着急着手界面设计,功能模块的组合,交互设计这些过程。当你在进行这些过程之前,需要认真的去考虑你的应用是不是适合在小程序上开发。 小程序的火爆,注定有许多人盲目的进入这一领域,但不是所有的应用都适合小程序的推广模式,也不是所有应用都能吃得起小程序的运行效率的。所以在确定开发小程序之前,正确的认识的合适与不合适,避免让自己的成本白白的付出。 如何确定适不适合用小程序来做呢?我们可以让小程序作为一个互联网平台,把应用带入到互联网的每一个平台中去,进行横向的对比,去找出各自的优点与不足,当做出客观的判断后,应用在小程序上相比于其他平台有没有优势就很明显了。 还是以【GS比赛计分】举例。我所横向对比平台包括PC、原生APP、Web平台、轻应用、小程序。首先从满足功能来说,比赛计分需要很稳定的实时性,所以我将Web平台排除(因为复杂的比赛环境,不同的设备浏览器难免会有问题)。 然后将剩下的平台做分析。PC平台使用可能性太小(原因:比赛现场成本太高昂,租借笔记本可能都不是现实的);原生APP不适合(原因:评委评分前需要下载APP,比赛结束后没什么用了,需要把这个APP卸载。而且IOS和安卓两大平台增加了研发成本);轻应用有阻碍(原因:百度轻应用由于装机量小,覆盖人群不多。支付宝小程序没有社交关系链,无法有效的推送给需要的人。产业联盟轻应用,苹果用户怎么考虑?) [图片] 经过一系列的对比,很明显。对于【GS比赛计分】来讲,微信小程序是最好的应用平台。同时微信小程序仍然可以和不同的平台进行联系,所以可扩展性,功能延展性都是最佳的。 3.应用设计要满足即用即走的理念 当确定小程序是最理想的应用平台时,我们需要对小程序进行便利化的设计。这就需要抛弃一部分原生应用开发或Web开发的一些设计理念。追求“极致、简约”的风格设计。 在【GS比赛计分】的详细设计时,我考虑到,以个人账户的形式去下发比赛的流程是行不通的,既然服务于比赛,那么就以比赛为最基本的账户组成单元,明确一个比赛ID。而同一个比赛的不同角色,以不同的IK进行区别,而角色的设置包含在比赛设计中,由比赛创建者在创建比赛时自行创建。 那么小程序的使用场景就出来了。以比赛现场的告知或者微信聊天的分享,告知比赛参与角色其ID和IK,就可以让角色快速的进入绑定角色并使用程序。免去注册的一系列烦恼。 同时在微信用户面前,这个账户体系是平权的,任何用户在得知ID和IK的情况下都可以进入(账户绑定情况下不可以进入),一定程度上免除密码和忘记密码,注销账户这一系列的麻烦。 当完成比赛后,程序已经完成了自己所有的任务时,使用者可以直接退出程序,不需要注销比赛等操作。真正做到即走。 在使用过程中,要清楚的考虑用户的使用过程,从而做一些保险机制。微信小程序运行在微信上,微信是社交工具,就免不了用户会退出小程序甚至微信去做一些其他的事情。所以【GS比赛计分】在设计时要保证用户回来要用到自己想要的,在程序设计中有中间状态界面能够保证用户可以迅速进入使用状态。 4.对于很大的系统,要把最适合小程序部分拿出来,而不是全部 现在的【GS比赛计分】其实是一个大的生态系统,结合有线下的网络接口,展示接口,线上小程序,web平台。每一个部分都承载着自己独特的应用价值。 比如Web平台就承载比赛管理的任务,创建比赛,上传比赛文件,选手图片,设置选手(名称、介绍、手机号、图片、出场顺序),设置评分项(名称、权值、预置分数),设置评委(名称、权值,IK)。从实际的分析来看,比赛管理最适合在PC端进行,不管是文件还是图片,公认都是PC上传比较容易。 在最初设计的时候,我错误的把系统分成了多个小程序进行系统搭建,在实际使用过程中造成了重大的缺陷和用户流失。最大的表现是,我开发了【GS比赛创建】小程序,作为比赛的入口程序。从而造成比赛数量增速缓慢,大的使用场景无法突破,老用户意见上升等一系列问题。不得不注销了【GS比赛创建】小程序,并进行很大的架构调整。【GS比赛计分】暂停使用,造成大量的用户流失。 [图片] 所以,当设计多场景,前后联动性很强的应用时,需要将功能进行使用划分,每一个划分需要找最适合的平台。最适合小程序的部分,就做好与其他平台的无缝结合。 5.小程序的应用场景和机会 目前来看,小程序的应用场景主要包括支付场景,比如扫码支付、快消餐饮、移动购物、交通出行等等,工具类小程序能更碎片化、垂直化地满足细分的应用场景需求。 根据微信的最近更新变化来看,公众号和小程序的协同作用将越发明显,公众号的作用也将进一步放大,因此未来发展的机会可能在这几方面。 内容营销,小程序能通过更完善更精准的服务进而提高用户黏性。具体来看包括各大知识付费的小程序以及在教育风口上的小程序。当然,这些小程序也可同时开发APP(按微信对小程序的开发步骤来看)从而实现用户沉淀。 具有支付场景的各类商家。包括传统的已有一定客户群的商家提供更方便的服务或者实现线上线下联动以及新零售。 工具类小程序。包括共享经济领域,这类小程序即用即走,轻量化,便捷化。 天生具有社交属性的小程序。比如抽奖、互赠礼品、拼团减价、社交性的小游戏以及帮拿快递等。 小程序的开发经验 1. 微信小程序开发文档是最好的学习文档 很多同学喜欢看视频教程,或者买一本小程序开发书来学习。觉得这么学会加深理解更加容易上手,而官方文档干巴巴不好学。现在的微信小程序能力更新速度很快,当一个教程或书出来的时候,其实已经过时了。建议同学去微信公开课中学习微信小程序的入门教程,开发入门后,根据自己的开发需要,自行阅读官方文档来学习。 [图片] 2. 必须了解小程序的运行原理 微信小程序是运行在微信中的,所以运行速度并不能和原生媲美。但是在开发小程序的过程中,可以用良好的编程思路来追求程序的高效运行水平。但前提是,你需要对小程序的运行环境有所理解,需要知道在开发环境和真实环境(IOS和Android)下的运行差别。大部分的开发坑都是因为不同运行环境造成的。 官方的声明:微信小程序运行在三端:iOS、Android 和 用于调试的开发者工具;在 iOS 上,小程序的 javascript代码是运行在JavaScriptCore中;在Android上,小程序的javascript代码是通过 X5 内核来解析;在开发工具上,小程序的javascript代码是运行在nwjs(chrome内核)中。 微信小程序的运行环境类似 ReactNative ,而不是纯 Html5。两者最大的不同在于,ReactNative 的界面是由原生控件渲染出来的,而 Html5 的界面是由浏览器内核渲染出来的。两者在性能上有较大的差异,从而表现出来微信小程序要比h5页面好很多。 3.安利小程序云开发 云开发为开发者提供完整的云端支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代,同时这一能力,同开发者已经使用的云服务相互兼容,并不互斥。目前提供三大基础能力支持: 云函数:在云端运行的代码,微信私有协议天然鉴权,开发者只需编写自身业务逻辑代码 数据库:一个既可在小程序前端操作,也能在云函数中读写的 JSON 数据库 存储:在小程序前端直接上传/下载云端文件,在云开发控制台可视化管理 云开发使得小程序开发门槛进一步降低,甚至偏前端的开发人员也可以独立开发小程序了。另外,云开发中云函数支持去请求第三方服务接口。所以云开发最大程度上提供了小程序所需基本API的构建,另外可以通过第三方服务请求能力扩展API能力。 [图片] 而且云开发的数据库和存储完全可以与云函数构建一个小型的后端平台。并发能力中等,适合大部分的小程序业务API的构建。 4. 养成良好的小程序代码习惯 微信小程序的代码文件分为4种,WXML、WXSS、JS、JSON。 JSON文件是小程序的配置文件,APP.JSON是小程序全局配置,另外每个页面也会有配置文件。建议页面中的JSON文件只填写页面需要配置的(比如页面标题),而不变化的不要写到里面(比如标题风格,背景颜色等等)。 JS主要是javascript语法,建议以模块的形式对通用的方法进行封装,尽可能的利用好代码,不要有大量重复的方法语句。如果多个页面需要,可以单独建立js文件,在需要的页面进行引入。 WXSS是样式文件,遵从css的编写规则,尽可能的少些样式表,多多利用已经写过的样式,如果多个页面需要,可以单独建立wxss或者写到app.wxss中 WXML是页面结构文件。如果多个页面中有相同的结构,可以单独封装为模块,而模块中的逻辑方面也尽可能遵从代码最大效益化。 总之,开发代码简洁,会使得编译后的小程序包很小,使加载速度更加快速。 5. 小程序填坑总结: 首先,微信开发者社区是好的小程序开发交流平台,从中可以获得大多数问题的解决方法: https://developers.weixin.qq.com/community/develop/question 另外,许多人常用搜索引擎来直接搜索问题的解决方法,一般大部分的错误或者问题都会在开发文档上写的很清楚,只不过很少有人去注意到,推荐几个小程序填坑的集合文章: https://blog.csdn.net/weixin_42448956/article/details/82414225 https://www.cnblogs.com/shaoting/p/6051261.html https://www.imooc.com/article/36148 https://www.cnblogs.com/wangking/p/6946438.html https://www.jianshu.com/p/4362e52f5c49 小程序的运营 小程序运营属于软件产品运营的一种类别。从产品生命期来看,小程序运营分别为研发期、种子期、成长期、成熟期、衰退期。另外由于小程序在运行模式和定位的不同,表现在推广形式上与平常的软件产品有很大的差别。接下来,我会用【GS比赛计分】【数据查询助手】小程序为例子佐证分享小程序的运营经验。 1. 小程序研发期,搞清楚产品的定位以及目标用户 当你不是运营小程序的产品策划者,你需要首先要搞清楚产品的定位以及目标用户。(这也是多数互联网公司将产品策划和运营作为一个岗位的原因)。在整个产品的研发期,需要跟进产品的每一个细微功能点,要明晰产品的使用用户;还要时刻去观察产品的领域有没有竞品的出现,用户习惯有什么改变;要时刻去衡量产品的竞争优势,为之后的发布运营做好准备。 【数据查询助手】是提供自定义信息查询服务的小程序。任何微信用户均可以上传自定义的数据(报表,成绩单等任何表格数据)建立查询。 [图片] 当在产品研发的一个月,我不断的探索小程序领域,APP领域,Web领域有没有相同的功能产品或服务。甚至将问卷系统(腾讯问卷、问卷星、问卷网)作为潜在的竞争对手。 另外我还制作了web版和原生APP的Demo去体验他们与小程序体验的不同。去确定微信小程序是很好的适应平台。从而为之后的运营做足了准备,提升了自己的信心。 2. 小程序种子期,要充分利用体验版过程 小程序体验版相当于其他应用平台的内测版。体验版可以更高层次的模仿真实的用户环境。在这一阶段更容易发现用户间连接要求高的应用缺陷。同时可以在安全的范围内去聆听用户的真是使用反馈。 【GS比赛计分】开展了长达1个月的体验版。邀请了20个核心用户去体验。由于应用需要与服务端建立实时连接。用户不同的设备,不同网络环境对程序的稳定性做出了很大的考验。在这一阶段修复了不少场景不同导致的错误或者效率低下问题。 另外一开始推出的小程序界面设计只遵从了功能设计,没有很好的考虑真实的使用场景。所以在这一阶段,我最大程度上听取体验用户的建议,对整个界面进行改版,使交互更加的亲近用户。 3. 小程序成长期,明晰获取用户的手段和推广手段,最大化的成长: 当小程序功能稳定后,到了成长阶段,用户使用是最核心的任务。获取用户的时候,必须先让对方了解自己的产品,建立认知,将产品介绍给用户,让用户进入小程序之后,想方设法让产品与用户产生交互,让用户不断体验产品,让活动始终覆盖用户,让用户对产品认可,要完成产品与用户的关系构建。 从现在许多小程序的运营手段来看,基本上都围绕着社交裂变和线下推广的方式来提升小程序的获客表现。而小程序由于较低的开发成本,较快的更新速度,以及较低的试错成本,使得多数很强势的小程序都采用功能矩阵发展模式,快速实现功能及迭代。 小程序获取用户的手段主要由下: 朋友圈分享(包括图片二维码、广告直接进入); 聊天好友推荐转发; 线下二维码(包含商家推广、广告推广); 微信搜索(一般由其他社交平台或者用户需求引起); 线上识别二维码(线上推广,文章推广,或其他社交平台的推广); 其他小程序的跳转(互相推荐) 公众号跳转(公众号运营推广) APP跳转(一般是APP延展的简洁功能推广,或者轻量级触达用户形式的推广) 小程序发掘社交推广的手段主要由下: 社交立减金,实现社交裂变; 社交比拼玩法,引导社交裂变; 互动加入分享按钮,提醒用户转发; 设计同伴环境,鼓励社群传播; 设计任务玩法,领取奖励; 设计福利,鼓励好友助力; 聚焦核心功能,促进口碑传播; 【数据查询助手】从产品定位上就自带社交裂变的元素,当查询创建者创建了一个查询后,他可以根据要分享的人群情况选择多种分享方法。如果是企业微信或者微信工作群,那么可以直接分享小程序到聊天窗口。其他的用户可以直接进入小程序进行查询。如果是线下的查询(推广会,现场发布等),可以通过小程序二维码的方式进入查询界面查询。对于其他社交平台来讲,可以用二维码来做分享,如果是常用用户(添加到我饿小程序或者桌面作为常用工具的用户),可以直接通过复制文字(含查询码)然后进入应用的方式快速开始查询。 同时每一个查询者,都可能是潜在的查询创建者和程序推广者。所以要在这一阶段不断的优化体验流程,尽可能做更多事情覆盖多场景的查询(比如微信搜一搜直接搜索查询码,直接查询),去吸引用户,留住用户。 由于小程序用完即走的理念,导致许多工具类小程序(不含深度融合线下和社交的)用户的留存普遍较低。既然工具类就是服务用户,那么就把小程序慢慢的做成一种用户习惯,从习惯的养成变为行业应用的转化。从转化中寻找切入点,进而挖掘可以创造价值的功能产品。 所以,做小程序不要过分贪图大规模的使用率,大批量的用户。他本身是一种服务理念的触达和养成,你需要在用户心里养成使用习惯,而不要上来糖衣炮弹把用户打蒙,甚至反感。这样小程序的生态就被搞乱了。 4. 小程序成熟期,稳定期最重要的就是小版本的迭代更新: 刚才讲到,小程序有较低的开发成本,较快的更新速度,以及较低的试错成本。所以在小程序成熟期需要根据用户数据不断的去更正调整功能,去保持较高的运营分数。在产品功能中,适时的进行用户付费转化。 付费转化一方面可以拉开用户层次,对小程序的用户是一种活水作用,提升用户的使用粘性,容易过滤最核心的用户,提供更加好的产品服务。如果在成长期很好的进行用户习惯的养成,这个过程会更加自然。反之,应用将会更快的进入衰退期。 付费转化的方式主要由几类: 电商类:主要靠活动、优惠刺激(现实抢购、秒杀、预约、限时满减、显示商品库存和抢购人数); 游戏类:游戏道具(向朋友求助、每日签到、社区活动等方式免费获得,但数量有限,且都是一些级别较低的道具)高价值道具付费、皮肤售卖、游戏币购买; 内容类:付费文章、阅读币购买、付费课程; 5.小程序衰退期:适时舍弃,进行新产品开始推出 小程序开发周期短,很多时候应用分析不够透彻,更多的是一些商业或推广试错。导致许多小程序还没有进入成长期就进入晚期了。这一类小程序直接舍弃就好,不需要什么转化了,这也不算小程序的衰退期(没有盛何来衰)。 对于经历过成熟期的产品主要有几个原因导致进入衰退期: 市场中有新的创新型产品,导致冲击衰退。 自身功能设计有缺陷,导致用户流失。 对用户了解不透彻,付费转化失败,用户迁移到同类产品。 产品质量下降,不能适应用户行为的变化。 由于运营组织的原因,产品运营出现危机。 在这一阶段,运营已经无能为力。那么就进行产品的复盘,积极的去投入到新产品或者新领域的探究上面去。小程序的开发成本较低,所以可以有很多机会去重新塑造一个好的产品。 6.总结,谈谈自己的想法: 现在多种多样的互联网产品不断产生,产品竞争异常激烈。互联网产品的运营手段也是推陈出新,花样繁多。在这里我不给大家分享一些运营花样,因为每个小程序都是独特的,应该有自己独特的运营手段,具体是什么,希望产品者和运营者本身用热爱小程序的心去发现和实践。 永远保持对产品的尊重,对用户的尊敬是每个产品人最重要的事情。如果对应用足够热爱,你会厌恶他被污染,他被别人嫌弃;你会尽自己的可能让他变得更加本真,你会合理的去运营突破,去帮助产品走向更高的位置。 如果你没有爱你的产品,再出色的产品也只是昙花一现而已,并不能给你带来任何长远的意义。 这是最好的小程序运营指南:https://developers.weixin.qq.com/miniprogram/product。 [图片]
2019-06-23 - 【优化】利用函数防抖和函数节流提高小程序性能
大家好,上次给大家分享了swiper仿tab的小技巧: https://developers.weixin.qq.com/community/develop/doc/000cc0b94ac5f8dcf4e7666475b804 今天给大家分享两个有用的函数,《函数防抖和函数节流》 函数防抖和函数节流是都优化高频率执行js代码的一种手段,因为是js实现的,所以在小程序里也是适用的。 首先先来理解一下两者的概念和区别: 函数防抖(debounce)是指事件在一定时间内事件只执行一次,如果在这段时间又触发了事件,则重新开始计时,打个很简单的比喻,比如在打王者荣耀时,一定要连续干掉五个人才能触发hetai kill '五连绝世'效果,如果中途被打断就得重新开始连续干五个人了。 函数节流(throttle)是指限制某段时间内事件只能执行一次,比如说我要求自己一天只能打一局王者荣耀。 这里也有个可视化工具可以让大家看一下三者的区别,分别是正常情况下,用了函数防抖和函数节流的情况下:http://demo.nimius.net/debounce_throttle/ 适用场景: 函数防抖 搜索框搜索联想。只需用户最后一次输入完,再发送请求 手机号、邮箱验证输入检测 窗口resize。只需窗口调整完成后,计算窗口大小。防止重复渲染 高频点击提交,表单重复提交 函数节流 滚动加载,加载更多或滚到底部监听 搜索联想功能 实现原理 函数防抖 [代码]const _.debounce = (func, wait) => {[代码][代码] [代码][代码]let timer;[代码] [代码] [代码][代码]return[代码] [代码]() => {[代码][代码] [代码][代码]clearTimeout(timer);[代码][代码] [代码][代码]timer = setTimeout(func, wait);[代码][代码] [代码][代码]};[代码][代码]};[代码] 函数节流 [代码]const throttle = (func, wait) => {[代码][代码] [代码][代码]let last = 0;[代码][代码] [代码][代码]return[代码] [代码]() => {[代码][代码] [代码][代码]const current_time = +[代码][代码]new[代码] [代码]Date();[代码][代码] [代码][代码]if[代码] [代码](current_time - last > wait) {[代码][代码] [代码][代码]func.apply([代码][代码]this[代码][代码], arguments);[代码][代码] [代码][代码]last = +[代码][代码]new[代码] [代码]Date();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]};[代码][代码]};[代码] 上面两个方法都是比较常见的,算是简化版的函数 lodash中的 Debounce 、Throttle lodash中已经帮我们封装好了这两个函数了,我们可以把它引入到小程序项目了,不用全部引入,只需要引入debounce.js和throttle.js就行了,链接:https://github.com/lodash/lodash 使用方法可以看这个代码片段,具体的用法可以看上面github的文档,有很详细的介绍:https://developers.weixin.qq.com/s/vjutZpmL7A51 系甘先,得闲饮茶
2019-01-14 - 路由的封装
小程序提供了路由功能来实现页面跳转,但是在使用的过程中我们还是发现有些不方便的地方,通过封装,我们可以实现诸如路由管理、简化api等功能。 页面的跳转存在哪些问题呢? 与接口的调用一样面临url的管理问题; 传递参数的方式不太友好,只能拼装url; 参数类型单一,只支持string。 alias 第一个问题很好解决,我们做一个集中管理,比如新建一个[代码]router/routes.js[代码]文件来实现alias: [代码]// routes.js module.exports = { // 主页 home: '/pages/index/index', // 个人中心 uc: '/pages/user_center/index', }; [代码] 然后使用的时候变成这样: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { wx.navigateTo({ url: routes.uc, }); }, }); [代码] query 第二个问题,我们先来看个例子,假如我们跳转[代码]pages/user_center/index[代码]页面的同时还要传[代码]userId[代码]过去,正常情况下是这么来操作的: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { const userId = '123456'; wx.navigateTo({ url: `${routes.uc}?userId=${userId}`, }); }, }); [代码] 这样确实不好看,我能不能把参数部分单独拿出来,不用拼接到url上呢? 可以,我们试着实现一个[代码]navigateTo[代码]函数: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, query }) { const queryStr = Object.keys(query).map(k => `${k}=${query[k]}`).join('&'); wx.navigateTo({ url: `${url}?${queryStr}`, }); } Page({ onReady() { const userId = '123456'; navigateTo({ url: routes.uc, query: { userId, }, }); }, }); [代码] 嗯,这样貌似舒服一点。 参数保真 第三个问题的情况是,当我们传递的参数argument不是[代码]string[代码],而是[代码]number[代码]或者[代码]boolean[代码]时,也只能在下个页面得到一个[代码]string[代码]值: [代码]// pages/index/index.js Page({ onReady() { navigateTo({ url: routes.uc, query: { isActive: true, }, }); }, }); // pages/user_center/index.js Page({ onLoad(options) { console.log(options.isActive); // => "true" console.log(typeof options.isActive); // => "string" console.log(options.isActive === true); // => false }, }); [代码] 上面这种情况想必很多人都遇到过,而且感到很抓狂,本来就想传递一个boolean,结果不管传什么都会变成string。 有什么办法可以让数据变成字符串之后,还能还原成原来的类型? 好熟悉,这不就是json吗?我们把要传的数据转成json字符串([代码]JSON.stringify[代码]),然后在下个页面把它转回json数据([代码]JSON.parse[代码])不就好了嘛! 我们试着修改原来的[代码]navigateTo[代码]: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, data }) { const dataStr = JSON.stringify(data); wx.navigateTo({ url: `${url}?jsonStr=${dataStr}`, }); } Page({ onReady() { navigateTo({ url: routes.uc, data: { isActive: true, }, }); }, }); [代码] 这样我们在页面中接受json字符串并转换它: [代码]// pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(options.jsonStr); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这里其实隐藏了一个问题,那就是url的转义,假如json字符串中包含了类似[代码]?[代码]、[代码]&[代码]之类的符号,可能导致我们参数解析出错,所以我们要把json字符串encode一下: [代码]function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } // pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(decodeURIComponent(options.encodedData)); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这样使用起来不方便,我们封装一下,新建文件[代码]router/index.js[代码]: [代码]const routes = require('./routes.js'); function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { routes, navigateTo, extract, }; [代码] 页面中我们这样来使用: [代码]const router = require('../../router/index.js'); // page home Page({ onLoad(options) { router.navigateTo({ url: router.routes.uc, data: { isActive: true, }, }); }, }); // page uc Page({ onLoad(options) { const json = router.extract(options); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] route name 这样貌似还不错,但是[代码]router.navigateTo[代码]不太好记,[代码]router.routes.uc[代码]有点冗长,我们考虑把[代码]navigateTo[代码]换成简单的[代码]push[代码],至于路由,我们可以使用[代码]name[代码]的方式来替换原来[代码]url[代码]参数: [代码]const routes = require('./routes.js'); function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const url = routes[name]; wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { push, extract, }; [代码] 在页面中使用: [代码]const router = require('../../router/index.js'); Page({ onLoad(options) { router.push({ name: 'uc', data: { isActive: true, }, }); }, }); [代码] navigateTo or switchTab 页面跳转除了navigateTo之外还有switchTab,我们是不是可以把这个差异抹掉?答案是肯定的,如果我们在配置routes的时候就已经指定是普通页面还是tab页面,那么程序完全可以切换到对应的跳转方式。 我们修改一下[代码]router/routes.js[代码],假设home是一个tab页面: [代码]module.exports = { // 主页 home: { type: 'tab', path: '/pages/index/index', }, uc: { path: '/pages/a/index', }, }; [代码] 然后修改[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; if (route.type === 'tab') { wx.switchTab({ url: `${route.path}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${route.path}?encodedData=${dataStr}`, }); } [代码] 搞定,这样我们一个[代码]router.push[代码]就能自动切换两种跳转方式了,而且之后一旦页面类型有变动,我们也只需要修改[代码]route[代码]的定义就可以了。 直接寻址 alias用着很不错,但是有一点挺麻烦得就是每新建一个页面都要写一个alias,即使没有别名的需要,我们是不是可以处理一下,如果在alias没命中,那就直接把name转化成url?这也是阔以的。 [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : name; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 在页面中使用: [代码]Page({ onLoad(options) { router.push({ name: 'pages/user_center/a/index', data: { isActive: true, }, }); }, }); [代码] 注意,为了方便维护,我们规定了每个页面都必须存放在一个特定的文件夹,一个文件夹的当前路径下只能存在一个index页面,比如[代码]pages/index[代码]下面会存放[代码]pages/index/index.js[代码]、[代码]pages/index/index.wxml[代码]、[代码]pages/index/index.wxss[代码]、[代码]pages/index/index.json[代码],这时候你就不能继续在这个文件夹根路径存放另外一个页面,而必须是新建一个文件夹来存放,比如[代码]pages/index/pageB/index.js[代码]、[代码]pages/index/pageB/index.wxml[代码]、[代码]pages/index/pageB/index.wxss[代码]、[代码]pages/index/pageB/index.json[代码]。 这样是能实现功能,但是这个name怎么看都跟alias风格差太多,我们试着定义一套转化规则,让直接寻址的name与alias风格统一一些,[代码]pages[代码]和[代码]index[代码]其实我们可以省略掉,[代码]/[代码]我们可以用[代码].[代码]来替换,那么原来的name就变成了[代码]user_center.a[代码]: [代码]Page({ onLoad(options) { router.push({ name: 'user_center.a', data: { isActive: true, }, }); }, }); [代码] 我们再来改进[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : `pages/${name.replace(/\./g, '/')}/index`; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 这样一来,由于支持直接寻址,跳转home和uc还可以写成这样: [代码]router.push({ name: 'index', // => /pages/index/index }); router.push({ name: 'user_center', // => /pages/user_center/index }); [代码] 这样一来,除了一些tab页面以及特定的路由需要写alias之外,我们也不需要新增一个页面就写一条alias这么麻烦了。 其他 除了上面介绍的navigateTo和switchTab外,其实还有[代码]wx.redirectTo[代码]、[代码]wx.navigateBack[代码]以及[代码]wx.reLaunch[代码]等,我们也可以做一层封装,过程雷同,所以我们就不再一个个介绍,这里贴一下最终简化后的api以及原生api的映射关系: [代码]router.push => wx.navigateTo router.replace => wx.redirectTo router.pop => wx.navigateBack router.relaunch => wx.reLaunch [代码] 最终实现已经在发布在github上,感兴趣的朋友可以移步了解:mp-router。
2019-04-26 - 微信小程序工具类,云函数模板 分享集合
wx-tool 微信小程序工具类集合,有效提高开发时间和效率! 另推荐云函数合集模板 重要:版本要求 小程序基础库版本 2.2.3 或以上 开发者工具 1.02.1808300 或以上开始,小程序支持使用 npm 安装第三方包。 使用微信开发者工具打开此项目,然后在项目根目录下运行: [代码]npm install --production [代码] 安装好依赖包之后,点击工具顶部菜单栏的详情: [图片] 勾选“使用 npm 模块”选项: [图片] 最后点击开发者工具中的菜单栏:工具 --> 构建 npm 即可运行。 [图片] 1.快速开始 [代码] $ npm i wx-tool --save [代码] 2.按需引入 [代码] var { showModal } = require('wx-tool') [代码] 3.使用例子 属性 类型 默认值 必填 说明 title string 是 提示的标题 content string 否 提示的内容 showCancel string true 否 是否显示取消按钮 cancelText string 取消 否 取消按钮的文字,默认为"取消",最多 4 个字符 cancelColor string #000000 否 取消按钮的文字颜色,默认为"#000000" confirmText string 确定 否 确定按钮的文字,默认为"确定",最多 4 个字符 confirmColor string #00aaff 否 确定按钮的文字颜色,默认为"#00aaff" [代码] // 等同于 wx.showModal // 返回Promise showModal({ title: '提示', content: '支付成功', showCancel: false }).then((res)=>{ console.log(res) }) // 也可以使用 async/await async test(){ try { let { confirm } = await showModal({ title: '提示', content: '是否确认收货?' }) if(confirm) { // 用户点击确认 //你的代码.... } } catch (error) { console.log(error) } } [代码] API showToast — 显示消息提示框 showModal — 显示模态对话框 requestPayment — 微信支付 cloudGetPay — 云函数微信支付 localEncoding — 读取本地文件内容 arrayEqual — 判断两个数组是否相等 isEmptyObject — 判断obj是否为空 randomColor — 随机生成颜色 randomNum — 生成指定范围随机数 bankSpace — 银行卡每四位+空格 getOptionsSync — 获取小程序启动时的参数 trim — 去除两端空格 isNew — 检测是否是新版本 isNum — 检测数字 isPhone — 检测该手机号是否正确 isMail — 检测该邮箱是否正确 isPlate — 检测该车牌号是否正确 isIdCard — 判断是否为身份证 validateURL — 合法uri validateLowerCase — 是否小写字母 validateUpperCase — 是否大写字母 validatAlphabets — 是否大小写字母 digitUppercase — 现金额转大写 decimalAdd — 浮点数相加 getDistance — 计算两点的距离 uuid — 返回一个v4兼容的UUID getUsernameColor — 通过哈希函数获取用户名的颜色 isLength — 检测该字符串是否为空 formatSeconds — 把秒转为 时分秒 parseTime — 格式化时间 formatTime — 返回目标时间距离当前时间时长 stringfyQueryString — 对象序列化 isBack — 该页面是否可以返回上一页 successBack — 成功后返回上一页 getQueryString — 获取url参数 bMapTransQQMap — 百度地图经纬度转腾讯地图经纬度 qqMapTransBMap — 腾讯地图经纬度转百度地图经纬度 cloudFn — 云函数调用 cloudDataBase — 云数据库调用 makePy — 提取中文首字母 附上本文外链地址 1.工具npm地址 2.工具github地址 3.云函数模板github地址
2019-04-19 - 新富文本组件
mp-html小程序富文本组件 news欢迎加入 QQ 交流群:699734691示例小程序添加获取组件包功能[图片] 功能介绍 支持在多个平台使用 支持丰富的标签(包括 table、video、svg 等) 支持丰富的事件效果(自动预览图片、链接处理等) 支持锚点跳转、长按复制等丰富功能 支持大部分 html 实体 丰富的插件(关键词搜索、内容编辑等) 效率高、容错性强且轻量化使用方法1. npm 方式 在项目根目录下执行 npm install mp-html 开发者工具中勾选 使用 npm 模块 并点击 工具 - 构建 npm 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "mp-html" } } 在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}" /> 在需要使用页面的 js 文件中添加 Page({ onLoad() { this.setData({ html: 'Hello World!' }) } }) 2. 源码方式 将源码中的代码包(dist/mp-weixin)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "/components/mp-html/index" } } 后续步骤同上 获取github 链接:https://github.com/jin-yufeng/mp-html npm 链接:https://www.npmjs.com/package/mp-html 文档链接:https://jin-yufeng.gitee.io/mp-html
2022-03-04 - scene扫码取值,decodeURIComponent不会用的看过来
[代码]if[代码] [代码](JSON.stringify(option) !== [代码][代码]'{}'[代码] [代码]&& option.scene !== undefined) {[代码][代码] [代码][代码]let scene = decodeURIComponent(option.scene);[代码][代码] [代码][代码]let sceneData = {};[代码][代码] [代码][代码]scene.split([代码][代码]'&'[代码][代码]).forEach(item => {[代码][代码] [代码][代码]sceneData[item.split([代码][代码]'='[代码][代码])[0]] = (item.split([代码][代码]'='[代码][代码])[1]);[代码][代码] [代码][代码]})[代码][代码] [代码][代码]console.log([代码][代码]'scene'[代码][代码], sceneData);[代码][代码] [代码][代码]}[代码]
2019-04-03 - 【周刊-1】三年大厂面试官-面试题精选及答案
前言 在阿里和腾讯工作了6年,当了3年的前端面试官,把期间我和我的同事常问的面试题和答案汇总在我 Github 的 Weekly-FE-Interview 中。希望对大家有所帮助。 如果你在bat面试的时候遇到了什么不懂的问题,欢迎给我提issue,我会把题目汇总并将面试要点和答案写好放在周刊里,大家一起共同进步和成长,助力大家进入自己理想的企业。 项目地址是:https://github.com/airuikun/Weekly-FE-Interview 常见面试题精选 以下是十道大厂一面的时候常见的面试题,如果全部理解并且弄透,在一面或者电话面的时候基本上能中1~2题。小伙伴可以先不急着看答案,先自己尝试着思考一下和自己实现一下,然后再看答案。 第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决 解析:第 1 题:http的状态码中,499是什么?如何出现499,如何排查跟解决 第 2 题:讲解一下HTTPS的工作原理 解析:第 2 题:讲解一下HTTPS的工作原理 第 3 题:讲解一下https对称加密和非对称加密。 解析:第 3 题:讲解一下https对称加密和非对称加密 第 4 题:如何遍历一个dom树 解析:第 4 题:如何遍历一个dom树 第 5 题:new操作符都做了什么 解析:第 5 题:new操作符都做了什么 第 6 题:手写代码,简单实现call 解析:第 6 题:手写代码,简单实现call 第 7 题:手写代码,简单实现apply 解析:第 7 题:手写代码,简单实现apply 第 8 题:手写代码,简单实现bind 解析:第 8 题:手写代码,简单实现bind 第 9 题: 简单实现项目代码按需加载,例如import { Button } from ‘antd’,打包的时候只打包button 解析:第 9 题: 简单实现项目代码按需加载,例如import { Button } from ‘antd’,打包的时候只打包button 第 10 题:简单手写实现promise 解析:第 10 题:简单手写实现promise 结语 本人还写了一些前端进阶知识的文章,如果觉得不错可以点个star。 blog项目地址是:https://github.com/airuikun/blog 我是小蝌蚪,腾讯高级前端工程师,跟着我一起每周攻克几个前端技术难点。希望在小伙伴前端进阶的路上有所帮助,助力大家进入自己理想的企业。
2019-04-08 - 不断升级优化并且可快速进行二次开发的B2C源码分享
[图片] 项目介绍 基于ThinkPhp5.0+Vue+EasyWeChat 开发的一套CRMEB新零售商城系统 CRMEB系统就是集客户关系管理+营销电商系统,能够真正帮助企业基于微信公众号、小程序、wap、pc、app等,实现会员管理、数据分析,精准营销的电子商务管理系统。可满足企业新零售、批发、分销、预约、O2O、多店等各种业务需求 CRMEB的优势:快速积累客户、会员数据分析、智能转化客户、有效提高销售、会员维护; CRMEB二次开发: 1.本源码遵循MIT开源协议,系统采用TP5.0框架; 2.后台采用form-builder组件和后台多任务操作框架,方便快速开发增删改、封装layui.table 可快速开发数据列表页面、PHPExcel数据导出、数据库在线词典、日志小工具、系统参数配置、系统强大完善的权限控制、系统菜单配置、组合数据模型等这些都是为了方便二次开发而准备的; 3.前台vue封装了常用小工具,以及所有数据接口; 技术亮点 1.form-builder PHP快速生成表单; 2.前台Vue、RequireJS、node封装所有接口; 3.PHPExcel数据导出,导出表格更加美观,可视; 4.EasyWeChat部署微信开发,微信接入更加快捷,简单; 5.iview ui组件是HTML编写更加便捷; 6.后台多任务桌面化操作界面; 7.TP5.0+form-builder+layui.table自己封装快速二次开发; 8.一键安装 功能 1.商品: 能够对商品的状态分类管理 (出售中、待上架、库存中、已售馨、库存警戒、回收站)、添加产品、添加商品分类等功能 2.会员:站内会员的管理 (发放优惠劵、发通知、发图文消息、增加余额、会员行为详情)、站内通知 、微信端用户管理 (微信用 户的管理、分组、行为观察、添加标签) 等功能 3.营销:能够管理优惠的发放和制作、用户积分的统计使用情况、秒杀产品的管理等 4.财务:能够对用户的消费、充值、返佣的记录 5.订单:能够完成用户的订单管理(发货、订单详情、修改订单、订单备注、订单记录、订单退款) 、售后服务 (评论的回复与删除) 6.分销:后台有分销统计管理,分销可以设置人人分销和指定人分销,也可以自己稍微开发一下修改规则,例如下单后成为分销等 7.数据统计图表统计分析(财务统计、产品统计、会员统计、营销统计、分销统计、交易统计等) 8.设置:能够完成管理员对网站的商品资料(添加大类、添加小类、商品添加、属性快速生成、商品审查)、商品交易(外理订单、发 货查询)、会员管理(会员审查)、操作管理(管理员添加、管理员审查、管理员退出)、系统配置、后台通知等功能 9.内容:管理文章分类 (添加分类、删除分类、修改分类) 、 管理文章 10.维护:查看系统日志、文件变动效验、刷新网站缓存、在线更新系统、清除数据、文件管理等功能 11.强大的权限管理系统 演示地址 演示后台:demo25.crmeb.net 帐号:demo123 密码:demo123 [图片] 开源源码下载地址:http://github.crmeb.net/u/crmeb 付费版 淘宝超值版: CRMEB微信公众号v2.5版:除了以上功能还带砍价、拼团功能 CRMEB微信小程序v2.5版:除了以上功能还带砍价、拼团功能 CRMEB微信公众号小程序打通v2.5版:除了以上功能还带砍价、拼团功能 需要申请微信开放平台 还有定制开发服务,例如:预约系统、O2O、付费阅读、多店版、多商家版 交流QQ:609147212
2019-04-08 - 5行代码实现微信模版消息推送,springboot实现微信推送,java微信推送
今天来带大家学习下微信模版消息推送。 先看效果图: [图片] 核心代码只有下面几行,即可轻松实现微信模版消息推送 [代码] //1,配置 WxMpInMemoryConfigStorage wxStorage = new WxMpInMemoryConfigStorage(); wxStorage.setAppId("wx77bb69292323a000"); wxStorage.setSecret("29bd368145806115ad6820133e62806e"); WxMpService wxMpService = new WxMpServiceImpl(); wxMpService.setWxMpConfigStorage(wxStorage); //2,推送消息 WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder() .toUser("o5kho6DgC7SDry8zCmXuvHJGvrgI")//要推送的用户openid .templateId("Tpln-Eue2obJ0B-8JNkgkiRJaDMPgVeIgGxna982xrg")//模版id .url("https://30paotui.com/")//点击模版消息要访问的网址 .build(); try { wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage); } catch (Exception e) { System.out.println("推送失败:" + e.getMessage()); } [代码] 所用知识点 1, springboot实现java后台 2,微信测试账号的申请 3,微信模版推送的配置 接下来就带领大家来一步步实现微信模版消息推送。 一,springboot创建java后台 至于springboot怎么创建java后台,我这里就不再唠叨了,大家百度一下,一大堆的文章。这里只需要重点讲解下以下几点。 1,在pom.xml文件里引入下面类库 [代码] <!--微信模版消息推送三方sdk--> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-mp</artifactId> <version>3.3.0</version> </dependency> [代码] 2,写一个推送的controller [代码]package com.qiushi.wxpush; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import me.chanjar.weixin.mp.api.WxMpInMemoryConfigStorage; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage; /** * Created by qcl on 2019-03-28 * 微信:2501902696 * desc: 模版消息推送模拟 */ @RestController public class PushController { /* * 微信测试账号推送 * */ @GetMapping("/push") public void push() { //1,配置 WxMpInMemoryConfigStorage wxStorage = new WxMpInMemoryConfigStorage(); wxStorage.setAppId("wx77bb69292323a000"); wxStorage.setSecret("29bd368145806115ad6820133e62806e"); WxMpService wxMpService = new WxMpServiceImpl(); wxMpService.setWxMpConfigStorage(wxStorage); //2,推送消息 WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder() .toUser("o5kho6DgC7SDry8zCmXuvHJGvrgI")//要推送的用户openid .templateId("Tpln-Eue2obJ0B-8JNkgkiRJaDMPgVeIgGxna982xrg")//模版id .url("https://30paotui.com/")//点击模版消息要访问的网址 .build(); //3,如果是正式版发送模版消息,这里需要配置你的信息 // templateMessage.addData(new WxMpTemplateData("name", "value", "#FF00FF")); // templateMessage.addData(new WxMpTemplateData(name2, value2, color2)); try { wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage); } catch (Exception e) { System.out.println("推送失败:" + e.getMessage()); e.printStackTrace(); } } } [代码] 二,接下来就来重点讲讲我们如何注册微信测试账号,并实现推送功能。 正常我们企业开发,实现微信模版消息推送,必须要有微信公众号,备案的网址,并且最麻烦的一点是要获取到用户的openid,作为个人,这些条件基本上都不具备。所以今天就来带大家注册微信开发测试账号,来轻松实现微信模版消息推送。 1,微信扫码登录下面网址 https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login 扫码登录成功后,就会给我们生成微信公号的appid和appsecret [图片] 2,微信扫码关注 测试号二维码,微信给我们返回我们的openid,这个openid在推送时特别重要。因为你推送肯定要知道推送给 谁啊,就比如你打电话,肯定要知道用户的电话号码吧。这个openid就是我们要推送给那个用户的唯一标示。 [图片] 3,拿到这些以后,我们就可以去实现微信推送了。推送的代码就只有下面这么点。 [代码] //1,配置 WxMpInMemoryConfigStorage wxStorage = new WxMpInMemoryConfigStorage(); wxStorage.setAppId("wx77bb69292323a000");//appid wxStorage.setSecret("29bd368145806115ad6820133e62806e");//appsecret WxMpService wxMpService = new WxMpServiceImpl(); wxMpService.setWxMpConfigStorage(wxStorage); //2,推送消息 WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder() .toUser("o5kho6DgC7SDry8zCmXuvHJGvrgI")//要推送的用户openid .templateId("Tpln-Eue2obJ0B-8JNkgkiRJaDMPgVeIgGxna982xrg")//模版id .url("https://30paotui.com/")//点击模版消息要访问的网址 .build(); //3,如果是正式版发送模版消息,这里需要配置你的信息 // templateMessage.addData(new WxMpTemplateData("name", "value", "#FF00FF")); // templateMessage.addData(new WxMpTemplateData(name2, value2, color2)); //发起推送 try { String msg = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage); System.out.println("推送成功:" + msg); } catch (Exception e) { System.out.println("推送失败:" + e.getMessage()); e.printStackTrace(); } [代码] 三,推送测试 代码都完成后,我们就可以来测试推送了。测试我们这个分两种 1,java的单元测试 2,运行springboot,通过get请求来触发推送 单元测试 [代码]package com.qiushi.wxpush; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.*; /** * Created by qcl on 2019-03-28 * 微信:2501902696 * desc:测试用例 */ @RunWith(SpringRunner.class) @SpringBootTest public class PushControllerTest { @Autowired PushController pushController; @Test public void push() { pushController.push(); } } [代码] 单元测试其实很简单,我们只需要点击箭头所指的绿色按钮,即可运行单元测试,运行通过后就可以看到发送消息成功了。 [图片] log里可以看出我们是10:46发起推送的,看下图我们微信接受到的推送消息也是10:46 [图片] 运行springboot,通过get请求来触发推送 这个就更简单了,我们启动springboot项目,然后调用get请求: [图片] [图片] 可以看到我们也推送成功了。 到这里我们就轻松通过简单几行代码实现了微信模版消息推送的功能了。 我们在企业生产环境时,实现这个功能,步骤和这里是一样的。代码也和这里差不多,只不过多了一个获取用户openid的步骤,这个步骤微信要求比较严格,必须要有备案的网址作为回调,今天就不给大家深入讲解了,后期我会专门写一篇获取微信用户openid的文章出来。 如果你有微信或者java开发方面的问题,可以加我微信交流学习:2501902696。也可以加我微信获取完整源码。
2019-03-28 - 个人微信号绑定的开发/体验者权限数量不够了,该怎么办?
近期有童鞋反映,个人微信号在绑定新的小程序账号的管理员/开发者/体验者时,因个人微信号绑定的小程序账号满额导致无法绑定成功,该怎么办?如果员工离职,原员工微信号与小程序账号没有解绑,那么会浪费小程序账号与员工微信号双方绑定账号数量的名额,对于双方来讲不是一件好事。 方法一:原小程序账号的管理员,登录微信公众平台 - 小程序后台 - 用户身份 找到该开发/体验者微信号的头像昵称可以删除其开发或体验权限,此时,小程序账号又多预留了以为可绑定名额,微信号也一样。 那么问题来了,个人微信号怎么知道自己绑定了多少个小程序账号?如果每个小程序账号的解绑,该微信号的使用者都要一一麻烦管理员去操作,是不是太繁琐? 方法二:搜索微信公众号:公众平台安全助手 ,关注公众号,必须要关注公众号。进入公众号对话的主界面,左下角找到 <绑定查询> 可以根据手机号、身份证号以及微信号的方式查询,这里要用根据 微信号绑定账号 的方式查询。点击进入后,会看到:公众号、小程序、开放平台。因为我们的目的是微信开发者/体验者自主发起解绑小程序账号,所以选择的是小程序,可以看到你目前作为管理员和作为项目成员分别下面有那些小程序账号,在这个页面可以看到每个小程序的头像、名称、原始ID以及是否授权第三方(作为管理员可查看)。 这里要注意,作为管理员下面绑定的小程序账号如果显示已经授权第三方,可以查看第三方的基本信息,其他情况下点击无效。作为项目成员下面绑定的小程序账号均可点击,点击后会提示“解除绑定”,当你点击解除绑定的时候,该小程序账号信息会从该列表消失,此时你的微信号多了一个可绑定小程序账号名额,而你此时无法访问已解绑小程序账号的开发版和体验版,在小程序“小程序开发助手”中也看不到原先解绑的小程序信息。 如果作为管理员下面的小程序不属于你负责的账号,那么这种情况需要管理员登录微信公众平台 - 小程序后台 - 用户身份 修改管理员,即将修改为管理员的微信号必须有可绑定小程序管理员名额且绑定了银行卡经过微信实名认证,具体不做展开,有疑问评论区留言。 关于个人微信号和小程序账号绑定的名额相关估计很多人有疑问,接下来总结梳理一下。 成员管理人数限制,每个小程序帐号可添加一定数量的项目成员,不区分成员身份,具体限制如下: 个人类型小程序账号:15个 未认证、未发布组织类型:30个 已认证未发布/未认证已发布组织类型:60个 已认证已发布组织类型:90个 补充说明 管理员及其他项目成员均可变更。 管理员及其他项目成员绑定帐号数不占用公众号绑定数量限制。 每个微信号可以成为50个小程序的项目成员。 一个微信号最多可以成为5个小程序账号的管理员。 成员权限说明 开发者权限:可使用小程序开发者工具及开发版小程序进行开发 体验者权限:可使用体验版小程序 登录:可登录小程序管理后台,无需管理员确认 数据分析:使用小程序数据分析功能查看小程序数据 开发管理:小程序提交审核、发布、回退 开发设置:设置小程序服务器域名、消息推送及扫描普通链接二维码打开小程序 暂停服务设置:暂停小程序线上服务 疑问:为什么经过 公众平台安全助手 公众号的查询,微信号已经绑定了那么多小程序账号,我尽全然无知? 在微信小程序刚推出的时候,每一次绑定关联开发者/体验者的时候,都需要管理员扫码登录、扫码授权、发送关联邀请、微信用户确认关联等一系列操作才能完成,其实是一个繁琐的过程。 后来在小程序账号绑定微信号做了简化,当管理员在小程序后台输入某个微信用户的ID,并确认点击添加的时候,无需微信用户确认即可完成绑定操作。 上面说了好处,目前这种操作有个不好的地方就是,当管理员如果没经过微信用户同意,也可以随意添加项目成员,这样,微信用户会在神不知鬼不觉的情况下,成为了某个小程序的项目成员。随着小程序账号的增多,参与小程序项目增加,微信号中默默绑定数量增加了,确实不太好。 这里建议官方,当管理员主动发起绑定项目成员和解除项目成员当时候,给微信用户推送一条消息提醒(之前有,现在不知道为何没有了)。
2018-10-18 - 这周撸了两款小程序,分享一些经验(前后台)。
本周撸了两款小程序,在这里总结下开发过程中的小经验,希望对大家有用。 小程序端 我们先说小程序要注意的地方。 默认入口转发问题 当一个小程序Page的js文件中存在 onShareAppMessage 方法时,可以触发转发功能,但是通过小程序开发者工具生成的模板中,入口文件的js中并没有此方法,为了发布后方便对小程序进行分享,建议在pages/index/index.js中添加。 当然默认情况下,我们需要点击小程序右上角的...才能看到转发,这样并不能对用户起到引导作用,通常的做法是使用一个button,并且设置open-type为share,这样就可以通过按钮启动分享。 但是原生按钮很难看,我们可以设置一个图片,在wxml内的代码一般如下 [代码]<[代码][代码]button[代码] [代码]open-type[代码][代码]=[代码][代码]"share"[代码][代码]><[代码][代码]image[代码] [代码]src[代码][代码]=[代码][代码]"/images/icon-share.png"[代码][代码]></[代码][代码]image[代码][代码]></[代码][代码]button[代码][代码]>[代码] 当然需要通过wxss将button的样式去掉,背景、边框等等,代码如下 [代码]button {[代码] [代码] [代码][代码]padding[代码][代码]:[代码][代码]0[代码][代码];[代码] [代码] [代码][代码]margin[代码][代码]:[代码][代码]0[代码] [代码]auto[代码][代码];[代码] [代码] [代码][代码]width[代码][代码]:[代码][代码]70[代码][代码]rpx;[代码] [代码] [代码][代码]height[代码][代码]:[代码][代码]70[代码][代码]rpx;[代码] [代码] [代码][代码]display[代码][代码]:[代码][代码]block[代码][代码];[代码] [代码] [代码][代码]border[代码][代码]:[代码][代码]0[代码][代码];[代码] [代码] [代码][代码]background[代码][代码]: [代码][代码]transparent[代码][代码];[代码] [代码]}[代码] [代码]button::after {[代码] [代码] [代码][代码]border[代码][代码]:[代码][代码]0[代码][代码]; [代码] [代码]}[代码]尤其是对 button::after 要进行设置,否则按钮的边框是无法去掉的。 [图片] 关于登录 关于小程序登录逻辑都差不多,从小程序发一个请求(含code)给服务器代码,服务器通过code换取open_id和session_key,其中open_id我们需要存储,用来表示用户身份,session_key用来获取用户基本信息时解密使用。 当服务器端进行了存储后要生成一个key,将其返给小程序,以后小程序凡是发起需要用户认证的请求,都带这个key用来判断用户身份,在yii2中,这个key就是我们restful中的access_token。 以上是关于小程序登录的前后台逻辑,如果你使用yii2类框架,很多都内置了,并不复杂。 但是这里还有几个问题 1. 小程序端何时进行登录逻辑? 2. 发起请求时access_token过期了如何处理? 小程序端何时进行登录逻辑? 在登录判断上,我们先进行小程序是否含有access_token来判断,当然即便存在,还需要对checkSession进行一次判断。 [代码]var[代码] [代码]session = Session.get();[代码][代码]// 获取access_token[代码] [代码]if[代码] [代码](session) {[代码] [代码] [代码][代码]wx.checkSession({[代码] [代码] [代码][代码]success: [代码][代码]function[代码] [代码]() {[代码][代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]fail: [代码][代码]function[代码] [代码]() {[代码][代码] [代码] [代码] [代码][代码]doLogin();[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]});[代码] [代码]} [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]doLogin();[代码] [代码]}[代码] 这里Session是对access_token的一次封装。 发起请求时access_token过期了如何处理? 这个问题最常发生的场景就是我们发送了一次需要用户认证的请求,此刻如果服务器端发现收到的access_token已经失效,会返回异常,此刻小程序一般要如何处理那? 我的推荐方式是静默状态的自动登录一次再,先看代码。 [代码]if[代码] [代码](response.statusCode === 401) {[代码] [代码] [代码][代码]Session.clear();[代码] [代码] [代码][代码]if[代码] [代码](!hasRetried) {[代码] [代码] [代码][代码]hasRetried = [代码][代码]true[代码][代码];[代码] [代码] [代码][代码]doRequestWithLogin();[代码][代码]// [代码] [代码] [代码][代码]return[代码][代码];[代码] [代码] [代码][代码]}[代码] [代码]}[代码] 我来解释一下这段代码,当小程序发起一次需要用户认证的请求但是被服务器驳回为401错误(一般为用户认证失败),此刻我先清理掉小程序端自身对access_token(Session.clear()方法实现),然后在进行登录后再发起请求(doRequestWithLogin())。 但是我们不能一直在执行请求失败就登录操作,因此可以设置一个开关hasRetried,只进行一次尝试。 客服消息 在小程序开发中,客服消息的重要性不言而喻,它除了作为客服服务外,还作为小程序到微信浏览器的一个渠道,比如本次在「宝宝爱识图」https://nai8.me/images/bao-say-qrcode.png 的开发中,我用它来实现将收款微信号到用户的推送工作,这主要是解决在ios端虚拟产品不能进行微信支付事情。 一般的策略是让客户点击客服按钮然后输入一个关键词,服务器端通过客服消息接口识别用户身份,结合关键词给予响应的消息推送。 [图片] 模板消息 很多人认为很鸡肋的方法,毕竟需要获取form_id和prepay_id后才能下发模板消息,似乎很受限制,但是我们可以建立一个formId的种子表,将尽可能多的用户和后台的行为都采用form表单提交的形式,并且获取formID,对,要变态的多,这样你的formId表数据起来了,以后想发消息的时候用就好了。 关于如何设置能获取formId的小程序表单也很简单,设置report-submit为真即可,如下代码 [代码]<[代码][代码]form[代码] [代码]bindsubmit[代码][代码]=[代码][代码]"abc"[代码] [代码]report-submit[代码][代码]=[代码][代码]"{{true}}"[代码][代码]></[代码][代码]form[代码][代码]>[代码] 然后在js端通过如下代码获取 [代码]abc(event) {[代码][代码] [代码][代码]var[代码] [代码]formId = event.detail.formId;[代码][代码] [代码][代码]xxx[代码][代码]},[代码] 尽可能多的。 服务器端 接下来总结下服务器端,我使用yii2的restful组件作为接口支持,关于restful的基本功能请参考yii2官方文档或我之前录制的课程《Yii2的RESTful讲解》https://nai8.me/book/view.html?id=13 ,在这里分享我认为关键的点。 让yii2能解析json的请求内容 默认情况下yii2并不能识别请求中的json格式,而我们小程序在发起请求时喜欢用它,因此我们要对yii2进行一下配置。 config/web.php [代码]'request'[代码] [代码]=> [[代码][代码] [代码][代码]'cookieValidationKey'[代码] [代码]=> [代码][代码]'xxx'[代码][代码],[代码][代码] [代码][代码]'parsers'[代码] [代码]=> [[代码][代码] [代码][代码]'application/json'[代码] [代码]=> [代码][代码]'yii\web\JsonParser'[代码][代码],[代码][代码] [代码][代码]],[代码][代码]],[代码] 对,在web.php中对组件request增加内容解析yii\web\JsonParser。 用户认证 小程序的登录需要服务端的**用户认证**配合,当然我使用yii2框架,内置的restful已经支持了,如果你的系统不支持用户认证,可以自行建立access_token的生成机制,具体可以参考腾讯开放的小程序服务端框架wafer。 在yii2的restful中的用户认证使用了行为机制,我们来看下流程代码 [代码]// 在需要授权的控制器内[代码][代码]class[代码] [代码]CardController [代码][代码]extends[代码] [代码]ActiveController {[代码] [代码] [代码][代码]public[代码] [代码]$modelClass[代码] [代码]= [代码][代码]'app\modules\say\models\Card'[代码][代码];[代码] [代码] [代码][代码]public[代码] [代码]function[代码] [代码]behaviors() {[代码][代码] [代码][代码]$behaviors[代码] [代码]= parent::behaviors();[代码][代码] [代码][代码]$behaviors[代码][代码][[代码][代码]'authenticator'[代码][代码]] = [[代码][代码] [代码][代码]'class'[代码][代码]=>HttpBearerAuth::className(),[代码][代码] [代码][代码]'only'[代码][代码]=>[[代码][代码] [代码][代码]'index'[代码][代码] [代码][代码]],[代码][代码] [代码][代码]];[代码][代码] [代码][代码]return[代码] [代码]$behaviors[代码][代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]...[代码][代码]}[代码] 就如上面的代码,我们生命index动作是需要用户认证的,并且认证机制为HttpBearerAuth类型 https://nai8.me/video/detail.html?id=175 ,在小程序端需要在header内包含如下代码 [代码]header: {[代码][代码] [代码][代码]'Authorization'[代码][代码]: [代码][代码]'Bearer '[代码] [代码]+ access_token[代码][代码]},[代码] 当服务器验证通过后,在action的代码内直接使用 Yii::$app->user->id 就可以获得用户ID。 ok~ 这就是前几天小程序开发过程中给大家分析的点,后续升级过程中会继续分享给大家。
2018-10-21 - 总结16点微信小程序推广技巧、营销方案
小程序已经成功上线了!那么,小程序线下如何推广?线下门店如何玩转小程序呢? [图片] 1、附近的小程序,让商家曝光率更高 小程序自带“附近的小程序”功能,利用LBS定位功能提高商家专属微信小程序的曝光度,用户走进某一个点位时,就能在附近的小程序中显示出商家的小程序信息,对于增加商业机会有一定的帮助。可以直观地在小程序界面展示门店商家的地理位置、门店信息、联系电话、经营时间等信息。 [图片] 2、小程序做内容营销,让信息传播更方便 小程序本身可以作为一个内容推送平台,可以向用户发送消息。通过这种方式,可以让商家的优惠活动信息等消息快速直接准确地传递到消费者眼前,增加客户的回头率。 3、线上小程序异业互推 同样,通过小程序跳转小程序功能,异业合作的商户只需将小程序绑定在同一公众号上,即可实现小程序间的互相跳转推广。 4、利用微信公众号本身推广 现在,在微信文章内部,可以直接以卡片的形式插入小程序,小程序可以和微信文章转发分享。当然用户阅读美食文章的同时,可直接通过门店小程序查询商家位置,促近到店消费,我们可以引导用户把文章分享给更多的人,这样流量也成倍的增加。 [图片] 5、公众号自定义菜单栏添加门店小程序 进入公众号后台,在“自定义菜单”中可添加门店小程序。当用户关注公众号时,“被添加自动回复”可发消息,引导用户在菜单栏进入门店小程序。 6、积分活动 商家可在门店小程序中投放会员卡、优惠券、买单券等卡券,吸引周围的目标受众到门店消费。这些卡券,还会展示在“附近的小程序”功能中,增加门店小程序打开率。 7.社交裂变在微信这个社交平台,小程序每一个功能的设计都应该围绕社交及分享这个点来打,才能达到快速裂变的效果; 8、荐客有礼小程序推广 通过荐客有礼功能,顾客可以通过推荐顾客、会员的形式,获取积分、余额、优惠券等福利,从而实现商家的社交裂变拓客。如用户推荐成功一名会员,即可获得1元余额。 9.微信群流量公众号、朋友圈、线下、搜索等功能给小程序带来的量有限,转化效果最好还是微信群,因其在微信群展示可以玩的创意较多,容易引导用户点击。据说*多多一开始每次采购微信群的数量在10万个以上。 10、社交立减金 通过微信的社交立减金功能,用户在小程序内购物后,会获得“立减金”,将“立减金”分享给好友/微信群的同时,自己也完成了领取。好友在领取“立减金”后,也可通过快速入口完成新的交易。通过社交裂变,带来新的流量。 11、新媒体软文推广 借助KOL的粉丝号召力推广小程序,不失为新媒体时代的一大主流推广方式。商家想进行精准性的推广,可以寻找行业内媒体人或KOL进行推广传播,如餐饮行业可寻找美食博主,通过优质的软文广告,获得精准用户。比如我在本文中插入微信小程序下载https://www.sucaihuo.com/source/213-0-0-0-0-0,这就是软文推广。速成应用小程序,推荐几个软文平台搜狐自媒体、百家号、新浪博客,今日头条、网易媒体人等等 12、异业流量互推 拥有小程序的商家可以选择相似目标客群,不同产品或服务的商户进行流量互推,如女装商户可以与美妆商户合作,在线上公众号,微信社群等渠道,互相推广小程序,实现流量共享。 [图片] 13、二维码+海报 这是小程序运营推广最常见的方法,将小程序加在海报、名片、门店、宣传册等,用户扫一扫就能了解详情。 14、线下异业合作 线下的商家也可选择服务类型互补的商户进行店内互推,如在大型商场内,餐厅可推送顶楼的电影院小程序,进行引流,而在电影票上也可放上楼下餐厅的小程序优惠活动,互相倒流,实现共赢。 15、传统广告推广 传统广告可以在一段时间内获得品牌曝光,如果需要进行快速曝光,商户可以在地铁,人流集中地寻找优质广告位,进行投放,广告上可加入小程序码,引导线下流量直接扫码转化。「速成应用」专业打造A+级微信小程序的平台,制作小程序不需要代码,通过拖动可视化组件,就可生成自己的微信小程序。 16、公众号底部广告 通过小程序公众号底部广告,电商商户将构建更多消费场景,加速商业变现。商户可以将自己的电商小程序投放公众号底部广告,点击广告外层图片直接跳转指定小程序落地页(目前仅支持电商推广与品牌活动推广两大类目能力)。
2019-01-06 - 各家小程序的差异点
ignore_js_op style="word-wrap: break-word; color: rgb(81, 81, 81); font-family: "Microsoft Yahei", Simsun; font-size: 14px; white-space: normal; background-color: rgb(255, 255, 255);">[图片]/ignore_js_op>ignore_js_op style="word-wrap: break-word; color: rgb(81, 81, 81); font-family: "Microsoft Yahei", Simsun; font-size: 14px; white-space: normal; background-color: rgb(255, 255, 255);">[图片] /ignore_js_op> APIs 的这么多方法都不一样,可能以后针对不同的平台打包不同的 api.js 来屏蔽差异性 ignore_js_op style="word-wrap: break-word; color: rgb(81, 81, 81); font-family: "Microsoft Yahei", Simsun; font-size: 14px; white-space: normal; background-color: rgb(255, 255, 255);">[图片] /ignore_js_op>
2019-01-09 - 社区经验分享与问题总结
1. 插件开发中,使用自定义组件需用到相对路径 "usingComponents":{ "alert":"../../components/alert/alert" } 注:选择自定义组件需要加上in(this) wx.createSelectorQuery().in(this).select('.xxxxx') 2. 获取小程序码相关 通过接口生成的小程序码page必须是已经发布的 生成小程序码后可在客户端开发工具中,通过二维码编译进行测试scene参数 [图片] 3. swiper组件current不重置问题(bug) 如:通过arr=[1,2]遍历swiper-item组件,当swiper滑动到current=1时,setData({arr:[2]});此时swiper会出现空白。 原因:current未重置为0,需自己去设置current。 https://developers.weixin.qq.com/community/develop/doc/00066c8beacfa05f24d7d144056800 4. 模板消息相关 时效性:1次支付可下发3条,1次提交表单可下发1条。(7天内有效) 对应性:发送消息的对象openId和formId是匹配的。 获取方式:发起支付或表单提交 5. 分包加载大小限制问题:使用分包加载时,如果在分包中使用插件,插件大小只会算在分包大小2MB与整包8MB内,不算入主包2MB。(之前算在了主包内,目前已修复)。 6. 获取unionId(包括openId)流程(前提:小程序 或 其主体公众号 与 微信开发平台账号关联) 开放平台关联同主体的公众号且 用户已经关注公众号: wx.login()=> 获取到code,后端通过appid+appserect+code,拿到openId+ session_key+unionId 开放平台关联小程序: 用户授权后通过wx. getUserInfo(需要授权)获取iv、encryptedData,然后解密(需要用到上面的 session_key),appid+ session_key + encryptedData + iv解密 得到unionId、openId及用户信息 [图片] 注:如果再次获取code会导致之前的session_key过期 7. h5与小程序跳转问题 公众号=>小程序:公众号自定义菜单可配置跳转到小程序 小程序=>h5:webview(需配置业务域名、webview不支持个人账号) 注:目前不支持h5与小程序的直接跳转 8. canvas原生组件覆盖自定义弹层的解决方案 用css样式控制器显示或隐藏,如hidden 纯显示性的canvas可以生成图片之后展示 9. 自定义弹层背景滚动问题 方法一:打开的函数中,如果自定义弹框当前显示,则isScroll设为true,否则设为false <scroll-viewclass="scanInvoice_content" height="100%"scroll-y="{{isScroll}}"> //设置Page的overflow-y属性值为hidden </scroll-view> 方法二:事件捕获,顶层加上catchtouchmove 10. 小程序图片分享截取变形或显示不全:保持分享的图片是5:4。 11. 授权问题 wx.authorize可以对除scope.userinfo之外的权限进行授权,scope.userinfo需要用<button open-type="getUserInfo"/>组件进行授权。 统一小程序下的用户拒绝授权之后会直接进入失败回调,这种情况可使用wx.openSetting引导用户授权。(用户手动删除小程序才会重新提示授权) 12. 数据绑定是双括号内只能是data里面的变量或者wxs里声明的函数。 13.不要用wx.request去访问微信接口,官方限制且不能把[代码]api.weixin.qq.com[代码]配置为服务器域名 基础库版本:v2.4.0 欢迎更新指正
2018-12-28 - 叠式轮播图
开发工具和iOS测过,android我没测过。。哈哈哈哈哈 https://developers.weixin.qq.com/s/kh8HhjmA7A4D 注释不知道写啥,简单描述了下 [图片]
2018-11-30 - 关于map层级的问题
[图片] 像这种浮层在地图的上方 是如何实现的? 地图好像是张图片不能移动
2018-12-10