20230408----重返学习-JavaScript前端跨域方案

128 阅读4分钟

day-045-forty-five-20230408-JavaScript前端跨域方案

JavaScript前端跨域方案

  • 浏览器有一个安全策略: 客户端与服务器的 协议、域名、端口号一致才能访问服务器的数据。

    • 客户端与服务器的 协议、域名、端口号 的三项有一项不同,客户端就无法访问服务器的数据。

        • 客户端网址:http://127.0.0.1:5500/1.html
        • 服务器网址:http://127.0.0.1:8888/articleList?date=2021-05-21
      • 客户端不能访问服务器的数据

  • 跨域指的是协议与域名与端口号就算有一项或多项不同,也可以让其访问数据。

  • 跨域的前提条件: 后台允许 或 后台不做限制。

  • 跨域的方式

    1. cors 后端设置,要了解的

      1. 设置白名单–修改白名单,一定要重新启动服务

        let safeList = ["http://127.0.0.1:8848","http://127.0.0.1:5500","http://127.0.0.1:5501","http://127.0.0.1:5502", "http://127.0.0.1:3000", "http://127.0.0.1:8080"];
        app.use((req, res, next) => {
            let origin = req.headers.origin || req.headers.referer || "";
            origin = origin.replace(/\/$/g, '');
            origin = !safeList.includes(origin) ? '' : origin;
            res.header("Access-Control-Allow-Origin", origin);//核心操作,后端设置允许跨域的网址
        });
        
      2. 设置值为 “*”—允许所有源访问

        • 如果服务器设置了res.header(“Access-Control-Allow-Origin”,“*”),客户端不允许携带跨域资源凭证

          • 但就算服务器端设置 res.header(“Access-Control-Allow-Credentials”, true)

            • 客户端也 不允许携带跨域资源凭证

              • 代码示例

                • 后端服务器

                  /*-CREATE SERVER-*/
                  const express = require("express");
                  let app = express();
                  app.listen(1001, () => console.log(`服务启动成功,正在监听1001端口!`));
                  /*-MIDDLE WARE-*/
                  // 设置白名单
                  app.use((req, res, next) => {
                    res.header("Access-Control-Allow-Origin", "*");//核心操作,后端设置允许跨域的网址
                    res.header("Access-Control-Allow-Credentials", true);//设置 res.header("Access-Control-Allow-Credentials", true),客户端也 不允许携带跨域资源凭证
                    req.method === "OPTIONS" ? res.send("OK") : next();
                  });
                  /*-API-*/
                  app.get("/list", (_, res) => {
                    res.send({
                      code: 0,
                      message: "zhufeng",
                    });
                  });
                  /* STATIC WEB */
                  app.use(express.static("./"));
                  
                  • 前端代码

                    <!DOCTYPE html>
                    <html lang="en">
                      <head>
                        <meta charset="UTF-8" />
                        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
                        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                        <title>1</title>
                      </head>
                      <body>
                        <script src="./axios.min.js"></script>
                        <script>
                          axios
                            .get("http://127.0.0.1:1001/list", {
                              // withCredentials: true,//后端res.header("Access-Control-Allow-Origin","*")或 res.header("Access-Control-Allow-Credentials", false)时会报错
                              withCredentials: false,
                            })
                            .then((response) => {
                              console.log(0, response);
                              return response.data;
                            })
                            .then((value) => {
                              console.log(1, value);
                            })
                            .catch((err) => {
                              console.log(2, err);
                            });
                        </script>
                      </body>
                    </html>
                    
    2. proxy 跨域代理 客户端(必须会)

      • 使用方式的种类

        • webpack(vue)
        • nodejs
        • nginx
      • 原理: 服务器与服务器之间没有域的限制
        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ASdUqQOn-1680969492598)(./proxy代理服务器跨域原理.png)]

        1. 在客户端与跨域目标服务器之间设置一个由前端控制的代理服务器

        2. 客户端向代理服务器发送一个请求

          • 这里要保证代理服务器网络路径与客户端代码网络路径是同域的

            • webpack(vue)的方式,就是改vue项目根路径/vue.config.js中的module.exports对象中的devServer.proxy属性,一个属性名代表一个代理服务器路径。
            • nodejs的方式,就是把前端html文件移动到当前网络服务监听到的静态目录中。
            • nginx的方式,就是改nginx的配置项。
          • 也就是说,要让客户端与代理服务器无跨域问题

            • 要保证客户端与代理服务器的协议、域名、端口号一致
        3. 代理服务器收到客户端的请求,就把请求路径改一下,转发给跨域目标服务器

          • 由于代理服务器是服务器,是没有跨域限制的,所以请求会成功
        4. 跨域目标服务器收到代理服务器,把结果给到代理服务器

        5. 代理服务器收到跨域目标服务器的结果,把跨域目标服务器结果转发给客户端

      • 实际步骤(以nodejs为域)

        1. 写好nodejs代码,监听一个网络端口,并定义静态目录
        2. 把前端html文件及相当依赖放到静态目录中,让其与nodejs服务同域
        3. 运行nodejs代码,监听具体端口
        4. 以网络方式打开html文件
      • 例子

        • 前端代码

          <!DOCTYPE html>
          <html lang="en">
            <head>
              <meta charset="UTF-8" />
              <meta http-equiv="X-UA-Compatible" content="IE=edge" />
              <meta name="viewport" content="width=device-width, initial-scale=1.0" />
              <title>2proxy.html</title>
            </head>
            <body>
              当前文件要放在服务器根目录中,即 http://127.0.0.1:1001/2proxy.html 这个路径对应的文件夹里
              <script src="./axios.min.js"></script>
          
              <script>
                axios
                  .get("http://127.0.0.1:1001/aaa", {
                    // withCredentials: true,
                    withCredentials: false,
                  })
                  .then((response) => {
                    console.log(0, response);
                    return response.data;
                  })
                  .then((value) => {
                    console.log(1, value);
                  })
                  .catch((err) => {
                    console.log(2, err);
                  });
              </script>
            </body>
          </html>
          
        • 后端代码

          /*-CREATE SERVER-*/
          const express = require('express');
          let request = require('request');
          let app = express();
          app.listen(1001, () => console.log(`服务启动成功,正在监听1001端口!`));
          // 服务器接收到客户端发送过来的请求
          app.get('/aaa', (req, res) => {
              // 向简书发送相同请求,从简书服务器获取想要的数据「不存在域的限制的」
              let jianURL = `https://www.jianshu.com/asimov/subscriptions/recommended_collections`;
              req.pipe(request(jianURL)).pipe(res);
          });
          /* STATIC WEB */
          app.use(express.static('./'));
          
    3. jsonp 客户端(老)

      • jsonp跨域 --> get

        • jsonp跨域只能对get请求生效
        • 而且一般jsonp要配合动态生成script标签来使用。
      • 原理: link(href)、img(src)、script(src)都没有域的限制,获取数据的方式都是get,使用script标签进行跨域
        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zcyeHIbA-1680969492600)(./script标签jsonp跨域原理.png)]

        1. 客户端代码里,定义一个全局函数,函数名随意,如jsonpCallback。
      • 实际步骤(以nodejs为域)

      • jsonp得后端配合,感觉很麻烦

    4. postMessage+iframe(太老)

      • postMessage、window.name、document.domin、location.hash

        • 这些方案结合 iframe 也可以实现跨域处理

