t1yOS 云函数

使用 t1yOS 云函数之前,你需要先前往:控制台(console.t1y.net)创建一个应用,并打开该应用,随后在云操作系统页面中打开“文件 App“,并在“文件 App“ 中打开“云函数(.functions)“文件夹,在空白区域右键“新建云函数“,取名“index“,再次右键“在代码编辑器中打开“,再次点击右上角绿色的运行按钮,即可看到云函数执行结果。

当前文档仅仅只是云函数的一些常用用法,对于日常开发已足够,云函数还有非常非常多的功能,请参阅:云函数高阶文档

快速开始

在云函数中引入 context 模块,即可操作 HTTP 请求响应上下文:

const ctx = require('context')

使用 http 模块可以使用云函数已定义常量,如:请求方法、状态码等,详情参阅:常量定义文档

const http = require('http')

文件系统结构

每个应用都有一个独立的文件系统,文件系统相当于一个 Web 服务器(类似 Nginx+PHP),可对外提供静态资源以及云函数访问。所有的云函数都必须放到云函数(.functions)目录下,支持二级目录,文件拓展名必须为 .jsc

├── /(根目录)
│   └── .functions(云函数目录)
│   		└── apps(App Center 中安装的云函数应用)
│   				└── helloworld(示列应用)
│   └── .private(私有目录)
├── index.html

注意:所有的 . 开头的文件或文件夹对外都是禁止访问的,敏感文件可使用隐藏文件命名或放到 .private 目录下,如:微信、支付宝等密钥文件。

Hello, World!

云函数支持 GETPOSTPUTPATCHDELETE 等请求方式,当请求到达云函数时,会触发执行 onRequest 函数,支持返回任意类型的数据。如 ObjectStringNumberBoolean 等,当然云函数还支持自定义状态码、返回文件、JSONJSONPCBORmsgPackXML、重定向、渲染网页等。

function onRequest() {
  return 'Hello, World!'
}

注意:云函数必须放到云函数(.functions)文件夹内,才能生效;并且支持二级目录,与普通 Web 服务器无异,但云函数性能非常强。

getGreeting!

新创建的云函数默认是一个获取问候语的云函数,代码如下所示:

const ctx = require('context')
const http = require('http')

function onRequest() {
  console.log('this is a console message.')
  return {
    code: http.StatusOK,
    message: `Hello ${ctx.query('name', 'WangHua')}, ${getGreeting()}!`,
    data: null,
  }
}

function getGreeting() {
  const hour = new Date().getHours()
  if (hour >= 5 && hour < 12) {
    return 'Good morning'
  } else if (hour >= 12 && hour < 18) {
    return 'Good afternoon'
  } else if (hour >= 18 && hour < 23) {
    return 'Good evening'
  } else {
    return 'Good night'
  }
}
请求云函数访问链接:https://myapp.t1y.net/<YourAppID>/index.jsc?name=Zhangsan
响应 200 OK:
{
  code: 200,
  message: "Hello Zhangsan, Good morning!",
  data: null
}

当你请求该云函数时,将执行 onRequest 入口函数,可以获得 JSON 对象问候响应,你还可以在云操作系统页面中的日志 App中看到 this is a console message. 日志输出。

打印日志

你可以使用 console 中的 loginfodebugwarnerror 打印不同级别的日志,这些日志都能够在云操作系统页面中的日志 App 中看到。例如:

function onRequest() {
  console.log('this is a console message.')
  console.info('this is a console message.')
  console.debug('this is a console message.')
  console.warn('this is a console message.')
  console.error('this is a console message.')
  return null
}

自定义模块

文件:/.functions/pkg/response.jsc

module.exports = {
  fail: (code, message, data) => {
    return { code: code, message: message, data: data }
  },
  success: (message, data) => {
    return { code: http.StatusOK, message: message, data: data }
  },
}

文件:index.jsc

