JavaScript实现可自定义行列数的响应式黑白棋盘格

作者:白露

时间:2026-11-13

浏览:209

使用HTML、CSS Grid与JavaScript实现可自定义行数和列数的响应式黑白棋盘格,详解二维奇偶着色原理、输入校验、DOM与Canvas方案对比及常见错位排查。

文章目录
JavaScript实现可自定义行列数的响应式黑白棋盘格
图 0:自定义行列数并随容器宽度等比缩放的黑白棋盘格整体结构

要在网页里做出一个可自定义行数和列数、且在不同屏幕宽度下都能保持格子方正的黑白棋盘格,最直接的实现方式是三层配合:用 HTML 数字输入框接收行列数,用 CSS Grid 的 1fr 轨道配合 aspect-ratio: 1 / 1 让格子自动适应容器宽度,再用 JavaScript 通过二维坐标 (row + col) % 2 交替设置黑白样式。下文按页面准备、响应式布局、脚本渲染、完整示例和方案对照的顺序逐步实现。

一、整体实现步骤与页面结构准备

本示例不需要任何构建工具或第三方库,只需新建一个名为 checkerboard.html 的文本文件,用任意现代浏览器打开即可运行。在动手写样式和脚本之前,先明确五个关键步骤的分工:控件只负责输入和展示状态,CSS 负责列数分配与正方形比例,JavaScript 负责输入兜底与节点生成。

JavaScript实现可自定义行列数的响应式黑白棋盘格
图 1:自定义行列数响应式棋盘格的五个实施步骤

先写好 HTML 骨架。把行列输入框、状态提示和棋盘容器放在同一居中容器内,默认值设为常见的 8 × 8,同时通过 min="1" 和 max="50" 给出原生输入范围提示:

<main class="panel">
  <div class="controls">
    <label>
      行数:
      <input type="number" id="rowsInput" min="1" max="50" value="8">
    </label>
    <label>
      列数:
      <input type="number" id="colsInput" min="1" max="50" value="8">
    </label>
    <button type="button" id="applyBtn">生成棋盘</button>
  </div>

  <p id="boardInfo" class="board-info">当前尺寸:8 行 × 8 列(共 64 格)</p>

  <div class="board-wrap">
    <div id="board" class="board" role="img" aria-label="黑白棋盘格"></div>
  </div>
</main>

二、用 CSS Grid 与 aspect-ratio 实现响应式正方形

做响应式棋盘格时,常见障碍有两个:一是列数不固定,写死 repeat(8, 1fr) 无法适应用户修改后的列数;二是把宽度设为百分比后,高度设 height: 100% 往往会失效或把格子拉成扁长方形。借助 CSS 自定义属性 --cols 和 aspect-ratio: 1 / 1 可以同时解决这两个问题。

.board-wrap {
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
}

.board {
  --cols: 8;
  display: grid;
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  width: 100%;
  border: 2px solid #22262a;
  background-color: #ffffff;
}

.cell {
  aspect-ratio: 1 / 1;
}

.cell--light {
  background-color: #ffffff;
}

.cell--dark {
  background-color: #1f2428;
}

这段样式里有三处细节决定了最终的自适应效果:

  • --cols 变量把列数控制权交给 CSS Grid。JavaScript 只需要修改容器的 --cols,浏览器就会自动把当前容器宽度平均分给每一列,无需在窗口缩放时用脚本重新计算像素宽度。
  • minmax(0, 1fr) 比单独写 1fr 更稳妥。列数较多(例如 30 列以上)时,minmax(0, 1fr) 允许列宽在极窄屏幕上收缩到接近 0,避免网格内容撑出右边界。
  • aspect-ratio: 1 / 1 放在 .cell 上,而不是把固定宽高比放在 .board 上。当用户输入 4 行 × 10 列 或 12 行 × 5 列 这样的非方阵时,棋盘整体是矩形,但每一个单元格依然能依据自身计算后的列宽自动推导高度,严格保持正方形。
提醒:不要给 .cell 单独加 border。列数达到三四十列且屏幕宽度只有三百多像素时,每格不足 10 像素,单元格自带边框会占用空间并在亚像素取整时让网格边缘出现轻微锯齿。只在外层 .board 保留一条边框,内部全靠背景色区分即可。

三、用 JavaScript 动态生成棋盘与交替着色

初学者编写棋盘格循环时,很容易直接按一维顺序索引 index % 2 === 0(或用 CSS 的 :nth-child(even))来区分黑白。这在列数为奇数(如 5 列、7 列)时看似正常,一旦列数变成偶数(如常见的 4 列、8 列),每行最后一个格子与下一行第一个格子的序号奇偶性就会发生反转,导致上下两行同列颜色完全相同,整张棋盘变成竖条纹。

JavaScript实现可自定义行列数的响应式黑白棋盘格
图 2:一维序号着色在偶数列下的竖条纹错误与二维坐标着色对比

