Skip to content

小枫音乐播放器接入指南 ​

小枫音乐播放器(XF Music Player)是可嵌入网站和 Web 应用的 HTML5 音乐播放器插件。本指南说明如何使用发布的 npm 包或 CDN 文件,通过 Web Component 标签、JavaScript 实例以及 Vue、React 接入歌单、歌词和主题功能。

同一页面只保留一个活跃播放器宿主,播放状态与音频管理器由实例共享。标签自动挂载与 JS 实例接入二选一;更换 tagName 不会创建相互隔离的并发播放器。重建前等待 destroy(),销毁后创建新控制器,不再挂载旧控制器。

技术栈与优势 ​

播放器核心基于 Lit + Howler + TypeScript 开发。Lit 负责 Web Component、响应式属性、Shadow DOM 渲染和生命周期;Howler 统一处理播放、暂停、进度、音量、切歌与音频资源释放。

  • 响应式稳定:配置、歌单、歌词、主题和播放状态自动驱动界面更新。
  • 跨框架复用:HTML、Vue、React、Svelte、Angular 与静态页面共享同一套标签和实例 API。
  • 音频生命周期可靠:统一回收音频实例、监听器、请求和计时器,降低多实例资源泄漏风险。
  • 性能可控:延迟初始化、封面懒加载、请求取消、状态节流与歌词滚动集中管理。
  • 生产可维护:公开配置、数据结构、生命周期和错误兜底保持清晰边界。

代码下载地址 ​

npm 安装命令 ​

bash
npm install xf-music-player

发布文件 ​

txt
dist/
  music-player.min.js
  music-player.esm.js
  old-music-player.min.js
  plugin/
    ie-out/
      index.js
    sakura/
      sakura.min.js
  • music-player.min.js:供 <script> 或 CDN 使用的固定名称 IIFE 产物。
  • music-player.esm.js:供 Vite、Webpack、Rollup 等现代构建工具使用的 ESM 产物。
  • old-music-player.min.js:旧版播放器兼容产物。
  • plugin/:可选的浏览器兼容检测和樱花特效插件。

安装 ​

bash
pnpm add xf-music-player

也可以使用 npm 或 yarn:

bash
npm install xf-music-player
yarn add xf-music-player

快速使用 ​

方式一:标签自动挂载 ​

适合静态页面、博客、官网、活动页等无需复杂运行时配置的场景。

html
<xf-music-player
  language="zh"
  theme="xf-original-theme"
  mode="cloud"
  api-url="https://music.api.xfyun.club/api/v1/music/top?platform=qq&topId=26"
  environment="production"
  remember-playback="true"
  play-mode="order"
  volume="0.8"></xf-music-player>

<script src="https://player.xfyun.club/js/music-player/music-player.min.js"></script>

标签方式只能传递字符串、数字、布尔值这类基础属性。playlist、audioProvider、生命周期钩子等复杂配置请使用 JS 实例化。

方式二:IIFE JS 实例化 ​

适合不使用构建工具,但需要传入歌单、远程接口或后续调用实例 API 的页面。

html
<div id="player-root"></div>
<script src="https://player.xfyun.club/js/music-player/music-player.min.js"></script>
<script>
  const player = new window.XfMusicPlayer.MusicPlayer({
    tagName: 'xf-site-player',
    language: 'zh',
    isMonitoring: false,
    mountElement: document.querySelector('#player-root'),
    attributes: {
      theme: 'xf-original-theme',
      mode: 'local',
      environment: 'production',
      playerWidth: '324px',
      bottom: '24px',
      songListHeight: '350px',
      visibleSongListCount: 4,
      rememberPlayback: true,
      memoryKey: 'xf-site-player-memory',
      autoplay: false,
      playMode: 'order',
      volume: 0.8,
      isAutoPopup: false,
      isAutoPlaylist: false,
      colorfulLyric: false,
      playlist: [
        {
          id: 'demo-1',
          title: 'Demo Song',
          artist: 'Xiao Feng',
          cover: 'https://player.xfyun.club/img/half.jpg',
          src: 'https://player.xfyun.club/mp3/half.mp3'
        }
      ]
    }
  })

  // 由用户点击播放器控件开始播放。
</script>

IIFE 产物会暴露 window.XfMusicPlayer.MusicPlayer。

方式三:ESM / npm 引入 ​

适合 Vue、React、Svelte、原生 Vite 等构建型项目。

ts
import { MusicPlayer } from 'xf-music-player'

const player = new MusicPlayer({
  tagName: 'xf-app-player',
  language: 'zh',
  isMonitoring: false,
  attributes: {
    theme: 'xf-original-theme',
    mode: 'cloud',
    apiUrl: 'https://music.api.xfyun.club/api/v1/music/top?platform=qq&topId=26',
    environment: 'production',
    rememberPlayback: true,
    memoryKey: 'xf-player-memory',
    playMode: 'order',
    volume: 0.8,
    isAutoPopup: true,
    isAutoPlaylist: false
  }
})

Vue / React 生命周期接入 ​

在应用根部放置一个播放器拥有者,不要在每个路由或自动挂载标签旁再创建播放器。框架卸载回调不会等待异步清理。将以下应用侧辅助代码保存为 player-session.ts:串行销毁与创建、跳过过期请求,并仅在客户端加载依赖浏览器的包。调用方应处理返回的 Promise;创建或销毁异常会原样交给调用方。Promise 完成只代表本次创建请求完成,不代表音频已加载;初始界面就绪使用 afterRender。

