浏览器基础知识点

1. 事件的触发过程是怎么样的?知道什么是事件代理嘛?

  • 事件捕获
  • 目标事件
  • 事件冒泡

事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个 body 中的子节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。

// 以下会先打印冒泡然后是捕获
node.addEventListener(
  'click',
  event => {
    console.log('冒泡')
  },
  false
)
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)			

注册事件

通常我们使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 false ,useCapture 决定了注册的事件是捕获事件还是冒泡事件。

  • true 捕获
  • false 冒泡

使用 addEventListener 添加的事件只能使用removeEventListener 来进行移除

如果我们只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。

stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。

node.addEventListener(
  'click',
  event => {
    event.stopImmediatePropagation()
    console.log('冒泡')
  },
  false
)
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)

事件代理/事件委托

利用事件冒泡,指定一个事件处理程序,管理某一类型的所有事件。

如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上

<ul id="ul">
	<li>1</li>
    <li>2</li>
	<li>3</li>
	<li>4</li>
	<li>5</li>
</ul>
<script>
	let ul = document.querySelector('#ul')
	ul.addEventListener('click', (event) => {
		console.log(event.target);
	})
</script>

事件代理的方式相较于直接给目标注册事件来说,有以下优点:

  • 节省内存
  • 不需要给子节点注销事件

使用事件代理,将每一个li 中的内容弹出

  1. 在父元素ul 上注册事件
  2. 根据每一个li 元素的id 进行判断
  3. 将li 的内容弹出

2. 什么是跨域?为什么浏览器要使用同源策略?你有几种方式可以解决跨域问题?了解预检请求嘛?

协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。

为什么浏览器要使用同源策略

防止 CSRF 攻击的。简单点说,CSRF 攻击是利用用户的登录态发起恶意请求。

也就是说,没有同源策略的情况下,A 网站可以被任意其他来源的 Ajax 访问到内容。如果你当前 A 网站还存在登录态,那么对方就可以通过 Ajax 获得你的任何信息。当然跨域并不能完全阻止 CSRF。

然后我们来考虑一个问题,请求跨域了,那么请求到底发出去没有? 请求必然是发出去了,但是浏览器拦截了响应。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会。因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

解决跨域的方法

1. jsonp

JSONP 的原理很简单,就是利用

JSONP 使用简单且兼容性不错,但是只限于 get 请求。

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP,以下是简单实现

function jsonp(url, jsonpCallback, success) {
  let script = document.createElement('script')
  script.src = url
  script.async = true
  script.type = 'text/javascript'
  window[jsonpCallback] = function(data) {
    success && success(data)
  }
  document.body.appendChild(script)
}
jsonp('http://xxx', 'callback', function(value) {
  console.log(value)
})

2. cors

CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。

简单请求

以 Ajax 为例,当满足以下条件时,会触发简单请求

使用下列方法之一:

  • GET
  • HEAD
  • POST

Content-Type 的值仅限于下列三者之一:

  • text/plain

  • multipart/form-data

  • application/x-www-form-urlencoded

请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

复杂请求

对于复杂请求来说,首先会发起一个预检请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

3. document.domain

该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。

只需要给页面添加 document.domain = ‘test.com’ 表示二级域名都相同就可以实现跨域

4. postMessage

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

// 发送消息端
window.parent.postMessage('message', 'http://test.com')
// 接收消息端
var mc = new MessageChannel()
mc.addEventListener('message', event => {
  var origin = event.origin || event.originalEvent.origin
  if (origin === 'http://test.com') {
    console.log('验证通过')
  }
})

3. 有几种方式可以实现存储功能,分别有什么优缺点?什么是 Service Worker

浏览器基础知识点

对于 cookie 来说,我们还需要注意安全性。
浏览器基础知识点

Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。

Service Worker 实现缓存功能一般分为三个步骤:

  1. 首先需要先注册 Service Worker,
  2. 然后监听到 install 事件以后就可以缓存需要的文件,
  3. 那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:
// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
      console.log('service worker 注册成功')
    })
    .catch(function(err) {
      console.log('servcie worker 注册失败')
    })
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll(['./index.html', './index.js'])
    })
  )
})

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response
      }
      console.log('fetch source')
    })
  )
})

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了
浏览器基础知识点
在 Cache 中也可以发现我们所需的文件已被缓存
浏览器基础知识点
当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的
浏览器基础知识点

4. 浏览器缓存机制

缓存可以说是性能优化中简单高效的一种优化方式了,它可以显著减少网络传输所带来的损耗。

对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

接下来的内容中我们将通过以下几个部分来探讨浏览器缓存机制:

  • 缓存位置
  • 缓存策略
  • 实际场景应用缓存策略

缓存位置

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络

  1. Service Worker
  2. Memory Cache
  3. Disk Cache
  4. Push Cache
  5. 网络请求

Service Worker

Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们*控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

Memory Cache

Memory Cache 也就是内存中的缓存,读取内存中的数据肯定比磁盘快。但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存
浏览器基础知识点

那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?

先说结论,这是不可能的。首先计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。内存中其实可以存储大部分的文件,比如说 JSS、HTML、CSS、图片等等。但是浏览器会把哪些文件丢进内存这个过程就很玄学了,我查阅了很多资料都没有一个定论。

当然,我通过一些实践和猜测也得出了一些结论:

  • 对于大文件来说,大概率是不存储在内存中的,反之优先
  • 当前系统内存使用率高的话,文件优先存储进硬盘

缓存策略

通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

强缓存

强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control 。强缓存表示在缓存期间不需要请求,state code 为 200。

a. Expires

Expires: Wed, 22 Oct 2018 08:41:00 GMT

Expires 是 HTTP/1 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

b. Cache-Control

Cache-control: max-age=30

Cache-Control 出现于 HTTP/1.1,优先级高于 Expires 。该属性值表示资源会在 30 秒后过期,需要再次请求。

Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令
浏览器基础知识点

从图中我们可以看到,我们可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等等。

接下来我们就来学习一些常见指令的作用
浏览器基础知识点

协商缓存

如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。

当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。

Last-Modified 和 If-Modified-Since

Last-Modified 表示本地文件最后修改日期,

If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码。

但是 Last-Modified 存在一些弊端:

如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag 。

ETag 和 If-None-Match

ETag 类似于文件指纹,
If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。

并且 ETag 优先级比 Last-Modified 高。

如果什么缓存策略都没设置,那么浏览器会怎么处理?

对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。

5. 实际场景应用缓存策略

单纯了解理论而不付诸于实践是没有意义的,接下来我们来通过几个场景学习下如何使用这些理论。

频繁变动的资源

对于频繁变动的资源,首先需要使用 Cache-Control: no-cache使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

代码文件

这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。

一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。

6. 浏览器渲染原理

JS 有一个 JS 引擎,那么执行渲染也有一个渲染引擎。同样,渲染引擎在不同的浏览器中也不是都相同的。比如在 Firefox 中叫做 Gecko,在 Chrome 和 Safari 中都是基于 WebKit 开发的。

浏览器接收到 HTML 文件并转换为 DOM 树

  1. 将字节数据转换为字符串
  2. 将字符串通过词法分析转换为token(标记)【过程:标记化】
  3. 将token 转换为 Node
  4. Node 通过Node 之间的联系转换为DOM 树

将 CSS 文件转换为 CSSOM 树

浏览器基础知识点
在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。

生成渲染树

浏览器基础知识点
在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上。

用我上一篇的博客来说就是合成位图,然后再将位图进行合成,合成之后再绘制到界面上

为什么操作 DOM 慢

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

什么情况阻塞渲染

首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。

或者用我自己的话来说,对于src 引入的文件会阻塞渲染,src 属于替换当前元素
当解析到属性时,会暂停其他资源的下载和处理,直到该资源加载,编译,执行完毕,类似于将所指向的文件嵌入当前标签内(img也是如此)

在不考虑缓存和优化网络协议的前提下,考虑可以通过哪些方式来最快的渲染页面,也就是常说的关键渲染路径,这部分也是性能优化中的一块内容。

提示如何加速:

  1. 从文件大小考虑
  2. 从 script 标签使用上来考虑
  3. 从 CSS、HTML 的代码书写上来考虑
  4. 从需要下载的内容是否需要在首屏使用上来考虑

xss

防御措施

转义字符

对于用户的输入应该是永远不信任的。最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

CSP

CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

通常可以通过两种方式来开启 CSP:

  • 设置 HTTP Header 中的 Content-Security-Policy
  • 设置 meta 标签的方式

这里以设置 HTTP Header 来举例

  • 只允许加载本站资源
Content-Security-Policy: default-src ‘self’
  • 只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*
  • 允许加载任何来源框架
Content-Security-Policy: child-src 'none'

CSRF

CSRF 中文名为跨站请求伪造。原理就是攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。如果用户是在登录状态下的话,后端就以为是用户在操作,从而进行相应的逻辑。

举个例子,假设网站中有一个通过 GET 请求提交用户评论的接口,那么攻击者就可以在钓鱼网站中加入一个图片,图片的地址就是评论接口

<img src="http://www.domain.com/xxx?comment='attack'"/>

那么你是否会想到使用 POST 方式提交请求是不是就没有这个问题了呢?其实并不是,使用这种方式也不是百分百安全的,攻击者同样可以诱导用户进入某个页面,在页面中通过表单提交 POST 请求。

如何防御

防范 CSRF 攻击可以遵循以下几种规则:

  1. Get 请求不对数据进行修改
  2. 不让第三方网站访问到用户 Cookie
  3. 阻止第三方网站请求接口
  4. 请求时附带验证信息,比如验证码或者 Token

SameSite

可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览器都兼容。

验证 Referer

对于需要防范 CSRF 的请求,我们可以通过验证 Referer 来判断该请求是否为第三方网站发起的。

Token

服务器下发一个随机 Token,每次发起请求时将 Token 携带上,服务器验证 Token 是否有效。

什么是点击劫持?如何防范点击劫持?

点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。
浏览器基础知识点

X-FRAME-OPTIONS

X-FRAME-OPTIONS 是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。

该响应头有三个值可选,分别是

  • DENY,表示页面不允许通过 iframe 的方式展示
  • SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
  • ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示

JS 防御

对于某些远古浏览器来说,并不能支持上面的这种方式,那我们只有通过 JS 的方式来防御点击劫持了。

<head>
  <style id="click-jack">
    html {
      display: none !important;
    }
  </style>
</head>
<body>
  <script>
    if (self == top) {
      var style = document.getElementById('click-jack')
      document.body.removeChild(style)
    } else {
      top.location = self.location
    }
  </script>
</body>

以上代码的作用就是当通过 iframe 的方式加载页面时,攻击者的网页直接不显示所有内容了。

什么是中间人攻击?如何防范中间人攻击?

中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了。攻击者不仅能获得双方的通信信息,还能修改通信信息。

通常来说不建议使用公共的 Wi-Fi,因为很可能就会发生中间人攻击的情况。如果你在通信的过程中涉及到了某些敏感信息,就完全暴露给攻击方了。

当然防御中间人攻击其实并不难,只需要增加一个安全通道来传输信息。HTTPS 就可以用来防御中间人攻击,但是并不是说使用了 HTTPS 就可以高枕无忧了,因为如果你没有完全关闭 HTTP 访问的话,攻击方可以通过某些方式将 HTTPS 降级为 HTTP 从而实现中间人攻击。