JavaScript学习教程,从入门到精通,Ajax数据交换格式与跨域处理(26)

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技术实现,只更新页面中需要变化的部分,而不是整个页面刷新。

实现步骤:

  1. 监听分页按钮点击事件
  2. 阻止默认行为(页面跳转)
  3. 发送Ajax请求获取分页数据
  4. 使用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>标签没有跨域限制的特性来实现。

实现原理:

  1. 前端定义一个回调函数
  2. 动态创建<script>标签,src指向跨域API并在URL中指定回调函数名
  3. 服务器返回的数据作为回调函数的参数
  4. 浏览器执行回调函数,处理返回的数据

示例代码: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 '未知上传错误';
    }
}
?>

五、总结

  1. 数据交换格式

    • XML:结构严谨,适合复杂数据结构,但体积较大
    • JSON:轻量级,与JavaScript无缝集成,是现代Web应用的首选
  2. 无刷新分页

    • 提升用户体验
    • 减少服务器负载
    • 通过Ajax动态加载数据
  3. 跨域解决方案

    • JSONP:简单易用,但只支持GET请求
    • CORS:功能强大,安全性好,需要服务器支持
    • 代理服务器:另一种解决方案(未在本文中详述)
  4. 文件上传

    • 使用FormData对象处理多文件上传
    • 提供上传进度反馈
    • 客户端预览和服务器端验证

通过掌握这些技术,可以构建更加现代化、用户友好的Web应用程序。

相关推荐
彷徨而立几秒前
【C++】频繁分配和释放会产生内存碎片
开发语言·c++
刺客-Andy几秒前
React 第三十六节 Router 中 useParams 的具体使用及详细介绍
前端·react.js·前端框架
bylander6 分钟前
【论文速读】《Scaling Scaling Laws with Board Games》
人工智能·学习
code_shenbing16 分钟前
C# 实现列式存储数据
开发语言·c#·存储
黄同学real32 分钟前
Vue 项目中运行 `npm run dev` 时发生的过程
前端·vue.js·npm
Kairo_011 小时前
在 API 模拟阶段:Apipost vs. Faker.js vs. Postman —— 为什么 Apipost 是最优选择
开发语言·javascript·postman
黄同学real1 小时前
vue 优化策略,大白话版本
前端·javascript·vue.js
Once_day1 小时前
研发效率破局之道阅读总结(4)个人效率
开发语言·研发效能·devops
xcLeigh1 小时前
HTML5好看的水果蔬菜在线商城网站源码系列模板8
java·前端·html5
痕5171 小时前
如何在idea中写spark程序。
开发语言