ts
// player-session.ts — application-owned helper, not a player API.
import type { MusicPlayer } from 'xf-music-player'
type PlayerOptions = ConstructorParameters<typeof MusicPlayer>[0]

let player: MusicPlayer | null = null
let pending: Promise<void> = Promise.resolve()
let revision = 0

export function replacePlayer(options: PlayerOptions | null): Promise<void> {
  const request = ++revision
  const operation = pending.then(async () => {
    if (request !== revision) return
    if (player) {
      await player.destroy()
      player = null
    }
    if (request !== revision || !options) return
    const { MusicPlayer } = await import('xf-music-player')
    if (request !== revision) return
    player = new MusicPlayer(options)
  })
  // Keep the queue usable; callers still receive and handle the original rejection.
  pending = operation.catch(() => {})
  return operation
}

React:

tsx
import { useEffect, useRef } from 'react'
import { replacePlayer } from './player-session'

export default function MusicPlayerWidget() {
  const rootRef = useRef<HTMLDivElement | null>(null)

  useEffect(() => {
    const root = rootRef.current
    if (!root) return
    void replacePlayer({
      tagName: 'xf-app-player',
      mountElement: root,
      language: 'zh',
      attributes: {
        mode: 'local',
        autoplay: false,
        playlist: [{ id: 'demo-1', title: 'Demo Song', src: 'https://player.xfyun.club/mp3/half.mp3' }]
      }
    }).catch(console.error)

    return () => {
      void replacePlayer(null).catch(console.error)
    }
  }, [])

  return <div ref={rootRef} className="music-player-root" />
}

Vue 3:

vue
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue'
import { replacePlayer } from './player-session'

const root = ref<HTMLElement | null>(null)
onMounted(() => {
  if (!root.value) return
  void replacePlayer({
    tagName: 'xf-app-player',
    mountElement: root.value,
    language: 'zh',
    attributes: {
      mode: 'local',
      autoplay: false,
      playlist: [{ id: 'demo-1', title: 'Demo Song', src: 'https://player.xfyun.club/mp3/half.mp3' }]
    }
  }).catch(console.error)
})
onBeforeUnmount(() => {
  void replacePlayer(null).catch(console.error)
})
</script>

<template>
  <div ref="root" class="music-player-root"></div>
</template>

Vue 和 React 示例二选一使用;需要跨页面连续播放时,将这个唯一拥有者放在路由切换区域之外。业务自行发起的 fetch 或动态导入应在卸载时取消,并在每次 await 后检查当前路由/请求是否仍有效;播放器只管理自己发起的任务。

如果你的 CDN 支持 ESM,也可以直接从 CDN URL 导入:

ts
import { MusicPlayer } from 'https://cdn.jsdelivr.net/npm/xf-music-player@latest/music-player.esm.js'

CDN 地址 ​

中国大陆推荐使用小枫音乐播放器静态 CDN;jsDelivr 和 unpkg 可作为 npm CDN 备用线路:

html
<!-- 中国大陆推荐 -->
<script src="https://player.xfyun.club/js/music-player/music-player.min.js"></script>
html
<!-- jsDelivr IIFE -->
<script src="https://cdn.jsdelivr.net/npm/xf-music-player@latest/music-player.min.js"></script>
html
<!-- unpkg IIFE -->
<script src="https://unpkg.com/xf-music-player@latest/music-player.min.js"></script>
ts
// jsDelivr ESM
import { MusicPlayer } from 'https://cdn.jsdelivr.net/npm/xf-music-player@latest/music-player.esm.js'

小枫静态 CDN 的固定地址指向当前稳定版。官网示例使用 jsDelivr 的 @latest 标签;生产项目如需严格锁定构建结果,可将其替换为明确的已发布版本号。

创建选项 ​

ts
interface CreateMusicPlayerTagOptions {
  /** 自定义元素标签名,必须包含连字符;非法值会回退为 xf-music-player */
  tagName?: string
  /** UI 语言 */
  language?: 'zh' | 'en'
  /** 是否开启右侧系统监控面板,建议仅开发环境开启 */
  isMonitoring?: boolean
  /** 播放器运行配置,复杂对象和函数都从这里传入 */
  attributes?: MusicPlayerAttributes
  /** 挂载容器,默认 document.body */
  mountElement?: HTMLElement
  /** 是否关闭自动挂载,由业务主动调用 mount() */
  manualMount?: boolean
  /** 生命周期钩子,也可以通过链式 API 后续注册 */
  hooks?: MusicPlayerHooks
}

国际化语言 ​

播放器当前支持 zh 和 en 两种语言。语言配置不属于 MusicPlayerAttributes,而是播放器实例或播放器标签的顶层配置。

标签方式:

html
<xf-music-player language="en"></xf-music-player>

JS / Vue / React 实例化方式:

ts
const player = new MusicPlayer({
  tagName: 'xf-music-player',
  language: 'en',
  attributes: {
    theme: 'xf-original-theme',
    mode: 'local',
    playlist: []
  }
})

如果生产环境需要在中英文之间切换,建议销毁当前实例后用新的 language 重新创建播放器,确保错误提示音、按钮文案、歌词占位和提示信息全部同步到目标语言。

ts
await player.destroy()

const nextPlayer = new MusicPlayer({
  tagName: 'xf-music-player',
  language: 'zh',
  attributes: {
    theme: 'xf-original-theme',
    mode: 'local',
    playlist
  }
})

