# 浏览器基础知识点

# 事件机制

事件触发有三个阶段:

  • window 往事件触发处传播,遇到注册的捕获事件会触发
  • 传播到事件触发处时触发注册的事件
  • 从事件触发处往 window 传播,遇到注册的冒泡事件会触发

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

// 以下会先打印冒泡然后是捕获
node.addEventListener(
  'click',
  event => {
    console.log('冒泡')
  },
  false
)
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 注册事件

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

  • capture:布尔值,和 useCapture 作用一样
  • once:布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
  • passive:布尔值,表示永远不会调用 preventDefault

一般来说,如果我们只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。

node.addEventListener(
  'click',
  event => {
    event.stopImmediatePropagation()
    console.log('冒泡')
  },
  false
)
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 事件代理

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

<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>
1
2
3
4
5
6
7
8
9
10
11
12
13

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

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

# 跨域

浏览器出于安全考虑,存在同源策略,也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。

那么是出于什么安全考虑才会引入这种机制呢? 其实主要是用来防止 CSRF 攻击的。简单点说,CSRF 攻击是利用用户的登录态发起恶意请求。

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

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

# 什么是跨域

由于浏览器的同源策略,所谓同源策略,指的是浏览器对不同源的脚本或者文本的访问方式进行的限制。比如源 a 的 js 不能读取或设置引入的源 b 的元素属性。

产生跨域原因:

  • 浏览器限制
  • 请求是跨域的
  • 请求是XHR(XMLHttpRequest)请求

# 什么是同源?

所谓同源,就是指两个页面具有相同的协议,主机(也常说域名),端口,三个要素缺一不可。例如:

URL1 URL2 说明 是否允许通信
http://www.foo.com/js/a.js http://www.foo.com/js/b.js 协议、域名、端口都相同 允许
http://www.foo.com/js/a.js http://www.foo.com:8888/js/b.js 协议、域名相同,端口不同 不允许
https://www.foo.com/js/a.js http://www.foo.com/js/b.js 主机、域名相同,协议不同 不允许
http://www.foo.com/js/a.js http://www.bar.com/js/b.js 协议、端口相同,域名不同 不允许
http://www.foo.com/js/a.js http://foo.com/js/b.js 协议、端口相同,主域名相同,子域名不同 不允许

同源策略限制了不同源之间的交互,但是有人也许会有疑问,我们以前在写代码的时候也常常会引用其他域名的 js 文件,样式文件,图片文件什么的,没看到限制啊,这个定义是不是错了。其实不然,同源策略限制的不同源之间的交互主要针对的是 js 中的 XMLHttpRequest 等请求,下面这些情况是完全不受同源策略限制的:

  • 页面中的链接,重定向以及表单提交是不会受到同源策略限制的
  • 跨域资源嵌入是允许的,例如 <img src=XXX>,当然,浏览器限制了 Javascript 不能读写加载的内容。

同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

接下来我们将来学习几种常见的方式来解决跨域的问题。

# 1.CORS

跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器「不同的域、协议或端口」请求一个资源时,资源会发起一个「跨域 HTTP 请求」。

而在 cors 中会有简单请求复杂请求的概念。

简单请求是满足以下所有条件的请求:

  • 只允许 GET、POST 和 HEAD 方法
  • 不能自定义请求头,除了代理自动设置的请求头外(Connection,User-Agent等),只允许 CORS 安全列出的请求头,它们是:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • Last-Event-ID
    • 其它名称是不区分字节大小写的匹配(DPR、Downdlink、Save-Data、Viewport-Width、Width等,没试过~)
  • Content-Type:只限于三个值
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • 于XMLHttpRequestUpload:
    • 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器
    • XMLHttpRequestUpload 对象可以使用XMLHttpRequest.upload 属性访问
  • 请求中没有使用 ReadableStream 对象

除了简单请求情况之外的就是复杂请求。

CORS 预检请求首先通过 OPTIONS 方法向另一个域上的资源发送 HTTP 请求,用来确定实际请求是否跨域安全的发送。预检请求通过后才会发送真实请求。

