诳言javascript 5期:跨域

一、什么是跨域

1.URL剖析

URL (Uniform Resource Locator )

一致资本定位符(URL)是用于完整地形貌Internet上网页和其他资本的地点的一种标识要领。

Internet上的每个网页都具有一个唯一的称号标识,一般称之为URL地点,这类地点可所以当地磁盘,也可所以局域网上的某一台计算机,更多的是Internet上的站点。简朴地说,URL就是Web地点,俗称“网址”。

URL一般由三部份构成:协定范例,主机名和途径及文件名。

《诳言javascript 5期:跨域》

2.同源战略

所谓
同源是指
协定
域名
端口均雷同。如果两个页面的协定,端口(如果有指定)和主机都雷同,则两个页面具有雷同的源。

同源战略是浏览器的一个平安功用,差别源的客户端剧本在没有明白受权的状况下,不能读写对方资本。所以a.com下的js剧本采纳ajax读取b.com内里的文件数据是会报错的。

同源战略/SOP(Same origin policy)是一种商定,由Netscape公司1995年引入浏览器,它是浏览器最中心也最基础的平安功用,如果缺少了同源战略,浏览器很轻易遭到XSS、CSFR等进击。

同源战略限定以下几种行动:

  1. Cookie、LocalStorage 和 IndexDB 没法读取
  2. DOM 和 Js对象没法取得
  3. AJAX 要求不能发送

3.跨域的定义

跨域是指从一个域名的网页去要求另一个域名的资本。比方从www.baidu.com 页面去要求 www.google.com

的资本。(然则浏览器的同源战略会限定你不能这么做,这是是浏览器对JavaScript施加的平安限定)

跨域的严厉一点的定义是:只需
协定
域名
端口有任何一个的
差别,就被看成是
跨域

二、跨域场景

           URL                           申明                    是不是允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js         统一域名,差别文件或途径           允许
http://www.domain.com/lab/c.js

http://www.domain.com:8000/a.js
http://www.domain.com/b.js         统一域名,差别端口                不允许
 
http://www.domain.com/a.js
https://www.domain.com/b.js        统一域名,差别协定                不允许
 
http://www.domain.com/a.js
http://192.168.4.12/b.js           域名和域名对应雷同ip              不允许
 
http://www.domain.com/a.js
http://x.domain.com/b.js           主域雷同,子域差别                不允许
http://domain.com/c.js
 
http://www.domain1.com/a.js
http://www.domain2.com/b.js        差别域名                         不允许

三、浏览器为何要限定跨域接见

缘由就是平安题目:如果一个网页能够随便地接见别的一个网站的资本,那末就有能够在客户完整不知情的状况下涌现平安题目。比方下面的操纵就有平安题目:

  1. 用户接见www.mybank.com ,上岸并举行网银操纵,这时刻cookie啥的都天生并存放在浏览器
  2. 用户倏忽想起件事,并模模糊糊地接见了一个罪恶的网站 www.xiee.com
  3. 这时刻该网站就能够在它的页面中,拿到银行的cookie,比方用户名,上岸token等,然后提议对www.mybank.com 的操纵。
  4. 如果这时刻浏览器不予限定,而且银行也没有做相应的平安处置惩罚的话,那末用户的信息有能够就这么泄露了。

四、为何要跨域?

既然有平安题目,那为何又要跨域呢? 偶然公司内部有多个差别的子域,比方一个是location.company.com ,而运用是放在app.company.com , 这时刻想从 app.company.com去接见 location.company.com 的资本就属于跨域。

五、处理跨域题目的要领

1、 经由历程jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe跨域
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资本共享(CORS)
7、 nginx代办跨域
8、 nodejs中心件代办跨域
9、 WebSocket协定跨域

1.经由历程jsonp跨域

【1】定义:

JSONP是一种跨域资本要求处理计划,
应用了<script>标签的src属性没有同源限定,举行跨域要求。

【2】道理:

经由历程动态建立<script>标签,然后经由历程标签的src属性猎取js文件的剧本,该剧本的内容是一个函数挪用,参数就是效劳器返回的数据,为了处置惩罚这些返回的数据,需要实如今页面定义好回调函数,本质上运用的并非ajax手艺

