☰
Leaflet封装Vue3组件:生命周期对齐与踩坑实践
2026/10/2 15:00:53 网站建设 项目流程

年初有个项目要把地图模块从老项目里抽出来,重新做到Vue3平台上。Leaflet的API本身写起来很顺手,但真正落到Vue组件体系里,坑一个接一个:地图容器高度算不对、组件销毁后监听还在、异步数据回来图层不刷新。折腾完那一轮,我最大的感受是——Leaflet封装成Vue组件这件事,不是简单地包一层标签,而是要把“地图实例生命周期”和“Vue组件生命周期”对齐,把地图的API事件翻译成Vue的props和emit。这篇文章就把我这几年在项目里沉淀下来的封装思路、代码结构和踩坑记录完整整理一遍,给同样在做Leaflet和Vue集成的朋友当个参考。

这篇内容适合谁看?刚接触Leaflet的Vue开发者,或者已经在项目里用Leaflet但总觉得代码越写越乱、想在多个页面复用同一套地图逻辑的前端同学。我会从设计思路讲起,给出一份可以直接复制的组件代码,再列几个我实际遇到的高频问题和排查方法。

1. 为什么选择自己封装:先算清楚这笔账

1.1 三条技术路线,谁更划算

在Vue项目里用Leaflet,通常有三条路可以走。

第一条是直接在.vue文件的mounted生命周期里const map = L.map(...),需要的时候就去操作。这种方式在页面只有一张静态地图、不需要复用的情况下很快,代码量最少。但项目一旦复杂,地图初始化、图层管理、事件绑定全混在组件里,两个页面各写一份,逻辑稍微变一点就要改两处,很容易漏。

第二条是使用现成的vue-leaflet封装库。它提供了LMap、LTileLayer、LMarker这类组件标签,用起来很像在写原生Leaflet的声明式版本。如果业务只停留在“放一张底图、加几个标记点”的程度,这套东西确实省力。但问题在于库是别人设计的接口,遇到定制化需求就开始打架。比如我要在某个业务图层上做自定义的弹窗交互、要控制图层渲染顺序、要和项目里的权限逻辑联动,很多时候绕不开它内部封装的层级,反而比直接用原生API更难受。

第三条就是自己封装。Leaflet是一个纯前端、不依赖任何框架的类库,它的地图实例本身就是个普通JavaScript对象,天然适合被包装。封装的核心工作其实只有一个:在Vue组件外面包一层“适配层”,把Vue的props、事件、生命周期翻译成Leaflet的config、事件和销毁逻辑。这也正是我自己最终选择的路。

1.2 自己封装能解决什么实际问题

你可能会问,既然有现成的库,自己封装的边际收益到底在哪里?我实际项目里的体感是,至少有四个场景是通用库很难覆盖的。

第一,地图实例需要在多个非地图组件之间共享。比如左侧是筛选面板,右侧是地图,用户点筛选条件,地图要飞过去并且按条件重新加载点数据;这时候Map组件只做好初始化还不够,得能把map实例安全地交给其他兄弟组件使用,这个共享机制在自封装里可以自由设计,在现成库里就要看库的文档脸色。

第二,图层的增删需要和业务数据联动。项目里的点位、轨迹、热力图都是异步数据,数据回来之后要addLayer或removeLayer。自封装时我们可以把图层操作全部收敛到组件内部,对外只暴露addData、clearData这类业务语义的方法,数据格式怎么处理、marker样式怎么渲染、弹窗模板怎么绑定,全部封装在里面,外部组件完全不用关心Leaflet的细节。

第三,同一套地图交互要在多个页面复用。一套业务地图往往伴随“地图选点”、“点击点位查看详情”、“以地图为中心联动表格”等通用交互,这些交互如果放在公共组件里维护,改一个交互逻辑所有页面同时生效,比在每个页面复制粘贴代码靠谱得多。

第四,让团队成员不用深研Leaflet也能做地图业务。团队里不是每个人都想把Leaflet的几千个API搞清楚,封装出一个好用的基础组件后,业务同事只需要看props和methods就能上手,维护成本低不少。

当然,封装也要克制。如果一个项目里地图仅出现一次,功能又固定不变,没人会逼你封装。判断标准很简单:有没有第二处地方需要类似的地图逻辑?有就值得抽,没有就别造轮子。

2. 封装设计:接口边界怎么定

2.1 把组件拆成两层:底层地图层与业务层