Node 中的解决方案

原生:

app.use(async (ctx, next) => {
  ctx.set("Access-Control-Allow-Origin", ctx.headers.origin);
  ctx.set("Access-Control-Allow-Credentials", true);
  ctx.set("Access-Control-Request-Method", "PUT,POST,GET,DELETE,OPTIONS");
  ctx.set(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept, cc"
  );
  if (ctx.method === "OPTIONS") {
    ctx.status = 204;
    return;
  }
  await next();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

中间件:

// koa 用 koa-cors
const cors = require('cors') // 快速处理跨域
app.use(cors())
1
2
3

关于 cors 的 cookie 问题

想要传递 cookie 需要满足 3 个条件:

  1. web 请求设置 withCredentials

这里默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials 来进行传递 cookie.

// 原生 xml 的设置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 设置方式
axios.defaults.withCredentials = true;
1
2
3
4
5
  1. Access-Control-Allow-Credentials 为 true

  2. Access-Control-Allow-Origin 为非 *

这里请求的方式,在 chrome 中是能看到返回值的,但是只要不满足以上其一,浏览器会报错,获取不到返回值。

# 2.Node 正向代理

代理的思路为,利用服务端请求不会跨域的特性,让接口和当前站点同域。

# Webpack (4.x)
devServer: {
  port: 8080,
  proxy: {
    "/api": {
      target: "http://localhost:8888"
    }
  }
},
1
2
3
4
5
6
7
8
# 利用 node 作为中间件代理(两次跨域)

实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。

代理服务器,需要做以下几个步骤:

  • 接受客户端请求
  • 将请求转发给服务器。
  • 拿到服务器响应数据。
  • 将响应转发给客户端。
var app = express()

var apiRoutes = express.Router()

apiRoutes.get('/getDiscList', function (req, res) {
  var url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg'
  axios.get(url, {
    headers: {
      referer: 'https://c.y.qq.com/',
      host: 'c.y.qq.com'
    },
    params: req.query
  }).then((response) => {
    res.json(response.data)
  }).catch((e) => {
    console.log(e)
  })
})

apiRoutes.get('/lyric', function (req, res) {
  var url = 'https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg'

  axios.get(url, {
    headers: {
      referer: 'https://c.y.qq.com/',
      host: 'c.y.qq.com'
    },
    params: req.query
  }).then((response) => {
    var ret = response.data
    if (typeof ret === 'string') {
      var reg = /^\w+\(({[^()]+})\)$/
      var matches = ret.match(reg)
      if (matches) {
        ret = JSON.parse(matches[1])
      }
    }
    res.json(ret)
  }).catch((e) => {
    console.log(e)
  })
})

app.use('/api', apiRoutes)
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
41
42
43
44

这里用 node 重写了2个接口,利用 node 去请求真实的服务器 https://c.y.qq.com,带上需要的 headers 请求头。

再看一个完整的简单案例:

本地文件 index.html 文件,通过代理服务器 http://localhost:3000 向目标服务器 http://localhost:4000 请求数据

/ index.html(http://127.0.0.1:5500)
 <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
	<script>
      $.ajax({
        url: 'http://localhost:3000',
        type: 'post',
        data: { name: 'xiamen', password: '123456' },
        contentType: 'application/json;charset=utf-8',
        success: function(result) {
          console.log(result) // {"title":"fontend","password":"123456"}
        },
        error: function(msg) {
          console.log(msg)
        }
      })
	</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
  // 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段
  response.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': '*',
    'Access-Control-Allow-Headers': 'Content-Type'
  })
  // 第二步:将请求转发给服务器
  const proxyRequest = http
    .request(
      {
        host: '127.0.0.1',
        port: 4000,
        url: '/',
        method: request.method,
        headers: request.headers
      },
      serverResponse => {
        // 第三步:收到服务器的响应
        var body = ''
        serverResponse.on('data', chunk => {
          body += chunk
        })
        serverResponse.on('end', () => {
          console.log('The data is ' + body)
          // 第四步:将响应结果转发给浏览器
          response.end(body)
        })
      }
    )
    .end()
})
server.listen(3000, () => {
  console.log('The proxyServer is running at http://localhost:3000')
})
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
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
  if (request.url === '/') {
    response.end(JSON.stringify(data))
  }
})
server.listen(4000, () => {
  console.log('The server is running at http://localhost:4000')
})
1
2
3
4
5
6
7
8
9
10
11

