主题
跨域与封装一个 Ajax 函数(Day15 2/2)
本页对应 Day15_Ajax,共 6 段视频,约 1 小时 53 分。
学习目标
- 能够解释同源策略与跨域限制,并使用 JSONP 实现搜索框提示案例,验证通过动态
script请求、回调函数接收并解析响应数据。 - 能够封装一个通用 Ajax 函数,支持 GET、POST、DELETE 请求,设置请求体与响应类型,并区分成功、业务失败和网络失败回调。
- 能够使用 Ajax 获取指定用户账单数据,判断响应
code,通过模板字符串和 DOM 操作将账单动态渲染到页面,并正确显示收入/支出样式与日期信息。 - 能够监听表单提交事件并阻止默认提交,使用
FormData作为请求体发送 POST 请求添加账单,并在添加成功后重新加载账单列表。 - 能够使用事件委托监听动态生成的删除按钮,通过自定义属性获取账单 ID,发送 DELETE 请求,并在后端确认删除成功后从页面移除对应元素。
知识点
同源策略:为什么需要跨域方案
浏览器有同源策略:协议、域名、端口三者相同才算同源。不同源时,浏览器会限制页面读取响应内容。转写稿里说得很直白:服务器本身通常是“来者不拒”的,真正限制的是浏览器。所以当我们在自己的页面里想拿百度搜索提示数据时,直接用普通 Ajax 请求会被浏览器拦住。
但 script 标签是个特殊入口:页面可以加载不同源的 JS 文件,服务器返回的 JS 又会在当前页面执行。JSONP 就是利用这一点:不直接“读响应”,而是让服务器返回一段“调用前端函数的 JS 代码”。这段代码执行时,就把数据交给了前端。
JSONP 怎么想:动态 script + 全局回调
先看百度搜索框的现象:输入 A,页面会提示 A 开头的搜索词。打开 Network 能看到它向某个地址发了请求;直接把请求地址复制到浏览器打开,响应不是 JSON 对象,而是一段类似:
js
parseSearch({ ... });也就是说,后端返回的是“调用 parseSearch 这个函数,并把数据作为实参传进来”。前端只要提前定义好 parseSearch,等这段 JS 被 script 加载执行,函数就会运行,实参就是数据。
因此 JSONP 的关键不是 XMLHttpRequest,而是:
- 前端动态创建
script; - 设置
src,把回调函数名传给后端; - 后端返回
函数名(数据); - 前端提前定义同名全局函数来接收数据。
转写稿里拆百度地址时,关键参数是 wd(关键字)和 cb(回调函数名)。地址形如:
text
https://www.baidu.com/su?wd=关键字&cb=parseSearch怎么做:搜索框提示案例
页面结构用 input 的 list 属性关联 datalist:
html
<input id="wd" list="datalist">
<datalist id="datalist"></datalist>回调函数必须是全局的,不能包在局部作用域里,否则后端返回的 parseSearch(...) 找不到它:
js
function parseSearch(res) {
console.log(res);
const datalist = document.querySelector('#datalist');
datalist.innerHTML = '';
res.g.forEach(function (item) {
const option = new Option('', item.q);
datalist.appendChild(option);
});
}绑定输入事件时,转写稿特意强调不要用 change,因为 change 往往要等失去焦点才触发;搜索提示要随着输入实时变化,应该用 input 事件:
js
const input = document.querySelector('#wd');
input.addEventListener('input', function () {
const script = document.createElement('script');
script.src = `https://www.baidu.com/su?wd=${encodeURIComponent(input.value)}&cb=parseSearch`;
document.body.appendChild(script);
document.body.removeChild(script);
});添加 script 后马上移除,不会影响已经发出的请求;移除只是不留 DOM 痕迹。
调试过程与易错点
调试时先看 Network:输入内容后是否发出 su?wd=...&cb=parseSearch 请求;再看响应,确认返回的是不是 parseSearch({...})。控制台里 console.log(res) 应看到对象,数据在 res.g 数组里,每项对象的 q 就是关键字。遍历 res.g,把 item.q 塞进 datalist 即可。
易错点:
- 回调函数必须是全局函数,不能写在局部函数里;
- 每次回调先清空
datalist.innerHTML,否则旧提示会叠加; - 参数要
encodeURIComponent,避免关键字里的特殊字符破坏 URL; - JSONP 只能发 GET,不适合提交敏感或大量数据;
- 百度接口可能变化,案例重点是理解 JSONP 原理,不要把某个地址当成永久可用接口;
- 动态
script的请求错误不容易像 Ajax 那样精细捕获,这是 JSONP 的局限。
这样,一个不刷新页面、从百度拿到搜索提示并显示到 datalist 的 JSONP 案例就完成了。
封装 Ajax 函数:从重复代码到可复用工具
为什么需要:在记账本案例里,获取账单、添加账单、删除账单都要发请求。如果每次都用 new XMLHttpRequest() 从头写一遍,重复且容易漏步骤。封装后,调用方只关心“请求地址、方式、参数、成功/失败后做什么”。
怎么想:用一个 options 对象接收配置;常用项给默认值;成功和失败用回调传出去,让封装函数不绑定具体业务。
怎么做:先设计选项:url、method、headers、body、dataType、success、error。其中 method 默认 GET,headers 默认 {},success、error 默认空函数。
易错点:默认值要写在解构里;headers 不能默认成 null,否则后面 for...in 会出问题;回调默认空函数可避免调用方不传时报错。
创建 xhr 与响应类型
为什么需要:XMLHttpRequest 是 Ajax 的核心对象;响应类型决定 xhr.response 是字符串还是对象。记账本调试时就踩过坑:后端返回 JSON 字符串,代码却直接写 res.code,结果判断失败。
怎么想:先创建 xhr;如果调用方传了 dataType,就设置 xhr.responseType = dataType。例如传 dataType: 'json',xhr.response 才是对象。
怎么做:按笔记代码保留如下实现:
js
/*
选项:
url: 请求地址
method: 请求方式,默认值 GET
headers: 请求头,默认值 {}
body: 请求体
dataType: 响应体类型
success: 成功的回调
error: 失败的回调
*/
function ajax(options) {
// 从 options 取出相关的选项
const {url, method='GET', headers={}, body, dataType, success=()=>{}, error=()=>{}} = options;
// 创建 xhr 对象
const xhr = new XMLHttpRequest();
// 如果指定了 dataType
if (dataType) {
xhr.responseType = dataType;
}
// 监听响应成功的事件
xhr.onload = () => {
if (xhr.status === 200) {
success(xhr.response);
} else {
error();
}
}
// 监听响应失败的事件
xhr.onerror = error;
// 请求初始化
xhr.open(method, url);
// 设置请求头
for (let key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
// 发送
xhr.send(body);
}易错点:dataType 不传时不要强行设置;设置 responseType 后,xhr.response 的类型会变化;若后端返回 JSON 但没设 dataType,要自己 JSON.parse,否则 res.code 取不到。
监听成功与失败
为什么需要:请求发出后,结果要么成功、要么失败;调用方需要拿到结果做页面更新或提示。
怎么想:xhr.onload 表示响应回来了,但还要看 HTTP 状态码;xhr.onerror 表示请求本身失败。笔记里用 xhr.status === 200 判断成功,否则走 error()。
怎么做:成功时把 xhr.response 传给 success;失败时调用 error。调用方可以这样用:success(res){ if(res.code==='0000'){...} else {...} }。记账本添加账单时,正是忘了 dataType:'json',导致 res 是字符串,res.code 判断失败,补上响应类型后才正常。
易错点:onload 不等于一定成功;onerror 与“业务失败”不是一回事,业务失败通常仍在 onload 里根据 code 判断;status 只认 200 是当前封装的约定,接口若返回 201/204 要相应调整。
初始化、请求头与发送
为什么需要:open 负责确定方法和地址;setRequestHeader 负责加请求头;send 才真正发出请求。顺序错了会报错。
怎么想:先 open,再设请求头,最后 send。请求头来自 headers 对象,用 for...in 逐个设置。
怎么做:xhr.open(method, url);遍历 headers 调 setRequestHeader;xhr.send(body)。GET 通常不传 body;POST 可传 FormData。记账本删除时用 DELETE,并在后端确认删除成功后再删页面元素,避免“糊弄用户”。
易错点:setRequestHeader 要放在 open 之后、send 之前;body 为 FormData 时一般不要手动设 Content-Type;method 要写对,GET/POST/DELETE 不能混。
调试过程带来的提醒
为什么需要:封装函数本身简单,真正容易错的是调用时的类型和时机。
怎么想:先看网络面板确认请求地址、方式、请求体、响应体;再看控制台确认 xhr.status 和 xhr.response 类型。
怎么做:记账本添加失败时,先输出 xhr.status 发现是 200,再输出 res 发现是字符串,于是调用时补 dataType:'json',res.code === '0000' 才成立。删除时则先发请求,成功回调里再 remove 元素。
易错点:不要把字符串当对象用;不要请求没成功就先改页面;不要只写成功分支,失败和 onerror 都要提示。
JSONP 搜索框提示:为什么需要、怎么想、怎么做、易错点
为什么需要:浏览器有同源策略,普通 Ajax 不能随便读别的域名数据;但 <script> 不受这个限制,JSONP 就借 script 发 GET 请求。转写稿里百度搜索框就是典型:输入关键字后,页面动态创建 script,地址里带 wd(关键字)和 cb(回调函数名),后端返回“调用函数”的 JS 代码。
怎么想:先在前端定义一个全局函数,后端响应 parseSearch({...}) 时就能执行;函数实参就是数据对象。
怎么做:监听输入框的 input 事件,创建 script,设置 src,插入页面,请求发出后再删除 script。
js
function parseSearch(data) {
console.log(data);
}
inputBox.addEventListener('input', () => {
const script = document.createElement('script');
// 转写稿拆出的关键参数:wd 是关键字,cb 是回调函数名
script.src = `${搜索提示地址}?wd=${inputBox.value}&cb=parseSearch`;
document.body.appendChild(script);
document.body.removeChild(script);
});拿到数据后,把提示词创建成 option 放进 datalist,并让 input 的 list 与 datalist 的 id 对应;每次渲染前先清空 datalist。
易错点:回调函数必须是全局的;用 change 不如 input 及时;script 删除前请求已经发出;new Option(text, value) 的第一个参数是显示文字,第二个才是 value;datalist 不清空会不断累加。
记账本项目分析:为什么需要、怎么想、怎么做、易错点
为什么需要:传统页面跳来跳去,后端既管数据又管页面;Ajax 记账本要让页面不刷新,前端只负责发请求、渲染。
怎么想:后端已提供 API,前端把后端当“黑盒”。转写稿明确三个接口:GET /api/account/用户ID 获取指定用户账单;POST /api/account/用户ID 添加账单;DELETE /api/account/账单ID 删除账单。路径相似、请求方式不同,这是 REST 风格约定。
怎么做:在 index.ejs 中写前端 JS,引入封装好的 Ajax 函数;注意模板中的用户 ID 是后端 EJS 替换后发给浏览器的。
易错点:用户 ID 和账单 ID 不要混;API 文档要看请求方式、参数、响应结构;不要以为 EJS 里的 JS 在后端执行,它最终在浏览器执行。
获取数据:为什么需要、怎么想、怎么做、易错点
为什么需要:页面初始账单不能写死,要从数据库按用户取。
怎么想:发 GET 请求,响应一般是 { code, message, data };code === '0000' 才成功,data 才是账单数组。
怎么做:封装 loadAccount(),先清空包裹元素,再请求并遍历 res.data,用模板字符串拼 HTML,最后赋给 innerHTML。
js
function loadAccount() {
accountBooks.innerHTML = '';
// 调用 ajax:GET /api/account/用户ID
// success 中判断 res.code === '0000',再遍历 res.data 渲染
}易错点:转写稿调试时添加失败,根因是忘了指定 responseType: 'json',拿到的 res 是字符串,res.code 自然不对;失败和请求未发出都要提示,可用 bootbox 弹框。
添加数据:为什么需要、怎么想、怎么做、易错点
为什么需要:点“添加”不能跳页面,要真正写数据库。
怎么想:监听表单 submit,阻止默认提交;用 FormData 收集表单内容,POST 到指定用户路径;成功后重新加载列表。
怎么做:
js
const fd = new FormData(addForm);
// ajax:POST /api/account/用户ID,请求体 fd
// success 中判断 res.code === '0000',提示添加成功并调用 loadAccount()易错点:一定 preventDefault();FormData 默认不需要手动设 Content-Type;添加成功后不能只提示,要刷新数据,否则页面看不到新记录。转写稿还出现后端未正确解析 FormData 的问题,前端请求体正常时不要误判为前端错。
删除账单:为什么需要、怎么想、怎么做、易错点
为什么需要:删除要真删数据库,不能只删页面元素糊弄用户。
怎么想:删除按钮是动态生成的,用事件委托绑到包裹元素;渲染时给删除按钮加 data-id 保存账单 ID;点击后发 DELETE,确认 code === '0000' 再删 DOM。
怎么做:
js
accountBooks.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-btn')) {
const id = e.target.dataset.id;
// ajax:DELETE /api/account/账单ID
// 成功后沿 parentElement 找到账单项并 removeChild
}
});易错点:不要提前删元素;classList.contains 判断类名;账单 ID 每个都不同,必须从 data-id 取;删除成功后可按模板层级用 parentElement 找到整条账单再删。
添加后的列表刷新:为什么需要、怎么想、怎么做、易错点
为什么需要:账单按时间排序,新记录不一定加在末尾,直接 append 会乱。
怎么想:要么 location.reload(),要么不刷新页面,重新执行“清空 + 请求 + 渲染”。转写稿选择封装 loadAccount()。
怎么做:添加成功后调用 loadAccount();函数内先 accountBooks.innerHTML = '' 再请求渲染。
易错点:清空要写在渲染前;转写稿曾把 innerHTML 写错导致添加两遍;删除和添加都要以数据库结果为准,再更新页面。
课堂画面






