可谓同源?URL由协议、域名、端口和路径组成,若是两个URL的协议、域名和端口相同,则表示他们同源。浏览器的同源策略,限制了来自不一样源的"document"或脚本,对当前"document"读取或设置某些属性,即从一个域上加载的脚本不容许访问另一个域的文档属性。好比一个恶意网站的页面经过iframe嵌入了银行的登陆页面(两者不一样源),若是没有同源限制,恶意网页上的javascript脚本就能够在用户登陆银行的时候获取用户名和密码。所谓道高一尺魔高一丈,虽然浏览器以同源策略限制了咱们随意请求资源,可是从这个策略出现开始就有不少各类各样的Hacker技巧来。javascript
JSONP是较为经常使用的一种跨域方式,不受到浏览器兼容性的限制,可是由于它只能以GET动词进行请求,这样就破坏了标准的REST风格,比较丑陋。JSONP本质上是利用<script>
标签的跨域能力实现跨域数据的访问,请求动态生成的JavaScript脚本同时带一个callback函数名做为参数。其中callback函数本地文档的JavaScript函数,服务器端动态生成的脚本会产生数据,并在代码中以产生的数据为参数调用 callback函数。当这段脚本加载到本地文档时,callback函数就被调用。html
(1)浏览器端构造请求地址java
function resolveJson(result) { console.log(result.name); } var jsonpScript = document.createElement("script"); jsonpScript.type = "text/javascript"; jsonpScript.src = "http://www.qiute.com?callbackName=resolveJson"; document.getElementsByTagName("head")[0].appendChild(jsonpScript);
标准的Script标签的请求地址为:请求资源的地址+获取函数的字段名+回调函数名称,这里的获取函数的字段名是须要和服务端提早约定好,譬如jQuery中默认的获取函数名就是callback
。而resolveJson
是咱们默认注册的回调函数,注意,该函数名须要全局惟一,该函数接收服务端返回的数据做为参数,而函数内容就是对于该参数的处理。node
(2)服务端构造返回值webpack
在接受到浏览器端 script 的请求以后,从url的query的callbackName获取到回调函数的名字,例子中是resolveJson
。git
而后动态生成一段javascript片断去给这个函数传入参数执行这个函数。好比:es6
resolveJson({name: 'qiutc'});
(3)客户端以脚本方式执行服务端返回值github
服务端返回这个 script 以后,浏览器端获取到 script 资源,而后会当即执行这个 javascript,也就是上面那个片断。这样就能根据以前写好的回调函数处理这些数据了。web
跨域资源共享,Cross-Origin Resource Sharing是由W3C提出的一个用于浏览器以XMLHttpRequest方式向其余源的服务器发起请求的规范。不一样于JSONP,CORS是以Ajax方式进行跨域请求,须要服务端与客户端的同时支持。目前CORS在绝大部分现代浏览器中都是支持的:ajax
CORS标准定义了一个规范的HTTP Headers来使得浏览器与服务端之间能够进行协商来肯定某个资源是否能够由其余域的客户端请求得到。尽管不少的验证与鉴权是由服务端完成,可是本质上大部分的检查和限制仍是应该由浏览器完成。通常来讲CORS会分为Simple Request,简单请求与Preflight,须要预检的请求两大类。其基本的流程以下:
当浏览器的请求方式是HEAD、GET或者POST,而且HTTP的头信息中不会超出如下字段:
Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
时,浏览器会将该请求定义为简单请求,不然就是预检请求。预检请求会在正式通讯以前,增长一次HTTP查询请求。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可使用哪些HTTP动词和头信息字段。只有获得确定答复,浏览器才会发出正式的XMLHttpRequest请求,不然就报错。预检请求的发送请求:
OPTIONS /cors HTTP/1.1 Origin: http://api.qiutc.me Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.qiutc.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪一个源。
除了Origin字段,”预检”请求的头信息包括两个特殊字段:
Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。
预检请求的返回:
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.qiutc.me Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
Access-Control-Allow-Methods:必需,它的值是逗号分隔的一个字符串,代表服务器支持的全部跨域请求的方法。注意,返回的是全部支持的方法,而不单是浏览器请求的那个方法。这是为了不屡次”预检”请求。
Access-Control-Allow-Headers:若是浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,代表服务器支持的全部头信息字段,不限于浏览器在”预检”中请求的字段。
Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即容许缓存该条回应1728000秒(即20天),在此期间,不用发出另外一条预检请求。
一旦服务器经过了”预检”请求,之后每次浏览器正常的CORS请求,就都跟简单请求同样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
对于简单的跨域请求或者经过了预检的请求,浏览器会自动在请求的头信息加上Origin
字段,表示本次请求来自哪一个源(协议 + 域名 + 端口),服务端会获取到这个值,而后判断是否赞成此次请求并返回。典型的请求头尾:
// 请求 GET /cors HTTP/1.1 Origin: http://api.qiutc.me Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
若是服务端容许,在返回的头信息中会多出几个字段:
// 返回 Access-Control-Allow-Origin: http://api.qiutc.me Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: Info Content-Type: text/html; charset=utf-8
Access-Control-Allow-Origin:必须。它的值是请求时Origin字段的值或者 *
,表示接受任意域名的请求。
Access-Control-Allow-Credentials:可选。它的值是一个布尔值,表示是否容许发送Cookie。默认状况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie能够包含在请求中,一块儿发给服务器。
再须要发送cookie的时候还须要注意要在AJAX请求中打开withCredentials属性:var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
须要注意的是,若是要发送Cookie,Access-Control-Allow-Origin就不能设为*
,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其余域名的Cookie并不会上传,且原网页代码中的document.cookie
也没法读取服务器域名下的Cookie。
Access-Control-Expose-Headers:可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()
方
法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若是想拿到其余字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('Info')
能够返回Info字段的值。
若是服务端拒绝了调用,即不会带上 Access-Control-Allow-Origin
字段,浏览器发现这个跨域请求的返回头信息没有该字段,就会抛出一个错误,会被 XMLHttpRequest
的 onerror
回调捕获到。这种错误没法经过 HTTP 状态码判断,由于回应的状态码有多是200。
window.postMessage 是一个用于安全的使用跨源通讯的方法。一般,不一样页面上的脚本当且仅当执行它们的页面所处的位置使用相同的协议(一般都是 http)、相同的端口(http默认使用80端口)和相同的主机(两个页面的 document.domain 的值相同)只在这种状况下被容许互相访问。 而window.postMessage 提供了一个受控的机制来安全地绕过这一限制。其函数原型以下:
windowObj.postMessage(message, targetOrigin);
windowObj: 接受消息的 Window 对象。
message: 在最新的浏览器中能够是对象。
targetOrigin: 目标的源, * 表示任意。
调用postMessage方法的window对象是指要接收消息的那一个window对象,该方法的第一个参数message为要发送的消息,类型只能为字符串;第二个参数targetOrigin用来限定接收消息的那个window对象所在的域,若是不想限定域,可使用通配符 * 。须要接收消息的window对象,但是经过监听自身的message事件来获取传过来的消息,消息内容储存在该事件对象的data属性中。上面所说的向其余window对象发送消息,其实就是指一个页面有几个框架的那种状况,由于每个框架都有一个window对象。在讨论第种方法的时候,咱们说过,不一样域的框架间是能够获取到对方的window对象的,虽然没什么用,可是有一个方法是可用的-window.postMessage。下面看一个简单的示例,有两个页面:
//在主页面中获取子页面的句柄 var iframe =document.getElementById('iframe'); var iframeWindow = iframe.contentWindow; //向子页面发送消息 iframeWindow.postMessage("I'm message from main page."); //在子页面中监听获取消息 window.onmessage = function(e) { e = e || event; console.log(e.data); }
使用代理方式跨域更加直接,由于SOP的限制是浏览器实现的。若是请求不是从浏览器发起的,就不存在跨域问题了。使用本方法跨域步骤以下:
把访问其它域的请求替换为本域的请求
本域的请求是服务器端的动态脚本负责转发实际的请求
不过笔者在本身的开发实践中发现目前服务端跨域仍是颇有意义的,特别当咱们但愿从不支持CORS或者JSONP的服务端获取数据的时候,每每只能经过跨域请求。
JavaScript 经过XMLHttpRequest(XHR)来执行异步请求,这个方式已经存在了很长一段时间。虽然说它颇有用,但它不是最佳API。它在设计上不符合职责分离原则,将输入、输出和用事件来跟踪的状态混杂在一个对象里。并且,基于事件的模型与最近JavaScript流行的Promise以及基于生成器的异步编程模型不太搭。新的 Fetch API打算修正上面提到的那些缺陷。 它向JS中引入和HTTP协议中一样的原语。具体而言,它引入一个实用的函数 fetch() 用来简洁捕捉从网络上检索一个资源的意图。Fetch 规范 的API明确了用户代理获取资源的语义。它结合ServiceWorkers,尝试达到如下优化:
改善离线体验
保持可扩展性
而与jQuery
相比, fetch
方法与 jQuery.ajax()
的主要区别在于:
fetch()
方法返回的Promise对象并不会在HTTP状态码为404
或者500
的时候自动抛出异常,而须要用户进行手动处理
默认状况下,fetch并不会发送任何的本地的cookie到服务端,注意,若是服务端依靠Session进行用户控制的话要默认开启Cookie
window.fetch是基于XMLHttpRequest的浏览器的统一的封装,针对老的浏览器可使用Github的这个polypill。fetch基于ES6的Promise,在旧的浏览器中首先须要引入Promise的polypill,能够用这个:
$ bower install es6-promise
对于fetch的引入,能够用bower或者npm:
$ bower install fetch
$ npm install whatwg-fetch --save
若是是基于Webpack的项目,能够直接在Webpack的config文件中引入这种polyfill:
plugins: [ new webpack.ProvidePlugin({ 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' }) ]
这个插件的配置主要依靠imports-loader
与exports-loader
,所以也须要导入它们:
$ npm i imports-loader exports-loader -S
若是感受这种方式比较麻烦,也可使用 isomorphic-fetch:
npm install --save isomorphic-fetch es6-promise
bower install --save isomorphic-fetch es6-promise
使用的时候也很是方便:
require('es6-promise').polyfill(); require('isomorphic-fetch'); fetch('//offline-news-api.herokuapp.com/stories') .then(function(response) { if (response.status >= 400) { throw new Error("Bad response from server"); } return response.json(); }) .then(function(stories) { console.log(stories); });
从笔者本身的体验中,仍是很是推荐使用isomorphic-fetch,其一大优点在于可以在node里直接进行单元测试与接口可用性测试。老实说笔者以前用Mocha进行带真实网络请求的测试时仍是比较不方便的,每每须要在浏览器或者phatomjs中进行,而且须要额外的HTML代码。而在笔者的model.test.js文件中,只须要直接使用babel-node model.test.js
便可以获取真实的网络请求,这样能够将网络测试部分与UI相剥离。
假设fetch
已经被挂载到了全局的window目录下。
// Simple response handling fetch('/some/url').then(function(response) { }).catch(function(err) { // Error :( }); // Chaining for more "advanced" handling fetch('/some/url').then(function(response) { return //... }).then(function(returnedValue) { // ... }).catch(function(err) { // Error :( });
Request对象表明了一次fetch
请求中的请求体部分,你能够自定义Request
对象:
A Request
instance represents the request piece of a fetch
call. By passingfetch
a Request
you can make advanced and customized requests:
method
- 使用的HTTP动词,GET
, POST
, PUT
, DELETE
, HEAD
url
- 请求地址,URL of the request
headers
- 关联的Header对象
referrer
- referrer
mode
- 请求的模式,主要用于跨域设置,cors
, no-cors
, same-origin
credentials
- 是否发送Cookie omit
, same-origin
redirect
- 收到重定向请求以后的操做,follow
, error
, manual
integrity
- 完整性校验
cache
- 缓存模式(default
, reload
, no-cache
)
var request = new Request('/users.json', { method: 'POST', mode: 'cors', redirect: 'follow', headers: new Headers({ 'Content-Type': 'text/plain' }) }); // Now use it! fetch(request).then(function() { /* handle response */ }); fetch('/users.json', { method: 'POST', mode: 'cors', redirect: 'follow', headers: new Headers({ 'Content-Type': 'text/plain' }) }).then(function() { /* handle response */ });
注意,fetch方法是自动会将URI中的双引号进行编码的,若是在URI中存入了部分JSON,有时候会出现意想不到的问题,譬如咱们以GET方法访问以下的URI:
[GET] http://api.com?requestData={"p":"q"}
那么fetch会自动将双引号编码,变成:
[GET] http://api.com?requestData={%22p%22:%22q%22}
那么这样一个请求传入到Spring MVC中时是会引起错误的,即URI对象构造失败这个很恶心的错误。笔者没有看过源代码,不过猜测会不会是Spring MVC看到{
这个字符没有被编码,所以默认没有进行解码,结果没想到后面的双引号被编码了,为了不这个无厘头的错误,笔者建议是对URI的Query Parameter部分进行统一的URI编码:
//将requestData序列化为JSON var requestDataString = encodeURIComponent(JSON.stringify(requestData).replace(/%22/g, "\"")); //将字符串连接 const packagedRequestURL = `${Model.BASE_URL}${path}?requestData=${requestDataString}&action=${action}`;
常见的请求方法有: append
, has
, get
, set
以及 delete
// Create an empty Headers instance var headers = new Headers(); // Add a few headers headers.append('Content-Type', 'text/plain'); headers.append('X-My-Custom-Header', 'CustomValue'); // Check, get, and set header values headers.has('Content-Type'); // true headers.get('Content-Type'); // "text/plain" headers.set('Content-Type', 'application/json'); // Delete a header headers.delete('X-My-Custom-Header'); // Add initial values var headers = new Headers({ 'Content-Type': 'text/plain', 'X-My-Custom-Header': 'CustomValue' });
var request = new Request('/some-url', { headers: new Headers({ 'Content-Type': 'text/plain' }) }); fetch(request).then(function() { /* handle response */ });
fetch('/users', { method: 'post', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Hubot', login: 'hubot', }) })
var input = document.querySelector('input[type="file"]') var data = new FormData() data.append('file', input.files[0]) data.append('user', 'hubot') fetch('/avatars', { method: 'post', body: data })
若是须要设置fetch
自动地发送本地的Cookie,须要将credentials设置为same-origin
:
fetch('/users', { credentials: 'same-origin' })
该选项会以相似于XMLHttpRequest的方式来处理Cookie,不然,可能由于没有发送Cookie而致使基于Session的认证出错。能够将credentials
的值设置为include
来在CORS状况下发送请求。
fetch('https://example.com:1234/users', { credentials: 'include' })
在fetch
的then
函数中提供了一个Response
对象,即表明着对于服务端返回值的封装,你也能够在Mock的时候自定义Response对象,譬如在你须要使用Service Workers的状况下,在Response
中,你能够做以下配置:
type
- basic
, cors
url
useFinalURL
- 是否为最终地址
status
- 状态码 (ex: 200
, 404
, etc.)
ok
- 是否成功响应 (status in the range 200-299)
statusText
- status code (ex: OK
)
headers
- 响应头
// Create your own response for service worker testing // new Response(BODY, OPTIONS) var response = new Response('.....', { ok: false, status: 404, url: '/' }); // The fetch's `then` gets a Response instance back fetch('/') .then(function(responseObj) { console.log('status: ', responseObj.status); });
The Response
also provides the following methods:
clone() - Creates a clone of a Response object. error() - Returns a new Response object associated with a network error. redirect() - Creates a new response with a different URL. arrayBuffer() - Returns a promise that resolves with an ArrayBuffer. blob() - Returns a promise that resolves with a Blob. formData() - Returns a promise that resolves with a FormData object. json() - Returns a promise that resolves with a JSON object. text() - Returns a promise that resolves with a USVString (text).
function checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response } else { var error = new Error(response.statusText) error.response = response throw error } } function parseJSON(response) { return response.json() } fetch('/users') .then(checkStatus) .then(parseJSON) .then(function(data) { console.log('request succeeded with JSON response', data) }).catch(function(error) { console.log('request failed', error) })
fetch('https://davidwalsh.name/demo/arsenal.json').then(function(response) { // Convert to JSON return response.json(); }).then(function(j) { // Yay, `j` is a JavaScript object console.log(j); });
fetch('/next/page') .then(function(response) { return response.text(); }).then(function(text) { // <!DOCTYPE .... console.log(text); });
若是你但愿经过fetch方法来载入一些相似于图片等资源:
fetch('flowers.jpg') .then(function(response) { return response.blob(); }) .then(function(imageBlob) { document.querySelector('img').src = URL.createObjectURL(imageBlob); });
blob()
方法会接入一个响应流而且一直读入到结束。
笔者在本身的项目中封装了一个基于ES6 Class的基本的模型请求类,代码地址。
/** * Created by apple on 16/5/3. */ //自动进行全局的ES6 Promise的Polyfill require('es6-promise').polyfill(); require('isomorphic-fetch'); /** * @function 基础的模型类,包含了基本的URL定义 */ export default class Model { //默认的基本URL路径 static BASE_URL = "/"; //默认的请求头 static headers = {}; /** * @function 默认构造函数 */ constructor() { this._checkStatus = this._checkStatus.bind(this); this._parseJSON = this._parseJSON.bind(this); this._parseText = this._parseText.bind(this); this._fetchWithCORS = this._fetchWithCORS.bind(this); } /** * @function 检测返回值的状态 * @param response * @returns {*} */ _checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response } else { var error = new Error(response.statusText); error.response = response; throw error } } /** * @function 解析返回值中的Response为JSON形式 * @param response * @returns {*} */ _parseJSON(response) { if (!!response) { return response.json(); } else { return undefined; } } /** * @function 解析TEXT性质的返回 * @param response * @returns {*} */ _parseText(response) { if (!!response) { return response.text(); } else { return undefined; } } /** * @function 封装好的跨域请求的方法 * @param packagedRequestURL * @returns {*|Promise.<TResult>} * @private */ _fetchWithCORS(packagedRequestURL, contentType) { return fetch(packagedRequestURL, { mode: "cors", headers: Model.headers }) .then(this.checkStatus, (error) => { return error; }) .then(contentType === "json" ? this._parseJSON : this._parseText, (error) => { return error; }); } /** * @function 利用get方法发起请求 * @param path 请求的路径(包括路径参数) * @param requestData 请求的参数 * @param action 请求的类型 * @param contentType 返回的类型 * @returns {Promise.<TResult>|*} Promise.then((data)=>{},(error)=>{}); */ get({ BASE_URL = Model.BASE_URL, path = "/", action = "GET", contentType = "json" }) { //封装最终待请求的字符串 const packagedRequestURL = `${BASE_URL}${(path)}?action=${action}`; //以CORS方式发起请求 return this._fetchWithCORS(packagedRequestURL, contentType); } /** * @function 利用get方法与封装好的QueryParams形式发起请求 * @param path 请求的路径(包括路径参数) * @param requestData 请求的参数 * @param action 请求的类型 * @returns {Promise.<TResult>|*} Promise.then((data)=>{},(error)=>{}); */ getWithQueryParams({ BASE_URL = Model.BASE_URL, path = "/", queryParams = {}, action = "GET", contentType = "json" }) { //初始化查询字符串 let queryString = ""; //根据queryParams构造查询字符串 for (let key in queryParams) { //注意,请求参数必须进行URI格式编码,若是是JSON等特殊格式须要在服务端进行解码 queryString += `${key}=${encodeURIComponent(queryParams[key])}&`; } //将查询字符串进行编码 let encodedQueryString = (queryString); //封装最终待请求的字符串 const packagedRequestURL = `${BASE_URL}${path}?${encodedQueryString}action=${action}`; //以CORS方式发起请求 return this._fetchWithCORS(packagedRequestURL, contentType); } /** * @function 利用get方法与封装好的RequestData形式发起请求 * @param path 请求的路径(包括路径参数) * @param requestData 请求的参数 * @param action 请求的类型 * @returns {Promise.<TResult>|*} Promise.then((data)=>{},(error)=>{}); */ getWithRequestData({ path = "/", requestData = {}, action = "GET", contentType = "json" }) { //将requestData序列化为JSON //注意要对序列化后的数据进行URI编码 var requestDataString = encodeURIComponent(JSON.stringify(requestData)); //将字符串连接 const packagedRequestURL = `${Model.BASE_URL}${path}?requestData=${requestDataString}&action=${action}`; return this._fetchWithCORS(packagedRequestURL, contentType); } /** * @function 考虑到将来post会有不一样的请求方式,所以作区分处理 * @param path * @param requestData * @param action * @returns {Promise.<TResult>|*} */ postWithRequestData({ path = "/", requestData = {}, action = "POST", contentType = "json" }) { //将requestData序列化为JSON //注意要对序列化后的数据进行URI编码 var requestDataString = encodeURIComponent(JSON.stringify(requestData)); //将字符串连接 const packagedRequestURL = `${Model.BASE_URL}${path}?requestData=${requestDataString}&action=${action}`; return this._fetchWithCORS(packagedRequestURL, contentType); } put({ path = "/", requestData = {}, action = "put", contentType = "json" }) {} delete({ path = "/", requestData = {}, action = "DELETE", contentType = "json" }) {} } Model.testData = {}; Model.testData.error = {};