- 小程序用户头像昵称获取(含部分常见问题)
小程序用户头像昵称获取 引言 前段时间,小程序头像昵称获取的接口进行了调整,原公告《小程序用户头像昵称获取规则调整公告》。 简单来说,现在获取用户的微信头像和昵称要用户自己填写了。 下面和大家分享一下「头像昵称填写能力」的实操过程。 「头像昵称填写功能」实操 一、头像部分 WXML 首先,在wxml页面上添加一个[代码]button[代码]组件,设置[代码]open-type[代码]属性为[代码]chooseAvatar[代码],再添加一个触发事件[代码]bindchooseavatar[代码],代码如下: [代码] <button open-type="chooseAvatar" bindchooseavatar="bindchooseavatar"> <image src="{{avatarUrl}}"></image> </button> [代码] 我这里在[代码]button[代码]里面添加了[代码]image[代码]组件是为了实现点击头像(图片)就触发头像选择。 JS 在js部分,写好[代码]bindchooseavatar[代码]事件回调。 [代码] bindchooseavatar(e) { console.log("avatarUrl",e.detail.avatarUrl) } [代码] 当我们触发组件,选择好头像后,我们可以从事件回调中得到头像链接[代码]avatarUrl[代码]。 我这里获取到的链接是: [代码]http://tmp/bnMmEbfpqclVa77acadd216b18c692b3a2aa1d505353.jpeg[代码]。 [图片] 需要注意的是 这里获取到的是本地临时链接,只能在本地中读取与使用,随时会失效。 我们还需要将这个临时路径保存到服务器中,从而换取一个永久链接。 我们可以用API[代码]wx.uploadFile[代码]将图片上传到自己的服务器,改写后的[代码]bindchooseavatar[代码]事件回调代码如下: [代码] bindchooseavatar(e) { const avatarUrl = e.detail.avatarUrl console.log("avatarUrl", avatarUrl) wx.uploadFile({ url: 'https://www.hlxuan.top/upload', // 仅为示例,非真实的接口地址 filePath: avatarUrl, name: 'file', // 文件对应的 key,开发者在服务端可以通过这个 key 获取文件的二进制内容 formData: { 'user': 'test' }, // HTTP 请求中其他额外的 form data success (res){ const data = JSON.parse(res.data) // do something } }) } [代码] 如果你用的是小程序云开发,[代码]bindchooseavatar[代码]事件回调代码示例如下: [代码] bindchooseavatar(e) { const avatarUrl = e.detail.avatarUrl console.log("avatarUrl", avatarUrl) wx.cloud.uploadFile({ cloudPath: 'example.png', // 上传至云端的路径 filePath: avatarUrl, // 小程序临时文件路径 success: res => { // 返回文件 ID console.log(res.fileID) // do something }, fail: console.error }) } [代码] 二、昵称部分 WXML 组件自带安全检测功能,根据官方的文档说明,检测是在[代码]onBlur[代码]事件触发时异步进行的,也就是说,如果使用[代码]bindinput[代码]、[代码]bindblur[代码]、[代码]bindfocus[代码]、[代码]bindconfirm[代码]这些的回调去获取用户输入的昵称,就可能会获取到未通过安全检测的内容。 在基础库2.29.1中,新加入了回调 [代码]bindnicknamereview[代码],当安全检测完成后会进行回调 [代码]event.detail = { pass, timeout }[代码] 。 你可以用这个判断用户当前输入的昵称是否通过了安全检测,和检测是否超时。 文章内容更新于2022年12月29日 建议使用表单[代码]form[代码]来收集用户输入的昵称。 [图片] [代码] <form bindsubmit="formsubmit"> <input type="nickname" placeholder="请输入昵称" name="nickname" /> <button form-type="submit" type="primary">提交</button> </form> [代码] 我们需要给[代码]input[代码]设置一个[代码]name[代码]属性,这样才能在回调中获取到用户输入的昵称。 JS [代码] formsubmit(e){ const nickName = e.detail.value.nickname console.log("nickName", nickName) // do something }, [代码] 获取到用户输入的昵称后,你可以将其保存到数据库里面。 Q&A 下面是我在微信开放社区经常看到的一些问题,稍微整理了一下,希望能帮助大家。 1. 为什么「wx.getUserProfile」返回的是“灰色头像”和“微信用户”? 「wx.getUserProfile」接口有调整,参考公告《小程序用户头像昵称获取规则调整公告》。 2. 为什么有部分小程序仍能使用以前的接口获取头像? 公告中:“生效期前发布的小程序版本不受影响”。你看到接口能正常返回头像和昵称的小程序,基本上都是在生效期前发布的小程序,你可以去「更多资料」页面看看它的更新时间。 [图片] 针对下面这个问题,我这边测试了一下,生效期后发布的小程序,基础库低于2.27.1版本,是可以正常返回微信头像和昵称的。这个目前应该只能在 PC/macOS 平台上使用,手机上大部分使用不了的,因为 PC/macOS 平台目前的基础库版本低于2.27.1,所以可以正常返回微信头像和昵称。 [图片] 文章内容更新于2022年12月29日 3. 我调整了基础库版本后,工具上可以返回头像和昵称,真机就不行了? 用户客户端的基础库版本开发者是无法控制的,这个基础库版本会随着微信客户端的更新而更新,可以看下「基础库版本分布」。 在开发者工具上设置的基础库版本,仅用于开发者工具内的调试,所以不能调整移动设备的基础库版本。 [图片] 旁边的「推送」按钮只能将基础库版本推送到你登录开发者工具的微信号上(登录这个微信号的手机上),这并不能改变用户的基础库版本。 4. 我在小程序后台设置了基础库2.27.0或以下版本,为什么还是不能获取头像和昵称? 小程序后台里面设置的是「基础库最低版本设置」,当用户的基础库版本低于你设置的最低版本要求时,将无法正常使用小程序,并提示更新微信版本。 [图片] 5. 「头像昵称填写」有安全检测吗? 有的,组件在基础库2.24.4版本起,已经接入了内容安全服务端口。如果昵称或头像有异常时,页面会显示消息提示框,输入的昵称会被清空,头像也不会返回临时路径。请不要完全依赖内容安全服务。 6. 选择的头像会自动裁剪为1:1吗? 对于来自基础库2.28.1及以上版本,组件自带压缩和裁剪功能;对于来自基础库2.28.1以下版本,你可以做兼容处理。 经验分享:我在自己的小程序里面进行了判断,当基础库2.28.1以下版本时,调用「wx.cropImage」接口对用户选择的头像进行裁剪。 7. 为什么获取到的头像链接在浏览器上打不开? 通过「头像填写能力」获取到的链接为本地临时链接,只能在本地中读取与使用,随时会失效。 [图片]
2023-01-08 - 小程序的editor组件中的delta是不是完全兼容quill.js的Delta的?
从API到class名称来看,小程序的editor组件及对应的API一定程度上借鉴了quill.js. 现在我们在考虑在后台用quill.js来编辑富文本,然后在小程序中显示,就是不知道quill.js导出的delta是不是能够直接在小程序的editor组件中使用?会不会存在兼容性问题?希望官方给个明确的答复。
2020-03-30 - 新的canvas的drawImage不支持本地tmp路径的临时文件吗?
【貌似官方修复了这个问题。现在可以了。】 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.drawImage.html drawImage( )不支持本地tmp路径的临时文件,谨慎使用,太坑了。 写法1. ``` path = 'tmp/wxf65e9ae5f68283d2.o6zAJs5h4IkyHaGS7_j6gUPGTR9c.arwyj04Eq2ok341457e272957e237fa21d743912f60b.jpg' ctx.drawImage(path, 0, 0, width, height, 0, 0, canvasWidth, canvasHeight) ``` 提示:Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The provided value is not of type '(CSSImageValue or HTMLImageElement or SVGImageElement or HTMLVideoElement or HTMLCanvasElement or ImageBitmap or OffscreenCanvas)';at SelectorQuery callback function 写法2. ``` const img = canvas.createImage() img.src = path img.onload = () =>{ ctx.drawImage(img, 0, 0, width, height, 0, 0, canvasWidth, canvasHeight) } ``` 提示:tmp/wxf65e9ae5f68283d2.o6zAJs5h4IkyHaGS7_j6gUPGTR9c.arwyj04Eq2ok341457e272957e237fa21d743912f60b.jpg:1 GET http://tmp/wxf65e9ae5f68283d2.o6zAJs5h4IkyHaGS7_j6gUPGTR9c.arwyj04Eq2ok341457e272957e237fa21d743912f60b.jpg net::ERR_PROXY_CONNECTION_FAILED
2020-03-29 - 那些微信小程序开发踩过的坑
小程序页面栈最多十层 问题:小程序内超过十层路由,你会发现wx.navigateTo跳转不到下一个页面。这是因为使用wx.navigateTo跳转会把当前页面保存到页面栈中,而小程序页面栈最多十层。 解决:超过十层使用redirectTo(重定向)操作 或者参考https://developers.weixin.qq.com/community/develop/article/doc/000a08e12185187bd5cebf3f651013 IOS使用New Date()报错IOS 的 Date 构造函数 不支持2018-04-26这种格式的日期,必须转换为2018/04/26这种格式,可以使用 replace(/-/g, '/')处理image组件使用webp图片时,IOS需要设置webp属性.Android手机在onShow内调用 wx.showModal ,如果不关闭弹窗(直接点击右上角退出小程序),弹窗不会销毁,再次进入页面触发onShow时会出现两次弹窗,IOS正常小程序中使用 web-view打开pdf , IOS 可以正常打开,Android 打开为空白 解决:使用wx.downloadFile和wx.openDocument 在手机相册中选择完图片后直接跳转会出现闪回的现象 原因:在选择完图片后,会重新执行一遍page的onShow生命周期 解决:在选择完图片后,做一个sleep延时1秒,再进行跳转 textarea 层级问题 问题:textarea的placeholder会显示在弹窗的层级之上 解决:使用wx:if 判断当没有值的时候用view代替textarea 最好封装为组件 或者 弹出层使用cover-view组件,而不是view,覆盖住所有原生组件。 小程序的 web-view 中页面跳转后,点击 Android 手机上的物理返回按钮会返回前一个页面。而点击左上角的返回按钮,会直接关闭整个 web-view。有关 web-view 中有背景音乐,后台后无法关闭的问题 https://developers.weixin.qq.com/community/develop/doc/c75139c842a40c67cade23d3f66e7992 var hiddenProperty = 'hidden' in document ? 'hidden' : 'webkitHidden' in document ? 'webkitHidden' : 'mozHidden' in document ? 'mozHidden' : null; if (hiddenProperty) { var visibilityChangeEvent = hiddenProperty.replace(/hidden/i, 'visibilitychange'); var onVisibilityChange = function() { if (document[hiddenProperty]) { !MpMovie.video.paused && MpMovie.video.pause(); } }; document.addEventListener(visibilityChangeEvent, onVisibilityChange);
2022-11-07 - 小程序全局状态管理工具
项目说明 原生微信小程序全局状态管理工具,轻量,便捷,高性能,响应式。 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 - 小程序新手引导实现思路整理
从个人开发和使用角度来说,新手引导属于一个比较鸡肋的功能。 [图片] [图片] 反正就是有几个提示,就弹几个框。这里采用的方式是,用三倍图片,然后用2倍的尺寸,然后加上一个黑科技属性,可以做到弹框图片高清不模糊的效果。 话不多说直接上代码:使用点击 +1,的逻辑动态显示对应的图片 <Mask v-if="showGuide"> <view class="maskGuide"> <view v-if="guideNumber == 1" @tap="clickToNext"> <image src="XXX.png" class="tl-img-1"/> </view> <view v-if="guideNumber == 2" @tap="clickToNext"> <image src="XXX.png" class="tl-img-2"/> </view> <view v-if="guideNumber == 3" @tap="clickToNext"> <image src="XXX.png" class="tl-img-3"/> </view> <view v-if="guideNumber == 4" @tap="clickToNext"> <image src="XXX.png" class="tl-img-4"/> </view> <view v-if="guideNumber == 5" @tap="clickToNext"> <image v-if="gender==1" src="XXX.png" class="tl-img-5"/> <image v-else src="XXX.png" class="tl-img-5"/> </view> </view> </Mask> js 逻辑: uni.setStorageSync('isGuide', true) //存储本地,只弹出一次的逻辑 // 新手引导初步下一步逻辑 clickToNext(){ this.guideNumber +=1 if(this.guideNumber == 6){ uni.setStorageSync('isGuide', true) this.showGuide = false; } }, less .maskGuide{ position: relative; width: 100vw; height: 100vh; .tl-img-1{ position: absolute; top: 48rpx; left: 20rpx; width: 584rpx; height: 230rpx; image-rendering: -webkit-optimize-contrast; } .tl-img-2{ position: absolute; top: 174rpx; left: 20rpx; width: 646rpx; height: 230rpx; image-rendering: -webkit-optimize-contrast; } .tl-img-3{ position: absolute; top: 40rpx; right: 0; width: 630rpx; height: 308rpx; image-rendering: -webkit-optimize-contrast; } .tl-img-4{ position: absolute; top: 240rpx; right: 10rpx; width: 604rpx; height: 112rpx; image-rendering: -webkit-optimize-contrast; } .tl-img-5{ position: absolute; top: 164rpx; right: 35rpx; width: 684rpx; height: 1048rpx; image-rendering: -webkit-optimize-contrast; } } 以上是目前想到的一个实现方案,如果有其他实现方案,欢迎大家补充。共勉! [图片]
2022-09-02 - 小程序端性能监控(1)
微信小程序前端性能监控(1) 本文分为两篇,认识与使用 performance,阅读各大约10分钟 01 打点: performance 是前端性能监控的API,小程序也实现了它。 首先我们回到一个古老的监控方法:Date.now(),用它打点行么? [图片] 图示:W3C提供的一段代码示例 回答当然是可以,那它与 performance 的区别在哪,为什么现在不推荐这种方式了? 首先就是精度问题,我们知道时间是个无穷小数,时间原点及精度取舍是不同的。有兴趣了解时间起源等更深入知识的小伙伴可以到文章结尾获取链接。 [图片] 图示:获取精度示例 最重要的是它提供了抓取各时间节点的API,定义了专用于测试的时间原点,及浮点数达到微秒级别的精确度等。Date.now() 时间戳可以衡量获取资源所需的时间,但是它不能分解页面加载在各个阶段花费的时间。此外,脚本无法轻松衡量获取标记中描述的资源所花费的时间。 大多系统运行一个守护程序定期同步时间,通常15-20min调整几毫秒,这个速率大约10S间隔的值会有1%的误差。而performence是恒定的速率,它定义了navigationStart,performance.timing.navigationStart + performance.now() 约等于 Date.now()。 02 基础API的认识: 在控制台打印 performance 如下: [图片] 图示:performance API memory 内存相关 jsHeapSizeLimit 内存大小的限制。 totalJSHeapSize 总内存的大小。 usedJSHeapSize 可使用的内存的大小。 注:如果 usedJSHeapSize 大于 totalJSHeapSize的话,那么就会出现内存泄露的问题,因此是不允许大于该值的。 navigation 页面的来源信息 redirectCount:如果有重定向,页面通过几次重定向跳转而来,默认为0 type:该值的含义表示的页面打开的方式。默认为0. 可取值为0、1、2、255这个值在实际项目中蛮实用,它可以判断来源页的跳转方法例如:手机触发返回,它的值为2 0:正常进入该页面(非刷新、非重定向)。 1:通过 window.location.reload 刷新的页面。如果我现在刷新下页面后,再来看该值就变成1了。 2:通过浏览器的前进、后退按钮进入的页面。如果我此时先前进下页面,再后退返回到该页面后,查看打印的值,发现变成2了。 255:非以上的方式进入页面的。 onresourcetimingbufferfull 回调函数 浏览器的资源时间性能缓冲区满了执行的回调 timeOrigin 时间戳它是一系列时间点的基准点,精确到万分之一毫秒。该值是动态的,刷新下,该值是会发生改变的。 timing 各时间点的集合 connectEnd:HTTP完成建立连接的时间(完成握手)。如果是持久链接的话,该值则和fetchStart值相同,如果在传输层发生了错误且需要重新建立连接的话,那么在这里显示的是新建立的链接完成时间。 connectStart:HTTP 开始建立连接的时间,如果是持久链接的话,该值则和fetchStart值相同,如果在传输层发生了错误且需要重新建立连接的话,那么在这里显示的是新建立的链接开始时间。 domComplete:DOM树解析完成,且资源也准备就绪的时间。Document.readyState 变为 complete,并将抛出 readystatechange 相关事件。 domContentLoadedEventEnd:DOM解析完成后,网页内资源加载完成的时间。 domContentLoadedEventStart:DOM解析完成后,网页内资源加载开始的时间。 domInteractive:完成解析DOM树的时间(只是DOM树解析完成,并没有开始加载页面资源)。 domLoading:开始解析渲染DOM树的时间。 domainLookupEnd:DNS域名查询完成的时间,如果使用了本地缓存或持久链接,该值则与fetchStart值相同。 domainLookupStart:DNS域名查询开始的时间,如果使用了本地缓存或持久链接,该值则与fetchStart值相同。 fetchStart:准备好使用http请求抓取文档的时间(发生在检查本地缓存之前)。 loadEventEnd:load事件的回调函数执行完毕的时间,如果没有绑定load事件,该值为0 loadEventStart:load事件发送给文档。也即load回调函数开始执行的时间,如果没有绑定load事件,则该值为0 navigationStar:同一个浏览器上一个页面卸载结束时的时间戳。如果没有上一个页面的话,那么该值会和fetchStart的值相同 redirectEnd:最后一个HTTP重定向完成时的时间戳。如果没有重定向或者重定向到一个不同的源,该值也返回为0 redirectStart:该值的含义是第一个http重定向开始的时间戳,如果没有重定向或者重定向到一个不同源的话,那么该值返回为0 requestStart:HTTP请求读取真实文档开始的时间,包括从本地读取缓存,链接错误重连时。 responseEnd:HTTP响应全部接收完成时的时间(获取到最后一个字节)。包括从本地读取缓存。 responseStart:开始接收到响应的时间(获取到第一个字节的那个时候)。包括从本地读取缓存。 secureConnectionStart:HTTPS 连接开始的时间,如果不是安全连接,则值为 0 unloadEventStart:前一个页面unload的时间戳,如果没有前一个页面,那么该值为0 unloadEventEnd:和 unloadEventStart 相对应,返回是前一个网页unload事件绑定的回调函数执行完毕的时间戳。 拓展学习 W3C Web Performance:https://www.w3.org/TR/resource-timing-2/#cross-origin-resources Web Performance Working Group:https://www.w3.org/webperf/ Chrome Web Fundamentals:https://developers.google.cn/web/fundamentals/performance/navigation-and-resource-timing chrome.loadTimes() API:https://developers.google.cn/web/updates/2017/12/chrome-loadtimes-deprecated#requesttime MDN Performence API:https://developer.mozilla.org/zh-CN/docs/Web/API/Performance MDN Timing-Allow-Origin API:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin Unix time:https://en.wikipedia.org/wiki/Unix_time Performance Navigation Timing:https://w3c.github.io/navigation-timing/#dom-performancenavigationtiming Resource Timinghttps://www.w3.org/TR/resource-timing-2/#cross-origin-resources
2022-05-18 - 微信小程序运行性能注意点
小程序的运行时性能直接决定了用户在使用小程序功能时的体验。如果运行时性能出现问题,很容易出现页面滚动卡顿、响应延迟等问题,影响用户使用。如果内存占用过高,还会出现黑屏、闪退等问题。 1.控制WXML节点数量和层级 建议一个页面 WXML 节点数量应少于 1000 个,节点树深度少于 30 层,子节点数不大于 60 个。太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长,影响体验。 2.避免滥用image组件的 widthFix/heightFix模式,控制图片资源的大小。 这种模式会在图片加载完成后,动态改变图片的高度或宽度。图片高度或宽度的动态改变,可能会引起页面内大范围的布局重排,导致页面发生抖动,并造成卡顿。 3.合理使用的setData setData 应只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。 不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用;与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下。对于列表来说,可以利用setData进行列表局部刷新。 4.避免不当的使用onPageScroll 每一次事件监听都是一次视图到逻辑的通信过程,所以只在必要的时候监听pageSrcoll。避免在 scroll 事件监听函数中执行复杂逻辑。 5.合理的利用缓存 利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新,这不仅优化了性能,在无网环境下,用户也能很顺畅的使用到关键服务。 6.采用独立分包技术 提升体验最直接的方法是控制小程序包的大小。目前很多小程序主包+子包的方式,这对用户停留时间比较短的场景中,体验不是很好,且浪费了部分流量。 可以采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源。 7.使用自定义组件 自定义组件的更新只在组件内部进行,不受页面其他不能分内容的影响。各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、setData调用。
2022-05-28 - Canvas 2d接口绘制问题踩坑总结
微信不再支持之前的旧Canvas接口,都升级到了2d,导致之前绘制海报的功能无法正常使用,这边进行了修正,把过程遇到的坑记录下。主要碰到的几个问题如下: 1、迁移到新接口; 新接口采用了不同的接入方式,可以参考迁移指南:https://developers.weixin.qq.com/miniprogram/dev/framework/ability/canvas-legacy-migration.html 这里要特别注意的是,导出Canvas到图片的时候: wx.canvasToTempFilePath({ canvas: this.canvas, // 这里一定不要弄错 success: () => { ... } }) 2、导出图片的时候,绘制的图片是空白的 导出图片之后,发现绘制的图片是空白的,文字和矩形是正常的。是因为Canvas 2d是采用同步绘制的方式,不需要调用draw方法,也不需要等上一步绘制完,所以导出图片之前,要等待一下,确定绘制都完成。加个Timeout就行。 setTimeout(() => { wx.canvasToTempFilePath({ canvas: this.canvas, success: () => { ... } }) }, 300); 3、绘制的文字、矩形被图片覆盖问题 绘制图片导出正常了,但是会出现后绘制的文字和矩形都被图片给覆盖遮住了。其实这个问题和第二个问题类似,也是因为同步绘制的关系。但是因为绘制图片需要等图片加载之后再绘制,会导致文字会先绘制。处理方式,在onload里绘制图片,绘制图片之后,再调用下一个绘制: 直接贴Taro的代码了。 [图片]
2022-06-24 - 如何优雅的做一个启动loading页?
一.背景需求及分析基于微信用户又有人跑来ask the question,emm,写了个简易版本开屏广告看看吧 🚺:小程序启动页有做过吗? 🚹:放在page 第一页,定时跳转完事, setInterval, page:['启动页'], onShow(){ wx.redirectTo}, 导航使用custom wx.redirectTo(这时候考虑页面栈的问题), 会出现HOME导航logo,那么使用 [图片] wx.hideHomeButton可以解决 🚺:斗地主小程序,初次进来会有启动页,再次进来就没有了? 🚹: wx.setStorageSync、app().globaldata 试试 这两个能力自己看咯,做个缓存机制就OK了,不知道斗地主啥效果,你自己完善吧。 二.效果图[图片] 三.代码片段https://developers.weixin.qq.com/s/x4CExcmQ7lAb 四.知识点参考链接小程序导航'Home'按钮知识库:https://developers.weixin.qq.com/community/develop/doc/000406fe6f41381173397478e5b809wx.setStorageSync:https://developers.weixin.qq.com/miniprogram/dev/api/storage/wx.setStorageSync.html app().globaldata:https://developers.weixin.qq.com/miniprogram/dev/reference/api/App.html TIPS:emm,ヾ(•ω•`)o,拿去吧你,emm
2022-06-29 - 小程序导出数据到excel表,借助云开发后台实现excel数据的保存
我们在做小程序开发的过程中,可能会有这样的需求,就是把我们云数据库里的数据批量导出到excel表里。如果直接在小程序里写是实现不了的,所以我们要借助小程序的云开发功能了。这里需要用到云函数,云存储和云数据库。可以说通过这一个例子,把我们微信小程序云开发相关的知识都用到了。 老规矩,先看效果图 [图片] 上图就是我们保存用户数据到excel生成的excel文件。 实现思路 1,创建云函数 2,在云函数里读取云数据库里的数据 3,安装node-xlsx类库(node类库) 4,把云数据库里读取到的数据存到excel里 5,把excel存到云存储里并返回对应的云文件地址 6,通过云文件地址下载excel文件 一,创建excel云函数 关于云函数的创建,我这里不多说了。如果你连云函数的创建都不知道,建议你去小程序云开发官方文档去看看。或者看下我录制的云开发入门的视频:https://edu.csdn.net/course/detail/9604 创建云函数时有两点需要注意的,给大家说下 1,一定要把app.js里的环境id换成你自己的 [图片] 2,你的云函数目录要选择你对应的云开发环境(通常这里默认选中的) 不过你这里的云开发环境要和你app.js里的保持一致 [图片] 二,读取云数据库里的数据 我们第一步创建好云函数以后,可以先在云函数里读取我们的云数据库里的数据。 1,先看下我们云数据库里的数据 [图片] 2,编写云函数,读取云数据库里的数据(一定要记得部署云函数) [图片] 3,成功读取到数据 [图片] 把读取user数据表的完整代码给大家贴出来。 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: "test-vsbkm" }) // 云函数入口函数 exports.main = async(event, context) => { return await cloud.database().collection('users').get(); } [代码] 三,安装生成excel文件的类库 node-xlsx 通过上面第二步可以看到我们已经成功的拿到需要保存到excel的源数据,我们接下来要做的就是把数据保存到excel 1,安装node-xlsx类库 [图片] 这一步需要我们事先安装node,因为我们要用到npm命令,通过命令行 [代码]npm install node-xlsx [代码] [图片] 可以看出我们安装完成以后,多了一个package-lock.json的文件 [图片] 四,编写把数据保存到excel的代码, 下图是我们的核心代码 [图片] 这里的数据是我们查询的users表的数据,然后通过下面代码遍历数组,然后存入excel。这里需要注意我们的id,name,weixin要和users表里的对应。 [代码] for (let key in userdata) { let arr = []; arr.push(userdata[key].id); arr.push(userdata[key].name); arr.push(userdata[key].weixin); alldata.push(arr) } [代码] 还有下面这段代码,是把excel保存到云存储用的 [代码] //4,把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }) [代码] 下面把完整的excel里的index.js代码贴给大家,记得把云开发环境id换成你自己的。 [代码]const cloud = require('wx-server-sdk') //这里最好也初始化一下你的云开发环境 cloud.init({ env: "test-vsbkm" }) //操作excel用的类库 const xlsx = require('node-xlsx'); // 云函数入口函数 exports.main = async(event, context) => { try { let {userdata} = event //1,定义excel表格名 let dataCVS = 'test.xlsx' //2,定义存储数据的 let alldata = []; let row = ['id', '姓名', '微信号']; //表属性 alldata.push(row); for (let key in userdata) { let arr = []; arr.push(userdata[key].id); arr.push(userdata[key].name); arr.push(userdata[key].weixin); alldata.push(arr) } //3,把数据保存到excel里 var buffer = await xlsx.build([{ name: "mySheetName", data: alldata }]); //4,把excel文件保存到云存储里 return await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }) } catch (e) { console.error(e) return e } } [代码] 五,把excel存到云存储里并返回对应的云文件地址 我们上面已经成功的把数据存到excel里,并把excel文件存到云存储里。可以看下效果。 [图片] 我们这个时候,就可以通过上图的下载地址下载excel文件了。 [图片] 我们打开下载的excel [图片] 其实到这里就差不多实现了基本的把数据保存到excel里的功能了,但是我们要下载excel,总不能每次都去云开发后台吧。所以我们接下来要动态的获取这个下载地址。 六,获取云文件地址下载excel文件 [图片] 通过上图我们可以看出,我们获取下载链接需要用到一个fileID,而这个fileID在我们保存excel到云存储时,有返回,如下图。我们把fileID传给我们获取下载链接的方法即可。 [图片] 1,我们获取到了下载链接,接下来就要把下载链接显示到页面 [图片] 2,代码显示到页面以后,我们就要复制这个链接,方便用户粘贴到浏览器或者微信去下载 [图片] 下面把我这个页面的完整代码贴给大家 [代码]Page({ onLoad: function(options) { let that = this; //读取users表数据 wx.cloud.callFunction({ name: "getUsers", success(res) { console.log("读取成功", res.result.data) that.savaExcel(res.result.data) }, fail(res) { console.log("读取失败", res) } }) }, //把数据保存到excel里,并把excel保存到云存储 savaExcel(userdata) { let that = this wx.cloud.callFunction({ name: "excel", data: { userdata: userdata }, success(res) { console.log("保存成功", res) that.getFileUrl(res.result.fileID) }, fail(res) { console.log("保存失败", res) } }) }, //获取云存储文件下载地址,这个地址有效期一天 getFileUrl(fileID) { let that = this; wx.cloud.getTempFileURL({ fileList: [fileID], success: res => { // get temp file URL console.log("文件下载链接", res.fileList[0].tempFileURL) that.setData({ fileUrl: res.fileList[0].tempFileURL }) }, fail: err => { // handle error } }) }, //复制excel文件下载链接 copyFileUrl() { let that=this wx.setClipboardData({ data: that.data.fileUrl, success(res) { wx.getClipboardData({ success(res) { console.log("复制成功",res.data) // data } }) } }) } }) [代码] 给大家说下上面代码的步骤。 1,下通过getUsers云函数去云数据库获取数据 2,把获取到的数据通过excel云函数把数据保存到excel,然后把excel保存的云存储。 3,获取云存储里的文件下载链接 4,复制下载链接,到浏览器里下载excel文件。 到这里我们就完整的实现了把数据保存到excel的功能了。 文章有点长,知识点有点多,但是大家把这个搞会以后,就可以完整的学习小程序云开发的:云函数,云数据库,云存储了。可以说这是一个综合的案例。 有什么不懂的地方,或者有疑问的地方,请在文章底部留言,我看到都会及时解答的。后面我还会出一系列关于云开发的文章,敬请关注。
2019-09-07 - 云函数利用npm exceljs导出excel 1000个SKU带高清图片
[图片] 想要导出大量数据就得从腾讯云登录你得小程序云开发 CloudBase可以修改900秒得超时时间,也就是可以执行15分钟;这次导出1000个SKU用了10分钟; [图片] [图片] 执行了10分钟,导出了24m的文件 [图片] [图片] 不多说,熬了俩个通宵,以及各位大佬的指点总结出来的 直接上源码 // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ // API 调用都保持和云函数当前所在环境一致 env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() //exceljs 安装:npm i exceljs const ExcelJS = require('exceljs'); //引入superagent 安装 :npm i superagent const superagent = require('superagent'); // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() //查询要生成表格的数据 集合名记得改 const dataList = await db.collection("user").where({ jsxj:true //自己改改 //突破100条数据限制 }).limit(1000).get() //获取查询到的数据 const data = dataList.data //新建一个工作簿 const workbook = new ExcelJS.Workbook(); //创建一个工作表 const worksheet = workbook.addWorksheet('Sheet 1'); //设置第一行的行高 worksheet.properties.defaultRowHeight = 152.1; //设置第一行的列宽 worksheet.properties.defaultColWidth = 24.95; //设置第一行的文字垂直居中 worksheet.getRow(1).alignment = { vertical: 'middle', horizontal: 'center' }; //设置第一行的文字大小加粗 worksheet.getRow(1).font = {size: 16, bold: true}; //创建表头信息 worksheet.columns = [{header:'图片',key:'id'},{header:'产品标题',key:'url'}]; //循环往工作表里加数据 for (let rowIndex in data) { const rowcontent = [] //获取图片链接 var urls = encodeURI(data[rowIndex].cpimg[0]+'/sf200') //获取urls的图片链接转化成Base64 const img2Base64 = await new Promise(async function (resolve, reject) { const url = urls; await superagent.get(url).buffer(true).parse((res) => { let buffer = []; res.on('data', (chunk) => { buffer.push(chunk); }); res.on('end', () => { const data = Buffer.concat(buffer); const base64Img = data.toString('base64'); resolve('data:image/png;base64,'+base64Img) } ); }); }) //把img2Base64的数据生成imageId2,并添加图片到工作表 const imageId2 = workbook.addImage({ base64: img2Base64, extension: 'png', }); //获取imageId2,修改上面工作表里的图片大小位置 worksheet.addImage(imageId2, { tl: { col: 0, row: rowIndex-1+2}, ext: { width:200, height:200 }, editAs: 'undefined' }); //因为第一行是图片,所以这里传一个第一行的空值 rowcontent.push("") rowcontent.push(data[rowIndex].cpname) //更新表格 worksheet.addRow(rowcontent); } //生成表格 const buffer = await workbook.xlsx.writeBuffer(); //上传到云存储 return await cloud.uploadFile({ cloudPath: 'nhb/' + Date.now() + '.xlsx', fileContent: buffer, }) }
2022-03-24 - 由一次分享引发的生产事故
~ 由一次分享引发的生产事故 ~ ~ 小程序开发笔记来啦,为你们加油ヾ(◍°∇°◍)ノ゙每天进步一点点, 01、场景今天有用户反馈,挑战答题小程序一个细节问题 这几天参与了一个答题活动的制作,在答题活动方案中,有一个细节就是分享可以获得积分 具体规则如下 [图片] 这上面有个具体的规则是邀请1个用户得1分,每日限得3分 我在开发的时候封装了一个分享链接的生成逻辑,这样每个页面都可以直接复用,具体代码如下所示 最后通过小程序右上角,系统分享出去 [图片] ~ [图片] ~ 但是在实际测试的时候缺发现问题了 ~ [图片] ~ 正常情况下,后面拼接一个fromOpenid就对了,但是这里却反复的不断拼接,导致实际拼接的fromOpenid就不符合预期的逻辑了 ~ 其实这个问题就是云函数写的很低级的错误,云函数并不是每次都重置data,这样就造成了,这个data会不断的拼接导致这个path会越来越长, 02、改造方案其实要改造这个地方十分容易 [图片] ~ 在这里生成path的时候不需要拼接即可,但是要真正理解这个问题,还是要从原函数的原理出发 03、参考文章 ~ 使用云函数切记不要再export 之外定义全局变量.? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/00068a06cb8340524a7aaa89c51413 ~
2022-04-07 - 如何使用微信小程序·云开发的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 - 微信小程序中遮罩层的滚动穿透问题
小程序的自定义的,遮罩层会出现滚动穿透的问题,即遮罩层下面的页面依旧可以滚动,这个问题有解决办法吗?
2017-09-25 - 投票评选活动小程序复盘
本文初写于2022-01-11,更新于2022-01-12 云开发搭建投票活动小程序 ~ 今天9:30分 历时一周开发的投票活动小程序迎来了第一波大考,为了迎接这次大考我做了如下几个工作 这是我做答题活动沿袭过来的经验 1、充值 本次选用的是云开发的按量付费服务,考虑到日活能达到10000+,提前充值20元,以备不时之需 [图片] 2、购买云开发的资源套餐包,具体可根据实际场景购买,由于云开发资源包第一次购买是有优惠的 具体花费如下 1)100GB CDN资源包 2)100万 GBs的云函数资源包 3)3000万读,1500万写的数据库资源包 分别消费为4.5,9.9,15元 ~ [图片] ~ [图片] 3、设置集合索引 将小程序中涉及的高频查询场景设置必要的索引,因提高查询的效率 比如投票明细集合 设置索引 为 historys_openid_today_index 用于查询用户每天是否投过哪些候选人 ~ [图片] ~ 在活动的过程中,会不断分享本次的一些数据 ~~~~ 我在前面写过3个优化的场景,唯独漏了一个场景4,那就是CDN,今天早上醒来,收到欠费的推送我就明白了 [图片] ~ 大家看上图就知道,CDN消费了130多,本来是期望用云开发减少开支的,没有达到预期的效果,所以现在补充一条最重要的一点 优化4 1)图片压缩,将涉及到的图片素材尽量压缩,在保证可辨识的情况下图片压缩 2)将图片找靠谱图床停靠 最终针对CDN消费太大的问题,我今天也花了一天的时间来优化 我描述下今天的方案历程 1、方案1就是将图片素材压缩,因为我发现用户给我的图片素材实在是太大了,之前大意了,以为买了CDN套餐就万事大吉了; 但是我发现这个方案升级后,依然没有改变CDN流量大的现状,如下图所示 [图片] 2、我在下午18:30左右将云存储的图片迁移到我的七牛云,CDN的流量马上降下来了,但是这并不解决问题,只是将费用转移了,最后还是要掏钱 最后不得已按照群里给的提示,找到白嫖图床的方案 [图片] ~ 实不相瞒,目前找的图床是我平时经常用的产品,也是我最大的收获,我一直使用这个产品,但是之前没有想到他们竟然可以作为图床,而且不会存在防盗链,不能访问的问题, 我使用的图床就是 https://yiban.io/ [图片] ~ 具体怎么用的我就不展开细讲了。 又是一个日活过万的小场面 [图片] ~
2022-01-12 - 线下培训机构刷题小程序2.0
~ 线下培训机构刷题小程序2.0 ~ 在一年前(2020-11-10)我写过这个小程序,当时小程序刚上线,功能都在但是细节不算特别精细,对于我这种有工匠精神,有追求的开发者来讲是说不过去的 具体见下文 线下培训机构专用刷题小程序? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000ae206698f70e7b83b7ca2756013 ~ 今天是2022年考研的最后一天,2022年考研已落下帷幕, 神兽出笼,我也可以好好掰扯掰扯这一年的工作 这一年我除了新 推出了几款针对考研专用的刷题小程序之外,一直没有忽略对原有小程序的完善 特别是 今天描述的这个线下培训机构专用刷题小程序 由于该小程序对接的合作机构有不少具备一定的实力,特别是在业内有一定的知名度,所以我对这个小程序的精细化完善功能一直就没有停过。 具体上图 为了这个文章我特意借了舍友的IOS 13 plus,就为了追求一个高精度哈 ~ ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~
2021-12-27 - wx.navigateTo ,跳转超过10次怎么点不动的解决办法。
自定义一个跳转方法思路:如果跳转的url是小程序页面栈已有的页面,则用wx.navigateBack的方式回退到那一层 wx.navigateBack相当于手动修改页面栈,将url后面的所有页面栈都删掉。 goPage(url) { // 如果这个小程序,入口页面用的不多的话,可以忽略掉indexUrl,不做判断 var indexUrl ="pages/index/index" // 小程序入口页面 var page = getCurrentPages(); // 获取到小程序的页面栈 var delta = -1; // 如果要跳转的url是入口页面,并且页面栈大于2,用回退的方式,清除页面栈内容 if(page.length>2 && indexUrl==url){ for(var i=0;i<page.length;i++){ if(url==page[i].route){ //入口页面的索引i delta = page.length - i -1 // 算出要回退几步回到 入口页面 break } } } // 回退-1步的话,表示页面栈 里面没有首页 if(delta==-1){ wx.navigateTo({ url: url }) }else{ wx.navigateBack({ delta: delta }) } } 其他优化方法: 入口页面的写法:组建tabbar,首页和我的都写成组建的形式。这种写法,在首页和我的页面切换的时候,不会使用wx.navigateTo ,不占用跳转的栈内存。 <home wx:if="{{PageCur=='0'}}" params="{{params}}" bind:changeTab="changeTab"></home> <mine wx:if="{{PageCur=='1'}}" params="{{params}}"></mine> <tabbar id="tabbar" tabNum="{{PageCur}}" bind:clickFun="changeTab"></tabbar> wx.redirectTo方法 代替 navigateTo 减少一层栈的使用,但是跳转的页面没有返回按钮。
2021-09-29 - 基于云开发的答题活动小程序v2.0-完整项目分享(附源码)
简介答题活动小程序v2.0,是一个微信小程序答题软件,它基于微信原生小程序+云开发实现。 它使用了最新的前端技术栈,具有原生APP体验服务的小程序框架,小程序视图层描述语言 WXML 和 WXSS,以及基于 JavaScript 的逻辑层框架,响应的数据绑定,提供了丰富的基础组件和API。 提炼了典型的业务模型,它可以帮助你快速搭建各种形式的答题软件产品。相信不管你的需求是什么,本项目都能帮助到你。 [图片] [图片] 目录结构小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件(app.js、app.json、app.wxss)组成,必须放在项目的根目录。目录结构如下: [图片] 一个小程序页面由这四个文件组成,你可以留意到这个项目里边有不同类型的文件: .json 后缀的 JSON 配置文件;.wxml 后缀的 WXML 模板文件;.wxss 后缀的 WXSS 样式文件;.js 后缀的 JS 脚本逻辑文件; 功能主要包含六大功能模块页面,首页、答题页、结果页、活动规则页、答题记录页、排行榜页。适用于交通安全答题、 消防安全知识宣传、 安全生产知识学习、百年历史知识答题活动、学法守法有奖答题等。 - 首页 - 微信授权登录 - 获取微信头像和昵称等 - 活动规则页 - 答题页 - 实现用云开发实现查询题库功能 - 题库随机抽题 - 实现动态题目数据绑定 - 答题交互逻辑 - 切换下一题 - 提交答卷保存到云数据库集合 - 系统自动判分 - 结果页 - 答题结果页从云数据库查询答题成绩 - 实现转发分享答题成绩功能 - 答题记录页 - 查询历史成绩 - 排行榜页 - 成绩由高到低进行排名 - 实现页面间跳转功能 - 路由 - 界面交互 - 消息提示框 - loading 提示框 源码地址uem/答题活动小程序v2 https://gitee.com/uemeng/answer-activity-applet-v2.0 作品体验[图片]
2021-12-20 - 谈谈这一年,身边跟小程序开发有关的人和事
谈谈这一年,身边跟小程序开发有关的人和事 ! 今天是2021-11-11,做今年的总结,还有点早,但不妨碍开始记录今年小程序运营过程中结交的一些人和事~ 本人主要记录一些流水记忆 很荣幸接触到小程序的开发,由于本人习惯记录开发过程中的种种心得和笔记,暂且谈不上文章,只能称呼为笔记,在社区相对比较活跃, 也通过社区或者其他一些渠道人接触到一些小程序发烧友 他们有的是全职开发,有的用爱发电,比如说我,平时在一个群内有说有笑,甚是欢喜 ~ 但是之前用心维护的一个小程序核心群,由于群友无约束的聊天被封号了,因为我在维护这些群的时候,我会有意识的把一些活跃的同学往一个核心群拉, 那就是这个核心群 之前聊天相当活跃,自从被封之后,再创建的其他群,都反响平平,我也疏于再去维护了,因为至于我而言,维护这种群纯碎是一种无意义的事情 之前维护那个活跃群,感觉还有点价值,因为里面确实积累了不少大(话)佬(痨) ~ 在群里也认识了不少优秀的小程序开发和运营以及靠小程序支持的老板,他们的小程序有的日活百万,有的日活可能就个位数,真的都有 我对这里面的几个大佬都很熟悉, 里面有无码科技的同学,有甚至图样的同学,有行业No1的同学,总之很优秀拉 今天我讲我印象比较深刻的几个 暂且称呼他为A A的小程序从各方面都很优秀,除此之外他在群里发言都很有价值,对事物的认知和看法是远超群里一般用户的 他的小程序偏小众,虽然竞争少,但是即使产品做的很出色,用户也陆续有上升的趋势,但是由于首先于受众人群本来就少,所以每日的流量主收益,离A同学的预期差距甚大 由于A同学本身是全职开发,这种收益不能养活自己甚至还入不敷出的时候,结局是可以预知的 (肯定滚回去上班了) A同学已近期入职某大厂,做一颗安安静静的螺丝刀,群内已很少再见到他发言了,我能怎么说呢 祝福A同学 我们能从A同学的经历,得到什么呢? 那就是目前纯靠小程序养活自己,养活团队,真的是太难了,即使那位小程序日活百万(行业排名No1)的老板,养活团队也是有很大的压力 确实 小程序的红利期已经到拐点了,如果还有同学天真的以为用心做一款产品真的可以养活自己那就答错特错了 其实我也一直在摸索求证,通过身边的人我其实已经放弃这种模式 今天絮絮叨叨很多,但是还有很多话没有说完, 2021-11-11 后面继续更新
2021-11-11 - 考研刷题小程序多选题的交互痛点
考研刷题小程序多选题的交互痛点 ~ 其实这个问题在我的待办事项里面已经放了很久,前一阵我做竞品分析的时候,发现过一些很好的解解方案,但是当时仅仅是竞品体验,没有总结相关的解决方案,也就是说当时的竞品分析没有落地形成文档 今天在回学校的火车上,有几个小时,重新把这块梳理了下。 不管是考研刷题小程序还是日常的答题活动小程序,对于多选题都存在一个痛点 那就是对于用户多选漏选的场景,如果区分展示,具体说就是,某个题的答案是ABD,用户只选择了AD,而漏了B,虽然AD都是正确答案,但是如何来标记B,并且明显的标记出当前选项为漏选 今天把这个方案在我的考研刷题小程序落地,具体的界面截图如下所示 ~ [图片] ~ [图片] ~ [图片] ~ [图片] ~ 总结 就是对于多选题里面的漏选项,一定要标记出是漏选,这样对于用户而言,能起到很明显的提示作用。
2021-09-24 - 让小程序支持代码高亮
对于编程技术类的小程序来说,在文章会有很多代码,那么代码高亮就是一个文章显得很出色的需求了。代码高亮功能的实现,主要是依靠小程序里对富文本内容的解析。对于富文本解析,微慕小程序专业版以前采用的开源的wxParse组件,但这个组件不支持代码高亮,且二次开发的难度较大。从微慕小程序专业版v3.8.0开始引入了mp-html组件,该组件提供对代码高亮显示的支持。 在小程序里通过mp-html实现代码高亮方式如下: 1.在小程序里引入mp-html将mp-html的源码中对应平台的代码包(dist/platform)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加如下代码: { "usingComponents": { "mp-html": "/components/mp-html/index" } } JSON复制代码 2.在小程序里使用mp-html1.在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}"></mp-html> HTML复制代码 2.在需要使用页面的 js 文件中添加 Page({ onLoad () { this.setData({ html:'<div>Hello World!</div>' }) } }) JavaScript复制代码 3.在mp-html里引入代码高亮highlight插件在mp-html的源代码里tools/config.js 中的 plugins 中启用highlight插件,设置完成后,可通过项目提供的命令行工具生成新的组件包。 编辑 plugins/highlight/config.js ,可以选择是否需要以下功能: copyByLongPress 是否需要长按代码块时显示复制代码内容菜单 showLanguageName 是否在代码块右上角显示语言的名称 showLineNumber 是否在左侧显示行号 引入本插件后,html 中符合以下格式的 pre 将被高亮处理: <!-- pre 中内含一个 code,并在 pre 或 code 的 class 中设置 language --> <pre><code class="language-css"> p { color: red } </code></pre> HTML复制代码 本插件的高亮功能依赖于prismjs,默认配置中仅支持 html、css、c-like、javascript 变成语言,如果需要更多语言下需要去prismjs网站下载对应的 prism.min.js 和 prism.css 并替换 plugins/highlight/ 目录下的文件。 目前微慕专业版小程序里代码高亮支持的编程语言是TIOBE排名前20的编程语言,比如C 、Java、Python 、C++、C Sharp、PHP等。 4.在wordpress里文章页面支持代码高亮微慕小程序是通过wordpress的api构建的,因此如果在wordpress文章页面也同时支持代码高亮就完美了,做到这个其实比较简单,只要把mp-html目录下plugins/highlight/prism.min.js 和 prism.css 引入到wordpress的主题模板即可。 如果在wordpress的文章里代码高亮支持:显示行号,复制代码,显示语言,可以去prismjs下载相应的插件。 1.显示编程语言的prismjs插件:https://prismjs.com/plugins/show-language/ 2.显示行号的prismjs插件:https://prismjs.com/plugins/line-numbers/ 3.复制代码的prismjs插件:https://prismjs.com/plugins/copy-to-clipboard/ 下载上述插件后,引入到wordpress主题里,在code 便签里加入data-prismjs-copy 和data-prismjs-copy-success,就可以支持上述三个功能了。 示例代码如下: <pre><code class="language-html line-numbers" data-prismjs-copy="复制代码" data-prismjs-copy-success="代码已复制"> </code></pre>
2021-08-13 - 基于云开发的答题活动小程序v2.0,终于赶在11月最后一天完成了
是的,你没听错没看错。这个,就是基于云开发的答题活动小程序,并且是v2.0版本! 之前在做“手把手教你搭建答题活动小程序v1.0系列文章”的教程的时候,我就已经马不停蹄地在构思v2.0的迭代版本的内容,以及持续地投入时间和精力进行设计与开发搭建中了。 终于!赶在11月最后一天!!完成了!!! 答题活动小程序v2.0,主要包含六大功能模块页面,首页、答题页、结果页、活动规则页、答题记录页、排行榜页。接下来,我简单的归纳总结一下,分析一下技术要点,以及分享源码给大家。与君共勉。 [图片] 先看看效果吧[图片] [图片] 再聊聊技术栈以微信原生小程序+云开发为主,使用微信原生小程序开发还是比较方便的,可以搭载云开发能力的小程序端SDK,使用javascript就能操作数据库。 当前版本功能微信答题活动小程序,当前版本是v2.0,简单地罗列一下实现了的功能: 活动规则页答题记录页排行榜页题库随机抽题查询历史成绩微信授权登录获取微信头像和昵称等首页、答题页、结果页实现页面间跳转功能实现转发分享答题成绩功能实现用云开发实现查询题库功能实现动态题目数据绑定答题交互逻辑切换下一题提交答卷保存到云数据库集合系统自动判分答题结果页从云数据库查询答题成绩 体验一下最后,需要来体验一下吗?欢迎各位在底部留言或者提提bug,然后想想,如果你会怎么解决这个问题。源码传送门,答题活动小程序v2.0 [图片] v1.0前程回顾这里可以简单地做一个前程回顾,有兴趣的可以去翻一翻,阅读一下这些短篇教程。真的做得很用心,特别适合新手小白快速入门微信小程序与云开发。 手把手教你搭建答题活动小程序系列文章: 消防安全知识竞答活动小程序 https://developers.weixin.qq.com/community/develop/article/doc/0002ca223b8370d5c9fce586e56813 安全知识线上答题活动小程序-答题功能解读 https://developers.weixin.qq.com/community/develop/article/doc/0008ea6e43894864020d8452456c13 不破不立,分享源码,优质的消防安全知识竞答活动小程序 https://developers.weixin.qq.com/community/develop/article/doc/000c08d38205f04c3f0d25f075c013 手把手教你搭建消防安全答题小程序-首页 https://developers.weixin.qq.com/community/develop/article/doc/000242a28904a022570d57cc856c13 手把手教你搭建消防安全答题小程序-答题页 https://developers.weixin.qq.com/community/develop/article/doc/000c282e6d0a78b86c0d02af555413 手把手教你搭建消防安全答题小程序-答题结果页 https://developers.weixin.qq.com/community/develop/article/doc/00064c224f0560f9790d4069256c13 手把手教你搭建消防安全答题小程序-实现页面间跳转功能 https://developers.weixin.qq.com/community/develop/article/doc/0008400ebac4b0938d0d4ca8e5bc13 手把手教你搭建消防安全答题小程序-实现转发分享答题成绩功能 https://developers.weixin.qq.com/community/develop/article/doc/00064a97294000c29e0d1154d5b813 手把手教你搭建消防安全答题小程序-用云开发实现查询题库功能 https://developers.weixin.qq.com/community/develop/article/doc/0002cac6f7018077a00d7a36456413 手把手教你搭建消防安全答题小程序-将用云开发获取到的题目渲染到答题页面 https://developers.weixin.qq.com/community/develop/article/doc/000c04a413c49850d90d281ec56c13 手把手教你搭建消防安全答题小程序-实现答题功能以及提交答卷到云数据库 https://developers.weixin.qq.com/community/develop/article/doc/00006654a7cce0d2e40d174755b413 手把手教你搭建消防安全答题小程序-在结果页中实现从云数据库查询成绩并展示 https://developers.weixin.qq.com/community/develop/article/doc/000a0e480f86b012f60da776c51413 基于云开发的答题活动小程序v1.0,开开开源啦 https://developers.weixin.qq.com/community/develop/article/doc/0006c6920245e07df20dccbde56c13 基于云开发的微信答题活动小程序v1.0搭建部署帮助文档 https://developers.weixin.qq.com/community/develop/article/doc/000ce8d4ef0dc864791d0f2ce56c13 用云开发搭建的微信答题小程序v1.0 https://developers.weixin.qq.com/community/develop/article/doc/000a2adbde4918698e1d5b7405b013
2021-11-30 - 开源答题小程序今天突破400个star了
开源答题小程序今天突破400个star了 ~ 之前给自己立了个flag,就是每增加100个star就写一篇总结的文章,这次100个star用了大概3个月的时间,还是比较欣慰的。 从开发小程序,到开发答题小程序,再到开源,一步步走来,慢慢的获得大家的认可,其实内心还是蛮高兴的 [图片] ~ 做这个答题小程序之前呢,其实也尝试过几个,刚开始从垃圾分类,到预约小程序,到群通知小程序,最后把垃圾分类小程序里面的垃圾自测提出来单独做了这个简化版的答题小程序 开源本不是我的初心,其实内心还是想通过开源作为引流进而实现答题小程序方案的销售 但是我发现一个规律,基本上,自己能搭建小程序的都不会付费购买的,跟费用多少没有太多关系 那最后还是回归到开源本身,也不期望能真实转化多少成交。 如果通过开源,能解决大家的问题,其实目的已经达到了。 在开源本身呢,我按照每年二个主打产品的计划,按部就班的实现我的答题小程序全场景覆盖,期望在未来,我的答题小程序产品能越来越被大家所接受和认可 在最近呢,我一直在维护今年的一个主要版本,该版本主要的场景是考研,科目考研,目前主要的功能都已完成,还在一些细节的打磨上,等该新产品上线,定会写一篇比较详细的开发过程记录文章,分享这其中的苦与乐 独立开发者跟考研无异,都是一个人的战场,孤独和寂寞随行 坚持住,前途是光明的 ~
2021-11-02 - 基于云开发的答题活动小程序v1.0,开开开源啦
基于云开发的微信答题活动小程序v1.0,开开开源啦!!!这个答题小程序,技术栈是基于云开发的微信原生小程序。 搭建教程系列文章11月是全国“119”消防宣传月,不少企事业单位会举办消防安全知识竞赛,因此我搭建了最新版的消防安全知识答题活动小程序。 我不但快速地完成了设计与开发工作并部署上线交付使用,还写了手把手教你搭建答题活动小程序系列文章,用以帮助初学者快速入门云开发。感兴趣的可以去我主页翻看系列文章,这里就不一一列举出来了。 答题小程序源码地址附上源码地址:uem/消防安全知识竞答活动小程序 下一篇,将补充一篇部署帮助文档,用以帮助初学者基于云开发快速部署上线一个答题活动小程序。 答题活动小程序界面[图片] 答题活动小程序版本当前版本v1.0 首页、答题页、结果页实现页面间跳转功能实现转发分享答题成绩功能实现用云开发实现查询题库功能实现动态题目数据绑定答题交互逻辑切换下一题提交答卷保存到云数据库集合系统自动判分答题结果页从云数据库查询答题成绩 后续我会在这个基础上继续开发,答题类微信小程序v2.0、v3.0,功能将会更加多以及更加完善。 计划版本v2.0 答题记录列表页登录页活动规则页题库随机抽题查询历史成绩微信授权登录获取微信头像和昵称排行榜等... 计划版本v3.0 题库练习支持多种题型选项乱序答题倒计时错题解析支持生成海报分享支持答题后抽奖后台数据监控后台管理等...
2021-11-17 - 一个标准的隐私协议模板如何填写
一个标准的隐私协议模板如何填写 ~ 近期审核都需要填写隐私协议,很多人都不知道从何处填写,现提供一个标准模板,可供大家参考 ~ [图片] ~ [图片] ~ 值得注意的是: 即使没有收集用户信息也建议如此操作,方可审核通过,这是经验贴,血淋淋的教训每天都还在发生 [图片] ~
2021-11-08 - 不破不立,分享源码,优质的消防安全知识竞答活动小程序
不破不立,分享源码。立过的flag,以立冬之名,分享优质的消防安全知识竞答活动小程序。 介绍消防安全知识答题活动, 答题、 交通安全答题、 消防安全知识宣传、 答题活动、有奖答题 应用场景11月是全国“119”消防宣传月,主题是“落实消防责任,防范安全风险”。不少单位会举办消防安全知识竞赛,因此我搭建了最新版的消防安全知识竞答小程序。帮助大家可以定期举办消防安全知识竞答活动,让大家了解火灾隐患,学习消防知识,预防火灾,认真贯彻消防法规。 软件架构微信原生小程序+云开发 安装教程下载开发者工具导入小程序项目创建云开发环境 当前版本 首页 转发分享答题 答题页 结果页 转发分享成绩 界面截图[图片] 此套优质UI可以用于二开,拿走使用请先赞赏支持,谢谢。需要持续更新完善版本的,请点赞、点Star反馈~ ↓↓ 源码地址
2021-11-08 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 获取手机号使用云函数调用的错误,变量未定义?
日志 [图片] 控制台报错 [图片] 报错代码 [图片] 云函数代码 [图片]
2021-06-04 - 如何实现一个自定义数据版省市区二级、三级联动
社区可能有其他的方案了,但是再分享下吧,给有需要的童鞋。 效果图: [图片] 额,这个视频转GIF因为社区上传不了大图,所以剪了一部分,具体的效果还是直接工具打开代码片段预览吧~ 第一步:你的页面JSON引入该组件: [代码]{ "usingComponents": { "city-picker": "/components/cityPicker/index" } } [代码] 第二步:你的页面WXML引入该组件 [代码]<city-picker visible="{{visible}}" column="2" bind:close="handleClick" bind:confirm="handleConfirm" /> [代码] 第三步:你的页面JS调用 [代码]// 显示/隐藏picker选择器 handleClick() { this.setData( visible: !this.data.visible }) }, // 用户选择城市后 点击确定的返回值 handleConfirm(e) { const { detail: { provinceName = '', provinceId = '', cityName, cityId='', areaName = '', areaId = '' } = {} } = e this.setData({ cityId, cityName, areaId, areaName, provinceId, provinceName }) } [代码] 组件属性 属性 默认值 描述 visible false 是否显示picker选择器 column 3 显示几列,可选值:1,2,3 values [0, 0, 0] 必填,默认回填的省市区下标,可选择具体省市区后查看AppData的regionValue字段 close function 点击关闭picker弹窗 confirm function 点击选择器的确定返回值 confirm: 属性 默认值 描述 provinceName 北京市 省份名称 provinceId 110000 省份ID cityName 市辖区 城市名称 cityId 110100 城市ID areaName 东城区 区域名称 areaId 110000 区域Id 至于怎么获取你想默认城市的下标,可以滑动操作下选中省市区后,点击确定后查看appData里的regionValue的值。 以上就是一个自定义数据版本的省市区二级、三级联动啦,老规矩,结尾放代码片段。 https://developers.weixin.qq.com/s/F9k9cTmT7LAz
2022-07-20 - 如何为老板节省几个亿之graphql介绍
写过接口和联调过接口的人想必都对接口的约定,文档维护和联调等问题经常头痛不已,因为我们的业务里有大量不同的应用和系统共同使用着许许多多的服务api,而随着业务的变化和发展,不同的应用对相同资源的不同使用方法最终会导致需要维护的服务api数量呈现爆炸式的增长,比如我试着跑了下我们自己业务里的接口数量,线上正在运行的就有超过1000多个接口,非常不利于维护。而另一方面,创建一个大而全的通用性接口又非常不利于移动端使用(流量损耗),而且后端数据的无意义聚合也对整个系统带来了很大的资源浪费。 总结一下,我们目前接口的开发遇到的主要痛点如下: 痛点 字段冗余 比如用户接口,刚开始时,返回的信息会比较少,例如只有 id,name,后来用户的信息增加了,就在用户接口中返回更多的信息,例如 id,name,age,city,addr,email,headimage,nick,但可能很多客户端只是想获取其中少部分信息,如name,headimage,却必须得到所有的用户信息,然后从中提取自己想要的,这个情况会增加网络传输量,并且也不利于客户端处理数据(为老板增加了很多不必要的开支😄。 参数类型校验 我们几乎每个接口都需要做基本的字段校验,比如某个参数是否必需,是不是数字,是不是数组,是不是布尔型,是不是字符串等等,而除了服务端要校验客户端传来的参数,客户端自己也需要去校验服务端返回的参数,比如客户端要的是数组,你有没有返回数组,试问有哪个前端想整天去写出[代码]var x = data?(data.obj?data.obj.name:null):null;[代码]这样的代码?我们业务自己线上出的很多问题也是因为客户端没有严格的校验服务端返回的数据是否符合预期造成的。比如下图是我们业务某天的js error错误日志上报: [图片] 文档与维护 这个可能是和接口打交道的人员感触最深的环节了,联调基本靠猜,维护历史遗留代码更是一种高深的猜一猜游戏。正常理论上来讲,写接口就要写对应的文档,接口一旦有变动,文档也一定要及时更新。但是,有不少团队因为种种原因,没有能及时维护文档甚至有不少连文档都没有,全部依赖口口相传和读源码。这个问题其实是困扰我个人在开发过程中最大的一个问题。 请求多个接口 我们经常会在某个需求中需要调用多个独立的API接口才能获取到足够的数据,例如客户端要去显示一篇文章的内容,同时也要显示评论、作者信息,那么就可能需要调用文章接口、评论接口、用户接口。这种方式非常的不灵活。另一个case是很多项目都会去拉一些不同的配置文件来决定展示什么,比如我们业务里的app,一打开就有10多个请求,非常不合理,不仅多个请求浪费了带宽,而且速度慢,客户端处理的请求也复杂。 如何解决? 对于上面的问题,我们来逐一攻破: **字段冗余?**那我可不可以客户端要什么字段,服务端就给什么字段的值? **参数类型校验?**那我可不可以定义一个返回数据格式与请求的数据格式的一个强类型的约束? **文档与维护?**这里的核心问题我觉的是因为我们写不写文档对于我们的代码没有约束力,所以我们写不写文档对代码的正常运行是没有任何影响的。所以我们能不能考虑和代码结合起来,我代码里怎么写的,文档就怎么生成的? **请求多个接口?**那我能不能客户端可以问服务端要1、2、3这些数据,服务端一次给我返回就行? graphql方案介绍 graphql的方案完美的解决了以上所有问题,连大名鼎鼎GitHub也抛弃了自己非常优秀的REST API接口,全面拥抱graphql了。 GraphQL是Facebook 在2012年开发的,2015年开源,2016年下半年Facebook宣布可以在生产环境使用,而其内部早就已经广泛应用了,用于替代 REST API。facebook的解决方案和简单:用一个“聪明”的节点来进行复杂的查询,将数据按照客户端的要求传回去,后端根据GraphQL机制提供一个具有强大功能的接口,用以满足前端数据的个性化需求,既保证了多样性,又控制了接口数量。 GraphQL并不是一门程序语言或者框架,它是描述你的请求数据的一种规范,是协议而非存储,GraphQL本身并不直接提供后端存储的能力,它不绑定任何的数据库或者存储引擎,它可以利用已有的代码和技术来进行数据源管理。 一个GraphQL查询是一个被发往服务端的字符串,该查询在服务端被解释和执行后返回JSON数据给客户端。 和Rest Api的对比 RESTful:服务端决定有哪些数据获取方式,客户端只能挑选使用,如果数据过于冗余也只能默默接收再对数据进行处理;而数据不能满足需求则需要请求更多的接口。 GraphQL:给客户端自主选择数据内容的能力,客户端完全自主决定获取信息的内容,服务端负责精确的返回目标数据。 为什么推荐用GraphQL方案 说人话就是:减少工作量 专业一点:提供一种更严格、可扩展、可维护的数据查询方式 优点 能为老板节省几个亿的流量(由前端定义需要哪些字段 再也不需要对接口的文档进行维护了(自动生成文档,代码里定义的结构就是文档 再也不用加班了(真正做到一个接口适用多个场景 再也不用改bug了(强类型,自动校验入参出参类型 新人再也不用培训了(所有的接口都在一颗数下,一目了然 再也不用前端去写假数据了(代码里定义好结构之后自动生成mock接口 再不用痛苦的联调了(代码里定义好结构之后,自动生成接口在线调试工具,直接在界面里写请求语句来调试返回,而且调试的时候各种自动补全 react/vue/express/koa无缝接入(relay方案/apollo方案 更容易写底层的工具去监控每个接口的请求统计信息(都在同一个端点的请求下 不限语言,除了官方提供的js实现,其他所有的语言都有社区的实现 生态是真的好啊,有各种方便易用的开发者工具 怎么用? [图片] 其实graphql的理念还算简单,总的来说,就如图中所示: 接口提供方定义好强类型的数据入参和返回的数据结构 客户端发送一个带有查询语句(graphql的查询协议)的请求,请求里定义了需要哪些数据。 服务端返回给客户端符合客户端预期的json字符串结果 github官方提供了github的graph api的在线调试工具,地址是https://developer.github.com/v4/explorer/,实际上这个工具是社区提供的,我们在写好graph服务的时候,也可以自动一键生成这样一个服务。你可以在github提供的这个调试工具里多试试查询语句。 在REST的API中,给哪些数据是服务端决定的,客户端只能从中挑选,如果A接口中的数据不够,再请求B接口,然后从他们返回的数据中挑出自己需要的,而在GraphQL 中,客户端直接对服务端说想要什么数据,服务端就负责精确的返回目标数据。 可以把GraphQL看成一棵树,GraphQL对外提供的其实就只有一个接口,所有的请求都通过这个接口去处理,相当于在graph服务内部做了针对不同请求的路由处理。 根节点下分为2大类,一类是Query类(主要是查询相关的,相当于get),一类是Mutation类(主要是非幂等操作,post,put,delete)。 下图是我写的一个微博类应用的demo,图是写完服务后用工具voyager生成的这个graph服务的数据结构图: [图片] 注:其中类型后面的感叹号表示这个属性必须提供。 GraphQL是一个强类型的协议,服务端在定义数据的时候必须指定每一个入参,出参的类型以及是否可选,这样的话,框架就可以自动帮我们去处理参数类型校验的问题了。GraphQL 的类型系统分为标量类型(Scalar Types,标量类型,也称为简单类型)和其他高级数据类型(复合类型),标量类型即可以表示最细粒度数据结构的数据类型,可以和 JavaScript的原始类型对应。 GraphQL 规范目前官方规定支持的标量类型有: Int : 整数,对应JavaScript的Number Float:浮点数,对应JavaScript的Number String:字符串,对应Javascript的String Boolean: 布尔值,对应JavaScript的Boolean ID:序列化后唯一的字符串,对应JavaScript的Symbol 高级数据类型包括:Object(对象)、Interface(接口,定义的时候继承这个接口的结构必须实现这个接口所有属性)、Union(联合类型,可以既是某个类型又是某个类型)、Enum(枚举类型)、Input Object(输入对象)、List(列表,对应js的数组)、Non-Null(不能为null) 这里不做详述,请参考官方指引 Schemas and Types。 查询示例 下面是对上面的graph服务的查询示例: [代码]// 获取单个用户信息的同时获取该位用户的文章列表,以及每篇文章对应的tag列表 { user(id: 1) { # 括号里的是参数,表示请求id为1的用户信息 id username:nickname # 这里可以给服务返回的字段设置别名,比如我们把服务返回的nickname设置为username gender avatar posts{ id source tagIds tags{ name } # tags也是一个数组,我们这里只需要数组里的tag里的name属性 } # posts的返回是一个数组,我们这里只需要定义数组里的项目对应的数据结构就可以 createdAt updatedAt } } //返回示例 { "data": { "user": { "id": "1c5c160c-b666-4728-b745-81c723e3e722", "username": "hello", "gender": "UNKNOW", "avatar": "http://qq.com", "posts": [ { "id": "ab9d26b9-23b8-4ca6-97e7-bb5f9091133e", "source": "黑队7首映归来!全程被卡黄发糖砸到懵逼!好不容易回过神了!被彩蛋砸了一顿玻璃渣!!!编剧我要给你寄刀片!!!!!!!! ", "tagIds": [ "a7475f18-4132-4155-be78-9d942de9fa89", "875d592a-a0f2-483f-b680-93c0db27b173" ], "tags": [ { "name": "任务1" }, { "name": "任务1" } ] }, { "id": "d96f7717-cd20-4ef5-b62e-01fbeb2e3b06", "source": "你好,世界", "tagIds": [ "14698f1e-2610-4d9c-baa0-8a94b3d8597a", "97557a0b-0bc5-48db-9338-3e864aa6b1c5" ], "tags": [ { "name": "任务1" }, { "name": "任务1" } ] } ], "createdAt": "2017-10-18T16:33:27.620Z", "updatedAt": "2017-10-18T16:33:27.620Z" } } } //下面是一个典型的创建文章示例 //需要指定为mutation类型,如果不写的话,默认是query类型 mutation { createPost(input:{source:"hello",sourceType:MARKDOWN,authorId:1,score:1}){ id,# 创建后返回id属性 createdAt # 创建后返回createdAt属性 } } //返回示例 { "data": { "createPost": { "id": "bcb1eafa-6440-4336-a4f0-5cafb334581f", "createdAt": "2017-10-18T16:35:48.297Z" } } } [代码] 除此之外,你还可以进行批量的请求,query类型的请求是并行的,mutation类的请求是顺序执行的。 服务端的数据结构定义 [代码]// 对应上面的GraphQL查询,GraphQL 服务需要建立以下自定义类型 type Query { # 单个用户 #号表示对这个字段的注释,感叹号表示必须传这个字段 user(id:ID!): User # User表示这个的返回类型的User } # 用户类 type User { # 用户uuid id: ID! # 昵称 nickname: String! # 性别 gender:Gender! # 头像地址 avatar:Url! # 注册时间 createdAt:DateTime! # 最后更新时间 updatedAt:DateTime! # 帖子列表 posts:[Post!]! } # 帖子 type Post { id: ID! # 帖子内容 source: String! # 作者ID authorId:ID! # 作者 author:User! # 帖子标签id数组 tagIds:[ID!]! # 帖子标签 tags:[Tag!]! # 创建时间 createdAt:DateTime! # 更新时间 updatedAt:DateTime! } # 标签 type Tag { # 标签uuid id: ID! # 标签名字 name:String! # 作者ID authorId:ID! # 作者 author:User! # 创建时间 createdAt:DateTime! # 最后更新时间 updatedAt:DateTime! } # 性别 enum Gender { # 男 MALE # 女 FEMALE # 未知 UNKNOW } [代码] GraphQL存在的问题 graphQl也不是没有缺点,主要有以下几个缺点: 改造成本 要使用GraphQL对数据源进行管理,相当于要对整个服务端进行一次换血。你需要考虑的不仅仅是需要针对现有数据源建立一套GraphQL的类型系统,同时需要改造服务端暴露数据的方式,这对业务久远的产品无疑是一场灾难,让人望而却步。 查询性能 GraphQL查询的每个字段如果都有自己的resolve方法,可能导致一次查询操作对数据库跑了大量了query,数据库里一趟select+join就能完成的事情在这里看来会产生大量的数据库查询操作,虽然网络层面的请求数被优化了,但是数据库查询可能会成为性能瓶颈。但是这个其实是可以优化,反正就算是用rest api,同时处理多个请求的时候,也总要运行那些数据库语句的。 一些有用的链接: facebook/graphql的github项目 — https://github.com/facebook/graphql GraphQl定义 — https://facebook.github.io/graphql/ GraphQ介绍网站,把graph讲的挺清楚的 http://graphql.org/ 谁在用 http://graphql.org/users/ 参考 http://imweb.io/topic/58499c299be501ba17b10a9e https://juejin.im/post/58fd6d121b69e600589ec740 https://segmentfault.com/a/1190000005766732 http://blog.kazaff.me/2016/01/01/GraphQL什么鬼/ https://neighborhood999.github.io/2017/01/12/Learning-GraphQL/
2019-03-20 - 关于云函数时区的问题
这个问题可能对有些场景不敏感,但是我下面说的场景那是太重要了,那就是签到 由于时区的问题,比如现在是28号,晚上8点,我在29号凌晨签到的时候,由于云函数端采用的是UTC+0 ,所以始终签到的是28号,问题非常重要, 这样就导致始终签到的是28号 [图片] [图片] 官方文档如下 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/notice.html 时区 云函数中的时区为 UTC+0,不是 UTC+8,在云函数中使用时间时需特别注意。如果需要默认 UTC+8,可以配置函数的环境变量,设置 [代码]TZ[代码] 为 [代码]asia/shanghai[代码]。 这种情况就造成了下面这个问题 云开发服务器的nodejs时区是utc+0 小程序本地开发的时区是utc+8 同一段云函数在本地调试和云端调试时表现不一致 关于云函数时区,我看了几个帖子,这里整理下 1、云函数中时区问题 https://developers.weixin.qq.com/community/develop/doc/0002eea7518aa0ea5f39ce7fd56c09 2、云开发,获得的日期怎么能成为北京时间的日期? https://developers.weixin.qq.com/community/develop/doc/000246fdf244305f44397a2e556000 这个帖子里面给出了两个方案,我验证后都没有生效, 3、云开发nodejs环境时区问题 https://developers.weixin.qq.com/community/develop/doc/0008c28e6687d8ddb2b8cf65056400 现在解决了,就是通过上面第三个问题里面的经验,增加环境变量之后,要重新部署云函数,或许要等个半小时。 写在2020-05-25 今天又写这块需求,增加环境变量之后一定要重新部署云函数,然后等个几分钟就好 [图片] 关于UTC不知道是什么的可以先了解下 https://time.is/UTC
2020-05-25 - 公众号的WEB开发者工具,开发者现在已经能自助解绑
官方需求地址:https://developers.weixin.qq.com/community/develop/issue/270 我自己的帖子:https://developers.weixin.qq.com/community/develop/doc/000c861a2683288928ea8df3853400 一、需求背景 WEB开发者工具扫码登录调试时,如果以前绑过较多公众号,会看到较长的一堆公众号授权列表,之前只能让公众号管理员在后台进行解绑,开发者自己是没有任何自助解绑途径的。 二、官方解决方案 经过官方同学的设计调整,目前该需求解决方案已上线。 1,微信打开公众号:公众平台安全助手,查看名下绑定的公众号 注意除了以往的“作为管理员”、“作为长期运营者”,新增了一个“作为开发者”列表。 [图片] 2,点击开发者列表中想要移除调试的公众号,在弹出的窗口点“解除绑定”即可。 3,解除绑定后,系统会向你推送一条解绑通知,同时还会向公众号管理员推送一条解绑通知。 [图片] 4,当你再次打开WEB开发者工具,扫码登录时,会发现公众号授权列表已经没有了刚才解绑的公众号。 这次官方同学还是十分给力的!终于解决了这个长期困扰开发者的问题,感谢!!!
2021-01-26 - 记录微信8.0对抽奖小程序的一点影响
1 小程序中跳转到公众号文章,无法长按识别个人微信二维码?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/000cea03c20e306a6f9b354535ec00 目前已亲测 小程序跳转到公众号文章,长按已不能识别二维码,包括个人微信号,视频号,群二维码,均不可以,公众号二维码是可以长按识别的。 1 [图片] 1 [图片] 1 [图片] 1 [图片] 1 群二维码 1 [图片] 1 [图片] 1 更新与2021-01-29 升级8.0.0版本后长按识别二维码失效?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/0006645873cdb8f2f19bd411851800[图片] ~
2021-01-29 - 抽奖活动小程序体验系列四
抽奖活动小程序体验,本文写于2021-01-13 09:00:00 体验产品:抽奖助手小程序 体验模式:仅限粉丝抽奖,粉丝关注公众号回复关键词,推送抽奖海报,进而完成抽奖 1)抽奖助手小程序体验系列一? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/000a601bbe0a5015498b61c6c56013 2)抽奖助手小程序体验系列二? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/0002808dfc86702c508b91b5056413 3)抽奖活动小程序体验系列三? - 微信开放社区 https://developers.weixin.qq.com/community/develop/article/doc/0006acd8274880a0ae8b4e24256413 ~ 体验的抽奖类型有以下 几种 1)观看激励式视频,然后抽奖「已-2021-01-07」 2)小程序体验15秒后抽奖「已-2021-01-07」 3)加群自动参与抽奖「2021-01-08」 4)抽奖完成引导用户去阅读「2021-01-11」 5)公众号粉丝定向抽奖,关注公众号才可以参与「2021-01-13」 今天体验小程序抽奖模式为公众号粉丝定向参与抽奖 触发路径为 1)点击按钮 2)引导关注公众号 3)进入webview公众号文章 4)关注公众号 5)公众号内回复关键词,收到推送抽奖海报 6)从海报进入抽奖 7)订阅消息 8)抽奖完成 1 [图片] 1 [图片] 1 [图片] 1 [图片] 1 [图片] 1 [图片] 1 [图片] 这个模式主要可以用于公众号加粉的的业务 关于这个模式的技术实现,其实我一直没有搞清楚,抽奖助手和公众号是两个不同的主体,如何判断抽奖用户是否关注供公众号,这一点需要后期的技术探索
2021-01-13 - mp://n2nHxqNukj7mnSv 进入小程序canvas比例错误?
如题,在聊天中发送 mp://n2nHxqNukj7mnSv scheme,点击打开小程序,显示的canvas不正确。 从小程序列表进入是正常的。 请问这是新版微信的bug 还是 mp:// 协议的bug?
2021-01-13 - 云开发之图片压缩裁剪(CloudBase图像处理扩展实战)
1、大约半年前在论坛里寻求云开发后端图片处理方案无果,无奈退而求其次使用小程序端canvas做图片处理: https://developers.weixin.qq.com/community/develop/doc/000c00a3d74758caca2a2b3ef5b400 (寻求方案发帖) 2、canvas做图片处理,代码量比较大,对手机性能要求比较高,而且如果一次处理图片多,还会偶现各种奇怪的不稳定问题。 3、最近iPhone微信更新到7.0.20更是直接不能使用了: https://developers.weixin.qq.com/community/develop/doc/000cc4b48a4378003b7b2f97d51400 (bug反馈发帖) 4、更换图片处理的方案刻不容缓,上次云开发峰会上陈宇明大佬分享案例中提到一嘴CloudBase的相关支持,于是翻到了相关文章,一步步跟着操作,在此感谢大佬指路: https://developers.weixin.qq.com/community/develop/article/doc/0004ec150708d0b57d5bd532a53413 (大佬文章) https://cloud.tencent.com/document/product/876/42103 (开发指南) 5、本人电商项目中有多处图片处理需求,比较典型的一个业务是上传商品主图,当用户任意上传一个图片后,自动居中裁剪生成一大一小两张正方形的图,大的用在详情页,小的用在列表页。CloudBase支持两种方式:获取图片时处理、持久化图像处理。本人业务采用后者。 6、代码示例: 小程序端选择图片,上传到云存储 wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: res => { const tempFilePaths = res.tempFilePaths const tempFile = tempFilePaths[0] let pictureLarge = tempFile let fileName = pictureLarge.split('.') let format = fileName[fileName.length -1] let cloudPath = 'products/sellerId/original-' + (new Date()).valueOf() + (format.length < 5 ? '.' + format : '') wx.cloud.uploadFile({ cloudPath: cloudPath, filePath: pictureLarge, success: res => { const pictureOrignial = res.fileID wx.cloud.callFunction({ name: 'addProduct', data: { operation: 'addPicture', pictureOrignial, cloudPath } }).then(res => { if (res.result.errCode) { wx.showModal({ title: '主图处理失败', content: res.result.errMsg, showCancel: false, confirmColor: '#67ACEB' }) } else { //拿到云文件ID做后续处理 res.result.picture } }).catch(err => { console.error(err) wx.showModal({ title: '主图处理失败', content: '主图处理失败,请重试', showCancel: false, confirmColor: '#67ACEB' }) }) }, fail: err => { console.error(err) wx.showModal({ title: '主图上传失败', content: '主图上传失败,请重试', showCancel: false, confirmColor: '#67ACEB' }) } }) } }) 云函数端处理图片,先放大到最小边大于1125px,再分别裁剪出1125px和258px的两张图,存到同一目录下,返回云文件ID 先安装包: npm install --save @cloudbase/extension-ci@latest 云函数: // 云函数入口文件 const cloud = require('wx-server-sdk') const extCi = require("@cloudbase/extension-ci") const tcb = require("tcb-admin-node") cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) tcb.init({ env: cloud.DYNAMIC_CURRENT_ENV }) tcb.registerExtension(extCi) // 云函数入口函数 exports.main = async (event) => { const wxContext = cloud.getWXContext() if (event.operation == 'addPicture') { return await addPicture(event.pictureOrignial, event.cloudPath) } else { } } async function addPicture(pictureOrignial, cloudPath) { //process picture const res = await process(cloudPath) if (res.errCode !== 0) { return { errCode: 100, errMsg: '商品主图处理失败' } } else { const pictureIDLarge = pictureOrignial.replace(/original/, 'large') const pictureID = pictureOrignial.replace(/original/, 'normal') return { errCode: 0, picture: { pictureIDLarge, pictureID } } } } async function process(cloudPath) { try { const opts = { //scale to 1125 rules: [ { fileid: '/' + cloudPath, // 处理结果的文件路径,如以’/’开头,则存入指定文件夹中,否则,存入原图文件存储的同目录 rule: "imageMogr2/thumbnail/!1125x1125r" // 处理样式参数,与下载时处理图像在url拼接的参数一致 } ] } await tcb.invokeExtension("CloudInfinite", { action: "ImageProcess", cloudPath: cloudPath, // 图像在云存储中的路径,与tcb.uploadFile中一致 operations: opts }) } catch (err) { return JSON.stringify(err, null, 4) } try { const opts = { rules: [ //crop large { fileid: '/' + cloudPath.replace(/original/, 'large'), rule: "imageView2/1/w/1125/h/1125/q/85" }, //crop normal { fileid: '/' + cloudPath.replace(/original/, 'normal'), rule: "imageView2/1/w/258/h/258/q/85" } ] } await tcb.invokeExtension("CloudInfinite", { action: "ImageProcess", cloudPath: cloudPath, // 图像在云存储中的路径,与tcb.uploadFile中一致 operations: opts }) } catch (err) { return JSON.stringify(err, null, 4) } return { "errCode": 0, "errMsg": "ok" } }
2022-04-26 - 获取图片exif元数据信息
官方api并没有关于图片exif信息提取的api,搜索各种资料无果 不过发现了一个js版本的exif.js 直接拿到小程序上并不能使用,经过修改现在可以使用。并没有太多的测试,如果有发现bug希望共同维护一下 直到官方出了可以获取exif的api 使用方法: 1,引入js var myexif = require('../../libs/myexif.js'); 2,调用handleBinaryFile()方法(方法参数是bufferArray) wx.chooseImage({//选择图片 sizeType: ['compressed'],//图片不能经过压缩处理 success(res) { var array = wx.getFileSystemManager().readFileSync(res.tempFilePaths[0] );; var r =myexif.handleBinaryFile(array); console.log(r); } }); 百度网盘(myexif.js): 链接: https://pan.baidu.com/s/1Z84kyPHowXlgyg1czKyu7A 提取码: we9d
2018-11-26 - 只有三行代码的神奇云函数的功能之三:100%成功获取unionid
这是一个神奇的网站,哦不,神奇的云函数,它只有三行代码:(真的只有三行哦) 云函数:login index.js: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event) => { return { ...event, ...cloud.getWXContext() } } 神奇功能之三:100%成功获取unionid: 保证100%成功获取unionid,需要用户信息授权。 强调一下:这个100%是指必须绑定了开放平台,那么不管用户是什么情况,不管有没有关注公众号,一定100%能获取到unionid。 依然需要符合unionid机制:第1条。 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html js: getUserInfo: function (e) { app.globalData.userInfo = e.detail.userInfo if (!app.globalData.unionid ) { wx.cloud.callFunction({ name: 'login', data: { weRunData: wx.cloud.CloudID(e.detail.cloudID) } }).then(res => { app.globalData.unionid = res.result.weRunData.data.unionId }) } }, 其他功能: 神奇功能之四:获取电话号码: 还是这三行代码,获取用户的电话号码。 https://developers.weixin.qq.com/community/develop/article/doc/0006a8ec7ac860c94bf90a34f5d813 神奇功能之五:获取群id: 将小程序分享到某群里,可获得该群的群id, https://developers.weixin.qq.com/community/develop/article/doc/000ea802c00f70894cf9fe72556013 神奇功能之一:获取openid: https://developers.weixin.qq.com/community/develop/article/doc/00080c6e3746d8a940f9b43e55fc13 神奇功能之二:不用授权获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/000a0c6b580338e947f9db0c65b813 [图片]
2020-10-25 - 排队叫号微信小程序-【请排队】的构思和实现
构思背景:2020年9月份,我家小孩儿幼儿园开学,需要到县防疫站接种疫苗,现场人多拥挤,一片混乱,工作台上堆满了乱七八糟的疫苗接种手册,工作人员扯着脖子叫人办理业务,突发奇想:如果有一个排队叫号软件,就能更方便的解决这个排队的难题 场景分析:在生活中,排队叫号经常可以见到,特别是银行办理业务的场景,餐厅排队就餐的场景等等,但是他们的叫号软件都有一些缺点: 1:必须到现场才可以排队 2:必须配合相关的硬件:取号机,叫号设备等 3:需要单独购买,出现故障维修麻烦 4:需要现场调试安装 … 在当今这个万物互联的时代,上边的排队叫号方式显然已经落伍了,我们能不能开发一个产品,使用手机就可以轻松高效的管理排队呢?答案是:必须可以 技术需求分析:1、排队管理员端+排队客户端的项目结构 2、任何人都是管理员或排队成员的用户结构 3、语音播报+微信小程序服务通知+短信通知+手机拨号的叫号方式 4、使用ThinkPhp5作为项目的服务端开发语言 5、使用微信小程序作为项目的客户端 场景业务流程分析:1、用户切换成管理员身份 2、管理员创建排队,并进行相关设置(时间,限制,人数,白名单等) 3、管理员将排队小程序码打印并张贴在显眼位置,提示客户扫码排队 4、客户使用微信扫描排队小程序码,完成排队领号 5、管理员叫号,语音播报 6、用户手机接收到:语音提示+震动+服务通知或短信通知 7、用户进场,管理员设置用户为已进场状态 8、办理完成后,对下一位排队者叫号 … 项目成品展示(微信搜索:请排队)[图片][图片][图片][图片][图片][图片][图片][图片]
2020-11-13 - 你好,我的公众号是已经通过微信认证的订阅号,无法插入连接?
你好,我的公众号是已经通过微信认证的订阅号,还是提示“请输入公众号相关链接,并以http://或https://开头” [图片][图片] [图片]
2020-11-09 - 小游戏资质提交审核指引Q&A
小游戏资质提交审核指引 温馨提醒:微信公众平台的账号名称是该账号的品牌表现,小游戏账号命名需符合平台规范。如需提前了解,或查询你的小游戏未符合平台规范 原因,可点击此处查阅平台命名规范 一、资质提交入口: 1.微信公众平台 -> 首页 -> 小程序发布流程 -> 小程序备案 -> 选择游戏情况 -> 资质提交(首次提交资质材料) [图片] 2.微信公众平台 -> 首页 -> 菜单栏左下角 -> 账号设置 -> 游戏设置 -> 资质管理 -> 前往管理(首次or后续更新资质材料)[图片] 二、主体迁移:微信公众平台 -> 首页 -> 菜单栏左下角 -> 账号设置 -> 基本设置 -> 主体信息 -> 小程序主体变更 -> 提交资质审核申请 [图片] 注:审核提交信息后预计1-2个工作日内处理完毕,审核请留意微信公众平台通知中心的通知。 说明:游戏资质文档规范及示例 一、各资质文档基本要求: (1)所有游戏需提交:《游戏自审自查报告》、《著作权自我声明》、《代备案授权书》 文档规范要求:上传文件,非个人主体请务必加盖清晰规范账号主体公章,个人主体请在落款处清晰规范签署个人签名并捺指印 查看示例: 《游戏自审自查报告》、《著作权自我声明-非个人主体》、《著作权自我声明-个人主体》、《代备案授权书-非个人主体》、《代备案授权书-个人主体》 (2)选填材料一个《计算机软件著作权登记证书》 文档规范要求:上传【原件或加盖著作权人鲜章的复印件】之扫描件,复印件务必清晰规范加盖著作权人公章或著作权人签名 查看示例:《计算机软件著作权登记证书》 注:游戏名称含有“软件”、“人名”、“英文”,《计算机软件著作权登记证书》为必填材料。 (3)牌类游戏需提交:《计算机软件著作权登记证书》、《网络游戏出版物号核发单》、《产品合规报告》、《产品运营报告》、《内容一致承诺函》、《关于使用微信支付服务的合法性承诺书》、《棋牌类网络游戏经营自查报告》、《游戏自审自查报告》、《著作权自我声明》、《代备案授权书》 文档规范要求:上传【原件或复印件】之扫描件,复印件非个人主体请务必加盖清晰规范账号主体公章,个人主体请清晰规范签署个人签名 查看示例:《计算机软件著作权登记证书》、《网络游戏出版物号核发单》、《产品合规报告》、《产品运营报告》、《内容一致承诺函》、《关于使用微信支付服务的合法性承诺书》、《棋牌类网络游戏经营自查报告》、《游戏自审自查报告》、《著作权自我声明-非个人主体》、《代备案授权书-非个人主体》 (4)开通虚拟支付游戏需提交:《网络游戏出版物号核发单》、《计算机软件著作权登记证书》、《游戏自审自查报告》、《内容一致承诺函》、《著作权自我声明》、《代备案授权书》 《网络游戏出版物号核发单》文档规范要求: 上传【原件】之扫描件 查看示例:《网络游戏出版物号核发单》 《计算机软件著作权登记证书》文档规范要求: 上传【原件或加盖著作权人鲜章的复印件】之扫描件,复印件务必清晰规范加盖著作权人公章或著作权人签名 查看示例:《计算机软件著作权登记证书》 《游戏自审自查报告》文档规范要求: 上传文件,请务必加盖清晰规范账号主体公章 查看示例: 《游戏自审自查报告》 《内容一致承诺函》文档规范要求: 上传文件,请务必加盖清晰规范账号主体公章 查看示例:《内容一致承诺函》 《著作权自我声明》文档规范要求: 上传文件,非个人主体请务必加盖清晰规范账号主体公章,个人主体请在落款处清晰规范签署个人签名并捺指印 查看示例:《著作权自我声明-非个人主体》、《著作权自我声明-个人主体》 《代备案授权书》文档规范要求: 上传文件,非个人主体请务必加盖清晰规范账号主体公章,个人主体请在落款处清晰规范签署个人签名并捺指印 查看示例:《代备案授权书-非个人主体》、《代备案授权书-个人主体》 (5)文化互动类目需提交:2个,《商标注册证》、《游戏自审自查报告》 《商标注册证》文档规范要求: 文档规范要求:上传【原件或复印件】之扫描件,复印件请务必加盖清晰规范商标注册人公章,商标注册人为个人请清晰规范签署个人签名 查看示例:《商标注册证》 《游戏自审自查报告》文档规范要求: 上传文件,非个人主体请务必加盖清晰规范账号主体公章,个人主体请在落款处清晰规范签署个人签名 查看示例: 《游戏自审自查报告》 (6)各个文件的格式、数量和大小要求 文件格式: 《网络游戏出版物号核发单》及《网络游戏出版物号核发单》授权书格式为 JPEG、JPG 《计算机软件著作权登记证书》文件为 JPEG、JPG、PNG、BMP、GIF、PDF 《计算机软件著作权登记证书》授权书为 JPEG、JPG、PNG、BMP 《游戏自审自查报告》文件为 JPEG、JPG、PNG、BMP、GIF 《内容一致承诺函》文件为 JPEG、JPG、PNG、BMP、GIF 《关于使用微信支付服务的合法性承诺书》文件为 JPEG、JPG、PNG、BMP 《棋牌类网络游戏经营自查报告》文件为 JPEG、JPG、PNG、BMP 《著作权自我声明》文件为 JPEG、JPG、PNG、BMP 《代备案授权书》文件为 JPEG、JPG、PNG、BMP 《产品合规报告》文件为 PDF 《产品运营报告》文件为 PDF 文件大小: 《网络游戏出版物号核发单》及《网络游戏出版物号核发单》授权书单个文件大小:≤ 200KB 《计算机软件著作权登记证书》及《计算机软件著作权登记证书》授权书单个文件大小:≤ 2000KB 《产品合规报告》、《产品运营报告》、《游戏自审自查报告》和《内容一致承诺函》大小不超过5M 《软著自我声明》、《代备案授权书》、《关于使用微信支付服务的合法性承诺书》和《棋牌类网络游戏经营自查报告》文件大小不超过2M 文件数量: 《网络游戏出版物号核发单》文件限制最多1张(多于1张请按顺序自行合并) 《网络游戏出版物号核发单》授权书限制最多2张(多于2张请按顺序自行合并) 《计算机软件著作权登记证书》限制最多1张(多于1张请按顺序自行合并) 《计算机软件著作权登记证书》授权书限制最多2张(多于2张请按顺序自行合并) 二、关于《计算机软件著作权登记证书》: (1)《计算机软件著作权登记证书》著作权人:提审软著证书的著作权人需与版号著作权人保持一致 《计算机软件著作权登记证书》著作权人说明示例图: [图片] (2)《计算机软件著作权登记证书》颁发日期:《计算机软件著作权登记证书》的颁发日期需在版号证书颁发日期前 《计算机软件著作权登记证书》颁发日期说明示例图: [图片] (备注:《计算机软件著作权登记证书》颁发日期为2015年5月6日,《网络游戏出版物号核发单》颁发日期为2016年11月8日) 三、关于各资质文档刊载游戏名称:各资质文件中的游戏名称,需与提审游戏名称保持完全一致 游戏名称一致说明示例图: [图片] 四、关于授权书: (1)提交入口:登录微信公众平台(mp.weixin.qq.com),进入公众平台菜单栏左下角->账号设置->游戏设置->资质管理->前往管理-> 在《授权书》入口上传相关授权书,该入口支持多个文档上传 [图片] (2)应用场景: 请针对小游戏资质软著授权、小游戏品牌合作等素材露出、使用他人的商标或IP等场景上传相关授权书及授权人的权利证明,包括但不限于著作权授权(不限于美术作品著作权、文字作品著作权、音乐作品著作权、计算机软件著作权);版号、备案信息中的运营单位授权他人发布小游戏;使用文学作品或影视作品名称或内容的小游戏;使用他人商标的小游戏;品牌联合推广等。 示例文档:《著作权授权书》、《运营授权书》、《商标授权书》 特别说明:若《网络游戏出版物号核发单》运营单位、《计算机软件著作权登记证书》著作权人与提审主体不一致,即《网络游戏出版物号核发单》运营单位委托他人发布小游戏的,请在《授权书》入口上传《计算机软件著作权登记证书》著作权人、《网络游戏出版物号核发单》运营单位、向提审主体出具的多份运营授权书。授权链条示例拆解,详见以下图表: [图片] [图片] 五、规范要求: (一)单份授权书 (1)明确标明授权人与被授权人准确名称 (2)写明授权作品的基本信息(软件名称及简写、版本号,计算机软件著作权登记证书登记号) (3)明确标明授权范围和期限 (4)授权方的盖公章或亲笔签名(个人) (5)授权书资质文件不得超过有效期 (二)多份授权书 (1)前后几份授权书之间需要有连续性 (2)在后授权书的授权范围不能超过在先授权书 (3)在后授权书的授权时间不能超过在先授权书 (4)注明是否有转授权的权利 文档规范要求:上传文件,务必加盖公章授权方盖公章或亲笔签名(个人) 查看示例:《著作权授权书》、《运营授权书》、《商标授权书》 六、其他易遗漏事项温馨提醒 (1)清晰:小游戏资质文件关键信息需清晰可见 (2)公章/签名:各资质文档需加盖公章或亲笔签名 附:如果你还不了解如何提审版本,请点击此处查看版本提审指引
11-01 - 针对新手很容易出现理解误区的微信小程序订阅消息模块
1. 写在前面 微信小程序下架了模板消息功能,取而代之的是订阅消息功能。这个订阅消息目前又分为「一次性订阅」和「永久订阅」。使用订阅消息也有一段时间了,感觉对新手订阅消息很容易让新开发者进入一个理解的误区,这里觉得有必要说出来 2. 理解误区 很多新手认为,只要用户勾选了小程序端订阅消息弹出时底部的「总是保持以上选择…」后,就可以「为所欲为」的不限次数的推送订阅消息给用户了。如下图: [图片] 3. 正确理解 如果你使用的「一次性订阅」模板(目前发现绝大多数开发者都是只能用一次性的,因为永久性的订阅消息申请门槛太高),那么勾选底部的「总是…」这个并不代表以后可以直接推送了。官方原话wx.requestSubscribeMessage的介绍里是这样写的: 3.1 官方说明 wx.requestSubscribeMessage(Object object) 基础库 2.8.2 开始支持,低版本需做兼容处理。 调起客户端小程序订阅消息界面,返回用户订阅消息的操作结果。当用户勾选了订阅面板中的“总是保持以上选择,不再询问”时,模板消息会被添加到用户的小程序设置页,通过 wx.getSetting 接口可获取用户对相关模板消息的订阅状态。 注意事项 一次性模板 id 和永久模板 id 不可同时使用。 低版本基础库2.4.4~2.8.3 已支持订阅消息接口调用,仅支持传入一个一次性 tmplId / 永久 tmplId。 2.8.2 版本开始,用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。 2.10.0 版本开始,开发版和体验版小程序将禁止使用模板消息 fomrId。 3.2 重点关注 这里重点关注第7条:「用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。」这就意味着你需要在用户主动点击某个组件是触发调用wx.requestSubscribeMessage方法再次订阅,订阅后,你才可以「为所欲为」推送一次模板消息,注意只能一次。下次再想推送时,需要用户再次点击触发wx.requestSubscribeMessage。 4. 破局方案 目前订阅消息功能,就是这么个情况,所以针对这个情况的替代方案有以下 4.1 永久性订阅消息 如果能达到申请「永久性订阅」消息的模板的门槛,那自然是极好的,直接用永久性模板「为所欲为」。 4.2 使用服务号的模板消息替代 比较常用的是使用公众号服务号的模板消息代替小程序的订阅消息功能,公众号的模板消息功能限制就比订阅号好多了,基本上可以「为所欲为」的推送。但是这个方案有个致命的运营成本:必须要用户关注公众号,还有小程序要跟公众号同一主体并绑定在开放平台下。同时开发成本有所增加,要采用unionId机制来打通小程序跟公众号的openId。这个具体的实现方案,大家有兴趣的话可以讨论下。笔者目前就是用这种方案的。 5. 几个注意点 5.1 官方提示 订阅消息如果选择选择‘总是保持以上选择,"不再询问"后的设置问题: 目前是选择‘总是保持以上选择,"不再询问"后,可以在设置中开启或拒绝接收,但不会再次拉起授权弹窗 6. 长期性订阅消息 请参考官方最新文档: 小程序模板消息能力调整通知 | 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/00008a8a7d8310b6bf4975b635a401 长期性订阅消息 一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。 目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。 7.题外话 鉴于被戴上各种「刷赞,冲级,让社区点赞“通货膨胀”」等等一些恶毒字眼(最近多了个职业回复的「雅称」),各种帽子戴得,做一个开发爱好者积极分享和解决各种问题太难了,姑且不论咱写一篇文章需要截图多少,单单排版就得废掉俺多少时间哈,很受伤,所以本人决定在微信开放者社区封笔。你看到是俺最后一篇发表在微信开放社区的文章。如果你想继续查看俺的一些文章可以私聊我。我会在其他平台保持继续创作。bye-bye~ 8. 最最重要的来了 看完后觉得有用记得点赞~~ ↓点赞处↓
2020-09-04 - 【笔记】云开发聚合实现分页,涉及跨表查询、逻辑计算、判断权限、数据格式化、限制输出
背景: 之前不会用聚合,因此把数据库结构分为了用户表、帖子表、喜欢表。小程序端请求一次列表,要根据帖子列表,循环查询用户表,并且还要做一系列的逻辑运算处理,计算当前帖子的权限、是否喜欢过、喜欢人数、是否有这个帖子管理权限等信息。 这样做有很多弊端: 处理速度慢,资源耗费严重,循环查询肯定慢且耗费资源,一个列表需要21次查询。需要写大量逻辑处理代码,如计算管理权限,喜欢数量、当前用户是否喜欢,格式处理等等。于是使用聚合进行了优化: 跨表查询数据格式化逻辑计算,权限判断、是否喜欢等数据统计,喜欢总人数权限判断,是否为管理员限制输出效果: 之前:上百行代码,多次查询,需要单独判断函数,处理时间在3000ms以上之后:几行代码,一次查询,直接查询时算出结果,处理时间在300ms以内 数据库结构 [图片] 代码实现: const { OPENID } = cloud.getWXContext(context) //构建查询条件 let query = null switch (Number(event.listType)) { case 0: query = db.collection('post').aggregate() .match({ //0我的 '_openid': OPENID }) .sort({ createTime: -1 }) .skip(20 * (event.pageNum - 1)) .limit(20) break; case 1: //1 随机 query = db.collection('post').aggregate() .match({ public: true, // feeling: _.gte(50) }) .sample({ size: 20 }) break; case 2: query = db.collection('post').aggregate() .match({ //2喜欢 likes: _.all([OPENID]) }) .sort({ createTime: -1 }) .skip(20 * (event.pageNum - 1)) .limit(20) break; case 4: query = db.collection('post').aggregate() .match({ //4指定 _id: event.id }) .sort({ createTime: -1 }) .skip(20 * (event.pageNum - 1)) .limit(20) break; } //使用聚合处理后续数据 let listData = await query .lookup({ from: "user", localField: "_openid", foreignField: "_id", as: "postList" })//联表查询用户表 .replaceRoot({ newRoot: $.mergeObjects([$.arrayElemAt(['$postList', 0]), '$$ROOT']) })//将用户表输出到根节点 .addFields({ day: $.dayOfMonth('$createTime'), month: $.month('$createTime'), year: $.year('$createTime'), isLike: $.in([OPENID, '$likes']), //是否喜欢 isLiked: $.in([OPENID, '$liked']), //是否喜欢过 isAdmin: $.eq([OPENID, 'oy0T-4yk7lCRFGDefpFC4Yvx_ppU']),//是否管理员 isAuthor: $.eq(['$_openid', OPENID]),//是否为作者 like: $.size('$likes'), //喜欢该帖子数 face: $.switch({ branches: [ { case: $.gte(['$feeling', 90]), then: 9 }, { case: $.gte(['$feeling', 80]), then: 8 }, { case: $.gte(['$feeling', 70]), then: 7 }, { case: $.gte(['$feeling', 60]), then: 6 }, { case: $.gte(['$feeling', 50]), then: 5 }, { case: $.gte(['$feeling', 40]), then: 4 }, { case: $.gte(['$feeling', 30]), then: 3 }, { case: $.gte(['$feeling', 20]), then: 2 }, { case: $.gte(['$feeling', 10]), then: 1 } ], default: 0 }) //根据心情值判断对应表情 }) .project({ postList: 0, userInfo: 0, liked: 0, likes: 0, city: 0, province: 0, country: 0, language: 0, nlp: 0, saveType: 0, }) //清楚掉不需要的数据 .end() return listData
2020-05-26 - 云函数端Aggregate聚合操作limit默认20条限制
云函数端 聚合 Aggregate 经过以下代码验证: [代码]// test_record 集合中 openid 为 test user openid 的实际上有超过20条数据的 db .collection('test_record') .aggregate() .match({ _openid: 'test user openid', }) .end(); [代码] 最后实际只返回了20条数据,所以说在云函数端如果某些接口返回确定以及肯定超过20条的话,还是老老实实加上 [代码].limit(100)[代码] ‘保命’吧。 改成如下代码即可超过默认20条的限制: [代码]db .collection('test_record') .aggregate() .match({ _openid: 'test user openid', }) .limit(100) // important .end(); [代码] 上限可以达到1万条数据 经过 stop eating 同学点拨,原来可以直接淦到10000 参考文档 Collection.limit(value: number): Collection [图片] 获取一个集合的数据 [图片]
2020-07-23 - 实战分享: 小程序云开发玩转订阅消息(二)
[图片]这是实战分享: 小程序云开发玩转订阅消息的第二部分 第一部分链接 《实战分享: 小程序云开发玩转订阅消息(一)》 将订阅消息存入云开发数据库接下来我们创建一个云函数 [代码]subscribe[代码] ,这个云函数的作用是将用户的订阅信息存入云开发数据库的集合 [代码]messages[代码] 中,等待将来需要通知用户时进行调用。 在微信开发者工具的云开发面板中创建数据库集合 [代码]messages[代码] [图片]微信开发者工具新增数据库集合 创建一个 [代码]subscribe[代码] 云函数,在云函数中我们将小程序端发送过来的课程订阅信息,存储在云开发数据库集合中,开发完成后,在微信开发者工具中右键上传并部署云函数。 cloudfunctions/subscribe/index.js [代码]const cloud = require('wx-server-sdk'); cloud.init(); const db = cloud.database(); exports.main = async (event, context) => { try { const {OPENID} = cloud.getWXContext(); // 在云开发数据库中存储用户订阅的课程 const result = await db.collection('messages').add({ data: { touser: OPENID, // 订阅者的openid page: 'index', // 订阅消息卡片点击后会打开小程序的哪个页面 data: event.data, // 订阅消息的数据 templateId: event.templateId, // 订阅消息模板ID done: false, // 消息发送状态设置为 false }, }); return result; } catch (err) { console.log(err); return err; } }; [代码]利用定时触发器来定期发送订阅消息接下来我们需要实现一个定时执行的云函数[代码]send[代码],来检查数据库中是否有需要发送给用户的订阅消息。如果有需要发送的订阅消息,会通过云调用 [代码]cloud.openapi.subscribeMessage.send[代码] 将订阅消息发送给用户。 创建一个名叫 [代码]send[代码] 的云函数,首先要配置云函数,在 [代码]config.json[代码] 的 [代码]permissions[代码] 中新增 [代码]subscribeMessage.send[代码]的云调用权限,然后新增一个 [代码]sendMessagerTimer[代码] 的定时触发器,定时触发器的语法和 [代码]linux[代码] 的 [代码]crontab[代码] 类似,比如,我们配置的 [代码]"0 * * * * * *"[代码] 代表每分钟执行一次云函数。 cloudfunctions/send/config.json [代码]{ "permissions": { "openapi": ["subscribeMessage.send"] }, "triggers": [ { "name": "sendMessagerTimer", "type": "timer", "config": "0 * * * * * *" } ] } [代码]接下来是实现发送订阅消息的云函数,这个云函数会从云开发数据库集合[代码]messages[代码]中查询等待发送的消息列表,检查数据库中是否有需要发送给用户的订阅消息,发送条件可以根据自己的业务实现,比如开课提醒可以根据课程开课日期来检查是否需要发送订阅消息,在我们下面的代码示例里做了简化,筛选条件只检查了状态为未发送。 查询到待发送的消息列表之后,我们会循环消息列表,依次发送每条订阅消息,发送成功后将数据库中消息的状态改为已发送。 cloudfunctions/send/index.js [代码]const cloud = require('wx-server-sdk'); exports.main = async (event, context) => { cloud.init(); const db = cloud.database(); try { // 从云开发数据库中查询等待发送的消息列表 const messages = await db .collection('messages') // 查询条件这里做了简化,只查找了状态为未发送的消息 // 在真正的生产环境,可以根据开课日期等条件筛选应该发送哪些消息 .where({ done: false, }) .get(); // 循环消息列表 const sendPromises = messages.data.map(async message => { try { // 发送订阅消息 await cloud.openapi.subscribeMessage.send({ touser: message.touser, page: message.page, data: message.data, templateId: message.templateId, }); // 发送成功后将消息的状态改为已发送 return db .collection('messages') .doc(message._id) .update({ data: { done: true, }, }); } catch (e) { return e; } }); return Promise.all(sendPromises); } catch (err) { console.log(err); return err; } }; [代码]最终效果 [图片]开课提醒订阅消息截图 源代码https://github.com/binggg/tcb-subscribe-demo[3] 参考资料 [1]注册小程序帐号: https://tencentcloudbase.github.io/2019-09-03-wx-dev-guide-register/ [2]开通云开发服务: https://tencentcloudbase.github.io/2019-09-03-wx-dev-guide-service/ [3]https://github.com/binggg/tcb-subscribe-demo: https://github.com/binggg/tcb-subscribe-demo
2019-10-23 - [开盖即食]小程序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生成,支持中文的输入。可在原生小程序,mpvue,taro中使用。(文末有一个使用示例)[图片] github地址(wxmp-qrcode)[https://github.com/Z-HNAN/wxmp-qrcode] 安装 [代码]npm install wxmp-qrcode [代码] 使用 创建一个canvas,设置其[代码]id[代码],与[代码]canvas-id[代码], 并设置canvas的样式,二维码基于其大小生成并居中 [代码]<canvas id="cav-qrcode" canvas-id="cav-qrcode"></canvas> [代码] 引入包并使用 [代码]import QR from 'wxmp-qrcode' QR.draw(str, canvasId, () => { console.log('draw success') } ) [代码] api [代码]/** * 根据canvas尺寸,画出合适居中的qrcode * @param {Object} str 二维码的内容 (必须) * @param {Object} canvasId canvasId的值 (必须) * @param {Object} $this 传入组件的this,兼容在组件中生成二维码 (可选,可省略该参数) * @param {Object} callback 回调函数 (可选) */ draw: function (str, canvasId, $this, callback) /** * 清除canvas内容 * @param {Object} canvasId canvasId (必须) * @param {Object} callback 回调函数 (可选) */ clear: function (canvasId, callback) [代码] 注意 canvas中 id, canvas-id必须保持一致 id 获取canvas节点,自动计算大小使用, 二维码大小基于canvas自动生成 canvas-id 绘制二维码使用 如果在组件中使用,需要传入组件的this,[代码]draw(str, canvasId, componentThis)[代码] 具体参见 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/wx.createCanvasContext.html 可以保存二维码为临时图片地址 具体可参见 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/wx.canvasToTempFilePath.html bug: 该方法有时保存的图片会有一个竖条。 [代码]createQrCode: function (content, canvasId) { QR.draw(content, canvasId) this.canvasToTempImage(canvasId) }, //获取临时缓存图片路径 canvasToTempImage: function (canvasId) { wx.canvasToTempFilePath({ canvasId, success: function (res) { let tempFilePath = res.tempFilePath; // 临时图片地址,可在放入图片src中使用 } }) } [代码] 原生小程序wxmp中使用 在项目设置中选择 [代码]使用npm模块[代码] [图片] 如果第一次使用npm模块,需要首先在根目录中[代码]npm init[代码], 之后再安装模块 [代码]npm i wxmp-qrcode[代码] 在工具中选择 [代码]构建npm[代码] [图片] index.wxml [代码]<view class="container"> <canvas id="{{canvasId}}" canvas-id="{{canvasId}}"></canvas> <button bindtap="creatQRCode"> 生成二维码 </button> </view> [代码] index.wxss [代码]canvas { border: 1rpx solid #eee; width: 400rpx; height: 400rpx; } button { margin-top: 100rpx; } [代码] index.js [代码]import QR from './qrcode' Page({ data: { canvasId: 'canvasId', QRdata: '你好 wxmp-qrcode' }, creatQRCode () { let str = this.data.QRdata let canvasId = this.data.canvasId QR.draw(str, canvasId) } }) [代码]
2019-09-01 - 内容安全检测图片API:openapi.security.imgSecCheck完美解决方案。
背景需求: 我个人做了一款小程序的小游戏,本质是小程序。里面有个自定义图片的功能。用户从本地相册选一张图片进行裁剪,之后保存到缓存中或者上传到服务器。然后用户再用这张图片作为素材进行其它操作。这里就涉及到内容安全了,提交审核没有通过也是因为这个没有做内容安全。防止一些色情低俗的事情发生。 正文: 思路:相册选图片 --> 裁剪小的图片 --> 内容安全检测 --> 通过 --> 裁剪大的图片 --> 保存。 失败的原因:绝大多数是因为检测图片不能大于1M,而导致超时,或者是errCode:-1,又或者是其它问题。 [图片] [图片] 核心代码图片: [代码]默认裁剪小尺寸图片 (我的业务需求是正方形图片,也可动态计算宽高比例) [代码] [图片] 检测图片 部分iOS不兼容encoding: ‘ucs2’。注释掉就好了 [图片] [图片] 云函数 [图片] 测试情况: 正常图片不含违法违规,测试20次,全部通过。小程序上线后暂无发现检测失败情况。百度搜索的“人体油画”等等均可通过。 PS:第一次写经验分享哈,看不懂可以问我。体验一下我的小程序想问我这个小程序其它的功能点也可以喔! 技术会迭代更新,用到的技术会有时效性,看编辑时间,可能当时的技术现在不适用了
2020-10-22 - [填坑手册]小程序新版订阅消息+云开发实战与跳坑
[图片] 老版本的订阅消息在2020年1月10日就下线了,相信不少人在接入新版本订阅系统的时候,或多或少会遇到一些问题,这里智库君跟大家介绍下新版订阅的机制和不需要node/后端的情况下 独立完成功能开发。 一、新版订阅的机制 其实开发过程不难,但是要理清楚它里面的机制,智库君还是花了一些时间的,也踩了不少坑 先来看下官方介绍: [图片] 可以设置多个订阅选项 感叹号里面可以看到详情 有个默认不被选中的“总是”选项 这些就是新不同的地方,智库君在开发的时候也有很多疑问,点了“总是”再点“取消”按钮会怎样?部分选择订阅会怎样?下面为大家一一梳理 (1)部分选中 [图片] 比如现在有三个选项 A,B,C,用户**“部分选中”**返回的情况: [图片] 这里用真机调试可以看到,有个返回值状态为“reject”。 如果我们反复几点点击同一个订阅后,这些值是如何计算的呢? 举例: [图片] 从这里看出,微信系统会自动记录用户点击的次数,并且做累加记录,如果用户只允许2次发送,而开发者发送了3次,最后一次将会被拒绝。 (2)点击“总是保持以上选择,不再询问”的情况 [图片] 当用户点击“总是”之后,同一个类型的订阅将不再弹出,那如果有多个订阅选项呢? 举例 订阅AAA 三个订阅模板为 X Y Z 订阅BBB 二个订阅模板为 Y W 这时候如果“订阅AAA”按钮选择了“总是”,那么再点击“订阅BBB”按钮,将只会弹出一个选项“W”,不会有 “Y” 的模板,因为在之前 “订阅AAA” 按钮中已经包含了。 [代码]wx.requestSubscribeMessage({ tmplIds: ["MECDDOdhbC3SrQmMY5XrfqiIGbMTzpEN8Z7ScXJfcd0", "iSb2NIlNnnO60wlI-8Wx5Pe82jR7TRdwjotSXtM1-ww"], success(res) { console.log(res); } }) [代码] 显示内容仅一个选项: [图片] 这里需要注意,“总是”选项是全局有效,不区分页面,选中“总是”的 W,X,Y,Z的模板,在全局任意页面中再次调用,再次调用将不再会显示! [图片] 返回值无提示用户是否选中“总是”。 (3)用户点击“总是”后,获取状态 [图片] [代码]wx.getSetting({ withSubscriptions: true, success(res) { console.log(res.authSetting) // res.authSetting = { // "scope.userInfo": true, // "scope.subscribeMessage": true // } console.log(res.subscriptionsSetting) // res.subscriptionsSetting = { // SYS_MSG_TYPE_INTERACTIVE: 'accept', // SYS_MSG_TYPE_RANK: 'accept', // zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: 'reject', // ke_OZC_66gZxALLcsuI7ilCJSP2OJ2vWo2ooUPpkWrw: 'ban', // } } }); [代码] [图片] 这里可以调用wx.getSetting方法,但是需要注意:如果用户第一次选“总是”后点击“取消”按钮或者订阅模板全部是未选中/reject的,那将获取不到状态(这里可能是BUG,期待官方未来修复)。 (4)用户点击“总是”后,让用户手动修改 前面说到用户点击“总是”后,系统将不再弹窗,但是我们可以通过**“wx.openSetting”**引导用户手动修改。 [代码]wx.openSetting({ success(res) { console.log(res.authSetting) // res.authSetting = { // "scope.userInfo": true, // "scope.userLocation": true // } } }) [代码] [图片] [图片] 当然用户自己也可以修改 [图片] 总结 【重点】选择“总是”,很多人认为就可无限发送订阅消息,这个是错误的,勾选和不勾选唯一的区别就是每次触发订阅的时候会不会弹授权窗口!!! 用户点击次数系统会自动累加,直接影响后台发送通知的次数。 用户选择“总是”后,小程序界面不再弹窗,但仍然有回调/callback。 任意订阅模板在用户选中“总是”(包括接受/拒绝2个状态)后,全局有效,就算其他订阅包含“此模板”也不再显示/弹出 当用户选择“总是”中“accept/选中/接受”的状态后,可以在wx.getSetting查询到用户是否选择“总是”。 当用户选择“总是”中“reject/未选中/拒绝”的状态后,返回值“无感知”(这里可能是BUG) 二、功能开发 使用微信自带的云开发,可以在没有node/后端开发支持下,完成整个订阅流程的开发。 (1)微信后台设置订阅模板和获取模板ID 1、打开小程序后台,找到订阅消息设置 [图片] 2、在公共模板库找模板或者自己申请新模板,建议能用现成模板用现成的,因为申请周期可能较长,且容易被拒 [图片] 3、选好模板后,点击详情 [图片] 4、查看模板内容和发送DATA的结构 [图片] 5、复制模板ID (2)配置云函数 [图片] [图片] 1、新建getOpenId云函数,用于获取用户的openID [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } } [代码] 2、新建订阅推送通知云函数 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() //订阅推送通知 exports.main = async (event, context) => { try { const result = await cloud.openapi.subscribeMessage.send({ touser: event.openid, //接收用户的openId page: 'pages/my/index', //订阅通知 需要跳转的页面 data: { //设置通知的内容 thing1: { value: '小程序订阅填坑' }, thing2: { value: '智库方程式' }, thing3: { value: '一起学习,一起进步' } }, templateId: '5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac' //模板id }) console.log(result) return result } catch (err) { console.log(err) return err } } [代码] 写完云函数记得右键部署下!!! (3)小程序代码部分 [代码]<!------------html -------------> <button bindtap="getOpenId" type='primary'>获取openId</button> <view class="subBtn" catch:tap="sub">订阅AAA</view> <view class="subBtn" catch:tap="send">订阅推送测试</view> <view class="subBtn" catch:tap="setting">设置“总是”后,跳转修改</view> [代码] [代码]//JS 部分 //获取用户的openid getOpenId() { wx.cloud.callFunction({ name: "getOpenId" }).then(res => { let openid = res.result.openid console.log("获取openid成功", openid) }).catch(res => { console.log("获取openid失败", res) }) }, //发送模板消息给指定的openId用户 send(openid){ wx.cloud.callFunction({ name: "sendSub", data: { openid: openid } }).then(res => { console.log("发送通知成功", res) }).catch(res => { console.log("发送通知失败", res) }); }, //消息订阅 sub: function () { wx.requestSubscribeMessage({ tmplIds: ["5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac"], success(res) { console.log("订阅授权成功:"+res); }, fail(res){ console.log("订阅授权失败:" + res); } }) }, //帮助用户跳转修改订阅状态 setting:function(){ wx.openSetting({ success(res) { console.log(res.authSetting) // res.authSetting = { // "scope.userInfo": true, // "scope.userLocation": true // } } }) }, [代码] (4)测试流程 点击发送通知后,获得这样的效果: [图片] [图片] 获得对应返回值: [图片] 当errCode为0时,即发送通知成功。 当errCode为43101,说明用户只授权了一次,但是你发送了2次,超过用户授权次数。 [图片] 三、进阶与思考 1、当你有多个订阅模板同时需要用户选择时,你可以通过以下代码记录,用户哪些选了,哪些没选。 [代码]wx.requestSubscribeMessage({ tmplIds: ["5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac", "OBB_Z10eh_Inm9p8EU6Ml_NS_mijXgTz3T07cxgKvX0","5Efr7IqIooYO9nPw047Iggxbm9Ge2Km10GQ4amGOUac"], success(res) { //console.log(res); if (res.errMsg == "requestSubscribeMessage:ok") { let acceptArray = []; //用户授权模板列表 for (let i = 0; i < tmplIds.length; i++) { const element = tmplIds[i]; if (res[element] == "accept") { acceptArray.push(element); } }; console.log(acceptArray); if (acceptArray.length > 0) { //执行下一步函数 } } } }) [代码] 2、一个关于是否需要记录用户对某个“订阅模板授权的次数”,以控制后台“发送的次数”,智库君在实战中认为,其实没有必要,顶多就是你发送返回一个错误码,微信之所有记录用户授权次数,也是为了保护用户不被骚扰。 3、你只需要记录用户点击了哪些需要授权的模板就行,为了是用户点击订阅后,改变按钮的状态,避免订阅按钮反复弹窗的问题,同时当检测到用户点错“总是”按钮后,可以自动跳转到“设置”界面。 4、这次智库君主要给大家简单介绍了下订阅全流程。后面大家可以根据自己的需要,添加和改进这些代码。比如: 配置云函数中的node函数,实现定时发送 配置云函数中的数据库,实现内容的自定义发送 最后,希望这篇文章能帮助到大家,一起学习,一起进步! (官方文档地址:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html) 往期回顾: [打怪升级]小程序自定义头部导航栏“完美”解决方案 [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二)
2021-09-13 - 怎样用一天时间,开发上架一个天气小程序
早上醒来,我不愿意回想昨天温度多少度,只想要知道今天比昨天热还是冷,适当增减衣服就行了。穿衣指数什么的根本不适合我,污染指数也没啥用,难道我能不上班嘛? 那么我的需求就是有个天气应用,告诉我今天和昨天天气对比就行了。 历史天气接口不好找,我花了几个小时搜了国内外十几个天气API,很少有历史天气查询,有的也是付费服务。免费天气预报接口倒是很多。 为一个没有几个人用的小程序付费购买接口太奢侈了,这时想到一个绝妙的(笨)方法:查询到今天天气以后,缓存起来,明天再来看就有昨日天气了! 说干就干。 1. 注册小程序 小程序注册入口在这里 https://mp.weixin.qq.com/wxopen/waregister?action=step1 填写基本信息后,验证邮箱和微信,就能登录管理后台了。 [图片] 在管理后台填写小程序名称、介绍和头像,会自动生成小程序码。 在开发设置页面可以看到AppID(小程序ID),记住这个 AppID。 2. 使用微信开发者工具 微信开发者工具在这里下载 https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html 下载后,用微信扫码打开,创建项目,这需要填入刚才的AppID。 [图片] 假设你已经知道了微信开发的基础,代码应该有类似的结构。 3. 获取位置信息 (注:和风天气支持经纬度查天气,第3步是可选步骤) 要预报当地天气,需要知道位置,微信小程序有 wx.getLocation 可以获取经纬度。 [代码]wx.getLocation({ type: 'wgs84', success: function(res) { var latitude = res.latitude var longitude = res.longitude var speed = res.speed var accuracy = res.accuracy } }) [代码] 4. 查询天气 使用经纬度查天气的接口,得到未来三天天气预报。 天气接口使用和风天气 https://www.heweather.com/douments/api/s6/weather-forecast 。和风天气的接口比较简洁,返回值也有中文描述可以直接显示。免费版的天气信息足够多。历史天气接口需要付费,我们先用免费的接口。 同上,要使用接口,需要先注册开发者账户,验证手机。 在小程序中使用前,要在小程序设置界面,开发设置中添加request合法域名: http://free-api.heweather.com 。 [代码]wx.request({ url:'https://free-api.heweather.com/s6/weather/forecast', data:{ location:location, key: '和风天气开发者密钥', rnd:new Date().getTime() // 随机数,防止请求缓存 }, success:function(res){ console.log(res); } }) [代码] 拿到天气以后在本地做缓存,最多只存两天的记录就可以了。 5. 美化前端界面 对前端程序员来说,设计酷炫的界面有点难,但是基本的审美还是有的。 用关键词 “simple weather app” 在搜索引擎搜图片,出来的看起来舒服的界面,借用一下配色。 搜索结果中还发现一个可爱的logo,还是免费的!只有一条要求,需要在使用时展示这个网站的链接,因为是小程序,不能外链,我放了文本格式的网站地址,就是这个 https://www.freepik.com/free-vector/simple-weather-app_874144.htm 。 [图片] 做好的界面。 晚上又优化了一下代码,还在12点前后做了测试,修改了几个问题,就提交审核了。 6. 测试小程序 就算是这么小的项目,测试也必不可少。 经过测试发现和风天气的返回值,是未来三天的天气数组,12月7日晚上调用返回的结构与API一样,包含了[12-7,12-8,12-9]的天气。 和风天气接口测试 问题1:但是过了午夜12点以后,返回的仍然是[12-7,12-8,12-9],就不能随便的使用 arr[0] 当作今日天气了。 问题2:早上起床不到8点,看审核还没通过,再调试一次看看,这次调用返回的数组只有一个天气[12-8],倒是有今天了,明天后天是没有的,好在我现在还不需要。 7. 审核通过 八点又看了一下,上面的API问题不会影响程序。一个小时以后,审核通过了, 审核后,兴奋的发给朋友试用。现在才发现一个重要的问题,如果哪天没打开,第二天就没有昨日天气了,需要每天都打开一次!真希望有免费的历史天气接口啊,哪怕只有简化的昨日天气也行啊。 [图片] 如果你想试用,可以在微信搜“昨日天气”小程序。如果这个需求很多,可能我会考虑买付费的历史天气接口。 后记 对比各家费用后,买了心知天气的付费接口,现在线上的版本已经去掉缓存这部分了,直接调用接口可以查询昨天的天气。 相关链接 小程序注册 小程序开发工具 心知天气 Designed by Freepik
2021-01-05 - 小程序模板消息能力调整通知
小程序模板消息能力在帮助小程序实现服务闭环的同时,也存在一些问题,如: 1. 部分开发者在用户无预期或未进行服务的情况下发送与用户无关的消息,对用户产生了骚扰; 2. 模板消息需在用户访问小程序后的 7 天内下发,不能满足部分业务的时间要求。 为提升小程序模板消息能力的使用体验,我们对模板消息的下发条件进行了调整,由用户自主订阅所需消息。 一次性订阅消息 一次性订阅消息用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。 [图片] (一次性订阅示例) 长期性订阅消息 一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。 目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。 调整计划 小程序订阅消息接口上线后,原先的模板消息接口将停止使用,详情如下: 1. 开发者可登录小程序管理后台开启订阅消息功能,接口开发可参考文档:《小程序订阅消息》 2. 开发者使用订阅消息能力时,需遵循运营规范,不可用奖励或其它形式强制用户订阅,不可下发与用户预期不符或违反国家法律法规的内容。具体可参考文档:《小程序订阅消息接口运营规范》 3. 原有的小程序模板消息接口将于 2020 年 1 月 10 日下线,届时将无法使用此接口发送模板消息,请各位开发者注意及时调整接口。 微信团队 2019.10.12
2019-10-13 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 云存储文件目录存在泄露风险
云存储目录文件存在泄露风险,浏览器中输入http://(云存储资源ID).tcb.qcloud.la/,会打开一个xml文件,里面包含云存储里面全部的文件地址等信息。
2019-11-09 - 从0到1开发一个小程序cli脚手架(一)--创建页面/组件模版篇
github地址:https://github.com/jinxuanzheng01/xdk-cli 原文地址:https://www.yuque.com/docs/share/c6dddfdf-5a18-4024-8604-5e619cb9845d cli工具是什么? 在正文之前先大致描述下什么是cli工具,cli工具英文名command-line interface,也就是命令行交互接口,比较典型的几个case例如,create-react-app,vue-cli,具体可以去百度一下,下面gif是小打卡目前用的一套自动化发布工具🔧 [图片] 可以看到整个发布流程大致是以选择或默认项的形式实现,大致分析下面几步 选择打包形式 开发模式/debug模式/发布模式 设置版本号 填写发布信息 选择环境 是否提交版本commit 是不是非常无脑?是不是再也不用担心线上发错环境了?有了它就算不同项目间,就算一天发n次版本还需要担心什么呢? 当然除了简单的发布功能还,还可以做很多的事情,比如创建page/component模版等一些更多有趣的事情 为了节约版面就不贴图了,具体可以看下仓库 https://github.com/jinxuanzheng01/xdk-cli(目前该工具是从小打卡现有的cli库中抽离的部分功能) 明确痛点 也就是我为什么要做这么一个工具,其实最开始我只是为了解决一个问题,就是在整个发布流程中需要人工去改动/确认发布环境和版本信息,大致可以想象下把线下环境发布到线上的尴尬处境 后续发现从cli角度触发,很多东西都变得简单了,大致列了下: 环境变量切换(线上环境,线下环境) 创建启动模版,包括页面,组件 自动化发布 … 准备工作 本文会以快速创建页面模版文件为例教你怎么快速撸一个属于自己的cli工具 如果觉得自己做比较麻烦,可以clone下我的仓库自己改装下 需要了解的三方库 中间会用到一些第三方库 commander, 一个解析命令行命令和参数工具 inquirer,常用交互式命令行用户界面的集合 chalk,美化你的终端输出样式 fuzzy,字符串模糊匹配的插件,根据输入关键词进行模糊匹配 json-format,json美化/格式化工具 其他的一些小知识:比如path模块,fs模块,大家可以去node官网自行查看:https://nodejs.org/api/ 搭建开发环境 创建一个空文件夹,并且npm初始化, 并且创建一个index.js页面,这个index.js将作为你整个包的入口文件 [代码]npm init -y [代码] 安装上述的三方包,当然也可以后续按需安装,这样更能清楚每个包是做什么的 [代码] npm install @moyuyc/inquirer-autocomplete-prompt commander chalk commander fuzzy inquirer json-format --save [代码] 在package.json里添加bin字段, 将自定义的命令软连到全局环境,同时执行npm link创建链接,这里如果报错{code EACCES,errno:13,…},是因为权限不足,可以尝试sudo npm link [代码] "bin": { "cli-demo": "./index.js" } [代码] 在入口文件,index.js 行首加入一行[代码]#!/usr/bin/env node[代码]指定当前脚本由node.js进行解析 [代码]#!/usr/bin/env node // 指定运行环境 // 输出文本 console.log('Hello World!!!'); [代码] 这时可以在命令行中执行[代码]cli-demo[代码]验收一下成果了 [图片] ok,可以看到当在全局状态下输入自定义命令时,正确运行了入口文件,也就意味着的开发玩具已经搭建完成 Let‘ Go 整理逻辑 以快速创建页面模版文件为例,就需要考虑需要哪些逻辑: 设置页面名称 找到已有模版文件 copy到项目中 修改app.json 识别命令行 在刚才的[代码]Hello World!!![代码]环节,已经可以正确识别cli-demo,但是需要在一个cli工具中集成更多功能,可能需要有不同的执行策略,以git为例:[代码]git clone, git status,git push[代码],所以需要识别不同的命令和参数, 是时候就需要用到[代码]commander[代码]这个第三方包帮助解析命令行参数了,当然你也可以自己撸一个lib,本质上还是方便解析[代码]process.argv[代码] index.js (本质上这个js就是一个路由) [代码]#!/usr/bin/env node const version = require('./package').version; // 版本号 /* = package import -------------------------------------------------------------- */ const program = require('commander'); // 命令行解析 /* = task events -------------------------------------------------------------- */ const createProgramFs = require('./lib/create-program-fs'); // 创建项目文件 /* = config -------------------------------------------------------------- */ // 设置版本号 program.version(version, '-v, --version'); /* = deal receive command -------------------------------------------------------------- */ program .command('create') .description('创建页面或组件') .action((cmd, options) => createProgramFs(cmd)); /* 后续可以根据不同的命令进行不同的处理,可以简单的理解为路由 */ // program // .command('build [cli]') // .description('执行打包构建') // .action((cmd, env) => callback); /* = main entrance -------------------------------------------------------------- */ program.parse(process.argv) [代码] 这时候当键入[代码]cli-demo create[代码]时会自动执行createProgramFs createProgramFs.js [代码]module.exports = function () { console.log('Hi, create-program-fs.js'); }; [代码] 命令行输入 cli-demo create [图片] 可以看到已经成功的开辟出了一块独立的业务模块,后续就只需要依据需求填补相应的内容即可 创建交互命令 收到执行命令,这个时候按第一张图,是需要开始一系列QA(当然你也可以不做交互式,直接配置命令行参数),<br />引入三方包 [代码]inquirer[代码],来指定问题队列 [代码]const question = [ // 选择模式使用 page -> 创建页面 | component -> 创建组件 { type: 'list', name: 'mode', message: '选择想要创建的模版', choices: [ 'page', 'component', ] }, // 设置名称 { type: 'input', name: 'name', message: answer => `设置 ${answer.mode} 名称 (e.g: index):`, }, ]; module.exports = function() { // 问题执行 inquirer.prompt(question).then(answers => { console.log(answers); }); }; [代码] [图片]、 可以看到通过一系列QA交互,实际输出拿到的是一个json对象,第一步已完成 创建模版文件 创建一个存放模版文件的文件夹template,并准备好你希望的模版 [图片] 项目中使用模版文件 为了方便阅读,下面的代码,需要明确下面变量的定义, Config.dir_root = 命令行执行目录 Config.root = cli项目根目录 Config.appRoot = 小程序项目路径 Config.template = 模版目录 这里有两个点,一个是执行路径的问题,另一个是分包的问题,具体如下: 执行路径 这里一定要弄明白**__dirname, process.cwd()**的区别,同时还有一些小程序是自己搭的gulp/webpack,可能小程序项目是在src目录下,一定要分清楚 __dirname: 被执行js文件的绝对路径,一般在index.js执行时缓存起来作为项目的全局路径,比如找到template文件夹就会使用 [代码]${__dirname}/template[代码] process.cwd():当前命令行运行时的工作目录,比如在/Users/xuan/Documents/cli-demo 如果当前项目在src,或其他文件夹里怎么办?可以提供一个给用户项目中的配置文件,类似于gulpfile.js或是webpack.config.js的形式,内容例如(具体可以看git仓库) [代码]module.exports = { // 小程序路径 app: './src', // 模版文件夹 template: './template' }; [代码] 可以看到对象中app属性,可以指定你当前小程序项目的路径 分包 因为小程序的分包机制会导致页面实际路径与在主包的路径不相符,例如: 主包:pages/index/index 分包:pages/main_module/pages/habit_enlist/habit_enlist 解决这个问题一方面是要有页面创建要有一定的规范,统一格式,另一方面需要根据规则解析app.json,<br />上面的主包,分包路径差不多是我目前使用的规范 解析app.json [代码]// 获取app.json function getAppJson() { let appJsonRoot = path.join(Config.appRoot, '/app.json'); try { return require(appJsonRoot); }catch (e) { Log.error(`未找到app.json, 请检查当前文件目录是否正确,path: ${appJsonRoot}`); process.exit(1); // 异常退出 } } // 解析app.json let parseAppJson = () => { // app Json 原文件 let appJson = __Data__.appJson = getAppJson(); // 获取主包页面 appJson.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = ''); // 获取分包,页面列表 appJson.subPackages.forEach(item => { __Data__.appModuleList[getPathSubSting(item.root)] = item.root; item.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = item.root); }); }; // __Data__.appPagesList = 小程序全部页面 // __Data__.appModuleList = 小程序全部分包页面 // item结构 {util_module: 'pages/util_module/'},这么定义结构是为了方便后续取数 [代码] question队列里,增加删选分包的选项 [代码] // 设置page所属module { type: 'autocomplete', name: 'modulePath', message: 'Set page ownership module', choices: [], suggestOnly: false, source(answers, input) { // none 代表放在主包 return Promise.resolve(fuzzy.filter(input, ['none', ...Object.keys(__Data__.appModuleList)]).map(el => el.original)); }, filter(input) { if (input === 'none') { return ''; } return __Data__.appModuleList[input]; }, when(answer) { return answer.mode === 'page'; } } [代码] autocomplete类型本质上是个列表,但是可以进行模糊查询,非常方便,像小打卡有接近30个分包的情况下效果尤为明显 [图片] 有了文件名,有了分包路径,有了可供copy的模版,接下来就很简单了,把模版文件塞进项目就可以了,下面是一串从仓库里copy的代码,利用async/await很方便的写出一维代码,基本上的流程: 获取路径 -> 校验 -> 获取文件信息 -> 复制文件 -> 修改app.json -> 输出结果信息 [代码]async function createPage(name, modulePath = '') { // 获取模版文件路径 let templateRoot = path.join(Config.template, '/page'); if (!Util.checkFileIsExists(templateRoot)) { Log.error(`未找到模版文件, 请检查当前文件目录是否正确,path: ${templateRoot}`); return; } // 获取业务文件夹路径 let page_root = path.join(Config.appRoot, modulePath, '/pages', name); // 查看文件夹是否存在 let isExists = await Util.checkFileIsExists(page_root); if (isExists) { Log.error(`当前页面已存在,请重新确认, path: ` + page_root); return; } // 创建文件夹 await Util.createDir(page_root); // 获取文件列表 let files = await Util.readDir(templateRoot); // 复制文件 await Util.copyFilesArr(templateRoot, `${page_root}/${name}`, files); // 填充app.json await writePageAppJson(name, modulePath); // 成功提示 Log.success(`createPage success, path: ` + page_root); } [代码] 扩展 一个基本的快速创建页面模版的cli工具就这样完成,但是有可能需要更多的一些功能 自定义模版 比如说每个项目的模版都有可能不太一样,很大程度上需要根据项目进行定制,这时候可能就需要前文提到的给用户开放config文件的插槽了 项目中的config: [代码]// xdk.config.js module.exports = { // 小程序路径 app: './', // 模版文件夹 template: './template' }; // create-program-fs.js module.exports = function() { // 校验:当前是否存在配置文件 let customConfPath = `${Config.dir_root}/xdk.config.js`; if (!Util.checkFileIsExists(customConfPath)) { Log.error('当前项目尚未创建xdk.config.js文件'); return; } // 获取用户配置项 let {app, template = ''} = require(customConfPath); // 小程序目录 Config.appRoot = path.resolve(path.join(Config.dir_root, app)); // 模版文件目录(默认使用cli提供的默认模版,当config文件有设置template路径时,使用自定义路径) !!template && (Config.template = path.resolve(path.join(Config.dir_root, template)))); // 问题执行 inquirer.prompt(question).then(answers => { console.log(answers); }); }; [代码] 发布的npm仓库 目前从开发到调试本质上是在本地提供服务,利用npm link提供软连接到全局PATH,<br />其实也可以直接发到npm上,让其他使用的该cli的成员一建安装,比如npm install -g xxxxxxx 教程的话百度,google有很多,作者表示很懒,遇到问题下面留言吧。。 最后 可以看到整个功能逻辑相对于平时写的复杂的业务逻辑来说相对简单,主要是工具库的一些使用方面的东西,中间的难点可能就是node中概念性的一些东西,然而这些多看一下文档基本就可以解决 顺便预告下后续的话可能会更新一些如何利用cli工具做到自动化发布,版本号控制,环境变量切换,自动生成文档等一系列有趣的功能 下文地址: 《从0到1开发一个小程序cli脚手架(二) --版本发布/管理篇》
2019-08-05