- 轻松实现小程序直接上传图片至腾讯云对象存储
概念介绍 对象存储(Cloud Object Storage,COS)是腾讯云提供的一种存储海量文件的分布式存储服务,用户可通过网络随时存储和查看数据。腾讯云 COS 使所有用户都能使用具备高扩展性、低成本、可靠和安全的数据存储服务。 前期准备 登录腾讯云对象存储控制台创建存储桶,获取 Bucket(存储桶名称) 和 Region(地域名称)。 通过管理控制台的 密钥管理 获取您的项目 SecretId 和 SecretKey 下载对象存储SDK,至小程序对象存储gitHub项目目录下载https://github.com/tencentyun/cos-wx-sdk-v5/tree/master/demo/lib下的cos-auth.js文件,添加至项目目录 项目实践 一、引用 [代码]var CosAuth = require('cos-auth');//引入对象存储SDK var config = require('../utils/api.js');//定义了项目的一些配置内容,例如存储桶名称、地区、请求域名等 var stsCache; //存储临时秘钥及秘钥过期时间内容 //定义上传接口 var uploadFile = function(filePath, cb) { // 请求用到的参数 var prefix = 'https://' + config.Bucket + '.cos.' + config.Region + '.myqcloud.com/'; // 对更多字符编码的 url encode 格式 var camSafeUrlEncode = function(str) { return encodeURIComponent(str) .replace(/!/g, '%21') .replace(/'/g, '%27') .replace(/\(/g, '%28') .replace(/\)/g, '%29') .replace(/\*/g, '%2A'); }; // 获取临时密钥 // 全局变量stsCache 存储临时秘钥及过期时间内容 var getCredentials = function(callback) { //判断临时秘钥未过期 if (stsCache && Date.now() / 1000 + 30 < stsCache.expiredTime) { callback(stsCache && stsCache.credentials); return; } //过期,服务器重新请求获取临时秘钥 wx.request({ method: 'GET', url: 'https://baidu.com/Api/Cos/getCosTempKeys', // 服务端签名,参考 server 目录下的两个签名例子 dataType: 'json', success: function(result) { var data = result.data.result; var credentials = data.credentials; if (credentials) { stsCache = data } else { wx.showModal({ title: '临时密钥获取失败', content: JSON.stringify(data), showCancel: false }); } callback(stsCache && stsCache.credentials); }, error: function(err) { wx.showModal({ title: '临时密钥获取失败', content: JSON.stringify(err), showCancel: false }); } }); }; // 计算签名 var getAuthorization = function(options, callback) { getCredentials(function(credentials) { callback({ XCosSecurityToken: credentials.sessionToken, Authorization: CosAuth({ SecretId: credentials.tmpSecretId, SecretKey: credentials.tmpSecretKey, Method: options.Method, Pathname: options.Pathname, }) }); }); }; // 上传文件 var uploadFile = function(filePath, cb) { var Key = filePath.substr(filePath.lastIndexOf('/') + 1); // 这里指定上传的文件名 getAuthorization({ Method: 'POST', Pathname: '/' }, function(AuthData) { var requestTask = wx.uploadFile({ url: prefix, name: 'file', filePath: filePath, formData: { 'key': Key, 'success_action_status': 200, 'Signature': AuthData.Authorization, 'x-cos-security-token': AuthData.XCosSecurityToken, 'Content-Type': '', }, success: function(res) { var url = prefix + camSafeUrlEncode(Key).replace(/%2F/g, '/'); if (res.statusCode === 200) { if (cb) { cb(url); } } else { wx.showModal({ title: '上传失败', content: JSON.stringify(res), showCancel: false }); } }, fail: function(res) { wx.showModal({ title: '上传失败', content: JSON.stringify(res), showCancel: false }); } }); requestTask.onProgressUpdate(function(res) { }); }); }; // 触发上传文件方法,按步骤调用执行 uploadFile(filePath, cb); }; module.exports = { uploadFile }; [代码] 2、调用以上SDK实现上传 1、 在需调用的文件,首先引入以上文件 var demoNoSdk = require(‘sdk文件路径’);//引入上述的对象存储SDK文件 2、 获取到上传文件的临时路径(备注:可以是直接调用wx.chooseImag方法获取的临时文件,也可以是调用wx.canvasToTempFilePath将画布导出的临时图片路径等等其他方式获取的到的临时图片文件路径) 这里调用wx. chooseImage为例 [代码]wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: function(res) { //临时文件路径 var tempFilePath= res.tempFilePaths[0]; //定义上传图片成功后的回调函数 var callback = function(url) { //url为上传至对象存储成功后返回的云存储文件路径 //do other something …… }; //调用对象存储SDK,实现文件上传 //传递参数临时文件路径及上传成功后的回调函数,若不需要回调可不传此参数 demoNoSdk.uploadFile(tempFilePath, callback); }, }) [代码] 实现上传的过程如下 获取图片临时文件路径 ->调用SDK的uploadFile方法 ->服务器请求获取请求对象存储功能所需要的临时秘钥(并将秘钥结果及过期时间存储至全局变量,方便后面直接调用已缓存的未过期的临时秘钥即可,无需重复请求服务器获取) ->调用wx.uploadFile接口上传图片 ->上传成功后调用我们自定义的回调函数实现我们自己的业务。 至此,小程序直接上传图片至对象存储完成!
2019-04-27 - 小程序实现列表拖拽排序
小程序列表拖拽排序 [图片] 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 - 小程序实现的列表上下拖拽排序
先来看看效果 快速拖拽排序测试演示视频地址:https://v.qq.com/x/page/r3207k4fxe1.html 完整拖拽排序效果演示视频地址:https://v.qq.com/x/page/y3207g6agur.html [图片] 采用技术:uni-app 接下来分析分析实现该效果所需要用到的标签 元素是通过拖拽进行排序的,此处采用的是官方出的 <movable-area> <movable-view> 两位标签大佬解决移动的问题 (主要是相信官方支持的动画会比自己搞更加丝滑一些)。支持拖拽到上下边界,检查可视区域的位置并自动进行滚动, 此处就需要我们的 <scroll-view> 标签大佬坐镇了。标签的选择搞定了,再来了解了解这些标签要用到的重点属性 movable-view 想要移动就必须作为 movable-area 的直接子元素,且 movable-area 必须设置 width,height 属性 (还有些提示可以查看文档)。movable-view 的 x, y 属性决定了 movable-view 再 movable-area 所处的位置 (是不是猜出了要搞些什么东东了)scroll-view 滚动到指定位置可以通过控制 scroll-top 的属性值来进行控制滚动 接下来就是怎么个实现思路,先来捋捋实现的步骤 列表该如何渲染如何控制拖拽元素的跟随如何使拖拽中的元素与相交互的元素进行位置调换如何判断拖拽元素至上下边界滚动屏幕如何使页面的滚动与拖拽时的滚动互不影响 描述完宏观的蓝图,接下来就是代码小细节,客官请随我来 一、解决列表渲染问题 /** * 上面说到 movable-view 可以通过 x,y 决定它的位置, 且 movable-area 需要设置 widht,height 属性 * 配置完这些属性 movable-view 就可以再 movable-area 愉快的拖拽玩耍了 * 思路: * 1. 通过列表的数量乘于显示列表项的高度得出最终可拖拽区域的总高度,赋值给 movable-area * 2. 扩展列表项一些字段,此处使用 y 保存当前项距离顶部位置, idx 保存当前项所在列表的下标 / // 伪代码 // js initList(list) { this.areaHeight = list.length * this.height; // aeraHieght 可拖拽区域总高度, height 为元素所需高度 this.internalList = list.map((item, idx) => { return { ...item, y: idx * this.height, // movable-view 当前项所处的高度 idx: idx, // 当前项所处于列表的下标,用于比较 animation: true, // 主要用于控制拖拽的元素要关闭动画, 其他的元素可以保留动画 } }) } // html 二、 如何控制拖拽元素的跟随 // 主要是通过监听 movable-view 的 touchstart touchmove touchend 三个事件完成拖拽动作的起始、移动、结束。 // methods { _dragStart(e){ // 取得触摸点距离行顶部距离 this.deviationY = (e.mp.touches[0].clientY - this.wrap.top) % this.height; this.internalList[idx].animation = false; // 关闭当前拖拽元素的动画属性 this.activeIdx = idx; // 保存当前拖拽元素的下标 }, _dragMove(e) { const activeItem = this.internalList[this.activeIdx]; if (!activeItem) return; // 实时取得触摸点的位置信息 const clientY = e.mp.touches[0].clientY; let touchY = clientY - this.wrap.top - this.deviationY + this.scrollTop; if (touchY <= 0 || touchY + this.height >= this.areaHeight) return; activeItem.y = touchY; // 拖拽元素的移动秘密就在于此 } } 三、如何使拖拽中的元素与相交互的元素进行位置调换 // 上述代码解决了当前拖拽元素的位置移动问题, 接下来就需要解决拖拽元素和上下元素交互的问题 // methods { __dragMove(e){ // ...同上代码一致 // 上下元素交互位置代码实现 for(let item of this.internalList) { if (item.idx !== activeItem.idx) { if (item.idx > activeItem.idx) { // 如果当前元素下标大于拖拽元素下标,则检查当前拖拽位置是否大于当前元素中心点 if (touchY > item.idx * this.height - this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; // 对调位置 item.y = item.idx * this.height; // 更新对调后的位置 break; // 退出循环 } } else { // 如果当前元素下标小于拖拽元素下标,则检查当前拖拽位置是否小于当前元素中心点 if (touchY < item.idx * this.height + this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; item.y = item.idx * this.height; break; } } } } } } 四、如何判断拖拽元素至上下边界滚动屏幕 // 将 movable-area 包裹在 scroll-view 标签中, 通过控制 scroll-top 的值来进行滚动 // 思路: 判断当前拖拽元素的位置信息与当前屏幕可视区域进行比较 // methods { _dragMove(e) { // ...同上代码 // 检查当前位置是否处于可视区域 if (activeItem.idx + 1 * this.height + this.height / 2 > this.scrollTop + this.wrap.top) { this.viewTop = this.scrollTop + this.height; // 往上滚动一个元素的高度 } else if (activeItem.idx * this.height - this.height / 2 < this.scrollTop ) { this.viewTop = this.scrollTop - this.height; // 往下滚动一个元素的高度 } } } 五、如何使页面的滚动与拖拽时的滚动互不影响 // 事实上我是通过一种取巧的方式, scroll-veiw 有一个 scroll-y 属性可以控制滚动方向 // 思路: // 1.不进行滚动的时候将 scroll-y 置为 true , 使用默认的滚动效果 // 2.当进入拖拽排序状态时则将 scroll0y 置为 false, 滚动通过拖拽代码比较计算滚动位置 完整代码: 主要小程序上的插槽不允许往外传值、所以自定义元素实现的方式相比于H5实现Vue的方式比较别扭。 因为有多个地方需要用到排序功能,所以边抽离了 js 部分进行混入。 // DargSortMixin.js 文件 export default { props: { list: { type: Array, default() { return []; }, }, sort: { type: Boolean, default: false, }, height: { type: Number, default: 66, }, }, data() { return { areaHeight: 0, // 区域总高度 internalList: [], // 列表 activeIdx: -1, // 移动中激活项 deviationY: 0, // 偏移量 // 包裹容器信息 wrap: { top: 0, height: 0, }, viewTop: 0, // 指定滚动高度 scrollTop: 0, // 容器实时滚动高度 scrollWithAnimation: false, canScroll: true, }; }, created() { // 组件使用选择器,需用使用this const query = this.createSelectorQuery(); query .select('#scroll-wrap') .boundingClientRect(rect => { if (rect) { this.wrap = { top: rect.top, height: rect.height, }; } }) .exec(); }, watch: { list: { handler(val) { this.initList(val); }, immediate: true, }, }, methods: { getList() { return this.internalList .sort((a, b) => { return a.idx - b.idx; }) .map(item => { let newItem = { ...item }; delete newItem.y; delete newItem.idx; delete newItem.animation; return newItem; }); }, initList(list) { this.areaHeight = list.length * this.height; this.internalList = list.map((item, idx) => { return { ...item, y: idx * this.height, idx, animation: true, }; }); }, _dragStart(e, idx) { // 取得触摸点距离行顶部距离 this.deviationY = (e.mp.touches[0].clientY - this.wrap.top) % this.height; this.internalList[idx].animation = false; // 关闭动画 this.activeIdx = idx; this.scrollWithAnimation = true; this.canScroll = false; }, _dragMove(e) { const activeItem = this.internalList[this.activeIdx]; if (!activeItem) return; // 保存触摸点位置和长按时中心一致 const clientY = e.mp.touches[0].clientY; let touchY = clientY - this.wrap.top - this.deviationY + this.scrollTop; if (touchY <= 0 || touchY + this.height >= this.areaHeight) return; activeItem.y = touchY; // 设置位置 // 检查元素和上下交互元素的位置 for (const item of this.internalList) { if (item.idx !== activeItem.idx) { if (item.idx > activeItem.idx) { if (touchY > item.idx * this.height - this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; // 对调位置 item.y = item.idx * this.height; // 更新位置 break; } } else { if (touchY < item.idx * this.height + this.height / 2) { [activeItem.idx, item.idx] = [item.idx, activeItem.idx]; // 对调位置 item.y = item.idx * this.height; // 更新位置 break; } } } } // 检查当前位置是否处于可视区域 if ( (activeItem.idx + 1) * this.height + this.height / 2 > this.scrollTop + this.wrap.height ) { this.canScroll = true; activeItem.y = activeItem.idx * this.height; this.$nextTick(() => { this.viewTop = this.scrollTop + this.height; }); } else if (activeItem.idx * this.height - this.height / 2 < this.scrollTop) { this.canScroll = true; activeItem.y = activeItem.idx * this.height; this.$nextTick(() => { this.viewTop = this.scrollTop - this.height; }); } }, _dragEnd(e) { const activeItem = this.internalList[this.activeIdx]; if (!activeItem) return; activeItem.animation = true; activeItem.disabled = true; activeItem.y = activeItem.idx * this.height; this.activeIdx = -1; this.scrollWithAnimation = false; this.canScroll = true; }, _onScroll(e) { this.scrollTop = e.detail.scrollTop; }, }, }; // TheDragSortAreaList.vue 文件 import DragSortMixin from '@/mixins/DragSortMixin'; export default { name: 'TheDragSortTableList', mixins: [DragSortMixin], }; .active-item { z-index: 10; } .drag-item { background: $theme-color; color: $white !important; .count { color: $white !important; } }
2020-11-27 - Swiper、video等组件自适应高度的办法。
用过swiper和video等组件的人都知道,这几个组件的高度是无法自适应屏幕高度的,也退style="height:100%"是无用的。 以下是解决组件自适应高度的办法,代码如下: <view style="height: 100%;display: flex;flex-direction: column;"> <swiper style="flex:1"> </swiper> </view> 这样,这个swiper就能自适应高度了。
2021-03-29 - 如何监听小程序中的手势事件(缩放、双击、长按、滑动、拖拽)
mina-touch [图片] [代码]mina-touch[代码],一个方便、轻量的 小程序 手势事件监听库 事件库部分逻辑参考[代码]alloyFinger[代码],在此做出声明和感谢 change log: 2019.03.10 优化监听和绘制逻辑,动画不卡顿 2019.03.12 修复第二次之后缩放闪烁的 bug,pinch 添加 singleZoom 参数 2020.12.13 更名 mina-touch 2020.12.27 上传 npm 库;优化使用方式;优化 README 支持的事件 支持 pinch 缩放 支持 rotate 旋转 支持 pressMove 拖拽 支持 doubleTap 双击 支持 swipe 滑动 支持 longTap 长按 支持 tap 按 支持 singleTap 单击 扫码体验 [图片] demo 展示 demo1:监听 pressMove 拖拽 手势 查看 demo 代码 [图片] [图片] demo2: 监听 pinch 缩放 和 rotate 旋转 手势 (已优化动画卡顿 bug) 查看 demo 代码 [图片] [图片] demo3: 测试监听双击事件 查看 demo 代码 [图片] [图片] demo4: 测试监听长按事件 查看 demo 代码 [图片] [图片] demo 代码 demo 代码地址 mina-tools-client/mina-touch 使用方法 大致可以分为 4 步: npm 安装 mina-touch,开发工具构建 npm 引入 mina-touch onload 实例化 mina-touch wxml 绑定实例 命令行 [代码]npm install mina-touch[代码] 安装完成后,开发工具构建 npm *.js [代码]import MinaTouch from 'mina-touch'; // 1. 引入mina-touch Page({ onLoad: function (options) { // 2. onload实例化mina-touch //会创建this.touch1指向实例对象 new MinaTouch(this, 'touch1', { // 监听事件的回调:multipointStart,doubleTap,longTap,pinch,pressMove,swipe等等 // 具体使用和参数请查看github-README(底部有github地址 }); }, }); [代码] NOTE: 多类型事件监听触发 setData 时,建议把数据合并,在 touchMove 中一起进行 setData ,以减少短时内多次 setData 引起的动画延迟和卡顿(参考 demo2) *.wxml 在 view 上绑定事件并对应: [代码]<view catchtouchstart="touch1.start" catchtouchmove="touch1.move" catchtouchend="touch1.end" catchtouchcancel="touch1.cancel" > </view> <!-- touchstart -> 实例对象名.start touchmove -> 实例对象名.move touchend -> 实例对象名.end touchcancel -> 实例对象名.cancel --> [代码] NOTE: 如果不影响业务,建议使用 catch 捕获事件,否则易造成监听动画卡顿(参考 demo2) 以上简单几步即可使用 mina-touch 手势库 😊😊😊 具体使用和参数请查看Github https://github.com/Yrobot/mina-touch 如果喜欢mina-touch的话,记得在github点个start哦!🌟🌟🌟
2021-06-24 - 云开发基础NodeJS
云函数的运行环境是 Node.js,我们可以在云函数中使用 Nodejs 内置模块以及使用 npm 安装第三方依赖来帮助我们更快的开发。借助于一些优秀的开源项目,避免了我们重复造轮子,相比于小程序端,能够大大扩展云函数的使用 云函数与 Nodejs由于云函数与 Nodejs 息息相关,需要我们对云函数与 Node 的模块以及 Nodejs 的一些基本知识有一些基本的了解。下面只介绍一些基础的概念,如果你想详细深入了解,建议去翻阅一下 Nodejs 的官方技术文档: 技术文档:Nodejs API 中文技术文档 Nodejs 的内置模块在前面我们已经接触过 Nodejs 的 fs 模块、path 模块,这些我们称之为 Nodejs 的内置模块,内置模块不需要我们使用 npm install 下载,就可以直接使用 require 引入: const fs = require('fs') const path = require('path') Nodejs 的常用内置模块以及功能如下所示,这些模块都是可以在云函数里直接使用的: fs 模块:文件目录的创建、删除、查询以及文件的读取和写入,下面的 createReadStream 方法类似于读取文件,path 模块:提供了一些用于处理文件路径的 APIurl 模块:用于处理与解析 URLhttp 模块:用于创建一个能够处理和响应 http 响应的服务querystring 模块:解析查询字符串until 模块 :提供用于解析和格式化 URL 查询字符串的实用工具;net 模块:用于创建基于流的 TCP 或 IPC 的服务器crypto 模块:提供加密功能,包括对 OpenSSL 的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装在云函数中使用 HTTP 请求访问第三方服务可以不受域名限制,即不需要像小程序端一样,要将域名添加到 request 合法域名里;也不受 http 和 https 的限制,没有域名只有 IP 都是可以的,所以云函数可以应用的场景非常多,即能方便的调用第三方服务,也能够充当一个功能复杂的完整应用的后端。不过需要注意的是,云函数是部署在云端,有些局域网等终端通信的业务只能在小程序里进行。 常用变量module、exports、require require 用于引入模块、 JSON、或本地文件。 可以从 node_modules 引入模块,可以使用相对路径(例如 ./、)引入本地模块或 JSON 文件,路径会根据 __dirname 定义的目录名或当前工作目录进行处理。 node 模块化遵循的是 commonjs 规范,CommonJs 定义的模块分为: 模块标识(module)、模块导出(exports) 、模块引用(require)。 在 node 中,一个文件即一个模块,使用 exports 和 require 来进行处理。 exports 表示该模块运行时生成的导出对象。如果按确切的文件名没有找到模块,则 Node.js 会尝试带上 .js、 .json 或 .node 拓展名再加载。 .js 文件会被解析为 JavaScript 文本文件, .json 文件会被解析为 JSON 文本文件。 .node 文件会被解析为通过 process.dlopen() 加载的编译后的插件模块。以 '/' 为前缀的模块是文件的绝对路径。 例如, require('/home/marco/foo.js') 会加载 /home/marco/foo.js 文件。以 './' 为前缀的模块是相对于调用 require() 的文件的。 也就是说, circle.js 必须和 foo.js 在同一目录下以便于 require('./circle') 找到它。 module.exports 用于指定一个模块所导出的内容,即可以通过 require() 访问的内容。 // 引入本地模块: const myLocalModule = require('./path/myLocalModule'); // 引入 JSON 文件: const jsonData = require('./path/filename.json'); // 引入 node_modules 模块或 Node.js 内置模块: const crypto = require('crypto'); wx-server-sdk 的模块tcb-admin-node、protobuf、jstslib 第三方模块Nodejs 有 npm 官网地址 Nodejs 库推荐:awesome Nodejs 当没有以 '/'、 './' 或 '../' 开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules 目录,比如 wx-server-sdk 就加载自 node_modules 文件夹: const cloud = require('wx-server-sdk') Lodash 实用工具库Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库,通过降低 array、number、objects、string 等数据类型的使用难度从而让 JavaScript 变得更简单。Lodash 的模块化方法非常适用于:遍历 array、object 和 string;对值进行操作和检测;创建符合功能的函数。 技术文档:Lodash 官方文档、Lodash 中文文档 使用开发者工具新建一个云函数,比如 lodash,然后在 package.json 增加 lodash 最新版 latest 的依赖: "dependencies": { "lodash": "latest" } 在 index.js 里的代码修改为如下,这里使用到了 lodash 的 chunk 方法来分割数组: const cloud = require('wx-server-sdk') var _ = require('lodash'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { //将数组拆分为长度为2的数组 const arr= _.chunk(['a', 'b', 'c', 'd'], 2); return arr } 右键 lodash 云函数目录,选择“在终端中打开”,npm install 安装模块之后右键部署并上传所有文件。我们就可以通过多种方式来调用它(前面已详细介绍)即可获得结果。Lodash 作为工具,非常好用且实用,它的源码也非常值得学习,更多相关内容则需要大家去 Github 和官方技术文档里深入了解。 在awesome Nodejs页面我们了解到还有 Ramba、immutable、Mout 等类似工具库,这些都非常推荐。借助于 Github 的 awesome 清单,我们就能一手掌握最酷炫好用的开源项目,避免了自己去收集收藏。 moment 时间处理开发小程序时经常需要格式化时间、处理相对时间、日历时间以及时间的多语言问题,这个时候就可以使用比较流行的 momentjs 了。 技术文档:moment 官方文档、moment 中文文档 使用开发者工具新建一个云函数,比如 moment,然后在 package.json 增加 moment 最新版 latest 的依赖: "dependencies": { "moment": "latest" } 在 index.js 里的代码修改为如下,我们将 moment 区域设置为中国,将时间格式化为 十二月 23 日 2019, 4:13:29 下午的样式以及相对时间多少分钟前: const cloud = require('wx-server-sdk') const moment = require("moment"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { moment.locale('zh-cn'); time1 = moment().format('MMMM Do YYYY, h:mm:ss a'); time2 = moment().startOf('hour').fromNow(); return { time1,time2} } 不过云函数中的时区为 UTC+0,不是 UTC+8,格式化得到的时间和在国内的时间是有 8 个小时的时间差的,我们可以给小时数+8,也可以修改时区。云函数修改时区我们可以使用 timezone 依赖(和 moment 是同一个开源作者)。 技术文档:timezone 技术文档 在 package.json 增加 moment-timezone 最新版 latest 的依赖,然后修改上面相应的代码即可,使用起来非常方便: const moment = require('moment-timezone'); time1 = moment().tz('Asia/Shanghai').format('MMMM Do YYYY, h:mm:ss a'); 获取公网 IP有时我们希望能够获取到服务器的公网 IP,比如用于 IP 地址的白名单,或者想根据 IP 查询到服务器所在的地址,ipify 就是一个免费好用的依赖,通过它我们也可以获取到云函数所在服务器的公网 IP。 技术文档:ipify Github 地址 使用开发者工具新建一个 getip 的云函数,然后输入以下代码,并在 package.json 的”dependencies”里新增 "ipify":"latest" ,即最新版的 ipify 依赖: const cloud = require('wx-server-sdk') const ipify = require('ipify'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { return await ipify({ useIPv6: false }) } 然后右键 getip 云函数根目录,选择在终端中打开,输入 npm install 安装依赖,之后上传并部署所有文件。我们可以在小程序端调用这个云函数,就可以得到云函数服务器的公网 IP,这个 IP 是随机而有限的几个,反复调用 getip,就能够穷举所有云函数所在服务器的 ip 了。 可能你会在使用云函数连接数据库或者用云函数来建微信公众号的后台时需要用到 IP 白名单,我们可以把这些 ip 都添加到白名单里面,这样云函数就可以做很多事情啦。 Buffer 文件流const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { const fileID = 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/cloudbase/1576500614167-520.png' const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent return buffer.toString('base64') } getServerImg(){ wx.cloud.callFunction({ name: 'downloadimg', success: res => { console.log("云函数返回的数据",res.result) this.setData({ img:res.result }) }, fail: err => { console.error('云函数调用失败:', err) } }) } "400px" height="200px" src="data:image/jpeg;base64,{{img}}">image> Buffer String Buffer JSON 图像处理 sharpsharp 是一个高速图像处理库,可以很方便的实现图片编辑操作,如裁剪、格式转换、旋转变换、滤镜添加、图片合成(如添加水印)、图片拼接等,支持 JPEG, PNG, WebP, TIFF, GIF 和 SVG 格式。在云函数端使用 sharp 来处理图片,而云存储则可以作为服务端和小程序端来传递图片的桥梁。 技术文档:sharp 官方技术文档 使用开发者工具新建一个 const cloud = require('wx-server-sdk') const fs = require('fs') const path = require('path') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const sharp = require('sharp'); exports.main = async (event, context) => { //这里换成自己的fileID,也可以在小程序端上传文件之后,把fileID传进来event.fileID const fileID = 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/1572315793628-366.png' //要用云函数处理图片,需要先下载图片,返回的图片类型为Buffer const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent //sharp对图片进行处理之后,保存为output.png,也可以直接保存为Buffer await sharp(buffer).rotate().resize(200).toFile('output.png') // 云函数读取模块目录下的图片,并上传到云存储 const fileStream = await fs.createReadStream(path.join(__dirname, 'output.png')) return await cloud.uploadFile({ cloudPath: 'sharpdemo.jpg', fileContent: fileStream, }) } 也可以让 sharp 不需要先 toFile 转成图片,而是直接转成 Buffer,这样就可以直接作为参数传给 fileContent 上传到云存储,如: const buffer2 = await sharp(buffer).rotate().resize(200).toBuffer(); return await cloud.uploadFile({ cloudPath: 'sharpdemo2.jpg', fileContent: buffer2, }) 连接数据库 MySQL公网连接数据库 MySQL技术文档:Sequelize const sequelize = new Sequelize('database', 'username', 'password', { host: 'localhost', //数据库地址,默认本机 port:'3306', dialect: 'mysql', pool: { //连接池设置 max: 5, //最大连接数 min: 0, //最小连接数 idle: 10000 }, }); 无论是MySQL,还是PostgreSQL、Redis、MongoDB等其他数据库,只要我们在 私有网络连接 MySQL默认情况下,云开发的函数部署在公共网络中,只可以访问公网。如果开发者需要访问腾讯云的 Redis、TencentDB、CVM、Kafka 等资源,需要建立私有网络来确保数据安全及连接安全。 连接数据库 Redisconst cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const Redis = require('ioredis') const redis = new Redis({ port: 6379, host: '10.168.0.15', family: 4, password: 'CloudBase2018', db: 0, }) exports.main = async (event, context) => { const wxContext = cloud.getWXContext() const cacheKey = wxContext.OPENID const cache = await redis.get(cacheKey) if (!cache) { const result = await new Promise((resolve, reject) => { setTimeout(() => resolve(Math.random()), 2000) }) redis.set(cacheKey, result, 'EX', 3600) return result } else { return cache } } 二维码 qrcode技术文档:node-qrcode Github 地址 邮件处理技术文档:Nodemailer Github 地址、Nodemailer 官方文档 使用开发者工具创建一个云函数,比如 nodemail,然后在 package.json 增加 nodemailer 最新版 latest 的依赖: "dependencies": { "nodemailer": "latest" } 发送邮件服务器:smtp.qq.com,使用 SSL,端口号 465 或 587 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { const nodemailer = require("nodemailer"); let transporter = nodemailer.createTransport({ host: "smtp.qq.com", //SMTP服务器地址 port: 465, //端口号,通常为465,587,25,不同的邮件客户端端口号可能不一样 secure: true, //如果端口是465,就为true;如果是587、25,就填false auth: { user: "344169902@qq.com", //你的邮箱账号 pass: "你的QQ邮箱授权码" //邮箱密码,QQ的需要是独立授权码 } }); let message = { from: '来自李东bbsky <344169902@qq.com>', //你的发件邮箱 to: '你要发送给谁', //你要发给谁 // cc:'', 支持cc 抄送 // bcc: '', 支持bcc 密送 subject: '欢迎大家参与云开发技术训练营活动', //支持text纯文字,html代码 text: '欢迎大家', html: '你好:' + '欢迎欢迎', attachments: [ //支持多种附件形式,可以是String, Buffer或Stream { filename: 'image.png', content: Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD/' + '//+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4U' + 'g9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC', 'base64' ), }, ] }; let res = await transporter.sendMail(message); return res; } Excel 文档处理Excel 是存储数据比较常见的格式,那如何让云函数拥有读写 Excel 文件的能力呢?我们可以在 Github 上搜索关键词“Node Excel”,去筛选 Star 比较多,条件比较契合的。 Github 地址:node-xlsx 使用开发者工具新建一个云函数,在 package.json 里添加 latest 最新版的 node-xlsx: "dependencies": { "wx-server-sdk": "latest", "node-xlsx": "latest" } 读取云存储的 Excel 文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const xlsx = require('node-xlsx'); const db = cloud.database() exports.main = async (event, context) => { const fileID = 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/china.csv' const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent const tasks = [] var sheets = xlsx.parse(buffer); sheets.forEach(function (sheet) { for (var rowId in sheet['data']) { console.log(rowId); var row = sheet['data'][rowId]; if (rowId > 0 && row) { const promise = db.collection('chinaexcel') .add({ data: { city: row[0], province: row[1], city_area: row[2], builtup_area: row[3], reg_pop: row[4], resident_pop: row[5], gdp: row[6] } }) tasks.push(promise) } } }); let result = await Promise.all(tasks).then(res => { return res }).catch(function (err) { return err }) return result } 将数据库里的数据保存为 CSV 技术文档:json2CSV HTTP 处理got、superagent、request、axios、request-promise 尽管云函数的 Nodejs 版本比较低(目前为 8.9),但绝大多数模块我们都可以使用 Nodejs 12 或 13 的环境来测试,不过有时候也要留意有些模块不支持 8.9,比如 got 10.0.1 以上的版本。node 中,http 模块也可作为客户端使用(发送请求),第三方模块 request 对其使用方法进行了封装,操作更方便!所以来介绍一下 request 模块 get 请求const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const rp = require('request-promise') exports.main = async (event, context) => { const options = { url: 'https://news-at.zhihu.com/api/4/news/latest', json: true, method: 'GET', }; return await rp(options) } post 请求结合文件流request('https://www.jmjc.tech/public/home/img/flower.png').pipe(fs.createWriteStream('./flower.png')) // 下载文件到本地 加解密 Cryptocrypto 模块是 nodejs 的核心模块之一,它提供了安全相关的功能,包含对 OpenSSL 的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装。由于 crypto 模块是内置模块,我们引入它是无需下载,就可以直接引入。 使用开发者工具新建一个云函数,比如 crypto,在 index.js 里输入以下代码,我们来了解一下 crypto 支持哪些加密算法,并以 MD5 加密为例: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const crypto = require('crypto'); exports.main = async (event, context) => { const hashes = crypto.getHashes(); //获取crypto支持的加密算法种类列表 //md5 加密 CloudBase2020 返回十六进制 var md5 = crypto.createHash('md5'); var message = 'CloudBase2020'; var digest = md5.update(message, 'utf8').digest('hex'); return { "crypto支持的加密算法种类":hashes, "md5加密返回的十六进制":digest }; } 将云函数部署之后调用从返回的结果我们可以了解到,云函数 crypto 模块支持 46 种加密算法。 发短信“qcloudsms_js”: “^0.1.1” const cloud = require('wx-server-sdk') const QcloudSms = require("qcloudsms_js") const appid = 1400284950 // 替换成您申请的云短信 AppID 以及 AppKey const appkey = "a33b602345f5bb866f040303ac6f98ca" const templateId = 472078 // 替换成您所申请模板 ID const smsSign = "统计小助理" // 替换成您所申请的签名 cloud.init() // 云函数入口函数 exports.main = async (event, context) => new Promise((resolve, reject) => { /*单发短信示例为完整示例,更多功能请直接替换以下代码*/ var qcloudsms = QcloudSms(appid, appkey); var ssender = qcloudsms.SmsSingleSender(); var params = ["1234", "15"]; // 获取发送短信的手机号码 var mobile = event.mobile // 获取手机号国家/地区码 var nationcode = event.nationcode ssender.sendWithParam(nationcode, mobile, templateId, params, smsSign, "", "", (err, res, resData) => { /*设置请求回调处理, 这里只是演示,您需要自定义相应处理逻辑*/ if (err) { console.log("err: ", err); reject({ err }) } else { resolve({ res: res.req, resData }) } } ); }) 使用开发者工具 wx.cloud.callFunction({ name: 'sendphone', data: { // mobile: '13217922526', mobile: '18565678773', nationcode: '86' }, success: res => { console.log('[云函数] [sendsms] 调用成功') console.log(res) }, fail: err => { console.error('[云函数] [sendsms] 调用失败', err) } })
2021-09-10 - 云数据库中的_id 怎样设置为 int?
云数据库中的_id 怎样设置为 int? 很多功能都是基于ID进行分页查询的,是否有这个功能?
2020-06-21 - 云开发中下载存储内容时能不能增加图片裁剪功能?
现在云开发存储功能,上传图片的时候很麻烦,为了方便用户高效访问,每次都要传不同尺寸/大小的相同图片到云存储,能不能像对象存储访问图片那样在url添加参数,返回裁剪后的图片?
2021-07-19 - 云开发之图片压缩裁剪(CloudBase图像处理扩展实战)
1、大约半年前在论坛里寻求云开发后端图片处理方案无果,无奈退而求其次使用小程序端canvas做图片处理: https://developers.weixin.qq.com/community/develop/doc/000c00a3d74758caca2a2b3ef5b400 (寻求方案发帖) 2、canvas做图片处理,代码量比较大,对手机性能要求比较高,而且如果一次处理图片多,还会偶现各种奇怪的不稳定问题。 3、最近iPhone微信更新到7.0.20更是直接不能使用了: https://developers.weixin.qq.com/community/develop/doc/000cc4b48a4378003b7b2f97d51400 (bug反馈发帖) 4、更换图片处理的方案刻不容缓,上次云开发峰会上陈宇明大佬分享案例中提到一嘴CloudBase的相关支持,于是翻到了相关文章,一步步跟着操作,在此感谢大佬指路: https://developers.weixin.qq.com/community/develop/article/doc/0004ec150708d0b57d5bd532a53413 (大佬文章) https://cloud.tencent.com/document/product/876/42103 (开发指南) 5、本人电商项目中有多处图片处理需求,比较典型的一个业务是上传商品主图,当用户任意上传一个图片后,自动居中裁剪生成一大一小两张正方形的图,大的用在详情页,小的用在列表页。CloudBase支持两种方式:获取图片时处理、持久化图像处理。本人业务采用后者。 6、代码示例: 小程序端选择图片,上传到云存储 wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: res => { const tempFilePaths = res.tempFilePaths const tempFile = tempFilePaths[0] let pictureLarge = tempFile let fileName = pictureLarge.split('.') let format = fileName[fileName.length -1] let cloudPath = 'products/sellerId/original-' + (new Date()).valueOf() + (format.length < 5 ? '.' + format : '') wx.cloud.uploadFile({ cloudPath: cloudPath, filePath: pictureLarge, success: res => { const pictureOrignial = res.fileID wx.cloud.callFunction({ name: 'addProduct', data: { operation: 'addPicture', pictureOrignial, cloudPath } }).then(res => { if (res.result.errCode) { wx.showModal({ title: '主图处理失败', content: res.result.errMsg, showCancel: false, confirmColor: '#67ACEB' }) } else { //拿到云文件ID做后续处理 res.result.picture } }).catch(err => { console.error(err) wx.showModal({ title: '主图处理失败', content: '主图处理失败,请重试', showCancel: false, confirmColor: '#67ACEB' }) }) }, fail: err => { console.error(err) wx.showModal({ title: '主图上传失败', content: '主图上传失败,请重试', showCancel: false, confirmColor: '#67ACEB' }) } }) } }) 云函数端处理图片,先放大到最小边大于1125px,再分别裁剪出1125px和258px的两张图,存到同一目录下,返回云文件ID 先安装包: npm install --save @cloudbase/extension-ci@latest 云函数: // 云函数入口文件 const cloud = require('wx-server-sdk') const extCi = require("@cloudbase/extension-ci") const tcb = require("tcb-admin-node") cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) tcb.init({ env: cloud.DYNAMIC_CURRENT_ENV }) tcb.registerExtension(extCi) // 云函数入口函数 exports.main = async (event) => { const wxContext = cloud.getWXContext() if (event.operation == 'addPicture') { return await addPicture(event.pictureOrignial, event.cloudPath) } else { } } async function addPicture(pictureOrignial, cloudPath) { //process picture const res = await process(cloudPath) if (res.errCode !== 0) { return { errCode: 100, errMsg: '商品主图处理失败' } } else { const pictureIDLarge = pictureOrignial.replace(/original/, 'large') const pictureID = pictureOrignial.replace(/original/, 'normal') return { errCode: 0, picture: { pictureIDLarge, pictureID } } } } async function process(cloudPath) { try { const opts = { //scale to 1125 rules: [ { fileid: '/' + cloudPath, // 处理结果的文件路径,如以’/’开头,则存入指定文件夹中,否则,存入原图文件存储的同目录 rule: "imageMogr2/thumbnail/!1125x1125r" // 处理样式参数,与下载时处理图像在url拼接的参数一致 } ] } await tcb.invokeExtension("CloudInfinite", { action: "ImageProcess", cloudPath: cloudPath, // 图像在云存储中的路径,与tcb.uploadFile中一致 operations: opts }) } catch (err) { return JSON.stringify(err, null, 4) } try { const opts = { rules: [ //crop large { fileid: '/' + cloudPath.replace(/original/, 'large'), rule: "imageView2/1/w/1125/h/1125/q/85" }, //crop normal { fileid: '/' + cloudPath.replace(/original/, 'normal'), rule: "imageView2/1/w/258/h/258/q/85" } ] } await tcb.invokeExtension("CloudInfinite", { action: "ImageProcess", cloudPath: cloudPath, // 图像在云存储中的路径,与tcb.uploadFile中一致 operations: opts }) } catch (err) { return JSON.stringify(err, null, 4) } return { "errCode": 0, "errMsg": "ok" } }
2022-04-26 - [开盖即食]小程序Canvas官方新版API实战
[图片] [图片] 最近本人在开发一个新项目的时候,注意到官方在2.9.0开始支持了一个canvas 2D的新API,同时对webGL上支持也有了很大的改进,相信很多人用canvas的组件做一些分享海报,战绩和新闻帖功能。 [图片] 这里是新的引入方式。 官方文档地址: https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html 那么新的canvas2D API有啥好处呢? 原本的API微信有做一定的修改,现在全面支持源生H5 JS的写法,迁移H5的老代码变成更加容易,学习成本更低 修复了一些诡异的BUG,例如原本在IOS早期版本写法顺序会导致clip()图片裁切失效等~ 性能上的优化和提升,复杂动画上帧数明显 举例写法上的一些改变: 1、设置font的写法: [代码]//原本(传值的写法) ctx.setFontSize(20); ctx.fillText('MINA', 100, 100) ctx.draw() //现在(和源生H5写法一致,赋值) ctx.font = "16px"; ctx.fillStyle = 'blue'; //可以直接写颜色,原本的不支持 //不需要 ctx.draw() [代码] 2、获取并添加图片写法: [代码]//原本 //使用的是 wx.getImageInfo的方法 wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { console.log(res); ctx.drawImage(res.path, 0, 0); ctx.draw(true); }, fail: function (res) { //失败回调 } }); //现在 //可以直接img.onload调用 const headerImg = canvas.createImage(); headerImg.src = headImage;//微信请求返回头像 headerImg.onload = () => { ctx.save(); ctx.beginPath()//开始创建一个路径 ctx.arc(38, 288, 18, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.drawImage(headerImg,0,0); ctx.closePath(); ctx.restore(); } [代码] 3、将canvas生成虚拟地址便于下载(重点): [图片] 由于官方文档没有写清楚,误导了挺多人的。这里canvas对象必须通过选择器获取,并获得对应的node节点。 [代码]async saveImg() { let self = this; //这里是重点 新版本的type 2d 获取方法 const query = wx.createSelectorQuery(); const canvasObj = await new Promise((resolve, reject) => { query.select('#posterCanvas') .fields({ node: true, size: true }) .exec(async (res) => { resolve(res[0].node); }) }); console.log(canvasObj); wx.canvasToTempFilePath({ //fileType: 'jpg', //canvasId: 'posterCanvas', //之前的写法 canvas: canvasObj, //现在的写法 success: (res) => { console.log(res); self.setData({ canClose: true }); //保存图片 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '已保存到相册', icon: 'success', duration: 2000 }) // setTimeout(() => { // self.setData({show: false}) // }, 6000); }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") } else { util.showToast("请截屏保存分享"); } }, complete(res) { wx.hideLoading(); console.log(res); } }) }, fail(res) { console.log(res); } }, this) }, [代码] 分享个canvas海报的代码片段: [图片] 片段名: PoCf4emw7TgE 片段link: https://developers.weixin.qq.com/s/PoCf4emw7TgE [图片] [图片] 总结,相对之前还要看官方文档的canvas自定义API,现在写起来更加的方便,老代码迁移起来得心应手,只要你之前会canvas,那么各种效果和动画,拿来就怼,没什么大问题~ 一些奇怪的问题(注意!!!) canvas 2d 目前(2020年4月3日)还不支持真机调试,会报错!!! IDE工具 1.02.2003190 直接保存新版本canvas的API图片是打不开的,但是直接用手机保存在相册是没问题的,请更新到1.02.2003250 最新版即可解决~ 一些老款手机用新的API保存图片会有报错问题,如华为NOTE10,请更新系统到能支持的最新,且微信也是,即可解决~ 部分Android设备诡异的闪退和报错 [图片] 这种有可能是代码写法的问题,比如: [代码]//缺省写法 会导致部分Android机器 闪退 ctx.font = "bold 16px"; ctx.fillStyle = "#000" //在canvas 2D的写法中,所以写法必须规范且完整 ctx.font = "normal bold 12px sans-serif"; ctx.fillStyle = '#707070'; [代码] 所以在canvas 2D 的环境,所以写法必须原始且规范,不能用缺省写法,不然就会有诡异的闪退/报错。 后续:官方在7.0.13的Android版本已修复。 https://developers.weixin.qq.com/community/develop/doc/00088c13e1437890692afd8d85ec00 看完觉得有帮助记得点个赞哦~ 你的赞是我继续分享的最大动力!^-^
2020-05-09 - canvas画布曲线(贝塞尔曲线)优化
上一篇文章小汪写到用canvas实现画板的功能,但是思前想后在曲线上有一定的问题,点我去看上篇canvas画板。由于我把自己代入了角色,但作为用户的感官不应该是这样子的。 二次贝塞尔曲线canvas手册在描述中讲出: [图片] 也就是需要得到起始点,控制点,结束点,从而自动绘制出相切切角的曲线。 而小汪在上篇的案例中实际效果为: [图片] 也就是说 用户在一开始可能并不知道这是贝塞尔曲线,即便给出相应点位也应该把操作点2通过计算改变位置才对。 所以我做出了修改,改成用户拖拽画贝塞尔曲线。 思路:起始点在用户手指/鼠标点击时记录,在拖拽的过程中记录“touchmove”事件的返回位置并存入一个全局数组中。在用户结束触摸是正式绘制。 理论语法为: this.ctx.moveTo(x1, y1); //起始点 this.ctx.quadraticCurveTo(x2, y2, x3, y3); //操作点与结束点 this.ctx.stroke();//封闭路径进行绘制 x1,y1 点位我们在触摸时获得,x3,y3 点位我们在结束触摸时获得,重点讲x2,y2 获得方式; x2,y2 由于我们在拖拽的过程中向全局的一个数组存入鼠标/手指 移动的位置,然后我们取数组的中间那一个为操作点,然后计算出绘制这条曲线的实际操作点为:(这里贴出代码片段,提供思路) setMoveTo() { this.ctx.moveTo(this.sX, this.sY); //起点 this.ctx.lineCap = "round"; //设置线条的结束端点样式 this.ctx.lineJoin = "round"; //设置两条线相交时,所创建的拐角类型 }, /* 手指触摸画布开始 */ drawTouStart(e) { let change = e.changedTouches[0]; this.ctx.beginPath(); //创建一条路径 let type = this.lineType; this.sX = change.x; this.sY = change.y; this.setMoveTo(); }, /* 手指触摸画布移动 */ drawTouMove(e) { let change = e.changedTouches[0]; if(!this.curveArr) this.curveArr = [];// 由于可能未定义 做一个判断 this.curveArr.push(change);// push位置信息 }, /* 手指触摸画布结束 */ drawTouEnd(e) { let data = e.changedTouches[0]; let type = this.lineType; this.lineTo2(data);// 结束时调用绘制方法 this.ctx.closePath(); //当鼠标移抬起时,创建从当前点回到起始点的路径 this.copyCanvas(); //每次结束都复制本次画布结果 }, /* 贝塞尔曲线 */ lineTo2(data) { let arr = this.curveArr;// 重新声明一个变量存储 let conNum = arr.length % 2 ? (arr.length + 1) / 2 : arr.length / 2; conNum = conNum - 1; /* 获取中间那个的数组 */ this.ctx.lineCap = "round"; //设置线条的结束端点样式 this.ctx.moveTo(this.sX, this.sY); //起点 /* 假设 x1,y1 = 起始点; x4,y4 = 用户绘制中间点 ; x3,y3 = 结束点; x2,y2 = 贝塞尔实际操作点 x4 = 2 * x2 - (x1 + x3) / 2 y4 = 2 * y2 - (y1 + y3) / 2 */ let x2 = 2 * arr[conNum].x - (this.sX + data.x) / 2; let y2 = 2 * arr[conNum].y - (this.sY + data.y) / 2; this.ctx.quadraticCurveTo(x2, y2, data.x, data.y); this.ctx.stroke(); this.curveArr = []; //每次结束都清空之前存坐标 }, 实际效果 :PS 录屏工具的问题鼠标和实际位置有偏差 所以点了3个点确认点位 [图片] 这个的话,小汪就想不到有啥办法能弄路径式的展现方法了,欢迎各位来探讨一下。
2021-07-02 - 小程序/web端实现画板功能,项目记录,如果能帮到各位点个 thumb
大家好,这是小汪在社区发表的第一篇文章。主要是实现小程序实现画板功能(web端同理),下面进入正题。(只贴出小程序的代码) [图片] 这次的项目需求是给搞绘图的学生弄一个画图的功能。最终实现效果如下: [图片] 弄了一个代码片段,有兴趣的朋友可以下下来看看。(最下方↓) 功能有:画直线、二次贝塞尔曲线、自由画线、矩形、圆、撤销、恢复等功能。 不过首先声明一下,由于canvas无法对单一路径进行删除,所以无法实现在绘制矩形、圆出现实时路径。(或者说小汪太菜不会,恳请指点), 但不是毫无办法,理论上使用一个元素,实时通过样式实现此效果也是可以的,但由于原生组件的层级问题,小程序并不是那么好实现,研究过,使用cover-view理论可以实现这一效果。但实际测试就是 cover-view无法盖在canvas上,也试过很多方法,如:给cover-view设置出现延迟,放在canvas里边;都不行,但是如果不需要背景图(也就是底图)就可以盖住。所以这个问题大家自己去想解决方案吧,看看官方准备怎么解决(也可能是我自己还不会用的问题,有兴趣可以评论区讨论)。当然,代码片段中也有矩形与圆的操作案例,有兴趣可以了解一下;下面讲讲实现思路: 1.矩形:在手指/鼠标按下时记录按下点位置,在手指移动的过程中不断更新盒子的大小实现此目的。 2.圆:与矩形同理,不过在样式上有一个调整,那就是改变圆心(因为小汪写的代码是以起始点为圆心向外扩张)。 3.贝塞尔曲线的话由于要标点,所以建议给出一个小提示。然后再有cover-view 标识出点位 如:画布的 (20,20) 标记数字1 (40,40)标记数字2.... 4.直线:直线的话处理起来比较麻烦,因为你在移动中绘制的话就一直保留着之前的路径,想用cover-view来弄一个显示效果还得算路径/角度 然后用css渲染,只能有时间在来补全这篇文章了。如果有大佬能搭把手那就更好啦(心中窃喜) 关于代码逻辑思路与备注 小汪在代码片段中已经写的挺详细,可以下下来观看。案例中有绘制路径点击确定保存的图片是无底图的,所以如果不考虑性能的情况下,可以使用2张画布,1张给用户做操作用,可见。一张绘制底图并保留用户在画布1绘制的内容。最后生成是以画布2为主。 [图片] 使用效果图,无路径:不知道文件太大还是咋地,反正没传上去。目前就放在代码片段里吧。 使用效果图,带路径: [图片] 代码片段地址:https://developers.weixin.qq.com/s/QHGXtlmj7prF 非常感谢各位姥爷能看到这里。如果这篇文章对你有所帮助,希望不要吝啬那几秒钟的时间点个赞吧[图片]。没帮助也看到这里,也给整一个? 哈哈 开个玩笑 老爷们随意。但是小汪期望与各位大佬多交流 嘿嘿。下次再见 [图片] 注:转载请注明出处!
2021-06-26 - 小程序云开发生成二维码并保存到文件
小程序云开发 云开发已经出来很久的时间了,但是一直没有使用,原因是一些基本框架都还在原来的服务。这次想参考礼物小盲盒做一个小程序。内容比较简单,刚好适合拿来做云开发练手,就从此开启云开发之路。 云开发整体使用还是比较方便的,这里不作过多的介绍,重点说下今天开发遇到的第一个小小的环节,生成一张二维码分享图可以保存分享到朋友圈。 页面效果 [图片] 实现方法 先来看看官方提供的文档:云开发获取小程序码 接口方法:[代码]openapi.wxacode.getUnlimited[代码] 需在 config.json 中配置 wxacode.getUnlimited API 的权限 属性 类型 默认值 必填 说明 scene string 是 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&’()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式) page string 主页 否 必须是已经发布的小程序存在的页面(否则报错),例如 pages/index/index, 根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面 1. 在 config.json 中配置 wxacode.getUnlimited API 的权限 在你云函数下的config.json文件中,增加以下代码: [代码]{ "permissions": { "openapi": [ "wxacode.getUnlimited" ] } } [代码] 2. scene只支持32个可见字符 如果你的参数过长,则需要将参数进行缩短,可以通过短码,短参数名的方式。我这里只需要一个boxId,刚好32位,这里直接使用boxId。 3. 云端生成二维码并保存 分享图除了二维码,还需要一些其他信息,这些信息是通过本地使用canvas进行绘制,而二维码需要从服务端生成。因为需要请求云函数,获取生成的二维码链接。 由于[代码]wxacode.getUnlimited[代码]返回结果图片buffer,这里使用云文件管理的方法,将获取到的buffer 写入本地文件,然后返回云文件ID给小程序端。 来看代码: [代码]async function getQrCode(scene, page, fileName) { try { var fileName = 'qrcode/' + fileName + '.png'; const result = await cloud.openapi.wxacode.getUnlimited({ scene: scene, page: page }) if (result && result.buffer) { var res = await cloud.uploadFile({ cloudPath: fileName, fileContent: result.buffer, }) if (res.fileID) { return res.fileID } } return false } catch (err) { console.error(err) return false } } [代码] 这里没有对二维码是否已经存在做检查,每次调用都会重新生成。因此在外部调用的地方需要检查是否已经生成,提高性能。 云开发文件存储相关API :https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html 将fileID 直接返回给小程序端,即可展示二维码。 但是这里还需要在小程序端画出分享图 4. 生成分享图 这里使用canvas绘制图片,但是canvas绘制图片需要为本地图片,先通过 [代码]wx.getImageInfo[代码]获取本地临时File地址。 [代码] loading: function (qrcode, avatarUrl, qrBackground) { var _this = this; var a = new Promise(function (r, j) { wx.getImageInfo({ src: qrcode, success: function (t) { r(t); } }); }), b = new Promise(function (r, j) { wx.getImageInfo({ src: avatarUrl, success: function (t) { r(t); } }); }), c = new Promise(function (r, j) { wx.getImageInfo({ src: qrBackground, success: function (n) { r(n); } }); }); Promise.all([a, b, c]).then(function (t) { _this.createNewImg(t[0].path, t[1].path, t[2].path); }); }, createNewImg: function (qrcode, avatarUrl, qrBackground) { var _this = this, config = _this.data.config, canvas = wx.createCanvasContext("myCanvas"); canvas.drawImage(qrcode, 260, 393, 150, 150), canvas.drawImage(qrBackground, 0, 0, 670, 670), canvas.font = "normal bold 28px simhei", canvas.fillStyle = "#000000"; var s = 335 - canvas.measureText("礼物份数:" + _this.data.num + "份").width / 2; canvas.fillText("礼物份数:" + _this.data.num + "份", s, 173), canvas.font = "normal bold 50px simhei", canvas.fillStyle = "#000000"; var qrTxt = config.giftConfig.qrTxt; if (qrTxt.length > 10) { var u = 335 - canvas.measureText(qrTxt.substr(0, 10)).width / 2, r = 335 - canvas.measureText(qrTxt.substr(10, 100)).width / 2; canvas.fillText(qrTxt.substr(0, 10), u, 250), canvas.fillText(qrTxt.substr(10, 100), r, 325); } else { var f = 335 - canvas.measureText(qrTxt).width / 2; canvas.fillText(qrTxt, f, 250); } canvas.arc(335, 468, 35, 0, 2 * Math.PI, !0), canvas.clip(), canvas.drawImage(avatarUrl, 300, 433, 70, 70), canvas.stroke(), canvas.draw(), setTimeout(function () { wx.canvasToTempFilePath({ canvasId: "myCanvas", success: function (n) { var e = n.tempFilePath; wx.hideLoading(), _this.setData({ url: e }); }, fail: function (t) { } }); }, 500); }, [代码] 再通过[代码]wx.canvasToTempFilePath[代码]函数将canvas 保存为本地临时文件,将url设置并展示即可。 wxml代码示例: [代码]<image id="wenan" mode="widthFix" src="{{url}}"></image> <canvas canvasId="myCanvas" style="width:670px;height:670px;margin-top:1000px;position:fixed"></canvas> [代码] 还有保存按钮申请存储权限这里就不说了,属于小程序基本操作。
2021-03-29 - 小程序云函数调用http或https请求外部数据
我们使用小程序云开发的时候,难免会遇到在云函数里做http获取https请求外部数据,然后再通过云函数返回给我们的小程序。今天就来教大家如何在云函数里做http和https请求。 老规矩,先看效果图 [图片] 通过上图,可以看到我们在云函数里成功的访问到了百度的数据。下面就来讲下实现步骤。 一,定义云函数 关于云函数如何创建,这里我就不多说了。不知道如何创建的同学可以去看下我的云开发基础视频:https://study.163.com/course/courseMain.htm?courseId=1209499804 二,使用npm安装request-promise库 使用npm命令行之前,我们需要先安装node.js,node的安装网上搜一下就行。 下面我就来讲下在小程序里使用npm安装类库的步骤。 1, 右键我们的云函数,然后点击在终端中打开 [图片] 2,在打开的终端中输入 npm install request-promise [图片] 3, request-promise安装成功的标示如下 [图片] 三,编写我们的云函数代码 [图片] 把代码给大家贴出来,代码很简单,里面也有相应的注释,我们这里以请求百度的数据为例。 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') //引入request-promise用于做网络请求 var rp = require('request-promise'); cloud.init() // 云函数入口函数 exports.main = async (event, context) => { let url = 'https://www.baidu.com'; return await rp(url) .then(function (res) { return res }) .catch(function (err) { return '失败' }); } [代码] 到这里我就成功的在云函数里实现了http和https请求了,这里使用的是get请求,至于post请求如何使用,自己去百度下“ request-promise post请求”即可。 再来看下我们请求成功的效果图 [图片] 是不是很简单,有任何关于小程序,云开发相关的问题,都可以留言或者私信我,我看到后会及时解答的。
2019-09-23 - 小程序端如何往云数据库插入时间(Date)类型数据?
我现在有个需求是筛选某个时间段的内容,要定义DATE类型字段isodate_time用于检索,但我发现存储后还是字符串类型的,请教大佬有什么方法在添加数据的时候规定字段类型。搜索了相关解答,要设置为db.serverDate(), 可我从前端传来的是data数组, 如何追加这个字段? 云函数处理 exports.main = async(event, context) => { switch (event.action) { case 'AddWlog': { let data = event.data; await db.collection('TABLE_WLOG').add({ data: data }) return { msg: 'ok' } } } } 发布页提交的数据 wx.cloud.callFunction({ name: 'wlogfun', data: { action: 'AddWlog', data: { openid: wx.getStorageSync('openid'), content: that.data.content, .... is_delete: 0, //isodate_time: new Date(Date.now() + 8 * 60 * 60 * 1000), create_time: timeutil.TimeCode(new Date()), update_time: timeutil.TimeCode(new Date()) } }, ... })
2021-03-03 - 小程序云数据库日常操作脚本整理
介绍校友录小程序采用腾讯云开发技术,云开发提供了一个 JSON 数据库,顾名思义,数据库中的每条记录都是一个 JSON 格式的对象。一个数据库可以有多个集合(相当于关系型数据中的表),集合可看做一个 JSON 数组,数组中的每个对象就是一条记录,记录的格式是 JSON 对象。 其主要特性: 安全性:对于数据库而言,数据安全是第一位的;易用性:与小程序的特征类似,“开箱即用,用完即走”,简单上手,免运维;低成本:按量收费,精细化成本控制;高性能:Nosql,支持高并发读写;灵活性:无固定的数据库表模式([代码]no-schema[代码]),支持弹性伸缩;在本案例中,校友录小程序总共用到了16个集合,包含校友用户,校友相册,校友活动,校友互助,校友聚会,校友后台管理员,校友资讯,校友日志等 在云开发控制台有个高级操作,这里可以执行开发者输入的脚本,比如清空集合,根据某个条件删除集合内部分数据,查询集合等等 [图片] 常用脚本1、清空操作 清空校友用户集合 db.collection('t_user') .where({ _id: _.exists(true) }) .remove() 2、删除操作删除1990年入学的校友用户 db.collection('t_user') .where({ USER_ENROLL: 1990 }) .remove() 3、查询操作查询名字为“覃建平”的校友用户的所有数据 db.collection('t_user') .where({ USER_NAME: '覃建平' }) .get() 多条件,指定字段查询 db.collection('t_user') .where({ USER_INVITE_ID: '' }) .field({ USER_INVITE_ID:true }) .skip(0) .limit(10) .get() 4、去掉某个字段删除校友用户集合的USER_VIP_MONEY,USER_VIP_RETURN_MONEY,UER_VIP_LEAVE_MONEY字段 db.collection('t_user').where({_id:_.neq(1)}) .update({ data: { USER_VIP_MONEY:_.remove(), USER_VIP_RETURN_MONEY:_.remove(), USER_VIP_LEAVE_MONEY:_.remove(), } }) 5、更新某个字段或者新增某个字段更新校友用户集合的USER_VIP_MONEY字段,如果原来没有这个字段,则自动新增该字段且赋值 db.collection('t_user').where({_id:_.neq(1)}) .update({ data: { USER_VIP_MONEY: 1111, } }) 6、复杂的多条件查询对于校友用户集合按毕业年份,行业,学校,班级,专业,用户身份,最近来访,访问次数等多维度查询,排序 /** 取得用户分页列表 */ async getUserList(userId, { search, // 搜索条件 sortType, // 搜索菜单 sortVal, // 搜索菜单 orderBy, // 排序 whereEx, //附加查询条件 page, size, oldTotal = 0 }) { orderBy = orderBy || { USER_LOGIN_TIME: 'desc' }; let fields = FILEDS_USER_BASE; let where = {}; where.and = { USER_OPEN_SET: ['>', 0], USER_STATUS: [ ['>=', UserModel.STATUS.COMM], ['<=', UserModel.STATUS.VIP] ], _pid: this.getProjectId() //复杂的查询在此处标注PID }; if (util.isDefined(search) && search) { where.or = [{ USER_NAME: ['like', search] }, { USER_ITEM: ['like', search] }, { USER_COMPANY: ['like', search] }, { USER_TRADE: ['like', search] }, { USER_TRADE_EX: ['like', search] }, ]; } else if (sortType && util.isDefined(sortVal)) { let user = {}; // 搜索菜单 switch (sortType) { case 'companyDef': // 单位性质 where.and.USER_COMPANY_DEF = sortVal; break; case 'trade': // 行业 where.and.USER_TRADE = ['like', sortVal] break; case 'workStatus': //工作状态 where.and.USER_WORK_STATUS = sortVal; break; case 'same_enroll': //同级 user = await UserModel.getOne({ USER_MINI_OPENID: userId }); if (!user) break; where.and.USER_ENROLL = user.USER_ENROLL; break; case 'same_item': //同班 user = await UserModel.getOne({ USER_MINI_OPENID: userId }); if (!user) break; where.and.USER_ITEM = user.USER_ITEM; break; case 'same_trade': //同行 user = await UserModel.getOne({ USER_MINI_OPENID: userId }); if (!user) break; let trade = user.USER_TRADE; if (trade.includes('-')) trade = trade.split('-')[0]; where.and.USER_TRADE = ['like', trade]; break; case 'same_city': //同城 user = await UserModel.getOne({ USER_MINI_OPENID: userId }); if (!user) break; where.and.USER_CITY = user.USER_CITY; break; case 'enroll': //按入学年份分类 switch (sortVal) { case 1940: where.and.USER_ENROLL = ['<', 1950]; break; case 1950: where.and.USER_ENROLL = [ ['>=', 1950], ['<=', 1959] ]; break; case 1960: where.and.USER_ENROLL = [ ['>=', 1960], ['<=', 1969] ]; break; case 1970: where.and.USER_ENROLL = [ ['>=', 1970], ['<=', 1979] ]; break; case 1980: where.and.USER_ENROLL = [ ['>=', 1980], ['<=', 1989] ]; break; case 1990: where.and.USER_ENROLL = [ ['>=', 1990], ['<=', 1999] ]; break; case 2000: where.and.USER_ENROLL = [ ['>=', 2000], ['<=', 2009] ]; break; case 2010: where.and.USER_ENROLL = ['>=', 2010]; break; } break; case 'sort': // 排序 if (sortVal == 'new') { //最新 orderBy = { 'USER_LOGIN_TIME': 'desc' }; } if (sortVal == 'last') { //最近 orderBy = { 'USER_LOGIN_TIME': 'desc', 'USER_ADD_TIME': 'desc' }; } if (sortVal == 'enroll') { //入学 orderBy = { 'USER_ENROLL': 'asc', 'USER_LOGIN_TIME': 'desc' }; } if (sortVal == 'info') { orderBy = { 'USER_INFO_CNT': 'desc', 'USER_LOGIN_TIME': 'desc' }; } if (sortVal == 'album') { orderBy = { 'USER_ALBUM_CNT': 'desc', 'USER_LOGIN_TIME': 'desc' }; } if (sortVal == 'meet') { orderBy = { 'USER_MEET_CNT': 'desc', 'USER_LOGIN_TIME': 'desc' }; } if (sortVal == 'login_cnt') { orderBy = { 'USER_LOGIN_CNT': 'desc', 'USER_LOGIN_TIME': 'desc' }; } break; } } let result = await UserModel.getList(where, fields, orderBy, page, size, true, oldTotal, false); return result; } 查询结果 [图片] 交流vx: cclinux0730
2021-07-12 - 最佳实践丨云数据库实现联表+聚合查询
聚合是云开发 CloudBase 数据库中非常重要的一种数据批处理操作方式。聚合操作可以将数据分组(或者不分组,即只有一组/每个记录都是一组),然后对每组数据执行多种批处理操作,最后返回结果。 有了聚合能力,可以方便的解决很多没有聚合能力时无法实现或只能低效实现的场景,包括分组查询、只取某些字段的统计值或变换值返回、流水线式分阶段批处理、获取唯一值(去重)等。 本文就以一个简单的实例解释如何在云数据库中,实现十分常用的联表+聚合查询操作。 场景说明假设数据库内存在两个集合:[代码]class[代码] 与 [代码]student[代码],存在以下数据: class(班级信息): [图片] student(学生信息): [图片] 现在需要查询徐老师所带的班级里面所有学生的平均成绩。 代码示例1、lookup 联表查询首先我们需要把 student 内的所有数据,按照 class_id 进行分组,这里我们使用云数据库的 lookup 操作符: lookup({ from: "student", //要关联的表student localField: "id", //class表中的关联字段 foreignField: "class_id", //student表中关联字段 as: "stu" //定义输出数组的别名 }).end(); 这个语句会查出来下面的结果,会查出班级的信息以及该班级所对应的所有学生的信息: {"list": [{ "id":1, "teacher":"王老师", "cname":"一班", "stu":[ { "sname":"宁一", "class_id":1, "score":90 } ] }, { "id":2, "teacher":"徐老师", "cname":"二班", "stu":[ { "class_id":2, "sname":"张二", "score":100 }, { "class_id":2, "sname":"李二", "score":80 } ] }] } 但是我们只需要徐老师所在班级学生的数据,所以需要进一步过滤。 2、match 条件匹配现在就只是返回徐老师所在班级的学生数据了,学生数据在 stu 对应的数组里面: .lookup({ from: 'student', localField: 'id', foreignField: 'class_id', as: 'stu' }) .match({ teacher:"徐老师" }) .end() 现在就只是返回徐老师所在班级的学生数据了,学生数据在 stu 对应的数组里面: { "list": [ { "_id": "5e847ab25eb9428600a512352fa6c7c4", "id": 2, "teacher": "徐老师", "cname": "二班", //学生数据 "stu": [ { "_id": "37e26adb5eb945a70084351e57f6d717", "class_id": 2, "sname": "张二", "score": 100 }, { "_id": "5e847ab25eb945cf00a5884204297ed8", "class_id": 2, "sname": "李二", "score": 80 } ] } ] } 接下来我们继续优化代码,直接返回学生的平均分数。 3、直接返回学生成绩平均值如果想要在被连接的表格中(本课程中的 student)做聚合操作,就用 pipeline 方法: .lookup({ from: 'student', pipeline: $.pipeline() .group({ _id: null, score: $.avg('$score') }) .done(), as: 'stu' }) .match({ teacher:"徐老师" }) .end() 现在输出的数据是这样的: { "list": [ { "_id": "5e847ab25eb9428600a512352fa6c7c4", "id": 2, "teacher": "徐老师", "cname": "二班", "stu": [{ "_id": null, "score": 90 }] } ] } 但是现在输出的数据有点复杂,如果只想显示 teacher 和 score 这两个值,我们再进行下面的操作。 4. 只显示 teacher 和 score 这两个值我们使用 replaceRoot、mergeObjects 和 project 进行最后的处理: .lookup({ from: 'student', pipeline: $.pipeline() .group({ _id: null, score: $.avg('$score') }) .done(), as: 'stu' }) .match({ teacher:"徐老师" }) .replaceRoot({ newRoot: $.mergeObjects([$.arrayElemAt(['$stu', 0]), '$$ROOT']) }) .project({ _id:0, teacher:1, score:1 }) .end() 现在输出的数据是这样的: { "list": [{ "score": 90, "teacher": "徐老师" }] } 相关文档:云开发聚合搜索:https://docs.cloudbase.net/database/aggregate.html 产品介绍云开发(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-04-08 - 最佳实践丨从 MySQL/MongoDB 迁移数据至 CloudBase 云数据库
迁移说明本篇文章从 MySQL、MongoDB 迁移到云开发数据库,其他数据库迁移也都大同小异。 迁移大致分为以下几步: 从 MySQL、MongoDB 将数据库导出为 JSON 或 CSV 格式创建一个云开发环境到云开发数据库新建一个集合在集合内导入 JSON 或 CSV 格式文件导出一、导出 MySQL 数据下面的流程中,我们使用 Navicat for MySQL 进行导出。您也可以使用其它 MySQL 导出工具。 1、导出为 CSV 格式 选中表后进行导出: [图片] 类型中选择 csv 格式: [图片] 注:在第 4 步时,我们需要勾选包含列的标题 [图片] 导出后的 csv 文件内容 第一行为所有键名,余下的每一行则是与首行键名相对应的键值记录。类似这样: [图片] 2、导出为 JSON 格式 同样的我们将选中的表进行导出为 json 格式: [图片] 剩余步骤全部选择默认即可。 导出后的样子: [图片] 我们将数组去除,最后是这样: [图片] 二、导出 MongoDB 数据首先我们先启动 mongod 服务: [图片] 启动后此终端不要关闭。 1、导出为 CSV 格式 新打开一个终端,输入以下命令: mongoexport -db <数据库> --collection <集合名称> --type csv -f <字段名1[,字段名2]> -o <输出的文件路径> 更详细的参数说明,请参考 MongoDB 文档。 注:导出 csv 格式时需要指定导出的列,否则会出现如下的报错信息:⚠️ csv mode requires a field list 导出后的样子: [图片] 2、导出为 JSON 格式 新打开一个终端,输入以下命令: mongoexport -db <数据库> --collection <集合名称> -o <输出的文件路径> 更详细的参数说明,请参考 MongoDB 文档。 导出后的样子: [图片] 导入1、新建云环境如果已有云环境,可直接跳过这一步打开云开发控制台新建云环境: [图片] 新建环境后耐心等待 2 分钟环境初始化过程。 2、数据库导入点击添加集合来创建一个集合: [图片] 新建之后我们点进去,并进行导入操作: [图片] 选择我们之前导出的 CSV 或 JSON 格式文件。 注意:这里有两种冲突处理模式:Insert 和 Upsert Insert 模式会在导入时总是插入新记录,同一文件不能存在重复的 _id 字段,或与数据库已有记录相同的 _id 字段。如果希望已经存在的数据不被覆盖掉,应该 Insert 模式。Upsert 模式会判断有无该条记录,如果有则更新该条记录,否则就插入一条新记录。如果不希望产生冗余重复的数据,应该使用 Upsert 模式。这里我们选择 Upsert 模式: [图片] 导入过程完毕后,数据库内可以看到导入的数据: [图片] 产品介绍云开发(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 【技术交流群】添加小助手微信号 Tcloudedu1,回复:技术交流 最新资讯关注微信公众号【腾讯云云开发】
2021-04-15 - 小程序里使用async和await变异步为同步,解决回调地狱问题
最近好多同学,学习完石头哥的云开发基础以后,自己实际项目中,总会遇到各种各样的异步问题。 一,异步问题 所谓异步:就是我们请求数据库的数据时,由于网速等各方面原因,数据返回的时间不确定,而我们要使用这些数据,就要等数据返回成功后才可以使用,否则就会报错。 1-1,问题描述 如下: [图片] 好多同学都会认为代码从上往下执行,会先执行请求成功,然后才会执行第11行的代码,商品个数也应该是2. 但是我们的第11行打印却是0.这是为什么呢。 这个错误的原因就是我们使用数据没有写在请求成功里面。正确数据请求返回是异步的,什么时候请求成功不知道,但是我们的第11行代码不会等我们数据请求成功才会执行,所以第11行的打印是0而不是2. 1-2,解决方案 要想解决上面的问题,把你使用数据的地方写到数据请求成功里。 [图片] 这样就能解决异步的问题,但是如果我们有很多地方要使用请求成功的数据,该怎么办呢,总不能把所有的代码都写在数据请求成功里吧。这个时候就要借助async和await来解决这个问题了。 二,使用async和await变异步为同步 所谓的同步,就是我们保持代码正常的从上往下执行。但是呢只要有数据请求,就会有异步问题。所以我们这里要想办法变异步为同步。这就要用到async和await了。 代码如下: [图片] 可以看出,我们不用把使用到数据的代码写到请求成功里就可以了,这样代码读起来是不是常规的从上往下执行的了。 await翻译过来就是等待的意思,其实这里的意思就是,我们等待数据请求完成后,把数据的返回结果赋值给res,然后等数据请求成功以后,就可以正常使用数据请求返回的结果啦。 注意事项 我们在小程序里使用async和await时,一定是成对的。 async放在函数名前面,await放在数据请求前面。 [图片] 并且也要勾选一下:增强编译 [图片] 现在最新版本的小程序开发者工具好像已经支持async和await方法了,好像不勾选增强编译也没事。但是安全起见,还是勾选下增强编译比较好。 三,回调地狱 比如我们有这么一个需求: 用户注册的时候,要先查询是否注册过,没有注册过,才可以新注册。而注册成功后,才可以查看商品列表。 3-1,问题描述 这里给大家分析下需求 [图片] 如果只看流程图,肯定会觉得很简单;但是里面的链路你要认清一个现实。 就是我们如果想最终把商品显示到页面上,必须依赖每个流程都要请求成功。现在是只有3个请求,如果有100个呢,一层套一层的,最后会把你绕晕。这就是回调地狱。 3-2,回调地狱代码 单纯的给你讲,你可能体会不到回调地狱的坏处。那么我用代码实现下我们上面的需求。 假设我们有 用户表:user 商品表:goods 比如我们要注册一个名为”小石头“的用户 第一步:先查询是否注册过 [图片] 可以看出返回的个数为0,代表没有注册过 第二步:注册用户 [图片] 可以看到我们已经可以注册成功了,但是这个时候代码已经嵌套了。 [图片] 第三步:查询商品 由于我们第二步,已经注册’小石头‘成功,所以我们这一步注册一个’大石头‘,注册成功后查询商品。 首先看下代码,这个时候已经嵌套3层了。代码已经变得有点乱了 [图片] 看下结果 [图片] 可以看出我们已经能够成功的查询到商品数据了。 这里只嵌套了三层,看起来还可以接受,如果再继续一层层的嵌套呢。后面代码会变得越来越乱,为了避免回调地狱,我们也可以使用async和await来改造代码。 四,async结合await解决回调地狱 首先看下改造后的代码 [图片] 可以看到代码简洁了很多,逻辑也就是正常的从上往下执行代码 为了更明显的比较。 [图片] 到这里我们就讲完了,是不是感觉使用async和await让你的代码简洁了很多。赶紧跟着石头哥的这篇文章去体验下吧。
2021-05-29 - 微信小程序开发-76种动画 animate.css
1、微信小程序动画有自己的方法:官方链接 但需要自己去写动画效果,比较麻烦。 2、本文介绍的是把animate.css这个非常棒的css库引入到小程序内使用。 animate.css包含76种动画,使用非常简单。animate.css官网 : https://daneden.github.io/animate.css/ 3、由于小程序对代码大小限制比较大,所以删除了animate.css中 所有@-webkit-部分css,减少了一半体积 再把文件后缀名改为wxss,以后出来的百度小程序、支付宝小程序、今日头条小程序估计修改对应的后缀名就可以直接使用了。 下载地址:http://nodejs999.com/animate.wxss 4、放到小程序代码中,然后@import到app.wxss文件中。 我项目是把animate.wxss文件放在utils文件夹下。 所以在app.wxss中加入 @import './utils/animate.wxss'; 即可。 就可以像animate.css一样使用了。
2018-11-01