const http = require('http')
const resp = require('/pkg/response.jsc') // 导入云函数模块无需 /.functions/pkg/response.jsc 这样的完整路径,导入 json 文件必须使用完整路径

function onRequest() {
  return resp.success('ok', null)
}
请求:https://myapp.t1y.net/<YourAppID>/index.jsc
响应:{"code": 200, "message": "ok", "data": null}

导入 JSON 文件

const data = require('/.private/data.json') // 导入有效的 JSON 文件,系统会自动转换为 Object 类型

function onRequest() {
  return data.name
}

导入远程 JavaScript 文件和 JSON 文件

云函数支持导入普通的纯 JavaScript 文件,可以使用例如:CryptoJS 等现有库或自己封装,更好的辅助用户开发。同时也支持导入本地的 JavaScript 文件或 JSON 文件。

  • 本地导入时需要完整文件路径
  • 远程导入时首次加载会稍慢(后续缓存访问变快)
  • 注意:无论本地导入还是远程导入都会缓存,如导入文件存在变更,请使用 clearCache 函数清除缓存
// const CryptoJS = require('/.librarys/crypto-js.min.js') // 本地导入
// const PackageJSON = require('/.librarys/package.json') // 本地导入

// 远程导入 JavaScript 和 JSON:
const CryptoJS = require('https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js')
const PackageJSON = require('https://unpkg.com/vue/package.json')

function onRequest() {
  clearCache() // 如果导入的 JavaScript 或者 JSON 文件发生变化可手动清理缓存(需按需调用,文件没有变化的情况下不需要清理,存在缓存访问加载速度会更快)
  return { name: PackageJSON.name, md5: CryptoJS.MD5('Hello, World!').toString() }
}

导入模块时,只有普通的 .js 文件或 .json 文件会被缓存,云函数内置模块以及 .jsc 云函数文件都是预编译的,不会被缓存,修改后会立即生效,推荐使用 .jsc 云函数文件进行封装,预编译,这样执行效率会更快一些。

获取身份标识

使用 ctx.userAgent 函数,你可以获取客户端的身份标识信息,如设备类型等。

const ctx = require('context')

function onRequest() {
  return ctx.userAgent()
}
请求:https://myapp.t1y.net/<YourAppID>/index.jsc
响应:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36

获取客户端 IP

使用 ctx.ip 函数可以获取客户端请求的 IP 地址:

const ctx = require('context')

function onRequest() {
  return ctx.ip()
}

使用 ctx.ips() 函数,可以获取 X-Forwarded-For 请求头中指定的 IP 地址数组。

const ctx = require('context')

function onRequest() {
  return ctx.ips()
}

获取请求方法

t1yOS 云函数支持 GETPOSTPUTPATCHDELETE 等请求方法,因此可使用 method 函数获取。

const ctx = require('context')

function onRequest() {
  return ctx.method()
}
GET 请求:https://myapp.t1y.net/<YourAppID>/index.jsc
响应:GET

POST 请求:https://myapp.t1y.net/<YourAppID>/index.jsc
响应:POST

...

例子:

const ctx = require('context')

function onRequest() {
  if (ctx.method() == 'GET') {
    return 'Hello'
  }
  return '仅允许使用 GET 请求'
}
GET 请求:https://myapp.t1y.net/<YourAppID>/index.jsc
响应:Hello

POST 请求:https://myapp.t1y.net/<YourAppID>/index.jsc
响应:仅允许使用 GET 请求

获取请求头

使用 ctx.get 函数获取请求头,该函数支持默认值(可选),当前获取的请求头不存在时返回默认值。

const ctx = require('context')

function onRequest() {
  return `你的 IP 地址是:${ctx.get('x-forwarded-for')}`
}

默认值:

const ctx = require('context')

function onRequest() {
  return `你的 IP 地址是:${ctx.get('x-forwarded-for', '127.0.0.1')}`
}

