使用 Supabase Storage 与 Edge Functions 自托管 Protomaps 静态地图

📅 发布时间:2026/9/7 18:41:15
使用 Supabase Storage 与 Edge Functions 自托管 Protomaps 静态地图 使用 Supabase Storage 与 Edge Functions 自托管 Protomaps 静态地图【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase在本文示例仓库中Supabase 官方提供了一个完整的自托管地图参考实现位于 examples/storage/protomaps先把地图数据切片成PMTiles静态文件上传到 Supabase Storage 私有桶再通过Edge Functions 代理完成细粒度访问控制最后由浏览器里的MapLibre GL直接渲染。读完本文你将掌握 PMTiles 文件的生成与上传、maps-private代理函数的每一行原理、Supabase 本地config.toml对存储与函数的声明式配置以及如何仅用几行静态 HTML 就在本地看到可缩放的地图。一、整体架构与数据链路整套方案只依赖四类资产全部可以在仓库中直接找到环节仓库内文件作用地图切片数据需要按 Protomaps 指南自行生成的my_area.pmtiles矢量瓦片与元数据打包成的单文件私有存储桶运行期在 Dashboard 创建的maps-private桶存放 PMTiles 文件不对公网直接暴露代理 Edge Functionsupabase/functions/maps-private/index.ts转发请求到authenticated对象路径实现白名单/鉴权前端渲染页index.html用 MapLibre GL PMTiles 协议从代理地址拉瓦片渲染链路可概括为一条单向数据流浏览器 MapLibre GL │ pmtiles:// 协议请求瓦片 ▼ Supabase Edge Functionmaps-privateverify_jwtfalse │ 校验 Origin 白名单注入 service role 级别密钥作为 Authorization ▼ Supabase Storage/storage/v1/object/authenticated/... ▼ 返回 PMTiles 二进制瓦片 → 浏览器解码并绘制关键在于PMTiles 是一种按字节范围随机读取的归档格式Edge Function 需要把Range等请求头原样透传给 Storage浏览器才能只拉取需要的瓦片块——这决定了代理必须是透传式的下文源码剖析会看到这一点。二、步骤一生成静态 PMTiles 地图文件原文档的第一步是遵循 Protomaps 官方入门指南从目标区域数据中提取出my_area.pmtiles。这里不重复切片工具链只强调三点与后续上传直接相关的实践注意点输出物是一个单文件PMTiles 设计目标就是把成千上万个瓦片合并进一个文件既便于复制分发也天然契合 Supabase Storage 的对象模型一个对象 一个文件。文件名即 URL 路径本示例前端访问地址最终是…/maps-private/my_area.pmtiles文件名含.pmtiles后缀会直接出现在 URL 与 Storage 对象路径中生成后不要随意改名否则需要同步修改代理后的前端 URL。区域越小、加载越快PMTiles 按需读取的特性意味着你的切片范围决定了首屏与缩放时的数据量示例仅用于演示时可先截取一个城市甚至更小的区域。三、步骤二上传到 Supabase Storage原文档给出了两个操作要点新建一个私有桶maps-private把my_area.pmtiles上传进去同时注意文件大小上限与套餐档位的关系。在本地开发场景中这个建私有桶的动作还可以用仓库里已经写好的声明式配置完成。查看 supabase/config.toml其中已包含与 Storage 有关的完整定义project_id protomaps [api] # 本示例不通过 PostgREST 客户端读取数据因此关闭数据 API。 enabled false [storage] # 项目内所有桶允许的最大文件大小。 file_size_limit 50MiB [storage.buckets.maps-private] public false # file_size_limit 50MiB # allowed_mime_types [application/vnd.pmtiles] # 取消注释可指定一个本地目录把其中的对象批量上传进该桶。 # objects_path ./buckets/maps-private逐项说明配置含义[api].enabled false本示例不需要 PostgREST 数据 API本地 CLI 启动时会跳过 Data API 服务。[storage].file_size_limit 50MiB全局文件大小上限。这里取 50 MiB 正好对齐 Free 套餐的上限。[storage.buckets.maps-private]声明式创建一个名为maps-private的桶。public false关键配置。桶不公开所有对象只能通过带鉴权信息的请求读取这正是后面要引入 Edge Function 代理的原因。被注释的allowed_mime_types [application/vnd.pmtiles]提示我们可以把桶约束成只接受 PMTiles 这一种 MIME 类型属于可选的加固项。被注释的objects_path ./buckets/maps-private提示本地开发时可以把 PMTiles 文件放进该目录由 CLI 自动同步为桶内对象。文件大小上限需要对照套餐档位原文档提醒我们依据项目套餐查阅文件大小限制对应到仓库文档 apps/docs/content/guides/storage/uploads/file-limits.mdx 有明确数据套餐全局最大文件大小上限Free50 MBPro500 GBTeam500 GBEnterprise可自定义需要理解的三层含义全局上限可在 Storage Settings 中调整但 Free 项目不能超过 50 MBPro 及以上最高可设 500 GB全局上限会作用于所有桶推荐把全局值设为应用能接受的最高值再用[storage.buckets.*]中更小的file_size_limit做单桶收窄标准上传方式最大支持约 5 GB 文件若 PMTiles 切片超过 6 MB 并需要更高可靠性仓库文档 standard-uploads.mdx 建议改用 TUS 断点续传。因此一个典型判断是Free 项目下整包切片若接近 50 MB应当缩小导出区域或调低瓦片层级否则会直接上传失败。四、步骤三用 Edge Function 代理私有切片私有桶的对象无法被浏览器匿名读取直接把前端 URL 指向 Storage 会得到 401。Supabase 给出的解法是把 Edge Function 当作看门代理函数本身可以verify_jwt false免 JWT 公开访问在函数内部自行校验来源并注入 Storage 所需的鉴权头。原文档给出两条部署命令与一条修改要求部署函数不校验 JWTsupabase functions deploy maps-private --no-verify-jwt修改 index.html 中的protomaps.url把占位的project_ref换成你自己的项目引用Project Ref。逐行拆解代理函数源码代理实现位于 supabase/functions/maps-private/index.ts全文很短逻辑却很完整import { withSupabase } from npm:supabase/server^1 const ALLOWED_ORIGINS [http://localhost:8000] // 公开的瓦片代理因此以 verify_jwt false 部署。 export default { fetch: withSupabase({ auth: none }, (req) { // 限制哪些来源可以读取私有瓦片。 const origin req.headers.get(Origin) if (!origin || !ALLOWED_ORIGINS.includes(origin)) { return new Response(Not Allowed, { status: 405 }) } const reqUrl new URL(req.url) const url ${Deno.env.get(SUPABASE_URL)}/storage/v1/object/authenticated${reqUrl.pathname} const SUPABASE_SECRET_KEYS JSON.parse(Deno.env.get(SUPABASE_SECRET_KEYS)!) const { method, headers } req // 追加 Auth 头使 Storage 提供私有对象。 const modHeaders new Headers(headers) modHeaders.append(authorization, Bearer ${SUPABASE_SECRET_KEYS[default]!}) return fetch(url, { method, headers: modHeaders }) }), }对应每一段的职责withSupabase({ auth: none }, ...)来自npm:supabase/server的封装。auth: none表示该函数不要求用户携带 Supabase Auth JWT配合--no-verify-jwt部署允许匿名请求到达函数本体。Origin 白名单L3、L9-L12读取请求头Origin只有命中ALLOWED_ORIGINS默认仅http://localhost:8000才放行否则返回 405。这是仅允许你的网页调用代理的廉价防线——MapLibre 发起瓦片请求时会携带页面 Origin。路径拼接L14-L15reqUrl.pathname形如/maps-private/my_area.pmtiles被拼到…/storage/v1/object/authenticated之后构成 Storage 私有对象的标准 REST 路径。密钥注入L17-L21从环境变量SUPABASE_SECRET_KEYS解析 JSON 后取出default键以Bearer形式追加进authorization头。这相当于用服务端凭据代读authenticated权限下的私有对象。透传代理L22原样转发method与头集合。Range请求头因此能抵达 StoragePMTiles 的按范围随机读取机制才得以生效——这是整个代理设计中最容易被忽视却最关键的一行。延伸如果改成仅登录用户可见原文档还提到一种常见变体——用 Supabase Auth JWT 控制地图可见性。对比仓库中另一个示例 read-storage/index.ts只需把封装参数换成withSupabase({ auth: user }, async (req, ctx) {...})即可拿到ctx.supabase客户端进而把 Origin 白名单校验替换成当前请求必须携带合法用户 JWT的判断。更细的策略还可叠加行级安全RLS查询实现按用户裁剪数据源。五、前端渲染MapLibre GL PMTiles 协议修改好 URL 后index.html 负责把切片画出来。它通过 CDN 引入了三类库并做了粘合MapLibre GL JS 4.1.2矢量地图渲染引擎pmtiles.js 3.0.6PMTiles 协议的浏览器实现protomaps-themes-base 2.0.0-alpha.5Protomaps 官方基础样式主题。渲染逻辑的核心片段如下script srchttps://unpkg.com/pmtiles3.0.6/dist/pmtiles.js/script ... script typetext/javascript let protocol new pmtiles.Protocol() maplibregl.addProtocol(pmtiles, protocol.tile) const map new maplibregl.Map({ hash: true, container: map, style: { version: 8, glyphs: https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf, sources: { protomaps: { attribution: Protomaps © OpenStreetMap, type: vector, url: pmtiles://https://project_ref.supabase.co/functions/v1/maps-private/my_area.pmtiles, }, }, layers: protomaps_themes_base.default(protomaps, dark), }, }) /script要点maplibregl.addProtocol(pmtiles, protocol.tile)把pmtiles://自定义 URL 协议注册进 MapLibresource 的url是pmtiles://前缀 你的 Edge Function 完整地址。其中project_ref即第三步要求替换的占位符域名固定为project_ref.supabase.co字体字形由 Protomaps 的公共 PBF 字体服务提供图层样式直接复用protomaps_themes_base.default(protomaps, dark)一行即得到完整的暗色底图图层栈也可以按需改为light等其他主题。六、步骤四本地起一个静态服务器由于瓦片请求依赖浏览器发送Origin请求头且代理白名单默认只放行http://localhost:8000直接双击打开index.htmlfile://协议会因 Origin 为空而被拒绝。原文档给出的做法是用 Python 起静态服务python3 -m http.server默认监听8000端口然后浏览器访问http://localhost:8000/看到地图即代表整条链路打通。若你希望换端口或换域名记得同时修改两处代理函数里的ALLOWED_ORIGINS数组index.html中 source 的url。七、安全边界与注意事项总结综合原文档与源码这套自托管方案的边界值得明确私有桶是第一道闸public false见 config.toml保证对象不直接暴露即使有人猜到 Storage URL 也无法匿名读取。Edge Function 是第二道闸默认通过 Origin 白名单限制调用方页面可升级为auth: user校验 Supabase Auth JWT 实现登录墙。密钥只应存在于服务端代理函数从SUPABASE_SECRET_KEYS环境变量读取default键该环境变量由平台自动注入切勿复制到前端代码。验证部署生效前先supabase start或确认函数已上传本地调试时建议先用本地 CLI 启动再执行部署命令核对函数日志避免 URL 已改但函数未部署导致的 404/405。八、文件清单与后续探索相对路径说明examples/storage/protomaps/README.md本示例原始操作文档四步指南examples/storage/protomaps/supabase/functions/maps-private/index.ts代理 Edge Function 完整实现examples/storage/protomaps/supabase/config.toml本地项目声明式配置桶、函数、大小限制examples/storage/protomaps/index.htmlMapLibre GL 前端渲染页apps/docs/content/guides/storage/uploads/file-limits.mdx官方文件大小限制说明apps/docs/content/guides/storage/uploads/standard-uploads.mdx大文件上传方式说明examples/edge-functions/supabase/functions/read-storage/index.ts带auth: user读取 Storage 的对照示例想进一步深化可从两条路线切入一是把 Origin 白名单换成 Auth JWT 校验让地图只对登录用户开放二是引入 Overture Places 等公开数据集配合 Protomaps 构建更大范围的自有底图服务。需要强调这套方案的存储带宽与函数调用都会计入项目用量生产化前请根据实际瓦片命中量评估成本与文件上限配置。【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考