前言
最近,在做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 希望你能有所收獲 😁