除此之外,你还能使用 ctx.getReqHeaders 函数获取全部请求头,该函数返回一个 Object 对象:

const ctx = require('context')

function onRequest() {
  const headers = ctx.getReqHeaders()
  return `你的 IP 地址是:${headers['X-Forwarded-For'][0]}`
}

最后你还能使用 ctx.hasHeader 函数判断一个请求头是否存在。

设置响应头

使用 ctx.set 函数可以将响应头设置为指定的 key 和 value。

const ctx = require('context')

function onRequest() {
  ctx.set('Content-Type', 'text/plain')
  return 'Hello, World!'
}

获取查询参数

获取查询参数所谓的意思就是在请求链接 ? 后的 key-value 值,如:https://myapp.t1y.net/<YourAppID>/index.jsc?name=Zhangsan&age=33&xxx=xxx

const ctx = require('context')

function onRequest() {
  return ctx.query('name')
}
请求:https://myapp.t1y.net/<YourAppID>/index.jsc?name=Zhangsan
响应:Zhangsan

使用 ctx.query 即可获取,该函数支持默认值(可选),如果 name 值不存在时,则返回默认值。

默认值:

const ctx = require('context')

function onRequest() {
  return ctx.query('name', 'WangHua')
}
请求:https://myapp.t1y.net/<YourAppID>/index.jsc
响应:WangHua

请求:https://myapp.t1y.net/<YourAppID>/index.jsc?name=Zhangsan
响应:Zhangsan

除此之外你还能使用 ctx.queries 函数获取全部查询参数,该函数返回一个 Object 对象:

const ctx = require('context')

function onRequest() {
  return ctx.queries().name
}

获取表单数据

使用 ctx.formValue 函数可获取 POST 表单中的数据。

const ctx = require('context')

function onRequest() {
  return ctx.formValue('name')
}

默认值(可选):

const ctx = require('context')

function onRequest() {
  return ctx.formValue('name', 'WangHua')
}

同样,你也可以使用 ctx.isForm 函数判断是不是表单。

获取请求体

除了 GET 请求外,POSTPUTPATCHDELETE 都可以使用 ctx.body() 函数获取请求体中的内容。如果请求体中是有效的 JSON 对象,那么在云函数中会自动转换为 Object 类型。

const ctx = require('context')

function onRequest() {
  if (ctx.method() != 'GET') {
    const body = ctx.body()
    return body.name
  }
  return null
}
请求:https://myapp.t1y.net/<YourAppID>/index.jsc
Body:{"name": "WangHua", "age": 23}
响应:WangHua

例子:

const ctx = require('context')

function onRequest() {
  if (ctx.method() != 'GET') {
    return ctx.body()
  }
  return null
}
请求:https://myapp.t1y.net/<YourAppID>/index.jsc
Body:你好!
响应:你好!

除此之外,你还可以使用 ctx.hasBody 函数判断存不存在请求体,如:

const ctx = require('context')

function onRequest() {
  if (ctx.method() != 'GET') {
    if (ctx.hasBody()) {
      return ctx.body()
    }
    return '请求体不能为空'
  }
  return null
}
请求:https://myapp.t1y.net/<YourAppID>/index.jsc
Body:null
响应:请求体不能为空

另外,你还能使用 ctx.isJSON 函数判断当前请求 Content-Type 是不是 application/json

重定向

使用 ctx.redirect 函数可以将云函数重定向至指定 url

const ctx = require('context')

function onRequest() {
  ctx.redirect(302, 'https://baidu.com/') // 临时重定向
  return null
}

临时重定向(使用 HTTP 状态码常量)

const ctx = require('context')
const http = require('http')

function onRequest() {
  ctx.redirect(http.StatusFound, 'https://baidu.com/')
  return null
}

永久重定向(使用 HTTP 状态码常量)

const ctx = require('context')
const http = require('http')

