- 如何彻底解决小程序滚动穿透问题
背景 俗话说,产品有三宝:弹窗、浮层加引导,足以见弹窗在产品同学心目中的地位。对任意一个刚入门的前端同学来说,实现一个模态框基本都可以达到信手拈来的地步,但是,当模态框里边的内容滚动起来以后,就会出现各种各样的让人摸不着头脑的问题,其中,最出名的想必就是滚动穿透。 什么是滚动穿透? 滚动穿透的定义:指我们滑动顶层的弹窗,但效果上却滑动了底层的内容。 具体解决方案分析如下: 改变顶层:从穿透的思路考虑,如果顶层不会穿透过去,那么问题就解决了,所以我们尝试给蒙层加catchtouchmove,但是发现部分场景无效果,那么就不再赘述了。 改变底层:既然是顶层影响了底层,要是底层不会滚动,那就没这个问题了。 如何改变底层解决该问题呢? 不成熟方案: 底部页面最外层view设置position: fixed;页面不可滚动,但是这个时候会导致页面回到顶部。 滚动时监听滚动距离,弹窗时记录滚动位置,关闭弹窗后使用wx.pageScrollTo回滚到记录的位置。 成熟方案 使用page-meta组件,通过该组件我们可以操作Page的style样式,类似于h5里body设置overflow: hidden; 控制页面不可滚动。文档地址:https://developers.weixin.qq.com/miniprogram/dev/component/page-meta.html 使用wx.setPageStyle设置overflow: hidden, 也可以实现给Page组件设置样式。) page-meta组件: 通过该组件我们可以直接操作[代码]Page[代码]组件 ,我们给它的wxss样式overflow动态设置[代码]hidden[代码]or[代码]visible[代码]or[代码]auto[代码] 就可以控制整个页面是否可以滚动。 [图片] wx.setPageStyle方法: 调用这个api,动态设置它为hidden/auto,用于控制页面是否可滚动,主要用于页面组件内使用,比如封装好的弹窗组件,就不用单独写page-meta组件了。。 [代码]wx.setPageStyle({ style: { overflow: 'hidden' // ‘auto’ } }) [代码] 老规矩,结尾放代码片段: https://developers.weixin.qq.com/s/U6ItgQmP7upQ 拓展 支付宝小程序虽然存在page-meta组件,但是由于内核为69版本,给page设置overflow: hidden 也无法控制底部元素不可滚动,目前已联系支付宝的底层开发同学提供API控制页面disableScroll,目前正在封装Appx,近期开放。
08-06 - 公众号推广带货
该课时将为您解答,如何将小商店的带货商品链接插入微信公众号推广? 一、带货链接插入公众号文章步骤 二、带货链接插入自定义菜单步骤 如何获取带货商品链接?请先查看「小商店无货源带货」获取带货链接! [图片][图片] 更多资讯,欢迎到【交流专区】微信小商店主页发帖和寻找答案。
2022-04-13 - 【好文】小程序动态换肤解决方案 - 终极篇
小程序动态换肤解决方案 – 终极篇 回顾 早些日子,我写过两篇文章介绍过在微信小程序内,如何实现换肤功能,下面贴出链接,没看过的同学可以先看看 小程序动态换肤解决方案 – 本地篇 小程序动态换肤解决方案 – 接口篇 但是上面两种方案都有不足之处,所以我在文末也备注了会出 终极篇解决方案,拖延了一些时间,今天看到评论区有人cue我说什么时候出终极篇,于是,今天花了写时间整理了一下,希望可以帮助到大家。 方案 其实这篇文章提供的解决方案,更多是 [代码]接口篇[代码]的优化版本。 解决思路就是: 将接口获取到的皮肤色值属性,动态设置到需要换肤的元素的某个属性上,本质上就是替换元素的css属性的属性值,方法就是通过给当前[代码]Page[代码]和[代码]Component[代码]对象的[代码]js文件[代码]嵌入提前设置好的[代码]css变量[代码]中,然后通过[代码]setData[代码]的方法回显到对应的[代码]wxml文件[代码]中。 采用 css变量 的方式替代原有 内联修改样式 的方式; 采用小程序原生提供的mixin解决方案 —— [代码]Behavior[代码],对页面还有组件对象来说,虽有一定的侵害性,但是可以极大程度的降低重复代码的编写; 代码 1. 监听器模块 我们知道,接口返回的数据是异步的,所以,当我们进入到指定的 [代码]Page[代码]和[代码]Component[代码] 对象内部的时候,有可能还没得到数据,就需要先注册一个监听函数,等到皮肤接口请求成功之后,然后再执行皮肤设值操作; [代码]// observer.js function Observer() { this.actions = new Map() } // 监听事件 Observer.prototype.addNotice = function(key, action) { // 因为同个Page(页面)或者Component(组件)对象有可能引入多个组件 // 这些组件都用到了同一个监听器,每个监听器的回调函数需要单独处理 // 因此,结果就是: key => [handler1, hander2, hander3....] if (this.actions.has(key)) { const handlers = this.actions.get(key) this.actions.set(key, [...handlers, action]) } else { this.actions.set(key, [action]) } } // 删除监听事件 Observer.prototype.removeNotice = function(key) { this.actions.delete(key) } // 发送事件 Observer.prototype.postNotice = function(key, params) { if (this.actions.has(key)) { const handlers = this.actions.get(key) // 皮肤接口获取数据成功,取出监听器处理函数,依次执行 handlers.forEach(handler => handler(params)) } } module.exports = new Observer() [代码] 2. 皮肤对象模型模块 因为皮肤接口只会在程序首次加载运行的时候执行,换言之,通过 [代码]发布-订阅[代码] 的方式来设置皮肤只会[代码]发生在第一次接口请求成功之后[代码],后期都不会再执行;因此,我们需要通过一个Model模型对象将数据存储起来,后面的皮肤设值操作都从该model对象中获取; [代码]// viModel.js /** * @param {*} mainColor 主色值 * @param {*} subColor 辅色值 * @param {*} reset 重置 */ function ViModel(mainColor, subColor, reset = false) { // 如果当前实例已经设置过,直接返回该实例 if (typeof ViModel.instance == 'object' && !reset) { return ViModel.instance } this.mainColor = mainColor this.subColor = subColor // 实例赋值动作触发在接口有数据返回的时候 if (this.mainColor || this.subColor) { ViModel.instance = this } return this } module.exports = { // 通过save方法来赋值要通过reset = true来重置对象 save: function(mainColor = '', subColor = '') { return new ViModel(mainColor, subColor, true) }, // 直接返回的都是已经有值的单例实例 get: function() { return new ViModel() } } [代码] 3. 小程序Mixin模块 —— Behavior 这个就是这次分享的最为重要的模块 —— 注入 themeStyle 的css变量 我们直接来看这段代码: [代码]setThemeStyle({ mainColor, subColor }) { this.setData({ themeStyle: ` --main-color: ${mainColor}; --sub-color: ${subColor}; ` }) } [代码] 想必看到这里,大家应该猜到开篇说的实现原理了 这里的 [代码]themeStyle[代码] 就是我们接下来要注入到 [代码]Page[代码] 和 [代码]Component[代码] 的 data 属性,也就是需要在页面和组件中设置的[代码]动态css变量属性[代码] [代码]//skinBehavior.js const observer = require('./observer'); const viModel = require('./viModel'); module.exports = Behavior({ data: { themeStyle: null }, attached() { // 1. 如果接口响应过长,创建监听,回调函数中读取结果进行换肤 observer.addNotice('kNoticeVi', function(res) { this.setThemeStyle(res) }.bind(this)) // 2. 如果接口响应较快,modal有值,直接赋值,进行换肤 const themeData = viModel.get() if (themeData.mainColor || themeData.subColor) { this.setThemeStyle(themeData) } }, detached() { observer.removeNotice('kNoticeVi') }, methods: { setThemeStyle({ mainColor, subColor }) { this.setData({ themeStyle: ` --main-color: ${mainColor}; --sub-color: ${subColor}; ` }) }, }, }) [代码] 4. 【应用】—— Component模块 js 文件引入[代码]skinBehavior.js[代码],通过[代码]Component对象[代码]提供的[代码]behaviors[代码]属性注入进去; wxml 文件根节点设置[代码]style="{{themeStyle}}"[代码],设置css变量值; wxss 文件通过css变量设置皮肤色值 [代码]background: var(--main-color, #0366d6);[代码] [代码]// wxButton2.js const skinBehavior = require('../../js/skinBehavior'); Component({ behaviors: [skinBehavior], properties: { // 按钮文本 btnText: { type: String, value: '' }, // 是否为辅助按钮,更换辅色皮肤 secondary: { type: Boolean, value: false } } }) [代码] [代码]<!-- wxButton2.wxml --> <view class="btn-default btn {{secondary ? 'btn-secondary' : ''}}" style="{{themeStyle}}">{{ btnText }}</view> [代码] [代码]/* wxButton2.wxss */ .btn { width: 200px; height: 44px; line-height: 44px; text-align: center; color: #fff; } .btn.btn-default { background: var(--main-color, #0366d6); } .btn.btn-secondary { background: var(--sub-color, #0366d6); } [代码] 5. 【应用】 —— Page模块 使用方法跟Component模块一样,就不写了,下面贴一下代码: [代码]// skin.js const skinBehavior = require('../../js/skinBehavior'); Page({ behaviors: [skinBehavior], onLoad() { console.log(this.data) } }) [代码] [代码]<!--skin.wxml--> <view class="page" style="{{themeStyle}}"> 换肤终极篇 <view class="body"> <wxButton2 class="skinBtn" btnText="按钮1"></wxButton2> <wxButton2 class="skinBtn"btnText="按钮2" secondary></wxButton2> <wxButton2 class="skinBtn" btnText="按钮2" ></wxButton2> </view> </view> [代码] [代码]/* skin.wxss */ .page { padding: 20px; color: var(--main-color); } .skinBtn { margin-top: 10px; float: left; } [代码] 6. 【初始化】—— 接口调用 这里就是在小程序的启动文件 app.js 调用皮肤请求接口,初始化皮肤 [代码]// app.js const { getSkinSettings } = require('./js/service'); App({ onLaunch: function () { // 页面启动,请求接口 getSkinSettings().catch(err => { console.log(err) }) } }) [代码] 效果展示 [图片] 项目地址 项目地址:https://github.com/csonchen/wxSkin 这是本文案例的项目地址,如果觉得好,希望大家都去点下star哈,谢谢大家。。。
2021-03-22 - 用movable组件写出简短的拖放/拖拽/拖动 排序,含详细的讲解【拎包哥】
「前言」 这应该是社区目前(2020/12/8)最简短的拖拽排序教程之一,助你快速上手哦。 拖放排序是前端中可以和订单规格选择等等比较的,知识点最密集的基础之一。 如果你有html的基础知识,你会发现微信小程序其实是集成度非常高的框架,和vue,react等响应式前端框架没有本质的区别,甚至集成度还更高。 所以在这里好好利用小程序自身的组件及其属性,就能快速写出简短的拖拽排序。 注:感谢@烟斗 留言帮助! ========================效果图============================= [图片] 微信小程序 ========================HTML篇============================= 只使用小程序提供的movable组件即可。它简化了拖放排序的条件 ,让我们只需要控制y值就可以确定组件的位置。拖放中的放动作有手指离开的动作,而movable组件没有这个属性,所以引用了touchend。注意z-index判断层级<movable-area class='ctr'> <block wx:for='{{arr}}' wx:key='x'> <movable-view bindchange='change' bindtouchend='end' y='{{item.y}}' class='item' direction='vertical' style="z-index:{{index==dragId?2:1}}"> {{item.name}} </movable-view> </block></movable-area>(ps. 由于微信社区难以理解的bug,这里的代码不能放在代码片段里) ========================CSS篇============================= 在这个CSS我只有item的height用到了px,因为y值的像素单位是px。在css尽量不要增加额外的height属性,否则这个组件就不精准了。.ctr{ width: 400rpx; height: 800rpx; border: 1rpx solid black; } .item{ width: 400rpx; height: 50px; /* 与后来确定y值的的 i * 50对应 */ border-bottom: 1rpx solid black; box-sizing: border-box; background:white; /* 让边框内嵌,否则会随着1rpx的叠加而让y值变得不精准 */ } =========================JS篇============================== 主要步骤 用y值来确定拖放动作中放的位置将源item放置在目标item前(这也是排序的本质)注意 拖拽的数组arr一开始就放在onLoad方法而不是data里,否则会因为data的提前渲染而产生缓慢的位移。movable-view一开始是重叠的,所以要根据下标来确定每个item的y值。bindchange对应的是拖行为,我们只需要在这个方法里获取我们在拖行为时产生的y值。拖动行为不会触发bindtap那么在touchend的时候就可以获得bindchange最后一个y值,并借此确定放行为的对应的下标。 Page({ onLoad() { var arr = [ {name: 'Mike'}, {name: 'Paul'}, {name: 'Peter'}, {name: 'Andy'}, {name: 'Larry'} ] for (var i in arr) { arr[i].y = i * 50 } // movable-view的y值单位是px console.log(arr) this.setData({ arr }) }, tap(){ // console.log('在拖拽时是否出发点击行为?') // 在拖拽时不触发点击行为 }, change(e) { this.y = e.detail.y var dragId = e.currentTarget.id // 默认item id,wx-for 分配给每个item的index,我在html里id={{index}},即用id变量记录分配后的index this.setData({ dragId }) }, end(e) { console.log('im 触摸结束') console.log(this.y) // this.y item下边线到movearea顶端的距离 var arr = this.data.arr var id = e.currentTarget.id var currentId = this.y / 50 // 移动时不断计算的id if (id > currentId) { var transferId = Math.ceil(currentId) } else { var transferId = Math.floor(currentId) } var save = arr[id] // 保存初始id arr.splice(id,1) arr.splice(transferId,0,save) // 精华 for (var i in arr) { arr[i].y = i * 50 } this.setData({ arr }) } }) ------------------------------------------进阶篇vue-cli------------------------------------------- vue-cli4 ========================HTML篇============================= 挖坑,在研究vue脚手架vue-cli4的拖拽排序,未完待续。
2021-01-17 - [拆弹时刻]小程序canvas生成海报(二)--优化方案
[图片] 海报生成速度缓慢问题的优化 微信头像在app.js中预先加载缓存 多图片异步加载 流程中断处理 二次授权失败的处理 请求或者下载图片失败处理 保存图片可被压缩 海报生成速度缓慢问题的优化 原因分析: 主要的时间消耗在于getImageInfo网络请求获取头像和下载图片获得临时地址的过程,可以看到海报中有3张图片(微信头像、主图、动态二维码(对应不同新闻的ID))需要下载,接下来主要就是对这3张图的优化 微信头像在app.js中预先加载缓存 [代码]//app.js //可以在app.js中使用小程序默认的全局变量,将头像在加载的时候预先缓存 App({ onLaunch: function () { // 获取用户信息 wx.getSetting({ success: res => { if (res.authSetting['scope.userInfo']) { // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 wx.getUserInfo({ success: res => { this.globalData.userInfo = res.userInfo; //从返回值中获取微信头像地址 let WxHeader = res.userInfo.avatarUrl; wx.getImageInfo({ src: WxHeader,//下载微信头像获得临时地址 success: res => { //将头像缓存在全局变量里 this.globalData.avatarUrlTempPath = res.path; }, fail: res => { //失败回调 } }); } }) } } }) }, globalData: { userInfo: null, //如果用户没有授权,无法在加载小程序的时候获取头像,就使用默认头像 avatarUrlTempPath: "./images/defaultHeader.jpg" } }) [代码] 大致思路是: 加载App.js的时候 ==> getSetting(判断是否授权) ==> getUserInfo(获取头像) ==> getImageInfo(生成临时地址) 将需要的网络请求在加载小程序的时候就异步完成,提前将临时地址缓存在全局变量globalData中,这样当用户进入新闻页面,点击生成海报的时候就不需要在请求微信头像,缩短了不少时间。 注意: 如果用户一开始没有微信授权,生成海报时又必须要用户头像不能使用默认的话,那就只能老老实实走之前的流程了。 多图片异步加载 [代码]let num = 0; //下载图片计数器,假设一共三张图片 //下载图片1 wx.getImageInfo({ src: image_1, success: function (res) { //判断是否是最后一张图 if (num >= 2) { console.log("图片全部下载完毕,可以绘制海报") } else { //如果不是最后一张图则+1,继续 num++; } }, fail: function (res) { //失败回调 } }); //下载图片2 wx.getImageInfo({ src: image_2, success: function (res) { //判断是否是最后一张图 if (num >= 2) { console.log("图片全部下载完毕,可以绘制海报") } else { //如果不是最后一张图则+1,继续 num++; } } }); ...... [代码] 这里智库君一开始是使用promise的同步办法,但是发现3张图片阻塞严重,如果一张图片下载过慢,就会影响整个海报生成时间,所以可以改为添加计数器判断的异步方法。 当海报生成需要多张图片的时候,完全可以异步的方式加载他们,通过计数器判断是否是最后一张。 流程中断处理 [图片] 从图中可以看出,整个海报生成过程有二次授权:用户信息授权获取头像和保存相册授权,非常可能因为用户的误点或者拒绝而导致流程中断。 主要分为二种情况: 需要的图片没有拿到,我们可以采取使用默认图片的方式替代。 保存相册授权被拒绝,我们可以提示用户“截图保存”,由于当前版本6.7.2+的**wx.openSetting()**被限制(无法直接被调用),如果必须要相册权限,我们可以通过showModal触发。 API/组件名称 终端类型 微信版本 触发方法 openSetting 6.7.2 2.3.0 showModal [代码]// 关于 openSetting 的调用方法 wx.showModal({ title: '相册权限', content: '需要你提供保存相册权限', success: function (res) { if (res.confirm) { wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取 相册 权限成功,给出再次点击图片保存到相册的提示。'); } else { console.log('获取 相册 权限失败,给出不给权限就无法正常使用的提示') } } }) } } }) //获取相册权限的流程处理 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, //canvasToTempFilePath API生成的临时地址 success: function (data) { console.log("提示图片保存成功"); }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") //调用上面说到的方法 wx.openSetting } else { console.log("提示:请截屏保存分享"); } }, complete(res) { console.log(res); } }) [代码] [图片] 保存图片可被压缩 小程序官方提供了一个API可以设置用户保存图片的质量,仅针对JPG。目前不完全确定:压缩会不会导致额外的性能开销而延长保存时间,自己测试下来 100%、80%、60% 保存时间上没有明显区别。 属性 默认值 说明 最低版本 quality 1.0 图片的质量,取值范围为 (0, 1] 1.7.0 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'canvasId', quality:0.8, //设置JPG保存质量 80% success: res => { }, fail:res => { } }, this) [代码] 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/wx.canvasToTempFilePath.html?search-key=canvasToTempFilePath [图片] [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 如果智酷君的分享能够帮助到你,或者想持续获得最新的全栈攻略 可以搜索公众号 Geek_Club 或者 智酷方程式 扫描二维码关注公众号哟👇👇👇 [图片]
2019-06-11 - [填坑手册]小程序Canvas生成海报(一)--完整流程
[图片] 海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 [图片] 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 [图片] 海报生成流程 [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 下面分享下主要的代码内容和“填坑现场”: 一、添加字体 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html [代码]canvasContext.font = value //示例 ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10 ctx.setTextAlign('left'); ctx.setTextBaseline("top"); ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本 [代码] 符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“…”代替) [代码]ctx.setTextAlign('left'); ctx.setFillStyle('#000');//文字颜色:默认黑色 ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10 let canvasTitleArray = canvasTitle.split(""); let firstTitle = ""; //第一行字 let secondTitle = ""; //第二行字 for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; } } //第一行文字 ctx.fillText(firstTitle, 20, 278, 280)//绘制文本 //第二行问题 if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本 } [代码] 通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。 (一行字允许宽度为280时,判断需要写小点,比如260) 二、获取临时地址并设置图片 [代码]let mainImg = "https://demo.com/url.jpg"; wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { //处理图片纵横比例过大或者过小的问题!!! let h = res.height; let w = res.width; let setHeight = 280, //默认源图截取的区域 setWidth = 220; //默认源图截取的区域 if (w / h > 1.5) { setHeight = h; setWidth = parseInt(280 / 220 * h); } else if (w / h < 1) { setWidth = w; setHeight = parseInt(220 / 280 * w); } else { setHeight = h; setWidth = w; }; console.log(setWidth, setHeight) ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); ctx.draw(true); }, fail: function (res) { //失败回调 } }); [代码] 在开发过程中如果封面图无法按照约定的比例(280x220)给到: 那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度100%,过宽的图是高度100%。 在canvas中draw图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的临时路径。 微信官方提供两个API: wx.downloadFile(OBJECT)和wx.getImageInfo(OBJECT)。都需先配置download域名才能生效。 三、裁切“圆形”头像画图 [代码]ctx.save(); //保存画图板 ctx.beginPath()//开始创建一个路径 ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.closePath(); ctx.drawImage(headImageLocal, 20, 10, 30, 30); ctx.draw(true); ctx.restore()//恢复之前保存的绘图上下文 [代码] 使用图形上下文的不带参数的clip()方法来实现Canvas的图像裁剪功能。该方法使用路径来对Canvas话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用clip()方法来设置裁剪区域。 需要注意的是裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小,也就是说画布是越切越小的,要想保证最后仍然能在canvas最初定义的大小下绘图需要注意save()和restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~ 小程序 canvas 裁切BUG [代码]ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); //第一个填充矩形 wx.downloadFile({ url: headUri, success(res) { ctx.beginPath() ctx.arc(50, 50, 25, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(res.tempFilePath, 25, 25); //第二个填充图片 ctx.draw() ctx.restore() ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); ctx.draw(true) ctx.restore() } }) [代码] clip裁切这个功能,如果有超过一张图片/背景叠加,则裁切效果失效。 错误参考:http://html51.com/info-38753-1/ 四、将canvas导出成虚拟地址 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'customCanvas', success: (res) => { console.log(res.tempFilePath) //为canvas的虚拟地址 } }) res: { errMsg: "canvasToTempFilePath:ok", tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" } [代码] 这里需要把canvas里面的内容,导出成一个临时地址才能保存在相册,比如: http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg 五、询问并获取访问手机本地相册权限 [代码]wx.getSetting({ success(res) { console.log(res) if (!res.authSetting['scope.writePhotosAlbum']) { //判断权限 wx.authorize({ //获取权限 scope: 'scope.writePhotosAlbum', success() { console.log('授权成功') //转化路径 self.saveImg(); } }) } else { self.saveImg(); } } }) [代码] 判断是否有访问相册的权限,如果没有,则请求权限。 六、保存到用户手机本地相册 [代码]wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '保存到系统相册成功', icon: 'success', duration: 2000 }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取权限成功,给出再次点击图片保存到相册的提示。') } else { console.log('获取权限失败,给出不给权限就无法正常使用的提示') } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }); } }, complete(res) { console.log(res); } }) [代码] 保存到本地需要一定的时间,需要加一个loading的状态。 七、关于组件中引用canvas [代码]let ctx = wx.createCanvasContext('posterCanvas',this); //需要加this [代码] 在components中canvas无法选中的问题: 在components自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas> ,如果省略则不在任何自定义组件内查找。
2021-09-13 - [打怪升级]小程序自定义头部导航栏“完美”解决方案
[图片] 为什么要做这个? 主要是在项目中,智酷君发现的一些问题 一些页面是通过扫码和订阅消息访问后,没有直接可以点击去首页的,需要添加一个home链接 需要添加自定义搜索功能 需要自定义一些功能按钮 [图片] 其实,第一个问题,在最近的微信版本更新中已经优化了,通过 小程序模板消息 过来的,系统会自动加上home按钮,但对于其他的访问方式则没有支持~ 一个不大不小的问题:两边ICON不对齐问题 [图片] 智酷君之前尝试了各种解决方法,发现有一个问题,就是现在手机屏幕太多种多样,有 传统头部、宽/窄刘海屏、水滴屏等等,无法八门,很多解决方案都无法解决特殊头部,系统**“胶囊按钮”** 和 自定义按钮在Android屏幕可能有 几像素不对齐 的问题(强迫症的噩梦)。 下面分享下一个相对比较完善的解决方案: [图片] 小程序代码段DEMO Link: https://developers.weixin.qq.com/s/cuUaCimT72cH ID: cuUaCimT72cH 智酷君做了一个demo代码段,方便大家直接用IDE工具查看源码~ [图片] 页面配置 1、页面JSON配置 [代码]{ "usingComponents": { "NavComponent": "/components/nav/common" //以插件的方式引入 }, "navigationStyle": "custom" //自定义头部需要设置 } [代码] 如果需要自定义头部,需要设置navigationStyle为 “custom” 2、页面代码 [代码]<!-- home 类型的菜单 --> <NavComponent v-title="自定义头部" bind:commonNavAttr="commonNavAttr"></NavComponent> <!-- 搜索菜单 --> <NavComponent is-search="true" bind:commonNavAttr="commonNavAttr"></NavComponent> [代码] 可以在自定义导航标签上添加属性配置来设置功能,具体按照实际需要来 3、目录结构 [代码]│ ├─components │ └─nav │ common.js │ common.json │ common.wxml │ common.wxss │ ├─images │ back.png │ home.png │ └─index index.js index.json index.wxml index.wxss search.js search.json search.wxml search.wxss [代码] 仅供参考 插件对应的JS部分 components/nav/common.js部分 [代码]const app = getApp(); Component({ properties: { vTitle: { type: String, value: "" }, isSearch:{ type: Boolean, value: false } }, data: { haveBack: true, // 是否有返回按钮,true 有 false 没有 若从分享页进入则没有返回按钮 statusBarHeight: 0, // 状态栏高度 navbarHeight: 0, // 顶部导航栏高度 navbarBtn: { // 胶囊位置信息 height: 0, width: 0, top: 0, bottom: 0, right: 0 }, cusnavH: 0, //title高度 }, // 微信7.0.0支持wx.getMenuButtonBoundingClientRect()获得胶囊按钮高度 attached: function () { if (!app.globalData.systeminfo) { app.globalData.systeminfo = wx.getSystemInfoSync(); } if (!app.globalData.headerBtnPosi) app.globalData.headerBtnPosi = wx.getMenuButtonBoundingClientRect(); console.log(app.globalData) let statusBarHeight = app.globalData.systeminfo.statusBarHeight // 状态栏高度 let headerPosi = app.globalData.headerBtnPosi // 胶囊位置信息 console.log(statusBarHeight) console.log(headerPosi) let btnPosi = { // 胶囊实际位置,坐标信息不是左上角原点 height: headerPosi.height, width: headerPosi.width, top: headerPosi.top - statusBarHeight, // 胶囊top - 状态栏高度 bottom: headerPosi.bottom - headerPosi.height - statusBarHeight, // 胶囊bottom - 胶囊height - 状态栏height (胶囊实际bottom 为距离导航栏底部的长度) right: app.globalData.systeminfo.windowWidth - headerPosi.right // 这里不能获取 屏幕宽度,PC端打开小程序会有BUG,要获取窗口高度 - 胶囊right } let haveBack; if (getCurrentPages().length != 1) { // 当只有一个页面时,并且是从分享页进入 haveBack = false; } else { haveBack = true; } var cusnavH = btnPosi.height + btnPosi.top + btnPosi.bottom // 导航高度 console.log( app.globalData.systeminfo.windowWidth, headerPosi.width) this.setData({ haveBack: haveBack, // 获取是否是通过分享进入的小程序 statusBarHeight: statusBarHeight, navbarHeight: headerPosi.bottom + btnPosi.bottom, // 胶囊bottom + 胶囊实际bottom navbarBtn: btnPosi, cusnavH: cusnavH }); //将实际nav高度传给父类页面 this.triggerEvent('commonNavAttr',{ height: headerPosi.bottom + btnPosi.bottom }); }, methods: { _goBack: function () { wx.navigateBack({ delta: 1 }); }, bindKeyInput:function(e){ console.log(e.detail.value); } } }) [代码] 解决不同屏幕头部不对齐问题的终极办法是 wx.getMenuButtonBoundingClientRect() 这个方法从微信7.0.0开始支持,通过这个方法我们可以获取到右边系统胶囊的top、height、right等属性,这样无论是水滴屏、刘海屏、异形屏,都能完美对齐右边系统默认的胶囊bar,完美治愈强迫症~ APP.js 部分 [代码]//app.js App({ /** * 加载页面 * @param {*} options */ onShow: function (options) { }, onLaunch: async function () { let self = this; //设置默认分享 this.globalData.shareData = { title: "智酷方程式" } // this.getSysInfo(); }, globalData: { //默认分享文案 shareData: {}, qrCodeScene: false, //二维码扫码进入传参 systeminfo: false, //系统信息 headerBtnPosi: false, //头部菜单高度 } }); [代码] 将获取的参数存储在一个全局变量globalData中,可以减少反复调用的性能消耗。 插件HTML部分 [代码]<view class="custom_nav" style="height:{{navbarHeight}}px;"> <view class="custom_nav_box" style="height:{{navbarHeight}}px;"> <view class="custom_nav_bar" style="top:{{statusBarHeight}}px; height:{{cusnavH}}px;"> <!-- 搜索部分--> <block wx:if="{{isSearch}}"> <input class="navSearch" style="height:{{navbarBtn.height-2}}px;line-height:{{navbarBtn.height-4}}px; top:{{navbarBtn.top+1}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;" maxlength="10" bindinput="bindKeyInput" placeholder="输入文字搜索" /> </block> <!-- HOME 部分--> <block wx:else> <view class="custom_nav_icon {{!haveBack||'borderLine'}}" style="height:{{navbarBtn.height}}px;line-height:{{navbarBtn.height-2}}px; top:{{navbarBtn.top}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;"> <view wx:if="{{haveBack}}" class="icon-back" bindtap='_goBack'> <image src='/images/back.png' mode='aspectFill' class='back-pre'></image> </view> <view wx:if="{{haveBack}}" class='navbar-v-line'></view> <view class="icon-home"> <navigator class="home_a" url="/pages/home/index" open-type="switchTab"> <image src='/images/home.png' mode='aspectFill' class='back-home'></image> </navigator> </view> </view> <view class="nav_title" style="height:{{cusnavH}}px; line-height:{{cusnavH}}px;"> {{vTitle}} </view> </block> </view> </view> </view> [代码] 主要是对几种状态的判断和定位的计算。 插件CSS部分 [代码]/* components/nav/test.wxss */ .custom_nav { width: 100%; background: #3a7dd7; position: relative; z-index: 99999; } .custom_nav_box { position: fixed; width: 100%; background: #3a7dd7; z-index: 99999; border-bottom: 1rpx solid rgba(255, 255, 255, 0.3); } .custom_nav_bar { position: relative; z-index: 9; } .custom_nav_box .nav_title { font-size: 28rpx; color: #fff; text-align: center; position: absolute; max-width: 360rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; top: 0; left: 0; right: 0; bottom: 0; margin: auto; z-index: 1; } .custom_nav_box .custom_nav_icon { position:absolute; z-index: 2; display: inline-block; border-radius: 50%; vertical-align: top; font-size:0; box-sizing: border-box; } .custom_nav_box .custom_nav_icon.borderLine { border: 1rpx solid rgba(255, 255, 255, 0.3); background: rgba(0, 0, 0, 0.1); } .navbar-v-line { width: 1px; margin-top: 14rpx; height: 32rpx; background-color: rgba(255, 255, 255, 0.3); display: inline-block; vertical-align: top; } .icon-back { display: inline-block; width: 74rpx; padding-left: 20rpx; vertical-align: top; /* margin-top: 12rpx; vertical-align: top; */ height: 100%; } .icon-home { /* margin-top: 8rpx; vertical-align: top; */ display: inline-block; width: 80rpx; text-align: center; vertical-align: top; height: 100%; } .icon-home .home_a { height: 100%; display: inline-block; vertical-align: top; width: 35rpx; } .custom_nav_box .back-pre, .custom_nav_box .back-home { width: 35rpx; height: 35rpx; vertical-align: middle; } .navSearch { width: 200px; background: #fff; font-size: 14px; position: absolute; padding: 0 20rpx; z-index: 9; } [代码] 总结: 通过微信API: getMenuButtonBoundingClientRect(),结果各类手机屏幕的适配问题 将算好的参数存储在全局变量中,一次计算全局使用,爽YY~ 往期回顾: [填坑手册]小程序PC版来了,如何做PC端的兼容?! [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二)
2021-09-13 - [填坑手册]小程序新版订阅消息+云开发实战与跳坑
[图片] 老版本的订阅消息在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 - 云开发数据设计浅析
云开发数据设计浅析该文转自以下链接,如需了解详情,请猛戳↓↓↓↓ 聊聊 MongoDB 数据库的设计 https://www.codesky.me/archives/talk-about-mongodb-collection-design.wind 转发原因 由于下面文章来自个人网站,具有不可访问的因素,我转到社区 自从正式使用了 MongoDB 之后,不止一次吐槽过 MongoDB 的各种垃圾设定,包括但不仅限于: 没有事务没有表连接(新版支持了,但估摸着性能堪忧)也就是说,同样的操作,在 SQL 下通过 JOIN 控制原子性的,通过 MongoDB 可能就不得不去查个两次,而且原子性不可保证——MongoDB 官方也是非常实诚,人家在选型的时候就说了,如果贵系统对并发性(Concurrency)有强要求,那么 MongoDB 可能就不是你的菜了。 在 MongoDB 初窥 中我们也简单的介绍了锁机制,如有需要可以阅读。 前两天无聊接着看《MongoDB 权威指南》的时候看到一些观点,觉得给 MongoDB 数据库的设计起到了一定指导和参考作用,故作此文(笔记): 首先我们了解两个概念: 范式化:将数据分散到不同的集合,多个集合之间可以相互引用,这样要修改这部分数据,只要修改这部分数据所在的文档,其他区域与数据内容无关。反范式化:每个文档所需的数据都存储在文档内部,每个文档都拥有自己的数据副本。如果要修改这部分数据,需要修改这块数据对应的每一个文档。对于我们前文所说的,由于没有「连接」,范式化意味着我们要取出完整所需的数据可能要进行多次查询,而反范式化则只要一次查询即可。从写入的角度,我们可以看到,范式化写入的消耗更少,配合锁机制,对于拥有 [代码]subdocument[代码] 概念的 MongoDB,我们需要根据自己的实际需求去权衡。 在 SQL 中,我们经常会提起:一对一,一对多,多对多,而在 MongoDB 这样的数据库中,我们可以分为新的类型:少和多,之后我们会根据少和多进行一些数据库设计的详细分析,先来简单根据之前的介绍引用一下《MongoDB 权威指南》中的表格: 更适合内嵌更适合引用子文档较小子文档较大数据不会定期改变数据经常改变最终数据一致即可中间阶段的数据必须一致文档数据小幅增加文档数据大幅增加数据通常需要执行二次查询才能获得数据通常不包含在结果中快速读取快速写入 通常来说,「少」的关系对于内嵌更为合适,「多」则对于引用更加合适:比如文章和标签的关系可能是多对少,文章和评论的关系可能是一(少)对多。 所以我们的 Tags 可以内嵌,而评论则使用引用更好。 由于 MongoDB 的文档会自动扩充大小,如果太过频繁的让 MongoDB 产生文档移动,将会造成性能问题,在设计阶段,可以预留足够的空间,提高写入速度。 根据这一设计原理,结合 MongoDB 的一些限制,可以在一定程度上解决以下问题,而不是看心情靠玄学去进行设计: 我该不该用 MongoDB?我该用引用还是 SubDocument?
2021-01-06 - 开发常用小程序推荐
小程序推荐 1、在平时 开发,我们经常会遇到内容安全监测的服务,推荐官方小程序 如需体验、接入安全能力,请扫一扫如下小程序二维码进行详细了解。 珊瑚内容安全助手 [图片] 2、在测试小程序时,时常遇到手机兼容性问题,可以扫码获取手机具体信息 [图片] 具体扫码截图如下所示 小程序查名称 [图片] 客服小程序 [图片] 1
2021-01-08 - 云开发·云调用生成小程序码
云开发·云调用生成小程序码 小程序云开发已经支持云调用,开放了很多接口,一直想要的获取小程序码也支持了。这下轻量的小程序也可以有自定义小程序码的功能。 1. 需求 获得一个带参数的小程序码,传播出去以后,用户扫码进入指定页面,根据参数做不同的处理。本文只讲小程序码生成、存储、展示部分。参数处理不多介绍,可以查看 项目代码 了解更多。 2. 开通云开发 新建小程序可以从开发工具的云开发模板初始化项目,根据云开发操作指引新建项目即可。 但是这里有个问题,已发布小程序的页面才能生成小程序码。如果现有的小程序没有开通云开发,需要做以下几步: 开发工具开通云开发,设定云开发的环境; 将原来的代码(除了[代码]project.config.json[代码]以外的所有文件)放到新建的 [代码]miniprogram[代码] 目录; 新增 [代码]cloudfunctions[代码] 目录; [代码]app.json[代码] 新增配置 [代码]"cloud": true[代码]; [代码]project.config.json[代码] 配置 [代码]"miniprogramRoot":"miniprogram/"[代码] 和 [代码]"cloudfunctionRoot":"cloudfunctions/"[代码]; 修改小程序基础库版本,最低要 2.3.0 [代码]"libVersion": "2.3.0"[代码]。 3. 生成小程序码 下面可以开始写代码开发了,开始之前,建议先看完官方教程。特别是开发工具的使用步骤,开发和调试时如果遇到奇怪的问题,可以尝试重启开发工具、重装开发工具,也可以去微信开放社区发帖。(重启和重装都是我在社区中发现的答案,能解决各种不应该存在的问题)。 3.1 准备文件 在 [代码]cloudfunctions[代码]目录右键新建Node.js云函数 [代码]getqr[代码]。 生成小程序码需要单独指定权限。在 [代码]getqr[代码] 目录新建 [代码]config.json[代码] ,里面写以下内容: [代码]{ "permissions": { "openapi": [ "wxacode.getUnlimited" ] } } [代码] 小程序码的获取方式有三种,这里只用到了接口 getUnlimited,选择这个接口的原因是漂亮的圆形小程序码,数量无限制。具体区别可以去 获取小程序码官方文档查看详情。 正常情况下,这个时候云函数可以部署测试了。如果遇到部署不成功、各种权限问题,可以尝试本地部署上传所有文件、重启试试。 3.2 生成小程序码 生成小程序码的代码如下,可以指定页面和页面参数 [代码]scene[代码],还有小程序码的尺寸。 注意这里的 [代码]scene[代码] 有限制: 最大32个可见字符; 只支持数字,大小写英文以及部分特殊字符:[代码]!#$&'()*+,/:;=?@-._~[代码]; 注意参数格式:下面实例代码生成小程序码后,扫码获得 [代码]pages/demo/demo?scene=id%3D6[代码] 。 [代码]try { const result = await cloud.openapi.wxacode.getUnlimited({ page: 'pages/demo/demo', scene: 'id=6', width: 240, }) console.log(result) return result } catch (err) { console.log(err) return err } [代码] 直接调用,比服务端调用少了 access_token 参数。 3.3 上传到云存储 返回值中的 buffer 就是图片内容,直接上传到云存储: [代码]const uploadResult = await cloud.uploadFile({ cloudPath: 'shareqr/' + qr_name_hash + '.jpg', fileContent: result.buffer, }); [代码] 我在云存储新建了 [代码]shareqr[代码] 目录保存小程序码; 图片名根据参数取md5摘要; [代码]getUnlimited[代码] 返回的图像是 [代码]jpeg[代码] 格式,后缀硬编码写 [代码].jpg[代码]。 3.4 获取图片临时路径 直接上代码 [代码]getURLReault = await cloud.getTempFileURL({ fileList: [uploadResult.fileID] }); fileObj = getURLReault.fileList[0] return fileObj [代码] 3.5 直接从存云存储获取 生成过以后图片已经保存在云存储,用同样的参数第二次调用没必要再生成一次,去掉一次网络请求,可以节省不少时间。 前面说到文件名使用请求参数摘要,知道了目录和文件名,再加上文件bucket前缀就可以拼出来 [代码]fileID[代码],用[代码]fileID[代码] 可以查询云存储的文件。 比如我刚刚生成的 fileID 是 [代码]cloud://dev-xxxx.8888-dev-xxxx/qr/44ea42f05091c3bec771123e6e8cd4c2.jpg[代码], 前缀就是 [代码]cloud://dev-xxxx.8888-dev-xxxx/[代码]。再拼上目录、文件名、后缀就是 [代码]fileID[代码]。 注:此处的 [代码]fileID[代码]拼接方法并不是来自官方文档,只是在使用中发现这个前缀不会变。还需要官方解释说明[代码]fileID[代码]规则。 如果会改变,就需要再用云数据库存储[代码]fileID[代码],更麻烦一些。 3.6 云函数完整代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk'); const crypto = require('crypto'); const bucketPrefix = 'cloud://dev-xxxx.8888-idc-4d11a4-1257831628/qr/'; // env: 'dev-xxxx' // 云函数入口函数 exports.main = async (event, context) => { const full_path = event.page + '?' + event.scene; const qr_name_hash = crypto.createHash('md5').update(full_path).digest('hex'); const temp_id = bucketPrefix + qr_name_hash + '.jpg'; // return { // full_path, // qr_name_hash, // temp_id // } try { // 先尝试获取文件,存在就直接返回临时路径 let getURLReault = await cloud.getTempFileURL({ fileList: [temp_id] }); // return getURLReault; let fileObj = getURLReault.fileList[0]; if (fileObj.tempFileURL != '') { fileObj.fromCache = true; return fileObj; } // 生成小程序码 const wxacodeResult = await cloud.openapi.wxacode.getUnlimited({ scene: event.scene, page: event.page, width: 280 //二维码的宽度,单位 px,最小 280px,最大 1280px }) // return wxacodeResult; if (wxacodeResult.errCode != 0) { // 生成二维码失败,返回错误信息 return wxacodeResult; } // 上传到云存储 const uploadResult = await cloud.uploadFile({ cloudPath: 'qr/' + qr_name_hash + '.jpg', fileContent: wxacodeResult.buffer, }); // return uploadResult; if (!uploadResult.fileID) { //上传失败,返回错误信息 return uploadResult; } // 获取图片临时路径 getURLReault = await cloud.getTempFileURL({ fileList: [uploadResult.fileID] }); fileObj = getURLReault.fileList[0]; fileObj.fromCache = false; // 上传成功,获取文件临时url,返回临时路径的查询结果 return fileObj; } catch (err) { return err } } [代码] 4. 小程序页面调用 调用页面就比较简单了,在小程序新建一个 [代码]pages/share/share[代码] 在 [代码]onLoad[代码] 函数调用云函数。 [代码]// 使用前记得先初始化云函数,一版放到 app.js onLaunch() 中 // wx.cloud.init({env: 'dev-8888'}) wx.cloud.callFunction({ name: 'getqr', data: { page: 'pages/demo/demo', scene: 'id=6', } }).then(res => { console.log(res.result); if (res.result.status == 0) { _this.setData({ qr_url: res.result.tempFileURL }) }else{ wx.showToast({ icon: 'none', title: '调用失败', }) } }).catch(err => { console.error(err); wx.showToast({ icon: 'none', title: '调用失败', }) }) [代码] 至此完整的调用过程已经全部完成,详细代码可以到 项目代码 查看。 代码中还对入口页面和share页面的参数做了包装,云函数可以直接使用,小程序可以稍做修改适应自己业务。 写在最后 小程序云开发已经开放了很多功能,除了这次提到的生成小程序码,云调用还可以发送模板消息。有需要的开发者又一个理由可以快速上线新功能了。 云开发还开放了[代码]HTTP API[代码],也就是用自己的服务器调用云函数。以前看完云开发介绍文章最大的疑问就是,你说的都很好,可是后台数据怎么管理呢?不能跟自己的服务器结合,只能放一些轻量的小程序。有了 [代码]HTTP API[代码] 以后就可以用自己的服务器做管理后台了。这时候你要问,都用上服务器了,还需要云开发做什么。首先,云开发免费;其次,免费功能已经够强,就差不能做Web管理后台了;最后,获取access_token(小程序及小游戏调用不要求IP地址在白名单内。)
2020-07-10 - 【组件库】编写一套小程序商城UI组件库
小程序商城UI组件库 wx-mall-components 最近很久没有更新文章了,在开源一个商城UI组件库,将日常商城用到的一些组件整合打包整理出来,并且支持一键更换组件皮肤等商城常用的功能,供大家使用,组件库还在完善当中,后续会陆陆续续加上各种商城用到的酷炫功能,希望大家可以来一起维护。 项目地址 https://github.com/csonchen/wx-mall-components 技术 pug # 编写静态模板 stylus # 编写样式 gulp # 文件编译,复制操作等 启动 [代码]# 安装&开发阶段 npm install (cnpm install) npm run start # 小程序调试阶段 打开“小程序开发者工具”,选择“导入项目”,选中 dist 目录 [代码] 四大模块 目前根据实际情况拆成四大组件版块,分别如下: 基础组件 交互组件 表单组件 业务组件 目录结构 [代码] wx-mall-components 微信小程序商城组件库 ./dist # 编译后的小程序文件目录存放路径 ./src ├── app.js ├── app.json ├── app.styl ├── components # 组件存放路径 │ ├── card-swiper │ │ ├── card-swiper.js │ │ ├── card-swiper.json │ │ ├── card-swiper.pug │ │ └── card-swiper.styl │ ├── date-picker │ ├── dropdown-select │ ├── form-input │ ├── search │ └── toast ├── images ├── node_modules ├── package.json ├── pages # 页面存放路径 │ ├── cardSwiperPage │ │ ├── cardSwiperPage.js │ │ ├── cardSwiperPage.json │ │ ├── cardSwiperPage.pug │ │ ├── cardSwiperPage.styl │ │ └── doc.js # 组件说明文档 │ ├── datePickerPage │ ├── dropdownSelectPage │ ├── index │ ├── inputPage │ ├── richTextPage │ ├── searchPage │ └── toastPage ├── sitemap.json ├── styles # 公用样式 │ ├── define.styl # 定义变量 │ ├── flex.styl # flex布局 │ ├── framework.styl # 框架定义 │ ├── normal.styl # 字体,边距等 │ └── plugin.styl # 三角形,下三角等插件定义 └── templates # 公用模板文件 └── pageHead.pug [代码] 示例 [图片] 小程序码 [图片]
2020-07-06 - 【好文】小程序动态换肤解决方案 - 本地篇
小程序动态换肤方案 – 本地篇 需求说明 在开发小程序的时候,尤其是开发第三方小程序,我们作为开发者,只需要开发一套模板,客户的小程序对我们进行授权管理,我们需要将这套模板应用到对方的小程序上,然后进行发版审核即可; 但是个别客户的小程序需要做 [代码]定制化配色方案[代码],也就是说,不同的小程序个体需要对页面的元素(比如:按钮,字体等)进行不同的配色设置,接下来我们来讨论一下怎么实现它。 方案和问题 一般来说,有两种解决方案可以解决小程序动态换肤的需求: 小程序内置几种主题样式,通过更换类名来实现动态改变小程序页面的元素色值; 后端接口返回色值字段,前端通过 [代码]内联[代码] 方式对页面元素进行色值设置。 当然了,每种方案都有一些问题,问题如下: 方案1较为死板,每次更改主题样式都需要发版小程序,如果主题样式变动不大,可以考虑这种; 方案2对于前端的改动很大,[代码]内联[代码] 也就是通过 [代码]style[代码] 的方式内嵌到[代码]wxml[代码] 代码中,代码的阅读性会变差,但是可以解决主题样式变动不用发版小程序的问题。 ps:我一直在尝试如何在小程序里面,通过js动态修改stylus的变量问题,这样就可以解决上面说的问题了,后期如果实现了,一定周知各位 本文先重点描述第一种方案的实现,文章末尾会贴上我的 [代码]github项目[代码] 地址,方便大家尝试。 前期准备 本文采用的是 [代码]gulp[代码] + [代码]stylus[代码] 引入预编译语言来处理样式文件,大家需要全局安装一下 [代码]gulp[代码],然后安装两个 [代码]gulp[代码] 的插件 [代码]gulp-stylus[代码](stylus文件转化为css文件) [代码]gulp-rename[代码](css文件重命名为wxss文件)。 gulp 这里简单贴一下gulpfile文件的配置,比较简单,其实就是借助 [代码]gulp-stylus[代码] 插件将 [代码].styl[代码] 结尾的文件转化为 [代码].css[代码] 文件,然后引入 [代码]gulp-rename[代码] 插件对文件重命名为 [代码].wxss[代码] 文件; 再创建一个任务对 [代码].styl[代码] 监听修改,配置文件如下所示: [代码]var gulp = require('gulp'); var stylus = require('gulp-stylus'); var rename = require('gulp-rename'); function stylusTask() { return gulp.src('./styl/*.styl') .pipe(stylus()) .pipe(rename(function(path) { path.extname = '.wxss' })) .pipe(gulp.dest('./wxss')) } function autosTask() { gulp.watch('./styl/*.styl', stylusTask) } exports.default = gulp.series(gulp.parallel(stylusTask, autosTask)) [代码] stylus 这里会分为两个文件,一个是主题样式变量定义文件,一个是页面皮肤样式文件,依次如下所示: 主题样式变量设置 [代码]// theme1 theme1-main = rgb(254, 71, 60) theme1-sub = rgb(255, 184, 0) // theme2 theme2-main = rgb(255, 158, 0) theme2-sub = rgb(69, 69, 69) // theme3 theme3-main = rgb(215, 183, 130) theme3-sub = rgb(207, 197, 174) [代码] 页面皮肤样式 [代码]@import './define.styl' // 拼接主色值 joinMainName(num) theme + num + -main // 拼接辅色值 joinSubName(num) theme + num + -sub // 遍历输出改变色值的元素类名 for num in (1..3) .theme{num} .font-vi color joinMainName(num) .main-btn background joinMainName(num) .sub-btn background joinSubName(num) [代码] 输出: [代码].theme1 .font-vi { color: #fe473c; } .theme1 .main-btn { background: #fe473c; } .theme1 .sub-btn { background: #ffb800; } .theme2 .font-vi { color: #ff9e00; } .theme2 .main-btn { background: #ff9e00; } .theme2 .sub-btn { background: #454545; } .theme3 .font-vi { color: #d7b782; } .theme3 .main-btn { background: #d7b782; } .theme3 .sub-btn { background: #cfc5ae; } [代码] 代码我写上了注释,我还是简单说明一下上面的代码:我首先定义一个主题文件 [代码]define.styl[代码] 用来存储色值变量,然后会再定义一个皮肤文件 [代码]vi.styl[代码] ,这里其实就是不同 [代码]主题类名[代码] 下需要改变色值的元素的属性定义,元素的色值需要用到 [代码]define.styl[代码] 预先定义好的变量,是不是很简单,哈哈哈。 具体使用 但是在具体页面中需要怎么使用呢,接下来我们来讲解一下 页面的 [代码]wxss[代码] 文件导入编译后的 [代码]vi.wxss[代码]文件 [代码]@import '/wxss/vi.wxss'; [代码] 页面的 [代码]wxml[代码] 文件需要编写需要改变色值的元素,并且引入变量 [代码]theme[代码] [代码]<view class="intro {{ theme }}"> <view class="font mb10">正常字体</view> <view class="font font-vi mb10">vi色字体</view> <view class="btn main-btn mb10">主色按钮</view> <view class="btn sub-btn">辅色按钮</view> </view> [代码] 页面 [代码]js[代码] 文件动态改变 [代码]theme[代码]变量值 [代码] data: { theme: '' }, handleChange(e) { const { theme } = e.target.dataset this.setData({ theme }) } [代码] 效果预览 [图片] 项目地址 项目地址:https://github.com/csonchen/wxSkin 这是本文案例的项目地址,为了方便大家浏览项目,我把编译后的wxss文件也一并上传了,大家打开就能预览,如果觉得好,希望大家都去点下star哈,谢谢大家。。。
2020-04-23 - html-canvas 生成小程序分享图
简介 基于 HTML 和 CSS 实现 Canvas 绘图。 项目地址 代码片段:https://developers.weixin.qq.com/s/9zFHKdmh7De2 原理 构建虚拟DOM 树,依据 CSS 规范计算样式,使用 CSS 盒模型对 DOM 进行布局,计算出所有元素的位置。最后将 DOM 树通过 Canvas Api 进行绘制。 小程序开发工具内运行 demo [代码]git clone https://github.com/alexayan/html-canvas.git npm i npm run watch [代码] 已支持的 CSS 属性 margin,margin-left,margin-top,margin-right,margin-bottom,padding,padding-left,padding-top,padding-right,padding-bottom,width,height,border,border-left,border-top,border-right,border-bottom,border-width,border-style,border-color,border-left-style,border-left-color,border-left-width,border-top-style,border-top-color,border-top-width,border-right-style,border-right-color,border-right-width,border-bottom-style,border-bottom-color,border-bottom-width,color,display,background-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-left-radius,border-bottom-right-radius,box-sizing,font,font-style,font-variant,font-weight,font-stretch,font-size,line-height,font-family,text-align,position,overflow,overflow-x,overflow-y,top,left,right,bottom,z-index demo canvas-draw.html [图片]
2020-01-08 - 生成小程序码接口返回一串乱码,怎么转换成图片或base64呢?
小程序接口返回的数据是一串乱码,如图所示,怎么转换成base64或者图片呢?目前采取的方法: 请求时设置responseType: "arraybuffer",拿到了二进制数据流,再使用wx.arrayBufferToBase64(data),转换出来的图片不显示。。。很奇怪,该怎么解决呢? [图片] [图片] [图片]
2019-09-05 - mina-lazy-image: 图片懒加载自定义组件
Github: https://github.com/alexayan/mina-lazy-image 功能 图片在视口中出现才进行加载显示,优化页面性能 使用方法 安装组件 [代码]npm install --save mina-lazy-image [代码] 在页面的 json 配置文件中添加 mina-lazy-image 使用此组件需要依赖小程序基础库 2.2.2 版本,同时依赖开发者工具的 npm 构建。具体详情可查阅官方 npm 文档。 [代码]{ "usingComponents": { "mina-lazy-image": "mina-lazy-image/index" } } [代码] WXML 文件中引用 mina-lazy-image [代码]<mina-lazy-image src="{{src}}" mode="widthFIx" image-class="custom-class-name"/> [代码] mina-lazy-image 的属性介绍如下: 字段名 类型 必填 描述 src String 是 图片链接 placeholder String 否 占位图片链接 mode String 否 请参考 image 组件 mode 属性 webp Number 否 请参考 image 组件 webp 属性 showMenuByLongpress Boolean 否 请参考 image 组件 show-menu-by-longpress 属性 styles String 否 设置图片样式 viewport Object 否 默认为 {bottom: 0},配置图片显示区域 mina-lazy-image 外部样式类 [代码]image-class[代码], [代码]image-container-class[代码]
2020-01-09 - 云开发实战-如何维护用户表?(优化版)
前言 之前写过一篇《云开发-如何维护用户表?》,这种方式是最简单的,经过阅读了一些开源项目的代码,我优化了部分写法。 对比 优化前实现思路: 通过 login 云函数获取 openid 存放到本地 在授权信息的时候去添加 userInfo 根据 openid 去查询是否已经存储 没有查到就是新用户进行添加并存放 id 到本地 查看后老用户就根据 id 进行更新信息 优化后实现思路: 在 app.js 通过 queryCurrentUser 云函数查询 openid 是否在用户表 得到状态后存放在 app.js 的全局变量 authorized 属性里面 当需要用户授权的时候判断状态,没有就跳转到授权页面 进行授权调用 authorize 云函数添加用户 代码 在 app.js 通过 queryCurrentUser 云函数查询 openid 是否在用户表 得到状态后存放在 app.js 的全局变量 authorized 属性里面。 [代码]wx.cloud.callFunction({ // 云函数名称 name: 'user', // 传给云函数的参数 data: { action: 'queryCurrentUser' } }).then(res => { if (res.result.errMsg === 'user.query.ok') { this.onAuthorized(res.result.data.userInfo); this.authorized = true; } wx.hideLoading(); }) onAuthorized(userInfo) { this.authorized = true; this.globalData.userInfo = userInfo; }, [代码] queryCurrentUser 云函数 [代码]async queryCurrentUser(context, params) { const { OPENID } = context; let res = await db.collection('users').where({ openid: OPENID }).get(); if (res.data.length === 0) { return { errMsg: 'user.query.none' }; } return { errMsg: 'user.query.ok', data: { userInfo: res.data[0].userInfo } }; }, [代码] 当需要用户授权的时候判断状态,没有就跳转到授权页面 index.js [代码]toInfo(res) { if (app.authorized !== true) { wx.navigateTo({ url: '/pages/authorize/authorize' }); return; } // 省略业务代码.... } [代码] 进行授权调用 authorize 云函数添加用户 authorize.js [代码]wx.cloud.callFunction({ // 云函数名称 name: 'user', // 传给云函数的参数 data: { action: 'authorize', userInfo: userInfo } }).then(res => { if (res.result.errMsg === 'user.authorize.ok' || res.result.errMsg === 'user.authorize:authorized') { app.onAuthorized(res.result.data.userInfo); wx.showLoading({ title: '授权成功' }); setTimeout(() => { wx.hideLoading(); app.navigateBack(); }, 1000); return; } wx.nextTick(() => { wx.showToast({ title: '授权失败', icon: 'none', duration: 1000 }); }); }); [代码] authorize 云函数 [代码]const authorizedRes = { env: cloud.DYNAMIC_CURRENT_ENV, errMsg: 'user.authorize:authorized' }; async authorize(context, params) { const { OPENID } = context; let getRes = await db.collection('users').where({ openid: OPENID }).get(); if (getRes.errMsg !== 'collection.get:ok') { return errorAuthorizeRes; } if (getRes.data.length > 0) { return authorizedRes; } let addRes = await db.collection('users').add({ data: { openid: OPENID, userInfo: params.userInfo, authorizedTime: new Date(), } }); return { errMsg: 'user.authorize.ok', data: { userInfo: params.userInfo } }; } [代码] 总结 这种方式优点如下: 用云函数来验证,云函数可以直接获取 openid 通用统一的授权页面进行授权,这样就不需要在不同的地方写同样的授权代码 添加逻辑在云函数中实现,改小程序前端代码需要重新发版,云函数部署就行 代码需要不断优化才能更好。
2020-09-16 - 云开发经验总结(展示两种增删改查的方法)
开发工具mpvue官方文档 云开发初始化[代码] [代码] [代码] wx.cloud.init({[代码] [代码] [代码][代码]env: [代码][代码]'wedding-10c111'[代码][代码] })[代码] [代码] [代码] 上面这段代码配置在src目录下的main.js文件中 数据库API(不使用云函数进行增删改查)以下说明均写在对应代码注释里,不清楚的请查看相关注释 查(获取数据)[代码] [代码] [代码] // 获取轮播图列表[代码] [代码] getBannerList () {[代码][代码] [代码][代码]// 获取数据库引用[代码][代码] [代码][代码]const db = wx.cloud.database()[代码][代码] [代码][代码]// 获取名为“banner”的集合引用[代码][代码] [代码][代码]const banner = db.collection([代码][代码]'banner'[代码][代码])[代码][代码] [代码][代码]// 获取集合(Promise 风格)[代码][代码] [代码][代码]banner.get().then(res => {[代码][代码] [代码][代码]this[代码][代码].list = res.data[0].bannerList[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 对应实例如下: [图片] 注意:之所以数据库只有一条数据,而把banner列表当成这条数据的一个字段存储,其目的是为了自己后续换图操作的方便 增(添加数据)[代码][代码][代码] [代码] [代码] // 添加用户[代码] [代码] addUser () {[代码][代码] [代码][代码]// 获取数据库引用[代码][代码] [代码][代码]const db = wx.cloud.database()[代码][代码] [代码][代码]// 获取名为“user”的集合引用[代码][代码] [代码][代码]const user = db.collection([代码][代码]'user'[代码][代码])[代码][代码] [代码][代码]// 向“user”集合中添加一条数据(Promise 风格)[代码][代码] [代码][代码]user.add({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]user: that.userInfo,[代码][代码] [代码][代码]// 构造一个服务端时间的引用,我的项目中都是取自己转化后的时间,[代码][代码] [代码][代码]// 取这个时间更加合理,可用于查询条件、更新字段值或新增记录时的字段值[代码][代码] [代码][代码]time: db.serverDate()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]// 添加成功后重新查询列表[代码][代码] [代码][代码]that.getUserList()[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 对应实例如下: [图片] 注意: 可以看出_id和_openid是添加完自动生成的属性 改(修改数据)[代码] [代码] [代码] // 改变某条留言的显示隐藏[代码] [代码] switchMessage (e) {[代码][代码] [代码][代码]// 获取数据库的引用[代码][代码] [代码][代码]const db = wx.cloud.database()[代码][代码] [代码][代码]// 获取名为“message”的集合的引用[代码][代码] [代码][代码]const message = db.collection([代码][代码]'message'[代码][代码])[代码][代码] [代码][代码]// 这里的id是拿到当前操作项对应的id,[代码][代码] [代码][代码]// 这里的show对应change事件传递过来的值[代码][代码] [代码][代码]message.doc(e.mp.target.dataset.id).update({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]show: e.mp.detail.value[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]console.log(res)[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 对应实例如下: [图片] 注意:这个界面在你们使用的小程序中是看不到的,只有本人才有权限查看 [图片] [代码] [代码] [代码] <[代码][代码]switch[代码] [代码]class[代码][代码]=[代码][代码]"switch"[代码] [代码]:data-id[代码][代码]=[代码][代码]"item._id"[代码] [代码]:checked[代码][代码]=[代码][代码]"item.show"[代码] [代码]@[代码][代码]change[代码][代码]=[代码][代码]"switchMessage"[代码][代码]></[代码][代码]switch[代码][代码]>[代码] [代码] [代码] 注意:上面我们之所以能得到e.mp.target.dataset.id是因为在<switch>标签上加了`:data-id="item._id"`,不然取不到对应id 删(删除数据)正好对应的上图有删除操作 [代码] [代码] [代码] deleteItem (id) {[代码] [代码] [代码][代码]// 记录this指向[代码][代码] [代码][代码]const that = [代码][代码]this[代码][代码] [代码][代码]// 这里之所以使用wx.showModal是防止误操作[代码][代码] [代码][代码]wx.showModal({[代码][代码] [代码][代码]title: [代码][代码]'提示'[代码][代码],[代码][代码] [代码][代码]content: [代码][代码]'你确定要删除这条留言?'[代码][代码],[代码][代码] [代码][代码]success (res) {[代码][代码] [代码][代码]if[代码] [代码](res.confirm) {[代码][代码] [代码][代码]// 获取数据库的引用[代码][代码] [代码][代码]const db = wx.cloud.database()[代码][代码] [代码][代码]// 获取名为“message”集合的引用[代码][代码] [代码][代码]const message = db.collection([代码][代码]'message'[代码][代码])[代码][代码] [代码][代码]// 删除操作(Promise 风格)[代码][代码] [代码][代码]message.doc(id).remove().then(res => {[代码][代码] [代码][代码]// 删除成功后再次请求列表,达到刷新数据的目的[代码][代码] [代码][代码]if[代码] [代码](res.errMsg === [代码][代码]'document.remove:ok'[代码][代码]) {[代码][代码] [代码][代码]that.getList()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 使用云函数进行增删改查 查(获取数据)[代码] [代码] [代码] // 云函数初始化[代码] [代码] const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码][代码] // 由于文章开始已经讲过初始化步骤,这里init(options)的options可以省略[代码][代码] // options参数定义了云开发的默认配置,该配置会作为之后调用其他所有云 API 的默认配置[代码][代码] cloud.init()[代码][代码] // 获取数据库的引用[代码][代码] const db = cloud.database()[代码][代码] exports.main = async (event, context) => {[代码][代码] [代码][代码]// 将集合名定义成一个变量,方便后续调用[代码][代码] [代码][代码]const dbName = [代码][代码]'message'[代码][代码] [代码][代码]// filter为指定的筛选条件,配合where()使用[代码][代码] [代码][代码]const filter = event.filter ? event.filter : [代码][代码]null[代码][代码] [代码][代码]// pageNum如果小程序端未传入则默认为1[代码][代码] [代码][代码]const pageNum = event.pageNum ? event.pageNum : 1[代码][代码] [代码][代码]// pageSize如果小程序端未传入则默认是10[代码][代码] [代码][代码]const pageSize = event.pageSize ? event.pageSize : 10[代码][代码] [代码][代码]// 数据库满足filter条件的数据总条数[代码][代码] [代码][代码]const countResult = await db.collection(dbName).where(filter).count()[代码][代码] [代码][代码]const total = countResult.total[代码][代码] [代码][代码]// 共多少页[代码][代码] [代码][代码]const totalPage = Math.ceil(total / pageSize)[代码][代码] [代码][代码]// 是否有下一页[代码][代码] [代码][代码]let hasMore[代码][代码] [代码][代码]if[代码] [代码](pageNum >= totalPage) {[代码][代码] [代码][代码]hasMore = [代码][代码]false[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]hasMore = [代码][代码]true[代码][代码] [代码][代码]}[代码][代码] [代码][代码]// 等待所有,orderBy()通过创建时间排序,查询单页数据[代码][代码] [代码][代码]return[代码] [代码]db.collection(dbName).orderBy([代码][代码]'time'[代码][代码], [代码][代码]'desc'[代码][代码]).where(filter).skip((pageNum - 1) * pageSize).limit(pageSize).get().then(res => {[代码][代码] [代码][代码]// 返回结果中顺带注入hasMore和total方便小程序端判断[代码][代码] [代码][代码]res.hasMore = hasMore[代码][代码] [代码][代码]res.total = total[代码][代码] [代码][代码]return[代码] [代码]res[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] [代码] [代码] [代码] getList () {[代码] [代码] [代码][代码]// 记录this指向[代码][代码] [代码][代码]const that = [代码][代码]this[代码][代码] [代码][代码]// 每次调用getList时重新从第一页开始[代码][代码] [代码][代码]that.pageNum = 1[代码][代码] [代码][代码]// 每次调用getList时,先将authorityList置空[代码][代码] [代码][代码]that.authorityList = [][代码][代码] [代码][代码]wx.cloud.callFunction({[代码][代码] [代码][代码]// 云函数名[代码][代码] [代码][代码]name: [代码][代码]'authorityList'[代码][代码],[代码][代码] [代码][代码]// 传入云函数的参数[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]// 查询的默认筛选条件,这里可以参考下面留言审核对应的两张图来看,左上角有个switch开关[代码][代码] [代码][代码]// 当开关开启时,filter:{show:false}生效[代码][代码] [代码][代码]filter: that.checkFlag ? {[代码][代码] [代码][代码]show: [代码][代码]false[代码][代码] [代码][代码]} : [代码][代码]null[代码][代码],[代码][代码] [代码][代码]// 查询页数[代码][代码] [代码][代码]pageNum: that.pageNum,[代码][代码] [代码][代码]// 每页条数[代码][代码] [代码][代码]pageSize: that.pageSize[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]// 配合下拉刷新使用,作用是停止刷新事件[代码][代码] [代码][代码]wx.stopPullDownRefresh()[代码][代码] [代码][代码]// 以下动作为赋值操作[代码][代码] [代码][代码]const temp = res.result[代码][代码] [代码][代码]that.total = temp.total[代码][代码] [代码][代码]that.hasMore = temp.hasMore[代码][代码] [代码][代码]that.authorityList = temp.data[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 上面代码对应实例如下:1.查询未通过审核的留言;2.查询全部的留言 [图片][图片] 增(添加数据)[代码] [代码] [代码] // 前面讲解过的注释之后的代码将不重复说明[代码] [代码] const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码][代码] cloud.init()[代码][代码] const db = cloud.database()[代码][代码] exports.main = async (event, context) => {[代码][代码] [代码][代码]const dbName = [代码][代码]'message'[代码][代码] [代码][代码]// 添加数据[代码][代码] [代码][代码]return[代码] [代码]db.collection(dbName).doc(event.id).add({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]desc: event.desc,[代码][代码] [代码][代码]type: event.type,[代码][代码] [代码][代码]show: event.show,[代码][代码] [代码][代码]time: event.time,[代码][代码] [代码][代码]url: event.url,[代码][代码] [代码][代码]name: event.name[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] [代码][代码][代码] [代码] [代码] sendMessage () {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码] [代码][代码]if[代码] [代码](that.desc) {[代码][代码] [代码][代码]wx.cloud.callFunction({[代码][代码] [代码][代码]// 云函数名[代码][代码] [代码][代码]name: [代码][代码]'addMessage'[代码][代码],[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]desc: that.desc,[代码][代码] [代码][代码]type: [代码][代码]'message'[代码][代码],[代码][代码] [代码][代码]show: [代码][代码]false[代码][代码],[代码][代码] [代码][代码]time: utils.getNowFormatDate(),[代码][代码] [代码][代码]url: that.userInfo.avatarUrl,[代码][代码] [代码][代码]name: that.userInfo.nickName[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]// 关闭所有页面,打开到应用内的某个页面,跳转到留言列表页[代码][代码] [代码][代码]wx.reLaunch({[代码][代码] [代码][代码]url: [代码][代码]'/pages/message/main'[代码][代码] [代码][代码]})[代码][代码] [代码][代码]})[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]tools.showToast([代码][代码]'说点什么吧~'[代码][代码])[代码][代码] [代码][代码]}[代码][代码] }[代码] [代码] [代码] [代码][代码] 对应实例如下: [图片][图片] 改(修改数据)[代码] [代码] [代码] const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码] [代码] cloud.init()[代码][代码] const db = cloud.database()[代码][代码] exports.main = async (event, context) => {[代码][代码] [代码][代码]const dbName = [代码][代码]'message'[代码][代码] [代码][代码]return[代码] [代码]db.collection(dbName).doc(event.id).update({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]show: event.show[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] [代码] [代码] [代码] switchMessage (e) {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码] [代码][代码]wx.cloud.callFunction({[代码][代码] [代码][代码]name: [代码][代码]'switchMessage'[代码][代码],[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]id: e.mp.target.dataset.id,[代码][代码] [代码][代码]show: e.mp.detail.value[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]if[代码] [代码](res.result.errMsg === [代码][代码]'document.update:ok'[代码][代码]) {[代码][代码] [代码][代码]that.getList()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 对应实例如下:(前面没使用云函数也实现了相同的功能,感兴趣的可以对比查阅) [图片] 删(删除数据)[代码] [代码] [代码] const cloud = require([代码][代码]'wx-server-sdk'[代码][代码])[代码] [代码] cloud.init()[代码][代码] const db = cloud.database()[代码][代码] exports.main = async (event, context) => {[代码][代码] [代码][代码]const dbName = [代码][代码]'message'[代码][代码] [代码][代码]return[代码] [代码]db.collection(dbName).doc(event.id).remove()[代码][代码] }[代码] [代码] [代码] [代码] [代码] [代码] deleteItem (id) {[代码] [代码] [代码][代码]// 记录this指向[代码][代码] [代码][代码]const that = [代码][代码]this[代码][代码] [代码][代码]// 这里之所以使用wx.showModal是防止误操作[代码][代码] [代码][代码]wx.showModal({[代码][代码] [代码][代码]title: [代码][代码]'提示'[代码][代码],[代码][代码] [代码][代码]content: [代码][代码]'你确定要删除这条留言?'[代码][代码],[代码][代码] [代码][代码]success (res) {[代码][代码] [代码][代码]if[代码] [代码](res.confirm) {[代码][代码] [代码][代码]wx.cloud.callFunction({[代码][代码] [代码][代码]name: [代码][代码]'deleteMessage'[代码][代码],[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]id[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}).then(res => {[代码][代码] [代码][代码]if[代码] [代码](res.result.errMsg === [代码][代码]'document.remove:ok'[代码][代码]) {[代码][代码] [代码][代码]that.getList()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] }[代码] [代码] [代码] 对应实例如下: [图片] 总结掌握上面两种对应的增删改查后,相信大家对云开发会有一个更清晰的认识,也希望大家多多使用云开发做出更多好玩的小程序作品; 如果觉得看完这篇文章让你有想尝试使用云开发的冲动,请不要吝啬你的赞,有什么问题欢迎留言,一起交流学习。 对应小程序 欢迎大家体验: [图片] 日记小程序 [图片] 小程序订制加本人微信:huangbin910419 可按给定UI图订制
2019-11-06