阅读 3082

【小哥哥, 跨域要不要了解下】JSONP

系列文章:

打算纯前端做一个接口测试工具, 直到遇到

2018-12-05-14-13-31
这个报错, 触碰到了知识盲区了, 怎么办???
2018-12-05-14-14-58

还好, 有谷哥和度娘. 原来是跨域 随手整理了一下常用的跨域方式处理方案, 这里马上分享给大家 😋

ps: 为了保证前后端编码的一致性, 本系列文章中涉及部分后端内容. 后端统一使用原生 nodejs 来搞, 请奔走相告.

准备工作

为了托管我们的静态页面, 我们需要一个可以提供服务器环境的插件, 这里推荐 live-server, 通过命令 npm i -g live-server 安装即可. 该插件支持html文件热更新. 那用户体验简直飞起. 一键启动, 只需要在需要托管的目录执行 live-server . 即可.

9150e4e5ly1fvmnifqdj2g207i07iaa4

ps: live-server 依赖 nodejs, 没有安装的小伙伴, 请参照这篇文章安装 nodejs.

AJAX 访问接口跨域解决方案

首先, 更正几个常见的错误认识:

  1. 同源策略是浏览器的行为, 和 js 关系不大.
  2. 所谓跨域是指请求发起方页面所在的 url 与访问的 api 存在协议, 域名, 端口中任意一个不同即视为跨域. 并不单单是指域名.
  3. 跨域这个东西, 日常工作中并不是很常用. 你想, 谁会闲的没事儿干总是请求人家别人的 api 去.

jsonp

可能有小伙伴会说. 圈圈, 你扯淡, 既然浏览器有跨域限制. 为什么我司项目从 bootcdn, 引入的 jquery 依然跑在信息高速路上, 没有任何低头的意思?

hhh, 😄. 这个质疑提的好. 浏览器同源策略禁止的是 ajax 请求. 然鹅, jquery 是一个 js 文件. 不受该策略的限制.

我尼玛, 那到底是啥限制啥不限制嘛???

2018-12-05-16-05-22

根据 MDN (自备梯子), 对于浏览器的同源策略的解释, 不受限制的外域资源加载情况有以下几种:

  • script
  • link
  • img
  • video
  • object embed applet
  • font-face 有的浏览器允许, 有的禁止
  • frame

那么问题来了, 挖掘机学校..., 不好意思走错片场了. 既然有这么多方式可以绕过浏览器同源策略的限制. 那么, 是不是我们可以做一点事情呢 ^_^

答: 是的 😄.

那还不抓紧搞起来?

6af89bc8gw1f8rf4zbaemg209105fka1

我们使用第一个特例 script 一步一步实现跨域访问 (jsonp).

  • 首先, 创建本次文章的项目目录
    2018-12-05-17-14-53
    目录中, be 代表是后端项目, fe 代表前端项目. jsonp 目录说明我们是用 jsonp 的方式实现跨域.
  • 在项目根目录下执行 live-server ./fe/jsonp/ 启动前端 web 容器
  • 编辑 ./fe/jsonp/ 目录下的 index.html 文件. 代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>jsonp 实现跨域</title>
</head>
<body>
    <h3>jsonp 实现跨域</h3>
</body>
</html>
复制代码
  • 浏览器访问 localhost:8080浏览器如下图说明前端 web 容器部署成功.

    2018-12-05-17-22-21

  • 编写后端代码, 编写 be/jsonp/index.js 文件, 文件内容如下

var http = require('http');
var PORT = 8888

// 创建一个 http 服务
var server = http.createServer(function(request, response) {
    response.end('hello world')
})

// 启动服务, 监听端口
server.listen(PORT, function() {
    console.log('服务启动成功, 正在监听: ', PORT)
})
复制代码
  • 编写完成后命令行执行 node ./be/jsonp/index.js 命令行中出现

    2018-12-05-17-57-06
    说明后端程序启动成功.此时可以通过浏览器访问 localhost:8888获得 hello world
    2018-12-05-19-15-36

  • 下来, 我们在前端的 index.html 中尝试通过 ajax 请求 http://localhost:8888/ 来获取返回数据, 添加如下代码, 添加以后代码