function onRequest() {
  ctx.redirect(http.StatusMovedPermanently, 'https://baidu.com/')
  return null
}

模板渲染

使用 ctx.render 函数可以将模板渲染为网页。

const ctx = require('context')

function onRequest() {
  ctx.render('/.templates/index.tmpl', {
    Title: 'My Website',
    User: { ID: 1001, Name: 'WangHua', Age: 23 },
    Version: '1.0',
  })
  return null
}
/.templates/index.tmpl
<html>
  <head>
    <title>{{ .Title }}</title>
  </head>
  <body>
    <h1>{{ .User.Name }}</h1>
    <p>ID: {{ .User.ID }}</p>
    <p>Age: {{ .User.Age }}</p>
    <p>Version: {{ .Version }}</p>
  </body>
</html>

请求级存储

你可以使用 ctx.locals 存储和获取当前请求生命周期中的一些值,该存储仅在当前请求生命周期内有效。

const ctx = require('context')

function onRequest() {
  // 存储
  ctx.locals('uid', 123456)
  ctx.locals('name', 'WangHua')
  ctx.locals('age', 23)
  ctx.locals('is_active', true)

  // 获取
  ctx.locals('uid') // 返回 123456
  ctx.locals('name') // 返回 WangHua
  ctx.locals('age') // 返回 23
  ctx.locals('is_active') // 返回 true
  return null
}

Cookie

使用 ctx.cookie 函数可以设置一个 Cookie

可接受的参数:

{
  /** Cookie 的名称 */
  name: string
  /** Cookie 的值 */
  value: string
  /** 允许接收该 Cookie 的 URL 路径 */
  path: string
  /** 允许接收该 Cookie 的域名 */
  domain: string
  /** Cookie 最大有效期(单位:秒) */
  max_age: number
  /** Cookie 过期时间 */
  expires: Date
  /** 是否仅通过 HTTPS 传输(安全连接) */
  secure: boolean
  /** 是否仅允许 HTTP 访问(JS 无法读取) */
  http_only: boolean
  /** 控制是否允许跨站请求携带 Cookie */
  same_site: string
  /** 是否启用分区 Cookie 存储 */
  partitioned: boolean
  /** 是否为会话 Cookie(关闭浏览器即失效) */
  session_only: boolean
}

例子:

const ctx = require('context')

function onRequest() {
  ctx.cookie({ name: 'john', value: 'doe', expires: new Date(Date.now() + 24 * 60 * 60 * 1000) })
  return null
}

使用 ctx.cookies 函数可以获取指定 Cookie,该函数支持默认值(可选):

const ctx = require('context')

function onRequest() {
  // 通过 Key 获取 Cookie:
  ctx.cookies('name') // 'john'
  ctx.cookies('empty', 'doe') // 'doe'
  return null
}

使用 ctx.clearCookie 函数可以清空客户端 Cookie 或指定 Cookie(可选),使 Cookie 过期。

const ctx = require('context')

function onRequest() {
  ctx.ClearCookie() // 清除所有 Cookie
  ctx.ClearCookie('user') // 按名称使特定 Cookie 过期
  ctx.ClearCookie('token', 'session', 'track_id', 'version') // 按名称使多个 Cookie 过期
  return null
}

上传文件

使用 ctx.formFileInfo 可获取上传的文件信息,使用 ctx.saveFile 可保存文件至文件系统。注意:ctx.formFileInfo 返回的是一个 Object 对象,包含 filenamesizeheader

const ctx = require('context')

function onRequest() {
  const fileInfo = ctx.formFileInfo('file')
  ctx.saveFile('file', `/.private/uploads/${fileInfo.filename}`)
  return '上传成功'
}

下载文件

使用 ctx.download 函数可以让浏览器访问时自动触发下载。

const ctx = require('context')

function onRequest() {
  ctx.download('/.private/uploads/w.png') // 下载私有目录中的文件
  return null
}

发送文件

