【前端44_前后端交互_跨域】前端解决:JSONP、后端解决:CORS 、后端代理


跨域

什么是跨域

跨域是浏览器为了安全而报的错误,如果不同源去请求资源,那么就会报跨域的错误。

同源概念:协议,域名,端口号一致

报错类似如下

Access to XMLHttpRequest at 'http://localhost:4000/getAjax' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

跨域是后台接口已经通了,但是浏览器拦截掉了,浏览器处于安全角度考虑,拦截掉了数据。

前端解决: JSONP

只支持 get 方式,不支持 post 方式

实现原理

html 中有些标签是没有同源限制的,比方说 scriptiframeimg 等,其中用到的就是 script

<script src="..."></script>
<img>
<link>
<iframe>

那么,我通过创建 script 标签,去访问跨域的资源,然后拿到资源就可以了~

步骤

前端:创建标签,拼接传递参数

前端:创建一个 script 标签,写上链接,可以看到参数跟 get 请求差不多,都是 queryString

let button = document.getElementById("btn");
button.addEventListener("click", () => {
    let myScript = document.createElement("script");	// 创建游离的 script 标签
    myScript.src = "http://localhost:4000/getAjax?name=1";	// 添加 src 属性
    document.querySelector("head").appendChild(myScript);	// 插入游离的标签到 head 标签中
});
后端:接收值,返回值

我们知道,前端引入了标签 script ,就会向后端发送个请求,去寻找资源。

后端通过 ctx.query 来获取前端传来的值

router.get('/getAjax', ctx => {
    console.log('请求到了');
    console.log(ctx.query);
    ctx.body = 'var a = 1'
})

然而这里会有个拐弯儿:后端返回的是 前端要执行的代码!!!

这里还会有一个问题,前端如何接收后端传来的东西呢,我如果直接打印 a,会 undefined

let button = document.getElementById("btn");
button.addEventListener("click", () => {
    let myScript = document.createElement("script");
    myScript.src = "http://localhost:4000/getAjax?name=1";
    document.querySelector("head").appendChild(myScript);
    console.log(a);		// undefined
});

这是因为异步了,可以等待标签加载完后再接收,如下所示:

let button = document.getElementById("btn");
button.addEventListener("click", () => {
    let myScript = document.createElement("script");
    myScript.src = "http://localhost:4000/getAjax?name=1";
    document.querySelector("head").appendChild(myScript);
    myScript.onload = function() {
    	// 当标签加载完成后打印变量
        console.log(a); // 1
    }
});

但是这样处理不是很好

有一个方法:我可以跟后端商量好一个函数名称,后台返回函数执行命令,这样后台处理完之后发送一个执行函数的命令,我前端执行函数即可。

这里会有一点绕,慢点看

前端代码:

let button = document.getElementById("btn");
button.addEventListener("click", () => {
    let myScript = document.createElement("script");
    myScript.src = "http://localhost:4000/getAjax?name=1&cb=cbfunc";	// 这里我会传递一个函数名,等待后端来发送执行命令
    document.querySelector("head").appendChild(myScript);
});
const cbfunc = (res) => {
    console.log(res);		// 这个函数名已经定义好了,就等着后端来调用了,在这里能够获得到后端传来的值
};

后端:

router.get('/getAjax', ctx => {
    console.log('请求到了');
    console.log(ctx.query);
    let cb = ctx.query.cb;		// 后端获取到前端传来的函数名
    let data = {	// 后端要返给前端的参数
        a: 1,
        b: 2,
        c: 3
    }
    ctx.body = `${cb}(${JSON.stringify(data)})`		// 注意序列化
    // 上面已经说了,这里返回前端能够执行的代码,所以这里就是一个前端函数执行的语句,类似这样:funcName()
    // 也就是后端命令前端去执行它了
})

这样就较好的解决异步问题了,也是 jsonp 的雏形

封装 Ajax 代码

这个是上一章的内容了,这里就是封装了一下,

