Ajax-Fetch-socket-WebRTC

重要通知

基本概况

  • 官网:<>
  • GitHub:<>

navigator.sendBeacon() 方法可用于通过 HTTP POST 将少量数据 异步 传输到 Web 服务器。

代码示例: 用户行为分析上报

避免使用 unload 和 beforeunload,在不同终端设备上不可靠。


第02章 HTTP状态码

----------------------------------------------------------------------------------
  状态码	消息	                            描述
----------------------------------------------------------------------------------
  100	    Continue	                       只有一部分请求被服务器接收,但只要没被服务器拒绝,客户端就会延续这个请求   
  101    	Switching Protocols	             服务器交换机协议   

  200    	OK	                             请求被确认   
  201    	Created	                         请求时完整的,新的资源被创建   
  202	    Accepted	                       请求被接受,但未处理完   
  203	    Non-authoritative Information	 
  204	    No Content	 
  205	    Reset Content	 
  # https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/206
  206	    Partial Content	                 成功状态响应代码表示请求已成功,并且主体包含所请求的数据区间,该数据区间是在请求的 Range 首部指定的   

  300	    Multiple Choices	               一个超链接表,用户可以选择一个超链接并访问,最大支持5个超链接  
  301	    Moved Permanently	               被请求的页面已经移动到了新的URL下   
  302	    Found	                           被请求的页面暂时性地移动到了新的URL下   
  303	    See Other	                       被请求的页面可以在一个不同的URL下找到   
  304	    Not Modified	                   HTTP缓存机制,即在协商缓存过程中,表示没有修改,   
  305	    Use Proxy	 
  306	    Unused	                         已经不再使用此状态码,但状态码被保留  
  307	    Temporary Redirect	             被请求的页面暂时性地移动到了新的URL下  

  # 请求错误
  400	    Bad Request	                     服务器无法识别请求   
  401	    Unauthorized	                   被请求的页面需要用户名和密码   
  402    	Payment Required	               目前还不能使用此状态码   
  403	    Forbidden	                       禁止访问所请求的资源   
  404	    Not Found	                       服务器无法找到所请求的页面   
  405	    Method Not Allowed	             请求中所指定的方法不被允许   
  406    	Not Acceptable	                 服务器只能创建一个客户端无法接受的响应   
  407	    Proxy Authentication Required	   在请求被服务前必须认证一个代理服务器  
  408    	Request Timeout	                 请求时间超过了服务器所能等待的时间,连接被断开   
  409	    Conflict	                       请求有矛盾的地方   
  410	    Gone	                           被请求的页面不再可用   
  411	    Length Required	"Content-Length" 没有被定义,服务器拒绝接受请求   
  412     Precondition Failed	             请求的前提条件被服务器评估为false  
  413    	Request Entity Too Large	       因为请求的实体太大,服务器拒绝接受请求  
  414    	Request-url Too Long	           服务器拒绝接受请求,因为URL太长。多出现在把"POST"请求转换为"GET"请求时所附带的大量查询信息   
  415    	Unsupported Media Type	         服务器拒绝接受请求,因为媒体类型不被支持   
  417	    Expectation Failed

  # 服务器错误	 
  500	    Internal Server Error	           请求不完整,服务器遇见了出乎意料的状况   
  501    	Not Implemented	                 请求不完整,服务器不提供所需要的功能   
  502    	Bad Gateway	                     请求不完整,服务器从上游服务器接受了一个无效的响应    
  503    	Service Unavailable	             请求不完整,服务器暂时重启或关闭   
  504    	Gateway Timeout	                 网关超时   
  505	    HTTP Version Not Supported	     服务器不支持所指定的HTTP版本
----------------------------------------------------------------------------------

Fetch

简介与核心思想

  • 当接收到一个代表错误的 HTTP 状态码时,从 fetch()返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。
  • fetch() 不会接受跨域 cookies;你也不能使用fetch() 建立起跨域会话。其他网站的Set-Cookie头部字段将会被无视。
  • fetch 不会发送 cookies。除非你使用了credentials的 初始化选项。(自2017年8月25日以后,默认的credentials政策变更为same-origin。Firefox也在61.0b13版本中,对默认值进行修改)

