# Q & A

# 可以通过哪些方式管理小程序云开发的资源?

管理小程序云开发资源如数据库、存储、云函数的方式有很多,并不局限于微信开发者工具自带的云开发控制台。

1、通过腾讯云网页后台管理云开发资源

我们还可以登录腾讯云官网的 云开发后台管理,使用微信公众号登录,然后用该小程序管理员的微信扫描二维码,就可以在网页控制台里看到我们使用微信开发者工具创建的小程序云开发资源。

腾讯云官网的云开发资源管理界面

在这里可以管理小程序云开发的数据库、文件(存储)、云函数、监控告警、日志检索以及环境设置,也可以对云开发资源的服务进行续费,是一个可以替代云开发控制台的可视化管理工具。

2、使用HTTP API来管理云开发资源

有不少朋友反馈,我们应该如何让市场、运营、产品等来管理云开发的资源(如添加商品、发表文章等),通常我们需要搭建一个后台,便于不懂代码的人员来进行可视化管理,尽管我们可以把这个后台直接搭建在小程序里面,但是PC端的后台可能更加方便一些,开发人员可以用到HTTP API搭建一个网页后台。

云开发官方技术文档有非常详细的 HTTP API技术文档 ,通过HTTP API我们可以实现对数据库的集合、记录、文件等的增删改查以及触发云函数。

3、跨小程序进行资源管理

如果你有多个小程序都开通了云开发,也就有了多个云开发资源环境,那我们是否可以在A小程序的云函数里调用B小程序的云开发资源呢?当然可以,通过tcb-admin-node这个依赖就能很方便的实现。

tcb-admin-node在我们安装wx-server-sdk时就已经同时安装了该依赖,该依赖更多信息可以通过阅读在Github上的技术文档来了解。

// 初始化示例
const tcb = require('tcb-admin-node');

// 初始化资源,云函数下不需要secretId和secretKey。env如果不指定将使用默认环境
tcb.init({
  secretId: 'xxxxx',
  secretKey: 'xxxx',
  env: 'xxx'
});

//云函数下使用默认环境
tcb.init()

//云函数下指定环境
tcb.init({
  env: 'xxx'
})

4、通过CLI工具来管理云开发资源

CloudBase CLI 是一个开源的命令行界面交互工具,用于帮助用户快速、方便的部署项目,管理云开发资源。对于开发人员来说,我们还可以通过cloudbase-cliCLI工具使用命令行对云开发资源进行管理。在云开发博客网站里,有该工具相当详细的使用教程

除了以上几种方式,我们还可以使用cloudbase-manager-node,它支持开发者通过接口形式对云开发提供的云函数、数据库、文件存储等资源进行创建、管理、配置等操作,它的功能更在tcb-admin-node之上;如果你是小程序服务商,会给很多商家开通并管理小程序的云开发资源,可以使用云开发的代开发的接口;如果你想将云开发这种免服务器免运维的开发理念和方式贯彻到web端,也可以直接把小程序云开发的资源用作web端,实现一云多端,详细可见云开发web端开发技术文档。关于这方面的内容,以后会深入介绍。

# 云开发是怎么计费的?相比于传统服务器,贵吗?

云开发的基础版是免费的(免费版虽然有有效期,但是到期会自动免费续期)。云开发资源也提供的是一套完整的综合性服务,也就是完全可以只依赖云开发提供的服务独立开发出可以落地的技术产品,这是传统云服务(暂且这么说吧)所不具备的。云开发相当于直接提供了传统云服务所包含的Nodejs环境云服务器、云存储、CDN、MongoDB数据库等(省掉了域名与运维服务),所以云开发服务也是综合性服务的计费方案。
云开发既然开发起来这么方便,服务也都打包好了,那用云开发划算吗?云开发计费相比传统云服务,贵吗?就微信读书这种获客数千万,日均PV过千万的大型应用来说,云开发的开发效率是APP的4倍,网页H5的2倍,这是开发成本;而使用云开发整个服务的成本也比自建Node服务器要低。
当然综合性的服务会有一个弊端,就是资源搭配的不合理会造成一定的浪费,因此云开发针对不同的业务类型所需要服务侧重点不同推出了多套方案,有资源均衡型、CDN资源消耗型、云函数资源消耗型、数据库资源消耗型,未来也会有单独服务包购买的方案,通过购买符合自身业务的资源类型,云开发也没有比传统云服务贵。

