一、事件机制:捕获与冒泡的双面世界
浏览器中的事件机制,也称为事件流,是前端开发中不可或缺的一部分。它主要分为三个阶段:捕获阶段、目标阶段和冒泡阶段。
- 捕获阶段:事件从 window 开始,逐层向下传播至目标元素。在这个过程中,事件会经过各个祖先元素,直到达到最终的目标元素。
- 目标阶段:当事件传播到目标元素时,会触发与该元素绑定的事件处理函数。这是事件处理的核心阶段,开发者可以在此处定义具体的交互逻辑。
- 冒泡阶段:事件从目标元素开始,逐层向上传播回 window。在默认情况下,JavaScript 的事件处理函数会在冒泡阶段执行。
JS的事件默认都是在冒泡阶段执行的。当我点击蓝色盒子时,打印顺序是蓝、绿、红。
javascript
box1.addEventListener('click',function(){
console.log('box1 red');
},)
box2.addEventListener('click',function(){
console.log('box2 green');
})
box3.addEventListener('click',function(){
console.log('box3 blue');
})
事件的第三个参数为 true 时,在捕获阶段执行,默认为 false(冒泡阶段)
javascript
box1.addEventListener('click', function() {
console.log('box1 red');
}, true);
讲到这,就得来聊一聊事件委托了。
事件委托:性能优化的利器
事件委托是一种通过将事件绑定到父容器上,来代替为多个子元素单独绑定事件的技术。它不仅可以减少内存开销,还能提高事件处理的效率。
xml
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
// let lis = document.querySelectorAll('li') //代码写成这样浪费内存
// lis.forEach((item, index) => {
// item.addEventListener('click', function() {
// console.log(this.innerText);
// })
// })
let ul = document.querySelector('ul')
ul.addEventListener('click', function(e) { //事件委托
console.log(e.target.innerText);
})
</script>
如果直接为每个<li>
元素单独绑定事件监听器(如注释部分的代码),会浪费内存,尤其是当列表项很多时。而事件委托只需要为父元素(<ul>
)绑定一个事件监听器,即可处理所有子元素的点击事件。
二、跨域
什么是跨域?
跨域问题 源于浏览器的同源策略。同源策略是浏览器为了保护用户数据安全和服务器安全而实施的一种安全机制,旨在防止恶意网站通过脚本访问其他网站的数据。
同源策略
以http://www.abc.com:8000/a.html
为例,协议 (http://
),子域名 (www
),主域名 (abc.com
),端口号 (:8000
),路径 (/a.html
)
同源策略的核心是基于协议、域名、端口号的匹配。只有当这三个部分完全一致时,才被认为是同源的。
当客户端(如浏览器)向 不同源 (协议、域名、端口任一不同)的服务器发起请求时,浏览器会默认拦截响应(跨域是浏览器不接受后端的响应数据),以防止跨站脚本攻击(XSS)和跨站请求伪造(CSRF)等安全问题。
解决跨域问题的方法
1. JSONP
原理:
- 利用
<script>
标签的src
属性不受同源策略限制的特性。 - 前端向后端发送一个GET请求,并携带一个
callback
参数。 - 后端将数据作为
callback
函数的参数,返回一个函数调用形式。 - 浏览器接收到响应后,自动执行全局的
callback
函数。
前端代码
xml
<script>
function jsonp(url, cb) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
window[cb] = function (data) {
// console.log(data) // 后端返回的数据
resolve(data)
}
script.src = `${url}?cb=${cb}`
document.body.appendChild(script)
// callback('hello world')
})
}
function handle() {
jsonp('http://localhost:3000', 'callback').then(res => {
console.log(res)
})
}
</script>
后端代码
javascript
const http = require('http');
http.createServer((req, res) => {
const query = new URL(req.url, `http://${req.headers.host}`).searchParams
// console.log(query.get('cb'));
if (query.get('cb')) {
const cb = query.get('cb') // 'callback'
const data = 'hello world'
const result = `${cb}("${data}")` // "callback('hello world')"
res.end(result)
}
// res.end('hello world')
}).listen(3000);
缺点:
- 只能发送GET请求 。浏览器通过
<script>
标签隐式发送 GET 请求 ,而<script>
标签仅支持 GET 方式的资源加载 - 不安全,容易受到XSS攻击(脚本攻击)。
- 前后端必须配合。
2. CORS (跨域资源共享)
原理:
通过服务器声明允许跨域访问的资源范围,浏览器根据响应头中的指令决定是否允许跨域请求。 由后端在响应头中设置Access-Control-Allow-Origin
等字段,指定允许跨域访问的域名。
dart
const Koa = require('koa');
const app = new Koa();
app.use(async(ctx, next) => { // 处理跨域
ctx.set('Access-Control-Allow-Origin', 'http://localhost:5173'); // 允许访问我这个后端的源
ctx.set('Access-Control-Allow-Methods', 'OPTIONS, GET, PUT, POST, DELETE'); // 允许的请求方法
ctx.set('Access-Control-Allow-Headers', 'x-requested-with, accept, origin, content-type,authorization'); // 允许的请求头
ctx.set('Access-Control-Allow-Credentials', 'true'); // 允许携带凭证(确保Authorization 请求头里的token不跨域)
// 请求预检(http协议规定每一个 http 请求发送前都会自动发送一个预检请求)
if (ctx.method === 'OPTIONS') {
ctx.status = 204
return;
}
await next();
})
凭证(Credentials)处理:
若需携带 Cookie(如用户登录态),需同时设置:
yaml
Access-Control-Allow-Origin: https://your-app.com // 不能为 *
Access-Control-Allow-Credentials: true
前端请求需添加 withCredentials: true
3. Nginx 反向代理
什么是反向代理?
- 角色:Nginx 作为中间代理服务器,接收客户端的请求后,将其转发到内部的后端服务器(如 Node.js、Python、MySQL 等)。
- 客户端感知:客户端认为请求直接到达目标服务,但实际流量经过 Nginx 中转。
前端服务器和后端服务器不在同一个域名下,前端服务器通过nginx 反向代理来访问后端服务器
4. Node.js 中间件代理
Node.js 中间件代理通过动态路由和请求改造,为前端与后端跨域通信提供轻量级解决方案,尤其适用于需要业务逻辑介入的 API 网关场景。
Nginx 反向代理和Node.js 中间件代理原理是一样的,只不过用两种不同的语言来实现的。Nginx是基于配置文件的,使用C语言编写,性能高,适合处理高并发和静态资源。而Node.js中间件代理则是基于JavaScript/TypeScript,运行在V8引擎上,更灵活,适合需要动态处理请求的场景,比如添加鉴权、修改请求头等。
5. Websocket
原理:
- WebSocket是基于TCP协议的双向通信协议。
- 一旦建立连接,客户端和服务器可以随时发送和接收数据。
- WebSocket天生支持跨域。
WebSocket 是基于 TCP 的全双工通信协议,通过一次握手建立持久连接,天然支持跨域通信 (需服务器配置 Sec-WebSocket-Origin
)。其核心优势在于低延迟和高频数据传输,适用于实时性要求高的场景,但需权衡资源消耗与业务需求。与 HTTP CORS 相比,WebSocket 的跨域机制更直接,且无需预检请求,是现代化实时应用的标配协议。
6. postMessage
核心场景
当父页面与 <iframe>
子页面处于不同域名时,传统 DOM 操作(如 iframe.contentWindow
)因浏览器同源策略会被拦截,postMessage
是 HTML5 提供的跨源通信解决方案。
原理与实现
postMessage
是 HTML5 引入的 跨源通信标准 ,通过消息传递机制安全实现父页面与 <iframe>
、不同窗口间的数据交互。其核心优势在于 灵活、安全、低延迟,是解决嵌套页面跨域问题的最优方案。开发者需注意数据序列化、源验证和性能优化,以确保通信的可靠性与安全性。
-
通信机制
- 发送方 (如父页面)通过
window.postMessage()
发送消息到目标窗口。 - 接收方 (如 iframe 页面)通过监听
message
事件接收消息。 - 消息包含 数据 、目标源 (
targetOrigin
)和 端口(可选)。
- 发送方 (如父页面)通过
-
安全策略
- 源验证 :接收方通过
event.origin
验证消息来源是否合法,防止跨站攻击。 - 同源限制:仅允许与发送方源(协议、域名、端口一致)匹配的窗口接收消息。
- 源验证 :接收方通过
父页面(发送方):
xml
<!-- 在父页面中向 iframe 发送消息 -->
<iframe id="myIframe" src="https://detail-domain.com/detail.html"></iframe>
<script>
const iframe = document.getElementById('myIframe');
// 向 iframe 发送消息(携带数据、目标源)
iframe.contentWindow.postMessage('Hello from parent!', 'https://detail-domain.com');
</script>
iframe 页面(接收方):
xml
<script>
// 监听 message 事件
window.addEventListener('message', (event) => {
// 验证消息来源是否合法
if (event.origin !== 'https://parent-domain.com') {
return;
}
// 处理接收到的数据
console.log('收到父页面消息:', event.data);
});
</script>
7. document.domain
document.domain
是一种早期用于解决浏览器同源策略下 跨子域名通信 的方法,但因安全风险被弃用,现代开发应优先选择 postMessage
。由于安全性问题,谷歌已经禁用 document.domain
。