重大警告

  • Responses 对象被设置为了 stream 的方式,所以它们只能被读取一次,即Response.body仅能被读一次,如需多次读取,需要重新设计其流,见public/fetch.js
  • 如果'Content-Type' = 'application/x-www-form-urlencoded',则body的传值必须为:body: name=eshen&sex=男

简易示例

fetch("http://example.com/movies.json")
  .then((response) => response.json())
  .then((data) => console.log(data));

结构示例

const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');
formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);

fetch(
  url, 
  options: {
    cache: 'no-cache',          // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, same-origin, omit
    headers: {
      'user-agent': 'Mozilla/4.0 MDN Example',
      'content-type': 'application/x-www-form-urlencoded',
      'content-type': 'multipart/form-data',
      'content-type': 'application/json',
      'content-type': 'text/plain'
    },
    method: 'POST',             // *GET, POST, PUT, DELETE, etc.
    mode: 'cors',               // no-cors, cors, *same-origin
    redirect: 'follow',         // manual, *follow, error
    referrer: 'no-referrer',    // *client, no-referrer  

    // JSON
    body: JSON.stringify(data), // must match 'Content-Type' header

    // 上传文件
    body: formData,

    // 这个属性非常特殊,keepalive 选项可以允许请求持续保持连接,甚至页面已经关闭的情况。使用 keepalive 标志的 Fetch 是 Navigator.sendBeacon() API 的一种替代方案。
    keepalive: true 
  }
)
.then((res)=> {
  // res.arrayBuffer();
  // res.blob();
  // res.json();
  // res.text()
  // res.formData();
  return res;
})
.then((res)=> {
  if (res.ok) {
    return res;
  }
  throw new Error(res);
})
.then((res) => {
    const reader = res.body.getReader();
    const readData = await reader.read();
})
.catch((err)=> {
  //
});

Interfaces

fetch('https://api.ysunlight.com/api/get')
  .then((res) => {
    return res.json();
  }).then((res) => {
    console.log(res);
  }).catch((err) => {
    console.log(err, 'Error');
  });

Polyfill

Fetch API 的支持情况,可以通过检测Headers, Request, Response 或 fetch()是否在Window 或 Worker 域中。

if(Window.fetch) {
  // run my fetch request here
} else {
  // do something with XMLHttpRequest?
}

Body

不管是请求还是响应都能够包含 body 对象。body 也可以是以下任意类型的实例。

  • ArrayBuffer
  • ArrayBufferView (Uint8Array 等)
  • Blob/File
  • string
  • URLSearchParams
  • FormData

基础方法

Body 类定义了以下方法(这些方法都被 Request 和 Response所实现)以获取 body 内容。这些方法都会返回一个被解析后的 Promise 对象和数据。

  • Request.arrayBuffer() / Response.arrayBuffer()
  • Request.blob() / Response.blob()
  • Request.formData() / Response.formData()
  • Request.json() / Response.json()
  • Request.text() / Response.text()

完整示例

/**
 * Author: 王军
 * Email: ysungod@163.com
 * 
  const { status, data } = await fetchAPI({
    path: '',
    method: 'post',
    data: {},
    headers: {}
  })
  .then(res => res)
  .catch(err => err);
 */
export interface OPTIONS {
  base?: string;
  path: string;
  data?: Record<string, any>;
  headers?: Record<string, any>;
  method?: "get" | "post";
  uploadType?: "api" | "file";
  dataType?: "formData" | "wwwForm" | "json" | "text";
  responseType?: "response" | "string" | "arrayBuffer" | "blob" | "json";
  mode?: "cors" | "no-cors" | "same-origin";
  progress?: (...args: any[]) => void;
}

