Web 练习
准备工作
在写代码前,首先我们需要一个比较智能的代码编辑器。当然,系统自带的记事本也是代码编辑器,只是没那么“聪明”,不能够给你很多提示,相对来说比较麻烦。所以今天向大家推荐Visual Studio Code(VS Code),可以从这里下载。
另外,在投身于代码“创作”前,我们还是先明确一下最终要实现的目标比较好:

可以看到,在网页的居中位置有一个黑底白字写着New Button的按钮,以及一个矩形的框。当点击按钮后,框内就会生成一个红色的小方块,并以每秒一格的速率下落,一直到消失。
其实这就是在做俄罗斯方块的游戏,只不过我们没有做出俄罗斯方块的各种形状,没有做出控制方块做左移右移、顺时针转逆时针转的逻辑,没有做出方块落到最底下堆积和堆满一行消除的逻辑。虽然听起来什么都没做的样子,但是麻雀虽小,五脏俱全,实现这个幼年版俄罗斯方块就要用到我们在Notes里讲到的Web三件套了。接下来就让我们用它们一步一步实现这个简陋的"201方块"。
开始冻手
基础架构
首先,要明确的是,做这个网页我们总共需要三个文件,一个HTML文件,一个CSS文件以及一个JavaScript文件。当然像Notes里提到的,借助内部引入或者行内引入的方法,其实仅仅用一个HTML文件也够,但是为了让三件套的分工看起来更清楚,且便于日后的阅读与修改等等,我们还是建议使用三个分别的文件。
所以我们需要先找一个地方(最好是比较好找的地方)创建一个文件夹,名字随便取,我们这边命名为practice。当然也是一样的道理,不创建文件夹固然可以,但是为了看着清楚,我们最好还是用一个文件夹来存放构成一个网页的各个文件。
然后打开安装好的VS Code,点击菜单栏的File选择Open Folder,并在随后选择刚刚创建的文件夹。


创好文件夹后,我们继续创建之前说的三个文件。同样的,也是在菜单栏的File里,点击New File(或者直接ctrl+N)。

然后我们可以点击编辑器在第一行自动提示的Select a language来选择这个文件中代码的种类。


随后我们便可以ctrl+S保存这个文件了。

上面展示的是HTML文件的创建,但另外两个文件的创建也是同理。在都创建完毕后,我们应该有这三个文件:

