JSONP

实现背景

浏览器通常允许跨域资源嵌入,例如:

  • < script >< /script > 标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。
  • < link rel="stylesheet" > 标签嵌入 CSS。由于 CSS 的松散的语法规则,CSS 的跨域需要一个设置正确的 Content-Type 消息头。不同浏览器有不同的限制
  • < img >嵌入图片。支持的图片格式包括 PNG,JPEG,GIF,BMP,SVG,...
  • < video\ > 和 < audio\ >嵌入多媒体资源。
  • < object\ >, < embed\ > 和 < applet\ > 的插件。
  • @font-face 引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。
  • < frame\ > 和 < iframe\ > 载入的任何资源。站点可以使用 X-Frame-Options 消息头来阻止这种形式的跨域交互。

所以可以用它来实现跨域,用破解的方式.

当服务端支持 JSONP 技术时,会做如下一些设置:

  • 识别请求的 URL,提取 callback 参数的值,并动态生成一个执行该参数值(一个函数)的 JavaScript 语句;
  • 将需要返回的数据放入动态生成的函数中,等待其加在到页面时被执行.

JSONP 的客户端具体实现

让我们直接看看 JSONP 的使用方式:

// 创建 Jsonp 类
// 初始化时传入两个参数, url 是接口的url
// cb 是对于接口返回的参数的处理
function Jsonp(url, cb) {
  this.callbackName = 'jsonp_' + Date.now();
  this.cb = cb;
  this.url = url;
  this.init();
}

// 初始化方法 用于拼接 url
Jsonp.prototype.init = function() {
  if (this.url.indexOf('?')) {
    this.url = this.url + '&callback=' + this.callbackName;
  } else {
    this.url = this.url + '?callback=' + this.callbackName;
  }
  this.createCallback();
  this.createScript();
};

// 创建 script 标签, src 取接口请求的url
Jsonp.prototype.createScript = function() {
  var script = document.createElement('script');
  script.src = this.url;
  script.onload = function() {
    this.remove();
    // 删除 window 下定义的无用方法
    delete window[this.callbackName];
  };
  document.body.appendChild(script);
};

// 绑定回调函数
Jsonp.prototype.createCallback = function() {
  window[this.callbackName] = this.cb;
};

// 创建 jsonp 实例, 并指定回调函数
new Jsonp('http://localhost:8888/', function(data) {
  console.log(data);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

jQuery 如何实现 jsonp 调用?

jQuery(document).ready(function() {
  $.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');
    }
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

如果不想把整个 jQuery 引入,可以下载一个 npm 模块 jsonp-client(更小)或者 jsonp-retry(更强大)

JSONP 的服务端具体实现

npm install jsonp-body --save
1
var koa = require('koa');
var jsonp = require('jsonp-body');
js;
var app = koa();
app.use(function*() {
  this.set('X-Content-Type-Options', 'nosniff');
  if (this.query.callback) {
    this.set('Content-Type', 'text/javascript');
  } else {
    this.set('Content-Type', 'application/json');
  }
  this.body = jsonp({ foo: 'bar' }, this.query.callback);
});
1
2
3
4
5
6
7
8
9
10
11
12
13

CORS

客户端细节

CORS 详解笔记

服务端细节

node

const http = require('http');

const PORT = 8888;

// 协议名必填, 如果同时存在 http 和 https 就写两条s
const allowOrigin = ['http://127.0.0.1:8080', 'http://localhost:8080', 'https://www.baidu.com'];

// 创建一个 http 服务
const server = http.createServer((request, response) = > {
  const { method, headers: { origin, cookie } } = request;
  if (allowOrigin.includes(origin)) {
    response.setHeader('Access-Control-Allow-Origin', origin);
  }
  response.setHeader('Access-Control-Allow-Methods', 'PUT');
  response.setHeader('Access-Control-Allow-Credentials', true);
  response.setHeader('Access-Control-Allow-Headers', 'token');
  response.setHeader('Access-Control-Expose-Headers', 'token');
  response.setHeader('token', 'quanquan');
  if (method === 'OPTIONS') {
    response.writeHead(204);
    response.end('');
  } else if (!cookie) {
    response.setHeader('Set-Cookie', 'quanquan=fe');
  }
  response.end("{name: 'quanquan', friend: 'guiling'}");
});

// 启动服务, 监听端口
server.listen(PORT, () = > {
  console.log('服务启动成功, 正在监听: ', PORT);
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

express4

// 跨域处理
app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Credentials', true);
  res.header(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept'
  );
  res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Credentials', true);
  res.header('Access-Control-Max-Age', 5);
  next();
});
1
2
3
4
5
6
7
8
9
10
11
12
13

Nginx 反向代理

nginx 配置

server {
    # 监听80端口号
    listen 80;

    # 监听访问的域名
    server_name a.com;

    # 根据访问路径配置
    location / {
        # 把请求转发到 http://127.0.0.1:9999
        proxy_pass http://127.0.0.1:9999;

        # 兼容websocket
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # 监听根目录下的 /api 路径
    location /api/ {
        # 把请求转发到 http://127.0.0.1:8888
        proxy_pass http://localhost:8888;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

不过有 cookie 相关问题,需要注意

WebSocket

HTML5 标准推出了 WebSocket 协议,使浏览器和服务器实现了双向通信,更妙的是,除了 IE9 及以下的 IE 浏览器,所有的浏览器都支持 WebSocket 协议。WebSocket 协议本身就不受浏览器“同源策略”的限制

下面是客户端告知服务端要升级为 WebSocket 协议的报头:

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
1
2
3
4
5
6
7
8

下面是服务端向客户端返回的响应报头:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
1
2
3
4
5
var ws = new WebSocket('wss://echo.websocket.org');
ws.send('Hi, server!');
1
2

传入的参数为响应 WebSocket 请求的地址。

同样类似 AJAX 的是,WebSocket 对象也有一个 readyState 属性,用来表示对象实例当前所处的链接状态,有四个值:

  • 表示正在连接中(CONNECTING);
  • 表示连接成功,可以通信(OPEN);
  • 表示连接正在关闭(CLOSING);
  • 表示连接已经关闭或打开连接失败(CLOSED);

除此之外,WebSocket 对象还提供给我们一系列事件属性,使我们控制连接过程中的通信行为:

  • onopen:用于指定连接成功后的回调函数;
  • onclose:用于指定连接关闭后的回调函数;
  • onmessage:用于指定收到服务器数据后的回调函数;
  • onerror:用于指定报错时的回调函数;

ServerProxy

浏览器有同源策略的限制服务器没有. 我们的前端项目托管在后端项目中所以访问我们自己的后端不跨域. 我们的后端请求第三方服务没有限制.

所以 ServerProxy 的原理大概就是通过页面通过 ajax 访问同域后端服务,后端服务访问目标服务并将目标服务返回的内容透传给前端.

TOC