上述代码经过两次跨域,值得注意的是浏览器向代理服务器发送请求,也遵循同源策略,最后在 index.html 文件打印出 {"title":"fontend","password":"123456"}

# 3.Nginx 反向代理

实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。

使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过 nginx 配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登录。

将 nginx 目录下的 nginx.conf 修改如下:

// proxy服务器
server {
    listen       80;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

nginx 使用配置大全请看nginx 最全操作总结 (opens new window)

# 4.JSONP

利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。 JSONP 请求一定需要对方的服务器做支持才可以。

JSONP 优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持 get 方法具有局限性,不安全可能会遭受 XSS 攻击。

jsonp 实现:

function jsonp ({url, params, callback}) {
  return new Promise((resolve, reject) => {
	// 创建 script 标签
	let script = document.createElement('script')
	// 将函数挂在 window 上
	window[callback] = function (data) {
	  resolve(data)
	  // 代码执行后,删除 script 标签
	  document.body.removeChild(script)
	}
	// 回调函数加在请求地址上
	params = {...params, callback} // wb=b&callback=show
	let arrs = []
	for (let key in params) {
	  array.push(`${key}=${params[key]}`)
	}
	script.src = `${url}?${arrs.join('&')}`
	document.body.appendChild(script)
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

使用

function show(data) {
  console.log(data)
}
jsonp({
  url: 'http://localhost:3000/say',
  params:{
	wd: 'I love you'
  },
  callback: 'show'
}).then(data => {
  console.log(data)
})
1
2
3
4
5
6
7
8
9
10
11
12

上面这段代码相当于向 http://localhost:3000/say?wd=I love you&callback=show 这个地址请求数据,然后后台返回 show('I love you too'),最后会运行 show() 这个函数,打印出'I love you too'

// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
  let { wd, callback } = req.query
  console.log(wd) // I love you
  console.log(callback) // show
  res.end(`${callback}('I love you too')`)
})
app.listen(3000)
1
2
3
4
5
6
7
8
9
10

# 5.Websocket

Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket 和 HTTP 都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了,因此也没有跨域的限制。

// socket.html
<script>
  let socket = new WebSocket('ws://localhost:3000');
  socket.onopen = function () {
    socket.send('我爱你');// 向服务器发送数据
  }
  socket.onmessage = function (e) {
    console.log(e.data);// 接收服务器返回的数据
  }
</script>
1
2
3
4
5
6
7
8
9
10
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');// 记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
  ws.on('message', function (data) {
    console.log(data);
    ws.send('我也爱你')
  });
})
1
2
3
4
5
6
7
8
9
10
11

# 6.window.postMessage

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

postMessage() 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

otherWindow.postMessage(message, targetOrigin, [transfer]);
1
  • message: 将要发送到其他 window的数据。
  • targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
  • transfer(可选):是一串和 message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

接下来我们看个例子: http://localhost:3000/a.html 页面向 http://localhost:4000/b.html 传递“我爱你”,然后后者传回"我不爱你"。

// a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
//内嵌在http://localhost:3000/a.html
  <script>
    function load() {
      let frame = document.getElementById('frame')
      frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
      window.onmessage = function(e) { //接受返回数据
        console.log(e.data) //我不爱你
      }
    }
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
// b.html
window.onmessage = function(e) {
  console.log(e.data) //我爱你
  e.source.postMessage('我不爱你', e.origin)
}
1
2
3
4
5

# 7.window.name + iframe

window.name 属性的独特之处:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

其中 a.html 和 b.html 是同域的,都是 http://localhost:3000;而 c.html 是 http://localhost:4000

