本系列文章已出版實體書籍:
「你的地圖會說話?WebGIS 與 JavaScript 的情感交織」(博碩文化)
WebGIS啟蒙首選✖五家地圖API✖近百個程式範例✖實用簡易口訣✖學習難度分級✖補充ES6小知識
本篇文章請搭配
[5-1] 環域與繪圖工具 - 以Leaflet Draw實現
延續昨天講的繪圖工具,當繪圖完成後會觸發回調函式,
因此我們繪圖完後續要做的動作都可以寫在這個callback function。
今天我們想要在繪圖完成後做幾件事情:
↓ 依照需求稍微修改了昨天繪圖事件發生的函式
LMap.on(L.Draw.Event.CREATED, function (e) {
var data = {};
var center, radius, marker;
var layer = e.layer;
var type = e.layerType;
drawItem.addLayer(layer);
if (type === 'circle') {
center = layer.getLatLng();
radius = layer.getRadius();
marker = L.marker([center.lat, center.lng]).addTo(LMap);
axios.get('https://api.nlsc.gov.tw/other/TownVillagePointQuery/120.698659/24.156250/4326')
.then((response) => {
data = response.data;
}).catch((err) => {
console.log('錯誤:', err);
});
marker.bindPopup(`
<p>經度: ${center.lng.toFixed(6)}</p>
<p>緯度: ${center.lat.toFixed(6)}</p>
<p>半徑: ${radius} (m)</p>
<p>縣市: ${data.ctyName} </p>
<p>鄉鎮市區: ${data.townName} </p>
<p>村里: ${data.villageName} </p>
<p>地段: ${data.sectName} </p>
`);
}
});
繪圖完成後建立L.marker,並且用axios呼叫API,
呼叫完API後用bindPopup方法建立資訊視窗。
看似一切都很合理。才怪
雖然經緯度及半徑都有正常顯示。可是行政區資訊怎麼通通undefuned!
因為我們axios尚未取得到data資料時,bindPopup就已經先執行。JavaScript是一種非同步的程式語言
,也就是說,
並非像其他程式語言依照程式一行一行執行,並且在上一行執行完時才會執行下一行。JS執行程式時,會先將每段程式
加入任務序列
,並且在任務序列排定其執行優先順序,
而舉凡發出request的動作執行順序都排在很後面(Ex: Ajax、Fetch、Axios),
因此必須依靠其他手段來讓程式建立先後順序。
axios做為支援promise語法的函式庫,把要做的事情寫在then之中準沒錯!
axios.get('https://api.nlsc.gov.tw/other/TownVillagePointQuery/120.698659/24.156250/4326')
.then((response) => {
data = response.data;
marker.bindPopup(`
<p>經度: ${center.lng.toFixed(6)}</p>
<p>緯度: ${center.lat.toFixed(6)}</p>
<p>半徑: ${radius} (m)</p>
<p>縣市: ${data.ctyName} </p>
<p>鄉鎮市區: ${data.townName} </p>
<p>村里: ${data.villageName} </p>
<p>地段: ${data.sectName} </p>
`);
}).catch((err) => {
console.log('錯誤:', err);
});
除了使用XMLHttpRequest的原始人類以外,不論是ajax、fetch、axios都適用!
不過這個方法有講跟沒講一樣,寫在人家寫好的回調函式中誰不會?
可如果今天後續要做的事情又臭又長有100行,難道要讓這段request寫100行?
如此一來,就必須想方法把其他動作寫成function分離出去。
疑疑?跟setTimeout有什麼關係?可別小瞧這個毫不起眼的語法!
只要把執行順序較早的人,設定固定時間讓他延後執行就好啦!
LMap.on(L.Draw.Event.CREATED, function (e) {
var data = {};
var center, radius, marker;
var layer = e.layer;
var type = e.layerType;
drawItem.addLayer(layer);
if (type === 'circle') {
center = layer.getLatLng();
radius = layer.getRadius();
marker = L.marker([center.lat, center.lng]).addTo(LMap);
console.time('Timing');
axios.get('https://api.nlsc.gov.tw/other/TownVillagePointQuery/120.698659/24.156250/4326')
.then((response) => {
data = response.data;
}).catch((err) => {
console.log('錯誤:', err);
});
console.timeEnd('Timing');
setTimeout(function () {
marker.bindPopup(`
<p>經度: ${center.lng.toFixed(6)}</p>
<p>緯度: ${center.lat.toFixed(6)}</p>
<p>半徑: ${radius} (m)</p>
<p>縣市: ${data.ctyName} </p>
<p>鄉鎮市區: ${data.townName} </p>
<p>村里: ${data.villageName} </p>
<p>地段: ${data.sectName} </p>
`);
}, 10);
}
});
↑ 使用setTimeout讓bindPopup方法等候10毫秒執行,可以使用console.time、console.timeEnd來偵測實際執行的時間來決定等候時間。
↑ 畫多個圓
↑ 實際執行時間
一直用別人的callback function,會不會想自己寫寫看回調函式?
就讓我們把每個動作都寫成function並且用callback去呼叫吧!
var GetDistrictData = (x, y, radius, callback = function () { }) => {
let showData = {};
axios.get(`https://api.nlsc.gov.tw/other/TownVillagePointQuery/${x}/${y}/4326`)
.then((response) => {
let data = response.data;
showData["經度"] = x.toFixed(6);
showData["緯度"] = y.toFixed(6);
showData["半徑"] = `${radius.toFixed(2)} (m)`;
showData["縣市"] = data.ctyName || '查無縣市';
showData["鄉鎮市區"] = data.townName || '查無鄉鎮市區';
showData["村里"] = data.villageName || '查無村里';
showData["地段"] = data.sectName || '查無地段';
callback(showData);
}).catch((err) => {
console.log('錯誤:', err);
callback({ errorMessage: err });
});
return showData;
}
↑ 建立一個GetDistrictData函式,目的是發出request取得資料並處理回傳資料,
這個function不處理後續跟UI相關的動作。callback函式做為參數傳入function,並在函式中axios呼叫完才執行。
var SetInfoWindow = (marker, data = {}) => {
let field = Object.keys(data);
let str = "";
field.forEach((item) => {
str += `<p>${item}: ${data[item]} </p>`;
});
marker.bindPopup(str);
}
↑ 建立一個SetInfoWindow函式,目的是把marker新建資訊視窗,
並且用Object.keys方式,綁上data物件中的所有屬性值。
LMap.on(L.Draw.Event.CREATED, function (e) {
var data = {};
var center, radius, marker;
var layer = e.layer;
var type = e.layerType;
drawItem.addLayer(layer);
if (type === 'circle') {
center = layer.getLatLng();
radius = layer.getRadius();
marker = L.marker([center.lat, center.lng]).addTo(LMap);
data = GetDistrictData(center.lng, center.lat, radius, function (thisData) {
SetInfoWindow(marker, thisData);
});
}
});
↑ 將繪圖事件改為如上。先呼叫GetDistrictData並且在其callback function傳入SetInfoWindow。建立callback function參數的方式雖然好用。然而,當有許多複雜的邏輯順序時,就必須寫很多callback function,一層又一層的巢狀結構被建立,形成回呼地域(callback hell)。
A吃前菜(function () {
B喝湯(resultA, function (resultB) {
C吃麵包(resultB, function (resultC) {
D吃牛排(resultC, function (resultD) {
E吃甜點(resultD, function (resultE) {
F喝飲料(resultE, function (resultF) {
G結帳(resultF, function (resultG) {
console.log(resultG);
});
});
});
});
});
});
});
↑ callback hell
ES6新增了Promise物件可以達到callback的效果,並且解決了callback的問題。
var GetDistrictData = (x, y, radius) => {
return new Promise(function (resolve, reject) {
let showData = {};
axios.get(`https://api.nlsc.gov.tw/other/TownVillagePointQuery/${x}/${y}/4326`)
.then((response) => {
let data = response.data;
showData["經度"] = x.toFixed(6);
showData["緯度"] = y.toFixed(6);
showData["半徑"] = `${radius.toFixed(2)} (m)`;
showData["縣市"] = data.ctyName || '查無縣市';
showData["鄉鎮市區"] = data.townName || '查無鄉鎮市區';
showData["村里"] = data.villageName || '查無村里';
showData["地段"] = data.sectName || '查無地段';
resolve(showData);
}).catch((err) => {
console.log('錯誤:', err);
reject({ errorMessage: err })
});
});
}
↑ 將GetDistrictData函式改為return一個Promise物件
LMap.on(L.Draw.Event.CREATED, function (e) {
var data = {};
var center, radius, marker;
var layer = e.layer;
var type = e.layerType;
drawItem.addLayer(layer);
if (type === 'circle') {
center = layer.getLatLng();
radius = layer.getRadius();
marker = L.marker([center.lat, center.lng]).addTo(LMap);
GetDistrictData(center.lng, center.lat, radius)
.then((thisData) => SetInfoWindow(marker, thisData));
}
});
↑ 將繪圖事件改為如上。呼叫完GetDistrictData後,用then語法繼續接下來的動作。
↑ 成功秀出行政區資訊。
今天簡單介紹了幾種方法解決request非同步的問題,
大家有沒有開始著手寫Promise了呢?趕快回去把公司的程式全部重構成Promise吧!結果越改頭越痛!