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
Karen O
0:000:00
组件特性
- 🎵 播放控制:播放/暂停按钮
- ⏱️ 进度条:可拖拽的播放进度控制
- 🔊 音量控制:音量滑块和静音按钮
- 🎨 美观界面:咖啡色调设计,支持明暗模式
- 📱 响应式:移动端友好的布局
- 🖼️ 封面显示:可选的音乐封面图片
组件属性
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
src | string | - | 音频文件URL(必需) |
title | string | '音乐' | 音乐标题 |
artist | string | '未知艺术 家' | 艺术家名称 |
cover | string | - | 封面图片URL(可选) |
className | string | - | 自定义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"
/>
);
}
常见问题
-
音频文件无法播放?
检查文件路径、CORS 设置和音频格式支持。
-
如何实现播放列表?
创建状态管理,使用数组存储歌曲信息,实现上一首/下一首逻辑。
-
如何自定义播放器样式?
通过 CSS Modules 或全局 CSS 覆盖默认样式。
-
支持哪些音频格式?
主要支持 MP3、WAV、OGG、AAC 等格式,具体取决于浏览器支持。
-
如何实现音频可视化?
使用 Web Audio API 的 AnalyserNode 创建频谱分析器。