基于Vue3中实现耳机和扬声器切换的完整方案

 更新时间:2026年02月03日 08:56:12   作者:经年未远  
在 Web 应用中实现音频输出设备的动态切换,允许用户在扬声器、耳机、蓝牙设备等不同音频输出设备之间自由切换,本文给大家介绍了基于Vue3中实现耳机和扬声器切换的完整方案,需要的朋友可以参考下

功能概述

在 Web 应用中实现音频输出设备的动态切换,允许用户在扬声器、耳机、蓝牙设备等不同音频输出设备之间自由切换。

应用场景

  • 在线教育平台的语音通话
  • 视频会议系统
  • 在线音乐播放器
  • 语音聊天应用

核心功能

  • 枚举所有可用的音频输出设备
  • 动态切换音频输出设备
  • 智能识别设备类型(扬声器/耳机/蓝牙)
  • 跨平台支持(H5 + 微信小程序)

技术原理

1. Web Audio API 架构

音频源 → AudioBufferSourceNode → MediaStreamDestination → <audio> 元素 → 音频输出设备
                                                                ↓
                                                          setSinkId(deviceId)

2. 关键 API

2.1 枚举音频设备

navigator.mediaDevices.enumerateDevices();

返回所有媒体设备,包括:

  • audioinput:音频输入设备(麦克风)
  • audiooutput:音频输出设备(扬声器/耳机)
  • videoinput:视频输入设备(摄像头)

2.2 切换音频输出

audioElement.setSinkId(deviceId);

<audio> 元素的输出切换到指定设备。

2.3 创建音频流

const destination = audioContext.createMediaStreamDestination();
audioElement.srcObject = destination.stream;

3. 为什么需要 MediaStreamDestination?

直接使用 AudioContext.destination 无法切换设备,因为它直接输出到默认设备。通过 MediaStreamDestination 创建一个中间流,再连接到 <audio> 元素,就可以使用 setSinkId 切换设备。

实现步骤

步骤 1:创建音频播放器类

class AudioStreamPlayer {
  private audioContext: AudioContext;
  private mediaStreamDestination: MediaStreamAudioDestinationNode;
  private audioElement: HTMLAudioElement;
  private currentSinkId: string = "";

  constructor() {
    this.initWebAudio();
  }

  private initWebAudio() {
    // 创建 AudioContext
    this.audioContext = new AudioContext({
      sampleRate: 16000,
      latencyHint: "interactive",
    });

    // 创建 MediaStreamDestination
    this.mediaStreamDestination = this.audioContext.createMediaStreamDestination();

    // 创建 audio 元素并连接到流
    this.audioElement = document.createElement("audio");
    this.audioElement.autoplay = true;
    this.audioElement.srcObject = this.mediaStreamDestination.stream;

    // 启动播放
    this.audioElement.play().catch((err) => {
      console.warn("自动播放被阻止,需要用户交互:", err);
    });
  }

  // 播放音频数据
  playAudio(audioBuffer: AudioBuffer) {
    const sourceNode = this.audioContext.createBufferSource();
    sourceNode.buffer = audioBuffer;

    // 连接到 MediaStreamDestination(关键!)
    sourceNode.connect(this.mediaStreamDestination);

    sourceNode.start();
  }

  // 切换音频输出设备
  async setSinkId(deviceId: string): Promise<boolean> {
    if (!this.audioElement) {
      console.warn("audio 元素未初始化");
      return false;
    }

    if (typeof this.audioElement.setSinkId !== "function") {
      console.warn("浏览器不支持 setSinkId");
      return false;
    }

    try {
      await this.audioElement.setSinkId(deviceId);
      this.currentSinkId = deviceId;

      // 确保 audio 元素仍在播放
      if (this.audioElement.paused) {
        await this.audioElement.play();
      }

      console.log("✅ 音频输出设备已切换:", deviceId);
      return true;
    } catch (error) {
      console.error("❌ 切换音频输出设备失败:", error);
      return false;
    }
  }
}

步骤 2:创建设备管理 Hook

// useSpeaker.ts
import { ref, onMounted } from "vue";

export enum SpeakerMode {
  Speaker = "speaker", // 扬声器
  Earphone = "earphone", // 耳机
  Receiver = "receiver", // 听筒(仅移动端)
}