<script>
    var xhr = new XMLHttpRequest()
    xhr.open('GET', 'http://localhost:8888/')
    xhr.send()
</script>
复制代码

回到浏览器, 查看页面控制台, 熟悉的错误出现了. Access to XMLHttpRequest at http://localhost:8888/ from origin http://127.0.0.1:8080 has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 这个错误说明了, 我们是不能通过 ajax 的方式从 http://127.0.0.1:8080 访问 http://localhost:8888/ 的.

既然不能通过 ajax 实现跨域的访问, 同时 mdn 又说 script 标签不受同源策略的限制. 那么, 我们尝试一下用 script 标签引入 http://localhost:8888/ 试试?

2018-12-05-19-29-19

此时的代码, 网络请求没有问题. 知识报了 js 文件不合法的问题. 如果我们把接口返回的数据调整为规范的 js 是不是, 嗯哼???

干起, 修改后端代码, 返回的内容由 hello world 改为 console.log('hello world'), 修改后的代码(修改完后端代码以后切记重启服务哈 ^_^)

不得了, 不得了, 返回的结果不紧没有报错, 甚至可以执行. 我们从后端返回的 hellow world 成功的答应到了控制台了.

2018-12-05-20-15-32

试想一下, 如果我们通过 js 文件里定义一个变量用于存放后端返回给前端的数据, 前端插入一个 script 标签, 把后端返回的变量定义执行一把. 那样定义的变量岂不是就可以在全局可以获取到后端定义的变量了. 赶紧试一把 😄

首先修改后端代码, 只需要调整一行.(修改完后端代码以后切记重启服务哈 ^_^)

response.end("var aaaa = {name: 'quanquan', friend: 'guiling'}");
复制代码

其次调整前端代码

<script>
    // 第一次因为还没有引入外部 js 所以打印 undefined
    console.log(window.aaaa)
    // 1 秒后, 外部 js 加载完成, 能打印出后端返回的变量定义
    setTimeout(() => {console.log(window.aaaa)}, 1e3)
</script>
<script src="http://localhost:8888/"></script>
复制代码

当前代码, 通过这种方式, 我们能够成功的获取到后端返回的数据. 但是, 接口这个东西时快时慢. 写个定时器轮询? 有点不够 666, 肿么办?

2018-12-05-21-15-37

======================== 思考 5 分钟 ========================

2018-12-05-21-18-13

======================== 5 分钟已过 ========================

既然, 写在 script 标签上的内容是可以直接执行的. 那么, 如果我们把变量的定义改写成一个函数的执行可不可以呢 ^_^, 试试?

后端(修改完后端代码以后切记重启服务哈 ^_^)

response.end("aaaa({name: 'quanquan', friend: 'guiling'})");
复制代码

前端

<script>
    // 由于后端返回的内容即将调用函数 aaaa, 那我们就预先定义一个呗, 这东西就叫回调函数
    function aaaa(param) {
        console.log('后端返回的参数是: ', param)
    }
</script>
<script src="http://localhost:8888/"></script>
复制代码

结果

2018-12-05-21-30-30

此时代码, 目前为止, 我们已经彻底解决了跨域的问题. 很靠谱有木有? 当然木有. 这个玩意儿只是说明了 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)
})
复制代码

后端(修改完后端代码以后切记重启服务哈 ^_^)

const http = require('http');
// 新引入了 url 模块, 主要用于解析请求参数
const url = require('url');

const PORT = 8888;

// 创建一个 http 服务
const server = http.createServer((request, response) => {
  // 获取前端请求数据
  const queryObj = url.parse(request.url, true).query;
  // 这里把前端传来的 callback 字段作为后端返回的回调函数的函数名称
  response.end(`${queryObj.callback}({name: 'quanquan', friend: 'guiling'})`);
});

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

目前代码, 至此我们已经能够顺利的获取跨域资源了. 👏🏻.

下集预告: jsonp 是一种传统的跨域解决方案, 关于这种方式的优缺点, 请度娘, 下一节, 我们一起学习相对比较现代一点的跨域解决方案. CORS, See You