- 贴一个云托管Gradle版本的Dockerfile
最近要在微信云托管里面搞一个SpringBoot版的服务,翻了官方的Dockerfile示例,都没有找到Gradle的版本。于是,自己就捯饬了一下,摸索出了一个Gradle版本的Dockerfile,贴出来,希望对其他小伙伴可以有所帮助。 # 二开推荐阅读[如何提高项目构建效率](https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/scene/build/speed.html) # 选择构建用基础镜像。如需更换,请到[dockerhub官方仓库](https://hub.docker.com/_/java?tab=tags)自行选择后替换。 #FROM openjdk:8-jdk-alpine FROM gradle:4.10.2-jdk8-alpine as builder # 指定构建过程中的工作目录 WORKDIR /app # 将项目目录下所有文件,拷贝到工作目录下(.gitignore/.dockerignore中文件除外) COPY ./ /app/ # 避免权限问题,无法启动gradle进行编译 USER root # 执行代码编译命令 排除测试用例 RUN gradle build -x test # 选择运行时基础镜像 FROM alpine:3.13 # 容器默认时区为UTC,如需使用上海时间请启用以下时区设置命令 RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone # 使用 HTTPS 协议访问容器云调用证书安装 RUN apk add ca-certificates # 安装依赖包,如需其他依赖包,请到alpine依赖包管理(https://pkgs.alpinelinux.org/packages?name=php8*imagick*&branch=v3.13)查找。 # 选用国内镜像源以提高下载速度 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tencent.com/g' /etc/apk/repositories \ && apk add --update --no-cache openjdk8-jre-base \ && rm -f /var/cache/apk/* # 指定运行时的工作目录 WORKDIR /app # 将构建产物jar包拷贝到运行时目录中 COPY --from=builder /app/ms-web/build/libs/*.jar . # 暴露端口 # 此处端口必须与「服务设置」-「流水线」以及「手动上传代码包」部署时填写的端口一致,否则会部署失败。 EXPOSE 80 # 执行启动命令. # 写多行独立的CMD命令是错误写法!只有最后一行CMD命令会被执行,之前的都会被忽略,导致业务报错。 # 请参考[Docker官方文档之CMD命令](https://docs.docker.com/engine/reference/builder/#cmd) CMD ["java", "-jar", "/app/ms-web-1.0.0-SNAPSHOT.jar"] 项目结构: [图片]
2022-05-18 - 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 - 记录恋爱小清单5.0升级完毕
白天忙着搬砖,晚上回家忙着捣腾小程序,断断续续还是把小清单5.0上线了。 当年搞小清单的时候云开发刚推出不久,那时还不能多表关联查,所以像首页的清单展示,一次要同时查好几个表,最后再组装好吐给小程序。整体交互体验比较差,后期维护起来也很蛋疼,所以这次下决心做了一次重构:表结构重新设计,底层逻辑重写,清洗历史数据,导入新表。 此外还做了一些调整: 1、清单增加完成功能,老版本是只要上传一张照片就表示完成清单,但是挺多用户会提前上传一张照片占坑,让整体看起来显得好看,而且这样有时也会让用户有些困惑。所以这次放开,让用户自己决定是否已完成清单。 2、清单、纪念日的分享海报重新设计,背景采用毛玻璃形式,颜值相比旧版有所提升[狗头]。 3、允许自定义清单主题,可增可减。 4、恋人圈改版,早期是仿朋友圈的熟人社交设计,做的也比较粗糙,这次改版后除了点赞评论以外还加入了祝福的概念,希望在这里大家分享的幸福回忆都可以得到美好祝福。 5、个人中心改版。增加了密码保护可能,开启后离开小程序5分钟重新进入则需要输入密码解锁,保护用户的隐私。 未来还考虑增加支持上传视频的方式记录回忆、匿名分享、日记本等等,时间有限,但想做的事情还挺多... 最后附上几张图片聊表心意,感谢阅读 [图片] [图片] [图片] [图片]
2023-05-30 - 图片安全检测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 - 加固插件已激活,加固前,加固配置内没有合法的js文件
最近,微信开发工具的代码加固秃然用不了了。每次预览时都会提示 加固插件已激活,加固前,加固配置内没有合法的js文件,请检查路径 [图片] 尝试了把加固插件卸载重装、开发工具升级最新版、基础库切换到最新版本依然无法解决问题。社区中也有人6月份反馈类似问题:https://developers.weixin.qq.com/community/develop/doc/0000cc339300d0c325ef9570b56c00 配置其实都是正确的,这个提示真是让人一头雾水。 没办法啊,只能自己继续摸索 尝试把开发工具的授权全部清除,这样在预览时加固插件会先请求授权,但是授权的弹窗提示让我觉得有些奇怪: [图片] null extension description。。。这是插件没有安装成功吗? 然后在工具的输出栏可以看到加固插件(appid是wxext871b2e053677418)运行日志,无意间发现插件的安装目录其实是在C:\Users\Yunfay\AppData\Local\微信开发者工具\User Data\,这个目录因为之前因为占用空间太大,已经达到了15G,C盘告警,所以我有更换过储存目录,然后用mklink建了一个软链,这个方法很早前官方人员也有在社区中提及过,这个是帖子链接 https://developers.weixin.qq.com/community/develop/doc/000e8eed8a4448555dc9c877f5b400 最后把软链删除,并将之前移走的文件挪回原来目录,代码加固插件也就恢复正常了。。。 现在只能把开发工具卸载重装,不然C盘的占用空间有些夸张。 不清楚加固插件里面的处理逻辑或者说是mklink有什么副作用(欢迎知道原理的大佬评论指点一下),这里简单把这个问题记录一下,希望那些还在被这个问题困扰的小伙伴也能有所启发。 [图片]
2023-09-17 - 记录云开发lookup多表嵌套查询优化
最近在优化一个云开发用lookup实现的多表嵌套查询SQL,发现测试环境最大的表数据量还不到2W,但是整体查询耗时竟然要2,3s,这让我觉得有些意外,能加的索引也都加了,有点头大。。。 主要是以下几个表: user-card-list(清单表,大概1.9W条)user-comment(评论表,大概500条)user-info(用户信息表,大概9000条)black-list(黑名单表,12条)具体的业务是分页查询出清单,并关联出发布清单的用户信息、清单评论信息、评论者的用户信息,同时过滤掉黑名单用户。在测试SQL的过程中发现在pipeline中用到的父表字段其实不会使用索引,如果是用localField/foreignField关联表时索引可以生效,但是遗憾的是这种方式不可用嵌套查询... 写惯了关系型数据库SQL,通常是会把分页以及过滤条件写在SQL最后的位置。但是在非关系型数据库,如果尽量把过滤条件或者分页的SQL前置,可能会有意想不到的效果。 下面的这个SQL我把match跟limit前置后查询速度从2,3s降到了3,400ms,有点苦笑不得[捂脸]。。 在这里蛮记录一下。或许对看到的小伙伴可以有一点点小启发。 db.collection('user-card-list').aggregate() .lookup({ from: 'black-list', let: { openid: '$openid'//将变量openid的值等于user-card-list表的openid,在pipeline可以使用,let需要和pipeline一起使用 }, pipeline: $.pipeline() .match(_.expr($.and([ $.eq(['$openid', '$$openid']), ]))) .done(), as: 'blackList', }) .addFields({ inBlackList: $.gt([$.size('$blackList'), 0]), //排序字段,由公开时间+ID组成 cursor: $.concat(['$lightAt', '', '$_id']), }) .match(_.expr($.and( $.eq(['$light', 'Y']), //$.gt(['$cursor', '2023-05-20 23:58:23ozzW05Gch7jMMhsn1r_SWLGdGtF0_add_1563634289607']) $.or([ //当前清单的发布用户不在黑名单中,直接展示 $.eq(['$inBlackList', false]), //当前清单的发布用户在黑名单中,且是本人浏览时,直接展示 $.and([ $.eq(['$inBlackList', true]), $.eq(['$openid', 'ozzW05Gch7jMMhsn1r_SWLGdGtF0']), ]) ]) ))) //按cursor降序排序 .sort({cursor: -1}) //分页前置,提升查询速度 .limit(30) .lookup({ from: 'user-comment', let: { id: '$_id'//将变量id的值等于user-card-list表的_id,在pipeline可以使用,let需要和pipeline一起使用 }, pipeline: $.pipeline() .match(_.expr($.eq(['$belongTo', '$$id']))) //按createAt降序 .sort({createAt: -1}) .lookup({ from: 'user-info', let: { replyOpenid: '$replyOpenid'//将变量replyOpenid的值等于user-comment表的replyOpenid,在pipeline可以使用,let需要和pipeline一起使用 }, pipeline: $.pipeline() .match(_.expr($.eq(['$openid', '$$replyOpenid']))) .done(), as: 'replyUserInfoList', }) .done(), as: 'userCommentList', }) .project({ momentContent: 1, author: 1, cursor: 1, lightAt: 1, comments: $.reverseArray('$commentsReverse'), userCommentList: 1, likes: 1, wishes: 1, time: '$lightAt', }) .end();
2023-05-26 - 省钱有道之 减少云函数调用次数
由于云函数有一项计费规则是按调用次数计费,在小程序访问量比较小的情况下还比较无所谓,但当体量上来之后不得不考虑控制一下对公共接口的调用次数从而减少一些不必要的开销。比如获取用户信息接口、获取配置信息接口 这里分享一个我自己几个小程序用到的方法,公共接口的调用都放在app.js,然后提供函数供其他页面调用。同时由于异步问题,有可能页面加载完接口还未返回,因此还需能够注册回调函数,在接口返回数据后回调给调用页面 代码示例: app.js App({ onLaunch: async function (options) { //判断是否需要更新小程序 updateCheck.check(); await api.wxCloudInit(); //获取用户信息 this._getUserInfo().catch(res => { console.warn("获取用户信息失败,准备重试"); this._getUserInfo().then(); }); }, /** * 获取用户信息 * @param callback * @param refresh 等于true时表示重新查询用户信息,同时也会更新会员状态 */ getUserInfo: function (callback, refresh) { if (!refresh) { const userInfo = this.globalData.userInfo; if (!userInfo.ready) { if (typeof callback == 'function') { this.callbackFunctions.userInfoReadyCallback.push(callback); } if (!this.userInfoReadyCallback) { this.userInfoReadyCallback = res => { console.log("获取用户信息完毕,开始回调", res); const callbacks = this.callbackFunctions.userInfoReadyCallback; while (callbacks.length) { const callback = callbacks.pop(); typeof callback == 'function' && callback(res); } /*callbacks.forEach(callback => { typeof callback == 'function' && callback(res); })*/ } console.log("注册userInfoReadyCallback成功"); } else { console.log("已经注册了userInfoReadyCallback,不再重复注册"); } } else { typeof callback == 'function' && callback(userInfo); } } else { console.log("准备更新用户信息") this._getUserInfo().then(userInfo => { typeof callback == 'function' && callback(userInfo); }); } }, /** * 执行云函数,获取用户信息 * @returns {Promise<unknown>} * @private */ _getUserInfo: function () { return new Promise((resolve, reject) => { api.callCloudUserCenterFunction("UserInfoHandler/getUserInfo", {}, res => { console.log("获取用户数据完毕:", res.result); const result = res.result; if (result.success) { const data = result.data; this.globalData.userInfo = data; // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 // 所以此处加入 callback 以防止这种情况 if (this.userInfoReadyCallback) { this.userInfoReadyCallback(data); } resolve(data); } else { console.error("没有获取到用户信息"); reject("没有获取到用户信息"); } }, e => { console.error("获取用户信息失败", e); reject("获取用户信息失败"); }); }); }, /** * 异步事件回调函数列表 * 增加这个列表是为了避免不同地方同时调用,互相覆盖回调函数 */ callbackFunctions: { //用户信息异步回调 userInfoReadyCallback: [], }, globalData: { userInfo: { confirm: false//用来标记用户信息查询动作是否已经结束,等于true时,userInfo才可信 }, } }) 某page.js app.getUserInfo(res => { const isVip = res.isVip; if (isVip) { console.log("已开通会员", res); } });
02-07