【3】历程:

  1. 网页端插进去一个script标签,src指向目的api 的 url(只能是 get api,因为 script 加载 js 文件是 http get 要领)。这里做一个小修正,url背面加上 query,?callback=handle
  2. 后端 api 处置惩罚函数吸收到要求,发明有 callback 参数,则将参数值拿下来,获得 handle
  3. 后端用 handle 包装数据,返回给浏览器,注重,返回的 content-type 必需是 text/javascript; charset=utf-8
  4. 网页端 script 内容加载完成
    handle(data)
  5. 浏览器发明内容是 js(检察 content-type),则挪用js诠释器实行 handle(data)

【4】中心点

  1. 读取 callback 名,比方 handle
  2. 将数据封装在 handle 中
  3. 将封装好的内容作为 js 剧本内容返回,注重 content-type 必需为 text/javascript; charset=utf-8 以便浏览器一般剖析实行 js

【5】设置:

要完成运用JSONP跨域需要三步:

第一步,动态建立一个script元素;
第二步,设置script元素的src为想要跨域要求资本的url,这个url的参数callback为要求到资本后的处置惩罚函数;
第三步,定义处置惩罚函数,处置惩罚返回的对象;
第四步,把script元素添加到页面中

var scriptEl = document.createElement('script');
scriptEl.src = 'http://www.freegeoip.net/json/?callback=handleResponse';
document.body.appendChild(scriptEl);
function handleResponse(response) {
  /*response的范例是Object*/
  alert(response.country_name);
}

【6】现实封装运用

// jsonp.js
export function getJSONP(url, cb) {
    if (url.indexOf('?') === -1) {
        url += '?callback=responseHandler';
    } else {
        url += '&callback=responseHandler';
    }
    // 建立script 标签
    var script = document.createElement('script');
    // 在函数内部完成包裹函数,因为要用到 cb
    // responseHandler 为全局变量
    window.responseHandler = function (json) {
        try {
            cb(json)
        } finally {
            // 函数挪用今后,移除对应的标签
            script.parentNode.removeChild(script);
        }
    }
    script.setAttribute('src', url)
    document.body.appendChild(script);
}

挪用:

import { getJSONP } from "../../utils/jsonp";

const onSearch = async (query) => {
    const url = `https://api.douban.com/v2/book/search?q=${query}`;
    getJSONP(url, e => {
        // 回调函数
        // e 为经由历程jsonp猎取的数据
        console.log(e)
    })
}

1.)原生完成:

 <script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参并指定回调实行函数为onBack
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
    document.head.appendChild(script);

    // 回调实行函数
    function onBack(res) {
        alert(JSON.stringify(res));
    }
 </script>

效劳端返回以下(返回时即实行全局函数):

onBack({"status": true, "user": "admin"})

2.)jquery ajax:

$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 要求体式格局为jsonp
    jsonpCallback: "onBack",    // 自定义回调函数名
    data: {}
});

3.)vue.js:

this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'onBack'
}).then((res) => {
    console.log(res); 
})

后端node.js代码示例:

var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
    var params = qs.parse(req.url.split('?')[1]);
    var fn = params.callback;

    // jsonp返回设置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');

    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

【7】优瑕玷

长处:

  • 它不像XMLHttpRequest对象完成的Ajax要求那样遭到同源战略的限定,JSONP能够逾越同源战略;
  • 它的兼容性更好,在越发陈旧的浏览器中都能够运转,不需要XMLHttpRequest或ActiveX的支撑
  • 在要求终了后能够经由历程挪用callback的体式格局回传效果。将回调要领的权限给了挪用方。这个就相当于将controller层和view层终究分 开了。我供应的jsonp效劳只供应纯效劳的数据,至于供应效劳以 后的页面衬着和后续view操纵都由挪用者来本身定义就好了。如果有两个页面需要衬着统一份数据,你们只需要有差别的衬着逻辑就能够了,逻辑都能够运用同 一个jsonp效劳。

瑕玷:

  • 它只支撑GET要求而不支撑POST等别的范例的HTTP要求
  • 它只支撑跨域HTTP要求这类状况,不能处理差别域的两个页面之间怎样举行JavaScript挪用的题目。
  • jsonp在挪用失利的时刻不会返回种种HTTP状况码。
  • 瑕玷是平安性。万一如果供应jsonp的效劳存在页面注入破绽,即它返回的javascript的内容被人掌握的。那末效果是什么?一切挪用这个 jsonp的网站都邑存在破绽。因而没法把风险掌握在一个域名下…所以在运用jsonp的时刻必需要保证运用的jsonp效劳必需是平安可托的