const ctx = require('context')

function onRequest() {
  ctx.sendFile('/.private/uploads/w.png') // 发送私有目录中的文件
  return null
}

XML

使用 ctx.XML 函数可以响应 XML 数据。

const ctx = require('context')

function onRequest() {
  const data = { name: 'WangHua', age: 23 }
  ctx.XML(data)
  return null
}

JSONP

JSONP(JSON with Padding)是一种跨域请求数据的早期解决方案,你可以使用 ctx.jsonp 函数发送数据。

const ctx = require('context')

function onRequest() {
  const data = { name: 'WangHua', age: 23 }
  ctx.jsonp(data)
  return null
}
响应:callback({"name": "WangHua", "age": 23});

自定义 callback:

const ctx = require('context')

function onRequest() {
  const data = { name: 'WangHua', age: 23 }
  ctx.jsonp(data, 'customFunc')
  return null
}
响应:customFunc({"name": "WangHua", "age": 23});

自定义状态码

使用 ctx.sendStatus 函数可以发送自定义的状态码(响应码)。除了使用数字之外,你还可以使用云函数 http 包中已经定义的常量。具体定义请参考:常量定义文档

const ctx = require('context')

function onRequest() {
  ctx.sendStatus(200) // HTTP 200 OK
  return null
}

使用常量:

const ctx = require('context')
const http = require('http')

function onRequest() {
  ctx.sendStatus(http.StatusInternalServerError) // 服务器内部错误
  return null
}

JWT 身份验证

生成 JWT Token:

jwt.generateToken(userId: string, roles: object, expiresAt: number) // number 单位:分钟
const jwt = require('jwt')

function onRequest() {
  const MINUTES_7_DAYS = 7 * 24 * 60 // 7 天有效期
  const token = jwt.generateToken('uid123', ['user', 'super_admin'], MINUTES_7_DAYS)
  return token
}

验证 JWT Token:

const ctx = require('context')
const http = require('http')
const jwt = require('jwt')

function onRequest() {
  const payload = jwt.verifyToken() // 只需调用即可,系统会自动进行判断是否有效
  if (payload.userId != 'uid123') {
    ctx.sendStatus(http.StatusUnauthorized)
    return { code: http.StatusUnauthorized, message: '权限不足', data: null }
  }
  if (!payload.roles.includes('super_admin')) {
    ctx.sendStatus(http.StatusUnauthorized)
    return { code: http.StatusUnauthorized, message: '权限不足', data: null }
  }
  return `Hello, ${payload.userId}!`
}

发送网络请求

云函数可以使用 http 包中的 send 函数发送网络请求。如果响应内容是有效的 JSON 对象,那么会自动转换为 Object 类型。

函数签名:

http.send(method: string, url: string, headers: object, body: string)

函数返回:

调用该函数会返回一个 Object 对象,包含以下内容:

  • status
  • statusCode
  • proto
  • protoMajor
  • protoMinor
  • headers
  • body
  • contentLength

不带请求头和请求体例子(响应是有效 JSON 对象)

const http = require('http')

function onRequest() {
  const resp = http.send('GET', 'https://myapp.t1y.net/timestamp', {}, '')
  if (resp.statusCode != http.StatusOK) {
    return '请求成功,但状态码不是 HTTP 200 OK'
  }
  return resp.body.data.unix
}

带请求头和请求体例子(响应是有效 JSON 对象)

const http = require('http')

function onRequest() {
  const reqBody = { name: 'WangHua', age: '23' }
  const resp = http.send(
    'POST',
    'https://myapp.t1y.net/timestamp',
    { Authorization: 'Bearer <JWT_TOKEN>' },
    JSON.stringify(reqBody)
  )
  if (resp.statusCode != http.StatusOK) {
    return '请求成功,但状态码不是 HTTP 200 OK'
  }
  return resp.body.data.unix
}

环境变量

