跳到主要内容
基于 HTML/CSS/JavaScript 实现网页版井字棋游戏 | 极客日志
HTML / CSS 大前端
基于 HTML/CSS/JavaScript 实现网页版井字棋游戏 综述由AI生成 通过 HTML、CSS 和 JavaScript 构建了一个网页版井字棋游戏。利用 CSS Grid 布局棋盘,Flexbox 居中元素,并通过类名切换悬停效果。JavaScript 负责状态管理、回合切换及胜负判定逻辑,实现了完整的交互体验。代码包含结构搭建、样式美化与核心逻辑,适合前端初学者练手。
WenxuanMa 发布于 2026/4/10 更新于 2026/5/22 10 浏览前言
学习完 HTML、CSS 和 JavaScript 后,实战一个小案例是巩固知识的好方法。这里提供一个网页版井字棋的实现方案,涵盖结构搭建、样式美化与交互逻辑。
前置知识
在开始之前,建议熟悉以下知识点:
HTML 基础标签与语义化
CSS 布局(Flexbox、Grid)、过渡动画、伪类选择器
JavaScript DOM 操作、事件监听、数组方法
1. HTML 骨架
我们需要搭建基本的页面结构。注意,为了让后续 JavaScript 能准确获取元素,需要给关键容器添加对应的 id。
<!DOCTYPE html >
<html lang ="zh-CN" >
<head >
<meta charset ="UTF-8" >
<meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
<title > 网页井字棋</title >
<link rel ="stylesheet" href ="./index.css" >
<script defer src ="./index.js" > </script >
</head >
<body >
<div class ="wrapper" >
<div id ="currentStatus" = >
's turn
Play Again
相关免费在线工具 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
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
class
"current-status"
<img id ="currentBeastImg" src ="./1.gif" alt ="player" >
<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" class ="reset-button" >
</button >
</div >
</div >
</div >
</body >
</html >
2. CSS 装饰 接下来处理视觉表现。使用 Flexbox 实现垂直水平居中,Grid 布局棋盘,并添加鼠标悬停时的动态反馈。
全局与字体 @import url("https://fonts.googleapis.com/css2?family=Bungee+Inline&display=swap" );
* {
padding : 0 ;
margin : 0 ;
box-sizing : inherit;
}
页面主体 body {
display : flex;
justify-content : center;
align-items : center;
height : 100vh ;
font-family : "Bungee Inline" , cursive;
color : #f5f5f5 ;
background-image : linear-gradient (to top, #a8edea 0% , #ffc7d9 100% );
overflow : hidden;
}
棋盘与格子 核心在于 Grid 布局,配合 minmax 保证响应式宽度。
.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% ;
max-width : 495px ;
margin : 0 auto 15px ;
}
.cell {
cursor : pointer;
position : relative;
background-color : #f5f5f5 ;
opacity : 0.5 ;
transition : opacity 0.2s ease-in-out;
}
.cell :hover {
opacity : 1 ;
}
悬停特效与按钮 利用 ::before 伪元素在空白格子上显示预置图标,增强交互感。
.board .unicorn .cell :not (.dragon ):not (.unicorn ):hover ::before {
content : "" ;
background-size : contain;
}
.reset-button {
color : #f5f5f5 ;
font-family : "Bungee Inline" , cursive;
border : none;
padding : 10px 20px ;
background-color : #a186be ;
box-shadow : 5px 5px 0 #55acee ;
cursor : pointer;
transition : transform 0.1s ease-in-out;
}
.reset-button :hover {
transform : scale (1.2 );
}
.reset-button :active {
top : 6px ;
left : 6px ;
box-shadow : none;
background-color : #9475b5 ;
}
3. JavaScript 交互 这是游戏的核心。我们需要管理游戏状态、判断胜负以及处理用户点击。
初始化与状态 定义变量记录游戏是否进行中、当前轮到谁、是否有赢家。
let gameIsLive = true ;
let unicornTurn = true ;
let winner = null ;
获胜组合 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 ]
];
核心逻辑
点击处理 :获取目标格子,添加对应玩家类名。
胜负判定 :遍历获胜组合,检查是否满足条件。
平局判定 :所有格子填满且无赢家。
状态更新 :切换回合,更新 UI 显示。
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 ();
}
};
完整代码 为了方便直接运行,将上述 HTML、CSS、JS 整合在一起。实际开发中建议拆分为独立文件。
<!DOCTYPE html >
<html lang ="zh-CN" >
<head >
<meta charset ="UTF-8" >
<meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
<title > Tic Tac Toe</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 ="player" >
<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" class ="reset-button" > 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