使用双层循环遍历行号 row 和列号 col,再根据 (row + col) % 2 === 0 决定单元格是白色还是黑色,可以保证任何相邻的上下左右两格坐标和必定一奇一偶。同时加上三个实用细节:输入范围兜底、文档碎片批量挂载、以及同步更新 CSS 变量 --cols。

const rowsInput = document.getElementById('rowsInput');
const colsInput = document.getElementById('colsInput');
const applyBtn = document.getElementById('applyBtn');
const boardEl = document.getElementById('board');
const boardInfoEl = document.getElementById('boardInfo');

const MIN_COUNT = 1;
const MAX_COUNT = 50;

function clampCount(rawValue, fallback = 8) {
  const parsed = Number.parseInt(rawValue, 10);
  if (Number.isNaN(parsed)) {
    return fallback;
  }
  return Math.min(MAX_COUNT, Math.max(MIN_COUNT, parsed));
}

function renderCheckerboard(rows, cols) {
  boardEl.style.setProperty('--cols', String(cols));

  const fragment = document.createDocumentFragment();
  for (let row = 0; row < rows; row += 1) {
    for (let col = 0; col < cols; col += 1) {
      const cell = document.createElement('div');
      const isLight = (row + col) % 2 === 0;
      cell.className = isLight ? 'cell cell--light' : 'cell cell--dark';
      fragment.appendChild(cell);
    }
  }

  boardEl.replaceChildren(fragment);
  boardInfoEl.textContent = `当前尺寸:${rows} 行 × ${cols} 列(共 ${rows * cols} 格)`;
}

function updateFromInputs() {
  const rows = clampCount(rowsInput.value, 8);
  const cols = clampCount(colsInput.value, 8);
  rowsInput.value = String(rows);
  colsInput.value = String(cols);
  renderCheckerboard(rows, cols);
}

applyBtn.addEventListener('click', updateFromInputs);
rowsInput.addEventListener('change', updateFromInputs);
colsInput.addEventListener('change', updateFromInputs);

updateFromInputs();

代码中的 clampCount 专门用来处理非法输入。HTML 的 min 和 max 属性只约束表单提交和上下微调按钮,用户依然可以手动输入 0、负数、小数或 9999。限制在 1 ~ 50 之间,既覆盖了常见展示和棋类场景,也能避免一次插入几十万个 DOM 节点导致页面假死。事件监听使用 change 而不是 input,是为了让用户在输入两位数字(例如想把 8 改成 12,中间会短暂出现空值)时,不会在敲下第一个字符或退格清空的瞬间被强制回填。

四、完整可运行代码与效果验证

将上面的结构、样式和脚本合并为一个完整的 checkerboard.html 文件。复制到本地后,直接在 Chrome、Edge、Firefox 或 Safari 中双击打开即可看到效果。

<!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>
    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      padding: 24px 16px;
      font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
      background-color: #f5f4f0;
      color: #252b30;
    }

    .panel {
      max-width: 720px;
      margin: 0 auto;
      padding: 24px;
      background-color: #ffffff;
      border: 1px solid #dedad0;
      border-radius: 12px;
    }

    .controls {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 12px 18px;
      margin-bottom: 12px;
    }

    .controls label {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      font-size: 15px;
    }

    .controls input {
      width: 76px;
      padding: 6px 8px;
      border: 1px solid #c5c0b4;
      border-radius: 6px;
      font-size: 15px;
    }

    .controls button {
      padding: 7px 16px;
      border: none;
      border-radius: 6px;
      background-color: #687d8d;
      color: #ffffff;
      font-size: 14px;
      cursor: pointer;
    }

    .board-info {
      margin: 0 0 16px;
      font-size: 14px;
      color: #5b656e;
    }

    .board-wrap {
      width: 100%;
      max-width: 640px;
      margin: 0 auto;
    }

    .board {
      --cols: 8;
      display: grid;
      grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
      width: 100%;
      border: 2px solid #22262a;
      background-color: #ffffff;
    }

    .cell {
      aspect-ratio: 1 / 1;
    }

    .cell--light {
      background-color: #ffffff;
    }

    .cell--dark {
      background-color: #1f2428;
    }
  </style>