在线调试页的语言选择会同步影响预览播放器,并在 HTML、JavaScript、Vue、React 配置代码中自动生成对应配置。

手动挂载示例:

ts
const player = new MusicPlayer({
  manualMount: true,
  mountElement: document.querySelector('#player-root') as HTMLElement,
  attributes: {
    rememberPlayback: true,
    playMode: 'single'
  }
})

player
  .beforeRender(el => console.log('before render', el))
  .afterRender(el => console.log('after render', el))
  .mount()

歌曲数据格式 ​

ts
interface SongInfo {
  id: string | number
  title: string
  artist?: string
  album?: string
  cover?: string
  format?: string
  /** 音频地址;云端数据允许为空,播放时会进入错误提示音兜底流程 */
  src?: string | null
  duration?: number
  lyrics?: string | ILyricItem[]
  lyricsUrl?: string | null
  lyrics_url?: string | null
}

lyrics 支持结构化数组或标准 LRC 字符串;lyricsUrl / lyrics_url 可指向外链 LRC 文本。切歌时播放器会取消旧歌词请求,只应用当前歌曲的歌词结果。

txt
[00:00.00] Song title
[00:05.00] First lyric line

duration 使用秒。format 是可选的格式提示,例如 flac、wav、aiff,适合 Blob URL 或不带扩展名的音频地址;它不会增加浏览器的解码能力。

配置项 ​

以下配置对应 MusicPlayerAttributes,通过 JS / Vue / React 实例化时放入 attributes,通过标签方式接入时使用短横线属性名,例如 rememberPlayback 对应 remember-playback。

左右滑动查看完整表格 · 也可聚焦表格后使用方向键

字段类型默认值说明
themestringxf-original-theme内置主题名,也支持 random-theme、auto-theme
customThemeNamestring-从页面样式表提取同名 class/id CSS 变量
customThemeStylestring-注入自定义主题 CSS
playerWidthstring324px播放器宽度
fontNamestring-字体名
bottomstring2em底部距离
songListHeightstring350px歌单展开高度
visibleSongListCountnumber4歌单可视歌曲数量
isAutoPopupbooleanfalse是否自动弹出/展开播放器,只控制界面状态,不触发播放
isAutoPlaylistbooleanfalse是否展开歌单
colorfulLyricbooleanfalse是否启用彩色歌词和封面取色背景
audioVisualizerbooleanfalse是否启用 2D Canvas 实时音频可视化;开启后进度条使用连续波形展示播放进度,底部歌词条同步显示底部对齐的柱状音频背景;多彩歌词开启时每个柱状线使用稳定分色,跨域音频无法分析时会自动降级
lazyLoadTimernumber0延迟初始化播放器的时间,单位 ms
lazyLoadAnimationUrlstring按主题自动选择主封面和歌单封面加载前展示的动画图
modecloud | localcloud数据源模式;云端模式使用 apiUrl,本地模式使用 playlist / audioProvider
apiUrlstringVITE_API_URL云端歌单接口地址,也可通过 api-url 属性自定义;支持 {environment} 占位符
environmentdevelopment | production | testproduction当前环境
rememberPlaybackbooleantrue是否记忆播放
memoryKeystringxf-music-player-memorylocalStorage 记忆 key
autoplaybooleanfalse是否挂载后尝试自动播放,只控制播放启动,不控制播放器弹出
playModeorder | single | randomorder播放模式
volumenumber0.8音量,范围 0-1
playlistSongInfo[][]本地初始歌单;构建产物不内置 mock 数据
audioProviderMusicAudioProvider-自定义音频接口

标签方式只适合字符串、数字、布尔值等基础属性;playlist、audioProvider、生命周期钩子和复杂对象请使用 JS / Vue / React 实例化。

TypeScript 配置类型:

ts
type MusicPlayerAttributes = {
  theme?: string
  customThemeName?: string
  customThemeStyle?: string
  playerWidth?: string
  fontName?: string
  bottom?: string
  songListHeight?: string
  visibleSongListCount?: number
  isAutoPopup?: boolean
  isAutoPlaylist?: boolean
  colorfulLyric?: boolean
  audioVisualizer?: boolean
  lazyLoadTimer?: number
  lazyLoadAnimationUrl?: string
  mode?: 'cloud' | 'local'
  apiUrl?: string
  environment?: 'development' | 'production' | 'test'
  rememberPlayback?: boolean
  memoryKey?: string
  autoplay?: boolean
  playMode?: 'order' | 'single' | 'random'
  volume?: number
  playlist?: SongInfo[]
  audioProvider?: MusicAudioProvider
}

播放器内部状态包含标签名、监控状态、语言、播放器配置、播放运行态和播放控制命令。业务侧通常不需要直接访问内部 Store,生产环境优先使用 MusicPlayer 实例 API;在线调试控制台会基于这些状态能力做可视化配置。

ts
type MusicStoreState = {
  tagName: string
  isMonitoring: boolean
  language: 'zh' | 'en'
  playerConfig: MusicPlayerAttributes
  playback: MusicPlaybackState
  command: MusicPlayerCommand | null
  setTagName: (val?: string) => void
  toggleMonitoring: (val?: boolean) => void
  setLanguage: (val: 'zh' | 'en') => void
  updatePlayerConfig: (config: Partial<MusicPlayerAttributes>) => void
  setPlaylist: (playlist: SongInfo[], currentIndex?: number) => void
  updatePlayback: (state: Partial<MusicPlaybackState>) => void
  setPlayMode: (mode: 'order' | 'single' | 'random') => void
  dispatchCommand: (type: MusicPlayerCommandType, payload?: unknown) => void
  clearMusicPlayerInstance: () => void
}

