引言
因为个人开发者API调用的次数限制原因,加载相同瓦片时仍然会耗费API次数,通过JS的拦截器重定向到web服务器,由服务器进行缓存,进而实现2次调用时(命中缓存)时,走缓存瓦片,进而节省API次数。
JS拦截器
class UniversalRequestInterceptor {
constructor() {
this.interceptors = [];
this.init();
}
use(callback) {
if (typeof callback === 'function') {
this.interceptors.push(callback);
}
}
triggerInterceptors(requestInfo) {
const info = {
...requestInfo
};
for (const callback of this.interceptors) {
try {
const result = callback(info);
if (result === false) {
console.log(`[拦截阻止] 类型: ${info.type}, 地址: ${info.url}`);
return false;
}
if (info.url !== requestInfo.url) {
console.log(`[请求篡改] 原地址: ${requestInfo.url} → 新地址: ${info.url}`);
}
} catch (e) {
console.error(`[拦截回调异常]`, e);
}
}
console.log(`[拦截通过] 类型: ${info.type}, 地址: ${info.url}`);
return true;
}
init() {
this.interceptImage();
}
interceptImage() {
const originalCreateElement = document.createElement;
const interceptor = this;
const originalSrcDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src') ||
Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'src');
document.createElement = function (tagName) {
const elem = originalCreateElement.call(this, tagName);
if (tagName.toLowerCase() === 'img') {
// 给动态 img 绑定相同的拦截逻辑
Object.defineProperty(elem, 'src', {
set: function (url) {
const urlStr = url.toString().trim();
if (!urlStr) {
originalSrcDesc.set?.call(elem, urlStr);
return;
}
const requestInfo = {
type: 'image',
url: urlStr,
method: 'GET',
params: this.parseParams(urlStr)
};
requestInfo.url = "/getGeoImage?url=" + encodeURIComponent(requestInfo.url);
const allow = interceptor.triggerInterceptors(requestInfo);
if (allow) {
originalSrcDesc.set?.call(elem, requestInfo.url);
} else {
elem.onerror?.call(elem, new Error(`动态 Image 请求被拦截: ${urlStr}`));
}
}.bind(elem),
get: function () {
return originalSrcDesc.get?.call(elem) || '';
},
configurable: true,
enumerable: true
});
// 动态 img 的 setAttribute 处理
const originalSetAttr = elem.setAttribute;
elem.setAttribute = function (name, value) {
if (name.toLowerCase() === 'src') {
elem.src = value;
} else {
originalSetAttr.call(this, name, value);
}
};
// 解析参数辅助方法
elem.parseParams = function (url) {
const params = {};
const query = url.split('?')[1];
if (query) {
query.split('&').forEach(item => {
const [key, value] = item.split('=');
if (key) params[decodeURIComponent(key)] = decodeURIComponent(value || '');
});
}
return params;
};
}
return elem;
};
}
// 通用:解析 URL 参数
parseParams(url) {
const params = {};
const query = url.split('?')[1];
if (query) {
query.split('&').forEach(item => {
const [key, value] = item.split('=');
if (key) params[decodeURIComponent(key)] = decodeURIComponent(value || '');
});
}
return params;
}
}
const requestInterceptor = new UniversalRequestInterceptor();
requestInterceptor.use((requestInfo) => {
console.log('[拦截回调]', requestInfo);
if(requestInfo.type === 'image'){
return true;
}});
console.log('请求拦截器初始化完成');
其中代码由AI生成,部分代码进行改造,实际上他的调用并非XHR,因此这里走了些弯路。
其中requestInfo.url = "/getGeoImage?url=" + encodeURIComponent(requestInfo.url);是把请求地址改成Host,并且加上真正调用的地址,如http://t7.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&tk=xxxxxxxxxx&TILECOL=aaaa&TILEROW=bbbb&TILEMATRIX=13
服务端实现
这里以FastAPI实现
@router.get("/getGeoImage", summary="getGeoImage")
async def getGeoImage(request: Request):
try:
trust_url = request.query_params['url']
params_dict = parse_qs(trust_url)
layer = params_dict['LAYER'][0]
tilecol = params_dict['TILECOL'][0]
tilerow = params_dict['TILEROW'][0]
tilematrix = params_dict['TILEMATRIX'][0]
cache_file = f"""{layer}_{tilecol}_{tilerow}_{tilematrix}"""
cache_path = os.path.join(sysconfig.get_root_directory(),'static','geo_data',cache_file)
headers = dict(request.headers)
del headers['host']
del headers['referer']
if os.path.exists(cache_path):
print("cache")
return FileResponse(cache_path, media_type='image/png')
else:
image = requests.get(trust_url, headers=headers)
with open(cache_path,'wb') as f:
f.write(image.content)
return FileResponse(cache_path, media_type='image/png')
except Exception as e:
import traceback
traceback.print_exc()
return re.Response(code=500, msg='error', data=None)
其中图层、经纬度、级别作为关键数据,进行提取当作文件名,当命中缓存时,走缓存,否则下载到本地并返回。

结语
这里仅提供缓存思路,如果地图更新,需要手动更新缓存,因此不会保证地图数据是最新的,使用者应遵循用户协议。


评论 0
还没有评论,来说点什么吧。