跨域
跨域本质是浏览器基于同源策略的一种安全手段
同源策略
浏览器的安全功能,协议、域名、端口一致则是同源,非同源的限制:
- 无法通过 XMLHttpRequest 或 fetch 发起跨域请求
- 无法访问不同源的文档的 DOM
- 无法读取其他域的 Cookie、Storage
跨源请求
如果一个请求是不同源的,浏览器始终会向其添加 Origin header,服务器可以检查 Origin,如果同意接受这样的请求,就会在响应中添加一个特殊的 header Access-Control-Allow-Origin
以前有 Referer 头,为什么还需要 Origin ?
是因为有时会没有 Referer。例如:
- 从 HTTPS(从高安全性访问低安全性)fetch HTTP 页面时,便没有
Referer。 - 内容安全策略 也可能会禁止发送
Referer。
正是因为 Referer 不可靠,才发明了 Origin。浏览器保证跨源请求的正确 Origin。
Referer | Origin | |
|---|---|---|
| 内容 | 完整的页面 URL | 只有“协议+域名+端口” |
| 安全性 | 不强制、可伪造、可关闭 | 强制加且不可篡改 |
| 用途 | 日志、流量分析、防盗链 | CORS、CSRF 防护 |
同源策略豁免情况
因为“跨域”这个概念只存在于浏览器对脚本访问(XHR / Fetch / WebGL / Canvas 等)所做的同源策略限制。对“单纯把资源拿回来”的标签式请求,不受同源策略影响:
- 导航请求:在浏览器地址栏输入 URL、点击链接 (
<a>标签)、表单提交等 <script src="CDN地址">:因为绕过了同源策略,所以需要完全信任脚本来源才引入<img src="跨域地址"><link href="跨域CSS"><video src="跨域视频">
简单请求
- 使用下列方法之一:
GET、POST、HEAD - 标头字段自定义的只能有:
Accept、Accept-Language、Content-Type、Content-Language、Range Content-Type所指定的媒体类型的值仅限于下列三者之一:text/plain、multipart/form-data、application/x-www-form-urlencoded
针对简单请求,浏览器不会拦截。但是非简单请求,浏览器会发送 预检请求(OPTIONS)
为什么要区分简单请求和非简单请求?
全部简单请求:
- 安全性问题:以前部署的大量服务器并没有设计跨域防护,直接发送请求会有安全问题
全部非简单请求:
- 兼容性问题:在 CORS 出现之前,很多网站已经存在一些 “跨域请求” 场景(上面的同源策略豁免情况),如果 CORS 强制要求所有请求都经过复杂验证,会破坏这些历史功能的兼容性
- 性能问题:如果全部用非简单请求,发送 2 次请求,比简单请求耗时多
解决跨域方法
1.CORS (Cross-Origin Resource Sharing,跨域资源共享)
请求携带 Origin header,服务端设置响应头:Access-Control-Allow-Origin,来允许或限制资源的跨域访问
2.代理服务器
在服务器端设置代理,将前端请求先发送到同源的后端服务器,然后由后端服务器转发请求到目标服务器
3.JSONP
通过创建 <script> 标签来请求跨域数据,然后服务器响应时将数据包装在回调函数中,实现跨域的原理是利用 script 标签没有跨域限制,通过 src 指向一个 URL,最后跟一个回调函数 callback(仅支持 GET 请求)
4.WebSocket
WebSocket 连接不受同源策略限制
安全问题
1.XSS (Cross-site scripting) 跨站脚本攻击
是一种代码注入攻击。攻击者将恶意代码植入到提供给其它用户使用的页面中,盗取存储在客户端的 cookie 或者用于识别客户端身份的敏感信息
根据攻击的来源,XSS 攻击可分为:
- 存储型:攻击者将恶意代码提交到目标网站的数据库中
- 反射型:攻击者构造出特殊的 URL,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器
- DOM 型:攻击者构造出特殊的 URL,前端 JS 执行恶意代码
预防
- 对用户输入和提交的内容进行转义和过滤
- 在使用 .innerHTML、.outerHTML、document.write() 时要注意,不要把不可信的数据作为 HTML 插到页面上
- 设置
httpOnlyCookie: 禁止 JavaScript 读取某些敏感 Cookie
2.CSRF (Cross-site request forgery) 跨站请求伪造
用户在已登录网站的情况下,攻击者通过引诱用户访问一个恶意网站或链接,发送伪造的请求到已登录的网站地址。由于浏览器会自动附带当前用户的身份验证信息(如 Cookies),目标应用程序会误以为该请求是用户自身发起的合法请求
预防
- 请求信息中携带 token
- 设置 Cookie 的
SameSite属性,防止跨站请求携带 CookieStrict:只有当前网页的 URL 与请求目标是同源的,才会带上 CookieLax:除了上面的情况,导航到目标网址的 GET 请求(只包括三种情况:链接,预加载请求,GET 表单)也会带上 Cookie
3.点击劫持
攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击
预防
设置 HTTP 响应头 X-Frame-Options:
DENY:禁止任何页面通过 iframe 嵌入当前页面SAMEORIGIN:允许相同源的页面通过 iframe 嵌入当前页面
内容安全策略(CSP)
内容安全策略(CSP)是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本(XSS)和数据注入攻击等。无论是数据盗取、网站内容污染还是恶意软件分发,这些攻击都是主要的手段。
- 使用:通过设置 HTTP Header 的
Content-Security-Policy - 原理:相当于白名单机制,指定网站允许加载的内容 (允许的内容源、脚本、样式表、图像、字体、对象、媒体(音频、视频)、iframe 等来源)
性能优化
资源方面:
- 图片压缩
- gzip 压缩
代码方面:
- HTML 中的 JS 文件使用 defer 或 async 加载
- 减少全局引入资源(如字体文件),页面访问时再加载
- 按需引入第三方包代码
- CSS 合理使用选择器,不要嵌套太深
- 减少 DOM 操作
- 路由懒加载
- 采用 keep-alive 缓存组件
- 大数据使用懒加载/虚拟列表
- 使用预加载 prefetch 对将来可能用到的资源进行提前缓存
打包方面:
- tree-shaking 去除无用代码(usedExports、sideEffects)
- 使用 plugin 压缩代码(内置terser-webpack-plugin、css-minimizer-webpack-plugin、html-minimizer-webpack-plugin)
- 代码进行拆分分离(SplitChunksPlugin 插件)
- 进行并发构建(HappyPack 插件)
- 打包分析:Webpack Bundle Analyzer 插件
网络方面:
- HTTP 缓存
- 使用 CDN 存放资源,加速访问
性能指标
| 阶段 | 指标名称 | 英文缩写 | 是否核心 | 计算方式 / 含义 | 推荐阈值 | 优化策略 |
|---|---|---|---|---|---|---|
| 加载 | 白屏时间 | FP (First Paint) | 浏览器第一次把像素画到屏幕 | ≤ 1.5 s | ||
| 首屏内容渲染 | FCP (First Contentful Paint) | 首屏第一个 DOM 元素绘制 | ≤ 1.8 s | |||
| 最大内容绘制 | LCP (Largest Contentful Paint) | √ | 首屏最大可见元素绘制 | ≤ 2.5 s | 图片压缩、懒加载、CDN | |
| 渲染 | 首次可交互 | FID (First Input Delay) | √ | 用户第一次点击到浏览器响应时间 | ≤ 100 ms | 代码分割 |
| 累积布局偏移 | CLS (Cumulative Layout Shift) | √ | 页面加载过程中意外抖动的总分 | ≤ 0.1 | 尺寸预留、字体预加载 | |
| 交互 | 可交互时间 | TTI (Time to Interactive) | 页面完全可交互且主线程空闲 | ≤ 3.8 s | ||
| 总阻塞时间 | TBT (Total Blocking Time) | 主线程被长任务阻塞的总时长 | ≤ 200 ms | |||
| 长期 | 累积长任务 | Long Tasks | 单次 JS 执行 > 50 ms 的任务数 | 越少越好 | ||
| 内存泄漏 | Memory Leak | 多次 GC 后堆内存持续增长 | 稳定即可 |
图片懒加载
有 3 种方案:
- 浏览器原生:
<img loading="lazy">,最简单,性能最好,缺点:阈值固定,无法自定义 - JS 监听滚动:监听
scroll事件,频繁回流、性能差 - 现代方案:
IntersectionObserver,性能好,自定义强
地址栏输入 URL 敲下回车后发生了什么
事件循环
JavaScript 是单线程的语言,它只有一个线程来执行代码,这意味着它一次只能执行一个任务。所有任务(同步/异步)都在主线程执行,事件循环是实现单线程非阻塞的机制
为什么 Javascript 要是单线程的
主要原因是 JS 最初的应用场景和核心目标:在浏览器中处理用户交互和操作 DOM,如果是多线程的,那么会产生冲突(如同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?)。单线程可以避免多线程带来的复杂性,比如线程同步、资源竞争等问题
任务分类
JavaScript 执行的异步代码分为宏任务和微任务
- 宏任务(MacroTask):
setTimeout、setInterval、setImmediate(Node.js)、I/O 操作、UI 渲染 - 微任务(MicroTask):
Promise.then/catch/finally、MutationObserver、process.nextTick(Node.js)
事件循环流程
- 所有任务都会在渲染进程的主线程上执行,首先执行整个脚本的同步代码
- 遇到异步任务,放入任务队列(宏任务和微任务队列)
- 执行完执行栈的同步代码
- 执行所有微任务,直到微任务队列清空
- 再执行一个宏任务
- 重复步骤 4~5,直到所有任务完成
请说明浏览器中的事件循环 (Event Loop)|ExplainThis
最常见的事件循环 (Event Loop) 面试题目汇整|ExplainThis
JS: 一战吃透Promise精修版 - 掘金
JS 读代码 | 前端面试派
Tip
await后面的代码 settled 之后,会把它下面的代码放入微任务队列new Promise内的代码是同步的,会直接执行。一个 promise 被 settled 时才会将它的 .then/catch 放入微任务队列,不然就是一直 pending 中Promise中执行到resolve()时并不会立即把对应的.then回调马上放到微任务队列,而是先继续执行resolve()后面的代码,如果后面的代码还有微任务则会插队.then后面再接.then会继续放入微任务队列末尾,一次执行完- 定时器的倒计时时间结束之后才会将任务放到宏任务队列
NOTE
目前最新规范取消了宏任务队列的说法,改成了按任务类型划分的多个独立任务队列,相同类型的任务放在一个队列(如:延时队列、交互队列等等),可以实现细粒度的优先级调度。
立即把一个函数添加到微任务队列:Promise.resolve().then(函数)
内存泄漏
内存泄漏的场景:
- 闭包
- 声明多余的全局变量
- 使用定时器没有清除
- 元素被移除后,事件监听器没有移除
- DOM 引用没有释放
检测:
使用浏览器开发者工具的 Memory 面板和 performance 面板查看
前端监控怎么做
- 监听
Vue.config.errorHandler,捕获 Vue 框架的错误 - 监听
window.onerror事件,捕获全局的宏任务错误 - 监听
window.unhandledrejection事件,捕获 promise 的报错 - 在 Axios 里拦截处理请求错误情况
performanceAPI 收集性能指标- 埋点收集用户行为(页面曝光、点击)
- 通过
navigator.sendBeacon发送数据
第三方框架:
- 开源框架:Sentry
封装组件注意的问题
- 不要追求大而全,注意拆分复用
- 不要忽略 html 的原生能力(如 input 的 type 属性,img 的 lazy 属性,details 折叠展开功能,dialog 原生对话框)
- 尽量使用语义化的 html(增加特殊人群的可访问性)
- 注释清晰
- 符合直觉
- 可扩展
- 不用写的太死,可以使用插槽交给父组件控制
组件库按需加载原理
前提:组件库在设计时将每个组件设计为独立模块(如单文件组件),每个组件对应独立的 JS/CSS 文件
实现按需加载的三种方式:
1.手动按路径导入
import Button from 'component-lib/lib/Button'; // 直接引入组件路径
import 'component-lib/lib/Button/style.css'; // 手动引入样式2.使用 Babel 插件(主流方案)
通过 babel-plugin-import 插件转换导入语句
import { Button, Input } from 'component-lib';
// 自动转换成:
import Button from 'component-lib/lib/Button';
import 'component-lib/lib/Button/style.css';
import Input from 'component-lib/lib/Input';
import 'component-lib/lib/Input/style.css';原理:在编译时通过 AST 转换代码
3.组件库侧优化(现代方案)
导出 ESM 模块:组件库在 package.json 中设置 module 字段指向 ESM 版本入口
Babel 原理
- 词法分析:将代码字符串拆分为 Tokens(语法单元)
- 语法分析:基于 Tokens 构建 AST(抽象语法树)
- 转换:Babel 插件遍历 AST 并修改节点
- 生成:将转换后的 AST 生成为目标代码
C 端开发常见问题
| 问题 | 解决方法 |
|---|---|
| 首屏加载时间(LCP)慢 | 首屏优化、骨架屏占位 |
| 交互响应速度(FID)慢 | 长阻塞任务使用 requestIdleCallback 分片执行 |
| 包体积过大 | Tree Shaking、动态导入、压缩等 |
| 兼容性(不同系统、不同机型、不同语言) | 特性适配 |
| 安全性 | 用户认证、接口校验、防抖节流 |
| 和原生通信 | JS Bridge 调用原生方法 |
文件 Hash 生成
文件 Hash 值是通过使用特定的哈希算法(如 MD5、SHA)对文件内容进行计算,生成一个固定长度的“数据指纹”,该指纹唯一标识了该文件,常用于验证文件完整性和数据安全性。