播放器插槽 ​

命名插槽接收播放器宿主的直接子节点。这些节点保留在页面 Light DOM 中,页面 CSS 可以直接设置其样式;不需要穿透播放器内部 Shadow DOM。

插槽名位置与行为
player-default位于主题容器内、浮动主体之前,附加业务内容,不替换内置控件;位置和样式由业务 CSS 设置
player-content替换浮动主体内的默认展开按钮、播放控制和歌单;传入后需要自行实现这些控件
player-lyrics替换内置底部歌词及全屏歌词交互区域;自定义节点的显示、隐藏和交互由业务管理
默认插槽未声明 slot 的直接子节点输出到宿主渲染层、核心播放器之前,不属于内置播放控制区

直接子节点:四类插槽 ​

先用下面的结构确认插槽位置。mode="local" 避免结构示例请求云端歌单;这四块文字只是占位内容。

html
<xf-music-player
  class="slot-map"
  theme="xf-original-theme"
  is-auto-popup="true"
  language="zh"
  mode="local"
  player-width="350px"
  bottom="3em"
  font-name="Google Sans Code"
  is-monitoring="false"
  colorful-lyric="false"
>
  <div class="slot-note">默认插槽</div>
  <div slot="player-default" class="slot-banner">播放器默认插槽</div>
  <div slot="player-content" class="slot-content">播放器内容插槽</div>
  <div slot="player-lyrics" class="slot-lyrics">歌词控制插槽</div>
</xf-music-player>
<script src="https://player.xfyun.club/js/music-player/music-player.min.js"></script>

四个节点都直接放在 <xf-music-player> 内,标签位于脚本前。player-content 会替换内置控件与歌单,player-lyrics 会替换内置歌词,因此上面的占位文字不会自动变成播放按钮或同步歌词。只想增加内容时,保留默认插槽与 player-default,移除另外两个节点即可恢复内置界面。

播放器宿主是零尺寸浮动层,不参与正文排版;自定义节点需要显式布局。下面的页面 CSS 直接设置这些业务节点,不影响内置主题。示例把两个附加信息块固定在顶部,自定义歌词条固定在底部,给播放器预留间距。font-name 仅指定字体名称;要使用该字体,页面还需要自行引入字体文件。

css
.slot-map {
  display: block;
  color: #273042;
  font: 14px/1.5 system-ui, sans-serif;
}
.slot-map .slot-note,
.slot-map .slot-banner {
  position: fixed;
  left: 12px;
  box-sizing: border-box;
  width: max-content;
  max-width: calc(100vw - 24px);
  padding: 8px 12px;
  border: 1px solid #dcdfea;
  border-radius: 8px;
  background: #f7f8fc;
}
.slot-map .slot-note { top: 16px; }
.slot-map .slot-banner { top: 68px; }
.slot-map .slot-content {
  box-sizing: border-box;
  width: min(350px, calc(100vw - 24px));
  padding: 24px;
  border: 1px solid #dcdfea;
  border-radius: 16px;
  background: #fff;
  text-align: center;
}
.slot-map .slot-lyrics {
  position: fixed;
  right: 12px;
  bottom: 8px;
  left: 12px;
  height: 28px;
  border-radius: 8px;
  background: #28243e;
  color: #f8f7ff;
  font-size: 12px;
  line-height: 28px;
  text-align: center;
}

实用示例:保留内置播放器,增加控制按钮 ​

下面是同一个独立页面的 HTML、CSS 和 JavaScript 三部分,按顺序一起使用。示例仅使用 player-default 添加两个按钮,保留原有播放器和歌词;与标签自动挂载示例二选一,同一页面不要再创建第二个播放器。

html
<div id="slot-player-root"></div>
<template id="slot-player-template">
  <div slot="player-default" class="slot-toolbar">
    <button type="button" data-player-action="toggle">播放 / 暂停</button>
    <button type="button" data-player-action="next">下一首</button>
  </div>
</template>
<script src="https://player.xfyun.club/js/music-player/music-player.min.js"></script>
css
.slot-demo .slot-toolbar {
  position: fixed;
  top: 16px;
  right: 16px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  max-width: calc(100vw - 32px);
  padding: 10px;
  border: 1px solid #d7dce5;
  border-radius: 12px;
  background: #ffffff;
  color: #1f2937;
  font: 14px/1.5 system-ui, sans-serif;
}
.slot-demo .slot-toolbar button {
  padding: 6px 10px;
  border: 1px solid #c4cbdb;
  border-radius: 8px;
  background: #f3f5fb;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.slot-demo .slot-toolbar button:focus-visible {
  outline: 2px solid #6254cc;
  outline-offset: 2px;
}
js
const root = document.querySelector('#slot-player-root')
const template = document.querySelector('#slot-player-template')
const events = new AbortController()
const player = new window.XfMusicPlayer.MusicPlayer({
  tagName: 'xf-slot-demo',
  language: 'zh',
  mountElement: root,
  manualMount: true,
  attributes: {
    mode: 'local',
    isAutoPopup: true,
    rememberPlayback: false,
    playlist: [{
      id: 'slot-demo',
      title: 'Demo Song',
      src: 'https://player.xfyun.club/mp3/half.mp3'
    }]
  }
})

