- 小程序使用Npm引入【VANT】组件全程教学
相信使用VANT的伙伴们肯定多多少少碰到过跟我一样的问题,楼主整理了一下,大概以下两点 第1.找不到依赖包文件夹node_modules 第2.各种文件路径报错(这个很恶心,有人说是开发者工具文件丢失,解决一个又来一个,事后楼主发现并非如此) 好了,废话不多说,跟着我的步骤一步步的走,基本都会成功。PS:可以在已经开发的项目中引入,不需要重新创建项目 第一步:备份项目文件中的package.json文件。打开cmd,使用管理员权限运行,找到项目目录,npm init。这时候会重新生成package文件 第二步:npm i vant-weapp -S --production,安装VANT组件。这时候项目里会多出两个文件夹 miniprogram_npm 和 node_modules。node_modules文件夹在开发者工具里不会显示出来。然后点工具,构建NPM.再点详情,使用NPM模块[图片] 第三步:第三点很关键,由于很多教程以及官方教程都介绍了两种方法来安装VANT,其实这根本就是两个步骤而不是两种方法。也就是说前面我们是进行了第一步。那么第二步就是根据官方的git地址https://github.com/youzan/vant-weapp,来下载该压缩包。下载完成之后,将其中的dist文件夹放到miniprogram_npm文件夹下的vant-weapp文件夹里面,如图:(其它的可以都删掉)[图片] 第四步:在对应页面的json文件中引入,不要按官方的写法。官方是这么写的: { "usingComponents": {"van-button": "/path/to/vant-weapp/dist/button/index"} } 我们将其改为自己的路径 "usingComponents": { "van-button": "/miniprogram_npm/vant-weapp/dist/button/index" } 然后在wxml里写个按钮试试 [图片] 大功告成
2019-03-30 - 微信小程序前端开发踩坑——引入weui组件库
前言 今天在写微信小程序前端页面,想引入weui组件库来完成开发。结果按着官方文档来遇到了一堆问题,最后靠着不断百度查资料才最终解决。所以将过程记录一下,避免后面再遇到这类坑。 注意:本文默认读者已知道怎么使用npm 1. 初始化 以管理员身份运行命令行窗口(cmd),在cmd中进入项目的根目录。然后输入以下命令: [代码]npm init [代码] 后面一路按回车健即可,最终会在项目的根目录中创建出一个名为package.json的文件。 2. 安装weui组件库 在cmd中紧接着输入以下命令: [代码]npm install weui-miniprogram [代码] 命令执行完毕后会多出来一个node_modules文件夹,里面包含了weui组件库。 3. 构建npm 在微信开发者工具中,选择“工具”->“构建npm”。如无意外会出现类似“没有找到可以构建的NPM包……”这样的报错。 这时就需要在项目根目录找到package.config.json文件,修改相关的配置如下: [代码]{ ... "setting": { ... "packNpmManually": true, "packNpmRelationList": [ { "packageJsonPath": "./package.json", "miniprogramNpmDistDir": "./" } ] } } [代码] 继续在开发者工具中的“详情”->“本地设置”里检查是否勾选上“使用npm模块”选项,若没勾选则勾选上。 完成上述配置后,重新构建npm,即可构建完成。 4. 重启项目 在开发者工具中“项目”->“重新打开此项目”,完成对项目的重启。 注意:这一步非常重要!!!否则引入组件会提示找不到文件!!! 5. 引入wxss 在app.wxss中,引入weui库的wxss文件 [代码]@import 'miniprogram_npm/weui-miniprogram/weui-wxss/dist/style/weui.wxss'; [代码] 引入时要根据实际情况调整路径,但最长后缀均为 [代码]/weui-miniprogram/weui-wxss/dist/style/weui.wxss [代码] 6. 引入组件 在想要使用组件的页面对应的js文件中,对组件进行的引入。一定要注意自己项目的目录结构!!! [图片] 而官方文档的写法是 [图片] 如果直接照搬官方文档的写法,则忽略了目录结构,会报错!! 接着在要使用组件的页面对应的wxml文件中使用该组件即可 [代码]<mp-dialog title="test" show="{{true}}" bindbuttontap="tapDialogButton" buttons="{{[{text: '取消'}, {text: '确认'}]}}"> <view>test content</view> </mp-dialog> [代码] 效果如下: [图片] 后记 不得不说,前端开发的坑实在是太多了,上面记录的过程我摸索了一个多小时。看来平时一定要多注意总结才行,不然真的非常消耗时间!!! 创作不易,觉得有用麻烦点个赞,谢谢~~~
2022-01-11 - 微信小程序云开发教程-小程序端调用云函数
同学们大家好,我是小伊同学,前面我们已经做好了小程序的页面,在开发好了后端的接口之后,我们就需要对前后端进行联通,而在云开发下,这部分内容就是我们今天将要一起学习的——小程序端如何调用云函数。 [图片] 在微信小程序中,我们使用wx.request这个API来发送http请求,但对于云开发而言,我们的接口都是云函数,这时我们就需要使用wx.cloud.callFunction这个API来调用云函数。 [图片] wx.cloud.callFunction和第四章我们讲到的API的语法和格式规范一致,使用的基本格式为:花括号中需要填写name属性,属性值为要调用的云函数名称,为必填项。data字段用于填写我们发给服务器的数据,我们可以在花括号中定义我们要发送的变量名称及其值,可以有多条,使用逗号分隔。当然如果我们没有数据要传,这里也可以为空或者不写data这部分。 其次是success,fail和complete这三个回调函数,他们将分别在api请求成功、失败和完成时执行内部的逻辑。唯一不同的是,这里在success函数中,我们拿到的函数参数中将含有服务器发送给我们的数据,例如这里回调参数为res,那么我们通过res.data就能够访问到服务器返回的数据。这里的数据是指整体数据区,如果服务器返回了电话号码phonnumber字段,那么我们的写法应该是res.data.phonnumber。 [图片] 下面我们具体根据某个接口的接口文档填写一个调用云函数api的示例。以登录接口为例,我们拿到的接口文档如左边的表格所示,这里的返回数据我们暂时不考虑,下面我们将它填写到api中,我们将函数名称直接复制过来,复制到name中。然后我们看到输入数据有三个,分别是avatarUrl、gender和nickName,我们在data中进行定义并给定值。最后,我们需要说明一下success和fail函数。大家需要明白两个概念,一个是api调用成功和失败,另一个是服务器请求成功和失败。 一般,我们看实际的接口文档,在返回数据中,会有关于错误的说明,通常会通过一个数字类型错误码来标识,这个错误码是干什么的呢?就是让前端知道,比如提交的信息不全或者信息比对不一致等等,以便于我们对用户进行提示。对于这类错误,实际上我们请求api是成功的,意思是说我已经成功访问到服务器了,但是服务器请求失败了,因为服务器经过判断发现信息不正确或者不能跟微信服务器获取信息从而导致这个函数并未按照预期方式执行成功。 因此,这里就会产生两种成功和失败。我们这里的success和fail是指云函数调用的成功和失败,也就是是否成功访问到服务器,在成功访问的前提下,服务器验证和后会告诉我登录成功还是失败,这是该接口功能是否如期完成的成功和失败,也可以说是业务层面的成功和失败,如果成功一般错误码是0,如果失败会给出失败信息。因此我们必须在success函数中判断服务器返回的错误码,以对服务器请求成功和失败分别进行处理。 我们可以通过调用云函数来和服务器进行数据交换,从而完成小程序的各个功能。学习完这部分内容之后,同学们就可以真正具备开发一个小程序的能力了。在前后端打通之后,同学们同样不要忘记按照之前讲过的方法将代码发布上传哦~ 本节的内容就是这些,如果大家有什么问题,欢迎在文末留言。
2020-09-06 - 微信小程序云开发教程-JavaScript入门(1)-学习环境、函数、调试
从本小节开始,我们将开始学习JavaScript。 Javascript是一门在互联网上非常流行的脚本语言,可以直接在所有现代的浏览器里面执行,不需要提前编译。 可以用来做网站开发,以及小程序的前端和后端开发。所以我们学好js,后面的小程序开发将易如反掌。 [图片] 我们首先来学习一下,在js中,函数是如何定义的,比如我们要定义一个函数,实现2个数字的加法运算。首先需要使用关键词function来定义函数,接着是函数名,括号里的是该函数被调用时需要传递的参数。 我们需要注意的是,每条代码语句的最后,可以写分号,也可以不写; 最后是通过return关键词返回函数的执行结果。 [图片] 那么javascript是如何进行调试、修改bug的呢,我们一般使用console.log函数,该函数可以打印输出任何类型的数据。比如我们先调用我们之前定义的加法函数,将函数的执行结果赋值给sum,最后再调用console.log函数打印sum,就可以得到sum的值了,那么我们就可以根据这个值,判断我们定义的函数里是否存在bug了。 [图片] 学习js是不需要安装任何的软件,浏览器就是最好的学习和运行环境。 我们可以通过百度搜索:js在线,就可以找到非常好用的js代码运行工具 [图片] 将js代码输入到在线的一个编辑器中,点击运行,就可以直接出结果了。 [图片]
2020-08-17 - 微信小程序云开发教程-一文学会Json
本小节我们将学习什么是json [图片] 我们首先学习三个概念,什么是数据项,什么是对象,什么是数组。 每一个数据项由key和value组成,key也可以叫name,代表数据项的名称,value代表数据项的值,也可以叫数据项的内容。比如id:123456就是一个数据项,这个数据项的key是id,value是123456。最后需要注意的是,一个数据项中,key不是必须有的。 那什么是对象呢?对象是由一组至少存在一个key的数据项组成的,其被包含在一个大括号内。比如下面的这个对象,由两个数据项构成,id:123456和province:黑龙江。 最后,我们来看数组的定义,数组是由一组都没有key的数据项构成,其被包含在一个中括号内。比如下面的这个数据,由两个数据项构成,每个数据项都没有key。 [图片] 数据项、对象、数组是可以互相嵌套和组合在一起的,比如说下面的这个数据。 所以,什么是json,Json就是一种由数据项、对象、数组组成的数据存储格式。 每个json就是一个对象,最外层被一个大括号包着。 每个括号里面可以是数据项、也可以是数据、还可以再嵌套对象。 最后需要注意的是,数据项之间由英文逗号相隔。 [图片] 那么,如何访问对象里的值呢,假设有一个数据对象叫location,我们可以通过点的方式获取到province的值,也可以通过下标的方式获取到province的值。 [图片] 最后,我们来学习如何访问数组里的值呢,假设有一个数据对象叫location,我们可以通过点的方式先找到cities,再通过下标的方式获取到cities里面的第一项的值。
2020-08-17 - 微信小程序云开发教程-JavaScript入门(5)-函数异步同步
本小节我们将学习同步和异步的概念 [图片] 同步是指下一行代码必须等待上一行代码执行完成,才执行。 异步是指下一行代码不必等待上一行代码执行完成,才执行。 同步的优点是,关心代码的执行结果,按顺序执行,保证了结果的顺序性; 异步的优点是,不关心代码执行结果,异步执行,可以提高执行效率,尤其适用于给用户发提醒通知的情景 [图片] 所有代码,默认的执行顺序都是同步的。那么,我们应该怎么把一个函数设置成可以异步执行的呢? 只需要在function的前面增加一个关键词async,那么在调用该函数时,该函数就是异步执行的。 在右边的代码里,console.log(2)的执行不必等待test()函数执行完 [图片] 可能有的同学想问,那怎么把异步函数变成同步执行的呢?我们只需要在调用该函数的时候,在前面增加一个关键词await。 那么在右边的代码里,console.log(2)的执行就必须等待test函数执行完毕,并放回对应的直接结果。
2020-08-18 - 小程序前端开发实战
4.1学习对象+课程目的+推荐工具+学习方法 [视频] 4.2微信小程序平台介绍 [视频] 4.3微信开发者工具介绍 [视频] 4.4微信小程序代码框架介绍 [视频] 4.5微信小程序代码wxml学习 [视频] 4.6微信小程序代码wxss学习 [视频] 4.7微信小程序代码js学习 [视频] 4.8微信小程序js中的数据处理 [视频] 4.9微信小程序代码API介绍h [视频] 4.10微信小程序前端实现思路(1) [视频] 4.10微信小程序前端实现思路(2) [视频] 4.11手把手1:注册界面(1) [视频] 4.11手把手1:注册界面(2) [视频] 4.12自主实操1:登录界面 [视频] 4.13手把手2:发布新树洞界面 [视频] 4.14手把手3:树洞广场界面(1) [视频] 4.14手把手3:树洞广场界面(2) [视频] 4.14手把手3:树洞广场界面(3) [视频] 4.15自主实操2:我的树洞界面 [视频] 4.16课后小练 [视频] 4.17本章小结 [视频] 第四章结束后全部代码,点此领取。
2021-09-15 - 云函数·云开发下的用户管理
云函数·云开发下的用户管理 [视频]
2021-09-22 - 小程序前端+服务端后台连通实战
6.1学习对象+课程目的+推荐工具 [视频] 6.2微信小程序API补充知识 [视频] 6.3微信小程序后台开发者配置 [视频] 6.4手把手1:对接注册接口 [视频] 6.5自主实操1:对接登录接口 [视频] 6.6手把手2:对接发布新树洞接口 [视频] 6.7手把手3:对接获取所有树洞接口 [视频] 6.8手把手4:对接点赞接口 [视频] 6.9自主实操2:对接我的树洞接口 [视频] 6.10自主实操3:对接删除树洞接口 [视频] 6.11微信小程序测试体验、审核发布 [视频] 6.12课后小练 [视频] 第六章结束后全部代码,点此领取。
2021-09-15 - 传统前端和小程序前端对比
一、教学课件 教学PPT下载:https://share.weiyun.com/kYNlcejh 欢迎大家前往官网注册并下载工具体验:https://edu.weixin.qq.com/ 二、在线视频 [视频] 本课程将为大家讲解传统前端和小程序对比。 通过本课程的学习,将可以掌握两种前端技术的区别以及彼此的特点,让有传统前端开发经验的人员可以有一个开发思维上的过渡。
09-03 - 云函数应用课程·微信支付下单支付
云函数应用课程·微信支付下单支付 [视频]
2021-09-22 - “会员”运营建议
会员策略的落地强依赖于门店引导。我们从引导注册、会员权益设置两个方面为大家梳理了一些运营建议。 01 引导注册 1)门店流程 • 店内收银台需布置物料引导用户通过扫码打开小程序(会员页面)。 • 部分商品参与会员优惠折扣可在货架内提示,并引导顾客通过小程序查看会员权益; • 收银员结算时需询问用户是否具备会员资格,如用户未开通会员,尝试引导用户现场开通 2)线上流程 • 如门店已上线扫码购能力,在扫码购环节内通过关联会员结算权益引导顾客完成会员注册 • 如门店已上线线上商城能力,可为会员配置专项权益来增强会员转化,如专属优惠,会员配送优惠等。 02 会员权益 常见的会员类型包括:积分会员、等级会员、付费会员。在实际经营环节,以上三种会员对应的权益可根据商家需求进行组合搭配。 1)积分类会员: 用户完成会员注册后,可根据消费金额兑换积分。积分可用于兑换店内单品优惠、现金优惠或其他专属优惠。 2)等级会员: 用户完成会员注册后可根据消费金额兑换积分。积分达到一定额度后可自动晋升会员等级,不同等级会员对应不同的消费权益,如折扣或优惠。 3)付费会员: 会员资格需付费购买,用户成为付费会员后可享受区别于普通会员的积分或优惠能力。 03 其他权益 1)配送优惠: 在具备配送到家场景的商家中经常可体验到会员配送优惠,如会员免配送费,会员可免费享受半小时达服务等(非会员配送时间更长)。 2)优惠优先兑换: 在积分会员场景内,积分通常用于兑换商家福利。在此场景内可为(付费/高级)会员提供更高价值的积分兑换服务。 总结:通过对会员与非会员的差异化权益设置可吸引更多用户注册成为商家会员,在成为会员后根据不同类型的会员设定针对会员做深度运营可获得更好的商家粘性。 以上各类运营策略,整理自试点商户线下门店及小程序,请根据自身情况参考借鉴。
2019-12-26 - “线上商城”介绍与开发指引
“线上商城“是线下商超/便利店把服务能力从线下向线上升级的重要手段。商家为了经营好线上商城,需要充分利用好各种场景下的小程序流量,我们建议线上商城需做好配送到家、延时配送、预购服务。同时,基于线上积累的数据,商家又可以形成用户的行为画像,有助于更好的开展线上优惠活动的运营,提升线上商城的转化率。 01 功能介绍 在线下商超/便利店行业,通过运营线上商城可以在时间及空间方面拓展门店的服务半径。用户可随时随地通过线上商城挑选自己满意的商品,避免因不愿到店,店面商品不够丰富而无法满足需求的情况发生。 [图片] [图片] 我们从“配送到家”“预购”“延时配送”三个场景详细为大家阐述如何做好在线商户里的多种购物服务场景。 场景1 配送到家 配送到家是商超/便利店行业最常见的商城场景,在日常用户使用在线商城的过程中,商家需根据不同时段,不同的用户需求提供下单流程指引,优惠指引来增加顾客从访问到下单的转化率。 下单完成后,在配送环节内需提供充分的配送信息提示,帮助顾客了解订单动态。 该场景主要用于满足用户不愿或不方便到店购物的需求。商品通过门店现货进行拣货发货。此场景注重时效性,商家需明确配送范围,并根据配送服务范围设定拣货及配货流程。 [图片] [图片] 场景2 延时配送 延时配送相较于门店即时配送在货物储备和物流成本方面具备更大优势,商家通过线上商城向顾客提供延时配送服务时刻不受门店现货商品的约束。商家可根据不同的商品类型及不同客户需求进行订单模式分配。 总体来说,延时配送时效性较低但成本更加可控,适合非急需,重量较大,易保存的商品类型。同城配送配送成本较高,适合急需商品。 配送模式可根据用户的收件地址交给用户选择。 [图片] [图片] 场景3 预购 预购场景常见流程为用户下单,选择门店,预约时间取货。商家需在用户预约的时间前完成商品配送(由仓至店),打包的环节。 在产品层面,需为用户提供明确的门店选择指引和提货流程指引。 预购环节因为节省了物流成本,适合通过毛利较低的商品为门店引流。 [图片] [图片] 02 开发/开通指引 开发者开发线上商城到家时,需用到微信以下接口能力: ①点击打开文档:“获取会员信息”文档内容较长,建议使用电脑详细查阅。 ②点击打开文档:“小程序·卡券”文档内容较长,建议使用电脑详细查阅。 ③点击打开文档:“物流接口”文档内容较长,建议使用电脑详细查阅。 [图片] 以上案例素材,整理自试点小程序。 点击打开““线上商城”运营建议”文章,查看详细指引。
2019-12-26 - 小程序零售行业能力应用(视频)
在日常的生活消费场景中,因为有了小程序的加入,更多服务变得触手可及。无论是逛街,购物,还是就餐都变得更加轻松。 通过小程序,你可以轻松的完成预约排队,点餐买单,离店购物等体验。未来,还会有更多的创新的场景通过小程序得以实现。让我们一起努力,通过小程序,让生活更加便利。 iframe class="embed-responsive-item vqq-player" type="text/html" width="640" height="390" src="https://v.qq.com/txp/iframe/player.html?vid=j0824eqb2lz&disableplugin=IframeBottomOpenClientBar&&auto=0" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen>
2021-09-22 - 商超零售门店&微信小程序应用
[图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] 《商超零售门店&微信小程序应用》
2020-10-22 - 零售渠道-商超/便利店解决方案
扫码购、小程序会员、小程序结算等服务在超市/便利店行业不仅仅是提升门店效率的工具,同时也是帮助商家将线下流量转化为线上流量的方式。通过附近的小程序、门店物料、线上优惠券等工具的配套使用,可以更有效的盘活小程序的流量。 01 行业痛点与趋势 通过小程序将线下流量及消费场景转化至线上可解决传统商超便利店经营环节内缺乏门店引流能力,到店顾客交易效率不可控,品牌合作场景单一的问题。 [图片] 02 解决方案概述 通过微信各项产品能力组合,结合零售商家的线下运营,小程序可为用户提供到店前/到店中/离店后的全流程服务。 1)顾客到店前 商家可通过公众号将优惠信息或新品信息展示给顾客,或通过附近的小程序、广告等能力,直接引导顾客通过小程序领取优惠券或进行预约。 [图片] 2)顾客到店时 商家可通过小程序,向顾客提供会员注册、扫码领券等服务,并通过扫码购、商品导航等,帮助顾客实现便捷购物体验。通过小程序与支付系统、会员系统的打通,商家可以更多维度的收集用户信息,实现对顾客更精准的营销。 [图片] 3)顾客离店后 通过提供到家服务、配置线上体验优惠券提升顾客的复购率。同时,通过用户画像积累,可实现用户在离店交易场景的个性化推荐,促进用户线上交易。 [图片] 那么,零售行业的商户如何通过上述丰富多样的能力组合,搭建更好用的小程序,为用户提供有价值的服务呢?以下是我们的建议。 03 分场景介绍 场景1:会员 会员注册及运营是线下超市、便利店常见的营销策略,有效的会员运营策略可提升顾客对品牌的粘性。通过小程序开卡组件等工具,商家可在小程序内实现扫码即会员、支付即会员,结合线下物料将为顾客提供快速注册会员的能力。 在线下场景常见的会员类型如下: 1)积分会员: 注册会员无门槛,顾客在店内完成消费后可获得积分。积分达到一定额度后可兑换奖品,或在消费过程中抵现。 2)等级会员: 注册会员无门槛,顾客在店内消费后可获得积分。积分用于兑换会员等级,会员达到一定等级后可享受消费折扣。 3)付费会员: 顾客需要为成为会员支付一定的费用,付费会员可分为不同等级,如年度会员,季度会员,月度会员。付费会员权益通常会结合积分会员或等级会员一同提供,不同等级的付费会员可享受不同等级的积分或折扣权益。 [图片] 场景2:优惠券 优惠券是零售场景内常用的营销工具,在不同场景下灵活运用优惠券,可达到不同的效果。常见优惠券使用场景如下: 1)引流到店: 在顾客离店状态下通过一些渠道向用户下发优惠券可实现线上向线下门店引流的效果。常见的引流优惠券包括:微信支付面对面优惠券、朋友圈优惠券。 另外,商家还可以通过配置支付完成后打开小程序,公众号图文发券、线下传单物料发券等能力进行优惠券下发实现引流到店。 2)提升门店购买效率: 在顾客到店状态下,通过引导用户领取优惠券可提升顾客在店内消费转化效率。 [图片] 场景3:扫码购 扫码购作为门店获取线下流量的重要方式,不仅可提升门店内的收银效率,而且可帮助门店通过小程序积累线下的流量及数据。 在超市/便利店内,扫码购可以作为顾客购物期间的营销工具。通过识别顾客的购物习惯,可在顾客使用扫码购期间,对其进行优惠券、满减或会员活动等类型的营销。 [图片] 场景4:到家 线上商城是超市/便利店行业流量变现的常见工具。通过线上商城,商家可以为未到店的顾客提供丰 富商品和服务,在时间及空间上拓展门店的经营能力。 到家场景配合充分的用户引导,合理的优惠促销方案,可有效增强用户从访问到交易的转化。 [图片] 04 常用入口 搭建完善的运营体系,用好小程序 为帮助商家通过小程序更好为用户提供服务,微信提供了多入口引流小程序;通过与公众号、微信群、线下物料宣传结合,商家可在宣传、推广等运营活动中,以小程序作为落地载体为用户提供后续服务,并通过有效的用户运营让用户保持活跃。 多入口引流 小程序在微信体系内具备丰富的访问渠道,如附近的小程序、线下扫码,线上长按二维码,公众号,会话,支付完成页等等。 1)附近的小程序 附近的小程序可以作为商家重要的小程序引流工具,通过在附近的小程序进行配置可让更多附近的潜在顾客了解门店的服务,并通过小程序实现预约、点单外卖等多项服务落地。 [图片] 2)线下物料 为提升小程序本身的留存,线下物料仍然是不可缺少的一部分。商家在为顾客提供商品服务的同时,也可以通过商品包装,商品小票,商品附赠传单的方式为顾客提供回流渠道,使顾客在离店状态下也可以使用小程序进行消费。 线下扫码引流场景可包括: ① 线下传单,购物小票 [图片] ② 门店堆头物料及店内引导: [图片] 3)线上长按二维码 小程序支持生成带有小程序码(二维码)的图片供用户进行线上传播。 常见场景:订单分享,商品分享 [图片] [图片] 4)公众号(图文消息)打开小程序 公众号可作为顾客了解商家动态,获取商家信息的重要渠道。商家在日常运营公众号的过程中,可通过公众号文章插入小程序、公众号菜单栏配置等多种方式,将有购买意向的顾客引入小程序,并快速转化。 [图片] [图片] 5)会话 在微信生态内,社交流量是最直接的流量来源。商家可通过扫码添加店长微信等方式,积累官方顾客群,并在群内定期更新优惠信息,用户可通过小程序直接购买或预约自提,提升核心顾客的购买频次。 [图片] [图片] 6)支付凭证 对于线下零售场景而言,支付是高频且必现的场景,有效利用支付完成后完成页,可实现小程序用户回流。 常见常见:支付完成页关联小程序订单页面,支付完成页关联小程序优惠券页面。 [图片]
2020-01-17 - 小程序云原生数据库的演进之路
云开发的数据库演进历程是怎样的? 在各种数据场景下如何正确地使用云开发数据库呢? 云开发数据库未来的发展方向又是如何? 通过视频,了解详情: [视频]
2021-09-22 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 农历日期组件 【恋爱小清单开发总结】
因为我过农历生日,所以开发了这个功能哈哈哈哈 自己说了算的感觉真好 言归正传 官方的日期组件不支持农历,所以需要借助官方的多列选择器做扩展 因为农历有闰年的缘故,在选择了年份后需要更新月份,选择了月份以后需要更新日期 另外一个需要注意的是闰年的情况下,多列选择器月份选中的索引需要注意一下,如果在闰月之前的月份索引需要减一,闰月之后的月份不需要减一 比如农历2020的四月,对应的月份索引应该等于3,闰4月对应的索引等于4.看下图片就明白了 [图片] 看下效果图: [图片][图片] 可以扫码体验 [图片] 1、wxml代码 这里用到官方的多列选择器 2、js const lunarDate = require('../../utils/lunarDate.js'); const {calendar} = require('../../utils/calendar.js'); Component({ /** * 组件的属性列表 */ properties: { lunarYear: Number,//农历年 lunarMonth: Number,//农历月 lunarDay: Number,//农历日 isLeapMonth: Boolean,//闰月标记 solarDate: String,//公历日期 solarDateRange: Object//公历年份起始 }, /** * 组件的初始数据 */ data: { lunarDateRange: [],//农历日期范围 lunarDateRangeIndex: []//选中农历索引 }, ready: function () { this._init(); }, /** * 组件的方法列表 */ methods: { _init: function () { const { lunarYear, lunarMonth, lunarDay, isLeapMonth, solarDate, solarDateRange } = this.properties; this._initLunarDateRange(solarDate, solarDateRange, lunarYear, lunarMonth, lunarDay, isLeapMonth); }, /** * 初始化农历日期数据 * @param solarDate * @param solarDateRange * @param lunarYear * @param lunarMonth * @param lunarDay * @param isLeapMonth * @private */ _initLunarDateRange: function (solarDate, solarDateRange, lunarYear, lunarMonth, lunarDay, isLeapMonth) { const lunarDateRange = lunarDate.initLunarDateRangeHandler(solarDateRange); const lunarDateRangeIndex = this._setDefaultLunarDateRangeIndex(lunarDateRange, solarDate, solarDateRange, lunarYear, lunarMonth, lunarDay, isLeapMonth); this._updateLunarDateRange(lunarDateRange, lunarDateRangeIndex); if (lunarYear && lunarMonth && lunarDay) { console.log("农历日期不为空,准备显示农历日期"); this._showLunarDateHandler(lunarDateRange, lunarDateRangeIndex, false); } else if (solarDate) { console.log("公历日期不为空,准备显示转化后的农历日期"); this._showLunarDateHandler(lunarDateRange, lunarDateRangeIndex, true); } console.log("农历日期:", lunarDateRange); }, /** * 填充默认的农历日期选中索引 * 默认为当前日期 * @param lunarDateRange * @param solarDate * @param solarDateRange * @param lunarYear * @param lunarMonth * @param lunarDay * @param isLeapMonth * @returns {*[]} * @private */ _setDefaultLunarDateRangeIndex: function (lunarDateRange, solarDate, solarDateRange, lunarYear, lunarMonth, lunarDay, isLeapMonth) { let lunarDateRangeIndex; if (lunarYear && lunarMonth && lunarDay) { console.log("外部传入农历日期:", lunarYear, lunarMonth, lunarDay, isLeapMonth); lunarDateRangeIndex = [lunarYear - solarDateRange.start, this._getLunarMonthIndex(lunarYear, lunarMonth, isLeapMonth), lunarDay - 1]; } else { console.log("外部传入公历日期:", solarDate); let date = solarDate != null ? new Date(solarDate.replace(/-/g,"/")) : new Date(); // let date = new Date(solarDate.replace(/-/g,"/")); const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); const targetLunarDate = calendar.solar2lunar(year, month, day); console.log("转化后的农历日期:", JSON.stringify(targetLunarDate)); const {lYear, lMonth, lDay, isLeap} = targetLunarDate; lunarYear = lYear; lunarMonth = lMonth; isLeapMonth = isLeap; lunarDateRangeIndex = [lYear - solarDateRange.start, this._getLunarMonthIndex(lunarYear, lunarMonth, isLeapMonth), lDay - 1]; } lunarDateRange[1] = lunarDate.resetLunarMonthRangeHandler(lunarYear); lunarDateRange[2] = lunarDate.resetLunarDayRangeHandler(lunarYear, lunarMonth, isLeapMonth); console.log("默认的农历日期选中索引:", lunarDateRangeIndex); return lunarDateRangeIndex; }, /** * 获取农历月份索引 * @param lunarYear * @param lunarMonth * @param isLeapMonth * @returns {number} * @private */ _getLunarMonthIndex: function (lunarYear, lunarMonth, isLeapMonth) { const leapMonth = calendar.leapMonth(lunarYear); //1、闰月索引不需要减一 //2、闰年、非闰月,并且当前月份在闰月之后,索引不需要减一 return (isLeapMonth || (!isLeapMonth && leapMonth > 0 && lunarMonth > leapMonth)) ? lunarMonth : lunarMonth - 1; }, /** * 更新农历日期选择范围、更新农历日期选中索引 * @param lunarDateRange * @param lunarDateRangeIndex * @private */ _updateLunarDateRange: function (lunarDateRange, lunarDateRangeIndex) { this.setData({ lunarDateRange, lunarDateRangeIndex }, function () { console.log("农历日期选择范围/农历日期选中索引更新完毕:", lunarDateRange, lunarDateRangeIndex) }); }, /** * 多列选择事件 * @param e */ selectMultiPickerColumnHandler: function (e) { const {column, value} = e.detail; let {lunarDateRange, lunarDateRangeIndex} = this.data; let selectYear; switch (column) { case 0: //选择年份,准备更新月份数据 selectYear = lunarDateRange[column][value].value; console.log("选择年份:", selectYear); const memoryLunarMonthRange = lunarDate.resetLunarMonthRangeHandler(selectYear); lunarDateRange[1] = memoryLunarMonthRange; //更新选中值 lunarDateRangeIndex = [value, 0, 0]; this._updateLunarDateRange(lunarDateRange, lunarDateRangeIndex); break; case 1: //选择月份,准备更新天数数据 const {value: selectMonth, isLeapMonth} = lunarDateRange[column][value]; selectYear = lunarDateRange[0][lunarDateRangeIndex[0]].value; console.log("选择年份:", selectYear, "选择月份:", selectMonth, "是否闰月:", isLeapMonth); const memoryLunarDayRange = lunarDate.resetLunarDayRangeHandler(selectYear, selectMonth, isLeapMonth); lunarDateRange[2] = memoryLunarDayRange; //更新选中值 lunarDateRangeIndex = [lunarDateRangeIndex[0], value, 0]; this._updateLunarDateRange(lunarDateRange, lunarDateRangeIndex); break; default: console.log("选择日:", lunarDateRange[column][value]); break; } }, /** * 选择农历日期 * @param e */ multiPickerChangeHandler: function (e) { console.log('选择农历日期:', e); const {value: selectValue} = e.detail; const {lunarDateRange} = this.data; this._showLunarDateHandler(lunarDateRange, selectValue, true); }, /** * 显示选中的农历日期 * @param lunarDateRange * @param lunarDateRangeIndex * @param doTriggerEvent 是否触发通知事件,告知父页面当前选中的农历日期 * @private */ _showLunarDateHandler: function (lunarDateRange, lunarDateRangeIndex, doTriggerEvent) { const [lunarYearIndex, lunarMonthIndex, lunarDayIndex] = lunarDateRangeIndex; const isLeapMonth = lunarDateRange[1][lunarMonthIndex].isLeapMonth; this.setData({ lunarDate: lunarDateRange[0][lunarYearIndex].text + lunarDateRange[1][lunarMonthIndex].text + lunarDateRange[2][lunarDayIndex].text, isLeapMonth, lunarDateRangeIndex }, function () { if (doTriggerEvent) { console.log("触发lunarDateConfirm事件"); this.triggerEvent("lunarDateConfirm", { lunarYear: lunarDateRange[0][lunarYearIndex].value, lunarMonth: lunarDateRange[1][lunarMonthIndex].value, lunarDay: lunarDateRange[2][lunarDayIndex].value, isLeapMonth }); } }); } } }); 3、lunarDate.js const {calendar} = require('/calendar.js'); /** * 填充农历日期 * @param solarDateRange * @returns {Array} */ const initLunarDateRangeHandler = function (solarDateRange) { console.log("公历日期范围:", solarDateRange); const lunarDateRange = []; const lunarYearRange = [], lunarMonthRange = [], lunarDayRange = []; for (let i = solarDateRange.start; i <= solarDateRange.end; i++) { lunarYearRange.push({text: calendar.toGanZhiYear(i) + '(' + i + ')', value: i}); } for (let i = 1; i <= 12; i++) { lunarMonthRange.push({text: calendar.toChinaMonth(i), value: i, isLeapMonth: false}); } for (let i = 1; i <= 29; i++) { lunarDayRange.push({text: calendar.toChinaDay(i), value: i}); } lunarDateRange.push(lunarYearRange); lunarDateRange.push(lunarMonthRange); lunarDateRange.push(lunarDayRange); console.log("农历日期:", lunarDateRange); return lunarDateRange; }; /** * 重置农历月份 * @param selectYear * @returns {Array} */ const resetLunarMonthRangeHandler = function (selectYear) { const leapMonth = calendar.leapMonth(selectYear); const lunarMonthRange = []; console.log("选择年份:", selectYear, "闰月:", leapMonth); for (let i = 1; i <= 12; i++) { lunarMonthRange.push({text: calendar.toChinaMonth(i), value: i, isLeapMonth: false}); } if (leapMonth > 0) { console.log("有闰月,准备更新月份数据"); lunarMonthRange.splice(leapMonth, 0, { text: "闰" + calendar.toChinaMonth(leapMonth), value: leapMonth, isLeapMonth: true }); } return lunarMonthRange; }; /** * 重置农历日 * @param selectYear * @param selectMonth * @param isLeapMonth * @returns {Array} */ const resetLunarDayRangeHandler = function (selectYear, selectMonth, isLeapMonth) { const lunarDayRange = []; console.log("选择年份:", selectYear, "选择月份:", selectMonth, "是否闰月:", isLeapMonth); const days = isLeapMonth ? calendar.leapDays(selectYear) : calendar.monthDays(selectYear, selectMonth); for (let i = 1; i <= days; i++) { lunarDayRange.push({text: calendar.toChinaDay(i), value: i}); } return lunarDayRange; }; module.exports = {initLunarDateRangeHandler, resetLunarMonthRangeHandler, resetLunarDayRangeHandler}; 4、calendar.js(来自网上) /** * @1900-2100区间内的公历、农历互转 * @charset UTF-8 * @Author Jea杨(JJonline@JJonline.Cn) * @Time 2014-7-21 * @Time 2016-8-13 Fixed 2033hex、Attribution Annals * @Time 2016-9-25 Fixed lunar LeapMonth Param Bug * @Version 1.0.2 * @公历转农历:calendar.solar2lunar(1987,11,01); //[you can ignore params of prefix 0] * @农历转公历:calendar.lunar2solar(1987,09,10); //[you can ignore params of prefix 0] */ const calendar = { /** * 农历1900-2100的润大小信息表 * @Array Of Property * @return Hex */ lunarInfo: [0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,//1900-1909 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,//1910-1919 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,//1920-1929 0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,//1930-1939 0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,//1940-1949 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,//1950-1959 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,//1960-1969 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,//1970-1979 0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,//1980-1989 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,//1990-1999 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,//2000-2009 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,//2010-2019 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,//2020-2029 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,//2030-2039 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,//2040-2049 /**Add By JJonline@JJonline.Cn**/ 0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,//2050-2059 0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,//2060-2069 0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,//2070-2079 0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,//2080-2089 0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,//2090-2099 0x0d520],//2100 /** * 公历每个月份的天数普通表 * @Array Of Property * @return Number */ solarMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], /** * 天干地支之天干速查表 * @Array Of Property trans["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"] * @return Cn string */ Gan: ["\u7532", "\u4e59", "\u4e19", "\u4e01", "\u620a", "\u5df1", "\u5e9a", "\u8f9b", "\u58ec", "\u7678"], /** * 天干地支之地支速查表 * @Array Of Property * @trans["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"] * @return Cn string */ Zhi: ["\u5b50", "\u4e11", "\u5bc5", "\u536f", "\u8fb0", "\u5df3", "\u5348", "\u672a", "\u7533", "\u9149", "\u620c", "\u4ea5"], /** * 天干地支之地支速查表<=>生肖 * @Array Of Property * @trans["鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪"] * @return Cn string */ Animals: ["\u9f20", "\u725b", "\u864e", "\u5154", "\u9f99", "\u86c7", "\u9a6c", "\u7f8a", "\u7334", "\u9e21", "\u72d7", "\u732a"], /** * 24节气速查表 * @Array Of Property * @trans["小寒","大寒","立春","雨水","惊蛰","春分","清明","谷雨","立夏","小满","芒种","夏至","小暑","大暑","立秋","处暑","白露","秋分","寒露","霜降","立冬","小雪","大雪","冬至"] * @return Cn string */ solarTerm: ["\u5c0f\u5bd2", "\u5927\u5bd2", "\u7acb\u6625", "\u96e8\u6c34", "\u60ca\u86f0", "\u6625\u5206", "\u6e05\u660e", "\u8c37\u96e8", "\u7acb\u590f", "\u5c0f\u6ee1", "\u8292\u79cd", "\u590f\u81f3", "\u5c0f\u6691", "\u5927\u6691", "\u7acb\u79cb", "\u5904\u6691", "\u767d\u9732", "\u79cb\u5206", "\u5bd2\u9732", "\u971c\u964d", "\u7acb\u51ac", "\u5c0f\u96ea", "\u5927\u96ea", "\u51ac\u81f3"], /** * 1900-2100各年的24节气日期速查表 * @Array Of Property * @return 0x string For splice */ sTermInfo: ['9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e', '97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd', '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722'], /** * 数字转中文速查表 * @Array Of Property * @trans ['日','一','二','三','四','五','六','七','八','九','十'] * @return Cn string */ nStr1: ["\u65e5", "\u4e00", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341"], /** * 日期转农历称呼速查表 * @Array Of Property * @trans ['初','十','廿','卅'] * @return Cn string */ nStr2: ["\u521d", "\u5341", "\u5eff", "\u5345"], /** * 月份转农历称呼速查表 * @Array Of Property * @trans ['正','一','二','三','四','五','六','七','八','九','十','冬','腊'] * @return Cn string */ nStr3: ["\u6b63", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341", "\u51ac", "\u814a"], /** * 返回农历y年一整年的总天数 * @param lunar Year * @return Number * @eg:var count = calendar.lYearDays(1987) ;//count=387 */ lYearDays: function (y) { var i, sum = 348; for (i = 0x8000; i > 0x8; i >>= 1) { sum += (calendar.lunarInfo[y - 1900] & i) ? 1 : 0; } return (sum + calendar.leapDays(y)); }, /** * 返回农历y年闰月是哪个月;若y年没有闰月 则返回0 * @param lunar Year * @return Number (0-12) * @eg:var leapMonth = calendar.leapMonth(1987) ;//leapMonth=6 */ leapMonth: function (y) { //闰字编码 \u95f0 return (calendar.lunarInfo[y - 1900] & 0xf); }, /** * 返回农历y年闰月的天数 若该年没有闰月则返回0 * @param lunar Year * @return Number (0、29、30) * @eg:var leapMonthDay = calendar.leapDays(1987) ;//leapMonthDay=29 */ leapDays: function (y) { if (calendar.leapMonth(y)) { return ((calendar.lunarInfo[y - 1900] & 0x10000) ? 30 : 29); } return (0); }, /** * 返回农历y年m月(非闰月)的总天数,计算m为闰月时的天数请使用leapDays方法 * @param lunar Year * @return Number (-1、29、30) * @eg:var MonthDay = calendar.monthDays(1987,9) ;//MonthDay=29 */ monthDays: function (y, m) { if (m > 12 || m < 1) { return -1 }//月份参数从1至12,参数错误返回-1 return ((calendar.lunarInfo[y - 1900] & (0x10000 >> m)) ? 30 : 29); }, /** * 返回公历(!)y年m月的天数 * @param solar Year * @return Number (-1、28、29、30、31) * @eg:var solarMonthDay = calendar.leapDays(1987) ;//solarMonthDay=30 */ solarDays: function (y, m) { if (m > 12 || m < 1) { return -1 } //若参数错误 返回-1 var ms = m - 1; if (ms == 1) { //2月份的闰平规律测算后确认返回28或29 return (((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)) ? 29 : 28); } else { return (calendar.solarMonth[ms]); } }, /** * 农历年份转换为干支纪年 * @param lYear 农历年的年份数 * @return Cn string */ toGanZhiYear: function (lYear) { var ganKey = (lYear - 3) % 10; var zhiKey = (lYear - 3) % 12; if (ganKey == 0) ganKey = 10;//如果余数为0则为最后一个天干 if (zhiKey == 0) zhiKey = 12;//如果余数为0则为最后一个地支 return calendar.Gan[ganKey - 1] + calendar.Zhi[zhiKey - 1]; }, /** * 公历月、日判断所属星座 * @param cMonth [description] * @param cDay [description] * @return Cn string */ toAstro: function (cMonth, cDay) { var s = "\u9b54\u7faf\u6c34\u74f6\u53cc\u9c7c\u767d\u7f8a\u91d1\u725b\u53cc\u5b50\u5de8\u87f9\u72ee\u5b50\u5904\u5973\u5929\u79e4\u5929\u874e\u5c04\u624b\u9b54\u7faf"; var arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22]; return s.substr(cMonth * 2 - (cDay < arr[cMonth - 1] ? 2 : 0), 2) + "\u5ea7";//座 }, /** * 传入offset偏移量返回干支 * @param offset 相对甲子的偏移量 * @return Cn string */ toGanZhi: function (offset) { return calendar.Gan[offset % 10] + calendar.Zhi[offset % 12]; }, /** * 传入公历(!)y年获得该年第n个节气的公历日期 * @param y公历年(1900-2100);n二十四节气中的第几个节气(1~24);从n=1(小寒)算起 * @return day Number * @eg:var _24 = calendar.getTerm(1987,3) ;//_24=4;意即1987年2月4日立春 */ getTerm: function (y, n) { if (y < 1900 || y > 2100) { return -1; } if (n < 1 || n > 24) { return -1; } var _table = calendar.sTermInfo[y - 1900]; var _info = [ parseInt('0x' + _table.substr(0, 5)).toString(), parseInt('0x' + _table.substr(5, 5)).toString(), parseInt('0x' + _table.substr(10, 5)).toString(), parseInt('0x' + _table.substr(15, 5)).toString(), parseInt('0x' + _table.substr(20, 5)).toString(), parseInt('0x' + _table.substr(25, 5)).toString() ]; var _calday = [ _info[0].substr(0, 1), _info[0].substr(1, 2), _info[0].substr(3, 1), _info[0].substr(4, 2), _info[1].substr(0, 1), _info[1].substr(1, 2), _info[1].substr(3, 1), _info[1].substr(4, 2), _info[2].substr(0, 1), _info[2].substr(1, 2), _info[2].substr(3, 1), _info[2].substr(4, 2), _info[3].substr(0, 1), _info[3].substr(1, 2), _info[3].substr(3, 1), _info[3].substr(4, 2), _info[4].substr(0, 1), _info[4].substr(1, 2), _info[4].substr(3, 1), _info[4].substr(4, 2), _info[5].substr(0, 1), _info[5].substr(1, 2), _info[5].substr(3, 1), _info[5].substr(4, 2), ]; return parseInt(_calday[n - 1]); }, /** * 传入农历数字月份返回汉语通俗表示法 * @param lunar month * @return Cn string * @eg:var cnMonth = calendar.toChinaMonth(12) ;//cnMonth='腊月' */ toChinaMonth: function (m) { // 月 => \u6708 if (m > 12 || m < 1) { return -1 } //若参数错误 返回-1 var s = calendar.nStr3[m - 1]; s += "\u6708";//加上月字 return s; }, /** * 传入农历日期数字返回汉字表示法 * @param lunar day * @return Cn string * @eg:var cnDay = calendar.toChinaDay(21) ;//cnMonth='廿一' */ toChinaDay: function (d) { //日 => \u65e5 var s; switch (d) { case 10: s = '\u521d\u5341'; break; case 20: s = '\u4e8c\u5341'; break; break; case 30: s = '\u4e09\u5341'; break; break; default : s = calendar.nStr2[Math.floor(d / 10)]; s += calendar.nStr1[d % 10]; } return (s); }, /** * 年份转生肖[!仅能大致转换] => 精确划分生肖分界线是“立春” * @param y year * @return Cn string * @eg:var animal = calendar.getAnimal(1987) ;//animal='兔' */ getAnimal: function (y) { return calendar.Animals[(y - 4) % 12] }, /** * 传入阳历年月日获得详细的公历、农历object信息 <=>JSON * @param y solar year * @param m solar month * @param d solar day * @return JSON object * @eg:console.log(calendar.solar2lunar(1987,11,01)); */ solar2lunar: function (y, m, d) { //参数区间1900.1.31~2100.12.31 if (y < 1900 || y > 2100) { return -1; }//年份限定、上限 if (y == 1900 && m == 1 && d < 31) { return -1; }//下限 if (!y) { //未传参 获得当天 var objDate = new Date(); } else { var objDate = new Date(y, parseInt(m) - 1, d) } var i, leap = 0, temp = 0; //修正ymd参数 var y = objDate.getFullYear(), m = objDate.getMonth() + 1, d = objDate.getDate(); var offset = (Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) - Date.UTC(1900, 0, 31)) / 86400000; for (i = 1900; i < 2101 && offset > 0; i++) { temp = calendar.lYearDays(i); offset -= temp; } if (offset < 0) { offset += temp; i--; } //是否今天 var isTodayObj = new Date(), isToday = false; if (isTodayObj.getFullYear() == y && isTodayObj.getMonth() + 1 == m && isTodayObj.getDate() == d) { isToday = true; } //星期几 var nWeek = objDate.getDay(), cWeek = calendar.nStr1[nWeek]; if (nWeek == 0) { nWeek = 7; }//数字表示周几顺应天朝周一开始的惯例 //农历年 var year = i; var leap = calendar.leapMonth(i); //闰哪个月 var isLeap = false; //效验闰月 for (i = 1; i < 13 && offset > 0; i++) { //闰月 if (leap > 0 && i == (leap + 1) && isLeap == false) { --i; isLeap = true; temp = calendar.leapDays(year); //计算农历闰月天数 } else { temp = calendar.monthDays(year, i);//计算农历普通月天数 } //解除闰月 if (isLeap == true && i == (leap + 1)) { isLeap = false; } offset -= temp; } if (offset == 0 && leap > 0 && i == leap + 1) if (isLeap) { isLeap = false; } else { isLeap = true; --i; } if (offset < 0) { offset += temp; --i; } //农历月 var month = i; //农历日 var day = offset + 1; //天干地支处理 var sm = m - 1; var gzY = calendar.toGanZhiYear(year); //月柱 1900年1月小寒以前为 丙子月(60进制12) var firstNode = calendar.getTerm(year, (m * 2 - 1));//返回当月「节」为几日开始 var secondNode = calendar.getTerm(year, (m * 2));//返回当月「节」为几日开始 //依据12节气修正干支月 var gzM = calendar.toGanZhi((y - 1900) * 12 + m + 11); if (d >= firstNode) { gzM = calendar.toGanZhi((y - 1900) * 12 + m + 12); } //传入的日期的节气与否 var isTerm = false; var Term = null; if (firstNode == d) { isTerm = true; Term = calendar.solarTerm[m * 2 - 2]; } if (secondNode == d) { isTerm = true; Term = calendar.solarTerm[m * 2 - 1]; } //日柱 当月一日与 1900/1/1 相差天数 var dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10; var gzD = calendar.toGanZhi(dayCyclical + d - 1); //该日期所属的星座 var astro = calendar.toAstro(m, d); return { 'lYear': year, 'lMonth': month, 'lDay': day, 'Animal': calendar.getAnimal(year), 'IMonthCn': (isLeap ? "\u95f0" : '') + calendar.toChinaMonth(month), 'IDayCn': calendar.toChinaDay(day), 'cYear': y, 'cMonth': m, 'cDay': d, 'gzYear': gzY, 'gzMonth': gzM, 'gzDay': gzD, 'isToday': isToday, 'isLeap': isLeap, 'nWeek': nWeek, 'ncWeek': "\u661f\u671f" + cWeek, 'isTerm': isTerm, 'Term': Term, 'astro': astro }; }, /** * 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历object信息 <=>JSON * @param y lunar year * @param m lunar month * @param d lunar day * @param isLeapMonth lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可] * @return JSON object * @eg:console.log(calendar.lunar2solar(1987,9,10)); */ lunar2solar: function (y, m, d, isLeapMonth) { //参数区间1900.1.31~2100.12.1 var isLeapMonth = !!isLeapMonth; var leapOffset = 0; var leapMonth = calendar.leapMonth(y); var leapDay = calendar.leapDays(y); if (isLeapMonth && (leapMonth != m)) { return -1; }//传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同 if (y == 2100 && m == 12 && d > 1 || y == 1900 && m == 1 && d < 31) { return -1; }//超出了最大极限值 var day = calendar.monthDays(y, m); var _day = day; //bugFix 2016-9-25 //if month is leap, _day use leapDays method if (isLeapMonth) { _day = calendar.leapDays(y, m); } if (y < 1900 || y > 2100 || d > _day) { return -1; }//参数合法性效验 //计算农历的时间差 var offset = 0; for (var i = 1900; i < y; i++) { offset += calendar.lYearDays(i); } var leap = 0, isAdd = false; for (var i = 1; i < m; i++) { leap = calendar.leapMonth(y); if (!isAdd) {//处理闰月 if (leap <= i && leap > 0) { offset += calendar.leapDays(y); isAdd = true; } } offset += calendar.monthDays(y, i); } //转换闰月农历 需补充该年闰月的前一个月的时差 if (isLeapMonth) { offset += day; } //1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点) var stmap = Date.UTC(1900, 1, 30, 0, 0, 0); var calObj = new Date((offset + d - 31) * 86400000 + stmap); var cY = calObj.getUTCFullYear(); var cM = calObj.getUTCMonth() + 1; var cD = calObj.getUTCDate(); return calendar.solar2lunar(cY, cM, cD); } }; module.exports = {calendar};
2021-09-09 - 为什么event.currentTarget.dataset取不到值?
[图片][图片] [图片] 天啊,一直取不到值为什么?
2021-01-11 - [渲染层错误] TypeError: Cannot read property xxx of undefined原因
一、产生错误的情况 在wxml文件中,引用wxs文件,wxs中有一个函数,参数是对象,返回对象。 wxml中使用wxs中的函数处理对象数据。 JS文件在onLoad()中处理相关数据和逻辑。 错误指向wxs文件中的具体函数。 系统:win10 工具版本:Stable 1.05.2103200 基础库:2.10.4 二、产生的原因 渲染机制中,对异步数据,是先显示初始数据,再显示带数据的数据。 因此,刚加载对象时,是空对象。即报属性未定义错误。 三、解决办法 对wxml中处理对象的外层加[代码]wx:if="{{!!对象}}"[代码],作用是未异步到对象数据时,不加载内部对对象的处理。
2021-05-23 - 史上最详细一步一步介绍微信小程序微信支付功能开发
前言微信小程序开发微信支付, 相对于微信的其他功能,实话说相比之下好太多,可能是开发文档是微信支付这边撰写的缘故吧?猜的。所以微信支付在小程序中,虽然参数十分的多,环节特别的细致,但也算不上无从下手。上个项目实施了一次微信小程序支付功能的开发,趁记忆力尚可,赶紧记录一番本次环境平台为 小程序端(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 - 云开发支付流程闭环
云开发支付流程闭环 extends 微信小程序–使用云开发完成支付闭环 在上述文章中,我们对支付结果的处理更多依赖于小程序端的操作 订单号存储在小程序端 支付结果采用小程序端定时触发器轮询 现在我对该流程进行了优化处理 1.流程介绍 [图片] 2.小程序端 请求统一下单云函数 调用支付接口 侦听器获取支付结果 [代码]// pages/index/details.js const app = getApp(); const db = wx.cloud.database(); var watcher = null Page({ /** * 页面的初始数据 */ data: { }, //付费解锁 payUnlock() { var that = this; this.setData({ vis: true }) //用户ID 即为OPENID let userid = this.data.selfcard._id; wx.cloud.callFunction({ name: 'userpay', data: { fee: 1, paydata: { userid } }, success: res => { console.log(res) //统一下单云函数中需要返回侦听器 需要的记录id that.payWatcher(res.result.docid); that.setData({ vis: false }) //根据统一下单参数 请求支付接口 const payment = res.result.payment wx.requestPayment({ ...payment, success(ans) { console.log(ans) }, fail(ans) { that.setData({ errMsg: '调用支付失败' }) } }) } }) }, payWatcher(docid){ var that = this; //为用户支付记录表设置侦听器,侦听docid信息的变动 this.watcher = db.collection('USERPAYLOG').doc(docid).watch({ onChange: async function (snapshot) { //只打印变动的信息 // console.log(snapshot) if (snapshot.docChanges.length != 0) { console.log(snapshot.docChanges) let paydoc = snapshot.docChanges[0].doc; //侦听到支付成功 if(paydoc.paystatus == 1){ that.setData({ succMsg:'支付成功', locked:false, bottom:0 }) } // await that.watcher.close(); } }, onError: function (err) { console.error('the watch closed because of error', err) } }) }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function () { try { this.watcher.close(); } catch (error) { console.log('暂未启动支付侦听器') } } }) [代码] 3.云函数端 [代码]userpay[代码] 云调用统一下单【CloudPay.unifiedOrder】 数据库中存入订单记录并设置为未支付状态 需要配置商户(云开发控制台) [图片] [代码]const cloud = require('wx-server-sdk') //需要在此处修改你的云环境ID cloud.init({ env: '' }) const db = cloud.database(); const _ = db.command; // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() var openid = event.openid || wxContext.OPENID //获取统一下单金额 var fee = parseInt(event.fee); let paydata = event.paydata; //生成订单号 let tradeno = GetTradeNo(); //调用统一下单接口 const res = await cloud.cloudPay.unifiedOrder({ //填写你的商户主体信息 例如 xx商贸 "body": "", "outTradeNo": tradeno, "spbillCreateIp": "127.0.0.1", //填写你的商户ID -- 可在云开发控制台中绑定获得(上图所示) "subMchId": "", "totalFee": fee, //填写你的云环境ID "envId": "", //填写你的回调函数名称 "functionName": "userpaynotify" }) console.log(res) res.outTradeNo = tradeno res.totalFee = fee //支付状态 0 为未支付 paydata.tradeno = tradeno paydata.paystatus = 0 paydata.totalfee = fee paydata.openid = openid paydata.paytime = TimeCode() //统一下单shuju paydata.uniOrder = res //拦截处理 为保持数据库字段一致性 if (res.returnCode == 'SUCCESS' && res.resultCode == 'SUCCESS') { //在云数据库中写入未支付的订单信息 let tdata = await db.collection('USERPAYLOG').add({ data: paydata }) console.log(tdata) //将该记录ID携带返回给小程序端 res.docid = tdata._id; return res; }else{ return res; } } function GetTradeNo() { var outTradeNo = ""; //订单号 for (var i = 0; i < 6; i++) //6位随机数,用以加在时间戳后面。 { outTradeNo += Math.floor(Math.random() * 10); } outTradeNo = "COP" + new Date().getTime() + outTradeNo; //时间戳,用来生成订单号。 return outTradeNo; } function TimeCode() { var date = new Date(); var year = date.getFullYear() var month = date.getMonth() + 1 var day = date.getDate() var hour = date.getHours() var minute = date.getMinutes() var second = date.getSeconds() return [year, month, day].map(formatNumber).join('-') + ' ' + [hour, minute, second].map(formatNumber).join(':') } function formatNumber(n) { n = n.toString() return n[1] ? n : '0' + n } [代码] 支付成功后触发云环境中该回调函数 回调函数携带的请求信息请在参考文档中查看 [代码]userpaynotify[代码] 修改数据库中订单状态 返回给回调请求SUCCESS数据【Cloud.paymentCallback】 订单在支付成功时会触发该回调函数 该回调函数必须有返回值,且必须是固定格式 根据回调函数携带的订单号,修改对应订单号的订单状态,并且返回对应格式的返回信息 字段名 变量名 必填 类型 描述 错误码 errcode 是 Number 0 错误信息 errmsg 是 String [代码]const cloud = require('wx-server-sdk') //填写你的云环境ID cloud.init({ env: '' }) const db = cloud.database(); const _ = db.command; // 云函数入口函数 exports.main = async (event, context) => { console.log('支付成功回调函数触发') console.log(event) let tradeno = event.outTradeNo; try { //修改数据库中订单状态 为已支付 db.collection('USERPAYLOG').where({ tradeno:tradeno }).update({ data:{ paystatus:1 } }) } catch (error) { return { errmsg: 'SERVER_ERROR', errcode: -1 } } return { errmsg: 'SUCCESS', errcode: 0 } } [代码] 参考文档 云开发文档 Cloud.CloudPay | 微信开放文档 (qq.com) 回调函数请求携带参数 [代码]{ appid: '', bankType: 'OTHERS', cashFee: 1, feeType: 'CNY', isSubscribe: 'N', mchId: '', nonceStr: '', openid: '', outTradeNo: '', resultCode: 'SUCCESS', returnCode: 'SUCCESS', subAppid: '', subIsSubscribe: 'N', subMchId: '', subOpenid: '', timeEnd: '', totalFee: 1, tradeType: 'JSAPI', transactionId: '', userInfo: { appId: '', openId: '' } } [代码]
2021-06-02 - 云开发快速入门
如果你想免费、快速的开发出一个完整的项目,用小程序的云开发可能是最好的选择。小程序的云开发所用到的主要是前端开发的知识。 注册微信小程序 小程序的注册非常方便,打开小程序注册页面,按照要求填入个人的信息,验证邮箱和手机号,扫描二维码绑定你的微信号即可,3 分钟左右的时间即可搞定。 注册页面:小程序注册页面 https://mp.weixin.qq.com/ 注册小程序时不能使用注册过微信公众号、微信开放平台的邮箱哦,也就是需要你使用一个其他邮箱才行。 当我们注册成功后,就可以自动登入到小程序的后台管理页面啦,如果你不小心关掉了后台页面,也可以点击小程序后台管理登录页进行登录。 后台管理页:小程序后台管理登录页 https://mp.weixin.qq.com/ 小程序和微信公众号的登录页都是同一个页面,他们会根据你的不同的注册邮箱来进行跳转。 进入到小程序的后台管理页后,点击左侧菜单的开发进入设置页,然后再点击开发设置,在开发者 ID 里就可以看到 AppID(小程序 ID),这个待会我们有用。 注意小程序的ID(AppID)不是你注册的邮箱和用户名,你需要到后台查看才行哦~ 必备工具与云开发文档 大家可以根据自己的电脑操作系统来下载相应的版本,注意要选择稳定版 Stable Build 的开发者工具。 开发者工具:小程序开发者工具下载地址 和学习任何编程一样,官方技术文档都是最值得阅读的参考资料。技术文档大家先只需要花五分钟左右的时间了解大致的结构即可,先按照我们的教学步骤学完之后再来看也不迟哦。 技术文档:云开发官方文档 由于小程序的云开发在不断新增功能,更新非常频繁,所以要确保自己的开发者工具是最新的哦(不然会报很多奇奇怪怪的错误),比如你之前下载过要先同步到最新才行~ 体验云开发模板小程序 安装完开发者工具之后,我们使用微信扫码登录开发者工具,然后使用开发者工具新建一个小程序的项目: 项目名称:这个可以根据自己的需要任意填写;目录:大家可以先在电脑上新建一个空文件夹,然后选择它;AppID:就是之前我们找到的 AppID(小程序 ID)(也可以下拉选择 AppID)开发模式为小程序(默认),后端服务选择小程序·云开发点击新建确认之后就能在开发者工具的模拟器里看到云开发 QuickStart 小程序,在编辑器里看到这个小程序的源代码。 接下来,我们点击开发者工具的工具栏里的预览图标,就会弹出一个二维码,使用微信扫描这个二维码就能在手机里看到这个小程序啦。 如果你没有使用微信登录开发者工具,以及你的微信不是该小程序的开发者是没法预览的哦。 在手机里(或模拟器)操作这个小程序,把小程序里的每个按键都点一遍,看看会有什么反应。我们会发现很多地方都会显示“调用失败”等,这非常正常,我们接下来会通过一系列的操作让小程序不报错。 开通云开发服务 点击微信开发者工具的“云开发”图标,在弹出框里点击“开通”,同意协议后,会弹出创建环境的对话框。这时会要求你输入环境名称和环境 ID,以及当前云开发的基础环境配额(基础配额免费,而且足够你使用哦)。 建议你环境名称可以使用 xly、环境ID自动生成即可,当你的云开发环境出现问题的时候,你可以提供你的环境ID,云开发团队会有专人为你解答。 按照对话框提示的要求填写完之后,点击创建,会初始化环境,环境初始化成功后会自动弹出云开发控制台,这样我们的云开发服务就开通啦。大家可以花两分钟左右的时间熟悉一下云开发控制台的界面。 找到云开发的环境 ID 点击云开发控制台窗口里的设置图标,在环境变量的标签页找到环境名称和环境 ID。 当云开发服务开通后,我们可以在小程序源代码 cloudfunctions 文件夹名看到你的环境名称。如果在 cloudfunctions 文件夹名显示的不是环境名称,而是“未指定环境”,可以鼠标右键该文件夹,选择“更多设置”,然后再点击“设置”小图标,选择环境并确定。 指定小程序的云开发环境 在开发者工具中打开源代码文件夹 miniprogram 里的 app.js,找到如下代码: wx.cloud.init({ // 此处请填入环境 ID, 环境 ID 可打开云控制台查看 env: 'my-env-id', traceUser: true, }) 在 env: 'my-env-id'处改成你的环境 ID,如 env: 'xly-snoop' 下载 Nodejs NodeJS 是在服务端运行 JavaScript 的运行环境,云开发所使用的服务端环境就是 NodeJS。npm 是 Node 包管理器,通过 npm,我们可以非常方便的安装云开发所需要的依赖包。 npm是前端开发必不可少的包(模块)管理器,它的主要功能就是来管理包package,包括安装、卸载、更新、查看、搜索、发布等,其他编程语言也有类似的包管理器,比如Python的pip,PHP的composer、Java的maven。我们可以把包管理器看成是windows的软件管理中心或手机的应用中心,只是它们用的是可视化界面,包管理器用的是命令行Command Line。 下载地址:Nodejs 下载地址 大家可以根据电脑的操作系统下载相应的 NodeJS 安装包并安装(安装时不要修改安装目录,啥也别管直接 next 安装即可)。打开电脑终端(Windows 电脑为 cmd 命令提示符,Mac 电脑为终端 Terminal),然后逐行输入并按 Enter 执行下面的代码: node --version npm --version 如果显示为 v10.15.0 以及 6.11.3(可能你的版本号会有所不同),表示你的 Nodejs 环境已经安装成功。 学编程要仔细,一个字母,一个单词,一个标点符号都不要出错哦。注意输上面的命令时node、npm的后面有一个空格,而且是两个短横杠–。 部署并上传云函数 部署并上传云函数 cloudfuntions 文件夹图标里有朵小云,表示这就是云函数根目录。展开 cloudfunctions,我们可以看到里面有 login、openapi、callback、echo 文件夹,这些就是云函数目录。而 miniprogram 文件夹则放置的是小程序的页面文件。 cloudfunctions里放的是云函数,miniprogram放的是小程序的页面,这并不是一成不变的,也就是说你也可以修改这些文件夹的名称,这取决于项目配置文件project.config.json里的如下配置项: "miniprogramRoot": "miniprogram/", "cloudfunctionRoot": "cloudfunctions/", 但是你最好是让放小程序页面的文件夹以及放云函数的文件夹处于平级关系且都在项目的根目录下,便于管理。 使用鼠标右键其中的一个云函数目录比如 login,在右键菜单中选择在终端中打开,打开后在终端中输入以下代码并按 Enter 回车执行: npm install 如果显示“npm不是内部或外部命令”,你需要关闭微信开发者工具启动的终端,而是重新打开一个终端窗口,并在里面输入 cd /D 你的云函数目录进入云函数目录,比如 cd /D C:\download\tcb-project\cloudfunctions\login进入login的云函数目录,然后再来执行npm install命令。 这时候会下载云函数的依赖模块,下载完成后,再右键 login 云函数目录,点击“创建并部署:所有文件”,这时会把本地的云函数上传到云端,上传成功后在 login 云函数目录图标会变成一朵小云。 在开发者工具的工具栏上点击“云开发”图标会打开云开发控制台,在云开发控制台点击云函数图标,就能在云函数列表里看到我们上传好的“login”云函数啦。 上传所有云函数 接下来我们按照这样的流程把其他所有云函数(如 openapi)都部署都上传,也就是要执行和上面相同的步骤,总结如下: 右键云函数目录,选择在终端中打开,输入 npm install命令下载依赖文件; 然后再右键云函数目录,点击“创建并部署:所有文件” 在云开发控制台–云函数–云函数列表查看云函数是否部署成功。 login、openapi、echo、callback这些云函数在后面都会用到的哦,一定要确定自己部署上传成功,不然后面会报错的哦。 npm 包管理器与依赖模块 为什么要在云函数目录执行 npm install,而不是其他地方?这是因为 npm install 会下载云函数目录下的配置文件 package.json 里的 dependencies,它表示的是当前云函数需要依赖的模块。package.json 在哪里,就在哪里执行 npm install,没有 package.json,没有 dependencies,就没法下载啊。 执行 npm install 命令下载的依赖模块会放在 node_modules 文件夹里,大家可以在执行了 npm install 命令之后,在电脑里打开查看一下 node_modules 文件夹里下载了哪些模块。 既然 npm install 是下载模块,那它是从哪里下载的呢?就以 wx-server-sdk 为例,我们可以在以下链接看到 wx-server-sdk 的情况: https://www.npmjs.com/package/wx-server-sdk 为什么 package.json 里依赖的是一个模块 wx-server-sdk,但是 node_modules 文件夹里却下载了那么多模块?这是因为 wx-server-sdk 也依赖三个包 tcb-admin-node、protobuf、jstslib,而这三个包又会依赖其他包,子子孙孙的,于是就有了很多模块。 node_modules 文件夹这么大(几十 M~几百 M 都可能),会不会影响小程序的大小?小程序的大小只与 miniprogram 文件夹有关,当你把云函数都部署上传到服务器之后,你把整个 cloudfuntions 文件夹删掉都没有关系。相同的依赖(比如都依赖 wx-server-sdk)一旦部署到云函数之后,你可以选择不上传 node_modules 时,因为已经上传过了。 获取 openid 与云函数 login 当我们把云函数 login 部署上传成功后,就可以在模拟器以及手机(需要重新点击预览图标并扫描二维码)里点击获取 openid 了。 点击获取 openid openid 是小程序用户的唯一标识,也就是每一个小程序用户都有一个唯一的 openid。点击“点击获取 openid”,在用户管理指引页面如果显示“用户 id 获取成功”以及一串字母+数字,那么表示你 login 云函数部署并上传成功啦。如果获取 openid 失败,你则需要解决 login 云函数的部署上传,才能进行下面的步骤哦。 调用云函数的解读 小程序的首页是”pages/index/index”,我们可以从 app.json 的配置项或者模拟器左下角的页面路径可以看出来。在 index.wxml 里有这段代码: <button class="userinfo-nickname" bindtap="onGetOpenid">点击获取 openid</button> 也就是当点击“点击获取 openid”按钮时,会触发 bindtap 绑定的事件处理函数 onGetOpenid,在 index.js 里可以看到 onGetOpenid 事件处理函数(在 index.js 里找到事件处理函数 onGetOpenid 对比理解)调用了 wx.cloud.callFunction()接口(打开技术文档对比理解) 技术文档:调用云函数 wx.cloud.callFunction 调用云函数的方法很简单,只需要填写云函数的名称 name(这里为 login),以及需要传递的参数(这里并没有上传参数),就可以进行调用。在 success 回调函数里添加以下代码打印 res 对象: console.log('调用login云函数返回的res',res) 添加完成之后记得保存代码哦,文件修改没有保存会在标签页有一个小的绿点。可以使用快捷键(同时按)Ctrl和S来保存(Mac电脑为Command和S)。 编译之后,再点击“点击获取 openid”按钮,就能看到完整的 res 对象,res 对象有三个参数: requestID:云函数执行 ID,可用于在云开发控制台查找日志,打开云开发控制台–云函数–日志,可以在这里根据云函数函数名以及 requestID 来筛选查看云函数的调用日志(含返回结果);result:云函数返回的结果,login 云函数返回的结果里包含 appid、event 对象,我们可以通过 res.result.appid 以及 res.result.event 访问它们;errMsg:显示云函数是否调用成功事件处理函数 onGetOpenid 调用云函数成功之后,干了三件事情: 使用 console.log 打印 openid,可以在点击按钮触发云函数在控制台看到该打印结果;把获取到的 appid 赋值给 app.js 文件里的 globalData 全局对象;跳转到 userConsole 页面;而 userConsole 页面就只是从 globalData 里将 openid 取出来通过 setData 渲染到页面。 小任务:你明白为啥wx.cloud.callFunction()是小程序端的API了么?思考一下为啥云开发会有小程序端的API和服务端API的区别?能理解多少是多少,不清楚也没有关系,后面会有更多内容助你理解。 云函数 login 解读 为什么调用云函数 login 返回的 res 的 result 对象里会包含 event 对象、appid、userInfo 这些结果?这就取决于云函数是怎么写的了。使用开发者工具打开 login 云函数(在 cloudfuntions 文件夹里)的 index.js。 exports.main = (event, context) => {} 这是一个箭头函数的写法,其中 event 和 context 是参数。我们将两个打印日志修改为以下代码,相当于备注一下到底打印到哪里去了: console.log('服务端打印的event',event) console.log('服务端打印的context',context) 保存之后,右键点击 index.js 文件,选择云函数增量上传:(更新文件),更新 login 云函数,我们再来点击“点击获取 openid”按钮,打印的结果在哪里呢?在云开发控制台的云函数日志里面(注意不是开发者工具的控制台)。打开云开发控制台–云函数–日志,按函数名筛选,选择 login 云函数,可以看到云函数被调用的日志记录,我们可以在日志里发现: event 对象包含程序用户的 openid 和小程序的 appid,而 openid 就相当于用户的身份证,我们可以根据 openid 获取到用户的昵称、头像等信息(后面会说明);而 context 对象则是云函数的调用信息和运行状态。在返回结果里我们可以看到 return 返回的数据小任务:比较一下云开发控制台的云函数日志打印的结果和开发者工具控制台打印的结果,深入了解 event 对象、context 对象、result 对象与返回结果,这是云函数的比较重要的知识点。云函数的打印日志会显示在云开发控制台的日志里面,这一点非常重要,要多加利用。只要是打印日志,无论是显示在开发者工具控制台还是显示在云开发控制台的就没有不重要的。 getWXContext() getWXContext()API 是云开发服务端的工具类 API,会返回小程序用户的 openid、小程序 appid、小程序用户的 unionid 等。说这么多不如直接打印,在下面添加一行打印信息: const wxContext = cloud.getWXContext() console.log('getWXContext返回的结果',wxContext) 保存之后,右键点击 index.js 文件,选择云函数增量上传:(更新文件),更新 login 云函数,我们再来点击“点击获取 openid”按钮,然后去云开发控制台的云函数日志里看到底返回了什么结果。 技术文档:getWXContext() 对照技术文档来理解返回的结果。 注意小程序用户 unionid只有在开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用才能获得。 return return 语句是终止函数的执行,并返回一个指定的值给函数调用者。这里返回了 4 个值,而前面我们就调用过 login 云函数,就是函数的调用者,所以我们打印的事件处理函数 onGetOpenid 的回调函数的 res 对象正是这个 return 返回的结果。那既然如此,我们在 return 多加一些内容看看,比如我们之前的一些数据结构案例,将 return 函数改为如下代码: let lesson = "云开发技术训练营"; let enname = "CloudBase Camp"; let x = 3, y = 4, z = 5.001, a = -3, b = -4, c = -5; let now = new Date(); return { movie: { name: "霸王别姬", img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1910813120.webp", desc: "风华绝代。" }, movielist:["肖申克的救赎", "霸王别姬", "这个杀手不太冷", "阿甘正传", "美丽人生"], charat: lesson.charAt(4), concat: enname.concat(lesson), uppercase: enname.toUpperCase(), abs: Math.abs(b), pow: Math.pow(x, y), sign: Math.sign(a), now: now.toString(), fullyear: now.getFullYear(), date: now.getDate(), day: now.getDay(), hours: now.getHours(), minutes: now.getMinutes(), seconds: now.getSeconds(), time: now.getTime(), event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } 保存之后,右键点击 index.js 文件,选择云函数增量上传:(更新文件),更新 login 云函数,我们再来点击“点击获取 openid”按钮,然后去云开发控制台的云函数日志里看到底返回了什么结果。 这里我们多次反复提及更新了index.js文件之后就要选择云函数增量上传:(更新文件),更新login云函数,希望大家平时的时候注意,这也是小程序云开发服务端和小程序端一个非常大的区别。 新建云函数 鼠标右键 cloudfunctions 云函数根目录,在弹出的窗口选择新建 Node.js 云函数,比如输入 sum,按 Enter 确认后,微信开发者工具会在本地(你的电脑)创建出 sum 云函数目录,同时也会在线上环境中创建出对应的云函数(也就是自动部署好了,可以到云开发控制台云函数列表里看到) 打开 sum 云函数目录下的 index.js,添加 sum:event.a+event.b,到 return 函数里(把多余的内容可以删掉了),然后记得选择云函数增量上传:(更新文件),更新 sum 云函数。 return { sum:event.a+event.b, } 这个 a 和 b 是变量,但是和前面不一样的是,在服务端我们并没有声明 a 和 b 啊,这是因为我们可以在小程序端声明变量。 点击开发者工具模拟器的“快速新建云函数”,会跳转到 addFunction 页面,打开 addFunction.wxml,我们看到测试云函数绑定的是 testFunction 事件处理函数。 <view class="list-item" bindtap="testFunction"> <text>测试云函数</text> </view> 我们去看 addFunction.js 里的 testFunction,看变量 a 和 b 这两个小程序端的变量是怎么和服务端的变量关联起来的,而又是如何把结果渲染到页面的。testFunction 调用云函数 sum 同样是通过 wx.cloud.callFunction,不同的是在 data 里有 a 和 b: data: { a: 1, b: 2 }, data 里填写的是传递给云函数的参数,也就是先把小程序端的参数传递给云函数,然后云函数处理之后再返回 res 对象,我们可以在 success 回调函数里打印 res 对象: console.log("sum云函数返回的对象",res) 编译之后,我们再点击测试云函数,在控制台就能看到打印的结果,res.result.sum 就是 3。直接把 res.result.sum 通过 setData 赋值到 result 就能渲染出数字,那这个 res.result 是什么?JSON.stringify()又是什么? result: JSON.stringify(res.result) 我们可以打印一下 res.result,以及 JSON.stringify(res.result) console.log("res.result是啥", res.result) console.log("JSON.stringify(res.result)是啥", JSON.stringify(res.result)) res.result 是对象,而 JSON.stringify(res.result)是 json 格式, JSON.stringify() 方法是将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串,因为对象如果直接渲染到页面是会显示 [object Object]的。 小任务:将小程序的参数传递给云端,有没有一点wx.request的感觉?相当于我们通过云函数写好了一个数据API,然后在小程序端调用。新建一个云函数,把各种数学运算都部署到云端,然后通过传递参数,调用这些算法,并将结果渲染到页面。 体验上传图片 上传图片到云存储 使用模拟器以及手机端点击云开发 QuickStart 小程序的上传图片按钮,选择一张图片并打开,如果在文件存储指引页面显示上传成功和文件的路径以及图片的缩略图,说明你的图片就上传到云开发服务器里啦。 点击云开发控制台的存储图标,就可以进入到存储管理页查看到你之前上传的图片啦,点击该图片名称可以看到这张图片的一些信息,如:文件大小、格式、上传者的 OpenID 以及存储位置、下载地址和 File ID。复制下载地址链接,在浏览器就能查看到这张图片啦。 值得注意的是由于QuickStart小程序将“上传图片”这个按钮上传的所有图片都命名为my-image,所以上传同一格式的图片就会被覆盖掉,也就是无论你上传多少张相同格式的图片,只会在后台里看到最后更新的那张图片。以后我们会教大家怎么修改代码,让图片不会被覆盖。 组件支持 我们可以把下载地址作为图床来使用的,也就是你可以把图片的下载地址放到其他网页图片是可以显示的。云存储的图片还有一个 FileID(既云文件 ID,以 cloud://开头)则只能用于小程序的特定场景,也只有部分组件的部分属性支持,把链接粘贴到浏览器也是打不开的。 技术文档:组件支持 比如我们在 index 页面的 index.wxml 里输入以下代码,在 image 组件的 src 属性里输入你的云存储图片的 FileID,它是可以显示出来的。 <image src="你的图片的FileID"></image> 但是如果你退出登录开发者工具,图片就不会显示,而且还会报错,所以不要把图片的 FileID 当做图床用,FileID 另有它用。 体验云调用之服务端调用 重新点击开发者工具的预览图标,然后用手机扫描二维码,在手机端点击云开发 QuickStart 的云调用里的服务端调用,就可以发送模板消息和获取小程序码。 点击获取小程序码,如果显示调用失败,说明你的 openapi 云函数没有部署成功,需要你先部署成功才行哦。调用成功,就能获取到你的小程序码啦,这个小程序码也会保存到云开发的存储里。 发送模板消息,只能在手机微信里预览测试哦,使用微信开发者工具是发送不了模板消息,而且控制台还会报错 点击发送模板消息,你的微信就会收到一则服务通知,该通知是由你的小程序发出的购买成功通知。这就是微信的模板消息啦,很多微信公众号、小程序都会有这样的功能,使用小程序云开发,我们也可以轻松定制自己的服务通知(后面会教大家如何定制)。 体验前端操作数据库 点击微信开发者工具的云开发图标,打开云开发控制台,点击数据库图标进入到数据库管理页,点击集合名称右侧的+号图标,就可以创建一个数据集合了,这里我们只需要添加一个 counters 的集合(不需添加数据)即可。 在开发者工具的编辑器里展开 miniprogram 文件夹,打开 pages 文件下 databaseGuide 里的 databaseGuide.js 文件,在这里找到 onAdd: function (){}、 onQuery: function (){}、 onCounterInc: function (){}、 onCounterDec: function (){}、 onRemove: function (){}分别选中绿色的代码块,然后同时按快捷键 Ctrl 和/(Mac 电脑的快捷键为 Command 和/),就可以批量取消代码的注释。 //是前端编程语言JavaScript的单行注释,位于 // 这一行的代码都不会执行,我们使用快捷键就是批量取消这些代码的注释,让整段代码生效。之所以显示为绿色,是微信开发者工具为了让我们看得更清晰而做的语法高亮。 前端操作数据库的页面逻辑 以上的函数是在小程序的前端页面来操作数据库,点击开发者工具模拟器云开发 QuickStart 里的前端操作数据库, 在第1步(数据库指引有标注),我们会获取到用户的openid,也就是说你没有获取到openid是没法通过小程序的前端来操作数据库的哦 第2步,需要我们在云开发控制台里的数据库管理页创建一个counters的集合(不需添加数据); 第3步,点击按钮页面的按钮“新增记录”(按钮就在这个页面的第4条与第5条之间,看起来不是那么明显),这时会调用 onAdd方法,往counters集合新增一个记录(之前手动添加有木有很辛苦?),我们可以去云开发控制台的数据库管理页查看一下counters集合是不是有了一条记录;大家可以多点击几下新增记录按钮,然后去云开发控制台看数据库又有什么变化。也就是小程序前端页面通过 onAdd方法,在数据库新增了记录。 第4步,点击按钮查询记录,这时调用的是 onQuery方法就能在小程序里获取到第3步我们存储到数据库里的数据啦 第5步,点击计数器按钮+号和-号,可以更新count的值,点击+号按钮会调用 onCounterInc方法,而点击-号 onCounterDec方法,比如我们点击加号到7,再去数据库管理页查看最新的一条记录(也就是最后一条),它的count由原来的1更新到了7(先点刷新按钮),我们再点击-号按钮到5,再来数据库管理页查看有什么变化变化(先点刷新按钮) 第6步,点击删除记录按钮,会调用 onRemove方法,这时会删掉数据库里最新的记录(也就是第5步里的那一条记录)。 通过实战我们了解到,databaseGuide.js 文件里的 onAdd、 onQuery、 onCounterInc、 onCounterDec、 onRemove 可以实现小程序的前端页面来操作数据库。 这些函数大家可以结合 databaseGuide.js 文件和云开发技术文档关于数据库的内容来理解。(关于前端是如何操作数据库的,我们之后还会深入讲解,这里只需要了解大致的逻辑即可) 在前面JavaScript的章节里我们了解到数据以及数据的存储是非常重要的,而有了数据库,用函数生成的数据能够比缓存存储的更加持久,而且在上面我们实现了对数据进行增(添加)、删(删除)、改(修改、更新)、查(查询并渲染到页面),不仅如此,缓存的容量也比较有限,最多不过10M,而数据库可以存几百G以上,可见它的重要性。 开始一个新的云开发项目 基于云开发 QuickStart 模板小程序 云开发 QuickStart 模板小程序有很多多余的页面,这个我们只需要把 miniprogram 文件夹下的 pages、images、components、style 文件夹里的文件清空,以及 app.json 的 pages 配置项里的页面删除,把 app.wxss 里的样式代码都删掉就是一个全新的开始啦。这是方法之一,也可以使用下面的方法(推荐学习时使用下面的方法)。 基于没有使用云开发的项目改造 当然我们也可以把前面章节没有使用云开发的项目改造成使用云服务,首先在小程序的根目录下新建一个文件夹,比如 cloudfunctions,然后在 project.config.json 添加云函数文件夹的路径配置即可, "cloudfunctionRoot": "cloudfunctions/", 然后新建一个 miniprogram 文件夹,把小程序除了 project.config.json 以外的其他文件,比如 pages、utils、images、app.js、app.json 等文件都放到 miniprogram 文件夹里,再在 project.config.json 添加 miniprogramRoot 配置: "cloudfunctionRoot": "cloudfunctions/", "miniprogramRoot":"miniprogram/", 值得一提的是,云函数部署上传成功,我们就可以一直调用,只要你的小程序的appid以及环境ID没有变,你创建再多的小程序项目,都可以直接调用部署好的云函数,比如前面的login、echo、callback、sum等云函数。也就是说云函数一旦部署成功,它就一直在云端服务器里,哪怕你把小程序本地的云函数都删掉也没有关系。 当新建了并配置了云函数根目录为 cloudfunctions 文件夹之后,云函数根目录里并没有云函数,我们可以右键点击云函数根目录 cloudfunctions 文件夹选择同步云函数列表,可以把所有云端的云函数列表都列举出来(这只是列举了列表),而要修改云函数里面的内容,我们可以右键点击其中的一个云函数目录选择下载云函数即可。 除此之外,我们需要小程序的 app.js 的生命周期函数 onLaunch 里使用 wx.cloud.init()来初始化云开发能力: onLaunch: function () { if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力') } else { wx.cloud.init({ env: '你的环境ID', traceUser: true, }) } }, 云开发能力全局只需要初始化一次即可,这里的 traceUser 属性设置为 true,会将用户访问记录到用户管理中,在云开发控制台的运营分析—用户访问里可以看到访问记录。 基础库与 wx.cloud 在小程序端初始化云开发能力的代码里,涉及到 wx.cloud 以及基础库版本的知识。关于 wx.cloud,我们可以和之前在控制台了解 wx 对象一样,直接在开发者工具的控制台里输入: wx.cloud 来了解对象有哪些属性与方法。我们可以看到有如下方法: CloudID: ƒ () //用于云调用获取开放数据 callFunction: ƒ () //调用云函数 database: ƒ () //获取数据库的引用 deleteFile: ƒ () //从云存储空间删除文件 downloadFile: ƒ () //从云存储空间下载文件 getTempFileURL: ƒ () //用云文件 ID 换取真实链接 init: ƒ () //初始化云开发能力 uploadFile: ƒ () //上传文件至云存储空间 而关于基础库,有三个地方需要注意它的存在,平时开发的时候需要留意开发者工具的project.config.json里有这样一个属性libVersion,这个也可以在开发者工具工具栏右上角的详情里的本地设置里的调试基础库,建议切换到最新,切换后libVersion的值也会修改到切换的版本; 官方文档基础库的更新日志,小程序更新非常频繁,而更新的核心就是基础库:所以基础库更新日志要经常留意每个API,技术文档都会标明它的基础库支持的最低版本,而小程序·云开发 SDK是2.2.3以上的基础库才开始支持的。
2021-09-10 - 小程序云开发CMS内容管理系统使用初体验
小程序云开发CMS内容管理系统于本周一(2020年5月18号)发布,现在当前发布周的周日,今天下午没事,我也算摸索了下,具体如何开通,如何操作,我也算是第一个吃螃蟹的。 下午具体参考文档为腾讯云云开发官方公众号推送的两篇图文 【如何三分钟为小程序打造管理后台】 https://mp.weixin.qq.com/s/TFc2fj-gOVwAvs603WZG_A https://developers.weixin.qq.com/community/develop/article/doc/0002c47d40c48044316ada7ba5b813 【新能力 | 云开发CMS内容管理系统,5分钟搞定小程序管理后台】 https://mp.weixin.qq.com/s/HZCVnnau3grmKA06E-M-yg https://developers.weixin.qq.com/community/develop/article/doc/0002a401aa0e5814306af240351c13 在操作过程中遇到几个问题,我汇总下: 1、登录腾讯云控制台,如何找到云开发Cloud Base 2、安装CMS内容管理系统扩展 3、安装成功后,入口链接怎么确定 成功操作完成后,使用CMS内容管理系统,我目前有下面几个问题尚未确定 1、字段是否支持数组或者对象 2、是否支持上传图片 3、是否支持数据导入和导出 4、系统基于管理员/运营者两种身份角色的访问控制,这两种权限有哪些区分 我依次作答下 第一个问题:字段支持数组,但是不支持对象 第二个问题:支持上传图片 第三个问题:目前支持导出,不支持导入 第四个问题:管理员可以做一些数据建模工作,而运营者知识对数据 建模后的表集合进行操作。 1 [图片] 2 [图片] 3 [图片] 4 [图片] 5 [图片] 6 [图片] 7 [图片] 8[图片] 9 [图片] 0 [图片] 1 [图片] 2 [图片] 3 [图片] 4 [图片] 5 [图片] 6 现在我接着说,CMS内容管理系统的优势 对于云开发小程序运营者而言,通过CMS内容管理系统会极大提供日常操作效率,对开发者而言,也会减轻开发工作量,正像图文里面描述的一样 为方便开发人员和内容运营者,随时随地管理小程序/ Web 等多端云开发内容数据,云开发 CMS支持 PC/移动端浏览器访问,支持文本、富文本、图片、文件、关联类型等多种内容类型的可视化编辑。 但是也存在不好的一点 CMS内容管理系统在数据建模之后,会在小程序云开发控制台的数据库里面生成几个集合,这算是对小程序数据库有侵入性的一个操作 当然对于这个新产品,我是双手鼓掌点赞的。 7
2020-05-25 - JavaScript函数与拓展
JavaScript进阶内容学习:函数与小程序特有的JS。 课程最终代码,点此领取:https://share.weiyun.com/5Kb7U6O [视频]
2021-11-26 - 小程序开发进阶之前端开发
4 节课,教你快速入门小程序前端。本系列视频,由腾讯课堂NEXT学院、微信学堂联合出品。
2021-12-14 - JavaScript基础语法
JavaScript从零到入门,本节带你学习编程语言常见的概念:变量、数据类型、条件语句、循环,并通过JavaScript编程来优化课堂首页开发。 [视频]
2021-11-26 - 首页开发实战
带你实战腾讯课堂首页开发,详解布局王者flex。 本节结束代码:https://share.weiyun.com/5Eqz7TV [视频]
2021-11-26 - 云开发基础NodeJS
云函数的运行环境是 Node.js,我们可以在云函数中使用 Nodejs 内置模块以及使用 npm 安装第三方依赖来帮助我们更快的开发。借助于一些优秀的开源项目,避免了我们重复造轮子,相比于小程序端,能够大大扩展云函数的使用 云函数与 Nodejs由于云函数与 Nodejs 息息相关,需要我们对云函数与 Node 的模块以及 Nodejs 的一些基本知识有一些基本的了解。下面只介绍一些基础的概念,如果你想详细深入了解,建议去翻阅一下 Nodejs 的官方技术文档: 技术文档:Nodejs API 中文技术文档 Nodejs 的内置模块在前面我们已经接触过 Nodejs 的 fs 模块、path 模块,这些我们称之为 Nodejs 的内置模块,内置模块不需要我们使用 npm install 下载,就可以直接使用 require 引入: const fs = require('fs') const path = require('path') Nodejs 的常用内置模块以及功能如下所示,这些模块都是可以在云函数里直接使用的: fs 模块:文件目录的创建、删除、查询以及文件的读取和写入,下面的 createReadStream 方法类似于读取文件,path 模块:提供了一些用于处理文件路径的 APIurl 模块:用于处理与解析 URLhttp 模块:用于创建一个能够处理和响应 http 响应的服务querystring 模块:解析查询字符串until 模块 :提供用于解析和格式化 URL 查询字符串的实用工具;net 模块:用于创建基于流的 TCP 或 IPC 的服务器crypto 模块:提供加密功能,包括对 OpenSSL 的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装在云函数中使用 HTTP 请求访问第三方服务可以不受域名限制,即不需要像小程序端一样,要将域名添加到 request 合法域名里;也不受 http 和 https 的限制,没有域名只有 IP 都是可以的,所以云函数可以应用的场景非常多,即能方便的调用第三方服务,也能够充当一个功能复杂的完整应用的后端。不过需要注意的是,云函数是部署在云端,有些局域网等终端通信的业务只能在小程序里进行。 常用变量module、exports、require require 用于引入模块、 JSON、或本地文件。 可以从 node_modules 引入模块,可以使用相对路径(例如 ./、)引入本地模块或 JSON 文件,路径会根据 __dirname 定义的目录名或当前工作目录进行处理。 node 模块化遵循的是 commonjs 规范,CommonJs 定义的模块分为: 模块标识(module)、模块导出(exports) 、模块引用(require)。 在 node 中,一个文件即一个模块,使用 exports 和 require 来进行处理。 exports 表示该模块运行时生成的导出对象。如果按确切的文件名没有找到模块,则 Node.js 会尝试带上 .js、 .json 或 .node 拓展名再加载。 .js 文件会被解析为 JavaScript 文本文件, .json 文件会被解析为 JSON 文本文件。 .node 文件会被解析为通过 process.dlopen() 加载的编译后的插件模块。以 '/' 为前缀的模块是文件的绝对路径。 例如, require('/home/marco/foo.js') 会加载 /home/marco/foo.js 文件。以 './' 为前缀的模块是相对于调用 require() 的文件的。 也就是说, circle.js 必须和 foo.js 在同一目录下以便于 require('./circle') 找到它。 module.exports 用于指定一个模块所导出的内容,即可以通过 require() 访问的内容。 // 引入本地模块: const myLocalModule = require('./path/myLocalModule'); // 引入 JSON 文件: const jsonData = require('./path/filename.json'); // 引入 node_modules 模块或 Node.js 内置模块: const crypto = require('crypto'); wx-server-sdk 的模块tcb-admin-node、protobuf、jstslib 第三方模块Nodejs 有 npm 官网地址 Nodejs 库推荐:awesome Nodejs 当没有以 '/'、 './' 或 '../' 开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules 目录,比如 wx-server-sdk 就加载自 node_modules 文件夹: const cloud = require('wx-server-sdk') Lodash 实用工具库Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库,通过降低 array、number、objects、string 等数据类型的使用难度从而让 JavaScript 变得更简单。Lodash 的模块化方法非常适用于:遍历 array、object 和 string;对值进行操作和检测;创建符合功能的函数。 技术文档:Lodash 官方文档、Lodash 中文文档 使用开发者工具新建一个云函数,比如 lodash,然后在 package.json 增加 lodash 最新版 latest 的依赖: "dependencies": { "lodash": "latest" } 在 index.js 里的代码修改为如下,这里使用到了 lodash 的 chunk 方法来分割数组: const cloud = require('wx-server-sdk') var _ = require('lodash'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { //将数组拆分为长度为2的数组 const arr= _.chunk(['a', 'b', 'c', 'd'], 2); return arr } 右键 lodash 云函数目录,选择“在终端中打开”,npm install 安装模块之后右键部署并上传所有文件。我们就可以通过多种方式来调用它(前面已详细介绍)即可获得结果。Lodash 作为工具,非常好用且实用,它的源码也非常值得学习,更多相关内容则需要大家去 Github 和官方技术文档里深入了解。 在awesome Nodejs页面我们了解到还有 Ramba、immutable、Mout 等类似工具库,这些都非常推荐。借助于 Github 的 awesome 清单,我们就能一手掌握最酷炫好用的开源项目,避免了自己去收集收藏。 moment 时间处理开发小程序时经常需要格式化时间、处理相对时间、日历时间以及时间的多语言问题,这个时候就可以使用比较流行的 momentjs 了。 技术文档:moment 官方文档、moment 中文文档 使用开发者工具新建一个云函数,比如 moment,然后在 package.json 增加 moment 最新版 latest 的依赖: "dependencies": { "moment": "latest" } 在 index.js 里的代码修改为如下,我们将 moment 区域设置为中国,将时间格式化为 十二月 23 日 2019, 4:13:29 下午的样式以及相对时间多少分钟前: const cloud = require('wx-server-sdk') const moment = require("moment"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { moment.locale('zh-cn'); time1 = moment().format('MMMM Do YYYY, h:mm:ss a'); time2 = moment().startOf('hour').fromNow(); return { time1,time2} } 不过云函数中的时区为 UTC+0,不是 UTC+8,格式化得到的时间和在国内的时间是有 8 个小时的时间差的,我们可以给小时数+8,也可以修改时区。云函数修改时区我们可以使用 timezone 依赖(和 moment 是同一个开源作者)。 技术文档:timezone 技术文档 在 package.json 增加 moment-timezone 最新版 latest 的依赖,然后修改上面相应的代码即可,使用起来非常方便: const moment = require('moment-timezone'); time1 = moment().tz('Asia/Shanghai').format('MMMM Do YYYY, h:mm:ss a'); 获取公网 IP有时我们希望能够获取到服务器的公网 IP,比如用于 IP 地址的白名单,或者想根据 IP 查询到服务器所在的地址,ipify 就是一个免费好用的依赖,通过它我们也可以获取到云函数所在服务器的公网 IP。 技术文档:ipify Github 地址 使用开发者工具新建一个 getip 的云函数,然后输入以下代码,并在 package.json 的”dependencies”里新增 "ipify":"latest" ,即最新版的 ipify 依赖: const cloud = require('wx-server-sdk') const ipify = require('ipify'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { return await ipify({ useIPv6: false }) } 然后右键 getip 云函数根目录,选择在终端中打开,输入 npm install 安装依赖,之后上传并部署所有文件。我们可以在小程序端调用这个云函数,就可以得到云函数服务器的公网 IP,这个 IP 是随机而有限的几个,反复调用 getip,就能够穷举所有云函数所在服务器的 ip 了。 可能你会在使用云函数连接数据库或者用云函数来建微信公众号的后台时需要用到 IP 白名单,我们可以把这些 ip 都添加到白名单里面,这样云函数就可以做很多事情啦。 Buffer 文件流const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { const fileID = 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/cloudbase/1576500614167-520.png' const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent return buffer.toString('base64') } getServerImg(){ wx.cloud.callFunction({ name: 'downloadimg', success: res => { console.log("云函数返回的数据",res.result) this.setData({ img:res.result }) }, fail: err => { console.error('云函数调用失败:', err) } }) } "400px" height="200px" src="data:image/jpeg;base64,{{img}}">image> Buffer String Buffer JSON 图像处理 sharpsharp 是一个高速图像处理库,可以很方便的实现图片编辑操作,如裁剪、格式转换、旋转变换、滤镜添加、图片合成(如添加水印)、图片拼接等,支持 JPEG, PNG, WebP, TIFF, GIF 和 SVG 格式。在云函数端使用 sharp 来处理图片,而云存储则可以作为服务端和小程序端来传递图片的桥梁。 技术文档:sharp 官方技术文档 使用开发者工具新建一个 const cloud = require('wx-server-sdk') const fs = require('fs') const path = require('path') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const sharp = require('sharp'); exports.main = async (event, context) => { //这里换成自己的fileID,也可以在小程序端上传文件之后,把fileID传进来event.fileID const fileID = 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/1572315793628-366.png' //要用云函数处理图片,需要先下载图片,返回的图片类型为Buffer const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent //sharp对图片进行处理之后,保存为output.png,也可以直接保存为Buffer await sharp(buffer).rotate().resize(200).toFile('output.png') // 云函数读取模块目录下的图片,并上传到云存储 const fileStream = await fs.createReadStream(path.join(__dirname, 'output.png')) return await cloud.uploadFile({ cloudPath: 'sharpdemo.jpg', fileContent: fileStream, }) } 也可以让 sharp 不需要先 toFile 转成图片,而是直接转成 Buffer,这样就可以直接作为参数传给 fileContent 上传到云存储,如: const buffer2 = await sharp(buffer).rotate().resize(200).toBuffer(); return await cloud.uploadFile({ cloudPath: 'sharpdemo2.jpg', fileContent: buffer2, }) 连接数据库 MySQL公网连接数据库 MySQL技术文档:Sequelize const sequelize = new Sequelize('database', 'username', 'password', { host: 'localhost', //数据库地址,默认本机 port:'3306', dialect: 'mysql', pool: { //连接池设置 max: 5, //最大连接数 min: 0, //最小连接数 idle: 10000 }, }); 无论是MySQL,还是PostgreSQL、Redis、MongoDB等其他数据库,只要我们在 私有网络连接 MySQL默认情况下,云开发的函数部署在公共网络中,只可以访问公网。如果开发者需要访问腾讯云的 Redis、TencentDB、CVM、Kafka 等资源,需要建立私有网络来确保数据安全及连接安全。 连接数据库 Redisconst cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const Redis = require('ioredis') const redis = new Redis({ port: 6379, host: '10.168.0.15', family: 4, password: 'CloudBase2018', db: 0, }) exports.main = async (event, context) => { const wxContext = cloud.getWXContext() const cacheKey = wxContext.OPENID const cache = await redis.get(cacheKey) if (!cache) { const result = await new Promise((resolve, reject) => { setTimeout(() => resolve(Math.random()), 2000) }) redis.set(cacheKey, result, 'EX', 3600) return result } else { return cache } } 二维码 qrcode技术文档:node-qrcode Github 地址 邮件处理技术文档:Nodemailer Github 地址、Nodemailer 官方文档 使用开发者工具创建一个云函数,比如 nodemail,然后在 package.json 增加 nodemailer 最新版 latest 的依赖: "dependencies": { "nodemailer": "latest" } 发送邮件服务器:smtp.qq.com,使用 SSL,端口号 465 或 587 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { const nodemailer = require("nodemailer"); let transporter = nodemailer.createTransport({ host: "smtp.qq.com", //SMTP服务器地址 port: 465, //端口号,通常为465,587,25,不同的邮件客户端端口号可能不一样 secure: true, //如果端口是465,就为true;如果是587、25,就填false auth: { user: "344169902@qq.com", //你的邮箱账号 pass: "你的QQ邮箱授权码" //邮箱密码,QQ的需要是独立授权码 } }); let message = { from: '来自李东bbsky <344169902@qq.com>', //你的发件邮箱 to: '你要发送给谁', //你要发给谁 // cc:'', 支持cc 抄送 // bcc: '', 支持bcc 密送 subject: '欢迎大家参与云开发技术训练营活动', //支持text纯文字,html代码 text: '欢迎大家', html: '你好:' + '欢迎欢迎', attachments: [ //支持多种附件形式,可以是String, Buffer或Stream { filename: 'image.png', content: Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD/' + '//+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4U' + 'g9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC', 'base64' ), }, ] }; let res = await transporter.sendMail(message); return res; } Excel 文档处理Excel 是存储数据比较常见的格式,那如何让云函数拥有读写 Excel 文件的能力呢?我们可以在 Github 上搜索关键词“Node Excel”,去筛选 Star 比较多,条件比较契合的。 Github 地址:node-xlsx 使用开发者工具新建一个云函数,在 package.json 里添加 latest 最新版的 node-xlsx: "dependencies": { "wx-server-sdk": "latest", "node-xlsx": "latest" } 读取云存储的 Excel 文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const xlsx = require('node-xlsx'); const db = cloud.database() exports.main = async (event, context) => { const fileID = 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/china.csv' const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent const tasks = [] var sheets = xlsx.parse(buffer); sheets.forEach(function (sheet) { for (var rowId in sheet['data']) { console.log(rowId); var row = sheet['data'][rowId]; if (rowId > 0 && row) { const promise = db.collection('chinaexcel') .add({ data: { city: row[0], province: row[1], city_area: row[2], builtup_area: row[3], reg_pop: row[4], resident_pop: row[5], gdp: row[6] } }) tasks.push(promise) } } }); let result = await Promise.all(tasks).then(res => { return res }).catch(function (err) { return err }) return result } 将数据库里的数据保存为 CSV 技术文档:json2CSV HTTP 处理got、superagent、request、axios、request-promise 尽管云函数的 Nodejs 版本比较低(目前为 8.9),但绝大多数模块我们都可以使用 Nodejs 12 或 13 的环境来测试,不过有时候也要留意有些模块不支持 8.9,比如 got 10.0.1 以上的版本。node 中,http 模块也可作为客户端使用(发送请求),第三方模块 request 对其使用方法进行了封装,操作更方便!所以来介绍一下 request 模块 get 请求const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const rp = require('request-promise') exports.main = async (event, context) => { const options = { url: 'https://news-at.zhihu.com/api/4/news/latest', json: true, method: 'GET', }; return await rp(options) } post 请求结合文件流request('https://www.jmjc.tech/public/home/img/flower.png').pipe(fs.createWriteStream('./flower.png')) // 下载文件到本地 加解密 Cryptocrypto 模块是 nodejs 的核心模块之一,它提供了安全相关的功能,包含对 OpenSSL 的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装。由于 crypto 模块是内置模块,我们引入它是无需下载,就可以直接引入。 使用开发者工具新建一个云函数,比如 crypto,在 index.js 里输入以下代码,我们来了解一下 crypto 支持哪些加密算法,并以 MD5 加密为例: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const crypto = require('crypto'); exports.main = async (event, context) => { const hashes = crypto.getHashes(); //获取crypto支持的加密算法种类列表 //md5 加密 CloudBase2020 返回十六进制 var md5 = crypto.createHash('md5'); var message = 'CloudBase2020'; var digest = md5.update(message, 'utf8').digest('hex'); return { "crypto支持的加密算法种类":hashes, "md5加密返回的十六进制":digest }; } 将云函数部署之后调用从返回的结果我们可以了解到,云函数 crypto 模块支持 46 种加密算法。 发短信“qcloudsms_js”: “^0.1.1” const cloud = require('wx-server-sdk') const QcloudSms = require("qcloudsms_js") const appid = 1400284950 // 替换成您申请的云短信 AppID 以及 AppKey const appkey = "a33b602345f5bb866f040303ac6f98ca" const templateId = 472078 // 替换成您所申请模板 ID const smsSign = "统计小助理" // 替换成您所申请的签名 cloud.init() // 云函数入口函数 exports.main = async (event, context) => new Promise((resolve, reject) => { /*单发短信示例为完整示例,更多功能请直接替换以下代码*/ var qcloudsms = QcloudSms(appid, appkey); var ssender = qcloudsms.SmsSingleSender(); var params = ["1234", "15"]; // 获取发送短信的手机号码 var mobile = event.mobile // 获取手机号国家/地区码 var nationcode = event.nationcode ssender.sendWithParam(nationcode, mobile, templateId, params, smsSign, "", "", (err, res, resData) => { /*设置请求回调处理, 这里只是演示,您需要自定义相应处理逻辑*/ if (err) { console.log("err: ", err); reject({ err }) } else { resolve({ res: res.req, resData }) } } ); }) 使用开发者工具 wx.cloud.callFunction({ name: 'sendphone', data: { // mobile: '13217922526', mobile: '18565678773', nationcode: '86' }, success: res => { console.log('[云函数] [sendsms] 调用成功') console.log(res) }, fail: err => { console.error('[云函数] [sendsms] 调用失败', err) } })
2021-09-10 - 云开发基本功能演示
云开发基础·云开发基本功能演示 [视频]
2021-09-13 - 云开发入门
重磅打造的小程序学习路径课,从微信小程序到微信云开发体系化的学习,带来更加顺畅的学习体验。
2021-11-19