export function fetchAPI(options = {} as OPTIONS) {
  const base = options.base || "";
  const path = options.path || "";
  const data = options.data || {};
  const headers = options.headers || {};
  // 请求方法类型,默认为'PUT'
  const method = (options.method || "GET").toUpperCase();
  // 'file' | 'api'
  const uploadType = options.uploadType || "api";
  // 请求数据类型,默认为'json', 枚举列表: json | text | string | FormData
  const dataType = options.dataType || "json";
  // 响应数据类型,默认为'json'
  const responseType = options.responseType || "json";
  // cors、no-cors 或者 same-origin
  const mode = options.mode || "cors";

  const basePath =
    path.includes("https://") || path.includes("http://") ? path : base + path;

  const CONTENT_TYPE = {
    formData: "multipart/form-data",
    wwwForm: "application/x-www-form-urlencoded",
    json: "application/json",
    text: "text/plain",
  };

  // 屏蔽上传文件,上传文件会自动携带Content-Type
  if (uploadType !== "file" && !headers["Content-Type"]) {
    headers["Content-Type"] = CONTENT_TYPE[dataType];
  }

  const params: Record<string, any> = {
    method,
    mode,
    headers: {
      ...headers,
    },
  };

  let requestAPI = basePath;

  if (method === "GET") {
    const url = new URL(basePath);
    const pathString = url.origin + url.pathname;

    const defineParams: Record<string, any> = {};
    for (const [key, value] of url.searchParams) {
      defineParams[key] = value;
    }

    const searchParams = new URLSearchParams(Object.assign(defineParams, data));

    requestAPI = pathString + "?" + searchParams.toString();
  }

  if (method === "POST") {
    if (headers["Content-Type"] === CONTENT_TYPE.wwwForm) {
      const searchParams = new URLSearchParams(data);
      params["body"] = searchParams.toString();
    } else {
      params["body"] = uploadType === "api" ? JSON.stringify(data) : data;
    }
  }

  return new Promise((resolve) => {
    fetch(requestAPI, {
      ...params,
    })
      .then((response) => {
        return { body: response.body, response: response }
      })
      .then(({ body, response }) => {
        // 若跨域请求,则获取不到文件字节长度。
        const filesize = +(response as any).headers.get('content-length');

        // 获取流
        const reader = (body as any).getReader();

        // 已下载字节
        let loadedLength = 0;

        return new ReadableStream({
          start(controller) {
            return pump();

            function pump(): any {
              return reader.read().then(({ done, value }: { done: any; value: any }) => {
                // 读不到更多数据就关闭流
                if (done) {
                  controller.close();
                  return;
                }

                // 已下载的字节
                loadedLength += value.length;

                // 下载进度
                const progress = Math.floor((loadedLength / filesize) * 100);

                options?.progress && options?.progress(progress)

                // 将下一个数据块置入流中
                controller.enqueue(value);
                return pump();
              });
            }
          }
        });
      })
      .then(stream => new Response(stream))
      .then((response) => {
        // 响应数据类型
        switch (responseType) {
          case "response":
            return resolve(response);
          case "string":
            return resolve(response.text());
          case "arrayBuffer":
            return resolve(response.arrayBuffer());
          case "blob":
            return resolve(response.blob());
          default:
            resolve(response.json());
        }
      })
      .catch(() => {
        resolve(undefined);
      });
  });
}

AJAX

完整示例

/**
 * Author: 王军
 * Email: ysungod@163.com
 * 
 * import ajax from '/public/ajax.js';
 * <script type="text/javascript" src="/public/ajax.js"></script>
    ajax({
      base: '',
      path: '',
      type: 'upload',
      method: 'GET',
      async: true,
      timeout: 5000,
      headers: {},
      dataType: 'json',
      responseType: 'json',
      data: {},
      uploadProgress: function() {},
      complete: function() {},
      success: function(res) {},
      fail: function(err) {}
    });
 */
