跳转至

Val Town 云函数平台完全指南

为什么要学 Val Town

  1. 即写即部署,零配置:Val Town 让你在浏览器中写一个函数,保存即部署。不需要配置服务器、Docker、CI/CD、域名。每个函数(val)自动获得一个 HTTPS 端点。从想法到上线可以在 30 秒内完成。

  2. 社交编程,代码即社区:Val Town 上的每个 val 默认公开,可以被其他人直接引用和组合。看到别人写的有用函数?直接 import 使用。这种"代码社交网络"让你能快速站在他人肩膀上构建。

  3. 内置持久化与调度:自带 SQLite 数据库(Turso)、Blob 存储、定时任务(Cron)、邮件收发。不需要注册外部服务就能构建完整的小型应用。

  4. TypeScript/Deno 运行时:运行在 Deno 环境中,原生支持 TypeScript、ESM import、Web Standard API(fetch、Request、Response)。不需要 npm install,直接 URL import。

  5. 适合自动化和原型验证:Webhook 接收、API 聚合、定时爬虫、Slack/Discord 机器人、邮件自动化——这些"胶水代码"场景用 Val Town 几分钟就能搞定,不值得为它们建一个完整项目。


核心概念详解

Val Town 是什么(白话解释)

Val Town 是一个"代码便利贴"平台。你写一小段代码(叫做 val),平台自动帮你运行它。val 可以是: - 一个 HTTP 端点(别人访问 URL 就执行你的代码) - 一个定时任务(每分钟/每小时/每天自动执行) - 一个邮件处理器(收到邮件就执行) - 一个普通函数(给其他 val 调用)

你不需要管服务器、部署、域名。写完就能用。

Val 的四种类型

类型用途触发方式示例
HTTPWeb 端点 / API通过 URL 访问REST API、Webhook 接收器
Cron定时任务按计划自动执行定时爬虫、监控告警
Email邮件处理收到邮件时执行邮件转发、自动回复
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 TownCloudflare WorkersAWS Lambda
开发体验浏览器内编辑+部署Wrangler CLISAM/CDK CLI
部署速度即时(保存即部署)~30秒~1-5分钟
运行时DenoV8 (workerd)多种
语言TS/JSTS/JS/Rust/Wasm多种
冷启动极低~0ms(V8隔离)100ms-数秒
存储内置SQLite+BlobKV/D1/R2DynamoDB/S3
定时任务内置CronCron TriggersEventBridge
社交/共享核心特性
适合场景原型/自动化/小工具边缘计算/API企业级后端
价格免费层慷慨免费10万次/天免费100万次/月
限制30s超时/512MB10ms CPU(免费)15分钟

安装与配置

注册使用

  1. 访问 https://val.town 注册账号(支持 GitHub 登录)
  2. 进入编辑器即可开始写代码
  3. 无需安装任何本地工具

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 中添加:

OPENAI_API_KEY=sk-...
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
DATABASE_URL=libsql://...

在代码中使用:

const apiKey = Deno.env.get("OPENAI_API_KEY");

导入其他人的 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 中正确设置。注意变量名区分大小写。

// 正确方式
const key = Deno.env.get("API_KEY");
if (!key) throw new Error("API_KEY not set");

问题三: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 底层运行时)