到这里为止,组成网页的文件的基本架构就完成了。那么接下来就让我们一个文件一个文件地完成201方块。
HTML
就像Notes里说的一样,HTML指定了每个元素在网页中的位置这样最基础的网页结构,所以我们从HTML文件写起。
首先我们把一个HTML最基本的结构写出来,就像下面这样:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
</body>
</html>
我们知道,<head>里可以指定整个文档的公共资源,所以我们就在这边指定外部引入的CSS文件和JavaScript文件:
<head>
<link rel="stylesheet" type="text/css" href="201tetris.css">
<script type="text/javascript" src="201tetris.js"></script>
</head>
其中href和src后面跟的分别是CSS文件和JavaScript文件的路径,由于都是在同一个文件夹下,所以这边直接写文件名字就好了。
然后我们需要在<body>部分定义页面上的元素,也就是一个按钮和一个矩形框了:
<body>
<div id="buttonContainer">
<button id="startButton" onclick="play()">New Game</button>
</div>
<div id="gameBoard"></div>
</body>
这边出现了一个大家可能没见过的标签,也就是<div>,即HTML文档分区元素。在不使用CSS的情况下,它对页面的布局没有影响。通俗地来讲,它就是一个无形的容器,其中包含的就是<div>标签里的其他HTML元素。
可能大家会有些疑惑,既然它是一个无形的容器,那我们为什么要用它?确实在我们练习的例子中,直接声明一个按钮也能达成一样的效果,但是这就要讲到<div>的一大优势了:当代码量比较大,网页上元素很多的时候,我们往往需要分区处理它们,<div>和CSS结合运用,就可以通过画出边界或是调整背景色等,很好地对网页进行分区。
在上面的代码中,我们定义了两个分区,其中第一个id为buttonContainer的分区包含了id为startButton的按钮,而第二个id为gameBoard的分区则代表了矩形框所占的区域。而其中按钮的点击事件(就是点击后发生的事情)被我们指定为了play()。所以写完HTML文件后,接下来的目标也就明确了——我们需要在CSS文件中定义id为buttonContainer、startButton以及gameBoard的元素的特性,并在JavaScript文件中定义play()函数。
CSS
首先我们先写id为buttonContainer的分区的CSS特性,由于我们只用这个分区来包含按钮,所以我们只是希望通过它来确定这个包含按钮容器本身的宽度以及它与其他分区元素的边距,这样的功能可以通过下面的代码实现:
#buttonContainer {
width: 200px;
margin: 20px auto 20px auto;
}
其中width指定了这个元素的宽度为200px(也就是200个像素);而margin的四个值分别对应上边距、右边距、下边距和左边距(也就是顺时针转过来的四个边距)。这里我们指定上下边距都为20px,让这个容器不会和网页的最上面以及下面的矩形框贴在一起;左右边距为auto,这个auto的意思就是让元素占据所有的可用空间,所以我们如果单让左边距为auto的话,就会让容器右对齐,因为左边距占据了除了容器本身占用宽度外的所有宽度,同理如果只设置右边距为auto,容器就会左对齐,而当我们同时设置左右边距为auto的话,就会让左右边距相等,使得容器居中。不过这里需要注意的就是,如果不定义容器的宽度,那么它的宽度就会默认为是父元素的宽度,也就是整个页面的宽度,这时候它就会独占一行,而不是在页面上水平居中。
然后我们再来写id为startButton的按钮的CSS特性,也就是定义它的宽高、边框以及它为黑底白字的特性的代码:
#startButton {
width: 200px;
height: 40px;
background-color: black;
color: white;
border-radius: 7px;
border: 0px;
}
其中由于按钮的宽度为200px,包含它的容器的宽度也为200px,而容器在页面水平居中,那么按钮自然也在页面水平居中了。另外background-color和color分别定义了底色以及上面字的颜色。border-radius则指定了边框的圆角为7px,让边角不再是直角而是半径为7px的圆弧;border则指定了边框宽度为0px,也就是按钮无边框。
最后我们再来写id为gameBoard的分区的CSS特性。由于<div>本身是没有任何内容的,所以我们需要利用CSS在里面画出一个矩形框,实现代码如下:
#gameBoard {
width: 200px;
height: 400px;
margin: 0 auto;
border: 1px solid black;
overflow: hidden;
position:relative;
}
其中依旧是定义了分区元素的宽度和高度,并配合margin: 0 auto;使得元素在页面水平居中。然后border: 1px solid black则表示我们需要宽度为1px的实线(由solid定义)黑色边框。而后面的overflow: hidden;和position: relative;则和后面方块的运动有关系,我们到后面讲JavaScript的时候再解释。
那么自此,我们就完成了整个CSS文件。不过要注意的是,如果是自己第一遍写的话,肯定不能一遍就把所有的属性都定义完全、定义对,所以我们往往需要打开HTML的网页(不是在代码编辑器中打开,而是在浏览器中打开),写完一些代码就保存一下,然后刷新HTML的网页,来看一下实现效果如何。
JavaScript
在写完了HTML和CSS后,一个网页的静态部分就基本定义完全了,互动的部分就要靠JavaScript了。而我们需要在JavaScript里写的就是按钮被点击后的事件,也就是先前提到的play()。
和写任何别的代码一样,在真正动手之前,我们最好先考虑一下我们大致需要哪些功能,对应之后要写的什么函数。首先,我们需要一个函数来生成方块,我们定义为generatePiece();其次,在生成方块后,我们需要一个函数来让方块下落,我们定义为pieceMoveDown()。所以如果要写一个大致的框架,那么就是像下面这样的:
const BLOCKSIZE = 20;
function generatePiece() {
//生成方块
}
function pieceMoveDown() {
//让方块下落一格
}
function play() {
generatePiece();
setInterval(pieceMoveDown, 1000);
}
在最上面我们通过关键字const定义了一个常量BLOCKSIZE表示201方块的尺寸,方便下面的函数共同使用。然后我们通过关键字function分别定义函数generatePiece()、pieceMoveDown()和play(),并在play()中先调用generatePiece()生成方块,然后再用JavaScript自带的setInterval()方法,来让pieceMoveDown函数每隔1000毫秒被调用一次,也就是每隔一秒让方块下落一格。
写完基本的框架后,我们就可以着手于写generatePiece()和pieceMoveDown()了。这两个函数虽然功能不同,但是思想是共通的:都是通过控制网页上的HTML元素实现对网页行为(也就是用户在页面上实际看到的互动)的控制。接下来就让我们一个一个实现。
generatePiece()
首先是generatePiece(),就像上面说的,我们是通过控制网页上的HTML元素实现对网页行为的控制的,那么也就是说,这个函数的本质其实就是创建一个代表小方块的HTML元素,所以我们要这么写:
var newPiece = document.createElement('div');
newPiece.id = 'piece';
newPiece.style.width = BLOCKSIZE + 'px';
newPiece.style.height = BLOCKSIZE + 'px';
newPiece.style.background = 'red';
newPiece.style.position = 'absolute';
其中document.createElement()可以创建一个由标签名称指定的HTML元素,我们这边的标签为'div',所以就会在HTML中相应地生成一个<div>元素,用来代表小方块(注意JavaScript通过关键字var来声明变量)。然后我们再定义这个元素的一些属性(需要注意的是标签中的属性是当作字符串处理的,所以我们也要让赋的值为字符串的格式):通过newPiece.id = 'piece'(由于newPiece相当于一个标签,而id是标签的一种属性,所以可以通过.id来获取)来指定这个元素的id,方便我们之后在pieceMoveDown的函数里更明确地指出需要被控制下落的元素;通过.style.width、.style.height以及.style.background分别指定方块的宽度、高度以及背景色(BLOCKSIZE在代码最开始就被指定为常量,值为20);最后,指定该元素CSS属性position为'absolute',至于为什么这么设置这个属性我们先留一留,到下面再讲。
但是光指定这些<div>元素自身的属性还不够,由于小方块是在id为gameBoard的<div>元素中运动的,所以我们如果能够以gameBoard做为小方块运动的参考系的话,应该会让定义小方块的位置变得更加方便,而这就需要我们让gameBoard成为小方块的父节点了:
var gameBoard = document.getElementById('gameBoard');
gameBoard.appendChild(newPiece);
其中document.getElementById()可以让我们获取指定id的HTML元素,然后我们再用appendChild()方法让小方块成为gameBoard的子节点。但如果仅仅只是指定了父子关系还不够,我们还需要对父节点和子节点的CSS属性position指定正确,这就要提到我们之前没讲的问题了——为什么我们要把gameBoard的position指定为relative,以及为什么我们要把小方块的position指定为absolute。
我们不妨稍微扯远一点讲:首先CSS的position属性有5个比较常用的值, static、relative、fixed、absolute以及sticky。其中static为该属性的默认值,而它代表的意思自然便是元素的默认位置,也就是浏览器按照源代码的顺序,并以元素与元素不重叠为原则所计算出的位置,这通常被称为“正常的页面流”(normal flow)。而relative、absolute以及fixed可以放在一起对比着讲,因为它们都是相对于某个基点的定位,区别只是在于基点不同,而为了表明偏移的方向与距离,便有了top、bottom、left和right四个CSS的属性,分别代表相较基点上下左右偏移的距离。relative的基点是元素的默认位置(也就是当position为static时的位置);absolute的基点是该元素的上级元素,一般来说是父元素,但这有一个很重要的限制就是,父元素的position属性不能为static,否则基点就会变成整个网页的根元素<html>,也就是该元素会相对于整个网页的边界进行偏移;fixed的基点是浏览器窗口,这和根元素还不太一样,如果以根元素为定位基点,那么元素会随页面的滚动而滚动,但是以浏览器窗口为基点的话,元素的位置就不会随页面的滚动而滚动,就好像固定在网页上一样(大家可以回想一下经常在网页上出现的竖条形小广告),所以叫fixed。最后一个属性值为sticky,这个属性值比较特殊,大家可以把它理解为一个relative和fixed的分段函数,而这个分段函数的阈值由top、bottom、left和right的取值来定义,也就是说没超过阈值时,position的取值为relative,而在超过后,position就变成了fixed。这么说可能大家没有一个具象的概念,所以可以看下图position取sticky时的一个简单运用(注意观察写有导航的橙色元素和侧面的滚动条):

