在电商类前端开发中,商品列表的增删改查(CRUD)是高频核心场景。本文将从原理拆解 + 代码实现 + 细节优化三个维度,手把手教你用 jQuery 完成商品列表的渲染、数据操作(新增 / 编辑 / 删除 / 数量调整)及实时统计,不仅能掌握核心语法,更能理解「数据驱动视图」的前端底层逻辑,适合新手入门与实战复盘。
一、核心需求与实现目标
本次实战需达成以下可落地的功能目标,覆盖电商列表核心场景:
- 基于 AJAX 异步加载本地 JSON 格式的初始商品数据,处理加载失败的异常场景;
- 渲染商品列表到表格,支持数量加减(数量≥1,禁止减至 0);
- 实现商品新增 / 编辑 / 删除,新增 / 编辑时做严格的输入合法性校验;
- 数据变化(新增 / 编辑 / 数量调整 / 删除)时,实时计算并更新总价格、总数量;
- 优化用户体验(如删除确认、金额格式化、输入提示等)。
二、技术栈与项目结构
1. 核心技术栈
- HTML5:语义化标签搭建页面结构,input 原生属性(min/step)做基础输入限制;
- CSS3:极简样式保证布局规整,兼顾基础交互体验;
- jQuery 3.7.1:简化 DOM 操作、AJAX 请求、事件绑定,降低新手学习成本;
- JSON:存储结构化的初始商品数据,模拟后端数据返回格式。
2. 标准化项目结构
plaintext
├── index.html // 核心页面(结构+逻辑)
├── js/
│ ├── jquery-3.7.1.min.js // jQuery核心库(需提前下载)
│ └── index.json // 商品初始数据文件
三、核心问题拆解与实现方案
问题 1:如何使用 AJAX 请求加载 JSON 格式的商品初始数据?
原理说明
AJAX(异步 JavaScript 和 XML)是前端异步请求数据的核心技术,jQuery 封装了$.ajax()方法,可简化请求流程。加载本地 JSON 文件的核心逻辑是:通过 GET 请求读取 JSON 文件,成功后将数据存入数组,触发列表渲染;失败时给出明确的错误提示。
完整实现代码
javascript
运行
// 全局变量:存储商品数据的核心数组 + 标记编辑状态(-1=新增,其他=编辑索引)
let goodsList = [];
let editIndex = -1;
$(function () {
// 1. AJAX加载初始商品数据
$.ajax({
url: "./js/index.json", // JSON文件路径(需与实际目录匹配)
type: "GET", // GET请求(读取静态文件)
dataType: "json", // 预期返回数据类型为JSON
timeout: 5000, // 超时时间:5秒
success: function (res) {
console.log("初始商品数据加载成功:", res);
goodsList = res; // 将返回数据存入核心数组
renderGoodsList(); // 触发列表渲染
},
error: function (xhr, status, error) {
// 分类处理错误,提升调试效率
if (status === "timeout") {
alert("数据请求超时,请检查网络!");
} else if (xhr.status === 404) {
alert("JSON文件不存在,请检查文件路径:./js/index.json");
} else {
console.error("数据加载失败详情:", error);
alert("初始数据加载失败,详情请查看控制台!");
}
}
});
});
// 配套JSON数据示例(index.json)
[
{"id": 1, "name": "小米14 Pro", "price": 4999.99, "num": 2},
{"id": 2, "name": "华为FreeBuds Pro 3", "price": 1299.00, "num": 5},
{"id": 3, "name": "苹果MagSafe充电器", "price": 149.00, "num": 10}
]
关键细节说明
- 路径问题 :
url需使用相对路径(如./js/index.json),避免绝对路径导致的跨域 / 找不到文件问题; - 超时处理 :添加
timeout参数,避免请求无响应时用户等待; - 错误分类 :根据
status和xhr.status区分超时、404 等错误,提升用户体验与调试效率; - 数据承接 :请求成功后将数据赋值给全局数组
goodsList,作为所有操作的「数据源」。
问题 2:如何实现商品的新增 / 编辑 / 删除功能?
核心思路
以全局数组goodsList为核心数据源,新增 / 编辑 / 删除本质是对数组的「增 / 改 / 删」操作,操作完成后重新渲染列表,实现「数据驱动视图」。
完整实现代码
步骤 1:HTML 结构(新增 / 编辑弹窗)
html
预览
<!-- 新增按钮 -->
<button class="add-btn">新增商品</button>
<!-- 新增/编辑弹窗(默认隐藏) -->
<div class="form-modal" style="display: none; margin: 20px 0;">
<input type="text" id="goods-name" placeholder="请输入商品名称" required>
<input type="number" id="goods-price" placeholder="请输入商品单价" min="0.01" step="0.01">
<input type="number" id="goods-num" placeholder="请输入商品数量" min="1" step="1">
<button class="confirm-btn">确定</button>
<button class="cancel-btn">取消</button>
</div>
步骤 2:JS 逻辑(新增 / 编辑 / 删除)
javascript
运行
$(function () {
// 2. 绑定新增按钮事件:打开弹窗+重置状态
$(".add-btn").on("click", function () {
$(".form-modal").show();
editIndex = -1; // 标记为「新增状态」
// 清空输入框,避免残留编辑数据
$("#goods-name, #goods-price, #goods-num").val("");
});
// 3. 绑定取消按钮事件:关闭弹窗+清空输入
$(".cancel-btn").on("click", function () {
$(".form-modal").hide();
$("#goods-name, #goods-price, #goods-num").val("");
editIndex = -1;
});
// 4. 绑定确定按钮事件:新增/编辑核心逻辑
$(".confirm-btn").on("click", function () {
// 1. 获取并格式化输入值(trim去除首尾空格)
const name = $("#goods-name").val().trim();
const price = parseFloat($("#goods-price").val());
const num = parseInt($("#goods-num").val());
// 2. 严格输入校验(前端兜底,避免无效数据)
if (!name) {
alert("商品名称不能为空!");
return; // 终止执行
}
if (isNaN(price) || price <= 0) {
alert("商品单价必须是大于0的有效数字(如:99.99)!");
return;
}
if (isNaN(num) || num <= 0) {
alert("商品数量必须是大于0的整数(如:1/5/10)!");
return;
}
// 3. 判断:新增 OR 编辑
if (editIndex === -1) {
// 新增逻辑:生成唯一ID(基于现有最大ID+1)
const maxId = goodsList.length > 0
? Math.max(...goodsList.map(item => item.id))
: 0;
const newGoods = {
id: maxId + 1,
name: name,
price: price,
num: num
};
goodsList.push(newGoods); // 新增商品到数组
} else {
// 编辑逻辑:更新对应索引的商品数据
goodsList[editIndex].name = name;
goodsList[editIndex].price = price;
goodsList[editIndex].num = num;
}
// 4. 操作完成:关闭弹窗+清空输入+重新渲染
$(".form-modal").hide();
$("#goods-name, #goods-price, #goods-num").val("");
editIndex = -1;
renderGoodsList(); // 重新渲染列表
});
});
// 5. 删除商品函数(绑定到表格删除按钮)
function deleteGoods(index) {
// 确认删除,防止误操作
if (!confirm(`确定要删除【${goodsList[index].name}】吗?`)) {
return;
}
goodsList.splice(index, 1); // 从数组删除对应索引的商品
renderGoodsList(); // 重新渲染
}
// 6. 编辑商品函数(绑定到表格编辑按钮)
function editGoods(index) {
editIndex = index; // 标记为「编辑状态」
const currentGoods = goodsList[index];
// 回填数据到输入框
$("#goods-name").val(currentGoods.name);
$("#goods-price").val(currentGoods.price);
$("#goods-num").val(currentGoods.num);
$(".form-modal").show(); // 打开弹窗
}
// 7. 列表渲染函数(核心:将数组数据转为DOM)
function renderGoodsList() {
calculateTotal(); // 先计算总数/总价
const $tbody = $("tbody");
$tbody.empty(); // 清空原有列表,避免重复渲染
// 遍历数组,生成表格行
$.each(goodsList, function (index, item) {
const tr = `
<tr>
<td>${item.name}</td>
<td>¥${item.price.toFixed(2)}</td>
<td>
<button onclick="reduceNum(${index})">-</button>
<span>${item.num}</span>
<button onclick="addNum(${index})">+</button>
</td>
<td>
<button onclick="editGoods(${index})">编辑</button>
<button onclick="deleteGoods(${index})">删除</button>
</td>
</tr>
`;
$tbody.append(tr);
});
}
// 8. 数量加减函数(配套列表)
function addNum(index) {
goodsList[index].num += 1;
renderGoodsList();
}
function reduceNum(index) {
if (goodsList[index].num <= 1) {
alert("商品数量不能小于1!");
return;
}
goodsList[index].num -= 1;
renderGoodsList();
}
关键细节说明
- 状态标记 :通过
editIndex区分「新增」(-1)和「编辑」(对应商品索引),避免新增 / 编辑逻辑混淆; - ID 生成:新增商品时基于现有最大 ID+1,保证 ID 唯一性;
- 输入校验 :不仅判断「非空」,还通过
isNaN判断是否为有效数字,避免用户输入非数字内容; - 用户体验 :删除时添加
confirm确认,数量减至 1 时给出提示,避免误操作; - 数据回填:编辑时将商品数据回填到输入框,提升编辑效率。
问题 3:如何实时计算并展示商品的总数量和总价格?
核心思路
封装「统计函数」,遍历商品数组累加「数量 × 单价」得到总价格,累加「数量」得到总数量;所有数据操作后(新增 / 编辑 / 删除 / 数量调整)调用该函数,实现实时更新。
完整实现代码
javascript
运行
// 统计总价格、总数量函数
function calculateTotal() {
let totalPrice = 0; // 总价格
let totalNum = 0; // 总数量
// 遍历数组累加数据
$.each(goodsList, function (index, item) {
totalPrice += item.price * item.num; // 单价×数量=单商品总价,累加
totalNum += item.num; // 数量累加
});
// 更新到页面(保留2位小数,符合金额展示规范)
$(".total-price").text(`¥${totalPrice.toFixed(2)}`);
$(".total-num").text(totalNum);
}
// HTML统计区域(表格tfoot)
<tfoot>
<tr>
<th colspan="2">总价格:</th>
<th colspan="2" class="total-price">¥0.00</th>
</tr>
<tr>
<th colspan="2">总数量:</th>
<th colspan="2" class="total-num">0</th>
</tr>
</tfoot>
关键细节说明
- 触发时机 :
calculateTotal()需在renderGoodsList()开头调用,确保每次渲染列表前先更新统计数据; - 金额格式化 :使用
toFixed(2)保留 2 位小数,避免出现1999.999999等不规范的金额格式; - 初始值 :统计变量初始化为 0,避免数组为空时出现
NaN; - 遍历方式 :使用
$.each()遍历数组,兼容低版本浏览器,新手更易理解。
四、完整源码整合(可直接运行)
html
预览
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jQuery商品列表CRUD</title>
<script src="./js/jquery-3.7.1.min.js"></script>
<style>
/* 基础样式优化 */
.goods-table {
width: 800px;
margin: 20px auto;
border-collapse: collapse;
text-align: center;
}
.goods-table th, .goods-table td {
border: 1px solid #333;
padding: 10px;
}
.form-modal {
width: 800px;
margin: 20px auto;
}
.form-modal input {
margin-right: 10px;
padding: 5px;
}
.btn-group {
width: 800px;
margin: 0 auto;
}
button {
padding: 5px 10px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="btn-group">
<button class="add-btn">新增商品</button>
</div>
<!-- 新增/编辑弹窗 -->
<div class="form-modal" style="display: none;">
<input type="text" id="goods-name" placeholder="请输入商品名称">
<input type="number" id="goods-price" placeholder="请输入商品单价" min="0.01" step="0.01">
<input type="number" id="goods-num" placeholder="请输入商品数量" min="1" step="1">
<button class="confirm-btn">确定</button>
<button class="cancel-btn">取消</button>
</div>
<!-- 商品表格 -->
<table class="goods-table">
<thead>
<tr>
<th>商品名称</th>
<th>单价</th>
<th>数量</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<th colspan="2">总价格:</th>
<th colspan="2" class="total-price">¥0.00</th>
</tr>
<tr>
<th colspan="2">总数量:</th>
<th colspan="2" class="total-num">0</th>
</tr>
</tfoot>
</table>
<script>
// 全局变量
let goodsList = [];
let editIndex = -1;
// 页面加载完成后执行
$(function () {
// 1. 加载初始数据
loadInitialData();
// 2. 绑定事件
bindEvents();
});
// 1. 加载初始JSON数据
function loadInitialData() {
$.ajax({
url: "./js/index.json",
type: "GET",
dataType: "json",
timeout: 5000,
success: function (res) {
goodsList = res;
renderGoodsList();
},
error: function (xhr, status, error) {
if (status === "timeout") {
alert("数据请求超时,请检查网络!");
} else if (xhr.status === 404) {
alert("未找到JSON文件,请检查路径:./js/index.json");
} else {
console.error("数据加载失败:", error);
alert("初始数据加载失败!");
}
}
});
}
// 2. 绑定所有事件
function bindEvents() {
// 新增按钮
$(".add-btn").on("click", function () {
$(".form-modal").show();
editIndex = -1;
$("#goods-name, #goods-price, #goods-num").val("");
});
// 取消按钮
$(".cancel-btn").on("click", function () {
$(".form-modal").hide();
$("#goods-name, #goods-price, #goods-num").val("");
editIndex = -1;
});
// 确定按钮(新增/编辑)
$(".confirm-btn").on("click", function () {
const name = $("#goods-name").val().trim();
const price = parseFloat($("#goods-price").val());
const num = parseInt($("#goods-num").val());
// 输入校验
if (!name) {
alert("商品名称不能为空!");
return;
}
if (isNaN(price) || price <= 0) {
alert("商品单价必须是大于0的有效数字!");
return;
}
if (isNaN(num) || num <= 0) {
alert("商品数量必须是大于0的整数!");
return;
}
// 新增/编辑逻辑
if (editIndex === -1) {
// 新增
const maxId = goodsList.length > 0 ? Math.max(...goodsList.map(item => item.id)) : 0;
goodsList.push({
id: maxId + 1,
name: name,
price: price,
num: num
});
} else {
// 编辑
goodsList[editIndex].name = name;
goodsList[editIndex].price = price;
goodsList[editIndex].num = num;
}
// 重置状态
$(".form-modal").hide();
$("#goods-name, #goods-price, #goods-num").val("");
editIndex = -1;
// 重新渲染
renderGoodsList();
});
}
// 3. 渲染商品列表
function renderGoodsList() {
calculateTotal(); // 先统计
const $tbody = $("tbody");
$tbody.empty();
$.each(goodsList, function (index, item) {
const tr = `
<tr>
<td>${item.name}</td>
<td>¥${item.price.toFixed(2)}</td>
<td>
<button onclick="reduceNum(${index})">-</button>
<span>${item.num}</span>
<button onclick="addNum(${index})">+</button>
</td>
<td>
<button onclick="editGoods(${index})">编辑</button>
<button onclick="deleteGoods(${index})">删除</button>
</td>
</tr>
`;
$tbody.append(tr);
});
}
// 4. 统计总价格/总数量
function calculateTotal() {
let totalPrice = 0;
let totalNum = 0;
$.each(goodsList, function (index, item) {
totalPrice += item.price * item.num;
totalNum += item.num;
});
$(".total-price").text(`¥${totalPrice.toFixed(2)}`);
$(".total-num").text(totalNum);
}
// 5. 数量加减
function addNum(index) {
goodsList[index].num += 1;
renderGoodsList();
}
function reduceNum(index) {
if (goodsList[index].num <= 1) {
alert("商品数量不能小于1!");
return;
}
goodsList[index].num -= 1;
renderGoodsList();
}
// 6. 编辑商品
function editGoods(index) {
editIndex = index;
const item = goodsList[index];
$("#goods-name").val(item.name);
$("#goods-price").val(item.price);
$("#goods-num").val(item.num);
$(".form-modal").show();
}
// 7. 删除商品
function deleteGoods(index) {
if (!confirm(`确定删除【${goodsList[index].name}】吗?`)) {
return;
}
goodsList.splice(index, 1);
renderGoodsList();
}
</script>
</body>
</html>
五、进阶优化建议(提升实战价值)
-
本地存储 :添加
localStorage,将goodsList数据持久化,刷新页面后数据不丢失;javascript
运行
// 示例:保存数据到本地 function saveToLocal() { localStorage.setItem("goodsList", JSON.stringify(goodsList)); } // 示例:从本地加载数据 function loadFromLocal() { const data = localStorage.getItem("goodsList"); if (data) goodsList = JSON.parse(data); } -
表单美化:使用 CSS / 第三方 UI 库优化弹窗样式,添加输入校验的视觉提示(如红色边框);
-
防抖处理:数量加减按钮添加防抖,避免快速点击导致数据异常;
-
对接后端:将 AJAX 请求改为对接真实后端接口(如 POST/DELETE/PUT),实现前后端交互;
-
分页功能:当商品数量较多时,实现分页渲染,提升页面性能。
六、核心知识点总结
1. AJAX 加载 JSON 核心
$.ajax()的核心参数:url/type/dataType/success/error;- 错误分类处理:超时、404 等场景需针对性提示;
- 路径规范:本地 JSON 使用相对路径,避免跨域问题。
2. CRUD 功能核心
- 数据驱动视图:所有操作围绕数组展开,操作后重新渲染;
- 状态标记:通过
editIndex区分新增 / 编辑,避免逻辑混淆; - 输入校验:前端兜底校验,保证数据合法性。
3. 实时统计核心
- 统计函数封装:遍历数组累加数据,统一触发时机;
- 金额格式化:
toFixed(2)保证金额展示规范; - 联动更新:统计函数需在所有数据操作后调用。
总结
本文从「问题拆解」角度,完整实现了 jQuery 商品列表的 CRUD 与实时统计,核心是抓住「数组作为核心数据源,操作数组后重新渲染视图」的逻辑。代码兼顾「可运行性」与「可维护性」,新增了错误处理、用户体验优化等实战细节,不仅能帮助新手掌握 jQuery 核心语法,更能理解前端「数据驱动视图」的底层思想。在此基础上,你可结合进阶优化建议,进一步提升项目的实战价值。