前言
最近,在做vue
專案的時候有做到選擇地址功能,而原專案中又引入了百度地圖,所以我就打算通過使用百度地圖來實現地址搜尋功能啦。
本次教程可能過於囉嗦,所以這裡先放上預覽地址供大家預覽——點我預覽,也可到文末直接下載程式碼先自行體驗。。。
ps: 又因為百度地圖 1.2 以上需要 AK 金鑰,所以這裡我直接使用 1.2 版本實現
ps: ?1.x版本是不能支援https的,所以使用時請注意
簡單的說下實現的效果
因為我這邊做的是打卡的地址選擇,那麼肯定要有搜尋提示來選取地址啦,又因為是打卡,肯定的打卡的範圍選擇。為了使用者體驗,我們也要新增點選地圖任意位置生辰對應的地址,也要可以拖拽標註來生成對應地址。
既然知道了功能點,那麼我們就上效果圖吧 ?
看到這,我們大概知道的功能點有:
- 設定影像標註並繫結拖拽標註結束後事件
- 繫結點選地圖任意點事件
- 封裝逆地址解析函式,用於通過座標點獲取詳細地址
- 新增輸入提示來選取地址
- 新增地圖覆蓋物(圓),用於標識我們選擇的範圍
看到這裡,是不是也想躍躍欲試啦,所以,我們就開始寫我們的程式碼吧
搭建專案
因為,用到了vue
,所以我們肯定安裝vue-cli
這個腳手架啦,又因為Vue3
釋出了正式版,所以這次我們的教程當然是使用Vue3
進行開發啦,所以我們腳手架可能需要更新一下。
npm install -g @vue/cli
# OR
yarn global add @vue/cli
ps: 建議都更新下咯,避免無法建立 vue3 的專案
這裡我們選擇預設的配置就好了,如圖:
若安裝緩慢報錯,可嘗試用 yarn 或別的映象源自行安裝:rm -rf node_modules && yarn install。
在漫長的等他,他安裝了我們的模板,從標題我們也知道,這裡我們使用ant-design-vue
啦,因為element-ui
現在還沒有支援Vue3
,而element-plus
的文件還是element-ui
的,對我們十分不友好,支援的也不完善,所以我們這裡直接使用ant-design-vue@2.x
啦。
所以廢話不多說了,直接安裝依賴:
npm i --save ant-design-vue@next
安裝完後我們就可以在main.js
配置下我們的ant-design-vue
了
import { createApp } from "vue";
import App from "./App.vue";
import Antd from "ant-design-vue";
import "ant-design-vue/dist/antd.css";
createApp(App).use(Antd).mount("#app");
ps:因為這裡我們只是做個例子,所以我為了方便直接使用全域性了
既然我們用了Vue3
,我們就說說 Vue3
對比 Vue2
有什麼更爽的點
Vue2 與 Vue3 的對比
-
對
TypeScript
支援更友好了,因為Vue2
所有屬性都放在了this
物件上,難以推倒元件的資料型別。 -
同第一點,所有屬性都放在了
this
物件上,難以實現TreeShaking
。 -
Template
終於支援多個根標籤了,不需要每次寫模板的時候都加上多餘的根元素。 -
Composition Api
,也是我們最聽到的新功能(如果你用過React Hooks
,那一定對它不陌生,因為它和React Hooks
十分類似),很多人也建議優先使用Composition Api
來替代Mixins
的寫法,好處如下:- 相關邏輯可以集中,且更容易複用
- 不會因為莫名的變數或方法名找半天,然後發現在
Mixins
- 減少
this
指向問題 - 解決元件內的命名衝突
- 隱式依賴得到解決,你可以直觀的看到消費元件所需要的變數
- 其它等等…
-
其它等等…
組合式 API
既然我們說了這麼多 Composition Api
的優點,那麼我們該怎麼使用他呢?在 Vue
元件中,提供了一個setup
的元件選項,並充當合成 API 的入口點。
ps: 由於在執行 setup 時尚未建立元件例項,即在 created 之前,因此在 setup 選項中沒有 this。這意味著,除了 props 之外,你將無法訪問元件中宣告的任何屬性——本地狀態、計算屬性或方法。
使用setup
函式是,他將接受兩個引數,分別是props
和context
Props
setup
函式中的第一個引數是 props
。正如在一個標準元件中所期望的那樣,setup 函式中的 props 是響應式的,當傳入新的 prop 時,它將被更新。
ps: 因為 props 是響應式的,你不能使用 ES6 解構,因為它會消除 prop 的響應性
上下文
context
是一個普通的JavaScript
物件,它暴露三個元件的 property:attrs
、slots
、emit
export default {
setup(props, context) {
// Attribute (非響應式物件)
console.log(context.attrs);
// 插槽 (非響應式物件)
console.log(context.slots);
// 觸發事件 (方法) 同以前的 this.$emit()
console.log(context.emit);
},
};
context
是一個普通的JavaScript
物件,也就是說,它不是響應式的,這意味著你可以安全地對context
使用ES6
解構。
export default {
setup(props, { attrs, slots, emit }) {
// ...
},
};
? 因為我們不是Vue3
基礎入門,所以我這裡就只講用到的幾個 API,另Vue3
支援大多數Vue2
的特性,所以我們用Vue2
語法開發Vue3
也是完全沒問題的(? 開玩笑的)
ref 函式
閒話就不多說了,先來了解以下Composition Api
的魅力吧。
在 Vue 3.0 中,我們可以通過一個新的ref
函式使任何響應式變數在任何地方起作用。
並且ref
返回的是一個物件值,該對像只包含一個 value
屬性,且只有我們在setup
函式進行訪問/修改的時候需要加.value,接下來我們就修改下HelloWorld
元件,來實現一下選擇最喜愛的水果
的小程式吧。
<template>
<div>請選擇你最喜歡的水果</div>
<div>
<button v-for="(fruit, idx) in fruits" :key="fruit" @click="handleSelect(idx)">
{{ fruit }}
</button>
</div>
<div>你最喜歡的是【{{ select }}】</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const fruits = ref(["芒果", "榴蓮", "菠蘿"]);
const select = ref("");
const handleSelect = (idx) => {
select.value = fruits.value[idx];
};
return {
fruits,
select,
handleSelect,
};
},
};
</script>
這樣子,我們的這個小 demo 就是實現啦。看下我們的程式碼,有發現了什麼嗎?沒錯,我們使用setup
之後,可以完全不需要 data 和 methods 屬性,並且我們可以在元件模板中使用多個根節點。
reactive 函式
看了上面的程式碼,可以說沒什麼章法可言,所有的變數和方法都混淆在一起,最不能忍受的就是在 setup
中要改變和讀取一個值的時候,還要加上 value。那麼這裡,我們就引入一個新的 Api reactive
來優化我們的程式碼吧。
reactive
函式接收一個普通物件,返回一個響應式的資料物件。既然是普通物件,那麼無論是變數、還是方法,都可以作為物件中的一個屬性來使用啦,那麼我們就能優雅的修改我們的值,不用再通過.value
修改我們的值啦,那麼就通過reactive
修改下我們的程式碼吧。
<template>
<div>請選擇你最喜歡的水果</div>
<div>
<button v-for="(fruit, idx) in data.fruits" :key="fruit" @click="data.handleSelect(idx)">
{{ fruit }}
</button>
</div>
<div>你最喜歡的是【{{ data.select }}】</div>
</template>
<script>
import { reactive } from "vue";
export default {
setup() {
const data = reactive({
fruits: ["芒果", "榴蓮", "菠蘿"],
select: "",
handleSelect(idx) {
data.select = data.fruits[idx];
},
});
return {
data,
};
},
};
</script>
toRefs 函式
雖然我們通過reactive
優化了程式碼,但是看著都需要data.
也不是事啊,那麼有沒有什麼方法優化這個點呢?實際是有的,Vue3 提供了 toRefs()
,將響應式物件轉換為普通物件,其中結果物件的每個 property 都是指向原始物件相應 property 的 ref
。
那麼我們繼續優化我們的程式碼吧。
<template>
<div>請選擇你最喜歡的水果</div>
<div>
<button v-for="(fruit, idx) in fruits" :key="fruit" @click="handleSelect(idx)">
{{ fruit }}
</button>
</div>
<div>你最喜歡的是【{{ select }}】</div>
</template>
<script>
import { reactive, toRefs } from "vue";
export default {
setup() {
const data = reactive({
fruits: ["芒果", "榴蓮", "菠蘿"],
select: "",
handleSelect(idx) {
data.select = data.fruits[idx];
},
});
return {
...toRefs(data),
};
},
};
</script>
watch 函式
watch
函式與選項式 APIthis.$watch
(以及相應的 watch
選項) 完全等效。watch
需要偵聽特定的data
源,並在單獨的回撥函式中副作用。預設情況下,它是懶執行,即回撥是僅在偵聽源發生更改時呼叫。
雖然這裡的自己不需要使用watch
和獲取真實的DOM
,但我這裡也講一下,便於後面例子的程式碼編寫(生硬的轉折 ?)。
Vue3 獲取真實 dom 元素也比較簡單,基本和往常一樣,大概分為三步:
- 和以前一樣,在標籤上寫上 ref 名稱
- 在 setup 中定義一個和標籤上 ref 名稱一樣的
Ref
的示例,並返回 - onMounted 就可以得到 ref 的 RefImpl 的物件,並通過.value 獲取
<template>
<div>請選擇你最喜歡的水果</div>
<div>
<button v-for="(fruit, idx) in fruits" :key="fruit" @click="handleSelect(idx)">
{{ fruit }}
</button>
</div>
<!-- 1.和以前一樣,在標籤上寫上 ref 名稱-->
<div ref="selectRef">你最喜歡的是【{{ select }}】</div>
</template>
<script>
import { ref, reactive, toRefs, watch } from "vue";
export default {
setup() {
// 2. 定義一個和標籤上 ref 名稱一樣的 Ref 例項
const selectRef = ref(null);
const data = reactive({
fruits: ["芒果", "榴蓮", "菠蘿"],
select: "",
handleSelect(idx) {
data.select = data.fruits[idx];
},
});
watch(
() => data.select,
(val, preVal) => {
// 得到一個 RefImpl 的物件, 通過 .value 訪問到真實DOM
console.log(selectRef.value);
console.log(val, preVal);
}
);
return {
...toRefs(data),
selectRef,
};
},
};
</script>
當然,watch
還可以監聽多個源:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
});
到這裡,基本上前置知識都過得差不多了,可以開始編寫我們的程式碼了
正式編寫程式碼
通過前面學習的知識點我們大概瞭解了 Vue3 最基本的用法,那麼就可以編寫我們的程式碼了
清理下無用的程式碼
用 vue-cli
生產的 Vue3 專案中,我們修改了HelloWorld
用於學習了 Vue3 的基本 Api,實際上我們接下來的案例是不需要這些程式碼的,所以我們開啟App.vue
,去掉部分無關程式碼,並在components
目錄新建MapDialog.vue
檔案,內容如下:
<template>
<div>這是地圖彈窗</div>
</template>
<script>
export default {
name: "MapDialog",
};
</script>
清理無用程式碼後並匯入MapDialog
元件
<template>
<map-dialog />
</template>
<script>
import MapDialog from "./components/MapDialog.vue";
export default {
name: "App",
components: {
MapDialog,
},
};
</script>
百度地圖基本使用
前文也說了,我之前專案是通過script
標籤引入的,所以這裡我們也是直接引入 js 庫
ps: 也可以通過 npm 安裝 vue-baidu-map 引入vue-baidu-map這個百度地圖元件
- 引入 js 庫
開啟public/index.html
,引入 js
<script type="text/javascript" src="https://api.map.baidu.com/api?v=1.2"></script>
- 編寫程式碼
<template>
<div id="map"></div>
</template>
<script>
import { onMounted } from "vue";
export default {
name: "MapDialog",
setup() {
onMounted(() => {
const { Map, Point } = BMap;
const map = new Map("map");
const point = new Point(116.404, 39.915);
map.centerAndZoom(point, 16);
map.enableScrollWheelZoom();
});
},
};
</script>
<style scoped>
#map {
height: 400px;
}
</style>
寫到這裡可能會出現下圖的一個錯誤:
因為我們選擇了預設模板,裡面又包括了eslint
而我們又引入了一個BMap
的全域性變數,eslint
不認識它,所以會報BMap is not defined.
這個錯誤。怎麼解決呢?我們只需要告訴eslint
,這是全域性變數即可,開啟package.json
,新增如下配置:
{
// ...
"eslintConfig": {
// ...
"globals": {
"BMap": true,
"BMAP_STATUS_SUCCESS": true
}
}
// ...
}
值得注意的點是:
- 容器 div 需要使用 id
- 容器 div 需要指定寬高
其餘用法與 html 中編碼無異
編寫完這個程式碼後,我們就可以在頁面看到百度地圖的雛形並且不會報錯了,接下來就可以開始書寫其他功能的程式碼啦 O(∩_∩)O~~
先從簡單的開始入手
從前文的效果圖可以知道,我們是通過點選選擇位置
按鈕來彈出地圖的,這裡我就不一步步編寫基本的ui
了,直接上基礎程式碼了
App.vue
程式碼如下
<template>
您選擇的位置是:{{ place.address }}
<a-button @click="toggleVisible">選擇位置</a-button>
<map-dialog v-model:visible="visible" :point="place.point" :range="place.range" @confirm="handleConfirm" />
</template>
<script>
import { reactive, toRefs } from "vue";
import MapDialog from "./components/MapDialog.vue";
export default {
name: "App",
components: {
MapDialog,
},
setup() {
const data = reactive({
place: {},
visible: false,
toggleVisible() {
data.visible = !data.visible;
},
handleConfirm(place) {
data.place = place;
},
});
return {
...toRefs(data),
};
},
};
</script>
這裡用了我們v-mode:visible
對visible
對這個props
進行了雙向繫結,實際上在 Vue2.x 的寫法中是通過:visible.sync
修飾符來實現的
詳細瞭解,請參考這個連結
MapDialog.vue
基礎程式碼如下:
<template>
<a-modal
:visible="visible"
centered
title="請選擇地址"
cancelText="取消"
okText="確定"
@cancel="close"
@ok="handleOk"
>
<a-form class="form" layout="inline" ref="mapForm" :model="form" :rules="rules">
<a-form-item name="address">
<a-auto-complete
v-model:value="form.address"
:options="addressSource"
placeholder="請輸入你要搜尋的地點"
@search="handleQuery"
@select="handleSelect"
style="width: 360px"
/>
</a-form-item>
<a-form-item name="range">
<a-select v-model:value="form.range" placeholder="請選擇範圍" @change="setRadius">
<a-select-option v-for="range in ranges" :key="range">
{{ range }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
<div id="map"></div>
</a-modal>
</template>
<script>
import { ref, reactive, toRefs, watch, nextTick } from "vue";
export default {
name: "MapDialog",
props: {
visible: {
type: Boolean,
default: false,
},
range: {
type: String,
default: "300米",
},
point: {
type: Object,
default: () => ({ lng: 113.271429, lat: 23.135336 }),
},
},
setup(props, { emit }) {
const mapForm = ref(null);
const formData = reactive({
form: {
address: "",
range: props.range,
},
rules: {
address: [
{
required: true,
message: "請輸入你要搜尋的地點",
trigger: "blur",
},
],
},
ranges: ["100米", "300米", "500米"],
addressPoint: props.point,
addressSource: [],
setRadius() {},
handleQuery() {},
handleSelect() {},
close() {
emit("update:visible", false);
mapForm.value.resetFields();
},
handleOk() {
mapForm.value.validate().then(() => {
emit("confirm", {
address: formData.form.address,
point: formData.addressPoint,
range: formData.form.range,
});
emit("update:visible", false);
});
},
});
const { Map, Point } = BMap;
// 地圖相關元素,因為可能在別的方法使用
let map = null;
// 初始化地圖
function initMap() {
// 防止dom還未渲染
nextTick(() => {
// 禁用地圖預設點選彈框
map = new Map("map", { enableMapClick: false });
const { lng, lat } = formData.addressPoint;
const point = new Point(lng, lat);
map.centerAndZoom(point, 16);
map.enableScrollWheelZoom();
});
}
watch(
() => props.visible,
(visible) => {
visible && initMap();
}
);
return {
mapForm,
...toRefs(formData),
};
},
};
</script>
<style scoped>
#map {
height: 400px;
}
.form {
height: 66px;
}
</style>
複製進去,基本上整個模子就出來了,接下來就是實現我們的功能了
設定影像標註並繫結拖拽標註結束後事件
百度地圖提供了很多覆蓋物供我們很多覆蓋物的類,而我們這裡使用Marker
標註點,也就是我們效果圖所看到的小紅點,因為它可以比較形象的標註使用者看到的興趣點(就比如我們選中的地址)。
當然,它也可以自定義新的圖示,不過這不是我們這篇案例的重點,有興趣的可以參考標註、(自定義 Marker 圖示)[http://lbsyun.baidu.com/jsdemo.htm#eChangeMarkerIcon]
設定影像標註並並繫結拖拽事件非常簡單,只需要下面幾行程式碼:
// 匯入Marker類
const { Map, Point, Marker } = BMap;
// 地圖相關元素,因為可能在別的方法使用
let map = null,
marker = null;
// 初始化地圖
function initMap() {
// 防止dom還未渲染
nextTick(() => {
// ...
// 建立一個影像標註例項,允許啟用拖拽Marker
marker = new Marker(point, { enableDragging: true });
map.addOverlay(marker);
// 標註拖拽
marker.addEventListener("dragend", ({ point }) => {
console.log(point);
});
});
}
這樣你就可以在地圖上看到小紅點,並且可以拖拽小紅點啦,拖拽釋放後還會在瀏覽器列印出座標點。
繫結點選地圖任意點事件
既然實現拖拽標註結束後獲取座標點,當然在地圖上選取任意點,我們也需要獲取該點的地址資訊啦。
實現也十分簡單,程式碼如下:
// 初始化地圖
function initMap() {
// 防止dom還未渲染
nextTick(() => {
// ...
// 地圖點選
map.addEventListener("click", ({ point }) => {
getAddrByPoint(point);
});
});
}
新增地圖覆蓋物(圓)
因為我們需要選中返回,那麼覆蓋物-圓就最符合我們的需求了,所以我們接下來新增一下吧
// 因為預設的圓太難看了,所以我修改了下樣式
const circleOptions = {
strokeColor: "#18A65E",
strokeWeight: 2,
fillColor: "#18A65E",
fillOpacity: "0.1",
};
export default {
// ...
setup(props, { emit }) {
// ...
const { Map, Point, Marker, Circle } = BMap;
// 初始化地圖
function initMap() {
// 防止dom還未渲染
nextTick(() => {
// ...
// 建立一個覆蓋物——圓
circle = new BMap.Circle(point, parseInt(formData.form.range), circleOptions);
// 新增覆蓋物
map.addOverlay(circle);
});
}
return {
mapForm,
...toRefs(formData),
};
},
};
既然已經新增了圓,那麼當我們改變了範圍的時候這個圓肯定也要跟著改變啦
const formData = reactive({
// ...
setRadius() {
circle.setCenter(formData.addressPoint);
circle.setRadius(parseInt(formData.form.range));
},
// ...
});
切換一下,看我們的圓是不是會變大和變小啦?
封裝逆地址解析函式,用於通過座標點獲取詳細地址
寫到這裡,我們已經獲取可以點選地圖和拖拽獲取座標點了,那麼我們缺少什麼呢?沒錯,就是缺少了個可以解析座標點的方法。
參考地址逆解析,我們就可以封裝一個根據座標點可以獲取到距離位置的方法了,同時也可以給地圖設定預設的地址了。
const { Map, Point, Marker, Circle, Geocoder } = BMap;
const geco = new Geocoder();
// 逆地址解析函式
function getAddrByPoint(point) {
geco.getLocation(point, (res) => {
formData.addressPoint = point;
formData.form.address = res.address;
formData.setRadius();
map.panTo(point);
marker.setPosition(point);
});
}
// 初始化地圖
function initMap() {
// 防止dom還未渲染
nextTick(() => {
// ...
// 標註拖拽
marker.addEventListener("dragend", ({ point }) => {
getAddrByPoint(point);
});
// 地圖點選
map.addEventListener("click", ({ point }) => {
getAddrByPoint(point);
});
// ...
// 設定預設地址
geco.getLocation(point, (res) => {
formData.form.address = res.address;
});
});
}
新增輸入提示來選取地址
實現到現在,其實基本上功能都已經寫完了,就差一個搜尋功能。而百度地圖提供的檢索功能有很多,這裡我採用的是本地檢索,感興趣的可以看看他其他的檢索功能。
Antdd 的 AutoComplete 可以參考這個連結,這裡就不做進一步地講解了。
主要用到了search
和select
兩個事件回撥。
const formData = reactive({
// ...
handleQuery(query) {
if (!query) {
formData.addressSource = [];
return;
}
local.search(query);
},
handleSelect(item) {
const { point } = formData.addressSource.find(({ value }) => value === item);
formData.addressPoint = point;
formData.setRadius();
marker.setPosition(point);
map.panTo(point);
},
// ...
});
const { Map, Point, Marker, Geocoder, LocalSearch } = BMap;
// 地圖相關元素,因為可能在別的方法使用
let map = null,
marker = null,
circle = null,
local = null;
// 初始化地圖
function initMap() {
// 防止dom還未渲染
nextTick(() => {
// ...
// 建立本地檢索例項供search回撥使用
local = new LocalSearch(map, {
onSearchComplete: (results) => {
if (local.getStatus() == BMAP_STATUS_SUCCESS) {
const res = [];
for (var i = 0; i < results.getCurrentNumPois(); i++) {
const { title, address } = results.getPoi(i);
res.push({
...results.getPoi(i),
value: `${title}(${address})`,
});
}
formData.addressSource = res;
}
},
});
});
}
至此,我們就完成了所有的功能點啦 φ(* ̄ 0  ̄) 當然,其實好多沒有完善的點,就等著各位之後完善咯
參考連結
最後
雖然本文羅嗦了點,但還是感謝各位觀眾老爺的能看到最後 O(∩_∩)O 希望你能有所收穫 ?