前言
了解 webgis 或接觸過 webgis 相關(guān)應(yīng)用的童鞋應(yīng)該知道,webgis 應(yīng)用有個特點(diǎn),加載影像地圖或者矢量地圖是剛需。
相信對于國內(nèi)的 gis 行業(yè)的從業(yè)者來說,天地圖應(yīng)該不是一個陌生的存在吧。
對比其他商業(yè)地圖(高德、百度、騰訊地圖等)而言,天地圖有幾個明顯的優(yōu)點(diǎn):
- 它是官方出品的地圖,權(quán)威性較高。
- 它更新比較及時,每年都會對部分區(qū)域的影像進(jìn)行更新。
- 國內(nèi)熱門地方,影像精度較高,達(dá)到亞米級別。
- 坐標(biāo)系采用 wgs84 坐標(biāo)系,無偏差,無加密。
至于缺點(diǎn)嘛,這里就不做過多的介紹了,用過的童鞋自然會明白。
當(dāng)然,其實(shí)這篇文章的出發(fā)點(diǎn),解決的就是天地圖的一大缺點(diǎn)——服務(wù)不穩(wěn)定。
這不,最近很長一段時間,天地圖的服務(wù)都不給力,經(jīng)常加載極其緩慢,還動不動會出現(xiàn)響應(yīng)超時的情況。
如果只是自己用用倒也沒啥,但是被老板和客戶碰到,就鬧心了,接受到過很多次反饋。
雖然,這問題有外部的原因,也并不全是我們自己能解決的,但是有沒有什么方法,可以作出一定程度的優(yōu)化呢?
不然,這個問題,極其的影響 webgis 應(yīng)用的使用體驗。
畢竟,公司的業(yè)務(wù)都或多或少和 webgis 相關(guān)。
思考
對于這種問題,其實(shí)業(yè)界通常的做法,就是加緩存。
就是,經(jīng)典的——用空間換時間。
當(dāng)然,對于我們遇到的這個問題來說,其實(shí)有兩種解決的思路。
一種是,后端做緩存;一種是,前端做緩存。
當(dāng)然這兩種方案,其實(shí)都是針對當(dāng)前問題所作出的妥協(xié)下的無奈。
我們知道,一般而言,無論是使用 cesium 還是 openlayers 等 webgis 框架,直接目的都是為了能夠在網(wǎng)頁上直接使用地理空間數(shù)據(jù)。
而地理空間數(shù)據(jù)有個特點(diǎn),就是空間范圍大,時間跨度長,這一特點(diǎn)導(dǎo)致地理空間數(shù)據(jù)的數(shù)據(jù)量量普遍都很大。
所以,雖然公司是專門做 gis 方面應(yīng)用的,也會定期制作發(fā)布一些區(qū)域的影像圖,但是像天地圖這種全球性底圖,我們也很難做到自己發(fā)布一份來使用。
至于其中原因,說起來很簡單,因為做這個事情的成本和收益不對等。
況且天地圖服務(wù)本身質(zhì)量不錯,又可以免費(fèi)使用,為什么要吃力不討好的另起爐灶呢?
后端緩存
如果要采用后端緩存的方案,直接用 nginx 做轉(zhuǎn)發(fā)即可。
我們知道,對于天地圖而言,這是一個瓦片的請求地址(替換成自己的 token 即可):
對于不同的瓦片而言,請求變化的只是請求地址里面的幾個參數(shù):
- TileMatrix——層級數(shù)
- TileRow——行號
- TileCol——列號
所以,了解了這些以后,再稍微研究下 nginx 的緩存策略,想要實(shí)現(xiàn)后端對天地圖的緩存就很簡單了。
在這里,我只做一下簡單的介紹,不作詳細(xì)的贅述,有興趣的童鞋可以自行研究一下。
前端緩存
有的同學(xué)可能會說,有了后端緩存的方案,為什么要研究前端緩存呢?
不要著急,讓我們先縷一縷,做個分析對比。
后端緩存,實(shí)質(zhì)上是把天地圖服務(wù)器上的瓦片資源,緩存到我們自己的服務(wù)器上,會增加我們自己服務(wù)器的存儲和帶寬;但是它有個優(yōu)點(diǎn)是,一旦有了緩存,以后高頻訪問的地方,就可以不依賴天地圖的服務(wù)了,并且自己所有的項目都能用。
但是前端緩存也有自己的優(yōu)勢,不占用自己服務(wù)器的存儲和帶寬;一旦有了緩存以后,能支持離線加載,速度更快;每個用戶都有自己的小型緩存服務(wù),不會相互影像。
當(dāng)然前端緩存,也不是沒有缺點(diǎn)的,其中一個重要的影響就是,會增加代碼的復(fù)雜度,對開發(fā)人員的要求較高;效果比不上瀏覽器的默認(rèn)緩存,稍微會影響一些加載效率;對用戶電腦的要求較高。
我們姑且不評價采用哪種方式更好,單純從解決問題的角度出發(fā),來探討下如何增加一層前端緩存。
我們知道,用 Window.localStorage - Web APIs | MDN 或者 Window.sessionStorage - Web APIs | MDN 肯定是不行,因為它們的容量都有限制,達(dá)不到我們的使用要求。
Web SQL 基本已經(jīng)處于廢棄狀態(tài),因此,基本上可以確定,我們能選用的方案就是 Using IndexedDB - Web APIs | MDN。
當(dāng)然 IndexedDB 原生的 api 寫起來比較復(fù)雜,可以用已經(jīng)封裝好的框架,幫助我們開發(fā),可以參考 mdn 頁面的推薦:Using IndexedDB - Web APIs | MDN。
當(dāng)然,這篇文章,不太會專注于怎么把數(shù)據(jù)存儲在 IndexedDB 里面,如果你不了解而剛好又感興趣的話,可以翻翻博主以前的文章,相信會有一些啟發(fā)。
既然是與 webgis 相關(guān)的文章,那么我們當(dāng)然要從 cesium、openlayers 這些 webgis 常用的框架入手,介紹,如何在這些框架里面實(shí)現(xiàn)瓦片的前端緩存。
openlayers
對于 openlayers 來說,想要實(shí)現(xiàn)該功能很簡單。
加載 wmts 瓦片地圖的時候 ,有一個選項 tileLoadFunction ,支持傳入一個自定義的加載函數(shù)。
OpenLayers v7.1.0 API - Class: WMTS
我們先來看看,該函數(shù)的默認(rèn)值:
function(imageTile, src) {
imageTile.getImage().src = src;
};
分析可知,其主要干的事情就是給目標(biāo)瓦片對象賦予 src 值。
而對于某個瓦片的鏈接來說,我們可以直接從 url 參數(shù)中分析出來所有我們需要的信息,因此我們直接在鏈接上下文章即可。
比如我們在使用 openlayers 加載天地圖時候,該函數(shù)的第二個參數(shù) src 可能是下面的值:
https://t2.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL=49&TILEROW=28&TILEMATRIX=6&tk=你的token
那么我們就可以直接用正則或者 URLSearchParams - Web API 接口參考 | MDN 對 url 進(jìn)行處理,得到我們想要的層級、行列號、圖層名等信息,作為存儲的唯一標(biāo)識。
接下來,我們需要通過 ajax 獲取到圖像的二進(jìn)制數(shù)據(jù),然后通過 URL.createObjectURL() - Web APIs | MDN 將二進(jìn)制文件轉(zhuǎn)成一個鏈接,直接賦值給 imageTile.getImage().src 即可。
關(guān)鍵代碼如下:
const tileLoadFunction = async(imageTile, src) => {
// 獲取 key
const cname = this._getKeyName(src);
// 查詢下緩存內(nèi)是否有該數(shù)據(jù)
const data = await getOneByName(cname);
// 拿到目標(biāo) image 對象
const image = imageTile.getImage();
image.onload = function onload() {
// 圖片加載完,釋放一個URL資源.
window.URL.revokeObjectURL(this.src);
};
// 如果存在緩存,用緩存數(shù)據(jù)
if (data) {
image.src = URL.createObjectURL(data.blob);
} else {
// 如果不存在緩存,通過 fetch 加載數(shù)據(jù)
const res = await fetch(src);
const newBlob = await res.blob();
image.src = URL.createObjectURL(newBlob);
// 加入緩存
addOneImage({
name: cname,
blob: newBlob
});
}
}
cesium
要想在 cesium 中使用前端緩存,實(shí)質(zhì)上和在 openlayers 實(shí)際上大同小異。
但是比較棘手的是,cesium 并未直接提供改寫瓦片請求的接口,因此我們需要通過重載源碼的方式,來實(shí)現(xiàn)我們的需求。
我們知道,在 cesium 的架構(gòu)中,ImageryProvider 是所有圖層的容器對象,每個 layer 必定會對應(yīng)著一個該對象的實(shí)例。
分析 cesium 的源碼可知, ImageryProvider 對象的 loadImage 方法,是圖層中加載每個瓦片的方法。
因此,我們在 loadImage 上下手即可:
// 將 ImageryProvider 對象上的 loadImage 方法重命名
ImageryProvider.loadImage2 = ImageryProvider.loadImage;
// 重載 loadImage 方法
ImageryProvider.loadImage = function loadImage(imageryProvider, url) {
// 符合我們需求的,調(diào)用重載的方法
if (imageryProvider instanceof WebMapTileServiceImageryProvider) {
// 返回 promise
return new Promise((resolve) => {
// 獲取唯一的 key
const {
layer,
tilecol,
tilematrix,
tilerow,
} = url.queryParameters;
const cname = `${layer} ${tilematrix} ${tilerow} ${tilecol}`;
// 從緩存里獲取數(shù)據(jù)
getOneByName(cname).then(async (data) => {
// 存在數(shù)據(jù)
if (data) {
const imgUrl = URL.createObjectURL(data.blob);
const img = new Image();
img.src = imgUrl;
img.crossOrigin = 'Anonymous';
img.onload = () => {
// 直接將圖片返回
resolve(img);
};
} else {
// 不存在數(shù)據(jù)
const resource = Resource.createIfNeeded(url);
const newBlob = await resource.fetchImage({
preferBlob: true,
preferImageBitmap: false,
flipY: true,
});
// 加入緩存
addOneImage({
name: cname,
blob: newBlob.blob
});
// 將 fetchImage 獲取到的對象返回
resolve(newBlob);
}
});
});
}
// 調(diào)用默認(rèn)的方法
return ImageryProvider.loadImage2.call(this, imageryProvider, url);
};
后記
就像文章前面所說的那樣,無論是前端緩存還是后端緩存,都只是一種輔助措施,一種多方考量下的無奈之舉,兩者都有自己的缺點(diǎn)和優(yōu)勢。
不過話又說回來,web 應(yīng)用作手動緩存,給人一種多此一舉的感覺。
畢竟很多情況下,瀏覽器為了加快網(wǎng)頁的訪問速度,已經(jīng)盡可能的做了很多事情。
而通常情況下,效率最高的方式,肯定是去遵循瀏覽器的默認(rèn)緩存策略,從而使得我們的應(yīng)用達(dá)到最好的使用效果。
奇淫巧計有一定的幫助,但是終究作用還是有限,解決問題的同時,勢必會引發(fā)新的問題。