工作中跨域的场景

前端工作中一定会跨域,开发环境一定会跨域,但服务器环境看公司

  1. 大公司有钱,多个服务器

    • 文件专门放不同服务器,客户端根据需求访问不同的服务器,就需要跨域

      • 图片专门设置一个服务器(A)
      • 压缩包专门设置一个服务器(B)
      • 其它数据文件服务器©
    • 生产环境和开发环境都会跨域

  2. 小公司没钱,就一个服务器

    • 开发过程中,不会将前端代码放到服务器上

      • 开发环境中一定会跨域,开发结束才用git放到服务器上
    • 最终开发完成后,有可能放到同一个服务器上

      • 有可能生产环境不跨域

cors跨域

  • 前端代码 index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>1</title>
      </head>
      <body>
        <script src="./axios.min.js"></script>
        <script>
          axios
            .get("http://127.0.0.1:1001/list", {
              // withCredentials: true,//后端res.header("Access-Control-Allow-Origin","*")或 res.header("Access-Control-Allow-Credentials", false)时会报错
              withCredentials: false,
            })
            .then((response) => {
              console.log(0, response);
              return response.data;
            })
            .then((value) => {
              console.log(1, value);
            })
            .catch((err) => {
              console.log(2, err);
            });
        </script>
      </body>
    </html>
    
  • 后端代码-后端设置

    1. 设置白名单–修改白名单,一定要重新启动服务

      /*-CREATE SERVER-*/
      const express = require("express"),
        app = express();
      app.listen(1001, () => console.log(`服务启动成功,正在监听1001端口!`));
      
      /*-MIDDLE WARE-*/
      // 设置白名单
      let safeList = ["http://127.0.0.1:8848", "http://127.0.0.1:5500"];
      app.use((req, res, next) => {
        let origin = req.headers.origin || req.headers.referer || "";
        origin = origin.replace(/\/$/g, "");
        origin = !safeList.includes(origin) ? "" : origin;
        res.header("Access-Control-Allow-Origin", origin);
        res.header("Access-Control-Allow-Credentials", true);
        req.method === "OPTIONS" ? res.send("OK") : next();
      });
      
      /*-API-*/
      app.get("/list", (_, res) => {
        res.send({
          code: 0,
          message: "zhufeng",
        });
      });
      
      /* STATIC WEB */
      app.use(express.static("./"));
      
    2. 设置值为 “*”—允许所有源访问

      /*-CREATE SERVER-*/
      const express = require("express"),
        app = express();
      app.listen(1001, () => console.log(`服务启动成功,正在监听1001端口!`));
      
      /*-MIDDLE WARE-*/
      app.use((req, res, next) => {
        res.header("Access-Control-Allow-Origin", '*');
        req.method === "OPTIONS" ? res.send("OK") : next();
      });
      
      /*-API-*/
      app.get("/list", (_, res) => {
        res.send({
          code: 0,
          message: "zhufeng",
        });
      });
      
      /* STATIC WEB */
      app.use(express.static("./"));
      

