- Skyline|小程序吸顶、网格、瀑布流布局都拿下~
在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用 list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容 {{item.name}} ... 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表 ... 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表 ... 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-08-03 - 云开发短信跳小程序(无代码版)教程
写在前面你可以通过视频演示的方式学习本教程,更加利于学习和理解。 [视频] 一、能力介绍境内非个人主体的认证的小程序,开通静态网站并后,可以免鉴权下发支持跳转到相应小程序的短信。短信中会包含支持在微信内或微信外打开的静态网站链接,用户打开页面后可一键跳转至你的小程序。 这个链接的网页在外部浏览器是通过 URL Scheme 的方式来拉起微信打开主体小程序的。 本教程将介绍如何操作开通CMS内容管理系统进而操作使用短信跳转小程序能力,全程无需写代码。 如果你想要进行自定义开发,可以参照自定义开发教程进行逐步实现。 二、操作步骤1.下载微信开发者工具访问微信公众平台工具下载页,按照自己的系统版本下载安装开发者工具。建议安装【开发版 Nightly Build】版本。 [图片] 2.打开开发者工具并登录安装完开发者工具后,打开工具会弹出二维码登录框。使用你目标小程序具有开发者权限的微信号扫码登录。 点击创建小程序+号,会有自动填写默认名称和目录,你可以直接默认,当然也可以自定义路径和名称;在APPID处填写你目标小程序的appid;后端服务选择【小程序·云开发】 [图片] 设置完毕后,点击新建,等待项目创建完成并全部加载完毕,最终效果如下: [图片] 3.开通云开发并创建环境如果你之前从未使用过云开发,点击左上角工具栏中的【云开发】按钮,会弹出一个窗口,显示如下: [图片] 此时只需要点击开通按钮,并在弹出提示框中同意【服务协议】,即可开通云开发。 注意这里的开通是创建一个新的腾讯云账号,如果你不想有太多的账号,可以选择通过已有的腾讯云账号开通,会绑定你目前的已有腾讯云账号,在统一管理和计费方面更加方便。 开通之后需要创建一个云开发环境,上一步同意之后会自动弹出创建窗口,效果如下: [图片] 这个时候,我们需要选择【按量付费:腾讯云账户扣款】,创建一个按量付费环境。 如果你之前已经使用过云开发了,建议可以创建一个新的环境。每一个微信小程序有两个免费环境,所以可以创建一个新的按量付费环境(有免费额度),或者2个环境转其中一个为按量付费环境(依然有免费额度)。 4.开通内容管理CMS我们创建一个按量计费环境之后,就进入这个环境的控制台了。我们只需要在顶部导航栏中选择【更多-内容管理】,进入内容管理开通页面,效果如下: [图片] 我们点击开通按钮,会弹出一个确认窗口,告诉我们是在环境中部署CMS应用,需要的资源。 [图片] 点击下一步后,会弹出管理员设置框,我们输入管理员的ID和密码即可。 [图片] 确定之后,内容管理就进入部署阶段,大约3分钟左右。完成之后效果如下: [图片] 我们只需要打开访问地址,通过浏览器进入内容管理平台,输入我们设置的密码就可以进入内容管理的主页了 [图片] 我们在上图所示页面,点击【创建新项目】,弹出创建项目信息框,随意输入名称和ID,比如在这里我们输入名称为「短信」,id为「SMS」 创建成功后会在我的项目中有对应名字的项目,如下图所示: [图片] 我们点击项目,进入项目的详情,如下图所示: [图片] 4.创建短信活动项目进入项目详情后,我们发现左侧栏会有【营销工具】,我们点击其中的营销活动 [图片] 在右上角点击【新建】按钮,创建一个新的活动,内容信息如下: 活动名:用来标记描述活动的名称。活动开启:是否开启活动,如果关闭活动,将不能通过页面拉起小程序。活动开闭时间:在开启时间内,才可以正常的拉起小程序。跳转中间页图片:建议海报,用于在跳转页中展示大图,可以不上传,会有默认样式。小程序跳转路径:已发布上线的小程序中页面路径,不填则默认首页。小程序跳转参数:附带路径的传入参数,一般配合小程序代码联动。我们创建一个活动后展示如上图所示 5.创建短信群发任务在左侧栏点击【群发短信】,进入群发短信页面,点击右上角新建群发,进入信息页。 [图片] 我们需要填写以下3个信息: 短信内容:实际发送时目标手机收到的短信内容,短信的前后缀无法自定义。手机号码包:发送的目标手机号,可以填写一个或多个手机号,用回车或者逗号分割。活动:选择刚才我们创建的活动。填写完毕后,我们点击【创建】按钮,系统会自动进行短信发送,此时我们便可以在目标手机号中收到短信了。 [图片] 在群发短信的页面列表中,可以查看短信的发送状态,以及每一个手机的接收情况。 6.测试短信跳转小程序点击短信的链接后,会跳转到浏览器打开链接,展示如下效果(左默认、右海报) [图片] 一般页面会自动拉起微信打开小程序,个别机型或浏览器有拦截会导致打开失败,需要手动点击按钮才可以打开。 7.投放外部平台短信的链接可以复制发布到其他外部平台。 8.查看短信监控图表打开微信开发者工具并登录,进入 云开发控制台 > 运营分析 > 监控图表 > 短信监控,即可查看短信监控曲线图、短信发送记录。 [图片] 总结短信发送能力的体验是每个有免费配额的环境首月100条,如有超过额度的需求可前往开发者工具-云开发控制台-对应按量付费环境-资源包-短信资源包,进行购买。如当前资源包无法满足需求也可通过云开发 工单 提交申请[图片]短信发送时间:8:00 - 22:00短信发送能力支持小程序和小游戏发送国内短信的号码是1069开头,尾数是运营商随机号的号码发送成功代表请求发送短信成功,短信异步下发,实际状态以运营商回执为准。没有发送成功的短信不计费,可用性参阅服务等级协议相同内容短信对同一个手机号,30 秒内发送短信条数不超过1条;对同一个手机号,1自然日内发送短信条数不超过10条CMS配置渠道投放、数据统计可参考官方文档
2021-04-07 - 云开发短信跳小程序(自定义开发版)教程
写在前面如果你想要自主开发,但没有云开发相关经验,可以采用演示视频来学习本教程: [视频] 一、能力介绍境内非个人主体的认证的小程序,开通静态网站后,可以免鉴权下发支持跳转到相应小程序的短信。短信中会包含支持在微信内或微信外打开的静态网站链接,用户打开页面后可一键跳转至你的小程序。 这个链接的网页在外部浏览器是通过 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 - 提高性能的几种写法
任何优秀的大软件里面都是一个优秀的小程序。<br> 点完赞,在查看哦 setData 频繁使用setData会造成js阻塞问题,甚至卡顿,崩溃(崩溃没试过哈);所以我们应该如何用正确的姿势提高性能呢? 尽量把需要setData的内容放到一起setData,不要出现一个函数多个或者同时使用setData; 更新对象内的某个值,不应该整个对象重新更新,只需要更新对应的值即可,例如<br> [代码]this.setData({'userInfo.headImg': '更新的内容'})[代码]; 如果是列表数据,需要改变某个值变成true,也不应该全部数据更新一遍,只需要更新某一个值即可,例如<br> [代码]this.setData({[`list[${index}].show`]: true})[代码]; 页面不需要渲染的数据,尽量不要写在js里面的data里面,你可以定义一个全局参数都可以或者this.timer等生成一个全局变量; setData单次不能超过1M,超过会导致设置不成功,建议分页数据可以使用二维数组设置,例如 [代码]// 1.通过一个二维数组来存储数据 let feedList = [[array]]; // 2.维护一个页面变量值,加载完一次数据page++ let page = 1 // 3.页面每次滚动到底部,通过数据路径更新数据 onReachBottom:()=>{ fetchNewData().then((newVal)=>{ this.setData({ ['feedList[' + (page - 1) + ']']: newVal, }) } } // 4.最终我们的数据是[[array1],[array2]]这样的格式,然后通过wx:for遍历渲染数据 [代码] relativeToViewport 我们会发现很多时候,列表展示都是图文展示,而图片后端又没有提供缩略图,这会导致浏览器一次请求多张图片会非常消耗http请求,并且浏览器也有请求数量限制,例如chrome浏览器就限制一次性最多6个,严重还会影响我们业务接口请求,这时我们使用[代码]IntersectionObserver[代码]就太美妙了 解决思路 先创建对象实例[代码]wx.createIntersectionObserver()[代码]; 用数据列表遍历监听页面渲染的元素; 条件判断,动态设置状态显示图片,如果返回滚动则过滤不需要在setData,减轻js工作任务; js代码 [代码] data: { list: [ // 测试数据自己随便造 {id: 0, url: 'https://devapicard.itop123.com/files/img/20201213/20201213141209123634220.png'}, {id: 1, url: 'https://devapicard.itop123.com/files/img/20201213/20201213141209123634220.png'}, {id: 2, url: 'https://devapicard.itop123.com/files/img/20201213/20201213141209123634220.png'}, ] }, onReady() { let list = this.data.list; list.forEach((item,index)=>{ // 遍历监听元素在页面的位置 wx.createIntersectionObserver().relativeToViewport({bottom: 0}).observe(`.img-${index}`,res=>{ if (res.intersectionRatio > 0 && !list[index].show){ // intersectionRatio值大于0,说明元素出现在视图中了 // console.log(item, 'list', res) this.setData({ [`list[${index}].show`]: true }) } }) }) }, [代码] wxml代码 [代码] <view> <block wx:for="{{list}}" mode="aspectFill" wx:key="index"> <view class="test-img img-{{index}}"> <image class="image" wx:if="{{item.show}}" src="{{item.url}}"></image> </view> </block> </view> [代码] wxss代码 [代码].test-img{ width: 600rpx; height: 400rpx; margin-bottom: 20rpx; overflow: hidden; } .image{ width: 100%; } [代码] onPageScroll 这个监听页面滚动事件,输出顶部滚出页面多少距离,单位是px;这个事件非必要时不要写也不要输出内容,这样会很消耗性能,如果必须要使用到,我们要学会写节流或者防抖事件,减少过快的去setData;下面代码是网上复制,结合自己业务改造下即可 [代码]// 防抖 function debounce(fn, wait) { var timeout = null; return function() { if(timeout !== null) clearTimeout(timeout); timeout = setTimeout(fn, wait); } } // 处理函数 function handle() { console.log(Math.random()); } // 滚动事件 window.addEventListener('scroll', debounce(handle, 1000)); // 节流throttle代码(定时器): var throttle = function(func, delay) { var timer = null; return function() { var context = this; var args = arguments; if (!timer) { timer = setTimeout(function() { func.apply(context, args); timer = null; }, delay); } } } function handle() { console.log(Math.random()); } window.addEventListener('scroll', throttle(handle, 1000)); [代码] 代码分包 代码肯定是越写越多,需求也是越来越多的,小程序单个分包最大只能是2M,这就不能让我们的业务代码都写在主包,这会导致我们无法提交代码,并且也会导致我们打开小程序会很慢,体验很差。 为了解决这个问题,微信提供了小程序总共代码包支持最大16M,还支持我们分包功能,这样我们就可以开发更强大的功能,分包又分为普通分包和独立分包,他们又是什么关系,有何不同? 总的不同是,主包不能调用任何分包的东西,例如js,组件等,但是分包或者独立分包是可以调用或者使用主包的任何东西 分包:分包就是一个可以节省主包代码的一个普通分包,启动分包页面,会把主包下载,普通分包可以使用主包的 js 文件、template、wxss、自定义组件、插件等 独立分包:独立分包不依赖主包即可运行,不需要下载主包,可以很大程度上提升分包页面的启动速度,固并不能使用主包的 js,组件,wxss,插件等 预加载:分包后,如果想打开主包后,跳转不出现加载模块中的等待提示,可以使用预加载,这样就可以做到无缝跳转,增强了用户体验,跳转就像主包跳转主包页面一样顺滑。更多分包讲解请查看微信文档 [代码]{ "pages": [ "pages/index", "pages/logs" ], "subpackages": [ { "root": "moduleA", // 分包 "pages": [ "pages/rabbit", "pages/squirrel" ] }, { "root": "moduleB", // 独立分包 "pages": [ "pages/pear", "pages/pineapple" ], "independent": true // 独立分包的标志 } ], "preloadRule": { // 预加载写法 "pages/index": { "network": "all", "packages": ["moduleA"] // 需要预加载的分包 }, "pages/logs": { "packages": ["moduleB"] // 需要预加载的分包 } } } [代码] 结束 本次写作先到这里,更多丰富好用的内容,请等待下一期~ 都看完了,别忘了给个赞再走~,创作不容易,谢谢哦 本文与掘金号文章同步,欢迎查阅掘金对应的文章
2021-07-09 - 使用云函数+云调用,四步实现微信支付
微信支付是云开发原生支持的微信生态能力之一,开发者只需要简单调用相应的函数即可完成整套支付流程,安全又高效。部分优势包括: 无需关心证书、签名,支付流程简化;基于微信私有协议和私有链路,更加安全、高效;免运维,高可用性;按需扩容,弹性伸缩,按量计费,成本缩减;支持通过云函数接受支付回调,无需自建回调服务。流程对比:传统流程 vs 云开发[图片] 代码示例第 1 步:小程序调用云函数C 端用户发起支付流程后,小程序端调用云函数(此处假设云函数名为 [代码]makeOrder[代码]): // 小程序代码 wx.cloud.callFunction({ name: "makeOrder", data: { /* 开发者自定义参数 */ } }); 第 2 步:云函数生成订单,返回订单信息云函数 makeOrder 收到调用之后,使用微信服务端 SDK 提供的 API,无需证书和签名,可直接生成订单。 生成订单之后,利用 CloudPay.unifiedOrder() 统一下单接口,将订单信息返回给小程序。 CloudPay.unifiedOrder() 接口文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/open/pay/CloudPay.unifiedOrder.html // 云函数 makeOrder const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); exports.main = async (event, context) => { const res = await cloud.cloudPay.unifiedOrder({ body: "小秋TIT店-超市", outTradeNo: "1217752501201407033233368018", spbillCreateIp: "127.0.0.1", subMchId: "1900009231", totalFee: 1, envId: "test-f0b102", functionName: "payCallback" // 支付回调的函数名 }); return res; }; 第 3 步:小程序端发起支付小程序端收到云函数返回的订单信息后,发起支付: // 小程序代码 wx.cloud.callFunction({ name: "makeOrder", data: { /* 开发者自定义参数 */ }, success: (res) => { // 取得云函数返回的订单信息 const payment = res.result.payment; // 调起微信客户端支付 wx.requestPayment({ ...payment, success(res) { /* 成功回调 */ }, fail(res) { /* 失败回调 */ } }); } }); 第 4 步:使用云函数接收支付回调,完成支付流程用户完成付款之后,微信后台将会调用指定的云函数(此处假设名为 payCallback),传入的参数中会带有订单信息。 开发者可以在此云函数中,实现自己的发货、完成订单的逻辑。 // 云函数 payCallback exports.main = async (event, context) => { const { return_code, // 状态码 appid, // 小程序 AppID mch_id, // 微信支付的商户号 device_info, // 微信支付分配的终端设备号 openid, // 用户在商户appid下的唯一标识 trade_type, // 交易类型:JSAPI、NATIVE、APP bank_type // 银行类型 // ...... // 更多参数请参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8 } = event; /* 开发者自己的逻辑 */ // 向微信后台返回成功,否则微信后台将会重复调用此函数 return { errcode: 0 }; }; 相关文档:云函数文档: https://docs.cloudbase.net/cloud-function/introduce.html 云调用文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/openapi/openapi.html wx-server-sdk 文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/Cloud.html CloudPay.unifiedOrder() 接口文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/open/pay/CloudPay.unifiedOrder.html
2021-07-08 - 同一行2个view或者2个text时,后者如果内容较多会挤压前者,如何处理比较好?
[图片][图片] 类似备注。前面是字段名:备注。后面是备注的内容。如果备注的内容比较多,会挤压前者字段名。 另外,这类情况,在查询的时候,与输入内容的时候,应该采用什么标签比较合理?
2021-06-23 - 小技巧!CSS 整块文本溢出省略特性探究
今天的文章很有意思,讲一讲整块文本溢出省略打点的一些有意思的细节。 文本超长打点 我们都知道,到今天(2020/03/06),CSS 提供了两种方式便于我们进行文本超长的打点省略。 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 对于单行文本,使用单行省略: { width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } [图片] 而对于多行文本的超长省略,使用 [代码]-webkit-line-clamp[代码] 相关属性,兼容性也已经非常好了: { width: 200px; overflow : hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } [图片] CodePen Demo -- inline-block 实现整块的溢出打点 问题一:超长文本整块省略 基于上述的超长打点省略方案之下,会有一些变化的需求。譬如,我们有如下结构: Sb Coco FEUIUX Designer前端工程师 [图片] 对于上述超出的情况,我们希望对于超出文本长度的整一块 -- 前端工程师,整体被省略。 如果我们直接使用上述的方案,使用如下的 CSS,结果会是这样,并非我们期待的整块省略: .person-card__desc { width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } [图片] 将 [代码]display: inline[代码] 改为 [代码]display: inline-block[代码] 实现整块省略 这里,如果我们需要实现一整块的省略,只需要将包裹整块标签元素的 [代码]span[代码] 的 [代码]display[代码] 由 [代码]inline[代码] 改为 [代码]inline-block[代码] 即可。 .person-card__desc span { display: inline-block; } [图片] 这样,就可以实现,基于整块的内容的溢出省略了。完整的 Demo,你可以戳这里: CodePen Demo - 整块超长溢出打点省略 问题二:iOS 不支持整块超长溢出打点省略 然而,上述方案并非完美的。经过实测,上述方案在 iOS 和 Safari 下,没能生效,表现为这样: [图片] 查看规范 - CSS Basic User Interface Module Level 3 - text-overflow,究其原因,在于 [代码]text-overflow[代码] 只能对内联元素进行打点省略。(Chrome 对此可能做了一些优化,所以上述非 iOS 和 Safari 的场景是正常的) 所以猜测是因为经过了 [代码]display: inline-block[代码] 的转化后,已经不再是严格意义上的内联元素了。 解决方案,使用多行省略替代单行省略 当然,这里经过试验后,发现还是有解的,我们在开头还提到了一种多行省略的方案,我们将多行省略的代码替换单行省略,只是行数 [代码]-webkit-line-clamp: 2[代码] 改成一行即可 [代码]-webkit-line-clamp: 1[代码]。 .person-card__desc { width: 200px; white-space: normal; overflow : hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } .person-card__desc span { display: inline-block; } 这样,在 iOS/Safari 下也能完美实现整块的超长打点省略: [图片] CodePen Demo -- iOS 下的整块超长溢出打点省略方案 值得注意的是,在使用 [代码] -webkit-line-clamp[代码] 的方案的时候,一定要配合 [代码]white-space: normal[代码] 允许换行,而不是不换行。这一点,非常重要。 这样,我们就实现了全兼容的整块的超长打点省略了。 当然,[代码] -webkit-line-clamp[代码] 本身也是存在一定的兼容性问题的,实际使用的时候还需要具体去取舍。 最后 好了,本文到此结束,一个简单的 CSS 小技巧,希望对你有帮助 :) 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 作者:chokcoco
2021-03-15 - C# 写的windows服务进程守护实现
最近在做一个windows服务实现windows socket server读取系统硬件,RFID的内容给WEB网站使用。在测试过程中,发现RFID读取过程有时会导致服务无端中止。因为涉及到硬件调用,查不到具体的原因。所以就简单的处理,做一个进程守护,当windows的服务进程退出之后,自动重新开启。以前使用的方法是,另外做一个程序监测。因为那种方法比较麻烦,现在介绍一种更简单的方法。使用cmd使用。 1. 首先建立复制一下内容。新建一个autostart.bat的脚本文件。@echo off rem 定义循环间隔时间和监测的服务: set secs=60 set srvname="服务名称" echo. echo ======================================== echo == 查询计算机服务的状态, == echo == 每间隔%secs%秒种进行一次查询, == echo == 如发现其停止,则立即启动。 == echo ======================================== echo. echo 此脚本监测的服务是:%srvname% echo. if %srvname%. == . goto end :chkit set svrst=0 for /F "tokens=1* delims= " %%a in ('net start') do if /I "%%a %%b" == %srvname% set svrst=1 if %svrst% == 0 net start %srvname% set svrst= rem 下面的命令用于延时,否则可能会导致cpu单个核心满载。 ping -n %secs% 127.0.0.1 > nul goto chkit :end 双击该批处理文件,运行界面如下 ======================================== == 查询计算机服务的状态, == == 每间隔%secs%秒种进行一次查询, == == 如发现其停止,则立即启动。 == ======================================== 此脚本监测的服务是:%srvname% 如果%srvname%停止后,该批处理检测到后会重启该服务,界面如下 ======================================== == 查询计算机服务的状态, == == 每间隔%secs%秒种进行一次查询, == == 如发现其停止,则立即启动。 == ======================================== 此脚本监测的服务是:%srvname% %srvname% 服务正在启动 %srvname% 服务已经启动成功。 按实际情况修改 set srvname="服务名称" 这里的服务名称。 把文件保存到服务的安装文件夹。 2. C#相关代码 /// /// 启动服务 /// /// protected override void OnStart(string[] args) { StartTimer(); } System.Timers.Timer timer = new System.Timers.Timer(); /// /// 启动定时器 /// private void StartTimer() { timer.Interval = 1000; timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed); timer.Start(); } ///执行的 void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { if (!PortInUse(13001)) { //LogOuts.Debug("open:"+1); //启动socket StartWS(); } else { //LogOuts.Debug("open:" + 2); } //如果没执行命令,执行cmd监听。 if (!execdCmd) { execdCmd = true; execCmd(); } } private bool execdCmd = false; private void execCmd() { Process proc = null; try { proc = new Process(); proc.StartInfo.FileName = AppDomain.CurrentDomain.BaseDirectory+ @"\\autostart.bat"; proc.StartInfo.Arguments = string.Format("");//this is argument proc.StartInfo.CreateNoWindow = false; proc.Start(); proc.WaitForExit(); } catch (Exception ex) { Console.WriteLine("Exception Occurred :{0},{1}", ex.Message, ex.StackTrace.ToString()); } } https://github.com/users/bvaoih007/projects/1039 https://github.com/users/bvaoih007/projects/1039%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1039 http://github.com/users/bvaoih007/projects/1039 https://github.com/users/bvaoih007/projects/1039?fullscreen=true https://github.com/users/bvaoih007/projects/1039?I50zc=tzYkC https://github.com/users/bvaoih007/projects/1039?zk4Bu=79 https://github.com/users/bvaoih007/projects/1039?zyqcl=cqrj https://github.com/users/bvaoih007/projects/1039?zsqrc=83928 https://github.com/users/bvaoih007/projects/1039?36HRL/UQUj4=99 https://github.com/users/bvaoih007/projects/1039?URZ52/97546=59 https://github.com/users/bvaoih007/projects/1039?ubrkh/suty=23 https://github.com/users/bvaoih007/projects/1039?baahb/76049=38 https://github.com/users/bvaoih007/projects/1039/ https://github.com/users/bvaoih007/projects/1039?/#/tibbh=682Sl https://github.com/users/bvaoih007/projects/1039/#/zAyS2=42 https://github.com/users/bvaoih007/projects/1039?ukira/#/ihhu=27 https://github.com/users/rl9322179/projects/1025 https://github.com/users/rl9322179/projects/1025%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1025 http://github.com/users/rl9322179/projects/1025 https://github.com/users/rl9322179/projects/1025?fullscreen=true https://github.com/users/rl9322179/projects/1025?xx2l0=8FLdv https://github.com/users/rl9322179/projects/1025?wbe0d=38 https://github.com/users/rl9322179/projects/1025?xudkt=coxx https://github.com/users/rl9322179/projects/1025?udeco=25697 https://github.com/users/rl9322179/projects/1025?7E56k/04mMt=24 https://github.com/users/rl9322179/projects/1025?E362t/73400=30 https://github.com/users/rl9322179/projects/1025?mmele/kcwv=94 https://github.com/users/rl9322179/projects/1025?wvolw/69182=57 https://github.com/users/rl9322179/projects/1025/ https://github.com/users/rl9322179/projects/1025?/#/6wNC9=7uDXv https://github.com/users/rl9322179/projects/1025/#/7xtNu=98 https://github.com/users/rl9322179/projects/1025?mbctl/#/txcd=64 https://github.com/users/sptmh16557105/projects/1037 https://github.com/users/sptmh16557105/projects/1037%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1037 http://github.com/users/sptmh16557105/projects/1037 https://github.com/users/sptmh16557105/projects/1037?fullscreen=true https://github.com/users/sptmh16557105/projects/1037?ovI7e=IW6Zp https://github.com/users/sptmh16557105/projects/1037?q6vyp=30 https://github.com/users/sptmh16557105/projects/1037?ppfef=wpvp https://github.com/users/sptmh16557105/projects/1037?oiezi=99020 https://github.com/users/sptmh16557105/projects/1037?6EyhV/Mp59I=20 https://github.com/users/sptmh16557105/projects/1037?4V1OI/34814=60 https://github.com/users/sptmh16557105/projects/1037?imnyw/wpwy=38 https://github.com/users/sptmh16557105/projects/1037?qepyz/46992=28 https://github.com/users/sptmh16557105/projects/1037/ https://github.com/users/sptmh16557105/projects/1037?/#/p75f2=N3P4X https://github.com/users/sptmh16557105/projects/1037/#/Zy8Ze=44 https://github.com/users/sptmh16557105/projects/1037?opoyq/#/hmqe=48 https://github.com/users/bvaoih007/projects/1040 https://github.com/users/bvaoih007/projects/1040%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1040 http://github.com/users/bvaoih007/projects/1040 https://github.com/users/bvaoih007/projects/1040?fullscreen=true https://github.com/users/bvaoih007/projects/1040?N6V01=voTcF https://github.com/users/bvaoih007/projects/1040?NdM7c=86 https://github.com/users/bvaoih007/projects/1040?nknxl=belv https://github.com/users/bvaoih007/projects/1040?vtoff=02709 https://github.com/users/bvaoih007/projects/1040?0Ux5L/x79OW=81 https://github.com/users/bvaoih007/projects/1040?2Lm74/50591=02 https://github.com/users/bvaoih007/projects/1040?eweud/dkox=91 https://github.com/users/bvaoih007/projects/1040?vecon/00465=08 https://github.com/users/bvaoih007/projects/1040/ https://github.com/users/bvaoih007/projects/1040?/#/Uvb41=oMvtU https://github.com/users/bvaoih007/projects/1040/#/nWBb9=87 https://github.com/users/bvaoih007/projects/1040?wvouv/#/xxvu=53 https://github.com/users/sptmh16557105/projects/1038 https://github.com/users/sptmh16557105/projects/1038%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1038 http://github.com/users/sptmh16557105/projects/1038 https://github.com/users/sptmh16557105/projects/1038?fullscreen=true https://github.com/users/sptmh16557105/projects/1038?50LfL=no502 https://github.com/users/sptmh16557105/projects/1038?l84YY=37 https://github.com/users/sptmh16557105/projects/1038?cpnuf=cgye https://github.com/users/sptmh16557105/projects/1038?dffpe=51766 https://github.com/users/sptmh16557105/projects/1038?F0du2/M4M6M=31 https://github.com/users/sptmh16557105/projects/1038?gvvP6/73218=82 https://github.com/users/sptmh16557105/projects/1038?xevly/mldd=29 https://github.com/users/sptmh16557105/projects/1038?pyely/26234=24 https://github.com/users/sptmh16557105/projects/1038/ https://github.com/users/sptmh16557105/projects/1038?/#/57vgu=Xul5o https://github.com/users/sptmh16557105/projects/1038/#/01MF8=43 https://github.com/users/sptmh16557105/projects/1038?vlxcm/#/nowy=65 https://github.com/users/rl9322179/projects/1026 https://github.com/users/rl9322179/projects/1026%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1026 http://github.com/users/rl9322179/projects/1026 https://github.com/users/rl9322179/projects/1026?fullscreen=true https://github.com/users/rl9322179/projects/1026?r8aeL=L9bLJ https://github.com/users/rl9322179/projects/1026?b95aa=59 https://github.com/users/rl9322179/projects/1026?btbir=takl https://github.com/users/rl9322179/projects/1026?rckrt=96823 https://github.com/users/rl9322179/projects/1026?3ci11/3Kv48=11 https://github.com/users/rl9322179/projects/1026?AA6rt/81424=78 https://github.com/users/rl9322179/projects/1026?rdecb/busm=85 https://github.com/users/rl9322179/projects/1026?vajjr/75378=96 https://github.com/users/rl9322179/projects/1026/ https://github.com/users/rl9322179/projects/1026?/#/JmjKK=CmE9d https://github.com/users/rl9322179/projects/1026/#/ECtc6=32 https://github.com/users/rl9322179/projects/1026?ijlse/#/reuk=90 https://github.com/users/bvaoih007/projects/1041 https://github.com/users/bvaoih007/projects/1041%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1041 http://github.com/users/bvaoih007/projects/1041 https://github.com/users/bvaoih007/projects/1041?fullscreen=true https://github.com/users/bvaoih007/projects/1041?Ey7H6=GY34n https://github.com/users/bvaoih007/projects/1041?0FfV3=64 https://github.com/users/bvaoih007/projects/1041?iqxyy=qzpn https://github.com/users/bvaoih007/projects/1041?fiepr=99256 https://github.com/users/bvaoih007/projects/1041?I30Gx/Y3EQn=59 https://github.com/users/bvaoih007/projects/1041?0gypP/61533=44 https://github.com/users/bvaoih007/projects/1041?rigpg/erhv=01 https://github.com/users/bvaoih007/projects/1041?noezg/45837=23 https://github.com/users/bvaoih007/projects/1041/ https://github.com/users/bvaoih007/projects/1041?/#/Nry94=23iPf https://github.com/users/bvaoih007/projects/1041/#/jRMl9=27 https://github.com/users/bvaoih007/projects/1041?rxifg/#/vizf=16 https://github.com/users/sptmh16557105/projects/1039 https://github.com/users/sptmh16557105/projects/1039%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1039 http://github.com/users/sptmh16557105/projects/1039 https://github.com/users/sptmh16557105/projects/1039?fullscreen=true https://github.com/users/sptmh16557105/projects/1039?O9wHX=o6i9w https://github.com/users/sptmh16557105/projects/1039?RN4y6=01 https://github.com/users/sptmh16557105/projects/1039?eowhp=fryq https://github.com/users/sptmh16557105/projects/1039?rfnwf=14357 https://github.com/users/sptmh16557105/projects/1039?pNYoG/4FNE7=45 https://github.com/users/sptmh16557105/projects/1039?WhG9f/36097=27 https://github.com/users/sptmh16557105/projects/1039?zxrig/fqzi=74 https://github.com/users/sptmh16557105/projects/1039?oaiza/94963=52 https://github.com/users/sptmh16557105/projects/1039/ https://github.com/users/sptmh16557105/projects/1039?/#/qXFIN=GP9aH https://github.com/users/sptmh16557105/projects/1039/#/FQ4aG=16 https://github.com/users/sptmh16557105/projects/1039?iwgon/#/gwza=11 https://github.com/users/rl9322179/projects/1027 https://github.com/users/rl9322179/projects/1027%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1027 http://github.com/users/rl9322179/projects/1027 https://github.com/users/rl9322179/projects/1027?fullscreen=true https://github.com/users/rl9322179/projects/1027?Q043F=3VFrz https://github.com/users/rl9322179/projects/1027?Hev13=54 https://github.com/users/rl9322179/projects/1027?xiznh=ighh https://github.com/users/rl9322179/projects/1027?gqwiv=41475 https://github.com/users/rl9322179/projects/1027?NZv7y/4YnNo=41 https://github.com/users/rl9322179/projects/1027?11xR5/67791=59 https://github.com/users/rl9322179/projects/1027?ozfny/hgri=63 https://github.com/users/rl9322179/projects/1027?xxovp/60472=77 https://github.com/users/rl9322179/projects/1027/ https://github.com/users/rl9322179/projects/1027?/#/X0g5g=i582P https://github.com/users/rl9322179/projects/1027/#/Y2yFx=28 https://github.com/users/rl9322179/projects/1027?geppv/#/pxvx=72 https://github.com/users/bvaoih007/projects/1042 https://github.com/users/bvaoih007/projects/1042%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1042 http://github.com/users/bvaoih007/projects/1042 https://github.com/users/bvaoih007/projects/1042?fullscreen=true https://github.com/users/bvaoih007/projects/1042?3k8yB=Jp20j https://github.com/users/bvaoih007/projects/1042?5CCT2=82 https://github.com/users/bvaoih007/projects/1042?sazjs=jgtt https://github.com/users/bvaoih007/projects/1042?rjhpr=02662 https://github.com/users/bvaoih007/projects/1042?1ABR6/IpY2Q=17 https://github.com/users/bvaoih007/projects/1042?4zgrI/81279=92 https://github.com/users/bvaoih007/projects/1042?ckaas/bcra=38 https://github.com/users/bvaoih007/projects/1042?arkiz/84443=04 https://github.com/users/bvaoih007/projects/1042/ https://github.com/users/bvaoih007/projects/1042?/#/9tGKK=ICRg2 https://github.com/users/bvaoih007/projects/1042/#/at06S=19 https://github.com/users/bvaoih007/projects/1042?jjahc/#/bjzj=90 https://github.com/users/rl9322179/projects/1028 https://github.com/users/rl9322179/projects/1028%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1028 http://github.com/users/rl9322179/projects/1028 https://github.com/users/rl9322179/projects/1028?fullscreen=true https://github.com/users/rl9322179/projects/1028?379PU=emD3m https://github.com/users/rl9322179/projects/1028?H6LW1=77 https://github.com/users/rl9322179/projects/1028?ymfux=depf https://github.com/users/rl9322179/projects/1028?dxplw=37509 https://github.com/users/rl9322179/projects/1028?fP62E/xG950=13 https://github.com/users/rl9322179/projects/1028?L1Mpg/54267=68 https://github.com/users/rl9322179/projects/1028?ynnuh/hhgn=79 https://github.com/users/rl9322179/projects/1028?eepwn/38937=78 https://github.com/users/rl9322179/projects/1028/ https://github.com/users/rl9322179/projects/1028?/#/v3uy1=6OXvE https://github.com/users/rl9322179/projects/1028/#/19L4v=53 https://github.com/users/rl9322179/projects/1028?heonp/#/muwv=37 https://github.com/users/sptmh16557105/projects/1040 https://github.com/users/sptmh16557105/projects/1040%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1040 http://github.com/users/sptmh16557105/projects/1040 https://github.com/users/sptmh16557105/projects/1040?fullscreen=true https://github.com/users/sptmh16557105/projects/1040?4BBZg=eFT7D https://github.com/users/sptmh16557105/projects/1040?HAAq6=34 https://github.com/users/sptmh16557105/projects/1040?hcgqj=ukek https://github.com/users/sptmh16557105/projects/1040?iaaaz=55741 https://github.com/users/sptmh16557105/projects/1040?u7MTV/KfoDx=46 https://github.com/users/sptmh16557105/projects/1040?OgTLe/48713=26 https://github.com/users/sptmh16557105/projects/1040?vnowl/wnnc=46 https://github.com/users/sptmh16557105/projects/1040?kvvmt/55803=16 https://github.com/users/sptmh16557105/projects/1040/ https://github.com/users/sptmh16557105/projects/1040?/#/eFuoG=d8d62 https://github.com/users/sptmh16557105/projects/1040/#/7nF2D=94 https://github.com/users/sptmh16557105/projects/1040?wwuol/#/gulw=15 https://github.com/users/bvaoih007/projects/1043 https://github.com/users/bvaoih007/projects/1043%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1043 http://github.com/users/bvaoih007/projects/1043 https://github.com/users/bvaoih007/projects/1043?fullscreen=true https://github.com/users/bvaoih007/projects/1043?jujQt=U3bbA https://github.com/users/bvaoih007/projects/1043?MSStK=11 https://github.com/users/bvaoih007/projects/1043?lrkrk=jbdc https://github.com/users/bvaoih007/projects/1043?bmqjk=33763 https://github.com/users/bvaoih007/projects/1043?6I3lU/jIm7T=30 https://github.com/users/bvaoih007/projects/1043?IU8T8/98745=87 https://github.com/users/bvaoih007/projects/1043?uuura/mqut=72 https://github.com/users/bvaoih007/projects/1043?uzbjl/73065=51 https://github.com/users/bvaoih007/projects/1043/ https://github.com/users/bvaoih007/projects/1043?/#/216Km=827l1 https://github.com/users/bvaoih007/projects/1043/#/al92q=45 https://github.com/users/bvaoih007/projects/1043?uuzbd/#/uucs=59 https://github.com/users/sptmh16557105/projects/1041 https://github.com/users/sptmh16557105/projects/1041%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1041 http://github.com/users/sptmh16557105/projects/1041 https://github.com/users/sptmh16557105/projects/1041?fullscreen=true https://github.com/users/sptmh16557105/projects/1041?Fv008=MuqZz https://github.com/users/sptmh16557105/projects/1041?4Tu52=70 https://github.com/users/sptmh16557105/projects/1041?yfooe=rkit https://github.com/users/sptmh16557105/projects/1041?mrmls=44436 https://github.com/users/sptmh16557105/projects/1041?139Ms/n1EX7=35 https://github.com/users/sptmh16557105/projects/1041?PH2nN/51340=91 https://github.com/users/sptmh16557105/projects/1041?lizub/wpou=23 https://github.com/users/sptmh16557105/projects/1041?gwdwg/97170=10 https://github.com/users/sptmh16557105/projects/1041/ https://github.com/users/sptmh16557105/projects/1041?/#/unv88=vVn70 https://github.com/users/sptmh16557105/projects/1041/#/mcMuN=69 https://github.com/users/sptmh16557105/projects/1041?wncuv/#/cfol=10 https://github.com/users/rl9322179/projects/1029 https://github.com/users/rl9322179/projects/1029%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1029 http://github.com/users/rl9322179/projects/1029 https://github.com/users/rl9322179/projects/1029?fullscreen=true https://github.com/users/rl9322179/projects/1029?BwkKe=DtO6S https://github.com/users/rl9322179/projects/1029?2KvS4=75 https://github.com/users/rl9322179/projects/1029?fsmel=vvwb https://github.com/users/rl9322179/projects/1029?udwwe=56875 https://github.com/users/rl9322179/projects/1029?Lsv66/1wU98=46 https://github.com/users/rl9322179/projects/1029?8C1d0/45830=06 https://github.com/users/rl9322179/projects/1029?dunvt/wvnn=65 https://github.com/users/rl9322179/projects/1029?veeeu/13109=88 https://github.com/users/rl9322179/projects/1029/ https://github.com/users/rl9322179/projects/1029?/#/s9w4T=cOk21 https://github.com/users/rl9322179/projects/1029/#/wlfT6=87 https://github.com/users/rl9322179/projects/1029?ekbvl/#/ckct=66 https://github.com/users/bvaoih007/projects/1044 https://github.com/users/bvaoih007/projects/1044%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1044 http://github.com/users/bvaoih007/projects/1044 https://github.com/users/bvaoih007/projects/1044?fullscreen=true https://github.com/users/bvaoih007/projects/1044?6y7yi=0PnQI https://github.com/users/bvaoih007/projects/1044?nRe2Z=27 https://github.com/users/bvaoih007/projects/1044?yfyqy=frre https://github.com/users/bvaoih007/projects/1044?gzgog=76204 https://github.com/users/bvaoih007/projects/1044?N5p2f/oIh5x=49 https://github.com/users/bvaoih007/projects/1044?eNxX7/25596=33 https://github.com/users/bvaoih007/projects/1044?rwwpg/qier=13 https://github.com/users/bvaoih007/projects/1044?ygywn/17203=23 https://github.com/users/bvaoih007/projects/1044/ https://github.com/users/bvaoih007/projects/1044?/#/rR7hy=V9vZN https://github.com/users/bvaoih007/projects/1044/#/RI492=68 https://github.com/users/bvaoih007/projects/1044?frfho/#/gnhe=83 https://github.com/users/sptmh16557105/projects/1042 https://github.com/users/sptmh16557105/projects/1042%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1042 http://github.com/users/sptmh16557105/projects/1042 https://github.com/users/sptmh16557105/projects/1042?fullscreen=true https://github.com/users/sptmh16557105/projects/1042?Ew3gO=f203u https://github.com/users/sptmh16557105/projects/1042?MO6XP=57 https://github.com/users/sptmh16557105/projects/1042?pexqd=gnyw https://github.com/users/sptmh16557105/projects/1042?fhgex=39527 https://github.com/users/sptmh16557105/projects/1042?8V6qq/9Yo03=18 https://github.com/users/sptmh16557105/projects/1042?o6wYx/21902=25 https://github.com/users/sptmh16557105/projects/1042?oyuuo/ohoo=34 https://github.com/users/sptmh16557105/projects/1042?mvueq/61624=53 https://github.com/users/sptmh16557105/projects/1042/ https://github.com/users/sptmh16557105/projects/1042?/#/4g7NH=f3YN5 https://github.com/users/sptmh16557105/projects/1042/#/X8V2g=43 https://github.com/users/sptmh16557105/projects/1042?eoodv/#/hmxd=37 https://github.com/users/rl9322179/projects/1030 https://github.com/users/rl9322179/projects/1030%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1030 http://github.com/users/rl9322179/projects/1030 https://github.com/users/rl9322179/projects/1030?fullscreen=true https://github.com/users/rl9322179/projects/1030?LdAZu=0lkuQ https://github.com/users/rl9322179/projects/1030?JczSU=89 https://github.com/users/rl9322179/projects/1030?sbdtd=ckuq https://github.com/users/rl9322179/projects/1030?bjzkd=20615 https://github.com/users/rl9322179/projects/1030?DZa02/6I6UD=45 https://github.com/users/rl9322179/projects/1030?j4T0a/34117=61 https://github.com/users/rl9322179/projects/1030?mkkbj/bzbz=49 https://github.com/users/rl9322179/projects/1030?qdbkr/82264=06 https://github.com/users/rl9322179/projects/1030/ https://github.com/users/rl9322179/projects/1030?/#/B9Mu2=S2ZzM https://github.com/users/rl9322179/projects/1030/#/3sILi=41 https://github.com/users/rl9322179/projects/1030?zkjtb/#/sjad=50 https://github.com/users/bvaoih007/projects/1045 https://github.com/users/bvaoih007/projects/1045%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1045 http://github.com/users/bvaoih007/projects/1045 https://github.com/users/bvaoih007/projects/1045?fullscreen=true https://github.com/users/bvaoih007/projects/1045?eO7o3=7O307 https://github.com/users/bvaoih007/projects/1045?V6qgw=69 https://github.com/users/bvaoih007/projects/1045?nhwvn=xfhy https://github.com/users/bvaoih007/projects/1045?ofhiy=79273 https://github.com/users/bvaoih007/projects/1045?8YzX9/HNxyq=39 https://github.com/users/bvaoih007/projects/1045?1O975/32660=59 https://github.com/users/bvaoih007/projects/1045?eixqi/znqz=96 https://github.com/users/bvaoih007/projects/1045?xvohr/08627=73 https://github.com/users/bvaoih007/projects/1045/ https://github.com/users/bvaoih007/projects/1045?/#/9Xq2r=5Elvv https://github.com/users/bvaoih007/projects/1045/#/js8A1=09 https://github.com/users/bvaoih007/projects/1045?hzovy/#/mrib=13 https://github.com/users/sptmh16557105/projects/1043 https://github.com/users/sptmh16557105/projects/1043%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1043 http://github.com/users/sptmh16557105/projects/1043 https://github.com/users/sptmh16557105/projects/1043?fullscreen=true https://github.com/users/sptmh16557105/projects/1043?Y95P0=qwo6d https://github.com/users/sptmh16557105/projects/1043?Z23V7=71 https://github.com/users/sptmh16557105/projects/1043?vozyw=dmqh https://github.com/users/sptmh16557105/projects/1043?onpqh=09341 https://github.com/users/sptmh16557105/projects/1043?d9o3g/q6OOz=21 https://github.com/users/sptmh16557105/projects/1043?vpnQ7/84481=76 https://github.com/users/sptmh16557105/projects/1043?dpqfd/vxwq=33 https://github.com/users/sptmh16557105/projects/1043?ofepp/89982=23 https://github.com/users/sptmh16557105/projects/1043/ https://github.com/users/sptmh16557105/projects/1043?/#/q78n4=Nevgx https://github.com/users/sptmh16557105/projects/1043/#/Nx2W1=42 https://github.com/users/sptmh16557105/projects/1043?yohne/#/nmhq=95 https://github.com/users/rl9322179/projects/1031 https://github.com/users/rl9322179/projects/1031%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1031 http://github.com/users/rl9322179/projects/1031 https://github.com/users/rl9322179/projects/1031?fullscreen=true https://github.com/users/rl9322179/projects/1031?r4skq=3aAjl https://github.com/users/rl9322179/projects/1031?jYSTk=71 https://github.com/users/rl9322179/projects/1031?qczcp=jpap https://github.com/users/rl9322179/projects/1031?ctlpj=07396 https://github.com/users/rl9322179/projects/1031?aTYl4/t8p2Z=18 https://github.com/users/rl9322179/projects/1031?j9Yi0/73639=14 https://github.com/users/rl9322179/projects/1031?thrsj/skiz=98 https://github.com/users/rl9322179/projects/1031?ythht/26354=72 https://github.com/users/rl9322179/projects/1031/ https://github.com/users/rl9322179/projects/1031?/#/Rh6az=c1q67 https://github.com/users/rl9322179/projects/1031/#/Mcv90=52 https://github.com/users/rl9322179/projects/1031?qpqyh/#/bbza=82 https://github.com/users/bvaoih007/projects/1046 https://github.com/users/bvaoih007/projects/1046%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1046 http://github.com/users/bvaoih007/projects/1046 https://github.com/users/bvaoih007/projects/1046?fullscreen=true https://github.com/users/bvaoih007/projects/1046?0KCTL=XdC5L https://github.com/users/bvaoih007/projects/1046?2F3GV=34 https://github.com/users/bvaoih007/projects/1046?iltll=futo https://github.com/users/bvaoih007/projects/1046?mneog=35110 https://github.com/users/bvaoih007/projects/1046?X0711/8X3UM=65 https://github.com/users/bvaoih007/projects/1046?FvcUf/91789=40 https://github.com/users/bvaoih007/projects/1046?pnvcp/txlm=27 https://github.com/users/bvaoih007/projects/1046?txoxf/80474=08 https://github.com/users/bvaoih007/projects/1046/ https://github.com/users/bvaoih007/projects/1046?/#/33tW4=7G46n https://github.com/users/bvaoih007/projects/1046/#/m2Tud=00 https://github.com/users/bvaoih007/projects/1046?pofvg/#/xnwe=13 https://github.com/users/sptmh16557105/projects/1044 https://github.com/users/sptmh16557105/projects/1044%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1044 http://github.com/users/sptmh16557105/projects/1044 https://github.com/users/sptmh16557105/projects/1044?fullscreen=true https://github.com/users/sptmh16557105/projects/1044?N7E6N=75737 https://github.com/users/sptmh16557105/projects/1044?d7t00=51 https://github.com/users/sptmh16557105/projects/1044?utttv=bbvk https://github.com/users/sptmh16557105/projects/1044?bcute=75444 https://github.com/users/sptmh16557105/projects/1044?jlE5B/nvkE8=51 https://github.com/users/sptmh16557105/projects/1044?8SF6u/27211=41 https://github.com/users/sptmh16557105/projects/1044?tffcl/jvmw=74 https://github.com/users/sptmh16557105/projects/1044?kjbbt/81810=75 https://github.com/users/sptmh16557105/projects/1044/ https://github.com/users/sptmh16557105/projects/1044?/#/sCD9D=17KFl https://github.com/users/sptmh16557105/projects/1044/#/68n45=58 https://github.com/users/sptmh16557105/projects/1044?mkvms/#/jctl=03 https://github.com/users/rl9322179/projects/1032 https://github.com/users/rl9322179/projects/1032%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1032 http://github.com/users/rl9322179/projects/1032 https://github.com/users/rl9322179/projects/1032?fullscreen=true https://github.com/users/rl9322179/projects/1032?6xWc6=N3EkC https://github.com/users/rl9322179/projects/1032?8OmN2=44 https://github.com/users/rl9322179/projects/1032?nfuft=loun https://github.com/users/rl9322179/projects/1032?uluco=49026 https://github.com/users/rl9322179/projects/1032?TmV5V/7ovNv=28 https://github.com/users/rl9322179/projects/1032?6tM88/99154=89 https://github.com/users/rl9322179/projects/1032?kcfxk/fcuc=81 https://github.com/users/rl9322179/projects/1032?wltck/90171=99 https://github.com/users/rl9322179/projects/1032/ https://github.com/users/rl9322179/projects/1032?/#/QrqH6=8UCb2 https://github.com/users/rl9322179/projects/1032/#/3BtQ6=56 https://github.com/users/rl9322179/projects/1032?sbkja/#/ctit=97 https://github.com/users/bvaoih007/projects/1047 https://github.com/users/bvaoih007/projects/1047%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1047 http://github.com/users/bvaoih007/projects/1047 https://github.com/users/bvaoih007/projects/1047?fullscreen=true https://github.com/users/bvaoih007/projects/1047?UTWGT=06u62 https://github.com/users/bvaoih007/projects/1047?3lGCl=44 https://github.com/users/bvaoih007/projects/1047?pocwx=tveu https://github.com/users/bvaoih007/projects/1047?ncpxw=45100 https://github.com/users/bvaoih007/projects/1047?dUFvU/MTEuD=05 https://github.com/users/bvaoih007/projects/1047?867Nv/12876=11 https://github.com/users/bvaoih007/projects/1047?fptot/ouoe=00 https://github.com/users/bvaoih007/projects/1047?omplo/97287=96 https://github.com/users/bvaoih007/projects/1047/ https://github.com/users/bvaoih007/projects/1047?/#/lTo2M=wx24G https://github.com/users/bvaoih007/projects/1047/#/edPO9=97 https://github.com/users/bvaoih007/projects/1047?fvvxd/#/fveu=75 https://github.com/users/sptmh16557105/projects/1045 https://github.com/users/sptmh16557105/projects/1045%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1045 http://github.com/users/sptmh16557105/projects/1045 https://github.com/users/sptmh16557105/projects/1045?fullscreen=true https://github.com/users/sptmh16557105/projects/1045?3z4C0=u0jJt https://github.com/users/sptmh16557105/projects/1045?Dl3It=52 https://github.com/users/sptmh16557105/projects/1045?isjmc=jqji https://github.com/users/sptmh16557105/projects/1045?murku=98138 https://github.com/users/sptmh16557105/projects/1045?dUCL0/4A3D9=25 https://github.com/users/sptmh16557105/projects/1045?61rD0/84021=49 https://github.com/users/sptmh16557105/projects/1045?mskik/mskt=86 https://github.com/users/sptmh16557105/projects/1045?kjaar/44633=22 https://github.com/users/sptmh16557105/projects/1045/ https://github.com/users/sptmh16557105/projects/1045?/#/9Ji2d=aMK6D https://github.com/users/sptmh16557105/projects/1045/#/ID5M0=45 https://github.com/users/sptmh16557105/projects/1045?llmlt/#/arsk=41 https://github.com/users/rl9322179/projects/1033 https://github.com/users/rl9322179/projects/1033%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1033 http://github.com/users/rl9322179/projects/1033 https://github.com/users/rl9322179/projects/1033?fullscreen=true https://github.com/users/rl9322179/projects/1033?qAdi9=0C42L https://github.com/users/rl9322179/projects/1033?ll9rs=76 https://github.com/users/rl9322179/projects/1033?cbskr=qkuj https://github.com/users/rl9322179/projects/1033?scarz=17413 https://github.com/users/rl9322179/projects/1033?99s7a/1Q7dI=09 https://github.com/users/rl9322179/projects/1033?04t2M/38636=03 https://github.com/users/rl9322179/projects/1033?kzcaq/tqam=85 https://github.com/users/rl9322179/projects/1033?tzcjd/46886=31 https://github.com/users/rl9322179/projects/1033/ https://github.com/users/rl9322179/projects/1033?/#/ql898=q2DcK https://github.com/users/rl9322179/projects/1033/#/ZTRC9=86 https://github.com/users/rl9322179/projects/1033?kdcar/#/rsrm=71 https://github.com/users/bvaoih007/projects/1048 https://github.com/users/bvaoih007/projects/1048%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1048 http://github.com/users/bvaoih007/projects/1048 https://github.com/users/bvaoih007/projects/1048?fullscreen=true https://github.com/users/bvaoih007/projects/1048?y58RT=83H85 https://github.com/users/bvaoih007/projects/1048?A4uyl=36 https://github.com/users/bvaoih007/projects/1048?rrkcb=bttl https://github.com/users/bvaoih007/projects/1048?hqiaz=58424 https://github.com/users/bvaoih007/projects/1048?uR36Y/cikI7=88 https://github.com/users/bvaoih007/projects/1048?5iyQs/50921=48 https://github.com/users/bvaoih007/projects/1048?ittyi/ssyk=06 https://github.com/users/bvaoih007/projects/1048?hrzic/85244=65 https://github.com/users/bvaoih007/projects/1048/ https://github.com/users/bvaoih007/projects/1048?/#/t25IA=21izR https://github.com/users/bvaoih007/projects/1048/#/uLYT6=16 https://github.com/users/bvaoih007/projects/1048?ihtbc/#/bkqi=58 https://github.com/users/sptmh16557105/projects/1046 https://github.com/users/sptmh16557105/projects/1046%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1046 http://github.com/users/sptmh16557105/projects/1046 https://github.com/users/sptmh16557105/projects/1046?fullscreen=true https://github.com/users/sptmh16557105/projects/1046?mD96q=xQp6d https://github.com/users/sptmh16557105/projects/1046?Myh0m=06 https://github.com/users/sptmh16557105/projects/1046?gwymf=wovx https://github.com/users/sptmh16557105/projects/1046?dpuvv=29660 https://github.com/users/sptmh16557105/projects/1046?3p2v7/fN8EU=13 https://github.com/users/sptmh16557105/projects/1046?o09q7/34833=76 https://github.com/users/sptmh16557105/projects/1046?nuxfy/emuo=45 https://github.com/users/sptmh16557105/projects/1046?nqgex/95516=58 https://github.com/users/sptmh16557105/projects/1046/ https://github.com/users/sptmh16557105/projects/1046?/#/9cLaq=693UH https://github.com/users/sptmh16557105/projects/1046/#/SZ8CA=58 https://github.com/users/sptmh16557105/projects/1046?yktzy/#/ybut=52 https://github.com/users/rl9322179/projects/1034 https://github.com/users/rl9322179/projects/1034%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1034 http://github.com/users/rl9322179/projects/1034 https://github.com/users/rl9322179/projects/1034?fullscreen=true https://github.com/users/rl9322179/projects/1034?rQXNy=W0O9h https://github.com/users/rl9322179/projects/1034?9YH2W=33 https://github.com/users/rl9322179/projects/1034?nprfr=eyox https://github.com/users/rl9322179/projects/1034?oexgv=92616 https://github.com/users/rl9322179/projects/1034?xyGIr/X0gyp=55 https://github.com/users/rl9322179/projects/1034?4oo88/34326=08 https://github.com/users/rl9322179/projects/1034?vyfhw/fznn=34 https://github.com/users/rl9322179/projects/1034?oohxg/67985=64 https://github.com/users/rl9322179/projects/1034/ https://github.com/users/rl9322179/projects/1034?/#/E9RIF=zE1Wi https://github.com/users/rl9322179/projects/1034/#/vO673=56 https://github.com/users/rl9322179/projects/1034?pfiip/#/wgwi=21 https://github.com/users/bvaoih007/projects/1049 https://github.com/users/bvaoih007/projects/1049%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1049 http://github.com/users/bvaoih007/projects/1049 https://github.com/users/bvaoih007/projects/1049?fullscreen=true https://github.com/users/bvaoih007/projects/1049?Ya7j6=zH2Xh https://github.com/users/bvaoih007/projects/1049?14A4b=48 https://github.com/users/bvaoih007/projects/1049?bsgqh=jkgr https://github.com/users/bvaoih007/projects/1049?aygyq=28875 https://github.com/users/bvaoih007/projects/1049?760g1/4axkY=21 https://github.com/users/bvaoih007/projects/1049?OBsQ5/05787=30 https://github.com/users/bvaoih007/projects/1049?zgboo/zhrs=36 https://github.com/users/bvaoih007/projects/1049?bgoqx/41545=51 https://github.com/users/bvaoih007/projects/1049/ https://github.com/users/bvaoih007/projects/1049?/#/93xoR=48Ga2 https://github.com/users/bvaoih007/projects/1049/#/K1S6Z=51 https://github.com/users/bvaoih007/projects/1049?ggrga/#/xyah=90 https://github.com/users/sptmh16557105/projects/1047 https://github.com/users/sptmh16557105/projects/1047%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1047 http://github.com/users/sptmh16557105/projects/1047 https://github.com/users/sptmh16557105/projects/1047?fullscreen=true https://github.com/users/sptmh16557105/projects/1047?2n0uV=j1caU https://github.com/users/sptmh16557105/projects/1047?8nBcu=68 https://github.com/users/sptmh16557105/projects/1047?cswat=navm https://github.com/users/sptmh16557105/projects/1047?blbnd=46760 https://github.com/users/sptmh16557105/projects/1047?K8190/WlcuL=64 https://github.com/users/sptmh16557105/projects/1047?2d3JW/44511=40 https://github.com/users/sptmh16557105/projects/1047?bvuev/kjus=01 https://github.com/users/sptmh16557105/projects/1047?aennb/29934=84 https://github.com/users/sptmh16557105/projects/1047/ https://github.com/users/sptmh16557105/projects/1047?/#/uNUjM=2ME8s https://github.com/users/sptmh16557105/projects/1047/#/j6bK4=89 https://github.com/users/sptmh16557105/projects/1047?sejle/#/kedc=40 https://github.com/users/rl9322179/projects/1035 https://github.com/users/rl9322179/projects/1035%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1035 http://github.com/users/rl9322179/projects/1035 https://github.com/users/rl9322179/projects/1035?fullscreen=true https://github.com/users/rl9322179/projects/1035?s47Si=pYRAa https://github.com/users/rl9322179/projects/1035?ho2xg=13 https://github.com/users/rl9322179/projects/1035?jhfxx=rgpo https://github.com/users/rl9322179/projects/1035?zzyfo=89565 https://github.com/users/rl9322179/projects/1035?X8Q5j/8iOz8=29 https://github.com/users/rl9322179/projects/1035?zf9Zw/50822=14 https://github.com/users/rl9322179/projects/1035?gjpoq/wqzx=17 https://github.com/users/rl9322179/projects/1035?zjahr/56010=93 https://github.com/users/rl9322179/projects/1035/ https://github.com/users/rl9322179/projects/1035?/#/dY2g5=U9Q69 https://github.com/users/rl9322179/projects/1035/#/u8H3X=21 https://github.com/users/rl9322179/projects/1035?qhmvx/#/edvu=00 https://github.com/users/bvaoih007/projects/1050 https://github.com/users/bvaoih007/projects/1050%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1050 http://github.com/users/bvaoih007/projects/1050 https://github.com/users/bvaoih007/projects/1050?fullscreen=true https://github.com/users/bvaoih007/projects/1050?gYAA3=4Pir7 https://github.com/users/bvaoih007/projects/1050?NAy05=41 https://github.com/users/bvaoih007/projects/1050?aqnyr=yzxr https://github.com/users/bvaoih007/projects/1050?gznrr=98746 https://github.com/users/bvaoih007/projects/1050?Qxy0I/92r8R=90 https://github.com/users/bvaoih007/projects/1050?Y26K6/04670=37 https://github.com/users/bvaoih007/projects/1050?iraaz/ofex=42 https://github.com/users/bvaoih007/projects/1050?hkccy/64601=65 https://github.com/users/bvaoih007/projects/1050/ https://github.com/users/bvaoih007/projects/1050?/#/aA1ih=2uCyJ https://github.com/users/bvaoih007/projects/1050/#/aR98j=16 https://github.com/users/bvaoih007/projects/1050?jtysz/#/aquz=74 https://github.com/users/sptmh16557105/projects/1048 https://github.com/users/sptmh16557105/projects/1048%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1048 http://github.com/users/sptmh16557105/projects/1048 https://github.com/users/sptmh16557105/projects/1048?fullscreen=true https://github.com/users/sptmh16557105/projects/1048?541uu=b3Q1R https://github.com/users/sptmh16557105/projects/1048?B45cD=96 https://github.com/users/sptmh16557105/projects/1048?baqkj=crld https://github.com/users/sptmh16557105/projects/1048?mdsdi=29796 https://github.com/users/sptmh16557105/projects/1048?aC5T8/ks9J7=00 https://github.com/users/sptmh16557105/projects/1048?bsI2s/40209=50 https://github.com/users/sptmh16557105/projects/1048?kczbm/udjl=91 https://github.com/users/sptmh16557105/projects/1048?qumqr/06634=04 https://github.com/users/sptmh16557105/projects/1048/ https://github.com/users/sptmh16557105/projects/1048?/#/qqq3r=LaMB9 https://github.com/users/sptmh16557105/projects/1048/#/4AJ31=18 https://github.com/users/sptmh16557105/projects/1048?ajzck/#/qidi=24 https://github.com/users/rl9322179/projects/1036 https://github.com/users/rl9322179/projects/1036%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1036 http://github.com/users/rl9322179/projects/1036 https://github.com/users/rl9322179/projects/1036?fullscreen=true https://github.com/users/rl9322179/projects/1036?dLnNf=373lu https://github.com/users/rl9322179/projects/1036?v5UJw=41 https://github.com/users/rl9322179/projects/1036?vfblw=vuts https://github.com/users/rl9322179/projects/1036?nmebt=34133 https://github.com/users/rl9322179/projects/1036?4wjcD/CD3Vd=39 https://github.com/users/rl9322179/projects/1036?mVTN6/62259=62 https://github.com/users/rl9322179/projects/1036?fcuns/tfvk=82 https://github.com/users/rl9322179/projects/1036?fknct/60260=16 https://github.com/users/rl9322179/projects/1036/ https://github.com/users/rl9322179/projects/1036?/#/T9sW7=1fb6w https://github.com/users/rl9322179/projects/1036/#/Iz422=01 https://github.com/users/rl9322179/projects/1036?bfmew/#/wdsj=03 https://github.com/users/bvaoih007/projects/1051 https://github.com/users/bvaoih007/projects/1051%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1051 http://github.com/users/bvaoih007/projects/1051 https://github.com/users/bvaoih007/projects/1051?fullscreen=true https://github.com/users/bvaoih007/projects/1051?i79Yw=xongG https://github.com/users/bvaoih007/projects/1051?ihQ7O=69 https://github.com/users/bvaoih007/projects/1051?xraqn=nfgw https://github.com/users/bvaoih007/projects/1051?nzjoy=52831 https://github.com/users/bvaoih007/projects/1051?pFyog/1hxQx=19 https://github.com/users/bvaoih007/projects/1051?Z43Jq/88692=29 https://github.com/users/bvaoih007/projects/1051?pnorn/zqwh=38 https://github.com/users/bvaoih007/projects/1051?hpgfy/19708=85 https://github.com/users/bvaoih007/projects/1051/ https://github.com/users/bvaoih007/projects/1051?/#/zFQ94=1X06j https://github.com/users/bvaoih007/projects/1051/#/FoH5h=47 https://github.com/users/bvaoih007/projects/1051?yfnjy/#/nnzg=97 https://github.com/users/sptmh16557105/projects/1049 https://github.com/users/sptmh16557105/projects/1049%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1049 http://github.com/users/sptmh16557105/projects/1049 https://github.com/users/sptmh16557105/projects/1049?fullscreen=true https://github.com/users/sptmh16557105/projects/1049?sWukM=MF6oN https://github.com/users/sptmh16557105/projects/1049?LCdlt=14 https://github.com/users/sptmh16557105/projects/1049?kbdev=kffd https://github.com/users/sptmh16557105/projects/1049?smtec=79508 https://github.com/users/sptmh16557105/projects/1049?vNk67/v2FVM=58 https://github.com/users/sptmh16557105/projects/1049?d3FoF/45266=50 https://github.com/users/sptmh16557105/projects/1049?nddnn/olsc=59 https://github.com/users/sptmh16557105/projects/1049?toodo/08514=83 https://github.com/users/sptmh16557105/projects/1049/ https://github.com/users/sptmh16557105/projects/1049?/#/SwtlC=VTVnw https://github.com/users/sptmh16557105/projects/1049/#/BlUE6=66 https://github.com/users/sptmh16557105/projects/1049?ebsmo/#/dndt=57 https://github.com/users/rl9322179/projects/1037 https://github.com/users/rl9322179/projects/1037%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1037 http://github.com/users/rl9322179/projects/1037 https://github.com/users/rl9322179/projects/1037?fullscreen=true https://github.com/users/rl9322179/projects/1037?493Ok=KTnfL https://github.com/users/rl9322179/projects/1037?6smVl=19 https://github.com/users/rl9322179/projects/1037?ovbbm=evns https://github.com/users/rl9322179/projects/1037?fwwot=44212 https://github.com/users/rl9322179/projects/1037?L9Ew0/ulLl2=21 https://github.com/users/rl9322179/projects/1037?n2UvC/49948=15 https://github.com/users/rl9322179/projects/1037?mbofc/vdlf=95 https://github.com/users/rl9322179/projects/1037?kdtfl/65221=99 https://github.com/users/rl9322179/projects/1037/ https://github.com/users/rl9322179/projects/1037?/#/DLS0n=m8kbK https://github.com/users/rl9322179/projects/1037/#/c0mww=92 https://github.com/users/rl9322179/projects/1037?vflff/#/vttv=73 https://github.com/users/bvaoih007/projects/1052 https://github.com/users/bvaoih007/projects/1052%3Ffullscreen%3Dtrue https://www.github.com/users/bvaoih007/projects/1052 http://github.com/users/bvaoih007/projects/1052 https://github.com/users/bvaoih007/projects/1052?fullscreen=true https://github.com/users/bvaoih007/projects/1052?O1fIz=qPhGq https://github.com/users/bvaoih007/projects/1052?p06Wp=63 https://github.com/users/bvaoih007/projects/1052?pnozi=zzgy https://github.com/users/bvaoih007/projects/1052?anzpw=79101 https://github.com/users/bvaoih007/projects/1052?20Z5H/H9Z38=62 https://github.com/users/bvaoih007/projects/1052?fAnOo/46251=63 https://github.com/users/bvaoih007/projects/1052?fhrya/xpfo=42 https://github.com/users/bvaoih007/projects/1052?iaqno/10513=61 https://github.com/users/bvaoih007/projects/1052/ https://github.com/users/bvaoih007/projects/1052?/#/2p74H=85R2p https://github.com/users/bvaoih007/projects/1052/#/epe9q=08 https://github.com/users/bvaoih007/projects/1052?eqaow/#/fqez=96 https://github.com/users/sptmh16557105/projects/1050 https://github.com/users/sptmh16557105/projects/1050%3Ffullscreen%3Dtrue https://www.github.com/users/sptmh16557105/projects/1050 http://github.com/users/sptmh16557105/projects/1050 https://github.com/users/sptmh16557105/projects/1050?fullscreen=true https://github.com/users/sptmh16557105/projects/1050?6d826=GWfnc https://github.com/users/sptmh16557105/projects/1050?mUE07=39 https://github.com/users/sptmh16557105/projects/1050?gledg=vuxw https://github.com/users/sptmh16557105/projects/1050?ccgog=66654 https://github.com/users/sptmh16557105/projects/1050?5X6tE/C3Dgx=69 https://github.com/users/sptmh16557105/projects/1050?k4tT9/86876=33 https://github.com/users/sptmh16557105/projects/1050?otmde/txxk=23 https://github.com/users/sptmh16557105/projects/1050?vkoed/82848=57 https://github.com/users/sptmh16557105/projects/1050/ https://github.com/users/sptmh16557105/projects/1050?/#/7x3wO=DNvun https://github.com/users/sptmh16557105/projects/1050/#/9Em75=94 https://github.com/users/sptmh16557105/projects/1050?txkcg/#/nele=50 https://github.com/users/rl9322179/projects/1038 https://github.com/users/rl9322179/projects/1038%3Ffullscreen%3Dtrue https://www.github.com/users/rl9322179/projects/1038 http://github.com/users/rl9322179/projects/1038 https://github.com/users/rl9322179/projects/1038?fullscreen=true https://github.com/users/rl9322179/projects/1038?lNeB2=9a618 https://github.com/users/rl9322179/projects/1038?Csb6V=22 https://github.com/users/rl9322179/projects/1038?tvkks=sluk https://github.com/users/rl9322179/projects/1038?lkevs=80380 https://github.com/users/rl9322179/projects/1038?CT31t/c20db=17 https://github.com/users/rl9322179/projects/1038?93a55/52175=36 https://github.com/users/rl9322179/projects/1038?wetsk/uknj=53 https://github.com/users/rl9322179/projects/1038?gwaqf/66687=08 https://github.com/users/rl9322179/projects/1038/ https://github.com/users/rl9322179/projects/1038?/#/Yh251=xw2O1 https://github.com/users/rl9322179/projects/1038/#/APryG=90 https://github.com/users/rl9322179/projects/1038?yhywg/#/ixop=94
2021-01-21 - 小程序粘性布局组件实现
一、前言 开发中,我们经常会遇需要让组件在屏幕范围内时,按照正常布局排列,而组件滚出屏幕范围时,让其始终固定在屏幕顶部的情况,也就是常说的粘性布局。今天我们就一起用小程序来实现一个适用于不同场景下的粘性布局组件。 二、demo演示 如图,实现的组件主要适用于以下几种场景: 吸顶页面最上方; 吸顶与页面有固定距离的位置; 在指定容器内吸顶; 嵌套在scroll-view中吸顶。 [图片] 三、代码演示 其中,粘性组件通过<weimob-sticky></weimob-sticky>调用,参数信息用法如下: 参数 说明 类型 默认值 offset-top 吸顶时与顶部的距离,单位px number 0 z-index 吸顶时的 z-index number 99 container 一个函数,返回容器对应的 NodesRef 节点 function - scroll-top 当前滚动区域的滚动位置,非 null 时会禁用页面滚动事件的监听 number - 滚动时触发scroll函数,其中isFixed为是否吸顶,scrollTop为距离顶部的位置。详细代码如下。 3.1 页面代码 3.1.1 基础用法 [代码]<view class="weimob-block"> <view class="weimob-title">基础用法</view> <view class="weimob-body"> <weimob-sticky> <!-- 需要粘性的部分 --> <button class="margin-left-base" size="mini"> 基础用法 </button> </weimob-sticky> </view> </view> [代码] 3.1.2 吸顶距离 [代码]<view class="weimob-block"> <view class="weimob-title">吸顶距离</view> <view class="weimob-body"> <!-- 吸顶时与顶部的距离,单位px --> <weimob-sticky offset-top="{{ 50 }}"> <!-- 需要粘性的部分 --> <button class="margin-left-top" type="primary" size="mini"> 吸顶距离 </button> </weimob-sticky> </view> </view> [代码] 3.1.3 指定容器 [代码]<view class="weimob-block"> <view class="weimob-title">指定容器</view> <view class="weimob-body"> <!-- 这里需要固定高度 --> <view id="container" style="height: 300rpx;background-color: #fff"> <weimob-sticky container="{{ container }}"> <button size="mini" class="margin-left-special"> 指定容器 </button> </weimob-sticky> </view> </view> </view> [代码] 3.1.4 嵌套在scroll-view使用 [代码]<view class="weimob-block"> <view class="weimob-title">嵌套在 scroll-view 内使用</view> <!-- 这里需要固定高度,scroll-view里的元素高度需要大于其高度 --> <scroll-view bind:scroll="onScroll" scroll-y id="scroller" style="height: 400rpx; background-color: #fff;margin-top: 40rpx;" > <view style="height: 800rpx"> <weimob-sticky scroll-top="{{ scrollTop }}" offset-top="{{ offsetTop }}" > <button size="mini" class="margin-left-scoll"> 嵌套在 scroll-view 内 </button> </weimob-sticky> </view> </scroll-view> </view> [代码] 页面js [代码]Page({ data: { container: null, //一个函数,返回容器对应的 NodesRef 节点 scrollTop: 60, // 当前滚动区域的滚动位置,非null时会禁用页面滚动事件的监听 offsetTop: 0 // 吸顶时与顶部的距离,单位px }, onReady() { // 页面渲染完,获取节点信息 this.setData({ container: () => wx.createSelectorQuery().select('#container'), }); }, onScroll(event) { // 容器滚动时获取节点信息 wx.createSelectorQuery() .select('#scroller') .boundingClientRect((res) => { this.setData({ scrollTop: event.detail.scrollTop, offsetTop: res.top, }); }) .exec(); } }); [代码] 3.2 组件代码 组件wxml [代码]<wxs src="./index.wxs" module="computed" /> <view class="weimob-sticky" style="{{ computed.containerStyle({ fixed, height, zIndex }) }}" > <view class="{{ fixed ? 'weimob-sticky-wrap--fixed' : ''}}" style="{{ computed.wrapStyle({ fixed, offsetTop, transform, zIndex }) }}" > <slot /> </view> </view> [代码] 组件wxs 这里使用使用小程序的wxs对吸顶元素的transform,top,height,z-index元素进行实时渲染,ios设备在滚动监听时性能会优于在js 2-20倍,androd设备效率暂无差异。 [代码]function wrapStyle(data) { var style = ""; if (data.transform) { style += 'transform: translate3d(0, ' + data.transform + 'px, 0);' } if (data.fixed) { style += 'top: ' + data.offsetTop + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } function containerStyle(data) { var style = ""; if (data.fixed) { style += 'height: ' + data.height + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } module.exports = { wrapStyle: wrapStyle, containerStyle: containerStyle } [代码] 组件js [代码]import pageScrollMixin from "./page-scroll"; const ROOT_ELEMENT = ".weimob-sticky"; Component({ options: { multipleSlots: true }, properties: { zIndex: { type: Number, value: 99 }, offsetTop: { type: Number, value: 0, observer: "onScroll" }, disabled: { type: Boolean, observer: "onScroll" }, container: { type: null, observer: "onScroll" }, scrollTop: { type: null, observer(val) { this.onScroll({ scrollTop: val }); } } }, data: { height: 0, fixed: false, transform: 0 }, behaviors: [pageScrollMixin(function pageScrollMixinCallback(event) { // 非null时会禁用页面滚动事件的监听 if (this.data.scrollTop != null) { return; } this.onScroll(event); })], lifetimes: { attached() { this.onScroll(); } }, methods: { onScroll({ scrollTop } = {}) { const { container, offsetTop, disabled } = this.data; if (disabled) { this.setDataAfterDiff({ fixed: false, transform: 0 }); return; } this.scrollTop = scrollTop || this.scrollTop; if (typeof container === "function") { // 情况一:指定容器下时,吸顶距离+吸顶元素高度>容器高度+容器距顶部距离,随页面滚动; // 情况二:指定容器下时,吸顶距离>吸顶元素高度,元素固定; // 情况三:元素初始化。 // this.getRect获取节点ROOT_ELEMENT相对于显示区域的top,height等信息,通过root获取 // this.getContainerRect获取父容器相对于显示区域的top,height等信息,通过container获取 Promise.all([this.getRect(ROOT_ELEMENT), this.getContainerRect()]).then( ([root, container]) => { if (offsetTop + root.height > container.height + container.top) { this.setDataAfterDiff({ fixed: false, transform: container.height - root.height }); } else if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height, transform: 0 }); } else { this.setDataAfterDiff({ fixed: false, transform: 0 }); } }); return; }else{ this.getRect(ROOT_ELEMENT).then(root => { // 吸顶时与顶部的距离小于可视区域的top距离时,随着滚动条滚动,否则吸顶 if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height }); this.transform = 0; } else { this.setDataAfterDiff({ fixed: false }); } return Promise.resolve(); }); } }, setDataAfterDiff(data) { // 比较数据是否与上次相同,不同则触发父组件scroll事件更新isFixed,scrollTop。 wx.nextTick(() => { const diff = Object.keys(data).reduce((prev, key) => { const prevCopy = prev; if (data[key] !== this.data[key]) { prevCopy[key] = data[key]; } return prevCopy; }, {}); this.setData(diff); this.triggerEvent("scroll", { scrollTop: this.scrollTop, isFixed: data.fixed || this.data.fixed }); }); }, getContainerRect() { const nodesRef = this.data.container(); return new Promise(resolve => nodesRef.boundingClientRect(resolve).exec()); }, getRect(selector) { return new Promise(resolve => { wx.createSelectorQuery().in(this).select(selector).boundingClientRect(rect => { resolve(rect); }).exec(); }); } } }); [代码] page-scroll.js 滚动事件在页面进入和离开时共享的pageScrollMixin函数。 [代码]function getCurrentPage() { const pages = getCurrentPages(); return pages[pages.length - 1] || {}; } function onPageScroll(event) { const { weimobPageScroller = [] } = getCurrentPage(); weimobPageScroller.forEach(scroller => { if (typeof scroller === "function" && event) { // @ts-ignore scroller(event); } }); } const pageScrollMixin = scroller => Behavior({ attached() { const page = getCurrentPage(); if (Array.isArray(page.weimobPageScroller)) { page.weimobPageScroller.push(scroller.bind(this)); } else { page.weimobPageScroller = typeof page.onPageScroll === "function" ? [page.onPageScroll.bind(page), scroller.bind(this)] : [scroller.bind(this)]; } page.onPageScroll = onPageScroll; }, detached() { const page = getCurrentPage(); page.weimobPageScroller = (page.weimobPageScroller || []).filter(item => item !== scroller); } }); export default pageScrollMixin; [代码] 总结 最后,我将上述代码放在了代码片段中供大家使用了解,https://developers.weixin.qq.com/s/qiym3wmr7znx ,希望能够帮到小伙伴们,欢迎评论区建议或指教哦~
2021-01-26 - 共享图书小程序3.0 全新UI 免费下载
[图片][图片][图片][图片][图片][图片][图片] 图书共享小程序-建始图书共享书 20201220新增功能: 1.电子书下载(年度豆瓣榜、亚马逊销售榜) 2.纸质书借阅(适合公司、学校、地区共享图书) 3.连载书阅读 4.媒体号推荐(收集一些达人视频号教学) 5.评星/留言 6.跟随系统调整风格 7.书籍多级分类筛选 8.书籍列表与瀑布流显示 9.分享到朋友圈/好友/群/生成海报 10.点赞/收藏 11.借阅帮助/借阅福利 12.语录增加酷炫音效播放(仿豆瓣FM特效) 13.搜索增加记录(仿QQ侧滑特效)
2020-12-27 - 小程序瀑布流组件实例
微信小程序瀑布流组件 实现效果图 [图片] 源代码链接:https://git.weixin.qq.com/xieyefeng888/waterfall 用法 下载组件源码到项目目录下 微信开发者工具导入项目即可查看效果 主要使用到的API有 this.getRelationNodes() ,案例结合文档更容易理解 [图片] [图片] [图片] [图片] 注意:必须在两个组件定义中都加入relations定义,否则不会生效。 知识扩展 this.selectComponent()可以获取到子组件的里面的数据和方法,还可以获取到properties里面的值 [图片] [图片] [图片] [图片] [图片] [图片]
2020-11-25 - 开源小程序-头像加口罩
来吧,请不要吝啬你的star [图片] 1、我不是作者,但是作者同意我在社区发帖,想联系作者的私聊我吧 2、开发环境:基于uniapp使用VUE快速实现 3、这个小程序从起名字到运营,基本走的是我的运营思路,2月份的时候我说过这个小程序,目前衍生比较完整,每月差不多4位数收益,累计用户10W+ [图片] 4、有图片安全检测,可放心使用 5、部分功能预览 [图片] [图片] https://github.com/infinityu/mina-wear-mask 不要吝啬你的STAR
2020-07-05 - 程序员的万圣节-开源云开发小程序-丧尸头像
3万圣节马上就到啦,有没有想好今年的万圣节干点啥?幽默的程序员可不会这样普通的过节,接下来带你们看看,程序员写的万圣节小程序-丧尸头像。 名字听着有些可怕,但是功能很搞怪,适合万圣节主题,话不多说上图 [图片] 大家看出来了吧,左边是我,右边是生成的丧尸头像,好吓人。 下面给大家解析一下实现效果,首先我们要做的就是图片安全识别,不能上传违规的头像哦~ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { // console.log(event, "1111111111") if (event.type == 'imgSecCheck') { return imgSecCheck(event); } else if (event.type == 'msgSecCheck') { return msgSecCheck(event); } else { return ''; } } // 图片内容检测 async function imgSecCheck(event) { try { const res = await cloud.downloadFile({ fileID: event.value, }) const imgResult = await cloud.openapi.security.imgSecCheck({ media: { header: { 'Content-Type': 'application/octet-stream' }, contentType: 'image/png', value: res.fileContent } }) return imgResult; } catch (err) { return err; } } 然后我们再上传图片时调用图片安全检测 // 内容安全检测(图片)。判断图片是否合法,不含有色情,等内容。 function imgSecCheck(imgUrl) { wx.showLoading({ title: '检测图片中', }) return new Promise((resolve, reject)=>{ wx.cloud.callFunction({ name: 'contentCheck', data: { value: imgUrl, type:"imgSecCheck" }, success: res => { wx.hideLoading(); console.log(res, '检查结果') if (res.result.errCode == 0) { // 没问题 resolve(TIPS.SUCCESS); }else if (res.result.errCode == 87014) { wx.showToast({ title: '图片含有敏感违法内容!', icon: 'none' }); // 违法删除图片 wx.cloud.deleteFile({ fileList: [imgUrl] }).then(resu => { console.log(resu,'删除图片') }) }else{ TIPS.error(res) } }, fail: res => { console.log(res, '报错结果') wx.hideLoading(); TIPS.error(res) } }) }) } 上传图片变异接口调用 wx.uploadFile({ url: 'https://deepgrave-image-processor-no7pxf7mmq-uc.a.run.app/transform', filePath, name: 'image', header: { 'Content-Type': 'multipart/form-data' }, formData: { method: 'POST' //请求方式 }, success(res) { const data = res.data //do something wx.hideLoading() if (data == 'No face found') { return wx.showToast({ title: '未检测到人物图像', icon: 'none', duration: 2500 }) } _this.setData({ zombie: data }) }, fail: () => { wx.hideLoading() } }) 到此结果就出来了,功能很简单,玩儿法很特别。下面给大家上一个体验码: [图片] 开源链接:https://citizenfour.coding.net/public/zombie-head/zombie-head/git/files 作者:小码农
2020-11-02 - js数组去重的两种方法
[代码]var arr = [1,1,2,2,3,4]; // 1.使用循环判断 var arr2 = []; for(var i = 0; i < arr.length; i++){ // 判断arr2中是否已经存在当前数字 (arr[i]) if(arr2.indexOf(arr[i]) === -1){ arr2.push(arr[i]); } } console.log(arr2); // 输出 [1,2,3,4]; // 2.使用Set对象去除数组的重复成员 var arr3 = [...new Set(arr)]; [代码] 上面第二种方法说明: 1.[代码]Set[代码]是ES6标准的一种新的数据结构,它类似于数组,但是成员的值都是唯一的,没有重复的值; 2.[代码]Set[代码]函数可以接受一个数组,它会帮我们去除数组的重复成员,最终返回一个[代码]Set[代码]类型的类数组实例; [代码]new Set(arr) // 输出 Set(4) {1, 2, 3, 4} [代码] [代码]Set[代码]类型实例和数组一样也可以使用三个点(…)来展开,所以把[代码]Set[代码]展开到一个新数组里实现去重 [代码][...new Set(arr)] // 输出 [1, 2, 3, 4] [代码]
2020-11-04 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 如何实现一个简单的http请求的封装
好久没发文章了,最近浏览社区看到比较多的请求封装,以及还有在使用原始请求的童鞋。为了减少代码,提升观赏性,我也水一篇吧,希望对大家有所帮助。 默认请求方式,大家每次都这样一些写相同的代码,会不会觉得烦,反正我是觉得头大 😂 [代码]wx.request({ url: 'test.php', //仅为示例,并非真实的接口地址 data: { x: '', y: '' }, header: { 'content-type': 'application/json' // 默认值 }, success (res) { console.log(res.data) } }) [代码] 来,进入正题吧,把这块代码封装下。 首先新建个request文件夹,内含request.js 代码如下: [代码]/** * 网络请求封装 */ import config from '../config/config.js' import util from '../util/util.js' // 获取接口地址 const _getPath = path => (config.DOMAIN + path) // 封装接口公共参数 const _getParams = (data = {}) => { const timestamp = Date.now() //时间戳 const deviceId = Math.random() //随机数 const version = data.version || config.version //当前版本号,自定或者取小程序的都行 const appKey = data.appKey || config.appKey //某个小程序或者客户端的字段区分 //加密下,防止其他人随意刷接口,加密目前采用的md5,后端进行校验,这段里面的参数你们自定,别让其他人知道就行,我这里就是举个例子 const sign = data.sign || util.md5(config.appKey + timestamp + deviceId) return Object.assign({}, { timestamp, sign, deviceId, version, appKey }, data) } // 修改接口默认content-type请求头 const _getHeader = (headers = {}) => { return Object.assign({ 'content-type': `application/x-www-form-urlencoded` }, headers) } // 存储登录态失效的跳转 const _handleCode = (res) => { const {statusCode} = res const {msg, code} = res.data // code为 4004 时一般表示storage里存储的token失效或者未登录 if (statusCode === 200 && (code === 4004)) { wx.navigateTo({ url: '/pages/login/login' }) } return true } /** * get 请求, post 请求 * @param {String} path 请求url,必须 * @param {Object} params 请求参数,可选 * @param {String} method 请求方式 默认为 POST * @param {Object} option 可选配置,如设置请求头 { headers:{} } * * option = { * headers: {} // 请求头 * } * */ export const postAjax = (path, params) => { const url = _getPath(path) const data = _getParams(params) //如果某个参数值为undefined,则删掉该字段,不传给后端 for (let e in data) { if (data[e] === 'undefined') { delete data[e] } } // 处理请求头,加上最近比较流行的jwtToken(具体的自己百度去) const header = util.extend( true, { "content-type": "application/x-www-form-urlencoded", 'Authorization': wx.getStorageSync('jwtToken') ? `Bearer ${wx.getStorageSync('jwtToken')}` : '', }, header ); const method = 'POST' return new Promise((resolve, reject) => { wx.request({ url, method, data, header, success: (res) => { const result = _handleCode(res) result && resolve(res.data) }, fail: function (res) { reject(res.data) } }); }) } [代码] 那么如何调用呢? [代码]//把request的 postAjax注册到getApp()下,调用时: const app = getApp() let postData = { //这里填写请求参数,基础参数里的appKey等参数可在这里覆盖传入。 } app.postAjax(url, postData).then((res) => { if (res.success) { //这里处理请求成功逻辑。 } else { //wx.showToast大家觉得麻烦也可以写到util.js里,调用时:util.toast(msg) 即可。 wx.showToast({ title: res.msg || '服务器错误,请稍后重试', icon: "none" }) } }).catch(err => { //这里根据自己场景看是否封装到request.js里 console.log(err) }) [代码] config.js 主要是处理正式环境、预发环境、测试环境、开发环境的配置 [代码]//发版须修改version, env const env = { dev: { DOMAIN: 'https://dev-api.weixin.com' }, test: { DOMAIN: 'https://test-api.weixin.com', }, pro: { DOMAIN: 'https://api.qtshe.com' } } module.exports = { ...env.pro } [代码] 以上就是简单的一个request的封装,包含登录态失效统一跳转、包含公共参数的统一封装。 老规矩,最后放代码片段,util里内置了md5方法以及深拷贝方法,具体的我也不啰嗦,大家自行查看即可~ https://developers.weixin.qq.com/s/gbPSLOmd7Aft
2020-04-03 - 炫酷的wxss动画效果
因为没啥事,研究了下小程序的粒子动画,最后放弃了,实在是头大。去搞了一些花里胡哨的效果,没啥实际用处,就分享玩玩,看能不能提供一些其他灵感啥的。 [图片] 代码片段如下:https://developers.weixin.qq.com/s/VQwYjYm47dgH 使用wxss绘制烟花动画 [图片] https://developers.weixin.qq.com/s/xcJdoMmW7lh3 蜡烛逼真燃烧效果: [图片] https://developers.weixin.qq.com/s/Iom47XmO7rh5 螺旋旋转效果 [图片] https://developers.weixin.qq.com/s/1BnRTXmZ7Rhj 炫酷wxss粒子动画 [图片] https://developers.weixin.qq.com/s/cRpjQXmb7khN 水文章
2020-08-03 - 新能力 | 云开发CMS内容管理系统,5分钟搞定小程序管理后台
小程序·云开发的云调用能力,让用户可以免鉴权快速调用微信的开放能力,极大节约了开发成本。现在,大家期待已久的云开发 CMS 内容管理系统,终于上线啦!顺便提示,接下来还可以二次开发哦! 云开发 CMS 管理系统是什么? 云开发 CMS 内容管理系统是云开发提供的一个扩展程序,可以在云开发控制台一键安装在自己的云开发环境中,方便开发人员和内容运营者随时随地管理小程序 / Web 等多端云开发内容数据。不用编写代码就可以使用,还提供了 PC /移动端浏览器访问支持,支持文本、富文本、图片、文件、关联类型等多种类型的可视化编辑。 [图片] 先来看看云开发CMS的"庐山真面目" 首先我们通过几张截图来直观感受一下 CMS 内容管理系统扩展: 图1 云开发控制台的安装界面截图 [图片] 图2 安装并配置好内容的 CMS 内容管理系统界面演示 [图片] 图3 CMS 内容管理系统界面的移动端演示 [图片] 云开发 CMS 内容管理系统有哪些功能特性 ? 特性 介绍 免开发 基于后台建模配置生成内容管理界面,无须编写代码 多端适配 支持 PC/移动端访问和管理内容 功能丰富 支持文本、富文本、图片、文件 等多种类型内容的可视化编辑,并且支持内容关联 权限控制 系统基于管理员/运营者两种身份角色的访问控制 外部系统集成 支持 Webhook 接口,可以用于在运营修改修改内容后通知外部系统,如自动构建静态网站、发送通知等 数据源兼容 支持管理小程序/ Web / 移动端的云开发数据,支持管理已有数据集合,也可以在 CMS 后台创建新的内容和数据集合 部署简单 可在云开发控制台扩展管理界面一键部署和升级 什么场景下适合使用 CMS ? 1. 适用于需要为小程序应用增加一个运营管理后台的业务 小程序应用有偏运营方面的文章编辑和发布、运营活动配置、素材管理等数据管理需求,使用 CMS 扩展之后,不用手动线上修改 db 数据,也不用投入人力物力开发管理后台,可以随时随地使用自己环境下部署的 CMS 内容管理系统来管理,同时还支持区分管理员和运营者的身份权限。 2. 适用于快速开发内容型的网站应用、小程序应用等场景 CMS 内容管理系统还可以帮助开发者提升开发网站应用、小程序应用的效率,省去一部分后端开发工作。例如安装了CMS 扩展之后,解决了内容和数据的管理和生产问题,直接可以结合前端应用框架读取 db 数据进行渲染。例如基于 CMS 可以快速开发博客、企业官网等小程序/网站应用,最后悄悄透露一下,云开发的官网 (http://cloudbase.net/) 就是基于 CMS 扩展 + Next.js + 云开发静态托管搭建和部署的。 如何安装和使用 CMS ? 第一步:切换为按量付费 由于 CMS 扩展需要用到静态网站托管资源,必须在按量计费的环境下才可以部署,因此首先要切换计费方式为按量付费。 1. 微信小程序开发者 登录微信开发者工具-云开发控制台 在【云开发控制台】-【设置】-【环境设置】-【支付方式】中点击切换【按量付费】即可。 注意:这里需要先保证腾讯云账户中是有充值金额的哦~ [图片] 2. 腾讯云开发者 登录腾讯云云开发控制台 在【云开发 CloudBase 控制台】-【环境】-【资源购买】-【计费模式】中点击【切换按量付费】即可。 [图片] 第二步:在腾讯云控制台安装扩展 登录腾讯云控制台 微信小程序开发者需要使用微信公众号登录! [图片] 在【云开发 CloudBase 控制台】-【扩展能力】-【扩展管理】中找到 CMS内容管理系统 扩展进行安装 安装时需要进行资源的授权和扩展程序的配置,比如管理员和运营者的账号密码配置等,同时需要提供自定义登录的密钥,可以点击自定义登录密钥旁边的小图标了解如何填写。 [图片] 第三步:使用 CMS 内容管理系统 完成【CMS内容管理系统】的安装以后,然后访问该扩展的管理页,可以在【扩展运行方式】Tab 查看使用指引,依照文档完成 CMS 的使用,下面简单介绍一下快速上手的步骤,更多细节可以参考运行方式。 [图片] 访问 CMS 系统 CMS 扩展已经部署在当前环境下的静态网站托管中,访问路径为“静态托管的默认域名+安装设置的部署路径” 访问地址的格式如下: [代码]云开发静态托管默认域名/部署路径[代码],例如 [代码]https://xxxx.tcloudbaseapp.com/tcb-cms/[代码] 账号登录 打开 CMS 系统后首先会提示需要登录,我们首先使用使用安装扩展时设置的管理员账号和密码进行登录 内容建模 登录成功后,首先需要进行内容的建模设置,例如我们想为自己的博客应用(小程序/网站)来生成管理界面。 假设当前已有一个管理 文章的数据库集合 [代码]articles[代码],我们可以在 CMS 管理后台新建一个 “文章” 内容(如果新建内容的时候指定的集合名不存在,CMS 扩展会自动新建集合)来生成“文章”类型的内容管理界面。 假设数据库集合 [代码]articles[代码] 的结构如下: 字段名 类型 描述 _id ID 文章唯一 id name String 文章标题 cover String 封面图,这里存放云开发的存储的文件的 cloudID content String 文章内容,采用 markdown 格式 author ID 作者的用户 id createTime DateTime 创建时间 updateTime DateTime 更新时间 tag String[] 标签,例如 [代码]["serverless","cms"][代码] category String[] 分类,例如 [代码]["前端","开发"][代码] 我们在“内容设置”中点击“新建”来创建“文章”类型时,可以对照上面的集合数据把字段类型和字段的限制进行配置,例如封面图可以直接选择 “图片”字段类型,文章内容可以直接选择 “Markdown” 类型,这样在生成的管理界面里可以直接上传图片和通过编辑器编写文章,保存在数据库集合的时候,依然会保存为数据库支持的类型,图片会存储为云存储的 CloudID, 内容会存储为字符串等。 [图片] 创建并保存之后会自动刷新生成”文章“的运营界面 管理内容 接下来就可以进行运营管理内容操作了,可以使用运营者身份登录,对新创建的“文章”进行操作,我们可以新建一篇文章。 [图片] 文章发布成功后,即可在文章列表中看到这篇文章 [图片] 使用内容数据 采用 CMS 管理的内容,依然可以通过云开发各端 SDK 进行访问(需要注意的是在前端访问时,需要正确设置数据库的安全规则设置,例如设置为所有用户可读,仅创建者可写)。 例如,在上面的例子里,我们需要在云函数中获取文章的标签是 [代码]CloudBase[代码] 的最新 10 条文章,可以采用以下代码来获取数据: [代码]db.collection("articles") .where({ tag: "CloudBase" }) .orderBy("createTime", "desc") .limit(10) .get(); [代码] 获取到内容数据就可以在各种场景使用了,比如在小程序/ Web 中构建应用和网站,具体的CMS + 应用开发的实践可以关注后期我们的实践教程。 [图片] 后续,云开发CMS内容管理系统将支持二次开发,用户可以自由定制自己的管理后台。云开发将始终坚持,为开发者提供一站式云服务! [图片] 最后,小编赠上《5分钟部署云开发CMS系统》教程,帮助大家快快上车! 视频链接: https://v.qq.com/x/page/f09687on1qv.html 文档链接 :(CMS 内容管理系统链接) https://cloud.tencent.com/document/product/876/44547
2020-09-14 - 针对新手很容易出现理解误区的微信小程序订阅消息模块
1. 写在前面 微信小程序下架了模板消息功能,取而代之的是订阅消息功能。这个订阅消息目前又分为「一次性订阅」和「永久订阅」。使用订阅消息也有一段时间了,感觉对新手订阅消息很容易让新开发者进入一个理解的误区,这里觉得有必要说出来 2. 理解误区 很多新手认为,只要用户勾选了小程序端订阅消息弹出时底部的「总是保持以上选择…」后,就可以「为所欲为」的不限次数的推送订阅消息给用户了。如下图: [图片] 3. 正确理解 如果你使用的「一次性订阅」模板(目前发现绝大多数开发者都是只能用一次性的,因为永久性的订阅消息申请门槛太高),那么勾选底部的「总是…」这个并不代表以后可以直接推送了。官方原话wx.requestSubscribeMessage的介绍里是这样写的: 3.1 官方说明 wx.requestSubscribeMessage(Object object) 基础库 2.8.2 开始支持,低版本需做兼容处理。 调起客户端小程序订阅消息界面,返回用户订阅消息的操作结果。当用户勾选了订阅面板中的“总是保持以上选择,不再询问”时,模板消息会被添加到用户的小程序设置页,通过 wx.getSetting 接口可获取用户对相关模板消息的订阅状态。 注意事项 一次性模板 id 和永久模板 id 不可同时使用。 低版本基础库2.4.4~2.8.3 已支持订阅消息接口调用,仅支持传入一个一次性 tmplId / 永久 tmplId。 2.8.2 版本开始,用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。 2.10.0 版本开始,开发版和体验版小程序将禁止使用模板消息 fomrId。 3.2 重点关注 这里重点关注第7条:「用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。」这就意味着你需要在用户主动点击某个组件是触发调用wx.requestSubscribeMessage方法再次订阅,订阅后,你才可以「为所欲为」推送一次模板消息,注意只能一次。下次再想推送时,需要用户再次点击触发wx.requestSubscribeMessage。 4. 破局方案 目前订阅消息功能,就是这么个情况,所以针对这个情况的替代方案有以下 4.1 永久性订阅消息 如果能达到申请「永久性订阅」消息的模板的门槛,那自然是极好的,直接用永久性模板「为所欲为」。 4.2 使用服务号的模板消息替代 比较常用的是使用公众号服务号的模板消息代替小程序的订阅消息功能,公众号的模板消息功能限制就比订阅号好多了,基本上可以「为所欲为」的推送。但是这个方案有个致命的运营成本:必须要用户关注公众号,还有小程序要跟公众号同一主体并绑定在开放平台下。同时开发成本有所增加,要采用unionId机制来打通小程序跟公众号的openId。这个具体的实现方案,大家有兴趣的话可以讨论下。笔者目前就是用这种方案的。 5. 几个注意点 5.1 官方提示 订阅消息如果选择选择‘总是保持以上选择,"不再询问"后的设置问题: 目前是选择‘总是保持以上选择,"不再询问"后,可以在设置中开启或拒绝接收,但不会再次拉起授权弹窗 6. 长期性订阅消息 请参考官方最新文档: 小程序模板消息能力调整通知 | 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/00008a8a7d8310b6bf4975b635a401 长期性订阅消息 一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。 目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。 7.题外话 鉴于被戴上各种「刷赞,冲级,让社区点赞“通货膨胀”」等等一些恶毒字眼(最近多了个职业回复的「雅称」),各种帽子戴得,做一个开发爱好者积极分享和解决各种问题太难了,姑且不论咱写一篇文章需要截图多少,单单排版就得废掉俺多少时间哈,很受伤,所以本人决定在微信开放者社区封笔。你看到是俺最后一篇发表在微信开放社区的文章。如果你想继续查看俺的一些文章可以私聊我。我会在其他平台保持继续创作。bye-bye~ 8. 最最重要的来了 看完后觉得有用记得点赞~~ ↓点赞处↓
2020-09-04 - 云函数时区问题解决方案
我在之前写文章整理过关于云函数时区的问题,具体见下面链接,今天不讨论多个方案,只推荐一个亲测可行的稳定方案 https://developers.weixin.qq.com/community/develop/article/doc/000c887a83874009534a4712a5b813 所谓云函数时区问题是指: 云函数中的时区为 UTC+0,不是 UTC+8,在云函数中使用时间时需特别注意。也就是是说,现在是2020-05-25 15:00:00,但是在云函数端new Date()打印的是2020-05-25 07:00:00 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/notice.html 具体解决方案 如果需要默认 UTC+8,可以配置函数的环境变量,设置 TZ 为 Asia/Shanghai。 注意事项 这里需要注意的是:设置环境变量和上传云函数的顺序问题,一定要在设置环境变量之后,重新部署云函数,并且部署完成之后要缓个几分钟测试, 该方案亲测可用
2020-05-25 - 少儿编程入门001,在家自己带孩子学编程
最近几年少儿编程越来越火,编程被有些地方纳入学校课程。有些省份高考题目中也逐渐把编程作为必考科目。作为孩子的家长,在人工智能越来越火的今天,怎么能让自己的孩子在编程方面输在起跑线上呢。今天石头哥就来开始录制一系列的少儿编程课程,来教家长朋友们在家自己带娃学习编程。 [图片] [图片] 学习编程能锻炼孩子的哪些技能 [图片] 还是老规矩,我们先看效果图 [图片] 这是当前全世界一款主流的编程教学软件叫Scratch,是由麻省理工学院的“终身幼儿园团队”开发的图形化编程工具,主要面对青少年开放。 Scratch将复杂的程序变为一个个积木块,孩子无需敲击代码或是背诵任何编程指令,只要用鼠标将积木块拖拽并连接在一起,就可以很方便的进行编程。孩子们使用Scratch创作出属于自己的动画、游戏、交互程序,培养综合能力,获得学习和创造的乐趣。比较适合5-10岁的孩子做编程启蒙使用。 今天呢,就来教大家如何在家里安装这么一款软件,来开启孩子的编程大门。 一,进入Scratch官网下载离线编辑器 这是官网地址https://scratch.mit.edu/ [图片] 进入官网以后,拉到最底部有个Download,直接点击下载。 [图片] 如果你不喜欢英文界面,也可以切换中文。 [图片] 页面换成中文以后,点击下载即可。 [图片] 选择对应的操作系统,直接下载 [图片] 由于官网是国外网站,有时候打开可能比较慢。我这里提前把安装包给大家准备好了。 [图片] 如果你在官网下载比较慢,可以找石头哥索取安装包。 二,安装Scratch离线编辑器 安装也很简单,只需要双击即可。 [图片] 直接点击安装即可 [图片] 下面就是安装进度了。等待安装成功即可 [图片] 三,打开Scratch离线编辑器。 [图片] 第一次打开可能有点慢,耐心等待即可。 [图片] 上图就是打开后的编辑器。主要的区域我都标注出来了。这一节的Scratch软件安装就讲到这里了,下一节开始,我们就来教大家属性里面的一些功能。然后家长朋友学会后,就可以在家带娃学编程了。 学习视频 少儿编程配套视频
2020-06-17 - 使用animation实现列表顺序加载动画
[图片] 之前使用纯transition实现动画时, 发现在部分手机上效果不是很好, 会有不流畅掉帧的现象! 现在换animation方法实现, 不知各位是否有什么高见, 大家一起交流交流 代码片段如下 https://developers.weixin.qq.com/s/pEBv6emG7Cdt
2019-11-29 - cover-view 爬坑记录
Q:cover-view为什么不支持overflow-x:scroll A:cover-view 支持 overflow-y:scroll,不支持overflow-x:scroll,官方 2019-06-10 在社群里表示后续也不会支持 overflow-x:scroll https://developers.weixin.qq.com/community/develop/doc/000c26cf018d48a4f8a83f7f756000 Q:cover-view容器中文字显示不全 A:官方 2018-11-26 在社区里回复,这是一个已知问题,后续会修复,截止到2020年6月10日 23:44为止 该bug尚未修复 https://developers.weixin.qq.com/community/develop/doc/00066aab25cd308b84b77c18854c00?highLine=cover-view%2520%25E6%2598%25BE%25E7%25A4%25BA%25E4%25B8%258D%25E5%2585%25A8 Q:cover-view无法使用多行省略 A:官方 2018-12-24 在社区里回复,cover-view不支持该样式 https://developers.weixin.qq.com/community/develop/doc/000a4c728bc2302bbcd7dc6b553400?highLine=cover-view%2520%25E5%25A4%259A%25E8%25A1%258C%25E6%258A%2598%25E8%25A1%258C%25E7%259C%2581%25E7%2595%25A5
2020-06-11 - canvas 插入gif图 怎么是使gif图在canvas中也可以动?
canvas 插入gif图 怎么使gif图在canvas中也可以动? 或者怎么在微信小程序中将gif图解析成一帧一帧的拿到每一帧数据?
2019-08-09 - 用小游戏实现一个VR看图小程序,附上源码
刚接触小游戏,分享一个VR看图小程序,用小游戏实现的,附上源码,可以查看效果。 视频效果 使用了three.js [图片] [代码]import * as THREE from './libs/three.min.js' const screenWidth = window.innerWidth const screenHeight = window.innerHeight var startX, endX, startY, endY; var that; var isVrMove = false; /* 物体 */ var scene; const R_BALL = 50; var vrSphere; export default class main { constructor() { that = this; setTimeout(function(){ that.drawBall('https://mamba-blog-images.oss-cn-shanghai.aliyuncs.com/2020-06-10/1c3352744d88b048b14d02648a064fbd.jpg') },1000) scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); this.camera.position.set(0,0,0) scene.add(this.camera); this.renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true }); this.renderer.shadowMapEnabled = true; this.renderer.setSize(window.innerWidth, window.innerHeight); this.start() wx.onTouchStart(function(e){ isVrMove = false; if(e.touches.length > 0) { console.log(vrSphere.rotation.y + ":" + vrSphere.rotation.x); var touch = e.changedTouches[0]; startX = touch.clientX; startY = touch.clientY; isVrMove = true } }) wx.onTouchMove(function (e) { if (e.touches.length > 0) { var touch = e.changedTouches[0]; endX = touch.clientX; endY = touch.clientY; var x = endX - startX; var y = endY - startY; if (isVrMove){ var moveObject = vrSphere moveObject.rotation.y = moveObject.rotation.y - x * 0.003; moveObject.rotation.x = moveObject.rotation.x - y * 0.003; // 判断是否超出范围 if (moveObject.rotation.x < -1) { moveObject.rotation.x = -1; } else if (moveObject.rotation.x > 1) { moveObject.rotation.x = 1; } if (moveObject.rotation.y > Math.PI * 2) { moveObject.rotation.y -= Math.PI * 2; } else if (moveObject.rotation.y < 0){ moveObject.rotation.y += Math.PI * 2; } } startX = endX; startY = endY; } }) window.requestAnimationFrame(this.loop.bind(this), canvas); } start() { } drawBall(url){ var segemnt = 32, rings = 32; var geometry = new THREE.SphereGeometry(R_BALL, segemnt, rings); // 加载纹理贴图 var texture = new THREE.TextureLoader().load(url); var material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.BackSide }); vrSphere = new THREE.Mesh(geometry, material); vrSphere.name = "vrball" vrSphere.position.set(0,0,0) scene.add(vrSphere); } update() { } loop() { this.update() this.renderer.render(scene, this.camera); window.requestAnimationFrame(this.loop.bind(this), canvas); } } [代码] github地址 https://github.com/kesixin 项目:VR
2020-06-10 - 教大家用20行js代码,开发好小程序订阅消息
微信小程序官方决定在2020-1-10全面线下小程序模板消息,要去替换为订阅消息。那对开发者而言,又要一个一个地方去修改代码兼容.... 所以我替大家写了段代码,来快速解决问题。复制下面这段代码到app.js文件最上面即可解决问题。代码的主要功能是在每一个tap类型的点击事件中触发订阅弹窗,这样用户点几次界面,你就可以发几次消息。这也是让发送次数最大化,不可能比这个次数还多了。 预期结果是:用户点几次弹窗,就会注意到有一个不再提醒按钮,一旦选了它,那你就可以随便发订阅消息了! // 记录原Page方法 const originPage = Page; // 重写Page方法 Page = (page) => { Object.keys(page).forEach(function(key){ if(key !== 'data'){ let originMethod = page[key]; page[key] = function () { let e = arguments[0]; //给所有的点击事件增加订阅消息弹窗 if(!!e && !!e.type && e.type === 'tap'){ wx.requestSubscribeMessage({ tmplIds: ['3E66jPXafsnikZoQR5uk0OUzIUVASZE5scyAu5YCHPI'], ////////这里替换为自己的模板ID///// success (res) { // console.log(res) }, fail (res) { // console.log('订阅消息失败',res) } }) } return originMethod.call(this,...arguments) } } }); return originPage(page); };
2020-01-09 - 【开箱即用】分享几个好看的波浪动画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 - 小程序页面(Page)扩展,为所有页面添加公共的生命周期、事件处理等函数
背景 在小程序的原生开发中,页面中经常会用到一些公共方法,例如在页面onLoad中验证权限、所有页面都需要onShareAppMessage设置分享等 假设我们在编码时每个页面都写一遍,显然不是一个高级程序员会干的事情,太Low了。如果我们定义一个公共文件,导出这些公共方法,每个页面都引入,然后再生命周期或者事件处理函数中调用,虽然看起来很方便,但不够优雅,达不到我们最终的目的(偷懒)。 下面给大家介绍一种相对比较优雅的实现方式,扩展Page来实现以上的操作。 Page(页面) 需要传入的是一个 [代码]object[代码] 类型的参数,那么我们重载一个 [代码]Page[代码] 函数,将这个 [代码]object[代码] 参数拦截改掉就可以了,下面直接上代码。 实现 1、在根目录新建一个 [代码]page-extend.js[代码] 文件,公共的逻辑都写在这里面 [代码]/** * * Page扩展函数 * * @param {*} Page 原生Page */ const pageExtend = Page => { return object => { // 导出原生Page传入的object参数中的生命周期函数 // 由于命名冲突,所以将onLoad生命周期函数命名成了onLoaded const { onLoaded } = object // 公共的onLoad生命周期函数 object.onLoad = function (options) { // 在onLoad中执行的代码 ... // 执行onLoaded生命周期函数 if (typeof onLoaded === 'function') { onLoaded.call(this, options) } } // 公共的onShareAppMessage事件处理函数 object.onShareAppMessage = () => { return { title: '分享标题', imageUrl: '分享封面' } } return Page(object) } } // 获取原生Page const originalPage = Page // 定义一个新的Page,将原生Page传入Page扩展函数 Page = pageExtend(originalPage) [代码] 2、在 [代码]app.js[代码] 中引入 [代码]page-extend.js[代码] 文件 [代码]require('./page-extend') App({ // 其他代码 ... }) [代码] 代码片段 https://developers.weixin.qq.com/s/Cyx8iGmV7Ldp 本文内容及评论未经允许,禁止任何形式的转载与复制(代码可在程序中使用)
2019-12-24 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - 借助云开发10行代码实现小程序短信验证码的发送
最近在做小程序验证码登陆时,用到了短信发送验证码的需求,自己也研究了下,用云开发结合云函数来实现验证码短信发送还是很方便的。 老规矩,先看效果图 [图片] 这是我调用腾讯云的短信平台发送的登陆验证码。核心代码其实只有下面这么多 [图片] 是不是感觉实现起来特别简单,怎么说呢,我们代码调用其实就这么几行,就可以实现短信的发送,但是腾讯云短信模板的审核比较繁琐,还有我们先去申请短信模板,短信模板审核通过后才可以使用。 我们就先来说代码实现,然后再带大家简单的学习下短信模板的申请。 一,安装node类库 其实我们这里用到了云开发的云函数,我们是在云函数里调用短信发送的。为什么要在云函数里调用呢,因为我们做短信发送,需要用到腾讯云的一个短信发送的类库,而这个类库是node库,所以只能在云函数里调用了。 在安装这个类库之前,我们需要先创建一个云函数,关于云函数的创建,我其实已经讲过很多遍了,不知道的同学,去翻看下我的历史文章,或者看下我录制的云开发入门视频《5小时零基础入门小程序云开发》 我后面也会把这节内容录制出视频出来。 创建完云函数后,右键点击在终端中打开,打开终端后,在终端中输入以下命令来安装qcloudsms_js类库 [代码]npm install qcloudsms_js [代码] [图片] 这里需要注意,我们安装类库前需要先下载node并配置npm环境变量,这里我也有写文章的 《nodeJs的安装与npm全局环境变量的配置》 二,编写云函数 上面类库安装好以后,我们就可以来编写云函数了。 其实代码编写起来很简单,就下面这些,对应的注解我也都已经写出来了。 [图片] 这里要发送的手机号,和随机验证码需要动态传进来的。 三,调用云函数 调用云函数这里也很简单,我们需要传入手机号和验证码 [图片] 手机号这里,我做了一个输入框,可以动态的输入。验证码的话,我写了一个方法来随机生成数字和字母的组合验证码。 [图片] 我等下会把完整的代码贴出来给大家。 [图片] 这样我们输入完手机号以后,点击发送短信按钮,就可以成功的发送短信给到对应的手机号了。 可以看到我们生成的随机验证码如下 [图片] 我们手机接受到的短信验证码如下 [图片] 这样我们做登陆或者做校验时,用户手机短信收到的验证码,和我们随机生成的验证码一样,即代表用户验证成功。 完整的index.js代码给大家贴出来 [代码]var chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; let phone = '' Page({ //获取随机验证码,n代表几位 generateMixed(n) { var res = ""; for (var i = 0; i < n; i++) { var id = Math.ceil(Math.random() * 35); res += chars[id]; } return res; }, //调用云函数发送短信 sendSMS() { if (phone.length != 11) { wx.showToast({ icon: 'none', title: '输入11位手机号', }) return } let code = this.generateMixed(4) console.log('本地生成的验证码', code) wx.cloud.callFunction({ name: "sendSms", data: { phone: phone, code: code //生成4位的验证码 } }).then(res => { console.log('发送成功', res) }).catch(res => { console.log('发送失败', res) }) }, //获取要发送的手机号 getPhone(event) { console.log(event.detail.value) phone = event.detail.value }, }) [代码] index.wxml如下 [图片] 到这里我们的短信验证码的发送就完整的实现了,是不是很简单。 短信发送参数的设置与获取 首先是去腾讯云自己开通短信功能,然后需要自己去申请模板,填写签名。 [图片] 我这里把所需要的参数,都给大家标准出来了。大家只需要自己去官网设置对应的模板和签名,然后审核通过后,把对应的参数放到我们的云函数里即可。 短信验证的原理讲解 在网上找了一张短信验证的原理图,如下 [图片] 大家可以对照这看下,这个原理图。对应的源码我上面其实已经给大家贴出来了。 如果大家觉得不完整,我也已经把完整源码放到网盘里了,有需要的同学可以到我公号里回复“短信”获取源码。 [图片]
2020-01-03 - 一行代码完成轻量的错误监控,已适配专业debug平台请自行忽略
已适配专业debug平台请自行忽略!!!! -------------------分割线-------------------------------- 只监听了[代码]wx.onError[代码]事件,可能会存在有一些错误无法捕获,欢迎各位大佬前来指导。 另外代码内大部分用来处理收集周边数据,帮助回看错误。 配置域名 打开微信小程序后台 , 配置下方域名为合法域名 [代码]https://push.hellyw.com[代码] [图片] 需要借助iGot聚合推送实现错误消息的即时送达 引入js文件 点击下载 为方便接入,建议更名为[代码]MError.js[代码]文件 [代码]/* v1.0.0 * 推送提醒: 服务依赖iGot小程序, 微信搜索“iGot”体验 key的获取方式请参照:https://support.qq.com/products/111465/faqs/58267 iGot桌面客户端 [windows && mac] 下载 : https://github.com/wahao/Electron-iGot * 微信实时日志: 开发者可从小程序管理后台“开发->运维中心->实时日志”进入日志查询页面,查看开发者打印的日志信息。 */ // 日志实时管理- 方便排查 const realtimeLogManager = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null // 获取当前小程序的账户信息 const accountInfo = wx.getAccountInfoSync ? wx.getAccountInfoSync() : {}; // 获取当前系统信息 const systemInfo = wx.getSystemInfoSync ? wx.getSystemInfoSync() : {}; // 获取启动信息 const launchOptions = wx.getLaunchOptionsSync ? wx.getLaunchOptionsSync() : {}; // 请求地址 module.exports = class MError { constructor(key = "", title) { try { this.title = title || (accountInfo.miniProgram ? `${accountInfo.miniProgram.appId}` : `${accountInfo.plugin.appId }(${accountInfo.plugin && accountInfo.plugin.version || ""}) `) || "小程序" this.key = key || "" this.host = `https://push.hellyw.com/${this.key}` let self = this wx.onError((error) => { self.log(error) }) } catch (err) { console.error(err) } } uploadLog(obj) { let self = this try { console.error(obj) realtimeLogManager && realtimeLogManager.error(obj) wx.request({ url: self.host, header: { "content-typ": "application/json" }, method: "POST", data: obj }) } catch (err) { console.error(err) } } _getUserInfo() { return new Promise((resolve, reject) => { try { wx.getUserInfo({ complete: (res) => { resolve(res.userInfo || {}) } }) } catch (err) { resolve({}) } }) } _getLocation() { return new Promise((resolve, reject) => { try { wx.getLocation({ complete: (res) => { resolve(res || {}) } }) } catch (err) { resolve({}) } }) } getUser() { // 读取设置 - 已被允许的权限 尽可能获取 let self = this return new Promise((resolve, reject) => { try { wx.getSetting({ complete: async res => { try { if (!res.authSetting || (!res.authSetting['scope.userInfo'] && !res.authSetting['scope.userLocation'])) return resolve({}) resolve({ userInfo: res.authSetting['scope.userInfo'] ? await self._getUserInfo() : {}, location: res.authSetting['scope.userLocation'] ? await self._getLocation() : {}, }) } catch (err) { resolve({}) } } }) } catch (err) { resolve({}) } }) } log(error) { try { let params = { title: this.title, content: `${typeof error !== 'object' ? error : '当前程序出现一个错误,请及时关注'} [更详细的错误参数可进入“iGot”小程序内查看]`, error: typeof error === 'object' ? error : '错误日志已打印。更多细节,请通过微信后台【“开发->运维中心->实时日志”】查看', extras: Object.assign({}, { accountInfo: accountInfo, systemInfo: systemInfo, launchOptions: launchOptions, storageInfo: wx.getStorageInfoSync ? wx.getStorageInfoSync() : {} }) } this.getUser().then(userInfo => { params.extras = Object.assign({}, params.extras, { user: userInfo }) this.uploadLog(params) }) } catch (err) { console.error(err) } } } [代码] 部署代码 在 [代码]app.json[代码] 中引入该js文件 [代码]const MError = require('/utils/MError') [代码] 在[代码]app.js[代码]的[代码]App({})[代码]内加入如下代码,[代码]key[代码]更换为您自己的key 。 关于iGot的使用,此处不赘述 建议使用临时key,如需项目组多人接收,建议使用不公开的订阅链接key。获取方式:https://support.qq.com/products/111465/faqs/58267 [代码]$mError: new MError(key,'推送标题') [代码] 其他地方使用可直接 [代码]app.$mError.log('error')[代码]即可 效果 至此,就已经成功实现了 。 来看看效果吧!!! [图片] 手机端成功接收。
2020-02-09 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: Android、IOS手机对于页面title的展示不一致,安卓title的显示不居中 页面的title只支持纯文本级别的样式控制,不能够做更丰富的title效果 左上角的事件无法监听、定制 路由导航单一,只能够返回上一页,深层级页面的返回不够友好 探索 小程序自定义导航栏已开放许久>>了解一下,相信不少小伙伴已使用过这个功能,同时不少小伙伴也会发现一些坑: 机型多如牛毛:自定义导航栏高度在不同机型始终无法达到视觉上的统一 调皮的胶囊按钮:导航栏元素(文字,图标等)怎么也对不齐那该死的胶囊按钮 各种尺寸的全面屏,奇怪的刘海屏,简直要抓狂 一探究竟 为了搞明白原理,我先去翻了官方文档,>>飞机,点过去是不是很惊喜,很意外,通篇大文尽然只有最下方的一张图片与这个问题有关,并且啥也看不清,汗汗汗… 我特意找了一张图片来 [图片] 分析上图,我得到如下信息: Android跟iOS有差异,表现在顶部到胶囊按钮之间的距离差了6pt 胶囊按钮高度为32pt, iOS和Android一致 动手分析 我们写一个状态栏,通过wx.getSystemInfoSync().statusBarHeight设置高度 Android: [图片] iOS:[图片] 可以看出,iOS胶囊按钮与状态栏之间距离为:4px, Android为8px,是不是所有手机都是这种情况呢? 答案是:苹果手机确实都是4px,安卓大部分都是7和8 也会有其他的情况(可以自己打印getSystemInfo验证)如何快速便捷算出这个高度,请接着往下看 如何计算 导航栏分为状态栏和标题栏,只要能算出每台手机的导航栏高度问题就迎刃而解 导航栏高度 = 胶囊按钮高度 + 状态栏到胶囊按钮间距 * 2 + 状态栏高度 注:由于胶囊按钮是原生组件,为表现一致,其单位在各种手机中都为px,所以我们自定义导航栏的单位都必需是px(切记不能用rpx),才能完美适配。 解决问题 现在我们明白了原理,可以利用胶囊按钮的位置信息和statusBarHeight高度动态计算导航栏的高度,贴一个实现此功能最重要的方法 [代码]let systemInfo = wx.getSystemInfoSync(); let rect = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null; //胶囊按钮位置信息 wx.getMenuButtonBoundingClientRect(); let navBarHeight = (function() { //导航栏高度 let gap = rect.top - systemInfo.statusBarHeight; //动态计算每台手机状态栏到胶囊按钮间距 return 2 * gap + rect.height; })(); [代码] gap信息就是不同的手机其状态栏到胶囊按钮间距,具体更多代码实现和使用demo请移步下方代码仓库,代码中还会有输入框文字跳动解决办法,安卓手机输入框文字飞出解决办法,左侧按钮边框太粗解决办法等等 胶囊信息报错和获取不到 问题就在于 getMenuButtonBoundingClientRect 这个方法,在某些机子和环境下会报错或者获取不到,对于此种情况完美可以模拟一个胶囊位置出来 [代码]try { rect = Taro.getMenuButtonBoundingClientRect ? Taro.getMenuButtonBoundingClientRect() : null; if (rect === null) { throw 'getMenuButtonBoundingClientRect error'; } //取值为0的情况 if (!rect.width) { throw 'getMenuButtonBoundingClientRect error'; } } catch (error) { let gap = ''; //胶囊按钮上下间距 使导航内容居中 let width = 96; //胶囊的宽度,android大部分96,ios为88 if (systemInfo.platform === 'android') { gap = 8; width = 96; } else if (systemInfo.platform === 'devtools') { if (ios) { gap = 5.5; //开发工具中ios手机 } else { gap = 7.5; //开发工具中android和其他手机 } } else { gap = 4; width = 88; } if (!systemInfo.statusBarHeight) { //开启wifi的情况下修复statusBarHeight值获取不到 systemInfo.statusBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - 20; } rect = { //获取不到胶囊信息就自定义重置一个 bottom: systemInfo.statusBarHeight + gap + 32, height: 32, left: systemInfo.windowWidth - width - 10, right: systemInfo.windowWidth - 10, top: systemInfo.statusBarHeight + gap, width: width }; console.log('error', error); console.log('rect', rect); } [代码] 以上代码主要是借鉴了拼多多的默认值写法,android 机子中 gap 值大部分为 8,ios 都为 4,开发工具中 ios 为 5.5,android 为 7.5,这样处理之后自己模拟一个胶囊按钮的位置,这样在获取不到胶囊信息的情况下,可保证绝大多数机子完美显示导航头 吐槽 这么重要的问题,官方尽然没有提供解决方案…竟然提供了一张看不清的图片??? 网上有很多ios设置44,android设置48,还有根据不同的手机型号设置不同高度,通过长时间的开发和尝试,本人发现以上方案并不完美,并且bug很多 代码库 Taro组件gitHub地址详细用法请参考README 原生组件npm构建版本gitHub地址详细用法请参考README 原生组件简易版gitHub地址详细用法请参考README 由于本人精力有限,目前只计划发布维护好这2种组件,其他组件请自行修改代码,有问题请联系 备注 上方2种组件在最下方30多款手机测试情况表现良好 iPhone手机打电话和开热点导致导航栏样式错乱,问题已经解决啦,请去demo里测试,这里特别感谢moments网友提出的问题 本文章并无任何商业性质,如有侵权请联系本人修改或删除 文章少量部分内容是本人查询搜集而来 如有问题可以下方留言讨论,微信zhijunxh 比较 斗鱼: [图片] 虎牙: [图片] 微博: [图片] 酷狗: [图片] 知乎: [图片] [图片] 知乎是这里边做的最好的,但是我个人认为有几个可以优化的小问题 打电话或者开启热点导致样式错落,这也是大部门小程序的问题 导航栏下边距太小,看起来不舒服 搜索框距离2侧按钮组距离不对等 自定义返回和home按钮中的竖线颜色重了,并且感觉太粗 如果您看到了此篇文章,请赶快修改自己的代码,并运用在实践中吧 扫码体验我的小程序: [图片] 创作不易,如果对你有帮助,请移步Taro组件gitHub原生组件gitHub给个星星 star✨✨ 谢谢 测试信息 手机型号 胶囊位置信息 statusBarHeight 测试情况 iPhoneX 80 32 281 369 48 88 44 通过 iPhone8 plus 56 32 320 408 24 88 20 通过 iphone7 56 32 281 368 24 87 20 通过 iPhone6 plus 56 32 320 408 24 88 20 通过 iPhone6 56 32 281 368 24 87 20 通过 HUAWEI SLA-AL00 64 32 254 350 32 96 24 通过 HUAWEI VTR-AL00 64 32 254 350 32 96 24 通过 HUAWEI EVA-AL00 64 32 254 350 32 96 24 通过 HUAWEI EML-AL00 68 32 254 350 36 96 29 通过 HUAWEI VOG-AL00 65 32 254 350 33 96 25 通过 HUAWEI ATU-TL10 64 32 254 350 32 96 24 通过 HUAWEI SMARTISAN OS105 64 32 326 422 32 96 24 通过 XIAOMI MI6 59 28 265 352 31 87 23 通过 XIAOMI MI4LTE 60 32 254 350 28 96 20 通过 XIAOMI MIX3 74 32 287 383 42 96 35 通过 REDMI NOTE3 64 32 254 350 32 96 24 通过 REDMI NOTE4 64 32 254 350 32 96 24 通过 REDMI NOTE3 55 28 255 351 27 96 20 通过 REDMI 5plus 67 32 287 383 35 96 28 通过 MEIZU M571C 65 32 254 350 33 96 25 通过 MEIZU M6 NOTE 62 32 254 350 30 96 22 通过 MEIZU MX4 PRO 62 32 278 374 30 96 22 通过 OPPO A33 65 32 254 350 33 96 26 通过 OPPO R11 58 32 254 350 26 96 18 通过 VIVO Y55 64 32 254 350 32 96 24 通过 HONOR BLN-AL20 64 32 254 350 32 96 24 通过 HONOR NEM-AL10 59 28 265 352 31 87 24 通过 HONOR BND-AL10 64 32 254 350 32 96 24 通过 HONOR duk-al20 64 32 254 350 32 96 24 通过 SAMSUNG SM-G9550 64 32 305 401 32 96 24 通过 360 1801-A01 64 32 254 350 32 96 24 通过
2019-11-17 - 纯CSS实现圆环型进度条
以下内容来自于去年的一次案例,随着微信小程序的不断改版,部分条件可能已不再适用,请谨慎参考。内容比较短,主要都在代码片段里。 案例 某个项目中需要用到如下图这样的一个圆环行的进度条。 [图片] 一开始的想法是使用canvas来实现,但是canvas是原生组件,层级最高(当时的情况),实际使用时不方便使用。所以决定尝试用纯CSS来实现这一效果。 实现原理 先上代码:https://developers.weixin.qq.com/s/gjmxwUmm76dG 这里主要用到的是CSS中的clip属性,将一个正方形裁剪后只显示右侧一半,但是仍然以正方形中心为圆心来旋转,来实现需要的角度。 [图片] [代码]clip: rect(0rpx, 46rpx, 92rpx, 0rpx); [代码] 这样最上面那个进度条就可以由以下三部分叠加,在最上面再叠加一个小一号的白色圆形,最外层加上圆角后就可以实现。(下图中红线示例了最外层的圆角以及最上层叠加的白色圆形位置) [图片] 叠加效果 [图片] 用到蓝色圆环小于180度的情况下,需要把背景色和前景色对调。
2019-12-26 - 纯云开发二手书商城的全开源demo
这是为母校写的一个纯粹的公益小程序,原生+云开发,写文章太累了,所以所有代码我都写了注释,还是很适合入门学习的,特别是云开发 [图片] [图片] [图片] 程序本身来说,我认为没啥多大的亮点,只不过把很多单个案例综合起来了,云开发方面,比如:支付、提现、获取用户手机号、发短信、发邮箱。。。。。。。界面上,清一色的flex布局。 和完整版得商城小程序,还差了一丢丢–购物车,因为思考了一下,这个小程序着实用不着,用来学习还是可以了滴 源码和使用教程发在Github: https://github.com/xuhuai66/used-book-pro
2019-09-18 - 十几套小程序视频教程免费分享
[图片] [图片] 闲来无事收集并整理了十几套小程序的视频教程和几百个小程序源码分享给大家,用于帮助大家更好地学习小程序,如果链接失效了可以给我留言。 下载地址: 点击下载 更多资源软件教程下载地址: https://www.90pan.com/n29950
2019-12-11 - 小程序如何发送永久模板消息
大家都很清楚模板消息的使命即将结束,具体可查看下文 10月12日,微信在小程序模板消息能力方面公布了一项重大调整。原有的模板消息将升级为「订阅消息」,支持一次性和长期性订阅消息。而模板消息将于2020年1月10日下线。 查看该通知详情请移步 https://developers.weixin.qq.com/community/develop/doc/00008a8a7d8310b6bf4975b635a401 [图片] 但是模板消息存在的一次表单只能发一次模板消息的限制,在订阅消息方案中并没有解决,那么有没有一种可永久推送消息的实现方案呢,便是本文接下来要讲的内容 关于模板消息转成订阅消息的各种坑,在下面帖子中很详细 https://developers.weixin.qq.com/community/develop/doc/0006088c2940586de249dffbb5b400 在聊具体方案之前,先看看群里讨论的几张截图 [图片] [图片] [图片] [图片] 该方案在微信记账本中实现,微信记账本是腾讯官方推出的一个用于同步账单的小程序 [图片] 可永久推送模板消息方案是: 通过小程序和订阅号绑定,消息通过公众号的模板消息推送发出,只要用户同意推送,我们就可以无限制的发送模板消息。 [图片] [图片] 关于公众号发送模板消息的文档如下所示 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html#5 该方案的弊端就是,在使用小程序的过程中,必须同时关注绑定的订阅号,在小程序用户原大于同主体订阅号的情况下,该方案慎用。 但是为了每天能收到推送我们未尝不可以多做一步,关注下订阅号。
2019-11-28 - 如何打开线上版本小程序的调试模式
生产版本的小程序如果出现问题,可以调试一下正式版看看,调试方式如下: 方式1、https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/wx.setEnableDebug.html [代码]wx.setEnableDebug(Object object) 基础库 1.4.0 开始支持,低版本需做兼容处理。 设置是否打开调试开关。此开关对正式版也能生效。 参数 Object object 属性 类型 默认值 必填 说明 enableDebug boolean 是 是否打开调试 success function 否 接口调用成功的回调函数 fail function 否 接口调用失败的回调函数 complete function 否 接口调用结束的回调函数(调用成功、失败都会执行) 示例代码 // 打开调试 wx.setEnableDebug({ enableDebug: true }) // 关闭调试 wx.setEnableDebug({ enableDebug: false }) Tips [代码] 方式2、先在开发版或体验版打开调试,再切到正式版就能看到vConsole
2019-11-29 - 仿掌上英雄联盟云顶之弈 - 微信小程序版
刚接触小程序开发。最近有玩云顶之弈,就仿照 掌上英雄联盟云顶之弈部分做了一个小程序的学习demo。 包含英雄、英雄详情、装备、阵容、阵容详情、棋子概率 6 个页面 历史 2.01 修复云id image未请求的bug 2.00 增加棋子概率页 修复阵容报错bug 更新版本监听 1.02 增加了阵容详情页面(可分享) 装备筛选栏优化固定 git地址 https://github.com/liulxin/ydzy 欢迎star,也希望有大佬可以一起交流 功能 英雄列表切换、排序、筛选、搜索、详情跳转 英雄详情、跳转 装备列表、搜索、筛选 阵容列表、上拉加载更多 云函数使用 优化页面提示,下拉刷新,英雄页渲染优化 阵容详情页 棋子概率页 demo效果 [图片]
2019-12-08 - 云开发,获取群ID——调试出来真的很简单。
1 app.js中 onLaunch: function (options) { if (!wx.cloud) console.error(‘请使用 2.2.3 或以上的基础库以使用云能力’) else wx.cloud.init({ traceUser: true, }) [代码]if (options.shareTicket) wx.getShareInfo({ shareTicket: options.shareTicket, success: function (res) { console.log('getShareTiket---shareTicket-->res', res) //获取cloudID let cID=res.cloudID //调用云函数mytest wx.cloud.callFunction({ name: 'mytest', // 这个 CloudID 值到云函数端会被替换 data: { weRunData: wx.cloud.CloudID(cID) }, success: function (res) { console.log('wx cloud mytest fun res', res); } }) } }) [代码] }, 2 云函数mytest const cloud = require(‘wx-server-sdk’) cloud.init() exports.main = (event, context) => { return { event } } /console.log(‘wx cloud mytest fun res’, res);查看打印出来的res, 真是一个惊喜。 不用npm,不用加密解密,不用传数据到自己开发服务器上。哎,一个群ID花了我好多时间啊,最后到底是迎来柳暗花明了。/
2019-09-24 - 做了一个颜色选择器
edit at 11/12 代码传到了:https://github.com/eclipseglory/zasi-components , DEMO演示在文章结尾 小程序没有提供color-picker类似的组件,只能自己做。 可传统的RGB颜色选择器,真的腻了,而且在手机上也不是很操作,就跑网上搜了一圈,发现有一种圆环形的(基于HSV)我很喜欢: [图片] 我自诩对canvas2d和webgl很熟悉,做个这玩意儿很轻松,开始做!没想到痛苦开始了。 从上周5开始,一共做了三个版本: 1.纯canvas版本 2.canvas+组件版本 3.纯组件版本 纯canvas版本这个版本做了整整一天! [图片] 由于canvas绘制性能问题,特别是因为没有requestAnimationFrame可以调用,别说在真机上测试特别不流畅,就是在模拟器上也小卡小卡的。而且,在纯的canvas进行触摸定位等事件响应处理,计算起来太麻烦,bug不断,只能放弃了。 混合版本因为wxs模块是提供requestAnimationFrame接口的,所以我就想,使用canvas作为底部颜色环,上面就直接用view作为指针,这样,事件触发和处理比起纯canvas要简单得多,而且还能利用rAF回调页面接口去绘制其他canvas。 的确,我的想法得到了证实,这个混合版本比起第一个要流畅得多! 可就要完工的时候,我却发现,在真机上,cover-view的鼠标事件有很大问题,坐标值飘忽不定,也就是说拖动指针会发生鬼畜般的抖动!加上我不知道怎么debug到wxs模块中,于是跟个sb一样fix,找了半天也没找到问题在哪儿,直到我搜索时,返现有人也遇到和我一样的问题,我才安心了:这是小程序的问题。 动手改!既然cover-view有不行,那就不用它。 实际上canvas在该组件中的作用无非就是绘制一个圆环而已,如果我利用离屏canvas事先画好,然后保存成图片,再用image加载它,这样就可以避免使用canvas来显示圆环了,也就可以不用cover-view放到其顶部! 想法是好的,可是到了真机上,绘制保存出来的图片时好时坏: [图片] 只能放弃,又耽误我一天。 无canvas版本刚才说了,canvas在该组件中的作用,仅仅是绘制一个颜色环而已,除此之外真没什么用。 那我就用css模拟一个类似圆环就好了,精确到每一度一个颜色一点意义没有。 所以就利用css的background-image属性,做了4个四分之一圆弧,然后拼在一起,得到了一个彩色原版,再用一个小的view遮挡,让它们只露出一部分,圆环就做好了。 之前的代码都不用改,直接用新作的圆环views替换canvas的标签即可。主体框架和功能,不到一天就完成了,不得不说,比起纯的canvas绘制,要方便太多太多。 这是截图: [图片] 代码片段这里是 演示DEMO,要使用的话,复制里面的组件出来用就好。 有些代码我混淆过,但不耽误使用。 有问题找我
2019-11-12 - 云开发如何通过微信发送通知
需求描述 在昨天的内容中,我们介绍了如何使用短信发送消息通知,但是,短信通知是收费的,我们似乎也不需要实时性那么高的消息通知方式。 有没有一个实时性还不错,但是免费的方案呢? 答案是有的,那就是借助微信发送消息通知。 解决方案 服务说明 你一定知道,微信本身对于发送消息是有限制的,一般来说,需要我们自己有一个服务号,才能发送消息。 但是,开发一个服务号对于我们来说,成本太高了,因此,我们可以考虑借助一些开放的第三方服务,来完成我们自己的需求。这次,我们借助于一个第三方服务 —— Server 酱 来完成我们的需求。 Server 酱是一个第三方服务,可以用来给我们自己发送通知,你需要做的,仅仅是关注 Server 酱的服务号。 Server 酱的工作原理见下图 [图片] 注册 Server 酱 ,并绑定微信 访问 https://sc.ftqq.com/3.version ,使用你的 Github 账号登陆, 如果你没有 Github 帐号,可以先去注册一个。如果你不知道 Github 是什么,可以等一等明天的企业微信教程。 登录成功,点击顶部菜单栏中的微信推送,扫码,绑定你自己的微信号。 [图片] 发送测试消息 点击顶部菜单栏中的发送消息,可以进入到消息的发送页面。 <img src=“https://postimg.aliavv.com/picgo/20191119210457.png” style=“zoom:80%;” /> 在下方的输入框内输入测试的信息,点击发送消息,就可以发送一条测试消息。 当你看到 [代码]{"errno":0,"errmsg":"success","dataset":"done"}[代码] 的返回,则说明发送成功,这个时候,打开微信,你可以看到这样的消息。 [图片] 这样就说明你的微信绑定成功了,可以进行下一步的开发了。 在进行云函数的编写之前,你需要拿到一个通信的 URL,用于后续的开发。 你可以在 Server 酱的发送消息的页面找到这个 URL。 [图片] 复制这个 URL,作为后续开发所需。 编写云函数 完成了服务的基本配置以后,接下来,我们可以编写一个云函数,来发送消息。 在云函数目录下创建一个名为[代码]sendToWeChat[代码] 的云函数,然后在这个函数上右击,选择在终端中打开 [图片] 在终端中,执行命令安装依赖 [代码]npm install got --save [代码] [图片] 接下来,在小程序开发者工具中,打开 [代码]sendToWeChat[代码] 函数的 [代码]index.js[代码] 文件, 在其中添加如下代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const got = require('got'); const url = '上面拿的消息通信 URL' cloud.init() // 云函数入口函数 exports.main = async (event, context) => { await got(`${url}?text=${encodeURI(event.text)}&desp=${encodeURI(event.desp)}`); return "ok"; } [代码] 这里记得将 URL 的值替换为刚刚你复制的通信 URL 。 设置完成后, 记得上传并部署你的云函数。 发送消息 当你的云函数部署完成后,你就可以通过调用云函数的方式,发送消息,具体的方法如下 [代码]wx.cloud.callFunction({ name:"sendToWeChat", data:{ text:"这是标题", desp:"这是描述" } }) [代码] 总结 昨天,我们介绍了如何发送短信,今天我们介绍了如何低成本的完成消息的推送。明天,我们将介绍,如何向企业微信发送消息。 延伸阅读 云开发如何实现管理员通知消息
2019-11-20 - 小程序顶部导航栏,可滑动,可动态选中放大
最近在研究小程序顶部导航栏时,学到了一个不错的导航栏,今天就来分享给大家。 老规矩,先看效果图 [图片] 可以看到我们实现了如下功能 1,顶部导航栏 2,可以左右滑动的导航栏 3,选中条目放大 原理其实很简单,我这里把我研究后的源码发给大家吧。 wxml文件如下 [代码]<!-- 导航栏 --> <scroll-view scroll-x class="navbar" scroll-with-animation scroll-left="{{scrollLeft}}rpx"> <view class="nav-item" wx:for="{{tabs}}" wx:key="id" bindtap="tabSelect" data-id="{{index}}"> <view class="nav-text {{index==tabCur?'tab-on':''}}">{{item.name}}</view> </view> </scroll-view> [代码] wxss文件如下 [代码]/* 导航栏布局相关 */ .navbar { width: 100%; height: 90rpx; /* 文本不换行 */ white-space: nowrap; display: flex; box-sizing: border-box; border-bottom: 1rpx solid #eee; background: #fff; align-items: center; /* 固定在顶部 */ position: fixed; left: 0rpx; top: 0rpx; } .nav-item { padding-left: 25rpx; padding-right: 25rpx; height: 100%; display: inline-block; /* 普通文字大小 */ font-size: 28rpx; } .nav-text { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; letter-spacing: 4rpx; box-sizing: border-box; } .tab-on { color: #fbbd08; /* 选中放大 */ font-size: 38rpx !important; font-weight: 600; border-bottom: 4rpx solid #fbbd08 !important; } [代码] js文件如下 [代码]// pages/test2/test2.js Page({ data: { tabCur: 0, //默认选中 tabs: [{ name: '等待支付', id: 0 }, { name: '待发货', id: 1 }, { name: '待收货', id: 2 }, { name: '待签字', id: 3 }, { name: '待评价', id: 4 }, { name: '五星好评', id: 5 }, { name: '差评订单', id: 6 }, { name: '编程小石头', id: 8 }, { name: '小石头', id: 9 } ] }, //选择条目 tabSelect(e) { this.setData({ tabCur: e.currentTarget.dataset.id, scrollLeft: (e.currentTarget.dataset.id - 2) * 200 }) } }) [代码] 代码里注释很明白了,大家自己跟着多敲几遍就可以了。后面会更新更多小程序相关的知识,请持续关注。
2019-11-22 - 那些年你没权限调用的API
api 顾名思义 wx.openMiniProgramHistoryList wx.openMiniProgramProfile wx.openMiniProgramSearch wx.openMiniProgramStarList 不再多BB直接上图 1.隐藏/显示右上角胶囊按钮 [图片] 2.截屏 [图片] 最近玩console有点上头 现在的小程序api已经达到了373个了 我是怎么知道的?看图 [图片] 往下拉一看 373 [图片] 这里边有很多的api 是文档中没写的(也可能永远不会写) 先说下 普通的小程序里边是没有权限的调用会提示 {errMsg: “openUserProfile:fail:access denied”} 或者 {errMsg: “getABTestConfig:fail permission denied”, errCode: 1} 代码片段:https://developers.weixin.qq.com/s/FgIssdmP7jdv 可用的api wx.navigateBackH5 --webview 通过这个api可以返回上一个web页面 更多的BUG等你们去处理 我吃饭去咯 end
2019-11-28 - 微信小程序客服消息回复开发
概述 微信小程序为了提高小程序的服务质量,提供了客服消息能力,目的是为了让用户快捷地与小程序服务提供方进行沟通。小程序的客服消息回复有两种方式:一种是接入用户消息到微信公众平台网页版客服工具和客服小助手小程序进行客服消息回复,接入后客服可以看到用户留言,根据用户问题进行专门解答。一种是开启消息推送,当客服无法及时回复的时候能够指导用户联系客服人员或者解决问题。 如果需要接入微信公众平台网页版客服工具和客服小助手,只需要在小程序后台->客服里头添加客服人员就可以,客服人员就可以实时接收到用户消息并且与用户沟通。 如果需要开启消息推送可以参考下文的接入过程。 消息推送开发准备条件 在小程序中设置button组件并且把open-type属性设置为contact 前往小程序后台开发->开发设置->消息推送配置相关信息,可在此指定消息加密方式和数据格式。注意当开启了消息推送,普通微信用户向小程序客服发消息时,微信服务器会先将消息 POST 到开发者填写的 URL 上。在此处的填写我选择的是明文模式和JSON数据格式。 开发过程 处理初次验证 填写完消息推送的配置并且提交后,微信服务器将发送GET请求到填写的URL地址进行校验。因此首先要进行的就是针对微信服务器的初次校验做处理。微信官方在消息推送章节已经提出了校验代码此处便不再重复。 [代码]//微信服务器验证处理 if (isset($_GET['echostr'])) { //调用微信提供的校验代码 if ($this->checkSignature() == false) { exit(); } $echoStr = $_GET['echostr']; echo $echoStr; exit; } [代码] 处理消息 校验成功后,微信服务器会将用户在客服会话中的消息转发到开发者的服务器上,针对微信服务器传入的消息的类型,开发者们可以编写不同的业务逻辑处理。以用户在客服会话中写入文本为例:根据选择的数据格式JSON或者XML,微信服务器会传入相应格式的数据包。根据"MsgType"可以分辨微信服务器转发的是何种类型消息,并编写不同的业务逻辑。 [代码]//1接受微信推送消息 $message = $GLOBALS["HTTP_RAW_POST_DATA"]; $message = json_decode($message, true); //2判定用户发送消息的类型 if (!empty($message['MsgType']) && $message['MsgType'] == 'text') { //do something } [代码] 在处理完微信转发的消息之后,开发者可以根需要调用服务端的客服消息发送接口发送消息给用户。 [代码]$fromUsername = $message['FromUserName']; //发送者openid $url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=$access_token";//根据需要获取access_token $data = array( "touser" => $fromUsername, "msgtype" => "text", "text" => array( "content" => "客服消息推送测试" ) ); $data = json_encode($data, JSON_UNESCAPED_UNICODE); $result = httpRequest($url, "post", $data); $result = json_decode($result, true); if ($result['errcode'] == 0) { //当处理成功之后返回空字符串或者success都可以防止微信服务器重新发起请求 echo ""; exit; } [代码] 转发客服消息 小程序设置了推送消息之后,还可以接入到网页版客服工具中,只需要设置返回数据的MsgType为transfer_customer_service返回给微信服务器。 [代码]//设置转发数据 $transferData = array( "ToUserName" => $message['FromUserName'],//用户的OpenID "FromUserName" => $message['ToUserName'],//小程序原始id "CreateTime" => $message['CreateTime'],//创建时间 "MsgType" => "transfer_customer_service",//指定为transfer_customer_service 消息将会转发到客服工具中 ); $transferData = json_encode($transferData, JSON_UNESCAPED_UNICODE); [代码] 完整代码演示 [代码]//接受微信服务器转发的请求。 public function getMessage() { // 判断是否为微信验证消息 if (isset($_GET['echostr'])) { if ($this->checkSignature() == false) { exit(); } $echoStr = $_GET['echostr']; echo $echoStr; exit; } //接受微信推送消息 $message = $GLOBALS["HTTP_RAW_POST_DATA"]; if (!empty($message)) { $access_token = $this->getAccess();//根据需要获取小程序对应的 access_token //设置转发客服消息 $fromUsername = $message['FromUserName'];//消息发起用户的open_id $transferData = array( "ToUserName" => $fromUsername,//接收方帐号(用户的OpenID) "FromUserName" => $message['ToUserName'],//小程序原始id "CreateTime" => $message['CreateTime'],//创建时间 "MsgType" => "transfer_customer_service",//指定为transfer_customer_service 消息将会转发到客服工具中 ); $transferData = json_encode($transferData, JSON_UNESCAPED_UNICODE); $message = json_decode($message, true); //判定消息类型并处理 if (!empty($message['MsgType']) && $message['MsgType'] == 'text') { //调用send接口发送相对应的消息 $url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=$access_token"; $data = array( "touser" => $fromUsername, "msgtype" => "text", "text" => array( "content" => "客服消息推送测试" ) ); $data = json_encode($data, JSON_UNESCAPED_UNICODE); $result = httpRequest($url, "post", $data); $result = json_decode($result, true); //回复消息之后 不转发消息到客服系统 返回success 或者空字符串 避免微信提示严重错误 if ($result['errcode'] == 0) { echo ""; exit; } //回复消息之后 转发客服消息到客服系统 将$transferData['MsgType']设置为transfer_customer_service //if ($result['errcode'] == 0) { //echo $transferData; //exit; //} } } } /** * 处理微信验证函数 */ public function checkSignature() { $signature = $_GET['signature']; $timestamp = $_GET['timestamp']; $nonce = $_GET['nonce']; $token = "customer12"; //填写在后台配置的Token(令牌) $tmpArr = array($token, $timestamp, $nonce); sort($tmpArr, SORT_STRING); $tmpStr = implode($tmpArr); $tmpStr = sha1($tmpStr); //加密 if ($tmpStr == $signature) { return true; } else { return false; } } /** * CURL请求 * @param $url 请求url地址 * @param $method 请求方法 get post * @param null $postfields post数据数组 * @param array $headers 请求header信息 * @param bool|false $debug 调试开启 默认false * @return mixed */ function httpRequest($url, $method, $postfields = null, $headers = array(), $debug = false) { $method = strtoupper($method); $ci = curl_init(); curl_setopt($ci, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); curl_setopt($ci, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0"); curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, 60); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */ curl_setopt($ci, CURLOPT_TIMEOUT, 7); /* 设置cURL允许执行的最长秒数 */ curl_setopt($ci, CURLOPT_RETURNTRANSFER, true); switch ($method) { case "POST": curl_setopt($ci, CURLOPT_POST, true); if (!empty($postfields)) { $tmpdatastr = is_array($postfields) ? http_build_query($postfields) : $postfields; curl_setopt($ci, CURLOPT_POSTFIELDS, $tmpdatastr); } break; default: curl_setopt($ci, CURLOPT_CUSTOMREQUEST, $method); /* //设置请求方式 */ break; } $ssl = preg_match('/^https:\/\//i', $url) ? TRUE : FALSE; curl_setopt($ci, CURLOPT_URL, $url); if ($ssl) { curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, FALSE); // https请求 不验证证书和hosts curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, FALSE); // 不从证书中检查SSL加密算法是否存在 } curl_setopt($ci, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ci, CURLOPT_MAXREDIRS, 2);/*指定最多的HTTP重定向的数量,这个选项是和CURLOPT_FOLLOWLOCATION一起使用的*/ curl_setopt($ci, CURLOPT_HTTPHEADER, $headers); curl_setopt($ci, CURLINFO_HEADER_OUT, true); $response = curl_exec($ci); $requestinfo = curl_getinfo($ci); $http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE); if ($debug) { echo "=====post data======\r\n"; var_dump($postfields); echo "=====info===== \r\n"; print_r($requestinfo); echo "=====response=====\r\n"; print_r($response); } curl_close($ci); return $response; } [代码]
2019-05-29 - 小程序上传图片到腾讯云对象存储COS的简单代码
const fs = require('fs') const COS = require('cos-nodejs-sdk-v5') const cos = new COS({ SecretId: 'SecretId', SecretKey: 'SecretKey', }) module.exports = async (ctx) => { const image = ctx.request.files.image //这里获得小程序wx.uploadFile上传的文件,文件标识名为image if(!image) return let ext = image.type.split('/')[1] let path = image.path let key = `image-${Date.now()}.${ext}`;//保存在cos的文件名 let TaskId; function p() { return new Promise((resolve, reject) => { cos.putObject({ Bucket: 'Bucket-1251490133', /* 必须 */ Region: 'cn-north', Key: key, /* 必须 */ Body: fs.createReadStream(path), ContentLength: fs.statSync(path).size }, function (err, data) { console.log(err || data); if (err) { reject(); } else { resolve("url/" + key); } fs.unlinkSync(path); }); }); } try{ ctx.body = await p() }catch (err){ console.log(err) } } [图片]
2020-10-20 - 传统原生支付用云开发实现(非云支付)
本文的代码已过时,请勿照抄。建议改用云支付。 本文的代码被论坛自动过滤了所有XML的标签,所以照抄是会出错的。需要代码的话,看以前的老版本: https://developers.weixin.qq.com/community/develop/doc/000620ec5acb482103b7bf41d51804?jumpto=comment&commentid=000ea67d7b4da8d6c47acd1e05b8 代码前提:只需要替换两个与自己相关的参数key和mch_id 1、小程序开通微信支付成功,去公众平台(https://mp.weixin.qq.com/); 成功后可以知道自己的mch_id,即商户号。 2、去这里:商户平台(https://pay.weixin.qq.com/),获取key = API密钥,如果是退款的话,还需要下载API证书。 [图片] 以下代码仅包含统一下单,以及小程序端拉起支付的代码。 小程序端: testWxCloudPay: function () { wx.cloud.callFunction({ name: 'getPay', // data: {body:"body",attach:"attach",total_fee:1}, // 可传入相关参数。 success: res => { console.log(res.result) if (!res.result.appId) return wx.requestPayment({ ...res.result, success: res => { console.log(res) } }) } }) }, 云函数getPay: const key = "ABC...XYZ" //换成你的商户key,32位 const mch_id = "1413092000" //换成你的商户号 //以下全部照抄即可 const cloud = require('wx-server-sdk') const rp = require('request-promise') const crypto = require('crypto') cloud.init() function getSign(args) { let sa = [] for (let k in args) sa.push(k + '=' + args[k]) sa.push('key=' + key) return crypto.createHash('md5').update(sa.join('&'), 'utf8').digest('hex').toUpperCase() } function getXml(args) { let sa = [] for (let k in args) sa.push('<' + k + '>' + args[k] + '') sa.push('' + getSign(args) + '') return '' + sa.join('') + '' } exports.main = async(event, context) => { const wxContext = cloud.getWXContext() const appId = appid = wxContext.APPID const openid = wxContext.OPENID const attach = 'attach' const body = 'body' const total_fee = 1 const notify_url = "https://mysite.com/notify" const spbill_create_ip = "118.89.40.200" const nonceStr = nonce_str = Math.random().toString(36).substr(2, 15) const timeStamp = parseInt(Date.now() / 1000) + '' const out_trade_no = "otn" + nonce_str + timeStamp const trade_type = "JSAPI" const xmlArgs = { appid, openid, attach, body, mch_id, nonce_str, notify_url, out_trade_no, spbill_create_ip, total_fee, trade_type } let xml = (await rp({ url: "https://api.mch.weixin.qq.com/pay/unifiedorder", method: 'POST', body: getXml(xmlArgs) })).toString("utf-8") if (xml.indexOf('prepay_id') < 0) return xml let prepay_id = xml.split("")[0] let payArgs = { appId, nonceStr, package: ('prepay_id=' + prepay_id), signType: 'MD5', timeStamp } return { ...payArgs, paySign: getSign(payArgs) } } packge.json: { "name": "getPay", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "zfe", "license": "ISC", "dependencies": { "wx-server-sdk": "latest", "crypto": "^1.0.1", "request-promise": "^4.2.2" } } 附:完整代码片段:如果你觉得在这上面花的时间超过一天了,就去下载代码片段吧。 [图片]
2020-10-20 - 「笔记」开发微信开放社区小程序版
前言 去年在微信公开课小程序里就看到有社区版块,但是就很好奇为啥社区自己不出一个小程序版本的。至少在3月份以前,微信开放社区在手机端虽然进行了简单适配,但是访问的时候用户体验并不是很好,到目前为止手机端访问社区时依然不支持搜索和回复功能。 微信开放社区已经做了手机的适配,为啥还要自己做一个小程序版本的呢? 2月13日在群里聊天的时候 @Stephen 突然发了一个社区列表接口地址,就聊起了要利用公开接口来自己做一个用于查看社区信息的想法。 隔了将近一周,有天实在是闲得没啥事干,于是就开始研究社区的接口来,大概用了一个上午的时间理清了社区栏目和文章页的接口信息和参数含义,接下来就可以做自己的小程序页面了。 体验地址 https://www.qzwu.com/WeChat 开发浏览功能 问题解答接口: https://developers.weixin.qq.com/community/ngi/timeline/{参数1}/{参数2}/{参数3}?page=1 [代码]参数1:版块分类 1:小程序 2:小游戏 8:微信支付 参数2:类型 1:默认 2:公告 参数3:标签ID 0:默认 [代码] 列表接口: https://developers.weixin.qq.com/community/ngi/{参数1}/list/{参数2}?page=1&blockType={参数3} [代码]参数1:文章分类 doc:普通帖子 article:文章 参数2:栏目ID 参数3:版块分类 1:小程序 2:小游戏 8:微信支付 [代码] 搜索接口: https://developers.weixin.qq.com/community/ngi/search?query=关键词&page=1&blogCategory={参数1} [代码]参数1:分类 511:相关帖子 1024:官方教程 512:小故事 [代码] 有了以上3个接口,简单的社区就能做出来了。 开发社区登陆功能 开发社区回复功能首先要解决登陆的问题,官方自然不会有API文档让小程序接入社区。以下登陆流程主要灵感来源于 @这都申请了 提供的思路。 1.获取state [代码]https://developers.weixin.qq.com/community/ngi/ [代码] 2.获取登录二维码页面 [代码]https://open.weixin.qq.com/connect/qrconnect?appid=wx1bb297ee890403a9&scope=snsapi_login&redirect_uri=https://developers.weixin.qq.com&state={state}&login_type=jssdk [代码] 3.解析获取登录二维码以及二维码的uuid 4.监听登录状态(超时时间设置为60秒) [代码]https://long.open.weixin.qq.com/connect/l/qrconnect?uuid={uuid}&_={随机数} [代码] 5.监听返回内容 wx_errcode为405代表登录成功,获取wx_code 6.登录身份验证地址获取用户cookie [代码]https://developers.weixin.qq.com/community/ngi/welogin?type=0&redirect_url=&code={wx_code}&state={state} [代码] 7.解析上一步页面中的set-cookie并保存到数据库 8.生成对应的小程序码 9.扫码进入小程序与小程序内登陆用户的openId或其它用户标识信息绑定 开发评论功能 发布评论接口: https://developers.weixin.qq.com/community/ngi/comment/create?random={随机数}&token={参数1} [代码]参数1:token 这个在登陆的时候能够获取到 [代码] 总结 开发目的 解决手机只能看不能回复的困扰; 可以方便的在床上、马桶上和社区网红 @卢霄霄 聊天; 不是为了采集数据或者以其它方式盈利,纯粹只是闲的; 还需要优化的地方 小程序内对富文本和一些H5特殊标签支持不是很好,会导致部分内容无法正常显示; 期待官方的社区小程序能早日推出; [图片] *最后鄙视下那些采集信息到自己网站上的人。
2020-03-12 - 答题小程序搭建系列一
答题小程序搭建系列一 后台界面 截图一 [图片] 截图二 [图片] 截图三 [图片] 截图四 [图片] 截图五 [图片] 截图六 [图片] 截图七 [图片] 截图八 [图片] 截图九 [图片] 截图十 [图片] 截图十一 [图片] 截图十二 [图片] 数据设计 [图片] 截图一 [图片] 截图二 [图片] 截图三 [图片] 截图四 [图片] 更新记录 答题小程序 2020-10-12 新增对主观题的支持,具体包括以下几种类型 [代码] "单选"=>"01", "多选"=>"02", "判断"=>"03", "填空"=>"04", "简答"=>"05", "论述"=>"06", "名词解释"=>"07" [代码] 其他
2020-10-12 - 云开发如何实现管理员通知消息
需求描述 小程序目前的主要能力还都在小程序端实现,但是我们在进行开发的小程序不可能只有小程序端能力,我们也会有一些管理端能力。比如说,当用户在小程序中提交了消息以后,我们的小程序应该可以通知到小程序的管理员,以便让管理员进行下一步操作。 解决方案 架构说明 由于小程序本身不支持长久性的消息通知能力,因此,我们可以考虑借助一些第三方的服务和能力,来完成我们自己的需求。 这个需求很适合使用小程序新发布的长期订阅消息能力,但是目前该能力开放的类目还不足以支持我们的需要。 一般而言,使用短信是我们目前到达率比较高的能力,且更为普遍的能力,其他通道的能力大多受限或不符合国情,为了确保通知信息的到达率,我们这篇文章就使用短信来完成需求。 架构图示 [图片] 具体操作 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 - 纯云开发 使用一个小程序访问另一个小程序的云资源
由于工作需要,我需要使用一个小程序与另一个小程序共同享用同一套云资源。这就需要用到'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 - 极简代码之云开发的触底无限加载
js: [代码]const db = wx.cloud.database() const _ = db.command const col = "test" const sql = { _id: _.neq(1) } //获取所有记录 Page({ data: { isEndOfList: false, list: [], limit: 20 //每次拉取数量 }, onLoad: function(options) { this.getData() }, getData: function() { db.collection(col) .where(sql) .skip(this.data.list.length) .limit(this.data.limit) .get() .then(res => { this.setData({ list: [...this.data.list, ...res.data], //合并数据 isEndOfList: res.data.length < this.data.limit ? true : false //判断是否结束 }) }) }, onReachBottom: function() { this.data.isEndOfList || this.getData() } }) [代码] wxml [代码]<view style="height:100px" wx:for='{{list}}' wx:key='none'>{{index}}</view> <view style="padding:15px;text-align:center;color:grey" wx:if='{{list.length>limit}}'> <view wx:if='{{(!isEndOfList)}}'>正在加载数据...</view> <view wx:else>----END----</view> </view> [代码]
2020-06-16 - 将小程序原生异步函数promisify后,在async/await中使用
目前,小程序中支持使用async/await有三种模式: 1、不勾选es6转es5,不勾选增强编译;该模式是纯es7的async/await,需要基础库高版本。 2、勾选es6转es5,勾选增强编译;一般是因为调用了第三方的es5插件,通过增强编译支持async/await。 3、勾选es6转es5,不勾选增强编译;手工引入runtime.js支持async/await。 据最近更新情况,原生的函数已经大部分同时原生支持同步化了,不需要本方案转化了,直接加上await即可;比如wx.chooseImage、wx.showModal。。。具体有哪些,可以自己试。 如果只是wx.request的同步化,可参考: https://developers.weixin.qq.com/community/develop/article/doc/0004cc839407a069f77a416c056813 app.js代码: function promisify(api) { return (opt, ...arg) => { return new Promise((resolve, reject) => { api(Object.assign({}, opt, { success: resolve, fail: reject }), ...arg) }) } } App({ globalData: {}, chooseImage: promisify(wx.chooseImage), request: promisify(wx.request), getUserInfo: promisify(wx.getUserInfo), onLaunch: function () { }, }) 某page的index.js代码: const app = getApp() testAsync: async function(){ let res = await app.chooseImage() console.log(res) res = await app.request({url:'url',method:'POST',data:{x:0,y:1}}) console.log(res) }, [图片]
2020-10-20 - 通过授权登录介绍小程序原生开发如何引入async/await、状态管理等工具
登陆和授权是小程序开发会遇到的第一个问题,这里把相关业务逻辑、工具代码抽取出来,展示我们如何引入的一些包使得原生微信小程序内也可以使用 async/await、fetch、localStorage、状态管理、GraphQL 等等特性,希望对大家有所帮助。 前端 目录结构 [代码]├── app.js ├── app.json ├── app.wxss ├── common │ └── api │ └── index.js ├── config.js ├── pages │ └── index │ ├── api │ │ └── index.js │ ├── img │ │ ├── btn.png │ │ └── bg.jpg │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── project.config.json ├── store │ ├── action.js │ └── index.js ├── utils │ └── index.js └── vendor ├── event-emitter.js ├── fetch.js ├── fetchql.js ├── http.js ├── promisify.js ├── regenerator.js ├── storage.js └── store.js [代码] 业务代码 app.js [代码]import store from './store/index' const { loginInfo } = store.state App({ store, onLaunch() { // 打开小程序即登陆,无需用户授权可获得 openID if(!loginInfo) store.dispatch('login') }, }) [代码] store/index.js [代码]import Store from '../vendor/store' import localStorage from '../vendor/storage' import actions from './action' const loginInfo = localStorage.getItem('loginInfo') export default new Store({ state: { // 在全局状态中维护登陆信息 loginInfo, }, actions, }) [代码] store/action.js [代码]import regeneratorRuntime from '../vendor/regenerator'; import wx from '../vendor/promisify'; import localStorage from '../vendor/storage' import api from '../common/api/index'; export default { async login({ state }, payload) { const { code } = await wx.loginAsync(); const { authSetting } = await wx.getSettingAsync() // 如果用户曾授权,直接可以拿到 encryptedData const { encryptedData, iv } = authSetting['scope.userInfo'] ? await wx.getUserInfoAsync({ withCredentials: true }) : {}; // 如果用户未曾授权,也可以拿到 openID const { token, userInfo } = await api.login({ code, encryptedData, iv }); // 为接口统一配置 Token getApp().gql.requestObject.headers['Authorization'] = `Bearer ${token}`; // 本地缓存登陆信息 localStorage.setItem('loginInfo', { token, userInfo } ) return { loginInfo: { token, userInfo } } } } [代码] common/api/index.js [代码]import regeneratorRuntime from '../../vendor/regenerator.js' export default { /** * 登录接口 * 如果只有 code,只返回 token,如果有 encryptedData, iv,同时返回用户的昵称和头像 * @param {*} param0 */ async login({ code, encryptedData, iv }) { const query = `query login($code: String!, $encryptedData: String, $iv: String){ login(code:$code, encryptedData:$encryptedData, iv:$iv, appid:$appid){ token userInfo { nickName avatarUrl } } }` const { login: { token, userInfo } } = await getApp().query({ query, variables: { code, encryptedData, iv } }) return { token, userInfo } }, } [代码] pages/index/index.js [代码]import regeneratorRuntime from '../../vendor/regenerator.js' const app = getApp() Page({ data: {}, onLoad(options) { // 将用户登录信息注入到当前页面的 data 中,并且当数据在全局范围内被更新时,都会自动刷新本页面 app.store.mapState(['loginInfo'], this) }, async login({ detail: { errMsg } }) { if (errMsg === 'getUserInfo:fail auth deny') return app.store.dispatch('login') // 继续处理业务 }, }) [代码] pages/index/index.wxml [代码]<view class="container"> <form report-submit="true" bindsubmit="saveFormId"> <button form-type="submit" open-type="getUserInfo" bindgetuserinfo="login">登录</button> </form> </view> [代码] 工具代码 事件处理 vendor/event-emitter.js [代码]const id_Identifier = '__id__'; function randomId() { return Math.random().toString(36).substr(2, 16); } function findIndexById(id) { return this.findIndex(item => item[id_Identifier] === id); } export default class EventEmitter { constructor() { this.events = {} } /** * listen on a event * @param event * @param listener */ on(event, listener) { let { events } = this; let container = events[event] || []; let id = randomId(); let index; listener[id_Identifier] = id; container.push(listener); return () => { index = findIndexById.call(container, id); index >= 0 && container.splice(index, 1); } }; /** * remove all listen of an event * @param event */ off (event) { this.events[event] = []; }; /** * clear all event listen */ clear () { this.events = {}; }; /** * listen on a event once, if it been trigger, it will cancel the listner * @param event * @param listener */ once (event, listener) { let { events } = this; let container = events[event] || []; let id = randomId(); let index; let callback = () => { index = findIndexById.call(container, id); index >= 0 && container.splice(index, 1); listener.apply(this, arguments); }; callback[id_Identifier] = id; container.push(callback); }; /** * emit event */ emit () { const { events } = this; const argv = [].slice.call(arguments); const event = argv.shift(); ((events['*'] || []).concat(events[event] || [])).map(listener => self.emitting(event, argv, listener)); }; /** * define emitting * @param event * @param dataArray * @param listener */ emitting (event, dataArray, listener) { listener.apply(this, dataArray); }; } [代码] 封装 wx.request() 接口 vendor/http.js [代码]import EventEmitter from './event-emitter.js'; const DEFAULT_CONFIG = { maxConcurrent: 10, timeout: 0, header: {}, dataType: 'json' }; class Http extends EventEmitter { constructor(config = DEFAULT_CONFIG) { super(); this.config = config; this.ctx = wx; this.queue = []; this.runningTask = 0; this.maxConcurrent = DEFAULT_CONFIG.maxConcurrent; this.maxConcurrent = config.maxConcurrent; this.requestInterceptor = () => true; this.responseInterceptor = () => true; } create(config = DEFAULT_CONFIG) { return new Http(config); } next() { const queue = this.queue; if (!queue.length || this.runningTask >= this.maxConcurrent) return; const entity = queue.shift(); const config = entity.config; const { requestInterceptor, responseInterceptor } = this; if (requestInterceptor.call(this, config) !== true) { let response = { data: null, errMsg: `Request Interceptor: Request can\'t pass the Interceptor`, statusCode: 0, header: {} }; entity.reject(response); return; } this.emit('request', config); this.runningTask = this.runningTask + 1; let timer = null; let aborted = false; let finished = false; const callBack = { success: (res) => { if (aborted) return; finished = true; timer && clearTimeout(timer); entity.response = res; this.emit('success', config, res); responseInterceptor.call(this, config, res) !== true ? entity.reject(res) : entity.resolve(res); }, fail: (res) => { if (aborted) return; finished = true; timer && clearTimeout(timer); entity.response = res; this.emit('fail', config, res); responseInterceptor.call(this, config, res) !== true ? entity.reject(res) : entity.resolve(res); }, complete: () => { if (aborted) return; this.emit('complete', config, entity.response); this.next(); this.runningTask = this.runningTask - 1; } }; const requestConfig = Object.assign(config, callBack); const task = this.ctx.request(requestConfig); if (this.config.timeout > 0) { timer = setTimeout(() => { if (!finished) { aborted = true; task && task.abort(); this.next(); } }, this.config.timeout); } } request(method, url, data, header, dataType = 'json') { const config = { method, url, data, header: { ...header, ...this.config.header }, dataType: dataType || this.config.dataType }; return new Promise((resolve, reject) => { const entity = { config, resolve, reject, response: null }; this.queue.push(entity); this.next(); }); } head(url, data, header, dataType) { return this.request('HEAD', url, data, header, dataType); } options(url, data, header, dataType) { return this.request('OPTIONS', url, data, header, dataType); } get(url, data, header, dataType) { return this.request('GET', url, data, header, dataType); } post(url, data, header, dataType) { return this.request('POST', url, data, header, dataType); } put(url, data, header, dataType) { return this.request('PUT', url, data, header, dataType); } ['delete'](url, data, header, dataType) { return this.request('DELETE', url, data, header, dataType); } trace(url, data, header, dataType) { return this.request('TRACE', url, data, header, dataType); } connect(url, data, header, dataType) { return this.request('CONNECT', url, data, header, dataType); } setRequestInterceptor(interceptor) { this.requestInterceptor = interceptor; return this; } setResponseInterceptor(interceptor) { this.responseInterceptor = interceptor; return this; } clean() { this.queue = []; } } export default new Http(); [代码] 兼容 fetch 标准 vendor/fetch.js [代码]import http from './http'; const httpClient = http.create({ maxConcurrent: 10, timeout: 0, header: {}, dataType: 'json' }); function generateResponse(res) { let header = res.header || {}; let config = res.config || {}; return { ok: ((res.statusCode / 200) | 0) === 1, // 200-299 status: res.statusCode, statusText: res.errMsg, url: config.url, clone: () => generateResponse(res), text: () => Promise.resolve( typeof res.data === 'string' ? res.data : JSON.stringify(res.data) ), json: () => { if (typeof res.data === 'object') return Promise.resolve(res.data); let json = {}; try { json = JSON.parse(res.data); } catch (err) { console.error(err); } return json; }, blob: () => Promise.resolve(new Blob([res.data])), headers: { keys: () => Object.keys(header), entries: () => { let all = []; for (let key in header) { if (header.hasOwnProperty(key)) { all.push([key, header[key]]); } } return all; }, get: n => header[n.toLowerCase()], has: n => n.toLowerCase() in header } }; } export default (typeof fetch === 'function' ? fetch.bind() : function(url, options) { options = options || {}; return httpClient .request(options.method || 'get', url, options.body, options.headers) .then(res => Promise.resolve(generateResponse(res))) .catch(res => Promise.reject(generateResponse(res))); }); [代码] GraphQL客户端 vendor/fetchql.js [代码]import fetch from './fetch'; // https://github.com/gucheen/fetchql /** Class to realize fetch interceptors */ class FetchInterceptor { constructor() { this.interceptors = []; /* global fetch */ this.fetch = (...args) => this.interceptorWrapper(fetch, ...args); } /** * add new interceptors * @param {(Object|Object[])} interceptors */ addInterceptors(interceptors) { const removeIndex = []; if (Array.isArray(interceptors)) { interceptors.map((interceptor) => { removeIndex.push(this.interceptors.length); return this.interceptors.push(interceptor); }); } else if (interceptors instanceof Object) { removeIndex.push(this.interceptors.length); this.interceptors.push(interceptors); } this.updateInterceptors(); return () => this.removeInterceptors(removeIndex); } /** * remove interceptors by indexes * @param {number[]} indexes */ removeInterceptors(indexes) { if (Array.isArray(indexes)) { indexes.map(index => this.interceptors.splice(index, 1)); this.updateInterceptors(); } } /** * @private */ updateInterceptors() { this.reversedInterceptors = this.interceptors .reduce((array, interceptor) => [interceptor].concat(array), []); } /** * remove all interceptors */ clearInterceptors() { this.interceptors = []; this.updateInterceptors(); } /** * @private */ interceptorWrapper(fetch, ...args) { let promise = Promise.resolve(args); this.reversedInterceptors.forEach(({ request, requestError }) => { if (request || requestError) { promise = promise.then(() => request(...args), requestError); } }); promise = promise.then(() => fetch(...args)); this.reversedInterceptors.forEach(({ response, responseError }) => { if (response || responseError) { promise = promise.then(response, responseError); } }); return promise; } } /** * GraphQL client with fetch api. * @extends FetchInterceptor */ class FetchQL extends FetchInterceptor { /** * Create a FetchQL instance. * @param {Object} options * @param {String} options.url - the server address of GraphQL * @param {(Object|Object[])=} options.interceptors * @param {{}=} options.headers - request headers * @param {FetchQL~requestQueueChanged=} options.onStart - callback function of a new request queue * @param {FetchQL~requestQueueChanged=} options.onEnd - callback function of request queue finished * @param {Boolean=} options.omitEmptyVariables - remove null props(null or '') from the variables * @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api) */ constructor({ url, interceptors, headers, onStart, onEnd, omitEmptyVariables = false, requestOptions = {}, }) { super(); this.requestObject = Object.assign( {}, { method: 'POST', headers: Object.assign({}, { Accept: 'application/json', 'Content-Type': 'application/json', }, headers), credentials: 'same-origin', }, requestOptions, ); this.url = url; this.omitEmptyVariables = omitEmptyVariables; // marker for request queue this.requestQueueLength = 0; // using for caching enums' type this.EnumMap = {}; this.callbacks = { onStart, onEnd, }; this.addInterceptors(interceptors); } /** * operate a query * @param {Object} options * @param {String} options.operationName * @param {String} options.query * @param {Object=} options.variables * @param {Object=} options.opts - addition options(will not be passed to server) * @param {Boolean=} options.opts.omitEmptyVariables - remove null props(null or '') from the variables * @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api) * @returns {Promise} * @memberOf FetchQL */ query({ operationName, query, variables, opts = {}, requestOptions = {}, }) { const options = Object.assign({}, this.requestObject, requestOptions); let vars; if (this.omitEmptyVariables || opts.omitEmptyVariables) { vars = this.doOmitEmptyVariables(variables); } else { vars = variables; } const body = { operationName, query, variables: vars, }; options.body = JSON.stringify(body); this.onStart(); return this.fetch(this.url, options) .then((res) => { if (res.ok) { return res.json(); } // return an custom error stack if request error return { errors: [{ message: res.statusText, stack: res, }], }; }) .then(({ data, errors }) => ( new Promise((resolve, reject) => { this.onEnd(); // if data in response is 'null' if (!data) { return reject(errors || [{}]); } // if all properties of data is 'null' const allDataKeyEmpty = Object.keys(data).every(key => !data[key]); if (allDataKeyEmpty) { return reject(errors); } return resolve({ data, errors }); }) )); } /** * get current server address * @returns {String} * @memberOf FetchQL */ getUrl() { return this.url; } /** * setting a new server address * @param {String} url * @memberOf FetchQL */ setUrl(url) { this.url = url; } /** * get information of enum type * @param {String[]} EnumNameList - array of enums' name * @returns {Promise} * @memberOf FetchQL */ getEnumTypes(EnumNameList) { const fullData = {}; // check cache status const unCachedEnumList = EnumNameList.filter((element) => { if (this.EnumMap[element]) { // enum has been cached fullData[element] = this.EnumMap[element]; return false; } return true; }); // immediately return the data if all enums have been cached if (!unCachedEnumList.length) { return new Promise((resolve) => { resolve({ data: fullData }); }); } // build query string for uncached enums const EnumTypeQuery = unCachedEnumList.map(type => ( `${type}: __type(name: "${type}") { ...EnumFragment }` )); const query = ` query { ${EnumTypeQuery.join('\n')} } fragment EnumFragment on __Type { kind description enumValues { name description } }`; const options = Object.assign({}, this.requestObject); options.body = JSON.stringify({ query }); this.onStart(); return this.fetch(this.url, options) .then((res) => { if (res.ok) { return res.json(); } // return an custom error stack if request error return { errors: [{ message: res.statusText, stack: res, }], }; }) .then(({ data, errors }) => ( new Promise((resolve, reject) => { this.onEnd(); // if data in response is 'null' and have any errors if (!data) { return reject(errors || [{ message: 'Do not get any data.' }]); } // if all properties of data is 'null' const allDataKeyEmpty = Object.keys(data).every(key => !data[key]); if (allDataKeyEmpty && errors && errors.length) { return reject(errors); } // merge enums' data const passData = Object.assign(fullData, data); // cache new enums' data Object.keys(data).map((key) => { this.EnumMap[key] = data[key]; return key; }); return resolve({ data: passData, errors }); }) )); } /** * calling on a request starting * if the request belong to a new queue, call the 'onStart' method */ onStart() { this.requestQueueLength++; if (this.requestQueueLength > 1 || !this.callbacks.onStart) { return; } this.callbacks.onStart(this.requestQueueLength); } /** * calling on a request ending * if current queue finished, calling the 'onEnd' method */ onEnd() { this.requestQueueLength--; if (this.requestQueueLength || !this.callbacks.onEnd) { return; } this.callbacks.onEnd(this.requestQueueLength); } /** * Callback of requests queue changes.(e.g. new queue or queue finished) * @callback FetchQL~requestQueueChanged * @param {number} queueLength - length of current request queue */ /** * remove empty props(null or '') from object * @param {Object} input * @returns {Object} * @memberOf FetchQL * @private */ doOmitEmptyVariables(input) { const nonEmptyObj = {}; Object.keys(input).map(key => { const value = input[key]; if ((typeof value === 'string' && value.length === 0) || value === null || value === undefined) { return key; } else if (value instanceof Object) { nonEmptyObj[key] = this.doOmitEmptyVariables(value); } else { nonEmptyObj[key] = value; } return key; }); return nonEmptyObj; } } export default FetchQL; [代码] 将wx的异步接口封装成Promise vendor/promisify.js [代码]function promisify(wx) { let wxx = { ...wx }; for (let attr in wxx) { if (!wxx.hasOwnProperty(attr) || typeof wxx[attr] != 'function') continue; // skip over the sync method if (/sync$/i.test(attr)) continue; wxx[attr + 'Async'] = function asyncFunction(argv = {}) { return new Promise(function (resolve, reject) { wxx[attr].call(wxx, { ...argv, ...{ success: res => resolve(res), fail: err => reject(err) } }); }); }; } return wxx; } export default promisify(typeof wx === 'object' ? wx : {}); [代码] localstorage vendor/storage.js [代码]class Storage { constructor(wx) { this.wx = wx; } static get timestamp() { return new Date() / 1000; } static __isExpired(entity) { if (!entity) return true; return Storage.timestamp - (entity.timestamp + entity.expiration) >= 0; } static get __info() { let info = {}; try { info = this.wx.getStorageInfoSync() || info; } catch (err) { console.error(err); } return info; } setItem(key, value, expiration) { const entity = { timestamp: Storage.timestamp, expiration, key, value }; this.wx.setStorageSync(key, JSON.stringify(entity)); return this; } getItem(key) { let entity; try { entity = this.wx.getStorageSync(key); if (entity) { entity = JSON.parse(entity); } else { return null; } } catch (err) { console.error(err); return null; } // 没有设置过期时间, 则直接返回值 if (!entity.expiration) return entity.value; // 已过期 if (Storage.__isExpired(entity)) { this.remove(key); return null; } else { return entity.value; } } removeItem(key) { try { this.wx.removeStorageSync(key); } catch (err) { console.error(err); } return this; } clear() { try { this.wx.clearStorageSync(); } catch (err) { console.error(err); } return this; } get info() { let info = {}; try { info = this.wx.getStorageInfoSync(); } catch (err) { console.error(err); } return info || {}; } get length() { return (this.info.keys || []).length; } } export default new Storage(wx); [代码] 状态管理 vendor/store.js [代码]module.exports = class Store { constructor({ state, actions }) { this.state = state || {} this.actions = actions || {} this.ctxs = [] } // 派发action, 统一返回promise action可以直接返回state dispatch(type, payload) { const update = res => { if (typeof res !== 'object') return this.setState(res) this.ctxs.map(ctx => ctx.setData(res)) return res } if (typeof this.actions[type] !== 'function') return const res = this.actions[type](this, payload) return res.constructor.toString().match(/function\s*([^(]*)/)[1] === 'Promise' ? res.then(update) : new Promise(resolve => resolve(update(res))) } // 修改state的方法 setState(data) { this.state = { ...this.state, ...data } } // 根据keys获取state getState(keys) { return keys.reduce((acc, key) => ({ ...acc, ...{ [key]: this.state[key] } }), {}) } // 映射state到实例中,可在onload或onshow中调用 mapState(keys, ctx) { if (!ctx || typeof ctx.setData !== 'function') return ctx.setData(this.getState(keys)) this.ctxs.push(ctx) } } [代码] 兼容 async/await vendor/regenerator.js [代码]/** * Copyright (c) 2014-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var regeneratorRuntime = (function (exports) { "use strict"; var Op = Object.prototype; var hasOwn = Op.hasOwnProperty; var undefined; // More compressible than void 0. var $Symbol = typeof Symbol === "function" ? Symbol : {}; var iteratorSymbol = $Symbol.iterator || "@@iterator"; var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator"; var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; function wrap(innerFn, outerFn, self, tryLocsList) { // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator. var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; var generator = Object.create(protoGenerator.prototype); var context = new Context(tryLocsList || []); // The ._invoke method unifies the implementations of the .next, // .throw, and .return methods. generator._invoke = makeInvokeMethod(innerFn, self, context); return generator; } exports.wrap = wrap; // Try/catch helper to minimize deoptimizations. Returns a completion // record like context.tryEntries[i].completion. This interface could // have been (and was previously) designed to take a closure to be // invoked without arguments, but in all the cases we care about we // already have an existing method we want to call, so there's no need // to create a new function object. We can even get away with assuming // the method takes exactly one argument, since that happens to be true // in every case, so we don't have to touch the arguments object. The // only additional allocation required is the completion record, which // has a stable shape and so hopefully should be cheap to allocate. function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } } var GenStateSuspendedStart = "suspendedStart"; var GenStateSuspendedYield = "suspendedYield"; var GenStateExecuting = "executing"; var GenStateCompleted = "completed"; // Returning this object from the innerFn has the same effect as // breaking out of the dispatch switch statement. var ContinueSentinel = {}; // Dummy constructor functions that we use as the .constructor and // .constructor.prototype properties for functions that return Generator // objects. For full spec compliance, you may wish to configure your // minifier not to mangle the names of these two functions. function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} // This is a polyfill for %IteratorPrototype% for environments that // don't natively support it. var IteratorPrototype = {}; IteratorPrototype[iteratorSymbol] = function () { return this; }; var getProto = Object.getPrototypeOf; var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); if (NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { // This environment has a native %IteratorPrototype%; use it instead // of the polyfill. IteratorPrototype = NativeIteratorPrototype; } var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype; GeneratorFunctionPrototype.constructor = GeneratorFunction; GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = "GeneratorFunction"; // Helper for defining the .next, .throw, and .return methods of the // Iterator interface in terms of a single ._invoke method. function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function(method) { prototype[method] = function(arg) { return this._invoke(method, arg); }; }); } exports.isGeneratorFunction = function(genFun) { var ctor = typeof genFun === "function" && genFun.constructor; return ctor ? ctor === GeneratorFunction || // For the native GeneratorFunction constructor, the best we can // do is to check its .name property. (ctor.displayName || ctor.name) === "GeneratorFunction" : false; }; exports.mark = function(genFun) { if (Object.setPrototypeOf) { Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); } else { genFun.__proto__ = GeneratorFunctionPrototype; if (!(toStringTagSymbol in genFun)) { genFun[toStringTagSymbol] = "GeneratorFunction"; } } genFun.prototype = Object.create(Gp); return genFun; }; // Within the body of any async function, `await x` is transformed to // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test // `hasOwn.call(value, "__await")` to determine if the yielded value is // meant to be awaited. exports.awrap = function(arg) { return { __await: arg }; }; function AsyncIterator(generator) { function invoke(method, arg, resolve, reject) { var record = tryCatch(generator[method], generator, arg); if (record.type === "throw") { reject(record.arg); } else { var result = record.arg; var value = result.value; if (value && typeof value === "object" && hasOwn.call(value, "__await")) { return Promise.resolve(value.__await).then(function(value) { invoke("next", value, resolve, reject); }, function(err) { invoke("throw", err, resolve, reject); }); } return Promise.resolve(value).then(function(unwrapped) { // When a yielded Promise is resolved, its final value becomes // the .value of the Promise<{value,done}> result for the // current iteration. result.value = unwrapped; resolve(result); }, function(error) { // If a rejected Promise was yielded, throw the rejection back // into the async generator function so it can be handled there. return invoke("throw", error, resolve, reject); }); } } var previousPromise; function enqueue(method, arg) { function callInvokeWithMethodAndArg() { return new Promise(function(resolve, reject) { invoke(method, arg, resolve, reject); }); } return previousPromise = // If enqueue has been called before, then we want to wait until // all previous Promises have been resolved before calling invoke, // so that results are always delivered in the correct order. If // enqueue has not been called before, then it is important to // call invoke immediately, without waiting on a callback to fire, // so that the async generator function has the opportunity to do // any necessary setup in a predictable way. This predictability // is why the Promise constructor synchronously invokes its // executor callback, and why async functions synchronously // execute code before the first await. Since we implement simple // async functions in terms of async generators, it is especially // important to get this right, even though it requires care. previousPromise ? previousPromise.then( callInvokeWithMethodAndArg, // Avoid propagating failures to Promises returned by later // invocations of the iterator. callInvokeWithMethodAndArg ) : callInvokeWithMethodAndArg(); } // Define the unified helper method that is used to implement .next, // .throw, and .return (see defineIteratorMethods). this._invoke = enqueue; } defineIteratorMethods(AsyncIterator.prototype); AsyncIterator.prototype[asyncIteratorSymbol] = function () { return this; }; exports.AsyncIterator = AsyncIterator; // Note that simple async functions are implemented on top of // AsyncIterator objects; they just return a Promise for the value of // the final result produced by the iterator. exports.async = function(innerFn, outerFn, self, tryLocsList) { var iter = new AsyncIterator( wrap(innerFn, outerFn, self, tryLocsList) ); return exports.isGeneratorFunction(outerFn) ? iter // If outerFn is a generator, return the full iterator. : iter.next().then(function(result) { return result.done ? result.value : iter.next(); }); }; function makeInvokeMethod(innerFn, self, context) { var state = GenStateSuspendedStart; return function invoke(method, arg) { if (state === GenStateExecuting) { throw new Error("Generator is already running"); } if (state === GenStateCompleted) { if (method === "throw") { throw arg; } // Be forgiving, per 25.3.3.3.3 of the spec: // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume return doneResult(); } context.method = method; context.arg = arg; while (true) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; } } if (context.method === "next") { // Setting context._sent for legacy support of Babel's // function.sent implementation. context.sent = context._sent = context.arg; } else if (context.method === "throw") { if (state === GenStateSuspendedStart) { state = GenStateCompleted; throw context.arg; } context.dispatchException(context.arg); } else if (context.method === "return") { context.abrupt("return", context.arg); } state = GenStateExecuting; var record = tryCatch(innerFn, self, context); if (record.type === "normal") { // If an exception is thrown from innerFn, we leave state === // GenStateExecuting and loop back for another invocation. state = context.done ? GenStateCompleted : GenStateSuspendedYield; if (record.arg === ContinueSentinel) { continue; } return { value: record.arg, done: context.done }; } else if (record.type === "throw") { state = GenStateCompleted; // Dispatch the exception by looping back around to the // context.dispatchException(context.arg) call above. context.method = "throw"; context.arg = record.arg; } } }; } // Call delegate.iterator[context.method](context.arg) and handle the // result, either by returning a { value, done } result from the // delegate iterator, or by modifying context.method and context.arg, // setting context.delegate to null, and returning the ContinueSentinel. function maybeInvokeDelegate(delegate, context) { var method = delegate.iterator[context.method]; if (method === undefined) { // A .throw or .return when the delegate iterator has no .throw // method always terminates the yield* loop. context.delegate = null; if (context.method === "throw") { // Note: ["return"] must be used for ES3 parsing compatibility. if (delegate.iterator["return"]) { // If the delegate iterator has a return method, give it a // chance to clean up. context.method = "return"; context.arg = undefined; maybeInvokeDelegate(delegate, context); if (context.method === "throw") { // If maybeInvokeDelegate(context) changed context.method from // "return" to "throw", let that override the TypeError below. return ContinueSentinel; } } context.method = "throw"; context.arg = new TypeError( "The iterator does not provide a 'throw' method"); } return ContinueSentinel; } var record = tryCatch(method, delegate.iterator, context.arg); if (record.type === "throw") { context.method = "throw"; context.arg = record.arg; context.delegate = null; return ContinueSentinel; } var info = record.arg; if (! info) { context.method = "throw"; context.arg = new TypeError("iterator result is not an object"); context.delegate = null; return ContinueSentinel; } if (info.done) { // Assign the result of the finished delegate to the temporary // variable specified by delegate.resultName (see delegateYield). context[delegate.resultName] = info.value; // Resume execution at the desired location (see delegateYield). context.next = delegate.nextLoc; // If context.method was "throw" but the delegate handled the // exception, let the outer generator proceed normally. If // context.method was "next", forget context.arg since it has been // "consumed" by the delegate iterator. If context.method was // "return", allow the original .return call to continue in the // outer generator. if (context.method !== "return") { context.method = "next"; context.arg = undefined; } } else { // Re-yield the result returned by the delegate method. return info; } // The delegate iterator is finished, so forget it and continue with // the outer generator. context.delegate = null; return ContinueSentinel; } // Define Generator.prototype.{next,throw,return} in terms of the // unified ._invoke helper method. defineIteratorMethods(Gp); Gp[toStringTagSymbol] = "Generator"; // A Generator should always return itself as the iterator object when the // @@iterator function is called on it. Some browsers' implementations of the // iterator prototype chain incorrectly implement this, causing the Generator // object to not be returned from this call. This ensures that doesn't happen. // See https://github.com/facebook/regenerator/issues/274 for more details. Gp[iteratorSymbol] = function() { return this; }; Gp.toString = function() { return "[object Generator]"; }; function pushTryEntry(locs) { var entry = { tryLoc: locs[0] }; if (1 in locs) { entry.catchLoc = locs[1]; } if (2 in locs) { entry.finallyLoc = locs[2]; entry.afterLoc = locs[3]; } this.tryEntries.push(entry); } function resetTryEntry(entry) { var record = entry.completion || {}; record.type = "normal"; delete record.arg; entry.completion = record; } function Context(tryLocsList) { // The root entry object (effectively a try statement without a catch // or a finally block) gives us a place to store values thrown from // locations where there is no enclosing try statement. this.tryEntries = [{ tryLoc: "root" }]; tryLocsList.forEach(pushTryEntry, this); this.reset(true); } exports.keys = function(object) { var keys = []; for (var key in object) { keys.push(key); } keys.reverse(); // Rather than returning an object with a next method, we keep // things simple and return the next function itself. return function next() { while (keys.length) { var key = keys.pop(); if (key in object) { next.value = key; next.done = false; return next; } } // To avoid creating an additional object, we just hang the .value // and .done properties off the next function object itself. This // also ensures that the minifier will not anonymize the function. next.done = true; return next; }; }; function values(iterable) { if (iterable) { var iteratorMethod = iterable[iteratorSymbol]; if (iteratorMethod) { return iteratorMethod.call(iterable); } if (typeof iterable.next === "function") { return iterable; } if (!isNaN(iterable.length)) { var i = -1, next = function next() { while (++i < iterable.length) { if (hasOwn.call(iterable, i)) { next.value = iterable[i]; next.done = false; return next; } } next.value = undefined; next.done = true; return next; }; return next.next = next; } } // Return an iterator with no values. return { next: doneResult }; } exports.values = values; function doneResult() { return { value: undefined, done: true }; } Context.prototype = { constructor: Context, reset: function(skipTempReset) { this.prev = 0; this.next = 0; // Resetting context._sent for legacy support of Babel's // function.sent implementation. this.sent = this._sent = undefined; this.done = false; this.delegate = null; this.method = "next"; this.arg = undefined; this.tryEntries.forEach(resetTryEntry); if (!skipTempReset) { for (var name in this) { // Not sure about the optimal order of these conditions: if (name.charAt(0) === "t" && hasOwn.call(this, name) && !isNaN(+name.slice(1))) { this[name] = undefined; } } } }, stop: function() { this.done = true; var rootEntry = this.tryEntries[0]; var rootRecord = rootEntry.completion; if (rootRecord.type === "throw") { throw rootRecord.arg; } return this.rval; }, dispatchException: function(exception) { if (this.done) { throw exception; } var context = this; function handle(loc, caught) { record.type = "throw"; record.arg = exception; context.next = loc; if (caught) { // If the dispatched exception was caught by a catch block, // then let that catch block handle the exception normally. context.method = "next"; context.arg = undefined; } return !! caught; } for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; var record = entry.completion; if (entry.tryLoc === "root") { // Exception thrown outside of any try block that could handle // it, so set the completion value of the entire function to // throw the exception. return handle("end"); } if (entry.tryLoc <= this.prev) { var hasCatch = hasOwn.call(entry, "catchLoc"); var hasFinally = hasOwn.call(entry, "finallyLoc"); if (hasCatch && hasFinally) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } else if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else if (hasCatch) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } } else if (hasFinally) { if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else { throw new Error("try statement without catch or finally"); } } } }, abrupt: function(type, arg) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { var finallyEntry = entry; break; } } if (finallyEntry && (type === "break" || type === "continue") && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc) { // Ignore the finally entry if control is not jumping to a // location outside the try/catch block. finallyEntry = null; } var record = finallyEntry ? finallyEntry.completion : {}; record.type = type; record.arg = arg; if (finallyEntry) { this.method = "next"; this.next = finallyEntry.finallyLoc; return ContinueSentinel; } return this.complete(record); }, complete: function(record, afterLoc) { if (record.type === "throw") { throw record.arg; } if (record.type === "break" || record.type === "continue") { this.next = record.arg; } else if (record.type === "return") { this.rval = this.arg = record.arg; this.method = "return"; this.next = "end"; } else if (record.type === "normal" && afterLoc) { this.next = afterLoc; } return ContinueSentinel; }, finish: function(finallyLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.finallyLoc === finallyLoc) { this.complete(entry.completion, entry.afterLoc); resetTryEntry(entry); return ContinueSentinel; } } }, "catch": function(tryLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc === tryLoc) { var record = entry.completion; if (record.type === "throw") { var thrown = record.arg; resetTryEntry(entry); } return thrown; } } // The context.catch method must only be called with a location // argument that corresponds to a known catch block. throw new Error("illegal catch attempt"); }, delegateYield: function(iterable, resultName, nextLoc) { this.delegate = { iterator: values(iterable), resultName: resultName, nextLoc: nextLoc }; if (this.method === "next") { // Deliberately forget the last sent value so that we don't // accidentally pass it on to the delegate. this.arg = undefined; } return ContinueSentinel; } }; // Regardless of whether this script is executing as a CommonJS module // or not, return the runtime object so that we can declare the variable // regeneratorRuntime in the outer scope, which allows this module to be // injected easily by `bin/regenerator --include-runtime script.js`. return exports; }( // If this script is executing as a CommonJS module, use module.exports // as the regeneratorRuntime namespace. Otherwise create a new empty // object. Either way, the resulting object will be used to initialize // the regeneratorRuntime variable at the top of this file. typeof module === "object" ? module.exports : {} )); [代码] 后端 [代码]const typeDefs = gql` # schema 下面是根类型,约定是 RootQuery 和 RootMutation schema { query: Query } # 定义具体的 Query 的结构 type Query { # 登陆接口 login(code: String!, encryptedData: String, iv: String): Login } type Login { token: String! userInfo: UserInfo } type UserInfo { nickName: String gender: String avatarUrl: String } `; const resolvers = { Query: { async login(parent, { code, encryptedData, iv }) { const { sessionKey, openId, unionId } = await wxService.code2Session(code); const userInfo = encryptedData && iv ? wxService.decryptData(sessionKey, encryptedData, iv) : { openId, unionId }; if (userInfo.nickName) { userService.createOrUpdateWxUser(userInfo); } const token = await userService.generateJwtToken(userInfo); return { token, userInfo }; }, }, }; [代码]
2019-04-21 - js异步编程
前言 我们都知道,JS是单线程执行的,天生异步。在开发的过程中会遇到很多异步的场景,只用回调来处理简单的异步逻辑,当然是可以,但是逻辑逐渐复杂起来,回调的处理方式显得力不从心。 接下来会介绍js中处理异步的方式,通过对比了解各自的原理以及优缺点,帮助我们更好的使用这些强大的异步处理方式。 回调 基本用法 回调函数作为参数传进方法中,在合适的时机被调用。 比如调用ajax,或是使用定时器: [代码] // ajax请求 $.ajax({ url: '/ajax/hdportal_h.jsp?cmd=xxx', error: function(err) { console.log(err) }, success: function(data) { console.log(data) } }) // 定时器的回调 setTimeout(function callback() { console.log('hi') }, 1000) [代码] 回调的问题 1. 回调地狱 过深的嵌套,形成回调地狱 使得代码难以阅读和调试 层层嵌套,代码间耦合严重,牵一发而动全身 2.信任缺失,错误处理无法保证 控制反转,回调函数的调用是在请求函数内部,无法保证回调函数一定会被正确调用,回调本身没有错误处理机制,需要额外设计。 可能存在以下问题: 调用回调过早 调用回调过晚 调用回调次数太多或者太少 未能把所需的参数成功传给你的回调函数 吞掉可能出现的错误或异常 Promise 基本用法 Promise对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败) 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。 [代码] new Promise((resovle, reject) => { setTimeout(() => { resovle('hello promise') }, 1000) }).then(res => { console.log(res) }).catch(err => { console.log(err) }) [代码] Promise与回调的区别 Promise 不是对回调的替代。 Promise 在回调代码和将要执行这个任务的异步代码之间提供了一种可靠的中间机制来管理回调 Promise 并没有完全摆脱回调。它们只是改变了传递回调的位置。我们并不是把回调传递给处理函数,而是从处理函数得到Promise,然后把回调传给这个Promise Promise 保证了行为的一致性,使其变得可信任,我们传递的回调会被正确的执行 Promise如何解决信任缺失问题? 调用时机上,不会调用过早,也不会调用过晚 根据PromiseA+规范,then中的回调会在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。 这个事件队列可以采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。 所以提供给then的回调也总会在JavaScript事件队列的当前运行完成后,再被调用,即异步调用。 [代码] var p = Promise.resolve('p'); console.log('A'); p.then(function () { p.then(function () { console.log('E'); }); console.log('C'); }) .then(function () { console.log('D'); }); console.log('B'); [代码] 运行这段代码,会依次打印出ABCED 这里要注意两个点: 会先执行同步代码,再执行then中的代码 then执行回调时,打印D的代码晚于打印E的代码 调用次数上,不会出现回调未调用,也不会出现调用次数太多或者太少 一个Promise注册了一个成功回调和拒绝回调,那么Promise在决议的时候总会调用其中一个。 即使是在决议后调用then注册的回调函数,也会被正确调用,所以不会出现回调未调用的情况。 Promise只能被决议一次。如果处于多种原因,Promise创建代码试图调用多次resolve(…)或reject(…),或者试图两者都调用,那么这个Promise将只会接受第一次决议,忽略任何后续调用,所以调用次数不会太多也不会太少。 错误处理上,不会吞掉可能出现的错误或异常 如果在Promise的创建过程中或在查看其决议结果的过程中的任何时间点上,出现了一个JavaScript异常错误,比如一个TypeError或ReferenceError,这个异常都会被捕捉,并且会使这个Promise被拒绝。 [代码] var p = new Promise(function (resolve, reject) { foo.bar(); // foo未定义 resolve(2); }); p.then(function (data) { console.log(data); // 永远也不会到达这里 }, function (err) { console.log('出错了', err); // err将会是一个TypeError异常对象来自foo.bar()这一行 }); [代码] Promise中的then then方法的设计是promise中最重要的部分之一,可以看promise/A+规范中对then方法的描述 then方法会返回一个新的promise,因此可以链式调用,下面的代码会打印出6 [代码] var p = Promise.resolve(0); p.then(function (data) { return 1; }).then(function (data) { return data + 2; }).then(function (data) { return data + 3; }).then(function (data) { console.log(data); }); [代码] 如果在then中主动返回一个promise,依旧会返回一个新的promise,只是这个promise的状态“跟随”主动返回的pormise [代码] var p1 = new Promise(function (resolve, reject) { resolve('p1'); }); var p2 = new Promise(function (resolve, reject) { resolve('p2'); }); var p3 = p2.then(function (data) { return p1; }); console.log(p3 === p1); // false p3.then(function (data) { console.log(data); // p1 }); [代码] 静态方法 Promise.resolve() Promise.resolve(value)方法返回一个以给定值解析后的 Promise 对象。 但如果这个值是个 thenable(即带有 then 方法),返回的 promise 会“跟随”这个 thenable的对象,采用它的最终状态;否则以该值为成功状态返回 promise 对象。 Promise.reject() Promise.reject(reason)方法返回一个用reason拒绝的Promise。 [代码] // 以下两个 promise 是等价的 var p1 = new Promise( (resolve,reject) => { resolve( "Oops" ); }); var p2 = Promise.resolve( "Oops" ); var p1 = new Promise( (resolve,reject) => { reject( "Oops" ); }); var p2 = Promise.reject( "Oops" ); [代码] Promise.all() Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例 [代码] const p = Promise.all([p1, p2, p3]); p.then(function (posts) { // ... }).catch(function(reason){ // ... }); [代码] p的状态由p1、p2、p3决定,分成两种情况。 (1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。 (2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。 Promise.race() Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。 [代码] const p = Promise.race([p1, p2, p3]); p.then(function (posts) { // ... }).catch(function(reason){ // ... }); [代码] 只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数 Generator 名词解释 迭代器 (Iterator) 迭代器是一种对象,它具有一些专门为迭代过程设计的专有接口,所有迭代器对象都有一个 next 方法,每次调用都返回一个结果对象。 结果对象有两个属性,一个是 value,表示下一个将要返回的值;另一个是 done,它是一个布尔类型的值,当没有更多可返回数据时返回 true。 迭代器还会保存一个内部指针,用来指向当前集合中值的位置,每调用一次 next() 方法,都会返回下一个可用的值。 可迭代对象 (Iterable) 可迭代对象具有 Symbol.iterator 属性,是一种与迭代器密切相关的对象。 Symbol.iterator 通过指定的函数可以返回一个作用于附属对象的迭代器。 在 ECMCScript 6 中,所有的集合对象(数组、Set、及 Map 集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。 生成器 (Generator) 生成器是一种返回迭代器的函数,通过 function 关键字后的 * 号来表示。 此外,由于生成器会默认为 Symbol.iterator 属性赋值,因此所有通过生成器创建的迭代器都是可迭代对象。 for-of 循环 for-of 循环每执行一次都会调用可迭代对象的迭代器接口的 next() 方法,并将迭代器返回的结果对象的 value 属性储存在一个变量中,循环将持续执行这一过程直到返回对象的属性值为 true。 生成器的一般使用形式 [代码] function *foo() { var x = yield 2 var y = x * (yield x + 1) console.log( x, y ) return x + y } var it = foo() it.next() // {value: 2, done: false} it.next(3) // {value: 4, done: false} it.next(3) // 3 9, {value: 12, done: true} [代码] 遍历器对象的next方法的运行逻辑如下: (1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。 (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。 (3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。 (4)如果该函数没有return语句,则返回的对象的value属性值为undefined。 需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。 异步迭代生成器 [代码] function foo() { setTimeout(() => { it.next('success') // 恢复*main() // it.throw('error') // 向*main()抛出一个错误 }, 2000); } function *main() { try { var data = yield foo() console.log(data) } catch(e) { console.log(e) } } var it = main() it.next() // 这里启动! [代码] 本例中我们在 *main() 中发起 foo() 请求,之后暂停;又在 foo() 中相应数据恢复 *mian() 继续运行,并将 foo() 的运行结果通过 next() 传递出来。 我们在生成器内部有了看似完全同步的代码(除了 yield 关键字本身),但隐藏在背后的是,在 foo(…)内的运行可以完全异步。并且在异步代码中实现看似同步的错误处理(通过try…catch)在可读性和合理性方面也都是一个巨大的进步。 Generator + Promise 通过promise来管理异步流程 [代码] function foo() { return new Promise(function(resolve, reject){ setTimeout(() => { resolve('fai'); }, 2000); }); } function *main() { try { var data = yield foo() console.log(data) } catch(e) { console.error(e) } } var it = main(); var p = it.next().value; // p 的值是 foo() // 等待 promise p 决议 p.then( function(data) { it.next(data); // 将 data 赋值给 yield }, function(err) { it.throw(err); } ) [代码] *mian() 中执行 foo() 发起请求,返回promise 根据promise 决议结果,根据结果选择继续运行迭代器或抛出错误 如何执行有多处yield的Generator 函数? [代码] function foo(name) { return new Promise(function(resolve, reject){ setTimeout(() => { resolve('hello ' + name); }, 2000); }); } var gen = function* (){ var r1 = yield foo('jarvis'); var r2 = yield foo('hth'); console.log(r1); console.log(r2); }; var g = gen(); // 手动执行 g.next().value.then(function(data){ g.next(data).value.then(function(data){ g.next(data); }); }); [代码] 手动执行的方式,其实就是用then方法,层层添加回调函数。理解了这一点,就可以写出一个自动执行器 自动执行Generator 函数 [代码] function foo(name) { return new Promise(function(resolve, reject){ setTimeout(() => { resolve('hello ' + name); }, 2000); }); } var gen = function* (){ var r1 = yield foo('jarvis'); var r2 = yield foo('hth'); console.log(r1); console.log(r2); }; function run(gen){ var g = gen(); function next(data){ var result = g.next(data); if (result.done) return result.value; result.value.then(function(data){ next(data); }); } next(); } run(gen); [代码] 只要保证yield后面总是返回promise,就能用run函数自动执行Generator 函数 Async/Await async 函数的一般使用形式 async 函数是什么? 其实就是 promise+自动执行的Generator 函数的语法糖。类似于我们上面的实现 [代码] function foo(p) { return fetch('http://my.data?p=' + p) } async function main(p) { try { var data = await foo(p) return data } catch(e) { console.error(e) } } main(1).then(data => console.log(data)) [代码] 与 Generator 函数不同是,* 变成了async、yeild变成了await,同时我们也不用再定义 run(…) 函数来实现 Promise 与 Generator 的结合。 async 函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句,并且最终返回一个 Promise 对象。 正常情况下,await 命令后面是一个 Promise 对象。如果不是,会被转成一个立即 resolve 的 Promise 对象。 await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到。 async 函数的使用注意点 前面已经说过,await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。 多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。 await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。 [代码] //getFoo 与 getBar 是两个互相独立、互不依赖的异步操作 // 错误写法 let foo = await getFoo(); let bar = await getBar(); // 正确写法一 let [foo, bar] = await Promise.all([getFoo(), getBar()]); // 正确写法二 let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise; [代码] async 函数比Promise好在哪? 类同步写法,使得在写复杂逻辑时,可以用一种顺序的方式来书写,大大降低了理解的难度。 错误处理上,可以用try catch来捕获,同时处理同步和异步错误。 总结 JavaScript异步编程的发展历程有以下四个阶段: 回调函数: 有两个问题,回调地狱和信任缺失,回调地狱的坏处主要是代码阅读性和可维护性差,同时不好对异步逻辑进行封装。信任缺失主要体现在调用的时机,调用的次数,对异常的处理上缺乏一致性。 Promise 基于PromiseA+规范的实现解决了控制反转带来的信任问题。 Generator 使用生成器函数Generator,我们得以用同步的方式来书写异步的代码,解决了顺序性的问题,这是一种重大的突破。但是使用比较繁琐,需要手动去调用next(…)去控制流程和传参。 Async/Await Async/Await结合了Promise和Generator,并实现了自动执行生成器函数逻辑。使得使用者通添加少量关键字就可以用同步的方式书写异步代码,大大提高了开发效率和代码可维护性。 可以看到,目前Async/Await方式可以说是处理异步的终极解决方案,在项目中应该优先使用这种方式。
2019-06-11 - SVG动画教程
SVG动画能够实现和CSS3动画类似的效果,除此之外还能够实现强大的路径动画,也就是使物体按照一定的路径移动。 从语法上看CSS代码是样式代码,而SVG动画,是SVG中的一些动画标签。 SVG 动画元素介绍 svg可以设置动画的元素有5个,分别为 <set> <animate> <animateColor> <animateTransform> <animateMotion> 接下来我们具体来看看每一个元素的作用 set set用于设置属性值,可以用于实现延迟设置功能,也就是可以在特定时间之后修改某个属性值。 [代码]<svg height="400" width="400"> <rect width="90" height="90" fill="yellow" > <set attributeName="x" attributeType="XML" to="60" begin="3s" /> </rect> </svg> [代码] 这里的attributeName是定义要设置的属性,attributeType是定义要设置属性的类型,to为定义的值,begin为延迟时间。后面会详细讲解。 animate 实现基础动画,跟css3中的animation类似。 [代码]<svg height="400" width="400"> <rect width="90" height="90" fill="yellow" > <animate attributeName="x" from="160" to="60" begin="0s" dur="3s"/> </rect> </svg> [代码] animateColor 用于设置颜色的动画,不过颜色动画使用animate也可以实现,所以这个属性已经被废弃。 animateTransform 用于实现样式过渡动画,类似于css3中的transition属性 [代码]<svg width="320" height="320" xmlns="http://www.w3.org/2000/svg"> <rect width="90" height="90" fill="yellow" > <animateTransform attributeName="transform" begin="0s" dur="3s" type="rotate" from="1" to="90" repeatCount="indefinite"/> </rect> </svg> [代码] animateMotion animateMotion 元素可以用于定义SVG路径动画。如下例子(例子中的path与动画无关,只是用于标示) [代码]<svg width="360" height="200" xmlns="http://www.w3.org/2000/svg"> <rect width="20" height="20" fill="red" > <animateMotion path="M0,0 Q50,60 80,140 T340,100" begin="0s" dur="20s" repeatCount="indefinite"/> </rect> <path xmlns="http://www.w3.org/2000/svg" d="M0,0 Q50,60 80,140 T340,100" id="svg_13" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="none"/> </svg> [代码] 效果如下 [图片] 不过这个矩形不管运动到哪都是水平的,我们可以设置成文本照着path的方向做旋转 [代码]<svg width="360" height="200" xmlns="http://www.w3.org/2000/svg"> <rect width="20" height="20" fill="red" > <animateMotion path="M0,0 Q50,60 80,140 T340,100" begin="0s" dur="20s" rotate="auto" repeatCount="indefinite"/> </rect> <path xmlns="http://www.w3.org/2000/svg" d="M0,0 Q50,60 80,140 T340,100" id="svg_13" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="none"/> </svg> [代码] [图片] 自由组合 自由组合也就是设置多个样式的动画,比方说同时设置位置和透明度的变化。只需设置多个animate即可 [代码]<svg width="320" height="320" xmlns="http://www.w3.org/2000/svg"> <rect width="90" height="90" fill="yellow" > <animate attributeName="x" from="160" to="60" begin="0s" dur="3s" repeatCount="indefinite" /> <animate attributeName="opacity" from="1" to="0" begin="0s" dur="3s" repeatCount="indefinite" /> </rect> </svg> [代码] [图片] SVG 动画标签参数详解 前面章节中,我们了解了SVG具有四种主要的动画标签<set>,<animate>,<animateTranform>,<animateMotion>。 这一章主要讲其中参数的作用。 1、attributeName = <attributeName> 要变化的元素属性名称,可以是SVG标签上的属性,如font-size,width,height等,也可以是CSS属性,如opacity这些。这个属性跟下面的attributeType 一起使用。 2、attributeType=“CSS | XML | auto” attributeType支持三个固定参数"CSS | XML | auto",用来表明定义在attributeName上面的属性值。比如我们定义的属性是属于SVG标签上的属性,那么直接设置attributeType=‘xml’,如果是设置css属性,则设置type值为css。auto为默认值,自动判别attributeName的属性是属于XML还是CSS(实际上是先当成CSS处理,如果发现不认识,直接XML类别处理)。因此,如果你不确信某属性是XML类别还是CSS类别的时候,我的建议是不设置attributeType值,直接让浏览器自己去判断,几乎无差错。 之所以需要这个attributeType,是因为有些属性,其实即是属于XML也是属于CSS,因此在设置的时候就需要标明一些属性的类别。 3、from,to,by,values 上面4个属性是一个系列的,用于表示动画执行过程的状态 from = “value” 动画的起始值。 to = “value” 指定动画的结束值。 by = “value” 动画的相对变化值。 values = “list” 用分号分隔的一个或多个值,可以看出是动画的多个关键值点。 from, to, by, values相互之间存在有一些制约关系。需要满足以下一些规则: 如果动画的起始值与元素的默认值是一样的,from参数可以省略。 to,by两个参数至少需要有一个出现(不考虑values)。否则动画效果没有。to表示绝对值,by表示相对值。 如果to,by同时出现,则by打酱油,只识别to。 values可以设置多个动画节点,不同于to/by只能设置单个动画节点,当values值有效设置了之后,from, to, by 的值都会被忽略。 [代码]<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"> <text font-family="microsoft yahei" font-size="120" y="150" x="160">马 <animate attributeName="x" values="160;40;160" dur="3s" repeatCount="indefinite" /> </text> </svg> [代码] 总结一下,可以设置的动画组合为from-to动画、from-by动画、to动画、by动画以及values动画。 4. begin, end begin是指动画开始的时间。跟CSS3中的-delta有点像,但begin具备的功能比-delta多很多。 begin可以设置的值: [代码]1、具体时间值 'h' | 'min' | 's' | 'ms'这些,默认单位是's' 2、偏移值,'+/-',应该指相对于,某个操作的值,如begin="x.end-1s", 3、基于另一个动画的值,如某一个动画执行完成后开始执行,如begin="x.end" 4、与事件关联的值,如点击时开始执行动画begin="circle.click" 5、与某个动画执行次数相关的,比如某个元素执行2次后开始执行begin="x.repeat(2)" 6、与键盘事件相关的,比如按下某个键开始执行 begin="accessKey(s)" [代码] [代码]<svg width="320" height="200" xmlns="http://www.w3.org/2000/svg"> <text font-family="microsoft yahei" font-size="120" y="160" x="160">马 <animate attributeName="x" to="60" begin="accessKey(s)" dur="3s" repeatCount="indefinite" /> </text> </svg> [代码] end 个属性跟begin相对,属性值的有效类型一样。 5. dur 动画执行的时长,可以设置的值为常规的时间值或者 ‘indefinite’。设置’indefinite’跟没有设置动画是一个意思。 6. calcMode, keyTimes, keySplines 用于设置动画执行的节奏和各个阶段执行的快慢,类似于CSS3中的-timing-function。 1、calcMode 用于定义动画执行的节奏,也就是在规定时间内是如何完成这一整套动画的。calcMode属性支持4个值分别为:discrete,linear,paced,spline。 discrete:不连续的运动,就是从一个状态直接跳到另一个状态下,如从from状态直接跳到to状态 linear:线性运动,animateMotion元素以外元素的calcMode默认值。动画从头到尾的速率都是一致的。每个动画阶段的执行时间都是一致。 paced :线性运动,动画从头到尾执行速度都是一样,如果设置了paced,keyTimes无效。 spline : 属于可以自定义执行速度,类似于-timing-function: cubic-bezier()这一CSS属性。我们可以通过keyTimes和keySplines属性来定义各个动画的执行效果。 [代码]<svg version="1.1" xmlns="http://www.w3.org/2000/svg"> <rect x="10" y="10" width="50" height="50" fill="blue"> <animate xlink:href="#ant" attributeName="x" calcMode="discrete" values="10;30;150" dur="4s"/> </rect> <rect x="10" y="60" width="50" height="50" fill="blue"> <animate xlink:href="#ant" attributeName="x" calcMode="linear" values="10;30;150" dur="4s" /> </rect> <rect x="10" y="110" width="50" height="50" fill="blue"> <animate xlink:href="#ant" attributeName="x" calcMode="paced" values="10;30;150" dur="4s"/> </rect> <rect x="10" y="160" width="50" height="50" fill="blue"> <animate xlink:href="#ant" attributeName="x" calcMode="spline" values="10;30;150" keyTimes="0; .8; 1" dur="4s" /> </rect> </svg> [代码] 2、keyTimes keyTimes用于限定各个阶段的执行时间,是一串0-1的浮点数字组,每个数值的有效取值范围为0-1,代表每一个动画阶段必须在那个时间点执行完成。 keyTimes的数字组长度要和values长度一直,如果是to/by动画就只有两个值。否则会当做无效处理。 [代码]<svg version="1.1" xmlns="http://www.w3.org/2000/svg"> <rect x="10" y="10" width="50" height="50" fill="blue"> <animate xlink:href="#ant" attributeName="x" calcMode="spline" values="10;90;150" keyTimes="0; .8; 1" dur="4s"/> </rect> <rect x="10" y="60" width="50" height="50" fill="blue"> <animate xlink:href="#ant" attributeName="x" calcMode="spline" values="10;90;150" keyTimes="0; .2; 1" dur="4s"/> </rect> </svg> [代码] 3、keySplines 当calcMode的值为spline时才有效,对应的值为一组三次贝塞尔曲线的控制点,默认为(0,0,1,1)。这个值完全类似于-timing-function: cubic-bezier()的这个CSS属性值。keySplines可以设置多组,每一组用";"隔开。 每一组代表在这个动画阶段中执行的速率 [代码]<svg version="1.1" xmlns="http://www.w3.org/2000/svg"> <rect x="10" y="10" width="50" height="50" fill="blue"> <animate xlink:href="#ant" attributeName="x" calcMode="spline" values="10;90;150" keyTimes="0; .8; 1" keySplines="0.42 0 1 1;0 0 0.59 1;0.42 0 1 1;" dur="4s"/> </rect> </svg> [代码] 7. repeatCount, repeatDur repeatCount表示动画执行次数,可以是合法数值或者"indefinite"。 repeatDur定义重复动画的总时间。可以是普通时间值或者"indefinite"。 8. fill fill表示动画间隙的填充方式。支持参数有:freeze | remove。 remove :是默认值,表示动画结束直接回到开始的地方。 freeze :表示动画结束后元素固定在结束的位置。 9. accumulate, additive accumulate是累积的意思。支持参数有:none | sum. 默认值是none。如果值是sum表示动画结束时候的位置作为下次动画的起始位置。 additive控制动画是否附加。支持参数有:replace | sum. 默认值是replace。如果值是sum表示动画的基础知识会附加到其他低优先级的动画上, [代码]<svg version="1.1" xmlns="http://www.w3.org/2000/svg"> <rect x="10" y="10" width="50" height="50" fill="blue"> <animateTransform attributeName="transform" type="scale" from="1" to="3" dur="10s" repeatCount="indefinite" additive="sum"/> <animateTransform attributeName="transform" type="rotate" from="0 30 20" to="360 30 20" dur="10s" fill="freeze" repeatCount="indefinite" additive="sum"/>; </rect> </svg> [代码] 这里,两个动画同时都是transform,都要使用一个type属性,好在这个例子additive="sum"是累加的而不是replace替换。于是,我们就可以是实现一边旋转一边放大的效果 10. restart 用于设置动画是否可以重复执行。可设置的值为’always | whenNotActive | never’。 always是默认值,表示总是,也就是没触发一次动画执行一次。 whenNotActive:表示动画正在进行的时候,是不能重启动画的。 never:表示动画不能被重新触发。 11. min, max 表示动画执行的最短和最长时间。 动画暂停裕播放 SVG 提供一些js接口可以用于控制动画 // 暂停 svg.pauseAnimations(); // 重启动 svg.unpauseAnimations(); 参考文章 超级强大的SVG SMIL animation动画详解
2019-09-12 - 搭建一个https网站的全过程
概述:本着学习的目的,做了这个分享。自己切切实实的做完了整个流程,发现其中的坑也是蛮多的,当然自己的收获对应也是蛮多的。写下这个流程一方面为了加深自己的印象、可以在将来回顾一下,另一方面也是为了给有需要的人提供帮助~ 一、服务器准备 [代码]为了演示方便,我购买了一台腾讯云服务器 [代码] [图片] 安装的操作系统是centos 二、域名准备 1、域名注册 可以从万网上进行注册:https://wanwang.aliyun.com/ 2、域名备案 备案流程略复杂,这里只列了一个步骤简介 入口:https://beian.aliyun.com/ [图片] 域名的备案时间较长,建议大家提前准备起来。 3、域名解析 域名备案通过之后,为我们的网站准备一个子域名,入口:https://dns.console.aliyun.com/?spm=a2c1d.8251892.aliyun_sidebar.daliyun_sidebar_dns.37575b76kNuXEO#/dns/domainList [图片] 点击上图的解析设置 [图片] 将记录值填写我们刚刚买的服务器的公网ip 三、申请ssl证书 我是从腾讯提供的证书服务里申请的,腾讯申请入口 https://console.cloud.tencent.com/ssl 点击“申请免费证书” [图片] 这里选择了手动Dns验证 [图片] 申请完成后会有个表格,是说明如何Dns校验的,要求域名下添加一条解析记录 [图片] Dns校验 首先,在域名下添加一条txt记录 [图片] 然后,单机自助诊断旁边的“查询” [图片] 下载证书 [图片] 点击图中的下载即可 四、服务器安装软件 我用ssh连接服务器 登录服务器:ssh root@212.129.*.* , 然后输入密码(从腾讯云管理后台进行密码的设置和获取)进入服务器,然后安装软件,如下~ 1、git:用于代码管理 2、nvm:用于管理node版本 3、node:用于启动web服务 4、pm2:用于守护node进程 安装git [代码]yum install git -y [代码] 下载nvm [代码]git clone git://github.com/creationix/nvm.git ~/nvm [代码] 设置nvm 自动运行; [代码]echo "source ~/nvm/nvm.sh" >> ~/.bashrc source ~/.bashrc [代码] 查询node版本 [代码]nvm list-remote [代码] 安装node.js [代码]nvm install v10.16.3 [代码] 使用nodejs [代码]nvm use v10.16.3 [代码] 使用npm安装pm2 [代码]npm install -g pm2 [代码] 五、下载一个web项目 & 使用 pm2 启动 这里我使用了自己的一个github上的项目:https://github.com/myronliu/ssr-koa-react-redux.git 1、git clone https://github.com/myronliu/ssr-koa-react-redux.git 2、cd ssr-koa-react-redux 3、npm install 4、pm2 start server.js 服务启动之后如下图: [图片] 现在我们可以用IP(服务器的公网ip) + 端口号来进行访问了 [图片] 六、安装nginx 步骤 1: 添加 yum 源 Nginx 不在默认的 yum 源中,可以使用 epel 或者官网的 yum 源,本例使用官网的 yum 源。 sudo rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm 安装完 yum 源之后,可以查看一下。 sudo yum repolist 步骤 2: 安装 yum 安装 Nginx,非常简单,一条命令: sudo yum install nginx 常用nginx命令介绍 设置开机启动 $ sudo systemctl enable nginx 启动服务 $ sudo systemctl start nginx 停止服务 $ sudo systemctl restart nginx 重新加载,因为一般重新配置之后,不希望重启服务,这时可以使用重新加载。 $ sudo systemctl reload nginx 步骤3: 打开防火墙端口 sudo firewall-cmd --permanent --zone=public --add-service=http sudo firewall-cmd --permanent --zone=public --add-service=https sudo firewall-cmd --reload [图片] 步骤4: 配置nginx 找到并查看配置文件/etc/nginx/nginx.conf [图片] 步骤5: 配置我们自己的conf [图片] 编写为 [图片] 步骤6: 现在可以使用域名访问我们的网站了,目前是http协议的 [图片] 七、安装证书 将第三步下载的证书里选择Nginx下的两个文件上传到服务器的/etc/nginx/conf.d文件夹下 [图片] Mac上利用scp上传 scp /Users/xxx/Downloads/todo.xxx.com/Nginx/1_todo.xx.com_bundle.crt root@212.129.*.*:/etc/nginx/conf.d [图片] 八、创建https的conf文件 进入到/etc/nginx/conf.d下,执行命令:touch https.conf && vi https.conf [图片] 测试nginx配置 & 重启 [图片] 九、访问https协议的站点 [图片] 备注: 如有问题,欢迎指出! 如有侵权,联系删除~
2019-09-24 - [拆弹时刻]小程序canvas生成海报(二)--优化方案
[图片] 海报生成速度缓慢问题的优化 微信头像在app.js中预先加载缓存 多图片异步加载 流程中断处理 二次授权失败的处理 请求或者下载图片失败处理 保存图片可被压缩 海报生成速度缓慢问题的优化 原因分析: 主要的时间消耗在于getImageInfo网络请求获取头像和下载图片获得临时地址的过程,可以看到海报中有3张图片(微信头像、主图、动态二维码(对应不同新闻的ID))需要下载,接下来主要就是对这3张图的优化 微信头像在app.js中预先加载缓存 [代码]//app.js //可以在app.js中使用小程序默认的全局变量,将头像在加载的时候预先缓存 App({ onLaunch: function () { // 获取用户信息 wx.getSetting({ success: res => { if (res.authSetting['scope.userInfo']) { // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 wx.getUserInfo({ success: res => { this.globalData.userInfo = res.userInfo; //从返回值中获取微信头像地址 let WxHeader = res.userInfo.avatarUrl; wx.getImageInfo({ src: WxHeader,//下载微信头像获得临时地址 success: res => { //将头像缓存在全局变量里 this.globalData.avatarUrlTempPath = res.path; }, fail: res => { //失败回调 } }); } }) } } }) }, globalData: { userInfo: null, //如果用户没有授权,无法在加载小程序的时候获取头像,就使用默认头像 avatarUrlTempPath: "./images/defaultHeader.jpg" } }) [代码] 大致思路是: 加载App.js的时候 ==> getSetting(判断是否授权) ==> getUserInfo(获取头像) ==> getImageInfo(生成临时地址) 将需要的网络请求在加载小程序的时候就异步完成,提前将临时地址缓存在全局变量globalData中,这样当用户进入新闻页面,点击生成海报的时候就不需要在请求微信头像,缩短了不少时间。 注意: 如果用户一开始没有微信授权,生成海报时又必须要用户头像不能使用默认的话,那就只能老老实实走之前的流程了。 多图片异步加载 [代码]let num = 0; //下载图片计数器,假设一共三张图片 //下载图片1 wx.getImageInfo({ src: image_1, success: function (res) { //判断是否是最后一张图 if (num >= 2) { console.log("图片全部下载完毕,可以绘制海报") } else { //如果不是最后一张图则+1,继续 num++; } }, fail: function (res) { //失败回调 } }); //下载图片2 wx.getImageInfo({ src: image_2, success: function (res) { //判断是否是最后一张图 if (num >= 2) { console.log("图片全部下载完毕,可以绘制海报") } else { //如果不是最后一张图则+1,继续 num++; } } }); ...... [代码] 这里智库君一开始是使用promise的同步办法,但是发现3张图片阻塞严重,如果一张图片下载过慢,就会影响整个海报生成时间,所以可以改为添加计数器判断的异步方法。 当海报生成需要多张图片的时候,完全可以异步的方式加载他们,通过计数器判断是否是最后一张。 流程中断处理 [图片] 从图中可以看出,整个海报生成过程有二次授权:用户信息授权获取头像和保存相册授权,非常可能因为用户的误点或者拒绝而导致流程中断。 主要分为二种情况: 需要的图片没有拿到,我们可以采取使用默认图片的方式替代。 保存相册授权被拒绝,我们可以提示用户“截图保存”,由于当前版本6.7.2+的**wx.openSetting()**被限制(无法直接被调用),如果必须要相册权限,我们可以通过showModal触发。 API/组件名称 终端类型 微信版本 触发方法 openSetting 6.7.2 2.3.0 showModal [代码]// 关于 openSetting 的调用方法 wx.showModal({ title: '相册权限', content: '需要你提供保存相册权限', success: function (res) { if (res.confirm) { wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取 相册 权限成功,给出再次点击图片保存到相册的提示。'); } else { console.log('获取 相册 权限失败,给出不给权限就无法正常使用的提示') } } }) } } }) //获取相册权限的流程处理 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, //canvasToTempFilePath API生成的临时地址 success: function (data) { console.log("提示图片保存成功"); }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") //调用上面说到的方法 wx.openSetting } else { console.log("提示:请截屏保存分享"); } }, complete(res) { console.log(res); } }) [代码] [图片] 保存图片可被压缩 小程序官方提供了一个API可以设置用户保存图片的质量,仅针对JPG。目前不完全确定:压缩会不会导致额外的性能开销而延长保存时间,自己测试下来 100%、80%、60% 保存时间上没有明显区别。 属性 默认值 说明 最低版本 quality 1.0 图片的质量,取值范围为 (0, 1] 1.7.0 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'canvasId', quality:0.8, //设置JPG保存质量 80% success: res => { }, fail:res => { } }, this) [代码] 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/wx.canvasToTempFilePath.html?search-key=canvasToTempFilePath [图片] [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 如果智酷君的分享能够帮助到你,或者想持续获得最新的全栈攻略 可以搜索公众号 Geek_Club 或者 智酷方程式 扫描二维码关注公众号哟👇👇👇 [图片]
2019-06-11 - 小程序导出数据到excel表,借助云开发后台实现excel数据的保存
我们在做小程序开发的过程中,可能会有这样的需求,就是把我们云数据库里的数据批量导出到excel表里。如果直接在小程序里写是实现不了的,所以我们要借助小程序的云开发功能了。这里需要用到云函数,云存储和云数据库。可以说通过这一个例子,把我们微信小程序云开发相关的知识都用到了。 老规矩,先看效果图 [图片] 上图就是我们保存用户数据到excel生成的excel文件。 实现思路 1,创建云函数 2,在云函数里读取云数据库里的数据 3,安装node-xlsx类库(node类库) 4,把云数据库里读取到的数据存到excel里 5,把excel存到云存储里并返回对应的云文件地址 6,通过云文件地址下载excel文件 一,创建excel云函数 关于云函数的创建,我这里不多说了。如果你连云函数的创建都不知道,建议你去小程序云开发官方文档去看看。或者看下我录制的云开发入门的视频:https://edu.csdn.net/course/detail/9604 创建云函数时有两点需要注意的,给大家说下 1,一定要把app.js里的环境id换成你自己的 [图片] 2,你的云函数目录要选择你对应的云开发环境(通常这里默认选中的) 不过你这里的云开发环境要和你app.js里的保持一致 [图片] 二,读取云数据库里的数据 我们第一步创建好云函数以后,可以先在云函数里读取我们的云数据库里的数据。 1,先看下我们云数据库里的数据 [图片] 2,编写云函数,读取云数据库里的数据(一定要记得部署云函数) [图片] 3,成功读取到数据 [图片] 把读取user数据表的完整代码给大家贴出来。 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: "test-vsbkm" }) // 云函数入口函数 exports.main = async(event, context) => { return await cloud.database().collection('users').get(); } [代码] 三,安装生成excel文件的类库 node-xlsx 通过上面第二步可以看到我们已经成功的拿到需要保存到excel的源数据,我们接下来要做的就是把数据保存到excel 1,安装node-xlsx类库 [图片] 这一步需要我们事先安装node,因为我们要用到npm命令,通过命令行 [代码]npm install node-xlsx [代码] [图片] 可以看出我们安装完成以后,多了一个package-lock.json的文件 [图片] 四,编写把数据保存到excel的代码, 下图是我们的核心代码 [图片] 这里的数据是我们查询的users表的数据,然后通过下面代码遍历数组,然后存入excel。这里需要注意我们的id,name,weixin要和users表里的对应。 [代码] for (let key in userdata) { let arr = []; arr.push(userdata[key].id); arr.push(userdata[key].name); arr.push(userdata[key].weixin); alldata.push(arr) } [代码] 还有下面这段代码,是把excel保存到云存储用的 [代码] //4,把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }) [代码] 下面把完整的excel里的index.js代码贴给大家,记得把云开发环境id换成你自己的。 [代码]const cloud = require('wx-server-sdk') //这里最好也初始化一下你的云开发环境 cloud.init({ env: "test-vsbkm" }) //操作excel用的类库 const xlsx = require('node-xlsx'); // 云函数入口函数 exports.main = async(event, context) => { try { let {userdata} = event //1,定义excel表格名 let dataCVS = 'test.xlsx' //2,定义存储数据的 let alldata = []; let row = ['id', '姓名', '微信号']; //表属性 alldata.push(row); for (let key in userdata) { let arr = []; arr.push(userdata[key].id); arr.push(userdata[key].name); arr.push(userdata[key].weixin); alldata.push(arr) } //3,把数据保存到excel里 var buffer = await xlsx.build([{ name: "mySheetName", data: alldata }]); //4,把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }) } catch (e) { console.error(e) return e } } [代码] 五,把excel存到云存储里并返回对应的云文件地址 我们上面已经成功的把数据存到excel里,并把excel文件存到云存储里。可以看下效果。 [图片] 我们这个时候,就可以通过上图的下载地址下载excel文件了。 [图片] 我们打开下载的excel [图片] 其实到这里就差不多实现了基本的把数据保存到excel里的功能了,但是我们要下载excel,总不能每次都去云开发后台吧。所以我们接下来要动态的获取这个下载地址。 六,获取云文件地址下载excel文件 [图片] 通过上图我们可以看出,我们获取下载链接需要用到一个fileID,而这个fileID在我们保存excel到云存储时,有返回,如下图。我们把fileID传给我们获取下载链接的方法即可。 [图片] 1,我们获取到了下载链接,接下来就要把下载链接显示到页面 [图片] 2,代码显示到页面以后,我们就要复制这个链接,方便用户粘贴到浏览器或者微信去下载 [图片] 下面把我这个页面的完整代码贴给大家 [代码]Page({ onLoad: function(options) { let that = this; //读取users表数据 wx.cloud.callFunction({ name: "getUsers", success(res) { console.log("读取成功", res.result.data) that.savaExcel(res.result.data) }, fail(res) { console.log("读取失败", res) } }) }, //把数据保存到excel里,并把excel保存到云存储 savaExcel(userdata) { let that = this wx.cloud.callFunction({ name: "excel", data: { userdata: userdata }, success(res) { console.log("保存成功", res) that.getFileUrl(res.result.fileID) }, fail(res) { console.log("保存失败", res) } }) }, //获取云存储文件下载地址,这个地址有效期一天 getFileUrl(fileID) { let that = this; wx.cloud.getTempFileURL({ fileList: [fileID], success: res => { // get temp file URL console.log("文件下载链接", res.fileList[0].tempFileURL) that.setData({ fileUrl: res.fileList[0].tempFileURL }) }, fail: err => { // handle error } }) }, //复制excel文件下载链接 copyFileUrl() { let that=this wx.setClipboardData({ data: that.data.fileUrl, success(res) { wx.getClipboardData({ success(res) { console.log("复制成功",res.data) // data } }) } }) } }) [代码] 给大家说下上面代码的步骤。 1,下通过getUsers云函数去云数据库获取数据 2,把获取到的数据通过excel云函数把数据保存到excel,然后把excel保存的云存储。 3,获取云存储里的文件下载链接 4,复制下载链接,到浏览器里下载excel文件。 到这里我们就完整的实现了把数据保存到excel的功能了。 文章有点长,知识点有点多,但是大家把这个搞会以后,就可以完整的学习小程序云开发的:云函数,云数据库,云存储了。可以说这是一个综合的案例。 有什么不懂的地方,或者有疑问的地方,请在文章底部留言,我看到都会及时解答的。后面我还会出一系列关于云开发的文章,敬请关注。
2019-09-07 - 小程序云函数调用http或https请求外部数据
我们使用小程序云开发的时候,难免会遇到在云函数里做http获取https请求外部数据,然后再通过云函数返回给我们的小程序。今天就来教大家如何在云函数里做http和https请求。 老规矩,先看效果图 [图片] 通过上图,可以看到我们在云函数里成功的访问到了百度的数据。下面就来讲下实现步骤。 一,定义云函数 关于云函数如何创建,这里我就不多说了。不知道如何创建的同学可以去看下我的云开发基础视频:https://study.163.com/course/courseMain.htm?courseId=1209499804 二,使用npm安装request-promise库 使用npm命令行之前,我们需要先安装node.js,node的安装网上搜一下就行。 下面我就来讲下在小程序里使用npm安装类库的步骤。 1, 右键我们的云函数,然后点击在终端中打开 [图片] 2,在打开的终端中输入 npm install request-promise [图片] 3, request-promise安装成功的标示如下 [图片] 三,编写我们的云函数代码 [图片] 把代码给大家贴出来,代码很简单,里面也有相应的注释,我们这里以请求百度的数据为例。 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') //引入request-promise用于做网络请求 var rp = require('request-promise'); cloud.init() // 云函数入口函数 exports.main = async (event, context) => { let url = 'https://www.baidu.com'; return await rp(url) .then(function (res) { return res }) .catch(function (err) { return '失败' }); } [代码] 到这里我就成功的在云函数里实现了http和https请求了,这里使用的是get请求,至于post请求如何使用,自己去百度下“ request-promise post请求”即可。 再来看下我们请求成功的效果图 [图片] 是不是很简单,有任何关于小程序,云开发相关的问题,都可以留言或者私信我,我看到后会及时解答的。
2019-09-23 - 【开箱即用】分享一个3D环物展示的解决方案
概述 有时候我们需要立体展示一个物体时,可能需要用到以下效果。当然实现的效果可能有很多,这里就为大家介绍一个大神写的方案,希望能帮到大家! 利用小程序开放的接口模拟简单的3D环物功能。只需传入物品序列照片数组即可。 [图片] 截图来自小程序“白海豚保护区” 一、小程序插件 AppID:wx0f253bdf656bfa08 基础库要求:>= 2.4.3 文档链接:https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx0f253bdf656bfa08 图片要求:网络URL链接 [图片] 二、兼容云开发 由于插件已经有段时间没有更新了,笔者在开发时又用到了小程序的云开发储存图片资源。拜原作者开源所赐,为了兼容云开发,我在开源的小程序插件代码中进行了部分修改。 开源代码:https://github.com/hiteochew/DimensionalShow-wxapp-plugin 修改方法: 将原代码中 downloadFile 方法替换为以下代码即可。 [代码]// 文件位置:plugin/api/util.js // 代码位置:第 124 行 function downloadFile(src) { return new Promise((resolve, reject) => { //云储存 wx.cloud.downloadFile({ fileID: src }).then(res => { resolve(res.tempFilePath); }).catch(error => { reject(err); }) }) } [代码] 结语 欢迎社区三连——关注点赞收藏!
2019-10-20 - 云函数发送email极简代码
const mailer = require('nodemailer-promise') exports.main = async (event, context) => { let sendEmail = mailer.config({ host: 'smtp.exmail.qq.com', //换成你邮箱的smtp port: 465, secure: true, //检查你邮箱的smpt服务器的设置 auth: { user: 'mailer@mycite.cn', //换成你的邮箱账号和密码 pass: 'Pwd' } }) let message = { from: 'anywords', to: event.to, subject: event.subject, text: event.text, } return await sendEmail(message) } [图片]
2020-10-20 - 云开发http api秒过的正确写法
let request = require('request-promise') let options = { method: 'POST', uri: 'https://api.weixin.qq.com/tcb/databasequery?access_token=' + access_token + '', body: { "env":'cloud-a8ee66', "query":`db.collection("col").doc("${colid}").get()`, }, json:true } let res = await request(options)
2019-08-22