网络请求与远程资源

JavaScript 提供了多种与服务器通信的方式,从传统的 XMLHttpRequest 到现代的 Fetch API。

XMLHttpRequest

基本用法

// 创建 XMLHttpRequest 实例
const xhr = new XMLHttpRequest()

// 配置请求
xhr.open('GET', 'https://api.example.com/data', true)

// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Authorization', 'Bearer token123')

// 设置响应类型
xhr.responseType = 'json'  // 'text', 'json', 'blob', 'arraybuffer', 'document'

// 发送请求
xhr.send()

// 处理响应
xhr.onreadystatechange = function() {
  if (xhr.readyState === XMLHttpRequest.DONE) {
    if (xhr.status === 200) {
      console.log('Response:', xhr.response)
    } else {
      console.error('Error:', xhr.status, xhr.statusText)
    }
  }
}

// 监听事件
xhr.onload = function() {
  console.log('Request completed successfully')
}

xhr.onerror = function() {
  console.error('Network error occurred')
}

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    const percent = (event.loaded / event.total) * 100
    console.log(`Progress: ${percent}%`)
  }
}

发送数据

// GET 请求
function sendGetRequest(url, params = {}) {
  const queryString = new URLSearchParams(params).toString()
  const fullUrl = queryString ? `${url}?${queryString}` : url

  const xhr = new XMLHttpRequest()
  xhr.open('GET', fullUrl)
  xhr.responseType = 'json'

  xhr.onload = function() {
    if (xhr.status === 200) {
      console.log('GET Response:', xhr.response)
    }
  }

  xhr.send()
}

// POST 请求
function sendPostRequest(url, data) {
  const xhr = new XMLHttpRequest()
  xhr.open('POST', url)
  xhr.setRequestHeader('Content-Type', 'application/json')

  xhr.onload = function() {
    if (xhr.status === 200 || xhr.status === 201) {
      console.log('POST Response:', xhr.response)
    }
  }

  xhr.send(JSON.stringify(data))
}

// 文件上传
function uploadFile(url, file) {
  const formData = new FormData()
  formData.append('file', file)

  const xhr = new XMLHttpRequest()
  xhr.open('POST', url)

  xhr.upload.onprogress = function(event) {
    const percent = (event.loaded / event.total) * 100
    console.log(`Upload progress: ${percent}%`)
  }

  xhr.onload = function() {
    if (xhr.status === 200) {
      console.log('File uploaded successfully')
    }
  }

  xhr.send(formData)
}

超时和取消

const xhr = new XMLHttpRequest()

// 设置超时
xhr.timeout = 5000  // 5秒超时
xhr.ontimeout = function() {
  console.error('Request timed out')
}

// 取消请求
const cancelButton = document.getElementById('cancelBtn')
cancelButton.addEventListener('click', () => {
  xhr.abort()
  console.log('Request cancelled')
})

Fetch API

基本用法

// GET 请求
fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }
    return response.json()
  })
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Error:', error))

// POST 请求
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({
    name: 'John Doe',
    email: 'john@example.com'
  })
})
.then(response => response.json())
.then(data => console.log('Created user:', data))

配置选项

const requestOptions = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'Authorization': `Bearer ${token}`,
    'X-Custom-Header': 'custom-value'
  },
  body: JSON.stringify(data),
  mode: 'cors',           // 'cors', 'no-cors', 'same-origin'
  credentials: 'include', // 'omit', 'same-origin', 'include'
  cache: 'no-cache',      // 'default', 'no-cache', 'reload', 'force-cache', 'only-if-cached'
  redirect: 'follow',     // 'follow', 'error', 'manual'
  referrer: 'client',     // 'no-referrer', 'client', 'no-referrer-when-downgrade'
  referrerPolicy: 'no-referrer-when-downgrade',
  integrity: '',          // 子资源完整性检查
  keepalive: false,       // 请求在页面卸载后继续
  signal: null            // AbortSignal 用于取消请求
}

fetch(url, requestOptions)

处理不同响应类型

// JSON 响应
fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data))

// 文本响应
fetch('/api/text')
  .then(response => response.text())
  .then(text => console.log(text))

// Blob 响应(文件下载)
fetch('/api/file')
  .then(response => response.blob())
  .then(blob => {
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = 'filename.ext'
    a.click()
    URL.revokeObjectURL(url)
  })

// ArrayBuffer 响应
fetch('/api/binary')
  .then(response => response.arrayBuffer())
  .then(buffer => {
    // 处理二进制数据
    const view = new Uint8Array(buffer)
    console.log(view)
  })

// FormData 响应
fetch('/api/form')
  .then(response => response.formData())
  .then(formData => {
    for (let [key, value] of formData) {
      console.log(key, value)
    }
  })

错误处理和超时