export function useSpeaker(setSinkIdCallback?: (sinkId: string) => Promise<boolean>) {
  const audioDevices = ref<MediaDeviceInfo[]>([]);
  const currentSpeaker = ref<SpeakerMode>(SpeakerMode.Speaker);

  // 获取所有音频输出设备
  const getAudioDevices = async () => {
    try {
      // 先请求麦克风权限,以获取完整的设备标签
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        stream.getTracks().forEach((track) => track.stop());
      } catch (err) {
        console.warn("无法获取麦克风权限:", err);
      }

      // 枚举所有设备
      const devices = await navigator.mediaDevices.enumerateDevices();
      audioDevices.value = devices.filter((device) => device.kind === "audiooutput");

      console.log("可用的音频输出设备:", audioDevices.value);
    } catch (error) {
      console.error("获取音频设备失败:", error);
    }
  };

  // 选择扬声器模式
  const selectSpeaker = async (mode: SpeakerMode) => {
    currentSpeaker.value = mode;

    // 刷新设备列表
    await getAudioDevices();

    if (audioDevices.value.length === 0) {
      console.warn("未找到可用的音频输出设备");
      return;
    }

    let targetDevice: MediaDeviceInfo | undefined;

    if (mode === SpeakerMode.Speaker) {
      // 扬声器:优先选择 default 设备
      targetDevice = audioDevices.value.find((device) => device.deviceId === "default");

      if (!targetDevice) {
        // 如果没有 default,找第一个非耳机设备
        targetDevice = audioDevices.value.find((device) => !device.label.toLowerCase().includes("headphone") && !device.label.toLowerCase().includes("headset") && !device.label.toLowerCase().includes("耳机") && !device.label.toLowerCase().includes("bluetooth"));
      }

      if (!targetDevice && audioDevices.value.length > 0) {
        targetDevice = audioDevices.value[0];
      }
    } else if (mode === SpeakerMode.Earphone) {
      // 耳机:选择包含 headphone/headset/耳机 的设备
      targetDevice = audioDevices.value.find((device) => device.label.toLowerCase().includes("headphone") || device.label.toLowerCase().includes("headset") || device.label.toLowerCase().includes("耳机"));

      if (!targetDevice) {
        console.warn("未检测到耳机设备");
        return;
      }
    }

    if (!targetDevice) {
      console.warn("未找到目标设备");
      return;
    }

    console.log("目标设备:", targetDevice.label, targetDevice.deviceId);

    // 调用切换回调
    if (setSinkIdCallback) {
      const success = await setSinkIdCallback(targetDevice.deviceId);
      if (success) {
        console.log(`✅ 已切换到: ${targetDevice.label}`);
      }
    }
  };

  // 初始化时获取设备列表
  onMounted(() => {
    getAudioDevices();
  });

  return {
    audioDevices,
    currentSpeaker,
    getAudioDevices,
    selectSpeaker,
  };
}

步骤 3:在 Vue 组件中使用

<template>
  <div class="audio-player">
    <!-- 扬声器切换按钮 -->
    <button @click="showDeviceSelector = true">切换音频设备</button>

    <!-- 设备选择弹窗 -->
    <div v-if="showDeviceSelector" class="device-selector">
      <div v-for="device in audioDevices" :key="device.deviceId" @click="switchDevice(device.deviceId)" class="device-item">
        {{ device.label }}
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { useSpeaker, SpeakerMode } from "./useSpeaker";

const showDeviceSelector = ref(false);
const audioPlayer = new AudioStreamPlayer();

// 使用设备管理 Hook
const { audioDevices, selectSpeaker } = useSpeaker((sinkId) => audioPlayer.setSinkId(sinkId));

// 切换设备
const switchDevice = async (deviceId: string) => {
  const success = await audioPlayer.setSinkId(deviceId);
  if (success) {
    showDeviceSelector.value = false;
  }
};
</script>

完整代码

1. AudioStreamPlayer 完整实现

/**
 * 音频流播放器
 * 支持动态切换音频输出设备
 */
export class AudioStreamPlayer {
  private audioContext: AudioContext | null = null;
  private audioQueue: ArrayBuffer[] = [];
  private isPlaying = false;
  private currentSinkId = "";
  private audioElement: HTMLAudioElement | null = null;
  private mediaStreamDestination: MediaStreamAudioDestinationNode | null = null;
  private sourceNodes: AudioBufferSourceNode[] = [];
  private nextPlayTime = 0;

