个人案例
- scroll-view组件内部的元素使用position:fixed定位,会随着scroll一起滚动
当scroll-view组件开启了下拉刷新功能之后,在组件内部使用了position:fixed定位的元素,期待的结果是fixed元素固定在屏幕上不会随页面滑动而移动,实际情况是在滑动页面时,fixed元素也会跟着滚动,如果把元素移出scroll-view就正常了,问题是fixed元素是在scroll-view里的组件里使用的,无法移出。 该问题只在scroll-view开启下拉刷新功能情况下出现 希望官方及时处理
2021-09-09 - 小程序placeholder-style为什么无效?
[图片] 就是第一次渲染的时候,占位字符的样式不改变,必须获取焦点,输入之后再删除输入的内容,才会起作用。 真的想知道这个bug什么时候能解决,两年了了
2021-08-25 - 小程序里面有__wxConfig这个变量吗?
小程序里面有__wxConfig这个变量吗? 可以通过这个变量判断不同的版本,是开发、体验、还是生产, 在群里看到这么一份代码 [图片]
2019-11-20 - vconsole调试
开发者在开发小程序的时候可能会碰到一些这样的问题: 问题1 1. 开发者工具上看效果没问题,但是在真机上测试不行? 问题2 2. 有用户遇到小程序功能无法使用的问题,但无法快速定位解决? vConsole开发利器和远程调试功能 针对问题1,我们提供了 vConsole 开发利器和远程调试功能,可以协助开发者在定位真机上的问题。 vConsole 的有四个Tab面板,可以先看下 Log 面板,看是否有异常信息,异常类型 thirdScriptError 是框架捕捉到的开发者的代码执行的异常,可以优先处理异常信息看是否可以解决问题。Log 面板可以看到异常出现的文件和行数。 [图片] 除了异常日志,开发者还可以通过 console.log 接口在一些关键执行路径上打日志来定位问题,这些日志会呈现在 Log 面板上。 vConsole 默认是不开启的,可以通过下面2个方法来开启: 1 开发版和体验版可以点击小程序页面右上角的...按钮打开的菜单项“打开调试”来开启 vConsole。 2 正式版没有“打开调试”的菜单项,可以先通过开发版和体验版来开启 vConsole,然后再打开正式版。或者可以预埋一个隐藏操作,比如连续点击某个 Button 多次,然后调用 API 接口 wx.setEnableDebug 来打开。 vConsole 虽然强大,但在手机上查看大量的日志信息不方便,此外,vConsole 没有断点调试、无法修改样式,定位复杂问题需要花费比较多的时间。 小程序的业务逻辑运行在 AppService 层,页面渲染在 WebView 运行,并通过微信客户端通信,因此,我们想到了可以让 AppService 运行在开发者工具,页面渲染还是在手机 WebView,两者通过网络来通信,这样借助开发者工具的调试能力,就可以实现远程调试功能。 远程调试窗口通过手机客户端扫描开发者工具上生成的二维码来打开,无需像普通手机 H5 页面调试一样,需要在手机端进行一些设置。 [图片] 打开的远程调试界面和开发者工具的模拟器的调试界面很像,需要注意的是,要在 Console 里对小程序进行调试,需要将调试的上下文切换到 VM Context 1 。 [图片] 更多的远程调试使用方法请参考使用文档。 2意见反馈能力 对于问题2,小程序的使用反馈来自用户投诉,这种情况用户无法联系到开发者。我们遇见过有小程序功能出现问题,用户无法使用,但投诉无门的情况,而这些问题,开发者也没有途径去收集以及处理,这就导致了小程序服务质量下降,用户流失。 为此,我们开发了“意见反馈”功能,当出现问题时,开发者可以引导用户使用“意见反馈”进行反馈,并上传日志来辅助开发者定位问题。操作过程如下: 引导用户进入小程序帐号详情页面,具体可以在小程序界面点击右上角...按钮,选择关于菜单。接着在帐号详情页面点击右上角...按钮,选择意见反馈菜单进入页面。页面可以上传图片和日志,建议用户上传异常情况的截图,以及勾选允许开发者使用小程序日志选项上传日志,反馈信息越详细,越有助于定位问题。 [图片] [图片] 如果觉得上面的操作步骤太麻烦,开发者可以通过在页面 WXML 添加下面的按钮,用户点击按钮可以直接打开“意见反馈”页面。 [图片] 开发者需要定时处理用户的反馈,这样才能保证小程序的质量。开发者可以登录小程序管理后台,进入左侧菜单客服反馈,就可以看到用户的反馈内容以及下载日志来辅助定位问题。 [图片] 为了保证日志信息足够详细,开发者需要用下面的接口在代码的关键执行路径上写日志。 [图片] wx.getLogManager 接口的更详细使用请参考文档。 希望通过这些小技巧,可以帮助大家顺畅地开发小程序。
2021-04-26 - 微信右上角的扫一扫,处理结果之后能不能知道二维码的来源?
微信右上角扫一扫,如何判断二维码的来源。比如来自相册,还是扫码。 [图片] 类似微信公众平台这种。只不过我们的是打开小程序。
2020-10-10 - 如何解决引入uView-ui导致主包过大问题?
如题。已经安装uView-ui官方说明进行了按需引入。 [图片]
2021-11-16 - 小程序有透传 properties 的 api 吗?
在 React 中,有这样的用法: function Parent(props) { return
; } 在 vue 中,也有 $attrs 属性: /template> 问下各位大神,小程序中有类似的 api 吗?之前大概看过一遍文档,貌似没看到。 2020-09-10 - 接口获取的小程序码一直是乱码,无法显示?
[图片] [图片] 小程序已经上线,通过二维码生成接口,获取的返回值是乱码,文档说是图片二进制数据,但是对这个返回的二进制存入本地图片,还是转为base64值给前端显示,图片都显示不出来。获取小程序二维码该怎么解决
2020-04-18 - 云函数 定时触发器该怎么用
云函数 定时触发器 我放到了目录下 云函数代码是新增一条记录 触发器是10秒 但是没有执行 到底应该怎么用
2018-12-06 - 小程序到底现在支持云开发定时推送订阅消息吗?
下载了最新的nightly build v1.0.2 1912252,[图片] [图片],每次点击按钮调阅上图云函数可以发送订阅消息,但是上图设置了触发器,想每5秒发送一个测试,结果没法应,求回答
2019-12-27 - 云开发云函数定时触发器讲解
任何可以产生事件,触发云函数执行的均可以被称为触发器,而定时触发器则是可以处理周期性的事情,比如时报、日报、周报等通知提醒,也可以处理倒计时任务,比如节假日、纪念日以及你可以指定一个具体时间的倒计时任务,除此之外,定时触发器还可以用来周期性处理一些定时任务。比如定期清理一些不必要的数据,定期更新集合内的数据。 13.5.1 定时触发器使用说明1、定时触发器的配置与部署配置了定时触发器的云函数,会在相应时间点被自动触发,云函数的返回结果不会返回给调用方。在对某个云函数使用定时触发器前,首先要保证该云函数在小程序端可以调用成功,更准确的说是能够在不传入参数的情况下在云开发控制台的云端测试能调试成功(小程序端调用有登录态)。 云函数目录里的 config.json 文件可以用来配置权限和定时触发器,如果你的云函数目录下面没有这个配置文件,可以自己创建一个,创建的结构目录如下: test //云函数目录 ├── config.json //权限和定时触发器等的配置文件 ├── index.js //云函数 ├── package.json //云函数的依赖管理 然后再来在配置文件 config.json 里进行类似如何格式的配置,config.json 严格遵循配置文件所要求的格式,比如数组最后一项不能有逗号[代码],[代码];配置文件里不能有注释等 triggers 字段是触发器数组,但是目前云函数只支持一个触发器,即数组只能填写一个,不可添加多个;name 是触发器的名字,最大支持 60 个字符,支持 a-z, A-Z, 0-9, - 和 _,必须以字母开头;type 为触发器类型,timer 是定时触发器config 是触发器的定时配置,里面为 cron 表达式(后面有介绍),cron 有七个必需字段,不能多也不能少(以下为每天早上 9 点到 12 点每隔 5 秒触发一次);{ "triggers": [ { "name": "tomylove", "type": "timer", "config": "*/5 * 9-12 * * * *" } ] } 当我们在修改触发器配置文件 config.json 后,首先鼠标右键 config.json 选择“云函数增量上传:更新文件”,然后再右键 config.json 选择“上传触发器”。这里的“云函数增量上传:更新文件”是让云函数端的触发器文件更新;而“上传触发器”则是让触发器开始生效执行。如果在云函数端的触发器没有更新的情况下就“上传触发器”来执行定时触发,文件可能没有更新,执行的还是旧的触发器内容。当我们想暂停或删除触发器时,可以右键选择“删除触发器”。 2、Cron 表达式语法Cron 表达式有七个必填字段,按空格分隔,既不能多写也不能少写,每一个字段都有它的含义对应着不同的时间点,表达式的取值都为整数且为时间制的范围(注意月在星期的前面): 第一位第二位第三位第四位第五位第六位第七位秒(0-59 )分钟(0-59)小时(0-23)日(1-31)月(1-12或三个字母的英文缩写)星期(0-6或三个字母的英文缩写)年(1970~2099 ) 下面是 cron 表达式的案例,以及我们需要了解一下 cron 表达式里的通配符以及直接写数字的含义: [代码],[代码],表示并集,在时间的表述里是“和”的意思,比如在“小时”字段中, [代码]1,2,3[代码]表示 1 点、2 点和 3 点;[代码]-[代码],指定范围的所有值,在时间的表述里是“到”的意思,比如在“日”字段中,[代码]1-15[代码]包含指定月份的 1 号到 15 号;[代码]*[代码],表示所有值,在时间的表述里是“每”的意思,比如在“小时”字段中,[代码]*[代码]表示每小时;[代码]/[代码],指定步长,在时间的表述里是“隔”的意思,比如在“秒”字段中,[代码]*/5[代码]表示每隔 5 秒;直接写数字,在时间的表述里是“第”(时间点)的意思,比如在“月”字段中,[代码]5[代码]表示每月的第 5 日;//表示每隔5秒触发一次, */5 * * * * * * //表示在每月的1日的凌晨2点触发 0 0 2 1 * * * //表示在周一到周五每天上午10:15触发 0 15 10 * * MON-FRI * //表示在每天上午10点,下午2点,4点触发 0 0 10,14,16 * * * * //表示在每天上午9点到下午5点内每半小时触发 0 */30 9-17 * * * * //表示在每个星期三中午12点触发 0 0 12 * * WED * 定时触发器的 Cron 语法没法实现每隔 90 秒钟或 90 分钟发送一次这样的效果,因为 90 秒超过了秒的时间制上限 60,而 cron 在跨位组合(比如 90 秒需要结合秒和分)上无法覆盖所有的时间;除此之外,云开发的触发器暂时不支持多个定时触发器的叠加;在 Cron 表达式中的“日”和“星期”字段同时指定值时,两者为“或”的关系,即两者的条件均生效;值得一提的是,尽管云函数的时区为 UTC+0 时区,但是定时触发器的时间还是北京时间。 13.5.2 用定时触发器调用云函数定时触发器的使用非常简单,使用开发者工具新建一个云函数比如 trigger,然后在 index.js 里输入以下代码: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { console.log(event); return event; }; 再在 trigger 云函数目录下的 config.json(如果没有这个文件,就创建一个),然后输入以下触发器,为了调试方便,我们可以每隔 5 秒触发一次: { "permissions": { "openapi": [ ] }, "triggers": [ { "name": "tomylove", "type": "timer", "config": "*/5 * * * * * *" } ] } 然后分别右键 index.js 和 config.json,选择“云函数增量上传:更新文件”,然后再来右键 config.json 选择“上传触发器”。云函数就会每隔 5 秒自动触发,相关的日志我们可以在开发者工具的云开发控制台以及腾讯云云开发网页控制台的云函数的日志里查看。 注意小程序端调用 trigger 云函数返回的 event 对象,和使用定时触发器返回的 event 对象的不同,用定时触发器触发云函数是获取不到 openId 的,同时这里有一个 Time 时间是时区为 UTC+0 的时间,比北京时间晚 8 个小时: //在小程序端调用trigger云函数之后返回的event对象 { "userInfo":{ "appId":"wxda99******7046", "openId":"oUL-m5F******buEDsn8" } } //使用定时触发器触发云函数之后返回的event对象 { "Message":"", "Time":"2020-06-11T11:43:35Z", "TriggerName":"tomylove", "Type":"timer", "userInfo":{ "appId":"wxda99********46" } } 13.5.3、定时触发器的应用定时触发器的应用非常广泛,以下仅举一些常用案例,并加以说明: 1、结合消息推送这里的消息推送不仅仅只是指订阅消息,还可以是统一服务消息、公众号的消息(可以用云函数开发微信公众号)、小程序内自己开发的通知(只是用户只有在打开小程序时才能看到)、Email 邮件等等。 比如用户订阅了日报、周报、月报等周期性的通知提醒或者我们需要给用户发送一些汇总信息,就可以固定写一个定时触发器,比如我们需要给指定用户发送工作周报,每周五晚上 17 点 30 分就定时从数据库获取数据发送消息,cron 表达式写法如下: * 30 17 * * FRI * 还可以用来处理一些倒计时(指定时间点)的任务,比如节假日、纪念日以及一些活动时间节点(定时触发器目前只能一个云函数配一个触发器,但是可以提前管理),比如我们希望在六一儿童节的早上 9 点调用云函数给指定用户群体发送消息: 0 0 9 1 6 * * 当然这样的具体时间点显得过于的不灵活,但是如果把时间与云开发数据库结合起来,灵活性就会大很多,比如在运营上每天早上 11 点是你们用户访问最多的时间点,你只需要写一个云函数,把所有的活动都在这个时间点来推送,让定时触发器每天这个时间点都触发,有活动(数据库里有数据)就会发消息,如果没有就不发(云函数调用一次的成本极低)。 如果是实时数据,我们还可以把定时触发器的频率调高,每 5 秒就触发一次,比如我们的数据库只要有最新的数据,就会发消息给指定用户。尽管不是完全的实时,但是 5 秒的频率和实时的差别也就不大了。你也可以根据情况,来调整触发器的频率,毕竟 5 秒和 1 分钟的频率给用户的体验差异并没有太大,但是成本却是 12 倍的关系。 可能你还希望在指定的时间段才触发云函数,比如你只希望在工作日、或者在早上 9 点到晚上 18 点才触发,在指定的时间段才触发既可以让触发更精准不扰民,也可以节约成本,比如下面的触发器就是工作日早上 9 点到 12 点和下午 14 点到 18 点这个时间段,每 5 秒触发一次。 */5 * 9-12,14-18 * MON,TUE,WED,THU,FRI * 从以上案例我们可以了解到,云函数的定时触发可以来自于 cron 表达式的配置,我们可以指定时间点时间段和频率来达到我们想要的效果,同时这个时间“也可以来自于数据库的配置”(伪装),意思是我们可以设置触发器的时间段或频率,如果数据库里有数据就发送,没有数据就不发送,这样就可以达到触发器在时间上的灵活性了。 2、实时获取数据有的时候我们的数据并不是来自于数据库,而是来自于第三方服务,比如前面介绍过的历史上的今天的 API,天气的 API,知乎日报的 API 等等,以及一些 webhook,这些 API 和第三方服务提供的是 json 格式的文件,API 的数据也会随时更新,但是它们更新了却并不会主动通知我们,这时我们可以使用定时触发器向这些 API 发起请求,如果数据出现更新,我们就可以将更新的数据存储到我们的数据库或者进行其他处理,比如企业微信的机器人等机器人通知服务就是如此。 当然定期获取的数据还可以是爬虫,比如我们可以定期抓取指定关键词的新闻或者指定网站的动态,当爬虫获取到了不同的数据的时候,就将最新的动态以机器人消息或者其他方式进行及时的处理。 也就是说,我们无法实时监听到第三方 API 或者网站数据的变动,但是可以用定时触发器来发起请求或者爬虫抓取数据,通过数据的变化来达到“实时”获取数据的目的。 3、自动化处理在数据库的设计里,我们就提到有时候需要对数据库里的数据进行定期的备份与删除等清理维护工作,比如超过一定时间的日志,具有很强时效性的活动数据,以及为了性能考虑而做的虚假删除(数据库性能与优化有介绍)等,毕竟数据库有一定的存储成本而且过多无用数据也会影响数据库的性能,我们可以写一个云函数用定时触发器来执行此类任务。 我们还可以在用户并发比较少的时间段(比如凌晨几点)来处理一些比较耗云函数、数据库性能的任务,比如图片的审核与裁剪、缩略等处理,用户评论是否包含敏感词汇(尽管经过安全处理,但是有时候我们还会设置特别的敏感词),数据的汇总,云存储里废弃文件的删除,用户信息是否完整等等。 也就是说,结合定时触发器,我们可以实现一些任务的自动化处理。 4、密集型任务分流我们知道云函数在处理一些复杂性的任务时是有一些限制的,一是执行时间的限制,建议在设置时执行时间一般不要超过 20s,最长不要超过 60s;二是并发的限制,云函数最大的并发为 1000;三是云函数在查询数据库时一次可以获取最多 1000 条的数据,面对这三个限制,我们应该如何处理密集型的任务呢,比如发送 100 万封邮件,导出几百万条数据到 Excel,发送十万级的订阅消息或消息等等,这个时候就可以使用到定时触发器来处理了。 借助于定时触发器,我们可以将需要耗时较长、对并发要求较高以及数据库请求等的任务进行分批处理,比如我们要给 100 万人发邮件:云函数发起数据库请求,一次只请求 1000 条未发送过邮件的用户(用 where 条件查询某个字段,比如[代码]status:false[代码]),然后将邮件发给 1000 个人(可以参考前面的邮件发送),发完邮件并对这 1000 条数据进行标记(比如使用更新指令将 status 改为 true),这样下次查询未发送过邮件的用户时,就不会重复发送了。通过定时触发器,每 2 秒执行一次发送任务,几十分钟就可以处理完任务。
2021-09-10 - 小程序流量主运营之ECPM怎么提高
目前小程序流量主共有8个广告类型:banner、视频、插屏、前贴、激励、格子、模板、开屏。 首先说下ECPM是什么意思。ECPM指的是每一千次展示可以获得的广告收入。 ECPM是取决于用户点击广告次数,也就是1000次曝光里面点击广告的人数越多,ECPM值越高,不单单是曝光1000次的收益。 也就是说如果你曝光了1000次banner广告,但是没有用户点击,那么你banner广告位的ECPM就是0。 [图片] 然后我们说下今天的主题,ECPM怎么提高。 1、首先你流量主小程序里面的所有广告,都是广告主去投放的。广告主可投放的类型有很多种,根据广告主投放城市、行业等决定。 1-1、比如我在呼和浩特,我把广告投放到呼和浩特,如果我按竞价CPC的投放,一个点击最低1.06,除了微信要扣除流量主收益的50%之外,剩下的50%就是你的收益。 [图片] 1-2、我按合约广告投放,1000次曝光我花费的是15元,微信扣除50%,你的ECPM就是7.5,这么说懂了吧。 [图片] 1-3、所以基于广告主广告投放,你要选好广告,尽量避免第二条里面的1000次曝光这种CPM广告主,这种广告主一般都是大品牌,不在乎你点不点,就是让你看。所以我们要有犀利的眼光去判断屏蔽哪种广告位。 [图片] 2、ECPM的高低取决于你的小程序用户分布年龄 比如30岁IOS用户就是比20岁的IOS用户质量高,30岁左右的IOS用户属于高价值用户,对电商、游戏等付费能力会更强,会比20岁的人群要高,所以我们在开发小程序的时候,尽量往这个年龄段靠。还有30岁的女性电商用户,都是ECPM拉高点的关键用户群体。(此处仅仅是举例分析,无年龄歧视) 所以我们再开发、购买小程序时,选题材、素材的时候可以尽量往这个年龄段靠一下,ECPM会高一些。 3、广告设计体验 广告设计是一门艺术,但是不能违规。要优雅的设计广告,让用户看着舒服,看的清楚,主动去点击广告,这也是广告主投放广告的目的。最近有多人私信和我说买1000多个手机,弄1000多个微信去刷广告。真的是想瞎了心了,如果你是广告主,你投放出去的广告希望用户这样点击吗?再说了,微信广告的风控机制是你承受不住的,微信也得保护广告主投放的利益。如果全部都是这种样点广告,微信生态早就活不下去了。不要想着投机取巧,踏踏实实做流量才是王道。 [图片] 4、站外引流提升ECPM 所有的APP都缺流量,不要以为微信是国民产品就不缺流量。微信小程序也喜欢站外流量引进来,所以大家在引流的时候,可以从其他APP引流,这样你的ECPM也是很高。 [图片] [图片]
2020-12-19 - 小程序审核被拒的情况汇总
常在河边走,哪有不湿鞋,上线了几十个小程序,遇到审核被拒的情况数不胜数,所谓是久病成医说的就是我,😄 从今天开始立贴,具体举例审核被拒的几种情况,以及怎么破解 1、 为避免您的小程序被滥用,请你完善内容审核机制,如调用小程序内容安全API,或使用其他技术、人工审核手段,过滤色情、违法等有害信息,保障发布内容的安全。 解决方式: 增加安全接口校验,具体可以参考下面文档 https://developers.weixin.qq.com/community/develop/doc/00004843288058ed4039d223951401 https://developers.weixin.qq.com/community/develop/doc/000cc03084c67888c97992e4756809 2、 小程序内容不符合规则: 你好,你的小程序涉及金融相关,属个人主体小程序未开放类目,建议申请企业主体小程序。请根据上述原因对小程序进行修改,并重新提交代码审核。 [图片] 解决方式: 由于个人主体小程序不能涉及金融,如果确认没有涉及,通过反馈渠道进行沟通。 3、 1:小程序内容不符合规则: (1):你好,你的小程序实际展示为测试商品/内容,请上架正式运营商品/内容后再提交代码审核。 请根据上述原因对小程序进行修改,并重新提交代码审核。 [图片] 解决方案 一般这种情况是由于小程序不完善或者小程序内容不完善,如果小程序内容比较完备,就需要把小程序内容做的正式一些。 4、 1:小程序内容不符合规则: (1):你好,你的小程序提供内容为在线游戏,请注册一个新账号,同时选择游戏类目。 请根据上述原因对小程序进行修改,并重新提交代码审核。 若对上述原因无法理解,可前往反馈页面进行反馈。 [图片] 解决方案 由于是小程序不能提交交互性强的游戏产品,需重新注册小游戏,重新开发。 5、 [图片] 解决方案 个人主体小程序不能涉及视频服务,所以这个无解,如果想继续发布只能走企业主体 6、 1:小程序内容不符合规则: (1):小程序页面内容涉及信息发布平台功能,属于个人主体类型未开放类目,建议申请企业主体类型小程序。 请根据上述原因对小程序进行修改,并重新提交代码审核。 若对上述原因无法理解,可前往反馈页面进行反馈。 [图片] 解决方案: 由于个人主体小程序不能涉及UGC,所以涉及这个功能基本是要被阉割掉的。无解 具体可参考下面文档 https://mp.weixin.qq.com/s/UgMTOwPGr-8GxzrdL-guHQ 8、 1:小程序内容不符合规则: (1):小程序页面内容涉及信息发布平台功能,属于个人主体类型未开放类目,建议申请企业主体类型小程序。 2:小程序功能不符合规则: (1):你好,小程序帐号登录功能暂未符合登录规范要求,包含但不限于存在:尚未体验完整服务功能即要求用户授权个人信息登录,帐号登录环节未能给用户清晰提供可取消/拒绝的选择权利等,请整改后再重新提交审核。参考文档 请根据上述原因对小程序进行修改,并重新提交代码审核。 若对上述原因无法理解,可前往反馈页面进行反馈。 [图片] 解决方案 由于小程序用户授权审核规范在9月1号改变,具体请参考相关文档,主旨就是在用户体验完小程序之前不要授权 具体文档请参考 https://mp.weixin.qq.com/s/X3XSEKRYmSFv7kM5sxr09A 9. 小程序广告审核驳回原因,这个一般是由于广告被遮挡造成的,可以通过样式布局修改来完成。 [图片] 10. 您的小程序实际展示位测试商品/内容,请商家正式运营商品后再提交代码审核 [图片] 11. 1: 你好,你的小程序涉及提供用户自行生成内容(文字、图片、音/视频)的记录、分享,需补充:社交-笔记类目。或自查代码,确保包括前端展示、小程序代码等整体均移除上述内容,再提交代码审核。 12 [图片]
2020-03-30 - 小程序如何生成海报分享朋友圈
摘要: 小程序开发必备技能啊… 原文:小程序如何生成海报分享朋友圈 作者:小白 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 - 小程序云开发如何区分是在开发环境还是生产环境?
- 需求的场景描述(希望解决的问题) 有没有环境参数来区分现在运行的代码是在生产环境还是开发环境下? 用伪代码来表示一下 [图片] 这样的话配合env这个参数就不用每次手动切换环境了吧?每次手动切换环境有点反人类呀。。。 [图片]
2018-09-12