写一个音乐播放器_基础知识介绍

这东西写得是真的累

项目简介

本项目采用原生 JavaScript 搭建类似网易云的音乐播放器,旨在帮助前端初学者快速入门项目开发,同时参照目前主流的响应式框架思想,未后续的框架学习打下一定的基础。其中应用到的技术包括 ES6 新增的语法糖如解构赋值、箭头函数、展开运算符模板字符串 ,异步处理 Promise,ES6 模块化,异步网络请求 Ajax,单页面应用思想,数据响应式思想。通过上述技术最终完成页面切换,轮播图,音乐播放器等功能

01-1-1

01-2

项目一般约束

  1. 开发环境约束:
    • 开发工具:VSCode
    • 开发语言:HTML5 、CSS3 、JavaScript
  2. 时间约束:建议开发周期控制在 2 周内,需要开发者合理规划好时间
  3. 技术约束:HTML5 、CSS3 、JavaScript(ES6)、Ajax

具体功能

首页

包括:轮播图,推荐歌单列表,歌曲控制栏,播放列表

01-3

涉及到的功能如下:

  • 共通部分(所有页面都使用到的):所用到的数据都是通过 Ajax 向后端发起请求,结合异步操作 Promise 等待结果;使用模板字符串,根据数据大小动态生成页面上所有的 DOM 元素;proxy 实现数据响应式,监听到数据变化自动渲染视图;基于单页面应用实现页面跳转;
  • 轮播图:循环显示图片;
  • 推荐歌单列表:使用 CSS3 的 transition 当移入歌单时会有缩放动画效果,点击歌单跳转到推荐歌单详情页(页面二)

  • 歌曲控制栏:使用 HMTL5 的 audio 元素的属性和方法,控制歌曲的播放、暂停、进度、音量,进度条、歌单列表及歌曲图标信息显示,点击歌单图标页面跳转到播放器页面(页面三);

  • 播放列表:对歌曲控制栏按钮添加事件监听,绑定点击事件实现关闭和隐藏,列表中的歌曲绑定点击事件实现歌曲播放

推荐歌单详情页

推荐歌单详情页(页面二)包括三部分:导航栏,歌单信息,歌曲列表

01-5

涉及到的功能如下:

  • 导航栏:实现返回首页功能,导航栏为 a 标签,点击导航栏 hash 值变化,hash 值是数据响应式,数据变化会调用页面跳转的函数;
  • 歌单信息:显示歌单信息,添加按钮绑定点击事件,将歌单添加到播放列表中,播放列表是数据响应式,数据变化播放列表重新渲染。

歌曲列表:显示歌曲列表绑定鼠标移入移出事件,鼠标移入高亮,双击歌曲播放对应歌曲

播放器页面

播放器页面(页面三)包括三部分:播放器背景,歌曲封面,歌词

01-8

  • 播放器背景:通过 canvas 实现图片的像素点操作,对图片进行高斯模糊,同时背景半透明黑化解决纯色画面显示不佳的问题
  • 歌曲封面:随歌曲播放/暂停,封面旋转/暂停
  • 歌词:通过正则表达式提取歌词和时间信息,并随歌曲进度自动滚动/高亮当前进度歌词

接口

获得轮播图信息

  • 接口描述:获取首页数据,包括轮播图和推荐歌单数据
  • 数据格式:JSON
  • 请求方式:GET
  • 接口 URL :http://localhost:3000/homepage/block/page
  • 提示:端口地址为本地启动端口,推荐使用 VSCode 下,Live Server 插件。

响应数据说明:用到的数据 data.blocks

名称说明
data.blocks[0].extInfo.banners轮播图数组
data.blocks[0].extInfo.banners.pic轮播图图片地址
data.blocks[0].extInfo.banners.bannerId轮播图图片 id
data.blocks[1].creatives推荐歌单数组
data.blocks[1].creatives.creativeId歌单 id
data.blocks[1].creatives.uiElement.image.imageUrl歌单图片
data.blocks[1].creatives.uiElement.mainTitle.title歌单名称
响应示例

获得推荐歌单列表信息

  • 接口描述:传入歌单 id, 可以获取对应歌单内的所有的音乐信息
  • 数据格式:JSON
  • 请求方式:GET
  • 接口 URL :http://localhost:3000/playlist/detail?id=5146191146
  • 必选参数 : id
  • 提示:端口地址为本地启动端口,推荐使用 VSCode 下,Live Server 插件。

