- 本人小白,练手代码,如有不足,请见谅,也欢迎评论交流
先看效果图

网站预览或源码下载
方式一:http://kevin5979.3vfree.net/share/javaScript/piano/index.html
方式二: http://kevin5979.3vfree.net
- 账号: admin
- 密码:123456
Ok,接下来详细讲解我书写代码的过程
- 我把我认为重要的知识点都写到每个步骤的前面
先构建思路
第一步:建立工程目录,编写html文件和css文件基本布局,完成基本的样式
第二步:获取所有需要的DOM元素对象,设计数据格式,并思考如何存储曲目的数据
第三步:实现键盘按钮事件,onkeydown
,拿到每个琴键的key值
第四步:实现鼠标的点击事件,onclick
,也是拿到每个DOM元素的对应key
第五步:实现钢琴点击/按键的动态效果
第六步:实现功能按钮的点击事件onclick
第七步:处理用户交互信息
第八步:列表渲染
第九步:接入声音类,将key转化成对应的琴音
第十步:保存的曲目播放
第十一步:优化代码,解决bug
第一步 编写html文件和css文件基本布局,完成基本的样式
这里就不用细讲了,直接上代码
目录组织
-
flex
布局 -
CSS3 animation
动画 -
iconfont
使用

HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>piano</title>
<link rel="stylesheet" href="./css/index.css">
</head>
<body>
<div class="piano-box">
<ul class="key-list">
<li class="item" data-key="q"><span class="bottom">1</span></li>
<li class="item" data-key="w"><span class="bottom">2</span></li>
<li class="item" data-key="e"><span class="bottom">3</span></li>
<li class="item" data-key="r"><span class="bottom">4</span></li>
<li class="item" data-key="t"><span class="bottom">5</span></li>
<li class="item" data-key="y"><span class="bottom">6</span></li>
<li class="item" data-key="u"><span class="bottom">7</span></li>
<li class="item" data-key="a"><span>1</span></li>
<li class="item" data-key="s"><span>2</span></li>
<li class="item" data-key="d"><span>3</span></li>
<li class="item" data-key="f"><span>4</span></li>
<li class="item" data-key="g"><span>5</span></li>
<li class="item" data-key="h"><span>6</span></li>
<li class="item" data-key="j"><span>7</span></li>
<li class="item" data-key="z"><span class="top">1</span></li>
<li class="item" data-key="x"><span class="top">2</span></li>
<li class="item" data-key="c"><span class="top">3</span></li>
<li class="item" data-key="v"><span class="top">4</span></li>
<li class="item" data-key="b"><span class="top">5</span></li>
<li class="item" data-key="n"><span class="top">6</span></li>
<li class="item" data-key="m"><span class="top">7</span></li>
</ul>
<ul class="btn-list">
<li><button class="record">录制</button></li>
<li><button class="end">结束录制</button></li>
</ul>
<h4>曲目单</h4>
<ul class="music-list">
</ul>
</div>
<script src="./js/index.js" type="module"></script>
</body>
</html>
CSS
@import "//at.alicdn.com/t/font_1724264_3u1pq0ga9u6.css";
* {
margin: 0;
padding: 0;
}
body,
html {
width: 100%;
height: 100%;
overflow: hidden;
background-color: rgba(82, 82, 82, 0.2);
}
.key-list {
list-style: none;
margin: 50px auto 30px auto;
width: 92%;
display: flex;
justify-self: start;
cursor: pointer;
}
.item {
width: 4.3%;
height: 250px;
background: #fff;
text-align: center;
margin-right: 3px;
user-select: none;
position: relative;
}
.item span {
position: absolute;
left: 50%;
bottom: 15px;
transform: translateX(-50%);
display: block;
}
.item .bottom::after {
content: ".";
font-size: 20px;
font-weight: bold;
display: block;
position: absolute;
top: 3px;
left: 50%;
transform: translateX(-50%);
}
.item .top::before {
content: ".";
font-size: 20px;
font-weight: bold;
display: block;
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
}
.active {
animation: press 0.5s;
}
@keyframes press {
0% {
transform: scale(1);
box-shadow: none;
}
50% {
transform: scale(1.05);
box-shadow: 2px 2px 2px 2px rgba(0, 0, 0, 0.2);
}
100% {
transform: scale(1);
box-shadow: none;
}
}
.btn-list {
width: 90%;
margin: 0 auto;
list-style: none;
display: flex;
justify-content: start;
}
button {
margin: 0 20px;
width: 70px;
height: 70px;
background-color: aqua;
cursor: pointer;
}
h4 {
margin-left: 6%;
margin-top: 20px;
}
.music-list {
width: 30%;
height: 200px;
margin-left: 6%;
margin-top: 10px;
list-style: none;
background-color: #fff;
overflow: scroll;
}
.music-list li {
padding: 5px 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
user-select: none;
}
.music-list li:nth-child(2n) {
background: rgb(216, 216, 216);
}
.icon-bofang {
color: #333;
font-size: 20px;
cursor: pointer;
}
.music-list .name {
color: #333;
margin-right: 20px;
}
.music-list .time {
color: #999;
font-size: 14px;
}
注意点
- 由于js代码中需要用到模块化,所以在
html
书写script
标签时需要添加type="module"
- 由于
css
中导入了阿里图标库iconfont
,所以必须联网访问,而且需要在编译器中打开代码,直接打开会报错
第二步:获取所有需要的DOM元素对象,设计数据格式,并思考如何存储曲目的数据

