云开发 云函数的模块知识
由于云函数与Nodejs息息相关,需要我们对云函数与Node的模块以及Nodejs的一些基本知识有一些基本的了解。下面只介绍一些基础的概念,如果你想详细深入了解,建议去翻阅一下Nodejs的官方技术文档:
技术文档:Nodejs API 中文技术文档
一、Nodejs的内置模块
在前面我们已经接触过Nodejs的fs模块、path模块,这些我们称之为Nodejs的内置模块,内置模块不需要我们使用npm install下载,就可以直接使用require引入:
const fs = require('fs')
const path = require('path')
const url = require('url')
Nodejs的常用内置模块以及功能如下所示,这些模块都是可以在云函数里直接使用的:
- fs 模块: 文件目录的创建、删除、查询以及文件的读取和写入;
- os模块: 提供了一些基本的系统操作函数;
- path 模块: 提供了一些用于处理文件路径的API;
- url模块: 用于处理与解析 URL;
- http模块: 用于创建一个能够处理和响应 http 响应的服务;
- querystring模块: 解析查询字符串;
- util模块: util 模块主要用于支持 Node.js 内部 API 的需求,大部分实用工具也可用于应用程序与模块开发者;
- net模块: 用于创建基于流的 TCP 或 IPC 的服务器;
- dns模块: 用于域名的解析;
- crypto模块: 提供加密功能,包括对 OpenSSL 的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装;
- zlib模块: zlib 可以用来实现对 HTTP 中定义的 gzip 和 deflate 内容编码机制的支持。
- process模块: 提供有关当前 Node.js 进程的信息并对其进行控制.作为一个全局变量,它始终可供 Node.js 应用程序使用,无需使用 require(), 它也可以使用 require() 显式地访问.
二、Node的global全局对象
和JavaScript的全局对象(Global Object)类似,Nodejs也有一个全局对象global,它以及它的所有属性(一些全局变量都是global对象的属性)都可以在程序的任何地方访问。下面就来介绍一下Nodejs在云函数里比较常用的全局变量。
1、dirname 和filename
dirname是获得当前执行文件所在目录的完整目录名,node还有另外一个常用变量filename,它是获得当前执行文件的带有完整绝对路径的文件名。我们可以新建一个云函数比如nodefile,然后在nodefile云函数的index.js里输入以下代码:
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
exports.main = async (event, context) => {
console.log('当前执行文件的文件名', __filename );
console.log('当前执行文件的目录名', __dirname );
}
将云函数部署上传之后,通过小程序端调用、本地调试或云端测试就可以执行云函数,得到如下的打印结果(还记得云函数的打印日志可以在哪里查看么?):
当前执行文件的文件名 /var/user/index.js
当前执行文件的目录名 /var/user
由此可见云函数在云端Linux环境就放置在/var/user
文件夹里面。
2、module、exports、require
还有一些变量比如module,module.exports,exports等实际上是模块内部的局部变量,它们指向的对象根据模块的不同而有所不同,但是由于它们通用于所有模块,也可以当成全局变量。
- module对当前模块的引用,module.exports 用于指定一个模块所导出的内容,即可以通过 require() 访问的内容。
- require用于引入模块、JSON、或本地文件,可以从node_modules引入模块,可以使用相对路径引入本地模块,路径会根据 __dirname定义的目录名或当前工作目录进行处理。
- exports表示该模块运行时生成的导出对象。如果按确切的文件名没有找到模块,则 Nodejs会尝试带上.js、.json或.node拓展名再加载。
以/
为前缀的模块是文件的绝对路径,放到云函数里require('/var/user/config/config.js')
会加载云函数目录里的config文件夹里的config.js,这里require('/var/user/config/config.js')
在云函数的路径里等同于相对路径的require('./config/config.js')
。当没有以 '/'、'./' 或 '../' 开头来表示文件时,这个模块必须是一个核心模块或加载自node_modules 目录。
在nodefile云函数的目录下面新建一个config文件夹,在config文件夹里创建一个config.js,云函数的目录结构如下图所示:
nodefile // 云函数目录
├── config //config文件夹
│ └── config.js //config.js文件
└── index.js
└── config.json
└── package.json
然后再在config.js里输入以下代码,通常我们用这样的方式申明一些比较敏感的信息,或者比较通用的模块:
module.exports = {
AppID: 'wxda99ae45313257046', //可以是其他变量,这里只是参考
AppKey: 'josgjwoijgowjgjsogjo',
}
然后在nodefile云函数的index.js里输入以下代码(下面并非实际代码,大家看着添加):
//下面两句放在exports.main函数的前面
const config = require('./config/config.js')
const {AppID,AppKey} = config
//省略了部分代码
exports.main = async (event, context) => {
console.log({AppID,AppKey})
}
将云函数的所有文件都部署上传到云端之后,再来执行云函数,我们就可以看到config/config.js里面的变量就被传递到了index.js里了,这同时也说明在云函数目录之下不仅可以创建文件(前面创建过图片),还可以创建模块,通过module.exports和require来达到创建并引入的效果。
3、process.env属性
process 对象提供有关当前 Node.js 进程的信息并对其进行控制,它有一个比较重要的属性process.env,返回包含用户环境的对象。
比如上面的nodefile云函数,打开云开发控制台,在云函数列表里找到nodefile,然后点击配置在弹窗的环境变量里添加一些环境变量,比如NODE_ENV、ENV_ID、name(因为是常量,建议用大写字母),它的值为字符串,然后我们将nodefile云函数的index.js代码改为如下:
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
exports.main = async (event, context) => {
return process.env //process可以不必使用require就可以直接用
}
右键云函数增量上传之后,调用该云函数,然后在云函数的返回的对象里就可以看到除了有我们设置的变量以外,还有一些关于云函数环境的信息。因此我们可以把一些需要手动可以修改或者比较私密的变量添加到配置里,然后在云函数里调用,比如我们想在小程序上线之后修改小程序的云开发环境,可以添加ENV_ID字段,值到时根据情况来修改:
const cloud = require('wx-server-sdk')
const {ENV_ID} = process.env
cloud.init({
env: ENV_ID
})
三、wx-server-sdk的模块
再来回顾一下wx-server-sdk这个第三方模块,它也是云开发必备的核心依赖,云开发的诸多API都是基于此。我们可以在给云函数安装了wx-server-sdk之后(也就是右键云函数,在终端执行了 npm install),在电脑上打开云函数的node modules文件夹,可以看到虽然只安装了一个wx-server-sdk,但是却下载了很多个模块,这些模块都是通过三个核心依赖@cloudbase/node-sdk(原tcb-admin-node)、protobuf、jstslib来安装的。
要想对wx-server-sdk有一个深入了解,我们可以研究一下最核心的@cloudbase/node-sdk(原tcb-admin-node),具体可以参考@cloudbase/node-sdk的Github官网,同时由于wx-server-sdk顺带下载了很多依赖,比如@cloudbase/node-sdk、xml2js、request等,这些依赖可以在云函数里直接引入。
const request = require('request')
request模块虽然是第三方模块,但是已经通过wx-server-sdk下载了,在云函数里直接通过require就可以引入。由于wx-server-sdk模块是每个云函数都会下载安装的,我们完全可以把它当成云函数的内置模块来处理,而通过wx-server-sdk顺带下载的N多个依赖,我们也可以直接引入,不必再来下载,而在使用npm install
安装完成之后的package-lock.json里查看这些依赖的版本信息。
四、第三方模块
Nodejs生态所拥有的第三方模块是所有编程语言里最多了,比Python、PHP、Java还要多,借助于这些开源的模块,可以大大节省我们的开发成本,这些模块在npm官网地址都可以搜索到,不过npm官网的第三方模块大而全,哪些才是Nodejs开发人员最常用最优秀的模块呢?我们可以在Github上面找到awesome Nodejs,这里有非常全面的推荐。
在awesome-nodejs里,这些优秀的模块被分为了近50个不同的类别,而其中大多数都是可以用于云函数的,可见云函数的强大远不只停留在云开发的技术文档上,我们接下来会在这一章会选取一些比较有代表性的模块来结合云函数进行讲解。
当我们要在云函数里引入第三方模块时,需要先在该云函数package.json里的dependencies里添加该模块并附上版本号"第三方模块名": "版本号"
,版本号的表示方法有很多,npm install 会下载相应的版本(只列举一些比较常见的):
latest
,会下载最新版的模块;
1.2.x
,等同于1.2,会下载>=1.2.0<3.0.0的版本;
~1.2.4
,会下载>=1.2.4 <1.3.0的版本;
^1.2.4
,会下载>=1.2.3 <2.0.0的版本
比如我们要在云函数里引入lodash的最新版,就可以去该云函数package.json里添加"lodash": "latest"
,注意是添加到dependencies属性里面,而且package.json的写法也要符合配置文件的格式要求,尤其要注意最后一项不能有逗号,
,以及不能在json配置文件里写注释:
"dependencies": {
"lodash": "latest"
}
在 npm install
时候生成一份package-lock.json文件,用来记录当前状态下实际安装的各个npm package的具体来源和版本号。不同的版本号可能对运行的结果造成不一样的影响,所以为了保证版一致会有package-lock.json,通常我们用最新的即可。
五、云函数的运行机制
云函数运行在服务端Linux的环境中,一个云函数在处理并发请求的时候会创建多个云函数实例,每个云函数实例之间相互隔离,没有公用的内存或硬盘空间,因此每个云函数的依赖也是相互隔离的,所以每个云函数我们都要下载各自的依赖,无法做到复用。
云函数实例的创建、管理、销毁等操作由平台自动完成。每个云函数实例都在 /tmp 目录下(这里是服务端的绝对路径/tmp,不是云函数目录下的./tmp)提供了一块 512MB 的临时磁盘空间用于处理单次云函数执行过程中的临时文件读写需求,需特别注意的是,这块临时磁盘空间在函数执行完毕后可能被销毁,不应依赖和假设在磁盘空间存储的临时文件会一直存在。如果要持久化的存储,最好是使用云存储。
云函数应是无状态的,也就是一次云函数的执行不依赖上一次云函数执行过程中在运行环境中残留的信息。为了保证负载均衡,云函数平台会根据当前负载情况控制云函数实例的数量,并且会在一些情况下重用云函数实例,这使得连续两次云函数调用如果都由同一个云函数实例运行,那么两者会共享同一个临时磁盘空间,但因为云函数实例随时可能被销毁,并且连续的请求不一定会落在同一个实例(因为同时会创建多个实例),因此云函数不应依赖之前云函数调用中在临时磁盘空间遗留的数据。总的原则即是云函数代码应是无状态的。
- 由于云函数是按需执行, 云函数在
return
返回之后就会停止运行, 和普通 node 本地运行的行为有些差异,这个要注意一下;
- 如果云函数需要处理一些文件的下载,可以把文件存储在服务器的临时目录
/tmp
里,云函数的目录是没有写权限的;
- 云函数存在冷启动和热启动的问题,所谓冷启动就是云函数完整执行整个实例化实例、加载函数代码和node,执行函数的整个过程,而热启动则是函数实例和执行被复用,main 函数外的代码可能不会被执行,因此有些变量的声明不要写在main 函数外面,当云函数被高并发调用时,main 函数外的变量可能会成为跨实例的“全局变量”;
- 不要在云函数异步流程中执行关键任务,也就是一些关键任务的函数前面要加一个
await
,以免任务没有执行完,云函数就终止了;
- 由于云函数是无状态的,因此执行环境通常会从头开始初始化(冷启动),当发生冷启动时,系统会对函数的全局环境进行评估。如果云函数导入了模块,那么在冷启动期间加载这些模块会增加延迟时间,因此正确加载依赖项而不加载函数不使用的依赖项,可以缩短此延迟时间以及部署函数所需的时间。
更多建议: