跳到主要内容

函数代码示例

如下提供了若干函数 2.0 的使用示例,以下示例为简单起见,主要使用 JavaScript 编写。

引入外部模块

在函数中引入外部模块,例如 lodash,并使用其提供的方法:

函数代码文件示例:

// index.js
const _ = require('lodash')

exports.main = function(event, context) {
return _.kebabCase('Hello world')
}

package.json文件:

// package.json
{
"name": "example",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"lodash": "^4.17.21"
}
}

执行该函数后客户端可以收到 hello-world 的响应。

在不同函数间共享代码

利用单实例多函数能力和函数间路由,可以使用一般的模块导入方法,在不同的函数间共享公共模块。

假设有函数 funcAfuncB 需要共享一个获取当前时间的方法 now,有如下目录结构:

.
├── cloudbase-functions.json # 多函数配置文件
├── cloudrunfunctions/common # 公共方法目录
│ └── time.js # 公共方法
├── cloudrunfunctions/funcA # 函数 A 目录
│ └── index.js
└── cloudrunfunctions/funcB # 函数 B 目录
├── package.json # 指定 index.mjs 为入口文件
└── index.mjs

time.js 中导出 now 方法:

// common/time.js
exports.now = function () {
return new Date().toLocaleString()
}

在函数 A 和函数 B 代码中直接引入即可:

// cloudrunfunctions/funcA/index.js
const now = require('../common/time').now
// cloudrunfunctions/funcB/index.mjs
import { now } from '../common/time.js'

cloudbase-functions.json 中对不同的函数进行声明:

{
"functionsRoot":"./cloudrunfunctions/", // 函数根目录,指定为当前目录
"functions": [ // 声明各函数及其入口文件
{
"name":"funcA", // 声明函数 A
"directory":"funcA",
"triggerPath": "/a"
},
{
"name":"funcB", // 声明函数 B
"directory":"funcB",
"triggerPath": "/b"
}
]
}

这样函数 A 和函数 B 将部署在同一个服务中,同时均可以使用 now 方法

在函数中路由

可根据 context 中获取到的 HTTP 相关路径、query 等信息,实现简单的路由功能。

函数代码:

exports.main = function(event, context) {
const { httpContext } = context
const { url } = httpContext
const path = new URL(url).pathname

// 根据访问路径返回不同的内容
switch (path) {
case '/':
return {
statusCode: 200,
body: 'Hello world!'
}
case '/index.html':
return {
statusCode: 200,
headers: {
'Content-Type': 'text/html'
},
body: '<h1>Hello world!</h1>'
}
default:
return {
statusCode: 404,
body: 'Not found'
}
}
}

返回不同类型的响应

可以通过不同的路由路径,返回不同的响应类型及响应内容。

函数代码:

exports.main = function(event, context) {
const { httpContext } = context
const { url } = httpContext
const path = new URL(url).pathname

// 根据访问路径返回不同的内容
switch (path) {
// 直接返回字符串
case '/':
return 'Hello world!'
// 返回当前时间戳
case '/now':
return new Date().getTime()
// 使用集成响应返回 HTML
case '/index.html':
return {
statusCode: 200,
headers: {
'Content-Type': 'text/html'
},
body: '<h1>Hello world!</h1>'
}
// 使用集成响应返回 JSON
default:
return {
statusCode: 404,
headers: {
'Content-Type': 'application/json'
},
body: {
message: 'Not found'
}
}
}
}

可综合使用集成响应、非集成响应,获得更丰富的响应类型。

使用 Server-sent Event 推送消息

为了适配 AI 大模型 API 常用的 SSE 协议,云函数 2.0 可以支持 SSE 方式推送内容。

函数代码:

exports.main = async function (event, context) {
// 切换到 SSE 模式
const sse = context.sse()

sse.on('close', () => {
console.log('sse closed')
})

// 发送事件到客户端,发送前先检查是否已经关闭,如未关闭可发送
if (!sse.closed) {
// 多次发送多个事件
sse.send({ data: 'No.1 message' })
sse.send({ data: 'No.2 message with\n\r\r\n\r\r\rtwo lines.' })

// 单次发送多个事件
sse.send([
{ data: 'No.1 message' },
{ data: 'No.2 message with\n\r\r\n\r\r\rtwo lines.' }
])

// 以下为发送原始消息的示例
// 该方式用于扩展 SSE 协议,例如发送其他 Event Field 字段
// 注意:末尾必须有换行符数据才会立即发送
sse.send('message: This is a raw message. ')
sse.send(['message: This is another raw message.\n\n'])

// 函数执行时间以函数返回时间计算
// 函数返回后,HTTP 请求处理完成,函数内的异步逻辑继续进行处理,不影响函数返回时间
// TCP 网络连接依然被 SEE 占用,在 SSE 连接被客户端或服务端关闭之前,可以继续发送消息到客户端
// SSE 协议已经将 HTTP 转换到长连接模式,需要客户端或服务端在适当的时候主动关闭连接,否则将导致连接一直占用,消耗网络资源
// 因TCP主动关闭的一方将进入TIME_WAIT 状态,大量 TIME_WAIT 状态的连接将导致网络资源耗尽,无法建立新的连接,所以客户端主动关闭连接更符合最佳实践
// 因客户端可能并不知晓在什么时间关闭连接,服务端可以发送一个特殊的消息,告诉客户端消息已经结束,可以关闭连接了
// 浏览器中调用 EventSource#close 关闭连接,见:https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close
return ''
}
}

使用 WebSocket 长连接收发消息

函数代码:

export function main (event, context) {
console.log({ event, context })
if (context.ws) {
context.ws.on('close', (msg) => {
console.log('close: ', msg)
})
context.ws.on('message', (msg) => {
console.log('message: ', msg)
})
setInterval(() => {
context.ws.send(`now: ${new Date().toISOString()}`)
}, 100)
}
}

// 支持同步异步
main.handleUpgrade = async function (upgradeContext) {
console.log(upgradeContext, 'upgradeContext')
if (upgradeContext.httpContext.url === '/upgrade-handle-throw-error') {
throw new Error('test throw error')
} else if (upgradeContext.httpContext.url === '/upgrade-handle-reject-error') {
return Promise.reject(new Error('test reject error'))
} else if (upgradeContext.httpContext.url === '/allow-websocket-false') {
return {
allowWebSocket: false,
statusCode: 403,
body: JSON.stringify({ code: 'code', message: 'message' }),
contentType: 'appliaction/json; charset=utf-8'
}
}
return { allowWebSocket: true }
}

node.js 客户端代码:

import WebSocket from 'ws'

function run () {
const ws = new WebSocket('ws://127.0.0.1:3000/')

ws.on('close', (code, reason) => {
console.log('close:', code, `${reason}`)
})
ws.on('error', (err) => {
console.error('error: ', err)
})
ws.on('upgrade', () => {
console.log('upgrade')
})
ws.on('ping', () => {
console.log('recv ping message')
})
ws.on('pong', () => {
console.log('recv pong message')
setTimeout(() => {
ws.ping()
}, 1000)
})
ws.on('unexpected-response', (ws, req, res) => {
// 非 upgrade 响应和 3xx 重定向响应认为是 unexpected-response
console.log('recv unexpected-response message')
})

ws.on('message', (data) => {
console.log('received: %s', data)
})

ws.on('open', () => {
ws.ping()
ws.send('string data')
ws.send(Buffer.from('buffer data'))
})
}

run()

使用 multipart/form-data 提交表单数据(文件)

云函数 2.0 支持前端以 multipart/form-data 格式提交表单内容,可以包含文件内容。

函数代码:

const path = require('path')
const fs = require('fs')

exports.main = async function (event, context) {
// 从 event.file 属性上获取要保存的文件(与传参对应)
// event.file 类型为 PersistentFile,见 https://www.npmjs.com/package/formidable#file
// 如有其他传参,可用 form data 传参中的对应名称 event.[your param name] 获取,如 event.name, event.size 等
const file = event.file
// 获取原始文件名
const fileName = file.originalFilename;
// 保存文件的目录
const fileDir = path.join(process.cwd(), 'tmp')
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir)
}
const filePath = path.join(fileDir, fileName)
// 从参数中读取文件流
const readStream = fs.createReadStream(file.filepath)
// 尝试保存文件到指定目录
try {
await fs.promises.writeFile(filePath, readStream)
} catch (error) {
return {
statusCode: 500,
body: `Error saving file: ${error.message}`
}
}

// 注意:删除临时文件

return {
statusCode: 200,
body: `File saved to: ${filePath}`
}
}

发送上传文件请求:

curl --location 'url' \
--form 'file=@file.png'

注意:

  1. 文件上传完成后会保存到本地文件,您应该在函数执行结束后删除文件,以免占用过多磁盘空间。
  2. 上传的文件如需持久化,应该保存到云存储或其他服务中,以免文件丢失,避免保存在本地。

使用 PUT 上传二进制数据或文件

函数代码:

const path = require('path')
const fs = require('fs')

exports.main = async function(event, context) {
const { httpContext } = context
const { url } = httpContext
// 从 query 中获取文件名
const filename = new URL(url).searchParams.get('filename')
// 保存文件的目录
const fileDir = path.join(process.cwd(), 'tmp')
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir)
}
// 保存文件的路径
const filePath = path.join(fileDir, filename)

try {
// 从 event 中获取文件内容
const buffer = Buffer.from(event, 'binary')
await fs.promises.writeFile(filePath, buffer);
return {
statusCode: 200,
body: `File saved to: ${filePath}`,
};
} catch (error) {
return {
statusCode: 500,
body: `Error saving file: ${error.message}`,
};
}
}

发送上传文件请求:

curl --location --request PUT 'url?filename=file.png' \
--header 'Content-Type: application/octet-stream' \
--data 'file.png'

使用 puppeteer 实现网页截屏

{
"name": "example",
"version": "1.0.0",
"main": "index.mjs",
"type": "module",
"dependencies": {
"puppeteer": "^23.11.1"
}
}
import * as path from 'path'
import * as url from 'url'
import * as fs from 'fs'
import puppeteer from 'puppeteer'

const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

async function screenshotWebPage(webPageUrl, savePath) {
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox']
})
const page = await browser.newPage()

await page.goto(webPageUrl)
await page.pdf({
path: savePath,
format: 'letter',
})

await browser.close()
}

export const main = async function (event, context) {
const webPageUrl = 'https://docs.cloudbase.net/cbrf/intro'
const savePath = 'cbrf-intro.pdf'
await screenshotWebPage(webPageUrl, savePath)

return {
statusCode: 200,
headers: {
'Content-Disposition': `attachment; filename=${savePath}`,
'Content-Type': 'application/pdf',
},
body: fs.createReadStream(path.join(__dirname, savePath))
}
}