function ajax(options) {
    let opts = Object.assign(
        {
            method: "get",
            url: "",
            headers: {
                "content-type": "application/x-www-form-urlencoded",
            },
            jsonp: "cb",
            data: "",
            success: function () {},
        },
        options
    );

    let xhr = new XMLHttpRequest();
    if (options.method == "get") {
        let data = o2u(opts.data);
        options.url = options.url + "?" + data;
    }
    xhr.open(options.method, options.url, true);
    for (let key in opts.headers) {
        xhr.setRequestHeader(key, opts.headers[key]);
    }
    let sendData;
    switch (opts.headers["content-type"]) {
        case "application/x-www-form-urlencoded":
            sendData = o2u(opts.data);
            break;
        case "application/json":
            sendData = JSON.stringify(opts.data);
            break;
    }
    xhr.onload = function () {
        let resData;
        if (xhr.getResponseHeader("content-type").includes("xml")) {
            resData = xhr.responseXML;
        } else {
            resData = JSON.parse(xhr.responseText);
        }
        options.success(resData);
    };
    if (options.method == "get") {
        xhr.send();
    } else {
        xhr.send(sendData);
    }
}

function o2u(obj) {
    let keys = Object.keys(obj);
    let values = Object.values(obj);
    return keys
        .map((v, k) => {
            return `${v}=${values[k]}`;
        })
        .join("&");
}

在封装的 Ajax 中添加 JSONP

我们在使用自己封装的ajax 的时候,调用的形式大概是这个样子的

ajax({
    url: "http://localhost:4000/getAjax",
    data: {
        name: "你好",
        age: 10,
    },
    dataType: "jsonp",		// 这里说明是跨域访问
    success(res) {			// 这是成功回调函数,稍后要处理它,是个小难点
        console.log(res);
    },
});
需求

我们希望在原有的 ajax 上添加跨域的功能

思路
  • 首先判断一下是不是跨域请求
  • 如果是的话,就去创建个 script 标签,
  • 设置它的 src 属性:传递的参数,回调函数的名称等
  • 把这个 script 标签 appendChildhead 中去!

做着做着你就会遇到这个问题:如何处理这个成功回调参数?

因为你观察咱们的调用方式,是通过 success() 处理返回值的,它你需要重新设置个名字的,不然直接拼接的话就成这个样子了

http://localhost:4000/getAjax?name=你好&age=10&cb=success(){}

这样肯定不行的啊。

解决的思路就是随机一个函数名,然后在 window 对象上定义一下函数,然后把 success() 语句 赋值给这个函数

function jsonpFunc(url, data, cbName, cbFunc) {
    let randomFunc =
        "myRandomFunciotn" + Math.random().toString().substr(2);		// 这样做防止函数重名
    window[randomFunc] = cbFunc; // window 注册此函数
    let path = `${url}?${o2u(data)}&${cbName}=${randomFunc}`;
    // console.log(path);
    let myScript = document.createElement("script");
    myScript.src = path;
    document.querySelector("head").appendChild(myScript);
}

解释的代码如下:
在这里插入图片描述
我们来进行证明:

  • 首先我来创建个跨域的 ajax 请求,成功回调函数体里写点东西哈
document.querySelector('button').addEventListener('click', function () {
    ajax({
        url: 'http://localhost:4000/getAjax',
        data: {
            name: 'hello',
            age: 10,
        },
        dataType: 'jsonp',
        success(res) {
            console.log('函数体:我是 success 成功回调函数的内容')
        },
    })
})
  • 接着点击按钮,发送请求
    在这里插入图片描述

  • 如何去验证呢? 去 window 对象下找函数去!!!我们打印 window 对象请添加图片描述

  • ok!验证成功

完整代码如下:这里也有 gitee 代码仓库 https://gitee.com/lovely_ruby/DailyPractice/tree/main/front/07/JSONP,可以克隆到本地

