这篇文章记录一次很“工程味”的小实践:在 阿里云 ESA 上,用 Pages 静态站点 + Functions 做一个极简的 IP 检测页面。
目标不是做复杂的“探针矩阵”,而是更务实地告诉访问者:
- 本次访问更偏向 IPv4 还是 IPv6
- 如果是 IPv6:提示「恭喜,你能访问纯 IPv6 网站」
- 同时展示 IP 归属地,并按归属地打出一条热情招呼
这非常适合放在
test-ipv6.jsw.ac.cn这种测试站点上,用来快速判断访客的网络环境。
一、准备:你需要什么
- 一个 ESA 的站点(Pages 项目)
- 域名已在 ESA 里做 NS 接入(托管 DNS)
- 一个绑定到 Pages 项目的域名,例如:
test-ipv6.jsw.ac.cn
为什么推荐 NS 接入?
因为后续你在 ESA 控制台里改 DNS、开代理加速、绑域名,同时便于ESA能够自动申请SSL证书。
二、先弄清楚:域名绑定 vs 路由
在 ESA 里,把请求导向 Pages/Functions,常见有两种入口:
1)域名绑定:一个域名整体交给 Pages,Functions
适合这种“纯 Pages/Functions 站点”:
/是静态页面/api/ip是函数接口- 其他路径要么静态资源,要么 404
优点:配置简单、心智负担小
缺点:它是“整站接管”,不适合你还要回源跑传统网站的场景
2)路由:只接管某些路径,其余仍走回源
适合混合架构:
/api/*交给 Functions- 其他路径继续走源站
坑点:如果你在 NS 托管下使用“函数路由”,你依然需要:
- 为那个域名补齐 DNS 记录
- 并开启 代理/加速(否则可能根本不会进入 ESA 的边缘链路)
本项目搭建用的是路由,因为能够让跟随站点管理同步启用全球ipv6
三、在 ESA 上创建 Pages 项目
你可以直接复用现有 Pages 项目,只要满足:
- 能部署
public/index.html - 能部署 Functions(例如
src/index.js) - Functions 能响应
/api/ip
四、绑定路由:test-ipv6.jsw.ac.cn
在 ESA 控制台中找到你的 Pages 项目,进行 绑定路由:
- 添加
test-ipv6.jsw.ac.cn并启用
如果你用的是 NS 托管,一般 ESA 会给你 DNS 编辑入口。此处重点是
- 只需要确保
test-ipv6.jsw.ac.cn有解析记录(A / AAAA / CNAME 其一),这边建议可以填A 记录 192.0.2.1 - 并且 代理已启用
五、实现 Functions:/api/ip
我们需要的能力很少:
- 从 ESA 的请求对象里拿到“本次连接的客户端 IP”
- 用一个简单的函数判断 IPv4/IPv6
- 把
request.info中的归属地信息透传给前端 - 响应禁用缓存,避免边缘缓存造成“别人看到你的 IP”
下面是一个可直接用的 src/index.js:
// src/index.js
function familyOf(ip) {
if (!ip) return "unknown";
if (ip.includes(":")) return "ipv6";
if (ip.includes(".")) return "ipv4";
return "unknown";
}
function baseHeaders() {
return {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store, max-age=0",
"x-content-type-options": "nosniff",
"referrer-policy": "no-referrer",
"permissions-policy": "geolocation=()",
};
}
function json(data, status = 200) {
return new Response(JSON.stringify(data, null, 2), {
status,
headers: baseHeaders(),
});
}
export default {
async fetch(request) {
const url = new URL(request.url);
// 只开放 /api/ip
if (request.method !== "GET" || url.pathname !== "/api/ip") {
return new Response("Not Found", { status: 404, headers: { "cache-control": "no-store" } });
}
// ESA 边缘运行时提供的 info 对象(重点:remote_addr)
const info = request.info || {};
const ip = info.remote_addr || "";
const family = familyOf(ip);
return json({
ok: true,
host: url.hostname,
time: new Date().toISOString(),
ip,
family,
// 透传:常见字段如 ip_city_en / ip_region_en / ip_country_en / ip_isp_en 等
info,
});
},
};
关于 request.info:为什么它比 X-Forwarded-For 更“稳”
很多人第一反应是读请求头 X-Forwarded-For,但在边缘/代理链路里它可能包含多个地址,甚至混入节点地址,容易误判。
ESA 运行时提供的 request.info 通常包含:
remote_addr:本次连接看到的客户端地址(最有用)remote_portip_city_en / ip_region_en / ip_country_en / ip_isp_en等归属地信息
因此这次实践的策略是:前端只相信 /api/ip 输出的 ip + family + info。
六、前端:展示
前端采用三文件分离:
public/index.html:页面骨架public/app.css:布局样式public/app.js:请求/api/ip,按浏览器语言自动展示(不提供手动切换)
你可以直接复用我之前给你的三文件版本。这里再强调一下前端逻辑要点:
只请求当前域名:
fetch('/api/ip')语言只跟随浏览器
navigator.language(s),不提供 UI 切换展示内容建议包含:
- 结论:当前使用 IPv4/IPv6
- 一句短提示:IPv6 则“恭喜可以访问纯 IPv6”
- IP:本次访问的公网地址
- 归属地 + ISP:来自
info字段 - 一句按归属地定制的招呼(让页面“有人味”)
七、验证:怎么判断是否成功?
部署完成后,打开:
https://test-ipv6.jsw.ac.cn/(页面)https://test-ipv6.jsw.ac.cn/api/ip(接口)
接口返回中关键字段:
ip:如36.251.161.35或2408:...family:ipv4/ipv6info:归属地信息
你会看到几种典型结果
family: ipv6
✅ 恭喜:你的环境可访问纯 IPv6 网站family: ipv4
说明本次连接偏向 IPv4(可能是 IPv6 未开、不可用,或浏览器/网络更倾向 IPv4)
八、这次实践的复盘:为什么最后选“单域名单函数”的方案?
一开始我也试过更像 test-ipv6.com 的思路:分别用 “A-only / AAAA-only” 域名做探针,但现实约束是:
- 因为阿里云ESA不像腾讯云EO支持可为域名单独ipv6设置是否开启
于是最终目标调整为更务实的版本:
不强行判断“你是否具备某一栈”,只告诉用户“本次访问更偏向哪一栈”,并给出可理解的提示。
对于一个放在 jsw.ac.cn 体系下的工具页来说,这个版本更稳定、更易维护,也更符合“白底黑字、无多余解释”的产品风格。
九、常见问题
Q1:为什么我的 x-forwarded-for 里有 IPv6,但 family 还是 ipv4?
因为我们判断的是 request.info.remote_addr,不是代理链里的“可能值”。
Q2:能不能强制让浏览器走 IPv6?
浏览器会根据网络/解析/延迟自动选择(Happy Eyeballs)
Q3:为什么要加 cache-control: no-store?
否则边缘缓存可能把某个访客的 IP 结果缓存下来,导致下一位访客看到错误的 IP。