// a.html(http://localhost:3000/b.html)
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
  let first = true
  // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
  function load() {
    if(first){
    // 第1次onload(跨域页)成功后,切换到同域代理页面
      let iframe = document.getElementById('iframe');
      iframe.src = 'http://localhost:3000/b.html';
      first = false;
    }else{
    // 第2次onload(同域b.html页)成功后,读取同域window.name中数据
      console.log(iframe.contentWindow.name);
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// c.html(http://localhost:4000/c.html)
<script>
  window.name = '我不爱你'  
</script>
1
2
3
4

总结:通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

# 8.location.hash + iframe

实现原理: a.html 欲与 c.html 跨域相互通信,通过中间页 b.html 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。

具体实现步骤:一开始 a.html 给 c.html 传一个 hash 值,然后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最后 b.html 将结果放到 a.html 的 hash 值中。 同样的,a.html 和 b.html 是同域的,都是 http://localhost:3000;而 c.html 是 http://localhost:4000

 // a.html
<iframe src="http://localhost:4000/c.html#iloveyou"></iframe>
<script>
  window.onhashchange = function () { //检测hash的变化
    console.log(location.hash);
  }
</script>
1
2
3
4
5
6
7
 // b.html
<script>
  window.parent.parent.location.hash = location.hash 
  //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
</script>
1
2
3
4
5
// c.html
<script>
  console.log(location.hash);
  let iframe = document.createElement('iframe');
  iframe.src = 'http://localhost:3000/b.html#idontloveyou';
  document.body.appendChild(iframe);
</script>
1
2
3
4
5
6
7

# 9.document.domain + iframe

该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。 只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

我们看个例子:页面 a.zf1.cn:3000/a.html 获取页面 b.zf1.cn:3000/b.html 中 a 的值

// a.html
<body>
  helloa
  <iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
  <script>
    document.domain = 'zf1.cn'
    function load() {
      console.log(frame.contentWindow.a);
    }
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
// b.html
<body>
   hellob
   <script>
     document.domain = 'zf1.cn'
     var a = 100;
   </script>
</body>
1
2
3
4
5
6
7
8

# 总结

  • CORS 支持所有类型的 HTTP 请求,是跨域 HTTP 请求的根本解决方案

  • JSONP 只支持 GET 请求,JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。

  • 不管是Node中间件代理还是 nginx 反向代理,主要是通过同源策略对服务器不加限制。

  • 日常工作中,用得比较多的跨域方案是 cors 和 nginx 反向代理

参考链接:

九种跨域方式实现原理(完整版) (opens new window)

# 存储

cookie 已经不建议用于存储。因为:每次都会携带在 header 中,对于请求性能影响

如果没有大量数据存储需求的话,可以使用 localStorage 和 sessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

对于 cookie 来说,我们还需要注意安全性:

  • value 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识
  • http-only 不能通过 JS 访问 Cookie,减少 XSS 攻击
  • secure 只能在协议为 HTTPS 的请求中携带
  • same-site 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击

# Service Worker

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

Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:

// 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')
    })
  )
})
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

# keep-alive 的原理

Keep-Alive是一种HTTP协议的特性,它允许在单个TCP连接上进行多个HTTP请求和响应。它的原理是在HTTP响应头中添加Connection: keep-alive字段,表示服务器希望保持与客户端的TCP连接持久化。

当客户端发送一个带有Connection: keep-alive字段的请求时,服务器在响应中也会添加Connection: keep-alive字段,表示服务器同意保持连接。这样,客户端和服务器之间的TCP连接将保持打开状态,可以在同一个连接上发送多个请求和响应,而无需每次都建立和关闭连接,从而减少了连接建立和关闭的开销。

如果超出缓存长度,即在一个TCP连接上发送的请求超过了服务器的缓存或处理能力,服务器可以采取以下几种处理方式:

  • 关闭连接:服务器可以在处理完当前请求后关闭连接,通知客户端不再保持连接。客户端在下次请求时会重新建立连接。
  • 拒绝请求:服务器可以返回一个错误响应(如503 Service Unavailable),告知客户端当前无法处理请求,请稍后重试。
  • 队列等待:服务器可以将超出缓存长度的请求放入一个请求队列中,按照先进先出的顺序处理请求。当服务器有空闲资源时,再逐个处理队列中的请求。
  • 并发处理:服务器可以采用多线程、多进程或异步处理的方式,以提高并发处理能力,从而能够处理更多的请求。

