函数编写指南
函数式托管提供了一种在云托管服务中运行函数式代码的能力,开发者可以通过编写函数式代码来实现自己的业务逻辑。本文档将介绍如何编写函数式代码。
本文将介绍如何编写函数代码,如何进行错误处理,以及函数结构、出入参等基本信息,并提供一系列的示例以供参考。
快速开始
第一步:编写函数
- 在本地创建一个空的文件夹,作为项目的根目录。
- 新建
index.js
文件,作为函数的入口文件。 - 在
index.js
文件中,填写以下内容:
exports.main = function (event, context) {
const { httpContext } = context
const { url, httpMethod } = httpContext
return `[${httpMethod}][${url}] Hello world!`
}
第二步:运行函数
云函数代码编写完毕后,既可以将函数运行起来,之后便可以通过 HTTP 请求来调用函数。
运行函数的方式有两种:
1. 通过命令行工具本地运行,通常在开发调试阶段
tcb fun run -w --source .
2. 在云托管环境中运行函数,通常在生产环境中
tcb fun deploy -e <环境 ID> -s <服务名称> --appId <微信 AppId>
更多用法:见 将函数部署到云托管
第三步:调用函数
当部署函数之后,即可通过 HTTP 请求来调用函数。
1. 调用本地运行的云函数
curl http://localhost:3000
2. 调用云托管环境中的云函数
常规调用
登录微信云托管控制台,
在 微信云托管-服务设置-基础信息
页面获取公网访问地址,通过 浏览器
或 curl
等客户端软件 访问改地址即可。
curl https://{}.run.tcloudbase.com/
也可以在 微信云托管-云端调试
页面 调试或访问 服务。
微信小程序 callContainer
调用
// 容器调用必填环境id,不能为空
var c1 = new wx.cloud.Cloud({
resourceEnv: '环境id'
})
await c1.init()
const r = await c1.callContainer({
path: '/', // 填入业务自定义路径
header: {
'X-WX-SERVICE': 'xxx', // 填入服务名称
},
// 其余参数同 wx.request
method: 'POST',
})
console.log(r)
通过该方式调用,您可以省去配置自定义域名、证书等操作,快速调用函数,并获得微信私密链路的安全性提升。
wx.cloud.callContainer 相关文档:
了解函数
函数的输入参数 event
和 context
函数的基本结构如下:
exports.main = function (event, context) {
// 函数逻辑
}
或
exports.main = async function (event, context) {
// 函数逻辑
}
该文件导出一个 main
函数,该函数即为函数的入口方法,当请求到达时,该方法会作为代码执行的起点。该方法接受两个固定的参数 event
和 context
,分别代表函数的输入和上下文。
其中,event
为函数的触发事件,在当前的 HTTP 访问场景下,可以认为是 HTTP 请求体,例如 POST
请求的 body
,multipart/form-data
请求的 form-data
,PUT
请求所传输的二进制文件等。如果 HTTP 请求没有传递请求体,event
将会是一个空对象。
context
为函数执行的上下文信息,包含以下属性和方法:
Name | 类型 | 含义 |
---|---|---|
eventID | string | 事件唯一标识,用于关联上下文 |
eventType | string | 事件类型,当前固定为 http |
timestamp | number | 请求时间戳 |
httpContext | httpBasis | HTTP 请求的相关信息 |
extendedContext | Record<string, unknown> | 扩展的上下文信息,平台提供,包含 环境相关信息 |
sse() | ISeverSentEvent | 返回用于发送 Server-Sent Event 格式响应的对象 |
httpBasis
定义如下:
名称 | 类型 | 含义 |
---|---|---|
url | string | 本次请求的完整 URL |
httpMethod | string | 本次请求的 HTTP 方法,如 GET |
headers | IncomingHttpHeaders | 本次请求的 HTTP 头部 |
其中,从 extendedContext
中可以获得以下属性:
{
envId: "xxx", // 环境 ID
uin: "xxx", // 请求的 UIN
userId: "xxx", // 请求的用户 ID
accessToken: "xxx", // 调用请求时的 AccessToken
serviceName: "xxx", // 云托管服务名称
serviceVersion: "xxx", // 云托管服务版本
source: "xxx" // 请求来源,如 wx
wechatContext: {
callId: "xxx", // 微信调用 ID
source: "xxx", // 请求来源,如 wx
appId: "xxx", // 小程序的 AppID
openId: "xxx", // 用户的 OpenID
unionId: "xxx", // 资源共享时的 UnionID
fromOpenId: "xxx", // 资源共享时的 fromOpenID
fromUnionId: "xxx", // 资源共享时的 fromUnionID
fromAppId: "xxx" // 资源共享时的 fromAppID
}
}
云函数支持 Server-Sent Event
格式的响应,通过调用 sse()
即可以切换到 SSE
模式并得到 ISeverSentEvent
实例。
interface ISeverSentEvent {
setEncoder(encoder: TextEncoder): void;
send(event: SseEvent): boolean;
end(msg: SseEvent): void;
}
interface SseEvent<T = string | Record<string, unknown>> {
data: T;
comment?: string;
event?: string;
id?: string;
retry?: number;
}
可以通过 send()
方法来发送 SSE 响应,例如:
const { sse } = context
sse.send({
data: "Hello world!"
})
当函数执行完毕(即从入口函数返回)时,返回值将会被直接返回给客户端,不会进行额外的序列化处理。
函数的输出(即返回值)
数执行最后的返回值将作为最终返回值返回给客户端,此时,HTTP 将使用默认的状态码(如正常返回时200
、未带有响应体时204
、出错时500
)和默认的响应头返回给客户端。
函数支持 同步、异步 两种形式的写法,支持 普通类型
| Promise 类型
的返回值,通过 Buffer | Stream
类型的返回值,可以直接返回二进制数据,实现下载文件的能力。
集成响应
通过集成响应,可以自定义 HTTP 响应的结构,包括响应状态码、头部字段等,可以采用集成响应的返回结构。集成响应的定义如下:
interface IntegrationResponse {
statusCode: number
headers: { [key: string]: string | string[] }
body?: unknown
}
statusCode
为 HTTP 响应状态码,headers
为自定义的 HTTP 响应头部,body
为 HTTP 响应体。例如返回下面的集成响应:
exports.main = function(event, context) {
return {
statusCode: 200,
headers: {
"Content-Type": "text/html"
},
body: {
message: "<h1>Hello world!</h1>"
}
}
}
将可以在浏览器中渲染出 HTML。
错误处理
函数运行过程中可能或抛出异常,异常整体可以分为以下几类:
- 不可恢复的错误:如启动时未加载成功代码、Out of Memory(OOM)、堆栈溢出,这类错误会导致函数无法启动或从运行中退出,此类错误是严重错误,会导致函数实例重启,应当尽量规避;
- 请求处理过程中的错误,即业务逻辑报错:这类错误通常是可捕获的,函数需要对这些错误进行处理,记录相关日志并返回相关信息给客户端。如果用户代码未捕获相关异常,函数框架将会捕获该错误,记录相关日志并返回框架定义的错误信息给客户端;
- 未被捕获的异常
UncaughtException
和UnhandledRejection
:通常发生在异步代码逻辑中,因为是未捕获异常,业务上是感知不到的,这类错误会被框架捕获,并打印相关信息到日志中。因为通常发生在异步逻辑中,该错误不会体现在函数返回值中; - 函数框架报错:函数框架内部可能发生一些错误,可能是函数框架代码存在 Bug,也可能是函数框架的限制,这类错误会被框架捕获,并打印相关信息到日志中;
- 网络错误:在客户端往返函数实例的网络链路中也可能出现异常,导致一些错误。该类错误需结合具体部署平台进行排查。
函数的模块化和依赖安装
如通常的 Node.js
代码一样,函数也可以通过 Javascript
模块化方式来组织代码。对于遵循 CommonJS
规范的模块,可以通过 require
来引入依赖。例如:
// index.js
const { add } = require('./sum')
exports.main = function(event, context) {
return add(1, 2)
}
// sum.js
exports.add = function(a, b) {
return a + b
}
对使用 ECMAScript
模块化(需在 package.json
中声明模块化方式)的代码,可以通过 import
语句来引入依赖。例如:
// index.mjs
import { add } from './sum.mjs'
export function main(event, context) {
return add(1, 2)
}
// sum.mjs
export function add(a, b) {
return a + b
}
// package.json
{
"name": "example",
"version": "1.0.0",
"main": "index.mjs", // 指定入口文件
"type": "module", // 声明模块化方式
"dependencies": {
}
}
对于外部依赖安装,可在 package.json
中声明依赖,函数构建时会根据依赖声明自动安装依赖包。如果目录下存在 package-lock.json
等 lock 文件,可以起到锁定依赖版本的作用。
对于通过云托管部署的云函数,构建过程支持 npm
、yarn
、pnpm
包依赖管理工具,会识别代码使用的包管理工具进行依赖安装。
注:目前不支持关闭云端安装依赖。
函数日志
在函数代码中打印的日志可以分为两类,一类是函数实例级别日志,即主入口函数之外的日志,这部分日志在函数实例启动时、模块被加载时被打印,跟某次具体的请求无关;另一类是请求级别日志,即某次请求触发、代码执行流程进入主入口函数内部之后才会打印的日志,这部分日志往往跟某次具体的请求相关联,一般可以通过请求的 eventID
搜索得到。
请求级别的日志从内容上划分,又可以分为以下几类:
- 每次请求自动打印的结构化访问日志(access log),包括该次请求的基本信息,如处理请求的主机名、访问的 URL、请求方法、响应结果、请求处理耗时等;
- 函数代码中主动打印的日志,即代码中使用
console.log
等方法打印的日志,这部分日志会跟eventID
所关联,有助于排查某次请求的具体执行流程; - 函数框架捕获的异常日志,如
UncaughtException
和UnhandledRejection
,这部分日志会自动跟eventID
关联,并打印出异常的堆栈信息等便于排查问题。
日志的逻辑级别如下所示:
函数日志
├── 函数实例日志
├── 请求级别日志
│ ├── 访问日志
│ ├── 代码中打印的日志
│ ├── 框架捕获的异常日志
例如下面的函数代码:
// 函数实例日志
console.log("func initialization started.")
// 函数实例日志
setTimeout(() => {
console.log("timeout log.")
}, 1000)
exports.main = function(event, context) {
// 请求级别日志
console.log({ event, context })
setTimeout(() => { throwReject() }, 1000)
return "Hello world"
}
async function throwReject() {
// 这里会触发未捕获的异常,因为没有捕获,会被框架捕获
// 该异常在 main 中 setTimeout 中抛出,因此不会体现在 main 的返回值中
// 该异常由 函数框架捕获并打印到日志中
Promise.reject(new Error('This is an error.'))
}
// 函数实例日志
console.log("func initialization finish.")
在函数实例启动时,App started.
会被打印。请求到达并处理完毕后,客户端会收到 Hello world
响应,1s 后该请求抛出异常被框架捕获,这次请求的访问日志形如:
{
"@timestamp": "2024-xx-xxTxx:xx:xx.xxxZ", // 日志打印的时间戳
"startAt": "2024-xx-xxTxx:xx:xx.xxxZ", // 请求开始处理的时间
"endAt": "2024-xx-xxTxx:xx:xx.xxxZ", // 请求处理结束的时间
"logType": "accesslog", // 日志类型
"hostname": "host", // 处理请求的主机名
"pid": 13506, // 进程 ID
"version": "x.y.z", // 函数框架版本
"nodeEnv": "", // Node.js 相关的环境变量
"tcbEnv": "", // 云开发相关的环境变量
"eventId": "b0900934-79d7-4441-856f-dd46392a5f91", // 本次请求的 eventID
"method": "GET", // 请求方法
"url": "http://127.0.0.1/", // 请求 URL
"path": "/", // 请求路径
"status": 200, // 响应状态码
"httphost": "127.0.0.1", // 请求的主机名
"ua": "PostmanRuntime/7.39.0", // 请求的 User-Agent
"connection": "keep-alive",
"contentType": "application/json", // 请求的 Content-Type
"clientIp": "::1", // 请求的客户端 IP
"resSize": 12, // 响应体大小
"resContentType": "text/plain", // 响应的 Content-Type
"timeCost": 11, // 本次请求的总耗时
"userTimeCost": 0 // 从函数主入口开始到从入口返回的耗时
}
随后异常会被框架捕获,可通过 eventID
(b0900934-79d7-4441-856f-dd46392a5f91
) 关联,并打印出如下堆栈:
Unhandled Rejection for: Error: This is an error.
at throwReject (/path/to/your/project/index.js:9:18)
at Timeout._onTimeout (/path/to/your/project/index.js:4:22)
注意:日志对于问题排查是非常重要的,建议在函数代码中适当打印日志,以便排查问题。 但是打印大量日志将会带来其他问题,如日志消耗磁盘空间可能导致磁盘可用空间不足,日志采集组件采集解析文件消耗较多 CPU,日志上报到存储系统会消耗大量网络带宽,存储大量日志导致成本增加,日志写入和查询性能下降延迟高等诸多问题,因此建议适度打印日志。
多函数和函数路由
函数框架支持通过配置文件声明多个函数,并通过请求 path
将不同的请求路由到不同函数上。要使用多函数路由能力,需要添加命名为cloudbase-functions.json
的配置文件,对不同函数进行声明,并进行路由配置。
假设有如下目录结构:
.
├── cloudbase-functions.json # 多函数配置文件
├── index.js # 默认函数入口文件
├── echo # 函数 echo
│ └── echo.js # 函数 echo 入口文件
├── sse # 处理 SSE 请求的函数 sse
│ ├── index.mjs
│ └── package.json
├── sum # 函数 sum
│ ├── index.js
│ ├── package.json
│ └── sum.js
└── ws # 处理 websocket 请求的函数 ws
├── client.mjs
├── index.mjs
└── package.json
在 cloudbase-functions.json
配置文件中,需要对默认函数(index.js
)、函数 echo
、sse
、sum
、ws
共 5 个函数进行声明和路由,配置文件如下:
{
"functionsRoot":".", // 函数根目录,指定为当前目录
"functions":[ // 声明各函数及其入口文件
{
"name":"default", // 声明默认函数,即入口为 index.js 的函数
"directory":"." // 函数目录为当前目录
},
{
"name":"echo", // 声明函数 echo
"directory":"echo", // 声明 echo 函数所在目录为 echo (相对于函数根目录)
"source":"echo.js" // 函数入口文件为 echo.js
},
{
"name":"sum", // 声明函数 sum
"directory":"sum"
},
{
"name":"ws", // 声明函数 ws
"directory":"ws"
},
{
"name":"sse", // 声明函数 sse
"directory":"sse"
}
],
"routes":[ // 声明路由规则
{
"functionName":"sum", // 路由至函数 sum,名称需要与上述定义的函数名称对应
"path":"/sum" // 满足请求 path 前缀匹配 /sum 条件的请求匹配该规则
},
// path 前缀匹配 /echo 的请求路由至函数 echo
{
"functionName":"echo",
"path":"/echo"
},
// path 前缀匹配 /ws 的请求路由至函数 ws
{
"functionName":"ws",
"path":"/ws"
},
// path 前缀匹配 /sse 的请求路由至函数 sse
{
"functionName":"sse",
"path":"/sse"
},
// 请求默认匹配到 default 函数
{
"functionName":"default",
"path":"/"
}
]
}
使用该配置文件启动服务,根据路径的不同,请求会被路由至不同的函数:
- path 前缀匹配
/sum
时,路由至函数 sum - path 前缀匹配
/echo
时,路由至函数 echo - path 前缀匹配
/ws
时,路由至函数 ws - path 前缀匹配
/sse
时,路由至函数 sse - 未能匹配任一规则的请求,路由至函数 default
使用 VSCode 开发时,可以在 .vscode/settings.json
中声明如下配置以获得 cloudbase-functions.json
的 schema 提示:
{
"json.schemas":[
{
"fileMatch":[
"cloudbase-functions.json"
],
"url":"./node_modules/@cloudbase/functions-framework/functions-schema.json"
}
]
}
函数的路由规则为:
- 每一条路由的 path 匹配均为前缀匹配,即
/aa
可以匹配/aa/bb
,/aa/bb/cc
等等 - path 规则中带
/
的和不带/
的路由视为同一条路由,如/aa
和/aa/
视为同一条路由 - 先声明的规则会覆盖后声明的规则,即以第一条声明的作为路由规则
- 如有多条规则匹配,将会命中最长匹配的规则,如有
/a -> functionA
,/a/b -> functionB
规则,那么请求 path/a/b/c
将会匹配至functionB
- 匹配以
/
分隔为最小分段,单个段内不会模糊匹配,如/aaa
不会匹配/a -> functionA
规则
函数示例
引入外部模块
在函数中引入外部模块,例如 lodash
,并使用其提供的方法:
函数代码:
// index.js
const _ = require('lodash')
exports.main = function(event, context) {
return _.kebabCase('Hello world')
}
// package.json
{
"name": "example",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"lodash": "^4.17.21"
}
}
执行该函数后客户端可以收到 hello-world
的响应。
在不同函数间共享代码
利用多函数能力和函数间路由,可以使用一般的模块导入方法,在不同的函数间共享公共模块。
假设有函数 funcA
和 funcB
需要共享一个获取当前时间的方法 now
,有如下目录结构:
.
├── cloudbase-functions.json # 多函数配置文件
├── common # 公共方法目录
│ └── time.js # 公共方法
├── funcA # 函数 A 目录
│ └── index.js
└── funcB # 函数 B 目录
├── package.json # 指定 index.mjs 为入口文件
└── index.mjs
在 time.js
中导出 now
方法:
// common/time.js
exports.now = function () {
return new Date().toLocaleString()
}
在函数 A 和函数 B 代码中直接引入即可:
// funcA/index.js
const now = require('../common/time').now
// funcB/index.mjs
import { now } from '../common/time.js'
在 cloudbase-functions.json
中对不同的函数进行声明:
{
"functionsRoot":".", // 函数根目录,指定为当前目录
"functions":[ // 声明各函数及其入口文件
{
"name":"funcA", // 声明函数 A
"directory":"funcA"
},
{
"name":"funcB", // 声明函数 B
"directory":"funcB",
}
],
"routes":[ // 声明路由规则
{
"functionName":"funcA",
"path":"/a"
},
{
"functionName":"funcB",
"path":"/b"
}
]
}
在函数中路由
可根据 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 推送消息
函数代码:
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
提交表单数据(文件)
函数代码:
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'
注意:
- 文件上传完成后会保存到本地文件,您应该在函数执行结束后删除文件,以免占用过多磁盘空间。
- 上传的文件如需持久化,应该保存到云存储或其他服务中,以免文件丢失,避免保存在本地。
使用 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'
使用 TypeScript
编写函数代码
相比 JavaScript
通过 TypeScript
编写代码可以获得诸多好处,例如:
更好的开发体验
:编辑器可以提供更好的代码补全、重构和导航功能。这使得开发过程更加高效语法特性
:TypeScript
支持最新的ECMAScript
特性,如装饰器、泛型、异步函数等静态类型检查
:可以在编译时捕获类型错误,减少运行时错误,提高代码的可靠性接口和类型别名
:TypeScript
支持接口和类型别名,允许开发者定义复杂的数据结构。这使得代码更加模块化和可重用可读性和可维护性
:通过明确的类型定义,TypeScript
代码通常更易于理解和维护更好的文档
:类型定义本身可以作为文档,帮助开发者理解函数和模块的用法,而不需要额外的文档
推荐使用 TypeScript
编写函数代码,尤其是相对复杂一点儿的项目中。
@cloudbase/functions-typings
提供了云函数的类型定义,可以增强对函数 event
参数类型的约束和感知能力,可以增强对函数返回值的约束能力。
npm install @cloudbase/functions-typings@v-1
如下为使用示例:
import { TcbEventFunction, File, IntegrationResponse } from '@cloudbase/functions-typings'
// GET no boyd
export const main: TcbEventFunction = function(event, context) {
}
// Content-Type: application/json
type jsonEvent = {a: number, b: string}
export const main: TcbEventFunction<jsonEvent> = function(event, context) {
event.a
event.b
}
// Content-Type: multipart/form-data
type formEvent = {str: string, file: File}
export const main: TcbEventFunction<formEvent> = function(event, context) {
event.str
event.file.filepath
}
// Content-Type: application/octet-stream
export const main: TcbEventFunction<Buffer> = function(event, context) {
event.byteLength
}
// 访问 Context 信息
export const main: TcbEventFunction<void, void> = function(event, context) {
context.extendedContext?.envId
context.extendedContext?.userId
}
// 普通响应-无返回值
export const main: TcbEventFunction = function(event, context) {
return
}
// 普通响应-有返回值
export const main: TcbEventFunction<void, string> = function(event, context) {
return 'done.'
}
// 集成响应
export const main: TcbEventFunction<void, IntegrationResponse<string>> = function(event, context) {
return {
statusCode: 200,
headers: {},
body: ''
}
}
// 异步函数
export const main: TcbEventFunction<void, Promise<string>> = async function(event, context) {
return new Promise((resolve) => {
setImmediate(() => {
resolve('done.')
})
})
}
// SSE
export const main: TcbEventFunction<void, Promise<string>> = async function(event, context) {
const sse = context.sse?.()
if (sse && !sse.closed) {
sse.on('close', () => {
console.log('sse closed')
})
sse.send({ data: 'hello from sse function, abcedfg..., €€€€€⭐⭐⭐⭐⭐' })
sse.send({ data: 'message with linebreak symbol:\\nThis is the first line.\\n\\nThis is the second line.\\r\\r\\r\n' })
// 单次发送多个事件
sse.send([
{ data: 'This is the first message.' },
{ data: 'This is the second message, it\n\r\r\n\r\r\rhas two lines.' },
{ data: 'This is the third message.' }
])
// 以下为发送原始消息的示例
// 该方式用于扩展 SSE 协议,例如发送其他 Event Field 字段
// 注意:末尾必须有换行符数据才会立即发送
sse.send('message: This is a raw message. ')
sse.send(['message: This is another raw message.\n\n'])
const msgs = [
'This is a raw message. \n',
'This is another raw message.\n\n'
]
sse.send(msgs)
}
return ''
}
// WebSocket
export const main: TcbEventFunction = async function(event, context) {
if (context.ws) {
context.ws.on('open', (msg) => {
console.log('open: ', msg)
})
context.ws.on('error', (msg) => {
console.log('error: ', msg)
})
context.ws.on('close', (msg) => {
console.log('close: ', msg)
})
context.ws.on('message', (msg) => {
console.log('message: ', msg)
})
context.ws.send('hello from websocket function')
}
}
main.handleUpgrade = async function(context) {
return {
allowWebSocket: true
}
}