proxy跨域代理服务器

  • 前端代码 index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        2. proxy 跨域代理  客户端(必须会)   webpack(vue)/nodejs/nginx
    
        客户端:http://127.0.0.1:5500/2023_01/JS%E9%83%A8%E5%88%86/day0408/2.html
        
        将客户端的页面(2.html),放到服务器(CrossDomain)上,
        用服务器的方式打开页面(2.html),不能使用 open with live server
        http://127.0.0.1:1001/2.html
    
        代理服务器:http://127.0.0.1:1001/aaa
        服务器:https://www.jianshu.com/asimov/subscriptions/recommended_collections
    
        <script src="axios.min.js"></script>
        <script>
            axios.get("http://127.0.0.1:1001/aaa")
            .then(res=>{
                return res.data
            }).then(value=>{
                console.log(value);
            }).catch(err=>{
                console.log(err);
            })
        </script>
    </body>
    </html>
    
  • 后端代码-后端设置

    /*-CREATE SERVER-*/
    const express = require('express'),
        request = require('request'),
        app = express();
    app.listen(1001, () => console.log(`服务启动成功,正在监听1001端口!`));
    
    // 服务器接收到客户端发送过来的请求
    app.get('/aaa', (req, res) => {
        // 向简书发送相同请求,从简书服务器获取想要的数据「不存在域的限制的」
        let jianURL = `https://www.jianshu.com/asimov/subscriptions/recommended_collections`;
        req.pipe(request(jianURL)).pipe(res);
    });
    
    /* STATIC WEB */
    app.use(express.static('./'));
    

jsonp跨域

jsonp的展示
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>

    <script>
      function fun(data) {
        console.log('页面每新增一个调用jsonp对应的get类型url并且配置了回调函数为fun时的script标签,就会调用一下',data);
      }
    </script>
    <script src="https://www.baidu.com/sugrec?prod=pc&wd=1&cb=fun"></script>
  </body>
</html>
jsonp的nodejs演示
  • 前端代码index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        3. jsonp跨域---》get
    
        link(href),img(src),script(src) 都没有域的限制,获取数据的方式都是get
        使用script来实现跨域
        <script>
            function fun(data){
              console.log(data);
            }
        </script> 
        <script src="http://127.0.0.1:1001/user/list?callback=fun"></script>
    </body>
    </html>
    
  • 后端代码

    /*-CREATE SERVER-*/
    const express = require('express'),
        app = express();
    app.listen(1001, () => console.log(`服务启动成功,正在监听1001端口!`));
    
    app.get('/user/list', (req, res) => {
        // 获取传递进来的callback值,例如:'func'
        let { callback } = req.query;
        // 准备数据
        let result = {
            code: 0,
            data: ['张三', '李四']
        };
        // 返回给客户端指定的格式,例如:’函数名(数据)‘
        res.send(`${callback}(${JSON.stringify(result)})`);
    });
    
    /* STATIC WEB */
    app.use(express.static('./'));
    
动态jsonp的使用-百度联想词组-配合动态script标签
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="text" id="inp" >
    <ul id="ulbox">
    </ul>
    <script>
        let key="";
        inp.onkeydown=function(){
           key=inp.value;
           let script=document.createElement("script");
           script.id="aaa";
           script.src=`https://www.baidu.com/sugrec?prod=pc&wd=${key}&cb=func`;
           document.body.appendChild(script);
        }

        function func(data){
            console.log(data);
           let arr=data.g||[];
           let str="";
           arr.forEach(item=>{
             let {q}=item;
             str+=`<li>${q}</li>`
           })
           ulbox.innerHTML=str;

           let scriptAAA=document.getElementById("aaa");
           document.body.removeChild(scriptAAA);
        }

    </script>

</body>
</html>

