社区/学习指南/小程序云开发学习指南

云开发与 Node.js

云函数的运行环境是 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 模块:提供了一些用于处理文件路径的 API
  • url 模块:用于处理与解析 URL
  • http 模块:用于创建一个能够处理和响应 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)
      }
    })
  }
<image width="400px" height="200px" src="data:image/jpeg;base64,{{img}}"></image>

Buffer String

Buffer JSON

图像处理 sharp

sharp 是一个高速图像处理库,可以很方便的实现图片编辑操作,如裁剪、格式转换、旋转变换、滤镜添加、图片合成(如添加水印)、图片拼接等,支持 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 等资源,需要建立私有网络来确保数据安全及连接安全。

连接数据库 Redis

const 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:
      '<p><b>你好:</b><img src="https://hackwork-1251009918.cos.ap-shanghai.myqcloud.com/handbook/html5/weapp.jpg"/></p>' +
      '<p>欢迎欢迎<br/></p>',
    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')) // 下载文件到本地

加解密 Crypto

crypto 模块是 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)
  }
})

本文出自 李东bbsky