跳到主要内容

Docusaurus 嵌入音乐播放器

Docusaurus 是一个基于 React 的静态网站生成器,专门用于构建文档网站。在 Docusaurus 中,我们可以通过多种方式嵌入音乐播放器,从简单的 HTML 标签到复杂的 React 组件。

方法一:HTML 标签嵌入

Docusaurus 支持在 MDX 文件中直接使用 HTML 标签,实现最基础的音频播放。但使用过程中需要注意语法规范,例如:

<audio controls name="media">
<source src="https://static.getiot.tech/audio/the-moon-song.mp3" type="audio/mp3" />
</audio>

示例效果:

注意事项:

  • 自闭合标签<source> 标签必须自闭合(使用 />)。
  • 属性格式:避免使用空值属性,如 controls="" 应改为 controls
  • 路径引用:可以使用相对路径或绝对路径。

方法二:使用自定义 AudioPlayer 组件

为了更好的用户体验,我们创建了自定义的 AudioPlayer 组件,提供完整的播放控制功能。

基本用法

import AudioPlayer from '@site/src/components/AudioPlayer';

<AudioPlayer
src="https://static.getiot.tech/audio/the-moon-song.mp3"
title="The Moon Song"
artist="Karen O"
cover="/img/the-moon-song-cover.png"
/>

示例效果:

The Moon Song

The Moon Song

Karen O

0:000:00

组件特性

  • 🎵 播放控制:播放/暂停按钮
  • ⏱️ 进度条:可拖拽的播放进度控制
  • 🔊 音量控制:音量滑块和静音按钮
  • 🎨 美观界面:咖啡色调设计,支持明暗模式
  • 📱 响应式:移动端友好的布局
  • 🖼️ 封面显示:可选的音乐封面图片

组件属性

属性类型默认值说明
srcstring-音频文件URL(必需)
titlestring'音乐'音乐标题
artiststring'未知艺术家'艺术家名称
coverstring-封面图片URL(可选)
classNamestring-自定义CSS类名

完整组件代码

如果你想在自己的项目中使用这个组件,以下是完整的实现代码。你可以直接复制这些代码到你的项目中:

AudioPlayer 组件

src/components/AudioPlayer/index.tsx
import React, {useState, useRef, useEffect} from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';

export interface AudioPlayerProps {
src: string;
title?: string;
artist?: string;
cover?: string;
className?: string;
}

export default function AudioPlayer({
src,
title = '音乐',
artist = '未知艺术家',
cover,
className,
}: AudioPlayerProps): JSX.Element {
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false);
const audioRef = useRef<HTMLAudioElement>(null);

useEffect(() => {
const audio = audioRef.current;
if (!audio) return;

const updateTime = () => setCurrentTime(audio.currentTime);
const updateDuration = () => setDuration(audio.duration);
const handleEnded = () => setIsPlaying(false);

audio.addEventListener('timeupdate', updateTime);
audio.addEventListener('loadedmetadata', updateDuration);
audio.addEventListener('ended', handleEnded);

return () => {
audio.removeEventListener('timeupdate', updateTime);
audio.removeEventListener('loadedmetadata', updateDuration);
audio.removeEventListener('ended', handleEnded);
};
}, []);

const togglePlay = () => {
if (!audioRef.current) return;

if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setIsPlaying(!isPlaying);
};

const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
const time = parseFloat(e.target.value);
if (audioRef.current) {
audioRef.current.currentTime = time;
setCurrentTime(time);
}
};

const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseFloat(e.target.value);
setVolume(newVolume);
if (audioRef.current) {
audioRef.current.volume = newVolume;
}
};

const toggleMute = () => {
if (!audioRef.current) return;

if (isMuted) {
audioRef.current.volume = volume;
setIsMuted(false);
} else {
audioRef.current.volume = 0;
setIsMuted(true);
}
};

const formatTime = (time: number) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};

return (
<div className={clsx(styles.audioPlayer, className)}>
<audio ref={audioRef} src={src} preload="metadata" />

{/* 封面和基本信息 */}
<div className={styles.playerHeader}>
{cover && (
<div className={styles.cover}>
<img src={cover} alt={title} />
</div>
)}
<div className={styles.info}>
<h4 className={styles.title}>{title}</h4>
<p className={styles.artist}>{artist}</p>
</div>
</div>

{/* 播放控制 */}
<div className={styles.controls}>
<button
className={clsx(styles.playButton, isPlaying && styles.playing)}
onClick={togglePlay}
aria-label={isPlaying ? '暂停' : '播放'}
>
{isPlaying ? '⏸️' : '▶️'}
</button>

{/* 进度条 */}
<div className={styles.progressContainer}>
<span className={styles.time}>{formatTime(currentTime)}</span>
<input
type="range"
min="0"
max={duration || 0}
value={currentTime}
onChange={handleSeek}
className={styles.progressBar}
aria-label="播放进度"
/>
<span className={styles.time}>{formatTime(duration)}</span>
</div>

{/* 音量控制 */}
<div className={styles.volumeContainer}>
<button
className={styles.volumeButton}
onClick={toggleMute}
aria-label={isMuted ? '取消静音' : '静音'}
>
{isMuted || volume === 0 ? '🔇' : volume < 0.5 ? '🔉' : '🔊'}
</button>
<input
type="range"
min="0"
max="1"
step="0.1"
value={isMuted ? 0 : volume}
onChange={handleVolumeChange}
className={styles.volumeBar}
aria-label="音量控制"
/>
</div>
</div>
</div>
);
}

AudioPlayer 样式文件

