- (11)低功耗蓝牙能力
在无线通信领域,蓝牙是最基础又常用的能力。蓝牙组网十分简单,两设备间做个配对就可以建立起连接,其通信距离理论值为10米至100米以内,是近距离通信应用场景的首选。今天我们想跟大家分享小程序连接蓝牙的能力。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 目前蓝牙最为普遍的两种规格为蓝牙基础率/增强数据率 (BR/EDR) 和低功耗 (LE) 蓝牙。br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 蓝牙基础率/增强数据率 (BR/EDR) 是经典的蓝牙协议,常用在对数据传输带宽有一定要求的场景上,比如需要传输音频数据的蓝牙音箱等; br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 低功耗 (LE) 蓝牙是从蓝牙4.0起支持的协议,特点就是耗电极低、传输速度更快,常用在对续航要求较高且只需小数据量传输的各种智能电子产品中,包括心率监测仪器、体温计、血糖仪、智能穿戴设备、胎压监测和电子烟等等,应用场景广泛,所以小程序在很早的版本(基础库 1.1.0)就优先支持了低功耗蓝牙能力。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 我们在开发基于低功耗蓝牙接口的小程序时,需要先理解一下蓝牙在连接和通信过程中的一些概念。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 工作模式 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 低功耗蓝牙协议给设备定义了若干角色,其中最主要的角色是:外围设备(Peripheral)与中心设备(Central)。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 1 外围设备是用来提供数据,通过不停地向外广播数据,让中心设备发现自己。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 2 中心设备会扫描外围设备,发现有外围设备存在后,可以与之建立连接,之后就可以使用外围设备提供的服务(Service)。一般而言,手机会担任中心设备的角色,利用外围设备提供的数据进行处理或展示等等。因此,小程序提供低功耗蓝牙接口是默认设定手机为中心设备的。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 通信协议 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 在两个蓝牙设备建立连接之后,双方的数据交互是基于一个叫做 GATT (Generic Attribute Profile) 的规范,根据该规范可以定义出一个个配置文件(Profile),描述该蓝牙设备提供的服务(Service)。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 在整个通信过程中,有三个最主要的概念:配置文件(Profile)、服务(Service)、特性(Characteristic)。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 1 Profile 并不真实存在于蓝牙设备中,它只是被蓝牙标准预先定义的一些 Service 的集合,如果蓝牙设备之间要相互兼容,它们只要支持相同的 Profile 即可。一个蓝牙设备可以支持多个 Profile。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 2 Service 可以理解为蓝牙设备提供的服务,一个设备可以提供多个服务,比如电量信息服务、系统信息服务等。每个 Service 又包含多个 Characteristic 特性值,比如电量信息服务就会有个 Characteristic 表示电量数据,同时还会有一个 16bit 或 128bit 的 UUID 唯一标识该服务,像微信硬件平台的蓝牙智能灯的主服务 UUID 为 0xFEE7。16 bit 的 UUID 实际上是 128 bit 的缩短版,接收方收到后会补上蓝牙的 UUID 基数,目的是为了提高传输效率。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 3 Characteristic 是在 GATT 规范中最小的逻辑数据单元,由一个 value 和多个描述特性的 Desciptor 组成。实际上,在与蓝牙设备打交道,主要就是通过读写 Characteristic 的 value 完成。同样的,Characteristic 也是通过一个 16bit 或 128bit 的 UUID 唯一标识。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 总结一下,如下图所示,我们可以简单地理解为:每个蓝牙设备可能提供多个 Service,每个 Service 可能有多个 Characteristic,我们根据蓝牙设备的协议用对应的 Characteristic 进行读写即可达到与其通信的目的。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> [图片] br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 在理解了上面的模式和概念后,接下来我们看看如何可以使用小程序提供的蓝牙接口。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> Step 1 :扫描并发现蓝牙外围设备 扫描并发现蓝牙外设。如果蓝牙功能未开启,可监听蓝牙状态变化以便自动进入下一步,提升用户体验。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> [图片] br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> Step 2 :连接蓝牙外围设备 若之前连接过某个设备,可跳过扫描步骤,直接传入 deviceId 连接。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> [图片] br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> Step 3 :查找蓝牙外围设备的服务 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> [图片] br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> Step 4 : 读写指定服务的特性值 [图片] br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 基本上,小程序暴露出来的蓝牙接口都是系统级 API ,但在使用流程上对安卓和 iOS 两个平台做了统一,因此在使用这一套接口时也会出现一些因系统限制而导致的问题,在这里我们整理了一些常见的问题供开发者参考: br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 1 在安卓上,部分机型无定位权限或者是定位开关未打开时会搜不到设备。原因是蓝牙功能是可以获取到定位的,系统基于安全考量,使用蓝牙接口时必须要有定位权限,否则搜索不到; br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 2 在安卓上,部分机型获取设备服务时会多出 00001800 和 00001801 UUID 的服务,这是系统行为,注意不要使用这两个服务; br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 3 建立连接和关闭连接必须要成对调用。如果未能及时关闭连接释放资源,容易导致 state 133 GATT ERROR的异常; br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 4 与蓝牙设备通信的 MTU(最大传输单元)系统限定为 20 字节,如果超过则会出错,这里应该根据蓝牙设备协议进行分片传输。 br ="" class="" style="max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;"> 更多有关小程序连接蓝牙功能的信息,可查阅[接口文档]。
2018-08-17 - 小程序获取微信语言设置
我的小程序需要根据用户的微信语言设置自动转换到对应的中英文界面,如何实现?
2017-04-06 - 云调用能力—图像处理和OCR
云调用有些接口属于 AI 服务的范畴,比如借助于人工智能来进行智能裁剪、扫描条码/二维码、图片的高清化等图像处理和识别银行卡、营业执照、驾驶证、身份证、印刷体、驾驶证等 OCR,有了这些接口我们也能在小程序里使用人工智能了。接下来我们以小程序的条码/二维码识别和识别印刷体为例来介绍一下云调用。 13.3.1 图像处理使用开发者工具新建一个云函数,如 scancode,然后在 config.json 里添加 img.scanQRCode 云调用的权限,使用 npm install 安装依赖之后,上传并部署所有文件(此时也会更新权限)。 { "permissions": { "openapi": [ "img.scanQRCode" ] } } 然后再在 index.js 里输入以下代码,注意[代码]cloud.openapi.img.scanQRCode[代码]方法和[代码]img.scanQRCode[代码]权限的对应写法,不然会报 604100 的错误。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { try { const result = await cloud.openapi.img.scanQRCode({ imgUrl: "https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/demo/qrcodetest.png", //注意二维码必须是条码/二维码,不能是小程序码 }); return result; } catch (err) { console.log(err); return err; } }; 调用该云函数之后,返回的 result 对象里包含 result 对象,在 codeResults 的 data 里可以得到二维码里包含的内容。 codeResults: [{ data: "使用云开发来开发微信小程序可以免费。。。", pos: {leftTop: {…}, rightTop: {…}, rightBottom: {…}, leftBottom: {…}},typeName: "QR_CODE"}] errCode: 0 errMsg: "openapi.img.scanQRCode:ok" imgSize: {w: 260, h: 260} 13.3.2 OCR 人工智能识别使用开发者工具新建一个云函数,如 ocrprint,然后在 config.json 里添加 ocr.printedText 云调用的权限,使用 npm install 安装依赖之后,上传并部署所有文件(此时也会更新权限)。 { "permissions": { "openapi": [ "ocr.printedText" ] } } 调用该云函数之后,返回的 result 对象里包含 result 对象,在 codeResults 的 data 里可以得到二维码里包含的内容。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { try { const result = await cloud.openapi.ocr.printedText({ imgUrl: "https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/demo/ocrprint.png", }); console.log(result); return result; } catch (err) { console.log(err); return err; } }; 调用该云函数之后,返回的 result 对象里包含 result 对象,在的 items 里可以返回图片包含的文字内容。 items: Array(4) 0: {text: "JavaScript入门", pos: {…}} 1: {text: "JavaScript是目前世界上最流行的编程语言之一,它也是小程序开发最重要的基础语言。要做出一个功能复杂的小程序,除了需要掌握JavaScript的基本语", pos: {…}} 2: {text: "法,还要了解如何使用JavaScript来操作小程序(通过API接口)", pos: {…}} 3: {text: "过API接口)。", pos: {…}} 13.3.3 图像处理拓展能力图片是小程序非常重要的元素,尤其是旅游照片、社交图片、电商产品图片、相册类小程序、媒体图文等,图片的加载速度、清晰度、图片的交互、图片效果的处理以及图片加载的 CDN 消耗都是一个不得不需要去关注的问题。而云开发图像处理拓展能力结合云存储则可以非常有效的解决很多问题。 强烈建议所有有图片处理需求的用户都应该安装图像处理拓展能力,这个能力大大弥补和增强了云存储在图片处理能力,尤其是图片按照需求的规格进行缩放可以大大减少 CDN 的消耗以及图片的加载速度以及我们可以按照不同的业务场景使用快速缩略模板,而这一切的操作和云存储的结合都是非常实用且易用的。 1、图像处理能力介绍云开发图像处理能力结合的是腾讯云数据万象的图片解决方案,图像处理提供多种图像处理功能,包含智能裁剪、无损压缩、水印、格式转换等,图像处理拓展能力所包含的功能非常丰富,使用如下图片处理的费用是按量计费的,计费周期为月,10TB 以内免费,超出 10TB,按 0.025 元/GB 来计费,省事而便宜: 缩放:等比缩放、设定目标宽高缩放等多种方式;裁剪:普通裁剪、缩放裁剪、内切圆、人脸智能裁剪;旋转:普通旋转、自适应旋转;格式转换:jpg、bmp、gif、png、webp、yjpeg 格式转换,gif 格式优化,渐进显示功能;质量变换:针对 JPG 和 WEBP 图片进行质量变换;高斯模糊:对图片进行模糊处理;锐化:对图片进行锐化处理;图片水印:提供图片水印处理功能;文字水印:提供实时文字水印处理功能;获取图片基本信息:查询图片基本信息,包括格式、长、宽等;获取图片 EXIF:查询图片 EXIF 信息,如照片的拍摄参数、缩略图等;获取图片主色调:获取图片主色调信息;去除元信息:去除图片元信息,减小图像体积;快速缩略模板:快速实现图片格式转换、缩略、剪裁等功能,生成缩略图;管道操作符:对图片按顺序进行多种处理当我们在腾讯云云开发网页控制台(注意要使用微信公众号的方式登录)添加完图像处理的拓展能力之后,我们可以在腾讯云的数据万象存储桶里看到云开发的云存储,而关于图像处理能力的深入使用,也可以参考腾讯云数据万象的技术文档。在小程序云开发里使用图像处理能力的方法有三种: 图像地址的拼接,只需要在图片的下载地址 url 里拼接一些简单的参数(API 管道操作符),就能够使用到图像处理的能力,非常方便易用,这个不会把图片处理的结果存储到云存储,不会占用云存储的空间;在获取图片基本信息、获取图片 EXIF、获取图片主色调等方面非常方便;在前端(小程序端)做持久化图像处理,支持有结果图输出的处理操作,也就是我们可以把缩放、裁剪、格式转换、质量变换等处理之后的图片存储到云存储方便以后使用;在云函数端做持久化图像处理,支持有结果图输出的处理操作 01图像地址的拼接在了解图像处理能力之前,我们需要先了解一下云存储文件的 fileID、下载地址以及下载地址携带的权限参数 sign(图像处理能力的参数拼接就是基于下载地址的),如下图所示: [图片] 在安装了图像处理拓展能力的情况下,我们可以直接拿云存储的下载地址进行拼接,拼接之后的链接我们既可以在小程序里使用,也可以用于图床,比如原始图片下载地址为: https://786c-xly-xrlur-1300446086.tcb.qcloud.la/hehe.jpg?sign=b8ac757538940ead8eed4786449b4cd7&t=1591752049 而相关的图像处理能力的拼接案例如下,具体的操作可以看技术文档,实际的效果,可以复制粘贴链接到浏览器或小程序里体验(换成自己的地址),注意拼接方式就是在下载地址后面加了一个[代码]&imageMogr2/thumbnail/!20p[代码](注意这里由于已经有了一个 sign 参数,所以拼接时用的是[代码]$[代码],不能写成[代码]?[代码],否则不会生效),直接就可以啦,非常易用: //将图片等比例缩小到原来的20% https://786c-xly-xrlur-1300446086.tcb.qcloud.la/hehe.jpg?sign=b8ac757538940ead8eed4786449b4cd7&t=1591752049&imageMogr2/thumbnail/!20p 后面为了方便,我们将[代码]https://786c-xly-xrlur-1300446086.tcb.qcloud.la/hehe.jpg?sign=b8ac757538940ead8eed4786449b4cd7&t=1591752049[代码]简写为 download_url: //缩放宽度,高度不变,下面案例为宽度为原图50%,高度不变 download_url&imageMogr2/thumbnail/!50px //缩放高度,宽度不变,下面案例为高度为原图50%,宽度不变 download_url&imageMogr2/thumbnail/!x50p //指定目标图片的宽度(单位为px),高度等比压缩,注意下面的是x,不是px,p与x在拼接里代表着不同的意思 download_url&imageMogr2/thumbnail/640x //指定目标图片的高度(单位为px),宽度等比压缩: download_url&imageMogr2/thumbnail/x355 //限定缩略图的宽度和高度的最大值分别为 Width 和 Height,进行等比缩放 download_url&imageMogr2/thumbnail/640x355 //限定缩略图的宽度和高度的最小值分别为 Width 和 Height,进行等比缩放 download_url&imageMogr2/thumbnail/640x355r //忽略原图宽高比例,指定图片宽度为 Width,高度为 Height ,强行缩放图片,可能导致目标图片变形 download_url&imageMogr2/thumbnail/640x355! //等比缩放图片,缩放后的图像,总像素数量不超过 Area download_url&imageMogr2/thumbnail/150000@ //取半径为300,进行内切圆裁剪 download_url&imageMogr2/iradius/300 //取半径为100px,进行圆角裁剪 download_url&imageMogr2/rradius/100 //顺时针旋转90度 download_url&imageMogr2/rotate/90 //将jpg格式的原图片转换为 png 格式 download_url&imageMogr2/format/png //模糊半径取8,sigma 值取5,进行高斯模糊处理 download_url&imageMogr2/blur/8x5 //获取图片的基础信息,返回的是json格式,我们可以使用https请求来查看图片的format格式,width宽度、height高度,size大小,photo_rgb主色调 download_url&imageInfo 2、小程序端持久化图像处理当我们希望把缩放、裁剪、旋转、格式变换等图像处理的结果(也就是处理之后的图片)存储到云存储,这个就叫做持久化图像处理,在安装了图像处理能力之后,我们也可以在小程序端做图像处理。 当用户把原始图片上传到小程序端时,我们需要对该图片进行一定的处理,比如图片过大就对图片进行裁剪缩小;比如图片需要进行一定的高斯模糊、旋转等处理,这些虽然在图像处理之前,也是可以使用 js 来做的,但是小程序端图像处理的效果并没有那么好或者过于复杂,使用图像处理的拓展能力就非常实用了。在小程序端构建图像拓展依赖 首先在开发者工具小程序根目录(一般为 miniprogram),右键“在终端中打开”,然后在终端里输入以下代码,也就是在小程序端安装图像拓展依赖,安装完时,我们就可以在 miniprogram 文件夹下看到 node_modules: npm install --save @cloudbase/extension-ci-wxmp@latest 然后点击开发者工具工具栏里的工具-构建 npm,构建成功之后,就可以在 miniprogram 文件夹下看到 minprogram_npm 里有@cloubase 文件夹,里面有 extension-ci-wxmp,说明图像拓展依赖就构建完成。 在小程序端进行图像处理 使用开发者工具新建一个 imgprocess 的页面,然后在 imgprocess.wmxl 里输入如下代码,我们新建一个 button 按钮: 处理图片button> 然后再在 imgprocess.js 的 Page()函数的上面(外面)引入图像处理依赖,代码如下: const extCi = require("./../../miniprogram_npm/@cloudbase/extension-ci-wxmp"); 然后再在 imgprocess.js 的 Page()函数的里面写一个 imgprocess 的事件处理函数,点击 button 之后会先执行 readFile()函数,也就是获取图片上传到小程序临时文件的结果(是一个对象),然后再调用 imageProcess()函数,这个函数会对图片进行处理,图片会保存为[代码]tcbdemo.jpg[代码],而处理之后的图片会保存为 image_process 文件夹下的 tcbdemo.png,相当于保存了两张图片: async imgprocess(){ const readFile = async function() { let res = await new Promise(resolve=>{ wx.chooseImage({ success: function(res) { let filePath = res.tempFilePaths[0] let fm = wx.getFileSystemManager() fm.readFile({ filePath, success(res){ resolve(res) } }) } }) }) return res } let fileResult = await readFile(); //获取图像的临时文件上传结果 const fileContent = fileResult.data //获取上传到临时文件的图像,为Uint8Array或Buffer格式 async function imageProcess() { extCi.invoke({ action: "ImageProcess", cloudPath: "tcbdemo.jpg", // 图像在云存储中的路径,有点类似于wx.cloud.uploadFile接口里的cloudPath,上传的文件会保存为云存储根目录下的hehe.jpg operations: { rules: [ { fileid: "/image_process/tcbdemo.png", //将图片存储到云存储目录下的image_process文件夹里,也就是我们用image_process存储处理之后的图片 rule: "imageMogr2/format/png", // 处理样式参数,我们可以在这里写图片处理的参数拼接 } ] }, fileContent }).then(res => { console.log(res); }).catch(err => { console.log(err); }) } await imageProcess() } 可能你的开发者工具会报以下错误:[代码]https://786c-xly-xrlur-1300446086.pic.ap-shanghai.myqcloud.com 不在以下 request 合法域名列表中,请参考文档:https://developers.weixin.qq.com/miniprogram/dev/framework/ability/network.html[代码],这个要按照参考文档将链接加入到合法域名当中,不然不会生成图片;[代码]action[代码]是操作类型,它的值可以为:ImageProcess 图像处理,DetectType 图片安全审核(后面会介绍),WaterMark 图片忙水印、DetectLabel 图像标签等。[代码]operations[代码]是图像处理参数,尤其是 rule 和我们之前 url 的拼接是一致的,比如[代码]imageMogr2/blur/8x5[代码]、[代码]imageMogr2/rradius/100[代码]等参数仍然有效。上面函数里的 fileContent 不是必要的,也就是说我们可以不在小程序端上传图片,而是直接修改云存储里面已有的图片,并将图片处理后的照片保存,这种情况代码可以写成如下: async imgprocess(){ extCi.invoke({ action: "ImageProcess", cloudPath: "tcbdemo.jpg", // 会直接处理这张图片 operations: { rules: [ { fileid: "/image_process/tcbdemo.png", rule: "imageMogr2/format/png", // 处理样式参数,与下载时处理图像在url拼接的参数一致 } ] }, }).then(res => { console.log(res); }).catch(err => { console.log(err); }) } 3、云函数端持久化图像处理在云函数端的处理和小程序端的处理,使用的方法大体上是一致的,不过云函数的处理图片的场景和小程序端处理图片的场景会有所不同,小程序端主要用于当用于上传图片时就对图片进行处理,云函数则主要用于从第三方下载图片之后进行处理或者对云存储里面的图片进行处理(比如使用定时触发器对云存储里指定文件夹的图片进行处理)。不建议把图片传输到云函数端再来对图片进行处理。 使用开发者工具新建一个 imgprocess 的云函数,然后在 package.json 里添加 latest 最新版的[代码]@cloudbase/extension-ci[代码],并右键云函数目录选择在终端中打开输入命令 npm install 安装依赖: "dependencies": { "wx-server-sdk": "latest", "@cloudbase/extension-ci": "latest" } 然后再在 index.js 里输入以下代码,代码的具体含义可以参考小程序端的内容讲解: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const extCi = require("@cloudbase/extension-ci"); cloud.registerExtension(extCi); async function process() { try { const opts = { rules: [ { fileid: "/image_process/tcbdemo.jpeg", rule: "imageMogr2/format/png", }, ], }; const res = await app.invokeExtension("CloudInfinite", { action: "ImageProcess", cloudPath: "tcbdemo.jpg", fileContent, operations: opts, }); console.log(res); return res; } catch (err) { console.log(err); } }
2021-09-10 - 数据库性能优化指导
云开发的数据库虽然是高性能、支持弹性扩容,但是很多用户在使用的过程中,更加注重功能的实现,而忽视了数据库的设计、索引的创建以及语句的优化等对性能的影响,因此会遇到很多影响数据库性能的问题,因此这里特意总结一下云开发数据库性能优化的注意事项。 12.7.1 数据库性能与优化建议以下是一些影响数据库性能的优化建议,当然要结合具体的业务情况来处理,不能一概而论。尤其是一些请求量比较大、比较频繁,比如小程序首页的数据请求,数据库的优化要格外重视。 1、要合理使用索引 使用索引可以提高文档的查询、更新、删除、排序操作,所以要结合查询的情况,适当创建索引。要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。更多索引的细节在索引的章节里有介绍。 2、擅于结合查询情况创建组合索引 对于包含多个字段(键)条件的查询,创建包含这些字段的组合索引是个不错的解决方案。组合索引遵循最左前缀原则,因此创建顺序很重要,如果对组合索引不了解,可以结合索引的命中情况来判断组合索引是否生效。要善于使用组合索引做到用最少的索引覆盖最多的查询。 3、查询时要尽可能通过条件和 limit 限制数据 在查询里 where 可以限制处理文档的数量,而在聚合运算中 match 要放在 group 前面,减少 group 操作要处理的文档数量。无论是普通查询还是聚合查询都应该使用 limit 限制返回的数据数量。 其实云开发针对普通查询 db.collection('dbName').get()默认都有 limit 限制,在小程序端的限制为 20 条(自定义上限也是 20 条),在云函数端的限制为 100 条(自定义上限可以设置为 1000 条),聚合则在小程序端和云函数端默认都为 20 条(自定义没有上限,几万条都可以,前提是取出来的数据不能大于 16M),也就是云开发数据库已经自带了一些性能优化,我们不应该把这些默认的限制当成是一种束缚,而去随意突破这些限制。 4、推荐在小程序端增删改查数据库 可以结合数据库的安全规则,让数据库的增删改查在小程序端进行,这样速度会更快,而且还可以节省云函数的资源。 云开发数据库的增删改查可以在小程序端进行,也可以在云函数端进行,那到底应该把数据库的增删改查放在小程序端还是云函数端呢?一般情况下建议放在小程序端,这样就只会消耗数据库请求的次数,而不会额外增加消耗云函数的资源使用量 GBs、外网出流量。而云函数虽然有数据库操作的更高的权限,但是小程序端结合安全规则也是可以让数据库的权限粒度更细,也能满足大部分权限要求。 5、尽可能限制返回的字段等数据量 如果查询无需返回整个文档或只是用来判断键值是否存在,普通查询可以通过 filed、聚合查询可以通过 project 来限制返回的字段,减少网络流量和客户端的内存使用。 { "title": "为什么要学习云开发", "content": "云开发是腾讯云为移动开发者提供的一站式后端云服务", "comments": [{ "name": "李东bbsky", "created_on": "2020-03-21T10:01:22Z", "comment": "云开发是微信生态下的最推荐的后台技术解决方案" }, { "name": "小明", "created_on": "2020-03-21T11:01:22Z", "comment": "云开发学起来太简单啦" }] } 云数据库是关系型数据库,一个记录里可以嵌套非常多的数组和对象,如果取出整个记录里的所有嵌套内容就太耗性能流量了,比如上面的嵌套数组,有时候业务上并不需要显示 comments 里的某些字段,是可以通过 field 的点表示法来限制返回的字段的。 //不显示comments里的created_on .field({ "comments.created_on":0 }) //只显示comments里的comment,comments里的其他字段都不显示 .field({ "comments.comment":1 }) 6、查询量大时建议不要用正则查询 正则表达式查询不能使用索引,执行的时间比大多数选择器更长,所以业务量比较大的地方,能不用正则查询就不用正则查询(尽量用其他方式来代替正则查询),即使使用正则查询也一定要尽可能的缩写模糊匹配的范围,比如使用开始匹配符 ^ 或结束匹配符 $ 。 比如有人是这样用正则查询的,他想根据省市来筛选客户来源数据,但是客户来源的地址 address 填写的是”广东省深圳市“或”广东深圳“,省市数据并不规范一致,于是使用正则进行模糊查询,但是如果你需要经常根据地址来筛选客户来源,那你应该在数据库对数据进行处理,比如 province 和 city 来清洗重组数据从而替代模糊查询。 7、尽可能使用更新指令 通过更新指令对文档进行修改,通常可以获得更好的性能,因为更新指令不需要查询到记录就可以直接对文档进行字段级的更新,尤其是不需要更新整个文档只需要更新部分字段的场景。 还是上面的那个记录为例,比如我们需要给文章添加评论,也就是往 comments 数组里添加值,我们可以使用 [代码]_.push[代码]来给数组字段进行字段级别的操作,而不是取出整个记录,然后把评论用数组的 concat 或 push 的方法添加到记录里,再更新整个记录: .update({ data:{ comments:_.push([{ "name": "小明", "created_on": "2020-03-21T11:01:22Z", "comment": "云开发学起来太简单啦" }]) } }) 云开发数据库一个记录可能会嵌套很多层,因此也会很大,使用更新指令进行字段级别的微操比直接使用 update 这种记录级别的更新性能要更好。 8、不要对太多数据进行排序 不要一次性取出太多的数据并对数据进行排序,如果需要排序,请尽量限制结果集中的数据量,比如我们可以先用 where、match 等操作限制数据量,也就是通常要把 orderBy 放在普通查询或聚合查询的最后面。 这里尤其强调的是,发现有不少人由于对数据库的排序 orderBy 与翻页 skip 没有理解,竟然把数据库所有数据使用遍历取出来之后再来排序,哪怕是数据量只有百千条,这也是不正确的处理方式,应该禁止这么干。排序使用数据库的普通查询或聚合查询的 orderBy 就可以做到了,云开发默认的 limit 数据限制不会影响排序的结果,禁止遍历取出所有数据再来排序的愚蠢行为。 当然如果业务会需要经常对同一数据的多个字段来排序,比如商品经常会按最新上架、价格高低、产地、折扣力度等进行排序,则建议一次性取出这些数据,存储在缓存中,使用 JavaScript 的数组来进行排序,而不是用数据库查询。 9、尽量少在业务量大的地方用以下查询指令 查询中的某些查询指令可能会导致性能低下,如判断字段是否存在的[代码]exists[代码],要求值不在给定的数组内的[代码]nin[代码],表示需满足任意多个查询筛选条件[代码]or[代码],表示需不满足指定的条件[代码]not[代码],尽量少在业务使用量比较大的地方用这些查询指令。 这里所说的尽量少用不代表不用,而是能够用最直接的方式就用最直接的方式代替,让数据库查询尽可能的简单而不是搞的过于复杂,尽可能少让查询指令做这些复杂的事情。 10、集合中文档的数量可以定期归档 集合中文档的数据量会影响查询性能,对不用的数据或过期的数据可以进行定期归档并删除。比如我们也可以借助于定时触发器周期性的对数据库里的数据进行备份、删除。 11、不要让数据库请求干多余的事情,尽量少干事 能够使用 JavaScript 替代的计算、数组、对象操作等,就尽量用 JavaScript 处理;能通过数据库设计让数据库查询少计算的就尽量合理设计数据库,要尽可能的让数据库少干活,不能一次查询多个指令、正则查询套来套去的。 12、在数据库设计时可以用内嵌文档来取代 lookup 云开发数据库是非关系型数据库,可以对经常要使用 lookup 跨表查询的情况做反范式化的内嵌文档设计,通过这种方式取代联表查询 lookup 可以提升不少性能。 减少使用联表查询 lookup 的使用的方式要注意两点,一是通过内嵌文档的方式是可以减少关系型数据库那种表与表之间的关联关系的,比如要联表取出博客里最新的 10 篇文章以及文章里相应的评论,这在关系型数据库里原本是需要联表查询的,但是当把评论内嵌到文章的集合里时,就不需要联表了;二是有的时候我们只是需要跨表而不是联表,可以通过多次查询来取代联表。 13、推荐使用短字段名 和关系型数据库不同的是,云开发数据库是文档型数据库,集合中的每一个文档都需要存储字段名,因此字段名的长度相比关系型数据库来说会需要更多的存储空间。 "comments": [{ "name": "李东bbsky", "created_on": "2020-03-21T10:01:22Z", "comment": "云开发是微信生态下的最推荐的后台技术解决方案" }, { "name": "小明", "created_on": "2020-03-21T11:01:22Z", "comment": "云开发学起来太简单啦" }] 这里的字段名 name、created_on、comment 有多少个记录,有多少个嵌套的对象就会被写多少次,有时候比字段的值还要长,是比较占空间的。 12.7.2 数据库设计以及处理的优化建议1、增加冗余字段在业务上有些关键的数据可以通过间接的方式查询获取到,但是由于查询时会存在计算、跨表等问题,这个时候建议新增一些冗余字段。 比如我们要统计文章下面的评论数,可能你将文章的评论独立建了一个集合如 comments,这时候要获取每篇文章的评论数是可以根据文章的 id 条件来 count 该文章有多少条评论的。或者你也可以把每篇文章的评论数组作为子文档内嵌到每个文章记录的 comments 字段,这个时候可以通过数组的长度来算出该文章的评论数。类似于评论数的还有点赞量、收藏量等,这些虽然都是可以通过 count 或数组 length 的方式来间接获取到的,但是在评论数很多的情况下,count 和数组的 length 是非常耗性能的,而且 count 还需要独立占据一个请求。 遇到这种情况,建议在数据库设计时,要用所谓的冗余字段来记录每篇文章的点赞量、评论数、收藏量,在小程序端直接用 inc 原子自增的方式更新该字段的值。 { "title": "为什么要学习云开发", "content": "云开发是腾讯云为移动开发者提供的一站式后端云服务", "commentNum":2, //新增一个评论数的字段 "comments": [{ "name": "李东bbsky", "created_on": "2020-03-21T10:01:22Z", "comment": "云开发是微信生态下的最推荐的后台技术解决方案" }, { "name": "小明", "created_on": "2020-03-21T11:01:22Z", "comment": "云开发学起来太简单啦" }] } 比如我们希望在博客的首页展示文章列表,而每篇文章要显示评论总数。虽然我们可以通过 comments 的数组长度以及如果存在二级三级评论(尤其是这种情况),也是可以通过数组方法获取到评论数,但是不如直接查询新增的冗余字段[代码]commentNum[代码]来得直接。 2、虚假删除有时候我们的业务会需要用户经常删除数据库里面的记录或记录里的数组的情况,但是删除数据是非常耗费性能的一件事,碰到业务高峰期,数据库就会出现性能问题。这个时候,建议新增冗余字段做虚假删除,比如给记录添加 delete 的字段,默认值为 false,当执行删除的时候,可以将字段的值设置 true,查询时只显示 delete 为 false 的记录,这样数据在前端就不显示了,做到了虚假删除,在业务低谷时比如凌晨可以结合定时触发器每天这个时候清理一遍。 3、尽量不要把数据库请求放到循环体内我们经常会有查询数据库里的数据,并对数据进行处理之后再写回数据库的需求,如果查询到的数据有很多条时,就会需要我们进行循环处理,不过这个时候一定要注意,不要把数据库请求放到循环体内,而是先一次性查询多条数据,在循环体内对数据进行处理之后再一次性写回数据库。 当然小程序有些接口不能进行数组操作,只能一条一条执行,比如发送订阅消息、上传文件等操作等,这个避免的不了的例外。但是有些是可以通过数据库的设计来规避这个问题的,比如把经常要新增大量记录的数据库设计为只需要新增内嵌文档的数组数据等。 4、尽量使用一个数据库请求代替多个数据库请求在数据库的设计上以及在数据库请求的代码上,尽可能用一个数据库请求来代替多个数据库请求,尤其是用户最常访问的首页,如果一个页面的数据库请求太多,会导致数据库的并发问题。有些数据能够缓存到小程序端就缓存到小程序度,不必过分强调数据的一致性。 5、规划好文档适时创建空字段我们有这样一个集合 user,最终会用来存储用户的个人信息,比如当我们在用户点击登录时会获取用户的昵称和头像,于是一般的逻辑是我们会在数据库创建一个记录,如下所示: _id:"", userInfo:{ "name":"李东bbsky", "avatarUrl":"头像地址" } 但是更好的方式是,我们应该创建一个完整的记录(按照最终的字段设计),哪怕现在还没有数据,也要一致性建好这些空字段,方便以后直接使用 update 的方式来往里面填充数据。 _id:"", userInfo:{ "name":"李东bbsky", "avatarUrl":"头像地址", "email":"", "address":"" ... }, stars:[],//存储点赞的文章 collect:[] //存储收藏的文章 12.7.3 慢查询与告警目前我们没法直接查看数据库请求所花费的时间,但是有一些其他数据作为佐证,在云函数端进行数据库请求,如果云函数的执行时间超过 100ms 甚至更多,则基本可以判定为慢查询,数据库需要优化。这时,慢查询不仅会影响数据库的性能,还会影响云函数的性能。 我们知道云函数和云数据库的并发都是非常依赖他们的耗时的,如果数据库查询速度变慢,查询一次耗时由几十毫秒增加到几百毫秒,甚至以秒计算,都是十分耗费资源和影响并发的: 云函数资源使用量 GBs:资源使用量 = 函数配置内存 X 运行计费时长,如果云函数里有数据库请求耗费了运行时长,云函数资源使用量也会增加;不过云函数的并发统一上限为 1000,通常是很难达到的;数据库的 QPS = 数据库同时连接数 * 1000ms/数据库请求的执行时间,如果数据库请求的执行时间出现大幅上升,QPS 也就会成倍的下降,非常影响数据库的并发,会出现[代码]Connection num overrun[代码]的报错。我们可以在云开发控制台设置-告警设置来给指定的云函数尤其是业务调用最频繁的云函数设置运行时间以及云函数运行错误的告警,以便随时了解云开发环境的运行状况。
2021-09-10 - 数据库原子操作和事务讲解
使用更新指令(如 inc、mul、addToSet)可以对云数据库的一条记录和记录内的子文档(结合反范式化设计)进行原子操作,但是如果要跨多个记录或跨多个集合的原子操作时,就需要使用云数据库的事务能力。 12.6.1 更新指令的原子操作关系型数据库是很难做到通过一个语句对数据强制一致性的需求来表示的,只能依赖事务。但是云开发数据库由于可以反范式化设计内嵌子文档,以及更新指定可以对单个记录或同一个记录内的子文档进行原子操作,所以通常情况下,云开发数据库不必使用事务。 比如调整某个订单项目的数量之后,应该同时更新该订单的总费用,我们可以设计采用如下方式设计该集合,比如订单的集合为 order: { "_id": "2020030922100983", "userID": "124785", "total":117, "orders": [{ "item":"苹果", "price":15, "number":3 },{ "item":"火龙果", "price":18, "number":4 }] } 客户在下单的时候经常会调整订单内某个商品比如苹果的购买数量,而下单的总价又必须同步更新,不能购买数量减少了,但是总价不变,这两个操作必须同时进行,如果是使用关系型数据库,则需要先通过两次查询,更新完数据之后,再存储进数据库,这个很容易出现有的成功,有的没有成功的情况。但是云开发的数据库则可以借助于更新指令做到一条更新来实现两个数据同时成功或失败: db.collection("order") .doc("2020030922100983") .update({ data: { "orders.0.number": _.inc(1), total: _.inc(15), }, }); 这个操作只是在单个记录里进行,那要实现跨记录要进行原子操作呢?更新指令其实是可以做到事务仿真的,但是比较麻烦,这时就建议用事务了。 12.6.2 事务与 ACID事务就是一段数据库语句的批处理,但是这个批处理是一个 atom(原子),多个增删改的操作是绑定在一起的,不可分割,要么都执行,要么回滚(rollback)都不执行。比如银行转账,需要做到一个账户的钱汇出去了,那另外一个账户就一定会收到钱,不能钱汇出去了,但是钱没有到另外一个的账上;也就是要执行转账这个事务,会对 A 用户的账户数据和 B 用户的账户数据做增删改的处理,这两个处理必须一起成功一起失败。 1、ACID一般来说,事务是必须满足 4 个条件(ACID): Atomicity(原子性)、Consistency(稳定性)、Isolation(隔离性)、Durability(可靠性): 原子性:整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中一部分操作,一致性:事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行前后,数据库都必须处于一致性状态。换句话说,事务的执行结果必须是使数据库从一个一致性状态转变到另一个一致性状态。比如在执行事务前,A 用户账户有 50 元,B 用户账户有 150 元;执行 B 转给 A 50 元事务后,两个用户账户总和还是 200 元。隔离性:事务的隔离性是指在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间事务之间,互不干扰。比如在线银行,同时转账的人虽然很多,但是不会出现影响 A 与 B 之间的转账;可靠性:即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束时的状态,已提交事务的更新不会丢失。 2、云函数事务注意事项01不支持批量操作,只支持单记录操作 在事务中不支持批量操作(where 语句),只支持单记录操作(collection.doc, collection.add),这可以避免大量锁冲突、保证运行效率,并且大多数情况下,单记录操作足够满足需求,因为在事务中是可以对多个单个记录进行操作的,也就是可以比如说在一个事务中同时对集合 A 的记录 x 和 y 两个记录操作、又对集合 B 的记录 z 操作。 02云数据库采用的是快照隔离 对于两个并发执行的事务来说,如果涉及到操作同一条记录的时候,可能会发生问题。因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。 脏读:指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据;不可重复读:在一个事务内两次读到的数据是不一样的,受到另一个事务修改后提交的影响,因此称为是不可重复读幻读:第一个事务对表进行读取,当第二个事务对表进行增加或删除操作事务提交后,第一个事务再次读取,会出现增加或减少行数的情况云开发的数据库系统的事务过程采用的是快照隔离(Snapshot isolation),可以避免并发操作带来数据不一致的问题。 事务期间,读操作返回的是对象的快照,而非实际数据事务期间,写操作会:1. 改变快照,保证接下来的读的一致性;2. 给对象加上事务锁事务锁:如果对象上存在事务锁,那么:1. 其它事务的写入会直接失败;2. 普通的更新操作会被阻塞,直到事务锁释放或者超时事务提交后,操作完毕的快照会被原子性地写入数据库中 12.6.3 事务操作的两套 API云开发数据库的事务提供两种操作风格的接口,一个是简易的、带有冲突自动重试的 runTransaction 接口,一个是流程自定义控制的 startTransaction 接口。通过 runTransaction 回调中获得的参数 transaction 或通过 startTransaction 获得的返回值 transaction,我们将其类比为 db 对象,只是在其上进行的操作将在事务内的快照完成,保证原子性。transaction 上提供的接口树形图一览: transaction |-- collection 获取集合引用 | |-- doc 获取记录引用 | | |-- get 获取记录内容 | | |-- update 更新记录内容 | | |-- set 替换记录内容 | | |-- remove 删除记录 | |-- add 新增记录 |-- rollback 终止事务并回滚 |-- commit 提交事务(仅在使用 startTransaction 时需调用) 1、通过 runTransaction 回调获得 transaction以下提供一个使用 runTransaction 接口的,两个账户之间进行转账的简易示例。事务执行函数由开发者传入,函数接收一个参数 transaction,其上提供 collection 方法和 rollback 方法。collection 方法用于取数据库集合记录引用进行操作,rollback 方法用于在不想继续执行事务时终止并回滚事务。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const _ = db.command; exports.main = async (event) => { try { const result = await db.runTransaction(async (transaction) => { const aaaRes = await transaction.collection("account").doc("aaa").get(); const bbbRes = await transaction.collection("account").doc("bbb").get(); if (aaaRes.data && bbbRes.data) { const updateAAARes = await transaction .collection("account") .doc("aaa") .update({ data: { amount: _.inc(-10), }, }); const updateBBBRes = await transaction .collection("account") .doc("bbb") .update({ data: { amount: _.inc(10), }, }); console.log(`transaction succeeded`, result); return { aaaAccount: aaaRes.data.amount - 10, }; } else { await transaction.rollback(-100); } }); return { success: true, aaaAccount: result.aaaAccount, }; } catch (e) { console.error(`事务报错`, e); return { success: false, error: e, }; } }; 事务执行函数必须为 async 异步函数或返回 Promise 的函数,当事务执行函数返回时,SDK 会认为用户逻辑已完成,自动提交(commit)事务,因此务必确保用户事务逻辑完成后才在 async 异步函数中返回或 resolve Promise。 2、通过 startTransaction 获得 transactiondb.startTransaction(),开启一个新的事务,之后即可进行 CRUD 操作;db.startTransaction().transaction.commit(),提交事务保存数据,在提交之前事务中的变更的数据对外是不可见的;db.startTransaction().rollback(),事务终止并回滚事务,例如,一部分数据更新失败,对已修改过的数据也进行回滚。const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const db = cloud.database({ throwOnNotFound: false, }); const _ = db.command; exports.main = async (event) => { try { const transaction = await db.startTransaction(); const aaaRes = await transaction.collection("account").doc("aaa").get(); const bbbRes = await transaction.collection("account").doc("bbb").get(); if (aaaRes.data && bbbRes.data) { const updateAAARes = await transaction .collection("account") .doc("aaa") .update({ data: { amount: _.inc(-10), }, }); const updateBBBRes = await transaction .collection("account") .doc("bbb") .update({ data: { amount: _.inc(10), }, }); await transaction.commit(); return { success: true, aaaAccount: aaaRes.data.amount - 10, }; } else { await transaction.rollback(); return { success: false, error: `rollback`, rollbackCode: -100, }; } } catch (e) { console.error(`事务报错`, e); } }; 也就是说对于多用户同时操作(主要是写)数据库的并发处理问题,我们不仅可以使用原子更新,还可以使用事务。其中原子更新主要用户操作单个记录内的字段或单个记录里内嵌的数组对象里的字段,而事务则主要是用于跨记录和跨集合的处理。
2021-09-10 - 云开发数据库设计指导
在我们要构建一个项目(应用程序)时,通常第一件事情就要设计数据库。与关系型数据库将数据存储在固定的表格(这些表格由行和列组成)里所不同的是,云开发的数据库使用结构化的文档来存储数据,不再是关系型数据库里每个行列交汇处都必须有且只有一个值,它可以是一个数组、一个对象,或者更加复杂的嵌套。 12.3.1 数据库的设计1、设计数据库需要预先思考哪些问题实现云开发数据库之前,需要了解存储的数据的性质,如何存储这些数据,以及将如何访问它们,这需要你预先就要做出决定,进而通过组织数据和页面数据交互来获得最佳性能。具体地说,你需要先预先思考如下问题: 页面交互需要使用哪些基本对象,时间、地址、价格、富文本、图片、商品属性等等这些实际的信息在数据库里对应的是啥数据类型?不同对象类型之间的关系是一对一、一对多还是多对多?如商品分类、详情页、评论页、购物车、会员信息、用户信息、配送地址等等这些复杂的关系是怎么联系起来的?云数据库添加新对象的频率有多高?集合里添加记录的频次是怎样的?往内嵌文档里添加字段的值的频率又是怎样的?修改记录以及记录里的字段的值的频率有多高?从数据库中删除记录或记录里的字段的频率有多高?根据条件查询数据库的频率有多高?是查询记录列表,还是记录里某个字段的值?查询记录或记录的值你将打算通过什么方式,是通过 ID、字段、条件还是其他方式?创建的集合哪个是最重要的?哪个会放在首页?哪个集合用户访问并发量会比较大?并发量大的集合应该怎么设计才能提升性能?哪些操作对数据的一致性要求比较高,需要进行原子操作或事务操作?(后面的原子操作和事务会介绍)哪个集合或哪个集合的记录的数据会增长比较快,数据量会比较大?哪个集合或哪个集合的记录会随着业务的发展,字段会有很大的调整? 2、功能的背后也是数据库的设计应用程序复杂业务功能的背后,都是简单的数据,在设计数据库的时候要清楚的知道哪些功能会执行什么样的数据操作,集合与集合、集合与字段之间有着什么关系。 比如新闻应用都会有文章列表以及文章详情页,这是两个功能,文章列表强调查询的是符合条件的记录;而文章详情页则是单个记录下的字段;这两者之间有什么差异?比如用户除了有个人信息之外还有身份读者与作者,读者与作者的身份是怎么体现的?管理员、编辑等人的角色呢?不同的角色在处理数据上又有哪些不同?比如用户的点赞、收藏、评论等这些是应该放到用户的集合里,还是应该放到文章的集合里,或者是单独拿出一个集合来存储这些数据?选择这个方式的依据是什么?前端通过表单增删改查数据在数据库里是怎么体现的?浏览页面、上拉下滑、搜索、轮播、菜单等在数据库是怎么体现的?文件上传、图片下载、地图数据获取、服务器时间等 API 是怎么与数据库结合的? 12.3.2 反范式化与范式化设计范式化(normalization) 是将数据像关系型数据库一样分散到不同的集合里,而不同的集合之间是可以通过唯一的 ID 来相互引用数据的。不过要引用这些数据往往需要进行多次查询或使用 lookup 进行联表查询。 而 反范式化(denormalization) 则是将文档所需的数据都嵌入到文档的内部,如果要更新数据,可能整个文档都要查出来,修改之后再存储到数据库里,如果没有更新指令这种可以进行字段级别的更新,大文档要新增字段性能会比较低下。反观范式化设计,由于集合比较分散,也就比较小,更新数据时可以只更新一个相对较小的文档。 数据既可以内嵌(反范式化),也可以采用引用(范式化),两种策略并没有优劣之分,也都有各自的优缺点,关键是要选择适合自己应用场景的方案。完全反范式化的设计(将文档所需要的所有数据都嵌入到一个文档里面)可以大大减少文档查询的次数。如果数据更新更频繁那么范式化的设计是一个比较好的选择,而如果数据查询更频繁,而不需要怎么更新,那就没有必要把数据分散到不同的集合而牺牲查询的效率。对于复杂的应用比如博客系统、商城系统,只用一个集合(完全反范式化设计)会导致集合过大,冗余数据更多,数据写入性能差等问题,这时候就需要进行一定的范式化设计,也就是用更多的集合,而不是更大的集合。 [图片] [图片] 像云开发数据库这种非关系型数据库,它的存储单位是文档,而文档的字段是可以嵌套数组和对象的,这种内嵌的方式把非关系型数据库的表与表之间的关系嵌套在了一个文档里,也就减少了需要跨集合操作的关联关系。 12.3.3 内嵌文档(内嵌数组或对象)在前面我们了解到云开发数据库的一个文档里可以内嵌非常多的数据,甚至做到一个完整的应用只需一个集合。比如一个用户,只有一个购物车在关系型数据库里,我们需要建两张表来存储数据,一张表是存储所有客户信息的用户列表 User,还有一张存储所有用户订单的订单列表 Order,但是云开发数据库可以将原本的多张表内嵌成一张表。 { "name": "小明", "age": 27, "address":"深圳南山腾讯大厦", "orders": [{ "item":"苹果", "price":15, "number":3 },{ "item":"火龙果", "price":18, "number":4 }] } } 采用这个内嵌式的设计模型,当我们要查询一个用户的信息和他的所有订单时,就可以只通过一次查询做到将用户的信息、所有的订单都获取到,而不像关系型数据库需要先在 User 表里查用户的信息,再根据用户的 id 去查所有订单。 同样一篇文章会有 N 个用户去评论产生 N 条评论数据,而这 N 条评论是只属于这一篇文章的,不存在评论既属于 A 文章,又属于 B 文章的情况。这种我们还是可以采用反范式化设计,将与该文章相关的评论都嵌入到这篇文章里: { "title": "为什么要学习云开发", "content": "云开发是腾讯云为移动开发者提供的一站式后端云服务", "comments": [{ "name": "李东bbsky", "created_on": "2020-03-21T10:01:22Z", "comment": "云开发是微信生态下的最推荐的后台技术解决方案" }, { "name": "小明", "created_on": "2020-03-21T11:01:22Z", "comment": "云开发学起来太简单啦" }] } 在我们要进入文章的详情页时,除了需要获取文章的信息,还要一次性把评论都读取出来,这种反范式化内嵌文档就能做到,也就是可以通过一次查询就能获取到所有需要的数据。但是如果文章都是属于大 V 一样的热点,经常会有几千条几万条的评论,将所有的评论都内嵌到文章记录里可能会存在记录溢出(比如超过 16M)、增删改查效率也会下降,这个时候就不适合用内嵌的方式,而是引用。 12.3.4 引用文档有时候数据与数据之间的关系会比较复杂,不再是一对一或者一对多的关系,比如共享协作时,一个用户可以发 N 个文档,而一个文档又有 N 个作者(用户),这种 N 对 N 的复杂关系,使用内嵌文档就不那么好处理了。 试想一下如果你只创建一个用户表,把 A 所参与编辑的文档都内嵌到相应记录的字段里,B 用户的也是,如果 A,B 用户都参与编辑过同一份文档,那么一份文档就被内嵌到了连个用户的记录了,如果这个文档有 N 个作者,就会被重复内嵌 N 次。如果我们只需要查用户编辑过哪些文档,这种方式就没有问题,但是如果要查一份文档被多少个作者编辑过,就比较困难了;如果文档更新比较频繁,那操作起来就更加复杂了,这时内嵌文档显然不合适,应该采用范式化的设计。 比如我们将用户存储到 user 集合里,将所有的文档存储到 file 集合里,集合与集合的会通过唯一的[代码]_id[代码]来连接,下面 user 集合主要存储用户的信息,而把需要引用的 files 集合记录的[代码]_id[代码]也写到 user 集合里, { "_id": "author10001", "name": "小云", "male":"female", "file": ["file200001","file200002","file200003"] } { "_id": "author10002", "male":"male", "name": "小开", "books": ["file200001","file200004"] } 而在 files 集合里,则存储所有文档的信息,在 files 集合里只需要有 user 集合引用的[代码]_id[代码]即可: { "_id": "file200001", "title": "云开发实战指南.pdf", "categories": "PDF文档", "size":"16M" } { "_id": "file200002", "title": "云数据库性能优化.doc", "categories": "Word文档", "size":"2M" } { "_id": "file200003", "title": "云开发入门指南.doc", "categories": "Word文档", "size":"4M" } { "_id": "file200004", "title": "云函数实战.doc", "categories": "Word文档", "size":"4M" } 如果我们想一次性查询用户参与编辑了哪些文件以及相应的文件信息,可以在云函数端使用聚合的 lookup,这样相当于两个集合整合到一个集合里面了。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const db = cloud.database(); const _ = db.command; const $ = db.command.aggregate; exports.main = async (event, context) => { const res = db .collection("user") .aggregate() .lookup({ from: "files", localField: "file", foreignField: "_id", as: "bookList", }) .end(); return res; }; 而如果我们要修改某个指定文档的信息,直接根据 files 集合的_id 来查询就可以了。文档更新一次,所有参与编辑该文档的信息都会更新,保证了文件内容的一致性。 值得一提的是,尽管我们将复杂的关系通过范式化设计把数据分散到了不同的集合,但是和关系型数据库、Excel 一个字段一列还是不一样,我们还是可以把关系不那么复杂的数据用数组、对象的方式内嵌。如果每个用户参与编辑的文档特别多而每个文档参与共同编辑的用户又相对比较少,把 file 都内嵌到 user 集合里就比较耗性能了,这时候可以反过来,把 user 的 id 嵌入 files 集合里,所以数据库的设计与实际业务有着很大的关系。 //由于file数组过大,user集合不再内嵌file了 { "_id": "author10001", "name": "小云", "male":"female", } //把用户的id嵌入到files集合里,相当于以文档为主,作者为辅 { "_id": "file200001", "title": "云开发实战指南.pdf", "categories": "PDF文档", "size":"16M", "author":["author10001","author10002","author10003"] } 这里再说明一下,跨表查询和联表查询是两码事,跨表查询我们可以通过集合与集合之间有关联的字段(意义相同的字段)多次查询来查找结果;而联表查询则是通过关联的字段将多个集合的数据整列整列的合并到一起处理。如果你不需要返回跨集合的整列数据,就不建议用联表查询,更不要妄图联 N 张表,能跨表查询就跨表查询。 12.3.5 数据库设计的注意事项1、数据库的数据模式云开发数据库的数据模式比较灵活,关系型数据库要求你在插入数据之前必须先定义好一个表的模式结构,而云数据库的集合 collection 则并不限制记录 document 结构。关系型数据库对有什么字段、字段是什么类型、长度为多少等等,而云数据库既不需要预先定义,而且记录的结构也没有限制,同一个集合的记录的字段可以有很大的差异。 这种灵活性让对象和数据库文档之间的映射变得很容易。即使数据记录之间有很大的变化,每个文档也可以很好的映射到各条不同的记录。当然在实际使用中,同一个集合中的文档最好都有一个类似的结构(相同的字段、相同的内嵌文档结构)方便进行批量的增删改查以及进行聚合等操作。 随着应用程序使用时间的增长和需求变化,数据库的数据模式可能也需要相应地增长和改变。最简单的方式就是在原有的数据模式基础之上进行添加字段,这样就能保证数据库支持所有旧版的模式。比如用户信息表,由于业务需要需要增加一些字段,比如性别、年龄,云数据库可以很轻松添加,但是这会出现一些问题,就是以往收集的用户信息性别、年龄这些字段是空的,而只有新添加的用户才有。如果业务的数据变动比较大,文档的数据模式也会存在版本混乱的冲突,这个在数据库设计之初也是要思考的。 2、预填充数据如果已经知道未来要用到哪些字段,在第一次插入的时候就将这些字段预填充了,以后用到的时候就可以使用更新指令进行字段级别的更新,而不再需要再给集合来新增字段,这样的效率就会高很多。 { "_id":"user20200001", "nickname": "小明", "age": 27, "address":"", "school":[{ "middle":"" },{ "college":"" }] } 比如简历网站的用户信息表的 address、school,用户登录的时候不必填,但是投递简历前这些信息必填,如果没有预先设置这些字段,收集这些信息时就需要使用 doc 对文档进行记录级别的更新。 db.collection("user") .doc("user20200001") .update({ data: { address: "深圳", school: [ { middle: "华中一附中", }, { college: "清华大学", }, ], }, }); 但是如果预先设置了这些字段,就是使用更新操作符进行字段级别的更新,当集合越大,修改的内容又比较少时,使用更新操作符来更新文档,性能会大大提升。 db.collection("user") .doc("user20200001") .update({ data: { address: _.set("深圳"), "school.0.middle": _.set("华中一附中"), "school.1.college": _.set("清华"), }, }); 3、考虑文档的增长采用内嵌文档这种反范式化设计在查询时是有很大的好处的,但是有一些文档的更新操作,会在内嵌文档的数组里增加元素或者增加一个新字段,如果随着业务的需求这类操作导致文档的大小变大,比如我们为了方便把评论内置到内嵌文档里,早期这样的设计是没有问题的,但是如果评论常年累积的增加会导致内嵌文档过大,越是往后新增的评论会越是影响性能,而且云数据库的一个记录的上限是 16M。如果出现这种数据增长的情况,也会影响到反范式化的设计模式,那么你可能要重新设计下数据模型,在不同文档之间使用引用的方式而非内嵌的数据结构。 由于更新指令不仅可以对数据进行字段级别的微操(增删改),而且还是原子操作,因此它不仅性能优异还支持高并发。更值得一提的是,通过反范式化设计内嵌文档的方式,更新指令的原子操作可以替代一部分事务的功能,这个在原子操作和事务章节会有介绍。
2021-09-10 - 云开发基础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 - 云开发入门
重磅打造的小程序学习路径课,从微信小程序到微信云开发体系化的学习,带来更加顺畅的学习体验。
2021-11-19 - 企业微信 jssdk invalid url domain
企业微信内部应用H5,在微信开发者工具中调用js sdk config,报错错误码:40048,invalid url domain 应用管理里面域名配置是已经成功 [图片]
2020-10-28 - 同一页面存在多个video时,video无法正常播放一直在加载转圈
不建议同个页面使用多个video组件,建议不超过3个video,如果要实现video列表功能,请进行优化(image列表,选中时将image替换成video)
2019-08-29 - 新能力解读:页面间通信接口
在 2019 年 7 月 2 日的小程序基础库版本更新 v2.7.3 中,小程序新增了一个页面间通讯的接口,帮助我们的小程序完成不同页面间数据同步的功能。 页面间通信接口能干嘛? 在 v2.7.3 之前,小程序不同页面间的大批量数据传递主要有两种: 借助诸如 Mobx 、Redux 等工具,来实现不同页面间的数据传递。 借助小程序提供的 storage ,来完成在不同页面间数据的同步。 前者需要引入一些第三方工具库,从而提升了整个应用的大小,同时,引入的工具也带来了学习生本。而后者则是基于小程序提供的存储,先将数据存入存储,再到另外一个页面去读取,如果数据涉及到了多个页面,则可能会导致数据的紊乱。 新的页面间通信接口则直接解决了上述的两个问题,你可以直接使用 API 在两个页面之间传递数据,再也无需担心数据的紊乱。 新增的页面间通信接口应当如何使用? 关于页面间通信接口的使用非常简单。 这里,我们假设存在 A 和 B 两个页面,其中 A 是首页,B是详情页。 A 向 B 传递数据 如果你需要从首页向详情页传递数据,则可以这样操作。 在页面 A 执行代码 [代码]wx.navigateTo({ url: 'test?id=1' success: function(res) { // 通过eventChannel向被打开页面传送数据 res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' }) } }) [代码] 这样,当 A 跳转到 B 时,就会出发 B 当中定义的 acceptDataFromOpenerPage,并将后续的数据传递过去。 在 B 中,你可以在 onLoad 去定义 eventChannel 的相关方法 [代码]Page({ onLoad: function(option){ // 监听acceptDataFromOpenerPage事件,获取上一页面通过eventChannel传送到当前页面的数据 let eventChannel = this.getOpenerEventChannel(); eventChannel.on('acceptDataFromOpenerPage', function(data) { console.log(data) }) } }) [代码] B 向 A 传递数据 如果需要被打开的页面向打开的页面传递数据,则可以使用如下代码: 在 A 中的跳转时,加入 events 的定义,定义你自己的函数,以及对应的处理函数。 [代码]wx.navigateTo({ url: 'test?id=1', events: { someEvent: function(data) { console.log(data) } }, }) [代码] 然后在 B 中,调用如下代码来发信息 [代码]Page({ onLoad: function(option){ const eventChannel = this.getOpenerEventChannel() eventChannel.emit('someEvent', {data: 'test'}); } }) [代码] 这样,就可以在 B 页面将数据传回到 A 页面了。 页面间通信接口使用注意事项? 在使用页面间通信接口时需要注意两点: 该功能从基础库 2.7.3 开始支持,低版本需做兼容处理。
2019-09-21 - onBluetoothDeviceFound在部分安卓手机,没办法回调
终端类型:微信安卓客户端 手机机型:华为STK-AL00 安卓系统版本:9 微信版本:7.0.14 基础库版本:2.11.0 用户微信:ceyn-2014 操作路径:openBluetoothAdapter执行成功->startBluetoothDevicesDiscovery执行成功->onBluetoothDeviceFound没有触发回调。 已采取措施:打开gps,打开位置权限,已上传日志,微信号:ceyn-2014 麻烦尽快回复问题,谢谢!
2020-05-15 - 开发版本0421,开发者工具骨架屏功能建议?
这边尝试使用骨架屏后在本地生成了两个文件。 [图片] 这两个文件能否新建一个文件夹 并且使用template标签包起来。我们这边直接用template标签就好了 [图片] 引用的时候就好了 [图片]
2020-04-22 - 云开发,获取微信运动步数为空?
[图片] 如题,用云函数获取微信运动步数,有个别用户拿不到weRunData数据,该用户用过多台手机都不行,但是我添加他为测试账号打开却又可以成功获取 我怀疑是小程序官方的bug,上图是高级日志查询,第一个红框是该用户请求云函数时的event数据,里面根本没有weRunData字段,第二个是其他用户请求云函数时的event数据是有weRunData字段的。 有遇到相关问题的小伙伴麻烦指点一下,获取小程序官方麻烦核实一下是否有其他问题
2020-02-02 - 云开发,获得的日期怎么能成为北京时间的日期?
获得日期的代码如下: [图片] 但获得的日期差了一天,应该是东八区8小时时差造成的,怎么消除这种差异? [图片]
2019-09-25 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27