目录:
一. Spring静态资源实现五子棋对战以及简单控制器访问
二. 引入thymeleaf模板引擎进行渲染以及控制器的正常使用
三. 基于控制器和javascript的异步交互
一. Spring静态资源访问以及简单控制器访问
首先我简单简述一下在springboot框架的目录结构中静态资源和动态内容的区别,在项目结构src/main下有文件夹resources用来存放资源,而resources分为static与templates:
templates/ static/
├── index.html ├── css/
├── game.html ├── js/
└── error.html └── images/ #示例
显而易见,templates用于放html文件,static用于放代码和图片等静态资源,
但现在我们先在static下创建index.html文件,并写好springboot启动类。
package com.example.gobang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GobangApplication {
public static void main(String[] args) {
SpringApplication.run(GobangApplication.class, args);
}
}
(依赖就自己去找好吧)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>五子棋对战</title>
<style>
#chessboard {
width: 450px;
height: 450px;
background-color: #DEB887;
position: relative;
margin: 50px auto;
border: 2px solid #8B4513;
}
.chess-line {
position: absolute;
background-color: #000;
}
.vertical {
width: 1px;
height: 450px;
}
.horizontal {
width: 450px;
height: 1px;
}
.chess {
position: absolute;
width: 28px;
height: 28px;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.black {
background-color: #000;
}
.white {
background-color: #fff;
border: 1px solid #000;
}
#status {
text-align: center;
font-size: 24px;
margin: 20px;
}
#restart {
display: block;
margin: 20px auto;
padding: 10px 20px;
font-size: 18px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="status">黑棋回合</div>
<div id="chessboard"></div>
<button id="restart">重新开始</button>
<script>
const board = document.getElementById('chessboard');
const status = document.getElementById('status');
const restart = document.getElementById('restart');
const GRID_SIZE = 30;
const BOARD_SIZE = 15;
let isBlackTurn = true;
let gameOver = false;
let chessBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));
// 创建棋盘
function createBoard() {
// 创建网格线
for (let i = 0; i < BOARD_SIZE; i++) {
// 垂直线
const vertical = document.createElement('div');
vertical.className = 'chess-line vertical';
vertical.style.left = i * GRID_SIZE + 'px';
board.appendChild(vertical);
// 水平线
const horizontal = document.createElement('div');
horizontal.className = 'chess-line horizontal';
horizontal.style.top = i * GRID_SIZE + 'px';
board.appendChild(horizontal);
}
}
// 放置棋子
function placeChess(x, y) {
if (gameOver || chessBoard[y][x] !== 0) return;
const chess = document.createElement('div');
chess.className = `chess ${isBlackTurn ? 'black' : 'white'}`;
chess.style.left = x * GRID_SIZE + 'px';
chess.style.top = y * GRID_SIZE + 'px';
board.appendChild(chess);
chessBoard[y][x] = isBlackTurn ? 1 : 2;
if (checkWin(x, y)) {
status.textContent = `${isBlackTurn ? '黑棋' : '白棋'}获胜!`;
gameOver = true;
return;
}
isBlackTurn = !isBlackTurn;
status.textContent = `${isBlackTurn ? '黑棋' : '白棋'}回合`;
}
// 检查是否获胜
function checkWin(x, y) {
const directions = [
[[0, 1], [0, -1]], // 垂直
[[1, 0], [-1, 0]], // 水平
[[1, 1], [-1, -1]], // 主对角线
[[1, -1], [-1, 1]] // 副对角线
];
const currentPlayer = isBlackTurn ? 1 : 2;
for (const direction of directions) {
let count = 1;
for (const [dx, dy] of direction) {
let newX = x + dx;
let newY = y + dy;
while (
newX >= 0 && newX < BOARD_SIZE &&
newY >= 0 && newY < BOARD_SIZE &&
chessBoard[newY][newX] === currentPlayer
) {
count++;
newX += dx;
newY += dy;
}
}
if (count >= 5) return true;
}
return false;
}
// 重新开始游戏
function restartGame() {
// 清除所有棋子
while (board.children.length > BOARD_SIZE * 2) {
board.removeChild(board.lastChild);
}
// 重置棋盘状态
chessBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));
isBlackTurn = true;
gameOver = false;
status.textContent = '黑棋回合';
}
// 初始化游戏
createBoard();
// 事件监听
board.addEventListener('click', (e) => {
const rect = board.getBoundingClientRect();
const x = Math.round((e.clientX - rect.left) / GRID_SIZE);
const y = Math.round((e.clientY - rect.top) / GRID_SIZE);
if (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE) {
placeChess(x, y);
}
});
restart.addEventListener('click', restartGame);
</script>
</body>
</html>
启动成功后运行代码你会发现可以自己和自己下棋了,这就是使用spring静态资源实现静态功能了
现在给你讲讲什么是控制器,控制器有普通控制器和rest控制器,分别是返回页面和数据的,暂时讲这么多,我们现在创建一个重定向控制器:
package com.example.gobang.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class RedirectController {
@GetMapping("/go")
public String gobang() {
return "redirect:/gobang.html"; // 重定向到静态HTML文件
}
这个属于返回页面的普通控制器,作用很明显,原先我们进入http://localhost:8080/gobang.html来进入页面,现在他对/go进行gobang.html的返回那我们访问http://localhost:8080/go会返回http://localhost:8080/gobang.html的页面。
二. 引入thymeleaf模板引擎进行渲染以及控制器的正常使用
1.什么是thymeleaf动态渲染
我们先对pom.xml导入thymeleaf的依赖。然后我进行简单讲解,前面给出了html文件,可以看到他的结构为:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>已而不在其然</title> <style></style> </head> <body> <div id="ddd"> </div> <script> function six(){} </script> </body> </html>
其中style部分放的就是css代码,scipt放javascript代码,
css用来改变页面的颜色位置形状等属性,
javascript用来实现方法比如判断五子棋的获胜,棋盘的生成,落子,
这些在方法html里面就已经全部完成了。
而现在我们来进行一个操作-thymeleaf进行渲染的话,举例:
<!-- 静态HTML -->
<div id="status">黑棋回合</div><!-- Thymeleaf版本 -->
<div id="status" th:text="${board.status}">无棋回合</div>
th:text即为动态渲染,你可能好奇${board.status}这个是什么,
以下是$的常见用法:
- ${变量名}:获取变量值
- ${条件 ? 值1 : 值2}:条件判断
- ${对象.属性}:访问对象属性
- ${方法调用}:调用方法
在这里是一个后端的数据,这个数据是会动态更新的,div内的内容由text的board.status来决定并且会将静态内容“无棋回合”覆盖。
2.动态渲染初始棋盘状态
以下是用静态资源和动态thymeleaf实现的初始棋盘,什么是棋盘,我们现在假定一个可以放[15]×[15]个棋子的棋盘,那我们就要使用spring控制器来实现棋盘:
注意:在使用javascript的function完成静态资源实现五子棋对战功能后要是想直接将static下的html移到templates目录下使用控制器动态渲染五子棋状态建议先把function删除,不然你的控制器没有完成的功能可能在javascript中已经完成导致你无法辨别。
两种不同方法实现初始化棋盘,静态与动态:
1.静态使用javascript创建空的初始棋盘状态:
<script>
const board = document.getElementById('chessboard');
const BOARD_SIZE = 15;
let chessBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(0));
</script>
2.控制器创建初始棋盘并使用thymeleaf动态渲染:
public class GameController {
private static final String BOARD_SESSION_KEY = "gameBoard";
@GetMapping("/")
public String index(Model model, HttpSession session) {
Board board = (Board) session.getAttribute(BOARD_SESSION_KEY);
if (board == null) {
board = new Board();
session.setAttribute(BOARD_SESSION_KEY, board);
}
model.addAttribute("board", board);
return "index";
}
<!-- 渲染棋子 -->
<div th:each="row, rowStat : ${board.grid}">
<div th:each="cell, colStat : ${row}"
class="cell"
th:classappend="${cell != 0} ? (${cell == 1} ? 'black' : 'white') : ''"
th:style="'left: ' + ${colStat.index * 30} + 'px; top: ' + ${rowStat.index * 30} + 'px'"
th:attr="data-row=${rowStat.index}, data-col=${colStat.index}">
</div>
</div>
public class Board { #Board类,控制器引用他
private int[][] grid;
private boolean isBlackTurn;
private boolean gameOver;
private String status;
private static final int BOARD_SIZE = 15;
public Board() {
this.grid = new int[BOARD_SIZE][BOARD_SIZE];
this.isBlackTurn = true;
this.gameOver = false;
this.status = "黑棋回合";
}
}
提示:上动态渲染文件是放在templates下的,普通控制器返回页面时不需要加上.html也能找到index.html所以return index就可以了。我的控制器中的为棋盘的模型专门创建了一个Board类封装了模型所有的属性,你也可以选择直接在控制类将属性写好。这个控制器使用for循环语句创建了15×15的空棋盘并将模型属性使用model.addAttribute("board",board);传递到了前端的 ${boardState}上,而下面的前端代码则是给棋盘的[15][15]个cell格子赋予了序列值Board
各部分含义:
- rowStat.index:当前行的索引(0, 1, 2, ...)
- colStat.index:当前列的索引(0, 1, 2, ...)
- data-row:自定义属性,存储行号
- data-col:自定义属性,存储列号
棋盘上可以放棋子了却还没有线条,我们可以使用thymeleaf循环语句加css创建棋盘的线条:
<!-- Thymeleaf版本 -->
<div id="chessboard">
<div th:each="i : ${#numbers.sequence(0, 14)}">
<div class="chess-line vertical" th:style="'left: ' + ${i * 30} + 'px'"></div>
<div class="chess-line horizontal" th:style="'top: ' + ${i * 30} + 'px'"></div>
</div>
</div>
.chess-line {
position: absolute;
background-color: #000;
}
.vertical {
width: 1px;
height: 100%;
}
.horizontal {
width: 100%;
height: 1px;
}
三. 基于控制器和javascript的异步交互
如果想要实现前端页面点击后向后端发送数据我们有两种方式解决
1.表单提交举例
<!-- 1. HTML中定义表单 -->
<form th:action="@{/move}" method="post" id="moveForm">
<input type="hidden" name="x" id="moveX">
<input type="hidden" name="y" id="moveY">
</form>
<!-- 2. JavaScript中获取点击位置并提交表单 -->
board.addEventListener('click', (e) => {
const rect = board.getBoundingClientRect();
const x = Math.round((e.clientX - rect.left) / 30);
const y = Math.round((e.clientY - rect.top) / 30);
if (x >= 0 && x < 15 && y >= 0 && y < 15) {
moveX.value = x;
moveY.value = y;
moveForm.submit(); // 提交表单,页面会刷新
}
});
<!-- 3. 控制器处理请求 -->
@PostMapping("/move")
public String makeMove(@RequestParam int x, @RequestParam int y,
Model model, HttpSession session) {
Board board = (Board) session.getAttribute(BOARD_SESSION_KEY);
board.makeMove(x, y);
model.addAttribute("board", board);
return "index"; // 返回页面,导致页面刷新
}
2.fetch异步请求举例
<!-- 1. HTML中直接定义单元格 -->
<div class="cell" th:attr="data-row=${rowStat.index}, data-col=${colStat.index}"></div>
<!-- 2. JavaScript中发送异步请求 -->
document.querySelectorAll('.cell').forEach(cell => {
cell.addEventListener('click', async function(event) {
const row = event.target.dataset.row;
const col = event.target.dataset.col;
const response = await fetch('/move', {
method: 'POST',
body: `x=${col}&y=${row}`
});
const result = await response.json();
// 更新界面,不刷新页面
});
});
<!-- 3. 控制器返回JSON数据 -->
@PostMapping("/move")
@ResponseBody
public Map<String, Object> makeMove(@RequestParam int x, @RequestParam int y,
HttpSession session) {
Board board = (Board) session.getAttribute(BOARD_SESSION_KEY);
board.makeMove(x, y);
return response; // 返回JSON数据,不刷新页面
}
由于老师的要求,我们选择第二种基于javascript的fetch异步请求:
用户点击 -> JavaScript发送请求 -> 控制器处理 -> 返回结果 -> JavaScript更新界面
我们可以通过上述步骤实现棋盘棋子状态的更新,
1.用户点击的单元格
<div class="cell"
th:classappend="${cell != 0} ? (${cell == 1} ? 'black' : 'white') : ''"
th:style="'left: ' + ${colStat.index * 30} + 'px; top: ' + ${rowStat.index * 30} + 'px'"
th:attr="data-row=${rowStat.index}, data-col=${colStat.index}">
</div>
2.javascript为每个单元格添加点击事件,在前端页面点击就会产生反应,并发送请求到后端
document.querySelectorAll('.cell').forEach(cell => {
cell.addEventListener('click', async function(event) {
const row = event.target.dataset.row;
const col = event.target.dataset.col;
const response = await fetch('/move', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `x=${col}&y=${row}`
});
});
});
3.控制器处理接受请求,处理落子逻辑并返回结果给前端
@PostMapping("/move")
@ResponseBody
public Map<String, Object> makeMove(@RequestParam int x, @RequestParam int y,
HttpSession session) {
Board board = (Board) session.getAttribute(BOARD_SESSION_KEY);
boolean success = board.makeMove(x, y);
Map<String, Object> response = new HashMap<>();
response.put("success", success);
response.put("status", board.getStatus());
response.put("isBlackTurn", board.isBlackTurn());
response.put("gameOver", board.isGameOver());
response.put("grid", board.getGrid());
return response; //对应下面json的各项属性,你可以访问/move页面查看数据
}
4.控制器处理后返回json格式数据
{ "isBlackTurn":false, "success":true, "grid":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], "gameOver":false, "status":"白棋回合" }
5.前端页面通过javascript进行更新
const result = await response.json();
if (result.success) {
// 更新状态显示
document.getElementById('status').textContent = result.status;
// 更新棋盘显示
const grid = result.grid;
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[i].length; j++) {
const cell = document.querySelector(`[data-row="${i}"][data-col="${j}"]`);
if (cell) {
cell.classList.remove('black', 'white');
if (grid[i][j] === 1) {
cell.classList.add('black');
} else if (grid[i][j] === 2) {
cell.classList.add('white');
}
}
}
}
}
简单的基于控制器和javascript的异步交互功能实现了,你可以自由探索胜负判定功能和游戏重置功能,主要在控制器和前端页面添加对应数据属性就行了。我在这将我的Board类给出来吧。
package com.example.gobang.model;
public class Board {
private int[][] grid;
private boolean isBlackTurn;
private boolean gameOver;
private String status;
private static final int BOARD_SIZE = 15;
public Board() {
this.grid = new int[BOARD_SIZE][BOARD_SIZE];
this.isBlackTurn = true;
this.gameOver = false;
this.status = "黑棋回合";
}
public boolean makeMove(int x, int y) {
if (gameOver || x < 0 || x >= BOARD_SIZE || y < 0 || y >= BOARD_SIZE || grid[y][x] != 0) {
return false;
}
grid[y][x] = isBlackTurn ? 1 : 2;
if (checkWin(x, y)) {
gameOver = true;
status = (isBlackTurn ? "黑棋" : "白棋") + "获胜!";
return true;
}
isBlackTurn = !isBlackTurn;
status = (isBlackTurn ? "黑棋" : "白棋") + "回合";
return true;
}
private boolean checkWin(int x, int y) {
int[][] directions = {
{0, 1}, // 垂直
{1, 0}, // 水平
{1, 1}, // 主对角线
{1, -1} // 副对角线
};
int currentPlayer = isBlackTurn ? 1 : 2;
for (int[] direction : directions) {
int count = 1;
// 正向检查
count += countDirection(x, y, direction[0], direction[1], currentPlayer);
// 反向检查
count += countDirection(x, y, -direction[0], -direction[1], currentPlayer);
if (count >= 5) {
return true;
}
}
return false;
}
private int countDirection(int x, int y, int dx, int dy, int player) {
int count = 0;
int newX = x + dx;
int newY = y + dy;
while (newX >= 0 && newX < BOARD_SIZE &&
newY >= 0 && newY < BOARD_SIZE &&
grid[newY][newX] == player) {
count++;
newX += dx;
newY += dy;
}
return count;
}
public void reset() {
this.grid = new int[BOARD_SIZE][BOARD_SIZE];
this.isBlackTurn = true;
this.gameOver = false;
this.status = "黑棋回合";
}
// Getters and Setters
public int[][] getGrid() {
return grid;
}
public void setGrid(int[][] grid) {
this.grid = grid;
}
public boolean isBlackTurn() {
return isBlackTurn;
}
public void setBlackTurn(boolean blackTurn) {
isBlackTurn = blackTurn;
}
public boolean isGameOver() {
return gameOver;
}
public void setGameOver(boolean gameOver) {
this.gameOver = gameOver;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
后面我会专门关于Spring Data JPA和数据库实现游戏保存和加载功能以及Spring Data JPA整合为Mybatis写一篇。