云开发配额方案支持多种类型
那不同类型的云开发资源大致可以支撑多少人访问呢?比如69元/月的数据库资源消耗型,每天支持读50万次,写30万次,平均一个uv大约会有5次左右的pv,大约会读取数据库10次,写会比较少一些,也就可以支持日均1万的访问;390元/月的云函数资源消耗型,每月支持400万GBs资源使用量,这里资源使用量GBS = 函数配置内存 X 运行计费时长,其中配置内存目前为256M,而单次云函数的执行时间大约300ms左右,平均来说执行一次云函数的资源使用量大约是0.06GBs,相当于每个月可以调用6700万次云函数,用于云调用群发订阅消息、处理内容安全、业务数据的增删改查都是比较足够的。云函数在未执行时是不产生任何费用的,对一些不需要常驻的业务进程来说,开销会大大降低。

# 云开发的并发是多少?云开发是否支持几十万、几百万PV的业务

由于用云开发可以一站式简单的开发业务,简化了开发难度,让不少人开始怀疑云开发处理高并发和复杂业务的能力。相比于购买传统云服务,云开发可能处理过于复杂的业务逻辑并非它的强项,但是处理高并发、海量用户却十分方便。
云开发的云存储自带CDN,CDN通过将云存储的内容发布至遍布全国的加速节点,使用户能就近获取所需内容,避免网络拥堵、地域、运营商等因素带来的访问延迟问题,有效提升下载速度、降低响应时间。CDN通常是没有带宽限制的,保证了静态资源在海量用户且地域分布广泛的加载速度。

腾讯云CDN 中国境内节点分布

而数据库的并发取决于数据库的同时连接数和数据库的请求耗时,同时连接数会根据付费类型的不同而有所差异,比如590元/月的数据库资源消耗型,支持的同时连接数为400,而数据库查询的请求耗时影响的因素很多,超过100ms则称之为慢查询需要检查,通常单次查询耗时在10~20ms,也就是连接数为400的数据库,它的并发大致在400*(1000/10)到400*(1000/20),即支持2万~4万的qps,这个qps足以支持每日亿级的PV。 云函数支持毫秒级别的实时弹性伸缩,完全根据请求量扩容或缩容,动态负载均衡将请求分发至后端近乎无限的函数实例上,完全无需任何手动配置和操作,满足并发量从 0 到成千上万的不同场景。就单个云函数而言,它的同时连接数为1000,并发量比较高的云函数则需要做好性能优化,降低执行耗时,通常可以75ms左右,也可以满足每日近亿的pv。

# 云开发在处理大量数据时需要注意哪些问题?

云开发的数据库是通用性的非关系型数据库MongoDB,稳定可靠、性能卓越、易于拓展,支持二级索引、范围查询、聚合、地理空间索引等丰富的功能。关系型数据库在处理互联网业务的诸多领域比如游戏、物流、社交、物联网IoT、视频直播、地图导览等.
和关系型数据库(如MySQL)以行和列、多表关系来设计表结构不同的是,云开发的数据库是基于文档的。我们可以在一个记录里嵌套多层数组和对象,可以把每个文档所需要的数据嵌入到一个文档里,而不是分散到多个不同的集合(反范式化设计)。这种处理方式便于快速查询。不过单个Doc不能超过16M,Doc嵌套的层数不能超过100层。在我们进行数据查询和聚合时,数据库单词出包的大小也不能超过16M。
我们都知道云开发有两套API,分为小程序端和服务端,在我们处理大量数据的时候,通常建议在小程序端进行数据的增删改查,一是可以节约云函数的资源,二是调用大量数据时,小程序端比云函数端要快,云函数调用数据库除了会有增删改查数据库的耗时,还有云函数的启动时间、以及将数据返回给小程序端的时间,因此云函数端调用数据库通常会比小程序端调用数据库慢几百毫秒。值得一提的是,云函数返回给小程序端的数据大小也是有限制的,不能超过1M,因此也不建议大家把图片做成base64返回给小程序端,建议使用云存储来过渡。 小程序端查询数据库时会默认查询结果集数量上限limit为20,取更多数据建议结合skip分页分批次获取,云函数端limit的数量限制为1000条。如果你需要一次处理几十万条业务数据,比如给数十万人群发订阅消息,可以在结合云调用在云函数端进行,云函数的默认超时时间为3s,可以在配置里修改为60s。

云函数超时时间与环境变量配置

# 云开发的环境初始化问题总结

很多人在导入云开发项目或新建一个云开发项目时,会遇到环境相关的问题。除了需要在微信开发者工具扫码登录具备开发者权限的微信账号外,还需要注意:

  • 项目配置文件project.config.json里注意配置小程序的appid为自己的appid;
  • 项目配置文件还可以配置云函数根目录如: "cloudfunctionRoot": "cloudfunctions/",这时在开发者工具的资源管理器该目录cloudfunctions还会显示当前环境的环境名,如果没有,可以右键菜单更多设置里进行设置。
  • 小程序端环境初始化为需要填入相应的云开发的环境ID
wx.cloud.init({
  env: 'xly-xrlur',//结果通常为环境名称+‘-’+一个后缀 
  traceUser: true,
})

