Val Town 云函数平台完全指南¶
为什么要学 Val Town¶
即写即部署,零配置:Val Town 让你在浏览器中写一个函数,保存即部署。不需要配置服务器、Docker、CI/CD、域名。每个函数(val)自动获得一个 HTTPS 端点。从想法到上线可以在 30 秒内完成。
社交编程,代码即社区:Val Town 上的每个 val 默认公开,可以被其他人直接引用和组合。看到别人写的有用函数?直接 import 使用。这种"代码社交网络"让你能快速站在他人肩膀上构建。
内置持久化与调度:自带 SQLite 数据库(Turso)、Blob 存储、定时任务(Cron)、邮件收发。不需要注册外部服务就能构建完整的小型应用。
TypeScript/Deno 运行时:运行在 Deno 环境中,原生支持 TypeScript、ESM import、Web Standard API(fetch、Request、Response)。不需要 npm install,直接 URL import。
适合自动化和原型验证:Webhook 接收、API 聚合、定时爬虫、Slack/Discord 机器人、邮件自动化——这些"胶水代码"场景用 Val Town 几分钟就能搞定,不值得为它们建一个完整项目。
核心概念详解¶
Val Town 是什么(白话解释)¶
Val Town 是一个"代码便利贴"平台。你写一小段代码(叫做 val),平台自动帮你运行它。val 可以是: - 一个 HTTP 端点(别人访问 URL 就执行你的代码) - 一个定时任务(每分钟/每小时/每天自动执行) - 一个邮件处理器(收到邮件就执行) - 一个普通函数(给其他 val 调用)
你不需要管服务器、部署、域名。写完就能用。
Val 的四种类型¶
| 类型 | 用途 | 触发方式 | 示例 |
|---|---|---|---|
| HTTP | Web 端点 / API | 通过 URL 访问 | REST API、Webhook 接收器 |
| Cron | 定时任务 | 按计划自动执行 | 定时爬虫、监控告警 |
| 邮件处理 | 收到邮件时执行 | 邮件转发、自动回复 | |
| Script | 普通函数 | 被其他 val 调用 | 工具函数、库 |
运行环境¶
- 运行时:Deno(V8 引擎 + Rust)
- 语言:TypeScript / JavaScript
- 标准 API:fetch、Request、Response、URL、crypto 等 Web Standards
- 导入方式:ESM URL import(来自 npm、esm.sh、deno.land 等)
- 执行限制:每次执行最长 30 秒(免费版),内存 512MB
- 存储:SQLite(通过 Turso)、Blob 存储、环境变量
Val Town vs Cloudflare Workers vs AWS Lambda 对比¶
| 特性 | Val Town | Cloudflare Workers | AWS Lambda |
|---|---|---|---|
| 开发体验 | 浏览器内编辑+部署 | Wrangler CLI | SAM/CDK CLI |
| 部署速度 | 即时(保存即部署) | ~30秒 | ~1-5分钟 |
| 运行时 | Deno | V8 (workerd) | 多种 |
| 语言 | TS/JS | TS/JS/Rust/Wasm | 多种 |
| 冷启动 | 极低 | ~0ms(V8隔离) | 100ms-数秒 |
| 存储 | 内置SQLite+Blob | KV/D1/R2 | DynamoDB/S3 |
| 定时任务 | 内置Cron | Cron Triggers | EventBridge |
| 社交/共享 | 核心特性 | 无 | 无 |
| 适合场景 | 原型/自动化/小工具 | 边缘计算/API | 企业级后端 |
| 价格 | 免费层慷慨 | 免费10万次/天 | 免费100万次/月 |
| 限制 | 30s超时/512MB | 10ms CPU(免费) | 15分钟 |
安装与配置¶
注册使用¶
- 访问 https://val.town 注册账号(支持 GitHub 登录)
- 进入编辑器即可开始写代码
- 无需安装任何本地工具
Val Town CLI(可选)¶
# 安装 CLI
npm install -g @valtown/vt
# 登录
vt login
# 拉取你的 vals
vt pull
# 推送更改
vt push
# 本地运行 val
vt run myVal.ts
环境变量设置¶
在 Val Town 网站的 Settings → Environment Variables 中添加:
在代码中使用:
导入其他人的 Val¶
// 导入其他用户的 val
import { myFunction } from "https://esm.town/v/username/valName";
// 导入 npm 包
import OpenAI from "npm:openai";
import { Hono } from "npm:hono";
import lodash from "npm:lodash";
// 导入标准库
import { email } from "https://esm.town/v/std/email";
import { blob } from "https://esm.town/v/std/blob";
import { sqlite } from "https://esm.town/v/std/sqlite";
快速上手:5 分钟最小示例¶
示例一:Hello World HTTP 端点¶
在 Val Town 编辑器中新建一个 HTTP val:
export default function(req: Request): Response {
const url = new URL(req.url);
const name = url.searchParams.get("name") || "World";
return new Response(`Hello, ${name}!`, {
headers: { "Content-Type": "text/plain" },
});
}
保存后自动获得 URL:https://username-valname.web.val.run?name=张三
示例二:JSON API¶
export default async function(req: Request): Promise<Response> {
if (req.method === "GET") {
return Response.json({
message: "欢迎使用 Val Town API",
timestamp: new Date().toISOString(),
endpoints: ["/api/users", "/api/posts"],
});
}
if (req.method === "POST") {
const body = await req.json();
return Response.json({
received: body,
processedAt: new Date().toISOString(),
}, { status: 201 });
}
return new Response("Method not allowed", { status: 405 });
}
示例三:定时任务¶
新建一个 Cron val,设置执行频率(如每小时):
import { email } from "https://esm.town/v/std/email";
export default async function() {
const res = await fetch("https://api.github.com/repos/denoland/deno/releases/latest");
const release = await res.json();
console.log(`Latest Deno release: ${release.tag_name}`);
// 如果有新版本,发邮件通知
if (release.tag_name !== "v1.40.0") {
await email({
to: "you@example.com",
subject: `Deno 新版本: ${release.tag_name}`,
text: `Deno 发布了新版本 ${release.tag_name}!\n${release.html_url}`,
});
}
}
进阶用法¶
场景一:使用 SQLite 数据库¶
import { sqlite } from "https://esm.town/v/std/sqlite";
// 创建表
await sqlite.execute(`
CREATE TABLE IF NOT EXISTS visitors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip TEXT,
path TEXT,
visited_at TEXT DEFAULT (datetime('now'))
)
`);
export default async function(req: Request): Promise<Response> {
const url = new URL(req.url);
// 记录访问
await sqlite.execute({
sql: "INSERT INTO visitors (ip, path) VALUES (?, ?)",
args: [req.headers.get("x-forwarded-for") || "unknown", url.pathname],
});
// 查询统计
const stats = await sqlite.execute(`
SELECT path, COUNT(*) as count
FROM visitors
GROUP BY path
ORDER BY count DESC
LIMIT 10
`);
return Response.json({
totalVisits: stats.rows.length,
topPages: stats.rows,
});
}
场景二:Webhook 接收 + Slack 通知¶
export default async function(req: Request): Promise<Response> {
if (req.method !== "POST") {
return new Response("Send POST", { status: 405 });
}
const payload = await req.json();
// 处理 GitHub webhook
const event = req.headers.get("x-github-event");
if (event === "push") {
const message = `🔔 *${payload.repository.full_name}*\n` +
`${payload.pusher.name} pushed ${payload.commits.length} commit(s) to ${payload.ref}\n` +
payload.commits.map((c: any) => `• ${c.message}`).join("\n");
// 转发到 Slack
await fetch(Deno.env.get("SLACK_WEBHOOK_URL")!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: message }),
});
}
return new Response("OK");
}
场景三:Hono 框架构建多路由 API¶
import { Hono } from "npm:hono";
import { cors } from "npm:hono/cors";
const app = new Hono();
app.use("*", cors());
app.get("/", (c) => c.json({ message: "Val Town Hono API" }));
app.get("/users", async (c) => {
const { sqlite } = await import("https://esm.town/v/std/sqlite");
const result = await sqlite.execute("SELECT * FROM users LIMIT 20");
return c.json(result.rows);
});
app.post("/users", async (c) => {
const body = await c.req.json();
const { sqlite } = await import("https://esm.town/v/std/sqlite");
await sqlite.execute({
sql: "INSERT INTO users (name, email) VALUES (?, ?)",
args: [body.name, body.email],
});
return c.json({ success: true }, 201);
});
app.get("/users/:id", async (c) => {
const id = c.req.param("id");
const { sqlite } = await import("https://esm.town/v/std/sqlite");
const result = await sqlite.execute({
sql: "SELECT * FROM users WHERE id = ?",
args: [id],
});
if (result.rows.length === 0) return c.json({ error: "Not found" }, 404);
return c.json(result.rows[0]);
});
export default app.fetch;
场景四:AI 聊天 API¶
import OpenAI from "npm:openai";
const openai = new OpenAI({ apiKey: Deno.env.get("OPENAI_API_KEY") });
export default async function(req: Request): Promise<Response> {
if (req.method !== "POST") {
return Response.json({ error: "POST only" }, { status: 405 });
}
const { message, history = [] } = await req.json();
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: "你是一个有帮助的助手。用中文回答。" },
...history,
{ role: "user", content: message },
],
max_tokens: 1000,
});
return Response.json({
reply: completion.choices[0].message.content,
usage: completion.usage,
});
}
场景五:网站可用性监控¶
import { email } from "https://esm.town/v/std/email";
import { sqlite } from "https://esm.town/v/std/sqlite";
const SITES = [
{ name: "主站", url: "https://example.com" },
{ name: "API", url: "https://api.example.com/health" },
{ name: "文档", url: "https://docs.example.com" },
];
// Cron val:每5分钟执行
export default async function() {
await sqlite.execute(`
CREATE TABLE IF NOT EXISTS uptime_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
site TEXT, status INTEGER, latency INTEGER,
checked_at TEXT DEFAULT (datetime('now'))
)
`);
const results = [];
for (const site of SITES) {
const start = Date.now();
try {
const res = await fetch(site.url, { signal: AbortSignal.timeout(10000) });
const latency = Date.now() - start;
const status = res.status;
results.push({ ...site, status, latency, ok: status < 400 });
await sqlite.execute({
sql: "INSERT INTO uptime_checks (site, status, latency) VALUES (?, ?, ?)",
args: [site.name, status, latency],
});
if (status >= 400) {
await email({
to: "admin@example.com",
subject: `⚠️ ${site.name} 异常: HTTP ${status}`,
text: `${site.url} 返回 HTTP ${status},延迟 ${latency}ms`,
});
}
} catch (err) {
results.push({ ...site, status: 0, latency: -1, ok: false, error: err.message });
await email({
to: "admin@example.com",
subject: `🔴 ${site.name} 不可达`,
text: `${site.url} 无法连接: ${err.message}`,
});
}
}
console.log("检查结果:", JSON.stringify(results, null, 2));
}
场景六:Blob 存储 + 图片处理¶
import { blob } from "https://esm.town/v/std/blob";
export default async function(req: Request): Promise<Response> {
const url = new URL(req.url);
if (req.method === "POST") {
// 上传文件
const formData = await req.formData();
const file = formData.get("file") as File;
if (!file) return Response.json({ error: "No file" }, { status: 400 });
const key = `uploads/${Date.now()}-${file.name}`;
const buffer = await file.arrayBuffer();
await blob.set(key, new Uint8Array(buffer));
return Response.json({ key, size: file.size, type: file.type });
}
if (req.method === "GET") {
// 获取文件
const key = url.searchParams.get("key");
if (!key) {
// 列出所有文件
const files = await blob.list("uploads/");
return Response.json(files);
}
const data = await blob.get(key);
if (!data) return new Response("Not found", { status: 404 });
return new Response(data, {
headers: { "Content-Type": "application/octet-stream" },
});
}
return new Response("Method not allowed", { status: 405 });
}
场景七:邮件处理器¶
// Email val:当收到邮件时执行
import { sqlite } from "https://esm.town/v/std/sqlite";
export default async function(email: {
from: string;
to: string[];
subject: string;
text: string;
html: string;
}) {
console.log(`收到邮件 from: ${email.from}, subject: ${email.subject}`);
// 保存到数据库
await sqlite.execute({
sql: `INSERT INTO received_emails (sender, subject, body, received_at)
VALUES (?, ?, ?, datetime('now'))`,
args: [email.from, email.subject, email.text],
});
// 自动回复
const { email: sendEmail } = await import("https://esm.town/v/std/email");
if (email.subject.toLowerCase().includes("unsubscribe")) {
await sendEmail({
to: email.from,
subject: "Re: " + email.subject,
text: "您已成功取消订阅。",
});
}
}
场景八:静态网站托管¶
export default function(req: Request): Response {
const html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>我的 Val Town 网站</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.card { background: white; padding: 3rem; border-radius: 1rem; box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 500px; text-align: center; }
h1 { margin-bottom: 1rem; color: #333; }
p { color: #666; line-height: 1.6; }
a { color: #667eea; text-decoration: none; }
</style>
</head>
<body>
<div class="card">
<h1>欢迎来到 Val Town</h1>
<p>这个页面完全由一个 Val 函数提供。</p>
<p>没有服务器配置,没有部署流程。</p>
<p><a href="https://val.town">了解更多 →</a></p>
</div>
</body>
</html>`;
return new Response(html, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
常见问题与排错¶
问题一:30 秒超时¶
症状:Val 执行超过 30 秒被中断。
解决: - 优化代码,减少不必要的请求 - 使用 AbortSignal.timeout() 为外部请求设置超时 - 拆分为多个 val,用 Cron 串联 - 升级付费版获得更长超时
问题二:环境变量不生效¶
确认在 Val Town 网站 Settings → Secrets 中正确设置。注意变量名区分大小写。
问题三:import 失败¶
// 错误:CommonJS 风格
const express = require("express"); // Deno 不支持
// 正确:ESM import
import express from "npm:express";
// npm 包需要 npm: 前缀
import _ from "npm:lodash";
import { z } from "npm:zod";
问题四:CORS 问题¶
export default async function(req: Request): Promise<Response> {
// 处理 preflight
if (req.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
const response = Response.json({ data: "..." });
response.headers.set("Access-Control-Allow-Origin", "*");
return response;
}
问题五:SQLite 查询语法¶
Val Town 的 SQLite 使用 Turso (libSQL)。注意参数化查询的写法:
// 正确:使用参数化防止 SQL 注入
await sqlite.execute({
sql: "SELECT * FROM users WHERE name = ? AND age > ?",
args: ["张三", 18],
});
// 错误:字符串拼接(SQL 注入风险)
await sqlite.execute(`SELECT * FROM users WHERE name = '${name}'`);
问题六:如何调试¶
// console.log 输出会显示在 Val Town 的 Evaluations 面板
console.log("debug:", someVariable);
console.error("error:", errorMessage);
// 使用 try-catch 捕获详细错误
try {
const result = await riskyOperation();
} catch (err) {
console.error("操作失败:", err.message, err.stack);
return Response.json({ error: err.message }, { status: 500 });
}
参考资源¶
- 官方网站:https://val.town
- 官方文档:https://docs.val.town
- 示例集合:https://val.town/examples
- Val Town Blog:https://blog.val.town
- GitHub:https://github.com/val-town
- 社区 Discord:https://discord.gg/valtown
- 标准库:https://www.val.town/v/std
- Deno 文档:https://deno.land/manual(Val Town 底层运行时)