具体采取哪种处理方式取决于服务器的配置和策略。在高负载情况下,服务器可能会选择关闭连接或拒绝请求来保护自身免受过载。而在负载较低的情况下,服务器可以选择队列等待或并发处理来提高响应能力。

需要注意的是,即使使用了Keep-Alive,服务器也会有一定的连接超时时间。如果在一段时间内没有新的请求到达,服务器可能会主动关闭连接,以释放资源。这个超时时间可以通过服务器的配置进行调整。同时,客户端和服务器也可以通过Connection: close字段来显式地关闭连接

# websocket 的优缺点和用处

WebSocket是一种基于TCP的协议,用于在客户端和服务器之间进行全双工通信。相比传统的HTTP请求-响应模式,WebSocket具有以下优点:

优点:

  1. 实时性:WebSocket提供了实时的双向通信,服务器可以主动推送数据给客户端,而不需要客户端发起请求。这使得WebSocket非常适合实时应用程序,如聊天应用、实时协作工具和实时数据更新等。

  2. 减少延迟:由于WebSocket使用单个TCP连接进行通信,避免了HTTP的连接建立和关闭的开销,从而减少了延迟。相比频繁地发起HTTP请求,WebSocket可以更快地传输数据。

  3. 较少的数据传输量:WebSocket使用较少的数据传输量,因为它使用二进制协议头和压缩扩展,以及更紧凑的消息格式。这对于移动设备和带宽受限的网络环境非常有利。

  4. 更高的并发性:WebSocket允许服务器同时与多个客户端保持连接,而不需要为每个客户端创建单独的连接。这减轻了服务器的负担,提高了并发性能。

  5. 跨域支持:WebSocket支持跨域通信,可以在不同域名或端口之间建立连接,而不受同源策略的限制。这使得开发跨域实时应用变得更加容易。

WebSocket的缺点相对较少,但也有一些需要考虑的因素:

缺点:

  1. 较高的服务器资源消耗:相比传统的HTTP请求,WebSocket需要服务器保持长时间的连接状态,这可能会增加服务器的资源消耗。服务器需要管理和维护与客户端的连接,特别是在大规模并发连接的情况下。

  2. 兼容性问题:WebSocket是HTML5引入的新特性,因此在较旧的浏览器中可能不受支持。为了兼容旧版浏览器,开发人员可能需要使用轮询或其他技术作为回退方案。

WebSocket的主要用途是实现实时通信和推送功能,特别适用于需要实时更新数据的应用程序。一些常见的应用场景包括:

  • 即时通讯:WebSocket可用于构建实时聊天应用程序,支持双向通信和实时消息传递。

  • 实时协作:WebSocket可用于实现实时协作工具,如实时编辑、协同绘图和实时文档共享等。

  • 实时数据更新:WebSocket可以用于实时展示股票行情、实时监控数据、实时游戏状态等需要实时更新的数据。

  • 通知和推送服务:WebSocket可用于向客户端推送通知、提醒和实时更新的内容。

总之,WebSocket通过提供实时、双向的通信机制,降低延迟和数据传输量,以及支持跨域通信,为开发实时应用程序提供了更好的工具和性能。

# 轮询和 websocket 哪个更好,如何选择 ?

轮询:

  1. 工作原理:轮询是一种基于HTTP的技术,客户端定期发送HTTP请求来获取服务器上的更新数据。 优点:
  2. 兼容性好:轮询是基于HTTP的,几乎所有的浏览器都支持。
  3. 简单易实现:轮询的实现相对简单,不需要特殊的服务器或协议支持。 缺点: 延迟较高:由于需要定期发送请求,轮询可能会导致较高的延迟,因为服务器不能主动推送数据,而是等待客户端发起请求。 频繁的请求:轮询会导致频繁的HTTP请求,增加了网络流量和服务器负载。 实时性较差:由于需要等待下一次轮询周期,数据的实时性较差。 WebSocket:

