1. 前后端通信

1.1. 同源策略和限制

源:协议、域名、端口

限制:不是一个源的文档没有权利去操作另一个源的文档,包括 cookie, localStorage, indexDB, DOM 无法获取, Ajax 无法发送

1.2. 前后端通信方式

  • Ajax 同源限制
  • WebSocket 不受限制,因为请求头中加入了 Origin。他不是 Http,是基于 frame、双全工 tcp 协议的通信方式。
  • CORS 支持同源和非同源

1.2.1. 如何实现浏览器内多个标签页之间的通信? (阿里)

WebSocket、SharedWorker;也可以调用 localStorage、cookies 等本地存储方式;

localStorage 另一个浏览上下文里被添加、修改或删除时,它都会触发一个事件,我们通过监听事件,控制它的值来进行页面信息通信。

Safari 在无痕模式下设置 localStorage 值时会抛出 QuotaExceededError 的异常;

// localStorage change, trigger below event
window.addEventListener('storage', function(e) {
  document.querySelector('.my-key').textContent = e.key;
  document.querySelector('.my-old').textContent = e.oldValue;
  document.querySelector('.my-new').textContent = e.newValue;
  document.querySelector('.my-url').textContent = e.url;
  document.querySelector('.my-storage').textContent = e.storageArea;
});

1.2.2. WebSocket 如何兼容低浏览器?(阿里)

  • Adobe Flash Socket
  • ActiveX HTMLFile (IE)
  • 基于 multipart 编码发送 XHR
  • 基于长轮询的 XHR

1.3. 创建 Ajax 要点

XMLHttpRequest 对象的工作流程、兼容性、事件触发条件和顺序

/**
 * [json 实现 ajax 的json]
 * @param  {[type]} options [description]
 * @return {[type]}         [description]
 */
util.json = function(options) {
  var opt = {
    url: '',
    type: 'get',
    data: {},
    success: function() {},
    error: function() {},
  };
  util.extend(opt, options);
  if (opt.url) {
    var xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
    var data = opt.data,
      url = opt.url,
      type = opt.type.toUpperCase(),
      dataArr = [];
    for (var k in data) {
      dataArr.push(k + '=' + data[k]);
    }
    if (type === 'GET') {
      url = url + '?' + dataArr.join('&');
      xhr.open(type, url.replace(/\?$/g, ''), true);
      xhr.send();
    }
    if (type === 'POST') {
      xhr.open(type, url, true);
      xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
      xhr.send(dataArr.join('&'));
    }
    xhr.onload = function() {
      // 304 缓存中数据未变,206 video等大文件媒体资源成功
      if (xhr.status === 200 || xhr.status === 304) {
        var res;
        if (opt.success && opt.success instanceof Function) {
          res = xhr.responseText;
          if (typeof res === 'string') {
            res = JSON.parse(res);
            opt.success.call(xhr, res);
          }
        }
      } else {
        if (opt.error && opt.error instanceof Function) {
          opt.error.call(xhr, res);
        }
      }
    };
  }
};

