跳到主要内容
前端实战:使用 HTML、CSS 和 JavaScript 实现网页井字棋 | 极客日志
HTML / CSS 大前端 算法
前端实战:使用 HTML、CSS 和 JavaScript 实现网页井字棋 综述由AI生成 介绍如何使用 HTML、CSS 和 JavaScript 构建网页版井字棋游戏。内容涵盖 HTML 结构搭建、CSS 布局美化(Flexbox 与 Grid)、以及 JavaScript 交互逻辑(回合切换、胜负判断)。文章提供了详细步骤解析及完整源代码,适合前端初学者练习 DOM 操作与事件处理。
时间旅人 发布于 2026/4/6 更新于 2026/5/22 19 浏览网页井字棋实现教程
本文介绍如何使用 HTML、CSS 和 JavaScript 构建网页版井字棋游戏。内容涵盖 HTML 结构搭建、CSS 布局美化(Flexbox 与 Grid)、以及 JavaScript 交互逻辑(回合切换、胜负判断)。文章提供了详细步骤解析及完整源代码,适合前端初学者练习 DOM 操作与事件处理。
前置知识
在开始之前,请确保掌握以下基础知识:
HTML :基础标签使用,语义化结构。
CSS :通用样式重置 (* { margin: 0; padding: 0; box-sizing: inherit; }),Flexbox 布局 (display: flex, justify-content, align-items),CSS Grid 布局 (grid-template-columns, grid-template-rows, grid-gap),背景和图像处理 (background-size, background-image),伪元素 (::before),伪类 :hover,动画与过渡效果 (transition, transform),使用 CSS 类控制显示与隐藏。
JavaScript :DOM 操作 (document.getElementById(), document.querySelector()),类操作 (element.classList.add(), element.classList.remove()),事件监听与处理 (addEventListener(), removeEventListener()),回调函数,条件判断与数组方法 (Array.prototype.some() 和 Array.prototype.every())。
1. HTML 骨架
首先搭建 HTML 骨架。由于结构相对简单,代码如下:
<!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" >
<meta http-equiv ="X-UA-Compatible" content ="IE=edge" >
<meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
< > 网页井字棋
's turn
play again
title
</title >
<link rel ="stylesheet" href ="./index.css" >
<script defer src ="./index.js" >
</script >
</head >
<body >
<div class ="wrapper" >
<div id ="currentStatus" class ="current-status" >
<img id ="currentBeastImg" src ="./1.gif" alt ="unicorn" >
<p >
</p >
</div >
<div id ="board" class ="board" >
<div data-cell >
</div >
<div data-cell >
</div >
<div data-cell >
</div >
<div data-cell >
</div >
<div data-cell >
</div >
<div data-cell >
</div >
<div data-cell >
</div >
<div data-cell >
</div >
<div data-cell >
</div >
</div >
<div id ="gameEndOverlay" class ="game-end-overlay" >
<div class ="winning-message" data-winning-message >
<p >
</p >
</div >
<div class ="btn-container" >
<button id ="resetButton" >
</button >
</div >
</div >
</div >
</body >
</html >
初始状态下,部分容器因无内容高度可能为 0,后续将通过 CSS 进行修饰。
2. CSS 装饰
1. 引入字体和全局样式
@import url("https://fonts.googleapis.com/css2?family=Bungee+Inline&display=swap" );
* {
padding : 0 ;
margin : 0 ;
box-sizing : inherit;
}
2. 设置 body 样式 body {
margin : 0 ;
padding : 0 ;
display : flex;
justify-content : center;
align-items : center;
height : 100vh ;
text-align : center;
font-family : "Bungee Inline" , cursive;
color : #f5f5f5 ;
overflow : hidden;
background-image : linear-gradient (to top, #a8edea 0% , #ffc7d9 100% );
}
使用 Flexbox 居中内容,设置视口高度和渐变背景。
3. 设置 .wrapper 样式 .wrapper {
background-color : #55acee53 ;
padding : 50px ;
}
4. 设置 .current-status 和其中的元素样式 .current-status {
display : flex;
justify-content : center;
align-items : center;
margin-bottom : 25px ;
}
.current-status p {
margin : 0 5px 0 0 ;
font-size : 24px ;
}
.current-status img {
width : auto;
height : 32px ;
}
5. 设置 board 和 .cell 样式 .board {
display : grid;
grid-template-columns : repeat (3 , minmax (90px , 1 fr));
grid-template-rows : repeat (3 , minmax (90px , 1 fr));
grid-gap : 12px ;
width : 100% ;
height : 100% ;
max-width : 495px ;
margin : 0 auto 15px ;
}
.cell {
cursor : pointer;
position : relative;
background-color : #f5f5f5 ;
width : 90px ;
height : 90px ;
opacity : 0.5 ;
transition : opacity 0.2s ease-in-out;
}
.cell :hover {
opacity : 1 ;
}
使用 Grid 布局创建 3x3 棋盘,设置单元格大小和悬停透明度效果。
6. 鼠标悬浮时的图片效果
.board .unicorn .cell :not (.dragon ):not (.unicorn ):hover ::before ,
.board .dragon .cell :not (.dragon ):not (.unicorn ):hover ::before {
content : "" ;
width : 70% ;
height : 70% ;
display : block;
position : absolute;
background-repeat : no-repeat;
top : 50% ;
left : 50% ;
transform : translate3d (-50% , -50% , 0 );
background-size : contain;
opacity : 50% ;
}
.board .unicorn .cell :not (.dragon ):hover ::before {
background-image : url ("./1.gif" );
}
.board .dragon .cell :not (.unicorn ):hover ::before {
background-image : url ("./2.gif" );
}
7. 设置 game-end-overlay 样式 .game-end-overlay {
display : none;
position : fixed;
top : 0 ;
left : 0 ;
right : 0 ;
bottom : 0 ;
background-color : #0d1021 ;
}
.game-end-overlay .show {
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
}
8. 设置 .winning-message 样式 .winning-message {
margin : -50px 0 20px ;
}
.winning-message img {
width : 100px ;
}
.winning-message p {
font-size : 48px ;
margin : 0 ;
}
9. 重启按钮样式 .reset-button {
color : #f5f5f5 ;
font-family : "Bungee Inline" , cursive;
font-size : 30px ;
white-space : nowrap;
border : none;
padding : 10px 20px ;
background-color : #a186be ;
box-shadow : 5px 5px 0 #55acee ;
cursor : pointer;
transition : transform 0.1s ease-in-out;
position : relative;
}
.reset-button :hover {
transform : scale (1.2 );
}
.reset-button :active {
top : 6px ;
left : 6px ;
box-shadow : none;
background-color : #9475b5 ;
}
3. JavaScript 交互
1. 获取页面元素 const board = document .getElementById ('board' );
const cells = document .querySelectorAll ('[data-cell]' );
const currentStatus = document .getElementById ('currentStatus' );
const resetButton = document .getElementById ('resetButton' );
const gameEndOverlay = document .getElementById ('gameEndOverlay' );
const currentBeastStatusImg = document .getElementById ('currentBeastImg' );
const winningMessage = document .querySelector ('[data-winning-message]' );
const winningMessageText = document .querySelector ('[data-winning-message] p' );
const winningMessageImg = document .createElement ('img' );
2. 初始化游戏状态 let gameIsLive = true ;
let unicornTurn = true ;
let winner = null ;
3. 所有获胜组合 const winningCombinations = [
[0 , 1 , 2 ], [3 , 4 , 5 ], [6 , 7 , 8 ],
[0 , 3 , 6 ], [1 , 4 , 7 ], [2 , 5 , 8 ],
[0 , 4 , 8 ], [2 , 4 , 6 ]
];
4. 设置鼠标悬停时的样式 const setBoardHoverClass = ( ) => {
board.classList .remove ('unicorn' );
board.classList .remove ('dragon' );
if (unicornTurn) {
board.classList .add ('unicorn' );
} else {
board.classList .add ('dragon' );
}
}
5. 在格子上放置图片 const placeBeastImg = (cell, currentBeast ) => {
cell.classList .add (currentBeast);
}
6. 切换回合 const swapTurns = ( ) => {
unicornTurn = !unicornTurn;
}
7. 更新当前状态 const updateCurrentStatus = ( ) => {
if (unicornTurn) {
currentBeastStatusImg.src = './1.gif' ;
currentBeastStatusImg.alt = 'unicorn' ;
} else {
currentBeastStatusImg.src = './2.gif' ;
currentBeastStatusImg.alt = 'dragon' ;
}
}
8. 检查是否获胜 const checkWin = (currentBeast ) => {
return winningCombinations.some (combination => {
return combination.every (i => {
return cells[i].classList .contains (currentBeast);
});
});
}
9. 判断是否平局 const isDraw = ( ) => {
return [...cells].every (cell => {
return cell.classList .contains ('unicorn' ) || cell.classList .contains ('dragon' );
});
}
10. 开始游戏 const startGame = ( ) => {
cells.forEach (cell => {
winningMessageImg.remove ();
cell.classList .remove ('unicorn' , 'dragon' );
cell.removeEventListener ('click' , handleCellClick);
cell.addEventListener ('click' , handleCellClick, { once : true });
});
setBoardHoverClass ();
gameEndOverlay.classList .remove ('show' );
}
11. 结束游戏 const endGame = (draw ) => {
if (draw) {
winningMessageText.innerText = `draw!` ;
} else {
winningMessageImg.src = unicornTurn ? './1.gif' : './2.gif' ;
winningMessageImg.alt = unicornTurn ? 'unicorn' : 'dragon' ;
winningMessage.insertBefore (winningMessageImg, winningMessageText);
winningMessageText.innerText = `wins!!!` ;
}
gameEndOverlay.classList .add ('show' );
}
12. 处理格子点击事件 const handleCellClick = (e ) => {
const cell = e.target ;
const currentBeast = unicornTurn ? 'unicorn' : 'dragon' ;
placeBeastImg (cell, currentBeast);
if (checkWin (currentBeast)) {
endGame (false );
} else if (isDraw ()) {
endGame (true );
} else {
swapTurns ();
updateCurrentStatus ();
setBoardHoverClass ();
}
}
13. 重置游戏与启动 resetButton.addEventListener ('click' , startGame);
startGame ();
完整源代码 将上述代码整合为一个完整的 HTML 文件即可运行。
<!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" >
<meta http-equiv ="X-UA-Compatible" content ="IE=edge" >
<meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
<title > 网页井字棋</title >
<style >
@import url("https://fonts.googleapis.com/css2?family=Bungee+Inline&display=swap" );
* { padding : 0 ; margin : 0 ; box-sizing : inherit; }
body { margin : 0 ; padding : 0 ; display : flex; justify-content : center; align-items : center; height : 100vh ; text-align : center; font-family : "Bungee Inline" , cursive; color : #f5f5f5 ; overflow : hidden; background-image : linear-gradient (to top, #a8edea 0% , #ffc7d9 100% ); }
.wrapper { background-color : #55acee53 ; padding : 50px ; }
.current-status { display : flex; justify-content : center; align-items : center; margin-bottom : 25px ; }
.current-status p { margin : 0 5px 0 0 ; font-size : 24px ; }
.current-status img { width : auto; height : 32px ; }
.board { display : grid; grid-template-columns : repeat (3 , minmax (90px , 1 fr)); grid-template-rows : repeat (3 , minmax (90px , 1 fr)); grid-gap : 12px ; width : 100% ; height : 100% ; max-width : 495px ; margin : 0 auto 15px ; }
.board .unicorn .cell :not (.dragon ):not (.unicorn ):hover ::before , .board .dragon .cell :not (.dragon ):not (.unicorn ):hover ::before { content : "" ; width : 70% ; height : 70% ; display : block; position : absolute; background-repeat : no-repeat; top : 50% ; left : 50% ; transform : translate3d (-50% , -50% , 0 ); background-size : contain; opacity : 50% ; }
.board .unicorn .cell :not (.dragon ):hover ::before { background-image : url ("./1.gif" ); }
.board .dragon .cell :not (.unicorn ):hover ::before { background-image : url ("./2.gif" ); }
.cell { cursor : pointer; position : relative; background-color : #f5f5f5 ; width : 90px ; height : 90px ; opacity : 0.5 ; transition : opacity 0.2s ease-in-out; }
.cell :hover { opacity : 1 ; }
.cell .dragon , .cell .unicorn { opacity : 1 ; position : relative; cursor : not-allowed; }
.cell .dragon ::before , .cell .unicorn ::before { content : "" ; width : 70% ; height : 70% ; display : block; position : absolute; background-repeat : no-repeat; top : 50% ; left : 50% ; transform : translate3d (-50% , -50% , 0 ); background-size : contain; }
.cell .dragon ::before { background-image : url ("./2.gif" ); }
.cell .unicorn ::before { background-image : url ("./1.gif" ); }
.game-end-overlay { display : none; position : fixed; top : 0 ; left : 0 ; right : 0 ; bottom : 0 ; background-color : #0d1021 ; }
.game-end-overlay .show { display : flex; flex-direction : column; justify-content : center; align-items : center; }
.winning-message { margin : -50px 0 20px ; }
.winning-message img { width : 100px ; }
.winning-message p { font-size : 48px ; margin : 0 ; }
.btn-container { position : relative; }
.reset-button { color : #f5f5f5 ; font-family : "Bungee Inline" , cursive; font-size : 30px ; white-space : nowrap; border : none; padding : 10px 20px ; background-color : #a186be ; box-shadow : 5px 5px 0 #55acee ; cursor : pointer; transition : transform 0.1s ease-in-out; position : relative; }
.reset-button :hover { transform : scale (1.2 ); }
.reset-button :active { top : 6px ; left : 6px ; box-shadow : none; background-color : #9475b5 ; }
</style >
</head >
<body >
<div class ="wrapper" >
<div id ="currentStatus" class ="current-status" >
<img id ="currentBeastImg" src ="./1.gif" alt ="unicorn" >
<p > 's turn</p >
</div >
<div id ="board" class ="board" >
<div data-cell > </div > <div data-cell > </div > <div data-cell > </div >
<div data-cell > </div > <div data-cell > </div > <div data-cell > </div >
<div data-cell > </div > <div data-cell > </div > <div data-cell > </div >
</div >
<div id ="gameEndOverlay" class ="game-end-overlay" >
<div class ="winning-message" data-winning-message >
<p > </p >
</div >
<div class ="btn-container" >
<button id ="resetButton" > play again</button >
</div >
</div >
</div >
<script >
const board = document .getElementById ('board' );
const cells = document .querySelectorAll ('[data-cell]' );
const currentStatus = document .getElementById ('currentStatus' );
const resetButton = document .getElementById ('resetButton' );
const gameEndOverlay = document .getElementById ('gameEndOverlay' );
const currentBeastStatusImg = document .getElementById ('currentBeastImg' );
const winningMessage = document .querySelector ('[data-winning-message]' );
const winningMessageText = document .querySelector ('[data-winning-message] p' );
const winningMessageImg = document .createElement ('img' );
let gameIsLive = true ;
let unicornTurn = true ;
let winner = null ;
const winningCombinations = [
[0 , 1 , 2 ], [3 , 4 , 5 ], [6 , , ],
[ , , ], [ , , ], [ , , ],
[ , , ], [ , , ]
];
= ( ) => {
board. . ( );
board. . ( );
(unicornTurn) { board. . ( ); }
{ board. . ( ); }
}
= ( ) => { cell. . (currentBeast); }
= ( ) => { unicornTurn = !unicornTurn; }
= ( ) => {
(unicornTurn) { currentBeastStatusImg. = ; currentBeastStatusImg. = ; }
{ currentBeastStatusImg. = ; currentBeastStatusImg. = ; }
}
= ( ) => {
winningCombinations. ( {
combination. ( { cells[i]. . (currentBeast); });
});
}
= ( ) => {
[...cells]. ( { cell. . ( ) || cell. . ( ); });
}
= ( ) => {
cells. ( {
winningMessageImg. ();
cell. . ( , );
cell. ( , handleCellClick);
cell. ( , handleCellClick, { : });
});
();
gameEndOverlay. . ( );
}
= ( ) => {
(draw) { winningMessageText. = ; }
{
winningMessageImg. = unicornTurn ? : ;
winningMessageImg. = unicornTurn ? : ;
winningMessage. (winningMessageImg, winningMessageText);
winningMessageText. = ;
}
gameEndOverlay. . ( );
}
= ( ) => {
cell = e. ;
currentBeast = unicornTurn ? : ;
(cell, currentBeast);
( (currentBeast)) { ( ); }
( ()) { ( ); }
{ (); (); (); }
}
resetButton. ( , startGame);
();
</script >
</body >
</html >
7
8
0
3
6
1
4
7
2
5
8
0
4
8
2
4
6
const
setBoardHoverClass
classList
remove
'unicorn'
classList
remove
'dragon'
if
classList
add
'unicorn'
else
classList
add
'dragon'
const
placeBeastImg
cell, currentBeast
classList
add
const
swapTurns
const
updateCurrentStatus
if
src
'./1.gif'
alt
'unicorn'
else
src
'./2.gif'
alt
'dragon'
const
checkWin
currentBeast
return
some
combination =>
return
every
i =>
return
classList
contains
const
isDraw
return
every
cell =>
return
classList
contains
'unicorn'
classList
contains
'dragon'
const
startGame
forEach
cell =>
remove
classList
remove
'unicorn'
'dragon'
removeEventListener
'click'
addEventListener
'click'
once
true
setBoardHoverClass
classList
remove
'show'
const
endGame
draw
if
innerText
`draw!`
else
src
'./1.gif'
'./2.gif'
alt
'unicorn'
'dragon'
insertBefore
innerText
`wins!!!`
classList
add
'show'
const
handleCellClick
e
const
target
const
'unicorn'
'dragon'
placeBeastImg
if
checkWin
endGame
false
else
if
isDraw
endGame
true
else
swapTurns
updateCurrentStatus
setBoardHoverClass
addEventListener
'click'
startGame
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online