- 申请开通「商家转账到零钱」需要什么材料?【案例】
前言 之前写过一篇《申请开通【商家转账到零钱】超时后如何反馈?》其中有读者提问提供那些资料才能通过审核的。 [图片] 这篇分享下我提供的材料内容,给大家一个案例作为参考。大家可以参考下方材料,然后根据自己产品具体业务进行修改调整即可。 材料准备 业务背景:配音工具,为了做让用户可以帮忙拉新,所以做了邀请好友赚钱的功能,需要用到商家转账到零钱。 1.商家转账到零钱业务界面截图 [图片] 2.业务界面截图+文字步骤标注 [图片] 3.分佣流程描述文档内容 [图片] 内容部分: 1.产品名称/APPID 2.产品介绍 3.盈利模式 4.需求背景 5.分佣流程 6.分佣规则 最后 祝大家顺利开通「商家转账到零钱」功能,如果觉得有帮助欢迎点赞/收藏。如果申请超时还没回复可以看《申请开通【商家转账到零钱】超时后如何反馈?》。
2023-07-14 - 腾讯云函数使用注意事项,解决云函数重复多次调用问题
云开发过程中遇到过多次云函数自动重复调用问题,每隔10秒重复调用共9次,百思不得其解。后与售后技术人员沟通了解,云函数不支持异步操作,并有多种内部限制因素,最终都会导致触发云函数自动重试机制,导致云函数重复调用。 触发云函数重试几个因素如下: 1.云函数执行耗时代码,超过云函数运行时限会被认为超时,触发云函数自动重试。建议将耗时代码拆解分发给其他云函数多批次执行,非小程序调用云函数超时最长可以设置900s。 2.云函数执行结束后没有return,超过云函数运行时限会被认为超时,触发云函数自动重试。云函数最后一定要return,return前建议延时1秒,防止残余代码未完全执行结束而被中断return。 3.云函数不支持异步操作,云函数return后仍有异步代码持续执行,会被认为超时,触发云函数自动重试。通过new promise包裹+async/await将异步代码转为同步。 4.云函数代码报错导致触发云函数自动重试。云函数代码一定要包裹try catch。 5.回调函数和触发函数一定要在规定时间内return,超过时限会被认为超时,触发云函数自动重试。事件触发必须5s内return,微信云支付回调大概是3s左右。 6.尽量使用腾讯云web端进行测试,不要使用微信开发工具IDE云端测试,由于控制台缓存版本不好处理,可能会导致IDE云端测试触发重复调用。官方建议web端进行云端测试。 7.对于高并发易超时的负责业务逻辑,通过多级调用+延时调用云函数,降低每秒并发量和缩短代码执行耗时,减少云函数超时自动重试带来的数据重复、流量翻倍等问题同时缩小问题影响范围。 示例代码: /** * 使用 Promise 和 async/await 来实现线程睡眠的效果,需要在 Node.js 版本 7.6 或更高版本上运行。 * @param {*} ms 等待指定的毫秒数 */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 函数内部异步代码按顺序同步执行 */ async function does(){ try { for (let i = 0; i < 3; i++) { //异步代码同步执行 await new Promise((resolve,reject)=>{ setTimeout(() => { let msg = '成功,第' + i + '个同步代码,等3s'; console.log(msg) if(msg){ resolve(msg) }else{ reject('失败') } }, 3000); }) } //耗时代码,通过分发给其他云函数执行,防止云函数超时报错,触发重试机制 cloud.callFunction({ // 云函数名称 name: 'test', // 传给云函数的参数 data: { }, }) } catch (error) { console.error("出错了:" + error); } }; // 云函数入口函数 exports.main = async (event, context) => { try { //包裹try catch防止云函数报错,触发重试机制 await does() await sleep(1000); //延迟一秒结束,防止未完全执行结束而被立刻return console.log('入口函数结束') } catch (error) { console.error("出错了:" + error); } return //一定要return,否则云函数认为没有执行结束导致超时报错,触发重试机制 }
2023-10-01 - 云开发模式下,怎么使用云调用发送相同主体公众号模板消息
https://developers.weixin.qq.com/community/develop/doc/000ae8d6348af08e7030bc2546bc01?blockType=1 [图片] 该指引说明,小程序可以下发相同主体下公众号模板消息,但是需要按照公众号模板消息接口整改 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html 该整改指引没有说明在云开发模式下,怎么下发相同主体下公众号模板消息
2023-09-22 - 【已成功】只用云开发,如何发送公众号模板消息?
下发统一消息接口回收后,如果你只有云开发,没有服务器和域名,不妨尝试使用本文的方法来发送公众号模板消息。 前提: 小程序和公众号同主体且已绑定到同一个微信开放平台账号。 思路推理: 小程序用户访问任意云函数都可以拿到小程序openid和unionid,提前将它们保存到云数据库用户集合中。 发送模板消息: 使用云函数A发送模板消息,需要调用公众号发送模板消息的接口,调用接口需要用到公众号的access_token。 获取和保存access_token: 使用云函数B获取access_token,推荐使用Stable Access token接口获取,减少出错率,云函数B可固定IP。因接口有日调用限制且access_token默认2小时过期,获取到access_token之后需要保存到云数据库中。为防止access_token过期,推荐云函数B设置定时触发,每隔1小时执行一次去重新获取access_token保存到云数据库中。 使用最新access_token: 每次发模板消息时从云数据库查询最新的access_token记录,发送模板消息需要用到公众号的openid。 获取公众号openid: 将小程序的云环境共享给公众号,小程序可使用云函数C来获取公众号的openid和unionid。如何操作呢? 1、使用云开发静态网站制作一个授权页面D,在该页面中访问云函数C,使用静默授权方式访问在云函数C即可获取到访问用户的公众号openid和unionid。 2、小程序使用webview来访问页面D,授权成功后在页面显示公众号的二维码,提示用户关注公众号获取通知功能。 将小程序和公众号用户关联: 通过上述引导用户获取到的公众号unionid来查询云数据库中的用户信息,保存公众号的openid到用户信息中。 经历以上安排之后,小程序云数据库的用户集合中,用户信息已经包含了小程序openid、unionid、公众号openid,到发消息的时机就可以发了。
2023-09-24 - 微信小程序的UnionID详解和使用场景
什么是UnionID ?(UnionID机制说明) 首先来看官方的定义: 如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过 UnionID 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,UnionID是相同的。(官方关于unionid原文档链接) 举个场景栗子:如果你有一个小程序A、一个小程序B、一个微信公众号【发发发】,并且将它们绑定到同一个微信开放平台账号下面。当微信号zhangsan666的用户进入到小程序A或者小程序B或者公众号发发发的时候,获取到的unionid是相同的。这样就可以根据unionid查询到当前微信用户在当前开放平台下的每个小程序或公众号里边的数据。对A小程序和B小程序和公众号的数据互通,提供了极大的便利。 UnionID获取途径 绑定了微信开放平台的小程序,可以通过以下途径获取 UnionID。 开发者可以直接通过 wx.login + [代码]code2Session[代码] 获取到该用户 UnionID,无须用户授权。小程序端调用云函数时,可在云函数中通过 Cloud.getWXContext 获取 UnionID。用户在小程序(暂不支持小游戏)中支付完成后,开发者可以直接通过[代码]getPaidUnionId[代码]接口获取该用户的 UnionID,无需用户授权。注意:本接口仅在用户支付完成后的5分钟内有效,请开发者妥善处理。 一般采用第一种途径获取用户的unionid,简单方便,在获取用户openid的同时,直接添加两个字段保存用户的session_key和unionid 微信开放平台的注册及绑定小程序、公众号... 如果想使用unionid,首先要注册微信开放平台,并且花300大洋进行开发者资质认证。微信开放平台访问地址: https://open.weixin.qq.com/ 认证通过后在 管理中心 — 小程序 — 绑定小程序 [图片] [图片] 如果感觉文章对你有帮助,请点个赞吧
2023-03-13 - 运用小程序Skyline技术构建无缝用户体验 —— 同程旅行酒店最佳实践分享
[图片] 动效衔接设计与小程序渲染框架 1、什么是动效衔接设计? 随着互联网技术和设计理念的不断发展,动效设计成为现代 UI 设计中不可或缺的一部分。其中,动效衔接是非常重要的一环。动效衔接设计是指通过巧妙的动效设计,将不同的 UI 元素在动画过程中自然、流畅地衔接起来,从而增强用户的交互体验和视觉感受。在实际应用中,动效衔接设计主要应用于界面转场、信息提示、状态变化等方面,通过顺畅的衔接,降低用户因白屏等待而产生的焦虑。 2、动效衔接设计的意义 (1)极大提高用户体验,让用户感受到界面的流畅和自然,从而增加用户对产品的好感度。 (2)降低用户的操作认知成本,帮助用户更好地理解执行操作后所带来的结果,从而减少用户对产品的困惑。 (3)强化视觉层,让用户更好地区分不同的信息和元素,从而增强视觉层次感。 (4)增加界面的美感度,让界面更加生动有趣,从而提升整体的美感和设计价值。 (5)提升品牌的认知度,让产品更加具有特色和独特性,从而提高品牌的认知度和市场竞争力。 3、什么是小程序渲染框架Skyline? 为了进一步优化小程序性能,小程序在原 webview 渲染引擎之外最新推出 小程序渲染框架Skyline,其使用更精简高效的渲染管线,并拥有诸多增强特性,让它拥有更接近原生渲染的性能体验。新的增强特性有 worklet 动画系统、手势系统、自定义路由、共享元素动画,而且许多常用的组件如 scroll-view、swiper 都有了更高性能的实现。 [图片] [图片] 实践理念和场景拆解 1、动效衔接设计的核心原则 简单而清晰的动效设计,需要遵守以下几个原则: (1)一致性:动效衔接应该与整体设计风格保持一致。包括颜色、字体、动画速度等方面。 (2)可预测性:用户能够感知动画元素的变化关联性,从而增加用户对产品的掌控感和对界面的理解。 (3)反馈性:动效衔接与用户操作相响应,从而帮助用户理解他们的操作所带来的结果。 (4)视觉层次:动效衔接遵循视觉层次原则,让用户区分页面中的上下关系以及三维物理世界的关系层次,给用户清晰的层级区分感知,提高用户体验。 (5)自然性:动效衔接符合物理规律,例如重力、加速度等,从而增强动画的真实感和用户体验。 2、理念孵化与使用场景拆解 以提炼的动态感受为出发点,理性的层面给予了我们大致的产品体验感知,为我们动效理念的建成提供了框架。对此我们将继续从感性层面出发,找寻可传递真实感受的运动现象并加以组合提炼。 本次 同程旅行小程序 以酒店预订链路中核心的相册页面进行应用场景,在用户操作图片的过程中运用小程序渲染框架承接。 (1)将整个过程进行了拆解,首先为了退出时行动的路径更加清晰,做了一个响应设计,当界面向右滑动退出的过程中,相册图片进行缩小,在缩放过程中,会根据位移距离控制缩放的比例,同时蒙层的透明度以及毛玻璃效果也跟随手指移动变化,和相册列表页在视觉上呈现 XY 轴以及上下层级的空间关系,在缩小到一定的比例时,触发震动效果,松手退出到相册列表页。 (2)在交互结束时,图片退回相册列表页原始位置,在返回路径的过程中,根据交互结束时的定位点,来判断运动的方向和距离,计算运动加速度,以及模拟运动加速度带来的惯性回弹的方向和角度变化,加强与模拟真实物理世界的运动定律,和视觉上的动态感知。 结合自然世界的运动规律来看,把页面进入的元素比作是行驶的汽车,用户当作是正在斑马线上行驶的人,将马路作为页面空间。若汽车采用的是缓入运动(加速)的话,马路上的行人则看到的是一辆不断加速向他行驶过来的车辆。因为担心车辆高速的逼近导致刹车不及时的情况,行人便会本能的作出躲闪的反应。其实页面也是一个道理,进入的元素使用加速运动出现过冲的运动感知会让用户体验时产生不适。 [图片] 小程序渲染框架技术开发实践过程剖析 1、开发自定义路由实现此交互,需要 自定义路由动画,因为小程序渲染框架的页面支持自定义跳转动画。当使用自定义路由后,页面跳转时指定路由类型,就会触发自定义路由动画,而不再是默认的从右往左的动画,此处的实现可以使得页面跳转时,没有默认的路由动画,页面将直接以透明的方式渲染在屏幕上,由开发者自己控制页面内元素的动画展示方式,具体实现如下: (1)在图片查看页面配置文件 index.json 中声明 { "backgroundColor": "#00000000", "backgroundColorContent": "#00000000", // 设置客户端页面背景为透明 "navigationStyle": "custom", "renderer": "skyline", // skyline渲染引擎 "disableScroll": true, "usingComponents": { } } (2)在 wxss 中,设置图片查看页面的 page 节点为透明背景 page { background: transparent; } (3)在 js 中,使用 wx.router.addRouteBuilder(routeType, fn) 来声明自定义路由动画 wx.router.addRouteBuilder('myCustomRoute', function (params) { const handlePrimaryAnimation = () => { 'worklet'; return { // 可在此处,根据 params.primaryAnimation.value 的值,来设置页面的动画效果 backgroundColor: `rgba(0,0,0,${ params.primaryAnimation.value })` }; }; return { opaque: false, handlePrimaryAnimation, barrierColor: '', barrierDismissible: false, transitionDuration: 320, reverseTransitionDuration: 250, canTransitionTo: true, canTransitionFrom: false }; }) (4)在图片列表页面中,使用 x.navigateTo 来跳转页面,并且设置 routeType 为 myCustomRoute wx.navigateTo({ url: '/pages/skyline-image-viewer/index?index=0', routeType: 'myCustomRoute' }) [图片] 需配置页面的渲染引擎为 Skyline,并且在跳转时使用 routeType 就可以实现让页面在跳转时没有默认的路由动画。 2、共享元素穿越在连续的页面跳转时,页面间 key 相同的 share-element 节点将产生飞跃特效,还可自定义插值方式和动画曲线,通常作用于图片。为保证动画效果,前后页面的 share-element 子节点结构应该尽量保持一致 <share-element key="share-key"> <view> you code here </view> <!-- 需要注意,share-element 内要求只有一个根节点 --> </share-element> [图片] 这时,界面的表现像上面视频一样,是一个连续的动画状态,这完全是由 share-element 来控制的,share-element 的动画原理如下图所示: [图片] 3、接入手势组件,实现图片放大、缩小、平移在图片查看页面有如下结构: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <share-element key="{{ shareKey }}" class="current-item"> <image src="{{ src }}"/> </share-element> </scale-gesture-handle> 这里,我们使用小程序渲染框架提供的 手势组件 <scale-gesture-handle>,来实现图片的放大、缩小、平移等手势交互。 注意,所有声明为 worklet 指令的方法它们运行在UI线程,不要在方法中修改普通的变量,因为跨线程的关系,只能修改使用 wx.worklet.shared 声明的变量。 const GestureState = { POSSIBLE: 0, // 此时手势未识别 BEGIN: 1, // 手势已识别 ACTIVE: 2, // 连续手势活跃状态 END: 3, // 手势终止 CANCELLED: 4 // 手势取消 }; Component({ attached() { this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.sharScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.sharScale.value})` } }); // 页面所需的数据,需要在 attached 事件里初始化完毕,使其可以参与首帧渲染 this.setData({ src: '...', shareKey: '...' }); }, methods: { // 当手势组件识别到手势时,触发此回调 onScaleGestureHandle(e) { 'worklet'; const { state } = e; // 在worklet函数里,不要使用 const {} = this 对this解构 const shareX = this.shareX; const shareY = this.shareY; const sharScale = this.sharScale; if (state === GestureState.BEGIN) { // 手势已经识别,此时,可以获取到手势的初始值 } else if (state === GestureState.ACTIVE) { // 手势活跃状态,此时,可以获取到手势的变化值,如平移的距离、缩放的比例等 // 将当前变化的值,设置到 `shared` 变量,就可以改变元素的样式,类似于vue3的数据驱动 shareX.value += e.focalDeltaX; shareY.value += e.focalDeltaY; sharScale.value = e.scale; } else if (state === GestureState.END || state === GestureState.CANCELLED) { // 手势终止或取消,此时,可以获取到手势的最终值 } } } }) [图片] 4、手势协商(解决手势冲突) 上面的 demo 简单演示如何使用手势组件来做图片交互,但是在图片查看页面中,我们还有其他的手势交互,如图片的左右滑动切换等,一般我们会使用 <swiper> 组件来实现,但是 <swiper>组件的内部实现和 <scale-gesture-handle> 组件,都会监听手势事件,手势组件的事件不支持冒泡的,就会导致下面结构横时: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </scale-gesture-handle> 使用手势横向滑动时,会优先触发 swiper 的横向切换事件,而无法触发 <scale-gesture-handle> 的手势事件了,这在图片放大时的图片横向移动产生了冲突。此时就需要使用手势协商来解决手势冲突。 什么是手势协商? 手势协商指的是:当页面同时有多个手势交互时,需通过一定的约定来决定哪些手势事件应该被执行,哪些需要被忽略。 小程序渲染框架解决手势冲突的方式,主要是通过手势组件的 tag、simultaneous-handlers、native-view 和 should-response-on-move 来实现 tag:手势组件的标识,用于区分不同的手势组件simultaneous-handlers:手势组件的协商者,表示需要同时触发事件的手势组件的标识should-response-on-move:参与手势时间的派发过程,返回 false时,表示该手势时间不会继续派发native-view:用当前手势组件来代理原生组件内部的手势事件,如<swiper>组件内部的手势事件<swiper> 的内部也是使用了 <horizontal-drag-gesture-handler>手势组件,但是我们不能直接在<swiper>上设置tag来使其参与手势协商,需要用相同的手势组件通过native-view=swiper将其内部的事件代理出来,使其可以参与协商<!-- <scale-gesture-handle> 缩放手势 --> <!-- <horizontal-drag-gesture-handler> 横向拖动手势 --> <!-- 通过 simultaneous-handlers=tag 来声明多个手势应该同时触发 --> <scale-gesture-handle tag="scale" simultaneous-handlers="{{['swiper']}}" worklet:ongesture="onScaleGestureHandle"> <!-- 此处使用 native-view=swiper 代理内部的手势组件 --> <!-- 通过 should-response-on-move=fn 来参与`事件派发`过程,决定手势的事件是否应该派发 --> <horizontal-drag-gesture-handler tag="swiper" native-view="swiper" simultaneous-handlers="{{['scale']}}" worklet:should-response-on-move="shouldResponseOnMove"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </horizontal-drag-gesture-handler> </scale-gesture-handle> const GuestureMode = { INIT: 0, SCALE: 1, SWIPE: 2, MOVE: 3 // ... }; Component({ attached() { this.GuestureModeShared = wx.worklet.shared(GuestureMode.INIT); this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.shareScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.shareScale.value})` } }); // ... }, methods: { onScaleGestureHandle(e) { 'worklet'; const { state } = e; if (state === GestureState.BEGIN) { this.GuestureModeShared.value = GuestureMode.INIT; } else if (state === GestureState.ACTIVE) { if(this.GuestureModeShared.value === GuestureMode.INIT) { this.gestureBefore(e); // 手势类型未知时,判断手势类型 } else { this.gestureHandle(e); // 手势类型已知时,处理手势事件 } } else if (state === GestureState.END || state === GestureState.CANCELLED) { this.GuestureModeShared.value = GuestureMode.INIT; } }, // 判断手势类型 gestureBefore(e) { 'worklet'; const { focalDeltaX, focalDeltaY, scale } = e; if (Math.abs(focalDeltaX) > Math.abs(focalDeltaY)) { this.GuestureModeShared.value = GuestureMode.SWIPE; } else if (scale > 1) { this.GuestureModeShared.value = GuestureMode.SCALE; } else { this.GuestureModeShared.value = GuestureMode.MOVE; } }, // 处理手势事件 gestureHandle(e) { 'worklet'; if (this.GuestureModeShared.value === GuestureMode.SCALE) { this.shareScale.value = e.scale; } else if (this.GuestureModeShared.value === GuestureMode.SWIPE) { // swiper 切换模式时,这里什么都不用做 } else if (this.GuestureModeShared.value === GuestureMode.MOVE) { this.shareX.value += e.focalDeltaX; this.shareY.value += e.focalDeltaY; } }, // 用于判断手势事件是否应该派发 shouldResponseOnMove(e) { 'worklet'; return this.GuestureModeShared.value === GuestureMode.SWIPE; // 当模式为SWIPE时,才响应手势事件 } } }) [图片] 通过上面的代码,我们实现了手势协商,当用户在图片上进行滑动的操作时,总是会触发 <scale-gesture-handler> 的手势事件,通过对图片当前状态的判断来决定应该触发哪种手势,我们通过此种协商让 <horizontal-drag-gesture-handle> 手势在合适的时机触发,以此避免手势冲突。 5、使用小程序渲染框架时需要注意的一些地方作为一款新的渲染优化方式,开发者使用小程序渲染框架需要注意以下内容,以保证渲染的效果和性能。 (1)自定义路由时首帧渲染&首帧性能优化 小程序渲染框架的首帧渲染对共享元素动画非常重要,若共享元素节点的key 错过首帧设置的话,可能会丢失飞跃动画,所以在使用小程序渲染框架时,共享元素的 key 应该尽量在 attached 中或之前设置到页面,并且在首帧渲染时,应尽可能的减少 UI 层的渲染工作 如下: 1)所需要的数据应尽可能使用提前计算好,避免构建页面时等待太久影响响应速度 2)首次设置的数据应该尽可能的少,避免首次渲染时,页面上的元素过多,导致首帧渲染时间过长,导致动画卡顿(如:不要同时初始化太多的 <swiper-item>) 3)确保首帧渲染时,共享元素的 key 正确的设置,避免在首帧渲染时,由于找不到对应的共享元素,导致动画丢失,看不到飞跃动画 4)由于手势事件触发频繁,应尽量避免大量需要的计算的逻辑高频执行,容易导致机器发烫,或者导致动画卡顿 **worklet 函数的使用** worklet 函数的使用有一些限制,主要是由于它是在 UI 线程执行的,所以 worklet 函数中的 this 并非是页面的 this 实例, 里面所使用到的变量也是通过特殊的 babel 插件转换到UI线程的,需要与逻辑层共用的变量都需要用 wx.worklet.shared 将它声明成共享变量,在 UI 线程调用逻辑层的函数需要使用 wx.worklet.runOnJS (2)与 web 规范的差异 虽然小程序渲染框架尽可能的与 web 规范保持一致,但是由底层渲染引擎的限制,还是有一些差异,如: 1)display: flex 的默认朝向是 column,而不是 row,这需要开发者注意,官方后续会支持 block 布局方式 2)暂不支持 css 伪元素,如 ::after、::before,官方正在支持中 3)position 仅支持 absolute、relative,不支持 sticky,实现滚动吸附的效果需用 sticky-* 组件来配合 scroll-view 实现 ** <share-element> 在非小程序渲染框架运行环境里的表现是什么** 在非小程序渲染框架的运行环境内,<share-element> 组件会被视为一个 <view> 组件,需要做好布局的兼容 6、何时使用小程序渲染框架开发时,请确保小程序开发者工具版本是 最新版 nightly,sdk 版本在 2.30.2+,具体限制可参考 文档。 这些新特性的引入,使得小程序渲染框架在小程序开发中的优势更加明显,开发者可以更加便捷地实现各种复杂的交互效果,并且达到接近原生APP的体验。 [图片] 未来展望 1、个性化产品形态:将会根据不同的用户需求和场景,设计出更加符合用户喜好和习惯的动效衔接,进行组件化调用。 2、更加自然和真实的动效衔接:动效衔接将会更加贴近自然规律和真实物理效应,从而增强动画的真实感和用户体验。 3、更加智能化和自适应的动效衔接:动效衔接将会根据用户的操作行为和使用习惯,自适应调整动画效果,从而提高用户体验和产品效果。 4、扩大产品、设计与开发的协作效应:设计对动效的把控、产品对用户的洞察以及开发对新技术的应用,才可以发挥最大化的协作效应。 附1:本文作者 同程旅行研发工程师 同程旅行体验设计师 同程旅行产品经理 附2:代码片段 相册小程序代码片段(请使用 PC 端浏览器打开):https://developers.weixin.qq.com/s/E979jCmP7oHG 附3:UE标注 [图片] 附4:AB 实验效果 AB 实验显著win0.23% [图片]
2023-04-28 - 🎆我们开源啦 | 基于Skyline开发的组件库🚀
我们开源啦,希望可以给大家的开发之旅带来一些灵感。我后溪的小程序也都会基于这个组件库开发,并且会保持组件库的更新与维护。 我是第一次进行开源,肯定会有错漏,欢迎大家指正,我会以最快的时间响应修改。 Skyline UI 组件库 前言 Skyline 是微信小程序推出的一个类原生的渲染引擎,其使用更精简高效的渲染管线,性能比 WebView 更优异,并且带来诸多增强特性,如 Worklet 动画、手势系统、自定义路由、共享元素等。 使用这个组件库的前提是:通过微信小程序原生+skyline框架开发,所以目前我们不保证兼容webview框架(也就是电脑端与低版本的微信),但后续会进行系统性的兼容。 使用 Skyline UI前,请确保你已经学习过微信官方的 微信小程序开发文档 和 Skyline 渲染引擎文档 。 背景 随着Skyline 渲染引擎 1.1.0 版本发布,我们所运营的小程序也平稳的渡过了阵痛期,团队使用Skyline也越来得心应手,所以接下来,团队的开发重心全面偏向Skyline渲染框架,考虑有大量的UI交互重复,我们决定基于Skyline开发了这个UI组件库。 但团队力量有限,这个新生的组件可能有很多的不尽如人意,所以希望能以开源的方式吸引更多开发者使用Skyline框架,如果这个框架不适合你,也可以借鉴其思路。 Gitee Gitee仓库 在线预览 以下是目前两个使用该框架的小程序 SkylineUI组件库 [图片] NONZERO COFFEE [图片] 开始使用 UI库结构 Skyline UI组件库 依赖于以下四部分,具体使用参考以下的具体说明 utils工具库: 其中包含了UI库自定义的一个工具类SkyUtils,它包含了组件中所含的各种函数,非常重要。 各组件元素:sky-*(组件名) skywxss样式库:其中包含深浅色色彩、文字字体、布局等样式wxss 在小程序中引入 UI库 一、直接下载引入 点击下载组件包 将src下所有文件复制到您项目根目录下的components文件夹中,没有的话请自行新建。 二、npm引入 1.在小程序项目中,可以通过 npm 的方式引入 SkylineUI组件库 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 2.安装组件库 [代码]npm install jieyue-ui-com [代码] 3.npm 命令执行完后,需要在开发者工具的项目中点菜单栏中的 工具 - 构建 npm 两种引入方式的不同可能导致后续使用时,引用组件的路径不同,请注意区别 1.直接引入components文件夹内,引用地址通常是 ‘./components/‘ 2.npm引入,组件引用地址通常是’./miniprogram_npm/jieyue-ui-com/’ 如何使用 1.在app.js文件中初始化工具类,并且添加两个全局变量 [代码]// app.js App({ onLaunch() { ;(async ()=>{ // 全局注册工具类SkyUtils // 这里默认npm引用,地址为'./components/utils/skyUtils',如果是直接引用组件,地址可能是'./components/utils/skyUtils',后面不再说明 const SkyUtils = await import('./components/utils/skyUtils'); wx.SkyUtils = SkyUtils.default; // 初始化设备与系统数据 wx.SkyUtils.skyInit() // 小程序自动更新方法 wx.SkyUtils.versionUpdate() })() }, globalData: { sky_system:{}, sky_menu:{} }, }) [代码] 2.在app.wxss文件中引入样式文件 [代码]//wxss * _dark.wxss 是适配深色模式的色彩变量 @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor_dark.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfontline.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfont.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyother.wxss'; [代码] 3.page.json中引用组件 [代码]//page.json { "usingComponents": { "sky-text":"/miniprogram_npm/jieyue-ui-com/sky-text/sky-text" } } [代码] 4.页面中使用 [代码] // wxml <sky-text content="文本内容" max-lines="2" fade></sky-text> [代码] 5.其他组件具体使用请参考组件包中的redeme.md 适配深色模式 如果您在开发时,全部使用我们预设好的颜色变量,那么可以自动适配深色模式。 [代码].page{ background-color: var(--bg-l0); } [代码] [代码] <view style="background-color: var(--bg-l0)"></view> <view style="background-color: {{color}}"></view> [代码] [代码] Page({ data: { color: "var(--bg-l0)" } }) [代码]
01-09 - Skyline|小程序页面转场动画
开发者A:小程序跳转时,页面切换效果可以自定义实现吗? 开发者B:搞个单页自己写呢 开发者A:这个写起来代码量有点大,而且放单页里面代码太复杂了🥹 官方:来啦来啦~小程序页面转场动画可以使用小程序自定义路由来实现,赶紧往下 ⬇️ 看~ 我们先来看下有无自定义路由的效果对比 没有自定义路由:只能使用默认路由切换效果,从右到左推入页面使用自定义路由:支持自定义转场动画,示例下沉式路由效果[图片] 在使用默认路由的时候,只需要调用 wx.navigateTo 即可,而自定义路由,则需要先声明 1、通过 wx.router.addRouteBuilder( 命名,builder 函数 ) 来声明自定义路由 2、页面跳转新增 routeType 参数,值为上一步的命名 [图片] 接着,我们来看下声明自定义路由中 builder 函数,这个函数主要是定义两个动画 定义页面推入动画:结合 推入进度、推入状态 等参数,由开发者自定义计算得来定义页面被推出动画:结合 被推出进度、被推出状态 等参数,由开发者自定义计算得来[图片] 当打开一个新页面的过程中,就会触发自定义路由动画,此时有两个动画 新页面:打开动画旧页面:隐藏动画下图演示在页面 A 打开 页面 B,打开过程的动画效果: 新页面:页面 B 半屏打开旧页面:页面 A 页面下沉[图片] 动画除了上述讲的动画效果之外,还有一个关键的点就是动画曲线,动画曲线可以让动画效果更加丰富。 👇 下面我们可以看出来,相同的动画效果,但是使用不同的动画曲线,展示出来的页面切换效果是很不一样的。worklet 支持了常见的动画缓动函数 Easing,开发者可以根据业务需求实现页面动画切换效果。 [图片] 那么我们就来看一下页面切换动画的代码是怎么实现的~ 页面的切换动画通过 worklet 函数来实现的,worklet 函数运行在 UI 线程,使得小程序可以做到 类原生动画般的体验。 我们先来看下这里设计的新页面打开时的动画,在页面打开的过程中,开发者可以收到一个 primaryAnimation 的参数,这个参数表示当前页面从 0 到 1 打开的进度,此时,我们通过 handlePrimaryAnimation 来实现页面打开过程我们希望页面展示的动画效果,例如:改变页面高度、圆角、与顶部的偏移距离等。 这样,就实现页面展示的动画效果。 // 新页面:页面 B 半屏打开 const handlePrimaryAnimation = () => { 'worklet' // primaryAnimation 为 builder 函数的参数,表示当前页面从 0 - 1 展示动画的进度 // primaryAnimation 为 sharedValue 类型,当 primaryAnimation 变化时,这个函数就会被执行 let t = primaryAnimation.value // 非手势触发时,可以通过动画曲线 easeInToLinear 来改变动画的进度值 // worklet 支持了常见的动画缓动函数,开发者可以根据业务需求实现需要的页面切换动画效果 if (!userGestureInProgress.value) { t = wx.worklet.Easing.bezier(0.35, 0.91, 0.33, 0.97).factory()(t) } const top = 0.12 // 半屏页面距离顶部的距离比例 const selfHeight = (1 - top) * screenHeight // 半屏页面高度 const marginTop = top * screenHeight // 半屏页面距离顶部的距离 const translateY = selfHeight * (1 - t) // 页面动画过程中的纵向偏移值 // 返回 AnimatedStyle,改变页面展示 return { marginTop: `${marginTop}px`, borderRadius: '10px', height: `${selfHeight}px`, transform: `translateY(${translateY}px)`, } } 同样的,页面隐藏跟页面展示是类似的,开发收到的是 secondaryAnimation 参数,表示的是页面从 1 到 0 的关闭进度,这里我们通过 [代码]handleSecondaryAnimation 来实现页面隐藏的效果。[代码] 注意:handleSecondaryAnimation 表示下一页面推入时,当前页面的隐藏动画,在下沉式动画这个案例中,handleSecondaryAnimation 是旧页面的,而 handlePrimaryAnimation 是新页面的。 // 旧页面:页面 A 页面下沉 const handleSecondaryAnimation = () => { 'worklet' // secondaryAnimation 为 builder 函数的参数,表示下一个页面推入时,当前页面从 1 - 0 隐藏动画的进度 // secondaryAnimation 为 sharedValue 类型,当 secondaryAnimation 变化时,这个函数就会被执行 let t = secondaryAnimation.value // 非手势触发时,可以通过动画曲线 fastOutSlowIn 来改变动画的进度值 if (!userGestureInProgress.value) { t = wx.worklet.Easing.bezier(0.4, 0.0, 0.2, 1.0).factory()(t) } const top = 0.1 // 页面距离顶部的距离比例 const scaleRatio = 0.08 // 缩放比例 const translateY = screenHeight * (top - 0.5 * scaleRatio) * t // 页面动画过程中的纵向偏移值 const scale = 1 - scaleRatio * t // 缩放过程中的比例 const radius = 12 * t // 页面圆角 // 返回 AnimatedStyle,改变页面展示 return { borderRadius: `${radius}px`, transform: `translateY(${translateY}px) scale(${scale})`, } } 实现完动画切换的函数之后,我们需要包装到 builder 函数中并注册这个 builder 函数。 // 注册 builder 函数 wx.router.addRouteBuilder("HalfScreenDialog", HalfScreenDialogRouteBuilder) wx.router.addRouteBuilder("ScaleTransition", ScaleTransitionRouteBuilder) // 实现页面 B 的 builder 函数:页面打开时半屏打开 const HalfScreenDialogRouteBuilder = ({ primaryAnimation }) => { return { handlePrimaryAnimation // 页面 B 打开时的动画,上文中实现的 handlePrimaryAnimation 函数 } } // 实现页面 A 的 builder 函数:下一个页面打开时,当前页面下沉 const ScaleTransitionRouteBuilder = ({ primaryAnimation, secondaryAnimation }) => { return { handlePrimaryAnimation, // 页面 A 打开时的动画 handleSecondaryAnimation // 页面 B 隐藏时的动画,上文中实现的 handleSecondaryAnimation 函数 } } 在文章开头我们知道,声明完自定义路由之后,需要在页面跳转时指定路由类型。 到这里,通过页面跳转,返回按钮已经达到我们要的效果了。 // home.js // 首页打开页面 A, wx.navigateTo({ url: 'pageA', routeType: 'ScaleTransition', }) // page.js // 页面 A 打开页面 B wx.navigateTo({ url: 'pageB', routeType: 'HalfScreenDialog', }) 我们在体验原生页面切换时,手势也是顺滑切换的重要组成部分,在上一篇 小程序手势:让半屏弹窗更顺滑 我们已经了解手势的使用,那么我们这里给页面绑定手势,支持向右、向下拖动页面返回。 我们在页面最外层嵌套一个手势组件 horizontal-drag-gesture-handler(横向滑动时触发) 当手势向右滑动时,根据触摸位置改变页面当前的状态 触摸中:页面随手指拖动触摸结束:根据手势速度和位置判断关闭还是打开页面// .wxml // .js // 根据手势状态改变页面展示状态 // this.customRouteContext 中包含当前页面定义路由 builder 时的全部变量 handleHorizontalDrag(gestureEvent) { "worklet"; if (gestureEvent.state === GestureState.BEGIN) { // 触摸开始 const { startUserGesture } = this.customRouteContext; startUserGesture(); } else if (gestureEvent.state === GestureState.ACTIVE) { // 触摸中,实现跟随手指拖动页面效果 const delta = gestureEvent.deltaX / windowWidth; const { primaryAnimation } = this.customRouteContext; const newVal = primaryAnimation.value - delta; primaryAnimation.value = clamp(newVal, 0.0, 1.0); } else if (gestureEvent.state === GestureState.END) { // 触摸结束 const { stopUserGesture, didPop } = this.customRouteContext; ... didPop(); // 退出页面调用 stopUserGesture(); // 结束必须调用 } else if (gestureEvent.state === GestureState.CANCELLED) { // 触摸取消 } } 添加完手势之后,就可以通过手势关闭页面了~ [图片] 除了案例中实现的下沉式半屏效果,自定义路由可以根据开发者需要自行定制动画。 目前,官方提供了几个常用的路由效果供大家使用,mark 这个 代码片段 即可使用。
2023-08-03 - Skyline|原生级卡片转场,小程序轻松实现
在上一篇文章《在小程序中实现原生相册》中,我们学习了自定义路由搭配共享元素实现的原生相册效果,共享元素可以让用户在体验小程序时视觉关联性更强。 除了相册实现之外,常见的卡片转场也非常适合。 [图片] ⬆️ 演示效果:默认动画 vs 卡片转场动画 👇 下面我们来看看卡片转场中通过 共享元素 + 自定义路由 来实现无痕跳转。 [图片] 这里的转场稍微有点复杂,涉及到以下 3 个点 旧卡片:图片放大、内容渐隐新页面:按比例放大、页面渐显手势搭配1、旧卡片:图片放大、内容渐隐 在本示例中,列表页采用的是 scroll-view 瀑布流布局的实现。 [图片] 这里我们的共享元素是卡片,即 grid-view 中的内容 card,卡片包括 图片、内容描述。 [图片] 默认情况下,共享元素是整个节点进行飞跃的,由于前后页面的图片元素一致但文本内容不一致, 导致在第一帧或者最后一帧会有跳动的效果。 为了让转场动画更加自然,我们需要在飞跃的过程中渐隐旧卡片的内容描述。 [图片] 在这里,我们需要先用 this.applyAnimatedStyle 来给对应的节点绑定 worklet 驱动动画。 .card_wrap 节点:整个卡片按比例放大.card_desc 节点:内容描述渐隐[图片] 关于动画执行的时机,我们可以通过配置项修改。 immediate:设置是否立即执行驱动动画flush:shareValue 更新时,applyAnimatedStyle 的 updater 函数刷新时机在本例中,需要保证共享元素的图片与目标页面图片位置重叠,所以 flush 设置 sync 在当前时间片刷新。 [图片] 绑定完驱动动画之后,我们需要给共享元素绑定帧回调事件,根据当前动画进度改变共享变量的值来驱动共享动画 [图片] 2、新页面:按比例放大、页面渐显 新页面在路由中的动画,需要在自定义路由中进行配置。关于自定义路由的更多介绍,可参考《小程序页面转场动画》 在路由动画过程中,我们将上一步的共享元素帧回调拿到 begin、end 的值,然后结合动画进度 t 计算得出新页面的位置、缩放比例。 还有根据动画进度,设置页面渐显,与前面的卡片渐隐承接。 [图片] 3、手势搭配 学习过我们前面的文章的同学都知道,自定义路由经常需要结合页面手势,来实现手势返回,关于手势的基础知识可参考《小程序页面转场动画》 [图片] 这里我们希望手势缩小整个当前页面,所以这里手势返回时只在当前页面做手势动画即可。 在页面详情页的最外层,嵌套一个手势组件 pan-gesture-handler,当手势拖动时根据手势的位置改变整个页面(通过 #fake-host 控制)的位置和大小来达到拖动的效果。 [图片] 同样绑定页面驱动动画,通过 applyAnimatedStyle 给 #fake-host 绑定驱动动画,当共享变量 transX、transY 等变化时则自动改变 transform 来驱动 #fake-host 缩小。 [图片] 接着绑定手势事件,根据手势拖动时拿到位置信息改变共享变量 transX、transY 的值。 [图片] 最后我们需要设置背景颜色透明,来达到类似把卡片拖回列表的视觉效果,更好的减少页面切换感~ [图片] 一个自定义路由的页面会有 3 层可以设置到背景色,要做到透明的效果需要将 3 个背景色都设置为透明。更多自定义路由背景色的详情参考官方文档。 [图片] 想要试试卡片转场的无恒效果~扫描 ⬇️ 下方小程序码即可体验。 如果你也想在小程序中实现卡片转场动画,mark 下这个 源码 直接接到到你的小程序吧~ [图片]
2023-08-03 - Skyline|小程序手势:让半屏弹窗更顺滑
在小程序页面开发中,我们经常用半屏弹窗来进来内容展示,例如:微信开放社区切换主页、加入购物车的选项页、文章留言区等等。 [图片] 常见的半屏弹窗展示逻辑是这样的: 打开弹窗:点击 “打开弹窗” 按钮展示弹窗关闭弹窗:点击“关闭按钮” or 遮罩层 关闭弹窗当我们想在半屏弹窗加一些交互动画时,可以监听节点的 touch 事件来做一些手势判断,进而处理拖拽事件。但是这种方式实现的滚动动画容易卡顿,出现延迟的情况,效果并不理想。 为了丰富小程序的交互体验,我们内置了一批手势组件,可以帮助开发者更好的实现交互动画的效果。 下图演示使用手势的半屏弹窗下拉效果与普通半屏下拉的对比。 当内部评论列表往下拉到顶部时,变为半屏的下拉,可直接下拉关闭弹窗。 [图片] 我们来看下这种操作是怎么实现的 在上面评论列表的半屏弹窗中会有一个 scroll-view 滚动组件,在 scroll-view 中会有滚动事件,当滚动到顶部时,我们希望有整个半屏的下拉事件。 所以我们需要在半屏的最外层放置一个拖动手势组件 pan-gesture-handler 由于拖动组件内部的 scroll-view 也是可以滚动的,所以这里需要进行一个手势协商的处理,就是什么条件下由哪个组件来响应手势。 当手势往下 ⬇️ 滚动时,此时判断内部 scroll-view 滚动条的位置 滚动条处于顶部:外层 pan-gesture-handler 响应滚动,此时半屏往下拖动至关闭半屏滚动条不处于顶部:内层 scroll-view 响应滚动,此时内部列表往上滚[图片] 当手势往上 ⬆️ 滚动时,此时判断半屏的位置 半屏不完全打开时:外层 pan-gesture-handler 响应滚动,此时半屏往上拖动至完全打开半屏半屏完全打开时:内层 scroll-view 响应滚动,此时内部列表往下滚[图片] 我们来看一下代码的实现,这里用到的手势组件 pan-gesture-handler(拖动时触发)和 vertical-drag-gesture-handler(纵向滑动时触发),手势组件有以下属性 on-gesture-event:手势回调事件should-response-on-move:是否响应当前手势的 move 阶段simultaneous-handlers:指定需要协商的手势是哪几个,下面演示表示 pan 和 scroll 协同触发。native-view:代理的原生节点,这里 scroll-view(scroll-y) 内有个 vertical-drag 手势,scroll-view 自身无法处理,需要被代理出来 ... 接着,我们看看在页面 js 中怎么处理手势。 在手势处理的回调中因为会改变半屏的状态值,所以这里的回调函数采用 worklet 函数,worklet 函数运行在 UI 线程,使得小程序可以做到类原生动画般的体验。 // page.js // shared 创建的变量为共享变量,可在 UI 线程和 JS 线程间同步 this.transY = wx.worklet.shared(1000) this.scrollTop = wx.worklet.shared(0) this.startPan = wx.worklet.shared(true) // shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商 shouldPanResponse() { 'worklet' return this.startPan.value }, shouldScrollViewResponse(pointerEvent) { 'worklet' // transY > 0 说明 pan 手势在移动半屏,此时 scroll-view 滚动不应生效 if (this.transY.value > 0) return false const scrollTop = this.scrollTop.value const { deltaY } = pointerEvent // deltaY > 0 是往上滚动,scrollTop <= 0 是滚动到顶部边界,此时 pan 开始生效,scroll-view 滚动不生效 const result = scrollTop <= 0 && deltaY > 0 this.startPan.value = result return !result }, // pan 手势处理 handlePan(gestureEvent) { 'worklet' if (gestureEvent.state === GestureState.ACTIVE) { const curPosition = this.transY.value const destination = Math.max(0, curPosition + gestureEvent.deltaY) // 改变半屏的位置 this.transY.value = destination } // 其他手势状态的处理,如滚动结束时计算半屏处于打开还是关闭的状态 } 目前,同程旅行 已经上线了手势结合半屏的效果 体验路径:酒店查询 - 选择酒店 - 选择入住人 - 新增入住人 [图片] 普通半屏结合手势代码片段:https://developers.weixin.qq.com/s/lx0RH1mD7rGj 手势除了在普通半屏的应用之外,也可以实现分段式半屏。下面演示的分段式半屏比普通半屏的判断条件更多一些。 判断条件同普通半屏类似,根据手势方向 和 分段式半屏当前的位置来判断是响应分段式半屏还是内部列表,响应分段式半屏是改变到哪一个位置。 [图片] 这里与普通半屏不同的是我们还改变了地图的缩放级别(scale) 因为 worklet 函数是在 UI 线程运行的,当要改变 data 值时,需要通过 wx.worklet.runOnJS 调回 JS 线程。 // page.js // 设置 map scale // 运行在 JS 线程 setMapScale(scale) { this.setData({ scale }) }, // worklet 函数,运行在 UI 线程 scrollTo(toValue) { 'worklet' let scale = 18 if (toValue > screenHeight / 2) { scale = 16 } // 从 UI 线程调回 JS 线程 wx.worklet.runOnJS(this.setMapScale.bind(this))(scale) this.transY.value = timing(toValue, { duration: 200 }) }, // 处理拖动半屏的手势 handlePan(gestureEvent) { 'worklet' // 滚动半屏的位置 if (gestureEvent.state === GestureState.ACTIVE) { // deltaY < 0,往上滑动 this.upward.value = gestureEvent.deltaY < 0 // 当前半屏位置 const curPosition = this.transY.value // 只能在 [statusBarHeight, screenHeight] 之间移动 const destination = clamp(curPosition + gestureEvent.deltaY, statusBarHeight, screenHeight) if (curPosition === destination) return // 改变 transY,来改变半屏的位置 this.transY.value = destination } if (gestureEvent.state === GestureState.END || gestureEvent.state === GestureState.CANCELLED) { if (this.transY.value <= screenHeight / 2) { // 在上面的位置 if (this.upward.value) { this.scrollTo(statusBarHeight) } else { this.scrollTo(screenHeight / 2) } } else if (this.transY.value > screenHeight / 2 && this.transY.value <= this.initTransY.value) { // 在中间位置的时候 if (this.upward.value) { this.scrollTo(screenHeight / 2) } else { this.scrollTo(this.initTransY.value) } } else { // 在最下面的位置 this.scrollTo(this.initTransY.value) } } }, 分段式页面代码片段:https://developers.weixin.qq.com/s/fw0U31mI7bGf 半屏的交互除了在页面内实现,也能跨页面实现,如常见的下沉式半屏交互。其中,半屏效果与上述实现类似,而前一页面的下沉实现需要结合自定义路由 后面的文章中我们会介绍自定义路由结合手势怎么去实现下沉式半屏效果,不仅如此,还有很多类原生的页面切换效果都能通过自定义路由实现 [图片]
2023-08-03 - 小程序渲染引擎Skyline小试牛刀--快书
今年年初,在官方文档上看到小程序团队要推出一款性能逼近原生的渲染引擎Skyline,就一直在关注。刚好最近打算做一款新的阅读小程序,作为一名独立开发者,对于性能和用户体验的追求是永无止境的,于是我决定用纯Skyline打造这款小程序。 当然,这个项目里面所用到的skyline特性只是冰山一角,并非全部,更多酷炫的特性请前往官方文档查阅。 接下来,我会结合快书小程序,从以下几个方面,逐条阐述关于skyline特性(快书项目中所用到的)的理解与应用: 效果演示。如何开启Skyline。新版组件swiper。新版组件scroll-view。全新组件snapshot。增强特性worklet动画。增强特性手势系统。增强特性自定义路由。增强特性共享元素动画。希望对于刚接触Skyline,或者想要了解Skyline的同学有所帮助。当然,如有错误或遗漏,欢迎在评论区批评指正,不胜感激。 一、效果演示 [图片] 二、如何开启Skyline 开启Skyline的方式非常简单,只需要在app.json文件中,加入以下配置即可(这里是全局Skyline,若只打算指定页面开启,则在指定页面的json文件中配置即可): "renderer": "skyline", "lazyCodeLoading": "requiredComponents", "rendererOptions": { "skyline": { "defaultDisplayBlock": true, } }, "componentFramework": "glass-easel", 三、新版组件-Swiper 旧版的Swiper基于webview的,在性能上有所局限,特别是当swiper-item的数量动态不断增加的情况下。当然,也可以自己想办法去优化,比如做懒加载和缓存,但相对来说比较麻烦。而Skyline版本的Swiper性能会大幅度提升,首先渲染引擎本身的性能提升了,另外官方也做了缓存的功能,只需要通过定义cache-extent的值,就能轻松定义缓存区域大小,例如值为 1 则表示提前渲染上下各一屏区域。 [图片] 用法上,和webview版本没有太大区别(这里就不放代码了),只需注意不要使用某些webview独有的特性即可。 四、新版组件-Scroll-view 同样,旧版的scroll-view也基于webview的,滚动元素过多的时候会有明显卡顿,当然也是可以通过虚拟Dom的方式自行优化。然而,Skyline版本的scroll-view官方已经实现了只会渲染在屏节点的特性,大大提升了滚动的流畅度,真正做到了开箱即用。 用法上,有以下几个点要注意的。 指定type属性,有2个可选值,分别为:list和custom,对应的是列表模式和自定义模式。如是普通列表,list即可,如果是稍微复杂的列表,比如常见的瀑布流表现形式(类似小红书那样),则可使用custom。只有直接子节点才能根据是否在屏来按需渲染。即你不能把你的列表项,都放在同一个父级view中,而是应该直接放在scroll-view组件下。 // 错误的方式: <scroll-view type="list" scroll-y> <view> <view class="item" wx:for="{{dataList}}" wx:key="id"></view> </view> </scroll-view> // 正确的方式 <scroll-view type="list" scroll-y> <view class="item" wx:for="{{dataList}}" wx:key="id"></view> </scroll-view> // 正确的方式 <scroll-view type="custom" scroll-y> <list-view> <view class="item" wx:for="{{dataList}}" wx:key="id"></view> <list-view> </scroll-view> 另外,上面提到了瀑布流的问题,实现方式也很简单,官方提供了一个叫做grid-view的组件,只需定义它的type="masonry"即可,但若是在webview下,除了性能不理想以外,还会有一些小BUG,比如我在社区提的这个问题:grid-view masonry 在webview模式下经常会出现大块区域的空白。在Skyline下,就不会出现此类问题。 [图片] <scroll-view type="custom" scroll-y> <grid-view type="masonry" main-axis-gap="15" cross-axis-gap="15"> <view wx:for="{{dataList}}" wx:key="id"></view> </grid-view> </scroll-view> 五、全新组件Snapshot 我们常常会有分享精美海报的需求,但由于海报上的内容是动态,仅仅使用一张图片分享达不到我们的目的。在以往,我们可能会使用到wxml-to-canvas,通过绘制 canvas ,导出图片。现在,在Skyline下(基础库3.0.0以上),实现此需求就非常简单。只需要将我们要分享的内容包裹在snapshot组件下就行。 [图片] // wxml: <snapshot id="target"> <view>content</view> </snapshot> // page: Page({ onReady() { this.createSelectorQuery() .select("#target") .node() .exec(res => { const node = res[0].node node.takeSnapshot({ type: 'arraybuffer', format: 'png', success: (res) => { fs.writeFileSync(savePath,res.data,'binary'); //图片保存至本地 wx.showShareImageMenu({ //唤起分享图片的界面 path:savePath }) }, fail(res) {} }) } }) 六、增强特性-worklet动画 worklet动画相比传统的方式,流畅度提升了不少,但如何使用呢?常见的普通动画无非是对于页面元素的平移,缩放,旋转等变换。那么,要让一个元素动起来,只需要做以下2件事: 将页面元素的样式与某个变量进行绑定,变量值的变化会自动触发样式的更新。实时动态地改变这个变量。结合快书的例子(下拉时,让页面缩小,松手后,页面弹回),来看一下具体的实现步骤。 [图片] 首先,如何绑定样式与参数呢?通过官方提供的一个applyAnimatedStyle函数: // Wxml: <view id="#box">content</box> // Page: this.scale = shared(1); //这里是定义一个共享变量,即可在UI线程和JS线程间同步的变量。 this.applyAnimatedStyle(`#box`, () => { 'worklet'; // 声明这是一个worklet函数 return { transform: `scale(${this.scale.value})`, }; }); // 1、这里使用共享变量是为了让后续改变这个变量时,worklet的函数能捕获到。 // 2、#box你要动起来的元素 // 3、当this.scale.value变化时,会自动触发函数体的执行,从而改变#box的样式 第二步,下拉时,根据下拉的偏移量,改变这个scale的值。 this.scale.value = (evt.deltaY / 100) * 0.15; // 这里的evt.deltaY是下拉时的位置偏移量,然后根据偏移量按比例计算缩放的值 // 如何获取这个下拉偏移量?下一小节讲手势系统时会讲到 第三步,松手时,复原scale的值。 this.scale.value = timing(1, { duration: 300, easing: Easing.ease }); // timing函数表示:在300毫秒内,scale.value会逐渐变成1 // easing: Easing.ease 表示缓动的方式,具体可参考https://easings.net // 如何知道已经松手了?下一小节讲手势系统时会讲到 更多动画参考请查阅官方文档。 七、增强特性-手势系统 还是上面那个例子,我们只说了下拉时根据下拉的偏移量改变scale的值,那如何得到下拉的偏移量呢?这里就涉及到了手势系统。下面讲讲如何让一个元素能响应拖动,缩放等手势。 说回上一小结的例子,我们只需要讲#box元素包裹在手势组件vertical-drag-gesture-handler即可。更多示例可查阅官方文档 // wxml: <vertical-drag-gesture-handler worklet:ongesture="handlePan"> <view id="box"></view> <vertical-drag-gesture-handler> // page: handlePan(evt) { 'worklet' if (evt.state === GestureState.ACTIVE) { // 拖拽时 if (evt.deltaY > 0) { // 下拉 this.scale.value = Math.max(this.scale.value - (evt.deltaY / 100) * 0.15, 0.85); } else { // 上拉 this.scale.value = Math.min(this.scale.value - (evt.deltaY / 100) * 0.15, 1); } } else if (evt.state === GestureState.END || evt.state === GestureState.CANCELLED) { // 拖拽结束或取消 this.scale.value = timing(1, { duration: 300, easing: Easing.ease }); } }, 然而,当手势组件与scroll-view等可以滚动的组件嵌套时,会出现冲突的问题。比如,同上一小节的示例,为了让文章内容过长时可以滚动,我们需要将文章的内容放在scroll-view中。当scroll-view已经滚动到顶部,再继续下拉的话,应当触发手势组件的拖拽事件,即缩放页面。相反,则继续滚动scroll-view。 [图片] // wxml: <vertical-drag-gesture-handler tag="pan" worklet:ongesture="handlePan" shouldResponseOnMove="shouldPanResponse" simultaneousHandlers="{{['scroll']}}"> <vertical-drag-gesture-handler tag="scroll" native-view="scroll-view" shouldResponseOnMove="shouldScrollResponse" simultaneousHandlers="{{['pan']}}"> <scroll-view type="list" scroll-y bindscroll="handleContentScroll">文章内容</scroll-view> </vertical-drag-gesture-handler> </vertical-drag-gesture-handler> // page: // 处理scroll-view的滚动事件,获取scrollTop的值 handleContentScroll(evt) { 'worklet' this.scrollTop.value = evt.detail.scrollTop; }, // return false 则表示scroll-view不再响应滚动事件 shouldScrollResponse(evt) { 'worklet'; const { deltaY } = evt if (this.scrollTop.value <= 0 && deltaY > 0) { //scroll-view已经滚动到顶部,继续下拉时 this.pan.value = true; return false; } if (this.scale.value < 1 && deltaY < 0) { //#box已经被缩放,继续上拉时 this.pan.value = true; return false; } this.pan.value = false; return true; }, shouldPanResponse() { 'worklet' return this.pan.value; // true表示响应手势组件的拖拽事件,false则不响应 }, 八、增强特性-自定义路由 以往在webview中,路由的的过渡动画仅支持从右到左,较为单调。在skyline之后,我们可以自定义路由的过渡动画了,比如常见的淡入淡出,从底部弹起等。比如以下这个例子,从首页点击图片,会跳转到分享的页面,这里就是用自定义路由实现的淡入效果。 [图片] 自定义路由的使用相比前几个特性稍微复杂一点,这里官方讲的更为具体和清晰,可查阅官方文档。唯一要注意的一点是,只有连续的skyline页面跳转时,才会有效果。 九、增强特性-共享元素动画 还是上面的例子,当从首页点击图片跳转到分享页面时,图片看起来像是从首页飞到了分享页,这里便是使用到了共享元素动画。我同时也做Flutter的开发,所以这里看起来非常类似Flutter的hero动画或者叫飞行动画。 使用方式也类似于Flutter。将2个页面的相似组件都用share-element组件包括起来,并且使用相同的key即可。再配合自定义路由,可使得飞行动画看起来非常的丝滑。 // A页面: <share-element key="唯一key"> <image src="imagePath" mode="aspectFill" /> </share-element> // B页面: <share-element key="唯一key"> <image src="imagePath" mode="aspectFill" /> </share-element> // 有几个要注意的地方 // 1、两个个页面的share-element组件必须使用相同的key。 // 2、key是唯一的,即同一个页面中,不能出现重复的key。 // 3、image不要写死宽高,应百分比100%,具体宽高数值写在share-element组件上。 有一个常见的问题,A页面是一个列表,B页面是详情页,列表中的数据都是通过接口从后台返回的,由于共享元素的key又不能重复,那么这个key怎么定义?一般后台返回的数据都会有一个唯一标识,假设为ID,我们可以用这个ID当作Key。 但是,另一个问题来了,如果数据是后台接口返回的,然后通过setData的方式响应到页面,那么很有可能B页面的首帧获取不到这个Key,因为这时候接口请求可能还未完成,那么动画也是不会生效的。针对这种情况,官方也提供了一种方式: 共享元素动画需保证下一个页面首帧即创建好 [代码]share-element[代码] 节点,并设置了 key,用于计算目标位置。如果是通过 [代码]setData[代码] 设置的,可能会错过首帧。针对这种情况,可以 使用 Component 构造器构造下一个页面,只要在组件 [代码]attached[代码] 生命周期前(含)通过 [代码]setData[代码] 设置上去,就会在首帧渲染 十、总结 Skyline还有一些其他有趣的特性,大家感兴趣的话可以查阅官方文档。总的来说,相比起webview,skyline对性能的提升是显而易见的,并且,一些在webview很难实现的效果,在skyline的基础上,也能轻易实现,开箱即用。目前,skyline还在不断地迭代中,还有许多的新特性还在评估和开发中,相信之后的版本会更完善更好用。 最后,大家多多使用快书呀,球球了~ [图片]
2023-09-01 - 关于 env(safe-area-inset-bottom)的适配问题?
如果才能做到iPhone全面屏情况下使用env(safe-area-inset-bottom)来适配底部安全距离,而如果是其他的安卓手机或IPhone手机,底部安全距离设置固定值,比如40rpx,因为iPhone的env(safe-area-inset-bottom)底部安全距离已经足够大了,如果使用calc(40rpx + env(safe-area-inset-bottom)),虽然安卓机或其他iPhone手机底部看起来挺协调,但iPhone的全面屏底部的空缺就会非常的大,请问有解决方法嘛?难道通过js判断当前设备的安全距离嘛
2023-07-19 - 小程序备案,不涉及相关类目承诺书
1.北京1)北京不涉及校外培训承诺书若你的公司/单位名称、互联网信息服务名称涉及“教育”、“培训”等关键字或其他与教育相关的文字,但互联网信息服务实际不从事校外培训相关内容,请上传《不涉及校外培训承诺书》,且承诺内容与实际情况必须保持一致。 下载模板 示例模板填写/上传说明[图片] 1. 填写小程序主办单位名称。 2. 描述小程序主办单位主要从事的业务内容。 3. 勾选小程序。 4. 填写小程序名称。 5. 描述小程序主要运营内容,应描述具体且有实际意义。 6. 填写小程序管理员(小程序负责人)姓名。 7. 填写小程序管理员(小程序负责人)身份证号码。 8. 填写小程序管理员(小程序负责人)联系电话号码。 9. 若小程序主办单位名称涉及教育、培训等相关文字,请勾选“单位名称”选项;若小程序名称涉及教育、培训等相关文字,请勾选“互联网信息服务名称”选项。。 10. 单位法定代表人签名(需手写正楷签名,接受签名章,不接受连笔签)。 11. 加盖单位公章(不接受合同章、项目章等)。 12. 填写日期应与提交备案日期一致,或小于1个月。 提示:法定代表人签字、公司或单位公章需清晰,方视为有效。承诺书成文时需将括号里的注解内容及下划线删除。 2)北京不涉及金融承诺书若你的公司名称、互联网信息服务名称涉及“投资、资产、资产管理、资本、股权投资(基金)、财务(财税)管理、财务咨询、财富管理、投资(财务、财税) 、融资、金融、金融服务、理财、贷款咨询、融资租赁、非融资性担保、互联网金融、网络借贷、P2P、借贷、支付、基金、股权众筹、投资基金、互联网保险、理财、交易所、网贷、网络借贷“等16个关键字或其他与金融相关的文字,但小程序实际不从事上述金融相关内容,请上传《不涉及金融承诺书》,且承诺内容与实际情况必须保持一致。 下载模板 示例模板填写/上传说明[图片] 1. 描述小程序主办单位主要从事的业务内容。 2. 勾选小程序。 3. 填写小程序名称。 4. 描述小程序主要运营内容,应描述具体且有实际意义。 5. 填写小程序管理员(小程序负责人)姓名。 6. 填写小程序管理员(小程序负责人)身份证号码。 7. 填写小程序管理员(小程序负责人)联系电话号码。 8. 若小程序主办单位名称涉及投资、资产等相关文字,请勾选“单位名称”选项;若小程序名称涉及投资、资产等相关文字,请勾选“互联网信息服务名称”选项。 9. 单位法定代表人签名(需手写正楷签名,接受签名章,不接受连笔签)。 10. 加盖公司/单位公章(不接受合同章、项目章等)。 11. 填写日期应与提交备案日期一致,或小于1个月。 提示:法定代表人签字、公司或单位公章需清晰,方视为有效。承诺书成文时需将括号里的注解内容及下划线删除。 2.湖北1)湖北省小程序电子商务情况说明书若你的单位名称、工商经营范围、小程序名称、小程序服务内容、小程序备注涉及“电子商务、商城、网上销售、零售、贸易”等关键字,实际小程序仅销售营业范围内产品,或者不销售任何产品,并且不涉及第三方商家入驻,无第三方付款等情况,请上传电子商务情况说明书。 下载模板 示例模板填写/上传说明[图片] 1. 填写备案小程序单位名称。 2. 填写本次备案小程序名称。 3. 描述小程序主要运营内容,应描述具体且有实际意义。 4. 单位法定代表人签名(需手写正楷签名,接受签名章,不接受连笔签)。 5. 加盖单位公章(不接受合同章、项目章等)。 6. 填写日期应与提交备案日期一致,或小于1个月。 2)湖北省小程序不开展教育培训承诺书若你的单位名称、工商经营范围、小程序名称、小程序服务内容、小程序备注涉及“教育、教育培训”等关键字,实际小程序不经营校外培训、学科类培训、教育培训等内容,请上传小程序不开展教育培训承诺书。 下载模板 示例模板填写/上传说明[图片] 1. 填写备案小程序单位名称。 2. 填写本次备案小程序名称。 3. 描述小程序主要运营内容,应描述具体且有实际意义。 4. 单位法定代表人签名(需手写正楷签名,接受签名章,不接受连笔签)。 5. 加盖单位公章(不接受合同章、项目章等)。 6. 填写日期应与提交备案日期一致,或小于1个月。 3.贵州1)贵州省小程序不涉及前置审批的承诺书若服务内容标识和小程序名称不涉及前置审批相关内容,仅单位名称、经营范围涉及前置审批关键字,可提供如下承诺书。若单位名称、经营范围涉及宗教和新闻单位必须提供资质。 下载模板 示例模板填写/上传说明[图片] 1. 填写本次备案小程序涉及的前置项目类别名称。 2. 填写备案小程序单位名称。 3. 填写备案小程序主办单位企业的统一社会信息用代码。 4. 填写本次备案小程序名称。 5. 描述小程序主要运营内容,应描述具体且有实际意义。 6. 填写本次备案小程序涉及的前置项目类别名称 7. 加盖单位公章(不接受合同章、项目章等)。 8. 单位法定代表人签名(需手写正楷签名,接受签名章,不接受连笔签)。 9. 填写日期应与提交备案日期一致,或小于1个月。 4.浙江1)小程序不涉及互联网金融承诺书仅单位名称、经营范围涉及金融关键字,但小程序实际不从事的,需配合提供【浙江】不涉及互联网金融承诺书。下载模板 示例模板填写/上传说明[图片] 1. 填写单位名称、经营范围涉及的金融相关关键词。 2. 描述小程序主要运营内容,应描述具体且有实际意义。 3. 请手抄如下内容(手写正楷字体):以上承诺如有违反,愿接受电信主管部门的依法处理,并承担一切法律责任。 4. 加盖单位公章(不接受合同章、项目章等)。 5. 单位法定代表人签名(需手写正楷签名,接受签名章,不接受连笔签)。 6. 填写日期应与提交备案日期一致,或小于1个月。 5. 其他省份1)小程序不涉及前置审批承诺书(普适版)若小程序服务类目和小程序名称不涉及前置审批相关内容,仅单位名称、经营范围涉及前置审批关键字,可提供如下承诺书。宗教和新闻类的主体必须提供资质。下载模板 示例模板填写/上传说明[图片] 1. 填写本次备案小程序涉及的前置项目类别名称。 2. 填写小程序备案省份。 3. 填写备案小程序单位名称。 4. 填写备案小程序主办单位企业的统一社会信息用代码。 5. 填写本次备案小程序名称。 6. 描述小程序主要运营内容,应描述具体且有实际意义。 7. 加盖单位公章(不接受合同章、项目章等)。 8. 填写日期应与提交备案日期一致,或小于1个月。 如需其他类目相关说明,可参考:https://help.aliyun.com/zh/icp-filing/support/pre-approval-letter-of-commitment?spm=a2c4g.11186623.0.0.7d98640fsOORk4
2023-12-11 - scroll-view滚动页面,input键盘弹出时,页面滚动到顶部,输入框内容错位问题。怎么解决?
从一个页面点击带一个id跳转到scroll-view问题的页面,通过传过来的id自动滚动到指定位置,然后点击输入框拉起键盘时,页面突然会滚动到顶部去了,输入框也飘了(输入框错位),跟着到顶部去了。然后我有使用input的adjust-position,键盘弹起时,不自动上推页面和获取input焦点时不滚动,失去焦点在滚,这俩种方法可以解决拉起输入框页面滚动到顶部的问题跟输入框错位的问题,但输入框错误还有点问题,点击输入框获取焦点他会闪一下,先错位在恢复,请问各位大大有什么好的方法解决这个问题吗,或者有什么方法解决先错误在恢复的问题 =。=[图片]
2020-09-02 - swiper禁止手动滑动解决方案?
swiper禁止手动滑动解决方案? ~ [图片] ~ [图片] 该方案可行,已亲测通过 [图片]
2021-06-23 - 云函数使用云存储的word模板文件生成新的word
今天遇到一个场景是需要使用云数据库里的数据生成word文件,word文件有模板,在这里做一下经验分享,如果各位大佬有其他方法,欢迎分享,下面直接上过程 1、需要的插件 使用“npm install docx-templates”命令安装 右键云函数,选择“在内建终端中打开”,执行命令 [图片] 2、云函数实现 因为云函数不能直接读取云存储的文件,所以这里先下载然后读取 exports.main = async (event, context) => { const word = cloud.downloadFile({ fileID:'这里使用你云存储的fileID' }) const template = (await word).fileContent const buffer = await createReport({ template, data: { name: '替换的内容', // 这里的key值和你在word模板里面写的要一致,可以有多个 }, cmdDelimiter: ['{', '}'] // 分隔符 }) const time = new Date(); const preDir = time.getFullYear()+"/"+(time.getMonth()+1)+"/"+time.getDate() const stringRandom = require('string-random') const randfilename = stringRandom(32) //随机文件名 const cloudPath = `templates/docx/${preDir}/${randfilename}.docx` //文件 return await cloud.uploadFile({ cloudPath, fileContent: Buffer.from(buffer, 'hex') }) } word模板文件内容(根据自己的实际使用去修改) [图片] 使用后生成的新的文件内容 [图片] 备注:没有找到云函数直接操作云存储的方法,各位大神如果知道,可以发享一下,欢迎评论留言 在解决这个问题搜到的比较有用的文章链接:https://blog.csdn.net/xjc8289555/article/details/118084368这个里面的代码我在使用上发现他的文件读取会有一点问题(云环境的原因),所以做了一些改动,但是确实是帮助到我了
2022-03-18 - 如何使用微信小程序·云开发的Node.js云函数生成Word文档(2021-10-15更新)
编者按 近期一个云开发项目有生成Word文档的需求,经过搜索,发现并没有小程序·云开发有关生成word文档的案例,因为本人还是本科生且非科班出身,一路摸着石头过河,遇到了不少困难,期间还试图向社区的大佬们求助;花了两天时间才搞定这一百行代码,现在分享给大家。 代码有些糙,希望大佬们不要嫌弃。 一、安装云函数依赖officegen、fs 工欲善其事必先利其器,我们知道云函数代码运行在云端Node.js环境中,因此,理论上来说,Node.js能做的事情,小程序·云开发的云函数基本上也能做到。officegen是Github上一款生成微软Office文档的工具,包括.docx、.xlsx、.pptx三种文件,由于我只用了.docx,本文将以Word文件为例。 https://github.com/Ziv-Barber/officegen [图片] 1. 首先我们在微信开发者工具中 新建一个云函数 => 右键云函数名 => 在终端中打开 [图片] 2. npm安装依赖officegen和fs,为了方便本地调试云函数,我们这里也安装wx-server-sdk。 [图片] 代码如下,请逐个安装,如果安装有问题,可以自行搜索“npm”或“npm taobao 镜像” ;这里不再赘述。 npm i officegen npm i fs npm i wx-server-sdk 3. 在云函数index.js开头写下以下代码,引用我们刚刚安装的包。 const cloud = require('wx-server-sdk') const officegen = require('officegen'); const fs = require('fs'); const docx = officegen('docx'); 二、创建Word文档的内容 文档地址: https://github.com/Ziv-Barber/officegen/blob/master/manual/docx/README.md 1. 首先我们根据文档定义(Ctrl CV)两个函数 //文档生成完成后调用,后来其实发现没啥用 // Officegen calling this function after finishing to generate the docx document: docx.on('finalize', async function (written) { console.log('Finish to create a Microsoft Word document.') }) //生成文档出现问题时调用 // Officegen calling this function to report errors: docx.on('error', function (err) { console.log(err) }) 2. 创建段落API: docx.createP(options) //声明一个创建段落的变量p0bj let pObj = docx.createP(options) //创建一个段落并插入文本 pObj = docx.createP({ align: 'center' //文字对齐方式,center、justify、right;默认为left indentLeft = 1440; // 段落缩进 Indent left 1 inch indentFirstLine = 440; // 首行缩进 }) pObj.addText('你要插入的文字,这里可以时变量', { bold: true, //是否加粗,默认false font_face: 'KaiTi', //字体,这里以“楷体为例”,如果填写了打开文档的电脑没有安装的字体名称,将使用默认字体。能不能用中文,我没试过。 font_size: 19, //字号 color: '595959' //文字颜色 }); 上述例子外,还可以添加下划线、设置斜体、超链接、分页等;还可以编辑页眉和页脚、插入图片等。详见后续代码示例或officegen文档。 3. 插入图片 这里以插入小程序码为例,直接上代码。 要注意的是officegen似乎不支持以buffer形式插入图片,因此要先将图片保存。 //首先定义一个用于保存小程序码图片的函数 //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } //要获取小程序码,首先要修改云函数config.json文件中的云调用权限 { "permissions": { "openapi": [ "wxacode.getUnlimited" ] } } //在云函数main中获取小程序码 //https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.get.html const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', //小程序页面地址,必须是线上版本中存在的页面的完整地址 scene: '', //小程序码参数 width: 240, //小程序码的宽度(是个正方形) }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // 这里的fileData是Buffer类型,关于路径会在第三部分生成Word文件中解释。 //将图片插入到文档中 pObj = docx.createP() //创建段落 pObj.options.indentFirstLine = 440; //首行缩进 pObj.addImage('/tmp/qr.jpg', { //图片文件路径 cx: 140, //长度 cy: 140 //宽度 }); 三、生成Word文件 文档内容完成后,就可以生成文档了。officegen似乎只能生成文件,没有文件buffer的接口,而要上传到小程序·云开发的云存储中,只能使用Buffer或fs.ReadStream,怎么办呢?先把文件保存下来再读取呗。 首先提一下云函数运行环境 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/mechanism.html 云函数运行在云端 Linux 环境中,一个云函数在处理并发请求的时候会创建多个云函数实例,每个云函数实例之间相互隔离,没有公用的内存或硬盘空间。云函数实例的创建、管理、销毁等操作由平台自动完成。每个云函数实例都在 [代码]/tmp[代码] 目录下提供了一块 [代码]512MB[代码] 的临时磁盘空间用于处理单次云函数执行过程中的临时文件读写需求,需特别注意的是,这块临时磁盘空间在函数执行完毕后可能被销毁,不应依赖和假设在磁盘空间存储的临时文件会一直存在。如果需要持久化的存储,请使用云存储功能。因此,我们将文件保存在/tmp路径下,文件名随便起,这里我取为exampl.docx。生成文档的代码如下: // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/example.docx') // Async call to generate the output file: docx.generate(out) 理论上来说,我们文档生成完毕后,通过fs.ReadFileStream读取文件调用cloud.uploadFile()即可上传到云存储 const fileStream = fs.createReadStream('/tmp/example.docx') return await cloud.uploadFile({ cloudPath: '/tmp/example.docx', fileContent: fileStream, }) 而在测试过程中我发现,云端测试时,云函数调用超时。而后使用本地调试查看问题出在何处。 云函数本地调试的方法不再赘述,看这里即可。https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/local-debug.html 通过本地调试,发现cloud.uplodaFile()的网络请求始终时挂起(pending)状态,没有传输数据。 [图片] 经过一天的调试,通过监听文件,发现officegen生成文件完成,执行了我们开头复制粘贴的生成文档后执行的docx.on("finalize",)函数,打印文档生成成功的日志后,仍有文件变动,也就是说,文件并没有生成完毕。这就导致了后续步骤的失败。 当时调试的界面我没有保存,就贴一下fs监听文件的代码吧。 let watcherObj = '/tmp/example.docx' //eventType 可以是 'rename' 或 'change'; 当改名或出现或消失的时候触发rename; recursive:是否监听到内层子目录,默认false; try { let myWatcher = fs.watch(watcherObj,{encoding:'utf8',recursive:true},(event,filename) => { if(event == 'change'){ console.log("触发change事件") } console.log(event) //encoding:文件名编码格式,buffer、默认:utf8等;filename有可能为空 if(filename){ console.log('filename: ' + filename) } }) //change 事件会触发多次 myWatcher.on('change',function(err,filename){ console.log(filename + '发生变化'); }); //50秒后 关闭监视 setTimeout(function(){ myWatcher.close() },5000); } catch (error) { console.log('文件不存在!!') } 为解决这一问题,我最先想到了await,结果发现await对officegen生成文档的接口并不起作用;最终我用了最原始的笨办法:用setTimeout等一会儿再读取文件,大佬们有更好的解决方案还请赐教。 return new Promise((resolve, reject) => { setTimeout(async function () { let data = fs.readFileSync('/tmp/example.docx'); let bufferData = new Buffer.from(data, 'base64'); console.log(bufferData); setTimeout(async function () { resolve(await cloud.uploadFile({ cloudPath: varpath, fileContent: bufferData, })); }, 1000); //等文件再读1秒 }, 6300); //等文件再写一会儿。根据自己的需求调试后确定等待时长,要预留出一定时间确保文档完全生成完毕。 }) //最终返回内容为文件云存储中的CloudID。 四、完整核心代码 const cloud = require('wx-server-sdk') const officegen = require('officegen'); const fs = require('fs'); const docx = officegen('docx'); cloud.init({ env: '这里填入你的云环境' }) // Officegen calling this function after finishing to generate the docx document: docx.on('finalize', async function (written) { console.log('Finish to create a Microsoft Word document.') }) // Officegen calling this function to report errors: docx.on('error', function (err) { console.log(err) }) //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } // 云函数入口函数 exports.main = async (event, context) => { var time = new Date() var filePath = 'exportVoluntaryData' var fileName = "zyzm" + Date.parse(new Date()) + '.docx' var varpath = filePath + '/' + fileName //get QRcode const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', scene: item._id, width: 240, }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // Add a Footer: var footer = docx.getFooter().createP(); footer.addText('XXXX证明_' + item._id, { font_size: 10 }); footer = docx.getFooter().createP(); footer.addText(time.toString(), { font_size: 10 }); //下方开始文档每一页的循环 for (var i in item.volunteerInfo) { //标题 let pObj = docx.createP({ align: 'center' }) pObj.addText('XXX证明', { bold: true,XXX font_face: 'KaiTi', font_size: 19, color: '595959' }); //此处省略了一些正文内容 pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('微信扫描下方小程序码,可核验此证明。', { font_face: 'FangSong', font_size: 12, color: '595959', italic: true, }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addImage('/tmp/qr.jpg', { cx: 140, cy: 140 }); pObj = docx.createP() pObj = docx.createP({ align: 'right' }) pObj.addText('落款', { font_face: 'FangSong', font_size: 15, color: '595959' }); if (i != ((item.volunteerInfo).length - 1)){ docx.putPageBreak() //分页 } } // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/example.docx') // Async call to generate the output file: docx.generate(out) return new Promise((resolve, reject) => { setTimeout(async function () { let data = fs.readFileSync('/tmp/example.docx'); let bufferData = new Buffer.from(data, 'base64'); console.log(bufferData); setTimeout(async function () { resolve(await cloud.uploadFile({ cloudPath: varpath, fileContent: bufferData, })); }, 1000); }, 6300); }) } 本人非计算机相关专业本科生,且本文大部分内容为手打,难免会有差错和疏漏,还请各位指教。 希望本文对你有所帮助。 Soochow University. HaoChen. 2020年2月 ======= 2021-10-15更新 ======= 经过一段时间的使用,上述内容主要存在两点问题:(1)难以判断文件何时生成完毕;(2)连续调用生成文档时,若上一个云函数实例未被销毁,会出现文件内容重复和错乱的问题。 前一段时间进行了更新,因为工作学习忙碌,此次暂不做详解,代码如下。 入口文件index.js// 云函数入口文件 delete require.cache[require.resolve('officegen')]; const cloud = require('wx-server-sdk') var office = require('office.js'); //https://github.com/Ziv-Barber/officegen/blob/master/manual/docx/README.md cloud.init({ env: 'sudaxmt1900' }) const db = cloud.database() const _ = db.command // 云函数入口函数 exports.main = async (event, context) => { return await office.genWord(event); } office.jsconst cloud = require('wx-server-sdk') const fs = require('fs'); function delDir(path) { console.log("delete Dir") let files = []; if (fs.existsSync(path)) { files = fs.readdirSync(path); files.forEach((file, index) => { let curPath = path + "/" + file; if (fs.statSync(curPath).isDirectory()) { delDir(curPath); //递归删除文件夹 } else { fs.unlinkSync(curPath); //删除文件 } }); // fs.rmdirSync(path); // 删除文件夹自身 } } readDocx_fs = function (path) { return new Promise((resolve, reject) => { fs.readFile(path,(err,data)=>{ resolve(data); reject(err); }) }) } //save QR saveFile = function (filePath, fileData) { return new Promise((resolve, reject) => { const wstream = fs.createWriteStream(filePath); wstream.on('open', () => { const blockSize = 128; const nbBlocks = Math.ceil(fileData.length / (blockSize)); for (let i = 0; i < nbBlocks; i += 1) { const currentBlock = fileData.slice( blockSize * i, Math.min(blockSize * (i + 1), fileData.length), ); wstream.write(currentBlock); } wstream.end(); }); wstream.on('error', (err) => { reject(err); }); wstream.on('finish', () => { resolve(true); }); }); } exports.genWord = async (event) => { let officegen = require('officegen'); let fs = require('fs'); let docx = officegen('docx'); //ini delDir('/tmp') var item = event.item var filePath = 'exportVoluntaryData' var fileName = "21zyzm" + Date.parse(new Date()) + '.docx' var varpath = filePath + '/' + fileName //=========以下建构文档内容========== //get QRcode const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/check/check', scene: item.id, width: 140, }) const QRcode = result.buffer await saveFile('/tmp/qr.jpg', QRcode); // 这里的fileData是Buffer类型 timeBottom = time.getFullYear() + '年' + (time.getMonth() + 1) + '月' + time.getDate() + '日' for (var i in item.volunteerInfo) { let pObj = docx.createP({ align: 'center' }) pObj = docx.createP({ align: 'center' }) pObj.addText('志愿服务时间证明', { bold: true, font_face: 'KaiTi', font_size: 19, color: '595959' }); pObj = docx.createP() pObj = docx.createP({ align: 'justify' }) pObj.options.indentFirstLine = 440; if (item.volunteerInfo[i].academy && item.volunteerInfo[i].major && item.volunteerInfo[i].grade) { var txt = item.volunteerInfo[i].academy + ' ' + item.volunteerInfo[i].major + '专业 ' + item.volunteerInfo[i].grade + ' ' + item.volunteerInfo[i].name + ' 同学(学号 ' + item.volunteerInfo[i].idnum + '),于 ' + date + '参加 ' + item.title + ' 工作,志愿服务时间达到 ' + item.hours + ' 小时。' } else { var txt = item.volunteerInfo[i].name + ' 同学(学号 ' + item.volunteerInfo[i].idnum + '),于 ' + date + '参加 ' + item.title + ' 工作,志愿服务时间达到 ' + item.hours + ' 小时。' } pObj.addText(txt, { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('特此证明。', { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('证明人:' + event.tea_info.name + ' ' + event.tea_info.phone, { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP() pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addText('微信扫描下方小程序码,可核验此证明。核验信息与此证明一致时,此证明不加盖公章仍然有效;若不一致,则以加盖公章的证明为准。', { font_face: 'FangSong', font_size: 12, color: '595959', italic: true, }); pObj = docx.createP() pObj.options.indentFirstLine = 440; pObj.addImage('/tmp/qr.jpg', { cx: 140, cy: 140 }); pObj = docx.createP() pObj = docx.createP() pObj = docx.createP({ align: 'right' }) pObj.addText('XXXXX', { font_face: 'FangSong', font_size: 15, color: '595959' }); pObj = docx.createP({ align: 'right' }) pObj.addText(timeBottom, { font_face: 'FangSong', font_size: 15, color: '595959' }); // Add a Footer: pObj = docx.createP() pObj = docx.createP() pObj = docx.createP() pObj.addText('XXXXX证明_' + item._id, { font_face: 'FangSong', font_size: 10, color: '808080' }); pObj = docx.createP() pObj.addText(time.toString(), { font_face: 'FangSong', font_size: 10, color: '808080' }); if (i != ((item.volunteerInfo).length - 1)) { docx.putPageBreak() } } //=======================建构文档内容结束========================= // Let's generate the Word document into a file: let out = fs.createWriteStream('/tmp/' + fileName) return new Promise((resolve, reject) => { docx.generate(out); out.on('close', async function(){ console.log("文件已被关闭,总共写入字节", out.bytesWritten) // console.log('写入的文件路径是'+ out.path); var fileBuf = await readDocx_fs(out.path); var upd = await cloud.uploadFile({ cloudPath: varpath, fileContent: fileBuf, }); console.log(docx) resolve({ event, upd, size: Math.floor(100*out.bytesWritten/1024)/100 + "KB" }) }); out.on('error', (err) => { console.error(err); reject({ errMsg: err }) }); }) }
2021-10-15 - 图片安全检测data exceed max size解决方案
最近在重构小程序恋爱小清单,在用云函数做图片的安全检测时报了一个错:cloud.callFunction:fail Error: data exceed max size 也就是图片超过了大小限制。 早期的版本是通过画布将图片缩小(wx.canvasToTempFilePath),接着读取文件流(wx.getFileSystemManager().readFile),然后再提交云函数检测,过程感觉有些繁琐复杂 最近发现其实有更简单的方法,可以借助临时的CDN,传递大数据,最终在云函数端会收到一个CDN地址,接着通过request-promise读取文件流,然后再做安全检测,相比旧版的方法个人感觉简单清爽不少。 参考官方文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/utils/Cloud.CDN.html 代码如下: 小程序端: const api = require("api.js"); /** * 图片安全检测 * 借助临时CDN传递大数据 * @param filePath 图片的临时文件路径 (本地路径) * @returns {Promise<unknown>} */ const imgSecCheckViaCDN = (filePath) => { return new Promise(function (resolve, reject) { api.callCloudFunction("securityCheck", { type: "imgSecCheckViaCDN", imgData: wx.cloud.CDN({ type: "filePath", filePath, }) }, res => { console.log("图片安全检测结果:", JSON.stringify(res)); const result = res.result; if (result.success) { resolve(result); } else { reject(result); } }, reject); }); } api.js /** * 云函数调用 * @param name * @param data * @param success * @param fail * @param complete */ const callCloudFunction = function (name, data, success, fail, complete) { //执行云函数 wx.cloud.callFunction({ // 云函数名称 name: name, // 传给云函数的参数 data: Object.assign({}, data, {env: env.activeEnv}) }).then(res => { typeof success == 'function' && success(res); }).catch(res => { typeof fail == 'function' && fail(res); }).then(res => { typeof complete == 'function' && complete(res); }); }; module.exports = {callCloudFunction} 云函数端: // 云函数入口文件 const cloud = require('wx-server-sdk'); const responce = require('easy-responce'); const requestHelper = require('./utils/requestHelper'); const headers = { encoding: null, headers: { "content-type": "application/octet-stream", // "content-type": "video/mpeg4", }, }; // 云函数入口函数 exports.main = async (event, context) => { cloud.init({ env: event.env }); let result = {}; try { const {type, content, imgData} = event; let {buffer} = event; console.log("检测类型:", type, "文本内容:", content, "图片内容:", imgData); switch (type) { case "imgSecCheckViaCDN": const imageResponse = await requestHelper.request(imgData, headers, {}); buffer = imageResponse.body; case "imgSecCheck": result = await cloud.openapi.security.imgSecCheck({ media: { contentType: 'image/png', // value: Buffer.from(imgBase64, "base64") value: Buffer.from(buffer) } }); break; case "msgSecCheck": result = await cloud.openapi.security.msgSecCheck({content}); break; default: console.log("不支持的检测类型:", type); break; } } catch (e) { console.error(e); result = e; } console.log("检测结果:", result); const {errCode, errMsg} = result; return errCode !== 87014 ? responce.success({errCode}) : responce.fail(errMsg); }; requestHelper.js const rp = require('request-promise'); /** * http请求 * @param url * @param options * @param data * @param autoFollowRedirect * @returns {Promise<unknown>} */ const request = function (url, options, data, autoFollowRedirect = true) { return new Promise(function (resolve, reject) { const p = Object.assign({ json: true, resolveWithFullResponse: true, followRedirect: autoFollowRedirect }, options, data, {url}); console.log("请求参数:", JSON.stringify(p)); return rp(p) .then(async function (repos) { //console.log("获取到最终内容,执行回调函数:", repos); return resolve(repos); }) .catch(async function (err) { if (err && (err.statusCode === 301 || err.statusCode === 302)) { // console.log("停止重定向,重定向信息:", err); console.log("停止重定向"); return resolve(err); } console.error("重定向失败:", err); return reject(err); }); }); } module.exports = {request }
2022-10-21 - 小程序全局状态管理工具
项目说明 原生微信小程序全局状态管理工具,轻量,便捷,高性能,响应式。 npm链接 :https://www.npmjs.com/package/@savage181855/mini-store 安装 [代码]npm i @savage181855/mini-store -S [代码] 快速入门 在[代码]app.js[代码]文件调用全局 api,这一步是必须的!!! [代码]import { proxyPage, proxyComponent } from "@savage181855/mini-store"; // 代理页面,让页面可以使用状态管理工具 proxyPage(); // 代理页面,让组件可以使用状态管理工具 proxyComponent(); // 这样子就结束了,很简单 [代码] 定义[代码]store.js[代码]文件,模块化管理 [代码]import { defineStore } from "@savage181855/mini-store"; const useStore = defineStore({ state: { count: 0, }, actions: { increment() { this.count++; }, }, }); export default useStore; [代码] [代码]indexA.js[代码]页面 [代码]// 导入定义的 useStore import useStore from "../../store/store"; Page({ // 注意:这里使用 useStore 即可,可以在this.data.store 访问 store useStoreRef: useStore, // 表示需要使用的全局状态,会自动挂载在到当前data里面,自带响应式 mapState: ["count"], // 表示想要映射的全局actions,可以直接在当前页面调用 ,例如:this.increment() mapActions: ["increment"], watch: { count(oldValue, value) { // 可以访问当前页面的实例 this console.debug(this); console.debug(oldValue, value, "count change"); }, }, onIncrement1() { // 不推荐 this.data.store.count++; }, onIncrement2() { this.data.store.patch({ count: this.data.store.count + 1, }); }, onIncrement3() { this.data.store.patch((store) => { store.count++; }); }, onIncrement4() { this.data.store.increment(); }, }); [代码] [代码]indexA.wxml[代码] [代码]<view> <view>indexA</view> <view>{{count}}</view> <button type="primary" bindtap="increment">+1</button> <button type="primary" bindtap="onIncrement1">+1</button> <button type="primary" bindtap="onIncrement2">+1</button> <button type="primary" bindtap="onIncrement3">+1</button> <button type="primary" bindtap="onIncrement4">+1</button> </view> [代码] [代码]indexB.js[代码]页面 [代码]// 导入定义的 useStore import useStore from "../xxxx/store.js"; Page({ // 注意:这里使用 useStore 即可,可以在 this.data.store 访问 store useStoreRef: useStore, // 表示需要使用的全局状态,会自动挂载在到当前data里面,自带响应式 mapState: ["count"], }); [代码] [代码]indexB.wxml[代码] [代码]<view> <view>indexB</view> <view>{{count}}</view> </view> [代码] 全局混入 [代码]app.js[代码]文件 [代码]import { proxyPage, proxyComponent } from "@savage181855/mini-store"; // 这里的配置可以跟页面的配置一样,但是有一些规则 // 'onShow', 'onReady', 'onHide', 'onUnload', 'onPullDownRefresh', 'onReachBottom', // 'onPageScroll', 'onResize', 'onTabItemTap'等方法,全局的和页面会合并,其余的方法,页面会覆盖全局的。 proxyPage({ onLoad() { console.debug("global onLoad"); }, onReady() { console.debug("global onReady"); }, onShow() { console.debug("global onShow"); }, onShareAppMessage() { return { title: "我是标题-- 全局", }; }, }); // 这里的配置可以跟组件的配置一样,但是有一些规则 // 'created','ready','moved','error','lifetimes.created','lifetimes.ready', // 'lifetimes.moved','lifetimes.error','pageLifetimes.show','pageLifetimes.hide', // 'pageLifetimes.resize'等方法,全局的和组件会合并,其余的方法,组件会覆盖全局的。 proxyComponent({ lifetimes: { created() { console.debug("global lifetimes.created"); }, }, }); [代码] 代码片段 https://developers.weixin.qq.com/s/ZO0SX2mr7xDj
2022-10-15 - 如何实现将批量生成的二维码图片,统一保存在一个文件中(PDF/WORD/EXCEL均可)?
一个场景是,我现在能批量生成二维码图片并且都保存到相册里,因为后面用户要打印,需要全部传到电脑里在手动排版,然后用用户提出能不能生成的时候就是一个文件包含所有图片,pdf或者word都可以,我不知道能不能实现,大家有没有遇到类似情况?
2021-08-16 - 微信小程序环境共享,多个小程序共享一个云开发数据库
我们在做小程序开发时,有时候需要多个小程序公用一个数据库,比如我们做一个外卖小程序,要配套一个骑手小程序,这个时候就要两个小程序公用一个云开发环境,公用一个数据库了。所以今天来教下大家如何多个小程序共享一个云开发环境和数据库。 其实官方给的文档很详细了,但是一个细节官方没有讲到,所以就会导致好多同学做多个小程序共享一个云开发环境时,遇到各种各样的问题。 比如下面这样的问题 [图片] 明明感觉自己按照官方要求,该配置的都配置了啊,但是为啥就是出错呢。所以我这里再带大家完整的配置一遍,把里面的一些注意事项也给大家好强调下。 一,准备条件 1-1,必须同一个主体 首先看官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/resource-sharing/ [图片] 要共享云开发资源可以 ,但是必须是同一个主体。什么是同一个主体呢,就是两个小程序必须都是你自己的,或者是你公司的。 如果不是同一个主体,会报如下错误 [图片] 1-2,最新的基础库,最新版开发工具 这里记得调到最新的基础库,开发者工具也尽量用最新的 [图片] 开发者工具这里官方是有要求的 [图片] 二,开通环境共享 我这里以两个小程序共享一个数据库为例 小程序A [图片] 小程序B [图片] 大家这里记得我们是小程序A 共享数据库给小程序B 2-1,开通环境共享 开通,使用 1.03.2009140 或以上版本的开发者工具,进入云控制台,到 “设置 - 拓展能力 - 环境共享” 点击开通即开通环境共享能力 [图片] 2-2,开通后授权给别的小程序 [图片] 环境共享开通后将在顶部tab显示环境共享功能,进入 “环境共享” 的页面,点击“添加共享”,即可授权同主体下其他小程序/公众号使用当前小程序下的云开发资源 [图片] 这里填写你要共享小程序的appid,我们这里取小程序B的appid [图片] 授权,选择共享的云环境,默认选中所有环境操作权限,可根据实际使用场景自定义授权。这里建议保持默认即可 [图片] 比如我这里分享给小程序B(编程小石头) [图片] [图片] 2-3,使用共享的云开发环境 我们上面操作好以后,就可以在小程序B的云开发后台看到共享的云开发环境了。将我们的云开发环境切换下就可以查看和使用共享的资源了。 [图片] 可以看到小程序B(编程小石头)可以查看小程序A的数据库了 [图片] 三,请求共享的数据库 我们接下来就在小程序B里调用小程序A的数据库了。官方提示的是调用之前要在小程序A里创建一个如下的云函数,但是我在测试的时候发现不用创建也可以的。 [图片] 所以我们就先不创建cloudbase_auth 云函数,来看看能不能调取到数据。 3-1,初始化云开发环境 我们小程序B想使用小程序A的云开发环境,这里要注意,初始化的时候要如下面注释里写的一样,用小程序A的appid和云开发环境id [图片] 3-2,调用资源方数据 初始化以后不能想正常调用云开发数据库那样了,会报错 [图片] 所以我们这里要改变下使用方法。如下 [图片] 这时候还会报错,是因为我们忽略了官方的一个要求:“ 跨账号调用,必须等待 init 完成”,所以我们必须给init加一个await语法,如下,记得await要结合着async一起使用。 [图片] 可以看到我们成功的请求到了小程序A的数据。直接get的时候记得改下数据库权限奥。 [图片] 代码贴出来给大家,记得改成自己的配置 [代码]Page({ async onLoad() { // 声明新的 cloud 实例 var c1 = new wx.cloud.Cloud({ // 资源方 小程序A的 AppID resourceAppid: 'wx7c54942dfc87f4d8', // 资源方 小程序A的 的云开发环境ID resourceEnv: 'test-ec396a', }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 await c1.init() // wx.cloud.database().collection('xiaoshitou').get() c1.database().collection('xiaoshitou').get() .then(res => { console.log('共享环境请求数据成功', res) }) } }) [代码] 四,调用共享环境的云函数 4-1,调用资源方里的云函数 我们这里在小程序B(编程小石头)里调用小程序A里的云函数试试。 如小程序A里有一个xiaoshitou的云函数 [图片] 可以看到我们可以成功的调用小程序A里的xiaoshitou云函数 [图片] 是不是很简单。今天就给大家讲到这里了,欢迎关注,后面会分享更多小程序开发的知识给大家。
2022-02-24 - 微信小程序未来可能支持路径别名写法吗?
小程序的require函数当前只支持相对路径的文件引用,当目录层级很深时,如果需要引用一个靠近根目录的文件,容易导致路径特别长,例如require("../../../../../../a.js"),希望官方未来能支持在app.json中支持配置alias,用于替换路径中过长的“../”
2022-03-09 - 小程序 怎么封装一个公共的js类?
在开发中有遇到所有的页面都用引用同一个公共的js类 然后想知道,,, 怎么可以在js类中,让类中的所有方法都成为app.js 中的一个属性或方法呢? 只要在 app.js 中引用一次,比如: var tdweapp = require('./utils/tdweapp.js') // TalkingData-sdk 其他页面就不需要再引用了,只要在其他页面的头部 写上 const app = getApp() 然后在方法中只要调用 app.td_app_sdk.share title: app.shareTitle, path: '/pages/index/idnex' }) 就可以操作这个js类中的方法。。。。
2019-05-29 - 在云函数中可以调用fs.readFile和fs.writeFile来读写文件吗?
在云函数中可以调用fs.readFile和fs.writeFile来读写文件吗? 我试了一下,报错如下: 日志 START error when writing file: { "errno": -30, "code": "EROFS", "syscall": "open", "path": "/var/user/node_modules/ffprobe-static/bin/linux/x64/temp.mov" }
2021-11-23 - 云开发可以做 企业付款可以到零钱吗?
企业付款到零钱功能提供由商户付款至用户微信零钱的能力 https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1 云开发可以做 企业付款可以到零钱吗?
2020-08-07 - 几行代码实现小程序云开发提现功能
先看效果: [图片] 纯云开发实现,下面说使用步骤: 一:开通商户的企业付款到领取功能 说明地址: https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1 使用条件 1、商户号(或同主体其他非服务商商户号)已入驻90日 2、截止今日回推30天,商户号(或同主体其他非服务商商户号)连续不间断保持有交易 使用条件是第一难,第二难在下面这里 [图片] 在网上找了很多,感觉是云开发这里的一个不完善地方,如果不填ip,会报这种错 [代码]{"errorCode":1,"errorMessage":"user code exception caught","stackTrace":"NO_AUTH"} [代码] [代码]<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[此IP地址不允许调用接口,如有需要请登录微信支付商户平台更改配置]]></return_msg> <mch_appid><![CDATA[wx383426ad9ffe1111]]></mch_appid> <mchid><![CDATA[1536511111]]></mchid> <result_code><![CDATA[FAIL]]></result_code> <err_code><![CDATA[NO_AUTH]]></err_code> <err_code_des><![CDATA[此IP地址不允许调用接口,如有需要请登录微信支付商户平台更改配置]]></err_code_des> </xml> [代码] 云开发没有ip这个概念,所以这里有些无从下手,不过这里我采用了个替代方案,参考了社区帖子: https://developers.weixin.qq.com/community/develop/doc/00088cff3a40d87d80f7267b65b800 之后我也亲自验证了,基本上就是这几个,当然肯定不够,但是可以自己在逻辑上进行处理,ip以下: [代码]172.81.207.12 172.81.212.74 172.81.236.99 172.81.235.12 172.81.245.51 212.64.65.131 212.64.84.22 212.64.85.35 212.64.85.139 212.64.87.134 [代码] 接着,可以动手了 二、云开发部分 1、设置云存储 证书配置地址: [图片] 下载后有三个文件,我们只需要p12结尾的那个 [图片] 然后,将这个apiclient_cert.p12文件上传到你的云存储 [图片] 这里处理完了,我们只需要一个东西,就是fileID也就是常说的云存储ID(上图红框内容) 2、配置云函数 新建云函数ref云函数 [图片] 代码如下: [代码]const config = { appid: 'wx383426ad9ffe1111', //小程序Appid envName: 'zf-shcud', // 小程序云开发环境ID mchid: '1111111111', //商户号 partnerKey: '1111111111111111111111', //此处填服务商密钥 pfx: '', //证书初始化 fileID: 'cloud://zf-shcud.11111111111111111/apiclient_cert.p12' //证书云存储id }; const cloud = require('wx-server-sdk') cloud.init({ env: config.envName }) const db = cloud.database(); const tenpay = require('tenpay'); //支付核心模块 exports.main = async(event, context) => { //首先获取证书文件 const res = await cloud.downloadFile({ fileID: config.fileID, }) config.pfx = res.fileContent let pay = new tenpay(config,true) let result = await pay.transfers({ //这部分参数含义参考https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 partner_trade_no: 'bookreflect' + Date.now() + event.num, openid: event.userinfo._openid, check_name: 'NO_CHECK', amount: parseInt(event.num) * 100, desc: '二手书小程序提现', }); if (result.result_code == 'SUCCESS') { //如果提现成功后的操作 //以下是进行余额计算 let re=await db.collection('user').doc(event.userinfo._id).update({ data: { parse: event.userinfo.parse - parseInt(event.num) } }); return re } } [代码] 需安装的依赖:wx-server-sdk、tenpay 这里只是实现了简单原始的提现操作,关于提现后,比如防止重复提交,提现限额这些,在开源二手书商城上有完整流程,地址: https://github.com/xuhuai66/used-book-pro 这种办法,不是每次都能成功提现,小概率遇到ip未在白名单情况,还是希望,云开发团队能尽快出一个更好的解决方案吧
2019-09-21 - 这些 Canvas 小技巧,保证你新年用得上
来自「微信开发者」公众号,作者为微信小程序技术研发工程师binnie。 本文主要介绍了3个隐藏的 Canvas 小技巧: - 绘制并生成图片 - Video 绘制 Canvas / webgl - 视频解码并绘制到 webgl - 录制并导出 webgl 视频 一键加滤镜 快速合成音视频 轻松挑选视频封面 …… Canvas 能够做这些? 作为资深的开发者,相信大家对 Canvas 都不陌生。这项能力在绘制图形方面发挥着极大的作用,高效支持图片编辑、数据可视化等应用场景。但是只局限于一般能力应用,那格局就小了。 Canvas 的应用场景非常丰富!赶紧往下看看这些隐藏的 Canvas 小技巧,保证你新年用得上!还有手把手教程以及文末彩蛋哟。 -- • 绘制并生成图片 • -- [图片] 示例:新年模板长按保存祝福 适用场景:图片分享海报 相关 API:RenderingContext/Canvas/wx.canvasToTempFilePath Step 1: 创建实例获取对象 创建 Canvas 实例,获取 CanvasRenderingContext2D 对象(Canvas 绘图上下文)来绘制形状、文本、图像等。 const query = wx.createSelectorQuery() let canvas = null query.select('#myCanvas') .fields({ node: true, size: true }) .exec((res) => { // 通过 wx.createSelectorQuery 获取到 canvas 实例 canvas = res[0].node // 通过 canvas.getContext('2d') 获取 CanvasRenderingContext2D 对象 const ctx = canvas.getContext('2d') }) Step 2: 设置宽高调整图片 获取 Canvas 绘图上下文后,将 Canvas 的宽高设置为节点宽高 * 设备像素比,绘制出来的图片更清晰 // 获取设备像素比 const dpr = wx.getSystemInfoSync().pixelRatio // 将 canvas 宽高设置为 canvas.width = res[0].width * dpr canvas.height = res[0].height * dpr Step 3: 绘制内容 使用 CanvasRenderingContext2D 绘制,根据业务需要在画布中绘制头像、文字、背景等 // 矩形 ctx.fillStyle = '#FFFFFF' ctx.fillRect(0, 0, canvas.width , canvas.height ) // 图片 var image = canvas.createImage() himage.src = 'https://example.com/example.jpg' headImage.onload = (res) => { ctx.drawImage(himage 0, 0, 32, 32; } // 文本 ctx.font = "18px SimHei"; ctx.textAlgin = "left" ctx.fillStyle = "#07c160"; ctx.fillText("这是我的名字", 0, 0); Step 4: 生成并保存本地 使用 wx.canvasToTempFilePath 将画布生成图片,wx.saveImageToPhotosAlbum 将图片保存到本地。 wx.canvasToTempFilePath({ canvas: canvas, // canvas 实例 success(res) { // canvas 生成图片成功 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success(res) { // 保存成功 } }) } }) -- • Video 绘制 Canvas / webgl • -- [图片] 示例:视频文件绘制 Canvas 适用场景:制作 Video 滤镜、挑选 Video 封面等 相关 API:RenderingContext/Canvas Step 1: 获取实例 通过 wx.createSelectorQuery 获取 VideoContext 实例 let video = null wx.createSelectorQuery().select('#video').context(res => { // 通过 wx.createSelectorQuery 获取 VideoContext 实例 video = res.context; }) Step 2: 绘制内容 获取 VideoContext 实例后,将 VideoContext 传递给 Canvas 进行绘制。开发者根据业务需求选择绘制类型: Canvas 2d 写法:canvas.drawImage(video, ...)webgl 写法:gl.texImage2D(..., video) wx.createSelectorQuery().selectAll('#myCanvas,#webglCanvas').node(res => { const ctx = res[0].node.getContext('2d') const gl = res[1].node.getContext('webgl') setInterval(() => { // canvas 2d // 将 video 纹理对象传入 drawImage 进行绘制 ctx1.drawImage(video, 0, 0, w * dpr, h * dpr); // 添加一个蒙层 ctx1.fillStyle = 'rgba(0, 0, 0, 0.3)' ctx1.fillRect(0, 0, w * dpr, h * dpr); // webgl const render = createRenderer(res[1].node, w, h) render(new Uint8Array(ctx1.getImageData(0, 0, w * dpr, h * dpr).data), w * dpr, h * dpr) }, 1000 / 24) }).exec() function createRenderer(canvas, width, height) { const gl = canvas.getContext("webgl") ... return (arrayBuffer, width, height) => { ... // 指定二维纹理图像 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, arrayBuffer) gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0) } } -- • 视频解码并绘制到 webgl • -- [图片] 示例:视频一键解码并绘制到 webgl 适用场景:添加特效、贴图等视频编辑场景 相关 API:wx.createVideoDecoder/VideoDecoder/RenderingContext/Canvas.requestAnimationFrame/wx.createMediaAudioPlayer/MediaAudioPlayer Step 1: 创建视频解码器进行解码 1. 调用 createVideoDecoder 对视频进行解码 2. 使用 videodecoder.start 启动解码,视频源文件不限制本地或远程路径 3. 通过 videodecoder.on('start', res => {}) 监听解码,通过 videodecoder.getFrameData() 获取到解码数据 // 获取视频解码器 getVideoDecoder(source, abortAudio) { return new Promise((resolve, reject) => { // 创建视频解码器 videodecoder = wx.createVideoDecoder() // 开始解码 videodecoder.start({ abortAudio: abortAudio, source: source, // 视频源文件,支持本地路径&远程路径 mode: 0 // 按pts解码,保证音画同步 }) // 监听解码 开始 videodecoder.on('start', res => { console.log('videodecoder start', res) // 状态初始化 isStop = false resolve(videodecoder) }) // 监听解码 结束 videodecoder.on('ended', res => { // 状态设置为结束,停止画面录制器 isStop = true }) }) }, Step 2: 解码数据绘制到 webgl 1. 通过 gl.texImage2D(..., image) 将解码数据绘制到 webgl 2. 使用 webgl.requestAnimationFrame 继续绘制,效果更加流畅 // 将解码数据绘制到 webgl 中 const query = wx.createSelectorQuery() query.select('#webglCanvas').node().exec((res) => { const webgl = res[0].node const requestAnimationFrame = webgl.requestAnimationFrame; // 初始化webgl let render = null if (!render) { render = createRenderer(webgl, 600, 400) } /** * 绘制视频帧到 canvas */ let i = 1 let loop = () => { // 解码结束,停止循环 if (isStop) { return } // 获取解码数据,绘制到 webgl 中 const imageData = videodecoder.getFrameData() if (imageData) { // render 的高宽需要设置为图片的宽高才可以绘制出来 render(new Uint8Array(imageData.data), imageData.width, imageData.height) } // 继续绘制 console.log('绘制帧数:', i++) requestAnimationFrame(loop) } // 启动录制循环 requestAnimationFrame(loop) }) Step 3: 添加音频播放器同步播放音频 完成 Step2 后,webgl 只有视频播放,缺少音频。因此使用 wx.createMediaAudioPlayer(),支持 addAudioSource 传入 videodecoder,保证视频帧渲染音画同步 /** * 创建媒体音频播放器 */ let mediaAudioPlayer = null let addAudio = () => { if (mediaAudioPlayer) return mediaAudioPlayer = wx.createMediaAudioPlayer() mediaAudioPlayer.start().then(() => { // 添加播放器音频来源 mediaAudioPlayer.addAudioSource(videodecoder).then(res => { console.log('add mediaAudioPlayer: ',) }) }) } // render 绘制视频同时添加音频 render(new Uint8Array(imageData.data), imageData.width, imageData.height) addAudio() -- • 录制并导出 webgl 视频 • -- [图片] 示例:录制并一键导出 webgl 视频 适用场景:将动画、编辑过的视频导出视频文件保存 相关 API:wx.createMediaRecorder/MediaRecorder/wx.createMediaContainer/MediaContainer/MediaTrack Step 1: 创建 webgl 画面录制器进行录制 通过 createMediaRecorder 创建页面录制器,并且绑定 webgl(建议离屏状态,效果更好)进行录制 /** * 获取画面录制器 */ getRecorder() { let canvas = this.getMainCanvasNode() let recorder = wx.createMediaRecorder(canvas, { fps: choosedVideoInfo.fps, // 实际视频的 fps videoBitsPerSecond: choosedVideoInfo.bitrate, // 实际视频的 bitrate gop: 12 }) // 监听录制事件 recorder.on("timeupdate", (res) => { console.log('recorder 录制中,当前时间:', res.currentTime) }) recorder.on("stop", (res) => { console.log('recorder停止') this.saveMedia(res.tempFilePath) }) // 开始录制 recorder.start() this.recorder = recorder return recorder }, // 初始化 画面录制器 并进行录制 await this.initRenderer() this.getDecoder().then((decoder) => { let recorder = this.getRecorder() var self = this function loop() { if (self.stopped) { return } let frameData = decoder.getFrameData() if (!frameData) { console.log('没取到帧') setTimeout(() => { loop() }, 1000/60) } else { self.renderFrame(frameData) recorder.requestFrame(() => { console.log('录制帧数:', i++) loop() }) } } loop() }) Step 2: 添加音频合成音视频 1. 通过 createMediaContainer 创建音视频处理容器来合成音视频 2. 通过 MediaContainer.extractDataSource 将视频源分离出视频轨道和音频轨道,将需要的轨道通过 MediaContainer.addTrack 添加到容器中 3. 通过 MediaContainer.export 导出即可获得合成后的视频文件 /** * 将视频和音频合到一起并保存到本地 * @param {*} videoTempFilePath */ saveMedia(videoTempFilePath) { const self = this let choosedFile = this.choosedFile const MediaContainer = wx.createMediaContainer() // webgl的取视频 MediaContainer.extractDataSource({ source: videoTempFilePath, success(res) { MediaContainer.addTrack(res.tracks[0]) // 源视频取音频 MediaContainer.extractDataSource({ source: choosedFile, success(res) { // 拿到音频轨道并加入到容器 res.tracks[0].kind == 'audio' && MediaContainer.addTrack(res.tracks[0]) res.tracks[1].kind == 'audio' && MediaContainer.addTrack(res.tracks[1]) // 合成视频并导出视频文件 MediaContainer.export({ success(res) { // 保存视频到本地 wx.saveVideoToPhotosAlbum({ filePath: res.tempFilePath, success() { wx.showToast({ title: '导出成功', icon: 'success', duration: 2000 }) self.destroy() } }) } }) } }) } }) }, -- •高效图像处理彩蛋 • -- 学会以上这些 Canvas 小技巧,还担心新年的美图美照美视频处理不过来?赶紧码下这个 Canvas 代码包,保证你就是家里最闪耀的靓女靓仔。 预祝大家新的一年 Canvas 在手,红包一直有!
2022-03-24 - 环形进度条完整Demo
之前做项目需要做一个弧形进度,做了个小Demo,图一到图二的效果。(加上动图了),仅供参考! 代码片段:https://developers.weixin.qq.com/s/0rR3A6mQ7i4y [图片]
2018-12-27 - 如何在小程序中快速实现环形进度条
在小程序开发过程中经常涉及到一些图表类需求,其中环形进度条比较属于比较常见的需求 [图片] [中间的文字部分需要自己实现,因为每个项目不同,本工具只实现进度条] 上图中,一方面我们我们需要实现动态计算弧度的进度条,还需要在进度条上加上渐变效果,如果每次都需要自己手写,那需要很多重复劳动,所以决定为为小程序生态圈贡献一份小小的力量,下面来介绍一下整个工具的实现思路,喜欢的给个star咯 https://github.com/lucaszhu2zgf/mp-progress 环形进度条由灰色底圈+渐变不确定圆弧+双色纽扣组成,首先先把页面结构写好: .canvas{ position: absolute; top: 0; left: 0; width: 400rpx; height: 400rpx; } 因为进度条需要盖在文字上面,所以采用了绝对定位。接下来先把灰色底圈给画上: const context = wx.createContext(); // 打底灰色曲线 context.beginPath(); context.arc(this.convert_length(200), this.convert_length(200), r, 0, 2*Math.PI); context.setLineWidth(12); context.setStrokeStyle('#f0f0f0'); context.stroke(); wx.drawCanvas({ canvasId: 'progress', actions: context.getActions() }); 效果如下: [图片] 接下来就要画绿色的进度条,渐变暂时先不考虑 // 圆弧角度 const deg = ((remain/total).toFixed(2))*2*Math.PI; // 画渐变曲线 context.beginPath(); // 由于外层大小是400,所以圆弧圆心坐标是200,200 context.arc(this.convert_length(200), this.convert_length(200), r, 0, deg); context.setLineWidth(12); context.setStrokeStyle('#56B37F'); context.stroke(); // 辅助函数,用于转换小程序中的rpx convert_length(length) { return Math.round(wx.getSystemInfoSync().windowWidth * length / 750); } [图片] 似乎完成了一大部分,先自测看看不是满圆的情况是啥样子,比如现在剩余车位是120个 [图片] 因为圆弧函数arc默认的起点在3点钟方向,而设计想要的圆弧的起点从12点钟方向开始,现在这样是没法达到预期效果。是不是可以使用css让canvas自己旋转-90deg就好了呢?于是我在上面的canvas样式中新增以下规则: .canvas{ transform: rotate(-90deg); } 但是在真机上并不起作用,于是我把新增的样式放到包裹canvas的外层元素上,发现外层元素已经旋转,可是圆弧还是从3点钟方向开始的,唯一能解释这个现象的是官方说:小程序中的canvas使用的是原生组件,所以这样设置css并不能达到我们想要的效果 [图片] 所以必须要在canvas画图的时候把坐标原点移动到弧形圆心,并且在画布内旋转-90deg [图片] // 更换原点 context.translate(this.convert_length(200), this.convert_length(200)); // arc原点默认为3点钟方向,需要调整到12点 context.rotate(-90 * Math.PI / 180); // 需要注意的是,原点变换之后圆弧arc原点也变成了0,0 真机预览效果达成预期 [图片] 接下来添加环形渐变效果,但是canvas原本提供的渐变类型只有两种: 1、LinearGradient线性渐变 [图片] 2、CircularGradient圆形渐变 [图片] 两种渐变中离设计效果最近的是线性渐变,至于为什么能够形成似乎是随圆形弧度增加而颜色变深的效果也只是控制坐标开始和结束的坐标位置罢了 const grd = context.createLinearGradient(0, 0, 100, 90); grd.addColorStop(0, '#56B37F'); grd.addColorStop(1, '#c0e674'); // 画渐变曲线 context.beginPath(); context.arc(0, 0, r, 0, deg); context.setLineWidth(12); context.setStrokeStyle(grd); context.stroke(); 来看一下真机预览效果: [图片] 非常棒,最后就剩下跟随进度条的纽扣效果了 [图片] 根据三角函数,已知三角形夹角根据公式radian = 2*Math.PI/360*deg,再利用cos和sin函数可以x、y,从而计算出纽扣在各部分半圆的坐标 const mathDeg = ((remain/total).toFixed(2))*360; // 计算弧度 let radian = ''; // 圆圈半径 const r = +this.convert_length(170); // 三角函数cos=y/r,sin=x/r,分别得到小点的x、y坐标 let x = 0; let y = 0; if (mathDeg <= 90) { // 求弧度 radian = 2*Math.PI/360*mathDeg; x = Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 90 && mathDeg <= 180) { // 求弧度 radian = 2*Math.PI/360*(180 - mathDeg); x = -Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 180 && mathDeg <= 270) { // 求弧度 radian = 2*Math.PI/360*(mathDeg - 180); x = -Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } else{ // 求弧度 radian = 2*Math.PI/360*(360 - mathDeg); x = Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } [图片] 有了纽扣的圆形坐标,最后一步就是按照设计绘制样式了 // 画纽扣 context.beginPath(); context.arc(x, y, this.convert_length(24), 0, 2 * Math.PI); context.setFillStyle('#ffffff'); context.setShadow(0, 0, this.convert_length(10), 'rgba(86,179,127,0.5)'); context.fill(); // 画绿点 context.beginPath(); context.arc(x, y, this.convert_length(12), 0, 2 * Math.PI); context.setFillStyle('#56B37F'); context.fill(); 来看一下最终效果 [图片] 最后我重新review了整个代码逻辑,并且已经将代码开源到https://github.com/lucaszhu2zgf/mp-progress,欢迎大家使用
2020-05-27 - html-canvas 生成小程序分享图
简介 基于 HTML 和 CSS 实现 Canvas 绘图。 项目地址 代码片段:https://developers.weixin.qq.com/s/9zFHKdmh7De2 原理 构建虚拟DOM 树,依据 CSS 规范计算样式,使用 CSS 盒模型对 DOM 进行布局,计算出所有元素的位置。最后将 DOM 树通过 Canvas Api 进行绘制。 小程序开发工具内运行 demo [代码]git clone https://github.com/alexayan/html-canvas.git npm i npm run watch [代码] 已支持的 CSS 属性 margin,margin-left,margin-top,margin-right,margin-bottom,padding,padding-left,padding-top,padding-right,padding-bottom,width,height,border,border-left,border-top,border-right,border-bottom,border-width,border-style,border-color,border-left-style,border-left-color,border-left-width,border-top-style,border-top-color,border-top-width,border-right-style,border-right-color,border-right-width,border-bottom-style,border-bottom-color,border-bottom-width,color,display,background-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-left-radius,border-bottom-right-radius,box-sizing,font,font-style,font-variant,font-weight,font-stretch,font-size,line-height,font-family,text-align,position,overflow,overflow-x,overflow-y,top,left,right,bottom,z-index demo canvas-draw.html [图片]
2020-01-08 - wx.nextTick() 里的时间片在小程序里是多久?
- 需求的场景描述(希望解决的问题) wx.nextTick 里有解释道会在下一个时间片执行,这里的时间片指的是什么,大概是多久? - 希望提供的能力
2018-10-09 - 小程序公众号干货运营之注销篇
各位亲,面对帐号注销是不是束手无策呢?帐号如何注销,怎么注销,注销需要提供什么信息内容呢?请仔细往下看看 小程序 关于小程序注销的条件,若未冻结的个人帐号和组织类帐号就不 一 一 细讲,详情请参腾讯客服文档:https://kf.qq.com/product/wx_xcx.html#hid=2826 1:小程序注销之政府无对公账户: 详细流程请参考:https://kf.qq.com/faq/190104YnQbYN190104RzaYba.html 政府的有一致主体是提供一致的主体证件和公章,如果有变更请提供4项材料:因机构改革、单位合并、撤一建一等情况,导致机构主体名称有变更,提供以下材料申请注销: 1、更名相关的红头文件(有鲜章); 2、主体名称变更情况说明书(加盖新主体公章); 3、变更后新主体的主体证件;(原件拍照或加盖公章的复印件) 4、注销申请函(加盖新主体公章); 2:小程序注销之个体工商户 若个体工商户存在对公账户,请使用对公账户小额打款注销 若个体工商户类型无对公账户注销小程序工单指引流程如下 工单所需材料 1、小程序绑定邮箱/原始ID: 2、主体证件材料(营业执照/组织机构代码证等): 3、小程序绑定的法人身份证原件正反面的清晰扫描件或照片: 4、小程序的注销书面申请,申请书必须加盖公章。(若个体户没有公章可支持法人手写签名) 附注:注销申请书模板(https://kf.qq.com/faq/200306R7N3mI200306I3aEBz.html) 材料提交链接:https://kf.qq.com/touch/bill/200306selfqaafe6c551.html(手机端打开) 3:小程序注销之帐号主体已注销 主体已注销小程序工单指引流程如下, 1、小程序绑定邮箱/原始ID: 2、主体注销证明: 3、小程序绑定的法人身份证原件正反面的清晰扫描件或照片: 4、小程序的注销书面申请,企业账号的申请必须有加盖公章的函件(公章被收的请法人手写签字)附注:注销申请书模板(https://kf.qq.com/faq/200306R7N3mI200306I3aEBz.html) 材料提交链接:https://kf.qq.com/touch/bill/200306selfqaafe6c551.html(手机端打开) 4:小程序注销之门店小程序 门店小程序依附于公众号,不支持单独注销,公众号注销门店小程序才支持注销 5:公众号正常运营,门店小程序如何释放昵称 如果需要释放该小店小程序昵称,发送邮件到“miniprogram@tencent.com”,标题格式【关于XXX名称释放请求】,需提供以下材料: 1、小程序帐号(原始ID); 2、绑定的管理员微信号; 3、小程序主体营业执照等主体证件; 4、小程序所有者的书面申请,申请书需加盖小程序主体公章;(个体户无公章:申请书需要加上法人签名); 邮件内容:需包含背景、释放请求原因。 6:复用公众号资质快速注册的小程序如何注销 复用资质申请的小程序是独立存在的,请按照正常流程注销即可 7:注册小程序选择微信认证,若未完成微信认证如何注销呢? 小程序30天未认证或认证失败且7天内未发起认证不会释放邮箱,但该邮箱支持重新注册小程序,会释放主体信息、管理员信息、昵称。 公众号 关于公众号若未冻结的个人帐号和组织类帐号就不一一细讲,详情请参考腾讯客服文档:https://kf.qq.com/product/weixinmp.html#hid=2267 1:公众号注销之政府无对公账户: 详细流程请参考:https://kf.qq.com/faq/190531qyuuiY190531BjyyEv.html 政府的有一致主体是提供一致的主体证件和公章,如果有变更请提供4项材料:因机构改革、单位合并、撤一建一等情况,导致机构主体名称有变更,提供以下材料申请注销: 1、更名相关的红头文件(有鲜章); 2、主体名称变更情况说明书(加盖新主体公章); 3、变更后新主体的主体证件;(原件拍照或加盖公章的复印件) 4、注销申请函(加盖新主体公章); 2:公众号注销之个体工商户 若个体工商户存在对公账户,请使用对公账户小额打款注销 若个体工商户类型无对公账户,请使用法人扫脸注销公众号 详情请参考:https://kf.qq.com/faq/220309bUvmIB220309BbAjMz.html 3:公众号注销之帐号主体已注销 主体已注销公众号工单指引流程如下, 1、公众号绑定邮箱/原始id/微信号: 2、主体注销证明: 3、公众号绑定的法人身份证原件正反面的清晰扫描件或照片: 4、公众号的注销书面申请,企业账号的申请必须有加盖公章的函件(公章被收的请法人手写签字) 附注:注销申请书模板(http://kf.qq.com/faq/171018R3IVBF171018INjUvA.html ) 材料提交链接:https://kf.qq.com/touch/bill/180227selfqa9ab6ac55.html(手机端打开) 4:未注册成功的帐号如何注销 若帐号当时没有走完注册流程且长期没有登录该帐号,到期会被系统注销。没有走完注册流程的帐号不占用个人信息,也不支持找回,建议重新注册 5:注册公众号选择微信认证,若未完成微信认证如何注销呢? 若公众号注册时选择微信认证,自注册日起30天内未进行认证(第30天仍在认证中不算),点击“重新提交材料”,帐号角色变为注册失败,不会释放帐号邮箱,但该邮箱支持重新注册公众号,会释放主体信息、管理员信息、昵称, 6:小程序公众号注销确认期 注销确认期的7天内每天会发送一次确认注销的通知,若管理员一直未点击确认注销则默认取消注销,注销失败。因此管理员请关注公众平台安全助手!!!
2022-04-08 - eventChannel的用法的一些经验,既好理解,又简单好用。
eventChannel具体用法不多介绍,看文档: https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.navigateTo.html eventChannel对于初学者,是有点弯绕,不好理解的,我们对其用法做了修改: 1、父页打开子页,用globalData传参; 2、子页返回父页,用eventChannel传参; 具体代码如下: pageFather.js: 原做法: fatherEC1:function(){ wx.navigateTo({ url: './child', events: { ec: e => console.log(e) }, success: res => { res.eventChannel.emit('ec', 'father') } }) }, 现做法: fatherEC2:function(){ app.globalData.ecData = 'father' wx.navigateTo({ url: './child', events: { ec: e => console.log(e) } }) }, pageChild.js: 原做法: childEC2:function(){ this.ec = this.getOpenerEventChannel() this.ec.once && this.ec.once('ec', e => { console.log(e) //'father' }) }, 现做法: childEC1:function(){ console.log(app.globalData.ecData) //'father' }, 当然从子页里往父页传参还是保持不变: onSubmit: function () { this.getOpenerEventChannel().emit('ec', 'child') wx.navigateBack() }, onDelete: function () { this.getOpenerEventChannel().emit('del', true) wx.navigateBack() }, onUpdate: function () { this.getOpenerEventChannel().emit('update', {data}) wx.navigateBack() }, 这样改一下,是不是简单了,好理解多了? 实际上效果也很好,而且大概率不会发生一种叫“21 events balabala”的告警。
2021-04-12 - 微信小程序云开发集合的某个字段是一个数组,请问这个数组存储有上限吗?
[图片]
2021-03-31 - 小程序 云开发 企业付款到零钱
终于轮到我来装一次b了 之前总是有求于各位神,现在来回馈了。 各位用小程序云开发,要实现退款、企业零钱的可以看过来。 // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const config = { appid: '**************', //小程序Appid,填自己的小程序id envName: '*************', // 小程序云开发环境ID mchid: '***********', //商户号,填自己的商户号 pfx: require('fs').readFileSync('./apiclient_cert.p12'),这里是下载的api证书。证书怎么下在呢?网上有 partnerKey: '123111111111111111111111111111111111111111111111111', //此处填商户密钥 notify_url: ' ', //支付回调网址,这里可以随意填一个网址 spbill_create_ip: '127.0.0.1' //不用改 }; const db = cloud.database(); const TcbRouter = require('tcb-router'); //云函数路由 const rq = require('request'); const tenpay = require('tenpay'); //支付核心模块 这里要是报错,直接搜 nps + 报错内容 // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() console.log("提现走到了函数",event) const api = tenpay.init(config); var tixian = event.tixian // 申请企业付款到用户零钱 const orderNumber= 'dlbmoney' + new Date().getTime() + Math.floor(Math.random() * 1000) const datas = { partner_trade_no: orderNumber, openid: wxContext.OPENID, amount: tixian * 100, desc: "订单说明", check_name: "NO_CHECK", //不检查实名 spbill_create_ip:"123.151.79.109" } const result = await api.transfers(datas) return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } }
2021-07-07 - 用canvas画布背景图画出来是黑色的,这是为什么?
[图片] createNewImg(){ var that = this; const query = wx.createSelectorQuery() var canvas = query.select('#mycanvas') query.select('#mycanvas') .fields({ node: true, size: true }) .exec((res) => { const canvas = res[0].node const context = canvas.getContext('2d') const imaUpload = canvas.createImage() console.log() imaUpload.src ='https://www.linkedsign.cn/Photo/Beauty/ChamberOfCommerce/20210907/yqh.png' // console.log(1) imaUpload.onload = function (){ context.drawImage(imaUpload, 0, 0, 345, 550) console.log(context) const dpr = wx.getSystemInfoSync().pixelRatio canvas.width = res[0].width * dpr canvas.height = res[0].height * dpr // console.log(res[0].width) context.scale(dpr, dpr) context.fillRect(0, 0, 100, 100) context.save(); var titl = that.data.Title; context.fillStyle='#191919'; context.textAlign='left'; context.font = 'normal bold 18px sans-serif'; context.fillText('嘻嘻哈哈', 60, 270); context.stroke(); context.save(); // context.draw(true) wx.canvasToTempFilePath({ canvasId:'mycanvas', // quality:'jpg', canvas:canvas, success:res=>{ console.log(res) that.setData({ imageUrl:res.tempFilePath }) } }) } }) }
2021-09-07 - 微信小程序如何实现页面传参?
前言 只要你的小程序超过一个页面那么可能会需要涉及到页面参数的传递,下面我总结了 4 种页面方法。 路径传递 通过在url后面拼接参数,参数与路径之间使用 ? 分隔,参数键与参数值用 = 相连,不同参数用 & 分隔;如 ‘path?key=value&key2=value2’。 案例:A页面带参数跳转到B页面 A页面跳转代码 [代码]goB(){ wx.navigateTo({ url: '/pages/B/index?id=value', }) }, [代码] B页面接收代码 [代码]onLoad: function (options) { console.log('id', options.id) } [代码] 上面的案例是字符串参数,但是很多情况下需要传递对象,如下方代码。 [代码]Page({ data: { userInfo:{ name:'cym', age:16 } }, goB(){ wx.navigateTo({ url: '/pages/B/index?id='+this.data.userInfo, }) }, }) [代码] 如果使用上面同样的方式结构,输出的结果是:[object Object] 这个时候需要先把对象通过JSON.stringify(obj)将 object 对象转换为 JSON 字符串进行参数传递,再到接收页面通过JSON.parse解析使用。 A页面跳转代码 [代码] goB(){ let userStr = JSON.stringify(this.data.userInfo) wx.navigateTo({ url: '/pages/B/index?id='+userStr, }) } [代码] B页面接收代码 [代码]onLoad: function (options) { console.log('id', JSON.parse(options.id)) } [代码] 全局变量 通过App全局对象存放全局变量。 app.js代码 [代码]App({ // 存放对象的全局变量 globalData:{}, }) [代码] A页面跳转代码 [代码]// 获取App对象 const app = getApp() Page({ /** * 页面的初始数据 */ data: { userInfo: { name: 'cym', age: 16 } }, goB() { app.globalData.userInfo = this.data.userInfo wx.navigateTo({ url: '/pages/B/index', }) }, }) [代码] B页面接收代码 [代码]// 获取全局对象 const app = getApp() Page({ onLoad: function (options) { console.log(app.globalData.userInfo) } }) [代码] 存放在 App 全局变量里面,可以被多个页面使用,直接从 App 对象获取即可。这个数据是保持在内测中,每次小程序销毁就没有了。 数据缓存 通过存储到数据缓存中。 A页面跳转代码 [代码] goB() { wx.setStorageSync('userInfo', this.data.userInfo) wx.navigateTo({ url: '/pages/B/index', }) } [代码] B页面接收代码 [代码] onLoad: function (options) { let userInfo = wx.getStorageSync('userInfo', this.data.userInfo) console.log(userInfo) } [代码] 存放在数据缓存里面,可以被多个页面使用,直接用 getStorageSync 获取即可。这个数据是保持在数据缓存中,除非清楚数据缓存或者删除小程序否则一直存在。 事件通信 通过事件通信通道。 A页面跳转代码 [代码]goB() { wx.navigateTo({ url: '/pages/B/index', success:(res)=>{ // 发送一个事件 res.eventChannel.emit('toB',{ userInfo: this.data.userInfo }) } }) } [代码] B页面接收代码 [代码]onLoad: function (options) { // 获取所有打开的EventChannel事件 const eventChannel = this.getOpenerEventChannel(); // 监听 index页面定义的 toB 事件 eventChannel.on('toB', (res) => { console.log(res.userInfo) }) } [代码] 总结 大家可以针对具体业务场景来进行选择合适自己的传参方式。
2022-02-19 - 实现最简单的公告滚动效果
[图片] wxml: <view style="--width:{{width}}px;--timer:{{timer}}s;"> <view class="marquee_text" style="font-size:{{size}}px">{{text}}</view> </view> wxss: @keyframes move { from { margin-left: 100%; } to { margin-left: var(--width); /* var接受传入的变量 */ } } .marquee_text{ display: inline-block; margin-left: 100%; white-space: nowrap; animation: move var(--timer) infinite linear; font-weight: bold;background: url('https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2025728819,3580500436&fm=26&gp=0.jpg');background-size: contain; -webkit-background-clip: text;color: transparent; } js: Page({ data: { text: '成都近日新增确诊新冠患者3例,请大家做好防护,外出请戴好口罩!', size: 14,//宽度即文字大小 moveTimes: 8,//刚好文字宽度等于屏幕宽度所需的时间 width: 0,//文字宽度 timer: 0 //滚动时间 }, onLoad: function () { var screenW = wx.getSystemInfoSync().windowWidth;//获取屏幕宽度 var contentW = this.data.text.length * this.data.size;//获取文本大概宽度 var timer = (contentW / screenW) * this.data.moveTimes;//动态计算文字滚动时间 timer = timer < 8 ? 8 : timer; //判断时间是否小于8s this.setData({ width: -contentW, timer: timer }); } }) 小结:wxss里面使用变量,在js中给定数据,wxml中通过内联样式动态绑定自定义属性--width(--width--这样也行,获取就是var(--width--))的值,wxss中通过var(--width)的方式接收变量的值,剩下的看注释,都备注上了。
2020-12-08 - 简单实现签到日历效果
wxml: <view class="box"> <view class="section"> <picker mode="date" value="{{date}}" fields="month" start="2010-01-01" end="{{cy+'-'+cm}}" bindchange="bindDateChange"> <view class="picker">{{cur_year || "--"}} 年 {{cur_month || "--"}} 月</view> </picker> </view> <view> <!-- 显示星期 --> <view class="week color9b"> <view wx:for="{{weeks_ch}}" wx:key="unique">{{item}}</view> </view> <view class='days'> <!-- 行 --> <view class="rows" wx:for="{{days.length/7}}" wx:for-index="i" wx:key="unique"> <!-- 列 --> <view class="columns" wx:for="{{7}}" wx:for-index="k" wx:key="unique"> <!-- 每个月份的空的单元格 --> <view class='cell' wx:if="{{days[7*i+k].date == null}}"> <text decode="{{true}}"> </text> </view> <!-- 每个月份的有数字的单元格 --> <view class='cell' wx:else> <!-- 当前日期已签到 --> <view wx:if="{{days[7*i+k].isSign == true}}" class='qianbg'> <text class="colorff">{{days[7*i+k].date}}</text> <text class="sourse">+{{days[7*i+k].Score}}</text> </view> <!-- 当前日期未签到 --> <view wx:else> <text>{{days[7*i+k].date}}</text> </view> </view> </view> </view> </view> </view> </view> 简单提下思路,首先默认确定当前年月,cy cm, 初始化:获取days遍历日历的格子,通过获取当前月第一天是星期几来判断前面有几个空格,放入days,再当月天数放入days,然后进行渲染,再通接口去拿签到信息,签到成功的突出显示。这里签到初始化时我默认给了标识isSign,将已签到列表和当前年月日进行比较,符合条件则更新签到状态。切换选择日期,这里我用的是选择器,当然可以写成点击左侧按钮上一月,右侧按钮下一月那种,重新选择日期后,initdata(e) 传入年月,就是当前选择年月的数据。将星期日作为第一日的我也备注上去了。样式根据自己的喜好改就行了,最后看看我写的两个项目效果: [图片][图片] 写了个demo:https://developers.weixin.qq.com/s/SSlwjGmb7Wm9
08-14 - 小程序首页onLaunch,onLoad为异步,调用app.js中的全局参数的解决方案。
解决方案就是在app.js加一个回调函数 wx.login({ fail: res => { console.log(res) }, success: res => { util.request(api.dl, { code: res.code, }, "GET").then(res => { if (this.employIdCallback){ this.employIdCallback(res); } this.globalData.userid=res 然后在你的首页onLoad进行判断是否有这个值然后进行不同的逻辑 onLoad: function (options) { console.log( ) if(!app.globalData.userid){ this.getstore() this.getfl() }else{ app.employIdCallback= param1=> { this.getstore() this.getfl() } } },
2021-02-02 - onLaunch执行太慢onLoad已经先行执行?
onLaunch中使用wx.login获取code然后登录,但是登录还没获取token页面的onLoad就已经开始执行请求,导致登录态错乱
2022-01-06 - 用户通过扫二维码进入小程序, 二维码所带参数偶尔获取不到
有一个广告投放的页面, 二维码携带参数, 发现部分用户扫码进入时在onLoad里是拿不到参数的, 退出小程序重新扫码就没问题. 请问这是什么原因? 问题出现也没有规律, 有人会出现这个问题, 有人没出现这问题 [图片]
2021-07-15 - 30分钟实现小程序打卡签到送积分功能,提升用户留存利器!
前言在小程序运营中,为了提升用户留存,通常会自建打卡签到模块送积分功能。用户可以通过打卡签到获取到积分,然后积分可以兑换礼物,从而实现用户留存的效果。 效果在我的页面加入了一个签到入口,点击进入打卡签到页面。进入页面后会自动签到,引导用户进行二次提醒推送。可在后台灵活配置每天签到的积分及抽奖、奖品配置。 [图片] 今天我来带大家看看如果快速实现这个功能,在这里需要用到小程序的「单页模版」功能。来看看官方文档介绍: 目前「单页模版」处于内测阶段,我这个是内测版本,所以大家在开发工具中没有也是正常的。 小程序开发过程中,有很多通用的业务模块,例如:打卡签到、邀请有礼、趣味小游戏、运营 banner 配置等。这些模块业务模型具备通用性,但是前端每个小程序都有自己的样式设计。因此,每个小程序都需要重复性的进行开发。单页模板致力于帮助小程序开发者聚焦前端交互展示,无需关注于实现接口以及管理端。开通单页模板后,运营人员可直接在管理端配置新功能,小程序前端源码组件可导入到小程序内快速接入,也可以对前端组件进行二开以满足业务需求。 简单来说就是把整套打卡签到的前端代码和后端业务代码插入到你的小程序中,自己可以对前端组件做二次开发。 接下来我就带你来看看如何使用: 目前该功能还在内测阶段,你的微信开发功能没有是正常的。在这里我只是演示内测版本,后续云开发团队会持续更新,正式上线。第一步:开通单页模版开发设置面板点击扩展设置找到其他组件安装单页模版[图片] 当你开通完成后来,选中miniprogram文件夹右键呼出菜单,选择「配置单页模版」点击免费使用 [图片] 则会进入腾讯云登录界面,扫码验证选择自己的小程序即可。验证成功后则会进入单页模板控制台的小程序组件页面。 [图片] 第二步:后台配置规则点击「前往配置」则会进入签到配置页面,分别可以对:奖品管理、签到记录、签到规则、签到配置。 奖品管理奖品列表【新增、删除、导出】[图片] 奖品新增,在这里要注意填写奖品总数直接和剩余数量填写相等即可,当用户中奖后会自动将剩余数量-1[图片] 签到记录显示所有的签到记录、中奖记录,可以通过搜索条件对openid、连续天数、奖品描述进行过滤,而从显示对具体信息的快速查找。 [图片] 签到规则这里编辑的内容会在小程序页面用户点击签到规则时进行弹出显示。 [图片] 签到配置在这里可以配置签到活动的开关,签到的积分奖励,目前支持积分和虚拟物品,下周会上线实体物品。还有就是抽奖设置,包括概率和触发条件。 [图片] 当配置一切搞定之后,接下来我们来看下如何导入前端的签到打卡组件。 第三步:导入代码我们回到刚才的控制面板,切换到第二个菜单导入小程序组件,点击导入组件到IDE。 [图片] [图片] 点击查看详情,会有这个组件的介绍。 [图片] 点击导入IDE会直接导入在小程序的miniprogram目录下 [图片] 在这里如果你想直接导入在pages下面在进入单页模版配置的时候直接选择pages文件夹即可,或者移动文件夹到pages下面也可以,因为通常来说我们的页面都是放在pages下面来进行管理的。 看下代码以及预览效果 [图片] 该组件代码的目录: . ├── README.md ├── cloudfunctions # 云开发云函数目录 ├── miniprogram # 小程序前端代码 │ ├── miniprogram_npm │ ├── node_modules # 如果希望在 miniprogram/pages 下调用 @cloudbase/saas-module,需要在 miniprogram 下安装依赖 │ ├── pages │ │ ├── index │ │ ├── page_module_sign_up # 导入目录规范 page_module_${模块名} │ │ │ ├── README.md │ │ │ ├── components # 模块内的组件 │ │ │ ├── config.js │ │ │ ├── images │ │ │ ├── miniprogram_npm # 构建后的npm包,包含 @cloudbase/page-module │ │ │ ├── package.json │ │ │ └── pages # 模块内示例页面 │ │ └── demo │ └── sitemap.json ├── project.config.json 从代码上来看,在这里使用 page_module_sign_up/pages/index/index.wxml 是直接使用的自定义组件如果想要在前端进行样式的调整可以在 page_module_sign_up/components进行相关组件的代码修改。 [图片]这就是在文档中提到的可以对前端组件进行二开以满足业务需求。除了前端之外我们来看看后端配置 第四步:接口配置回到单页模版页面,选择接口自定义接口。 设置消息提醒首先选择一个具体的云环境进行部署云函数 [图片] 部署成功后进行代码下载 [图片] [图片] 这样你就可以得到发送订阅消息的代码,这里要注意就是要修改成自己的订阅模版。登录当前小程序的 mp 管理系统,从菜单中找到订阅消息-公共模版库-搜索【签到模版】-选中第一个进行选用。 [图片] 选用后三个字段 活动名称签到奖励温馨提示[图片] 提交即可,模版申请成功后会在我的模版中显示 [图片] 复制模版ID去小程序前端代码中进行替换,找到page_module_sign_up/config.js [图片] 替换temId和你想要用户点击推送订阅消息卡片跳转的页面page [图片] 除了小程序前端之外还需要在云函数page_module_tcb_sign_up/api/sendmsg.js进行修改。 [图片][图片] 在这里需要和管理后台的模版详情的字段序号对应上,默认下载的代码是 温馨提示:{{thing2.DATA}} 签到奖励:{{thing1.DATA}} 活动名称:{{thing3.DATA}} 代码中的逻辑是第二天发送模版消息,但是实际上测试肯定不会等到第二天要不然效率就太慢了,可以在 page_module_tcb_sign_up/api/set_remind.js 的主题逻辑run方法中修改 addDelayedFunctionTask 的 delayTime 参数,比如:我想点击10秒后发就可以设置为10。 [图片] addDelayedFunctionTask:用于延时调用云函数 delayTime:延迟时间,单位:秒,合法范围:6s-30天 设置完成后,点击明天提醒就可以立即收到模版消息推送了。 [图片] 在这里点击卡片的时候如果你签到打卡是一个新的页面会提醒找不到这个页面,因为发送推送消息默认环境跳转到正式环境去了,所以需要在 page_module_tcb_sign_up/api/sendmsg.js 改动下跳转环境。 [图片] subscribeMessage:发送订阅消息 miniprogramState:developer为开发版;trial为体验版;formal为正式版;默认为正式版 这样调试起来就不会有问题了,但是一定要记得上线后记得把时间和环境都调整过来。 接下来再来看看监听积分数据和奖品数据。 监听积分与奖品这两个监听相对简单,而且方法都是一样的所以就一起讲解。 监听积分发放需要在云函数下新建 /api/send_face_value.js 文件并实现该接口。 [图片] 监听奖品发放需要在云函数下新建 /api/send_prize.js 文件并实现该接口。 [图片] 具体实现,两种只是入参不一样其他都是一样的,以下为官方给出的代码示例。 const objCloud = require('wx-server-sdk'); /** * 具体的业务函数,在这里实现您发奖,发积分的逻辑,data 入参是固定的,出参必须遵循规范 * @param { object } data - 业务入参 * @returns { object } - 返回参数 * @returns { number } code 返回的状态标记,成功返回0, 非0代表错误 * @returns { string } msg 如果成功,则可以不返回,如果失败把相应的错误原因中文描述放在这里 * @returns { object } result 接口调用返回的信息 * */ module.exports = async (data) => { console.log("参数:", data); // 这里实现您具体的业务逻辑,例如发积分,发奖 return { code:0, msg:'suc', // 出参需要遵守自定接口规范 result: { sendResult: true }}; }; 我基于这个案例写了一个实际的业务,当用户签到获取到积分的时候我就存在我的积分表里面。 // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() module.exports = async (data) => { console.log("参数:", data); // 具体业务 const integral = await db.collection('integral') await integral.add({ data: data }) return { code: 0, msg: 'suc', result: { sendResult: true } }; }; 数据结构: [图片] 当然你自己也可以根据自己的业务来新增相关字段,做到这一步基于「单页模版」的签到打卡送积分功能就全部完成了。 注意我已经上线使用了1周了,发现了一个问题。 [图片] 用了单页模式新增了一个lowcode环境,所以在实现礼物接口和积分接口的时候需要指定下环境,要不然会出现概率报错找不到数据库。 [图片] [图片] 最后其实我很早就想实现自己的积分体系,但是由于时间不够的原因,导致迟迟没有去迈出第一步,正好遇到的单页模版的签到打卡送积分这个功能,于是在wilsonsliu的高度配合下完成了积分体系v1.0上线了。同时也非常期待后续的单页模块的邀请有礼、趣味小游戏、运营 banner 配置等上线。 只要你的小程序想要提高用户留存都需要搭建自己的积分体系,而单页模版的签到打卡功能可以成为用户获取积分的途径之一,要有积分才能去消费,具体消费积分场景可以配合积分商城或特殊功能上去进行消费。
2022-01-14 - [开盖即食]小程序Canvas官方新版API实战
[图片] [图片] 最近本人在开发一个新项目的时候,注意到官方在2.9.0开始支持了一个canvas 2D的新API,同时对webGL上支持也有了很大的改进,相信很多人用canvas的组件做一些分享海报,战绩和新闻帖功能。 [图片] 这里是新的引入方式。 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html 那么新的canvas2D API有啥好处呢? 原本的API微信有做一定的修改,现在全面支持源生H5 JS的写法,迁移H5的老代码变成更加容易,学习成本更低 修复了一些诡异的BUG,例如原本在IOS早期版本写法顺序会导致clip()图片裁切失效等~ 性能上的优化和提升,复杂动画上帧数明显 举例写法上的一些改变: 1、设置font的写法: [代码]//原本(传值的写法) ctx.setFontSize(20); ctx.fillText('MINA', 100, 100) ctx.draw() //现在(和源生H5写法一致,赋值) ctx.font = "16px"; ctx.fillStyle = 'blue'; //可以直接写颜色,原本的不支持 //不需要 ctx.draw() [代码] 2、获取并添加图片写法: [代码]//原本 //使用的是 wx.getImageInfo的方法 wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { console.log(res); ctx.drawImage(res.path, 0, 0); ctx.draw(true); }, fail: function (res) { //失败回调 } }); //现在 //可以直接img.onload调用 const headerImg = canvas.createImage(); headerImg.src = headImage;//微信请求返回头像 headerImg.onload = () => { ctx.save(); ctx.beginPath()//开始创建一个路径 ctx.arc(38, 288, 18, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.drawImage(headerImg,0,0); ctx.closePath(); ctx.restore(); } [代码] 3、将canvas生成虚拟地址便于下载(重点): [图片] 由于官方文档没有写清楚,误导了挺多人的。这里canvas对象必须通过选择器获取,并获得对应的node节点。 [代码]async saveImg() { let self = this; //这里是重点 新版本的type 2d 获取方法 const query = wx.createSelectorQuery(); const canvasObj = await new Promise((resolve, reject) => { query.select('#posterCanvas') .fields({ node: true, size: true }) .exec(async (res) => { resolve(res[0].node); }) }); console.log(canvasObj); wx.canvasToTempFilePath({ //fileType: 'jpg', //canvasId: 'posterCanvas', //之前的写法 canvas: canvasObj, //现在的写法 success: (res) => { console.log(res); self.setData({ canClose: true }); //保存图片 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '已保存到相册', icon: 'success', duration: 2000 }) // setTimeout(() => { // self.setData({show: false}) // }, 6000); }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") } else { util.showToast("请截屏保存分享"); } }, complete(res) { wx.hideLoading(); console.log(res); } }) }, fail(res) { console.log(res); } }, this) }, [代码] 分享个canvas海报的代码片段: [图片] 片段名: PoCf4emw7TgE 片段link: https://developers.weixin.qq.com/s/PoCf4emw7TgE [图片] [图片] 总结,相对之前还要看官方文档的canvas自定义API,现在写起来更加的方便,老代码迁移起来得心应手,只要你之前会canvas,那么各种效果和动画,拿来就怼,没什么大问题~ 一些奇怪的问题(注意!!!) canvas 2d 目前(2020年4月3日)还不支持真机调试,会报错!!! IDE工具 1.02.2003190 直接保存新版本canvas的API图片是打不开的,但是直接用手机保存在相册是没问题的,请更新到1.02.2003250 最新版即可解决~ 一些老款手机用新的API保存图片会有报错问题,如华为NOTE10,请更新系统到能支持的最新,且微信也是,即可解决~ 部分Android设备诡异的闪退和报错 [图片] 这种有可能是代码写法的问题,比如: [代码]//缺省写法 会导致部分Android机器 闪退 ctx.font = "bold 16px"; ctx.fillStyle = "#000" //在canvas 2D的写法中,所以写法必须规范且完整 ctx.font = "normal bold 12px sans-serif"; ctx.fillStyle = '#707070'; [代码] 所以在canvas 2D 的环境,所以写法必须原始且规范,不能用缺省写法,不然就会有诡异的闪退/报错。 后续:官方在7.0.13的Android版本已修复。 https://developers.weixin.qq.com/community/develop/doc/00088c13e1437890692afd8d85ec00 看完觉得有帮助记得点个赞哦~ 你的赞是我继续分享的最大动力!^-^
2020-05-09 - canvas 画圆形头像
const ctx = wx.createCanvasContext(‘myCanvas’); wx.getImageInfo({ src: liveImg, //网络图片 success(res) { var avatarurl_width = 30; //绘制的头像宽度 var avatarurl_heigth = 30; //绘制的头像高度 var avatarurl_x = 5; //绘制的头像在画布上的位置 var avatarurl_y = 5; //绘制的头像在画布上的位置 ctx.save(); ctx.beginPath(); //开始绘制 //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针 ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI *2, false); ctx.closePath() ctx.clip(); //画好了圆 剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 这也是我们要save上下文的原因 ctx.drawImage(res.path, avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片,必须是https图片 ctx.restore(); //恢复之前保存的绘图上下文 恢复之前保存的绘图上下午即状态 还可以继续绘制 ctx.draw(true) },fail() { console.log(“失败”) } })
2020-06-09 - [填坑手册]小程序Canvas生成海报(一)--完整流程
[图片] 海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 [图片] 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 [图片] 海报生成流程 [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 下面分享下主要的代码内容和“填坑现场”: 一、添加字体 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html [代码]canvasContext.font = value //示例 ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10 ctx.setTextAlign('left'); ctx.setTextBaseline("top"); ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本 [代码] 符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“…”代替) [代码]ctx.setTextAlign('left'); ctx.setFillStyle('#000');//文字颜色:默认黑色 ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10 let canvasTitleArray = canvasTitle.split(""); let firstTitle = ""; //第一行字 let secondTitle = ""; //第二行字 for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; } } //第一行文字 ctx.fillText(firstTitle, 20, 278, 280)//绘制文本 //第二行问题 if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本 } [代码] 通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。 (一行字允许宽度为280时,判断需要写小点,比如260) 二、获取临时地址并设置图片 [代码]let mainImg = "https://demo.com/url.jpg"; wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { //处理图片纵横比例过大或者过小的问题!!! let h = res.height; let w = res.width; let setHeight = 280, //默认源图截取的区域 setWidth = 220; //默认源图截取的区域 if (w / h > 1.5) { setHeight = h; setWidth = parseInt(280 / 220 * h); } else if (w / h < 1) { setWidth = w; setHeight = parseInt(220 / 280 * w); } else { setHeight = h; setWidth = w; }; console.log(setWidth, setHeight) ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); ctx.draw(true); }, fail: function (res) { //失败回调 } }); [代码] 在开发过程中如果封面图无法按照约定的比例(280x220)给到: 那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度100%,过宽的图是高度100%。 在canvas中draw图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的临时路径。 微信官方提供两个API: wx.downloadFile(OBJECT)和wx.getImageInfo(OBJECT)。都需先配置download域名才能生效。 三、裁切“圆形”头像画图 [代码]ctx.save(); //保存画图板 ctx.beginPath()//开始创建一个路径 ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.closePath(); ctx.drawImage(headImageLocal, 20, 10, 30, 30); ctx.draw(true); ctx.restore()//恢复之前保存的绘图上下文 [代码] 使用图形上下文的不带参数的clip()方法来实现Canvas的图像裁剪功能。该方法使用路径来对Canvas话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用clip()方法来设置裁剪区域。 需要注意的是裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小,也就是说画布是越切越小的,要想保证最后仍然能在canvas最初定义的大小下绘图需要注意save()和restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~ 小程序 canvas 裁切BUG [代码]ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); //第一个填充矩形 wx.downloadFile({ url: headUri, success(res) { ctx.beginPath() ctx.arc(50, 50, 25, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(res.tempFilePath, 25, 25); //第二个填充图片 ctx.draw() ctx.restore() ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); ctx.draw(true) ctx.restore() } }) [代码] clip裁切这个功能,如果有超过一张图片/背景叠加,则裁切效果失效。 错误参考:http://html51.com/info-38753-1/ 四、将canvas导出成虚拟地址 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'customCanvas', success: (res) => { console.log(res.tempFilePath) //为canvas的虚拟地址 } }) res: { errMsg: "canvasToTempFilePath:ok", tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" } [代码] 这里需要把canvas里面的内容,导出成一个临时地址才能保存在相册,比如: http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg 五、询问并获取访问手机本地相册权限 [代码]wx.getSetting({ success(res) { console.log(res) if (!res.authSetting['scope.writePhotosAlbum']) { //判断权限 wx.authorize({ //获取权限 scope: 'scope.writePhotosAlbum', success() { console.log('授权成功') //转化路径 self.saveImg(); } }) } else { self.saveImg(); } } }) [代码] 判断是否有访问相册的权限,如果没有,则请求权限。 六、保存到用户手机本地相册 [代码]wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '保存到系统相册成功', icon: 'success', duration: 2000 }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取权限成功,给出再次点击图片保存到相册的提示。') } else { console.log('获取权限失败,给出不给权限就无法正常使用的提示') } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }); } }, complete(res) { console.log(res); } }) [代码] 保存到本地需要一定的时间,需要加一个loading的状态。 七、关于组件中引用canvas [代码]let ctx = wx.createCanvasContext('posterCanvas',this); //需要加this [代码] 在components中canvas无法选中的问题: 在components自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas> ,如果省略则不在任何自定义组件内查找。
2021-09-13 - canvas画图随记
最近画了一张分享图,在此记录一下遇到的问题及解决方法。 画布尺寸自适应 微信小程序尺寸为rpx,会自适应各种机型,但canvas的方法参数默认为px,所以需要对画布上的每一项参数乘以(画布宽度/设备屏幕宽度),将rpx换算成px,达到尺寸自适应的目的,所以将此系数设置为全局变量。代码如下: [代码]var app = getApp(); const device = wx.getSystemInfoSync(); const width = device.windowWidth;//设备屏幕宽度 const xs = width / 375; [代码] 调用: [代码]createCard: function() { var context = wx.createCanvasContext('myCanvas'); context.fillText('内容', 100 * xs , 100 * xs) } [代码] 长文本换行 由于fillText只能画一行,但很多情况下是需要将长文本自动换行展示的,这个时候则需要对文本进行处理。 方法:遍历该文本,计算出每一字宽度之和,当该宽度大于文本最大宽度时绘制当前截取部分,并将绘制高度加上行高,宽度置0,重新计算并绘制下一行。当只剩最后一字时,绘制剩余部分。 缺陷:当文本内有换行符时,绘制会换行,但当前计算宽度不会增加,导致格式混乱。所以需要在计算宽度之和前判断该字符是否为换行符,若果是,则绘制当前部分,开始下一行的计算。 完善:如果需要知道绘制文本的总高度,设置初始文本高度为0,在绘制一行时加上行高则可。代码如下: [代码] /** * context:当前画布对象 * text:文本内容 * leftWidth:文本左上角x坐标 * initHeight:文本左上角y坐标 * canvasWidth:一行文本最大宽度 */ drawText: function(context, text, leftWidth, initHeight, canvasWidth) { var lineWidth = 0; //文本宽度 var textHeight = 0; //文本总高度 var lastSubStrIndex = 0; //每次开始截取的字符串的索引 for (let i = 0; i < text.length; i++) { if (text[i] == "\n") { //如遇换行 context.fillText(text.substring(lastSubStrIndex, i), leftWidth, initHeight, canvasWidth); //绘制截取部分 initHeight += 17.5 * xs; //17.5为字体高度 lineWidth = 0; lastSubStrIndex = i + 1; //截取字符串时跳过换行符 textHeight += 17.5 * xs; } else { lineWidth += context.measureText(text[i]).width; //计算每个字的宽度之和 if (lineWidth > canvasWidth) { context.fillText(text.substring(lastSubStrIndex, i), leftWidth, initHeight, canvasWidth); initHeight += 17.5 * xs; lineWidth = 0; lastSubStrIndex = i; textHeight += 17.5 * xs; } } if (i == text.length - 1) { //绘制剩余部分 context.fillText(text.substring(lastSubStrIndex, i + 1), leftWidth, initHeight, canvasWidth); textHeight += 17.5 * xs; } } return textHeight; }, [代码] 调用: [代码] var text = '新建项目选择小程序项目,选择代码存放的硬盘路径,填入刚刚申请到的小程序的 AppID,给你的项目起一个好听的名字,最后,勾选 "创建 QuickStart 项目" (注意: 你要选择一个空的目录才会有这个选项),点击确定,你就得到了你的第一个小程序了,点击顶部菜单编译就可以在微信开发者工具中预览你的第一个小程序。'; context.setFontSize(15 * xs) that.drawText(context, text, 30 * xs, 100 * xs, 320 * xs) [代码] 高度自适应 如碰到画布高度需要根据内容高度不同而不同,或者某元素与可变化高度的元素固定距离的情况,则需要计算出可变化元素高度,再根据该高度进行计算其他高度。例如: [图片] 微信图标始终距离文本30px,而该文本高度可变,所以图标的左上角y轴坐标=文本y轴坐标+文本高度+下边距,代码如下: [代码]var textHeight = that.drawText(context, text, 30 * xs 100 * xs, 320 * xs) context.drawImage('/images/wx.png', 68 * xs, (100 + 30) * xs + textHeight, 80 * xs, 80 * xs) [代码] 注意:因为计算文本高度的方法里已经乘过系数,所以这里不需要乘。宽度自适应同理。 绘制圆角矩形框 由于没有绘制圆角矩形的方法,所以需要将圆角矩形分开绘制。 方法:将四个圆角当成四分之一圆绘制,然后分别画四条边,坐标如下图所示。 [图片] 代码: [代码] /** * context:当前画布对象 * x:圆角矩形左上角x坐标 * y:圆角矩形左上角y坐标 * w:宽度 * h:高度 * r:border-radius * color:填充颜色 */ roundRect(ctx, x, y, w, h, r, color) { ctx.beginPath() // 左上角 ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5) // 上边框 ctx.moveTo(x + r, y) ctx.lineTo(x + w - r, y) ctx.lineTo(x + w, y + r) // 右上角 ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2) // 右边框 ctx.lineTo(x + w, y + h - r) ctx.lineTo(x + w - r, y + h) // 右下角 ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5) // 下边框 ctx.lineTo(x + r, y + h) ctx.lineTo(x, y + h - r) // 左下角 ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI) // 左边框 ctx.lineTo(x, y + r) ctx.lineTo(x + r, y) //填充颜色 ctx.setFillStyle(color); ctx.fill() ctx.closePath() } [代码] 调用: [代码]that.roundRect(context, 15 * xs, 60 * xs, 350* xs, 200 * xs, 14 * xs, '#ffffff') [代码] 文本加粗 官方文档里有说到font的使用规则与css语法一致,有几个需要注意的地方,否则可能会导致设置无效。 [图片] 调用: [代码] context.font = "normal bold 27px sans-serif"; context.setFontSize(27 * xs) context.fillText('加粗字体', 100 * xs , 145 * xs) [代码] 效果: [图片] 注意:在真机上若没有写第一个normal参数,则不能成功设置。 字体大小可以在下面重新赋值。 如果没有效果可以注意console有没有如下图所示 设置无效的警告,原因很大可能是因为参数写的不对。 [图片] 圆形头像绘制 方法:在画布上剪切一个圆,然后在圆上画头像,最后恢复即可。有一个需要注意的地方,drawImage方法只能绘制本地图片,如果需要绘制网络图片需下载完成之后再画。代码如下: [代码] context.save() context.beginPath() context.arc(77 / 2 * xs + 150 * xs, 77 / 2 * xs + 73 * xs, 77 / 2 * xs, 0, Math.PI * 2, false) context.clip() var headimg = '/images/headimg.jpg' context.drawImage(headimg, 150 * xs, 73 * xs, 77 * xs, 77 * xs) context.restore() context.draw(); [代码] 遇到的问题:当图片为长方形时,强行将图片压缩为正方形会导致头像变形。 解决办法:image组件里,参数mode有一个值为aspectFill,即保持纵横比缩放图片,只保证图片的短边能完全显示出来,我们参考这种思路来截取图片。 [图片] 这里以宽比高长的图为例。如上图所示,圆为头像显示位置,线为中线,矩形框为一张宽大于高的图片。矩形左上角即为画图时的左上角坐标。截部分如图所示,得到图片宽高后,短边固定为头像尺寸,长边根据短边缩放比计算得到。图片宽=原图宽 /(头像高 / 原图高)。左上角的x轴坐标为:中线x坐标 - 图片宽 / 2。代码如下所示: [代码] context.save() context.beginPath() context.arc(77 / 2 * xs + 150 * xs, 77 / 2 * xs + 73 * xs, 77 / 2 * xs, 0, Math.PI * 2, false) context.clip() var headimg = '/images/headimg.jpg'; //头像路径 var headimgHeight = 0; var headimgWidth = 0; wx.getImageInfo({ src: headimg, success(res) { headimgHeight = res.height; //原图高度 headimgWidth = res.width; //原图宽度 //当宽 > 高时 if (headimgWidth > headimgHeight) { var width = headimgWidth / (headimgHeight / (77 * xs)); //图片宽度 var x = (150 + 77 / 2) * xs - width / 2; //x轴坐标 context.drawImage(headimg, x, 73 * xs, width, 77 * xs) } else { //当高>=宽时 var height = headimgHeight / (headimgWidth / (77 * xs)); //图片高度 var h = (73 + 77 / 2) * xs - height / 2; //y轴坐标 context.drawImage(headimg, 150 * xs, h, 77 * xs, height) context.restore() context.draw(); } } }) [代码] 注意:这里得到的图片宽高已经是px为单位,所以不乘系数。
2019-03-18 - Scroll-View 组件的scroll-x属性不起作用
When using "scroll-y" property within the scroll-view component, the image shows as below: [图片] the code as below: [代码]<[代码][代码]scroll-view[代码] [代码]class[代码][代码]=[代码][代码]""[代码] [代码]scroll-y [代码][代码]style[代码][代码]=[代码][代码]"height:250px;"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]id[代码][代码]=[代码][代码]"green"[代码] [代码]class[代码][代码]=[代码][代码]"scroll-view-item_H bc_green"[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]id[代码][代码]=[代码][代码]"red"[代码] [代码]class[代码][代码]=[代码][代码]"scroll-view-item_H bc_red"[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]id[代码][代码]=[代码][代码]"yellow"[代码] [代码]class[代码][代码]=[代码][代码]"scroll-view-item_H bc_yellow"[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]id[代码][代码]=[代码][代码]"blue"[代码] [代码]class[代码][代码]=[代码][代码]"scroll-view-item_H bc_blue"[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码]</[代码][代码]scroll-view[代码][代码]>[代码] When using "scroll-x"property within the scroll-view component, the image shows as below (same result as above): [图片] the code as below: [代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"section section_gap"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"section__title"[代码][代码]>horizontal scroll</[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]scroll-view[代码] [代码]class[代码][代码]=[代码][代码]"scroll-view_H"[代码] [代码]scroll-x [代码][代码]style[代码][代码]=[代码][代码]"width:100%;"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]id[代码][代码]=[代码][代码]"green"[代码] [代码]class[代码][代码]=[代码][代码]"scroll-view-item_H bc_green"[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]id[代码][代码]=[代码][代码]"red"[代码] [代码]class[代码][代码]=[代码][代码]"scroll-view-item_H bc_red"[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]id[代码][代码]=[代码][代码]"yellow"[代码] [代码]class[代码][代码]=[代码][代码]"scroll-view-item_H bc_yellow"[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]id[代码][代码]=[代码][代码]"blue"[代码] [代码]class[代码][代码]=[代码][代码]"scroll-view-item_H bc_blue"[代码][代码]></[代码][代码]view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]scroll-view[代码][代码]>[代码][代码]</[代码][代码]view[代码][代码]>[代码] 有人可以帮助solve 这个问题吗?
2018-07-07 - 新手贴之云函数如何获取云存储的文件夹信息
最近在学习微信云开发,不得不感叹云开发真的厉害哈,对于前端开发来说,在这种serverless模式下,很多需要后台的工作,只需要查看官方文档调用各种接口,数据就可以信手拈来~ 但是我看到云开发里的存储可以建立一个个文件夹的时候 如果想要在云函数里查看文件夹里的图片的时候,一个个id地调用出来太长了,太麻烦了 [图片] 这里的微信开发文档里面并没有说接口去查看文件夹里的信息https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html 然后就出现了这一幕https://developers.weixin.qq.com/community/develop/doc/000ce2df44cf48d9f46bef1a051800 接着热心的赵大大告诉我要去Node.js 管理端 SDK API 参考 这里去看 啊原来还分小程序API和服务端API啊[图片] [图片] 好家伙,一打开这里,萌新的我简直发现新世界。但是实际告诉这里一个高级的副本,想要刷这个副本,你得对node.js和npm有了解才行 话说回来,我就不信我还不会用这个接口(菜鸡理直气壮)。 没错就是它了,列出文件夹里的所有文件!listDirectoryFiles [图片] [图片] 看着文档代码我咔咔一顿复制粘贴 [图片] 结果,控制台各种泛红。[图片] [图片] 这时候赵大大发来了热(chao)心(feng)问候:[图片]。 垂头丧气的我,仔细查阅文档。 第一步:先在需要用的云函数上安装 npm install --save wx-server-sdk@latest [图片] 第二步:执行 npm install [图片] 第三步:引入 这里建议尽量使用第一个了,因为用import语法的话,可能会报错。因为好像是Node.js运行时不支持的任何JavaScript语法都会引发UserCodeSyntaxError: SyntaxError: Unexpected identifier这个问题。 [图片] 第四步:快乐coding并查看数据 [图片] [图片] 最后拿到数据就可以根据result里面的路径为所欲为了哈哈 下面有注意的几个点: 1.SDK初始化的时候,如果是在云函数里面使用的,可以免密钥初始化(这里可是吃一堑长一智[图片]) 毕竟本萌新可是赵师傅热心的指导下走出新手村的 赵老师: [图片] [图片] 2.代码不能直抄,一不小心就会出现这种报错Class constructor CloudBase cannot be invoked without 'new' 出现这种问题,多半是老师的结构跟我的不一样,这种高端的语法怎么适合我这种萌新呢,还是老老实实,初始化写在外面再+一个export main吧 [图片] 3.一定要仔细查看文档。本萌新血泪教训,急于求成只会弄巧成拙。例如: 赵老师:[图片] 赵老师:[图片] 最后的最后,感谢赵大大赵老师~~~~!在您的英明指引下,本萌新成功使用到了高端的接口~~ 虽然您的反复劝退 赵老师:[图片] 赵老师:[图片] 赵老师:[图片] 但是本萌新不会放弃的!撒花,谢谢[图片]
2020-12-23 - 小程序读取excel表格数据,并存储到云数据库
最近一直比较忙,答应大家的小程序解析excel一直没有写出来,今天终于忙里偷闲,有机会把这篇文章写出来给大家了。 老规矩先看效果图 [图片] 效果其实很简单,就是把excel里的数据解析出来,然后存到云数据库里。说起来很简单。但是真的做起来的时候,发现其中要用到的东西还是很多的。不信。。。。 那来看下流程图 流程图 [图片] 通过流程图,我看看到我们这里使用了云函数,云存储,云数据库。 流程图主要实现下面几个步骤 1,使用wx.chooseMessageFile选择要解析的excel表格 2,通过wx.cloud.uploadFile上传excel文件到云存储 3,云存储返回一个fileid 给我们 4,定义一个excel云函数 5,把第3步返回的fileid传递给excel云函数 6,在excel云函数里解析excel,并把数据添加到云数据库。 可以看到最神秘,最重要的就是我们的excel云函数。 所以我们先把前5步实现了,后面重点讲解下我们的excel云函数。 一,选择并上传excel表格文件到云存储 这里我们使用到了云开发,使用云开发必须要先注册一个小程序,并给自己的小程序开通云开发功能。这个知识点我讲过很多遍了,还不知道怎么开通并使用云开发的同学,去翻下我前面的文章,或者看下我录的讲解视频《5小时入门小程序云开发》 1,先定义我们的页面 页面很简单,就是一个按钮如下图,点击按钮时调用chooseExcel方法,选择excel [图片] 对应的wxml代码如下 [图片] 2,编写文件选择和文件上传方法 [图片] 上图的chooseExcel就是我们的excel文件选择方法。 uploadExcel就是我们的文件上传方法,上传成功以后会返回一个fildID。我们把fildID传递给我们的jiexi方法,jiexi方法如下 3 把fildID传递给云函数 [图片] 二,解下来就是定义我们的云函数了。 1,首先我们要新建云函数 [图片] 如果你还不知道如何新建云函数,可以翻看下我之前写的文章,也可以看我录的视频《5小时入门小程序云开发》 如下图所示的excel就是我们创建的云函数 [图片] 2,安装node-xlsx依赖库 [图片] 如上图所示,右键excel,然后点击在终端中打开。 打开终端后, 输入 npm install node-xlsx 安装依赖。可以看到下图安装中的进度条 [图片] 这一步需要你电脑上安装过node.js并配置npm命令。 3,安装node-xlsx依赖库完成 [图片] 三,编写云函数 我把完整的代码贴出来给大家 [代码]const cloud = require('wx-server-sdk') cloud.init() var xlsx = require('node-xlsx'); const db = cloud.database() exports.main = async(event, context) => { let { fileID } = event //1,通过fileID下载云存储里的excel文件 const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent const tasks = [] //用来存储所有的添加数据操作 //2,解析excel文件里的数据 var sheets = xlsx.parse(buffer); //获取到所有sheets sheets.forEach(function(sheet) { console.log(sheet['name']); for (var rowId in sheet['data']) { console.log(rowId); var row = sheet['data'][rowId]; //第几行数据 if (rowId > 0 && row) { //第一行是表格标题,所有我们要从第2行开始读 //3,把解析到的数据存到excelList数据表里 const promise = db.collection('users') .add({ data: { name: row[0], //姓名 age: row[1], //年龄 address: row[2], //地址 wechat: row[3] //wechat } }) tasks.push(promise) } } }); // 等待所有数据添加完成 let result = await Promise.all(tasks).then(res => { return res }).catch(function(err) { return err }) return result } [代码] 上面代码里注释的很清楚了,我这里就不在啰嗦了。 有几点注意的给大家说下 1,要先创建数据表 [图片] 2,有时候如果老是解析失败,可能是有的电脑需要在云函数里也要初始化云开发环境 [图片] 四,解析并上传成功 如我的表格里有下面三条数据 [图片] 点击上传按钮,并选择我们的表格文件 [图片] 上传成功的返回如下,可以看出我们添加了3条数据到数据库 [图片] 添加成功效果图如下 [图片] 到这里我们就完整的实现了小程序上传excel数据到数据库的功能了。 再来带大家看下流程图 [图片] 如果你有遇到问题,可以在底部留言,我看到后会及时解答。后面我会写更多小程序云开发实战的文章出来。也会录制本节的视频出来,敬请关注。
2019-11-12 - 调用wx.uploadFile在开发工具和真机调试都可以提交图片,但是上线后提交无反应?
上传图片的接口是'http://47.114.106.14:8991/app/uploadPicture uploadFile合法域名 https://www.zhulicloud.com在开发者工具和真机调试都是可以提交图片病接收到后台的数据返回,但是真实上线后,点击提交图片却只能将上传的图片展示出来,提交之后就没有任何效果,提交之后没没往下执行了。并且后台也没有打印日志,是不是根本就没调用这个接口?如果真实上线的小程序没有调用上传图片的接口,但是在真机调试和开发者工具中都是可以的。
2021-08-09 - 云函数调用security.imgSecCheck接口返回"errcode": 41005?
提交检测的是wx.shooseImage上传图片返回的tempFilePaths,云函数调用日志: [图片][图片] 云函数和调用如图 {"errCode":41005,"errMsg":"openapi.security.imgSecCheck:fail media data missing rid: 5f914f73-61ac7eb9-409087ed"}
2020-10-22 - 《非经营性互联网信息服务备案核准》就是ICP/IP地址/域名信息备案。哪小程序云开发怎么把域名备案?
[图片] 用的小程序云开发,有用户发布功能。要《非经营性互联网信息服务备案核准》。哪要域名备案。小程序有域名吗?这么小程序云开发备案?
2020-08-31 - 免费ICP备案攻略。不花1分钱拥有一台云服务器并顺利ICP备案。
写在前面: 大家不要将ICP证和ICP备案搞混了。 ICP证指的是【电信增值业务经营许可证】,这个资质需要企业主体至少100万注金,去工信部办理,比较难办理;社交-交友需要ICP证。 而ICP备案,【非经营性互联网信息服务备案核准】仅仅是指企业主体的域名备案,可以简单的按以下步骤免费办理成功,其他社交类目如社区、论坛、笔记等,只需要ICP备案即可。 1、在腾讯云注册一个账号并认证企业主体(不吹不黑,开发小程序当然首选腾讯云,好用)。http://www.qcloud.com/ 如果你是个人主体,就不要往下看了,没必要折腾了。 2、找到腾讯云免费活动页:https://cloud.tencent.com/act/free?from=10107 3、选择一款云服务器,180天免费试用。 云服务器申请成功后,它的使命就完成了,没用了,让它自生自灭吧。 在整个备案过程中,也不需要部署网站(域名都没有备案,哪来的网站?)。 [图片] 云服务器180天到期后,可以自己决定是否续费,每个月也才99元,促销期甚至更低,完全可以接受吧。 备案成功后,该服务器就没什么作用了,让它180天后自然欠费销毁得了。 服务器销毁后会有什么影响?答:没有任何影响。 但是。。。。。 你备案的域名最后还得指向一个网站,因为腾讯云会应工信部的要求定期检查网站是否合规,所以你还是要建一个简单的网站,(备案期间,可以暂时不管网站的事,等将来需要的时候再管理)。 至于有多简单,答,多简单都行。此时你可以在七牛、腾讯云、阿里云租点免费的对象存储空间,做个简单的网站。 4、在进行ICP备案之前,你需要在腾讯云注册你的域名地址,如果你已有域名,但不在腾讯云,建议先将要域名过户到腾讯云的账号上。 5、进入控制台,开始ICP备案,这个流程就不介绍了,因为完全一看就懂。而且现在使用备案小程序后,不需要幕布或现场拍照了,极其方便,大家跟着流程走就一点问题没有,有人脸识别和在线拍一段小视频。另外,大家可以随便作,随便填,填错或者填得不合适也不用怕,会有专门的备案客服打电话告诉你哪哪要改,还会告诉你应该怎么填才更容易通过工信部的审核,客服的态度好得发指。 仅说一点其中的几个小坑: a、人脸识别的时候,白色背景、白色背景、白色背景,笔者在人脸识别的时候,满世界找白墙,结果还被打回来重拍了3次。 b、网站用途一律写:公司官网,好通过工信部审核。 6、腾讯云提交资料到工信部审核。这是一个漫长的让人无语的等待,20-30天。笔者最近两次都是20天才过审;不要幻想会有可能提前完成审核,这是政府部门在审核,提前完成说明某政府人员的工作安排有问题,会犯错误的。 7、备案成功后,会有短信通知你,但是,你需要去工信部网站查询结果,并将结果切屏拷贝下来,因为小程序类目审核需要上传这张图片。http://beian.miit.gov.cn/publish/query/indexFirst.action [图片] 把上面这张图片保存好,小程序类目审核的时候需要上传。收到通知后,如果在这里查不到结果,也别急,据说需要24小时。 8、接下来是小程序上线审核。 因为ICP备案的小程序内容肯定涉及到社交,最后小程序上线时还要提交到工信部审核,还需要7天左右的时间,加上前面ICP备案的时间,加起来怎么也得30-40天。大家估计时间,别影响小程序上线。这7天也是政府部门在审核,不要幻想会提前。 9、计算一下时间: 腾讯云注册账号和认证:1-3天; 域名备案:腾讯云环节:1-3天; 域名备案:工信部环节:20-30天; 小程序添加服务类目:社交类目审核:1-3天; 小程序上线审核:腾讯环节:1-2天; 小程序上线审核:工信部环节:7+天; 总天数:30-40天; 10、节省时间的一些建议: 在开发小程序之前,就开始备案工作,小程序可以同时开发,相互不影响; 在开发完成之前一、两星期之内,先发布一版小程序,别管功能是不是完整,能通过审核就行,这样会有7天的等待类目审核的时间,这个时间里,小程序可以照常开发,不影响进度; 只要是社交类,基本需要有文字和图片安全检查功能,别忘了加上,别到时审核通过不了。 11、结束。 [图片]
2021-01-19 - 微信小程序获取云存储中的文件列表
安装依赖 npm i @cloudbase/manager-node 云函数 const cloud = require('wx-server-sdk') const CloudBase = require('@cloudbase/manager-node') /* 初始化 */ cloud.init() const { storage } = new CloudBase() exports.main = async (event, context) => { /* listDirectoryFiles(cloudPath: string): Promise列出文件夹下所有文件的名称 downloadDirectory(options): Promise下载文件夹 listCollections(options: object): object来获取所有集合的名称,然后使用export(collectionName: string, file: object, options: object): object接口来导出所有记录到指定的json或csv文件里。 */ const res = await storage.listDirectoryFiles('images/') console.log(res) return { data: {}, } } 返回的文件列表: [图片]
2021-03-10 - (7)小程序音频能力介绍
小程序支持播放和录制音频。小程序播放音频的方式有两种:内部音频和背景音频。 1.内部音频支持用户在使用小程序过程中播放音效; 2.背景音频支持在用户离开小程序后继续播放音效。 一、播放音频 (一)背景音频 播放背景音频 背景音频接口适用于音乐类小程序,如“音乐站”、“QQ 音乐小电台”。通过 wx.getBackgroundAudioManager() 接口可以获取全局唯一的背景音频管理器,所有关于背景音频的操作都由它来实现。 微信内只有一个背景音频,一个小程序开始播放背景音频之后,就持有背景音频播放器,只要当前小程序持有背景音频播放器,即使这个小程序进入后台(即用户离开小程序),也可以继续使用背景音频接口,且当前小程序不会被微信主动回收;一旦背景音频播放器被抢占(可能是其他小程序、微信内其他音乐、其他 App 的音乐),则小程序不再持有背景音频播放器。 [图片] [图片] (音乐站小程序) 在系统播放面板显示和控制 通过设置标题、专辑名、歌手名、封面图等属性,小程序音频接口支持在系统音乐播放面板显示出来。通过响应系统面板的点击事件([代码]onPrev[代码],[代码]onNext[代码]),可以实现列表播放。 [图片] (系统播放面板控制效果) (二)内部音频播放内部音频内部音频适用于所有小程序,尤其是游戏类目的小程序,如“跳一跳”。通过 wx.createInnerAudioContext() 接口可以创建一个音频实例。 [图片] 每个小程序可以同时持有和播放多个内部音频,但一旦小程序进入后台(onHide),所有内部音频都会被暂停,且在用户回到前台(即打开小程序)之前无法再被播放。 静音下也能播放在 iOS 系统中,内部音频默认遵循静音键设置。如果希望在静音时也能播放,可以设置 [代码]obeyMuteSwitch[代码] 为 [代码]false[代码]。 [图片] 安卓系统没有统一的静音开关,暂不支持此特性。 处理音频中断事件以游戏为例,在游戏中,经常有播放使用内部音频来播放游戏背景音乐的场景。音频中断事件指的是在游戏期间,音频被系统打断时触发的事件。音频中断事件分为中断开始和中断结束事件,分别使用 wx.onAudioInterruptionBegin() 和 wx.onAudioInterruptionEnd() 来监听。 以下事件会触发音频中断开始事件:接到电话、闹钟响起、系统提醒、收到微信好友的语音/视频通话请求。被中断之后,小游戏内所有音频会被暂停,并在中断结束之前都不能再播放成功。 中断结束之后,被暂停的音频不会自动继续播放,游戏可监听音频中断结束事件,并在收到中断结束事件之后调用背景音乐继续播放。 [图片] 如果游戏的逻辑强依赖音乐的播放(如音乐类游戏),需要在音频开始中断的时候暂停游戏 [图片] [图片] (跳一跳小游戏) 二、录制音频 通过 wx.getRecorderManager 接口,可以获取全局唯一的录音管理器。 [图片] 实现边录边传 默认情况下,录音结束后会生成一个本地文件,并通过回调返回本地文件的地址。对于实时性要求比较高的小程序(如“面对面翻译”),可以通过设置 [代码]frameSize[代码] 参数来设置一个帧的大小,这样每录制指定帧大小的内容后,会通过 [代码]onFrameRecorded[代码] 回调返回本次分片的数据。 [图片] 注意事项:不建议使用的历史接口上述接口可以满足所有音频相关的需求。除了上述接口,小程序内还有若干跟音频相关的接口(如 [代码]wx.startRecord[代码]、[代码]wx.playVoice[代码]、[代码]wx.playBackgroundAudio[代码] 等)。这些接口由于早期设计存在一些缺陷,我们不建议继续使用。
2018-08-17 - 云函数触发定时器的时间可以动态设置吗?
动态传递一个时间给定时器应该怎么做?
2020-07-18 - 如何提升你的云函数性能
在使用云开发一段时间后,你一定会遇见一个问题:虽然云函数非常的方便,但我的云函数似乎性能不够好,为什么我的云函数每次加载都需 2 ~ 3 秒种,时间太长了!。 这篇文章,就来告诉你,应该如何提升你的云函数性能。 如何了解云函数运行情况? 在了解如何优化云函数的运行情况之前, 我们需要先了解,如何查看当前的云函数运行情况,这样才能有个对比。 [图片] 打开小程序开发者工具,并打开你的项目 进入到你要调试的页面,打开调试器 调用云函数,并在调试器中切换到 Network 页面,找到你的请求。 点击你的请求,然后切换到 Timing 页面,查看具体的情况。 在这个页面中,你可以理解其中的 Waiting(TTFB) 是你发起请求到你接收到返回结果的第一个字节的时间,简单的来说,就是服务器计算结果需要花费的时间。而下方的 Content Download 则是下载内容所需的时间,你可以理解是表现出网络速度快慢的数据。 总结来说,就是如果 Waiting TTFB 的值比较大,你就去优化云函数性能。如果 Content Download 的数值毕竟大,你就需要优化网络情况 优化 Waiting TTFB 云函数的运行机制 Waiting TTFB 的优化是云函数性能端的优化,那么在优化之前,我们就需要先来了解一下云函数的运行机制,以便帮助我们了解应该如何去进行性能优化。 [图片] 在蕴含运行时,具体的顺序是这样的 用户发起请求,请求发送到云开发的后台 云开发后台的调度器将请求分发给下方的执行的 worker 、容器 容器创建环境、下载代码 执行代码 在这个过程中,发起请求到云开发、调度器调度速度、调度器传递信息到容器、函数调用等,都是可以优化的,但是我们在具体的使用过程中。这些大都需要由云开发的工作人员来完成,对于我们自己来说,只能去尽可能的优化容器内部到代码层面的东西。 接下来,我们可以看看更细致的调用逻辑。 [图片] 在云开发中,我们可以将调用分为三种类型: 冷启动:图中的红色阶段,需要重新创建容器、下载代码,耗时最长 温启动:图中的黄色阶段,需要下载代码,耗时较长 热启动:图中的蓝色阶段,不需要下载代码,耗时最短 我们可以看到,最快的,是热启动,函数不需要创建容器,不需要启动函数就可以完成执行,显然比要创建容器或要下载代码的温启动和冷启动速度更快。这样,我们就得到了优化云函数性能的第一个方法 1. 让你的云函数每次调用都走热启动 当我们可以让我们的云函数的每一次调用都走热启动,少了容器的创建和函数的部署,请求的速度理所当然的要比冷启动和温启动更快。 我们可以测试一下,我设置每秒调用一次云函数,看看 TTFB 的变化。 [代码]setInterval(()=>{wx.cloud.callFunction({name:'profile'})},1000) [代码] 函数内代码是默认创建的云函数代码。 则对应的执行效果如下 [图片] 可以看到,函数的执行时间从第一次的 1.2s 降低到了 200ms左右,性能提升了 80%,我们仅仅是简单的提升了函数的调用频次,就可以实现提升函数的调用性能,这就是热启动带给我们的价值。 实施方案 如果你需要足够高的性能,不妨借助云开发的定时器,定期唤起你的容器,从而为你的容器保活,确保你的函数时刻被热启动。 2. 缩小你的函数大小 在前面我们曾介绍过,云函数在启动过程中,会创建容器和下载代码。创建容器的过程对于开发者来说不可控,不过我们可以使用一些方法,缩小我们的代码,提升代码的下载速度,比如说,缩小我们的函数代码。 这里我们可以做个测试,这里我创建了两个函数,两个函数的代码完全一致,不同的是,在实验组的函数中,我加入了一个 temp 变量的声明,这个变量的值是一个非常长的字符串,从而使得两个函数的大小分别是 68K 和 4K。 接下来,我们看看二者的执行时间。 [图片] 我们会发现,几乎没有差距的代码,因为加入了变量声明的因素,在性能上会略慢几毫秒,后续随着容器的不断复用,函数的之间的差距也越来越小,几乎可以忽视。 实施方案 对于你的代码,要尽可能的精炼,减少无用的代码,减少代码下载所需时间。 3. 削减不需要的 Package 除了下载代码以外,还需要下载 Node 环境运行所需的依赖包,虽然云开发可能针对 Node Modules 已经做了缓存,但依然存在下载的时间差区别,这里我也做了一个实验。 空包:什么都没装,把 wx-server-sdk 都卸载掉了。 复杂包:装了 Mongoose、sequelize、sails 等依赖的包。 函数逻辑上也相差无几,都是返回 Event ,则结果如下 [图片] 我们发现,前三次可能是因为涉及到依赖包的下载问题,所以前三次的时长大小对比特别的明显,而从第四次开始,二者的区别就不大了,可能是因为依赖已经完成了缓存,所以可以直接使用缓存来完成函数的执行。 实施方案 你可以选择看看你的 package.json ,看看其中是否有你不需要的依赖,将其删除,仅保留有需要的依赖,可以有效提升你的代码执行速度。 优化 Content Download 如果你想要优化 Content Download ,核心需要优化的是两个点: 手机到服务端的节点的距离和速度 内容的大小 前者一般来说,你可以通过切换不同的网络环境来实现优化,比如从 3G 切换到 4G ,从 4G 升级到 5G,这些都可以提升你的手机到服务端节点之间的速度。 此外,还可以借助内容分发网络 CDN 能力来完成缩小你到服务端节点之间的距离,不过对于云函数来说,因为你不可控,无法控制,所以这一点不再谈。 这里补充一句,云开发的文件存储都是有 CDN 的,因此,你通过云存储下载的文件才会比别人更快。 后者则一般通过调整代码来完成,比如只返回必须的资源,对于不需要的内容,不再返回,或压缩返回。 总结 最后,我们回顾一下这篇文章中介绍的优化云函数的方法: 函数下载性能优化 保持函数容器的热启动,提升函数启动性能 缩小函数大小,提升代码下载速度 削减不必要的包,减少依赖大小 网络优化 使用更好的网络,比如 Wi-Fi 云函数中仅返回所需要的内容,减少下载时间。 以上这些方法,你都在你的函数中试过么?有没有其他的优化方法?欢迎你与我分享。
2019-12-08