(function(global, factory) {
  if (typeof exports == 'object' && typeof module == 'object') {
    module.exports = factory();
  } else if (typeof define == 'function' && define.amd) {
    define([], factory);
  } else if (typeof exports == 'object') {
    exports.ajax = factory;
  } else {
    (global = global || self, global.ajax = factory);
  }
  // typeof exports == 'object' && typeof module == 'object' ? module.exports = factory() : 
  // typeof define == 'function' && define.amd ? define([], factory) : 
  // typeof exports == 'object' ? exports.ajax = factory : 
  // (global = global || self, global.ajax = factory);
})(this, function(options) {'use strict';
  const JSON_STRING = 'json';
  options = options || {};
  options.base = options.base || "";
  options.path = options.path || "";
  options.method = (options.method || 'GET').toUpperCase();
  options.async = options.async || true;
  options.timeout = options.timeout || 15000;
  options.headers = options.headers || {};
  //设置为"json"时,responseText与responseXML就不能读取,否则报错
  options.dataType = options.dataType || JSON_STRING;
  options.responseType = (options.responseType || 'json').toLowerCase();
  options.data = options.data || {};

  options.base = options.base.match(/\/$/g) == null ? (options.base + '/') : options.base;

  //
  var url = "";
  if (options.path.match(/^(https?:\/\/)/g) != null) {
    url = options.path;
  } else {
    options.path = options.path.match(/^\//g) == null ? options.path : options.path.replace(/^\//g, '');
    url = options.base + options.path;
  }

  console.info(options);


  /**
   * XMLHttpRequest
   */
  var xhr = null;
  if (window.XMLHttpRequest) {
    //IE7+, Firefox, Chrome, Opera, Safari 
    xhr = new XMLHttpRequest();
  } else if (window.ActiveXObject) {
    //IE5、IE6
    xhr = new window.ActiveXObject("Microsoft.xhr");
  } else {
    throw new Error("Your brower don’t support XMLHttpRequest, please use Chrome to open it.");
  } 

  //time out
  var TIMEOUT = null;

  if (options.responseType) {
    xhr.responseType = options.responseType;
  }




  /**
   * 上传进度设计
   * 
   */
  xhr.upload.onprogress = function(e) {
    let size = e.total;
    let pros = e.loaded;
  
    let scale = size/100;
    let value = 0;
    value= (pros/scale).toFixed(2);
  
    if (pros >= size) {
      value = 100;
    }
    options.uploadProgress && options.uploadProgress(value);
  }
  xhr.upload.onload = function(e) {
    e['upload'] = e['type'];
    // console.log(e);
  }
  xhr.upload.onloadend = function(e) {
    e['upload'] = e['type'];
    // console.log(e);
  }
  xhr.upload.ontimeout = function(e) {
    e['upload'] = e['type'];
    // console.log(e);
  }
  xhr.upload.onabort = function(e) {
    e['upload'] = e['type'];
    // console.log(e);
  }
  xhr.upload.onerror = function(e) {
    e['upload'] = e['type'];
    // console.log(e);
  }






  /**
   * 下载进度
   * 
   */
  xhr.onloadstart = function(e) {
    // console.log(e);
  }
  xhr.onprogress = function(e) {
    // console.log(e);
  }
  xhr.onload = function(e) {
    // console.log(e);
  }
  xhr.onloadend = function(e) {
    // console.log(e);
  }
  xhr.ontimeout = function(e) {
    // console.log(e);
  }
  xhr.onabort = function(e) {
    // console.log(e);
  }
  xhr.onerror = function(e) {
    // console.log(e);
  }





  //request state
  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
      //complete
      window.clearTimeout(TIMEOUT);
      if (options.complete) {
        options.complete();
      }
      if (xhr.status == 200) {
        //success
        if (options.success) {
          console.log(xhr);
          options.success(xhr.response);
        }
        return ;
      }
      //fail
      if (options.fail) {
        options.fail({state: xhr.status, msg: options.dataType !== JSON_STRING ? xhr.responseText : '消息通道出现异常错误'});
      }
    }
  }

  //转换数据为PATH参数
  var transArguments = function(data) {
    data = data || {};
    var _path = "";
    for (var i in data) {
      _path += i + '=' + encodeURIComponent(data[i]) + '&';
    }
    _path = _path.replace(/&$/g, '');
    return _path;
  }


  //GET
  if (options.method == 'GET') {
    var _url = url + '?' + transArguments(options.data);
    _url = _url.replace(/(\&|\?)$/g, '');
    xhr.open(options.method, _url, true);
    for (var key in options.headers) {
      xhr.setRequestHeader(key, options.headers[key]);
    }
    xhr.send(null);
  }


  //POST
  if (options.method == 'POST') {
    xhr.open(options.method, url, true);

    //上传文件清空Content-type, 否则会异常
    if (options.type !== 'upload') {
      xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    }

    for (var key in options.headers) {
      xhr.setRequestHeader(key, options.headers[key]);
    }
    
    //上传文件
    if (options.type === 'upload') {
      xhr.send(options.data);
    } else {
      xhr.send(transArguments(options.data));
    }
  }
  

  //超时
  TIMEOUT = window.setTimeout(function () {
    xhr.abort();
    //完成
    options.complete && options.complete({});

    //失败
    options.fail && options.fail({state: 408, msg: 'timeout'});

  }, options.timeout);


});