  constructor() {
    this.initWebAudio();
  }

  /**
   * 初始化 Web Audio API
   */
  private initWebAudio() {
    this.audioContext = new AudioContext({
      sampleRate: 16000,
      latencyHint: "interactive",
    });

    // 创建 MediaStreamDestination 和 audio 元素以支持设备切换
    this.mediaStreamDestination = this.audioContext.createMediaStreamDestination();

    this.audioElement = document.createElement("audio");
    this.audioElement.autoplay = true;
    this.audioElement.srcObject = this.mediaStreamDestination.stream;

    // 尝试播放
    const tryPlay = () => {
      if (this.audioElement) {
        this.audioElement
          .play()
          .then(() => {
            console.log("✅ audio 元素已开始播放");
          })
          .catch((err) => {
            console.warn("⚠️ 自动播放被阻止,需要用户交互:", err);
            // 监听用户交互后重试
            const retryPlay = () => {
              if (this.audioElement) {
                this.audioElement.play();
              }
            };
            document.addEventListener("click", retryPlay, { once: true });
          });
      }
    };

    tryPlay();
    console.log("✅ 已创建 audio 元素用于设备切换");
  }

  /**
   * 设置音频输出设备
   */
  async setSinkId(sinkId: string): Promise<boolean> {
    if (!this.audioElement) {
      console.warn("⚠️ audio 元素未初始化");
      return false;
    }

    if (typeof (this.audioElement as any).setSinkId !== "function") {
      console.warn("⚠️ 浏览器不支持 setSinkId");
      return false;
    }

    try {
      console.log("🔊 当前 sinkId:", (this.audioElement as any).sinkId);
      console.log("🔊 准备切换到设备:", sinkId);

      await (this.audioElement as any).setSinkId(sinkId);
      this.currentSinkId = sinkId;

      console.log("✅ 音频输出设备已切换:", sinkId);

      // 确保 audio 元素仍在播放
      if (this.audioElement.paused) {
        await this.audioElement.play();
      }

      return true;
    } catch (error) {
      console.error("❌ 切换音频输出设备失败:", error);
      return false;
    }
  }

  /**
   * 添加音频数据块
   */
  addAudioChunk(arrayBuffer: ArrayBuffer): void {
    if (!arrayBuffer || arrayBuffer.byteLength === 0) {
      return;
    }

    this.audioQueue.push(arrayBuffer);
    this.processQueue();
  }

  /**
   * 处理音频队列
   */
  private processQueue() {
    if (this.audioQueue.length === 0 || !this.audioContext) {
      return;
    }

    const chunk = this.audioQueue.shift();
    if (!chunk) return;

    // 解码音频数据
    this.audioContext
      .decodeAudioData(chunk.slice(0))
      .then((decodedData) => {
        this.playAudio(decodedData);
      })
      .catch((error) => {
        console.error("音频解码失败:", error);
      })
      .finally(() => {
        this.processQueue();
      });
  }

  /**
   * 播放音频
   */
  private playAudio(audioBuffer: AudioBuffer) {
    if (!this.audioContext || !this.mediaStreamDestination) return;

    const sourceNode = this.audioContext.createBufferSource();
    sourceNode.buffer = audioBuffer;

    // 🔥 关键:连接到 mediaStreamDestination
    sourceNode.connect(this.mediaStreamDestination);

    // 计算播放时间
    if (this.nextPlayTime === 0 || this.nextPlayTime < this.audioContext.currentTime) {
      this.nextPlayTime = this.audioContext.currentTime + 0.05;
    }

    sourceNode.start(this.nextPlayTime);
    this.nextPlayTime += audioBuffer.duration;

    this.sourceNodes.push(sourceNode);

    sourceNode.onended = () => {
      sourceNode.disconnect();
      this.sourceNodes = this.sourceNodes.filter((n) => n !== sourceNode);

      if (this.sourceNodes.length === 0 && this.audioQueue.length === 0) {
        this.nextPlayTime = 0;
        this.isPlaying = false;
      }
    };

    if (!this.isPlaying) {
      this.isPlaying = true;
    }
  }

  /**
   * 清空播放队列
   */
  clear(): void {
    this.audioQueue = [];
    this.sourceNodes.forEach((node) => {
      try {
        node.stop();
        node.disconnect();
      } catch (e) {
        // ignore
      }
    });
    this.sourceNodes = [];
    this.nextPlayTime = 0;
    this.isPlaying = false;
  }