player.beforeRender(host => {
  host.classList.add('slot-demo')
  host.append(template.content.cloneNode(true))
  host.querySelector('[data-player-action="toggle"]').addEventListener(
    'click', () => player.toggle(), { signal: events.signal }
  )
  host.querySelector('[data-player-action="next"]').addEventListener(
    'click', () => player.next(), { signal: events.signal }
  )
}).beforeDestroy(() => events.abort()).mount()

async function disposeSlotPlayer() {
  await player.destroy()
}

beforeRender 在首次渲染前把插槽节点加入宿主,确保命名插槽被识别。运行中可以更新已存在节点的文字、样式和内容;要增加新的插槽区域,应在创建阶段准备好对应节点。不要使用 host.innerHTML = ... 覆盖已有业务节点。

若使用 player-content 或 player-lyrics,把对应节点也放入模板作为宿主直接子节点,并为其编写 CSS 与事件处理。它们替换而非叠加内置界面;自定义按钮继续调用 player.play()、player.pause()、player.next() 等公开 API,不查询或修改内部 Shadow DOM。

页面退出或重建前调用并等待 disposeSlotPlayer();beforeDestroy 通过 AbortController 移除本示例注册的业务事件。播放器不会自动清理接入方绑定在页面其他节点上的事件。

云端模式 ​

mode 默认是 cloud。未显式配置 apiUrl 时,播放器会读取构建时的 VITE_API_URL:

txt
VITE_API_URL=https://music.api.xfyun.club/api/v1/music/top?platform=qq&topId=26

也可以通过标签或 JS 配置覆盖:

html
<xf-music-player
  mode="cloud"
  api-url="https://music.api.xfyun.club/api/v1/music/top?platform=qq&topId=26"></xf-music-player>
ts
new MusicPlayer({
  attributes: {
    mode: 'cloud',
    apiUrl: 'https://music.api.xfyun.club/api/v1/music/top?platform=qq&topId=26'
  }
})

云端模式配置有效 apiUrl 且没有自定义 audioProvider 时,播放器先预加载歌单。等待超过 2 秒显示国际化加载提示;超过 10 秒、请求失败或空歌单会显示国际化失败提示。请求失败时,当前宿主渲染可操作的空播放器并保留错误状态,不会立即重复同一次失败预加载。可通过 player.setPlaylist(...) 补入歌单,或更换 apiUrl、environment、audioProvider 加载新数据。已离开的旧宿主不会因请求完成重新出现。内置请求使用 credentials: 'omit';需要业务请求选项时使用 provider。

云端歌单接口需要返回统一结构:

json
{
  "code": 200,
  "msg": "success",
  "data": [
    {
      "id": "002qU5aY3Qu24y",
      "title": "青花瓷",
      "artist": "周杰伦",
      "album": "我很忙",
      "cover": "https://y.qq.com/music/photo_new/T002R300x300M000002eFUFm2XYZ7z.jpg",
      "src": null,
      "duration": 239,
      "lyrics_url": "https://music.api.xfyun.club/api/v1/music/lyric?platform=qq&songId=002qU5aY3Qu24y"
    }
  ]
}

失败结构:

json
{
  "code": 500,
  "msg": "服务器错误",
  "data": null
}

当 code !== 200 时,播放器会读取 msg 作为错误信息。src 可以为空;用户播放这类歌曲时,播放器不会创建空音频实例,而是播放当前语言的错误提示音,提示音结束后切换下一首。连续 3 首失败后停止播放。

如果 apiUrl 中包含 {environment},播放器会自动替换为当前 environment:以下路径表示业务自己的接口,不是 XF API Server 的现成端点。

ts
new MusicPlayer({
  attributes: {
    mode: 'cloud',
    apiUrl: '/api/{environment}/music/list',
    environment: 'production'
  }
})

歌词接口支持普通 LRC 文本,也支持统一 JSON 响应。播放器只读取 data.lyric:

json
{
  "code": 200,
  "msg": "success",
  "data": {
    "lyric": "[00:00.00] 第一行歌词"
  }
}

XF API Server 接入示例 ​

官网调试页提供 XF API 地址生成器,可根据服务地址、平台和接口参数自动生成 api-url,并支持拉取预览。推荐直接用于播放器云端模式的接口:

GET https://music.api.xfyun.club/api/v1/music/top?platform=netease&topId=3778678GET https://music.api.xfyun.club/api/v1/music/top?platform=qq&topId=26GET https://music.api.xfyun.club/api/v1/music/playlist-songs?platform=netease&playlistId=3778678GET https://music.api.xfyun.club/api/v1/music/playlist-songs?platform=qq&playlistId=8523075134

歌曲数据中的 lyrics_url 会在播放或切歌时请求,播放器只读取歌词接口响应中的 data.lyric:

GET https://music.api.xfyun.club/api/v1/music/lyric?platform=netease&songId=5257138GET https://music.api.xfyun.club/api/v1/music/lyric?platform=qq&songId=002qU5aY3Qu24y

top、playlist-songs 的 data 直接是播放器歌曲数组。search 只返回歌曲基础信息,不包含 src、duration、lyrics_url,不作为调试器的 api-url 生成入口,避免把不可播放的基础歌曲信息写入播放器。

本地模式 ​