2.document.domain + iframe跨域

此计划仅限主域雷同,子域差别的跨域运用场景。

完成道理:两个页面都经由历程js强迫设置document.domain为基础主域,就完成了同域。

1.)父窗口:(http://www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

2.)子窗口:(http://child.domain.com/b.html)

<script>
    document.domain = 'domain.com';
    // 猎取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>



3.location.hash + iframe跨域

完成道理: a欲与b跨域互相通信,经由历程中心页c来完成。三个页面,差别域之间应用iframe的location.hash传值,雷同域之间直接js接见来通信。

详细完成:A域:a.html -> B域:b.html -> A域:c.html,a与b差别域只能经由历程hash值单向通信,b与c也差别域也只能单向通信,但c与a同域,所以c可经由历程parent.parent接见a页面一切对象。

1.)a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调要领
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

2.)b.html:(http://www.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

3.)c.html:(http://www.domain1.com/c.html)

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再经由历程操纵同域a.html的js回调,将效果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>


4.window.name + iframe跨域

window.name属性的奇特的地方:name值在差别的页面(以至差别域名)加载后照旧存在,而且能够支撑异常长的 name 值(2MB)。

1.)a.html:(http://www.domain1.com/a.html)

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事宜会触发2次,第1次加载跨域页,并保存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)胜利后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)胜利后,切换到同域代办页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 猎取数据今后烧毁这个iframe,开释内存;这也保证了平安(不被其他域frame js接见)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 要求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

2.)proxy.html:(http://www.domain1.com/proxy….

中心代办页,与a.html同域,内容为空即可。

3.)b.html:(http://www.domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>

总结:经由历程iframe的src属性由外域转向当地区,跨域数据即由iframe的window.name从外域通报到当地区。这个就奇妙地绕过了浏览器的跨域接见限定,但同时它又是平安操纵。

5.postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多能够跨域操纵的window属性之一,它可用于处理以下方面的题目:

a.) 页面和其翻开的新窗口的数据通报
b.) 多窗口之间音讯通报
c.) 页面与嵌套的iframe音讯通报
d.) 上面三个场景的跨域数据通报

用法:postMessage(data,origin)要领吸收两个参数
data: html5范例支撑恣意基础范例或可复制的对象,但部份浏览器只支撑字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协定+主机+端口号,也能够设置为”*”,示意能够通报给恣意窗口,如果要指定和当前窗口同源的话设置为”/”。

1.)a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 吸收domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2.)b.html:(http://www.domain2.com/b.html)

<script>
    // 吸收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处置惩罚后再发还domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

6.跨域资本共享(CORS)

一般跨域要求:效劳端设置Access-Control-Allow-Origin即可,前端不必设置,若要带cookie要求:前后端都需要设置

需注重的是:因为同源战略的限定,所读取的cookie为跨域要求接口地点域的cookie,而非当前页。如果想完成当前页cookie的写入,可参考下文:nginx反向代办中设置proxy_cookie_domain 和 NodeJs中心件代办中cookieDomainRewrite参数的设置。
现在,一切浏览器都支撑该功用(IE8+:IE8/9需要运用XDomainRequest对象来支撑CORS)),CORS也已经成为主流的跨域处理计划

1、 前端设置:

1.)原生ajax

// 前端设置是不是带cookie
xhr.withCredentials = true;
示例代码:
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是不是带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};

2.)jQuery ajax

$.ajax({
    ...
   xhrFields: {
       withCredentials: true    // 前端设置是不是带cookie
   },
   crossDomain: true,   // 会让要求头中包括跨域的分外信息,但不会含cookie
    ...
});

3.)vue框架

a.) axios设置:
axios.defaults.withCredentials = true
b.) vue-resource设置:
Vue.http.options.credentials = true

2、 效劳端设置:

若后端设置胜利,前端浏览器掌握台则不会涌现跨域报错信息,反之,申明没设胜利。
1.)Java背景:

/*
 * 导入包:import javax.servlet.http.HttpServletResponse;
 * 接口参数中定义:HttpServletResponse response
 */

// 允许跨域接见的域名:如有端口需写全(协定+域名+端口),若没有端口末端不必加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com"); 

// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必需指定详细的域名,不然浏览器会提醒
response.setHeader("Access-Control-Allow-Credentials", "true"); 

// 提醒OPTIONS预检时,后端需要设置的两个经常使用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");

2.)Nodejs背景示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块吸收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据吸收终了
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域背景设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许接见的域(协定+域名+端口)
            /* 
             * 此处设置的cookie照样domain2的而非domain1,
             * 因为后端也不能跨域写cookie(nginx反向代办能够完成),
             * 但只需domain2中写入一次cookie认证,背面的跨域接口都能从domain2中猎取cookie,
             * 从而完成一切的接口都能跨域接见
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  
             // HttpOnly的作用是让js没法读取cookie
        });

        res.write(JSON.stringify(postData));
        res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080...');

7.nginx代办跨域

1、 nginx设置处理iconfont跨域

浏览器跨域接见js、css、img等通例静态资本被同源战略允许,但iconfont字体文件(eot|otf|ttf|woff|svg)破例,此时可在

nginx的静态资本效劳器中到场以下设置。

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

2、 nginx反向代办接口跨域

跨域道理: 同源战略是浏览器的平安战略,不是HTTP协定的一部份。效劳器端挪用HTTP接口只是运用HTTP协定,

不会实行JS剧本,不需要同源战略,也就不存在逾越题目。

完成思绪:经由历程nginx设置一个代办效劳器(域名与domain1雷同,端口差别)做跳板机,反向代办接见domain2接口,
而且能够趁便修正cookie中domain信息,轻易当前域cookie写入,完成跨域登录。

nginx详细设置:

proxy效劳器

server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代办
        proxy_cookie_domain www.domain2.com www.domain1.com; #修正cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中心件代办接口接见nignx时,
        # 此时无浏览器介入,故没有同源限定,下面的跨域设置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

1.) 前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是不是读写cookie
xhr.withCredentials = true;

// 接见nginx中的代办效劳器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

2.) Nodejs背景示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));

    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:剧本没法读取
    });

    res.write(JSON.stringify(params));
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

8.nodejs中心件代办跨域

node中心件完成跨域代办,道理大抵与nginx雷同,都是经由历程启一个代办效劳器,完成数据的转发,也能够经由历程设置cookieDomainRewrite参数修正相应头中cookie中域名,完成当前域的cookie写入,轻易接口登录认证。

1、 非vue框架的跨域(2次跨域)

应用node + express + http-proxy-middleware搭建一个proxy效劳器。

1.)前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是不是读写cookie
xhr.withCredentials = true;

// 接见http-proxy-middleware代办效劳器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();

2.)中心件效劳器:

var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代办跨域目的接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修正相应头信息,完成跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修正相应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 能够为false,示意不修正
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');

3.)Nodejs背景同(nginx)

2、 vue框架的跨域(1次跨域)

应用node + webpack + webpack-dev-server代办接口跨域。在开辟环境下,因为vue衬着效劳和接口代办效劳都是webpack-dev-server统一个,所以页面与代办接口之间不再跨域,不必设置headers跨域信息了。

webpack.config.js部份设置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代办跨域目的接口
            changeOrigin: true,
            secure: false,  // 当代办某些https效劳报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 能够为false,示意不修正
        }],
        noInfo: true
    }
}


9.WebSocket协定跨域

WebSocket protocol是HTML5一种新的协定。它完成了浏览器与效劳器全双工通信,同时允许跨域通信,是server

push手艺的一种很好的完成。

原生WebSocket API运用起来不太轻易,我们运用Socket.io,它很好地封装了webSocket接口,供应了更简朴、天真的接口,也对不支撑webSocket的浏览器供应了向下兼容。

1.)前端代码:

<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 衔接胜利处置惩罚
socket.on('connect', function() {
    // 监听效劳端音讯
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听效劳端封闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

2.)Nodejs socket背景:

var http = require('http');
var socket = require('socket.io');

// 启http效劳
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket衔接
socket.listen(server).on('connection', function(client) {
    // 吸收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处置惩罚
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

参考文章:
前端罕见跨域处理计划(全)

如果你以为这篇文章对你有所协助,那就趁便点个赞吧,点点关注不迷路~

黑芝麻哇,白芝麻发,黑芝麻白芝麻哇发哈!

前端哇发哈

    原文作者:前端哇发哈
    原文地址: https://segmentfault.com/a/1190000018748510
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