简介:面向使用 Folium 进行 Web 地图可视化的 Python 开发者,这份本地化静态资源包能有效应对默认远程加载 CDN 缓慢、跨境访问不稳定等痛点,让地图打开速度不再受限于外网环境。资源将 Leaflet、Bootstrap、FontAwesome 等依赖的 JS、CSS、字体与图标完整收录,并附带 Python 脚本,可自动把模板中的资源引用改写为本地路径,无需改动业务逻辑,既可用于本地开发调试,也能直接部署到生产环境。压缩包共 77 个文件,约 1.81MB,包含 10 个 JS、9 个 CSS 运行文件,14 个 SCSS 与 14 个 Less 源文件,以及 PNG/SVG 图标和 EOT/WOFF/TTF 字体,方便按需定制样式;目录按组件归类,同时保留了源码映射 map 文件和未压缩版本,便于调试与排查版本问题。已有 1467 人学习。无论是纯离线部署,还是通过服务器代理远程资源,都能有效优化地图展示体验,尤其适合内网隔离、跨地域机房或对首屏速度敏感的 Web 项目。
1. 为什么 folium 的地图一断网就白屏:src 资源本地化是离线交付的起点
某次给车间做故障点可视化,机器能起服务,地图却一直灰底白屏,打开浏览器开发者工具才发现 script 还在请求公网 CDN。Folium 定位是快速生成 Leaflet 地图,默认把 js/css 放在 CDN 上,浏览器联网时没问题,一旦进入内网、离线环境或者公共机房的受限网络,<script src>和<link href>全部失败,地图自然起不来。这篇笔记就把这件事拆透:folium 的 js/css 到底从哪里加载,怎么把本地资源接到src上,以及替换过程中最常见的几个坑。适合所有用 Flask、Django 或 Notebook 做地图交付的开发者,尤其适合被“白屏”折磨过的人。
2. 摸清 folium 的 js/css 加载链路:先知道 src 指向谁,再决定改哪里
Folium 并不是把 Leaflet 的 JS 和 CSS 打包进最终的 HTML,而是通过模板引用外部资源。浏览器解析 HTML 时看到<script src="https://...">,就去外网拉取;看到<link href="https://...">,就去外网拉取样式。只要这台机器访问不了这个域名,后面的 map 渲染就全断。要改成本地资源,第一步是搞明白渲染出来的 HTML 里有哪些链接、文件之间什么关系。
2.1 渲染一张地图,HTML 里的 link 和 script 都指向了哪些资源
先用最小代码生成地图,并把 HTML 打印出来看:
import folium m = folium.Map(location=[31.23, 121.47], zoom_start=12) html = m.get_root().render() # 只打印 head 和前 800 个字符,避免被地图数据刷屏 print(html[:800])这段代码的逻辑是先创建 Map 对象,再调用get_root().render()渲染成完整 HTML 字符串。location是地图中心经纬度,zoom_start是初始缩放级别,这两个参数与资源本地化无关,只是保证地图能画出个形状,方便观察 head 里的引用。html[:800]的截断只是为了不让地图的经纬度数据和瓦片配置刷掉信息,实际渲染不会受影响。
输出里通常能看到两类标签:一类是<link rel="stylesheet" href="...leaflet.css">,一类是<script src="...leaflet.js">。有的版本还会额外引用 Font Awesome 或 marker 图标相关的 CSS,但核心就是 Leaflet 的 js/css。这一步的意义在于:你要改的目标就是这些src和href的值。如果渲染后根本没有外部资源链接,那说明你的 Folium 版本已经做了内联或资源变量被改过,后面的方案可以跳过一部分。
这也是排查线上问题最直接的办法:让出错的地图对象在本地打印 HTML,一眼就能看出 src 是否还指向公网地址。通常 Folium 的 Map 对象会把资源引用放在<head>的尾部,地图容器内部大多只有数据和初始化脚本。所以判断是否“纯离线”也很容易:在渲染后的字符串里搜https://,如果没有,资源引用就只剩本地路径了。这个搜索动作本身就能帮你在改完后自测。
2.2 Leaflet 资源包不止 js/css:连带 images 目录一起规划
很多人只把leaflet.js和leaflet.css下载到本地,结果地图主体能显示,但 marker 图标变成一张裂图。原因是 Leaflet 的 CSS 文件中用相对路径引用了 marker 图片,比如url(images/marker-icon.png)。浏览器拿到本地 CSS 之后,会以 CSS 文件所在的 URL 为基准去请求images/marker-icon.png。
所以本地资源目录规划要按 Leaflet 发行包的 dist 结构来放:
| 文件/目录 | 作用 | 本地建议路径 |
|---|---|---|
leaflet.js | 地图核心逻辑 | static/leaflet/leaflet.js |
leaflet.css | 地图基础样式 | static/leaflet/leaflet.css |
images/marker-icon.png | 默认标记图标 | static/leaflet/images/marker-icon.png |
images/marker-icon-2x.png | 高清屏标记图标 | static/leaflet/images/marker-icon-2x.png |
images/marker-shadow.png | 标记阴影 | static/leaflet/images/marker-shadow.png |
images/layers.png和layers-2x.png | 图层控件图标 | static/leaflet/images/layers.png |
images/attribution.png | 版权控件图标 | static/leaflet/images/attribution.png |
获取这些资源最稳妥的方式,是找一台有外网的机器,用 npm 下载 Leaflet 包,或者从 CDN 直接保存同名文件。下载核心文件的命令可以参考:
mkdir -p local_leaflet/images curl -o local_leaflet/leaflet.js https://cdn.jsdelivr.net/npm/leaflet/dist/leaflet.js curl -o local_leaflet/leaflet.css https://cdn.jsdelivr.net/npm/leaflet/dist/leaflet.css curl -o local_leaflet/images/marker-icon.png https://cdn.jsdelivr.net/npm/leaflet/dist/images/marker-icon.png curl -o local_leaflet/images/marker-icon-2x.png https://cdn.jsdelivr.net/npm/leaflet/dist/images/marker-icon-2x.png curl -o local_leaflet/images/marker-shadow.png https://cdn.jsdelivr.net/npm/leaflet/dist/images/marker-shadow.pngcurl -o的作用是把 URL 内容保存为指定文件。这里用不带版本号的 CDN 路径,方便看到当前跟随的规则;如果你在项目里用固定版本,建议把版本号也写进文件目录,比如local_leaflet/1.9.x/。关键是 images 目录必须和 CSS 文件在同一级,放在不同目录会导致 CSS 里的相对路径解析不到图片。
如果 Leaflet 的 CSS 文件里有其他url(...)引用,比如控制按钮、工具栏图标,也一并从发行包的dist/images复制到本地同名目录。判断方法很简单:下载完 CSS 后,用文本编辑器搜索url(,每条相对路径都必须在本地存在。
2.3 两种本地 src 策略:相对路径、绝对路由,以及它们的生效范围
路径写进 HTML 时,有两种常见选择。第一种是相对路径,比如src="static/leaflet/leaflet.js",它依赖浏览器当前页面的 URL。如果你的地图页面固定在/map/index,相对路径会解析成/map/static/...,这时候资源可能 404;如果页面 URL 是/map/,则解析成/map/static/...,又可能正常。这就是为什么很多人在开发环境好好的,换个路由就白屏。
第二种是绝对路径,比如src="/static/leaflet/leaflet.js",浏览器永远以站点根路径为基准去请求。这种方式在 Flask、Django 里配合静态文件路由最稳定,也是我推荐的默认做法。缺点是如果应用被部署在域名子路径下,比如访问地址是https://example.com/portal/map,那/static/...会指向域名根而不是/portal/static/...,这时需要把前缀改成/portal/static/...。
因此做资源本地化不是简单改一个路径,还要考虑应用运行在什么 URL 下。常见做法是定义一个配置项:
STATIC_RESOURCE_PREFIX = "/static/leaflet" RESOURCE_MAP = { "leaflet.js": f"{STATIC_RESOURCE_PREFIX}/leaflet.js", "leaflet.css": f"{STATIC_RESOURCE_PREFIX}/leaflet.css", }这个配置专门管理本地资源的 URL 前缀。好处是将来部署到子路径,只需要改一处前缀,不用在地图代码里一个个搜索替换。STATIC_RESOURCE_PREFIX在 Flask 里通常等于app.static_url_path + "/leaflet",在 Django 里通常等于STATIC_URL + "leaflet"。
到这里,你已经知道 folium 引用了哪些资源、本地目录长什么样、路径策略怎么选。接下来就可以动手把 folium 的 src 改成指向本地文件的真正代码路径。
3. folium 本地资源改造的三种落地路径:字符串替换、类变量覆盖和源码兜底
这一章是实操核心。根据你的项目是临时交付还是长期维护,选择不同做法。我会按推荐程度从高到低讲,但先把最简单的一招放在前面,因为排障时经常要用它快速验证思路。
3.1 临时救急:在 render() 之后对 HTML 做批量替换
如果你有一个已经在运行的 Flask 服务,或者只是想私下确认本地文件能不能让地图起来,最快的方式是这样:
import folium m = folium.Map(location=[31.23, 121.47], zoom_start=12) html = m.get_root().render() replace_map = { "https://unpkg.com/leaflet@latest/dist/leaflet.css": "/static/leaflet/leaflet.css", "https://unpkg.com/leaflet@latest/dist/leaflet.js": "/static/leaflet/leaflet.js", "https://cdn.jsdelivr.net/npm/leaflet/dist/leaflet.css": "/static/leaflet/leaflet.css", "https://cdn.jsdelivr.net/npm/leaflet/dist/leaflet.js": "/static/leaflet/leaflet.js", } for old_url, new_url in replace_map.items(): html = html.replace(old_url, new_url) with open("offline_map.html", "w", encoding="utf-8") as f: f.write(html)逻辑说明:先渲染出完整 HTML,再用字典保存“外部 URL 到本地 URL”的映射,逐个replace。replace_map故意写了 unpkg 和 jsDelivr 两组常见地址,因为 Folium 不同版本默认的 CDN 域名可能不同。这个脚本执行后,offline_map.html里不再有公网地址,浏览器会尝试请求/static/leaflet/leaflet.js。
参数说明:replace_map的 key 必须精确匹配渲染结果里的字符串,多一个末尾斜杠都不行;value 建议用绝对路径,少踩相对路径的坑。如果你的环境里静态服务没开,这个方法拿到本地只是“路径变了”,浏览器依然 404。所以它适合验证资源路径逻辑,不适合直接交付。
还要注意:如果 HTML 里有 marker 图标的 URL 也来自 CDN,比如marker-icon.png,同样要加入 replace_map。判断方法是把替换后的 HTML 打开,在开发者工具里看 Network 面板还有没有外网地址请求。
这个方法的缺陷是每次都要在运行时做一次字符串处理,而且 Folium 升级后 CDN 地址一变,替换表又要跟着改。所以它适合应急,不适合当作长期方案。
3.2 推荐做法:用类变量覆盖默认资源列表,从 src 源头调整
Folium 的地图对象在初始化时会把默认的 js/css 资源列表追加到渲染上下文里。多数版本的Map类都有default_js和default_css两个类属性,分别是一个(name, url)的列表。我们可以通过子类覆盖这个列表,让所有由这个子类创建的地图都从本地加载资源。
import folium class LocalMap(folium.Map): default_js = [ ("leaflet", "/static/leaflet/leaflet.js"), ] default_css = [ ("leaflet_css", "/static/leaflet/leaflet.css"), ] m = LocalMap(location=[31.23, 121.47], zoom_start=12) html = m.get_root().render() print(html[:800])运行这段代码后,输出 HTML 的 head 里应该只有/static/leaflet/leaflet.js和/static/leaflet/leaflet.css。这里的逻辑是:子类把父类的默认资源列表整体替换掉,Folium 在渲染模板时会拿default_js/default_css去生成 link 和 script 标签。只要你的 Folium 版本确实使用这两个属性名,这个方法就是最干净的。
参数说明:列表里第一个元素是内部名称,建议沿用leaflet和leaflet_css,有些模板或插件可能依赖这个名字查找资源。第二个元素是最终输出到 HTML 的 URL,注意这里不要再写static/leaflet/...这样的相对路径,统一写成/static/leaflet/...加开头的斜杠。如果你要把资源放在另一个目录,比如assets/vendor/leaflet,改 value 即可,key 不用动。
如果你的 Folium 版本里看到的属性名不是default_js,可以打开源码确认:
import inspect import folium print(inspect.getsource(folium.Map))这段代码中,inspect.getsource会把folium.Map的类源码打印出来,搜索default_js或default_css就能看到当前版本的真实命名。不同小版本可能把列表写成_default_js、default_js或者放在__init__的局部变量里,遇到时顺着源码改。使用inspect的目的不是绕开问题,而是帮你在升级 Folium 后迅速确认资源变量是否变了。
与直接改源码相比,子类覆盖只在你的项目代码里生效,不影响其他代码。即使以后升级 Folium,被覆盖的类属性也只是随版本变化,不会像改 site-packages 那样被 pip 静默覆盖。
3.3 终极兜底:直接修改 folium 源码里的默认资源列表
在一些内网环境里,项目代码被拆成系统服务,业务代码很难改动,或者团队统一用一个固定的虚拟环境,这时候可以直接改 folium 安装目录下的源码。先找到文件路径:
import folium from pathlib import Path target = Path(folium.__file__).parent / "folium.py" print(target)拿到路径后,用 Python 脚本做替换,避免手滑改坏整个文件:
import re from pathlib import Path import folium target = Path(folium.__file__).parent / "folium.py" text = target.read_text(encoding="utf-8") text = re.sub( r"https://[^\"']+leaflet\.css", "/static/leaflet/leaflet.css", text, ) text = re.sub( r"https://[^\"']+leaflet\.js", "/static/leaflet/leaflet.js", text, ) target.write_text(text, encoding="utf-8")这个脚本是直接读写 site-packages 下的文件,逻辑是通过正则匹配源码里所有指向 leaflet CSS/JS 的外链,统一替换成本地路径。使用re.sub而不是字符串精确匹配,是为了兼容不同 CDN 域名和版本号路径。运行前建议先备份folium.py,或者至少记录原文件哈希,因为 pip 升级或重装包会把修改冲掉。
参数说明:正则https://[^"']+leaflet\.css会匹配以https://开头、后接任意非引号字符,直到leaflet.css的整段 URL。[^\"']+负责吃掉路径中的版本号、目录层级,既不会漏掉末尾的.css,也不会误伤其他资源。替换后的路径是硬编码的/static/leaflet/leaflet.js,如果要部署到子路径,记得同步修改。
这个方法有个隐藏风险:如果你的系统里别的项目共享同一个虚拟环境,这次修改会污染全局。所以它适合团队已经约定好离线部署的专用环境,不适合本地个人电脑上随意改。
3.4 三种路径怎么选:改动范围、升级兼容与维护成本对比
| 方法 | 改动位置 | 升级影响 | 维护成本 | 推荐场景 |
|---|---|---|---|---|
| 渲染后替换 | 业务代码 | 无,但替换表需跟随版本更新 | 中 | 临时排查、一次性导出 HTML |
| 子类覆盖资源列表 | 项目代码 | 低,属性名可能变化 | 低 | Flask/Django 项目长期维护 |
| 修改 site-packages 源码 | 第三方包 | 高,pip 升级即失效 | 高 | 专用离线环境、建镜像 |
选型时先看交付方式。如果你是输出一个 HTML 文件给同事,用 3.1 最快;如果你在维护一个地图服务,用 3.2 最稳;如果你负责的是一个不会变更的离线镜像,3.3 也能接受。多数团队其实用 3.2 就够了,因为把路径收敛到一个子类里,后续加版本参数、加内容安全策略都能在一个文件里改。
4. 在 Flask、Django 和 Notebook 里让本地 src 真正被浏览器命中
前面把 folium 的 HTML 改成了/static/leaflet/...,但浏览器能不能真的拿到这些文件,取决于你的 Web 容器是否把静态目录暴露在对应路由上。这一章讲三种常见宿主环境的落地方法。
4.1 Flask:用 static_url_path 与 url_for 获得可迁移的静态目录
Flask 默认会在项目根目录找static文件夹,并把它映射到/static路由。所以只要你把 leaflet 文件放在static/leaflet/下,路径/static/leaflet/leaflet.js就能命中。但为了可移植,建议显式声明:
from flask import Flask import folium app = Flask( __name__, static_folder="static", static_url_path="/static", ) class LocalMap(folium.Map): default_js = [("leaflet", "/static/leaflet/leaflet.js")] default_css = [("leaflet_css", "/static/leaflet/leaflet.css")] @app.route("/map") def map_view(): m = LocalMap(location=[31.23, 121.47], zoom_start=12) return m._repr_html_()这段代码的逻辑是把 Flask 静态目录显式绑定到项目static文件夹,static_url_path="/static"定义了外部访问前缀。map_view返回m._repr_html_(),这是 Folium 对象在 Notebook 里显示时用的完整 HTML 字符串,Flask 会直接把它作为响应体返回。
参数说明:static_folder对应磁盘目录,static_url_path对应 URL 前缀。两者可以不一致,比如磁盘目录叫assets,URL 前缀叫/public,那资源地址就是/public/leaflet/leaflet.js,但此时LocalMap里的 value 也要同步改成/public/leaflet/leaflet.js。建议保持 folder 和 path 都叫static,少一层心智负担。
m._repr_html_()是 Folium 提供的内置方法,返回的 HTML 不带外部的模板壳,但包含 head 里的静态资源链接。如果你用的是老版本,也可以用m.get_root().render()拿到同样的字符串传给Response。两者的区别在于_repr_html_会附加少量 Notebook 环境需要的处理,在 Flask 里表现一致,优先用它没毛病。
Flask 还有一个常见的坑:如果你在蓝图里用url_for('static', filename='leaflet/leaflet.js'),它返回的是相对当前请求上下文的 URL,但 Folium 默认资源列表是模块级路径,不能直接调用url_for。所以我的习惯是只把app.static_url_path拼成字符串写进LocalMap,不去依赖蓝图判断。
4.2 Django:把 leaflet 文件放进 staticfiles,并处理好前缀
Django 的静态文件机制比 Flask 严格。你要在settings.py里声明STATIC_URL和STATICFILES_DIRS,再把文件放到对应目录,开发环境下runserver才能访问。
# settings.py STATIC_URL = "/static/" STATICFILES_DIRS = [ BASE_DIR / "static", ]然后在项目static/leaflet/目录放好 leaflet.js、leaflet.css 和 images。Django 的视图里返回 folium 地图时,资源路径可以直接使用STATIC_URL + "leaflet/leaflet.js"。为了避免硬编码,可以在应用配置里定义一个工具函数:
from django.conf import settings import folium class LocalMap(folium.Map): default_js = [ ("leaflet", settings.STATIC_URL + "leaflet/leaflet.js"), ] default_css = [ ("leaflet_css", settings.STATIC_URL + "leaflet/leaflet.css"), ]这段代码要放在视图或独立模块中,保证settings已经被正确加载。settings.STATIC_URL在开发环境通常是/static/,部署到生产后如果用了 CDN 或 OSS,这个值会变成外部域名,此时 leaflet 资源也会跟着从外部域名加载。如果你的部署策略要求 Leaflet 必须本地化,就不能依赖STATIC_URL指向公网,而是把它固定为本地/static/,或用单独的MAP_STATIC_PREFIX配置。
Django 还有一个容易被忽略的环节:collectstatic会把STATICFILES_DIRS里的文件收集到STATIC_ROOT。你在本机测试时直接能用,是因为开发服务器帮忙路由了;部署到 Nginx 或容器时,必须执行python manage.py collectstatic,否则 leaflet 文件不会出现在最终静态目录。这是很多人本地地图正常、上线后 404 的原因。
如果你把地图嵌入 Django 模板,不要试图在模板里用{% static %}拼接 Folium 生成的完整 HTML。因为 Folium 输出的是独立 HTML 片段,不是 Django 模板。正确思路是让 Folium 子类自己知道STATIC_URL,在 Python 侧把路径拼好,模板只负责输出变量。
4.3 Notebook 场景:srcdoc 里的相对路径为什么失效,怎么改成绝对 URL
Notebook 里显示 Folium 地图时,生成的 HTML 通常被放在<iframe srcdoc="...">内。srcdoc 里的内容没有自己的 URL,它继承的是 Notebook 页面的 URL。如果你在 Folium 资源里写相对路径src="static/leaflet/leaflet.js",浏览器会相对于 Notebook 页面地址解析,结果请求到了/tree/static/...之类的位置,几乎必然 404。
解决方法是把路径改成绝对路径/static/leaflet/leaflet.js。但 Notebook 本身没有 Flask 那样的静态路由,除非你的 Notebook 是放在 Web 应用后面。更通用的做法是把 leaflet 的 js/css 直接内联进 HTML 的 head。这里给一个简化版的内联替换工具:
import folium from folium import Element INLINE_FILES = { "static/leaflet/leaflet.css": "<style>{}</style>", "static/leaflet/leaflet.js": "<script>{}</script>", } def to_offline_map(m, dirname="static/leaflet"): root = m.get_root() for filename, wrapper in INLINE_FILES.items(): with open(filename, encoding="utf-8") as fp: content = fp.read() root.header.add_child(Element(wrapper.format(content))) return m这段代码把 CSS 和 JS 读到内存,分别包成 style 和 script 标签,加入 folium 根节点的 header 里。注意这里用的是root.header.add_child(Element(...)),它会在渲染时把内容输出到 head 区域。参数说明:dirname是本地资源目录,wrapper是标签模板,{}位置放文件内容。如果资源较多,可以把INLINE_FILES设计成一个字典,循环读取。
内联方案会让 HTML 体积变大,leaflet.js 本身约 140 KB 左右,CSS 约 20 KB,对 Notebook 显示来说可以接受。如果你还要引用 marker 图标图片,内联不方便,建议把图片转成 base64 data URI,或把 images 目录和 Notebook 放在同一个根目录下,再用绝对路径访问。
4.4 多环境部署:用配置项控制 src 前缀,避免改完地图又白屏
如果你同一个项目要部署到开发机、测试机、客户内网,静态资源的前缀可能不同。比如内网环境下应用挂在域名根路径,测试环境挂在/test/portal,生产环境挂在/。这时把路径写死成/static/...会有问题。
我一般会做一个配置对象,专门管理 folium 的资源前缀:
import os import folium STATIC_PREFIX = os.getenv("MAP_STATIC_PREFIX", "/static") class LocalMap(folium.Map): default_js = [("leaflet", f"{STATIC_PREFIX}/leaflet/leaflet.js")] default_css = [("leaflet_css", f"{STATIC_PREFIX}/leaflet/leaflet.css")]环境变量MAP_STATIC_PREFIX在 Flask 应用里可以直接从app.config读取,在 Django 里放到settings.py。它的作用是把“静态资源 URL 前缀”和 folium 解耦,前端最终输出的 src 由部署环境决定。参数默认值是/static,满足大多数站点根部署;如果子路径部署,启动服务时设置MAP_STATIC_PREFIX=/portal/static,其他代码不用动。
这里要提醒一句:前缀最后不要加多余的斜杠,否则会生成//static/leaflet/...这种双斜杠 URL。检查方式是在浏览器打开地图页,查看资源的完整src,确认路径拼接正确。
5. 避坑:folium 本地资源模式最容易翻车的六个现场
这一章整理我实际遇到过的六个高频问题,按“现象→原因→解决”的顺序写,每条都可直接对照排错。
5.1 白屏且控制台净是 404:js 和 css 必须成对落盘,少一个都起不来
现象:页面能看到地图容器,但整块灰底白屏,开发者工具 Console 里一堆 404,请求的路径已经在本地,但文件不存在。最常见的原因是只下载了leaflet.js,没下载leaflet.css,或者反之。Leaflet 依赖 CSS 计算地图容器的尺寸、控制按钮位置、图标样式。JS 加载后找不到 CSS 里定义的规则,DOM 虽然建出来了,但看不到视觉结果。
原因:资源本地化时从 CDN 上手动保存文件,容易漏掉某一个;或者下载 CSS 时扩展名写错,保存成了leaflet.css.txt,服务器返回 200 但 MIME 类型是 text/plain,浏览器拒绝执行。
解决:复制文件后,打开浏览器 Network 面板,按请求路径逐个检查 200;再用file命令确认 MIME。生产环境更直接的办法是写一个启动自检脚本,检查关键文件存在且大小大于阈值。我一般会在代码里加一个断言:
from pathlib import Path required_files = [ "static/leaflet/leaflet.js", "static/leaflet/leaflet.css", "static/leaflet/images/marker-icon.png", ] for f in required_files: if not Path(f).exists(): raise RuntimeError(f"leaflet 本地资源缺失: {f}")这个检查放在 Flask 的启动钩子或 Django 应用 ready 里都可以。它不解决 404,但能让你在启动阶段就发现问题,而不是等用户打开地图才看到白屏。
5.2 地图出来但 marker 图标是裂图:Leaflet 的 images 目录也要搬运
现象:地图、图层、缩放按钮都正常,但每个 marker 位置显示一个带着感叹号的小方块。控制台通常没有致命错误,只有图片加载 404。
原因:Leaflet 的 CSS 文件里写的是相对路径images/marker-icon.png,而不是绝对路径。当你把 CSS 放到/static/leaflet/leaflet.css后,浏览器会向/static/leaflet/images/marker-icon.png发请求。如果你只复制了 JS 和 CSS,没有复制 images 目录,请求必然 404。
解决:把 Leaflet 发行包里dist/images/下所有 PNG 文件复制到与 CSS 同级的images/目录。常见文件我在第二章列过,包括marker-icon.png、marker-icon-2x.png、marker-shadow.png、layers.png、layers-2x.png、attribution.png。如果你使用自定义图标,并且 Icon 参数里给出了完整的图片 URL,则不会触发这条,但默认 marker 一定会依赖这些文件。
检查方法:打开地图页,Network 筛选 img,看有没有 404 图片;或在本地执行curl -I /static/leaflet/images/marker-icon.png,返回 200 才算通过。
5.3 地图在/map/下正常,在/map/123下白屏:相对路径被当前路由拆了
现象:同一个地图页面,URL 是/map/时显示正常,URL 变成/map/123时白屏,Network 里所有 leaflet 请求的 URL 都多了一层/map/,比如/map/static/leaflet/leaflet.js。
原因:如果资源地址写成static/leaflet/leaflet.js(不带开头斜杠),浏览器会以当前页面 URL 的目录为基准解析。/map/123的目录是/map/,于是请求/map/static/...;/map/的目录是/,所以请求/static/...正常。这不是 folium 的问题,是相对路径的本质规则。
解决:所有 folium 资源地址统一使用绝对路径,也就是 value 以/开头,写成/static/leaflet/leaflet.js。如果你的应用部署在子路径,就在前缀里包含子路径,比如/portal/static/leaflet/leaflet.js。之后测试时至少选两级 URL 各访问一次,不能只测根路径。
5.4 页面里 leaflet.js 被加载两次:默认资源列表没替换干净
现象:Network 里能看到/static/leaflet/leaflet.js和https://.../leaflet.js同时存在,页面功能正常但请求量翻倍,可能带来重复初始化警告。
原因:部分 Folium 版本会把默认资源写进Map的default_js/default_css,同时在某些插件类(比如Marker、Popup)自己的模板里也追加了 CDN 引用。你只改了 Map,没有改其他组件的资源。也可能因为你在Map.__init__里追加了本地资源,却没有删除原来的 CDN 资源,导致两者共存。
解决:先渲染后搜索src=和href=,统计 leaflet 出现的次数。如果同一 URL 出现两次,检查是重复引用还是不同 URL。通常在创建 Map 之后,可以通过m.get_root().render()的输出来定位。修改资源时,应该整体替换default_js列表,而不是在原有列表上append。用append做扩展很容易把旧地址留在列表里。经验法则是:列表整体赋值,不要追加。
5.5 改了文件路径但页面还在加载旧版本:浏览器缓存和 Python 缓存的双重问题
现象:leaflet.js 已经从/static/leaflet/leaflet.js换成了新版本,文件内容也确认变了,但浏览器 Network 里显示的仍是旧文件大小,甚至 200 (from disk cache)。
原因:浏览器对静态文件有强缓存,尤其响应头里带了Cache-Control或ETag;Folium 一旦把路径写死成/static/leaflet/leaflet.js,URL 不变,浏览器就认为资源没变。另外 Python 侧的.pyc缓存也可能让类属性修改不生效。
解决:静态资源 URL 加版本参数,例如/static/leaflet/leaflet.js?v=20250104。这个v不是文件路径的一部分,浏览器会把它当成新地址请求。在LocalMap里可以直接写f"/static/leaflet/leaflet.js?v={VERSION}"。改完还看不到最新内容,强制刷新或用无痕窗口,排除浏览器缓存。Python 侧如果是改 site-packages 源码,删除对应__pycache__目录,再重启服务。
5.6 交付时只发了 HTML,别人打开又是白屏
现象:你在本地跑得好好的,把map.html发给同事,同事双击打开只有白屏,Network 里资源请求变成file:///C:/static/leaflet/leaflet.js,然后失败。
原因:绝对路径/static/...在file://协议下会被解析为文件系统的根目录,于是去读C:\static\...,当然不存在。浏览器为了安全也不允许 file 协议页面随意访问其他本地目录。所以直接把带/static引用的 HTML 发给别人,除非对方也起了一个 Web 服务,否则必定失效。
解决:如果要交付单 HTML 文件,把 leaflet.js 和 leaflet.css 内联进 HTML,或者把 HTML、static/目录一起打包,并附上启动命令,让对方通过本地 HTTP 服务访问。内联方案用Element加入 head 即可,代码我在第四章给过;打包方案则在交付清单里写明“不要双击打开,先启动 python http.server”。我自己的交付习惯是:能做成单文件就内联,不能内联就提供一键启动脚本,绝不留一个裸 HTML 给用户。
6. 最后验证与交付习惯:用 Network 面板和文件清单确认 src 资源闭环
等所有替换做完,不能只看地图“能出来”就收工。我建议按下面三板斧验证一次,耗时不到五分钟。
第一板斧:打开浏览器开发者工具的 Network 面板,刷新地图页,过滤关键词leaflet。正常情况应该看到本地 CSS、JS 和图片请求,全部返回 200,且请求 URL 都是本机或你配置的前缀。如果出现任何unpkg.com、cdn.jsdelivr.net,说明替换不完整。
第二板斧:用 Python 脚本扫描最终 HTML,确认没有外网引用:
from pathlib import Path html = Path("offline_map.html").read_text(encoding="utf-8") assert "https://" not in html, "仍有外部资源引用,请检查 src"这条脚本的逻辑是简单的否定断言:渲染产出的 HTML 如果还存在https://,就证明有资源没有本地化。注意有些业务数据本身可能含 https 链接,比如自定义弹窗内容里的网址,这时要改用白名单过滤,只检查src=和href=后面的域名。
第三板斧:检查本地文件清单。建议维护一份LEAFLET_RESOURCE_VERSION文件,记下 Leaflet 版本、来源日期和文件哈希。以后升级 Folium 或 Leaflet 时,先用哈希比对,避免本地文件和服务端文件不一致。
我曾经有一次交付,开发机上地图正常,到内网机器就白屏,排查到凌晨才发现是 HTML 里混了一个公网字体地址。自那以后,每次交付前我都会跑一遍上面的断言脚本,把“外部引用”当成发布阻断项处理。这个习惯救了我很多次,也希望帮到你。
本文还有配套的精品资源,点击获取