mode="local" 不会主动请求 apiUrl,适合离线页面、静态歌单或业务自己控制数据源:

ts
new MusicPlayer({
  attributes: {
    mode: 'local',
    playlist: [
      {
        id: 'local-1',
        title: 'Local Song',
        src: '/audio/demo.mp3'
      }
    ]
  }
})

本地音频元数据 ​

播放器支持 FLAC、WAV、AIFF / AIFF-C、MP3、M4A、Ogg 等格式的元数据读取。解析按文件内容识别,不依赖扩展名;元数据读取成功与浏览器支持播放该编码是两件事,播放器不负责转码。

  • readAudioMetadata(input):接受 File / Blob / ArrayBuffer / Uint8Array,返回 AudioMetadata,不创建临时 URL。
  • readAudioFile(file):接受 File,返回 { song, metadata, dispose };song 可直接传给本地 playlist,包含音频 Blob URL、可用封面、歌词和 format 提示。
  • metadata.format 包含可用的 duration、sampleRate、numberOfChannels、bitsPerSample、bitrate;metadata.common 包含已有标签、picture 和 lyrics,metadata.native 保留原始标签。缺失字段保持缺失,标题缺失时歌曲使用文件名。
  • 空文件、非音频、无有效音频流或解析失败会抛出错误,并通过 cause 保留原始异常;仍可读取的不完整标签可见于 metadata.quality.warnings。这不等于全文件解码或完整性验证。

以下函数在用户通过 <input type="file" accept="audio/*,.flac,.wav,.aif,.aiff,.aifc"> 选择文件后调用。调用前先完成旧播放器的销毁;返回的清理函数由页面退出或下次选文件前调用并等待,避免同时预览多个文件。

ts
import { MusicPlayer, readAudioFile, readAudioMetadata } from 'xf-music-player'

async function previewFile(file: File) {
  const loaded = await readAudioFile(file)
  console.log(loaded.metadata.format, loaded.metadata.common, loaded.metadata.quality.warnings)
  let player: MusicPlayer
  try {
    player = new MusicPlayer({
      attributes: { mode: 'local', rememberPlayback: false, playlist: [loaded.song] }
    })
  } catch (error) {
    loaded.dispose()
    throw error
  }
  return async () => {
    try { await player.destroy() }
    finally { loaded.dispose() }
  }
}

async function inspectFile(file: File) {
  return readAudioMetadata(file)
}

dispose() 可重复调用;仅在歌曲已停止使用后释放音频和封面 Blob URL。临时 URL 不应写入持久歌单,示例关闭记忆播放。普通云端播放不会为了扫描标签而自动下载整个音频文件。使用 IIFE 时对应 window.XfMusicPlayer.readAudioFile / readAudioMetadata;SSR 项目在客户端事件中动态导入这些函数。

自定义音频接口 ​

ts
type MusicSongListPayload = SongInfo[] | { list: SongInfo[] } | { playlist: SongInfo[] } | { songs: SongInfo[] }

interface MusicApiResponse<T> {
  code: number
  msg?: string
  data: T | null
}

type MusicAudioProvider = (
  context: MusicAudioProviderContext
) => Promise<MusicSongListPayload | MusicApiResponse<MusicSongListPayload>> | MusicSongListPayload | MusicApiResponse<MusicSongListPayload>

interface MusicAudioProviderContext {
  environment: 'development' | 'production' | 'test'
  playerConfig: MusicPlayerAttributes
  signal?: AbortSignal
}

audioProvider 优先级高于内置 mode/apiUrl 逻辑,适合业务需要自行签名、鉴权或组合多接口的场景。它支持直接返回 SongInfo[]、{ list: SongInfo[] },也兼容 { code: 200, msg: 'success', data: SongInfo[] }。播放器会自动过滤危险协议和非法数据;当环境快速切换或组件销毁时,旧请求会被 AbortSignal 取消并且不会覆盖新歌单。

下面使用公开 XF API 展示最小 provider;鉴权或签名应由业务自己的服务处理。将 signal 传给 fetch,并检查 HTTP 状态,不把失败响应伪装成空数组:

ts
const player = new MusicPlayer({
  attributes: {
    mode: 'local',
    environment: 'production',
    audioProvider: async ({ signal }) => {
      const response = await fetch('https://music.api.xfyun.club/api/v1/music/top?platform=qq&topId=26', { signal })
      if (!response.ok) throw new Error(`Playlist HTTP ${response.status}`)
      return response.json()
    }
  }
})

只有消费 signal 的业务网络请求才会实际停止;即使 provider 忽略取消,播放器也会丢弃旧请求结果。业务附加的日志/提示应先检查 signal?.aborted,取消时保留原始异常而不是弹出失败提示或无限重试。真实网络、HTTP 和业务 code 错误仍按失败处理。更新 environment、apiUrl 或 provider 引用会重新加载;本地歌单可直接 player.setPlaylist(nextList),清空使用 player.setPlaylist([])。