  /**
   * 关闭音频上下文
   */
  close(): void {
    this.clear();
    if (this.audioContext && this.audioContext.state !== "closed") {
      this.audioContext.close();
      this.audioContext = null;
    }
  }
}

2. useSpeaker Hook 完整实现

import { ref, onMounted } from "vue";

export enum SpeakerMode {
  Speaker = "speaker",
  Earphone = "earphone",
  Receiver = "receiver",
}

export interface SpeakerOption {
  label: string;
  value: SpeakerMode;
  icon: string;
}

export function useSpeaker(setSinkIdCallback?: (sinkId: string) => Promise<boolean>) {
  const showSpeakerPopup = ref(false);
  const currentSpeaker = ref<SpeakerMode>(SpeakerMode.Speaker);
  const audioDevices = ref<MediaDeviceInfo[]>([]);

  const speakerOptions: SpeakerOption[] = [
    { label: "扬声器", value: SpeakerMode.Speaker, icon: "speaker" },
    { label: "耳机", value: SpeakerMode.Earphone, icon: "headset" },
    { label: "听筒", value: SpeakerMode.Receiver, icon: "phone" },
  ];

  /**
   * 获取可用的音频输出设备
   */
  const getAudioDevices = async () => {
    try {
      if (!navigator.mediaDevices?.enumerateDevices) {
        console.warn("浏览器不支持枚举设备");
        return;
      }

      // 先请求麦克风权限,以获取完整的设备标签
      try {
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
        });
        stream.getTracks().forEach((track) => track.stop());
        console.log("✅ 已获取麦克风权限");
      } catch (err) {
        console.warn("⚠️ 无法获取麦克风权限:", err);
      }

      const devices = await navigator.mediaDevices.enumerateDevices();
      audioDevices.value = devices.filter((device) => device.kind === "audiooutput");

      console.log("🔊 可用的音频输出设备:", audioDevices.value);
      audioDevices.value.forEach((device, index) => {
        console.log(`  ${index + 1}. ${device.label} (${device.deviceId})`);
      });
    } catch (error) {
      console.error("❌ 获取音频设备失败:", error);
    }
  };

  /**
   * 切换弹窗显示
   */
  const toggleSpeakerPopup = () => {
    showSpeakerPopup.value = !showSpeakerPopup.value;
  };

  /**
   * 选择扬声器模式
   */
  const selectSpeaker = async (mode: SpeakerMode) => {
    currentSpeaker.value = mode;
    showSpeakerPopup.value = false;

    // 检查浏览器是否支持 setSinkId
    const testAudio = document.createElement("audio");
    if (typeof (testAudio as any).setSinkId !== "function") {
      console.warn("浏览器不支持 setSinkId API");
      return;
    }

    // 刷新设备列表
    await getAudioDevices();

    if (audioDevices.value.length === 0) {
      console.warn("未找到可用的音频输出设备");
      return;
    }

    let targetDevice: MediaDeviceInfo | undefined;

    if (mode === SpeakerMode.Speaker) {
      // 扬声器:优先选择 default 设备
      targetDevice = audioDevices.value.find((device) => device.deviceId === "default");

      if (!targetDevice) {
        // 找第一个非耳机设备
        targetDevice = audioDevices.value.find((device) => !device.label.toLowerCase().includes("headphone") && !device.label.toLowerCase().includes("headset") && !device.label.toLowerCase().includes("耳机") && !device.label.toLowerCase().includes("bluetooth"));
      }

      if (!targetDevice && audioDevices.value.length > 0) {
        targetDevice = audioDevices.value[0];
      }
    } else if (mode === SpeakerMode.Earphone) {
      // 耳机:选择包含 headphone/headset/耳机 的设备
      targetDevice = audioDevices.value.find((device) => device.label.toLowerCase().includes("headphone") || device.label.toLowerCase().includes("headset") || device.label.toLowerCase().includes("耳机"));

      if (!targetDevice) {
        console.warn("未检测到耳机设备");
        return;
      }
    } else if (mode === SpeakerMode.Receiver) {
      // 听筒:PC 不支持
      console.warn("PC 不支持听筒模式");
      return;
    }

    if (!targetDevice) {
      console.warn("未找到目标设备");
      return;
    }

    console.log("🔊 目标设备:", targetDevice.label, targetDevice.deviceId);

    // 切换音频输出
    if (setSinkIdCallback) {
      const success = await setSinkIdCallback(targetDevice.deviceId);
      if (success) {
        console.log(`✅ 已切换到: ${targetDevice.label}`);
      }
    }
  };

  // 初始化时获取设备列表
  onMounted(() => {
    getAudioDevices();
  });

  return {
    showSpeakerPopup,
    currentSpeaker,
    audioDevices,
    speakerOptions,
    toggleSpeakerPopup,
    selectSpeaker,
    getAudioDevices,
  };
}