<script>
    document.querySelector("button").addEventListener("click", function () {
        ajax({
            url: "http://localhost:4000/getAjax",
            data: {
                name: "你好",
                age: 10,
            },
            dataType: "jsonp",
            success(res) {
                console.log(res);
            },
        });
    });

    function ajax(options) {
        let opts = Object.assign(
            {
                method: "get",
                url: "",
                headers: {
                    "content-type": "application/x-www-form-urlencoded",
                },
                jsonp: "cb",
                data: "",
                success: function () {},
            },
            options
        );

        if (opts.dataType === "jsonp") {
            // 做跨域的处理
            jsonpFunc(opts.url, opts.data, opts.jsonp, opts.success);
            return; // 这里记得return截断
        }

        function jsonpFunc(url, data, cbName, cbFunc) {
            let randomFunc =
                "myRandomFunciotn" + Math.random().toString().substr(2); // substr 截取
            window[randomFunc] = cbFunc; // window 注册此函数
            let path = `${url}?${o2u(data)}&${cbName}=${randomFunc}`;
            // console.log(path);
            let myScript = document.createElement("script");
            myScript.src = path;
            document.querySelector("head").appendChild(myScript);
        }

        let xhr = new XMLHttpRequest();
        if (options.method == "get") {
            let data = o2u(opts.data);
            options.url = options.url + "?" + data;
        }
        xhr.open(options.method, options.url, true);
        for (let key in opts.headers) {
            xhr.setRequestHeader(key, opts.headers[key]);
        }
        let sendData;
        switch (opts.headers["content-type"]) {
            case "application/x-www-form-urlencoded":
                sendData = o2u(opts.data);
                break;
            case "application/json":
                sendData = JSON.stringify(opts.data);
                break;
        }
        xhr.onload = function () {
            let resData;
            if (xhr.getResponseHeader("content-type").includes("xml")) {
                resData = xhr.responseXML;
            } else {
                resData = JSON.parse(xhr.responseText);
            }
            options.success(resData);
        };
        if (options.method == "get") {
            xhr.send();
        } else {
            xhr.send(sendData);
        }
    }

    // 把对象传换成 queryString,就是 get 请求后面的 ? &
    function o2u(obj) {
        let keys = Object.keys(obj);
        let values = Object.values(obj);
        return keys
            .map((v, k) => {
                return `${v}=${values[k]}`;
            })
            .join("&");
    }
</script>

练习:JSONP 请求 百度模糊搜索 接口

写在另一篇文章里了,点击这里跳转

瀑布流加载数据的的实现思路

检测滚动条是否到达底部,如果到达底部了就去加载文件

document.onscroll = function () {
    let windowHeight = document.documentElement.clientHeight; // 用户窗口的大小
    let contentHeight = document.documentElement.offsetHeight; // 页面总共的大小

    let scrollHeight = contentHeight - windowHeight; // 计算出滚动条的高度
    let scrollTop = document.documentElement.scrollTop; // 获取当前滚动条的高度

    console.log('windowHeight:>>', windowHeight);
    console.log('contentHeight:>>', contentHeight);
    console.log('scrollTop:>>', scrollTop);

    if (scrollTop > scrollHeight - 10) {
        // 加载下一页数据的逻辑
    }
};

后端解决:CORS 跨域资源共享

既然浏览器是为了安全考虑的,那么后端就可以在返回头中告诉浏览器安全即可。

解决的一个思路是:后端在返还头中给个标识,告诉浏览器不要去拦截

设置响应头

ctx.set("Access-Control-Allow-Origin","*");  
// 允许所有源,任何人请求这个接口都可以,会在返回头中写这么一段,告诉浏览器安全了,但是不建议全都通过,不建议这样写

ctx.set("Access-Control-Allow-Origin","http://localhost:3000");  // 只是允许本地 3000 访问,建议这样写,指定域名

写法类似如下图所示:
在这里插入图片描述
插叙:但是你这样设置,前端浏览器可能也报错,就像我下面列举的奇怪的错误中图片描述的,CORS 并不好使,这可能是因为预检请求的原因,解决的办法就是下面描述的,加上 options 的请求