媒体权限与 iOS 音量 ​

  • autoplay: true 只尝试自动播放;浏览器用户手势策略或嵌入页面权限仍可能阻止它。isAutoPopup 只展开界面,与播放权限无关。推荐由用户点击播放器按钮启动;业务按钮也应在点击回调中调用 player.play(),该方法返回控制器而非可等待的播放 Promise。
  • iframe 接入需结合父页面的 allow="autoplay" 和 Permissions Policy 配置;它不保证免除用户手势要求。播放器不录音,也不需要麦克风权限。
  • 音频 URL 需可访问并符合页面 HTTPS / CSP 配置;API 与歌词跨域请求需要服务端允许 CORS。封面取色和 Web Audio 分析有额外的跨域读取要求,普通音频可播放不代表可分析。
  • iOS/iPadOS 操作音量图标或音量条会显示中英文居中提示,请使用设备音量键或控制中心;不展开无效滑块、不派发音量变更,3 秒内重复操作只提示一次。volume / setVolume() 不保证改变设备实际输出音量。其他平台仍保留音量滑块。

播放交互 ​

  • iOS/iPadOS 以外的平台,音量图标点击后只展开或收起音量滑块,静音能力保留给公开 API、调试控制台或业务自定义入口。
  • 音量面板支持拖动调节,并实时显示 0% 到 100%。
  • 主封面切歌时会先渲染主题加载图,并保留约 120ms 最小展示时间。
  • 进度条支持拖动;拖动开始时暂停音频,拖动中预览时间和歌词,松手后 seek 并播放,即使拖动前处于暂停状态。
  • 上一首、下一首、歌单开关和播放模式按钮带前沿防抖。
  • 点击当前歌单歌曲会播放或暂停,点击其他歌曲会切换并播放。
  • 歌单图片使用懒加载策略,进入视口并停留 150ms 后替换真实图片。
  • 播放、暂停、切歌、静音和切换播放模式会触发居中提示;歌单展开/收起不弹出提示。
  • 歌词遵循播放生命周期:播放时显示,暂停或停止时隐藏;手动关闭会记忆为持续隐藏,后续切歌不自动重新打开。
  • 点击底部歌词条会打开全屏歌词弹窗,点击歌词行会跳转到对应播放时间。

键盘控制 ​

按键行为
Tab切换播放器收缩/展开状态,即 isAutoPopup
Space播放或暂停当前歌曲
ArrowLeft上一首
ArrowRight下一首

键盘控制会跳过输入框、下拉框、按钮、滑块、contenteditable 和组合输入状态;同时会忽略 Ctrl、Meta、Alt 组合键,避免抢占系统或浏览器原生快捷键。

失败兜底 ​

音频加载或播放失败时,播放器不会直接卡死在当前歌曲,而是按当前语言播放内置错误提示音。错误提示音播放完成后会自动切换下一首;连续 3 首歌曲播放失败时停止自动播放,并通过错误提示告知用户。

首次自动播放受到浏览器策略阻止时会降级为暂停,不自动跳到下一首;这与用户主动操作时遇到真实资源错误不同。异常媒体事件在 15 秒内触发 3 次自动切歌也会熔断,后续用户主动播放或选歌才开启新的恢复周期。业务不应在外围添加无限重试或自动 next() 循环。

记忆播放 ​

rememberPlayback 默认开启;业务显式传入 rememberPlayback: false 或 remember-playback="false" 后会关闭。开启时播放器会在 memoryKey 下保存结构化记忆:

ts
interface MusicMemoryState {
  current: MusicMemoryRecord | null
}

current 保存当前歌曲的记忆,包括歌曲 id、音频地址、进度、音量、静音状态、播放模式和歌词手动隐藏偏好。

实例 API ​

ts
player.play()
player.pause()
player.toggle()
player.stop()
player.prev()
player.next()
player.select(0)
player.seek(30)
player.setVolume(0.6)
player.mute(true)
player.setPlayMode('single')
player.setPlaylist(list, 0)
player.setConfig({
  theme: 'xf-dark-theme',
  songListHeight: '350px',
  visibleSongListCount: 4,
  lazyLoadAnimationUrl: '/imgs/custom-loader.svg'
})
await player.destroy()

生命周期 ​

播放器生命周期分为外层 MusicPlayer 控制器和内部 Web Component 生命周期。接入方只需要使用公开钩子,不需要直接操作内部组件。

destroy() 返回 Promise<void>,同一控制器的重复调用复用同一个 Promise;默认 timer: 0 也为异步销毁。替换实例前必须等待。destroy(options) 的一次性 beforeDestroy 会被等待,注册的生命周期钩子与 afterDestroy 则是同步通知,不是异步屏障。一次性回调抛错时宿主仍会移除,接入方应处理拒绝而非直接继续成功交接分支。替换后停止调用旧控制器:播放指令作用于共享状态。

触发顺序 ​

  1. new MusicPlayer(options):归一化配置、注册自定义元素、创建宿主 DOM。
  2. mount():宿主 DOM 插入页面;自动挂载模式下该步骤会在构造后的微任务执行。
  3. beforeRender(el):宿主元素连接到页面后触发。
  4. 内部初始化:写入运行配置、解析主题、加载歌单、恢复记忆播放。
  5. afterRender(el):宿主元素首次渲染完成后触发。
  6. beforeUpdate(el, changed):响应式属性或状态联动触发 DOM 更新前执行。
  7. afterUpdate(el, changed):DOM 更新完成后执行。
  8. destroy() 或 DOM 移除:触发 beforeDestroy(el),随后清理计时器、订阅、音频实例、页面监听和临时资源。
  9. afterDestroy():内部清理完成后触发。

钩子注册 ​

ts
player
  .beforeRender(el => {})
  .afterRender(el => {})
  .beforeUpdate((el, changed) => {})
  .afterUpdate((el, changed) => {})
  .beforeDestroy(el => {})
  .afterDestroy(() => {})