一个appid可以创建两个免费的云开发资源,用于生产环境和测试环境,在小程序端可以通过修改wx.cloud.init的env属性来切换,而在云函数端也是可以通过环境的初始化在两个环境进行切换的。小程序端和一个云函数里只能初始化一次环境,初始化多次也只有第一次会生效。

const cloud = require('wx-server-sdk')
cloud.init({
  env: 'test-x1dzi'
})

//还可以给env参数传入对象,指定数据库、存储、云函数的默认环境
// cloud.init({
//   env: {
//     database:'test-x1dzi',
//     storage:'xly-xrlur',
//     functions:'test-x1dzi',
//   }
// })

建议每个云函数都配置初始化环境,如果你没有跨环境调资源的开发需求,可以统一把所有的云函数的环境初始化写为如下,cloud.DYNAMIC_CURRENT_ENV,标志当前云开发资源所在的环境。

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV,
})

有时我们希望小程序上线之后,在不修改提交代码的情况下将测试版环境改成小程序的生成环境(当然小程序上线也是可以修改云函数的,只是不能用于测试),这时我们可以配置云函数的环境变量,如上图所示,然后将云函数初始化的env设置为env: process.env.env即可(值即为上图的xly-xrlur)。

# 云开发控制台的数据库管理功能太少,比如如何批量删除一个集合内的多条记录、批量新增记录、给数据进行排序等等。

1、批量删除一个集合内的多条记录

我们在开发的过程中,一个集合内有几百条、几千条数据希望全部清空,但是又不想删掉该集合再重建,那应该如何做呢,总不能一条一条删除吧?云开发控制台的可视化操作目前无法做到批量删除一个集合内的多条记录的,但是这个功能我们可以通过控制台数据库高级操作的脚本来轻松进行批量删除,而且还可以创建一个脚本模板,有需要直接点击执行脚本模板做到长期复用。比如我们要删除集合为china的所有记录:

db.collection('china')
  .where({
      _id: _.exists(true)
    })
  .remove()

由于remove请求只支持通过匹配 where 语句来删除,我们可以在where里包含一个条件只要存在_id就删除,由于基本每个记录都有_id,所以就能都删除了。

2、如何给集合内所有数据都新增一个字段

我现在一个集合内有N条数据,由于数据库初期设计的问题,现在想给所有记录新增一个字段,想像进行关系型数据库和Excel新增一列的类似操作,那我应该怎么做呢?同样我们也可以通过控制台数据库高级操作的脚本。比如我们想给china集合内的所有记录都新增一个updateTime的字段,我们可以查询到需要新增字段的记录,然后使用update请求,当记录内没有updateTime字段就会新增:

const serverDate = db.serverDate
db.collection('china')
  .where({
    _id: _.exists(true)
  })
  .update({
    data: {
      updateTime: serverDate(),
    }
  })

3、如何让记录按照自己预想的方式来排序

我在小程序端批量上传了图片、文章,但是发现它们的显示顺序并不是按照我上传顺序来进行排序,但是我有不少功能却非常依赖排序这个功能,请问我应该怎么做?

批量上传或者你按时间上传,记录的排序并不会按照你认为的顺序来排序是很正常的,查询到的数据的顺序一般也不会是控制台数据库显示的顺序,这个都是非常正常的。你如果对排序有需求,有两种方式,一种是你在开发时就能设计好排序的字段,比如想让文章能按时间来排序,就应该在小程序发表文章时就设置一个字段来记录文章的发布时间,还有一种方式就是手动加字段来自定义,比如轮播的顺序,文章置顶或调整顺序这些,可能你还没有来得及开发相关功能,我们可以使用控制台来自定义,比如给你要排序的记录新增一个字段来自定义你想要的排序顺序,然后再在数据查询时使用orderBy。

云开发控制台的数据可视化管理和高级操作还可以实现很多类似于关系型数据库GUI管理工具的功能,毕竟GUI管理的背后就是数据库的脚本操作,更多功能大家可以自己多探索。

# 如果批量获取云存储的fileID以及批量导出数据库里所有数据

我有很多图片、文件批量导入到了云存储,但是我批量获取这些文件的fileID应该怎么做?我的数据库有几十个集合,数据库经常需要备份,每次都要一个个导出非常麻烦,有没有好的方法?

如果大家有类似的功能,大家可以使用cloudbase-manager-node。cloudbase-manager-node的功能非常强大,里面有相比于tcb-admin-node更加丰富的接口,当然这些功能都需要开发人员可以结合接口进行一定的开发。