</head>
<body>
  <main class="panel">
    <div class="controls">
      <label>
        行数:
        <input type="number" id="rowsInput" min="1" max="50" value="8">
      </label>
      <label>
        列数:
        <input type="number" id="colsInput" min="1" max="50" value="8">
      </label>
      <button type="button" id="applyBtn">生成棋盘</button>
    </div>

    <p id="boardInfo" class="board-info"></p>

    <div class="board-wrap">
      <div id="board" class="board" role="img" aria-label="黑白棋盘格"></div>
    </div>
  </main>

  <script>
    const rowsInput = document.getElementById('rowsInput');
    const colsInput = document.getElementById('colsInput');
    const applyBtn = document.getElementById('applyBtn');
    const boardEl = document.getElementById('board');
    const boardInfoEl = document.getElementById('boardInfo');

    const MIN_COUNT = 1;
    const MAX_COUNT = 50;

    function clampCount(rawValue, fallback = 8) {
      const parsed = Number.parseInt(rawValue, 10);
      if (Number.isNaN(parsed)) {
        return fallback;
      }
      return Math.min(MAX_COUNT, Math.max(MIN_COUNT, parsed));
    }

    function renderCheckerboard(rows, cols) {
      boardEl.style.setProperty('--cols', String(cols));

      const fragment = document.createDocumentFragment();
      for (let row = 0; row < rows; row += 1) {
        for (let col = 0; col < cols; col += 1) {
          const cell = document.createElement('div');
          const isLight = (row + col) % 2 === 0;
          cell.className = isLight ? 'cell cell--light' : 'cell cell--dark';
          fragment.appendChild(cell);
        }
      }

      boardEl.replaceChildren(fragment);
      boardInfoEl.textContent = `当前尺寸:${rows} 行 × ${cols} 列(共 ${rows * cols} 格)`;
    }

    function updateFromInputs() {
      const rows = clampCount(rowsInput.value, 8);
      const cols = clampCount(colsInput.value, 8);
      rowsInput.value = String(rows);
      colsInput.value = String(cols);
      renderCheckerboard(rows, cols);
    }

    applyBtn.addEventListener('click', updateFromInputs);
    rowsInput.addEventListener('change', updateFromInputs);
    colsInput.addEventListener('change', updateFromInputs);

    updateFromInputs();
  </script>
</body>
</html>

打开页面后,可以按下面四组测试条件核对功能是否正常:

  1. 默认状态(8 行 × 8 列):页面加载后显示 64 个正方形格子,左上角第一格为白色,第二格为黑色,文字提示显示“当前尺寸:8 行 × 8 列(共 64 格)”。
  2. 奇偶不对称行列(例如 5 行 × 8 列、6 行 × 7 列):修改数字并点击“生成棋盘”或移出输入框失焦后,格子保持正方形,相邻行之间不会出现同色竖条纹。
  3. 浏览器窄屏缩放:按 F12 打开开发者工具切换至 320px 宽的手机视图,或手动拖窄浏览器窗口,棋盘格随容器等比缩小,页面不出现横向滚动条。
  4. 越界输入兜底:输入 0、-5 或 200 后触发更新,输入框会自动修正为 1 或 50,界面不卡顿也不留空白。

五、实现方案对照:DOM + CSS Grid 与 Canvas

在网页中绘制自定义行列数棋盘格,除了上面用到的“DOM 元素 + CSS Grid”,另一种常见做法是使用 HTML5 的 <canvas> 按像素绘制矩形。两种方法在同样要求“可改行列数”和“响应式缩放”的前提下,机制和适用条件有明显区别。

JavaScript实现可自定义行列数的响应式黑白棋盘格
图 3:DOM + CSS Grid 与 Canvas 两种实现方式在响应式与复杂度上的差异
对比项目 DOM + CSS Grid(本文方案) HTML5 Canvas 绘制
响应式实现成本 由 CSS 引擎自动完成,窗口缩放时无需重新执行绘制脚本。 需要配合 ResizeObserver 重新设置画布缓冲尺寸并重绘,同时要乘以 window.devicePixelRatio 才能避免在高分屏下发虚。
大行列数表现 节点数量随 rows × cols 线性增长,控制在 50 × 50(2500 个节点)以内渲染流畅;再往上会明显增加布局耗时。 始终只有一个 <canvas> 元素,即使绘制 200 × 200 的高密度网格也不会增加 DOM 树负担。
后续交互扩展 可直接为 .cell 添加 :hover 高亮、点击落子事件或无障碍属性。 单个格子不是独立元素,需要监听鼠标偏移坐标再除以单格宽度反算 (row, col)。

选择依据很明确:如果棋盘行列数在 1 ~ 50 之间,或者后续需要在格子里放棋子、图标、悬停提示,使用 DOM + CSS Grid 代码更少、响应更稳;只有当业务确实需要生成上百行、上百列的密集网格或导出整张位图时,才值得切到 <canvas>。

六、常见问题与整体要点总结

在实际使用或根据需要微调上述代码时,还需要注意两点边界情况:

  • 高瘦型棋盘超出视口高度:当用户把行数设得远大于列数(例如 30 行 × 4 列)时,由于每个格子维持正方形且宽度占满容器,棋盘总高度会明显高于屏幕。这种情况下应允许页面自然纵向滚动,不要为了“一屏装下”而给 .board 强行加上 max-height 或固定 height,否则会破坏正方形比例或裁剪底部格子。
  • 给棋盘增加点击交互时避免逐格绑定监听:如果后续希望点击某格切换状态,只需在父容器 #board 上绑定一个事件委托监听器,配合创建格子时写入的 dataset.row 和 dataset.col 读取坐标即可,不要在循环里给几千个 .cell 各自注册 addEventListener。
JavaScript实现可自定义行列数的响应式黑白棋盘格
图 4:响应式自定义行列数黑白棋盘格的核心技术要点总结

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章