- Skyline|长列表也可以丝滑~
[图片] [图片] 对于长列表出现的白屏、卡顿、界面跳动等问题,小程序提供了新 scroll-view 来解决这一系列问题。我们先来看看效果~ 快速滚动效果对比我们通过一组长列表来展示新旧 scroll-view 在快速滚动下的效果对比。 当长列表快速滚动时,旧 scroll-view 容易出现白屏的情况,新 scroll-view 则不会出现白屏。 左:旧 scroll-view、右:新 scroll-view [视频] 在安卓机器快速滚动过程中,旧 scroll-view 反应卡顿,容易出现手指离开操作时,滚动动画还在进行。 而新 scroll-view 则可以保持界面滚动效果跟随手指,停止滚动则缓慢结束动画效果。 左:旧 scroll-view、右:新 scroll-view ,测试机型:Xiaomi MIX 3 [视频] 反向滚动效果对比在对话等场景下,反向滚动是常见的功能,旧 scroll-view 并没有提供反向滚动的能力,我们来看看旧 scroll-view 下是怎么完成反向滚动的~ 在对话数据在加载的时候,对话列表需要在更新完列表数据之后,再使用 scroll-into-view 或者 scroll-top 来保持当前滚动位置,因为设置滚动位置会有延迟,所以容易出现 界面跳动 的情况。 // .js // scroll-view 滚动到顶部时触发 bindscrolltoupper() { // 先更新列表数据 this.setData({ recycleList: getnewList() }, () => { // 更新完数据后再设置滚动位置 this.setData({ scrollintoview: scrollintoview }) }) } 为了解决界面跳动的问题,社区上也有通过翻转的方法来解决,将 scroll-view 与 scroll-view 的子元素进行翻转。 // .wxss .reserve { transform: rotateX(180deg); } // .wxml 然而进行翻转之后,会遇到手指滚动方向与列表滚动方向相反、scroll-into-view 属性无效等问题。 为了帮开发者们解决反向滚动类列表的一系列问题,新 scroll-view 直接提供了 reverse 属性支持反向滚动的能力,滚动效果更加顺滑。 左:旧 scroll-view、右:新 scroll-view(图片加载期间,GIF 渲染较慢) [视频] 怎么接入新 scroll-view ?新的 scroll-view 使用起来很简单,主要有以下两个步骤: 修改小程序配置scroll-view 增加 type="list"// app.json // "renderer": "skyline" 开启之后所有页面会变成自定义导航,可参考 https://developers.weixin.qq.com/s/Y5Y8rrm37qEY 实现自定义导航 // 也可在 page.json 中配置 "renderer": "skyline" 逐个页面开启 { ... "lazyCodeLoading": "requiredComponents", "renderer": "skyline" } // page.json { ... "disableScroll": true, "navigationStyle": "custom" } // page.wxml ... // 反向滚动 新的 scroll-view 从安卓 8.0.28 / iOS 8.0.30 开始支持,如需兼容低版本需要进行兼容处理。 wx.getSkylineInfo({ success(res) { if (res.isSupported) { // 使用新版 scroll-view } else { // 使用旧版 scroll-view } } }) 如需体验长列表效果,可在微信开发者工具导入该代码片段即可体验:https://developers.weixin.qq.com/s/Y5Y8rrm37qEY 更多接入详情请参考文档 怎么做到的?大家肯定好奇为什么新 scroll-view 可以解决这个头疼的问题呢? 我们来对比一下新旧 scroll-view 有什么区别就可以知道答案啦~ 旧 scroll-view 逻辑层与渲染层的通信需要通过 JSBridge 进行转换,需要一定的时间开销渲染采用异步分块光栅化,当渲染赶不上滚动的速度,来不及渲染则会出现白屏渲染大量节点内存占用高,需要开发者自行优化只渲染在屏节点,开发成本高新 scroll-view 逻辑层与渲染层的通信无需通过 JSBridge 进行转换,减少了大量通信时间开销渲染采用同步光栅化,滚动与渲染在同一线程,不会出现白屏针对长列表进行优化,只渲染在屏节点,内存占用低,减少了一些渲染耗时,且开发接入成本低[图片] 除此之外,新 scroll-view 后续将提供 type="custom" 支持 sticky 吸顶效果、网格布局、瀑布流布局等能力便于开发者接入使用~
2023-08-03 - 在scroll-view中使用sticky
.container{ width: 600rpx; height: 800rpx; } .a { position: sticky; position: -webkit-sticky; top: 0; width: 100%; height: 200rpx; background-color: black; } .b { width: 100%; height: 3000rpx; background-color: red; } <scroll-view class="container"> <view> <view class="a"></view> <view class="b"></view> </view> </scroll-view> sticky的元素在到达父元素的底部时会失效 scroll-view的高度为800rpx,但是scrollHeight为3200rpx,所以在scroll-view中嵌套一个view就能顺利定位
2019-12-18 - 小程序推荐图表库uchart
话说这三个图表库,我都有用过,echart,wx-chart,ucharts, wx-chart由于个人目前没有维护了,echart,总的来说,还是更倾向于pc,而ucharts就不一样了,他的诞生,就是为小程序而生的,他是伴随小程序跨端框架uni-app而来的,所以uchart更小程序更具有一种天然的属性。 ----- 高性能跨平台图表库,支持H5图表、APP图表、小程序图表(微信小程序、支付宝小程序、百度小程序、头条小程序、QQ小程序、360小程序),支持饼图、圆环图、线图、柱状图、区域图、雷达图、圆弧进度图、仪表盘、K线图、条状图、混合图、玫瑰图、漏斗图、词云图、地图。 首页 https://www.ucharts.cn 码云主页 https://gitee.com/uCharts/uCharts 扫码体验地址 [图片] - - - - - - - - - - - - - - - - H5端流行的echart报表因为涉及大量dom操作,无法跨端使用,而wx-chart在跨端和更新方面都不足,如果要做小程序,推荐使用全端可用的[uChart](https://ext.dcloud.net.cn/plugin?id=271)。 - 如只考虑H5端,也可以继续使用echart、f2等常规web图表。 - 如不考虑小程序,那么App端和H5,还可以通过renderjs技术来使用echart、f2等web图表,功能性能比uchart更好。[什么是renderjs](https://uniapp.dcloud.io/frame?id=renderjs)、[基于renderjs使用echart的示例](https://ext.dcloud.net.cn/plugin?id=1207)
2020-04-12 - 小商店获取订单列表API接口中access_token如何获取?
目前我已经开通了组件版微信小商店也已经产生了订单,现在我们自己的web应用希望通过 获取订单列表接口 https://api.weixin.qq.com/product/order/get_list?access_token=xxxxxxxxx,拉取到小商店支付成功的订单。 现在遇到问题 上述接口的access_token参数如何获取呢? (是通过 https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=小程序APPID&secret=小程序APPSECRET 获取吗?)
2021-03-02 - 视频号直播带货公屏互动需要一一回复吗?
每次直播带货公屏上都会有粉丝问问题,这些问题达人需要全部都一一作答吗? 不全部都回的话,那么哪些可以回,哪些没必要回呢? 如果你直播的时候也遇到这个问题可以看下面这个视频,看一下专业主播的建议吧! [视频] 学会了吗?相信看完这个视频后,下次开播就知道怎么回复公屏处粉丝的问题了!
2022-01-30 - 免费接入!快递100上线小程序【快递查询】功能插件,向第三方小程序开放接入
快递100“快递跟踪”微信小程序插件上线啦! 帮助所有小程序解决快递物流查询问题,现面向所有第三方小程序、小程序开发服务商(包括个体小程序)免费开放。 [图片] 点链接→https://fuwu.weixin.qq.com/service/detail/00008caeab84c07c17dcdabf55b815,立即添加插件(添加时请从电脑端打开链接) 公开数据显示,今年上半年微信小程序数量已超过430万。 随着小程序生态不断发展,越来越多商家和开发者在小程序上建立自有商城。大到京东这样的巨型平台,小到一个公众号、博主自己开的店铺,用户都可以在小程序上下单。业务逐渐壮大后,物流却成为困扰不少商家和开发者的一大难题。 [图片] 大平台有大量的资金和人力来调配资源,自主开发接入物流公司系统,给顾客及时物流反馈;而对于那些中小店铺的小程序商家们来说,没有足够人力、财力支撑,无法自主开发接入。 近日,中国领先的快递物流信息服务商快递100宣布,正式上线“快递跟踪”小程序功能插件,开放快递物流信息查询模块,允许第三方小程序 免费 接入。 “快递跟踪”小程序插件整合了快递100快递查询能力,支持全球1000+快递物流公司信息查询,对全行业的小程序免费开放接入,包括电商平台、商家、医药寄送、信息查询或本地生活服务平台等 任何有物流查询需求的小程序开发者,为企业、商家、个体小程序赋能。 点链接→https://fuwu.weixin.qq.com/service/detail/00008caeab84c07c17dcdabf55b815,立即添加插件 01 无门槛免费接入 无论是电商商城,还是社群团购、回收类等任何有涉及快递物流环节的小程序,物流信息查询是必须重视的一项服务。卖家是否能提供及时的物流信息更新服务,会影响到用户的二次购买决策。 “快递跟踪”小程序插件,是免费接入。接入插件后,用户只要在小程序内点击快递单号,就可以查看最新物流信息,有效提升用户的购物体验,提高小程序的回访率和复购转化率。 [图片] 02 原生体验,无第三方跳转 快递100“快递跟踪”插件依托微信小程序生态,第三方小程序接入后无需任何跳转,在自己的小程序内即可直接查看物流信息,简化用户操作流程。 [图片] 接入方式也非常简单快捷,模板化快速接入,无需再次开发,几个小时即可完成接入,大大降低开发和运营成本。 快递100“快递跟踪”插件开放接入,不仅能够帮助小程序开发者降低物流服务的开发门槛和成本,同时也为小程序商家提供了更好服务用户的方式。 03 支持国内外1000+家快递公司物流查询 通过快递100“快递跟踪”小程序插件,支持全网快递物流查询,可查看国内、国际1000+快递物流公司的信息,同时还提供官方客服热线。 “快递跟踪”插件服务稳定,让商家、开发者管理更加方便。 除了常见的电商场景,“快递跟踪”插件同样非常适合有特定物品物流信息查询需求的机构和企业接入 —— 例如医院类公众号,病历档案预约寄出后的进度查询;驾校机构,寄出驾照后的进度查询;校园机构的报到证、档案等资料的快递查询等。 点链接→https://fuwu.weixin.qq.com/service/detail/00008caeab84c07c17dcdabf55b815,立即添加插件 另外,快递100也可提供快递信息推送、实时快递、地图轨迹API等服务,点这里→https://api.kuaidi100.com/ 了解详情 快递100是中国领先的快递物流信息服务商,国家高新技术企业、新基建代表企业。 快递100目前拥有个人注册用户1.6亿,企业客户60万+,日均查询量3亿次,是国内查询量最大的快递物流信息查询平台;年寄件量超8亿单,寄件功能官方合作京东、邮政、德邦、圆通、韵达、DHL、TNT、UPS等多家国内外快递公司。 快递100致力构建中国最大的物流信息服务枢纽,始终秉承开放态度与快递行业共创共赢,为用户、商家、企业提供专业、可靠的服务,实现互联互通互动。 点链接→https://fuwu.weixin.qq.com/service/detail/00008caeab84c07c17dcdabf55b815,立即添加插件
2021-10-08 - 云开发批量上传图片,上传完图片再上传数据库 [即抄即用,拎包入住]
大家好,又是我拎包哥,今天我们来实现在云开发中批量上传图片。 经过Stephen哥的指正,我改用了Promise.all的方法来达到目的。 Promise.all的作用就是等待所包含的promise函数结束后再执行下一步逻辑,非常方便好用!const db = wx.cloud.database() const test = db.collection('test') Page({ onLoad() { this.imgList = [] wx.chooseImage({ success: (res) => { this.TFP = res.tempFilePaths } }) }, btn() { let promiseMethod = new Array(this.TFP.length) for (let i = 0; i < this.TFP.length; i++) { promiseMethod[i] = wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } Promise.all([...promiseMethod]).then(() => { test.add({ data: { imgList: this.imgList } }) }) } }) --------------------------------------我是分割线-------------------------------------- async await 要点: ctrl c + ctrl v这里用了await阻塞在wx.cloud.uploadFile前面,避免还没上传完图片就往数据库插入数组。减少了then里的代码,美观逼格高。嘻嘻嘻。await wx.cloud.uploadFile不能放在wx.chooseImage里,如果可以的话,请告诉我怎么做,谢谢!欢迎交流,指出错误,我立刻修改么么哒。 标准版 const db = wx.cloud.database() const test = db.collection('test') Page({ onLoad() { this.imgList = [] wx.chooseImage({ success: (res) => { this.TFP = res.tempFilePaths } }) }, async btn() { this.imgList = [] console.log(this.TFP) for (let i = 0; i < this.TFP.length; i++) { await wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } test.add({ data: { imgList: this.imgList } }) } }) 新手最爱一锅炖版(不推荐) 为什么不推荐呢,因为选择图片并不意味着要上传图片,用户还没进行最终的确定操作(不过可以用来了解async await)。 onLoad() { this.imgList = [] wx.chooseImage({ success: async res => { this.TFP = res.tempFilePaths for (let i = 0; i < this.TFP.length; i++) { await wx.cloud.uploadFile({ cloudPath: 'img' + i + '.png', filePath: this.TFP[i] }).then(res => { this.imgList.push(res.fileID) }) } test.add({ data: { imgList: this.imgList } }) } }) } [图片] ==========================end==========================
2020-05-17 - 如何突破一次只能获取20条记录的limit限制?只需要一行代码。
笔者刚遇到需要一次性拉取超过100条(云函数里超过1000条)记录的这种需求。 一般情况下,会有下面两种处理方式: 1、先获取总数,再for循环,每次拉取limit条记录;(可结合Promise.all并发) 2、递归拉取,每次拉取limit条记录,直到拉取的记录数量小于limit。 以上两种方式都比较麻烦,于是动了一脑筋,以最简单的方式实现上面的需求。 极简代码如下: db.collection('order').aggregate() .match({ status:'已付费' }) .addFields({ tempTag:1 //增加一个临时标签;也可以不要addFields这个阶段; }) .group({ _id:'$tempTag', orders:$.push('$$ROOT') //一次性拉取超过100条或者1000条记录 }) .end() .then(res=>{ let orders = res.list[0].orders console.log(orders) }) 一个临时标签,搞定。 小心数据量太大搞崩了,崩溃的极限是多少,需要各位自行摸索了。 需要注意的是,如果是云函数里执行以上代码(比如lookup),返回小程序端的数据量不要超过1M。
2021-03-15 - 数据库原子操作和事务讲解
使用更新指令(如 inc、mul、addToSet)可以对云数据库的一条记录和记录内的子文档(结合反范式化设计)进行原子操作,但是如果要跨多个记录或跨多个集合的原子操作时,就需要使用云数据库的事务能力。 12.6.1 更新指令的原子操作关系型数据库是很难做到通过一个语句对数据强制一致性的需求来表示的,只能依赖事务。但是云开发数据库由于可以反范式化设计内嵌子文档,以及更新指定可以对单个记录或同一个记录内的子文档进行原子操作,所以通常情况下,云开发数据库不必使用事务。 比如调整某个订单项目的数量之后,应该同时更新该订单的总费用,我们可以设计采用如下方式设计该集合,比如订单的集合为 order: { "_id": "2020030922100983", "userID": "124785", "total":117, "orders": [{ "item":"苹果", "price":15, "number":3 },{ "item":"火龙果", "price":18, "number":4 }] } 客户在下单的时候经常会调整订单内某个商品比如苹果的购买数量,而下单的总价又必须同步更新,不能购买数量减少了,但是总价不变,这两个操作必须同时进行,如果是使用关系型数据库,则需要先通过两次查询,更新完数据之后,再存储进数据库,这个很容易出现有的成功,有的没有成功的情况。但是云开发的数据库则可以借助于更新指令做到一条更新来实现两个数据同时成功或失败: db.collection("order") .doc("2020030922100983") .update({ data: { "orders.0.number": _.inc(1), total: _.inc(15), }, }); 这个操作只是在单个记录里进行,那要实现跨记录要进行原子操作呢?更新指令其实是可以做到事务仿真的,但是比较麻烦,这时就建议用事务了。 12.6.2 事务与 ACID事务就是一段数据库语句的批处理,但是这个批处理是一个 atom(原子),多个增删改的操作是绑定在一起的,不可分割,要么都执行,要么回滚(rollback)都不执行。比如银行转账,需要做到一个账户的钱汇出去了,那另外一个账户就一定会收到钱,不能钱汇出去了,但是钱没有到另外一个的账上;也就是要执行转账这个事务,会对 A 用户的账户数据和 B 用户的账户数据做增删改的处理,这两个处理必须一起成功一起失败。 1、ACID一般来说,事务是必须满足 4 个条件(ACID): Atomicity(原子性)、Consistency(稳定性)、Isolation(隔离性)、Durability(可靠性): 原子性:整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中一部分操作,一致性:事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行前后,数据库都必须处于一致性状态。换句话说,事务的执行结果必须是使数据库从一个一致性状态转变到另一个一致性状态。比如在执行事务前,A 用户账户有 50 元,B 用户账户有 150 元;执行 B 转给 A 50 元事务后,两个用户账户总和还是 200 元。隔离性:事务的隔离性是指在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间事务之间,互不干扰。比如在线银行,同时转账的人虽然很多,但是不会出现影响 A 与 B 之间的转账;可靠性:即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束时的状态,已提交事务的更新不会丢失。 2、云函数事务注意事项01不支持批量操作,只支持单记录操作 在事务中不支持批量操作(where 语句),只支持单记录操作(collection.doc, collection.add),这可以避免大量锁冲突、保证运行效率,并且大多数情况下,单记录操作足够满足需求,因为在事务中是可以对多个单个记录进行操作的,也就是可以比如说在一个事务中同时对集合 A 的记录 x 和 y 两个记录操作、又对集合 B 的记录 z 操作。 02云数据库采用的是快照隔离 对于两个并发执行的事务来说,如果涉及到操作同一条记录的时候,可能会发生问题。因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。 脏读:指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据;不可重复读:在一个事务内两次读到的数据是不一样的,受到另一个事务修改后提交的影响,因此称为是不可重复读幻读:第一个事务对表进行读取,当第二个事务对表进行增加或删除操作事务提交后,第一个事务再次读取,会出现增加或减少行数的情况云开发的数据库系统的事务过程采用的是快照隔离(Snapshot isolation),可以避免并发操作带来数据不一致的问题。 事务期间,读操作返回的是对象的快照,而非实际数据事务期间,写操作会:1. 改变快照,保证接下来的读的一致性;2. 给对象加上事务锁事务锁:如果对象上存在事务锁,那么:1. 其它事务的写入会直接失败;2. 普通的更新操作会被阻塞,直到事务锁释放或者超时事务提交后,操作完毕的快照会被原子性地写入数据库中 12.6.3 事务操作的两套 API云开发数据库的事务提供两种操作风格的接口,一个是简易的、带有冲突自动重试的 runTransaction 接口,一个是流程自定义控制的 startTransaction 接口。通过 runTransaction 回调中获得的参数 transaction 或通过 startTransaction 获得的返回值 transaction,我们将其类比为 db 对象,只是在其上进行的操作将在事务内的快照完成,保证原子性。transaction 上提供的接口树形图一览: transaction |-- collection 获取集合引用 | |-- doc 获取记录引用 | | |-- get 获取记录内容 | | |-- update 更新记录内容 | | |-- set 替换记录内容 | | |-- remove 删除记录 | |-- add 新增记录 |-- rollback 终止事务并回滚 |-- commit 提交事务(仅在使用 startTransaction 时需调用) 1、通过 runTransaction 回调获得 transaction以下提供一个使用 runTransaction 接口的,两个账户之间进行转账的简易示例。事务执行函数由开发者传入,函数接收一个参数 transaction,其上提供 collection 方法和 rollback 方法。collection 方法用于取数据库集合记录引用进行操作,rollback 方法用于在不想继续执行事务时终止并回滚事务。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const _ = db.command; exports.main = async (event) => { try { const result = await db.runTransaction(async (transaction) => { const aaaRes = await transaction.collection("account").doc("aaa").get(); const bbbRes = await transaction.collection("account").doc("bbb").get(); if (aaaRes.data && bbbRes.data) { const updateAAARes = await transaction .collection("account") .doc("aaa") .update({ data: { amount: _.inc(-10), }, }); const updateBBBRes = await transaction .collection("account") .doc("bbb") .update({ data: { amount: _.inc(10), }, }); console.log(`transaction succeeded`, result); return { aaaAccount: aaaRes.data.amount - 10, }; } else { await transaction.rollback(-100); } }); return { success: true, aaaAccount: result.aaaAccount, }; } catch (e) { console.error(`事务报错`, e); return { success: false, error: e, }; } }; 事务执行函数必须为 async 异步函数或返回 Promise 的函数,当事务执行函数返回时,SDK 会认为用户逻辑已完成,自动提交(commit)事务,因此务必确保用户事务逻辑完成后才在 async 异步函数中返回或 resolve Promise。 2、通过 startTransaction 获得 transactiondb.startTransaction(),开启一个新的事务,之后即可进行 CRUD 操作;db.startTransaction().transaction.commit(),提交事务保存数据,在提交之前事务中的变更的数据对外是不可见的;db.startTransaction().rollback(),事务终止并回滚事务,例如,一部分数据更新失败,对已修改过的数据也进行回滚。const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const db = cloud.database({ throwOnNotFound: false, }); const _ = db.command; exports.main = async (event) => { try { const transaction = await db.startTransaction(); const aaaRes = await transaction.collection("account").doc("aaa").get(); const bbbRes = await transaction.collection("account").doc("bbb").get(); if (aaaRes.data && bbbRes.data) { const updateAAARes = await transaction .collection("account") .doc("aaa") .update({ data: { amount: _.inc(-10), }, }); const updateBBBRes = await transaction .collection("account") .doc("bbb") .update({ data: { amount: _.inc(10), }, }); await transaction.commit(); return { success: true, aaaAccount: aaaRes.data.amount - 10, }; } else { await transaction.rollback(); return { success: false, error: `rollback`, rollbackCode: -100, }; } } catch (e) { console.error(`事务报错`, e); } }; 也就是说对于多用户同时操作(主要是写)数据库的并发处理问题,我们不仅可以使用原子更新,还可以使用事务。其中原子更新主要用户操作单个记录内的字段或单个记录里内嵌的数组对象里的字段,而事务则主要是用于跨记录和跨集合的处理。
2021-09-10 - 小程序奇技淫巧之 -- 日志能力
日志与反馈 前端开发在进行某个问题定位的时候,日志是很重要的。因为机器兼容性问题、环境问题等,我们常常无法复现用户的一些bug。而微信官方也提供了较完整的日志能力,我们一起来看一下。 用户反馈 小程序官方提供了用户反馈携带日志的能力,大概流程是: 开发中日志打印,使用日志管理器实例 LogManager。 用户在使用过程中,可以在小程序的 profile 页面(【右上角胶囊】-【关于xxxx】),点击【投诉与反馈】-【功能异常】(旧版本还需要勾选上传日志),则可以上传日志。 在小程序管理后台,【管理】-【反馈管理】,就可以查看上传的日志(还包括了很详细的用户和机型版本等信息)。 这个入口可能对于用户来说过于深入(是的,官方也发现这个问题了,所以后面有了实时日志),我们小程序也可以通过[代码]button[代码]组件,设置[代码]openType[代码]为[代码]feedback[代码],然后用户点击按钮就可以直接拉起意见反馈页面了。利用这个能力,我们可以监听用户截屏的操作,然后弹出浮层引导用户主动进行反馈。 [代码]<view class="dialog" wx:if="{{isFeedbackShow}}"> <view>是否遇到问题?</view> <button open-type="feedback">点击反馈</button> </view> wx.onUserCaptureScreen(() => { // 设置弹窗出现 this.setData({isFeedbackShow: true}) }); [代码] LogManager 关于小程序的 LogManager,大概是非常实用又特别低调的一个能力了。它的使用方式其实和 console 很相似,提供了 log、info、debug、warn 等日志方式。 [代码]const logger = wx.getLogManager() logger.log({str: 'hello world'}, 'basic log', 100, [1, 2, 3]) logger.info({str: 'hello world'}, 'info log', 100, [1, 2, 3]) logger.debug({str: 'hello world'}, 'debug log', 100, [1, 2, 3]) logger.warn({str: 'hello world'}, 'warn log', 100, [1, 2, 3]) [代码] 打印的日志,从管理后台下载下来之后,也是很好懂: [代码]2019-6-25 22:11:6 [log] wx.setStorageSync api invoke 2019-6-25 22:11:6 [log] wx.setStorageSync return 2019-6-25 22:11:6 [log] wx.setStorageSync api invoke 2019-6-25 22:11:6 [log] wx.setStorageSync return 2019-6-25 22:11:6 [log] [v1.1.0] request begin 2019-6-25 22:11:6 [log] wx.request api invoke with seq 0 2019-6-25 22:11:6 [log] wx.request success callback with msg request:ok with seq 0 2019-6-25 22:11:6 [log] [v1.1.0] request done 2019-6-25 22:11:7 [log] wx.navigateTo api invoke 2019-6-25 22:11:7 [log] page packquery/pages/index/index onHide have been invoked 2019-6-25 22:11:7 [log] page packquery/pages/logs/logs onLoad have been invoked 2019-6-25 22:11:7 [log] [v1.1.0] logs | onShow | | [] 2019-6-25 22:11:7 [log] wx.setStorageSync api invoke 2019-6-25 22:11:7 [log] wx.setStorageSync return 2019-6-25 22:11:7 [log] wx.reportMonitor api invoke 2019-6-25 22:11:7 [log] page packquery/pages/logs/logs onShow have been invoked 2019-6-25 22:11:7 [log] wx.navigateTo success callback with msg navigateTo:ok [代码] LogManager 最多保存 5M 的日志内容,超过5M后,旧的日志内容会被删除。基础库默认会把 App、Page 的生命周期函数和 wx 命名空间下的函数调用写入日志,基础库的日志帮助我们定位具体哪些地方出了问题。 实时日志 小程序的 LogManager 有一个很大的痛点,就是必须依赖用户上报,入口又是右上角胶囊-【关于xxxx】-【投诉与反馈】-【功能异常】这么长的路径,甚至用户的反馈过程也会经常丢失日志,导致无法查问题。 为帮助小程序开发者快捷地排查小程序漏洞、定位问题,微信推出了实时日志功能。从基础库 2.7.1 开始,开发者可通过提供的接口打印日志,日志汇聚并实时上报到小程序后台。 使用方式如下: 使用 wx.getRealtimeLogManager 在代码⾥⾯打⽇志。 可从小程序管理后台【开发】-【运维中心】-【实时日志】进入日志查询页面,查看开发者打印的日志信息。 开发者可通过设置时间、微信号/OpenID、页面链接、FilterMsg内容(基础库2.7.3及以上支持setFilterMsg)等筛选条件查询指定用户的日志信息: [图片] 由于后台资源限制,实时日志使用规则如下: 为了定位问题方便,日志是按页面划分的,某一个页面,在onShow到onHide(切换到其它页面、右上角圆点退到后台)之间打的日志,会聚合成一条日志上报,并且在小程序管理后台上可以根据页面路径搜索出该条日志。 每个小程序账号每天限制500万条日志,日志会保留7天,建议遇到问题及时定位。 一条日志的上限是5KB,最多包含200次打印日志函数调用(info、warn、error调用都算),所以要谨慎打日志,避免在循环里面调用打日志接口,避免直接重写console.log的方式打日志。 意见反馈里面的日志,可根据OpenID搜索日志。 setFilterMsg 可以设置过滤的 Msg。这个接口的目的是提供某个场景的过滤能力,例如[代码]setFilterMsg('scene1')[代码],则在 MP 上可输入 scene1 查询得到该条日志。比如上线过程中,某个监控有问题,可以根据 FilterMsg 过滤这个场景下的具体的用户日志。FilterMsg 仅支持大小写字母。如果需要添加多个关键字,建议使用 addFilterMsg 替代 setFilterMsg。 日志开发技巧 既然官方提供了 LogManager 和实时日志,我们当然是两个都要用啦。 log.js 我们将所有日志的能力都封装在一起,暴露一个通用的接口给调用方使用: [代码]// log.js const VERSION = "0.0.1"; // 业务代码版本号,用户灰度过程中观察问题 const canIUseLogManage = wx.canIUse("getLogManager"); const logger = canIUseLogManage ? wx.getLogManager({level: 0}) : null; var realtimeLogger = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null; /** * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function DEBUG(file, ...args) { console.debug(file, " | ", ...args); if (canIUseLogManage) { logger!.debug(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } /** * * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function RUN(file, ...args) { console.log(file, " | ", ...args); if (canIUseLogManage) { logger!.log(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } /** * * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function ERROR(file, ...args) { console.error(file, " | ", ...args); if (canIUseLogManage) { logger!.warn(`[${VERSION}]`, file, " | ", ...args); } if (realtimeLogger) { realtimeLogger.error(`[${VERSION}]`, file, " | ", ...args); // 判断是否支持设置模糊搜索 // 错误的信息可记录到 FilterMsg,方便搜索定位 if (realtimeLogger.addFilterMsg) { try { realtimeLogger.addFilterMsg( `[${VERSION}] ${file} ${JSON.stringify(args)}` ); } catch (e) { realtimeLogger.addFilterMsg(`[${VERSION}] ${file}`); } } } } // 方便将页面名字自动打印 export function getLogger(fileName: string) { return { DEBUG: function(...args) { DEBUG(fileName, ...args); }, RUN: function(...args) { RUN(fileName, ...args); }, ERROR: function(...args) { ERROR(fileName, ...args); } }; } [代码] 通过这样的方式,我们在一个页面中使用日志的时候,可以这么使用: [代码]import { getLogger } from "./log"; const PAGE_MANE = "page_name"; const logger = getLogger(PAGE_MANE); [代码] autolog-behavior 现在有了日志组件,我们需要在足够多的地方记录日志,才能在问题出现的时候及时进行定位。一般来说,我们需要在每个方法在被调用的时候都打印一个日志,所以这里封装了一个 autolog-behavior 的方式,每个页面(需要是 Component 方式)中只需要引入这个 behavior,就可以在每个方法调用的时候,打印日志: [代码]// autolog-behavior.js import * as Log from "../utils/log"; /** * 本 Behavior 会在小程序 methods 中每个方法调用前添加一个 Log 说明 * 需要在 Component 的 data 属性中添加 PAGE_NAME,用于描述当前页面 */ export default Behavior({ definitionFilter(defFields) { // 获取定义的方法 Object.keys(defFields.methods || {}).forEach(methodName => { const originMethod = defFields.methods![methodName]; // 遍历更新每个方法 defFields.methods![methodName] = function(ev, ...args) { if (ev && ev.target && ev.currentTarget && ev.currentTarget.dataset) { // 如果是事件类型,则只需要记录 dataset 数据 Log.RUN(defFields.data.PAGE_NAME, `${methodName} invoke, event dataset = `, ev.currentTarget.dataset, "params = ", ...args); } else { // 其他情况下,则都记录日志 Log.RUN( defFields.data.PAGE_NAME, `${methodName} invoke, params = `, ev, ...args); } // 触发原有的方法 originMethod.call(this, ev, ...args); }; }); } }); [代码] 我们能看到,日志打印依赖了页面中定义了一个[代码]PAGE_NAME[代码]的 data 数据,所以我们在使用的时候可以这么处理: [代码]import { getLogger } from "../../utils/log"; import autologBehavior from "../../behaviors/autolog-behavior"; const PAGE_NAME = "page_name"; const logger = getLogger(PAGE_NAME); Component({ behaviors: [autologBehavior], data: { PAGE_NAME, // 其他数据 }, methods: { // 定义的方法会在调用的时候自动打印日志 } }); [代码] 页面如何使用 Behavior 看看官方文档:事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应[代码]json[代码]文件中包含[代码]usingComponents[代码]定义段。 完整的项目可以参考wxapp-typescript-demo。 参考 LogManager 实时日志 Component构造器 behaviors 结束语 使用自定义组件的方式来写页面,有特别多好用的技巧,behavior 就是其中一个比较重要的能力,大家可以发挥自己的想象力来实现很多奇妙的功能。
2019-12-10 - 微信小程序 -- 基于 movable-view 实现拖拽排序
微信小程序 – 基于 movable-view 实现拖拽排序 项目基于[代码]colorui[代码]样式组件 ColorUI组件库 (color-ui.com) 1.实现效果 [图片] 2. 设计思路 movable-view 绑定块移动事件的 块[代码]ID[代码] ,块移动的坐标 移动结束后触发[代码]moveEnd[代码]事件,根据[代码]Y[代码]坐标对对象数组进行排序 根据排序结果重置块位置 3.实现代码 代码已经进行了最简化处理 图中效果实现需引入[代码]colorui[代码]的[代码]main.wxss[代码]样式部分。 wxml [代码]<movable-area class="padding text-center bg-grey" style="width:100%;height:500px;" > <movable-view class="radius shadow bg-white" style="width:80%;height:80px;z-index:{{index==moveId?2:1}}" wx:for="{{tabList}}" wx:key="index" x="{{item.x}}" y="{{item.y}}" direction="all" bindchange="moveStatus" bindtouchend='moveEnd' data-moveid="{{index}}"> {{item.name}}</movable-view> </movable-area> [代码] js [代码]var compare = function (obj1, obj2) { var val1 = obj1.y; var val2 = obj2.y; if (val1 < val2) { return -1; } else if (val1 >= val2) { return 1; } else { return 0; } } Page({ /** * 页面的初始数据 */ data: { branchid:'', appdocid:'', tabList:[ { name:'十步杀一人' }, { name:'千里不留行' }, { name:'事了拂衣去' }, { name:'深藏身与名' } ], //移动的是哪个元素块 moveId:null, //最终停止的位置 endX:0, endY:0 }, initMove(){ let tabList = this.data.tabList; var tarr = [] tabList.forEach(function(ele,index){ let obj = ele obj.id = index obj.x = 30 obj.y = 100*index +20 tarr.push(obj) }) console.log(tarr) this.setData({ tabList:tarr }) }, moveEnd(e){ console.log(e) var that = this; that.setData({ ["tabList["+that.data.moveId+"].x"]:that.data.endX, ["tabList["+that.data.moveId+"].y"]:that.data.endY },()=>{ let tabList = this.data.tabList; tabList = tabList.sort(compare); that.setData({ tabList },()=>{ setTimeout(function(){ that.initMove(); },500) }) }) //计算位置 }, moveStatus(e){ // console.log(e) //移动的块ID var moveid = e.currentTarget.dataset.moveid; //最终坐标 let x = e.detail.x let y = e.detail.y this.setData({ moveId:moveid, endX:x, endY:y }) }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function () { this.initMove(); } }) [代码] 4.参考文档 movable-view | 微信开放文档 (qq.com)
2021-06-17 - 小程序/小游戏动态生成分享海报 - 技术方案分享
在应用开发过程中,我们会遇到各种各样的分享场景,例如邀请、拉新、分享内容等。分享链接是 Web 时代常见的分享形式,实现也相对容易。但是现在人们时间大都花在了 APP 上,所以应用之间的分享越来越重要,然而应用之间分享链接却不是那么顺利和有效果。往往受以下制约: 纯文字链接,依靠文字向外界传达信息,信息量小、可信度低。群里丢了一个链接进来,什么描述都没有,大多数人都不会去点。链接的描述一般也不会太长,信息不会太多。分享的目标应用的外链策略。淘宝购物链接不能分享到微信、营销链接容易被微信封禁,都是受微信外链策略的影响。平台分享机制限制。小程序的转发功能允许用户直接将小程序分享到联系人中,却无法分享到朋友圈。若开发者希望用户可以分享小程序到朋友圈中,通常需要生成分享海报图片,分享图片到朋友圈中。所以 APP、H5、小程序等应用中分享功能,除了实现分享链接以外,还需要生成分享海报图片,在图片上展现更丰富的内容,一图胜千言。 如何低成本地生成内容丰富的海报图片,就是我们想要解决的问题。 常见技术方案从生成图片的位置划分,可以将方案划分为两种:客户端生成、服务端生成。 在客户端生成图片是将图片中的元素都下载到本地,然后使用绘图 API 进行绘制,典型方案就是使用 Canvas 来绘制图片。 在服务端生成图片,又可以分为两种,一种是在服务端使用绘图库绘制,然后返回图片或图片链接给客户端;另一种是在服务端使用HTML + CSS 生成带有样式的网页,然后使用无头浏览器截图,返回图片或图片链接给客户端。 简而言之,一般会使用下面这三种方式: 在客户端使用 Canvas 生成图片在服务器上使用网页完成样式渲染,然后截图返回给客户端使用后端绘图库绘制,然后返回给客户端下面逐一分析各种方案生成海报图片的优缺点。 客户端使用 Canvas 生成海报图片优点: 渲染过程在每个客户端中完成,渲染相对独立,基本上不需要考虑并发的问题。Canvas 特性丰富,可以实现样式复杂的图片渲染。缺点也很明显: 上手门槛高,需要灵活使用 Canvas API代码可读性差,调试过程复杂。代码复用程度低,每个端都需要重新编码。客户端型号众多,用户设备上的表现还可能与在开发机上的表现存在差异。兼容性太差,这是客户端渲染最大的痛点。如果有远程图片,可能会因跨域,无法下载,导致绘制失败。推荐阅读:小程序canvas绘制海报 服务端浏览器,网页截图在服务器上使用 HTML + CSS 在无头浏览器中完成网页样式布局与内容填充,然后使用无头浏览器提供的截图 API,将生成的网页截图保存。无头浏览器一般会选用 [代码]Puppeteer[代码]。 [代码]Puppeteer[代码] 是谷歌官方团队开发的一个 Node.js 库,它提供了一些 API 用来控制浏览器的行为,比如打开网页、模拟输入、点击按钮、屏幕截图等操作,通过这些 API 可以完成很多有趣的事情,比如本文要讲的海报渲染服务,就会用到屏幕截图功能。 这种方案的优缺点也很明显。 优点: 上手简单,只需要了解 HTML 、CSS 就可以代码可读性高,易于调试得力于HTML、CSS,表达力强。只要在网页上能实现,就可以应用到海报图片中。返回给客户端的是图片链接,不用考虑兼容性。服务端生成图片带来的最大好处是多端兼容。但这也会引入一个问题,成本高。 在后端需要运行一个 Node 服务来跑[代码]Puppeteer[代码] 控制一个浏览器,性能太低。一个4核16G内存的服务器生成图片,峰值QPS只有 10-20,在较差情况下每秒只能生成10张图片。 推荐阅读:使用 Puppeteer 搭建统一海报渲染服务 服务端绘图库绘制图片在服务端中,使用绘图库,绘制图片,然后将图片保存至 CDN 中,再返回图片链接给客户端。常用编程语言都有绘图库,例如 PHP 的 GD 库。相较于控制浏览器截图,这个方案的性能更高,也具有服务端渲染的好处,但灵活性却没有使用CSS控制样式高。 优点: 性能高服务端架构统一,开发者不用单独维护一个Node.js 服务。代码可读性高缺点: 复杂样式,开发时间长,需要微调。自适应布局困难。推荐阅读:PHP 使用GD库合成带二维码的海报步骤以及源码实现 上面介绍了三种生成分享海报图片的常用方案,了解了实现原理。开发者在实现这些方案时都需要进行独立的开发,维护复杂的样式代码,每增加一种海报,就需要维护一份样式代码。 海报只是业务中很小的一环,自己维护一个海报渲染服务,付出的成本与收益之间不成正比。所以我们更推荐使用第三方海报/图片渲染服务,来完成实现我们的想法。 imgrender.cn 动态图片渲染服务Imgrender 是一个免费的图片渲染服务,通过一个API,根据配置动态渲染图片,快速生成不同内容的图片。渲染模板配置简单,特别适合拥有不同分享海报的应用,快速、动态地生成分享海报。Imgrender 支持「文本」、「图片」、「二维码」、「矩形」、「线段」五种组件,可满足绝大多数海报的渲染需求。 👏 免费📝 API 优先🛣 动态渲染,内容可动态调整🖥 易于配置,方便调试📱 兼容性高,渲染结果只与配置有关⚡️ 快速、稳定,平均响应时间 400msImgrender 只有一个核心 API,通过 API 传递海报内容,就可以动态生成不同内容的图片。海报内容完全配置化,在完成设计稿后,按照设计尺寸和位置生成配置即可。 使用服务也很简单,只需要请求 [代码]POST https://api.imgrender.net/open/v1/pics[代码]。将下面的 curl 命令复制到终端请求一下,就可以得到渲染好的海报图片链接。 curl 命令中 Apikey 是临时的,可能会失效,你可以在 imgrender 中免费获取 API Key。 curl -X "POST" "https://api.imgrender.net/open/v1/pics" \ -H "Authorization: Apikey 183666749185461475.PLbfIpBpeMkpgbj1Tr+177Mv3Jo3wIIySyf8V5ZeDhs=" \ -H "Content-Type: application/json; charset=utf-8" \ -d '{ "width": 640, "height": 1050, "backgroundColor": "#d75650", "blocks":[ { "x": 15, "y": 268, "width": 610, "height": 770, "backgroundColor": "#fff", "borderColor": "#fff" } ], "texts":[ { "x": 320, "y": 185, "text": "Davinci Li", "font": "jiangxizhuokai", "fontSize": 22, "color": "#fff", "width": 320, "textAlign": "center" }, { "x": 320, "y": 220, "text": "邀请你来参加抽奖", "font": "jiangxizhuokai", "fontSize": 22, "color": "#fff", "width": 320, "textAlign": "center" }, { "x": 30, "y": 640, "text": "奖品: 本田-CB650R 摩托车", "font": "jiangxizhuokai", "fontSize": 22, "color": "#000", "width": 580, "textAlign": "left" }, { "x": 30, "y": 676, "text": "01 月 31 日 18:00 自动开奖", "font": "jiangxizhuokai", "fontSize": 18, "color": "#9a9a9a", "width": 580, "textAlign": "left" }, { "x": 320, "y": 960, "text": "长按识别二维码,参与抽奖", "font": "jiangxizhuokai", "fontSize": 22, "color": "#9a9a9a", "width": 580, "textAlign": "center" } ], "lines":[ { "startX": 30, "startY": 696, "endX": 610, "endY": 696, "width": 1, "color": "#E1E1E1", "zIndex": 1 } ], "images":[ { "x": 248, "y": 25, "width": 120, "height": 120, "url": "https://img-chengxiaoli-1253325493.cos.ap-beijing.myqcloud.com/bikers_327390-13.jpg", "borderRadius": 60, "zIndex": 1 }, { "x": 108, "y": 285, "width": 400, "height": 300, "url": "https://img-chengxiaoli-1253325493.cos.ap-beijing.myqcloud.com/cb650R.jpeg", "zIndex": 1 } ], "qrcodes":[ { "x": 208, "y": 726, "size": 200, "content": "http://weixin.qq.com/r/yRzk-JbEbMsTrdKf90nb", "foregroundColor": "#000", "backgroundColor": "#fff", "zIndex": 1 } ] }' 请求返回内容: { "code":0, "message":"OK", "data":"https://davinci.imgrender.cn/c3037d467c163bd903760f96a34f3bcd.jpg?sign=1616722062-plQZQ4xtth9tEthx-0-2a98ba98e5fd44cc6dffb3aec6d3398f" } [代码]data[代码] 字段就是动态渲染好的海报图片链接,下载或打开链接就可保存图片。查看详细使用方法。 [图片] imgrender.net 推荐按以下最佳实践来使用海报生成服务: [图片] 所有的海报配置全都管理在服务端中,服务端只需要提供一个 API 给客户端。客户端通过这个 API 请求不同的分享图,服务端接收到请求后,先检查服务端是否缓存分享图。若没有缓存图片,则使用海报配置去 imgrender 动态生成海报,然后将生成的图片链接返回给客户端,供用户下载保存。 使用 imgrender 动态渲染海报,在满足需求的同时,可以大幅度降低开发成本,提高多端兼容性。无论是开发小程序海报,还是原生应用中的海报,一套代码即可搞定。 [图片] 原文链接:https://mp.weixin.qq.com/s/6ss1D4wneNDuhUfplualQQ 关键词:海报图、海报分享图、海报生成、图片生成、小程序海报生成
2023-11-11 - 如何设计“企业微信+社群+小程序+APP”的裂变矩阵?
今年,大家会发现一个趋势,无论是朋友圈的裂变活动,商城下单后的引流动作,还是线下门店加好友送礼品,用户最终都在被引导到企业微信中。 根据企业微信截至到去年年底的数据显示,有80%的500强企业开通了企业微信,越来越多的企业都在用企业微信来搭建自己的私域流量池。 那么,为什么要用企业微信来搭建私域流量池呢?娜娜认为,企业微信主要有以下5大优势: 1、打开率高 企业微信消息推送时,在用户端的展示与普通好友发来的消息呈现是一样的,是在聊天对话框内展示,用户不会忽略,相对于其他推送形式,打开率更高。 2、合规 2019年,企业微信从OA赛道正式切入CRM赛道,同年微信官方大力打压黑灰产业,封杀营销号,企业微信成了官方推荐的私域营销工具。 企业微信能够与微信互联互通,带有「@品牌名称」的小尾巴标签,且不易被封号。相较于个人微信,更加稳定、安全。 3、高效 企业微信支持群发、活码、打标签、自动发送欢迎语等多种营销功能,管理用户更高效。 4、客户资产化 以前,顾客是员工的个人人脉,员工走了就把顾客也带走了,有了企业微信,客户资源都能保留在企业上。员工离职,可以通过离职继承,将客户转接到新员工的通讯录。 5、容量大 相较于个人号的5000人好友量,企业微信的2w好友量是真的香,而且单日主动添加好友量也是个人号上限的好几倍,对于大品牌大流量来说,可以极大节省很多号的人力和资金成本。 那么,更重要的一个问题来了,企业如何高效的、搭建引流用户到企业微信呢? 除了两个基础的裂变玩法,企业微信+公众号双重裂变,企业微信客服号裂变,还有哪些新的玩法呢? 一、企业微信+社群裂变 活动案例:元气森林 裂变玩法:企业微信客服号裂变 [图片] 用户通过添加企业微信,获取自己的专属海报+活动规则文案,邀请好友扫码添加企业微信号完成助力,达到指定邀请任务目标后,用户领取活动奖励。 好友只需添加企业微信即可助力成功,若对活动奖品感兴趣则会继续分享转发海报,从而形成裂变循环。 活动规则:邀请40位好友添加企业微信客服号,得一箱气泡水 [图片] 社群转化用户: 完成任务后,并没有让用户直接填写收货地址,而是先引导用户入群,将用户沉淀在企业微信群中,在社群内推送折扣活动,或新人福利。 活动结束后,根据标签给用户发送不同的营销内容,将用户引流至社群。进入社群后,首先会发出一个新人5折券,引导用户进入元气森林小程序商城,购买指定产品。 除了5折券外,还有代金券、折扣券,根据节日、产品上新、产品套餐等契机轮番上阵,再加上优惠时效的限制,刺激用户快速下单,转化购买。 案例总结:企业微信+社群裂变,可帮助企业快速完成拉新+转化动作。 裂变玩法升级:将入群链接插入推送内容中,提高人群率、转化率! 企业微信+社群·裂变路径: [图片] 1、扫码添加企业微信,参与活动 2、企业微信自动推送活动文案+入群链接+活动海报 3、点击进入社群,企业微信自动发送入群欢迎语 4、转发活动海报邀请好友助力 5、完成任务领取奖品 二、企业微信+线下核销 活动案例:口袋固安 裂变玩法:企业微信客服号裂变(同元气森林) [图片] 活动规则:邀请5位好友添加企业微信客服号,领蛋糕一盒 [图片] 线下核销: 邀请5位好友助力,完成任务,填写收货地址,获取核销码,到距离自己最近的线下门店核销。 用户到店后,通过门店限时折扣活动,刺激用户下单消费,完成二次转化。 案例总结:企业微信+线下核销,通过企业微信裂变获取线上流量,用线下核销引导用户到店,完成付费转化。 裂变玩法升级:用户完成任务填写地址后,自动弹出核销码,将核销植入裂变路径的最后环节,为线下门店精准引流,提高门店流量! 企业微信+线下核销·裂变路径 [图片] 1、扫码参与活动,邀请好友助力,完成任务填写个人信息 2、收到核销码,去线下门店核销 3、到线下门店核销成功后,领取奖品 4、已核销的用户,去其他分店不可再次核销 三、企业微信+小程序裂变 活动案例:唯品会 裂变玩法:企业微信+公众号联动裂变 [图片]用户扫码参与活动,首先关注公众号,公众号推送企业微信二维码,添加企业微信号获取专属海报+活动规则文案。 邀请好友扫码助力,完成助力需要关注公众号+添加企业微信号,达到指定邀请任务目标后,用户领取活动奖励。 一场活动同时给企业微信、公众号做了涨粉! 活动规则:邀请8位好友关注公众号+添加企业微信客服号,领电动牙刷一个;邀请20位好友助力,领美容导入仪一台 为小程序引流: [图片] 在活动结束后、节假日、大促日等,通过企业微信群发,给用户推送唯品会小程序商城,引导用户进入小程序,为小程序定向引流。 还可通过小程序授权,获取用户的手机号码等信息。 案例总结:企业微信+小程序裂变,不仅可以为小程序做定向引流,还可通过小程序授权获取用户信息。 裂变玩法升级:将小程序的卡片链接,插入到推送内容中,提高小程序的点击率,为小程序做精准引流。 企业微信+小程序·裂变路径 [图片] 1、扫码添加企业微信,参与活动 2、企业微信自动推送活动文案+小程序链接+活动海报 3、点击进入企业小程序 4、转发活动海报邀请好友助力 5、完成任务领取奖品 四、企业微信+企业自有平台裂变 活动案例:斑马英语 裂变玩法:企业微信客服号裂变(同元气森林) [图片] 活动规则:邀请25位好友添加企业微信客服号,得活动奖品 [图片] 自有平台引流: 活动过程中,或活动结束后,企业微信推送低价课程链接给用户,以专属福利为诱饵,引导用户购买低价课,通过低价课的训练营,最终转化用户购买正价课。 案例总结:企业微信+企业自有平台裂变(如app、商城、官网、课程、报名链接等),帮助企业精准获客。 裂变玩法升级:将需要引流的平台链接,插入推送内容中,为企业精准引流! 企业微信+自有平台·裂变路径: [图片] 1、扫码添加企业微信,参与活动 2、企业微信自动推送活动文案+自有平台链接+活动海报 3、点击进入企业官网/商城/课程/报名入口等 4、转发活动海报邀请好友助力 5、完成任务领取奖品
2021-04-28 - python入门012~使用python3爬取网络图片并保存到本地
上一节我们学习了python3借助requests类库爬取网页数据,这一节我们继续深入的讲解python爬虫的实现。今天要将的是使用python3爬取网络图片,并保存到本地。 本节知识点 1,python3爬取网站源码 2,正则匹配获取图片链接 3,使用python3将不怕保存到本地 一,首先我们来看下要爬取的网址 下图箭头所指的就是我们要爬取的图片。 [图片] 二,爬取网址源码到本地 [图片] 通过上图我们可以看到,我们成功的爬取到了网站源码,而这个网站的 <img 图片显示标签里用了 data-src 懒加载来显示图片,所以我们接下来要做的就是使用正则表达式来匹配出网站源码里的图片链接。 三,正则表达式匹配图片链接 [图片] 通过上图可以看出,我们成功的匹配到了网站源码里的图片链接,接下来,我们就要把这个图片保存在本地了。 四,保存图片到本地 [图片] 如上图,我们做保存图片的时候,需要先在我们代码的外层目录创建一个 imgs文件夹,用于存放图片。然后编写核心代码。 五,完整代码如下。 [图片] 六,运行代码,看下效果 [图片] 可以看下我们爬取到的图片 [图片] 最后把完整代码贴给大家 [代码]# python3爬取网络图片 # 编程小石头微信:2501902696 import requests import re # 第一个爬取网址 url = 'http://www.nipic.com/photo/jingguan/ziran/index.html' # 获得网页源码 data = requests.get(url).text # print("网站源码", data) # 图片正则表达式 regex = r'data-src="(.*?.jpg)"' # re是一个列表 pa = re.compile(regex) # 创建一个pa模板,使其符合匹配的网址 ma = re.findall(pa, data) # findall 方法找到data中所有的符合pa的对象,添加到re中并返回 # print(ma) # 将ma中图片网址依次提取出来 i = 0 for image in ma: i += 1 image = requests.get(image).content print(str(i) + '.jpg 正在保存。。。') with open('../imgs/' + str(i) + '.jpg', 'wb') as f: # 注意打开的是就jpg文件 f.write(image) print('保存完毕') [代码] 往期文章 python入门001~python开发工具 pycharm的安装与破解(mac和window都有讲) https://www.jianshu.com/p/dc8299467718 python入门002~创建属于自己的第一个python项目 https://www.jianshu.com/p/eda772bde32a python入门003~python3的安装~以python3最新版为例(Mac window都有讲) https://www.jianshu.com/p/4bb23e40a7ac python入门004~创建属于自己的第一个python3项目~python3基础知识的讲解 https://www.jianshu.com/p/0fadc0369abd python入门005~基本数据类型和变量的学习 https://www.jianshu.com/p/44c2a7b34cbf python入门010~python3操作数据库 借助pycharm快速连接并操作mysql数据库 https://www.jianshu.com/p/a23f414cc2f2 python入门011~python3借助requests类库3行代码爬取网页数据 https://www.jianshu.com/p/cf22a679e96f python入门012~使用python3爬取网络图片并保存到本地 https://www.jianshu.com/p/651effd4f3b8 python入门013~爬虫篇,网页爬虫,图片爬虫,文章爬虫,Python爬虫爬取新闻网站新闻 https://www.jianshu.com/p/7e59f52ea0b6 python入门014~把爬取到的数据存到数据库,带数据库去重功能 https://www.jianshu.com/p/5ba719a7d8cb python入门015—python爬取前程无忧51job的职位信息并存入mysql数据库(带数据去重) https://www.jianshu.com/p/fe434693781f 视频讲解 https://edu.csdn.net/course/detail/25009
2019-07-15 - 小程序搜索优化指南(SEO)
2019年上半年微信发布了基于小程序页面的搜索,为了让我们更好地发现及理解小程序的页面,结合过去一段时间来我们遇到的各种情况,我们强烈建议各位开发者花一些宝贵的时间认真阅读本文:) 爬虫访问小程序内页面时,会携带特定的 user-agent "mpcrawler" 及场景值:1129 1. 小程序里跳转的页面 (url) 可被直接打开。 小程序页面内的跳转url是我们爬虫发现页面的重要来源,且搜索引擎召回的结果页面 (url) 是必须能直接打开,不依赖上下文状态的。特别的:建议页面所需的参数都包含在url 2. 页面跳转优先采用navigator组件。 小程序提供了两种页面路由方式: a.navigator 组件 b. 路由 API,包括 navigateTo / redirectTo / switchTab / navigateBack / reLaunch 建议使用 navigator 组件,若不得不使用API,可在爬虫访问时屏蔽针对点击设置的时间锁或变量锁。 3.清晰简洁的页面参数。 结构清晰、简洁、参数有含义的 querystring 对抓取以及后续的分析都有很大帮助,但是将 JSON 数据作为参数的方式是比较糟糕的实现。 4. 必要的时候才请求用户进行授权、登录、绑定手机号等。 建议在必须的时候才要求用户授权(比如阅读文章可以匿名,而发表评论需要留名)。 5. 我们不收录 web-view 中的任何内容。 我们暂时做不到这一点,长期来看,我们可能也做不到。 6. 利用 sitemap 配置引导爬虫抓取,同时屏蔽无搜索价值的路径。 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html 7. 设置一个清晰的标题和页面缩略图。 页面标题和缩略图对于我们理解页面和提高曝光转化有重要的作用。 通过wx.setNavigationBarTitle或 自定义转发内容onShareAppMessage对页面的标题和缩略图设置,另外也为 video、audio 组件补齐 poster /poster-for-crawler属性。 8. 使用页面路径推送能力 可极大丰富微信可以收录的内容,进而提高小程序内容的曝光机会。请参考: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/search/search.submitPages.html
2020-01-14 - 如何查看并复制小程序的页面路径?
1.需求: 在发布微信公众号文章时,我们可以添加微信小程序的入口,默认是跳转到小程序的首页,但是首页远远不是我们想要的,我们想要复制任何一个页面的页面路径,让读者可以跳转任意页面,所以我们就需要复制小程序的页面路径,微信官方也提供了复制页面路径的方法,接下来请按照以下步骤操作,即可完成。 2.步骤: 公众号添加微信小程序 登陆微信公众号平台,打开编写图文消息的页面,点击小程序,弹出选择小程序弹窗,这里需要我们根据小程序名称/AppID/帐号原始ID去搜索。 [图片] 如何获取小程序AppId 找到你想要跳转的小程序,点击右上角三个点,然后弹出小程序信息的弹窗,进入小程序的页面,点击更多资料,即可以看到小程序的AppID和原始ID。然后复制AppID或原始ID到步骤一去搜索。下面我用微信开放社区小程序举个例子,告诉大家如何找到微信小程序的AppID。 [图片] 打开小程序的复制路径的权限,在第一步中我们输入小程序AppID,搜索出微信开放社区小程序,然后点击下一步,进入填写详细信息弹窗中,默认小程序路径为小程序首页路径,这里我们选择【获取更多页面路径】,然后右侧弹出窗口,将你的微信号输入进去即可开启复制页面路径的入口。[图片] 去小程序中获取制定页面路径:打开微信开放社区小程序,进入到你想要复制页面路径的页面,点击右上角三个点,弹出弹窗,在第二排又一个按钮【复制页面路径】,点击即可复制。[图片] 以此类推,我们可以复制任何其他小程序的链接,但是如果复制出的链接中包含一些参数,导致跳转出了问题,这就要弄清楚参数的含义了。注意⚠️:复制出来的页面路径在小程序里使用的时候记得删除 .html 才能正常访问
2021-02-27 - UNI-APP使用云开发跨全端开发实战讲解
UNI-APP 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。 本文为大家讲解如何采用云开发官方JS-SDK,接入云开发后端服务并支持UNI-APP全部端(不止于微信小程序) JS-SDK和UNI-APP适配器 1.JS-SDK和适配器 云开发官方提供的@cloudbase/js-sdk,主要用来做常规WEB、H5等应用(浏览器运行)的云开发资源调用,也是目前最为完善的客户端SDK。 目前市面上大部分的轻应用、小程序包括移动应用APP都是采用JS来作为开发语言的,所以我们可以对TA进行轻微改造,就可以轻松使用在各种平台中。 但是单独改造SDK包会有些许风险,比如在原SDK包升级时需要重新构造,就造成了无穷无尽的麻烦,改造成本相当大。 官方的产品小哥哥深知这种不适和痛苦,所以在@cloudbase/js-sdk 中提供一套完整的适配扩展方案,遵循此方案规范可开发对应平台的适配器,然后搭配 @cloudbase/js-sdk 和适配器实现平台的兼容性。 不了解的小伙伴肯定会有些茫然,我来用浅显的语言解释一下,就是@cloudbase/js-sdk 将底层的网络请求以及相关基础需求以接口的形式暴露出来,我们按照平台的特殊API来补充这些接口,sdk就可以根据这些补充的接口,无障碍的运行在平台中了。 如果我们想在UNI-APP中使用@cloudbase/js-sdk ,底层网络请求你需要来补充,因为sdk原本是适应浏览器的,TA不知道UNI-APP怎么对外发请求,所以你需要将uni.request 方法补充到TA暴露的接口中。补充完毕后,@cloudbase/js-sdk 就可以在UNI-APP中活泼的运行了。 我们将所有的uni方法全部补充到JS-SDK暴漏的接口中去,就形成了一个完整的适配器,我们将其成为uni-app适配器。 2.UNI-APP适配器 UNI-APP的整体接口都是公开透明的,我们在开发UNI-APP时也都遵照同一套接口标准。所以小编已经将uni-app适配器制作完毕,大家只需要在使用时接入适配器就可以了。 我们在项目目录main.js中引入云开发JS-SDK,然后接入我们的UNI-APP适配器即可。 [代码]import cloudbase from '@cloudbase/js-sdk' import adapter from 'uni-app/adapter.js' cloudbase.useAdapters(adapter); cloudbase.init({ env: '',//云开发环境ID appSign: '',//凭证描述 appSecret: { appAccessKeyId: 1,//凭证版本 appAccessKey: ''//凭证 } }) [代码] 移动应用登录凭证 云开发SDK在使用过程中,向云开发服务系统发送的请求都会需要验证请求来源的合法性。 我们常规 Web 通过验证安全域名,而由于 UNI-APP 并没有域名的概念,所以需要借助安全应用凭证区分请求来源是否合法。 登录云开发 CloudBase 控制台,在安全配置页面中的移动应用安全来源一栏: [图片] 点击“添加应用”按钮,输入应用标识:uni-app(也可以输入其他有标志性的名称),需要注意应用标识必须是能够标记应用唯一性的信息,比如微信小程序的 appId 、移动应用的包名等。 [图片] 添加成功后会创建一个安全应用的信息,如下图所示: [图片] 我们需要保存一下上图中的版本(示例为1)、应用标识(示例为uni-app)、以及点击获取到的凭证(示例为demosecret) 在项目目录中,我们将main.js中的init部分补全 [代码]import cloudbase from '@cloudbase/js-sdk' import adapter from 'uni-app/adapter.js' cloudbase.useAdapters(adapter); cloudbase.init({ env: 'envid',//云开发环境ID,保证与你操作登录凭证一致 appSign: 'uni-app',//凭证描述 appSecret: { appAccessKeyId: 1,//凭证版本 appAccessKey: 'demosecret'//凭证 } }) [代码] 如此,你就可以正常的进行云开发的登录使用了。 需要注意以下4点: 你需要设置uni-app的各端安全域名为:request:tcb-api.tencentcloudapi.com、uploadFile:cos.ap-shanghai.myqcloud.com、download:按不同地域配置 使用此种方法接入云开发是全端支持,并不会享有微信小程序生态的一些便利,微信小程序开发还是需要依赖正常请求调用过程(将云开发作为服务器来对待),但你可以判断wx来使用wx.cloud来兼容。 使用云开发的匿名登录时,受各端实际情况影响,可能不能作为常久唯一登录id,需要根据自身业务建立统一账户体系,具体可使用自定义登录来进行。 UNI-APP支持WEB网页端上线时,需要将网页域名配置到云开发安全域名中(防止WEB下载文件导致跨域) 示例代码详解 示例项目中已经基本构建了uni-app使用云开发的各种流程代码。 在页面中进行匿名登录: [代码]// index.vue import cloudbase from '@cloudbase/js-sdk' export default { data() { return { title: '登录中' } }, onLoad() { cloudbase.auth().anonymousAuthProvider().signIn().then(res => { this.title = '匿名登录成功' }).catch(err => { console.error(err) }) } } [代码] 调用云函数并收到返回结果: [代码]import cloudbase from '@cloudbase/js-sdk' export default { methods: { call: function() { cloudbase.callFunction({ name: "test", data: { a: 1 } }).then((res) => { console.log(res) }); } } } [代码] 操作数据库: [代码]import cloudbase from '@cloudbase/js-sdk' export default { methods: { database: function() { cloudbase.database().collection('test').get().then(res => { console.log(res) }) } } } [代码] 实时数据库监听: [代码]import cloudbase from '@cloudbase/js-sdk' export default { methods: { socket: function() { let ref = cloudbase.database().collection('test').where({}).watch({ onChange: (snapshot) => { console.log("收到snapshot", snapshot); }, onError: (error) => { console.log("收到error", error); } }); } } } [代码] 上传文件(框架限制,WEB端无法操作): [代码]import cloudbase from '@cloudbase/js-sdk' export default { methods: { upload: function() { uni.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album'], success: function(res) { console.log(res.tempFilePaths[0]) cloudbase.uploadFile({ cloudPath: "test-admin.png", filePath: res.tempFilePaths[0], onUploadProgress: function(progressEvent) { console.log(progressEvent); var percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); } }).then((result) => { console.log(result) }); } }); } } } [代码] 下载文件(需要注意地域域名,配置安全域名): [代码]import cloudbase from '@cloudbase/js-sdk' export default { methods: { download: function() { cloudbase.downloadFile({ fileID: "cloud://demo-env-1293829/test-admin.png" }).then((res) => { console.log(res) }); } } } [代码] 部署步骤 将项目下载后使用HBuilderX打开。 按照获取移动安全凭证的指引,填写至mian.js相应处。 打开目录命令行,npm i执行安装依赖。 打开云开发控制台,开启匿名登录。 新建一个默认的云函数,名称为test(逻辑内容直接返回event即可) 新建一个数据库,名称为test(随便添加几个记录,设置权限为所有人可读) 调整项目pages/index/index.vue中,21行代码,在登录成功后调用相应函数。 以下是WEB端运行时展示: [图片] 关于 uni-app适配器在util/adapter中,只进行了简单的测试,保证可用性,后续请关注官网获取最新适配器依赖 此方法有别与uniCloud,是直接使用uni请求底层,依赖官方JS-SDK进行云开发服务的交互处理,在使用时注意区别。 项目地址:https://github.com/AceZCY/UNI-for-CloudBase 产品介绍 云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2020-12-09 - 全云开发V3支付电商收付通,第一天,统一下单,居然秒过了。
接了个甲方的项目,要求用全云开发实现微信支付V3电商收付通,谈妥后,于是开干。 先看文档,再找攻略,第一步开始写段统一下单的测试代码。非常神奇的事情发生了,居然是秒过的,直接拿到prepaid_id!秒过,秒过,秒过,一个红错误都没有,难以置信; V3收付通文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/guide.shtml 遥想当年做V2小程序支付,仅一个统一下单的接口,就调得让人欲生欲死,前前后后一个星期不止。 我自己也有点懵圈,这是什么情况,V3新的接口里,签名?Authorization?Headers?这些都是闹着玩的吗?能不能严肃点? 想了一会儿静静,继续,看能不能在小程序里拉起wx.requestPayment,结果V3文档里一句小程序支付的都没有,难道不支持?立马在论坛大佬群里@了一下娇华同学,得到肯定回答,于是,wx.requestPayment拉起,美滋滋地支付了0.01元,统一下单,完美。 云函数相关代码: //JSAPI下单+JS调起支付 async function partner_transactions_jsapi(event) { let wxContext = cloud.getWXContext() let jsonStr = JSON.stringify({ sp_appid: wxContext.APPID, sp_mchid: mchid, notify_url: config.notify_url, payer: { sp_openid: wxContext.OPENID }, ...event.unified_order,//小程序端传入的统一下单参数 }) let method = 'POST' let url = '/v3/pay/partner/transactions/jsapi' let headers = getHeaders(method, url, jsonStr) let res = await rp({ method, uri: config.host + url, headers, body: jsonStr }) let prepay_id = JSON.parse(res).prepay_id return getPayment(prepay_id) //返回拉起小程序 wx.requestPayment 所需参数 } 小程序端测试代码: //JSAPI下单+JS调起支付 partner_transactions_jsapi: async function () { console.log('partner_transactions_jsapi') let res = await wx.showModal({content: '统一下单?'}) if (res.confirm) { } else return wx.showToast({ icon: 'loading' }) let unified_order = { //测试订单 sub_mchid: SUB_MCHID, description: '电商收付通测试', out_trade_no: "otn" + Math.random().toString(36).substr(2, 15) + parseInt(Date.now() / 1000), amount: { total: 10, currency: 'CNY' }, } res = await wx.cloud.callFunction({ name: 'wxPayV3', data: { action: 'partner_transactions_jsapi', unified_order } }) res = await wx.requestPayment({ ...res.result }).catch(err => console.log(err)) console.log(res) wx.showToast({}) }, 今天先弄这么多,更多踩坑实录继续。 更多相关内容: [图片]
2020-10-20 - 全云开发V3支付电商收付通,第二天,上传图片,此处慎用官方文档
不得不喷一下V3支付,上传图片环节,官方的文档太坑人了,没有它还好,照着它做,足足卡了我 6个多小时,全耗在body的拼接上了。 此处建议大家慎用官方的拼接方式,因为完全象撞大运一样,还不一定能拼接成功。我也是在放弃折腾后,不断攻略才找到了简单方法,如下所示: 官方文档: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/chapter3_1.shtml 最后还是按照自己喜欢的方式实现了,用的是request-promise的formData,后台基于nodejs的可以简单参考,轻轻松松通过了。 代码如下: //上传图片,获得media_id async function merchant_media_upload(event) { let res = await cloud.downloadFile({ fileID: event.fileID }) let filebuffer = res.fileContent let filename = event.filename let sha256 = crypto.createHash('sha256').update(filebuffer).digest('hex') let method = 'POST' let url = '/v3/merchant/media/upload' let jsonStr = JSON.stringify({ filename, sha256 }) let headers = getHeaders(method, url, jsonStr) headers['Content-Type'] = `multipart/form-data;boundary=boundary` let formData = { 'meta': jsonStr, 'file': { value: filebuffer, options: { filename, contentType: 'image/jpg' } } } return JSON.parse(await rp({ method, uri: config.host + url, headers, formData })) } 更多相关内容: [图片]
2020-10-20 - 小程序发送红包相关问题调研
https://blog.csdn.net/weixin_42253589/article/details/81127899 https://www.jianshu.com/p/43668ae7a302 https://www.cnblogs.com/xinweiyun/p/9361212.html 社交红包小程序开通条件: 1、商户号,商户号入驻满90天,连续交易30天,中间不能中断;(开通微信支付) 2、需选择社交红包类目,所需资质为:《增值电信业务经营许可证》,即ICP证; 3、商户号开通成功后并绑定小程序主体。 小程序红包开通条件: 1、开通商户号; 2、商户号开通成功后绑定小程序主体。 领取限制: 目前小程序红包仅支持用户微信扫码打开小程序,进行红包领取。 小程序红包有哪些内容是不能做的? 1、小程序内暂不支持红包广场业务场景。即小程序的红包发放与领取仅限在群和好友中进行,禁止在无好友或群关系的场景传播,如基于LBS的红包广场。 2、红包类活动暂不支持朋友圈传播。 3、暂不支持“回赏”类红包玩法。即A发一个红包给任何用户,任何用户需回包一个红包给A,才能拆开A的红包。 4、暂不支持单笔交易支付金额突破商户号额度(当前商户号额度为204元)。 3、服务类目中没有社交红包类目,如何开通?(社区) https://developers.weixin.qq.com/community/develop/doc/0006aaa26fca405e51aae14b95b800?highLine=%25E7%25BA%25A2%25E5%258C%2585 4、发红包接口(微信支付) https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_4&index=3 5、小程序如何发红包(社区) https://developers.weixin.qq.com/community/develop/doc/0006e057e80c800eb92ad503556400?highLine=%25E7%25BA%25A2%25E5%258C%2585 6、小程序红包(微信支付) https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=18_4&index=1 7、微信红包封面开放平台 https://cover.weixin.qq.com/cgi-bin/mmcover-bin/readtemplate?t=page%2Fdoc%2Fguide%2Fintroduce.html 8、小程序红包领取和企业付到零钱(社区) https://developers.weixin.qq.com/community/develop/article/doc/000268a1c608d8eee7aa667635c813 9、小程序红包配置及开发小结(社区) https://developers.weixin.qq.com/community/develop/article/doc/000a28c8c90e50bbf5a9272f956c13 10、小程序营销利器,wx.sendBizRedPacket小程序红包完整详细开发代码(社区) https://developers.weixin.qq.com/community/develop/article/doc/000c6009a5cbd805d8790d4ab56013?page=1#comment-list
2021-01-06 - 云开发短信跳小程序(自定义开发版)教程
写在前面如果你想要自主开发,但没有云开发相关经验,可以采用演示视频来学习本教程: [视频] 一、能力介绍境内非个人主体的认证的小程序,开通静态网站后,可以免鉴权下发支持跳转到相应小程序的短信。短信中会包含支持在微信内或微信外打开的静态网站链接,用户打开页面后可一键跳转至你的小程序。 这个链接的网页在外部浏览器是通过 URL Scheme 的方式来拉起微信打开主体小程序的。 总之,短信跳转能力的实现分为两个步骤,「配置拉起网页」和「发送短信」。本教程将介绍如何执行操作完成短信跳转小程序的能力。 如果你想要无需写代码就能完成短信跳转小程序的能力,可以参照无代码版教程进行逐步实现。 二、操作指引1、网页创建首先我们需要构建一个基础的网页应用,在任何代码编辑器创建一个 html 文件,在教程这里命名为 index.html 在这个 html 文件中输入如下代码,并根据注释提示更换自己的信息: window.onload = function(){ window.web2weapp.init({ appId: 'wx999999', //替换为自己小程序的AppID gh_ID: 'gh_999999',//替换为自己小程序的原始ID env_ID: 'tcb-env',//替换小程序底下云开发环境ID function: { name:'openMini',//提供UrlScheme服务的云函数名称 data:{} //向这个云函数中传入的自定义参数 }, path: 'pages/index/index.html' //打开小程序时的路径 }) } 以上引入的 web2weapp.js 文件是教程封装的有关拉起微信小程序的极简应用,我们直接引用即可轻松使用。 如果你想进一步学习和修改其中的一些WEB展示信息,可以前往 github 获取源码并做修改。 有关于网页拉起小程序的更多信息可以访问官方文档 如果你只想体验短信跳转功能,在执行完上述文件创建操作后,继续以下步骤。 2、创建服务云函数在上面创建网页的过程中,需要填写一个UrlScheme服务云函数。这个云函数主要用来调用微信服务端能力,获取对应的Scheme信息返回给调用前端。 我们在示例中填写的是 openMini 这个命名的云函数。 我们前往微信开发者工具,定位对应的云开发环境,创建一个云函数,名称叫做 openMini 。 在云函数目录中 index.js 文件替换输入以下代码: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { return cloud.openapi.urlscheme.generate({ jumpWxa: { path: '', // 打开小程序时访问路径,为空则会进入主页 query: '',// 可以使用 event 传入的数据制作特定参数,无需求则为空 }, isExpire: true, //是否到期失效,如果为true需要填写到期时间,默认false expire_time: Math.round(new Date().getTime()/1000) + 3600 //我们设置为当前时间3600秒后,也就是1小时后失效 //无需求可以去掉这两个参数(isExpire,expire_time) }) } 保存代码后,在 index.js 右键,选择增量更新文件即可更新成功。 接下来,我们需要开启云函数的未登录访问权限。进入小程序云开发控制台,转到设置-权限设置,找到下方未登录,选择上几步我们统一操作的那个云开发环境(注意:第一步配置的云开发环境和云函数所在的环境,还有此步操作的环境要一致),勾选打开未登录 [图片] 接下来,前往云函数控制台,点击云函数权限,安全规则最后的修改,在弹出框中按如下配置: [图片] 3、本地测试我们在本地浏览器打开第一步创建的 index.html ;唤出控制台,如果效果如下图则证明成功! 需要注意,此处本地打开需要时HTTP协议,建议使用live server等扩展打开。不要直接在资源管理器打开到浏览器,会有跨域的问题! [图片] 4、上传本地创建好的 index.html 至静态网站托管将本地创建好的 index.html 上传至静态网站托管,在这里静态托管需要是小程序本身的云开发环境里的静态托管。 如果你上传至其他静态托管或者是服务器,你仍然可以使用外部浏览器拉起小程序的能力,但会丧失在微信浏览器用开放标签拉起小程序的功能,也不会享受到云开发短信发送跳转链接的能力。 如果你的目标小程序底下有多个云开发环境,则不需要保证云函数和静态托管在一个环境中,无所谓。 比如你有A、B两个环境,A部署了上述的云函数,但是把 index.html 部署到B的环境静态托管中了,这个是没问题的,符合各项能力要求。只需要保证第一步 index.html 网页中的云开发环境配置是云函数所在环境即可。 部署成功后,你便可以访问静态托管的所在地址了,可以通过手机外部浏览器以及微信内部浏览器测试打开小程序的能力了。 5、短信发送云函数的配置在上面创建 openMini 云函数的环境中再来一个云函数,名字叫 sendsms 。 在此云函数 index.js 中配置如下代码: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { try { const config = { env: event.env, content: event.content ? event.content : '发布了短信跳转小程序的新能力', path: event.path, phoneNumberList: event.number } const result = await cloud.openapi.cloudbase.sendSms(config) return result } catch (err) { return err } } 保存代码后,在 index.js 右键,选择增量更新文件即可更新成功。 6、测试短信发送能力在小程序代码中,在 app.js 初始化云开发后,调用云函数,示例代码如下: App({ onLaunch: function () { wx.cloud.init({ env:"tcb-env", //短信云函数所在环境ID traceUser: true }) wx.cloud.callFunction({ name:'sendsms', data:{ "env": "tcb-env",//网页上传的静态托管的环境ID "path":"/index.html",//上传的网页相对根目录的地址,如果是根目录则为/index.html "number":[ "+8616599997777" //你要发送短信的目标手机,前面需要添加「+86」 ] },success(res){ console.log(res) } }) } }) 重新编译运行后,在控制台中看到如下输出,即为测试成功: [图片] 你会在发送的目标手机中收到短信,因为短信中包含「退订回复T」字段,可能会触发手机的自动拦截机制,需要手动在拦截短信中查看。 需要注意:你可以把短信云函数和URLScheme云函数分别放置在不同云开发环境中,但必须保证所放置的云开发环境属于你操作的小程序 另外,出于防止滥用考虑,短信发送的云调用能力需要真实小程序用户访问才可以生效,你不能使用云端测试、云开发JS-SDK以及其他非wx.cloud调用方式(微信侧WEB-SDK除外),会提示如下错误: [图片] 如果你想在其他处使用此能力,可以使用服务端API来做正常HTTP调用,具体访问官方文档 7、查看短信监控图表进入 云开发控制台 > 运营分析 > 监控图表 > 短信监控,即可查看短信监控曲线图、短信发送记录。 [图片] 三、总结短信跳转小程序核心是静态网站中配置的可跳转网页,外部浏览器通过URL Scheme 来实现的,这个方式不适用于微信浏览器,需要使用开放标签才可以URL Scheme的生成是云调用能力,需要是目标小程序的云开发环境的云函数中使用才可以。并且生成的URL Scheme只能是自己小程序的打开链接,不能是任意小程序(和开放标签的任意不一致)短信发送能力的体验是每个有免费配额的环境首月100条,如有超过额度的需求可前往开发者工具-云开发控制台-对应按量付费环境-资源包-短信资源包,进行购买。如当前资源包无法满足需求也可通过云开发 工单 提交申请[图片]短信发送也是云调用能力,需要真实小程序用户调用才可以正常触发,其他方式均报错返回参数错误,出于防止滥用考虑云函数和网页的放置可以不在同一个环境中,只需要保证所属小程序一致即可。(需要保证对应环境ID都能接通)如果你不需要短信能力,可以忽略最后两个步骤CMS配置渠道投放、数据统计可参考官方文档
2021-04-07 - 云开发实战:实现短信跳小程序
先看效果 [视频] 小程序支持短信跳转小程序了,可以说是打开了一个巨大的流量入口。 效果过程分析 从短信到网页从网页到小程序那么就涉及到两个点 发送短信网页跳转实现步骤分析 先要有个网页,可以跳转到小程序然后发送短信,短信内容包含地址具体实现步骤 1. 先要有个网页,可以跳转到小程序 首先开通静态网页托管 [图片] 创建一个云开发的项目,点击左上方「云开发」按钮 [图片] 点击静态网页进行开通。 然后点击「下载资源包」,解压缩我们会看到 [图片] 第一个是云函数,第二个是跳转的网页。首先我们编辑下跳转的网页 [图片] 打开文件编辑以下 6 处即可(通过“replace”搜索可以快速定位修改的地方): [图片][图片]添加好对应参数后,上传部署到你的静态托管文件目录中 [图片] 这个时候网页这块就已经搞定了,接下来部署下云函数。 刚才的 cloudfunctions 文件夹下面有个 public 文件夹里面的 index.js 复制内容到自己新建的云函数的 index.js 中,然后替换自己小程序的path(友情提示:覆盖完成后别忘记上传部署云函数)[图片]这个云函数的作用,主要是静态网页会调用它生成跳转的URL Scheme。以下为网页调用这个函数的代码区域[图片] 到这里网页显示与网页跳转就只差最后一步了,设置云函数权限。 [图片] [图片] 自定义安全规则配置: { "*": { "invoke": "auth != null" }, "public": { "invoke": true } } 2. 然后发送短信,短信内容包含地址 创建一个sendSms到云函数,复制以下代码: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event, context) => { try { const result = await cloud.openapi.cloudbase.sendSms({ env: 'online-12345678910', // 替换环境ID content: '云开发支持短信跳转小程序了',// 替换短信文案 path: '/index.html',// 替换网页路径 phoneNumberList: [ "+8612345678910" ] }) return result } catch (err) { return err } } 替换以上 3 处内容即可。 环境ID,可以在设置中找到短信内容,这个自己自定义网页路径,在静态网页托管中点击上传到网页即可查看复制[图片] 修改完成后,部署即可。 大功告成 [视频] 小程序就可以调用这个云函数发送短信,短信就会自带网页地址,点击即可跳转到小程序了。
2021-01-08 - 小程序实现的列表上下拖拽排序
先来看看效果 快速拖拽排序测试演示视频地址:https://v.qq.com/x/page/r3207k4fxe1.html 完整拖拽排序效果演示视频地址:https://v.qq.com/x/page/y3207g6agur.html [图片] 采用技术:uni-app 接下来分析分析实现该效果所需要用到的标签 元素是通过拖拽进行排序的,此处采用的是官方出的 <movable-area> <movable-view> 两位标签大佬解决移动的问题 (主要是相信官方支持的动画会比自己搞更加丝滑一些)。支持拖拽到上下边界,检查可视区域的位置并自动进行滚动, 此处就需要我们的 <scroll-view> 标签大佬坐镇了。标签的选择搞定了,再来了解了解这些标签要用到的重点属性 movable-view 想要移动就必须作为 movable-area 的直接子元素,且 movable-area 必须设置 width,height 属性 (还有些提示可以查看文档)。movable-view 的 x, y 属性决定了 movable-view 再 movable-area 所处的位置 (是不是猜出了要搞些什么东东了)scroll-view 滚动到指定位置可以通过控制 scroll-top 的属性值来进行控制滚动 接下来就是怎么个实现思路,先来捋捋实现的步骤 列表该如何渲染如何控制拖拽元素的跟随如何使拖拽中的元素与相交互的元素进行位置调换如何判断拖拽元素至上下边界滚动屏幕如何使页面的滚动与拖拽时的滚动互不影响 描述完宏观的蓝图,接下来就是代码小细节,客官请随我来 一、解决列表渲染问题 /** * 上面说到 movable-view 可以通过 x,y 决定它的位置, 且 movable-area 需要设置 widht,height 属性 * 配置完这些属性 movable-view 就可以再 movable-area 愉快的拖拽玩耍了 * 思路: * 1. 通过列表的数量乘于显示列表项的高度得出最终可拖拽区域的总高度,赋值给 movable-area * 2. 扩展列表项一些字段,此处使用 y 保存当前项距离顶部位置, idx 保存当前项所在列表的下标 / // 伪代码 // js initList(list) { this.areaHeight = list.length * this.height; // aeraHieght 可拖拽区域总高度, height 为元素所需高度 this.internalList = list.map((item, idx) => { return { ...item, y: idx * this.height, // movable-view 当前项所处的高度 idx: idx, // 当前项所处于列表的下标,用于比较 animation: true, // 主要用于控制拖拽的元素要关闭动画, 其他的元素可以保留动画 } }) } // html 二、 如何控制拖拽元素的跟随 // 主要是通过监听 movable-view 的 touchstart touchmove touchend 三个事件完成拖拽动作的起始、移动、结束。 // methods { _dragStart(e){ // 取得触摸点距离行顶部距离 this.deviationY = (e.mp.touches[0].clientY - this.wrap.top) % this.height; this.internalList[idx].animation = false; // 关闭当前拖拽元素的动画属性 this.activeIdx = idx; // 保存当前拖拽元素的下标 }, _dragMove(e) { const activeItem = this.internalList[this.activeIdx]; if (!activeItem) return; // 实时取得触摸点的位置信息 const clientY = e.mp.touches[0].clientY; let touchY = clientY - this.wrap.top - this.deviationY + this.scrollTop; if (touchY <= 0 || touchY + this.height >= this.areaHeight) return; activeItem.y = touchY; // 拖拽元素的移动秘密就在于此 } } 三、如何使拖拽中的元素与相交互的元素进行位置调换 // 上述代码解决了当前拖拽元素的位置移动问题, 接下来就需要解决拖拽元素和上下元素交互的问题 // methods { __dragMove(e){ // ...同上代码一致 // 上下元素交互位置代码实现 for(let item of this.internalList) { if (item.idx !== activeItem.idx) { if (item.idx > activeItem.idx) { // 如果当前元素下标大于拖拽元素下标,则检查当前拖拽位置是否大于当前元素中心点 if (touchY > item.idx * this.height - this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; // 对调位置 item.y = item.idx * this.height; // 更新对调后的位置 break; // 退出循环 } } else { // 如果当前元素下标小于拖拽元素下标,则检查当前拖拽位置是否小于当前元素中心点 if (touchY < item.idx * this.height + this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; item.y = item.idx * this.height; break; } } } } } } 四、如何判断拖拽元素至上下边界滚动屏幕 // 将 movable-area 包裹在 scroll-view 标签中, 通过控制 scroll-top 的值来进行滚动 // 思路: 判断当前拖拽元素的位置信息与当前屏幕可视区域进行比较 // methods { _dragMove(e) { // ...同上代码 // 检查当前位置是否处于可视区域 if (activeItem.idx + 1 * this.height + this.height / 2 > this.scrollTop + this.wrap.top) { this.viewTop = this.scrollTop + this.height; // 往上滚动一个元素的高度 } else if (activeItem.idx * this.height - this.height / 2 < this.scrollTop ) { this.viewTop = this.scrollTop - this.height; // 往下滚动一个元素的高度 } } } 五、如何使页面的滚动与拖拽时的滚动互不影响 // 事实上我是通过一种取巧的方式, scroll-veiw 有一个 scroll-y 属性可以控制滚动方向 // 思路: // 1.不进行滚动的时候将 scroll-y 置为 true , 使用默认的滚动效果 // 2.当进入拖拽排序状态时则将 scroll0y 置为 false, 滚动通过拖拽代码比较计算滚动位置 完整代码: 主要小程序上的插槽不允许往外传值、所以自定义元素实现的方式相比于H5实现Vue的方式比较别扭。 因为有多个地方需要用到排序功能,所以边抽离了 js 部分进行混入。 // DargSortMixin.js 文件 export default { props: { list: { type: Array, default() { return []; }, }, sort: { type: Boolean, default: false, }, height: { type: Number, default: 66, }, }, data() { return { areaHeight: 0, // 区域总高度 internalList: [], // 列表 activeIdx: -1, // 移动中激活项 deviationY: 0, // 偏移量 // 包裹容器信息 wrap: { top: 0, height: 0, }, viewTop: 0, // 指定滚动高度 scrollTop: 0, // 容器实时滚动高度 scrollWithAnimation: false, canScroll: true, }; }, created() { // 组件使用选择器,需用使用this const query = this.createSelectorQuery(); query .select('#scroll-wrap') .boundingClientRect(rect => { if (rect) { this.wrap = { top: rect.top, height: rect.height, }; } }) .exec(); }, watch: { list: { handler(val) { this.initList(val); }, immediate: true, }, }, methods: { getList() { return this.internalList .sort((a, b) => { return a.idx - b.idx; }) .map(item => { let newItem = { ...item }; delete newItem.y; delete newItem.idx; delete newItem.animation; return newItem; }); }, initList(list) { this.areaHeight = list.length * this.height; this.internalList = list.map((item, idx) => { return { ...item, y: idx * this.height, idx, animation: true, }; }); }, _dragStart(e, idx) { // 取得触摸点距离行顶部距离 this.deviationY = (e.mp.touches[0].clientY - this.wrap.top) % this.height; this.internalList[idx].animation = false; // 关闭动画 this.activeIdx = idx; this.scrollWithAnimation = true; this.canScroll = false; }, _dragMove(e) { const activeItem = this.internalList[this.activeIdx]; if (!activeItem) return; // 保存触摸点位置和长按时中心一致 const clientY = e.mp.touches[0].clientY; let touchY = clientY - this.wrap.top - this.deviationY + this.scrollTop; if (touchY <= 0 || touchY + this.height >= this.areaHeight) return; activeItem.y = touchY; // 设置位置 // 检查元素和上下交互元素的位置 for (const item of this.internalList) { if (item.idx !== activeItem.idx) { if (item.idx > activeItem.idx) { if (touchY > item.idx * this.height - this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; // 对调位置 item.y = item.idx * this.height; // 更新位置 break; } } else { if (touchY < item.idx * this.height + this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; // 对调位置 item.y = item.idx * this.height; // 更新位置 break; } } } } // 检查当前位置是否处于可视区域 if ( (activeItem.idx + 1) * this.height + this.height / 2 > this.scrollTop + this.wrap.height ) { this.canScroll = true; activeItem.y = activeItem.idx * this.height; this.$nextTick(() => { this.viewTop = this.scrollTop + this.height; }); } else if (activeItem.idx * this.height - this.height / 2 < this.scrollTop) { this.canScroll = true; activeItem.y = activeItem.idx * this.height; this.$nextTick(() => { this.viewTop = this.scrollTop - this.height; }); } }, _dragEnd(e) { const activeItem = this.internalList[this.activeIdx]; if (!activeItem) return; activeItem.animation = true; activeItem.disabled = true; activeItem.y = activeItem.idx * this.height; this.activeIdx = -1; this.scrollWithAnimation = false; this.canScroll = true; }, _onScroll(e) { this.scrollTop = e.detail.scrollTop; }, }, }; // TheDragSortAreaList.vue 文件 import DragSortMixin from '@/mixins/DragSortMixin'; export default { name: 'TheDragSortTableList', mixins: [DragSortMixin], }; .active-item { z-index: 10; } .drag-item { background: $theme-color; color: $white !important; .count { color: $white !important; } }
2020-11-27 - 用movable组件写出简短的拖放/拖拽/拖动 排序,含详细的讲解【拎包哥】
「前言」 这应该是社区目前(2020/12/8)最简短的拖拽排序教程之一,助你快速上手哦。 拖放排序是前端中可以和订单规格选择等等比较的,知识点最密集的基础之一。 如果你有html的基础知识,你会发现微信小程序其实是集成度非常高的框架,和vue,react等响应式前端框架没有本质的区别,甚至集成度还更高。 所以在这里好好利用小程序自身的组件及其属性,就能快速写出简短的拖拽排序。 注:感谢@烟斗 留言帮助! ========================效果图============================= [图片] 微信小程序 ========================HTML篇============================= 只使用小程序提供的movable组件即可。它简化了拖放排序的条件 ,让我们只需要控制y值就可以确定组件的位置。拖放中的放动作有手指离开的动作,而movable组件没有这个属性,所以引用了touchend。注意z-index判断层级<movable-area class='ctr'> <block wx:for='{{arr}}' wx:key='x'> <movable-view bindchange='change' bindtouchend='end' y='{{item.y}}' class='item' direction='vertical' style="z-index:{{index==dragId?2:1}}"> {{item.name}} </movable-view> </block></movable-area>(ps. 由于微信社区难以理解的bug,这里的代码不能放在代码片段里) ========================CSS篇============================= 在这个CSS我只有item的height用到了px,因为y值的像素单位是px。在css尽量不要增加额外的height属性,否则这个组件就不精准了。.ctr{ width: 400rpx; height: 800rpx; border: 1rpx solid black; } .item{ width: 400rpx; height: 50px; /* 与后来确定y值的的 i * 50对应 */ border-bottom: 1rpx solid black; box-sizing: border-box; background:white; /* 让边框内嵌,否则会随着1rpx的叠加而让y值变得不精准 */ } =========================JS篇============================== 主要步骤 用y值来确定拖放动作中放的位置将源item放置在目标item前(这也是排序的本质)注意 拖拽的数组arr一开始就放在onLoad方法而不是data里,否则会因为data的提前渲染而产生缓慢的位移。movable-view一开始是重叠的,所以要根据下标来确定每个item的y值。bindchange对应的是拖行为,我们只需要在这个方法里获取我们在拖行为时产生的y值。拖动行为不会触发bindtap那么在touchend的时候就可以获得bindchange最后一个y值,并借此确定放行为的对应的下标。 Page({ onLoad() { var arr = [ {name: 'Mike'}, {name: 'Paul'}, {name: 'Peter'}, {name: 'Andy'}, {name: 'Larry'} ] for (var i in arr) { arr[i].y = i * 50 } // movable-view的y值单位是px console.log(arr) this.setData({ arr }) }, tap(){ // console.log('在拖拽时是否出发点击行为?') // 在拖拽时不触发点击行为 }, change(e) { this.y = e.detail.y var dragId = e.currentTarget.id // 默认item id,wx-for 分配给每个item的index,我在html里id={{index}},即用id变量记录分配后的index this.setData({ dragId }) }, end(e) { console.log('im 触摸结束') console.log(this.y) // this.y item下边线到movearea顶端的距离 var arr = this.data.arr var id = e.currentTarget.id var currentId = this.y / 50 // 移动时不断计算的id if (id > currentId) { var transferId = Math.ceil(currentId) } else { var transferId = Math.floor(currentId) } var save = arr[id] // 保存初始id arr.splice(id,1) arr.splice(transferId,0,save) // 精华 for (var i in arr) { arr[i].y = i * 50 } this.setData({ arr }) } }) ------------------------------------------进阶篇vue-cli------------------------------------------- vue-cli4 ========================HTML篇============================= 挖坑,在研究vue脚手架vue-cli4的拖拽排序,未完待续。
2021-01-17 - 微信小程序页面停留时间统计
近来在研究微信小程用户是否在使用小程序或者查看用户在小程序停留的时间,无意中在git上找到了相关的解决问题方法,希望正在开发这个功能的的你,能帮助你解决! [图片]但是好像有 收到一个需求,要统计一个用户在我们小程序的每个页面的停留时间。 看了下现成的API,除了这个好像也没有别的可以用:https://mp.weixin.qq.com/debug/wxadoc/dev/api/analysis-visit.html#访问趋势, 这个里面貌似有页面停留时间的数据, 参数说明ref_date时间,如:"20170306-20170312"session_cnt打开次数(自然周内汇总)visit_pv访问次数(自然周内汇总)visit_uv访问人数(自然周内去重)visit_uv_new新用户数(自然周内去重)stay_time_uv人均停留时长 (浮点型,单位:秒)stay_time_session次均停留时长 (浮点型,单位:秒)visit_depth平均访问深度 (浮点型) 但是好像有查询时间限制,只能查询一天的数据。毕竟小程序数据很大,估计也是怕数据量太大查询慢吧。 算了,自己写一个吧, 初步想法,在页面的[代码]onShow[代码]事件里面,打一个开始的时间戳,然后在[代码]onHide[代码]里面再弄一个时间戳,两个一减,然后把得出来的数据,一提交,齐活。 BUT~,尼玛,[代码]onShow[代码]和[代码]onHide[代码]不仅在页面切换的时候会触发,小程序切换到后台和回到前台,也会触发,这就有干扰了。 但是在[代码]app.js[代码]里面的[代码]onShow[代码]和[代码]onHide[代码]事件只在小程序前后台切换的时候才会触发,不会在页面切换的时候触发,利用这点,把前后台切换排除掉,只在页面切换的时候,上报页面停留时间就好了 在[代码]app.js[代码]里面,初始化以下三个状态, globalData: { firstIn:1, onShow: 0, onHide: 0 } [代码]onShow[代码]和[代码]onHide[代码]的值默认为[代码]0[代码],当小程序进入后台或者返回前台的时候,给这两个值变为[代码]1[代码],用来告诉页面,刚才的切换是前后台切换,不是页面切换,不用上报页面停留时间。代码如下: 依旧是在[代码]app.js[代码]里面 onShow(){ if(this.globalData.firstIn){ this.globalData.firstIn = 0; } else{ this.globalData.onShow = 1; } }, onHide(){ this.globalData.onHide = 1; } 里面的[代码]firstIn[代码]表示是不是第一次进入小程,因为第一次进入的时候也会触发[代码]onShow[代码](相当于从后台切换到前台了),要把这个也排除在外。默认是第一次进入,进入之后就把这个值置为[代码]0[代码] OK,[代码]app.js[代码]准备好了,然后看下具体页面的, 在页面里面,先声明两个变量,一个[代码]startTime[代码],一个[代码]endTime[代码]分别来存储用户进入页面的时间和离开的时间 var startTime, endTime, app = getApp(); Page({ onShow(){ setTimeout(function () { if (app.globalData.onShow) { app.globalData.onShow = 0; console.log("demo前后台切换之切到前台") } else { console.log("demo页面被切换显示") startTime = +new Date(); } }, 100) }, onHide(){ setTimeout(function () { if (app.globalData.onHide) { app.globalData.onHide = 0; console.log("还在当前页面活动") } else { endTime = +new Date(); console.log("demo页面停留时间:" + (endTime - startTime)) var stayTime = endTime - startTime; //这里获取到页面停留时间stayTime,然后了可以上报了 } }, 100) } }) 有几个页面要统计的,就把这几个页面都加一下。 嫌麻烦的话,可以修改一下[代码]Page[代码]方法,默认自带[代码]onShow[代码]和[代码]onHide[代码],然后如果外面有传入的话,可以合并。页面在使用的时候,直接用这个心的[代码]Page[代码],就不用每个页面都[代码]onHide[代码]、[代码]onShow[代码]了,这里就不上具体的代码了。 关于[代码]setTimeout[代码]的说明: 页面的[代码]onShow[代码]和[代码]onHide[代码]会在[代码]app.js[代码]的[代码]onShow[代码]和[代码]onHide[代码]之前执行,加个延迟,放到后面执行,这样每次都可以先检测是页面切换还是前后台切换,然后再去做对应的逻辑,不然就反了。 参考地址:https://github.com/ireeoome/reeoome/issues/3 作者:Ams
2020-12-01 - 【建议收藏】推荐 10 款使用云开发的开源项目
1. 美食地图 地址:https://github.com/cloudkits/miniprogram-foodmap 2. 猫叫助手 地址:https://github.com/Rychou/mpvue-cloud 3. 图书管理系统 地址:https://github.com/AmosXu/library-mini-program 4. 网易云课堂 地址:https://github.com/MarchYuanx/study163 5. 五指棋 地址:https://github.com/Rateler-Inc/five-chesses-min-cloud 6. 古诗词大全 地址:https://github.com/caochangkui/miniprogram-project 7. 吸猫小程序 地址:https://github.com/godbasin/kitty-wxapp 8. 朋友圈 地址:https://github.com/xiaozhaoqi/moments 9. 一起算账 地址:https://github.com/GzhiYi/accounting-together 10. 云开发优秀实践案例集合 地址:https://github.com/TencentCloudBase/Good-practice-tutorial-recommended
2020-08-30 - 【集合】花了 3 个月,写了 40 篇小程序文章
前言 花了3个月,一共输出 40 篇文章,这也算是一个阶段性的总结。在此做个文章分类集合,希望对大家有所帮助。 小程序前端 《专治按钮效果不明显(扩散动画效果)》 《小程序开发必备,这 5 款超实用开源插件!》 《仿抽奖助手奖品详情页面向上翻页效果》 《推荐 5 款高仿知名应用的开源项目!》 《生成海报很复杂?有它轻松搞定!》 《推荐一个自定义导航栏开源库》 《前端开发,必备的学习网站!》 《情侣券-领取动画分析》 《通过玩游戏来学习CSS》 《CSS不规范导致的布局显示问题》 《微信小程序如何引入npm包?》 《情侣券-选中卡片翻转动画》 《CSS:实现卡片洗牌效果》 《情侣券 v2.0 使用的 4 款开源组件》 小程序云开发 《使用聚合函数实现打卡排行榜》 《使用云开发做内容安全检查》 《云开发-实现分页功能》 《云开发-实现维护用户表》 《云开发-实现模糊搜索》 《云开发实战:实现订阅消息推送》 《如何优雅的调用云函数?》 《云开发实战-如何维护用户表?(优化版)》 《推荐 10 款使用云开发的开源项目》 《云开发:CloudBase CMS 实战使用指南》 小程序产品 《如何利用小程序提高10倍活动效果?》 《实战:让数据说话之自定义埋点分析》 《#小程序云开发挑战赛#-情侣券》 《小程序运营必备的 3 款官方小程序》 《小程序云开发挑战赛:情侣券 v1.1 版本迭代》 《云开发挑战赛复赛:情侣券介绍PPT》 《参加#小程序云开发挑战赛#复赛收获》 《云开发挑战赛决赛:情侣券介绍PPT》 通用知识 《如何重构?》 《如何高效学习?》 《如何看懂时序图?》 《为什么优秀的程序员都写博客?》 《我从 Android 转到 微信小程序 的思考》 最后 后续计划会写更多云开发相关的文章以及小程序基础系列学习文章。
2020-11-24 - 开源闯关答题小程序
全民闯关答题小程序是使用云开发的在线答题小程序,无需搭建服务器,无需域名即可使用云端能力。 V2.0版已完成功能 答题闯关挑战答题发放红包(可设置几题发放红包,每题设置红包数额)答题等级设置(自定义等级名称和关卡)观看激励视频获取金币(限制每天观看次数)排行榜显示红包兑换物品设置更多软件推广集成banner 视频广告,插屏广告,激励视频广告等展示截图 [图片][图片][图片][图片] 现将V1.0初始版进行开源学习,欢迎大家学习,如果上线需要经过作者许可哦 ~ 开发不易、创作不易。 码云地址: https://gitee.com/yingwuniao/chuangguan 技术文档下次会发文章补上
2020-10-10 - #小程序云开发挑战赛#-流浪猫速查手册-猫
项目背景 今年5月中下旬,作者在网上发现了一款非常有爱的小程序,叫做《燕园猫速查手册》,因为作者本身也参与流浪猫的救助工作,流浪猫的管理主要是靠记忆,非常不方便管理,于是用业余时间参考《燕园猫速查手册》开发了《流浪猫速查手册》小程序,主要针对的是社会上的流浪猫信息的管理。后续也会开放 [代码]领养[代码] 等模块,致力于倡导大家关爱流浪猫,领养代替购买。 6月1日上线了 [代码]1.0.0[代码] 版本,小程序中只有查看功能,配套也开发了一个 [代码]PC[代码] 管理端,上线后发现志愿者和救助站等人员不习惯使用 [代码]PC[代码] 端操作,也发现了很多细节问题,如流浪猫的隐私保护等。 8月中旬本作者和《燕园猫速查手册》的作者 circle 在微信中聊了聊,重新梳理的整个小程序的功能模块,也趁此机会报名参加了 [代码]云开发挑战赛[代码],重构了 [代码]1.0.0[代码] 版本的代码,并迭代了 [代码]2.0.0[代码] 版本。 应用场景 为了方便管理流浪猫的信息,把流浪猫的信息分享给喜爱猫咪的小伙伴,流浪猫信息的上报等。 目标用户 所有喜欢猫的用户 功能模块 [图片] 实现思路 本项目前端采用 uni-app 框架开发,后端完全采用 微信小程序云开发 实现 业务流程图 [图片] 功能演示 腾讯视频:https://v.qq.com/x/page/w3153x67dxh.html 效果截图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] 代码链接 GitHub: https://github.com/zhiiee/cats 开源许可 《流浪猫速查手册》的源代码基于 [代码]GPL-3.0[代码] 协议全网开源,可用于商业用途,如果您使用了《流浪猫速查手册》的源代码,那么您的项目必须遵守 [代码]GPL-3.0[代码] 协议进行全网开源,点击 LICENSE 查看许可协议。 体验二维码 线上版本 最新版本还在审核中 可以扫描下面的体验版本申请访问 [图片] 体验版本 同步最新的开发进度(生产环境) [图片] 团队/作者简介 Stephen: 一个热爱宠物,兼职创业,想获得一次天使轮投资,在宠物行业闯出一片天的程序员。
2020-09-20 - 关于虚拟支付的汇总整理
本文场景 在iso端做支付,虚拟支付问题是绕不过去,没有处理好,轻则封禁搜索,重则永久封号,所以对待这个问题不可谓不慎重 本文内容本文主要汇总社区相关的几个经典帖子,特别是带有官方回复的帖子,进行截图,最后做一个总结 截图一 [图片] 截图二 [图片] 截图三 [图片] 截图四 [图片] 参考文章 为什么“小鹅通”的小程序可以做虚拟支付?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/84ef0895eb9e4261b114a88d73dc7621 所谓的小程序IOS不允许虚拟支付到底限制的是谁?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000246265d01201064ea17bc65b813 实名举报手机充值小程序虚拟支付可以正常使用?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/00066aedd70a907b38ea5043856400 跑腿小程序,支付跑腿费,属于虚拟支付吗?可以在ios里进行支付吗?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/0002e8ec24cbd0f29cba2e28156400 虚拟业务指南请收好。? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/000cc6c0b383a047c7798e0045b409 本文总结关于虚拟支付的整理 关于一个支付业务到底是不是虚拟支付,在有参考的情况,可以根据官方的 上面社区的锅巴同学整理的非常详细,给出了官方明确定义为非虚拟支付的6种情况 以下内容摘录自锅巴同学的社区回帖内容 小程序在线直播课程,充值加油卡,手机流量等这6种,不算小程序虚拟支付,请参照以下6种情况即可,因为你不符合这6种情况,所以被判定为违规 ① 小程序在线课程直播。用户先买课程,后续在线上安排老师在小程序直播。补充选择教育-在线视频课程类目 ② 线上报名活动,线下培训的类型 ③ 充值加油卡加油,涉及预付卡销售服务,补充商家自营-预付卡销售类目 ④ 充值手机流量,补充IT科技-电信运营商类目 ⑤ 悬赏问答功能,需选择社交红包-社交红包类目,并完成新商户号申请后,再提交代码审核。 ⑥ 微信支付充值积分,签到积分等,积分兑换实物商品,兑换成功后,会直接给用户寄过去。有实际服务存在 最后总结下 你所认为的虚拟支付可能并不是虚拟支付,你所强调的非虚拟支付也有可能被定义为虚拟支付,所以一切以官方口径为依据。
2020-09-07 - 小程序中通过CSS实现炫酷的动画效果
1.Animate.css简介Animate.css是一个可在您的Web项目中使用的即用型跨浏览器动画库。非常适合强调,首页,滑块和引导注意的提示。它是一个来自国外的 CSS3 动画库,它预设了抖动(shake)、闪烁(flash)、弹跳(bounce)、翻转(flip)、旋转(rotateIn/rotateOut)、淡入淡出(fadeIn/fadeOut)等多达 60 多种动画效果,几乎包含了所有常见的动画效果。虽然借助Animate.css 能够很方便、快速的制作 CSS3 动画效果,但还是建议看看Animate.css 的代码,也许你能从中学到一些东西。不论是在Web端和小程序内都可以正常使用,详细内容请到官方地址学习。 2.动画效果的实现在使用过程中,可以根据自己的喜好来改造css代码来达到你想要的效果,文中动图显示可能不是特别直观,建议自己写一遍代码,即利于学习,又能够直观的体会到动画效果。 1.发光的盒子 [图片] wxml代码: <view id="box">I am LetCode!</view> wxss代码: @keyframes animated-border { 0% { box-shadow: 0 0 0 0 rgba(255,255,255,0.4); } 100% { box-shadow: 0 0 0 20px rgba(255,255,255,0); } } #box { animation: animated-border 1.5s infinite; height: 100rpx; font-family: Arial; font-size: 18px; font-weight: bold; color: white; border: 2px solid; border-radius: 10px; margin: 100px 15px; line-height: 100rpx; text-align: center; } 2.文字的缩放效果 [图片] wxml代码: <view class="animate_zoomOutDown">关注公众号“Let编程”,有更多分享!</view> wxss代码: @keyframes zoomOutDown { 40% { opacity: 1; transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } to { opacity: 0; transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } .animate_zoomOutDown { animation:2s linear 0s infinite alternate zoomOutDown; font-family: Arial; font-size: 18px; font-weight: bold; color: white; margin-top: 70px; text-align: center; margin-top: 15px; } 3.加载动画 [图片] wxml代码: <view class="load-container load"> <view class="loader"> </view> </view> <view class="txt">关注公众号“Let编程”,有更多分享!</view> wxss代码: .load-container { width: 240px; height: 240px; margin: 0 auto; position: relative; overflow: hidden; box-sizing: border-box; } .load .loader { color: #ffffff; font-size: 90px; text-indent: -9999em; overflow: hidden; width: 1em; height: 1em; border-radius: 50%; margin: 72px auto; position: relative; transform: translateZ(0); animation: load 1.7s infinite ease, round 1.7s infinite ease; } @keyframes load { 0% { box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;} 5%, 95% { box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;} 10%, 59% { box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;} 20% { box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;} 38% { box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;} 100% { box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;} } @keyframes round{ 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } 4.抖动的文字 [图片] wxml代码: <view class="shake-slow txt">关注公众号“Let编程”,有更多分享!</view> wxss代码: @keyframes shake-slow { 2% { transform: translate(6px, -2px) rotate(3.5deg); } 4% { transform: translate(5px, 8px) rotate(-0.5deg); } 6% { transform: translate(6px, -3px) rotate(-2.5deg); } 8% { transform: translate(4px, -2px) rotate(1.5deg); } 10% { transform: translate(-6px, 8px) rotate(-1.5deg); } 12% { transform: translate(-5px, 5px) rotate(1.5deg); } 14% { transform: translate(4px, 10px) rotate(3.5deg); } 16% { transform: translate(0px, 4px) rotate(1.5deg); } 18% { transform: translate(-1px, -6px) rotate(-0.5deg); } 20% { transform: translate(6px, -9px) rotate(2.5deg); } 22% { transform: translate(1px, -5px) rotate(-1.5deg); } 24% { transform: translate(-9px, 6px) rotate(-0.5deg); } 26% { transform: translate(8px, -2px) rotate(-1.5deg); } 28% { transform: translate(2px, -3px) rotate(-2.5deg); } 30% { transform: translate(9px, -7px) rotate(-0.5deg); } 32% { transform: translate(8px, -6px) rotate(-2.5deg); } 34% { transform: translate(-5px, 1px) rotate(3.5deg); } 36% { transform: translate(0px, -5px) rotate(2.5deg); } 38% { transform: translate(2px, 7px) rotate(-1.5deg); } 40% { transform: translate(6px, 3px) rotate(-1.5deg); } 42% { transform: translate(1px, -5px) rotate(-1.5deg); } 44% { transform: translate(10px, -4px) rotate(-0.5deg); } 46% { transform: translate(-2px, 2px) rotate(3.5deg); } 48% { transform: translate(3px, 4px) rotate(-0.5deg); } 50% { transform: translate(8px, 1px) rotate(-1.5deg); } 52% { transform: translate(7px, 4px) rotate(-1.5deg); } 54% { transform: translate(10px, 8px) rotate(-1.5deg); } 56% { transform: translate(-3px, 0px) rotate(-0.5deg); } 58% { transform: translate(0px, -1px) rotate(1.5deg); } 60% { transform: translate(6px, 9px) rotate(-1.5deg); } 62% { transform: translate(-9px, 8px) rotate(0.5deg); } 64% { transform: translate(-6px, 10px) rotate(0.5deg); } 66% { transform: translate(7px, 0px) rotate(0.5deg); } 68% { transform: translate(3px, 8px) rotate(-0.5deg); } 70% { transform: translate(-2px, -9px) rotate(1.5deg); } 72% { transform: translate(-6px, 2px) rotate(1.5deg); } 74% { transform: translate(-2px, 10px) rotate(-1.5deg); } 76% { transform: translate(2px, 8px) rotate(2.5deg); } 78% { transform: translate(6px, -2px) rotate(-0.5deg); } 80% { transform: translate(6px, 8px) rotate(0.5deg); } 82% { transform: translate(10px, 9px) rotate(3.5deg); } 84% { transform: translate(-3px, -1px) rotate(3.5deg); } 86% { transform: translate(1px, 8px) rotate(-2.5deg); } 88% { transform: translate(-5px, -9px) rotate(2.5deg); } 90% { transform: translate(2px, 8px) rotate(0.5deg); } 92% { transform: translate(0px, -1px) rotate(1.5deg); } 94% { transform: translate(-8px, -1px) rotate(0.5deg); } 96% { transform: translate(-3px, 8px) rotate(-1.5deg); } 98% { transform: translate(4px, 8px) rotate(0.5deg); } 0%, 100% { transform: translate(0, 0) rotate(0); } } .shake-slow{ animation:shake-slow 5s infinite ease-in-out; } 在实际开发过程中,远不止这些炫酷的动画效果,在互联网迅速的发展状态下,还需要更多的程序员来实现功能需求,因此本文只做简单的介绍,未完待续.....
2020-06-04 - 微信小程序云开发数据库脚手架
介绍 1、封装、简化、模型化数据库操作 2、支持查看执行的sql语句 基础库 大于2.8.1 代码片段DEMO 小程序demo: https://developers.weixin.qq.com/s/oXiGFFmI7mia 说明文档: https://www.yuque.com/docs/share/80cbef90-f262-4d2a-b245-079bc462d5e3 如何使用 一、小程序端 1、下载wx-cloud-db-falsework npm i wx-cloud-db-falsework 2、将wx-cloud-db-falsework.min.js拷贝到小程序项目中,如小程序根目录下的lib文件夹内 [图片] 3、在app.js中引入wx-cloud-db-falsework.min.js,使用重写过的Page,和原生Page一致,只是增加了use属性 [代码]// 根目录/app.js import { page } from './lib/wx-cloud-db-falsework.min' Page = page App({ onLaunch: function () {} } [代码] 4、创建模型,如: Order,根目录/model/order.js,集合名,默认为model名,首字母会强制小写 [代码]// 根目录/model/order.js import { Model, Controller } from '../lib/wx-cloud-db-falsework.min' class Order extends Model { constructor(o) { let t = super(o) return t } } // collectionName 集合名,默认为model名,如Order默认为order,首字母会小写 // collectionKey 集合主键,默认_id const Odr = Order.init({ env: '环境ID', cloud: wx.cloud [, collectionName = '集合名称', , collectionKey = '主键字段名'] }) export { Odr as Order, Controller } [代码] 5、创建控制器,如: Order的controller,根目录/controller/order.js [代码]// 根目录/controller/order.js import { Order, Controller as Base } from '../model/order' class Controller extends Base { constructor(o){ super(o) } // 在控制器里可以自己封装一些业务功能方法 getById(id){ return Order.find(id) } } let controller = new Controller(Order) export { controller } [代码] 6、在页面中通过use配置需要使用的控制器,控制器实例会挂载在页面实例上,直接通过 this.控制器实例 即可访问,页面: 根目录/pages/index/index.js [代码]// 根目录/pages/index/index.js import { DB } from '../../lib/wx-cloud-db-falsework.min' Page({ use:{ Order: require('../../controller/order.js') }, data:{}, onLoad: async function() { // 直接通过 this.控制器实例 即可访问控制器的封装的方法 await this.Order.getById(1).then(res=>{ console.log(res) // res 可为 DB.DbError实例 或者 DB.DbRes实例 // 为DB.DbError实例,表示数据库操作异常 // 为DB.DbRes实例,表示数据库操作正常,而DB.DbRes又包括了DB.DbResultJson和DB.DbResultArray子实例 // DB.DbResultJson实例说明结果为JSON对象,DB.DbResultArray实例则说明结果为Array对象 }) // 页面中也可以通过 this.(控制器实例名+'Md')访问模型的方法 let row = await this.OrderMd.find('xxxx') // 页面中也可以通过 this.(控制器实例名+'Dd')访问原生db对象,进行原生操作 let doc = await this.OrderDb.collection('xxx').doc(xx).get() } } [代码] 二、云函数端 1、配置云函数package.json,添加wx-cloud-db-falsework [代码]"dependencies": { "wx-cloud-db-falsework": "latest", "wx-server-sdk": "^2.1.2" } [代码] 2、配置云函数package.json,添加wx-cloud-db-falsework [代码]const cloud = require('wx-server-sdk') let { Model, DB } = require('wx-cloud-db-falsework') # order模型 class Order extends Model { constructor(o) { let t = super(o) return t } } exports.main = async (event, context) => { const Odr = Order.init({ env: '环境ID', cloud }) return await Odr.add({ _id: (new Date().getTime())+'add'}) } [代码] 数据库操作 添加记录 以上述Order模型为例,在页面中添加记录 [代码]// 使用model.add(data)添加记录, data为数据json对象或array对象 // 添加一条记录 let res = await this.OrderMd.add({ a:1, b:2 }) // 返回DbResultJson对象 {_id:'xxxx'} // 添加多条记录 res = await this.OrderMd.add([{ a:1, b:2 }, { a:3, b:4 }]) // 返回DbResultArray对象 [{_id:'xxxx'},{_id:'xxxx'}] // 支持原生参数 res = await this.OrderMd.add({ data:{ a:1, b:2 } }) res = await this.OrderMd.add({ data: [{ a:1, b:2 }, { a:3, b:4 }] }) // 支持回调函数 this.OrderMd.add({ data:{ a:1, b:2 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.add({ data:[{ a:1, b:2 }, { a:3, b:4 }], success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 删除记录 以上述Order模型为例,在页面中删除记录 [代码]// 使用model.remove(where)删除记录, where为删除添加,可为空 // 根据条件删除记录 let res = await this.OrderMd.where({ _id: 'xxx', abc: 123 }).remove() res = await this.OrderMd.remove({ _id: 'xxx', abc: 123 }) // 根据主键删除 res = await this.OrderMd.doc('xxx').remove() // 返回DB.DbError对象或DB.DbRes对象 {removed:Number} // 支持回调函数 this.OrderMd.where({ _id: 'xxx', abc: 123 }).remove({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.remove({ where:{ _id: 'xxx', abc: 123 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 查询记录 查询一条记录 以上述Order模型为例,在页面中查询记录 [代码]// 使用model.find()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ _id: 'xxx', abc: this.OrderMd._.eq(123) }).find() res = await this.OrderMd.find({ _id: 'xxx', abc: this.OrderMd._.eq(123) }) // 根据主键查询记录 res = await this.OrderMd.doc('xxx').find() res = await this.OrderMd.find('xxx') // 返回DB.DbError对象或DB.DbRes对象 {_id:'xxx', ....} // 使用model.where().get()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ _id: 'xxx' }).get() // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}] // 根据主键查询记录 res = await this.OrderMd.doc('xxx').get() // 返回DB.DbError对象或DB.DbRes对象 {_id:'xxx', ....} // 支持回调函数 this.OrderMd.find({ where:{ _id: 'xxx', abc: 123 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ _id: 'xxx', abc: 123 }).find({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ _id: 'xxx' }).get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.doc('xxx').get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 特殊支持: [代码]// json字符串条件 let res = await this.OrderMd.find(`{ _id: 'xxx' }`) res = await this.OrderMd.where(`{ _id: 'xxx' }`).find() // 使用command时需要加this res = await this.OrderMd.find(`{ _id: this._.eq('xxx') }`) res = await this.OrderMd.where(`{ _id: this._.eq('xxx') }`).find() [代码] 查询多条记录 [代码]// 使用model.findAll()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ abc: 'xxx', abc: this.OrderMd._.eq(123) }).findAll() res = await this.OrderMd.findAll({ abc: 'xxx', abc: this.OrderMd._.eq(123) }) // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}] // 使用model.where().get()查询记录 // 根据条件查询记录 let res = await this.OrderMd.where({ _id: 'xxx' }).get() // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}] // 支持回调函数 this.OrderMd.findAll({ where:{ abc: 'xxx' }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ abc: 'xxx' }).findAll({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.where({ _id: 'xxx' }).get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) this.OrderMd.doc('xxx').get({ success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 特殊支持: [代码]// json字符串条件 let res = await this.OrderMd.findAll(`{ abc: 'xxx' }`) res = await this.OrderMd.where(`{ abc: 'xxx' }`).findAll() // 使用command时需要加this res = await this.OrderMd.findAll(`{ abc: this._.eq('xxx') }`) res = await this.OrderMd.where(`{ abc: this._.eq('xxx') }`).findAll() [代码] 更新记录 以上述Order模型为例,在页面中查询记录 [代码]// 使用model.update()更新记录 // 根据条件更新记录 let res = await this.OrderMd.where({ _id: 'xxx', abc: this.OrderMd._.eq(123) }).update({ cde: 456 }) res = await this.OrderMd.update({ cde: 456 }, { _id: 'xxx', abc: this.OrderMd._.eq(123) }) // 根据主键更新记录 res = await this.OrderMd.doc('xxx').update({ cde: 456 }) // 返回DB.DbError对象或DB.DbRes对象 {updated:Number} // 支持回调函数 this.OrderMd.where({ _id: 'xxx', abc: this.OrderMd._.eq(123) }).update({ data:{ cde: 456 }, success(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 }, fail(e){ // e 为 DB.DbError实例 }, complete(r){ // r 为 DB.DbError实例 或者 DB.DbRes实例 } }) [代码] 聚合操作 以上述Order模型为例,在页面中查询记录 [代码]// 使用model.aggregate()开始聚合操作 let res = await this.OrderMd.aggregate() .addFields({ test: 1 }) .match({ test: 1 }) ... // 其他聚合操作 .end() // 返回DB.DbError对象或DB.DbRes对象 [{_id:'xxx', ....}, ...] [代码] 数据监听 以上述Order模型为例,在页面中监听数据 [代码]// 使用model.wcthis(ops)监听记录, let res = await this.OrderMd.wcthis({ onChange(res){ // 监听操作 }, onError(err){ // 监听出错 } }).where({ _id: 'xxx' }).update({ cde: 456 }) // 使用原生watch方法监听记录, await this.OrderMd.where({ _id: 'xxx' }).watch({ onChange(res){ // 监听操作 }, onError(err){ // 监听出错 } }) let res = this.OrderMd.where({ _id: 'xxx' }).update({ cde: 456 }) [代码] 数据库操作结果 数据库操作结果返回 DB.DbError实例 或者 DB.DbRes实例 add结果 res = model.add(…) 添加一条数据返回 { _id: ‘xxx’ } ,添加多条时返回 [{ _id: ‘xxx’ }, { _id: ‘xxx’ }] 返回的结果可以直接调用update,remove方法,如: [代码]// 添加一条记录 let res = await this.OrderMd.add({ a:1, b:2 }) // 返回DbResultJson对象 {_id:'xxxx'} if(res instanceof DB.DbRes){ // 更新操作,更新刚新增的这条记录 let back = res.update({c: 555}) // 删除操作,删除刚新增的这条记录 back = res.remove() } // 添加多条记录 res = await this.OrderMd.add([{ a:1, b:2 }, { a:3, b:4 }]) // 返回DbResultArray对象 [{_id:'xxxx'},{_id:'xxxx'}] if(res instanceof DB.DbRes){ // 更新操作,更新刚新增的这几条记录 let back = res.update({c: 555}) // 删除操作,删除刚新增的这几条记录 back = res.remove() } [代码] remove结果 res = model.remove() 删除记录返回 { removed:Number } [代码]// 删除记录 let res = await this.OrderMd.remove({ a:1, b:2 }) // 返回DbResultJson对象 {removed:Number} if(res instanceof DB.DbRes){ if(res.removed){ // 删除成功 }else{ // 删除失败 } }else{ consloe.errr(res) } [代码] find/findAll/get结果 res = model.find() 返回 { _id: ‘xxx’, … } res = model.findAll/get() 返回 [{ _id: ‘xxx’, … }, { _id: ‘xxx’, … }, …] 返回的结果可以直接调用update,remove方法,如: [代码]// 添加一条记录 let res = await this.OrderMd.doc('xxx').find() // 返回DbResultJson对象 {_id:'xxxx'} if(res instanceof DB.DbRes){ // 更新操作,更新这条记录 // let back = res.update({c: 555}) // 删除操作,删除这条记录 let back = res.remove() } // 添加多条记录 res = await this.OrderMd.where({abc:123}).findAll() res = await this.OrderMd.where({abc:123}).get() // 返回DbResultArray对象 [{ _id: 'xxx', ... }, { _id: 'xxx', ... }, ...] if(res instanceof DB.DbRes){ // 更新操作,更新这几条记录 // let back = res.update({c: 555}) // 删除操作,删除这几条记录 let back = res.remove() } [代码] update结果 res = model.update() 更新数据返回 { updated: Number } [代码]// 更新记录 let res = await this.OrderMd.where({ a:1, b:2 }).update({c:555}) res = await this.OrderMd.doc('xxx').update({c:555}) // 返回DbResultJson对象 {update:Number} if(res instanceof DB.DbRes){ if(res.update){ // 更新成功 }else{ // 更新失败 } }else{ consloe.errr(res) } [代码] 查看SQL语句 以上述Order模型为例,在页面中查询记录 数据库操作结果返回 DB.DbRes 对象时,可查看执行的SQL语句 [代码]var res = await this.OrderMd.add({ a:1, b:2 }) console.log(res.sql) var res = await this.OrderMd.remove({ a:1, b:2 }) console.log(res.sql) var res = await this.OrderMd.doc('xxx').find() console.log(res.sql) var res = await this.OrderMd.where({ a:1, b:2 }).update({c:555}) console.log(res.sql) var res = await this.OrderMd.aggregate() .addFields({ test: 1 }) .match({ test: 1 }) .end() console.log(res.sql) [代码] ps: 可以拿去练练手,不保证无BUG
2020-07-18 - 如何使用scroll-view制作左右滚动导航条效果
最新:2020/06/13。修改为scroll-view与swiper联动效果,新增下拉刷新以及上拉加载效果。。具体效果查看代码片段,以下文章内容和就不改了 刚刚在社区里看到 有老哥在问如何做滚动的导航栏。这里简单给他写了个代码片段,需要的大哥拿去随便改改,先看效果图: [图片] 代码如下: wxml [代码]<scroll-view class="scroll-wrapper" scroll-x scroll-with-animation="true" scroll-into-view="item{{currentTab < 4 ? 0 : currentTab - 3}}" > <view class="navigate-item" id="item{{index}}" wx:for="{{taskList}}" wx:key="{{index}}" data-index="{{index}}" bindtap="handleClick"> <view class="names {{currentTab === index ? 'active' : ''}}">{{item.name}}</view> <view class="currtline {{currentTab === index ? 'active' : ''}}"></view> </view> </scroll-view> [代码] wxss [代码].scroll-wrapper { white-space: nowrap; -webkit-overflow-scrolling: touch; background: #FFF; height: 90rpx; padding: 0 32rpx; box-sizing: border-box; } ::-webkit-scrollbar { width: 0; height: 0; color: transparent; } .navigate-item { display: inline-block; text-align: center; height: 90rpx; line-height: 90rpx; margin: 0 16rpx; } .names { font-size: 28rpx; color: #3c3c3c; } .names.active { color: #00cc88; font-weight: bold; font-size: 34rpx; } .currtline { margin: -8rpx auto 0 auto; width: 100rpx; height: 8rpx; border-radius: 4rpx; } .currtline.active { background: #47CD88; transition: all .3s; } [代码] JS [代码]const app = getApp() Page({ data: { currentTab: 0, taskList: [{ name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, ] }, onLoad() { }, handleClick(e) { let currentTab = e.currentTarget.dataset.index this.setData({ currentTab }) }, }) [代码] 最后奉上代码片段: https://developers.weixin.qq.com/s/nkyp64mN7fim
2020-06-13 - 手把手带你使用云开发实现微信小程序支付功能(完整视频讲解)
首先看下效果图 [图片] 下面把完整的讲解视频,免费发给大家。 1~注册企业小程序 [视频] 2~微信支付商户号注册及注意事项 [视频] 3~小程序关联微信支付的商户号 [视频] 4~创建云开发项目 [视频] 5~云开发控制台配置微信支付商户号 [视频] 6~编写支付的云函数 [视频] 7~云开发实现微信支付 [视频] 8~动态修改商品名和价格 [视频] 9~编写商品页面 [视频] 10~单个商品的支付 [视频] 11~商品列表实现购买支付 [视频] 如果你是B站用户,也可以去石头哥B站观看完整的视频讲解: https://www.bilibili.com/video/BV1Lp4y1D7oY/
2020-06-11 - 动手打造更强更好用的微信开发者工具-编辑器扩展篇
1. 写在前面 1.1 微信开发者工具现状 具备一些基本的通用IDE功能,但是第三方的支持扩展需要加强。 1.2 开发者工具自带的编辑器扩展功能 可能很多老铁没用过官方的微信开发者工具的编辑器扩展(我一般称为编辑器插件)。官方把这块功能也隐藏得很深,也没有相关文档介绍,但是预留了相关的入口。合理利用第三方编辑器插件,可以极大的提升开发效率。下面先来看看官方预留的编辑器插件入口: [图片] (图一) 2. 几个不错插件安装效果 2.1 标签高亮插件-vincaslt.highlight-matching-tag [图片] 功能:可以把当前行对应的标签开头和结尾高亮起来,让开发者一目了然 2.2 小程序开发助手插件-overtrue.miniapp-helper [图片] 功能:必须要说的这个是纯国产的插件,里面的代码片段功能很全,具体介绍:小程序开发助手 - Visual Studio Marketplace https://marketplace.visualstudio.com/items?itemName=overtrue.miniapp-helper 2.3 minapp插件-qiu8310.minapp-vscode [图片] 功能:这个是今天的明星插件,里面的跳转功能很强,可以在wxml里CMD+点击对应变量/方法和CSS样式名称直接跳转到对应的js/wxss文件对应的地方。具体的下面是官方介绍: 标签名与属性自动补全 根据组件已有的属性,自动筛选出对应支持的属性集合 属性值自动补全 点击模板文件中的函数或属性跳转到 js/ts 定义的地方(纯 wxml 或 pug 文件才支持,vue 文件不完全支持) 样式名自动补全(纯 wxml 或 pug 文件才支持,vue 文件不完全支持) 在 vue 模板文件中也能自动补全,同时支持 pug 语言 支持 link(纯 wxml 或 pug 文件才支持,vue 文件不支持) 自定义组件自动补全(纯 wxml 文件才支持,vue 或 pug 文件不支持) 模板文件中 js 变量高亮(纯 wxml 或 pug 文件才支持,vue 文件不支持) 内置 snippets 支持 emmet 写法 wxml 格式化 3. DIY添加适合自己的插件 3.1 添加插件功能简介 仔细研究过微信开发者工具的人可能知道或者了解,其实微信开发者工具编辑器跟微软的开源编辑器vsCode「颇有渊源」。再深入研究发现,vsCode的插件完全可以无缝移植到微信开发者工具编辑器里来,所以今天的内容就是移植vsCode的插件到微信开发者工具。咱们先看看微信开发者工具自带的「管理编辑器扩展」功能(图1标注为2的地方) [图片](图二) 3.2 插件添加具体步骤 3.2.1 安装插件,获取插件文件 安装vsCode并安装你需要移植的插件,必须要说的是vsCode的插件非常多,好的插件也很多。相关安装,搜索插件教程建议大家百度相关教程。或者直接下载vsCode亲自体验,插件安装过程还是非常简单的。 3.2.2 复制插件文件夹 找到vsCode相关插件的安装文件夹: 操作系统 安装路径 windows %USERPROFILE%.vscode\extensions macOS ~/.vscode/extensions Linux ~/.vscode/extensions 复制对应插件文件夹到微信开发者工具的「打开编辑器扩展目录」(图一标注为1的地方) 3.2.3 添加插件配置文件 新版开发者工具直接进入图形设置,扩展设置里勾选对应插件即可。如下图: [图片] 旧版操作方法:进入微信开发者工具的「管理编辑器扩展」功能页面,在尾端加入对应添加的插件名称。以以上3个介绍的插件为例,在原来的尾端加入: “vincaslt.highlight-matching-tag”, “overtrue.miniapp-helper”, “qiu8310.minapp-vscode” 3.2.4 见证奇迹 重启微信开发者工具,见证插件带来的编码便利吧! 4 需要注意的 vsCode的插件很多,小程序相关的也越来越多了,但是插件质量参差不齐,所以安装时建议选择「标星」star比较多的插件。
2020-05-02 - 在线答题小程序数据库设计
背景介绍 最近有不少朋友咨询我在线答题小程序的数据库集合设计,现专门写文整理下,该小程序目前包含以下十个集合 数据库设计 admin 该集合主要用于指定管理员openid,对于部分openid开放创建试卷以及数据报表导入、导出等功能 category 该集合主要用于设置题目分类,也就是所谓的题库信息,比如语文、数学、英语 depts 该集合主要面向企业用户,维护企业部门信息词典 favor 该集合用于设置题目收藏记录信息 history 该集合主要用于用于记录考试时间、考试得分,考生信息 mediatype 该集合作为素材字典定义,比如文本、图片 notes 该集合主要用于记录错题记录 profiles 该集合主要用于记录用户信息,比如openid、昵称、头像以及个人相关信息 question 该集合主要用于题目信息,题目的结构本文不做介绍 questype 该集合用于维护题目类型字典,比如单选、多选、判断、填空、简答 [图片] 该小程序题目组织为一级结构,也就是所谓的一级分类,类别下面直接是题目信息,不存在二级分类 后续计划 后面会写两篇文章,第一篇文章介绍二级分类的在线答题小程序,第二篇文章为会员邀请制在线答题小程序的数据库设计 在线答题小程序邀请码设计? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/0004ec55980978dce7f99896153413 备注 后面我有时间慢慢维护,这个文章 扫码体验 [图片]
2020-05-28 - 史上最详细一步一步介绍微信小程序微信支付功能开发
前言微信小程序开发微信支付, 相对于微信的其他功能,实话说相比之下好太多,可能是开发文档是微信支付这边撰写的缘故吧?猜的。所以微信支付在小程序中,虽然参数十分的多,环节特别的细致,但也算不上无从下手。上个项目实施了一次微信小程序支付功能的开发,趁记忆力尚可,赶紧记录一番本次环境平台为 小程序端(uniapp)| 原生一样,只介绍js部分 + egg.js端(node)流程基本都一致,只是语法上有许区别开发准备工欲善其事,必先利其器。开发之前我们需要先准备好哪些必须的开发前提或环境呢?资料:1. 审核并开通微信商户号: •微信支付页链接地址[1][图片] •等待审核, 审核通过后对小程序进行关联[图片] •小程序开启支付[图片] •小程序支付开通后[图片] •打开微信支付页,可以进行绑定查看[图片] •再到微信支付中去记录mch_id,和merchantkey(商户密码),记录merchantkey的方式如图:[图片][图片] •记录下merchantkey ,注意:merchantkey是敏感key,不可泄漏 2. 前面开发好拉取用户的授权,获取用户在当下小程序中的openid(重点必须) 3. 搭建好服务器接口调用, 记录下需要传递给微信服务器使用回调的服务器ip地址以及接口的url地址(提前准备好,可以使用postman做好测试)。(可以为本服务器也可以为另外服务器,主要作为回调) 4. 其他:微信官方文档要求 审核支付功能需要微信小程序已上线,但是当时我申请的时候小程序并未上线也过了,所以这一块我无法做出解释。另外,程序访问商户服务都是通过HTTPS,开发部署的时候需要安装HTTPS服务器 开发流程5. 先来看看官方微信支付给出的流程图[图片] •感觉有点懵逼 •我总结如下:[图片] 开始开发1.根据以上流程图,我们开始进行调用 •第一步 小程序发起支付的代码如下: async wxappay (openid, money) { return new Promise(async (resolve, reject) => { let Objct = { openid, //拉取授权获取到的openid money, //money必须是整数类型, 以RMB分为单位! body: 'xxx' } let temp = await wxappWxPay(obj) //进入到第一阶段, 预支付阶段 //后面的逻辑为第二阶段 }) } 注意,强烈推荐使用promise函数来实现,可以保证逻辑代码体在实现流程的一致性•第一步 后端node服务器接口获取支付第一部参数的代码如下: /** * 微信统一下单(微信支付)的接口数据(!!!!小程序专用付款方式) * @param {OBject} * 调用微信预支付接口(必填项) * @@排列顺序不可以错! * 1.appid * 2.body: 商品描述 * 3.mch_id: 支付申请配置的商户号 * 4.NonceStr: 随机字符串 * 5.notify_url: 微信付款后的回调地址 //后端egg的接口接收此地址来响应支付成功的回调 * 6.openid: * 7.out_trade_no: 订单号(32位) * 8.spbill_create_ip:后端调用API支付微信的ip地址 (支持32位和64位IP地址) * 9.total_fee: 支付金额 * 10. * /** * //生成微信支付的参数进行ASCII码从小到大排序 * @params: * body: 支付内容 * totalmoney: 支付金额 */ async getPrePayId(obj) { const { config, ctx } = this const { appid, merchantid, merchantkey } = config.wxapp //后台预先设置的appid,merchantid, merchantkey const { ip, notify_url } = config.payaddress const NonceStr = Math.random().toString(36).substr(2, 15) const orderid = uuid.v4().replace(/-/g, '') const { body, totalmoney, openid } = obj //预发起支付第一次签名 const uniorderParams = { appid, body, mch_id: merchantid, nonce_str: NonceStr, notify_url, openid, out_trade_no: orderid, spbill_create_ip: ip, total_fee: totalmoney, trade_type: 'JSAPI' } uniorderParams.sign = ctx.helper.getPreSign(uniorderParams, merchantkey) //根据上面的这个uniorderParams统一下单参数根据ASCii码从小到大排序,加上商户密钥做sign加密 let xml = ' ' + //重点, 必须使用xml格式来发送给微信服务器端 '' + uniorderParams.appid + ' ' + '' + uniorderParams.body + ' ' + '' + uniorderParams.mch_id + ' ' + '' + uniorderParams.nonce_str + ' ' + '' + uniorderParams.notify_url + '' + '' + uniorderParams.openid + ' ' + '' + uniorderParams.out_trade_no + '' + '' + uniorderParams.spbill_create_ip + ' ' + '' + uniorderParams.total_fee + ' ' + '' + uniorderParams.trade_type + ' ' + '' + uniorderParams.sign + ' ' + ''; const temp = await ctx.curl('https://api.mch.weixin.qq.com/pay/unifiedorder', { //统一下单的地址 method: 'POST', data: xml }) let result = {} if (temp.status == 200) { result = await ctx.helper.xmlToJson(temp.data.toString()) } /** * 获取预支付的sign签名 * 带字符串QUERY的URL的&拼接 */ getPreSign: (signParams, merchantkey) => { let keys = Object.keys(signParams).sort() let newArgs = {} keys.forEach( val => { if(signParams[val]) { newArgs[val] = signParams[val] } }) const string = queryString.stringify(newArgs) + '&key=' + merchantkey return crypto.createHash('md5').update(queryString.unescape(string), 'utf8').digest("hex").toUpperCase() } //二次签名... 1. 第一步中,如果 参数没问题,发送给微信服务器中会响应到一个prepay_id,而这个 prepay_id 就是预支付的code 2. 第二步,node服务器向微信服务器发起第二次签名,小程序端无感知 //二次签名 const paysign2 = { appId: result.appid, nonceStr: result.nonce_str, package: `prepay_id=${result.prepay_id}`, timeStamp: parseInt((Date.now() / 1000)).toString(), //注意:时间必须为秒 signType: 'MD5' } paysign2.paySign = ctx.helper.getPreSign(paysign2, merchantkey) const data = { paysign2, orderid } await ctx.model.Ordertable.create({ orderid, openid, money: totalmoney * 100, status: 0 }) //在这里我做了一个支付预处理落地到数据库的操作,当预支付 return data } 在这里我做了一个支付预处理落地到数据库的操作,当预支付通过,支付数据库插入一条status为0的待确认支付状态的数据1.第二步,第二次签名中的回调数据,一起通过接口返回给小程序端 async wxappay (openid, money) { return new Promise(async (resolve, reject) => { let Objct = { openid, //拉取授权获取到的openid money, //money必须是整数类型, 以RMB分为单位! body: 'xxx' } let temp = await wxappWxPay(obj) //进入到第一阶段, 预支付阶段 //前面的逻辑为第一阶段 //下面的逻辑为第二阶段 //第二次签名 let einfo = temp.data //小程序使用wx.requestPayment拉去支付逻辑 wx.requestPayment({ timeStamp: parseInt(einfo.paysign2.timeStamp).toString(), nonceStr: einfo.paysign2.nonceStr, package: einfo.paysign2.package, signType: 'MD5', paySign: einfo.paysign2.paySign, success: async res => { if (res) { let checkdata = { nonceStr: einfo.paysign2.nonceStr, out_trade_no: einfo.orderid, sign: einfo.paysign2.paySign } //下面是第四步,省略... } }, fail: res => { console.log('@', res) reject(res) } }) } }) } 注意, wx.requestPayment的回调success , 并不一定获取到正确结果,严谨的说。由于发起支付后,后端(node) 发送成功支付后,微信服务器会要求后端进行支付成功数据回调的响应。微信支付的官方说明如下:1.第三步 回调 notify_url填写注意事项•notify_url需要填写商户自己系统的真实地址,不能填写接口文档或demo上的示例地址。•notify_url必须是以https://或http://开头的完整全路径地址,并且确保url中的域名和IP是外网可以访问的,不能填写localhost、127.0.0.1、192.168.x.x等本地或内网IP。•notify_url不能携带参数。[图片]1.node服务器中回调地址代码如图: /** * 确认支付之后的订单 * 回调(微信)再次签名(响应支付成功的结果) * @param {Object} * 1.必须给微信一个响应。支fu的结果, * */ async wxPayNotify(xmldata) { const { ctx } = this let result = await ctx.helper.xmlToJson(xmldata) if (result) { let resxml = ' ' + '' + '' + '' + '' + '' + '' + ' ' if (result.result_code == 'SUCCESS') { await ctx.model.Ordertable.update({ status: 1, transactionid: result.transaction_id, money: result.total_fee }, { where: { orderid: result.out_trade_no, openid: result.openid } }) } return resxml } } > 在这里我上面落地存储数据的支付表,在收到微信的支付成功的回调后,将状态status :0 改为1 表示支付明确 6. 第四步 主动查询 > 由于微信的回调是异步,前端不可能等待微信的回调再来进行下一步逻辑处理,万一网络波动或者其他因素导致微信服务器的回调迟迟没有到我们的数据库中来呢?所以我们需要自己主动发起查询支付结果的API > 此API为:[微信查询支付接口](https://api.mch.weixin.qq.com/pay/orderquery) > 小程序端发起查询请求给后端,后端再向微信服务器调取查询结果: > node服务器的代码如下:(本次逻辑十分重要,关系我们支付的闭环) /** * 微信支付主动调取查询订单状态API */ async checkWxPayResult(obj) { const { ctx, config } = this const { appid, merchantid, merchantkey } = config.wxapp const { nonceStr, out_trade_no } = obj let Params = { appid, mch_id: merchantid, nonce_str: nonceStr, out_trade_no: out_trade_no } Params.sign = ctx.helper.getPreSign(Params, merchantkey) let xml = ''; const temp = await ctx.curl('https://api.mch.weixin.qq.com/pay/orderquery', { method: 'POST', data: xml }) let result = {} if (temp.status == 200) { result = await ctx.helper.xmlToJson(temp.data.toString()) return result } } 7. 将响应结果发送给小程序端 + 小程序端支付的完整逻辑就呈现出来了,代码如下: async wxappay (openid, money) { return new Promise(async (resolve, reject) => { let Objct = { openid, //拉取授权获取到的openid money, //money必须是整数类型, 以RMB分为单位! body: 'xxx' } let temp = await wxappWxPay(obj) //进入到第一阶段, 预支付阶段 //前面的逻辑为第一阶段 //下面的逻辑为第二阶段 //第二次签名 let einfo = temp.data //小程序使用wx.requestPayment拉去支付逻辑 wx.requestPayment({ timeStamp: parseInt(einfo.paysign2.timeStamp).toString(), nonceStr: einfo.paysign2.nonceStr, package: einfo.paysign2.package, signType: 'MD5', paySign: einfo.paysign2.paySign, success: async res => { if (res) { //虽然是res调取成功,但是我们并不需要这个参数的逻辑回调 let checkdata = { nonceStr: einfo.paysign2.nonceStr, out_trade_no: einfo.orderid, sign: einfo.paysign2.paySign } //下面是第四步,主动查询订单的支付情况 const result = await getWxappPayResult(checkdata) if (result.code == 200) { resolve(result.data) //最后,作为promise进行返回,此刻的支付是100%正确的。还可以参照落地的数据库表进行辅助对照 } } }, fail: res => { console.log('@', res) reject(res) } }) } }) } ``` 至此,小程序的支付就完成了。后续,此文仅仅是使用的node 作为后端, 实际上流程来说,JAVA,PHP等等语言来说,逻辑思路都基本一致的。[图片] 原创不易,喜欢的朋友麻烦点点关注!后续会写java版本的支付流程 ,敬请关注References[代码][1][代码] 微信支付页链接地址: https://pay.weixin.qq.com/
2020-04-29 - 纯云开发 使用一个小程序访问另一个小程序的云资源
由于工作需要,我需要使用一个小程序与另一个小程序共同享用同一套云资源。这就需要用到'tcb-admin-node'这个sdk来帮我实现这个功能。 这个sdk有详细的教程如下:https://github.com/TencentCloudBase/tcb-admin-node 作为一个新手,刚看这个文档感觉有些懵逼,不过在群友的帮助下,还是慢慢地实现了一小步的功能,就是小程序访问另一个小程序的云函数。 废话不多说,我的使用步骤如下: 1,你要有一个已经有在使用自己开通的云资源的小程序,称为小程序A;还要有一个空的小程序,称为小程序B。 2,为小程序B开通云开发。 3,小程序B创建云函数的方法我就不多说了。按照文档来说,你是需要每建一个云函数就安装一次tcb-admin-node的,但是最新版本的wx-server-sdk貌似已经集成了tcb-admin-node,所以你可以选择安装或者不安装。 4 ,不多说,代码如下图: [图片] 其中secretId和secretKey都是必须的,均为小程序A的secretId和secretKey,获取方式文档中有链接,即从腾讯云中获取你的api密匙。如下图:env为小程序A使用的环境ID [图片] 取一对就可以了,还有必须从你的小程序A进入。 name为你小程序A使用过的云函数,data为参数,与云函数所需参数一致。 5,这就封装完成了一个云函数。别忘记上传。,这时候在前台,就像普通云函数一样调用这个云函数就可以了。我的代码如下: [图片] 访问结果如下: [图片] 这时候小程序B就成功地访问了小程序A。 当然,这只是我实践的结果,成功了,于是把方法分享给大家。你们成功不成功,就看你们自己的实践了。 由于第一次发帖,可能写的有不好的地方,希望大家多多包涵,若有不妥可以纠正一下,谢谢大家。
2019-11-15 - 使用腾讯云服务,实现安全稳定的高并发架构
随着云的发展,各大公司提供更加安全稳定的云服务,越来越多的企业也选择将自己的业务上云,使用云服务带来的好处本文不再赘述了,直接进入主题,使用腾讯云服务,快速构建一个基本的架构,实现低成本、安全稳定的部署服务。在这里希望可以和大家交流,提供更完善的方案。 涉及到的云服务主要包括:服务器,对象存储,缓存,数据库,消息队列,负载均衡,弹性伸缩,Web应用防火墙;项目开发管理:TAPD敏捷项目管理,腾讯Git代码托管。不分先后,每个产品都很好用。 所有产品都提供线上管理,各种报表、图表,实时数据统计可以分析系统运行现状,完善升级系统。 [图片] 服务器 服务器主要用来部署运行代码。根据需求,购买不同规格的服务器,支持多种操作系统,CPU、内存、带宽等资源可以随时调整,适应企业业务发展对服务器需求的调整,并且支持不同的计费方式,可以帮助节约成本。可以根据已经配置好的服务器环境,制作镜像,购买新的服务器时可以选择已有镜像,服务器环境无需重新配置,并且在实现弹性伸缩时必须要用到自定义镜像。 对象存储 主要用来存储资源文件,包括图片、视频、普通文件,资源文件可以通过外网访问。特点是:使用简单,提供多种语言SDK,集成简单,安全稳定,按照流量收费,无需关心并发、带宽、丢失等问题。 缓存 腾讯云提供多种缓存,redis、memcache都有,最大连接数支持都是万为单位,保证可以满足企业的需求,可以包年包月,也可以按照使用量付费,更重要的是安全稳定,热备容灾保证业务不中断。 数据库 关系型数据库,文档型数据库等都支持,单台数据库轻松搞定万级以上qps,如mysql,支持主从、读写,数据同步、容灾、恢复、数据迁移,简单几步操作就可以完成,保证数据安全。 消息队列 腾讯云消息队列提供了两种消息模型:消息队列模型和消息主题模型。产品优势:高性能,单实例轻松到5000qps,高可靠性,保证所有消息不丢失,每个消息被正确使用,提供多语言SDK,文档完善,接入方便简单。 负载均衡 负载均衡提供安全快捷的流量分发服务,访问流量经由负载均衡可以自动分配到云中的多台云服务器上,扩展系统的服务能力并消除单点故障,购买后,配置访问到负载均衡即可。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。 配合弹性伸缩,可以实现服务器根据使用率实时增加减少服务器数量,实现业务负载。 弹性伸缩 可周期性地执行管理策略或创建实时监控策略,来管理服务器实例数量,并完成对实例的环境部署,保证业务平稳顺利运行。在需求高峰时,弹性伸缩自动增加服务器数量,以保证性能不受影响;当需求较低时,则会减少服务器数量以降低成本。根据已经配置好的服务器,制作镜像,设置伸缩策略,伸缩的新服务器使用已有镜像,新服务器启动后可立即提供服务,配合负载均衡,实现并发流量分发。 WEB应用防火墙 帮助用户应对 Web 攻击、入侵、漏洞利用、挂马、篡改、后门、爬虫、域名劫持等网站及 Web 业务安全防护问题。企业组织通过部署腾讯云网站管家服务,将 Web 攻击威胁压力转移到腾讯云网站管家防护集群节点,分钟级获取腾讯 Web 业务防护能力,为组织网站及 Web 业务安全运营保驾护航。 TAPD敏捷项目管理 TAPD(Tencent Agile Product Development)是源自于腾讯的敏捷研发协作平台,提供贯穿敏捷研发生命周期的一站式服务。覆盖从产品概念形成、产品规划、需求分析、项目规划和跟踪、质量测试到构建发布、用户反馈跟踪的产品研发全生命周期,提供了灵活的可定制化应用和强大的集成能力,帮助研发团队有效地管理需求、资源、进度和质量,规范和改进产品研发过程,提高研发效率和产品质量,可以配合工蜂使用,让代码与任务关联。并且在微信端可以实现任务推送,实时关注任务进度,好用免费!!! 腾讯Git代码托管工蜂 为开发者提供基于 Git 的在线代码托管工具,包含代码提交/存储/下载/复刻/分支/历史/比对/合并等功能。可一站式完成对代码及代码质量管理,项目及项目人员管理,大大提升研发效率,配合TAPD使用,让每个commit都有一个任务,免费没限制!!! 使用以上产品,可以帮助企业快速建立安全稳定的后端架构,并且所有服务是由专业团队维护,有任何问题都有官方专业团队解答,关键是可以提供N多个9的可用性。同时,企业也可以减少运营成本,将更多精力放在研发产品上,专注自己的核心业务,实现更好发展。
2019-05-20 - 论函数复用的几大姿势
开发过小程序的朋友们应该都遇到这样的情况,可能很多个页面有相同的函数,例如[代码]onShareAppMessage[代码],有什么最佳实践吗,应该如何处理呢? 本次开发技巧,我从以下几种解决办法剖析: 将它复制粘贴到每个地方(最烂的做法) 抽象成一个公共函数,每个[代码]Page[代码]都手动引用 提取一个behavior,每个页面手动注入 通过[代码]Page[代码]封装一个新的[代码]newPage[代码],以后每个页面都通过[代码]newPage[代码]注册 劫持Page函数,注入预设方法,页面仍可使用[代码]Page[代码]注册 复制粘贴大法 这是最直观,也是初学者最常用到的办法。也是作为工程师最不应该采取的办法。这有一个致命的问题,如果某一天,需要改动这个函数,岂不是要将所有的地方都翻出来改,所以这个办法直接否决。 抽象公共函数 这种方式,解决了复制粘贴大法的致命问题,不需要改动很多地方,只需要改动这个抽象出来的函数即可。但是其实,这个方式不便捷,每次新增页面都需要手动引入这个函数。 以下都通过[代码]onShareAppMessage[代码]方法举例。 假设在[代码]app.js[代码]通过[代码]global[代码]注册了[代码]onShareAppMessage[代码]方法: [代码]// app.js global.onShareAppMessage = function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } } [代码] 那么此时每次新增的Page都需要这样引入: [代码]// page.js Page({ ...global.onShareAppMessage, data: {} }) [代码] 这样的缺点也是非常明显的: 创建新页面时,容易遗忘 如果多个相同的函数,则需要每个独立引入,不方便 提取Behavior 将多个函数集成到一个对象中,每个页面只需要引入这个对象即可注入多个相同的函数。这种方式可以解决 抽象公共函数 提到的 缺点2。 大致的实现方式如下: 同样在[代码]app.js[代码]通过[代码]global[代码]注册一个[代码]behavior[代码]对象: [代码]// app.js global.commonPage = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onHide: function() { // do something } } [代码] 在新增的页面注入: [代码]// page.js Page({ data: {}, ...global.commonPage, }}) [代码] 缺点仍然是,新增页面时容易遗忘 封装新Page 封装新的[代码]Page[代码],然后每个页面都通过这个新的[代码]Page[代码]注册,而不是采用原有的[代码]Page[代码]。 同理,在[代码]app.js[代码]先封装一个新的[代码]Page[代码]到全局变量[代码]global[代码]: [代码]// app.js global.newPage = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return Page({...defaultSet, ...obj}) } [代码] 往后在每个页面都使用新的[代码]newPage[代码]注册: [代码]// page.js global.newPage({ data: {} }) [代码] 好处即是全新封装了[代码]Page[代码],后续只需关注是否使用了新的[代码]Page[代码]即可;此外大家也很清晰知道这个是采用了新的封装,避免了覆盖原有的[代码]Page[代码]方法。 我倒是觉得没什么明显缺点,要是非要鸡蛋里挑骨头的话,就是要显式调用新的函数注册页面。 劫持Page 劫持函数其实是挺危险的做法,因为开发人员可能会在定位问题时,忽略了这个被劫持的地方。 劫持[代码]Page[代码]的做法,简单的说就是,覆盖[代码]Page[代码]这个函数,重新实现[代码]Page[代码],但这个新的[代码]Page[代码]内部仍会调用原有的[代码]Page[代码]。说起来可能有点拗口,通过代码看就一目了然: [代码]// app.js let originalPage = Page Page = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return originalPage({ ...defaultSet, ...obj}) } [代码] 通过这种方式,不改变页面的注册方式,但可能会让不了解底层封装的开发者感到困惑:明明没注册的方法,怎么就自动注入了呢? 这种方式的缺点已经说了,优点也很明显,不改变任何原有的页面注册方式。 其实这个是一个挺好的思路,在一些特定的场景下,会有事半功倍的效果。
2020-03-23 - 实现通讯录字母索引——生成与中文字符串相对映的拼音首字母串
代码片段:https://developers.weixin.qq.com/s/jNaCECmd7Wfb [图片][图片] 代码片段:https://developers.weixin.qq.com/s/jNaCECmd7Wfb 汉字拼音首字母列表 本列表包含了20902个汉字,用于配合 ToChineseSpell 函数使用,本表收录的字符的Unicode编码范围为19968至40869, XDesigner 整理
2020-03-05 - 小程序中使用css var变量,使js可以动态设置css样式属性
使用sass,stylus可以很方便的使用变量来做样式设计,其实css也同样可以定义变量,在小程序中由于原生不支持动态css语法,so,可以使用css变量来使用开发工作变简单。 基本用法 基础用法 [代码]<!--web开发中顶层变量的key名是:root,小程序使用page--> page { --main-bg-color: brown; } .one { color: white; background-color: var(--main-bg-color); margin: 10px; } .two { color: white; background-color: black; margin: 10px; } .three { color: white; background-color: var(--main-bg-color); } [代码] 提升用法 [代码]<div class="one"> <div class="two"> <div class="three"> </div> <div class="four"> </div> <div> </div> [代码] [代码].two { --test: 10px; } .three { --test: 2em; } [代码] 在这个例子中,[代码]var(--test)[代码]的结果是: class=“two” 对应的节点: 10px class=“three” 对应的节点: element: 2em class=“four” 对应的节点: 10px (继承自父级.two) class=“one” 对应的节点: 无效值, 即此属性值为未被自定义css变量覆盖的默认值 上述是一些基本概念,大致说明css变量的使用方法,注意在web开发中,我们使用[代码]:root[代码]来设置顶层变量,更多详细说明参考MDN的 文档 妙用css变量 开发中经常遇到的问题是,css的数据是写死的,不能够和js变量直通,即有些数据使用动态变化的,但css用不了。对了,可以使用css变量试试呀 wxml js [代码]// 在js中设置css变量 let myStyle = ` --bg-color:red; --border-radius:50%; --wid:200px; --hgt:200px; ` let chageStyle = ` --bg-color:red; --border-radius:50%; --wid:300px; --hgt:300px; ` Page({ data: { viewData: { style: myStyle } }, onLoad(){ setTimeout(() => { this.setData({'viewData.style': chageStyle}) }, 2000); } }) [代码] wxml [代码]<!--将css变量(js中设置的那些)赋值给style--> <view class="container"> <view class="my-view" style="{{viewData.style}}"> <image src="/images/abc.png" mode="widthFix"/> </view> </view> [代码] wxss [代码]/* 使用var */ .my-view{ width: var(--wid); height: var(--hgt); border-radius: var(--border-radius); padding: 10px; box-sizing: border-box; background-color: var(--bg-color); transition: all 0.3s ease-in; } .my-view image{ width: 100%; height: 100%; border-radius: var(--border-radius); } [代码] 通过css变量就可以动态设置css的属性值 代码片段 https://developers.weixin.qq.com/s/aWfUGCmG7Efe github 小程序演示 [图片]
2020-03-05 - 云开发,从0到1实现小程序内微信支付功能
首先很感谢云开发提供的生态以及示例代码,安耐不住内心的激动,在这一刻实现了小程序内微信支付,在3年前做过基于公众号的微信支付以及企业红包当时玩的很溜,动不动就给群里的小伙伴定向推送红包 本次微信支付参考以下官方开源项目 https://github.com/TencentCloudBase/mp-book https://github.com/TencentCloudBase/tcb-demo-basic 具体交互截图如下所示: [图片] [图片] [图片] [图片] 我总结以下几点把,以下3点不全,但是对于一个有云开发经验的同学,这足够了。 1、在微信企业支付后台 进行相应的设置 [图片] 2、在上面源代码的基础上,配置,appi d,商户 号,安全 密钥以及api证书 module.exports = { ENV: 'xxx', // TCB环境ID MCHID: 'xxx',//商户id KEY: '0123456789abcdefghijklmnopqrstuv', CERT_FILE_CONTENT: fs.existsSync(CERT_PATH) ? fs.readFileSync(CERT_PATH) : null, TIMEOUT: 10000 // 毫秒 }; 3、云开发数据库新增goods、orders两个集合,并赋予所有可读写权限,这一点很重要。 开发过程中遇到的问题: 1、云函数执行失败 这是由于在之前没有在数据库里面创建goods集合和orders集合 2、签名错误 这是在正确配置之后还报这个错误,这个时候不要慌,首先核对自己的配置有没有问题,在确保配置没问题的前提下,相信自己,重新运行下就好了。 [图片] 备注:想了解更多关于微信支付的逻辑流转,请别走开,继续阅读,下面是腾讯云课堂,有视频有文档,不容错过。 附几个微信支付实现的文档 https://cloud.tencent.com/developer/team/tcb/courses https://cloud.tencent.com/edu/learning/course-1276-4318 https://cloud.tencent.com/edu/learning/learn-1276-3815
2020-03-02 - 微信小程序直播资料整理
可以通过此脑图大概了解小程序直播插件发展过程:http://naotu.baidu.com/file/597625fbd8659aa87e54143df1ed7f39?token=5bad276062c05585 [图片] 小程序直播组件是微信给开发者提供的实时视频直播工具,可以帮助开发者快速通过小程序向用户提供优质的直播内容,在小程序内流畅完成购买交易闭环,提升转化率; 小程序直播组件包括观众端、主播端及后台管理端,其中观众端提供拉流、实时互动、订阅提醒、商品购买等能力,主播端提供开播、推流、音视频效果优化等能力,后台管理端则负责直播房间、商品货架以及营销活动配置等。 【开通条件】 https://mp.weixin.qq.com/s/oqNdNEnRblEzwR_61d3vHA 满足以下条件的电商平台、自营商家,即有机会被邀请到小程序直播公测中来: (同时满足以下1、2、3条件,加上4、5、6条件的其中之一即可。) 1. 满足小程序18个开放类目(包括:电商平台、商家自营-百货、食品、初级食用农产品、酒/盐、图书报刊/音像/影视/游戏/动漫、汽车/其他交通工具的配件、服装/鞋/箱包、玩具/母婴用品(不含食品)、家电/数码/手机、美妆/洗护、珠宝/饰品/眼镜/钟表、运动/户外/乐器、鲜花/园艺/工艺品、家居/家饰/家纺、汽车内饰/外饰、办公/文具、机械/电子器件) 2. 主体下小程序近半年没有严重违规 3. 小程序近90天存在支付行为 4. 主体下公众号累计粉丝数大于100 5. 主体下小程序连续7日日活跃用户数大于100 6. 主体在微信生态内近一年广告投放实际消耗金额大于1w 【第三方平台(小程序服务商)】 https://developers.weixin.qq.com/community/develop/doc/000a06014745f00d95f9e03d951401?from=groupmessage&isappinstalled=0 服务商接入指引 具体接入指引请参考《【小程序直播】服务商接入指引》,以下为服务商接入步骤。 1. 权限申请 1) 在问卷《服务商“小程序直播”接入申请》填写相关信息并等待权限开通,发送申请后7个工作日内,可登陆微信开放平台查看第三方平台权限集并勾选 “小程序直播” 能力; 2) 开通后,即可登陆 “微信开放平台” (open.weixin.qq.com)勾选 “小程序直播” 第三方权限集并全网发布; 2.功能开发 小程序直播需要实现【直播组件】与【后台API】两个部分,其中组件部分需要在小程序中进行配置开发。 具体开发文档,请参考《小程序直播组件接入指引》。 【看点直播与小程序直播区别】 看点直播是不是腾讯官方?有什么区别? 小程序直播是腾讯WXG(也就是微信团队)公测推出的能力,为商家提供的经营工具,可在商家自有的小程序中实现直播互动与商品销售闭环。优点是可以直接内嵌到商家小程序,和公众号打通;直播吸引的流量都沉淀在商家自有小程序,不用跳转其他渠道,有利于形成私域流量,转化率高。 腾讯看点直播是腾讯PCG(平台与内容事业群,做QQ、腾讯视频的事业群)推出的直播产品,有App、小程序端、小程序端是基于微信小程序的开发能力开发的,商家必须通服务商才能接入,无法直接在商家自有的小程序内闭环交易。(需要直播服务商提交资料开通,服务费用599半年。) 【2020-05-22更新】 近期已上线新功能:评论管理,支持对单个用户进行禁言,与敏感词设置。 未全面开放新功能:直播间可以直接发放微信代金券,目前内测中,敬请期待! [图片] 【2020-06-02更新】 小程序后台直播功能接口支持进一步提升,可以通过接口添加直播间与商品管理,在没有涉及抽奖情况下及推送商品功能情况下,已经可以完整脱离小程序后台完成直播相关设置,对于一些服务商来说是一个不错的消息,按这个进度,下一步官方应该优先支持推送商品及抽奖活动设置配置接口的开放。 创建直播间文档:https://developers.weixin.qq.com/miniprogram/dev/framework/liveplayer/live-player-plugin.html 拉到三/9 商品接口文档:https://developers.weixin.qq.com/miniprogram/dev/framework/liveplayer/commodity-api.html [图片] 【2020-06-09更新】 直播间优惠券功能已经全面开放,很多玩法将会产生。 直播间领取的券可直接插入微信卡包(代表着可以提醒一次)。 如何配置:http://note.youdao.com/noteshare?id=afbff99580c4540cc011e3ed7ab5fbcf&sub=CDE480AAEC07437CB58902E0FF42C329 [图片][图片]
2020-06-09 - 小程序websocket心跳库——websocket-heartbeat-miniprogram
前言在16年的时候因为项目接触到websocket,而后对心跳重连做了一次总结,写了篇博客,而后18年对之前github上的demo代码进行了再次开发和开源,最终封装成库。如下: 博客:www.cnblogs.com/1wen/p/5808…github: github.com/zimv/websoc…npm: www.npmjs.com/package/web… 在2020年也就是今年初,同事建议说可以考虑兼容一下小程序,心想也挺好的。便有了今天的 websocket-heartbeat-miniprogram,这次基于以前的代码新建了一个项目,只做小程序的版本,因为涉及到各种小程序以及相关框架的兼容,觉得还是单独出一个包,更专注一点。 介绍websocket-heartbeat-miniprogram基于小程序的websocket相关API进行封装,主要目的是保障客户端websocket与服务端连接状态。该程序有心跳检测及自动重连机制,当网络断开或者后端服务问题造成客户端websocket断开,程序会自动尝试重新连接直到再次连接成功。兼容市面上大部分小程序微信,百度,支付宝等,只要都是统一的小程序weboscket-API规范。也支持小程序框架比如Taro等。无论如何,业务是需要一层心跳机制的,否则一些情况下会丢失连接导致功能无法使用。 用法 安装npm install --save websocket-heartbeat-miniprogram 复制代码 引入使用import WebsocketHeartbeat from 'websocket-heartbeat-miniprogram'; WebsocketHeartbeat({ miniprogram: wx, connectSocketParams: { url: 'ws://xxx' } }) .then(task => { task.onOpen = () => {//钩子函数 console.log('open'); }; task.onClose = () => {//钩子函数 console.log('close'); }; task.onError = e => {//钩子函数 console.log('onError:', e); }; task.onMessage = data => {//钩子函数 console.log('onMessage', data); }; task.onReconnect = () => {//钩子函数 console.log('reconnect...'); }; task.socketTask.onOpen(data => {//原生实例注册函数,重连后丢失 console.log('socketTask open'); }); task.socketTask.onMessage(data => {//原生实例注册函数,重连后丢失 console.log('socketTask data'); }); }) 本程序内部总是使用小程序connectSocket方法进行socket连接,如果socket断开,本程序内部会再次执行小程序的connectSocket方法,以此来重新建立连接,重连都会建立新的小程序socket实例。 WebsocketHeartbeat方法返回一个promise,返回的task对象是本程序的一个实例,提供了onOpen,onClose,onError,onMessage,onReconnect等钩子函数。也暴露了小程序本身的实例(socketTask),task.socketTask就是小程序connectSocket返回的实例,而task.socketTask是小程序的原生实例,它们通过onXXX方法传递函数进行监听注册,在socket重连以后,内部重新通过connectSocket新建实例,将会返回新的小程序原生实例,因此之前通过socketTask.onXXX注册的这些函数将会丢失。而本程序内部提供的钩子函数重连上以后依然有效。大部分情况下推荐就使用本程序的钩子函数。 支付宝小程序差异支付宝小程序只允许同时存在一个socket连接,原生的API也和其他小程序有一点小差异,本程序已经做了兼容,但是还是要注意支付宝只允许建立一个socket,如果建立多个socket,前面的socket都会被系统关闭,一定要通过本程序实例的task.close关闭旧的socket,否则程序会一直重连,导致所有socket相互冲突无法使用。 约定 1.只能通过前端主动关闭socket连接 如果需要断开socket,应该执行task.close()方法。如果后端想要关闭socket,应该下发一个消息,前端判断此消息,前端再调用task.close()方法关闭。因为无论是后端调用close还是因为其他原因造成的socket关闭,前端的socket都会触发onClose事件,程序无法判断是什么原因导致的关闭。因此本程序会默认尝试重连。 import WebsocketHeartbeat from 'websocket-heartbeat-miniprogram'; WebsocketHeartbeat({ miniprogram: wx, connectSocketParams: { url: 'ws://xxxx' } }) .then(task => { task.onMessage = data => { if(data.data == 'close') task.close();//关闭socket并且,不再重连 }; }) 2.后端对前端心跳的反馈 前端发送心跳消息,后端收到后,需要立刻返回响应消息,后端响应的消息可以是任何值,因为本程序并不处理和判断响应的心跳消息,而只是在收到任何消息后,重置心跳,因为收到任何消息就说明连接是正常的。因此本程序收到任何后端返回的消息都会重置心跳倒计时,以此来减少不必要的请求,减少服务器压力。 API详情参考: https://github.com/zimv/websocket-heartbeat-miniprogram 结语程序已经经过单元测试,也在百度,支付宝,微信等小程序之中实际测试和基于Taro测试。程序长期维护,发现兼容问题或者程序问题,希望得到issue反馈!
2020-02-22 - js获取一段时间内的工时,除去周末,午休时间
老规矩,先看效果图 [图片] 比如我们上午9点到12点半,下午2点到6点半算工时。那么我们早晨9点商标,下午6点半下班,就应该算一个完整的工作日,8个工时。再如下图,就应该是15个工时,1天7小时。 [图片] 还可以跨月计算 [图片] 下面就把完整的js代码贴出来给大家 [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>计算时长</title> </head> <body> <script> /* * 0-15分不算工时 * 15-45算半个小时 * 45-60算一个小时 * */ function carryTime(date) { if (date.getMinutes() > 0 && date.getMinutes() < 15) { date.setMinutes(0); } if (date.getMinutes() >= 15 && date.getMinutes() < 30) { date.setMinutes(30); } if (date.getMinutes() > 30 && date.getMinutes() < 45) { date.setMinutes(30); } if (date.getMinutes() >= 45) { date.setHours(date.getHours() + 1); date.setMinutes(0); } return date; } let number = DateDiffNoWeekDay(new Date("2020-02-25 09:00"), new Date("2020-03-26 17:30")); console.log("总小时数", number); function DateDiffNoWeekDay(startTime, endTime) { if (startTime >= endTime) return 0; //1,分钟取整 startTime = carryTime(startTime); endTime = carryTime(endTime); //2,计算总天数 var totalTime = 0;//工时,天数 if (startTime.getDay() == 6 || startTime.getDay() == 0) { totalTime = endTime.getDate() - startTime.getDate(); } else { totalTime = Math.floor(((endTime - startTime) / (3600 * 1000)) / 24); } //3,拿初始值赋值给一个临时变量 var tempStartTime = new Date(); tempStartTime.setTime(startTime.getTime()); //4,计算出总天数 while (tempStartTime.getDate() < endTime.getDate()) { if (tempStartTime.getDay() == 6 || tempStartTime.getDay() == 0) {//周六或者周日减去 totalTime--; } tempStartTime.setDate(tempStartTime.getDate() + 1); } //5,计算出总小时数 var temp = 0;//工时,小时 do { if (startTime.getDay() == 6 || startTime.getDay() == 0) {//周六周日 startTime.setDate(startTime.getDate() + 1); //*********周六周日直接跳过,初始化为早晨9点 startTime.setHours(9); startTime.setMinutes(0); continue; } let tempMinutes = startTime.getHours() * 60 + startTime.getMinutes(); //上午9点到12点半,算工时 if (tempMinutes >= 9 * 60 && tempMinutes < (12 * 60 + 30)) { temp += 0.05; } //上午14点到18点半,算工时 if (tempMinutes >= 14 * 60 && tempMinutes < (18 * 60 + 30)) { temp += 0.05; } startTime.setTime(startTime.getTime() + 0.5 * 3600 * 1000);//每次增加半个小时 } while (startTime.getHours() * 60 + startTime.getMinutes() != endTime.getHours() * 60 + endTime.getMinutes()) { totalTime += Math.floor(temp / 0.8); totalTime += temp % 0.8; totalTime = Math.round(totalTime * 100) / 100 } var days = Math.floor(totalTime); var hours = Math.round((totalTime - days) * 100) / 10; console.log(days + '天', hours + '小时'); return days * 8 + hours; } </script> </body> </html> [代码] 代码里注释很详细了,这里就不在做讲解了,后面我会更新更多js相关的实用知识出来,敬请关注。
2020-02-23 - 实战分享: 小程序云开发玩转订阅消息(二)
[图片]这是实战分享: 小程序云开发玩转订阅消息的第二部分 第一部分链接 《实战分享: 小程序云开发玩转订阅消息(一)》 将订阅消息存入云开发数据库接下来我们创建一个云函数 [代码]subscribe[代码] ,这个云函数的作用是将用户的订阅信息存入云开发数据库的集合 [代码]messages[代码] 中,等待将来需要通知用户时进行调用。 在微信开发者工具的云开发面板中创建数据库集合 [代码]messages[代码] [图片]微信开发者工具新增数据库集合 创建一个 [代码]subscribe[代码] 云函数,在云函数中我们将小程序端发送过来的课程订阅信息,存储在云开发数据库集合中,开发完成后,在微信开发者工具中右键上传并部署云函数。 cloudfunctions/subscribe/index.js [代码]const cloud = require('wx-server-sdk'); cloud.init(); const db = cloud.database(); exports.main = async (event, context) => { try { const {OPENID} = cloud.getWXContext(); // 在云开发数据库中存储用户订阅的课程 const result = await db.collection('messages').add({ data: { touser: OPENID, // 订阅者的openid page: 'index', // 订阅消息卡片点击后会打开小程序的哪个页面 data: event.data, // 订阅消息的数据 templateId: event.templateId, // 订阅消息模板ID done: false, // 消息发送状态设置为 false }, }); return result; } catch (err) { console.log(err); return err; } }; [代码]利用定时触发器来定期发送订阅消息接下来我们需要实现一个定时执行的云函数[代码]send[代码],来检查数据库中是否有需要发送给用户的订阅消息。如果有需要发送的订阅消息,会通过云调用 [代码]cloud.openapi.subscribeMessage.send[代码] 将订阅消息发送给用户。 创建一个名叫 [代码]send[代码] 的云函数,首先要配置云函数,在 [代码]config.json[代码] 的 [代码]permissions[代码] 中新增 [代码]subscribeMessage.send[代码]的云调用权限,然后新增一个 [代码]sendMessagerTimer[代码] 的定时触发器,定时触发器的语法和 [代码]linux[代码] 的 [代码]crontab[代码] 类似,比如,我们配置的 [代码]"0 * * * * * *"[代码] 代表每分钟执行一次云函数。 cloudfunctions/send/config.json [代码]{ "permissions": { "openapi": ["subscribeMessage.send"] }, "triggers": [ { "name": "sendMessagerTimer", "type": "timer", "config": "0 * * * * * *" } ] } [代码]接下来是实现发送订阅消息的云函数,这个云函数会从云开发数据库集合[代码]messages[代码]中查询等待发送的消息列表,检查数据库中是否有需要发送给用户的订阅消息,发送条件可以根据自己的业务实现,比如开课提醒可以根据课程开课日期来检查是否需要发送订阅消息,在我们下面的代码示例里做了简化,筛选条件只检查了状态为未发送。 查询到待发送的消息列表之后,我们会循环消息列表,依次发送每条订阅消息,发送成功后将数据库中消息的状态改为已发送。 cloudfunctions/send/index.js [代码]const cloud = require('wx-server-sdk'); exports.main = async (event, context) => { cloud.init(); const db = cloud.database(); try { // 从云开发数据库中查询等待发送的消息列表 const messages = await db .collection('messages') // 查询条件这里做了简化,只查找了状态为未发送的消息 // 在真正的生产环境,可以根据开课日期等条件筛选应该发送哪些消息 .where({ done: false, }) .get(); // 循环消息列表 const sendPromises = messages.data.map(async message => { try { // 发送订阅消息 await cloud.openapi.subscribeMessage.send({ touser: message.touser, page: message.page, data: message.data, templateId: message.templateId, }); // 发送成功后将消息的状态改为已发送 return db .collection('messages') .doc(message._id) .update({ data: { done: true, }, }); } catch (e) { return e; } }); return Promise.all(sendPromises); } catch (err) { console.log(err); return err; } }; [代码]最终效果 [图片]开课提醒订阅消息截图 源代码https://github.com/binggg/tcb-subscribe-demo[3] 参考资料 [1]注册小程序帐号: https://tencentcloudbase.github.io/2019-09-03-wx-dev-guide-register/ [2]开通云开发服务: https://tencentcloudbase.github.io/2019-09-03-wx-dev-guide-service/ [3]https://github.com/binggg/tcb-subscribe-demo: https://github.com/binggg/tcb-subscribe-demo
2019-10-23 - 复制任意微信小程序页面路径
以下以微信小程序“虎牙直播”为例,演示如何复制微信小程序页面的路径。 1.进入小程序的“关于虎牙直播”页面 [图片] 2.点击右上角的“…”进入“更多资料”页面 [图片] [图片] [图片] 3.复制AppID:wx74767bf0b684f7d3 4.进入小程序后台输入appid并搜索,然后点下一步 [图片] 5.鼠标移动到“获取更多页面路径”,在弹出窗口输入当前登陆的小程序的任意开发者微信号,然后点击开启,出现顶部的“开启入口成功”就可以使用手机访问“虎牙直播”任意页面进行复制了 [图片] 6.某个直播间的页面路径:pages/main/liveRoom/index.html?anchorUid=1678113423&source=search[图片] PS:复制出来的页面路径在小程序里使用的时候记得删除 .html 才能正常访问。
2020-01-16 - 【必收】精心整理!小程序开发资源汇总(附带源码)
很多小伙伴想在春节放假期间学小程序,但是小程序学习的资源和教程可能不太好找。所以小助手精心整理了一期,全是干货!认真学,开启美妙的小程序开发之旅,做一个属于自己的微信小程序。有需要的小伙伴收藏好这期文章哦~ 本文收集整理了微信小程序开发资源,包括官方文档,云开发训练营文档,视频教程以及实战源码推荐,会不间断更新。。 欢迎添加云开发小助手CloudBase微信:Tcloudedu1 ,一起加入技术交流群~ 小程序云开发官方公众号 [图片] 目录 官方文档 云开发训练营 视频教程 小程序·云开发Demo 技术交流群 官方文档 小程序开发者工具 小程序设计指南 小程序开发教程 小程序框架 小程序组件 小程序API 小程序开发者工具 小程序云开发文档 云开发训练营 小程序开发入门 小程序与JavaScript 云开发快速入门 [图片] 视频教程 腾讯云云开发B站:https://space.bilibili.com/447496276 [图片] 小程序·云开发Demo 技术博客小程序 包括文章的发布及浏览、评论、点赞、浏览历史、分类、排行榜、分享、生成海报图等。 网盘小程序 兼具文件存储与分享功能的专属网盘小程序。 教务助手小程序 用完即走,查个成绩和课表,无需下载app或去翻看公众号内的历史内容。 功能日历小程序 既能查看日历又能备注事项,看云开发如何支持功能性日历小程序的快速开发。 客户业务需求收集小程序 用云开发快速制作客户业务需求收集小程序,教你用云开发实现小程序版“朋友圈”的发布与展示。 小程序朋友圈 把朋友圈装进小程序需要几步?借助云开发实现小程序朋友圈的发布与展示。 南苑导览 一款由学生独立开发的以地图为载体,提供中山大学南方学院具体地点的位置信息、导航、校园历史及文化介绍的小程序。 互动打卡小程序 用云开发轻松构建精美互动打卡小程序,交互式双人打卡,快乐加倍。 个性头像小程序 别再@官方啦!云开发教你轻松制作个性头像小程序,趣味挂件、个性icon。 二手书商城小程序 云开发轻松制作二手书交易商城小程序,让智慧延续,让温暖传递。 后台数据批量导出 小程序开发过程中如何将云数据库中的数据批量导出至excel。 发送邮件 初学者福音,手把手教你用小程序云开发实现邮件发送功能。 高考查分小程序 实现高考分数轻松查,小程序源码。 mini论坛 仅需两天轻松搭建mini论坛小程序。 运动圈小程序 打造运动圈小程序(以乒乓球为例),实现球友间高效互动。 心情日记小程序 我能想到最浪漫的事,可能就是“你的心事我全知晓”。 最美恋爱小程序 小程序前端用的是taro框架写的,后台用的云开发。教你用云开发为心爱的人做个小程。 校园约拍小程序 校园场景下,小程序·云开发大显身手,校园约拍小程序源码。 体重记录小程序 只想记录每日体重还得下个APP,不用那么麻烦!用云开发做个专属体重记录小程序,看看你每天瘦了多少。 口袋工具 口袋工具之历史上的今天。一个基于云开发的小程序,看看历史上的今天都发生了啥。 迷你微博 独立做个精简版微博出来让你刷刷刷吗?而且,它还兼具搜索、点赞、主页的功能 多媒体小程序 使用小程序·云开发构建多媒体小程序。 技术交流群 交流技术为主,开发学习工作中遇到问题可以在群内交流,欢迎有需要的朋友加群。 添加小助手微信(Tcloudedu1),回复“技术群”,即可加入云开发技术群。 最后 如果你有关于使用腾讯云云开发相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们~ 关注腾讯云云开发,后台回复【源码】,获取更多微信小程序云开发实战源码。 [图片] [图片] [图片] 关注「腾讯云云开发」,后台回复【 源码 】,获取更多微信小程序云开发实战源码。 持续更新中… [图片]
2020-01-16 - 商品数据接入(内测)
一、商品数据应用场景商品数据目前应用于微信扫一扫识物、小程序商品搜索、扫条码三个功能。 这些功能可以很好的满足微信用户对商品的信息获取诉求,同时也能为商家小程序带来曝光流量和建立用户品牌认知的机会。 扫一扫识物- 效果图: [图片][图片] 小程序商品搜索- 效果图: [图片][图片] 扫一扫商品条码- 效果图: [图片] 二、商品数据接入方式 目前微信已经爬得部分商品详情页,并对页面的商品信息进行了一定的分析理解。商家小程序可以配合接入商品数据,帮助微信更好地发现更多更丰富的商品信息,提高商品的曝光机会。 成功接入需要完成以下三步: 第一步:开启「爬虫开关」确保爬虫开关处于开启状态,保证小程序页面内容获得被微信收录的机会。爬虫开关在微信公众平台上设置,可参考如下示意图。 [图片]第二步:推送「页面路径」 通过接口主动推送商品详情页的页面路径至微信后台,保证推送页面被微信爬虫及时发现,获得曝光机会。具体参考:小程序search.submitPages接口文档 第三步:接入「数据更新协议」接入数据更新协议,可支持微信实时的获取到商品的价格、上下架状态等最新信息,避免由于信息不准确而影响商品的曝光效果。 具体参考文档:小程序商品数据实时更新文档 完成以上三步后,商家小程序的商品详情页将被收录,获得在“扫一扫识物功能”、“小程序商品搜索功能”和“扫条码”的曝光机会。 此外,我们建议商家小程序还可继续标记页面结构化内容和优化页面结构: 一、标记「页面结构化内容」 通过对页面结构化内容的标记,帮助微信爬虫更好的理解页面信息,提高页面的召回排序精准度和曝光转化率。具体参考:页面标记商品结构化数据文档 二、优化页面结构设计 基于小程序搜索优化指南,优化页面结构设计,提高页面对爬虫的友好度。具体参考:小程序SEO建议 如本文档版本过旧,请访问Git查看最新版本:点我
2020-01-13 - 干货满满:个人小程序广告日入1k实战操作
众所周知,个人小程序受限于微信的政策,很多内容或者类目无法去做,对于变现,大家的目光基本也就聚焦在广告变现这一条路上。 随着小程序各种广告的开放,个人小程序的变现之路似乎也越来越明朗。然而,现实的情况却是,月底结算的时候,流量主们看着结算单上的两位数,欲哭无泪。 那么,对于个人小程序,广告变现的路子该如何去走? 如何才能实现收益最大化? 如何才能广告月入30k,当上CEO,赢取白富美,走上人生巅峰? **下面是满满的干货,一定要看到最后一行!** 1. 做什么方向?做什么内容?起什么名字? 以变现为目的的小程序,名字很重要,一定要和其他热门小程序相似、相近、相仿! 内容? 内容从来不是重点,只需要记住一点, 什么火做什么,什么话题热做什么! 以这个小程序为例: [图片] 看明白了吧,短视频现在多火啊,做他准没错! 说到这里,有人发现问题了,视频类目明明是个人不允许做的啊,这怎么办? 这个问题待会说。 2. 善用广告 目前可用的广告基本有banner,开屏,激励视频,贴片,这几种广告形式一定要在小程序中运用的淋漓尽致! 仍以此小程序为例: [图片] [图片] [图片] a. 打开小程序就开屏广告,切换tab就开屏广告,没事滚滚屏也给你来一个,不怕你不点,反正不小心也会点到 b. 视频流里面能放多少个就放多少个,万一不小心点到了呢? c. 视频的贴片广告一定要加的,不然就浪费了 d. 视频下方的相关视频一定要做,想看可以,先看一下激励视频再说。反正客户群体主要是大爷大妈,有的是时间,不在乎这15秒到30秒。 3. 善用分享 凡是目所能及的地方,一定要加上分享,分享朋友圈这种不仅要加还要做动态效,让用户记得去点。 [图片] 4. 善于召回用户 一定要用好小程序的下发通知功能,收集formid,没事就给用户发个通知什么的,不愁他不回头(至于formid怎么收集,不在本教程讨论范畴之内,自行搜索)。 [图片] 6. 绕审(敲黑板!划重点!) 说了这么多,一定有人要说了,你这小程序类目不对,广告乱搞,怎么可能过审啊? 重点来了,绕审,这个一定要做。 众所周知,微信的审核是 有人工环节的,只需要在提审期间,让审核人员看到你想让他看到的,不就行了么? 这个也很简单,审核期间,接口返回的数据做点手脚,页面上加个判断不就是了。 [图片] 看到了吧,审核期间所有视频都变成了图片,点击查看大图而已。 完美解决!! 好了说了这么多,对于月入30K是不是有点心得了???如果有兴趣,继续往下看 -------------------- -------------------- -------------------- 以上内容均属严重违规行为,请大家引以为鉴!!! 以上内容均属严重违规行为,请大家引以为鉴!!! 以上内容均属严重违规行为,请大家引以为鉴!!! 以上内容均属严重违规行为,请大家引以为鉴!!! 以上内容均属严重违规行为,请大家引以为鉴!!!
2019-09-10 - 小程序云开发之数据库自动备份
数据是无价的,我们通常会把重要的业务数据存放在数据库中,并需要对数据库做定时的自动备份工作,防止数据异常丢失,造成无法挽回的损失。 小程序云开发提供了方便的云数据库供我们直接使用,云开发使用了腾讯云提供的云数据库,拥有完善的数据保障机制,无需担心数据丢失。但是,我们还是不可避免的会担心数据库中数据的安全,比如不小心删除了数据集合,写入了脏数据等。 还好,云开发控制台提供了数据集合的导出,导入功能,我们可以手动备份数据库。不过,总是手动备份数据库也太麻烦了点,所有重复的事情都应该让代码去解决,下面我们就说说怎么搞定云开发数据库自动备份。 通过查阅微信的文档,可以发现云开发提供了数据导出接口databaseMigrateExport [代码]POST https://api.weixin.qq.com/tcb/databasemigrateexport?access_token=ACCESS_TOKEN [代码] 通过这个接口,结合云函数的定时触发功能,我们就可以做数据库定时自动备份了。梳理一下大致的流程: 创建一个定时触发的云函数 云函数调用接口,导出数据库备份文件 将备份文件上传到云存储中以供使用 1. 获取 access_token 调用微信的接口需要 access_token,所以我们首先要获取 access_token。通过文档了解到使用 auth.getAccessToken 接口可以用小程序的 appid 和 secret 获取 access_token。 [代码]// 获取 access_token request.get( `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`, (err, res, body) => { if (err) { // 处理错误 return; } const data = JSON.parse(body); // data.access_token } ); [代码] 2. 创建数据库导出任务 获取 access_token 后,就可以使用 [代码]databaseMigrateExport[代码] 接口导出数据进行备份。 [代码]databaseMigrateExport[代码] 接口会创建一个数据库导出任务,并返回一个 job_id,这个 job_id 怎么用我们下面再说。显然数据库的数据导出并不是同步的,而是需要一定时间的,数据量越大导出所要花费的时间就越多,个人实测,2W 条记录,2M 大小,导出大概需要 3~5 S。 调用 [代码]databaseMigrateExport[代码] 接口需要传入环境 Id,存储文件路径,导出文件类型(1 为 JSON,2 为 CSV),以及一个 query 查询语句。 因为我们是做数据库备份,所以这里就导出 JSON 类型的数据,兼容性更好。需要备份的数据可以用 query 来约束,这里还是很灵活的,既可以是整个集合的数据,也可以是指定的部分数据,这里我们就使用 [代码]db.collection('data').get()[代码] 备份 data 集合的全部数据。同时我们使用当前时间作为文件名,方便以后使用时查找。 [代码]request.post( `https://api.weixin.qq.com/tcb/databasemigrateexport?access_token=${accessToken}`, { body: JSON.stringify({ env, file_path: `${date}.json`, file_type: '1', query: 'db.collection("data").get()' }) }, (err, res, body) => { if (err) { // 处理错误 return; } const data = JSON.parse(body); // data.job_id } ); [代码] 3. 查询任务状态,获取文件地址 在创建号数据库导出任务后,我们会得到一个 job_id,如果导出集合比较大,就会花费较长时间,这时我们可以使用 databaseMigrateQueryInfo 接口查询数据库导出的进度。 当导出完成后,会返回一个 [代码]file_url[代码],即可以下载数据库导出文件的临时链接。 [代码]request.post( `https://api.weixin.qq.com/tcb/databasemigratequeryinfo?access_token=${accessToken}`, { body: JSON.stringify({ env, job_id: jobId }) }, (err, res, body) => { if (err) { reject(err); } const data = JSON.parse(body); // data.file_url } ); [代码] 获取到文件下载链接之后,我们可以将文件下载下来,存入到自己的云存储中,做备份使用。如果不需要长时间的保留备份,就可以不用下载文件,只需要将 job_id 存储起来,当需要恢复备份的时候,通过 job_id 查询到新的链接,下载数据恢复即可。 至于 job_id 存在哪,就看个人想法了,这里就选择存放在数据库里。 [代码]await db.collection('db_back_info').add({ data: { date: new Date(), jobId: job_id } }); [代码] 4. 函数定时触发器 云函数支持定时触发器,可以按照设定的时间自动执行。云开发的定时触发器采用的 [代码]Cron[代码] 表达式语法,最大精度可以做的秒级,详细的使用方法可以参考官方文档:定时触发器 | 微信开放文档 这里我们配置函数每天凌晨 2 点触发,这样就可以每天都对数据库进行备份。在云函数目录下新建 [代码]config.json[代码]文件,写入如下内容: [代码]{ "triggers": [ { "name": "dbTrigger", "type": "timer", "config": "0 0 2 * * * *" } ] } [代码] 完整代码 最后,贴出可以在云函数中使用的完整代码,只需要创建一个定时触发的云函数,并设置好相关的环境变量即可使用 appid secret backupColl:需要备份的集合名称,如 ‘data’ backupInfoColl:存储备份信息的集合名称,如 ‘db_back_info’ 注意,云函数的默认超时时间是 3 秒,创建备份函数时,建议将超时时间设定到最大值 20S,留有足够的时间查询任务结果。 [代码]/* eslint-disable */ const request = require('request'); const cloud = require('wx-server-sdk'); // 环境变量 const env = 'xxxx'; cloud.init({ env }); // 换取 access_token async function getAccessToken(appid, secret) { return new Promise((resolve, reject) => { request.get( `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`, (err, res, body) => { if (err) { reject(err); return; } resolve(JSON.parse(body)); } ); }); } // 创建导出任务 async function createExportJob(accessToken, collection) { const date = new Date().toISOString(); return new Promise((resolve, reject) => { request.post( `https://api.weixin.qq.com/tcb/databasemigrateexport?access_token=${accessToken}`, { body: JSON.stringify({ env, file_path: `${date}.json`, file_type: '1', query: `db.collection("${collection}").get()` }) }, (err, res, body) => { if (err) { reject(err); } resolve(JSON.parse(body)); } ); }); } // 查询导出任务状态 async function waitJobFinished(accessToken, jobId) { return new Promise((resolve, reject) => { // 轮训任务状态 const timer = setInterval(() => { request.post( `https://api.weixin.qq.com/tcb/databasemigratequeryinfo?access_token=${accessToken}`, { body: JSON.stringify({ env, job_id: jobId }) }, (err, res, body) => { if (err) { reject(err); } const { status, file_url } = JSON.parse(body); console.log('查询'); if (status === 'success') { clearInterval(timer); resolve(file_url); } } ); }, 500); }); } exports.main = async (event, context) => { // 从云函数环境变量中读取 appid 和 secret 以及数据集合 const { appid, secret, backupColl, backupInfoColl } = process.env; const db = cloud.database(); try { // 获取 access_token const { errmsg, access_token } = await getAccessToken(appid, secret); if (errmsg && errcode !== 0) { throw new Error(`获取 access_token 失败:${errmsg}` || '获取 access_token 为空'); } // 导出数据库 const { errmsg: jobErrMsg, errcode: jobErrCode, job_id } = await createExportJob(access_token, backupColl); // 打印到日志中 console.log(job_id); if (jobErrCode !== 0) { throw new Error(`创建数据库备份任务失败:${jobErrMsg}`); } // 将任务数据存入数据库 const res = await db.collection('db_back_info').add({ data: { date: new Date(), jobId: job_id } }); // 等待任务完成 const fileUrl = await waitJobFinished(access_token, job_id); console.log('导出成功', fileUrl); // 存储到数据库 await db .collection(backupInfoColl) .doc(res._id) .update({ data: { fileUrl } }); } catch (e) { throw new Error(`导出数据库异常:${e.message}`); } }; [代码]
2019-08-12 - 小程序红包配置及开发小结
配置: 1、进入商户平台 在产品中心找到小程序红包 开通小程序红包功能 2、开通后在左边的APPID授权管理中关联该小程序APPID 3、进入小程序后台 在功能==》微信支付中确认关联并授权 4、回到商户平台APPID授权管理中确认关联 5、这是最容易忽略的一点 在商户平台 产品中心 小程序红包的产品设置中 拉到最下面 小程序红包权限中开通该小程序的红包功能 到此小程序红包配置完成 开发: 发送红包 var mdhbhe = Convert.ToInt32(fee * 100); string mch_billno = mdminihb.Mch_id + DateTime.Now.ToString("yyyyMMdd") + GenerateNonceStr(); WxPayData hb = new WxPayData(); hb.SetValue("act_name", mdminihb.Act_name);//活动名称 hb.SetValue("mch_billno", mch_billno);//单号 hb.SetValue("mch_id", mdminihb.Mch_id);//发送红包的商户号 hb.SetValue("nonce_str", GenerateNonceStr()); hb.SetValue("notify_way", "MINI_PROGRAM_JSAPI"); hb.SetValue("re_openid", openid); hb.SetValue("remark", mdminihb.Remark); hb.SetValue("send_name", mdminihb.Send_name);//商户名称 hb.SetValue("total_amount", mdhbhe);//红包金额 单位分 hb.SetValue("total_num", 1);//红包数量 hb.SetValue("wishing", mdminihb.Wishing);//祝福语 hb.SetValue("wxappid", mdminihb.Wxappid);//绑定在商户的小程序的appid 不是公众号的 hb.SetValue("scene_id", mdminihb.Scene_id); var sign = hb.MakeSign2(mdminihb.Mch_key);//商户秘钥 hb.SetValue("sign", sign); string xml = hb.ToXml(); string response = HttpService.HbPost(xml, url, true, 6, mdminihb.Mch_path, mdminihb.Mch_certkey); WxPayData result = new WxPayData(); result.FromXml(response);//将xml格式的结果转换为对象以返回 var package = ""; if (result.GetValue("return_code").ToString() == "SUCCESS" && result.GetValue("result_code").ToString() == "SUCCESS") { //这边是成功后返回的代码 具体逻辑判断自己处理 package = result.GetValue("package").ToString();//成功后返回的 package = HttpUtility.UrlEncode(package); //这是用于领取红包的代码 WxPayData inputObj = new WxPayData(); inputObj.SetValue("appId", mdminihb.Wxappid);//这边是小程序的appId 这个appId 一定要记住 I要大写 inputObj.SetValue("timeStamp", timeStamp); inputObj.SetValue("nonceStr", nonceStr); inputObj.SetValue("package", package); var paySign = inputObj.HBMakeSign(mdminihb.Mch_key);//商户秘钥 } 签名方法: public string MakeSign2(string key) { //转url格式 string str = ToUrl(); //在string后加入API KEY str += "&key=" + key + ""; var rd = Md5.md5(str, 32); // 所有字符转为大写 return rd.ToUpper(); } 还有记得带证书 写的比较笼统 有不清楚的再补充 补充说明1:目前小程序红包仅支持用户微信扫码打开小程序,进行红包领取。(场景值1011,1025,1047,1124,小程序场景值详情参见文档 这个条件一定要注意 所以特别注意一定要通过wx.getLaunchOptionsSync()先看下场景值对不对 特别说明 体验版的二维码是无法领取红包的(第三方的要注意) 补充说明2:第二次领取红包的签名不需要大写
2020-01-02 - 借助云开发搭建专属技术博客小程序丨实战
▌博客小程序介绍 主要功能: 包括文章的发布及浏览、评论、点赞、浏览历史、分类、排行榜、分享、生成海报图等。 [图片] 效果展示: [图片] ▌数据库设计 数据库主要就7张表,分别为:用户表,分类表,文章表,文章内容表,评论表,点赞表,历史浏览表。 [图片] ▌评论功能设计 以文章评论功能为例,我们来看看代码以及小程序云开发的整个流程。 1. 实现思路 一开始的实现思路是准备搞两张表,一张评论主表,一张回复评论的子表,后来想着不用这么复杂,其实就用一张表也能实现评论及回复的功能。 2. 代码实现 发表评论有三种情况,第一种是评论文章,为一级评论,第二种是评论别人的评论,为二级评论,第三种是回复别人的评论,为三级评论。 2.1 如何新增一条评论 [图片] 结合上面图片,我们再来看看代码,就很清晰了。 [代码]/** * 发布评论 */ submit() { var comment = this.data.inputData if (comment == '') { wx.showToast({ title: '请填写评论', icon: 'none' }) } else { console.log("我的评论:" + this.data.inputData) var type = this.data.type; if (type == 1) { // 1是评论别人的评论》二级评论 this.replyComment(1) } else if (type == 2) { this.replyComment(2) // 2是回复别人的评论》三级评论 } else if (type == 3) { // 3是评论文章》一级评论 this.addComment(); } } }, /** * 新增评论 */ addComment() { var _this = this; var openid = wx.getStorageSync("openid") wx.showLoading({ title: '正在加载...', }) var create_date = util.formatTime(new Date()); console.log("当前时间为:" + create_date); var timestamp = Date.parse(new Date()); timestamp = timestamp / 1000; console.log("当前时间戳为:" + timestamp); // 调用云函数 wx.cloud.callFunction({ name: 'addComment', data: { //_id: timestamp + _this.data.otherUserInfo._id, id: _this.data.articleDetail._id, _openid: openid, avatarUrl: _this.data.userInfo.avatarUrl, nickName: _this.data.userInfo.nickName, comment: _this.data.inputData, create_date: create_date, flag: 0, article_id: _this.data.articleDetail.article_id, timestamp: timestamp, childComment: [], }, success: res => { // res.data 包含该记录的数据 console.log("新增评论成功---") wx.showToast({ title: '评论提交成功', }) wx.navigateBack({ delta: 1 }) }, fail: err => { console.error('[云函数]调用失败', err) }, complete: res => { wx.hideLoading() } }) }, /** * 回复评论 */ replyComment(commentType) { var _this = this; wx.showLoading({ title: '正在加载...', }) var create_date = util.formatTime(new Date()); console.log("当前时间为:" + create_date); var timestamp = Date.parse(new Date()); timestamp = timestamp / 1000; wx.cloud.callFunction({ name: 'replyComment', data: { id: _this.data.articleDetail._id, _id: _this.data.otherUserInfo._id, avatarUrl: _this.data.userInfo.avatarUrl, nickName: _this.data.userInfo.nickName, openId: _this.data.openid, comment: _this.data.inputData, createDate: create_date, flag: commentType, opposite_avatarUrl: _this.data.otherUserInfo.avatarUrl, opposite_nickName: _this.data.otherUserInfo.nickName, opposite_openId: _this.data.otherUserInfo._openid, timestamp: timestamp, }, success: res => { // res.data 包含该记录的数据 console.log("回复评论成功---") wx.showToast({ title: '回复提交成功', }) wx.navigateBack({ delta: 1 }) }, fail: err => { console.error('[云函数]调用失败', err) }, complete: res => { wx.hideLoading() } }) }, [代码] 下面是新增评论和回复评论的两个云函数,主要用到了async和await这两个函数,让新增和回复函数执行完后我们再更新一下article文章表的评论字段,让其加1,async和await的好处就是可以让函数有序的进行,这里就不赘述。 [代码]// 新增评论云函数 const cloud = require('wx-server-sdk') var env = 'hsf-blog-product-xxxxx'; // 正式环境 // var env = 'xxxxxxxxxxxxx'; // 测试环境 cloud.init({ env: env }) const db = cloud.database() const _ = db.command exports.main = async(event, context) => { try { let res = await db.collection('comment').add({ data: { _openid: event._openid, avatarUrl: event.avatarUrl, nickName: event.nickName, comment: event.comment, create_date: event.create_date, flag: event.flag, article_id: event.article_id, timestamp: event.timestamp, childComment: [], } }).then(res => { return res; }) await db.collection('article').doc(event.id).update({ data: { comment_count: _.inc(1) } }) return res; } catch (e) { console.error(e) } } [代码] [代码]// 回复评论云函数 const cloud = require('wx-server-sdk') var env = 'hsf-blog-product-xxxxx'; // 正式环境 // var env = 'xxxxxxxxxxxxxx'; // 测试环境 cloud.init({ env: env }) const db = cloud.database() const _ = db.command exports.main = async(event, context) => { try { let res = await db.collection('comment').doc(event._id).update({ data: { childComment: _.push({ avatarUrl: event.avatarUrl, nickName: event.nickName, openId: event.openId, comment: event.comment, createDate: event.createDate, flag: event.flag, opposite_avatarUrl: event.opposite_avatarUrl, opposite_nickName: event.opposite_nickName, opposite_openId: event.opposite_openId, timestamp: event.timestamp, }) } }).then(res => { return res; }) await db.collection('article').doc(event.id).update({ data: { comment_count: _.inc(1) } }) return res; } catch (e) { console.error(e) } } [代码] 2.2 如何显示每一条评论 从数据库取出评论的数据,循环遍历每一条父评论,如果有子回复也一并循环。这里每一条评论的唯一标识是用户的openId,那么我们可以用这个做一些事情,如:可以判断如果是自己的评论是不能回复的。 [代码]<view class="comment" wx:if="{{commentList.length>0}}"> <view class="comment-line"> <text class="comment-text">评论交流</text> <view class="bottom-line"></view> </view> <block wx:for='{{commentList}}' wx:key='*this' wx:for-item="itemfather"> <view class='commentList'> <view class="top-info"> <view class='img-name'> <image src="{{itemfather.avatarUrl}}"></image> <label>{{itemfather.nickName}}</label> </view> </view> <view class="father-content"> <text class="text">{{itemfather.comment}}</text> <view class="father-reply-time"> <text class="create-time">{{itemfather.create_date}}</text> <text class="reply" data-item="{{itemfather}}" bindtap='clickFatherConter' wx:if="{{openid != itemfather._openid}}">回复</text> </view> </view> <view class="children-content"> <block wx:for='{{itemfather.childComment}}' wx:key='*this'> <view class='childComment'> <view class="child-img-name"> <view class="avatar-name"> <image src="{{item.avatarUrl}}"></image> <text class='nickName'>{{item.nickName}}</text> </view> </view> <view class="child-comment" wx:if="{{item.flag==2 }}"> <text class='huifu'>回复</text> <text class='opposite-nickName'>{{item.opposite_nickName}}</text> <text class='comment-text'>{{item.comment}}</text> </view> <view class="child-comment" wx:if="{{item.flag==1}}"> <text class='comment-text'>{{item.comment}}</text> </view> <view class="child-reply-time"> <text class="child-create-time">{{item.createDate}}</text> <text class="reply" data-item="{{item}}" data-id="{{itemfather._id}}" bindtap='clickChildrenConter' wx:if="{{openid != item.openId}}">回复</text> </view> </view> </block> </view> </view> </block> </view> [代码] ▌项目运行 1. 下载源码 在github上将代码下载到本地: https://github.com/husanfeng/hsf_blog.git ** 2. 环境准备** (1)下载小程序开发工具; (2)注册appid; (3)使用小程序开发工具导入下载的代码,填入自己注册的AppID。 3. 云开发准备 (1)开通云开发功能。 [图片] (2)创建测试环境和生产环境。 [图片] 4. 修改环境ID (1)修改app.js中的环境ID为自己的环境ID。 [图片] (2)修改所有云函数中的环境ID为自己的环境ID。 [图片] 5. 云函数部署 (1)右键云函数目录,点击在终端中打开,执行npm install。 (2)右键执行上传并部署:所有文件。 6. 构建npm (1)勾选使用npm模块。 [图片] (2)点击顶部功能栏,执行构建npm。 7. 执行编译 ▌发布注意事项 小程序现在审核也是越来越严谨了,为了不让大家在审核道路上走弯路,我把我的一些经验分享给大家。 在微信公众平台上为小程序选择正确恰当的服务类目,例如博客类的小程序就可以选择教育信息服务。 如果你的小程序需要账号密码登录,提交审核时需要提交一个账号和密码,而且这个账号不能是测试账号,不能出现测试数据。 提交审核的版本首页需要有数据展示,例如:博客小程序你需要发布一篇或者多篇文章。 文章内容不能存在敏感内容。 评论功能审核比较严格了,一旦评论中存在敏感词汇,肯定审核不通过,官方建议调用小程序内容安全API,或使用其他技术、人工审核手段,过滤色情、违法等有害信息,保障发布内容的安全。 源码地址 https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你想要了解更多关于云开发CloudBase相关的技术故事/技术实战经验,请扫码关注【腾讯云云开发】公众号~ [图片]
2019-12-26 - 开源在线答题小程序
项目概述 本文介绍的是一款能在小程序上刷题的工具类小程序,目前主要面向的用户是证券从业人员、基金从业人员,本小程序题库均来自历年真题。 小程序名字:答题优等生 [图片] 小程序技术架构 小程序端未采用第三方框架,使用微信原生开发,未引入任何UI组件库 后端接口采用PHP YII2框架 目前小程序已经实现的功能有: 选择科目在线答题,答题可以选择单题模式还是列表模式 每种考试,可以选择科目,这样保持了考试、科目二级结构 答题历史纪录查询,可以查阅当时做题情况 从目前的功能实现来看,本小程序已完成了一个在线答题小程序的全闭环功能。 未来优化的几个地方在 答题结果页UI优化 答题环节的分享优化 开发小程序过程中遇到的问题 第一个问题:radio取值问题 在单选选择题的时候,用到以下两个表单组件 radio-group https://developers.weixin.qq.com/miniprogram/dev/component/radio-group.html radio https://developers.weixin.qq.com/miniprogram/dev/component/radio.html 默认的radio组件事件 wxml文件 [代码] <radio-group class="radio-group" bindchange="radioChange"> <radio class="radio" wx:for-items="{{items}}" wx:key="name" value="{{item.name}}" checked="{{item.checked}}"> <text>{{item.value}}</text> </radio> </radio-group> [代码] js文件 [代码]Page({ data: { items: [ { name: 'USA', value: '美国' }, { name: 'CHN', value: '中国', checked: 'true' }, { name: 'BRA', value: '巴西' }, { name: 'JPN', value: '日本' }, { name: 'ENG', value: '英国' }, { name: 'FRA', value: '法国' }, ] }, radioChange: function (e) { console.log('radio发生change事件,携带value值为:', e.detail.value) } }) [代码] 没错,用的就是官方示例代码,我们看到在选择的时候,默认e.detail.value,只能取一个字符串,当时遇到的第一个问题就在这里,如果把这整个选项的信息提取出来,能简单的用{{JSON.stringfy(item)}}吗,当然不可以,因为原生小程序本身不支持。 当时在社区查到解决方案具体可以参考 [单选框radio除了可以传value可以传其他的值吗?] https://developers.weixin.qq.com/community/develop/article/doc/0006ce9771c528ed7b89a6f485bc13 方案就是引入wxs,之前看官方文档,每次到这里,因为不知道这是干什么的,以及解决什么问题,现在明白了,想了解更多关于wxs的内容,也请移步下面两篇文档 [微信小程序wxs有什么用?] https://developers.weixin.qq.com/community/develop/article/doc/0008888a01c69872b689448a051013 [小程序里面精度计算问题] https://developers.weixin.qq.com/community/develop/article/doc/0000ae30ea4da802b989540175b013 第二个问题:每次10道题目是如何选择的 在答题的时候,每次会展示10个题目,这10个题目是从当前科目题库中,随机抽取10个,在题库足够大的情况下,基本可以保证每次进来答题的10个题目跟前面的答题都是不一样的。 小程序截图 [图片] [图片] [图片] [图片] [图片] [图片] 代码地址 前端小程序代码,请移步下面 https://gitee.com/xiaofeiyang3369/myexamapp 后端接口用的PHP,代码链接如下,由于我几个小程序都用这个PHP服务,项目代码要远比该小程序的PHP代码要多一些。 https://gitee.com/xiaofeiyang3369/phpapp 如果大家细心,数据库也是可以在线登录的,如果遇到问题,可以微信我。 适用人群 该开源代码适用于小程序初学者,以及大学做在线答题小程序的毕业设计时可以参考。 扫码体验 微信小程序搜索 从业资格题库或者直接扫码 [图片]
2020-06-08 - 5行代码获取小程序用户手机号
最近有很多同学有获取小程序用户手机号的需求。其实云开发出现之前我们获取小程序用户的手机号特别繁琐。自从有了云开发,我们获取用户手机号变得非常简单。只需要5行代码即可。 老规矩,我们先来看下效果图 [图片] 再来看下核心的代码,其实只有下面这一些。 [图片] 甚至可以说核心代码只有上图红色框里的两行。是的,你没听错,只靠这2行代码,就可以轻松的获取用户小程序绑定的手机号。 下面我们就来具体讲解吧。 注意:只有企业小程序才可以获取用户手机号,个人小程序没有办法获取的。 一,首先要用到button组件的开发能力 [图片] 编写wxml文件,代码很简单 [图片] 可以看到我们的button按钮,使用了open-type。 再来看下我们对应的js方法。这样我们点击按钮时,就会弹出授权弹窗。如下图 [图片] 不管用户点击拒绝还是允许,我们都能拿到对应的回调。再用户点击了允许以后,就可以获取到以下数据。 [图片] 大家看到我们获取的数据里有一个cloudID,其实这个值很有用的。 二,开发数据检验与解密 1,首先我们看下官方提供的获取手机号的文档。 [图片] 看官方文档,可以知道,我们这里涉及到一个数据的检验与解密问题 2,开发数据检验与解密 [图片] 这里我们要使用的就是方式二,使用云函数来实现解密,然后拿到用户的手机号。 三,云函数的编写 [图片] 通过上图可以看到,我们编写的云函数很简单。这里主要用的就是cloud.getOpenData这个功能。而这个功能需要的参数就是我们上面第一步获取的cloudID [图片] 这样我们调用云函数的时候,只需要把对应的cloudID传进来即可。 [图片] 看下我们的cloudID的作用,再来看下我们通过button的open-type获取的cloudID [图片] 可以看出,我们的cloudID和encryptedData一样,是一串加密数据。我们要通过云函数获取手机号,需要的就是这串加密字段。 四,上传cloudID获取手机号。 上面第三步云函数编写好以后,我们就可以来调用了。调用之前一定要记得部署下云函数,一定要记得部署下云函数。。。。 [图片] 上图就是我们的云函数的调用。如果你对云开发和云函数还不了解,建议你去看下我之前写的云开发相关的文章,获取看下我录的《微信小程序云开发云函数入门》 这时候点击按钮,我们就可以获取到了我们所需要的手机号了 [图片] 到这里我们就可以轻松的通过云开发获取用户的手机号了,比起传统的后台开发来获取,是不是简单了很多。 今天就讲到这里了,后面我还会写更多小程序相关的技术文章出来,请持续关注。
2019-12-16 - ✨5G时代来了,电商小程序商品视频播放解决方案,利用腾讯视频插件,好处多多,不占自己服务器空间和带宽
今天给大家介绍一下电商小程序商品加视频的解决方案 样例小程序: 各种好处: 空间节约:5G时代来临,视频播放为基本要求,还在为视频的存储空间发愁么,用腾讯视频插件,视频直接上传到腾讯服务器,无任何服务器空间消耗.带宽节约:空间不是问题的话,如何保证视频的播放流畅度?用腾讯视频插件播放,不占用自己服务器带宽,还省了CDN的钱,至于速度,你觉得腾讯视频的服务器会卡么?技术成本:视频的增删改查,代码都不用写,视频文件的地址直接用VID不到20个字符代替,开发方便,维护简单资质问题:用原生video标签,视频多了,上线审核的时候会要求文娱资质,有官方正面回答用腾讯视频播放,无需资质[图片] 视频审核:视频上传腾讯服务器的时候,腾讯自己就会审核好视频合法性 好处多多,我们来看下实现方法 一 添加插件: [图片] 进入小程序后台:设置->第三方设置->添加插件 输入APPID: wxa75efa648b60994b 腾讯视频插件官网地址:https://developers.weixin.qq.com/community/servicemarket/detail/00066e5ce0ce503bd9d837c1456415 二 小程序代码: 在app.json中加入代码,引用插件,版本号如果不是最新,开发工具上会有提示最新版版本号 [代码]"plugins": {[代码] [代码] "tencentvideo": {[代码] [代码] "version": "1.3.7",[代码] [代码] "provider": "wxa75efa648b60994b"[代码] [代码] }[代码] [代码]}[代码] 在需要播放视频的小程序页面的json中加入代码: [代码]{[代码] [代码] "navigationBarTitleText": "商品详情",[代码] [代码] "usingComponents": {[代码] [代码] "txv-video": "plugin://tencentvideo/video"[代码] [代码] }[代码] [代码]}[代码] 需要播放视频页面的wxml中加入代码: [代码]<!-- 商品轮播图开始 -->[代码] [代码]<swiper autoplay="{{autoplay}}"[代码] [代码]indicator-dots='{{indicator}}'[代码] [代码]indicator-active-color='#f54000'[代码] [代码]class='swiper'>[代码] [代码]<swiper-item wx:if="{{good.video}}">[代码] [代码] <txv-video vid="{{good.video}}"[代码] [代码]playerid="txv1"[代码] [代码]width="750rpx"[代码] [代码]height="720rpx"[代码] [代码]bindplay="videoplay"[代码] [代码]bindpause='videopause'[代码] [代码]bindpause='videoended'[代码] [代码]isHiddenStop="true"></txv-video>[代码] [代码]</swiper-item>[代码] [代码]<block wx:for="{{good.img}}"[代码] [代码]wx:key=''>[代码] [代码] <swiper-item>[代码] [代码] <image src="{{item}}"[代码] [代码]class='swiperimg'[代码] [代码]bindtap='previewImage'[代码] [代码]data-current='{{item}}'/>[代码] [代码] </swiper-item>[代码] [代码]</block>[代码] [代码]</swiper>[代码] [代码]<!-- 商品轮播图结束 -->[代码] 我这里是放到轮播图的第一张,做了判断 如果该商品无视频则不显示 讨论几个问题,视频播放的时候轮播图自动滚动了,页面下滑,视频继续播放,影响用户体验 所以增加三个方法和一个设置 视频播放时,设置轮播图为不自动轮播,消除轮播图位置点 binplay:视频播放时触发,设置轮播图为不自动播放,不显示位置点 [代码]//视频播放方法[代码] [代码]videoplay:function(){[代码] [代码] this.setData({[代码] [代码] autoplay:false,[代码] [代码] indicator: false,[代码] [代码] })[代码] [代码]},[代码] bindpause:视频暂停时触发,设置轮播图为自动播放,显示位置点 [代码]//视频暂停方法[代码] [代码] videopause:function(){[代码] [代码] this.setData({[代码] [代码] autoplay: true,[代码] [代码] indicator: true,[代码] [代码] })[代码] [代码] },[代码] bindended:视频播放结束时触发,设置轮播图为自动播放,显示位置点 [代码]//视频播放结束方法[代码] [代码]videoended:function(){[代码] [代码] this.setData({[代码] [代码] autoplay: true,[代码] [代码] indicator: true,[代码] [代码] })[代码] [代码]},[代码] 设置页面滑动,使视频不在可见范围时自动停止播放视频,isHiddenStop:ture 样例如图: [图片] 三 后台代码: 前端用VID播放了,可是后台客户怎么在后台网页上预览他上传的视频呢?------引用腾讯视频H5播放器插件 <div id="txvideo"></div> <input id="vid" name="video" class="inputl" placeholder="请输入腾讯视频vid" value='{$vid}'/> [代码]<script type="text/javascript"[代码] [代码]src="//vm.gtimg.cn/tencentvideo/txp/js/iframe/api.js"></script>[代码] [代码]<script>[代码] [代码] // 点播[代码] [代码] var[代码] [代码]vid = document.getElementById("vid").value//获取输入框元素[代码] [代码] var[代码] [代码]player = new[代码] [代码]Txp.Player({[代码] [代码] containerId: 'txvideo',[代码] [代码] vid: vid[代码] [代码] });[代码] [代码]</script>[代码] 效果如图: [图片] 四 获取腾讯视频vid方法: 进入腾讯视频.找到要播放的视频 ,鼠标放到分享那里,点击复制通用代码 [图片] 复制出来的代码如下: <iframe frameborder="0" src="https://v.qq.com/txp/iframe/player.html?vid=y3033v5o6ru" allowFullScreen="true"></iframe> 其中的vid=y3033v5o6ru 就是该视频的vid码 五 通过腾讯视频地址,盗播获取真实播放地址对比 用腾讯视频插件播放视频,是有前置广告的,可以花钱去广告,具体费用看该网站:https://v.qq.com/open 网上也有通过腾讯的视频地址盗播获取该视频真实播放地址的方法,将地址直接写到video标签就行.而且播放时没有广告,怎么说这种方法试过,毕竟上面的Q&A截图里人家说了,这种方法弊端,毕竟违规.仁者见仁吧 我没用这个方法主要是这个方法是有时效的,腾讯视频的地址是动态生成的,等token有效期一过,你的盗播地址就要跟着换,视频少还好,我这个是给电商加的视频播放,那么多商品,得换死... 六 槽点 1.视频广告,哎,不多说了 2.本来我得视频占比是750rpx:750rpx 结果这样得话下面得播放控制条显示不全,所以成了750x720 3.放到轮播图里广告结束时,跳过广告那个按钮会显示到其他轮播图上 七 建议 这个插件是真的好,算是官方得良心产品了,建议是开放视频上传接口,你说说这么好用得东西,唯一让客户不买单的就是我上传个视频还要到腾讯视频里上传.毕竟你有上传接口,共享出来,资质得话可以和小程序得APPID绑定啊 用token验证,毕竟能上线小程序得都能通过实名认证么 ,如果开放了上传接口,基本以后小程序根本不需要为播放视频开发任何东西了,你可以广告收费啊,毕竟现在也是收费去广告,大不了多收点,这点钱比起我们开发代码,买服务器空间,增加带宽,增加CDN得钱,根本不算什么
2021-04-15 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - 如何提升你的云函数性能
在使用云开发一段时间后,你一定会遇见一个问题:虽然云函数非常的方便,但我的云函数似乎性能不够好,为什么我的云函数每次加载都需 2 ~ 3 秒种,时间太长了!。 这篇文章,就来告诉你,应该如何提升你的云函数性能。 如何了解云函数运行情况? 在了解如何优化云函数的运行情况之前, 我们需要先了解,如何查看当前的云函数运行情况,这样才能有个对比。 [图片] 打开小程序开发者工具,并打开你的项目 进入到你要调试的页面,打开调试器 调用云函数,并在调试器中切换到 Network 页面,找到你的请求。 点击你的请求,然后切换到 Timing 页面,查看具体的情况。 在这个页面中,你可以理解其中的 Waiting(TTFB) 是你发起请求到你接收到返回结果的第一个字节的时间,简单的来说,就是服务器计算结果需要花费的时间。而下方的 Content Download 则是下载内容所需的时间,你可以理解是表现出网络速度快慢的数据。 总结来说,就是如果 Waiting TTFB 的值比较大,你就去优化云函数性能。如果 Content Download 的数值毕竟大,你就需要优化网络情况 优化 Waiting TTFB 云函数的运行机制 Waiting TTFB 的优化是云函数性能端的优化,那么在优化之前,我们就需要先来了解一下云函数的运行机制,以便帮助我们了解应该如何去进行性能优化。 [图片] 在蕴含运行时,具体的顺序是这样的 用户发起请求,请求发送到云开发的后台 云开发后台的调度器将请求分发给下方的执行的 worker 、容器 容器创建环境、下载代码 执行代码 在这个过程中,发起请求到云开发、调度器调度速度、调度器传递信息到容器、函数调用等,都是可以优化的,但是我们在具体的使用过程中。这些大都需要由云开发的工作人员来完成,对于我们自己来说,只能去尽可能的优化容器内部到代码层面的东西。 接下来,我们可以看看更细致的调用逻辑。 [图片] 在云开发中,我们可以将调用分为三种类型: 冷启动:图中的红色阶段,需要重新创建容器、下载代码,耗时最长 温启动:图中的黄色阶段,需要下载代码,耗时较长 热启动:图中的蓝色阶段,不需要下载代码,耗时最短 我们可以看到,最快的,是热启动,函数不需要创建容器,不需要启动函数就可以完成执行,显然比要创建容器或要下载代码的温启动和冷启动速度更快。这样,我们就得到了优化云函数性能的第一个方法 1. 让你的云函数每次调用都走热启动 当我们可以让我们的云函数的每一次调用都走热启动,少了容器的创建和函数的部署,请求的速度理所当然的要比冷启动和温启动更快。 我们可以测试一下,我设置每秒调用一次云函数,看看 TTFB 的变化。 [代码]setInterval(()=>{wx.cloud.callFunction({name:'profile'})},1000) [代码] 函数内代码是默认创建的云函数代码。 则对应的执行效果如下 [图片] 可以看到,函数的执行时间从第一次的 1.2s 降低到了 200ms左右,性能提升了 80%,我们仅仅是简单的提升了函数的调用频次,就可以实现提升函数的调用性能,这就是热启动带给我们的价值。 实施方案 如果你需要足够高的性能,不妨借助云开发的定时器,定期唤起你的容器,从而为你的容器保活,确保你的函数时刻被热启动。 2. 缩小你的函数大小 在前面我们曾介绍过,云函数在启动过程中,会创建容器和下载代码。创建容器的过程对于开发者来说不可控,不过我们可以使用一些方法,缩小我们的代码,提升代码的下载速度,比如说,缩小我们的函数代码。 这里我们可以做个测试,这里我创建了两个函数,两个函数的代码完全一致,不同的是,在实验组的函数中,我加入了一个 temp 变量的声明,这个变量的值是一个非常长的字符串,从而使得两个函数的大小分别是 68K 和 4K。 接下来,我们看看二者的执行时间。 [图片] 我们会发现,几乎没有差距的代码,因为加入了变量声明的因素,在性能上会略慢几毫秒,后续随着容器的不断复用,函数的之间的差距也越来越小,几乎可以忽视。 实施方案 对于你的代码,要尽可能的精炼,减少无用的代码,减少代码下载所需时间。 3. 削减不需要的 Package 除了下载代码以外,还需要下载 Node 环境运行所需的依赖包,虽然云开发可能针对 Node Modules 已经做了缓存,但依然存在下载的时间差区别,这里我也做了一个实验。 空包:什么都没装,把 wx-server-sdk 都卸载掉了。 复杂包:装了 Mongoose、sequelize、sails 等依赖的包。 函数逻辑上也相差无几,都是返回 Event ,则结果如下 [图片] 我们发现,前三次可能是因为涉及到依赖包的下载问题,所以前三次的时长大小对比特别的明显,而从第四次开始,二者的区别就不大了,可能是因为依赖已经完成了缓存,所以可以直接使用缓存来完成函数的执行。 实施方案 你可以选择看看你的 package.json ,看看其中是否有你不需要的依赖,将其删除,仅保留有需要的依赖,可以有效提升你的代码执行速度。 优化 Content Download 如果你想要优化 Content Download ,核心需要优化的是两个点: 手机到服务端的节点的距离和速度 内容的大小 前者一般来说,你可以通过切换不同的网络环境来实现优化,比如从 3G 切换到 4G ,从 4G 升级到 5G,这些都可以提升你的手机到服务端节点之间的速度。 此外,还可以借助内容分发网络 CDN 能力来完成缩小你到服务端节点之间的距离,不过对于云函数来说,因为你不可控,无法控制,所以这一点不再谈。 这里补充一句,云开发的文件存储都是有 CDN 的,因此,你通过云存储下载的文件才会比别人更快。 后者则一般通过调整代码来完成,比如只返回必须的资源,对于不需要的内容,不再返回,或压缩返回。 总结 最后,我们回顾一下这篇文章中介绍的优化云函数的方法: 函数下载性能优化 保持函数容器的热启动,提升函数启动性能 缩小函数大小,提升代码下载速度 削减不必要的包,减少依赖大小 网络优化 使用更好的网络,比如 Wi-Fi 云函数中仅返回所需要的内容,减少下载时间。 以上这些方法,你都在你的函数中试过么?有没有其他的优化方法?欢迎你与我分享。
2019-12-08 - 【能力体验】“后台持续定位”接口的使用与踩坑
在小程序基础库 v2.8.0 版本中,新增了小程序后台持续定位功能,没错就是这个接口 wx.startLocationUpdateBackground(Object object)。对于一些有这方面需求的项目来说,这无疑是个好消息!刚好我有个项目刚好需要用到这个功能,这里就分享一下使用的体验吧。 [图片] 一、使用前提醒 要使用这个能力,官方文档有以下提醒: 1、基础库 2.8.0 开始支持,低版本需做兼容处理。 2、调用前需要 用户授权 scope.userLocationBackground (需要写一个button供用户点击) 除此之外,你还需要知道: 这个接口仅支持在真机上调试! 因此写代码时可以先用wx.startLocationUpdate(前台时接收位置消息)替代,在运行没问题后再换回并在真机测试。 二、使用时感受 微信团队在公众号里说“满足线路导航、路线记录等服务场景下,小程序需要长时间持续定位来提供服务”,但在实际情况中是,小程序很难实现所谓的“长时间”(有时甚至不到5分钟)。这样就会导致我们记录的数据还没来得及上传就丢失了。 当我发现这个问题,第一时间是抱怨为什么小程序销毁前不能给我们返回一个提醒。但思考后才理解:微信APP自身都不能确保“长时间”在后台运行,还怎么样保证小程序的“长时间”运行呢?(如被一些软件清理了) 三、使用后经验 既然无法避免突发性关闭,又不宜频繁上传地点数据。那么,我们就只能用到缓存了!以实现类似断线重连的功能。 [图片] 实现效果: [图片] 代码:(仅供参考) [代码] var points_map = [ ] // 实时绘制地图 var points_yun = [ ] // 云开发需要的格式 var point Page({ data: { polyline: [{ points: points_map, color: '#FFA500', width: 3 }], }, onLoad: function (options) { var that = this wx.getStorage({ key: 'TimeStamp', success(res) { console.log('有缓存') wx.getStorage({ key: 'points_yun', success(res) { points_yun = res.data } }) wx.showModal({ content: '检测到您有一个未完成的巡护记录!', cancelText: '不保存', cancelColor: '#DC143C', confirmText: '继续巡护', confirmColor: '#228B22', success(k) { if (k.confirm) { console.log('用户点击确定-继续巡护') that.setData({ TimeStamp: res.data }) that.GoContinue() } else if (k.cancel) { console.log('用户点击取消-不保存') wx.removeStorage({ key: 'TimeStamp' }) //删缓存 wx.removeStorage({ key: 'points_yun' }) points_yun = [] } } }) }, fail(res) { console.log('无缓存') points_yun = [] } }) }, Go: function () { // 开始巡护(首次) var that = this wx.startLocationUpdateBackground({ success(res) { var TimeStamp = (new Date()).valueOf() that.GetNowGeo() wx.setStorage({ //设置缓存(TimeStamp) key: "TimeStamp", data: TimeStamp }) }, fail() { // 这里弹窗引导用户授权使用地理位置 } }) }, GoContinue: function () { // 开始巡护(再续) var that = this wx.startLocationUpdateBackground({ success(res) { that.GetNowGeo() }, fail() { // 这里弹窗引导用户授权使用地理位置 } }) }, End: function () { var that = this wx.stopLocationUpdate() clearInterval(this.data.setInter) wx.showModal({ title: '', content: '是否要上传数据?', success(res) { if (res.confirm) { that.updateGeo() } } }) }, GetNowGeo: function () { var that = this wx.onLocationChange(function (res) { point = { latitude: res.latitude, longitude: res.longitude } }) this.data.setInter = setInterval(function () { if (points_map.length == 0) { points_yun.push([]) } points_map.push(point); //画地图 that.setData({ 'polyline[0].points': points_map }) var n = [point.longitude, point.latitude] // 云开发数据库需要的格式 var r = points_yun.length - 1 points_yun[r].push(n) wx.setStorage({ // 设置缓存(路程数据) key: "points_yun", data: points_yun }) }, 6000); }, updateGeo: function () { var that = this wx.showLoading({ title: '数据上传中', }) db.collection('patrol_geo').add({ // 云开发上传 data: { location: { type: 'MultiLineString', coordinates: points_yun } }, success: res => { wx.showToast({ title: '上传成功' }) wx.removeStorage({ key: 'TimeStamp' }) // 清理缓存 wx.removeStorage({ key: 'points_yun' }) }, fail: res => { wx.showToast({ title: '上传失败,请重新操作!' }) console.log(res) } }) }, }) [代码]
2019-09-26 - 用云开发制作教务助手小程序丨实战
▌项目背景 本项目由一人承担从后端到前端的构思以及开发,下面我就讲讲从教务助手小程序的构思到开发实现(基于云开发)。 1、灵感来源 教务小程序的灵感来源:用完即走,查个成绩和课表,无需下载app或去翻看公众号内的历史内容。 加上本人很久以前就想实现开发一个类似的app,但app的开发对于开发小白不太友好,不知从何下手!幸好 小程序·云开发 的出现解决了我的需求,它的低入门门槛和免后端运维等优势让非科班出身的我也能快速动手开发一款应用类小程序。 2、构思 教务小程序需要核心就是: 成绩查询、课表查询、教务通知查询 ! 那么问题来了,学校教务处只有网页版,教务小程序数据从何而来呢? 经过一系列思考,百度各种问题,思路就来了: 后端模拟登陆——拿到页面数据——整理数据——反馈到小程序前端渲染 大概结构如下: [图片] ▌项目开发 1、后端 后端的实现 完全基于云开发。 部分目录: [图片] 采用云开发后端node.js语言,主要利用模块有: Router模块: [代码]const cloud = require('tcb-admin-node'); // npm install tcb-router const TcbRouter = require('tcb-router'); cloud.init({ env: '//' }) const db = cloud.database(); const _ = db.command; // 云函数入口函数 exports.main = async (event, context) => { const app = new TcbRouter({ event }); /** 教务处登陆 eg*/ app.router('login', async (ctx, next) => { const test = require('login/login.js'); ctx.body = test.main(event, context); }); /**查取成绩*/ app.router('getpoint', async (ctx, next) => { const logList = require('getpoint/index.js'); ctx.body = logList.main(event, context); }); /**学术活动*/ app.router('academic', async (ctx, next) => { const userList = require('schoolnews/academic.js'); ctx.body = userList.main(event, context); }); app.router('xsxx', async (ctx, next) => { const userList = require('schoolnews/xsxx.js'); ctx.body = userList.main(event, context); }); return app.serve(); } [代码] Cherrio实现课表成绩等网页解析: [代码]const cloud = require('tcb-admin-node') const rp = require('request-promise'); var cheerio = require("cheerio"); cloud.init() module.exports = { main: async (event, context) => { var url ='URL' var res = await rp({method: 'get',uri: url,json: false}).then((body) => { var academic = []; var $ = cheerio.load(body); $('.fl').find('dl').each(function (i, elem) { //业务代码未写 /** **/ academic.push({date: date,time:time,title:title,speaker:speaker,place:place,link:link}) }); return academic }).catch(err => { return err; }) return res } } [代码] 数据库access_token定时修改 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk'),rp = require('request-promise'),key=require('key.js') cloud.init({ env: "//" }) //指定数据库环境 const db = cloud.database({ env: "//" }), _ = db.command; // 云函数入口函数 exports.main = async (event, context) => { try { var res = await rp( { method: 'get', uri: 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' key.APPID '&secret=' key.APPSECRET, qs: {},//参数 headers: {},//请求头 json: true //是否json数据 } ).then((body) => { return body }).catch(err => { return {errmsg:"rp函数获取失败"} }) /*将获取的access_token存到数据库*/ console.log(res) if (res.hasOwnProperty('access_token')) { await db.collection('key').where({ type: "accesstoken" }).update({ data: { accesstoken: res.access_token, datearray: _.unshift(new Date(new Date().getTime())), num: _.inc(1) } }) } else { console.log("err错误" res) } } catch (err) { console.log(err) } } [代码] 此外还借助了其他模块实现登陆、数据处理(课表等数据格式化)、云开发数据库操作(用户信息储存,消息发布)、用户权限鉴定(确保后台信息安全)等,在此就不赘述。 2、前端 [图片] 小白就是“简单粗暴”的进行各种if、var操作; 部分详细介绍如下: ① 课程表: 实现了一键导入(其实课程表这一功能可以单独形成一个通用的小程序上线),每天一卡片形式在首页提醒:今天有什么课,上完没有? [图片] ②主题全局替换: 支持自定义主题色,给用户自定义能力。 [图片] ③校历: 利用了插件【极点日历】再加以美化。 [图片] ▌感悟 一个从小白到从后端到前端到UI全部自己写的入门者参赛的心路历程就这么多了,目前源码暂不开放,对本小程序有疑问与建议均可在留言,同时希望大家能够利用好小程序实现自己的想法和创意! 源码地址 https://github.com/TencentCloudBase/Good-practice-tutorial-recommended 如果你想要了解更多关于云开发CloudBase相关的技术故事/技术实战经验,请扫码关注【腾讯云云开发】公众号~ [图片]
2019-12-03 - 微信小程序WXS 有什么用?
微信WXS介绍 WXS(WeiXin Script)是微信创造的一套脚本语言,它的官方说法是:“WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致”。 那微信为何要脱离 JavaScript ,单独创造一套语言呢?这要从微信小程序的底层逻辑(运行环境)讲起。 小程序的运行环境分为逻辑层和视图层,分别由2个线程管理,其中: WXML 模板和 WXSS 样式工作在视图层,界面使用 WebView 进行渲染 JavaScript代码工作在逻辑层,运行在JsCore或v8里 小程序在视图层与逻辑层两个线程间提供了数据传输和事件系统。这样的分离设计,带来了显而易见的好处: 逻辑和视图分离,即使业务逻辑计算非常繁忙,也不会阻塞渲染和用户在视图层上的交互 但同时也带来了明显的坏处: 视图层(webview)中不能运行JS,而逻辑层JS又无法直接修改页面DOM,数据更新及事件系统只能靠线程间通讯,但跨线程通信的成本极高,特别是需要频繁通信的场景 从本质来讲,wxs是一种被限制过的、运行在视图层webview里的js。它并不是真的发明了一种新语言。 WXS特征及适用场景 WXS具备如下特征: WXS是可以在视图层(webview)中运行的JS WXS无法直接修改业务数据,仅能设置当前组件的class和style,或者对数据进行格式化。 WXS是被限制过的JavaScript,可以进行一些简单的逻辑运算 故可以得出WXS的适用场景,主要包括: 数据格式处理,比如文本、日期格式化,或者国际化。 类比vue 微信小程序的wxs语法 可以当做vue的计算属性和vue filter 使用。因为wxs中的函数可以写在{{ }}中 。 例如: 可用在 <view>{{ foo() }}</view> 知识背景 其实今天总结wxs是有真实背景的,在周末我在开发在线答题小程序的过程中,需要在一个radio中存放一个字符串,但是这个字符串需要通过JSON.stringfy转化成字符串,但是微信小程序本身的wxml绑定{{}}并不支持,所以才有了本文。 就是说在wxml中如果需要对小程序的this.data数据进行额外的操作,就可以用wxs,这个在vue里面就是计算属性。 帖子来源 https://developers.weixin.qq.com/community/develop/doc/000a2ab8840b9011be89e3d3d56c00
2019-12-02 - 使用animation实现列表顺序加载动画
[图片] 之前使用纯transition实现动画时, 发现在部分手机上效果不是很好, 会有不流畅掉帧的现象! 现在换animation方法实现, 不知各位是否有什么高见, 大家一起交流交流 代码片段如下 https://developers.weixin.qq.com/s/pEBv6emG7Cdt
2019-11-29 - 揭开微信小程序 Kbone 的神秘面纱
微信小程序诞生至今,渗透到用户生活的方方面面,包括餐厅点餐,网上购物,乘车出行,挂号就医…… 做为开发者,你是否有这样的烦恼? “当 Web 项目完成之后,产品也想搭一套一样的在小程序端。” 这时的你,是不是有种透心凉的感觉?再搭一套同样的项目,肯定会有成本。比如同时维护两套类似的代码,这对于开发者来说是相当头疼的一件事情!! 针对上述的问题,微信小程序推出了 Kbone 来解决微信小程序的同构问题。Kbone是什么,以及做了什么,我们继续来一探究竟。 Kbone 是什么 Kbone 是一个致力于微信小程序和 Web 端同构的解决方案。 微信小程序的底层模型和 Web 端不同,我们想直接把 Web 端的代码挪到小程序环境内执行是不可能的。Kbone 的诞生就是为了解决这个问题,它实现了一个适配器,在适配层里模拟出了浏览器环境,让 Web 端的代码可以不做什么改动便可运行在小程序里。 这样,我们就可以借助 Kbone 快速实现 Web 项目转化为微信小程序项目。 Kbone的优劣 你可能想问:“市面上同构的方案那么多?我为什么要选择 Kbone 呢?” 这里,我们来谈谈 Kbone 到底有什么优势。 1)大部分流行的前端框架都能够在 Kbone 上运行,比如 Vue、React、Preact 等。 2)支持更为完整的前端框架特性,因为 Kbone 不会对框架底层进行删改(比如 Vue 中的 v-html 指令、Vue-router 插件)。 3)提供了常用的 DOM/BOM 接口,让用户代码无需做太大改动便可从 Web 端迁移到小程序端。 4)在小程序端运行时,仍然可以使用小程序本身的特性(比如像 live-player 内置组件、分包功能)。 5)提供了一些 Dom 扩展接口,让一些无法完美兼容到小程序端的接口也有替代使用方案(比如 getComputedStyle 接口)。 那么,Kbone 就没有劣势吗? 不是所有的方案都是无懈可击的,就像每个人都有优缺点,Kbone 也不例外。 Kbone 是使用一定的性能损耗来换取更为全面的 Web 端特性支持。 如何选择同构方案? 因为 Kbone 会损耗一定的性能,所以我们建议你这样选择: 1)如果你对小程序的性能特别苛刻,建议直接使用原生小程序开发。 2)如果你的页面节点数量特别多(通常在 1000 节点以上),同时还要保证在节点数无限上涨时仍然有稳定的渲染性能的话,可以尝试一下业内采用静态模板转译的方案。 3)其他情况就可以直接采用 Kbone 了。 快速上手 说了这么多,你肯定想知道 Kbone 到底是怎么用的。下面我们就来看看~ 如果,你的项目还没有开始,那么推荐你使用 kbone-cli 来快速开发。只需两步: 1)安装 kbone-cli [代码]npm install -g kbone-cli [代码] 2)创建项目 [代码]kbone init my-app [代码] [图片] [图片] 项目初始化成功后,就可以按照 [代码]README.md[代码] 的指引进行开发~ 自行搭建 如果你不想要使用官方提供的模板,想要更灵活地搭建自己的项目,又或者是想对已有的项目进行改造,那么只需要自己补充对应配置来实现 Kbone 项目的构建。 一般需要补充两个配置: 构建到小程序代码的 webpack 配置。 使用 webpack 构建中使用到的特殊插件 mp-webpack-plugin 配置。 具体配置方式和操作流程 点此查看。 最后 如果你对 Kbone 感兴趣或者有相关问题需要咨询, 欢迎加入 Kbone 技术交流 QQ 群:926335938 。
2019-12-26 - 云开发如何实现管理员通知消息
需求描述 小程序目前的主要能力还都在小程序端实现,但是我们在进行开发的小程序不可能只有小程序端能力,我们也会有一些管理端能力。比如说,当用户在小程序中提交了消息以后,我们的小程序应该可以通知到小程序的管理员,以便让管理员进行下一步操作。 解决方案 架构说明 由于小程序本身不支持长久性的消息通知能力,因此,我们可以考虑借助一些第三方的服务和能力,来完成我们自己的需求。 这个需求很适合使用小程序新发布的长期订阅消息能力,但是目前该能力开放的类目还不足以支持我们的需要。 一般而言,使用短信是我们目前到达率比较高的能力,且更为普遍的能力,其他通道的能力大多受限或不符合国情,为了确保通知信息的到达率,我们这篇文章就使用短信来完成需求。 架构图示 [图片] 具体操作 1. 开通腾讯云短信服务并获取配置信息 我们想要发送短信,就需要先有一个短信服务,用于发送短信,这里我们可以使用腾讯云提供的云短信服务来发送短信。 开通腾讯云短信,并创建应用 首先,你需要访问 https://console.cloud.tencent.com/smsv2 ,点击开通腾讯云·云短信。 在开通完成后,点击界面中的【添加应用】,添加一个新的短信应用,你可以根据自己的实际情况,添加短信应用的名称和简介。 [图片] 获取 AppID、App Key 添加完成后,点击你创建好的应用,进入到应用详情页,在应用的详情页中的应用信息栏目中,你可以找到 AppID 和 AppKey ,复制并保存这两个值,稍候我们会用到。 [图片] 2. 配置短信模板、短信签名 开通了腾讯云短信服务以后,我们需要去创建短信模板,以及短信签名 腾讯云短信并不是让你随意发所有的内容的,而是你需要创建一个模板,并使用特定的模板来完成短信的发送。 短信签名则是原来让收到短信的用户知道他所收到的短信来自于他的那一个服务,一般来说,设置为产品的品名。 在腾讯云控制台中,进入到【云短信】控制台 创建短信签名 首先,点击【国内短信】,进入到短信的页面,点击【创建签名】,然后在弹出的窗口中输入你的签名的具体信息,比如这里我就是以公众号【程序百晓生】来创建签名。 [图片] 签名创建完成后,你需要等待腾讯云官方的审核,审核通过以后,你添加的签名才可以被使用。 创建短信模板 创建完签名,你需要创建一个短信的正文模板,用于发送短信。 输入模板名称、短信类型,然后选择标准模板中的模板,这里我们选择“您有新的{1}订单,请注意查收!”这个模板。 除了使用标准模板,你也可以自己编写一个模板,为了方便文章撰写,这里使用标准模板。 [图片] 然后点击提交,等待审核就可以了。 3.编写云函数发送短信 在完成了基础的配置后,我们在微信开发者工具中实现一个云函数,用于调用腾讯云的短信服务,实现具体的通知。 首先,我们创建一个新的云函数,名为 [代码]notifyAdmin[代码],意为用于通知管理员的云函数。 [图片] 然后,选择我们刚刚创建的 [代码]notifyAdmin[代码] 云函数,在函数上右击,选择【在终端中打开】,进入到控制台,并输入如下命令,安装所需的短信 SDK。 [代码]npm install --save sms-node-sdk [代码] [图片] 然后,修改云函数的 [代码]index.js[代码],加入如下代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const { SmsClient } = require('sms-node-sdk'); const AppID = 1400286810; // SDK AppID是1400开头 // 短信应用SDK AppKey ,替换为你自己的 AppKey const AppKey = 'xxxx'; // 需要发送短信的手机号码 const phoneNumber = '10000000'; // 短信模板ID,需要在短信应用中申请 const templId = 476457; // 签名,替换为你自己申请的签名 const smsSign = '程序百晓生'; // 实例化smsClient cloud.init() // 云函数入口函数 exports.main = async (event, context) => { let orderId = event.orderId; let smsClient = new SmsClient({ AppID, AppKey }); return await smsClient.init({ action: 'SmsSingleSendTemplate', data: { nationCode: '86', phoneNumber, templId: templId, params: [orderId], sign: smsSign // 签名参数未提供或者为空时,会使用默认签名发送短信 } }) } [代码] 完成代码的修改后,就可以部署你的云函数了,右键你的云函数,选择【上传并部署云函数:云端安装依赖】 4. 在小程序端触发短信 在前面我们提到,在一些特定的场景下,我们希望用户的操作可以给管理员发送消息通知。在具体的实现的时候,我们可以根据自己的实际业务需求,来设定我们的通知发送的条件,比如说,在用户支付成功后发送消息,则相关代码如下: [代码]let orderId = 'this is a orderId'; wx.requestPayment({ success:res => { console.log("User Payment Success"); // 调用云函数发送短信 wx.cloud.callFunction({ name:"notifyAdmin", data:{ orderId: orderId } }); } }) [代码] 总结 经过本次的分享,我们了解到了如何借助短信服务,实现云开发的后台通知能力,实际上,除了短信服务,你还可以借助一些其他的工具,比如邮件、企业微信机器人等能力,实现后台管理信息的推送。 明天,我们将分享如何借助通过微信发送订单消息。
2019-11-19 - 像后端一样做微信云开发
接触云开发两个多星期了,总结出一套类似后端的MVC结构。 首先我们需要用到的是官方提供的tcb-router。 云函数入口没什么特别的,正常使用,无非是在原有基础上进行了模块化。 首先创建三个文件夹:entity、service、dao; 熟悉后端开发的同学都知道:entity主要是存放一些实体类。我们看下具体有什么吧 [图片] 上图是一个通用返回的实体对象,这样一来,在返回客户端数据的时候,直接实例化出来一个CommonResponse对象 [图片] [图片] 讲完实体类,接下来要说的是dao层 这层是数据层,主要做一些数据库的操作: 查询参数通过service层传入,这样数据库操作层被独立出来,增加了复用性 [图片] 紧接着要登场的是service层 service层里面主要是一些业务逻辑的处理,拿到dao层返回的数据以后,对数据进行处理后返回给客户端 [图片] 在来看看index的入口 [图片] 这样 逻辑是不是清晰了很多? 不管是service层还是dao层都可以完美地被复用,而且便于维护
2019-11-06 - 小程序·云开发之数据库自动备份丨云开发101
小程序云开发之数据库自动备份 小程序云开发提供了方便的云数据库供我们直接使用,云开发使用了腾讯云提供的云数据库,拥有完善的数据保障机制,无需担心数据丢失。但是,我们还是不可避免的会担心数据库中数据的安全,比如不小心删除了数据集合,写入了脏数据等。 还好,云开发控制台提供了数据集合的导出,导入功能,我们可以手动备份数据库。不过,总是手动备份数据库也太麻烦了点,所有重复的事情都应该让代码去解决,下面我们就说说怎么搞定云开发数据库自动备份。 通过查阅微信的文档,可以发现云开发提供了数据导出接口databaseMigrateExport [代码]POST https://api.weixin.qq.com/tcb/databasemigrateexport?access_token=ACCESS_TOKEN [代码] 通过这个接口,结合云函数的定时触发功能,我们就可以做数据库定时自动备份了。梳理一下大致的流程: 创建一个定时触发的云函数 云函数调用接口,导出数据库备份文件 将备份文件上传到云存储中以供使用 1. 获取 access_token 调用微信的接口需要 access_token,所以我们首先要获取 access_token。通过文档了解到使用 auth.getAccessToken 接口可以用小程序的 appid 和 secret 获取 access_token。 [代码]// 获取 access_token request.get( `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`, (err, res, body) => { if (err) { // 处理错误 return; } const data = JSON.parse(body); // data.access_token } ); [代码] 2. 创建数据库导出任务 获取 access_token 后,就可以使用 [代码]databaseMigrateExport[代码] 接口导出数据进行备份。 [代码]databaseMigrateExport[代码] 接口会创建一个数据库导出任务,并返回一个 job_id,这个 job_id 怎么用我们下面再说。显然数据库的数据导出并不是同步的,而是需要一定时间的,数据量越大导出所要花费的时间就越多,个人实测,2W 条记录,2M 大小,导出大概需要 3~5 S。 调用 [代码]databaseMigrateExport[代码] 接口需要传入环境 Id,存储文件路径,导出文件类型(1 为 JSON,2 为 CSV),以及一个 query 查询语句。 因为我们是做数据库备份,所以这里就导出 JSON 类型的数据,兼容性更好。需要备份的数据可以用 query 来约束,这里还是很灵活的,既可以是整个集合的数据,也可以是指定的部分数据,这里我们就使用 [代码]db.collection('data').get()[代码] 备份 data 集合的全部数据。同时我们使用当前时间作为文件名,方便以后使用时查找。 [代码]request.post( `https://api.weixin.qq.com/tcb/databasemigrateexport?access_token=${accessToken}`, { body: JSON.stringify({ env, file_path: `${date}.json`, file_type: '1', query: 'db.collection("data").get()' }) }, (err, res, body) => { if (err) { // 处理错误 return; } const data = JSON.parse(body); // data.job_id } ); [代码] 3. 查询任务状态,获取文件地址 在创建号数据库导出任务后,我们会得到一个 job_id,如果导出集合比较大,就会花费较长时间,这时我们可以使用 databaseMigrateQueryInfo 接口查询数据库导出的进度。 当导出完成后,会返回一个 [代码]file_url[代码],即可以下载数据库导出文件的临时链接。 [代码]request.post( `https://api.weixin.qq.com/tcb/databasemigratequeryinfo?access_token=${accessToken}`, { body: JSON.stringify({ env, job_id: jobId }) }, (err, res, body) => { if (err) { reject(err); } const data = JSON.parse(body); // data.file_url } ); [代码] 获取到文件下载链接之后,我们可以将文件下载下来,存入到自己的云存储中,做备份使用。如果不需要长时间的保留备份,就可以不用下载文件,只需要将 job_id 存储起来,当需要恢复备份的时候,通过 job_id 查询到新的链接,下载数据恢复即可。 至于 job_id 存在哪,就看个人想法了,这里就选择存放在数据库里。 [代码]await db.collection('db_back_info').add({ data: { date: new Date(), jobId: job_id } }); [代码] 4. 函数定时触发器 云函数支持定时触发器,可以按照设定的时间自动执行。云开发的定时触发器采用的 [代码]Cron[代码] 表达式语法,最大精度可以做的秒级,详细的使用方法可以参考官方文档:定时触发器 | 微信开放文档 这里我们配置函数每天凌晨 2 点触发,这样就可以每天都对数据库进行备份。在云函数目录下新建 [代码]config.json[代码]文件,写入如下内容: [代码]{ "triggers": [ { "name": "dbTrigger", "type": "timer", "config": "0 0 2 * * * *" } ] } [代码] 完整代码 最后,贴出可以在云函数中使用的完整代码,只需要创建一个定时触发的云函数,并设置好相关的环境变量即可使用 appid secret backupColl:需要备份的集合名称,如 ‘data’ backupInfoColl:存储备份信息的集合名称,如 ‘db_back_info’ 注意,云函数的默认超时时间是 3 秒,创建备份函数时,建议将超时时间设定到最大值 20S,留有足够的时间查询任务结果。 [代码]/* eslint-disable */ const request = require('request'); const cloud = require('wx-server-sdk'); // 环境变量 const env = 'xxxx'; cloud.init({ env }); // 换取 access_token async function getAccessToken(appid, secret) { return new Promise((resolve, reject) => { request.get( `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`, (err, res, body) => { if (err) { reject(err); return; } resolve(JSON.parse(body)); } ); }); } // 创建导出任务 async function createExportJob(accessToken, collection) { const date = new Date().toISOString(); return new Promise((resolve, reject) => { request.post( `https://api.weixin.qq.com/tcb/databasemigrateexport?access_token=${accessToken}`, { body: JSON.stringify({ env, file_path: `${date}.json`, file_type: '1', query: `db.collection("${collection}").get()` }) }, (err, res, body) => { if (err) { reject(err); } resolve(JSON.parse(body)); } ); }); } // 查询导出任务状态 async function waitJobFinished(accessToken, jobId) { return new Promise((resolve, reject) => { // 轮训任务状态 const timer = setInterval(() => { request.post( `https://api.weixin.qq.com/tcb/databasemigratequeryinfo?access_token=${accessToken}`, { body: JSON.stringify({ env, job_id: jobId }) }, (err, res, body) => { if (err) { reject(err); } const { status, file_url } = JSON.parse(body); console.log('查询'); if (status === 'success') { clearInterval(timer); resolve(file_url); } } ); }, 500); }); } exports.main = async (event, context) => { // 从云函数环境变量中读取 appid 和 secret 以及数据集合 const { appid, secret, backupColl, backupInfoColl } = process.env; const db = cloud.database(); try { // 获取 access_token const { errmsg, access_token } = await getAccessToken(appid, secret); if (errmsg && errcode !== 0) { throw new Error(`获取 access_token 失败:${errmsg}` || '获取 access_token 为空'); } // 导出数据库 const { errmsg: jobErrMsg, errcode: jobErrCode, job_id } = await createExportJob(access_token, backupColl); // 打印到日志中 console.log(job_id); if (jobErrCode !== 0) { throw new Error(`创建数据库备份任务失败:${jobErrMsg}`); } // 将任务数据存入数据库 const res = await db.collection('db_back_info').add({ data: { date: new Date(), jobId: job_id } }); // 等待任务完成 const fileUrl = await waitJobFinished(access_token, job_id); console.log('导出成功', fileUrl); // 存储到数据库 await db .collection(backupInfoColl) .doc(res._id) .update({ data: { fileUrl } }); } catch (e) { throw new Error(`导出数据库异常:${e.message}`); } }; [代码] 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心! [图片]
2019-10-12