社区/学习指南/小程序·云开发高级教程

云函数的模块知识

由于云函数与 Nodejs 息息相关,需要我们对云函数与 Node 的模块以及 Nodejs 的一些基本知识有一些基本的了解。下面只介绍一些基础的概念,如果你想详细深入了解,建议去翻阅一下 Nodejs 的官方技术文档:

技术文档:Nodejs API 中文技术文档

11.1.1 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() 显式地访问.

11.1.2 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,
});

11.1.3 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 里查看这些依赖的版本信息。

11.1.4 第三方模块

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,通常我们用最新的即可。

11.1.5 云函数的运行机制

云函数运行在服务端 Linux 的环境中,一个云函数在处理并发请求的时候会创建多个云函数实例,每个云函数实例之间相互隔离,没有公用的内存或硬盘空间,因此每个云函数的依赖也是相互隔离的,所以每个云函数我们都要下载各自的依赖,无法做到复用。

云函数实例的创建、管理、销毁等操作由平台自动完成。每个云函数实例都在 /tmp 目录下(这里是服务端的绝对路径/tmp,不是云函数目录下的./tmp)提供了一块 512MB 的临时磁盘空间用于处理单次云函数执行过程中的临时文件读写需求,需特别注意的是,这块临时磁盘空间在函数执行完毕后可能被销毁,不应依赖和假设在磁盘空间存储的临时文件会一直存在。如果要持久化的存储,最好是使用云存储。

云函数应是无状态的,也就是一次云函数的执行不依赖上一次云函数执行过程中在运行环境中残留的信息。为了保证负载均衡,云函数平台会根据当前负载情况控制云函数实例的数量,并且会在一些情况下重用云函数实例,这使得连续两次云函数调用如果都由同一个云函数实例运行,那么两者会共享同一个临时磁盘空间,但因为云函数实例随时可能被销毁,并且连续的请求不一定会落在同一个实例(因为同时会创建多个实例),因此云函数不应依赖之前云函数调用中在临时磁盘空间遗留的数据。总的原则即是云函数代码应是无状态的。

  • 由于云函数是按需执行, 云函数在return返回之后就会停止运行, 和普通 node 本地运行的行为有些差异,这个要注意一下;

  • 如果云函数需要处理一些文件的下载,可以把文件存储在服务器的临时目录/tmp里,云函数的目录是没有写权限的;

  • 云函数存在冷启动和热启动的问题,所谓冷启动就是云函数完整执行整个实例化实例、加载函数代码和 node,执行函数的整个过程,而热启动则是函数实例和执行被复用,main 函数外的代码可能不会被执行,因此有些变量的声明不要写在 main 函数外面,当云函数被高并发调用时,main 函数外的变量可能会成为跨实例的“全局变量”;

  • 不要在云函数异步流程中执行关键任务,也就是一些关键任务的函数前面要加一个await,以免任务没有执行完,云函数就终止了;

  • 由于云函数是无状态的,因此执行环境通常会从头开始初始化(冷启动),当发生冷启动时,系统会对函数的全局环境进行评估。如果云函数导入了模块,那么在冷启动期间加载这些模块会增加延迟时间,因此正确加载依赖项而不加载函数不使用的依赖项,可以缩短此延迟时间以及部署函数所需的时间。

本文出自 李东bbsky