- 一直提示“企业信息或法定代表人信息不一致”。法定代表人验证失败。验证了好几次都不行,该怎么办啊?
最近在办理个体工商户开通微信公众号,在进行微信认证时,走到法定代表人验证这一步时,总是提示:工商数据返回:“企业信息或法定代表人信息不一致”。法定代表人验证失败。请刷新重试。2020年09月18日注册的营业执照,国家企业信用信息公示系统已经可以查到了。公众号ID:gh_ea73fcc38a9c
2020-09-23 - 手机怎么操作个体户小商店?
如题,手机怎么登陆小商店呢,个体户的。总不能一直用电脑端登陆操作上架下架之类的。例如抖音,手机端就是抖店可以操作。微信小商店怎么弄呢 [图片]
2020-10-16 - 餐饮行业小程序的应用及模式思考
为什么大型餐饮企业做小程序,都是先从点餐服务做起? 做小程序,应该关注哪些数据? 小程序里怎么做外卖? 我们更新了小程序餐饮行业解决方案,希望跟大家一起交流学习。 [视频]
2020-01-16 - 小商店新能力介绍
一、个人店支持电脑端登录管理后台拥有个人店的用户现在可以在电脑上登录管理自己的小商店了。 1、个人店怎么登录电脑端? 前往电脑端打开网页:https://shop.weixin.qq.com,使用手机微信扫一扫页面上的二维码,选择自己的小商店以登录个人店管理后台; [图片] 2、个人店商家可以在电脑端后台进行订单、物流管理、售后等基础功能操作,也支持店铺直播、发放优惠券、店铺分类、批量发货等新功能,更好地助力店铺经营(各功能模块的使用教程可前往商家成长中心查阅); [图片] 二、服务市场商家还可通过服务市场满足个性化经营需求,获得一键搬家、商品批量管理、批量打单等服务(10月20日前,服务市场所有服务将限时免费)。 [图片] 三、新增支持抢购功能商家在手机端登录店铺管理后台,可设置商品抢购活动(该功能暂时只支持个人店,企业店能力将后续上线); 1、通过手机端「小商店助手」登录到店铺管理后台,在首页找到「营销工具 — 限时抢购」; [图片] 2、点击「限时抢购 — 立即创建」进入抢购设置页,设置活动的开始和结束时间,并添加参与抢购的商品; [图片] 3、设置抢购价格和抢购数量,点击创建; [图片] 4、创建完成后即可将该抢购商品分享给客户; [图片] 5、可在限时抢购首页对所有进行中和已结束的抢购活动进行管理,向左滑动可结束或删除对应的抢购活动; [图片] 四、下单后改价商家可修改「待付款订单」的价格(目前仅支持调低价格)、减免运费。 1、用户拍下商品但未付款前,商家可以在订单管理中找到对应订单并修改订单价格(可直接输入修改后的价格或折扣,系统会自动计算)及选择是否减免运费,设置完成后点击确定; [图片] 2、用户微信上会收到订单价格已修改的服务通知,点击后可按修改后的价格进行付款; [图片] 五、商家可主动联系用户用户下单及发起售后/投诉时,商家可通过「联系买家」功能主动联系用户(该功能暂时只支持移动端,电脑端能力将后续上线) [图片] 更多资讯,欢迎到【交流专区】微信小商店主页发帖和寻找答案。
2020-10-30 - 云开发快速入门
如果你想免费、快速的开发出一个完整的项目,用小程序的云开发可能是最好的选择。小程序的云开发所用到的主要是前端开发的知识。 注册微信小程序 小程序的注册非常方便,打开小程序注册页面,按照要求填入个人的信息,验证邮箱和手机号,扫描二维码绑定你的微信号即可,3 分钟左右的时间即可搞定。 注册页面:小程序注册页面 https://mp.weixin.qq.com/ 注册小程序时不能使用注册过微信公众号、微信开放平台的邮箱哦,也就是需要你使用一个其他邮箱才行。 当我们注册成功后,就可以自动登入到小程序的后台管理页面啦,如果你不小心关掉了后台页面,也可以点击小程序后台管理登录页进行登录。 后台管理页:小程序后台管理登录页 https://mp.weixin.qq.com/ 小程序和微信公众号的登录页都是同一个页面,他们会根据你的不同的注册邮箱来进行跳转。 进入到小程序的后台管理页后,点击左侧菜单的开发进入设置页,然后再点击开发设置,在开发者 ID 里就可以看到 AppID(小程序 ID),这个待会我们有用。 注意小程序的ID(AppID)不是你注册的邮箱和用户名,你需要到后台查看才行哦~ 必备工具与云开发文档 大家可以根据自己的电脑操作系统来下载相应的版本,注意要选择稳定版 Stable Build 的开发者工具。 开发者工具:小程序开发者工具下载地址 和学习任何编程一样,官方技术文档都是最值得阅读的参考资料。技术文档大家先只需要花五分钟左右的时间了解大致的结构即可,先按照我们的教学步骤学完之后再来看也不迟哦。 技术文档:云开发官方文档 由于小程序的云开发在不断新增功能,更新非常频繁,所以要确保自己的开发者工具是最新的哦(不然会报很多奇奇怪怪的错误),比如你之前下载过要先同步到最新才行~ 体验云开发模板小程序 安装完开发者工具之后,我们使用微信扫码登录开发者工具,然后使用开发者工具新建一个小程序的项目: 项目名称:这个可以根据自己的需要任意填写;目录:大家可以先在电脑上新建一个空文件夹,然后选择它;AppID:就是之前我们找到的 AppID(小程序 ID)(也可以下拉选择 AppID)开发模式为小程序(默认),后端服务选择小程序·云开发点击新建确认之后就能在开发者工具的模拟器里看到云开发 QuickStart 小程序,在编辑器里看到这个小程序的源代码。 接下来,我们点击开发者工具的工具栏里的预览图标,就会弹出一个二维码,使用微信扫描这个二维码就能在手机里看到这个小程序啦。 如果你没有使用微信登录开发者工具,以及你的微信不是该小程序的开发者是没法预览的哦。 在手机里(或模拟器)操作这个小程序,把小程序里的每个按键都点一遍,看看会有什么反应。我们会发现很多地方都会显示“调用失败”等,这非常正常,我们接下来会通过一系列的操作让小程序不报错。 开通云开发服务 点击微信开发者工具的“云开发”图标,在弹出框里点击“开通”,同意协议后,会弹出创建环境的对话框。这时会要求你输入环境名称和环境 ID,以及当前云开发的基础环境配额(基础配额免费,而且足够你使用哦)。 建议你环境名称可以使用 xly、环境ID自动生成即可,当你的云开发环境出现问题的时候,你可以提供你的环境ID,云开发团队会有专人为你解答。 按照对话框提示的要求填写完之后,点击创建,会初始化环境,环境初始化成功后会自动弹出云开发控制台,这样我们的云开发服务就开通啦。大家可以花两分钟左右的时间熟悉一下云开发控制台的界面。 找到云开发的环境 ID 点击云开发控制台窗口里的设置图标,在环境变量的标签页找到环境名称和环境 ID。 当云开发服务开通后,我们可以在小程序源代码 cloudfunctions 文件夹名看到你的环境名称。如果在 cloudfunctions 文件夹名显示的不是环境名称,而是“未指定环境”,可以鼠标右键该文件夹,选择“更多设置”,然后再点击“设置”小图标,选择环境并确定。 指定小程序的云开发环境 在开发者工具中打开源代码文件夹 miniprogram 里的 app.js,找到如下代码: wx.cloud.init({ // 此处请填入环境 ID, 环境 ID 可打开云控制台查看 env: 'my-env-id', traceUser: true, }) 在 env: 'my-env-id'处改成你的环境 ID,如 env: 'xly-snoop' 下载 Nodejs NodeJS 是在服务端运行 JavaScript 的运行环境,云开发所使用的服务端环境就是 NodeJS。npm 是 Node 包管理器,通过 npm,我们可以非常方便的安装云开发所需要的依赖包。 npm是前端开发必不可少的包(模块)管理器,它的主要功能就是来管理包package,包括安装、卸载、更新、查看、搜索、发布等,其他编程语言也有类似的包管理器,比如Python的pip,PHP的composer、Java的maven。我们可以把包管理器看成是windows的软件管理中心或手机的应用中心,只是它们用的是可视化界面,包管理器用的是命令行Command Line。 下载地址:Nodejs 下载地址 大家可以根据电脑的操作系统下载相应的 NodeJS 安装包并安装(安装时不要修改安装目录,啥也别管直接 next 安装即可)。打开电脑终端(Windows 电脑为 cmd 命令提示符,Mac 电脑为终端 Terminal),然后逐行输入并按 Enter 执行下面的代码: node --version npm --version 如果显示为 v10.15.0 以及 6.11.3(可能你的版本号会有所不同),表示你的 Nodejs 环境已经安装成功。 学编程要仔细,一个字母,一个单词,一个标点符号都不要出错哦。注意输上面的命令时node、npm的后面有一个空格,而且是两个短横杠–。 部署并上传云函数 部署并上传云函数 cloudfuntions 文件夹图标里有朵小云,表示这就是云函数根目录。展开 cloudfunctions,我们可以看到里面有 login、openapi、callback、echo 文件夹,这些就是云函数目录。而 miniprogram 文件夹则放置的是小程序的页面文件。 cloudfunctions里放的是云函数,miniprogram放的是小程序的页面,这并不是一成不变的,也就是说你也可以修改这些文件夹的名称,这取决于项目配置文件project.config.json里的如下配置项: "miniprogramRoot": "miniprogram/", "cloudfunctionRoot": "cloudfunctions/", 但是你最好是让放小程序页面的文件夹以及放云函数的文件夹处于平级关系且都在项目的根目录下,便于管理。 使用鼠标右键其中的一个云函数目录比如 login,在右键菜单中选择在终端中打开,打开后在终端中输入以下代码并按 Enter 回车执行: npm install 如果显示“npm不是内部或外部命令”,你需要关闭微信开发者工具启动的终端,而是重新打开一个终端窗口,并在里面输入 cd /D 你的云函数目录进入云函数目录,比如 cd /D C:\download\tcb-project\cloudfunctions\login进入login的云函数目录,然后再来执行npm install命令。 这时候会下载云函数的依赖模块,下载完成后,再右键 login 云函数目录,点击“创建并部署:所有文件”,这时会把本地的云函数上传到云端,上传成功后在 login 云函数目录图标会变成一朵小云。 在开发者工具的工具栏上点击“云开发”图标会打开云开发控制台,在云开发控制台点击云函数图标,就能在云函数列表里看到我们上传好的“login”云函数啦。 上传所有云函数 接下来我们按照这样的流程把其他所有云函数(如 openapi)都部署都上传,也就是要执行和上面相同的步骤,总结如下: 右键云函数目录,选择在终端中打开,输入 npm install命令下载依赖文件; 然后再右键云函数目录,点击“创建并部署:所有文件” 在云开发控制台–云函数–云函数列表查看云函数是否部署成功。 login、openapi、echo、callback这些云函数在后面都会用到的哦,一定要确定自己部署上传成功,不然后面会报错的哦。 npm 包管理器与依赖模块 为什么要在云函数目录执行 npm install,而不是其他地方?这是因为 npm install 会下载云函数目录下的配置文件 package.json 里的 dependencies,它表示的是当前云函数需要依赖的模块。package.json 在哪里,就在哪里执行 npm install,没有 package.json,没有 dependencies,就没法下载啊。 执行 npm install 命令下载的依赖模块会放在 node_modules 文件夹里,大家可以在执行了 npm install 命令之后,在电脑里打开查看一下 node_modules 文件夹里下载了哪些模块。 既然 npm install 是下载模块,那它是从哪里下载的呢?就以 wx-server-sdk 为例,我们可以在以下链接看到 wx-server-sdk 的情况: https://www.npmjs.com/package/wx-server-sdk 为什么 package.json 里依赖的是一个模块 wx-server-sdk,但是 node_modules 文件夹里却下载了那么多模块?这是因为 wx-server-sdk 也依赖三个包 tcb-admin-node、protobuf、jstslib,而这三个包又会依赖其他包,子子孙孙的,于是就有了很多模块。 node_modules 文件夹这么大(几十 M~几百 M 都可能),会不会影响小程序的大小?小程序的大小只与 miniprogram 文件夹有关,当你把云函数都部署上传到服务器之后,你把整个 cloudfuntions 文件夹删掉都没有关系。相同的依赖(比如都依赖 wx-server-sdk)一旦部署到云函数之后,你可以选择不上传 node_modules 时,因为已经上传过了。 获取 openid 与云函数 login 当我们把云函数 login 部署上传成功后,就可以在模拟器以及手机(需要重新点击预览图标并扫描二维码)里点击获取 openid 了。 点击获取 openid openid 是小程序用户的唯一标识,也就是每一个小程序用户都有一个唯一的 openid。点击“点击获取 openid”,在用户管理指引页面如果显示“用户 id 获取成功”以及一串字母+数字,那么表示你 login 云函数部署并上传成功啦。如果获取 openid 失败,你则需要解决 login 云函数的部署上传,才能进行下面的步骤哦。 调用云函数的解读 小程序的首页是”pages/index/index”,我们可以从 app.json 的配置项或者模拟器左下角的页面路径可以看出来。在 index.wxml 里有这段代码: <button class="userinfo-nickname" bindtap="onGetOpenid">点击获取 openid</button> 也就是当点击“点击获取 openid”按钮时,会触发 bindtap 绑定的事件处理函数 onGetOpenid,在 index.js 里可以看到 onGetOpenid 事件处理函数(在 index.js 里找到事件处理函数 onGetOpenid 对比理解)调用了 wx.cloud.callFunction()接口(打开技术文档对比理解) 技术文档:调用云函数 wx.cloud.callFunction 调用云函数的方法很简单,只需要填写云函数的名称 name(这里为 login),以及需要传递的参数(这里并没有上传参数),就可以进行调用。在 success 回调函数里添加以下代码打印 res 对象: console.log('调用login云函数返回的res',res) 添加完成之后记得保存代码哦,文件修改没有保存会在标签页有一个小的绿点。可以使用快捷键(同时按)Ctrl和S来保存(Mac电脑为Command和S)。 编译之后,再点击“点击获取 openid”按钮,就能看到完整的 res 对象,res 对象有三个参数: requestID:云函数执行 ID,可用于在云开发控制台查找日志,打开云开发控制台–云函数–日志,可以在这里根据云函数函数名以及 requestID 来筛选查看云函数的调用日志(含返回结果);result:云函数返回的结果,login 云函数返回的结果里包含 appid、event 对象,我们可以通过 res.result.appid 以及 res.result.event 访问它们;errMsg:显示云函数是否调用成功事件处理函数 onGetOpenid 调用云函数成功之后,干了三件事情: 使用 console.log 打印 openid,可以在点击按钮触发云函数在控制台看到该打印结果;把获取到的 appid 赋值给 app.js 文件里的 globalData 全局对象;跳转到 userConsole 页面;而 userConsole 页面就只是从 globalData 里将 openid 取出来通过 setData 渲染到页面。 小任务:你明白为啥wx.cloud.callFunction()是小程序端的API了么?思考一下为啥云开发会有小程序端的API和服务端API的区别?能理解多少是多少,不清楚也没有关系,后面会有更多内容助你理解。 云函数 login 解读 为什么调用云函数 login 返回的 res 的 result 对象里会包含 event 对象、appid、userInfo 这些结果?这就取决于云函数是怎么写的了。使用开发者工具打开 login 云函数(在 cloudfuntions 文件夹里)的 index.js。 exports.main = (event, context) => {} 这是一个箭头函数的写法,其中 event 和 context 是参数。我们将两个打印日志修改为以下代码,相当于备注一下到底打印到哪里去了: console.log('服务端打印的event',event) console.log('服务端打印的context',context) 保存之后,右键点击 index.js 文件,选择云函数增量上传:(更新文件),更新 login 云函数,我们再来点击“点击获取 openid”按钮,打印的结果在哪里呢?在云开发控制台的云函数日志里面(注意不是开发者工具的控制台)。打开云开发控制台–云函数–日志,按函数名筛选,选择 login 云函数,可以看到云函数被调用的日志记录,我们可以在日志里发现: event 对象包含程序用户的 openid 和小程序的 appid,而 openid 就相当于用户的身份证,我们可以根据 openid 获取到用户的昵称、头像等信息(后面会说明);而 context 对象则是云函数的调用信息和运行状态。在返回结果里我们可以看到 return 返回的数据小任务:比较一下云开发控制台的云函数日志打印的结果和开发者工具控制台打印的结果,深入了解 event 对象、context 对象、result 对象与返回结果,这是云函数的比较重要的知识点。云函数的打印日志会显示在云开发控制台的日志里面,这一点非常重要,要多加利用。只要是打印日志,无论是显示在开发者工具控制台还是显示在云开发控制台的就没有不重要的。 getWXContext() getWXContext()API 是云开发服务端的工具类 API,会返回小程序用户的 openid、小程序 appid、小程序用户的 unionid 等。说这么多不如直接打印,在下面添加一行打印信息: const wxContext = cloud.getWXContext() console.log('getWXContext返回的结果',wxContext) 保存之后,右键点击 index.js 文件,选择云函数增量上传:(更新文件),更新 login 云函数,我们再来点击“点击获取 openid”按钮,然后去云开发控制台的云函数日志里看到底返回了什么结果。 技术文档:getWXContext() 对照技术文档来理解返回的结果。 注意小程序用户 unionid只有在开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用才能获得。 return return 语句是终止函数的执行,并返回一个指定的值给函数调用者。这里返回了 4 个值,而前面我们就调用过 login 云函数,就是函数的调用者,所以我们打印的事件处理函数 onGetOpenid 的回调函数的 res 对象正是这个 return 返回的结果。那既然如此,我们在 return 多加一些内容看看,比如我们之前的一些数据结构案例,将 return 函数改为如下代码: let lesson = "云开发技术训练营"; let enname = "CloudBase Camp"; let x = 3, y = 4, z = 5.001, a = -3, b = -4, c = -5; let now = new Date(); return { movie: { name: "霸王别姬", img: "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1910813120.webp", desc: "风华绝代。" }, movielist:["肖申克的救赎", "霸王别姬", "这个杀手不太冷", "阿甘正传", "美丽人生"], charat: lesson.charAt(4), concat: enname.concat(lesson), uppercase: enname.toUpperCase(), abs: Math.abs(b), pow: Math.pow(x, y), sign: Math.sign(a), now: now.toString(), fullyear: now.getFullYear(), date: now.getDate(), day: now.getDay(), hours: now.getHours(), minutes: now.getMinutes(), seconds: now.getSeconds(), time: now.getTime(), event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } 保存之后,右键点击 index.js 文件,选择云函数增量上传:(更新文件),更新 login 云函数,我们再来点击“点击获取 openid”按钮,然后去云开发控制台的云函数日志里看到底返回了什么结果。 这里我们多次反复提及更新了index.js文件之后就要选择云函数增量上传:(更新文件),更新login云函数,希望大家平时的时候注意,这也是小程序云开发服务端和小程序端一个非常大的区别。 新建云函数 鼠标右键 cloudfunctions 云函数根目录,在弹出的窗口选择新建 Node.js 云函数,比如输入 sum,按 Enter 确认后,微信开发者工具会在本地(你的电脑)创建出 sum 云函数目录,同时也会在线上环境中创建出对应的云函数(也就是自动部署好了,可以到云开发控制台云函数列表里看到) 打开 sum 云函数目录下的 index.js,添加 sum:event.a+event.b,到 return 函数里(把多余的内容可以删掉了),然后记得选择云函数增量上传:(更新文件),更新 sum 云函数。 return { sum:event.a+event.b, } 这个 a 和 b 是变量,但是和前面不一样的是,在服务端我们并没有声明 a 和 b 啊,这是因为我们可以在小程序端声明变量。 点击开发者工具模拟器的“快速新建云函数”,会跳转到 addFunction 页面,打开 addFunction.wxml,我们看到测试云函数绑定的是 testFunction 事件处理函数。 <view class="list-item" bindtap="testFunction"> <text>测试云函数</text> </view> 我们去看 addFunction.js 里的 testFunction,看变量 a 和 b 这两个小程序端的变量是怎么和服务端的变量关联起来的,而又是如何把结果渲染到页面的。testFunction 调用云函数 sum 同样是通过 wx.cloud.callFunction,不同的是在 data 里有 a 和 b: data: { a: 1, b: 2 }, data 里填写的是传递给云函数的参数,也就是先把小程序端的参数传递给云函数,然后云函数处理之后再返回 res 对象,我们可以在 success 回调函数里打印 res 对象: console.log("sum云函数返回的对象",res) 编译之后,我们再点击测试云函数,在控制台就能看到打印的结果,res.result.sum 就是 3。直接把 res.result.sum 通过 setData 赋值到 result 就能渲染出数字,那这个 res.result 是什么?JSON.stringify()又是什么? result: JSON.stringify(res.result) 我们可以打印一下 res.result,以及 JSON.stringify(res.result) console.log("res.result是啥", res.result) console.log("JSON.stringify(res.result)是啥", JSON.stringify(res.result)) res.result 是对象,而 JSON.stringify(res.result)是 json 格式, JSON.stringify() 方法是将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串,因为对象如果直接渲染到页面是会显示 [object Object]的。 小任务:将小程序的参数传递给云端,有没有一点wx.request的感觉?相当于我们通过云函数写好了一个数据API,然后在小程序端调用。新建一个云函数,把各种数学运算都部署到云端,然后通过传递参数,调用这些算法,并将结果渲染到页面。 体验上传图片 上传图片到云存储 使用模拟器以及手机端点击云开发 QuickStart 小程序的上传图片按钮,选择一张图片并打开,如果在文件存储指引页面显示上传成功和文件的路径以及图片的缩略图,说明你的图片就上传到云开发服务器里啦。 点击云开发控制台的存储图标,就可以进入到存储管理页查看到你之前上传的图片啦,点击该图片名称可以看到这张图片的一些信息,如:文件大小、格式、上传者的 OpenID 以及存储位置、下载地址和 File ID。复制下载地址链接,在浏览器就能查看到这张图片啦。 值得注意的是由于QuickStart小程序将“上传图片”这个按钮上传的所有图片都命名为my-image,所以上传同一格式的图片就会被覆盖掉,也就是无论你上传多少张相同格式的图片,只会在后台里看到最后更新的那张图片。以后我们会教大家怎么修改代码,让图片不会被覆盖。 组件支持 我们可以把下载地址作为图床来使用的,也就是你可以把图片的下载地址放到其他网页图片是可以显示的。云存储的图片还有一个 FileID(既云文件 ID,以 cloud://开头)则只能用于小程序的特定场景,也只有部分组件的部分属性支持,把链接粘贴到浏览器也是打不开的。 技术文档:组件支持 比如我们在 index 页面的 index.wxml 里输入以下代码,在 image 组件的 src 属性里输入你的云存储图片的 FileID,它是可以显示出来的。 <image src="你的图片的FileID"></image> 但是如果你退出登录开发者工具,图片就不会显示,而且还会报错,所以不要把图片的 FileID 当做图床用,FileID 另有它用。 体验云调用之服务端调用 重新点击开发者工具的预览图标,然后用手机扫描二维码,在手机端点击云开发 QuickStart 的云调用里的服务端调用,就可以发送模板消息和获取小程序码。 点击获取小程序码,如果显示调用失败,说明你的 openapi 云函数没有部署成功,需要你先部署成功才行哦。调用成功,就能获取到你的小程序码啦,这个小程序码也会保存到云开发的存储里。 发送模板消息,只能在手机微信里预览测试哦,使用微信开发者工具是发送不了模板消息,而且控制台还会报错 点击发送模板消息,你的微信就会收到一则服务通知,该通知是由你的小程序发出的购买成功通知。这就是微信的模板消息啦,很多微信公众号、小程序都会有这样的功能,使用小程序云开发,我们也可以轻松定制自己的服务通知(后面会教大家如何定制)。 体验前端操作数据库 点击微信开发者工具的云开发图标,打开云开发控制台,点击数据库图标进入到数据库管理页,点击集合名称右侧的+号图标,就可以创建一个数据集合了,这里我们只需要添加一个 counters 的集合(不需添加数据)即可。 在开发者工具的编辑器里展开 miniprogram 文件夹,打开 pages 文件下 databaseGuide 里的 databaseGuide.js 文件,在这里找到 onAdd: function (){}、 onQuery: function (){}、 onCounterInc: function (){}、 onCounterDec: function (){}、 onRemove: function (){}分别选中绿色的代码块,然后同时按快捷键 Ctrl 和/(Mac 电脑的快捷键为 Command 和/),就可以批量取消代码的注释。 //是前端编程语言JavaScript的单行注释,位于 // 这一行的代码都不会执行,我们使用快捷键就是批量取消这些代码的注释,让整段代码生效。之所以显示为绿色,是微信开发者工具为了让我们看得更清晰而做的语法高亮。 前端操作数据库的页面逻辑 以上的函数是在小程序的前端页面来操作数据库,点击开发者工具模拟器云开发 QuickStart 里的前端操作数据库, 在第1步(数据库指引有标注),我们会获取到用户的openid,也就是说你没有获取到openid是没法通过小程序的前端来操作数据库的哦 第2步,需要我们在云开发控制台里的数据库管理页创建一个counters的集合(不需添加数据); 第3步,点击按钮页面的按钮“新增记录”(按钮就在这个页面的第4条与第5条之间,看起来不是那么明显),这时会调用 onAdd方法,往counters集合新增一个记录(之前手动添加有木有很辛苦?),我们可以去云开发控制台的数据库管理页查看一下counters集合是不是有了一条记录;大家可以多点击几下新增记录按钮,然后去云开发控制台看数据库又有什么变化。也就是小程序前端页面通过 onAdd方法,在数据库新增了记录。 第4步,点击按钮查询记录,这时调用的是 onQuery方法就能在小程序里获取到第3步我们存储到数据库里的数据啦 第5步,点击计数器按钮+号和-号,可以更新count的值,点击+号按钮会调用 onCounterInc方法,而点击-号 onCounterDec方法,比如我们点击加号到7,再去数据库管理页查看最新的一条记录(也就是最后一条),它的count由原来的1更新到了7(先点刷新按钮),我们再点击-号按钮到5,再来数据库管理页查看有什么变化变化(先点刷新按钮) 第6步,点击删除记录按钮,会调用 onRemove方法,这时会删掉数据库里最新的记录(也就是第5步里的那一条记录)。 通过实战我们了解到,databaseGuide.js 文件里的 onAdd、 onQuery、 onCounterInc、 onCounterDec、 onRemove 可以实现小程序的前端页面来操作数据库。 这些函数大家可以结合 databaseGuide.js 文件和云开发技术文档关于数据库的内容来理解。(关于前端是如何操作数据库的,我们之后还会深入讲解,这里只需要了解大致的逻辑即可) 在前面JavaScript的章节里我们了解到数据以及数据的存储是非常重要的,而有了数据库,用函数生成的数据能够比缓存存储的更加持久,而且在上面我们实现了对数据进行增(添加)、删(删除)、改(修改、更新)、查(查询并渲染到页面),不仅如此,缓存的容量也比较有限,最多不过10M,而数据库可以存几百G以上,可见它的重要性。 开始一个新的云开发项目 基于云开发 QuickStart 模板小程序 云开发 QuickStart 模板小程序有很多多余的页面,这个我们只需要把 miniprogram 文件夹下的 pages、images、components、style 文件夹里的文件清空,以及 app.json 的 pages 配置项里的页面删除,把 app.wxss 里的样式代码都删掉就是一个全新的开始啦。这是方法之一,也可以使用下面的方法(推荐学习时使用下面的方法)。 基于没有使用云开发的项目改造 当然我们也可以把前面章节没有使用云开发的项目改造成使用云服务,首先在小程序的根目录下新建一个文件夹,比如 cloudfunctions,然后在 project.config.json 添加云函数文件夹的路径配置即可, "cloudfunctionRoot": "cloudfunctions/", 然后新建一个 miniprogram 文件夹,把小程序除了 project.config.json 以外的其他文件,比如 pages、utils、images、app.js、app.json 等文件都放到 miniprogram 文件夹里,再在 project.config.json 添加 miniprogramRoot 配置: "cloudfunctionRoot": "cloudfunctions/", "miniprogramRoot":"miniprogram/", 值得一提的是,云函数部署上传成功,我们就可以一直调用,只要你的小程序的appid以及环境ID没有变,你创建再多的小程序项目,都可以直接调用部署好的云函数,比如前面的login、echo、callback、sum等云函数。也就是说云函数一旦部署成功,它就一直在云端服务器里,哪怕你把小程序本地的云函数都删掉也没有关系。 当新建了并配置了云函数根目录为 cloudfunctions 文件夹之后,云函数根目录里并没有云函数,我们可以右键点击云函数根目录 cloudfunctions 文件夹选择同步云函数列表,可以把所有云端的云函数列表都列举出来(这只是列举了列表),而要修改云函数里面的内容,我们可以右键点击其中的一个云函数目录选择下载云函数即可。 除此之外,我们需要小程序的 app.js 的生命周期函数 onLaunch 里使用 wx.cloud.init()来初始化云开发能力: onLaunch: function () { if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力') } else { wx.cloud.init({ env: '你的环境ID', traceUser: true, }) } }, 云开发能力全局只需要初始化一次即可,这里的 traceUser 属性设置为 true,会将用户访问记录到用户管理中,在云开发控制台的运营分析—用户访问里可以看到访问记录。 基础库与 wx.cloud 在小程序端初始化云开发能力的代码里,涉及到 wx.cloud 以及基础库版本的知识。关于 wx.cloud,我们可以和之前在控制台了解 wx 对象一样,直接在开发者工具的控制台里输入: wx.cloud 来了解对象有哪些属性与方法。我们可以看到有如下方法: CloudID: ƒ () //用于云调用获取开放数据 callFunction: ƒ () //调用云函数 database: ƒ () //获取数据库的引用 deleteFile: ƒ () //从云存储空间删除文件 downloadFile: ƒ () //从云存储空间下载文件 getTempFileURL: ƒ () //用云文件 ID 换取真实链接 init: ƒ () //初始化云开发能力 uploadFile: ƒ () //上传文件至云存储空间 而关于基础库,有三个地方需要注意它的存在,平时开发的时候需要留意开发者工具的project.config.json里有这样一个属性libVersion,这个也可以在开发者工具工具栏右上角的详情里的本地设置里的调试基础库,建议切换到最新,切换后libVersion的值也会修改到切换的版本; 官方文档基础库的更新日志,小程序更新非常频繁,而更新的核心就是基础库:所以基础库更新日志要经常留意每个API,技术文档都会标明它的基础库支持的最低版本,而小程序·云开发 SDK是2.2.3以上的基础库才开始支持的。
2021-09-10 - 云开发能力基础讲解
在上一节,我们大致体验了一下云开发:开通了云开发服务,相当于在云端拥有了一个 Nodejs 的环境,我们可以把云函数部署到云端。通过云开发的能力进行调用云函数、上传图片、操作数据库以及使用小程序的一些开放接口,下面来进一步了解和使用云开发能力,并加强对云端测试、本地调试以及本地 Console 日志打印,云端日志打印的理解。 用编程来写项目,就像是在做一系列精密而复杂的实验,你不能总是劳烦他人帮你解决问题,而是要掌握调试、测试、日志打印等手段来检查每一步操作是否正确,你需要学会查看报错信息,如果不正确问题在哪、出了什么问题,你才能有针对性的去搜索,有针对性的去咨询他人。 本地调试与云端测试 为了能够让大家更加清楚的了解:完整操作一个云函数的流程以及本地调试与云端测试的重要性,我们以长方形的边长(a、b)求周长、面积这个简单的数学公式为例。 第一步:新建云函数 首先我们右键点击云函数根目录(也就是 cloudfunctions 文件夹),选择新建 Nodejs 云函数,函数名为长方形的英文 rectangle,然后打开 index.js,修改 return 里的内容为如下: exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { circum:(event.width+event.height)*2, area:event.width*event.height } } circum 是周长,周长=(宽度 width+高度 height)✖️2;area 是面积,面积=宽度 width✖️ 高度 height,只要我们之后把长方形的参数宽度 width 和高度 height 传递进来(之后我们会来讲怎么传),即可获得长方形的周长和面积。 建好云函数之后,我们右键点击云函数目录,也就是 rectangle 文件夹,选择在终端中打开,使用 npm install 来安装依赖。 npm install 第二步:本地调试云函数是否正确 对于一个复杂的云函数,我们最好是先在本地测试一下云函数是否正确,然后再部署上传到云端。那如何本地测试呢?右键点击云函数目录,也就是 rectangle 文件夹,选择本地调试(这种方式进入本地调试会默认开启 rectangle 的本地调试),修改以下代码: { "key": "value" } 我们给参数宽度 width 和高度 height 赋值(注意传递的是 JSON 格式,最后一个参数结尾不能有逗号,),比如赋值为 3 和 6: { "width": 3, "height":7 } 然后点击调用,如果显示函数执行成功(注意仍然是在调试的 console 标签),并得到周长 circum 和面积 area 的结果分别为 20、21,那证明云函数没有写错,这时候我们就可以部署并上传到云端了。 第三步:云端测试云函数是否正确 打开云开发控制台的云函数标签页,找到 rectangle 云函数,点击云端测试,同样我们给参数赋值,将以下代码进行修改: { "key1": "test value 1", "key2": "test value 2" } 比如给宽度 width 赋值为 4,高度 height 赋值为 7,即代码修改为: { "width": 4, "height": 7 } 然后点击运行测试,(会等一段时间),再来查看测试的结果,如果返回结果如下,则表示在云端的云函数可以正常调用: {"circum":22,"area":28} 在云端测试的调用结果也是可以在云开发控制台云函数的日志里查看到的。 在第一节我们要触发云函数,需要在小程序页面里写一个组件(比如button)并绑定事件处理函数,然后再在事件处理函数(或在页面的生命周期函数)里使用wx.cloud.callFunction()调用云函数,通过这种方式来触发云函数,会比较麻烦,而本地调试和云端测试则可以直接触发云函数查看结果,大大提升了调试的便利度。 云函数的调用采用事件触发模型,小程序端的调用、本地调试和云端测试都会触发云函数的调用事件,其中本地调试调用的不是云端的云函数,而是小程序本地的云函数。 小任务:rectangle云函数需要传入两个参数才能返回值,有些云函数,比如前面的login云函数不需要传入参数,你知道应该怎么进行本地调试和云端测试吗?在本地调试的请求方式有手动触发和模拟器触发,开启模拟器触发,点击第一节“点击获取openid”的按钮试试看(注意这时调用的是本地的云函数,修改一下login云函数不上传试试看); 小程序端与服务端 小程序端与云端的初始化 小程序默认可以创建两个环境,这两个环境都有云函数配置、数据库、云存储且独立隔离,开发上会存在两个环境切换的情况(一个用于生产环境,一个用于测试环境),而区别这两个环境的就是它们的环境 ID,小程序端与云端的初始化时要注意。 在前面我们介绍过小程序的初始化是在 app.js 文件里使用 wx.cloud.init 来初始化,如下: wx.cloud.init({ env: 'my-env-id', //可以填写生产环境或者测试环境的环境ID traceUser: true, }) 这里的 env 只会决定小程序端 API 调用的云环境(如云函数、云存储、数据库,毕竟有两个环境里都有),并不会决定云函数中的 API 调用的环境。在开发者工具的控制台,也会打印默认环境: 当前代码初始化的默认环境为:你的默认环境 ID 当前代码初始化的默认环境为:你的默认环境 ID 云函数中的 API 调用的环境也可以使用初始化来设置。 cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) cloud.DYNAMIC_CURRENT_ENV 设置 API 默认环境等于当前所在环境。建议所有的云函数都使用以上方式来初始化,也就是配置 env 的值为 cloud.DYNAMIC_CURRENT_ENV 或使用你的环境 ID,不要为空。 关于 wx-server-sdk 每一个云函数都会用到 wx-server-sdk 这个 Node 包,而要使用这个包都需要有 Nodejs 环境,小程序端的本地需要我们自己下载 Nodejs(前面已下载),而云端则自带 Nodejs 环境。那这个 wx-server-sdk 到底什么呢?我们可以打开它的 npm 包地址: npm 包地址:wx-server-sdk 包地址 在 Dependencies 标签页可以看到 wx-server-sdk 依赖 tcb-admin-node、protobufjs、tslib 这三个包,而其中 tcb-admin-node 是核心,学有余力的童鞋可以看一下它的技术文档。 技术文档:tcb-admin-node 的 Github 地址 async 与 await 在 wx-server-sdk 中不再兼容 success、fail、complete 回调,只会返回 Promise。在云函数中也经常会需要处理一些异步操作,在异步操作完成后再返回结果给到调用方,我们可以通过在云函数中返回一个 Promise 的方法来实现。Promise 表示异步操作返回的结果。在新建的云函数里会看到下面这样的一个语句(有 async): exports.main = async (event, context) => { } async 表示函数里有异步操作,async 函数的返回值是一个 Promise 对象。在后面还会遇到 await,表示紧跟在它后面的表达式需要等待结果;以及 promise 对象的 then()方法(有点类似于 success 回调函数),和 catch()方法(有点类似于 fail 回调函数),这些我们以后会经常遇到,先理解不了也没有关系,大家在书写时推荐云函数使用上面的写法就对了。 云函数的注意事项 在云函数部署并上传到云端之后,更新里面的文件比如index.js、config.json,建议右键点击更新好的文件(不是云函数目录)选择云函数增量上传:更新文件,不建议通过上传并部署所有文件的方式,否则在几分钟内会出现云函数调用失败的情况; 删除一个云函数之后,不建议再新建一个同名的云函数并上传部署,否则在十多分钟内会出现云函数调用失败的情况,建议换一个云函数名,比如login换成user,在小程序端使用 wx.cloud.callFunction({name: ''})调用云函数时把name的值换成user就可以了 调用云函数时,我们还可以在开发者工具调试面板的NetWork标签查看调用云函数的情况。 获取用户信息和登录 在生命周期章节,我们大致介绍了一下如何使用 wx.getUserInfo API 和通过组件的 open-type=”getUserInfo” 来获取用户的信息(如头像、昵称),下面我们就来详细介绍云开发的免鉴权登录与用户信息的结合。 使用open-type=”getUserInfo” 来获取用户信息的作用和 wx.getUserInfo API基本效果是一样的,区别在于wx.getUserInfo 这种方式最好是在用户允许获取公开信息(也就是res.authSetting[‘scope.userInfo’]的值为true)之后再调用,如果用户拒绝了授权就不会再有弹窗(除非用户删掉了你的小程序再使用),调用就会失败,而使用组件的方式是用户主动点击,用户即使拒绝了,再点击仍会弹出授权弹窗。所以推荐先使用组件来获取用户授权,然后再来使用wx.getUserInfo来获取用户信息。 通过 button 获取用户信息 使用开发者工具新建一个 login 页面,然后在 login.wxml 里输入以下代码,我们通过组件的方式来获取用户的信息: <button open-type="getUserInfo" bindgetuserinfo="getUserInfomation">点击获取用户信息</button> <image src="{{avatarUrl}}"></image> <view>{{city}}</view> <view>{{nickName}}</view> 在 login.js 的 data 里初始化 avatarUrl、nickName 以及 city,没有获取到用户信息时,用一张默认图片代替,昵称显示用户未登录,city 显示为未知: data: { avatarUrl: '/images/user-unlogin.png', nickName:"用户未登陆", city:"未知", }, 然后在 login.js 文件里输入以下代码,在事件处理函数 getUserInfomation 我们可以打印 event 对象,open-type=”getUserInfo”的组件的 event 对象的 detail 里就有 userInfo: getUserInfomation: function (event) { console.log('getUserInfomation打印的事件对象', event) let { avatarUrl, city, nickName}= event.detail.userInfo this.setData({ avatarUrl,city, nickName }) }, 将获取的 avatarUrl,city,nickName 通过 this.setData()赋值给 data。编译之后点击点击获取用户信息按钮,首先会弹出授权弹窗,当用户确认之后,就会显示用户的信息。 获取用户高清头像 我们发现获取到的头像不是很清晰,这是因为默认的头像大小为 132132(UserInfo 用户头像说明),如果把 avatarUrl 链接后面的 132 修改为 0 就能获取到 640640 大小的头像了: getUserInfomation: function (event) { let { avatarUrl, city, nickName}= event.detail.userInfo avatarUrl = avatarUrl.split("/") avatarUrl[avatarUrl.length - 1] = 0; avatarUrl = avatarUrl.join('/'); this.setData({ avatarUrl,city, nickName }) }, 页面加载时就显示用户信息 在获得了用户授权和用户信息的情况下,刷新页面或进行页面跳转,用户的个人信息还是不会显示,这是因为 getUserInfomation 事件处理函数点击组件时才触发,我们需要在页面加载时也能触发获取用户信息才行。 我们可以在 login.js 的 onLoad 生命周期函数里输入以下代码,当用户授权之后来调用 wx.getUserInfo() API: wx.getSetting({ success: res => { if (res.authSetting['scope.userInfo']) { wx.getUserInfo({ success: res => { let { avatarUrl, city, nickName } =res.userInfo this.setData({ avatarUrl, city, nickName }) } }) } } }); 这样当我们加载页面时,用户的信息就能显示出来了,不过这里的头像是从 API 里重新取的,也会不清晰。我们当然可以像之前一样把头像的链接替换一下,但是如果每个页面都这么写就会很麻烦,解决的方法有 2 种,一种是把高清头像存储到缓存里,还有一种是把代码封装成一个组件(大家可以自己研究如何自定义组件了)。 openid、用户信息与登录 尽管我们已经获取到了用户的头像、昵称等信息,但是这不能称之为真正意义的登录,只有获取到了用户身份的唯一 ID 也就是 openid,我们才能把用户行为比如点赞、评论、发布文章、收藏等与用户挂钩,用户这些行为都与数据库有关,而能够确定点赞、评论、文章、收藏这些数据与用户关系的就是 openid,也就是说只要获取到了 openid 就意味着用户已经登录,而获取用户信息(如头像、昵称)不过是一个附加服务,这两个是可以完全独立的。没有 openid,我们也无法把用户信息给存储到数据库,也就没法让用户自定义用户信息。无论是用户行为,还是用户的信息,openid 都是一个重要的桥梁。 通过前面的login云函数,我们就已经可以获取到用户的openid。无需维护复杂的鉴权机制,即可获取天然可信任的用户登录态(openid),是云开发的一个重要优势。无论是云存储还是云数据库,openid都扮演着一个重要的角色。 小程序端上传图片到云存储 要把图片上传到云存储,会使用到 wx.cloud.uploadFile,这个 API 是小程序端的 API,它是把本地资源也就是临时文件里的文件上传到云存储里。在前面《图片、缓存与文件》章节里我们已经了解到如何把图片上传到小程序的临时文件,而要把临时文件上传到云存储,则需要调用 wx.cloud.uploadFile API。 技术文档:wx.cloud.uploadFile 在 wx.cloud.uploadFile 技术文档里,可以看到要调用 API,需要获取图片的 filePath,在小程序里为临时文件的路径,也就是要把上传到小程序的临时文件路径赋值给它;还有一个 cloudPath,这个为文件的云存储路径,这个是我们可以任意设置的。 使用开发者工具在 login.wxml 里添加以下代码,代码和前面章节基本一致,大家也可以回顾一下以前的内容: <button bindtap="chooseImg">选择图片</button> <image mode="widthFix" src="{{imgurl}}"></image> 然后在 login.js 的 data 里初始化 imgurl,这里 imgurl 是一个字符串, data: { imgurl: "", }, 然后在 login.js 里添加事件处理函数 chooseImg,我们再来回顾一下临时文件的知识: chooseImg: function () { wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: function (res) { console.log(res) console.log(res.tempFilePaths) } }) }, 编译后,上传一张图片,在控制台里我们可以看到 res.tempFilePaths 是一个数组格式,而 wx.cloud.uploadFile 的 filePath 是一个字符串,所以我们在上传时,可以把第一张图片的路径(字符串)赋值给 filePath: const filePath = res.tempFilePaths[0] 文件名与后缀的处理 我们知道一个文件由文件名称和文件后缀构成,比如 tcb.jpg 和 cloudbase.png,jpeg 说明图片的格式是 JPG 格式,而 png 说明图片是 PNG 格式,文件名称相同且格式相同就是出现覆盖,如果我们随意更改了文件的后缀,大多数文件就会打不开。所以要把 cloudPath 云存储的路径需要我们把文件名和后缀给处理好。 当我们把图片上传到小程序的临时文件后,我们可以查看一下临时路径是什么样子的: http://tmp/wx7124afdb64d578f5.o6zAJs291xB-a5G1FlXwylqTqNQ4.esN9ygu5Hmyfccd41d052e20322e6f3469de87f662a0.png 临时路径的文件名就不是原来的文件名,会变成一段长字符,但文件的格式还是原来的文件格式(后缀)。那 cloudPath 要输入文件的路径,就需要填写文件名和文件格式,这个要怎么处理呢? 在上一节的 QuickStart 小程序里,文件上传到云存储的处理方式如下: const cloudPath = 'my-image' + filePath.match(/\.[^.]+?$/)[0] 也就是它把上传的所有图片都命名为 my-image,而文件的后缀还是原来的文件后缀(也就是文件格式不变)。这里的 filePath.match(/.[^.]+?$/)[0]是字符串的正则处理,后面我们会来详细了解。我们先可以在开发者工具的控制台输入以下代码了解一下它的功能: const filepath="http://tmp/wx7124afdb64d578f5.o6zAJs291xB-a5G1FlXwylqTqNQ4.esN9ygu5Hmyfccd41d052e20322e6f3469de87f662a0.png" filepath.match(/\.[^.]+?$/)[0] 打印可以得到临时文件的后缀,这里为.png。这种把所有文件都命名为 my-image 的做法,会导致当文件的后缀相同时文件会被覆盖,如果不希望文件被覆盖,我们需要给文件命不同的名字,我们可以这样处理: const cloudPath = `${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/\.[^.]+?$/)[0] 给文件名加上时间戳和一个随机数,时间戳是以毫秒计算,而随机数是以 1000 内的正整数,除非 1 秒钟(1 秒=1000 毫秒)上传几十万张照片,不然文件名是不会重复的。 结合上面的内容,我们可以把 wx.chooseImage()的 success 回调函数如下处理: success: function (res) { const filePath = res.tempFilePaths[0] const cloudPath = `${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/\.[^.]+?$/)[0] wx.cloud.uploadFile({ cloudPath, filePath, success: res => { console.log('上传成功后获得的res:', res) }, }) } 编译之后,我们再次上传一张图片就会打印上传成功之后的 res 对象,里面包含图片在云存储里的 fileID,注意它的文件名和文件后缀,以及我们可以在云开发控制台的存储里找到你上传的图片,也就是说我们上传图片到云存储是无法直接获取到图片的下载地址的。 云存储的二级目录 在存储里我们都是把所有的图片放在根目录下,没有二级目录,那我们能不能建一个二级目录呢?当然是可以的,我们可以在 cloudPath 的前面加一个文件路径就可以了,比如: const cloudPath = `cloudbase/${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/\.[^.]+?$/)[0] 渲染云存储图片到组件 在上一节组件支持部分了解到,我们是可以把 fileID 直接在小程序的某些组件里渲染出来的。综合以上内容 chooseImg 事件处理函数最终为以下代码(注意 this.setData 的 this 指向,这里为了方便把 success 回调都写成了箭头函数): chooseImg: function () { wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: res=> { const filePath = res.tempFilePaths[0] const cloudPath = `cloudbase/${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/\.[^.]+?$/)[0] wx.cloud.uploadFile({ cloudPath, filePath, success: res => { console.log('上传成功后获得的res:', res) const imgurl=res.fileID this.setData({ imgurl }) }, }) } }) }, 在云开发控制台的存储里,我们可以看到每张图片的详细信息都有上传者 Open ID,无论你是使用开发者工具在模拟器的小程序里上传还是预览在手机的小程序里上传,只要你用的是同一个微信账号,这个上传者openid都是一致的,云存储会自动记录上传者的openid。 小任务:结合《图片、缓存与文件》章节里的wx.chooseMessageFile()的知识,将客户端会话(微信聊天窗口)里的视频、音频、PDF、Excel 等也上传到云存储里。 云函数上传图片到云存储 云开发不仅在小程序端可以上传文件到云存储,还可以通过云函数也就是云端上传图片到云存储(这里会涉及到一点 Nodejs 的知识)。 技术文档:uploadFile 注意云函数上传图片的 API 属于服务端 API,与 wx.cloud.uploadFile 是小程序端 API 不同。 使用开发者工具右键点击云函数根目录也就是 cloudfunctions 文件夹,选择新建 Node.js 云函数,云函数的名称命名为 uploadimg,右键点击 uploadimg 文件夹,选择硬盘打开,然后拷贝一张图片如 demo.jpg 进去,文件结构如下: uploadimg云函数目录 ├── index.js ├── package.json ├── demo.jpg 然后打开 index.js,输入以下代码: const cloud = require('wx-server-sdk') const fs = require('fs') const path = require('path') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { const fileStream = fs.createReadStream(path.join(__dirname, 'demo.jpg')) return await cloud.uploadFile({ cloudPath: 'tcbdemo.jpg', fileContent: fileStream, }) } 然后右键点击 uploadimg 文件夹,选择在终端中打开,输入 npm install 安装依赖,再点击 uploadimg 文件夹,选择上传并部署所有文件(这时图片也一并上传到了云端)。 由于云端测试无法获取用户登陆态信息,所以我们不能在云端测试里把图片上传到云存储,需要在小程序端调用,使用开发者工具在 login.wxml 输入以下代码,也就是新建一个绑定 uploadimg 事件处理函数的 button 用于触发: <button bindtap="uploadimg">云函数上传图片</button> 然后在 login.js 里输入以下代码,在事件处理函数 uploadimg 里调用 uploadimg 云函数,并返回调用之后的 res 对象: uploadimg() { wx.cloud.callFunction({ name: 'uploadimg', success: res => { console.log(res) } }) }, 编译之后,点击云函数上传图片按钮,就可以调用 uploadimg 云函数,从而调用 uploadFile API 将服务端/云端的图片上传到云存储里面啦,可以打开云开发控制台的云存储查看是否有 tcbdemo.jpg 这张图片。 注意,通过这种方式上传到云存储的图片,是没有上传者 Open ID的,在云存储里查看这张图片的详细信息,就可以了解到。 调用数据库 数据库的导入 在调用数据库之前,我们需要先有一个比较贴近实际的数据库案例,为此把前面章节用到的知乎日报数据整理出了一个数据库文件。云开发数据库支持用文件的方式导入已有的数据(这里推荐大家使用 json)。 数据库下载:知乎日报文章数据 右键点击链接,将 data.json 存储到电脑。为了方便大家阅读与编辑 data.json 文件的内容,推荐大家使用 Visual Studio Code 编辑器。 代码编辑器:Visual Studio Code 编辑器的汉化与插件:可能你安装的VS Code的界面是英文的,可以参照VSCode设置中文显示,将VS Code汉化。 使用 VS Code 编辑器打开 data.json,发现数据的内容与写法我们都比较熟悉,知识各个记录对象之间是使用回车 \n 分隔,而不是逗号,这一点需要大家注意。 打开云开发控制台,在数据库里新建一个集合 zhihu_daily,导入该 json 文件,导入时会有冲突模式选择,看下面的介绍,推荐大家使用 upsert: Insert:总是插入新记录Upsert:如果记录存在则更新,否则插入新记录导入后,发现数据库自动给每一条数据(记录)都加了唯一的标识_id。 小程序端调用数据库 在小程序端调用数据库的方式很简单,我们可以把下面的代码写到一个事件处理函数里,然后点击组件触发事件处理函数来调用;也可以直接写到页面的生命周期函数里面;还可以把它写到 app.js 小程序的生命周期函数里面。 使用开发者工具,将下面的代码写到 login.js 的 onLoad 函数里面,我们 先使用 wx.cloud.database()获取数据库的引用(相当于连接数据库);再使用 db.collection()获取集合的引用;再通过 Collection.get 来获取集合里的记录.const db = wx.cloud.database() db.collection('zhihu_daily') .get() .then(res => { console.log(res.data) }) .catch(err => { console.error(err) }) 编译之后,就能在控制台看到调用的 20 条数据库记录了(如果没有指定 limit,则默认最多取 20 条记录)。 云函数调用数据库 使用云函数也可以调用数据库,使用开发者工具右键点击云函数根目录也就是 cloudfunctions 文件夹,选择新建 Node.js 云函数,云函数的名称命名为 zhihu_daily,然后打开 index.js,输入以下代码: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async (event, context) => { return await db.collection('zhihu_daily') .get() } 然后右键点击 index.js,选择云函数增量上传:更新文件,我们既可以使用云函数的本地调试(要本地调试需要使用 npm install 安装 wx-server-sdk 依赖),也可以使用云端测试来了解云函数调用数据库的情况。 openid 与数据库 在云开发控制台的数据库标签里,打开上一节内容里的 counters 集合,在这个集合里我们可以看到每条记录除了有_id 字段以外,还有一个_openid 字段用来标志每条记录的创建者(也就是小程序的用户)。但是在我们使用管理端(控制台和云函数)中创建的数据比如我们之前导入的 zhihu_daily,就不会有 _openid 字段,因为这些记录属于管理员(不是用户)创建的记录。 我们可以自定义 _id(也就是给数据添加一个_id 字段并填入任意值),但不可自定义和修改 _openid 。 _openid 是在文档创建时由系统根据小程序用户默认创建的,可以用来标识和定位文档。和云存储一样,数据库的记录也和 openid 有着紧密的联系。
2021-09-10 - 云开发数据库入门讲解
任何一个大型的应用程序和服务,都必须会使用到高性能的数据存储解决方案,用来准确(ACID,原子性 Atomicity、一致性 Consistency、隔离性 Isolation、持久性 Durability,可以拓展了解一下)、快速、可靠地存储和检索用户的账户信息、商品以及商品交易信息、产品数据、资讯文章等等等等,而云开发就自带高性能、高可用、高拓展性且安全的数据库。 云数据库的基础知识在操作数据库时,我们要对数据库database、集合collection、记录doc以及字段field要有一定的了解,首先要记住这些对应的英文单词,当你要操作某个记录doc的字段内容时,就像投送快递一样,要先搞清楚它到底在哪个数据库、在哪个集合、在哪个记录里,一级一级的去找。操作数据库通常都是对数据库、集合、记录、字段进行增、删、改、查,当你清楚了这些,操作数据库就不会迷糊了。 云数据库与 Excel、MySQL 的对应理解我们可以结合 Excel 以及 MySQL(之前没有接触过 MySQL 也没有关系,只看与 Excel 的对应就行)来理解云开发的数据库。 [图片] 集合的创建与数据类型我们现在来创建一个 books 的集合(相当于创建一张 Excel 表),用来存放图书馆里面书籍的信息,比如这样一本书: [图片] 打开云开发控制台的数据库标签,新建集合 books,然后选择该集合,给 books 里添加记录(类似于填写 Excel 含字段的第一行和其中一行关于书的信息记录),依次添加字段: 字段名:title,类型:string,值: JavaScript 权威指南(第 6 版)字段名:author,类型:string,值:弗兰纳根(David Flanagan)字段名:isbn,类型:string,值:9787111376613字段名:publishInfo,类型:object然后我们再在 publishInfo 的下面(二级)添加字段 press,类型为 string,值为:机械工业出版社;year,类型为 number,值为:2012 另外,如需批量添加数据库集合中的记录时,推荐使用云开发数据库自带的“高级操作”,高级操作支持数据库的增删改查以及聚合等操作,可有效提高数据库管理效率。 [图片] 在控制台数据库管理页中可以编写和执行数据库脚本,这些脚本的语法会跟我们之后会学习的 SDK 数据库语法一样,可以作为我们日常“调试”数据库的一种基础方式,可理解为云开发控制台的 console。 技术文档:数据库高级操作 云数据库的导入和导出 除了在集合中添加数据库记录,云开发数据库还支持 json 和 csv 文件的导入和导出。 [图片] json 文件的导入 打开云开发控制台,在数据库里新建一个集合,即可选择导入本地的 json 文件。 注:导入时会有冲突模式选择,看下面的介绍,推荐大家使用 upsert: Insert:总是插入新记录 Upsert:如果记录存在则更新,否则插入新记录 导入后,发现数据库自动给每一条数据(记录)都加了唯一的标识 _id 。 csv 文件的导入 在云开发控制台里新建一个数据库集合,点击“导入”,选中本地的 csv 文件即可导入到集合当中。 本教程中以某年的中国部分城市经济数据为例:创建一个名为“china”的集合,并导入 china.csv 文件,这里我们也准备了一份线上版本供使用。 此处创建的集合,以及导入的数据,在下方章节的学习中会使用到。 数据库的导出 [图片] 点击 云开发控制台-数据库-导出,填写需要导出的字段,即可选择导出 json 和 csv 文件,其中导出 csv 文件时必须填写字段,字段间需以逗号隔开。 数据库的权限控制与安全规则在数据库创建之后,我们需要在云开发控制台-数据库-集合的权限设置标签对数据库进行权限设置。数据库的权限分为小程序端和服务端(云函数、云开发控制台)。服务端拥有读写所有数据的读写权限,所以这里的权限设置只是在设置小程序端的用户对数据库的操作权限。权限控制分简易权限控制和自定义权限(也就是安全规则),建议开发者用安全规则取代简易的权限控制。 技术文档:权限控制 要使用自定义权限(也就是安全规则)来全面取代简易的权限控制,我们需要了解 4 个简易的权限控制所表示的意思,以及安全规则应该如何一一取代它们,也就是我们在配置集合的权限时,不再选择简易的权限控制,而是统一选择自定义权限,填写与之对应的 json 规则即可。 安全规则可以让更加灵活而又明确地自定义前端数据库读写权限的能力,通过配置安全规则,开发者可以精细化的控制集合中所有记录的读read、写write权限。其中write权限还可以细分为create新建、update更新、delete删除等权限,还支持比较、逻辑运算符进行更加精细化的权限配置。 所有用户可读,仅创建者可读写:比如用户发的帖子、评论、文章,这里的创建者是指小程序端的用户,也就是存储 UGC(用户产生内容)的集合要设置为这个权限; { "read": true, "write": "doc._openid == auth.openid" } 仅创建者可读写:比如私密相册,用户的个人信息、订单,也就是只能用户自己读与写,其他人不可读写的数据集合; { "read": "doc._openid == auth.openid", "write": "doc._openid == auth.openid" } 所有人可读:比如资讯文章、商品信息、产品数据等你想让所有人可以看到,但是不能修改的内容; { "read": true, "write": false } 所有用户不可读写:如后台用的不暴露的数据,只能你自己看到和修改的数据; { "read": false, "write": false } 小程序端 API 拥有严格的调用权限控制,比如在小程序端 A 用户是不能修改 B 用户的数据的,没有这样的权限,在小程序端只能修改非敏感且只是针对单个用户的数据;对于有更高安全要求的数据,我们可以在云函数内通过服务端 API 来进行操作。 如果数据库集合里的数据是通过导入的方式获取的,这个集合的权限默认为“仅创建者可读写”,这个权限在服务端(云函数)可以调用,但是在小程序端可能会返回空数组哦,所以一定要记得根据情况修改权限。 小程序端与云函数的服务端无论是在权限方面、API的写法上(有时看起来一样,但是写法不一样),还是在异步处理上(比如服务端不再使用success、fail、complete回调,而是返回Promise对象),都存在非常多的差异,这一点要分清楚。 一窥数据查询的全貌 查询集合 collection 里的记录 查询集合 collection 里的记录是云开发数据库操作最重要的知识,在上文中我们已经将中国城市经济数据 china.csv 的数据导入到了集合 china 之中,并介绍了如何设置集合权限,接下来我们就以此为例来讲解数据库的查询。 [图片] 在中国城市经济数据线上 excel 版以及云开发控制台 china 集合里,我们可以看到中国 332 个城市的名称 city、省份 province、市区面积 city_area、建成区面积 builtup_area、户籍人口 reg_pop、常住人口 resident_pop、GDP 的数据。 查询中国 GDP 在 3000 亿元以上的前 10 个城市,并要求不显示_id 字段,显示城市名、所在省份以及 GDP,并按照 GDP 大小降序排列。 使用开发者工具新建一个 chinadata 页面,然后再在 index.js 的 onLoad 生命周期函数里输入以下代码。操作集合里的数据涉及的知识点非常繁杂,下面的案例相对比较完整,便于大家有一个整体性的理解: const db = wx.cloud.database() //获取数据库的引用 const _ = db.command //获取数据库查询及更新指令 db.collection("china") //获取集合china的引用 .where({ //查询的条件指令where gdp: _.gt(3000) //查询筛选条件,gt表示字段需大于指定值。 }) .field({ //显示哪些字段 _id:false, //默认显示_id,这个隐藏 city: true, province: true, gdp:true }) .orderBy('gdp', 'desc') //排序方式,降序排列 .skip(0) //跳过多少个记录(常用于分页),0表示这里不跳过 .limit(10) //限制显示多少条记录,这里为10 .get() //获取根据查询条件筛选后的集合数据 .then(res => { console.log(res.data) }) .catch(err => { console.error(err) }) 大家可以留意一下数据查询的链式写法, wx.cloud.database().collection('数据库名').where().get().then().catch(),前半部分是数据查询时对对象的引用和方法的调用;后半部分是Promise对象的方法,Promise对象是get的返回值。写的时候为了让结构更加清晰,我们做了换行处理,写在同一行也是可以的。 构建查询条件的 5 个方法在上面的案例中,就包含了构建查询条件的五个方法: Collection.where()、 Collection.field()、 Collection.orderBy()、 Collection.skip()、 Collection.limit(),这五个方法是可以单独拆开使用的,比如只使用 where 或只使用 field、limit,也可以从这 5 个中抽几个组合在一起使用,还可以一次查询里写多个相同的方法,比如 orderBy、where 可以写多次相同的。 不过值得注意的是这5个方法顺序不同查询的结果有时也会有所不同(比如orderBy多次打乱顺序的情况下),查询性能也会有所不同。通常skip最好放在后面,不要让skip略过大量数据。skip().limit()和limit().skip()效果是等价的。构建查询条件的5个方法是基于集合引用Collection的,就拿where来说,不能写成 wx.cloud.database().where(),也不能是 wx.cloud.database().collection("china").doc.where(),只能是 wx.cloud.database().collection("china").where(),也就是只能用于查询集合collection里的记录。 指令查询条件 where,注意在后面我们会介绍的 command 查询指令比如筛选字段大于/小于/不等于某个值的比较指令,同时满足多个筛选条件的逻辑指令等,以及模糊查询的正则都是写在 where 内;技术文档:Collection.where 指定返回哪些字段 field,查询时只需要传入 true|false(或 1|-1)就可以返回或不返回哪些字段,在上面的案例里我们就只返回 city、province、gdp 三个字段的值:技术文档:Collection.field 数据排序 orderBy,排序的语法如下,里面为排序的条件,这里的字段名可不受 field 的限制(不在 field 内,没有显示,但是还是会起作用): orderBy('字段名', '排序方式')。排序方式只支持 desc 降序、asc 升序这两种方式,如果字段里面的值时数字就按照大小,如果是字母就按照先后顺序,不支持中文的排序方式。排序支持按多个字段排序,多次调用 orderBy 即可,多字段排序时的顺序会按照 orderBy 调用顺序先后对多个字段排序。如果需要对嵌套字段排序,可以使用点表示法,比如上面的 books 根据出版年份 year 从旧到新排序,可以写为 orderBy('publishInfo.year', 'asc')。技术文档:Collection.orderBy 分页显示 skip,skip 常与 limit 一起用于分页,比如商品列表一页只显示 20 个商品,第 1 页显示整个数据的 0~20 个,那么第 2 页我们用 skip(20)可以跳过第一页的 20 条数据,第 3 页则跳过 40 个数据,第 N 页则是 skip((n-1)*20)个数据:技术文档:Collection.skip 限制数量上限的 limit,集合数据查询的数量上限 limit 在小程序端最大数量为 20,在服务端为 100,比如 limit(30)在小程序端还是只会显示 20 条数据,更多数据则需要我们结合分页 skip 与 javascript 进行编程处理。技术文档:Collection.limit 小程序查询数据显示的结果虽然有数量限制,比如服务端为 100 个,但是排序仍然是基于整个集合的数据进行排序的,并不是只针对这 100 个数据。 匹配查询传入的对象的每个 构成一个筛选条件,有多个 则表示需同时满足这些条件,是 与的关系,如果需要 或关系,可使用 command.or 技术文档:command.or 查询指令 Command指令用于查询时,都会写在 where 内,主要对字段的值进行比较和逻辑的筛选判断。数据库 API 提供了大于、小于等多种查询指令,这些指令都暴露在 db.command 对象上。 指令Command可以分为查询指令和更新指令,这两者的用法有很大的区别,查询指令用于db.collection的where条件筛选,而更新指令则是用于db.collection.doc的update请求的字段的更新里,这两者的区别在后面我们也会反复提及。 比较操作符和逻辑操作符下面我们把查询指令的比较操作符和逻辑操作符整理成了一张表格,并附上相应的技术文档,方便大家对它们有一个清晰而整体的认识。 查询指令之比较 [图片]查询指令的写法指令 command 是基于 database 数据库引用的,我们以大于 gt 在小程序端(以大于 3000 为例)的完整写法为例: wx.cloud.database().command.gt(3000) 为了简便,通常我们会把 wx.cloud.database()会赋值给一个变量,如 db, db.command 又会赋值给 _,使用时最终被简化为 _.gt(3000)。通过一层一层的声明变量并赋值,大大简化了指令的写法,大家可以在其他指令都沿用这种写法。 用法丰富的等于指令 Command.eq相比于其他的比较指令等于 eq 和不等于 neq 操作符的用法非常丰富,它可以进行数值比较,我们查询某个字段比如 GDP 等于某个数值如 17502.8 亿的城市: .where({ gdp: _.eq(17502.8), }) 它还可以进行字符串的匹配,比如我们查询某个字段比如 city 完整匹配一个字符串如深圳: .where({ city: _.eq("深圳"), }) 注意:在查询时,gdp: _.eq(17502.8)的效果等同于gdp:17502.8,而city: _.eq(“深圳”)等同于city:”深圳”,虽然两种方式查询的结果都是一致的,但是它们的原理不同,前者用的是等于指令,后者用的是传递对象值。 eq 还可以用于字段的值是数组以及对象的情况,在后面的章节我们会再来介绍。 字段内的逻辑指令查询广东省内、GDP 在 3000 亿以上且在 1 万亿以下的城市。在广东省内也就是让字段 province 的值等于”广东”,而 GDP 的要求则是 GDP 这个字段同时满足大于 3000 亿且小于 1 万亿,这时就需要用到 and(条件与,也就是且的意思): .where({ province:_.eq("广东"), gdp:_.gt(3000).and(_.lt(10000)) }) 跨字段的逻辑指令上面的案例中 where 内的两个条件, province:.eq("广东")和 gdp:.gt(3000).and(_.lt(10000))带有跨字段的条件与 and(也就是且)的关系,那如何实现跨字段的条件或 or 呢? 查询中国 GDP 在 3000 亿元以上且常住人口在 500 万以上或建城区面积在 300 平方公里以上的前 20 个大城市。这里常住人口和建成区面积只需要满足其中一个条件即可,这就涉及到条件或 or(注意下面代码的格式写法): .where( { gdp: _.gt(3000), resident_pop:_.gt(500), }, _.or([{ builtup_area: _.gt(300)} ]), ) 注意上面三个条件, gdp: .gt(3000)和 resident_pop:.gt(500)是逻辑与,而与 builtup*area: *.gt(300)}的关系是逻辑或。 _.or([{条件一 },{条件二 }])内是一个数组,条件一与条件二又构成逻辑与的关系。 正则查询 db.RegExp正则表达式能够灵活有效匹配字符串,可以用来检查一个串里是否含有某种子串,比如“CloudBase 技术训练营”里是否含有”技术”这个词。云数据库正则查询支持 UTF-8 的格式,可以进行中英文的模糊查询。正则查询也是写在 where 字段的条件筛选里。 技术文档:Database.RegExp 字段字符串的模糊查询我们可以用正则查询来查询某个字段,比如 city 城市名称内,包含某个字符串比如”州”的城市: .where({ city: db.RegExp({ regexp: '州', options: 'i', }) }) 注意这里的 city 是字段,db.RegExp()里的 regexp 是正则表达式,而 options 是 flag,i 是 flag 的值表示不区分字母的大小写。当然我们也可以直接在 where 内用 JavaScript 的原生写法或调用 RegExp 对象的构造函数。比如上面的案例也可以写成: //JavaScript原生正则写法 .where({ city:/州/i }) //JavaScript调用RegExp对象的构造函数写法 .where({ city: new db.RegExp({ regexp: "州", options: 'i', }) }) 数据库查询的正则表达式也支持模板字符串,比如我们可以先声明 const cityname=”州”,然后用模板字符串包住 cityname 变量: city: db.RegExp({ regexp:`${cityname}`, options: 'i', }) 简单的正则表达式入门正则表达式的用法是非常繁杂的,关于正则表达式的知识可以去 MDN 以及掘金上面搜索了解更多细节。 技术文档:正则表达式、掘金正则搜索列表 值得注意的是,在数据库查询时应尽可能避免过度使用正则表达式来做复杂的匹配,尤其是用户访问触发较多的场景,通常情况下数据查询的响应时间(无论是小程序端还是云函数端)最好要低于500ms。 在小程序端新增记录和统计记录在前面我们已经介绍了集合数据请求的查询方法 get,除了 get 查询外,请求的方法还有 add 新增,remove 删除、update 改写/更新、count 统计以及 watch 监听,这些方法都是基于数据库集合的引用 Collection 的,接下来我们再来介绍如何基于 Collection 新增记录和统计记录的数量。 基于数据库集合的引用Collection所查询到的记录都是多条记录,也就是说我们可以对N条记录进行增、删、改、查等操作,不过目前还不支持在小程序端进行多条记录的update和remove,只能在云函数端进行这样的操作。 统计记录 Collection.count 统计集合记录数或统计查询语句对应的结果记录数。小程序端与云函数端的表现会有如下差异:小程序端:注意与集合权限设置有关,一个用户仅能统计其有读权限的记录数云函数端:因属于管理端,因此可以统计集合的所有记录数。 技术文档:Collection.count() const db = wx.cloud.database() const _ = db.command db.collection("china") .where({ gdp: _.gt(3000) }) .count().then(res => { console.log(res.total) }) field、orderBy、skip、limit 对 count 是无效的,只有 where 才会影响 count 的结果,count 只会返回记录数,不会返回查询到的数据。 新增记录 Collection.add在前面我们将知乎日报的数据导入到了 zhihu_daily 的集合里,接下来,我们就来给 zhihu_daily 新增记录。 技术文档:Collection.add 使用开发者工具新建一个 zhihudaily 的页面,然后在 zhihudaily.wxml 里输入以下代码,新建一个绑定了事件处理函数为 addDaily 的 button 按钮: 新增日报数据 然后再在 zhihudaily.js 里输入以下代码,在事件处理函数 addDaily 里调用 Collection.add,往集合 zhihu_daily 里添加一条记录,如果传入的记录对象没有 _id 字段,则由后台自动生成 _id;若指定了 _id,则不能与已有记录冲突。 addDaily(){ db.collection('zhihu_daily').add({ data: { _id:"daily9718005", title: "元素,生生不息的宇宙诸子", images: [ "https://pic4.zhimg.com/v2-3c5d866701650615f50ff4016b2f521b.jpg" ], id: 9718005, url: "https://daily.zhihu.com/story/9718005", image: "https://pic2.zhimg.com/v2-c6a33965175cf81a1b6e2d0af633490d.jpg", share_url: "http://daily.zhihu.com/story/9718005", body:"谨以此文,纪念元素周期表发布 150 周年。\r\n地球,世界,和生活在这里的芸芸众生从何而来,这是每个人都曾有意无意思考过的问题。\r\n科幻小说家道格拉斯·亚当斯给了一个无厘头的答案,42;宗教也给出了诸神创世的虚构场景;\r\n最为恢弘的画面,则是由科学给出的,另一个意义上的生死轮回,一场属于元素的生死轮回。" } }) .then(res => { console.log(res) }) .catch(console.error) } 点击新增日报数据的 button,会看到控制台打印的 res 对象里包含新增记录的_id 为我们自己设置的 daily9718005。打开云开发控制台的数据库标签,打开集合 zhihu_daily,翻到最后一页,就能看到我们新增的记录啦。 _openid 与集合权限注意和导入的数据不同的是,在小程序端新增记录,都会自动添加一个_openid 的字段,它的值等于用户 openid,_openid 的值是不允许修改的。当我们把集合的权限改为仅创建者可读写,或所有人可读,仅创建者可读写,在小程序端查询或更新记录时,会自动添加一个条件, .where({ _openid:"当前用户的openid" }) 所以这就是为什么尽管集合里面有数据,但是由于有了这个条件,只要记录里没有_openid 或 openid 不匹配就查询不到记录。 集合请求方法注意事项 get、update、count、remove、add等都是请求,在小程序端可以有callback和promise两种写法,但是在云函数端只能用promise,不能用callback。为了方便,建议大家统一使用promise的写法,也就是then、catch。 get、update、count、remove、add请求不能在一个数据库引用里同时存在。比如不能又是get(),又是count()的,不能这么写: db.collection('china').where({ _openid: 'xxx', }).get().count().add() 云函数端操作集合内记录云函数端调用数据库在云开发能力章节我们已经介绍过如何在云函数端调用数据库,这里也是一样。新建一个云函数 chinadata,然后在 exports.main = async (event, context) => {}输入以下代码,注意是 const db = cloud.database(),wx. cloud.database(),云函数端的数据库引用和小程序端有所不同: const db = cloud.database() const _ = db.command return await db.collection("china") .where({ gdp: _.gt(3000) }) .field({ _id: false, city: true, province: true, gdp: true }) .orderBy('gdp', 'desc') .skip(0) .limit(10) .get() try/catch async错误处理 当 async 函数中只要一个 await 出现 reject 状态,则后面的 await 都不会被执行。如果有多个 await 则可以将其都放在 try/catch 中。 然后右键 chinadata 云函数根目录选择在终端中打开,输入 npm install,之后上传并部署所有文件。 在前面我们了解到,调用云函数可以使用本地调试、云端测试,我们还可以在小程序端调用云函数,将云函数的数据返回到小程序端来。使用开发者工具在 chinadata.wxml 里输入以下代码,也就是我们通用点击按钮触发事件处理函数: 调用chinadata云函数 再在事件处理函数里调用云函数,在 chinadata.js 里输入 getChinaData 事件处理函数来调用 chinadata 云函数: getChinaData() { wx.cloud.callFunction({ name: 'chinadata', success: res => { console.log("云函数返回的数据",res.result.data) }, fail: err => { console.error('云函数调用失败:', err) } }) }, 在模拟器里点击调用 chinadata 云函数的 button 按钮,就能在控制台里看到云函数返回的查询到的结果,大家可以通过 setData 的方式将查询的结果渲染到小程序页面,这里就不介绍啦。 删除多条数据记录基于数据库集合的引用 Collection,我们可以先匹配 where 语句查询到相关条件的多条记录,再来调用 Collection.remove()来进行删除。五个查询方法,skip 和 limit 不支持,field、orderBy 没有意义,只有 where 条件可以用来筛选记录。数据一旦删除就不能再找回了。 技术文档:Collection.remove() 我们可以把之前建好的 chinadata 云函数 exports.main = async (event, context) => {}里的代码修改为如下,即删除省份 province 为广东的所有数据: return await db.collection('china') .where({ province:"广东" }) .remove() 在模拟器里点击调用 chinadata 云函数的 button 按钮,就能在控制台里看到云函数返回的对象,其中包含 stats: {removed: 22},即删除了 22 条数据。 更新多条记录 Collection.update 我们可以把之前建好的 chinadata 云函数 exports.main = async (event, context) => {}里的代码修改为如下,也就是先查询省份 province 为湖北的记录,给这个记录更新一个字段英文省份名 pro-en: return await db.collection('china') .where({ province:"湖北" }) .update({ data: { "pro-en": "Hubei" }, }) 这里要注意的是,pro-en 这个字段之前是没有的,通过 Collection.update 不只是起到更新的作用,还可以批量新增字段并赋值,也就是 update 时记录里有相同字段就更新,没有就新增; "pro-en": "Hubei",直接使用 pro-en 会报错,用双引号效果等价。 如果你想给导入的数据添加_openid字段,只用云函数是没法实现的,因为云函数没有用户的登录态。我们需要先在小程序端调用云函数比如login返回openid,再将openid的值再传给chinadata云函数,才能给记录添加openid。 操作单个记录 doc 的字段值在前面我们已经了解了基于集合引用 Collection 构建查询条件的 5 个方法,以及一些请求方法,接下来我们来讲一下基于集合记录引用 Document 的四个请求方法:获取单个记录数据 Document.get()、删除单个记录 Document.remove()、更新单个记录 Document.update()、替换更新单个记录 Document.set()。和基于 Collection 不一样的是,前者的增删改查是可以批量多条的,而基于 Document 则是操作单条记录。 查询集合collection里的记录常用于获取文章、资讯、商品、产品等等的列表;而查询单个记录doc的字段值则常用于这些列表里的详情内容。如果你在开发中需要增删改查某个记录的字段值,为了方便让程序可以根据_id找到对应的记录,建议在创建记录的时候_id用程序有规则的生成。 查询单个记录 doc 的字段值集合里的每条记录都有一个 _id 字段用以唯一标志一条记录,_id 的数据格式可以是 number 数字,也可以是 string 字符串。这个_id 是可以自定义的,当导入记录或写入记录没有自定义时系统会自动生成一个非常长的字符串。查询记录 doc 的字段 field 值就是基于_id 的。 技术文档:获取单个记录数据 Document.get() 比如我们查询其中知乎日报的一篇文章(也就是其中一条记录)的数据,使用开发者工具 zhihudaily 页面的 zhihudaily.js 的 onLoad 生命周期函数里输入以下代码(db 不要重复声明): db.collection('zhihu_daily').doc("daily9718006") .get() .then(res => { console.log('单个记录的值',res.data) }) .catch(err => { console.error(err) }) }, 如果集合的数据是导入的,那_id 是自动生成的,自动生成的_id 是字符串 string,所以 doc 内使用了单引号(双引号也是可以的哦),如果你自定义的_id 是 number 类型,比如自定义的_id 为 20191125,查询时为 doc(20191125)即可,这只是基础知识啦。 删除单条记录技术文档:删除单个记录Document.remove() removeDaily(){ db.collection('zhihu_daily').doc("daily9718006") .remove() .then(console.log) .catch(console.error) } 更新单条记录技术文档:更新单个记录 Document.update() updateDaily(){ db.collection('zhihu_daily').doc("daily9718006") .update({ data:{ title: "【知乎日报】元素,生生不息的宇宙诸子", } }) }, 替换更新记录技术文档:替换更新单个记录 Document.set() setDaily(){ db.collection('zhihu_daily').doc("daily9718006") .set({ data: { "title": "为什么狗会如此亲近人类?", "images": [ "https://pic4.zhimg.com/v2-4cab2fbf4fe9d487910a6f2c54ab3ed3.jpg" ], "id": 9717547, "url": "https://daily.zhihu.com/story/9717547", "image": "https://pic4.zhimg.com/v2-60f220ee6c5bf035d0eaf2dd4736342b.jpg", "share_url": "http://daily.zhihu.com/story/9717547", "body": "让狗从凶猛的野兽变成忠实的爱宠,涉及了宏观与微观上的两层故事:我们如何在宏观上驯养了它们,以及这些驯养在生理层面究竟意味着什么。\r\ncontent-image\" src=\"http://pic1.zhimg.com/70/v2-4147c4b02bf97e95d8a9f00727d4c184_b.jpg\" alt=\"\">\r\n狗是灰狼(Canis lupus)被人类驯养后形成的亚种,至少可以追溯到 1 万多年以前,是人类成功驯化的第一种动物。在这漫长的岁月里,人类的定向选择强烈改变了这个驯化亚种的基因频率,使它呈现出极高的多样性,尤其体现在生理形态上。" } }) }
2021-09-10 - 云开发云存储入门讲解
在云开发能力章节我们了解到小程序端和服务端都可以上传文件到云存储,不过在实际开发中云存储里的文件链接需要被记录在数据库里才方便调用。接下来我们就来介绍云存储文件的增删改查是如何与数据库的增删改查结合在一起的。在云数据库入门章节我们所涉及到的数据库里数据类型还非常简单,在这一章里我们会来介绍如何操作数据库的数组和对象等复杂数据类型的增删改查。 云存储与数据库的关系不经过数据库直接把文件上传到云存储里,这样文件的上传、删除、修改、查询是无法和具体的业务对应的,比如文章商品的配图、表单图片附件的添加与删除,都需要图片等资源能够与文章、商品、表单的 ID 能够一一对应才能进行管理(在数据库里才能对应),而这些文章、商品、表单又可以通过数据库与用户的 ID、其他业务联系起来,可见数据库在云存储的管理上扮演着极其重要的角色。 数据库的设计与结构和 Excel 表、关系型数据库(如 MySQL)以行和列、多表关系来设计表结构不同的是,云开发的数据库是基于文档的。我们可以在一个记录里嵌套多层数组和对象,把每个文档所需要的数据都嵌入到一个文档里,而不是分散到多个不同的集合。 比如我们想做一个网盘小程序,用来记录用户信息,以及创建的相册、文件夹,这里相册和文件夹因为可以创建很多个,所以它是一个数组;而每一个相册对象和文件夹对象里都可以存储一个照片列表和文件列表,我们发现在云开发数据库里一个元素的值是数组,数组里又嵌套对象,对象里又有元素是数组是非常常见的事情。 以下是网盘小程序的数据库设计,包含了一个用户的信息,上传的所有文件和照片等信息: { "_id": "自动生成的ID", "_openid": "用户在当前小程序的openid", "nickName": "用户的昵称", "avatarUrl": "用户的头像链接", "albums": [ { "albumName": "相册名称", "coverURL": "相册封面地址", "photos": [ { "comments": "照片备注", "fileID": "照片的地址" } ] } ], "folders": [ { "folderName": "文件夹名称", "files": [ { "name": "文件名称", "fileID": "文件的地址", "comments": "文件备注" } ] } ] } 如果是用关系型数据库,就会建 user 表来存储用户信息,albums 表存储相册信息,folders 表存储文件夹信息,photos 表存储照片信息,files 表存储文件信息,相信大家可以通过这个案例对云数据库是面向文档的有一个大致的了解。 当然云开发的数据库也是可以把数据分散到不同集合的,需要视不同的情况而定,在后面章节我们会介绍。这种将每个文档所需的数据都嵌入到一个文档内部的做法,我们称之为反范式化(denormalization),将数据分散到多个不同的集合,不同集合之间相互引用称之为范式化(normalization),也就是说反范式化文档里包含子文档,而范式化呢,文档的子文档则是存储在另一个集合之中。 fileID 是存储与数据库的纽带从上面可以看出,云存储与数据库就是通过 fileID 来取得联系的,数据库只记录文件在云存储的 fileID,我们可以访问数据库相应的 fileID 属性进行记录的增删改查操作,与此同时调用云存储的上传文件、下载文件、删除文件等 API,这样云存储就被数据库给管理起来了。 打开云开发技术文档里云存储的所有 API,如上传文件 uploadFile、下载文件 downloadFile、删除文件 deleteFile、用云文件 ID 换取真实链接 getTempFileURL,我们发现这些 API 始终是围绕 fileID 来展开的,要么 fileID 是 success 回调返回的对象,要么 fileID 是 API 必备的属性。 建立用户与数据的关系 openid 与云开发在前面我们已经了解到,用户在小程序里有着独一无二的 openid,用 openid 完全可以区分用户;使用云开发时用户在小程序端上传文件到云存储,这个 openid 会被记录在文件信息里;添加数据到数据库这个 openid 会被保存在_openid 的字段里(也就是说我们除了可以用云函数如前面的 login 来获取用户的 openid,还可以通过数据库的_openid 字段来获取 openid);而且我们在小程序端查询数据时(查询时改、删、更新等的前提),都会默认有一个 where({_openid:当前用户的 openid})的条件,限制了用户 write 写(改、删、更新)的权限。 _id 与云开发当用户在小程序端往数据库用 Collection.add 添加记录 document 时,会自动给该记录生成_id,同时也会创建一个_openid,_id 和_openid 由于都是独一无二的,只要我们获取每个用户创建的记录_id,也就能同时确定这个用户的 openid。 判断用户是否存在并创建记录打开云开发控制台的数据库标签,新建一个 clouddisk 的集合,并修改它的权限为为“所有人可读,仅创建者可读写”(或使用安全规则)。使用开发者工具新建一个 folder 的页面,然后在 folder.js 的页面生命周期函数 onLoad 里输入以下代码: this.checkUser() this 调用自定义函数,开发者可以添加任意的函数或数据到 Object 参数中,在页面的函数中用 this 可以访问 然后再在 Page()对象里输入以下代码,代码的意思是如果 clouddisk 里没有用户创建的数据,那就在 clouddisk 里新增一条记录;如果有数据,就返回数据: async checkUser() { //获取clouddisk是否有当前用户的数据,注意这里默认带了一个where({_openid:"当前用户的openid"})的条件 const userData = await db.collection('clouddisk').get() console.log("当前用户的数据对象",userData) //如果当前用户的数据data数组的长度为0,说明数据库里没有当前用户的数据 if(userData.data.length === 0){ //没有当前用户的数据,那就新建一个数据框架,其中_id和_openid会自动生成 return await db.collection('clouddisk').add({ data:{ //nickName和avatarUrl可以通过getUserInfo来获取,这里不多介绍 "nickName": "", "avatarUrl": "", "albums": [ ], "folders": [ ] } }) }else{ this.setData({ userData }) console.log('用户数据',userData) } }, 一个用户只能创建一条记录,如果是开一个用户可以创建多条记录… 预先搭好文档的数据框架方便我们在后面以 update 的方式来更新数据。 async/await 的使用说明async 是“异步”的简写,async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成,await 只能出现在 async 函数中。await 在 async 函数中才会有效。假设一个业务需要分步完成,每个步骤都是异步的,而且依赖上一步的执行结果,甚至依赖之前每一步的结果,就可以使用 Async Await 来完成 小程序端现在完全支持 async/await 的写法,不过需要在开发者工具-详情-本地设置,勾选增强编译才行,否则会报以下错误。 Uncaught ReferenceError: regeneratorRuntime is not defined async 函数返回值是 Promise 对象, async 函数内部 return 返回的值。会成为 then 方法回调函数的参数。如果 async 函数内部抛出异常,则会导致返回的 Promise 对象状态变为 reject 状态。抛出的错误而会被 catch 方法回调函数接收到。async 函数返回的 Promise 对象,必须等到内部所有的 await 命令的 Promise 对象执行完,才会发生状态改变。也就是说,只有当 async 函数内部的异步操作都执行完,才会执行 then 方法的回调。 在 async 函数中使用 await,那么 await 这里的代码就会变成同步的了,意思就是说只有等 await 后面的 Promise 执行完成得到结果才会继续下去,await 就是等待,这样虽然避免了异步,但是它也会阻塞代码,所以使用的时候要考虑周全。await 会阻塞代码,每个 await 都必须等后面的 fn()执行完成才会执行下一行代码 云存储文件夹管理在小程序端创建一个文件夹,需要考虑三个方面,一是文件夹在云存储里是怎么创建的;二是文件夹在数据库里的表现形式;三是小程序端页面应该怎么交互才算是创建了一个文件夹; 文件夹在云存储里是怎么创建的在云开发能力章节我们了解到,要上传 demo.jpg 到云存储的 cloudbase 文件夹里,只需要指明 cloudPath 云存储的路径为 cloudbase/demo.jpg 即可,这里的 cloudbase 文件夹,在我们上传文件时代码会自动创建,也就是说我们在小程序端创建文件夹不需要对云存储做任何事情,因为在云存储这里,文件夹是只有在文件上传时才会创建。 文件夹在数据库里的表现形式尽管文件夹在小程序端的页面交互看来非常复杂,但是它在数据库的形式看起来却非常简单,我们创建文件夹只是在操作(增删改查)数组和对象而已,以下的 folders 数组是文件夹列表,而一个文件夹只是数组里的一个对象而已。 "folders": [ { "folderName": "文件夹名称", "files": [ ] } ] 文件夹的创建与页面交互通过前面的分析可知,在小程序端创建文件夹,只会操作数据库的数据,而不会操作云存储,我们来看具体的代码实现。使用开发者工具新建一个 folder 的页面,然后在 folder.wxml 里输入以下代码: <form bindsubmit="formSubmit"> <input name="name" placeholder='请输入文件夹名' auto-focus value='{{inputValue}}' bindinput='keyInput'></input> <button type="primary" formType="submit">新建文件夹</button> </form> 方法一:使用 push 和 在 folder.js 里输入以下代码: async createFolder(e) { let foldersName = e.detail.value.foldersName const folders = this.data.userData.data[0].folders folders.push({ foldersName: foldersName, files: [] }) const _id= this.data.userData.data[0]._id return await db.collection('clouddisk').doc(_id).update({ data: { folders: _.set(folders) } }) }, 技术文档:字段更新操作符 set 方法二: 在 folder.js 里输入以下代码: async createFolder(e) { let foldersName = e.detail.value.foldersName const _id= this.data.userData.data[0]._id return await db.collection('clouddisk').doc(_id).update({ data: { folders: _.push([{ foldersName: foldersName, files: [] }]) } }) }, 技术文档:数组更新操作符 push 先读后写与先写后读 上传单个文件到文件夹相信大家都应该在其他小程序体验过文件上传的功能,在交互上这个功能虽然看起来简单,但是在代码的逻辑上却包含着四个关键步骤: 首先把文件上传到小程序的临时文件,并获取临时文件地址以及文件的名称;将临时文件上传到云存储指定云文件里,并 q 取到文件的 FileID;将文件在云存储的 FileID 和文件的名称上传到数据库;获取文件夹内所有文件的信息。 上传文件到小程序的临时文件使用开发者工具在 folder.wxml 里输入以下代码: <form bindsubmit="uploadFiles"> <button type="primary" bindtap="chooseMessageFile">选择文件</button> <button type="primary" formType="submit">上传文件</button> </form> 然后在 folder.js 里输入以下代码: chooseMessageFile(){ const files = this.data.files wx.chooseMessageFile({ count: 5, success: res => { console.log('选择文件之后的res',res) let tempFilePaths = res.tempFiles for (const tempFilePath of tempFilePaths) { files.push({ src: tempFilePath.path, name: tempFilePath.name }) } this.setData({ files: files }) console.log('选择文件之后的files', this.data.files) } }) }, 将临时文件上传到云存储技术文档:wx.cloud.uploadFile uploadFiles(e) { const filePath = this.data.files[0].src const cloudPath = `cloudbase/${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/\.[^.]+?$/) wx.cloud.uploadFile({ cloudPath,filePath }).then(res => { this.setData({ fileID:res.fileID }) }).catch(error => { console.log("文件上传失败",error) }) }, 上传成功后会获得文件唯一标识符,即文件 ID,后续操作都基于文件 ID 而不是 URL。 将文件信息存储到数据库 addFiles(fileID) { const name = this.data.files[0].name const _id= this.data.userData.data[0]._id db.collection('clouddisk').doc(_id).update({ data: { 'folders.0.files': _.push({ "name":name, "fileID":fileID }) } }).then(result => { console.log("写入成功", result) wx.navigateBack() } ) } 匹配数组第 n 项元素 如果想找出数组字段中数组的第 n 个元素等于某个值的记录,那在 匹配中可以以 字段.下标 为 key,目标值为 value 来做匹配。如对上面的例子,如果想找出 number 字段第二项的值为 20 的记录,可以如下查询(注意:数组下标从 0 开始) 获取文件夹内文件列表在 onload 生命周期函数里输入 this.getFiles() 然后再在 Page 对象里添加 getFiles()方法,获取该用户的数据 getFiles(){ const _id= this.data.userData.data[0]._id db.collection("clouddisk").doc(_id).get() .then(res => { console.log('用户数据',res.data) }) .catch(err => { console.error(err) }) } 要实际开发一个具体的功能,一定要先思考这个功能的页面交互是怎样的,而页面交互的背后都只不过是简单的数据,但正是这些简单的数据经过页面交互处理之后却“蒙蔽”了用户的双眼,让用户觉得复杂,觉得这个功能真实存在。 嵌套数组和对象的查询我们可以对对象、对象中的元素、数组、数组中的元素进行匹配查询,甚至还可以对数组和对象相互嵌套的字段进行匹配查询/更新 匹配记录中的嵌套字段// 方式一 db.collection('todos').where({ style: { color: 'red' } }).get() // 方式二 db.collection('todos').where({ 'style.color': 'red' }).get() 匹配并更新数组中的元素 上传多个文件到文件夹 查询所有数据const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const MAX_LIMIT = 100 exports.main = async (event, context) => { // 先取出集合记录总数 const countResult = await db.collection('china').count() const total = countResult.total // 计算需分几次取 const batchTimes = Math.ceil(total / 100) // 承载所有读操作的 promise 的数组 const tasks = [] for (let i = 0; i < batchTimes; i++) { const promise = db.collection('china').skip(i * MAX_LIMIT).limit(MAX_LIMIT).get() tasks.push(promise) } // 等待所有 return (await Promise.all(tasks)).reduce((acc, cur) => { return { data: acc.data.concat(cur.data), errMsg: acc.errMsg, } }) } 小程序端下载并预览文件技术文档:wx.openDocument()、wx.cloud.downloadFile 使用云开发来下载云存储里面的文件,就不会有域名校验备案的问题 previewFile(){ wx.cloud.downloadFile({ fileID: 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/cloudbase/技术工坊预备手册.pdf' }).then(res => { const filePath = res.tempFilePath wx.openDocument({ filePath: filePath }) }).catch(error => { console.log(error) }) } 删除记录与删除字段技术文档:deleteFile 可以根据文件 ID 下载文件,用户仅可下载其有访问权限的文件: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { const fileIDs = ['xxx', 'xxx'] const result = await cloud.deleteFile({ fileList: fileIDs, }) return result.fileList } 嵌套删除字段 return await db.collection("clouddisk").doc("_id").update({ data:{ "folders.0.files.1": _.remove() } }) 获取临时链接并分享文件技术文档:getTempFileURL 将服务端的文件传到小程序端技术文档:downloadFile
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