GeoLibre Chrome 扩展「Open data in GeoLibre」深度解析:最小权限设计、数据链接发现与 Chrome Web Store 上架实践
【免费下载链接】GeoLibreA lightweight, cloud-native GIS platform for visualizing, exploring, and analyzing geospatial data. It runs in the web browser, on the desktop, on mobile, and inside Jupyter notebooks.项目地址: https://gitcode.com/GitHub_Trending/ge/GeoLibre
本文以 GeoLibre 仓库中 STORE_LISTING.md 这份 Chrome Web Store 上架清单为主体,完整梳理扩展的命名、描述文案、分类与权限声明;并结合 scanner.mjs、service-scanner.mjs、url-builder.mjs 等源码与 chrome-extension.test.ts 测试用例,说明这些文案背后的实际实现——链接如何被识别、地图服务如何被还原、深链如何被构建——以及从源码到可发布 ZIP 的完整打包流程。读完本文,你既能理解一份“可直接粘贴进商店后台”的权限说明为何这样写,也能掌握该扩展的本地加载、打包与验证方式。
扩展定位:把数据目录页变成地图的发射台
商店清单为该扩展定义的身份如下(均为 STORE_LISTING.md 原文内容的逐条对应):
- 名称(Name):Open data in GeoLibre
- 摘要(Summary):Find geospatial datasets and map services on a webpage and open them in GeoLibre.(在网页中发现地理空间数据集与地图服务,并在 GeoLibre 中打开它们。)
- 分类(Category):Developer Tools
- 语言(Language):English
详细描述部分说明了它的工作方式:把数据集目录、文档页面和项目网站变成交互地图的“发射台”。点击扩展图标后,用户可以在当前页面中找到受支持的数据链接、按矢量/栅格类型过滤、勾选所需文件,然后一起在 GeoLibre 中打开。
清单宣称的能力边界可以归纳为三类:
- 静态文件链接:GeoJSON、GeoParquet、PMTiles、Cloud-Optimized GeoTIFF(COG),以及包含 GeoJSON 的 ZIP 压缩包;
- 结构化元数据与既有链接:读取 schema.org 下载元数据、识别既有的 GeoLibre 深链、把匹配的 GeoLibre 样式文件与数据集配对,并在虚拟化的 Source Cooperative 仓库页面上恢复完整的文件清单;
- 交互式地图服务:识别当前页面已经发出的 WMS、WMTS、WFS、OGC API Features、ArcGIS Feature Service、XYZ/TMS 与矢量瓦片请求。
这些能力与 manifest.json 中的字段一一对应:name为 "Open data in GeoLibre",description为 "Find geospatial datasets and services used by the current page and open them in GeoLibre.",当前版本0.3.1,minimum_chrome_version为 106。整个扩展由 7 个运行时文件构成(manifest、popup.html/css/mjs、scanner.mjs、service-scanner.mjs、url-builder.mjs)加 4 个图标,这一点在打包脚本 package-chrome-extension.mjs 的runtimeFiles列表中可以得到确认。
商店权限声明:为何只需要 activeTab 和 scripting
STORE_LISTING.md 中最核心的部分,是逐字段可直接粘贴进 Chrome Web Store 后台 Privacy 页签的权限论证(Permission justification)。清单明确给出了一条维护规则:
这些文字块必须与
manifest.json保持同步:manifest 中新增的每一项权限,都必须在这里和商店后台有对应的论证,否则该版本会被拒审。截至 0.3.0,manifest 只请求activeTab和scripting,因此 storage、webRequest 与 host 权限的说明字段不再出现。
manifest.json 的实际内容印证了这一声明——permissions数组中只有两项,且没有任何host_permissions字段:
"permissions": ["activeTab", "scripting"],activeTab 的论证
清单为activeTab给出的原文论证是:该权限仅在用户点击扩展工具栏图标后,授予对当前页面的临时访问权。扩展利用该访问权读取页面链接与结构化元数据,从中挑出 GeoJSON、GeoParquet、PMTiles、COG、含 GeoJSON 的 ZIP 等地理空间数据集,列入弹窗供用户选择。用户离开或刷新页面后访问即结束,任何其它时刻都不会读取页面内容。
从源码看,这一承诺对应的执行路径在 popup.mjs 的inspectPage()中:弹窗打开时先chrome.tabs.query({ active: true, currentWindow: true })拿到当前标签页,再用chrome.scripting.executeScript把 scanDocumentForDatasets 注入页面执行。没有任何后台 service worker,也没有任何监听器——扩展的全部行为都发生在“图标被点击”这个时刻之后。
scripting 的论证
清单为scripting给出的论证是:用户打开弹窗时,该权限把两个打包在扩展内的函数注入活动标签页。第一个函数读取页面链接与元数据以发现数据集文件;第二个函数读回页面已经发出的请求地址,以便识别页面地图所使用的服务——因为地图是从 JavaScript 发起这些请求的,它们从来不是文档中的链接。两个函数都包含在扩展包内,不涉及任何远程代码;每次调用只运行一次,并把结果返回给弹窗。
对应到源码,第二个函数正是 collectRequestedUrls:它调用performance.getEntriesByType("resource")读回每个文档自己维护的 Resource Timing 缓冲,只保留 http/https 条目。popup.mjs中第二次executeScript使用allFrames: true从顶层框架及所有同源子框架各取一次,再汇总交给collectServiceCandidates归类。注释里特别说明:Chrome 对跨源框架的注入按帧拒绝,因此可达的同源框架仍会返回各自的缓冲。
明确“不请求”什么
清单同样用一整节说明扩展不请求什么:没有 host 权限、没有监听网络请求的权限、没有存储权限、没有后台运行;不请求浏览历史、下载记录、Cookie、活动标签之外的标签页或远程代码。商店问卷中关于远程代码的问题应回答“No, I am not using remote code”——因为它运行的每一个脚本都随包分发。这一点在 manifest 的 CSP 中得到代码级佐证:
"content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self' }script-src 'self'在策略层面封死了远程脚本注入的可能,与清单声明互相咬合。配套的可发布隐私政策源文件在 PRIVACY.md,商店提交前需要把该政策托管到一个公开 URL 上(这一要求也写在 README.md 的打包章节中)。
数据链接发现:清单承诺在源码里的落点
清单宣称的“受支持链接”清单——GeoJSON、GeoParquet、PMTiles、COG、ZIP、schema.org 元数据、既有 GeoLibre 链接、样式配对、Source Cooperative 完整文件清单——在 scanner.mjs 的scanDocumentForDatasets()中有完整的逐条实现。
分类规则:扩展名、线索与置信度
核心函数classify(url, hint)按优先级判定格式,并给出 1–3 的置信度:
| 判定条件 | 格式 | 类型 | 置信度 |
|---|---|---|---|
路径以.geojson结尾,或线索含 geojson / geo+json / feature collection | GeoJSON | 矢量 | 3 |
.geoparquet/.parquet结尾,或线索含 geoparquet / parquet | GeoParquet | 矢量 | 3 |
.pmtiles结尾,或线索含 pmtiles | PMTiles | 矢量 | 3 |
.tif/.tiff/.cog结尾,或线索含 geotiff / cloud optimized / cog | GeoTIFF | 栅格 | 3 |
.zip结尾,或线索含 application/zip、geojson+zip 组合 | ZIP | 矢量 | 2 |
.json结尾且线索含 geo/spatial/vector/dataset 等词 | JSON | 矢量 | 1 |
注意其中的一个关键防误报设计:当 URL 明显是网页(.html等页面扩展名,或尾斜杠目录式路径且链接文本“复读”了 slug 中的词)时,同样的词汇只描述页面本身而不是数据,此时线索判定被禁用。chrome-extension.test.ts 中专门有一个用例验证这一点:文档站链接 “Add a GeoJSON line” 指向/examples/add-a-geojson-line/这类 HTML 页面,不会被误报为数据集。
扫描面:锚点、资源链接、数据属性与 JSON-LD
scanDocumentForDatasets的扫描面覆盖了清单与 README 描述的四个来源:
a[href]与link[href],线索取自type、rel、download、title属性与链接文本的拼接;[data-url]、[data-href]、source[src]三类元素(对应清单中“selected data attributes”);- 所有
script[type="application/ld+json"]块,递归遍历其中对象的contentUrl与downloadUrl字段,线索取自encodingFormat、fileFormat、name、description(对应“schema.org JSON-LD”); - Source Cooperative 仓库页的 Next.js 内嵌清单(下文单列)。
去重与合并策略是:以规范化后的 URL 为键,高置信度候选覆盖低置信度候选;若已有候选没有样式而新候选带来样式,则补挂样式而不替换数据集。
样式配对与既有深链解包
清单提到“pairs matching GeoLibre style files”:实现上,所有…style.json/…geolibre.style.json结尾的链接被单独收集进styleLinks,最后按“去掉扩展名后的文件主干”与数据集做匹配——roads.geojson与roads.style.json或roads.geolibre.style.json互为配对,测试用例pairs a neighboring style by filename stem验证了这一行为。
“understands existing GeoLibre links”对应另一段逻辑:若链接本身是geolibre.app域名下带data查询参数的深链,扩展会解包其中的每一对data/style参数并递归加入候选,使它们能与页面上其它链接组合后再一起打开。
站点特化:Source Cooperative 与 Hugging Face
清单中“discovers the complete file inventory on virtualized Source Cooperative repository pages”的实现值得细看:Source Coop 的仓库列表页做了虚拟滚动,DOM 中只有可见行是锚点;但其 Next.js 载荷(self.__next_f.push脚本块)里仍嵌有当前目录的完整对象清单。扫描器从这些脚本中提取每个"path":"...","type":"file"条目,拼接到公开数据主机data.source.coop下,从而把不可见的文件也变成可选数据集,并与页面上已可见的 page/download 重复链接去重(规范化到同一data.source.coop主机是去重的前提,测试用例reads Source Cooperative's complete virtualized inventory...覆盖了这一行为,包括把形似文件格式的/products?tags=cloud optimised geotiff导航链接排除在外)。
此外还有一个未在清单摘要中点名、但同属“链接规范化”的实现:Hugging Face Hub 的同一文件可从 blob/raw/blame/edit/delete/commits/resolve 等七条路由链接到,而只有resolve路由会 302 到能供地图数据源读取字节的 CDN 地址。扫描器按路由在路径中的位置(而非第一个匹配段)解析仓库路径,把七条路由折叠成一条resolveURL,并去掉行锚点以免一个文件被拆成两条候选——对应测试collapses every Hugging Face file route onto the direct resolve URL等三、四个用例。
交互式服务识别:读回请求,而非监听网络
清单承诺识别“当前页面已经发出”的七类服务请求。README 与源码共同说明了这套机制的完整逻辑与边界,这也是商店文案中“runs no network watcher”声明的技术基础。
原理:Resource Timing 缓冲
地图从 JavaScript 拉取瓦片与服务文档,这些 URL 从不出现在文档链接里。扩展读的是performance.getEntriesByType("resource")——每个文档为自身加载记录在案的资源时序缓冲——从顶层框架与所有同源子框架分别收集。service-scanner.mjs 的classifyServiceRequest()把这些 URL 归一成 GeoLibre 可打开的服务候选:
- WMS / WMTS / WFS:按
service与request参数识别(如 WMS 的 GetCapabilities/GetMap/GetFeatureInfo)。端点还原时会剥掉 OGC 操作参数(bbox、layers、styles、tilematrixset等一长串白名单),但保留token之类的签名参数; - WMTS GetTile 特例:把
TileMatrix/TileCol/TileRow三个数值参数还原为{z}/{x}/{y}占位符,生成可直接渲染的瓦片模板——比裸端点更有价值,因为模板已经携带了层名、矩阵集与格式; - ArcGIS Feature Service:正则匹配
…/FeatureServer[/N]路径,并保留子图层索引N——因为只给裸FeatureServer时 GeoLibre 会回退到服务的第一个要素层,对页面正在显示其它层的站点会静默画错层; - OGC API Features:
/collections/<id>/items路径足以独立成立;裸/collections则必须伴随 OGCf格式参数才算数(商店站点恰好也有/collections这种普通路由,测试用例recognizes OGC API Features and ArcGIS feature service requests明确验证/collections?page=2之类不被误判); - 瓦片 URL:
/…/6/17/25.png形式的坐标路径被折叠为{z}/{x}/{y}模板,并按坐标合法性校验(如column < 2^zoom),以避开日期组织的资产目录(/uploads/2024/03/15.png这类路径在测试中被明确排除);.pbf/.mvt判为矢量瓦片,其余图片格式判为 XYZ/TMS 栅格瓦片。
随服务一起携带的“层”与“样式”
清单说这些服务“the current page has already made”,README 进一步解释了为什么结果还要携带额外字段:光有一个端点通常不足以加层,所以每个结果还带上页面向该服务请求的层——WMS 的LAYERS值、WFS 的typeName、WMTS 的层名,以及矢量瓦片集所加载的同源样式文档。这些以serviceLayer与serviceStyle参数进入 GeoLibre,落入对应表单字段,使 Add Data 对话框以“可立即提交”的状态打开,而不是落在一个图层字段为空的端点上。由于一个端点可服务多个层,结果按层列出(如WMS service: topp:states)而不是按 URL 合并——测试keeps two layers of one service apart despite their shared endpoint验证同一 WMS 端点的roads与rivers两层各自独立保留。
三个设计边界(README 明示的取舍)
- Worker 请求不可见:MapLibre 等渲染器从 web worker 里拉矢量瓦片,worker 的时序记录在自己的时间线上,文档缓冲里看不到。此类瓦片集从主线程确实获取的元数据恢复:优先 TileJSON(按文件名
tile.json、tiles.json、tilejson.json识别),再退而求其次用样式文档——GeoLibre 可以从样式单独解析出层。样式只在它的来源上没发现任何瓦片集时才作为独立候选提供。由于从不读取响应体,描述栅格瓦片的 TileJSON 与矢量瓦片无法区分、会按矢量提供——这种误报无法成为图层(Add Data 提交时会解析文档并在解析不出源层时拒绝),而在这里读响应体需要跨源抓取,恰恰需要本设计要规避的 host 权限; - 缓冲有限:Resource Timing 每个文档默认只保留 250 条记录且满后停止记录。很繁忙的页面可能丢掉后加入的服务;提高上限需要
document_start注入脚本与宽泛 host 权限,因此该上限被明确接受。对应地,候选总数在collectServiceCandidates中被MAX_SERVICE_CANDIDATES = 100封顶,且封顶时先扣普通服务、为样式回退候选保留名额(测试keeps room for a style fallback on a page that fills the cap验证了这一点); - 跨源框架不可达:
activeTab只授予标签页主框架的来源,Chrome 不会把该授权延伸到他源框架;allFrames: true因此只能覆盖顶层与其同源框架,跨源框架注入被逐帧拒绝(可达框架仍返回缓冲)。完全运行在跨源 iframe 里的地图对扫描不可见,弹窗会报“无服务”而不是报错——要触达它需要该来源的 host 权限,即本设计刻意回避的常驻访问。
深链构建:从勾选到 GeoLibre 表单
用户勾选条目后,url-builder.mjs 的buildGeoLibreUrl()负责把选择翻译成一个 GeoLibre 深链(基础地址为 GeoLibre Web 入口,见文件顶部常量)。其参数协议如下:
数据集(可多选):每个数据集追加一个data参数;若任一数据集带样式,则按位置一一对应地追加style参数(无样式的条目以空串占位,保证位置对齐)。
地图服务(限单选):七类服务的格式名被映射为add参数值——XYZ / TMS→xyz、WMS→wms、WMTS→wmts、WFS→wfs、OGC API→ogc-features、ArcGIS Feature Service→arcgis、Vector tiles→ogc-vector-tiles——并追加serviceUrl;请求携带的层与样式分别进serviceLayer与serviceStyle。
该函数内置一组显式校验,全部有对应测试(tests/chrome-extension.test.ts的 "URL builder" 描述块):
- 空选择抛错 “Select at least one dataset.”;
- 两个以上服务同选抛错 “Open one map service at a time.”;
- 服务与其它数据混选抛错 “A map service cannot be opened together with other data.”(服务走
add=…入口,文件走data入口,两者在 GeoLibre 侧是不同入口点); - 任何非 http/https 的 URL(包括
file://与清单中提到的blob:临时链接)抛出 “GeoLibre can only open HTTP or HTTPS …”——这与商店描述中 “Temporaryblob:links cannot be transferred” 的边界声明完全一致; - 服务 URL 与其样式 URL 相同时只给
serviceStyle而不重复serviceUrl,避免 GeoLibre 把样式文档再当 TileJSON 解析一次而失败。
传输边界:CORS、签名参数与会话凭证
商店详细描述与 PRIVACY.md 共同界定了数据流边界,这部分内容应视为对部署方的直接约束:
- CORS 前置条件:数据集服务器必须允许浏览器跨源访问(CORS),否则 GeoLibre 端抓取会失败;
- 完整 URL 原样转发:完整的 HTTP(S) URL(包括签名查询参数、URL 内嵌的访问令牌)被逐字放入深链查询串,因此清单提醒:不要把包含你不愿发送给 GeoLibre 的信息的 URL 选进去;
- 会话凭证不转发:Cookie 与其它浏览器会话凭证不会随选择转发,依赖 Cookie 或会话认证的链接可能失效;
- blob: 不可迁移:临时
blob:链接与浏览器内部链接无法传输(对应 url-builder 的协议校验); - 扩展本身不抓取数据集:GeoLibre 直接向上游服务器请求数据,扩展从不持久化任何 URL——列表只存在于弹窗打开期间,关闭即丢弃。
本地开发与商店打包
本地加载(开发态)
按 README.md 的步骤:打开chrome://extensions,启用Developer mode,选择Load unpacked,指向extensions/geolibre-chrome目录。之后的一切发生在点击工具栏图标之后:扫描文档链接、读回各可达框架的 Resource Timing 缓冲,除activeTab与scripting外不持有任何权限,没有后台 service worker,不存储任何东西。
README 还附了一张手工验证矩阵,可作为验收标准:针对 WMS、WMTS、WFS、OGC API Features、ArcGIS Feature Service、XYZ 栅格瓦片、同源 iframe 中的矢量瓦片七类真实第三方地图示例页,逐一确认扩展列出预期服务,且选中后打开的 Add Data 对话框字段已填好——“对话框打开但必填字段为空”被定义为 bug 而非预期步骤。
打包为商店上传 ZIP
在仓库根目录执行:
npm run build:chrome-extension该脚本由 scripts/package-chrome-extension.mjs 实现:读取 manifest 版本号,按runtimeFiles清单收集 7 个运行时文件与 4 个图标,用fflate以最大压缩级别、固定 mtime(1980-01-01)打成确定性 ZIP,输出到dist/geolibre-chrome-<version>.zip(如dist/geolibre-chrome-0.3.1.zip)。固定 mtime 保证同一版本重复打包得到逐字节一致的归档,便于审计与分发。
回归验证
整个识别与构建逻辑由 tests/chrome-extension.test.ts 以 node:test 覆盖,共四个描述块(scanner / URL builder / service request scanner / request history),用例包括:JSON-LD 无扩展名下载端点、文档链接措辞防误报、Source Coop 虚拟化清单恢复与规范化、Hugging Face 七路由折叠、WMTS GetTile 模板还原、同一端点多层去重、250 条缓冲下的样式回退保位、100 条候选封顶等——商店文案中承诺的每一项行为,基本都能在该文件中找到可执行的断言。
小结
STORE_LISTING.md 表面上是一份上架文案,实质是 GeoLibre Chrome 扩展的契约文件:它声明的能力(发现文件链接、识别七类服务、只读当前页、不存储、无后台)、它论证的权限(仅activeTab+scripting,且与 manifest 同步维护)、它声明的边界(CORS、签名参数转发、会话凭证不转发、blob: 不可迁移),都能在 manifest.json、scanner.mjs、service-scanner.mjs、url-builder.mjs 与 chrome-extension.test.ts 中找到逐条对应的实现与断言。对维护者而言,清单与 manifest 的同步规则(新增权限即需补写两处论证,否则拒审)是该文件最重要的一条操作规范;对使用者而言,理解“读回请求而非监听网络”这一机制,就理解了它在能力覆盖(worker 瓦片、跨源 iframe、250 条缓冲上限)上的全部边界所在。
【免费下载链接】GeoLibreA lightweight, cloud-native GIS platform for visualizing, exploring, and analyzing geospatial data. It runs in the web browser, on the desktop, on mobile, and inside Jupyter notebooks.项目地址: https://gitcode.com/GitHub_Trending/ge/GeoLibre
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考