跨域与JSONP(同源策略和跨域, JSONP 案例-淘宝搜索, 防抖和节流)

397 阅读5分钟

跨域与JSONP

1. 了解同源策略和跨域

1.1 同源策略

1. 什么是同源

如果两个页面的协议,域名端口都相同,则两个页面具有相同的源。

例如,下表给出了相对于 www.test.com/index.html 页面的同源检测:

image.png

2. 什么是同源策略

同源策略(英文全称 Same origin policy)是浏览器提供的一个安全功能。

MDN 官方给定的概念:

  • 同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。
  • 这是一个用于隔离潜在恶意文件的重要安全机制。 通俗的理解: 浏览器规定,A 网站的 JavaScript,不允许和非同源的网站 C 之间,进行资源的交互

例如:

  • 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
  • 无法接触非同源网页的 DOM
  • 无法向非同源地址发送 Ajax 请求

image.png

1.2 跨域

1. 什么是跨域

同源指的是两个URL的协议域名端口一致,反之,则是跨域。

出现跨域的根本原因:浏览器的同源策略不允许非同源的 URL 之间进行资源的交互。

2. 浏览器对跨域请求的拦截

image.png 注意:浏览器允许发起跨域请求,但是,跨域请求回来的数据,会被浏览器拦截,无法被页面获取到!

3. 如何实现跨域数据请求

实现跨域数据请求,最主要的两种解决方案,分别是JSONPCORS

  • JSONP:出现的早,兼容性好(兼容低版本IE)。是前端程序员为了解决跨域问题,被迫想出来的一种临时解决方案。缺点是只支持 GET 请求,不支持 POST 请求。
  • CORS:出现的较晚,它是 W3C 标准,属于跨域 Ajax 请求的根本解决方案。支持 GET 和 POST 请求。缺点是不兼容某些低版本的浏览器。

image.png 注意:目前 JSONP 在实际开发中很少会用到,CORS 是跨域的主流技术解决方案。

2. CORS

2.1 CORS 的概念

CORS 是解决跨域数据请求的终极解决方案,全称是 Cross-origin resource sharing。

CORS 技术需要浏览器和服务器同时支持,二者缺一不可:

  1. 浏览器要支持 CORS 功能(主流的浏览器全部支持,IE 不能低于 IE10)
  2. 服务器要开启 CORS 功能(需要后端开发者为接口开启 CORS 功能)

实现 CORS 的关键,是在服务器端

  • 原因:如果服务器端没有开启 CORS 功能,则客户端无法访问那些跨域的接口!

2.2 CORS 的原理

服务器端通过 Access-Control-Allow-Origin 响应头,来告诉浏览器当前的 API 接口是否允许跨域请求。

image.png

2.3 CORS 的两个主要优势

  1. CORS 是真正的 Ajax 请求,支持 GET、POST、DELETE、PUT、PATCH 等这些常见的 Ajax 请求方式
  2. 只需要后端开启 CORS 功能即可,前端的代码无须做任何改动

3. JSONP

3.1 什么是JSONP

JSONP (JSON with Padding) 是 JSON 的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。

3.2 JSONP的实现原理

由于浏览器同源策略的限制,网页中无法通过 Ajax 请求非同源的接口数据。但是 <script>标签不受浏览器同源策略的影响,可以通过 src 属性,请求非同源的 js 脚本。

JSONP 的实现原理

JSONP 在底层,用到了 <script> 标签的 src 属性!

原因:

  • <script> 标签的 src 属性,不受浏览器同源策略的限制
  • 可以把非同源的 JavaScript 代码请求到本地,并执行

3.3 JSONP 的底层实现原理

把非同源的 JavaScript 代码请求到本地,并执行:

<body>
    <!-- 把非同源的 JavaScript 代码请求到本地,并执行 -->
    <script src="http://www.liulongbin.top:3009/api/getscript"></script>
</body>

image.png

如果请求回来的 JavaScript 代码只包含函数的调用,则需要程序员手动定义 show 方法。示例代码如下:

 <script>
      // 1. 手动定义 show 方法
      function show(data) {
        console.log(data);
      }
    </script>
    <!-- 2. 把非同源的 JavaScript 代码请求到本地,并执行 -->
    <script src="http://www.liulongbin.top:3009/api/getscript2"></script>

缺点:服务器响应回来的代码中,调用的函数名是写死的!

在指定 <script> 标签的 src 属性时,可以通过查询参数中的 callback,自定义回调函数的名称:

<script>
      // 1. 手动定义 showInfo 方法
      function showInfo(data) {
        console.log(data);
      }
</script>
    <!-- 通过 callback 参数,自定义回调函数的名字 -->
    <script src="http://www.liulongbin.top:3009/api/jsonp?callback=showInfo"></script>

