# 网络请求

# 同源策略

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。 同源的判断标准是:协议、域名、端口 三者必须完全相同。

像以下形式可以为同源:

https://www.example.com/dir1/page1.html
https://www.example.com/dir2/page2.html
1
2

而以下形式为不同源:

https://www.example.com/dir1/page1.html
http://www.example.com/dir2/page2.html (协议不同)
https://www.example2.com/dir2/page2.html (域名不同)
https://www.example.com:8000/dir2/page.html (端口不同)
1
2
3
4

由于同源策略的限制:

  • 节点脚本无法读取非同源页面的 Cookie、DOM 和 JS 对象。
  • 节点脚本无法与非同源资源建立 XHR 请求。
  • iframe 无法与框架之外的非同源内容进行交互。

# 跨域

跨域是指域名、端口或协议不同的两个页面进行的资源交互。由于同源策略的限制,跨域资源交互是被禁止的。

浏览器同源政策及其规避方法 (opens new window)

跨域资源共享 CORS 详解 (opens new window)

浏览器为了安全遵循同源策略 (opens new window)(域名+端口+协议),控制不同源之间的交互

img

如果非同源,共有三种行为受到限制。

(1) Cookie、LocalStorage 和 IndexDB 无法读取。

(2) DOM 无法获得。

(3) AJAX 请求不能发送。

# 解决跨域

# CORS

跨域资源共享(CORS,Cross-Origin Resource Sharing)是浏览器为 AJAX 请求设置的一种跨域机制,让其可以在服务端允许的情况下进行跨域访问。主要通过 HTTP 响应头来告诉浏览器服务端是否允许当前域的脚本进行跨域访问。

# 简单请求和非简单请求

跨域资源共享将 AJAX 请求分成了两类:简单请求和非简单请求。其中简单请求符合下面 2 个特征。

  • 请求方法为 GETPOSTHEAD

  • 请求头只能使用下面的字段:

    • Accept
    • Accept-Language
    • Content-Type (只限于 text/plain、multipart/form-data、application/x-www-form-urlencoded)
    • Content-Language

任意一条要求不符合的即为非简单请求, 非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT 或DELETE,或者Content-Type字段的类型是application/json等。

# 处理简单请求

对于简单请求,处理流程如下:

  • 浏览器发出简单请求的时候,会在请求头部增加一个 Origin字段,对应的值为当前请求的源信息;
  • 当服务端收到请求后,会根据请求头字段 Origin做出判断后返回相应的内容。
  • 浏览器收到响应报文后会根据响应头部字段Access-Control-Allow-Origin 进行判断,这个字段值为服务端允许跨域请求的源,其中通配符“*”表示允许所有跨域请求。如果头部信息没有包含Access-Control-Allow-Origin 字段或者响应的头部字段 Access-Control-Allow-Origin不允许当前源的请求,则会抛出错误。

# 处理非简单请求

当处理非简单的请求时,浏览器会先发出一个预检请求(Preflight)。这个预检请求为 OPTIONS 方法,并会添加了 1 个请求头部字段Access-Control-Request-Method,值为跨域请求所使用的请求方法。

如果添加了不属于上述简单请求的头部字段,所以浏览器在请求头部添加了Access-Control-Request-Headers 字段,值为跨域请求添加的请求头部字段 authorization。

在服务端收到预检请求后,除了在响应头部添加 Access-Control-Allow-Origin 字段之外,至少还会添加Access-Control-Allow-Methods 字段来告诉浏览器服务端允许的请求方法,并返回 204 状态码。

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

# JSONP