响应数据说明:用到的数据 playlist

名称说明
playlist歌单对象
playlist.name歌单名
playlist.createTime歌单创建时间
playlist.coverImgUrl歌单封面
playlist.creator.avatarUrl歌单创建者头像
playlist.description歌单描述
playlist.tracks歌单歌曲列表数组
playlist.tracks.id歌曲 id
playlist.tracks.name歌曲名称
playlist.tracks.ar歌曲歌手信息
playlist.tracks.al歌曲所属专辑信息
playlist.tracks.dt歌曲时长
响应示例

获得歌曲信息

  • 接口描述: 传入音乐 ids, 可获得歌曲详情
  • 数据格式:JSON
  • 请求方式:GET
  • 接口 URL :http://localhost:3000/song/detail?ids=5243631
  • 必选参数 : ids
  • 提示:端口地址为本地启动端口,推荐使用 VSCode 下,Live Server 插件。

响应数据说明:用到的数据 songs

名称说明
name歌曲名称
id歌曲 id
ar歌手信息
al专辑信息
响应示例

获得歌曲歌词

  • 接口描述: 传入音乐 id, 可获得对应音乐的歌词
  • 数据格式:JSON
  • 请求方式:GET
  • 接口 URL :http://localhost:3000/lyric?id=5243631
  • 必选参数 : id
  • 提示:端口地址为本地启动端口,推荐使用 VSCode 下,Live Server 插件。

响应数据说明:用到的数据 lrc

名称说明
lrc歌词

模块化以及ajax使用

知识点

  • 模块化及使用
  • Ajax 及使用
  • Promise 异步处理

模块就是一个 JS 文件,它实现了一部分功能,并隐藏自己的内部实现,同时提供了一些接口供其他模块使用。模块解决的问题是全局变量污染和依赖混乱问题,为前端开发大型应用提供了基础。目前成熟的模块化解决方案有 CommonJS/AMD/CMD/ESM。

common js使用module.exports和require.

es6使用import和export

下面介绍一下es6的模块

本实验采用的是 ESM,ESM 作为 ES6 模块化的正式标准 ,目前主流浏览器能够正常运行。

  • ESM 使用

首先在浏览器使用 ESM,仅需要在 script 标签加入 type="module"属性

1
<script src="入口文件" type="module">
  • 基本导入导出

    基本导出:类似于 exports.xxx = xxxx ,基本导出可以有多个,每个必须有名称

    基本导出的语法如下:

    1
    2
    3
    export 声明表达式

    export {具名符号}

    由于基本导出必须具有名称,所以要求导出内容必须跟上声明表达式或具名符号。

    基本导入:由于使用的是依赖预加载,因此,导入任何其他模块,导入代码必须放置到所有代码之前。对于基本导出,如果要进行导入,使用下面的代码

    1
    import { 导入的符号列表 } from "模块路径";

    注意以下细节:

    • 导入时,可以通过关键字as对导入的符号进行重命名
    • 导入时使用的符号是常量,不可修改
    • 可以使用*号导入所有的基本导出,形成一个对象

默认导入导出

默认导出:每个模块,除了允许有多个基本导出之外,还允许有一个默认导出,默认导出类似于 CommonJS 中的module.exports,由于只有一个,因此无需具名

具体的语法是

1
2
3
export default 默认导出的数据

export {默认导出的数据 as default}

由于每个模块仅允许有一个默认导出,因此,每个模块不能出现多个默认导出语句

默认导入:需要想要导入一个模块的默认导出,需要使用下面的语法

1
import 接收变量名 from "模块路径";

由于默认导入时变量名是自行定义的,因此没有别名一说

如果希望同时导入某个模块的默认导出和基本导出,可以使用下面的语法

1
import 接收默认导出的变量, { 接收基本导出的变量 } from "模块路径";

注:如果使用*号,会将所有基本导出和默认导出聚合到一个对象中,默认导出会作为属性 default 存在

需要好好学习

Ajax介绍及使用

什么是 Ajax

Ajax 全称 Asynchronous JavaScript and XML,AJAX 是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

3-2

基于 Ajax 工作原理,使用 Ajax 最主要就是完成下列事项:

  • 在不重新加载页面的情况下发送请求给服务器。
  • 接受并使用从服务器发来的数据。

代码表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1. 创建 xmlHttpRequest 对象
let xmlhttp;
if (window.XMLHttpRequest) {
// IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
xmlhttp = new XMLHttpRequest();
} else {
// IE6, IE5 浏览器执行代码
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}

