个人案例
- 该账号已注销
中英文语音翻译,语音播放结果。
随心翻译扫码体验
- 微信小程序swiper的自适应高度
小程序组件swiper需要指定固定高度,但在某些场景中我们需要动态设置swiper的高度以完整展示swiper中的内容,比如高度不同的图片,笔者最近项目中的日历组件(31号有时会多出一行)等等,如何使swiper组件自适应高度呢? 翻阅了一些网上的例子,一般的解决方法是通过设置style.height来解决 [代码]<swiper style="{{style}}" > <swiper-item></swiper-item> </swiper> [代码] [代码] Page({ data: { style: '' }, onReady(){ this.setData({style: 'height: 100px'}) } }) [代码] 问题:状态丢失 直接设置样式可以动态设置高度,但这样做的不好之处在于会重新渲染结构,导致之前设置的状态丢失,比如我们在日历中选中的日期 我们的需求是,1. 动态设置swiper高度,2. 不丢失之前的状态 一番折腾过后,发现这条路是个死胡同,不能解决问题。 解决: CSS变量 后来发现使用css变量也能够动态改变样式,抱着试一试的想法 模板 [代码]<view class="box" style="{{boxStyle}}"> <swiper class="container"> <swiper-item></swiper-item> </swiper> </view> [代码] 样式 [代码].box{ --box-height: 400px; --append-height: 0; width: 100vw; height: calc(var(--box-height) + var(--append-height)) } .container{ height: 100%; width: 100%; } [代码] js [代码]Page({ data: { boxStyle: '' }, onReady(){ if (...) { this.setData({boxStyle: '--append-height: 50px'}) } else { this.setData({boxStyle: '--append-height: 0'}) } } }) [代码] 上述设置,居然能够完美的实现项目需求,现在项目正在上线中,等待测试出bug,哈哈 欢迎关注github项目 关注下面的小程序查看最新的DEMO示例 [图片]
2020-06-24 - 只有三行代码的神奇云函数的功能之三:100%成功获取unionid
这是一个神奇的网站,哦不,神奇的云函数,它只有三行代码:(真的只有三行哦) 云函数:login index.js: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event) => { return { ...event, ...cloud.getWXContext() } } 神奇功能之三:100%成功获取unionid: 保证100%成功获取unionid,需要用户信息授权。 强调一下:这个100%是指必须绑定了开放平台,那么不管用户是什么情况,不管有没有关注公众号,一定100%能获取到unionid。 依然需要符合unionid机制:第1条。 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html js: getUserInfo: function (e) { app.globalData.userInfo = e.detail.userInfo if (!app.globalData.unionid ) { wx.cloud.callFunction({ name: 'login', data: { weRunData: wx.cloud.CloudID(e.detail.cloudID) } }).then(res => { app.globalData.unionid = res.result.weRunData.data.unionId }) } }, 其他功能: 神奇功能之四:获取电话号码: 还是这三行代码,获取用户的电话号码。 https://developers.weixin.qq.com/community/develop/article/doc/0006a8ec7ac860c94bf90a34f5d813 神奇功能之五:获取群id: 将小程序分享到某群里,可获得该群的群id, https://developers.weixin.qq.com/community/develop/article/doc/000ea802c00f70894cf9fe72556013 神奇功能之一:获取openid: https://developers.weixin.qq.com/community/develop/article/doc/00080c6e3746d8a940f9b43e55fc13 神奇功能之二:不用授权获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/000a0c6b580338e947f9db0c65b813 [图片]
2020-10-25 - 多张图片上传(源码分享+实现分析)
本篇文章以小程序中的代表【微信小程序】为例,分享一下在微信小程序中实现多图上传的源码实现。 代码片段(可导入微信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 - 微信对话开放平台初体验
初步了解 前几天我在【微信开放社区】,看到了这个活动【微信对话开放平台】的活动,感觉很有趣,就去官网上看了下,发现这个是一个智能对话系统,里面的功能还挺多的,比方说可以查天气、成语接龙等。咋一看好像没有什么特别的地方,因为图灵机器人这样的也可以做到啊,并没有什么稀奇的,只是一个智能机器人而已。 按照之前的方法,微信公众号后台接入图灵机器人,再自定义一些关键字回复,也就实现了所谓的智能。 我仔细看了官网的介绍,发现微信对话平台也大概是这回事。不过呢,微信对话平台有一些天然的优势,比方说接入方便简单、有测试接口、功能强大可配置等,这样的服务接入到微信公众号、小程序等,是非常方便的。使用微信自家的服务,做微信开发,可以节省很多成本,便于快速开发。除了微信相关的,还可以用于网站等其他产品的业务场景。 [图片] 开发门槛低,接入简单,这对个人和初创团队来说,是十分友好的。既然微信提供了这样的能力,而且是免费的,为什么试着不用一下呢? [图片] 基于这样的想法,我决定在我的个人小程序【EXIF查看器】中,接入【微信对话开放平台】的小程序插件,看看实际体验,是不是如同官方介绍的那样,轻便又好用。 登陆后台 登陆这一步,还是经典又熟悉的操作,进入官网(https://openai.weixin.qq.com/),微信扫描二维码,确认后即可登陆,跟登陆小程序和公众号一样一样的。 登陆后,点击【开始使用】,即可进入后台。 [图片] 如果你翻到了页面底部,也有个这样的按钮在等着你,还算比较贴心的,不然还真找不到这个后台入口在哪。 [图片] 后台界面 进入后台,发现模块划分还是比较合理的,只是有些功能按钮,第一次接触不一定能很快速的找到。下面我把最常用的按钮框选了出来,主要是三个部分:调试与发布、创建技能、对话技能开关。下面我会针对这三块,一一说明。 [图片] 调试与发布 首先是调试与发布,这个一般是机器人配置好了后进行的操作,调试可以让你很方便的看到对话相应的结果。有时候会返回系统智能匹配的回复,有时候是你自己预设好的,这个实际表现怎么样,通过这个可以很直观的看出来。 [图片] 但这个有个缺陷,不支持语音输入。所以实际体验,还是要以真机为主。为了能让真机看到效果,需要在【机器人发布】中,执行发布操作。 [图片] 创建技能 点击【创建技能】按钮,输入一个技能名称。 [图片] 确定后,即可进入技能设置页面。 [图片] 界面十分简洁,没有什么多余的内容。点击【新建问答】,设置一个标准问题,相似度阈值可以默认0.8,问题写好后,点击确定。 [图片] 短暂loading加载后,出现这个单个问答配置页面。 [图片] 右边的上半部分,你可以添加关键词触发条件,下面则是对应关键词触发后的回复,比方说我先添加一个关键词「谁」。 [图片] 接着就是要添加对应回答了,点击回答,会出现很多类型,有纯文本、公众号文章、公众号图片、公众号音频、公众号视频、小程序、H5、回调函数8个分类。 根据实际需要,选择对应的消息形式。 [图片] 只是这里写完后,保存按钮不在旁边,跟上面的问题不一样,也没有添加按钮。我开始因为这个保存内容交互不一致的问题,感到困惑。后来发现,这个上面是有个【保存配置】按钮的,点击这里就可以了。 [图片] 添加回答保存后,这个提示也不会消失,感觉是个bug…… [图片] 另外这个回答的【添加】按钮也不明显,点这里可以添加多条回复。 [图片] 经过测试,发现机器人会在这几种可能中随机触发回复语。 [图片] 问题的阈值,要设置为1,这样才能百分比命中自定义的回答。 [图片] 如果不是1,回复的是系统默认的推荐的随机回答。 [图片] 对话技能开关 这里面的技能很丰富,测试的时候可以把每个都试一下,很好玩。 但是要注意,如果是小程序接入,务必要记得取消哪些不必要的技能勾选,如新闻、股票之类的,因为的你小程序可能不是这类主体,接入官方拒审,并会要求你修改分类。为了接入小程序修改分类不划算,所以根据实际情况,尽可能勾选少的技能是最好的。 [图片] 总结 走完以上的操作,基本上就可以成功创建一个小微机器人了,你可以用她来陪你聊天,或者是帮你解答用户遇到的常见问题,还有一些其他有趣的操作,开动脑洞,玩起来会十分有趣。在下一篇小程序接入的介绍文章中,我会带你领略微信对话开放平台更加有趣的玩法,敬请期待。
2019-11-30 - 微信支付接口报【签名错误】,看这一篇就够了
此文章致力解决在开发微信支付相关接口报【签名错误】,并不断升级更新 文章demo以’普通商户版’-‘JSAPI支付’作为案例(JSAPI支付文档) 先讲一下开发步骤和经验,文章后半部分讲排错经验 开发步骤 一 设置支付目录(文档链接) 支付目录,一定要设置实际支付页面的路径以 / 结尾,如果提示<当前页面URL未注册>,请检查自己实际支付页面的路径是否填写正确 发起支付的业务流程,我们做的操作应该是这样的:用户选择支付金额和其他参数–>用户点击支付–>前端向后台发起请求获取签名等参数–>后台调用统一下单接口,返回给前端需要的签名参数–>前端调用WeixinJSBridge.invoke–>用户填写密码–>支付成功–>微信发送通知给统一下单填写的回调方法。详细业务流程点我查看 二 后台调用统一下单接口(文档链接) 此接口参数非常多,第一次开发的时候,建议开发者仔仔细细对每个参数进行比对。遇到签名错误的同学,大部分人的原因是因为参数填写错误导致的 后台在给前端准备参数的时候,是要进行两次签名的:第一次是发送统一下单请求之前,对发送给微信的所有参数进行签名;第二次是微信返回预支付交易会话标识后,对传给前端的所有参数进行签名。 请注意,第一次签名和第二次签名的时候,参数是不一样的,第二次签名的时候,签名需要哪些参数呢?签名的参数是WeixinJSBridge.invoke需要用到的参数,和第一次签名需要的参数是不一样的! 对于参数package我第一次粗心大意,没有拼接字符串‘prepay_id=’希望大家也注意一些,前后台都需要拼接这个字符串‘prepay_id=’ 这是我刚刚花费10分钱获取的统一下单截图 [图片] 只有result_code和return_code都为SUCCESS的时候,说明调用成功,成功拿到预支付的id 三 前端获取参数后拉起微信支付(文档链接) [图片] 其实完整坐下来,微信支付就这么点东西,只是大家可能有些不熟悉,对于大家遇到的签名错误问题,绝大部分是参数没有认真进行参数比对,参数不能多,也不能少。如果还报错,建议从下面一些方式进行排查 排错经验 · 首先排查签名方法是否正确(签名效验工具),如果自己写的签名方法和工具展示出来的结果一模一样,说明你签名的工具方法写的没有问题,那么就剩下参数的问题了! · 然后进行参数比对,根据开发文档,进行比对,一个字母都不能差 · 第一次签名和第二次签名的APPID ,后台签名的i是小写,前端调用的是大写 · 后端第二次签名,参数package一定不要忘记拼接prepay_id · 请再三确认appid和mch_id是否正确,如果同时进行多个公众号支付开发,一定不要弄混 · 第二次签名参数timeStamp时间差距太大(你服务器时间要尽量准确,好像误差不能超过10分钟) · 中文参数错误,英文参数没有问题的,本文以MD5加密为例,请在加密的时候,指定编码格式为UTF-8 对于企业付款到零钱/银行卡 · 尝试在商户平台的账户信息中更改API密钥(账户设置-安全设置-API安全), 15分钟后生效 · 还是参数,参数,参数 终极杀器·缓存 作者开发语言是java,之前缓存无处不在,myeclipse(开发者工具)的缓存,本地编译缓存,服务器tomcat的缓存,如果你觉得我就是对的,什么都排查过了,没有问题,OK,建议清理缓存(先删除tomcat里边的项目,再添加然后重新编译项目;服务器tomcat缓存,清理tomcat文件夹下work-catalina文件夹的内容)。实在不行,重启本地电脑。重启服务器server。 请各位同学一定要先自行排查问题,如果还无法解决问题,或者你遇到过其他bug情况,欢迎留言,我会及时更新到文章,以便帮助更多人解决签名错误的问题。ღ( ´・ᴗ・` )比心 ------------------分割线------------------- 签名方式是否真的正确? ----------2019年12月2日更新,感谢斌斌反馈---------- 由于作者做支付是在2016年,辛辛苦苦整理了一份demo,一直沿用至今,忘记当时是否有官方sdk了,如果大家用的是官方sdkDemo,以上bug排查完,还是报签名错误,请检查签名方式,实际是MD5还是HMACSHA256。具体情况可以看这篇提问【JSAPI第二次签名到底什么机制?】
2021-03-18 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 揭开微信小程序 Kbone 的神秘面纱
微信小程序诞生至今,渗透到用户生活的方方面面,包括餐厅点餐,网上购物,乘车出行,挂号就医…… 做为开发者,你是否有这样的烦恼? “当 Web 项目完成之后,产品也想搭一套一样的在小程序端。” 这时的你,是不是有种透心凉的感觉?再搭一套同样的项目,肯定会有成本。比如同时维护两套类似的代码,这对于开发者来说是相当头疼的一件事情!! 针对上述的问题,微信小程序推出了 Kbone 来解决微信小程序的同构问题。Kbone是什么,以及做了什么,我们继续来一探究竟。 Kbone 是什么 Kbone 是一个致力于微信小程序和 Web 端同构的解决方案。 微信小程序的底层模型和 Web 端不同,我们想直接把 Web 端的代码挪到小程序环境内执行是不可能的。Kbone 的诞生就是为了解决这个问题,它实现了一个适配器,在适配层里模拟出了浏览器环境,让 Web 端的代码可以不做什么改动便可运行在小程序里。 这样,我们就可以借助 Kbone 快速实现 Web 项目转化为微信小程序项目。 Kbone的优劣 你可能想问:“市面上同构的方案那么多?我为什么要选择 Kbone 呢?” 这里,我们来谈谈 Kbone 到底有什么优势。 1)大部分流行的前端框架都能够在 Kbone 上运行,比如 Vue、React、Preact 等。 2)支持更为完整的前端框架特性,因为 Kbone 不会对框架底层进行删改(比如 Vue 中的 v-html 指令、Vue-router 插件)。 3)提供了常用的 DOM/BOM 接口,让用户代码无需做太大改动便可从 Web 端迁移到小程序端。 4)在小程序端运行时,仍然可以使用小程序本身的特性(比如像 live-player 内置组件、分包功能)。 5)提供了一些 Dom 扩展接口,让一些无法完美兼容到小程序端的接口也有替代使用方案(比如 getComputedStyle 接口)。 那么,Kbone 就没有劣势吗? 不是所有的方案都是无懈可击的,就像每个人都有优缺点,Kbone 也不例外。 Kbone 是使用一定的性能损耗来换取更为全面的 Web 端特性支持。 如何选择同构方案? 因为 Kbone 会损耗一定的性能,所以我们建议你这样选择: 1)如果你对小程序的性能特别苛刻,建议直接使用原生小程序开发。 2)如果你的页面节点数量特别多(通常在 1000 节点以上),同时还要保证在节点数无限上涨时仍然有稳定的渲染性能的话,可以尝试一下业内采用静态模板转译的方案。 3)其他情况就可以直接采用 Kbone 了。 快速上手 说了这么多,你肯定想知道 Kbone 到底是怎么用的。下面我们就来看看~ 如果,你的项目还没有开始,那么推荐你使用 kbone-cli 来快速开发。只需两步: 1)安装 kbone-cli [代码]npm install -g kbone-cli [代码] 2)创建项目 [代码]kbone init my-app [代码] [图片] [图片] 项目初始化成功后,就可以按照 [代码]README.md[代码] 的指引进行开发~ 自行搭建 如果你不想要使用官方提供的模板,想要更灵活地搭建自己的项目,又或者是想对已有的项目进行改造,那么只需要自己补充对应配置来实现 Kbone 项目的构建。 一般需要补充两个配置: 构建到小程序代码的 webpack 配置。 使用 webpack 构建中使用到的特殊插件 mp-webpack-plugin 配置。 具体配置方式和操作流程 点此查看。 最后 如果你对 Kbone 感兴趣或者有相关问题需要咨询, 欢迎加入 Kbone 技术交流 QQ 群:926335938 。
2019-12-26 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: 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 - 同一开发者账号两个不同的云开发空间,数据存储串库的问题。
一个appid下开通了两个云开发空间,在一个空间调试好后,切换到另外一个云开发空间,云函数和云存储都能正常运行,但数据库这块的数据还会保存到原来那个空间中,而云函数的日志显现都正常运行有数据获取,而新开发空间是没有任何数据库信息的。
2019-08-28 - [有点炫]自定义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 - ThorUI V1.3.0
小程序二维码 [图片] [图片] ThorUI组件文档参考 http://www.thorui.cn/doc 本项目已同步更新到uni-app,后续会同步支付宝小程序,百度小程序,头条小程序等 更新日志 V1.6.5(2021-05-24) 1.tui-validation(表单验证)优化,新增validator自定义验证配置项,具体查看文档。 2.tui-round-progress(圆形进度条)组件优化,修复已知问题。 3.tui-cascade-selection(级联选择器)组件优化,修复已知问题。 4.tui-tabs(标签页)组件优化,选项卡可设置数字角标。 ===================== 【ThorUI示例V1.1.0】更新: 1.tui-org-tree(组织架构树)组件优化,可控制节点内容排版方式、节点选中状态、展开收起子节点,具体查看文档。 2.新增tui-form(表单)组件,主要用于表单验证。 3.新增tui-input(输入框)组件,原生input组件增强。 4.新增tui-textarea(多行输入框)组件。 5.新增tui-label(标签)组件,用来改进表单组件的可用性。 6.新增tui-radio(单项选择器)组件。 7.新增tui-checkbox(多项选择器)组件。 8.新增tui-switch(开关)组件。 9.新增tui-picker(选择器)组件,支持1~3级数据。 10.新增tui-landscape(压屏窗)组件。 11.新增tui-segmented-control(分段器)组件。 12.新增tui-notice-bar(通告栏)组件。 13.新增tui-alerts(警告框)组件。 14.新增tui-request(数据请求)封装,支持Promise,支持请求拦截和响应拦截,支持请求未结束之前阻止重复请求等。 15.tui-utils(工具类)优化,具体查看文档。 16.新增tui-row组件,配合组件tui-col组件使用(24栅格化布局)。 17.新增tui-tree-view(树型菜单)组件。 18.新增tui-charts-column(柱状图-css版)组件。 19.新增tui-charts-bar(横向柱状图-css版)组件。 20.新增tui-charts-line(折线图表-css版)组件。 21.新增tui-charts-pie(饼状图表-css版)组件。 22.tui-lazyload-img(图片懒加载)组件优化,修复已知问题。 23.新增tui-pagination(分页器)组件。 部分功能截图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] V1.3.0 1.新增倒计时组件:时分秒倒计时,支持设置大小,颜色等。 2.新增分隔符组件:Divider分隔符,可设置占据高度,线条宽度,颜色等。 3.新增卡片轮播:包含顶部轮播,秒杀商品轮播等。 4.已知问题修复以及优化。 V1.2.1 1.新增组件Modal弹框:可设置按钮数,按钮样式,提示文字样式等,还可自定义弹框内容。 2.修复部分已知bug。
2021-06-01 - switcha按钮false时候 边框不清晰咋办
[图片] 右边和白色背景融为一体了。这个样式可以修改吗
2019-06-21 - 小程序如何生成海报分享朋友圈
摘要: 小程序开发必备技能啊… 原文:小程序如何生成海报分享朋友圈 作者:小白 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