通过在30分钟内构建一个记忆游戏来学习JS,CSS和HTML!
本教程介绍了一些基本的关于HTML5,CSS3和JavaScript概念。 我们将讨论数据属性,定位,透视,转换,flexbox,事件处理,超时和三元表达式。 读懂此文章不需要大家有许多编程方面的知识。 如果您已经知道HTML,CSS和JS的用途,那就绰绰有余了!
项目结构
让我们在终端中创建项目文件:
???? mkdir memory-game ???? cd memory-game ???? touch index.html styles.css scripts.js ???? mkdir img
HTML
连接css和js文件的初始页面模板。
<!-- index.html --><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Memory Game</title> <link rel="stylesheet" href="./styles.css"></head><body> <script src="./scripts.js"></script></body></html>
这个游戏有12张卡片。每个卡片由一个名为.memory-card的容器div组成,其中包含两个img元素。第一个代表卡片的front-face(意为正面),第二个代表卡片的back-face(意为背面)。
<div class="memory-card"> <img class="front-face" src="img/react.svg" alt="React"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"></div>
我们可以在Memory Game Repo下载该项目的资源文件。
这组卡片将被包装在section容器元素中。最终代码结果是这样的:
<!-- index.html --><section class="memory-game"> <div class="memory-card"> <img class="front-face" src="img/react.svg" alt="React"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/react.svg" alt="React"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/angular.svg" alt="Angular"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/angular.svg" alt="Angular"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/ember.svg" alt="Ember"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/ember.svg" alt="Ember"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/vue.svg" alt="Vue"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/vue.svg" alt="Vue"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/backbone.svg" alt="Backbone"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/backbone.svg" alt="Backbone"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/aurelia.svg" alt="Aurelia"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div> <div class="memory-card"> <img class="front-face" src="img/aurelia.svg" alt="Aurelia"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div></section>
CSS
我们将使用一个简单但非常有用的重置,适用于所有项目:
/* styles.css */* { padding: 0; margin: 0; box-sizing: border-box;}
box-sizing: border-box属性使元素充满整个边框,因此我们可以跳过数学计算。
通过设置display: flex到body和margin: auto到.memory-game容器,它将垂直地和水平地居中。
.memory-game也将是一个flex-container。默认情况下,里面的元素会缩小宽度来适应这个容器。通过将flex-wrap设置为wrap,flex-items会根据弹性元素的大小进行自适应。
/* styles.css */body { height: 100vh; display: flex; background: #060AB2;}.memory-game { width: 640px; height: 640px; margin: auto; display: flex; flex-wrap: wrap;}
每个卡片的width(意为宽度)和 height(意为高度)都是用calc() CSS函数计算的。我们将width设置为25%,height设置为33.333% ,并从margin(意为边距)中减去10px,来制作三行四张牌。
对于.memory-card子元素,我们添加position: relative,这样我们就可以相对它进行子元素的绝对定位。
把属性front-face和back-face的属性设置为position: absolute,这样就可以从原始位置移除元素,并使它们堆叠在一起。
/* styles.css */.memory-card { width: calc(25% - 10px); height: calc(33.333% - 10px); margin: 5px; position: relative; box-shadow: 1px 1px 1px rgba(0,0,0,.3);}.front-face,.back-face { width: 100%; height: 100%; padding: 20px; position: absolute; border-radius: 5px; background: #1C7CCC;}
这时页面模版看上去应该是这样:
让我们也添加一个点击效果。:active伪类将在每次点击元素时触发。它引发一个 0.2秒的过渡:
.memory-card { width: calc(25% - 10px); height: calc(33.333% - 10px); margin: 5px; position: relative; transform-style: preserve-3d; box-shadow: 1px 1px 0 rgba(0, 0, 0, .3);+ transform: scale(1);}+.memory-card:active {+ transform: scale(0.97);+ transition: transform .2s;+}
翻转卡片
要在单击时翻转卡片,我们需要向元素添加类别flip(意为翻转)。 为此,让我们使用document.querySelectorAll选择所有的memory-card元素。 然后使用forEach循环遍历它们并附加一个事件监听器。 每次卡片被点击时,都会触发flipCard(意为翻转卡片)功能。 this变量表示被单击的卡片。 该函数访问元素的classList并切换flip类:
// scripts.jsconst cards = document.querySelectorAll('.memory-card');function flipCard() { this.classList.toggle('flip');}cards.forEach(card => card.addEventListener('click', flipCard));
在CSS里flip类把卡片旋转了180度:
.memory-card.flip { transform: rotateY(180deg);}
为了产生3D翻转效果,我们将把perspective属性添加到.memory-game中。这个属性用来设置对象与用户在 z轴上的距离。数值越低,透视效果越大。为了达到最佳的效果,让我们设置为1000px:
.memory-game { width: 640px; height: 640px; margin: auto; display: flex; flex-wrap: wrap;+ perspective: 1000px;}
对于.memory-card元素,我们添加transform-style: preserve-3d属性,这样就把卡片置于在父节点中创建的3D空间中,而不是将其平铺在z = 0平面上(transform-style)。
.memory-card { width: calc(25% - 10px); height: calc(33.333% - 10px); margin: 5px; position: relative; box-shadow: 1px 1px 1px rgba(0,0,0,.3); transform: scale(1);+ transform-style: preserve-3d;}
现在,我们需要把transition属性的值设置为transform就可以生成动态效果了:
.memory-card { width: calc(25% - 10px); height: calc(33.333% - 10px); margin: 5px; position: relative; box-shadow: 1px 1px 1px rgba(0,0,0,.3); transform: scale(1); transform-style: preserve-3d;+ transition: transform .5s;}
所以,我们使得卡片可以3D翻转了,耶! 但为什么卡片的另一面不出现呢? 现在,.front-face和.back-face都堆叠在一起,因为它们被绝对定位了。 每个元素都有一个back face(意为背面),这是它front face(意为正面)的镜像。 属性backface-visibility默认为visible(意为可见的),因此当我们翻转卡片时,我们得到的是背面的JS徽章。
为了显示它背面的图像,让我们把backface-visibility: hidden应用到.front-face和.back-face。
.front-face,.back-face { width: 100%; height: 100%; padding: 20px; position: absolute; border-radius: 5px; background: #1C7CCC;+ backface-visibility: hidden;}
如果我们刷新页面并翻转一张卡片,它就消失了!
因为我们把两个图像都隐藏在了背面,所以另一面什么也没有。接下来我们需要把.front-face旋转180度:
.front-face { transform: rotateY(180deg);}
现在,我们有了想要的翻转效果!
匹配卡片
现在我们已经完成翻转卡片的功能之后,接下来我们来处理匹配的逻辑。
当我们点击第一张牌时,它需要等待另一张牌被翻转。变量hasFlippedCard和flippedCard将管理翻转状态。如果没有翻转的卡片,hasFlippedCard被设置为true, flippedCard被设置为已点击的卡片。让我们切换toggle方法来add(意为添加):
const cards = document.querySelectorAll('.memory-card');+ let hasFlippedCard = false;+ let firstCard, secondCard; function flipCard() {- this.classList.toggle('flip');+ this.classList.add('flip');+ if (!hasFlippedCard) {+ hasFlippedCard = true;+ firstCard = this;+ } }cards.forEach(card => card.addEventListener('click', flipCard));
现在,当用户点击第二张牌时,我们将进入else块。我们会检查一下它们是否匹配。为了做到这一点,我们需要做到能够识别每一张卡片。
每当我们想向HTML元素添加额外的信息时,我们就可以使用数据属性。通过使用以下语法:data-,其中,可以是任何单词,该属性将插入元素的dataset属性中。所以,让我们为每张卡片添加一个data-framework:
<section class="memory-game">+ <div class="memory-card" data-framework="react"> <img class="front-face" src="img/react.svg" alt="React"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="react"> <img class="front-face" src="img/react.svg" alt="React"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="angular"> <img class="front-face" src="img/angular.svg" alt="Angular"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="angular"> <img class="front-face" src="img/angular.svg" alt="Angular"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="ember"> <img class="front-face" src="img/ember.svg" alt="Ember"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="ember"> <img class="front-face" src="img/ember.svg" alt="Ember"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="vue"> <img class="front-face" src="img/vue.svg" alt="Vue"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="vue"> <img class="front-face" src="img/vue.svg" alt="Vue"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="backbone"> <img class="front-face" src="img/backbone.svg" alt="Backbone"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="backbone"> <img class="front-face" src="img/backbone.svg" alt="Backbone"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="aurelia"> <img class="front-face" src="img/aurelia.svg" alt="Aurelia"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div>+ <div class="memory-card" data-framework="aurelia"> <img class="front-face" src="img/aurelia.svg" alt="Aurelia"> <img class="back-face" src="img/js-badge.svg" alt="Memory Card"> </div></section>
所以现在我们可以通过访问两个卡片数据集来检查匹配。 让我们将匹配逻辑提取到它自己的方法checkForMatch(),并将hasFlippedCard设置为false。 如果匹配,则调用disableCards()并分离两个卡片上的事件侦听器,以防止再一次翻转。 否则,unflipCards()会将两张卡都恢复成超过1500毫秒的超时,从而删除.flip类:
把全部代码组合在一起:
const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false; let firstCard, secondCard; function flipCard() { this.classList.add('flip'); if (!hasFlippedCard) { hasFlippedCard = true; firstCard = this;+ return;+ }++ secondCard = this;+ hasFlippedCard = false;++ checkForMatch();+ }++ function checkForMatch() {+ if (firstCard.dataset.framework === secondCard.dataset.framework) {+ disableCards();+ return;+ }++ unflipCards();+ }++ function disableCards() {+ firstCard.removeEventListener('click', flipCard);+ secondCard.removeEventListener('click', flipCard);+ }++ function unflipCards() {+ setTimeout(() => {+ firstCard.classList.remove('flip');+ secondCard.classList.remove('flip');+ }, 1500);+ } cards.forEach(card => card.addEventListener('click', flipCard));
编写匹配条件的更简练的方法是使用三元运算符。 它由三个块组成。 第一个块是要判断的条件。 如果条件符合就执行第二个块,否则执行的块是第三个:
- if (firstCard.dataset.name === secondCard.dataset.name) {- disableCards();- return;- }-- unflipCards();+ let isMatch = firstCard.dataset.name === secondCard.dataset.name;+ isMatch ? disableCards() : unflipCards();
锁定
现在我们已经完成了匹配逻辑,接着为了避免同时转动两组卡片,我们还需要锁定它们,否则翻转将会失败。
我们先声明一个lockBoard变量。 当玩家点击第二张卡片时,lockBoard将设置为true,条件 if (lockBoard) return;在卡片被隐藏或匹配之前会阻止其他卡片翻转:
const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false;+ let lockBoard = false; let firstCard, secondCard; function flipCard() {+ if (lockBoard) return; this.classList.add('flip'); if (!hasFlippedCard) { hasFlippedCard = true; firstCard = this; return; } secondCard = this; hasFlippedCard = false; checkForMatch(); } function checkForMatch() { let isMatch = firstCard.dataset.name === secondCard.dataset.name; isMatch ? disableCards() : unflipCards(); } function disableCards() { firstCard.removeEventListener('click', flipCard); secondCard.removeEventListener('click', flipCard); } function unflipCards() {+ lockBoard = true; setTimeout(() => { firstCard.classList.remove('flip'); secondCard.classList.remove('flip');+ lockBoard = false; }, 1500); } cards.forEach(card => card.addEventListener('click', flipCard));
点击同一卡片
可能会出现玩家在同一张卡上点击两次的情况。 如果匹配条件判断为true,从该卡片上删除事件侦听器。
为了防止这种情况,需要检查当前点击的卡片是否等于firstCard,如果是肯定的则返回。
if (this === firstCard) return;
变量 firstCard和 secondCard需要在每一轮之后被重置,所以让我们将它提取到一个新方法 resetBoard()中, 再其中写上 hasFlippedCard = false;和 lockBoard = false。ES6的解构赋值功能 [var1, var2] = ['value1', 'value2']允许我们把代码写得超短:
function resetBoard() { [hasFlippedCard, lockBoard] = [false, false]; [firstCard, secondCard] = [null, null];}
接着我们调用新方法disableCards()和unflipCards():
const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false; let lockBoard = false; let firstCard, secondCard; function flipCard() { if (lockBoard) return;+ if (this === firstCard) return; this.classList.add('flip'); if (!hasFlippedCard) { hasFlippedCard = true; firstCard = this; return; } secondCard = this;- hasFlippedCard = false; checkForMatch(); } function checkForMatch() { let isMatch = firstCard.dataset.name === secondCard.dataset.name; isMatch ? disableCards() : unflipCards(); } function disableCards() { firstCard.removeEventListener('click', flipCard); secondCard.removeEventListener('click', flipCard);+ resetBoard(); } function unflipCards() { lockBoard = true; setTimeout(() => { firstCard.classList.remove('flip'); secondCard.classList.remove('flip');- lockBoard = false;+ resetBoard(); }, 1500); }+ function resetBoard() {+ [hasFlippedCard, lockBoard] = [false, false];+ [firstCard, secondCard] = [null, null];+ } cards.forEach(card => card.addEventListener('click', flipCard));
洗牌
我们的游戏现在看起来相当不错,但是如果不能洗牌就失去了乐趣,所以我们现在来处理这个功能。
当 display: flex在容器上被声明时,flex-items会按照组和源的顺序进行排序。 每个组由order属性定义,该属性包含正整数或负整数。 默认情况下,每个flex-item都将其order属性设置为0,这意味着它们都属于同一个组,并将按源的顺序排列。 如果有多个组,则首先按组升序顺序排列。
游戏中有12张牌,因此我们将迭代它们,生成0到12之间的随机数并将其分配给flex-item order属性:
function shuffle() { cards.forEach(card => { let ramdomPos = Math.floor(Math.random() * 12); card.style.order = ramdomPos; });}view raw
为了调用shuffle函数,让它成为一个立即调用函数表达式(IIFE),这意味着它将在声明后立即执行。 脚本应如下所示:
const cards = document.querySelectorAll('.memory-card'); let hasFlippedCard = false; let lockBoard = false; let firstCard, secondCard; function flipCard() { if (lockBoard) return; if (this === firstCard) return; this.classList.add('flip'); if (!hasFlippedCard) { hasFlippedCard = true; firstCard = this; return; } secondCard = this; lockBoard = true; checkForMatch(); } function checkForMatch() { let isMatch = firstCard.dataset.name === secondCard.dataset.name; isMatch ? disableCards() : unflipCards(); } function disableCards() { firstCard.removeEventListener('click', flipCard); secondCard.removeEventListener('click', flipCard); resetBoard(); } function unflipCards() { setTimeout(() => { firstCard.classList.remove('flip'); secondCard.classList.remove('flip'); resetBoard(); }, 1500); } function resetBoard() { [hasFlippedCard, lockBoard] = [false, false]; [firstCard, secondCard] = [null, null]; }+ (function shuffle() {+ cards.forEach(card => {+ let ramdomPos = Math.floor(Math.random() * 12);+ card.style.order = ramdomPos;+ });+ })(); cards.forEach(card => card.addEventListener('click', flipCard));
看之后
点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓-_-)
关注公众号「新前端社区」,享受文章首发体验!
每周重点攻克一个前端技术难点。