你写的代码,不用买服务器也能跑遍全球

你写的代码,不用买服务器也能跑遍全球

div.bytemd-html-doc{ --ink:#1b2733; --ink-2:#4a5b6c; --line:#e6e2d8; --paper:#fbf9f4; --card:#ffffff; --blue:#3d6f8a; --blue-soft:#eaf1f6; --green:#4f8a6d; --green-soft:#e7f2ec; --orange:#e07a3f; --orange-soft:#fbeee3; --mono:"SFMono-Regular",Consolas,"Liberation Mono","Courier New",monospace; } div.bytemd-html-doc, div.bytemd-html-doc *{box-sizing:border-box;-webkit-text-size-adjust:100%} div.bytemd-html-doc{ margin:0; background:var(--paper); color:var(--ink); font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif; line-height:1.85; font-size:17px; } .wrap{max-width:720px;margin:0 auto;padding:0 20px 80px} /* 顶部封面 */ .cover{ margin:0 -20px 34px; padding:56px 24px 40px; background:linear-gradient(160deg,#2f5570 0%,#3d6f8a 55%,#5a8f86 100%); color:#fff; position:relative; overflow:hidden; } .cover::after{ content:"";position:absolute;right:-60px;top:-60px;width:220px;height:220px; border-radius:50%;background:rgba(255,255,255,.07); } .cover::before{ content:"";position:absolute;left:-40px;bottom:-80px;width:160px;height:160px; border-radius:50%;background:rgba(224,122,63,.28); } .kicker{ display:inline-block;font-size:12px;letter-spacing:2px; background:rgba(255,255,255,.16);padding:5px 12px;border-radius:99px;margin-bottom:18px; } .cover h1{ font-size:31px;line-height:1.35;margin:0 0 14px;font-weight:800;letter-spacing:.5px; } .cover p{margin:0;font-size:15px;opacity:.9} .cover .meta{margin-top:22px;font-size:13px;opacity:.78} /* 目录 */ .toc{ background:var(--card);border:1px solid var(--line);border-radius:16px; padding:18px 20px;margin-bottom:38px; } .toc b{font-size:14px;color:var(--blue);letter-spacing:1px} .toc ol{margin:12px 0 0;padding-left:20px;color:var(--ink-2);font-size:15px} .toc li{margin:6px 0} .toc a{color:var(--ink);text-decoration:none} .toc a:active{color:var(--orange)} h2{ font-size:23px;margin:52px 0 18px;padding-left:14px; border-left:5px solid var(--orange);line-height:1.4; } h3{font-size:18px;margin:30px 0 10px;color:var(--blue)} p{margin:14px 0} .lead{font-size:18px;color:var(--ink)} .lead b{color:var(--orange)} .hero{ background:var(--orange-soft);border-radius:14px;padding:18px 20px; font-size:18px;font-weight:600;color:#8a4a1f;text-align:center;margin:22px 0; border:1px dashed #e0a878; } /* 图片卡 */ .fig{ margin:26px 0;background:#fff;border:1px solid var(--line); border-radius:16px;overflow:hidden; } .fig img{width:100%;display:block} .fig .cap{font-size:13px;color:var(--ink-2);padding:10px 14px 14px;text-align:center} /* 路线对比 */ .route{ display:flex;gap:12px;flex-wrap:wrap;margin:22px 0; } .route .box{ flex:1 1 240px;border-radius:14px;padding:16px 18px;color:#fff; } .route .slow{background:linear-gradient(135deg,#c86a4a,#e07a3f)} .route .fast{background:linear-gradient(135deg,#3d7c6a,#5a8f86)} .route .tag{font-size:12px;opacity:.85;letter-spacing:1px} .route .big{font-size:20px;font-weight:800;margin:8px 0} .route .path{font-size:13px;opacity:.95;line-height:1.7} /* 表格 */ .tbl{overflow-x:auto;margin:20px 0;-webkit-overflow-scrolling:touch} table{border-collapse:collapse;width:100%;font-size:15px;background:var(--card)} th,td{border:1px solid var(--line);padding:11px 13px;text-align:left;vertical-align:top} th{background:var(--blue-soft);color:var(--blue);font-weight:700;white-space:nowrap} tr:nth-child(even) td{background:#faf8f3} /* 代码块 */ pre{ background:#1f2a34;color:#e8eef2;border-radius:14px; padding:18px 16px;overflow-x:auto;margin:20px 0; font-family:var(--mono);font-size:13.5px;line-height:1.75; -webkit-overflow-scrolling:touch; } pre .c{color:#8fa3b0} pre .k{color:#7ec8b0} pre .s{color:#e0b978} pre .n{color:#d99b6a} code.inline{ background:var(--blue-soft);color:var(--blue); padding:2px 7px;border-radius:6px;font-family:var(--mono);font-size:.9em; } /* 步骤链 */ .chain{margin:24px 0} .step{ position:relative;background:var(--card);border:1px solid var(--line); border-radius:14px;padding:16px 18px 16px 60px;margin-bottom:14px; } .step .num{ position:absolute;left:16px;top:16px;width:32px;height:32px;border-radius:50%; background:var(--blue);color:#fff;font-weight:800;font-size:15px; display:flex;align-items:center;justify-content:center; } .step:nth-child(2) .num{background:var(--green)} .step:nth-child(3) .num{background:var(--orange)} .step:nth-child(4) .num{background:#7a6aa0} .step:nth-child(5) .num{background:#c86a4a} .step h4{margin:0 0 4px;font-size:16px} .step p{margin:2px 0 0;font-size:15px;color:var(--ink-2)} /* 两栏 import 对照 */ .cols{display:flex;gap:14px;flex-wrap:wrap;margin:22px 0} .col{flex:1 1 280px;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:16px 18px} .col .badge{ display:inline-block;font-size:12px;color:#fff;padding:3px 10px;border-radius:99px;margin-bottom:10px; } .col.a .badge{background:var(--blue)} .col.b .badge{background:var(--green)} .col h4{margin:4px 0 8px;font-size:16px} .col .verdict{ margin-top:12px;font-size:14px;background:var(--orange-soft); color:#8a4a1f;border-radius:10px;padding:10px 12px; } .note{ background:var(--green-soft);border-left:4px solid var(--green); padding:14px 18px;border-radius:0 12px 12px 0;margin:22px 0;font-size:15.5px; } .warn{ background:#fdf1ec;border-left:4px solid var(--orange); padding:14px 18px;border-radius:0 12px 12px 0;margin:22px 0;font-size:15.5px; } ul{margin:14px 0;padding-left:22px} li{margin:8px 0} .end{ margin-top:54px;text-align:center;padding:26px 18px; background:linear-gradient(160deg,#2f5570,#5a8f86);color:#fff;border-radius:18px; } .end .q{font-size:20px;font-weight:800;margin-bottom:8px} .end .a{font-size:15px;opacity:.92} .foot{text-align:center;color:#9aa7b2;font-size:12.5px;margin-top:34px} a{color:var(--blue)} @media(max-width:480px){ div.bytemd-html-doc{font-size:16.5px} .cover h1{font-size:26px} pre{font-size:12.8px} }
SUPABASE · EDGE FUNCTIONS

你写的代码,
不用买服务器也能跑遍全球

边缘函数 · 写给 TypeScript 新手的零门槛入门

读完带走三件事:它解决什么 · 一次调用走了哪几步 · 那两行 import 为什么这么写
本文目录
  1. 一、先别学语法,搞清楚它为啥存在
  2. 二、它到底解决了哪三件事
  3. 三、一次调用,背后发生了什么
  4. 四、那个「标配开场白」逐行拆
  5. 五、一个能直接跑的最小闭环
  6. 六、别踩这三个坑

一、先别学语法,搞清楚它为啥存在

假设你做了个小程序:用户点一下「生成今日鸡汤」,让他等了 1.2 秒。不是你代码慢,是请求绕了大半个地球。

你的服务器在新加坡,用户在深圳。一次点击的真实路线:

没有边缘函数
≈ 300ms 起
深圳 → 新加坡机房 → 算完 → 深圳
光在路上就花掉大半
用了边缘函数
≈ 50ms
深圳 → 华南节点当场算 → 深圳
谁喊,谁旁边就跑一下
请求路线对比图:绕地球 vs 就近响应
上:请求绕远路的橙色长弧线;下:就近节点响应的绿色短线
边缘函数 = 一份代码撒在全球 CDN 上,谁调用、谁附近的节点就当场算一下再把结果扔回来。

它不是「你的那台服务器」,而是一个共享的、按需现起的运行时池子:没请求不占资源、不花钱;来一个请求就近抓个节点,冷启动几十毫秒,跑完释放。下次再来,可能换另一个节点——所以它无状态,别指望内存里存个变量下次还在。

二、它到底解决了哪三件事

① 距离:让代码贴着用户跑

适合那种每个用户都要跑一遍、逻辑又轻的活:把三四个接口的数据拼成一包返回、A/B 分流、灰度开关、地区与语言初始化、算个签名。

冷启动快 + 离人近,这是数据库和普通后端都平替不了的。

② 脏活:有些事数据库「不该干,也干不了」

脏活为啥不能全丢给数据库
调微信支付 / OpenAI / 发短信邮件要往外发请求,密钥不能露
会变的业务规则SQL 写起来想哭,改一次要迁移
接 Webhook 回调要验签、要异步、要重试
给用户返回「加工过」的数据前端不该自己拼,后端又懒得搭

边缘函数就是数据库和第三方之间的胶水层:密钥塞进 Supabase 的 Secrets,代码里只管 fetch。

③ 运维:不为一小段逻辑养一台机器

为了「注册完发封邮件」你要干嘛?买机器、装 Node、配 nginx、守护进程、监控、扩容、半夜看告警……

# 就这一句
supabase functions deploy send-welcome-email

完了。没流量 0 成本,涨了它自己扛。

判断口诀:查数据走库,脏活秘密走函数,重活(转视频、长任务)走正经服务器。

三、一次调用,背后到底发生了什么

这张图从上往下读,就是完整链路:

五段链路解剖图:fetch到就近节点到Deno沙箱到带身份查库到Response
前端 fetch → DNS 就近节点 → Deno 沙箱跑函数 → 带用户身份查库(RLS) → Response 回家
1

前端 fetch 一下函数 URL

Supabase 给你的地址,形如 https://项目.functions.supabase.co/hello。记得带 apikey 头,那是门禁。

2

DNS 把你甩到最近的节点

深圳进华南、纽约进美东。这事你一行代码都不用写,平台自动选。

3

Deno 沙箱当场跑你的函数

它默认啥也碰不到:不能读硬盘、不能乱联网,想干嘛得显式开权限。密钥从后台 Secrets 运行时注入,代码扒走了也扒不到 sk_live_xxx。

4

以「调用者本人」的身份查库

函数跑在服务端,但行级权限(RLS)得知道「现在是谁在点」。把前端 Authorization 令牌原样透传过去,RLS 一看:是小明本人,该看的放行。

5

Response 飞回前端

全程常在一百毫秒内,吐一个 JSON 完事。

四、那个「标配开场白」,逐行拆给你

你迟早会看到这两行。先说个反直觉的:第一行经常可以没有。

// 你常在模板里看到的「两行标配」
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
两行import图解:第一行起接口,第二行接数据库
上栏:第一行起接口;下栏:第二行拿一把「带身份的遥控器」去碰 Supabase
第一行 · 起接口

其实多半是多余的

Deno.serve(handler) 是运行时内置的全局 API,跟浏览器里的 fetch、console 一样,不用引。

边缘函数 99% 这么写就够,别引。只有你想捏端口、TLS 证书、生命周期(close/onListen)才去引 std 版。
第二行 · 接数据库

只要碰库,必写

Deno 不玩 npm install、没有 node_modules,依赖直接从网址拉。这一个包进来,其实是五合一。

auth 鉴权 · postgrest 查库 · realtime 订阅 · storage 文件 · functions 调别的函数,一把全到手。

createClient 三个参数,各有各的活

const supabase = createClient(
  Deno.env.get("SUPABASE_URL")!,      // 你项目的地址
  Deno.env.get("SUPABASE_ANON_KEY")!, // 公开钥匙,能放前端
  {
    global: {
      headers: { Authorization: req.headers.get("Authorization")! },
    },
  }
);
参数是啥为什么放这
URL项目地址告诉客户端往哪打请求
ANON_KEY公开钥匙负责「进门」,真正的锁是 RLS
Authorization 透传用户的登录令牌让查库「装得成那个人」
密钥负责让函数 进得了门,令牌负责让查库 装得成那个人。

五、一个能直接跑的最小闭环

新建 supabase/functions/hello/index.ts,整段贴进去:

Deno.serve(async (req) => {

  // 1️⃣ 只接 GET,别的姿势直接轰走
  if (req.method !== "GET") {
    return new Response("只收 GET 哦", { status: 405 });
  }

  // 2️⃣ 抠出 ?name=xxx,没有就默认「路人」
  const name = new URL(req.url).searchParams.get("name") ?? "路人";

  // 3️⃣ 读密钥:代码里没明文,只在后台 Secrets 配
  const apiKey = Deno.env.get("MY_SECRET") ?? "没配置密钥";

  // 4️⃣ 拼结果(密钥只露头四位,好习惯)
  const payload = {
    msg: `嗨 ${name},你刚 hit 到了边缘函数!`,
    time: new Date().toISOString(),
    node: "离你最近的那个节点,平台自动选",
    keyPreview: apiKey.slice(0, 4) + "****",
  };

  // 5️⃣ 回 JSON + 跨域头(不加浏览器会拦)
  return new Response(JSON.stringify(payload), {
    headers: {
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
    },
  });
});
行人话
Deno.serve(...)接口入口,来一个请求跑一次里面的函数
req.method门卫,只放 GET
new URL(req.url)拆 ?name=小明 这种参数
Deno.env.get(...)从后台注入密钥,前端永远摸不到
slice(0,4)+"****"就算返回也打码
new Response(...)标准答法,浏览器那套,零学习成本
Access-Control-Allow-Origin跨域通行证,本地调试最常卡这

跑起来,三步

# 1. 本地起,浏览器访问 localhost:54321/functions/v1/hello?name=小明
supabase functions serve hello

# 2. 推到云端,拿到真实 URL
supabase functions deploy hello

# 3. 后台 Settings → Edge Functions → Secrets 加一条 MY_SECRET

前端调它,跟你平时 fetch 没有任何区别:

const res = await fetch(
  "https://你的项目.functions.supabase.co/hello?name=小明",
  { headers: { apikey: "你的 SUPABASE_ANON_KEY" } }
);
const data = await res.json();
console.log(data.msg); // 嗨 小明,你刚 hit 到了边缘函数!

六、别踩这三个坑

1. 别当持久服务器用。无状态、跑完就散,别指望全局变量活着,也别往里塞长任务。
2. 密钥永远不进代码。一律 Deno.env.get,返回前先打码。
3. 查库一定透传 Authorization。不然 RLS 把你当匿名,查个空。
所以,它到底是个啥?
就是 Supabase 给你的一块「能跑我函数的地方」。你交代码,它管跑、管分发、管活着 —— 你没买服务器,却真的有人帮你部署了。区别只是:你没有「那台机器」,能控制的只有 代码 + 密钥 + 路由。
起个口子用 Deno.serve,碰数据库加 createClient,剩下的,交给离用户最近的那个节点。
全文完 · 代码均可直接复制运行 · 配图为示意插图,用于辅助理解链路