// 2. 设置回调函数
xmlHttp.onreadystatechange = callback;

// 3. 使用 open 方法与服务器建立连接
xmlhttp.open(method, url, async);
/*
method:请求的类型;GET 或 POST
url:文件在服务器上的位置
async:true(异步)或 false(同步)
*/

// 添加请求头信息(可选)
xmlhttp.setRequestHeader(header, value);

// 4. 使用 send 方法发送请求
xmlhttp.send(string);
/*
string:仅用于 POST 请求,格式可以为 multipart/form-data,JSON,XML
*/

当请求发送出去后,会得到一个响应结果,在回调函数中针对不同的响应状态进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function callback() {
if (xmlHttp.readyState == 4) {
//判断交互是否成功
/*
readyState属性:表示请求/响应过程的当前阶段
0:未初始化。尚未调用 open()方法。
1:启动。已经调用 open()方法,但尚未调用 send()方法。
2:发送。已经调用 send()方法,但尚未接收到响应。
3:接收。已经接收到部分响应数据。
4:完成。已经接收到全部响应数据,而且已经可以在客户端使用了。
只有在XMLHttpRequest对象完成了以上5个步骤之后,才可以获取从服务器端返回的数据。
*/
if (xmlHttp.status == 200) {
/*
status属性:响应的 HTTP 状态码,常见的状态码如下
200:响应成功
301:永久重定向/永久转移
302:临时重定向/临时转移
304:本次获取内容是读取缓存中的数据
400:请求参数错误
401:无权限访问
404:访问的资源不存在
*/
//服务器的响应,可使用 XMLHttpRequest 对象的 responseText(获得字符串形式的响应数据) 或
// responseXML (获得 XML 形式的响应数据) 属性获得
let responseText = xmlHttp.responseText;
} else {
// 失败,根据响应码判断失败原因
}
}
}

针对响应成功和响应失败,除了对状态码进行判断外,XMLHttpRequest 提供了响应成功和失败的 api 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//将请求时候步骤2改为以下代码
xmlHttp.onload = function () {};
//等效于
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status == 200) {
}
}
};

//将请求时候步骤2改为以下代码
xmlHttp.onerror = function () {};
//等效于
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status !== 200) {
}
}
};

Promise异步处理

事件循环

JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。对于事件循环的理解,首先需要明确的三个概念:栈(Stack),堆(Heap),和事件队列(Queue)。

(执行栈):每个函数调用形成了一个由若干帧组成的栈。

1
2
3
4
5
6
7
8
9
10
11
function foo(b) {
let a = 10;
return a + b + 11;
}

function bar(x) {
let y = 3;
return foo(x * y);
}

console.log(bar(7)); //

当调用 bar 时,第一个帧被创建并压入栈中,帧中包含了 bar 的参数和局部变量。 当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧 )。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了

:对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。

队列:一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。

在浏览器中,队列分为两种:

  • 宏任务(队列):macroTask,计时器结束的回调、事件回调、http 回调等等绝大部分异步函数进入宏队列
  • 微任务(队列):MutationObserver,Promise 产生的回调进入微队列

MutationObserver 用于监听某个 DOM 对象的变化

当执行栈清空时,JS 引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log("这是开始");

function fn1() {
console.log("这是一条消息2");
fn2();
}

function fn2() {
console.log("这是一条消息3");
}

setTimeout(function cb1() {
console.log("这是来自第一个回调的消息");
});

console.log("这是一条消息1");
fn1();

setTimeout(function cb2() {
console.log("这是来自第二个回调的消息");
}, 0);

console.log("这是结束");

上面程序执行顺序如下:

04-7

输出的结果:

1
2
3
4
5
6
7
//这是开始
//这是一条消息1
//这是一条消息2
//这是一条消息3
//这是结束
//这是来自第一个回调的消息
//这是来自第二个回调的消息

Promise 异步处理

Promise 提供了一套异步处理的通用模型,理解该 API,最重要的,是理解它的异步模型。

  1. ES6 将某一件可能发生异步操作的事情,分为两个阶段:unsettledsettled

04-8

  • unsettled: 未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
  • settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转

事情总是从 未决阶段 逐步发展到 已决阶段的。并且,未决阶段拥有控制何时通向已决阶段的能力。

  1. ES6 将事情划分为三种状态: pending、resolved、rejected
  • pending: 挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
  • resolved:已处理,已决阶段的一种状态,表示整件事情已经出现结果,并是一个可以按照正常逻辑进行下去的结果
  • rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法按照正常逻辑进行下去的结果,通常用于表示有一个错误

