Ajax数据交换格式与跨域处理
一、Ajax数据交换格式
1. XML (eXtensible Markup Language)
XML是一种标记语言,类似于HTML但更加灵活,允许用户自定义标签。
特点:
- 可扩展性强
- 结构清晰
- 数据与表现分离
- 文件体积相对较大
示例代码:XML数据格式
xml
<students>
<student>
<id>1</id>
<name>张三</name>
<age>20</age>
<major>计算机科学</major>
</student>
<student>
<id>2</id>
<name>李四</name>
<age>22</age>
<major>软件工程</major>
</student>
</students>
使用Ajax获取XML数据:
javascript
// 创建XMLHttpRequest对象
let xhr = new XMLHttpRequest();
// 配置请求
xhr.open('GET', 'students.xml', true);
// 设置响应类型
xhr.responseType = 'document';
// 发送请求
xhr.send();
// 处理响应
xhr.onload = function() {
if (xhr.status === 200) {
// 获取XML文档对象
let xmlDoc = xhr.responseXML;
// 获取所有student节点
let students = xmlDoc.getElementsByTagName('student');
// 遍历处理数据
for (let i = 0; i < students.length; i++) {
let id = students[i].getElementsByTagName('id')[0].textContent;
let name = students[i].getElementsByTagName('name')[0].textContent;
let age = students[i].getElementsByTagName('age')[0].textContent;
let major = students[i].getElementsByTagName('major')[0].textContent;
console.log(`ID: ${id}, 姓名: ${name}, 年龄: ${age}, 专业: ${major}`);
}
}
};
2. JSON (JavaScript Object Notation)
JSON是一种轻量级的数据交换格式,易于人阅读和编写,也易于机器解析和生成。
特点:
- 轻量级,文件体积小
- 易于解析
- 与JavaScript原生兼容
- 支持多种数据类型(字符串、数字、布尔值、数组、对象、null)
示例代码:JSON数据格式
json
{
"students": [
{
"id": 1,
"name": "张三",
"age": 20,
"major": "计算机科学"
},
{
"id": 2,
"name": "李四",
"age": 22,
"major": "软件工程"
}
]
}
使用Ajax获取JSON数据:
javascript
// 创建XMLHttpRequest对象
let xhr = new XMLHttpRequest();
// 配置请求
xhr.open('GET', 'students.json', true);
// 发送请求
xhr.send();
// 处理响应
xhr.onload = function() {
if (xhr.status === 200) {
// 解析JSON数据
let data = JSON.parse(xhr.responseText);
// 遍历处理数据
data.students.forEach(student => {
console.log(`ID: ${student.id}, 姓名: ${student.name}, 年龄: ${student.age}, 专业: ${student.major}`);
});
}
};
使用fetch API获取JSON数据(现代方法):
javascript
fetch('students.json')
.then(response => {
if (!response.ok) {
throw new Error('网络响应不正常');
}
return response.json();
})
.then(data => {
data.students.forEach(student => {
console.log(`ID: ${student.id}, 姓名: ${student.name}, 年龄: ${student.age}, 专业: ${student.major}`);
});
})
.catch(error => {
console.error('获取数据时出错:', error);
});
二、无刷新列表分页
无刷新分页通过Ajax技术实现,只更新页面中需要变化的部分,而不是整个页面刷新。
实现步骤:
- 监听分页按钮点击事件
- 阻止默认行为(页面跳转)
- 发送Ajax请求获取分页数据
- 使用JavaScript动态更新页面内容
示例代码:无刷新分页实现
HTML部分:
html
<div id="product-list">
<!-- 产品列表将在这里动态加载 -->
</div>
<div class="pagination">
<a href="#" class="page-link" data-page="1">1</a>
<a href="#" class="page-link" data-page="2">2</a>
<a href="#" class="page-link" data-page="3">3</a>
<!-- 更多分页链接 -->
</div>
JavaScript部分:
javascript
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// 默认加载第一页
loadPage(1);
// 为所有分页链接添加点击事件
document.querySelectorAll('.page-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault(); // 阻止默认跳转行为
let page = this.getAttribute('data-page');
loadPage(page);
});
});
});
// 加载分页数据的函数
function loadPage(page) {
// 显示加载状态
document.getElementById('product-list').innerHTML = '<p>加载中...</p>';
// 使用fetch API获取数据
fetch(`api/products.php?page=${page}`)
.then(response => {
if (!response.ok) {
throw new Error('网络响应不正常');
}
return response.json();
})
.then(data => {
// 清空当前内容
let productList = document.getElementById('product-list');
productList.innerHTML = '';
// 检查是否有数据
if (data.length === 0) {
productList.innerHTML = '<p>没有找到产品</p>';
return;
}
// 创建产品列表HTML
let html = '<ul class="products">';
data.forEach(product => {
html += `
<li class="product-item">
<h3>${product.name}</h3>
<p>价格: ¥${product.price}</p>
<p>${product.description}</p>
</li>
`;
});
html += '</ul>';
// 更新DOM
productList.innerHTML = html;
// 更新活动分页链接样式
document.querySelectorAll('.page-link').forEach(link => {
link.classList.remove('active');
if (link.getAttribute('data-page') == page) {
link.classList.add('active');
}
});
})
.catch(error => {
console.error('获取数据时出错:', error);
document.getElementById('product-list').innerHTML = '<p>加载数据时出错,请稍后再试</p>';
});
}
三、跨域处理
1. 什么是跨域
跨域是指浏览器不能执行其他网站的脚本,它是由浏览器的同源策略造成的。
同源策略限制以下行为:
- Cookie、LocalStorage和IndexDB无法读取
- DOM无法获得
- AJAX请求不能发送
同源的定义:
- 协议相同(http/https)
- 域名相同
- 端口相同
2. JSONP (JSON with Padding)
JSONP是一种跨域解决方案,利用<script>
标签没有跨域限制的特性来实现。
实现原理:
- 前端定义一个回调函数
- 动态创建
<script>
标签,src指向跨域API并在URL中指定回调函数名 - 服务器返回的数据作为回调函数的参数
- 浏览器执行回调函数,处理返回的数据
示例代码:JSONP实现
前端代码:
javascript
// 定义回调函数
function handleResponse(data) {
console.log('接收到数据:', data);
// 在这里处理返回的数据
}
// 创建script标签
let script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
// 添加到文档中
document.body.appendChild(script);
// 请求完成后移除script标签
script.onload = function() {
document.body.removeChild(script);
};
服务器端响应(PHP示例):
php
<?php
$data = array(
'name' => '张三',
'age' => 25,
'city' => '北京'
);
$callback = $_GET['callback'];
echo $callback . '(' . json_encode($data) . ')';
?>
jQuery中的JSONP:
javascript
$.ajax({
url: 'https://api.example.com/data',
dataType: 'jsonp',
jsonpCallback: 'handleResponse', // 可选,自定义回调函数名
success: function(data) {
console.log('接收到数据:', data);
},
error: function(xhr, status, error) {
console.error('请求失败:', status, error);
}
});
3. CORS (Cross-Origin Resource Sharing)
CORS是一种官方推荐的跨域解决方案,需要服务器端支持。
简单请求与非简单请求:
- 简单请求:GET、HEAD、POST,且Content-Type为text/plain、multipart/form-data、application/x-www-form-urlencoded
- 非简单请求:PUT、DELETE等,或自定义头部的请求
服务器端设置响应头:
php
<?php
header("Access-Control-Allow-Origin: *"); // 允许所有域名
// 或
header("Access-Control-Allow-Origin: http://example.com"); // 允许特定域名
// 对于需要携带凭证的请求
header("Access-Control-Allow-Credentials: true");
// 允许的HTTP方法
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");
// 允许的头部
header("Access-Control-Allow-Headers: Content-Type, Authorization");
?>
前端使用CORS:
javascript
// 简单GET请求
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
// 带凭证的请求
fetch('https://api.example.com/data', {
credentials: 'include' // 包含cookie等凭证
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
// 非简单请求(PUT请求)
fetch('https://api.example.com/data/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({
name: '新名称',
value: '新值'
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
预检请求(Preflight Request):
对于非简单请求,浏览器会先发送一个OPTIONS请求进行预检,服务器需要正确处理。
四、综合案例:多图上传功能
实现一个支持多图上传、预览、删除和进度显示的功能。
HTML部分:
html
<div class="upload-container">
<h2>多图上传</h2>
<!-- 文件选择区域 -->
<div class="upload-area">
<input type="file" id="file-input" multiple accept="image/*" style="display: none;">
<button id="select-btn">选择图片</button>
<span id="file-info">未选择文件</span>
</div>
<!-- 图片预览区域 -->
<div id="preview-area" class="preview-area"></div>
<!-- 上传按钮和进度条 -->
<button id="upload-btn" disabled>开始上传</button>
<div class="progress-container">
<div id="progress-bar" class="progress-bar"></div>
<span id="progress-text">0%</span>
</div>
<!-- 上传结果 -->
<div id="result-area"></div>
</div>
CSS部分:
css
.upload-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.upload-area {
margin-bottom: 20px;
padding: 15px;
border: 2px dashed #ccc;
border-radius: 5px;
text-align: center;
}
#select-btn {
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
#select-btn:hover {
background-color: #45a049;
}
.preview-area {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.preview-item {
position: relative;
width: 120px;
height: 120px;
}
.preview-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.remove-btn {
position: absolute;
top: 5px;
right: 5px;
background-color: #f44336;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
cursor: pointer;
}
#upload-btn {
padding: 10px 20px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
#upload-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#upload-btn:hover:not(:disabled) {
background-color: #0b7dda;
}
.progress-container {
margin-top: 10px;
display: none;
}
.progress-bar {
height: 20px;
background-color: #f1f1f1;
border-radius: 4px;
overflow: hidden;
}
.progress-bar::after {
content: '';
display: block;
height: 100%;
width: 0%;
background-color: #4CAF50;
transition: width 0.3s;
}
#result-area {
margin-top: 20px;
padding: 10px;
border-radius: 4px;
}
.success {
background-color: #ddffdd;
border-left: 6px solid #4CAF50;
}
.error {
background-color: #ffdddd;
border-left: 6px solid #f44336;
}
JavaScript部分:
javascript
document.addEventListener('DOMContentLoaded', function() {
const fileInput = document.getElementById('file-input');
const selectBtn = document.getElementById('select-btn');
const fileInfo = document.getElementById('file-info');
const previewArea = document.getElementById('preview-area');
const uploadBtn = document.getElementById('upload-btn');
const progressContainer = document.querySelector('.progress-container');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const resultArea = document.getElementById('result-area');
let selectedFiles = [];
// 点击选择按钮触发文件输入
selectBtn.addEventListener('click', function() {
fileInput.click();
});
// 文件选择变化事件
fileInput.addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
if (selectedFiles.length > 0) {
fileInfo.textContent = `已选择 ${selectedFiles.length} 个文件`;
uploadBtn.disabled = false;
// 清空预览区域
previewArea.innerHTML = '';
// 显示预览图
selectedFiles.forEach((file, index) => {
if (!file.type.match('image.*')) {
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const previewItem = document.createElement('div');
previewItem.className = 'preview-item';
const img = document.createElement('img');
img.className = 'preview-img';
img.src = e.target.result;
img.alt = file.name;
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '×';
removeBtn.addEventListener('click', function() {
// 从数组中移除文件
selectedFiles.splice(index, 1);
// 从DOM中移除预览项
previewItem.remove();
// 更新文件信息
fileInfo.textContent = selectedFiles.length > 0
? `已选择 ${selectedFiles.length} 个文件`
: '未选择文件';
// 如果没有文件了,禁用上传按钮
uploadBtn.disabled = selectedFiles.length === 0;
});
previewItem.appendChild(img);
previewItem.appendChild(removeBtn);
previewArea.appendChild(previewItem);
};
reader.readAsDataURL(file);
});
} else {
fileInfo.textContent = '未选择文件';
uploadBtn.disabled = true;
}
});
// 上传按钮点击事件
uploadBtn.addEventListener('click', function() {
if (selectedFiles.length === 0) {
alert('请先选择文件');
return;
}
// 显示进度条
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
progressText.textContent = '0%';
// 清空结果区域
resultArea.innerHTML = '';
resultArea.className = '';
// 创建FormData对象
const formData = new FormData();
// 添加文件到FormData
selectedFiles.forEach(file => {
formData.append('images[]', file);
});
// 添加其他数据(如果需要)
formData.append('userId', '123');
// 创建XMLHttpRequest对象
const xhr = new XMLHttpRequest();
// 配置请求
xhr.open('POST', 'upload.php', true);
// 进度事件
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percent + '%';
progressText.textContent = percent + '%';
}
};
// 加载完成事件
xhr.onload = function() {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
resultArea.className = 'success';
resultArea.innerHTML = `
<p>上传成功!</p>
<p>共上传 ${response.data.uploaded_count} 张图片</p>
<ul>
${response.data.images.map(img => `
<li>
<strong>${img.original_name}</strong> -
大小: ${(img.size / 1024).toFixed(2)} KB,
<a href="${img.url}" target="_blank">查看</a>
</li>
`).join('')}
</ul>
`;
} else {
resultArea.className = 'error';
resultArea.innerHTML = `
<p>上传失败</p>
<p>${response.message}</p>
${response.errors ? `<ul>${response.errors.map(err => `<li>${err}</li>`).join('')}</ul>` : ''}
`;
}
} catch (e) {
resultArea.className = 'error';
resultArea.innerHTML = '<p>解析服务器响应时出错</p>';
}
} else {
resultArea.className = 'error';
resultArea.innerHTML = `<p>上传失败,服务器返回状态码: ${xhr.status}</p>`;
}
// 重置上传按钮和文件选择
uploadBtn.disabled = true;
fileInput.value = '';
selectedFiles = [];
fileInfo.textContent = '未选择文件';
};
// 错误事件
xhr.onerror = function() {
resultArea.className = 'error';
resultArea.innerHTML = '<p>上传过程中发生网络错误</p>';
};
// 发送请求
xhr.send(formData);
});
});
服务器端PHP代码(upload.php):
php
<?php
header('Content-Type: application/json');
// 设置跨域头(如果需要)
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Allow-Headers: Content-Type");
// 响应数据结构
$response = [
'success' => false,
'message' => '',
'errors' => [],
'data' => []
];
// 检查是否有文件上传
if (empty($_FILES['images'])) {
$response['message'] = '没有接收到任何文件';
echo json_encode($response);
exit;
}
// 创建上传目录(如果不存在)
$uploadDir = 'uploads/' . date('Y/m/d');
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
// 处理每个上传的文件
$uploadedFiles = [];
$errors = [];
foreach ($_FILES['images']['tmp_name'] as $key => $tmpName) {
// 检查上传错误
if ($_FILES['images']['error'][$key] !== UPLOAD_ERR_OK) {
$errors[] = '文件 ' . $_FILES['images']['name'][$key] . ' 上传失败: ' . getUploadError($_FILES['images']['error'][$key]);
continue;
}
// 验证文件类型
$fileType = mime_content_type($tmpName);
if (!in_array($fileType, ['image/jpeg', 'image/png', 'image/gif'])) {
$errors[] = '文件 ' . $_FILES['images']['name'][$key] . ' 不是有效的图片格式 (JPEG, PNG, GIF)';
continue;
}
// 验证文件大小(限制为5MB)
if ($_FILES['images']['size'][$key] > 5 * 1024 * 1024) {
$errors[] = '文件 ' . $_FILES['images']['name'][$key] . ' 大小超过5MB限制';
continue;
}
// 生成唯一文件名
$extension = pathinfo($_FILES['images']['name'][$key], PATHINFO_EXTENSION);
$filename = uniqid() . '.' . $extension;
$destination = $uploadDir . '/' . $filename;
// 移动文件到目标位置
if (move_uploaded_file($tmpName, $destination)) {
$uploadedFiles[] = [
'original_name' => $_FILES['images']['name'][$key],
'saved_name' => $filename,
'size' => $_FILES['images']['size'][$key],
'type' => $fileType,
'url' => 'http://' . $_SERVER['HTTP_HOST'] . '/' . $destination
];
} else {
$errors[] = '无法保存文件 ' . $_FILES['images']['name'][$key];
}
}
// 设置响应
if (!empty($uploadedFiles)) {
$response['success'] = true;
$response['message'] = '部分文件上传成功';
$response['data'] = [
'uploaded_count' => count($uploadedFiles),
'images' => $uploadedFiles
];
if (!empty($errors)) {
$response['errors'] = $errors;
}
} else {
$response['message'] = '没有文件上传成功';
$response['errors'] = $errors;
}
// 返回JSON响应
echo json_encode($response);
// 辅助函数:获取上传错误信息
function getUploadError($errorCode) {
switch ($errorCode) {
case UPLOAD_ERR_INI_SIZE:
return '文件大小超过服务器限制';
case UPLOAD_ERR_FORM_SIZE:
return '文件大小超过表单限制';
case UPLOAD_ERR_PARTIAL:
return '文件只有部分被上传';
case UPLOAD_ERR_NO_FILE:
return '没有文件被上传';
case UPLOAD_ERR_NO_TMP_DIR:
return '缺少临时文件夹';
case UPLOAD_ERR_CANT_WRITE:
return '写入磁盘失败';
case UPLOAD_ERR_EXTENSION:
return '文件上传被PHP扩展阻止';
default:
return '未知上传错误';
}
}
?>
五、总结
-
数据交换格式:
- XML:结构严谨,适合复杂数据结构,但体积较大
- JSON:轻量级,与JavaScript无缝集成,是现代Web应用的首选
-
无刷新分页:
- 提升用户体验
- 减少服务器负载
- 通过Ajax动态加载数据
-
跨域解决方案:
- JSONP:简单易用,但只支持GET请求
- CORS:功能强大,安全性好,需要服务器支持
- 代理服务器:另一种解决方案(未在本文中详述)
-
文件上传:
- 使用FormData对象处理多文件上传
- 提供上传进度反馈
- 客户端预览和服务器端验证
通过掌握这些技术,可以构建更加现代化、用户友好的Web应用程序。