这东西写得是真的累
项目简介
本项目采用原生 JavaScript 搭建类似网易云的音乐播放器,旨在帮助前端初学者快速入门项目开发,同时参照目前主流的响应式框架思想,未后续的框架学习打下一定的基础。其中应用到的技术包括 ES6 新增的语法糖如解构赋值、箭头函数、展开运算符模板字符串 ,异步处理 Promise,ES6 模块化,异步网络请求 Ajax,单页面应用思想,数据响应式思想。通过上述技术最终完成页面切换,轮播图,音乐播放器等功能
项目一般约束
- 开发环境约束:
- 开发工具:VSCode
- 开发语言:HTML5 、CSS3 、JavaScript
- 时间约束:建议开发周期控制在 2 周内,需要开发者合理规划好时间
- 技术约束:HTML5 、CSS3 、JavaScript(ES6)、Ajax
具体功能
首页
包括:轮播图,推荐歌单列表,歌曲控制栏,播放列表
涉及到的功能如下:
- 共通部分(所有页面都使用到的):所用到的数据都是通过 Ajax 向后端发起请求,结合异步操作 Promise 等待结果;使用模板字符串,根据数据大小动态生成页面上所有的 DOM 元素;proxy 实现数据响应式,监听到数据变化自动渲染视图;基于单页面应用实现页面跳转;
- 轮播图:循环显示图片;
推荐歌单列表:使用 CSS3 的 transition 当移入歌单时会有缩放动画效果,点击歌单跳转到推荐歌单详情页(页面二)
歌曲控制栏:使用 HMTL5 的 audio 元素的属性和方法,控制歌曲的播放、暂停、进度、音量,进度条、歌单列表及歌曲图标信息显示,点击歌单图标页面跳转到播放器页面(页面三);
- 播放列表:对歌曲控制栏按钮添加事件监听,绑定点击事件实现关闭和隐藏,列表中的歌曲绑定点击事件实现歌曲播放
推荐歌单详情页
推荐歌单详情页(页面二)包括三部分:导航栏,歌单信息,歌曲列表
涉及到的功能如下:
- 导航栏:实现返回首页功能,导航栏为 a 标签,点击导航栏 hash 值变化,hash 值是数据响应式,数据变化会调用页面跳转的函数;
- 歌单信息:显示歌单信息,添加按钮绑定点击事件,将歌单添加到播放列表中,播放列表是数据响应式,数据变化播放列表重新渲染。
歌曲列表:显示歌曲列表绑定鼠标移入移出事件,鼠标移入高亮,双击歌曲播放对应歌曲
播放器页面
播放器页面(页面三)包括三部分:播放器背景,歌曲封面,歌词
- 播放器背景:通过 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
3export 声明表达式
或
export {具名符号}由于基本导出必须具有名称,所以要求导出内容必须跟上声明表达式或具名符号。
基本导入:由于使用的是依赖预加载,因此,导入任何其他模块,导入代码必须放置到所有代码之前。对于基本导出,如果要进行导入,使用下面的代码
1
import { 导入的符号列表 } from "模块路径";
注意以下细节:
- 导入时,可以通过关键字
as
对导入的符号进行重命名 - 导入时使用的符号是常量,不可修改
- 可以使用*号导入所有的基本导出,形成一个对象
- 导入时,可以通过关键字
默认导入导出
默认导出:每个模块,除了允许有多个基本导出之外,还允许有一个默认导出,默认导出类似于 CommonJS 中的module.exports
,由于只有一个,因此无需具名
具体的语法是1
2
3export default 默认导出的数据
或
export {默认导出的数据 as default}
由于每个模块仅允许有一个默认导出,因此,每个模块不能出现多个默认导出语句
默认导入:需要想要导入一个模块的默认导出,需要使用下面的语法1
import 接收变量名 from "模块路径";
由于默认导入时变量名是自行定义的,因此没有别名一说
如果希望同时导入某个模块的默认导出和基本导出,可以使用下面的语法1
import 接收默认导出的变量, { 接收基本导出的变量 } from "模块路径";
注:如果使用*号,会将所有基本导出和默认导出聚合到一个对象中,默认导出会作为属性 default 存在
需要好好学习
Ajax介绍及使用
什么是 Ajax
Ajax 全称 Asynchronous JavaScript and XML,AJAX 是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
基于 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
31function 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
11function 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
23console.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("这是结束");
上面程序执行顺序如下:
输出的结果:1
2
3
4
5
6
7//这是开始
//这是一条消息1
//这是一条消息2
//这是一条消息3
//这是结束
//这是来自第一个回调的消息
//这是来自第二个回调的消息
Promise 异步处理
Promise 提供了一套异步处理的通用模型,理解该 API,最重要的,是理解它的异步模型。
- ES6 将某一件可能发生异步操作的事情,分为两个阶段:unsettled 和 settled
- unsettled: 未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
- settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转
事情总是从 未决阶段 逐步发展到 已决阶段的。并且,未决阶段拥有控制何时通向已决阶段的能力。
- ES6 将事情划分为三种状态: pending、resolved、rejected
- pending: 挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
- resolved:已处理,已决阶段的一种状态,表示整件事情已经出现结果,并是一个可以按照正常逻辑进行下去的结果
- rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法按照正常逻辑进行下去的结果,通常用于表示有一个错误
既然未决阶段有权力决定事情的走向,因此,未决阶段可以决定事情最终的状态! 我们将 把事情变为 resolved 状态的过程叫做:resolve,推向该状态时,可能会传递一些数据 我们将 把事情变为 rejected 状态的过程叫做:reject,推向该状态时,同样可能会传递一些数据,通常为错误信息
始终记住,无论是阶段,还是状态,是不可逆的!
- 当事情达到已决阶段后,通常需要进行后续处理,不同的已决状态,决定了不同的后续处理。
- resolved 状态:这是一个正常的已决状态,后续处理表示为 thenable
- rejected 状态:这是一个非正常的已决状态,后续处理表示为 catchable
后续处理可能有多个,因此会形成作业队列,这些后续处理会按照顺序,当状态到达后依次执行
- 整件事称之为 Promise
Promise 的基本使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const 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为状态数据
}
);
细节
- 未决阶段的处理函数是同步的,会立即执行
- thenable 和 catchable 函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,并且,加入的队列是微队列
- pro.then 可以只添加 thenable 函数,pro.catch 可以单独添加 catchable 函数
- 在未决阶段的处理函数中,如果发生未捕获的错误,会将状态推向 rejected,并会被 catchable 捕获
- 一旦状态推向了已决阶段,无法再对状态做任何更改
- 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
13async 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
11async 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
18function 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 包装后按照规则运行。