

> 本文最初发表于[博客园](https://www.cnblogs.com/smyhvae/p/8523576.html)，并在[GitHub](https://github.com/qianguyihao/Web)上持续更新**前端的系列文章**。欢迎在GitHub上关注我，一起入门和进阶前端。

> 以下是正文。


## 前言

从本章起，对代码的要求没之前那么高了，但是，要求你对知识面的掌握要足够宽。

前端通信类的问题，主要包括以下内容：

- 1、什么是**同源策略**及限制

同源策略是一个概念，就一句话。有什么限制，就三句话。能说出来即可。

- 2、**前后端如何通信**

如果你不准备，估计也就只能说出ajax。这个可以考察出知识面。

- 3、如何创建**Ajax**

Ajax在前后端通信中经常用到。做业务时，可以借助第三方的库，比如vue框架里的库、jQuery也有封装好的方法。但如果让你用原生的js去实现，该怎么做？

这就是考察你的动手能力，以及框架原理的掌握。如果能写出来，可以体现出你的基本功。是加分项。

- 4、**跨域通信**的几种方式

这部分非常重要。无非就是问你：什么是跨域、跨域有什么限制、**跨域有几种方式**。

下面分别讲解。


## 同源策略的概念和具体限制

**同源策略**：限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。（来自MDN官方的解释）

具体解释：

（1）`源`包括三个部分：协议、域名、端口（http协议的默认端口是80）。如果有任何一个部分不同，则`源`不同，那就是跨域了。

（2）`限制`：这个源的文档没有权利去操作另一个源的文档。这个限制体现在：（要记住）

- Cookie、LocalStorage和IndexDB无法获取。

- 无法获取和操作DOM。

- 不能发送Ajax请求。我们要注意，Ajax只适合**同源**的通信。

## 前后端如何通信

主要有以下几种方式：

- Ajax：不支持跨域。

- WebSocket：不受同源策略的限制，支持跨域。

- CORS：不受同源策略的限制，支持跨域。一种新的通信协议标准。可以理解成是：**同时支持同源和跨域的Ajax**。

## 如何创建Ajax

> 关于Ajax请求，可以看本人的基础文章：[Ajax入门和发送http请求](https://github.com/smyhvae/Web/blob/master/08-Ajax/02-Ajax%E5%85%A5%E9%97%A8%E5%92%8C%E5%8F%91%E9%80%81http%E8%AF%B7%E6%B1%82.md)

在回答 Ajax 的问题时，要回答以下几个方面：

- 1、XMLHttpRequest 的工作原理

- 2、兼容性处理

XMLHttpRequest只有在高级浏览器中才支持。在回答问题时，这个兼容性问题不要忽略。

- 3、事件的触发条件

- 4、事件的触发顺序

XMLHttpRequest有很多触发事件，每个事件是怎么触发的。

### 发送 Ajax 请求的五个步骤（XMLHttpRequest的工作原理）

（1）创建XMLHttpRequest 对象。

（2）使用open方法设置请求的参数。open(method, url, 是否异步)。

（3）发送请求。

（4）注册事件。 注册onreadystatechange事件，状态改变时就会调用。

如果要在数据完整请求回来的时候才调用，我们需要手动写一些判断的逻辑。

（5）获取返回的数据，更新UI。

### 发送 get 请求和 post 请求

get请求举例：

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<h1>Ajax 发送 get 请求</h1>
<input type="button" value="发送get_ajax请求" id='btnAjax'>

<script type="text/javascript">
    // 绑定点击事件
    document.querySelector('#btnAjax').onclick = function () {
        // 发送ajax 请求 需要 五步

        // （1）创建异步对象
        var ajaxObj = new XMLHttpRequest();

        // （2）设置请求的参数。包括：请求的方法、请求的url。
        ajaxObj.open('get', '02-ajax.php');

        // （3）发送请求
        ajaxObj.send();

        //（4）注册事件。 onreadystatechange事件，状态改变时就会调用。
        //如果要在数据完整请求回来的时候才调用，我们需要手动写一些判断的逻辑。
        ajaxObj.onreadystatechange = function () {
            // 为了保证 数据 完整返回，我们一般会判断 两个值
            if (ajaxObj.readyState == 4 && ajaxObj.status == 200) {
                // 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
                // 5.在注册的事件中 获取 返回的 内容 并修改页面的显示
                console.log('数据返回成功');

                // 数据是保存在 异步对象的 属性中
                console.log(ajaxObj.responseText);

                // 修改页面的显示
                document.querySelector('h1').innerHTML = ajaxObj.responseText;
            }
        }
    }
</script>
</body>
</html>
```

post 请求举例：

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<h1>Ajax 发送 get 请求</h1>
<input type="button" value="发送put_ajax请求" id='btnAjax'>
<script type="text/javascript">

    // 异步对象
    var xhr = new XMLHttpRequest();

    // 设置属性
    xhr.open('post', '02.post.php');

    // 如果想要使用post提交数据,必须添加此行
    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

    // 将数据通过send方法传递
    xhr.send('name=fox&age=18');

    // 发送并接受返回值
    xhr.onreadystatechange = function () {
        // 这步为判断服务器是否正确响应
        if (xhr.readyState == 4 && xhr.status == 200) {
            alert(xhr.responseText);
        }
    };
</script>
</body>
</html>
```

### onreadystatechange 事件

注册 onreadystatechange 事件后，每当 readyState 属性改变时，就会调用 onreadystatechange 函数。

readyState：（存有 XMLHttpRequest 的状态。从 0 到 4 发生变化）

- 0: 请求未初始化

- 1: 服务器连接已建立

- 2: 请求已接收

- 3: 请求处理中

- 4: 请求已完成，且响应已就绪

### 事件的触发条件

![](http://img.smyhvae.com/20180307_1443.png)

### 事件的触发顺序

![](http://img.smyhvae.com/20180307_1445.png)

上图的参考链接：

- [你真的会使用XMLHttpRequest吗？](https://segmentfault.com/a/1190000004322487)


### 实际开发中用的 原生Ajax请求

```javascript

    var util = {};

    //获取 ajax 请求之后的json
    util.json = function (options) {

        var opt = {
            url: '',
            type: 'get',
            data: {},
            success: function () {
            },
            error: function () {
            },

        };
        util.extend(opt, options);
        if (opt.url) {
            //IE兼容性处理：浏览器特征检查。检查该浏览器是否存在XMLHttpRequest这个api，没有的话，就用IE的api
            var xhr = XMLHttpRequest ? new XMLHttpRequest() : new window.ActiveXObject('Microsoft.XMLHTTP');

            var data = opt.data,
                url = opt.url,
                type = opt.type.toUpperCase();
            dataArr = [];
        }

        for (var key in data) {
            dataArr.push(key + '=' + data[key]);
        }

        if (type === 'GET') {
            url = url + '?' + dataArr.join('&');
            xhr.open(type, url.replace(/\?$/g, ''), true);
            xhr.send();
        }

        if (type === 'POST') {
            xhr.open(type, url, true);
            // 如果想要使用post提交数据,必须添加此行
            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhr.send(dataArr.join('&'));
        }

        xhr.onload = function () {
            if (xhr.status === 200 || xhr.status === 304) { //304表示：用缓存即可。206表示获取媒体资源的前面一部分
                var res;
                if (opt.success && opt.success instanceof Function) {
                    res = xhr.responseText;
                    if (typeof res === 'string') {
                        res = JSON.parse(res);  //将字符串转成json
                        opt.success.call(xhr, res);
                    }
                }
            } else {
                if (opt.error && opt.error instanceof Function) {
                    opt.error.call(xhr, res);
                }
            }
        };
    }


```

Ajax 的推荐链接：<https://segmentfault.com/a/1190000006669043>

## 跨域通信的几种方式

方式如下：

- 1、JSONP

- 2、WebSocket

- 3、CORS

- 4、Hash

- 5、postMessage

上面这五种方式，在面试时，都要说出来。

### 1、JSONP

面试会问：JSONP的原理是什么？怎么实现的？

在CORS和postMessage以前，我们一直都是通过JSONP来做跨域通信的。

**JSONP的原理**：通过`<script>`标签的异步加载来实现的。比如说，实际开发中，我们发现，head标签里，可以通过`<script>`标签的src，里面放url，加载很多在线的插件。这就是用到了JSONP。

**JSONP的实现：**

比如说，客户端这样写：

```html
    <script src="http://www.smyhvae.com/?data=name&callback=myjsonp"></script>
```

上面的src中，`data=name`是get请求的参数，`myjsonp`是和后台约定好的函数名。
服务器端这样写：

```bash
        myjsonp({
            data: {}

        })
```


于是，本地要求创建一个myjsonp 的**全局函数**，才能将返回的数据执行出来。

**实际开发中，前端的JSONP是这样实现的：**

```html
<script>

    var util = {};

    //定义方法：动态创建 script 标签
    /**
     * [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 处理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标签
        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); //往html中增加这个标签，目的是把请求发送出去
    };

</script>

```

### 2、WebSocket

WebSocket的用法如下：

```javascript
    //

    var ws = new WebSocket('wss://echo.websocket.org'); //创建WebSocket的对象。参数可以是 ws 或 wss，后者表示加密。

    //把请求发出去
    ws.onopen = function (evt) {
        console.log('Connection open ...');
        ws.send('Hello WebSockets!');
    };


    //对方发消息过来时，我接收
    ws.onmessage = function (evt) {
        console.log('Received Message: ', evt.data);
        ws.close();
    };

    //关闭连接
    ws.onclose = function (evt) {
        console.log('Connection closed.');
    };
```

Websocket的推荐链接：<http://www.ruanyifeng.com/blog/2017/05/websocket.html>

面试一般不会让你写这个代码，一般是考察你是否了解 WebSocket概念，知道有这么回事即可。

### 3、CORS

CORS 可以理解成是**既可以同步、也可以异步**的Ajax。

fetch 是一个比较新的API，用来实现CORS通信。用法如下：

```javascript
      // url（必选），options（可选）
      fetch('/some/url/', {
          method: 'get',
      }).then(function (response) {  //类似于 ES6中的promise

      }).catch(function (err) {
        // 出错了，等价于 then 的第二个参数，但这样更好用更直观
      });
```

- CORS的推荐链接：<http://www.ruanyifeng.com/blog/2016/04/cors.html>

推荐链接里有详细的配置。

另外，如果面试官问：“CORS为什么支持跨域的通信？”

答案：跨域时，浏览器会拦截Ajax请求，并在http头中加Origin。

### 4、Hash

url的`#`后面的内容就叫Hash。**Hash的改变，页面不会刷新**。这就是用 Hash 做跨域通信的基本原理。

补充：url的`?`后面的内容叫Search。Search的改变，会导致页面刷新，因此不能做跨域通信。

**使用举例：**

**场景**：我的页面 A 通过iframe或frame嵌入了跨域的页面 B。

现在，我这个A页面想给B页面发消息，怎么操作呢？

（1）首先，在我的A页面中：

```javascript
    //伪代码
    var B = document.getElementsByTagName('iframe');
    B.src = B.src + '#' + 'jsonString';  //我们可以把JS 对象，通过 JSON.stringify()方法转成 json字符串，发给 B
```

（2）然后，在B页面中：

```javascript
    // B中的伪代码
    window.onhashchange = function () {  //通过onhashchange方法监听，url中的 hash 是否发生变化
        var data = window.location.hash;
    };
```

### 5、postMessage()方法

> H5中新增的postMessage()方法，可以用来做跨域通信。既然是H5中新增的，那就一定要提到。

**场景**：窗口 A (`http:A.com`)向跨域的窗口 B (`http:B.com`)发送信息。步骤如下。

（1）在A窗口中操作如下：向B窗口发送数据：


```javascript
	// 窗口A(http:A.com)向跨域的窗口B(http:B.com)发送信息
 	Bwindow.postMessage('data', 'http://B.com'); //这里强调的是B窗口里的window对象
```

（2）在B窗口中操作如下：

```javascript
    // 在窗口B中监听 message 事件
    Awindow.addEventListener('message', function (event) {   //这里强调的是A窗口里的window对象
        console.log(event.origin);  //获取 ：url。这里指：http://A.com
        console.log(event.source);  //获取：A window对象
        console.log(event.data);    //获取传过来的数据
    }, false);
```


## 我的公众号

想学习<font color=#0000ff>**代码之外的技能**</font>？不妨关注我的微信公众号：**千古壹号**（id：`qianguyihao`）。

扫一扫，你将发现另一个全新的世界，而这将是一场美丽的意外：

![](http://img.smyhvae.com/2016040102.jpg)