常见问题

Q1: 为什么蓝牙耳机可以切换,有线耳机不行?

A: 这是硬件和驱动的设计导致的:

  • 蓝牙耳机:通过蓝牙适配器连接,被识别为独立的音频输出设备,有独立的设备 ID
  • 有线耳机:插入 3.5mm 或 Type-C 接口后,与内置扬声器共享同一个音频芯片,系统只看到一个设备

当插入有线耳机时,音频芯片会在硬件层面自动切换输出到耳机,对操作系统和浏览器来说仍然是同一个设备,因此无法通过软件切换。

Q2: 为什么需要请求麦克风权限?

A: 出于隐私保护,浏览器在未获得权限时,enumerateDevices() 返回的设备标签(label)会是空的或通用名称。请求麦克风权限后,才能获取完整的设备名称,便于识别设备类型。

Q3: 如何判断浏览器是否支持设备切换?

A: 检查 HTMLMediaElement.setSinkId 方法是否存在:

const audio = document.createElement("audio");
if (typeof audio.setSinkId === "function") {
  console.log("✅ 浏览器支持设备切换");
} else {
  console.log("❌ 浏览器不支持设备切换");
}

Q4: 为什么自动播放会失败?

A: 现代浏览器的自动播放策略要求:

  • 用户必须与页面有过交互(点击、触摸等)
  • 或者音频是静音的

解决方案:

  1. 在用户交互后再初始化音频
  2. 监听用户交互事件,重试播放
  3. 提示用户点击页面以启用音频

Q5: 如何在微信小程序中实现设备切换?

A: 微信小程序使用不同的 API:

// 切换到扬声器
uni.setInnerAudioOption({
  obeyMuteSwitch: false,
  speakerOn: true,
});

// 切换到听筒
uni.setInnerAudioOption({
  obeyMuteSwitch: true,
  speakerOn: false,
});

浏览器兼容性

setSinkId API 支持情况

浏览器版本支持情况
Chrome49+✅ 完全支持
Edge79+✅ 完全支持
Firefox116+✅ 完全支持
Safari❌ 不支持
Opera36+✅ 完全支持

兼容性检测

function checkAudioDeviceSwitchSupport() {
  // 检查 enumerateDevices
  if (!navigator.mediaDevices?.enumerateDevices) {
    return {
      supported: false,
      reason: "浏览器不支持枚举设备",
    };
  }

  // 检查 setSinkId
  const audio = document.createElement("audio");
  if (typeof audio.setSinkId !== "function") {
    return {
      supported: false,
      reason: "浏览器不支持 setSinkId API",
    };
  }

  return {
    supported: true,
    reason: "浏览器完全支持音频设备切换",
  };
}

// 使用
const result = checkAudioDeviceSwitchSupport();
console.log(result);

最佳实践

1. 错误处理

async function switchAudioDevice(deviceId: string) {
  try {
    // 检查浏览器支持
    if (typeof audioElement.setSinkId !== 'function') {
      throw new Error('浏览器不支持设备切换');
    }

    // 切换设备
    await audioElement.setSinkId(deviceId);

    // 确保播放状态
    if (audioElement.paused) {
      await audioElement.play();
    }

    // 用户反馈
    showToast('设备切换成功');

  } catch (error) {
    console.error('设备切换失败:', error);
    showToast('设备切换失败,请重试');
  }
}

2. 用户体验优化

// 1. 记住用户选择
localStorage.setItem("preferredAudioDevice", deviceId);

// 2. 自动恢复上次选择
const savedDeviceId = localStorage.getItem("preferredAudioDevice");
if (savedDeviceId) {
  await switchAudioDevice(savedDeviceId);
}