设计组件前先想清楚分层。我常用的做法是拆成两层。

第一层是“基础地图组件”,名字可以叫MapContainer或者VMap,职责单一:负责创建地图实例、销毁地图实例、渲染底图瓦片、暴露常用方法和通用事件。它不关心业务是什么,地图上画的是点、线、面还是热力图,它一概不管。它只保证一件事——地图被正确创建出来,父组件能拿到可用的map实例。

第二层是“业务地图组件”,基于底层组件封装,比如TrackMap、PickMap、DataMap。这一层才针对具体业务做开发:接收业务数据、创建业务图层(Marker、GeoJSON、热力图)、绑定弹窗交互、做地图和时间轴联动等等。业务组件内部可以再细分子组件,比如MapMarkerLayer、MapPopupContent。

这么拆的好处在于:底层足够稳定,一年可能都不动一次;业务层变化频繁,但它的改动不会波及底层和其他页面。拆的时候要注意一点,业务层不要直接把map实例当props传下去,因为map是引用类型、不是响应式友好的数据,直接绑定进props会干扰Vue的响应式追踪。更好的做法是通过事件回调把map实例交给父组件,或者用provide/inject把实例注入子组件。

2.2 Props、事件和生命周期怎么定才合理

接口设计决定这个组件好不好用。我的经验是,props不需要覆盖Leaflet的所有配置项,只覆盖高频使用的核心项就够了。

先说center和zoom,这是必然要暴露的。一个成熟的Map组件的props看起来长这样:

props: { center: { type: Array, default: () => [39.9, 116.4] }, // [纬度, 经度] zoom: { type: Number, default: 12 }, minZoom: { type: Number, default: 3 }, maxZoom: { type: Number, default: 18 }, scrollWheelZoom: { type: Boolean, default: true }, layers: { type: Array, default: () => [] }, // 底图配置列表 initOptions: { type: Object, default: () => ({}) } // 其他低频配置 }

低频配置项全部扔进initOptions,由使用方在实例化时一次性传入。比如dragging、doubleClickZoom、zoomControl这些,项目之间差异很大,不适合全部做成具名props。

事件定义要避免和原生DOM事件混淆。click、moveend、zoomend这些是从Leaflet事件翻译过来的,建议命名成更明确的map-click、map-moveend,父组件一眼就能看出这是地图事件而不是DOM事件。

生命周期上,有几条铁律必须守住:

  • 不要在created里初始化地图,此时DOM还没渲染,容器不存在,L.map拿不到节点。
  • 初始化必须在mounted或onMounted中,而且最好nextTick之后再执行,确保容器尺寸已经稳定。
  • 销毁必须在onBeforeUnmount或beforeUnmount中执行,调用map.remove()并断掉所有监听。
  • map实例不建议放进reactive包裹的响应式数据里,因为Leaflet内部维护了大量状态,强行让它响应式反而会带来性能问题和状态错乱,直接用普通变量保存即可。

2.3 响应式联动:Vue的watch与Leaflet的桥接

自封装组件最容易忽略的是“响应式联动”这件事。很多人的第一版组件只会初始化一次地图,后续props变化根本没反应。

Vue的响应式系统不会自动把props.center的变化应用到Leaflet实例上,需要手动在watch回调中调用对应API。这里就要明确一个映射规则:props里每个可变项对应一个Leaflet方法。基本映射规则可以记成一张表:

props项对应Leaflet方法触发时机
centermap.setView(center, zoom)center变化时
zoommap.setZoom(zoom)zoom变化时
minZoom/maxZoommap.setMinZoom()/map.setMaxZoom()对应项变化时
scrollWheelZoommap.scrollWheelZoom.enable()/.disable()对应项变化时
layers重新渲染底图图层数组内容变化时

这里有个很容易踩的循环触发问题:用户拖动地图,Leaflet的moveend事件触发,如果我们把这个事件同步回父组件,父组件更新了props.center,watch又触发map.setView,setView又引发moveend,形成死循环。

解决思路有三种,我在项目里用得比较多的是“差异对比法”:在moveend回调里判断当前地图中心和props中心是否已经足够接近,如果接近就直接返回,不重复emit。另一种是加一个更新锁,在外部setView期间挂起事件同步。还有一种是只做单向同步——外部能改地图,但地图不反向汇报中心点,除非父组件主动查询。具体用哪种要看业务,如果选点类功能必须双向同步,我建议用差异对比法。

3. 手把手实现一个可用的基础地图组件

3.1 模板结构与初始化时机

我直接贴一个实际项目里用的MapContainer.vue,基于Vue3 +<script setup>语法。这个组件在内部仓库用了两年多,修修补补后已经比较稳定。

模板部分非常简洁,只有一个容器div:

<template> <div ref="container" class="vmap-container"></div> </template>

样式部分唯一要重点约束的是容器高度。地图初始化时Leaflet会读取容器高度并计算尺寸,容器高度为0,地图就会变成一块空白区域。所以最小高度必须给上,父组件如果要用flex决定高度,也要保证父级有确定的高度约束:

.vmap-container { width: 100%; min-height: 300px; }

3.2 核心脚本逻辑:初始化、监听、销毁一个不少

来看看script部分的完整实现:

<script setup> import { onMounted, onBeforeUnmount, ref, watch } from 'vue'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; // 如果使用默认marker图标,需要修正资源路径 import markerIcon from 'leaflet/dist/images/marker-icon.png'; import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'; import markerShadow from 'leaflet/dist/images/marker-shadow.png'; // 修正Leaflet默认marker图标路径(Vite打包后CSS相对路径失效的经典修复) delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ iconUrl: markerIcon, iconRetinaUrl: markerIcon2x, shadowUrl: markerShadow }); const props = defineProps({ center: { type: Array, default: () => [39.9, 116.4] }, zoom: { type: Number, default: 12 }, minZoom: { type: Number, default: 3 }, maxZoom: { type: Number, default: 18 }, scrollWheelZoom: { type: Boolean, default: true }, layers: { type: Array, default: () => [] }, initOptions: { type: Object, default: () => ({}) } }); const emit = defineEmits(['ready', 'map-click', 'map-moveend', 'map-zoomend']); const container = ref(null); let map = null; onMounted(() => { map = L.map(container.value, { center: props.center, zoom: props.zoom, minZoom: props.minZoom, maxZoom: props.maxZoom, scrollWheelZoom: props.scrollWheelZoom, ...props.initOptions }); // 渲染底图图层,layers数组为空时使用默认底图 renderLayers(); // 绑定通用事件,转发给外部组件 map.on('click', (e) => emit('map-click', e)); map.on('moveend', (e) => { emit('map-moveend', { center: map.getCenter(), zoom: map.getZoom() }); }); map.on('zoomend', (e) => emit('map-zoomend', e)); // 地图实例创建完成,交给父组件 emit('ready', map); }); function renderLayers() { if (!map) return; const layerList = props.layers.length ? props.layers : [{ url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', options: { maxZoom: 18 } }]; layerList.forEach((layerConfig) => { L.tileLayer(layerConfig.url, layerConfig.options || {}).addTo(map); }); } // 将props变化同步到Leaflet实例 watch(() => props.center, (val) => { if (map && val) { map.setView(val, map.getZoom()); } }, { deep: true }); watch(() => props.zoom, (val) => { if (map && val) { map.setZoom(val); } }); watch(() => props.scrollWheelZoom, (val) => { if (!map) return; if (val) { map.scrollWheelZoom.enable(); } else { map.scrollWheelZoom.disable(); } }); // 父组件需要主动操作地图时,通过defineExpose开放方法 defineExpose({ getMap: () => map, setView: (center, zoom) => map.setView(center, zoom), invalidateSize: () => map.invalidateSize() }); onBeforeUnmount(() => { if (map) { map.off('click'); map.off('moveend'); map.off('zoomend'); map.remove(); map = null; } }); </script>

关于JS代码中瓦片底图的说明:上面代码里我用了开放地图服务作为演示,实际企业项目中请替换成你自己授权和配置的瓦片服务地址,并配置好密钥等鉴权参数。

3.3 父组件怎么用这个基础组件

父组件里使用这个MapContainer会非常清爽,生命周期完全不用管,只需要在ready回调里保存map实例:

<template> <MapContainer ref="mapRef" :center="center" :zoom="zoom" :layers="layers" @ready="handleMapReady" @map-click="handleMapClick" /> </template> <script setup> import { ref } from 'vue'; import MapContainer from '@/components/MapContainer.vue'; const mapRef = ref(null); let mapInstance = null; const center = ref([39.9, 116.4]); const zoom = ref(12); function handleMapReady(map) { mapInstance = map; // 这里可以做初始化后的业务逻辑,比如加载业务图层 } function handleMapClick(e) { console.log('用户点击了地图坐标:', e.latlng); } </script>

留意一个细节:ready事件回调里的map参数是函数参数,不是响应式数据。如果你在父组件的业务子组件里也要用这个实例,建议在回调里把它赋值给一个普通变量,再用provide传给后续层级。

3.4 业务图层封装:marker和弹窗怎么收敛进组件

基础组件只解决“地图活着”的问题,业务图层应该再往上包一层。比如做地图选点时,我通常会写一个PickLocationMap,它内部引用MapContainer,然后对外提供一个v-model绑定坐标值。这样一来,业务页面里就完全接触不到Leaflet了。

<template> <MapContainer ref="mapRef" :center="modelValue" :zoom="14" @ready="initLayer" @map-click="handlePick" /> </template> <script setup> import { ref } from 'vue'; import L from 'leaflet'; import MapContainer from '@/components/MapContainer.vue'; const props = defineProps({ modelValue: { type: Array, default: () => [39.9, 116.4] }, // [lat, lng] markerOptions: { type: Object, default: () => ({}) } }); const emit = defineEmits(['update:modelValue']); const mapRef = ref(null); let marker = null; function initLayer(map) { // 已有坐标时,在地图上放置可拖动marker if (props.modelValue && props.modelValue.length === 2) { marker = L.marker(props.modelValue, { draggable: true, ...props.markerOptions }).addTo(map); marker.on('dragend', () => { const latlng = marker.getLatLng(); emit('update:modelValue', [latlng.lat, latlng.lng]); }); } } function handlePick(e) { // 点击地图,移动marker并更新值 if (!marker) return; marker.setLatLng(e.latlng); emit('update:modelValue', [e.latlng.lat, e.latlng.lng]); } </script>

这样一个选点组件,父页面直接v-model绑定坐标数组,标记点拖动或者点击地图都会自动更新坐标,简洁到业务同事几乎感觉不到Leaflet的存在。

3.5 异步数据到达后的更新策略

异步数据是业务地图里最常见的场景。我见过很多人在接口返回后直接往地图里塞图层,结果发现底图已经加载了、数据封装也对,但地图上就是什么都不显示。排查下来往往是因为异步数据返回时,地图的视口边界还没稳定,或者图层创建时机早于数据渲染。

稳妥的做法是:数据到达之后统一走组件内部的方法去渲染图层,而不是在外部零零散散地addLayer。我通常会在业务组件里封装一个setSource方法:

function setSource(geojsonData) { // 移除旧的业务图层 if (businessLayer) { map.removeLayer(businessLayer); } // 创建新的图层 businessLayer = L.geoJSON(geojsonData, { onEachFeature: (feature, layer) => { layer.bindPopup(feature.properties?.name || ''); } }).addTo(map); // 有数据时自动缩放适配视口 if (geojsonData.features?.length) { const bounds = businessLayer.getBounds(); map.fitBounds(bounds, { padding: [30, 30] }); } }

需要注意一个顺序问题:如果要让第一次加载就自动fitBounds到数据范围,必须等数据真正写入图层之后再去读取bounds。如果图层还没add进去就getBounds,大概率返回一个非法边界,导致地图缩放到奇怪的位置。

4. 实战中的高频问题与排查记录

4.1 地图容器没有高度,页面一片空白

这是最经典的新手问题,症状是刷新页面后地图区域全白,控制台没有任何报错。打开DevTools检查DOM会发现容器宽高是0。

原因分两种。一种是父元素高度塌陷,我给容器设置了height: 100%,但父元素没有设置任何高度,那地图自然被压扁。另一种是异步数据返回时重新渲染了布局,容器高度从0忽然变成正常值,但Leaflet在初始化时已经按0尺寸计算过了,之后的尺寸变化它感知不到。

排查方向就两条:

  • 给容器一个明确的最小高度,比如min-height: 300px,先确认地图能出来,再回头处理布局。
  • 如果是布局变化导致地图尺寸不对,调用map.invalidateSize()让地图重新计算尺寸。我通常在组件里暴露这个方法,并配合ResizeObserver监听容器尺寸变化,自动调用:
const observer = new ResizeObserver(() => { map?.invalidateSize(); }); observer.observe(container.value);

这个方法还能顺带解决“侧边栏收起后地图右侧出现白边”的经典问题。

4.2 组件重复初始化导致贴图错乱

项目中使用v-if切换页面时,或者组件被动态加载多次时,可能遇到地图容器里出现了两张底图交错叠在一起、拖动起来非常卡顿的问题。

原因通常是:组件实例虽然被销毁了,但地图实例没有走map.remove(),底图瓦片层还挂在DOM上。或者容器节点被复用了,旧地图实例绑定的监听器还在处理事件。

解决方式就是在组件卸载时严格执行销毁流程:

onBeforeUnmount(() => { map?.remove(); map = null; });

map.remove()会一并清空底图图层和事件监听,不要图省事只把container的innerHTML清空。Leaflet的实例有它自己维护的内部状态,不走remove()会导致后续重建时状态残留。

另外一个关联问题是容器DOM节点必须唯一,多实例场景要用不同的id或者唯一key,不要让两个地图实例共用同一个节点。

4.3 组件销毁后事件回调还在执行

这个问题的场景很隐蔽。当业务组件里通过map.on('click', handler)绑定了自定义事件,而组件销毁时只调用了map.remove(),没有手动解绑这部分事件监听,如果这个map实例还被外部引用着,回调函数就会持续存在。

虽然map.remove()会清掉Leaflet自身的监听,但如果你在回调里又引用了业务组件的数据和方法,闭包会让整个组件无法被垃圾回收。模块化了以后,这种泄漏不太直观但确实存在。

我的习惯是:凡是我自己通过map.on绑定的监听,都要在销毁前用map.off逐一解绑。特别是配合popup、tooltip等交互组件时,宁可多写两行也别省。父组件拿到map实例后自己绑定的监听,父组件也要自己在销毁前解绑,这块不能被底层组件包办。

4.4 marker图标加载不出来,小绿点变成小破图

Leaflet默认的marker图标文件是CSS通过相对路径引用的。在Vite或Webpack打包后,CSS中的相对路径会指向构建后的资源目录,经常找不到图片,表现就是marker位置出现一个碎裂的图标。

修复方式在3.2的代码里已经写过了,核心是手动用mergeOptions把图标资源指向打包后的真实URL。如果你不做自定义图标,这就是必踩的坑,建议直接把这段修复代码写进公共组件里,一劳永逸。

如果是要自定义marker图标,直接传icon选项到L.marker里:

L.marker(latlng, { icon: L.icon({ iconUrl: require('@/assets/my-marker.png'), iconSize: [32, 32], iconAnchor: [16, 32] }) });

iconUrl的取值要注意不要用纯字符串路径,要交给构建工具处理,否则部署到子路径后资源一样会404。

4.5 watch深层对象导致地图频繁闪烁

给center设置deep: true是一个常见做法,但在某些场景下会引发性能问题。如果父组件每次更新表格数据都把整个响应式对象改了,哪怕地图中心其实没变,watch也会触发,导致地图频繁往返跳动,用户拖图时体验极差。

我的方法是给watch回调里加一层“值比较”,只有新旧值确实不同时才真的调用setView。可以参考这个思路:

watch(() => props.center, (val, oldVal) => { if (!map || !val) return; if (Math.abs(val[0] - oldVal[0]) < 0.0001 && Math.abs(val[1] - oldVal[1]) < 0.0001) return; map.setView(val, map.getZoom()); });

这个最小阈值可以根据你的业务场景来调整,在地理坐标系下,保留到小数点后四位已经可以忽略锚点级的微小抖动。

4.6 打包体积优化:别把全套地图都塞进主包

Leaflet本身体积不大,完整构建大概是42KB左右Gzip后。但问题往往出在引入方式上。如果你在入口文件里直接import * as L from 'leaflet'又从leaflet/dist/leaflet.css引入样式,同时业务组件里又用了GeoJSON、热力图插件、测距插件,打包体积就会膨胀不少。

我的优化经验是:

  • 基础组件单独打包,不要混进业务组件的异步chunk里。
  • 插件按需引入,比如leaflet.heat、leaflet.markercluster,用到那个页面再import,并且留意插件的CSS是否需要一并引入。
  • 如果项目里多个页面都要用地图,建议把基础组件和Leaflet核心做成一个共享chunk,配合Vite的build.rollupOptions配置手动分包。

体验上差别是很明显的。之前有个项目地图刚开始在主包里,首屏加载慢了两三秒,分包之后主包轻了很多,地图页面因为按需加载,页面权重反而更合理。

5. 进阶封装思路与玩法扩展

5.1 表单地图选点组件与v-model绑定

地图选点在表单场景中非常常见,比如选择收货地址、标注设备坐标。用v-model语义去封装组件,可以无缝融入Form表单体系。我在3.4里写过一个简化版,这里再补充一个和表单组件库联动的细节:当v-model的值为null或undefined时,地图不显示marker;当值变为合法坐标时,需要自动把地图飞到对应位置并显示marker。

这个需求本质上是在封装里增加了“外部值变化时的地图联动”逻辑,和之前说的center联动类似,但需要注意marker和map要同时更新,不能只更新一个。否则会出现坐标已经写到表单里了,但地图上标记点还在旧位置的诡异Bug。

5.2 地图与业务图层的联动:ECharts和轨迹展示

地图组件封装好了之后,和ECharts这类图表库联动就变得非常顺。比如地图上点击某个区域,右侧图表切换为该区域的数据;或者反过来,图表hover某个柱子,地图上的对应点位闪烁。

实现方式可以利用我们暴露的map实例和事件机制。点击地图获得经纬度后,回调里通过逆地理编码或者前端空间计算,找到所属区域,再通知图表组件更新数据。这一步的关键在于:地图组件只负责“告诉上层用户点了哪”,至于点击之后干什么业务,全部交给上层;组件内部不做任何和业务强相关的逻辑,这样组件才能在各种页面里复用。

5.3 多底图切换与图层管理

多底图切换在GIS业务里几乎必做。卫星图、街道图、地形图之间切换,如果每次切换都重新创建TileLayer再add/remove,会给用户一种明显的闪断感。

我这里提供一个更顺滑的思路:在MapContainer内部维护一个baseLayers对象,每个key对应一个TileLayer实例。切换时调用map.removeLayer(oldLayer)和map.addLayer(newLayer),或者更简单——用map.setMaxZoom和新图层的zIndex控制层叠顺序,让新底图在旧底图上层。

底图列表我会设计成props传入,格式类似于:

[ { name: '街道图', url: 'xxx', options: { maxZoom: 18 } }, { name: '卫星图', url: 'xxx', options: { maxZoom: 20 } } ]

切换逻辑收敛在组件内部,对外只暴露一个switchBaseLayer(name)方法。这种设计下,页面里切换底图只需要一行调用,而且不会出现图层叠错、删除不干净的坑。

5.4 与Vue Router和动态路由配合的实践

有的项目地图页本身是动态路由的,不同路由对应不同视野范围或者不同图层数据。封装组件时需要注意,路由切换不等于组件销毁。如果你的路由配置中,两个路由复用了同一个组件实例,那么onMounted可能只在第一次进入页面时执行。这时候如果还想处理路由参数变化,就要在父组件里对route做watch,并调用子组件暴露的方法去更新地图。

watch(() => route.params.id, (newId) => { mapRef.value?.clearData(); fetchData(newId).then((data) => { mapRef.value?.setSource(data); }); });

把焦点放在组件的对外方法上,而不是每次路由变化都重新创建地图实例。这样既保留了地图的连续交互体验,也避免频繁初始化导致的白屏闪烁。

5.5 封装成独立组件库的必要准备

如果组件成熟了,想抽成独立组件库给多个项目用,有几个额外的细节要处理。一是包名和版本管理,用vue-leaflet-components这类语义化名字,发布到私有仓库时注意组件库依赖Vue和Leaflet的版本范围要标注清楚。二是样式的隔离,建议用BEM命名规范给内部样式加前缀,避免和业务样式互相污染。三是构建产物上要同时输出ESM和UMD格式,不同项目构建环境不一样,至少要保证两种主流格式。

还有一点容易被忽略:组件库的demo文档里要覆盖不同使用场景,特别是和表格、表单组件联动的示例。文档示例本身就是最好的回归测试,改代码时跑一遍demo,基本能发现大部分破坏性变更。我就是靠着一份demo页面,几次重构都没有引入严重的回归问题。

这几年用下来,我最大的感觉是:Leaflet封装成Vue组件,本质上是给Leaflet这辆“手动挡汽车”装了一套Vue的“自动变速箱”。前期花两三个小时把基础组件的接口设计清楚,后面每个业务地图页面都能少踩一堆重复的坑。如果你现在手上的项目里地图逻辑已经开始在多个页面复制粘贴了,不妨找个时间抽出这一层底层组件。一次投入,后面每次做地图业务都能感受到回报。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询