代码示例
示例 1:封装一个 Ajax 函数(可直接运行的 HTML 页面)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>封装 ajax 函数</title>
<style>
body { font-family: sans-serif; padding: 24px; }
button { padding: 6px 16px; }
#result { margin-top: 16px; padding: 12px; background: #f5f5f5; white-space: pre-wrap; }
</style>
</head>
<body>
<button id="btn">请求 /api/user</button>
<div id="result">尚未请求</div>
<script>
/*
选项:
url: 请求地址
method: 请求方式,默认值 GET
headers: 请求头,默认值 {}
body: 请求体
dataType: 响应体类型
success: 成功的回调
error: 失败的回调
*/
function ajax(options) {
// 从 options 取出相关的选项
const { url, method = 'GET', headers = {}, body, dataType, success = () => {}, error = () => {} } = options;
// 创建 xhr 对象
const xhr = new XMLHttpRequest();
// 如果指定了 dataType
if (dataType) {
xhr.responseType = dataType;
}
// 监听响应成功的事件
xhr.onload = () => {
if (xhr.status === 200) {
success(xhr.response);
} else {
error();
}
};
// 监听响应失败的事件
xhr.onerror = error;
// 请求初始化
xhr.open(method, url);
// 设置请求头
for (let key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
// 发送
xhr.send(body);
}
</script>
<script>
document.querySelector('#btn').onclick = () => {
ajax({
url: '/api/user', // 换成你自己的接口地址
method: 'GET',
dataType: 'json', // 关键:让 xhr.response 直接是对象
success(res) {
document.querySelector('#result').textContent = JSON.stringify(res, null, 2);
},
error() {
document.querySelector('#result').textContent = '请求失败';
}
});
};
</script>
</body>
</html>关键行与易错点
const { url, method = 'GET', ... } = options;:用解构默认值一次性拿到所有选项,调用时不必写全。xhr.responseType = dataType;:只有设置了dataType: 'json',回调里拿到的xhr.response才是对象;不设置就是字符串,后面res.code永远取不到值(记账本里“明明成功却走了失败分支”就是这个坑)。该属性必须在send()之前设置。xhr.onload里判断xhr.status === 200才走success,其它状态码走error;而xhr.onerror处理的是“请求根本没发出去/网络断开”这种情况,两条路都要有回调。success = () => {}、error = () => {}:默认空函数,避免调用方不传回调时报not a function。for (let key in headers)必须在xhr.open()之后调用,open之前setRequestHeader会报错。- 如果
body是FormData对象,不要手动设置Content-Type,浏览器会自动带上 boundary,手写反而导致后端解析不出数据。
示例 2:JSONP 搜索框提示(datalist + 动态 script)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>JSONP 搜索框提示</title>
<style>
body { font-family: sans-serif; padding: 24px; }
#wd { width: 320px; padding: 6px 10px; }
</style>
</head>
<body>
<!-- list 的值必须和 datalist 的 id 一致,两者才会关联 -->
<input type="text" id="wd" list="searchList" autocomplete="off">
<datalist id="searchList"></datalist>
<script>
const inputBox = document.querySelector('#wd');
const dataListBox = document.querySelector('#searchList');
// 这个函数必须是全局的,后端返回的脚本里才能调用到
function parseSearch(res) {
// 每次响应先清空,否则提示词会不断累加
dataListBox.innerHTML = '';
res.g.forEach(item => {
// new Option(text, value),文字留空,value 就是提示词
dataListBox.appendChild(new Option('', item.q));
});
}
inputBox.oninput = function () {
if (!this.value) {
dataListBox.innerHTML = '';
return;
}
// 第一步:动态创建 script 元素
const script = document.createElement('script');
// 第二步:设置 src(wd 是关键字,cb 是前端定好的函数名)
script.src = `https://www.baidu.com/sugrec?wd=${encodeURIComponent(this.value)}&cb=parseSearch`;
// 第三步:添加到页面,请求才会真正发出
document.body.appendChild(script);
// 第四步:请求已发出,删掉标签不留痕迹
document.body.removeChild(script);
};
</script>
</body>
</html>关键行与易错点
- 监听事件用
input而不是change:change要内容改变且失去焦点才触发,而百度那种是边打字边提示。 parseSearch必须挂在全局(不能包在 IIFE 或模块作用域里),否则后端响应回来的parseSearch({...})执行时找不到函数,直接报is not defined。cb=parseSearch里的函数名是前端定的,后端只是把这段名字原样拼进响应的 JS 代码里;改函数名就要同步改cb参数。script.src里拼关键字要encodeURIComponent,中文/特殊字符不编码会拼出非法 URL。appendChild之后请求就发出去了,removeChild删的是标签,不影响已发出的请求——这就是“不留痕迹”的原因。- 每次回调进来先
dataListBox.innerHTML = ''清空,否则每输入一次就往 datalist 里再塞一批,提示项越堆越多。 new Option('', item.q):第一个参数是显示文字,第二个是 value;datalist 里文字为空时会显示 value,所以提示词能正常显示。
示例 3:Ajax 记账本前端(渲染 + 删除 + 添加,页面自始至终不刷新)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Ajax 记账本</title>
<style>
body { font-family: sans-serif; padding: 24px; }
.panel { border: 1px solid #ccc; border-radius: 4px; margin-bottom: 10px; position: relative; }
.panel-heading { padding: 8px 12px; background: #f5f5f5; }
.panel-body { padding: 8px 12px; }
.panel-success .panel-heading { background: #dff0d8; }
.panel-danger .panel-heading { background: #f2dede; }
.remove-btn { position: absolute; top: 6px; right: 10px; cursor: pointer; font-weight: bold; }
#addForm { margin-top: 20px; }
#addForm input, #addForm select { display: block; margin: 6px 0; padding: 4px 8px; }
</style>
</head>
<body>
<h3>账单列表</h3>
<div id="accountBooks"></div>
<form id="addForm">
<input type="text" name="title" placeholder="事项">
<input type="text" name="remarks" placeholder="备注">
<input type="number" name="account" placeholder="金额">
<select name="type">
<option value="1">收入</option>
<option value="-1">支出</option>
</select>
<input type="date" name="time">
<button type="submit">添加</button>
</form>
<!-- 上一段封装的 ajax 函数,放在 public/js/ajax4.js -->
<script src="/ajax4.js"></script>
<script>
// 由后端模板渲染替换进来的当前用户 ID(单独运行时手动改成真实 ID)
const userId = '<%= user.id %>';
const accountBooks = document.querySelector('#accountBooks'); // 账单列表包裹元素
const addForm = document.querySelector('#addForm');
// 请求并把账单渲染到页面上
function loadAccount() {
ajax({
url: '/api/account/' + userId,
dataType: 'json',
success(res) {
if (res.code !== '0000') {
alert('数据获取失败');
return;
}
// 遍历前先清空,否则数据会累加
accountBooks.innerHTML = '';
res.data.forEach(item => {
accountBooks.innerHTML += `
<div class="panel ${item.type > 0 ? 'panel-success' : 'panel-danger'}">
<div class="panel-heading">
${item.title}
<span class="remove-btn" data-id="${item._id}">×</span>
</div>
<div class="panel-body">
<p>${item.type > 0 ? '<span>收入</span>' : '<span>支出</span>'}</p>
<p>备注:${item.remarks}</p>
<p>金额:${item.account}</p>
<p>时间:${item.time}</p>
</div>
</div>`;
});
},
error() {
alert('数据获取失败');
}
});
}
loadAccount();
// 删除:事件委托(按钮是动态生成的)
accountBooks.addEventListener('click', function (e) {
if (!e.target.classList.contains('remove-btn')) return;
ajax({
url: '/api/account/' + e.target.dataset.id,
method: 'DELETE',
dataType: 'json',
success(res) {
if (res.code === '0000') {
// 确认后端删掉了,再删页面元素
accountBooks.removeChild(e.target.parentElement.parentElement);
alert('删除成功');
} else {
alert('删除失败');
}
},
error() {
alert('删除失败');
}
});
});
// 添加:监听表单提交事件并阻止默认提交
addForm.addEventListener('submit', function (e) {
e.preventDefault();
const fd = new FormData(this); // 表单内容直接作为请求体
ajax({
url: '/api/account/' + userId,
method: 'POST',
body: fd,
dataType: 'json',
success(res) {
if (res.code === '0000') {
alert('添加成功');
loadAccount(); // 重新拉数据,保证顺序正确
} else {
alert('添加失败');
}
},
error() {
alert('添加失败');
}
});
this.reset();
});
</script>
</body>
</html>关键行与易错点
dataType: 'json'必须写:后端返回的是 JSON 字符串,不指定的话res是字符串,res.code得到undefined,于是“添加成功”却弹“添加失败”——这是转写稿里真实踩过的坑。e.preventDefault():表单里有type="submit"的按钮,不阻止默认行为页面就会跳转刷新,Ajax 也就失去意义了。- 渲染前
accountBooks.innerHTML = '':清空再拼;如果写成accountBooks.innerHTML += ...而不清空,每请求一次列表就翻倍(转写稿里“怎么加载了两遍”就是这个原因)。 - 模板字符串里
${item.type > 0 ? 'panel-success' : 'panel-danger'}和收入/支出文字的三元判断,${}里只能写表达式,不能写if语句。 data-id="${item._id}":每个账单 ID 都不一样,渲染时用自定义属性把它存到删除按钮上,删除时通过e.target.dataset.id取回。- 事件委托绑在
accountBooks上而不是每个×上:列表元素是动态生成的,直接给按钮绑事件会绑不到;判断用e.target.classList.contains('remove-btn'),不要用className。 - 删除必须先等后端返回
code === '0000'再删 DOM,否则数据库没删、页面却没了,就是糊弄用户;removeChild(e.target.parentElement.parentElement)删的是panel那一层,层级要数清楚。 - 添加成功后调用
loadAccount()重新请求:列表是按时间倒序排的,自己插到页面里位置不一定对,重新拉一遍最稳。 new FormData(this)作为body时,headers不要传Content-Type,浏览器会自己带 boundary。const userId = '<%= user.id %>':这是后端模板引擎在返回页面之前替换好的,浏览器拿到时已经是具体 ID 了——不要以为浏览器有这本事。
常见错误
- JSONP 回调报
xxx is not defined→ 回调函数写在局部作用域或请求发出后才定义 → 请求前把回调挂到全局(如window.parseSearch),并确保函数名与cb参数一致。 - 多次输入后页面残留多个
script、回调重复执行 → 只append未清理,或未处理重复请求 → 请求发出后立即移除script,必要时清理旧节点并做输入防抖。 datalist提示项越积越多 → 每次只追加option,没有清空旧选项 → 渲染前先datalist.innerHTML = '',再追加新option。res.code判断永远失败、res像对象却取不到属性 → 未设置responseType = 'json',拿到的是字符串 → 设置xhr.responseType = 'json'或显式JSON.parse,并确认后端返回确是 JSON。- 添加成功后列表重复或看不到新记录 → 重载函数被调用两次、容器未清空、未重新请求 → 重载前清空列表容器,确保只调用一次,按后端排序重新渲染。
- 删除提示成功但元素还在,或刷新后才消失 → 只删 DOM 未等后端成功,或只发请求未删 DOM → 先等后端返回成功码,再
removeChild目标行;失败时不要改 DOM。 - 动态生成的删除按钮点不动 → 直接给新增元素绑事件或绑定时元素还不存在 → 用事件委托绑到包裹元素,通过
e.target.classList.contains(...)判断目标。 - 点“添加”页面刷新/跳转 → 表单默认提交未被阻止 → 在
submit回调中e.preventDefault(),再用FormData发 Ajax 请求。
练习
- 给出一段 JSONP 搜索提示代码:回调定义在局部、
script未移除、datalist未清空。请指出三处问题并改写关键代码。 - 改写
ajax封装:要求同时区分“网络失败”“HTTP 状态非 200”“业务code !== '0000'”三种情况,并说明各自触发哪个回调。 - 记账本新增账单后出现“列表重复/顺序错乱/新记录不显示”。请设计一个重载列表函数,并说明清空容器与重新请求的先后顺序。
- 删除按钮是动态渲染的。请写出“事件委托 + 请求成功后再删 DOM”的伪代码,并说明为什么不能先删 DOM 再发请求。
- 用
FormData提交添加账单表单:列出关键步骤,并写出两个必须检查的失败分支。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | jsonp案例-搜索框提示 | 18:31 |
| 2 | Ajax记账本分析 | 12:52 |
| 3 | Ajax记账本-获取数据 | 34:14 |
| 4 | Ajax记账本-添加数据 | 25:32 |
| 5 | Ajax记账本-删除账单 | 12:29 |
| 6 | Ajax记账本-添加账单02 | 09:57 |
小结
- JSONP 的本质仍是
script跨域,回调必须是请求前可访问的全局函数,且只适合 GET 场景。 - Ajax 封装不能只看
onload,必须同时覆盖网络失败、HTTP 失败和业务码失败。 responseType与后端实际响应必须匹配,否则最容易出现“看着像对象、实际是字符串”的判断错误。- 记账本不跳转的核心是:页面不动,请求照发,成功后局部重载或局部删改 DOM。
- 新增后要“先清空再重载”,否则会出现重复渲染;删除要“后端成功后再删 DOM”,否则会糊弄用户。
- 动态元素不要逐个绑事件,事件委托更稳;表单提交必须先阻止默认行为。
- 前后端模板混用时,模板替换发生在服务端,前端拿到的已是替换后的代码;调试时要分清问题在前端请求、后端解析还是模板渲染。