跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
HTML / CSS大前端算法

前端实战:使用 HTML、CSS 和 JavaScript 实现网页井字棋

介绍如何使用 HTML、CSS 和 JavaScript 构建网页版井字棋游戏。内容涵盖 HTML 结构搭建、CSS 布局美化(Flexbox 与 Grid)、以及 JavaScript 交互逻辑(回合切换、胜负判断)。文章提供了详细步骤解析及完整源代码,适合前端初学者练习 DOM 操作与事件处理。

时间旅人发布于 2026/4/6更新于 2026/9/655 浏览
前端实战:使用 HTML、CSS 和 JavaScript 实现网页井字棋

网页井字棋实现教程

本文介绍如何使用 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">
    <title>网页井字棋</title>
    <!-- 引入外部样式表 -->
    <link rel="stylesheet" href="./index.css">
    <!-- 延迟加载外部 JavaScript 文件 -->
    <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>&nbsp; 's turn</p>
        </div>
        <!-- 游戏棋盘 -->
        <div id="board" class="board">
            <!-- 游戏棋盘的每个格子,标记为 data-cell -->
            <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>
</body>
</html>

初始状态下,部分容器因无内容高度可能为 0,后续将通过 CSS 进行修饰。

2. CSS 装饰

编写 HTML 后,通过 CSS 进行样式美化。

1. 引入字体和全局样式

/* 引入自定义字体 Bungee Inline */
@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, 1fr));
    grid-template-rows: repeat(3, minmax(90px, 1fr));
    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 交互

添加 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, 1fr)); grid-template-rows: repeat(3, minmax(90px, 1fr)); 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>&nbsp; '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, 7, 8],
            [0, 3, 6], [1, 4, 7], [2, 5, 8],
            [0, 4, 8], [2, 4, 6]
        ];
        const setBoardHoverClass = () => {
            board.classList.remove('unicorn');
            board.classList.remove('dragon');
            if (unicornTurn) { board.classList.add('unicorn'); }
            else { board.classList.add('dragon'); }
        }
        const placeBeastImg = (cell, currentBeast) => { cell.classList.add(currentBeast); }
        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 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');
        }
        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');
        }
        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(); }
        }
        resetButton.addEventListener('click', startGame);
        startGame();
    </script>
</body>
</html>

目录

  1. 网页井字棋实现教程
  2. 前置知识
  3. 1. HTML 骨架
  4. 2. CSS 装饰
  5. 1. 引入字体和全局样式
  6. 2. 设置 body 样式
  7. 3. 设置 .wrapper 样式
  8. 4. 设置 .current-status 和其中的元素样式
  9. 5. 设置 board 和 .cell 样式
  10. 6. 鼠标悬浮时的图片效果
  11. 7. 设置 game-end-overlay 样式
  12. 8. 设置 .winning-message 样式
  13. 9. 重启按钮样式
  14. 3. JavaScript 交互
  15. 1. 获取页面元素
  16. 2. 初始化游戏状态
  17. 3. 所有获胜组合
  18. 4. 设置鼠标悬停时的样式
  19. 5. 在格子上放置图片
  20. 6. 切换回合
  21. 7. 更新当前状态
  22. 8. 检查是否获胜
  23. 9. 判断是否平局
  24. 10. 开始游戏
  25. 11. 结束游戏
  26. 12. 处理格子点击事件
  27. 13. 重置游戏与启动
  28. 完整源代码

更多推荐文章

查看全部
  • 用 ClawdBot 和 MoltBot 在本地部署粤英翻译 Telegram 机器人
  • 程序员转行网络安全的原因及学习路径
  • 通义万相 2.1 文生图模型特性与部署实践
  • DALL·E 3 绘图功能详解与 API 集成指南
  • 使用 LLaMA-Factory 微调 Qwen2.5 模型并转换为 GGUF 格式部署
  • 第十五届蓝桥杯 Python B 组省赛真题解析
  • .NET Web API 控制器与 Action 注解详解
  • Spring 配置文件详解:Properties 与 YAML 格式对比及实战
  • Django REST Framework 企业级 API 架构实战
  • LeetCode 链表经典题目:移除、反转、中间节点、合并与回文结构
  • Java Servlet 过滤器实现敏感字符过滤
  • Python 月相可视化系统:从天文计算到 Web 界面生成
  • 详解 Python 常见文件后缀:.py、.ipynb、.pyi、.pyc、.pyd
  • 豆包 Seedream 4.0 多图融合实测:主体一致性与生成速度解析
  • 什么是大模型?一文搞懂大模型原理与应用
  • GLM-5 模型代码生成能力深度评测与实战
  • FPGA 实现 CIC 抽取滤波器
  • Spring Cloud Alibaba 集成 SkyWalking 全链路追踪实战
  • 腾讯云智能客服 Java 集成与生产环境优化
  • 零门槛上手!小白也能封神,好用的AI写作平台

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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