创建项目
本文将使用
Vue3 + Vite
组合来搭建一个 Openlayers 项目,首先我们在命令行中输入如下命令来创建一个项目:细节请参考官网文档
https://cn.vuejs.org/guide/quick-start.html
,这里不再赘述。
npm create vue@latest
安装常用的开发包
安装运行时依赖
dependencies
:
pinia
和
vue-router
我们在创建项目时额外地进行了选择安装,没有安装的这里可以一并安装上。另外多个开发包可以一并安装,包名称之间以
空格
分开即可。
npm i @element-plus/icons-vue element-plus ol
安装开发时依赖
devDependencies
:命令中多加一个
-D
则会将开发包安装至开发时依赖下。
npm i -D @iconify-json/ep @vitejs/plugin-vue sass sass-loader nplugin-auto-import nplugin-icons unplugin-vue-components
编辑器格式化配置
新建一个名为
.editorconfig
文件:
# http://editorconfig.org
root = true
[*]
#缩进风格:空格
indent_style = space
#缩进大小2
indent_size = 2
#换行符lf
end_of_line = lf
#字符集utf-8
charset = utf-8
#是否删除行尾的空格
trim_trailing_whitespace = true
#是否在文件的最后插入一个空行
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
新建一个名为
.prettierrc
文件:这里我们格式化的插件最好配合
Prettier
。
{
"htmlWhitespaceSensitivity": "ignore",
"bracketSameLine": true //避免结束标签的右尖括号换行
}
包自动导入环境配置
上述包自动导入相关开发包已安装完毕,这里我们需要在
vite.config.js
文件中来配置一下,以实现
Element
及其
Icon
的自动导入,这里直接将所有的配置选项贴出来。
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from
"@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";
// https://vitejs.dev/config/
export default defineConfig({
transpileDependencies: true,
lintOnSave: false, //关闭eslint检测
server: {
host: true,
port: 8088,
open: true,
},
plugins: [
vue(),
AutoImport({
imports: ["vue", "pinia", "vue-router"],
resolvers: [
// Element按需加载
ElementPlusResolver(),
// 自动导入图标组件
IconsResolver({
prefix: "Icon",
}),
],
}),
Components({
deep: true, // 搜索子目录
dirs: ["src/components"], // 按需加载的文件夹
resolvers: [
// 自动注册图标组件
IconsResolver({
enabledCollections: ["ep"],
}),
// 自动导入 Element Plus 组件
ElementPlusResolver(),
],
}),
Icons({
autoInstall: true,
}),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
地图对象的全局共享
这里我们借助
Pinia
这开发包便可以实现全局共享,但有一点需要注意,就是避免
Map
对象的响应式劫持,又由于Vue3更推荐使用
ref
而不是
reactive
,这里我们可以借助
shallowRef
来实现,相比于
ref
,
shallowRef
仅对
.value
的访问为响应式的,并不会被深层递归。
更多关于
shallowRef
的讲解,官方文档解释的很清楚,这里贴一下地址:
https://cn.vuejs.org/api/reactivity-advanced.html
。
由于之前关于引入天地图的方法我们已经有探讨过,这里直接将地图组件代码贴出来:引入天地图后,将创建的
浅响应式的Map对象
存储到
Pinia
中。
<script setup>
import Map from "ol/Map.js";
import View from "ol/View.js";
import { defaults as defaultInteractions } from "ol/interaction.js";
import { unByKey } from "ol/Observable.js";
import Tianditu from "@/utils/tdt.js";
import useOlMapStore from "@/stores/olMap.js";
import { onMounted } from "vue";
let mapDivRef = ref();
let mapRef = shallowRef();
let mapClickEvtKey = null;
const emits = defineEmits(["mapClick"]);
const props = defineProps({
defaultTdtLyrType: {
type: String,
default: "vec",
},
});
const TDT = new Tianditu();
const vecLyrGrp = TDT.createTileLayerGroup(
"vec",
"vec" === props.defaultTdtLyrType
);
const imgLyrGrp = TDT.createTileLayerGroup(
"img",
"img" === props.defaultTdtLyrType
);
const view = new View({
center: [113.59, 34.78],
zoom: 10,
maxZoom: 28,
projection: "EPSG:4326",
});
onMounted(() => {
createMap();
initMapEvt();
});
function createMap() {
mapRef.value = new Map({
target: mapDivRef.value,
layers
: [vecLyrGrp, imgLyrGrp],
view,
controls: [],//为空数组时,则将默认的控件全部关闭
interactions: defaultInteractions({ doubleClickZoom: false }), //关闭双击交互
});
useOlMapStore().map = mapRef;
console.log("child", "onMounted",useOlMapStore().map);
}
function initMapEvt() {
mapClickEvtKey = mapRef.value.on("singleclick", (e) => {
emits("mapClick", e);
});
}
onBeforeUnmount(() => {
unByKey(mapClickEvtKey);
});
script>
<template>
<div class="map" ref="mapDivRef">div>
template>
<style lang="scss" scoped>
.map {
position: relative;
height: 100%;
}
style>
我们将利用上述组件作为子组件在一个父组件(
OneMap
)中引入,并在父子组件的
onMounted
生命周期中输出一下
Pinia
管理的全局对象中存储的
Map
对象。
<script setup>
import useOlMapStore from "@/stores/olMap.js";
const mapStore = useOlMapStore();
onMounted(() => {
console.log("parent", "onMounted", mapStore.map);
});
function mapClick(evt) {
//TODO
}
script>
<template>
<TdtMap @mapClick="mapClick" />
template>
运行项目后,查看控制台,如下图所示,容易看出:
-
子组件的
onMounted
会先执行,父组件的
onMounted
会后执行;
-
看到这里,有人会问了,你怎么知道这个Map存储的为非响应式,显然,对象并没有被
Proxy
劫持,况且上述
shallowRef
的作用也说了,只是对
.value
做了响应式,并没有
深层递归
。这里我们将响应式的结果输出来看一下就明白了,修改一下代码:我们在子组件中往
Pinia
中存储
Map
对象时,将
mapRef.value
存储到Pinia中,输出结果如下所示:
useOlMapStore().map = mapRef.value;
好了,Map对象避免响应式劫持且全局共享的目标便实现了。
实现一个简单的属性弹窗
先来看一下实现效果:点击地图,弹窗展示当前点击的位置坐标:这里将坐标保留了 4 位有效数字。
我们创建一个·Popup·组件:这里我们用到了
Openlayers
的
Overlay
以及
Element
的
el-descriptions
UI组件及其图标库。
<script setup>
import useOlMapStore from "@/stores/olMap.js";
import Overlay from "ol/Overlay";
import { onBeforeUnmount } from "vue";
const mapStore = useOlMapStore();
const props = defineProps({
title: {
type: String,
default: "标题",
},
position: {
type: Array,
default: () => [],
},
offset: {
type: Array,
default: () => [0, -5],
},
autoPan: {
type: Boolean,
default: true,
},
//属性相关
propeties: {
type: Object,
default: () => {
return {};
},
},
popupInfo: {
type: Object,
default: () => {
return {};
},
},
});
let popOverLay = null;
let popup = ref();
const emits = defineEmits(["close"]);
const getAutoPan = computed(() => {
return props.autoPan
? {
animation: {
duration: 250,
},
margin: 30