JSONP(JSON with Padding)的大概意思就是用 JSON 数据来填充,怎么填充呢?结合它的实现方式可以知道,就是把 JSON 数填充到一个回调函数中。这种比较 hack 的方式,利用的是 script 标签跨域引用 js 文件不会受到浏览器同源策略的限制。

 function jsonp({url,params,cb}) {
     return new Promise((resolve,reject)=>{
       // 处理传参成x=a&y=b的形式
       params = {...params,cb}
       let arrs = [];
       for(let key in params){
         arrs.push(`${key}=${params[key]}`);
       }
       
       let script = document.createElement('script'); // 标签
       window[cb] = function (data) {
         resolve(data);
         document.body.removeChild(script);
       }
       script.src = `${url}?${arrs.join('&')}`;
       document.body.appendChild(script);
     });
 }

    // 只能发送get请求 不支持post put delete
    // 不安全 xss攻击  不采用
    jsonp({
      url: 'http://localhost:3000/say',
      params:{wd:'我爱你'},
      cb:'show'
    }).then(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

自写服务端

let express = require('express');
let app = express();

app.get('/say',function (req,res) {
  let {wd,cb} = req.query;
  console.log(wd); 
  res.end(`${cb}('我不爱你')`) 
})
app.listen(3000);
1
2
3
4
5
6
7
8
9

# 代理转发

在服务端进行跨域,比如设置代理转发

webpack-dev-server

// webpack.config.js
module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': 'http://localhost:3000'
    }
  }
};
1
2
3
4
5
6
7
8
9

在 Nginx 服务器上配置同样的转发规则也非常简单,下面是示例配置。

location /api {
    proxy_pass   http://localhost:3000;
}
1
2
3

通过 location 指令匹配路径,然后通过 proxy_pass 指令指向代理地址即可。

或者nginx配置允许跨域,如下:

location / {
    add_header Access-Control-Allow-Origin *;
}
1
2
3

# WebSocket协议跨域

WebSocket 协议本身就支持跨域通信。它通过 HTTP 协议进行握手,握手成功后,服务器和客户端就建立了直接的 TCP 连接,两者之间的通信就不再受同源策略的限制。 使用 WebSocket 跨域主要有以下几个步骤:

  1. 服务器实现 WebSocket,监听某个端口(如:8000)
  2. 客户端通过 ws:// 或者 wss:// (SSL 加密) 访问 WebSocket 服务器
  3. 客户端通过 WebSocket 连接服务器后,就可以通过 send() 方法跨域发送消息
  4. 服务器接收消息后,也可以通过 send() 方法跨域回传消息
  5. 客户端监听 onmessage 事件接收服务器消息,完成跨域通信 示例代码:

服务端

const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 8000 })

wss.on('connection', function(ws) {
  ws.on('message', function(data) {
  console.log('received: %s', data)
  ws.send('response')
  })
})
1
2
3
4
5
6
7
8
9

客户端

const ws = new WebSocket('ws://localhost:8000')

ws.onopen = () => {
	ws.send('message')
}

ws.onmessage = (e) => {
	console.log(e.data)  // response
}
1
2
3
4
5
6
7
8
9

WebSocket 跨域的优点:

  • 支持双向通信,更实时
  • 支持复杂类型的跨域传输(blob、arraybuffer 等)
  • 封装性好,API 简单,容易使用 缺点:
  • 兼容性稍差,老版浏览器需要 polyfill
  • 建立连接需要握手,会有一定性能开销
  • 面向消息通信,会话状态需要自己维护 所以,如果需要双向通信,或传输复杂格式跨域数据,WebSocket 是一个很好的选择。但也需要权衡浏览器兼容性和性能开销。 理解 WebSocket 跨域机制,可以更熟练和深入地使用这一优秀的 web 通信手段。

# 页面跨域解决方案

除了浏览器请求跨域之外,页面之间也会有跨域需求,例如使用 iframe 时父子页面之间进行通信。

# postMessage

