这是一个完整的项目实现,内容包括网页排版,利用js css实现一些动态特效,特在此记录项目实现过程中的核心思想,以及难点的解决方法和在此项目中学到的新知识。
首先看一下该项目的官网效果
可以看到该网页的实现我们可以将其分为5个部分,即1.导航栏2.歌单推荐栏、新歌首发、精彩推荐、新碟首发、MV3.排行榜4.一个固定在页面右侧的窗口,话不多说,接下来开始实现布局。
一、导航栏
毋庸置疑首先创建一个铺满全局的div,然后创建一个div用来装我们的导航栏部分,在这里我又分为了4个div,分别实现第一行列表,搜索部分,登录部分,和第二行列表,可以看到这个导航栏还是比较经典的格式,这里都通过ul标签实现即可,没有什么复杂的点。
一、css部分:
居中效果:外层大盒子宽高百分百,内层固定宽高margin:0 auto;
清除浮动:在对li设置浮动后,在ul后加伪类::after{}清除浮动(注意 content:‘’;必须写)
对于a标签的文字样式:注意,若a标签存在于一个标签内,直接对外标签设置文字样式,a标签无法继承这些属性。
特效实现,1.鼠标覆盖上文字,文字变色,直接利用:hover实现。
2.鼠标覆盖,画出下拉框: 在存在下拉框的li标签(块元素)内部,创建一个div利用定位,将其固定到合适的位置,在实现样式。然后最初将其样式设置为display:none;利用hover效果在鼠标移动到li标签上,其display变为block;即可实现。
二、js部分:刚刚利用css实现下拉栏效果只能设置开通vip部分(在存在下拉框的li标签(块元素)内部,创建一个div)而充值部分无法正确快捷设置,因为官网效果上,在滑到li标签时不仅出现下拉栏,而li本身是白色,在鼠标覆盖到上有变成绿色的效果移动到子元素,又变完白色。所以用css实现起来比较复杂。所以我们脱离在子元素内设置div的想法,在li后设置,这样就可以避免hover子元素的影响了,然后直接简单的监听mouseover、mousedone从而改变display属性值即可
总结:在这种下滑栏效果实现时:当hover:若子元素改变属性时,父元素也要改变某种属性,那么我们就将此div设置于父元素之后,利用js;若只是单纯的只有父元素或只有子元素有属性改变,则将此div设置于父元素内,利用css即可。
二、歌单推荐+新歌首发+精彩推荐+新碟首发+MV
这几个模块在效果上都是一样的,所以放到一起说。
一、css部分
1.文字居中:两种方法①文字div设置margin 0 auto,设置宽高,设置display:flex;主轴居中。②在对外div设置居中的基础上,对p元素设置宽高,margin 0 auto;
2.下方ul居中:由于这里我利用了绝对定位,使其脱离文档流,所以普通的margin:0 auto;无效,由于实现左右居中,所以使left:0;,right:0;之后在设置margin即可。当然也可以直接利用padding给它挤过来…(最初的白痴做法)
3.下方的歌单列表静态样式不在赘述,并且对同大小的img盒子div设置overflow:hidden;至于变暗部分在img后设置一个div(mask),利用定位重叠于img,最初透明色,鼠标移入变色。
二、js部分
这里有两个圆点,点击跳转页面,鼠标移入还会出现两个按钮,实现滚动切换页面,并且覆盖在上面会使图片视觉拉近,变暗)
首先说下最简单的1.视觉拉近(坑点):一定是对div(mask)元素设置hover实现img元素的scale(1.1)。
2.画面切换:两个ul外有一个wrap(作为视觉窗口,通过设置定位从而实现滚动效果)为按钮添加监听器后,点击时更改wrap的left值。
3.圆点效果实现:首先一个圆点清空函数,清除样式(颜色),然后复杂的点来了…这里说下整体思路首先实现点击滚动,对于圆点点击事件:封装一个判断函数:获取wrap视觉窗口的left值,从而判断出现在处于哪个ul显示区再为圆点设置一个属性num,分别等于0、1、利用计算让其与left发生关联,从而使对应的颜色发生变化,上述过程封装到一个函数里,(方便mouseout事件使用).接下来就是鼠标移动到圆点上,其颜色的变化,这里还要判断当前带特效的圆点,是否为展示页面所关联的圆点,若不是则移出时变回本色,若是则仍保持特效色。
4.鼠标移入到图片上,会有一个小的播放按钮出现,并且是以一种渐变的效果呈现,这里我才用的方法是对该播放按钮设置初值为scale(0) 在鼠标移入时变成scale(1),移出时恢复scale(0)这样就可以出现渐变缩放消失效果啦~
5.鼠标移入到图片上,图片会在视觉上拉近距离,这里实现方法为:为img标签设置一个盒子,该盒子宽高固定,并且拥有overflow:hidden;属性,然后鼠标移入时img放大,移出时恢复,即可实现啦。
总结:
1.最初实现图片变暗,缩放时妄想只用css,后来发现mask生效时必须在img上层,这时img的hover失效,所以必须用js实现缩放效果。
2.圆点实现获取left值,注意获取的是xxpx字符串,所以利用slice()方法截取后,并Number转换。
3.滚动效果,对wrap,left设置的值为-ul的长度,下一个变为-2ul的长度,上一个恢复为最初。(最初设置行间样式初始值不然js无法获取的到)
4.为li设置浮动无效时为ul设置宽度
三、排行榜
一、css部分:
这里的样式设计没有什么难点,就是比较费时间,值得一提的是官网的此处背景是引入的背景图片(五张连在一起),被迫利用background-position进行调整位置。
二、js部分:
这里的特效效果为鼠标移入,中间横线变为播放按钮,移出恢复,且背景图片也相应拉近。
1.横线转换部分:
监听每个li,鼠标移入到后,横线所在的div display:none;因为display这个属性,它在html中不在占据位置。此外,在同位置上设置播放按钮的样式,最初仍是scale(0),鼠标移入后它变大,移出缩回。
2.背景图片拉近效果同上。
总结:这里的css部分新掌握的内容为对于文字的展示部分,在设置超出部分以省略号的形式显示时,通常要设置三个属性:white-space:nowrap;
text-overflow:eclipse;over-flow:hideen;此外这三个属性,要直接对你文字所在的标签设置,尤其要注意的是 a标签文字设置样式时不继承父类!
四、轮播按钮部分
网页有一个特点,就是移入到每一块(除排行榜)都会有一左一右两个button滑出,移入又会滑走(隐藏到屏幕两侧)并且,不会随页面缩放位置发生变化。
css:这里就是简单的button即可,重点是它相对于定位的父类必须宽度是100%,这样才可以实现其页面缩放时位置总是从两侧滑出来,所以为他的父类设置width:100%,position:relative;再对它设置absolute
js:为每个模块,挂监听器,移入时,prebutton的left:-按钮宽度,nextbutton的right:-按钮宽度。移出时为0即可。
五、固定视窗
这里都是基本的样式,不在赘述。
总结:在实现的过程中,成功解决了一个知识盲点:就是在对内联元素设置margin:0 auto;居中时,光设置宽高是无效的,必须要将其设置为块元素,才生效!
六、页脚部分
这里没有什么难点,就是正常css+hover即可~
值得一提的是时间部分,利用js的Date()渲染页面,就可完成自动更新时间啦
搜索播放功能的实现
一搜索页面的ajax功能实现(调用API)
安装api:将api下载并解压到我们任意一个根磁盘下,然后打开cmd,进入该api文件夹目录下,运行node app.js (提前安装好node),这是我们已经实现了api接口的调用,接下来我们可以根据api文档,利用ajax获取数据,从而实现搜索,播放等功能。
1.搜索歌曲:
因为要频繁的运用ajax的get方法,所以先将其封装成一个get函数,接下来就比较方便封装使用其他功能的函数了,原生的ajax有规范的四步曲:
1.xhr=new XMLHttpRequest()
2.xhr.open(‘get’,url,true)
3.xhr.send()
4.xhr.onreadystatechange=function()
{//判断状态码,状态值}
所以封装的重点就是这个url的传入,我们要利用参数的形式将其传入。
代码如下:
var get=function(url,data,callback)
{
var xhr=new XMLHttpRequest()
//判断自身属性是否存在
var param='?'
for(var key in data)
{
if(data.hasOwnProperty(key))
{
param+=key+'='+data[key]+'&'
}
}
// 我们最终想要的格式http://localhost:3000/song/url?id=33894312
param=param.slice(0,param.length-1)
xhr.open('GET',url+param,true)
xhr.send()
xhr.onreadystatechange=function()
{
if(xhr.readyState==4&&xhr.status==200)
{ //callback回调函数存在的话 就把结果传给他
if(callback)
{
callback(JSON.parse(xhr.response))
}
}
}
}
1.值得注意的是在ajax中通常都使用异步处理,所以要用回调函数,来明确现在的状态,所以在封装ajax的方法时,参数大多都有callback函数。
2.xhr.response都是JSON格式,注意解析
至此get方法已经封装好,接下来实现将想要搜索的歌名打入搜索框,就可展示出来相关歌曲,歌手,专辑,时长这四个信息。
同样的,利用get方法进而封装出一个search方法,查看api文档可以看到search接口的格式/search?keywords= ‘xxx’ ,封装过程不再赘述,同上,直接上代码。
var search=function(keywords,callback)
{
get('http://localhost:3000/search',{keywords:keywords},function(res)
{
if(callback)
{
callback(res.result.songs)
}
})
}
接下来就可以封装一个searchRender()函数,在这里获取input.value,将其值作为search的参数,调用search(),即可获得其歌曲名,歌手信息。重点是渲染部分,因为渲染的数据较多,所以利用for循环+replace()方法。首先创建一个tpl=‘要渲染的部分的html,注意将要渲染的部分用特殊符号标识一下(也可以利用ES6的字符串模板)。此外还要注意的是,渲染的时候将音乐的播放地址、id(获取歌词)作为a标签的url参数,实现点击标签就跳转到相应播放页面的效果。
var renderSearch = function(key) {
search(key, function(final) {
var html = ''
var count = 0
//将id和名字放于query内 在服务器端获取
//而let由于是块作用域,所以如果在块作用域内定义的变量,比如说在for循环内,在其外面是不可被访问的,所以for循环推荐用let
for (let i = 0; i < 12; i++) { //解决问题关键在于let确定id值 不使用let的话 会使数据只有一条 闭包
var id = final[i].id
getUrl(id, function(res) {
let tpl = `
<a class='songsa' href="/getPlayer?id={$id}&url={$url}&name={$name}&artist={$artists}">
<li class="song_list_item" data-id="{$id}">
<span class="song">{$songname}</span><span class="artist">{$artist}</span>
<span class="album">{$album}</span>
<span class="time"></span>
</li>
</a>`
url = res.url
console.log(url)
html += tpl.replace("{$name}", final[i].name)
.replace("{$songname}", final[i].name)
.replace("{$artists}", final[i].artists[0].name)
.replace("{$artist}", final[i].artists[0].name)
.replace("{$album}", final[i].album.name)
.replace("{$id}", final[i].id)
.replace("{$url}", url)
console.log(html)
songList.innerHTML = html
})
}
addEverntListner()
})
}
(因为是对渲染后的标签每项都要挂在监听事件发起ajax请求,实现页面跳转,所以我封装了一个addeventlistner函数,在这里对li挂载事件监听)所以在页面渲染后还要调用此方法。
var addEverntListner = function() {
var songListItem = $('.song_list_item')
songListItem.on('click', function() {
getPlayer()
})
// songListItem.css('background-color', 'blue')
var songsa = $('.songsa')
// songsa.css('background-color', 'blue')
}
搜索一首喜欢的歌:
(由于样式设计我只获取前10个)
PS:官网样式,这里可以将li列表的偶数项背景色设为浅灰,奇数项设置为白色,这里可以利用:nth-child(odd){}、:nth-child(even){}
1.播放歌曲:
在上述基础上,我们要实现点击一首歌就相应跳转到播放器页面。
所以利用ejs进行服务器端渲染,通过query中的参数(id、播放地址)进行渲染,然后利用audio标签,传入query中的url参数,并将其样式设置为visibilty:hidden;隐藏起来。
app.get('/getPlayer', function(req, res) {
var id = req.query.id
var name = req.query.name
var url = req.query.url
var artist = req.query.artist
//从query中获取相关信息
console.log(id, name, url)
res.sendFile(path.resolve('./static/search.html'))
ejs.renderFile('./ejs-tpl/player.ejs', { artist: artist, id: id, name: name, url: url }, (err, html) => {
if (err) {
res.status(500).send(err)
console.log(err)
}
res.send(html)
})
})
然后就是歌词展示与高亮部分:
根据文档,利用get方法获取到歌词。定义一个解析歌词的函数,每条获取到的歌词都是时间:歌词,字符串格式的数据,对于每一条处理的歌词数据都首先处理时间,把时间数据>毫秒形式,获取到的歌词数据显示形式是一个字符串::,split切割->转为数组,然后转成小数,换算成毫秒形式->利用字符串拼接把相应的歌词区域利用这个数据渲染出来。
// 歌词格式:[00:10.254]他只是经过 你的 世界
// 需要将它解析为[{time:10.254s,content:'他只是经过 你的 世界'},{}]
//1.把字符串(歌词)通过split()方法->数组
//2.数组的每一项为一句歌词,把每一项变成对象格式
//3.time转化为数字格式
//获取歌词
var getDetail = function(id, callback) {
get('http://localhost:3000/song/detail', { ids: id }, function(e) {
if (callback) {
callback(e)
}
})
}
var getLrc = function(id, callback) {
get('http://localhost:3000/lyric', { id: id }, function(e) {
lrcString = e.lrc.lyric
if (callback) {
callback(lrcString) //回调函数里有获取到的lrc
}
})
}
//歌词解析函数(时间要进行处理)
var parseTime = function(time) {
//01:55.73 -> 分钟(转秒)01*60 : 秒55.73 -> 秒60+ 秒55.73->秒 115.73-> 毫秒1157300
var minutes = parseInt(time.split(':')[0])
var second = parseFloat(time.split(':')[1])
var sumSecond = ((minutes * 60 + second) * 1000)
return sumSecond
}
var LrcParse = function(lrcString) {
var lrcArr = [] //在这里面存储我们要的歌词
var lrcArrObj = []
lrcArr = lrcString.split('\n') //转化为数组
lrcArr.forEach(function(e) {
var line = e.split(']') //split切割成数组
// 因为要把time转数字格式 所以在这里就处理了
var time = parseTime(line[0].slice(1, line[0].length))
var content = line[1]
//因为是用\n分割的 所以最后一项不加入到数组中
//跳过最后一项
if (content != undefined && !isNaN(time)) {
lrcArrObj.push({ time: time, content: content }) //提取时间 歌词
}
})
console.log(lrcArrObj.length)
return lrcArrObj
// 切割的第二种方法,但是有可能时间有变化
// var time = lrcArr[i].slice(0, 11)
// var content = lrcArr[i].slice(11, lrcArr[i].length)
// lrcArr[i] = { time: time, content: content }
}
然后就是难点歌词高亮+滚动,在这里监听,timeupdate事件,事件中封装一个比较函数,在比较函数里的我是通过对比当前歌词所处时间与已播放时长相差是否小于300毫秒来确定其是否是高亮样式的,这里有一点,就是比较时利用做差比较,是要用现在播放的歌词时间-已经播放时间,如果反过来,之前已经播放过-当前播放的会是一个负数,也小于300就不对了,设置歌词是高亮样式。滚动效果的实现是通过改变歌词展示这个ul的marginTop每次调用这个函数就让它-=一个li的高度,并且对于歌词区域超出部分隐藏,这样就实现了歌词滚动效果
var compareLrc = function() {
// 获取所有歌词,对比时间确定哪个歌词正在播放
var lrcItem = $('.lrc-item')
// 对比时间确定那句歌词在播放
// 播放的歌词总是递增的,现在的播放的那句歌词时间-当前时间
// 才可以 ,反过来的话,之前的项-当前播放时间为负数 也小于了
if ((nowLrcObJArr[index].time - audio.currentTime * 1000) < 300) {
lrcItem[index].style.color = '#31c27c'
// 歌词滚动展示的时候 每次都让ul的margin-top -一个li的高度
// 这样就有像上滚动的效果
marginTop -= 50
lrcul.css('marginTop', marginTop)
if (index - 1 > -1) {
lrcItem[index - 1].style.color = ''
//
}
index++
console.log(index)
}
// 对比下一句歌词
}
若想自定义播放栏可以直接根据当前时长/歌曲总长,求出歌曲目前所处的时间占比。音量同理。
//<-------------------------------------------自定义播放器部分-------------------------------------------------->
var playButton = $('.play-button')
var pauseButton = $('.stop-button')
//播放按钮 :点击时 暂停按钮出现 自身隐藏 开始播放
playButton.on('click', function() {
pauseButton.show()
playButton.hide()
audio.play()
})
//暂停按钮:点击时 播放按钮出现 自身隐藏 暂停播放
pauseButton.on('click', function() {
pauseButton.hide()
playButton.show()
audio.pause()
})
// 时长部分处理
var currentTimeShow = $('.currentTime')
var sumTimeShow = $('.sumTime')
var duration = audio.duration //页面最初不知道音频是什么状态 所以为NaN
// 秒->分
var secondToMin = function(second) {
//获取分钟
var min = parseInt(second / 60)
min = min > 9 ? min : '0' + min
var s = parseInt(second % 60)
s = s > 9 ? s : '0' + s
var mins = min + ':' + s
return mins
}
//进度条时间展示部分
audio.addEventListener('timeupdate', function() {
currentTimeShow.html(secondToMin(audio.currentTime))
sumTimeShow.html(secondToMin(audio.duration))
percent(audio.duration, audio.currentTime)
})
音量控制:
var volumeLoad = $('.volume_load')
var volumePoint = $('.volume_point')
var volumeControl = $('.volume_control')
var volumeProgress = $('.volume-progress')
//jq实现元素拖拽
var offsetX = 0
var offsetY = 0
var position
var canMove = false
//思路是 想要移动的按钮 开启可移动状态,然后在你想要移动的范围内挂监听move的时间
//在父类移动范围里计算激动的位置 最后关闭移动状态
volumePoint.mousedown(function(e) {
offsetX = $(this).position().left //jQ中用position获取相对于父类的偏移量,原生是offsetLeft
canMove = true
})
var percentVolume
$(volumeControl).mousemove(function(e) {
if (canMove) {
var initLeft = $(this).offset().left //初始的位置
position = e.clientX - initLeft
// console.log(e.clientX - initLeft) //相对于页面的位置
if ((position) < 75 && (position) > 0) {
volumePoint.css({
left: position - 2 //相对于页面的位置-初始位置 为left移动距离
})
percentVolume = position / 80
volumeProgress.width(percentVolume * 80)
// console.log(position / 80)
if (percentVolume <= 0.125) {
console.log(1)
audio.volume = 0
}
if (percentVolume >= 0.925) { audio.volume = 1 } else { audio.volume = percentVolume }
}
}
})
$(document).mouseup(function() {
canMove = false
// $(document).off("mousemove");
})
// console.log(audio.volume)
//点击静音、恢复
var volumeIcon = $('.volume_icon')
var canChange = true
volumeIcon.on('click', function() {
console.log(percentVolume)
if (canChange) {
audio.volume = 0
canChange = false
} else {
audio.volume = percentVolume
canChange = true
}
})
//点击调整音量
volumeLoad.on('click', function(e) {
console.log(percentVolume)
var pos = $(this).position().left
console.log(pos)
volumeProgress.width(percentVolume * 80)
// audio.volume = percentVolume
volumePoint.css({
left: position - 2 //相对于页面的位置-初始位置 为left移动距离
})
})
还有背景图片我们可以直接利用接口数据,实现对应歌曲对应图片的效果
var playBc = $('.player-bc')
getDetail(id, (res) => {
var imgUrl = res.songs[0].al.picUrl
console.log(imgUrl)
playBc.css('background', 'url(' + imgUrl + ')')
})
效果展示: