笔记3

181 阅读11分钟

event.currentTarget,event.target

  • target在事件流的目标阶段;
  • currentTarget在事件流的捕获,目标及冒泡阶段。只有当事件流处在目标阶段的时候,两个的指向才是一样的;

当处于捕获和冒泡阶段的时候:

  • target指向被单击的对象,当前点击的元素;
  • currentTarget指向当前事件触发器活动的对象(一般为父级),click函数绑定的元素;
  • 当addEventListener的第三个参数为true的时候,代表是在捕获阶段绑定,当第三个参数为false或者为空的时候,代表在冒泡阶段绑定,默认空,是冒泡阶段。
addEventListener的第三个参数默认是空(冒泡阶段)
<body>
    <div id="a">
        <div id="b">
            <div id="c">
                <div id="d"></div>
            </div>
        </div>
    </div>
    
    <script>
        document.getElementById('a').addEventListener('click', function ( e ) {
            console.log('target:' + e.target.id + '&currentTarget:' + e.currentTarget.id);
        });
        document.getElementById('b').addEventListener('click', function ( e ) {
            console.log('target:' + e.target.id + '&currentTarget:' + e.currentTarget.id);
        });
        document.getElementById('c').addEventListener('click', function ( e ) {
            console.log('target:' + e.target.id + '&currentTarget:' + e.currentTarget.id);
        });
        document.getElementById('d').addEventListener('click', function ( e ) {
            console.log('target:' + e.target.id + '&currentTarget:' + e.currentTarget.id);
        });
    </script>
</body>

处于冒泡阶段
点击d元素时: target:d&currentTarget:d // d触发 target:d&currentTarget:c // c触发 target:d&currentTarget:b // b触发 target:d&currentTarget:a // a触发

处于捕获阶段 点击d元素时: target:d&currentTarget:a // a触发 target:d&currentTarget:b // b触发 target:d&currentTarget:c // c触发 target:d&currentTarget:d // d触发

单点登录原理

单点登录有同域和跨域两种场景

1)同域

适用场景:都是企业自己的系统,所有系统都使用同一个一级域名通过不同的二级域名来区分。

举个例子:公司有一个一级域名为 zlt.com ,我们有三个系统分别是:门户系统(sso.zlt.com)、应用1(app1.zlt.com)和应用2(app2.zlt.com),需要实现系统之间的单点登录,实现架构如下

核心原理:

  1. 门户系统设置的cookie的domain为一级域名也是zlt.com,这样就可以共享门户的cookie给所有的使用该域名xxx.zlt.com的系统
  2. 使用Spring Session等技术让所有系统共享Session
  3. 这样只要门户系统登录之后无论跳转应用1或者应用2,都能通过门户Cookie中的sessionId读取到Session中的登录信息实现单点登录

2)跨域

单点登录之间的系统域名不一样,例如第三方系统。由于域名不一样不能共享Cookie了,需要的一个独立的授权系统,即一个独立的认证中心(passport),子系统的登录均可以通过passport,子系统本身将不参与登录操作,当一个系统登录成功后,passprot将会颁发一个令牌给子系统,子系统可以拿着令牌去获取各自的保护资源,为了减少频繁认证,各个子系统在被passport授权以后,会建立一个局部会话,在一定时间内无需再次向passport发起认证

基本原理:

  1. 用户第一次访问应用系统的时候,因为没有登录,会被引导到认证系统中进行登录
  2. 根据用户提供的登录信息,认证系统进行身份校验,如果通过,返回给用户一个认证凭据-令牌
  3. 用户再次访问别的应用的时候,带上令牌作为认证凭证
  4. 应用系统接收到请求后会把令牌送到认证服务器进行校验,如果通过,用户就可以在不用登录的情况下访问其他信任的业务服务器。 登录流程

SSO

登录流程

1.  用户访问系统1的受保护资源,系统1发现用户没有登录,跳转到sso认证中心,并将自己的地址作为参数
2.  sso认证中心发现用户未登录,将用户引导到登录页面
3.  用户提交用户名、密码进行登录
4.  sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称之为全局会话,同时创建授权令牌
5.  sso 带着令牌跳转回最初的请求的地址(系统1)
6.  系统1拿着令牌,去sso认证中心校验令牌是否有效
7.  sso认证中心校验令牌,返回有效,注册系统1(也就是返回一个cookie)
8.  系统一使用该令牌创建与用户的会话,成为局部会话,返回受保护的资源
9.  用户访问系统2受保护的资源
10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
11. sso认证中心发现用户已登录,跳转回系统2的地址,并且附上令牌
12. 系统2拿到令牌,去sso中心验证令牌是否有效,返回有效,注册系统2
13. 系统2使用该令牌创建与用户的局部会话,返回受保护资源
14. 用户登录成功之后,会与sso认证中心以及各个子系统建立会话,
    用户与sso认证中心建立的会话称之为全局会话,用户与各个子系统建立的会话称之为局部会话,
    局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心

注销流程 注销流程

1.  用户向系统提交注销操作
2.  系统根据用户与系统1建立的会话,拿到令牌,向sso认证中心提交注销操作
3.  sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
4.  sso认证中心向所有注册系统发起注销请求,各注册系统销毁局部会话
5.  sso认证中心引导用户到登录页面

判断数组方式

  • [] instanceof Array

  • Object.prototype.toString.call([]) === '[object Array]'

  • Array.prototype.isPrototypeOf([])

  • [].constructor === Array

  • Array.isArray([])

如何处理类数组对象

1)JavaScript 类数组对象的定义

  • 可以通过索引访问元素,并且拥有 length 属性;
  • 没有数组的其他方法,例如 push , forEach , indexOf 等。
var foo = {
    0: 'JS',
    1: 'Node',
    2: 'TS',
    length: 3
}

2)转换方式

// 方式一
Array.prototype.slice.call(arguments);
Array.prototype.slice.apply(arguments)
[].slice.call(arguments)

// 方式二
Array.from(arguments);

// 方式三
// 这种方式要求 数据结构 必须有 遍历器接口
[...arguments] 

// 方式四
[].concat.apply([],arguments)

// 方式五:手动实现
function toArray(s){
  var arr = [];  
  for(var i = 0,len = s.length; i < len; i++){   
    arr[i] = s[i];   
  }  
  return arr;  
}

3)转换后注意几点

  • 数组长度由类数组的length属性决定
  • 索引不连续,会自动补位undefined
  • 仅考虑0和正整数索引
  • slice会产生稀疏数组,内容是empty而不是undefined
  • 类数组push注意,push操作的是索引值为length的位置

CORS

CORS即Cross Origin Resource Sharing(跨来源资源共享),通俗说就是我们所熟知的跨域请求。众所周知,在以前,跨域可以采用代理、JSONP等方式,而在Modern浏览器面前,这些终将成为过去式,因为有了CORS。

CORS在最初接触的时候只大概了解到,通过服务器端设置Access-Control-Allow-Origin响应头,即可使指定来源像访问同源接口一样访问跨域接口,最近在使用CORS的时候,由于需要传输自定义Header信息,发现原来CORS的规范定义远不止这些。

CORS可以分成两种:

1.简单请求:

HTTP方法是下列之一

  • HEAD
  • GET
  • POST HTTP头信息不超出以下几种字段
  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type,但仅能是下列之一
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain 任何一个不满足上述要求的请求,即被认为是复杂请求。一个复杂请求不仅有包含通信内容的请求,同时也包含预请求(preflight request)。

简单请求的发送从代码上来看和普通的XHR没太大区别,但是HTTP头当中要求总是包含一个域(Origin)的信息。该域包含协议名、地址以及一个可选的端口。不过这一项实际上由浏览器代为发送,并不是开发者代码可以触及到的。

简单请求的部分响应头及解释如下:

  • Access-Control-Allow-Origin(必含)- 不可省略,否则请求按失败处理。该项控制数据的可见范围,如果希望数据对任何人都可见,可以填写"*"。
  • Access-Control-Allow-Credentials(可选) – 该项标志着请求当中是否包含cookies信息,只有一个可选值:true(必为小写)。如果不包含cookies,请略去该项,而不是填写false。这一项与XmlHttpRequest2对象当中的withCredentials属性应保持一致,即withCredentials为true时该项也为true;withCredentials为false时,省略该项不写。反之则导致请求失败。
  • Access-Control-Expose-Headers(可选) – 该项确定XmlHttpRequest2对象当中getResponseHeader()方法所能获得的额外信息。通常情况下,getResponseHeader()方法只能获得如下的信息:
  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Etag
  • Last-Modified
  • Pragma
  • 当你需要访问额外的信息时,就需要在这一项当中填写并以逗号进行分隔 如果仅仅是简单请求,那么即便不用CORS也没有什么大不了,但CORS的复杂请求就令CORS显得更加有用了。简单来说,任何不满足上述简单请求要求的请求,都属于复杂请求。比如说你需要发送PUT、DELETE等HTTP动作,或者发送Content-Type: application/json的内容。

2.复杂请求

复杂请求表面上看起来和简单请求使用上差不多,但实际上浏览器发送了不止一个请求。其中最先发送的是一种"预请求",此时作为服务端,也需要返回"预回应"作为响应。预请求实际上是对服务端的一种权限请求,只有当预请求成功返回,实际请求才开始执行。

预请求以OPTIONS形式发送,当中同样包含域,并且还包含了两项CORS特有的内容:

  • Access-Control-Request-Method – 该项内容是实际请求的种类,可以是GET、POST之类的简单请求,也可以是PUT、DELETE等等。
  • Access-Control-Request-Headers – 该项是一个以逗号分隔的列表,当中是复杂请求所使用的头部。

显而易见,这个预请求实际上就是在为之后的实际请求发送一个权限请求,在预回应返回的内容当中,服务端应当对这两项进行回复,以让浏览器确定请求是否能够成功完成。

复杂请求的部分响应头及解释如下:

  • Access-Control-Allow-Origin(必含) – 和简单请求一样的,必须包含一个域。
  • Access-Control-Allow-Methods(必含) – 这是对预请求当中Access-Control-Request-Method的回复,这一回复将是一个以逗号分隔的列表。尽管客户端或许只请求某一方法,但服务端仍然可以返回所有允许的方法,以便客户端将其缓存。
  • Access-Control-Allow-Headers(当预请求中包含Access-Control-Request-Headers时必须包含) – 这是对预请求当中Access-Control-Request-Headers的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。这里在实际使用中有遇到,所有支持的头部一时可能不能完全写出来,而又不想在这一层做过多的判断,没关系,事实上通过request的header可以直接取到Access-Control-Request-Headers,直接把对应的value设置到Access-Control-Allow-Headers即可。
  • Access-Control-Allow-Credentials(可选) – 和简单请求当中作用相同。
  • Access-Control-Max-Age(可选) – 以秒为单位的缓存时间。预请求的的发送并非免费午餐,允许时应当尽可能缓存。

一旦预回应如期而至,所请求的权限也都已满足,则实际请求开始发送。

通过caniuse.com得知,目前大部分Modern浏览器已经支持完整的CORS,但IE直到IE11才完美支持,所以对于PC网站,还是建议采用其他解决方案,如果仅仅是移动端网站,大可放心使用。

import原理(实际上就是ES6 module的原理)

  1. 简单来说就是闭包的运用
  2. 为了创建Module的内部作用域,会调用一个包装函数
  3. 包装函数的返回值也就是Module向外公开的API,也就是所有export出去的变量
  4. import也就是拿到module导出变量的引用

与require的不同

  • CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用
  • CommonJS模块是运行时加载,ES6模块是编译时输出接口

CommonJS是运行时加载对应模块,一旦输出一个值,即使模块内部对其做出改变,也不会影响输出值,如:

