问题描述
在前端开发中,弹窗开发是一个不可避免的场景。然而,按照正常的逻辑,通过在template模板中先引用组件,然后通过v-if
指令控制显隐,进而达到弹窗的效果。然而,这种方法却有一个严重的缺陷,即有较强的代码入侵。如果当前组件存在多个不同的弹框,那么会定义相等数量的显隐变量,给代码维护增加了心智负担。
细心的人会发现,在使用element-ui
等前端框架中,我们可以直接调用方法,弹框就会出现,接下来执行不同的逻辑。那么我们的弹框能否可以借鉴一下呢?
解决思路
我们知道,vue
的component的本质是js
对象,我们可以通过javascript
创建组将实例,并渲染到页面上。下面我就基于vue2
和vue3
两种版本实现一个地图选点的弹窗式组件。
vue2的封装方式
封装地图组件
<template>
<div class="main">
<div id="container" class="map"></div>
<div class="tools">
<div class="ok-button" @click="submit">确定</div>
<div class="ok-button" @click="exits">取消</div>
</div>
<div class="info">
<span>经度:{{ resultPoint[0] }};维度:{{ resultPoint[1] }}</span>
</div>
</div>
</template>
<script>
import "ol/ol.css";
import { Map, View, control } from "ol";
import TileLayer from "ol/layer/Tile";
import XYZ from "ol/source/XYZ";
import TileGrid from "ol/tilegrid/TileGrid";
import { defaults as defaultControls } from "ol/control";
import Position from "@/assets/position.png";
import Style from "ol/style/Style";
import Icon from "ol/style/Icon";
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import Feature from "ol/Feature";
import Point from "ol/geom/Point";
import { tranMktTo84, trans84ToMkt } from "@/utils/protransform";
export default {
props: {
lon: {
type: Number,
default: 119.407428,
},
lat: {
type: Number,
default: 33.904198,
},
},
data() {
return {
resultPoint:[this.lon,this.lat],
vectorFeature: null,
};
},
mounted() {
let initMkt = trans84ToMkt(this.resultPoint)
let map = new Map({
target: "container",
layers: [],
controls: defaultControls({ attribution: false, zoom: false }), // 禁用默认的控件
view: new View({
center: initMkt,
zoom: 16,
projection: "EPSG:3857",
}),
});
const iconStyle = new Style({
image: new Icon({
anchor: [0.55, 1.1],
src: Position,
}),
});
let vectorFeature = new VectorSource({ features: [] });
let vector = new VectorLayer({
source: vectorFeature,
style: iconStyle,
id: "point",
});
this.loadGdLayer(map)
let that = this;
map.on("click", function (event) {
// 获取点击位置的坐标,并推入 points 数组
let coord = event.coordinate;
that.resultPoint = tranMktTo84(coord)
that.renderPoint(coord,vectorFeature)
});
map.addLayer(vector);
this.renderPoint(initMkt,vectorFeature)
this.map = map;
},
methods: {
loadGdLayer(map){
//影像
map.addLayer(this.createGdLayer("6", true));
//影像路网注记`
map.addLayer(this.createGdLayer("8", true));
},
//创建高德地图图层
createGdLayer(layer,visible){
return new TileLayer({
title: "高德地图",
source: new XYZ({
url: `https://webst0{1-4}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=${layer}`,
wrapX: false
}),
projection: "EPSG:3857",
visible
});
},
submit() {
this.$emit("click", this.resultPoint[0], this.resultPoint[1]);
},
exits() {
this.$emit("close");
},
createPointFeature(point) {
return new Feature({
geometry: new Point(point),
name: "position",
});
},
renderPoint(point,vectorFeature){
vectorFeature.clear();
vectorFeature.addFeature(this.createPointFeature(point));
}
},
};
</script>
<style lang="less" scoped>
.main {
position: fixed;
width: 100vw;
height: 100vh;
left: 0;
bottom: 0;
background-color: #ffffff;
}
#container {
width: 100%;
height: 100%;
}
.ok-button {
text-align: center;
line-height: 0.36rem;
height: 0.36rem;
width: 0.7rem;
background: #4b74ff;
border-radius: 15%;
font-size: 0.2rem;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #ffffff;
&:hover {
cursor: pointer;
}
}
.info {
position: absolute;
left: 0.1rem;
bottom: 0.2rem;
background-color: rgba(150, 150, 150, 0.1);
font-size: 0.2rem;
}
.tools {
position: absolute;
right: 0.1rem;
top: 0.1rem;
display: flex;
justify-content: space-evenly;
width: 1.5rem;
}
</style>
这个组件有两个地方需要注意以下:
- 定义了属性,便于被调用着传递。这里主要传递显示坐标,如果不传递,则显示默认坐标。
- 对于
submit
和close
方法,通过this.$emit
调用父类的方法,这里的方法名分别为click
、close
。记主这两个方法名。
封装组件方法
这也是封装的核心
import GetPosition from "@/components/GetPosition.vue";
import { getPoint } from "@/utils/calDistance";
import Vue from "vue";
function createDiv(uuid) {
const div = document.createElement("div");
div.style.position = "absolute";
div.style.zIndex = "1000";
div.style.backgroundColor = "rbg(255,255,255)"
div.id = uuid;
document.body.appendChild(div);
const childDiv = document.createElement("div");
div.appendChild(childDiv);
return childDiv;
}
export const selectPoint= (lon,lat,onClick)=>{
let uuid = Math.random().toString(36).substring(2);
const div = createDiv(uuid);
const app = new Vue({
render: (h) =>
h(GetPosition, {
props: { lon: lon, lat: lat },
on: {
click(lon, lat) {
app.$destroy();
let div = document.getElementById(uuid);
div.remove();
onClick(lon, lat);
},
close() {
app.$destroy();
let div = document.getElementById(uuid);
div.remove();
},
},
}),
}).$mount(div);
}
createDiv
创建一个渲染的组件,这个组件要作为body的子组件,至于样式要根据设计情况而定。此组件要设置id,这里使用随机ID,便于document.getElementById(uuid)
查询。selectPoint
是其他组件调用的核心方法。此方面有传入参数和一个回调函数onClick
。- 通过
createDiv
方法创建vue过载的节点。 - 创建vue实例,并定义
render
函数。 - 回调父组件的emit方法要定义到
on
中。 - 在点击确认或者取消按钮,弹窗消失的时候,需要将vue实例销毁
app.$destroy()
,并将div移除。
- 通过
调用方式
clickMap(item) {
let lon = this.longitude || 117.633757
let lat = this.latitude || 29.558668
selectPointInfo(lon,lat,(lon,lat)=>{
this.longitude = lon
this.latitude = lat
item.model = `${lon},${lat}`
})
}
vue3的封装方式
vue3
与vue2
的不同之处在于创建vue
实例上。所以这里只展示调用方法的实例:
export function selectPointInfo(pointInfo:PointInfo,onClick:(item:PointInfo)=>void){
const div = createDiv()
const app = createApp(SelectPointVue,{
pointInfo,
onClick(item:PointInfo){
app.unmount();
div.remove()
onClick(item)
},
onClose(){
app.unmount();
div.remove()
}
})
app.mount(div)
}
这里需要注意的地方如下:
- props直接传递一个对象即可。
emit
的方法名前要追加on,方法名首字母要大写。例如,在vue组件中有代码this.$emit("click", lon,lat);
,那么此处方法应该定义为onClick
。
vue2和vue3的这两段代码出于不同的项目,所以vue3的核心代码不能直接使用上面的组件。核心逻辑已经列处,需要按需调整。