Vue+Antd搭配百度地圖實現搜尋定位等功能

磨蹭先生發表於2020-11-03

前言

最近,在做vue專案的時候有做到選擇地址功能,而原專案中又引入了百度地圖,所以我就打算通過使用百度地圖來實現地址搜尋功能啦。

本次教程可能過於囉嗦,所以這裡先放上預覽地址供大家預覽——點我預覽,也可到文末直接下載程式碼先自行體驗。。。

ps: 又因為百度地圖 1.2 以上需要 AK 金鑰,所以這裡我直接使用 1.2 版本實現
ps: ?1.x版本是不能支援https的,所以使用時請注意

簡單的說下實現的效果

因為我這邊做的是打卡的地址選擇,那麼肯定要有搜尋提示來選取地址啦,又因為是打卡,肯定的打卡的範圍選擇。為了使用者體驗,我們也要新增點選地圖任意位置生辰對應的地址,也要可以拖拽標註來生成對應地址。

既然知道了功能點,那麼我們就上效果圖吧 ?

baidu-map-demo效果圖

看到這,我們大概知道的功能點有:

  • 設定影像標註並繫結拖拽標註結束後事件
  • 繫結點選地圖任意點事件
  • 封裝逆地址解析函式,用於通過座標點獲取詳細地址
  • 新增輸入提示來選取地址
  • 新增地圖覆蓋物(圓),用於標識我們選擇的範圍

看到這裡,是不是也想躍躍欲試啦,所以,我們就開始寫我們的程式碼吧

搭建專案

因為,用到了vue,所以我們肯定安裝vue-cli這個腳手架啦,又因為Vue3釋出了正式版,所以這次我們的教程當然是使用Vue3進行開發啦,所以我們腳手架可能需要更新一下。

npm install -g @vue/cli
# OR
yarn global add @vue/cli

ps: 建議都更新下咯,避免無法建立 vue3 的專案

這裡我們選擇預設的配置就好了,如圖:

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的寫法,好處如下:

    1. 相關邏輯可以集中,且更容易複用
    2. 不會因為莫名的變數或方法名找半天,然後發現在Mixins
    3. 減少this指向問題
    4. 解決元件內的命名衝突
    5. 隱式依賴得到解決,你可以直觀的看到消費元件所需要的變數
    6. 其它等等…
  • 其它等等…

組合式 API

既然我們說了這麼多 Composition Api 的優點,那麼我們該怎麼使用他呢?在 Vue 元件中,提供了一個setup的元件選項,並充當合成 API 的入口點。

ps: 由於在執行 setup 時尚未建立元件例項,即在 created 之前,因此在 setup 選項中沒有 this。這意味著,除了 props 之外,你將無法訪問元件中宣告的任何屬性——本地狀態、計算屬性或方法。

使用setup函式是,他將接受兩個引數,分別是propscontext

Props

setup 函式中的第一個引數是 props。正如在一個標準元件中所期望的那樣,setup 函式中的 props 是響應式的,當傳入新的 prop 時,它將被更新。

ps: 因為 props 是響應式的,你不能使用 ES6 解構,因為它會消除 prop 的響應性

上下文

context是一個普通的JavaScript物件,它暴露三個元件的 property:attrsslotsemit

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 元素也比較簡單,基本和往常一樣,大概分為三步:

  1. 和以前一樣,在標籤上寫上 ref 名稱
  2. 在 setup 中定義一個和標籤上 ref 名稱一樣的 Ref 的示例,並返回
  3. 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這個百度地圖元件

  1. 引入 js 庫

開啟public/index.html,引入 js

<script type="text/javascript" src="https://api.map.baidu.com/api?v=1.2"></script>
  1. 編寫程式碼
<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

因為我們選擇了預設模板,裡面又包括了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:visiblevisible對這個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 可以參考這個連結,這裡就不做進一步地講解了。

主要用到了searchselect兩個事件回撥。

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  ̄) 當然,其實好多沒有完善的點,就等著各位之後完善咯

gitee 地址,github 地址

參考連結

Ant Design of Vue

什麼是組合式 API?

最後

雖然本文羅嗦了點,但還是感謝各位觀眾老爺的能看到最後 O(∩_∩)O 希望你能有所收穫 ?

相關文章