- 小程序跨页面通信解决方案
场景介绍 在小程序开发过程中,难免会遇到一种情况,当A页面需要用户设置数据 点击进入B页面,在B页面设置成功后返回并将设置的值传递给A页面。但是wx.navigateBack()并不支持返回传参。这种情况下就可以使用onfire.js,onfire.js 是一个很简单的事件分发的 Javascript 库(仅仅 0.9kb),简洁实用。 下载地址 onfire.js下载地址 https://www.bootcdn.cn/onfire.js/ https://gitee.com/mirrors/onfire-js 如何使用 将onfire.js下载下来并放置在开发项目某个目录下,例如根目录lib文件夹内。 在使用页面对应的js文件中引入该文件。 代码如下: A页面 [代码]<!--index.wxml--> <view class="order"> <view class="order-alert">设置您的联系方式</view> <view class="order-mobile" bindtap="setMobile"> <view class="order-mobile__caption">联系方式</view> <view class="order-mobile__content"> <text class="valign-mid"> <text>{{ mobile }}</text> </text> <text class="iconfont order-mobile__content__more"></text> </view> </view> </view> [代码] [代码]//index.js const onfire = require('../../lib/onfire.js') Page({ data: { mobile: '' }, onLoad: function () { // 绑定事件,当名为EventPhoneChange的事件发生的时 onfire.on('EventPhoneChange', e => { this.setData({ mobile: e }) }) }, // 设置手机号 setMobile: function () { wx.navigateTo({ url: '../phone/phone?mobile=' + this.data.mobile, }) }, onUnload: function () { // 取消事件绑定 onfire.un("EventPhoneChange"); } }) [代码] B页面 [代码]<!--phone.wxml--> <view class="phone"> <input class="phone-input" placeholder="手机号码" bindinput="bindinput" value="{{mobile}}"></input> <button class="phone-setting" bindtap="tapSetting">设置</button> </view> [代码] [代码]// phone.js const onfire = require('../../lib/onfire.js') Page({ data: { mobile: '' }, onLoad: function (e) { this.setData({ mobile: e.mobile }) }, tapSetting: function () { let mobile = this.data.mobile; if (!mobile) { wx.showToast({ title: '请填写手机号!', icon: 'none', duration: 2000, }) return; } // 触发名为EventPhoneChange的事件,并且携带变量mobile值。 onfire.fire('EventPhoneChange', mobile) wx.navigateBack() }, bindinput: function (e) { this.setData({ mobile: e.detail.value.trim() }) } }) [代码] 其效果图如下: 图一 [图片] 图二 [图片] 图三 [图片] 相关API 关于onfire.js的API 1.on(event_name, callback) 绑定事件,参数为event_name和callback, 当有名字为event_name的事件发生的时候,callback方法将会被执行。这个方法会返回一个eventObj,这个可以用于使用un(eventObj)方法来取消事件绑定。 2.one(event_name, callback) 绑定(订阅)事件,参数为 event_name with callback. 当被触发一次之后失效。只能被触发一次,一次之后自动失效。 3.fire(event_name, data) 触发名字为event_name的事件,并且赋予变量data为callback方法的输入值。 4.un(eventObj / eventName / function)取消事件绑定。可以仅仅取消绑定一个事件回调方法,也可以直接取消全部的事件; 5.clear() 清空所有事件。 总结 个人对于onfire.js的理解就是一个全局通知,从B页面发出一个通知带上Key和Value,在其他某一个页面监听这个Key值来获取传出来的Value。 其他解决方案 该方案来自摩拜单车小程序 https://juejin.im/post/5da80767f265da5b5f7584dc
2019-12-20 - [打怪升级]小程序自定义头部导航栏“完美”解决方案
[图片] 为什么要做这个? 主要是在项目中,智酷君发现的一些问题 一些页面是通过扫码和订阅消息访问后,没有直接可以点击去首页的,需要添加一个home链接 需要添加自定义搜索功能 需要自定义一些功能按钮 [图片] 其实,第一个问题,在最近的微信版本更新中已经优化了,通过 小程序模板消息 过来的,系统会自动加上home按钮,但对于其他的访问方式则没有支持~ 一个不大不小的问题:两边ICON不对齐问题 [图片] 智酷君之前尝试了各种解决方法,发现有一个问题,就是现在手机屏幕太多种多样,有 传统头部、宽/窄刘海屏、水滴屏等等,无法八门,很多解决方案都无法解决特殊头部,系统**“胶囊按钮”** 和 自定义按钮在Android屏幕可能有 几像素不对齐 的问题(强迫症的噩梦)。 下面分享下一个相对比较完善的解决方案: [图片] 小程序代码段DEMO Link: https://developers.weixin.qq.com/s/cuUaCimT72cH ID: cuUaCimT72cH 智酷君做了一个demo代码段,方便大家直接用IDE工具查看源码~ [图片] 页面配置 1、页面JSON配置 [代码]{ "usingComponents": { "NavComponent": "/components/nav/common" //以插件的方式引入 }, "navigationStyle": "custom" //自定义头部需要设置 } [代码] 如果需要自定义头部,需要设置navigationStyle为 “custom” 2、页面代码 [代码]<!-- home 类型的菜单 --> <NavComponent v-title="自定义头部" bind:commonNavAttr="commonNavAttr"></NavComponent> <!-- 搜索菜单 --> <NavComponent is-search="true" bind:commonNavAttr="commonNavAttr"></NavComponent> [代码] 可以在自定义导航标签上添加属性配置来设置功能,具体按照实际需要来 3、目录结构 [代码]│ ├─components │ └─nav │ common.js │ common.json │ common.wxml │ common.wxss │ ├─images │ back.png │ home.png │ └─index index.js index.json index.wxml index.wxss search.js search.json search.wxml search.wxss [代码] 仅供参考 插件对应的JS部分 components/nav/common.js部分 [代码]const app = getApp(); Component({ properties: { vTitle: { type: String, value: "" }, isSearch:{ type: Boolean, value: false } }, data: { haveBack: true, // 是否有返回按钮,true 有 false 没有 若从分享页进入则没有返回按钮 statusBarHeight: 0, // 状态栏高度 navbarHeight: 0, // 顶部导航栏高度 navbarBtn: { // 胶囊位置信息 height: 0, width: 0, top: 0, bottom: 0, right: 0 }, cusnavH: 0, //title高度 }, // 微信7.0.0支持wx.getMenuButtonBoundingClientRect()获得胶囊按钮高度 attached: function () { if (!app.globalData.systeminfo) { app.globalData.systeminfo = wx.getSystemInfoSync(); } if (!app.globalData.headerBtnPosi) app.globalData.headerBtnPosi = wx.getMenuButtonBoundingClientRect(); console.log(app.globalData) let statusBarHeight = app.globalData.systeminfo.statusBarHeight // 状态栏高度 let headerPosi = app.globalData.headerBtnPosi // 胶囊位置信息 console.log(statusBarHeight) console.log(headerPosi) let btnPosi = { // 胶囊实际位置,坐标信息不是左上角原点 height: headerPosi.height, width: headerPosi.width, top: headerPosi.top - statusBarHeight, // 胶囊top - 状态栏高度 bottom: headerPosi.bottom - headerPosi.height - statusBarHeight, // 胶囊bottom - 胶囊height - 状态栏height (胶囊实际bottom 为距离导航栏底部的长度) right: app.globalData.systeminfo.windowWidth - headerPosi.right // 这里不能获取 屏幕宽度,PC端打开小程序会有BUG,要获取窗口高度 - 胶囊right } let haveBack; if (getCurrentPages().length != 1) { // 当只有一个页面时,并且是从分享页进入 haveBack = false; } else { haveBack = true; } var cusnavH = btnPosi.height + btnPosi.top + btnPosi.bottom // 导航高度 console.log( app.globalData.systeminfo.windowWidth, headerPosi.width) this.setData({ haveBack: haveBack, // 获取是否是通过分享进入的小程序 statusBarHeight: statusBarHeight, navbarHeight: headerPosi.bottom + btnPosi.bottom, // 胶囊bottom + 胶囊实际bottom navbarBtn: btnPosi, cusnavH: cusnavH }); //将实际nav高度传给父类页面 this.triggerEvent('commonNavAttr',{ height: headerPosi.bottom + btnPosi.bottom }); }, methods: { _goBack: function () { wx.navigateBack({ delta: 1 }); }, bindKeyInput:function(e){ console.log(e.detail.value); } } }) [代码] 解决不同屏幕头部不对齐问题的终极办法是 wx.getMenuButtonBoundingClientRect() 这个方法从微信7.0.0开始支持,通过这个方法我们可以获取到右边系统胶囊的top、height、right等属性,这样无论是水滴屏、刘海屏、异形屏,都能完美对齐右边系统默认的胶囊bar,完美治愈强迫症~ APP.js 部分 [代码]//app.js App({ /** * 加载页面 * @param {*} options */ onShow: function (options) { }, onLaunch: async function () { let self = this; //设置默认分享 this.globalData.shareData = { title: "智酷方程式" } // this.getSysInfo(); }, globalData: { //默认分享文案 shareData: {}, qrCodeScene: false, //二维码扫码进入传参 systeminfo: false, //系统信息 headerBtnPosi: false, //头部菜单高度 } }); [代码] 将获取的参数存储在一个全局变量globalData中,可以减少反复调用的性能消耗。 插件HTML部分 [代码]<view class="custom_nav" style="height:{{navbarHeight}}px;"> <view class="custom_nav_box" style="height:{{navbarHeight}}px;"> <view class="custom_nav_bar" style="top:{{statusBarHeight}}px; height:{{cusnavH}}px;"> <!-- 搜索部分--> <block wx:if="{{isSearch}}"> <input class="navSearch" style="height:{{navbarBtn.height-2}}px;line-height:{{navbarBtn.height-4}}px; top:{{navbarBtn.top+1}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;" maxlength="10" bindinput="bindKeyInput" placeholder="输入文字搜索" /> </block> <!-- HOME 部分--> <block wx:else> <view class="custom_nav_icon {{!haveBack||'borderLine'}}" style="height:{{navbarBtn.height}}px;line-height:{{navbarBtn.height-2}}px; top:{{navbarBtn.top}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;"> <view wx:if="{{haveBack}}" class="icon-back" bindtap='_goBack'> <image src='/images/back.png' mode='aspectFill' class='back-pre'></image> </view> <view wx:if="{{haveBack}}" class='navbar-v-line'></view> <view class="icon-home"> <navigator class="home_a" url="/pages/home/index" open-type="switchTab"> <image src='/images/home.png' mode='aspectFill' class='back-home'></image> </navigator> </view> </view> <view class="nav_title" style="height:{{cusnavH}}px; line-height:{{cusnavH}}px;"> {{vTitle}} </view> </block> </view> </view> </view> [代码] 主要是对几种状态的判断和定位的计算。 插件CSS部分 [代码]/* components/nav/test.wxss */ .custom_nav { width: 100%; background: #3a7dd7; position: relative; z-index: 99999; } .custom_nav_box { position: fixed; width: 100%; background: #3a7dd7; z-index: 99999; border-bottom: 1rpx solid rgba(255, 255, 255, 0.3); } .custom_nav_bar { position: relative; z-index: 9; } .custom_nav_box .nav_title { font-size: 28rpx; color: #fff; text-align: center; position: absolute; max-width: 360rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; top: 0; left: 0; right: 0; bottom: 0; margin: auto; z-index: 1; } .custom_nav_box .custom_nav_icon { position:absolute; z-index: 2; display: inline-block; border-radius: 50%; vertical-align: top; font-size:0; box-sizing: border-box; } .custom_nav_box .custom_nav_icon.borderLine { border: 1rpx solid rgba(255, 255, 255, 0.3); background: rgba(0, 0, 0, 0.1); } .navbar-v-line { width: 1px; margin-top: 14rpx; height: 32rpx; background-color: rgba(255, 255, 255, 0.3); display: inline-block; vertical-align: top; } .icon-back { display: inline-block; width: 74rpx; padding-left: 20rpx; vertical-align: top; /* margin-top: 12rpx; vertical-align: top; */ height: 100%; } .icon-home { /* margin-top: 8rpx; vertical-align: top; */ display: inline-block; width: 80rpx; text-align: center; vertical-align: top; height: 100%; } .icon-home .home_a { height: 100%; display: inline-block; vertical-align: top; width: 35rpx; } .custom_nav_box .back-pre, .custom_nav_box .back-home { width: 35rpx; height: 35rpx; vertical-align: middle; } .navSearch { width: 200px; background: #fff; font-size: 14px; position: absolute; padding: 0 20rpx; z-index: 9; } [代码] 总结: 通过微信API: getMenuButtonBoundingClientRect(),结果各类手机屏幕的适配问题 将算好的参数存储在全局变量中,一次计算全局使用,爽YY~ 往期回顾: [填坑手册]小程序PC版来了,如何做PC端的兼容?! [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二)
2021-09-13 - 如何让previewImage支持索引
问题背景 若一个页面下有以下几张图片 [A, A, A, B] 通过 [代码]wx.previewImage[代码] 预览时,由于 [代码]current[代码] 只支持 [代码]string[代码] 类型,点击[代码]A[代码]图片时,不管是哪一张,都会被认为是第一张(即右滑时还是 [代码]A[代码] 图片) 要解决这个问题,必须使得相同 [代码]src[代码] 的图片能被区分开 方案一 主要思想 给图片链接后加一个没有用的参数使得图片链接各不相同 如上题中将链接变为 [A, A?id=1, A?id=2, B] 即可解决这个问题 缺陷 加上参数后每个图片都要被请求一次(不能被缓存),增加了请求次数 方案二 主要思想 在预览时删除重复的图片 如图片为 [A, B, A, C] ,则在预览第一张 [代码]A[代码] 图片时 [代码]urls[代码] 传入 [A, B, C],在预览第二张 [代码]A[代码] 图片时传入 [B, A, C] 缺陷 无法通过左右滑动看到重复的图片 若是开头的例子仍会出现不管点哪一张 [代码]A[代码] 图片都会是右滑后就是 [代码]B[代码] 图片,无法区分 方案三 主要思想 [代码]indexOf[代码] 是区分大小写的,而链接一般不区分大小写,因此可以把链接的一部分改为大写来区分 具体分析 [代码]wx.previewImage[代码] 只支持网络链接,网络链接的一般结构为 [代码][协议]://[域名]/[路径][代码] ,其中协议和域名是不区分大小写的,而路径则可能影响。因此可以将协议和域名的大小写重新组合实现链接的不同,但显示的是同一张图片,例如传入的 [代码]urls[代码] 为 [“http://example.com”, “http://example.com”] 可以修改为 [“http://example.com”, “Http://example.com”] 这样就可以区分开,通过协议+域名的大小写重新组合,一般可以提供足够多不同的 [代码]src[代码] (有 [代码]2^n[代码] 种) 优点 可以实现同方案一的效果,预览重复的图片不会定位错误 不更改图片链接,不会产生多余的请求 程序实现 [代码]function previewImage(object) { console.log("修改前:", object.urls); var urls = []; for (var i = 0; i < object.urls.length; i++) { var url = object.urls[i]; // 网络链接 if (/:\/\//.test(object.urls[i])) { // 有重复 while (urls.indexOf(url) != -1) { var j; url = ""; for (j = 0; j < object.urls[i].length; j++) { var c = object.urls[i][j]; // 这里直接用了random函数来确定大小写,因为协议+域名通常在10位以上,有大于1000种可能,重复概率低 if (/[a-zA-Z]/.test(c)) url += (Math.random() >= 0.5 ? c.toUpperCase() : c); else url += c; if (c == '/' && object.urls[i][j - 1] != '/' && object.urls[i][j + 1] != '/') break; } url += object.urls[i].substr(j + 1); //路径部分直接添加 } } urls.push(url); } console.log("修改后:", urls); wx.previewImage({ current: urls[object.current], urls, success: object.success, fail: object.fail }) } previewImage({ current: 1, urls: ["https://www.baidu.com/img/bd_logo1.png", "https://www.baidu.com/img/bd_logo1.png"] }) [代码] [图片] 这个函数可以实现 [代码]current[代码] 直接传入索引值,并且存在多张相同图片时,不会定位错误(如这个示例中设置了 [代码]current[代码] 为 [代码]1[代码],则预览时只能左滑查看第 [代码]0[代码] 张,无法右滑) ps: 如果只是在 [代码]wx.previewImage[代码] 中使用,协议和域名都不区分大小写,但是如果在 [代码]image[代码] 组件的 [代码]src[代码] 中使用,协议必须小写(否则无法显示),只能对域名进行修改
2019-10-31 - 小程序设计规范总结
一、设计稿用二倍图 UI设计师都喜欢用一倍图,但为什么用一倍图,方便适配是表面现象,真正原因是开发用的开发工具支持一倍图开发,开发不用再换算数值了。而小程序的开发工具不支持一倍图开发,支持二倍图开发。小程序里的尺寸单位叫 rpx,可以根据屏幕宽度进行自适应。在750*1334的设计稿里,1px = 1rpx。 [图片] 如果你心疼你的开发小哥哥,让他少掉点头发,设计稿用750*1334的尺寸。但如果你的开发小哥哥之前老欺负你,那你就看着办吧,他都能搞定。 [图片] 二、切图只需切750的尺寸 设计稿用二倍图做的(750 * 1334), 只需给一倍的切片;如果是按一倍图设计的(375 * 667),只要给二倍的切图。真的不管其它尺寸吗?不是不想管,而是小程序的开发工具不支持。 三、导航栏不要自定义 标准高度:128rpx 小程序很轻量,同时也有很多限制。导航栏不能自定义就是其中之一,能改变的只有颜色。 [图片] 这是官方的意思,但是,要改也是可以的,需要客户端版本是6.6.0以上,而且下拉会使整个页面下拉,也不好维护。 [图片] 你会说这不有小程序已经使用了嘛,是,这个小程序叫汪卡,现在,他们已经改回来了。所以,你也别难为你的开发小哥哥了。 [图片] 四、标签栏爱素颜 这个标准名称叫:标签分页导航,标准高度:98rpx,简称标签栏。 1. 偷笑别人花了脸 又是一个安静的晚上,一个人窝在公司里设计,我承认这样真的很无奈,和其它小程序一样。听说你还在搞什么原创,加个投影来点渐变,自以为这样很棒简直无懈可击。结果开发小哥哥哭了,我相信是很美美的图,但是开发做不到啊,那种表情可以想象。 [图片] 虽然也可以,做点其它形态,那就拜托别让开发见到你。如果再能看到你,一定就是这么说,原生控件好处多多***能用它。不用担心出问题,不用维护怕麻烦。 2. 图标只要81rpx 还是一个安静的晚上,还是我在做设计,这次我真的按耐不住,和其它小程序一样。听说你又再搞什么原创,中间图标变大大,破形破形再破形,燃烧开发脑细胞。 [图片] 但是,要知道,图标只要81rpx,小于大于都变形,一定记得规格框。数量只能2至5,多了少了不算数,你只能去改图标,其它组件说了算。 五、弹窗不覆盖导航跟标签 在小程序里导航栏跟标签栏的层级是最高的,以至于享受惯最高待遇的弹窗在这里,也要臣服于他们。 [图片] 六、视频限时多 小程序对视频的支持不是特别的好,原则上在滚动控件里不能放视频,而且微信官方文档里是这样要求的。这点我也像我们开发小哥哥求证,确实是这样,优酷和腾讯视频都是将视频固定。 [图片] 但其实吧,这点已经被有些公司攻克了,比如开眼跟京东。攻克是攻克了,但是体验不怎么好。如果公司产品需要放视频,建议专门新开一个页面,视频部分***不要有左右滚动。 七、一稿适配 iPhone X 怎么办,安卓怎么办,这些都不用再设计了,开发小哥哥都能搞定,相信他们。 [图片] 八、关于设计资源 微信官方虽然有提供,但是更新时间停留在了2016年,没有现在新版的小程序样式,和现在的区别就在顶部导航栏上。
2019-08-05 - 小程序尽快出推送解决方案
- 需求的场景描述(希望解决的问题) 系统中用到消息推送功能,比如通知公告、审批、预警等场景 - 希望提供的能力 能够提供消息推送功能,权限可以放给用户
2019-06-27 - 小程序组件化开发
一、组件实现方式:template模板和component构造器 除了component,小程序中还有另一种组件化你的方式template模板 区别: 1、template主要是展示,方法则需要在调用的页面中定义。简单来说,如果只是展示,使用template就足够了 2、而component组件则有自己的业务逻辑,可以看做一个独立的page页面。如果涉及到的业务逻辑交互比较多,那就最好使用component组件了。 二、template模板 1、模板定义 建议单独创建template目录,在template目录中创建管理模板文件。 由于模板只有wxml、wxss文件,一个template的模板文件和样式文件只需要命名相同即可,方法则需要在调用的页面中定义 模板文件(wxml):用name区分多个模板 [代码]<template name="packModule"> <view class="packModule">packModule</view> </template> [代码] 模板文件(wxss):自定义模板的样式文件(略),实例中模块相关样式都统一集中在module.wxss中 2、页面引用:(如首页引用) index.wxml: [代码]<!--导入模板--> <import src="./modules/pack.wxml"/> <!--嵌入模板--> <view class="moduleWrap" wx:for="{{moduleInfoList}}" wx:for-item="moduleInfo" wx:key="index"> <!--自由容器模块 里面还有子模块--> <template is="packModule" data="{{moduleInfo}}" wx-if="{{moduleInfo.style == 5}}"></template> <view> [代码] index.wxss: [代码]@import "../../libs/templates/module.wxss"; [代码] 备注: 一个模板文件中可引用多个template,每个template均以name进行区分,页面调用的时候也是以name指向对应的template; template模板没有配置文件(.json)和业务逻辑文件(.js),所以template模板中的变量引用和业务逻辑事件都需要在引用页面的js文件中进行定义; 三、Component组件: [图片] 1. 组件创建: 新建component目录——创建子目录——新建Component(如示例组件:dialog组件) 示例dialog组件也由4个文件构成,与page文件类型相同,但是js文件和json文件与页面有所不同。 dialog.wxml: [代码]<view class="dialog" wx:if="{{ isShow }}"> <!-- 遮罩层 --> <view class="dialog_mask" catchtouchmove="_catchTouch"></view> <!-- 内容 --> <view class="container" catchtouchmove="_catchTouch"> <view class="title" wx:if="{{title}}">{{title}}</view> <view class="content" wx:if="{{content}}"> {{content}} </view> <view class="footer"> <view class="btn cancel_btn" wx:if="{{showCancelButton}}" bindtap="hide" style='background-color:{{globalColor}};'>{{cancelButton}}</view> <view class="btn comfirm_btn" bindtap="comfirm" style='background-color:{{globalColor}};'>{{confirmButton}}</view> </view> </view> </view> [代码] dialog.json: [代码]{ "component": true, "usingComponents": {} } [代码] dialog.wxss:(组件对应 wxss 文件的样式,只对组件wxml内的节点生效) [代码]/* components/dialog/dialog.wxss */ .dialog { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; position: fixed; top:0; z-index: 9999; } ... [代码] dialog.js: [代码]/* * dialog 模态对话框 * Props * 通过事件调用组件 * globalColor:全局色 * Event * show 模态对话框 @param {Object} 配置参数 * hide 模态对话框 * Example * 1、页面中引用dialog:(示例index页面) * 1-1:index.json中声明组件引用 * { * "usingComponents": { * "dialog": "../../components/dialog/dialog" * } * 1-2:index.wxml引用模板 * <dialog id='dialog' global-color="{{globalColor}}"></toast> * 1-3:index页面所引用js文件中,获取组件实例 * onReady: function () { * //获得组件 * this.dialog = this.selectComponent("#dialog"); * } * 2、根据业务条件进行调用: * 2-1、页面中调用: * this.dialog.show({ * title: "提交成功", * content: "描述信息", * cancelButton: "取 消", * showCancelButton:false,//是否显示取消按钮,可缺省,默认不显示 * confirmButton: '确 定', * callback: function () {}//确定按钮回调函数,可缺省,默认只关闭对话框 * }); * 2-1、页面组件中调用: * getCurrentPages()[getCurrentPages().length - 1].dialog.show(...);//传参同上 * 备注:不直接在组件ready中获取dialog组件,会产生多个组件实例,故直接调用页面已有组件实例方法 */ Component({ /** * 组件的属性列表 */ properties: { 'globalColor': String }, /** * 组件的初始数据 */ data: { isShow: false, title: '标题',// 弹窗标题 content: "", // 弹窗内容 cancelButton: '取 消', showCancelButton:false,//是否显示"取消"按钮 confirmButton: '确 定', callback: null //回调函数 }, /** * 组件的方法列表 */ methods: { //隐藏信息提示 hide() { this.setData({ isShow: !this.data.isShow }) }, // 阻止页面滚动 _catchTouch: function () { return; }, //展示信息提示 show(options) { this.setData({ isShow: !this.data.isShow, callback:null }); let _this = this; // 通过options参数配置 if (options) { this.setData(options); } }, // 确定回调 comfirm(){ this.hide(); this.data.callback && this.data.callback();//执行各dialog的回调逻辑 } } }) [代码] 2、页面引用: index.json:(需在json配置文件中进行配置开启使用组件) [代码]{ "usingComponents": { "dialog": "/components/dialog/dialog" } } [代码] index.wxml:(模板文件中引用) [代码]<!-- 自定义dialog组件 --> <dialog id='dialog' global-color="{{globalColor}}"></dialog> [代码] 四、注意点: 1、component组件中扩展自定义节点: 在组件模板中可以提供一个 <slot> 节点,用于承载组件引用时提供的子节点。 默认情况下,一个组件的wxml中只能有一个slot。需要使用多slot时,可以在组件js中声明启用,以不同的 name 来区分。 [代码]Component({ options: { multipleSlots: true // 在组件定义时的选项中启用多slot支持 } }) [代码] 2、component组件样式注意点: 组件对应 wxss 文件的样式,只对组件wxml内的节点生效。 2-1、组件和引用组件的页面不能使用id选择器(#a)、属性选择器([a])和标签名选择器,请改用class选择器。 2-2、组件和引用组件的页面中使用后代选择器(.a .b)在一些极端情况下会有非预期的表现,如遇,请避免使用。 2-3、子元素选择器(.a>.b)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况。 2-4、继承样式,如 font 、 color ,会从组件外继承到组件内。 2-5、除继承样式外, app.wxss 中的样式、组件所在页面的的样式对自定义组件无效(除非更改组件样式隔离选项)。 [代码] #a { } /* 在组件中不能使用 */ [a] { } /* 在组件中不能使用 */ button { } /* 在组件中不能使用 */ .a > .b { } /* 除非 .a 是 view 组件节点,否则不一定会生效 */ [代码] 五、组件间通信与事件: 1、父组件(调用页面)向子组件传值通讯: 通过properties向自定义组件传递数据 2、子组件向父组件(调用页面)传值通讯: 1、监听事件 自定义组件可以触发任意的事件,引用组件的页面可以监听这些事件 [代码]<!-- 当自定义组件触发“myevent”事件时,调用“onMyEvent”方法 --> <component-tag-name bindmyevent="onMyEvent" name="{{name}}" /> Page({ data:{ name:"test" }, onMyEvent: function(e){ e.detail // 自定义组件触发事件时提供的detail对象 } }) [代码] 2、触发事件 自定义组件触发事件时,需要使用 triggerEvent 方法,指定事件名、detail对象和事件选项: [代码]<!-- 在自定义组件中 --> <view>{{name}}</view> <button bindtap="onTap">点击这个按钮将触发“myevent”事件</button> Component({ properties: { name: { type: String, value: '' } }, methods: { onTap: function(){ var myEventDetail = {} // detail对象,提供给事件监听函数 var myEventOption = {} // 触发事件的选项 this.triggerEvent('myevent', myEventDetail, myEventOption) } } }) [代码] 总结 自定义组件,可以理解为一个自定义的标签,页面的一个片段,可以分为template方式和component组件方式实现;如果是简单的内容展示,逻辑单一,使用template方式即可,但如果每一个组件都有自己的业务逻辑,各自独立,建议使用component组件方式实现,灵活性更高。 参考文献 官方文档
2019-06-20 - 小程序自定义单页面、全局导航栏
摘要: 小程序开发技巧。 作者:小白 原文:小程序自定义单页面、全局导航栏 Fundebug经授权转载,版权归原作者所有。 需求 产品说小程序返回到首页不太方便,想添加返回首页按钮,UI说导航栏能不能设置背景图片,因为那样设计挺好看的。 [图片] 需求分析并制定方案 这产品和UI都提需求了,咱也不能反驳哈,所以开始调研,分析可行性方案;1、可以添加悬浮按钮。2、自定义导航栏。 添加悬浮按钮,是看起来是比较简单哈,但是感觉不太优雅,会占据页面的空间,体验也不太好。所以想了下第二种方案,自定义导航栏既可以实现产品的需求还可以满足UI的设计美感,在顶部空白处加上返回首页的按钮,这样和返回按钮还对称(最终如图所示,顶部导航栏是个背景图片,分两块组合起来)。 实现方案 一、实现的前提 1、首先查看文档,看文档里关于自定义导航栏是怎么规定的,有哪些限制;还有小程序自定义导航栏全局配置和单页面配置的微信版本和调试库的最低支持版本。 2、在app.json window 增加 navigationStyle:custom ,顶部导航栏就会消失,只保留右上角胶囊状的按钮,如何修改胶囊的颜色呢;胶囊体目前只支持黑色和白色两种颜色 在app.josn window 加上 “navigationBarTextStyle”:“white/black” 3、还要考虑加返回按钮和返回首页的按钮,适配不同的机型 先说下两种配置方法: ①全局配置navigationStyle: 调试基础库>=1.9.0 微信客户端>=6.6.0 app.json [代码]{ "usingComponents": { "navigationBar": "/components/navigationBar/navigationBar" }, "window": { "navigationStyle": "custom" } } [代码] ②单页面配置navigationStyle 调试基础库>=2.4.3 微信客户端版本>=7.0.0 自定义的页面.json [代码]{ "window": { "navigationStyle": "default" } } { "navigationStyle": "custom", "usingComponents": { "navigationBar": "/components/navigationBar/navigationBar" } } [代码] 两者的区别就是,全局配置放在app.json文件里,单页面配置放在自定义页面配置文件里。 二、实现的步骤 以下说下几个要点: 1、自定义导航栏文本,是否显示返回,是否显示返回首页,导航栏高度 2、statusBarHeight,用来获取手机状态栏的高度,这个需要在全局app.js中的onLaunch,调用wx.getSystemInfo获取,navigationBarHeight+默认的高度,这个是设定整个导航栏的高度, 3、还有注意的,在写样式距离和大小时建议都用px,因小程序右边的胶囊也是用的px,不是rpx。 4、因为自定义导航栏每个页面都要写,所以把导航栏封装了公共组件,这样只需要在每个页面引入即可。 如下是封装的导航栏组件: wxml [代码]<view class="navbar" style="{{'height: ' + navigationBarHeight}}"> <view style="{{'height: ' + statusBarHeight}}"></view> <view class='title-container'> <view class='capsule' wx:if="{{ back || home }}"> <view bindtap='back' wx:if="{{back}}"> <image src='/images/back.png'></image> </view> <view bindtap='backHome' wx:if="{{home}}"> <image src='/images/home.png'></image> </view> </view> <view class='title'>{{text}}</view> </view> </view> <view style="{{'height: ' + navigationBarHeight}};background: white;"></view> [代码] 这里有个需注意的问题,就是一般会出现自定义导航栏,下拉页面,导航栏也随着会下拉,这种问题是因为设置fixed后页面元素整体上移了navigationBarHeight,所以在此组件里设置一个空白view元素占用最上面的navigationBarHeight这块高度 wxss [代码].navbar { width: 100%; background-color: #1797eb; position: fixed; top: 0; left: 0; z-index: 999; } .title-container { height: 40px; display: flex; align-items: center; position: relative; } .capsule { margin-left: 10px; height: 30px; background: rgba(255, 255, 255, 0.6); border: 1px solid #fff; border-radius: 16px; display: flex; align-items: center; } .capsule > view { width: 45px; height: 60%; position: relative; .capsule > view:nth-child(2) { border-left: 1px solid #fff; } .capsule image { width: 50%; height: 100%; position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); } .title { color: white; position: absolute; top: 6px; left: 104px; right: 104px; height: 30px; line-height: 30px; font-size: 14px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } [代码] js [代码]const app = getApp() Component({ properties: { text: { type: String, value: 'Wechat' }, back: { type: Boolean, value: false }, home: { type: Boolean, value: false } }, data: { statusBarHeight: app.globalData.statusBarHeight + 'px', navigationBarHeight: (app.globalData.statusBarHeight + 44) + 'px' }, methods: { backHome: function () { let pages = getCurrentPages() wx.navigateBack({ delta: pages.length }) }, back: function () { wx.navigateBack({ delta: 1 }) } } }) [代码] json [代码]{ "component": true, "usingComponents": {} } [代码] 最终还需要考虑下版本兼容的问题,毕竟还有一些用户,微信版本并没有更新到最新版本。 首先可以在app.js里面获取下当前用户的微信版本,做下版本比较,如果小于这个版本,设置个全局变量,也可以在组件写个方法,在不同的页面打开显示不同的顶部导航栏,或者可以控制是否显示导航栏,这里就不详细说了。 亲自试了下,在低于7.0版本的微信中,如果采用单页面自定义导航栏,会出现两个导航栏,这时候通过判断版本号不要再渲染自定义的导航栏组件了,在页面的配置文件里写上title名,还有相应的背景色,这样就会显示自带的导航栏了。 总结 小程序开发是有些坑的地方,从不支持自定义导航栏,到支持全局自定义导航栏,再到现在的支持单页面配置,可以看出在慢慢完善。还有底部tabbar,可自己选择配置的太少了,虽然也支持自定义,但是发现自定义写的底部导航组件体验并不好,每次打开页面都会重新渲染底部的按钮,如果全部写成在一个页面里的tab切换,虽然按钮每次不用重新加载了,但是业务多肯定不行,写到一个单页面里东西也太多了。 希望微信能够多添加或放开一些功能,让开发者更好的服务于产品,给用户更好的体验。
2019-06-22 - 使用wx.setStorage实现小程序类Redis操作,可设置数据有效期
[代码]/**[代码][代码] [代码][代码]* 设置[代码][代码] [代码][代码]* k 键key[代码][代码] [代码][代码]* v 值value[代码][代码] [代码][代码]* t 秒[代码][代码] [代码][代码]*/[代码][代码]function[代码] [代码]putLS(k, v, t) {[代码][代码] [代码][代码]try[代码] [代码]{[代码][代码] [代码][代码]wx.setStorageSync(k, v)[代码][代码] [代码][代码]var[代码] [代码]seconds = parseInt(t)[代码][代码] [代码][代码]if[代码] [代码](seconds > 0) {[代码][代码] [代码][代码]var[代码] [代码]newtime = Date.parse([代码][代码]new[代码] [代码]Date())[代码][代码] [代码][代码]newtime = newtime / 1000 + seconds;[代码][代码] [代码][代码]wx.setStorageSync(k + [代码][代码]'xz'[代码][代码], newtime + [代码][代码]""[代码][代码])[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]wx.removeStorageSync(k + [代码][代码]'xz'[代码][代码])[代码][代码] [代码][代码]}[代码][代码] [代码][代码]} [代码][代码]catch[代码] [代码](e) {[代码][代码] [代码][代码]// console.log(e);[代码][代码] [代码][代码]}[代码][代码]}[代码][代码]/**[代码][代码] [代码][代码]* 获取[代码][代码] [代码][代码]* k 键key[代码][代码] [代码][代码]*/[代码][代码]function[代码] [代码]getLS(k) {[代码][代码] [代码][代码]try[代码] [代码]{[代码][代码] [代码][代码]var[代码] [代码]deadtime = parseInt(wx.getStorageSync(k + [代码][代码]'xz'[代码][代码]))[代码][代码] [代码][代码]if[代码] [代码](deadtime) {[代码][代码] [代码][代码]if[代码] [代码](parseInt(deadtime) < Date.parse([代码][代码]new[代码] [代码]Date()) / 1000) {[代码][代码] [代码][代码]//wx.removeStorageSync(k);[代码][代码] [代码][代码]remLS(k)[代码][代码] [代码][代码]console.log([代码][代码]"过期了"[代码][代码])[代码][代码] [代码][代码]return[代码] [代码]false[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]var[代码] [代码]res = wx.getStorageSync(k)[代码][代码] [代码][代码]if[代码][代码](res){[代码][代码] [代码][代码]return[代码] [代码]res[代码][代码] [代码][代码]}[代码][代码]else[代码][代码]{[代码][代码] [代码][代码]return[代码] [代码]false[代码][代码] [代码][代码]}[代码][代码] [代码][代码]} [代码][代码]catch[代码] [代码](e) {[代码][代码] [代码][代码]// console.log(e);[代码][代码] [代码][代码]}[代码][代码]}[代码][代码] [代码] [代码]/**[代码][代码] [代码][代码]* 删除[代码][代码] [代码][代码]*/[代码][代码]function[代码] [代码]remLS(k) {[代码][代码] [代码][代码]try[代码] [代码]{[代码][代码] [代码][代码]wx.removeStorageSync(k);[代码][代码] [代码][代码]wx.removeStorageSync(k + [代码][代码]'xz'[代码][代码]);[代码][代码] [代码][代码]} [代码][代码]catch[代码] [代码](e) {[代码][代码] [代码][代码]// console.log(e);[代码][代码] [代码][代码]}[代码][代码]}[代码] [代码]/**[代码][代码] [代码][代码]* 清除所有key[代码][代码] [代码][代码]*/[代码][代码]function[代码] [代码]remAllLS() {[代码][代码] [代码][代码]try[代码] [代码]{[代码][代码] [代码][代码]wx.clearStorageSync();[代码][代码] [代码][代码]} [代码][代码]catch[代码] [代码](e) {[代码][代码] [代码][代码]// console.log(e);[代码][代码] [代码][代码]}[代码][代码]}[代码]module.exports = { putLS, getLS, remLS, remAllLS } [代码][代码]
2019-06-22 - 路由的封装
小程序提供了路由功能来实现页面跳转,但是在使用的过程中我们还是发现有些不方便的地方,通过封装,我们可以实现诸如路由管理、简化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