比如我们想批量获取云存储文件的fileID,可以使用listDirectoryFiles(cloudPath: string): Promise<IListFileInfo[]>列出文件夹下所有文件的名称,也可以使用downloadDirectory(options): Promise<void>来下载文件夹,比如我们想对所有集合的数据进行备份,可以使用listCollections(options: object): object来获取所有集合的名称,然后使用export(collectionName: string, file: object, options: object): object接口来导出所有记录到指定的json或csv文件里。

# 小程序云开发是否可以防止恶意刷资源

小程序云开发的资源都是按照次数来付费的,特别担心我的云开发资源被人恶意刷从而影响业务进行或造成不必要的成本,这个应该怎么处理?

对于这个问题,开发者可以在小程序端做一些限制;云开发本身对单个微信用户的请求是有限流,超限部分是不算次数的,所以完全不必担心。

# 定时触发器怎么用?定时触发器为啥报错?为什么没有生效?

定时触发器可以处理周期性的事情,比如时报、日报、周报等通知提醒,也可以处理倒计时任务,比如节假日、纪念日以及你可以指定一个具体时间的倒计时任务,除此之外,定时触发器还可以用来周期性处理一些定时任务。比如定期清理一些不必要的数据,定期更新集合内的数据。

在使用定时触发器时,要注意以下问题: 1、要想让定时触发器少犯错误,以及可以定位到是不是触发器的问题,我们在对某个云函数使用触发器前,首先要保证该云函数在小程序端可以调用成功;

2、开发者工具的版本对触发器也存在影响这一点要注意。如果你要使用定时触发器来触发云调用,请保证你的开发者工具的版本是2019年10月18日之后的版本,目前官网最新的稳定版是没有问题的; 3、你的config.json文件是否配置正确,config.json文件可以用来配置权限和定时触发器,比如该云函数需要使用到订阅消息和内容安全两个权限,以及每5秒钟定时发送一次订阅消息,config.json的写法如下:

{
  "permissions": {
    "openapi": [
      "subscribeMessage.send",
      "security.imgSecCheck"
    ]
  },
  "triggers": [
    {
      "name": "tomylove",
      "type": "timer",
      "config": "*/5 * * * * * *"
    }
  ]
}

config.json配置文件的格式,数组最后一项不能有逗号,;配置文件里不能有注释等;Cron 表达式有七个必需字段,不能多也不能少; 4、务必要注意的是更新触发器文件上传触发器两个概念的不同。当我们在修改触发器配置文件config.json后,首先鼠标右键config.json选择“云函数增量上传:更新文件”,然后再右键config.json选择“上传触发器”。这里的“云函数增量上传:更新文件”是让云函数端的触发器文件更新;而“上传触发器”则是让触发器开始生效执行。不能在云函数端的触发器没有更新的情况下就“上传触发器”来执行定时触发,因为你的文件没有更新,执行的还是旧的触发器内容。

# 聚合应该怎么使用?为什么我总是用不对?

聚合aggregate和数据查询get时不同的两套体系,聚合更偏向于数据的统计分析,用聚合来查询的功能非常强大,但是目前是不能对集合进行增、删、改等write的操作,因此所有结果都需要返回到小程序端。数据库查询的结果也必须限制在16MB以内。

由于聚合和数据查询都能对数据库进行查询,而且两个的很多方法都特别类似,所以很多人会混淆,甚至错误的混用,比如会在aggregate()加where条件、get请求,这里为了让大家更好理解,特整理一下两个的对比,大家写数据库查询和聚合时都可以先写类似以下的模板。

普通数据查询

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)
  })

聚合查询

const db = wx.cloud.database()
const _ = db.command
const $ = db.command.aggregate
db.collection('china').aggregate()
  .match({       //类似于where,对记录进行筛选
    price: $.gt(3000)
  })
  .project({     //类似于field    
  })
  .sort({        //类似于orderBy
      age: -1,
      score: -1
  })
  .skip(5)      //类似于skip
  .limit(2)     //类似于limit
  .end()        
  .then(res => console.log(res))
  .catch(err => console.error(err))
  • match是根据条件过滤文档,进行的是查询匹配,语法和where比较类似,在写聚合时,应尽可能的把match放在流水线的前面。match内可以写db.command查询操作符 _ 和聚合操作符db.command.aggregate $,但是除了match阶段,在其他聚合阶段中传入的对象可使用的操作符都是聚合操作符;
  • project 把指定的字段传递给下一个流水线,指定的字段可以是某个已经存在的字段,也可以是计算出来的新字段,它和field不同的是可以新增一些不存在的字段(只是显示用,也没写进数据库);
  • sort 根据指定的字段,对输入的文档进行排序。<排序规则>可以是以下取值:1 代表升序排列(从小到大);-1 代表降序排列(从大到小);功能和orderBy类似;
  • 小程序端 limit 默认 20,也就是如果你使用聚合查询,你查询到的数据都会默认显示20条数据,但是你可以设置更多,而普通查询是不能超过20条的