image.png

在指定 <script> 标签的 src 属性时,还可以通过查询参数的方式,指定要发送给服务器的数据:

    <script>
      // 1. 手动定义 showInfo 方法
      function showInfo(data) {
        console.log(data);
      }
    </script>
    <!-- 通过 callback 参数,自定义回调函数的名字 -->
    <!-- 指定 name 和 age 这两个参数 -->
    <script src="http://www.liulongbin.top:3009/api/jsonp?callback=showInfo&name=zs&age=20"></script>

image.png

3.4 JSONP的缺点

由于 JSONP 是通过 <script> 标签的 src 属性,来实现跨域数据获取的,所以,JSONP 只支持 GET 数据请求,不支持 POST 请求。

注意:JSONP 和 Ajax 之间没有任何关系,不能把 JSONP 请求数据的方式叫做 Ajax,因为 JSONP 没有用到 XMLHttpRequest 这个对象。

3.5 jQuery中的JSONP

jQuery 提供的 $.ajax() 函数,除了可以发起真正的 Ajax 数据请求之外,还能够发起 JSONP 数据请求,例如

$.ajax({
    url'http://ajax.frontend.itheima.net:3006/api/jsonp?name=zs&age=20',
    // 如果要使用 $.ajax() 发起 JSONP 请求,必须指定 datatype 为 jsonp
    dataType'jsonp',
    successfunction(res) {
       console.log(res)
    }
 })

默认情况下,使用 jQuery 发起 JSONP 请求,会自动携带一个 callback=jQueryxxx 的参数,jQueryxxx 是随机生成的一个回调函数名称。

3.6 自定义参数及回调函数名称

在使用 jQuery 发起 JSONP 请求时,如果想要自定义 JSONP 的参数以及回调函数名称,可以通过如下两个参数来指定:

<body>
    <script src="../lib/axios.js"></script>
    <script src="../lib/jquery.js"></script>
    <script>
      $.ajax({
        url: 'http://www.liulongbin.top:3006/api/jsonp?name=zs&age=20',
        // 如果要使用 $.ajax() 发起 JSONP 请求,必须指定 datatype 为 jsonp
        dataType: 'jsonp',
        jsonp:'callback',
        jsonpCallback:'abc',
        success: function (res) {
          console.log(res);
        },
      });
    </script>
  </body>

image.png

3.7 jQuery中JSONP的实现过程

jQuery 中的 JSONP,也是通过 <script> 标签的 src 属性实现跨域数据访问的,只不过,jQuery 采用的是动态创建和移除 <script> 标签的方式,来发起 JSONP 数据请求。 在发起 JSONP 请求的时候,动态向 <header> 中 append 一个 <script> 标签; 在 JSONP 请求成功以后,动态从 <header> 中移除刚才 append 进去的 <script> 标签;

3.8 手写jsonp

<script>
      // 实现跨域 通过jsonp的原来跨域
      // jsonp("url").then(result => { })
      function jsonp(url) {
        return new Promise(function (resolve, reject) {
          // 要根据jsonp的实现原理 做跨域请求
          // script的src不受同源策略的限制
          // src不受跨域的限制 => url给src => 跨域会返回一段执行代码 => 对应的函数接收这个代码数据
          let script = document.createElement("script"); // 创建一个标签

          // 定义一个回调的方法 -> 将方法名传入到url -> 调用对应的方法
          // 创建了一个高频方法 - 用完方法之后 - 销毁
          // 方法不能是固定的
          const callbackName = `callback${Date.now()}`; // 随机会生成一个方法名
          // 如何定义一个全局方法
          window[callbackName] = function (data) {
            // data就是返回的数据
            // console.log(data);
            resolve(data); // 此时任务已经结束
            delete window[callbackName]; // 销毁方法
            document.body.removeChild(script); // 移除标签
          };
          url += "?callback=" + callbackName;
          script.src = url; // 赋值标签内容

          document.body.append(script); // 将标签加到页面上
        });
      }
      jsonp("http://www.liulongbin.top:3009/api/jsonp").then((result) => {
        console.log(result);
      });
    </script>

4. 案例 – 淘宝搜索

4.1 要实现的UI效果

image.png

4.2 获取用户输入的搜索关键词

为了获取到用户每次按下键盘输入的内容,需要监听输入框的 keyup 事件,示例代码如下:

// 监听文本框的 keyup 事件
 $('#ipt').on('keyup'function() {
   // 获取用户输入的内容
   var keywords = $(this).val().trim()
   // 判断用户输入的内容是否为空
   if (keywords.length <= 0) {
     return
   }
   // TODO:获取搜索建议列表
 })

