前端面试四之Fetch API同步和异步
Fetch API(Fetch Application Programming Interface)是一个现代的、基于Promise的网络请求接口,用于在浏览器环境中发起网络请求并处理响应。它是对传统XMLHttpRequest
的改进,提供了更简洁、灵活和强大的功能,广泛应用于前端开发中。
1. Fetch API 的基本概念
Fetch API 是一个用于从网络获取资源的接口,它提供了一个全局的fetch()
方法,该方法返回一个Promise
对象。通过fetch()
方法,开发者可以轻松地发送HTTP请求,获取服务器的响应,并处理返回的数据。
Fetch API 的设计目标是提供一个更简洁、更灵活的网络请求方式,同时支持现代的异步编程模式(基于Promise)。它支持多种HTTP方法(如GET、POST、PUT、DELETE等),并且可以处理各种类型的响应数据(如JSON、文本、Blob等)。
2. Fetch API 是异步的
Fetch API 是异步的,主要原因如下:
-
网络请求的特性:网络请求需要时间来完成,浏览器需要等待服务器响应。如果网络请求是同步的,那么在请求完成之前,浏览器的主线程会被阻塞,导致页面无法响应用户的其他操作,这会严重影响用户体验。
-
基于 Promise 的设计:Fetch API 返回一个
Promise
对象。Promise
是 JavaScript 中用于处理异步操作的机制,它允许在不阻塞主线程的情况下处理异步任务。
2.1.异步操作的典型代码
fetch('https://api.example.com/data').then(response => {if (!response.ok) {throw new Error(`HTTP error! Status: ${response.status}`);}return response.json();}).then(data => {console.log(data); // 处理返回的数据}).catch(error => {console.error('Fetch error:', error);});
在这个例子中:
-
fetch()
发起一个网络请求,并立即返回一个Promise
。 -
.then()
方法用于处理Promise
的成功结果。 -
.catch()
方法用于处理Promise
的失败结果。 -
浏览器不会等待
fetch()
完成,而是继续执行后续代码。当网络请求完成时,Promise
会触发.then()
或.catch()
中的回调函数。
2.2. 使用 async/await
让异步操作看起来像同步
虽然 Fetch API 是异步的,但可以通过 async/await
语法让代码看起来像是同步的。async/await
是 JavaScript 中处理异步操作的一种更简洁的方式,它基于 Promise
,但可以让代码的结构更接近同步代码。
使用 async/await
的代码
async function fetchData() {try {const response = await fetch('https://api.example.com/data');if (!response.ok) {throw new Error(`HTTP error! Status: ${response.status}`);}const data = await response.json();console.log(data); // 处理返回的数据} catch (error) {console.error('Fetch error:', error);}
}fetchData();
在这个例子中:
-
async
关键字用于声明一个异步函数。 -
await
关键字用于等待一个Promise
完成。 -
await fetch()
会暂停函数的执行,直到fetch()
的Promise
完成。如果Promise
成功,await
会返回Promise
的结果;如果Promise
失败,会抛出错误。 -
try...catch
用于捕获异步操作中可能抛出的错误。
虽然代码看起来像是同步的,但实际上仍然是异步的。await
只是让代码在逻辑上更直观,但它不会阻塞主线程。浏览器仍然可以在等待 fetch()
完成时执行其他任务。
2.3. 同步操作与异步操作的区别
同步操作
-
特点:代码按顺序执行,每一步操作必须等待前一步操作完成。
-
问题:如果某个操作耗时较长(如网络请求),会阻塞主线程,导致页面卡顿。
异步操作
-
特点:代码不会按顺序执行,某些操作可以在后台完成,主线程可以继续执行其他任务。
-
优点:不会阻塞主线程,用户体验更好。
3. Fetch API 的基本用法
3.1 基本语法
fetch(url, options).then(response => {// 处理响应}).catch(error => {// 处理错误});
-
url
:请求的资源地址,通常是字符串形式的URL。 -
options
:可选参数,用于配置请求的细节,例如请求方法、请求头、请求体等。
4. 浏览器同源检查(Same-Origin Policy)
4.1 什么是同源策略?
同源策略(Same-Origin Policy)是浏览器的一种安全机制,用于限制不同来源(origin)的文档或脚本之间的交互。它旨在防止恶意网站窃取用户数据或干扰其他网站的正常运行。
“同源”是指两个资源的协议(protocol)、域名(domain)和端口号(port)完全相同。如果这三个部分有任何一个不同,就被视为跨域。
例如:
-
同源:
http://example.com
和http://example.com/path
是同源的,因为它们的协议、域名和端口号都相同。 -
跨域:
http://example.com
和https://example.com
是跨域的,因为协议不同(http
vshttps
)。 -
跨域:
http://example.com
和http://sub.example.com
是跨域的,因为域名不同(example.com
vssub.example.com
)。 -
跨域:
http://example.com
和http://example.com:8080
是跨域的,因为端口号不同(默认端口80 vs 8080)。
4.2 同源策略的作用
-
保护用户隐私:防止恶意网站通过脚本访问其他网站的敏感信息(如用户的登录状态、个人数据等)。
-
防止XSS攻击:限制不同来源的脚本之间的交互,避免恶意脚本注入。
-
防止CSRF攻击:确保只有同源的请求可以操作敏感数据。
5. 跨域(Cross-Origin)
5.1 什么是跨域?
跨域是指两个资源的来源(origin)不同。当一个网页尝试与另一个来源的资源交互时,就会触发跨域问题。由于同源策略的限制,浏览器会阻止这种跨域操作。
5.2 跨域的常见场景
-
跨域AJAX请求:尝试从一个域向另一个域发起HTTP请求。
5.3 解决跨域问题的方法
5.3.1请求响应头解决跨域问题
通过在服务器端设置特定的HTTP响应头,允许浏览器从特定的源(或所有源)访问资源。这种方法基于浏览器的CORS(跨域资源共享)机制。
关键响应头
-
Access-Control-Allow-Origin
:指定允许跨域请求的域名。可以设置为具体域名(如http://example.com
),也可以设置为*
(允许所有域名)。 -
Access-Control-Allow-Methods
:指定允许的HTTP方法(如GET, POST, PUT, DELETE
)。 -
Access-Control-Allow-Headers
:指定允许的请求头。 -
Access-Control-Allow-Credentials
:指定是否允许携带Cookie等认证信息。如果设置为true
,Access-Control-Allow-Origin
不能为*
。 -
Access-Control-Max-Age
:指定预检请求结果的缓存时间。
以下是一个Node.js服务器的示例,展示如何设置响应头以允许跨域请求:
const http = require('http');
const server = http.createServer((req, res) => {res.writeHead(200, {'Access-Control-Allow-Origin': 'http://example.com', // 允许特定域名访问'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE','Access-Control-Allow-Headers': 'Content-Type, Authorization','Access-Control-Allow-Credentials': 'true'});res.end('hello world');
});
server.listen(3000);
5.3.2 代理解决跨域问题
通过设置一个同域的代理服务器,将前端的跨域请求转发到目标服务器。浏览器认为请求是同源的,从而绕过跨域限制。
1.使用Nginx作为代理服务器
server {listen 80;server_name yourdomain.com;location /api/ {proxy_pass http://backend-server:3000/; # 转发到后端服务proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;}
}
-
说明:前端请求
/api
路径时,Nginx会将请求代理到目标服务器。
2.使用Node.js中间件
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();app.use('/api', createProxyMiddleware({target: 'http://example.com',changeOrigin: true
}));app.listen(3000);
-
说明:所有发送到
/api
的请求都会被代理到http://example.com
。
总结
-
请求响应头解决跨域问题:通过服务器端设置CORS响应头,适用于需要灵活控制跨域策略的场景。
-
代理解决跨域问题:通过代理服务器转发请求,适用于开发环境或需要统一管理跨域请求的场景。
6.面试问题
1.什么是同源策略?为什么要设置同源策略?
同源策略是一种浏览器的安全机制,它要求只有当协议、域名和端口号完全相同的两个页面才能互相访问DOM或发起AJAX请求。它的主要目的是防止恶意网站通过脚本访问其他网站的敏感信息,从而保护用户的隐私和网站的安全。
2. 什么是跨域?常见的跨域场景有哪些?
跨域是指两个资源的来源不同,例如协议、域名或端口号不同。常见的跨域场景包括:从一个域向另一个域发起AJAX请求、嵌入不同源的资源(如图片、脚本、样式表等),以及尝试访问嵌入页面(如iframe)的DOM。
3. 如何解决跨域问题?
解决跨域问题的方法有多种:
-
JSONP:通过动态创建
<script>
标签来加载跨域资源,适用于GET请求。 -
CORS:通过在服务器响应中添加
Access-Control-Allow-Origin
等HTTP头来允许跨域请求。 -
代理服务器:通过设置一个同域的代理服务器,将跨域请求转发到目标服务器。
-
文档域:适用于主域相同但子域不同的情况,通过设置
document.domain
来实现跨域。
4. 什么是CORS?它是如何工作的?
CORS是一种跨域解决方案,允许服务器通过HTTP头明确指定哪些外部域可以访问其资源。当浏览器发起跨域请求时,会在请求中添加Origin
头。服务器检查这个头,决定是否允许请求。如果允许,服务器会在响应中添加Access-Control-Allow-Origin
头,指定允许的来源。
5.在vue项目中最常用的一种解决跨域方式是什么 ?
在 Vue 项目中,最常用且推荐的解决跨域问题的方式是通过 代理服务器。这种方式简单高效,尤其适合开发环境,因为它不需要修改后端代码,也不需要在前端代码中添加额外的跨域处理逻辑。
1. 使用 Vue CLI 的代理功能
Vue CLI 提供了一个非常方便的代理功能,可以在开发环境中轻松解决跨域问题。它基于 Webpack 的 devServer.proxy
配置,允许你将特定的请求转发到目标服务器。
配置步骤
-
在
vue.config.js
中配置代理 如果你的 Vue 项目使用了 Vue CLI,可以在项目的根目录下创建或修改vue.config.js
文件,添加代理配置。// vue.config.js module.exports = {devServer: {proxy: {'/api': {target: 'http://example.com', // 目标服务器地址changeOrigin: true, // 允许跨域pathRewrite: {'^/api': '' // 重写路径,去掉/api前缀}}}} };
在这个配置中:
-
/api
是前端请求的路径前缀。 -
target
是目标服务器的地址。 -
changeOrigin
设置为true
,可以避免跨域问题。 -
pathRewrite
用于重写路径,去掉/api
前缀,这样目标服务器不会收到/api
路径。
2.前端代码中的请求
在前端代码中,你可以直接使用 /api
作为请求路径,Vue CLI 会自动将其转发到目标服务器。
// 使用 fetch 或 axios 发起请求
fetch('/api/data').then(response => response.json()).then(data => console.log(data)).catch(error => console.error(error));
或者使用 axios
:
import axios from 'axios';axios.get('/api/data').then(response => console.log(response.data)).catch(error => console.error(error));