1.4. 跨域

  • Jsonp
  • Hash (hash # 改变页面不刷新,? 后面 queryString 改变会刷新页面)
  • postMessage (html5)
  • WebSocket (html5)
  • CORS(支持跨域的变种 ajax,当发送 ajax 跨域请求时,http 请求头加入 origin)

1.4.1. Jsonp

利用 script 标签可以不同源加载实现的。Jsonp 只支持 GET 请求

  1. 在主站客户端 window 全局注册一个函数 cb
  2. url 传递参数和回调函数名字
  3. 服务端解析 url 后根据参数拿到数据,执行这个函数,数据作为函数的参数
  4. 删除全局注册函数

Jsonp 的优缺点

  1. 优点

    • 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要 XMLHttpRequest 或 ActiveX 的支持
    • 在请求完毕后可以通过调用 callback 的方式回传结果。将回调方法的权限给了调用方。这个就相当于将 controller 层和 view 层分开了。我提供的 Jsonp 服务只提供纯服务的数据,至于提供服务以后的页面渲染和后续 view 操作都由调用者来自己定义就好了。如果有两个页面需要渲染同一份数据,你们只需要有不同的渲染逻辑就可以了,逻辑都可以使用同一个 Jsonp 服务。
  2. 缺点

    • 它只支持 GET 请求而不支持 POST 等其它类型的 HTTP 请求
    • Jsonp 在调用失败的时候不会返回各种 HTTP 状态码。解决:timeout 触发 onerror 事件
    • 安全性。万一假如提供 Jsonp 的服务存在页面注入漏洞,即它返回的 javascript 的内容被人控制的。那么所有调用这个 Jsonp 的网站都会存在漏洞。于是无法把危险控制在一个域名下。所以在使用 Jsonp 的时候必须要保证使用的 Jsonp 服务必须是安全可信的。

basic demo 1

Jsonp.html 页面定义一个函数,然后在远程 remote.js 中传入数据进行调用。

<script>
var localHandler = function(data){
    alert('我是本地函数,可以被跨域的remote.js文件调用,远程js带来的数据是:' + data.result);
};
</script>
<script src="http://remoteserver.com/remote.js"></script>

remote.js 文件代码如下:

localHandler({ result: '我是远程js带来的数据' });

demo 2

怎么让远程 js 知道它应该调用的本地函数叫什么名字呢?

通过 queryString 传递回调函数名字,Jsonp 服务端读取并动态创建

Jsonp.html 页面的代码:

<script>
// 得到航班信息查询结果后的回调函数
var flightHandler = function(data){
    alert('你查询的航班结果是:票价 ' + data.price + ' 元,' + '余票 ' + data.tickets + ' 张。');
};

// 提供 Jsonp 服务的 url 地址(不管是什么类型的地址,最终生成的返回值都是一段javascript代码)
var url = "http://flightQuery.com/Jsonp/flightResult.aspx?code=CA1998&callback=flightHandler";

// 创建 script 标签,设置其属性
var script = document.createElement('script');
script.setAttribute('src', url);

// 把 script 标签加入 head,此时调用开始
document.getElementsByTagName('head')[0].appendChild(script);
</script>

不再直接把远程 js 文件写死,而是编码实现动态查询,而这也正是 Jsonp 客户端实现的核心部分。我们看到调用的 url 中传递了一个 code 参数,告诉服务器我要查的是 CA1998 次航班的信息,而 callback 参数则告诉服务器,我的本地回调函数叫做 flightHandler,所以请把查询结果传入这个函数中进行调用。

服务器读取 url 根据 queryString 生成代码:

flightHandler({
  code: 'CA1998',
  price: 1780,
  tickets: 5,
});

jQuery 如何实现 Jsonp 调用?

$.ajax({
  type: 'get',
  async: false,
  url: 'http://flightQuery.com/Jsonp/flightResult.aspx?code=CA1998',
  dataType: 'jsonp',
  jsonp: 'callback', //传递给请求处理程序或页面的,用以获得Jsonp回调函数名的参数名(一般默认为:callback)
  jsonpCallback: 'flightHandler', //自定义的Jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据
  success: function(json) {
    alert('您查询到航班信息:票价: ' + json.price + ' 元,余票: ' + json.tickets + ' 张。');
  },
  error: function() {
    alert('fail');
  },
});

为什么我这次没有写 flightHandler 这个函数呢?而且竟然也运行成功了!jquery 在处理 Jsonp 类型的 ajax 时(虽然 jquery 也把 Jsonp 归入了 ajax,但其实它们真的不是一回事儿),自动帮你生成回调函数并把数据取出来供 success 属性方法来调用。

jsonp 错误捕获

如果你把 url 参数改成某个不存在的地址,你会惊奇的发现:虽然浏览器终端报出错误(404 或其他网络错误),但你的 error 回调却没有被执行!?

那怎么做才能使 Jsonp 的 error 回调被执行呢?

有两个方法,方法一:添加 timeout 参数。

$.ajax({
  url: 'https://api.github.com/users/jarontai/repos',
  type: 'GET',
  dataType: 'Jsonp', // dataType 为 Jsonp
  timeout: 5000, // 添加timeout参数
  success: function(data) {
    $('.result').text(JSON.stringify(data));
  },
  error: function(jqXHR, textStatus) {
    // 此时textStatus为‘timeout’
    $('.result').text('error');
    alert('Jsonp error!');
  },
});

添加 timeout 参数后,虽然 Jsonp 请求本身的错误没有被捕获,但是最终会因为超时而执行 error 回调。

方法二出场:使用 jquery Jsonp 插件 - https://github.com/jaubourg/jquery-jsonp

$.jsonp({
  url: 'https://api.github.com/users/jarontai/repos',
  callbackParameter: 'callback',
  timeout: 5000,
  error: function(xOptions, textStatus) {
    // 错误发生时,立即执行
    $('.result').text('error');
    alert('Jsonp error!');
  },
  success: function(data) {
    $('.result').text(JSON.stringify(data));
  },
});

使用 jsonp 插件,能够在错误发生时立即执行 error 回调,并且还附带如 数据过滤 等功能

jsonp 封装

/**
 * [function 在页面中注入js脚本]
 * @param  {[type]} url     [description]
 * @param  {[type]} charset [description]
 * @return {[type]}         [description]
 */
util.createScript = function(url, charset) {
  var script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  charset && script.setAttribute('charset', charset);
  script.setAttribute('src', url);
  script.async = true;
  return script;
};

/**
 * [function 获取一个随机的5位字符串]
 * @param  {[type]} prefix [description]
 * @return {[type]}        [description]
 */
util.getName = function(prefix) {
  return (
    prefix +
    Math.random()
      .toString(36)
      .replace(/[^a-z]+/g, '')
      .substr(0, 5)
  );
};

/**
 * [function jsonp]
 * @param  {[type]} url      [description]
 * @param  {[type]} onsucess [description]
 * @param  {[type]} onerror  [description]
 * @param  {[type]} charset  [description]
 * @return {[type]}          [description]
 */
util.jsonp = function(url, onsuccess, onerror, charset) {
  var callbackName = util.getName('tt_player');
  window[callbackName] = function() {
    if (onsuccess && util.isFunction(onsuccess)) {
      onsuccess(arguments[0]);
    }
  };
  var script = util.createScript(url + '&callback=' + callbackName, charset);
  script.onload = script.onreadystatechange = function() {
    if (!script.readyState || /loaded|complete/.test(script.readyState)) {
      script.onload = script.onreadystatechange = null;
      // 移除该script的 DOM 对象
      if (script.parentNode) {
        script.parentNode.removeChild(script);
      }
      // 删除函数或变量
      window[callbackName] = null;
    }
  };
  script.onerror = function() {
    if (onerror && util.isFunction(onerror)) {
      onerror();
    }
  };
  document.getElementsByTagName('head')[0].appendChild(script);
};

1.4.2. Hash 原理

页面 A 中通过 iframe 嵌入 B, 需求是 A 给 B 发消息

  1. 拿到 B 的 url
  2. 改变 B 的 hash
  3. B 中接受 onhashchange
// 利用hash,场景是当前页面 A 通过 iframe 嵌入了跨域的页面 B
// 在A中伪代码如下:
var B = document.getElementsByTagName('iframe');
B.src = B.src + '#' + 'data';

// 在B中的伪代码如下
window.onhashchange = function() {
  var data = window.location.hash;
};

1.4.3. WebSocket

WebSocket 是双全工、实时、基于 frame 的 tcp 通信协议,使用 ws://(非加密)和 wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面代码中,有一个字段是 Origin,表示该请求的请求源(origin),即发自哪个域名。因为有了 Origin 这个字段,而且非 http 协议,所以 WebSocket 才没有实行同源政策。

  1. var ws = new WebSocket('wss://echo.websocket.org');
  2. onopen, onmessage, onclose
// 客户端
var ws = new WebSocket('wss://echo.websocket.org');

ws.onopen = function(e) {
  console.log('Connection open ...');
  ws.send('Hello WebSockets!');
};

ws.onmessage = function(e) {
  console.log('Received Message: ', e.data);
  ws.close();
};

ws.onclose = function(e) {
  console.log('Connection closed.');
};

1.4.4. postMessage

窗口 A(http:A.com)向跨域的窗口 B(http:B.com)发送信息

// A 中代码
window.postMessage('Hi B, from A', 'http://B.com');

// B中监听代码
window.addEventListener(
  'message',
  function(event) {
    console.log(event.origin);
    console.log(event.source);
    console.log(event.data);
  },
  false,
);

这是一个安全的跨域通信方法,postMessage(message, targetOrigin) 也是 HTML5 引入的特性。 可以给任何一个 window 发送消息,不论是否同源。第二个参数可以是 *,但如果你设置了一个 URL 但不相符,那么该事件不会被分发。看一个普通的使用方式吧:

/*
 * In window A's scripts, with A being on <http://example.com:8080>:
 */

var popup = window.open(...popup details...);

// This does nothing, assuming the window hasn't changed its location.
popup.postMessage("The user is 'bob' and the password is 'secret'",
                  "https://secure.example.net");

// This will successfully queue a message to be sent to the popup, assuming
// the window hasn't changed its location.
popup.postMessage("hello there!", "http://example.com");

function receiveMessage(event)
{
  // Do we trust the sender of this message?  (might be
  // different from what we originally opened, for example).
  if (event.origin !== "http://example.com")
    return;

  // event.source is popup
  // event.data is "hi there yourself!  the secret response is: rheeeeet!"
}
window.addEventListener("message", receiveMessage, false);
/*
 * In the popup's scripts, running on <http://example.com>:
 */

// Called sometime after postMessage is called
function receiveMessage(event) {
  // Do we trust the sender of this message?
  if (event.origin !== 'http://example.com:8080') return;

  // event.source is window.opener
  // event.data is "hello there!"

  // Assuming you've verified the origin of the received message (which
  // you must do in any case), a convenient idiom for replying to a
  // message is to call postMessage on event.source and provide
  // event.origin as the targetOrigin.
  event.source.postMessage(
    'hi there yourself!  the secret response ' + 'is: rheeeeet!',
    event.origin,
  );
}

window.addEventListener('message', receiveMessage, false);

1.4.5. CORS 跨域资源共享 (Cross-Origin Resource Sharing)

参考资料:

CORS 就是为了让 AJAX 可以实现可控的跨域访问而生的。

  1. 服务器设置 Access-Control-Allow-Origin HTTP 响应头之后,允许浏览器跨域请求。
  2. 浏览器在头信息之中,增加一个 Origin 字段。Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。
  3. 服务器根据这个值,决定是否同意这次请求。CORS 支持所有类型的 HTTP 请求

如果服务器同意,响应结果头:

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

step 1:

CORS 请求默认不发送 Cookie 和 HTTP 认证信息。如果要把 Cookie 发到服务器,服务器需指定 Access-Control-Allow-Credentials 字段。

Access-Control-Allow-Credentials: true

step 2:

客户端方面,在 AJAX 请求中打开 withCredentials 属性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理。

但是,如果省略 withCredentials 设置,有的浏览器还是会一起发送 Cookie。这时,可以显式关闭 withCredentials

xhr.withCredentials = false;

需要注意的是,如果要发送 Cookie,Access-Control-Allow-Origin 就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨源)原网页代码中的 document.cookie 也无法读取服务器域名下的 Cookie。

// CORS 参考资料: http://www.ruanyifeng.com/blog/2016/04/cors.html

// url(必选),options(可选)
// 实现了 CORS 通信

let myHeaders = new Headers({
  'Access-Control-Allow-Origin': '*',
  'Content-Type': 'text/plain',
});
fetch(url, {
  method: 'GET',
  headers: myHeaders,
  mode: 'cors',
}).then((res) => {
  // TODO
});

为什么 fetch、CORS 会实现跨域通信?

ajax 跨域请求默认会被浏览器拦截,fetch、CORS 在请求头加入了 origin,服务器端设置了 Access-Control-Allow-Origin 发在源是允许就返回数据。

Copyright © Guanghui Wang all right reserved,powered by GitbookFile Modified: 2019-08-25 13:56:34

results matching ""

    No results matching ""