使用 os 包可以操作环境变量。

获取环境变量:

const os = require('os')

function onRequest() {
  return os.getEnv('API_KEY')
}

设置环境变量:

const os = require('os')

function onRequest() {
  os.setEnv('API_KEY', 'sk-xxx')
  return null
}

取消(删除)环境变量:

const os = require('os')

function onRequest() {
  os.unSetEnv('API_KEY')
  return null
}

文件操作

使用 os 包可以操作文件系统中的文件。

读取文件:

  • 支持 utf-8gb18030gbk 三种编码格式
const os = require('os')

function onRequest() {
  const content = os.readFile('/.private/a.txt', 'utf-8', {})
  return content
}

checksum 校验读取:

const os = require('os')

function onRequest() {
  const opts = {
    verify_checksum: true, // 是否启用校验
    expected_checksum: '5d41402abc4b2a76b9719d911017c592', // 期望 md5(用于一致性验证)
  }
  const content = os.readFile('/.private/a.txt', 'utf-8', opts)
  return content
}

写入文件:

  • opts 结构:
{
  atomic?: boolean, // 是否原子写(默认 true)
  sync?: boolean, // 是否 fsync 落盘
  return_checksum?: boolean // 是否返回 md5
}
  • 函数返回结构:
{
  bytes_written: number,
  checksum: string
}

普通写入文件:

const os = require('os')

function onRequest() {
  const res = os.writeFile('/.private/hello.txt', 'hello world', {})
  return res
}

原子写入(推荐):

const os = require('os')

function onRequest() {
  const res = os.writeFile('/.private/user.json', JSON.stringify({ id: 1, name: 'WangHua' }), {
    atomic: true,
  })
  return res
}

强制落盘:

const os = require('os')

function onRequest() {
  const res = os.writeFile('/.private/log.txt', 'critical log', {
    sync: true,
  })
  return res
}

获取 checksum

const os = require('os')

function onRequest() {
  const res = os.writeFile('/.private/a.txt', 'content', {
    return_checksum: true,
  })
  return res.checksum
}

常用加解密函数

云函数可以使用 crypto 包中提供的常用加解密函数完成诸如:md5sha1sha256hmac_sha256hmac_sha512base64bcryptargon2idaes 等加解密场景下的使用。具体内容请参考:常用加解密函数文档

开启安全模式

云操作系统页面中的设置 App 中可以开启安全模式,开启安全模式请求和响应都需要采用 AES 加密算法进行动态加密,有效防止中间人抓包攻击和重放攻击。开启后有如下特点:

  • 请求和响应的全链路 AES 动态加密,杜绝中间人窃听。

注意:设置 App 中有没有开启安全模式都不会对云函数造成影响(只会对 RESTful API 生效),云函数需手动调用 ctx.enableSafeMode 该函数显式开启,例如:

const ctx = require('context')

function onRequest() {
  ctx.enableSafeMode() // 手动调用时,该云函数的所有请求和响应内容都会加密(请求体需遵循规则进行加密处理,响应体则会自动加密)
  return null
}

验证请求签名

调用 ctx.sign 函数可以对当前的请求进行签名验证,如果签名不正确可以拒绝,该函数和 ctx.enableSafeMode() 函数搭配可以获得最好的效果,但需按需选择。调用该函数验证签名有如下特点:

  • 采用 HMAC 加密算法对每个请求进行签名,确保请求来源可信、内容完整未被篡改。
  • 请求签名仅在 10 秒内有效,超时自动拒绝,有效防御重放攻击。

注意:云函数需手动调用 ctx.sign 函数验证签名,而 RESTful API 则是默认开启且不可关闭,确保请求来源可信、内容完整未被篡改。

const ctx = require('context')

function onRequest() {
  if (!ctx.sign()) {
    return '签名验证失败'
  }
  return '你好!'
}

请求签名和安全模式下的 AES 动态加解密规则见:签名文档