javascript实现ArrayBuffer转字符串微信小程序蓝牙数据转换

/** * ArrayBuffer转字符串 * @param {ArrayBuffer} e 需要转换的ArrayBuffer类型数值 * @param {function} t 转换成功后的回调 */ getUint8Value(e, t) { for (var a = e, i = new DataView(a), n = "", s = 0; s < i.byteLength; s++) n += String.fromCharCode(i.getUint8(s)); t(n); }效果图显示: 微信小程序蓝牙通讯wx.onBLECharacteristicValueChange返回的value值是 ArrayBuffer类型,需要转换前端才能进行naf

April 26, 2019 · 1 min · jiezi

微信小程序怎么使用icon

小程序怎么使用icon第一步 下载icon我选择的图标库是阿里图标官网, 加入购物车 第二步 转换ttf因为小程序的wxss文件的font-face的url不接受http地址作为参数,但可以接受base64,因此需将字体文件下载后,转换为base64,然后引用 进入https://transfonter.org/平台点击Add fonts,添加iconfont.ttf勾选Base64 encode点击 convert 完成转换 按照上图1,2,3的步骤 转换完成, 点击Download 第三步 在微信小程序中使用icon解压 第二步中的文件夹找到stylesheet.css 文件 打开第一步中压缩包的iconfont.css,把里面圈红的部分(即fontface部分我们不需要啦)复制到stylesheet.css 修改后的stylesheet.css 修改stylesheet.css的文件后缀为wxss,即stylesheet.wxss把stylesheet.wxss放进微信小程序的公共文件里面,然后把整个文件import到页面的样式表里 在wxml页面引入使用 完结原文地址

April 24, 2019 · 1 min · jiezi

WordPress版微信小程序35版发布

最近花时间对WordPress版微信小程序做了一些完善和调整,修复不少程序的问题。一个程序的完善是持续和渐进的,没有最好,只有更完善。虽然会采纳一些用户的建议和意见,但我会从一个产品角度去考虑,哪些功能应该加,哪些需要舍弃,如果你需要更专业的解决方案,可以参考我的专业版小程序-微慕小程序. WordPress版微信小程序3.5版本的更新内容说明如下: 1.调整小程序海报小程序原来的海报程序存在以下的问题: 1)图片没有裁剪,导致海报的图片发生变形。2)海报的文字调整起来比较麻烦。 为了解决上述问题,我采用一个开源的小程序组件:wxa-plugin-canvas可以比较好的解决这两个问题,同时为了增加海报的转发量,把转发者的头像也放到海报里,增强互动性。新生成的海报样式如下: 更新海报程序需要注意以下问题: 1)downloadfile域名设置需要在微信小程序的管理后台设置downloadfile域名,域名需要包括以下三个域名: A) wx.qlogo.cn :用于显示转发者的头像 B)首图地址的域名 :用首图作为转发的图片,必须设置首图的地址的域名。 C)特色图片地址的域名:用特色图片作为转发的图片,必须设置特色图片的地址的域名。 如果上述域名设置错误或没有设置,将会导致生成海报失败。 如果需要修改海报里的文字、图片样式、布局等,请看detail.js里的creatArticlePoster方法 2.完善用户信息用户信息的完善主要是两个部分:1) 在wordpress的后台增加微信用户的头像,使用微信用户的昵称作为wordpress用户的昵称和显示名,让网站的管理者更方便管理用户。 2) 在小程序端的“我的”页面的调整。在“我的”页面增加了用户的角色,显示当前用户在wordpress网站里的角色;增加了更新用户信息和退出登录的功能。 更新用户信息:如果用户更新了微信里的昵称或头像等,可以使用这个功能更新用户信息。退出登录:如果用户想退出当前小程序的登录,可以点击退出。 特别提示:如果发生登录失败的情况,可以通过退出登录的功能清除缓存,再次登录。 退出登录的图标如下图所示: 3.增加评论审核在以前的版本里,在小程序里对文章进行评论后,会直接显示的评论的内容,不需要经过站长审核。在这个版本里,增加了对评论是否审核的选项。开启后,所有的小程序端的评论都必须经过审核后才会显示(在网站端也不会显示)。这个选项在插件REST API TO MiniProgram里设置,如下图所示: 4.调整支付代码在插件里的支付代码是在的微信官方提供的微信支付示例代码基础上修改的,而这个代码不少的具有支付功能的插件也用了,因为使用了同一套代码,可能会导致支付的失败或者插件的冲突,为了解决这个问题,我调整了微信支付代码的类名规则,避免插件的冲突。 5.功能完善1)修复获取文章是否点赞的bug.2)评论的数量过滤掉未通过审核的评论.3)排行数据里过滤非文章类的内容。4)增加sitemap.json,支持微信优先收录。5)修复腾讯视频过多导致的加载失败的问题。 wordpress版小程序及配套wordpress插件下载提示:如果在开发工具里看到提示“无效的 appJSON"window"”,不用理会,在app.json文件里加qbDebugKey是为了调试qq浏览器小程序的,不用理会,这个提示不会影响程序的运行。 小程序下载地址:版本3.5 https://github.com/iamxjb/win... 如果因为某些原因github无法访问,可以选择以下镜像地址: 1.https://git.oschina.net/iamxj... 2.https://code.aliyun.com/iamxj... 3.https://coding.net/u/xjb/p/wi... 4.https://gitlab.com/xiajianbo/... 插件下载地址:版本1.5.2 插件的更新支持wordpress后台直接更新 Wordpress官方下载地址:https://wordpress.org/plugins... github站下载地址:https://github.com/iamxjb/res... 有关开源版,你有什么好的建议,欢迎告诉我,我们一起来完善这个开源项目。 如果本程序对你有所帮助,请不吝在github上Star。 谢谢你阅读这篇文章,谢谢你对我的支持。

April 23, 2019 · 1 min · jiezi

模版消息智能推送!我们教你发得更快更多更省

「模板消息」能力,几乎是小程序触达用户的唯一渠道。有了它,运营人员才有足够的发挥空间,来提高用户的留存,转化。微信为了防止有人用模板消息频繁骚扰用户,推出了一套严格的监督机制。一旦发现有人滥用,立刻对模板消息进行封禁处理,这将直接影响线上业务。然而受人为因素影响,模板消息的审查有很大的不确定性,运营者即使遵照微信运营规范执行也有可能被误伤封禁。以下是模版消息被封的一些常见原因。模版消息被封的常见原因:同一时间段大批量发送,被系统判定为行为异常。推送内容违规或推送内容与用户需求不符,导致用户举报。推送次数过于频繁,一天内向同一个用户推送多次模板消息,导致用户举报。简单总结,主要还是因为模板消息推给了不该推的人,被人当垃圾邮件一样处理,自然容易被封。「知晓推送」就针对以上场景做了功能优化,其新推出的智能推送服务可以助力小程序高效匹配目标用户,极大的降低模板消息被封的概率,为小程序业务的稳定运行再上一层保险。除此之外,「知晓推送」还解决了你的难题:运营灵活性差,每次推送内容变更都得通过修改代码来实现。formid 限制太大,收集麻烦,每一次业务变更都需要重新埋点。推送成本高,后端服务需要单独开发,还不能保证消息推送的稳定性与效率。「知晓推送」提前尝鲜5 月 8 日,我们将正式推出「知晓推送」服务,帮助运营者处理好粉丝转化、消息推送、数据分析等多个层面的麻烦事,真正解决复购难、留存差的问题。四大核心功能助力实现小程序用户的高效转化:1.在线发送完全可视化操作,提前预览发送效果。让模板消息推送变得和发微博一样简单。使用在线推送服务,运营人员不用在对着代码一脸懵逼了。2.自动收集 formid 组件让 formid 不足导致无法推送模板消息的状况成为过去式,两行代码即可完成接入,业务变更也无需重复埋点。3.智能推送支持根据自定义规则过滤出与小程序匹配的目标用户,同时可以设置同一批模板消息的推送次数与单次发送数量,避免骚扰用户,实现精准推送,高效转化。4.便宜限时福利大放送「知晓推送」正式上线前完全免费,服务上线当天还有额外福利。5.8 日前注册的用户都可获赠价值不少于 198 元的资源包(详询,微信:minsupport3)哦!抓紧时间注册吧~

April 19, 2019 · 1 min · jiezi

小程序ColorUI框架完成3d轮播效果

一、效果预览二、ColorUI克隆地址https://github.com/weilanwl/C…三、使用方法步骤一需要在红框文件中引入篮框的两个文件如下图:@import ‘colorui.wxss’;@import ‘icon.wxss’;步骤二打开下载的文件 打开demo目录3d轮播代码在swiper页面上(卡片式轮播效果),直接复制就好了四、总结其实做这个效果我踩了很多坑 在网上找大牛用微信swiper插件改出的3d轮播效果 已经使用上了 被领导说使用卡顿 又花时间重新想的这个办法 优点:使用流畅不卡顿 缺点:占太多内存 因为微信上传代码规定不能超过两兆 要是本身小程序内容很多可能就不能用这个方法了

April 18, 2019 · 1 min · jiezi

【已解决】mpvue使用微信小程序map中的bindregionchange失效问题

<map @regionchange=“regionChange” @begin=“regionChangeBegin” @end=“regionChangeEnd”><map>官方微信小程序只写bindregionchange即可,但是mpvue需要上面三个方法都需要写。如果不写@regionchange,则不会触发后面两个方法@begin和@end

April 18, 2019 · 1 min · jiezi

码code | 拒绝996,不用服务器也能高效开发小游戏

不久前,GitHub上掀起了一场反对996运动,国内关于“996”工作制的讨论热度也在持续上升中。如何才能避免加班,在小程序开发中少走弯路?今天我们向大家推荐一个用云开发开发小游戏的方法,摆脱对服务器的依赖,提高开发效率。转载来源:云开发x原作者:代长新前言我是代长新,来自上海享物说,主要负责游戏客户端研发。《乐享花园》是我们在小游戏领域的第一个实践。这个游戏,从立项到做完,准确的说是客户端做完,我们一共用了3天的时间。但是,当时我们种花浇花、领水滴任务都是通过浏览器缓存实现的,如果要上线还要等服务端人员到位,否则玩家清理一下手机,自己种的花就没了。但等我们服务端人员到位,再到游戏上线,就是几周以后的事情了。小游戏开发之痛:无法摆脱对服务端的依赖相信,这也是大部分小游戏开发时会遇到的问题 —— 功能很简单,但就是摆脱不了对服务端的依赖。如下图:具体来说,小游戏对服务端的依赖主要有以下两个方面:1、微信接口只支持在服务端调用这就意味着,我们必须为这些接口架设一个中转服务器。如果没有这个中转服务器,我们就没法做用户登录,没法获取用户头像、名称信息,也拿不到access_token,更没有办法调用其他微信接口,如内容审查。2、游戏功能实现需要服务器开发对于很多小游戏来说,我们唯一用到服务端的地方就是,储存当前的关卡进度,展示一个世界排行,就可以了。而想要实现这么一个简单的需求时,你会发现,隔行如隔山。如何用云开发解决小游戏在服务端痛点?1、云函数实现微信接口调用曾经,我想过绕开服务器,直接通过客户端请求微信接口,结果踩了一个坑。当时做的是聊天功能,需要对玩家发送的消息进行内容审查。我看完了接口文档,就跑过去跟服务器同学说,内容审查我这边全部做掉就可以,他那边不需要做额外的处理。等我们调通,上了体验版,一打开报错,我才想起来,这个接口文档的上面,有一行小字,而且颜色是灰色的,上面写着:此接口应在后端服务器调用。第一次看到这句话,还以为它只不过是一个警告,所以根本没把它放在心上,哪知道它居然是一个error!而在这之前,我还特意做了一些我认为比较人性化的设计,比如使用这个接口需要一个密钥,这个密钥是有有效期的,当密钥过期的时候,我会把玩家发送的内容保存起来,向后端拉取新的密钥后,再发送出去,这样对于玩家来说,整个过程是无感知的。而现在则意味着所有这些都要服务器去实现了。后来,我通过云开发来实现多有接口调用,事情就简单多了。就拿登录来说吧。由于云函数具有微信天然鉴权的能力,可以直接返回openid,这一点对做登录确实很方便。乐享花园需要和享物说平台打通小红花积分数据,所以需要用户的unionid信息,这一步也是在云函数中实现的。还有access_token,就是刚才用到的密钥,为什么要单独说这个密钥呢?因为它会用到云函数特别有意思的功能,那就是定时触发器。由于这个密钥是有两个小时有效期的,我们设定一个小时间隔定时刷新,保存到数据库中,用的时候直接从数据库中取出来就可以了,这样可以保证密钥永远是不过期的。通过云开发,为微信接口准备的中转服务器就不需要了;更重要的是,服务端与微信接口分离,无需关心客户端场景。不管这个客户端,是来自h5游戏,还是来自小游戏环境,对于服务端来说,都是一样的,再也不需要为客户端提供这样那样的权限接口。2、云函数+数据库,实现全局排行榜功能正如前面提到的痛点,小游戏开发对服务端的另一个依赖是游戏功能的实现。对于大部分小游戏来说,我们唯一用到服务端的地方就是:保存用户数据,展示一个世界排行榜。而如果用传统服务器实现这些功能的话,你会发现需要了解的后端架构知识非常庞大。有次,我到服务端同学的旁边,原本是打算diss他的,因为我功能已经写完了,他还不知道在忙些什么东西。这时我看到他在做什么呢 —— 一边写dockfile文件,一边写linux命令,一边打开Postman调试,完了后发邮件给运维说要执行几个mysql语句。而所有这些都还没有涉及到他要开发的游戏功能!所以说,一门后端语言从会写,到可以放到生产环境中,是两个完全不一样的概念。云开发提供了数据库、云函数、云存储,通过这些能力,我们完全可以取代服务器来实现游戏功能。在《乐享花园》里,我们通过云开发实现了全民成语接龙这个游戏功能,并且只用了2个云函数就实现了我们对服务器的全部需求。这里简单介绍一下这两个云函数:第一个云函数是用来展示世界排行榜。由于云函数拉取数据库的条目是有限制的,最大是100条,其实这个已经足够满足需求了;当然了,你要说我们的客户端很牛,性能不是问题,数据什么的先给我来个2000条,也不是不可以,这里做个处理就可以了。另外在检索数据库数据时,这个过程会很慢,一定要记得,在后台添加数据库索引,可以把这个过程理解为通过磁盘换取CPU计算。这样速度会快很多。第二个云函数是用来上报玩家数据。这个比较简单,一行代码搞定。就这样从微信接口调用,到游戏功能开发,一款不需要服务器的小游戏就全部开发完成了。小结其实,云开发可以使用的业务场景,还有很多,比如,绕过微信https域名请求限制 存放游戏的全局设置 保存玩家的个性化数据 。。。作为开发者,也希望云开发未来,可以提供更多的业务场景支持,比如,websocket,刚才说的聊天服务器,就可以省掉了; 帧同步,实时对战类游戏的实现,就不再有压力;日志服务,方便统计,和排查玩家的行为,方便游戏迭代优化;大数据统计分析,可以做一些事件漏斗等等~这样小游戏的研发门槛,就降得很低很低了!了解更多小程序开发相关内容,欢迎微信扫描下方二维码关注「微信极客WeGeek」公众号,共筑微信生态。

April 17, 2019 · 1 min · jiezi

iOS不使用微信sdk,直接打开小程序

直接贴代码iOS审核不让有支付代码,所以只使用轻度功能的话,可以不使用微信SDK。使用前需要先去微信开放平台绑定。我的封装/** * 开发前需要到微信开放平台把App绑定小程序,然后在小程序的管理员微信上点击同意绑定,就可以转跳了 * 字段解释: * @appid:小程序appid * @username:‘gh’开头的小程序公用id * @path:小程序需要打开页面的路径 * @type:0是正式版,1是开发版,2是体验版 **/-(void)jumpToWechatMiniProgram:(NSString *)appid ghId:(NSString *)username path:(NSString *)path type:(NSString *)miniProgramtype{ NSString *mPath = [path stringByReplacingOccurrencesOfString:@"/" withString:@"%2F"]; NSString *url = [NSString stringWithFormat:@“weixin://app/%@/jumpWxa/?userName=%@&path=%@&miniProgramType=%@&extMsg=",appid,username,mPath,miniProgramtype]; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]options:@{} completionHandler:^(BOOL success) { NSLog(@“跳转成功”); }];}调用-(IBAction)jumpWithUrl:(id)sender{ [self jumpToWechatMiniProgram:@“wx8888888888888” ghId:@“gh_88888888888” path:@“pages/index/index?session=自己定的参数” type:@“2”];}获取微信sdk的其他功能iOS中,app互相转跳走的都是openUrl这个接口,通过scheme就可以转跳到目标程序,但是scheme是不审核的,可以随意指定,所以我们可以通过写一个假微信(scheme是weixin),来拦截微信SDK的启动请求,从而获取到对应的启动字符串,然后自己拼接字符串即可。伪造微信在info.plist里添加(注意缩进不要弄错了,最好在模拟器上试,如果安装了微信,是不会跳到我们的假微信里的。):<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>weixin</string> </array> <key>CFBundleURLName</key> <string>1111</string> </dict> </array>看不到源码页面的话,右键info.plist,选择Open As -> Source Code就能看到了,改完了切回Property List模式,不报错就说明格式是对的。获取转跳参数在appDelegate.m里增加:// 这方法显示已经废弃了,但是只是获取参数还是可以的- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{ //显示截取的urlscheme UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@“接收到的urlScheme” message:url.absoluteString delegate:nil cancelButtonTitle:nil otherButtonTitles:@“确定”, nil]; [alert show];// 复制到剪贴板 UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; pasteboard.string = url.absoluteString; return YES;}然后就能看到弹窗里的urlscheme就可以了,只要拼接出一个一样的urlscheme,就可以启用微信SDK同样的功能。 ...

April 12, 2019 · 1 min · jiezi

1000个奖学金名额,只为热爱科技的「妳」们

在前沿科技领域,许多女性开发者同样在发挥着自己的价值,她们有着一个共同的名字——“粉红极客”。本期我们邀请了三位女性开发者,分享她们成为“粉红极客”的故事。她们对程序开发都具有浓厚的兴趣,并且忠于自己的选择。那么,她们是如何实现蜕变的呢?Yancey :“不被传统界定,兴趣是我们最好的老师”Yancey 之前是一名日语老师,但是重复性劳动并没有让她获得成就感,后来便开始自学编程与前端开发。目前,现代人对手机越来越依赖,而开发小程序和开发前端网站虽然非常类似,却能够更好地解决生活所需,所以她开始学习微信小程序。对于她而言,最大的挑战是入门计算机基础。刚开始学习计算机的时候,对这个领域的理解是一片空白。幸好计算机是门实践课,她说:“不断的尝试,不断的请教,不知不觉就找到了感觉。”从语言学的感性,到科技的理性,Yancey 认为两者是互通的。计算机的存储能力与大脑的记忆是类似的,“其实计算机就是一直在模范大脑,人工智能不就是最好的例子嘛。所以学习心理学也让我之后转型成程序媛变得轻松了许多。”Yancey说。科技一直在发展,技术的进步要求人也要一直进步。对此 Yancey 的经验是,在实践中不断尝试、不断请教,摆脱传统的观念束缚,让兴趣成为我们最好的导师。Hawli :“每一次华丽的转身,只因你当初做了正确的选择”从心理学专业转行到IT服务业的 Hawli ,曾做过很多类型的工作并取得一定的成绩,但总觉得生活缺乏了一种感觉,她现在回想起来,就是缺了一种“确定性”。她说,“数据作为一种理解和透析世界的工具,代表着一种趋近于确定的世界,降低不确定性所带来的风险。”对于 Hawli 而言,技术学习最大的困难在于过去对编程基础与思维方式的不了解。因此前期她曾踩过很多坑,有时候卡在一些很小的点上一直过不去。幸亏她提前做了充足的准备,最终靠着兴趣坚持下来。现在,Hawli 在 IT 服务行业承担通讯类的数据分析工作,参与了几个大型项目,管控着小组项目的进展,每天都在思考着如何发挥数据的价值。Hawli 说:“学习必须像搭积木一般,有适配自己的进阶路线和明确的台阶,才能让新手不至于走了几步就掉链子。”她最大的感悟是,当初花费时间精力所学的技能,在日后的工作中都用得上,时间最终会告诉我们答案。Kris :“平时最大的消遣,是学习前沿科技”Kris 从小就对编程方面感兴趣,又因为IT行业的就业面和收入比较好,所以走上编程的道路。从安卓到数据分析和前端,再到后来的机器学习、深度学习、商业数据分析,她对学习的“快感”,来源于不断突破舒适区,并从中获得成就感。对于今后的职业规划,Kris 的打算是往金融数据、风险控制这方面发展。“我大概给自己一年左右的准备期吧,已经储备了很多的编程知识,现在再来夯实一下法律知识,以后总有用武之地的。”关于如何平衡学习、生活、工作这几方面的比重,她认为学习兴趣占了很大一部分,但更重要的还是自律,因为自律能够保证长效的学习效率。除此之外,思考工作中的重点优先次序,对提高工作效率也很有必要。对于她而言,“终身学习”这个词就是要发掘自己的兴趣,并不停地学下去。“没有功利心地学,用完善自身、追求更多知识的心态,我觉得会有不一样的收获。”通向科技的道路上,她们有选择与彷徨,有思考与感悟。每一位女性开发者怀着自己的信仰,不断确证和坚定自己的选择。2018 年 3 月微信联合 Udacity 推出纳米学位项目——微信小程序开发课程,已累计获得近 20 万名关注者,零基础的学员也参与了其中。为了帮助更多对科技领域感兴趣的女性实现梦想,微信推出专为支持女性开发者的「粉红极客」科技教育奖学金,资助 1000 名女性开发者申请此课程,鼓励更多女性参与前沿科技发展。扫描下方二维码,填写并提交相关信息,即可申请奖学金。申请日期截止至 2019 年 5 月 8 日,限量 1000 名。了解更多小程序开发相关内容,欢迎微信扫描下方二维码关注「微信极客WeGeek」公众号,共筑微信生态。

April 12, 2019 · 1 min · jiezi

微信小程序调用moveToLocation失效问题【已解决】

上图所示,mapUpdated表示地图加载完成后,再初始化数据。为什么moveToLocation失败?第一:可能你的ID取错了;第二:调用moveToLocation时,必须需要调用wx.getLocation,并且用户授权后,才能使用moveToLocation方法

April 12, 2019 · 1 min · jiezi

不止微信、支付宝!一文带你了解所有小程序平台

小程序的平台越来越多了,开发者的精力也越来越分散。事实上,这些平台有怎样的特色?他们有怎样的代表作品?他们有几个入口?开发成本高吗?他们有给开发者怎样的扶持政策?一文为你解析小程序六大平台。01 微信关于微信小程序2017 年 1 月 9 日,微信小程序正式上线。经过三年的发展,它俨然成为了开发者最关注的小程序开发平台。在 2018 年 8 月,马化腾就透露已有 150 万开发者加入了小程序的开发队伍,小程序应用数量超过 100 万,覆盖 200 多个细分行业,日活用户达到 2 亿。而在 2018 的全年财报也表明,微信小程序日活跃账户数增长迅速,用户人均日访问量同比增长 54%,而中长尾小程序也占到了小程序日均总访问量的 43%。在小游戏中,微信已有单月流水过亿的杰出小游戏作品。10 款内购道具月流水过千万小游戏变为了 10 款,11 款广告月流水过千万小游戏。头部小游戏的成功已经无需证明,但从整个行业来看,三十天留存率 43% 亦足够优秀。微信里的社交关系让小程序拥有了更多的吸引力,小程序则让微信的「操作系统梦」成为现实。在微信中,小程序拥有着超越公众号之外的连接和服务能力,也能将公众号、群聊、对话、朋友圈串联起来,为用户提供更延展也更丰富的服务。代表作品小程序:拼多多、跳一跳、享物说、小打卡、轻芒、小年糕、乘车码、粤省事等小游戏:跳一跳、物理弹球正版、头脑王者、欢乐斗地主、星途 WeGoing、羽毛球高高手等主要入口微信聊天下拉的小程序界面是小程序最重要的入口之一。聊天的小程序卡片、公众号推文中的、分享小程序、四散的小程序都是小程序的重要入口。据不完全统计,微信小程序已有 60 余个入口。开发成本小程序的主要开发语言是 JavaScript ,小程序的开发同普通的网页开发相比有很大的相似性。对于前端开发者而言,从网页开发迁移到小程序的开发成本并不高,但是二者还是有些许区别的,毕竟小程序还处于一个封闭的程序运行环境。开发文档:https://developers.weixin.qq….扶持政策在小程序和小游戏上,微信都推出了不同的扶持计划。????小程序腾讯云在微信公开课上宣布推出的「小程序 · 云开发」资源扶持计划价值 10 亿。该计划分为长期普惠、进阶扶持和高阶扶持三种机制。腾讯云提供的基础版套餐、专业版资源、旗舰版资源都是扶持奖励。????小游戏创意小游戏鼓励计划:创意标识、初始用户、分成激励、创意保护。种子用户计划:针对新游,微信将无差别分配等额的种子用户。在第一批无差别等额用户发布后,微信会持续观察所有小游戏的数据,根据平台的数据模型,给新游戏第二批的种子用户。在这两个计划之外,微信提供的应收转投放和应收预提将帮助开发者快周转。同时对普通开发者的抽成比例进一步降低。月流水 50 万以下的小游戏免抽成。月流水在 50 万以上的部分,游戏内购抽成 40%;日流水在 100 万以下的部分,广告抽成 50%。02 支付宝关于支付宝小程序2018 年 9 月 12 日,支付宝小程序正式上线,主要活跃在商业和生活生活领域,暂不支持小游戏(严格来说目前仅支持支付宝官方开发游戏类小程序,如「叠叠乐」)。据悉,截至 2019 年 1 月,支付宝小程序的数量已增至 13 万,日活跃用户数突破 2.3 亿,平均 7 日留存率为 43.26%。目前,支付宝小程序对企业账号及个人账号(已开启个人开发者限量公测)开放。支付宝的实名认证并与「钱」挂钩让用户和商家都更具信任感;而有独特优势的信用体系则让支付宝在与押金、租赁相关的场景大放异彩,比如共享单车、共享充电宝等支付宝小程序,在接入芝麻信用后为用户提供了免押金等便捷服务。代表作品免押金类:来电、内啥、人人租机、哈啰出行等其他:好食期、青团社兼职、航旅纵横、淘票票电影等主要入口主要为支付宝 app 首页顶部搜索栏,以及首页下拉呼出「小程序收藏」。更多支付宝小程序入口,可以查看 ???? 《细数支付宝小程序的 35 个入口,我们终于找全了》。开发成本与微信小程序的整体架构基本一致。如果你想将微信小程序迁移至支付宝,只需重命名小程序文件后缀、一些事件函数和部分 API 即可。开发文档:https://docs.alipay.com/mini/…扶持政策3 月 21 日,在 2019 阿里云峰会 · 北京上,阿里巴巴旗下的阿里云、支付宝、淘宝、钉钉、高德等联合发布「阿里巴巴小程序繁星计划」:提供 20 亿元补贴,扶持 200W+ 小程序开发者、100W+ 商家。凡入选「超星」的小程序,入驻支付宝、淘宝、钉钉、高德后还能得到流量重点支持。03 百度关于百度智能小程序2018 年 7 月 4 日,百度智能小程序正式上线,主要运行在百度 app 上,主打「体验、流量、智能、开放」四大特点。据了解,12 月时百度 app 已为小程序开放了 40 多个流量入口,并为小程序开发者提供了超过 60 个 AI 接口和超过 20 个 NA 化组件。百度智能小程序支持小游戏开发,不过目前支持的主体主要为媒体、企业、政府及其他组织,个人主体类型开发者暂时无法入驻。代表作品AI 相关:爱说唱、长隆 AR 动物园、AI 分诊助手等其他:小红书、爱奇艺视频、几何大逃亡、贴吧等主要入口可以从百度 app 的搜索栏联想、搜索结果、底部菜单「我的」中进入百度智能小程序。更多百度智能小程序入口,可以查看 ????《百度的搜索流量终于全面开放!新入口就在王牌产品主场景》。开发成本与微信小程序的整体架构基本一致。如果你想将微信小程序迁移至百度,只需使用百度的「搬家工具」转换已有的微信小程序即可。开发文档:https://smartprogram.baidu.co…扶持政策在 2018 年的百度世界大会上,百度宣布了智能小程序「开发者共筑计划」——「千百十一」,分别对应着百度将为智能小程序开放全域千亿用户流量、提供百亿的广告分成、成立一个提供投资与推广的创新基金以及像公开课一样的一对一专人服务。04 淘宝关于「轻店铺」2018 年 9 月,手机淘宝上的小程序「轻店铺」开始内测。淘宝「轻店铺」是一个支持个人或者企业进行开店的工具,具有群聊、发文章、门店导航等功能。据了解,「轻店铺」将把淘宝品牌主的直播、微淘内容以及门店信息进行汇集,最后形成了一个信息块,当用户在线上搜索该品牌时会打包弹出。代表作品严格来说,目前手淘上只有「星巴克中国」是真正的「轻店铺」,底层与支付宝小程序相通。其他的淘宝店铺虽然同样有小程序标志性「胶囊键」,但并不是小程序,且胶囊键与「轻店铺」按键不同。主要入口在淘宝搜索「星巴克中国」,即可从顶部的横幅进入。开发成本因仍处于内测阶段,开发者可将姓名、联系方式、公司名称、职务发送邮件至 taolite@service.taobao.com 申请成为内测用户。未获取内测资格的用户将无法进行入驻。05 字节跳动关于字节跳动小程序2018 年 11 月 8 日,字节跳动小程序正式上线,小程序旨在利用优质内容所关联和产生的使用场景进行小程序导流,解决开发者流量与转化困扰。开发一套小程序就可以服务多个产品是头条小程序的优势所在。在字节跳动开发者平台开发的小程序有机会出现在抖音、西瓜视频等不同平台。目前字节跳动被人所熟知的小程序,多为其他平台的已有内容。但在游戏、电商、娱乐等领域,字节跳动反倒有了更多拿得出手的作品。不过由于平台尚在发展阶段,很多内容构架都尚未完善。代表作品小游戏:光与森、行星毁灭、音悦球球、一笔画完等主要入口今日头条文章详情页、微头条、小视频、搜索等入口均为今日头条小程序入口,新版今日头条「常用」底部导航栏还在内测一个类似小程序桌面的新界面。而在其他平台,抖音短视频左下角也有对应的小程序入口。开发成本字节跳动小程序暂不支持一键转换工具。暂不支持 wepy,mpvue 等框架,需开发者主动把利用 wepy 框架开发的代码,编译为小程序的语法,才能在头条上正常的渲染。开发文档:https://microapp.bytedance.com/扶持政策目前字节跳动公布的扶持政策仅面向小游戏。针对普通游戏,开发者可以拿到广告日流水不超过 100 万部分的 60%,超过 100 万的部分开发者可保留 50%。而在字节跳动首发的游戏将获得更多资源倾斜,开发者可以拿到广告日流水不超过 100 万部分的 70%,而超过 100 万的部分开发者可保留 60%。06 快应用关于快应用vivo、华为、OPPO、小米、联想、金立、魅族、中兴、努比亚、一加、海信、中国移动终端想要一起做一个基于手机硬件平台的新型应用形态。十二家手机厂商和服务商组成的快应用联盟来势汹汹。覆盖 10 亿设备、月活 2 亿、打开快应用 20 亿次、留存 1 亿个桌面图标,35% 的流量来自桌面留存图标。快应用与其他小程序平台应用同质化严重,联盟 12 个合作伙伴对开发者的资源分配亦有较大不同。代表作品工具类:菜鸟裹裹、西窗烛、吐司工具箱等主要入口快应用有四类入口:应用分发、智能场景、二次入口、开发者自身入口开发成本快应用使用前端技术栈开发,原生渲染,同时具备 HTML 5 页面和原生应用的双重优点。快应用使用 MVVM 的设计模式进行开发,开发者也无需直接操作 DOM 节点的增删,利用数据驱动的方式完成节点更新。相比微信、支付宝小程序,快应用的开发语法标准,其语法也更接近传统网页。开发文档:https://doc.quickapp.cn/除了以上几大平台推出的小程序,其实优酷、钉钉、网易云音乐也出现过小程序的身影:优酷的小程序、钉钉的 E 应用,还有网易云音乐的「私藏推荐」。▲ 左为优酷小程序,右为网易云音乐小程序 在已有几大平台的努力下,小程序存在的方式和其代表的服务形态已经被越来越多的平台所认可。这些平台或许不会 all in 小程序,却也会在自己的生态内对小程序进行更多的尝试。和一个成熟、有包容性的平台相比,这几个平台或有严格的类目限制;或者只是将原有的 H5 内容转为了小程序;有的则只是模仿了小程序相似的界面。对于开发者而言,他们似乎不能算一个开放的开发平台,但也与小程序有着不小的联系。知晓云福利参与 iOS、Android SDK 内测领福利「知晓云」cloud.minapp.com,诞生于 2017 年 8 月 8 日,是以小程序开发为起点的后端云服务,它免去了小程序开发中服务器搭建、域名备案、数据接口开发、线上运维等繁琐流程,让开发者更快、更地做出优质的小程序。你的主要业务不在小程序,而是在移动端?没关系,iOS、Android 的应用开发也可以使用知晓云来完成了哦~知晓云移动端(iOS、Android)支持的内测活动已经开启:即日起至 4 月 17 日,填写表单报名参与内测活动的用户,在成功接入移动端(iOS、Android)应用后将获得 100 元无门槛优惠券???? 报名点这里 ???? ...

April 11, 2019 · 1 min · jiezi

企业级微信小程序实战详解

项目地址:https://github.com/wudiufo/We…完成效果展示:https://www.bilibili.com/vide…小爱心是否点赞组件 components/like思路:like 默认为 false,显示空心小爱心触摸执行tap:onLike 方法,因为 this.setData({count:count,like:!like})是异步的,先执行count = like ? count - 1 : count + 1,这时like还是false,执行count+1。然后在执行this.setData()方法,将like变为true,显示实心小爱心。let behavior = like ? ’like’ : ‘cancel’ //自定义事件 this.triggerEvent(’like’, { behavior: behavior }, {})自定义事件like,当like为真时,behavior为like,在models/like.js中,let url = behavior === ’like’ ? ’like/cancel’ : ’like’,因为behavior === ’like’为真,就调用服务器接口’like/cancel’,相反就调用like接口。刚开始实心就调用’like/cancel’接口,空心就调用’like’接口底部左右切换组件 components/navi思路:在navi/index.js中:先定义哪些数据是外部传来的数据,哪些数据是私有数据properties: {//外部传来的数据 title: String, first: Boolean, //如果是第一期向右的箭头就禁用,默认是false latest: Boolean //如果是最新的一期向左的箭头就禁用,默认是false }, data: {//私有数据 disLeftSrc: ‘./images/triangle.dis@left.png’, leftSrc: ‘./images/triangle@left.png’, disRightSrc: ‘./images/triangle.dis@right.png’, rightSrc: ‘./images/triangle@right.png’ },左箭头:在navi/index.wxml中<image bind:tap=“onLeft” class=“icon” src="{{latest?disLeftSrc:leftSrc}}"/>src显示图片规则:如果是最新的期刊,就显示向左禁用状态disLeftSrc箭头;如果不是最新一期的期刊,就显示向左可用状态leftSrc箭头为图片绑定触摸事件onLeft,在navi/index.js中:在 methods 中:如果不是最新的期刊,就继续绑定自定义事件leftonLeft: function(event) { //不是最新一期 if (!this.properties.latest) { this.triggerEvent(’left’, {}, {}) } },右箭头:在navi/index.wxml中<image bind:tap=“onRight” class=“icon” src="{{first?disRightSrc:rightSrc}}"/>src显示图片规则:如果是第一期的期刊,就显示向右禁用状态disRightSrc箭头;如果不是第一期的期刊,就显示向右可用状态rightSrc箭头为图片绑定触摸事件onRight,在navi/index.js中:在 methods 中:如果不是第一期的期刊,就继续绑定自定义事件rightonRight: function(event) { //不是第一期 if (!this.properties.first) { this.triggerEvent(‘right’, {}, {}) } }在pages/classic中:1:在 classic.json 中,注册使用navi自定义组件{ “usingComponents”: { “v-like”: “/components/like/index”, “v-movie”: “/components/classic/movie/index”, “v-episode”: “/components/episode/index”, “v-navi”: “/components/navi/index” }}2:在 classic.wxml 中:绑定自定义事件left, 获取当前一期的下一期;绑定自定义事件right,获取当前一期的上一期<v-navi bind:left=“onNext” bind:right=“onPrevious” class=“nav” title="{{classic.title}}" first="{{first}}" latest="{{latest}}"/> 3:在 classic.js 中:// 获取当前一期的下一期,左箭头onNext: function(evevt) {this._updateClassic(’next’) },// 获取当前一期的上一期,右箭头onPrevious: function(evevt) { this._updateClassic(‘previous’) },// 重复代码过多,利用函数封装的思想,新建一个函数抽取公共代码// 发送请求,获取当前页的索引,更新数据 _updateClassic: function(nextOrPrevious) { let index = this.data.classic.index classicModel.getClassic(index, nextOrPrevious, (res) => { // console.log(res) this.setData({ classic: res, latest: classicModel.isLatest(res.index), first: classicModel.isFirst(res.index) }) }) }, 4:在 models/classic.js 中:// 当前的期刊是否为第一期,first就变为true,右箭头就显示禁用 isFirst(index) { return index === 1 ? true : false }// 当前的期刊是否为最新的一期,latest就变为TRUE,左箭头就显示禁用// 由于服务器数据还会更新,确定不了最新期刊的索引,所以就要利用缓存机制,将最新期刊的索引存入到缓存中,如果外界传进来的索引和缓存的最新期刊的索一样,latest就变为TRUE,左箭头就显示禁用 isLatest(index) { let latestIndex = this._getLatestIndex() return latestIndex === index ? true : false }// 将最新的期刊index存入缓存 _setLatestIndex(index) { wx.setStorageSync(’latest’, index) } // 在缓存中获取最新期刊的index _getLatestIndex() { let index = wx.getStorageSync(’latest’) return index }优化缓存。解决每次触摸左右箭头都会频繁向服务器发送请求,这样非常耗性能,用户体验极差。解决方法,就是把第一次发送请求的数据都缓存到本地,再次触摸箭头时,会先查找本地缓存是否有数据,有就直接从缓存中读取数据,没有就在向服务器发送请求,这样利用缓存机制大大的提高了用户的体验。(但也有一部分是需要实时更新的,比如是否点赞的小爱心组件,需要每次都向服务器发送请求获取最新数据)在 models/classic.js 中:1:// 设置缓存中的key 的样式,classic-1这种样式 _getKey(index) { let key = classic-${index} return key }2: // 因为getPrevious,getNext实现代码相似,所以为了简化代码可以合并为一个函数 // 缓存思路:在缓存中寻找key,找不到就发送请求 API,将key写入到缓存中。解决每次都调用Api向服务器发请求,耗费性能 // 在缓存中,确定key getClassic(index, nextOrPrevious, sCallback) { //0: 是next,触摸向左箭头获取下一期,触摸向右箭头否则获取上一期 let key = nextOrPrevious === ’next’ ? this._getKey(index + 1) : this._getKey(index - 1) //1:在缓存中寻找key let classic = wx.getStorageSync(key) //2:如果缓存中找不到key,就调用服务器API发送请求获取数据 if (!classic) { this.request({ url: classic/${index}/${nextOrPrevious}, success: (res) => { //将获取到的数据设置到缓存中 wx.setStorageSync(this._getKey(res.index), res) //再把获取到的数据返回,供用户调取使用 sCallback(res) } }) } else { //3:如果在缓存中有找到key,将缓存中key对应的value值,返回给用户,供用户调取使用 sCallback(classic) } }——————————————————————————–// 获取最新的期刊利用缓存机制进一步优化 //获取最新的期刊 getLatest(cb) { this.request({ url: ‘classic/latest’, success: (res) => {//将最新的期刊index存入缓存,防止触摸向左箭头时,没有设置latest的值,左箭头会一直触发发送请求找不到最新的期刊报错 this._setLatestIndex(res.index) //再把获取到的数据返回,供用户调取使用 cb(res) // 将最新的期刊设置到缓存中,先调取 this._getKey() 方法,为最新获取的期刊设置key值,调用微信设置缓存方法将key,和对应的value值res存进去 let key = this._getKey(res.index) wx.setStorageSync(key, res) } }) }处理是否点赞小爱心组件的缓存问题:他不需要缓存,需要实时获取最新数据在 models/like.js 中://编写一个获取点赞信息的方法,从服务器获取最新点赞信息的数据 // 获取点赞信息 getClassicLikeStatus(artID, category, cb) { this.request({ url: classic/${category}/${artID}/favor, success: cb }) }在 pages/classic/classic.js 中://设置私有数据初始值data: { classic: null, latest: true, first: false, likeCount: 0,//点赞的数量 likeStatus: false //点赞的状态 }, // 在classic.wxml中: <v-like class=“like” bind:like=“onLike” like="{{likeStatus}}" count="{{likeCount}}"/> // 编写一个私有方法获取点赞信息 // 获取点赞信息 _getLikeStatus: function(artID, category) { likeModel.getClassicLikeStatus(artID, category, (res) => { this.setData({ likeCount: res.fav_nums, likeStatus: res.like_status }) }) }, //生命周期函数–监听页面加载 onLoad: function(options) { classicModel.getLatest((res) => { console.log(res) // this._getLikeStatus(res.id, res.type) //不能这样写,会多发一次favor请求,消耗性能 this.setData({ classic: res, likeCount: res.fav_nums, likeStatus: res.like_status }) })在 classic/music/index.js 中:解决切换期刊时,其他期刊也都是播放状态的问题。应该是,切换期刊时音乐就停止播放,回到默认不播放状态利用组件事件的通信机制,小程序中只有父子组件在 components/classic/music/inddex.js 中:方案一://利用组件生命周期,只有 wx:if 才可以从头掉起组件生命周期// 组件卸载的生命周期函数 // 组件卸载音乐停止播放,但这时不生效是因为,在classic.wxml中用的是hidden,应改为if detached: function(event) { mMgr.stop() }, // 在 pages/classic/classic.wxml 中 // <v-music wx:if="{{classic.type===200}}" img="{{classic.image}}" content="{{classic.content}}" src="{{classic.url}}" title="{{classic.title}}"/> 知识点补充:wx:if vs hidden,和Vue框架的v-if和v-show 指令一样:wx:if 》他是惰性的,如果初始值为false框架什么也不做,如果初始值为true框架才会局部渲染。true或false的切换就是从页面中局部加入或移除的过程。wx:if 有更高的切换消耗,如果在运行时条件不大可能改变则 wx:if 较好。生命周期会重新执行。hidden 》组件始终会被渲染,只是简单的控制显示与隐藏。hidden 有更高的初始渲染消耗。如果需要频繁切换的情景下,用 hidden 更好。生命周期不会重新执行。方案二:(推荐使用)解决切换期刊时音乐可以当做背景音乐一直播放,而其他的期刊是默认是不播放状态在 components/classic/music/inddex.js 中://为了保证期刊在切换时,背景音乐可以一直播放,就要去除掉 mMgr.stop() 事件方法detached: function(event) { // mMgr.stop() //为了保证背景音乐的持续播放就不能加stop }, // 监听音乐的播放状态,如果当前页面没有播放的音乐,就设置playing为false。如果当前页面的音乐地址classic.url和当前正在播放的音乐的地址一样,就让播放状态为true_recoverStatus: function() { if (mMgr.paused) { this.setData({ playing: false }) return } if (mMgr.src === this.properties.src) { this.setData({ playing: true }) } }, // 监听播放状态,总控开关就可以控制播放状态,结局总控开关和页面不同步问题 _monitorSwitch: function() { console.log(‘monitorSwitch背景音频’, ‘触发3’) // 监听背景音频播放事件 mMgr.onPlay(() => { this._recoverStatus() console.log(‘onPlay ’ + this.data.playing) }) // 监听背景音频暂停事件 mMgr.onPause(() => { this._recoverStatus() console.log(‘onPause ’ + this.data.playing) }) // 关闭音乐控制台,监听背景音频停止事件 mMgr.onStop(() => { this._recoverStatus() console.log(‘onStop ’ + this.data.playing) }) // 监听背景音频自然播放结束事件 mMgr.onEnded(() => { this._recoverStatus() console.log(‘onEnded ’ + this.data.playing) }) }, //调用生命周期函数,每次切换都会触发attached生命周期 // 在组件实例进入页面节点树时执行 // hidden,ready,created都触发不了生命周期函数 attached: function(event) { console.log(‘attach实例进入页面’, ‘触发1’) this._monitorSwitch() this._recoverStatus() },播放动画旋转效果制作:在 components/classic/music/index.wxss 中://定义帧动画用CSS3.rotation { -webkit-transform: rotate(360deg); animation: rotation 12s linear infinite; -moz-animation: rotation 12s linear infinite; -webkit-animation: rotation 12s linear infinite; -o-animation: rotation 12s linear infinite;}@-webkit-keyframes rotation { from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(360deg); }}补充css3知识点:》使用CSS3开启GPU硬件加速提升网站动画渲染性能:为动画DOM元素添加CSS3样式-webkit-transform:transition3d(0,0,0)或-webkit-transform:translateZ(0);,这两个属性都会开启GPU硬件加速模式,从而让浏览器在渲染动画时从CPU转向GPU,其实说白了这是一个小伎俩,也可以算是一个Hack,-webkit-transform:transition3d和-webkit-transform:translateZ其实是为了渲染3D样式,但我们设置值为0后,并没有真正使用3D效果,但浏览器却因此开启了GPU硬件加速模式。》这种GPU硬件加速在当今PC机及移动设备上都已普及,在移动端的性能提升是相当显著地,所以建议大家在做动画时可以尝试一下开启GPU硬件加速。》适用情况通过-webkit-transform:transition3d/translateZ开启GPU硬件加速的适用范围:使用很多大尺寸图片(尤其是PNG24图)进行动画的页面。 页面有很多大尺寸图片并且进行了css缩放处理,页面可以滚动时。 使用background-size:cover设置大尺寸背景图,并且页面可以滚动时。(详见:https://coderwall.com/p/j5udlw) 编写大量DOM元素进行CSS3动画时(transition/transform/keyframes/absTop&Left) 使用很多PNG图片拼接成CSS Sprite时 》总结 通过开启GPU硬件加速虽然可以提升动画渲染性能或解决一些棘手问题,但使用仍需谨慎,使用前一定要进行严谨的测试,否则它反而会大量占用浏览网页用户的系统资源,尤其是在移动端,肆无忌惮的开启GPU硬件加速会导致大量消耗设备电量,降低电池寿命等问题。在 components/classic/music/index.wxml 中://为图片加上播放就旋转的类,不播放 就就为空字符串<image class=“classic-img {{playing?‘rotation’:’’}}” src="{{img}}"></image>用 slot 插槽,解决在公用组件中可以加入其他修饰内容问题。其实就是,在定义公用组件时,用 slot 命名插槽占位,在父组件调用时可以传递需要的内容补位。和Vue的指令 v-slot 相似。在 components/tag/index.js 中://在 Component 中加入// 启用slot options: { multipleSlots: true },在定义的公共组件 components/tag/index.wxml 中://定义几个命名插槽,供父元素占位使用<view class=“container tag-class”> <slot name=“before”></slot> <text>{{text}}</text> <slot name=“after”></slot></view>在 pages/detail/detail.json 中://注册并使用组件{ “usingComponents”: { “v-tag”: “/components/tag/index” }}在 pages/detail/detail.wxml 中://使用组件v-tag,补位命名插槽<v-tag tag-class="{{index===0?’ex-tag1’:’’||index===1?’ex-tag2’:’’}}" text="{{item.content}}"> <text class=“num” slot=“after”>{{’+’+item.nums}}</text></v-tag>在 pages/detail/detail 中,解决评论内容自定义组件 v-tag 评论前两条显示两种颜色的做法:第一种方法:(推荐使用)在 pages/detail/detail.wxss 中:/* v-tag是自定义组件,不能使用css3,在微信小程序中,只有内置组件才可以用css3 //用CSS hack方式给自定义组件加样式/.comment-container>v-tag:nth-child(1)>view { background-color: #fffbdd;}.comment-container>v-tag:nth-child(2)>view { background-color: #eefbff;}第二种方法:定义外部样式方法,像父子组件传递属性一样,传递样式类在 detail.wxss 中:/ 定义外部样式 /.ex-tag1 { background-color: #fffbdd !important;}.ex-tag2 { background-color: #eefbff !important;}在 detail.wxml 中:/将自定义的样式类通过属性传值的方式传递给自定义子组件v-tag /<v-tag tag-class="{{index===0?’ex-tag1’:’’||index===1?’ex-tag2’:’’}}" text="{{item.content}}"> <text class=“num” slot=“after”>{{’+’+item.nums}}</text></v-tag>在 components/tag/index.js 中://将外部传进来的样式写在Component中,声明一下// 外部传进来的css,样式 externalClasses: [’tag-class’],在 components/tag/index.wxml 中:// 把父组件传递过来的类 tag-calss 写在 class 类上<view class=“container tag-class”> <slot name=“before”></slot> <text>{{text}}</text> <slot name=“after”></slot></view>解决服务器返回的内容简介有 n 换行符的问题:原因:是因为服务器返回的原始数据 是 \n ,经过转义就变成 \n 而 \n 在text文本标签中默认转义为换行解决方法:WXS:WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。和Vue 中的 Vue.filter(过滤器名,过滤器方法) 很相似。WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异。在 utils/filter.wxs 中:// 定义过滤器函数,处理服务器返回的数据,将 \n 变成 \n// 会打印两次,undefined和请求得到的数据,因为第一次初始时text为null,发送请求得到数据后调用setData更新数据一次var format = function(text) { console.log(text) if (!text) { return } var reg = getRegExp(’\\n’, ‘g’) return text.replace(reg, ‘\n’)}module.exports.format = format在 pages/detail/detail.wxml 中://引入<wxs src="../../utils/filter.wxs" module=“util”/>//在需要过滤的数据中使用<text class=“content”>{{util.format(book.summary)}}</text>解决解决服务器返回的内容简介首行缩进的问题:在 pages/detail/detail.wxss 中://对需要缩进的段落前加以下的类,但这时只有第一段缩进.content { text-indent: 58rpx; font-weight: 500;}在 utils/filter.wxs 中://用转义字符 &nbsp; 作为空格,但这时小程序会以&nbsp;样式输出,不是我们想要的效果var format = function(text) { if (!text) { return } var reg = getRegExp(’\\n’, ‘g’) return text.replace(reg, ‘\n&nbsp;&nbsp;&nbsp;&nbsp;’)}module.exports.format = format在 pages/detail/detail.wxml 中://加入属性 decode="{{true}}",首行缩进问题解决<text class=“content” decode="{{true}}">{{util.format(book.summary)}}</text>解决短评过多让其只显示一部分的问题:在 utils/filter.wxs 中://添加一个限制短评长度的过滤器,并导出// 限制短评的长度的过滤器var limit = function(array, length) { return array.slice(0, length)}module.exports = { format: format, limit: limit};在 pages/detail/detail.wxml 中:<wxs src="../../utils/filter.wxs" module=“util”/><view class=“sub-container”> <text class=“headline”>短评</text> <view class=“comment-container”> <block wx:for="{{util.limit(comments,10)}}" wx:key=“content”> <v-tag tag-class="{{index===0?’ex-tag1’:’’||index===1?’ex-tag2’:’’}}" text="{{item.content}}"> <text class=“num” slot=“after”>{{’+’+item.nums}}</text> </v-tag> </block> </view> </view>在 pages/detail/detail.wxml 中:进一步优化// 由于 <v-tag tag-class="{{index===0?’ex-tag1’:’’||index===1?’ex-tag2’:’’}}" text="{{item.content}}"> 过于乱,改写成wxs形式: //先定义wxs过滤器 <wxs module=“tool”> var highlight = function(index){ if(index===0){ return ’ex-tag1’ } else if(index===1){ return ’ex-tag2’ } return ’’ } module.exports={ highlight:highlight }</wxs> //改写为: <v-tag tag-class="{{tool.highlight(index)}}" text="{{item.content}}"> 详情最底部短评的实现:用户提交评论内容:点击标签向服务器提交评论内容:在 componentstagindex.wxml 中://为短评组件绑定出没事件 onTap<view bind:tap=“onTap” class=“container tag-class”> <slot name=“before”></slot> <text>{{text}}</text> <slot name=“after”></slot></view>在 componentstagindex.js 中:// 当触摸短评小标签时,触发一个自定义事件,将短评内容传进去,公父组件调用自定义事件tapping methods: { // 触摸短评小标签时,触发的事件,触发一个自定义事件 onTap(event) { this.triggerEvent(’tapping’, { text: this.properties.text }) } }在 pagesdetaildetail.wxml 中://在父组件中调用子组件的自定义tapping事件,并且触发事件onPost<v-tag bind:tapping=“onPost” tag-class="{{tool.highlight(index)}}" text="{{item.content}}"> <text class=“num” slot=“after”>{{’+’+item.nums}}</text></v-tag>在 modelsbook.js 中://调取新增短评的接口// 新增短评 postComment(bid, comment) { return this.request({ url: ‘/book/add/short_comment’, method: ‘POST’, data: { book_id: bid, content: comment } }) }在 pagesdetaildetail.js 中:// 触摸tag组件会触发,input输入框也会触发事件onPost// 获取用户的输入内容或触发tag里的内容,并且对用户输入的评论做校验,如果评论的内容长度大于12就弹出警告不向服务器发送请求//如果评论内容符合规范,就调用新增短评接口并将最新的评论插到comments数组的第一位,更新数据,并且把蒙层mask关闭onPost(event) { // 获取触发tag里的内容 const comment = event.detail.text // 对用户输入的评论做校验 if (comment.length > 12) { wx.showToast({ title: ‘短评最多12个字’, icon: ’none’ }) return } // 调用新增短评接口并将最新的评论插到comments数组的第一位 bookModel.postComment(this.data.book.id, comment).then(res => { wx.showToast({ title: ‘+1’, icon: ’none’ }) this.data.comments.unshift({ content: comment, nums: 1 //这是后面的数量显示 }) this.setData({ comments: this.data.comments, posting: false }) }) },在 pagesdetaildetail.wxml 中:// input有自己的绑定事件bindconfirm,会调用手机键盘完成按键<input bindconfirm=“onPost” type=“text” class=“post” placeholder=“短评最多12个字”/>在 pagesdetaildetail.js 中:点击标签向服务器提交评论内容完成:// 触摸tag组件会触发,input输入框也会触发事件onPost事件;然后获取触发tag里的内容或获取用户input输入的内容;对tag里的内容或对用户输入的评论做校验并且输入的内容不能为空;最后调用新增短评接口并将最新的评论插到comments数组的第一位,并且把蒙层mask关闭 onPost(event) { const comment = event.detail.text||event.detail.value console.log(‘comment’+comment) console.log(‘commentInput’+comment) if (comment.length > 12|| !comment) { wx.showToast({ title: ‘短评最多12个字’, icon: ’none’ }) return } bookModel.postComment(this.data.book.id, comment).then(res => { wx.showToast({ title: ‘+1’, icon: ’none’ }) this.data.comments.unshift({ content: comment, nums: 1 //这是后面的数量显示 }) this.setData({ comments: this.data.comments, posting: false }) }) },细节处理:如果没有短评显示问题:在 pagesdetaildetail.wxml 中://在短评后加上还没有短评标签,如果没有comments短评就不显示还没有短评标签<text class=“headline”>短评</text><text class=“shadow” wx:if="{{!comments.length}}">还没有短评</text>//在尽可点击标签+1后加上暂无评论标签,如果没有comments短评就不显示暂无评论标签<text wx:if="{{!comments.length}}">尽可点击标签+1</text><text wx:else>暂无短评</text>由于需要加载的数据较多,为了提高用户体验,需要加一个loading提示数据正在加载中,数据加载完成后就消失;由于都是利用promise异步加载数据,这时取消loading显示应该加到每个promise后,显然不符合需求。如果利用回调函数机制,先加载1在一的回调函数里在加载2依次顺序加载,在最后一个回调函数中写取消loading操作,这样的方式虽然可以实现,但非常耗时间,请求是串行的,假如一个请求需要花费2s中,发三个请求就要花费6秒,非常耗时,而且还会出现回调地狱的现象,不推荐使用。解决方法:在Promise中,有一个Promise.all()方法就可以解决。补充知识点:Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。简单来说就是:只要有一个数组里的promise获取失败就调用reject回调,只有全部数组里的promise都成功才调用resolve回调。Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。race 函数返回一个 Promise,它将与第一个传递的 promise 相同的完成方式被完成。它可以是完成( resolves),也可以是失败(rejects),这要取决于第一个完成的方式是两个中的哪个。如果传的迭代是空的,则返回的 promise 将永远等待。如果迭代包含一个或多个非承诺值和/或已解决/拒绝的承诺,则 Promise.race 将解析为迭代中找到的第一个值。简单来说就是:不论成功还是失败的回调,哪一个快就执行哪个。在 pagesdetaildetail.js 中://用了 Promise.all(iterable) 方法就不用写三个Promise方法分别来更新数据了,可以简写成一个all方法再返回的成功的promise中调用setData(),更新请求回的数据onLoad: function(options) { // 数据加载时显示loading效果 wx.showLoading() const bid = options.bid console.log(bid) // 获取书籍详细信息 const detail = bookModel.getDetail(bid) // 获取书籍点赞情况 const likeStatus = bookModel.getLikeStatus(bid) // 获取书籍短评 const comments = bookModel.getComments(bid) // 数据加载完成时取消显示loading效果 Promise.all([detail, comments, likeStatus]).then(res => { console.log(res) this.setData({ book: res[0], comments: res[1].comments, likeStatus: res[2].like_status, likeCount: res[2].fav_nums }) wx.hideLoading() }) },图书的搜索:高阶组件:如果一个组件里面的内容比较复杂,包含大量的业务知识点补充:工作中我们通常把业务处理逻辑写在models中:可以写在单个公用组件里,只供自己写业务逻辑调取使用;可以写在components中,只供components内的组件调取使用,如果想把components发布出去给其他项目用或者提供给其他开发者使用;可以写在项目根目录models下,供整个项目调取使用写业务逻辑,如果只是做个项目建议写在这里不会乱。在 componentssearchindex 中:处理历史搜索和热门搜索:历史搜索:将历史搜索关键字写入缓存中,在从缓存中获取历史搜索关键字。热门搜索:调取服务器API GET /book/hot_list将业务逻辑写在 modelskeyword.js 中://首先从缓存中获取历史搜索关键字数组,判断获取的数组是否为空,如果为空,为了防止报错就返回空数组;如果不为空就直接返回获取的数组。//将搜索关键字写入缓存中,先从缓存中获取历史关键字的数组,判断是否包含此次输入的关键字,如果没有此关键字,如果获取的长度大于最大长度,就将数组的最后一项删除;如果获取数组的长度小于最大长度,就将此次输入的关键字加到数组的第一位,并且设置到缓存中。class KeywordModel { constructor() { // 把key属性挂载到当前实例上,供实例调取使用 this.key = ‘q’, this.maxLength = 10 //搜索关键字的数组最大长度为10 } //从缓存中,获取历史搜索关键字数组,如果缓存中没有直接返回空数组 getHistory() { const words = wx.getStorageSync(this.key) if (!words) { return [] } return words } // 将历史搜索关键字写入缓存中。先从缓存中获取历史关键字的数组,判断数组中是否已经有此关键字。如果没有,并且获取数组的长度大于最大长度,就将数组最后一项删除。获取数组的长度小于最大长度就将此次输入的关键字加到数组第一位,并且设置到缓存中; addToHistory(keyword) { let words = this.getHistory() const has = words.includes(keyword) if (!has) { const length = words.length if (length >= this.maxLength) { words.pop() } words.unshift(keyword) wx.setStorageSync(this.key, words) } } // 获取热门搜素搜关键字 getHot() { }}export {KeywordModel}在 componentssearchindex.wxml 中://为input输入框绑定onConfirm事件<input bind:confirm=“onConfirm” type=“text” class=“bar” placeholder-class=“in-bar” placeholder=“书籍名” auto-focus=“true”/>在 componentssearchindex.js 中:// onConfirm事件执行,调用将输入的内容添加到缓存中的方法Keywordmodel.addToHistory(word),就可以将历史关键字添加到缓存中methods: { // 点击取消将搜索组件关掉,有两种方法:一是,在自己的内部创建一个变量控制显隐,不推荐,因为万一还有其他操作扩展性不好。二是,创建一个自定义事件,将自定义事件传给父级,让父级触发 onCancel(event) { this.triggerEvent(‘cancel’, {}, {}) }, // 在input输入框输入完成将输入的内容加到缓存中 onConfirm(event) { const word = event.detail.value Keywordmodel.addToHistory(word) } }在 componentssearchindex.js 中://将历史搜索的内容从缓存中取出来data: { historyWords: [] //历史搜索关键字 }, // 组件初始化时,小程序默认调用的生命周期函数 attached() { const historywords = Keywordmodel.getHistory() this.setData({ historyWords: historywords }) },在 componentssearchindex.json 中://注册引用小标签 tag 组件,组件中也可以引入其他组件"usingComponents": { “v-tag”: “/components/tag/index” }在 componentssearchindex.wxml 中:// 遍历historyWords数组中的每一项,呈现在页面中 <view class=“history”> <view class=“title”> <view class=“chunk”></view> <text>历史搜索</text> </view> <view class=“tags”> <block wx:for="{{historyWords}}" wx:key=“item”> <v-tag text=’{{item}}’/> </block> </view> </view>热门搜索:在 modelskeyword.js 中:// 引入自己封装的API请求方法import { HTTP} from ‘../utils/http-promise’// 获取热门搜素搜关键字 getHot() { return this.request({ url: ‘/book/hot_keyword’ }) }在 componentssearchindex.js 中://定义组件初始值,通过调用传进来的getHot方法获取热门搜索关键字,并更新到初始值hotWords中data: { historyWords: [], //历史搜索关键字 hotWords: [] //热门搜索关键字 },// 组件初始化时,小程序默认调用的生命周期函数 attached() { const historywords = Keywordmodel.getHistory() const hotword = Keywordmodel.getHot() this.setData({ historyWords: historywords }) hotword.then(res => { this.setData({ hotWords: res.hot }) }) },在 componentssearchindex.wxml 中://将从服务器获取到的hotWords数组遍历,呈现到页面中<view class=“history hot-search”> <view class=“title”> <view class=“chunk”></view> <text>热门搜索</text> </view> <view class=“tags”> <block wx:for="{{hotWords}}" wx:key=“item”> <v-tag text=’{{item}}’/> </block> </view> </view>注意点:由于在 componentssearchindex.js 调用了 Keywordmodel.getHot()方法,这个方法是和服务器相关联的,这样做,会使组件复用性降低。如果要想让search组件复用性变高,应该在 componentssearchindex.js 的 properties 中开放一个属性,然后再引用search组件的pages页面里调用models里的方法,再把数据通过属性传递给search组件,然后再做数据绑定,这样就让search组件具备了复用性在 modelsbook.js 中://定义search函数,封装向服务器发送请求功能// 书籍搜索 search(start, q) { return this.request({ url: ‘/book/search?summary=1’, data: { q: q, start: start } }) }在 componentssearchindex.js 中:// 导入并实例化BookModel类,负责向服务器发送搜索图书的请求;在data中声明私有变量 dataArray 数组,为搜索图书当summary=1,返回概要数据。在用户输入完成点击完成时,调用bookmodel.search方法,并更新数据到dataArray中。//注意点:不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中import { BookModel} from ‘../../models/book’const bookmodel = new BookModel()data: { historyWords: [], //历史搜索关键字 hotWords: [], //热门搜索关键字 dataArray: [] //搜索图书当summary=1,返回概要数据 }, // 在input输入框输入完成将输入的内容加到缓存中 onConfirm(event) { const word = event.detail.value // 获取搜索的关键词q,调取search方法返回当summary=1,返回概要数据:并更新数据到dataArray中 const q = event.detail.value bookmodel.search(0, q).then(res => { this.setData({ dataArray: res.books }) // 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中 Keywordmodel.addToHistory(word) }) }在 componentssearchindex.wxml 中:解析得到的搜索数据,并遍历呈现到页面中://搜索得到的数据和热门搜索,历史搜索是不能一起显示的,一个显示,另一个就得隐藏,搜索得到的结果页面是默认不显示的,需要定义searching一个变量来控制显隐<view wx:if="{{searching}}" class=“books-container”> <block wx:for="{{dataArray}}" wx:key="{{item.id}}"> <v-book book="{{item}}" class=“book”></v-book> </block> </view>在 componentssearchindex.js 中://在data私有属性中定义searching变量来控制显隐,默认为false;在触发onConfirm事件中, 为了用户体验好,应该点击完立即显示搜索页面,并将searching改为true,让其搜索的内容显示到页面上data: { historyWords: [], //历史搜索关键字 hotWords: [], //热门搜索关键字 dataArray: [], //搜索图书当summary=1,返回概要数据 searching: false //控制搜索到的图书数据的显隐,默认不显示 }, // 在input输入框输入完成将输入的内容加到缓存中 onConfirm(event) { // 为了用户体验好,应该点击完立即显示搜索页面 this.setData({ searching: true }) // 获取搜索的关键词q,调取search方法返回当summary=1,返回概要数据:并更新数据到dataArray中 const q = event.detail.value bookmodel.search(0, q).then(res => { this.setData({ dataArray: res.books }) // 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中 Keywordmodel.addToHistory(q) }) }实现 搜索框里的 x 按钮功能:在 componentssearchindex.js 中:// 在 components\search\index.wxml 中,为 x 图片绑定触摸时触发的 onDelete 事件<image bind:tap=“onDelete” class=“cancel-img” src=“images/cancel.png”/>// 触摸搜索图片里的x回到原来输入搜索的页面onDelete(event){ this.setData({ searching: false }) },实现 用户点击历史搜索和热门搜索里的标签也能跳转到相应的搜索到的结果显示页面:只要监听到用户点击标签的事件就可以实现在 componentssearchindex.js 中:// 在 components\search\index.wxml 中:绑定v-tag组件自定事件tapping触发onConfirm事件:&lt;v-tag bind:tapping="onConfirm" text='{{item}}'/&gt; // 在input输入框输入完成将输入的内容加到缓存中 onConfirm(event) { // 为了用户体验好,应该点击完立即显示搜索页面 this.setData({ searching: true }) // 获取搜索的关键词q:一种是用户输入的内容或是通过调用tag组件的自定义事件tapping,里面有携带的text文本;调取search方法返回当summary=1,返回概要数据:并更新数据到dataArray中 const q = event.detail.value || event.detail.text bookmodel.search(0, q).then(res => { this.setData({ dataArray: res.books }) // 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中 Keywordmodel.addToHistory(q) }) }解决再点击tag标签搜索时应该在input输入框中显示书名的问题:在 componentssearchindex.js 中://通过数据绑定给input输入框绑定value="{{q}}"//&lt;input value="{{q}}" bind:confirm="onConfirm" type="text" class="bar" placeholder-class="in-bar" placeholder="书籍名" auto-focus="true"/&gt;//先在data中定义私有数据 q: ’’ 代表输入框中要显示的内容,当数据请求完成后把点击标签的内容q赋值给私有数据q并更新data: { historyWords: [], //历史搜索关键字 hotWords: [], //热门搜索关键字 dataArray: [], //搜索图书当summary=1,返回概要数据 searching: false, //控制搜索到的图书数据的显隐,默认不显示 q: ’’ //输入框中要显示的内容 }, // 在input输入框输入完成将输入的内容加到缓存中 onConfirm(event) { // 为了用户体验好,应该点击完立即显示搜索页面 this.setData({ searching: true }) // 获取搜索的关键词q,调取search方法返回当summary=1,返回概要数据:并更新数据到dataArray中 const q = event.detail.value || event.detail.text bookmodel.search(0, q).then(res => { this.setData({ dataArray: res.books, q: q }) // 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中 Keywordmodel.addToHistory(q) }) }实现数据分页加载功能:第一种方法:用微信小程序提供的内置组件 scroll-view 。第二种方法:用 pages 里的 页面上拉触底事件的处理函数 onReachBottom。:在 pagesbookbook.js 中://在 data里设置私有变量more为false,代表的是是否需要加载更多数据,默认是不加载data: { books: [], searching: false, //控制搜索框组件search显隐,默认不显示 more: false //是否需要加载更多数据,默认是不加载 }, //用pages里自带的 页面上拉触底事件的处理函数 onReachBottom 监听页面是否到底了,如果到底了就会就会将more改变为true,就可以实现加载更多数据方法 onReachBottom: function() { console.log(‘到底了’) this.setData({ more: true }) }, //由于 search 组件不是页面级组件,没有 onReachBottom 函数,就需要通过属性传值的方式将more私有变量控制是否加载更多数据传给子组件search // 在pages\book\book.wxml中: &lt;v-search more="{{more}}" bind:cancel="onCancel" wx:if="{{searching}}"/&gt; //然后在search组件里接收父级传递过来的属性more,并利用监听函数observer,只要外界传来的数据改变就会触发此函数执行 properties: { more: { type: String, observer: ‘_load_more’ } //从pages/book/book.js 传来的属性,监听滑到底步操作.只要外界传来的属性改变就会触发observer函数执行 }, methods: { // 只要外界传来的属性改变就会触发observer函数执行 _load_more() { console.log(‘监听函数触发到底了’) }, }但现在存在一个问题就是:observer只会触发一次,因为下拉到底会把more变为true,之后就都是true不会再发生变化了,就不会再触发监听函数observer执行。解决方法:用随机字符串触发observer函数,因为observer函数的执行必须是监听的数据发生改变才会执行此函数。和Vue中的watch很相似。在 pagesbookbook.js 中://将私有数据data中的more改为空字符串data: { books: [], searching: false, //控制搜索框组件search显隐,默认不显示 more: ’’ //是否需要加载更多数据,默认是不加载 }, //触发 页面上拉触底事件的处理函数,将more变为随机数,导入random自定义随机处理函数,问题解决 onReachBottom: function() { console.log(‘到底了’) this.setData({ more: random(16) }) }, // 在 utils\common.js 中:// 定义随机生成字符串处理函数,n是生成随机字符串的位数const charts = [‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’, ‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’, ‘G’, ‘H’, ‘I’, ‘J’, ‘K’, ‘L’, ‘M’, ‘N’, ‘O’, ‘P’, ‘Q’, ‘R’, ‘S’, ‘T’, ‘U’, ‘V’, ‘W’, ‘X’, ‘Y’, ‘Z’]const random = function generateMixed(n) { var res = ’’ for (var i = 0; i < n; i++) { var id = Math.ceil(Math.random() * 35) res += charts[id] } return res}export { random}在 componentssearchindex.js 中:实现加载更多数据:// 和onConfirm一样都需要调取search方法返回当summary=1,返回概要数据:并更新数据到dataArray中// 先判断已得到的搜索数据的长度,在调用search方法将最新获取的数据和原来的数据拼接到一起更新数据然后呈现到页面中 _load_more() { console.log(‘监听函数触发到底了’) const length = this.data.dataArray.length bookmodel.search(length, this.data.q).then(res => { const tempArray = this.data.dataArray.concat(res.books) this.setData({ dataArray: tempArray }) }) },细节完善://如果关键字q初始没有值就直接返回什么也不做_load_more() { console.log(‘监听函数触发到底了’) if (!this.data.q) { return } const length = this.data.dataArray.length bookmodel.search(length, this.data.q).then(res => { const tempArray = this.data.dataArray.concat(res.books) this.setData({ dataArray: tempArray }) }) },//问题:当下拉刷新没有更多数据时,还会继续向服务器发送请求非常耗性能;还有就是用户操作过快没等第一次请求的结果回来,就又发送一次相同的请求,会加载重复的数据,非常耗性能 //解决:使用锁的概念解决重复加载数据的问题//其实就是事件节流操作,在data中定义一个loading:false,表示是否正在发送请求,默认是没有发送请求,在_load_more中,判断如果正在发送请求就什么也不做,如果没有正在发送请求就将loading变为true,调用search方法向服务器发送请求,待请求完毕并返回结果时将loading变为false。 data:{ loading: false //表示是否正在发送请求,默认是没有发送请求 }, _load_more() { console.log(‘监听函数触发到底了’) if (!this.data.q) { return } // 如果是正在发送请求就什么也不做 if (this.data.loading) { return } const length = this.data.dataArray.length bookmodel.search(length, this.data.q).then(res => { const tempArray = this.data.dataArray.concat(res.books) this.setData({ dataArray: tempArray, loading: false }) }) }, 进一步封装优化,组件行为逻辑抽象分页行为,顺便解决 是否还有更多数据的问题:在 components中,创建并封装一个公用行为和方法的组件pagination:在 componentsbehaviorspagination.js 中://封装一个公用行为和方法的类paginationBevconst paginationBev = Behavior({ data: { dataArray: [], //分页不断加载的数据 total: 0 //数据的总数 }, methods: { // 加载更多拼接更多数据到数组中;新加载的数据合并到dataArray中 setMoreData(dataArray) { const tempArray = this.data.dataArray.concat(dataArray) this.setData({ dataArray: tempArray }) }, // 调用search方法时返回起始的记录数 getCurrentStart() { return this.data.dataArray.length }, // 获取设置从服务器得到数据的 总长度 setTotal(total) { this.data.total = total }, // 是否还有更多的数据需要加载。如果得到数据的长度大于服务器返回的总长度,代表没有更多数据了,就停止发请求 hasMore() { if (this.data.dataArray.length >= this.data.total) { return false } else { return true } } }})export { paginationBev}在 componentssearchindex.js 中:// 先导入封装的公用行为方法,再进一步改写_load_more和onConfirm方法,将写好的公用方法用上import { paginationBev} from ‘../behaviors/pagination’ _load_more() { console.log(‘监听函数触发到底了’) if (!this.data.q) { return } // 如果是正在发送请求就什么也不做 if (this.data.loading) { return } if (this.hasMore()) { this.data.loading = true//必须放在hasMore()里 bookmodel.search(this.getCurrentStart(), this.data.q).then(res => { this.setMoreData(res.books) this.setData({ loading: false }) }) } }, onConfirm(event) { // 为了用户体验好,应该点击完立即显示搜索页面 this.setData({ searching: true }) // 获取搜索的关键词q,调取search方法返回当summary=1,返回概要数据:并更新数据到dataArray中 const q = event.detail.value || event.detail.text bookmodel.search(0, q).then(res => { this.setMoreData(res.books) this.setTotal(res.total) this.setData({ q: q }) // 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中 Keywordmodel.addToHistory(q) }) }但这时又会出现一个小问题:就是每次点x回退到搜索页面时,再次搜索同样的书籍时,会存在以前请求的数据没有清空又会重新向服务器发送请求,就会出现更多的重复数据解决方法:就是在每次点x时,清空本次搜索的数据也就是Behavior里面的数据状态 ,上一次搜索的数据才不会影响本次搜索在 componentsbehaviorspagination.js 中://加入清空数据,设置初始值的方法initialize() { this.data.dataArray = [] this.data.total = null }在 componentssearchindex.js 中://在触发onConfirm函数时调用this.initialize()方法先清空上一次搜索的数据在加载onConfirm(event) { // 为了用户体验好,应该点击完立即显示搜索页面 this.setData({ searching: true }) // 先清空上一次搜索的数据在加载 this.initialize() // 获取搜索的关键词q,调取search方法返回当summary=1,返回概要数据:并更新数据到dataArray中 const q = event.detail.value || event.detail.text bookmodel.search(0, q).then(res => { this.setMoreData(res.books) this.setTotal(res.total) this.setData({ q: q }) // 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中 Keywordmodel.addToHistory(q) }) }搜索代码重构:增强代码可阅读性:在 componentssearchindex.js 中://多封装一些小的函数import { KeywordModel} from ‘../../models/keyword’import { BookModel} from ‘../../models/book’import { paginationBev} from ‘../behaviors/pagination’const Keywordmodel = new KeywordModel()const bookmodel = new BookModel()// components/search/index.jsComponent({ // 组件使用行为需加 behaviors: [paginationBev], / * 组件的属性列表 / properties: { more: { type: String, observer: ’loadMore’ } //从pages/book/book.js 传来的属性,监听滑到底步操作.只要外界传来的属性改变就会触发observer函数执行 }, / * 组件的初始数据 / data: { historyWords: [], //历史搜索关键字 hotWords: [], //热门搜索关键字 // dataArray: [], //搜索图书当summary=1,返回概要数据 searching: false, //控制搜索到的图书数据的显隐,默认不显示 q: ‘’, //输入框中要显示的内容 loading: false //表示是否正在发送请求,默认是没有发送请求 }, // 组件初始化时,小程序默认调用的生命周期函数 attached() { // const historywords = Keywordmodel.getHistory() // const hotword = Keywordmodel.getHot() this.setData({ historyWords: Keywordmodel.getHistory() }) Keywordmodel.getHot().then(res => { this.setData({ hotWords: res.hot }) }) }, /* * 组件的方法列表 */ methods: { // 只要外界传来的属性改变就会触发observer函数执行 loadMore() { console.log(‘监听函数触发到底了’) // 和onConfirm一样都需要调取search方法返回当summary=1,返回概要数据:并更新数据到dataArray中 // 先判断已得到的搜索数据的长度,在调用search方法将最新获取的数据和原来的数据拼接到一起更新数据然后呈现到页面中 if (!this.data.q) { return } // 如果是正在发送请求就什么也不做 if (this._isLocked()) { return } // const length = this.data.dataArray.length if (this.hasMore()) { this._addLocked() bookmodel.search(this.getCurrentStart(), this.data.q).then(res => { this.setMoreData(res.books) this._unLocked() }) } }, // 点击取消将搜索组件关掉,有两种方法:一是,在自己的内部创建一个变量控制显隐,不推荐,因为万一还有其他操作扩展性不好。二是,创建一个自定义事件,将自定义事件传给父级,让父级触发 onCancel(event) { this.triggerEvent(‘cancel’, {}, {}) }, // 触摸搜索图片里的x回到原来输入搜索的页面 onDelete(event) { this._hideResult() }, // 在input输入框输入完成将输入的内容加到缓存中 onConfirm(event) { // 为了用户体验好,应该点击完立即显示搜索页面 this._showResult() // 先清空上一次搜索的数据在加载 this.initialize() // 获取搜索的关键词q,调取search方法返回当summary=1,返回概要数据:并更新数据到dataArray中 const q = event.detail.value || event.detail.text bookmodel.search(0, q).then(res => { this.setMoreData(res.books) this.setTotal(res.total) this.setData({ q: q }) // 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中 Keywordmodel.addToHistory(q) }) }, // 更新变量的状态,显示搜索框 _showResult() { this.setData({ searching: true }) }, // 更新变量的状态,隐藏搜索框 _hideResult() { this.setData({ searching: false }) }, // 事件节流机制,判断是否加锁 _isLocked() { return this.data.loading ? true : false }, // 加锁 _addLocked() { this.data.loading = true }, // 解锁 _unLocked() { this.data.loading = false }, }})小问题:当加载的时候突然断网,数据还没加载完,等在恢复网络的时候,就不能继续向服务器发送请求了。问题存在的原因在于出现死锁,只有请求成功才会解锁继续发送请求,如果请求失败,就不会解锁什么也做不了。解决方法:在 componentssearchindex.js 中://只要在请求失败的回调函数里加上解锁就可以了 if (this.hasMore()) { this._addLocked() bookmodel.search(this.getCurrentStart(), this.data.q).then(res => { this.setMoreData(res.books) this._unLocked() }, () => { this._unLocked() }) }加入loading效果,提升用户体验:先创建一个loading公共组件,只需写简单的样式效果就行,在search组件中注册并使用。在 componentssearchindex.js 中:// 在 components\search\index.wxml 中:加入两个loading组件。 第一个在中间显示,获取搜获数据中;第二个在底部显示,数据加载更多时显示//<v-loading class=“loading-center” wx:if="{{loadingCenter}}"/>// <v-loading class=“loading” wx:if="{{loading}}"/>//在data中添加一个loadingCenter变量控制loading效果是否在中间显示,并且加两个私有函数控制loading的显隐。在onConfirm函数中调用this._showLoadingCenter()函数,显示loading效果,在 数据加载完成,调取this._hideLoadingCenter(),取消显示loading效果,data: {loadingCenter: false}, // 改变loadingCenter的值 _showLoadingCenter() { this.setData({ loadingCenter: true }) }, // 改变loadingCenter的值 _hideLoadingCenter() { this.setData({ loadingCenter: false }) }onConfirm(event) { // 为了用户体验好,应该点击完立即显示搜索页面 this._showResult() // 显示loading效果 this._showLoadingCenter() this.initialize() const q = event.detail.value || event.detail.text bookmodel.search(0, q).then(res => { this.setMoreData(res.books) this.setTotal(res.total) this.setData({ q: q }) Keywordmodel.addToHistory(q) // 数据加载完成,取消显示loading效果 this._hideLoadingCenter() }) },知识点补充:特别注意setData与直接赋值的区别:setData:调用setData函数更新的数据会触发页面重新渲染,和REACT里的setState相似。而直接赋值,只是在内存中改变的状态,并没有更新到页面中空搜索结果的处理:在 componentsbehaviorspagination.js 中://在公共行为中加入noneResult:false,控制是否显示没有得到想要的搜索结果,在setTotal方法中,如果返回的结果为0,就是没有得到想要的搜索结果。将noneResult:true显示出来。在initialize设置初始值并清空数据函数,再将noneResult:false,取消显示。 data: { dataArray: [], //请求返回的数组 total: null, //数据的总数 noneResult: false //没有得到想要的搜索结果 }, // 获取设置数据的 总长度 // 如果返回的结果为0,就说明没有得到搜索结果,将提示内容显示出来 setTotal(total) { this.data.total = total if (total === 0) { this.setData({ noneResult: true }) } }, // 清空数据,设置初始值,将提示隐藏 initialize() { this.setData({ dataArray: [], noneResult: false }) this.data.total = null } 在 componentssearchindex.wxml 中://加入空搜索显示的结果结构<text wx:if="{{ noneResult}}" class=“empty-tip”>没有搜索到书籍</text>在 componentssearchindex.js 中:// 触摸搜索图片里的x回到原来输入搜索的页面,先回到初始值,再将搜索组件隐藏。在onConfirm中,不用等数据加载完,输入完成后就把输入的内容显示在输入框中。onDelete(event) { this.initialize() this._hideResult() }, onConfirm(event) { this._showResult() this._showLoadingCenter() const q = event.detail.value || event.detail.text // 不用等数据加载完,输入完成后就把输入的内容显示在输入框中。 this.setData({ q: q }) bookmodel.search(0, q).then(res => { this.setMoreData(res.books) this.setTotal(res.total) Keywordmodel.addToHistory(q) this._hideLoadingCenter() }) }, 处理一个小问题:就是在热门搜索里搜索王小波,返回的搜索结果页面每本书里会显示有喜欢字样,去掉喜欢字样。在 componentsbookindex.js 中://在 components\search\index.wxml 中://通过搜索组件搜索显示的书籍都不显示喜欢字样,通过属性传值的方式将喜欢字样去掉,把false传递给子组件,子组件通过showLike变量接收,通过数据控制显隐将喜欢字样去掉。<v-book book="{{item}}" class=“book” show-like="{{false}}"></v-book>//添加一个showLike属性,代表每本书里面的喜欢字样是否显示properties: { book: Object, showLike: { //控制每本书下面有个喜欢字样的显示与隐藏 type: Boolean, value: true } },//在 components\book\index.wxml 中://showLike属性的显示和隐藏控制喜欢字样的显示和隐藏 <view class=“foot” wx:if="{{showLike}}"> <text class=“footer”>{{book.fav_nums}} 喜欢</text> </view>对 search 组件进一步优化,将锁提取到分页行为中:在 componentsbehaviorspagination.js 中://把在components\search\index.js中的三个锁方法提取到公用行为方法中,在公用行为方法中,在data里添加loading:false属性。在initialize函数中,把loading:false也加进去即可// 事件节流机制,判断是否加锁 isLocked() { return this.data.loading ? true : false }, // 加锁 addLocked() { this.setData({ loading: true }) }, // 解锁 unLocked() { this.setData({ loading: false }) },两种方法监听移动端触底的操作:scroll-view 或 Pages 里的 onReachBottom。如果要想用scroll-view把view组件换成scroll-view就可以。微信open-data显示用户信息:https://developers.weixin.qq.com/miniprogram/dev/api/wx.getUserInfo.html用户授权,需要使用 button 来授权登录。很多时候我们需要把得到信息保存到我们自己的服务器上去,需要通过js代码中操作用户头像等信息。封装一个image-button通用组件就可以,改变他的图片,并且可以在不同的方式中调用,只需要改变open-type属性就可以。分享的实现:自定义分享button:小程序之间的跳转:这两个小程序都必须关联同一个公众号==============================================================bug解决在components/episode/index.js中报错RangeError: Maximum call stack size exceeded:原因://错误写法properties: { index: { //默认显示为0 type: String, observer: function(newVal, oldVal, changedPath) { let val = newVal < 10 ? ‘0’ + newVal : newVal this.setData({ index: val }) } } },//小程序的observer,相当于vue中的watch监听函数,只要监听的index数据改变,就会调用observer方法,会形成死循环,就会报错RangeError: Maximum call stack size exceeded解决: //第一种解决方法 this.setData({ _index: val }) data: { year: 0, month: ‘’, _index: ’’ },//第二种解决方法 (推荐)在components/music/index.js中:报错:setBackgroundAudioState:fail title is nil!;at pages/classic/classic onReady function;at api setBackgroundAudioState fail callback functionError: setBackgroundAudioState:fail title is nil!原因:少 title 外传属性解决://在components/music/index.js中:properties: { src: String, title: String },methods: { // 为图片绑定触摸播放事件 onPlay: function() { //图片切换 this.setData({ playing: true }) mMgr.src = this.properties.src mMgr.title = this.properties.title } }—————————————————————————–//在 app.json 中加上:“requiredBackgroundModes”: [ “audio” ],============================================================================移动端增加用户体验优化在components/navi中:点击的左右小三角要足够大,用户触摸时才能点击到。两种方法,第一种是再切图时,切得大一些;第二种是,自己编写代码控制操作区域完成效果展示:视频地址 ...

April 10, 2019 · 13 min · jiezi

微信小程序内使用canvas绘制自定义折线图表

话不多说,最终实现效果如下:图中难点:圆角矩形绘制;转载他人帖子:看此处:https://www.jb51.net/article/…最左或者最右边的气泡需要做动态偏移本项目是由mpvue写的小程序:所以用的是vue的书写格式(微信小程序可以自行修改):使用方法:将下列代码新建linechart.vue文件再项目中调用本组件的drawAll方法传入日期和值即可代码中有少量注解请不懂的给我留言<template> <div class=“linechart”> <canvas class=“circle” canvas-id=“canvasline” style=“width: 750rpx;height: 280rpx;"> </canvas></div></template><script> export default { data() { return { canvasWidth: 375, canvasHeight: 123, date: [’-/-’,’-/-’,’-/-’,’-/-’,’-/-’,’-/-’,’-/-’], value: [0,0,8,10,6,0,0,], len: 4, xcoords: [] } }, onLoad() { this.drawAll() }, methods: { drawAll(date, value) { this.date = date || this.date this.value = value || this.value var ctx = wx.createCanvasContext(‘canvasline’) this.roundRect(ctx, this.px2PX(10), 0, this.px2PX(this.canvasWidth) - this.px2PX(20), this.px2PX(this.canvasHeight), this.px2PX(8), ‘#F5F3ED’); this.drawYLine(ctx, this.px2PX(20), 0, this.px2PX(20), this.px2PX(this.canvasHeight),this.px2PX(55), this.px2PX(1), ‘white’) this.drawXLine(ctx, this.len, this.px2PX(1), ‘white’); this.drawLine(ctx, this.px2PX(1.5), this.px2PX(3)) ctx.draw() }, px2PX(px) { // px (Int) 375为设计稿宽度,根据屏幕动态设置像素大小解决模糊问题和适配 return (wx.getSystemInfoSync().screenWidth / 375) * Number(px) }, /** * * @param {CanvasContext} ctx canvas上下文 * @param {number} x 圆角矩形选区的左上角 x坐标 * @param {number} y 圆角矩形选区的左上角 y坐标 * @param {number} w 圆角矩形选区的宽度 * @param {number} h 圆角矩形选区的高度 * @param {number} r 圆角的半径 * @param {color} fillColor 填充的颜色 / // 绘制矩形 roundRect(ctx, x, y, w, h, r, fillColor) { if (w < 2 * r) r = w / 2; if (h < 2 * r) r = h / 2; // 开始绘制 ctx.beginPath() // 因为边缘描边存在锯齿,最好指定使用 transparent 填充 // 这里是使用 fill 还是 stroke都可以,二选一即可 // ctx.setFillStyle(’transparent’) // ctx.setStrokeStyle(’transparent’) // 左上角 ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5) // border-top ctx.moveTo(x + r, y) ctx.lineTo(x + w - r, y) ctx.lineTo(x + w, y + r) // 右上角 ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2) // border-right ctx.lineTo(x + w, y + h - r) ctx.lineTo(x + w - r, y + h) // 右下角 ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5) // border-bottom ctx.lineTo(x + r, y + h) ctx.lineTo(x, y + h - r) // 左下角 ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI) // border-left ctx.lineTo(x, y + r) ctx.lineTo(x + r, y) ctx.setFillStyle(fillColor); // 这里是使用 fill 还是 stroke都可以,二选一即可,但是需要与上面对应 ctx.fill() // ctx.stroke() ctx.closePath() // 剪切 // ctx.clip() }, /* * * @param {CanvasContext} ctx canvas上下文 * @param {number, number, number, number} x1, y1, x2, y2 第一条线的起始坐标和结束坐标 * @param {number} spacing 线条直接的间隔 * @param {number} lineWidth 线条宽度 * @param {color} color线条的颜色 / // 绘制竖线网格和底部文字 drawYLine(ctx, x1, y1, x2, y2, spacing, lineWidth, color) { ctx.beginPath(); let width = this.px2PX(this.canvasWidth) - (x1 * 2) let len = Math.floor(width /spacing) for (let i = 0; i <= len; i++) { let spaced = spacing * i + i; this.xcoords.push(x1 + spaced) ctx.setLineWidth(lineWidth) ctx.setStrokeStyle(color) ctx.moveTo(x1 + spaced, y1); ctx.lineTo(x2 + spaced, y2); / — 底部标尺文字 – / ctx.setFontSize(this.px2PX(12)); ctx.setTextAlign(‘center’); ctx.setFillStyle(’#DFDACD’); ctx.fillText(this.date[i], x1 + spaced, y2 + this.px2PX(14)) / —- 底部标尺文字 — / } ctx.stroke() }, /* * * @param {CanvasContext} ctx canvas上下文 * @param {number} len 绘制多少条横线 * @param {number} lineWidth 线条宽度 * @param {color} color线条的颜色 / // 绘制横线网格 drawXLine(ctx, len, lineWidth, color) { ctx.beginPath(); let spaced = this.px2PX(this.canvasHeight) / len let x = this.px2PX(this.canvasWidth) for (let i = 0; i < len; i++) { let hei = spaced * i + i ctx.moveTo(0, hei); ctx.lineTo(x, hei); } ctx.setLineWidth(lineWidth) ctx.setStrokeStyle(color) ctx.stroke() }, /* * * @param {CanvasContext} ctx canvas上下文 * @param {number} width 折线的线条宽度 * @param {number} r 折线拐角的圆的半径 / // 绘制折线,折线区域,气泡,气泡文字 drawLine(ctx, width,r) { let arrMax = Math.max.apply({},this.value) let height = this.px2PX(this.canvasHeight) let hei = this.px2PX(this.canvasHeight) - this.px2PX(24) let average = arrMax <= 0 ? 0 : hei / arrMax let len = this.value.length - 1 ctx.beginPath(); / 折线 / for (let i = 0; i < len; i++) { let x1 = this.xcoords[i], y1 = height - this.value[i] * average, x2 = this.xcoords[i+1], y2 = height - this.value[i + 1] * average ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) } ctx.setStrokeStyle(’#F9B213’); ctx.setLineWidth(width); ctx.stroke() / 折线 / / 折线区域 / ctx.beginPath(); for (let i = 0; i < len; i++) { let x1 = this.xcoords[i], y1 = height - this.value[i] * average, x2 = this.xcoords[i+1], y2 = height - this.value[i + 1] * average ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) ctx.lineTo(x2, height) ctx.lineTo(x1, height) } / 折线区域 */ ctx.setFillStyle(‘rgba(249,178,19,0.08)’); ctx.fill(); for (let i = 0; i <= len; i++) { let x1 = this.xcoords[i], y1 = height - this.value[i] * average ctx.beginPath(); ctx.arc(x1, y1, r, 0, 2 * Math.PI) ctx.setStrokeStyle(’#F9B213’); ctx.setLineWidth(width); ctx.setFillStyle(‘white’); ctx.fill(); ctx.stroke() } for (let i = 0; i <= len; i++) { let x1 = this.xcoords[i], y1 = height - this.value[i] * average let defaultWidth = this.px2PX(24), defaultHeight = this.px2PX(16) let fontsize = this.px2PX(10) let lense = this.value[i].toString().length if (lense > 1) { defaultWidth = defaultWidth + lense * fontsize / 2.5 } let x = x1 - defaultWidth / 2 let y = y1 - defaultHeight - r * 2 if (i === 0) { // 第一个文字tip向右 x = x1 - fontsize / 2 ctx.setTextAlign(’left’); } else if (i === len) { // 最后一个文字tip向左 x = x - defaultWidth / 2 + fontsize / 2 ctx.setTextAlign(‘right’); } else { ctx.setTextAlign(‘center’); } this.roundRect(ctx, x, y, defaultWidth, defaultHeight, this.px2PX(8), ‘white’) ctx.beginPath(); ctx.setFontSize(fontsize); ctx.setFillStyle(’#F9B213’); ctx.fillText(’+’+this.value[i], x1, y1 - this.px2PX(10)) ctx.closePath() } } } }</script><style lang=“scss”> .linechart { width: 750upx; height: 280upx; }</style>以上列子如有疑问,请给我留言。 ...

April 10, 2019 · 4 min · jiezi

微信小程序开发中遇到的问题及解决办法:微信小程序ad自适应布局(二)

场景:微信小程序中添加广告,可以是微信广告和自定义广告的自适应布局; 问题:微信广告在小屏(比如:320)手机上或是设置ad组件父组件宽度小于300px,内容会超出布局范围; 截图效果:说明:可以从截图中看出,微信广告组件ad都自动添加了行内样式,而且其样式的权重都是最高的!imporant;解决办法: 1) 百度的解决办法是添加样式: ad { zoom: 0.8; }百度方法效果截图:说明:从截图的效果可以看出,现在广告未超出父元素,但是没有100%在父元素中撑开;这种方法我用过,个人总结是:当是使用场景宽度小于300px的时候,可以使用这种方法。附:在微信开发者工具中审查元素,元素好像错位了,不过展示效果正常,这个就忽略吧,微信开发者工具还应该和我一样,继续努力啊~2) 我的解决办法:在微信广告组件父元素上添加弹性盒子布局;样式如下.ad-block { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; }效果截图:说明:从截图中可以看出,广告正常展示。使用ad(微信广告)需要注意的问题:1、问题:添加微信广告的小程序在正式上线后微信广告不会正常展示,在开发版本和体验版本微信却展示正常;微信社区解释是:开发者首次提交广告组件审核,线上版本的告功能将暂时关闭,我们会在一个工作日完成广告组件合规性审核。2、问题:添加微信广告的小程序在正式上线并且通过审查后,如果有除了纯展示的其他需求,比如:需要点击观看广告6秒才能领取奖励。如果是微信广告,一天点击几次后,微信广告不会正常展示;解释:当前用户没有展示广告可能是由于该用户当前不适合浏览广告; 解决办法:通过在ad的广告发生错误的回调binderror可以做对应的处理。我所接的需求是:当微信广告展示不出来的时候,就展示自定义广告,通过binderror这个回调函数就可以实现这个需求。 注意:广告加载成功的回调bindload,页面每次重新渲染并且广告加载成功的话都会执行这个回调。附:社区中的官方解释我暂时没有找到,后期如果我找到了,再附上链接地址。随记:今天是4月7号,清明就剩一天咯。已踏青,已出游,该静一下咯。

April 7, 2019 · 1 min · jiezi

WordPress版微信小程序3.2版发布

WordPress版微信小程序(下称开源版)距离上次更新已经过去大半年了,在此期间,我开发新的专业版本-微慕小程序(下称微慕版),同时开源版的用户越来越多,截止到2018年11月26日,在github star 数达到795。很多人会有疑问,我开发了微慕版,是否放弃更新开源版,现在我给你答案:不会。我会继续完善和优化。WordPress版微信小程序3.2版发布,就是对这个答案的践行。微慕版虽然在表面上只是在开源版上增加了一点功能,但实际上无论是插件程序代码,还是小程序的代码都完全重构了,真正朝“专业”迈进了。有关微慕版的详细介绍,请看文章:微慕-专业WordPress微信小程序通过微慕版的开发,我踏不少的坑,也积累很多开发wordpress插件和微信小程序的经验,这让我在完善和维护开源版时候,有了更多的信心和心得。WordPress版微信小程序3.2版主要的更新如下:全新的wordpress插件:REST API TO MiniProgram1.由于我是第一次开发wordpress插件,开源版的wordpress插件:wp-rest-api-for-app ,代码粗糙而丑陋,混乱的命名规则,杂乱的文件结构。此次更新,我完全重构了插件,推出新版的插件:REST API TO MiniProgram。代码结构参考wordpress 官方 rest api,遵循wordpress插件的编写规范,性能和运行速度都有所提高。下载地址:Wordpress官方下载地址:https://wordpress.org/plugins…github站下载地址:https://wordpress.org/plugins…2.插件完全兼容老版本的开源版的微信小程序前端,做到平滑的升级。升级后,不会对已经上线的微信小程序产生影响。注意:必须停用老版本的wordpress插件,才能使用启用新版本的插件。3.在新版的插件里加入了微信支付的代码,只需要在插件的后台配置微信支付商户信息,即可使用赞赏和捐赠的功能。4.浏览数的代码也放进了插件,无需改动主题的functions.php文件,只需要在主题的显示浏览数位置加入: < ?php ram_post_views(’ ‘, ’ 次’); ?>5.我已将插件的代码提交到wordpress官方插件,希望可以通过审核。如果通过审核,更新插件就可以通过wordpress后台直接更新了。完善优化小程序的授权登录功能2018年10月10日起新提交的版本,用户在小程序、小游戏中需要点击组件后,才可以触发登录授权弹窗、授权自己的昵称头像等数据。本次更新,调整授权登录的代码,以适应微信getUserInfo接口的调整,并优化了用户信息的缓存。有关开源版,你有什么好的建议,欢迎告诉我,我们一起来完善这个开源项目。谢谢你阅读这篇文章,谢谢你对我的支持。

April 2, 2019 · 1 min · jiezi

微慕-专业WordPress微信小程序

2018年9月,微慕小程序(以下简称微慕版)发布以来,一直想写一篇详细详细的说明文字,全面解读这套专业的WordPress小程序。昨天,又上线了一个稳定版本后,我才下决心,也更有信心,写点文字,向你推荐我花了半年时间开发这套小程序产品。自从2017年初,我发布开源版的WordPress微信小程序(以下简称开源版)。开源版程序最开始是我学习小程序开发的成果,为了让更多的WordPress站长可以方便搭建自己的微信小程序,于是我就开源到了github上,没想到很多WordPress站长喜欢,截止到2018年11月8日,在github star 数达到755。程序后续也不断地优化和完善,现在已经更新到3.15版本了。开发微慕版的初衷开源版是我当初学习的作品,因此无论是WordPress插件还是小程序的架构都比较初级,站在专业程序员的角度来看,代码写得很丑陋。同时存在一些不足:1、WordPress 插件性能不高,代码容错机制不好。2、WordPress 插件代码结构有些混乱和随意,没有遵循wordpress插件的编写规范。3、微信小程序结构不够完善,不利于二次开发,界面也不够友好。4、缺乏完善的用户会员中心,无法灵活适应微信授权机制的调整。5、缺少互动的功能,只能简单地阅读和评论。6、配置不够灵活,改动设置需要修改代码,重新提交审核。为了解决这些痛点,我曾想去整体重构开源版,但发现如果在开源版基础上去重构的话,积重难返,难度很大,同时无法做到版本的兼容性,对已经上线的小程序带来不良的影响。于是在2018年初,萌生了重新开发一个专业版的微信小程序的想法。经过半年的努力(时间这么长,主要我水平差,还有拖拉导致)我完全重写了插件和小程序的代码。在我的眼里,专业的WordPress小程序应该有哪些特点呢?有关代码结构部分,我会在以后的文章里详细介绍,本重点介绍微慕版在专业性方面的特点。基础功能微慕版的基础功能涵盖开源版所有的功能,部分功能优于开源版,基础功能列表如下:1.多种缩略图方式显示文章列表(首页,分类文章),包括显示文章分类和发布时间,分页加载;同时用户可以自己选择显示的方式,并立即生效2.在首页用轮播方式显示指定文章。3.显示文章分类(专题),包括显示分类的封面图片。支持多级分类,理论上支持无限分类。考虑在小程序里的显示,目前小程序支持3级分类。4.显示文章内容页,包括文章站内链接跳转,站外链接复制到剪切板,显示猜你喜欢的相关文章。5.显示文章评论,提交评论和回复评论,加载评论分页,显示微信用户评论者的头像。回复评论,给评论者发送回复的模板消息。6.支持全文搜索。7.授权用户对文章点赞,转发,分享,或阅读原文,实时显示文章浏览数,点赞数,评论数。8.支持微信支付对文章赞赏,赞赏后发送模板消息致谢赞赏人。9.web-view内嵌网页跳转。10.生成带小程序二维码文章海报,用于转发朋友圈及微信群。高级功能1.付费阅读虽然点赞可以通过微信支付的方式,让文章的作者获取经济的收益,但这个过程是比较被动。在内容创业不断深入的今天,为内容付费已经成为互联网中比较普遍的消费行为了。有不少的作者,希望提供有价值的内容给有需要的人,同时获得一份收益。微慕版支持付费阅读,可以在微慕插件的后台设置专题订阅收费和单篇付费阅读。比如在目录分类设置付费项目:也可以在针对单篇文章设置付费项目:设置后,在小程序端打开文章后会显示需要支付才可以阅读。同时,在pc端打开文章后,也提示需要在小程序支付后才可以阅读注意:目前微信限制在ios设备的虚拟支付,因此付费阅读目前只支持安卓支付。通过安卓支付后,可以在苹果设备打开文章。2.在线投稿微慕版小程序支持具有投稿权限的用户,通过小程序撰写文章和在线投稿。不必打开pc版的WordPress发表文章,直接可以通过手机来发布。微慕版支持通过小程序端提交内容和图片。后续版本将会考虑支持投递付费文章,作者在投稿时,可以设置付费阅读的金额。让作者和小程序管理者共同分享收益。3.动态圈子微信小程序是微信端一个重要的功能,那么互动交流成为一种必不可少的需要,如果缺乏互动,只简单地阅读,小程序的留存率就会不高。微慕版小程序支持WordPress的论坛插件bbpress,微慕小程序插件提供了对bbpress插件的api支持,让bbpress的论坛可以通过rest api支持小程序端,主要功能包括发表话题和回帖。4.积分系统为了增强小程序的互动性,鼓励用户为小程序提供内容。微慕版提供了一套积分系统,用户登录小程序、签到、发表文章、评论文章、发表话题、回复话题、点赞、赞赏、付费阅读后都可以获得相应的积分。微慕版插件后台可以自定义积分的奖励数目后续版本将会推出用户利用积分获得相应的权限和收益,比如利用积分阅读付费内容,积分兑换奖品,积分兑换现金提现等等。5.用户中心微慕版提供了一套完整的用户中心,汇集会员在小程序里所有个性化的内容。同时小程序的管理员也可以通过微慕插件后台来管理会员用户。6.订单管理赞赏、付费阅读等都会产生微信支付,为了方便小程序的管理员,查询和统计付费的情况,微慕版插件在WordPress在后台提供了订单管理。同时用户也可以在用户中心查看账单情况:7.自定义表单字段因为WordPress提供的的自定义字段功能有限,微慕版插件提供了自定表单字段的功能,管理员可以自定义字段,组成自定义的表单,主要的用途可以做成留言、预约等功能。同时,自定义的字段也支持WordPress的文章和页面。自定义表单列表:自定义表单的编辑:通过自定义表单字段实现的留言功能:8.完善的后端管理微慕版把小程序里的相关设置尽可能放到WordPress后端里,这样改动有关选项调整后,不必更改小程序的代码,不用重新提交审核,相关设置改变后实时生效。常规设置:显示选项:功能设置:消息模版设置:9.四套小程序前端模板在微慕版里提供4套,分别是标准版、企业版、图片版、旅游版,从不同的风格角度来展示微慕小程序的功能。整个微慕小程序产品包括:一个WordPress插件+4套小程序前端模板关于开源版开源版,我会一如既往的进行维护更新,会参考微慕版的代码进行重构和优化,继续为只需要基础功能的WordPress站长提供技术支持服务。谢谢你阅读这篇文章,谢谢你对我的支持。

April 2, 2019 · 1 min · jiezi

微信小程序npm安装第三方包(引用第三方插件avtv f2.js)

由于我要使用微信小程序引用图标插件,就以AntV F2插件为例;AntV F2官网:https://antv.alipay.com/zh-cn…需要准备:微信开发工具(必须支持npm功能); node.js安装; npm基础知识;以下操作是node.js已经安装过了。1、先建好小程序模版。比如下图:然后使用dos命令打开这个当前的文件夹。比如下图:注意:dos命令打开的是小程序文件夹所放的路径。2、初始化指令如果是该文件夹第一次使用: 请先使用指令npm init(初始化指令);如下图:如果出现这种情况就对了,进行对这个文件夹进行编辑,也可以全部点击回车键,一路回车;3、再次安装npm install –production建议使用–production选项,可以减少安装一些业务无关的 npm 包,从而减少整个小程序包的大小如下图:4、安装微信小程序 F2 图表组件npm i @antv/f2-canvas如下图:恭喜你,操作完以上这几步已经安装好依赖包。5、安装好依赖包之后,打开微信开发工具点击开发者工具顶部详情,勾选 使用npm模块,再点击菜单栏中工具下的构建npm即可运行操作图第一步:点击详情,勾选npm模块操作图第二步:工具菜单栏打开,点击构建npm点击构建npm后,会出现:完成后会出现:一个文件夹为miniprogram_npm恭喜自己吧,npm安装第三方包已经完成;以下就是在小程序中代码的撰写:1.index.json;2.index.wxml;3.index.wxss;#myCanvas { width: 100%; height: 300px;}4.index.js;let chart = null;function initChart(canvas, width, height, F2) { const data = [ { year: ‘1951 年’, sales: 38 }, { year: ‘1952 年’, sales: 52 }, { year: ‘1956 年’, sales: 61 }, { year: ‘1957 年’, sales: 145 }, { year: ‘1958 年’, sales: 48 }, { year: ‘1959 年’, sales: 38 }, { year: ‘1960 年’, sales: 38 }, { year: ‘1962 年’, sales: 38 }, ]; chart = new F2.Chart({ el: canvas, width, height }); chart.source(data, { sales: { tickCount: 5 } }); chart.tooltip({ showItemMarker: false, onShow(ev) { const { items } = ev; items[0].name = null; items[0].name = items[0].title; items[0].value = ‘$ ’ + items[0].value; } }); chart.interval().position(‘yearsales’); chart.render(); return chart;}Page({ data: { opts: { onInit: initChart } }, onLoad(){ }, onReady() { }})看右侧是不是已经出来柱形图了,如果需要做其他图表,去官网让选择就行了,官网的例子比较多,选择做自己需要的就行了。 ...

April 1, 2019 · 1 min · jiezi

微信小程序访问豆瓣电影api

解决微信小程序调豆瓣电影(小说)api时显示400、403问题以获取豆瓣热映电影为例:请求接口:我这里使用的是uni-app框架(多端发布的一款框架,基于vue语法,正在踩坑中)onLoad() { uni.request({ url: ‘http://api.douban.com/v2/movie/in_theaters', method: ‘GET’, data: {}, success: res => { console.log(res) }, fail: () => {}, complete: () => {} }); },来看看返回了什么:原因是豆瓣那边设置了对小程序的访问权限解决办法总是有的!1.设置nginx代理:在nginx.conf加入以下配置:location /v2/ { proxy_store off; proxy_redirect off; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Referer ’no-referrer-when-downgrade’; proxy_set_header User-Agent ‘Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36’; proxy_connect_timeout 600; proxy_read_timeout 600; proxy_send_timeout 600; proxy_pass https://api.douban.com/v2/; }重启nginx修改小程序那边的请求路径为http://localhost/v2/movie/top2502.更改官方域名:替换官方给的域名http://api.douban.com为https://douban.uieee.com,https://douban.uieee.com是某大佬搭建的代理,相当于别人替你去做了方法一的事情,你只需要搬过来用就行了。似乎可以了我们来看看现在获取到了data没有还差一步,修改请求头header:{ “Content-Type”:“application/xml” },再来看看吧获取一下杭州的热映电影的前四个是什么吧:onLoad() { uni.request({ url: ‘https://douban.uieee.com/v2/movie/in_theaters', method: ‘GET’, header:{ “Content-Type”:“application/xml” }, data: { start: 0, count: 4, city: ‘杭州’ }, success: res => { console.log(res) }, fail: () => {}, complete: () => {} }); },附上豆瓣电影的常用接口:获取正在热映的电影:https://douban.uieee.com/v2/movie/in_theaters访问参数:start : 数据的开始项count:单页条数city:城市获取电影Top250:https://douban.uieee.com/v2/movie/top250访问参数:start : 数据的开始项count:单页条数获取即将上映电影:https://douban.uieee.com/v2/movie/coming_soon访问参数:start : 数据的开始项count:单页条数电影搜索:https://douban.uieee.com/v2/movie/search访问参数:start : 数据的开始项count:单页条数q:要搜索的电影关键字tag:要搜索的电影的标签电影详情:https://douban.uieee.com/v2/movie/subject/:id访问参数:电影id ...

March 31, 2019 · 1 min · jiezi

拓展微信小程序Component类型定义

引言和ColorUI一比,原生的导航栏简直是丑的没法看,为了不破坏框架的整体风格,我决定遵从框架的设计,也自定义导航栏。既然好多个页面都要用到,就考虑一下组件吧!原生组件原生的JavaScript组件长这样:Component({ /** * 组件的属性列表 / properties: { star: { type: Number, value: 0 } }, /* * 组件的初始数据 / data: { }, /* * 组件的方法列表 / methods: { init: function () { } }, attached: function() { this.init(); }});TypeScript组件想着使用TypeScript开始写组件,写个Component发现没有提示。咦?官方的TypeScript API里没有Component方法吗?找了一圈,也没找见。打开腾讯官方的小程序仓库,发现赫然一个大issue,1月19号提的,至今未解决。很好奇提issue的老哥最后是怎么解决的,不能官方库有bug,我就不写了吧?探究看Page源码定义Page:Page({})定义Component:Component({})通过观察,发现Component与Page两者类似,想着去看看Page是怎么定义类型的,我抄过来不就得了?.d.ts这是Page的类型声明文件,第一次点进去看,这是啥呀,啥也看不懂,一脸懵逼。最开始啥也不会,新建一个wx.extend.d.ts就开始抄代码,抄着抄着我发现我学会了。/* * 微信小程序扩展类型 组件TypeScript * @author zhangxishuo /declare namespace YunzhiComponent { /* * 组件实例对象 * Data: 组件内部属性规范 / interface ComponentInstance<Data extends IAnyObject> { /* * 组件对外属性 / properties?: object | Map<string, object>; /* * 组件内部数据 / data?: Data; /* * 组件内部方法 / methods?: object; } /* * 组件构造器 */ interface ComponentConstructor { <Data extends IAnyObject = {}, T extends IAnyObject & ComponentInstance<Data> = {}> ( options: ComponentInstance<Data> & T ): void }}declare const Component : YunzhiComponent.ComponentConstructor;总结之前也不知道.d.ts,现在写了之后,发现其实也没什么,这个文件就是写TypeScript会有代码提示,编译的时候提示提示类型,其实最终的功能,和.d.ts文件没啥关系。宝剑锋自磨砺出,梅花香自苦寒来。996.ICU要数当前最火的话题了,为什么要996呢?加班了,生产力一定提升吗?写代码是一个神奇的工作,同样一个功能,一个小时是它,三个小时也是它,如果你让他加班呢?他会怎么选呢?除非必要,永远不要加班,因为他们会把工作都留到晚上。程序员为什么不去多想想,为什么我要在这里加班?记住,世界是公平的。一句话,加班不推荐。但如果生产力低,不加班怎么办呢? ...

March 29, 2019 · 1 min · jiezi

实战:MPVue + 腾讯地图开发微信小程序

原文链接最近小程序的发展越来越火了,作为各个产品线的extra服务入口,以轻便、快速、强大的社交链吸引着大量的用户和开发者。业内开发框架层出不穷,wepy,mpvue,taro等等,都在朝着更快,更强大的方向发展,有统一 H5、微信、支付宝、百度和头条小程序的大趋势。本文旨在以mpvue框架为基础,探讨地图类小程序的开发思路,权当分享和总结。 话不多说,先体验一下:github源码地址: https://github.com/WarpPrism/...mpvue 介绍 及项目搭建mpvue = miniprogram + vue framework,说白了就是用vue框架开发小程序。mpvue最近升级为2.x版本,支持微信、支付宝、百度和头条小程序。和传统方式相比,mpvue开发具有以下优点:彻底的组件化开发能力:提高代码复用性完整的 Vue.js 开发体验方便的 Vuex 数据管理方案:方便构建复杂应用快捷的 webpack 构建机制:自定义构建策略、开发阶段 hotReload支持使用 npm 外部依赖使用 Vue.js 命令行工具 vue-cli 快速初始化项目H5 代码转换编译成小程序目标代码的能力就个人使用体验来看,还是挺丝滑顺畅的,传统web应用开发无缝切换至小程序开发,基本零门槛。要注意的就是小程序的限制及和vue的差异:小程序使用相对像素 rpx 进行样式布局部分css选择符不支持,目前只支持 #id | .class | tag | tag,tag | ::after ::before,所以要特别注意组合式生命周期,mpvue将小程序和vue的生命周期混在一块,详情见 http://mpvue.com/mpvue/#_3 ,目前这个地方还有很多坑,比如在小程序page unload时,vue实例却没被销毁,导致下次进入页面时,页面状态不变,必须在unLoad时手动重置状态等mpvue 会封装小程序数据对象,通常以$mp开头,如event.$mp.detail.target等小程序的组件和vue组件有差异,不要幻想vue组件的特性都能用,如slot,异步组件等等vue store 和 wx localstorage 最好不要弄混,要根据不同需要选择不同的存储方式不要用vue路由,要采用小程序原生的导航机制然后,我们搭建开发环境,mpvue脚手架是开箱即用的:# 全局安装 vue-cli# 一般是要 sudo 权限的$ npm install –global vue-cli@2.9# 创建一个基于 mpvue-quickstart 模板的新项目# 新手一路回车选择默认就可以了$ vue init mpvue/mpvue-quickstart my-project# 安装依赖,走你$ cd my-project$ npm install$ npm run dev接着,完善文件结构,增加 config、store、mixins等模块,如图:app.json是小程序专用文件,也需完善下:{ “pages”: [ “pages/citylist/main”, “pages/citydetail/main” ], “permission”: { “scope.userLocation”: { “desc”: “你的位置信息将用于小程序位置接口的效果展示” } }, “window”: { “backgroundTextStyle”: “light”, “navigationBarBackgroundColor”: “#eee”, “navigationBarTitleText”: “全球地铁,全程为你”, “navigationBarTextStyle”: “black” }}然后就可以愉快的写Vue代码了,咔咔一个页面,咔咔又是一个页面,组件,store,数据驱动,你喜欢的样子,它都有。腾讯地图+ 小程序着重说一下地图的接入,腾讯地图提供了两个对接入口给小程序,1是个性化地图展示,2是专用SDK,二者共同完善了小程序的地图生态。(1)个性地图展示需要开发者自行注册并申请开发者密钥(key),并在管理后台绑定小程序,然后设置个性地图的样式,才能使用:<map id=“citymap” name=“citymap” :longitude=“lng” :latitude=“lat” :polyline=“polyline” :markers=“markers” scale=“12” :subkey=“YOUR_OWN_QQMAP_KEY” show-location show-compass enable-rotate style=“width: 100%; height: 100%;"> <cover-view class=“map-cover-view”> <button class=“explore-btn” type=“primary” @tap=“exploreCity”>查看旅游攻略</button> </cover-view></map>其中,map是小程序的原生组件,原生组件脱离在 WebView 渲染流程外,它的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上。说白了就是原生组件是微信客户端提供的,它不属于内置浏览器,为此,小程序专门提供了 cover-view 和 cover-image 组件,可以覆盖在部分原生组件上面。这两个组件也是原生组件,但是使用限制与其他原生组件有所不同。笔者就因为这个坑耽误了不少时间,有时候开发工具可以用,但到了真机上组件就完全乱了,所以还是要以真机调试为准。对于原生组件,不要用太复杂的css,它的很多css属性支持的都不好。map可以定义多个参数,经纬度不用说,scale指放缩比例,也就是地图比例尺,polyline在地图上绘制折线,markers用于标记地图上的点,show-location用于显示用户所在位置,show-compass显示指北针。(2)专用SDK,目前提供这些能力:search(options:Object) 地点搜索,搜索周边poi,比如:“酒店” “餐饮” “娱乐” “学校” 等等getSuggestion(options:Object) 用于获取输入关键字的补完与提示,帮助用户快速输入reverseGeocoder(options:Object) 提供由坐标到坐标所在位置的文字描述的转换。输入坐标返回地理位置信息和附近poi列表geocoder(options:Object) 提供由地址描述到所述位置坐标的转换,与逆地址解析的过程正好相反direction(options:Object) 提供驾车,步行,骑行,公交的路线规划能力getCityList() 获取全国城市列表数据getDistrictByCityId(options:Object) 通过城市ID返回城市下的区县calculateDistance(options:Object) 计算一个点到多点的步行、驾车距离我们以公共交通路线规划为例来看下(以下代码经过简化处理):第一步,初始化地图SDK对象import config from ‘@/config’import QQMapWX from ‘../../assets/lib/qqmap-wx-jssdk.js’ // 这里用未压缩版的代码const QQMapSDK = new QQMapWX({ key: config.qqMapKey || ‘’})第二步,获取起止坐标点,并进行路线查询// 坐标从上一页query传进来,坐标为浮点数,可通过geocoder接口获取this.fromLocation = { latitude: +query.from.split(’,’)[0] || -1, longitude: +query.from.split(’,’)[1] || -1}this.toLocation = { latitude: +query.to.split(’,’)[0] || -1, longitude: +query.to.split(’,’)[1] || -1}// 查询地图路线queryMapRoutine() { QQMapSDK.direction({ mode: ’transit’, // ’transit’(公交路线规划) // from参数不填默认当前地址 from: this.fromLocation, to: this.toLocation, success: (res) => { console.log(‘路线规划结果’, res); let routes = res.result.routes; this.routes = routes.map(r => { // 对每一种路线方案,分别进行解析 return this.parseRoute(r) }) console.log(‘parsed routes’, this.routes) } })}第三步,路线解析,生成路线描述等// 解析路线,包括距离,时间,描述,路线,起止点等parseRoute(route) { let result = {} // 出发时间 result.setOutTime = formatTime(new Date()) result.distance = route.distance < 1000 ? ${route.distance}米 : ${(route.distance / 1000).toFixed(2)}公里 result.duration = route.duration < 60 ? ${route.duration}分钟 : ${parseInt(route.duration / 60)}小时${route.duration % 60}分钟 result.desc = [] // 每一个路线分很多步,如先步行,后乘公交,再搭地铁等 route.steps.forEach(step => { // if (step.mode == ‘WALKING’ && step.distance > 0) { // result.desc.push(向${step.direction}步行${step.distance}米) // } if (step.mode == ‘TRANSIT’ && step.lines[0]) { let line = step.lines[0] if (line.vehicle == ‘BUS’) line.title = 公交车-${line.title} if (line.vehicle == ‘RAIL’) line.title = 铁路 result.desc.push(${line.title}: ${line.geton.title} —&gt; ${line.getoff.title},途经 ${line.station_count} 站。) } }) result.polyline = [] result.points = [] //获取各个步骤的polyline,也就是路线图 for(let i = 0; i < route.steps.length; i++) { let step = route.steps[i] let polyline = this.getStepPolyline(step) if (polyline) { result.points = result.points.concat(polyline.points) result.polyline.push(polyline) } } // 标记路线整体显示坐标 this.getStepPolyline.colorIndex = 0 let midPointIndex = Math.floor(result.points.length / 2) result.latitude = result.points[midPointIndex].latitude result.longitude = result.points[midPointIndex].longitude // 标记路线起止点 let startPoint = result.points[0] let endPoint = result.points[result.points.length - 1] result.markers = [ { iconPath: this.startIcon, id: 0, latitude: startPoint.latitude, longitude: startPoint.longitude, width: 28, height: 28, zIndex: -1, anchor: {x: 0.5, y: 1} }, { iconPath: this.endIcon, id: 1, latitude: endPoint.latitude, longitude: endPoint.longitude, width: 28, height: 28, zIndex: -1, anchor: {x: 0.5, y: 1} } ] return result},第四步,getStepPolyline函数 获取路线每一步的路线polylinegetStepPolyline(step) { let coors = []; // 随机颜色 let colorArr = [’#1aad19’, ‘#10aeff’, ‘#d84e43’] let _dottedLine = true if (step.mode == ‘WALKING’ && step.polyline) { coors.push(step.polyline); _dottedLine = false } else if (step.mode == ‘TRANSIT’ && step.lines[0].polyline) { coors.push(step.lines[0].polyline); } else { return null } //坐标解压(返回的点串坐标,通过前向差分进行压缩) let kr = 1000000; for (let i = 0 ; i < coors.length; i++){ for (let j = 2; j < coors[i].length; j++) { coors[i][j] = Number(coors[i][j - 2]) + Number(coors[i][j]) / kr; } } //定义新数组,将coors中的数组合并为一个数组 let coorsArr = []; let _points = []; for (let i = 0 ; i < coors.length; i ++){ coorsArr = coorsArr.concat(coors[i]); } //将解压后的坐标放入点串数组_points中 for (let i = 0; i < coorsArr.length; i += 2) { _points.push({ latitude: coorsArr[i], longitude: coorsArr[i + 1] }) } if (!this.getStepPolyline.colorIndex) { this.getStepPolyline.colorIndex = 0 } let colorIndex = this.getStepPolyline.colorIndex % colorArr.length this.getStepPolyline.colorIndex++ // 最终polyline结果 let polyline = { width: 7, points: _points, color: colorArr[colorIndex], dottedLine: _dottedLine, arrowLine: true, // 带箭头的线, 开发者工具暂不支持该属性 borderColor: ‘#fff’, borderWidth: 1 } return polyline}最后,绑定到地图上并输出,我们可以得到一个大致这样的结果:广州火车站 -> 广州塔15km 1小时地铁5号线 广州火车站 -> 珠江新城,途径6站地铁3号线 珠江新城 -> 广州塔,途径1站这样我们就通过direction接口进行了简单的路线规划功能,接着把生成的数据绑定到地图组件上,一个简易的小程序就做好了,是不是很简单?当然如果想做得更好,就要调用其他相似接口,慢慢完善细节。<map id=“citymap” name=“citymap” :latitude=“currentRoute.latitude” :longitude=“currentRoute.longitude” :polyline=“currentRoute.polyline” :markers=“currentRoute.markers” scale=“12” :subkey=“qqMapKey” show-location show-compass enable-rotate style=“width: 100%; height: 100%;"></map>更多实现请参照源码和小程序本身,如果对你有帮助,可以star支持。其他这里还有一款音频类的 web应用,发布在gitee page上http://crystalworld.gitee.io/…感兴趣的朋友可以看下,之后有时间笔者会补上开发教程和源码,另作分享。 ...

March 28, 2019 · 3 min · jiezi

小程序多端框架全面测评:chameleon、Taro、uni-app、mpvue、WePY

摘要: 微信小程序开发技巧。作者:coldsnap原文:小程序多端框架全面测评Fundebug经授权转载,版权归原作者所有。最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个?作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。那么,当我们在讨论多端框架时,我们在谈论什么:多端笔者以为,现在流行的多端框架可以大致分为三类:1. 全包型这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。2. Web 技术型这类框架把 Web 技术(JavaScript,CSS)带到移动开发中,自研布局引擎处理 CSS,使用 JavaScript 写业务逻辑,使用流行的前端框架作为 DSL,各端分别使用各自的原生组件渲染。代表框架是 React Native 和 Weex,这样做的优点有:开发迅速复用前端生态易于学习上手,不管前端后端移动端,多多少少都会一点 JS、CSS缺点有:交互复杂时难以写出高性能的代码,这类框架的设计就必然导致 JS 和 Native 之间需要通信,类似于手势操作这样频繁地触发通信就很可能使得 UI 无法在 16ms 内及时绘制。React Native 有一些声明式的组件可以避免这个问题,但声明式的写法很难满足复杂交互的需求。由于没有渲染引擎,使用各端的原生组件渲染,相同代码渲染的一致性没有第一种高。3. JavaScript 编译型这类框架就是我们这篇文章的主角们:Taro、WePY 、uni-app 、 mpvue 、 chameleon,它们的原理也都大同小异:先以 JavaScript 作为基础选定一个 DSL 框架,以这个 DSL 框架为标准在各端分别编译为不同的代码,各端分别有一个运行时框架或兼容组件库保证代码正确运行。这类框架最大优点和创造的最大原因就是小程序,因为第一第二种框架其实除了可以跨系统平台之外,也都能编译运行在浏览器中。(Qt 有 Qt for WebAssembly, Flutter 有 Hummingbird,React Native 有 react-native-web, Weex 原生支持)另外一个优点是在移动端一般会编译到 React Native/Weex,所以它们也都拥有 Web 技术型框架的优点。这看起来很美好,但实际上 React Native/Weex 的缺点编译型框架也无法避免。除此之外,编译型框架的抽象也不是免费的:当 bug 出现时,问题的根源可能出在运行时、编译时、组件库以及三者依赖的库等等各个方面。在 Taro 开源的过程中,我们就遇到过 Babel 的 bug,React Native 的 bug,JavaScript 引擎的 bug,当然也少不了 Taro 本身的 bug。相信其它原理相同的框架也无法避免这一问题。但这并不意味着这类为了小程序而设计的多端框架就都不堪大用。首先现在各巨头超级 App 的小程序百花齐放,框架会为了抹平小程序做了许多工作,这些工作在大部分情况下是不需要开发者关心的。其次是许多业务类型并不需要复杂的逻辑和交互,没那么容易触发到框架底层依赖的 bug。那么当你的业务适合选择编译型框架时,在笔者看来首先要考虑的就是选择 DSL 的起点。因为有多端需求业务通常都希望能快速开发,一个能够快速适应团队开发节奏的 DSL 就至关重要。不管是 React 还是 Vue(或者类 Vue)都有它们的优缺点,大家可以根据团队技术栈和偏好自行选择。如果不管什么 DSL 都能接受,那就可以进入下一个环节:生态以下内容均以各框架现在(2019 年 3 月 11 日)已发布稳定版为标准进行讨论。1. 开发工具就开发工具而言 uni-app 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具,鼠标点点点就能编译测试发布。其它的框架都是使用 CLI 命令行工具,但值得注意的是 chameleon 有独立的语法检查工具,Taro 则单独写了 ESLint 规则和规则集。在语法支持方面,mpvue、uni-app、Taro 、WePY 均支持 TypeScript,四者也都能通过 typing 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。CSS 方面,所有框架均支持 SASS、LESS、Stylus,Taro 则多一个 CSS Modules 的支持。所以这一轮比拼的结果应该是:uni-app > Taro > chameleon > WePY、mpvue2. 多端支持度只从支持端的数量来看,Taro 和 uni-app 以六端略微领先(移动端、H5、微信小程序、百度小程序、支付宝小程序、头条小程序),chameleon 少了头条小程序紧随其后。但值得一提的是 chameleon 有一套自研多态协议,编写多端代码的体验会好许多,可以说是一个能戳到多端开发痛点的功能。uni-app 则有一套独立的条件编译语法,这套语法能同时作用于 js、样式和模板文件。Taro 可以在业务逻辑中根据环境变量使用条件编译,也可以直接使用条件编译文件(类似 React Native 的方式)。在移动端方面,uni-app 基于 weex 定制了一套 nvue 方案 弥补 weex API 的不足;Taro则是暂时基于 expo 达到同样的效果;chameleon 在移动端则有一套 SDK 配合多端协议与原生语言通信。H5 方面,chameleon 同样是由多态协议实现支持,uni-app 和 Taro 则是都在 H5 实现了一套兼容的组件库和 API。mpvue 和 WePY 都提供了转换各端小程序的功能,但都没有 h5 和移动端的支持。所以最后一轮对比的结果是:chameleon > Taro、uni-app > mpvue > WePY3. 组件库/工具库/demo作为开源时间最长的框架,WePY 不管从 Demo,组件库数量 ,工具库来看都占有一定优势。uni-app 则有自己的插件市场和 UI 库,如果算上收费的框架和插件比起 WePy 也是完全不遑多让的。Taro 也有官方维护的跨端 UI 库 taro-ui ,另外在状态管理工具上也有非常丰富的选择(Redux、MobX、dva),但 demo 的数量不如前两个。但 Taro 有一个转换微信小程序代码为 Taro 代码的工具,可以弥补这一问题。而 mpvue 没有官方维护的 UI 库,chameleon 第三方的 demo 和工具库也还基本没有。所以这轮的排序是:WePY > uni-app 、taro > mpvue > chameleon4. 接入成本接入成本有两个方面:第一是框架接入原有微信小程序生态。由于目前微信小程序已呈一家独大之势,开源的组件和库(例如 wxparse、echart、zan-ui 等)多是基于原生微信小程序框架语法写成的。目前看来 uni-app 、Taro、mpvue 均有文档或 demo 在框架中直接使用原生小程序组件/库,WePY 由于运行机制的问题,很多情况需要小改一下目标库的源码,chameleon 则是提供了一个按步骤大改目标库源码的迁移方式。第二是原有微信小程序项目部分接入框架重构。在这个方面 Taro 在京东购物小程序上进行了大胆的实践,具体可以查看文章《Taro 在京东购物小程序上的实践》。其它框架则没有提到相关内容。而对于两种接入方式 Taro 都提供了 taro convert 功能,既可以将原有微信小程序项目转换为 Taro 多端代码,也可以将微信小程序生态的组件转换为 Taro 组件。所以这轮的排序是:Taro > mpvue 、 uni-app > WePY > chameleon流行度从 GitHub 的 star 来看,mpvue 、Taro、WePY 的差距非常小。从 NPM 和 CNPM 的 CLI 工具下载量来看,是 Taro(3k/week)> mpvue (2k/w) > WePY (1k/w)。但发布时间也刚好反过来。笔者估计三家的流行程度和案例都差不太多。uni-app 则号称有上万案例,但不像其它框架一样有一些大厂应用案例。另外从开发者的数量来看也是 uni-app 领先,它拥有 20+ 个 QQ 交流群(最大人数 2000)。所以从流行程度来看应该是:uni-app > Taro、WePY、mpvue > chameleon5. 开源建设一个开源作品能走多远是由框架维护团队和第三方开发者共同决定的。虽然开源建设不能具体地量化,但依然是衡量一个框架/库生命力的非常重要的标准。从第三方贡献者数量来看,Taro 在这一方面领先,并且 Taro 的一些核心包/功能(MobX、CSS Modules、alias)也是由第三方开发者贡献的。除此之外,腾讯开源的 omi 框架小程序部分也是基于 Taro 完成的。WePY 在腾讯开源计划的加持下在这一方面也有不错的表现;mpvue 由于停滞开发了很久就比较落后了;可能是产品策略的原因,uni-app 在开源建设上并不热心,甚至有些部分代码都没有开源;chameleon 刚刚开源不久,但它的代码和测试用例都非常规范,以后或许会有不错的表现。那么这一轮的对比结果是:Taro > WePY > mpvue > chameleon > uni-app最后补一个总的生态对比图表:未来从各框架已经公布的规划来看:WePY 已经发布了 v2.0.alpha 版本,虽然没有公开的文档可以查阅到 2.0 版本有什么新功能/特性,但据其作者介绍,WePY 2.0 会放大招,是一个「对得起开发者」的版本。笔者也非常期待 2.0 正式发布后 WePY 的表现。mpvue 已经发布了 2.0 的版本,主要是更新了其它端小程序的支持。但从代码提交, issue 的回复/解决率来看,mpvue 要想在未来有作为首先要打消社区对于 mpvue不管不顾不更新的质疑。uni-app 已经在生态上建设得很好了,应该会在此基础之上继续稳步发展。如果 uni-app 能加强开源开放,再加强与大厂的合作,相信未来还能更上一层楼。chameleon 的规划比较宏大,虽然是最后发的框架,但已经在规划或正在实现的功能有:快应用和端拓展协议通用组件库和垂直类组件库面向研发的图形化开发工具面向非研发的图形化页面搭建工具如果 chameleon 把这些功能都做出来的话,再继续完善生态,争取更多第三方开发者,那么在未来 chameleon 将大有可为。Taro 的未来也一样值得憧憬。Taro 即将要发布的 1.3 版本就会支持以下功能:快应用支持Taro Doctor,自动化检查项目配置和代码合法性更多的 JSX 语法支持,1.3 之后限制生产力的语法只有 只能用 map 创造循环组件 一条H5 打包体积大幅精简同时 Taro 也正在对移动端进行大规模重构;开发图形化开发工具;开发组件/物料平台以及图形化页面搭建工具。结语那说了那么多,到底用哪个呢?如果不介意尝鲜和学习 DSL 的话,完全可以尝试 WePY 2.0 和 chameleon ,一个是酝酿了很久的 2.0 全新升级,一个有专门针对多端开发的多态协议。uni-app 和 Taro 相比起来就更像是「水桶型」框架,从工具、UI 库,开发体验、多端支持等各方面来看都没有明显的短板。而 mpvue 由于开发一度停滞,现在看来各个方面都不如在小程序端基于它的 uni-app 。当然,Talk is cheap。如果对这个话题有更多兴趣的同学可以去 GitHub 另行研究,有空看代码,没空看提交:chameleon: https://github.com/didi/chameleonmpvue: https://github.com/Meituan-Dianping/mpvueTaro: https://github.com/NervJS/tarouni-app: https://github.com/dcloudio/uni-appWePY: https://github.com/Tencent/wepy(按字母顺序排序)关于FundebugFundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用! ...

March 28, 2019 · 2 min · jiezi

【实战教程】只需三步,用云函数又快又安全地实现小程序支付

本文主要侧重于讲述小程序在线支付功能中的编程思想和编程模式,并在必要的地方提供关键代码示例。(文末也将附上关键的 js 代码)为方便演示,这里将实现一个最简单的虚拟商品的订单支付功能,订单略去了收货地址和多规格、多数量的情况,示例中仅讨论在商品详情页中直接创建订单并发起支付的情况。需要分别定义 Product 表和 Order 表进行数据存取,在 BaaS 后台中创建两张数据表。一、数据表结构设计Product 表:数据表录入权限:所有人数据行读写权限:创建者可写,所有人可读Order 表:数据表录入权限:所有人数据行读写权限:创建者可写,创建者可读商品的订单结算和支付流程一般包括“创建订单 -> 支付 -> 更新订单状态”三个步骤。下文中将分析几种实现该流程的方案,供我们一起探讨。二、客户端创建订单,客户端更新订单状态我们先来看下只在客户端中如何处理这些逻辑。1) 创建订单:Order 表中创建一条新记录,status 字段默认值为 “no_paid”,保存订单金额,商品快照和商品 id 以及订单创建者,其中订单创建者由 BaaS 的用户系统自动处理,值为创建订单的用户 id:/** * 创建订单处理函数 /createOrderHandle() { const orderTableId = 12345678 const tableObject = new wx.BaaS.TableObject(orderTableId) const createObject = tableObject.create() const product = this.data.product const data = { product_id: product.id, product_snapshot: product, total_cost: product.price, status: ’no_paid’, } // 客户端创建订单,客户端更新订单状态 return createObject.set(data).save().then(res => { this.order = res.data || {} return this.pay(this.order) }).then(transactionNo => { return this.updateOrder(transactionNo) }).then(res => { wx.navigateTo({ url: ‘../order/order’ }) })2)支付:调用 BaaS SDK 提供的支付方法 wx.BaaS.pay,调起微信支付:/* * 发起微信支付 * @param {Object} order /pay(order) { const product = this.data.product const orderTableId = 12345678 const params = { totalCost: order.total_cost, merchandiseDescription: product.title, merchandiseSchemaID: orderTableId, merchandiseRecordID: order.id, merchandiseSnapshot: product, } return wx.BaaS.pay(params).then(res => { return res.transaction_no })}3)更新订单状态:支付成功后,更新 status 字段值为 “paid”,并更新微信支付序列号:/* * 更新订单状态 * 仅在由客户端更新订单状态时使用 * @param {String} transaction_no 支付成功后由微信返回的微信支付序列号 /updateOrder(transaction_no) { const orderTableId = 12345678 const tableObject = new wx.BaaS.TableObject(orderTableId) const recordId = this.order.id const record = tableObject.getWithoutData(recordId) record.set(‘status’, ‘paid’) record.set(’transaction_no’, transaction_no) return record.update()}我们从整体上来看支付流程,便能发现订单状态实质上是由客户端中 updateOrder 方法发起请求来进行更新的。而这一情况将导致极大的安全隐患。因为从原则上来说,我们认为来自客户端的信息都是不可信的,订单状态很容易被伪造出的一个请求跳过支付直接将状态更新为 ‘paid’,并更新一个假的 transaction_no。这意味着,不花一分钱也能将订单变为已支付。在生产环境中,任何情下都不应该使用这种支付流程。三、客户端创建订单,触发器更新订单状态基于这种情况,你或许会想:既然由客户端来更新订单状态会引起安全问题,又没有后端开发者参与,要怎么做?BaaS 平台中触发器和云函数可以帮你解决这个问题。它们可以完成这种非客户端的处理逻辑,同时使用它们的时候跟开发后端应用又有很大的不同。首先来看一下触发器(Trigger),触发器是一种当触发条件被满足,将会执行触发器中的事先定义的动作,定义好的动作可以是操作数据库或者调用云函数。我们希望当支付完成之后,触发器可以帮我们自动地操作数据库,更新订单对应的 status 和 transaction_no 字段。触发器设置如下:「触发类型」选择微信支付回调,条件是支付成功后执行触发器。一般触发器类型常见的还有操作数据表,定时任务等,分别对应操作数据表后触发和定时触发。「动作」定义了触发器将要执行的操作,这里是更新 Order 数据表对应的 status、total_cost 和 transaction_no 字段。更多触发器的具体细节,不同平台的实现有所不同,在此不展开讨论。借助触发器,客户端创建订单成功后不需要再调 updateOrder 方法,Order 订单的数据会自动更新成支付成功对应的状态:/* * 创建订单处理函数 /createOrderHandle() { … // 与上文相同 // 客户端创建订单,触发器自动更新订单状态 return createObject.set(data).save().then(res => { this.order = res.data || {} return this.pay(this.order) }).then(res => { wx.navigateTo({ url: ‘../order/order’ }) })}值得注意的是,上面介绍的第一种方案中 Order 表的 ACL 数据行读写权限是创建者可写的,意味着创建者可以对数据进行任意操作,将更新订单状态的工作交给触发器后,Order 表的 ACL 数据行读写权限应设置为「不可写」,保证 Order 表的数据创建后不会由外部更改,提高了数据的安全性。四、云函数创建订单,触发器更新订单状态细心的读者可能发现了除了 status 和 transacton_no 字段外,还由触发器自动更新了 total_cost 字段,保存的是实际支付的金额。这就引出了另外一个问题,虽然现在不能通过客户端修改订单状态,但是创建订单的所有数据仍是由客户端发起请求,在请求参数中定义的,这种方式同样很容易被人篡改数据,比如 1000 元的商品可以被更改成 1 元甚至 0 元,造成只需要花很少的钱就可以买到高价值的商品。使用触发器自动根据微信支付回调更新 total_cost 可以保证无论何种情况下,数据中保存的都是最终用户实际支付的金额。虽然这种方式可以事后帮助我们发现订单金额异常的问题,但还是不能解决在创建订单时金额被篡改的问题,这又要如何解决呢?这时候创建订单的功能应该交给后端逻辑去做了,在 BaaS 平台中就需要用到云函数了,云函数又被称为 FaaS(Functions as a Service)函数即服务。云函数是一段可以部署在服务端的代码,关键词是一段代码,而不是一整套的后端逻辑,它本质上就是函数而已,特别是对于运行在 node.js 环境下的云函数来说,它跟平常所写的 JavaScript 代码几乎一模一样,对前端开发者来说非常容易上手。云函数可以由 SDK 或触发器调用,也可以在云函数之间相互调用。为了避免创建订单时客户端数据篡改或商品信息不能实时同步的问题,我们将创建订单的逻辑迁移到 BaaS 平台的云函数中:关注「知晓云」微信公众号,在微信后台回复「创建订单」,获取完整的【创建订单】云函数源码。调用该云函数时传入商品 id,云函数先查出此商品的具体信息,再使用该商品信息来创建订单,整个过程在 BaaS 平台的云函数系统中完成,保证了数据的准确性。支付完成后,触发器同样会自动更新订单状态。客户端中使用 invokeFunction 方法调用云函数:/* * 创建订单处理函数 /createOrderHandle() { … // 与上文相同 // 使用云函数创建订单,触发器更新订单状态 wx.BaaS.invokeFunction(‘createOrder’, { product_id: this.data.product.id }).then(res => { this.order = res.data || {} return this.pay(this.order) }).then(res => { wx.navigateTo({ url: ‘../order/order’ }) })}由于创建订单和更新订单的操作已经分别交由云函数和触发器处理了,为了更好的安全性,Order 表的数据创建权限和修改权限都不应该对客户端开放。需要额外说明的是,而触发器和云函数系统级别的操作,相当于拥有最高权限,所以我们这里相当于禁止了客户端中除了读取数据外的所有操作,也就使得 Order 表的权限控制和数据的准确性得到了安全的保障。五、云函数创建订单,云函数校验并更新订单状态我们再来研究一下代码,在 pay 这个方法中 wx.BaaS.pay(params) 所做的事情实际上是发起一个请求,获取 BaaS 系统返回的支付解密数据,然后使用这些支付解密数据调用微信客户端的支付功能,最终由用户输入密码完成支付。同理,根据客户端提供的数据都不可信的原则,这个请求中 params 参数时的数据同样可以被伪造,比如修改掉 totalCost 的值,也会导致最终支付的金额跟实际应该支付的金额不一值,根据之前触发器的设定,虽然会如实地记录了最终支付的金额,可以为后台追溯金额异常的订单提供依据,但是并不会阻止订单更新为已支付的状态。当用户支付成功后,我们更希望在更新订单状态前可以先进行支付数据的校验,校验不通过则不更新订单状态。想要实现这个功能,则要将触发器和云函数进行搭配使用了。先将触发器的动作类型改为云函数:微信支付成功后会触发调用 verifyPayment 云函数:客户端的代码保持不变,此时整个流程是:调用 createOrder 云函数创建订单,拿到创建订单成功的回调数据后,发起支付,支付成功之后,由触发器自动调用 verifyPayment 云函数,校验实付金额是否跟该商品的价格一致,若一致则更新该订单为已支付状态。在 verifyPayment 云函数中只考虑了校验实付金额这一个维度,在实际开发中应综合考虑更多维度来确保数据准确,在此不再展开讨论。至此,本文完成了一个小程序在线支付的案例,介绍了如何借助 BaaS 平台最快地实现小程序在线支付功能,通过开发过程中发现的各种安全问题,迭代出四种不同的实现方案,一步步完善支付功能的安全性,最后得出一个最快最安全实现小程序在线支付的方案。六、商品详情页和云函数 js 代码商品详情页 js 代码/* 商品详情页 js 代码 /const productTableId = 12345678const orderTableId = 123456789Page({ data: { product: {} }, onLoad(options) { // 设置默认的商品 id,方便调试 const productId = options.id || ‘5ade97135acfb521865bf766’ this.getProductDetail(productId) }, / * 获取商品详情信息 * @param {String} id / getProductDetail(id) { const tableObject = new wx.BaaS.TableObject(productTableId) const query = new wx.BaaS.Query() query.compare(‘id’, ‘=’, id) tableObject.setQuery(query).find().then(res => { const objects = res.data.objects || [] const product = objects[0] || {} this.setData({ product }) }) }, /* * 点击立即购买按钮事件 / createOrder(e) { wx.getSetting({ success: res => { if (res.authSetting[‘scope.userInfo’]) { this.createOrderHandle() } else { wx.BaaS.login() } } }) }, /* * 创建订单处理函数 / createOrderHandle() { const tableObject = new wx.BaaS.TableObject(orderTableId) const createObject = tableObject.create() const product = this.data.product const data = { product_id: product.id, product_snapshot: product, total_cost: product.price, status: ’no_paid’, } // 客户端创建订单,客户端更新订单状态 // return createObject.set(data).save().then(res => { // this.order = res.data || {} // return this.pay(this.order) // }).then(transactionNo => { // return this.updateOrder(transactionNo) // }).then(res => { // wx.navigateTo({ url: ‘../order/order’ }) // }) // 客户端创建订单,触发器更新订单状态 // return createObject.set(data).save().then(res => { // this.order = res.data || {} // return this.pay(this.order) // }).then(res => { // wx.navigateTo({ url: ‘../order/order’ }) // }) // 使用云函数创建订单,触发器或云函数更新订单状态 wx.BaaS.invokeFunction(‘createOrder’, { product_id: this.data.product.id }).then(res => { this.order = res.data || {} return this.pay(this.order) }).then(res => { wx.navigateTo({ url: ‘../order/order’ }) }) }, /* * 发起微信支付 * @param {Object} order / pay(order) { const product = this.data.product const params = { totalCost: order.total_cost, merchandiseDescription: product.title, merchandiseSchemaID: orderTableId, merchandiseRecordID: order.id, merchandiseSnapshot: product, } return wx.BaaS.pay(params).then(res => { return res.transaction_no }) }, /* * 更新订单状态 * @param {String} transaction_no 支付成功后返回的微信支付订单号 / updateOrder(transaction_no) { const tableObject = new wx.BaaS.TableObject(orderTableId) const recordId = this.order.id const record = tableObject.getWithoutData(recordId) record.set(‘status’, ‘paid’) record.set(’transaction_no’, transaction_no) return record.update() }})创建订单云函数/* 创建订单云函数 /const productTableId = 12345678const orderTableId = 123456789exports.main = function createOrder(event, callback) { const {product_id} = event.data const user_id = event.request.user.id getProductDetail(product_id).then(product => { return createOrderHandel(product, user_id) }).then(res => { const order = res.data || {} callback(null, order) }).catch(err => { callback(err) })}function getProductDetail(id) { const tableObject = new BaaS.TableObject(productTableId) const query = new BaaS.Query() query.compare(‘id’, ‘=’, id) return tableObject.setQuery(query).find().then(res => { const objects = res.data.objects || [] const product = objects[0] || {} return product })}function createOrderHandel(product, user_id) { const tableObject = new BaaS.TableObject(orderTableId) const createObject = tableObject.create() const data = { product_id: product.id, product_snapshot: product, total_cost: product.price, status: ’no_paid’, created_by: user_id } return createObject.set(data).save()}校验并更新订单状态云函数/ 校验并更新订单状态云函数 **/const productTableId = 12345678const orderTableId = 123456789exports.main = function verifyPayment(event, callback) { const data = event.data const totalCost = data.total_cost const orderId = data.merchandise_record_id const transactionNo = data.transaction_no const merchandiseSnapshot = data.merchandise_snapshot const productId = merchandiseSnapshot.id getProductDetail(productId).then(product => { if (product.price === totalCost) { updateOrder(orderId, transactionNo) } })}function getProductDetail(id) { const tableObject = new BaaS.TableObject(productTableId) const query = new BaaS.Query() query.compare(‘id’, ‘=’, id) return tableObject.setQuery(query).find().then(res => { const objects = res.data.objects || [] const product = objects[0] || {} return product })}function updateOrder(orderId, transaction_no) { const tableObject = new BaaS.TableObject(orderTableId) const recordId = orderId const record = tableObject.getWithoutData(recordId) record.set(‘status’, ‘paid’) record.set(’transaction_no’, transaction_no) return record.update()}知晓云是国内首家专注于小程序开发的后端云服务。使用知晓云,小程序开发快人一步。 ...

March 25, 2019 · 4 min · jiezi

微信小程序登录

概况描述后台使用Spring boot,部分功能需要在小程序上实现。我们的后台是有权限验证的,所以需要用户登录才能调用后端的API。所以,小程序端就需要登录后端。本文实现了微信小程序的登录来完成自己服务器后端的登录功能。实现原理首先,我们需要登录小程序,用微信对其授权,然后带着授权的状态进行后端的登录。我们来看一下微信官方提供给我们的时序图:我们看到,最开始会调用wx.login以获取code,然后携带获取的code,去请求后我们的后端的。然后,我们需要在后端,使用获取的code和小程序的appId以及secret,共同请求微信接口,实现授权,然后微信会返回给我们openId和session_key。接着,我们需要将openId和session_key进行保存,就和我们原来后端的登录做法一样,对登录的用户进行session存储,然后再登录的时候,去session中请求,看是否存在用户信息,以达到验证登录的目的。这之后,我们就可以带着登录的状态去请求后端的API了。大致的原理知道了,我们就去实现一下登录的过程。实现小程序端首先我们要知道一点,如果要验证你是哪个用户,就需要微信进行授权。微信给我们提供了比较方便的解决办法。button中有这么一个属性:open-type,通过他我们可以直接去获取用户的微信信息。<button open-type=‘getUserInfo’ bindgetuserinfo=‘doLogin’>登录</button>将open-type设置为getUserInfo,然后再使用bindgetuserinfo去触发登录的方法,这个函数会返回用户的信息。然后,就是调用wx.login()来获取code。 wx.login({ success: function(res) { console.log(res) } })当微信端登录成功的时候,我们就会获取到相应的code,这是用户的临时登录凭证,每次登录都会不同,过一段时间也会失效:接着带着code去请求我们的后端: wx.login({ success: function(res) { console.log(res) // 获取登录的临时凭证 var code = res.code; // 调用后台,获取session_key,openid wx.request({ url: ‘http://localhost:8080/user/wxLogin?code=’ + code, method: ‘POST’, }) } })好了,现在我们需要一个支撑微信小程序登录的后台了。后端控制器: /** * 微信登录 * @param code 登录临时凭证code */ @PostMapping("/wxLogin") public voidwxLogin(String code) { userService.wxLogin(code); }service: public void wxLogin(String code) { String url = “https://api.weixin.qq.com/sns/jscode2session?" + “appid=APPID&” + “secret=SERECT&” + “js_code=” + code + “&” + “grant_type=authorization_code”; logger.debug(“请求微信api,进行登录授权,获取session_key和openid”); String jsonString = restTemplate.getForObject(url, String.class); JSONObject jsonObject = JSONObject.parseObject(jsonString); logger.debug(“获取openid,进行存储”); String openid = jsonObject.get(“openid”).toString(); httpSession.setAttribute(“openid”, openid); }因为这里,对于同一个用户而言,openid是相同的,所以,当下次再来请求的时候,我们只需要获取它,就能判断是否登录了。后面,对于其他请求的处理,还需要涉及到拦截器,但是原理和之前一样,这里就不做赘述。官方参考:https://developers.weixin.qq….https://developers.weixin.qq….https://developers.weixin.qq….https://developers.weixin.qq….https://developers.weixin.qq…. ...

March 25, 2019 · 1 min · jiezi

初探微信小程序

最近的项目中,需要学生端使用微信小程序,所以这两天做了一下小程序的原型。在这里总结一下踩过的坑。组件库的选择上来遇到的第一个大问题就是组件的选择。因为微信原生的实在是又不好看,又不好用。虽然有官方文档,但不得不说,不适合新手使用。第一次选择: WeUI。这是以前老师提到过的一个组件库,所以先尝试了它。但是效果不大好。有两个问题:1.组件库不强大。虽然涉及的种类挺全面的,但是一旦想去实现某个功能时,总会有不顺手的地方。2.没有文档。WeUI本身是有较完善的文档的,但是小程序版的就没有了,所以就造成了想去实现一个功能,就只能根据他给的Demo去扒代码了。第二次选择:ZanUI。一个第三方组件库,相较上面的来说,最大的提升就是文档完善。组件方面也是种类比较齐全的。基本上可以满足开发的需求。第三次选择:VantUI。它是ZanUI的升级版本,功能上更加齐全,而且同种的功能有了更多的选择,可以适应更多变的场景。文档方面也是极其优秀的,不仅有适合新手的入门教程,最大的亮点就是可以同步展示,可以更快的找到想要的效果。最终使用了VantUI进行原型的制作,虽然过程中还是有些地方没法更设想的完全一样,但是好在还能找到替代的解决办法。总体上比较满意。返回键基本上我们看到的小程序,上面的返回键都是不可缺少的。最开始,路由跳转,使用官方给的API进行路由跳转:wx.redirectTo({ url: ‘/pages/personal/changePassword/changePassword’})然后就是添加返回按钮了。找到组件文档,添加个返回键:<van-nav-bar title=“标题” left-text=“返回” left-arrow bind:click-left=“onClickLeft”/>结果是这样的,好像还行,到是感觉有点别扭。打开微信,发现人家的样式都是只有一行的,我的就显得有点怪异。google一下,发现是我路由跳转的方法用错了。wx.navigateTo({ url: ‘changePassword/changePassword’})使用这个可以直接出现返回的按钮:上面两种方法的区别是,第一个是关闭当前页面,然后跳转;后一种是保留当前页面,然后跳转。所以可以看到上面的url也有一些区别。第一个我写了完整的路径,而后一种我只写了相对路径(当然完整的也可以)。使用后面的好处,还可以利用API:wx.navigateBack实现返回,也省却了不少麻烦。界面风格在这次做原型的过程中,看了好多个别人的小程序,发现其中cell这个组件被用的很频繁。它的效果是这样的:它的大面积使用,代替了按钮的使用,所以我也借鉴了这一点,不得不说,效果确实比按钮要好。先看一下按钮实现的风格:再对比一下cell的风格:虽然按钮好像也不差,但是后面的给人感觉要更好一点。不仅如此,我还利用他解决了table的问题。因为小程序本身是没有table的,各大组件库中也没有看到,所以最后便用VantUI库中的两个组件panel和cell组合实现:<van-panel title=“2018-2019学年第一学期” use-footer-slot> <view slot=“footer”> <van-cell-group wx:for="{{ scores }}" wx:for-item=“score”> <van-cell title="{{ score.name }}"/> <van-cell title=“平时成绩” value="{{ score.usualScore }}" /> <van-cell title=“期中成绩” value="{{ score.middleScore }}" /> <van-cell title=“期末成绩” value="{{ score.finalScore }}" /> <van-cell title=“总评” value="{{ score.totalScore }}" /> </van-cell-group> </view></van-panel>实现效果如下:尽管没有达到我的预期,但是作为第一版也可以了。安利VantUI库中添加了很多图标,但如果还不能满足你的需求,可以去这里找找灵感:https://www.iconfont.cn/最后附上VantUI的地址:https://github.com/youzan/van…https://youzan.github.io/vant…

March 16, 2019 · 1 min · jiezi

技本功丨收藏!斜杠青年与你共探微信小程序云开发(下篇)

2019年2月26日,人们为了一个杯子疯了一天。星巴克猫爪杯,一场已经与猫无关了的“圣杯战争“。网上的倒卖价格,已炒至近千元!求而不得,舍而不能,得而不惜。这是人最大的悲哀。。。所以,请珍惜以下内容,知识才是人生最大的财富!前情回顾技本功丨收藏!斜杠青年与你共探微信小程序云开发(中篇)在上一篇文章中我们完成了首页的编写,目前基本的功能已经确定了,本节我们进行列表页的编写。列表页比较简单:将所有food列表展示出来,根据openid进行判断是否展示删除按钮即可。用户可以删除自己创建的记录。开发步骤 新建页面文件编写页面wxml编写js编写样式测试下面我们开始进行列表页的编写。编写页面wxmlmatch约定好js中的数据为foodlist,我们对foodlist字段进行循环输出即可:另外有一个删除按钮根据条件渲染,即当前item的openid跟我们正在使用小程序的用户openid是否一致,一致的话我们展示删除按钮供用户操作即可。之所以这样做是因为目前小程序不支持写他人数据,关于小程序用户权限值可以参考下图:可以看到管理端拥有绝对的读写权限,但是不支持用户操作另一用户的数据,数据库的操作可以在小程序开发者工具中的云开发工具中进行设置:编写js写完页面的模版之后我们就可以进行js的编写,js主要逻辑是:加载完成时调用云开发api获取所有food,提供删除方法调用云开发api进行删除。涉及到的云开发api有getCount获取数据数量、get获取数据、remove删除记录。通过官方文档我们可以看到remove函数的参数:我们需要记录的引用来调用删除函数,获取某条记录的引用我们可以通过doc函数获取:我们可以传入记录的id就能获取这条记录的引用,下面我们实现具体的删除方法delItem。删除方法我们只需要记录的id即可进行删除:编写好删除方法后我们编写获取所有数据记录的方法,这里仅仅用作演示获取所有记录,实际环境中还是建议使用分页特性分批次获取数据。使用get方法获取数据默认最多获取的是20条数据,因此要获取所有数据我们要结合count函数和skip函数进行递归来获取集合所有数据:1、首先我们写一个根据相应参数获取数据的方法然后编写一个调用findfood方法的用于递归的函数最后我们编写供js直接使用的getAllFood函数在这个函数里我们使用getCount函数获取集合数量,然后计算页数,再根据页数进行循环调用flag_pop函数即可。编写获取所有数据方法后我们就回到页面的js编写上,值得注意的是有一个云函数需要调用,我们有一个getUserInfo的方法来获取用户的openid,云函数是微信服务端进行的私有鉴权,因此我们部署上去后就可以使用,获取用户openid的函数体是:创建云函数在根目录下的cloudFunc|(环境名称)右键然后点击“新建nodeJS云函数”即可创建云函数模版,部署云函数在云函数的文件夹下右键然后选择“创建并部署”即可。使用云函数可以通过wx.cloud.callFunction()调用,该函数在官方文档中描述为:云函数的具体使用可以参考官方示例文档:https://dwz.cn/C95gkLVv接下来我们在页面的js文件里编写一个出初始方法用于获取所有数据,并在onShow的时候调用它。我们首先使用getUserInfo云函数获取当前用户的openid,然后再去获取所有的数据。详细代码如下:openid的作用主要是页面上用于判断是否显示删除按钮,数据库中的所有记录都会有一个_openid字段,值是这条记录的创建者的openid,当前小程序的使用用户的openid跟记录中的__openid字段相同才可以进行删除。下面我们在页面的js中定义好删除事件deletefood,删除事件有模版传过来一个记录值的id,我们使用deleteFood方法把参数也就是id传给until中的delItem即可。deletefood方法实现如下:全部完成后我们就能在列表页查看所有food了:给这个页面加上简单的css之后就变成这样的:可以看到我创建的“不加醋的糖醋里脊“可以被删除。结 语至此我们的“吃什么”小程序就开发完啦,首页长这样子:集成了云开发的添加,删除特性,使用了一个简单的云函数,虽然简单但是对于新手上手云开发还是很有帮助的,小程序源码开放在Github中,需要源码的可以自行下载:https://github.com/topiniu/ea…

March 15, 2019 · 1 min · jiezi

“知否精选”,小程序版本知乎日报诞生笔记

引子Question: 为什么要小程序版本的“知乎日报”?Answer: 国内大部分成熟的应用一般都会有对应的小程序版本的客户单,但是“知乎日报”没有。而我自己是非常喜欢小程序的,索性就自己做一个。至于知乎官方为什么不实现这个小程序客户端我也不得而知,我通过在微信公众号后台查询得知 “知乎日报”这个名字的小城是已经被注册了,如下图:根据现在的微信小程序名称命名的审核规则,像“知乎日报”这样名称的小程序主体一定是在知乎官方手中的。总之,不管怎么说,既然知乎官方没有做,那么我就来做一个吧。1. 关于知否精选本项目的初始版本已经正式发布在微信小程序,名称为 “知否精选”(官方名称知乎日报已经被注册,所以选择了这么一个名字)。除此之外,本项目的源代码也已经开源,源代码非常的简单,可以供想要学习小程序的同学们参考练手,地址 https://github.com/llyer/wech…。在开始实现之前,我在网站搜索了一下,找到了知乎日报的 API,仓库的地址在下面https://github.com/izzyleung/…API 的地址有了,接下来就是代码,同样在 github 上已经有了知乎日报的小程序版本代码,但是已经很久没有更新了,而且之前的作者也在仓库里面表示不再更新,仓库地址如下https://github.com/myronliu34…知否精选的主要资料来源如上。2. 目前实现的功能1. 加载文章列表2. 查看文章内容这两个功能是知乎日报最核心的功能,都已经实现。当然在发开过程中也遇到了一些问题,目前已经有了一些已知的BUG,也正在紧急修复中。例如知乎的API中返回的文章内容是富文本格式的,部分文章的格式解析可能没有做到完全的适配。如果对于本项目感兴趣的同学可以到 https://github.com/llyer/wech… 点个 Star,如果发现了有什么问题,欢迎提Issue。如果你只是单纯的喜欢这个小程序版本的知乎日报。请在微信小程序搜索 “知否精选”即可打开本程序,或者使用扫描下方二维码打开 “知否精选”,如果遇到问题,请在下方留言告诉我,我会尽快解答。

March 14, 2019 · 1 min · jiezi

分享使用tcb-router路由开发的云函数短信平台SDK

上篇文章我们分享了如何使用纯的云函数开发的榛子短信短信(http://smsow.zhenzikj.com)SDK,由于微信对于未付费云函数个数的限制,这种方法存在缺陷,经过改进,使用tcb-router作为路由,这样只需要整合到一个云函数中就行下载sdk和demo: http://smsow.zhenzikj.com/sdk…目前SDK中包含三个功能: send(发送短信)、balance(查询余额)、findSmsByMessageId(查询单条短信)SDK源码// 云函数入口文件const cloud = require(‘wx-server-sdk’)const TcbRouter = require(’tcb-router’)const rq = require(‘request’)const baseUrl = ‘https://smsdeveloper.zhenzikj.com’cloud.init()// 云函数入口函数exports.main = async (event, context) => { const app = new TcbRouter({ event }); app.router(‘send’, async (ctx) => { ctx.body = new Promise(resolve => { rq({ url: baseUrl + ‘/sms/send.html’, method: “POST”, json: true, form: { apiUrl: event.apiUrl, appId: event.appId, appSecret: event.appSecret, message: event.message, number: event.number, messageId: event.messageId, } }, function (error, response, body) { resolve({ body: body, error: error }) }); // setTimeout(() => { // resolve(‘male’); // }, 500); }); }); app.router(‘balance’, async (ctx) => { ctx.body = new Promise(resolve => { rq({ url: baseUrl + ‘/sms/balance.html’, method: “POST”, json: true, form: { apiUrl: event.apiUrl, appId: event.appId, appSecret: event.appSecret } }, function (error, response, body) { resolve({ body: body, error: error }) }); }); }); app.router(‘findSmsByMessageId’, async (ctx) => { ctx.body = new Promise(resolve => { rq({ url: baseUrl + ‘/sms/findSmsByMessageId.html’, method: “POST”, json: true, form: { apiUrl: event.apiUrl, appId: event.appId, appSecret: event.appSecret, messageId: event.messageId } }, function (error, response, body) { resolve({ body: body, error: error }) }); }); }); return app.serve();}如何使用SDK//index.jsconst app = getApp()Page({ data: { }, onLoad: function() { }, // 发送短信 send: function () { wx.cloud.callFunction({ name: ‘zhenzisms’, data: { $url: ‘send’, apiUrl: ‘https://sms_developer.zhenzikj.com’, appId: ‘你的appId’, appSecret: ‘你的appSecret’, message: ‘你的验证码为:3333’, number: ‘15811111111’, messageId: ’’ } }).then((res) => { console.log(res.result.body); }).catch((e) => { //console.log(e); }); }, // 查询余额 balance: function () { wx.cloud.callFunction({ name: ‘zhenzisms’, data: { $url: ‘balance’, apiUrl: ‘https://sms_developer.zhenzikj.com’, appId: ‘你的appId’, appSecret: ‘你的appSecret’ } }).then((res) => { console.log(res.result.body); }).catch((e) => { //console.log(e); }); }, // 查询单条信息 findSmsByMessageId: function () { wx.cloud.callFunction({ name: ‘zhenzisms’, data: { $url: ‘findSmsByMessageId’, apiUrl: ‘https://sms_developer.zhenzikj.com’, appId: ‘你的appId’, appSecret: ‘你的appSecret’, messageId: ‘aaaabbbbba’ } }).then((res) => { console.log(res.result.body); }).catch((e) => { //console.log(e); }); }}) ...

March 12, 2019 · 2 min · jiezi

如何使用微信小程序云函数发送短信验证码

其实微信小程序前端和云端都是可以调用短信平台接口发送短信的,使用云端云函数的好处是无需配置域名,也没有个数限制。本文使用的是榛子云短信平台(http://smsow.zhenzikj.com) ,SDK下载: http://smsow.zhenzikj.com/doc…安装下载后的SDK在cloudfunctions文件夹下会包含3个云函数文件夹,如下:由于目前IDE没有云函数导入功能,您需要手工创建同名的云函数,然后将云函数下的文件手工拷进去注:下载的SDK是一个完整的工程,包含SDK和使用示例,可实际运行演示 2.申请账号,获取AppId、AppSecret免费注册地址: http://sms_developer.zhenzikj…使用注册账号登录用户中心,在"我的应用"-> “详情"中可以查询AppId、AppSecretAppId、AppSecret是用于开发者使用账号和秘钥, 以下的所有api中都需要用到 3.发送短信wx.cloud.callFunction({ // 云函数名称 name: ‘zhenzisms_send’, // 传给云函数的参数 data: { apiUrl: ‘你的apiUrl’, appId: ‘你的appId’, appSecret: ‘你的appSecret’, message: ‘你的验证码为:1234’, number: ‘15811111111’, messageId: ’’ }, success(res) { console.log(res.result.body) }, fail: console.error }) }apiUrl为请求地址,个人开发者使用https://sms_developer.zhenzik…,企业开发者使用https://sms.zhenzikj.comsend方法用于单条发送短信参数message:发送的短信内容参数number:接收者手机号码参数messageId:该条信息的唯一标识,可用于查询返回结果是json格式的字符串, code: 发送状态,0为成功。非0为发送失败,可从data中查看错误信息 4.查看余额通过该接口可查看当前剩余的短信条数wx.cloud.callFunction({ // 云函数名称 name: ‘zhenzisms_balance’, // 传给云函数的参数 data: { apiUrl: ‘你的apiUrl’, appId: ‘你的appId’, appSecret: ‘你的appSecret’ }, success(res) { console.log(res.result.body) }, fail: console.error }) }返回结果是json格式的字符串, code: 查询状态,0为成功,data为剩余短信条数。非0为查询失败,可从data中查看错误信息错误代码表错误码 原因 解决方案100 参数格式错误 检查请求参数是否为空105 appId错误或应用不存在 请联系工作人员申请应用或检查appId是否输入错误106 应用被禁止 请联系工作人员查看原因107 ip错误 如果设置了ip白名单,系统会检查请求服务器的ip地址,已确定是否为安全的来源访问110 应用秘钥(AppSecret)错误 检查AppSecret是否输入错误,或是否已在用户中心进行了秘钥重置1000 系统位置错误 请联系工作人员或技术人员检查原因5.查询短信接口描述根据messageId查询已发送短信wx.cloud.callFunction({ // 云函数名称 name: ‘zhenzisms_findSmsByMessageId’, // 传给云函数的参数 data: { appId: ‘你的appId’, appSecret: ‘你的appSecret’, messageId: ‘messageId信息’ }, success(res) { console.log(res.result.body) }, fail: console.error })请求参数参数名称 必选 类型 描述messageId 是 string 信息id,对应发送短信接口的messageId字段返回结果返回结果是json格式的字符串, code: 查询状态,0为成功。非0为失败,可从data中查看错误信息 { “code”:0, “data”:{} } 返回结果是json格式的字符串, code: 查询状态,0为成功,data短信信息的json字符串 ...

March 12, 2019 · 1 min · jiezi

Natsuha - 用Taro写个天气微信小程序

去年年底 o2 开源了 Taro,一直手痒痒没去玩。考虑到 wx 的审核制度,所以决定写个工具类小程序。赶在 Taro 喜提第 2000 个 issues 之际,Natsuha 也终于上线了。源码全部释出(除涉及私钥部分,GitHub有说明),文章后面会贴出一些仍需优化的点,欢迎大家一起讨论。前言Natsuha Weather 已开源,欢迎大家一起折腾,给个 star 也是极好的~ GitHub Repo技术栈是 Taro + mobx + TypeScript,接口来自 Yahoo Weather API,当然设计也是参 (chao) 考 (xi) 的 Yahoo Weather. 接口有时会访问失败,尤其是晚上,我也没办法啊。????功能下拉刷新华氏温度、摄氏温度切换分时展示一天的天气预报展示未来10天的天气预报展示当前风向、风速展示日出日落、月相等信息展示一天内的降水预报城市天气检索显示检索记录踩坑小程序篇云开发解决 Bei An 问题由于众所周知的原因,wx 小程序无法调用未 bei an 的接口,哪怕是在开发环境。所以我们用云开发的云函数来 “反代” 接口,下面通过一个例子说一下技术要点。首先在根目录的 project.config.json 文件里添加 “cloudfunctionRoot”: “functions/",然后在根目录创建文件夹 functions. 并点击右键创建一个新的云函数,比如我们叫 getRegion。因为我们的目标是通过云函数请求一个未 bei an 的接口,所以为了更方便的处理异步请求,我们引入 request-promise 这个库。通过硬盘打开进入到这个云函数的文件夹,然后安装依赖:yarn add request request-promise接下来我们在 index.js 中写逻辑,直接上代码。云函数通过 event 对象来获取前端传过来的参数,然后通过 Promise 对象将结果返回。这个例子中我们需要拿到region,// 云函数入口文件const cloud = require(‘wx-server-sdk’)const rp = require(‘request-promise’)cloud.init()exports.main = async (event, context) => { const region = event.region; const res = await rp({ method: ‘get’, uri: https://www.yahoo.com/news/_tdnews/api/resource/WeatherSearch;text=${region}, json: true }).then((body) => { return { regionList: body } }).catch(err => { return err; }) return res;}接下来是前端发请求了,注意这里不能再用 Taro.request(), 而是云函数独有的 wx.cloud.callFunction(), 因为我现在的 Taro 版本尚未实现 Taro.cloud.callFunction(),所以直接用 wx 打头即可。首先封装一下 wx.cloud.callFunction(),其实感觉什么卵用????:export const httpClient = (url: string, data: any) => new Promise((resolve, reject): void => { wx.cloud.callFunction({ name: url, data, }).then(res => { resolve(res.result); }).catch(e => { reject(e) });});然后我们在 store 里面写逻辑,这样基本上就解决了数据请求的坑。 public getRegion = (text: string) => { httpClient(‘getRegion’, { region: encodeURI(text), }) .then((res: any) => { runInAction(() => { if (res.regionList) { this.regionList = res.regionList; } }); }) .catch(() => { setToast(toastTxt.cityFail); }); };????题外话:因为当前版本尚未实现 Taro.cloud.callFunction(),所以 lint 会报错,虽然不影响使用,大家有什么好的方法,可以说一下。地理信息授权问题在这个项目里,我们需要通过小程序拿到的经纬度来反查城市信息,而小程序获取经纬度需要用户授权。这里有个坑,当用户拒绝授权后,小程序默认询问授权的 dialog 在一段时间内不会重复弹出,所以我们必须手动将用户引导到授权页面。以前小程序有个接口叫做 wx.openSetting(),但 tx 把它废掉了,现在只能让用户点击一个特定的按钮。为此我做了一个 modal,这里贴出关键代码。<Button openType=‘openSetting’ onOpenSetting={() => this.onOpenSetting()}> OK</Button>首先我们必须给按钮声明 openType=‘openSetting’,这样当用户点击了之后就会跳转到设置页面。其次,我们需要在用户离开授权页面时,也就是点击了左上角那个返回按钮时,再次去检查一下用户的授权情况。所以我们要添加onOpenSetting={() => this.onOpenSetting(),不得不吐槽这个事件命名,明明应该叫做 onLeaveSetting才合理。在 onOpenSetting() 方法中我们再次执行判断用户是否授权的方法,未授权的话接着弹 modal,否则放行请求相应的数据接口。文字有些累,直接看图。无法用传统方式清空文本框文字当用户关闭搜索 dialog 时,文本框的文字应当被清空,所以一开始写成下面的这样,即点击关闭按钮时将 inputValue = ’’ ,然而发现不行。<Input type=‘text’ value={inputValue} placeholder=‘Enter City or ZIP code’ onInput={e => handleInputTextChange(e)}/><Button onClick={() => hideSearchDialog()}>Close</Button>查了一下官方文档,必须将 Input 和 Button 包裹在一个 Form 下,且要给关闭按钮加上 formType=‘reset’,最后给 Form 添加 onReset 事件指向关闭 dialog 的方法。<Form onReset={() => hideSearchDialog()}> <Input className={styles.input} type=‘text’ placeholder=‘Enter City or ZIP code’ onInput={e => handleInputTextChange(e)} /> <Button formType=‘reset’>Close</Button></Form>;Taro篇大多是编译问题和它 webpack 配的问题,相应的我都提了 issue,有兴趣的话可以跟进。Taro 编译会忽略模版两个之间的空格举个例子,<Text>day - night</Text>,可以正常编译,页面可以正常看到 day - night,但是假如是变量,就会被编译成 day- night,注意,空格被吃掉了。const day = ‘day’const night = ’night’<Text>{day} - {night}</Text>我提了个 issue #2261,然并没人鸟我,有兴趣可以跟进一下。ts 不能识别wx因为用到了云开发,而 Taro 现阶段还没有Taro.cloud(…),所以在使用原生的wx.cloud(…)时,ts 肯定会报错。css module 等静态文件 找不到路径一开始用的import来引入静态文件,但报“找不到路径”,可以看下图(但不影响使用)。提了个 issue #2213,按照大佬的回复修改也没解决问题,实在受不了一片红,索性改成了commonJS.Problem下面是项目中存在的一些问题,有兴趣的话欢迎大家一起讨论。图片加载不友好接口图片的 url 来自aws,因为众所周知的原因,图片经常会挂掉,所以有必要在图片挂掉的时候触发onError事件,然后给用户一个提示。因为小程序不支持new Image(),所以只能用官方提供的Image组件,幸好这个组件支持onLoad和onError事件。加载失败的问题解决了,但因为aws的速度太慢,所以正常加载时也很不友好(可以自行体会)做了一些尝试,比如先加载缩略图,再展示完整图片,但接口提供的最小尺寸的图片也已经达到了70 多 k,并且该死的 Yahoo 恰好将图片 url 控制大小的那段用了加密,所以这个方式 pass 掉了。搜索输入框加个节流现在的做法是在 store 的 构造器加个节流,但不知道这样合不合理。 construtor() { this.getRegion = _.debounce(this.getRegion, 150); }TODO国际化性能优化图片加载优化Jest 搞起来(初始化已搭好)Travis CI 搞起来(初始化已搭好)将搜索模块放到一个新页面(强行加个路由????)最后老子再也不写小程序了! ...

March 12, 2019 · 2 min · jiezi

想打通 Web 与小程序的账号系统?来试试知晓云吧

3 月 1 日,知晓云对支付宝小程序的支持已正式上线,参与公测的用户也获得了丰厚的奖励。多平台账号系统打通的体验让一切变得简单;与此同时,希望 Web 应用接入的呼声也越来越强烈。于是,这一次的 Web 端公测活动,我们希望有更多的开发者能拿到这笔「启动资金」,使用知晓云助力你的开发之路。「知晓云」cloud.minapp.com,诞生于 2017 年 8 月 8 日,是以小程序开发为起点的后端云服务,它免去了应用开发中服务器搭建、域名备案、数据接口开发、线上运维等繁琐流程,让开发者更快、更低成本地做出优质的小程序。我们在逐步兑现我们的承诺 —— 全平台 Serverless ,让未来触手可及。「未来」总来的比想象快,Web 端的公测活动正式开启:即日起至 3 月 15 日,报名并通过审核的公测的用户,在正式接入后都可获得获账户礼金 100 元。报名点这里 ???? 知晓云公测活动 ????知晓云能带给你什么?如果想做一个应用,基本上一定要有用户系统、数据存储、网络接口、管理后台等配套,这都是后端的工作。而通过我们多年实践,在大多数应用场景里,这类配套的定制(需要开发)的部分,均发生在应用层,也即,这类业务模块完全可以做成通用的服务。将后端的用户系统、数据存储、网络接口、服务运维甚至支付网关等通用业务做成服务提供给用户,通过管理后台控制,就可以做到无需后端工程师参与开发而完成项目。因此我们在微信小程序上线后的半年里,倾入全部力量,打造了知晓云——一个为小程序设计的后端云服务。17 年以来,我们花了一年半反复打磨产品,与广大的小程序开发者共同成长。在刚过去的 2018 年 ,我们累计服务了 40000+ 开发者,实现了服务规模同比 1000% 的增长。各大厂纷纷入局重注小程序,标志着移动互联网正式进入新时代——一个强调快速上线、专注服务本身的时代。在这个时代里,各大平台的应用都需要无服务器(Serverless)的开发方式。通过接入 SDK 就可以使用后端服务,开发者可以快速构建一个完整产品,上线推广。我们希望将从服务数万开发者的过程中总结出的,各个应用场景的经验推广到各大平台去,帮助更多的开发者。我们相信,将业务与后端云服务结合,将使开发过程变得更加简单、迅速和高效,让更多富有创造力的开发者创造更多的价值。好用、省心的后端云服务有了一个好的想法,再花上几分钟接入知晓云的 SDK,你就不再需要去管什么 PHP、数据库等后端逻辑,也无需管理服务器或维护后端服务,更不用担心自己服务器的负载和运维……与此同时,你还将获得:成本降低 100%数百元每月的服务器费用,9 元/月的套餐即可替代,大幅节约成本。运维时间节省 100%24 小时待命的线上运维、峰值动态扩容,服务器运维再无后顾之忧。效率提升 200%2 秒即可授权登录,多平台账号系统支持,开发效率将提升数倍!一句话,有了「知晓云」,核心业务逻辑以外的事情都不需要你操心。开发、推广都无后顾之忧我们提供的不仅仅是后端云服务。在历经多年的「移动生态」构建过程后,我们深知产品研发上线只是第一步,一切只算刚刚开始。我们还在推广环节铺了一条「高速车道」,使你的产品更快触达更多的用户:Appso:知名应用推荐服务平台知晓程序公众号:小程序生态第一大号知晓程序商店:首家小程序「应用商店」知晓市场:应用定制对接平台我们将技术服务与推广服务相结合,为用户提供免费的冷启动资源及完善的应用推广服务。知晓云,一个起点还记得 2017 年 6 月,知晓云内测用户线下见面会上,我们打出了「知晓云,一个起点」的 Slogan 。我们的愿景:一个刚开始探索移动互联网的少年,可以从知晓程序了解移动开发的最新趋势,通过知晓课堂学习开发姿势,利用知晓云一天搞定开发并获得我们提供的冷启动资源,逐步做大走上人生巅峰。2019 年知晓云正式开启了全平台 Serverless 计划,希望能服务更多的开发者。我们会为此不懈努力,希望愿望能有实现的那天。知晓云是国内首家专注于小程序开发的后端云服务。使用知晓云,小程序开发快人一步。

March 11, 2019 · 1 min · jiezi

百度的搜索流量终于全面开放!新入口就在王牌产品主场景

用户常说小程序难找。这不是因为各大 APP 的搜索做得不好,而是因为用户还没有养成需要什么就去搜索的使用习惯。当然,搜索应用可能是个例外。在用户已有固定搜索习惯的情况下,如果小程序能在用户习惯使用的搜索应用中有一个入口,那是不是就不用担心第一批用户来源了。近日,知晓程序发现,用户在百度搜索引擎搜索时,部分搜索结果的首页页面也新增了不少智能小程序入口。百度智能小程序终于入驻了百度主场景,得到了更多的曝光度。百度智能小程序的主场景入口在百度 APP 内进行搜索,你可能会在百度搜索结果页面看到智能小程序的身影。在搜索电影时,用户首先能看到搜索智能聚合的电影排行榜。往下滑动搜索结果,用户就能看到电影热门片单及购票服务,排在第四位的搜索结果就是百度智能小程序。用户在搜索结果页面能看到小程序的服务类目和基本信息,对小程序有一个基本了解。点击打开即可「秒开」智能小程序,享受极速体验。这个过程非常流畅,从「看到」到「得到」,仅需一两秒。知晓程序也用自己常用的智能小程序进行了测试。在搜索唯品会、爱奇艺、爱说唱、虎牙直播时,这几个智能小程序在搜索结果页面中所处的位置都排在前五。搜索小程序名或关键字可以看到智能小程序入口,那搜索某一具体的问题还能在搜索页面看到其入口吗?答案是能。在搜索具体问题时,百度搜索结果也会推荐智能小程序。以「装修什么公司好」为例,在百度搜索该问题,结果页面就会显示齐家的智能小程序。智能小程序的入口与其它搜索结果保持了较大一致,只是多了智能小程序的提示字样。这让用户在使用智能小程序时,没有操作门槛就能体验智能小程序的流畅。从这个角度来说,以多种形式出现在百度搜索结果页前列的智能小程序有了真正属于自己的主场景入口。降低获客成本,开发者的福音在去年 5 月 22 日百度联盟峰会上,百度 APP 总经理平晓黎曾表示,百度将通过开放百度系全域流量和 AI 赋能等方式扶持开发者,共建智能小程序生态。开放百度系全域流量?随着智能小程序越来越多的出现在百度搜索结果页面中,我们似乎可以说,百度全域的全域流量已经在向开发者展开怀抱了。就在两个月前的 2019 百家号创作者盛典上,百家号曾向公众公布了部分数据,其中最引人注目的就是百度的信息流日均推荐量已超过 150 亿。作为百度搜索引擎的主场景,百度搜索结果页面在移动互联网时代依然有着巨大的价值。日活过亿,信息流推荐量 150 亿的百度 APP 对于所有开发者来说都是不能忽视的流量池。现在,百度将它开放了,百度将自己的主场景与开发者共享,所有的小程序开发者都可以用它获取自己的第一批用户。百度智能小程序的工作人员向知晓程序透露,在百度系应用中,同质内容情况下小程序权重大于 H5,分发流量会有正向趋势。 而在百度 APP 端内,百度也会将智能小程序和同内容的 H5 去重,优先展现和调起智能小程序。百度搜索结果优先推荐智能小程序,这是行动上对开发者最好的扶持。对开发者来说,这也标志着百度搜索的全域流量至此全部开放。在流量越来越贵的前提下,这意味着更低的获客成本和更简单的初期发展。百度搜索结果优先推荐智能小程序,这也能帮助用户免去跳转的麻烦,顺畅「秒开」。这种流畅的体验也能够让用户体验更深层次的服务。百度智能小程序就曾在公开课上举例,小红书智能小程序和其 H5 相比,人均时长提升了 50%;携程智能小程序和 H5 相比,访问深度也提升了 150%。从「知道」到「得到」的一站式满足,没有「割裂感」的百度智能小程序带给了用户更好的体验。▲ 百度 APP 总经理平晓黎借助百度搜索结果页面的巨大流量池,小程序可以稳定的获取用户,用户也能得到更好的体验。搜索:百度智能小程序生态的最佳路径百度视频的 CEO 胡浩曾说过,尽管 BAT 都在发力小程序,但微信小程序是一个头部效应非常明显的生态,且强依赖社交分发;支付宝的小程序主要面向交易类应用;百度智能小程序则可以帮助很多开发者找到更适合自己的长尾流量。正如胡浩所说,微信有社交,支付宝有交易,百度也有自己最大的倚仗——搜索。深耕搜索领域多年,百度在流量分发和搜索上都有自己的独到经验。百度在发布智能小程序之时,就公布了智能小程序的四大关键词:体验、流量、智能、开放。在体验上,百度智能小程序「可以最大限度地接近 Native APP」,同时可以弥补原生应用的高开发发布成本,难被检索等问题。在流量上,日推荐量 150 亿次的信息流也足以满足开发者,为开发者输送源源不断的弹药。百度搜索结果页面就是智能小程序的点火助推器,可以帮助智能小程序的开发者获得更好的成长空间。在智能方面,百度智能识图、百度智能语音识别等百度技术接口也都已开放或即将开放。开发者可以更好地调用它们,从而用更智能的方式、减少用户的操作成本。在百度智能小程序中,「智能」是核心,开发者可以通过小程序使得 AI 能力普惠,「让开发者重回业务理解与创意赛道」。在开放方面,百度已经将搜索结果页面的主场景开放给了开发者,给了开发者更多的流量入口。百度 APP 总经理平晓黎在之前的演讲时也曾表示,百度智能小程序已有 40+ 个入口及 1.5+ 亿的日活。出现在百度搜索结果页面的智能小程序,在体验、流量、智能、开放四个方面都做到了发布之初对开发者和用户的承诺。事实上,百度搜索加智能小程序的应用「秒开」体验对于用户和开发者来说都非常有吸引力。百度做搜索起家,用户也习惯在百度搜索引擎上进行检索。对于用户而言,在百度上搜索并打开智能小程序是很自然的,已有的用户习惯也能帮助用户迅速适应搜索结果+智能小程序的搭配。开放主场景,吸引开发者,流畅体验,吸引用户。用搜索赋能智能小程序,这可能会是百度智能小程序生态发展的关键节点。在重视搜索入口这方面,百度和张小龙的想法都极为相似。张小龙曾说过「其实搜索一直应该是小程序的一个主要流量来源。并且小程序和 APP 有一个很大不同,APP 是一个个的信息孤岛,互相之间没法交换信息。但是小程序是可以被系统统一检索到,是可以直接搜索到小程序里面内容的。」无论是微信小程序在搜索的不断加码,还是百度搜索结果优先推荐智能小程序,这都说明了一点:搜索是小程序触达用户的重要方式之一,搜索也是小程序连接用户的最佳路径。全平台后端云服务 —— 知晓云礼金赠送即日起至 3 月 15 日,报名并通过 Web 端公测审核的用户,在正式接入后都可获得获账户礼金 100 元。????「知晓云」cloud.minapp.com,诞生于 2017 年 8 月 8日,是以小程序开发为起点的后端云服务,它免去了应用开发中服务器搭建、域名备案、数据接口开发、线上运维等繁琐流程,让开发者更快、更低成本地做出优质的小程序。报名点这里 ???? 知晓云公测活动 ???? 公测活动结束后还将挑选出 5 名积极反馈的用户获得知晓云限量纪念 T 恤本文首发于「知晓程序」公众号:https://mp.weixin.qq.com/s/Bw…知晓云是国内首家专注于小程序开发的后端云服务。使用知晓云,小程序开发快人一步。 ...

March 11, 2019 · 1 min · jiezi

微信小程序框架wepy踩坑记录(与vue对比)

wepy框架借鉴了vue的语法风格和功能特性,但是在使用过程中还是发现与vue有很大的不同。现在总结一下自己开发中遇到的问题,共大家参考一下。如果第一次用wepy开发,强烈建议仔细阅读一下这篇文章,一定对你有帮助,会帮你节约很多宝贵的时间。开发过程中也建议大家时不时的回来阅读一次,巩固加强记忆。wepy中的组件组件里面的坑还不是一般的多!首先来说说组件间的数据共享。在vue中你也能做到这一点,只要把 data 写成一个对象就可以了,当然你不想让所有的子组件都共享同一份数据,vue中的解决方案是给 data 写成一个函数就好了,return出来所有的数据,这样组件间的数据就不会共享了。但是wepy中不能。文档中介绍:WePY中的组件都是静态组件,是以组件ID作为唯一标识的,每一个ID都对应一个组件实例,当页面引入两个相同ID的组件时,这两个组件共用同一个实例与数据,当其中一个组件数据变化时,另外一个也会一起变化。所以如果同一个页面引用多个组件,你只能给每个组件定义不同的ID,类似这样import Child from ‘../components/child’; export default class Index extends wepy.page { components = { //为两个相同组件的不同实例分配不同的组件ID,从而避免数据同步变化的问题 child: Child, anotherchild: Child }; }看起来是不是很蠢。但是没办法,你只能这么用。如果页面中只引用两三个同类型组件还好,但是如果我是一个循环,我也不知道我要引用多少组件,该怎么办?接下来再说说组件的循环。wepy官方文档中说明:当需要循环渲染WePY组件时(类似于通过wx:for循环渲染原生的wxml标签),必须使用WePY定义的辅助标签<repeat>。但是不支持在 repeat 的组件中去使用 props, computed, watch 等等特性。什么?props 都不让用??那父组件如何给子组件传参??后来实践发现,如果 props 中的数据在 template 中是能取到的,但是在 method 或者event 中就取不到了,你说神不神奇!所以最后的解决办法是用的 wepy-redux,类似vuex,放在 store 中实现的。视图的渲染之异步数据异步数据的获取后需要手动调用 this.$apply() 方法才能重新渲染视图,这一点也一定要记得。刚开始做的时候是在页面 data 中写的假数据,渲染的好好的。但是数据换成从接口读取后,死活视图出不来。琢磨了半天才想起来需要手动调用 this.$apply()。而 vue 是不需要这么做的。方法定义wepy中页面中的事件需要些在 methods 中,组件之间的处理函数需要写在 events 中,而自己写的自定义方法需要写在与 methods 同级中。不像vue,可以写在 methods 里。在 events 中写的函数,不需要在调用子组件的时候写在子组件中,子组件 $emit 会自动去 events 中寻找同名方法执行。这点也与vue不同。事件传参wepy优化了原生小程序在事件中的传参形式。比如页面中有一个方法,叫 getIndex,目的是取一个循环的 index 属性,在原生中需要额外定义一个 data-index 属性,然后在 getIndex 中通过event.currentTarget.dataset.index 来获取。而wepy中可以直接在事件里传递,但需要加上{{}},写成 getIndex({{index}})这样,这点也与vue不同。数据绑定这个是小程序原生方法中的不好点,wepy不能帮忙背这个锅。数据绑定也是使用 {{}},但是{{}} 里面只能进行简单的运算,具体支持哪些运算可以看官方文档。需求是一个列表,选中的变个样式,正常的思路就是选中的时候触发一个方法,将 index 赋值给 currentActive,在 {{}} 中判断如果 currentActive == index 就应用 active 样式,命名很简单的一个需求。但是写好了就是不好使,找了半天也没发现哪错了,最后看文档,原来是根本就不支持这种写法!!只支持简单的运算,这种不属于简单的范围!!最后的解决办法是弄了一个数组 arr,选中将对应位置置为 true,在 {{}} 判断 arr[index] 是否为 true 解决了这个问题。总结一句话:{{}} 一点也不强大。动态绑定classwepy中需要遵循小程序原生的绑定方式,与vue中也不同。在vue中,动态的和非动态的需要分别写,类似这样:<div class=“class-a” :class="{true ? ‘class-b’: ‘class-c’}"></div>。但是在wepy中,动态和非动态的可以写在一起,类似这样:<view class=“class-a {{true ? ‘class-b’ : ‘class-c’}}">mixin混合wepy中的 mixin 分为两种。对于组件data数据,compontents 组件,events 事件及其他自定义方法采用默认式混合,即如果组件中未定义这些东西,那么 mixin 中的将生效,如果组件中已经定义了,将以组件中定义的为主,mixin 中定义的不会生效。但对于 methods 事件及小程序页面事件,将采用兼容式混合,只要定义了就都会生效。但是先响应组件中定义的,再响应 mixin 中定义的。而vue组件中 methods 里的事件如果与 mixin 中的重名,会采用组件中的事件。而生命周期的钩子函数则是先响应 mixin 中的,在响应组件中的。注:以上问题均是采用wepy1.7.2的版本,祝大家开发愉快,少踩些坑。最后附上官方文档链接,供大家参考:小程序官方文档wepy官方文档 ...

March 11, 2019 · 1 min · jiezi

【实战教程】使用云函数将数据表导出为 Excel 文件

在日常的工作中,常常需要根据运营需求对数据进行各种格式的处理和导出。导出后,不少人偏爱将数据放入 excel 在进行处理。一般来说,处理数据导出时需要对数据进行一些运算整理。在以前,处理的方式是在一台独立的服务器上跑脚本。而现在有了知晓云,不再需要维护服务器,直接写代码就能把相关事都都丢给云函数。 本文将介绍通过知晓云云函数来实现将数据表导出为 excel 文件的功能,并使用 webpack 和 mincloud 将代码打包上传到知晓云。技术栈:打包工具:webpack@4.22.0部署工具:mincloud@1.0.4Excel 处理:node-xlsx@0.14.1其他:知晓云 SDK一、项目搭建项目文件结构: export-excel-file ├── index.js ├── package.json ├── src │ └── index.js ├── webpack.config.js └── yarn.lock项目搭建与云函数代码打包示例文档基本一致。项目搭建好后,还需要安装以下依赖(两种安装方式选其一即可):// 使用 yarn 安装yarn add node-xlsx mincloud// 使用 npm 安装npm install –save node-xlsx minclou修改 deploy 脚本,如下:// package.json…“scripts”: { “build”: “webpack –mode production”, “predeploy”: “npm run build”, “deploy”: “mincloud deploy export-excel-file ../”},…最终我们会使用以下两个命令来部署和测试:npm run deploy // 部署到知晓云mincloud invoke export-excel-file // 测试已经部署到知晓云上的云函数二、将数据表导出为 excel 文件我们需要准备两张表:order: 订单表 (新建字段:name, price)export_task:导出任务记录表 (新建字段:file_download_link) 知晓云的云函数调用有同步和异步两种方式,同步调用的最大超时时间为 5 s,异步调用的则为 300 s。假定 order 订单表有十万条数据,由于知晓云单次拉取数据的最大限制为 1000 条,所以需要分批获取数据,加上后续可能需要对数据进行处理,所花费的时间将会超过 5 s,因此对该云函数的调用将采用异步的方式。这时候就需要 export_task 导出任务记录表来对导出任务进行管理了。export_task 表对导出任务进行管理的流程如下:调用云函数时在 export_task 表中创建一条记录 A,此时记录 A 中的 file_download_link 字段值为空,同时拿到记录 A 的 id,记这个 id 为 jobId进行 order 表数据查询,excel 文件生成,文件上传等操作,拿到文件下载链接之后根据 jobId 来更新第一步创建的记录,保存文件下载链接到 file_download_link 字段中更新完后就能在 export_task 表中拿到文件下载链接通过上面的准备和分析,对导出 excel 文件操作分为以下 4 个步骤:order 订单表数据获取使用获取的数据在云函数环境下创建 excel 文件将创建出的 excel 文件上传到知晓云保存文件下载链接到 export_task 表中的 file_download_link 字段完整代码如下:const fs = require(‘fs’)const xlsx = require(’node-xlsx’)const EXPORT_DATA_CATEGORY_ID = ‘5c711e3119111409cdabe6f2’ // 文件上传分类 idconst TABLE_ID = { order: 66666, // 订单表 export_task: 66667, // 导出任务记录表}const TMP_FILE_NAME = ‘/tmp/result.xlsx’ // 本地临时文件路径,以 /tmp 开头,具体请查看:https://doc.minapp.com/support/technical-notes.html (云函数的临时文件存储)const ROW_NAME = [’name’, ‘price’] // Excel 文件列名配置const MAX_CONNECT_LIMIT = 5 // 最大同时请求数const LIMIT = 1000 // 单次最大拉取数据数let result = []/** * 更新导出记录中的 file_download_link 字段 * @param {} tableID * @param {} recordId * @param {} fileLink /function updateExportJobIdRecord(tableID, recordId, fileLink) { let Schame = new BaaS.TableObject(tableID) let schame = Schame.getWithoutData(recordId) schame.set(‘file_download_link’, fileLink) return schame.update()}/* * 创建数据导出任务 * 设置初始 file_download_link 为空 * 待导出任务执行完毕后将文件下载地址存储到 file_download_link 字段中 * @param {} tableID /function createExportJobIdRecord(tableID) { let Schame = new BaaS.TableObject(tableID) let schame = Schame.create() return schame.set({file_download_link: ‘’}).save().then(res => { return res.data.id })}/* * 获取总数据条数 * @tableId {} tableId /function getTotalCount(tableId) { const Order = new BaaS.TableObject(tableId) return Order.count() .then(num => { console.log(‘数据总条数:’, num) return num }) .catch(err => { console.log(‘获取数据总条数失败:’, err) throw new Error(err) })}/* * 分批拉取数据 * @param {} tableId * @param {} offset * @param {} limit /function getDataByGroup(tableId, offset = 0, limit = LIMIT) { let Order = new BaaS.TableObject(tableId) return Order.limit(limit).offset(offset).find() .then(res => { return res.data.objects }) .catch(err => { console.log(‘获取分组数据失败:’, err) throw new Error(err) })}/* * 创建 Excel 导出文件 * @param {*} sourceData 源数据 /function genExportFile(sourceData = []) { const resultArr = [] const rowArr = [] // 配置列名 rowArr.push(ROW_NAME) sourceData.forEach(v => { rowArr.push( ROW_NAME.map(k => v[k]) ) }) resultArr[0] = { data: rowArr, name: ‘sheet1’, // Excel 工作表名 } const option = {’!cols’: [{wch: 10}, {wch: 20}]} // 自定义列宽度 const buffer = xlsx.build(resultArr, option) return fs.writeFile(TMP_FILE_NAME, buffer, err => { if (err) { console.log(‘创建 Excel 导出文件失败’) throw new Error(err) } })}/* * 上传文件 */function uploadFile() { let MyFile = new BaaS.File() return MyFile.upload(TMP_FILE_NAME, {category_id: EXPORT_DATA_CATEGORY_ID}) .catch(err => { console.log(‘上传文件失败’) throw new Error(err) })}module.exports = async function(event, callback) { try { const date = new Date().getTime() const groupInfoArr = [] const groupInfoSplitArr = [] const [jobId, totalCount] = await Promise.all([createExportJobIdRecord(TABLE_ID.export_task), getTotalCount(TABLE_ID.order)]) const groupSize = Math.ceil(totalCount / LIMIT) || 1 for (let i = 0; i < groupSize; i++) { groupInfoArr.push({ offset: i * LIMIT, limit: LIMIT, }) } console.log(‘groupInfoArr:’, groupInfoArr) const length = Math.ceil(groupInfoArr.length / MAX_CONNECT_LIMIT) for (let i = 0; i < length; i++) { groupInfoSplitArr.push(groupInfoArr.splice(0, MAX_CONNECT_LIMIT)) } console.log(‘groupInfoSplitArr:’, groupInfoSplitArr) const date0 = new Date().getTime() console.log(‘处理分组情况耗时:’, date0 - date, ‘ms’) let num = 0 // 分批获取数据 const getSplitDataList = index => { return Promise.all( groupInfoSplitArr[index].map(v => { return getDataByGroup(TABLE_ID.order, v.offset, v.limit) }) ).then(res => { ++num result.push(…Array.prototype.concat(…res)) if (num < groupInfoSplitArr.length) { return getSplitDataList(num) } else { return result } }) } Promise.all([getSplitDataList(num)]).then(res => { const date1 = new Date().getTime() console.log(‘结果条数:’, result.length) console.log(‘分组拉取数据次数:’, num) console.log(‘拉取数据耗时:’, date1 - date0, ‘ms’) genExportFile(result) const date2 = new Date().getTime() console.log(‘处理数据耗时:’, date2 - date1, ‘ms’) uploadFile().then(res => { const fileLink = res.data.file_link const date3 = new Date().getTime() console.log(‘上传文件耗时:’, date3 - date2, ‘ms’) console.log(‘总耗时:’, date3 - date, ‘ms’) updateExportJobIdRecord(TABLE_ID.export_task, jobId, fileLink) .then(() => { const date4 = new Date().getTime() console.log(‘保存文件下载地址耗时:’, date4 - date3, ‘ms’) console.log(‘总耗时:’, date4 - date, ‘ms’) callback(null, { message: ‘保存文件下载地址成功’, fileLink, }) }) .catch(err => { callback(err) }) }).catch(err => { console.log(‘上传文件失败:’, err) throw new Error(err) }) }) } catch (err)三、部署并测试跟 npm 一样,部署前需要先登录,请参照文档配置。使用以下命令即可将云函数部署到知晓云:npm run deploy执行结果如下:使用以下的命令来测试:mincloud invoke export-excel-file执行结果如下:export_task 表记录:上传到知晓云的 excel 文件如下:文件内容:四、参考文档知晓云开发文档:https://doc.minapp.com/node-xlsx 文档:https://www.npmjs.com/package…五、源码仓库地址:https://github.com/ifanrx/exp…六、福利即日( 3 月 8 日)起,前 50 名报名并通过审核的 Web 端公测的用户,在正式接入后即获账户礼金 100 元。????报名点这里 ???? 知晓云公测活动 ???? 除了这个好消息,小云还要告诉大家,公测活动结束后还将挑选出 5 名积极反馈的用户获得知晓云限量纪念 T 恤呀 (。・・。)ノ♡本文首发于「知晓云」公众号:https://mp.weixin.qq.com/s/g6…知晓云是国内首家专注于小程序开发的后端云服务。使用知晓云,小程序开发快人一步。 ...

March 8, 2019 · 4 min · jiezi

微信小程序页面返回优化

页面栈微信小程序的路由历史,用一个栈来管理,这个栈最多累积10层(以前是最多5层,小程序的api说变就变!)。场景:一个学生信息列表,当需要修改信息时跳转到修改页面。每修改一个学生信息就需要跳转一次页面。同时又需要保留修改页面,而不是每次销毁它。这时候用wx.navigateTo,wx.redirectTo,显然是不满足的。同时用wx.redirectTo返回页面也会出现中间历史页面闪现一下的问题。解决办法:wx.navigateBack(Object object)关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层。自己封装返回路由:routeBack(‘pages/subpackage/index/main’)

March 5, 2019 · 1 min · jiezi

微信小程序刷新页面

有时候我们会遇到这样的问题,退出小程序以后重新进入小程序历史数据还在。需要我们手动清除历史数据,而页面属性那么多不能一个一个去初始化。最佳解决办法: onLoad: function (option) { Object.assign(this, this.$options.data()); // 重置页面数据 }其它解决办法:page.onshow();page.onLoad();

March 5, 2019 · 1 min · jiezi

微信小程序网络组件 weapp.request

地址:https://github.com/afishhhhh/weapp.requestweapp.request 是基于 wx.request 进行扩展的一个网络组件,相较于 wx.request 有更简单的调用方式。此组件目前主要提供两个功能,分别的 Promise 以及缓存控制,使用 Promise 之后能让原先 wx.request 的调用变得更加简洁,避免了回调函数,对于服务器返回的数据可以根据项目需要在本地进行缓存。欢迎各位 JS 大佬提出改进意见。FeaturesPromise API缓存控制Install推荐使用 npm 安装npm install weapp.request -SQuick Start引入 weapp.requestconst request = require(‘weapp.request’)发送一个 GET 请求request(‘https://api.github.com’).then(onFulfilled).catch(onRejected)因为所有的 request 调用都会返回一个 Promise,所以可以使用 then 对请求结果进行进一步处理,用 catch 来捕获内部抛出的错误。发送一个 GET 请求,并写入缓存request(‘https://api.github.com’, {}, { cache: true})发送一个 POST 请求request.post(‘https://api.github.com’, { user: ‘afishhhhh’})除了 GET 请求以外,所有其他的 method 都要以 request.method 的形式进行调用。根据微信官方文档的说明,以上 POST 方法且 Content-Type 默认为 application/json,会对数据进行 JSON 序列化。如果需要以 query string 的形式将数据发送给服务器,可以采取以下调用方法,不需要显示的将 Content-Type 写为 application/x-www-form-urlencoded:request.post(‘https://api.github.com’, { form: { user: ‘afishhhhh’ }})全局配置配置选项类型说明必填默认值baseUrlString/Undefined基础请求路径否 cacheMaxAgeNumber/Undefined缓存有效期,时间单位为秒否1800validStatusCodeFunction/Undefinedstatus code 合法区间,该函数接受一个参数,并返回一个 Boolean否code => code >= 200 && code < 300request.config({ baseUrl: ‘https://api.github.com’})APIsrequest(url, params, options)发起一个 GET 请求。params:请求参数,类型为 Object,非必填。options:配置项,类型为 Object,非必填,可以有以下属性值:属性类型必填默认值说明cacheBoolean/Undefined否undefinedundefined 表示从服务器获取最新数据,不写入缓存;true 表示优先从缓存中获取数据,如果缓存中不存在该数据或者缓存已失效,则从服务器获取数据,并写入缓存;false 表示优先从服务器获取数据,并将数据写入缓存header 同微信官方文档dataType 同微信官方文档responseType 同微信官方文档request.method(url, params, options)method 可以是 get,post 等等。request.config(options)options:配置项,类型为 Object。 ...

March 1, 2019 · 1 min · jiezi

微信小程序搜索功能!附:小程序前端+PHP后端

开发需求微信小程序已经是非常火了,而且学习也比较容易,但是对于初学者来说还是一件比较伤脑筋的事,接下来给大家分享一下小程序搜索的思路。流程1、表单(输入框、提交按钮、提交的name值)2、接收表单数据(js获取表单name=keyword的值)3、通过wx.request向服务器后端发起请求查询数据库4、返回JSON格式的数据给小程序,js解析渲染到小程序前端展示界面代码index.wxml<!– 标题 –><view class=“title”>小程序搜索</view><!– 搜索框view –><view class=“search_con”><!– 表单 –> <form bindsubmit=“formSubmit”> <!– 记得设置name值,这样JS才能接收name=keyword的值 –> <input type=“text” name=“keyword” class=“search_input” placeholder=‘你要找什么呢?’/> <button formType=“submit” class=“search_btn”>搜索</button> </form></view><!– 搜索结果展示 –><view wx:for="{{re}}" wx:key=“re” class=“search_result”><!– 当提交空白表单的时候 –> <view class=“empty”>{{item.empty}}</view> <!– 当有搜索结果的时候 –> <view class=“resname”>{{item.resname}}</view> <!– 当查询不到结果的时候 –> <view class=“noresult”>{{item.noresult}}</view></view>index.js其中里面的http://localhost/search.php?k…是服务器后端接口,用于接收小程序传过去的关键词的,下面会有这个后端PHP文件。const app = getApp()Page({ data: {}, //执行点击事件 formSubmit: function (e) { //声明当天执行的 var that = this; //获取表单所有name=keyword的值 var formData = e.detail.value.keyword; //显示搜索中的提示 wx.showLoading({ title: ‘搜索中’, icon: ’loading’ }) //向搜索后端服务器发起请求 wx.request({ //URL url: ‘http://localhost/search.php?keyword=’ + formData, //发送的数据 data: formData, //请求的数据时JSON格式 header: { ‘Content-Type’:‘application/json’ }, //请求成功 success: function (res) { //控制台打印(开发调试用) console.log(res.data) //把所有结果存进一个名为re的数组 that.setData({ re: res.data, }) //搜索成功后,隐藏搜索中的提示 wx.hideLoading(); } }) },})index.wxss/* 搜索样式 /.title{ text-align: center; font-size: 20px; font-weight: bold;}.search_con{ width: 80%; margin:20px auto;}.search_con .search_input{ border: 1px solid rgb(214, 211, 211); height: 45px; border-radius: 100px; font-size: 17px; padding-left: 15px;/此处要用padding-left才可以把光标往右移动15像素,不可以用text-indent/ color: #333;}.search_con .search_btn{ margin-top: 15px; width: 100%; height: 45px; background: #56b273; color: #fff; border-radius: 100px;}.search_result{ width: 80%; margin:10px auto;}.search_result .empty{ text-align: center; color: #f00; font-size: 15px;}.search_result .noresult{ text-align: center; color: #666; font-size: 15px;}.search_result .resname{ text-align: left; color: #333; font-size: 15px;}服务端search.php<?phpheader(‘Content-Type:application/json’);//获取表单数据$keyword1 = $_GET[“keyword”];//过滤表单空格$keyword2 = trim($keyword1);//当表单提交空白数据时if(empty($keyword2)){ //构建数组 $arr = array( “empty” => “表单不能为空” ); //把数组转换为json $data = json_encode($arr); echo “[$data]”;}else{//过滤表单特殊字符$replace = array(’!’,’@’,’#’,’$’,’%’,’^’,’&’,’’,’(’,’)’,’_’,’-’,’+’,’=’,’{’,’}’,’[’,’]’,’;’,’:’,’"’,’<’,’>’,’?’,’/’,’|’);$keyword3 = str_replace($replace, ‘’, $keyword2);// 连接数据库$con = mysql_connect(“数据库地址”,“数据库账号”,“数据库密码”);if (!$con){die(‘Could not connect: ’ . mysql_error());}mysql_select_db(“数据库名”, $con);mysql_query(“SET NAMES UTF8”);//查询数据库$result = mysql_query(“SELECT * FROM 表名 WHERE 需要查询的字段 like ‘%$keyword3%’ ORDER BY ID DESC”);$results = array();//查询数据库是否存在这条记录$exist = mysql_num_rows($result);if ($exist) { //遍历输出 while ($row = mysql_fetch_assoc($result)){ $results[] = $row; } //输出JSON echo json_encode($results); //当查询无结果的时候 }else{ //构建数组 $arr = array( “noresult” => “暂无结果” ); //把数组转换为json $data = json_encode($arr); echo “[$data]”;}//断开数据库连接mysql_close($con);}?>服务端也是非常简单的,大家自己把服务端写好一点,毕竟安全和效率是很重要的。演示 ...

February 27, 2019 · 2 min · jiezi

开发支付宝小程序无从下手?我们给你创造了一条捷径

作为国内首家专注于小程序领域的后端云服务,知晓云正式开启 3.0 计划——全平台 Serverless 服务。「知晓云」cloud.minapp.com,诞生于 2017 年 8 月 8 日,是国内第一个专注于小程序开发的后端云服务,它免去了小程序开发中服务器搭建、域名备案、数据接口开发、线上运维等繁琐流程,让开发者更快、更低成本地做出优质的小程序。即日起至 3 月 1 日公测活动期间,成功参与新版公测活动并接入支付宝小程序的用户,可获得个人版套餐 6 个月的免费使用资格。计划的第一站我们选择了支付宝小程序。在接下来的时间里,我们还将逐步完成对其他各大平台的支持:(百度、头条等)小程序、Web 端、iOS、Android ,一步步完成全平台后端云服务建设。知晓云能带给你什么?如果想做一个应用,基本上一定要有用户系统、数据存储、网络接口、管理后台等配套,这都是后端的工作。而通过我们多年实践,在大多数应用场景里,这类配套的定制(需要开发)的部分,均发生在应用层,也即,这类业务模块完全可以做成通用的服务。将后端的用户系统、数据存储、网络接口、服务运维甚至支付网关等通用业务做成服务提供给用户,通过管理后台控制,就可以做到无需后端工程师参与开发而完成项目。因此我们在微信小程序上线后的半年里,倾入全部力量,打造了知晓云——一个为小程序设计的后端云服务。17 年以来,我们花了一年半反复打磨产品,与广大的微信小程序开发者共同成长。在刚过去的 2018 年 ,我们累计服务了 40000+ 开发者,实现了服务规模同比 1000% 的增长。各大厂纷纷入局重注小程序,标志着移动互联网正式进入新时代——一个强调快速上线、专注服务本身的时代。支付宝以及其他平台的小程序,在这个时代里,更加需要无服务器(Serverless)的开发方式。通过接入 SDK 就可以使用后端服务,开发者可以快速构建一个完整产品,上线推广。我们希望将从服务数万开发者的过程中总结出的,各个应用场景的经验推广到各大平台去,帮助更多的开发者。我们相信,小程序和后端云服务结合,将使开发过程变得更加简单、迅速和高效,让更多富有创造力的开发者创造更多的价值。好用、省心的后端云服务有了一个好的想法,再花上几分钟在小程序中接入知晓云的 SDK,你就不再需要去管什么 PHP、数据库等后端逻辑,也无需管理服务器或维护后端服务,更不用担心自己服务器的负载和运维……与此同时,你还将获得:成本降低 100%数百元的服务器及数据库费用,一天 3 毛钱就能搞定,大大节约开发成本。运维时间节省 100%24 小时待命的线上运维、峰值动态扩容,服务器运维再无后顾之忧。效率提升 200%2 秒即可授权登录,一键部署新体验,你的开发效率将至少提升一倍!一句话,有了「知晓云」,核心业务逻辑以外的事情都不需要你操心。即便你已经拥有了自己的服务器,仍然可以来体验我们的产品——「知晓云」还开放了运营后台的 API 支持、第三方服务器的 OpenAPI 支持,方便开发者快速将小程序业务和其他业务联合起来。从「晓」开始,再无后顾之忧我们提供的不仅仅是后端云服务。在历经多年的「移动生态」构建过程后,我们深知一款产品的研发上线只是第一步。上线了,一切才算正式开始。因此,我们在小程序推广环节铺了一条「高速轨道」,使你的产品借此触达到更多的用户。我们拥有:知晓程序公众号:小程序生态第一大号知晓程序商店:首家小程序商店知晓市场:小程序定制对接平台作为小程序生态里唯一一家,技术服务与推广服务都做到了行业领先的团队。我们将为用户提供免费的冷启动资源及推广支持。真正实现:「从『晓』开始,让您再无后顾之忧」2019 年,知晓云迈出了新的一步,希望大家也能顺利的迈出自己的每一步。本文首发于「知晓程序」公众号:https://mp.weixin.qq.com/s/rJ…知晓云是国内首家专注于小程序开发的后端云服务。使用知晓云,小程序开发快人一步。

February 20, 2019 · 1 min · jiezi

canvas绘图按照contain或者cover方式适配,并居中显示

canvas绘图时drawImage,需要绘制的图片大小不同,比例各异,所以就需要像html+css布局那样,需要contain和cover来满足不同的需求。contain保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。图片按照contain模式放到固定盒子的矩形内,则需要对图片进行一定的缩放。原则是:如果图片宽高不等,使图片的长边能完全显示出来,则原图片高的一边缩放后等于固定盒子对应的一边,等比例求出另外一边,如果图片宽高相等,则根据固定盒子的宽高来决定缩放后图片的宽高,固定盒子的宽大于高,则缩放后的图片高等于固定盒子的高度,对应求出另外一边即可,反之亦然。 /** * @param {Number} sx 固定盒子的x坐标,sy 固定盒子的y左标 * @param {Number} box_w 固定盒子的宽, box_h 固定盒子的高 * @param {Number} source_w 原图片的宽, source_h 原图片的高 * @return {Object} {drawImage的参数,缩放后图片的x坐标,y坐标,宽和高},对应drawImage(imageResource, dx, dy, dWidth, dHeight) / function containImg(sx, sy , box_w, box_h, source_w, source_h){ var dx = sx, dy = sy, dWidth = box_w, dHeight = box_h; if(source_w > source_h || (source_w == source_h && box_w < box_h)){ dHeight = source_hdWidth/source_w; dy = sy + (box_h-dHeight)/2; }else if(source_w < source_h || (source_w == source_h && box_w > box_h)){ dWidth = source_wdHeight/source_h; dx = sx + (box_w-dWidth)/2; } return{ dx, dy, dWidth, dHeight } } var c=document.getElementById(“myCanvas”); var ctx=c.getContext(“2d”); ctx.fillStyle = ‘#e1f0ff’; //固定盒子的位置和大小–图片需要放在这个盒子内 ctx.fillRect(30, 30, 150, 200); var img = new Image(); img.onload = function () { console.log(img.width,img.height); var imgRect = containImg(30,30,150,200,img.width,img.height); console.log(‘imgRect’,imgRect); ctx.drawImage(img, imgRect.dx, imgRect.dy, imgRect.dWidth, imgRect.dHeight); } img.src = “./timg2.jpg”; //注:img预加载模式下,onload应该放在为src赋值的上面,以避免已有缓存的情况下无法触发onload事件从而导致onload中的事件不执行的情况发生cover保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。原理:按照固定盒子的比例截取图片的部分 /** * @param {Number} box_w 固定盒子的宽, box_h 固定盒子的高 * @param {Number} source_w 原图片的宽, source_h 原图片的高 * @return {Object} {截取的图片信息},对应drawImage(imageResource, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)参数 / function coverImg(box_w, box_h, source_w, source_h){ var sx = 0, sy = 0, sWidth = source_w, sHeight = source_h; if(source_w > source_h || (source_w == source_h && box_w < box_h)){ sWidth = box_wsHeight/box_h; sx = (source_w-sWidth)/2; }else if(source_w < source_h || (source_w == source_h && box_w > box_h)){ sHeight = box_hsWidth/box_w; sy = (source_h-sHeight)/2; } return{ sx, sy, sWidth, sHeight } } var c=document.getElementById(“myCanvas”); var ctx=c.getContext(“2d”); ctx.fillStyle = ‘#e1f0ff’; //固定盒子的位置和大小–图片需要放在这个盒子内 ctx.fillRect(30, 30, 150, 200); var img = new Image(); img.onload = function () { console.log(img.width,img.height); var imgRect = coverImg(150,200,img.width,img.height); console.log(‘imgRect’,imgRect); ctx.drawImage(img, imgRect.sx, imgRect.sy, imgRect.sWidth, imgRect.sHeight, 30, 30, 150, 200); } img.src = “./timg2.jpg”; //注:img预加载模式下,onload应该放在为src赋值的上面,以避免已有缓存的情况下无法触发onload事件从而导致onload中的事件不执行的情况发生 ...

February 15, 2019 · 2 min · jiezi

在微信小程序中保存网络图片

微信代码片段点这里, 该功能需要添加appid才能进行正常的测试。在小程序的文档中我们得知,wx.saveImageToPhotosAlbum 是用来保存图片到相册的。但是仔细一看会发现这个接口的filePath参数只接受临时文件路径或永久文件路径,不支持网络图片路径,意味着我们不能直接调用这个接口。。因此先需要把该文件下载至本地,使用 wx.downloadFile 。但值得注意的是小程序只可以跟指定的域名与进行网络通信,也就是说下载图片之前,我们需要先去微信公众者平台的开发设置里设置uploadFile合法域名。示例代码如下:<!– index.wxml –><image class=“qr-code” src="{{url}}" mode=“aspectFill” /><button class=“text” bindtap=“saveImage”>保存图片</button>// index.jsconst app = getApp()Page({ data: { url: ‘https://avatars3.githubusercontent.com/u/23024075?s=460&v=4' }, // 保存图片 saveImage() { this.wxToPromise(‘downloadFile’, { url: this.data.url }) .then(res => this.wxToPromise(‘saveImageToPhotosAlbum’, { filePath: res.tempFilePath })) .then(res => { // do something wx.showToast({ title: ‘保存成功~’,icon: ’none’ }); }) .catch(err) => { console.log(err); // 如果是用户自己取消的话保存图片的话 // if (err.errMsg.indexOf(‘cancel’)) return; }) }, /** * 将 callback 转为易读的 promise * @returns [promise] */ wxToPromise(method, opt) { return new Promise((resolve, reject) => { wx[method]({ …opt, success(res) { opt.success && opt.success(); resolve(res) }, fail(err) { opt.fail && opt.fail(); reject(err) } }) }); },})然后理论上就可以保存图片了… 用户第一次在我们的小程序使用保存图片这个功能是会弹出一个授权弹框,如果用户手滑点了拒绝授权后再点一次保存图片,然后就会发现什么反应都没有了。。。出现这样的原因是因为这个授权弹框只会出现一次,所以我们得想办法再让用户重新授权一次。这时就想到使用 wx.authorize .但是经过测试后发现,使用 wx.authorize 后,会报 authorize:fail auth deny 的错误。然后经过查阅资料得知:如果用户未接受或拒绝过此权限,会弹窗询问用户,用户点击同意后方可调用接口;如果用户已授权,可以直接调用接口;如果用户已拒绝授权,则不会出现弹窗,而是直接进入接口 fail 回调。请开发者兼容用户拒绝授权的场景。emmm… 那这样效果当然不符合我们预期,只能在换一种方式。这时就想到了使用<button open-type=“openSetting”/>,在交互上做一个提示弹框,引导用户重新授权:<image class=“qr-code” src="{{url}}" mode=“aspectFill” /><button class=“text” bindtap=“saveImage”>保存图片</button><!– 简陋版提示 –><view wx:if="{{showDialog}}" class=“dialog-wrap”> <view class=“dialog”> 这是一段提示用户授权的提示语 <view class=“dialog-footer”> <button class=“btn” open-type=“openSetting” bindtap=“confirm” > 授权 </button> <button class=“btn” bindtap=“cancel”>取消</button> </view> </view></view>const app = getApp()Page({ data: { url: ‘https://avatars3.githubusercontent.com/u/23024075?s=460&v=4', showDialog: false, }, saveImage() { this.wxToPromise(‘downloadFile’, { url: this.data.url }) .then(res => this.wxToPromise(‘saveImageToPhotosAlbum’, { filePath: res.tempFilePath })) .then(res => { console.log(res); // this.hide(); wx.showToast({ title: ‘保存成功’, icon: ’none’, }); }) .catch(({ errMsg }) => { console.log(errMsg) // if (~errMsg.indexOf(‘cancel’)) return; if (!errMsg.indexOf(‘auth’)) { wx.showToast({ title: ‘图片保存失败,稍后再试’, icon: ’none’ }); } else { // 调用授权提示弹框 this.setData({ showDialog: true }) }; }) }, // callback to promise wxToPromise(method, opt) { return new Promise((resolve, reject) => { wx[method]({ …opt, success(res) { opt.success && opt.success(); resolve(res) }, fail(err) { opt.fail && opt.fail(); reject(err) } }) }); }, confirm() { this.setData({ showDialog:false }) }, cancel() { this.setData({ showDialog: false }) }})最后这样就完成啦https://anran758.github.io/bl… ...

February 11, 2019 · 2 min · jiezi

公测招募!知晓云已支持支付宝小程序

2017 年 8 月 8 日,知晓云正式上线。在这一年多的时间里,随着微信小程序的蓬勃发展,知晓云取得了一些成绩——累计服务 40000+ 开发者,18 年更是实现了服务规模同比增长 1000%。对于用户来说,省去服务器搭建、域名备案、数据接口开发、线上运维等繁琐流程与不到 3 毛钱一天的价格,是选择我们重要因素。我们在做哪些努力这一年多来,我们聚焦在 Serverless(无服务器架构)= BaaS(后端即服务) + FaaS(函数即服务)两大基础能力的同时,也针对微信小程序平台的各项能力做了深度支持,提供了大量开箱即用的功能,如——最好用的模板消息推送一行代码接入微信支付最省事的小程序码生成器一键开通智能客服消息小程序富文本 CMS 和排版渲染组件等等紧接着,随着开发者规模的进一步扩大,各式各样的小程序,也对平台提出了更多的服务需求,如短信能力、邮件能力、错误监测能力等等,我们一一满足。在这个过程中,我们明白,仅在深度上继续深挖是不够的,还需要在服务的广度上多思考。「如何给予小程序开发者更多的支持?」2018 年,对于这个问题,我们的答案是: 除了「想法」(业务逻辑)本身,其他事情都可以交给我们。我们提供的,从来不止是后端云服务,而是一整个服务体系——小程序开发入门培训平台:「知晓课堂」小程序插件开发者的交易平台:「插件市场」小程序定制服务平台:「知晓市场」最好的微信新商业服务平台:「知晓程序」在 2019 年,我们将继续探索答案,希望能和知晓云的用户一起成长。更好地服务于用户这半年,小程序生态也发生了翻天覆地的变化,各大厂商争先恐后接连重注小程序:百度智能小程序:千亿流量、开源、90%广告分成头条小程序:打通抖音,还有八大流量入口支付宝小程序:3年砸10亿激励小程序创业者……这一现象,既反映了大厂们对小程序这一应用形态的看好,也象征着一种趋势。在见过更多的用户后,我们对「愿景」一词有了更深刻的理解:以微信小程序作为起点,随着用户量增加和模式验证的完成 ——> 经历更多的挫折并成长 ——> 去到更多更大的舞台。这个起点,不止是你(小程序)的,也是我们(服务商)的。至此,我们决定开始知晓云的 3.0 计划——全平台 Serverless 服务。希望能让用户得以验证的想法,在全世界开花。在 19 年伊始,我们选择了支付宝小程序作为 3.0 计划的第一站。而在接下来的时间里,我们将一步步对其他各大平台:(百度、头条等)小程序、Web 端、iOS、Android 进行支持,逐步完成全平台后端云服务建设,这一切会发生得非常快。招募公测用户现在,知晓云对支付宝的支持已初步完成。我们将开放公测并招募 50 位用户免费参与。我们希望你是这样的:有小程序开发的经验对开发支付宝小程序感兴趣希望有一个后端云服务平台帮助更快、更省地上线业务公测期间,你将能直接与知晓云的核心工程师深度交流,一起探索小程序领域中更多的可能性。公测开启时间:2019 年 2 月 13 日报名截止时间:2019 年 2 月 18 日期待你的参与,快提交申请吧????本文首发于「知晓云」公众号:https://mp.weixin.qq.com/s/DH…知晓云是国内首家专注于小程序开发的后端云服务。使用知晓云,小程序开发快人一步。

January 29, 2019 · 1 min · jiezi

【实战教程】使用知晓云完成小程序客服消息的自动回复

在上次推送完「卡券核销消息推送」的教程后,我们决定再多出点教程。此次教程将再次带领大家体验「消息推送」,实现另一个「微信消息推送」的触发器,关于自动回复小程序客服消息的。 详细步骤如下:开通知晓云消息推送,并开通小程序消息推送功能,将知晓云消息推送配置同步到小程序创建 sendCustomMsg 触发器,触发器类型为“微信消息推送”,动作类型为云函数创建 sendCustomMsg 云函数总体流程如下流程图:开通消息推送进入知晓云,选择 控制台 -> 设置 -> 小程序 ,拉到最底找到「消息推送」,点击「立即开通」,即可开通消息推送功能。开通之后需要将消息推送的相关配置项配置到小程序或者公众号后台。由于此次我们要实现的是小程序的客服消息回复,所以需要在知晓云的小程序后台开通消息推送(设置->开发设置->消息推送 )并完成相关配置的填写。▲ 知晓云配置项配置项填写完成后还需要将消息推送的相关配置同步到微信小程序后台,两部分内容均填写完成后微信消息推送类型的触发器才可以正常使用。▲ 小程序消息推送配置项创建触发器由于该步骤中在创建触发器时的动作类型需要选择「云函数」,且需要选定对应的云函数 js 文件,所以我们需要在创建触发器前先创建(在控制台点击 引擎 -> 云函数 -> 添加)一个空的云函数,以便顺利执行后续步骤。现在我们创建一个名为 sendCustomMsg 的云函数:如上图点击所示确定即可,云函数的代码等会再写,先回到本小节的重点上来。我们需要(通过控制台 引擎 -> 触发器 -> 添加)创建一个触发器,这个触发器将帮助我们在设置好的条件被触发的情况下运行 sendCustomMsg 云函数来实现用户消息的自动回复。第一步,配置触发器。记得触发类型一定要选择「微信消息推送」。第二步,设置触发条件。这一步需要填写小程序的 appID (小程序 appID 可以在微信小程序后台获取) ,其他部分的设置如下图。第三步,设置动作。按照下图所示配置后点击完成即可成功创建「微信消息推送」触发器。编辑云函数回到我们刚才创建的云函数(名为 sendCustomMsg 的空的云函数), 本次教程中作为示例的云函数代码写的比较简单,只是将用户发送的内容以原样再发回给用户。代码里用到了给用户回复消息的小程序客服消息接口,该接口需要的 access_token 和 open_id 可以分别从云函数的 API 和 云函数的参数 event.data.FromUserName 获取。(标灰色两个地址见置顶留言 )全部代码如下:测试最后我们可以通过小程序客服按钮(contact-button)进入到客服对话框进行测试。在下图中可以看到,我们发送什么给客服,客服都会按原样发回给我们。这意味着,我们的想要的功能实现了 :)本文首发于「知晓云」公众号:https://mp.weixin.qq.com/s/VZ…如果你还想了解 更多小程序开发技巧,快速掌握小程序开发能力。欢迎扫描下方二维码关注「知晓云」,我们会持续为更新与小程序有关的实战教程哦~

January 29, 2019 · 1 min · jiezi

列表渲染 wx:key 的作用、条件渲染 wx:if 与 hidden 的区别

这是微信小程序踩坑系列的第三篇,想要了解更多关于微信小程序开发的那些事,欢迎关注我的《微信小程序》专栏。前言开发微信小程序离不开“页面渲染”,对于初学者来说很难理解小程序里的“页面渲染”是什么、怎么用?而学过 vue 的同学来说,这个就比较熟悉了,实际上就是数据绑定页面渲染。那么关于页面渲染最重要的是列表渲染和条件渲染这两块,先来看看几个简单的例子。下面是个“列表渲染”的例子:<view wx:for="{{array}}"> {{index}}: {{item.message}}</view>Page({ data: { array: [{ message: ‘foo’, }, { message: ‘bar’ }] }})上面的例子可以看出,默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item。当然,使用 wx:for-item 可以指定数组当前元素的变量名,使用 wx:for-index 可以指定数组当前下标的变量名,如下:<view wx:for="{{array}}" wx:for-index=“idx” wx:for-item=“itemName”> {{idx}}: {{itemName.message}}</view>下面是个“条件渲染”的例子:<view wx:if="{{condition}}">True</view>Page({ data: { condition: true }})上面的例子说明,当 condition 为真时,页面渲染上面的 view 标签。当然也可以用 wx:elif 和 wx:else 来添加一个 else 块,如下:<view wx:if="{{length > 5}}">1</view><view wx:elif="{{length > 2}}">2</view><view wx:else>3</view>下面接入正题,探索文章题目的疑问一、 列表渲染中的 wx:key 有什么作用其实初次看 官方文档 可能会对 wx:key 有点懵,官方解释是这样的:根据我多年看文档经验,一般我看不懂的可以忽略不重要的文字,只需关注重点,例如上图的文字加粗部分,因此,一开始我选择不写 wx:key 这个属性。然而在开发过程中写多了列表渲染(而没有加 wx:key)之后,控制台会报很多的 wx:key 的警告,对于有点代码洁癖的我看起来很不爽,但又苦于不清楚 wx:key 的真正作用,于是自创了一个解决办法,那就是在每个列表渲染后面加上 wx:key="{{index}}",类似下面这样:<view wx:for="{{array}}" wx:key="{{index}}"> {{item}}</view>于是我惊奇地发现警告统统不见了,也没有其他负面影响,于是我就这样用了大半年。然而,半年前我做的一个项目里面有个列表渲染需要试试获取用户头像和昵称,于是我之前的做法不管用了,每次获取到的用户信息跟当前内容不对应,并且会发生错乱。于是我重新理解了一遍 wx:key,结合下面的例子,我似乎明白了:<switch wx:for="{{objectArray}}" wx:key=“unique” style=“display: block;"> {{item.id}}</switch>Page({ data: { objectArray: [ {id: 5, unique: ‘unique_5’}, {id: 4, unique: ‘unique_4’}, {id: 3, unique: ‘unique_3’}, {id: 2, unique: ‘unique_2’}, {id: 1, unique: ‘unique_1’}, {id: 0, unique: ‘unique_0’}, ] }})其实,wx:key 是用来绑定当前列表中的项目特征的,也就是说,如果列表是动态更新的,那么 wx:key 的作用是保持原有项目的整个状态不变。结合上面的例子,我们可以知道,对于列表数组是个对象数组,那么 wx:key 属性直接写对应的唯一的属性名就可以了,比如上面的 wx:key=“unique”, 或者 wx:key=“id” 也是可以的,只要保持属性是唯一值就行了,有点类似页面标签里面的 id 属性在页面是唯一的。对于列表数组是个基本类型数组,那么直接写 wx:key="*this” 就可以了,如下:<block wx:for="{{[1, 2, 3]}}" wx:key="*this"> <view>{{index}}:</view> <view>{{item}}</view></block>巧用 wx:key 属性如果很明确自己的列表渲染是个静态列表,那么你可以像我一开始那样做,加个 wx:key="{{index}}" 就可以了反之,如果是个动态列表,那么就得在数组里找到唯一的键值,放在 wx:key 里面当然如果你无视警告,也不影响功能,不加也行二、 wx:if 和 hidden 有什么区别其实我们用条件渲染更多地在用 wx:if 而不是 hidden,因为前者可以拓展,后者缺乏一定的逻辑。然而他们到底有什么区别呢?官方文档 是这样描述的:上图中,我们大概可以了解到,如果需要频繁切换状态,用 hidden,否则用 wx:if。也就是说,wx:if 能够实时创建渲染组件或销毁组件,而且当他为真时才会创建,初始为假时什么也不做,由真变为假时则进行销毁。所以频繁切换他是一个比较耗性能举动。而 hidden 则代表页面初始渲染时就会把该组件渲染在页面上,值的真假只是控制其显示隐藏罢了。页面不销毁,则该组件也不会被销毁。明白了这一点,你会发现,从我们开发者的角度来说,灵活使用这两个条件判断会事半功倍。下面列举几种使用场景给开发者参考:<view class=“load-event” hidden="{{!isAdd}}">加载中……</view><view class=“load-event” hidden="{{!isAdded && isMore}}">没有更多了</view>上面代码是一个上拉加载动画显示与隐藏组件,可以看到用的是 hidden,因为他是一个需要频繁切换的组件。<block wx:if="{{node.name === ‘p’}}"> <view class="{{node.attrs.class}}" data-index="{{index}}" bindtap=“toText”> <text selectable=“true”>{{node.children[0].text}}</text> </view></block><block wx:if="{{node.name === ‘img’}}"> <image class="{{node.attrs.class}}" src="{{node.attrs.src}}"></image></block><block wx:if="{{node.name === ‘video’}}"> <video class="{{node.attrs.class}}" src="{{node.attrs.src}}"></video></block>上面代码展示的是渲染文字还是图片或者是视频,只展示其中的一个那么用 wx:if 最佳。下面是一个自定义 input 组件:<view wx:if="{{isInput}}" class=“show-input”> <view class=“input-item”> <input class=“item-input” bindconfirm=“onSend” bindinput=“inputHandle” bindblur=“hideInput”></input> <view class=“send-btn” catchtap=“onSend”>发送</view> </view></view>其功能是点击评论按钮能实时显示输入框,否则隐藏。这里为什么用 wx:if 呢?因为我希望它显示时是新的 input 组件,不是之前渲染好的,这样如果我刚输入完文字,再次评论不会出现上一次的文字残留。巧用 wx:if 和 hidden有时我们需要提前渲染好里面的子组件,那么要用 hidden,否则待显示时需要加上渲染的时间通常情况下,我在隐藏的时候都不需要该组件的话,那就用 wx:if如果需要在页面中点击切换的渲染,那么考虑小程序性能问题,还是用 hidden 为好三、思考(引伸)1、 <block> 这个元素在列表和条件渲染上是很好用的,不过要注意不要在这个标签上绑定其他属性,比如 data- 或者绑定事件 bindtap。下面是一个反例:<block wx:if="{{true}}" data-id=“1” bindtap=“tapName”> <view>view1</view> <view>view2</view></block>上面的代码里,在 js 中定义绑定事件后,你会发现不会执行。原因就在 <block> 元素在渲染页面后并不会存在,他不是个组件,不会渲染在页面树节点里面,所以在他上面绑定的事件或者属性也不会有效。2、 当 wx:for 的值为字符串时,会将字符串解析成字符串数组;另外,花括号和引号之间如果有空格,将最终被解析成为字符串,请看下面的例子:<view wx:for=“array”> {{item}}</view>等同于<view wx:for="{{[‘a’,‘r’,‘r’,‘a’,‘y’]}}"> {{item}}</view><view wx:for="{{[1,2,3]}} “> {{item}}</view>等同于<view wx:for=”{{[1,2,3] + ’ ‘}}"> {{item}}</view>四、参考链接列表渲染·小程序条件渲染·小程序(完)本文作者 Thinker本文如有错误之处,请留言,我会及时更正觉得对您有帮助的话就点个赞或收藏吧!欢迎转载或分享,转载时请注明出处阅读原文 ...

January 23, 2019 · 2 min · jiezi

微信小程序脚本语言 WXS 怎么用

这是微信小程序踩坑系列的第二篇,想要了解更多关于微信小程序开发的那些事,欢迎关注我的《微信小程序》专栏。前言前几天有个同学问我 微信小程序支持管道过滤器 吗?用过 angular 或者 vue 的同学都应该会在项目里用到 filter,然而在小程序中是不支持的。但是也有一些解决办法,有一篇文章讲得还算不错,我在这里贴一下 微信小程序 使用filter过滤器几种方式 。但我只是关心 WXS 能不能实现 filter 以及还能做什么?带着这样的疑问,我重新看了一遍微信小程序官方的 WXS。下面举个简单的例子:<wxs module=“m1”>var msg = “hello world”; module.exports.message = msg;</wxs><view>{{m1.message}}</view>上面的例子可以输出 hello world 页面,当你阅读完 官方文档,会发现小程序的脚本语言的功能很捉鳖,比如只支持 es5 语法,不支持外部引入 js 等等。但是我仍然期待它未来支持更多的能力。下面接入正题,探索文章题目的疑问一、用 WXS 实现 filter前端通常有一个需求,那就是把后台传过来的时间戳转为不同规格的日期后显示出来。以往的做法一般是用一个函数对数据进行包装,然后输出到页面。就像前面提到的那篇文章里面所说的第一种方法一样,但是有人考虑到性能问题,认为在js里面循环处理比较耗性能(这点我不做评价,毕竟自己没有真正测试过)关于日期格式化的例子在前面提到的文章已经有了,在这里我再举一个比较简单的例子。在我开发过的项目里面,后台返回的网路图片地址通常是相对地址,也就是说要把图片显示出来,还得加上配置好的域名前缀。而我通常是拿到数据后进行遍历操作,把需要前端展示的图片加上前缀。但是有了 WXS,我们可以这样做:<wxs module=“filter”> function getFullPath(url) { return “https://shiyuanjieyi.cn” + url } module.exports.getFullPath = getFullPath</wxs><image src="{{filter.getFullPath(url)}}"></image>在上面这个例子中,可以看到整个过程基本类似于 vue 等框架自定义 filter 的做法。二、 WXS 还能做什么其实很多时候,我们并不了解 WXS 还能做更多条件渲染的一些东西。请看下面一个例子:<wxs module=“filter”> function getData(entry, type) { var imgUrl = ‘’; var content = ‘’; switch (entry) { case ’needs’: imgUrl = ‘/images/goods_empty.png’; content = ‘暂时没有需求’; break; case ‘goods’: imgUrl = ‘/images/goods_empty.png’; content = ‘暂时没有商品’; break; case ‘activity’: imgUrl = ‘/images/activity_empty.png’; content = ‘该专栏暂时没有活动’; break; case ‘channel’: imgUrl = ‘/images/article_empty.png’; content = ‘该专栏暂时没有资讯’; break; case ‘micro-circle’: imgUrl = ‘/images/article_empty.png’; content = ‘没有相关的话题哦’; break; case ’needs-release’: imgUrl = ‘/images/goods_release_empty.png’; content = ‘你还没有发布任何需求哦’; break; case ‘goods-release’: imgUrl = ‘/images/goods_release_empty.png’; content = ‘你还没有发布任何商品哦’; break; case ‘goods-collection’: imgUrl = ‘/images/goods_collect_empty.png’; content = ‘你还没有收藏任何商品哦’; break; case ‘apply’: imgUrl = ‘/images/activity_apply_empty.png’; content = ‘你还没有报名任何活动哦’; break; case ‘activity-collection’: imgUrl = ‘/images/activity_collect_empty.png’; content = ‘你还没有收藏任何活动哦’; break; default: break; } if (type === ‘image’) { return imgUrl; } else { return content; } } module.exports.getData = getData;</wxs><template name=“nodata”> <view class=“no-data”> <image src="{{filter.getData(entry, ‘image’)}}" class=“no-data-icon”></image> <view class=“no-data-text”>{{filter.getData(entry, ‘content’)}}</view> </view></template>上例中,我使用了 WXS 的函数功能帮我做条件判断,拿到对应的模板输出,其功能就是一个空数据展示页面。或许你会问这样写的好处是什么?那我可以告诉你,它很容易扩展,比如新增一个页面需要对应的空数据模板,那么直接在 switch 语句中加多一个 case 即可。况且如果把逻辑写在 WXML 上代码会很复杂,不容易看懂。明白了这一点,你会发现,只要是在 WXML 上需要做一些逻辑判断的操作都可以 WXS 代替。也就是说,在开发中,我们都可以用 WXS 的函数功能帮助我们清晰有效地处理 WXML 上渲染的一些视图。三、思考(引伸)1、 对于需要做成 filter 形式的 WXS,最好把它写在一个.wxs文件里,需要使用时,直接在对应 WXML 上引用即可。var foo = “‘hello world’ from tools.wxs”;var bar = function (d) { return d;}module.exports = { FOO: foo, bar: bar,};module.exports.msg = “some msg”;<wxs src="./../tools.wxs" module=“tools” /><view>{{tools.msg}}</view><view>{{tools.bar(tools.FOO)}}</view>2、 在 .wxs 模块中引用其他 wxs 文件模块,可以使用 require 函数,但是不能引用其他 js 文件模块。四、参考链接WXS·小程序(完)本文作者 Thinker本文如有错误之处,请留言,我会及时更正觉得对您有帮助的话就点个赞或收藏吧!欢迎转载或分享,转载时请注明出处阅读原文 ...

January 22, 2019 · 2 min · jiezi

小程序开发:左滑删除

导语首先声明两点:思路以及代码,是根据资料进行一些修改以及补充,原文地址在此下面的只是 demo,各位根据自己的需要进行修改完善实现的思路摘抄如下1,首先页面每个item分为上下两层,上面一层放置正常内容,下面一层放置左滑显示出的按钮,这个可以使用z-index来实现分层。2,item上层使用绝对定位,我们操纵 left 属性的值来实现像左移动。3,我们通过微信小程序api提供的touch对象和3个有关手指触摸的函数(touchstart,touchmove,touchend)来实现item随手指移动。页面部分在页面中没有太复杂的逻辑,除了正常的循环输出数据,需要添加绑定 touch 事件。<view wx:for="{{array}}"> <view bindtouchstart=“touchS” bindtouchmove=“touchM” bindtouchend=“touchE” style="{{item.txtStyle}}" data-index="{{index}}"> <!– 省略数据 –> </view> <view catchtap=“delOrder” data-index=’{{index}}’ data-order_id=’{{item.order_id}}’>删除</view></view>JS 部分JS 中根据绑定的 touch 事件触发删除按钮,用户点击删除,发送请求,根据返回值对用户进行反馈。Page({ /** * 页面的初始数据 / data: { array:[], delBtnWidth: 150//删除按钮宽度单位(rpx) }, /* * 手指触摸开始 / touchS: function (e) { //判断是否只有一个触摸点 if (e.touches.length == 1) { this.setData({ //记录触摸起始位置的X坐标 startX: e.touches[0].clientX }); } }, /* * 手指触摸滑动 / touchM: function (e) { var that = this; if (e.touches.length == 1) { //记录触摸点位置的X坐标 var moveX = e.touches[0].clientX; //计算手指起始点的X坐标与当前触摸点的X坐标的差值 var disX = that.data.startX - moveX; //delBtnWidth 为右侧按钮区域的宽度 var delBtnWidth = that.data.delBtnWidth; var txtStyle = “”; if (disX == 0 || disX < 0) {//如果移动距离小于等于0,文本层位置不变 txtStyle = “left:0px”; } else if (disX > 0) {//移动距离大于0,文本层left值等于手指移动距离 txtStyle = “left:-” + disX + “px”; if (disX >= delBtnWidth) { //控制手指移动距离最大值为删除按钮的宽度 txtStyle = “left:-” + delBtnWidth + “px”; } } //获取手指触摸的是哪一个item var index = e.currentTarget.dataset.index; var list = that.data.array; //将拼接好的样式设置到当前item中 list[index].txtStyle = txtStyle; //更新列表的状态 this.setData({ array: list }); } }, /* * 手指触摸结束 / touchE: function (e) { var that = this; if (e.changedTouches.length == 1) { //手指移动结束后触摸点位置的X坐标 var endX = e.changedTouches[0].clientX; //触摸开始与结束,手指移动的距离 var disX = that.data.startX - endX; var delBtnWidth = that.data.delBtnWidth; //如果距离小于删除按钮的1/2,不显示删除按钮 var txtStyle = disX > delBtnWidth / 2 ? “left:-” + delBtnWidth + “px” : “left:0px”; //获取手指触摸的是哪一项 var index = e.currentTarget.dataset.index; var list = that.data.array; list[index].txtStyle = txtStyle; //更新列表的状态 that.setData({ array: list }); } }, /* * 删除订单 / delOrder: function (e) { var order_id = e.currentTarget.dataset.order_id; var index = e.currentTarget.dataset.index; var that = this; // 请求接口 wx.request({ url: ‘xxxx’, data: { order_id: order_id }, success: function (res) { if (res.data.message == ‘success’) { // 删除成功 that.delItem(index) } else if (res.data.message == ’error’) { // 删除失败 } }, fail: function () { // 网络请求失败 } }) }, /* * 删除页面item */ delItem: function (index) { var list = this.data.array list.splice(index, 1); this.setData({ array: list }); }})参考资料:微信小程序左滑删除效果的实现、touch 事件。 ...

January 21, 2019 · 2 min · jiezi

小程序开发:上拉加载数据

导语需求是上拉加载数据,实际就是获取分页数据。后台就是正常的ajax请求分页数据,小程序部分稍复杂些,查了一些资料完成的, 但是资料的链接找不到了,不能放上以供参考了。小程序页面涉及到数据循环,下面是简单的实例 <view wx:for="{{array}}"> <view >{{item.name}}</view> <view >{{item.age}}</view> </view>MVVM 的开发模式(例如 React, Vue),提倡把渲染和逻辑分离。简单来说就是不要再让 JS 直接操控 DOM,JS只需要管理状态即可,然后再通过一种模板语法来描述状态和界面结构的关系即可。小程序JS部分JS部分负责的是获取数据,以及拼接数据Page({ /** * 页面的初始数据 / data: { array: [], page: 1, isReachBottom: true // 是否上拉加载 }, // 获取数据 getList: function () { var that = this; wx.request({ url: ‘https://xxx’, data: { p: that.data.page }, success: function (res) { if (res.data.message == ‘success’) { // 获取成功,数据追加 var list = []; var count = res.data.data.length for (var i = 0; i < count; i++) { var data = {name: ‘’, age: ‘’}; data.name = res.data.data[i].name; data.age = res.data.data[i].age; list.push(data); } Array.prototype.push.apply(that.data.array, list); that.setData({ array: that.data.array }) } else if (res.data.message == ‘finish’) { // 没有数据,禁止再次上拉加载 that.setData({ isReachBottom: false }) } } }) }, /* * 页面上拉触底事件的处理函数 */ onReachBottom: function () { if (this.data.isReachBottom == true) { this.setData({ page: this.data.page + 1 }) this.getList() } }})关于上拉触底,还有这些特性参考资料:小程序、列表渲染、注册页面。 ...

January 21, 2019 · 1 min · jiezi

小程序开发:新页面打开链接

导语开发的小程序中有个用户中心,需求是用户可以点击按钮从而跳转到新链接。实际上在做这个需求的时候,并没有很好的方法,以往的一些经验,也不适用于小程序,查了一些资料,也没有头绪。最终的实现方法就是使用navigator组件,如果哪位有更好的方法,或者我的实现方法有缺陷,请在下方指出。外链展示页面首先建立一个目录,这个目录用来展示外链中的内容。因为是外链,所以要用到web-view(注意这个组件有一些特性,从下方链接中查看)。看一下目录结构,其中navigator就是展示外链的页面在navigator.wxml中,只需要一行代码即可<web-view src="{{url}}"></web-view>在navigator.js中,修改url中的值onLoad: function (options) { if (options.url) { this.setData({ // 设置当前链接 url: options.url }) } },用户中心在用户中心,只需要将链接跳转到/navigator/navigator中,并且带上参数即可,看下实例<!– {{url}}中是外链地址 –><navigator url="/navigator/navigator?url={{url}}"></navigator>上面的方法可以实现,当然实际项目要复杂些的,根据实际需求进行修改。参考资料:小程序、navigator、web-view。

January 20, 2019 · 1 min · jiezi

事件关键词 bind 和 catch 的区别、事件对象 target 和 currentTarget 的差异

这是微信小程序采坑系列的第一篇,想要了解更多关于微信小程序开发的那些事,欢迎关注我的《微信小程序》专栏。前言开发微信小程序离不开“事件”,对于初学者来说很难理解小程序里的“事件”是什么、怎么用?先看看看官方文档的解释:看着好像摸不着头脑,其实说白了就是视图(view)与逻辑(js)交互的通信方式,类似于传统网页中的 onclick 事件,了解 vue 的同学也可以认为是监听指令。一个简单的绑定事件例子如下:<view id=“tapTest” data-hi=“WeChat” bindtap=“tapName”>Click me!</view>Page({ tapName(e) { console.log(e) }})乍一看,确实跟 vue 语法有点像,但是有区别,那就是传参方式不一样。所以这里需要注意的是小程序事件传参是通过当前组件上由data-开头的自定义属性组成的集合。比如上面代码定义了一个 hi 属性,tapName 方法拿到的参数 e 展开大致如下:{ “type”: “tap”, “timeStamp”: 895, “target”: { “id”: “tapTest”, “dataset”: { “hi”: “WeChat” } }, “currentTarget”: { “id”: “tapTest”, “dataset”: { “hi”: “WeChat” } }, “detail”: { “x”: 53, “y”: 14 }, “touches”: [ { “identifier”: 0, “pageX”: 53, “pageY”: 14, “clientX”: 53, “clientY”: 14 } ], “changedTouches”: [ { “identifier”: 0, “pageX”: 53, “pageY”: 14, “clientX”: 53, “clientY”: 14 } ]}我们看到这个 hi 属性在 dataset.target 和 dataset.currentTarget 下,它(参数 e)是一个对象,也就是说绑定的 hi 属性可以通过 e.dataset.target.hi 或者 e.dataset.currentTarget.hi 拿到。于是,我们对小程序的事件有了初步的了解,但是当我们用的时候就发现还有很多没有注意到的地方。下面接入正题,探索文章题目的疑问(当然后面还有一些彩蛋)一、 bind 和 catch 有什么区别如果学过前端基础的应该都知道 浏览器事件模型,它分为捕获、目标和冒泡三个阶段(如果需要了解具体详情,可自行百度)。而小程序事件模型没那么复杂,原本只有冒泡阶段,分为冒泡事件(bind)和非冒泡事件(catch)。当然目前也支持捕获阶段,本文暂不做深入讲解,有兴趣可自行查看 官方文档 。看到这里,你知道了 bind 属于冒泡事件,catch 属于非冒泡事件,如你还不知道冒泡是什么意思的话,那我可以稍微解释一下:鱼????在水里突出的水泡总是从下向上浮动,最后浮出水面。而冒泡就是基于这一原理,在节点树中有父子关系,父标签总是嵌套着子标签。当点击绑定事件的子标签之后,如果父标签也绑定了一个事件,那么冒泡的话也会触发父标签的事件,而非冒泡则不会触发。也就是冒泡与否决定事件是否想外传递,方向是向“外”的(由此也提一下,捕获与冒泡相反,事件传递方向是向“内”的)。这里看一个例子:<view id=“outer” bindtap=“handleTap1”> outer view <view id=“middle” catchtap=“handleTap2”> middle view <view id=“inner” bindtap=“handleTap3”>inner view</view> </view></view>在上面这个例子中,点击 inner view 会先后调用 handleTap3 和 handleTap2 (因为tap事件会冒泡到 middle view,而 middle view 阻止了 tap 事件冒泡,不再向父节点传递),点击 middle view 会触发 handleTap2,点击 outer view 会触发 handleTap1。好了,到这里我们就知道了 bind 不会阻止冒泡,事件会一直向上冒泡,而 catch 则会阻止冒泡,只会触发点击的节点事件。巧用 bind 和 catch 绑定事件WXML 代码如果没有很复杂的层层嵌套关系,或者不涉及父子标签都绑定事件的话,我们用 bind 开头的事件即可如果遇到卡片式列表,里面还有点赞或者评论等按钮的,那么这些子节点的事件要用 catch 开头,否则会同时触发父节点的事件在一些自定义弹窗中,可以给父容器定义一个隐藏弹窗的事件,而子容器的事件用 bind 开头,可以达到点击子容器对应事件的同时自动触发隐藏弹窗事件二、 target 和 currentTarget 有什么差异在前面绑定事件我们提到 hi 属性可以通过 target 和 currentTarget 获得,那是不是随便用它们其中一个就可以了呢?答案显然不是的。把上面的例子做一个修改:<view id=“outer” bindtap=“handleTap1”> outer view <view id=“middle” data-hi=“middle” catchtap=“handleTap2”> middle view <view id=“inner” data-hi=“inner” bindtap=“handleTap3”>inner view</view> </view></view>上例中,点击 inner view 时,handleTap3 收到的事件对象 target 和 currentTarget 都是 inner,而 handleTap2 收到的事件对象 target 就是 inner,currentTarget 就是 middle。也就是说,target 里的属性来源于真正点击的那个组件,而 currentTarget 里的属性总是指向触发事件绑定的组件上。进一步说就是,currentTarget 能拿到触发事件所在组件上绑定的任何data- 开头的属性,而 target 则不一定是,它拿到的只是用户所真正点击的那个组件上的data- 属性。明白了这一点,你会发现,从我们开发者的角度来说,更多地是拿到 currentTarget 对象,而不是 target 对象。也就是说,在开发中,我们只要用 currentTarget 这个对象就能够符合我们的开发要求了,没必要用 target 对象,因为一不小心就会拿不到真正想要的参数。下面这种场景是我们以前经常犯的错误:<view id=“outer” data-hi=“outer” bindtap=“handleTap”> outer view … <view id=“inner”>inner view</view></view>上面代码中,如果我们点击 inner view 这个组件,照样会触发 handleTap 事件,然后我们按照以往习惯地用长度比较短的 target 对象去拿我们绑定的属性,但是你会发现取不到 hi 这个属性!然后你又点了一下 outer view,这时候有见鬼了,又可以拿到 hi 这个属性了!(百思不得其解,看来这个 target 对象不稳定啊,还是用 currentTarget 对象会可靠点)以前的想法就是这样,拿参数只用 currentTarget 对象。但是我们现在知道原理了,用起来就好办很多了。下面是点击 inner view 得到的参数:{ “type”: “tap”, “timeStamp”: 2083, “target”: { “id”: “inner”, “offsetLeft”: 140, “offsetTop”: 459, “dataset”: {} }, “currentTarget”: { “id”: “outer”, “offsetLeft”: 140, “offsetTop”: 434, “dataset”: { “hi”: “outer” } }, … 省略无关部分 …}下面是点击 outer view 得到的参数:{ “type”: “tap”, “timeStamp”: 2083, “target”: { “id”: “inner”, “offsetLeft”: 140, “offsetTop”: 459, “dataset”: { “hi”: “outer” } }, “currentTarget”: { “id”: “outer”, “offsetLeft”: 140, “offsetTop”: 434, “dataset”: { “hi”: “outer” } }, … 省略无关部分 …}这里结果验证的正是我前面所说的结论,如果还没理解,那就多看几遍吧。巧用 target 和 currentTarget 对象一般情况下,建议只使用 currentTarget 对象,因为它能拿到我们想要的参数属性如果我们需要获取不同子组件绑定的属性,比如点击某个组件拿到该组件上绑定的属性,才用 target 对象具体应用有待挖掘,回到上面的自定义弹窗,我们点击子容器时可以在隐藏弹窗事件里通过 target 对象拿到子容器上绑定的属性三、思考(引伸)1、 data- 开头的属性书写方式需要注意的地方:以data- 开头,多个单词由连字符-链接,不能有大写(大写会自动转成小写)如 data-element-type,最终在 event.currentTarget.dataset 中会将连字符转成驼峰 elementType。下面是一个简单的例子:<view data-alpha-beta=“1” data-alphaBeta=“2” bindtap=“bindViewTap”> DataSet Test</view>Page({ bindViewTap(e) { console.log(e.currentTarget.dataset.alphaBeta) // 1 (- 会转为驼峰写法) console.log(e.currentTarget.dataset.alphabeta) // 2 (大写会转为小写) }})2、 在前面参数 e 的对象里面的 detail 对象一般是用来取表单数据,比如在 input 组件上的 value 属性,绑定事件里可以通过 detail.value 拿到。(关于 input 组件的详解,敬请期待)四、参考链接事件·小程序(完)本文作者 Thinker本文如有错误之处,请留言,我会及时更正觉得对您有帮助的话就点个赞或收藏吧!欢迎转载或分享,转载时请注明出处阅读原文 ...

January 18, 2019 · 2 min · jiezi

小程序开发:获取用户信息

导语最近在开发小程序,记录下来一些。以前获取用户信息可以使用 wx.getUserInfo ,但是后来官方进行了调整,所以要换一个思路了。获取用户昵称、头像这一步还是很方便的,使用小程序内置的组件就可以实现,可以获取以下的数据可以看到,能获取到的相关信息还是不少的,下面是以头像和昵称为例<!– 头像 –><open-data type=“userAvatarUrl”></open-data><!– 昵称 –><open-data type=“userNickName” lang=“zh_CN”></open-data>获取用户UnionID获取用户的昵称、头像很简单,但实际开发中,我们经常需要用户的UnionID,可以使用wx.login、wx.request来实现。先看下官方给出的流程可以看到流程并不复杂,下面是小程序的JS实例onLoad: function (options) { var that = this; wx.login({ success: function (res) { if (res.code) { // 发起网络请求,获取用户UnionID wx.request({ url: ‘https://xxxx’, data: { code: res.code }, success: function (res) { if (res.data.message == ‘success’) { // 获取数据成功 console.log(res.data.data) } } }) } else { // 获取code失败 console.log(‘登录失败!’ + res.errMsg) } } });}下面是后端PHP的代码<?php$code = $_GET[‘code’];$url = ‘https://api.weixin.qq.com/sns/jscode2session?appid=' . $this->appid . ‘&secret=’ . $this->secret . ‘&js_code=’ . $code . ‘&grant_type=authorization_code’;$userInfo = file_get_contents($url);$userInfo = json_decode($userInfo, true);if (!$userInfo[‘unionid’]) { echo json_encode(array(‘data’=>’’,‘message’=>’error’));} else { echo json_encode(array(‘data’=> $userInfo[‘unionid’],‘message’=>‘success’));}获取到UnionID后,可以继续业务流程了。参考资料:小程序、小程序组件、UnionID机制说明、小程序API。 ...

January 18, 2019 · 1 min · jiezi

微信小程序之scroll-view的flex布局问题

关于微信小程序的scroll-view组件,第一次写的时候是直接在scroll-view中用了一层容器包裹子元素,然后用了flex布局,并且是用了组件来实现的横向滚动,后面有提出改进,但是不记录下,就发现过了几天,就有点懵了1.效果图2.在scroll-view里加一层容器,使用flex布局实现这里用flex布局实现的话,就要用组件的形式wxss文件.scrollView{ padding: 0 20rpx; white-space: nowrap; box-sizing: border-box;}.item{ display: inline-block; margin-right: 20rpx; width: calc(100% / 3); height: 100rpx; background: #ff00ff;}.scrollView1{ display: flex; margin-top: 40rpx; padding: 0 20rpx; width: 100%; flex-wrap: nowrap; box-sizing: border-box;}.item1{ margin-right: 20rpx; width: calc(100% / 3); height: 100rpx; background: #ff00ff;}.scrollView2{ margin-top: 40rpx; padding: 0 20rpx; width: 100%; box-sizing: border-box;}.itemContainer{ display: flex; width: 100%; flex-wrap: nowrap;}.scrollItem{ margin-right: 20rpx;}.scrollView3{ margin-top: 40rpx; padding: 0 20rpx; width: 100%; box-sizing: border-box;}.item3{ margin-right: 20rpx; /* width: calc(100% / 3); */ width: 240rpx; height: 100rpx; background: #aa22dd;}wxml文件<!– 要想使用flex布局实现横向滚动,就要在scroll-view里加一层容器包裹,并且使用子组件才会出现滚动效果 –><scroll-view scroll-x class=“scrollView2”> <view class=“itemContainer”> <block wx:for="{{4}}" wx:key="{{index}}"> <view-item class=“scrollItem” /> </block> </view></scroll-view>子组件里就一个view标签,可以自己直接写3.直接使用display:inline-blockwxml文件<scroll-view scroll-x class=“scrollView”> <block wx:for="{{4}}" wx:key="{{index}}"> <view class=“item”></view> </block></scroll-view>4.自己的理解scroll-view不可以直接使用flex布局,使用flex布局会使得他不会按照预想的那样横向排列、滚动要使用flex布局则要麻烦一点如果直接使用flex布局,不用子组件的话,则会被挤成一排正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)实现单行及多行文字超出后加省略号微信小程序之购物车和父子组件传值及calc的注意事项 ...

January 15, 2019 · 1 min · jiezi

小程序性能优化总结

历史总结:小程序倒计时深究小程序实战踩坑之B2B商城项目总结初试小刀自我简历小程序启动加载优化在小程序启动时,微信会在背后完成几项工作:下载小程序代码包、加载小程序代码包、初始化小程序首页。初始化小程序环境是微信环境做的工作,我们只需要控制代码包大小,和通过一些相关的缓存策略控制,和资源控制,逻辑控制,分包加载控制来进行启动加载优化。勾选开发者工具中, 上传时压缩代码(若采用wepy高级版本,自带压缩,请按官网文档采取点击)精简代码,去掉不必要的WXML结构和未使用的WXSS定义。减少在代码包中直接嵌入的资源文件。(比如全国地区库,微信有自带的,在没必要的时候,勿自用自己的库)及时清理无用的资源(js文件、图片、demo页面等)压缩图片,使用适当的图片格式,减少本地图片数量等如果小程序比较复杂,优化后的代码总量可能仍然比较大,此时可以采用分包加载的方式进行优化,分包加载初始化时只加载首评相关、高频访问的资源,其他的按需加载。提前做异步请求,页面最好在onLoad时异步请求数据,不要在onReady时请求启用缓存数据策略,请求时先展示缓存内容,让页面尽快展示,请求到最新数据之后再刷新避免白屏,使用骨架屏等数据通信优化为了提升数据更新的性能,开发者在执行setData调用时,最好遵循以下原则:不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用;数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用setData来设置这些数据;与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下。提升数据更新性能方式的代码示例:Page({ onShow: function() { // 不要频繁调用setData this.setData({ a: 1 }) this.setData({ b: 2 }) // 绝大多数时候可优化为 this.setData({ a: 1, b: 2 }) // 不要设置不在界面渲染时使用的数据,并将界面无关的数据放在data外 this.setData({ myData: { a: ‘这个字符串在WXML中用到了’, b: ‘这个字符串未在WXML中用到,而且它很长…………………………’ } }) // 可以优化为 this.setData({ ‘myData.a’: ‘这个字符串在WXML中用到了’ }) this._myData = { b: ‘这个字符串未在WXML中用到,而且它很长…………………………’ } }})事件通信优化视图层会接受用户事件,如点击事件、触摸事件等。当一个用户事件被触发且有相关的事件监听器需要被触发时,视图层会将信息反馈给逻辑层。这个反馈是异步的,会产生延迟,降低延迟的方法有两个:去掉不必要的事件绑定(WXML中的bind和catch),从而减少通信的数据量和次数;事件绑定时需要传输target和currentTarget的dataset,因而不要在节点的data前缀属性中放置过大的数据。渲染优化页面方法onPageScroll使用, 每次页面滚动都会触发,避免在里面写过于复杂的逻辑 ,特别是一些执行重渲染页面的逻辑(另外,可以看我的文章——移动端滚动研究,说明了在滚动的情况下导致的渲染性能低下的各种分析和应付方法总结)在进行视图重渲染的时候,会进行当前节点树与新节点树的比较,去掉不必要设置的数据、减少setData的数据量也有助于提升这一个步骤的性能。

January 9, 2019 · 1 min · jiezi

微信小程序地图(map)组件点击(tap)获取经纬度

微信小程序中使用地图(map)组件,通过点击(tap)获取经纬度,按照官方的回应,暂时是没法做到的,从地图组件API多有残缺判断,怀疑是个实习生干的…做个变通,适用性有限,请大家参考。基本思路就是在地图上铺满一层marker,从而通过点击marker获得经纬度。<map id=“map” longitude=“102.324520” latitude=“40.099994” scale=“4” bindcontroltap=“controltap” polygons="{{polygons}}" bindregionchange=“regionchange” markers="{{markers}}" bindmarkertap=“markertap” show-location style=“width: 100%; height: 700px;"></map>const app = getApp()const markersize = 30function range(start, edge, step) { for (var ret = []; (edge - start) * step > 0; start += step) { ret.push(start); } return ret;}function markers(northeast, southwest, scale, width, height) { const markerslng = (northeast.longitude - southwest.longitude) * markersize / width const markerslat = (northeast.latitude - southwest.latitude) * markersize / height const maxlon = northeast.longitude const minlon = southwest.longitude const maxlat = northeast.latitude const minlat = southwest.latitude const lons = range(minlon, maxlon, markerslng) const lats = range(minlat, maxlat, markerslat) let _markers = [] lons.forEach((lon, i) => { lats.forEach((lat, j) => { _markers.push({ id: lon + ‘,’ + lat, latitude: lat, longitude: lon, iconPath: ‘/marker.png’, alpha: 0.1, //将图片设置为透明,通过开发者工具看不出效果,但真机是有效果的 width: markersize, height: markersize }) }) }) return _markers}Page({ data: { polygons: [], controls: [{ id: 1, position: { left: 0, top: 300 - 50, width: 50, height: 50 }, clickable: true }], markers: [] }, createMarkers() { this.mapCtx = wx.createMapContext(‘map’) const query = wx.createSelectorQuery() const map = query.select(’#map’).boundingClientRect() let that = this that.mapCtx.getRegion({ success(res1) { that.mapCtx.getScale({ success(res2) { query.exec((res) => { let width = res[0].width; let height = res[0].height; let _markers = markers(res1.northeast, res1.southwest, res2.scale, width, height) that.data.markers = _markers that.setData(that.data) }) } }) } }) }, regionchange(e) { this.createMarkers() }, markertap(e) { console.log(e.markerId) }, controltap(e) { console.log(e.controlId) }, onReady(e) { this.createMarkers() }})效果如图 ...

January 9, 2019 · 2 min · jiezi

微信小程序解码工具

项目地址 https://github.com/sjatsh/unw… & 个人博客 https://sjatsh.com起因 前段时间想学习微信小程序开发但是又没有什么深厚前端功底,看到很多很好玩的小程序想要做一个类似的学习学习,所以想着借鉴一下现有的小程序。但是苦于没有源码,抓包也没有办法获取源码。google后知道可以用安卓模拟器安装微信然后安装小程序,然后在文件系统中找到小程序对应的wxapkg文件,拿到压缩包解压后就可以得到小程序源码。但是压缩包是2进制文件,找了一遍后发现一个现有现有的开源项目可以直接解压小程序压缩包。废话不多说,直接开干~获取小程序压缩包文件一、下载网易MuMu安卓模拟器下载地址 http://mumu.163.com/二、安装微信和RE文件管理器下载微信和RE文件管理器三、安装好微信和RE文件管理器后访问/data/data/com.tencent.mm/MicroMsg/{{一个16进制字符串}}/appbrand/pkg/目录进入小程序文件目录四、压缩文件并且发送到电脑压缩小程序压缩包发送到电脑在这之后我们就可以使用工具进行小程序压缩包的解压了,下面直接看如何使用工具解压。工具使用一、源码安装安装golang没有用过golang的人可以直接去官网下载go get github.com/sjatsh/unwxapkgcd ~/go/src/github.com/sjatsh/unwxapkg二、使用可执行文件下载地址 https://github.com/sjatsh/unw…使用unwxapkg -f dest/102.wxapkg图片发自简书App项目地址也希望可以关注我的个人博客 https://sjatsh.com

January 8, 2019 · 1 min · jiezi

小程序云开发实战系列02--云数据库

以前一直是使用关系型数据库,第一次使用NoSQL,跟大家分享一下我有限的使用心得,希望对像我一样初使用NoSQL的开发者有所帮助。首先说说微信小程序云开发里集成的这个NoSQL,官方并没有说明是哪种NoSQL数据库,但从开发文档和暴露的API,还有官方论坛里的讨论来看应该是一个简化版的MongoDB。需要指出的是微信小程序关于云数据库的开发文档非常的简略,对于像我这样没有太多NoSQL经验的用户,很多时候需要参考MongoDB的相关文档。接下来重点谈谈我在使用这个NoSQL云数据库时最不适应的一个痛点—-文档级别的原子操作。我们经常要使用到原子操作,来避免当多个用户同时对同一个field(字段)编辑时发生冲突。我在使用前其实最担心的痛点是有无schema的区别,但是使用下来发现我挺习惯,也挺喜欢无schema的,后文再详说。现在具体来看看MongoDB只支持document(文档)级别的原子操作。对于我来说,这个限制鼓励我尽量把所有关系都放在一个document里。对此我一开始是有点抗拒的,对于从关系型数据库过来的人特别不习惯。而更让我苦恼的是微信小程序云开发集成的这个云数据库是一个简化版MongoDB,只提供了非常有限的原子操作指令(command)。对于一些常用的document级别原子操作,我必须构想自己的解决办法,而没有提供直接对应的command。以下是两个我在实际开发中遇到的这类问题及我的解决办法:1.应用场景:对于一个视频,我需要一个叫total_likes的field(字段),当有用户点击“喜欢”时该field递增1,当有用户取消“喜欢”时该field递减1。痛点:小程序云数据库只提供了递增指令的原子操作,没有提供递减指令。const _ = db.commanddb.collection(‘video’).doc(‘video-id’).update({ data: { total_likes: _.inc(1) }})解决办法:要实现递减的原子操作,只需在递增指令里传入负数,如data: { total_likes: _.inc(-1)}2.应用场景:对于一个线上课程,我需要一个叫subscribers的field(字段)来记录有多少人订阅了该课程。当有用户点击“订阅”时该字段需记录该用户的id,名字及头像;当有用户取消“订阅”时需把该用户从subscribers字段里删除。痛点:我们很自然的会想到用数组(Array)数据类型来维护subscribers这个字段,虽然小程序云数据库提供了一些针对数组的原子操作,如push,pop,shift和unshfit,可是无法实现取消订阅这个场景的原子操作。解决办法:弃用Array转而使用对象(object)数据类型来维护subscribers这个字段。最终的数据看起来会是这样的:{ “subscribers”: { “userID-1”: { “name”: “小明”, “avatar”: “https://avatar-1.com” }, “userID-2”: { “name”: “小红”, “avatar”: “https://avatar-2.com” }, “userID-3”: { “name”: “小李”, “avatar”: “https://avatar-3.com” }, … }}当有用户订阅时的原子操作:const subscriber = “subscribers.” + user.id;db.collection(‘class’).where({ _id: ‘classID’,}).limit(1).update({ data: { [subscriber]: { avatar: user.avatar, name: user.name, } }})当有用户取消订阅时的原子操作:const subscriber = “subscribers.” + user.id;db.collection(‘class’).doc(‘classID’).update({ data: { [subscriber]: _.remove() }})前文说到我很喜欢无schema,因为它非常适合快速迭代开发。而且由于云数据库使用的是类似JSON的数据结构,对于全栈开发者,基本上可以实现由前端来定义数据结构。这样的开发流程非常适合小团队,不需要庞大的并行开发,突出沟通效率和对产品需求的随机应变。顺带一提的是微信小程序云开发能力是从基础库2.2.3开始支持的,但如果要支持所有版本的基础库,可以在 app.json / game.json 中增加字段 “cloud”: true本系列第一章:小程序云开发实战系列01–云环境设置《Meetup丨活动报名组局》是我最近开发的一个活动报名预约工具小程序,这个系列文章主要来自我在开发这款小程序时的一些体会心得。感兴趣的小伙伴可以扫下面的二维码进入我的小程序。 ...

January 6, 2019 · 1 min · jiezi

微信小程序发送短信验证码完整实例

微信小程序注册完整实例,发送短信验证码,带60秒倒计时功能,无需服务器端。效果图:代码:index.wxml<!–index.wxml–><view class=“container”> <view class=‘row’> <input placeholder=‘请输入姓名’ bindinput=‘bindNameInput’/> </view> <view class=‘row’> <input placeholder=‘请输入手机号’ bindinput=‘bindPhoneInput’/> </view> <view class=‘row’> <input placeholder=‘请输验证码’ bindinput=‘bindCodeInput’ style=‘width:70%;’/> <button class=‘codeBtn’ bindtap=‘getCode’ hidden=’{{hidden}}’ disabled=’{{btnDisabled}}’>{{btnValue}}</button> </view> <view> <button class=‘save’ bindtap=‘save’ >保存</button> </view> </view>index.js//index.jsvar zhenzisms = require(’../../utils/zhenzisms.js’);//获取应用实例const app = getApp(); Page({ data: { hidden: true, btnValue:’’, btnDisabled:false, name: ‘’, phone: ‘’, code: ‘’, second: 60 }, onLoad: function () { }, //姓名输入 bindNameInput(e) { this.setData({ name: e.detail.value }) }, //手机号输入 bindPhoneInput(e) { console.log(e.detail.value); var val = e.detail.value; this.setData({ phone: val }) if(val != ‘’){ this.setData({ hidden: false, btnValue: ‘获取验证码’ }) }else{ this.setData({ hidden: true }) } }, //验证码输入 bindCodeInput(e) { this.setData({ code: e.detail.value }) }, //获取短信验证码 getCode(e) { console.log(‘获取验证码’); var that = this; zhenzisms.client.init(‘https://sms_developer.zhenzikj.com’, ‘appId’, ‘appSecret’); zhenzisms.client.send(function (res) { if(res.data.code == 0){ that.timer(); return ; } wx.showToast({ title: res.data.data, icon: ’none’, duration: 2000 }) }, ‘15801636347’, ‘验证码为:3322’); }, timer: function () { let promise = new Promise((resolve, reject) => { let setTimer = setInterval( () => { var second = this.data.second - 1; this.setData({ second: second, btnValue: second+‘秒’, btnDisabled: true }) if (this.data.second <= 0) { this.setData({ second: 60, btnValue: ‘获取验证码’, btnDisabled: false }) resolve(setTimer) } } , 1000) }) promise.then((setTimer) => { clearInterval(setTimer) }) }, //保存 save(e) { console.log(‘姓名: ’ + this.data.name); console.log(‘手机号: ’ + this.data.phone); console.log(‘验证码: ’ + this.data.code); //省略提交过程 }})index.wxss/index.wxss/page{ height: 100%; width: 100%; background: linear-gradient(#5681d7, #486ec3); display: flex; flex-direction: column;}.container{ display: flex; flex-direction: column; justify-content: space-around; width: 90%; margin: 50rpx auto;} .row{ position: relative; height: 80rpx; width: 100%; border-radius: 10rpx; background: #fff; margin-bottom: 20rpx; padding-left: 20rpx; box-sizing: border-box;}.row input{ width: 100%; height:100%;}.codeBtn{ position: absolute; right: 0; top: 0; color: #bbb; width: 30%; font-size: 26rpx; height: 80rpx; line-height: 80rpx;}.subBtn{ width: 200rpx; height: 80rpx; background: #fff; color: #000; border-radius: 50rpx; line-height: 80rpx;}完整下载: 下载详情参考: http://smsow.zhenzikj.com/doc… ...

January 6, 2019 · 2 min · jiezi

「前端早读君009」快速小程序开发之微信小程序内嵌 H5

今日励志语要接受自己行动所带来的责任而非自己成就所带来的荣耀。前言微信小程序中可以直接运行 web 页面,这一新组件 web-view 的产生,可能直接导致小程序数量迎来一波高峰。本篇博文将从业务选型,微信小程序后台配置,使用 web-view 完成登录业务以及在实战中如何调试一次性带你感受小程序内嵌 H5 的风采,帮你更有底气的使用微信小程序新组件 web-view。技术选择H5 转成小程序方案路线优缺点对比上表是从原有 H5 转相似业务逻辑的微信小程序的方案路线优缺点对比,基于时间的限制以及当前主流多端编译的可靠性考量,最终团队认为通过使用小程序组件 web-view 内嵌 H5 的方式比较适合当前的开发需求。 web-view 的兼容性问题主要是要在基础库在1.6.4+及以上才可以用,而微信官方统计基础库在1.6.4+ 的覆盖率已达 95% 以上,这个指标也符合产品大人的要求,于是便愉快的选择了 web-view 内嵌 H5 的技术方案。H5 和小程序技术对比从上表中可以对比出 H5 相较于小程序的优缺点,方便各位前端大大评估需求。尤为重要的是因为小程序封装的比较严重所以小程序开发的灵活性没有 H5 那么高,这就要我们注意对交互设计的评估。web-view 微信小程序配置系列问题配置域名业务域名中配置的就是小程序以及 H5 和 H5 中引用 iframe 的域名。这里要特别注意的是假如 H5 中有内嵌的 iframe 也要配置进去这里需要服务端的朋友配合一下,将校验文件放置在将要嵌套的业务域名的根目录。所以要注意后端是否可以支持,否则会有各种扯皮的问题选择基础库开发的时候不要忘记配置微信小程序的基础库,注意 web-view 要在基础库1.6.4以上的版本库才能使用H5 中引入微信的 jssdk,其中包含了h5和小程序直接的通讯方法web-view 与小程序的通信官方给出了两种通信方法(如下图所示) 1、postMessage 通信 在 H5 中需要先用 wx.miniProgram.postMessage 接口,把需要分享的信息,推送给小程序。 在用户点击了小程序后退、组件销毁、分享这些特殊事件之后,小程序页面通过 bindmessage 绑定的函数读取 post 信息。 2、设置 web-view 组件的 URL 通信 H5 跳转小程序:toWeixin() { wx.miniProgram.navigateTo({url: ‘/pages/myWelfare/myWelfare’});}小程序跳转 H5: 首先在 .wxml 中引入 web-view 组件<view><web-view src="{{url}}" ></web-view></view>之后在小程序的 js 文件中设置通过 URL 以问号传参的方式传入参数到 H5 中if(!option.page){ this.setData({ url: ${this.data.url}?${test} }); } else { this.setData({ url: ${this.data.url}${option.page}?${test} }); }小程序内嵌 H5 登录实例小程序登录实现方案流程图:如上图所示:整个登录的 cookie 的传递经历了四个步骤:在小程序登录后获取到后端传递的 cookie 并保存通过 web-view 中的 url 传递到 H5 中在 H5 中得到传递的值并写到 cookie 中在访问接口的时候带上 cookie 2、从微信小程序响应头中获得 cookie 存到 storage 中:首先在登录页获取到响应头中的登录 cookie 放到 storage 中wx.setStorageSync(‘cookie’, res.header[“Set-Cookie”]);在微信小程序中每次请求接口的时候,将 storage 中的 cookie 取出来,放到请求头中,如果传入不正确或者没有传入 cookie 值,后台将返回 errorCode 为 3002 ,此时前端跳转到登录页面。 var headerCookie = wx.getStorageSync(‘cookie’); wx.request({ url: murl, data: parameter.data || {}, header: { ‘Cookie’: headerCookie }, method: parameter.method || ‘POST’, success: function(res) { if(res.data.code == 3002) { wx.redirectTo({ url: ‘../login/login’ }) } else { parameter.success && parameter.success(res); } }3、获得 cookie 并拼接到 URL 中首先在 web-view 页面获取 cookie ,并匹配到需要传递的字段,之后将此字段放到 url 中通过问号传参的方式传递到 H5 中try { var value = wx.getStorageSync(‘cookie’); if (value) { test = value.match(new RegExp("(^| )"+“jxi-m-sid”+"=([^;]*)(;|$)"))[2] ; } }url: ‘https://www.xxx.com#', if(!option.page){ this.setData({ //在这里放入传递的字段(如test) url: ${this.data.url}?${test} }); } else { this.setData({ //在这里放入传递的字段,也可以拼接更多的信息(如option.page) url: ${this.data.url}${option.page}?${test} }); }4、获取 cookie 并在 H5 中使用 在 H5 中获取 cookie 值,并带入 cookie ,注意 domain 和 path 的设置,这两个值必须都有:let isDebug = (window.location.href).indexOf(‘myf’) > -1;let host = isDebug ? ‘jd’ : ‘jdf’;//获得传递的字段let c =window.location.href.split(’?’)[1];//设置cookiedocument.cookie = jxi-m-sid=${c};domain=${host};path=/;小程序内嵌 H5 调试解决方案关于调试效果缓存的问题 小程序的更新机制即当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是 5 分钟)会被微信主动销毁。 小程序销毁后再重新启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次销毁后再重新启动时才会应用上。内嵌 H5 调试问题 因为微信小程序开发者工具中并没有提供内嵌 H5 的调试工具,所以我们可以采取以下方法调试在 H5 开发的时候,我们可以在微信小程序开发者工具中找到 web-view 传递给 H5 的 URL 链接将此链接黏贴到浏览器中即可像平时开发 H5 一样调试对于手机端 H5 的测试调试可以在 H5 中引入 vconsole 这个插件来调试程序,他可以让我们看到接口情况以及 H5 中的日志。总结看到这里,我们分别从web-view 技术的优缺点web-view 微信小程序配置系列问题web-view 与小程序的通信小程序内嵌 H5 登录实例小程序内嵌 H5 调试解决方案五个方面梳理了关于微信小程序 webview 组件的使用。 当然,微信小程序组件 web-view 还并不完善,其中很难实现一些特殊的交互,比如返回按钮返回的页面只能是上一级不能是任意自定义的页面等问题,这就需要我们更好的沟通以及规划交互设计,同时反馈并等待微信小程序官方的更新和支持。文章来源:京东设计中心打开微信扫一扫关注早读君,每天早晨为你推送前端知识,度过挤地铁坐公交的时光 ...

January 3, 2019 · 2 min · jiezi

【实战教程】使用知晓云完成微信卡券消息的处理

知晓云新推出「消息推送」功能,该功能通过「消息推送」类型的触发器接收微信推送过来的消息,开发者可以对此消息实行相关操作。此教程将带领大家体验这个新功能,实现一个「微信消息推送」类型的触发器。该触发器将会在消费者核销微信优惠券的时候触发,并调用一个云函数。调用的云函数会将核销的相关信息保存在一个特定的数据表中,由此来记录公众号卡券的使用情况。一、开通知晓云消息推送功能知晓云控制台 -> 设置 -> 小程序,拉到最底找到「消息推送」,点击 立即开通,即可开通消息推送功能:开通之后需要将消息推送的相关配置项配置到小程序或者公众号后台,此教程为实现接收微信卡券核销事件的消息的功能,所以需要将配置项配置到公众号后台。进入公众号后台 点击开发->基本配置->服务器配置,同步配置成知晓云消息推送的相关配置,完成配置后,微信消息推送类型的触发器方可使用。▲ 知晓云配置项▲ 微信公众平台配置项二、创建云函数后面我们会创建一个动作类型为「云函数」的触发器,所以我们需要先创建一个云函数,供触发器使用。现在我们创建一个云函数,用于用户在核销卡券的事件触发时调用该云函数操作数据表,将微信推送过来的核销的相关数据保存到相关到表中。云函数 user_consume_card.js,tableID 必须是已存在 table (需要提前在知晓云控制台创建表),并且有 data 中相应字段的列,否则将创建不成功。微信消息推送触发器返回的数据可在云函数的第一个参数 event.data 获取到,具体返回的数据有哪些可以查看微信开发者文档。例如卡券核销的返回数据如下,只是触发器将微信的 XML 格式的数据转换成了 JSON 格式。三、创建触发器添加一个「微信消息推送」类型的触发器,该触发器会在指定的条件触发时触发该触发器,点击 引擎 -> 触发器 -> 添加 。1.触发器配置,触发器类型一定要选择「微信消息推送」2.触发条件设置这一步需要填写公众号的 AppID ,ID 的获取需要进入「公众号平台」,点击 开发->基本配置->服务器配置 即可看到 AppID。参数规则: 触发触发器的参数规则可以填写微信事件名称,如果需要查看相关事件名称可以查看微信卡券事件推送教程。此教程为卡券核销事件推送,因此触发条件就为 evnet = user_consume_card 的事件:3.动作 动作类型选择云函数,当然你也可以选择其他的动作类型,选中我们创建云函数步骤中创建的云函数,配置好之后,当用户核销卡券时即会触发该触发器,并调用对应的云函数。至此,我们的微信消息推送触发器的 Demo 就完成了。后续需要通过创建卡券,并核销卡券触发该触发器:四、创建优惠券接下来我们来制作微信卡券,制券成功并核销后,将触发我们上面创建的触发器。1.制券 微信公众平台 -> 卡券功能 -> 优惠券,点击「新建优惠券」,并选择卡券类型填写卡券详细信息注意选择合适的「核销方式」,这关联到后面的核销可以采用那种方式进行核销。填写好相关信息点击下一步进入到「使用设置」,填写相关信息后提交审核。2.投放 审核通过后即可投放,支持二维码、公众号图文、群发、摇周边等多种形式投放,这里选择下载二维码进行投放消费者扫码领取优惠券,点击「立即使用」显示优惠券码,如果在第三步中「使用设置」的「核销方式」选择了二维码或者条形码,则会以优惠券的二维码或者条形码 + 卡券号的形式显示,本教程选择了「仅卡券号」,所以只显示了卡券号。核销(三种方式)手机核销网页核销自助核销本教程采用手机核销,其他核销方式可在「微信公众平台」查看。添加核销员之前,店员需要先关注「卡券商户助手」公众号,才能配置核销权限:添加核销员 添加核销员后,店员可在「卡券商户助手」中选择「扫一扫核销」|者「卡券号核销」五、查看触发器触发日志卡券核销之后,可以在知晓云控制台 -> 引擎 -> 触发器,找到之前创建的触发器( user_consume_card ),查看日志。触发器调用云函数,并且创建了一条核销记录,可在数据表中找到该表进行查看。本文首发于「知晓云」公众号:https://mp.weixin.qq.com/s/_w…如果你还想了解 更多小程序开发技巧,快速掌握小程序开发能力。欢迎扫描下方二维码关注「知晓云」,我们会持续为更新与小程序有关的实战教程哦~

December 29, 2018 · 1 min · jiezi

个税计算器 / 微信小程序开发

2019年1月1日即将到来,码农们除了关心自己的技能之外,还有薪资是不是可以多拿点。 每次算的时候 都要百度一下个人所得税,但是很多都是老的税率计算,找一个新的出来还是比较麻烦,所以个人开发了一个最新税率的小程序。解决和我有着一样痛苦的码农们的问题。根据最新税改后计算个人所得税的计算器。目前支持南京,后续开放 杭州 上海 北京等城市。 如果有疑问的可以加最下方 开发者微信。首先在微信官网下载微信小程序开发工具 https://mp.weixin.qq.com在https://mp.weixin.qq.com 注册小程序账号,完成个人实名认证。在小程序后台拿到appid,下面就可以开发了。实例查看二维码:先使用weui 小程序ui框架就行页面布局<button block type=“dark” bindtap=‘calculationBindtap’>计算</button>其次写JS代码(计算按钮逻辑代码)import data from ‘./data’const app = getApp;Page({ data: { options1: data, value: ‘1’, checked: true, standard: 1, marking: 5000, beforetaxCount: 0, specialitemCount: 0 }, calculationBindtap:function(){ // 开始计算 计算完成把计算结果放在result对象中 var beforetaxCount = this.data.beforetaxCount; var specialitemCount = this.data.specialitemCount; var marking = this.data.marking; if (beforetaxCount == null || beforetaxCount == 0 || beforetaxCount == ‘’){ wx.showToast({ title: ‘输入正确薪资’, mask: true, icon: ’loading’ }) return; } if (specialitemCount == null || specialitemCount == ‘’){ specialitemCount = 0; } // 开始计算 var oldNum = 0.08; var medNum = 0.02; var unemNum = 0.005; var workNum = 0; var giveNum = 0; var providentfundNum = 0.08; var insuranceBase = 19935; var providentfundBase = 25300; var oldcount = 0; var medcount = 0; var unemcount = 0; var workcount = 0; var givecount = 0; var providentfundcount = 0; var privateFee = 0; var plusFee = 0; if(this.data.checked){ if (parseFloat(beforetaxCount) > parseFloat(insuranceBase)) { oldcount = parseFloat(insuranceBase) * parseFloat(oldNum); medcount = parseFloat(insuranceBase) * parseFloat(medNum); unemcount = parseFloat(insuranceBase) * parseFloat(unemNum); workcount = parseFloat(insuranceBase) * parseFloat(workNum); givecount = parseFloat(insuranceBase) * parseFloat(giveNum); } else { oldcount = parseFloat(beforetaxCount) * parseFloat(oldNum); medcount = parseFloat(beforetaxCount) * parseFloat(medNum); unemcount = parseFloat(beforetaxCount) * parseFloat(unemNum); workcount = parseFloat(beforetaxCount) * parseFloat(workNum); givecount = parseFloat(beforetaxCount) * parseFloat(giveNum); } if (parseFloat(beforetaxCount) > parseFloat(providentfundBase)) { providentfundcount = parseFloat(providentfundBase) * parseFloat(providentfundNum); } else { providentfundcount = parseFloat(beforetaxCount) * parseFloat(providentfundNum); } } // 保险总费用 var totalInsuranceFee = parseFloat(oldcount) + parseFloat(medcount) + parseFloat(unemcount) + parseFloat(workcount) + parseFloat(givecount); // 公积金费用 var totalProvidentfundFee = providentfundcount; // 下面的钱 交税 console.log(this.data.marking); var otherFee = parseFloat(beforetaxCount) - parseFloat(totalInsuranceFee) - parseFloat(totalProvidentfundFee) - parseFloat(this.data.marking) - parseFloat(specialitemCount); if (parseFloat(otherFee) <= 3000 && parseFloat(otherFee) > 0) { privateFee = parseFloat(otherFee) * 0.03; plusFee = 0; } if (parseFloat(otherFee) <= 12000 && parseFloat(otherFee) > 3000) { privateFee = parseFloat(otherFee) * 0.1; plusFee = 210; } if (parseFloat(otherFee) <= 25000 && parseFloat(otherFee) > 12000) { privateFee = parseFloat(otherFee) * 0.2; plusFee = 1410; } if (parseFloat(otherFee) <= 35000 && parseFloat(otherFee) > 25000) { privateFee = parseFloat(otherFee) * 0.25; plusFee = 2660; } if (parseFloat(otherFee) <= 55000 && parseFloat(otherFee) > 35000) { privateFee = parseFloat(otherFee) * 0.3; plusFee = 4410; } if (parseFloat(otherFee) <= 80000 && parseFloat(otherFee) > 55000) { privateFee = parseFloat(otherFee) * 0.35; plusFee = 7160; } if ( parseFloat(otherFee) > 80000) { privateFee = parseFloat(otherFee) * 0.45; plusFee = 15160; } var result = {}; result.insuranceCount = totalInsuranceFee; result.providentfundCount = totalProvidentfundFee; result.providentfundNum = parseFloat(providentfundNum) * 100; result.money = parseFloat(beforetaxCount) - parseFloat(totalInsuranceFee) - parseFloat(totalProvidentfundFee) - parseFloat(privateFee) + parseFloat(plusFee); result.privateFee = privateFee - parseFloat(plusFee); result.specialitemCount = specialitemCount; result.oldNum = parseFloat(oldNum) * 100; result.medNum = parseFloat(medNum) * 100; result.unemNum = parseFloat(unemNum) * 100; result.workNum = parseFloat(workNum) * 100; result.giveNum = parseFloat(giveNum) * 100; result.oldcount = parseFloat(oldcount); result.medcount = parseFloat(medcount); result.unemcount = parseFloat(unemcount); result.workcount = parseFloat(workcount); result.givecount = parseFloat(givecount); wx.setStorage({ key: ‘result’, data: result, success:function(){ wx.navigateTo({ url: ‘../calculation/calculationResult’, }) } }) },})把计算好的结果放在result对象中 通过wx.setStorage 放在缓存中,传到下一个页面。最后展示出来。 ...

December 29, 2018 · 3 min · jiezi

小程序倒计时深究

小程序倒计时重叠抖动问题因为请求数据写在onShow 函数里面,所以每次切换界面都会刷新,这就会导致,如果当前 定时器在跑的话,再次刷新会再次常见定时, 那么就会导致刷新几次有几个定时器,同时在跑,那么前端界面显示的计时数字 就会不时跳动,所以需要保证在跑的定时器只有一个。将定时器对象创建为全局的,在每次开启定时器的时候先清空之前的定时器。就可以解决刷新后计时闪动的问题了,或者在在tab页面,运用 onHide 周期 进行 clearTimeInterval清空 , 在 非tab页面,运用onUload() 周期 进行 clearTimeInterval清空,百度都可以找到类似解决方案,其中在我的历史文章小程序实战踩坑之B2B商城项目总结也有总结,代码类似如下:/** * 清除interval * @param that / clearTimeInterval: function (that) { var interval = that.data.interval; clearInterval(interval) }, /* * 生命周期函数–监听页面卸载 * 退出本页面时停止计时器 / onUnload:function () { var that = this; that.clearTimeInterval(that) }, /* * 生命周期函数–监听页面隐藏 * 在后台运行时停止计时器 */ onHide:function () { var that = this; that.clearTimeInterval(that) }倒计时使用setInterval或setTimeout触摸屏幕导致时间显示的突跳,突慢问题,卡顿,甚至停止不信的同学,可以尝试用手指触摸屏幕,上下小幅上下移动不放,你会发觉时间竟然停止了。(特别是针对低端机型)通常同学写代码都会如此: let self = this; let lefttimeSec = time - new Date().getTime(); let calc = setInterval(function() { lefttimeSec -= 1000; self.endtimestr = ‘距离拼单结束还有’ + self.dateformat(lefttimeSec); self.$apply(); if (lefttimeSec <= 0) { clearInterval(calc); } }, 1000);使用setInterval后,即使用了上面说的“小程序倒计时重叠抖动问题”解决方案,只是解决了倒计时重叠问题,这样写法,会导致的一些精准度不高。其实很简单,解决代码如下: showCountTime(time){ let self = this; setTimeout(function(){ let lefttimeSec = time - new Date().getTime(); lefttimeSec -= 1000; self.endtimestr = ‘距离拼单结束还有’ + self.dateformat(lefttimeSec); self.$apply(); self.showCountTime(time); },1000); }注意,这里用了setTimeout,要tab页面,运用onHide周期进行clearTimeout清空, 在非tab页面,运用onUload()周期 进行clearTimeout清空定时器。这步必须要做,就不多说了,要不还是会出现上面说的“小程序倒计时重叠抖动问题”问题。用了上面代码,补失的精准度不足。小心的测试同学会发现触摸屏幕导致的突跳,突慢问题,甚至停止!于是各种寻思,去找了拼多多小程序,京东购物小程序各种对比。 结论是拼多多存在和我一样的问题,京东购物小程序的倒计时没这样的问题,给个赞!出现问题环境描述:小程序框架:wepy : “^1.7.2"测试机型:红米3自身思路是wepy脏检查在触摸(滚动)屏幕下引起性能占用导致的一些效率不足问题,做了进一步测试,还是用红米3机型,抛掉组件,抛掉data,只保留data,做一个简单的渲染,将页面高度固定,让屏幕可以上下滑动,代码如下:<style> .content { height: 2000rpx; border: 1rpx solid red; } .child { height: 500rpx; }</style><template> <view class=“content”> <view class=“child”></view> {{endtimestr}} </view></template><script> import wepy from ‘wepy’; export default class test extends wepy.page { data = { endtimestr: ’’ } showCountTime(time) { let self = this; setTimeout(function() { let lefttimeSec = time - new Date().getTime(); lefttimeSec -= 1000; self.endtimestr = ‘距离拼单结束还有’ + self.dateformat(lefttimeSec); self.$apply(); self.showCountTime(time); }, 1000); } dateformat = (micro_second) => { // 总秒数 var second = Math.floor(micro_second / 1000); // 天数 var day = Math.floor(second / 3600 / 24); // 小时 var hr = Math.floor(second / 3600 % 24); // 分钟 var min = Math.floor(second / 60 % 60); // 秒 var sec = Math.floor(second % 60); hr = hr < 10 ? ‘0’ + hr : hr; min = min < 10 ? ‘0’ + min : min; sec = sec < 10 ? ‘0’ + sec : sec; if (day > 0) { return day + " 天” + ’ ’ + hr + “:” + min + “:” + sec; } else { return hr + “:” + min + “:” + sec; } } onLoad() { //api模拟得到time this.showCountTime(1545899950167); } }</script>结论是: 倒计时在触摸(滚动)情况下正常了!!!那也表明wepy的脏检查存在一些性能的不足呀,希望未来wepy有改进! ...

December 27, 2018 · 2 min · jiezi

个人申请官方微信支付接口,即时到账!还支持个人小程序支付!附支付demo

我们知道要想使用微信支付,要具备以下条件1、申请服务号2、要有企业资料(营业执照,对公账户)3、微信认证(300元/年)这样才能申请到微信支付的,对于个人开发者,根本就是一个大门槛,为了微信支付而去注册一家公司,有点成本高了,那么个人可以用微信支付吗?在这之前不可以,现在可以了!因为微信官方有一个叫做微信服务商,有能力的企业可以申请成为微信官方签约的服务商,服务商签约后,就可以获得开发权限。所以有一家公司:北京顶风科技有限公司,他成为了服务商,然后开发了PAYJS,用于开放接口给个人开发者接入官方微信支付!北京顶风科技有限公司是微信支付官方签约的全国服务商,所以我们可以利用PAYJS的接口进行开发即可。而且申请这个接口只需要个人身份证!PAYJS官网:https://payjs.cn/ref/DNXBJDPAYJS的优点:1、即时到账2、微信官方接口3、安全稳定4、提供清晰易懂的开发文档和DEMO下面看我用PAYJS的接口开发的一个扫码支付:http://liketube.cn/payjs/扫码后,就是这样的收款方显示商户名称。扫码支付成功后,就是这样的微信支付商户助手是微信支付官方的收款助手,也就是你的每一笔收款,微信支付商户助手就会收到通知的,而且是即时到账,第二天统一由财务通结算,是财付通结算,财付通是腾讯的,所以大家不用担心金额安全问题。那么PAYJS有微信支付的能力吗?以上就是能力,可以做扫码支付,例如我上面的demo,还可以做jsapi支付,也就是在微信里面访问的页面,点击按钮发起支付的方式,还有一个能力就是支持个人微信小程序的,个人微信小程序无需认证也可以使用PAYJS支付接口实现。JSAPI微信内置浏览器支付:需要在微信内置浏览器打开链接。PAYJS小程序支付:还有网友们提供的DEMO和成品OK介绍到这里,牛不牛逼自己玩玩就知道了:https://payjs.cn/ref/DNXBJD

December 27, 2018 · 1 min · jiezi

小程序实战踩坑之B2B商城项目总结

坑一:支付完成页面,点击“完成” 按钮会触发返回的页面的onShow()生命周期 (秘坑)如下图,原以为是微信的页面,不会影响到小程序,实际情况下是会触发返回的页面的onShow()的。触发bug业务场景详细描述:因为业务需要,我会在下订单页面触发 onShow(),在onShow里面会有调取查询购物车的商品数据,如果商品数据不存在,就会跳转到首页。当用户支付完成后,返回下订单页面,触发onShow生命周期,导致调取查询购物车api,因为已经创建订单去支付了,所以购车车的商品数据就在后台不存在了,所以在 跳过去支付成功页面的那一刻,页面闪了下,异步api在执行,又立刻跳回页面首页。解决方法: self.cancelOnShow = true; self.$apply(); wx.requestPayment({ ’timeStamp’: d.timeStamp, ’nonceStr’: d.nonceStr, ‘package’: d.package, ‘signType’: d.signType, ‘paySign’: d.paySign, ‘success’: function(res) { console.log(res, ‘微信支付成功返回’); wx.setStorageSync(“isSucPay”, true); wx.setStorageSync(“orderCode”, orderCode); wx.navigateTo({ url: ‘./paymentnote’, complete:function(){ self.cancelOnShow = false; } }) }, ‘fail’: function(res) { console.log(res, ‘微信支付失败返回’); wx.setStorageSync(“isSucPay”, false); wx.setStorageSync(“repayParams”,{amount:balance,invoiceid:orderCode}); wx.navigateTo({ url: ‘./paymentnote’, complete:function(){ self.cancelOnShow = false; } }) } });onsShow(){ if(!this.cancelOnShow) { //业务代码****** }}主要是在调取支付前,通过设置 cancelOnShow 为true,然后在跳转 支付成功(失败)页面的complete回调函数,将设置cancelOnShow为false,不影响其他下次支付。有人问,将self.cancelOnShow = false;放在wx.requestPayment的 success 回调函数,在wx.navigateTo跳转之前,是否可行?我自身测试过,是不行的,原因自身体会去。其次,在onShow生命周期函数,要根据cancelOnShow来判断执行业务代码。坑二:多个倒计时重碟问题问题描述: 在一个页面如果用倒计时,如果切换到其他页面返回,会看到多个倒计时在错误重碟显示。解决代码如下: /** * 清除interval * @param that / clearTimeInterval: function (that) { var interval = that.data.interval; clearInterval(interval) }, /* * 生命周期函数–监听页面卸载 * 退出本页面时停止计时器 / onUnload:function () { var that = this; that.clearTimeInterval(that) }, /* * 生命周期函数–监听页面隐藏 * 在后台运行时停止计时器 */ onHide:function () { var that = this; that.clearTimeInterval(that) }在tab页面,运用 onHide 周期 进行 clearTimeInterval , 在 非tab页面,运用onUload() 周期 进行 clearTimeInterval。坑三:swiper的swiper-item变动,导致显示异常swiper 要根据地区选择不同的内容(swiper-item)播放动画,当切换 地区的时候,我的 swiper-item 个数也在变化, 但是原 swiper 的 current 还是之前的,比如切换2张变1张数据的时候,就会 导致swiper 不显示。解决方法和代码:坑四:微信二次授权无法再次获取授权问题获取个人地址授权,点击取消,再去获取,无反应,这个不算bug,但是可以总结下,这块是属于 微信二次授权问题,百度也可以找到相关,代码如下:通过 wx.openSetting 打开授权过的 权利,再次打开,在回调中,在执行你想要的业务逻辑即可。坑五:倒计时显示卡顿倒计时在触摸状态拖住不放的情况下,会发现有倒计时停止了,也就是所谓的卡顿,放开,或者随便动下页面又正常(而且时间恢复得特别快),暂时未找到解决方案,有知悉的同学麻烦告知。下面是我组队员志新同学总结的一些UI展示的坑位:坑六:button标签需清除默认的样式小程序button标签默认的样式不符合我们的设计稿,会出现一些比较丑的border,background等。去除button border线条button::after { border: 0;}去除button背景色button{background:none !important;}坑七:行高不够被截头小程序的标签貌似没有写 默认行高是多少。不过根据截图文本可以发现文本的蓝色底色范围框会比文本内容高一些。如果担心这些蓝色的范围影响了一些设计稿的边距 ,想要收一收它的占位间隙,那么可以将视图内的文本行高设置为 1.1~1.3之间。为什么我不推荐直接设置为行高 1 呢? 因为行高为1的话,在部分安卓机器上面有坑,会出现文字 头部 一些 笔画被 截取没了,出现貌似被砍头的效果!view,text{ line-height:(1.1 或者 1.3 ) 可以适当的收一收它默认行高的一些间隙;}被截头就如下面的图片:坑八:微信小程序通过background-image设置背景:只支持线上图片和base64图片,不支持本地图片坑九:小程序input需要调整聚焦的时候输入框和键盘的距离,增加体验感!cursor-spacingNumber 指定光标与键盘的距离,单位px(2.4.0起支持rpx)。取 input 距离底部的距离和 cursor-spacing 指定的距离的最小值作为光标与键盘的距离<input type=“text” cursor-spacing=“70” placeholder=“请输入具体的营业地址”>坑十:textarea层级穿透(独家秘坑)小程序的textarea 里 placeholder提示不知道为什么,存在一个非常高的层级,平时布局放着还好,要是和一个自己写的弹出层展示,就会发现 textarea的层级比你自己写的弹出层层级还要高,导致结构穿透,紊乱。不管你自己定义的弹出层层级有多高,textarea依然会把它穿透!解决办法:障眼法,在textarea同级在写一个view 仿textarea的样式,当你的弹出层 弹出的时候,把这个真实的 textarea先hidden起来,(注意不要用 wx:if因为display:none会把之前输入的备注内容消息又清没了)。然后把那个高仿的textarea显示出来。当弹出层消失的时候,就把高仿的textarea隐藏,真实的textarea显示出来。 ...

December 24, 2018 · 1 min · jiezi

微信小程序获得微信头像和昵称

{ wx.login({ success: res => { if (res.authSetting && res.authSetting[‘scope.userInfo’]) { // 已经授权,可以直接调用 getUserInfo 获取头像昵称 wx.getUserInfo({ success: function (data) { console.log(data.userInfo); // { // avatarUrl: ‘微信头像img文件path’ // nickname: ‘微信昵称’ // } } }); } else { this.showShouquan = true; // 打开模态框进行授权 } } });}<template> <div> <!– 这里采用vantui进行弹框,只不过把弹出框的button类型设置成了"getUserInfo",原理就是下面这个button <button wx:if="{{showShouquan}}" open-type=“getUserInfo” bindgetuserinfo=“getUserInfoFromWx”>授权登录</button> –> <van-dialog use-slot async-close :show=“showShouquan” show-cancel-button confirm-button-open-type=“getUserInfo” @close=“cancelShouquan” @getuserinfo=“getUserInfoFromWx”> <h4 class=“wx-shouquan-title”>微信授权</h4> <p class=“wx-shouquan-content”> xxxxxxx小程序将获得您的微信头像、昵称等公开消息 </p> </van-dialog> <div></template> getUserInfoFromWx (e) { this.$store.state.user_userinfo = e.mp.detail.userInfo; this.showShouquan = false; }, cancelShouquan () { this.showShouquan = false; }, ...

December 24, 2018 · 1 min · jiezi

Koa2 + Mongo + 爬虫 搭建 小说微信小程序(本地开发篇)

前言:根据慕课网 Koa2 实现电影微信公众号前后端开发 学习后的改造由于上下班期间会看会小说,但是无奈广告太多,还要收费,于是结合课程,进行开发,并上传到自己的微信小程序。github大致的思路:1.连接数据库2.跑定时任务,进行数据库的更新3.开启接口服务4.微信小程序接口调用1.连接数据库连接本地的mongodb数据库const mongoose = require(‘mongoose’)var db = ‘mongodb://localhost/story-bookShelf’exports.connect = () => { let maxConnectTimes = 0 return new Promise((resolve, reject) => { if (process.env.NODE_ENV !== ‘production’) { mongoose.set(‘debug’, false) } mongoose.connect(db) mongoose.connection.on(‘disconnected’, () => { maxConnectTimes++ if (maxConnectTimes < 5) { mongoose.connect(db) } else { throw new Error(‘数据库挂了吧,快去修吧’) } }) mongoose.connection.on(’error’, err => { console.log(err) maxConnectTimes++ if (maxConnectTimes < 5) { mongoose.connect(db) } else { throw new Error(‘数据库挂了吧,快去修吧’) } }) mongoose.connection.once(‘open’, () => { resolve() console.log(‘MongoDB Connected successfully!’) }) })}然后初始化定义好的Schemaconst mongoose = require(‘mongoose’)const Schema = mongoose.Schemaconst bookSchema = new Schema({ name: { type: String }, bookId: { unique: true, type: Number }})……mongoose.model(‘Book’, bookSchema)2.跑定时任务,进行数据库的更新这一步骤主要是在定时进行数据库小说章节的更新,用的是 node-schedule进行定时跑任务。小说章节数是否增加,没增加不用进行爬取。同时在爬取的时候需要提前前5章爬取,避免一些作者为了占坑,提前写的预告。每一本小说就开一个子进程child_process去跑,将数据存储到mongo, 同时存储子进程对后续有用。定时跑任务时候会遇到上一条任务还在跑,所以在每一次跑之前都清空一遍储存的子进程,将子进程杀掉。章节任务// chapter.js const cp = require(‘child_process’)const { resolve } = require(‘path’)const mongoose = require(‘mongoose’)const { childProcessStore } = require(’../lib/child_process_store’) // 全局存储子进程/** * * @param {书本ID} bookId * @param {从哪里开始查找} startNum */exports.taskChapter = async(bookId, startNum = 0) => { const Chapter = mongoose.model(‘Chapter’) const script = resolve(__dirname, ‘../crawler/chapter.js’) // 真正执行爬虫任务模块 const child = cp.fork(script, []) // 开启IPC通道,传递数据 let invoked = false // 这里等子进程将数据传回来,然后存储到mongo中(具体爬取看下一段代码) child.on(‘message’, async data => { // 先找一下是否有数据了 let chapterData = await Chapter.findOne({ chapterId: data.chapterId }) // 需要将拿到的章节与存储的章节做对比 防止作者占坑 if (!chapterData) { chapterData = new Chapter(data) await chapterData.save() return } // 进行字数对比 相差50字符 if ((data.content.length - 50 >= 0) && (data.content.length - 50 > chapterData.content.length)) { Chapter.updateOne ( { chapterId: +data.chapterId }, { content : data.content } ); } }) child.send({ // 发送给子进程进行爬取 bookId, // 哪本小说 startNum // 从哪个章节开始爬 }) // 存储所有章节的爬取 用于跑进程删除子进程 childProcessStore.set(‘chapter’, child)} 真正开启爬虫,用的是 puppeteer,谷歌内核的爬取,功能很强大。分两步:1.爬对应小说的章节目录,拿到章节数组2.根据传进来的startNum 进行章节startNum 的往后爬取// crawler/chapter.jsconst puppeteer = require(‘puppeteer’)let url = http://www.mytxt.cc/read/ // 目标网址const sleep = time => new Promise(resolve => { setTimeout(resolve, time)})process.on(‘message’, async book => { url = ${url}${book.bookId}/ console.log(‘Start visit the target page — chapter’, url) // 找到对应的小说,拿到具体的章节数组 const browser = await puppeteer.launch({ args: [’–no-sandbox’], dumpio: false }).catch(err => { console.log(‘browser–error:’, err) browser.close }) const page = await browser.newPage() await page.goto(url, { waitUntil: ’networkidle2’ }) await sleep(3000) await page.waitForSelector(’.story_list_m62topxs’) // 找到具体字段的class let result = await page.evaluate((book) => { let list = document.querySelectorAll(’.cp_dd_m62topxs li’) let reg = new RegExp(${book.bookId}\/(\\S*).html) let chapter = Array.from(list).map((item, index) => { return { title: item.innerText, chapterId: item.innerHTML.match(reg)[1] } }) return chapter }, book) // 截取从哪里开始爬章节 let tempResult = result.slice(book.startNum, result.length) for (let i = 0; i < tempResult.length; i++) { let chapterId = tempResult[i].chapterId console.log(‘开始爬url:’, ${url}${chapterId}.html) await page.goto(${url}${chapterId}.html, { waitUntil: ’networkidle2’ }) await sleep(2000) const content = await page.evaluate(() => { return document.querySelectorAll(’.detail_con_m62topxs p’)[0].innerText }) tempResult[i].content = content tempResult[i].bookId = book.bookId process.send(tempResult[i]) // 通过IPC将数据传回去,触发child.on(‘message’) } browser.close() process.exit(0)})3.开启接口做的任务主要是,拿mongodb的数据,同时通过koa-router发布路由先定义好路由装饰器,方便后续使用 具体看 decorator.js底层拿到数据库的数据service/book.js // 拿到数据库存储的值const Chapter = mongoose.model(‘Chapter’)// 获取具体的章节内容export const getDetailChapter = async (data) => { const chapter = await Chapter.findOne({ chapterId: data.chapterId, bookId: data.bookId }, { content: 1, title: 1, chapterId: 1 }) // console.log(‘getDetailChapter::’, chapter) return chapter}…路由定义 后续的接口就是 ‘/api/book/chapter’@controller(’/api/book’)export class bookController { @post(’/chapter’) async getDetailChapter (ctx, next) { const { chapterId, bookId } = ctx.request.body.data const list = await getDetailChapter({ chapterId, bookId }) ctx.body = { success: true, data: list } }}4.微信小程序使用wepy进行开发,功能也是很简单,具体开发可以参见小程序代码,这里不做详细讲述。支持记录每一章的进度,与全局设置。后续可以自己发挥。在目标网站找到小说的Id之后就能进行查找了。接下来讲解部署到服务器细节。最后,在这里特别感谢@汪江 江哥的帮助,我前后琢磨了两个月,而他就用了三天,谢谢你不厌其烦的帮助,与你共事很开心。以上只是我的不成熟的技术,欢迎各位留言指教。 ...

December 24, 2018 · 3 min · jiezi

微信小程序获得openid免密登录

{ wx.login({ success: res => { let d = { appid: ‘wx111111111111’, // 从微信公众平台开发设置中获取 secret: ‘sec2222222222’ // 从微信公众平台开发设置中获取 }; // 微信官方的获取openid的接口 var wxLoginUrl = ‘https://api.weixin.qq.com/sns/jscode2session?appid=' + d.appid +’&secret=’ + d.secret + ‘&js_code=’ + res.code + ‘&grant_type=authorization_code’; wx.request({ url: wxLoginUrl, data: {}, method: ‘GET’, success: res => { let openid = res.data.openid; this.judgeIsWxlogin(openid); // 向后端发送openid判断是否可以直接用该微信号登陆 } }); } });}从微信公众平台开发设置中获取appid和secret:

December 23, 2018 · 1 min · jiezi

小程序--人脸识别功能(百度ai)

文档中心:https://ai.baidu.com/docs#/Begin/a2bbf4b2接入流程1. 按照文档获取AppID、API Key、Secret Key,进行Access Token(用户身份验证和授权的凭证)的生成const getBaiduToken = function () { return new Promise((resolve, reject) => { //自行获取APIKey、SecretKey const apiKey = APIKey; const secKey = SecretKey; const tokenUrl = https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&amp;client_id=${apiKey}&amp;client_secret=${secKey}; wx.request({ url: tokenUrl, method: ‘POST’, dataType: “json”, header: { ‘content-type’: ‘application/json; charset=UTF-8’ }, success: function (res) { resolve(res); }, fail: function (res) { wx.hideLoading(); wx.showToast({ title: ‘网络错误,请重试!’, icon: ’none’, duration: 2000 }) reject(res); }, complete: function (res) { resolve(res); } }) })}2. 选择人脸识别–>人脸检测,人脸识别接口分为V2和V3两个版本,确认在百度云后台获得的是V2还是v3版本接口权限。//封装识别方法const getImgIdentify = function(tokenUrl, data){ return new Promise((resolve, reject) => { const detectUrl = https://aip.baidubce.com/rest/2.0/face/v3/detect?access_token=${tokenUrl}; wx.request({ url: detectUrl, data: data, method: ‘POST’, dataType: “json”, header: { ‘content-type’: ‘Content-Type:application/json; charset=UTF-8’ }, success: function (res) { resolve(res); }, fail: function (res) { wx.hideLoading(); wx.showToast({ title: ‘网络错误,请重试!’, icon: ’none’, duration: 2000 }) reject(res); }, complete: function (res) { resolve(res); } }) })}3. 调用识别方法getBaiduToken().then((res) => { let token = res.data.access_token; let data = { “image”: self.data.img, “image_type”:“URL”, “face_field”:“ge,beauty,expression,face_shape,gender,glasses,landmark,race,quality,eye_status,emotion,face_type” } util.getImgIdentify(token, data).then((res)=>{ //百度接口返回的结果 let score = parseInt(res.data.result.face_list[0].beauty); self.setData({ score: score, }) })})4. 结果如下:哼~一点都不准原文地址:https://github.com/liujianxi/… ...

December 22, 2018 · 1 min · jiezi

记一次微信小程序在安卓的白屏问题

在做小程序的时候,做到了一个限时商品售卖,用到了倒计时,因为这个原因导致了安卓手机上使用小程序时,将小程序放入后台运行一段时间后,再次进入小程序后出现了页面白屏或者点击事件失效的情况,这里记录下1.相关代码文件我这里是使用了自定义组件的形式来渲染的外部的引用的自定义组件的wxml文件/* limitCommodity是一个数组,返回的是商品对象,包含商品价格、商品结束时间、商品图片等 /<block wx:for="{{limitCommodity}}" wx:key="{{item.id}}"> <commodityItem class=“specialContent” goods="{{item}}" /></block>自定义组件的js文件Component({ properties: { goods: Object }, data: { }, timer: null, / 在组件实例进入页面节点树时执行,开始定时器 / attached: function() { if(this.timer) { clearInterval(this.timer); } this.filterTime(); let that = this; this.timer = setInterval(function () { that.filterTime(); }, 1000) }, / 在组件实例被从页面节点树移除时执行,将定时器清除 / detached: function() { clearInterval(this.timer); this.timer = null; }, methods: { / 用于将时间戳转换成自定义的时间格式 / filterTime() { let totalTime = new Date(parseInt(this.data.goods.endtime) * 1000) - new Date(); let days = parseInt(totalTime / 1000 / 60 / 60 / 24, 10); let hours = parseInt(totalTime / 1000 / 60 / 60 % 24, 10); let minutes = parseInt(totalTime / 1000 / 60 % 60, 10); let seconds = parseInt(totalTime / 1000 % 60, 10); let day = days >= 10 ? days : ‘0’ + days; day = day == 0 ? ’’ : day + ‘天’; let hour = hours >= 10 ? hours : ‘0’ + hours; let minute = minutes >= 10 ? minutes : ‘0’ + minutes; let second = seconds >= 10 ? seconds : ‘0’ + seconds; this.setData({ limitTime: day + hour + “:” + minute + “:” + second }) }, }})2.引起的原因因为在外部引入自定义的组件时,直接就是调用了定时器并且进行了setData操作,这就导致了当在外部引用这个组件时,如果传入的商品数组长度较大时,定时器增多的同时,setData操作也不断的增多setData多了就会导致内存占用多3.改进方法改进方法就是减少setData操作可以再自定义一个组件,用于将整个数组传入然后对商品数组里的时间先进行计算改进后的js文件Component({ properties: { limitCommodity:Array }, data: { }, timeOut:null, / 在组件实例进入页面节点树时执行 / attached(){ this.calculate(); }, / 在组件实例被从页面节点树移除时执行,将定时器清除 */ detached(){ clearTimeout(this.timeOut); this.timeOut = null; }, methods: { filterTime(endtime) { let totalTime = new Date(parseInt(endtime) * 1000) - new Date(); let days = parseInt(totalTime / 1000 / 60 / 60 / 24, 10); let hours = parseInt(totalTime / 1000 / 60 / 60 % 24, 10); let minutes = parseInt(totalTime / 1000 / 60 % 60, 10); let seconds = parseInt(totalTime / 1000 % 60, 10); let day = days >= 10 ? days : ‘0’ + days; day = day == 0 ? ’’ : day + ‘天’; let hour = hours >= 10 ? hours : ‘0’ + hours; let minute = minutes >= 10 ? minutes : ‘0’ + minutes; let second = seconds >= 10 ? seconds : ‘0’ + seconds; return day + hour + “:” + minute + “:” + second }, calculate(){ let limitCommodity = this.data.limitCommodity; for (let i = 0; i < limitCommodity.length;i++){ limitCommodity[i][’endtime_date’] = this.filterTime(limitCommodity[i][’endtime’]) } this.setData({ limitCommodity }) this.timeOut = setTimeout(()=>{ this.calculate(); },1000); } }})改进就是计算时间后再返回时间,而setData的是整个商品列表数组,这样就减少了setData次数正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)实现单行及多行文字超出后加省略号微信小程序之购物车和父子组件传值及calc的注意事项 ...

December 22, 2018 · 2 min · jiezi

用微信小程序云开发做一个错误日志

为什么要用云开发做错误日志:我司没有测试,所以产品上线的话比较多不确定性云开发业务并不稳定,且有限制,所以不建议直接用做整个小程序的后台开发。做错误日志并不会影响小程序的流程出bug后,难定位问题,尤其是线上错误如果叫后端小伙伴给接口记录错误,总是不方便,还是自己动手丰衣足食尝鲜基于以上的原因,在小程序云开发刚出来没多久,我就开始着手在我的小程序上尝试构建一个错误日志:一般用一门新技术的第一步,你需要先浏览一下小程序云开发的官方文档:https://developers.weixin.qq….初始化环境然后你需要一个小程序,一个小程序开发工具:新建一个空白的小程序后,点击左上角的云开发按钮,初始化一个云开发的环境我这里给它取名叫error-storage。当然我是因为之后我的小程序也不太可能会用到云开发做后台,所以可以浪费一个环境命名。如果你是要用云开发做后台业务的话。那么还是按照微信推荐,一个做测试一个做正式,命名也尽量规范一点。然后它的环境限制也是我们目前不拿它当主要后端脚本的主要原因之一。ok,点击确定就创建了一个云开发后台了。然后我们回到代码。我们需要写后端脚本记录错误信息,那么根据云开发的文档,我们需要修改project.config.json给他配置一下小程序代码放在哪里,后端代码放在哪里。以下是我的配置:我把小程序文件都放在了根目录下的client文件夹内,而云函数的文件则都放在了根目录的server文件夹的cloudFunctions里那么现在我们的目录结构是这样的。我们需要手动移动一下我们的小程序代码。注意这里project.config.json是在根目录现在先添加一个数据库集合,打开云开发控制台,点击数据库。添加一个叫errors的集合编写接收错误的云函数然后我们先开始写接收错误的云函数用微信开发工具打开代码,然后点击目录树上面的cloudFunctiions目录,新建一个云函数,我们将它取名叫做errorHandler然后写上我们的代码逻辑// 云函数入口文件const cloud = require(‘wx-server-sdk’);cloud.init();const db = cloud.database();const errorCllection = db.collection(’errors’);function addError(data) { return errorCllection.add({ data: { …data, createTime: Date.now() } });}// 云函数入口函数exports.main = async event => { event.openid = event.userInfo.openId; delete event.userInfo; await addError(event); return true;};这面这段代码很简单就是将小程序端传过来的错误写进数据库里面,错误信息是什么由小程序端决定,它仅仅只是将所有数据丢进数据库去而已保存字后再次右键cloudFunctions选择上传并部署。那么我们的服务端就基本搞定了。(好简单啊)写小程序端的错误处理函数首先,根据教程,我们要在app.js里面做云能力初始化在app.js的第一行添加代码try { wx.cloud.init({ // 云开发初始化 env: ‘云环境id’, traceUser: true });} catch(err) {}给它加上catch是因为防止出现一些错误导致app.js运行失败,那简直是灾难吧!这里的env是你的云环境id,那么在哪里拿呢,在这里然后我们在client/utils文件夹里面添加一个处理错误的模块吧名字就叫 error.jsconst { config } = require(’./config.js’);global.onError = function (message, showModal = true) { return function (error) { wx.hideLoading(); if (showModal) { wx.showModal({ title: ‘错误’, content: error.msg || message, // 这里的error.msg是因为与后端约定如果有什么错误,则带一个msg的描述 // 而message则是传入进来备用的错误信息 confirmText: ‘返回首页’, cancelText: ‘继续操作’, success: res => { if (res.confirm) { // 重新加载首页 wx.switchTab({ url: ‘/pages/index/index’ }); } else { // 取消就不管了 } } }); } // 上传到小程序云数据库 try { let userInfo = getApp().globalData.userInfo, systemInfo = wx.getSystemInfoSync(), page = getCurrentPages(); // 只有不在开发工具上触发的才上报 if (systemInfo.platform !== ‘devtools’) { wx.getNetworkType({ success: res => { wx.cloud.callFunction({ name: ’errorHandler’, data: { username: userInfo.nickName, uid: userInfo.id, clientType: systemInfo.model, systemInfo: JSON.stringify(systemInfo), pageRoute: page[page.length - 1].route, message: error.msg || message, version: config.version, networkType: res.networkType, errorTime: new Date(), error: typeof error === ‘object’ ? JSON.stringify(error) : String(error) } }); } }); } } catch(err) {} console.error(‘程序发生错误:\n’, ‘时间:\n’, new Date(), ‘\n错误信息:\n’, message, ‘\nvvvvvvvvvvvvvv\n’, error); };};如上,除了错误信息之外,还储存了系统信息,网络类型,最顶部的页面路由,错误时间,用户信息,版本号等(这里我加了个config.js用以储存版本号,版本描述等信息)用try catch包起来也是为了防止触发错误导致死循环并且为了方便使用,我将其直接挂载到global。不喜欢全局变量的童鞋可以选择export出去再引用并且在触发错误的时候弹出一个提示框提示错误了。错误处理函数的使用那么接下来就是对这个方法的使用了,首先我们要监听app.js的onError方法,小程序页面逻辑出现错误都会到这个方法内request(’./utils/error.js’); // 别忘了引入error.jsApp({ onError: global.onError(‘程序发生错误’) // 这里返回接受错误的函数的闭包,并且传入的’程序发生错误’则是未知错误发生时的提示语});而其他的应用基本是应用到在promise的错误监听上,由于promise中的错误不会被app.js的onError接收到,所以我们需要在每个promise的catch中使用global.onError这也是我为什么将onError挂载global的原因,因为基本每个页面都会用到,所以每个页面去引用的话很麻烦例子:demo.jsPage({ onLoad() { wx.showLoading(); this.requestList() .then(res => { // 做一些事情 }) .catch(global.onError(‘获取列表数据失败!’)); }, requestList() { return new Promise((resolve, reject) => { wx.request({ url: ‘https://mock.com/aaa', success: resolve, fail: reject }); }); }});那么,到现在,基本上已经完成了,愉快的开发,再也不担心出现完全没有头绪的错误啦最后贴一张我收集到的错误吧。但是有些字段我做了调整最后还是要吐槽一下微信本身它自己的bug有点多。实在是很无奈的 ...

December 21, 2018 · 2 min · jiezi

手把手教你迁移微信小程序到 QQ 浏览器

继微信、QQ 之后,QQ 浏览器上也可以使用小程序了。12 月 5 日,QQ浏览器小程序宣布,实现与微信小程序打通。QQ 浏览器 Android 版现已上线小程序,在搜索的场景下,小程序嵌入 QQ 浏览器「搜索直达」,作为直接的内容承载。用户在搜索框输入关键词后,相关小程序会在关键词智能推荐列表优先推荐,并直接展示相关内容。墨迹天气、腾讯翻译君等小程序已经成功入驻。除此之外,QQ 浏览器小程序兼容适配了微信小程序,号称「只需三步」开发者即可完成适配工作将微信小程序移植到 QQ 浏览器上运行。知晓程序也在第一时间体验了整个适配过程,接下来将为大家一一讲解其中的细节和关注点。值得一提的是,腾讯官方将 QQ 浏览器(QQ Browser)小程序称为 QB 小程序,听到这个名字想必大家会有种奇妙的感觉。安装并注册 QB 小程序调试工具在你正式上手调试前,我们需要提醒以下三点注意事项:目前开发者工具只支持安卓;小程序的正式名称、图标和简介是用户可见的,并且填写完成后暂时没有办法修改,所以填写一定要谨慎;qbDebugKey 是设备唯一的,每台手机都会生成一个 qbDebugKey。目前 QB 小程序的调试页面和介绍页面还十分简陋,也没有相关的 PC 端开发工具,所以开发者仍需在微信开发者工具上完成小程序的开发,然后适配成 QB 小程序。1. 使用微信扫描二维码进入 QB 小程序调试页面。扫描二维码后,页面可能会提示「调试内核版本过旧」,需按照提示长按识别页面中的二维码下载安装最新版调试内核,安装完成后再重新扫描上方二维码进入。2. 进入调试页面后,需先完成「注册」。 在注册页面中:packageName 是小程序的唯一标识,一旦注册成功,packageName 会在后台与 qbDebugKey 绑定,注册后只有当前设备可以使用这个 packageName 进行登录,如果需要给这个 packageName 绑定其他开发设备,可以在登录后进行添加。开发者昵称是 qbDebugKey 的别名,方便开发者管理开发设备。小程序的正式名称、图标和简介是用户可见的,注册完成后暂时不提供修改方法,请谨慎填写。同时,你需要将 qbDebugKey 添加到微信小程序的 app.json 配置文件里,如下所示:{ “window”: { “navigationBarBackgroundColor”: “#FFF”, “navigationBarTitleText”: “知晓课堂”, “navigationBarTextStyle”: “black”, “qbDebugKey”: [“495f18a64485eeac5e78ccbxxx”, “7e2f29d50e78411b3915128exxx”] },}注意:只有在 app.json 里配置过测试机生成的 qbDebugKey,测试机才能使用 QQ 浏览器小程序调试工具调试该小程序。一台手机对应一个 qbDebugKey。3.如果你完成了注册或已有其他开发者为你添加了开发权限,输入对应小程序 并点击「登录」按钮进入进入开发者管理后台页面。在此页面中,你可以点击「开发者权限管理」添加其它开发设备,需要输入待添加设备的 qbDebugKey 和昵称,添加成功后,新设备就可以使用该 packageName 进行登录了。安装调试版 QQ 浏览器点击「启动 QB 打开小程序」按钮,如果没有下载调试版 QQ 浏览器,这步操作会下载调试版 QQ 浏览器。如果你手机中未安装 QQ 浏览器或安装的版本非正确的调试版本,在点击后会提示「请先下载调试版 QQ 浏览器」,按照提示再次点击按钮即可开始下载安装;安装完成后再次回该页面点击启动按钮即可拉起 QQ 浏览器启动要调试的小程序。调试兼容性在以上两步完成后,接下来我们需要调试兼容性,直到小程序能够跑起来。这里主要注意 QB 小程序和微信小程序的几点区别:QB 小程序的登录态与微信小程序不互通,并且没有 unionId 概念QB 小程序不支持自定义导航栏颜色QB 小程序的 canvas 不支持 measureText, 意味着没有办法在 canvas 上进行文本换行QB 小程序不支持下拉刷新QB 小程序的 intersectionObserver 无法使用QB 小程序不支持模版消息QB 小程序不支持打开跳转其他小程序微信强相关的 API 都不支持这里主要参考QQ浏览器小程序开发者文档。提交预览,在 QQ 浏览器打开处理好了兼容性问题之后,就可以在QQ浏览器中预览小程序了。1.首先在 QB 小程序调试工具中点击「微信扫码」 按钮,扫描微信开发者工具中预览生成的二维码,此时会进入到微信环境下的小程序,先点击右上角圆点退出小程序,返回到 QB 小程序调试工具中。2.然后点击「启动 QB 打开小程序」按钮,在已安装好调试版 QQ 浏览器情况下,会自动跳转到 QQ 浏览器小程序环境,进入后就能看到效果。▲ 在 QQ 浏览器中的预览效果3.预览没问题之后就可以上传一个体验版,输入版本号,上传成功后会有一个 url 返回,复制这个 url 到 QQ 浏览器中就能打开小程序了。4.关于分享问题。QQ 浏览器小程序可以分享到微信朋友圈、微信好友、QQ 好友、QQ 空间,和微信小程序分享不同的是,QQ 浏览器小程序分享是创建一张分享海报,里面有 QQ 浏览器小程序二维码,在安卓手机中长按识别即可自动打开小程序。提交审核并上线体验版测试没问题之后,在 QB 小程序调试工具中点击「包状态管理」,进入到提交包的历史列表,点击需要提审的版本提交审核。审核通过后即可上线发布。本文首发于「知晓云」公众号:https://mp.weixin.qq.com/s/Jo…如果你还想了解 更多小程序开发技巧,快速掌握小程序开发能力。欢迎扫描下方二维码关注「知晓云」,我们会持续为更新与小程序有关的实战教程哦~ ...

December 18, 2018 · 1 min · jiezi

微信小程序Taro开发(1):Taro安装及使用

全局安装 Taro 开发工具 @tarojs/clinpm install -g @tarojs/cli在要创建项目的目录下,创建项目:taro init test以微信小程序为例,创建项目完毕,要运行项目,则如下:npm run dev:weapp当项目在运行时,在项目目录下dist文件中可以看到编译的小程序代码,打开微信小程序开发工具,就可以边开发边在微信小程序开发工具中看到效果打包项目:npm run build:weapptaro不仅支持微信小程序还支持百度小程序,支付宝小程序,H5,react native等等,具体可进入官网了解更多,官网地址:https://nervjs.github.io/taro…

December 18, 2018 · 1 min · jiezi

微信小程序Taro开发(2):生命周期及开发中注意点

生命周期componentWillMount在微信小程序中这一生命周期方法对应页面的onLoad或入口文件app中的onLaunchcomponentDidMount在微信小程序中这一生命周期方法对应页面的onReady或入口文件app中的onLaunch,在 componentWillMount后执行componentDidShow在微信小程序中这一生命周期方法对应 onShowcomponentDidHide在微信小程序中这一生命周期方法对应 onHidecomponentDidCatchError错误监听函数,在微信小程序中这一生命周期方法对应 onErrorcomponentDidNotFound页面不存在监听函数,在微信小程序中这一生命周期方法对应 onPageNotFoundshouldComponentUpdate页面是否需要更新componentWillUpdate页面即将更新componentDidUpdate页面更新完毕componentWillUnmount页面退出,在微信小程序中这一生命周期方法对应 onUnload在小程序中 ,页面还有一些专属的方法成员,如下:1. onPullDownRefresh: 页面相关事件处理函数–监听用户下拉动作2. onReachBottom: 页面上拉触底事件的处理函数3. onShareAppMessage: 用户点击右上角转发4. onPageScroll: 页面滚动触发事件的处理函数5. onTabItemTap: 当前是 tab 页时,点击 tab 时触发6. componentWillPreload: 预加载,只在微信小程序中可用注意1.通常入口文件会包含一个 config 配置项,这里的配置主要参考微信小程序的全局配置而来,在编译成小程序时,这一部分配置将会被抽离成 app.json,而编译成其他端,亦会有其他作用。2.入口文件继承自 Component 组件基类,它同样拥有组件生命周期,但因为入口文件的特殊性,他的生命周期并不完整,如:componentWillMount、componentDidMount、componentDidShow、componentDidHide、componentDidCatchError、componentDidNotFound。3.入口文件需要包含一个 render 方法,一般返回程序的第一个页面,但值得注意的是不要在入口文件中的 render 方法里写逻辑及引用其他页面、组件,因为编译时 render 方法的内容会被直接替换掉,你的逻辑代码不会起作用。4.Taro 支持组件化开发,组件代码可以放在任意位置,不过建议放在 src 下的 components 目录中。一个组件通常包含组件 JS 文件以及组件样式文件,组织方式与页面类似。taro项目目录如下:├── config 配置目录| ├── dev.js 开发时配置| ├── index.js 默认配置| └── prod.js 打包时配置├── src 源码目录| ├── components 公共组件目录| ├── pages 页面文件目录| | ├── index index 页面目录| | | ├── banner 页面 index 私有组件| | | ├── index.js index 页面逻辑| | | └── index.css index 页面样式| ├── utils 公共方法库| ├── app.css 项目总通用样式| └── app.js 项目入口文件└── package.json ...

December 18, 2018 · 1 min · jiezi

微信小程序Taro开发(3):canvas制作钟表

制作钟表分成两部分,一部分是表盘,一部分是时针、分针、秒针的走动,首先,先绘制表盘:// 绘制表盘 getDialClock = () => { const width = this.state.width; const height = this.state.height; const ctx = Taro.createCanvasContext(‘myCanvas’, this.$scope); const R = width/2 - 30;//圆半径 const r = R - 15; //设置坐标轴原点 ctx.translate(width/2, height/2); ctx.save(); // 圆心 ctx.beginPath(); ctx.arc(0, 0, 5, 0, 2 * Math.PI, true); ctx.fill(); ctx.closePath(); // 表盘外圆 ctx.setLineWidth(2); ctx.beginPath(); ctx.arc(0, 0, R, 0, 2 * Math.PI, true); ctx.closePath(); ctx.stroke(); // 表盘刻度(大格) ctx.beginPath(); ctx.setLineWidth(5); for(var i = 0; i < 12; i++) { ctx.beginPath(); ctx.rotate(Math.PI / 6); ctx.moveTo(R, 0); ctx.lineTo(r, 0); ctx.stroke(); } ctx.closePath(); // 表盘刻度(小格) ctx.beginPath(); ctx.setLineWidth(1); for(var i = 0; i < 60; i++) { ctx.beginPath(); ctx.rotate(Math.PI / 30); ctx.moveTo(R, 0); ctx.lineTo(R-10, 0); ctx.stroke(); } ctx.closePath(); // 表盘时刻(数字) ctx.beginPath(); ctx.setFontSize(16)//设置字体样式 // ctx.setTextBaseline(“middle”);//字体上下居中,绘制时间 for(let i = 1; i < 13; i++) { //利用三角函数计算字体坐标表达式 var x = (r-10) * Math.cos(i * Math.PI / 6 - Math.PI/2); var y = (r-10) * Math.sin(i * Math.PI / 6 - Math.PI/2); let sz = i + ‘’; ctx.fillText(sz, x - 5, y + 5, 15); } ctx.closePath(); // 开始绘制 ctx.draw(); }表盘绘制完毕,再绘制时针,分针,秒针的运动,这里需要新建一个组件来专门管理这个时间运动,在组件中,如下:// 绘制 针表 drawTime = () => { const width = this.props.dataRes.width; const height = this.props.dataRes.height; const ctx = Taro.createCanvasContext(’timeId’, this.$scope); const R = width/2 - 30;//圆半径 //设置坐标轴原点 ctx.translate(width/2, height/2); ctx.save(); const t = new Date();//获取当前时间 let h = t.getHours();//获取小时 h = h>12?(h-12):h;//将24小时制转化为12小时制 const m = t.getMinutes();//获取分针 const s = t.getSeconds();//获取秒 //绘制时针 ctx.beginPath(); ctx.setStrokeStyle(‘green’) ctx.setLineWidth(10); ctx.rotate((Math.PI/6)(h+m/60+s/3600)-Math.PI/2); ctx.moveTo(0,0); ctx.lineTo(R-90,0); ctx.stroke(); ctx.restore(); ctx.save(); // 绘制分针 ctx.beginPath(); ctx.setStrokeStyle(‘gold’) ctx.setLineWidth(5); ctx.rotate((Math.PI/30)(m+s/3600)-Math.PI/2); ctx.moveTo(0,0); ctx.lineTo(R-60,0); ctx.stroke(); ctx.restore(); ctx.save(); // 绘制秒针 ctx.beginPath(); ctx.setStrokeStyle(‘red’) ctx.setLineWidth(1); ctx.rotate((Math.PI/30)*s-Math.PI/2); ctx.moveTo(0,0); ctx.lineTo(R-20,0); ctx.stroke(); ctx.restore(); ctx.save(); ctx.draw(); }结果显示:源码地址:https://gitee.com/hope93/canv… ...

December 18, 2018 · 2 min · jiezi

微信小程序黑客马拉松落幕,28小时见证27个小程序从0到1诞生!

2018 年 12 月 16 日下午,由腾讯公司微信事业群主办的「WeGeek 微信小程序黑客马拉松」(WeGeek Hackathon)在北京顺利闭幕。 WeGeek Hackathon 是面向全球小程序开发者、爱好者的黑客马拉松,旨在通过微信小程序平台进行小程序的创新开发,共同建设小程序生态。 WeGeek Hackathon,最酷的Mini Program Creators聚集地本次 WeGeek Hackathon 主题分为「工具、生活服务、教育」三大类目,一经发布即引起了圈内外广泛关注,报名人数在两周内超过 500 人。经过两轮筛选后, 160 名开发者入围并参与最后的角逐。 在这 160 名开发者和爱好者中,有来自一线互联网公司的开发工程师,武汉大学、电子科大等高校学生、以及来自台湾的小程序爱好者。他们或对小程序的使用场景有着深刻的理解,或非常熟练掌握小程序的开发技能,可谓是最酷的 Mini Program Creators 聚集地!28小时!见证27个小程序从0到1的诞生12 月 15 日上午 9 点开始,160 名小程序开发者和爱好者组队,在现场通过28小时的封闭式开发,从 0 到 1 创造出了 27 个全新产品并参与评选。 这 27 个小程序,涉及到教育、交通、出行、工具、大数据等多个行业或领域;利用到了扫码、分享、拍照、地图、AI、语音、AR 等众多接口,解决了幼儿园管理、口语学习、线上拼车、社团管理、灵感记录、活动报名、健康管理、抽奖、打卡等多个真实场景下的问题。小小幼教小程序解决了幼儿园老师在学生签到,给家长分享小朋友照片、布置作业等真实场景下的问题,甚至在小朋友走失的场景下提供了一定的帮助功能。AI 有声小程序基于深度学习引擎,将 AI 与 AR 技术融合,提供了便捷的实时手语翻译功能。不仅为普通人学习手语提供了更好的指导,更为 3000 万聋哑人更好地融入社会提供帮助。换听小程序利用 AI 功能,将文字内容转换为语音内容,并为使用者建立待听列表,将获取内容的方式由“看”换为“听”,解放使用者的双眼。其队长坦言,完成这款功能完善的小程序开发,真正用的时间不过 10 个小时。英语口语助手小程序以电影配音的方式,提供了低门槛进行口语练习的工具,解决了师生互动以及内容沉淀的问题。除此之外,时锦小程序还设定了非常动人的 slogan——“每个人都是一本书”,还有的小程序甚至规划出了完整的商业模式。小小幼教小程序最终摘下桂冠在 28 小时的竞速开发后,各团队将作品进行了展示。通过评委打分和团队互评,小小幼教小程序最终摘下桂冠! 获得二等奖和三等奖的分别是 AI 有声小程序和英语口语助手小程序。轻芒合伙人、小程序资深观察员阿禅表示,小程序的「小」和各种被吐槽的「规则」看起来是「讨厌的」限制,但事实上反而给开发者提供了冷静下来先思考解决什么问题、如何解决问题,再去想其他事情。他还提到:“优秀的产品是从解决真实场景下的问题为出发点,不是为了完成任务而设置一些很奇怪的应用场景。在 WeGeek Hackathon 上很多参赛者把思考放在解决问题本身,回到了产品的本质,这是非常好的。”爱范儿 CTO 何世友表示,非常兴奋能亲历小程序在创意落地上的“执行力”,科技的进步往往不是一蹴而就,更多时候正是以降低一点门槛这种方式逐渐累积成质变。SegmentFault思否 CTO 祁宁表示,微信小程序为开发者提供了天然的流量入口与传播渠道,可以让开发人员专注于产品建设,丰富的接口和 SDK 支持降低了上手难度。在 WeGeek Hackathon 上开发者展现了强大执行力与创意,更可贵的是一些产品中还体现了人文关怀。他个人特别欣赏敢于想象的精神,希望能有更多的开发者从幕后走到台前,在小程序上展现大理想。活动最后,阿禅还对参赛作品进行了逐一点评,并且从商业价值方面给予了专业的指导。很多参赛者表示获益良多。WeGeek Hackathon助力更好的小程序生态此次 WeGeek Hackathon 汇聚了五湖四海的开发者,各行各业的小程序爱好者通过小程序将自己的想法变成了现实。有参赛者表示“我的想法落地了,虽然没有获奖,但是我感觉非常值得”。有的参赛者在现场临时组队,短时间的思想碰撞激发出了他们的灵感火花,两天过去,他们成为了默契的队友,比赛之后还将会继续将小程序开发进行到底。WeGeek Hackathon 将会继续助力更好的小程序生态,为小程序的开发者和爱好者们提供一个展示自己的舞台。 ...

December 17, 2018 · 1 min · jiezi

微信小程序计算用户离商家的距离(利用经纬度求距)

前言最近在帮朋友(商家)写小程序,所以看了不少关于小程序的知识,总结一下计算距离这条线。思路一共有两种方法,各有利弊:1.利用小程序的wx.getLocation 方法得到用户的经纬度,然后用已知的商家的经纬进行计算;2.利用腾讯地图位置服务calculateDistance直接计算先熟悉下两个单词:**longitude:经度;latitude:纬度;**下边是两种方法的具体实现一、获取用户的位置信息,再进行计算(wx.getLocation)1.小程序提供了获取用户位置信息的api,所以我们能直接获取到经纬度;2.在百度拾取坐标系统,获取商家的具体经纬度(例:北京故宫116.403414(经度),39.924091(纬度)。)3.利用公式进行两点的经纬度计算代码:Page({ data:{ }, onLoad: function() { var _this = this; _this.findXy() //查询用户与商家的距离 }, findXy() { //获取用户的经纬度 var _this = this wx.getLocation({ type: ‘wgs84’, success(res) { _this.getDistance(res.latitude, res.longitude, 39.924091,116.403414) } }) }, Rad: function(d) { //根据经纬度判断距离 return d * Math.PI / 180.0; }, getDistance: function(lat1, lng1, lat2, lng2) { // lat1用户的纬度 // lng1用户的经度 // lat2商家的纬度 // lng2商家的经度 var radLat1 = this.Rad(lat1); var radLat2 = this.Rad(lat2); var a = radLat1 - radLat2; var b = this.Rad(lng1) - this.Rad(lng2); var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); s = s * 6378.137; s = Math.round(s * 10000) / 10000; s = s.toFixed(2) + ‘公里’ //保留两位小数 console.log(‘经纬度计算的距离:’ + s) return s } )}二、利用腾讯地图的位置服务1.这里配置的地方就比较多一点了,先到腾讯位置服务注册登录,申请key、引入依赖。下图的第三步配置是要在小程序的后台那里设置,记得不要找错地方了。如图:2.配置完成了之后,小程序重新编译一下3.我们看下腾讯的api,是怎么求两点距离的腾讯位置-两点求距4.读完可知,我们只需要商家的经纬度即可,我们在小程序里实验一下// 引入SDK核心类var QQMapWX = require(’../../utils/qqmap-wx-jssdk.js’);Page({ onLoad: function() { var _this = this; _this.findShop() //查询用户与商家的距离 }, findShop() { //拿到商家的地理位置,用到了腾讯地图的api // 实例化API核心类 var _that = this var demo = new QQMapWX({ key: ‘你申请到的key’ // 必填 }); // 调用接口 demo.calculateDistance({ to: [{ latitude: 39.924091, //商家的纬度 longitude: 116.403414, //商家的经度 }], success: function(res) { let hw = res.result.elements[0].distance //拿到距离(米) if (hw && hw !== -1) { //拿到正确的值 //转换成公里 hw = (hw / 2 / 500).toFixed(2) + ‘公里’ } else { hw = “距离太近或请刷新重试” } console.log(‘腾讯地图计算距离商家’ + hw); } }); }})可能会出现的错误:{status:199,message:‘此key未开启webservice功能’},不要紧,打开腾讯位置-key配置,设置一下刚才申请key的详情页面,把下列选项全部勾上,把你小程序的appid也写上。保存完,重新编译再试优缺点优点:第一种方法,不用配置任何东西,只需两点的经纬度即可,没有使用次数限制; 第二种方法,不需要自己计算,腾讯会计算好,距离比较精确,只需要只要商家的经纬度即可缺点:第一种方法,计算精度上可能有待考量,在我的实验下,感觉是在上帝视角,直接计算两个点的距离,不过好像两点距离不太远,问题不大;下图是我用两种方法计算的杭州-石家庄的距离,方法一显然比腾讯的少一点距离,腾讯可能参考了一些实际的路程、路况之类的吧,感觉跟从地图上查行程规划出来的距离差不多。第二种方法,有使用次数上的限制,每天只能用1万次,当然可以再去买配额建议既然腾讯的api有使用次数限制,那我们就写个方法,先用腾讯的,加上判断,用完了再用 经纬度计算的。当然,有钱的大佬可以另外买腾讯的配额。 ...

December 17, 2018 · 1 min · jiezi

微信小程序解除 10 个请求并发限制了?!

这可能是一个冷消息,所以标题比较劲爆。小程序并发限制由来已久,从刚发布时的 5 并发,到后来的 10 并发,同时发出的请求数若超出这个限制则将被残忍抛弃,由此催生了很多开发者在自己的项目中造了「请求排队」的轮子。然而事实上,早在一年半以前,该限制就被微信官方取消。10 个请求的并发限制关于并发限制,微信开发者文档中是这么写的:这一限制的意思是在同一时刻, wx.request、wx.uploadFile、wx.downloadFile 加起来的并发总数不能超出 10 个。至今,仍有很多开发者一直遵守着这个规则。许多人在写业务的时候小心翼翼地维护着请求数。为了将请求数控制好,特地将一些并行请求改为串行,或者引入请求队列来维护小程序请求。这部分资深开发者为了遵守这一规则所花的功夫,多少反映出了早年他们在面对数额超出后请求被残忍抛弃时的无奈。附小程序基础库版本 1.3.0 的控制台报错:时至今日,仍有开发者在讨论解决小程序并发限制的方法:被忽略的消息实际上,微信在 2017 年 7 月的基础库 1.4.0 版本升级中就做了优化,对超过并发限制的请求做了队列处理,只是还有很多开发者并不知道这一消息。从严格意义上来说,此次优化并没有完全解除原有的并发限制。目前同时处理请求的上限仍是 10 个,但在 10 个以外的请求会排队,当前面有请求完成的时候,队列中的请求按顺序发送并处理,*不会像之前那样直接将超出 10 个的请求丢弃。附件小程序基础库 1.4.0 更新日志(部分):现在,我们终于可以忽略请求并发限制,愉快地发送请求了。毕竟请求都是可以都发送出去的,只不过在效率上会比无并发限制的情况慢一些。发送请求的正确姿势如上文所说,微信小程序是在基础库 1.4.0 版本中加入对超过并发限制的请求做队列处理优化的,在 1.4.0 以下的版本中超出并发部分的请求会被丢弃。据微信官方数据,截止到 2018 年 12 月,1.4.0 版本以下用户占比大约是 0.04%,虽然目前小程序很少会兼容到这么低的版本,但是对一些有特殊需要的小程序也要注意基础库的差异。另外要注意的是小程序并发请求的排队机制。当同时调用的请求超过 10 个时,小程序会先发起 10 个并发请求,超过 10 个的部分按调用顺序进行排队,当前一个请求完成时,再发送队列中的下一个请求。附 20 个请求并发测试:测试结果:从图中可以看到,前 10 个请求同时发出,而后面的请求的起始点,对应了前面某个请求的结束点,可以反映出请求的排队行为。这意味着,在并发请求很多的时候应该做好排队策略,按请求的重要程度和响应时间调整调用顺序,如果遇到请求的响应很慢的情况,可以考虑做 timeout 处理,以免大量等待,影响用户体验。搜索关注公众号「知晓云」,让你的小程序开发快人一步。

December 12, 2018 · 1 min · jiezi

原生js实现瀑布流及微信小程序中使用左右两列实现瀑布流

使用css实现瀑布流并不实用,因为潮汕市实现的瀑布流都是以列来排列的,这里记录下用js实现瀑布流,以及微信小程序中使用左右两列来实现瀑布流1.效果图2.原生js实现瀑布流html文件<div id=“root”> <div class=“item”> <div class=“itemImg”> <img src="../images/1.jpeg" alt="" /> </div> </div> <div class=“item”> <div class=“itemImg”> <img src="../images/3.jpeg" alt="" /> </div> </div> <div class=“item”> <div class=“itemImg”> <img src="../images/2.jpg" alt="" /> </div> </div></div>图片可以自己找点替换下就可以了css文件*{ margin: 0; padding: 0;}#root{ position: relative;}.item{ float: left; padding: 5px;}/* 添加阴影的时候,加上border会显得更加有点悬浮感 /.itemImg{ padding: 5px; border: 1px solid #ccc; box-shadow: 0 0 5px #ccc; border-radius: 5px;}.itemImg img{ width: 230px; height: auto;}js文件window.onload = function () { / 计算图片列数及获取最小高度图片 / generateImg(‘root’, ‘item’); / 对窗口大小改变进行监听,大小改变则重新布局 / window.addEventListener(‘resize’, function() { generateImg(‘root’, ‘item’) }); / 图片对象 / let imgData = { images: [ { “src”:“23.png” }, { “src”:“22.png” }, { “src”:“2.jpg” }, { “src”:“4.jpg” }, { “src”:“7.jpg” } ] }; / 对滚动监听 / window.addEventListener(‘scroll’, function() { if(checkIsScroll()) { let rootElement = document.getElementById(‘root’); / 利用documentFragment来创建 /// let documentFragment = document.createDocumentFragment(); let length = imgData.images.length; / 循环创建图片组 / for(let i = 0; i < length; i++) { let itemElement = document.createElement(‘div’); itemElement.className = ‘item’; rootElement.appendChild(itemElement); let itemImgElement = document.createElement(‘div’); itemImgElement.className = ‘itemImg’; itemElement.appendChild(itemImgElement); let itemImg = document.createElement(‘img’); itemImg.style.cssText = ‘opacity: 0; transform:scale(0)’; itemImg.src = “../images/” + imgData.images[i].src; itemImgElement.appendChild(itemImg);// documentFragment.appendChild(itemElement); / 在1秒后让图片显示出来 / (function(img){ setTimeout(function(){ img.style.cssText=“opacity:1;transform:scale(1)”; },1000); })(itemImg); }// rootElement.appendChild(documentFragment); generateImg(‘root’, ‘item’); } });};/ 计算图片列数及获取最小高度图片 /function generateImg(parent, content) { / 获取父元素及其所以节点内容 / let parentElement = document.getElementById(parent); let childContent = getChildElement(parentElement, content); / 获取图片宽度 / let imgWidth = childContent[0].offsetWidth; / 获取一行图片形成的列数 / let imgColumn = Math.floor(document.documentElement.clientWidth / imgWidth); / 重新设置父级容器的宽度 / parentElement.style.cssText = ‘width:’ + imgColumn * imgWidth + ‘px;margin:0 auto’; / 存储每个图片的高度,以此来找到最小图片高 / let imgHeightArray = []; let length = childContent.length; for(let i = 0; i < length; i++) { / i<imgColumn统计每一行的图片高度 / if(i < imgColumn) { / 防止用户改变窗口大小时,内容样式错乱 / childContent[i].style.cssText = ‘’; imgHeightArray.push(childContent[i].offsetHeight); } else { / 如果不是这一行的,则找到最小值和最小值的索引值 / let minHeight = getMinImgHeight(imgHeightArray); let minHeightIndex = getMinHeightIndex(imgHeightArray, minHeight); / 对这个图片设置位置 / childContent[i].style.position = ‘absolute’; childContent[i].style.top = minHeight + ‘px’; childContent[i].style.left = childContent[minHeightIndex].offsetLeft + ‘px’; / 更换此时的最小高度 / imgHeightArray[minHeightIndex] = childContent[i].offsetHeight + minHeight; } }}/ 检测滚动是否达到了可视区 /function checkIsScroll() { / 获取root根节点 / let parentElement = document.getElementById(‘root’); / 获取父元素下的类名为box的元素节点 / let childContent = getChildElement(parentElement, ‘item’); / 获取最后一个元素的高度 / let lastElementHeight = childContent[childContent.length - 1].offsetTop; / 获取滚动的距离 / let scrollTopSpace = document.documentElement.scrollTop || document.body.scrollTop; / 获取可视区的距离 / let clientHeight = document.documentElement.clientHeight || document.body.clientHeight; if(lastElementHeight > scrollTopSpace + clientHeight) { return true; }}/ 获取子节点的所有内容 /function getChildElement(parentElement, content) { / 存储元素信息 / let elementArray = []; / 获取父元素下的所有节点信息 / let allElement = parentElement.getElementsByTagName(’’); let length = allElement.length; for (let i = 0; i < length; i++) { /* 找到对应的类名 / if (allElement[i].className === content) { elementArray.push(allElement[i]); } } return elementArray;}/ 获取图片最小高度 /function getMinImgHeight(heightArray) { let length = heightArray.length; let minHeight = heightArray[0]; for(let i = 0; i < length; i++) { minHeight = Math.min(minHeight, heightArray[i]); } return minHeight;}/ 获取图片最小高度的索引值 /function getMinHeightIndex(heightArray, minHeight) { let length = heightArray.length; for(let i = 0; i < length; i++) { if(heightArray[i] == minHeight) { return i; } }}3.微信小程序中实现瀑布流效果图wxml文件<view class=“cateCommodity”> <view class=“leftContainer”> <block wx:for="{{imageArray}}" wx:key="{{item.id}}"> <view class=“cateItem” wx:if="{{index%2==0}}"> <view class=“item”> <image src="{{item.src}}" class=“itemImg” mode=“widthFix”></image> <view class=“title”>{{item.title}}</view> </view> </view> </block> </view> <view class=“rightContainer”> <block wx:for="{{imageArray}}" wx:key="{{item.id}}"> <view class=“cateItem” wx:if="{{index%2==1}}"> <view class=“item”> <image src="{{item.src}}" class=“itemImg” mode=“widthFix”></image> <view class=“title”>{{item.title}}</view> </view> </view> </block> </view></view><view class=“skipTop” catchtap=“skipTop” wx:if="{{showTopImage}}"> <image src=“http://boweisou.oss-cn-shenzhen.aliyuncs.com/images/0/2018/11/ZBtqujbbcGjBDgjt0bbJqbTuGqq0z8.png"></image></view>wxss文件page{ background: #f6f6f6;}/ 最外层 /.cateCommodity { display: flex; padding: 20rpx 28rpx 8rpx; box-sizing: border-box; font-size: 28rpx;}/ 左右两个容器 /.leftContainer{ display: flex; margin-right: 22rpx; flex-direction: column;}.rightContainer{ display: flex; flex-direction: column;}/ 图片容器 /.cateItem { margin-bottom: 20rpx;}.item{ padding: 20rpx 22rpx; width: 335rpx; box-sizing: border-box; background: #fff; border-radius: 6rpx;}.itemImg{ margin-bottom: 14rpx; width: 100%; vertical-align: middle; border-radius: 6rpx;}.title{ display: -webkit-box; overflow: hidden; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.5;}/ 返回顶部 /.skipTop { position: fixed; bottom: 30rpx; right: 20rpx; width: 90rpx; height: 90rpx;}.skipTop image { width: 100%; height: 100%; vertical-align: middle;}js文件Page({ data: { imageArray: [ { id: 1, src: ‘../../images/avatar.jpeg’, title: ‘现代新中式创意陶瓷简约摆件客厅家居玄关软装饰品家居酒柜盘子’ }, { id: 1, src: ‘../../images/avatar3.jpg’, title: ‘秋冬季新款2018休闲运动服套装女士韩版金丝绒卫衣加绒加厚两件套’ }, { id: 1, src: ‘../../images/avatar4.jpeg’, title: ‘女童床上用品四件套公主房1.2m床品纯棉女孩1.8儿童床单三件套1.5’ }, { id: 1, src: ‘../../images/avatar7.jpg’, title: ‘婴儿床圆床蚊帐落地款宝宝椭圆床蚊帐支架款儿童床蚊帐BB床小蚊帐’ }, { id: 1, src: ‘../../images/avatar9.jpeg’, title: ‘包邮动感158T速滑鞋轮滑鞋竞速鞋高端碳纤鞋 固定码 专业定制’ }, { id: 1, src: ‘../../images/logo7.jpg’, title: ‘Infanton落地婴儿床蚊帐带支架儿童床蚊帐宝宝蚊帐婴童蚊帐’ }, { id: 1, src: ‘../../images/logo6.jpg’, title: ‘老A轮滑 米高seba hl碳纤版SEBA HL CARBON 平花鞋刹车鞋全能鞋’ }, { id: 1, src: ‘../../images/logo.jpeg’, title: ‘洋洋法代 sandro 17秋冬 一粒扣羊毛长款大衣外套EKIN M9575H’ }, ], showTopImage: false, }, onPageScroll(event) { / 利用两个条件,防止重复的进行setData操作 / if (event.scrollTop > 300 && this.data.showTopImage == false) { this.setData({ showTopImage: true }) } else if (event.scrollTop < 300 && this.data.showTopImage == true) { this.setData({ showTopImage: false }) } }, skipTop() { / 返回顶部 */ wx.pageScrollTo({ scrollTop: 0, duration: 300 }); this.setData({ showTopImage: false }); }, onReachBottom: function () { let temporaryArray = this.data.imageArray; temporaryArray.push(…this.data.imageArray); this.setData({ imageArray: temporaryArray }) },})左右两列实现瀑布流其实就是对同一数组进行了两次渲染,只是把其中的一半给隐藏了正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)实现单行及多行文字超出后加省略号微信小程序之购物车和父子组件传值及calc的注意事项 ...

December 11, 2018 · 4 min · jiezi

WeGeek 微信小程序黑客马拉松主题公布!

12 月 15 日-12 月 16 日,由腾讯公司微信事业群主办的「WeGeek 微信小程序黑客马拉松」将在北京正式启动,Hackathon 面向全球小程序开发者、爱好者,旨在通过微信小程序平台进行小程序的创新开发,共同建设小程序生态。Hackathon 一经发布备受圈内外关注,报名通道开启两周内即有近 500 人报名,通过先后两轮筛选共入围 160 人,现场预计将有近 40 支团队进行小程序开发。现公布本次小程序黑客马拉松的主题及评分规则等事宜,快来了解一下吧~主题公布本次 WeGeek 微信小程序黑客马拉松主题分为「工具、生活服务、教育」三大类目,各团队可选择任意类目进行小程序开发,最终奖项将进行统一评选。各团队开发内容可包括但不限于以下参考方向:工具类例如:提供日常消费记账,实时汇率查询换算,公共交通便捷支付,极端天气预警和查询,办公文档设计,旅行计划制定,朋友聚会发起,活动报名与抽奖,个人健康数据管理,企业 OA 及流程管理等服务。生活服务类例如:提供家政陪护预约,公益环保宣传或发起,婚庆摄影摄像,美容美发预约,社区生活服务的查询办理等服务。教育类例如:提供校园教务查询,学习计划制定,在线词典查询,早教育儿培训,特殊人群教育等服务。作品评选本次评分由评委打分和团队互评两部分组成,满分为 100 分,评委分数占 70%,团队互评占 30%。其中评委从作品的完成度、创意性、实用性、商业前景四个维度点评打分,团队对最喜欢的 5 个作品投票。工作人员核对提交数据后为有效计分。完成度(40%):提交方案、作品的实现程度(以作品为主)创意性(20%):作品的创新程度(模式创新、非已上线产品和非跨平台移植产品)实用性(20%):产品在应用场景中的实际应用程度商业前景(20%):在短期或中短期内,产品可以在日常工作生活中产生的商业价值议程安排时间事项2018.12.15 8:00-9:00签到9:00-9:30开幕式9:30-12:00组队,现场开发12:00午餐13:00-18:00作品开发18:00晚餐19:00 后作品开发2018.12.16 8:00早餐8:00-12:00作品开发+提交12:00-13:00作品初筛,午餐13:00-16:30作品展示,评委打分17:00评委点评颁奖,闭幕式本次微信小程序黑客马拉松召集了各大互联网公司的开发者、在校大学生等众多微信小程序爱好者参与,期待大家的表现!

December 10, 2018 · 1 min · jiezi

从项目中由浅入深的学习vue,react,微信小程序和快应用(1)

**才见岭头云似盖,已惊岩下雪如尘。—《南秦雪》**前言这几天好多地方都下雪了,雪花真美呀,特地在网上搜上好看的图片和诗句写上。本文主要从template【模板】讲到一个demo,快速上手vue、react和微信小城序的项目开发。如果你不熟悉这中间的某一个技术栈,可以clone下来跑一跑。 如果全部能上手,中间有些细节耶可以看看。开撸1.template篇1.1 vue-template-pc1.效果图vue-template-pc项目,欢迎star2.技术栈vue+vue-router+vuex+axios+element-UI+iconfont(阿里)3.适配方案左侧固定宽度,右侧自适应左侧导航和右侧导航分别配置滚动条4.技能点分析技能点对应api常用指令@(v-on)绑定事件, v-if/v-show是否创建/和是否显示,v-for循环生命周期8个生命周期beforeCreate,created,beforeMount,mounted,beforeUpdate,updated,beforeDestroy和destroy观察计算computed和watchdata属性定义变量,同样变量使用必须先定义组件注册components局部注册,Vue.component()全局注册组件通讯子传父:this.$emit,父传子:props,平级组件:vuex或路由传参插件注册Vue.use()注册插件,如Vue.use(element)是调用element内部的install方法路由注册vue-router:Vue.use(router)也是调用内部的install方法,挂载到vue实例中生成route和router属性路由模式mode属性可以设置history和hash子路由children:[]可以配置子路由路由钩子router.beforeEach(实现导航钩子守卫)和router.afterEachvuex4个属性,state,getters, actions(异步获取数据)和mutations(同步获取数据)vuex4个辅助函数,mapState,mapGetters, mapActions和mapMutations,就是辅助处理commit或distapch方法axios拦截器,interceptors.request请求拦截器,interceptors.response响应拦截器axiosbaseUrl配置公共请求路径,必须符合http标准的链接,否则设置无效axios请求方法,get,post,put,delete等axios跨域,withCredentials: true,需要后端支持csssass,对应嵌套不超过三层,滚动条样式设置,文本两行超出build问题iconfont阿里字体图标,可以自定义icon5.那么问题来了?computed和watch的区别? 解析路由传参的方法? 解析vue.use,vue.install,mixins方法区别? 解析history和hash区别及实现原理? 区别解析原理解析vue-router官网使用history和hash模式部署服务器有什么问题?问题解析vuex的辅助函数和基本属性使用的区别?vuex官网axios原理?axios源码简单实现一个vue+vue-router+vuex的框架?1.2 react-pc-template1.效果图react-pc-template项目, 欢迎star2.技术栈dva+umi+ant-design-prodva:可拔插的react应用框架,基于react和reduxmui:集成react的router和reduxant-design-pro:基于react和ant-pc的中后台解决方案3.适配方案左侧固定宽度,右侧自适应右侧导航分别配置滚动条.控制整个page4.技能点分析技能点对应apiJSXreact是基于jSX语法生命周期实例化(5个):getDefaultProps,getInitialState,componentWillMount,render,componentDidMount生命周期更新:5个生命周期生命周期销毁:componentWillUnmout路由基于umi,里面有push,replace,go等方法状态管理dva里面的redux的封装,属性有state,effects,reducers组件传值父子:props,平级redux或umi的routermodel项目的model和dom是通过@connect()连接并将部分属性添加到props里登陆登陆是通过在入口js里面做路由判断5.那么问题来了?umi的router传参形式? 解析dva封装的redux和原生的redux使用有那些不同? dva使用解析redux使用解析umi里面router实现原理?umi源码对比vue和react在原理和使用上的区别?1.3 vue-mobile-template移动端代码见demo篇1.4 小程序模板由于小程序的IDE里面有生成的模板,mobile也是基于vue,所以只在demo篇展示demo1.5 快应用模板1.template代码实现官方template生成教程2.技能点分析技能点对应api布局基于弹性布局指令for:循环,if、show生命周期页面的生命周期:onInit、onReady、onShow、onHide、onDestroy、onBackPress、onMenuPressapp生命周期onCreate、onDestroy事件$on、$off、$emit、$emitElement路由配置manifest文件的router属性配置路由跳转router.page组件通讯父子组件:$emit,props,兄弟组件:通过 Publish/Subscribe 模型原生组件list,map,tabs和canvas消息机制websocket使用2.demo篇2.1 vue-demo(vue-pc-demo)1.效果图vue-demo项目地址, 欢迎star2.技术栈vue+vue-router+vuex+axios+element-UI+高德map+vue-split-table高德map:高德地图vue-split-table:表格拆分插件,vue-split-table插件3.适配方案同上4.技能点分析比template篇多了map的使用,高德使用手册实现axios的api模块化,并全局挂载api和axios所以由此可以看出只要有了template,后期开发so-easy,只是新加tab页2.2 react-pc-demo参考ant的ant-design-pro项目2.3 vue-mobile-demo1.效果图vue-mobile项目2.技术栈vue+vue-router+vuex+vant+rem+sass+iconfont(阿里)vant:有赞的电商mobile插件3.适配方案rem4.技能点分析iconfont的使用:官网配置icon,导出图标,引入assets目录下vant使用:详见vant官网全局配置rem:在index.html文件配置全局配置sass函数和mixin:在build/utils下面的scss的options属性配置static目录下面的函数和混入5.那么问题来了vue-cli生成的项目src下面的assets和根路径下面的static目录的区别?解析2.4 小程序demo1.效果min-program-demo项目,欢迎star2.技术栈weui+tabbar+分包+iconfont+自定义顶部导航+组件传值+wx.request封装weui:Tencent推出的小程序UI3.适配方案rpx:微信小程序的单位4.技能点分析技能点对应api常用指令bindtap绑定事件, wx:if/wx:show是否创建/和是否显示,wx:for循环生命周期1应用生命周期(app.js里):launch,show,hide生命周期2页面生命周期(page里):load,show,ready,hide,unload生命周期3组件周期(component里):created,attached,moved,detachedwx.requestajax请求背景音乐wx.getBackgroundAudioManager音频wx.createAudioContext图片wx.chooseImage文件wx.getFileInfo路由在app.json里面pages属性定义pages目录下面的文件路由切换wx.navigateTo,wx.navigateBack, wx.redirectTo,wx.switchTab,wx.reLaunch分包在app.json里面subPackages属性定义分包路由weui组件weui官网原生组件微信原生组件业务组件在json文件usingComponents注册组件通讯定义globalData,storage和路由5.那么问题来了小程序的生命周期执行顺序?page和应用生命周期 , component生命周期解释几种路由切换有什么不同?路由介绍小程序怎么实现watch监听数据变化?实现watch6.小程序框架wepy官网基于wepy的商城项目mpVue基于mpVue的项目分析:这两个框架都是通过预编译将对应风格的格式转化成小程序格式2.5 快应用demo类似书单项目的快应用3.结语对比下vue,react,微信小程序和快应用这几种技术栈开发,可以分为两类,一类是mvvm式的开发:vue,微信小程序和快应用一类是基于JSX的view开发

December 10, 2018 · 1 min · jiezi

微信小程序发送模板消息!附前端+后端源码~

前端,index.wxml<form bindsubmit=“submit” report-submit=‘true’ > <input type=‘text’ value=‘填写openid’ name=“openid”></input> <input type=‘text’ value=‘填写ACCESS_TOKEN’ name=“token”></input> <input type=‘text’ value=‘填写模板ID’ name=“template”></input> <input type=‘text’ value=‘模板的第1个关键词’ name=“keyword1”></input> <input type=‘text’ value=‘模板的第2个关键词’ name=“keyword2”></input> <input type=‘text’ value=‘模板的第3个关键词’ name=“keyword3”></input> <input type=‘text’ value=‘模板的第4个关键词’ name=“keyword4”></input> <input type=‘text’ value=‘模板的第5个关键词’ name=“keyword5”></input> <button form-type=“submit” type=“default”>推送</button></form>index.js// pages/mubanxiaoxi/mubanxiaoxi.jsPage({ data: { }, submit: function (e) { var openid = e.detail.value.openid; var access = e.detail.value.token; var template = e.detail.value.template; var keyword1 = e.detail.value.keyword1; var keyword2 = e.detail.value.keyword2; var keyword3 = e.detail.value.keyword3; var keyword4 = e.detail.value.keyword4; var keyword5 = e.detail.value.keyword5; var that = this; wx.request({ url: ‘域名/muban.php?openid=’ + e.detail.value.openid + ‘&token=’ + e.detail.value.token + ‘&template=’ + e.detail.value.template + ‘&formid=’ + e.detail.formId + ‘&keyword1=’ + e.detail.value.keyword1 + ‘&keyword2=’ + e.detail.value.keyword2 + ‘&keyword3=’ + e.detail.value.keyword3 + ‘&keyword4=’ + e.detail.value.keyword4 + ‘&keyword5=’ + e.detail.value.keyword5, //接口地址,我学习就用get,建议用post data: { open_id: openid, tok_en: access, temp_late: template, form_id: e.detail.formId, keyword_1: keyword1, keyword_2: keyword2, keyword_3: keyword3, keyword_4: keyword4, keyword_5: keyword5 }, success: function (res) { // console.log(e.detail.formId); // console.log(res.data); } }) }})后端,muban.php<?php //GET参数 $access_token=$_GET[’token’]; $openid=$_GET[‘openid’]; $templateid=$_GET[’template’]; $formid=$_GET[‘formid’]; $keyword1=$_GET[‘keyword1’]; $keyword2=$_GET[‘keyword2’]; $keyword3=$_GET[‘keyword3’]; $keyword4=$_GET[‘keyword4’]; $keyword5=$_GET[‘keyword5’]; echo $keywordd1; //此处开始处理数据 $dataa=array( “keyword1”=>array( “value”=>$keyword1, “color”=>"#9b9b9b"), “keyword2”=>array( “value”=>$keyword2, “color”=>"#9b9b9b"), “keyword3”=>array( “value”=>$keyword3, “color”=>"#9b9b9b"), “keyword4”=>array( “value”=>$keyword4, “color”=>"#9b9b9b"), “keyword5”=>array( “value”=>$keyword5, “color”=>"#9b9b9b") ); $data=array(); $data[’touser’]=$openid; $data[’template_id’]=$templateid; $data[‘form_id’]=$formid; $data[‘data’]=$dataa; $url = ‘https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token='.$access_token; $type=“json”; if($type==‘json’){//json $_POST=json_decode(file_get_contents(‘php://input’), TRUE); $headers = array(“Content-type: application/json;charset=UTF-8”,“Accept: application/json”,“Cache-Control: no-cache”, “Pragma: no-cache”); $data=json_encode($data); } $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_POST, 1); // 发送一个常规的Post请求 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE); if (!empty($data)){ curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS,$data); } curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_HTTPHEADER, $headers ); $output = curl_exec($curl); if (curl_errno($curl)) { echo ‘Errno’.curl_error($curl);//捕抓异常 } curl_close($curl); echo $output;?>至于openid和access_token怎么获取,自己另外学习咯!推送成功! ...

December 5, 2018 · 2 min · jiezi

去除富文本中的html标签及vue、react、微信小程序中的过滤器

在获取富文本后,又只要显示部分内容,需要去除富文本标签,然后再截取其中一部分内容;然后就是过滤器,在微信小程序中使用还是挺多次的,在vue及react中也遇到过1.富文本去除html标签去除html标签及 空格let richText = ’ <p style=“font-size: 25px;color: white”>&nbsp; &nbsp; &nbsp; &nbsp;sdaflsjf的丰富及饿哦塞尔</p><span>dsfjlie</span>’;/* 去除富文本中的html标签 // 、+限定符都是贪婪的,因为它们会尽可能多的匹配文字,只有在它们的后面加上一个?就可以实现非贪婪或最小匹配。/let content = richText.replace(/<.+?>/g, ‘’);console.log(content);/* 去除&nbsp; /content = content.replace(/&nbsp;/ig, ‘’);console.log(content);/ 去除空格 /content = content.replace(/\s/ig, ‘’);console.log(content);截取字符串content = formatRichText(content);console.log(content);/ 使用substring来截取字符串 /if (content.length > 10) { content = content.substring(0, 10) + ‘…’;}console.log(content);/ 限制字数后添加省略号 /function formatRichText(richText) { let temporaryText = ‘’; / 设置多长后添加省略号 / const len = 142; if (richText.length * 2 <= len) { return richText; } / 用于记录文字内容的总长度 / let strLength = 0; for (let i = 0; i < richText.length; i++) { temporaryText = temporaryText + richText.charAt(i); / charCodeAt()返回指定位置的字符的Unicode编码,值为128以下时一个字符占一位,当值在128以上是一个字符占两位 / if (richText.charCodeAt(i) > 128) { strLength = strLength + 2; if (strLength >= len) { return temporaryText.substring(0, temporaryText.length - 1) + “…”; } } else { strLength = strLength + 1; if (strLength >= len) { return temporaryText.substring(0, temporaryText.length - 2) + “…”; } } } return temporaryText;}2.vue中使用过滤器filters: { localData(value) { let date = new Date(value * 1000); let Month = date.getMonth() + 1; let Day = date.getDate(); let Y = date.getFullYear() + ‘年’; let M = Month < 10 ? ‘0’ + Month + ‘月’ : Month + ‘月’; let D = Day + 1 < 10 ? ‘0’ + Day + ‘日’ : Day + ‘日’; let hours = date.getHours(); let minutes = date.getMinutes(); let hour = hours < 10 ? ‘0’ + hours + ‘:’ : hours + ‘:’; let minute = minutes < 10 ? ‘0’ + minutes : minutes; return Y + M + D + ’ ’ + hour + minute; }}/ 使用,直接在div中添加就可以了,| 前面的是参数,后面的是过滤器 /<div class=“time”>{{data.etime | localData}}</div>3.微信小程序中使用过滤器新建.wxs文件var localData = function (value) { var date = getDate(value * 1000); var Month = date.getMonth() + 1; var Day = date.getDate(); var hours = date.getHours(); //计算剩余的小时 var minutes = date.getMinutes(); //计算剩余的分钟 var Y = date.getFullYear() + ‘-’; var M = Month < 10 ? ‘0’ + Month + ‘-’ : Month + ‘-’; var D = Day + 1 < 10 ? ‘0’ + Day + ’’ : Day + ‘’; var H = hours < 10 ? ‘0’ + hours + ‘:’ : hours + ‘:’ var m = minutes < 10 ? ‘0’ + minutes : minutes; return Y+M + D + " " + H + m;}module.exports = { localData: localData}使用,用<wxs />标签来引入,src为路径,module为引入的文件模块名<wxs src="./filters.wxs" module=“tool” /><text class=“scoreText”>{{tool.filterScore(item.shop.score)}}分</text>直接在.wxml文件中用<wxs></wxs>包裹<wxs module=“foo”>var some_msg = “hello world”;module.exports = { msg : some_msg,}</wxs><view> {{foo.msg}} </view>4.react中使用react中使用,其实就是定义一个方法import noBanner from ‘@/assets/storeDetail/no-banner.jpg’const filterImg = item => { let bgImg; if (item.shopimages == null) { bgImg = noBanner; } else { bgImg = item.shopimages[0]; } return bgImg;};/ 使用 */ <img src={filterImg(storeitem)} className={style.topImg} alt="" />正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)实现多行文字及单行的省略号微信小程序之购物车和父子组件传值及calc的注意事项 ...

November 20, 2018 · 2 min · jiezi

小程序云开发今日头条项目实战

最近自己正做一个仿今日头条移动端App的小程序,是基于小程序云开发的,在做小程序项目的时候使用云开发确实方便是很多。有关于云开发后面我也会讲到,毕竟这个项目就是使用的云开发,更多的有关云开发内容有需要的小伙伴可以去看官方文档,云开发文档。 项目的开发也有一段时间了虽然只是实现了部分功能,但是我还是忍不住来写篇文章来分享一下这段时间的成果和项目中遇到的问题,后面我也会逐步的完善项目。项目源码在github上,如果小伙伴们觉得不错可以给个star,仿头条项目地址。项目介绍 今日头条现在应该是最火的新闻类的app了,很多人空闲下来的时候都会逛一下头条看看新闻什么的,我自己也挺喜欢逛头条的,刚好腾讯也推出了云开发,所以我打算仿一个头条的app同时也在实战中用一下云开发功能。新闻首页使用云函数从数据库获取新闻信息,每一个标签页都是对应的新闻信息,向下拉获取最新信息,网上拉获取更多新闻,在推荐页有一个置顶的新闻详情页在首页点击任意一个新闻,进入到对应新闻的详情页进入详情页后显示新闻的详细信息未登录不能发布评论登录后发布评论每一项评论可以点赞和取消点赞分享,收藏,点击评论按钮直接到底部的达评论页面评论插入图片项目详解 下面将详细的介绍项目,虽然使用的云开发节省了很多时间但是前后端的东西都需要做工作量有点大,在这短时间内我没有完成整个项目,只是实现了首页,详情页,和登录页等主要功能 我首先将界面需要获取数据的地方设计好数据库为后面数据的获取做准备,数据库使用的是小程序云开发的MongoDB数据库,将数据存储在云数据库上,并且使用云函数来操作数据库新闻首页 首页相对于详情页要简单一些,在头部使用了一个搜索框和搜索按钮,然后下面是一个tab标签栏含有多个标签页,每一个标签页显示标签对应有关的新闻信。在标签页的右边有一个按钮,点击按钮会出现一个弹出框。这里有个特别的地方,就是在推荐页的顶部设置了一个置顶的新闻 输入框绑定了一个tap事件,使得在点击输入框但不输入值的时候改变placeholder的值。 在tab栏的右边有一个按钮点击按钮将会出现一个弹出层,前面的gif中有演示,是新闻种类的选择框,点击关闭按钮可以关闭弹出层 最后就是首页最重要的新闻显示页面了,为了节省项目的时间,这里使用了有赞的框架vant-weapp有兴趣的小伙伴可以去了解下。在tab标签栏设置了6个标签页,但是只会显示4个标签页想要显示其他的可以左右拖动标签栏,这里将推荐页设置为了默认激活的。由于每个每个标签页代码基本都相同的,只是在推荐页是的第一栏是置顶信息,还有就是获取的数据不同,有关数据获取在下面介绍代码将会细讲,为了提高代码的复用,这里使用了模板,将复用的代码写在写在另外的文件下,使用时直接调用就可以了。 每个标签对应都创建了一个集合,这里我为置顶新闻也另外创建了一个集合,并且给每条信息设计好需要用的字段方便自己获取数据和使用数据,由于云数据库是可以导入json文件或者csv文件,并且每个新闻也都需要上拉加载数据需要更多的数据,自己造数据费时间又麻烦,所以我这里自己写了爬虫爬取自己需要的数据并保存到json文件中,直接将数据导入到数据库中。 这样设计数据库也是使得从数据库获取数据方便了一些。写一个module函数就可以获取每个标签的数据。每条数据的字段如下,其中news_id起到很重要的作用,将首页的每条新闻和对应的详情页面联系起来。 在每一个标签页使用模板,并且设置了一个data(给不同页面传入需要显示的对应新闻信息,用于在页面显示),由于默认激活页面是推荐页所以在onload事件触发时将默认加载推荐页的数据,同时将推荐页设置为已被激活页面,数据加载这里写了一个加载函数 module: function(title) { let counter = this.data.counter // console.log(title) wx.cloud.callFunction({ name: ‘module’, data: { counter: counter, title: title } }).then(res => { // console.log(res) let cnews = this.data.cnews let data = res.result.data // console.log(data) for(let i = 0; i < data.length; i++) { // console.log(data[i].date) data[i].date = data[i].date.slice(0, 10) cnews.push(data[i]) this.imgCheck(data[i].images, data.new_id, title) } // console.log(data) this.setData({ hiddenLoading: true, cnews: cnews, counter: counter+1 }) }) }传入一个title就是当前显示的标签的标题,默认的是推荐,使用一个counter计数,每次只会加载5条新闻条数据,从数据库获取新闻的信息是由一个云函数来解决的// 云函数入口文件const cloud = require(‘wx-server-sdk’)cloud.init()const db = cloud.database()// 云函数入口函数exports.main = async (event, context) => { return await db.collection(event.title).skip((event.counter-1)*5).limit(5).get()}给云函数传入两个数据,一个是title就是从对应集合获取信息,还有一个就是counter用来计算获取信息的位置,因为,在向上拉取加载更多新闻的时候需要加载数据,我这里设置每次加载5条数据,所以传递给云函数一个counter,每次调用了云函数从与数据库获取一次数据counter就会+1,从而使得每次上拉加载数据时忽略已经加载的数据从后面加载数据。每次加载数据都会更新一次保存数据的数组,在主页的index.wxml页面将会判断并获取数据使用一个for循环将数据显示到对应的标签页在置顶新闻那部分在数据获取的数据其实也没很特别,我只是将置顶新闻集合中最新的新闻从云数据库拿下来,然后展示页面中这里也实现了下拉刷新,使用了小程序的onPullDownRefresh函数下拉刷新将会获取最新的数据,并且将最新的数据插显示在最上部分,由于每次下拉需要插入数据到集合的前面,所以我这里显示不明显 onPullDownRefresh: function () { // 监听下拉动作来获取最新新闻信息 wx.showToast({ title: ‘推荐中’, image: ‘../../../image/加载.png’ }) let title = this.data.title; wx.cloud.callFunction({ name: ‘module’, data: { counter: 1, title: title } }).then(res => { // console.log(res) let cnews = this.data.cnews let datas = res.result.data let data = datas.concat(cnews) this.setData({ hiddenLoading: true, cnews: data, }) }) }在module函数有使用了一个图片鉴黄功能,使用了腾讯的一个图片识别接口,毕竟不是什么图片都能显示出来,所以写了一个imgCheck函数来检测每一条新闻的所有图片,当图片不合格的时候则将这条新闻删除。我没有使用图片去测试,更不可能展示出来是吧,相信大家都懂(猥琐笑)。当不同标签页进行切换的时候会有一个onchange事件,onchange事件会获得title和index。并且将onchange之后的数据保存到一个数组中,在onchange事件里面使用module函数来获取对应的title的数据并且判断这条数据是否是最近加载过的,如果是上次onchange事件加载过的函数将会显示上次事件保存的数据,在wxml中也会判断是否是上次激活的页面来显示对应的数据。 前面一直再讲后端的东西,我也这篇文章也是为了讲云开,说实话有了云开发真的方便了很多,一个人就可以搞定前后端的东西。不过为了方便大家的理解,我还是讲一下界面的内容。 既然前都已经将数据拿过来了,最后要做的就是将数据展示出来了,每条数据<van-tab title=“推荐” > <!– 推荐tab标签页的置顶新闻框 每一个新闻框可以点击进入到详情页–> <van-panel class=“topping” title="{{topTitle}}" status=“置顶” use-footer-slot bind:tap=“showTopDetail” data-item="{{topNew_id}}"> <view class=“images”> <image src="{{images[0]}}" /> <image src="{{images[1]}}" /> <image src="{{images[2]}}" /> </view> <view solt=“footer” class=“footer”> <view class=“author”>{{topAuthor}}</view> <view class=“comment”>评论{{topComment}}</view> <view class=“Date”>{{topDate}}</view> </view> </van-panel> <template is=“container” data="{{news: active1 == ’news’?news:cnews, hiddenLoading}}"></template> </van-tab><template name=“container”> <view class=“container”> <loading hidden="{{hiddenLoading}}"></loading> <view class=“news” wx:for="{{news}}" wx:for-item=“info” wx:key=“info.new_id”> <van-panel class=“new” title="{{info.title}}" bind:tap=“showDetail” use-footer-slot data-item="{{info.new_id}}"> <view class=“images” wx:if="{{info.images.length > 0}}"> <image src="{{info.images[0]}}"/> <image src="{{info.images[1]}}"/> <image src="{{info.images[2]}}"/> </view> <view solt=“footer” class=“footer”> <view class=“author”>{{info.author}}</view> <view class=“comment”>评论{{info.comments}}</view> <view class=“Date”>{{info.date}}</view> </view> </van-panel> </view> </view></template>这里用的就是MVVM思想,将数据绑定到UI界面,在js文件中获取到数据后,这里将数据拿过来使用。这里由于每条新闻的图片数量是不确定的,并且最多只显示三张图片。所以直接固定了3个image标签并且固定了image的大小,当图片没有的时候就不会显示图片。详情页很多在首页讲过的东西我在详情页也就不再多说了,大家有不懂可以去看源码,毕竟讲那么多废话就是浪费时间,我尽量挑出最精彩的部分来写。在首页的每一条新闻都绑定了一个跳转tap事件,当点击新闻后将会跳转到详情页,并且将新闻的id和title作为参数传给详情页。 showDetail: function(e) { // 点文章显示文章详情 let item = e.currentTarget.dataset.item; let title = this.data.title; // console.log(e) wx.navigateTo({ url:../detail/detail?contentId=${item}&amp;title=${title} }) }在点击跳转到详情页后,将会在onload的事件中获取到对应的新闻id,并将id存到data里面。由于在爬取详情页的时候没有爬下来,所以我随便将一些简单的内容放在content里面。详情页这部分我将页面分为了内容部分和评论。然后还有就是使用了一个fixed将输入框等按钮固定在屏幕底部 内容部分又分为了四部分,分别是标题部分,作者头像和昵称,内容部分,点赞转发部分。第二部分为显示像和昵称我使用了一个flex的浮动布局将并且将昵称部分的flex设置为1使得头像和关注按钮分别在两边。头像使用了一个image标签并且将image标签的大小固定,毕竟用户上传的图片肯定大小不一样。第四部分只要使用4个view在把图片和内容放进去再使用一个flex布局就可以搞定。既然界面布局已经搞定现在就是要拿数据了,在点击新闻进入来详情页的时候会的到新闻的id和title,这样可以通过唯一id(每条doc的id)的和title(集合的名字)从云数据库拿出对应新闻数据。这里代码就不贴出来了,跟前面首页的差不多,有需要的可以去github看源码。接下来就是评论部分的内容了,个人认为这个地方还是挺有趣而且在更新数据库的时候还有权限问题,前面没有讲这个问题就是打算放到评论部分一起来讲。在页面的底部固定了一个评论框,包含输入框,跳转到评论的按钮,收藏按钮,转发按钮。点击转发按钮会出现一个弹窗,可以选择需要转发到的渠道,并且给弹出层背景添加了蒙层效果,只有在点击蒙层或者取消按钮弹出框才会消失。这里只实现了转发到微信的功能,只需要调用一下微信小程序的onShareAppMessage接口就可以搞定,当点击微信的图标后可以转发给朋友或者微信群。 收藏按钮我就是用了一个wx:if来判断显示的是那个image点击一个队bool值取反。点击评论按钮可以从直接跳转到评论的顶部,使用一个scrollview将整个详情包裹住然后使用它的一个属性scroll-into-view当点击底部的评论按钮时将评论部分的id赋值给scroll-into-view就可以实现锚点跳转了。在这个地方我踩了一个坑,没有给scroll-into-view设置一个高度导致效果一直出不来,由于详情页需要评论页面高度是改变的,所以直接给它设置一个100vh就可以完美搞定这个地方的锚点跳转了。 最后就是输入框了点击输入框或者左边的输入按钮就可以弹出评论输入框了,当输入框内有值的时候发布按钮会改变颜色。当未授权登录将无法发布评论这里就需要在我的页面点击登录进行授权,获取获取用户信息。登录功能的实现在页面登录按钮设置属性为open-type=getUserInfo,bindgetuserinfo=getUserInfo点击登录按钮授权登录将会获取用户信息,并将用户信息保存到全局上,这样在详情页面便可以判断或者使用用户信息。授权就可以发布新闻评论了,由于在登录的时候获取到了用户使用,所以在评论的是就有用户avatar和nickname。当在评论输入框中输入了值并且用户授权了登录的时候点击发布,同时将数据保存到数据库中。下面是评论功能函数submit: function() { // 实现评论功能,将发布的评论同步到云数据库 let value = this.data.inputValue; let new_id = this.data.new_id; let userInfo = this.data.userInfo // let new_id = ‘6594157273642172936’ if(userInfo){ comments.where({ new_id: new_id }).get({ success: (res) => { // console.log(res) let comms= res.data[0].comments; let people = { content: value, like: 0, avatar: userInfo.avatarUrl, nickname: userInfo.nickname } comms.unshift(people); // console.log(comm) this.setData({ comms: comms, input: ‘’, }) wx.cloud.callFunction({ name: ‘updateComments’, data: { new_id: new_id, comms: comms } }).then(res =>{ console.log(res) }) } }) } }评论部分的数据库我只创建了一个comments集合,开始的新闻new_id就起到作用了,每一comment都有一个new_id,新闻的每一条评论就是设置为一个对象,毕竟评论还包括头像昵称,点赞数,评论内容等。这样设置评论数据库好处就是,只要获取新闻id在一个集合中就可以获取到新闻对应的评论。开始在更新数据库的时候我没有使用云函数,而是在js中直接更新数据,获得返回信息显示是请求成功但是update数为0,但是当我到数据库中查看是发现数据并没有更新成功,查了一下文档发现是权限的问题,因为数据的修改只能是管理者或者数据的创建者,而数据又是我自己手动输入到云数据库的,在js中直接更新的数据库的时候不是创建者而在小程序端又没有管理者权限,所以没有权限修改数据。既然无法是创建者想要修改数据只能是管理员了,所以这里我使用了云函数来修改数据。说到这里大家应该意识到了云函数的权限是什么级别了吧,这里给大家看下官方文档的说明。从官方文档能看出云函数是有多强大了,还就是云函数也不能乱用,毕竟权限是最高的。 既然可以评论那就少不了点赞功能吧,虽然点赞是很普通的功能但是这里涉及到了云数据而且具体实现还是很有趣的。每条评论都可以点赞一次再次点击时将会取消点赞。评论部分的点赞我这里写的addLike函数绑定到点赞按钮,由于每条评论都绑定了相同的点赞函数,所以需要区分是那条评论被点赞所以给每条评论设置了data-item="{{index}}“i(index是在使用for循环展示评论使所产生的)同时对应了评论在数据库保存的位置,这样一来就方便来区分被点赞的那一条评论了。当被点赞后点赞按钮将换为红色的按钮,同时数据库中like也要加一。再次点赞按钮则还原,like也将还原。其实点赞功能还是很有趣的,这完全是我个人的想法,可能还有不好的地方,但是我还是推荐大家看一下。下面就是具体实现的代码addLike: function(e) { // 点击点赞图标增加点赞数同时保存到数据库 let item = e.currentTarget.dataset.item; let new_id = this.data.new_id; let comms = this.data.comms; let likeItem = this.data.likeItem; let likebool = ’likeItem[’+item+’].bool’ let liken = ’likeItem[’+item+’].n’ if(typeof(likeItem[item]) == “undefined”){ this.setData({ [likebool]: false, [liken]: 0, }) } if(likeItem[item]){ likeItem[item].n += 1; if(likeItem[item].n%2){ comms[item].like += 1; }else{ comms[item].like -= 1; } likeItem[item].bool = !(likeItem[item].bool); }else{ likeItem[item].bool = true; likeItem[item].n = 0; comms[item].like += 1; } this.setData({ comms: comms, likeItem: likeItem }) // console.log(comms) wx.cloud.callFunction({ name: ‘updateComments’, data: { new_id: new_id, comms: comms } }).then(res =>{ // console.log(res) }) },在评论输入框的下边栏有一个复选框按钮,图片按钮等这里我使用了了flex布局轻松搞定,这是我实现了下评论插入图片功能,同时将图片保存到云端。其实评论插入图片还需要优化,我在写完文章后也还会继续优化。我这是使用了小程序云开发的一个文件上传接口wx.cloud.uploadFile,将图片上传后会生成一个fileID,我将fileID(也就是图片地址)保存到当前评论对象的image下,同时更新本地的数据,再通过一个if来判断当前的评论是否含有图片,有的话就将图片显示在评论中。这里代码我就不贴出来,有需要的可以看源码。源码地址:云开发仿头条地址由于项目有点大所以我在短时间内只实现了部分功能,在后续的时间我会实现其他功能。其实写这个项目也是为了实战云开发,同时我也体验到了云开发的好处。项目中有些不足的地方欢迎大家指出,有什么好的建议也可以联系我。大家相互学习 ...

November 17, 2018 · 2 min · jiezi

微信小程序之登录态的探索

上一篇:开发微信小程序必须要知道的事登录,几乎什么项目都会用到,其重要性不言而喻,而小程序的登录却一直是为人头疼的一件事,这里我分享下我们在小程序登录上的探索通常的登录都是通过一个表单,这很正常,但如果在小程序里你也这么做那就有点不可思议了,微信的一键登录对用户体验有多好你难道不知道?不用是不是脑子有坑?最主要——你要利用微信的生态必须需要用微信的登录,以获取相关信息来和微信交互,OK,我们进入正题用户在小程序、小游戏中需要点击组件后,才可以触发登录授权弹窗、授权自己的昵称头像等数据友情提示一下:wx.login并不需要点击组件,需要的是wx.getUserInfo,但通常我们都会用到UnionID、encryptedData、iv等信息完成完整的登录流程,本文主要聚焦的也是这种场景所以之前直接通过调用API的方式就行不通了,那么问题来了——这个点击按钮要放到哪里?放到首页,一进小程序就必须先登录。这样显然很粗暴,而且问题并没有解决,反而会把用户直接拒之门外,毕竟你不是用小程序做后台系统,什么场景都需要授权,先授权也是必须的在需要授权的时候跳到登陆页面。这样就解决了上面遇到的不需要授权的时候也被强制授权,可是这样好吗?体验上不好,操作被打断,尤其整个页面都不需要授权只有在一个地方需要授权的,例如:你正在读一篇文章,读罢深有感触,想评论一番,洋洋洒洒几十字写完正准备点击呢,他妈的跳转了!跳转了!又一个漏斗,增加用户流失率。还TM要登录!很多用户心里一定这么想那就直接放在需要登录的页面上(这不是漏斗吗?很多读者一定这么想。但想想看上面那个场景,点评论时只是需要点击下弹出的登录按钮,而且还假模假样的以微信的口吻提醒你需要登录,那你会不会登录?最起码你很愿意登录,而且来的很突然,我控几不住自己的手就点了!点了!) 可是这种方式有一个问题怎么在需要的页面都能弹出登录按钮应该很多人都能想到:抽离出组件,那怎么保证在需要的页面都有这个组件呢?错杀一千也不能放过一个!把登录组件集成到共用的父组件,然后在每个页面都使用。我也建议这么做,因为这个共用的父组件其实又很多用处,例如iPhoneX适配等 等等,什么都准备好了,什么时候需要登录呢?XX,这个肯定是你自己控制的啦。嗯~好吧,我们来理一理在哪里校验是否需要鉴权请求接口的时候,嗯~这是大家的共识BOSS来了怎么鉴权官方的这张图已经做了很详尽的说明,这里不做赘述 但是看到session_key了吗?看到官方同时说的了吗所以问题又来了怎么保证session_key的有效性诚如上图要保证调用接口时后端session_key不失效,只能在每次调用前先使用wx.checkSession检查是否有效实践中也发现wx.checkSeesion非常耗时,大约200ms,所以也不能每次接口调用前都使用wx.checkSession检查是否有效同时要注意⚠️前端不能随便重新执行wx.login,因为可能导致正在进行的其它后端任务session_key失效天啦噜,怎么办?! 通过实践和偶然的发现——官方的示例代码 得知:在使用小程序期间session_key是不会失效的,so,你想到了什么?在每个请求前去校验有效性将校验有效性的结果存储起来通过async/await和刚才存储起来的结果来保证不过多调用wx.checkSession先问个问题:你准备用什么方式来存储校验的结果?。。。让思考先飞一会。。。。。。。。。。。。。。。。。。。。。。。。。。。storage吗?当然可以,不过不够完美,为什么?因为storage是永久的存储,而session_key的有效期却只是在使用小程序期间,所以你需要在小程序结束后手动重置该状态以重新校验其有效性,那是不是在app的onUnload里重置呢?不是!开发过小程序的应该都知道,那就是结束使用小程序的方式太多,不能保证每种方式都会触发onUnload,例如用户直接销毁了微信进程????(其实你也可以在app的onShow里搞)那用什么呢?直接用内存啊,借助内存的自动管理来智能管理,所以最终代码应该是这样的// doRequest.jslet wxSessionValid = null // 微信session_key的有效性// 带鉴权的请求封装async function doRequestWithCheckAuth() { … if (typeof wxSessionValid !== ‘boolean’) { wxSessionValid = await checkWxSession() // 检查微信session是否有效 } if (!wxSessionValid) { await reLogin() // 重新登录 } wxSessionValid = true // 重新登陆后session_key一定有效 …}这样是不是看起来比较完美了?嗯~不知道有没有同学着急问业务侧的session(自定义的登录态)怎么没讲?嗯,那现在讲吧怎么校验完整的认证体系其实很简单,都不想把它作为一部分来讲,但既然讲了就必然有我想强调的校验微信端的session_key略有麻烦,但不应该把它抛给服务端服务端不能直接校验session_key的有效性而是通过调用接口发现错误了才知道失效了,这是被动的服务端需要同时维护两个session而放在前端我们只需要校验两个session的有效性即可,任何一个失效就重新登录,这是积极主动有效的操作,应该被提倡贯通OK,基本上梳理的差不多了,就差弹登录按钮了,这个简单,调用刚才封装的组件的方法就行了嘛,bingo,可是,点完允许后呢?怎么继续用户的操作呢?怎么能让用户的体验不被打断呢?先回放下刚才reLogin的代码async function reLogin() { // 确保有用户信息 await new Promise(resolve => { // ⚠️注意开头有await!!! wx.getSetting({ success: (res) => { // 如果用户没有授权或者没有必要的用户信息 if (!res.authSetting[‘scope.userInfo’] || !_.isRealTrue(wx.getStorageSync(‘userInfoRes’).userInfo)) { navToLogin(resolve) // 去提示用户点击登录按钮,⚠️注意:并把当前的resolve带过去 } else { resolve() // 静默登录 } } }) }) return new Promise((resolve) => { wx.login({ success: res => { login(res.code).then((jwt) => { resolve(jwt) // resolve jwt }) // 通过code进行登录 }, fail(err) { wx.showToast({ title: err.errMsg, icon: ’none’, duration: 2000 }) } }) })}function navToLogin(resolve) { /* eslint-disable no-undef */ const pages = getCurrentPages() const page = pages[pages.length - 1] // 当前page page.openLoginModal(resolve) // 打开登录按钮弹框,并把当前的resolve带过去}上面的代码注释里有两个⚠️注意看到没?是的,通过回调的方式????当用户同意授权了就继续余下的逻辑,如果被拒绝了,则安利他,再拒绝就终止操作,下次需要授权也会继续弹出授权 有不明白欢迎评论留言指出,我再做说明修改完整源码以后会放出的,通过wepy搭建的一个框架 下一篇会讲api的封装谢谢 ...

November 16, 2018 · 1 min · jiezi

开发微信小程序必须要知道的事

为什么是小程序?为什么我们会开发小程序呢?或许是因为工作需要,或许是源于自己的追求(来自名利的欲望),但我要说——这是一种缘分,很美好的缘分,很多年后还值得庆幸的缘分小程序目前可以分为三个阶段一是语音和摇一摇(还有yue pao利器的传说)二是公众号,也就是这时注定了小程序的出现是历史的必然选择(shihouzhuge),为什么这么说呢?因为微信在开放了webview的同时加入了js-sdk的开发工具包,而这就是小程序的前身三就是当下的微信os,能跑小程序的微信我们继续来说说第二点,有了js-sdk不就可以了吗?不就可以打通微信了吗?还要什么小程序?!可是人家是有梦想的鹅厂啊!!?先从技术上说,js-sdk只是为传统网页提供包含微信api的开发工具包,并没有解决移动网页遇到的体验不良问题,所以小程序就做了资源离线存储,提高加载速度提供更强的开放能力通过构建组件系统实现对安全性的管控通过内置实现的组件提高开发速度和降低开发成本开放入口啊,不像网页只能通过链接打开加上其他XXX就搞出了比拟原生的体验(chadebushao)微信的梦想上说作为一个月活超10亿的超级app,人口红利已达天花板,所以现在开始打时长红利的主意,拓展微信的使用场景,拓宽微信的边界马化腾亲口说过:从消费互联网到产业互联网,随着产业互联网时代的到来,我们也在“连接产业”上寻求突破,而小程序就是连接产业互联网的“利器”(听不懂什么玩意)赚钱啊!鹅厂的梦想啊!做什么样的小程序?回到上面的引子,为什么值得庆幸?因为上面说了——能降低开发成本,能提高用户体验,能褥流量!能让每个人都有机会搞事情!!!搞什么呢?我也不知道,如果你有好的想法欢迎联系我????不过,这里分享下我的想法战线不能长!张小龙说过——“用完即走”,其实这不是原因,只是我调个书袋而已????但微信确实是这样啊,聊天、朋友圈,好像确实没什么太丧心病狂的一直拉着你不走(好像现在还有看一看?始终记住——鹅厂有梦想!)所以你做的东西要符合人家产品的思想!好吧,说个实际的场景,如果你要在小程序里的创作文章,那别人来一条信息你回不回?不回?你的思绪回不回被打乱?不会?你的心情会不会很烦?不会?????你赢了,来生再见,所以权衡好你的功能设计,战线越长越容易死短!平!快! 对应上面。就是快速进入主题,功能点一目了然,功能也尽量单一,该干什么尽快干什么!例如让用户分享,让用户pay,快!快!快!不犹豫,让用户尽快上车做矩阵,对应上面,如果你想把app的功能都搬到小程序上,那一定要做功能拆分,做多个小程序形成矩阵小程序os长什么样?想种一个小程序总先知道这片土地什么样吧?OK,欢迎来到小程序黑土地。。。首先我们先说说小程序用到的两个线程——渲染线程、脚本线程,与网页开发不同,这两个线程是分开的,分别运行在不同的线程中,而网页则是互斥的,也就是说视图和脚本被分开了,在不同的线程里,这就导致了和普通网页开发一个很大的不同——没有DOM API,而且也不是运行在浏览器里所以也没有BOM API我们再看图还发现小程序总共有三种运行环境,并且!每个环境两个线程还都不一样!所以同学们啊,在开发工具上行的在真机上不一定行啊!可别太天真了,一定要在真机上验证功能的可行性!我们有没有想过小程序为什么要费那么大功夫重造轮子?不直接用成熟的web技术?说是体验,其实我觉得最主要的是微信想管控一切,不是你想怎样就可以怎样,而是我让你怎样,你才能怎样(至今我还对此很不爽)所以就干掉了灵活的web,别被我带沟里——小程序并不是完全没有了web,实际上你看到的就是web,只是没有暴露出来,而是微信直接通过编译小程序来替你操作了既然是两个线程,那必然要通信啊,要协作完成任务,那怎么实现的呢?看下面的通信模型 没看到图也能先想到是这个样式????,这里提示几点上图中的Native是指微信客户端逻辑层发送网络请求也经由Native转发渲染层是由多个webview组成的,为什么?为了提供更好的交互体验呀,这样也更贴近原生体验,同时避免了单个WebView的任务过于繁重,同时导致了小程序的生命周期不容易被理解(下面带你理解)通信是有时间成本的,所以在开发中我们最好使用异步接口来看下生命周期 其实了解了渲染层是由多个webview组成的就很容易理解生命周期了navigatebBack是返回上一个webview,销毁当前的webviewnavigateTo是打开承载新页面的webview,同时保留老的webviewredirectTo是在当前webview里打开新的页面左下角有两张拼一起的图是switchTab的Tabbar页面初始化之后不会被销毁!所以Tabbar页面不会unLoad,更多请参阅图片有用请点赞????下一篇文章将讲讲小程序的登录态????

November 15, 2018 · 1 min · jiezi

微信小程序之店铺评分组件及vue中用svg实现的评分显示组件

在微信小程序中,有遇到要展示店铺评分,或者是订单完成后对商品进行评价,用到了星星展示,查了下,在微信中无法使用svg实现图片,微信中只能将svg图片转成base64来显示,所以是在vue中使用的svg来实现评分1.效果图微信中的可以点击及显示,但是,显示的话,在4.2分,4点多分的时候,显示的是半颗星vue中用的是svg实现,所以用的是占比的形式,可以有一点点的星星星图片2.微信实现店铺评分显示及商品评价星星展示子组件index.wxml,可以动态的控制星星的大小<!– (size * stars.length + (size/2) * 4 + 20 )这里的话,是在可以点击的时候,加上了好评的字体的长度 –><view class=‘starsBox’ style=‘width:{{isClick?(size * stars.length + (size/2) * 4 + 20 ):(size * stars.length)}}rpx;height:{{size}}rpx;’> <view class=‘stars’ style=‘width:{{size * stars.length}}rpx;height:{{size}}rpx;’> <block wx:for="{{stars}}" wx:key="{{index}}"> <image src="/images/{{item == 0 ? ‘grayStar’:item}}.png" style=‘width:{{size}}rpx;height:{{size}}rpx;’ data-index="{{index}}" catchtap=“computeScore”></image> </block> </view> <view wx:if="{{isClick}}" class=‘text’ style=‘font-size:{{size/2}}rpx;’> <text wx:if="{{value==‘0’}}" class=‘pointText’>暂无评分</text> <text wx:elif="{{value==‘1’}}" class=‘pointText’>差评</text> <text wx:elif="{{value<‘4’}}" class=‘pointText’>中评</text> <text wx:else class=‘pointText’>好评</text> </view></view>子组件index.wxss.starsBox{ display: flex; align-items: center; justify-content: flex-start;}.stars{ width: 150rpx; height: 50rpx; display: flex; align-items: center; justify-content: flex-start;}.stars image{ width: 30rpx; height: 30rpx;}.text{ color: #ccc; margin-left: 20rpx;}子组件index.jsComponent({ properties: { /* 显示有色星星的个数 / value: { type: Number, value: 0, / 监听value值的变化 / observer: function (newVal, oldVal, changedPath) { this.init() } }, / 设置星星大小 / size: { type: Number, value: 30 }, / 是否可点击,type为null表示值可以是任意类型 / isClick: { type: null, value: false } }, attached() { / 组件生命周期函数,在组件实例进入页面节点树时执行 / this.init(); }, data: { stars: [0, 0, 0, 0, 0] }, methods: { init() { let star = this.properties.value; let stars = [0, 0, 0, 0, 0]; / 图片名称,通过设置图片名称来动态的改变图片显示 / for (let i = 0; i < Math.floor(star); i++) { stars[i] = ‘star’; } if (star > Math.floor(star)) { stars[Math.floor(star)] = ‘halfStar’; } for (let i = 0; i < stars.length; i++) { if (stars[i] == 0) { stars[i] = ‘grayStar’; } } this.setData({ stars }) }, / 可点击时,用于计算分数 / computeScore(e) { let index = e.currentTarget.dataset.index; let isClick = this.data.isClick; if (isClick) { let score = index + 1; this.triggerEvent(‘compute’, { score }); } } }})3.父组件中引用父组件index.wxml<view class=“score”> <view class=“scoreItem”> <score value="{{shopGrade}}" size=“46” isClick=“true” bindcompute=“computeGrade” /> </view> <view class=“scoreItem”> <score value="{{shopGrade1}}" size=“46” /> </view></view>父组件index.json{ “usingComponents”: { “score”: “/component/score/index” }}父组件index.jsdata: { shopGrade: 0, shopGrade1: 4.6,},/ 评分处理事件 /computeGrade(e) { let score = e.detail.score; this.setData({ shopGrade: score })},4.vue中使用svg实现评分首先在vue使用的index.html的模板文件中添加一个rem转换算法,因为我后面用的单位是rem/ 在头部添加 /<script type=“text/javascript”> document.getElementsByTagName(“html”)[0].style.fontSize = 100 / 750 * window.screen.width + “px”; </script>然后添加svg.vue文件,这个svg文件可以自己找图片生成,并设置对应的id<template> <svg xmlns=“http://www.w3.org/2000/svg" xmlns:xlink=“http://www.w3.org/1999/xlink" style=“position:absolute;width:0;height:0;visibility:hidden”> <defs> <symbol id=“star” viewBox=“0 0 32 32”> <path class=“path1” d=“M16 26.382l-8.16 4.992c-1.5 0.918-2.382 0.264-1.975-1.435l2.226-9.303-7.269-6.218c-1.337-1.143-0.987-2.184 0.755-2.322l9.536-0.758 3.667-8.835c0.674-1.624 1.772-1.613 2.442 0l3.667 8.835 9.536 0.758c1.753 0.139 2.082 1.187 0.755 2.322l-7.269 6.218 2.226 9.303c0.409 1.71-0.485 2.347-1.975 1.435l-8.16-4.992z”> </path> </symbol> </defs> </svg></template><script></script><style></style>rating.vue文件引用svg.vue<template> <div class=“ratingstar”> <section class=“star_container”> <svg class=“grey_fill” v-for="(num,index) in 5” :key=“index”> <use xmlns:xlink=“http://www.w3.org/1999/xlink" xlink:href="#star”></use> </svg> </section> <div class=“star_overflow” :style="‘width:’+rating2/10+‘rem’"> <section class=“star_container”> <svg class=“orange_fill” v-for="(num,index) in 5” :key=“index”> <use xmlns:xlink=“http://www.w3.org/1999/xlink" xlink:href="#star”></use> </svg> </section> </div> <svgIcon></svgIcon> </div></template><script> import svgIcon from ‘@/components/svg’ export default { components: { svgIcon }, data() { return { rating: 4.2 } }, }</script><style lang=“less” rel=“stylesheet/less” scoped> .ratingstar { position: relative; width: 100%; .star_overflow { overflow: hidden; position: relative; height: 0.65rem; } .star_container { position: absolute; top: 0.05rem; width: 1rem; display: flex; justify-content: flex-start; align-items: center; .grey_fill { width: 0.94rem; height: 0.2rem; fill: #d1d1d1; } .orange_fill { width: 0.94rem; height: 0.2rem; fill: #ff9a0d; } } }</style>都有用到组件,可以查看下方的推荐文章中的购物车中的父子组件传值正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端实现单行文字及多行文字的省略号微信小程序之购物车和父子组件传值及calc的注意事项纯css实现瀑布流(multi-column多列及flex布局) ...

November 15, 2018 · 3 min · jiezi

「腾讯地图」微信小程序插件:提供简单的路线多方案规划服务

上期,我们在《「腾讯视频」微信小程序插件介绍》一文中介绍了「腾讯视频」小程序插件的意义、使用场景以及使用方法。今天我们会与大家分享一款同样优秀的小程序插件——「腾讯地图」插件,从使用场景到使用方法,都将作出详细的介绍。「腾讯地图」插件能做什么?顾名思义,「腾讯地图」插件由腾讯地图官方出品,旨在为开发者提供简单的路线多方案规划服务,可在插件中显示指定位置间的路线方案,支持驾车、公交、步行的路线规划能力及 ETA 等基础路线信息。如果你想像「腾讯地图+」小程序一样实现地图的基础功能,使用「腾讯地图」插件是你的最佳选择。「腾讯地图+」小程序截图作为首批推出的小程序插件,「腾讯地图」插件经过了多个版本的优化,从最初的显示目标位置信息以及提供附近地图功能,到现在已经支持路线规划等能力,地图的功能几乎已经全部配备齐全。「腾讯地图」插件的使用场景场景一:收到小程序的婚礼请柬,但是请柬上的地址找不到?怎么办?如果你开发的是请柬邀请类的小程序,就会遇到上述场景。在传统开发模式中,引入完整的地图选点、路线规划组件,开发成本非常高,更多开发者选择让用户直接输入文字地址进行展示,以此作为降低开发成本的妥协方案。这样的设计不可点击,更没有路线规划的能力,用户还需手动输入去查询地址和交通路线。传统请柬 不可交互但如果开发者选择使用腾讯地图提供的同名小程序插件,开发成本将大幅降低,用户体验也能直线上升。我们在这里以婚庆请柬小程序为例进行说明:用户在编辑请柬小程序的过程中,提前设置好婚礼举办地点;当婚礼宾客收到请柬,点击地点,「腾讯地图」插件就能根据其宾客当前位置和目的地坐标,自动生成精准的导航路线。一键导航 简洁明了场景二:会议服务小帮手——提前了解参会路线与会者应该如何从高铁站、机场、火车站前往会议地点,一直都是各类会议邀请的必备内容。但长期以来,此类信息都习惯以纯文字形式进行发布,体验上存在不便理解、记忆难的问题。如果小程序能够使用「腾讯地图」插件,这类场景的体验将发生质的改变:会议组织方在小程序中提前设置多组起终点(如:机场 - 会议中心、高铁站 - 会议中心),与会者收到会议邀请后点击指定线路,就能在地图插件中查看到精确的参会路线。这样是不是比枯燥的文字多了几分智能呢?如何接入「腾讯地图」插件?「腾讯地图」的功能强大,使用起来却十分简单:1.申请使用插件。在「小程序管理后台 - 设置 - 第三方服务 - 插件管理」中查找插件名称「腾讯地图」(目前最新版本:1.0.6,appid:wx5bc2ac602a747594),并申请使用。2.引入插件代码后修改配置文件 JSON:{ “usingComponents”: { “map-route”: “plugin://myPlugin/mapRoute” }}3.使用地图插件。在相应的 WXML 文件中添加以下标签:<map-route route-info="{{routeInfo}}"></map-route>4.最后,按需求在 JS 文件中处理传入插件数据,数据包括:起点,终点经纬度及名称,路线算路方式,封装在 routeInfo 结构中:let plugin = requirePlugin(“myPlugin”)let routeInfo = { startLat: 39.90469, // 起点纬度 选填 startLng: 116.40717, // 起点经度 选填 startName: “我的位置”, // 起点名称 选填 endLat: 39.94055, // 终点纬度必传 endLng :116.43207, // 终点经度 必传 endName:“来福士购物中心”, // 终点名称 必传 mode:“car” //算路方式 选填}Page({ data: { routeInfo: routeInfo }})参数说明:起点:不填写或 startName=“我的位置” 或 startName=“当前位置” 或 startName=“currentLocation” 则插件会获取当前的定位位置作为起点位置发起算路,若正确填写起点信息,则以传入的起点信息发起算路。终点:必传参数,不正确传入则不会发起算路或者算路失败。算路方式:mode,目前支持三种算路方式,分别是:驾车(car),公交(bus),步行(walk);不传则默认发起驾车算路。注意:数据要在 data 中初始化,不要在 onLoad 中直接 setData,因为 onLoad 中直接 setData,properties 的 routeInfo 的 observer: function (newVal, oldVal),newVal 接收不到参数,导致参数报错,之后可以通过 bind 其他事件 setData 更新 routeInfo 达到变更起终点参数的效果。「腾讯地图」插件使用效果图如果你想了解更多「腾讯地图」插件详情,欢迎访问开发者社区插件版块相应页面(建议电脑访问):????https://developers.weixin.qq….手机端用户也可以扫码访问 ????内容来自:微信开放社区《小程序·小故事》栏目原作者:熊明钧任何问题,欢迎前往微信开放社区:https://developers.weixin.qq.com了解更多小程序开发相关内容,欢迎微信扫描下方二维码关注微信极客WeGeek公众号,共筑微信生态。 ...

November 14, 2018 · 1 min · jiezi

SegmentFault 社区上线小程序开发频道,助力小程序开发者生态

2016 年 9 月 21 日,微信发布小程序内测,11 月 3 日对开发者公测,2017 年 1 月 9 日微信小程序正式面向 C 端用户开放。作为腾讯生态中的重要一环,微信小程序的开发浪潮席卷了整个互联网圈,它以一种轻应用、高体验的方式实现了应用 “触手可及” 的梦想。截至 2018 年 7 月 11 日,小程序数量达到 100 万,小程序开发者超过 150 万人,覆盖了 200 多个细分行业,日活用户达到 2 亿。在微信小程序还在内测时,SegmentFault 就与爱范儿合作在广州举办过第一届民间的小程序黑客马拉松。2016 年国庆节期间在广州微信总部的园区举办的的小程序黑客马拉松。在 2018 年 7、8 月,百度和支付宝也相继推出了自己的小程序平台。百度小程序主要是依于托百度App超级内容生态,通过百度的流量能力和 AI 的能力提供赋能。正式上线后的支付宝小程序把定位锁定在商业和生活服务领域,它的使命是让支付宝从支付工具走向能提供各种服务的场景平台。越来越多的商业机会和生态也在小程序中发生。在 2018 年 12 月份,腾讯微信事业群也联合 SegmentFault 在北京正式启动「微信小程序WeGeek Hackathon」本次黑客马拉松面向全球小程序开发者、爱好者,旨在通过微信小程序平台进行小程序的创新开发,共同建设小程序生态!同时微信官方运营的小程序开发者服务平台 - 「微信极客WeGeek」也正式入驻 SegmentFault ,推荐各位社区开发者关注。借助此次机会,SegmentFault 也正式上线小程序开发频道,助力小程序开发者生态,在首页的小程序频道,开发者可以更快速的获取小程序相关的技术内容和资讯。也欢迎大家贡献更多小程序相关的优质内容,大家也可以通过关注小程序标签,更快速的获得相关内容的推荐。相关链接SegmentFault 小程序开发频道微信小程序WeGeek Hackathon 大赛官网微信小程序开发文档支付宝小程序开发文档百度智能小程序开发文档

November 14, 2018 · 1 min · jiezi

微信小程序黑客马拉松即将开始,来做最酷的 Mini Program Creators!

微信小程序黑客马拉松正式启动近日,小程序斩获一项世界级殊荣——作为一项全新的技术和应用创新,小程序首次获选世界互联网领先科技成果。目前小程序应用数量已超过 100 万,覆盖了 200 多个细分行业,日活用户达到 2 亿。微信小程序已经构建了一个完整的开发环境和开发者生态,有超过 150 万开发者进入小程序生态,在 2017 年带动就业 104 万,社会效应不断提升。小程序走进了高校计算机课堂,新职业「小程序员」也应运而生。小程序的蓬勃发展也使得「小程序人文化创业」兴起,小程序的技术难度降低,更多非技术人才加入,掀起了小程序全民创业热潮。正如马化腾作为腾讯公司代表上台领奖时说到:过去开发软件,我们常常要考虑不同开发环境的语言、设备的适配性和成本。现在,开发者可以在一个「类操作底层」去开发应用,打破了过去受限的开发环境,为「跨系统开发」这个世界难题给出了中国的解决方案,小程序创业者们迎来了新的机遇。由腾讯公司微信事业群主办、SegmentFault 承办的「微信小程序WeGeek Hackathon」(黑客马拉松)将于 2018 年 12 月 15 日 - 12 月 16 日在北京正式启动,本次黑客马拉松面向全球小程序开发者、爱好者,旨在通过微信小程序平台进行小程序的创新开发,共同建设小程序生态!做最酷的 Mini Program Creators本次黑客马拉松的主题是 Mini Program Creators,我们将召集最富有创造力的 开发者、产品经理、设计师,在 48 小时内 运用微信小程序框架,打造出一款 足够好玩、足够创新 的小程序。具体题目会分为多个方向,并在大赛前一周正式公布。黑客马拉松自诞生起就一直是开发者、设计师、创业团队的狂欢盛宴。小程序因其结构简单、搭建快速、开发成本低等特点,让两天内的快速开发成为了可能。在这场黑客马拉松的规定时间里,你可以充分发挥创造力,并与团队成员高度配合,从 0 开始创造出一个全新的产品,收获一个能够实现自己想法的小程序。你们准备好接受小程序的挑战了吗?加入微信小程序黑客马拉松,来做最酷的 Mini Program Creators!奖项设置大赛共设一、二、三等奖各一名,获奖团队将获得大赛奖金。一等奖 ¥102420二等奖 ¥102416三等奖 ¥1024*10此外,凡在规定时间内完成作品的参赛团队均可以获得微信大礼包一份,更多奖品内容持续公布中。参与对象本次黑客马拉松面向全国职业开发者、高校学生等小程序爱好者,同时还将招募部分产品经理和 UI 设计师,与开发者自由组队参赛。参赛者以个人身份报名,以团队身份参赛的每个成员都要单独报名。报名后官方会对报名人员进行筛选。团队规模每个团队 1-5 人,团队最高人限 5 人,一人只能参与一个团队。现场组队参赛者可提前组队,也可到现场自由组队。现场设有白板和便利贴,参赛者可在便利贴上写明自己擅长的技能、想要担任的团队角色,以便大家更快组队。活动时间与地点???? 2018年12月15日(周六)09:00至2018年12月16日(周日)17:00???? 北京腾讯众创空间(回龙观)报名参与微信扫描下方二维码关注微信极客WeGeek公众号,了解WeGeek Hackathon最新信息

November 13, 2018 · 1 min · jiezi

WeGeek | WePY 开源框架

今天前来专栏分享的极客,是腾讯微信支付团队。小程序公测一个月时,微信支付团队开源了小程序上的组件化开发框架 WePY,在 Github 上一经发布便受到了众多开发者的追捧,网上搜索「微信小程序 WePY 开源框架资源汇总」尽是网友们自发分享的相关干货。尽管 WePY 开源框架如今倍受推崇,回忆起开源的初衷,来自微信支付团队的 Gcaufy 还是表示:「WePY 开源框架的对外开源并不是要去分享一个很成功的解决方案,而是我认为这套方案能够解决在小程序开发中遇到的一些实际问题,并且希望能借助外界的力量去帮助一起完善这套方案。」接下来,让我们一起看看 WePY 开源框架背后的开发故事吧。如果你有一定的开发经验,会明显感受到小程序的开发十分容易上手——小程序本身提供一些特性如:模块化,模板,数据绑定等,极大地方便了习惯使用 MVVM 框架的用户。但同时,由于运行环境有限,小程序尚不能使用市面上的流行框架。在几个月的开发历程里,作者 Gcaufy 便一直希望可以设计出一套方案,更大可能地让小程序开发贴近于当下开发习惯,WePY 开源框架应运而生。WePY 开源框架的原理很简单:通过 WePY 开源框架开发的代码在经过编译后,能生成一份完美运行在小程序端的代码,使得小程序开发更贴近于传统 H5 框架开发,可以像开发 H5 一样支持引入 npm 包,并且支持组件化开发以及支持 JS 新特性等,实现类 Vue 的开发体验。WePY 开源框架实现小程序的预加载我们知道,传统的 H5 可以通过预加载来提升用户体验,那么小程序能够实现吗?答案是肯定的。在小程序中使用预加载,比在 H5 中实现起来更为简单方便,但也更容易被开发者忽视。传统 H5 在启动时,page1.html 只会加载 page1.html 的页面与逻辑代码,当 page1.html 跳转至 page2.html 时,page1 所有的 Javascript 数据将会从内存中消失。在 page1 与 page2 之间的数据通信只能通过 URL 参数传递或者浏览器的 cookie,localStorge 存储处理。而小程序在启动时,会直接加载所有页面逻辑代码进内存。即便 page2 可能都不会被使用,在 page1 跳转至 page2 时,page1 的逻辑代码 Javascript 数据也不会从内存中消失。page2 甚至可以直接访问 page1 中的数据。小程序的这种机制差异正好可以更好的实现预加载。通常情况下,我们习惯将数据拉取写在 onLoad 事件中。但是小程序的 page1 跳转到 page2,到 page2 的 onLoad 存在一个 300ms ~ 400ms 的延时。如下图:因为小程序的特性,完全可以在 page1 中预先拿取数据,然后在 page2 中直接使用,这样就可以避开 redirecting 的 300ms ~ 400ms了。如下图:对于上述问题,WePY 开源框架中封装了两种概念去解决:预加载数据用于小程序中 page1 主动传递数据给 page2,比如 page2 需要加载一份耗时很长的数据。我可以在 page1 闲时先加载好,进入 page2 时直接就可以使用。预查询数据用于避免于 redirecting 延时,在跳转时调用 page2 预查询。WePY 开源框架添加了 onPrefetch 事件,会在 redirect 之时被主动调用,这一改进扩展了生命周期;同时 onLoad 事件也添加了一个参数,用于接收预加载或者是预查询的数据:// params// data.from: 来源页面,page1// data.prefetch: 预查询数据// data.preload: 预加载数据onLoad (params, data) {}WePY 开源框架实现小程序的数据优化可能有开发者还不了解,其实小程序的视图层与逻辑层是完全分离的,这二者之间的通信全都依赖于 WeixinJSBridge 实现。如:开发者工具中是基于 window.postMessageiOS 中基于 window.webkit.messageHandlers.invokeHandler.postMessageAndroid 中基于 WeixinJSCore.invokeHandler数据绑定方法 this.setData() 亦然,于是很容易想到,频繁的数据绑定会不会导致通信的成本大大增加呢?为了验证 setData() 存在性能问题,微信支付团队做了一个相关测试:动态绑定 1000 条数据的列表进行性能测试,主要针对以下三种情况:最优绑定: 在内存中添加完毕后最后执行 setData() 操作。最差绑定: 在添加一条记录执行一次 setData() 操作。最智能绑定:不管中间进行了什么操作,在运行结束时执行一次脏检查,对需要设置的数据进行 setData() 操作。经过十次刷新运行测试后得出了以下结果:从测试结果可以明显看出,实现同样的逻辑,性能数据却相差 40 倍左右。通过分析测试结果,我们可以得知,在开发过程中,应当尽量避免同一流程内多次 setData() 操作。那么有什么优化方式呢?采取人工维护肯定能够实现,但当页面逻辑负责起来之后,即便花很大的精力去维护也不一定能保证每个流程只存在一次 setData(),可维护性也不高。因此,WePY 开源框架选择了使用脏检查去做数据绑定优化。虽然 WePY 开源框架在语法上借鉴了 Vue,原理则是完全不同的。比如 WePY 开源框架使用的是 ng 的脏检查设计,而不是使用的 Vue 的 getter,setter 等。用户不用再担心在流程里,数据被修改了多少次,只用在流程最后做一次脏检查,并且按需执行 setData() 即可。使用 WePY 开源框架在开发效率上的提升除了上述基于性能上做出的优化以外,WePY 开源框架也作出了一系列开发效率上的优化。支持丰富的编译器js 可以选择用 Babel 或者 TypeScript 编译。wxml 可以选择使用 Pug(原Jade)。wxss 可以选择使用 Less、Sass、Styus。支持丰富的插件处理可以通过配置插件对生成的js进行压缩混淆,压缩图片,压缩 wxml 和 json 已节省空间等等。支持 ESLint 语法检查添加一行配置就可以支持 ESLint 语法检查,可以避免低级语法错误以及统一项目代码的风格。生命周期优化WePY 开源框架添加了 onRoute 的生命周期。用于页面跳转后触发。 这一优化项是因为小程序中并不存在一个页面跳转事件。onShow 事件可以用作页面跳转事件,但同时也存在负作用,比如按 HOME 键后切回来,或者拉起支付后取消,拉起分享后取消都会触发 onShow 事件。优化事件传参原有的传参写法:<view data-alpha-beta=“1” data-alphaBeta=“2” bindtap=“bindViewTap”> DataSet Test </view>Page({ bindViewTap:function(event){ event.target.dataset.alphaBeta === 1 // - 会转为驼峰写法 event.target.dataset.alphabeta === 2 // 大写会转为小写 }})优化后:<view @tap=“bindViewTap(“1”, “2”)"> DataSet Test </view>methods: { bindViewTap(p1, p2, event) { p1 === “1”; p2 === “2”; }}更多详情可以参看 WePY 开源框架文档:https://tencent.github.io/wep…。WePY 开源框架2.0 计划目前 WePY 开源框架主要由微信支付团队内相关人员利用业余时间,与几个外部贡献者一起来维护。技术社区中有不少热心的贡献者,不仅自己参与,也会带来了一些新的贡献者力量,不时还会提供一些比较核心的功能。当被问及「WePY 开源框架 2.0 进度」问题时,微信支付团队表示现已将进入了 WePY 2.0 内测阶段,相信不久后就将与大家正式见面。「希望 2.0 是一个全新的,对得起开发者的版本。」了解更多小程序开发相关内容,欢迎微信扫描下方二维码关注微信极客WeGeek公众号,共筑微信生态。 ...

November 13, 2018 · 2 min · jiezi

微信小程序之购物车和父子组件传值及calc的注意事项

在做微信小程序时,觉得小组里对购物车的实现不是很完美,就自己尝试的写了下,然后用到了父子组件传值,父子组件传值的话,和vue框架上是非常相似的,以及calc这个css函数,calc有个注意点,自己不怎么用,一时间有差点忘了,这里记录下1.效果图2.子组件实现要实现图中删除的效果,使用组件的形式更好做点,我当时本想直接在pages里实现,不过结果就是,滑动时,所有的商品都显示了删除按钮,除非用数组将每个商品要移动的距离存储起来,不过这样的话就很麻烦,所以我也是用组件来实现的关于微信组件,可以直接点击链接访问官网查看自定义组件子组件index.wxml<view class=“commodityItem” bindtouchstart=“handleTouchStart” bindtouchmove=“handleTouchMove” style=“transform:translateX({{-rightSpace}}px)"> <view class=“selectedBtn” bindtap=“handleSelect” data-is-selected=”{{commodity.isselected}}"> <view class=“noSelected” wx:if="{{commodity.isselected==0}}"></view> <image class=“selectedImg” wx:else src="/images/selected.png"></image> </view> <view class=“commodityInfo”> <view class=“commodityImg”> <image src="{{commodity.image}}"></image> </view> <view class=“commodityTitle”> <view class=“title”>{{commodity.title}}</view> <view class=“standard”>规格:{{commodity.standard?commodity.standard:‘无’}}</view> <view class=“count”> <view class=“price”>¥{{commodity.price}}</view> <view class=“commodityNum”> <i-input-number value="{{selectedNum}}" min=“1” max="{{commodity.stock}}" bindchange=“numChange” /> </view> </view> </view> </view> <view class=“deleteBtn”> <image class=“deleteImg” src="/images/delete.png"></image> <text class=“deleteText”>删除</text> </view></view>子组件index.wxss/* 商品 /.commodityItem{ display: flex; position: relative; padding: 10rpx 24rpx 20rpx 30rpx; box-sizing: border-box; background: #fff; transition: all .5s;}/ 选择按钮 /.selectedBtn{ display: flex; align-items: center; width: 80rpx;}.noSelected{ width: 46rpx; height: 46rpx; border-radius: 50%; border: 1px solid #ef5225;}.selectedBtn .selectedImg{ width: 50rpx; height: 50rpx;}/ 商品信息 /.commodityInfo{ display: flex; width: calc(100% - 80rpx);}.commodityImg{ margin-right: 18rpx; width: 220rpx; height: 220rpx;}.commodityImg image{ width: 100%; height: 100%; vertical-align: middle; }/ 商品title /.commodityTitle{ width: calc(100% - 220rpx);}.title{ display: -webkit-box; width: 100%; height: 70rpx; line-height:35rpx; font-size: 24rpx; font-weight:600; overflow: hidden; -webkit-line-clamp: 2; -webkit-box-orient: vertical;}.standard{ padding-top: 16rpx; width: 100%; height: 90rpx; box-sizing: border-box;}.count{ display: flex; align-items: center; justify-content: space-between; width: 100%; height: 60rpx;}/ 删除按钮 /.deleteBtn{ display: flex; position: absolute; width: 70px; height: 100%; top: 0rpx; right: -70px; flex-direction: column; align-items: center; justify-content: center; background: #ef5225;}.deleteImg{ margin-bottom: 10rpx; width: 50rpx; height: 50rpx; vertical-align: middle;}.deleteText{ color: #fff;}子组件index.json,这里用了iview中的数字输入框{ “component”: true, “usingComponents”: { “i-input-number”: “/component/iview/input-number/index” }}子组件index.jsComponent({ properties: { commodity: Object, }, data: { touchStart: null, rightSpace: 0, selectedNum: 1, }, methods: { / 商品是否选中 / handleSelect() { let selectedNum = this.data.selectedNum; let commodity = this.data.commodity; if(commodity.isselected == 0) { commodity.isselected = 1; } else { commodity.isselected = 0; } this.triggerEvent(‘handleselect’, { commodity, selectedNum}) }, / 处理触摸滑动开始 / handleTouchStart(e) { / 记录触摸滑动初始位置 / let touchStart = e.changedTouches[0].clientX; this.setData({ touchStart }) }, / 处理触摸滑动 / handleTouchMove(e) { console.log(e) let moveSpace = e.changedTouches[0].clientX; let touchStart = this.data.touchStart; if (touchStart != null) { if (moveSpace - touchStart > 70) { this.setData({ touchStart: null, rightSpace: 0 }) } else if (moveSpace - touchStart < -70) { this.setData({ touchStart: null, rightSpace: 70 }) } } }, numChange(e) { let selectedNum = e.detail.value; let commodity = this.data.commodity; this.setData({ selectedNum }) this.triggerEvent(‘handleselect’, { commodity, selectedNum}) } }})3.父组件实现父组件index.wxml,这里用的是假数据,所以操作上会有一些是联调时不必要的操作<view class=“cart”> <view class=“item” wx:for="{{cartList}}" wx:key="{{items.shopid}}" wx:for-item=“items”> <view class=“storeInfo”> <image class=“avatar” src="{{items.logo}}"></image> <view class=“storeName”>{{items.shopname}}</view> </view> <view class=“discount”>满¥100包邮,满10件包邮</view> <view class=“commodity” wx:for="{{items.commodity}}" wx:key="{{item.id}}"> <cart-item commodity="{{item}}" bind:handleselect=“handleSelect” /> </view> </view> <view class=“count”> <view class=“selectAll” bindtap=“handleSelectAll”> <view class=“noSelected” wx:if="{{!isSelectedAll}}"></view> <image class=“selectedImg” wx:else src="/images/selected.png"></image> <text class=“selectAllText”>全选</text> </view> <view class=“countPrice”> <text>合计:</text> <text>¥{{countPrice}}</text> </view> <view class=“account”> <text>结算</text> <text>({{countSelectedNum}})</text> </view> </view></view>父组件index.wxsspage{ background: #f8f8f8;}.cart{ padding-bottom: 100rpx; font-size: 26rpx;}.item{ border-bottom: 1px solid #eee;}/ 头部店铺信息 /.storeInfo{ display: flex; padding: 18rpx 0rpx 18rpx 30rpx; background: #fff; box-sizing: border-box;}.storeInfo .avatar{ width: 56rpx; height: 56rpx; border-radius: 50%; vertical-align: middle;}.storeInfo .storeName{ margin-left: 16rpx; line-height: 56rpx;}/ 包邮信息 /.discount{ padding-left: 30rpx; height:50rpx; line-height: 50rpx; font-size:20rpx; color: #666; box-sizing: border-box;}/ 底部操作 /.count{ display: flex; position: fixed; padding-left: 30rpx; bottom: 0; left: 0; width: 100%; height: 100rpx; line-height: 100rpx; box-sizing: border-box; color: #232323; background: #eee;}/ 全选 /.selectAll{ display: flex; padding-right: 20rpx; align-items: center; width: 25%; font-size: 30rpx;}.selectAll .noSelected{ width: 46rpx; height: 46rpx; border-radius: 50%; border: 1px solid #ef5225;}.selectAll .selectedImg{ width: 50rpx; height: 50rpx;}.selectAllText{ margin-left: 18rpx;}.countPrice{ position: absolute; top: 0; right: 270rpx; height: 100%; line-height: 100rpx; text-align: center; font-size: 30rpx;}.countPrice text{ margin-right: 15rpx;}.account{ position: absolute; top: 0; right: 0; width: 270rpx; height: 100%; line-height: 100rpx; text-align: center; font-size: 30rpx; background: #ef5225; color: #fff;}父组件index.json,引用子组件{ “usingComponents”: { “cart-item”: “/component/cart/index” }}父组件index.jsPage({ data: { cartList: [ { shopname: ‘猫咪小店’, logo: ‘/images/avatar.jpeg’, shopid: 11, commodity: [ { id: 1, image:’/images/commodity.jpg’, title: ‘雅诗兰黛鲜活焕亮红石榴晚霜50ml 补水保湿 滋润排浊’, standard: ‘111 + 黑色’, price: ‘100’, stock: 10, quantity: 1, isselected: 0, }, { id: 2, image:’/images/avatar7.jpg’, title: ‘雅诗兰黛鲜活焕亮红石榴晚霜50ml 补水保湿 滋润排浊’, price: ‘10’, stock: 5, quantity: 1, isselected: 0, } ] }, { shopname: ‘猫咪小店’, logo: ‘/images/avatar5.jpg’, shopid: 450, commodity: [ { id: 3, image:’/images/commodity.jpg’, title: ‘雅诗兰黛鲜活焕亮红石榴晚霜50ml 补水保湿 滋润排浊’, price: ‘90’, stock: 10, quantity: 1, isselected: 0, }, { id: 4, image:’/images/avatar7.jpg’, title: ‘雅诗兰黛鲜活焕亮红石榴晚霜50ml 补水保湿 滋润排浊’, price: ‘100’, stock: 5, quantity: 1, isselected: 0, }, { id: 5, image:’/images/commodity.jpg’, title: ‘雅诗兰黛鲜活焕亮红石榴晚霜50ml 补水保湿 滋润排浊’, standard: ‘111 + 黑色’, price: ‘100’, stock: 2, quantity: 1, isselected: 0, } ] }, { shopname: ‘猫咪小店’, logo: ‘/images/avatar.jpeg’, shopid: 550, commodity: [ { id: 6, image:’/images/avatar8.jpg’, title: ‘雅诗兰黛鲜活焕亮红石榴晚霜50ml 补水保湿 滋润排浊’, standard: ‘111 + 黑色’, price: ‘100’, stock: 1, quantity: 1, isselected: 0, } ] }, ], / 商品是否全选中 / isSelectedAll: false, / 已选中商品的价格 / countPrice: 0, / 统计所有选中的商品数量 / countSelectedNum: 0, }, / 处理商品选中 / handleSelect(e) { let countPrice = 0; let countSelectedNum = 0; let cartList = this.data.cartList; let length = cartList.length; / 因为是假数据,所以需要循环查找到对应的数据将其替换 / for(let i = 0; i < length; i++) { for(let j = 0; j < cartList[i].commodity.length; j++) { if (cartList[i].commodity[j].id == e.detail.commodity.id) { cartList[i].commodity[j] = e.detail.commodity; cartList[i].commodity[j].selectedNum = e.detail.selectedNum; } if (cartList[i].commodity[j].isselected == 1) { / 点击选中的时候,计算价格,要判断下设置的商品选中数量, * 我这里的是对点击了的商品才设置了选中的数量,所以需要对没有点击的商品数量设置为1,然后就默认的加一 / if (cartList[i].commodity[j].selectedNum != undefined) { countPrice += cartList[i].commodity[j].price * cartList[i].commodity[j].selectedNum; countSelectedNum += cartList[i].commodity[j].selectedNum } else { countPrice += cartList[i].commodity[j].price * 1; countSelectedNum += 1; } } } } / 对是否全选中进行判断 / let isSelectedAll = true; for (let i = 0; i < length; i++) { for (let j = 0; j < cartList[i].commodity.length; j++) { / 若商品中的isselecetd有为0的就终止循环,直接设置为未全选 / if (cartList[i].commodity[j].isselected == 0) { isSelectedAll = false; break; } } } this.setData({ cartList, isSelectedAll, countPrice, countSelectedNum }) }, / 全选中商品 / handleSelectAll() { let isSelectedAll = !this.data.isSelectedAll; let cartList = this.data.cartList; let length = cartList.length; let countPrice = 0; let countSelectedNum = 0; / 遍历数据中的isselected来进行全选的操作 / for(let i = 0; i < length; i++) { for (let j = 0; j < cartList[i].commodity.length; j++) { if(isSelectedAll) { cartList[i].commodity[j].isselected = 1; / 全选的时候,计算价格,要判断下设置的商品选中数量, * 我这里的是对点击了的商品才设置了选中的数量,所以需要对没有点击的商品数量设置为1,然后就默认加一 / if (cartList[i].commodity[j].selectedNum != undefined) { countPrice += parseInt(cartList[i].commodity[j].price) * cartList[i].commodity[j].selectedNum; countSelectedNum += cartList[i].commodity[j].selectedNum; } else { countPrice += cartList[i].commodity[j].price * 1; countSelectedNum += 1; } } else { cartList[i].commodity[j].isselected = 0; } } } this.setData({ isSelectedAll, cartList, countPrice, countSelectedNum }) },})4.父子组件传值较常用的都是父组件往子组件传值,所以子组件往父组件传值就会不是很熟悉我这里的话,是因为用的假数据,在点击商品选中或者不选中时,需要改变商品里的选中属性,所以用到了子组件往父组件传值,也包括传递选中的商品数量子组件往父组件传值的话,是通过在调用this.triggerEvent()来实现的/ 在父组件中定义方法:bind:handleselect或者也可以直接写成bindhandleselect*/<cart-item commodity="{{item}}" bind:handleselect=“handleSelect” />在子组件中调用this.triggerEvent(‘handleselect’, { commodity, selectedNum})这个this.triggerEvent(‘handleselect’, { commodity, selectedNum })方法中,handleselect的名称要与父组件中引用子组件时绑定的方法名称一样,后面的对象就是传递的值,也可以直接是以直接量的形式传递,然后再父组件中通过e.detail来获取对应的值handleSelect(e) { console.log(e.detail) console.log(e.detail.commodity) console.log(e.detail.selectedNum)}5.calc的注意事项我以前也遇到过,然后现在再用的时候,一时间把这点给忘了,在看到编译器样式的时候,才猛然想起.user-content{ padding: 10px 0 10px 50px; width: calc(100% - 50px); /* 计算宽度,’+‘或’-‘符号前后有空格 / height: 18px;}css中使用calc可以进行简单的运算:单位可以是百分比,px,rem,em等单位使用"+","-","","/“运算符(使用”+“或者”-“符号时,符号前后必须加上空格)在Firefox浏览器上使用要加上-moz前缀chrome浏览器上使用要加上-webkit前缀(使用”+“或者”-“符号时,符号前后必须加上空格)6.部分想法其实在样式上还是挺快就完成了,就是在计算商品价格的时候,想了挺久在计算价格时,当时就有点蒙圈,总是想着要怎么判断他是增加数量还是减少数量,然后就陷入死循环的之中。其实不用想她是增加还是减少数量,因为你都是传的是商品的数量,而且在计算时,也是判断了商品是否选中,所以,直接点,计算价格乘以数量就可以了然后选中的商品数量的统计就和计算价格的思路是一样的了正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端css实现波浪线及立方体微信小程序中遇到的多规格问题(一)实现单行及多行文字省略号 ...

November 13, 2018 · 5 min · jiezi

微信小程序黑客马拉松即将开始,来做最酷的 Mini Program Creators!

微信小程序黑客马拉松正式启动小程序一开始就以「轻应用」快速切入应用场景,让大家摆脱手机上安装大量 APP 的情景。作为移动互联网的头部产品微信服务于生活的方方面面,小程序又使得普通用户可以快速完成 APP 上的「高频」生活服务。早期的有赞、拼多多等社交电商在微信高流量中快速发展壮大,「国民 APP」拼多多尽管饱受诟病,在「社区团购」中也让拼团模式再次兴起。电商依托于微信生态的社交裂变,在短时间内低成本快速获取用户并完成转化,小程序创业者们迎来了新的机遇。如今,微信小程序的开发浪潮已席卷了整个互联网。截止 2018 年 7 月 11 日,小程序数量达到 100 万,小程序开发者超过 150 万人。由腾讯公司微信事业群主办、SegmentFault 承办的微信小程序黑客马拉松将于 2018 年 12 月 15 日 - 12 月 16 日在北京正式启动,本次黑客马拉松面向全球小程序开发者、爱好者,旨在通过微信小程序平台进行小程序的创新开发,共同建设小程序生态!做最酷的 Mini Program Creators本次黑客马拉松的主题是 Mini Program Creators,我们将召集最富有创造力的开发者、产品经理、设计师,在 48 小时内运用微信小程序框架,打造出一款足够好玩、足够创新的小程序。具体题目会分为多个方向,并在大赛前一周正式公布。黑客马拉松自诞生起就一直是开发者、设计师、创业团队的狂欢盛宴。小程序因其结构简单、搭建快速、开发成本低等特点,让两天内的快速开发成为了可能。在这场黑客马拉松的规定时间里,你可以充分发挥创造力,并与团队成员高度配合,从 0 开始创造出一个全新的产品,收获一个能够实现自己想法的小程序。你们准备好接受小程序的挑战了吗?加入微信小程序黑客马拉松,来做最酷的 Mini Program Creators!奖项设置大赛共设一、二、三等奖各一名,获奖团队将获得大赛奖金。奖项奖金一等奖¥102420二等奖¥102416三等奖¥1024*10此外,凡在规定时间内完成作品的参赛团队均可以获得微信大礼包一份,更多奖品内容持续公布中。参与对象本次黑客马拉松面向全国职业开发者、高校学生等小程序爱好者,同时还将招募部分产品经理和 UI 设计师,与开发者自由组队参赛。参赛者以个人身份报名,以团队身份参赛的每个成员都要单独报名。报名后官方会对报名人员进行筛选。团队规模每个团队 1-5 人,团队最高人限 5 人,1 人只能参与 1 个团队。现场组队参赛者可提前组队,也可到现场自由组队。现场设有白板和便利贴,参赛者可在便利贴上写明自己擅长的技能、想要担任的团队角色,以便大家更快组队。时间地点时间:2018 年 12 月 15 日 星期六 09:00 - 2018 年 12 月 16 日 星期日 17:00地点:北京腾讯众创空间(回龙观)报名参赛更多大赛详情及报名请前往: WeGeek Hackathon 2018 官网Hackathon 参赛指南Hackathon 参赛指南——如何愉快地参加一场 HackathonHackathon 交流欢迎扫描下方二维码加入 WeGeek Hackathon 交流群,发送“参加小程序黑客马拉松”添加管理员好友,即可入群和所有参赛者一起交流讨论。管理员二维码:志愿者招募本次微信小程序黑客马拉松公开招募志愿者,在这两天中,志愿者将肩负重要的责任,希望通过我们共同的努力,让所有热爱黑客马拉松、热爱微信小程序的朋友度过一个愉快的周末!申请成为志愿者 ➜说明:志愿者报名截止日期为 2018 年 12 月 9 日(人数招满将立即截止)。 ...

November 9, 2018 · 1 min · jiezi

微信小程序中遇到的多规格问题(一)

刚进入公司的时候就有遇到过多规格的问题,当时就觉得很麻烦,就只是看了下,没有尝试,最近在写微信小程序的时候,又遇到了多规格问题,就自己尝试了下,在这里记录下1.效果图2.文件及部分思路index.wxml文件<view class=“multipleStandard” bindtap=“standardSelect”> <view class=“standard”> 规格: <text class=“item”>颜色</text> <text class=“item”>颜色</text> <text class=“item”>颜色</text> </view> <image src="/images/arrows.png" class=“arrows”></image></view><view style=“background:rgb(207, 224, 232); height: 300rpx;margin-top: 50rpx;"></view><view animation=”{{animationData}}" class=“selectStandard” catchtouchmove=“noMove”> <view class=“tophead”> <view class=“topimg”> <image src="{{goods.original_img}}"></image> </view> <view class=“topright”> <view class=“selectClose”> <view style=“color:#ef5426;">{{standardObject.price}}</view> <image src="/images/close.png” bindtap=“handleClose”></image> </view> <view style=“margin:10rpx 0;color:#999999;">库存:{{standardObject.store_count}}</view> <view>规格: {{mergeStandard}}</view> </view> </view> <view class=“standard” wx:for=”{{commodityStandard}}" wx:key="{{standardIndex}}" wx:for-index=“standardIndex”> <view class=“standardTitle”>{{item[0].spec_name}}</view> <view class=“standardItem”> <block wx:for="{{item}}" wx:key="{{item.item_id}}"> <view class=“selectItem {{isSelect[item.isClick]}}” bindtap=“handleStandardClick” data-standard-index="{{standardIndex}}" data-index="{{index}}" data-id="{{item.item_id}}">{{item.item}}</view> </block> </view> <view style=“height:1px; width:100%;background-color:#eeeeee;"></view> </view> <view class=“selectCount”> <view class=“countname”>数量</view> <view class=“countright”> <i-input-number value=”{{commodityNum}}" min=“1” max="{{store_count}}" bindchange=“handleCommodityNumber” /> </view> </view> <view class=“submitBtn” bindtap=“submitSelected”> 确定 </view></view><i-message id=“message” />index.wxss文件.multipleStandard{ display: flex; margin-top: 20rpx; padding: 15rpx 20rpx; justify-content: space-between; align-items: center; box-shadow: 0 0 10px #ccc;}.item{ margin-right: 15rpx;}.arrows{ width: 16rpx; height: 27rpx;}/* 规格弹窗 /.selectStandard { width: 100%; height: 1000rpx; background-color: #fff; position: fixed; z-index: 333; bottom: -600px; border-top-left-radius: 20rpx; border-top-right-radius: 20rpx;}/ 头部选中的规格 /.tophead { display: flex; margin: 30rpx 3%; width: 94%; align-items: center; text-align: center;}.topimg { width: 200rpx; height: 200rpx;}.topimg image { width: 100%; height: 100%; border-radius: 20rpx; background-color: red;}.topright { margin-left: 30rpx; font-size: 28rpx; text-align: left; width: 66%;}/ 关闭按钮 /.selectClose { display: flex; justify-content: space-between; align-items: center;}.selectClose image { width: 30rpx; height: 30rpx;}.standard { margin: 0 3%; width: 94%; font-size: 28rpx;}.standardTitle { font-size: 30rpx; margin-top: 20rpx;}.standardItem { display: flex; flex-wrap: wrap; width: 100%;}.selectItem { border: solid 1px #666; margin: 20rpx; padding: 5rpx 23rpx; border-radius: 16rpx;}/ 规格选中时样式 /.standardSelected{ color: #fff; background: #f26740; background-color:#fe6732; border:solid 1px #fe6732; margin:20rpx; padding:5rpx 23rpx; border-radius:16rpx;}.standardNormal{ color: #000;}.standardDisable{ color: #eee;}/ 选择的数量 /.selectCount { margin: 30rpx 3%; width: 94%; font-size: 28rpx;}.countright { float: right; display: flex; text-align: center; align-items: center;}.countname { margin-bottom: 20rpx; font-size: 30rpx;}.submitBtn { position: absolute; bottom: 0; width: 70%; margin: 0 15% 20rpx 15%; text-align: center; z-index: 66; height: 80rpx; background-color: #fe6732; border-radius: 40rpx; color: white; font-size: 32rpx; line-height: 80rpx;}index.js文件const { $Message} = require(’../../component/iview/base/index’);Page({ data: { animationData: {}, isSelect: [“standardNormal”, “standardSelected”, “standardDisable”], / 用于区别当前的规格是否选中 / goods: { goods_name: “男鞋”, store_count: 158, cost_price: “10.00”, original_img: “/images/commodity.jpg”, }, commodityStandard: [ [ { spec_name: “颜色”, item_id: 535385, item: “白色”, src: “”, isClick: 0 }, { spec_name: “颜色”, item_id: 535386, item: “黑色”, src: “”, isClick: 0 } ], [ { spec_name: “尺寸”, item_id: 535692, item: “170”, src: “”, isClick: 0 }, { spec_name: “尺寸”, item_id: 535693, item: “180”, src: “”, isClick: 0 } ], [ { spec_name: “重量”, item_id: 552569, item: “11”, src: “”, isClick: 0 }, { spec_name: “重量”, item_id: 552570, item: “15”, src: “”, isClick: 0 } ] ], standardInfo: [ { id: 1018269, key: “535385_535692_552569”, price: “10.00”, productprice: “0.00”, store_count: 20 }, { id: 1018270, key: “535385_535692_552570”, price: “20.00”, productprice: “0.00”, store_count: 20 }, { id: 1018271, key: “535385_535693_552569”, price: “30.00”, productprice: “0.00”, store_count: 20 }, { id: 1018272, key: “535385_535693_552570”, price: “40.00”, productprice: “0.00”, store_count: 20 }, { id: 1018273, key: “535386_535692_552569”, price: “50.00”, productprice: “0.00”, store_count: 20 }, { id: 1018274, key: “535386_535692_552570”, price: “60.00”, productprice: “0.00”, store_count: 20 }, { id: 1018275, key: “535386_535693_552569”, price: “70.00”, productprice: “0.00”, store_count: 20 }, { id: 1018276, key: “535386_535693_552570”, price: “80.00”, productprice: “0.00”, store_count: 18 } ], selectedId: [], selectedStandard: [], standardObject: {}, commodityNum: 1, }, onLoad: function (options) { let goods = this.data.goods; let standardObject = this.data.standardObject; standardObject.price = goods.cost_price; standardObject.store_count = goods.store_count; let store_count = goods.store_count; this.setData({ standardObject, store_count }) }, / 规格选择 / standardSelect() { var that = this var animal1 = wx.createAnimation({ timingFunction: ’ease-in’ }).translate(0, -600).step({ duration: 300 }) that.setData({ animationData: animal1.export(), }) }, / 关闭规格选择 / handleClose() { var that = this var animal1 = wx.createAnimation({ timingFunction: ’ease-in’ }).translate(0, 600).step({ duration: 300 }) that.setData({ animationData: animal1.export() }) }, / 每个规格的点击事件 / handleStandardClick(e) { let id = e.currentTarget.dataset.id; // 总规格名称索引 let standardIndex = e.currentTarget.dataset.standardIndex; // 单个规格名称索引 let index = e.currentTarget.dataset.index; let commodityStandard = this.data.commodityStandard; let standardLength = commodityStandard[standardIndex].length; // 用于存储规格的id let selectedId = this.data.selectedId; // 用总规格名称索引来存储每个选中的规格id selectedId[standardIndex] = id; let selectedStandard = this.data.selectedStandard; // 在点击的时候,只需要对点击的这个规格所在的数组进行循环 for (let i = 0; i < standardLength; i++) { // 找到对应的单个规格索引,并设置isClick及单个规格名称 if (index == i) { commodityStandard[standardIndex][index].isClick = 1; selectedStandard[standardIndex] = commodityStandard[standardIndex][index].item; } else { commodityStandard[standardIndex][i].isClick = 0; } } // 将id用_连接起来 let mergeId = selectedId.join(’_’); console.log(mergeId); let mergeStandard = selectedStandard.join(’ ‘); console.log(mergeStandard); let standardInfo = this.data.standardInfo; let standardInfoLength = standardInfo.length; // 用于存储选中的规格 let standardObject = {}; for (let i = 0; i < standardInfoLength; i++) { if (standardInfo[i].key == mergeId) { standardObject = standardInfo[i]; break; } else { standardObject = this.data.standardObject; } } this.setData({ currentId: id, commodityStandard, selectedId, standardObject, mergeStandard, selectedStandard, }) }, / 选择数量 / handleCommodityNumber(e) { let commodityNum = e.detail.value; if (commodityNum >= this.data.store_count) { commodityNum = this.data.store_count; } this.setData({ commodityNum }) }, / 保存选择的规格 */ submitSelected() { let selectedStandard = this.data.selectedStandard; let length = selectedStandard.length; console.log(length) if (length == 0) { $Message({ content: ‘请选择规格’, type: ’error’ }); return false; } for (let i = 0; i < length; i++) { if (length < this.data.commodityStandard.length) { $Message({ content: ‘请选择规格’, type: ’error’ }); break; } if (selectedStandard[i] == undefined) { $Message({ content: ‘请选择规格’, type: ’error’ }); break; } } $Message({ content: ‘选择成功’, type: ‘success’ }); },})我这里提示信息使用了插件iview,可以在官网直接下载后使用iview weapp3.部分思路及改进方法开始想到的是将需合并的id的位置写死,比如,第一个位置就传第一个规格里选中的规格id,第二个位置就传第二个规格里选中的规格id,不过此时有问题,就是后台的id拼接是根据当前规格长度来拼接的,从最短的开始往长的拼接,然后,长度相等的时候,我这边的话,后台是从第一个开始拼接的,而且你选规格时,也可能是随机点的,此时若是采用第一种位置写死的方法就会有问题,因为会找不到对应的合并后的规格id因为我这里一开始用的数据就刚好满足我的设想,但是后面换了个数据后就出现问题了index.wxml文件也做部分修改,可以直接查找下,类名为standardItem<view class=“standardItem”> <block wx:for="{{item}}" wx:key="{{item.item_id}}"> <view class=“selectItem {{item.item_id == Specifications[standardIndex]?‘standardSelected’:‘standardNormal’}}” bindtap=“handleStandardClick” data-standard-index="{{standardIndex}}" data-name="{{item.item}}" data-id="{{item.item_id}}">{{item.item}}</view> </block></view>增加另一种模拟数据goods: { goods_name: “男鞋”, store_count: 95, market_price: “10.00”, shop_price: “101.00”, cost_price: “10.00”, original_img: “/images/commodity.jpg”, store_id: 170,},commodityStandard: [ [ { spec_name: “颜色”, item_id: 532825, item: “白色”, src: “”, isClick: 0 }, { spec_name: “颜色”, item_id: 532826, item: “黑色”, src: “”, isClick: 0 }, { spec_name: “颜色”, item_id: 532827, item: “红色”, src: “”, isClick: 0 } ], [ { spec_name: “大小”, item_id: 532828, item: “160”, src: “”, isClick: 0 }, { spec_name: “大小”, item_id: 532829, item: “150”, src: “”, isClick: 0 } ], [ { spec_name: “重量”, item_id: 552581, item: “10”, src: “”, isClick: 0 } ]],/ 这里合并规格的信息 /spec_goods_price: [ { id: 1018286, key: “552581_532828_532825”, price: “10.00”, productprice: “0.00”, store_count: 9 }, { id: 1018287, key: “552581_532828_532826”, price: “20.00”, productprice: “0.00”, store_count: 10 }, { id: 1018288, key: “552581_532828_532827”, price: “30.00”, productprice: “0.00”, store_count: 10 }, { id: 1018289, key: “552581_532829_532825”, price: “40.00”, productprice: “0.00”, store_count: 10 }, { id: 1018290, key: “552581_532829_532826”, price: “50.00”, productprice: “0.00”, store_count: 7 }, { id: 1018291, key: “552581_532829_532827”, price: “60.00”, productprice: “0.00”, store_count: 10 }],index.js文件onLoad: function (options) { let goods = this.data.goods; / 用于存储对应的价格及库存 / let standardObject = this.data.standardObject; standardObject.price = goods.cost_price; standardObject.store_count = goods.store_count; let spec_goods_price =this.data.spec_goods_price; if (spec_goods_price) { this.checkPrice(spec_goods_price); } this.setData({ standardObject, })},handleStandardClick: function (e) { // 总规格名称索引 let standardIndex = e.currentTarget.dataset.standardIndex; let id = e.currentTarget.dataset.id; / 存储选中的规格名称 / let name = e.currentTarget.dataset.name; let selectedStandard = this.data.selectedStandard; selectedStandard[standardIndex] = name; let mergeStandard = selectedStandard.join(’ ‘); let Specifications = this.data.Specifications; Specifications[standardIndex] = id; console.log(mergeStandard) this.setData({ Specifications, mergeStandard, selectedStandard, }) this.checkPrice(this.data.spec_goods_price);},/在还未选择完规格时,暂时选用第一个合并后的规格价格及库存/checkPrice: function (spec_goods_price) { let standardObject = this.data.standardObject; if (!this.checkSpecifications(spec_goods_price)) { standardObject.price = spec_goods_price[0].price; standardObject.store_count = spec_goods_price[0].store_count; this.setData({ standardObject, }) }},/ 保存及校验是否选好了规格 /submitSelected: function (e) { let spec_goods_price = this.data.spec_goods_price; let i = 0; let optionid = “”; if (spec_goods_price) { optionid = this.checkSpecifications(); if (optionid) { $Message({ content: ‘选择成功’, type: ‘success’ }); } else { $Message({ content: ‘请选择规格’, type: ’error’ }); } }},checkSpecifications(spec_goods) { let spec_goods_price = spec_goods || this.data.spec_goods_price; let Specifications = this.data.Specifications; let SpecificationsLength = spec_goods_price[0].key.split("_").length; let standardObject = this.data.standardObject; if (Specifications.length != SpecificationsLength) { return false; } else { for (let i = 0; i < spec_goods_price.length; i++) { / 若selectSpecifications全为true,则选中了对应的合并后的规格 */ let selectSpecifications = true; for (let j = 0; j < Specifications.length; j++) { if (spec_goods_price[i].key.indexOf(Specifications[j]) == -1) { selectSpecifications = false; break; } } if (selectSpecifications) { standardObject.price = spec_goods_price[i].price; standardObject.store_count = spec_goods_price[i].store_count; this.setData({ standardObject, }) return spec_goods_price[i].id; } } } return false;},利用indexOf来判断合并后的key值,就不用通过写死位置存储规格,然后这里用了动画,可以自己查看官方完档微信创建动画这里的规格啥的,格式可能会是多样的,我这里的话,就是返回这样的格式,其实我还想实现就是根据库存来判断是否可以点击,比如选尺码28,然后可能颜色为黑色的就库存不足,此时,是不能让他点击的,日后会在做下这个功能,欢迎大家在评论区指正,共同进步^^正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^^)往期好文推荐:判断ios和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)实现文字的省略号 ...

November 5, 2018 · 6 min · jiezi

推荐一款快速生成海报的微信小插件

现在很多小程序都有生成海报,分享海报的功能。我们自己的几个小程序 (如:爸妈搜商城、爸妈搜云课堂、幼师大学、跟着外教学英语等) 也都有生成海报的功能。因此技术团队萌生出制作一个简单易用的微信小插件,只要传入简单图片和对应的坐标值,就可以拼接成一幅完整的宣传海报。今天,我们提交了第一版,刚刚通过微信审核,现在让我开始说一说怎么使用我们刚新鲜出炉的小程序插件 —— 「爸妈搜海报 Maker」。爸妈搜海报自定义生成海报。使用方法1、 在微信小程序管理后台——设置——第三方服务,按 AppID(wxbf07f0f22c6c200d)搜索到该插件并申请授权(ps:一般不会出现拒绝的情况。如果申请被拒绝了,请重新申请,有时候管理员手抽点错了,请见谅)。 2、在要使用该插件的小程序 app.json 文件中引入插件声明。“plugins”: { “poster”: { “version”: “1.0.0”, “provider”: “wxbf07f0f22c6c200d” }}3、在需要使用到该插件的小程序页面的 JSON 配置文件中,做以下配置:{ “usingComponents”: { “poster”: “plugin://poster/poster” }}4、在相应的 HTML 页面中添加以下语句即可完成插件的嵌入。<poster />当然,只有这样,肯定不行,还需要给该插件控件传入对应的图片和文字属性。属性当前版本,传入的属性主要有两个:drawing 和 savebtnText:属性名类型默认值说明 drawing Array[] 画图的数据 savebtnText String"点击按钮进行图片保存" 按钮文字信息 drawing参数说明 drawing 数据目前有两种数据类型,一种是图片信息,另一种是文字信息。图片信息 属性名 类型 值 说明 type Stringimage图片类型 url String 图片路径,为线上图片 left Number 距离画布的左边距topNumber 距离画布的顶部距离widthNumber 绘画图片的宽度heightNumber 绘画图片的高度circleBooleantrue、false是否是绘制圆形,默认为 false文字信息 属性名 类型 值 说明 type Stringtext文字类型 content String 绘图的文字内容 left Number 距离画布的左边距topNumber 距离画布的顶部距离widthNumber 文字绘画的宽度 color String 文字信息 textAlign String 文字水平对齐方式 fontSize Number默认为 26 rpx文字大小其中,textAlign 参数:属性名类型说明leftString 左对齐 centerString 居中对齐 rightString 右对齐 如图:实例页面传入的控件简单明了:<poster drawing=’{{drawing}}’ savebtnText=’{{savebtnText}}’ canvas-style=‘canvas-style’ savebtn-style=‘savebtn-style’ />我们接着看传入的参数:Page({ data: { drawing: [ ], savebtnText: ‘点击按钮,保存图片’ }, onLoad: function () { wx.showLoading({ title: ‘绘图中..’ }) }, onShow: function () { this.setData({ drawing: [ { type: ‘image’, url: ‘https://i.loli.net/2018/10/30/5bd85117675b3.png’, left: 0, top: 0, width: 650, height: 960, }, { type: ‘image’, url: ‘https://wx.qlogo.cn/mmopen/vi_32/M8cK5rMR16udYRpanaZiaYz2KHgibVVHhFqG01h3rZUAGDKQerZwNv9baVDeNicjZ1bZzs4hUribjLX9bNaAmhia7pQ/132’, left: 72, top: 53, width: 78, height: 75, }, { type: ’text’, content: ‘咖啡’, fontSize: 26, color: ‘white’, textAlign: ’left’, left: 170, top: 50, width: 650, }, { type: ’text’, content: ‘这里是小程序码’, fontSize: 30, color: ‘red’, textAlign: ’left’, left: 390, top: 720, width: 200 }, { type: ‘image’, url: ‘https://i.loli.net/2018/10/30/5bd851175ce40.jpg’, left: 388, top: 660, width: 190, height: 190, circle: true } ] }) }})样式也很简单:类名说明canvas-style画布样式样式savebtn-style按钮样式/* 画布样式 /.canvas-style{ width: 650rpx !important; height: 960rpx !important; margin: 0 auto; border: 1px solid orangered; margin-top: 10rpx;}/ 保存图片按钮样式 */.savebtn-style{ height: 70rpx; line-height: 70rpx;}注意: 样式的优先级!好了,我们可以看看效果了总结这是我们团队做的第三个微信小插件,每个插件制作的标准就是,把复杂的逻辑交给我们来做。使用者只要简单的引入,用最便捷的输入参数,以达到最好的效果。欢迎微信小程序开发者使用我们的插件:爸妈搜日历提供简约不简单的日历基本功能,自定义样式,考勤状态等功能。插件地址:https://mp.weixin.qq.com/wxopen/pluginbasicprofile?action=intro&appid=wx23a9cef3522e4f7c爸妈搜富文本小程序富文本处理 rich-text, 将无法识别的标签改为可识别的, 适配移动设备。插件地址:https://mp.weixin.qq.com/wxopen/pluginbasicprofile?action=intro&appid=wx54e7e5b0ebeda242爸妈搜海报Maker插件地址:https://mp.weixin.qq.com/wxopen/pluginbasicprofile?action=intro&appid=wxbf07f0f22c6c200d最后,放出我们的插件开发者的联系方式,有什么问题都可以联系她哦~ ...

October 30, 2018 · 2 min · jiezi

微信小程序中的ios兼容性问题

记录下在微信小程序中遇到的一些兼容性问题,ios兼容性1.ios中input的placeholder属性字体不居中对placeholder设置line-height及font-size对input设置高度2.ios中滚动卡顿设置-webkit-overflow-scrolling:touch;3.微信小程序中解决ios中new Date() 时间格式不兼容在实现倒计时,根据后台返回的时间格式转换时,后台返回了时间格式为”2018-11-12 11:12:11”,然后利用new Date() 转换时,ios中无法展示,安卓中显示正常let time = ‘2018-12-10 11:11:11’;let temporaryTime1 = new Date(time);this.setData({ timeRemain1: temporaryTime1,})/* 利用正则表达式替换时间中的”-”为”/”即可 */let time = ‘2018-12-10 11:11:11’;let temporaryTime = new Date(time.replace(/-/g,’/’));let temporaryTime1 = new Date(time);this.setData({ timeRemain: temporaryTime, timeRemain1: temporaryTime1, })4. 微信小程序scroll-view隐藏滚动条方法在wxss里加入以下代码:::-webkit-scrollbar{width: 0;height: 0;color: transparent;}暂时遇到的兼容性就是这么多,会持续更新,若大家有遇到,可在评论区告知下,感谢正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断ios和Android及PC端实现文字的省略号纯css实现瀑布流(multi-column多列及flex布局)

October 30, 2018 · 1 min · jiezi

即时通讯App怎样才能火?背后的技术原理,可以从这5个角度切入

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由腾讯云视频发表于云+社区专栏关注公众号“腾讯云视频”,一键获取 技术干货 | 优惠活动 | 视频方案社交场景iMessage隐藏的省话费小秘密融合通信原理通过短信和IM的结合,可以实现从APP内到APP外的沟通。若你的朋友没有安装应用,你也可以在应用内,导入通讯录好友,给其发消息,只是这个“消息”,会以短信的形式触达。企业办公沟通场景休假旅行,老板电话,这2个词总能凑一起融合通信原理通过IM与呼叫中心结合,企业可以在APP内发起互联网IP电话,经过“中转机”,转为传统电话。企业间沟通的场景甲方和乙方又有了个安全高效的沟通途径融合通信原理两家企业通过对接各自的IM系统,可实现企业通讯录互通,适合于合作伙伴企业间的沟通。警察应急指挥场景网络不佳的情况,采用对讲机,可以向总部大屏幕同步现场情况融合通信原理通过对讲机通信和IM的结合,可以满足一些极端恶劣环境下的消息同步,从对讲机到指挥中心、微信群、app内的消息同步。智能客服场景过(现)去(在),在大众点评上联系商家,只能拨打商家预留的电话,现(将)在(来),“联系商家”入口悄然变成了智能客服,你永远不知道对面是真实的人还是AI机器人融合通信原理通过IM、视频通话、呼叫中心和大数据系统的结合,从APP、小程序内唤起智能客服、视频通话,可以极大的提升用户体验。如何打通IM、传统外呼中心、视频通话答案尽在“融而开放、合以创新”T-HIM融合通信技术开发实战时间:2018.9.8 13:30 -19:30 周六地点:北京市海淀区中关村创业大街10号楼天使汇极客咖啡3楼问答请问小程序即时通讯如何接入发送消息?相关阅读IM即时通讯实现原理iOS 即时通讯 + 仿微信聊天框架 + 源码开发一款即时通讯App,从这几步开始 【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识

October 26, 2018 · 1 min · jiezi

CTF编码全家桶小程序

CTF编码全家桶小程序提供Base64、Url、HTML实体、莫尔斯电码等编码转换工具,凯撒密码、栅栏密码、ROT13、MD5、SHA等加密工具,及IP地址查询、Whois信息查询等工具。功能编码转换Hex 编码Base64 编码Url 编码Html Entity 编码Escape 编码Morse Code(莫尔斯电码)古典密码Rail-fence(栅栏密码)Caesar(凯撒密码)ROT13(ROT5、ROT18、ROT47)密码学MD5(MD2、MD4、RIPEMD)SHA(SHA1、SHA256、SHA512、SHA224、SHA384)其他工具IP地址信息查询Whoise信息查询IP地址计算Hex 编码Base64(Base16、Base32、Base58)Url 编码Html Entity 编码Morse Code(莫尔斯电码)Caesar(凯撒密码)凯撒密码是一种替换加密的技术,明文中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后被替换成密文。Rail-fence(栅栏密码)栅栏密码的加密方式:把文本按照一定的字数分成多个组,取每组第一个字连起来得到密文1,再取每组第二个字连起来得到密文2……最后把密文1、密文2……连成整段密文。ROT13(ROT5、ROT18、ROT47) MD5(MD2、MD4、RIPEMD)SHA(SHA1、SHA256、SHA512、SHA224、SHA384)IP地址信息查询Whoise信息查询IP地址计算其他

October 23, 2018 · 1 min · jiezi

微信小程序图片预加载组件 wxapp-img-loader

由于微信小程序没有提供类似 Image 这样的 JS 对象,要实现图片的预加载要麻烦一些, wxapp-img-loader自定义组件可以在微信小程序中实现图片预加载功能。使用1、下载 wxapp-img-loader项目源代码(https://github.com/o2team/wxa…),将 img-loader 目录拷贝到你的项目中2、在页面的 WXML 文件中添加以下代码,将组件模板引入<import src="../../img-loader/img-loader.wxml"/><template is=“img-loader” data="{{ imgLoadList }}"></template>3、在页面的 JS 文件中引入组件脚本const ImgLoader = require(’../../img-loader/img-loader.js’)4、实例化一个 ImgLoader 对象,将 this(当前 Page 对象) 传入,第二个参数可选,为默认的图片加载完成的回调方法this.imgLoader = new ImgLoader(this)5、调用 ImgLoader 实例的 load 方法进行图片加载,第一个参数为图片链接,第二个参数可选,为该张图片加载完成时的回调方法。图片加载完成的回调方法的第一个参数为错误信息(加载成功则为 null),第二个参数为图片信息(Object 类型,包括 src、width 及 height)。this.imgLoader.load(imgUrlOriginal, (err, data) => { console.log(‘图片加载完成’, err, data.src, data.width, data.height)})wxapp-img-loader组件可以加载单张图片、也可以加载多张图片。运行效果:其他wxapp-img-loader项目地址:https://github.com/o2team/wxa…【小程序推荐】百科知识词典

October 18, 2018 · 1 min · jiezi

在小程序开发中使用 npm

微信小程序在 2.2.1 版本后增加了对 npm 包加载的支持,使得小程序支持使用 npm 安装第三方包。1. 在小程序中加载 npm 包npm install miniprogram-datepicker –productionnode_modules可以 在小程序根目录下,也可以存在于小程序根目录下的各个子目录中。但是不可以 在小程序根目录外。使用–production选项,可以减少安装一些业务无关的 npm 包,从而减少整个小程序包的大小。2. 构建 npm 包在微信小程序开发工具的「工具」菜单下点击「构建 npm」命令,进行 npm 包的构建,此构建可以将 npm 包构建成在小程序中可加载使用的包。node_modules 目录不会参与编译、上传和打包中,所以小程序想要使用 npm 包必须走一遍“构建 npm”的过程,在最外层的 node_modules 的同级目录下会生成一个 miniprogram_npm 目录,里面会存放构建打包后的 npm 包,也就是小程序真正使用的 npm 包。构建打包分为两种:小程序 npm 包会直接拷贝构建文件生成目录下的所有文件到 miniprogram_npm 中;其他 npm 包则会从入口 js 文件开始走一遍依赖分析和打包过程(类似 webpack)。寻找 npm 包的过程和 npm 的实现类似,从依赖 npm 包的文件所在目录开始逐层往外找,直到找到可用的 npm 包或是小程序根目录为止。构建完成后还需要确认项目已勾选了「使用 npm 模块」。3.使用npm包js 中引入 npm 包:const package = require(‘packageName’)使用 npm 包中的自定义组件:{ “usingComponents”: { “datepicker”: “miniprogram-datepicker” }}miniprogram-datepicker组件运行效果其他微信小程序npm支持文档:https://developers.weixin.qq….【小程序推荐】百科知识词典

October 17, 2018 · 1 min · jiezi

云开发初探 —— 更简便的小程序开发模式

原文链接李成熙,腾讯云高级工程师。2014年度毕业加入腾讯AlloyTeam,先后负责过QQ群、花样直播、腾讯文档等项目。2018年加入腾讯云云开发团队。专注于性能优化、工程化和小程序服务。小程序诞生以来,业界关注小程序前端的技术演进较多,因此众多小程序前端的框架、工具也应运而生,前端开发效率大大提高,而后台的开发技术则关注不多,痛点不少,具体痛在哪里呢?小程序后台开发之痛第一个是脑袋瓜疼,怎么疼呢?随着像腾讯云等的云服务商提供的云服务越来越便捷,业务上云已经是大势所趋。但是从简单地在云虚拟机上部署页面,到实现真正全面地上云,还是有很多区别。要真正实现全面的上云,要了解的东西非常多,当你第一次接触这些概念的时候,学的这些东西是一个接一个,让你应接不暇,往往分散了你的对业务的专注力。比如我自己,来腾讯云之后,为了对云服务有更好地了解,就去报了个腾讯云的课程。这课程系列分云架构师、云开发、云运维三门课程,还分初级、中级、高级,需要花费大量时间才能理清这些知识概念,并且还要花大量的时间去上机做实验。所以对于开发来说,要彻底搞清楚,还真的不是件容易事,绝对让你的脑袋疼。第二是肉疼,尤其是你老板肉疼。最开始当互联网还没有云服务商的时候,公司都得自己搭服务,不仅花大价钱买机器、买宽带流量,还得请人过来维护。如果在这种情况下要搞小程序开发,公司得请一个维护服务器硬件的、一个维护网络的,一个数据工程师,一个后台还有一个前端,刚好五个人。当云服务商开始进入变革整个市场的时候,我们就不用再自己维护硬件了,由云服务商来维护,因此我们可以少请一个维护硬件的,但还是得有一个运维去维护云服务。当云服务商将数据库、容器服务都抽象出来上云之后,咱们连专业的数据库维护都可以不请了,由后台或者云维兼岗就行。云服务商的不断发展,确实是让云服务的成本不断下降,但投入的钱还是很多呀,要投入的人还是不少,这几年生意难做,作为老板肯定是想投入成本、试错成本越少越好。第三个是肾疼。大家都知道,开发是一个走肾的工作。比如,这些年流行的前后端分离,虽然让专人专项,但却引入了联调这个事,所以也增加了肾的负担。这里列出了三个前后端分离带来的麻烦。权责往往不清晰,有很多临界的位置,谁管都可以,容易引发扯皮。沟通时间增多,因为毕竟是两个人工作嘛,需要不少的沟通除了沟通,还需要两边的代码调试,看看数据、展示通不通,这个时间也很不可控,尤其是如果环境特别复杂,调起来不仅麻烦重重,还很有挫败感。无服务开发小程序是未来趋势正因为小程序后台开发的麻烦重重,因此业内都想出了各种各样的开发方案,其中一种方案,“无服务开发小程序”,我们认为,将会是未来的趋势。但这个未来,其实今天已经到来了。那什么是无服务开发呢?无服务,又称为 Serverless。Serverless 还处在一个比较初期的阶段,目前也没有权威和官方的定义,不同人不同公司有不同说法,今天我也不打算讲太复杂。顾名思义, Serverless 就是指应用的开发不再需要考虑服务器这样的硬件基础设施,基于 Serverless 架构的应用主要依赖于像腾讯云这样的云服务商提供的后台服务。比如说无服务云函数、云数据库、对象存储服务等等。简单来说,相当于你现在要开个水果店卖水果,以前你还得要租店面,搞水电、装修门面。现在这些都不用了,你就在一个已经搭好各种各样设施的超市里,租一个已经帮你搞好门面的架子或者箱子,卖得好你就租大一点,卖不好就租小一点,随时随地随你的心意,非常灵活。为什么说无服务化开发是趋势呢?因为云服务的进程,已经从物理机,演进到 IAAS,再到 PAAS。IAAS 就是包括像云虚拟机、私有网络、网络专线、负载均衡等等的基础服务;PAAS 则更抽象一些,比如像云数据库、网络防护等等。基于 IAAS、PAAS,云服务商发展出 Serverless 这类更高级的开发服务。因此呢,无服务开发就会是今后开发类似小程序这类轻量应用的新的开发趋势。一句话概括就是说,有了无服务开发之后,你就不用再处理安装、运维,底层了,只管写接口、写逻辑就好。总得来说,虽然你管的东西越来越少,但开发效率却越来越高,开发出来的轻应用、小程序却是具备高性能、高可用、高扩展的特性。那无服务开发,具体怎么去解决刚刚提到的后台开发痛点呢?第一是让你更加关注你的业务逻辑。云服务许多好用但难理解的概念,什么冷备热备、弹性伸缩、负载均衡等等,通通都不用管,你只需要写好你的业务,服务好用户就行。第二,更省人力更省资金,老板不再肉疼。因为有了无服务开发,运维工作也不用操心了,像小程序这类的轻应用,有一个全栈开发,或者一个前端,半个后台就可以轻松应付了,资金和人力的需求可谓大大节省。第三,就是前端工程师向全栈工程师的转变。有了无服务开发,前端工程师其实也可以安全、高性能地去操作一些以前只有后台才敢操作的数据和逻辑,如果要开发的应用是像小程序一样轻量的、简单的,完全可以由前端工程师完成,除非是特别复杂的,可能才需要后台的介入。这样也省缺了先前提到的前后端联调的麻烦。小程序·云开发说了这么多无服务开发的概念、优点,在小程序无服务开发这一块,腾讯云有什么样的作品呢。这就是今天要重点介绍的,小程序·云开发,这就是腾讯云与微信联合研发后,交出的答卷。云开发,一共提供了三大能力,分别是存储、数据库、云函数。简而言之,就是提供了存文件、存数据和运行业务逻辑的能力。接下来,我会采取前后对比的方式,从方方面面去对比云开发和旧有的开发模式的不同。首先是开发模式与架构上的对比。在云开发模式出来之前,旧的小程序后台开发模式就是上面这幅图,在小程序端发请求,往往你得引入额外封装好的 SDK,然后你需要在云服务这边配置大量的运维产品才能做出性能、可用性非常好的产品。开发者要关心的内容,从前端、后台一直关心到运维这块。而云开发的全新模式,只要调用小程序原生的接口,就可以操作最基本的三大资源,而云开发背后又有腾讯云的基础服务作为支撑,本身就高可用、高性能、可扩展,你要关心的事情是大大减少了。其次是资源管理平台的对比。以前你需要管理云资源,你需要在腾讯云的面板里,几十上百的产品里找到你需要的产品。而云开发呢,你在小程序开发工具里,就可以找到云开发的控制面板入口。进入后,我们将你要关注的产品,做成一个独立面板供你使用,极为简洁方便。第三,我们对比一下在小程序端调用资源。以上传文件为例,旧的开发模式,小程序端,你需要用 wx.chooseImage 还有 wx.uploadFile 小程序接口,后台要部署业务框架、路由,还有写逻辑上传到腾讯云的对象存储,你还要考虑这个后台服务的性能与安全,万一用户量峰值很大怎么办,有黑客攻击怎么办。而云开发的例子,则极为简单,十几行代码,就可以写出安全、性能好的代码上传逻辑!假设开发者是一个菜鸟,只懂 JavaScript 基础,对比下来,传统的开发模式,前端耗时2分钟开发,1小时联调,后台框架、逻辑和联调一共8小时,运维,要花一整天时间去学,总共要花1142分钟,对比只要写2分钟就能完成的云开发模式,足足是云开发耗时的571倍!最后,我们来对比在服务端里插入数据。这里的服务端里指的包括有云函数、还有你自己买的服务器。旧模式下,小程序端要用一个 wx.request 发送请求到后台,后台搭建好框架、路由等服务之后,开始写插入数据到腾讯云MongoDB实例的逻辑,自然也是需要考虑服务的性能与安全。而云开发的新模式,十几行代码,就可以开发出性能好、安全性高的插入数据逻辑。假设开发者是一个菜鸟,对比下来,传统的开发模式,前端要花31分钟进行开发与联调,后台要用6小时部署服务开发逻辑还要30分钟联调,而运维的话从学习到会用大概也得10小时,基本上是云开发模式耗时的1000多倍。从代码、耗时等多个方面去对比新旧两种开发模式,我们可以发现,云开发是绝对的碾压。小程序·云开发背后的技术力量大家现在知道了无服务开发是未来的开发新趋势,带有无服务特性的小程序云开发带来的各种各样的好处,那么腾讯云在背后,做了些什么技术进行支撑呢?架构上,一个请求操作从小程序端,通过微信后台,一直到腾讯云这边的云开发服务层,云开发服务层调用的这些数据库、存储、云函数,其实都是基于腾讯云的各种基础服务。在这个请求通路上面,微信会将小程序的用户 openid, 小程序 appid 直接带过来,将用户的信息写到云函数、数据和文件元信息里面,为更方便的权限控制打下基础。另外,既然是复用了腾讯云的基础资源,那自然是具备了云资源的特性。比如存储自动接入了 CDN 加速, 数据库天然就带有自动备份、无损恢复等功能,云函数有弹性伸缩、多地可用的特性,能响应峰值不同的服务。而云开发服务层,我们也做了负载均衡、并且与微信后台进行就近接入,让性能更好。目前云开发正式上线5天(注:9月10日深夜发布,掘金技术大会是在9月16日),我们的服务所支撑的 API 日调用量最大的单个小程序,已经达到 1000W+ 的调用量了,这个调用量是什么概念呢?一般只有BAT,一些高频使用的独角兽开发的小程序才能达到这个调用量级。因此90%以上的小程序用我们这个服务都是没有问题的。推荐实践讲一项技术,除了讲功能、讲底层,其实更重要地说讲怎么去用这门技术去实践。接下来,我会介绍一些我们推荐的实践方式,但我只会是点到为止,我们其实更希望社区能基于云开发,做出更多更好的实践。第一点是资源操作的推荐实践。在小程序端操作资源方面,我们是使用小程序的原生接口进行操作,而在小程序端操作资源,由于安全的考虑问题,基本上操作存储、数据库等的资源只能写用户自己的数据,而读数据则根据规则来判断是否有权限。在服务端操作资源方面,我们使用 wx-server-sdk 或者 tcb-admin-node 来处理,前者是基于后者的能力进行了封装。在服务端使用这两个 SDK 去操作资源,所拥有的权限是管理级的,就是意味着可以操作一切的资源。左边的图是数据库的权限控制,右边的图是存储的权限控制。这两个控制面板都有各自不同权限的一些推荐的使用场景,大家可以打开控制去读下面每个权限的灰色的解释。第二点,是数据库的推荐实践。这里以腾讯乘车码为例,像这种交通的小程序,可能会面对弱网或者无网的情况,开发初期为了省事,将大量的配置信息都写在小程序端中。但随着向更多城市的推进,配置文件越来越大,小程序的包体积越来越大。正好这个时候云开发推出了,腾讯乘车码就采用云开发的数据库,将一些不一定要在离线环境使用的配置迁移到云开发,另外还采用云开发的存储服务来存放静态资源。这就大大压缩了乘车码小程序的体积,为其它新增功能腾挪了空间。第三点,推荐使用云开发的存储存放小程序中所需要的静态资源。因为云开发的存储天然自带 CDN 加速。比如在控制面版的存储中,文件的详情里获取的下载地址,就是 CDN 已经加速的地址。第四点,是云函数的使用。目前云函数暂时不支持过于耗时、太复杂的操作,目前的超时时间为20s,函数包大小控制在20M左右。但其实这也已经能满足超过80%的需求,随着服务的逐步稳定,我们会考虑将这些限制进一步放宽。云函数另一种用法就是,我们可以将相同的一些操作,比如用户管理、支付逻辑,按照业务的相似性,归类到一个云函数里,这样比较方便管理、排查问题以及逻辑的共享。甚至如果你的小程序的后台逻辑不复杂,请求量不是特别大,完全可以在云函数里面做一个单一的微服务,根据路由来处理任务。比如这里就是传统的云函数用法,一个云函数处理一个任务,高度解耦。第二幅架构图就是尝试将请求归类,一个云函数处理某一类的请求,比如有专门负责处理用户的,或者专门处理支付的云函数。最后一幅图显示这里只有一个云函数,云函数里有一个分派任务的路由管理,将不同的任务分配给不同的本地函数处理。云函数还有一种用法就是,可以作为中间路由,然后将 appid, openid,转发给原有的服务。这里以腾讯相册为例。具体怎么操作呢。比如腾讯相册之前将评论功能接入了云开发,但一些敏感操作,像删除、编辑评论,这个请求发送到云函数,然后云函数会将用户信息转发给相册原本的后台,然后再将该用户是否有权限返回来告诉云函数,如果有权限,就在云函数里删除评论。最后,如果你们想在云函数调用 AI 服务,还有一些微信相关的操作,可以使用我封装的这两个 SDK。第一个 image-node-sdk 覆盖面比较全,覆盖了全部的腾讯云智能图像服务,下面的 wx-js-utils,也提供了微信支付、模板消息、用户信息获取等几个常用的接口。可以关注我的微博或者 Github 获取最新云开发的资讯或者技术资料。我的微博我的github

September 20, 2018 · 1 min · jiezi

小程序的全栈开发新时代

原文链接什么是小程序·云开发小程序·云开发是微信团队和腾讯云团队共同研发的一套小程序基础能力,简言之就是:云能力将会成为小程序的基础能力。整套功能是基于腾讯云全新推出的云开发(Tencent Cloud Base)所研发出来的一套完备的小程序后台开发方案。小程序·云开发为开发者提供完整的云端流程,简化后端开发和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代。该解决方案目前提供三大基础能力支持:存储:在小程序前端直接上传/下载云端文件,在小程序云控制台可视化管理数据库:一个既可在小程序前端操作,也能在云函数中读写的文档型数据库云函数:在云端运行的代码,微信私有协议天然鉴权,开发者只需编写业务逻辑代码未来,我们还会集成更多的服务能力,为小程序提供更强有力的云端支持。如何使用小程序·云开发上面就是小程序·云开发简单的使用图谱:在小程序端,直接用官方提供的接口,在云函数端,直接用官方提供的 Node SDK,就可以操作你云的资源。以前开发小程序所担忧的数据库搭建、文件系统部署,通通没有。你只需要有在小程序开发 IDE 里面的 云开发,开通一下,填写环境 ID,便可以拥有小程序的云能力!当然,其实用云开发,并不排斥原有的后台架构,通过下面的架构,你也可以无缝与原有的后台服务兼容,也简化了一些小程序鉴权的逻辑:接下来,我会分别从小程序端、服务端讲述如何使用这些云资源。使用云能力小程序端客户端,这里是指在小程序端中。如果要使用云开发能力,请做以下配置:在 app.json / game.json 中, 中增加字段 “cloud”: trueproject.config.json 中增加了字段 cloudfunctionRoot 用于指定存放云函数的目录初始化云开发能力://app.jsApp({ onLaunch: function () { wx.cloud.init({ traceUser: true // 用户信息会显示在云开发控制台的用户面板中 }); }});小程序端初始化能力文档在用户管理中会显示使用云能力的小程序的访问用户列表,默认以访问时间倒叙排列,访问时间的触发点是在小程序端调用 wx.cloud.init 方法,且其中的 traceUser 参数传值为 true。服务端如果你想在云函数中,操作文件、数据库和云函数资源,你可以使用我们提供的服务端 SDK 进行操作。首先,进入到你的某个云函数中,安装以下依赖包:npm i –save tcb-admin-node在云函数中初始化// 初始化示例const app = require(’tcb-admin-node’);// 初始化资源// 云函数下不需要secretId和secretKey。// env如果不指定将使用默认环境app.init({ secretId: ‘xxxxx’, secretKey: ‘xxxx’, env: ‘xxx’});//云函数下使用默认环境app.init()//云函数下指定环境app.init({ env: ‘xxx’});服务端初始化文档存储云开发提供存储空间、上传文件、下载文件、CDN加速文件访问等能力,开发者可以在小程序端与服务端通过 API 使用这些能力。小程序端// 选择图片wx.chooseImage({ success: dRes => { // 上传图片 const uploadTask = wx.cloud.uploadFile({ cloudPath: ${Date.now()}-${Math.floor(Math.random(0, 1) * 10000000)}.png, // 随机图片名 filePath: dRes.tempFilePaths[0], // 本地的图片路径 success: console.log, fail: console.error }); }, fail: console.error,});小程序端存储文档服务端const app = require(’tcb-admin-node’);app.init();app.uploadFile({ cloudPath: “cover.png”, fileContent: fs.createReadStream(${__dirname}/cover.png)}).then((res) => { console.log(res);}).catch((err) => { console.error(err);});;控制台上传好的文件,就会出现在控制台中,如下图。你可以在控制台里删除、下载或者查看图片的详情。你还可以控文件整体的权限,这里还有一些具体的介绍。服务端存储文档数据库小程序云提供文档型数据库 ( document-oriented database ),数据库包含多个集合(相当于关系型数据中的表),集合近似于一个 JSON 数组,数组中的每个对象就是一条记录,记录的格式是 JSON 文档。每条记录都有一个 _id 字段用以唯一标志这条记录、一个 _openid 字段用以标志记录的创建者,即小程序的用户。开发者可以自定义 _id,但不可在小程序端自定义(在服务端可以) _openid 。_openid 是在文档创建时由系统根据小程序用户默认创建的,开发者可使用其来标识和定位文档。数据库 API 分为小程序端和服务端两部分,小程序端 API 拥有严格的调用权限控制,开发者可在小程序内直接调用 API 进行非敏感数据的操作。对于有更高安全要求的数据,可在云函数内通过服务端 API 进行操作。云函数的环境是与客户端完全隔离的,在云函数上可以私密且安全的操作数据库。数据库 API 包含增删改查的能力,使用 API 操作数据库只需三步:获取数据库引用、构造查询/更新条件、发出请求。切记,在操作数据库前,请先在控制台中创建 collection。小程序端const db = wx.cloud.database();// 插入数据db.collection(‘photo’).add({ data: { photo: ‘cloud://tcb-xxx/05ca1d38f86f90d66d4751a730379dfa6584dde05ab4-Ma9vMN_fw658.jpg’, title: ‘风景’ }});// 提取数据db.collection(‘photo’).get().then((res) => { let data = res.data; console.log(data);});// 输出// 在小程序端, _openid 会自动插入到数据库中{ photo: ‘cloud://tcb-xxx/05ca1d38f86f90d66d4751a730379dfa6584dde05ab4-Ma9vMN_fw658.jpg’, title: ‘风景’, _openid: ‘oLlMr5FICCQJV-QgVLVzKu2312121’}小程序端数据库文档服务端const app = require(’tcb-admin-node’);app.init();const db = app.database();db.collection(‘photo’).limit(10).get().then((res) => { console.log(res);}).catch((err) => { console.error(err);});// 输出// 因为是在服务端,其它用户的也可以提取出来{ photo: ‘cloud://tcb-xxx/05ca1d38f86f90d66d4751a730379dfa6584dde05ab4-Ma9vMN_fw658.jpg’, title: ‘风景’, _openid: ‘oLlMr5FICCQJV-QgVLVzKu4312121’}{ photo: ‘cloud://tcb-xxx/0dc3e66fd6b53641e328e091ccb3b9c4e53874232e6bf-ZxSfee_fw658.jpg’, title: ‘美女’, _openid: ‘DFDFEX343xxdf-QgVLVzKu3312121’}{ photo: ‘cloud://tcb-xxx/104b27e339bdc93c0da15a47aa546b6e9c0e3359c315-L8Px2Y_fw658.jpg’, title: ‘动物’, _openid: ‘DFDFEX343xxdf-QgVLVzKu3412121’}服务端数据库文档控制台可以在控制台里,看到用户操作的数据,你也可以自己在控制台上添加、更新或删除数据。如果数据量庞大,可以设置索引提供查询的效率。数据库也可以通过设置权限,管控每个 collection。云函数云函数是一段运行在云端的代码,无需管理服务器,在开发工具内一键上传部署即可运行后端代码。开发者可以在云函数内获取到每次调用的上下文(appid、openid 等),无需维护复杂的鉴权机制,即可获取天然可信任的用户登录态(openid)。小程序端wx.cloud.callFunction({ name: ‘addblog’, // 云函数名称 data: { // 传到云函数处理的参数 title: ‘云开发 TCB’, content: ‘存储、数据库存、云函数’ }}).then(res => { console.log(res)}).catch((err) => { console.error(err);});小程序端云函数文档服务端const app = require(“tcb-admin-node”);app.init();app.callFunction({ name: ‘addblog’, // 云函数名称 data: { // 传到云函数处理的参数 title: ‘云开发 TCB’, content: ‘存储、数据库存、云函数’ }}).then((res) => { console.log(res);}).catch((err) => { console.error(err);});服务端云函数文档控制台上传好之后的云函数,都会在这里罗列出来。每次调用云函数,都可以在这里看到日志,还可以构造测试的参数,用于调试。语法糖大部份的接口,目前都支持两种写法,分别是Promise 和 Async/Await,本节以 callFunction 作为例子,在云函数中介绍这两种写法。 Async/Await 本质上是基于 Promise 的一种语法糖,它只是把 Promise 转换成同步的写法而已。Promiseconst app = require(“tcb-admin-node”);app.init();exports.main = (event, context, callback) => { app.callFunction({ name: ‘addblog’, // 云函数名称 data: { // 传到云函数处理的参数 title: ‘云开发 TCB’, content: ‘存储、数据库存、云函数’ } }).then((res) => { console.log(res); callback(null, res.data); }).catch((err) => { callback(err); });};Async/Awaitconst app = require(“tcb-admin-node”);app.init();exports.main = async (event, context) => { let result = null; try { result = await app.callFunction({ name: ‘addblog’, // 云函数名称 data: { // 传到云函数处理的参数 title: ‘云开发 TCB’, content: ‘存储、数据库存、云函数’ } }); } catch (e) { return e; } return result;};在云函数里使用,由于是 Node 8.9 或以上的环境,因此天然支持 Async/Await 诘法,但在小程端要使用的话,需要额外引入 Polyfill,比如这个开源的项目:regenerator开发者资源由于小程序·云开发是基于腾讯云的云开发开发的功能,因此在腾讯云与小程序两边都有不少的开发者资源,这里供大家参阅读:腾讯云开发者资源及文档腾讯云云开发平台官方 Github微信小程序·云开发文档 ...

September 20, 2018 · 2 min · jiezi

anu小程序快速入门

众所周知,微信推出小程序以来,可谓火遍大江南北,就像当前互联网兴起时,大家忙着抢域名与开私人博客一样。小程序之所以这么火,是因为微信拥有庞大的用户量,并且腾讯帮你搞定后台问题及众多功能问题(如分享,支付,视频播放,文件上传),相当于你一个人也能做一个公司的事情。在手机上,每个人不可能装超过100个以上的APP,因此这么多小公司想生存下来很不容易,但傍上微信这个大平台,个人也能出一个有上千万人玩的爆款游戏,也能搞一些小商城,避开淘宝京东的锋芒。对于大公司,这也是一个赚钱导流的新途径。相信今后,小程序会越来越火。但是,小程序对开发人员来说,则不怎么给力。它的API非常原始,没有类继承,npm支持滞后,不能使用CSS预处理器。于是市面上出现各种转译框架。转译框架与我们常用的框架不太大一样,转译框架是将我们写的代码翻译成小程序支持的各种文件形式,比如说wxml, wxss。而转译框架在编写业务时,允许我们使用更为流行的框架形式来写(之所以说形式,因为以vue方式写,它实际不是跑一个真正的vue,以react方式写,也不是跑一个真正的react)。由于小程序不存在DOM,因此流行那几个框架是无法跑在微信中,但可以用它们的仿造品。转译框架的最高目标是统一公司的技术栈,让以react为技术栈的公司不用再搞vue,为vue为技术栈的公司不用学react。这在招聘与维护上有很大优势。目前,市面上的转译框架有wepy, mpvue, taro。前两者是vue风格,使用的是vue1的语法,但还是有这么多vue语法无法支持。taro是京东近半年出品的,react风格,目前还不够稳定,最大的问题是组件不包含组件。好了,到本文的主角出场。anu原本是一个迷你React框架,对react16的兼容程度非常高,能跑react-router, react-redux, antd, rematch等等。而anu小程序只是在其上面的一个扩展,为它添加了一个cli及一个新的render.cli用在编译期,将JSX转换成wxml等,而miniapp render用在运行期,让它跑在微信内部。由于小程序的限制,一切涉及DOM的API都不能用,即findDOMNode, dangerouslySetInnerHTML及refs.dom。目前也不支持使用render props时,因为wxml里面不能运行函数。其他,都可以正常使用,包括各种生命周期钩子,页面组件还有componentDidShow, componentDidHide两个新钩子div, h1, span, b等html标签用户已经用小程序方式写好的各种组件事件里面可以传参,多次bind this(这是一个重大特殊,其他转译框架做不到)多重循环支持es6, es7语法糖支持组件标签包含组件标签(solt机制)无状态组件的支持onClick属性自动映射成bindtapReact.wx对象拥有原wx对象的所有方法,并且对所有请求接口进行Promise化提供两个通用别名@components与@react,方便用户import React与通用组件目录的内容说了这么多,我们看一下如何使用。1.到https://github.com/RubyLouvre… 下载anugit clone https://github.com/RubyLouvre/anu.git2.进入anu/packages/cli目录, 执行npm link命令 (如果之前执行过,需要npm unlink)3.回到anu目录,这时可以使用mpreact <projectname>创建一个小程序项目4.执行npm start命令,构建工程5.然后使用微信开发工具,打开<projectname>下面的dist目录作为一个演示项目。去哪儿网模板包含一些简单的使用演示。大家可以用 vs code打 <projectname>。src目录是源码,dist目录是最终生成给微信运行的代码。根据微信小程序的要求,src 主要分为三大块, app.js, pages目录的页面组件, components目录的通用组件。app.js会import所有用到的页面组件的JS文件页面组件的源码与生成代码大概如下import React from ‘@react’;import Dog from ‘@components/Dog/index’;class P extends React.Component { render() { return ( <div> <div>类继承的演示</div> <Dog age={12} /> </div> ); }}export default P;会生成两个文件"use strict";Object.defineProperty(exports, “__esModule”, { value: true});var _ReactWX = require("../../../../ReactWX");var _ReactWX2 = _interopRequireDefault(_ReactWX);var _index = require("../../../../components/Dog/index");var _index2 = _interopRequireDefault(_index);function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj };}function P() {}P = _ReactWX2.default.miniCreateClass( P, _ReactWX2.default.Component, { render: function() { var h = _ReactWX2.default.createElement; return h( “view”, null, h(“view”, null, “u7C7Bu7EE7u627Fu7684u6F14u793A”), h(_ReactWX2.default.template, { age: 12, templatedata: “data09558693”, is: _index2.default }) ); }, classUid: “c70258” }, {});Page(_ReactWX2.default.createPage(P, “pages/demo/syntax/extend/index”));exports.default = P;wxml<import src="../../../../components/Dog/index.wxml" /><view> <view>类继承的演示</view> <template is=“Dog” data="{{…data}}" wx:for="{{data09558693}}" wx:for-item=“data” wx:for-index=“index” wx:key="*this"></template></view>我们再来看一下另一个拼多多商城模板。那是使用sass做预处理器。由于用到https请求数据,因此需要大家打开右上角进行一个设置它的全貌如下从这里两个示例来看,anu小程序是能hold住非常复杂的应用。如果想了解 anu小程序的进度或一些注意事项,大家可以访问这里 https://github.com/RubyLouvre…也欢迎大家试用与提PR! ...

September 7, 2018 · 1 min · jiezi

简陋至极:微信小程序日历组件(思路)

最近在做微信小程序项目,其中涉及到日历。一直以来,遇到日历,就是网上随便找个插件,这次心血来潮,想着自己去实现一下。这次不是封装功能强大,健硕完美的组件,只是记录一下,主体思路。更多功能还得根据项目需要,自己去挖掘、实现。(大佬轻喷)思路分析首先最主要的一点,就是要计算出某年某月有多少天,其中涉及到大小月,闰、平年二月。其次,弄清楚每月一号对应的是周几。然后,有时为填充完整,还需显示上月残余天数以及下月开始几天,这些又该如何展示。最后,根据自己项目需求实现其它细枝末节。计算每月天数按照一般思路,[1,3,5,7,8,10,12]这几个月是31天,[2,3,6,9,11]这几个月是30天,闰年2月29天,平年2月28天。每次需要计算天数时,都得如此判断一番。方案可行,而且也是大多数人的做法。但是,这个方法,我却觉得有些繁琐。其实换一种思路,也未尝不可。时间戳就是一个很好的载体。当前月一号零时的时间戳,与下月一号零时的时间戳之差,不就是当前月天数的毫秒数嘛。// 获取某年某月总共多少天 getDateLen(year, month) { let actualMonth = month - 1; let timeDistance = +new Date(year, month) - +new Date(year, actualMonth); return timeDistance / (1000 * 60 * 60 * 24); },看到上述代码,你可能会问,是不是还缺少当月为12月时的特殊判断,毕竟涉及到跨年问题。当然,你无需担心,根据MDN中关于Date的表述,js已经为我们考虑好了这一点当Date作为构造函数调用并传入多个参数时,如果数值大于合理范围时(如月份为13或者分钟数为70),相邻的数值会被调整。比如 new Date(2013, 13, 1)等于new Date(2014, 1, 1),它们都表示日期2014-02-01(注意月份是从0开始的)。其他数值也是类似,new Date(2013, 2, 1, 0, 70)等于new Date(2013, 2, 1, 1, 10),都表示时间2013-03-01T01:10:00。计算每月一号是周几呃,这个就不需要说了吧,getDay()你值得拥有// 获取某月1号是周几 getFirstDateWeek(year, month) { return new Date(year, month - 1, 1).getDay() },每个月的数据如何展示如果只是简单展示当月数据,那还是很简单的,获取当月天数,依次遍历,就可以拿到当月所有数据。// 获取当月数据,返回数组 getCurrentArr(){ let currentMonthDateLen = this.getDateLen(this.data.currentYear, this.data.currentMonth) // 获取当月天数 let currentMonthDateArr = [] // 定义空数组 if (currentMonthDateLen > 0) { for (let i = 1; i <= currentMonthDateLen; i++) { currentMonthDateArr.push({ month: ‘current’, // 只是为了增加标识,区分上下月 date: i }) } } this.setData({ currentMonthDateLen }) return currentMonthDateArr },很多时候,为了显示完整,需要显示上下月的残余数据。一般来说,日历展示时,最大是7 X 6 = 42位,为啥是42位,呃,自己去想想吧。当月天数已知,上月残余天数,我们可以用当月1号是周几来推断出来,下月残余天数,正好用42 - 当月天数 -上月残余。// 上月 年、月 preMonth(year, month) { if (month == 1) { return { year: –year, month: 12 } } else { return { year: year, month: –month } } },// 获取当月中,上月多余数据,返回数组 getPreArr(){ let preMonthDateLen = this.getFirstDateWeek(this.data.currentYear, this.data.currentMonth) // 当月1号是周几 == 上月残余天数) let preMonthDateArr = [] // 定义空数组 if (preMonthDateLen > 0) { let { year, month } = this.preMonth(this.data.currentYear, this.data.currentMonth) // 获取上月 年、月 let date = this.getDateLen(year, month) // 获取上月天数 for (let i = 0; i < preMonthDateLen; i++) { preMonthDateArr.unshift({ // 尾部追加 month: ‘pre’, // 只是为了增加标识,区分当、下月 date: date }) date– } } this.setData({ preMonthDateLen }) return preMonthDateArr },// 下月 年、月 nextMonth(year, month) { if (month == 12) { return { year: ++year, month: 1 } } else { return { year: year, month: ++month } } },// 获取当月中,下月多余数据,返回数组 getNextArr() { let nextMonthDateLen = 42 - this.data.preMonthDateLen - this.data.currentMonthDateLen // 下月多余天数 let nextMonthDateArr = [] // 定义空数组 if (nextMonthDateLen > 0) { for (let i = 1; i <= nextMonthDateLen; i++) { nextMonthDateArr.push({ month: ’next’,// 只是为了增加标识,区分当、上月 date: i }) } } return nextMonthDateArr },整合三组数据,就得到了完整的当月数据,格式如下[ {month: “pre”, date: 30}, {month: “pre”, date: 31}, {month: “current”, date: 1}, {month: “current”, date: 2}, … {month: “current”, date: 31}, {month: “next”, date: 1}, {month: “next”, date: 2}]至于上下月切换,选择某年某月等功能,无非就是参数变化而已,自己琢磨琢磨即可。骨架都有了,你想创造什么样的功能还不是手到擒来。完整代码 GitHub ...

September 3, 2018 · 2 min · jiezi