跳到主要内容
原生 HTML/CSS/JS 实现网页井字棋游戏 | 极客日志
HTML / CSS 大前端 算法
原生 HTML/CSS/JS 实现网页井字棋游戏 综述由AI生成 基于 HTML、CSS 和 JavaScript 构建的双人井字棋游戏。实现了棋盘网格布局、鼠标悬停预览效果、回合切换逻辑及胜负判定。包含样式重置、Flexbox/Grid 布局应用以及 DOM 事件监听与状态管理,适合作为前端基础交互练习案例。
NodeJser 发布于 2026/4/7 更新于 2026/5/22 14 浏览前言
在学习完 HTML、CSS 和 JavaScript 后,动手做一个小案例是巩固知识的最佳方式。本文将带你从零开始构建一个双人网页井字棋游戏,涵盖布局、样式交互以及核心逻辑实现。
前置知识准备
在开始之前,确保你熟悉以下基础概念:
HTML : 语义化标签与 DOM 结构搭建
CSS : Flexbox 布局、Grid 网格系统、伪类选择器 (:hover)、过渡动画 (transition)
JavaScript : DOM 操作、事件监听、数组方法 (some, every)、状态管理
1. HTML 骨架搭建
首先构建页面的基本结构。我们需要一个容器包裹整个游戏区域,包括状态显示区、棋盘格子和结果反馈层。
<!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" >
's turn
play again
相关免费在线工具 加密/解密文本 使用加密算法(如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
<div class ="current-status" >
<img id ="currentBeastImg" src ="./1.gif" alt ="player" >
<p id ="statusText" >
</p >
</div >
<div class ="board" id ="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 class ="game-end-overlay" id ="gameEndOverlay" >
<div class ="winning-message" data-winning-message >
<p id ="winningMessageText" >
</p >
</div >
<button class ="reset-button" id ="resetButton" >
</button >
</div >
</div >
</body >
</html >
这里我们使用了 data-cell 属性来标记棋盘格子,方便后续通过 JS 批量获取。初始状态下,由于没有内容,部分容器高度可能为 0,接下来我们通过 CSS 进行美化。
2. CSS 样式设计
全局重置与字体 引入自定义字体并清除默认边距,确保不同浏览器下的一致性。
@import url("https://fonts.googleapis.com/css2?family=Bungee+Inline&display=swap" );
* {
padding : 0 ;
margin : 0 ;
box-sizing : inherit;
}
页面布局 使用 Flexbox 将内容垂直水平居中,并设置全屏高度。
body {
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% );
}
棋盘网格 利用 CSS Grid 创建 3x3 的棋盘,并设置单元格间距。
.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 ;
width : 90px ;
height : 90px ;
opacity : 0.5 ;
transition : opacity 0.2s ease-in-out;
}
.cell :hover {
opacity : 1 ;
}
悬停预览效果 这是提升体验的关键点。当鼠标悬停在空白格子上时,根据当前玩家显示对应的图标预览。
.board .unicorn .cell :not (.dragon ):not (.unicorn ):hover ::before {
content : "" ;
width : 70% ;
height : 70% ;
position : absolute;
top : 50% ;
left : 50% ;
transform : translate3d (-50% , -50% , 0 );
background-image : url ("./1.gif" );
background-size : contain;
opacity : 50% ;
}
.board .dragon .cell :not (.dragon ):not (.unicorn ):hover ::before {
content : "" ;
width : 70% ;
height : 70% ;
position : absolute;
top : 50% ;
left : 50% ;
transform : translate3d (-50% , -50% , 0 );
background-image : url ("./2.gif" );
background-size : contain;
opacity : 50% ;
}
按钮与遮罩层 设计重启按钮的交互态(Hover/Active),以及游戏结束时的覆盖层样式。
.reset-button {
color : #f5f5f5 ;
font-family : "Bungee Inline" , cursive;
font-size : 30px ;
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 ;
}
.game-end-overlay {
display : none;
position : fixed;
top : 0 ; left : 0 ; right : 0 ; bottom : 0 ;
background-color : rgba (13 , 16 , 33 , 0.8 );
}
.game-end-overlay .show {
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
}
3. JavaScript 交互逻辑
初始化与元素获取 在脚本加载时获取关键 DOM 节点,并定义游戏状态变量。
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 , 7 , 8 ],
[0 , 3 , 6 ], [1 , 4 , 7 ], [2 , 5 , 8 ],
[0 , 4 , 8 ], [2 , 4 , 6 ]
];
核心函数实现 切换回合与状态更新
每次点击后,需要判断是否有效,然后切换当前玩家并更新 UI。
const swapTurns = ( ) => {
unicornTurn = !unicornTurn;
};
const updateCurrentStatus = ( ) => {
if (unicornTurn) {
currentBeastStatusImg.src = './1.gif' ;
currentBeastStatusImg.alt = 'unicorn' ;
} else {
currentBeastStatusImg.src = './2.gif' ;
currentBeastStatusImg.alt = 'dragon' ;
}
};
const checkWin = (currentBeast ) => {
return winningCombinations.some (combination => {
return combination.every (i => {
return cells[i].classList .contains (currentBeast);
});
});
};
const isDraw = ( ) => {
return [...cells].every (cell => {
return cell.classList .contains ('unicorn' ) || cell.classList .contains ('dragon' );
});
};
处理点击事件
这是游戏的入口。如果格子已被占用则忽略,否则放置标识并检查游戏状态。
const handleCellClick = (e ) => {
const cell = e.target ;
const currentBeast = unicornTurn ? 'unicorn' : 'dragon' ;
if (cell.classList .contains ('unicorn' ) || cell.classList .contains ('dragon' )) return ;
cell.classList .add (currentBeast);
if (checkWin (currentBeast)) {
endGame (false );
} else if (isDraw ()) {
endGame (true );
} else {
swapTurns ();
updateCurrentStatus ();
setBoardHoverClass ();
}
};
const endGame = (draw ) => {
if (draw) {
winningMessageText.innerText = 'draw!' ;
} else {
winningMessageImg.src = unicornTurn ? './1.gif' : './2.gif' ;
winningMessage.insertBefore (winningMessageImg, winningMessageText);
winningMessageText.innerText = 'wins!!!' ;
}
gameEndOverlay.classList .add ('show' );
};
const startGame = ( ) => {
cells.forEach (cell => {
cell.classList .remove ('unicorn' , 'dragon' );
cell.removeEventListener ('click' , handleCellClick);
cell.addEventListener ('click' , handleCellClick, { once : false });
});
setBoardHoverClass ();
gameEndOverlay.classList .remove ('show' );
winningMessageText.innerText = '' ;
winningMessageImg.remove ();
};
resetButton.addEventListener ('click' , startGame);
setBoardHoverClass ();
startGame ();
完整代码参考 为了方便大家直接运行,以下是整合后的完整 HTML 文件代码。你可以将其保存为 .html 文件并在浏览器中打开。
<!DOCTYPE html >
<html lang ="zh-CN" >
<head >
<meta charset ="UTF-8" >
<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 class ="current-status" >
<img id ="currentBeastImg" src ="./1.gif" >
<p id ="statusText" > 's turn</p >
</div >
<div class ="board" id ="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 class ="game-end-overlay" id ="gameEndOverlay" >
<div class ="winning-message" data-winning-message >
<p id ="winningMessageText" > </p >
</div >
<button class ="reset-button" id ="resetButton" > play again</button >
</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. . ( );
winningMessageText. = ;
}
= ( ) => {
(draw) {
winningMessageText. = ;
} {
winningMessageImg. = unicornTurn ? : ;
winningMessageImg. = unicornTurn ? : ;
winningMessage. (winningMessageImg, winningMessageText);
winningMessageText. = ;
}
gameEndOverlay. . ( );
}
= ( ) => {
cell = e. ;
currentBeast = unicornTurn ? : ;
(cell. . ( ) || cell. . ( )) ;
(cell, currentBeast);
( (currentBeast)) {
( );
} ( ()) {
( );
} {
();
();
();
}
}
resetButton. ( , startGame);
();
</script >
</body >
</html >
至此,一个简单的交互式井字棋游戏就完成了。通过这个案例,你可以复习到 DOM 操作、事件委托、条件判断以及基本的游戏循环逻辑。试着修改一下配色或者增加 AI 功能,让它变得更有趣吧!
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
false
setBoardHoverClass
classList
remove
'show'
innerText
''
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'
if
classList
contains
'unicorn'
classList
contains
'dragon'
return
placeBeastImg
if
checkWin
endGame
false
else
if
isDraw
endGame
true
else
swapTurns
updateCurrentStatus
setBoardHoverClass
addEventListener
'click'
startGame