jsonp函数封装

  • 前端代码

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <script src="jsonp.js"></script>
        <script>
          // jsonp(url,config)---》Promise实例
    
          jsonp("http://127.0.0.1:1001/user/list", {
            jsonpName: "callback",
          })
            .then((value) => {
              console.log(value);
            })
            .catch((err) => {
              console.log(err);
            });
    
          jsonp("https://www.baidu.com/sugrec", {
            jsonpName: "cb",
            params: {
              prod: "pc",
              wd: 1,
            },
          })
            .then((value) => {
              console.log(value);
            })
            .catch((err) => {
              console.log(err);
            });
        </script>
      </body>
    </html>
    
    /* 
      封装一个jsonp方法「基于promise管理」,执行这个方法可以发送jsonp请求 
        jsonp([url],[options])
          options配置项
          + params:null/对象 问号参数信息
          + jsonpName:'callback' 基于哪个字段把全局函数名传递给服务器
          + ...
    */
    (function () {
    //是不纯对象
      const isPlainObject = function isPlainObject(obj) {
        let proto, Ctor;
        if (!obj || Object.prototype.toString.call(obj) !== "[object Object]") return false;
        proto = Object.getPrototypeOf(obj);
        if (!proto) return true;
        Ctor = proto.hasOwnProperty('constructor') && proto.constructor;
        return typeof Ctor === "function" && Ctor === Object;
      };
    
    //将纯对象--》urlencoded格式
      const stringify = function stringify(obj) {
        let keys = Reflect.ownKeys(obj),
          str = ``;
        keys.forEach(key => {
          str += `&${key}=${obj[key]}`;
        });
        return str.substring(1);
      };
    
      const jsonp = function jsonp(url, options) {
          if(typeof url!=="string"){
              throw new Error("url必须是字符串,必须填写");
          }
    
          //判断 options 是不是纯对象,不是就转化为纯对象
          if(!isPlainObject(options)){
            options={}
          }
    
          //设置默认值  params:null/jsonpName:'callback'
          options=Object.assign({
            params:null,
            jsonpName:'callback'
          },options)
    
          return new Promise((resolve,reject)=>{
            let {params,jsonpName}=options;
    
            //函数一定要是全局的,并且是唯一的
            //  let funName="fun_"+new Date().getTime();
            let funName = "fun_" + new Date().getTime()+Math.random().toString().slice(3,9);
            window[funName]=function(data){
                resolve(data);
                clear();
            }
    
            //有 jsonpName
            if(jsonpName){
                url+=`${url.includes("?")?"&":"?"}${jsonpName}=${funName}`
            }
            
            //检查params  有并且是纯对象---》urlencoded
            if(params&&isPlainObject(params)){
                url+=`${url.includes("?")?"&":"?"}${stringify(params)}`
            }
              
            //清除函数
            function clear(){
                //script 标签删除
                let myScript=document.getElementById(funName);
                document.body.removeChild(myScript);
    
                //全局函数删除
                delete window[funName]
            }
    
            let script=document.createElement("script");
            script.id=funName;
            script.src=url;
            document.body.appendChild(script);
          //如果请求失败
            script.onerror=function(err){
              reject(err);
              clear();
            }
    
          })
      };
    
      /* 暴露API */
      if (typeof module === 'object' && typeof module.exports === 'object') module.exports = jsonp;
      if (typeof window !== 'undefined') window.jsonp = jsonp;
    })();
    
  • 后端代码

    /*-CREATE SERVER-*/
    const express = require('express'),
        app = express();
    app.listen(1001, () => console.log(`服务启动成功,正在监听1001端口!`));
    
    app.get('/user/list', (req, res) => {
        // 获取传递进来的callback值,例如:'func'
        let { callback } = req.query;
        // 准备数据
        let result = {
            code: 0,
            data: ['张三', '李四']
        };
        // 返回给客户端指定的格式,例如:’函数名(数据)‘
        res.send(`${callback}(${JSON.stringify(result)})`);
    });
    
    /* STATIC WEB */
    app.use(express.static('./'));
    

Object.assign()方法

  • Object.assign(前面对象,后面对象)用于合并多个对象的属性值。

    • 返回一个综合了多个对象所有键值对的新对象。

      • 后面对象的属性会把前面对象的同名属性的属性值给覆盖掉。

        • 如果前面对象有,但后面对象没有的属性,用的是前面对象的属性值。
        • 如果前面对象有,后面对象也有的属性,用的是后面对象的属性值。
        • 如果前面对象没有,后面对象有的属性,用的后面对象的属性值。
        • 前面对象和后面对象都没有的属性,新的对象就没有。
      • 类似于{…前面对象,…后面对象}。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
       let obj={
         name:"lili",
         age:10
       }

       let obja={
         name:"Tom",
         n:100
       }

       //合并对象
       //后面会把前面有的给覆盖,
       //后面多出的,保留
       //前面没有的,保留
        let objb=Object.assign(obj,obja);
        console.log(objb);//{name: 'Tom', age: 10, n: 100}
    </script>
</body>
</html>

进阶参考