1. 事件机制

       面试题:事件的触发过程是怎么样的?什么是事件代理?

  • 事件触发
    事件触发的三个阶段:
  1. 从 window 往事件触发处传播,遇到注册的捕获事件会触发
  2. 传播到事件触发处时触发注册的时间
  3. 从事件触发处往 window 传播,遇到注册的冒泡事件会触发

       注:如果给 body 中的子节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。
       通常使用 addEventListener 注册事件,第三个参数是布尔值或者对象,布尔值默认为 false 表示捕获,对象可以使用{captrue:布尔值(true 冒泡,false 捕获), once:布尔值(true 表示回调只会调用一次,调用后移除), passive:布尔值(true 表示永远不会调用 preventDefault)}
       stopPropagation 可以阻止事件的冒泡和捕获,stopImmediatePropagation 也可以阻止事件,还能阻止该事件目标执行别的注册事件。

1
2
3
4
5
6
7
8
node.addEventListener('click', event => {
event.stopImmediatePropagation();
console.log('冒泡');
}, false);
// node被点击后,以下的事件不会执行了
node.addEventListener('click', event => {
console.log('捕获');
}, true);
  • 事件代理(事件委托)
           只指定一个事件处理程序,就可以管理某一类型的所有事件。原理是利用事件的冒泡机制,从最深的节点开始逐渐向上传播事件,冒泡到指定的父节点代为执行事件。
           例如, ul 上面的所有 li 都需要相同的 click 事件,没有用事件代理就会用 for 循环的方式,遍历所有的 li 来添加事件,这样的问题是添加到页面上的事件处理函数数量与页面整体运行性能挂钩,因为需要不断地与 dom 进行交互,引起的浏览器重绘和重排就越多,会延长整个页面的交互就绪时间,再者每个函数都是一个对象,对象就会占用内存,内存占用率越大性能就会越差。使用事件委托与 dom 操作就只需要操作一次,可以提高性能和节约内存空间。

2. 跨域

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

       浏览器处于安全考虑有同源策略。协议、域名、端口有一个不同就是跨域,请求会失败。主要是用来防止 CSRF(Cross-site request forgery)攻击,简单来说是利用用户的登录状态发起恶意请求。
       session 的理解:例如有一张会员卡,可以享受一些会员权利,会员卡是客户的唯一标识,会员卡卡号就是保存在 cookie 的 sessionId,会员卡权利和个人信息就是服务端的 session 对象。http 请求是无状态的,但是每次 http 请求都会将本域名下的所有 cookie 作为 http 请求头的一部分发送给服务端,所以服务端就根据请求中的 cookie 中的 sessionId 去 session 对象中查找用户信息了。
       CSRF 攻击的主要目的是让用户在不知情的情况下攻击自己已登录的一个系统,例如在网站中点击一个图片就会构造一个去论坛发帖的请求,去你的论坛发帖,由于你的浏览器状态是登录的,所以 session 登录的 cookie 信息都会和正常的请求一样。
       防御可以通过 referer、token 或者验证码检测用户提交,不要在链接中暴露用户信息,使用 post 操作,严格设置 cookie 的域。
       请求跨域后,请求发出去了,但是浏览器拦截了响应。通过表单可以发起跨域请求,因为表单不会获取新的内容,但是 ajax 可以获取响应,所以 ajax 不可以发起跨域请求。所以说明跨域限制并不能完全阻止 CSRF,因为请求已经发出去了。
       如何解决跨域的问题?

同源策略限制下接口请求的方式

  1. JSONP
           在 HTML 标签中,script、img 这样获取资源的标签是没有跨域限制的,所以利用这一点,我们可以通过 script 标签指向一个需要访问的地址并提供一个回调函数来接收数据,不过 script 加载资源是 GET 请求,如果要使用 POST 请求,可以用空 iframe+form。
1
2
3
4
5
6
7
//callback是前后端约定的方法名,后端返回一个直接执行的方法给前端,由于是用script标签发起的请求,所以返回方法后立即执行,并且把要返回的数据放在方法的参数里
<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>

       做一个简单的封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* JSONP请求工具