工作原理:WebSocket提供了实时的双向通信,使用单个TCP连接,在客户端和服务器之间进行全双工通信。 优点: 实时性好:WebSocket支持服务器主动推送数据,实现实时通信,数据的实时性更高。 较低的延迟和数据传输量:由于使用单个TCP连接,避免了HTTP的连接建立和关闭开销,减少了延迟和数据传输量。 更高的并发性:WebSocket允许服务器同时与多个客户端保持连接,提高了并发性能。 缺点: 兼容性较差:WebSocket是HTML5引入的新特性,较旧的浏览器可能不支持。需要使用回退方案来兼容旧版浏览器。 服务器资源消耗较高:由于需要保持长时间的连接状态,服务器可能需要管理和维护大量的连接,增加了资源消耗。 选择策略:

实时性要求高且浏览器支持WebSocket:如果应用程序需要实时通信、实时数据更新等实时性要求较高的功能,并且目标浏览器支持WebSocket,那么选择WebSocket是更好的选择。

兼容性要求高或实时性要求不高:如果应用程序需要兼容较旧的浏览器或实时性要求不高,而且对延迟和网络流量的要求相对较低,那么可以选择使用轮询。

混合使用:在某些情况下,可以根据具体需求和浏览器支持情况,采用混合使用的方式。例如,对于支持WebSocket的现代浏览器,使用WebSocket进行实时通信;对于不支持WebSocket的旧版浏览器,使用轮询作为回退方案。

综上所述,选择轮询还是WebSocket取决于应用程序的需求和浏览器兼容性。如果实时性要求高且浏览器支持WebSocket,那么选择WebSocket更好;如果兼容性要求高或实时性要求不高,可以选择使用轮询。在一些情况下,也可以考虑混合使用两种技术。

# WebSocket 如何连接 ?服务端给客户端发送的 http 状态码是什么?

WebSocket连接的建立是通过HTTP协议的握手过程来实现的。下面是WebSocket连接的建立步骤:

  1. 客户端发送WebSocket握手请求:客户端发送一个HTTP GET请求,其中包含特定的WebSocket头部信息,以表明客户端希望建立WebSocket连接。请求中需要包含以下头部信息:
  • Upgrade: websocket:表明客户端希望升级到WebSocket协议。
  • Connection: Upgrade:表明客户端希望使用升级后的协议进行连接。
  • Sec-WebSocket-Key:一个随机生成的字符串,用于安全验证。
  1. 服务端发送WebSocket握手响应:服务端收到客户端的握手请求后,会发送一个HTTP响应作为握手响应。响应中包含以下头部信息:
  • Upgrade: websocket:表明服务端同意升级到WebSocket协议。
  • Connection: Upgrade:表明服务端同意使用升级后的协议进行连接。
  • Sec-WebSocket-Accept:服务端对客户端提供的Sec-WebSocket-Key进行处理后的结果,用于验证握手请求的合法性。
  1. WebSocket连接建立:一旦客户端收到服务端的握手响应,WebSocket连接就建立成功了。此时客户端和服务端可以通过该连接进行实时的双向通信。

关于服务端给客户端发送的HTTP状态码,WebSocket握手成功时,服务端会返回状态码101 Switching Protocols。这个状态码表示服务器已经理解并接受了客户端的请求,成功切换到WebSocket协议。

需要注意的是,WebSocket连接的建立是通过HTTP握手过程来升级到WebSocket协议,之后的通信不再遵循HTTP协议。WebSocket使用自己的协议格式进行数据传输,相比于HTTP协议,具有更低的延迟和更高的实时性。

WebSocket 在服务端是怎么处理消息的?

服务器处理WebSocket消息的一般步骤包括建立连接、接收和解析消息、处理消息、发送响应消息以及保持连接状态。具体的实现方式取决于所使用的编程语言和框架。

WebSocket消息通常以帧(frame)的形式发送,服务器需要根据WebSocket协议规范解析这些帧,并提取出有效的消息内容。