-

在上一节,我们大致体验了一下云开发:开通了云开发服务,相当于在云端拥有了一个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有着紧密的联系。