// 3. 监听设备变化
navigator.mediaDevices.addEventListener("devicechange", () => {
  console.log("设备列表已变化,刷新设备列表");
  getAudioDevices();
});

3. 性能优化

// 避免频繁枚举设备
let deviceListCache: MediaDeviceInfo[] = [];
let lastEnumerateTime = 0;
const CACHE_DURATION = 5000; // 5秒缓存

async function getAudioDevices() {
  const now = Date.now();
  if (now - lastEnumerateTime < CACHE_DURATION) {
    return deviceListCache;
  }

  const devices = await navigator.mediaDevices.enumerateDevices();
  deviceListCache = devices.filter(d => d.kind === 'audiooutput');
  lastEnumerateTime = now;

  return deviceListCache;
}

总结

通过 Web Audio API 的 MediaStreamDestinationHTMLMediaElement.setSinkId,我们可以实现灵活的音频输出设备切换功能。关键点:

  1. 使用 MediaStreamDestination 创建音频流
  2. 将音频流连接到 <audio> 元素
  3. 使用 setSinkId 切换输出设备
  4. 处理浏览器兼容性和自动播放策略
  5. 提供良好的用户体验和错误处理

这个方案已在生产环境中验证,适用于在线教育、视频会议等场景。

以上就是基于Vue3中实现耳机和扬声器切换的完整方案的详细内容,更多关于Vue3耳机和扬声器切换的资料请关注脚本之家其它相关文章!

相关文章

  • 在Vue3中创建和使用全局组件的实现方式

    在Vue3中创建和使用全局组件的实现方式

    在前端开发中,Vue.js 是一个广泛使用的框架,因其灵活性和强大的功能,得到许多开发者的喜爱,Vue 3 的发布为这一框架带来了很多新的特性和改进,在本文中,我们将详细讨论如何在 Vue 3 中创建和使用全局组件,并通过示例代码展示具体实现方式,需要的朋友可以参考下
    2024-07-07
  • vue实现拖拽小图标

    vue实现拖拽小图标

    这篇文章主要为大家详细介绍了vue实现拖拽小图标,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • vue+element UI实现树形表格

    vue+element UI实现树形表格

    这篇文章主要为大家详细介绍了vue+element UI实现树形表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-12-12
  • cesium开发之如何在vue项目中使用cesium,使用离线地图资源

    cesium开发之如何在vue项目中使用cesium,使用离线地图资源

    这篇文章主要介绍了cesium开发之如何在vue项目中使用cesium,使用离线地图资源问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • vuejs+element UI table表格中实现禁用部分复选框的方法

    vuejs+element UI table表格中实现禁用部分复选框的方法

    今天小编就为大家分享一篇vuejs+element UI table表格中实现禁用部分复选框的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-09-09
  • FastApi+Vue+LayUI实现前后端分离的示例代码

    FastApi+Vue+LayUI实现前后端分离的示例代码

    本文主要介绍了FastApi+Vue+LayUI实现前后端分离的示例代码,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • 在Nginx上部署前端Vue项目的详细步骤(超级简单!)

    在Nginx上部署前端Vue项目的详细步骤(超级简单!)

    这篇文章主要介绍了在Nginx上部署前端Vue项目的详细步骤,Nginx是一款高效的HTTP和反向代理Web服务器,作为开源软件,Nginx以其高性能、可扩展性和灵活性广泛应用于Web架构中,文中将步骤介绍的非常详细,需要的朋友可以参考下
    2024-10-10
  • vue+elementUI实现表单和图片上传及验证功能示例

    vue+elementUI实现表单和图片上传及验证功能示例

    这篇文章主要介绍了vue+elementUI实现表单和图片上传及验证功能,结合实例形式分析了vue+elementUI表单相关操作技巧,需要的朋友可以参考下
    2019-05-05
  • vue使用advanced-mark.js实现高亮文字效果

    vue使用advanced-mark.js实现高亮文字效果

    在日常项目中我们往往会有搜索高亮的需求,下面这篇文章主要介绍了vue使用advanced-mark.js实现高亮文字效果的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-08-08
  • vue路由对不同界面进行传参及跳转的总结

    vue路由对不同界面进行传参及跳转的总结

    这篇文章主要介绍了vue路由对不同界面进行传参及跳转的总结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-04-04

最新评论