- 获取DOM元素
window.onload = function () {
const keysUl = document.querySelector(".key-list")
const keysList = keysUl.querySelectorAll("li")
const btnList = document.querySelector(".btn-list")
const record = btnList.querySelector(".record")
const end = btnList.querySelector(".end")
let musicList = document.querySelector('.music-list')
}
- 设计数据格式
由于我们需要做的功能是存储钢琴曲的琴键的按键信息,可以使用数组songs: Array
这时又想到需要处理每个按键之间的时间距离,可以在使用一个数组stamps: Array
我们需要一个录制开始时间startStamps: Array
,录制结束时间endStamps: Array
来作为参照物,才可计算每个按键的时间差,并计算出的每个曲目的总时间songTime: Array
,也需要记录
如果我们需要保存曲目的信息,就必须给保存的数据命名,这时需要一个命名数组names: Array
这时我们想到需要定义的数据太多了,存储到localStorage
不方便,这时定义一个对象resources: Object
let resources = {
songs: [],
stamps: [],
startStamps: [],
endStamps: [],
names: [],
songTime: []
}
// 定义其他需要的变量,后续随着代码变多,这里的变量可增加
// 定义所有的key值
const keys = "qwertyuasdfghjzxcvbnm"
- 思考如何存储曲目的数据
想到每次用户打开网页能得到前面的数据,避免每次关闭网页,数据会消失,这里使用localStorage
解决,但是不能直接存储对象类型,这时,我们使用了JSON.stringify(obj)
和JSON.parse(str)
来对我们存储的数据进行对象和字符串之间的转换
第三步:实现键盘按钮事件,onkeydown
,拿到每个琴键的key值
注册全局事件
window.onkeydown = function (e) {
console.log(e.key) // e.key : a,b,c ...
showEffect(e.key) // showEffect为控制动画方法,后面有写
}
第四步:实现鼠标的点击事件,onclick
,也是拿到每个DOM元素的对应key
事件代理
-
data-xxx属性
的使用
keysUl.onclick = function (e) {
console.log(e.target.dataset.key) // a,b,c ...
key && onViews.showEffect(key) // showEffect为控制动画方法,后面有写
}
- 这里使用了事件代理,给父元素添加事件,通过data-xxx属性判断是点击了那个子元素,这样可以避免给所有子元素都添加事件,减少了浏览器的压力,不会产生太大花销,优化性能
第五步:实现钢琴点击/按键的动态效果
js控制类名来改变样式
/**
* 控制钢琴动画效果
* @param {*} key 按钮对应的key
*/
function showEffect (key) {
const index = keys.indexOf(key) // 得到下标
if (index !== -1) {
// 添加类名,用于动画展示
keysList[index].classList.add("active")
// 300ms后移除类
setTimeout(() => {
keysList[index].classList.remove('active')
}, 300)
}
}
第六步:实现功能按钮的点击事件 onclick
代码触发点击事件
切换状态变量管理
let isStart = false
let isEnd = true
let startstamp = null // 开始录制时间戳
let endstamp = null // 结束录制事件戳
let tempSong = [] // 记录当前录制曲目的按键信息
let tempStamp = [] // 记录当前录制每个按键之间的时间差
// 开始录制
record.onclick = function () {
if (isEnd) {
startstamp = new Date().getTime()
tempSong = []
tempStamp = []
isStart = true
isEnd = false
} else {
const isOver = confirm("当前正在录制,是否结束录制?")
isOver && end.onclick()
}
}
// 结束录制
end.onclick = function () {
if (isStart) {
isStart = false
isEnd = true
endstamp = new Date().getTime()
const isSave = confirm("是否保存曲目?")
if (isSave) {
let name = prompt("输入曲目名")
if (!resources.names.includes(name) && name !== null) {
// 到这里说明用户完成了保存操作,我们需要将数据整理并保存到本地或全局变量中
resources.endStamps.push(endstamp)
resources.startStamps.push(startstamp)
resources.songs.push(tempSong)
resources.stamps.push(tempStamp)
resources.names.push(name)
resources.songTime.push(endstamp - startstamp)
localStorage.setItem("resources", JSON.stringify(resources))
renderList({ name, time: endstamp - startstamp })
} else {
alert("保存失败,输入为空或者该名字已存在")
}
}
} else {
alert("未开始录制")
}
}
- 这里需要控制录制状态和结束状态两个状态的转换,这里使用两个变量来控制(
isStart
和isEnd
),需要获取当前时间戳,定义变量startstamp
,既然开始录制,则需要存储按钮信息tempSong: Array
和每个按键的时间差tempStamp:Array
- 如果当前已经是录制状态,需要提示用户
为避免文章过长,本章先讲解第1~6步,后续会继续写完这个小功能,欢迎点赞,评论

网友评论