终究没有人在意一家民营企业的生死

去泰国看了一场“成人秀”,画面尴尬到让人窒息.....

【少儿禁】马建《亮出你的舌苔或空空荡荡》

网友建议:远离举报者李X夫!

司马南|脱口秀算什么?

生成图片,分享到微信朋友圈

自由微信安卓APP发布,立即下载! | 提交文章网址
查看原文

【第2841期】为你的 React 应用添加音乐播放器

Doma 前端早读课 2023-01-30

前言

程序员的就是这么想什么做什么,完全自己满足。今日前端早读课文章由 @Doma 投稿分享。

正文从这开始~~

本文介绍 aplayer-react 库,以及我如何使用 aplayer-react 在我的 Gatsby 博客中播放网易云音乐歌曲。

最近在培养每周在我的博客更新周记的习惯。在周记的开头,我会分享本周我在网易云音乐上新收藏的歌曲,有时是一首,有时也会有多首。相比仅仅是将歌曲的链接贴在文中,我想干脆在周记中显示一个音乐播放器,将歌曲播放出来。

播放器的选型上我选择 @DIYgod 创建的 APlayer 库,因为我很喜欢它的外观。由于我的博客使用 React 编写,我编写了 aplayer-react 库,使得可以以 React 组件的形式使用 APlayer。

aplayer-react

aplayer-react 的实现上其实并未调用 APlayer 的 API。事实上,它只是使用了 APlayer 的样式表,功能逻辑则完全使用 React 重写。可以将其理解为 “APlayer 原型的 React 实现”。

示例效果如下:

主要的特性包括:

  • 滚动歌词

  • 音量控制,可以切换静音

  • 播放列表,可切换顺序播放 / 随机播放,以及单曲循环 / 列表循环

  • 根据歌曲封面自动适配主题色

  • 支持服务端渲染

基础用例

一个最基本的用例如下,为 APlayer 组件的 audio 属性传入一个包含歌曲信息的对象。

import { APlayer } from "aplayer-react"
import "aplayer/dist/APlayer.min.css"

render(
<APlayer
audio={{
name: "Dancing with my phone",
artist: "HYBS",
url: "https://music.163.com/song/media/outer/url?id=1969744125",
cover: "https://p1.music.126.net/tOtUdKjS9rktAFRamcomWQ==/109951167748733958.jpg",
}}
/>
)

滚动歌词

在 audio 对象的 lrc 字段设置 LRC 格式的歌词,即可在界面上显示跟随歌曲进度滚动的歌词。

render(
<APlayer
audio={{
name: "Dancing with my phone",
artist: "HYBS",
url: "https://music.163.com/song/media/outer/url?id=1969744125",
cover: "https://p1.music.126.net/tOtUdKjS9rktAFRamcomWQ==/109951167748733958.jpg",
lrc: "[00:00.000] 作词 : James Alyn Wee/Kasidej Hongladaromp...",
}}
/>
)

播放列表

audio 属性除了接收单个歌曲信息对象外,也可以接收包含多个歌曲信息的数组。当 audio 为数组时,会显示一个播放列表。

render(
<APlayer
audio={[
{
name: "Dancing with my phone",
artist: "HYBS",
url: "https://music.163.com/song/media/outer/url?id=1969744125",
cover: "https://p1.music.126.net/tOtUdKjS9rktAFRamcomWQ==/109951167748733958.jpg",
lrc: "[00:00.000] 作词 : James Alyn Wee/Kasidej Hongladaromp...",
},
{
name: "僕は今日も",
artist: "Vaundy",
url: "https://music.163.com/song/media/outer/url?id=1441997419",
cover: "https://p1.music.126.net/AnR2ejcBgGnOJXPsytivBQ==/109951164922366027.jpg",
lrc: "[00:00.000] 作词 : Vaundy\n[00:00.002] 作曲 : Vaundy\n...",
},
...
]}
/>
)

以上是几个常用功能的简介,完整的使用说明见 https://aplayer-react.js.org。

源码仓库见 https://github.com/SevenOutmanm/aplayer-react。

我如何使用 aplayer-react

我的博客使用 Gatsby 搭建,使用 Markdown 编写文章,并可以在 frontmatter 中存放一些信息,通过 GraphQL 查询。在周记中,我将本周收藏的歌曲链接放到 frontmatter 中的 songs 字段。

例如:

---
title: 22w47
date: 2022-11-25
songs:
- https://music.163.com/song?id=1969744125
- https://music.163.com/song?id=1441997419
---

在文章页面组件中,使用 gatsby-transformer-remark 插件提供的 markdownRemark GraphQL 查询可以读取出 frontmatter 中的数据

export const pageQuery = `graphql
query {
markdownRemark(id: { eq: $id }) {
frontmatter {
title
date(formatString: "MMMM DD, YYYY")
songs
}
}
}
`;

export default function Weekly({
data: { markdownRemark }
}) {
return (
<NeteaseMusicPlayer songUrls={markdownRemark.frontmatter.songs} />
...
)
}

这里的 NeteaseMusicPlayer 组件,是用于根据 songUrls 属性中的歌曲链接来获取歌曲的名称、歌手、封面图、歌词等信息。大致逻辑如下:

import { APlayer } from "aplayer-react"

export function NeteaseMusicPlayer({ songUrls }) {
// 从歌曲分享链接中提取歌曲 id
const songIds = useMemo(() => {
return songUrls.map(url => getSongId(url))
}, [songUrls])

// 根据歌曲 id 查询歌曲详细信息、歌词
const songInfos = useSongInfos(songIds)

return <APlayer audio={songInfos} theme="auto" autoPlay />
}

useSongInfos 钩子中通过请求 NeteaseCloudMusicApi 来查询歌曲的详细信息和歌词。

export function useSongInfos(songIds) {
// 初始返回歌曲的媒体播放地址,从而播放器可以先开始播放
const [songInfos, setSongInfos] = useState(() =>
songIds.map(id => composeMediaUrl(id))
)

useEffect(() => {
// 获取歌曲详细信息
fetch(`ncm.api/song/detail?ids=${songIds.join(",")}`)
.then(response => response.json())
.then(({ songs }) => {
setSongInfos(
songs.map(songInfo => {
return {
name: songInfo.name,
artist: songInfo.ar.map(artist => artist.name).join("/"),
url: `https://music.163.com/song/media/outer/url?id=${songInfo.id}`,
cover: songInfo.al.picUrl,
}
})
)

songs.forEach((songInfo, index) => {
// 获取歌曲歌词
fetch(`ncm.api/lyric?id=${songInfo.id}`)
.then(response => response.json())
.then(({ lrc: { lyric } }) => {
setSongInfos(prev => {
const song = prev[index]
return [
...prev.slice(0, index),
{
...song,
lrc: lyric,
},
...prev.slice(index + 1),
]
})
})
})
})
}, [songIds])

return songInfos
}

效果如下:

但是到目前为止,这样的实现存在一个小问题。在歌曲信息加载完成之前,播放器会短暂地显示缺省状态,观感上还是有些奇怪。

既然每篇周记中包含的歌曲,在周记创建的时候就已经确定了,能否提前将歌曲的详细信息获取完直接静态地写进页面呢?刚好 Gatsby 提供了相关的能力。

Gatsby 允许在页面中通过编写 GraphQL 的形式查询数据用于展示,并且这个查询发生在构建阶段,查询到的数据直接以静态数据的形式写进页面。于是我们可以通过 Gatsby 提供的创建自定义 GraphQL 的能力,创建一个能够读取网易云音乐详情的 GraphQL 查询。

在 gatsby-node.js 中,添加 createSchemaCustomization 方法,来添加自定义的 GraphQL 类型声明。

// gatsby-node.js
/**
* @type {import('gatsby').GatsbyNode['createSchemaCustomization']}
*/

exports.createSchemaCustomization = ({ actions: { createTypes } }) => {
createTypes(`
# Netease Cloud Music songs' info
type NeteaseCloudMusicSong {
id: Int
name: String
mediaUrl: String
ar: [NeteaseCloudMusicArtist]
al: NeteaseCloudMusicAlbum
lrc: String
}

type NeteaseCloudMusicArtist {
name: String
}

type NeteaseCloudMusicAlbum {
picUrl: String
}

type MarkdownRemark implements Node {
frontmatter: Frontmatter
}

type Frontmatter {
songs: [NeteaseCloudMusicSong]
}
`
)
}

这里我根据我所需要的歌曲详情信息,创建了歌曲信息类型 NeteaseCloudMusicSong。并扩展了 gatsby-transformer-remark 提供的 MarkdownRemark 类型,使得 frontmatter 中增加一个 songs 字段,来查询我们的 NeteaseCloudMusicSong 信息。

接着,添加 createResolvers 方法,来声明如何解析 songs 字段。

// gatsby-node.js
const ncmApi = require("NeteaseCloudMusicApi")
/**
* @type {import('gatsby').GatsbyNode['createResolvers']}
*/

exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
Frontmatter: {
songs: {
type: ["NeteaseCloudMusicSong"],
resolve(source) {
if (!source.songs) return source.songs

// 从歌曲分享链接中提取歌曲 id
const songIds = source.songs.map(url => getSongId(url))

// 根据歌曲 id 查询歌曲详细信息
return ncmApi
.song_detail({ ids: songIds.join(",") })
.then(response => response.body.songs)
},
},
},
NeteaseCloudMusicSong: {
mediaUrl: {
type: "String",
resolve(source) {
return composeMediaUrl(source.id)
},
},
lrc: {
type: "String",
resolve(source) {
return ncmApi
.lyric({ id: source.id })
.then(response => response.body.lrc.lyric)
},
},
},
}
createResolvers(resolvers)
}

接着,在文章页面组件中,修改 GraphQL 查询,直接读取 songs 的各个详情字段。

export const pageQuery = `graphql
query {
markdownRemark(id: { eq: $id }) {
frontmatter {
title
date(formatString: "MMMM DD, YYYY")
songs {
name
ar {
name
}
mediaUrl
al {
picUrl
}
lrc
}
}
}
}
`;

export default function Weekly({
data: { markdownRemark }
}) {
return (
<NeteaseMusicPlayer songs={markdownRemark.frontmatter.songs} />
...
)
}

最后,从 NeteaseMusicPlayer 组件移除请求 API 的逻辑,仅仅将 GraphQL 查询结果的结构转为 aplayer-react 接收的结构即可。

export function NeteaseMusicPlayer({ songs }) {
return (
<APlayer
audio={songs.map(songInfo => ({
name: songInfo.name,
artist: songInfo.ar.map(artist => artist.name).join("/"),
url: songInfo.mediaUrl,
cover: songInfo.al.picUrl,
lrc: songInfo.lrc,
}))}
theme="auto"
autoPlay
/>
)
}

这样一来,首次加载时播放器就已经具有完整的歌曲详细信息,不会再显示缺省状态。

结语

最后,欢迎大家 star 收藏 aplayer-react GitHub 仓库 ,也欢迎来我的博客留言。

关于本文
作者:@Doma
原文:https://doma.land/blog/ 为你的 React 应用添加音乐播放器 /

关于【React】相关推荐,欢迎读者自荐投稿,前端早读课等你来。+v:zhgb_f2er

【第2816期】React Server Component: 混合式渲染

【第2803期】React 数据获取:避免条件竞争

文章有问题?点此查看未经处理的缓存