4.3 封装getSuggestList函数

将获取搜索建议列表的代码,封装到 getSuggestList 函数中,示例代码如下:

 function getSuggestList(kw) {
   $.ajax({
      // 指定请求的 URL 地址,其中,q 是用户输入的关键字
      url'https://suggest.taobao.com/sug?q=' + kw,
      // 指定要发起的是 JSONP 请求
      dataType'jsonp',
      // 成功的回调函数
      successfunction(res) { console.log(res) }
   })
 }

4.4 渲染建议列表的UI结构

1. 定义搜索建议列表

<div class="box">
    <!-- tab 栏区域 -->
    <div class="tabs"></div>
    <!-- 搜索区域 -->
    <div class="search-box"></div>
    <!-- 搜索建议列表 -->
    <div id="suggest-list"></div>
 </div>

2. 定义模板结构

<!-- 模板结构 -->
 <script type="text/html" id="tpl-suggestList">
    {{each result}}
       <div class="suggest-item">{{$value[0]}}</div>
    {{/each}}
 </script>

3. 定义渲染模板结构的函数

// 渲染建议列表
 function renderSuggestList(res) {
    // 如果没有需要渲染的数据,则直接 return
    if (res.result.length <= 0) {
       return $('#suggest-list').empty().hide()
    }
    // 渲染模板结构
    var htmlStr = template('tpl-suggestList', res)
    $('#suggest-list').html(htmlStr).show()
 }

4. 搜索关键词为空时隐藏搜索建议列表

$('#ipt').on('keyup'function() {
    // 获取用户输入的内容
    var keywords = $(this).val().trim()
    // 判断用户输入的内容是否为空
    if (keywords.length <= 0) {
       // 如果关键词为空,则清空后隐藏搜索建议列表
       return $('#suggest-list').empty().hide()
    }
    getSuggestList(keywords)
 })

4.5 输入框的防抖

1. 什么是防抖

防抖策略(debounce)是当事件被触发后,延迟 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。

image.png

2. 防抖的应用场景

用户在输入框中连续输入一串字符时,可以通过防抖策略,只在输入完后,才执行查询的请求,这样可以有效减少请求次数,节约请求资源;

3. 实现输入框的防抖

 var timer = null                    // 1. 防抖动的 timer

 function debounceSearch(keywords) { // 2. 定义防抖的函数
    timer = setTimeout(function() {
    // 发起 JSONP 请求
    getSuggestList(keywords)
    }, 500)
 }

 $('#ipt').on('keyup'function() {  // 3. 在触发 keyup 事件时,立即清空 timer
    clearTimeout(timer)
    // ...省略其他代码
    debounceSearch(keywords)
 })

4.6 缓存搜索的建议列表

1. 定义全局缓存对象

// 缓存对象
  var cacheObj = {}

2. 将搜索结果保存到缓存对象中

// 渲染建议列表
 function renderSuggestList(res) {
    // ...省略其他代码
    // 将搜索的结果,添加到缓存对象中
    var k = $('#ipt').val().trim()
    cacheObj[k] = res
 }

3. 优先从缓存中获取搜索建议

