- canvas 无法生成长图?模拟器正常生成 超过4000 ios截屏放大 安卓白屏 是逻辑问题
<canvas id="canvas" type="2d" canvas-id="canvas" style="height:{{canvasHeight}}rpx"></canvas> <canvas id="canvas1" type="2d" canvas-id="canvas1" style="height:{{canvasHeight}}rpx"></canvas> // 画布绘制&初始化画布 drawCanvas(dom, name) { return new Promise((resolve, reject) => { const query = wx.createSelectorQuery().in(this) query.select(dom) .fields({ node: true, size: true }) .exec(res => { const data = res[0]; let canvas = data.node this._data[name] = canvas; const ctx = canvas.getContext('2d') canvas.width = data.width * dpr canvas.height = data.height * dpr // 绘制前清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.scale(dpr, dpr) resolve() console.log('init canvas', canvas.width, canvas.height) }) }) }, // 绘制内容 async drawContent(canvas, type = 1) { console.log('drawing content') console.time() const { checkListMap } = this._data; const ctx = canvas.getContext('2d') ctx.fillStyle = '#121C60'; ctx.fillRect(0, 0, canvas.width, canvas.height); await this.drawImage(canvas, ctx, '/assets/workbench/w-joint-research-banner.png', 0, 0, 750 / ratio, 308 / ratio) this._data.yHeight = 308; this.drawTitle(ctx, `03.05 ~ 03.11|江浙|电新 第${type}张`) for (let key in checkListMap) { const { day, week, data } = checkListMap[key] this.drawTag(ctx, `${day} ${week}`, this._data.yHeight + 30); await data.forEach(item => { const { dayShow: time, record_name: title, other } = item; this.drawCard(ctx, { time, title, address: other.address, person: '', discuss: '' }, this._data.yHeight + 30, type) }) } this.drawFooter(ctx, this._data.yHeight) console.log('draw canvas', checkListMap) console.timeEnd(); console.log('draw content end') }, // 绘制图片 draw(dom, id) { return new Promise((resolve, reject) => { const { canvasHeight: cnavasheight } = this.data const { yHeight, canvasHeight } = this._data // const dpr = wx.getSystemInfoSync().pixelRatio // 是否超过一屏 const isBeyond = yHeight > canvasHeight const height = isBeyond ? cnavasheight : canvasHeight // console.log('height', height, dom.height, dom.height / dpr) wx.canvasToTempFilePath({ canvas: dom, // canvasId: id, width: 750 / ratio, height: 3000, // destWidth: dom.width, // destHeight: dom.height, success: (res) => { resolve(res.tempFilePath) wx.getImageInfo({ src: res.tempFilePath, success: (res) => { console.log('getImageInfo', res) } }) console.log('success', res) }, fail: (err) => { reject(err) }, complete: (res) => { // resolve(res.tempFilePath) console.log('complete', res) } }, this) }) }, // 生成海报 generatePoster() { wx.showLoading({ title: '生成中...', }) // this.draw(this._data.canvas, 'canvas') console.log('----------------') Promise.all([this.draw(this._data.canvas1, 'canvas1')]).then(res => { wx.previewImage({ current: res[0], // 当前显示图片的http链接 urls: res // 需要预览res.tempFilePath的图片http链接列表 }) wx.hideLoading() }) }, [图片]
2023-03-27 - 使用 movable-area 和 movable-view 实现小程序图片裁剪功能
需要做一个页面以实现图片按照指定比例缩放和裁切的功能。然后发现微信小程序刚好自带 movable-area/movable-view 组件,可以实现缩放和平移等功能。 以受控方式使用 movable-view 组件是真的坑。但终于成功了… 先放一下图片裁剪界面截图。实现了拖动和缩放裁剪图片,以及图片被拖动到裁剪边界之外时自动吸附至边界的功能。图片非本人 _(:3… 被裁剪部分外围的白色边框和半透明黑色遮罩层是使用 movable-area 的 :before 伪元素的 box-shadow 属性实现的。 点 Done 按钮后返回前一页,并且在控制台输出 { x, y, w, h } 形式的图片裁剪信息。进入图片裁剪页面时,通过 URL 参数传入图片地址和裁剪比例。 [图片] 指向代码片段的链接: https://developers.weixin.qq.com/s/0Vk2Zsmr7ggJ JS: Page({ data: { image: "", ratio: 1, imageRatio: 1, x: 0, y: 0, scale: 1, ready: false, animating: false }, onLoad: function (queryParams) { this.imageWidth = 0; this.imageHeight = 0; this.currentChange = null; this.currentScale = null; this.touchLock = false; this.isDone = false; this.initLock = true; this.image = queryParams.image; this.ratio = +queryParams.ratio; }, onReady: function () { var that = this, image = this.image, ratio = this.ratio; wx.getImageInfo({ src: image, success: function (obj) { that.imageWidth = obj.width; that.imageHeight = obj.height; var imageRatio = obj.width / obj.height; that.setData({ image: that.image, ratio: ratio, imageRatio: imageRatio }, function () { var query = wx.createSelectorQuery(); query.select("#crop").boundingClientRect(); query.select("#move").boundingClientRect(); query.exec(function (arr) { var bcr = arr[0]; that.setData({ x: imageRatio > ratio ? bcr.width * (.5 - imageRatio / ratio) : -bcr.width * .5, y: imageRatio > ratio ? -bcr.height * .5 : bcr.height * (.5 - ratio / imageRatio), scale: .5 }); setTimeout(function () { that.initLock = false; that.setData({ ready: true }); }, 20); }); that.query = query; }); } }); }, done: function () { var that = this; this.query.exec(function (bcrs) { var inner = bcrs[0], outer = bcrs[1], leftRatio = (inner.left - outer.left) / outer.width, topRatio = (inner.top - outer.top) / outer.height, scaleX = inner.width / outer.width, scaleY = inner.height / outer.height; if (leftRatio < 0) { leftRatio = 0; } else if (leftRatio + scaleX > 1) { leftRatio = 1 - scaleX; } if (topRatio < 0) { topRatio = 0; } else if (topRatio + scaleY > 1) { topRatio = 1 - scaleY; } var x = Math.round(that.imageWidth * leftRatio), y = Math.round(that.imageHeight * topRatio), w = that.imageWidth * scaleX, h = Math.round(w / that.data.ratio); that.isDone = true; var ec = that.getOpenerEventChannel(); ec.emit('crop', { x: x, y: y, w: Math.round(w), h: h }); wx.navigateBack(); }); }, detached: function () { var ec = this.getOpenerEventChannel(); if (!this.isDone) { ec.emit('cropcancel', null); } }, handleChange: function (e) { if (this.initLock) { return; } if ('x' in e.detail) { this.currentChange = e.detail; } if ('scale' in e.detail) { this.currentScale = e.detail.scale; } }, processChange: function () { var that = this; this.query.exec(function (bcrs) { var inner = bcrs[0], outer = bcrs[1], deltax = 0, deltay = 0; if (inner.left < outer.left) { deltax = inner.left - outer.left; } else if (inner.right > outer.right) { deltax = inner.right - outer.right; } if (inner.top < outer.top) { deltay = inner.top - outer.top; } else if (inner.bottom > outer.bottom) { deltay = inner.bottom - outer.bottom; } if ((deltax || deltay) && that.currentChange) { that.setData({ x: that.currentChange.x + deltax + .01 * Math.random() - .005, y: that.currentChange.y + deltay + .01 * Math.random() - .005, scale: that.currentScale || that.data.scale, animating: deltax * deltax + deltay * deltay > 100 }); setTimeout(() => { that.setData({ animating: false }); }, 100); } that.currentChange = null; }); } }); WXML: <movable-area class="crop" id="crop" style="height:{{80/ratio}}vw"> <movable-view class="crop__view" direction="all" scale id="move" x="{{x}}" y="{{y}}" animation="{{animating || scale>.99}}" scale-max="1" scale-value="{{scale}}" damping="160" out-of-bounds bindscale="handleChange" bindchange="handleChange" bindtouchend="processChange" bindtouchcancel="processChange" style="{{imageRatio>ratio?'width:'+imageRatio*200/ratio+'%;height:200%':'width:200%;height:'+ratio*200/imageRatio+'%'}}" > <image src="{{image}}" class="crop__image" /> </movable-view> </movable-area> <button class="done" bindtap="done">Done</button> <view class="loading-cover {{ready ? 'loading-cover--hidden' : ''}}">Loading</view> 以上。
2020-04-08 - canvasToTempFilePath部分ios手机导出图片裁剪?
真机截图(正常): [图片] iphone 7中截图(图片发生了裁剪): [图片] 核心代码片段(具体见代码片段:https://developers.weixin.qq.com/s/fUJ4d3m17UxT): <view class="page"> <canvas type="2d" style="width:{{canvasWidth}}px;height:{{canvasHeight}}px;" id="myCanvas" class="myCanvas" ></canvas> <image src="{{imageSrc}}" mode="widthFix"></image> </view> const app = getApp() Page({ data: { imageSrc: "", canvasWidth: '', canvasHeight: '', imageSrc:"" }, onLoad: async function () { let imageSrc = './1.jpg'; let imageInfo = await this.getImageInfo(imageSrc); let r = 2.165914247136675; let canvasWidth = Math.abs(imageInfo.width / r); let canvasHeight = Math.abs(imageInfo.height / imageInfo.width*canvasWidth); this.setData({ canvasWidth, canvasHeight },async ()=>{ let { canvas, context } = await this.initCanvas('#myCanvas', 1); let img = await this.createImage(canvas, imageSrc); context.drawImage(img, 0, 0, imageInfo.width, imageInfo.height, 0, 0, canvasWidth, canvasHeight) let src = await wx.canvasToTempFilePath({ x: 0, y: 0, width:canvasWidth, height:canvasHeight, destWidth:canvasWidth, destHeight:canvasHeight, canvas, success:(res)=>{ this.setData({ imageSrc:res.tempFilePath }) } }) }) }, getImageInfo(src) { return new Promise(async (resolve, reject) => { wx.getImageInfo({ src, success: (result) => { resolve(result); }, fail: (e) => { reject(e); } }); }); }, initCanvas(selector, dpr) { return new Promise((resolve, reject) => { wx.createSelectorQuery() .select(selector) .fields({ node: !0, size: !0 }) .exec((res) => { let canvasWidth = res[0].width; let canvasHeight = res[0].height; let canvas = res[0].node; let context = canvas.getContext('2d'); canvas.width = canvasWidth * dpr; canvas.height = canvasHeight * dpr; context.scale(dpr, dpr); resolve({ context, canvas, canvasWidth, canvasHeight }); }); }); }, createImage(ctxNode, imageSrc) { return new Promise((resolve, reject) => { const img = ctxNode.createImage(); img.src = imageSrc; img.onload = () => { resolve(img); }; }); } }) drawImage这一步是正常的,就是canvasToTempFilePath的时候发生了裁剪
2022-02-14 - 小程序图片裁剪插件 image-cropper
之前的插件类目没有了导致搜不到了,重新发个文章。 image-cropper 一款高性能的小程序图片裁剪插件,支持旋转。 [图片] 优势 [代码]1.功能强大。[代码] [代码]2.性能超高超流畅,大图毫无卡顿感。[代码] [代码]3.组件化,使用简单。[代码] [代码]4.点击中间窗口实时查看裁剪结果。[代码] ㅤ 初始准备 1.json文件中添加image-cropper [代码] "usingComponents": { "image-cropper": "../image-cropper/image-cropper" }, "navigationBarTitleText": "裁剪图片", "disableScroll": true [代码] 2.wxml文件 [代码]<image-cropper id="image-cropper" limit_move="{{true}}" disable_rotate="{{true}}" width="{{width}}" height="{{height}}" imgSrc="{{src}}" bindload="cropperload" bindimageload="loadimage" bindtapcut="clickcut"></image-cropper> [代码] 3.简单示例 [代码] Page({ data: { src:'', width:250,//宽度 height: 250,//高度 }, onLoad: function (options) { //获取到image-cropper实例 this.cropper = this.selectComponent("#image-cropper"); //开始裁剪 this.setData({ src:"https://raw.githubusercontent.com/1977474741/image-cropper/dev/image/code.jpg", }); wx.showLoading({ title: '加载中' }) }, cropperload(e){ console.log("cropper初始化完成"); }, loadimage(e){ console.log("图片加载完成",e.detail); wx.hideLoading(); //重置图片角度、缩放、位置 this.cropper.imgReset(); }, clickcut(e) { console.log(e.detail); //点击裁剪框阅览图片 wx.previewImage({ current: e.detail.url, // 当前显示图片的http链接 urls: [e.detail.url] // 需要预览的图片http链接列表 }) }, }) [代码] 参数说明 属性 类型 缺省值 取值 描述 必填 imgSrc String 无 无限制 图片地址(如果是网络图片需配置安全域名) 否 disable_rotate Boolean false true/false 禁止用户旋转(为false时建议同时设置limit_move为false) 否 limit_move Boolean false true/false 限制图片移动范围(裁剪框始终在图片内)(为true时建议同时设置disable_rotate为true) 否 width Number 200 超过屏幕宽度自动转为屏幕宽度 裁剪框宽度 否 height Number 200 超过屏幕高度自动转为屏幕高度 裁剪框高度 否 max_width Number 300 裁剪框最大宽度 裁剪框最大宽度 否 max_height Number 300 裁剪框最大高度 裁剪框最大高度 否 min_width Number 100 裁剪框最小宽度 裁剪框最小宽度 否 min_height Number 100 裁剪框最小高度 裁剪框最小高度 否 disable_width Boolean false true/false 锁定裁剪框宽度 否 disable_height Boolean false true/false 锁定裁剪框高度 否 disable_ratio Boolean false true/false 锁定裁剪框比例 否 export_scale Number 3 无限制 输出图片的比例(相对于裁剪框尺寸) 否 quality Number 1 0-1 生成的图片质量 否 cut_top Number 居中 始终在屏幕内 裁剪框上边距 否 cut_left Number 居中 始终在屏幕内 裁剪框左边距 否 [代码]img_width[代码] Number 宽高都不设置,最小边填满裁剪框 支持%(不加单位为px)(只设置宽度,高度自适应) 图片宽度 否 [代码]img_height[代码] Number 宽高都不设置,最小边填满裁剪框 支持%(不加单位为px)(只设置高度,宽度自适应) 图片高度 否 scale Number 1 无限制 图片的缩放比 否 angle Number 0 (limit_move=true时angle=n*90) 图片的旋转角度 否 min_scale Number 0.5 无限制 图片的最小缩放比 否 max_scale Number 2 无限制 图片的最大缩放比 否 bindload Function null 函数名称 cropper初始化完成 否 bindimageload Function null 函数名称 图片加载完成,返回值Object{width,height,path,type等} 否 bindtapcut Function null 函数名称 点击中间裁剪框,返回值Object{src,width,height} 否 函数说明 函数名 参数 返回值 描述 参数必填 upload 无 无 调起wx上传图片接口并开始剪裁 否 pushImg src 无 放入图片开始裁剪 是 getImg Function(回调函数) [代码]Object{url,width,height}[代码] 裁剪并获取图片(图片尺寸 = 图片宽高 * export_scale) 是 setCutXY X、Y 无 设置裁剪框位置 是 setCutSize width、height 无 设置裁剪框大小 是 setCutCenter 无 无 设置裁剪框居中 否 setScale scale 无 设置图片缩放比例(不受min_scale、max_scale影响) 是 setAngle deg 无 设置图片旋转角度(带过渡效果) 是 setTransform {x,y,angle,scale,cutX,cutY} 无 图片在原有基础上的变化(scale受min_scale、max_scale影响) 根据需要传参 imgReset 无 无 重置图片的角度、缩放、位置(可以在onloadImage回调里使用) 否 GitHub https://github.com/wx-plugin/image-cropper/tree/master 如果有什么好的建议欢迎提issues或者提pr
2021-12-15 - SAUI-table组件
前提:现成的框架里,table的组件,都是很简单的。遇到复杂的业务时,很多都是不满足的。 - 支持表头和表格body分组(自定义配置) - 支持表格左右滑动(可自定义配置表格固定列) - 支持点击事件 /*==================mock start =====================*/ //简单的数据结构 const headData = [ { title: '截止日期', key: 'time', hasClick: true}, { title: '是否经过审计', key: 'isSJ', hasClick: true}, { title: '审计日期', key: 'sjTime', align: 'right', hasClick: true}, ] const data = [ {time: '2020/3/31', isSJ: '未审计', sjTime: '2020/02/02'}, {time: '2020/3/31', isSJ: '已审计', sjTime: '2020/02/02'}, {time: '2020/3/31', isSJ: '未审计', sjTime: '2020/02/02'}, ] // 固定列,左右滚动的table,可点击修改 const data2 = [ {name: '财务报表截止日期', column1: '2020/01/02', column2: '2020/01/02', column3: '2020/01/02', column4: '2020/01/02'}, {name: '是否经过审计', column1: '未审计', column2: '未审计', column3: '已审计', column4: '已审计'}, {name: '审计日期', column1: '2020/01/02', column2: '2020/01/02', column3: '2020/01/02', column4: '2020/01/02'}, {name: '财务报表类型', column1: '1', column2: '1', column3: '2', column4: '2'}, ] const headData2 = [ { title: { '@item': { title: [ { title: '截止日期' }, { title: '指标' } ], titleClass: 'lineTd' } }, key: 'name', width: '260', sticky: true, }, { title: '2020/01/02', key: 'column1', width: '200', hasClick: true }, { title: '2020/01/03', key: 'column2', width: '200', hasClick: true }, { title: '2020/01/04', key: 'column3', width: '200', hasClick: true }, { title: '2020/01/05', key: 'column4', width: '200', hasClick: true }, ] const mock2 = { headData: ["2021-12-31", "2021-12-30", "2021-12-29", "2021-12-29"], data: [ { code: 'a', name: '经营活动产生的现金流量', value: [], isLevel1: true, isEdit: false, }, { code: 'a1', name: '销售商品', value: ['a1v', 'a2v', 'a3v', 'a4v'], isLevel1: false, }, { code: 'a2', name: '提供劳务收到的现金', value: ['a1v', 'a2v', '提供劳务收到的现金', 'a4v'], isLevel1: false, }, { code: 'a3', name: '提供劳务收到的现金', value: ['a1v', 'a2v', 'a3v', 'a4v'], isLevel1: false, }, { code: 'b', name: '经营活动产生的现金流量', value: ['', '', '', ''], isLevel1: true, isEdit: true, }, { code: 'b1', name: '销售商品', value: ['b1v', null, 'b3v', 'b4v'], isLevel1: false, }, { code: 'b2', name: '提供劳务收到的现金', value: ['b1v', 'b2v', 'b3v', 'b4v'], isLevel1: false, }, { code: 'b3', name: '提供劳务收到的现金', value: ['b1v', 'b2v', 'b3v', 'b4v'], isLevel1: false, }, ] } /*==================mock end =====================*/ //tableInst, tableInst2 由aotoo封装的table组件调用方式(图一) 正常的table返回数据构成 //tableInst3, tableInst4 由wxml封装的table组件调用方式(图二) 正常的table返回数据构成 //tableInst5 纯wxml构成(图三) 非正常的table返回数据构成 Pager({ data: { tableInst: tableList({ headData: headData, data: data, tableClass: 'border mb-default', // isHideHeader: true, // unit: '%', autoWidth: true }), tableInst2: Pager.item({}), tableInst3: null, tableInst4: null, tableInst5: null, }, onReady() { const $tableInst2 = this.getElementsById('tableInst2') this.$modal = this.getElementsById('modal') this.vm = null const that = this const inst = tableList({ headData: headData2, data: data2, tableClass: 'auto border sticky mb-default', onItemClick(params) { that.vm = this // 组件内部的this that.openModal(params) }, }) $tableInst2.update(inst) this.setData({ tableInst3: { headData: headData, data: data, tableClass: 'border auto mb-default', }, tableInst4: { headData: headData3, data: data3, tableClass: 'border auto mb-default', onItemClick(params) { const {idx, key, value} = params const dt = this.data.data const abc = that.filterClas(dt, params) this.setData({data: abc}) that.vm1 = this that.openModal(params, 1) }, }, tableInst5: { headData: mock2.headData, data: mock2.data }, }) } }) 图一: [图片] 图二: [图片] 图三: [图片] 效果: [图片]
2022-04-02 - 微信小程序环境共享,多个小程序共享一个云开发数据库
我们在做小程序开发时,有时候需要多个小程序公用一个数据库,比如我们做一个外卖小程序,要配套一个骑手小程序,这个时候就要两个小程序公用一个云开发环境,公用一个数据库了。所以今天来教下大家如何多个小程序共享一个云开发环境和数据库。 其实官方给的文档很详细了,但是一个细节官方没有讲到,所以就会导致好多同学做多个小程序共享一个云开发环境时,遇到各种各样的问题。 比如下面这样的问题 [图片] 明明感觉自己按照官方要求,该配置的都配置了啊,但是为啥就是出错呢。所以我这里再带大家完整的配置一遍,把里面的一些注意事项也给大家好强调下。 一,准备条件 1-1,必须同一个主体 首先看官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/resource-sharing/ [图片] 要共享云开发资源可以 ,但是必须是同一个主体。什么是同一个主体呢,就是两个小程序必须都是你自己的,或者是你公司的。 如果不是同一个主体,会报如下错误 [图片] 1-2,最新的基础库,最新版开发工具 这里记得调到最新的基础库,开发者工具也尽量用最新的 [图片] 开发者工具这里官方是有要求的 [图片] 二,开通环境共享 我这里以两个小程序共享一个数据库为例 小程序A [图片] 小程序B [图片] 大家这里记得我们是小程序A 共享数据库给小程序B 2-1,开通环境共享 开通,使用 1.03.2009140 或以上版本的开发者工具,进入云控制台,到 “设置 - 拓展能力 - 环境共享” 点击开通即开通环境共享能力 [图片] 2-2,开通后授权给别的小程序 [图片] 环境共享开通后将在顶部tab显示环境共享功能,进入 “环境共享” 的页面,点击“添加共享”,即可授权同主体下其他小程序/公众号使用当前小程序下的云开发资源 [图片] 这里填写你要共享小程序的appid,我们这里取小程序B的appid [图片] 授权,选择共享的云环境,默认选中所有环境操作权限,可根据实际使用场景自定义授权。这里建议保持默认即可 [图片] 比如我这里分享给小程序B(编程小石头) [图片] [图片] 2-3,使用共享的云开发环境 我们上面操作好以后,就可以在小程序B的云开发后台看到共享的云开发环境了。将我们的云开发环境切换下就可以查看和使用共享的资源了。 [图片] 可以看到小程序B(编程小石头)可以查看小程序A的数据库了 [图片] 三,请求共享的数据库 我们接下来就在小程序B里调用小程序A的数据库了。官方提示的是调用之前要在小程序A里创建一个如下的云函数,但是我在测试的时候发现不用创建也可以的。 [图片] 所以我们就先不创建cloudbase_auth 云函数,来看看能不能调取到数据。 3-1,初始化云开发环境 我们小程序B想使用小程序A的云开发环境,这里要注意,初始化的时候要如下面注释里写的一样,用小程序A的appid和云开发环境id [图片] 3-2,调用资源方数据 初始化以后不能想正常调用云开发数据库那样了,会报错 [图片] 所以我们这里要改变下使用方法。如下 [图片] 这时候还会报错,是因为我们忽略了官方的一个要求:“ 跨账号调用,必须等待 init 完成”,所以我们必须给init加一个await语法,如下,记得await要结合着async一起使用。 [图片] 可以看到我们成功的请求到了小程序A的数据。直接get的时候记得改下数据库权限奥。 [图片] 代码贴出来给大家,记得改成自己的配置 [代码]Page({ async onLoad() { // 声明新的 cloud 实例 var c1 = new wx.cloud.Cloud({ // 资源方 小程序A的 AppID resourceAppid: 'wx7c54942dfc87f4d8', // 资源方 小程序A的 的云开发环境ID resourceEnv: 'test-ec396a', }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 await c1.init() // wx.cloud.database().collection('xiaoshitou').get() c1.database().collection('xiaoshitou').get() .then(res => { console.log('共享环境请求数据成功', res) }) } }) [代码] 四,调用共享环境的云函数 4-1,调用资源方里的云函数 我们这里在小程序B(编程小石头)里调用小程序A里的云函数试试。 如小程序A里有一个xiaoshitou的云函数 [图片] 可以看到我们可以成功的调用小程序A里的xiaoshitou云函数 [图片] 是不是很简单。今天就给大家讲到这里了,欢迎关注,后面会分享更多小程序开发的知识给大家。
2022-02-24 - movable-area关闭movable-view会触发 bindchange甚至会交换位置 ?
一次性多加一些movable-view [图片]然后点击 [图片]然后他就会触发bindchange ,甚至会交换位置 代码链接 https://developers.weixin.qq.com/s/Y5gZBamL7Pxf
2022-02-18 - 用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 - 小程序实现编辑相册功能
代码片段 https://developers.weixin.qq.com/s/HbsH5pmu74tk 打开 [代码]ES6[代码] 转 [代码]ES5[代码] 打开增强编译 打开不校验合法域名 需求分析 图片的宽高初始化不能大于镜头的宽高,如果大于需要缩放到镜头的宽高 图片的宽高不能小于镜头宽高,如果小于则需要按比例缩放 图片单指移动,移动停止的范围不能超过镜头的范围 图片双指缩放,缩放后的大小不能小于镜头宽高,如果小于则需要等比缩放到镜头宽高 图片可以旋转,点击下面的旋转按钮每次旋转 [代码]90deg[代码] 初始化展示样式 [图片] 初始化数据结构 [代码]// 图片信息 const imageInfo = { startX: 0, // 起始按下X坐标 startY: 0, // 起始按下Y坐标 moveX: 0, // 当前移动X坐标 moveY: 0, // 当前移动Y坐标 width: 0, // 图片宽度 height: 0, // 图片高度 rotate: 0, // 旋转度数 distance: 0, // 双指按下的距离 touchCount: 0, // 当前按下的手指数量 transformStyle: 'translate(0, 0) rotate(0deg)', // 图片的 transform 信息 rotateDirection: 'horizontal', // 旋转的轴向 isDoubleFingerMove: false, // 是否是双指移动 } Page({ data: { imageInfo: { ...imageInfo }, shotInfo: { width: 0, // 镜头宽度 height: 0, // 镜头高度 left: 0, // 镜头 x 坐标 top : 0, // 镜头 y 坐标 } }, }) [代码] 获取镜头信息 [代码]Page({ onReady() { this.initEditShot() }, /** * 初始化镜头信息 */ initEditShot() { const query = wx.createSelectorQuery() query.select('.edit-shot').boundingClientRect() query.exec((res) => { const { width, height, left, top } = res[0] const shotInfo = { width, height, left, top, } this.data.shotInfo = shotInfo this.initImageInfo() // 初始化图片信息 }) }, }) [代码] 初始化图片信息 [代码]Page({ /** * 初始化图片信息 */ async initImageInfo() { // 处理图片信息 const imgUrl = 'https://image-beta.djcars.cn/3/ef665764-8e83-4a31-ac85-a3292f379d4b.jpg' const { width, height, path } = await wx.getImageInfo({ src: imgUrl }) const { width: shotWidth } = this.data.shotInfo let imageWidth = Math.ceil((shotWidth / width) * width) // 按比例缩放宽度 let imageHeight = Math.ceil((shotWidth / width) * height) // 按比例缩放高度 this.setData({ imageUrl: path, ['imageInfo.width']: imageWidth, ['imageInfo.height']: imageHeight, }) }, }) [代码] [图片] 定义按比例缩放函数 上面的图片的高度已经小于镜头的高度了,所以需要按比例缩放 [代码]Page({ /** * 初始化图片信息 */ async initImageInfo() { // 处理图片信息 const imgUrl = 'https://image-beta.djcars.cn/3/ef665764-8e83-4a31-ac85-a3292f379d4b.jpg' const { width, height, path } = await wx.getImageInfo({ src: imgUrl }) const { width: shotWidth } = this.data.shotInfo let imageWidth = Math.ceil((shotWidth / width) * width) let imageHeight = Math.ceil((shotWidth / width) * height) const imageSizeInfo = this._handleImageMinSizeScale( imageWidth, imageHeight ) imageWidth = imageSizeInfo.imageWidth imageHeight = imageSizeInfo.imageHeight // 记录图片缩放后大小,还原需要用到 imageInfo.width = imageWidth imageInfo.height = imageHeight this.setData({ imageUrl: path, ['imageInfo.width']: imageWidth, ['imageInfo.height']: imageHeight, }) }, /** * 处理图片低于最小大小缩放 */ _handleImageMinSizeScale(imageWidth, imageHeight) { const { width, height } = this.data.shotInfo let scale = 1 // 如果图片宽度小于镜头宽度 if (imageWidth < width) { const diffWidth = width - imageWidth // 获取相差的宽度 scale = diffWidth / imageWidth + 1 scale = Number(scale.toFixed(3)) imageWidth = Math.ceil(imageWidth * scale) imageHeight = Math.ceil(imageHeight * scale) } // 如果图片高度小于镜头高度 if (imageHeight < height) { const diffHeight = height - imageHeight // 获取相差的高度 scale = diffHeight / imageHeight + 1 scale = Number(scale.toFixed(3)) imageWidth = Math.ceil(imageWidth * scale) imageHeight = Math.ceil(imageHeight * scale) } return { imageWidth, imageHeight } }, }) [代码] [图片] 处理图片平移和旋转 图片展示的逻辑已经处理完毕,接下来是处理移动、缩放逻辑 [代码]touch[代码] 的事件全部使用 [代码]catch[代码] 去绑定,不要去使用 [代码]bind[代码] 去绑定,否则你会发觉在手机上面卡的一批,使用 [代码]catch[代码] 后由于我的图片层级过低移动不了,在所有层级高的元素加上 [代码]point-events: none[代码] 的样式效果即可解决 处理图片平移 [代码]Page({ /** * 监听图片按下 */ changeImageStat(e) { const touches = e.touches const { imageInfo } = this.data const { clientX, clientY } = touches[0] // 防止第二次移动时候回到原点,所以要减去上一次移动的过的位置 this.data.imageInfo.startX = clientX - imageInfo.moveX this.data.imageInfo.startY = clientY - imageInfo.moveY }, /** * 监听图片移动 */ changeImageMove(e) { const touches = e.touches const { startX, startY, rotate } = this.data.imageInfo const { clientX, clientY } = touches[0] const x = clientX - startX const y = clientY - startY const transformStyle = `translate(${x}px, ${y}px) rotate(${rotate}deg)` // 记录当前移动的 x,y this.data.imageInfo.moveX = x this.data.imageInfo.moveY = y this.setData({ ['imageInfo.transformStyle']: transformStyle, }) }, }) [代码] 处理图片的位置超过镜头的范围 [代码]Page({ /** * 监听图片是否移除最大范围 */ _handleImageMove() { const { shotInfo, imageInfo } = this.data const { width: shotWidth, height: shotHeight } = shotInfo const { width, height, moveX, moveY, rotate } = imageInfo let maxX = (width - shotWidth) / 2 // 最大 x let maxY = (height - shotHeight) / 2 // 最大 y let x = moveX let y = moveY // 判断是否超出左边或超出右边 if (maxX - x < 0) { x = maxX } else if (maxX + x < 0) { x = -maxX } // 判断是否超出上边或超出下边 if (maxY - y < 0) { y = maxY } else if (maxY + y < 0) { y = -maxY } const transformStyle = `translate(${x}px, ${y}px) rotate(${rotate}deg)` this.data.imageInfo.moveX = x this.data.imageInfo.moveY = y this.setData({ ['imageInfo.transformStyle']: transformStyle, }) }, /** * 监听移动结束 */ changeImageEnd() { this._handleImageMove() }, }) [代码] 处理图片旋转 点击旋转按钮每次旋转 [代码]90deg[代码] [代码]Page({ /** * 监听图片旋转 */ changeImageRotate() { const rotateStep = 90 const imageInfo = this.data.imageInfo const { rotate, moveX, moveY, width, height } = imageInfo const currentRotate = rotate + rotateStep const handledRotate = currentRotate === 360 ? 0 : currentRotate const transformStyle = `translate(${moveX}px, ${moveY}px) rotate(${handledRotate}deg)` let imageWidth = width let imageHeight = height this.data.imageInfo.rotate = handledRotate this.setData({ ['imageInfo.transformStyle']: transformStyle, ['imageInfo.width']: imageWidth, ['imageInfo.height']: imageHeight, }) // 旋转时候可能也会超出了镜头范围,所以需要处理一下最大移动范围 wx.nextTick(() => { this._handleImageMove() }) }, }) [代码] [图片] 处理图片旋转后的宽高 上面的图片效果是因为对 [代码]image[代码] 标签进行了旋转,由于 [代码]transform[代码] 旋转的轴向改变了,所以在审查元素时候 宽变成高、高变成宽,所以需要在旋转的时候判断轴向,然后对 宽高进行取反 ,然后再去判断当前图片宽高是否小于镜头宽高,再做缩放处理 [代码]Page({ /** * 监听图片旋转 */ changeImageRotate() { const rotateStep = 90 const imageInfo = this.data.imageInfo const { rotate, moveX, moveY, width, height, rotateDirection } = imageInfo const currentRotate = rotate + rotateStep const handledRotate = currentRotate === 360 ? 0 : currentRotate const transformStyle = `translate(${moveX}px, ${moveY}px) rotate(${handledRotate}deg)` const direction = rotateDirection === 'horizontal' ? 'vertical' : 'horizontal' let imageWidth = width let imageHeight = height // 如果是垂直方向则宽变高、高变框,同时处理缩放 if (direction === 'vertical') { const imageSizeInfo = this._handleImageMinSizeScale(height, width) imageWidth = imageSizeInfo.imageHeight imageHeight = imageSizeInfo.imageWidth } // 记录旋转的方向和当前旋转的度数 this.data.imageInfo.rotate = handledRotate this.data.imageInfo.rotateDirection = direction this.setData({ ['imageInfo.transformStyle']: transformStyle, ['imageInfo.width']: imageWidth, ['imageInfo.height']: imageHeight, }) // 旋转时候可能也会超出了镜头范围,所以需要处理一下最大移动范围 wx.nextTick(() => { this._handleImageMove() }) }, }) [代码] 处理图片旋转后的平移 由于旋转后的宽高取反了,所以根据当前旋转的轴向然后把 宽高取反即可 [代码]Page({ /** * 监听图片是否移除最大范围 */ _handleImageMove() { const { shotInfo, imageInfo } = this.data const { width: shotWidth, height: shotHeight } = shotInfo const { width, height, moveX, moveY, rotate, rotateDirection } = imageInfo let maxX = (width - shotWidth) / 2 let maxY = (height - shotHeight) / 2 let x = moveX let y = moveY // 处理旋转后的最大X和最大Y if (rotateDirection === 'vertical') { maxX = (height - shotWidth) / 2 maxY = (width - shotHeight) / 2 } if (maxX - x < 0) { x = maxX } else if (maxX + x < 0) { x = -maxX } if (maxY - y < 0) { y = maxY } else if (maxY + y < 0) { y = -maxY } const transformStyle = `translate(${x}px, ${y}px) rotate(${rotate}deg)` this.data.imageInfo.moveX = x this.data.imageInfo.moveY = y this.setData({ ['imageInfo.transformStyle']: transformStyle, }) }, }) [代码] 处理图片双指缩放 首先来分析一下需求: 触发缩放的前提必须是两只手指以上(多于两只取第二条按下的手指作为参考) 两条手指往近移则缩小、往远移则放大 两条手指移动时不会触发单指平移的效果,只会缩放 处理双指按下时不触发平移效果 当多只手指按下屏幕时,会同时触发基于手指数量的 [代码]touchstart[代码] 事件,所以需要通过详细的数字去判断,而不能只根据长度去判断 [代码]Page({ /** * 监听图片按下 */ changeImageStat(e) { const doubleFingerMoveCount = 2 const touches = e.touches const { imageInfo } = this.data // 判断是否是单指 if (touches.length < doubleFingerMoveCount) { const { clientX, clientY } = touches[0] this.data.imageInfo.startX = clientX - imageInfo.moveX this.data.imageInfo.startY = clientY - imageInfo.moveY } else if (touches.length === doubleFingerMoveCount) { this.data.imageInfo.isDoubleFingerMove = true // 记录是双指操作 this.data.imageInfo.touchCount = doubleFingerMoveCount // 记录双指数量 this.data.imageInfo.distance = this._calculateDistance(touches) // 计算两只手指距离 } }, }) [代码] 计算双指按下的距离 [图片] 通过勾股定理求出两只手指的距离: [代码]Page({ /** * 计算双指距离 */ _calculateDistance(touches) { const x1 = touches[0].clientX const y1 = touches[0].clientY const x2 = touches[1].clientX const y2 = touches[1].clientY const clientX = x1 - x2 const clientY = y1 - y2 const distance = Math.sqrt(clientX ** 2 + clientY ** 2) return distance }, }) [代码] 处理双指移动时缩放效果 当多只手指移动屏幕时,会同时触发基于手指数量的 [代码]touchmove[代码] 事件,所以需要通过详细的数字去判断,而不能只根据长度去判断 [代码]Page({ /** * 监听图片移动 */ changeImageMove(e) { const touches = e.touches const doubleFingerMoveCount = 2 const { startX, startY, width, height, rotate, distance, isDoubleFingerMove, } = this.data.imageInfo if (!isDoubleFingerMove && touches.length < doubleFingerMoveCount) { const { clientX, clientY } = touches[0] const x = clientX - startX const y = clientY - startY const transformStyle = `translate(${x}px, ${y}px) rotate(${rotate}deg)` this.data.imageInfo.moveX = x this.data.imageInfo.moveY = y this.setData({ ['imageInfo.transformStyle']: transformStyle, }) } else if (isDoubleFingerMove && touches.length === doubleFingerMoveCount) { const moveDistance = this._calculateDistance(touches) // 获取当前移动的双指距离 const diffScale = Number((moveDistance / distance).toFixed(3)) // 获取当前缩放值 const imageWidth = width * diffScale const imageHeight = height * diffScale this.data.imageInfo.distance = moveDistance // 实时记录双指距离才能实现每次移动慢慢缩放的效果 this.setData({ ['imageInfo.width']: imageWidth, ['imageInfo.height']: imageHeight, }) } }, }) [代码] 处理缩放时候手指松开效果 当多只手指离开屏幕时,会同时触发基于手指数量的 touchend 事件,所以需要所有手指离开屏幕时候才触发效果 [代码]isDoubleFingerMove[代码] 和 [代码]currentToucnCount[代码] 一起判断是为了处理结束后的细节,比如两只手指按下后触发了缩放逻辑,但是松开我只搜开一只手指,这时候我不想让它再触发平移效果,否则会很混乱,只有两只手指离开 [代码](currentToucnCount = 0)[代码] 才会把 [代码]isDoubleFingerMove[代码] 变为 [代码]false[代码] [代码]Page({ /** * 监听移动结束 */ changeImageEnd() { const { width, height, touchCount, rotateDirection, isDoubleFingerMove } = this.data.imageInfo const currentToucnCount = touchCount - 1 // 手指离开就减一 // 记录当前手指数量 this.data.imageInfo.touchCount = currentToucnCount // 如果是双指按下并且手指数量为0,则触发双指缩放离开处理逻辑 if (isDoubleFingerMove && currentToucnCount <= 0) { let imageSizeInfo = {} // 根据旋转轴向判断当前图片缩放是否小于镜头宽高 if (rotateDirection === 'vertical') { imageSizeInfo = this._handleImageMinSizeScale(height, width) this.setData({ ['imageInfo.width']: imageSizeInfo.imageHeight, ['imageInfo.height']: imageSizeInfo.imageWidth, }) } else { imageSizeInfo = this._handleImageMinSizeScale(width, height) this.setData({ ['imageInfo.width']: imageSizeInfo.imageWidth, ['imageInfo.height']: imageSizeInfo.imageHeight, }) } // 重置双指缩放控制变量 this.data.imageInfo.isDoubleFingerMove = false // 缩放后判断是否超出了镜头的范围 wx.nextTick(() => { this._handleImageMove() }) return } // 如果是单指则直接处理平移逻辑 if (!isDoubleFingerMove) { this._handleImageMove() } } }) [代码] 将处理后的图片生成新的图片 由于画布是放在镜头元素的里面,而镜头在页面的效果是居中的,所以我的 [代码]canvas[代码] 也是居中在屏幕中间的,而宽高和镜头是相等的(边框差2像素后面补充细节) [代码]<view class="edit-shot"> <canvas class="image-canvas" type="2d"></canvas> </view> [代码] 初始化画布信息 [代码]Page({ /** * 初始化镜头画布 */ async initCanvas() { const query = wx.createSelectorQuery() query.select('.image-canvas').fields({ node: true, size: true }) query.exec((res) => { const imageCanvas = res[0].node const imageCtx = imageCanvas.getContext('2d') const dpr = systemInfo.pixelRatio const width = res[0].width const height = res[0].height imageCanvas.width = width * dpr imageCanvas.height = height * dpr imageCtx.scale(dpr, dpr) this.imageCtx = imageCtx this.imageCanvas = imageCanvas }) }, }) [代码] 点击确认按钮裁切图片 [代码]Page({ /** * 生成图片 */ async generatorImage() { await this._drawHandledImage() const { width, height } = this.data.shotInfo const { tempFilePath } = await wx.canvasToTempFilePath({ width, height, canvas: this.imageCanvas, }) wx.previewImage({ urls: [tempFilePath], }) }, /** * 绘制图片 */ async _drawHandledImage() { const { imageInfo, imageUrl, shotInfo } = this.data const imageCanvas = this.imageCanvas const imageCtx = this.imageCtx const image = imageCanvas.createImage() const { width, height, moveX, moveY, rotate } = imageInfo let rotateMoveX = moveX let rotateMoveY = moveY /** * - 由于有旋转,则需要按照当前旋转的方向取反 * - 由于镜头的宽高和canvas差2像素,所以根据旋转加或减一半, * 当然还有其他处理方法 */ if (rotate === 0) { rotateMoveX = moveX - 1 rotateMoveY = moveY - 1 } if (rotate === 90) { rotateMoveY = -moveX + 1 rotateMoveX = moveY - 1 } if (rotate === 180) { rotateMoveY = -moveY + 1 rotateMoveX = -moveX + 1 } if (rotate === 270) { rotateMoveY = moveX - 1 rotateMoveX = -moveY + 1 } // 获取当前移动的 x,y 在图片的坐标 const diffX = (shotInfo.width - width) / 2 + rotateMoveX const diffY = (shotInfo.height - height) / 2 + rotateMoveY // 获取镜头中心点 const rawX = shotInfo.width / 2 const rawY = shotInfo.height / 2 /** * - 由于我的 canvas 的 x,y 将会移动到中心点,因为旋转需要 * - 图片移动的 x,y 药减去镜头的一半,因为坐标 0 是在中心点, * 而不是在镜头左上角 */ const x = diffX - rawX const y = diffY - rawY await new Promise((resolve) => { image.src = imageUrl image.onload = resolve }) imageCtx.save() imageCtx.translate(rawX, rawY) imageCtx.rotate(rotate * (Math.PI / 180)) imageCtx.drawImage(image, x, y, width, height) imageCtx.restore() }, }) [代码]
2021-09-18 - 如何提单“公众平台”腾讯客服?
第一步:用手机打开微信 在微信首页的顶部“发现搜索按钮”(如图1.1)->搜索“腾讯客服”,打开“腾讯客服”小程序(如图1.2) 图1.1: [图片] 图1.2: [图片] 第二步:进入“腾讯客服”小程序,点击相应的“标签”进行跳转 在顶部tab,点击“其他”(如图2.1)->点击“更多其他问题”(如图2.1)->点击“公众平台”(如图2.2)->点击底部的“提交问题”(如图2.3) 图2.1: [图片] 图2.2 :[图片] 图2.3:[图片] 第三步:点击“提交问题”,跳转到编辑页面。 选择“问题类型”(如图3.1)->对应的选项进行填写(如图3.2) 图3.1:[图片] 图3.2:[图片]
2021-02-25 - 这个库能轻松解决99%的异步和逻辑加载时机问题(异步篇)
[图片] 你是否纠结过底层业务逻辑(登陆、获取用户信息等)到底是放app.js的onLaunch还是page的onLoad里比较好,或者因为异步问题被迫放在了onload,我们来分析一下优劣 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 分析 onLaunch处理 优点:底层业务逻辑集中并且只需写一次,比较好维护 缺点:目前没有一个理想的方案来解决onLaunch和onLoad的异步问题,包括注册回调、重写onLoad、请求拦截等。 onLoad处理 优点:因为不涉及跨页面通知,因此异步逻辑比较好处理 缺点:每个页面都得写一次底层业务逻辑,非常繁琐,而且既然是公用的底层业务逻辑,分散在每个页面的onLoad里,好像也不大对劲。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 抉择 按照高内聚低耦合的原则,那逻辑和数据放onLaunch里肯定的,不应该和普通page逻辑耦合在一起,通用的数据和逻辑应该在入口去处理,执行一次到处使用,就像vue的main.js一样,会注册一些技术层的基础设施(路由、状态管理等插件),那业务层的基础设施不就是token、用户信息、所在位置等逻辑吗? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 想象中的最佳实践 那我们的目标就是如何满足两者的优点,避免两者的缺点,做到真正的“高内聚低耦合” 1.保持底层业务逻辑写在入口app.js,避免耦合page里的逻辑 2.能在任何page里第一时间拿到globalData数据 3.使用方便,做到在业务开发中无感知,不需要写额外的调用、通知等代码 4.无任何副作用,不会影响其他功能,比如重写阻塞onLoad 5.灵活可配,适用以后此类任何业务 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 梦想成真先看一段代码 ⬇️ // page.js export default { name: 'Home', onLoadLogin(){ //登录成功(拿到token) && 页面初始化完成 //Tips:适用于某页面发送的请求依赖token的场景 }, onLoadUser(){ //页面初始化完成 && 获取用户信息完成 //Tips:适用于页面初始化时需要用到用户信息去做判断再走页面逻辑的场景 }, onReadyUser(){ //dom渲染完成 && 获取用户信息完成 //Tips:适用于首次进入页面需要在canvas上渲染头像的类似场景 }, onReadyShow(){ //小程序内页面渲染完成 && 页面显示 //Tips:适用于需要获取小程序组件或者dom,并且每次页面显示都会执行的场景 }, } 应该懂什么意思了吧?是不是你理想中的样子,使用起来跟没有似的 ⬆️ 这段示例代码满足了上面的第2、3、4条目标 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 再来看一段 ⬇️ // app.js // 配置自定义钩子,所有钩子都可以随意组合搭配使用,执行机制类似于Promise.all(但不是用Promise实现的) CustomHook.install({ 'Login':{ // 自定义钩子名称、必须大写字母开头 name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ // 自定义钩子名称 name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } //依赖globalData中数据 }, globalData) 怎么样,是不是很棒,依赖globalData,名字可配,连触发规则都可配,而且还附加了可随意组合的功能(意外还解决了页面内逻辑执行时机问题,在下篇讲) ⬆️ 这段示例代码满足了上面的第1、5条目标。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 是不是跃跃欲试了,那就赶紧试试,好用回来告诉我! ⬇️(公司内部已接入两年了很稳定) GitHub:https://github.com/1977474741/spa-custom-hooks [图片]
2023-07-07 - 用云函数这一利器改写了ai抠图
抠图效果 [图片] 引言 上次写了一篇用小程序实现ai抠图,就差一步可以能在小程序全盘使用第三方库去抠图,苦于不能将Buffer图片源转成base64赋给[代码][代码],上了node.js后端去实现,这两天突然想起可以用云函数去实现,果断用云函数代替自己写后端。 纯微信小程序端实现ai抠图代码如下: [代码]wx.chooseImage({ count: 1, // 默认9 sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 success: res => { var tempFilePaths = res.tempFilePaths const file = tempFilePaths[0] this.setData({ origin: file }) console.log(file) wx.uploadFile({ header: { 'X-Api-Key': 'your key' }, url: ' https://api.remove.bg/v1.0/removebg', filePath: file, name: 'image_file', success: res => { const data = res.data console.log(data) console.log('base64') const base64 = data.toString('base64') console.log(base64) let url = base64 this.setData({ url: url }) } }) } }) [代码] 只可惜上面的代码最终还是乱码的,不能赋值给image来渲染 于是后来,才动用node.js koa框架写了一个简单的后端实现。 使用云函数改写 koa就为了转发一下请求,这一跳板有点大材小用,于是乎想到用云函数,就免去买服务器,配后端环境,起koa项目。 建立云函数目录并配置 在项目根目录新建文件夹functions,并在project.config.json中增加如下设置 [代码]{ "cloudfunctionRoot": "./functions/" } [代码] 然后项目目录下会出现如下标识 [图片] 添加云函数 鼠标右击functions文件,创建云函数ps,安装依赖,[代码]npm install request-promise --save[代码] 具体代码如下 [代码]// 云函数入口文件 const rp = require('request-promise') // 云函数入口函数 exports.main = async (event, context) => { // const wxContext = cloud.getWXContext() const file = event.file const buffer = new Buffer.from(file, 'base64') const result = await rp.post({ url: 'https://api.remove.bg/v1.0/removebg', formData: { image_file: buffer, size: 'auto' }, headers: { 'X-Api-Key': 'wkMhcc4TRNFpxjL79Kf8mMU1' }, encoding: null }) const body = result const image = body.toString('base64') return { image } } [代码] 原理就是将文件的base64编码,再转换成buffer,再提交给remove.bg这个ai抠图api地址。于是现在只剩下一件事,就是将小程序端本来是用二进制文件上传的,要先将它改成用base64后,才能传递给云函数。 小程序端上传传base64编码 这里使用小程序的FileSystemManager.readFile方法将图片二进制文件,转成base64再提交给云函数。 相关文档: https://developers.weixin.qq.com/minigame/dev/api/file/FileSystemManager.readFile.html 于是完整的小程序前端代码如下 [代码]wx.getFileSystemManager().readFile({ filePath: file, //选择图片返回的相对路径 encoding: 'base64', //编码格式 success: res => { //成功的回调 wx.cloud.callFunction({ name: 'ps', data: { file: res.data }, success: (res) => { console.log(res) const data = res.result.image let url = 'data:image/png;base64,' + data this.setData({ url: url }) // //do something console.log(res) }, fail(err) { console.log(err) } }) } }) [代码] 先作base64编码,然后调用云函数,最后将云函数返回的base64图片资源渲染到[代码][代码],整个流程走完。 源码 https://gitee.com/laeser/demo-weapp 代码位于[代码]pages/ps-cloud[代码]文件夹下 关注我 [图片]
2020-08-10 - 一个云函数五行代码搞定云调用openapi
云调用接口如下: https://developers.weixin.qq.com/miniprogram/dev/api-backend/ 1、该文档中的几十个接口,全部可由下面5行代码实现: 2、同时支持共享环境下的云调用 云函数名:openapi index.js代码: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async event => { let appid = cloud.getWXContext().FROM_APPID || cloud.getWXContext().APPID return await cloud.openapi({appid})[event.action](event.body) } 小程序端调用代码: onOpenapi: function () { wx.cloud.callFunction({ name: 'openapi', data: { action: 'urlscheme.generate', body: {} } }).then(res => { console.log(res) }) }, 将云调用相关的云函数合并成一个。 而且,极简。。。
2022-09-29 - 个体工商户没有对公账户注销小程序的方法?
联系腾讯客服公众号,输入文字”无对公账户注销小程序“ 然后按照提示选择小程序,准备资料(1.营业执照,2.法人身份证正反面,3.注销申请书,4.法人手持身份证照片)提交 等待审核即可。我已经成功注销了。 [图片] https://kf.qq.com/faq/200306R7N3mI200306I3aEBz.html 官方提供的注销申请书模板。
2021-11-12 - listDirectoryFiles获取云存储文件夹下所有图片报错?
代码调用 [图片] 报错 [图片] 腾讯云的id 、密匙,环境id都是正确的。
2021-07-22 - 微信小程序开发之富文本编辑器
微信小程序开发之富文本编辑器 一年多去了,还有这么多人关注这个编辑器,那就索性把这个组件放上去,各位直接引用吧!如果您感觉很好用,很实用,也请大家给点一个赞!前言:富文本在Web开发上的地位大家可想而知,很多地方都需要用到富文本编辑器,比如开发类似新闻管理小程序、商品简介等。微信小程序在基础库2.7.0之后上线了一个editor富文本编辑器组件,这个组件是本次要讲的内容。组件相关的内容大家可以去看官方文档的内容,这里我们就不进行讲解。而我们要做的就是将官方的富文本组件进行二次开发达到一个好用而又实用的地步:https://developers.weixin.qq.com/miniprogram/dev/component/editor.html 先看效果图(以下只是一个基础的实用): [图片] 代码方案: 1.引入组件(组件的下载地址链接:https://pan.baidu.com/s/15D3ejvs30BZPwn94RgyNmw 提取码:hg66) 2、在你需要的使用的页面的JSON文件中引入该组件,引入方法如下: "usingComponents": { "hg-editor":"../../../components/hg-editor/hg-editor(根据自己的放置位置修改,其中/hg-editor/hg-editor是固定的)" } 3、在wxml文件中使用,使用案例如下,可选参数有四个 参数详解: [图片] showTabBar :是否显示工具栏(默认为true,显示,如果改为false则为不显示)placeholder:文本框提示文字,默认为“请输入相关内容”name:是编辑器的name属性,默认为空uploadImageURL:图片的上传地址,默认为空使用属性案例测试: bind:input可以获得用户输入的内容: onInputtingDesc: function (e) { let html = e.detail.html; //相关的html代码 let originText = e.detail.text; //text,不含有任何的html标签 this.setData({ ['topic.text']: html, ['topic.originText']: originText }); } 使用案例: [图片][图片][图片] 您的想法有多大,组件拓展的无限可能就有多大,欢迎各位留言,欢迎各位使用! 好用,就来收藏一下,更新不易,点个赞!
2022-04-22 - 小程序盈利模式入门指南
如何通过小程序盈利、如何开通流量主、小程序有哪些广告类型 本文概述 由于上一篇介绍的文章,没法进行编辑,本文主要在上一篇的基础上着重再讲广告的分成模式。 小程序如何盈利 目前对个人小程序开发者而言,只有通过开通流量主,并且按照官方规范要求添加广告位,才能获取收益,当然打赏除外。 小程序流量主 流量主是微信对外提供的一个服务,通过开通流量主,就可以在小程序合适的位置引入广告位,进而实现收益 小程序流量主开通流程 https://developers.weixin.qq.com/community/develop/doc/000046f02244a041da79753e557009 1.开通条件:小程序累计独立访客(UV)1000以上,且无违规记录,即可开通流量主功能。 温馨提示:如满足条件仍无法开通,可能是数据同步问题,建议等待1-2个工作日后再试。 2.申请方法:进入微信公众平台小程序后台,点击左侧面板“流量主”,满足开通门槛的小程序开发者点击“开通”,提交财务资料,待审核通过后成功开通流量主功能,即可创建相应的广告位。 3.广告接入指引: 广告接入可查看下面具体文档 微信小程序广告接入指引 https://wximg.qq.com/wxp/pdftool/get.html?id=BJSyDkLqz&pa=14&name=miniprogramAds_supplier_manual 小程序流量主结算方式 对个人小程序流量主而言,广告收益是在扣除个人所得税之后,直接到个人银行账户的,目前每月结算两次。 [流量主结算周期及开票规则调整说明][2019-12-03发布] https://mp.weixin.qq.com/promotion/readtemplate?t=notice/detail_page&time=1575340587¬ice_id=634169 小程序广告位介绍 广告类型有哪些 下面主要介绍小程序广告组件以及重点介绍下分成模式 Banner广告 激励式视频 插屏广告 视频广告 视频前贴视频 下面我们从目前几个主流的小程序给大家截图展示,让大家更真实的认识不同类型的广告 [banner广告] [图片] [插屏广告] [图片] 视频广告 [图片] 激励式广告 以下截图来自抽奖助手(没错,就是那个广告收入月入200多W的无码科技旗下冯大辉老师的抽奖助手) [图片] 以下截图来自激励式广告指引官方文档 [图片] 不同广告类型的分成模式 Banner广告的分成模式如下: 根据用户的每一次广告点击,平台收取广告费用并与流量主分成。100W元以下广告流水,按照5:5分成,100W元以上广告流水,按照3:7分成(流量主3成),分成不设封顶。 实际分成以后台“数据统计”页面展示为准 视频广告、插屏广告、激励视频广告分成模式如下 按照每一千次有效的广告曝光,平台收取广告费用并与流量主分成。100W元以下广告流水,按照5:5分成,100W元以上广告流水,按照3:7分成(流量主3成),分成不设封顶 实际分成以后台“数据统计”页面展示为准。 哪种广告类型收益相对最大 直接从后台截图吧 [图片] 在10月30号,将banner广告同一替换为激励式视频广告和视频广告,收益很明显从30元上升到90元、150元 可以看到视频广告相对于banner广告,对于收益增加是有用的。 下图是某小程序12月4号一天的收益数据 [图片] 12月4号一天,不同广告类型,收益分析 总收益 194.74+23.27+147.82=365.83 具体分拆来看 广告类型 点击量 总收益 单个点击收益(元) banner 1956 194.74 0.099 插屏广告 62 23.27 0.375 激励式视频广告 152 147.82 0.972 通过上图我们对比分析,不难得出以下结论:激励式视频广告单个点击的收益最大、 当然我们不能通过单一维度来了解哪种收益最好,还要综合考虑,比如哪种广告对用户影响最小,毕竟不管哪种方式,广告的接入肯定会带来交互体验上的障碍, 我们必须在交互体验和广告收益这两者之间做好权衡。 系统公告 2019年4月25号,小程序广告组件新增激励式视频广告与插屏广告,进一步丰富开发者的广告变现方式,开发者可到流量主模块进行开通,具体规则见《小程序激励式视频广告开通指引》,《小程序插屏广告开通指引》。 激励式广告于2019年7月31日支持30秒视频素材,广告流量将逐步放开,MP后台-广告位管理模块可支持选择6-15秒视频或6-30秒视频素材的功能,请流量主根据产品进行调整。 小程序视频前贴广告组件已于8月30日正式全量上线,开通后即按广告曝光获得分成收入,进一步提升流量变现收益。 程序视频广告已于9月4日正式全量上线,开通后即按广告曝光获得分成收入,进一步提升流量变现收益。 2019-09-04小程序视频广告与视频前贴广告已面向非游戏类小程序全量上线,进一步丰富开发者的广告变现方式。 开发者可到流量主模块进行开通,开通后即按广告曝光获分成收入,进一步提升广告收益水平。 具体规则见《小程序视频广告流量主主指引》,《小程序视频前贴广告流量主指引》。 官方文档 流量主接入指引文档、小程序广告组件流量主操作指引 https://wximg.qq.com/wxp/pdftool/get.html?id=BJSyDkLqz&pa=14&name=miniprogramAds_supplier_manual 应用规范 https://wxa.wxs.qq.com/mpweb/delivery/legacy/pdftool/get.html?id=rynYA8o3f&pa=10&name=miniprogramAds_supplier_guidance 小程序流量主应用规范 https://wximg.qq.com/wxp/pdftool/get.html?id=rynYA8o3f&pa=10&name=miniprogramAds_supplier_guidance 处罚标准 https://wxa.wxs.qq.com/mpweb/delivery/legacy/pdftool/get.html?id=BkTGkbs2G&pa=1&name=miniprogramAds_supplier_regulation 小程序视频广告流量主指引 https://wximg.qq.com/wxp/pdftool/get.html?post_id=1317 小程序视频前贴广告流量主指引 https://wximg.qq.com/wxp/pdftool/get.html?post_id=1318 小程序激励式视频广告开通指引 https://developers.weixin.qq.com/community/develop/doc/00060ef22cc00855a4681691c5bc01 小程序插屏广告开通指引 https://developers.weixin.qq.com/community/develop/doc/0002680c7f4960344578da0a756801 小程序视频广告流量主主指引 https://wximg.qq.com/wxp/pdftool/get.html?post_id=1317 小程序视频前贴广告流量主指引 https://wximg.qq.com/wxp/pdftool/get.html?post_id=1318 提升广告收益怎么做?四大流量主增收妙招 https://wximg.qq.com/wxp/pdftool/get.html?post_id=1699 文中流量主单篇文章插入两条广告使用邀请 https://wximg.qq.com/wxp/pdftool/get.html?post_id=331 总结 从纯收益的角度来讲,在各种广告类型中,视频广告(包含激励式视频广告、视频广告、视频前贴广告)要比banner广告要好,而且好很多 从用户体验来讲,插屏广告是首次打开带插屏广告的页面强制弹出的,但是广告过后,在页面是不占空间的,这是区分与其他广告的地方,banner广告、激励式视频广告、视频广告、视频前贴广告都是在页面中占固定的空间的,这一点要小程序运营同学权衡。 Banner广告是按点击,激励式视频、视频广告、插屏广告都是按照曝光来收取广告费用的,这一点非常重要,难怪我每次手工点击我的视频广告没有见流量的增加[哭脸.jpg]。[感谢 @ 仙森 补充于2019年12月9号] 虽然对个人开发者而言,我们开发小程序的目的是为了收益(当然也有为了情怀而开发),在了解如何收益的情况下,我们还是应该尽量把精力放在小程序本身的开发上面。 结算对账单 [图片] [图片] 收入截图 1 [图片] 1 收益截图 1 [图片] 1 1 感谢 在此特别感谢,小程序运营讨论群的两位小伙伴,微信号中间两位已打码 1、@迭戈 (yang_##chun) 2、@风猫 (cs##26) 3、@citizen four
2021-01-24 - 「激励式视频广告」向非游戏类小程序流量主开放
[图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片]
2019-04-16 - 小程序发送红包相关问题调研
https://blog.csdn.net/weixin_42253589/article/details/81127899 https://www.jianshu.com/p/43668ae7a302 https://www.cnblogs.com/xinweiyun/p/9361212.html 社交红包小程序开通条件: 1、商户号,商户号入驻满90天,连续交易30天,中间不能中断;(开通微信支付) 2、需选择社交红包类目,所需资质为:《增值电信业务经营许可证》,即ICP证; 3、商户号开通成功后并绑定小程序主体。 小程序红包开通条件: 1、开通商户号; 2、商户号开通成功后绑定小程序主体。 领取限制: 目前小程序红包仅支持用户微信扫码打开小程序,进行红包领取。 小程序红包有哪些内容是不能做的? 1、小程序内暂不支持红包广场业务场景。即小程序的红包发放与领取仅限在群和好友中进行,禁止在无好友或群关系的场景传播,如基于LBS的红包广场。 2、红包类活动暂不支持朋友圈传播。 3、暂不支持“回赏”类红包玩法。即A发一个红包给任何用户,任何用户需回包一个红包给A,才能拆开A的红包。 4、暂不支持单笔交易支付金额突破商户号额度(当前商户号额度为204元)。 3、服务类目中没有社交红包类目,如何开通?(社区) https://developers.weixin.qq.com/community/develop/doc/0006aaa26fca405e51aae14b95b800?highLine=%25E7%25BA%25A2%25E5%258C%2585 4、发红包接口(微信支付) https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_4&index=3 5、小程序如何发红包(社区) https://developers.weixin.qq.com/community/develop/doc/0006e057e80c800eb92ad503556400?highLine=%25E7%25BA%25A2%25E5%258C%2585 6、小程序红包(微信支付) https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=18_4&index=1 7、微信红包封面开放平台 https://cover.weixin.qq.com/cgi-bin/mmcover-bin/readtemplate?t=page%2Fdoc%2Fguide%2Fintroduce.html 8、小程序红包领取和企业付到零钱(社区) https://developers.weixin.qq.com/community/develop/article/doc/000268a1c608d8eee7aa667635c813 9、小程序红包配置及开发小结(社区) https://developers.weixin.qq.com/community/develop/article/doc/000a28c8c90e50bbf5a9272f956c13 10、小程序营销利器,wx.sendBizRedPacket小程序红包完整详细开发代码(社区) https://developers.weixin.qq.com/community/develop/article/doc/000c6009a5cbd805d8790d4ab56013?page=1#comment-list
2021-01-06 - 小程序红包领取和企业付到零钱
用户吐槽:默认就是markdown不好吗? 场景 开发中遇到这样一个需求,用户在小程序里输入密令,点击抽奖,奖品内容分别是,平台积分,平台优惠券,平台线下体验卡,和红包。 怎么发“红包” 现金红包>小程序红包 客户既然说了是红包,搜索微信支付和红包相关的API,然后找到了这篇API,但是,这个API更适合线下。原因是场景值限制,只能“扫一扫”,还是用摄像头的那种扫,多亏了这位仁兄踩雷。但我们的业务需求是用户线上抽到红包就是要立马领取到的,只能另寻思路。 企业付款到零钱 看微信支付官方还能提供到什么文档嘛!果然,我看到这个,企业付,官方描述里,说的是通过APPID+OPENID指定收款用户,那这个openid可不可以是小程序的openid呢?答案是,可以!有人问了 总结一下 如果你要做线下,推荐红包 如果你要做线上,推荐企业付到零钱
2020-07-21 - 视频号开通直播推流权限,可以支持小商店可以通过电脑直播卖货啦
完成认证之后,很快就收到了视频号团队发过来的直播推流功能开通通知,通知如下: [图片] 这对于小商店商家有什么好处呢?也就是可以在电脑上直播卖货啦,你会发现视频号的页面其实装修比较朴素,但是通过电脑之后,可以装扮了,比如直播页面贴片等,有很多工作室做OBS插件,可以实现现有所有直播间里呈现的所有特效。自动欢迎,自动语音,自动答谢,动感背景,K歌歌词,滚屏文字图片,去背景等等太多了,可以好好挖掘下。 所以你收到开通通知了吗?
2020-12-11 - 云函数post请求api错误码44002,POST BODY 为空,应该怎么改?
请求获取集合下的数据,post请求后提示44002,查阅文档后是POST BODY 为空,要怎么进行调整写法? [图片][图片]
2020-09-15 - 小程序直播组件服务端云调用接口全不能用,提示errCode: -604100 API not fo?
直播服务端云调用接口全都用不起!提示找不到api,报错:系统错误!sdk 2.4.0开发者版本1.5以上。如:deleteSubAnchoreditRoomgetAssistantListgetFollowersgetLiveInfogetPushUrl以上这些都是找不到api, 云函数代码如下: const cloud = require('wx-server-sdk') cloud.init({ env: "songzhaoyun-5gy764xh2fc21226480" }) exports.main = async (event, context) => { const getLiveInfo = cloud.openapi.livebroadcast.getLiveInfo({ url: 'https://api.weixin.qq.com/wxa/business/getliveinfo', start: 0, limit: 10 }); const GET = await getLiveInfo return { event, GET } } config.josn: { "permissions": { "openapi": [ "livebroadcast.getLiveInfo" ] } }
2021-04-07 - 微博热门视频流小程序插件上线了,海量优质视频,期待你的接入
插件说明 在你的 微信小程序 中,可以通过 热门视频插件 ,十分方便的在你的小程序里,实现目前特别流行的沉浸式全屏视频流功能,提升小程序的用户粘性和停留时长。 接入前准备 https://open.weibo.com/wiki/Wxminiappplugin [图片] 插件地址 https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx501c3c6e8f8dfcaf 接入过程中遇到问题可以查看我的资料,联系我。
2021-02-20 - 个体户资质开发本地餐饮服务小程序相关问题?
资质为个体户,开发一个专注省内餐饮服务的小程序,功能为餐饮店入驻,但是不搞线上点餐以及外卖点餐,只做入驻后的餐饮店发放优惠券(小程序收取一定的手续费),用户抢券后到店消费减去优惠券额(小程序用户点击用券后流程结束),问题是这样的小程序能不能通过审核,如果不通过,不通过的原因有哪些? [图片]
2021-03-30 - 只用来发放优惠卡券的小程序属于什么类目?软件服务开发商还是电商平台?
只用来发放优惠卡券的小程序属于什么类目?软件服务开发商还是电商平台?@小程序运营专员
2021-06-08 - [开盖即食]九宫格抽奖component组件分享
[图片] 这次继续分享第二个抽奖组件,参考了网上多个版本,本人根据实际工作中进行了一些优化,并将其做成component组件方便大家食用~ [图片] 1、现在上吃的,呸,上代码 页面引用部分: [代码]<!-- 数据是根据外部配置的,同时也修改组件自定义callback返回内容 --> <LuckComponent lucks-data="{{lucksData}}" bind:callBack="luckCb"></LuckComponent> <view class="roll">当前抽奖结果index:{{luck_num}}</view> [代码] [代码]Page({ data: {}, onLoad() { //在这里配置显示数据,未来还能添加图片等等 let lucksData = [{ //这里修改后,可以通过后台请求配置 "key": "baofu", "name": "暴富", "indexli": 1 }...]; this.setData({ lucksData }) }, /** * 结果回调函数 * @param {*} e */ luckCb(e){ console.log(e); if(e.detail){ this.setData({ luck_num:e.detail }) } } }) [代码] Component组件部分 [代码]<view class="luck_box"> <view class="luck"> <view class='li {{amplification_index===item.indexli?"indexli":""}}' wx:key="item" wx:for="{{lucksData}}"> <!-- 开始 --> <view bindtap="startrolling" class="startrolling" wx:if="{{item.indexli === -1}}"> <view class="st1">抽奖</view> </view> <block wx:if="{{item.indexli !== -1}}"> <view class="setup_title"> <view class="txt">{{item.name}}</view> <view class="index">当前index:{{item.indexli}}</view> <view wx:if="{{item.parentsClass}}" class="^parentsClass">{{item.parentsClass}}</view> </view> <view class="indexli_view"></view> </block> </view> </view> </view> [代码] [代码]Component({ /** * 组件的属性列表 */ properties: { lucksData: { type: Array, value: [] }, }, /** * 组件的初始数据 */ data: { amplification_index: 0, //轮盘的当前滚动位置 roll_flag: true, //是否允许滚动 max_number: 8, //轮盘的全部数量 speed: 300, //速度,速度值越大,则越慢 初始化为300 myInterval: "", //定时器 max_speed: 40, //滚盘的最大速度 minturns: 8, //最小的圈数为2 runs_now: 0, //当前已跑步数 luck_num: 0, // 中奖位置!!!!!!!!!!!!!!!!!!!!!!!!! end_amp: 0, //上一次滚动的位置 start_flag: true, lucksData: [], //这里是渲染数据 }, /** * 组件的方法列表 */ methods: { //开始滚动 startrolling: function () { let _this = this; //roll点 let random = parseInt(Math.random() * 8 + 1); if (this.data.start_flag == true) { _this.setData({ luck_num: random, start_flag: false }) //初始化步数 _this.data.runs_now = 0; //当前可以点击的状态下 if (_this.data.roll_flag) { _this.data.roll_flag = false; //启动滚盘, _this.rolling(); } }; //回调行数,把结果传出去 this.triggerEvent('callBack', random); }, //滚动轮盘的动画效果 rolling: function (amplification_index) { let _this = this; this.data.myInterval = setTimeout(function () { _this.rolling(); }, this.data.speed); this.data.runs_now++; //已经跑步数加一 this.data.amplification_index++; //当前的加一 //获取总步数,接口延迟问题,所以最后还是设置成1s以上 let count_num = this.data.minturns * this.data.max_number + this.data.luck_num - this.data.end_amp; //上升期间 if (this.data.runs_now <= (count_num / 3) * 2) { this.data.speed -= 30; //加速 if (this.data.speed <= this.data.max_speed) { this.data.speed = this.data.max_speed; //最高速度为40; } } //抽奖结束 else if (this.data.runs_now >= count_num) { clearInterval(this.data.myInterval); this.data.roll_flag = true; this.setData({ end_amp: _this.data.amplification_index, start_flag: true }) if (_this.data.is_selected == 0) { wx.showModal({ title: '很遗憾', content: _this.data.prize_name, showCancel: false, success(res) { } }) } else if (_this.data.is_selected == 1) { wx.showModal({ title: '恭喜您', content: _this.data.prize_name, showCancel: false, success(res) { } }) } } //下降期间 else if (count_num - this.data.runs_now <= 10) { this.data.speed += 20; } //缓冲区间 else { this.data.speed += 10; if (this.data.speed >= 100) { this.data.speed = 100; //最低速度为100; } } if (this.data.amplification_index > this.data.max_number) { //判定!是否大于最大数 this.data.amplification_index = 1; } this.setData(this.data); }, } }) [代码] 2、食用指南 可以通过 [代码]<slot>[代码] 、 [代码]^class[代码] 和 [代码]~class[代码] 等方法外部配置组件的样式,使其能在多个地方复用 如果还想配置如起始点,速度等,可以统一通过option传参的方式,二次开发下这个组件。 可以通过修改组件让callback返回更多参数 [图片] 3、具体代码片段 地址: https://developers.weixin.qq.com/s/a5NiCwms7gpI 建议将IDE工具升级到 1.03.24以上,避免一些BUG [图片] 如有疑问请留言~ 觉得有用,请点个赞哦,让我继续分享更有动力~
2021-04-13 - 如何在云开发中优雅地管控 CDN 流量?
在微信开放社区中,有不少使用云开发的小伙伴反馈遇到了“CDN流量消耗如流水”的情况。 有一觉醒来超额的: [图片] 有被高质量图片的加载“吓”到不敢用的: [图片] 遇到以上情况不要慌,很可能是你的使用姿势不对! 那么问题来了,如何在云开发中优雅地管控 CDN 流量消耗呢?本文就来和你详细聊聊! 按量付费和管道付费 为了便于理解,先来看看云开发的流量计费模式。 简单来说,按量计费就是:你有多少个量跑出去了,就给你算多少量;但是它并不限制你同一时间跑出去的量,也就是流量峰值不设限。 如果你在同一时间需要跑出去100M的量,那么峰值就给你开到100M,在同一时间跑完,最后算费用是100M的钱。 而管道付费则是限制给你开多宽的“道路”,按照这个“道路”的宽窄收费,比如你选择1M的网络道路,那就按照1M的价格来收钱;在使用时,你的量只能达到1M的速度,再也高不了了,这个最大速度就被称做带宽。 但是即使低峰没多少量时,你还必须为这个“道路”付钱。还是上述例子,你同一时间需要跑出去100M,但是速度只能给你开到1M,那么这些量在100秒后才可以走完,最后算费用是1M的道路租金*租用时长。 而在现实项目中,总会有业务的高峰和低峰,流量几乎不可能始终保持在一个恒定的速率,这就会造成管道计费有速度的天花板、但闲置时仍在计费的资源浪费问题。 云开发作为 Serverless 云原生一体化后端服务,提供的流量是按量计费的,不限制同一时刻的流量速度,因此,如果开发者使用不合理就会导致流速过快,进而造成“一眨眼的功夫,流量就超额了”的情况。 了解了按量付费和管道付费的区别后,咱们进入正题,说说 CDN。 什么是CDN流量? 我们在使用云开发时,几乎在各处都会看到CDN这个词。 CDN又称内容分发网络,通俗来讲就是将你主存储(源站)中的文件,复制给各地的存储点(CDN节点),当有用户访问这个资源时,直接从就近的存储点(CDN节点)获取即可。 云开发的云存储和静态网站托管天然支持 CDN 加速,所以你的用户通过客户端下载文件跑的流量都是 CDN 流量。 以上 CDN 流量只适用于各种渠道走加速公网下载文件的情况,比如浏览器加载网站,客户端下载资源,外部系统请求文件,通过临时地址的各种访问打开文件都在消耗 CDN 流量。 但是如果是内网访问文件,则不走 CDN 流量消耗,比如在云函数中通过 fileID 访问文件等。 什么是CDN回源流量? 当我们的存储中有文件更新时,存储在 CDN 节点的旧文件又该如何处理呢?在这里引入一个知识点——缓存时间。 这里的缓存时间其实就是文件副本在各地存储点(CDN节点)的有效时间,比如默认是两小时,那么每次文件副本在各地存储点的有效时间就是两小时,超过这个时间之后再收到请求时,存储点(CDN节点)就会丢弃过期的旧文件,向主存储(源站)请求最新的文件,而这一请求所产生的流量就称为 CDN 回源流量。 缓存时间既不能太长也不可太短,如果 CDN 缓存间隔时间过短,那么 CDN 节点上的数据会经常失效,导致频繁回源,增加了源站的负载,进而影响了整体的传输效率;如果缓存间隔时间过长,会带来数据更新不及时等严重的业务问题。 云开发的云存储就提供了非常细微颗粒度的缓存时间设置,你可以针对一个文件、一个路径甚至是文件后缀来进行分别设置。 [图片] 另外,多个缓存规则设置中还有优先级策略,调配变得更加灵活。 云存储是以从后到前的配置模式来做策略计算的,比如一个云存储的域名做了如下缓存配置: [图片] 现在请求此路径下资源/test/abc.jpg,其从后到前匹配方式如下: 匹配第一条所有文件,命中,此时缓存时间为 2 分钟。 匹配第二条,未命中。 匹配第三条,命中,此时缓存时间为300秒。 匹配第四条,命中,此时缓存时间为400秒。 匹配第五条,命中,此时缓存时间为200秒。 如何合理管控CDN? 我们需要把握一个原则: 缩减大小,善用缓存。 缩减大小的意思就是,我们在开发应用时,所需要的多媒体文件,如果没有特殊要求(比如摄像馆的原图发送),需要尽可能的压缩。只有减轻了业务资源的大小,才能够根本的减少流量资源的消耗。 另外,用户在上传多媒体资源时,仍然可以使用平台或框架能力对资源进行压缩后再上传,保证资源都是经过优化后进入存储,这样在请求下载时就会减轻很多负担。 善用缓存意思就是同一个用户、同一份资源尽可能不要请求一次以上,要合理使用客户端的本地缓存能力,将固定资源全部缓存。当用户再此进入时,直接使用缓存的资源。 例如,微信小程序官方就提供了图片缓存配置,开启后所有图片均进行缓存,下次读取相同资源时,直接从缓存中读取。 文档链接:http://mrw.so/6wT3TR 再送给大家一句网络金句:缓存用的好,PV 的效果用的只是 UV 的量。 而在具体使用时,大家还是需要根据自己的业务情况来合理把握。比如你的资源变更非常频繁,就不太适合缓存优化;而你的资源不能压缩,要保持原大小,则就不适合压缩优化。 结语 以上攻略送给各位 hold 不住 CDN 流量的小伙伴们,如果大家觉得 CDN 消耗如流水,用户活跃却没有多少起色,可能就需要好好检查一下自己哪里浪费了。勤俭节约可是中华民族的传统美德,云开发虽好,也需要节约使用哦~ 小程序·云开发「错误监控」功能有奖调研 诚邀各位云开发者参与小程序·云开发「错误监控」功能有奖调研,参与即送小礼品。 [图片] 期待您的宝贵建议,快扫描下图中的二维码参与吧! 产品介绍 云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2021-01-21 - 请问可以关闭云储存的CDN功能?
如题。
2021-01-26 - 小程序云开发之二维码生成器
该功能主要是将你输入的文字或者网站亦或者上传的图片生成二维码,别人通过扫描二维码就可访问你输入的内容或者打开对应的网站亦或者图片。页面的模板和样式这里就不赘述了,直奔主题。如何通过js实现这些功能呢? 一、监听用户输入 输入的文本或者网址/* 小程序端 js */ // 监听用户输入的文本 bindText: function (e) { this.setData({ textVal: e.detail.value }) }, // 监听用户输入的网站 bindWebsite: function (e) { this.setData({ websiteVal: e.detail.value.replace(/[\u4e00-\u9fa5]/g, '') }) }, 上传图片/* 小程序端 js */ // 上传图片 uploadImg: function() { const that = this dbUpload({ name: 'album', data: { openid: wx.getStorageSync('openid'), time: util.formatDate(new Date()) + ' ' + util.formatTime(new Date()) } }).then( async res=>{ if (res.url) { success('上传成功') that.setData({ uploadedImg: res.url, showImg: false }) } else { fail('上传失败!') } }) }, 图片生成二维码的思路:将图片传给后台,返回该图片的url及地址,然后将这个地址生成二维码使用。 textVal文本,websiteVal网址,uploadedImg图片地址 其中 dbUpload() 方法进行了封装,封装代码如下,这个小程序是云开发,小程序端可直接操作云开发后台数据库wx.cloud.uploadFile() /* 小程序端 util.js封装 */ //添加或者更新数据 export const dbAdd = (param) => { if (param.loading) { wx.showLoading({ title: '正在处理...' }) } const promise = new Promise((resolve, reject) => { let control ={}; if (param.where){ //数据替换 control = db.collection(param.name).doc(param.where).update({ data: param.data }) }else{ //直接添加 control = db.collection(param.name).add({ data:param.data }) } control.then(res => { resolve(res) // tip('操作成功!') }) .catch(err=>{ tip('操作失败,请重试!') }) }) return promise } //上传图片 export const dbUpload = (param) => { const promise = new Promise((resolve, reject) => { // 待插入的集合名字 let nameStr = param.name wx.chooseImage({ sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: function (res) { wx.showLoading({ title: '正在上传...' }) //文件名称 let filePath = res.tempFilePaths[0]; const name = parseInt(Math.random() * 1000000000); const cloudPath = nameStr + name + filePath.match(/\.[^.]+?$/)[0] if (param.fileId){ //更新文件得情况则先删除文件 wx.cloud.deleteFile({ fileList: [param.fileId] }) } //上传文件到服务器 wx.cloud.uploadFile({ cloudPath,//云存储图片名字 filePath//临时路径 }).then( async res=>{ const uploadedImg = await wx.cloud.getTempFileURL({ fileList: [{ fileID: res.fileID, }] }).then(res => { return res.fileList[0].tempFileURL }) if (param.data) { param.data['fileID'] = res.fileID; param.data['url'] = uploadedImg; //存储或者更新数据 const data = await dbAdd({ name: nameStr, where: param.where || null, data: param.data }); data['fileID'] = res.fileID; data['url'] = uploadedImg; return data } else { return uploadedImg } }).then(res=>{ resolve(res) }); } }) }) return promise } 在数据库中新建集合 album ,我这里已经新建好了。 [图片] 二、设置画布大小(及二维码容器) // 设置画布大小 setCanvasSize: function () { const size = {}; try { const res = wx.getSystemInfoSync(); const scale = 750 / 370; const width = res.windowWidth / scale; const height = width;//canvas画布为正方形 size.w = width; size.h = height; } catch (e) { // Do something when catch error console.log("获取设备信息失败" + e); } return size; }, 三、生成二维码 在生成二维码时,引入了一个js库,需要的请关注公众号留言哦! const QR = require("../../../../utils/qrcode.js"); //调用插件中的draw方法,绘制二维码图片 createQrCode: function (str, canvasId, cavW, cavH) { QR.api.draw(str, canvasId, cavW, cavH); }, // 生成二维码 makeQrcode: function () { const active = this.data.active; const textVal = this.data.textVal.trim(); const websiteVal = this.data.websiteVal.trim(); if (active==0) { if (!textVal) return false; this.createQrCode(this.data.textVal, "qrcCanvas", this.size.w, this.size.h); } else if (active == 1) { if (!websiteVal) return false; this.createQrCode(this.data.websiteVal, "qrcCanvas", this.size.w, this.size.h); } else if(active == 2) { if (this.data.showImg) return false; this.createQrCode(this.data.uploadedImg, "qrcCanvas", this.size.w, this.size.h); } this.setData({ canvasHidden: false, }) }, 四、保存二维码至相册 通过画布 wx.canvasToTempFilePath() 生成图片 小程序api:wx.saveImageToPhotosAlbum()保存图片(需用户授权) // 保存二维码 save: function () { const that = this; if (!that.data.canvasHidden) { wx.canvasToTempFilePath({ canvasId: 'qrcCanvas', success: (res) => { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: (res) => { success('保存成功') }, fail: (err) => { if (err.errMsg == 'saveImageToPhotosAlbum:fail auth deny') { wx.showModal({ title: '提示', content: '您未授权,请点击底部按钮打开授权!', showCancel: false }) that.setData({ openSettingBtnHidden: false }) } } }) }, fail: (err) => { console.log(err) } }, that) } }, [图片] [图片] 识别小程序二维码体验功能
2020-04-08 - 图片批量鉴黄,如何实现按图片上传顺序返回结果?
期望的结果 index = 0 按(图片压缩成功)=>(图片格式转换成功)=>(图片检测结果)的顺序执行完,再执行index = 1 [图片] onChangeFlockData: function (e) { wx.chooseImage({ count: 3, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: async res => { const tempFilePaths = res.tempFilePaths; this.setData({ tempFile: [...this.data.tempFile, ...tempFilePaths] }); for (let i = 0; i < this.data.tempFile.length; i++) { await this.compressImg(this.data.tempFile[i], i) } } }) }, //图片压缩 async compressImg(imgUrl, index) { return new Promise((resolve, reject) => { wx.getImageInfo({ src: imgUrl, }).then(res => { const imgInfo = res.path; const imgWidth = res.width; const imgHeight = res.height; const query = wx.createSelectorQuery() query.select('#canvas') .fields({ node: true, size: true }) .exec(async res => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = wx.getSystemInfoSync().pixelRatio; const imgW = Math.trunc(imgWidth / dpr); const imgH = Math.trunc(imgW / imgWidth * imgHeight); canvas.width = imgW; canvas.height = imgH; ctx.clearRect(0, 0, imgW, imgH); this.setData({ canvasWidth: imgW, canvasHeight: imgH }); let imageObj = canvas.createImage(); imageObj.src = imgInfo; imageObj.onload = (res) => { ctx.drawImage(imageObj, 0, 0, imgW, imgH) }; const cfgSave = { fileType: "jpg", quality: 0.5, width: imgW, height: imgH, destWidth: imgW, destHeight: imgH, canvas: canvas, }; wx.canvasToTempFilePath({ ...cfgSave, }).then(async res => { console.log("图片压缩成功:::", res.tempFilePath + "index:::", index) resolve(res.tempFilePath) let tempUrl = res.tempFilePath; await this.imgSecCheck(tempUrl, index); }).catch(err => { reject(err) }) }) }) }) }, //图片送审 imgSecCheck: async function (tempUrl, index) { wx.showLoading({ mask: true }); wx.getFileSystemManager().readFile({ filePath: tempUrl, encoding: "base64", success: async (res) => { console.log("图片格式转换成功:::", res + "index:::", index); let imgBuffer = res.data await this.wait(imgBuffer, index) }, fail: err => { console.error(err); }, }) }, wait: async function (imgBuffer, index) { return new Promise((resolve, reject) => { wx.cloud.callFunction({ name: "msgSecCheck", data: { type: 'imgSecCheckBuffer', value: imgBuffer, } }).then(res => { console.log("图片检测结果:", res.result + "index:::", index) wx.hideLoading() if (res.result.errCode === 87014) { wx.hideLoading() wx.showToast({ title: '图片含有违法违规内容', icon: 'none' }) return } resolve(res.result) // this._uploadImg(tempUrl) }).catch(err => { console.log(err) reject(err) wx.hideLoading() wx.showModal({ title: '提示', content: '图片尺寸过大,请调整图片尺寸', success(res) { if (res.confirm) { console.log('用户点击确定') } else if (res.cancel) { console.log('用户点击取消') } } }) }) }) },
2021-05-19 - 云开发安全规则讲解
安全规则是一个可以灵活地自定义数据库和云存储读写权限的权限控制方式,通过配置安全规则,开发者可以在小程序端、网页端精细化的控制云存储和集合中所有记录的增、删、改、查权限,自动拒绝不符合安全规则的前端数据库与云存储请求,保障数据和文件安全。 12.2.1 {openid} 变量在前面我们建议使用安全规则取代简易版的权限设置,当使用安全规则之后,这里有一个重要的核心就是 {openid} 变量 ,无论在前端(小程序端、web 端)查询时,它都是必不可少的(也就是说云函数,云开发控制台不受安全规则控制)。 1、查询写入都需明确指定 openid{openid} 变量在小程序端使用时无需先通过云函数获取用户的 openid,直接使用[代码]'{openid}'[代码]即可,而我们在查询时都需要显式传入 openid。之前我们使用简易权限配置时不需要这么做,这是因为查询时会默认给查询条件加上一条 _openid 必须等于用户 openid,但是使用安全规则之后,就没有这个默认的查询条件了。 比如我们在查询 collection 时,都需要在 where 里面添加如下如下的条件,{openid}变量就会附带当前用户的 openid。 db.collection("china").where({ _openid: "{openid}", //安全规则里有auth.openid时都需要添加 }); 更新、删除等数据库的写入请求也都需要明确在 where 里添加这样的一个条件(使用安全规则后,在小程序端也可以进行批量更新和删除)。 db.collection("goods") .where({ _openid: "{openid}", category: "mobile", }) .update({ //批量更新 data: { price: _.inc(1), }, }); 开启安全规则之后,都需要在 where 查询条件里指定[代码]_openid: '{openid}'[代码],这是因为大多数安全规则里都有[代码]auth.openid[代码],也就是对用户的身份有要求,where 查询条件为安全规则的子集,所以都需要添加。当然你也可以根据你的情况,安全规则不要求用户的身份,也就可以不传入[代码]_openid: '{openid}'[代码]了。 2、doc 操作需转为 where 操作由于我们在进行执行 doc 操作[代码]db.collection('china').doc(id)[代码]时,没法传入 openid 的这个条件,那应该怎么控制权限呢?这时候,我们可以把 doc 操作都转化为 where 操作就可以了,在 where 查询里指定 _id 的值,这样就只会查询到一条记录了: db.collection("china").where({ _id: "tcb20200501", //条件里面加_id _openid: "{openid}", //安全规则里有auth.openid时都需要添加 }); 至于其他的 doc 操作,都需要转化为基于 collection 的 where 操作,也就是说以后不再使用doc 操作[代码]db.collection('china').doc(id)[代码]了。其中 doc.update、doc.get 和 doc.remove 可以用基于 collection 的 update、get、remove 取代,doc.set 可以被更新指令[代码]_.set[代码]取代。当然安全规则只适用于前端(小程序端或 Web 端),后端不受安全规则的权限限制。 3、嵌套数组对象里的 openid在使用简易权限配置时,用户在小程序端往数据库里写入数据时,都会给记录 doc 里添加一个_openid 的字段来记录用户的 openid,使用安全规则之后同样也是如此。在创建记录时,可以把{openid}变量赋值给非_openid 的字段或者写入到嵌套数组里,后台写入记录时发现该字符串时会自动替换为小程序用户的 openid: db.collection("posts").add({ data: { books: [ { title: "云开发快速入门", author: "{openid}", }, { title: "数据库入门与实战", author: "{openid}", }, ], }, }); 以往要进行 openid 的写入操作时需要先通过云函数返回用户 openid,使用安全规则之后,直接使用{openid}变量即可,不过该方法仅支持 add 添加一条记录时,不支持 update 的方式。 12.2.2 安全规则的写法使用安全规则之后,我们可以在控制台(开发者工具和网页)对每个集合以及云存储的文件夹分别配置安全规则,也就是自定义权限,配置的格式是 json,仍然严格遵循 json 配置文件的写法(比如数组最后一项不能有逗号,,配置文件里不能有注释等)。 1、粒度更细的增删改查我们先来看简易权限配置[代码]所有用户可读,仅创建者可写[代码]、[代码]仅创建者可读写[代码]、[代码]所有用户可读[代码]、[代码]所有用户不可读写[代码]所对应的安全规则的写法,这个 json 配置文件的[代码]key[代码]表示操作类型,[代码]value[代码]是一个表达式,也是一个条件,解析为 true 时表示相应的操作符合安全规则。 // 所有人可读,仅创建者可读写 { "read": true, "write": "doc._openid == auth.openid" } //仅创建者可读写 { "read": "doc._openid == auth.openid", "write": "doc._openid == auth.openid" } //所有人可读 { "read": true, "write": false } //所有用户不可读写 { "read": false, "write": false } 简易的权限配置只有读 read 与写 write,而使用安全规则之后,支持权限操作有除了读与写外,还将写权限细分为 create 新建、update 更新、delete 删除,也就是既可以只使用写,也可以细分为增、删、改,比如下面的案例为 [代码]所有人可读,创建者可写可更新,但是不能删除[代码] "read": true, "create":"auth.openid == doc._openid", "update":"auth.openid == doc._openid", "delete":false 操作类型无外乎增删改查,不过安全规则的 value 是条件表达式,写法很多,让安全规则也就更加灵活。值得一提的是,如果我们不给 read 或者 write 赋值,它们的默认值为 false。 2、所有用户可读可写的应用安全规则还可以配置所有人可读可写的类型,也就是如下的写法,让所有登录用户(用户登录了之后才有 openid,即 openid 不为空)可以对数据可读可写。 { "read": "auth.openid != null", "write": "auth.openid != null" } 在小程序端,我们可以把数据库集合的安全规则操作 read 和 write 都写为 true(这是所有人可读可写,而这里强调的是所有用户),因为只要用户使用开启了云开发的小程序,就会免鉴权登录有了 openid,但是上面安全规则的写法则通用于云存储、网页端的安全规则。集合里的数据让所有用户可读可写在很多方面都有应用,尤其是我们希望有其他用户可以对嵌套数组和嵌套对象里的字段进行更新时。比如集合 posts 存储的是所有资讯文章,而我们会把文章的评论嵌套在集合里。 { _id:"tcb20200503112", _openid:"用户A", //用户A也是作者,他发表的文章 title:"云开发安全规则的使用经验总结", stars:223, comments:[{ _openid:"用户B", comment:"好文章,作者有心了", }] } 当用户 A 发表文章时,也就会创建这条记录,如果用户 B 希望可以评论(往数组 comments 里更新数据)、点赞文章(使用 inc 原子更新更新 stars 的值),就需要对该记录可读可写(至少是可以更新)。这在简易权限配置是无法做到的(只能使用云函数来操作),有了安全规则之后,一条记录就可以有被多个人同时维护的权限,而这样的场景在云开发这种文档型数据库里比较常见(因为涉及到嵌套数组嵌套对象)。 安全规则与查询 where 里的条件是相互配合的,但是两者之间又有一定的区别。所有安全规则的语句指向的都是符合条件的文档记录,而不是集合。使用了安全规则的 where 查询会先对文档进行安全规则的匹配,比如小程序端使用 where 查询不到记录,就会报错[代码]errCode: -502003 database permission denied | errMsg: Permission denied[代码],然后再进行条件匹配,比如安全规则设置为所有人可读时,当没有符合条件的结果时,会显示查询的结果为 0。我们要注意无权查询和查询结果为 0 的区别。 3、全局变量要搞清楚安全规则写法的意思,我们还需要了解一些全局变量,比如前面提及的[代码]auth.openid[代码]表示的是登录用户的 openid,而[代码]doc._openid[代码]表示的是当前记录[代码]_openid[代码]这个字段的值,当用户的 openid 与当前记录的_openid 值相同时,就对该记录有权限。全局变量还有 now(当前时间戳)和 resource(云存储相关)。 变量类型说明[代码]auth[代码]object用户登录信息,[代码]auth.openid[代码] 也就是用户的openid,如果是在web端它还有loginType登录方式、uid等值[代码]doc[代码]object表示[代码]当前记录[代码]的内容,用于匹配记录内容/查询条件[代码]now[代码]number当前时间的时间戳,也就是以从计时原点开始计算的毫秒[代码]resource[代码]object[代码]resource.openid[代码]为云存储文件私有归属标识,标记所有者的openid 4、运算符安全规则的表达式还支持运算符,比如等于[代码]==[代码],不等于[代码]!=[代码],大于[代码]>[代码],大于等于[代码]>=[代码],小于[代码]<[代码],小于等于[代码]<=[代码],与[代码]&&[代码],或[代码]||[代码]等等,后面会有具体的介绍。 运算符说明示例==等于auth.openid == 'zzz'用户的 openid 为 zzz!=不等于auth.openid != 'zzz'用户的 openid 不为 zzz>大于doc.age>10查询条件的 age 属性大于 10>=大于等于doc.age>=10查询条件的 age 属性大于等于 10<小于doc.age<10查询条件的 age 属性小于 10<=小于等于doc.age<=10查询条件的 age 属性小于等于 10in存在在集合中auth.openid in ['zzz','aaa']用户的 openid 是['zzz','aaa']中的一个!(xx in [])不存在在集合中,使用 in 的方式描述 [代码]!(a in [1,2,3])[代码]!(auth.openid in ['zzz','aaa'])用户的 openid 不是['zzz','aaa']中的任何一个&&与auth.openid == 'zzz' && doc.age>10用户的 openid 为 zzz 并且查询条件的 age 属性大于 10||或auth.openid == 'zzz' || doc.age>10用户的 openid 为 zzz 或者查询条件的 age 属性大于 10.对象元素访问符auth.openid用户的 openid[]数组访问符属性doc.favorites[0] == 'zzz'查询条件的 [代码]favorites[代码] 数组字段的第一项的值等于 [代码]zzz[代码] 12.2.3 身份验证全局变量 auth 与 doc 的组合使用可以让登录用户的权限依赖于记录的某个字段,auth 表示的是登录用户,而 doc、resource 则是云开发环境的资源相关,使用安全规则之后用户与数据库、云存储之间就有了联系。resource 只有 resource.openid,而 doc 不只有_openid,还可以有很多个字段,也就让数据库的权限有了很大的灵活性,后面我们更多的是以 doc 全局变量为例。 1、记录的创建者auth.openid 是当前的登录用户,而记录 doc 里的 openid 则可以让该记录与登录用户之间有紧密的联系,或者可以说让该记录有了一个身份的验证。一般来说 doc._openid 所表示的是该记录的创建者的 openid,简易权限控制比较的也是当前登录用户是否是该记录的创建者(或者为更加开放且粗放的权限)。 //登录用户为记录的创建者时,才有权限读 "read": "auth.openid == doc._openid", //不允许记录的创建者删除记录(只允许其他人删除) "delete": "auth.openid != doc._openid", 安全规则和 where 查询是配套使用的,如果你指定记录的权限与创建者的 openid 有关,你在前端的查询条件的范围就不能比安全规则的大(如果查询条件的范围比安全规则的范围大就会出现[代码]database permission denied[代码]: db.collection('集合id').where({ _openid:'{openid}' //有doc._openid,因此查询条件里就需要有_openid这个条件, key:"value" }) .get().then(res=>{ console.log(res) }) 2、指定记录的角色01把权限指定给某个人 安全规则的身份验证则不会局限于记录的创建者,登录用户的权限还可以依赖记录的其他字段,我们还可以给记录的权限指定为某一个人(非记录的创建者),比如很多个学生提交了作业之后,会交给某一个老师审阅批改,老师需要对该记录有读写的权限,在处理时,可以在学生提交作业(创建记录 doc)时时可以指定 teacher 的 openid,只让这个老师可以批阅,下面是文档的结构和安全规则示例: //文档的结构 { _id:"handwork20201020", _openid:"学生的openid", //学生为记录的创建者, teacher:"老师的openid" //该学生被指定的老师的openid } //安全规则 { "read": "doc.teacher == auth.openid || doc._openid == auth.openid", "write": "doc.teacher == auth.openid || doc._openid == auth.openid", } 让登录用户 auth.openid 依赖记录的其他字段,在功能表现上相当于给该记录指定了一个角色,如直属老师、批阅者、直接上级、闺蜜、夫妻、任务的直接指派等角色。 对于查询或更新操作,输入的 where 查询条件必须是安全规则的子集,比如你的安全规则如果是[代码]doc.teacher == auth.openid[代码],而你在 where 里没有[代码]teacher:'{openid}'[代码]这样的条件,就会出现权限报错。由于安全规则和 where 查询需要配套使用,安全规则里有[代码]doc.teacher[代码]和[代码]doc._openid[代码],在 where 里也就需要写安全规则的子集条件,比如[代码]_openid:'{openid}'[代码]或[代码]teacher:'{openid}'[代码],由于这里老师也是用户,我们可以传入如下条件让学生和老师共用一个数据库请求: const db = wx.cloud.database(); const _ = db.command; //一条记录可以同时被创建者(学生)和被指定的角色(老师)读取 db.collection("集合id") .where( _.or([ { _openid: "{openid}" }, //与安全规则doc._openid == auth.openid对应 { teacher: "{openid}" }, //与安全规则doc.teacher == auth.openid对应 ]) ) .get() .then((res) => { console.log(res); }); 2、把权限指定给某些人 上面的这个角色指定是一对一、或多对一的指定,也可以是一对多的指定,可以使用[代码]in[代码]或[代码]!(xx in [])[代码]运算符。比如下面是可以给一个记录指定多个角色(学生创建的记录,多个老师有权读写): //文档的结构 { _id:"handwork20201020", _openid:"学生的openid", //学生为记录的创建者, teacher:["老师1的openid","老师2的openid","老师3的openid"] } //安全规则 { "read": "auth.openid in doc.teacher || doc._openid == auth.openid", "write": "auth.openid in doc.teacher || doc._openid == auth.openid", } 这里要再强调的是前端(小程序端)的 where 条件必须是安全规则权限的子集,比如我们在小程序端针对老师进行如下查询([代码]'{openid}'[代码]不支持查询指令,需要后端获取) db.collection("集合id") .where({ _openid: "{openid}", teacher: _.elemMatch(_.eq("老师的openid")), }) .get() .then((res) => { console.log(res); }); 前面我们实现了将记录的权限指定给某个人或某几个人,那如何将记录的权限指定给某类人呢?比如打车软件为了数据的安全性会有司机、乘客、管理员、开发人员、运维人员、市场人员等,这都需要我们在数据库里新建一个字段来存储用户的类型,比如[代码]{role:3}[代码],用 1、2、3、4 等数字来标明,或者用[代码]{isManager:true}[代码]boolean 类型来标明,这个新增的字段可以就在查询的集合文档里[代码]doc.role[代码],或者是一个单独的集合(也就是存储权限的集合和要查询的集合是分离的,这需要使用 get 函数跨集合查询),后面会有具体介绍。 3、doc.auth 与文档的创建者 下面有一个例子可以加深我们对安全规则的理解,比如我们在记录里指定文档的 auth 为其他人的 openid,并配上与之相应的安全规则,即使当前用户实际上就是这个记录的创建者,这个记录有该创建者的_openid,他也没有操作的权限。安全规则会对查询条件进行评估,只要符合安全规则,查询才会成功,违反安全规则,查询就会失败。 //文档的结构,比如以下为一条记录 { _id:"handwork20201020", _openid:"创建者的openid", auth:"指定的auth的openid" } //安全规则 { "权限操作": "auth.openid == doc.auth" //权限操作为read、write、update等 } //前端查询,不符合安全规则,即使是记录的创建者也没有权限 db.collection('集合id').where({ auth:'{openid}' }) 12.2.4 安全规则常用场景简易版权限设置没法在前端实现记录跨用户的写权限(含 update、create、delete),也就是说记录只有创建者可写。而文档型数据库一个记录因为反范式化嵌套的原因可以承载的信息非常多,B 用户操作 A 用户创建的记录,尤其是使用更新指令 update 字段以及内嵌字段的值这样的场景是非常常见的。除此之外,仅安全规则可以实现前端对记录的批量更新和删除。 比如我们可以把评论、收藏、点赞、转发、阅读量等信息内嵌到文章的集合里,以往我们在小程序端(只能通过云函数)是没法让 B 用户对 A 用户创建的记录进行操作,比如点赞、收藏、转发时用更新指令 inc 更新次数,比如没法直接用更新指令将评论 push 写入到记录里: { _id:"post20200515001", title:"云开发安全规则实战", star:221, //点赞数 comments:[{ //评论和子评论 content:"安全规则确实是非常好用", nickName:"小明", subcomment:[{ content:"我也这么觉得", nickName:"小军", }] }], share:12, //转发数 collect:15 //收藏数 readNum:2335 //阅读量 } 在开启安全规则,我们就可以直接在前端让 B 用户修改 A 用户创建的记录,这样用户阅读、点赞、评论、转发、收藏文章等时,就可以直接使用更新指令对文章进行字段级别的更新。 "read":"auth.openid != null", "update":"auth.openid != null" 这个安全规则相比于[代码]所有人可读,仅创建者可读写[代码],开放了 update 的权限,小程序端也有 limit 20 的限制。而如果不使用安全规则,把这些放在云函数里进行处理不仅处理速度更慢,而且非常消耗云函数的资源。 db.collection("post") .where({ _id: "post20200515001", openid: "{openid}", }) .update({ data: { //更新指令的应用 }, }); 12.2.5 数据验证 doc 的规则匹配我们还可以把访问权限的控制信息以字段的形式存储在数据库的集合文档里,而安全规则可以根据文档数据动态地允许或拒绝访问,也就是说doc 的规则匹配可以让记录的权限动态依赖于记录的某一个字段的值。 doc 规则匹配的安全规则针对的是整个集合,而且要求集合里的所有记录都有相应的权限字段,而只有在权限字段满足一定条件时,记录才有权限被增删改查,是一个将集合的权限范围按照条件要求收窄的过程,where 查询时的条件不能比安全规则规定的范围大(查询条件为安全规则子集);配置了安全规则的集合里的记录只有两种状态,有权限和没有权限。 这里仍然再强调的是使用 where 查询时要求查询条件是安全规则的子集,在进行 where 查询前会先解析规则与查询条件进行校验,如果 where 条件不是安全规则的子集就会出现权限报错,不能把安全规则看成是一个筛选条件,而是一个保护记录数据安全的不可逾越的规则。 1、记录的状态权限doc 的规则匹配,特别适合每个记录存在多个状态或每个记录都有一致的权限条件(要么全部是,要么全部否),而只有一个状态或满足条件才有权限被用户增删改查时的情形,比如文件审批生效(之前存在审批没有生效的多个状态),文章的发布状态为 pubic(之前为 private 或其他状态),商品的上架(在上架前有多个状态),文字图片内容的安全检测不违规(之前在进行后置校验),消息是否撤回,文件是否删除,由于每个记录我们都需要标记权限,而只有符合条件的记录才有被增删改查的机会。 比如资讯文章的字段如下,每个记录对应着一篇文章,而 status 则存储着文章的多个状态,只有 public 时,文章才能被用户查阅到,我们可以使用安全规则[代码]"read": "doc.status=='public'"[代码]。而对于软删除(文章假删除),被删除可以作为一个状态,但是文章还是在数据库里。 { _id:"post2020051314", title:"云开发发布新能力,支持微信支付云调用", status:"public" }, { _id:"post2020051312", title:"云函数灰度能力上线", status:"edit" }, { _id:"post2020051311", title:"云开发安全规则深度研究", status:"delete" } 而在前端(小程序端)与之对应的数据库查询条件则必须为安全规则的子集,也就是说安全规则不能作为你查询的过滤条件,安全规则会对查询进行评估,如果查询不符合安全规则设置的约束(非子集),那么前端的查询请求没有权限读取文档,而不是过滤文档: db.collection("集合id") .where({ status: "public", //你不能不写这个条件,而指望安全规则给你过滤 }) .get() .then((res) => { console.log(res); }); 2、记录禁止为空有时候我们需要对某些记录有着非常严格的要求,禁止为空,如何为空一律不予被前端增删改查,比如已经上架的 shop 集合里的商品列表,有些核心数据如价格、利润、库存等就不能为空,给企业造成损失,相应的安全规则和查询如下: //安全规则 { "权限操作": "doc.profit != null", } //权限操作,profit = 0.65就是安全规则的子集 db.collection('shop').where({ profit:_.eq(0.65) }) 3、记录的子集权限安全规则记录的字段值不仅限于一个状态(字符串类型),还可以是可以运算的范围值,如大于[代码]>[代码],小于[代码]<[代码]、[代码]in[代码]等,比如商品的客单价都是 100 以上,管理员在后端(控制台,云函数等)把原本 190 元的价格写成了 19,或者失误把价格写成了负数,这种情况下我们对商品集合使用安全规则[代码]doc.price > 100[代码],前端将失去所有价格低于 100 的商品的操作权限,包括查询。 //安全规则 "操作权限":"doc.price > 100" //相应的查询 db.collection('shop').where({ price:_eq(125) }) 安全规则的全局变量 now 表示的是当前时间的时间戳,这让安全规则可以给权限的时间节点和权限的时效性设置一些规则,这里就不具体讲述了。 12.2.5 全局函数 get 构建权限体系全局函数 get 可以实现跨集合来限制权限。doc 的权限匹配更多的是基于文档性质的权限,也就是集合内所有文档都有相同的字段,根据这个字段的值的不同来划分权限。但是有时候我们希望实现多个用户和多个用户角色来管理集合的文档,拥有不同的权限,如果把用户和角色都写进文档的每个记录里,就会非常难以管理。也就是说 doc 的权限匹配并不适合复杂的用户管理文档的权限体系。 我们可以把单个复杂的集合文档(反范式化的设计)拆分成多个集合文档(范式化设计),将用户和角色从文档里分离出来。比如博客有文章 post 集合,而 user 集合除了可以把用户划分为作者、编辑、投稿者这样的用户身份,还可以是管理员组,编辑组等。如果我们把记录的权限赋予给的人员比较多或群组比较复杂,则需要把角色存储在其独立的集合中,而不是作为目标文档中的一个字段,用全局函数 get 来实现跨集合的权限限制。 get 函数是全局函数,可以跨集合来获取指定的记录,用于在安全规则中获取跨集合的记录来参与到安全规则的匹配中,get 函数的参数格式是 [代码]database.集合名.记录id[代码]。 比如我们可以给文章 post 集合设置如下安全规则,只有管理员才可以删除记录,而判断用户是否为管理员则需要跨集合用 user 集合里的字段值来判断: //user集合的结构 { _id:"oUL-m5FuRmuVmxvbYOGuXbuEDsn8", //用户的openid isManager:true } //post集合的权限 { "read": "true", "delete": "get(`database.user.${auth.openid}`).isManager== true" } db.collection('post').where({ //相应的条件,并不受子集的限制 }) get 函数还可以接收变量,值可以通过多种计算方式得到,例如使用字符串模版进行拼接,这是一个查询的过程,如果相应的文档里有记录,则函数返回记录的内容,否则返回空(注意反引号的写法): `(database.${doc.collction}.${doc._id})`; get 函数的限制条件 安全规则里的 get 函数 参数中存在的变量 doc 需要在 query 条件中以 == 或 in 方式出现,若以 in 方式出现,只允许 in 唯一值, 即 doc.shopId in array, array.length == 1一个表达式最多可以有 3 个 get 函数,最多可以访问 3 个不同的文档。get 函数的嵌套深度最多为 2, 即 get(get(path))。读操作触发与配额消耗说明 get 函数的执行会计入数据库请求数,同样受数据库配额限制。在未使用变量的情况下,每个 get 会产生一次读操作,在使用变量时,对每个变量值会产生一次 get 读操作。例如: 假设某集合 shop 上有如下规则: { "read": "auth.openid == get(`database.shop.${doc._id}`).owner", "write": false } 在执行如下查询语句时会产生 5 次读取。 db.collection("shop") .where(_.or([{ _id: 1 }, { _id: 2 }, { _id: 3 }, { _id: 4 }, { _id: 5 }])) .get();
2021-09-10 - 使用云开发做内容安全检查
效果展示 少啰嗦,看图! [图片] 需求实现分析 需求:输入违规内容,点击提交按钮,提示请注意言论。 技术方案:使用微信提供的 msgSecCheck 函数进行内容校验。 新增一个云函数 配置openapi权限 实现msgSecCheck函数 调用返回验证结果 根据结果给出提示 代码实现 新增一个 msgSecCheck 的云函数 [图片] 配置 config.json [图片] 实现 msgSecCheck 函数 [图片] 调用 msgSecCheck 云函数,根据不同结果进行处理 [图片] 总结 这里面用到了1个关键函数:msgSecCheck。 msgSecCheck:官方文档传送门 如果觉得有收获,欢迎点赞收藏~
2020-08-13 - 实战:如何降低云开发服务器成本?
成本完全hold不住 [图片] 首先我们可以看到这幅图,CDN流量消耗和存储读请求次数特别大。 原因是因为在大量的登记里面上传图片需求非常高频,加上当时「群登记助手」日活高达4w。 我原本采购的 CDN 3(2199/月)套餐完成抗不住这个CDN消耗。 [图片] 所以我直接升级到旗舰 3 套餐。这个套餐每月4699/月(目前该套餐已下线) 作为一名独立开发者来说,这个成本非常高。 成本降低 1 倍 [图片] [图片] 于是我在「微信开放社区」进行了提问 我说什么时候能推出按需计费? 当天就得到了官方人员的回复,已经支持了。 然后我切换完成按量付费模式后,效果很明显。 成本直接从每个月4699降低到了2千多。 整体费用降低了 1 倍,但是这成本还是有点小贵。 再次降低 10 倍 [图片] 后来使用了云扩展的「图片处理」服务。 我使用了图片处理服务的「快速压缩模版」,使得成本再一次的降低。 [图片] 上次是 1 倍,这次是 10 倍,从原来的2249变成了现在的2百多。 总结 从每月4699套餐到每月208的成本,一共降低了20倍成本。当然本次案例是一个CDN流量消耗过大的案例,其他点也可以根据这个思路来优化。 优化思路 先找到消耗的关键点(如:本次案例为CDN) 如果是单个点很高,就按需收费。(套餐其他资源没被利用,也是浪费) 根据具体业务场景来优化次数。(如:请求次数,可以一次性放多点内容,不需要请求多次。CDN流量,可以把列表图片压缩,因为列表不需要看高清图片。)
2020-12-02 - 新鲜出炉一个简易小程序图文编辑器组件,需要的自取
功能比较简单,使用、部署指南也没啥好写的,一看就明白了,有兴趣完善,欢迎参与完善。 音频、视频也是可以加的,只是暂时没加上去 另外文件上传本来做了云存储,不过提交的代码中去掉了,这个和业务逻辑联系在一起的,大伙可以自行开发。 # 项目地址 https://gitee.com/kyalong/zEditor # 编辑器效果图[图片] [图片][图片]
2020-09-19 - 手把手教你如何搭建抽奖小程序
由于该抽奖小程序采用小程序云开发来实现,在阅读本文之前建议您对小程序云开发有一个基本的认知,包括但不限于云函数、数据库集合、触发器、订阅消息等 本文背景本文主要讲述了具体如何部署抽奖小程序v6版本,该版本抽奖小程序目前已正常运营2个月了,可支持单次抽奖人数在3000左右,每天产生的历史记录在8万条 每天投放20个活动,每个活动平均有1500用户侧参与 20*1500=3万条抽奖记录+3万条订阅消息+其他相关记录包括用户 本文内容本抽奖小程序基于小程序云开发,所以部署的时候主要涉及以下几步: (1)代码导入 (2)新建订阅消息 (3)导入集合,并修改集合权限 (4)上传云函数 (5)上传触发器 接下来我细细讲下具体每一步 第一步,代码导入后,记得修改云环境id /Users/xfy/lotteryassistant-v6/miniprogram/cloud.js [图片] 在第二步,新建订阅消息,具体的订阅消息为 截图一 [图片] [图片] 截图二 [图片] 截图三 /Users/xfy/lotteryassistant-v6/miniprogram/app.js [图片] 截图四 /Users/xfy/lotteryassistant-v6/cloudfunctions/sendmore/index.js [图片] [图片] 第三步导入集合,具体位于项目的data目录 具体集合以及权限在截图xnip2020-xx-xx可查 截图五 [图片] 第四步:上传云函数 由于我已经在本地安装了node,所以只需要上传全部即可, 截图六 第五步:上传触发器 由于开奖以及订阅消息推送的逻辑都是用触发器来 控制,所以上传触发器至关重要 截图七 每个整点的0时执行 /Users/xfy/lotteryassistant-v6/cloudfunctions/draw/index.js [图片] 每个整点的5分执行 /Users/xfy/lotteryassistant-v6/cloudfunctions/run/index.js [图片] 截图八 每个整点的10分执行 /Users/xfy/lotteryassistant-v6/cloudfunctions/sendmore/index.js [图片] 截图九 由于小程序不能支持太多历史数据,所以目前小程序只保留了三天数据,需要定时清理 /Users/xfy/lotteryassistant-v6/cloudfunctions/removeHistoryData/index.js [图片] 截图十 扫码体验请具体扫描下放小程序 码进行体验 [图片] 本文总结本文通过截图以及文字描述的形式具体讲述了如何搭建一个抽奖小程序
2020-09-16 - 小程序实现列表拖拽排序
小程序列表拖拽排序 [图片] wxml [代码]<view class='listbox'> <view class='list kelong' hidden='{{!showkelong}}' style='top:{{kelong.top}}px'> <view class='index'>?</view> <image src='{{kelong.xt}}' class='xt'></image> <view class='info'> <view class="name">{{kelong.name}}</view> <view class='sub-name'>{{kelong.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> </view> <view class='list' wx:for="{{optionList}}" wx:key=""> <view class='index'>{{index+1}}</view> <image src='{{item.xt}}' class='xt'></image> <view class='info'> <view class="name">{{item.name}}</view> <view class='sub-name'>{{item.subname}}</view> </view> <image src='/images/sl_36.png' class='more'></image> <view class='moreiconpl' data-index='{{index}}' catchtouchstart='dragStart' catchtouchmove='dragMove' catchtouchend='dragEnd'></view> </view> </view> [代码] wxss [代码].map-list .list { position: relative; height: 120rpx; } .map-list .list::after { content: ''; width: 660rpx; height: 2rpx; background-color: #eee; position: absolute; right: 0; bottom: 0; } .map-list .list .xt { display: block; width: 95rpx; height: 77rpx; position: absolute; left: 93rpx; top: 20rpx; } .map-list .list .more { display: block; width: 48rpx; height: 38rpx; position: absolute; right: 30rpx; top: 40rpx; } .map-list .list .info { display: block; width: 380rpx; height: 80rpx; position: absolute; left: 220rpx; top: 20rpx; font-size: 30rpx; } .map-list .list .info .sub-name { font-size: 28rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #646567; } .map-list .list .index { color: #e4463b; font-size: 32rpx; font-weight: bold; position: absolute; left: 35rpx; top: 40rpx; } [代码] js [代码]data:{ kelong: { top: 0, xt: '', name: '', subname: '' }, replace: { xt: '', name: '', subname: '' }, }, dragStart: function(e) { var that = this var kelong = that.data.kelong var i = e.currentTarget.dataset.index kelong.xt = this.data.optionList[i].xt kelong.name = this.data.optionList[i].name kelong.subname = this.data.optionList[i].subname var query = wx.createSelectorQuery(); //选择id query.select('.listbox').boundingClientRect(function(rect) { // console.log(rect.top) kelong.top = e.changedTouches[0].clientY - rect.top - 30 that.setData({ kelong: kelong, showkelong: true }) }).exec(); }, dragMove: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function(rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top < -60) { kelong.top = -60 } else if (kelong.top > rect.height) { kelong.top = rect.height - 60 } that.setData({ kelong: kelong, }) }).exec(); }, dragEnd: function(e) { var that = this var i = e.currentTarget.dataset.index var query = wx.createSelectorQuery(); var kelong = that.data.kelong var listnum = that.data.optionList.length var optionList = that.data.optionList query.select('.listbox').boundingClientRect(function (rect) { kelong.top = e.changedTouches[0].clientY - rect.top - 30 if(kelong.top<-20){ wx.showModal({ title: '删除提示', content: '确定要删除此条记录?', confirmColor:'#e4463b' }) } var target = parseInt(kelong.top / 60) var replace = that.data.replace if (target >= 0) { replace.xt = optionList[target].xt replace.name = optionList[target].name replace.subname = optionList[target].subname optionList[target].xt = optionList[i].xt optionList[target].name = optionList[i].name optionList[target].subname = optionList[i].subname optionList[i].xt = replace.xt optionList[i].name = replace.name optionList[i].subname = replace.subname } that.setData({ optionList: optionList, showkelong:false }) }).exec(); }, [代码]
2019-07-28 - 实战丨如何制作一个完整的外卖小程序(已开源)
最近微信小店开放了,赶着微信全面开放之前,把自己的小程序开源出来给大家使用~ 小程序效果 [图片] [图片] [图片] 开发心得 如何在项目中集成云开发 一开始项目并非基于云开发而开发的,目前考虑用云开发,因此,需要在项目中开启云开发的相关选项。 首先,在小程序文件夹中建立 [代码]cloud[代码] 文件夹,并在package文件中配置,建立用户登录的云函数并上传到微信小程序云中。相关的操作可以参考官方文档。 我在项目目录中添加了 [代码]cloud[代码] 和 [代码]miniprogram[代码] 两个目录,并在 [代码]project.config.json[代码] 文件夹进行配置 [代码]{ "miniprogramRoot": "./miniprogram" "cloudfunctionRoot": "./cloud/" } [代码] 开通云开发 配置完成后,可以点击控制台中的「云开发」来开通云开发。 [图片] 在云开发的界面中配置,并开通云开发。 [图片] 开通数据库集合 云开发不会自动创建数据库集合,因此,你需要手动创建集合。分别创建 店铺表Seller、分类表Category、商品表Food、订单表Order、地址表Address、用户表*_User*。 [图片] 数据操作 有了数据库的表后,就可以在代码中对数据进行操作了。 下方是我进行目录操作的代码。 [代码]const db = wx.cloud.database() const { showModal } = require('../../utils/utils') Page({ onLoad: function(options) { // 管理员认证 getApp().auth() if (options.objectId) { // 缓存数据 this.setData({ isEdit: true, objectId: options.objectId }) // 请求待编辑的分类对象 db.collection('Category') .doc(options.objectId) .get() .then(res => { // 获取分类信息 this.setData({ category: res.data }) }) } }, add: function(e) { var form = e.detail.value if (form.title == '') { wx.showModal({ title: '请填写分类名称', showCancel: false }) return } form.priority = Number.parseInt(form.priority) // 添加或者修改分类 // 修改模式 if (this.data.isEdit) { const category = this.data.category db.collection('Category') .doc(category._id) .update({ data: form }) .then(res => { console.log(res) showModal() }) } else { db.collection('Category') .add({ data: form }) .then(res => { console.log(res) showModal() }) } }, showModal() { // 操作成功提示并返回上一页 wx.showModal({ title: this.data.isEdit ? '修改成功' : '添加成功', showCancel: false, success: () => { wx.navigateBack() } }) }, delete: function() { // 确认删除对话框 wx.showModal({ title: '确认删除', success: res => { if (res.confirm) { const category = this.data.category db.collection('Category') .doc(category._id) .remove() .then(res => { console.log(res) wx.showToast({ title: '删除成功' }) wx.navigateBack() }) } } }) } }) [代码] 联表查询 在使用数据库时,难免要进行联表查询,云开发支持在云函数侧进行联表查询,你可以参考我的代码,来实现联表查询的功能。 [代码]const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() // 云函数入口函数 exports.main = async (event, context) => { const result = await db.collection('Food') .aggregate() .lookup({ from: 'Category', localField: 'category', foreignField: '_id', as: 'categories' }) .end() // .orderBy('priority', 'asc') // .get() console.log(result) return result.list } [代码] 文件上传 在小程序的操作中,难免会遇到需要进行图片上传的场景。在进行图片上传时,云开发提供了方便的云存储供我们查询数据。 在获取到文件的本地路径后,调用 [代码]wx.cloud.uploadFile[代码] 即可上传文件。 [代码]chooseImage() { wx.chooseImage({ count: 1, // 默认9 sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 success: res => { const tempFilePaths = res.tempFilePaths const file = tempFilePaths[0] const name = utils.random_filename(file) //上传的图片的别名,建议可以用日期命名 console.log(name) wx.cloud.uploadFile({ cloudPath: name, filePath: file, // 文件路径 }).then(res => { console.log(res) const fileId = res.fileID // 将文件id保存到数据库表中 db.collection('Seller').doc(this.data.seller._id) .update({ data: { logo_url: fileId } }).then(() => { wx.showToast({ title: '上传成功' }) // 渲染本地头像 this.setData({ new_logo: fileId }) }, err => { console.log(err) wx.showToast({ title: '上传失败' }) }) }) } }) } [代码] 微信支付逻辑的实现 作为一个商城,难免会有微信支付相关逻辑的实现。在这种情况下,可以借助云开发提供的微信支付云调用功能实现快速的 API 调用和接口的实现。 绑定商户 在使用云开发提供的微信支付时,需要先执行微信支付的绑定,在云开发控制台添加相应的商户号 [图片] 添加后微信会发来通知 [图片] 根据提示,开通账号即可。 [图片] 如果不绑定,将报“受理关系不存在”的错误 [图片] 函数代码调用 配置完成后,只需要在云函数中调用微信支付的接口,就可以实现相关调用的能力 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { console.log('请求中') console.log(cloud.getWXContext().ENV) let { orderId, amount, body } = event const wxContext = cloud.getWXContext() const res = await cloud.cloudPay.unifiedOrder({ body: body, outTradeNo: orderId, spbillCreateIp: '127.0.0.1', subMchId: '1447716902', totalFee: amount, envId: 'dinner-cloud', functionName: 'pay_cb' }) return res.payment } [代码] 这里 [代码]functionName: 'pay_cb'[代码]指的就是支付成功后,微信支付那侧给我的回调信息,后面我们就用它来更新我们的订单状态 小程序端代码调用 调用云函数后,会获得微信支付所需要的各种参数, [图片] 这个时候,就可以在小程序端调用微信支付接口,进行支付,相关代码可以参考 [代码]const { result: payData } = res wx.requestPayment({ timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, signType: 'MD5', paySign: payData.paySign, success: res => { console.log('支付成功', res) wx.showModal({ title: '支付成功', showCancel: false, success: () => { // 跳转订单详情页 wx.navigateTo({ url: '/order/detail/detail?objectId=' + order._id }) } }) }, ... [代码] 微信支付回调处理 微信统一下单里一个pay_cb回调函数,它是一个云函数,后续微信支付的支付信息将会发送在这个函数中,相应的,我们需要编写处理的方法 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ // API 调用都保持和云函数当前所在环境一致 env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() // 云函数入口函数 exports.main = async (event, context) => { console.log('支付回调') console.log(event) console.log(cloud.getWXContext().ENV) const orderId = event.outTradeNo const resultCode = event.resultCode if (resultCode === 'SUCCESS') { const res = await db .collection('Order') .doc(orderId) .update({ data: { status: 1 } }) console.log(res) return { errcode: 0 } } } [代码] 总结 云开发体验下来,优点自不必多说,微信登录与支付原生支持,调用与调试都很方便,特别是不用启本地服务开发,真的好用; 这个小程序的源码我已经开源了,你可以访问社区官网 获取源码,自行使用~ 作者:黄秀杰,16年开始从事小程序开发与技术布道,同名个人公众号「黄秀杰」。 云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 产品文档:https://cloud.tencent.com/product/tcb 技术文档:https://cloudbase.net 技术交流加Q群:601134960 最新资讯关注微信公众号【腾讯云云开发】
2020-07-29 - 免费ICP备案攻略。不花1分钱拥有一台云服务器并顺利ICP备案。
写在前面: 大家不要将ICP证和ICP备案搞混了。 ICP证指的是【电信增值业务经营许可证】,这个资质需要企业主体至少100万注金,去工信部办理,比较难办理;社交-交友需要ICP证。 而ICP备案,【非经营性互联网信息服务备案核准】仅仅是指企业主体的域名备案,可以简单的按以下步骤免费办理成功,其他社交类目如社区、论坛、笔记等,只需要ICP备案即可。 1、在腾讯云注册一个账号并认证企业主体(不吹不黑,开发小程序当然首选腾讯云,好用)。http://www.qcloud.com/ 如果你是个人主体,就不要往下看了,没必要折腾了。 2、找到腾讯云免费活动页:https://cloud.tencent.com/act/free?from=10107 3、选择一款云服务器,180天免费试用。 云服务器申请成功后,它的使命就完成了,没用了,让它自生自灭吧。 在整个备案过程中,也不需要部署网站(域名都没有备案,哪来的网站?)。 [图片] 云服务器180天到期后,可以自己决定是否续费,每个月也才99元,促销期甚至更低,完全可以接受吧。 备案成功后,该服务器就没什么作用了,让它180天后自然欠费销毁得了。 服务器销毁后会有什么影响?答:没有任何影响。 但是。。。。。 你备案的域名最后还得指向一个网站,因为腾讯云会应工信部的要求定期检查网站是否合规,所以你还是要建一个简单的网站,(备案期间,可以暂时不管网站的事,等将来需要的时候再管理)。 至于有多简单,答,多简单都行。此时你可以在七牛、腾讯云、阿里云租点免费的对象存储空间,做个简单的网站。 4、在进行ICP备案之前,你需要在腾讯云注册你的域名地址,如果你已有域名,但不在腾讯云,建议先将要域名过户到腾讯云的账号上。 5、进入控制台,开始ICP备案,这个流程就不介绍了,因为完全一看就懂。而且现在使用备案小程序后,不需要幕布或现场拍照了,极其方便,大家跟着流程走就一点问题没有,有人脸识别和在线拍一段小视频。另外,大家可以随便作,随便填,填错或者填得不合适也不用怕,会有专门的备案客服打电话告诉你哪哪要改,还会告诉你应该怎么填才更容易通过工信部的审核,客服的态度好得发指。 仅说一点其中的几个小坑: a、人脸识别的时候,白色背景、白色背景、白色背景,笔者在人脸识别的时候,满世界找白墙,结果还被打回来重拍了3次。 b、网站用途一律写:公司官网,好通过工信部审核。 6、腾讯云提交资料到工信部审核。这是一个漫长的让人无语的等待,20-30天。笔者最近两次都是20天才过审;不要幻想会有可能提前完成审核,这是政府部门在审核,提前完成说明某政府人员的工作安排有问题,会犯错误的。 7、备案成功后,会有短信通知你,但是,你需要去工信部网站查询结果,并将结果切屏拷贝下来,因为小程序类目审核需要上传这张图片。http://beian.miit.gov.cn/publish/query/indexFirst.action [图片] 把上面这张图片保存好,小程序类目审核的时候需要上传。收到通知后,如果在这里查不到结果,也别急,据说需要24小时。 8、接下来是小程序上线审核。 因为ICP备案的小程序内容肯定涉及到社交,最后小程序上线时还要提交到工信部审核,还需要7天左右的时间,加上前面ICP备案的时间,加起来怎么也得30-40天。大家估计时间,别影响小程序上线。这7天也是政府部门在审核,不要幻想会提前。 9、计算一下时间: 腾讯云注册账号和认证:1-3天; 域名备案:腾讯云环节:1-3天; 域名备案:工信部环节:20-30天; 小程序添加服务类目:社交类目审核:1-3天; 小程序上线审核:腾讯环节:1-2天; 小程序上线审核:工信部环节:7+天; 总天数:30-40天; 10、节省时间的一些建议: 在开发小程序之前,就开始备案工作,小程序可以同时开发,相互不影响; 在开发完成之前一、两星期之内,先发布一版小程序,别管功能是不是完整,能通过审核就行,这样会有7天的等待类目审核的时间,这个时间里,小程序可以照常开发,不影响进度; 只要是社交类,基本需要有文字和图片安全检查功能,别忘了加上,别到时审核通过不了。 11、结束。 [图片]
2021-01-19 - 小程序图文编辑器页面制作
[图片] wxml [代码]<view class='ceng' hidden='{{!showceng}}' catchtouchmove='true'></view> <!-- editTxt-titt --> <view class='edit-txt' hidden='{{!showedittitt}}' catchtouchmove='true'> <textarea placeholder='请编辑文字' value='{{edittitt}}' bindconfirm="editedtitt" maxlength="300" /> </view> <!-- editTxt-content --> <view class='edit-txt' hidden='{{!showeditcontent}}' catchtouchmove='true'> <textarea placeholder='请编辑文字' value='{{editcontent}}' bindconfirm="editedcontent" maxlength="300" /> </view> <!-- reditTxt-content 标题输入,首编辑输入,重新编辑输入是分别使用三个不同的编辑框实现--> <view class='edit-txt' hidden='{{!showreditcontent}}' catchtouchmove='true'> <textarea placeholder='请编辑文字' value='{{reditcontent}}' bindconfirm="reditedcontent" maxlength="300" /> </view> <view class='k-bai'> <view class='txt titt' data-name='titt' bindtap='towrite'> {{titt}} <image src='/images/delet.png' class='delet-icon' data-name='titt' catchtap='ondelettitt' wx:if='{{titt!="请输入标题文字"}}'></image> </view> </view> <view class='content k-bai' wx:for="{{content}}" wx:key="" data-leixing='{{item.leixing}}' data-index='{{index}}' bindtap='redit'> <view class='txt' wx:if="{{item.leixing=='txt'}}">{{item.neirong}}</view> <image src='{{item.url}}' class='image' mode="widthFix" wx:if="{{item.leixing=='img'}}"></image> <image src='/images/delet.png' class='delet-icon' data-index='{{index}}' catchtap='ondelet' wx:if="{{content[index]!=''}}"></image> </view> <view class='add'> <image src='/images/add_06.png' bindtap='addtxt'></image> <image src='/images/add_03.png' bindtap='addimg'></image> </view> <view class='save-btn' bindtap='onsave'>上传</view> [代码] js [代码]// pages/edit/edit.js Page({ /** * 页面的初始数据 */ data: { titt: '请输入标题文字', showceng: false, showedittitt: false, showeditcontent: false, showreditcontent: false, edittitt: '', editcontent: '', reditcontent: '', target: '', content: [{ leixing: 'txt', neirong: '我爱你' }, { leixing: 'img', url: 'http://wechatpx.oss-cn-beijing.aliyuncs.com/card1_03.png' } ], }, /** * 生命周期函数--监听页面加载 */ towrite: function(e) { var that = this var target = e.currentTarget.dataset.name that.setData({ showceng: true, showedittitt: true, target: target, edittitt: '' }) }, editedtitt: function(e) { var that = this var target = that.target that.setData({ titt: e.detail.value, showceng: false, showedittitt: false, [target]: e.detail.value }) }, ondelettitt: function(e) { var that = this wx.showModal({ title: '重置标题', content: '您确定要重置标题吗?', success(res) { if (res.confirm) { that.setData({ titt: '请输入标题文字' }) } else if (res.cancel) { } }, confirmColor: '#5677fc' }) }, ondelet: function(e) { var that = this var index = e.currentTarget.dataset.index var content = that.data.content wx.showModal({ title: '删除提示', content: '您确定要删除这段编辑吗?', success(res) { if (res.confirm) { content.splice(index, 1) that.setData({ content: content }) } else if (res.cancel) {} }, confirmColor: '#5677fc' }) }, addtxt: function() { var that = this var content = that.data.content that.setData({ editcontent:'', showceng: true, showeditcontent: true }) }, editedcontent: function(e) { var that = this var input = new Object input.leixing = 'txt' input.neirong = e.detail.value var content = that.data.content content.push(input) that.setData({ content:content, showceng: false, showeditcontent: false }) }, redit:function(e){ var that = this var index = e.currentTarget.dataset.index var leixing = e.currentTarget.dataset.leixing var target = that.data.target if(leixing=='txt'){ target = "content["+index+"].neirong" that.setData({ reditcontent:'', showceng: true, showreditcontent: true, target: target }) }else if(leixing=='img'){ wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { const tempFilePaths = res.tempFilePaths target = "content[" + index + "].url" that.setData({ [target]: tempFilePaths }) } }) } }, reditedcontent: function (e) { var that = this var target = that.data.target that.setData({ [target]: e.detail.value, showceng: false, showreditcontent: false, }) }, addimg:function(){ var that = this wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { const tempFilePaths = res.tempFilePaths var input = new Object input.leixing = 'img' input.url = tempFilePaths var content = that.data.content content.push(input) that.setData({ content: content, }) } }) }, onsave:function(){ wx.showToast({ title: '上传成功!', }) }, onLoad: function(options) { }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady: function() { }, /** * 生命周期函数--监听页面显示 */ onShow: function() { }, /** * 生命周期函数--监听页面隐藏 */ onHide: function() { }, /** * 生命周期函数--监听页面卸载 */ onUnload: function() { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function() { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function() { }, /** * 用户点击右上角分享 */ onShareAppMessage: function() { } }) [代码] wxss [代码]/* pages/edit/edit.wxss */ page { background-color: #f1f1f1; font-size: 28rpx; padding-bottom: 150rpx; } .k-bai { background-color: #fff; padding: 30rpx; color: #666; } .txt { padding: 30rpx; border: 1rpx solid #eee; margin-top: 20rpx; position: relative; } .delet-icon { display: block; width: 50rpx; height: 50rpx; border-radius: 50rpx; position: absolute; right: -20rpx; top: -20rpx; z-index: 10; } .ceng { width: 750rpx; height: 1334rpx; position: fixed; left: 0; top: 0; z-index: 50; background-color: rgba(0, 0, 0, 0.3); } .edit-txt { width: 660rpx; background-color: #fff; padding: 30rpx; margin: 0 auto; position: fixed; left: 50%; margin-left: -360rpx; top: 220rpx; z-index: 60; } .edit-txt .save-btn { display: block; width: 450rpx; height: 80rpx; border-radius: 80rpx; background-color: #5677fc; color: #fff; text-align: center; line-height: 80rpx; position: absolute; left: 50%; margin-left: -225rpx; bottom: -40rpx; box-shadow: 0 0 10rpx rgba(0, 0, 0, 0.2); } .edit-txt textarea { width: 660rpx; height: 500rpx; } .add { position: relative; padding: 20rpx 0; width: 200rpx; margin: 0 auto; height: 70rpx; background-color: #fff; border-radius: 0 0 20rpx 20rpx; box-shadow: 0 7rpx 5rpx rgba(0, 0, 0, 0.1); } .add image { display: inline-block; width: 70rpx; height: 70rpx; } .add image:first-child { margin-right: 20rpx; margin-left: 20rpx; } .content { margin-top: 30rpx; position: relative; } .content .delet-icon { right: 12rpx; top: 25rpx; } .content .image { display: block; padding: 30rpx; border: 1rpx solid #eee; margin-top: 20rpx; position: relative; width: 630rpx; } .save-btn { width: 690rpx; height: 90rpx; text-align: center; line-height: 90rpx; background-color: #5677fc; color: #fff; font-size: 34rpx; border-radius: 90rpx; position: fixed; left: 50%; margin-left: -345rpx; bottom: 30rpx; z-index: 100; } [代码]
2019-07-29 - 富文本编辑器
这样的富文本编辑器,可好? 开发语言: mpvue。 形式:自定义组件。一行代码就可以在任意地方引用。 [图片] 微信提供的富文本编辑器只有接口,要做一个富文本编辑器需要自己做界面,然后调用相应接口。花一天时间封装了下。 如果大家觉得还行,到时候整理好了,放 github 上。 不足: 颜色选择器目前只能支持预设的一些颜色,没有色盘可选取任意颜色值。
2019-10-14 - 小程序跳转页面加载优化
适应场景: 小程序页面跳转redirect/navigate/其它方式 分析: 从用户触发跳转行为到下一个页面onload生命周期函数内时间差会有500ms左右,如果在页面跳转之后进行onload函数内才开始去加载页面数据,那么这500ms左右的时间就浪费了。 改进: 在页面触发跳转行为的处理函数里结合promise预先加载下个页面的数据,并将promise对象缓存,此时页面跳转和加载数据同时进行,到了目标页面再取出缓存的promise对象进行判断和取数据操作。 效果: 跳转页面加载速度提高了600ms。 示例: 代码结构 [图片] pageManager.js [代码]// 写在utils里的公用方法 const pageList = {}; module.exports = { putData:function(pageName, data){ pageList[pageName] = data; }, getData:function(pageName){ return pageList[pageName]; } } [代码] util.js [代码]const myPromise = fn => obj => { return new Promise((resolve, reject) => { obj.complete = obj.success = (res) => { resolve(res); } obj.fail = (err) => { reject(err); } fn(obj); }) } module.exports = { myPromise : myPromise } [代码] index.js [代码]// 跳转页面 const {myPromise} = require('../../utils/util'); const pageManager = require('../../utils/pageManager'); page({ data: { }, onLoad:function(){ }, gotoPageA:function(){ const PromisePageA = myPromise(wx.request)({ url : '' }).then((res)=>{ return res.data; }) pageManager.putData('pageA',promisePageA); wx.navigateTo({ url: 'pages/pageA/pageA' }) } }) [代码] pageA.js [代码]// 被跳转页面 const util = require('../../utils/util.js'); const pageManager = require('../../utils/pageManager'); const {myPromise} = require('../../utils/util'); Page({ data:{ logs:[] }, onLoad: function(){ const promisePageA = pageManager.getData('pageA'); if(promisePageA){ const resData = promisePageA.then( function(data){ }, function(){ console.log("err"); } ) } } }) [代码]
2019-10-31