在前端页面中有时候我们会需要写一个音乐播放器的组件,来实现音乐播放功能,今天我们来实现一个音乐播放器的功能。如果你是前端小白,本篇文章将为你非常细致的讲解前端vue的基础以及如何使用vue搭建一个音频播放器,代码我放在了最后 <audio></audio>这是一个音频元素 :src="attachImageUrl(songUrl)" :这是动态绑定音频的源地址,songUrl 可能是在组件的 data 或计算属性中定义的变量,通过 attachImageUrl方法进行处理后作为音频的源 `:ref="player"` 中的冒号 `:` 是用于绑定属性的语法。`ref` 是一个特殊的属性,用于在组件中获取对 DOM 元素或组件实例的引用。 通过在模板中使用 `ref` 属性并给它赋值(例如 `player`),可以在 Vue 组件的实例中通过 `this.$refs` 对象来访问对应的真实的 DOM 元素或组件实例,从而进行操作或获取信息。 在上述代码中,给 `<audio>` 标签添加 `ref="player"`,意味着可以在组件的其他地方(例如 methods 方法中)通过 `this.$refs.player` 来获取该 `<audio>` 元素的引用,以便进行相关操作,例如播放、暂停音频,获取或设置音频的当前时间等。 例如,可以通过 ` this.$refs.player.play()` 来播放音频,通过 `this.$refs.player.pause()` 来暂停播放等。 `ref` 属性的主要作用包括: 1. 操作 DOM 元素:在一些必要情况下,可以直接对获取到的 DOM 元素进行操作,如设置焦点、修改样式等。 2. 组件间通信:获取子组件的实例,从而调用子组件的方法或访问其数据。 需要注意的是,只有在组件挂载后才能访问通过 `ref` 获取的引用,如果在初始渲染时访问,可能会得到 `null`,因为此时对应的元素或组件实例还不存在。 另外,`$refs` 不是响应式的,不应该试图在模板中进行数据绑定。同时,在函数式组件或通过 `v-for` 渲染的列表中,`$refs` 将不会自动更新为数组或对象,需要特别处理。 @canplay="canplay" @timeupdate="timeupdate" @ended="ended"这些都是用@定义的方法,用来表示音频可以播放,更新时间,播放结束三个方法
import { defineComponent, ref, getCurrentInstance, computed, watch } from "vue";
这段代码是在 Vue 3 中引入了一些常用的功能模块和函数。
- `defineComponent`:用于定义组件。
- `ref`:用于创建响应式的数据。
- `getCurrentInstance`:用于获取当前组件的实例,可以访问组件的一些内部属性和方法,但在大多数情况下,应优先使用 `setup` 函数的参数来获取所需信息,除非确实需要访问一些特殊的内部属性。
- `computed`:用于创建计算属性,根据其他响应式数据计算出一个派生的响应式值。
- `watch`:用于监听响应式数据的变化,执行相应的回调函数。
这里有人要问,为什么ref用于创建响应式的数据,但是`$refs` 不是响应式的?
那你还记得$的作用吗?$表示当前实例:提供了对实例属性、方法、生命周期钩子和其他特殊功能的访问权限。
`$refs` 不是响应式的原因是,它本身的设计目的并不是用于创建响应式数据,而是用于在组件中引用 DOM 元素或子组件实例。
`ref` 用于创建响应式的数据,通过 `ref` 定义的变量是响应式的,当该变量的值发生变化时,相关的视图会自动更新。
而 `$refs` 是一个对象,它持有在父组件中注册过 `ref` 的所有 DOM 元素和组件实例的引用。这些引用在组件渲染完成后才被填充,并且它不会自动随响应式数据的变化而更新。
在 Vue 中,`ref` 的主要作用是使普通变量具有响应式特性,方便在组件中进行数据的交互和操作。而 `$refs` 则更侧重于提供一种方式,让开发者能够直接访问和操作 DOM 元素或子组件实例,例如获取 DOM 元素的属性、调用子组件的方法等。 例如,在模板中可以通过 `this.$refs.refName` 来访问绑定了 `ref` 的 DOM 元素或子组件实例,但它本身不会对数据的变化做出实时响应。
一般来说,应避免在模板中或计算属性中使用 `$refs` 来做数据绑定或依赖于其值的计算,因为它不是响应式的。如果需要进行响应式的数据操作和更新,应该使用通过 `ref` 创建的响应式变量。同时,也要注意不要过度使用 `$refs` 直接操作 DOM,尽量遵循 Vue 的数据驱动和组件化的设计理念,通过数据的变化来驱动视图的更新,以提高代码的可维护性和可读性。
以下是一个简单的示例代码,展示了 `ref` 创建响应式数据和 `$refs` 非响应式的特点:
<template>
<div>
<button @click="changeValue">Change Value</button>
<p>{{ value }}</p>
<child ref="childComponent" />
</div>
</template>
<script>
import { defineComponent, ref, getCurrentInstance } from "vue";
import child from "./child.vue";
export default defineComponent({
components: {
child,
},
setup() {
// 使用 ref 创建响应式数据
const value = ref(10);
const changeValue = () => {
value.value = 20;
};
// 获取当前组件实例
const instance = getCurrentInstance();
// 模拟在某个时刻操作 $refs
setTimeout(() => {
console.log(instance.$refs.childComponent);
}, 1000);
return {
value,
changeValue,
};
},
});
</script>
在上述代码中: `value` 是通过 `ref` 创建的响应式数据,点击按钮修改其值时,页面会自动更新显示。 -而 `$refs.childComponent` 是对 `child` 组件的引用,它不会随组件内其他数据的变化而自动更新。在 `setTimeout` 中打印 `$refs.childComponent` 时,其值不会因为 `value` 的变化而改变。
import { useStore } from "vuex"
在 Vue 中,import { useStore } from "vuex"
用于导入 useStore
函数,该函数用于在组件中获取 Vuex 存储实例,以便访问和操作存储中的状态、提交 mutations 以及调用 actions 等。
import { HttpManager } from "@/api";
这段代码通常表示从项目中的 @/api
路径导入了一个名为 HttpManager
的模块或类。
这意味着在当前文件中,您可以使用导入的 HttpManager
来进行与 HTTP 相关的操作,比如发送请求、处理响应等,具体的功能取决于 HttpManager
的实现。@/api其实就是src/api路径。
这个src/api路径属于代码的一部分,随后会放在后面的代码上。
`export default defineComponent({})` 是在 Vue 中用于定义组件的一种方式。 其中,`defineComponent` 是一个函数,它接受一个对象作为参数(即括号中的 `{}`)。这个对象用于配置组件的各种属性和行为。 通过将组件的定义放在 `defineComponent` 函数中,可以获得一些额外的好处,尤其是在使用 TypeScript 进行开发时,它有助于进行类型推导和提供更好的类型提示。 括号中的 `{}` 内部可以包含许多属性和方法,常见的有: - `name`:组件的名称。 - `props`:定义组件接收的属性。 - `data`:一个函数,用于返回组件的初始数据。 - `methods`:定义组件的方法。 - `created`、`mounted` 等:组件的生命周期钩子函数。 - `template` 或 `render`:定义组件的模板或渲染函数,用于描述组件的外观和结构。 例如,以下是一个简单的 Vue 组件定义示例:
import { defineComponent } from 'vue';
export default defineComponent({
name: 'MyComponent',
props: {
message: {
type: String,
default: ''
}
},
data() {
return {
counter: 0
};
},
methods: {
increment() {
this.counter++;
}
}
});
在上述示例中: - `name` 为组件指定了名称 `'MyComponent'`。 - `props` 定义了一个名为 `message` 的属性,其类型为字符串 `String`,默认值为空字符串 `''`。这意味着使用该组件时可以传递一个 `message` 属性给组件。 - `data` 函数返回了组件的初始数据,这里定义了一个 `counter` 属性初始值为 0。 - `methods` 中定义了一个 `increment` 方法,用于增加 `counter` 的值。
setup() {}的作用
在 Vue 3 中,setup()
函数是一个用于定义组件逻辑的重要部分。它具有以下主要作用:
- 集中管理组件逻辑:可以将组件相关的逻辑,如数据定义、方法、生命周期钩子等,集中在
setup()
函数中进行处理,使组件的逻辑更加清晰和易于维护。 - 替代部分生命周期钩子:
setup()
函数会在组件的created
钩子之前执行,它不能直接调用其他生命周期相关函数,但其他生命周期函数可以调用setup
中定义的属性和方法。 - 定义响应式数据:使用
ref
、reactive
等函数在setup()
中创建响应式数据,方便处理组件状态的变化,实现数据的动态更新。 - 组织和复用逻辑:可以将相关的逻辑抽取为可复用的函数,并在
setup()
函数中进行调用,提高代码的复用性。同时,将相关逻辑放在一起,使得组件更加清晰和易于维护。 - 更好的类型推断:由于
setup()
函数本身是一个普通的 JavaScript 函数,可以更好地与 TypeScript 配合,提供更好的类型推断和代码提示。 - 暴露数据和方法给模板:通过返回一个对象,将对象中的属性和方法暴露给组件的模板,在模板中可以直接使用这些数据和方法。
const divRef = ref<HTMLAudioElement>();
这行代码使用 ref
函数创建了一个名为 divRef
的响应式引用。
ref<HTMLAudioElement>
表示这个引用期望引用的是一个 HTMLAudioElement
类型的元素。
通过这样的定义,后续可以在组件的其他地方(比如模板或其他方法中)使用 divRef
来操作或获取与之相关的 HTMLAudioElement
元素。
const player = (el) => { divRef.value = el; };的作用是什么?
这段代码定义了一个名为 `player` 的函数,该函数接收一个参数 `el` 。 在函数内部,将传入的参数 `el` 赋值给 `divRef.value` 。这意味着通过调用 `player` 函数并传入一个元素,会将该元素存储在 `divRef` 所引用的对象的 `value` 属性中。
这两行代码相当于定于了一个响应式的引用,当后定义了一个player函数用来这个响应式数据传值
const muted = ref(true);
这里是定义了一个响应式数据muted,表示静音
这里就会发现一个问题:const定义在setup()里面和定义在setup()外面有什么区别?
在 Vue 3 中,`const` 定义在 `setup()` 里面和定义在 `setup()` 外面的主要区别在于作用域和能否在模板中直接使用。
当 `const` 定义在 `setup()` 里面时: -定义的变量、函数或导入的内容可以在组件的模板中直接使用。这是因为 `setup()` 中的代码会被编译成组件的相关部分,其中顶层的绑定会自动暴露给模板。 - 每次组件实例被创建时,`setup()` 中的代码都会执行。
例如:
<script setup>
const msg = 'Hello!';
function log() {
console.log(msg);
}
</script>
<template>
<button @click="log">{{ msg }}</button>
</template>
在上述代码中,`msg` 和 `log` 函数定义在 `setup()` 内, 可以在模板中直接使用 `{{ msg }}` 和 `@click="log"` 而当 `const` 定义在 `setup()` 外面时,它只是一个普通的 JavaScript 变量或常量,不能直接在组件的模板中使用。它的作用域取决于其定义的位置(例如是在模块级别、函数内部等)。 例如,如果在模块的其他地方定义了一个 `const`:
const someGlobalValue = 'Global'; ```
这个 `someGlobalValue` 不能直接在组件的模板中访问和使用。如果需要在组件中使用该值,可能需要通过某种方式将其传递给组件或者在组件内部进行适当的处理。 另外,需要注意的是,在 Vue 3 的 `<script setup>` 语法中,更推荐将相关的变量和函数定义在 `setup()` 内部,以便于更好地组织和管理组件的逻辑,并且能够直接在模板中使用这些响应式数据和方法。这样可以使组件的代码更加简洁和直观,也更符合 Vue 3 的组合式 API 风格。
这段代码使用 document.querySelector('audio') 来获取页面中第一个 <audio> 元素,并将其赋值给常量 audioDom 。
需要注意的是,如果页面中不存在 <audio> 元素,audioDom 将为 null 。并且,这样获取到的元素不是响应式的,如果元素在后续发生了变化(例如被动态添加、删除或修改),这个 audioDom 变量不会自动更新。在 Vue 应用中,如果需要对 DOM 元素进行响应式的操作,通常会使用 ref 或者结合其他的 Vue 相关机制来实现。
if (audioDom) { // 设置为静音并尝试自动播放 audioDom.muted = true; audioDom.play() .then(() => { // 自动播放成功 }) .catch(error => { // 自动播放失败,可能是因为没有用户交互 console.error('自动播放失败,需要用户交互。', error); }); }
如果成功获取到了 audioDom
(即页面中存在对应的音频元素),就将其设置为静音,并尝试自动播放。
如果自动播放成功,会执行 then
中的回调函数(这里没有具体的操作)。
如果自动播放失败,通常是因为浏览器的限制(例如需要用户先进行交互才能自动播放),就会执行 catch
中的回调函数,在控制台输出错误信息,提示自动播放失败是因为需要用户交互,并打印出具体的错误对象。
这里需要单独说明一下audioDom.muted和audioDom.play()本身是<audio>
元素自带的元素和方法
除了前面提到的`muted`(表示是否静音)、`play()`(开始播放音频)等属性和方法外,`<audio>`元素还有以下一些常见的属性和方法:
**属性**:
- `autoplay`:布尔值属性,声明该属性后,音频会尽快自动播放,而无需等待整个音频文件下载完成。但自动播放可能会影响用户体验,需谨慎使用。
- `controls`:如果声明该属性,浏览器将提供一个包含声音、播放进度、播放暂停等的控制面板,以便用户控制音频的播放。
- `crossorigin`:枚举属性,用于展示音频资源是否可以通过 CORS 加载。可选值包括
`anonymous`(在发送跨域请求时不携带验证信息)和`use-credentials`(发送跨域请求时携带验证信息)。
- `currentTime`:读取该属性将返回一个双精度浮点值,用以标明以秒为单位的当前音频的播放位置。设置该属性可改变播放位置,但需注意一些限制,例如不能设置到媒体时间轴开始之前的时间等。 -
`duration`:只读属性,返回一个双精度浮点数,指明音频在时间轴中的持续时间(总长度),以秒为单位。如果没有媒体或媒体不可用,则返回`NaN`;如果是不确定长度的直播流等,则返回`+infinity`。
- `loop`:布尔属性,声明该属性后,音频将循环播放。 - `preload`:枚举属性,用于提示浏览器使用何种加载方式以达到较好的用户体验。可取值包括`none`(示意用户可能不会播放该音频,或服务器希望节省带宽)、
`metadata`(示意获取元数据是有必要的,即便用户可能不播放音频)、`auto`(示意用户可能会播放音频)以及空字符串(等效于`auto`属性)。
需注意,`autoplay`属性的优先级高于`preload`。
- `src`:嵌入音频的 URL。也可以使用`<source>`元素来替代该属性指定嵌入的音频。 - `defaultMuted`:布尔值,表示默认是否关闭音量。
- `defaultPlaybackRate`:浮点数,表示默认的播放速率,默认是1.0。 - `disableRemotePlayback`:布尔值,是否允许远程回放时显示工具栏。
在 Safari 中,可使用`x-webkit-airplay="deny"`作为替代方案。
- `networkState`:表示当前网络状态,可能的值包括:0(没有数据)、1(媒体元素处在激活状态,但还没开始下载)、2(下载中)、3(没有找到媒体文件)。
- `paused`:只读的布尔值,表示媒体文件是否处在暂停状态。
- `playbackRate`:浮点数,表示媒体文件的播放速度,1.0 为正常速度,负数表示向后播放。 - `played`:返回一个`TimeRanges`对象,表示已播放的媒体内容范围。
- `readyState`:整数,表示媒体文件的准备状态,可能的值为 0(没有任何数据)、1(已获取元数据)、2(可播放当前帧,但不足以播放多个帧)、3(可以播放多帧,至少为两帧)、4(可以流畅播放)。
- `seekable`:返回一个`TimeRanges`对象,表示用户可以搜索的媒体内容范围。 - `volume`:浮点数,表示音量,0.0 表示静音,1.0 表示最大音量。 **方法**:
- `pause()`:暂停当前播放的音频。
- `captureStream()`:返回一个`MediaStream`对象,用于捕获当前媒体文件的流内容。 - `canPlayType()`:接受一个 MIME 字符串作为参数,用于判断该类型的媒体文件是否可以播放,返回值可能为`probably`(似乎可播放)、
`maybe`(无法在不播放的情况下判断是否可播放)、空字符串(表示无法播放)。 - `addTextTrack()`:添加文本轨道(如字幕)到音频文件。
- `load()`:重新加载音频元素。
由于字数太多,接下来的内容下一篇再分析
<template>
<audio :src="attachImageUrl(songUrl)" controls="true" :ref="player" preload="true" @canplay="canplay" @timeupdate="timeupdate" @ended="ended">
<!--(1)属性:controls,preload(2)事件:canplay,timeupdate,ended(3)方法:play(),pause() -->
<!--controls:向用户显示音频控件(播放/暂停/进度条/音量)-->
<!--preload:属性规定是否在页面加载后载入音频-->
<!--canplay:当音频/视频处于加载过程中时,会发生的事件-->
<!--timeupdate:当目前的播放位置已更改时-->
<!--ended:当目前的播放列表已结束时-->
</audio>
</template>
<script lang="ts">
import { defineComponent, ref, getCurrentInstance, computed, watch } from "vue";
import { useStore } from "vuex";
import { HttpManager } from "@/api";
export default defineComponent({
setup() {
const { proxy } = getCurrentInstance();
const store = useStore();
const divRef = ref<HTMLAudioElement>();
const player = (el) => {
divRef.value = el;
};
const muted = ref(true); // 添加一个 reactive 的 muted 属性
const audioDom = document.querySelector('audio');
if (audioDom) {
// 设置为静音并尝试自动播放
audioDom.muted = true;
audioDom.play()
.then(() => {
// 自动播放成功
})
.catch(error => {
// 自动播放失败,可能是因为没有用户交互
console.error('自动播放失败,需要用户交互。', error);
});
}
const songUrl = computed(() => store.getters.songUrl); // 音乐链接
const isPlay = computed(() => store.getters.isPlay); // 播放状态
const volume = computed(() => store.getters.volume); // 音量
const changeTime = computed(() => store.getters.changeTime); // 指定播放时刻
const autoNext = computed(() => store.getters.autoNext); // 用于触发自动播放下一首
// 监听播放还是暂停
watch(isPlay, () => togglePlay());
// 跳到指定时刻播放
watch(changeTime, () => (divRef.value.currentTime = changeTime.value));
watch(volume, (value) => (divRef.value.volume = value));
// 开始 / 暂停
function togglePlay() {
isPlay.value ? divRef.value.play() : divRef.value.pause();
}
// 获取歌曲链接后准备播放
function canplay() {
// 记录音乐时长
proxy.$store.commit("setDuration", divRef.value.duration);
// 开始播放
if (muted.value) {
divRef.value.muted = false;
muted.value = false;
}
// divRef.value.play();
proxy.$store.commit("setIsPlay", true);
}
// 音乐播放时记录音乐的播放位置
function timeupdate() {
proxy.$store.commit("setCurTime", divRef.value.currentTime);
}
// 音乐播放结束时触发
function ended() {
proxy.$store.commit("setIsPlay", false);
proxy.$store.commit("setCurTime", 0);
proxy.$store.commit("setAutoNext", !autoNext.value);
}
return {
songUrl,
player,
canplay,
timeupdate,
ended,
muted,
attachImageUrl: HttpManager.attachImageUrl,
};
},
});
</script>
<style scoped>
audio {
display: none;
}
</style>