postMessage是HTML5新增的特性,它允许跨窗口通信,Implemented 一个窗口中的代码发送消息给另一个窗口,实现跨域通信。 使用postMessage跨域,主要分为三步:

  1. 窗口A(http://A.com)向窗口B(http://B.com)发送消息:
window.postMessage(' message ', 'http://B.com')
1

第一个参数是要发送的消息,第二个参数是接收窗口的源(origin),即协议 + 域名 + 端口。

  1. 窗口B设置消息监听,接收窗口A的消息:
window.addEventListener('message', function(e) {
console.log(e.data)  // message
console.log(e.origin)  // http://A.com
}, false)
1
2
3
4
  1. 窗口A发送消息时,需要完整的目标源作为第二个参数,否则窗口B拒绝接收。
  2. 回传数据时,需要指定发送窗口的源,否则A窗口会拒绝接收,以防止越域消息:
e.source.postMessage(' response ', 'http://A.com')
1

所以,postMessage机制实现跨域通信需要遵循的规则是:

  • 发送消息时指定接收窗口的完整源
  • 接收消息窗口判断消息来源,确定是否接收
  • 回传消息时指定消息来源的完整源 postMessage跨域通信的优点:
  • 简单易用,浏览器内置对象直接支持
  • 安全性高,可以防止 CSRF 攻击
  • 支持复杂类型传输,不仅限于字符串 缺点:
  • 只支持跨域通信,同源窗口间请直接使用事件等方式
  • IE8及以下不支持,需要 polyfill 实现 总之,postMessage是实现跨域通信的一种重要方式,具有一定的安全性和兼容性。理解其工作原理和规则,可以较好地避免跨域通信中的风险。

完整代码实例:

// https://lagou.com
var child = window.open('https://kaiwu.lagou.com');
child.postMessage('hi', 'https://kaiwu.lagou.com');


// https://kaiwu.lagou.com
window.addEventListener('message', function(e) {
  console.log(e.data);
},false);
1
2
3
4
5
6
7
8
9

# 改域

对于主域名相同,子域名不同的情况,可以通过修改 document.domain 的值来进行跨域。如果将其设置为其当前域的父域,则这个较短的父域将用于后续源检查。

比如,有一个页面,它的地址是 https://www.lagou.com/parent.html (opens new window),在这个页面里面有一个 iframe,其 src 是 http://kaiwu.lagou.com/child.html (opens new window)

这时只要把 http://www.lagou.com/parent.html (opens new window)http://kaiwu.lagou.com/child.html (opens new window) 这两个页面的 document.domain 都设成相同的域名,那么父子页面之间就可以进行跨域通信了,同时还可以共享 cookie。

但要注意的是,只能把 document.domain 设置成更高级的父域才有效果

例如在 http://kaiwu.lagou.com/child.html (opens new window) 中可以将 document.domain 设置成 lagou.com。

# window.name

window.name 属性可以利用来实现跨域通信。原因是:

  • window.name 属性有一个特征:在同一个窗口(或标签页)下导航到不同网址时,window.name 的内容会被自动带到新的网址中。
  • window.name 是全局作用域的,可以在不同的窗口(或标签页)间传递。 所以,利用这个特性,可以实现跨域通信:
  1. 第一个窗口(A域)将信息存入window.name:
window.name = 'message'
1

然后跳转到第二个窗口(B域)。

  1. 第二个窗口从window.name取出信息:
let data = window.name
1
  1. 第二个窗口跳转回第一个窗口,并将信息存入window.name:
window.name = 'response'
1
  1. 第一个窗口再次获取window.name,就能得到响应信息:
let response = window.name
1

此时,窗口A(http://A.com)和窗口B(http://B.com)就实现了跨域通信。 window.name 跨域的优点:

  • 所有浏览器都支持,无需 polyfill
  • 简单易用

缺点:

  • 只能携带字符串,不能跨域传输复杂类型
  • 容易被 CSRF 攻击
  • 如果window.name被第三方窗口修改过,会导致通信失败
  • 多个窗口同时通信时容易混淆 所以,比较理想的跨域通信方案应该是: 同源环境或低安全性场景下使用 window.name 高安全性场景下使用 postMessage 理解 window.name 跨域原理,可以在一定场景下简单便捷地解决跨域通信问题,但也需注意其潜在的安全风险。

# XMLHttpRequest

XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequest 在 AJAX 编程中被大量使用。

尽管名称如此,XMLHttpRequest 可以用于获取任何类型的数据,而不仅仅是 XML。它甚至支持 HTTP以外的协议(包括 file:// 和 FTP),尽管可能受到更多出于安全等原因的限制。

const xhr = new XMLHttpRequest()
1

# 发送请求

# open / send

  • open() :初始化一个请求

    • 参数1:method
    • 参数2:url
    • 参数3:默认true,为异步
  • setRequestHeader():设置 HTTP 请求头的值。必须在open()之后、send() 之前调用该方法。

  • send():发送请求。如果请求是异步的(默认),那么该方法将在请求发送后立即返回。

# readyState属性和onreadystatechange事件

readyState属性表示请求/响应过程的当前活动阶段。这个属性的值如下:

  • 0(UNSENT)未初始化。尚未调用open()方法。
  • 1(OPENED)启动。已经调用open()方法,但没有调用send()方法。
  • 2(HEADERS_RECEIVED)发送。已经调用send()方法,但尚未接收到响应。
  • 3(LOADING)接收。已经接收到部分响应数据。
  • 4(DONE)完成。已经接收到全部响应数据。

只要readyState属性的值发生变化,都会触发一次onreadystatechange事件。利用这个事件来检测每次状态变化后readyState的值。一般情况下只对readyState值为4的阶段做处理,这时所有数据都已经就绪。

xhr.onreadystatechange = function () {
  if(xhr.readyState !== 4) {
    return  
  }
  if(xhr.status >= 200 && xhr.status < 300) {
    console.log(xhr.responseText)
  }
}
1
2
3
4
5
6
7
8

# timeout属性和ontimeout事件

timeout属性表示请求在等待响应多少毫秒之后就终止。如果在规定的时间内浏览器还没有接收到响应,就会触发ontimeout事件处理程序。

//将超时设置为3秒钟
xhr.timeout = 3000 
// 请求超时后请求自动终止,会调用 ontimeout 事件处理程序
xhr.ontimeout = function(){
    console.log('请求超时了')
}
1
2
3
4
5
6

# overrideMimeType()方法

overrideMimeType()方法能够重写服务器返回的MIME类型,从而让浏览器进行不一样的处理

xhr.overrideMimeType('text/plain')
1

# responseType属性

responseType属性是一个字符串,表示服务器返回数据的类型。使用xhr.response属性来接收。

这个属性是可写的,可以在调用open()方法之后,send()方法之前设置这个属性的值,告诉服务器返回指定类型的数据。 如果responseType设为空字符串,等同于默认值text。

responseType属性可以设置的格式类型如下:

responseType属性的值 response属性的数据类型 说明
"" String字符串 默认值,等同于text(在不设置responseType时)
"text" String字符串 服务器返回文本数据
"document" Document对象 希望返回XML格式数据时使用
"json" javaScript对象 IE10/IE11不支持
"blob" Blob对象 服务器返回二进制对象
"arrayBuffer" ArrayBuffer对象 服务器返回二进制数组

当将responseType设置为一个特定的类型时,你需要确保服务器所返回的类型和你所设置的返回值类型是兼容的。那么如果两者类型不兼容,服务器返回的数据就会变成null,即使服务器返回了数据。

# withCredentials属性

withCredentials 属性是一个布尔值,表示跨域请求时是否协带凭据信息(cookie、HTTP认证及客户端SSL证明等)。默认为false。

const xhr = new XMLHttpRequest()
xhr.open('get', '/server', true)
xhr.withCredentials = true
xhr.send(null)
1
2
3
4

当配置了 withCredentials 为 true 时,必须在后端增加 response头 信息 Access-Control-Allow-Origin ,且必须指定域名,而不能指定为*。还要添加 Access-Control-Allow-Credentials 这个头信息为 true。

response.addHeader("Access-Control-Allow-Origin", "http://example.com")
response.addHeader("Access-Control-Allow-Credentials", "true")
1
2

GET 参数的编码方式

const xhr = new XMLHttpRequest()
// 使用encodeURIComponent()进行编码
const tempParam = encodeURIComponent('age')
const tempValue = encodeURIComponent('20')
xhr.open('get', '/server?tempParam=tempValue&money=100', true)
1
2
3
4
5

# 接收响应

# 响应头相关

getResponseHeader

getAllResponseHeaders

  • Content-Type:服务器告诉客户端响应内容的类型和采用字符编码。比如:Content-Type: text/html; charset=utf-8。
  • Content-Length:服务器告诉客户端响应实体的大小。比如:Content-Length: 8368。
  • Content-Encoding:服务器告诉客户端返回的的压缩编码格式。比如:Content-Encoding: gzip, deflate, br。
  • status:状态码
  • statusText:状态说明

# response属性

response属性表示服务器返回的数据。它可以是任何数据类型,比如字符串、对象、二进制对象等等,具体的类型由XMLHttpRequest.responseType属性决定。该属性只读。

https://juejin.cn/post/6844904052875067400

字节面试官:如何实现Ajax并发请求控制 (opens new window)

字节跳动面试官:请用JS实现Ajax并发请求控制 (opens new window)

# fetch

https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch

https://www.ruanyifeng.com/blog/2020/12/fetch-tutorial.html

  • 比传统ajax更简单,强大,可以看作XHR的升级版
  • 基于Promise实现
fetch('https://api.github.com/users/ruanyf')
  .then(response => response.json())
  .then(json => console.log(json)) //这里才是数据
  .catch(err => console.log('Request Failed', err)); 
1
2
3
4
  • response.text()是字符串
  • response.json()是JSON(前提是返回值符合JSON格式)

请求参数

  • method
  • body
  • headers

fetch()发出请求以后,有一个很重要的注意点:只有网络错误,或者无法连接时,fetch()才会报错,其他情况都不会报错,而是认为请求成功。

这就是说,即使服务器返回的状态码是 4xx 或 5xx,fetch()也不会报错(即 Promise 不会变为 rejected状态)。

只有通过Response.status属性,得到 HTTP 回应的真实状态码,才能判断请求是否成功。

async function fetchText() {
  let response = await fetch('/readme.txt');
  if (response.status >= 200 && response.status < 300) {
    return await response.text();
  } else {
    throw new Error(response.statusText);
  }
}
1
2
3
4
5
6
7
8

img

# 实现axios

function axios({ method, url, params, data }) {
    return new Promise((resolve, reject) => {

        method = method.toUpperCase()

        //1.创建对象
        const xhr = new XMLHttpRequest();

        //2.初始化
        let str = '';
        for (let k in params) {
            str += `${k}=${params[k]}&`;
        }
        if (str) {
            url = url + '?' + str.slice(0, -1);
        }


        xhr.open(method, url);

        //3.发送
        if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
            xhr.setRequestHeader('Content-type', 'application/json;charset=utf-8')
            //设置请求体
            xhr.send(JSON.stringify(data))
        } else {
            xhr.send();
        }

        //4.处理结果
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve({
                        status: xhr.status,
                        message: xhr.statusText,
                        body: JSON.parse(xhr.response)
                    })
                } else {
                    reject(new Error('request error status is ' + xhr.status))
                }
            }
        }
    })
}


axios.get = function (url, options) {
    return axios(Object.assign(options, { url, method: 'get' }))
}

//post delete put 同

// // 使用形式:
 axios({

     url: 'https://api.apiopen.top/getJoke',
     method: 'GET',
     params: {
         a: 1,
         b: 2
     },
     data: {
         a: 2,
         b: 4
     }
 }).then(
     response => {
         console.log(response)
     }).catch(error => {
         console.log(error)
     })


// 使用形式:
axios.get('https://api.apiopen.top/getJoke', {
    params: {
        a: 1,
        b: 2
    }
}).then(
    response => {
        console.log(response)
    }).catch(error => {
        console.log(error)
    })
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

面试官:Vue项目中有封装过axios吗?怎么封装的 (opens new window)