既然未决阶段有权力决定事情的走向,因此,未决阶段可以决定事情最终的状态! 我们将 把事情变为 resolved 状态的过程叫做:resolve,推向该状态时,可能会传递一些数据 我们将 把事情变为 rejected 状态的过程叫做:reject,推向该状态时,同样可能会传递一些数据,通常为错误信息

始终记住,无论是阶段,还是状态,是不可逆的!

04-9

  1. 当事情达到已决阶段后,通常需要进行后续处理,不同的已决状态,决定了不同的后续处理。
  • resolved 状态:这是一个正常的已决状态,后续处理表示为 thenable
  • rejected 状态:这是一个非正常的已决状态,后续处理表示为 catchable

后续处理可能有多个,因此会形成作业队列,这些后续处理会按照顺序,当状态到达后依次执行

04-10

  1. 整件事称之为 Promise

04-11

Promise 的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const pro = new Promise((resolve, reject) => {
// 未决阶段的处理
// 通过调用resolve函数将Promise推向已决阶段的resolved状态
// 通过调用reject函数将Promise推向已决阶段的rejected状态
// resolve和reject均可以传递最多一个参数,表示推向状态的数据
});

pro.then(
(data) => {
//这是thenable函数,如果当前的Promise已经是resolved状态,该函数会立即执行
//如果当前是未决阶段,则会加入到作业队列,等待到达resolved状态后执行
//data为状态数据
},
(err) => {
//这是catchable函数,如果当前的Promise已经是rejected状态,该函数会立即执行
//如果当前是未决阶段,则会加入到作业队列,等待到达rejected状态后执行
//err为状态数据
}
);

细节

  1. 未决阶段的处理函数是同步的,会立即执行
  2. thenable 和 catchable 函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,并且,加入的队列是微队列
  3. pro.then 可以只添加 thenable 函数,pro.catch 可以单独添加 catchable 函数
  4. 在未决阶段的处理函数中,如果发生未捕获的错误,会将状态推向 rejected,并会被 catchable 捕获
  5. 一旦状态推向了已决阶段,无法再对状态做任何更改
  6. Promise 并没有消除回调,只是让回调变得可控

举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 题目一
const promise1 = new Promise((resolve, reject) => {
console.log("promise1");
resolve("resolve1");
});
const promise2 = promise1.then((res) => {
console.log(res);
});
console.log("1", promise1);
console.log("2", promise2);
/*
'promise1'
'1' Promise{<resolved>: 'resolve1'}
'2' Promise{<pending>}
'resolve1'
*/

// 题目二
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);

/*
1
2
4
"timerStart"
"timerEnd"
"success"
*/

//题目三
Promise.resolve().then(() => {
console.log("promise1");
const timer2 = setTimeout(() => {
console.log("timer2");
}, 0);
});
const timer1 = setTimeout(() => {
console.log("timer1");
Promise.resolve().then(() => {
console.log("promise2");
});
}, 0);
console.log("start");
/*
'start'
'promise1'
'timer1'
'promise2'
'timer2'
*/

为了简化 Promise api 的使用,ES2016 中新增了 async 和 await 两个关键字,下面我们来看下其简写方式又是怎么替代 Promise api 的。

async 和 await

async:目的是简化在函数的返回值中对 Promise 的创建,用于修饰函数(无论是函数字面量还是函数表达式),放置在函数最开始的位置,被修饰函数的返回结果一定是 Promise 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
async function test() {
console.log(1);
return 2;
}

//等效于

function test() {
return new Promise((resolve, reject) => {
console.log(1);
resolve(2);
});
}

await:await 关键字必须出现在 async 函数中,用在某个表达式之前,如果表达式是一个 Promise,则得到的是 thenable 中的状态数据。

1
2
3
4
5
6
7
8
9
10
11
async function test1() {
console.log(1);
return 2;
}

async function test2() {
const result = await test1();
console.log(result);
}

test2();

等效于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function test1() {
return new Promise((resolve, reject) => {
console.log(1);
resolve(2);
});
}

function test2() {
return new Promise((resolve, reject) => {
test1().then((data) => {
const result = data;
console.log(result);
resolve();
});
});
}

test2();

如果 await 的表达式不是 Promise,则会将其使用 Promise.resolve 包装后按照规则运行。

-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道