# Vue + OpenLayers 完整项目开发指南

文章目录

    • 项目概述
    • 项目初始化
      • [1. 创建Vue项目](#1. 创建Vue项目)
      • [2. 安装依赖](#2. 安装依赖)
      • [3. 项目结构](#3. 项目结构)
    • 核心代码实现
      • [1. 状态管理 (Pinia)](#1. 状态管理 (Pinia))
      • [2. 地图容器组件](#2. 地图容器组件)
      • [3. 图层控制组件](#3. 图层控制组件)
      • [4. 标记点功能实现](#4. 标记点功能实现)
      • [5. 测量工具组件](#5. 测量工具组件)
      • [6. 路径规划组件](#6. 路径规划组件)
      • [7. 地图工具栏组件](#7. 地图工具栏组件)
      • [8. 主页面集成](#8. 主页面集成)
    • 项目优化与扩展
      • [1. 主题切换功能](#1. 主题切换功能)
      • [2. 地图事件总线](#2. 地图事件总线)
      • [3. 性能优化](#3. 性能优化)
    • 项目部署
      • [1. 生产环境构建](#1. 生产环境构建)
      • [2. Docker部署](#2. Docker部署)
      • [3. CI/CD配置 (GitHub Actions)](#3. CI/CD配置 (GitHub Actions))
    • 项目总结

项目概述

技术栈

  • Vue 3 (Composition API)
  • OpenLayers 7.x
  • Vite 构建工具
  • Pinia 状态管理
  • Element Plus UI组件库

功能模块

  1. 基础地图展示
  2. 图层切换与控制
  3. 地图标记与信息弹窗
  4. 距离与面积测量
  5. 路径规划与导航
  6. 地图截图与导出
  7. 主题样式切换
  8. 响应式布局

项目初始化

1. 创建Vue项目

bash 复制代码
npm create vite@latest vue-ol-app --template vue
cd vue-ol-app
npm install

2. 安装依赖

bash 复制代码
npm install ol @vueuse/core pinia element-plus axios

3. 项目结构

复制代码
src/
├── assets/
├── components/
│   ├── MapContainer.vue       # 地图容器组件
│   ├── LayerControl.vue       # 图层控制组件
│   ├── MeasureTool.vue        # 测量工具组件
│   ├── RoutePlanner.vue       # 路径规划组件
│   └── MapToolbar.vue         # 地图工具栏
├── composables/
│   ├── useMap.js              # 地图相关逻辑
│   └── useMapTools.js         # 地图工具逻辑
├── stores/
│   └── mapStore.js            # Pinia地图状态管理
├── styles/
│   ├── ol.css                 # OpenLayers样式覆盖
│   └── variables.scss         # 样式变量
├── utils/
│   ├── projection.js          # 坐标转换工具
│   └── style.js               # 样式生成工具
├── views/
│   └── HomeView.vue           # 主页面
├── App.vue
└── main.js

核心代码实现

1. 状态管理 (Pinia)

javascript 复制代码
// stores/mapStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useMapStore = defineStore('map', () => {
  // 地图实例
  const map = ref(null);
  
  // 当前视图状态
  const viewState = ref({
    center: [116.404, 39.915],
    zoom: 10,
    rotation: 0
  });
  
  // 图层状态
  const layers = ref({
    baseLayers: [
      { id: 'osm', name: 'OpenStreetMap', visible: true, type: 'tile' },
      { id: 'satellite', name: '卫星地图', visible: false, type: 'tile' }
    ],
    overlayLayers: []
  });
  
  // 当前激活的工具
  const activeTool = ref(null);
  
  // 标记点集合
  const markers = ref([]);
  
  // 获取当前可见的底图
  const visibleBaseLayer = computed(() => {
    return layers.value.baseLayers.find(layer => layer.visible);
  });
  
  // 切换底图
  function toggleBaseLayer(layerId) {
    layers.value.baseLayers.forEach(layer => {
      layer.visible = layer.id === layerId;
    });
  }
  
  return {
    map,
    viewState,
    layers,
    activeTool,
    markers,
    visibleBaseLayer,
    toggleBaseLayer
  };
});

2. 地图容器组件

vue 复制代码
<!-- components/MapContainer.vue -->
<template>
  <div ref="mapContainer" class="map-container">
    <slot></slot>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { useMapStore } from '../stores/mapStore';
import Map from 'ol/Map';
import View from 'ol/View';
import { fromLonLat } from 'ol/proj';

const props = defineProps({
  viewOptions: {
    type: Object,
    default: () => ({
      center: [116.404, 39.915],
      zoom: 10
    })
  }
});

const mapContainer = ref(null);
const mapStore = useMapStore();

// 初始化地图
function initMap() {
  const map = new Map({
    target: mapContainer.value,
    view: new View({
      center: fromLonLat(props.viewOptions.center),
      zoom: props.viewOptions.zoom,
      minZoom: 2,
      maxZoom: 18
    })
  });
  
  mapStore.map = map;
  
  // 保存视图状态变化
  map.on('moveend', () => {
    const view = map.getView();
    mapStore.viewState = {
      center: view.getCenter(),
      zoom: view.getZoom(),
      rotation: view.getRotation()
    };
  });
  
  return map;
}

// 响应式调整地图大小
function updateMapSize() {
  if (mapStore.map) {
    mapStore.map.updateSize();
  }
}

onMounted(() => {
  initMap();
  window.addEventListener('resize', updateMapSize);
});

onUnmounted(() => {
  window.removeEventListener('resize', updateMapSize);
  if (mapStore.map) {
    mapStore.map.setTarget(undefined);
    mapStore.map = null;
  }
});
</script>

<style scoped>
.map-container {
  width: 100%;
  height: 100%;
  position: relative;
}
</style>

3. 图层控制组件

vue 复制代码
<!-- components/LayerControl.vue -->
<template>
  <div class="layer-control">
    <el-card shadow="hover">
      <template #header>
        <div class="card-header">
          <span>图层控制</span>
        </div>
      </template>
      
      <div class="base-layers">
        <div v-for="layer in mapStore.layers.baseLayers" 
             :key="layer.id" 
             class="layer-item"
             @click="mapStore.toggleBaseLayer(layer.id)">
          <el-radio v-model="mapStore.visibleBaseLayer.id" :label="layer.id">
            {{ layer.name }}
          </el-radio>
        </div>
      </div>
      
      <el-divider></el-divider>
      
      <div class="overlay-layers">
        <div v-for="layer in mapStore.layers.overlayLayers" 
             :key="layer.id" 
             class="layer-item">
          <el-checkbox v-model="layer.visible" @change="toggleLayerVisibility(layer)">
            {{ layer.name }}
          </el-checkbox>
        </div>
      </div>
    </el-card>
  </div>
</template>

<script setup>
import { useMapStore } from '../stores/mapStore';
import { onMounted, watch } from 'vue';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import XYZ from 'ol/source/XYZ';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';

const mapStore = useMapStore();

// 初始化图层
function initLayers() {
  // 添加OSM底图
  const osmLayer = new TileLayer({
    source: new OSM(),
    properties: {
      id: 'osm',
      name: 'OpenStreetMap',
      type: 'base'
    }
  });
  
  // 添加卫星底图
  const satelliteLayer = new TileLayer({
    source: new XYZ({
      url: 'https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/{z}/{x}/{y}?access_token=your_mapbox_token'
    }),
    properties: {
      id: 'satellite',
      name: '卫星地图',
      type: 'base'
    }
  });
  
  // 添加标记图层
  const markerLayer = new VectorLayer({
    source: new VectorSource(),
    properties: {
      id: 'markers',
      name: '标记点',
      type: 'overlay'
    }
  });
  
  mapStore.map.addLayer(osmLayer);
  mapStore.map.addLayer(satelliteLayer);
  mapStore.map.addLayer(markerLayer);
  
  // 默认隐藏卫星图层
  satelliteLayer.setVisible(false);
  
  // 更新store中的图层状态
  mapStore.layers.overlayLayers.push({
    id: 'markers',
    name: '标记点',
    visible: true,
    olLayer: markerLayer
  });
}

// 切换图层可见性
function toggleLayerVisibility(layer) {
  layer.olLayer.setVisible(layer.visible);
}

// 监听底图变化
watch(() => mapStore.visibleBaseLayer, (newLayer) => {
  mapStore.map.getLayers().forEach(layer => {
    const props = layer.getProperties();
    if (props.type === 'base') {
      layer.setVisible(props.id === newLayer.id);
    }
  });
});

onMounted(() => {
  if (mapStore.map) {
    initLayers();
  }
});
</script>

<style scoped>
.layer-control {
  position: absolute;
  top: 20px;
  right: 20px;
  z-index: 100;
  width: 250px;
}

.layer-item {
  padding: 8px 0;
  cursor: pointer;
}

.base-layers, .overlay-layers {
  margin-bottom: 10px;
}
</style>

4. 标记点功能实现

javascript 复制代码
// composables/useMap.js
import { ref, onMounted } from 'vue';
import { useMapStore } from '../stores/mapStore';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import { fromLonLat } from 'ol/proj';
import { Style, Icon } from 'ol/style';

export function useMapMarkers() {
  const mapStore = useMapStore();
  const markerSource = ref(null);
  
  // 初始化标记源
  function initMarkerSource() {
    const markerLayer = mapStore.map.getLayers().getArray()
      .find(layer => layer.get('id') === 'markers');
    
    if (markerLayer) {
      markerSource.value = markerLayer.getSource();
    }
  }
  
  // 添加标记
  function addMarker(coordinate, properties = {}) {
    if (!markerSource.value) return;
    
    const marker = new Feature({
      geometry: new Point(fromLonLat(coordinate)),
      ...properties
    });
    
    marker.setStyle(createMarkerStyle(properties));
    markerSource.value.addFeature(marker);
    
    return marker;
  }
  
  // 创建标记样式
  function createMarkerStyle(properties) {
    return new Style({
      image: new Icon({
        src: properties.icon || '/images/marker.png',
        scale: 0.5,
        anchor: [0.5, 1]
      })
    });
  }
  
  // 清除所有标记
  function clearMarkers() {
    if (markerSource.value) {
      markerSource.value.clear();
    }
  }
  
  onMounted(() => {
    if (mapStore.map) {
      initMarkerSource();
    }
  });
  
  return {
    addMarker,
    clearMarkers
  };
}

5. 测量工具组件

vue 复制代码
<!-- components/MeasureTool.vue -->
<template>
  <el-card shadow="hover" class="measure-tool">
    <template #header>
      <div class="card-header">
        <span>测量工具</span>
      </div>
    </template>
    
    <el-radio-group v-model="measureType" @change="changeMeasureType">
      <el-radio-button label="length">距离测量</el-radio-button>
      <el-radio-button label="area">面积测量</el-radio-button>
    </el-radio-group>
    
    <div v-if="measureResult" class="measure-result">
      <div v-if="measureType === 'length'">
        长度: {{ measureResult }} 米
      </div>
      <div v-else>
        面积: {{ measureResult }} 平方米
      </div>
    </div>
    
    <el-button 
      type="danger" 
      size="small" 
      @click="clearMeasurement"
      :disabled="!measureResult">
      清除
    </el-button>
  </el-card>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useMapStore } from '../stores/mapStore';
import Draw from 'ol/interaction/Draw';
import { LineString, Polygon } from 'ol/geom';
import { getLength, getArea } from 'ol/sphere';
import { unByKey } from 'ol/Observable';
import { Style, Fill, Stroke } from 'ol/style';

const mapStore = useMapStore();
const measureType = ref('length');
const measureResult = ref(null);
const drawInteraction = ref(null);
const measureListener = ref(null);

// 测量样式
const measureStyle = new Style({
  fill: new Fill({
    color: 'rgba(255, 255, 255, 0.2)'
  }),
  stroke: new Stroke({
    color: 'rgba(0, 0, 255, 0.5)',
    lineDash: [10, 10],
    width: 2
  })
});

// 改变测量类型
function changeMeasureType() {
  clearMeasurement();
  setupMeasureInteraction();
}

// 设置测量交互
function setupMeasureInteraction() {
  const source = new VectorSource();
  const vector = new VectorLayer({
    source: source,
    style: measureStyle
  });
  
  mapStore.map.addLayer(vector);
  
  let geometryType = measureType.value === 'length' ? 'LineString' : 'Polygon';
  
  drawInteraction.value = new Draw({
    source: source,
    type: geometryType,
    style: measureStyle
  });
  
  mapStore.map.addInteraction(drawInteraction.value);
  
  let sketch;
  drawInteraction.value.on('drawstart', function(evt) {
    sketch = evt.feature;
    measureResult.value = null;
  });
  
  measureListener.value = drawInteraction.value.on('drawend', function(evt) {
    const feature = evt.feature;
    const geometry = feature.getGeometry();
    
    if (measureType.value === 'length') {
      const length = getLength(geometry);
      measureResult.value = Math.round(length * 100) / 100;
    } else {
      const area = getArea(geometry);
      measureResult.value = Math.round(area * 100) / 100;
    }
    
    // 清除临时图形
    source.clear();
  });
}

// 清除测量
function clearMeasurement() {
  if (drawInteraction.value) {
    mapStore.map.removeInteraction(drawInteraction.value);
    unByKey(measureListener.value);
    drawInteraction.value = null;
  }
  
  // 移除测量图层
  mapStore.map.getLayers().getArray().forEach(layer => {
    if (layer.get('name') === 'measure-layer') {
      mapStore.map.removeLayer(layer);
    }
  });
  
  measureResult.value = null;
}

onUnmounted(() => {
  clearMeasurement();
});
</script>

<style scoped>
.measure-tool {
  position: absolute;
  top: 20px;
  left: 20px;
  z-index: 100;
  width: 250px;
}

.measure-result {
  margin: 10px 0;
  padding: 5px;
  background: rgba(255, 255, 255, 0.8);
  border-radius: 4px;
}
</style>

6. 路径规划组件

vue 复制代码
<!-- components/RoutePlanner.vue -->
<template>
  <el-card shadow="hover" class="route-planner">
    <template #header>
      <div class="card-header">
        <span>路径规划</span>
      </div>
    </template>
    
    <el-form label-position="top">
      <el-form-item label="起点">
        <el-input v-model="startPoint" placeholder="输入起点坐标或地址"></el-input>
      </el-form-item>
      
      <el-form-item label="终点">
        <el-input v-model="endPoint" placeholder="输入终点坐标或地址"></el-input>
      </el-form-item>
      
      <el-form-item>
        <el-button type="primary" @click="calculateRoute">计算路线</el-button>
        <el-button @click="clearRoute">清除</el-button>
      </el-form-item>
    </el-form>
    
    <div v-if="routeDistance" class="route-info">
      <div>距离: {{ routeDistance }} 公里</div>
      <div>预计时间: {{ routeDuration }} 分钟</div>
    </div>
  </el-card>
</template>

<script setup>
import { ref } from 'vue';
import { useMapStore } from '../stores/mapStore';
import { useMapMarkers } from '../composables/useMap';
import LineString from 'ol/geom/LineString';
import Feature from 'ol/Feature';
import VectorSource from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';
import { Style, Stroke } from 'ol/style';

const mapStore = useMapStore();
const { addMarker } = useMapMarkers();
const startPoint = ref('');
const endPoint = ref('');
const routeDistance = ref(null);
const routeDuration = ref(null);

let routeLayer = null;
let startMarker = null;
let endMarker = null;

// 计算路线
async function calculateRoute() {
  // 在实际应用中,这里应该调用路线规划API
  // 这里使用模拟数据
  
  // 清除旧路线
  clearRoute();
  
  // 解析起点和终点坐标
  const startCoords = parseCoordinates(startPoint.value) || [116.404, 39.915];
  const endCoords = parseCoordinates(endPoint.value) || [116.404, 39.925];
  
  // 添加标记
  startMarker = addMarker(startCoords, { 
    title: '起点',
    icon: '/images/start-marker.png'
  });
  
  endMarker = addMarker(endCoords, { 
    title: '终点',
    icon: '/images/end-marker.png'
  });
  
  // 创建路线图层
  const source = new VectorSource();
  routeLayer = new VectorLayer({
    source: source,
    style: new Style({
      stroke: new Stroke({
        color: '#0066ff',
        width: 4
      })
    })
  });
  
  mapStore.map.addLayer(routeLayer);
  
  // 模拟路线数据
  const routeCoords = [
    startCoords,
    [startCoords[0] + 0.005, startCoords[1] + 0.005],
    [endCoords[0] - 0.005, endCoords[1] - 0.005],
    endCoords
  ];
  
  // 计算距离和时间
  routeDistance.value = calculateDistance(routeCoords).toFixed(2);
  routeDuration.value = Math.round(routeDistance.value * 10);
  
  // 添加路线到图层
  const routeFeature = new Feature({
    geometry: new LineString(routeCoords.map(coord => fromLonLat(coord)))
  });
  
  source.addFeature(routeFeature);
  
  // 调整视图以显示整个路线
  const view = mapStore.map.getView();
  view.fit(source.getExtent(), {
    padding: [50, 50, 50, 50],
    duration: 1000
  });
}

// 解析坐标
function parseCoordinates(input) {
  if (!input) return null;
  
  // 尝试解析类似 "116.404,39.915" 的格式
  const parts = input.split(',');
  if (parts.length === 2) {
    const lon = parseFloat(parts[0]);
    const lat = parseFloat(parts[1]);
    if (!isNaN(lon) && !isNaN(lat)) {
      return [lon, lat];
    }
  }
  
  return null;
}

// 计算路线距离 (简化版)
function calculateDistance(coords) {
  // 在实际应用中应该使用更精确的算法
  let distance = 0;
  for (let i = 1; i < coords.length; i++) {
    const dx = coords[i][0] - coords[i-1][0];
    const dy = coords[i][1] - coords[i-1][1];
    distance += Math.sqrt(dx*dx + dy*dy) * 111; // 粗略转换为公里
  }
  return distance;
}

// 清除路线
function clearRoute() {
  if (routeLayer) {
    mapStore.map.removeLayer(routeLayer);
    routeLayer = null;
  }
  
  if (startMarker) {
    startMarker.getSource().removeFeature(startMarker);
  }
  
  if (endMarker) {
    endMarker.getSource().removeFeature(endMarker);
  }
  
  routeDistance.value = null;
  routeDuration.value = null;
}
</script>

<style scoped>
.route-planner {
  position: absolute;
  top: 20px;
  left: 300px;
  z-index: 100;
  width: 300px;
}

.route-info {
  margin-top: 10px;
  padding: 10px;
  background: rgba(255, 255, 255, 0.8);
  border-radius: 4px;
}
</style>

7. 地图工具栏组件

vue 复制代码
<!-- components/MapToolbar.vue -->
<template>
  <div class="map-toolbar">
    <el-button-group>
      <el-tooltip content="放大" placement="top">
        <el-button @click="zoomIn">
          <el-icon><zoom-in /></el-icon>
        </el-button>
      </el-tooltip>
      
      <el-tooltip content="缩小" placement="top">
        <el-button @click="zoomOut">
          <el-icon><zoom-out /></el-icon>
        </el-button>
      </el-tooltip>
      
      <el-tooltip content="复位" placement="top">
        <el-button @click="resetView">
          <el-icon><refresh /></el-icon>
        </el-button>
      </el-tooltip>
      
      <el-tooltip content="全屏" placement="top">
        <el-button @click="toggleFullscreen">
          <el-icon><full-screen /></el-icon>
        </el-button>
      </el-tooltip>
      
      <el-tooltip content="截图" placement="top">
        <el-button @click="exportMap">
          <el-icon><camera /></el-icon>
        </el-button>
      </el-tooltip>
    </el-button-group>
  </div>
</template>

<script setup>
import { useMapStore } from '../stores/mapStore';
import { useFullscreen } from '@vueuse/core';
import { toPng } from 'html-to-image';

const mapStore = useMapStore();
const { toggle: toggleFullscreen } = useFullscreen();

// 放大
function zoomIn() {
  const view = mapStore.map.getView();
  const zoom = view.getZoom();
  view.animate({
    zoom: zoom + 1,
    duration: 200
  });
}

// 缩小
function zoomOut() {
  const view = mapStore.map.getView();
  const zoom = view.getZoom();
  view.animate({
    zoom: zoom - 1,
    duration: 200
  });
}

// 复位
function resetView() {
  const view = mapStore.map.getView();
  view.animate({
    center: fromLonLat([116.404, 39.915]),
    zoom: 10,
    duration: 500
  });
}

// 导出地图为图片
async function exportMap() {
  try {
    const mapElement = mapStore.map.getViewport();
    const dataUrl = await toPng(mapElement);
    
    const link = document.createElement('a');
    link.download = 'map-screenshot.png';
    link.href = dataUrl;
    link.click();
  } catch (error) {
    console.error('导出地图失败:', error);
    ElMessage.error('导出地图失败');
  }
}
</script>

<style scoped>
.map-toolbar {
  position: absolute;
  bottom: 20px;
  right: 20px;
  z-index: 100;
  background: rgba(255, 255, 255, 0.8);
  padding: 5px;
  border-radius: 4px;
}
</style>

8. 主页面集成

vue 复制代码
<!-- views/HomeView.vue -->
<template>
  <div class="home-container">
    <MapContainer :view-options="initialView">
      <LayerControl />
      <MeasureTool />
      <RoutePlanner />
      <MapToolbar />
    </MapContainer>
  </div>
</template>

<script setup>
import MapContainer from '../components/MapContainer.vue';
import LayerControl from '../components/LayerControl.vue';
import MeasureTool from '../components/MeasureTool.vue';
import RoutePlanner from '../components/RoutePlanner.vue';
import MapToolbar from '../components/MapToolbar.vue';

const initialView = {
  center: [116.404, 39.915],
  zoom: 12
};
</script>

<style scoped>
.home-container {
  width: 100vw;
  height: 100vh;
  position: relative;
}
</style>

项目优化与扩展

1. 主题切换功能

javascript 复制代码
// stores/themeStore.js
import { defineStore } from 'pinia';
import { ref, watch } from 'vue';

export const useThemeStore = defineStore('theme', () => {
  const currentTheme = ref('light');
  
  function toggleTheme() {
    currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light';
  }
  
  watch(currentTheme, (newTheme) => {
    document.documentElement.setAttribute('data-theme', newTheme);
  }, { immediate: true });
  
  return { currentTheme, toggleTheme };
});

2. 地图事件总线

javascript 复制代码
// utils/eventBus.js
import mitt from 'mitt';

export const eventBus = mitt();

// 在组件中使用
import { eventBus } from '../utils/eventBus';

// 发送事件
eventBus.emit('marker-clicked', markerData);

// 接收事件
eventBus.on('marker-clicked', (data) => {
  // 处理事件
});

3. 性能优化

  1. 矢量图层聚类
javascript 复制代码
import Cluster from 'ol/source/Cluster';

const clusterSource = new Cluster({
  distance: 40,
  source: new VectorSource({
    url: 'data/points.geojson',
    format: new GeoJSON()
  })
});

const clusterLayer = new VectorLayer({
  source: clusterSource,
  style: function(feature) {
    const size = feature.get('features').length;
    // 根据聚类点数量返回不同样式
  }
});
  1. WebGL渲染
javascript 复制代码
import WebGLPointsLayer from 'ol/layer/WebGLPoints';

const webglLayer = new WebGLPointsLayer({
  source: vectorSource,
  style: {
    symbol: {
      symbolType: 'circle',
      size: ['interpolate', ['linear'], ['get', 'size'], 8, 8, 12, 12],
      color: ['interpolate', ['linear'], ['get', 'value'], 0, 'blue', 100, 'red']
    }
  }
});
  1. 懒加载图层
javascript 复制代码
function setupLazyLayer() {
  const layer = new VectorLayer({
    source: new VectorSource(),
    visible: false
  });
  
  map.addLayer(layer);
  
  // 当图层可见时加载数据
  layer.on('change:visible', function() {
    if (layer.getVisible() && layer.getSource().getFeatures().length === 0) {
      loadLayerData();
    }
  });
  
  async function loadLayerData() {
    const response = await fetch('data/large-dataset.geojson');
    const geojson = await response.json();
    layer.getSource().addFeatures(new GeoJSON().readFeatures(geojson));
  }
}

项目部署

1. 生产环境构建

bash 复制代码
npm run build

2. Docker部署

dockerfile 复制代码
# Dockerfile
FROM nginx:alpine

COPY dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
nginx 复制代码
# nginx.conf
server {
    listen 80;
    server_name localhost;
    
    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
    
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}

3. CI/CD配置 (GitHub Actions)

yaml 复制代码
# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
    
    - name: Install dependencies
      run: npm install
    
    - name: Build project
      run: npm run build
    
    - name: Deploy to server
      uses: appleboy/scp-action@master
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USERNAME }}
        key: ${{ secrets.SSH_KEY }}
        source: "dist/*"
        target: "/var/www/vue-ol-app"
    
    - name: Restart Nginx
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USERNAME }}
        key: ${{ secrets.SSH_KEY }}
        script: |
          sudo systemctl restart nginx

项目总结

通过这个完整的Vue + OpenLayers项目,我们实现了:

  1. 基础地图功能:地图展示、缩放、平移、旋转
  2. 图层管理:多种底图切换、叠加图层控制
  3. 交互功能:标记点添加、信息展示、测量工具
  4. 高级功能:路径规划、地图截图、主题切换
  5. 性能优化:图层懒加载、WebGL渲染、矢量聚类

项目特点:

  • 采用Vue 3 Composition API组织代码
  • 使用Pinia进行状态管理
  • 组件化设计,高内聚低耦合
  • 响应式布局,适配不同设备
  • 良好的性能优化策略

扩展方向:

  1. 集成真实的地图服务API(如Google Maps、Mapbox)
  2. 添加3D地图支持(通过ol-cesium)
  3. 实现更复杂的地理分析功能
  4. 开发移动端专用版本
  5. 添加用户系统,支持地图数据保存

这个项目展示了如何将OpenLayers的强大功能与Vue的响应式特性相结合,构建出功能丰富、性能优良的WebGIS应用。开发者可以根据实际需求进一步扩展和完善各个功能模块。

相关推荐
roman_日积跬步-终至千里23 分钟前
【Go语言基础【3】】变量、常量、值类型与引用类型
开发语言·算法·golang
roman_日积跬步-终至千里31 分钟前
【Go语言基础】基本语法
开发语言·golang·xcode
拉不动的猪41 分钟前
都25年啦,还有谁分不清双向绑定原理,响应式原理、v-model实现原理
前端·javascript·vue.js
Felven42 分钟前
C. Basketball Exercise
c语言·开发语言
烛阴1 小时前
Python枚举类Enum超详细入门与进阶全攻略
前端·python
孟孟~1 小时前
npm run dev 报错:Error: error:0308010C:digital envelope routines::unsupported
前端·npm·node.js
孟孟~1 小时前
npm install 报错:npm error: ...node_modules\deasync npm error command failed
前端·npm·node.js
狂炫一碗大米饭1 小时前
一文打通TypeScript 泛型
前端·javascript·typescript
海棠蚀omo1 小时前
C++笔记-C++11(一)
开发语言·c++·笔记
wh_xia_jun1 小时前
在 Spring Boot 中使用 JSP
java·前端·spring boot