src/components/AudioPlayer/styles.module.css
/* AudioPlayer 组件样式 */
.audioPlayer {
background: rgba(255, 255, 255, 0.9);
border: 2px solid var(--ifm-color-primary);
border-radius: 12px;
padding: 1.5rem;
margin: 2rem 0;
box-shadow: 0 4px 12px rgba(111, 78, 55, 0.15);
transition: all 0.3s ease;
}

.audioPlayer:hover {
box-shadow: 0 6px 20px rgba(111, 78, 55, 0.25);
transform: translateY(-2px);
}

/* 暗色模式 */
[data-theme='dark'] .audioPlayer {
background: rgba(45, 45, 45, 0.9);
border-color: var(--ifm-color-primary);
box-shadow: 0 4px 12px rgba(199, 161, 122, 0.2);
}

[data-theme='dark'] .audioPlayer:hover {
box-shadow: 0 6px 20px rgba(199, 161, 122, 0.3);
}

/* 播放器头部 */
.playerHeader {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
gap: 1rem;
}

.cover {
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
border: 2px solid var(--ifm-color-primary);
}

.cover img {
width: 100%;
height: 100%;
object-fit: cover;
}

.info {
flex: 1;
}

.title {
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
font-weight: 600;
color: var(--ifm-color-primary);
}

.artist {
margin: 0;
font-size: 0.9rem;
color: var(--ifm-font-color-base);
opacity: 0.8;
}

/* 控制区域 */
.controls {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}

/* 播放按钮 */
.playButton {
background: var(--ifm-color-primary);
color: white;
border: none;
border-radius: 50%;
width: 48px;
height: 48px;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}

.playButton:hover {
background: var(--ifm-color-primary-dark);
transform: scale(1.1);
}

.playButton.playing {
background: var(--ifm-color-primary-darker);
}

/* 进度条容器 */
.progressContainer {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
min-width: 200px;
}

.time {
font-size: 0.8rem;
color: var(--ifm-font-color-base);
opacity: 0.7;
font-family: monospace;
flex-shrink: 0;
}

.progressBar {
flex: 1;
height: 6px;
border-radius: 3px;
background: rgba(111, 78, 55, 0.2);
outline: none;
cursor: pointer;
-webkit-appearance: none;
}

.progressBar::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--ifm-color-primary);
cursor: pointer;
transition: all 0.2s ease;
}

.progressBar::-webkit-slider-thumb:hover {
background: var(--ifm-color-primary-dark);
transform: scale(1.2);
}

.progressBar::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--ifm-color-primary);
cursor: pointer;
border: none;
transition: all 0.2s ease;
}

.progressBar::-moz-range-thumb:hover {
background: var(--ifm-color-primary-dark);
transform: scale(1.2);
}

/* 音量控制 */
.volumeContainer {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}

.volumeButton {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
transition: all 0.2s ease;
}

.volumeButton:hover {
background: rgba(111, 78, 55, 0.1);
}

[data-theme='dark'] .volumeButton:hover {
background: rgba(199, 161, 122, 0.15);
}

.volumeBar {
width: 80px;
height: 4px;
border-radius: 2px;
background: rgba(111, 78, 55, 0.2);
outline: none;
cursor: pointer;
-webkit-appearance: none;
}

.volumeBar::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--ifm-color-primary);
cursor: pointer;
transition: all 0.2s ease;
}

.volumeBar::-webkit-slider-thumb:hover {
background: var(--ifm-color-primary-dark);
transform: scale(1.2);
}

.volumeBar::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--ifm-color-primary);
cursor: pointer;
border: none;
transition: all 0.2s ease;
}

.volumeBar::-moz-range-thumb:hover {
background: var(--ifm-color-primary-dark);
transform: scale(1.2);
}

/* 响应式设计 */
@media (max-width: 768px) {
.audioPlayer {
padding: 1rem;
}

.playerHeader {
flex-direction: column;
text-align: center;
gap: 0.5rem;
}

.cover {
width: 60px;
height: 60px;
}

.controls {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}

.progressContainer {
min-width: auto;
}

.volumeContainer {
justify-content: center;
}
}

你可以参考此方法创建自己想要的播放器组件,例如迷你播放器组件、播放列表组件等。

方法三:集成第三方播放器

网易云音乐外链播放器

function NeteaseMusicPlayer({songId}) {
return (
<iframe
frameBorder="no"
border="0"
marginWidth="0"
marginHeight="0"
width="330"
height="86"
src={`//music.163.com/outchain/player?type=2&id=${songId}&auto=1&height=66`}
/>
);
}

使用示例:

<NeteaseMusicPlayer songId="1901371647" />

Spotify 嵌入播放器

function SpotifyPlayer({trackId}) {
return (
<iframe
style={{borderRadius: '12px'}}
src={`https://open.spotify.com/embed/track/${trackId}?utm_source=generator`}
width="100%"
height="152"
frameBorder="0"
allowFullScreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
/>
);
}

常见问题

  1. 音频文件无法播放?

    检查文件路径、CORS 设置和音频格式支持。

  2. 如何实现播放列表?

    创建状态管理,使用数组存储歌曲信息,实现上一首/下一首逻辑。

  3. 如何自定义播放器样式?

    通过 CSS Modules 或全局 CSS 覆盖默认样式。

  4. 支持哪些音频格式?

    主要支持 MP3、WAV、OGG、AAC 等格式,具体取决于浏览器支持。

  5. 如何实现音频可视化?

    使用 Web Audio API 的 AnalyserNode 创建频谱分析器。

参考资源

🤔