这里我们设定写有导航的<div>元素的position为sticky,并用top: 0px;来定义阈值,所以当它与浏览器窗口顶部的距离没有达到0px时一直是relative的定位,向下滚动网页时随着网页一起滚动,但当它与顶部的距离达到0px后就变为fixed的定位,和浏览器窗口保持相对静止,仿佛有粘性一样黏在了窗口上,sticky也由此得名。
当然,讲了那么多,大家可能不能全部理解或者全部记住,但是并不要紧,这只是为了让大家对这些属性有一个大致的认知,以后要是碰到类似的问题可以有一个大体的方向,至于具体的实现或是更多更深更妙的应用,还是要靠大家自己多上网查查,多了解了解。
回归到我们这堂课的练习上来,可能大家都已经被上面繁杂的各种取值扯晕了,那么我们就再看一遍问题。为什么我们要把gameBoard的position指定为relative,还有为什么我们要把小方块的position指定为absolute?相信很多人其实已经有了答案:因为我们想把小方块的定位基点设定为其父元素,也就是gameBoard,所以我们设定小方块的position取值为根据上级元素定位的absolute,而为了让该上级元素为父元素,而不是网页的根<html>,我们不能让其父元素,也就是gameBoard的position取值为static,所以我们将其设定为了relative。
而在定义完这些基础的属性之后,我们还需要真正地把小方块放到gameBoard的最上面:
newPiece.rowPos = 0;
newPiece.colPos = 4;
newPiece.style.top = newPiece.rowPos * BLOCKSIZE + 'px';
newPiece.style.left = newPiece.colPos * BLOCKSIZE + 'px';
其中的rowPos和colPos都是我们自己定义newPiece所拥有的属性,我们只是用两个整数来记录小方块当前所在的行数和列数,其实这对小方块在页面上的位置没有直接的影响,只是方便我们之后控制小方块的移动(因为直接看到行数列数比通过style.top和style.left得到距离的像素值要更加直观,且前者的整数相较后者的字符串更方便计算)。而下面我们则通过行数和列数乘以每一行的高度以及每一列的宽度得到小方块距离gameBoard上边界的距离以及左边界的距离。有些同学可能会奇怪为什么只定义了style.top和style.left,而没有定义style.bottom和style.right,这是因为style.top和style.bottom只要选其一就可以确定小方块在垂直方向的位置,同理style.left和style.right只要选其一就可以确定小方块在水平方向的位置了,所以说我们只需要定义四个距离中的两个就好了。同样需要注意的是,最后style.top和style.left的值都必须是字符串的形式。
那么至此,小方块的初始化就完成了。
pieceMoveDown()
接下来我们来写控制小方块下落一行的函数pieceMoveDown()。
其实有了上面的基础,pieceMoveDown()可以说是相当简单,只要先通过小方块的id得到它,再通过style.top更改小方块与gameBoard上边界的距离就可以了:
function pieceMoveDown() {
var piece = document.getElementById('piece');
piece.rowPos += 1;
piece.style.top = piece.rowPos * BLOCKSIZE + 'px';
}
play()
最后再说回我们的函数play():
function play() {
generatePiece();
setInterval(pieceMoveDown, 1000);
}
由于在这个函数里只有生成小方块和控制小方块下落的函数,并没有写小方块触底之后停止运动的函数,所以小方块其实会一直以每秒1行的速率下落,那我们如何实现当小方块超出gameBoard边界的时候让它不显示的呢?这就又要提到我们在CSS中还剩下的最后一个问题:为什么要在gameBoard的CSS特性里写overflow: hidden;这么一行。这是因为overflow: hidden;其中的一个功能就是隐藏溢出,也就是隐藏超出父元素边界的子元素,所以当小方块落到gameBoard的下边界再继续下落后,就不见了。其实方块本身并没有消失,只是我们看不到罢了。
当然overflow: hidden还有许多其他神奇的功效,比如消除浮动等等,我们在这也不赘述了。因为Web制作里千千万万的技巧是讲不完的,所以大家最重要的还是要学会面向谷歌编程(GoP, Google-oriented Programming),有不懂的多查查,查了也不要只看自己当下需要的内容,有时间就顺便多看看相关的内容,多了解一些其他内容。
希望今天的Web练习能够起到抛砖引玉的作用,让大家更快地入门Web三件套的使用,也对日后应该如何写代码有更深刻的理解。
最后,附上完整的代码(依次为HTML、CSS和JavaScript),供大家参考:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="tetris.css" media="screen">
<script type="text/javascript" src="./tetris.js"></script>
</head>
<body>
<div id="buttonContainer">
<button id="startButton" onclick="play()">New Game</button>
</div>
<div id="gameBoard"></div>
</body>
</html>
#buttonContainer {
width: 200px;
margin: 20px auto 20px auto;
}
#startButton {
width: 200px;
height: 40px;
background-color: black;
color: white;
border-radius: 7px;
border: 0px;
}
#gameBoard {
width: 200px;
height: 400px;
margin: 0 auto;
border: 1px solid black;
overflow: hidden;
position:relative;
}
const BLOCKSIZE = 20;
function generatePiece() {
var newPiece = document.createElement('div');
newPiece.id = 'piece';
newPiece.style.width = BLOCKSIZE + 'px';
newPiece.style.height = BLOCKSIZE + 'px';
newPiece.style.background = 'red';
newPiece.style.position = 'absolute';
var gameBoard = document.getElementById('gameBoard');
gameBoard.appendChild(newPiece);
newPiece.rowPos = 0;
newPiece.colPos = 4;
newPiece.style.top = newPiece.rowPos * BLOCKSIZE + 'px';
newPiece.style.left = newPiece.colPos * BLOCKSIZE + 'px';
}
function pieceMoveDown() {
var piece = document.getElementById('piece');
piece.rowPos += 1;
piece.style.top = piece.rowPos * BLOCKSIZE + 'px';
}
function play() {
generatePiece();
setInterval(pieceMoveDown, 1000);
}