- wx.request偶尔一直pending,不返回
- 当前 Bug 的表现(可附上截图) 我们的小程序wx.request一个API的时候,偶尔会出现一直在pending,复制这个API直接在浏览器里打开的时候是正常的有数据,各地区用户都有反馈这个问题,排除了网络不行,排除手机问题,服务器和域名我们也检查了,没有发现问题,用户如果把小程序浏览记录删除掉,重新搜索小程序打开,又恢复正常了,一般是第一个接口出现这种情况,其他的接口也就这样了,在开发工具里测试一直正常,但是真机调试的时候,就会偶尔出现这种情况了!代码里我们检查了,没有多余的东西,只是简单的调用了一个api接口,全GET格式。这个问题也是最近反馈比较多,之前一直都没有出现过! getAuditStatus:function(fn){ wx.request({ url: this.domain + '/index.php?g=Wap&m=Wxa&a=get_audit_status&id=' + this.commit_id + '&token=' + this.token, success: function (res) { if (fn) { fn(res) } } }) }, - 预期表现 - 复现路径 - 提供一个最简复现 Demo
2019-05-30 - iOS 微信小程序在控制台中使用物理键盘输入“getCurrentPages()”会闪退
开小程序时的 query: 'redirectTo=%2Fpages%2Fdetails%2Fdetails%3Fid%3D998'
04-11 - canvas绘制毛玻璃背景分享海报
最近重新设计了分享海报,用毛玻璃作为背景,使整体更有质感,如果没有用到canvas,毛玻璃效果其实很好实现,给元素添加一个滤镜即可(比如:filter: blur(32px)),但是实践的过程中发现,canvas在IOS端一直没有效果,查了一个文档发现IOS端不支持filter。。。有点想骂人。。(PS:微信官方有关CanvasRenderingContext2D的文档还比较简略,更加详细的文档大家可以移步至https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D) [图片] 没办法,实在是喜欢毛玻璃的效果,决定想想办法迈过这道坎,继续网上查阅资料,查到大概的方法就是 1、用canvas上下文先画一个image,image的src就是要模糊处理的图片路径, 2、用canvas上下文把步骤1的image提取imageData,然后进行高斯模糊,高斯模糊的算法网上有挺多,但是拿来执行运算时间要7,8s,比较不理想,最后我是找到了一个stackblur-canvas的插件,这个插件也适用于H5,这里只需要调用里面高斯模糊算法的方法(stackBlur.imageDataRGBA)即可,执行下来几百毫秒就能出来,跟网上的算法差距还是很明显啊。。 3、最后把已经模糊处理的imageData绘制到位图中即可。 canvas滤镜的问题搞定了,还有一个小问题是开发者工具上面canvas的层级始终会比其他元素高,但在IOS真机上元素是可以布局在canvas上。由于不知道在其他客户端会不会也会像开发者工具那样有层级显示问题。所以保险起见我干脆就把canvas放在在页面可视区域底部,在可视区域用其他页面元素做了一个分享效果图出来。 底下两张图,左边是预览,右边是最终画布生成图片。可以看出wxss的滤镜效果还是会比通过第三方高斯算法处理后的模糊效果来的更自然舒服,不过整体效果也还在可接受范围内。 [图片] [图片] 附上核心代码,感谢阅读 组件shareList.wxml <view class="share-box" wx:if="{{visibility}}"> <!-- style="display:none;"--> <view class="share-view" style="opacity:{{shareView?1:0}}"> <view class="cover-image"> <view class="cover"></view> <image class="image" src="{{shareInfo.imgSrc}}"></image> </view> <view class="content-box"> <view class="detail"> <view class="up"> <view class="expand"> <view class="time" wx:if="{{shareInfo.date}}">{{shareInfo.date}}</view> <view class="place" wx:if="{{shareInfo.place}}">{{shareInfo.place}}</view> </view> <image mode="widthFix" class="image" src="{{shareInfo.imgSrc}}" bind:load="imageLoaded"></image> </view> <view class="middle flex-end-vertical"> <image class="header-img" src="{{shareInfo.avatarUrl}}"></image> <image class="joiner-header-img" wx:if="{{shareInfo.joinerAvatarUrl}}" src="{{shareInfo.joinerAvatarUrl}}"></image> <!--<view class="nickname flex-full">showms</view>--> </view> <view class="down"> <view class="desc"><view class="title" wx:if="{{shareInfo.title}}">#{{shareInfo.title}}#</view>{{shareInfo.content}}</view> </view> </view> </view> </view> <view class="save-tool-bar {{device.isPhoneX?'phx_68':''}} flex-center" style="transform: translateY({{shareView?0:100}}%)"> <view class="op_btn flex-full"> <view class="icon-view" bindtap="close"> <image class="icon" src="/images/share/close.png"></image> </view> <text class="text" bindtap="close">关闭</text> </view> <view class="op_btn flex-full {{!allowSave?'disable':''}}"> <view class="icon-view" bindtap="save"> <image class="icon" src="/images/share/save.png"></image> </view> <text class="text" bindtap="save">保存到相册</text> </view> </view> <!--transform: translateY(100%);--> <canvas class="share-canvas" type="2d" id="myCanvas" style="position:absolute;top:0%;width:100%;height:100%;z-index:1000;transform: translateY(100%);"></canvas> </view> <!--toast--> <toast id="toast"></toast> 组件shareList.wxss @import "/app.wxss"; .share-box{ position: absolute; left: 0; right: 0; top: 0; bottom: 0; } .share-cover{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 1200; background-color: #111; opacity: 0.8; } .share-view{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 300; opacity: 0; transition: opacity .3s; } .share-view .cover-image{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; } .share-view .cover-image .image{ width: 100%; height: 100%; transform: scale(3); filter: blur(32px); } .share-view .cover-image .cover{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 120; background-color: rgba(255, 255, 255, .5); } .share-view .content-box{ /*padding: 300rpx 40rpx 40rpx; position: relative;*/ position: fixed; left: 40rpx; right: 40rpx; top: 50%; transform: translateY(-50%); margin-top: -80rpx; z-index: 500; box-sizing: border-box; } .share-view .content-box .detail{ background-color: #fff; box-sizing: border-box; /*padding: 70rpx 30rpx 30rpx;*/ border-radius: 36rpx; overflow: hidden; } .share-view .content-box .detail .up{ box-sizing: border-box; position: relative; } .share-view .content-box .detail .up .image{ width: 100%; /*height: auto;*/ display: block; } .share-view .content-box .detail .up .expand{ position: absolute; right: 20rpx; bottom: 20rpx; z-index: 10; text-align: right; font-size: 22rpx; font-weight: 500; color: #fff; text-shadow: 0px 0px 10rpx rgba(158, 163, 175, 1); } .share-view .content-box .detail .middle{ position: relative; } .share-view .content-box .detail .middle .header-img, .share-view .content-box .detail .middle .joiner-header-img{ width: 100rpx; height: 100rpx; border-radius: 50%; border: 6rpx solid #fff; box-sizing: border-box; margin-left: 24rpx; margin-right: 10rpx; margin-top: -64rpx; position: relative; z-index: 80; display: block; } .share-view .content-box .detail .middle .joiner-header-img{ z-index: 60; margin-left: -60rpx; } .share-view .content-box .detail .middle .nickname{ font-size: 30rpx; font-weight: 500; color: #434343; padding-bottom: 10rpx; display: none; } .share-view .content-box .detail .down{ padding: 10rpx 30rpx 70rpx; } .share-view .content-box .detail .down .title{ font-size: 28rpx; font-weight: 500; color: #303133; margin-bottom: 10rpx; margin-right: 10rpx; display: inline; } .share-view .content-box .detail .down .desc{ /*font-size: 28rpx; font-weight: 500; color: #303133; display: inline;*/ font-size: 28rpx; font-weight: 500; color: #303133; line-clamp: 2; box-orient: vertical; text-overflow: ellipsis; overflow: hidden; /*将对象作为弹性伸缩盒子模型显示*/ display: -webkit-box; /*从上到下垂直排列子元素(设置伸缩盒子的子元素排列方式)*/ -webkit-box-orient: vertical; /*这个属性不是css的规范属性,需要组合上面两个属性,表示显示的行数*/ -webkit-line-clamp: 2; height: 76rpx; } .share-canvas{ } .save-tool-bar{ position: absolute; left: 0; right: 0; bottom: 0; z-index: 1600; border-radius: 40rpx 40rpx 0 0; text-align: center; background-color: #f0f0f0; transform: translateY(100%); transition: transform .3s; } .save-tool-bar .op_btn{ text-align: center; padding: 50rpx 0 20rpx; transition: opacity .3s; } .save-tool-bar .op_btn .icon-view{ padding: 26rpx; background-color: #fff; border-radius: 50%; display: inline-block; margin-bottom: 10rpx; } .save-tool-bar .op_btn .icon{ display: block; width: 48rpx; height: 48rpx; } .save-tool-bar .op_btn .text{ display: block; font-size: 20rpx; font-weight: 400; } 组件shareList.js //获取应用实例 const app = getApp(); const tabbar = require('../../utils/tabbar.js'); const canvasHelper = require('../../utils/canvasHelper'); const fonts = require("../../utils/fonts.js"); let ctx, canvas; Component({ /** * 组件的属性列表 */ properties: {}, /** * 组件的初始数据 */ /*{left: 'rgba(26, 152, 252, 0.8)', right: 'rgba(26, 152, 252, 1)'}*/ data: { visibility: false, paddingLeft: 34, letterSpace: 2, width: 300, height: 380, shareView: false, shareInfo: { /*imgSrc: "cloud://ydw-49d951.7964-ydw-49d951-1259010930/love/images/default/dangao.jpg", avatarUrl: "cloud://test-wjaep.7465-test-wjaep-1259010930/images/ozzW05Gch7jMMhsn1r_SWLGdGtF0/avatarUrl_1668440942555.webp", joinerAvatarUrl: " https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83erg8t0El6jTaZY87icvjR71ww52VMibg8fONBgggRtYHTnR2tXibB0IRQ45dCVgNCX5BRhY0KibjfxjGA/132 ", title: "一起去旅游", content: "这里是内容啊,怎么没写内容呢这里是内容啊,怎么没写内容呢,我特知道当时的回复对方法国发过快速减肥", date: "2022-12-28", place: "四川成都", shareQrcode: "cloud://ydw-49d951.7964-ydw-49d951-1259010930/qrcode/mini-qrcode.jpg"*/ }, allowSave: false, }, ready() { const device = app.getSystemInfo(); this.setData({ device, }); this.toast = this.selectComponent('#toast'); }, /** * 组件的方法列表 */ methods: { save() { const {device} = this.data; let that = this; that.toast.showLoadingToast({text: "保存中", mask: false}); canvasHelper.saveImage( this, canvas, 0, 0, device.screenWidth, device.screenHeight, device.screenWidth * 5, device.screenHeight * 5 ).then(res => { that.toast.hideLoadingToast(); that.toast.showToast({text: "保存成功"}); that.close(); console.log(res); }).catch(res => { console.error("保存失败:", JSON.stringify(res)); that.toast.showToast({text: "保存失败"}); that.toast.hideLoadingToast(); }); }, imageLoaded: function (e) { console.log("图片加载完毕:", e); this.setData({shareView: true}); }, show(shareInfo) { console.log("开始显示画布:", shareInfo); this.setData({ shareInfo }); tabbar.hideTab(this); this.setData({visibility: true}, () => { canvasHelper.init(app, this, "#myCanvas").then(async (res) => { ctx = res.ctx; canvas = res.canvas; this.toast.showLoadingToast({text: "生成中", mask: false}); const {device} = this.data; //加大尺寸 const largerSize = 100; console.log("1.绘制毛玻璃背景图片:", { width: device.screenHeight + largerSize, height: device.screenHeight + largerSize, }); /*await canvasHelper.drawImage( canvas, ctx, shareInfo.imgSrc, -(device.screenHeight - device.screenWidth) / 2.0, -largerSize / 2, device.screenHeight + largerSize, device.screenHeight + largerSize, 190);*/ await canvasHelper.drawBlurImage( canvas, ctx, shareInfo.imgSrc, -(device.screenHeight - device.screenWidth) / 2.0, 0, device.screenHeight, device.screenHeight, 180); console.log("2.绘制毛玻璃覆盖层灰色背景"); canvasHelper.drawRoundRect(ctx, 0, 0, device.screenWidth, device.screenHeight, 0, 'rgba(255, 255, 255, .5)'); console.log("3.绘制内容承载区域"); const leftPadding = 20,//边距20 headerImgHeight = 50,//头像尺寸 descHeight = 40,//内容区域高度 descPaddingTop = 0,//内容区域paddingTop descPaddingBottom = 25,//内容区域paddingBottom adjustHeight = 40;//调节高度,人为设定 const contentWidth = device.screenWidth - leftPadding * 2; const contentHeight = contentWidth + headerImgHeight + descHeight + descPaddingTop + descPaddingBottom; canvasHelper.drawRoundRect( ctx, (device.screenWidth - contentWidth) / 2.0, (device.screenHeight - contentHeight) / 2.0 - adjustHeight, contentWidth, contentHeight, 18, 'rgba(255, 255, 255, 1)' ); console.log("4.绘制内容区域图片"); ctx.clip();//裁剪后父元素的圆角才会显示 await canvasHelper.drawImage( canvas, ctx, shareInfo.imgSrc, (device.screenWidth - contentWidth) / 2.0, (device.screenHeight - contentHeight) / 2.0 - adjustHeight, contentWidth, contentWidth, 0, ); ctx.restore(); console.log("5.绘制头像边框"); const headerSize = 50, borderWidth = 3, headerMarginLeft = 12; if (shareInfo.joinerAvatarUrl) { console.log("5.1.绘制共享对象头像"); await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.joinerAvatarUrl, leftPadding + headerMarginLeft + 30, (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2, headerSize, borderWidth, "#fff", ); console.log("5.2.绘制当前用户头像"); await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.avatarUrl, leftPadding + headerMarginLeft, (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2, headerSize, borderWidth, "#fff", ); } else { console.log("5.1.绘制当前用户头像"); await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.avatarUrl, leftPadding + headerMarginLeft, (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2, headerSize, borderWidth, "#fff", ); } console.log("6.绘制日期和地点"); let textPositionY = (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2 + 14; if (shareInfo.place) { console.log("6.1.绘制地点"); canvasHelper.drawText( ctx, shareInfo.place, leftPadding + contentWidth - 10, textPositionY, "right", 11, 400, fonts.list["SFRounded-Regular"].name, "#fff", "rgba(158, 163, 175, 1)", 0, 0, 5); textPositionY = textPositionY - 16 } if (shareInfo.date) { console.log("6.2.绘制日期"); canvasHelper.drawText( ctx, shareInfo.date, leftPadding + contentWidth - 10, textPositionY, "right", 11, 400, fonts.list["SFRounded-Regular"].name, "#fff", "rgba(158, 163, 175, 1)", 0, 0, 5); } if (shareInfo.title || shareInfo.desc) { console.log("7.绘制标题和内容", contentWidth); let leftContent = (shareInfo.title ? "#" + shareInfo.title + "# " : "") + shareInfo.content; //显示区域宽度 const displayWidth = contentWidth - 40; textPositionY = (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth + headerSize; const contentFontSize = 14, contentFontWeight = 600; for (let i = 1; i <= 2; i++) { let dynamicText = ""; for (let j = 0; j < leftContent.length;) { ctx.font = contentFontWeight + " " + contentFontSize + "px " + fonts.list["SFRounded-Semibold"].name; let metrics = ctx.measureText(dynamicText + leftContent[j]); if (metrics.width >= displayWidth) { //最后一行,最后一个字替换成省略号 if (i === 2) { dynamicText = dynamicText.substring(0, dynamicText.length - 1); dynamicText += "…" } break; } dynamicText += leftContent[j]; leftContent = leftContent.slice(1); } //console.log("文本内容:", dynamicText); canvasHelper.drawText( ctx, dynamicText, leftPadding + 20, textPositionY, "left", contentFontSize, contentFontWeight, fonts.list["SFRounded-Semibold"].name, "#303133", "", 0, 0, 0 ); textPositionY = textPositionY + 20; } } console.log("8.绘制二维码"); const qrcodeSize = 66, qrcodeHolderSize = 70; canvasHelper.drawRoundRect( ctx, (device.screenWidth - qrcodeHolderSize) / 2.0, (device.screenHeight - contentHeight) / 2.0 + contentHeight, qrcodeHolderSize, qrcodeHolderSize, 10, 'rgba(255, 255, 255, 1)' ); ctx.clip(); await canvasHelper.drawImage( canvas, ctx, shareInfo.shareQrcode, (device.screenWidth - qrcodeSize) / 2.0, (device.screenHeight - contentHeight) / 2.0 + contentHeight + 2, qrcodeSize, qrcodeSize, 0, ); ctx.restore(); /** await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.shareQrcode, (device.screenWidth - qrcodeSize) / 2.0, (device.screenHeight - contentHeight) / 2.0 + contentHeight, qrcodeSize, 0 );*/ ctx.fill(); this.setData({allowSave: true}); this.toast.hideLoadingToast(); }); }); }, /** * 下载头像 * @param avatarUrl * @returns {Promise} * @private */ _downloadHeaderImg(avatarUrl) { return new Promise(((resolve, reject) => { wx.downloadFile({ url: avatarUrl, //下载头像 success(res) { console.log("下载结束:", res); if (res.statusCode === 200) { //res.tempFilePath resolve(res); } }, fail(res) { reject(res); } }); })); }, /** * 关闭分享页面 */ close() { this.toast.hideLoadingToast(); this.setData({visibility: false, shareView: false, allowSave: false}); tabbar.showTab(this); }, } }) ; 工具类canvasHelper const stackBlur = require("stackblur-canvas"); /** * 初始化画布 * @param app * @param base * @param canvasId * @returns {Promise} */ const init = (app, base, canvasId) => { return new Promise((resolve, reject) => { const device = app.getSystemInfo(); const query = base.createSelectorQuery(); query.select(canvasId) .fields({node: true, size: true}) .exec((res) => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = device.pixelRatio; canvas.width = res[0].width * dpr; canvas.height = res[0].height * dpr; ctx.scale(dpr, dpr); console.log("画布初始化完毕,画布宽:", canvas.width, "画布高:", canvas.height, "设备像素比:", dpr); resolve({ctx, canvas}); }); }); } /** * 绘制圆角矩形 * @param ctx * @param {number} x 圆角矩形选区的左上角 x坐标 * @param {number} y 圆角矩形选区的左上角 y坐标 * @param {number} w 圆角矩形选区的宽度 * @param {number} h 圆角矩形选区的高度 * @param {number} r 圆角的半径 * @param {string} f 填充颜色 */ const drawRoundRect = (ctx, x, y, w, h, r, f) => { ctx.save(); // 开始绘制 ctx.beginPath(); // 因为边缘描边存在锯齿,最好指定使用 transparent 填充 // 这里是使用 fill 还是 stroke都可以,二选一即可 ctx.fillStyle = f; // ctx.setStrokeStyle('transparent') // 左上角 ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5); // border-top ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.lineTo(x + w, y + r); // 右上角 ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2); // border-right ctx.lineTo(x + w, y + h - r); ctx.lineTo(x + w - r, y + h); // 右下角 ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5); // border-bottom ctx.lineTo(x + r, y + h); ctx.lineTo(x, y + h - r); // 左下角 ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI); // border-left ctx.lineTo(x, y + r); ctx.lineTo(x + r, y); // 这里是使用 fill 还是 stroke都可以,二选一即可,但是需要与上面对应 ctx.fill(); // ctx.stroke() ctx.closePath(); // 剪切 //ctx.clip(); }; /** * 绘制圆形 * @param ctx * @param circlePointX * @param circlePointY * @param radius * @param backgroundColor * @param save */ const drawCircle = (ctx, circlePointX, circlePointY, radius, backgroundColor, save = true) => { if (save) ctx.save(); //ctx.save(); ctx.beginPath(); ctx.arc(circlePointX, circlePointY, radius, 0, 2 * Math.PI, false); ctx.fillStyle = backgroundColor; ctx.fill(); ctx.clip(); //ctx.restore(); if (save) ctx.restore(); }; /** * 绘制圆形白边图像 * @param canvas * @param ctx 图像 * @param imageUrl 图像 * @param startPositionX x坐标 * @param startPositionY y坐标 * @param size 图像大小,除以2就是圆半径 * @param borderWidth 边框宽度 * @param borderColor 边框颜色 * @returns {Promise<*>} * @private */ const drawCircleImage = (canvas, ctx, imageUrl, startPositionX, startPositionY, size, borderWidth, borderColor) => { return new Promise((resolve, reject) => { //叠加圆形的绘制,需要先保存一下原始环境,然后绘制完第一个圆形后恢复绘制环境,即ctx.restore(); ctx.save(); drawCircle( ctx, startPositionX + size / 2, startPositionY + size / 2, size / 2, borderColor, false ); if (borderWidth) { drawCircle( ctx, startPositionX + size / 2, startPositionY + size / 2, size / 2 - borderWidth, borderColor, false ); } drawImage( canvas, ctx, imageUrl, startPositionX, startPositionY, size, size, 0, ).then(res => { ctx.restore(); resolve(res); }).catch(res => { reject(res); }); }); }; /** * 绘制高斯模糊效果的图像 * @param canvas * @param ctx * @param imageUrl * @param startPositionX * @param startPositionY * @param width * @param height * @param blur * @returns {Promise} * @private */ const drawBlurImage = (canvas, ctx, imageUrl, startPositionX, startPositionY, width, height, blur) => { return new Promise((resolve, reject) => { wx.getImageInfo({ src: imageUrl,//服务器返回的图片地址 success: function (res) { console.log("=>", res); let imgObj = canvas.createImage(); imgObj.src = res.path; imgObj.onload = async function (e) { console.log("=========>", imgObj.width, imgObj.height) ctx.save(); ctx.beginPath(); ctx.drawImage(imgObj, startPositionX, startPositionY, width, height); //提取图片信息 let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); //进行高斯模糊 let gd = stackBlur.imageDataRGBA(imageData, 0, 0, canvas.width, canvas.height, blur); //绘制模糊图像 ctx.putImageData(gd, 0, 0) ctx.restore(); console.log("图片加载完毕:", e); resolve(); }; }, fail: function (res) { console.log(res); reject(res); } }); }) }; /** * 绘制文本 * @param ctx * @param text * @param startPositionX * @param startPositionY * @param textAlign * @param fontSize * @param fontWeight * @param color * @param shadowColor 阴影颜色 * @param shadowOffsetX 阴影水平偏移距离 * @param shadowOffsetY 阴影垂直偏移距离 * @param shadowBlur 模糊程度 * @param letterSpace 间隔 * @returns {number} * @private */ const drawText = (ctx, text, startPositionX, startPositionY, textAlign, fontSize, fontWeight, fontFamily, color, shadowColor, shadowOffsetX, shadowOffsetY, shadowBlur, letterSpace = 0) => { if (!text) { return 0; } let textWidth = 0; ctx.save(); ctx.beginPath(); ctx.fillStyle = color; ctx.textAlign = textAlign; ctx.font = fontWeight + " " + fontSize + "px " + fontFamily; if (shadowColor) { console.log("设置阴影:", shadowColor); // 设置阴影 ctx.shadowColor = shadowColor; //阴影颜色 ctx.shadowOffsetX = shadowOffsetX; //偏移 ctx.shadowOffsetY = shadowOffsetY; ctx.shadowBlur = shadowBlur; //模糊程度 } if (!letterSpace) { let metrics = ctx.measureText(text); console.log("文字[" + text + "]宽度:", metrics.width); ctx.fillText( text, startPositionX, startPositionY ); textWidth = metrics.width; } else { //对齐方式调整为left ctx.textAlign = "left"; let positionXArr = [];//坐标集合 textWidth = ctx.measureText(text).width + (text.length - 1) * letterSpace;//含letterSpace的文字总宽度 for (let i = 0; i < text.length; i++) { if (i === 0) { switch (textAlign) { case "left": positionXArr.push(startPositionX); break; case "center": positionXArr.push(startPositionX - textWidth / 2); break; case "right": positionXArr.push(startPositionX - textWidth); break; default: console.warn("暂不支持的textAlign:", textAlign); break; } } else { let metrics = ctx.measureText(text[i - 1]); positionXArr.push(positionXArr[i - 1] + metrics.width + letterSpace); } } for (let i = 0; i < text.length; i++) { ctx.fillText( text[i], positionXArr[i], startPositionY ); } } ctx.restore(); return textWidth; }; /** * 绘制图图像 * @param canvas * @param ctx * @param startPositionX * @param startPositionY * @param width * @param height * @param blur * @param imageUrl * @returns {Promise} * @private */ const drawImage = (canvas, ctx, imageUrl, startPositionX, startPositionY, width, height, blur) => { return new Promise((resolve, reject) => { wx.getImageInfo({ src: imageUrl,//服务器返回的图片地址 success: function (res) { console.log("=>", res); let imgObj = canvas.createImage(); imgObj.src = res.path; imgObj.onload = function (e) { ctx.save(); ctx.beginPath(); ctx.filter = 'blur(' + blur + 'px)'; // ctx.globalAlpha = 0.6 ctx.drawImage(imgObj, startPositionX, startPositionY, width, height); ctx.restore(); console.log("图片加载完毕:", e); resolve(res); }; }, fail: function (res) { console.log(res); reject(res); } }); }) }; /** * 将画布内容保存为图片 * @param base * @param canvas * @param x * @param y * @param width * @param height * @param destWidth * @param destHeight * @param quality * @param fileType * @returns {Promise} */ const saveImage = (base, canvas, x, y, width, height, destWidth, destHeight, quality = 1, fileType = "png") => { return new Promise((resolve, reject) => { wx.canvasToTempFilePath({ x, y, width, height, destWidth, destHeight, quality, fileType, canvas, success(res) { console.log(res.tempFilePath); wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: (res) => { resolve(res); }, fail: (err) => { console.error(err) reject(err); } }) }, fail(res) { console.error("保存失败:", JSON.stringify(res)); reject(res); } }, base) }); } module.exports = { init, drawCircle, drawCircleImage, drawImage, drawBlurImage, drawText, drawRoundRect, saveImage }
2023-05-30 - 小程序开发新能力解读 - 2021.12
快速知悉 网络调优相关 - 前后台切换 - 网络状态变化 wx.onNetworkStatusChange - 弱网状态变化 wx.onNetworkWeakChange - request/download 新协议 enableHttp2 / enableQuic / enableCache - wx.connectSocket 压缩扩展 perMessageDeflate 插件登录 wx.pluginLogin无障碍优化 aria-component文件系统新增readCompressedFile 接口支持读取指定类型压缩文件 1. 网络调优相关 [视频] 网络调优:小程序和小游戏网络相关 API 使用方式相同, 所以我们用网络接口来统称 。 网络接口的构成主要包括四个类型: requestdownloaduploadwebsocket对于网络调优,我们提供了以下的优化建议: 1.1. 前后台切换 小程序切后台 5s 后, 会中断网络请求, 开发者会收到 interrupted 的回调, 此时需要做好兼容逻辑。 1.2. 网络状态变化 wx.onNetworkStatusChange 使用介绍:当用户网络状态变化时会通过事件 wx.onNetworkStatusChange 进行通知, 不少网络问题是断网引起的, 可以通过此事件给用户更好的提示。 示例代码: wx.onNetworkStatusChange(function (res) { console.log(res.isConnected)//当前是否有网络链接,返回的是布尔值 console.log(res.networkType)//返回的是网络类型 }) 1.3. 弱网状态变化 wx.onNetworkWeakChange 使用介绍:基础库从 2.19.0 版本开始, 提供 wx.onNetworkWeakChange 弱网变化通知, 很多超时类的问题都是用户处于弱网引起的, 可以通过此事件给用户更好的提示。 在最近的八次网络请求中, 出现下列三个现象之一则判定弱网。 出现三次以上连接超时出现三次 rtt 超过 400出现三次以上的丢包弱网事件通知规则是: 弱网状态变化时立即通知, 状态不变时 30s 内最多通知一次。 示例代码: wx.onNetworkWeakChange(function (res) { console.log(res.weakNet)//当前是否处于弱网状态 console.log(res.networkType)//当前网络类型 }) // 取消监听 wx.offNetworkWeakChange() 1.4. request/download 新协议 enableHttp2 / enableQuic / enableCache 从 Android 7.0.12 / iOS 8.0.3 开始, 提供下面三个新参数: [图片] h2 连接速度更快, 建议支持, 这里需要注意 h2 的 header 是需要为全小写, 打开 enableHttp2 开关前需要注意代码逻辑。 1.5. wx.connectSocket 压缩扩展 perMessageDeflate 压缩参数目前已在 Android 和 iOS 上全量支持。 使用介绍:可以通过 wx.connectSocket 来进行创建一个 WebSocket 连接然后使用perMessageDeflate进行压缩。 示例代码: wx.connectSocket({ url: 'wss://example.qq.com',//开发者服务器 wss 接口地址 header:{//HTTP Header,Header 中不能设置 Referer 'content-type': 'application/json' }, protocols: ['protocol1']//子协议数组 perMessageDeflate:"true"//是否开启压缩扩展,默认是false }) 点击查看 网络调优 官方文档 2. 插件登录 wx.pluginLogin [视频] 使用介绍: 该接口仅在小程序插件中可调用。调用接口获得插件用户标志凭证(code),无需通过 用户信息功能页 进行授权。插件可以此凭证换取用于识别用户的标识 openpid。用户不同、宿主小程序不同或插件不同的情况下,该标识均不相同,即当且仅当同一个用户在同一个宿主小程序中使用同一个插件时,openpid 才会相同。示例代码: wx.pluginLogin(){ success(res) { // 用于换取 openpid 的凭证(有效期五分钟)。插件开发者可以用此 code 在开发者服务器后台调用 auth.getPluginOpenPId 换取 openpid。 console.log(res.code) }, fail(err){ console.log(err) } } 点击查看 插件登录 官方文档 3. 无障碍优化 aria-component [视频] 使用介绍: 1.满足视障人士对于小程序的访问需求。 2.以 view 组件为例,开发者可以增加aria-role和aria-label属性。 其中aria-role表示组件的角色,当设置为'img'时,读屏模式下聚焦后系统会朗读出'图像'。设置为'button'时,聚焦后后系统朗读出'按钮'。 aria-label表示组件附带的额外信息,聚焦后系统会自动朗读出来。 3.小程序 aria 属性对齐 web 标准。 示例代码: <view aria-role="button" aria-label="提交表单">提交</view> 注意: 安卓和iOS读屏模式下设置aria-role后朗读的内容不同系统之间会有差异。可设置的aria-role可参看 Using Aria 中的Widget Roles,部分role的设置在移动端可能无效。点击查看 无障碍优化 官方文档 4. 文件系统新增readCompressedFile 接口支持读取指定类型压缩文件 使用介绍: 1.读取指定压缩类型的本地文件内容。 2.其中compressionAlgorithm属性,文件压缩类型,目前仅支持 'br'(brotli压缩文件)。 示例代码: const fs = wx.getFileSystemManager() // 异步接口 fs.readCompressedFile({ filePath: '${wx.env.USER_DATA_PATH}/hello.br', compressionAlgorithm: 'br', success(res) { console.log(res.data) }, fail(res) { console.log('readCompressedFile fail', res) } }) // 同步接口 const data = fs.readCompressedFileSync({ filePath: '${wx.env.USER_DATA_PATH}/hello.br', compressionAlgorithm: 'br', }) console.log(data) 点击查看 readCompressedFile 官方文档 5. 更多能力 worker里添加USER_DATA_PATH 详情云托管支持 websocket 连接 详情apiCategory对应API限制调整开 详情
2022-06-24 - 微信开发者工具项目管理能不能将卡片布局改成列表布局?
一些小程序在本地会同时存在多个版本,且目录比较深,此时需要一个一个打开查看,不对再关闭,很操蛋!
2023-01-10 - 企业付款到零钱,必须使用原商户订单号重试疑问
有部分错误码需要“如果要继续付款必须使用原商户订单号重试”。有一些疑问,望解答: 背景:有用户A和B先后申请提现,A的提现出现了V2_ACCOUNT_SIMPLE_BAN(无法给非实名用户付款)的错误。 这时候B的提现是要用A的订单号发起吗? A用户在一个月后完成了实名认证,再次申请提现(金额可能发生改变),还需要用原订单号付款吗? 用户C申请提现6000元,出现MONEY_LIMIT(已经达到今日付款总额上限/已达到付款给此用户额度上限),于是用户C重新修改金额,申请2000元的提现,这时候需要用原订单号支付吗?
2019-08-19 - 如何判断企业付款接口是否成功?
付款接口返回SUCCESS ,不代表已经付款成功。 需要用查询接口:status参数进行最终判断。 处理中一般指付款还在进行处理,需要一定的时间。 如果是银行卡付款到账实效为1-3日,最快次日到账,到零钱一般是15分钟左右。 极少情况下会出现订单一直处于处理中的状态,此时建议商户使用原付款单号重试或等待到第13日如订单还是处于处理中的状态则该笔订单付款失败。
2020-12-31 - 怎么解决小程序 scroll-view 中的下拉加载更多时 会出现页面跳动?
[图片] 需要实现一个上拉加载历史消息的功能,消息是纯文本偶尔是抖动,消息是图片或者是其他需要实时渲染的内容,页面会跳的厉害。 把 upper-threshold 数值设置大一点,模拟器上好的一些,真机还是跳动。
2021-03-23