* @param url 请求的地址
* @param data 请求的参数
* @returns {Promise<any>}
*/
const request = ({url, data}) => {
return new Promise((resolve, reject) => {
// 处理传参成xx=yy&aa=bb的形式
const handleData = (data) => {
const keys = Object.keys(data)
const keysLen = keys.length
return keys.reduce((pre, cur, index) => {
const value = data[cur]
const flag = index !== keysLen - 1 ? '&' : ''
return `${pre}${cur}=${value}${flag}`
}, '')
}
// 动态创建script标签
const script = document.createElement('script')
// 接口返回的数据获取
window.jsonpCb = (res) => {
document.body.removeChild(script)
delete window.jsonpCb
resolve(res)
}
script.src = `${url}?${handleData(data)}&cb=jsonpCb`
document.body.appendChild(script)
})
}
// 使用方式
request({
url: 'http://localhost:9871/api/jsonp',
data: {
// 传参
msg: 'helloJsonp'
}
}).then(res => {
console.log(res)
})
  1. CORS
           CORS 是一个 W3C 标准,全称是“跨域资源共享”(cross-origin resource sharing)。只要服务器实现了 CORS 接口,就可以跨域通信,都是浏览器自动完成。
           浏览器将 CORS 请求分为两类:简单请求和非简单请求。
           同时满足以下两大条件,就属于简单请求:
  • 请求方法是以下三种方法之一: HEAD GET POST
  • http 的头信息不超出以下几种字段:Accept Accept-Language Content-Language Last-Event-ID Content-Type(只限于 application/x-www-form-urlencoded、mutipart/form-data、text/plain)
           不同时满足以上两个条件就属于非简单请求。

简单请求

       浏览器发现跨域的 AJAX 请求是简单请求,就自动在头信息之中加一个 Origin 字段,用来说明请求源(协议+域名+端口),服务器根据这个值决定是否同意这次请求。
       如果 Origin 指定的源不在许可范围内,服务器会返回一个正常的 HTTP 回应,浏览器发现回应的头没有包含 Access-Control-Allow-Origin 字段就知道出错了,从而抛出错误。

非简单请求

       非简单请求是对服务器有特殊要求的请求,比如请求方法是 PUT/DELETE,或者 Content-Type 字段类型是 application/json。
       非简单请求的 CORS 请求,会在正式通信前增加一次 HTTP 查询请求,称为“预检”请求。浏览器先询问服务器,当前域名是否在服务器的许可名单中,以及可以使用哪些 HTTP 动词和头信息字段,只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则报错。
       一旦服务器通过了”预检”请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。

  1. 代理
           使用 Nginx 配置来把请求转发到真正的后端域名上。
1
2
3
4
5
6
7
8
9
10
server{
# 监听9099端口
listen 9099;
# 域名是localhost
server_name localhost;
#凡是localhost:9099/api,都转发到真正的服务端地址http://localhost:9871
location ^~ /api {
proxy_pass http://localhost:9871;
}
}

       如果后端接口是公共 API,调用时就不能去配 Nginx,CORS 才是通用的做法。

同源策略限制下 Dom 查询的方式

  1. postMessage()
           它是 H5 的一个接口,用来实现不同窗口不同页面的跨域通讯。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 发送方
window.addEventListener('message', (e) => {
// 这里一定要对来源做校验
if (e.origin === 'http://crossdomain.com:9099') {
// 来自http://crossdomain.com:9099的结果回复
console.log(e.data)
}
})
// 向http://crossdomain.com:9099发消息
postMessage () {
const iframe = window.frames['crossDomainIframe']
iframe.postMessage('我是[http://localhost:9099], 麻烦你查一下你那边有没有id为app的Dom', 'http://crossdomain.com:9099')
}

//接收方
window.addEventListener('message', (e) => {
// 这里一定要对来源做校验
if (e.origin === 'http://localhost:9099') {
// http://localhost:9099发来的信息
console.log(e.data)
// e.source可以是回信的对象,其实就是http://localhost:9099窗口对象(window)的引用
// e.origin可以作为targetOrigin
e.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,这就是你想知道的结果:${document.getElementById('app') ? '有id为app的Dom' : '没有id为app的Dom'}`, e.origin);
}
})
  1. document.domain
           这种方式只适合主域名相同,但子域名不同的 iframe 跨域。
           比如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,这种情况下给两个页面指定一下document.domain即document.domain = crossdomain.com 就可以访问各自的 window 对象了。