// a.js
var a = 1;
function changeA(val) {
    a = val;
}
module.exports = {
    a: a,
    changeA: changeA,
}

// b.js
var modA = require('./a.js');
console.log('before', modA.a); // 输出1
modA.changeA(2);
console.log('after', modA.a); // 还是1

代码执行的错误捕获

1.try……catch

  • 能捕获到代码执行的错误
  • 捕获不到语法的错误
  • 无法处理异步中的错误
  • 使用try... catch 包裹,影响代码可读性

2.window.onerror

  • 无论是异步还是非异步错误,onerror 都能捕获到运行时错误
  • onerror 主要是来捕获预料之外的错误,而 try-catch 则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。
  • window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx
  • 当我们遇到 <img src="./404.png"> 报 404 网络请求异常的时候,onerror 是无法帮助我们捕获到异常的。

缺点:  监听不到资源加载的报错onerror,事件处理函数只能声明一次,不会重复执行多个回调:

强调:window.onerror只能捕获即时运行错误,对于资源加载错误不能捕获。资源加载错误这种错误不会冒泡,也就是说script标签这个节点发生了错误,到触发它本身的onerror事件就OK了,不会再触发向上冒泡的window了,也就是window.onerror无法捕获资源加载错误的原因

3.window.addEventListener('error')

可以监听到资源加载报错,也可以注册多个事件处理函数。 window.addEventListener('error',(msg, url, row, col, error) => {}, true)

如果将捕获true改为冒泡false,则不能捕获到错误

但是这种方式虽然可以捕捉到网络请求的异常,却无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。

4.window.addEventListener('unhandledrejection')

捕获Promise错误,当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件;这可能发生在 window 下,但也可能发生在 Worker 中。 这对于调试回退错误处理非常有用。

资源加载的错误捕获

  • imgObj.onerror()

    • 比如说我这个地方是图片,图片的话有一个onerror事件,通过这个onerror事件的话就能捕获到它的加载错误;
    • script标签上也可以加载一个onerror事件,它出错了也会触发onerror事件
  • performance.getEntries(),获取到成功加载的资源,对比可以间接的捕获错误

  • window.addEventListener('error', fn, true), 会捕获但是不冒泡,所以window.onerror 不会触发,捕获阶段可以触发

    • 在window上通过事件捕获一样可以拦截到资源加载错误,上面说的onerror它阻止了冒泡,但是它没有阻止捕获,所以可以通过捕获阶段拿到这个资源加载错误
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title>错误监控</title>
        <script type="text/javascript">
            window.addEventListener('error', function (e) {
               console.log('捕获', e);
            }, true); //true为捕获,false为冒泡
        </script>
    </head>
    <body>
        <!-- 没有这个文件 -->
        <script src="//baidu.com/test.js" charset="urf-8"></script>
    </body>
    </html>
    

问题延伸

问题延伸:如果我们的js错误是跨域的,跨域的js运行错误还可以被捕获到吗?如果可以的话会出现什么错误提示?那么对于这种情况应该怎么处理?

跨域的话也是可以捕获到错误的,但是它会拿到一个错误,就是所有跨域文件js的代码错误,获得一个错误信息的时候都是一个Script error,出错的行号和列号和出错详情都没法拿到,因为已经跨域了没有权限,只能捕获到这个错误但是没有拿到相应的具体信息。

image.png

怎么办?两步走:

1.在script标签上增加crossorigin属性

(这是在客户端做的)

2.设置js资源响应头Access-Control-Allow-Origin:*

(在服务端做的,在响应你这个js资源的时候在HTTP头上加一个Access-Control-Allow-Origin:,这个后面可以是,也可以是指定你的域名,总之要加这两个处理,你就可以拿到详细的信息了,这块是一定要注意的)

上报错误的基本原理?

1.采用Ajax通信的方式上报 能做到,但是所有的错误监控都不是通过这种方式来做的

2.利用Image对象上报 是所有的监控体系来做的,比如Google的JA,国内的CNZZ,他们都是都是通过Image对象上报的