销毁示例:

ts
await player.destroy({
  timer: 300,
  beforeDestroy: async el => {
    // 一次性销毁前回调。
  },
  afterDestroy: () => {
    // 一次性销毁后回调。
  }
})

自定义主题 ​

通过 CSS 变量覆盖播放器主题:

css
.my-player-theme {
  --background-color: #fbfffd;
  --theme-color: #1e7d58;
  --text-color: #7d9e92;
  --song-name-color: #4d9678;
  --text-deputy-color: #6a8e80;
  --text-hover-color: #26996c;
  --text-hover-color-rgb: 38, 153, 108;
}
ts
player.setConfig({ customThemeName: 'my-player-theme' })

内联主题样式同时传入选择器名称与 CSS 文本;使用播放器变量,不依赖页面选择器穿透 Shadow DOM:

ts
player.setConfig({
  customThemeName: 'my-player-theme',
  customThemeStyle: `.my-player-theme {
    --background-color: #fbfffd;
    --theme-color: #1e7d58;
    --text-color: #45665a;
    --song-name-color: #24523f;
    --text-deputy-color: #58776a;
    --text-hover-color: #26996c;
    --text-hover-color-rgb: 38, 153, 108;
  }`
})

音频可视化 ​

audioVisualizer 默认关闭,可在现有实例上切换,不必重新创建播放器:

ts
player.setConfig({ audioVisualizer: true, colorfulLyric: true })
// 关闭波形后保留普通播放。
player.setConfig({ audioVisualizer: false })

开启后以 Canvas 绘制连续波形进度条及底部歌词柱状背景;colorfulLyric 为柱状条使用稳定分色。跨域音频须允许 Web Audio 分析,否则保留普通播放并降级显示。跨域封面须允许读取像素才能取色,否则使用主题背景。关闭可视化后继续使用当前播放器进行普通播放,不需要重新创建播放器。

浏览器兼容性 ​

IIFE / ESM 产物以 ES2020 为编译目标,转换逻辑赋值和类字段;不注入全局 polyfill,仍需 Web Components、BigInt 等运行时与媒体能力。元数据可解析不代表每个浏览器都可播放该编码,请用目标浏览器/WebView 和真实音频格式验证。

推荐使用现代 Chrome、Edge Chromium、Firefox、Safari 以及主流移动端现代浏览器。播放器依赖 customElements、HTMLElement、Shadow DOM、Promise、Symbol、Object.assign、JavaScript 和 HTML5 Audio。

以下环境不支持或不建议用于生产:

  • Internet Explorer 全部版本:明确不支持,可使用兼容检测插件引导升级。
  • 旧版 Edge Legacy:不建议支持 EdgeHTML 内核版本。
  • Android 旧 WebView / 旧 UC / 旧 QQ 浏览器:缺少 Web Components 或基础 ES 能力时无法运行。
  • iOS 10 及以下 Safari:Web Components、Shadow DOM 与音频策略支持不稳定。
  • 禁用 JavaScript、限制 Web Components 或不支持 HTML5 Audio 的环境:播放器无法初始化或播放。

浏览器插件 ​

兼容检测插件放在播放器主包之前。检测到 IE、Edge Legacy 或关键能力缺失时,会跳转到浏览器升级页:

html
<script src="/player/plugin/ie-out/index.js"></script>
<script src="/player/music-player.min.js"></script>

樱花特效插件可独立使用,与播放器同页时建议放在主包之后:

html
<script src="/player/music-player.min.js"></script>
<script src="/player/plugin/sakura/sakura.min.js"></script>

CDN 地址:

html
<script src="https://cdn.jsdelivr.net/npm/xf-music-player@latest/plugin/ie-out/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xf-music-player@latest/plugin/sakura/sakura.min.js"></script>

旧版迁移 ​

新项目推荐使用 music-player.min.js 或 npm 包中的 MusicPlayer 实例 API。旧项目可暂时引入兼容产物:

html
<script src="/player/old-music-player.min.js"></script>
  • 新页面使用 <xf-music-player> 或 new MusicPlayer(...)。
  • 旧页面短期保留 old-music-player.min.js,逐步迁移配置、歌单和生命周期。
  • 需要完整旧版代码与示例时,查看发布目录中的 xf-MusicPlayer-master/。

生产环境建议 ​

  • CDN 引入时锁定明确版本号,不使用不带版本的 latest 地址。
  • 复杂数据通过 JS 实例化传入,不建议塞进 HTML 属性。
  • 生产环境关闭 isMonitoring。
  • 同一页面只保留一个活跃播放器;自定义 tagName 必须包含连字符,更换标签名不绕开单实例约束。
  • 重新初始化前 await player.destroy();框架清理回调的返回值不构成等待,应由应用共享交接队列串行协调。
  • 对远程歌曲、封面、歌词资源配置稳定的 HTTPS 地址。

FAQ ​

标签引入和 JS 引入怎么选? ​

静态页面、配置简单时用标签自动挂载;需要动态歌单、远程接口、生命周期钩子或实例 API 时用 JS 实例化。

CDN 和 npm 能同时提供吗? ​

可以。npm 包用于构建型项目,CDN 地址用于静态页面或低成本接入。两者建议指向同一个版本的打包产物。

源码不公开会影响使用吗? ​

不会。接入方只需要 npm 包或 CDN 文件,以及本文档里的配置项、数据结构和实例 API。