// 错误处理
fetch('/api/data')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    return response.json()
  })
  .then(data => console.log('Success:', data))
  .catch(error => {
    if (error.name === 'TypeError') {
      console.error('Network error:', error.message)
    } else {
      console.error('Other error:', error.message)
    }
  })

// 超时控制
function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), timeout)

  return fetch(url, {
    ...options,
    signal: controller.signal
  })
  .finally(() => clearTimeout(timeoutId))
}

// 使用 AbortController 取消请求
const controller = new AbortController()
const cancelBtn = document.getElementById('cancelBtn')

cancelBtn.addEventListener('click', () => {
  controller.abort()
})

fetch('/api/long-running', {
  signal: controller.signal
})
.catch(error => {
  if (error.name === 'AbortError') {
    console.log('Request was cancelled')
  }
})

Beacon API

// 发送分析数据(在页面卸载时发送)
function sendAnalytics(data) {
  const blob = new Blob([JSON.stringify(data)], {
    type: 'application/json'
  })

  navigator.sendBeacon('/api/analytics', blob)
}

// 页面卸载时发送数据
window.addEventListener('unload', () => {
  sendAnalytics({
    url: location.href,
    timestamp: Date.now(),
    userAgent: navigator.userAgent
  })
})

// 或者在页面隐藏时发送
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    sendAnalytics({
      action: 'page_hidden',
      timestamp: Date.now()
    })
  }
})

Web Socket

基本连接

// 创建 WebSocket 连接
const ws = new WebSocket('ws://localhost:8080')

// 连接事件
ws.onopen = function(event) {
  console.log('WebSocket connected')
  ws.send('Hello Server!')
}

// 消息事件
ws.onmessage = function(event) {
  console.log('Received:', event.data)
  const data = JSON.parse(event.data)
  handleMessage(data)
}

// 错误事件
ws.onerror = function(event) {
  console.error('WebSocket error:', event)
}

// 关闭事件
ws.onclose = function(event) {
  console.log('WebSocket closed:', event.code, event.reason)
}

// 发送消息
function sendMessage(type, payload) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({
      type,
      payload,
      timestamp: Date.now()
    }))
  }
}

// 关闭连接
ws.close(1000, 'Normal closure')

心跳检测

class WebSocketManager {
  constructor(url, options = {}) {
    this.url = url
    this.options = {
      heartbeatInterval: 30000,  // 30秒
      reconnectInterval: 5000,   // 5秒
      maxReconnectAttempts: 5,
      ...options
    }
    this.ws = null
    this.heartbeatTimer = null
    this.reconnectTimer = null
    this.reconnectAttempts = 0
    this.connect()
  }

  connect() {
    this.ws = new WebSocket(this.url)

    this.ws.onopen = () => {
      console.log('WebSocket connected')
      this.reconnectAttempts = 0
      this.startHeartbeat()
      this.options.onOpen && this.options.onOpen()
    }

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      if (data.type === 'pong') {
        console.log('Heartbeat received')
      } else {
        this.options.onMessage && this.options.onMessage(data)
      }
    }

    this.ws.onclose = (event) => {
      console.log('WebSocket closed:', event.code, event.reason)
      this.stopHeartbeat()
      this.options.onClose && this.options.onClose(event)

      if (!event.wasClean && this.reconnectAttempts < this.options.maxReconnectAttempts) {
        this.scheduleReconnect()
      }
    }

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error)
      this.options.onError && this.options.onError(error)
    }
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping' }))
      }
    }, this.options.heartbeatInterval)
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
      this.heartbeatTimer = null
    }
  }

  scheduleReconnect() {
    this.reconnectAttempts++
    console.log(`Reconnecting in ${this.options.reconnectInterval}ms (attempt ${this.reconnectAttempts})`)

    this.reconnectTimer = setTimeout(() => {
      this.connect()
    }, this.options.reconnectInterval)
  }

  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data))
    }
  }

  close() {
    this.stopHeartbeat()
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
    }
    if (this.ws) {
      this.ws.close()
    }
  }
}

// 使用示例
const wsManager = new WebSocketManager('ws://localhost:8080', {
  onOpen: () => console.log('Connected!'),
  onMessage: (data) => console.log('Message:', data),
  onClose: () => console.log('Disconnected!'),
  onError: (error) => console.error('Error:', error)
})

// 发送消息
wsManager.send({ type: 'chat', message: 'Hello!' })

// 清理
window.addEventListener('beforeunload', () => {
  wsManager.close()
})

总结

网络请求是现代 Web 应用的核心功能:

  1. XMLHttpRequest:传统的 AJAX 请求方式
  2. Fetch API:现代的 Promise-based 请求 API
  3. Beacon API:可靠的后台数据传输
  4. WebSocket:全双工通信协议

选择合适的网络技术可以显著提升应用性能和用户体验。