如果设置了规则为通配符*的话,你会发现 cookie 没了
一是因为用了通配符,用了通配符的话就不让携带 cookie
二是要设置允许携带凭证,withCredentials ,(cookie 其实也算是一种凭证)。

在这里插入图片描述

设置请求头的样子如下图所示:
在这里插入图片描述

koa2-cors 依赖

安装这个依赖并配置

const cors = require("koa2-cors");
app.use(
  cors({
    origin: "*",
    allowMethods: ["GET"],
  })
);

一个例子的截图如下
在这里插入图片描述


预检请求

介绍

复杂请求的时候,请求真正接口前先去探探路,看看服务器让不让访问,类似敢死队?

浏览器的同源策略,就是出于安全考虑,浏览器会限制从脚本发起的跨域 HTTP 请求(比如异步请求GET, POST, PUT, DELETE, OPTIONS等等),所以浏览器会向所请求的服务器发起两次请求

  • 第一次是浏览器使用OPTIONS方法发起一个预检请求
  • 第二次才是真正的异步请求

第一次的预检请求获知服务器是否允许该跨域请求:如果允许,才发起第二次真实的请求;如果不允许,则拦截第二次请求。
Access-Control-Max-Age用来指定本次预检请求的有效期,单位为秒,,在此期间不用发出另一条预检请求

预检请求发生的条件

如果你是解决跨域,并且是通过 CORS 解决的,就会有这种预检请求

预检请求发送条件:简单的请求没有预检请求,比方说

  • 请求方式是 getpostheadcontent-type 只有如下几条时
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

会被认为是简单请求

除了简单请求,其他的情况都算是复杂请求,需要发送预检请求

处理预检请求

随便给他们返还点东西即可,但是也要设置跨域,还有请求头

router.options('/*', (ctx) => {
    console.log('走了options')
    ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000') // 预检请求也需要允许跨域
    ctx.set(
        'Access-Control-Allow-Headers',
        'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , mytest'
    )
    ctx.body = {}
})
预检请求的有效期

我们会发现每次请求都需要走两次接口,性能不太好,所以成功一次的话可以把这个缓存记录下来,下次就只请求一次了

// 在预检请求中设置返回头的信息
ctx.set('Access-Control-Max-Age', 10)   // 单位是秒

在这里插入图片描述

测试如下:
请添加图片描述

Chrome 没有 Options 请求的问题(已解决,因为自己傻掉了,忘记关掉过滤)

以下是 Chrome 老版本的截图
在这里插入图片描述
最新的 Chrome 浏览器没有这个报错(猜测),而是如下这样子(目前:2021年7月)
在这里插入图片描述
所以换家浏览器, 用火狐来测试。

在这里插入图片描述
不对,是我傻掉了,我把这个过滤掉了,放开过滤就好了!!!

在这里插入图片描述


后端解决:后端代理

思路

既然跨域是浏览器的安全策略,那么就让后端去访问跨域的链接,后端和后端之间的访问就没有跨域这一说了,绕过了浏览器!

后端服务器去请求跨域的资源后,再返还给前端

主要的是知道数据流是怎么走的,知道思路!!!

在这里插入图片描述


奇怪的错误

问题:后台设置 CORS 不好使 ?? 浏览器还报奇怪的错误

中文路径的问题,把路径里的中文改成英文的就好了,淦!!!
在这里插入图片描述
在这里插入图片描述
我试了试,又报哪个错误了,然后把这句话加上之后,就好了,但是你如果注释掉,他虽然是好使的,但是等一会儿就不好使了,报跨域的错误了

router.options("/*",ctx=>{
    ctx.set("Access-Control-Allow-Origin","http://localhost:3000");
    ctx.set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Authorization, Accept, X-Requested-With , mytest");
    ctx.set("Access-Control-Max-Age",600);
    ctx.body = "";
})

时效性是因为我设置了 Access-Control-Max-Age",600 这个东西

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值