(如果你能说出第二点,说明你对错误监控这块的原理了解的很清楚,如果你说的第一种,你这块不算及格)

如何利用Image对象上报那?

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>错误监控</title>
    </head>
<body>
    <script type="text/javascript">
        //利用这种方式发送一个请求非常简单,比Ajax简单的多,而且不需要借助任何第三方的库
        //一行代码就可以轻松地实现一个资源向上报,这就是Image对象比较特殊的特点,一定要记住这个用法
        //http://baidu.com/tesjk是你上传的路径,后面可以加任何一个参数
        //在页面中的Network中可以看到请求已经发出去了
        (new Image()).src='http://baidu.com/tesjk?r=tksjk';
    </script>
</body>
</html>

Vue、React中

Vue有 errorHandler,React有 componentDidCatch 进行错误捕获

tab切换取消请求

多个 tab 只对应一个内容框,点击每个 tab 都会请求接口并渲染到内容框,怎么确保频繁点击 tab 但能够确保数据正常显示?
null

一、分析

因为每个请求处理时长不一致,可能会导致先发送的请求后响应,即请求响应顺序和请求发送顺序不一致,从而导致数据显示不正确。

即可以理解为连续触发多个请求,如何保证请求响应顺序和请求发送顺序一致。对于问题所在场景,用户只关心最后数据是否显示正确,即可以简化为:连续触发多个请求,如何保证最后响应的结果是最后发送的请求(不关注之前的请求是否发送或者响应成功)

类似场景:input输入框即时搜索,表格快速切换页码

二、解决方案

防抖(过滤掉一些非必要的请求) + 取消上次未完成的请求(保证最后一次请求的响应顺序)

取消请求方法:

  • XMLHttpRequest 使用 abort api 取消请求
  • axios 使用 cancel token 取消请求

伪代码(以 setTimeout 模拟请求,clearTimeout 取消请求)

/**
    函数防抖,一定时间内连续触发事件只执行一次 * @param {*} func 需要防抖的函数 * @param {*} delay 防抖延迟 * @param {*} immediate 是否立即执行,为true表示连续触发时立即执行,即执行第一次,为false表示连续触发后delay ms后执行一次.
*/

let debounce = function(func, delay = 100, immediate = false) {
    let timeoutId, last, context, args, result
  
    function later() {
      const interval = Date.now() - last
      if (interval < delay && interval >= 0) {
        timeoutId = setTimeout(later, delay - interval)
      } else {
        timeoutId = null
        if (!immediate) {
          result = func.apply(context, args)
          context = args = null
        }
      }
    }
  
    return function() {
      context = this
      args = arguments
      last = Date.now()
  
      if (immediate && !timeoutId) {
        result = func.apply(context, args)
        context = args = null // 解除引用
      }
      
      if (!timeoutId) {
        timeoutId = setTimeout(later, delay)
      }
  
      return result
    }
  }

-----------------------------------------------------------

let flag = false   // 标志位,表示当前是否正在请求数据
let xhr = null

let request = (i) => {
    if (flag) {
        clearTimeout(xhr)
        console.log(`取消第${i - 1}次请求`)
    }
    flag = true
    console.log(`开始第${i}次请求`)
    xhr = setTimeout(() => {
        console.log(`请求${i}响应成功`)
        flag = false
    }, Math.random() * 200)
}

let fetchData = debounce(request, 50)  // 防抖

// 模拟连续触发的请求
let count = 1 
let getData = () => {
  setTimeout(() => {
    fetchData(count)
    count++
    if (count < 11) {
        getData()
    }
  }, Math.random() * 200)
}
getData()

/* 某次测试输出:
    开始第2次请求
    请求2响应成功
    开始第3次请求
    取消第3次请求
    开始第4次请求
    请求4响应成功
    开始第5次请求
    请求5响应成功
    开始第8次请求
    取消第8次请求
    开始第9次请求
    请求9响应成功
    开始第10次请求
    请求10响应成功
*/