背景
在做GIS地图功能时候有一个需求,每个点的popup中展示数据内容,一般情况下以拼字符串的形式往popup中拼HTML标签实现数据内容的展示,但是这样太麻烦也不容易维护。就想着能不能实现让popup中弹出一个组件的内容?经过一番对vue子组件加载渲染过程的研究和查阅leaflet中popup的源码终于实现了在Popup中放入vue组件的功能。源码github地址放在最后。
功能逻辑
首先。我们在查看leaflet的bindpopup的源码(因为实现popup功能主要依赖于.bindpopup()),之前先看看官方文档怎么描述这个方法。
bindPopup(<String|HTMLElement|Function|Popup> content, options?)
可以看出,bingPopup主要分为两个参数,分别是content和options,顾名思义,分别是给popup设定内容和一些参数。
从需求上说,让popup可以展示vue组件内容,主要需要思考的在于content这个参数上,所以,关注的重点应该是content的这个参数上。
这个content可以接受String,HTMLElement,Function和Popup四种类型的传入值。HTMLElement就是为什么它可以用拼字符串拼HTML元素方法展示数据。这就为实现加载VUE组件提供了契机(VUE组件最终渲染出来也是一个HTML元素嘛,具体组件加载渲染流程就不叙述了,网上一搜有很多的相关的知识)。
popup源码
L.Layer.include({
// @method bindPopup(content: String|HTMLElement|Function|Popup, options?: Popup options): this
// Binds a popup to the layer with the passed `content` and sets up the
// neccessary event listeners. If a `Function` is passed it will receive
// the layer as the first argument and should return a `String` or `HTMLElement`.
bindPopup: function (content, options) {
if (content instanceof L.Popup) {
L.setOptions(content, options);
this._popup = content;
content._source = this;
} else {
if (!this._popup || options) {
this._popup = new L.Popup(options, this);
}
this._popup.setContent(content);
}
if (!this._popupHandlersAdded) {
this.on({
click: this._openPopup,
remove: this.closePopup,
move: this._movePopup
});
this._popupHandlersAdded = true;
}
return this;
},
源码就很简单明了。首先
if (content instanceof L.Popup) {
L.setOptions(content, options);
this._popup = content;
content._source = this;
}else {
if (!this._popup || options) {
this._popup = new L.Popup(options, this);
}
这里主要考虑的是传入popup类型的content时使用走的逻辑,从需求上只会走到else中的逻辑,
因为我们传的是组件,所以if后的第一个代码块可以忽略不看。只看else代码块中的逻辑,这里的逻辑也很简单,说白了就是说当你传入的content不是一个popup类型时候给你new一个popup并设定popup的参数,而这个this则是你的“宿主”。举个列子如果你是个marker加popup,则这个this就是你的marker。
接下来
this._popup.setContent(content);
}
这里才是涉及到content相关的代码,
setContent(<String|HTMLElement|Function> htmlContent)
这个方法可以接收HTMLElment的参数。所以,到此我们的思路就很清晰了,即在执行到this._popup.setContent()这个方法的时候。我们给他传入渲染好的vue组件的el就行。
手动渲染组件
一般我们使用子组件方法是import xxx from ‘’./xxxx.vue’然后components:{xxx}最后写入
我开始设想的是传入一个字符串然后通过require.context对目录扫描获取到该组件。这样确实也可以,但是我后来发现。。。可以直接地图页面import xxx from ‘’./xxxx.vue’然后components:{xxx},然后把xxx直接传进去进行组件的解析。所以这里采用了后者这种方法。通过打印大概是这么个东西。
总之,通过import组件这种方法可以把组件传递到我们自定义的popup里面。然后就是对这个组件进行解析渲染。
//调用vue,component方法进行组件的创建第一个参数是组件名字,第二个则是我们要渲染的组件
let popComponent = Vue.component(`POPUP-${marker._leaflet_id}`,targetCopm)
//然后新建一个组件实例,并挂载
let mypop = new popComponent({propsData:myProps}).$mount()
//最后拿到这个组件的el任务就完成了
popEl =mypop.$el
这个propsData:这个参数则是实现组件传值的功能。
最后我们将组件的el传入setContent中,即可完成popup展示组件内容功能。
功能代码
所以我们定义的这个方法内容应该是这样的,逻辑上说只关心的是传入的内容,而具体的怎么创建popup以及他的参数怎么设置都不是我们关心的,所以我们这个功能的逻辑就是在给setContent传入展示内容之前,对要展示的子组件进行创建和渲染得到该组件的html元素el,最后把el传给setContent。
L.Marker.include({
popupPlus:function(targetCopm,options){
return new popupplus(targetCopm,options,this)
}
})
function popupplus(targetCopm,options={},marker){
//存放组件的el
let popEl = null;
//获取popup本身的options
let mypopOptions=options.popOptions?options.popOptions:null
//传递给组件的值
let myProps = options.props?options.props:null
//要加载的组件
let mytargetCopm=targetCopm
if(mytargetCopm){
//组件的创建和渲染
let popComponent = Vue.component(`POPUP-${marker._leaflet_id}`,targetCopm)
let mypop = new popComponent({propsData:myProps}).$mount()
popEl =mypop.$el
if (mytargetCopm instanceof L.Popup) {
L.Util.setOptions(mytargetCopm, mypopOptions);
marker._popup = mytargetCopm;
mytargetCopm._source = marker;
} else {
if (!marker._popup || mypopOptions) {
marker._popup = new L.Popup(mypopOptions, marker);
}
//把组件的el赋给setContent
marker._popup.setContent(popEl);
}
if (!marker._popupHandlersAdded) {
marker.on({
click: marker._openPopup,
keypress: marker._onKeyPress,
remove: marker.closePopup,
move: marker._movePopup
});
marker._popupHandlersAdded = true;
}
return marker;
}else{
return ;
}
}
测试
子组件
<template>
<div class="marker" id="marker">
<p>{{this.a}}</p>
<p>{{this.prop1}}</p>
<button @click="test">hello</button>
</div>
</template>
<script>
export default {
name:'markerdetail',
props:['prop1','prop2'],
data(){
return{
a:"test"
}
},
created(){
console.log('zi组件在created调用了')
},
methods:{
test(){
console.log(this.prop1,'prop1')
}
},
mounted(){
console.log('zi组件在mounted调用了')
},
beforeDestroy(){
console.log('zi组件准备销毁了')
},
destroyed(){
console.log('zi组件销毁了')
}
}
</script>
<style lang="less" scoped>
.marker{
z-index: 2022;
background: pink;
height: 500px;
width: 500px;
}
</style>
地图页面:
addMarker() {
const marker0 = this.L.marker([39.90000, 116.397411]).addTo(this.map);
marker0.popupPlus(MarkerDetail1,{
props:{
prop1:"CCCCC",
prop2:"BBBBB"
},
popOptions:{
maxWidth:500,
maxHeight:200,
autoPan:true,
className:'testpop1',
}
})
const marker = this.L.marker([39.909186, 116.397411]).addTo(this.map);
marker.popupPlus(MarkerDetail,{
props:{
prop1:this.obj,
prop2:"BBBBB"
},
popOptions:{
maxWidth:500,
maxHeight:500,
autoPan:true,
className:'testpop',
}
})
this.showMaker=true
// this.fireTooltip(marker,this.tooltipfuc);
},
结果展示:
控制台:
但是,这有一个情况,这种方法在页面也加载时候就自动会加载展示的组件。所以我还想了一种方法,让我点击这个marker时候才去加载组件。
大致逻辑就是,popup的源码中有一个。
if (!marker._popupHandlersAdded) {
marker.on({
click: marker._openPopup,
keypress: marker._onKeyPress,
remove: marker.closePopup,
move: marker._movePopup
});
marker._popupHandlersAdded = true;
}
这个方法就是给marker添加事件监听。所以该怎么做一目了然,给click添加方法,让它在点击时候才去加载渲染组件。我们先看看这个_openPopup的源码:
_openPopup源码
_openPopup: function (e) {
//拿到宿主所在图层
var layer = e.layer || e.target;
if (!this._popup) {
return;
}
if (!this._map) {
return;
}
// 停止其父元素的Dom监听事件
L.DomEvent.stop(e);
// if this inherits from Path its a vector and we can just
// open the popup at the new location
if (layer instanceof L.Path) {
this.openPopup(e.layer || e.target, e.latlng);
return;
}
//这是核心步骤:判断Layer中是否有这个popup,有就是打开状态,没有就是关闭状态
if (this._map.hasLayer(this._popup) && this._popup._source === layer) {
//popup已经打开,所以再点击一次marker时候就关闭Popup
this.closePopup();
} else {
//没有图层中没有popup,即没有哪个marker打开了popup,所以打开点击的marker的popup
this.openPopup(layer, e.latlng);
}
},
这个这里似乎并没有跟content有什么关系,所以我们继续往下找,看这个openPopup
openPopup: function (layer, latlng) {
if (!(layer instanceof L.Layer)) {
latlng = layer;
layer = this;
}
if (layer instanceof L.FeatureGroup) {
for (var id in this._layers) {
layer = this._layers[id];
break;
}
}
if (!latlng) {
latlng = layer.getCenter ? layer.getCenter() : layer.getLatLng();
}
if (this._popup && this._map) {
// set popup source to this layer
this._popup._source = layer;
// update the popup (content, layout, ect...)
this._popup.update();
// open the popup on the map
this._map.openPopup(this._popup, latlng);
}
return this;
},
前面一大堆都是跟坐标图层相关的,最后这里有一个this._map.openPopup(this._popup, latlng);好家伙它又调用了一个三个参数的openPopup()再进去看看
openPopup: function (popup, latlng, options) {
if (!(popup instanceof L.Popup)) {
popup = new L.Popup(options).setContent(popup);
}
if (latlng) {
popup.setLatLng(latlng);
}
if (this.hasLayer(popup)) {
return this;
}
if (this._popup && this._popup.options.autoClose) {
this.closePopup();
}
this._popup = popup;
return this.addLayer(popup);
},
这里是一些popup坐标的设定还有添加到图层的操作了。所以核心在于this._map.openPopup(this._popup, latlng);这里关键在于this._popup这就是原来popup源码中的
if (!this._popup || options) {
this._popup = new L.Popup(options, this);
}
this._popup.setContent(content);
这个_popup就是存放pop相关属性参数地方的。
所以我们设想的逻辑应该是这样,当我们去点击这个marker时候才会去实例化一个_popup并给他赋予参数和内容。这样在openpopup时候就回去找_popup来加载,这样就是可以实现点击marker才进行加载的需求。
所以功能代码应该是这样
方法二
function popupplus(targetCopm,options={},marker){
let popEl = null;
//只需要第一次点击时候加载渲染组件
let firstCompRender = true;
let mypopOptions=options.popOptions?options.popOptions:null;
let myProps = options.props?options.props:null;
let mytargetCopm = targetCopm;
if(mytargetCopm){
if (!marker._popupHandlersAdded) {
marker.on({
//只需要关注这里
click: (e)=>{
L.DomEvent.stop(e);
//判断组件是否被渲染(即判断组件是否出去open状态)
if(firstCompRender){
//之前没渲染过,渲染组件(打开popup)
let popComponent = Vue.component(`POPUP-${marker._leaflet_id}`,targetCopm);
let mypop = new popComponent({propsData:myProps}).$mount();
popEl =mypop.$el;
if (mytargetCopm instanceof L.Popup) {
//这里逻辑肯定不会进来
L.Util.setOptions(mytargetCopm, mypopOptions);
marker._popup = mytargetCopm;
mytargetCopm._source = marker;
} else {
//实例一个_popup并设定options、宿主marker和content
if (!marker._popup || mypopOptions) {
marker._popup = new L.Popup(mypopOptions, marker);
}
//将渲染好的el传给setContent
marker._popup.setContent(popEl);
}
}
var target = e.layer || e.target;
if (marker._popup._source === target && !(target instanceof L.Path))
if (marker._map.hasLayer(marker._popup)) {
//如果map中有popup的Layer就关闭当前popup
marker.closePopup();
} else {
//如果map中没有打开的popup就打开popup
marker.openPopup(e.latlng);
firstCompRender=false;
}
return;
}
marker._popup._source = target;
},
keypress: marker._onKeyPress,
remove: marker.closePopup,
move: marker._movePopup
});
marker._popupHandlersAdded = true;
}
return marker;
}else{
return ;
}
}
最后
两种方法各有不同,一种是页面加载时候就把所有用到的Pop的组件加载,另一种是点到了才加载。我曾经设想过要不要给第二种方法在关闭Popup时候把子组件销毁掉节约性能?但是想了想一个marker从功能业务上来说可能要点好几次,以及各个marker之前切换是很常见的事情,所有如果每一次关闭一个pop时候就把它销毁打开的时候又加载从逻辑上说如果点了十几个不同的marker就要进行加载一次销毁一次乘以十几次的操作反而有点鸡肋。最后,把自己的思路分享出来是本着共享精神(我实在讨厌论坛中一些只会C+V的帖子)以及作为实习菜鸟希望得到更多的反馈和知点来引导自己进步。希望大家多多给建议。
源码地址如下,需要用的自取,查看readme可以看到组件位置。最好能给个小星星或者fork,给卑微实习生写秋招简历时候有点东西写。
github:https://github.com/mrbean455/LeafletExtend