- 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: 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 - 统计多张图片上传到服务器过程中失败的张数,示例代码
// 统计多张图片上传到服务器过程中失败的张数,示例代码 function submit(){ var countPromise = []; var files = [ "http://tmp/wx2020.o6AdxKt6o.0jCN63JzAkZfc367690b907625ee19dde167f9a0c2e5.png", "http://tmp/wx2020.o6AdxKt6o.hcpVIUyYJ8Hn3e05e34de47d5a979b9fbc111f8e9590.png", "http://tmp/wx2020.o6AdxKt6o.uX5umpwlnx4N78f7c7bfbaa55a74f3561922d0d0e358.png", "http://tmp/wx2020.o6AdxKt6o.te0sK16lJEvP526fc649f4de145daa982abfe685f806.png", "http://tmp/wx2020.o6AdxKt6o.te0sK16lJEvP526fc649f4de145daa982abfe685f805.png", ]; // 临时地址 // 将临时地址队列循环上传到服务器 files.forEach((item,index)=>{ var uploadPromise = new Promise((resolve, reject) => { ajaxData(item, index , function(status){ resolve(status ? true : false) }) }) countPromise.push(uploadPromise); Promise.all(countPromise).then(uploadFiles => { console.log("uploadFiles=>",uploadFiles); // [false, true, false, true, false] if(uploadFiles.length == countPromise.length ){ var errNum = 0; uploadFiles.forEach((status) => { if (!status) { errNum++ } }) if(errNum){ console.log("上传失败的图片共有:" + errNum + "张") } } }); }) /** * ajaxData 网络通信请求 **/ function ajaxData(item, index , callback){ // console.log("ajaxData 1 index=>",index); setTimeout(()=>{ console.log("ajaxData 2 index=>",index); if(!!callback){ callback(index % 2 == 1) } }, 1 * 1000); // 零秒时刻 : 1 3 2 // console.log("ajaxData 3 index=>",index); } }
2020-04-23 - 多张图片上传(源码分享+实现分析)
本篇文章以小程序中的代表【微信小程序】为例,分享一下在微信小程序中实现多图上传的源码实现。 代码片段(可导入微信WEB开发者工具体验):https://developers.weixin.qq.com/s/DHrt69mk7af3 两种不同实现方法的优缺点,请查看我的 博客原创文章,在文章中有详细的说明 小程序 多张图片上传 文章地址:https://blog.csdn.net/u013350495/article/details/104326088。 源码: const app = getApp() Page({ data: { // 已选择上传的本地图片地址 urlArr:['helang.jpg','1846492969.jpg','web.jpg'] }, onLoad: function () { }, // 多图上传-回调式 uploadCallback(){ let index = 0; // 当前位置,标识已上传到第几张图片 let newUrls = []; // 上传成功后的图片地址数组 // 图片上传方法 let upload = ()=>{ let nowUrl = this.data.urlArr[index]; //当前待上传的图片地址 wx.showLoading({ title: '正在上传', }); /* 无图片上传接口,收setTimeout 模拟延迟状态 项目中替换为 wx.uploadFile 即可 */ // 假设每 1000ms 上传一张图片 setTimeout(()=>{ // 此处为已上传成功后的回调函数内容 let resUrl = `服务器返回上传后的地址 ${nowUrl}`; //假设这是上传成功后返回的地址 newUrls.push(resUrl); // 将上传后的地址添加到成功数组中 // 判断图片是否已经全部上传完成 if (index >= (this.data.urlArr.length-1)){ send(); }else{ //未全部上传完时标识位置+1并再次调用上传方法 index++; upload(); } },1000); } // 发送方法,用作图片上传完后,得到图片地址提交给其它接口或其它操作 let send = () => { // 关闭加载提示 wx.hideLoading(); wx.showToast({ title: '上传成功', icon:'success' }) // 输出已经上传完的图片地址,请查看控制台结果 console.log(newUrls); } // 调用上传方法 upload(); }, // 多图上传-Promise uploadPromise(){ /* Promise 对象数组 */ let p_arr = []; /* 新建 Promise 方法,nowUrl参数为当前上传的图片地址 */ let new_p = (nowUrl) => { return new Promise((resolve, reject) => { /* 无图片上传接口,收setTimeout 模拟延迟状态 项目中替换为 wx.uploadFile 即可 */ // 假设每 1000ms 上传一张图片 setTimeout(()=>{ // 此处为已上传成功后的回调函数内容 let resUrl = `服务器返回上传后的地址 ${nowUrl}`; //假设这是上传成功后返回的地址 resolve(resUrl); },1000); }) } // 遍历数据,创建相应的 Promise 数组数据 this.data.urlArr.forEach((item, index) => { let nowUrl = this.data.urlArr[index]; //当前待上传的图片地址 p_arr.push(new_p(nowUrl)); }); wx.showLoading({ title: '正在上传', }); /* 所有图片上传完成后调用该方法 */ Promise.all(p_arr).then((res) => { // 关闭加载提示 wx.hideLoading(); wx.showToast({ title: '上传成功', icon: 'success' }) // 输出已经上传完的图片地址,请查看控制台结果 console.log(res); }); } })
2020-02-15 - [有点炫]自定义navigate+分包+自定义tabbar
自定义navigate+分包+自定义tabbar,有需要的可以拿去用用,可能会存在一些问题,根据自己的业务改改吧 大家也可以多多交流 代码片段:在这里 {"version":"1.1.5","update":[{"title":"修复 [复制代码片段提示] 无法使用的问题","date":"2020-06-15 09:20","imgs":[]}]} 更新日志: 2019-11-25 自定义navigate 也可以调用wx.showNavigationBarLoading 和 wx.hideNavigationBarLoading 2019-11-25 页面滚动条显示在自定义navigate 和 自定义tabbar上面的问题(点击“体验custom Tabbar” [图片] [图片] 其他demo: 云开发之微信支付:代码片段
2020-06-15 - 仅需6步7分钟,用原生 js 接入腾讯云即时通信 IM
### 概述 本文用6个步骤,讲解了如何用原生 js 在微信小程序接入腾讯云即时通信 IM,耗时大约7分钟。值得一看,不耽误事。 ### 背景 危机,有危就有机。疫情给全世界带来了很大的危害,也给很多场景和应用带来了前所未有的机会,如在线会议、在线教学、直播带货、全媒体客服等获得了前所未有的使用和增长。实现这些场景推荐使用腾讯云 TRTC,感兴趣的老板可以详细了解下,在此不多赘述。那如何实现会议、教学场景的聊天、文件传输、禁言功能,直播场景的弹幕、送礼、过滤涉黄涉政不雅词功能,全媒体客服的文本、语音、图片咨询功能呢?推荐使用腾讯云即时通信 IM,简单接入、稳定必达、覆盖全球。 “万事开头难”,接入一个 SDK 相当于开头,快速接入成功就开了个好头,心情愉悦干活有劲,后续可以调用相关 API 实现复杂具体的功能。下面就给大家介绍如何快速、心情愉悦地“开头”。 ### 步骤 ##### 1、打开微信开发者工具,新建小程序项目 [图片] ##### 2、命令行打开刚创建的项目文件目录,`npm init -y` 快速生成 package.json ##### 3、下载腾讯云即时通信 IMSDK:`npm install tim-wx-sdk@latest --production` [图片] ##### 4、构建 npm [图片] 构建成功后能在项目目录看到 miniprogram_npm 和 tim-wx-sdk [图片] ##### 5、详情-》本地设置-》使用 npm 模块 [图片] ##### 6、引入 TIM 模块 import TIM from 'tim-wx-sdk' //app.js App({ onLaunch: function () { // 创建 SDK 实例,TIM.create() 方法对于同一个 SDKAppID 只会返回同一份实例 let options = { SDKAppID: 0 // 接入时需要将0替换为您的即时通信应用的 SDKAppID }; let tim = TIM.create(options); // SDK 实例通常用 tim 表示 // 设置 SDK 日志输出级别,详细分级请参见 setLogLevel 接口的说明 tim.setLogLevel(0); // 普通级别,日志量较多,接入时建议使用 }, globalData: { userInfo: null } }) 控制台如果能看到 TIM.VERSION: 2.6.1 的日志,表示接入成功。 [图片] ### 相关文档 更新日志SDK 接口文档AVChatRoom 使用指南Web Demo小程序[图片] iOS(密码 123)[图片]Android [图片]
2020-04-13 - 【优化】利用函数防抖和函数节流提高小程序性能
大家好,上次给大家分享了swiper仿tab的小技巧: https://developers.weixin.qq.com/community/develop/article/doc/000040a5dc4518005d2842fdf51c13 [代码]今天给大家分享两个有用的函数,《函数防抖和函数节流》 函数防抖和函数节流是都优化高频率执行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-02-22 - 【技巧】利用canvas生成朋友圈分享海报
大家好,上次给大家讲了函数防抖和函数节流https://developers.weixin.qq.com/community/develop/doc/0002c892fb80a8326bf70f56d5bc04 今天给大家分享一下利用canvas生成朋友圈分享海报 由于小程序的限制,我们不能很方便地在微信内直接分享小程序到朋友圈,所以普遍的做法是生成一张带有小程序分享码的分享海报,再将海报保存到手机相册,有两种方法可以生成分享海报,第一种是让后台生成然后返回图片链接,这一种方法比较简单,只需要传后台所需要的参数就行了,今天给大家介绍的是第二种方法,用canvas生成分享海报。 首先先来看下效果: [图片] 主要步骤: 1. 把海报样式用标签先写好,方便画图时可以比对 2. 用canvas进行画图,canvas要注意定好宽高 3. canvas利用wx.canvasToTempFilePath这个api将canvas转化为图片 4. 将转化好的图片链接放入image标签里 5. 再利用wx.saveImageToPhotosAlbum保存图片 这里有几个坑点需要注意下: 1. 用canvas进行画图的时候要注意画出来的图的大小一定要是你用标签写好那个样式的两倍大小,比如你的海报大小是400*600的大小,那你用canvas画的时候大小就要是800*1200,宽高可以写在样式里,如果你画出来的图跟你海报图是一样的大小的话生成的图片是会很模糊的,所以才需要放大两倍。 2. 画图的时候要注意尺寸的转化,如果你是用rpx做单位的话,就要对单位进行转化,因为canvas提供的方法都是经px为单位的,所以这一点要注意一下,px转rpx的公式是w/750*z*2,w是手机屏幕宽度screenWidth,可以通过wx.getSystemInfo获取,z是你需要画图的单位,2就是乘以两倍大小。 3. 图片来源问题,因为canvas不支持网络图片画图,所以你的图片要么是固定的,如果不是固定的,那就要用wx.downloadFile下载后得到一个临时路径才行 4. 小程序码问题,小程序需要后台请求接口后返回一个二进制的图片,因为二进制图片canvas也是不支持的,所以也是要用wx.downloadFile下载后得到一个临时路径,或者可以叫后台直接返回一个小程序码的路径给你 5. 这里保存的时候是有个授权提醒的,如果拒绝的话再次点击就没有反应了,所以这里我做了一个判断是否有授权的,如果没有就弹窗提醒,确认的话会打开设置页面,确认授权后再次返回就行了,这里有个坑注意下,就是之前拒绝后再进入设置页面确认授权返回页面时保存图片会不成功,官方还没解决,我是加了个setTimeOut处理的,详情可以看这里https://developers.weixin.qq.com/community/develop/doc/000c46600780f0fa68d7eac345a400 代码实现: [图片] 这里图片我先用的是网上的链接,实际项目中是后台返回的数据,这个可以自行处理,这里只是为了演示方便,生成临时路径的方法我这里是分别定义了一个方法,其实可以合成一个方法的,只是生成小程序码时如果要传入参数要注意一下。 绘图方法是drawImg,这里截一部分,详细的可以看代码片段 [图片] 不足: 由于在实际项目中返回的图片宽高是不固定的,但是canvas画出来的又需要固定宽高,所以分享图会有图片变形的问题,使用drawImage里的参数也不能解决,如果各位有比较好的方案可以一起讨论一下。 代码片段: https://developers.weixin.qq.com/s/3pcsjDmS7M5Y 系甘先,得闲饮茶
2019-01-23 - 小程序请求数据双向混合加密和防篡改+防重放攻击的实现
前言 大家好,借着中秋放假明日又要上班的这个晚上,平常又没空,趁这个时间点就决定来一篇。 [图片] 我们都知道微信小程序的服务端API上,官方可谓是做足了心思,对用户的数据进行加密,虽然对我们开发者来说似乎是一种麻烦,但是从长远角度来看,是十分有必要的,用户隐私高于一切。 那么在小程序的开发过程中与后端对接接口时是否有想过这样的问题呢? HTTPS真的百分百安全吗? 数据被成功抓包后安全吗? 数据被篡改后还有效吗? 请求被重放后安全吗? 世界上没有绝对安全的系统,但我们可以让它被破解的成本变高。 本篇文章专业性并不高,如果存在错误请大家为我指出来,以免误导别人,谢谢! 以下我们将小程序端称为C端,服务端称为S端,服务端代码是Node.js,仅供参考,但原理都一样,后端可以是其它语言。 思考 围绕着以上的问题,探究一下问题的答案? 本部分只对问题做思考,具体实现请参考下面的实现部分。 HTTPS真的百分百安全吗?只能说是相对安全,当然,在微信小程序的沙箱环境里,HTTPS通信会更加安全,否则官方可能会要求我们对请求加密了对吧。但百密一疏,C端是否存在漏洞,假设C端安全了难道S端就安全嘛?细思恐极,退一万步讲,百分百安全是不存在的,由于篇幅问题相关漏洞大家可以搜索探究。 假设数据被中间人成功抓包,如果数据是明文传输,那么将导致数据泄露,因此对数据进行加密是必要的,但应该如何加密呢?如何做到密钥安全?C端和S端如何进行数据的加解密?RSA和AES加密应该使用哪种加密明文?如何充分发挥RSA和AES两个加密算法的特长? 假设数据被篡改,如果因为请求数据被篡改而导致严重后果,那么很大程度上其实是代码设计有问题,正常设计中安全应被摆在重要的位置,至少不应该出现购买某样商品时是通过C端发送商品价格给S端调起支付那样(只需篡改商品价格即可支付极少的钱购买商品),如果代码设计并不存在严重问题,那么数据被篡改也是不可忽视的,我们需要进行数据签名,让不同的数据拥有唯一的MD5哈希值,如果数据被篡改,通过哈希值即可判断数据是否被篡改,当然也可能会有人问,既然数据被篡改,那攻击者会不会重新生成一个哈希值代替?是的。但是我们有接下来会说到的key,key会作为签名的数据变量之一,由于攻击者并不知道key值因此无法重新签名,key值建议不是固定值而是周期性更换的随机值,例如随着用户的登录态而产生并随之抹除。 假设请求被重放,大部分时候由于数据被加密和防篡改处理过,攻击者并无法直接获得数据或篡改,但如果通过劫持到的登录认证请求的原始数据并重新发起该请求,则攻击者将可能获得重要的认证数据,使得系统将攻击者作为正常用户处理,后续的请求攻击者仍能伪装成正常的用户进行后续攻击。 期望效果 在开始实现之前先看一下完成后的效果,以下是截取了Network中其中一个GET请求发送的数据: [图片] 实际上发送的数据是如下图所示: [图片] 然后是该请求返回的数据: [图片] 实际上返回的数据是如下图所示: [图片] 整个流程: [图片] 实现 由于整个流程在C端的实现上顺序反过来的,因此下面的步骤也将是反向而行。 工具库下载 工欲善其事,必先利其器!这三个库是经过修改压缩的,支持在小程序上使用并且体积可观(总共69.1KB,如果不涉及密码哈希处理只需要前两个库,体积只有65.3KB),接下来的实现操作将会使用到,建议大家可以根据实际情况对功能进行二次封装: CryptoJS.js:点击下载 RSA.js:点击下载 SHA256.js(可选):点击下载 在线的各类加解密工具(可选,可以收藏起来,平时测试挺有用):点击访问 请求数据防重放 要防范请求重放攻击,首先需要了解Unix时间戳 timestamp概念,和时间戳不一样的是它的单位是秒,事实上这个需求也只需要秒级即可。除此之外还将用到另一个值:nonce,它是一个随机产生并只能被使用一次的值,长度自定,请求越频繁长度需要越长(降低同一时间产生相同nonce的几率),C端发送请求时需要将timestamp和生成的nonce加入发送的参数中。那么,如何将两者结合呢?我这为防重放的目标下了一个简单的定义。 同样的请求只能发生一次,且请求必须在规定时间内发出。 何为同样的请求?不是指两次发送的参数一致就是一样的,而是连timestamp和nonce也一样才算是同样的请求。 那么S端如何确认其是同样的请求呢? S端每次接收到一个请求,都会将该请求的nonce存入缓存并保持60秒(这个阈值不一定是60秒,可以根据实际需要定义),时间过后该值将被移除,建议S端采用Redis存储nonce,这样可省去检测和移除nonce的代码。如果S端发现当前请求的nonce存在于已存储的nonce之中,则此请求发生重复,那么timestamp有何用? 如果只使用nonce我们只能保证该请求60秒内不会重复,但60秒后依然任人宰割,这不是要的结果。所以timestamp将用来限制时间,S端时间戳减去C端发送请求的时间戳,得到的差值为N秒,如果N秒大于60秒则此请求过期,那么则可以保证,60秒内因为nonce相同而被判为请求重放,60秒后因为时间差超过而被判为请求已过期,因此确保了请求不会被重放。。 以下展示三种情况: [代码]C端时间戳:1568487720 //2019/9/15 03:02:00 C端NONCE:5rKbMs2Fm3 C端发送请求 -> S端接收请求 S端时间戳:1568487722 //2019/9/15 03:02:02 CS端时间差:1568487722 - 1568487720 = 2 C端NONCE是否存在于缓存:false 【重放校验通过】 [代码] [代码]C端时间戳:1568487722 //2019/9/15 03:05:22 C端NONCE:IzFEs52bAC C端发送请求 -> S端接收请求 S端时间戳:1568487922 //2019/9/15 03:02:00 CS端时间差:1568487922 - 1568487722 = 200 C端NONCE是否存在于缓存:false 【重放校验不通过,请求已过期,因为时间差超过60秒】 [代码] [代码]C端时间戳:1568487720 //2019/9/15 03:02:00 C端NONCE:IxwPHQU0nA C端发送请求 -> S端接收请求 S端时间戳:1568487722 //2019/9/15 03:02:02 CS端时间差:1568487722 - 1568487720 = 2 C端NONCE是否存在于缓存:true 【重放校验不通过,此请求为重放请求,因为nonce已经存在,此请求已经完成,不可重复】 [代码] timestamp和nonce将作为参数参与下面部分的签名。 请求数据防篡改 C端数据签名 首先通过对参数按照参数名进行字典排序(调过一些第三方API的朋友应该明白),假设当前需要传输的参数如下: [代码]{ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3" } [代码] 进行字典排序,参数名顺序应为: [代码]const keys = Object.keys(data); //获得参数名数组 keys.sort(); //字典排序 console.log(key); //["a", "b", "c", "nonce", "timestamp"]; [代码] 参数字典排序后应和参数一起拼接为字符串,至于使用什么拼接符就要与S端商量了,如果参数值是一个数组或一个对象(如c为[1,2,3])那么可以将数据值转为JSON字符串再拼接。以上参数拼接后字符串如下: [代码]a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720 [代码] 下一步是计算拼接字符串的MD5哈希值了嘛? 不是。因为这样拼接的字符串很容易被攻击者伪造签名并篡改数据,这样就失去了签名的意义了。也就是说缺了一个key值。key值又从何而来?上面思考部分有提到建议从登录后发放,并且这个key与该用户的登录态绑定,登录态有效期间,将使用这个key进行请求的签名与验签,至于如何鉴别用户,我们会在最终发送给S端的参数加入一个sessionId作为登录态唯一标识,这里不加是因为这部分数据是需要参与后续的加密的,而sessionId不参与加密。 但是可能又会有一个问题,登录前没有key怎么实现的登录请求?事实上,登录请求并不怕篡改,因为攻击者自己也不知道账号密码,所以无需提供key用于登录请求的签名。 提到登录密码这个需要注意一点,密码不能明文传输,请计算哈希值后传输,S端比对账户密码哈希值即可确认是否正确,同样S端非特殊情况也不能明文存储密码,建议SHA-1或更高级的SHA-256计算后的值,MD5值可能被使用彩虹表(一种为各种常见密码建立的MD5映射表)破解。SHA256计算的库已在上面工具库下载提供。 拼接上我们登录时随机生成的key: [代码]a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720&key=gUelv79KTcFaCkVB [代码] 接下来计算32位MD5哈希值 为什么上面说MD5会被破解而这里却用MD5计算?因为此处计算MD5的目的并不是为了隐藏明文数据,而只是用于数据校验 此处引入了CryptoJS.js [代码]const CryptoJS = require('./CryptoJS'); const signStr = "a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720&key=gUelv79KTcFaCkVB"; const sign = CryptoJS.MD5(signStr).toString(); //a42af0962de99e698d27030c5c9d3b0e [代码] 这么一来我们的数据签名阶段就完成了,然后需要把签名加入参数之中,将和参数一起传输,需要注意的是传输参数无需在意参数名排序。以下是当前的参数处理结果: [代码]{ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" } [代码] 其实从上面这两部分内容来看,会发现防重放和防篡改是相辅相成的,就像两兄弟一样,少了谁都干不好这件事。 S端验证签名 既然有签名那必定也有验签,验签流程其实就是重复C端的前面流程并比对CS两端得出的签名值是否一致。S端取得请求数据后(假设数据未加密,暂时不讨论解密),将除了sign之外的参数名进行字典排序,sign用一个临时变量存下,然后排好序的参数和C端一样拼接得到字符串。 接下来根据上面部分提到的未加密参数sessionId获得用户登录态并获取到该状态的临时key拼接到字符串末尾,接下来进行MD5计算即可得到S端方获得的签名,此时与请求中携带的sign比较是否一致则可确定签名是否有效,如果不一致返回签名错误。具体流程请参考以下: [代码]C端发送请求 -> S端接收请求 //请求参数为data const _sign = data.sign; //排序并拼接除sign外的参数 let signStr = a=789&b=456&c=123&nonce=5rKbMs2Fm3×tamp=1568487720 const sessionId = ...; //用户的sessionId const sessionData = getUserSessionData(sessionId);//根据sessionId查询用户登录态sessionData并取出key,并拼接到字符串尾部 const key = sessionData.key signStr += key; //MD5计算并比对,代码仅供参考 const sign = crypto.md5(signStr); if(sign !== _sign) { //签名错误! } [代码] 请求数据加解密 有了上面部分的参数处理铺垫后,接下来就该开始本文章最核心的加解密,在这之前我们先了解AES和RSA两种加密算法。了解概念后我们再来思考一下两者的一些特性。 AES加解密需要密钥,除某些模式外还需要提供初始化向量,可使用密钥解出明文,是对称加密算法。 RSA加解密需要一对密钥,分别为公钥和私钥,公钥加密,私钥解密,是非对称加密算法。 两者在加密长文本性能上AES占优势。 根据这些让我们发现,他们可以形成互补关系,RSA加解密安全性高但长文本处理性能不及AES。AES加解密长文本性能优于RSA但需要明文密钥和向量加解密,密钥的安全性成问题。 那么在C端生成随机的AES密钥和向量,使用密钥和向量使用aes-128-cbc加密模式(也可以根据实际需要采用其它的模式)加密真正需要传输的参数(参数则是经过防重放+防篡改处理的参数)得到encryptedData,然后将该密钥使用RSA公钥加密得到encryptedKey,下面我顺便把AES加密的向量也一起加密了得到encryptedIV,这样就完美的互补了对方的缺点,既能够较快的完成数据加密又能保证密钥安全性,两全其美。 整个个加解密流程如下图所示: [图片] 下面的四个小章节将逐一描述流程实现: C端加密数据 C端加密后的数据应如下(并非固定格式,根据自己需要定制): [代码]{ "sessionId": "xxxxxxxx", "encryptedData": "xxxxxx", "encryptedKey": "xxxxxx", "encryptedIV": "xxxxxxx" } [代码] 但RSA公钥又是怎么发放到C端的呢?答案是在登录认证的时候服务器下发的,登录成功时服务器会创建RSA密钥对并把公钥发放给C端,私钥存在服务器上该用户的登录态数据中。流程如下所示: [代码]C端发起登录请求 -> S端接收登录请求 S端登录认证是否通过 true S端生成RSA密钥对 - publicKey , privateKey S端查询相关用户信息 S端生成登录态信息 -> 向登录态信息存入privateKey私钥,登录态信息类似如下 "xxxxxx": { id: "xxxxx", authData: { key: "xxxxxx", privateKey: "xxxxxx" } } S端返回登录态唯一标识sessionId和publicKey公钥以及相关用户信息 -> C端 C端存储登录态信息于本地,后续请求将使用服务器提供的公钥进行加密 [代码] 其中privateKey就是该用户当前登录态所使用的解密私钥,C端通过公钥加密后的AES密钥数据只能用该私钥解密。 如果希望登录阶段的请求也加密,那么可以手动生成一个RSA密钥对,然后客户端放置一个固定的公钥,服务器也使用一个固定的私钥进行登录阶段的加解密。 具体实现如下: 此处引入了CryptoJS.js和RSA.js [代码]const CryptoJS = require('./CryptoJS'); const RSA = require('./RSA.js'); //假设当前已登录成功并获得S端下发的RSA公钥且已存入本地存储 //createRandomStr为生成随机大小写英文和数字的字符串 const aesKey = createRandomStr(16); //生成AES128位密钥 16字节=128位 const aesIV = createRandomStr(16); //生成初始化向量IV const raw = JSON.stringfly({ "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" }); const encryptedData = CryptoJS.AES.encrypt(data: raw, key: aesKey, { iv: aesIV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); //使用CBC模式和Pkcs7填充加密 const authData = wx.getStorageSync("authData"); //读取本地存的RSA公钥 RSA.setPublicKey(authData.publicKey); //设置RSA公钥 const encryptedKey = RSA.encrypt(aesKey); //RSA加密AES加密密钥 const encryptedIV = RSA.encrypt(aesIV); //RSA加密AES加密初始化向量,是否加密向量可由自己决定 //最后的处理结果 const result = { sessionId: authData.sessionId, encryptedData, encryptedKey, encryptedIV }; [代码] S端解密数据 [代码]const crypto = require('crypto'); const cryptojs = require('crypto-js'); const { sessionId, encryptedData, encryptedKey, encryptedIV } = requestData; const authData = getUserSessionData(sessionId); //获取用户登录态数据 const privateKey = authData.privateKey; const aesKey = crypto.privateDecrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, encryptedKey); //使用私钥解密得到AES密钥 const aesIV = crypto.privateDecrypt({ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, encryptedIV); //使用私钥解密得到AES iv向量 const key = cryptojs.enc.Base64.parse(aesKey); const iv = cryptojs.enc.Utf8.parse(aesIV); const decryptedData = cryptojs.AES.decrypt(encryptedData, key, { iv, mode: cryptojs.mode.CBC, padding: cryptojs.pad.Pkcs7 }); //采用与加密统一的模式和填充进行解密 const { "c": 123, "b": 456, "a": 789, "timestamp": 1568487720, "nonce": "5rKbMs2Fm3", "sign": "a42af0962de99e698d27030c5c9d3b0e" } = decryptedData; //至此解密得到C端传来的数据 [代码] S端加密数据 当S端处理完C端的请求后应加密响应数据,那么加密响应数据应该使用什么密钥呢?既然C端已经将加密的密钥发送过来了,那么干脆将C端使用的AES密钥拿来加密响应数据就可以了。加密的数据传回C端后,C端只需使用该请求所使用的AES加密密钥进行解密即可。响应的加密数据如下: [代码]const cryptojs = require('crypto-js'); const responseData = JSON.stringify(...); //此为S端需要返回给C端的数据 const aesKey = ...; //此为之前C端用来加密数据的AES密钥 const aesIV = createRandomStr(16); //生成初始化向量IV const encryptedData = cryptojs.AES.encrypt(data, aesKey, { iv: aesIV, mode: cryptojs.mode.CBC, padding: cryptojs.pad.Pkcs7 }); //加密响应数据 const encryptedResponse = { "encryptedData": encryptedData, "iv": aesIV }; //得到加密后的响应数据并返回给C端 [代码] C端解密数据 C端接收到S端的响应数据后应对加密的数据进行解密,此次解密就是单纯的AES解密了,使用发起请求时用于加密数据的AES密钥配合响应数据的iv向量对encryptedData进行解密,得到解密后的数据即为S端真正的响应数据。实现过程如下: 此处引入了CryptoJS.js [代码]const CryptoJS = require('./CryptoJS'); const key = ...; //之前用于加密请求参数的AES加密密钥 const { encryptedData, iv } = responseData; const aesIV = CryptoJS.enc.Utf8.parse(iv); const aesKey = CryptoJS.enc.Utf8.parse(key); const decryptedData = CryptoJS.AES.decrypt(encryptedData, aesKey, { iv: aesIV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); //AES解密响应数据 const { ... } = decryptedData; //得到解密后的响应数据 [代码] 结语 第一次写这么长的文章,可能存在大量纰漏,如果大佬发现问题欢迎指出(`・ω・´)我会马上修改。 也许小程序的运行环境会比我想象中的更安全 也许HTTPS也会比我想象中的更安全 也许Web服务器引擎也比我想象中的更安全 但,安全不总是先行一步的吗?
2019-10-24 - 微信小程序之swiper轮播图片高度自适应
微信小程序中使用swiper组件可以实现图片轮播效果,但是默认swiper高度是固定的150px,如果项目中图片大于固定高度就会被隐藏,所以本篇文章要实现轮播图片的高度自适应。 效果图: [图片] wxml代码: [代码]<[代码][代码]swiper[代码] [代码]class[代码][代码]=[代码][代码]"t-swiper"[代码] [代码]indicator-dots[代码][代码]=[代码][代码]"{{indicatordots}}"[代码] [代码]indicator-active-color[代码][代码]=[代码][代码]"{{color}}"[代码] [代码]autoplay[代码][代码]=[代码][代码]"{{autoplay}}"[代码] [代码]interval[代码][代码]=[代码][代码]"{{interval}}"[代码] [代码]duration[代码][代码]=[代码][代码]"{{duration}}"[代码] [代码]style[代码][代码]=[代码][代码]"height:{{height}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{img}}"[代码] [代码]wx:key[代码][代码]=[代码][代码]""[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]swiper-item[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]image[代码] [代码]src[代码][代码]=[代码][代码]"{{item}}"[代码] [代码]mode[代码][代码]=[代码][代码]"widthFix"[代码] [代码]bindload[代码][代码]=[代码][代码]'goheight'[代码] [代码]/>[代码][代码] [代码][代码]</[代码][代码]swiper-item[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]block[代码][代码]>[代码][代码]</[代码][代码]swiper[代码][代码]>[代码]wxss代码: [代码].t-swiper image {[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]100%[代码][代码];[代码][代码]}[代码]js代码: [代码]Page({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]img: [[代码][代码] [代码][代码]'img/1.jpg'[代码][代码],[代码][代码] [代码][代码]'img/2.jpg'[代码][代码],[代码][代码] [代码][代码]'img/3.jpg'[代码][代码] [代码][代码]],[代码][代码] [代码][代码]indicatordots: [代码][代码]true[代码][代码],[代码][代码] [代码][代码]//是否显示面板指示点[代码][代码] [代码][代码]autoplay: [代码][代码]true[代码][代码],[代码][代码] [代码][代码]//是否自动切换[代码][代码] [代码][代码]interval: 5000,[代码][代码] [代码][代码]//自动切换时间间隔[代码][代码] [代码][代码]duration: 500,[代码][代码] [代码][代码]//滑动动画时长[代码][代码] [代码][代码]color: [代码][代码]'#ffffff'[代码][代码],[代码][代码] [代码][代码]//当前选中的指示点颜色[代码][代码] [代码][代码]height: [代码][代码]''[代码][代码] [代码][代码]//swiper高度[代码][代码] [代码][代码]},[代码][代码] [代码][代码]goheight: [代码][代码]function[代码] [代码](e) {[代码][代码] [代码][代码]var[代码] [代码]width = wx.getSystemInfoSync().windowWidth[代码][代码] [代码][代码]//获取可使用窗口宽度[代码][代码] [代码][代码]var[代码] [代码]imgheight = e.detail.height[代码][代码] [代码][代码]//获取图片实际高度[代码][代码] [代码][代码]var[代码] [代码]imgwidth = e.detail.width[代码][代码] [代码][代码]//获取图片实际宽度[代码][代码] [代码][代码]var[代码] [代码]height = width * imgheight / imgwidth + [代码][代码]"px"[代码][代码] [代码][代码]//计算等比swiper高度[代码][代码] [代码][代码]this[代码][代码].setData({[代码][代码] [代码][代码]height: height[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]})[代码]
2019-07-14 - 帮你查天气——基于百度地图天气API
先上截图: [图片] 小程序二维码: [图片] 一、申请百度地图的AK 在百度地图开放平台上注册账号并登录,然后在“控制台”中创建一个应用。 [图片] 选择“微信小程序”,并填入创建小程序时得到的APP ID。 [图片] 提交后就可以在前一个页面中看到新创建的应用的AK了。 二、下载SDK 在“开发文档”中点击“微信小程序JavaScript API” [图片] 在“相关下载”中点击“全部下载”。 [图片] 压缩包里主要包括两个文件夹:[代码]demo[代码]和[代码]src[代码],开发时主要用到[代码]src[代码]里的js文件:[代码]bmap-wx.js[代码](为讲解方便,本文不使用[代码].min[代码])。 三、配置服务器域名 登录微信小程序管理后台,进入开发——开发设置 [图片] 在服务器域名中,填入request合法域名:https://api.map.baidu.com [图片] PS:微信要求域名都为https,所以有些API为http的就不能用了。 四、写代码 打开微信开发者工具,并新建一个小程序项目,会自动生成一些文件(新建项目的方法、各文件的作用,网上有很多教程,不再赘述)。 1、打开[代码]index.js[代码]文件,添加对[代码]bmap-wx.js[代码]文件的引用: [代码]//index.js //获取应用实例 const app = getApp() //调用百度地图天气API的js文件 var bmap = require('../../utils/bmap-wx.js'); [代码] 2、在[代码]onLoad[代码]方法中,新建一个[代码]BMapWX[代码]对象,并填入AK: [代码]var BMap = new bmap.BMapWX({ ak: '你申请的百度地图AK' }); [代码] 3、在[代码]onLoad[代码]方法中,发起查询天气的请求: [代码]BMap.weather({ fail: fail, success: success }); [代码] 4、在[代码]onLoad[代码]方法中,定义查询成功和失败的方法: [代码]var fail = function(data) { console.log('查询失败') }; var success = function(data) { console.log('查询成功'); var currentWeather = data; this.setData({ currentWeather: currentWeather }); } [代码] 注意,[代码]setData[代码]中的[代码]currentWeather[代码]要与[代码]Page[代码]中的[代码]data[代码]部分对应: [代码]data: { currentWeather: '' }, [代码] 至此,就可以获得返回的天气数据了,剩下的工作就是解析返回的数据。 五、解析数据 在第三步中,查询成功获得的返回数据[代码]data[代码]包括了我们需要展示的信息,因此解析的工作也主要是针对[代码]data[代码]。 data主要包括两部分内容:currentWeather和originalData [图片] 1、解析data.currentWeather 结构如下: [图片] 解析方式如下: [代码]var currentWeather = data.currentWeather[0]; //currentWeather.currentCity:"济南市" //currentWeather.date:"周四 01月17日 (实时:3℃)" //currentWeather.pm25:"85" //currentWeather.temperature:"7 ~ -2℃" //currentWeather.weatherDesc:"晴" //currentWeather.wind:"南风微风" [代码] 注意,data.currentWeather是一个JSON数组,关键的数据都存在data.currentWeather[0]中 2、解析data.originalData 结构如下: [图片] data.originalData也是JSON格式,我们关心的数据存在data.originalData.results数组中,结构如下: [图片] 可以看出,data.originalData.results[0]数组中关键的部分也是两块:index数组和weather_data数组: [图片] data.originalData.results[0].index[0]是穿衣的相关信息 data.originalData.results[0].index[1]是洗车的相关信息 data.originalData.results[0].index[2]是感冒的相关信息 data.originalData.results[0].index[3]是运动的相关信息 data.originalData.results[0].index[4]是紫外线强度的相关信息 [图片] data.originalData.results[0].weather_data[0]是今天的天气情况 data.originalData.results[0].weather_data[1]是明天的天气情况 data.originalData.results[0].weather_data[2]是后天的天气情况 data.originalData.results[0].weather_data[3]是大后天的天气情况 至此,就完成了数据解析工作,通过setData可以将数据与index.wxml中的变量绑定。修改success如下: [代码]var success = function(data) { console.log('查询成功'); //实时天气 var currentWeather = data.currentWeather[0]; //感冒信息 var flu = data.originalData.results[0].index[2]; //未来三天的天气 var forecast = new Array(3); for (var i = 0; i < 3; i++) { forecast[i] = data.originalData.results[0].weather_data[i + 1]; } //配置数据 this.setData({ currentWeather: currentWeather, flu: flu, forecast: forecast }); } [代码] 在index.wxml文件中调用: [代码]<!--当前定位的城市--> <view class='cityName'>{{currentWeather.currentCity}}</view> <!--未来3天的天气情况,包括日期、天气描述、温度范围、风力--> <view class='forecast'> <view class='next-day' wx:key="{{index}}" wx:for="{{forecast}}"> <view class='detail date'>{{item.date}}</view> <view class='detail'>{{item.weather}}</view> <view class='detail'>{{item.temperature}}</view> <view class='detail'>{{item.wind}}</view> </view> </view> <!--感冒信息--> <view class='tips'> <view class='index'>感冒指数:{{flu.zs}}</view> <view class='description'>{{flu.des}}</view> </view> [代码] 六、bmap-wx.js简单介绍 打开bmap-wx.js文件,可以看到只有一个类BMapWX。该类包含了四个方法,而我们需要用到的是weather(param)方法: [代码]/** * 天气检索 * * @param {Object} param 检索配置 */ weather(param) { var that = this; param = param || {}; let weatherparam = { coord_type: param["coord_type"] || 'gcj02', output: param["output"] || 'json', ak: that.ak, sn: param["sn"] || '', timestamp: param["timestamp"] || '' }; let otherparam = { success: param["success"] || function () {}, fail: param["fail"] || function () {} }; let type = 'gcj02'; let locationsuccess = function (result) { weatherparam["location"] = result["longitude"] + ',' + result["latitude"]; wx.request({ url: 'https://api.map.baidu.com/telematics/v3/weather', data: weatherparam, header: {"content-type": "application/json"}, method: 'GET', success(data) { let res = data["data"]; if (res["error"] === 0 && res["status"] === 'success') { let weatherArr = res["results"]; // outputRes 包含两个对象, // originalData为百度接口返回的原始数据 // wxMarkerData为小程序规范的marker格式 let outputRes = {}; outputRes["originalData"] = res; outputRes["currentWeather"] = []; outputRes["currentWeather"][0] = { currentCity: weatherArr[0]["currentCity"], pm25: weatherArr[0]["pm25"], date: weatherArr[0]["weather_data"][0]["date"], temperature: weatherArr[0]["weather_data"][0]["temperature"], weatherDesc: weatherArr[0]["weather_data"][0]["weather"], wind: weatherArr[0]["weather_data"][0]["wind"] }; otherparam.success(outputRes); } else { otherparam.fail({ errMsg: res["message"], statusCode: res["status"] }); } }, fail(data) { otherparam.fail(data); } }); } let locationfail = function (result) { otherparam.fail(result); } let locationcomplete = function (result) { } if (!param["location"]) { that.getWXLocation(type, locationsuccess, locationfail, locationcomplete); } else { let longitude = param.location.split(',')[0]; let latitude = param.location.split(',')[1]; let errMsg = 'input location'; let res = { errMsg: errMsg, latitude: latitude, longitude: longitude }; locationsuccess(res); } } [代码] 可以看出,成功返回的数据为data,通过解析,封装成了我们在前面得到的data.currentWeather和data.originalData两个数组。 另外,程序中默认查询的城市是当前定位的城市,依据的参数是经纬度坐标。 [代码]weatherparam["location"] = result["longitude"] + ',' + result["latitude"]; [代码] 如果想查询其它城市,例如北京,可以修改为: [代码]weatherparam["location"] = param["北京"]; [代码] 其中param就是weather()函数的参数。
2019-02-22 - 七牛传图,上传图片至七牛云
七牛传图插件说明文档 GitHub项目地址:https://github.com/oubingbing/qiniu-upload 1.目前七牛传图插件信息 AppId:wx00caa212d6710dcb 版本号:1.3.0 2.具体使用方法 2.1 配置app.json和使用页面的***.json [代码]//app.json[代码] [代码]{[代码] [代码] [代码][代码]"pages"[代码][代码]: [[代码] [代码] [代码][代码]"pages/index/index"[代码] [代码] [代码][代码]],[代码] [代码] [代码][代码]"plugins"[代码][代码]: { [代码][代码]//配置插件[代码] [代码] [代码][代码]"myPlugin"[代码][代码]: {[代码] [代码] [代码][代码]"version"[代码][代码]: [代码][代码]"1.0.0"[代码][代码],[代码] [代码] [代码][代码]"provider"[代码][代码]: [代码][代码]"wx00caa212d6710dcb"[代码] [代码]//appId[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码]}[代码] [代码]***.json[代码] [代码]{[代码] [代码] [代码][代码]"usingComponents"[代码][代码]: {[代码] [代码] [代码][代码]"upload"[代码][代码]: [代码][代码]"plugin://myPlugin/upload"[代码] [代码]//引入插件[代码] [代码] [代码][代码]}[代码] [代码]}[代码] 2.2 在页面的WXML文件中添加标签 [代码]<[代码][代码]upload[代码] [代码]icon-info[代码][代码]=[代码][代码]"{{icon}}"[代码] [代码]//上传图标信息,包括图标的宽高[代码] [代码] [代码][代码]qiniu-info[代码][代码]=[代码][代码]"{{qiniu}}"[代码] [代码]//七牛信息,包括七牛授权token、七牛存储区域、七牛域名以及可以一次性选择图片的最大数量[代码] [代码] [代码][代码]bind:success[代码][代码]=[代码][代码]"uploadSuccess"[代码] [代码]//上传成功回调 [代码] [代码] [代码][代码]bind:delete[代码][代码]=[代码][代码]"deleteSuccess"[代码] [代码]//删除成功回调[代码] [代码] [代码][代码]bind:error[代码][代码]=[代码][代码]"uploadError"[代码][代码]/> //上传错误回调[代码] 2.3 JS文件中处理传入插件数据 [代码]var[代码] [代码]plugin = requirePlugin([代码][代码]"myPlugin"[代码][代码])[代码] [代码]const token = [代码][代码]'dJVFK8ibSzHKWhcVOupqzn22EKJ9QXqjLIqqDsqn:FHJcjNuTs5HtSbsUQveZU3vLGcw=:eyJzY29wZSI6InNjaG9vbCIsImRlYWRsaW5lIjoxNTM3ODczODE0fQ=='[代码][代码];[代码] [代码]Page({[代码] [代码] [代码][代码]data:{[代码] [代码] [代码][代码]test:[代码][代码]"plugin"[代码][代码],[代码] [代码] [代码][代码]icon:{[代码] [代码] [代码][代码]width[代码][代码]:[代码][代码]"130rpx"[代码][代码],[代码][代码]//图片和图标的宽[代码] [代码] [代码][代码]height[代码][代码]: [代码][代码]"130rpx",[代码][代码]//图片和图标的高[代码] [代码] [代码] [代码][代码] path:"",//选择图片icon的URL地址,必须是网络地址,不能是本地地址,如果不填则选默认图标, showImage:true//是否显示上传后的图片 [代码] [代码][代码]},[代码] [代码] [代码][代码]qiniu:{[代码] [代码] [代码][代码]uploadNumber:4,[代码][代码]//一次性选择图片的最大限制[代码] [代码] [代码][代码]region: [代码][代码]"SCN"[代码][代码],[代码][代码]//ECN, SCN, NCN, NA,您的七牛存储区域[代码] [代码] [代码][代码]token: token,[代码][代码]//七牛上传token凭证,需您在服务器获取,然后自行维护这个token的有效期[代码] [代码] [代码][代码]domain: [代码][代码]"http://image.kucaroom.com",[代码][代码]//七牛域名[代码] returnAllImage:true,//是否返回上传图片的数组,false上传一个返回一个 [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码] [代码] [代码][代码]onLoad: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]},[代码] [代码] [代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 上传成功后的回调,返回已上传的图片数组[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]uploadSuccess:[代码][代码]function[代码][代码](uploadImage){[代码] [代码] [代码][代码]console.log([代码][代码]"外部获取到上传照片:"[代码] [代码]+ JSON.stringify(uploadImage.detail));[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 删除图片的回调,返回已上传的图片数组[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]deleteSuccess: [代码][代码]function[代码] [代码](uploadImage) {[代码] [代码] [代码][代码]console.log([代码][代码]"外部获取到删除后的照片:"[代码] [代码]+ JSON.stringify(uploadImage.detail));[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 上传图片出错的回调[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]uploadError:[代码][代码]function[代码][代码](res){[代码] [代码] [代码][代码]console.log([代码][代码]"上传出错:"[代码][代码]+res);[代码] [代码] [代码][代码]}[代码] [代码]})[代码] 参数说明 上传成功返回参数 [代码][[代码] [代码] [代码][代码]{[代码] [代码] [代码][代码]"localPath"[代码][代码]: [代码][代码]"http://tmp/wx00caa212d6710dcb.o6zAJs3oh85Zb1lJE8oWix57vny0.VzUQ2ebaMb87c6609a10aab7698d8645216baafb307e.jpg"[代码][代码],[代码] [代码] [代码][代码]"uploadResult"[代码][代码]: {[代码] [代码] [代码][代码]"hash"[代码][代码]: [代码][代码]"Fia6Y7gP4gXuaEsTfk5D9rslFWEz"[代码][代码],[代码] [代码] [代码][代码]"key"[代码][代码]: [代码][代码]"tmp/wx00caa212d6710dcb.o6zAJs3oh85Zb1lJE8oWix57vny0.VzUQ2ebaMb87c6609a10aab7698d8645216baafb307e.jpg"[代码][代码],[代码] [代码] [代码][代码]"imageURL"[代码][代码]: [代码][代码]"http://image.kucaroom.com/tmp/wx00caa212d6710dcb.o6zAJs3oh85Zb1lJE8oWix57vny0.VzUQ2ebaMb87c6609a10aab7698d8645216baafb307e.jpg"[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}, {[代码] [代码] [代码][代码]"localPath"[代码][代码]: [代码][代码]"http://tmp/wx00caa212d6710dcb.o6zAJs3oh85Zb1lJE8oWix57vny0.2sYa1Iz3HXvM983982ba2e07d25e79212b11b8c75742.jpg"[代码][代码],[代码] [代码] [代码][代码]"uploadResult"[代码][代码]: {[代码] [代码] [代码][代码]"hash"[代码][代码]: [代码][代码]"FmNPIb1IJ9xraRdl26zXAjjyTjfz"[代码][代码],[代码] [代码] [代码][代码]"key"[代码][代码]: [代码][代码]"tmp/wx00caa212d6710dcb.o6zAJs3oh85Zb1lJE8oWix57vny0.2sYa1Iz3HXvM983982ba2e07d25e79212b11b8c75742.jpg"[代码][代码],[代码] [代码] [代码][代码]"imageURL"[代码][代码]: [代码][代码]"http://image.kucaroom.com/tmp/wx00caa212d6710dcb.o6zAJs3oh85Zb1lJE8oWix57vny0.2sYa1Iz3HXvM983982ba2e07d25e79212b11b8c75742.jpg"[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码]][代码] localPath:上传图片的本地路径 uploadResult:上传七牛后成功返回的参数 上传失败后返回的参数 [代码]{error: [代码][代码]"bad token"[代码][代码], imageURL: [代码][代码]"http://image.kucaroom.com/undefined"[代码][代码]}[代码] 3.效果图 [图片]
2018-12-28