WebSocket

WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。

代码示例

// Create socket connection.
const socket = new socket('ws://localhost:8080');

switch (socket.readyState) {
  case socket.CONNECTING:  // 值为0,表示正在连接。
    // do something
    break;
  case socket.OPEN:        // 值为1,表示连接成功,可以通信了。
    // do something
    break;
  case socket.CLOSING:     // 值为2,表示连接正在关闭。
    // do something
    break;
  case socket.CLOSED:      // 值为3,表示连接已经关闭,或者打开连接失败。
    // do something
    break;
  default:
    // this never happens
    break;
}
// 发送数据
var data = new ArrayBuffer(10000000);
socket.send(data);

// socket.bufferedAmount:表示还有多少字节的二进制数据没有发送出去。
if (socket.bufferedAmount === 0) {
  // 发送完毕
} else {
  // 发送还没结束
}
// 事件:连接成功
socket.onopen = function(event) {
  socket.send(data);  // 对要传输的数据进行排队。
}

// 事件:接收数据
socket.onmessage = function(event) {
  //
}
// 事件:连接错误
socket.onerror = function(event) {
  //
}
// 事件:连接关闭
socket.onclose = function(event) {
  //
}
socket.close([code[, reason]]);  // 关闭当前链接。

Socket.IO

Socket.IO 实现了实时双向的基于事件的通讯机制,旨在让各种浏览器与移动设备上实现实时 app 功能,模糊化各种传输机制。

代码示例

  • 服务端
import { Server } from 'socket.io';

const io = new Server(3000);
io.on('connection', (socket) => {
  // send a message
  socket.emit('sendMsg', {});

  // receive a message
  socket.on('receiveMsg', (data) => {
    console.log(data);
  });
});
  • 客户端
import { io } from 'socket.io-client';

const socket = io('ws://localhost:3000');
// receive a message
socket.on('sendMsg', (data) => {
  console.log(data);
});
// send a message
socket.emit('receiveMsg', {});

WebRTC

WebRTC(Web Real-Time Communication),即网页即时通信,是一个支持网页浏览器进行实时语音对话或视频对话的API,可以在基于浏览器应用中实现任意点对点(peer-to-peer)的数据、音频、视频 或它们的任意组合的通信。

WebRTC 协议

  • ICE:交互式连接设施Interactive Connectivity Establishment (ICE) 是一个允许你的浏览器和对端浏览器建立连接的协议框架。
  • STUN:是一个允许位于 NAT 后的客户端找出自己的公网地址,判断出路由器阻止直连的限制方法的协议。
  • NAT:网络地址转换协议Network Address Translation (NAT) 用来给你的(私网)设备映射一个公网的 IP 地址的协议。
  • TURN:NAT 的中继穿越方式Traversal Using Relays around NAT (TURN) 通过 TURN 服务器中继所有数据的方式来绕过“对称型 NAT”。
  • SDP:会话描述协议Session Description Protocol (SDP) 是一个描述多媒体连接内容的协议,例如分辨率,格式,编码,加密算法等。

adapter.js

adapter.js 是一个用于将应用程序与 WebRTC 中的规范更改和前缀差异综合的适配器。

Last Updated:
Contributors: 709992523