网络和通讯✅
XMLHttpRequest 和 Fetch API 的区别 ?
答案
维度 | XMLHttpRequest (XHR) | Fetch API |
---|---|---|
API风格 | 基于回调函数,事件驱动 | 基于Promise,链式调用 |
语法简洁性 | 语法繁琐,需多步配置 | 语法简洁,易于维护 |
请求/响应处理 | 需手动解析JSON、处理状态 | json()等方法自动解析 |
请求头设置 | setRequestHeader方法 | Headers对象,配置灵活 |
请求体 | 字符串、FormData等 | 支持FormData、Blob、URLSearchParams等 |
取消请求 | 通过abort()方法 | 支持AbortController/AbortSignal |
进度事件 | 支持upload/download进度事件 | 仅部分支持,需额外处理 |
跨域 | 支持CORS,配置复杂 | 支持CORS,配置直观 |
错误处理 | 需判断readyState/status | 仅网络错误/中止抛异常,状态码需手动判断 |
文件上传/下载 | 支持 | 支持 |
浏览器兼容性 | 兼容性好,老浏览器支持 | 新标准,低版本需polyfill |
自定义请求 | 支持 | 支持Request对象,功能丰富 |
补充说明
- XHR 适合兼容性要求高、需进度事件的场景,但API繁琐。
- Fetch API 语法现代,Promise链式处理更优雅,推荐新项目优先使用。
- Fetch 支持通过 AbortController 取消请求,XHR 通过 abort() 方法实现。
- 错误处理上,Fetch 只对网络错误抛异常,状态码错误需手动判断。
// XHR 示例
const xhr = new XMLHttpRequest()
xhr.open('GET', '/api/data')
xhr.onload = () => { if (xhr.status === 200) console.log(xhr.responseText) }
xhr.send()
// Fetch 示例
fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err))
推荐新项目优先使用 Fetch API,语法更现代,易于维护。需兼容老浏览器时可用 XHR 或引入 polyfill。
延伸阅读
AbortController 和 AbortSignal 有什么区别?
答案
- AbortController 是主动方,用于创建与触发取消;AbortSignal 是被动方,只读的取消信号,传给支持的 API(如 fetch、ReadableStream、Axios 等)。
- 一对多:一个 controller.signal 可分发给多个请求;controller.abort(reason) 一次触发,所有持有该 signal 的操作同时中止。
- 能力扩展:AbortSignal.timeout(ms) 自动超时;AbortSignal.any([s1,s2]) 组合信号;signal.aborted/signal.reason 标识与原因传递。
- 事件模型:signal 触发一次性 abort 事件;取消在规范层面由 DOM Standard/WHATWG Fetch 统一定义。
属性/对象 | AbortController | AbortSignal |
---|---|---|
职责 | 创建/触发取消 | 承载取消状态与原因 |
是否可写 | 可写(abort) | 只读 |
典型用法 | new AbortController(); ctrl.abort('reason') | fetch(url, { signal }); signal.aborted/signal.reason |
// 1) 手动取消:一对多广播
const ctrl = new AbortController()
const s = ctrl.signal
Promise.allSettled([
fetch('https://httpbin.org/delay/3', { signal: s }),
fetch('https://httpbin.org/delay/3', { signal: s })
]).then(rs => console.log('manual:', rs.map(x => x.status)))
setTimeout(() => ctrl.abort(new DOMException('by user', 'AbortError')), 300)
// 2) 自动超时(Node≥17.3/现代浏览器)
fetch('https://httpbin.org/delay/3', { signal: AbortSignal.timeout(500) })
.catch(e => console.log('timeout ->', e.name, e.message))
// 3) 组合取消:任一触发即取消(现代浏览器)
const slow = new AbortController()
const fast = AbortSignal.timeout(400)
const combo = AbortSignal.any([slow.signal, fast])
fetch('https://httpbin.org/delay/2', { signal: combo })
.catch(e => console.log('any ->', e.name, combo.reason?.message))
延伸阅读:
- MDN: AbortController — 控制端 API
- MDN: AbortSignal — 信号与事件
- WHATWG Fetch: termination — 规范中的取消模型
- Streams API: abort — 流与取消的关系
取消请求如何实现?
答案
核心概念:
请求取消是前端开发中的重要功能,主要用于避免过时请求、节约网络资源、优化用户体验。现代实现方式包括:XMLHttpRequest.abort()、Fetch API + AbortController、自定义取消令牌等。关键在于理解请求生命周期和正确的清理机制。
实际示例:
// 方法1:XMLHttpRequest 取消
class XHRManager {
constructor() {
this.activeRequests = new Map();
}
request(url, options = {}) {
const requestId = this.generateId();
const xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
// 存储请求信息
this.activeRequests.set(requestId, { xhr, resolve, reject });
xhr.open(options.method || 'GET', url, true);
// 设置请求头
if (options.headers) {
Object.entries(options.headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
}
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
this.activeRequests.delete(requestId);
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
data: xhr.responseText,
status: xhr.status,
headers: xhr.getAllResponseHeaders()
});
} else {
reject(new Error(`Request failed with status ${xhr.status}`));
}
}
};
xhr.onerror = () => {
this.activeRequests.delete(requestId);
reject(new Error('Network error'));
};
xhr.onabort = () => {
this.activeRequests.delete(requestId);
reject(new Error('Request cancelled'));
};
xhr.send(options.body);
});
}
cancel(requestId) {
const requestInfo = this.activeRequests.get(requestId);
if (requestInfo) {
requestInfo.xhr.abort();
this.activeRequests.delete(requestId);
}
}
cancelAll() {
this.activeRequests.forEach((requestInfo, requestId) => {
requestInfo.xhr.abort();
});
this.activeRequests.clear();
}
generateId() {
return Date.now() + Math.random().toString(36).substr(2, 9);
}
}
// 方法2:Fetch + AbortController(推荐)
class FetchManager {
constructor() {
this.activeRequests = new Map();
}
async request(url, options = {}) {
const controller = new AbortController();
const requestId = this.generateId();
// 合并 AbortSignal
const mergedOptions = {
...options,
signal: this.combineSignals([controller.signal, options.signal].filter(Boolean))
};
// 存储控制器
this.activeRequests.set(requestId, controller);
try {
const response = await fetch(url, mergedOptions);
this.activeRequests.delete(requestId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
} catch (error) {
this.activeRequests.delete(requestId);
if (error.name === 'AbortError') {
throw new Error('Request cancelled');
}
throw error;
}
}
cancel(requestId) {
const controller = this.activeRequests.get(requestId);
if (controller) {
controller.abort();
this.activeRequests.delete(requestId);
}
}
cancelAll() {
this.activeRequests.forEach(controller => controller.abort());
this.activeRequests.clear();
}
combineSignals(signals) {
if (signals.length === 0) return undefined;
if (signals.length === 1) return signals[0];
const controller = new AbortController();
signals.forEach(signal => {
if (signal.aborted) {
controller.abort();
return;
}
signal.addEventListener('abort', () => controller.abort(), { once: true });
});
return controller.signal;
}
generateId() {
return Date.now() + Math.random().toString(36).substr(2, 9);
}
}
// 方法3:React Hook 封装
function useRequest() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
const execute = useCallback(async (url, options = {}) => {
// 取消之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const controller = new AbortController();
abortControllerRef.current = controller;
setLoading(true);
setError(null);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setLoading(false);
return data;
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
setLoading(false);
}
throw err;
}
}, []);
const cancel = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
}, []);
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return { execute, cancel, loading, error };
}
// 方法4:超时自动取消
class TimeoutRequest {
static async fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
}
// 使用示例
const fetchManager = new FetchManager();
// 发起请求
const requestId = await fetchManager.request('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: 'test' })
});
// 取消特定请求
fetchManager.cancel(requestId);
// 取消所有请求
fetchManager.cancelAll();
// 超时请求
try {
const response = await TimeoutRequest.fetchWithTimeout('/api/slow', {}, 3000);
console.log(await response.json());
} catch (error) {
console.error('Request failed:', error.message);
}
// React 组件中使用
function MyComponent() {
const { execute, cancel, loading, error } = useRequest();
const handleFetch = async () => {
try {
const data = await execute('/api/data');
console.log(data);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Request failed:', error);
}
}
};
return (
<div>
<button onClick={handleFetch} disabled={loading}>
{loading ? 'Loading...' : 'Fetch Data'}
</button>
<button onClick={cancel}>Cancel</button>
{error && <p>Error: {error}</p>}
</div>
);
}
面试官视角:
要点清单:
- 理解 XMLHttpRequest.abort() 和 AbortController 的使用
- 掌握请求取消的时机和清理机制
- 了解请求取消对用户体验和性能的影响
加分项:
- 实现统一的请求管理和批量取消
- 结合超时机制和错误处理
- 在框架中正确使用请求取消(如 React useEffect 清理)
常见失误:
- 忘记清理已取消的请求引用导致内存泄漏
- 没有区分 AbortError 和其他错误类型
- 在组件卸载时未取消进行中的请求
延伸阅读:
- AbortController - MDN — 现代请求取消API
- XMLHttpRequest.abort() - MDN — XHR取消方法
- Fetch API - MDN — 现代网络请求API
如何拦截 web 应用的请求
答案
核心概念:
请求拦截是前端开发中的重要技术,用于监听、修改或处理 Web 应用中的网络请求。主要实现方式包括:原生API拦截(重写fetch/XMLHttpRequest)、Service Worker代理、第三方库拦截器(如Axios)、代理模式等。常用于日志记录、身份验证、错误处理、数据监控等场景。
实际示例:
// 方法1:原生 Fetch API 拦截
class FetchInterceptor {
constructor() {
this.originalFetch = window.fetch;
this.interceptors = {
request: [],
response: []
};
this.install();
}
// 安装拦截器
install() {
const self = this;
window.fetch = async function(url, options = {}) {
// 执行请求拦截器
let config = { url, options };
for (const interceptor of self.interceptors.request) {
config = await interceptor(config);
}
try {
// 发送请求
const response = await self.originalFetch(config.url, config.options);
// 执行响应拦截器
let result = response;
for (const interceptor of self.interceptors.response) {
result = await interceptor(result);
}
return result;
} catch (error) {
// 执行错误拦截器
for (const interceptor of self.interceptors.error || []) {
await interceptor(error);
}
throw error;
}
};
}
// 添加请求拦截器
addRequestInterceptor(interceptor) {
this.interceptors.request.push(interceptor);
}
// 添加响应拦截器
addResponseInterceptor(interceptor) {
this.interceptors.response.push(interceptor);
}
// 恢复原始fetch
restore() {
window.fetch = this.originalFetch;
}
}
// 方法2:XMLHttpRequest 拦截
class XHRInterceptor {
constructor() {
this.originalXHR = window.XMLHttpRequest;
this.interceptors = {
request: [],
response: []
};
this.install();
}
install() {
const self = this;
window.XMLHttpRequest = function() {
const xhr = new self.originalXHR();
const originalOpen = xhr.open;
const originalSend = xhr.send;
// 拦截 open 方法
xhr.open = function(method, url, async, user, password) {
this._method = method;
this._url = url;
// 执行请求拦截器
for (const interceptor of self.interceptors.request) {
const result = interceptor({ method, url, xhr: this });
if (result) {
this._url = result.url || url;
this._method = result.method || method;
}
}
return originalOpen.call(this, this._method, this._url, async, user, password);
};
// 拦截 send 方法
xhr.send = function(data) {
const self = this;
// 监听响应
this.addEventListener('readystatechange', function() {
if (this.readyState === 4) {
// 执行响应拦截器
for (const interceptor of self.interceptors.response) {
interceptor({
status: this.status,
response: this.response,
xhr: this
});
}
}
});
return originalSend.call(this, data);
};
return xhr;
};
}
addRequestInterceptor(interceptor) {
this.interceptors.request.push(interceptor);
}
addResponseInterceptor(interceptor) {
this.interceptors.response.push(interceptor);
}
restore() {
window.XMLHttpRequest = this.originalXHR;
}
}
// 方法3:Service Worker 拦截
// sw.js (Service Worker 文件)
self.addEventListener('fetch', function(event) {
const url = event.request.url;
// 拦截特定域名的请求
if (url.includes('api.example.com')) {
event.respondWith(
(async () => {
try {
// 可以修改请求
const modifiedRequest = new Request(event.request, {
headers: {
...event.request.headers,
'Authorization': 'Bearer ' + await getToken(),
'X-Timestamp': Date.now().toString()
}
});
// 发送请求
const response = await fetch(modifiedRequest);
// 可以修改响应
if (response.status === 401) {
// 处理未授权情况
await refreshToken();
return fetch(modifiedRequest);
}
return response;
} catch (error) {
console.error('Network error:', error);
return new Response('Network Error', { status: 500 });
}
})()
);
}
});
// 主线程中注册 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// 方法4:Axios 拦截器
import axios from 'axios';
// 创建axios实例
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000
});
// 请求拦截器
apiClient.interceptors.request.use(
config => {
// 添加认证token
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 添加请求ID用于追踪
config.headers['X-Request-ID'] = generateRequestId();
// 记录请求日志
console.log(`[${new Date().toISOString()}] ${config.method?.toUpperCase()} ${config.url}`);
return config;
},
error => {
console.error('Request error:', error);
return Promise.reject(error);
}
);
// 响应拦截器
apiClient.interceptors.response.use(
response => {
// 记录响应日志
console.log(`[${new Date().toISOString()}] ${response.status} ${response.config.url}`);
return response;
},
error => {
// 统一错误处理
if (error.response?.status === 401) {
// 清除token,跳转登录
localStorage.removeItem('authToken');
window.location.href = '/login';
} else if (error.response?.status >= 500) {
// 服务器错误提示
showErrorMessage('服务器错误,请稍后重试');
}
return Promise.reject(error);
}
);
// 方法5:通用拦截器类
class RequestInterceptor {
constructor() {
this.fetchInterceptor = new FetchInterceptor();
this.xhrInterceptor = new XHRInterceptor();
this.requestLog = [];
}
// 添加统一的请求日志
addRequestLogger() {
this.fetchInterceptor.addRequestInterceptor(config => {
this.requestLog.push({
type: 'fetch',
url: config.url,
method: config.options?.method || 'GET',
timestamp: Date.now()
});
return config;
});
this.xhrInterceptor.addRequestInterceptor(config => {
this.requestLog.push({
type: 'xhr',
url: config.url,
method: config.method,
timestamp: Date.now()
});
return config;
});
}
// 添加统一的错误处理
addErrorHandler() {
this.fetchInterceptor.addResponseInterceptor(async response => {
if (!response.ok) {
console.error(`Request failed: ${response.status} ${response.url}`);
// 可以发送错误统计
this.sendErrorStats({
url: response.url,
status: response.status,
timestamp: Date.now()
});
}
return response;
});
}
// 获取请求统计
getRequestStats() {
return {
total: this.requestLog.length,
byType: this.requestLog.reduce((acc, req) => {
acc[req.type] = (acc[req.type] || 0) + 1;
return acc;
}, {}),
recent: this.requestLog.slice(-10)
};
}
sendErrorStats(errorInfo) {
// 发送错误统计到监控系统
fetch('/api/stats/error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorInfo)
}).catch(() => {
// 忽略统计发送失败
});
}
}
// 使用示例
const interceptor = new RequestInterceptor();
// 启用请求日志
interceptor.addRequestLogger();
// 启用错误处理
interceptor.addErrorHandler();
// 添加认证拦截
interceptor.fetchInterceptor.addRequestInterceptor(config => {
const token = localStorage.getItem('authToken');
if (token) {
config.options = {
...config.options,
headers: {
...config.options?.headers,
'Authorization': `Bearer ${token}`
}
};
}
return config;
});
// 查看请求统计
console.log(interceptor.getRequestStats());
// 辅助函数
function generateRequestId() {
return Math.random().toString(36).substr(2, 9);
}
function showErrorMessage(message) {
// 显示错误提示的具体实现
alert(message);
}
async function getToken() {
// 获取认证token的具体实现
return localStorage.getItem('authToken');
}
async function refreshToken() {
// 刷新token的具体实现
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
const data = await response.json();
localStorage.setItem('authToken', data.token);
} catch (error) {
console.error('Token refresh failed:', error);
}
}
面试官视角:
要点清单:
- 理解不同拦截方式的适用场景和局限性
- 掌握拦截器的安装、使用和清理机制
- 了解Service Worker拦截的工作原理和注册过程
加分项:
- 实现统一的请求监控和错误处理系统
- 结合认证、日志、性能监控等实际业务需求
- 理解拦截对性能的影响并做出权衡
常见失误:
- 忘记恢复原始方法导致内存泄漏
- 拦截器中的错误处理不当影响正常请求
- Service Worker缓存策略不当影响数据实时性
延伸阅读:
- Service Worker API - MDN — Service Worker拦截技术
- Axios Interceptors — Axios拦截器官方文档
- Fetch API - MDN — 现代网络请求API
Long-Polling、Websockets 和 Server-Sent Event 之间有什么区别?
答案
维度 | Long-Polling | WebSocket | Server-Sent Event (SSE) |
---|---|---|---|
连接方式 | HTTP短连接 | TCP长连接 | HTTP长连接 |
数据流向 | 单向(服务端→客户端) | 双向(全双工) | 单向(服务端→客户端) |
实时性 | 较好 | 极佳 | 好 |
兼容性 | 广泛 | 现代浏览器 | 较好(IE不兼容) |
典型场景 | 消息推送、IM | 实时协作、游戏、IM | 实时通知、数据流 |
特点/限制 | 每次响应后需重新请求,资源消耗大 | 需服务端支持,初始握手复杂 | 仅支持文本,无法双向通信 |
补充说明
- Long-Polling 通过不断发起 HTTP 请求模拟实时推送,兼容性好但效率低。
- WebSocket 支持双向通信,适合高实时性场景,但需服务端专门支持。
- SSE 适合服务端单向推送,API简单,自动重连,但不支持 IE,且仅支持文本数据。
如需双向实时通信优先选 WebSocket,单向推送可用 SSE,兼容性优先时可选 Long-Polling。
延伸阅读
说下 navigator.sendBeacon
答案
特性 | sendBeacon | fetch(keepalive) |
---|---|---|
用途 | 异步发送小量数据,常用于埋点、统计 | 可发送更复杂请求,支持 keepalive 保证卸载时发送 |
触发时机 | 页面卸载、关闭等 | 页面卸载、关闭等 |
数据类型 | 文本、Blob、FormData 等 | 任意 body 类型 |
数据量限制 | 较小(通常几十 KB) | 约 64KB(各浏览器略有差异) |
返回值 | 布尔值,表示是否入队 | Promise,无法保证一定送达 |
阻塞卸载 | 否 | 否 |
兼容性 | 现代浏览器广泛支持 | 现代浏览器支持,部分低版本不支持 keepalive |
核心概念与原理
navigator.sendBeacon(url, data)
设计用于在页面卸载时异步、可靠地发送小量数据,不阻塞页面关闭。底层通过浏览器任务队列保证请求尽量送达,适合统计、日志等场景。fetch
的keepalive: true
选项也可用于类似场景,但有更严格的数据量限制,且部分老浏览器不支持。
常见误区
- 误以为 sendBeacon 能保证 100% 送达,实际上网络异常等仍可能丢失。
- sendBeacon 只适合小数据量,超出限制可能被丢弃。
- fetch keepalive 不是所有浏览器都支持,且请求体过大时会失败。
代码示例
// 推荐:页面卸载时发送埋点
window.addEventListener('unload', () => {
const data = { event: 'leave', time: Date.now() }
navigator.sendBeacon('/api/track', JSON.stringify(data))
})
// 备选方案:fetch + keepalive
window.addEventListener('beforeunload', () => {
const data = { event: 'leave', time: Date.now() }
fetch('/api/track', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
keepalive: true
})
})
统计/埋点等需保证卸载时送达的小数据,优先用 sendBeacon。需发送大数据或自定义请求时可用 fetch+keepalive,但注意兼容性和体积限制。
延伸阅读
SSE
答案
核心概念:
Server-Sent Events (SSE) 是 HTML5 标准,允许服务器主动向客户端推送数据的技术。基于 HTTP 协议,使用 EventSource API 创建持久连接,服务器可随时发送文本数据给浏览器。支持自动重连、自定义事件类型,是实现实时通信的轻量级解决方案,适合单向数据推送场景。
实际示例:
// 基本 SSE 连接
const eventSource = new EventSource('/events')
// 监听默认消息事件
eventSource.onmessage = function (event) {
console.log('收到消息:', event.data)
const data = JSON.parse(event.data)
updateUI(data)
}
// 监听自定义事件
eventSource.addEventListener('notification', function (event) {
const notification = JSON.parse(event.data)
showNotification(notification.title, notification.body)
})
// 监听连接状态
eventSource.onopen = function () {
console.log('SSE 连接已建立')
}
eventSource.onerror = function (event) {
console.error('SSE 连接错误:', event)
if (eventSource.readyState === EventSource.CLOSED) {
console.log('SSE 连接已关闭')
}
}
// 手动关闭连接
function closeConnection () {
eventSource.close()
}
// 跨域 SSE 连接示例
const corsEventSource = new EventSource('https://api.example.com/events', {
withCredentials: true
})
// 服务器端数据格式示例 (Node.js)
/*
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 发送普通消息
res.write('data: {"message": "Hello World"}\n\n');
// 发送自定义事件
res.write('event: notification\n');
res.write('data: {"title": "新消息", "body": "您有一条新消息"}\n\n');
// 设置重连间隔
res.write('retry: 10000\n\n');
});
*/
面试官视角:
要点清单:
- 理解SSE的单向通信特性和应用场景
- 掌握EventSource API的基本用法
- 了解SSE与WebSocket的区别和选择
加分项:
- 提及HTTP/2下的连接数限制改善
- 了解服务器端实现的数据格式要求
- 知道如何处理网络异常和重连机制
常见失误:
- 混淆SSE与WebSocket的适用场景
- 不了解同域连接数限制问题
- 忽略错误处理和连接管理
延伸阅读:
- Server-sent events - MDN — SSE 完整文档
- EventSource API - MDN — EventSource 接口说明
- SSE vs WebSocket comparison — 技术选型对比
如何实现浏览器内多个标签页之间的通信?
答案
核心概念:
浏览器内多个标签页之间的通信是指在同一域名下的不同页面或标签页之间传递数据和消息。主要方法包括:
- Broadcast Channel API - HTML5标准的跨页面通信机制,支持实时双向通信
- LocalStorage + storage事件 - 利用本地存储的变化触发事件实现通信
- SharedWorker - 共享Worker线程,多个页面可共享同一后台进程
- postMessage API - 适用于iframe和跨窗口通信场景
- ServiceWorker + MessageChannel - 现代PWA应用的通信方案
- WebSocket连接共享 - 通过服务器中转实现页面间通信
实际示例:
SharedWorker 实现示例:
// worker.js - 共享Worker文件
self.onconnect = function (event) {
const port = event.ports[0]
port.onmessage = function (event) {
const message = event.data
// 处理来自不同页面的消息
port.postMessage('Response from SharedWorker: ' + message)
}
port.start()
}
// 页面中使用SharedWorker
const sharedWorker = new SharedWorker('worker.js')
const port = sharedWorker.port
// 发送消息
port.postMessage('Hello from page')
// 接收消息
port.onmessage = function (event) {
console.log('Received:', event.data)
}
Window.postMessage 使用示例:
// 发送消息到目标窗口
window.postMessage('Hello, World!', 'https://example.com')
// 监听消息事件
window.addEventListener('message', function (event) {
// 确保消息来自指定域名
if (event.origin === 'https://example.com') {
const message = event.data
console.log('Received message:', message)
}
})
// iframe 通信示例
// 父页面向子iframe发送消息
const iframe = document.getElementById('myIframe')
iframe.contentWindow.postMessage('message from parent', 'https://child-domain.com')
// 子iframe向父页面发送消息
window.parent.postMessage('message from child', 'https://parent-domain.com')
面试官视角:
要点清单:
- 了解多种跨标签页通信方案的适用场景
- 理解同源策略对通信的限制
- 掌握Broadcast Channel API的基本用法
加分项:
- 提及性能和兼容性考虑
- 了解SharedWorker和ServiceWorker的区别
- 知道如何处理通信异常和错误处理
常见失误:
- 混淆跨域通信和同域通信的区别
- 不了解storage事件的触发条件
- 忽视浏览器兼容性问题
延伸阅读:
- Broadcast Channel API - MDN — 标准跨页面通信API
- SharedWorker - MDN — 共享Worker使用指南
- Window.postMessage() - MDN — 跨窗口通信方法
跨页面通信方式?
答案
方式 | 适用场景 | 特点 | 典型用法 |
---|---|---|---|
URL参数 | 简单数据传递 | 无需存储,易实现 | 页面跳转时拼接查询参数 |
localStorage/sessionStorage | 同源页面间共享 | 容量大,持久/会话级 | 存取结构化数据,刷新/新开页可用 |
Cookies | 同源页面间共享 | 小容量,支持跨域(配置) | 存储小型数据,兼容性好 |
postMessage API | 窗口/iframe间通信 | 安全、支持跨域 | 父子窗口、iframe消息传递 |
Broadcast Channel API | 多标签页/窗口通信 | 广播机制,简单高效 | 同源多页面实时同步 |
Shared Worker | 多页面共享数据 | 可维护全局状态,支持通信 | 多页面间共享Worker实例 |
WebSocket | 实时通信 | 支持服务端推送,跨页面需管理连接 | 多页面共享同一WebSocket连接 |
延伸阅读
- MDN: Web Storage API - localStorage/sessionStorage 说明
- MDN: postMessage - 跨窗口通信
- MDN: Broadcast Channel API - 多页面广播
- MDN: SharedWorker - 多页面共享Worker
- MDN: WebSocket - 实时通信
文件上传和上传文件解析的原理是啥?
答案
核心概念:
文件上传是Web开发中的重要功能,涉及前端文件选择和传输、后端数据解析和存储。主要基于HTTP的multipart/form-data编码格式,利用FormData API构建请求体,服务端通过专门的解析器提取文件数据。现代实现还包括断点续传、分片上传、进度监控等高级功能,确保大文件传输的可靠性和用户体验。
实际示例:
// 前端文件上传实现
class FileUploader {
constructor(options = {}) {
this.chunkSize = options.chunkSize || 1024 * 1024; // 1MB per chunk
this.maxRetries = options.maxRetries || 3;
this.concurrency = options.concurrency || 3;
}
// 单文件上传
async uploadFile(file, url, options = {}) {
const formData = new FormData();
formData.append('file', file);
// 添加额外的字段
if (options.metadata) {
Object.entries(options.metadata).forEach(([key, value]) => {
formData.append(key, value);
});
}
try {
const response = await fetch(url, {
method: 'POST',
body: formData,
signal: options.signal // 支持取消上传
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Upload error:', error);
throw error;
}
}
// 带进度的文件上传
uploadWithProgress(file, url, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
// 监听上传进度
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
onProgress(progress, event.loaded, event.total);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
resolve(response);
} catch (error) {
reject(new Error('Invalid response format'));
}
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Network error'));
});
xhr.open('POST', url);
xhr.send(formData);
});
}
// 分片上传
async uploadChunks(file, url, options = {}) {
const chunks = this.createChunks(file);
const fileId = this.generateFileId(file);
// 并发上传chunks
const results = await this.uploadChunksConcurrently(chunks, url, fileId, options);
// 合并chunks
return await this.mergeChunks(url, fileId, file.name, options);
}
createChunks(file) {
const chunks = [];
const totalChunks = Math.ceil(file.size / this.chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, file.size);
const chunk = file.slice(start, end);
chunks.push({
index: i,
chunk,
start,
end,
size: chunk.size
});
}
return chunks;
}
async uploadChunksConcurrently(chunks, url, fileId, options) {
const results = [];
const semaphore = new Semaphore(this.concurrency);
const uploadPromises = chunks.map(async (chunkInfo) => {
await semaphore.acquire();
try {
const result = await this.uploadSingleChunk(chunkInfo, url, fileId, options);
results[chunkInfo.index] = result;
return result;
} finally {
semaphore.release();
}
});
await Promise.all(uploadPromises);
return results;
}
async uploadSingleChunk(chunkInfo, url, fileId, options, retryCount = 0) {
const formData = new FormData();
formData.append('chunk', chunkInfo.chunk);
formData.append('fileId', fileId);
formData.append('chunkIndex', chunkInfo.index);
formData.append('chunkSize', chunkInfo.size);
formData.append('totalChunks', Math.ceil(options.totalSize / this.chunkSize));
try {
const response = await fetch(`${url}/chunk`, {
method: 'POST',
body: formData,
signal: options.signal
});
if (!response.ok) {
throw new Error(`Chunk upload failed: ${response.status}`);
}
return await response.json();
} catch (error) {
if (retryCount < this.maxRetries) {
console.warn(`Retrying chunk ${chunkInfo.index}, attempt ${retryCount + 1}`);
await this.delay(1000 * Math.pow(2, retryCount)); // 指数退避
return this.uploadSingleChunk(chunkInfo, url, fileId, options, retryCount + 1);
}
throw error;
}
}
async mergeChunks(url, fileId, filename, options) {
const response = await fetch(`${url}/merge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileId,
filename,
totalSize: options.totalSize
}),
signal: options.signal
});
if (!response.ok) {
throw new Error(`Merge failed: ${response.status}`);
}
return await response.json();
}
generateFileId(file) {
return `${file.name}_${file.size}_${file.lastModified}_${Date.now()}`;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 信号量实现并发控制
class Semaphore {
constructor(count) {
this.count = count;
this.waiting = [];
}
async acquire() {
if (this.count > 0) {
this.count--;
return;
}
return new Promise(resolve => {
this.waiting.push(resolve);
});
}
release() {
this.count++;
if (this.waiting.length > 0) {
const resolve = this.waiting.shift();
this.count--;
resolve();
}
}
}
// 后端解析示例 (Node.js + Koa)
const Koa = require('koa');
const multer = require('@koa/multer');
const path = require('path');
const fs = require('fs').promises;
const app = new Koa();
// 配置multer
const storage = multer.diskStorage({
destination: async (req, file, cb) => {
const uploadDir = path.join(__dirname, 'uploads');
await fs.mkdir(uploadDir, { recursive: true });
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage,
limits: {
fileSize: 50 * 1024 * 1024 // 50MB limit
},
fileFilter: (req, file, cb) => {
// 文件类型验证
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'), false);
}
}
});
// 单文件上传路由
app.use(async (ctx, next) => {
if (ctx.path === '/upload' && ctx.method === 'POST') {
try {
await new Promise((resolve, reject) => {
upload.single('file')(ctx.req, ctx.res, (err) => {
if (err) reject(err);
else resolve();
});
});
const file = ctx.req.file;
if (!file) {
ctx.status = 400;
ctx.body = { error: 'No file uploaded' };
return;
}
ctx.body = {
success: true,
file: {
filename: file.filename,
originalname: file.originalname,
size: file.size,
mimetype: file.mimetype,
path: file.path
}
};
} catch (error) {
ctx.status = 400;
ctx.body = { error: error.message };
}
} else {
await next();
}
});
// 分片上传处理
const chunkStorage = new Map(); // 存储chunk信息
app.use(async (ctx, next) => {
if (ctx.path === '/upload/chunk' && ctx.method === 'POST') {
try {
await new Promise((resolve, reject) => {
upload.single('chunk')(ctx.req, ctx.res, (err) => {
if (err) reject(err);
else resolve();
});
});
const { fileId, chunkIndex, totalChunks } = ctx.req.body;
const chunk = ctx.req.file;
if (!chunkStorage.has(fileId)) {
chunkStorage.set(fileId, {
chunks: new Array(parseInt(totalChunks)),
receivedChunks: 0
});
}
const fileInfo = chunkStorage.get(fileId);
fileInfo.chunks[parseInt(chunkIndex)] = chunk;
fileInfo.receivedChunks++;
ctx.body = {
success: true,
chunkIndex: parseInt(chunkIndex),
received: fileInfo.receivedChunks,
total: parseInt(totalChunks)
};
} catch (error) {
ctx.status = 400;
ctx.body = { error: error.message };
}
} else {
await next();
}
});
// 合并chunks
app.use(async (ctx, next) => {
if (ctx.path === '/upload/merge' && ctx.method === 'POST') {
try {
const { fileId, filename } = ctx.request.body;
const fileInfo = chunkStorage.get(fileId);
if (!fileInfo || fileInfo.chunks.some(chunk => !chunk)) {
ctx.status = 400;
ctx.body = { error: 'Missing chunks' };
return;
}
// 合并文件
const outputPath = path.join(__dirname, 'uploads', filename);
const writeStream = require('fs').createWriteStream(outputPath);
for (const chunk of fileInfo.chunks) {
const chunkData = await fs.readFile(chunk.path);
writeStream.write(chunkData);
await fs.unlink(chunk.path); // 删除临时chunk文件
}
writeStream.end();
chunkStorage.delete(fileId);
ctx.body = {
success: true,
filename,
path: outputPath
};
} catch (error) {
ctx.status = 500;
ctx.body = { error: error.message };
}
} else {
await next();
}
});
// 使用示例
const uploader = new FileUploader({
chunkSize: 2 * 1024 * 1024, // 2MB chunks
maxRetries: 3,
concurrency: 3
});
// 单文件上传
document.getElementById('uploadBtn').addEventListener('click', async () => {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('Please select a file');
return;
}
try {
if (file.size > 10 * 1024 * 1024) { // 大于10MB使用分片上传
const result = await uploader.uploadChunks(file, '/upload', {
totalSize: file.size
});
console.log('Chunked upload completed:', result);
} else {
const result = await uploader.uploadWithProgress(file, '/upload', (progress) => {
console.log(`Upload progress: ${progress}%`);
document.getElementById('progress').style.width = `${progress}%`;
});
console.log('Upload completed:', result);
}
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed: ' + error.message);
}
});
面试官视角:
要点清单:
- 理解multipart/form-data编码格式和FormData API
- 掌握文件上传的进度监控和错误处理机制
- 了解大文件分片上传和断点续传的实现原理
加分项:
- 实现并发控制和重试机制
- 结合Web Worker处理文件哈希计算
- 了解安全性考虑(文件类型验证、大小限制、病毒扫描)
常见失误:
- 忽略文件大小限制和浏览器内存占用
- 没有适当的错误处理和用户反馈
- 分片上传时缺乏完整性校验
延伸阅读:
- FormData - MDN — 文件上传API
- File API - MDN — 文件对象接口
- XMLHttpRequest.upload - MDN — 上传进度监控
使用 ajax 封装一个上传文件的函数
答案
核心概念:
AJAX文件上传是一种无需页面刷新即可上传文件的技术,基于XMLHttpRequest API实现。核心要素包括:FormData构建文件数据、upload事件监听进度、readyState状态处理响应、错误处理机制等。现代实现还支持多文件上传、拖拽上传、断点续传等高级功能。
实际示例:
// 通用文件上传类
class AjaxFileUploader {
constructor(options = {}) {
this.defaultOptions = {
method: 'POST',
timeout: 30000,
withCredentials: false,
headers: {},
...options
};
}
// 基础上传方法
upload(file, url, options = {}) {
const config = { ...this.defaultOptions, ...options };
return new Promise((resolve, reject) => {
// 验证文件
if (!this.validateFile(file, config)) {
reject(new Error('File validation failed'));
return;
}
const xhr = new XMLHttpRequest();
const formData = new FormData();
// 构建FormData
this.buildFormData(formData, file, config);
// 设置上传进度监听
if (config.onProgress) {
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
config.onProgress(progress, event.loaded, event.total);
}
});
}
// 设置请求完成监听
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = this.parseResponse(xhr.responseText, xhr.getResponseHeader('content-type'));
resolve({
data: response,
status: xhr.status,
statusText: xhr.statusText,
headers: this.parseHeaders(xhr.getAllResponseHeaders())
});
} catch (error) {
reject(new Error(`Response parsing failed: ${error.message}`));
}
} else {
reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
}
});
// 设置错误监听
xhr.addEventListener('error', () => {
reject(new Error('Network error occurred'));
});
// 设置超时监听
xhr.addEventListener('timeout', () => {
reject(new Error('Upload timeout'));
});
// 设置中止监听
xhr.addEventListener('abort', () => {
reject(new Error('Upload aborted'));
});
// 配置请求
xhr.open(config.method, url, true);
xhr.timeout = config.timeout;
xhr.withCredentials = config.withCredentials;
// 设置请求头
Object.entries(config.headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
// 发送请求
xhr.send(formData);
// 返回可中止的上传对象
return {
xhr,
abort: () => xhr.abort()
};
});
}
// 多文件上传
async uploadMultiple(files, url, options = {}) {
const config = { ...this.defaultOptions, ...options };
const results = [];
const errors = [];
if (config.concurrent) {
// 并发上传
const promises = Array.from(files).map(async (file, index) => {
try {
const result = await this.upload(file, url, {
...config,
onProgress: config.onProgress ?
(progress, loaded, total) => config.onProgress(index, progress, loaded, total) :
undefined
});
results[index] = result;
} catch (error) {
errors[index] = error;
}
});
await Promise.allSettled(promises);
} else {
// 顺序上传
for (let i = 0; i < files.length; i++) {
try {
const result = await this.upload(files[i], url, {
...config,
onProgress: config.onProgress ?
(progress, loaded, total) => config.onProgress(i, progress, loaded, total) :
undefined
});
results[i] = result;
} catch (error) {
errors[i] = error;
if (config.stopOnError) break;
}
}
}
return { results, errors };
}
// 拖拽上传
setupDragDrop(element, url, options = {}) {
const config = { ...this.defaultOptions, ...options };
// 防止默认拖拽行为
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
element.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
});
});
// 处理拖拽视觉反馈
['dragenter', 'dragover'].forEach(eventName => {
element.addEventListener(eventName, () => {
element.classList.add('drag-over');
});
});
['dragleave', 'drop'].forEach(eventName => {
element.addEventListener(eventName, () => {
element.classList.remove('drag-over');
});
});
// 处理文件拖拽
element.addEventListener('drop', async (e) => {
const files = Array.from(e.dataTransfer.files);
if (files.length === 0) return;
try {
if (files.length === 1) {
await this.upload(files[0], url, config);
} else {
await this.uploadMultiple(files, url, config);
}
if (config.onSuccess) {
config.onSuccess(files);
}
} catch (error) {
if (config.onError) {
config.onError(error);
}
}
});
}
// 文件验证
validateFile(file, config) {
// 大小验证
if (config.maxSize && file.size > config.maxSize) {
throw new Error(`File size exceeds limit: ${file.size} > ${config.maxSize}`);
}
// 类型验证
if (config.allowedTypes && !config.allowedTypes.includes(file.type)) {
throw new Error(`File type not allowed: ${file.type}`);
}
// 扩展名验证
if (config.allowedExtensions) {
const extension = file.name.split('.').pop().toLowerCase();
if (!config.allowedExtensions.includes(extension)) {
throw new Error(`File extension not allowed: ${extension}`);
}
}
return true;
}
// 构建FormData
buildFormData(formData, file, config) {
// 添加文件
const fieldName = config.fieldName || 'file';
formData.append(fieldName, file);
// 添加额外字段
if (config.extraFields) {
Object.entries(config.extraFields).forEach(([key, value]) => {
formData.append(key, value);
});
}
// 添加文件元信息
if (config.includeMetadata) {
formData.append('fileName', file.name);
formData.append('fileSize', file.size);
formData.append('fileType', file.type);
formData.append('lastModified', file.lastModified);
}
}
// 解析响应
parseResponse(responseText, contentType) {
if (!responseText) return null;
if (contentType && contentType.includes('application/json')) {
return JSON.parse(responseText);
}
return responseText;
}
// 解析响应头
parseHeaders(headerString) {
const headers = {};
if (!headerString) return headers;
headerString.split('\r\n').forEach(line => {
const [key, value] = line.split(': ');
if (key && value) {
headers[key.toLowerCase()] = value;
}
});
return headers;
}
}
// 使用示例
const uploader = new AjaxFileUploader({
timeout: 60000,
maxSize: 10 * 1024 * 1024, // 10MB
allowedTypes: ['image/jpeg', 'image/png', 'image/gif'],
allowedExtensions: ['jpg', 'jpeg', 'png', 'gif'],
includeMetadata: true,
extraFields: {
category: 'avatar',
userId: '12345'
}
});
// 基础上传
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await uploader.upload(file, '/api/upload', {
onProgress: (progress, loaded, total) => {
console.log(`Upload progress: ${progress}%`);
document.getElementById('progressBar').style.width = `${progress}%`;
document.getElementById('progressText').textContent =
`${(loaded / 1024 / 1024).toFixed(2)}MB / ${(total / 1024 / 1024).toFixed(2)}MB`;
}
});
console.log('Upload successful:', result);
alert('File uploaded successfully!');
} catch (error) {
console.error('Upload failed:', error);
alert(`Upload failed: ${error.message}`);
}
});
// 多文件上传
document.getElementById('multiFileInput').addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
if (files.length === 0) return;
try {
const { results, errors } = await uploader.uploadMultiple(files, '/api/upload', {
concurrent: true,
stopOnError: false,
onProgress: (fileIndex, progress, loaded, total) => {
console.log(`File ${fileIndex + 1} progress: ${progress}%`);
document.getElementById(`progress-${fileIndex}`).style.width = `${progress}%`;
}
});
console.log('Upload completed:', { results, errors });
const successCount = results.filter(r => r).length;
const errorCount = errors.filter(e => e).length;
alert(`Upload completed: ${successCount} successful, ${errorCount} failed`);
} catch (error) {
console.error('Upload error:', error);
}
});
// 拖拽上传
const dropZone = document.getElementById('dropZone');
uploader.setupDragDrop(dropZone, '/api/upload', {
onProgress: (progress) => {
console.log(`Drag upload progress: ${progress}%`);
},
onSuccess: (files) => {
console.log('Drag upload successful:', files);
},
onError: (error) => {
console.error('Drag upload failed:', error);
}
});
// 简化版本(向后兼容)
function uploadFile(file, url, progressCallback, successCallback, errorCallback) {
const uploader = new AjaxFileUploader();
uploader.upload(file, url, {
onProgress: progressCallback ?
(progress) => progressCallback(progress) :
undefined
})
.then(result => {
if (successCallback) successCallback(result.data);
})
.catch(error => {
if (errorCallback) errorCallback(error);
});
}
面试官视角:
要点清单:
- 理解XMLHttpRequest的文件上传机制和FormData构建
- 掌握上传进度监控和状态处理
- 了解文件验证、错误处理和用户体验优化
加分项:
- 实现多文件上传和并发控制
- 支持拖拽上传和视觉反馈
- 提供完整的错误处理和恢复机制
常见失误:
- 忘记处理网络错误和超时情况
- 没有适当的文件大小和类型验证
- 缺乏用户进度反馈和操作控制
延伸阅读:
- XMLHttpRequest - MDN — 核心上传API
- FormData - MDN — 文件数据构建
- Drag and Drop API - MDN — 拖拽上传实现
如何实现网页加载进度条?
答案
核心概念:
网页加载进度条是提升用户体验的重要组件,用于显示页面资源加载状态。实现方式包括:基于DOM readyState监听、Performance API资源追踪、自定义事件触发、第三方库集成等。关键在于准确计算加载进度、提供视觉反馈、处理异步加载场景。
实际示例:
可以通过 window.performance
对象来监听页面资源加载进度。该对象提供了各种方法来获取资源加载的详细信息。
可以使用 performance.getEntries()
方法获取页面上所有的资源加载信息。可以使用该方法来监测每个资源的加载状态,计算加载时间,并据此来实现一个资源加载进度条。
下面是一个简单的实现方式:
const resources = window.performance.getEntriesByType('resource')
const totalResources = resources.length
let loadedResources = 0
resources.forEach((resource) => {
if (resource.initiatorType !== 'xmlhttprequest') {
// 排除 AJAX 请求
resource.onload = () => {
loadedResources++
const progress = Math.round((loadedResources / totalResources) * 100)
updateProgress(progress)
}
}
})
function updateProgress (progress) {
// 更新进度条
}
该代码会遍历所有资源,并注册一个 onload
事件处理函数。当每个资源加载完成后,会更新 loadedResources
变量,并计算当前的进度百分比,然后调用 updateProgress()
函数来更新进度条。需要注意的是,这里排除了 AJAX 请求,因为它们不属于页面资源。
当所有资源加载完成后,页面就会完全加载。
实现进度条
网页加载进度条可以通过前端技术实现,一般的实现思路是通过监听浏览器的页面加载事件和资源加载事件,来实时更新进度条的状态。下面介绍两种实现方式。
- 使用原生进度条
在 HTML5 中提供了 progress
元素,可以通过它来实现一个原生的进度条。
<progress id="progressBar" value="0" max="100"></progress>
然后在 JavaScript 中,监听页面加载事件和资源加载事件,实时更新 progress
元素的 value
属性。
const progressBar = document.getElementById('progressBar')
window.addEventListener('load', () => {
progressBar.value = 100
})
document.addEventListener('readystatechange', () => {
const progress = Math.floor((document.readyState / 4) * 100)
progressBar.value = progress
})
- 使用第三方库
使用第三方库可以更加方便地实现网页加载进度条,下面以 nprogress
库为例:
- 安装
nprogress
库
bashCopy codenpm install nprogress --save
- 在页面中引入
nprogress.css
和nprogress.js
<link rel="stylesheet" href="/node_modules/nprogress/nprogress.css">
<script src="/node_modules/nprogress/nprogress.js"></script>
- 在 JavaScript 中初始化
nprogress
并监听页面加载事件和资源加载事件
// 初始化 nprogress
NProgress.configure({ showSpinner: false })
// 监听页面加载事件
window.addEventListener('load', () => {
NProgress.done()
})
// 监听资源加载事件
document.addEventListener('readystatechange', () => {
if (document.readyState === 'interactive') {
NProgress.start()
} else if (document.readyState === 'complete') {
NProgress.done()
}
})
使用 nprogress
可以自定义进度条的样式,同时也提供了更多的 API 供我们使用,比如说手动控制进度条的显示和隐藏,以及支持 Promise 和 Ajax 请求的进度条等等。
面试官视角:
要点清单:
- 理解DOM加载状态和Performance API的使用
- 掌握资源加载监听和进度计算方法
- 了解用户体验设计和视觉反馈机制
加分项:
- 实现AJAX请求拦截和追踪
- 支持自定义样式和配置选项
- 提供框架集成和组件化封装
常见失误:
- 过度依赖不准确的加载指标
- 忽略异步资源和动态内容加载
- 缺乏适当的性能优化和内存清理
延伸阅读:
- Performance API - MDN — 性能监控API
- Document.readyState - MDN — 文档加载状态
- PerformanceObserver - MDN — 性能监听器
如何实现大文件断点续传
答案
核心概念:
大文件断点续传是一种可靠的文件传输技术,通过将大文件分割成小块(chunks)进行上传,支持网络中断后从断点处继续传输。核心要素包括:文件分片、进度存储、断点检测、续传恢复、完整性校验等。实现需要前后端协同,确保传输可靠性和用户体验。
实际示例:
前端实现断点续传一般涉及到以下几个步骤:
-
分片上传:将大文件分割成多个小的文件块。可以使用 JavaScript 的
File
对象的slice
方法来实现分片。 -
上传文件块:使用 XMLHttpRequest 或 Fetch API 发送每个文件块到服务器。可以将每个文件块的索引、总文件大小等信息一同发送到服务器。
-
保存上传进度:在每个文件块上传成功后,可以将已上传的块数、已上传的字节数等信息保存到本地,以便在继续上传时恢复进度。
-
续传:在继续上传时,先从本地恢复已上传的进度信息。然后根据已上传的字节数,计算出下一个文件块的起始位置,然后继续上传剩余的文件块。
-
合并文件块:在所有文件块都上传完成后,服务器可以将这些文件块合并为完整的文件。可以通过将所有文件块的内容拼接在一起或使用服务器端的工具进行合并。
需要注意的是,断点续传的实现还需要服务器端的支持。服务器端需要接收和处理分片上传的请求,并保存和管理已上传的文件块,以便在续传时恢复文件的完整性。因此,前端实现断点续传需要和后端进行协作。
大文件切片上传时,切片数量取决于几个关键因素:文件总大小、每个切片的大小(即切片大小),以及任何特定于应用或服务的限制。计算切片数量的过程包括确定合理的切片大小,然后根据文件总大小来计算需要多少个这样大小的切片。以下是一些步骤和考虑因素,可以帮助你确定切片数量:
- 确定切片大小
- 切片大小:首先,需要确定每个切片的大小。这通常是一个权衡的结果,考虑到效率、可靠性和服务器限制。太小的切片会增加请求的数量,降低效率;而太大的切片可能会增加单个请求失败的风险,并且对于每次请求消耗更多的内存和带宽。
- 通常,切片大小选取在
1MB
至10MB
之间比较合适,当然这取决于具体应用和网络环境。
- 计算切片数量
- 文件总大小:知道文件的总大小后,可以通过简单的数学计算来决定切片的数量。公式如下:
切片数量 = 向上取整(文件总大小 / 每个切片的大小)
- 例如,如果文件是
50MB
,每个切片大小为5MB
,则切片数量为10
。
- 考虑特殊情况
- 最后一个切片可能会小于你设定的标准切片大小,这是正常情况,需要在上传逻辑中进行处理。
- 示例代码
function calculateChunks (fileSize, chunkSize) {
// 文件总大小(byte),切片大小(byte)
const chunksCount = Math.ceil(fileSize / chunkSize)
return chunksCount
}
// 示例:文件大小 52MB,切片大小 5MB
const fileSize = 5210241024 // 52MB
const chunkSize = 510241024 // 5MB
const chunksCount = calculateChunks(fileSize, chunkSize)
console.log(`需要切片数量: ${chunksCount}`)
注意事项
- 网络条件:切片大小可能需要根据网络环境调整。在网络条件较差的情况下,选择更小的切片大小可能更加可靠。
- 服务器限制:某些服务器或云服务可能对上传文件的大小有限制。确保了解和遵守这些限制,以避免上传失败。
- 并发上传:在选择切片大小和数量时,考虑是否会并行上传多个切片,因为这也会影响上传速度和效率。
通过以上步骤和考虑因素,你可以合理地决定大文件上传时的切片数量,以优化上传过程的效率和可靠性。
面试官视角:
要点清单:
- 理解文件分片和断点续传的基本原理
- 掌握进度存储和恢复机制
- 了解并发控制和错误重试策略
加分项:
- 实现文件完整性校验(哈希验证)
- 支持上传暂停、恢复和取消功能
- 提供用户友好的进度反馈和状态管理
常见失误:
- 忽略网络异常和服务器错误处理
- 缺乏适当的并发控制导致性能问题
- 没有实现有效的进度持久化机制
延伸阅读:
- File API - MDN — 文件分片基础
- Web Crypto API - MDN — 文件哈希计算
- LocalStorage - MDN — 进度存储
前端需要加载一个大体积的文件时, 一般有哪些优化思路
答案
核心概念:
大体积文件加载优化是前端性能优化的重要方面,涉及文件压缩、分片加载、懒加载、缓存策略、CDN加速等技术。目标是减少初始加载时间、提升用户体验、优化网络资源利用。需要综合考虑文件类型、用户网络条件、设备性能等因素。
实际示例:
分片上传是一种将大文件分割成多个小片段进行上传的方法,在分片上传过程中校验文件完整性非常重要,可以确保上传的文件在服务器端能够正确地组合成完整的文件。以下是一些校验文件完整性的思路:
一、使用哈希算法
- 计算文件哈希值:
- 在客户端上传文件之前,先对整个文件计算哈希值。常用的哈希算法有 MD5、SHA-1、SHA-256 等。
- 例如,使用 JavaScript 的
crypto-js
库计算文件的 MD5 哈希值:
import CryptoJS from 'crypto-js'
const calculateFileHash = async (file) => {
const fileReader = new FileReader()
return new Promise((resolve, reject) => {
fileReader.onload = (event) => {
const hash = CryptoJS.MD5(event.target.result)
resolve(hash.toString())
}
fileReader.onerror = reject
fileReader.readAsArrayBuffer(file)
})
}
- 上传过程中携带哈希值:
- 在进行分片上传时,将文件的哈希值作为一个参数一起上传给服务器。
- 可以在每个分片的请求中携带哈希值,或者在上传开始时先将哈希值发送给服务器。
- 服务器端校验:
- 服务器在接收到所有分片并组合成完整文件后,再次计算文件的哈希值,并与客户端上传的哈希值进行比较。
- 如果两个哈希值一致,则说明文件完整无误;如果不一致,则说明文件在上传过程中可能出现了问题。
二、校验和(Checksum)
- 计算校验和:
- 除了哈希算法,还可以使用校验和来校验文件完整性。校验和是通过对文件的每个字节进行特定的数学运算得到的一个值。
- 例如,可以使用简单的累加校验和算法,将文件的每个字节的值相加得到一个总和作为校验和。
- 上传和校验:
- 在客户端计算文件的校验和,并在分片上传时将校验和发送给服务器。
- 服务器在组合完文件后,计算文件的校验和并与客户端上传的校验和进行比较,以确定文件的完整性。
三、文件大小比较
- 记录文件大小:
- 在客户端上传文件之前,记录文件的大小。可以通过
File
对象的size
属性获取文件的大小。
- 服务器端验证:
- 服务器在接收到所有分片并组合成完整文件后,检查文件的大小是否与客户端上传的文件大小一致。
- 如果大小一致,则说明文件可能是完整的;如果不一致,则说明文件在上传过程中出现了问题。
四、上传状态跟踪
- 客户端跟踪上传状态:
- 在客户端,可以使用一个数据结构来跟踪每个分片的上传状态,例如使用一个数组记录每个分片是否成功上传。
- 当所有分片都成功上传后,可以认为文件上传完整。
- 服务器端确认:
- 服务器在接收到每个分片时,可以回复一个确认消息给客户端。客户端根据服务器的确认消息来更新上传状态。
- 当客户端收到服务器对所有分片的确认后,可以确定文件上传完整。
当前端需要加载大体积文件时,可以从以下几个方面进行优化:
一、文件压缩
- 服务器端压缩:
- 在服务器上配置文件压缩功能,如使用 Gzip 或 Brotli 压缩算法对文件进行压缩后再传输。这样可以显著减少文件的大小,降低传输时间。
- 例如,在 Nginx 服务器中,可以通过配置开启 Gzip 压缩:
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css application/JavaScript application/json image/svg+xml;
- 客户端解压缩:
- 现代浏览器通常支持对 Gzip 和 Brotli 压缩的文件进行自动解压缩。当浏览器接收到压缩后的文件时,会自动解压缩并使用。
- 无需额外的客户端代码,浏览器会自动处理压缩文件的解压缩过程,提高文件加载速度。
二、文件分割与懒加载
- 文件分割:
- 将大体积文件分割成多个较小的文件。例如,对于一个大型的 JavaScript 库,可以将其拆分成多个模块,根据需要逐步加载。
- 这样可以避免一次性加载整个大文件,减少初始加载时间。
- 例如,使用 Webpack 等构建工具可以将代码分割成多个 chunk,根据路由或特定条件进行加载。
- 懒加载:
- 对于不是立即需要的文件或资源,可以采用懒加载的方式。当用户实际需要使用该资源时,再进行加载。
- 例如,对于图片、视频等资源,可以在用户滚动到可视区域时再进行加载,避免在页面初始加载时加载所有资源。
- 对于 JavaScript 模块,可以使用动态导入(dynamic import)的方式实现懒加载:
const loadModule = async () => {
const module = await import('./largeModule.js')
// 使用加载的模块
}
三、缓存策略
- 浏览器缓存:
- 设置合理的缓存策略,让浏览器缓存已经加载过的文件。这样,当用户再次访问时,可以直接从缓存中读取文件,而无需再次从服务器下载。
- 可以通过设置 HTTP 响应头来控制缓存,例如:
location / {
add_header Cache-Control "max-age=3600";
}
- 上述配置将设置文件的缓存时间为 1 小时。
- 缓存更新机制:
- 当文件内容发生变化时,需要确保浏览器能够获取到最新的版本。可以通过在文件名中添加版本号或哈希值来实现缓存更新。
- 例如,将文件名改为
largeFile_v1.2.js
或largeFile_abc123.js
,当文件内容变化时,更新版本号或哈希值,浏览器会认为这是一个新的文件并进行下载。
四、优化加载顺序
- 关键资源优先加载:
- 确定哪些资源是页面加载的关键资源,优先加载这些资源。对于大体积文件,如果不是关键资源,可以延迟加载。
- 例如,对于一个图片库应用,先加载页面的基本结构和导航部分,图片可以在用户交互时再进行加载。
- 异步加载:
- 使用异步加载的方式加载大体积文件。例如,对于 JavaScript 文件,可以使用
<script async>
标签或动态创建<script>
标签并插入到页面中进行异步加载。
<script async src="largeScript.js"></script>
- 这样可以避免阻塞页面的渲染,提高用户体验。
五、CDN 加速
- 使用内容分发网络(CDN):
- 将大体积文件托管在 CDN 上,利用 CDN 的分布式节点,可以让用户从离自己最近的节点获取文件,减少网络延迟,提高加载速度。
- 例如,将图片、视频、静态文件等托管在 CDN 上,通过 CDN 的 URL 进行访问。
- CDN 缓存:
- CDN 通常会对文件进行缓存,进一步提高文件的加载速度。当文件内容发生变化时,需要及时更新 CDN 上的缓存。
- 可以通过设置 CDN 的缓存策略或使用版本号等方式来管理 CDN 缓存。
面试官视角:
要点清单:
- 理解不同优化策略的适用场景和实现方式
- 掌握文件压缩、分片和缓存的基本原理
- 了解CDN和懒加载对用户体验的影响
加分项:
- 实现完整性校验和错误恢复机制
- 结合具体业务场景选择最优方案
- 提供性能监控和优化效果评估
常见失误:
- 过度优化导致复杂性增加
- 忽略不同网络环境下的用户体验
- 缺乏对文件完整性和安全性的考虑
延伸阅读:
- Web Performance - MDN — Web性能优化指南
- HTTP Caching - MDN — HTTP缓存机制
- Compression - MDN — Web内容压缩