- 教你怎么监听小程序的返回键
更新:2020年7月28日08:51:11 基础库2.12.0起,可以调用wx.enableAlertBeforeUnload监听原生右上角返回、物理返回以及wx.navigateBack时弹框提示 AIP详情请看: https://developers.weixin.qq.com/miniprogram/dev/api/ui/interaction/wx.enableAlertBeforeUnload.html //======================================== 怎么监听小程序的返回键? 应该有很多人想要监听用户的这个动作吧,但是很遗憾,小程序不会给你这个API的,那是不是就没辙了? 幸好我们还可以自定义导航栏,这样一来我们就可以监听用户的这一动作了。 什么?这你已经知道啦? 那好咱们就不说自定义导航栏的返回监听了,说一下物理返回和左滑?右滑?(不管了,反正是滑)返回上一页怎么监听。 监听物理返回 首先说一下这个监听方法的缺点,虽说是监听,但是还是无法真正意义上的监听并拦截来阻止页面跳转,页面还是会返回上一页,而后重新载入刚刚的页面,如果这不是你想要的,那可以不用往下看了 其次说一下用到什么东西: wx.onAppRoute、wx.showModal 最后是一些主要代码: 重写wx.showModal,主要是加个confirmStay参数和使wx.showModal Promise化 [代码]const { showModal } = wx; Object.defineProperty(wx, 'showModal', { configurable: false, // 是否可以配置 enumerable: false, // 是否可迭代 writable: false, // 是否可重写 value(...param) { return new Promise(function (rs, rj) { let { success, fail, complete, confirmStay } = param[0] param[0].success = (res) => { res.navBack = (res.confirm && !confirmStay) || (res.cancel && confirmStay) wx.setStorageSync('showBackModal', !res.navBack) success && success(res) rs(res) } param[0].fail = (res) => { fail && fail(res) rj(res) } param[0].complete = (res) => { complete && complete(res) (res.confirm || res.cancel) ? rs(res) : rj(res) } return showModal.apply(this, param); // 原样移交函数参数和this }.bind(this)) } }); [代码] 使用wx.onAppRoute实现返回原来的页面 [代码]wx.onAppRoute(function (res) { var a = getApp(), ps = getCurrentPages(), t = ps[ps.length - 1], b = a && a.globalData && a.globalData.pageBeforeBacks || {}, c = a && a.globalData && a.globalData.lastPage || {} if (res.openType == 'navigateBack') { var showBackModal = wx.getStorageSync('showBackModal') if (c.route && showBackModal && typeof b[c.route] == 'function') { wx.navigateTo({ url: '/' + c.route + '?useCache=1', }) b[c.route]().then(res => { if (res.navBack){ a.globalData.pageBeforeBacks = {} wx.navigateBack({ delta: 1 }) } }) } } else if (res.openType == 'navigateTo' || res.openType == 'redirectTo') { if (!a.hasOwnProperty('globalData')) a.globalData = {} if (!a.globalData.hasOwnProperty('lastPage')) a.globalData.lastPage = {} if (!a.globalData.hasOwnProperty('pageBeforeBacks')) a.globalData.pageBeforeBacks = {} if (ps.length >= 2 && t.onBeforeBack && typeof t.onBeforeBack == 'function') { let { onUnload } = t wx.setStorageSync('showBackModal', !0) t.onUnload = function () { a.globalData.lastPage = { route: t.route, data: t.data } onUnload() } } t.onBeforeBack && typeof t.onBeforeBack == 'function' && (a.globalData.pageBeforeBacks[t.route] = t.onBeforeBack) } }) [代码] 改造Page [代码]const myPage = Page Page = function(e){ let { onLoad, onShow, onUnload } = e e.onLoad = (() => { return function (res) { this.app = getApp() this.app.globalData = this.app.globalData || {} let reinit = () => { if (this.app.globalData.lastPage && this.app.globalData.lastPage.route == this.route) { this.app.globalData.lastPage.data && this.setData(this.app.globalData.lastPage.data) Object.assign(this, this.app.globalData.lastPage.syncProps || {}) } } this.useCache = res.useCache res.useCache ? reinit() : (onLoad && onLoad.call(this, res)) } })() e.onShow = (() => { return function (res) { !this.useCache && onShow && onShow.call(this, res) } })() e.onUnload = (() => { return function (res) { this.app.globalData = Object.assign(this.app.globalData || {}, { lastPage: this }) onUnload && onUnload.call(this, res) } })() return myPage.call(this, e) } [代码] 在需要监听的页面加个onBeforeBack方法,方法返回Promise化的wx.showModal [代码]onBeforeBack: function () { return wx.showModal({ title: '提示', content: '信息尚未保存,确定要返回吗?', confirmStay: !1 //结合content意思,点击确定按钮,是否留在原来页面,confirmStay默认false }) } [代码] 运行测试,Oj8K 是不是很简单,马上去试试水吧,效果图就不放了,静态图也看不出效果,动态图懒得弄,想看效果的自己运行代码片段吧 代码片段 https://developers.weixin.qq.com/s/hc2tyrmw79hg
2020-07-28 - iphoneX兼容之自定义底部菜单
当我们需要自定义底部导航栏时 首先要解决iphoneX的底部大横条对这个兼容 通常不设置兼容 都会被挡住 如何编写 在你要编写的底部菜单中插入 样式 [代码]padding-bottom[代码][代码]: calc(env(safe-area-inset-[代码][代码]bottom[代码][代码]) / [代码][代码]2[代码][代码]) 即可兼容 [代码] [代码] 例如:css中插入[代码] [代码]@supports ([代码][代码]bottom[代码][代码]: constant(safe-area-inset-[代码][代码]bottom[代码][代码])) or ([代码][代码]bottom[代码][代码]: env(safe-area-inset-[代码][代码]bottom[代码][代码])) {[代码][代码] [代码][代码].fixed-wrap {[代码][代码] [代码][代码]height[代码][代码]: calc(env(safe-area-inset-[代码][代码]bottom[代码][代码]) / [代码][代码]2[代码][代码]);[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]100%[代码][代码];[代码][代码] [代码][代码]}[代码] [代码] [代码][代码].fixed-pay {[代码][代码] [代码][代码]padding-bottom[代码][代码]: calc(env(safe-area-inset-[代码][代码]bottom[代码][代码]) / [代码][代码]2[代码][代码]);[代码][代码] [代码][代码]}[代码] [代码]}[代码]其中 [代码]env(safe-area-inset-[代码][代码]bottom[代码][代码]) 是计算兼容的高度 通常一半即可 [代码] calc 是计算css 你也可以加入高度 假设有第二层 底部固定栏【即底部导航栏上面还有一层固定栏】 可如下编写 view.footer { bottom: calc(100rpx + env(safe-area-inset-bottom)); } 这样轻轻松松解决兼容 不需要写js代码 <-------------大横条-------------> [图片]
2019-05-28 - 小程序如何生成海报分享朋友圈
摘要: 小程序开发必备技能啊… 原文:小程序如何生成海报分享朋友圈 作者:小白 Fundebug经授权转载,版权归原作者所有。 项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优化等,二是对坑的地方做个记录,避免以后遇到类似的问题。 需求 利用微信强大的社交能力通过小程序达到裂变的目的,拉取新用户。 生成的海报如下: [图片] 需求分析 1、利用小程序官方提供的api可以直接分享转发到微信群打开小程序 2、利用小程序生成海报保存图片到相册分享到朋友圈,用户长按识别二维码关注公众号或者打开小程序来达到裂变的目的 实现方案 一、分析如何实现 相信大家应该都会有类似的迷惑,就是如何按照产品设计的那样绘制成海报,其实当时我也是不知道如何下手,认真想了下得通过canvas绘制成图片,这样用户保存这个图片到相册,就可以分享到朋友圈了。但是要绘制的图片上面不仅有文字还有数字、图片、二维码等且都是活的,这个要怎么动态生成呢。认真想了下,需要一点一点的将文字和数字,背景图绘制到画布上去,这样通过api最终合成一个图片导出到手机相册中。 二、需要解决的问题 二维码的动态获取和绘制(包括如何生成小程序二维码、公众号二维码、打开网页二维码) 背景图如何绘制,获取图片信息 将绘制完成的图片保存到本地相册 处理用户是否取消授权保存到相册 三、实现步骤 这里我具体写下围绕上面所提出的问题,描述大概实现的过程 ①首先创建canvas画布,我把画布定位设成负的,是为了不让它显示在页面上,是因为我尝试把canvas通过判断条件动态的显示和隐藏,在绘制的时候会出现问题,所以采用了这种方法,这里还有一定要设置画布的大小。 [代码]<canvas canvas-id="myCanvas" style="width: 690px;height:1085px;position: fixed;top: -10000px;"></canvas> [代码] ②创建好画布之后,先绘制背景图,因为背景图我是放在本地,所以获取 <canvas> 组件 canvas-id 属性,通过createCanvasContext创建canvas的绘图上下文 CanvasContext 对象。使用drawImage绘制图像到画布,第一个参数是图片的本地地址,后面两个参数是图像相对画布左上角位置的x轴和y轴,最后两个参数是设置图像的宽高。 [代码]const ctx = wx.createCanvasContext('myCanvas') ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085) [代码] ③创建好背景图后,在背景图上绘制头像,文字和数字。通过getImageInfo获取头像的信息,这里需要注意下在获取的网络图片要先配置download域名才能生效,具体在小程序后台设置里配置。 获取头像地址,首先量取头像在画布中的大小,和x轴Y轴的坐标,这里的result[0]是我用promise封装返回的一个图片地址 [代码]let headImg = new Promise(function (resolve) { wx.getImageInfo({ src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) let avatarurl_width = 60, //绘制的头像宽度 avatarurl_heigth = 60, //绘制的头像高度 avatarurl_x = 28, //绘制的头像在画布上的位置 avatarurl_y = 36; //绘制的头像在画布上的位置 ctx.save(); // 先保存状态 已便于画完圆再用 ctx.beginPath(); //开始绘制 //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false); ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片 [代码] 这里举个例子说下如何绘制文字,比如我要绘制如下这个“字”,需要动态获取前面字数的总宽度,这样才能设置“字”的x轴坐标,这里我本来是想通过measureText来测量字体的宽度,但是在iOS端第一次获取的宽度值不对,关于这个问题,我还在微信开发者社区提了bug,所以我想用另一个方法来实现,就是先获取正常情况下一个字的宽度值,然后乘以总字数就获得了总宽度,亲试是可以的。 [图片] [代码]let allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325; ctx.font = 'normal normal 30px sans-serif'; ctx.setFillStyle('#ffffff') ctx.fillText('字', allReading, 150); [代码] ④绘制公众号二维码,和获取头像是一样的,也是先通过接口返回图片网络地址,然后再通过getImageInfo获取公众号二维码图片信息 ⑤如何绘制小程序码,具体官网文档也给出生成无限小程序码接口,通过生成的小程序可以打开任意一个小程序页面,并且二维码永久有效,具体调用哪个小程序二维码接口有不同的应用场景,具体可以看下官方文档怎么说的,也就是说前端通过传递参数调取后端接口返回的小程序码,然后绘制在画布上(和上面写的绘制头像和公众号二维码一样的) [代码]ctx.drawImage('小程序码的本地地址', x轴, Y轴, 宽, 高) [代码] ⑥最终绘制完把canvas画布转成图片并返回图片地址 [代码] wx.canvasToTempFilePath({ canvasId: 'myCanvas', success: function (res) { canvasToTempFilePath = res.tempFilePath // 返回的图片地址保存到一个全局变量里 that.setData({ showShareImg: true }) wx.showToast({ title: '绘制成功', }) }, fail: function () { wx.showToast({ title: '绘制失败', }) }, complete: function () { wx.hideLoading() wx.hideToast() } }) [代码] ⑦保存到系统相册;先判断用户是否开启用户授权相册,处理不同情况下的结果。比如用户如果按照正常逻辑授权是没问题的,但是有的用户如果点击了取消授权该如何处理,如果不处理会出现一定的问题。所以当用户点击取消授权之后,来个弹框提示,当它再次点击的时候,主动跳到设置引导用户去开启授权,从而达到保存到相册分享朋友圈的目的。 [代码]// 获取用户是否开启用户授权相册 if (!openStatus) { wx.openSetting({ success: (result) => { if (result) { if (result.authSetting["scope.writePhotosAlbum"] === true) { openStatus = true; wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } } }, fail: () => { }, complete: () => { } }); } else { wx.getSetting({ success(res) { // 如果没有则获取授权 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) }, fail() { // 如果用户拒绝过或没有授权,则再次打开授权窗口 openStatus = false console.log('请设置允许访问相册') wx.showToast({ title: '请设置允许访问相册', icon: 'none' }) } }) } else { // 有则直接保存 openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } }, fail(err) { console.log(err) } }) } [代码] 总结 至此所有的步骤都已实现,在绘制的时候会遇到一些异步请求后台返回的数据,所以我用promise和async和await进行了封装,确保导出的图片信息是完整的。在绘制的过程确实遇到一些坑的地方。比如初开始导出的图片比例大小不对,还有用measureText测量文字宽度不对,多次绘制(可能受网络原因)有时导出的图片上的文字颜色会有误差等。如果你也遇到一些比较坑的地方可以一起探讨下做个记录,下面附下完整的代码 [代码]import regeneratorRuntime from '../../utils/runtime.js' // 引入模块 const app = getApp(), api = require('../../service/http.js'); var ctx = null, // 创建canvas对象 canvasToTempFilePath = null, // 保存最终生成的导出的图片地址 openStatus = true; // 声明一个全局变量判断是否授权保存到相册 // 获取微信公众号二维码 getCode: function () { return new Promise(function (resolve, reject) { api.fetch('/wechat/open/getQRCodeNormal', 'GET').then(res => { console.log(res, '获取微信公众号二维码') if (res.code == 200) { console.log(res.content, 'codeUrl') resolve(res.content) } }).catch(err => { console.log(err) }) }) }, // 生成海报 async createCanvasImage() { let that = this; // 点击生成海报数据埋点 that.setData({ generateId: '点击生成海报' }) if (!ctx) { let codeUrl = await that.getCode() wx.showLoading({ title: '绘制中...' }) let code = new Promise(function (resolve) { wx.getImageInfo({ src: codeUrl, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) let headImg = new Promise(function (resolve) { wx.getImageInfo({ src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`, success: function (res) { resolve(res.path) }, fail: function (err) { console.log(err) wx.showToast({ title: '网络错误请重试', icon: 'loading' }) } }) }) Promise.all([headImg, code]).then(function (result) { const ctx = wx.createCanvasContext('myCanvas') console.log(ctx, app.globalData.ratio, 'ctx') let canvasWidthPx = 690 * app.globalData.ratio, canvasHeightPx = 1085 * app.globalData.ratio, avatarurl_width = 60, //绘制的头像宽度 avatarurl_heigth = 60, //绘制的头像高度 avatarurl_x = 28, //绘制的头像在画布上的位置 avatarurl_y = 36, //绘制的头像在画布上的位置 codeurl_width = 80, //绘制的二维码宽度 codeurl_heigth = 80, //绘制的二维码高度 codeurl_x = 588, //绘制的二维码在画布上的位置 codeurl_y = 984, //绘制的二维码在画布上的位置 wordNumber = that.data.wordNumber, // 获取总阅读字数 // nameWidth = ctx.measureText(that.data.wordNumber).width, // 获取总阅读字数的宽度 // allReading = ((nameWidth + 375) - 325) * 2 + 380; // allReading = nameWidth / app.globalData.ratio + 325; allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325; console.log(wordNumber, wordNumber.toString().length, allReading, '获取总阅读字数的宽度') ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085) ctx.save(); // 先保存状态 已便于画完圆再用 ctx.beginPath(); //开始绘制 //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false); ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片 ctx.restore(); //恢复之前保存的绘图上下文状态 可以继续绘制 ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.setFontSize(28); // 文字字号 ctx.fillText(that.data.currentChildren.name, 103, 78); // 绘制文字 ctx.font = 'normal bold 44px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(wordNumber, 325, 153); // 绘制文字 ctx.font = 'normal normal 30px sans-serif'; ctx.setFillStyle('#ffffff') ctx.fillText('字', allReading, 150); ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText('打败了全国', 26, 190); // 绘制文字 ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#faed15'); // 文字颜色 ctx.fillText(that.data.percent, 154, 190); // 绘制孩子百分比 ctx.font = 'normal normal 24px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText('的小朋友', 205, 190); // 绘制孩子百分比 ctx.font = 'normal bold 32px sans-serif'; ctx.setFillStyle('#333333'); // 文字颜色 ctx.fillText(that.data.singIn, 50, 290); // 签到天数 ctx.fillText(that.data.reading, 280, 290); // 阅读时长 ctx.fillText(that.data.reading, 508, 290); // 听书时长 // 书籍阅读结构 ctx.font = 'normal normal 28px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(that.data.bookInfo[0].count, 260, 510); ctx.fillText(that.data.bookInfo[1].count, 420, 532); ctx.fillText(that.data.bookInfo[2].count, 520, 594); ctx.fillText(that.data.bookInfo[3].count, 515, 710); ctx.fillText(that.data.bookInfo[4].count, 492, 828); ctx.fillText(that.data.bookInfo[5].count, 348, 858); ctx.fillText(that.data.bookInfo[6].count, 212, 828); ctx.fillText(that.data.bookInfo[7].count, 148, 726); ctx.fillText(that.data.bookInfo[8].count, 158, 600); ctx.font = 'normal normal 18px sans-serif'; ctx.setFillStyle('#ffffff'); // 文字颜色 ctx.fillText(that.data.bookInfo[0].name, 232, 530); ctx.fillText(that.data.bookInfo[1].name, 394, 552); ctx.fillText(that.data.bookInfo[2].name, 496, 614); ctx.fillText(that.data.bookInfo[3].name, 490, 730); ctx.fillText(that.data.bookInfo[4].name, 466, 850); ctx.fillText(that.data.bookInfo[5].name, 323, 878); ctx.fillText(that.data.bookInfo[6].name, 184, 850); ctx.fillText(that.data.bookInfo[7].name, 117, 746); ctx.fillText(that.data.bookInfo[8].name, 130, 621); ctx.drawImage(result[1], codeurl_x, codeurl_y, codeurl_width, codeurl_heigth); // 绘制头像 ctx.draw(false, function () { // canvas画布转成图片并返回图片地址 wx.canvasToTempFilePath({ canvasId: 'myCanvas', success: function (res) { canvasToTempFilePath = res.tempFilePath that.setData({ showShareImg: true }) console.log(res.tempFilePath, 'canvasToTempFilePath') wx.showToast({ title: '绘制成功', }) }, fail: function () { wx.showToast({ title: '绘制失败', }) }, complete: function () { wx.hideLoading() wx.hideToast() } }) }) }) } }, // 保存到系统相册 saveShareImg: function () { let that = this; // 数据埋点点击保存学情海报 that.setData({ saveId: '保存学情海报' }) // 获取用户是否开启用户授权相册 if (!openStatus) { wx.openSetting({ success: (result) => { if (result) { if (result.authSetting["scope.writePhotosAlbum"] === true) { openStatus = true; wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } } }, fail: () => { }, complete: () => { } }); } else { wx.getSetting({ success(res) { // 如果没有则获取授权 if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) }, fail() { // 如果用户拒绝过或没有授权,则再次打开授权窗口 openStatus = false console.log('请设置允许访问相册') wx.showToast({ title: '请设置允许访问相册', icon: 'none' }) } }) } else { // 有则直接保存 openStatus = true wx.saveImageToPhotosAlbum({ filePath: canvasToTempFilePath, success() { that.setData({ showShareImg: false }) wx.showToast({ title: '图片保存成功,快去分享到朋友圈吧~', icon: 'none', duration: 2000 }) }, fail() { wx.showToast({ title: '保存失败', icon: 'none' }) } }) } }, fail(err) { console.log(err) } }) } }, [代码]
2019-06-15 - Painter 一款轻量级的小程序海报生成组件
生成海报相信大家有的人都做过,但是canvas绘图的坑太多。大家可以试试这个组件。然后附上楼下大哥做的可视化拖拽生成painter代码的工具:链接地址https://developers.weixin.qq.com/community/develop/article/doc/000e222d9bcc305c5739c718d56813
2019-09-27 - 完美解决小程序session问题
小程序不像web浏览器有cookie机制,在默认使用cookie存sessionid的机制下,后台将无法正常使用session功能,如果正确使用session呢,提供两个方案。 [代码]1、将sessionid通过url进行传递 用户每次登录成功后将生成的sessionid值使用参数回传到客户端, 客户端接到sessionid后保存到本地, 在发起网络请求的底层接口中默认自动带上sessionid=本地存储的sessionid值。 需要配合服务器一起更改,服务器后端默认使用cookie机制 2、无缝对接cookie, 将服务器的set-cookie值保存到本地,再请求的时候模拟浏览器头部信息并带上保存的cookie信息 1)保存cookie值: _XHR('login',{'code':res.code}).then(function( ret ){ ret.header["Set-Cookie"] != undefined && wx.setStorageSync("cookie", ret.header["Set-Cookie"]); }); 2)请求的时候自动带上cookie信息 var header={}; header = { 'content-type': 'application/x-www-form-urlencoded' }; var cookie = wx.getStorageSync("cookie"); if( url != 'login' && !isNull( cookie ) ){ header['cookie'] = cookie; } 将header 赋值到 request的header内 wx.request({ url: qryDomian + url + '.html', data: _data, method: 'POST', header: header, dataType:'json' ...... 第二种方案服务器无需做任务操作。[代码]
2019-05-20 - 自定义标题栏
使用效果 [图片][图片][图片][图片] 使用方法 属性介绍 属性名 类型 默认值 是否必须 说明 menuSrc String ‘’ 否 按钮图片地址 bgImgSrc String ‘’ 否 背景图片地址 bgImgMode String aspectFill 否 背景图片的显示模式 title String ‘’ 否 标题 titleTextColor String ‘’ 否 字体和按钮以及loading图标的颜色,按钮和loading暂时只有黑白2色 backgroundColor String ‘’ 否 整个标题栏的背景颜色 loading Boolean false 否 是否是加载状态 backProxy Boolean false 否 是否重写了返回键 标题栏中属性的默认数据会自动获取json配置以及系统的默认数据,如果不需要动态更改样式,可以在json中设置,组件中同样起作用 事件介绍 属性名 detail NaviBack 返回的逻辑方法 MenuTap 按钮的点击事件 [代码]"usingComponents": { "toolBar": "/component/toolbar" }, [代码] [代码]<toolBar menuSrc='/image/menu_white.png' bindMenuTap='onMenuTap' bgImgSrc='/image/navi-bg.jpg' /> [代码] 高度说明: 为了方便适配,这里给出自定义标题栏的计算公式: const MenuRect = wx.getMenuButtonBoundingClientRect() const statusBarHeight = wx.getSystemInfoSync().statusBarHeight; const height = (MenuRect.top - statusBarHeight) * 2 + MenuRect.height +MenuRect.top Github地址:https://github.com/Aracy/wx-mini-navigationbar
2019-05-21 - 小程序需要https域名,不会配置HTTPS?给我5分钟,手把手教你
本文针对不会配置HTTPS或者小白开发着,请开发者社区的大佬们自动忽略。非广告,心得分享,勿喷,谢谢。 👇 推荐一个小程序商城,全开源,码云GVP项目,有兴趣的可以了解一下:【点击下载】 👇 👇 正文开始 01、关于 FreeSSL.cn FreeSSL.cn 是一个免费提供 HTTPS 证书申请、HTTPS 证书管理和 HTTPS 证书到期提醒服务的网站,旨在推进 HTTPS 证书的普及与应用,简化证书申请的流程。 当然了,我看重的不是免费(微笑~),而是 FreeSSL 使用起来非常人性化。我是一个计算机常识非常薄弱的程序员(羞愧一下),但通过 FreeSSL,我竟然可以独自完成 Tomcat 的 HTTPS 配置! 很多年以前,公司要做华夏银行的接口对接,需要 HTTPS 访问,大概花了 3000 块买的证书,最后证书还有问题,HTTPS 也没搞定。总之,坑的很! FreeSSL.cn 有很大的不同,申请非常便捷,优点很多,值得推荐一波。毕竟再也不用邮件、电话各种联系了(也许时代进步了)。 100% 永久免费;这要感谢 Let’s Encrypt 与 TrustAsia 提供的免费 SSL 证书。 在 HTTPS 证书到期前,FreeSSL.cn 会及时地提醒更换证书,免费的服务。 私钥不在网络中传播,确保 HTTPS 证书的安全。 02、使用 FreeSSL 申请证书 第一步,填写域名,点击「创建免费的 SSL 证书」 [图片] 第二步,填写邮箱,点击「创建」 [图片] 1)证书类型默认为 RSA RSA 和 ECC 有什么区别呢?可以通过下面几段文字了解一下。 HTTPS 通过 TLS 层和证书机制提供了内容加密、身份认证和数据完整性三大功能,可以有效防止数据被监听或篡改,还能抵御 MITM(中间人)攻击。TLS 在实施加密过程中,需要用到非对称密钥交换和对称内容加密两大算法。 对称内容加密强度非常高,加解密速度也很快,只是无法安全地生成和保管密钥。在 TLS 协议中,应用数据都是经过对称加密后传输的,传输中所使用的对称密钥,则是在握手阶段通过非对称密钥交换而来。常见的 AES-GCM、ChaCha20-Poly1305,都是对称加密算法。 非对称密钥交换能在不安全的数据通道中,产生只有通信双方才知道的对称加密密钥。目前最常用的密钥交换算法有 RSA 和 ECDHE:RSA 历史悠久,支持度好,但不支持 PFS(Perfect Forward Secrecy);而 ECDHE 是使用了 ECC(椭圆曲线)的 DH(Diffie-Hellman)算法,计算速度快,支持 PFS。 2)验证类型默认为 DNS DNS 和文件验证有什么区别呢?我们再来一起了解下。 首先,我们需要明白一点,CA(Certificate Authority,证书颁发机构) 需要验证我们是否拥有该域名,这样才给我们颁发证书。 文件验证(HTTP):CA 将通过访问特定 URL 地址来验证我们是否拥有域名的所有权。因此,我们需要下载给定的验证文件,并上传到您的服务器。 DNS 验证:CA 将通过查询 DNS 的 TXT 记录来确定我们对该域名的所有权。我们只需要在域名管理平台将生成的 TXT 记录名与记录值添加到该域名下,等待大约 1 分钟即可验证成功。 所以,如果对服务器操作方便的话,可以选择文件验证;如果对域名的服务器操作比较方便的话,可以选择 DNS 验证。如果两个都方便的话,请随意选啦。 3)CSR生成默认为离线生成 离线生成、浏览器生成 和 我有 CSR 又有什么区别呢?来,我们继续了解一下。 离线生成 推荐!!!:私钥在本地加密存储,更安全;公钥自动合成,支持常见证书格式转换,方便部署;支持部分 WebServer 的一键部署,非常便捷。 离线生成的时候,需要先安装 KeyManager,可以提供安全便捷的 SSL 证书申请和管理。下载地址如下: https://keymanager.org/ Windows 的话,安装的时候要选择“以管理员身份运行”。 浏览器生成:在浏览器支持 Web Cryptography 的情况下,会使用浏览器根据用户的信息生成 CSR 文件。 Web Cryptography,网络密码学,用于在 Web 应用程序中执行基本加密操作的 JavaScript API。很多浏览器并不支持 我有 CSR:可以粘贴自己的 CSR,然后创建。 第三步,选择离线生成,打开 KeyManager 填写密码后点击「开始」,稍等片刻。 第四步,返回浏览器,点击「下一步」,出现如下界面。 [图片] 第五步,下载文件,并上传至服务器指定目录下。 第六步,点击「验证」,通过后,出现以下界面。 [图片] 第七步,点击「保存到KeyManager」,可以看到证书状态变成了已颁发。 03、为 Tomcat 配置 jks 格式证书 第一步,导出证书。假如服务器选择的 Tomcat,需要导出 Java keystone (简拼为 jks)格式的证书。 [图片] 注意:私钥的密码在配置 Tomcat 的时候用到。 [图片] 第二步,上传证书至服务器。 第三步,配置 Tomcat 的 server.xml。 [代码] <Connector port="81" protocol="HTTP/1.1" maxThreads="250" maxHttpHeaderSize="8192" acceptCount="100" connectionTimeout="60000" keepAliveTimeout="200000" redirectPort="8443" useBodyEncodingForURI="true" URIEncoding="UTF-8" compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml,application/xml,application/json,text/javascript,application/javascript,text/css,text/plain,text/json,image/png,image/gif"/> <Connector protocol="org.apache.coyote.http11.Http11NioProtocol" port="443" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" keystoreFile="/home/backup/qingmiaokeji.cn.jks" keystorePass="Chenmo" clientAuth="false" sslProtocol="TLS" useBodyEncodingForURI="true" URIEncoding="UTF-8" compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml,application/xml,application/json,text/javascript,application/javascript,text/css,text/plain,text/json,image/png,image/gif" /> [代码] 其中 keystorePass 为导出证书时私钥的加密密码。 第四步,重启 Tomcat,并在浏览器地址栏中输入 https://你的域名/ 进行测试。 注意到没,浏览器地址栏前面有一个绿色的安全锁,这说明 HTTPS 配置成功了!好了,为自己鼓个掌! 04、最后 你有没有订个五分钟的时间沙漏?如果超过五分钟 HTTPS 还没有配置成功,你过来揍我!反正你又打不来我!我在CRMEB等你! 👇 👇 👇 最后亿遍,再次发一下我的项目:全开源啊!公众号+小程序啊!商城系统啊!免费啊!了解一下啊→→→点我点我!
2019-05-10 - 自定义 tabBar iphoneX 适配问题,不吸底
自定义组件到不了 底部,但有时又可以。 [图片] [图片] [图片]
2019-03-14 - 基于加速度计判断横竖屏
也许有人会问,小程序中都是竖直app形态,要横竖屏判断有什么用?即使判断出了横屏状态,你能把小程序横过来?答案是不能的,但是判断当前设备处于横屏或者竖屏状态来实现一些友好的用户体验交互方式的需求确实存在。例如手机横屏,让视频播放自动全屏,手机竖屏,让视频切换回来小屏。 然而,截止至目前,小程序官方的API中并没有提供这样的横竖屏判断的方法。那么我们只能自己想办法实现这样的判断。小程序的设备API中提供了加速度计的监听方法,使用方法如下: [代码]wx.onAccelerometerChange([代码][代码]function[代码][代码](res) {[代码] [代码] [代码][代码]console.log(res.x)[代码] [代码] [代码][代码]console.log(res.y)[代码] [代码] [代码][代码]console.log(res.z)[代码] [代码]})[代码] 加速度计的三轴以下是一般移动设备的加速度计三轴坐标系示例图: [图片] 以手机竖直面向用户为例,加速计的三轴坐标系统的X、Y、Z轴定义如下: 沿着手机屏幕顶部向上是Y轴正方向,向下是Y轴负方向; 当手机顶部朝上时,沿着手机屏幕向右是X轴正方向,向左是X轴负方向; 正对手机时,垂直屏幕向外是Z轴正方向,垂直屏幕向里是Z轴负方向; 当手机处于静止状态时,手机此时只受一个重力加速度(1g=9.8m/s²)的作用,加速度计返回的res.x、res.y、res.z的值就是设备的三轴受到的加速度的值,取值范围从[-1g,1g]。设备以不同方式放置时,x/y/z的值如下: [图片] 计算姿态角在stackoverflow上找到了根据加速度计三轴的值计算姿态角公式(https://stackoverflow.com/questions/3755059/3d-accelerometer-calculate-the-orientation),经过结合设备的三轴坐标方向对公式进行调整,最终得出了公式: [代码]Pitch = atan2(Y, Z) * 180/M_PI;Roll = atan2(-X, sqrt(Y*Y + Z*Z)) * 180/M_PI;[代码] [代码][代码]Roll = atan2(-X, sqrt(Y*Y + Z*Z)) * 180/M_PI;[代码][代码] Roll(绕Y轴旋转的角度) 当设备绕着自身Y轴旋转时(表示手机左侧或右侧翘起的角度),该角度值将会发生变化,取值范围是-90到90度。 Pitch(绕X轴旋转的角度) 当手机绕着自身的Y轴旋转(表示手机顶部或尾部翘起的角度),该角度会发生变化,值的范围是-180到180度。 [图片] 接下来就是根据自己对横竖屏角度的观测,再结合微信小程序中,视频全屏只能以手机向左旋转方式全屏的特性,只对用户左侧横屏判断为横屏状态,实现代码片段如下: [代码] // 0为竖屏,1为横屏[代码][代码] [代码][代码]let lastState = 0;[代码][代码] [代码][代码]let lastTime = Date.now();[代码] [代码] [代码][代码]wx.startAccelerometer();[代码] [代码] [代码][代码]wx.onAccelerometerChange((res) => {[代码][代码] [代码][代码]const now = Date.now();[代码][代码] [代码] [代码] [代码][代码]// 500ms检测一次[代码][代码] [代码][代码]if[代码] [代码](now - lastTime < 500) {[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]lastTime = now;[代码] [代码] [代码][代码]let nowState;[代码] [代码] [代码][代码]// 57.3 = 180 / Math.PI[代码][代码] [代码][代码]const Roll = Math.atan2(-res.x, Math.sqrt(res.y * res.y + res.z * res.z)) * 57.3;[代码][代码] [代码][代码]const Pitch = Math.atan2(res.y, res.z) * 57.3;[代码] [代码] [代码][代码]// console.log('Roll: ' + Roll, 'Pitch: ' + Pitch)[代码] [代码] [代码][代码]// 横屏状态[代码][代码] [代码][代码]if[代码] [代码](Roll > 50) {[代码][代码] [代码][代码]if[代码] [代码]((Pitch > -180 && Pitch < -60) || (Pitch > 130)) {[代码][代码] [代码][代码]nowState = 1;[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码]((Roll > 0 && Roll < 30) || (Roll < 0 && Roll > -30)) {[代码][代码] [代码][代码]let absPitch = Math.abs(Pitch);[代码] [代码] [代码][代码]// 如果手机平躺,保持原状态不变,40容错率[代码][代码] [代码][代码]if[代码] [代码]((absPitch > 140 || absPitch < 40)) {[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码](Pitch < 0) { [代码][代码]/*收集竖向正立的情况*/[代码][代码] [代码][代码]nowState = 0;[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]// 状态变化时,触发[代码][代码] [代码][代码]if[代码] [代码](nowState !== lastState) {[代码][代码] [代码][代码]lastState = nowState;[代码][代码] [代码][代码]if[代码] [代码](nowState === 1) {[代码][代码] [代码][代码]console.log([代码][代码]'change:横屏'[代码][代码]);[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]console.log([代码][代码]'change:竖屏'[代码][代码]);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码] 然后就可以在横竖屏切换的状态下,去切换视频的横竖屏了 [代码]if[代码] [代码](state === 1) {[代码][代码] [代码][代码]video.requestFullScreen();[代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]video.exitFullScreen();[代码][代码]}[代码] 其他 另外,在这里发现小程序的一个小bug,就是当进入一个页面,马上就调用requestFullScreen()方法去拉起视频全屏时,会破坏整个页面的布局,并且再调用全屏方法时,视频就无法再全屏了,像这样: [图片] 所以为了防止用户直接以横屏的状态进入一个视频播放页,而我们的横屏判断检测生效立即触发全屏引发bug,我将监听横竖屏的事件通过setTimeout(listener, 3000)延迟3s监听,这样横屏才不会触发bug。 最后 文中的很多知识点很多都是从网络文章学来,可能存在错误的理解,如有错误,欢迎各位指正。 最后再打个广告,欢迎喜欢看游戏直播的小伙伴来试用我们的《TG电竞》直播小程序,这里聚合各大平台的知名主播,总有一款适合你哦。 [图片]
2018-01-25 - 一次在微信小程序里跑 h5 页面的尝试
前言 标题看起来有点唬人,在微信小程序里跑 h5 页面,不会又是说使用 web-view 组件来搞吧?确实,使用 web-view 组件可以达到跑 h5 页面的要求,但是 web-view 组件在使用上还是有一些限制:不支持个人类型与海外类型的小程序、不支持全屏、页面与小程序通信不方便、很多小程序接口无法直接调用等。 那么,还有其他的法子么?理论上来说还是有的。 运行环境 h5 页面是运行在 web 环境下,小程序本身也是基于 web 的,那为什么一直没有办法让 h5 在小程序里直接运行呢?原因在于小程序特有的运行环境。 以一个小程序的页面为例,通常一个小程序的页面至少包含三个文件:wxss 文件、 wxml 文件和 js 文件。其中 wxml 文件和 wxss 文件组成了页面的视图层,js 文件则属于页面的逻辑层,在小程序中,视图层和逻辑层是在不同的线程中执行的。小程序里所有页面的逻辑层都在一个 js 线程中运行,而视图层则分别在不同的 view 线程中。通常一个页面对应一个 view 线程,为了对性能的控制,不会允许用户无节制的启动 view 线程,所以也就有了页面栈数量的限制(目前最多允许打开十层页面)。 [图片] 在 view 线程中是有类似浏览器一样的环境,但是只有页面的视图层在上面跑,页面的渲染完全基于另一个 js 线程传输过来的数据。js 线程是一个纯净的 js 环境,那些你想要调的 document.getElementById、location.href 等 dom/bom 接口通通都没有,你只能在这里执行 js 代码,调用官方提供的接口,而页面的逻辑层就是在这样的线程中跑。这样问题就出来了,页面会渲染成什么样子,完全基于初始模板和数据,你想调接口来修改页面结构,门都没有~ 方案制定 小程序的运行环境如此特殊,以至于它的开发模式也比较另类,但是还是有很多人希望能够将开发过程大一统,一份代码各端运行。那么在限制如此之大的情况下,要怎么做的? 现在市面上有一些基于 react 或 vue 搞出来的工具,它们要求你使用 react 或者 vue 来写页面,由构建工具来编译到各个环境上可运行的目标代码,因为 react 和 vue 本身是基于数据来驱动的组件化框架,在一定程度上限制开发者直接调用 dom/bom 接口,所以可以比较方便的实现代码的编译转化。 PS:因为小程序本身的限制,react 和 vue 的小部分功能也是无法做到完全兼容的,不过大部分的实现都是 OK 的。 不过这不是我想要的,因为它们虽然实现得很漂亮,但是仍有不小的开发局限,你必须选择 vue、react 等框架的其中一种来使用。我想要的是更原生、更底层的大一统方案,底层到除了 vue、react 之外,你甚至可以在小程序中使用 jQuery。 目标太大会扯着蛋,至少得先有一个思路才行。 回到先前提到,小程序的逻辑层是在一个纯净的 js 线程中跑,那里没有 dom/bom 接口,只能运行页面逻辑层的代码,那么我们想办法在逻辑层建一棵 dom 树,把基本的 dom/bom 接口都模拟出来不就行了么? [图片] 乍一想好像可以,但是这里隐藏着一个问题:逻辑层中 dom 树的变更要如何转变成数据并更新到视图层呢? 这里很重要的一点:小程序提供了自定义组件,并且支持递归引用。也就是说,我们可以将 h5 中的 div、span、ul 等 dom 节点转成自定义组件,逻辑层里的每个 dom 节点都会对应一个视图层中的自定义组件实例,节点更新了,我们找到对应的自定义组件实例进行更新即可。 开始实施 方案很简单,但是实现起来的话细节和坑特别多,下面且听我挑几个关键的 trick 来介绍: dom 树的模拟 想要在小程序的逻辑层模拟出一个 dom 树,本质上和建立一棵虚拟树类似,因为它并不是真实的 dom 树。整个流程简单来说是这样的: [图片] 不管是页面中的静态 html 内容还是使用 innerHTML 等接口动态插入的 html 内容都可以走上面的流程来进行 dom 树的创建。dom 树创建比较简单,只是细节比较多,此处的问题是建立好的 dom 树要如何渲染到视图层呢?上面提到了一个关键点:将 dom 节点转成小程序的自定义组件。 此处再重复一次表强调:小程序的自定义组件支持递归引用! 什么叫递归引用?感觉用文字表达不如直接用代码说明来的快,我们先直接编写一个名为 element 的自定义组件: [代码]// element.js Component({ data: { children: ['div', 'span'], } }) [代码] [代码]<!-- element.wxml --> <block wx:for="{{children}}"> <div wx:if="{{item === 'div'}}"></div> <span wx:elif="{{item === 'span'}}"></span> </block> [代码] [代码]{ component: true, usingComponents: { "div": "./element", "span": "./element" } } [代码] 看代码应该很容易就能明白,所谓递归引用就是自己可以作为自己的一个子组件使用,这样可以很方便地将 dom 树给渲染在逻辑层,并且也不需要我们为每种节点标签给编写一个自定义组件,如上述例子,只需要编写一个 element 自定义组件基本可以覆盖所有标签。 同样,根据上述例子可以很明显的看出,每个节点只负责渲染自己的子节点,其他如孙子节点等后代节点一概不管,这样就避免了在更新时需要将整棵树结构传递给视图层的麻烦问题。 对于从逻辑层向视图层传递数据,小程序里有个数据包大小的限制,此处若同步整棵树结构,一来可能爆了这个限制,二来会传递很多无用数据,增大更新开销。 建立联系 创建好 dom 树后,下一步就是要想办法将小程序创建的自定义组件实例和我们创建的 dom 节点给一一联系起来。首先,我们得给每个 dom 节点生成一个节点 id 用于区分,根节点的节点 id 可以在构建过程中直接生成,假定根节点就是 body 节点,那么生成的页面的 wxml 和 json 中大概是这样的: [代码]<!-- page.wxml --> <body data-private-node-id="node-id-xxxxxx"></body> [代码] [代码]{ usingComponents: { "body": "../components/element", } } [代码] body 节点同样是用上面例子中的 element 自定义组件来渲染,修改下 element 自定义组件的 js 文件: [代码]// element.js Component({ data: { children: []. }, attached() { const nodeId = this.dataset.privateNodeId // 节点 id } }) [代码] 如上述代码所述,在自定义组件的 attached 生命周期中即可拿到节点 id,然后根据此节点 id 从 dom 树中找到对应的 dom 节点,两者之间即可建立关系。 找到根节点后,后续节点就好办了。根节点对应的自定义组件实例在和 dom 节点建立联系后,就可以通过 dom 节点拿到子节点列表,进而开始渲染子节点。 由上可知,每个节点只负责渲染自己的子节点,每个节点的渲染流程都和根节点一样: 拿到节点 id 和 dom 节点建立联系 通过 dom 节点拿到子节点列表 渲染子节点 根据这个逻辑修改一下上述例子中的 element 自定义组件的代码: [代码]// element.js Component({ data: { children: []. }, attached() { const nodeId = this.dataset.privateNodeId // 节点 id const domNode = getDomNode(nodeId) // 根据节点 id 找到 dom 节点 const children = domNode.children // 拿到子节点列表 // 渲染子节点 this.setData({ children, }) } }) [代码] [代码]<!-- element.wxml --> <block wx:for="{{children}}"> <div wx:if="{{item.tagName === 'DIV'}}" data-private-node-id="{{item.nodeId}}"></div> <span wx:elif="{{item.tagName === 'SPAN'}}" data-private-node-id="{{item.nodeId}}"></span> </block> [代码] 如此递归下去,即可将自定义组件实例和 dom 节点一一联系起来,同时将整棵 dom 树渲染出来。 渲染信息的同步 假若逻辑层想要获取视图层的渲染信息,比如调 [代码]getComputedStyle[代码] 接口获取节点样式、使用 [代码]clientWidth[代码] 或取节点宽度等,因为逻辑层本身只是模拟了 dom 树,并没有实现布局和渲染的模拟(理论上可以实现,只是开销巨大,而且很难与小程序本身环境的渲染结果对齐),所以这块实现起来很头疼。 目前的方案上使用小程序的 [代码]selectorQuery[代码] 接口来拉取渲染信息,因为此接口只能异步拉取,所以没法完整模拟渲染信息的即时同步。为了尽可能做到相对同步,在初始渲染完成后尝试拉取一次渲染信息,之后在每次触发节点更新后再异步拉取渲染信息,同时提供一个异步接口给某些需要即时拉取渲染信息的场景中使用。 全局对象的处理 上面提到小程序的逻辑层是跑在一个 js 线程中,这个 js 线程是一个纯净的 js 线程,别说那些 bom、dom 接口了,连一个正经的全局对象都没有。 小程序中其实有 global 对象,但是和我们熟悉的 nodejs 中的 global 对象实现不一样,所以此处按下不提。 做前端开发的同学们应该都知道,h5 环境中声明在全局的变量/函数会挂在 window 下,在页面的其他地方是可以使用或者是通过 [代码]window.xxx[代码] 的方式来访问的。在小程序的运行环境中这些是不存在的,那就意味着会有下述这些问题: [代码]// a.js function xxx() {} window.yyy = function() {} // b.js window.xxx() // 报 xxx 不存在 yyy() // 报 yyy 不存在 [代码] 这个问题很严重,要知道我们很多时候访问一些对象都会忽略掉 [代码]window.[代码] 这个前缀,比如使用 location 对象的时候。也就是说,必须对这些 js 做一遍后处理,强制将全局函数挂在 window 下,强制通过使用 [代码]window.xxx[代码] 的方式访问全局变量/函数。经过后处理后,上述例子会变成下面这样: [代码]// a.js function xxx() {} window.yyy = function() {} window.xxx = xxx // 强制挂在 window 下 // b.js window.xxx() // 正常执行 window.yyy() // 强制补充 window 前缀,正常执行 [代码] 嗯,完美解决! 其他 上面提到的是整体的实现框架,也简单的提到几个坑。当然,这只不过是冰山一角,事实上需要兼容的坑、无法按照标准实现的接口和完全无法实现的特性犹如满天星,数都数不清,幸运的是我们并不完全依赖到所有的接口,有些时候我们仅仅需要用到像 querySelector、事件系统、异步请求等接口就可以构造出大部分 h5 页面,而这些大多是能做到兼容的,所以对于某些不太复杂的场景来说还是可行的。 工具试用 按照上面的思路实现了一个差不多的构建工具,暂且命名为 [代码]h5-to-miniprogram[代码],只看名字就知道这是个想干嘛的工具,不过这终究是个玩票性质的尝试,有兴趣的同学也可以点击此处翻看源码~
2019-02-27 - 自定义导航栏所有机型的适配方案
写在前面的话 大家看到这个文章时一定会感觉这是在炒剩饭,社区中已经有那么多分享自定义导航适配的文章了,为什么我还要再写一个呢? 主要原因就是,社区中大部分的适配方案中给出的大小是不精确的,并不能完美适配各种场景。 社区中大部分文章给到的值是 iOS -> 44px , Android -> 48px 思路 正常来讲,iOS和Android下的胶囊按钮的位置以及大小都是相同且不变的,我们可以通过胶囊按钮的位置和大小再配合 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 来计算出导航栏的位置和大小。 小程序提供了一个获取菜单按钮(右上角胶囊按钮)的布局位置信息的API,可以通过这个API获取到胶囊按钮的位置信息,但是经过实际测试,这个接口目前存在BUG,得到的值经常是错误的(通过特殊手段可以偶尔拿到正确的值),这个接口目前是无法使用的,等待官方修复吧。 下面是我经过实际测试得到的准确数据: 真机和开发者工具模拟器上的胶囊按钮不一样 [代码]# iOS top 4px right 7px width 87px height 32px # Android top 8px right 10px width 95px height 32px # 开发者工具模拟器(iOS) top 6px right 10px width 87px height 32px # 开发者工具模拟器(Android) top 8px right 10px width 87px height 32px [代码] [代码]top[代码] 的值是从 [代码]statusBarHeight[代码] 作为原点开始计算的。 使用上面数据中胶囊按钮的高度加 [代码]top[代码] * 2 上再加上 [代码]statusBarHeight[代码] 的高度就可以得到整个导航栏的高度了。 为什么 [代码]top[代码] * 2 ?因为胶囊按钮是垂直居中在 title 那一栏中的,上下都要有边距。 扩展 通过胶囊按钮的 [代码]right[代码] 可以准确的算出自定义导航的 [代码]左边距[代码]。 通过胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]右边距[代码] 。 通过 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]windowWidth[代码] - 胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]width[代码] 。 再扩展 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 每个机型都不一样,刘海屏得到的数据也是准确的。 如果是自定义整个页面,iPhone X系列的刘海屏,底部要留 [代码]68px[代码] ,不要问我为什么! 代码片段 https://developers.weixin.qq.com/s/Q79g6kmo7w5J
2019-02-25