// 监听文本框的 keyup 事件
 $('#ipt').on('keyup'function() {
    // ...省略其他代码

    // 优先从缓存中获取搜索建议
    if (cacheObj[keywords]) {
       return renderSuggestList(cacheObj[keywords])
    }
    // 获取搜索建议列表
    debounceSearch(keywords)
  })

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>淘宝搜索案例</title>
    <!-- 导入页面的基本样式 -->
    <link rel="stylesheet" href="./css/search.css" />
  </head>

  <body>
    <div class="container">
      <!-- Logo -->
      <img src="./images/taobao_logo.png" alt="" class="logo" />

      <div class="box">
        <!-- tab 栏 -->
        <div class="tabs">
          <div class="tab-active">宝贝</div>
          <div>店铺</div>
        </div>
        <!-- 搜索区域(搜索框和搜索按钮) -->
        <div class="search-box">
          <input
            type="text"
            class="ipt"
            placeholder="请输入要搜索的内容"
          /><button class="btnSearch">搜索</button>
        </div>

        <!-- 搜索建议列表 -->
        <div id="suggest-list">
          <!-- <div class="suggest-item">搜索建议1</div>
          <div class="suggest-item">搜索建议2</div>
          <div class="suggest-item">搜索建议3</div>
          <div class="suggest-item">搜索建议4</div> -->
        </div>
      </div>
    </div>

    <!-- 导入 jQuery -->
    <script src="./lib/jquery.js"></script>
    <!-- 导入 axios -->
    <script src="./lib/axios.js"></script>
    <script>
      // onchange(离开焦点触发) oninput(输入触发)
      let timeId = null;
      $(".ipt").on("input", function () {
        // 防抖 -> 搜索场景 -> 停下来的时搜索的搜索才有意义
        clearTimeout(timeId); // 只会清除500毫秒之内的逻辑
        const kw = $(this).val().trim(); // 得到关键字
        if (!kw) return;
        timeId = setTimeout(() => {
          // 执行搜索
          getSuggestList(kw);
        }, 300);
      });
      function getSuggestList(kw) {
        // 进行查询
        jsonp("http://www.liulongbin.top:3006/api/suggest", {
          q: kw,
        }).then(({ result }) => {
          $("#suggest-list").html(
            result.map((item) => `<div class="suggest-item">${item}</div>`)
          );
        });
      }
      function jsonp(url, query) {
        return new Promise(function (resolve, reject) {
          // 要根据jsonp的实现原理 做跨域请求
          // script的src不受同源策略的限制
          // src不受跨域的限制 => url给src => 跨域会返回一段执行代码 => 对应的函数接收这个代码数据
          let script = document.createElement("script"); // 创建一个标签

          // 定义一个回调的方法 -> 将方法名传入到url -> 调用对应的方法
          // 创建了一个高频方法 - 用完方法之后 - 销毁
          // 方法不能是固定的
          const callbackName = `callback${Date.now()}`; // 随机会生成一个方法名
          // 如何定义一个全局方法
          window[callbackName] = function (data) {
            // data就是返回的数据
            // console.log(data);
            resolve(data); // 此时任务已经结束
            delete window[callbackName]; // 销毁方法
            document.body.removeChild(script); // 移除标签
          };
          url += "?callback=" + callbackName;
          // 遍历对象
          // 传进来的查询参数
          var arr = [];
          for (var key in query) {
            arr.push(`${key}=${query[key]}`);
          }
          script.src = url + "&" + arr.join("&"); // 赋值标签内容

          document.body.append(script); // 将标签加到页面上
        });
      }
    </script>
  </body>
</html>

5. 防抖和节流

5.1 什么是节流

节流策略(throttle),顾名思义,可以减少一段时间内事件的触发频率。

image.png

5.2 节流的应用场景

  • 鼠标连续不断地触发某事件(如点击),只在单位时间内只触发一次;
  • 懒加载时要监听计算滚动条的位置,但不必每次滑动都触发,可以降低计算的频率,而不必去浪费 CPU 资源;

5.3 节流案例 – 鼠标跟随效果

image.png

1. 渲染UI结构并美化样式

<!-- UI 结构 -->
<img src="./assets/angel.gif" alt="" id="angel" />

/* CSS 样式 */
html, body {
  margin0;
  padding0;
  overflow: hidden;
}
#angel {
  position: absolute;
}

2. 不使用节流时实现鼠标跟随效果

$(function() {
   // 获取图片元素
   var angel = $('#angel')
   // 监听文档的 mousemove 事件
   $(document).on('mousemove'function(e) {      // 设置图片的位置
      $(angel).css('left', e.pageX + 'px').css('top', e.pageY + 'px')
   })
})

3. 节流阀的概念

高铁卫生间是否被占用,由红绿灯控制,红灯表示被占用,绿灯表示可使用。 假设每个人上卫生间都需要花费5分钟,则五分钟之内,被占用的卫生间无法被其他人使用。

上一个人使用完毕后,需要将红灯重置为绿灯,表示下一个人可以使用卫生间。 下一个人在上卫生间之前,需要先判断控制灯是否为绿色,来知晓能否上卫生间。

节流阀为空,表示可以执行下次操作;不为空,表示不能执行下次操作。 当前操作执行完,必须将节流阀重置为空,表示可以执行下次操作了。 每次执行操作前,必须先判断节流阀是否为空。

4. 使用节流优化鼠标跟随效果

$(function() {
  var angel = $('#angel')
  var timer = null // 1.预定义一个 timer 节流阀
  $(document).on('mousemove'function(e) {
    if (timer) { return } // 3.判断节流阀是否为空,如果不为空,则证明距离上次执行间隔不足16毫秒
    timer = setTimeout(function() {
      $(angel).css('left', e.pageX + 'px').css('top', e.pageY + 'px')
      timer = null // 2.当设置了鼠标跟随效果后,清空 timer 节流阀,方便下次开启延时器
    }, 16)
  })
})

5.4 总结防抖和节流的区别

防抖:如果事件被频繁触发,防抖能保证只有最有一次触发生效!前面 N 多次的触发都会被忽略!

节流:如果事件被频繁触发,节流能够减少事件触发的频率,因此,节流是有选择性地执行一部分事件!